diff --git a/script.js b/script.js index 22f399d..bc16031 100644 --- a/script.js +++ b/script.js @@ -416,6 +416,37 @@ async function syncClassSwitcher() { renderSemesterTabs(); } +function prepareSearchIndex(data) { + const now = new Date(); + data.forEach(pdf => { + // Pre-calculate lowercased search string for faster filtering + const title = (pdf.title || '').toLowerCase(); + const desc = (pdf.description || '').toLowerCase(); + const cat = (pdf.category || '').toLowerCase(); + const author = (pdf.author || '').toLowerCase(); + pdf._searchStr = `${title} ${desc} ${cat} ${author}`; + + // Handle Firestore Timestamp or ISO string + let uploadDateObj; + if (pdf.uploadDate && typeof pdf.uploadDate.toDate === 'function') { + uploadDateObj = pdf.uploadDate.toDate(); + // We don't overwrite uploadDate here because it's already stringified when saving to cache. + // But if it's fresh from DB, we might want to ensure it's a string, or just let new Date() handle it. + } else { + uploadDateObj = new Date(pdf.uploadDate); + } + + // Pre-calculate new badge status + const timeDiff = now - uploadDateObj; + pdf._isNew = timeDiff < (7 * 24 * 60 * 60 * 1000); // 7 days + + // Pre-calculate formatted date + 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(pdfDatabase); // --- FIX: CALL THIS TO POPULATE UI --- syncClassSwitcher(); renderSemesterTabs(); @@ -477,6 +509,8 @@ async function loadPDFDatabase() { data: pdfDatabase })); + prepareSearchIndex(pdfDatabase); + // --- FIX: CALL THIS TO POPULATE UI --- syncClassSwitcher(); renderPDFs(); @@ -905,26 +939,21 @@ function renderPDFs() { // Locate renderPDFs() in script.js and update the filter section const filteredPdfs = pdfDatabase.filter(pdf => { - const matchesSemester = pdf.semester === currentSemester; + // Fast early returns for exact matches + if (pdf.class !== currentClass) return false; + 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; - - 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); + // Fast text search using pre-calculated search string + // Note: the `searchTerm` variable is lowercased at the top of renderPDFs + if (searchTerm && (!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,11 +1023,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 ? `NEW` : ''; @@ -1010,10 +1035,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' - }); + // Use pre-calculated formatted date + const formattedDate = pdf._formattedDate; // Uses global escapeHtml() now diff --git a/verify_perf.py b/verify_perf.py new file mode 100644 index 0000000..dace185 --- /dev/null +++ b/verify_perf.py @@ -0,0 +1,193 @@ +import asyncio +from playwright.async_api import async_playwright +import time +import json + +async def main(): + async with async_playwright() as p: + browser = await p.chromium.launch(headless=True) + page = await browser.new_page() + + # Mock Firebase to prevent network requests and overwrite local cache + await page.route("**/*firebase*", lambda route: route.abort()) + await page.route("**/*googleapis*", lambda route: route.abort()) + await page.route("**/*gstatic*", lambda route: route.abort()) + + # Seed local storage with mock data + mock_data = { + "timestamp": int(time.time() * 1000), + "data": [ + { + "id": "1", + "title": "Organic Chemistry Notes", + "description": "Notes for organic chemistry.", + "category": "Organic", + "author": "Alice", + "class": "MSc Chemistry", + "semester": 1, + "uploadDate": "2023-10-01T00:00:00.000Z" + }, + { + "id": "2", + "title": "Inorganic Chemistry Notes", + "description": "Notes for inorganic chemistry.", + "category": "Inorganic", + "author": "Bob", + "class": "MSc Chemistry", + "semester": 1, + "uploadDate": "2023-10-02T00:00:00.000Z" + }, + { + "id": "3", + "title": "Physics Notes", + "description": "Basic physics.", + "category": "Physics", + "author": "Charlie", + "class": "BSc Physics", + "semester": 2, + "uploadDate": "2023-10-03T00:00:00.000Z" + } + ] + } + + # Navigate to a blank page on the same origin first to set localStorage + await page.goto("http://localhost:8000/", wait_until="domcontentloaded") + + await page.evaluate(f"localStorage.setItem('classnotes_db_cache', '{json.dumps(mock_data)}');") + await page.evaluate("localStorage.setItem('currentClass', 'MSc Chemistry');") + await page.evaluate("localStorage.setItem('currentSemester', '1');") + + # Mock snapshot empty check (simulate cache use) + # Because we abort firebase requests, window.firebase is undefined, and the app waits. + # So we evaluate window.firebase mock so that app can proceed. + await page.evaluate(""" + window.firebase = { + apps: [{name: 'mock'}], + initializeApp: function() {}, + auth: function() { return { onAuthStateChanged: function(cb) { cb({uid: '123'}); }, signInAnonymously: function() { return Promise.resolve({user: {uid: '123'}}); } } }, + firestore: function() { + return { + collection: function(c) { + return { + doc: function(d) { + return { + set: function() {}, + onSnapshot: function(cb, errCb) { errCb('mock error'); } + }; + }, + orderBy: function() { + return { + limit: function() { + return { + get: function() { + return Promise.resolve({ empty: false, docs: [{id: '1'}] }); + } + } + }, + get: function() { + return Promise.resolve({ + forEach: function(cb) {} + }); + } + } + } + }; + } + }; + } + }; + window.firebase.firestore.FieldValue = { + serverTimestamp: function() { return new Date(); }, + increment: function(v) { return v; } + }; + """) + + # Reload to let app load from cache + await page.reload(wait_until="domcontentloaded") + + # Inject the mock again immediately + await page.evaluate(""" + window.firebase = { + apps: [{name: 'mock'}], + initializeApp: function() {}, + auth: function() { return { onAuthStateChanged: function(cb) { cb({uid: '123'}); }, signInAnonymously: function() { return Promise.resolve({user: {uid: '123'}}); } } }, + firestore: function() { + return { + collection: function(c) { + return { + doc: function(d) { + return { + set: function() {}, + onSnapshot: function(cb, errCb) { errCb('mock error'); } + }; + }, + orderBy: function() { + return { + limit: function() { + return { + get: function() { + return Promise.resolve({ empty: false, docs: [{id: '1'}] }); + } + } + }, + get: function() { + return Promise.resolve({ + forEach: function(cb) {} + }); + } + } + } + }; + } + }; + } + }; + window.firebase.firestore.FieldValue = { + serverTimestamp: function() { return new Date(); }, + increment: function(v) { return v; } + }; + """) + + # Wait for the app to initialize + await page.wait_for_timeout(2000) + + # Hide full screen overlays + 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 a bit for DOM updates + await page.wait_for_timeout(500) + + # Let's inspect what is in pdfDatabase + pdf_count_db = await page.evaluate("pdfDatabase.length") + print(f"pdfDatabase length: {pdf_count_db}") + + # Verify initial rendering (MSc Chemistry, Semester 1) + pdf_count = await page.evaluate("document.querySelectorAll('.pdf-card').length") + print(f"Initial PDF count: {pdf_count} (expected 2)") + + # Verify prepareSearchIndex populated _searchStr + if pdf_count_db > 0: + search_str_exists = await page.evaluate("pdfDatabase[0]._searchStr !== undefined") + print(f"_searchStr calculated: {search_str_exists} (expected True)") + + # Test Search (should match Organic Chemistry) + await page.fill("#searchInput", "organic") + # trigger input event + await page.evaluate("document.getElementById('searchInput').dispatchEvent(new Event('input'))") + await page.wait_for_timeout(500) + pdf_count_search = await page.evaluate("document.querySelectorAll('.pdf-card').length") + print(f"PDF count after search 'organic': {pdf_count_search} (expected 1)") + + # Test filter mismatch + await page.fill("#searchInput", "notfoundxyz") + await page.evaluate("document.getElementById('searchInput').dispatchEvent(new Event('input'))") + await page.wait_for_timeout(500) + pdf_count_search = await page.evaluate("document.querySelectorAll('.pdf-card').length") + print(f"PDF count after search 'notfoundxyz': {pdf_count_search} (expected 0)") + + await browser.close() + +if __name__ == "__main__": + asyncio.run(main()) \ No newline at end of file