diff --git a/.jules/bolt.md b/.jules/bolt.md new file mode 100644 index 0000000..ef38631 --- /dev/null +++ b/.jules/bolt.md @@ -0,0 +1,4 @@ + +## 2025-02-18 - [Optimizing List Filtering] +**Learning:** Pre-calculating derived search strings and dates immediately after data load and explicitly indexing it dramatically improves performance during list filtering compared to recalculating on the fly. Doing inline calculations or object cloning on large array iterations is costly. +**Action:** When filtering or looping over large datasets frequently, proactively calculate common string concatenation or boolean derivation into the object beforehand. diff --git a/script.js b/script.js index ef463ba..b8dab56 100644 --- a/script.js +++ b/script.js @@ -374,6 +374,30 @@ function getAdData(slotName) { /* ========================================= 5. DATA LOADING WITH CACHING ========================================= */ + +// ⚡ Bolt: Pre-calculate derived fields to avoid recalculating in high-frequency loops (render/filter) +function prepareSearchIndex(data) { + if (!data || !Array.isArray(data)) return; + const now = new Date(); + const SEVEN_DAYS = 7 * 24 * 60 * 60 * 1000; + const dateFormatter = new Intl.DateTimeFormat('en-US', { + year: 'numeric', month: 'short', day: 'numeric' + }); + + for (let i = 0; i < data.length; i++) { + const pdf = data[i]; + + // Pre-calculate lowercased search string + pdf._searchStr = ((pdf.title || '') + ' ' + (pdf.description || '') + ' ' + (pdf.category || '') + ' ' + (pdf.author || '')).toLowerCase(); + + // Pre-calculate date fields + const uploadDateObj = new Date(pdf.uploadDate); + if (!isNaN(uploadDateObj)) { + pdf._isNew = (now - uploadDateObj) < SEVEN_DAYS; + pdf._formattedDate = dateFormatter.format(uploadDateObj); + } + } +} function renderSemesterTabs() { const container = document.getElementById('semesterTabsContainer'); if (!container) return; @@ -490,6 +514,7 @@ async function loadPDFDatabase() { if (shouldUseCache) { pdfDatabase = cachedData; + prepareSearchIndex(pdfDatabase); // ⚡ Bolt: Index cached data before rendering // --- FIX: CALL THIS TO POPULATE UI --- syncClassSwitcher(); renderSemesterTabs(); @@ -513,6 +538,8 @@ async function loadPDFDatabase() { data: pdfDatabase })); + prepareSearchIndex(pdfDatabase); // ⚡ Bolt: Index fresh data before rendering + // --- FIX: CALL THIS TO POPULATE UI --- syncClassSwitcher(); renderPDFs(); @@ -948,26 +975,22 @@ function renderPDFs() { // Locate renderPDFs() in script.js and update the filter section const filteredPdfs = pdfDatabase.filter(pdf => { - const matchesSemester = pdf.semester === currentSemester; + // ⚡ Bolt: Fast explicit early returns + if (pdf.semester !== currentSemester) return false; - // NEW: Check if the PDF class matches the UI's current class selection + // Check if the PDF class matches the UI's current class selection // Note: If old documents don't have this field, they will be hidden. - const matchesClass = pdf.class === currentClass; + if (pdf.class !== currentClass) return false; - let matchesCategory = false; if (currentCategory === 'favorites') { - matchesCategory = favorites.includes(pdf.id); - } else { - matchesCategory = currentCategory === 'all' || pdf.category === currentCategory; + if (!favorites.includes(pdf.id)) return false; + } else if (currentCategory !== 'all') { + if (pdf.category !== currentCategory) return false; } - const matchesSearch = pdf.title.toLowerCase().includes(searchTerm) || - pdf.description.toLowerCase().includes(searchTerm) || - pdf.category.toLowerCase().includes(searchTerm) || - pdf.author.toLowerCase().includes(searchTerm); - - // Update return statement to include matchesClass - return matchesSemester && matchesClass && matchesCategory && matchesSearch; + // ⚡ Bolt: Use pre-calculated search string, guarding against unindexed/malformed items + if (!pdf._searchStr) return false; + return pdf._searchStr.includes(searchTerm); }); updatePDFCount(filteredPdfs.length); @@ -1037,9 +1060,23 @@ function createPDFCard(pdf, favoritesList, index = 0, highlightRegex = null) { const heartIconClass = isFav ? 'fas' : 'far'; const btnActiveClass = isFav ? 'active' : ''; - const uploadDateObj = new Date(pdf.uploadDate); - const timeDiff = new Date() - uploadDateObj; - const isNew = timeDiff < (7 * 24 * 60 * 60 * 1000); // 7 days + // ⚡ Bolt: Use pre-calculated fields with fallback for legacy cold caches + let isNew = pdf._isNew; + let formattedDate = pdf._formattedDate; + + if (isNew === undefined || !formattedDate) { + const uploadDateObj = new Date(pdf.uploadDate); + if (!isNaN(uploadDateObj)) { + const timeDiff = new Date() - uploadDateObj; + isNew = timeDiff < (7 * 24 * 60 * 60 * 1000); // 7 days + formattedDate = uploadDateObj.toLocaleDateString('en-US', { + year: 'numeric', month: 'short', day: 'numeric' + }); + } else { + isNew = false; + formattedDate = 'Unknown Date'; + } + } const newBadgeHTML = isNew ? `NEW` @@ -1053,11 +1090,6 @@ function createPDFCard(pdf, favoritesList, index = 0, highlightRegex = null) { }; const categoryIcon = categoryIcons[pdf.category] || 'fa-file-pdf'; - // Formatting Date - const formattedDate = new Date(pdf.uploadDate).toLocaleDateString('en-US', { - year: 'numeric', month: 'short', day: 'numeric' - }); - // Uses global escapeHtml() now const highlightText = (text) => {