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-17 - [Optimizing Render Loop]
**Learning:** Moving string concatenation and object creations (like `Date`) out of the loop and precalculating derived fields into object properties avoids overhead per render.
**Action:** Use precalculated properties during data fetches.
73 changes: 50 additions & 23 deletions script.js
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,39 @@ async function syncClassSwitcher() {
renderSemesterTabs();
}

function prepareSearchIndex() {
const now = new Date();
const sevenDaysMs = 7 * 24 * 60 * 60 * 1000;

for (let i = 0; i < pdfDatabase.length; i++) {
const pdf = pdfDatabase[i];

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

// 2. Pre-calculate Date and New status
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(); // Fallback
}

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

const timeDiff = now - uploadDateObj;
pdf._isNew = timeDiff < sevenDaysMs;
}
}

async function loadPDFDatabase() {
if (isMaintenanceActive) return;

Expand Down Expand Up @@ -454,6 +487,7 @@ async function loadPDFDatabase() {

if (shouldUseCache) {
pdfDatabase = cachedData;
prepareSearchIndex(); // ⚡ BOLT: Pre-calculate derived fields to avoid per-render overhead
// --- FIX: CALL THIS TO POPULATE UI ---
syncClassSwitcher();
renderSemesterTabs();
Expand All @@ -472,6 +506,8 @@ async function loadPDFDatabase() {
pdfDatabase.push({ id: doc.id, ...doc.data() });
});

prepareSearchIndex(); // ⚡ BOLT: Pre-calculate derived fields to avoid per-render overhead

localStorage.setItem(CACHE_KEY, JSON.stringify({
timestamp: new Date().getTime(),
data: pdfDatabase
Expand Down Expand Up @@ -904,27 +940,23 @@ function renderPDFs() {
}

// Locate renderPDFs() in script.js and update the filter section
// ⚡ BOLT: Explicit early returns and pre-calculated _searchStr for huge perf win
const filteredPdfs = pdfDatabase.filter(pdf => {
const matchesSemester = pdf.semester === currentSemester;
// 1. Cheap checks first (equality)
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;
// 2. Category / Favorites check
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);
// 3. Expensive check last (string search using pre-calculated _searchStr)
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 @@ -994,11 +1026,8 @@ 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
// ⚡ BOLT: Using pre-calculated _isNew and _formattedDate to avoid per-render Date overhead
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 @@ -1011,9 +1040,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 || 'Unknown Date';

// Uses global escapeHtml() now

Expand Down
136 changes: 136 additions & 0 deletions test_frontend.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
from playwright.sync_api import sync_playwright

def test_frontend():
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()

# Block external dependencies
page.route("**/*", lambda route: route.continue_() if "127.0.0.1" in route.request.url or "localhost" in route.request.url or route.request.url.startswith("file://") else route.abort())

# Seed local storage and block requests
page.goto("file:///app/index.html", wait_until="domcontentloaded")

page.evaluate("""
localStorage.setItem('currentClass', 'MSc Chemistry');
localStorage.setItem('currentSemester', '1');

// Provide a mock cached database
const mockDb = {
timestamp: new Date().getTime(),
data: [
{
id: 'pdf1',
title: 'Organic Synthesis Note',
description: 'Detailed reactions',
category: 'Organic',
author: 'Dr. John',
class: 'MSc Chemistry',
semester: 1,
uploadDate: new Date().toISOString()
},
{
id: 'pdf2',
title: 'Quantum Mechanics',
description: 'Schrodinger equation',
category: 'Physical',
author: 'Dr. Smith',
class: 'MSc Chemistry',
semester: 1,
uploadDate: new Date(Date.now() - 10 * 24 * 60 * 60 * 1000).toISOString() // 10 days old
}
]
};
localStorage.setItem('classnotes_db_cache', JSON.stringify(mockDb));
""")

# Reload to apply mock
page.goto("file:///app/index.html", wait_until="networkidle")

# Console output for debugging
page.on("console", lambda msg: print(f"Browser console: {msg.text}"))

# Mock Firestore and other variables missing since external JS is blocked
page.evaluate("""
window.firebase = {
apps: [{ name: 'mock' }],
initializeApp: () => {},
auth: () => ({
signInAnonymously: () => Promise.resolve({ user: { uid: 'mock_uid' } }),
onAuthStateChanged: (cb) => cb(null)
}),
firestore: () => {
const mockFirestore = () => ({
collection: (colName) => ({
doc: () => ({
set: () => Promise.resolve(),
onSnapshot: () => {}
}),
orderBy: () => ({
limit: () => ({
get: () => Promise.resolve({
empty: false,
docs: [{ id: 'pdf1' }] // Match cache ID to bypass "empty" logic and use cache
})
}),
get: () => Promise.resolve({
forEach: () => {}
})
})
})
});
mockFirestore.FieldValue = {
serverTimestamp: () => 'mock_timestamp',
increment: () => 'mock_increment'
};
return mockFirestore();
}
};
window.db = window.firebase.firestore();

// Remove preloader
document.getElementById('preloader').classList.add('hidden');
document.getElementById('contentWrapper').classList.add('active');

// Programmatically call init manually as we mocked after domcontentloaded
if (typeof loadPDFDatabase === 'function') {
loadPDFDatabase().then(() => console.log('loadPDFDatabase finished'));
} else {
console.log('loadPDFDatabase not found!');
}
""")

page.wait_for_timeout(2000) # wait for render

# Check if items are rendered
cards_count = page.locator('.pdf-card').count()
print(f"Number of PDF cards rendered: {cards_count}")
assert cards_count == 2, f"Expected 2 cards, got {cards_count}"

# Check text in cards to verify _isNew and _formattedDate logic didn't break them
card1_text = page.locator('.pdf-card').nth(0).inner_text()
print("Card 1 text:", card1_text)
assert "Organic Synthesis Note" in card1_text
assert "NEW" in card1_text # 1st one is new

card2_text = page.locator('.pdf-card').nth(1).inner_text()
print("Card 2 text:", card2_text)
assert "Quantum Mechanics" in card2_text
assert "NEW" not in card2_text # 2nd one is 10 days old

# Test search logic refactoring
page.fill('#searchInput', 'Quantum')

# We need to trigger input event as Playwright's fill doesn't always trigger standard input listeners that we hooked on
page.evaluate("document.getElementById('searchInput').dispatchEvent(new Event('input', { bubbles: true }))")

page.wait_for_timeout(500)

cards_count = page.locator('.pdf-card').count()
print(f"Number of PDF cards after search 'Quantum': {cards_count}")
assert cards_count == 1, f"Expected 1 card after search, got {cards_count}"

browser.close()

if __name__ == "__main__":
test_frontend()