diff --git a/.jules/bolt.md b/.jules/bolt.md new file mode 100644 index 0000000..f351eef --- /dev/null +++ b/.jules/bolt.md @@ -0,0 +1,3 @@ +## 2024-05-24 - Pre-calculate search index and format dates +**Learning:** Pre-calculating derived properties (search string concatenations, boolean date thresholds, formatted date strings) directly into in-memory collection objects during initial data load drastically speeds up rendering cycles in vanilla JavaScript list components. Instantiating `new Date()` within a high-frequency `render()` loop is a noticeable performance bottleneck. +**Action:** When filtering or rendering lists containing dates or complex search fields, process them once upstream (before or right after storing locally), cache the derived strings/booleans on the object itself, and read those pre-computed values dynamically downstream in the filter and render functions. diff --git a/screenshot.png b/screenshot.png new file mode 100644 index 0000000..818c05c Binary files /dev/null and b/screenshot.png differ diff --git a/script.js b/script.js index 22f399d..d234c3b 100644 --- a/script.js +++ b/script.js @@ -416,6 +416,37 @@ async function syncClassSwitcher() { renderSemesterTabs(); } +function prepareSearchIndex() { + const now = new Date(); + const SEVEN_DAYS = 7 * 24 * 60 * 60 * 1000; + + pdfDatabase.forEach(pdf => { + // Pre-calculate search string + const t = pdf.title ? pdf.title.toLowerCase() : ''; + const d = pdf.description ? pdf.description.toLowerCase() : ''; + const c = pdf.category ? pdf.category.toLowerCase() : ''; + const a = pdf.author ? pdf.author.toLowerCase() : ''; + pdf._searchStr = `${t} ${d} ${c} ${a}`; + + // Pre-calculate date info + let uploadDateObj; + if (pdf.uploadDate && typeof pdf.uploadDate.toDate === 'function') { + uploadDateObj = pdf.uploadDate.toDate(); + } else if (pdf.uploadDate) { + uploadDateObj = new Date(pdf.uploadDate); + } else { + uploadDateObj = new Date(); + } + + const timeDiff = now - uploadDateObj; + pdf._isNew = timeDiff < SEVEN_DAYS; + + pdf._formattedDate = uploadDateObj.toLocaleDateString('en-US', { + year: 'numeric', month: 'short', day: 'numeric' + }); + }); +} + async function loadPDFDatabase() { if (isMaintenanceActive) return; @@ -454,6 +485,7 @@ async function loadPDFDatabase() { if (shouldUseCache) { pdfDatabase = cachedData; + prepareSearchIndex(); // --- FIX: CALL THIS TO POPULATE UI --- syncClassSwitcher(); renderSemesterTabs(); @@ -477,6 +509,7 @@ async function loadPDFDatabase() { data: pdfDatabase })); + prepareSearchIndex(); // --- FIX: CALL THIS TO POPULATE UI --- syncClassSwitcher(); renderPDFs(); @@ -905,26 +938,23 @@ function renderPDFs() { // Locate renderPDFs() in script.js and update the filter section const filteredPdfs = pdfDatabase.filter(pdf => { - const matchesSemester = pdf.semester === currentSemester; + if (pdf.semester !== currentSemester) 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; + 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); + if (searchTerm) { + if (!pdf._searchStr || !pdf._searchStr.includes(searchTerm)) return false; + } - // Update return statement to include matchesClass - return matchesSemester && matchesClass && matchesCategory && matchesSearch; + return true; }); updatePDFCount(filteredPdfs.length); @@ -994,9 +1024,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 isNew = pdf._isNew || false; const newBadgeHTML = isNew ? `NEW` @@ -1011,9 +1039,7 @@ 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' - }); + const formattedDate = pdf._formattedDate || ''; // Uses global escapeHtml() now diff --git a/verify.py b/verify.py new file mode 100644 index 0000000..46b0eba --- /dev/null +++ b/verify.py @@ -0,0 +1,102 @@ +import asyncio +from playwright.async_api import async_playwright +import time + +async def verify(): + async with async_playwright() as p: + browser = await p.chromium.launch(headless=True) + page = await browser.new_page() + + # Block firebase/google to prevent hanging in playwright and overwriting our mock DB + await page.route("**/*firebase*", lambda route: route.abort()) + await page.route("**/*google*", lambda route: route.abort()) + + # Seed local storage with mock data so it renders immediately + await page.goto('http://127.0.0.1:8000', wait_until="commit") + + await page.evaluate('''() => { + const timestamp = new Date().getTime(); + const pastDate = new Date(timestamp - 1000 * 60 * 60 * 24 * 2).toISOString(); // 2 days old (new) + const oldDate = new Date(timestamp - 1000 * 60 * 60 * 24 * 10).toISOString(); // 10 days old (not new) + + const mockData = [ + { id: '1', title: 'Organic Chemistry Basics', description: 'Intro to alkanes', category: 'Organic', author: 'Dr. Smith', uploadDate: pastDate, semester: 1, class: 'MSc Chemistry' }, + { id: '2', title: 'Quantum Mechanics', description: 'Schrodinger equation', category: 'Physical', author: 'Dr. Jones', uploadDate: oldDate, semester: 1, class: 'MSc Chemistry' } + ]; + + localStorage.setItem('classnotes_db_cache', JSON.stringify({ timestamp, data: mockData })); + localStorage.setItem('currentClass', 'MSc Chemistry'); + localStorage.setItem('currentSemester', '1'); + + // Mock firestore globally before app init + window.firebase = { + apps: [{ name: '[DEFAULT]' }], + initializeApp: () => {}, + firestore: () => ({ + collection: (col) => ({ + doc: (id) => ({ + onSnapshot: (cb) => { cb({ exists: false, data: () => ({}) }); return () => {}; }, + set: async () => {}, + collection: () => ({ add: async () => {}, doc: () => ({ set: async () => {} }) }) + }), + orderBy: () => ({ + limit: () => ({ + get: async () => ({ empty: true }) + }), + get: async () => ({ + forEach: (cb) => { + const data = JSON.parse(localStorage.getItem('classnotes_db_cache') || "{}").data || []; + data.forEach(d => cb({ id: d.id, data: () => d })); + } + }) + }) + }) + }), + auth: () => ({ + onAuthStateChanged: (cb) => { cb(null); }, + signInAnonymously: async () => ({ user: { uid: 'mock_uid' } }) + }) + }; + window.firebase.firestore.FieldValue = { + serverTimestamp: () => new Date(), + increment: () => 1 + }; + }''') + + # Reload page with seeded data and mock + await page.goto('http://127.0.0.1:8000', wait_until="domcontentloaded") + + # Force script initialization if needed (mock data) + await page.evaluate('''() => { + if (typeof loadPDFDatabase === 'function') { + pdfDatabase = JSON.parse(localStorage.getItem('classnotes_db_cache') || "{}").data || []; + prepareSearchIndex(); + renderPDFs(); + } + }''') + + # Hide UI blockers + await page.evaluate("() => document.getElementById('preloader')?.classList.add('hidden')") + await page.evaluate("() => document.getElementById('holidayOverlay')?.classList.add('hidden')") + await page.evaluate("() => document.getElementById('contentWrapper')?.classList.add('active')") + + # Wait for the grid to render + await page.wait_for_selector('.pdf-card', timeout=5000) + + # Get count + cards = await page.locator('.pdf-card').count() + print(f"Rendered {cards} cards") + + # Take screenshot + await page.screenshot(path="screenshot.png") + print("Screenshot saved to screenshot.png") + + # Search test + await page.fill('#searchInput', 'Organic') + await page.wait_for_timeout(1000) # wait for debounce & render + organic_cards = await page.locator('.pdf-card').count() + print(f"Rendered {organic_cards} cards after searching 'Organic'") + + await browser.close() + +asyncio.run(verify())