Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## 2024-05-24 - Pre-calculating search strings and dates for render loops
**Learning:** Repeated Date formatting, object instantiations, and string lowercasing operations inside the `renderPDFs` loop and its filter functions cause significant CPU overhead and UI lag during user search typing, as these operations run for every item on every keystroke/render cycle.
**Action:** Implement `prepareSearchIndex` to pre-calculate `_searchStr` (a lowercased concatenation of searchable fields), `_formattedDate`, and `_isNew` once when the PDF database is loaded. Use these cached properties in `renderPDFs` and `createPDFCard` along with early returns in the filter callback to speed up filtering and rendering.
77 changes: 54 additions & 23 deletions script.js
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,33 @@ function getAdData(slotName) {
/* =========================================
5. DATA LOADING WITH CACHING
========================================= */

// OPTIMIZATION: Pre-calculate expensive values for search and rendering
function prepareSearchIndex(pdfs) {
const now = new Date();
const sevenDaysMs = 7 * 24 * 60 * 60 * 1000;

pdfs.forEach(pdf => {
// Handle both raw Date strings and Firestore Timestamps
const uploadDateObj = pdf.uploadDate && typeof pdf.uploadDate.toDate === 'function'
? pdf.uploadDate.toDate()
: new Date(pdf.uploadDate);

// Pre-calculate search string
pdf._searchStr = `${pdf.title} ${pdf.description} ${pdf.category} ${pdf.author || ''}`.toLowerCase();

// Pre-calculate formatted date
pdf._formattedDate = uploadDateObj.toLocaleDateString('en-US', {
year: 'numeric', month: 'short', day: 'numeric'
});

// Pre-calculate isNew boolean
const timeDiff = now - uploadDateObj;
pdf._isNew = timeDiff < sevenDaysMs;
});
return pdfs;
}

function renderSemesterTabs() {
const container = document.getElementById('semesterTabsContainer');
if (!container) return;
Expand Down Expand Up @@ -451,6 +478,10 @@ async function loadPDFDatabase() {

if (shouldUseCache) {
pdfDatabase = cachedData;
// The cached data already went through prepareSearchIndex when it was saved,
// but we need to re-run it in case `now` has changed to update `_isNew`.
prepareSearchIndex(pdfDatabase);

// --- FIX: CALL THIS TO POPULATE UI ---
syncClassSwitcher();
renderSemesterTabs();
Expand All @@ -469,13 +500,20 @@ async function loadPDFDatabase() {
pdfDatabase.push({ id: doc.id, ...doc.data() });
});

// Pre-calculate expensive properties before saving to cache
prepareSearchIndex(pdfDatabase);

localStorage.setItem(CACHE_KEY, JSON.stringify({
timestamp: new Date().getTime(),
data: pdfDatabase
}));

// --- FIX: CALL THIS TO POPULATE UI ---
syncClassSwitcher();
// Missing render calls for fresh fetch (similar to cache hit):
renderSemesterTabs();
renderCategoryFilters();

renderPDFs();
hidePreloader();

Expand Down Expand Up @@ -902,26 +940,25 @@ function renderPDFs() {

// Locate renderPDFs() in script.js and update the filter section
const filteredPdfs = pdfDatabase.filter(pdf => {
const matchesSemester = pdf.semester === currentSemester;
// OPTIMIZATION: Early returns for cheap equality checks first

// 1. Check Class
if (pdf.class !== currentClass) return false;

// NEW: 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;
// 2. Check Semester
if (pdf.semester !== currentSemester) return false;

let matchesCategory = false;
// 3. Check Category / Favorites
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);
// 4. Check Search (using pre-calculated lowercased string)
if (searchTerm && !pdf._searchStr.includes(searchTerm)) return false;

// Update return statement to include matchesClass
return matchesSemester && matchesClass && matchesCategory && matchesSearch;
return true;
});

updatePDFCount(filteredPdfs.length);
Expand Down Expand Up @@ -991,11 +1028,7 @@ 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

const newBadgeHTML = isNew
const newBadgeHTML = pdf._isNew
? `<span style="background:var(--error-color); color:white; font-size:0.6rem; padding:2px 6px; border-radius:4px; margin-left:8px; vertical-align:middle;">NEW</span>`
: '';

Expand All @@ -1007,10 +1040,8 @@ 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'
});
// OPTIMIZATION: Use pre-calculated date string
const formattedDate = pdf._formattedDate;

// Uses global escapeHtml() now

Expand Down