Welcome Back ๐ŸŒธ

Choose who's using the hub today

๐Ÿ‘‹ Hi, Allegra!

Let's have a great day of learning together.

12:00PM
Loadingโ€ฆ
โ€œStay curious.โ€
โ€” Anonymous
0
Today's Items
0
Completed
0
Carried Over
0
Day Streak
This Week's Spelling Words

Assignments

Calendar

Upcoming Events
Major Milestones & Holidays

Lessons

Tasks & Projects

To Do
In Progress
Done

Gradebook

Attendance

Mark Attendance for Any Date

Tip: You can also tap any day in the grid below to cycle through statuses.

School Year Attendance
Present Absent Excused Holiday/Break Future Weekend

Progress Tracker

Curriculum Standards

My Journal

STEM Corner

Hands-on science experiments, engineering challenges, and discovery projects to spark curiosity. Each one connects to grade-level science skills and works with materials you probably already have at home.

Arts & Crafts Corner

Creative projects to do together โ€” painting, sculpting, seasonal crafts, and mixed media adventures. A space to make mess, try new techniques, and bond over making things with your hands.

Printable Templates

Pick a template, customize it for today's lesson, then click Print. Each template is sized for a standard 8.5ร—11 sheet with room for handwriting and notes.

Curriculum Overview

About 3rd Grade Curriculum

Books & Reading

Track every workbook, textbook, and curriculum book you're using this year. Tag by subject to see which materials support each class.

Settings

Student Info
Security & Access

Set a passcode to protect mom-only features (private tasks, edit grades, settings, etc.).


Important: This is a soft access fence, not real security. Anyone with access to this device's browser can view the source. For real protection of sensitive data, only use this on trusted devices.

Backup & Restore

Manual export downloads everything to a JSON file. Auto-backups run every 12 hours and keep the last 4 snapshots in your browser.



Auto-Backup History

Import Lesson Pack

Adds detailed lessons to the lesson planner and schedules them onto the calendar/agenda. This does not erase grades, attendance, or tasks. Existing lessons on the same date+subject are replaced; duplicate scheduled items are skipped.

Tip: the pack targets a specific grade. Make sure you're viewing that grade, or the importer will offer to switch.

About & Hosting

This is a single-file HTML app for Stella Maris Prep Academy. Upload index.html to your GoDaddy cPanel public_html folder. All data is stored in your browser's localStorage on each device.

For cross-device sync: A static HTML file can't sync data between devices on its own. Currently you'd need to export from one device and import on another. A future enhancement could add a small PHP file to enable server-side sync.

Saved

${escapeHtml(state.student.name)} โ€” Standards Progress Report

Stella Maris Prep Academy
Grade ${grade}  โ€ข  ${state.student.year}  โ€ข  Generated ${dateStr}

Overall Progress

${overallPct}%
Total Progress
${mastered}
Mastered
${inProgress}
In Progress
${totalCount}
Total Standards
${subjectsHtml} `; // Open in new window for printing const w = window.open('', '_blank'); if (!w) { // Fallback: download downloadFile(`allegra-standards-report-grade${grade}-${isoDate(today_)}.html`, html, 'text/html'); toast('Report downloaded (pop-ups blocked)'); } else { w.document.write(html); w.document.close(); toast('Report opened in new tab'); } } function importData(e){ const file = e.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = ev => { try { const data = JSON.parse(ev.target.result); state = Object.assign(defaultState(), data); currentGrade = state.currentGrade || 3; save(); toast('Backup restored'); document.querySelectorAll('.grade-switch button').forEach(b => b.classList.toggle('active', +b.dataset.grade===currentGrade)); renderAll(); } catch(err) { toast('Invalid backup file'); } }; reader.readAsText(file); } /* ---------- NON-DESTRUCTIVE LESSON PACK IMPORT ---------- */ function importLessonPack(e){ const file = e.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = ev => { let pack; try { pack = JSON.parse(ev.target.result); } catch(_){ toast('Invalid lesson pack file'); e.target.value=''; return; } if (!pack || !pack.lessonPack || !pack.lessons || typeof pack.lessons !== 'object'){ toast('Not a lesson pack (missing lessonPack flag)'); e.target.value=''; return; } const packGrade = pack.grade || currentGrade; if (packGrade !== currentGrade){ if (!confirm(`This pack is for Grade ${packGrade}, but you're viewing Grade ${currentGrade}. Import into Grade ${packGrade}? (You'll be switched to it.)`)){ e.target.value=''; return; } currentGrade = packGrade; state.currentGrade = packGrade; document.querySelectorAll('.grade-switch button').forEach(b => b.classList.toggle('active', (b.dataset.grade===String(packGrade)) || (+b.dataset.grade===packGrade))); } const g = packGrade; if (!state.lessons[g]) state.lessons[g] = {}; if (!state.assignments[g]) state.assignments[g] = {}; let lessonCount = 0, scheduledCount = 0, skippedDup = 0; Object.keys(pack.lessons).forEach(key => { const lesson = pack.lessons[key]; const [dateKey, subj] = key.split(':'); if (!dateKey || !subj || !SUBJECTS[subj]) return; // 1) Store the detailed lesson (replaces any existing on same date+subj) state.lessons[g][key] = { title: lesson.title || '', objective: lesson.objective || '', materials: Array.isArray(lesson.materials) ? lesson.materials : [], assignment: lesson.assignment || '', assessment: lesson.assessment || '', standards: Array.isArray(lesson.standards) ? lesson.standards : [] }; lessonCount++; // 2) Schedule onto the calendar/agenda (skip if same subj+title already there) if (!state.assignments[g][dateKey]) state.assignments[g][dateKey] = []; const exists = state.assignments[g][dateKey].some(a => a.subj===subj && a.title===(lesson.title||'')); if (exists){ skippedDup++; } else { state.assignments[g][dateKey].push({ id: uid(), subj, title: lesson.title || '(untitled)', done:false, carried:false, isTest: /\b(quiz|test|assessment)\b/i.test(lesson.title||''), seedDate: dateKey, notes:'' }); scheduledCount++; } }); save(); renderAll(); toast(`Imported ${lessonCount} lessons ยท ${scheduledCount} scheduled${skippedDup?` ยท ${skippedDup} dup skipped`:''}`); e.target.value=''; }; reader.readAsText(file); } function resetData(){ if (!confirm('This will erase ALL data including grades, attendance, tasks, and assignments. Are you sure?')) return; if (!confirm('Really sure? This cannot be undone.')) return; localStorage.removeItem(STORAGE_KEY); location.reload(); } /* ---------- MODALS ---------- */ function openModal(id){ document.querySelectorAll('.modal-bg').forEach(m => m.classList.remove('show')); document.getElementById(id).classList.add('show'); if (id === 'addAssignmentModal' && !window._editingAssignment){ document.getElementById('naTitle').value = ''; document.getElementById('naNotes').value = ''; document.getElementById('naDate').value = isoDate(today()); } if (id === 'addGradeModal'){ document.getElementById('ngTitle').value = ''; document.getElementById('ngScore').value = ''; document.getElementById('ngTotal').value = ''; document.getElementById('ngDate').value = isoDate(today()); } if (id === 'addTaskModal'){ document.getElementById('ntTitle').value = ''; document.getElementById('ntDue').value = ''; } if (id === 'addLinkModal'){ ['nlName','nlUrl','nlDesc','nlIcon'].forEach(i => document.getElementById(i).value = ''); } } function closeModal(){ document.querySelectorAll('.modal-bg').forEach(m => m.classList.remove('show')); window._editingAssignment = null; } /* ---------- TOAST ---------- */ let toastTimer; function toast(msg){ const t = document.getElementById('toast'); document.getElementById('toastMsg').textContent = msg; t.classList.add('show'); clearTimeout(toastTimer); toastTimer = setTimeout(()=>t.classList.remove('show'), 2400); } /* ---------- CONFETTI ---------- */ function celebrate(){ const colors = ['#ff8aa8','#ff9b7a','#5dd5d5','#ffc857','#ff5c87']; for (let i=0; i<22; i++){ const piece = document.createElement('div'); piece.className = 'confetti-piece'; piece.style.left = Math.random()*100 + '%'; piece.style.background = colors[Math.floor(Math.random()*colors.length)]; piece.style.animationDuration = (1.6 + Math.random()*1.2) + 's'; piece.style.animationDelay = (Math.random()*.3) + 's'; piece.style.borderRadius = Math.random()>.5 ? '50%' : '2px'; document.body.appendChild(piece); setTimeout(()=>piece.remove(), 3500); } } /* ---------- ESCAPE HELPERS ---------- */ function escapeHtml(s){ if (s == null) return ''; return String(s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); } function escapeAttr(s){ return String(s).replace(/'/g,"\\'"); } /* ============================================================ USER MODE (Mom / Allegra) + LOGIN ============================================================ */ function applyModeUI(){ document.body.classList.remove('mode-mom','mode-kid'); document.body.classList.add('mode-' + (state.currentMode || 'mom')); } function renderUserBadge(){ const wrap = document.getElementById('userBadge'); if (!wrap) return; const mode = state.currentMode || 'mom'; const isMom = mode === 'mom'; wrap.className = 'user-badge role-' + (isMom?'mom':'kid'); wrap.innerHTML = `
${isMom?'๐Ÿ‘ฉ':'๐Ÿ‘ง'}
${isMom?'Mom':'Allegra'}
`; } function selectLoginRole(role){ document.querySelectorAll('.login-role').forEach(r => r.classList.toggle('selected', r.dataset.role===role)); document.getElementById('loginPinField').style.display = role==='mom' ? 'block' : 'none'; window._loginPickedRole = role; if (role === 'mom') setTimeout(() => document.getElementById('loginPin')?.focus(), 80); } async function completeLogin(){ const role = window._loginPickedRole; if (!role) { toast('Please choose Mom or Allegra'); return; } if (role === 'mom') { const pin = document.getElementById('loginPin').value; if (state.momPinHash) { const hash = await sha256(pin); if (hash !== state.momPinHash) { toast('Incorrect passcode'); return; } } else { // No passcode set yet โ€” allow entry, prompt to set one setTimeout(() => toast('Tip: Set a passcode on the Settings page to protect mom-only features'), 1200); } state.currentMode = 'mom'; } else { state.currentMode = 'kid'; } save(); document.getElementById('loginOverlay').classList.remove('show'); document.getElementById('loginPin').value = ''; applyModeUI(); renderAll(); } function logout(){ document.getElementById('loginOverlay').classList.add('show'); document.querySelectorAll('.login-role').forEach(r => r.classList.remove('selected')); document.getElementById('loginPinField').style.display = 'none'; window._loginPickedRole = null; } async function changePin(){ const oldPin = document.getElementById('oldPin').value; const newPin = document.getElementById('newPin').value; const newPin2 = document.getElementById('newPin2').value; if (state.momPinHash) { const oldHash = await sha256(oldPin); if (oldHash !== state.momPinHash) { toast('Current passcode is incorrect'); return; } } if (newPin !== newPin2) { toast('New passcodes don\'t match'); return; } if (newPin.length === 0) { state.momPinHash = null; toast('Passcode removed'); } else if (newPin.length < 4) { toast('Use at least 4 characters'); return; } else { state.momPinHash = await sha256(newPin); toast('Passcode updated'); } save(); closeModal(); ['oldPin','newPin','newPin2'].forEach(i => document.getElementById(i).value = ''); } /* ============================================================ MOBILE SIDEBAR ============================================================ */ function toggleSidebar(){ const sb = document.getElementById('sidebar'); const bd = document.getElementById('sidebarBackdrop'); // On mobile, this opens/closes the off-canvas drawer // On desktop with collapsed state, this expands it if (window.innerWidth > 760 && document.getElementById('app').classList.contains('sidebar-collapsed')) { toggleSidebarCollapse(); return; } sb.classList.toggle('open'); bd.classList.toggle('show', sb.classList.contains('open')); } function closeSidebar(){ document.getElementById('sidebar')?.classList.remove('open'); document.getElementById('sidebarBackdrop')?.classList.remove('show'); } function toggleSidebarCollapse(){ const app = document.getElementById('app'); app.classList.toggle('sidebar-collapsed'); const collapsed = app.classList.contains('sidebar-collapsed'); // Show or hide the menu toggle in desktop mode when sidebar is hidden const mt = document.getElementById('menuToggle'); if (mt) mt.classList.toggle('show-collapsed', collapsed); // Save preference state.sidebarCollapsed = collapsed; save(); } /* ============================================================ CLOCK + ROTATING QUOTES ============================================================ */ let clockTimer = null; let quoteTimer = null; function tickClock(){ const timeEl = document.getElementById('clockTime'); const dayEl = document.getElementById('clockDay'); if (!timeEl || !dayEl) return; const now = new Date(); // Use toLocaleTimeString โ€” respects user's locale & timezone reliably const timeStr = now.toLocaleTimeString('en-US', {hour:'numeric', minute:'2-digit', hour12:true}); // Split into "1:23" and "PM" parts for styling const match = timeStr.match(/^(.+)\s+(AM|PM)$/i); if (match) { timeEl.innerHTML = `${match[1]}${match[2]}`; } else { timeEl.textContent = timeStr; } dayEl.textContent = now.toLocaleDateString('en-US',{weekday:'long',month:'long',day:'numeric',year:'numeric'}); } function renderClock(){ // Tick once immediately for current values tickClock(); // Start global ticker only once (don't recreate on every renderAll) if (!clockTimer) clockTimer = setInterval(tickClock, 1000); // Set initial quote if not present if (document.getElementById('quoteText')?.textContent === 'Stay curious.') renderQuote(false); // Rotate quotes every 25 seconds if (!quoteTimer) quoteTimer = setInterval(() => renderQuote(true), 25000); } function renderQuote(advance){ const wrap = document.getElementById('clockQuote'); if (!wrap) return; if (advance) state.lastQuoteIdx = ((state.lastQuoteIdx||0) + 1) % QUOTES.length; const q = QUOTES[(state.lastQuoteIdx||0) % QUOTES.length]; document.getElementById('quoteText').textContent = q.q; document.getElementById('quoteAuthor').textContent = 'โ€” ' + q.a; wrap.classList.remove('quote-anim'); void wrap.offsetWidth; wrap.classList.add('quote-anim'); } /* ============================================================ GRADE LOOKUP FOR ASSIGNMENTS (NEW) Find a grade that matches an assignment's date or title ============================================================ */ function findGradeForAssignment(a, dateKey){ // Match by exact title + same date (loose: within ยฑ7 days) + same subject const grades = state.grades[currentGrade] || []; if (!a || !a.title || !a.subj) return null; const d = parseDate(dateKey).getTime(); const aTitleLower = a.title.toLowerCase(); const matches = grades.filter(g => { if (!g || !g.title || !g.date) return false; if (g.subj !== a.subj) return false; const gTitleLower = g.title.toLowerCase(); const titleMatch = gTitleLower === aTitleLower || gTitleLower.includes(aTitleLower) || aTitleLower.includes(gTitleLower); if (!titleMatch) return false; const gd = parseDate(g.date).getTime(); return Math.abs(gd - d) <= 7 * 86400000; }); // Return closest by date if (!matches.length) return null; matches.sort((x,y) => Math.abs(parseDate(x.date).getTime()-d) - Math.abs(parseDate(y.date).getTime()-d)); return matches[0]; } /* ============================================================ JOURNAL ============================================================ */ function dayOfYear(d){ const start = new Date(d.getFullYear(),0,0); return Math.floor((d - start) / 86400000); } function getTodayPrompt(){ return JOURNAL_PROMPTS[dayOfYear(today()) % JOURNAL_PROMPTS.length]; } function renderJournal(){ const promptCard = document.getElementById('todayPromptCard'); if (!promptCard) return; const tKey = isoDate(today()); const todayEntry = state.journal.find(j => j.date === tKey); promptCard.innerHTML = `
Today's Prompt
${escapeHtml(getTodayPrompt())}
${todayEntry ? `` : `` } `; const list = document.getElementById('journalList'); const entries = [...state.journal].sort((a,b) => b.date.localeCompare(a.date)); if (!entries.length) { list.innerHTML = `

No entries yet. Today's a great day to start!

`; return; } list.innerHTML = entries.map(e => `
${fmtLong(parseDate(e.date))}
${e.mood ? `${e.mood}` : ''} ${e.prompt ? `
${escapeHtml(e.prompt)}
` : ''}
${escapeHtml(e.content)}
`).join(''); } function openJournalEditor(){ window._editingJournal = null; document.getElementById('journalModalTitle').textContent = 'New Journal Entry'; document.getElementById('jeDate').value = isoDate(today()); document.getElementById('jePromptDisplay').textContent = getTodayPrompt(); document.getElementById('jeContent').value = ''; document.querySelectorAll('#moodPicker .mood-opt').forEach(m => m.classList.remove('selected')); window._selectedMood = ''; openModal('journalModal'); setTimeout(() => document.getElementById('jeContent')?.focus(), 100); } function editJournalEntry(id){ const entry = state.journal.find(j => j.id === id); if (!entry) return; window._editingJournal = id; document.getElementById('journalModalTitle').textContent = 'Edit Entry'; document.getElementById('jeDate').value = entry.date; document.getElementById('jePromptDisplay').textContent = entry.prompt || getTodayPrompt(); document.getElementById('jeContent').value = entry.content; document.querySelectorAll('#moodPicker .mood-opt').forEach(m => m.classList.toggle('selected', m.dataset.mood === entry.mood)); window._selectedMood = entry.mood || ''; openModal('journalModal'); } function saveJournalEntry(){ const date = document.getElementById('jeDate').value || isoDate(today()); const content = document.getElementById('jeContent').value.trim(); if (!content) { toast('Write something first ๐ŸŒธ'); return; } const mood = window._selectedMood || ''; const prompt = document.getElementById('jePromptDisplay').textContent; if (window._editingJournal) { const idx = state.journal.findIndex(j => j.id === window._editingJournal); if (idx >= 0) state.journal[idx] = {...state.journal[idx], date, content, mood, prompt, updatedAt:new Date().toISOString()}; } else { state.journal.push({id:uid(), date, content, mood, prompt, createdAt:new Date().toISOString()}); } save(); closeModal(); toast('Entry saved ๐Ÿ’•'); renderJournal(); } function deleteJournalEntry(id){ if (!confirm('Delete this journal entry?')) return; state.journal = state.journal.filter(j => j.id !== id); save(); renderJournal(); } /* ============================================================ STEM CORNER + ARTS & CRAFTS ============================================================ */ let stemFilter = 'all'; let artsFilter = 'all'; function getAllStem(){ const custom = (state.discoverCustom.stem || []).map(p => ({...p, custom:true})); return [...STEM_LIBRARY, ...custom]; } function getAllArts(){ const custom = (state.discoverCustom.arts || []).map(p => ({...p, custom:true})); return [...ARTS_LIBRARY, ...custom]; } function bannerClass(cat){ const map = { 'Experiment':'b-experiment','Engineering':'b-engineering','Nature':'b-nature','Space':'b-space', 'Math':'b-math','Technology':'b-tech','History':'b-space','English':'b-paint','Marine Biology':'b-nature', 'Physical Science':'b-tech','Geology':'b-nature','Weather':'b-space','Space':'b-space', 'Paint':'b-paint','Craft':'b-craft','Sculpture':'b-sculpt','Seasonal':'b-seasonal' }; return map[cat] || 'b-experiment'; } function bannerEmoji(cat){ return {'Experiment':'๐Ÿงช','Engineering':'โš™๏ธ','Nature':'๐ŸŒฟ','Space':'๐ŸŒŒ','Math':'๐Ÿ“','Technology':'๐Ÿ’ก','History':'๐Ÿ›๏ธ','English':'โœ๏ธ','Marine Biology':'๐ŸŒŠ','Physical Science':'โšก','Geology':'๐Ÿชจ','Weather':'๐ŸŒฆ๏ธ','Paint':'๐ŸŽจ','Craft':'โœ‚๏ธ','Sculpture':'๐Ÿ—ฟ','Seasonal':'๐ŸŽƒ'}[cat] || 'โœจ'; } function renderStem(){ const grid = document.getElementById('stemGrid'); if (!grid) return; const projects = getAllStem(); const cats = ['all', ...new Set(projects.map(p => p.category))]; const sf = document.getElementById('stemFilter'); if (sf && !sf.dataset.built) { sf.dataset.built = '1'; sf.innerHTML = cats.map(c => ``).join(''); sf.querySelectorAll('.chip').forEach(c => c.onclick = () => { stemFilter = c.dataset.stemCat; sf.querySelectorAll('.chip').forEach(x => x.classList.toggle('active', x.dataset.stemCat===stemFilter)); renderStem(); }); } const filtered = projects.filter(p => stemFilter==='all' || p.category===stemFilter); grid.innerHTML = filtered.map(p => renderDiscoverCard(p, 'stem')).join('') || `

No projects match this filter.

`; } function renderArts(){ const grid = document.getElementById('artsGrid'); if (!grid) return; const projects = getAllArts(); const cats = ['all', ...new Set(projects.map(p => p.category))]; const sf = document.getElementById('artsFilter'); if (sf && !sf.dataset.built) { sf.dataset.built = '1'; sf.innerHTML = cats.map(c => ``).join(''); sf.querySelectorAll('.chip').forEach(c => c.onclick = () => { artsFilter = c.dataset.artsCat; sf.querySelectorAll('.chip').forEach(x => x.classList.toggle('active', x.dataset.artsCat===artsFilter)); renderArts(); }); } const filtered = projects.filter(p => artsFilter==='all' || p.category===artsFilter); grid.innerHTML = filtered.map(p => renderDiscoverCard(p, 'arts')).join('') || `

No projects match this filter.

`; } function renderDiscoverCard(p, type){ const st = (state.projectStatus && state.projectStatus[p.id]) || null; const doneBadge = (st && st.done) ? `โœ“ Done` : ''; return `
${bannerEmoji(p.category)}
${escapeHtml(p.title)}
${escapeHtml(p.category)} ${escapeHtml(p.difficulty||'easy')} ${p.time ? `โฑ๏ธ ${escapeHtml(p.time)}` : ''} ${doneBadge}
${escapeHtml(p.desc)}
`; } function openDiscover(type, id){ const list = type === 'stem' ? getAllStem() : getAllArts(); const p = list.find(x => x.id === id); if (!p) return; window._currentDiscover = {type, id, custom: !!p.custom}; document.getElementById('discoverModalTitle').textContent = p.title; const content = document.getElementById('discoverModalContent'); content.innerHTML = `
${escapeHtml(p.category)} ${escapeHtml(p.difficulty||'easy')} ${p.time ? `โฑ๏ธ ${escapeHtml(p.time)}` : ''}

${escapeHtml(p.desc)}

You'll need

Steps

    ${(p.steps||[]).map(s => `
  1. ${escapeHtml(s)}
  2. `).join('')}
${p.learning ? `

What you're learning

${escapeHtml(p.learning)}

` : ''} `; // Completion / grade status banner const st = (state.projectStatus && state.projectStatus[id]) || null; const gradeBtn = document.getElementById('discoverGradeBtn'); if (st && st.done) { const g = (state.grades[currentGrade] || []).find(x => x.id === st.gradeId) || Object.values(state.grades).flat().find(x => x.id === st.gradeId); const scoreTxt = g ? ` โ€” scored ${g.score}/${g.total} (${Math.round(g.score/g.total*100)}%)` : ''; content.innerHTML += `
โœ“ Completed${st.doneAt?' on '+fmtLong(parseDate(st.doneAt.slice(0,10))):''}${scoreTxt}
`; if (gradeBtn) gradeBtn.innerHTML = ' Update Grade'; } else { if (gradeBtn) gradeBtn.innerHTML = ' Mark Complete & Grade'; } // Show delete only for custom document.getElementById('discoverDeleteBtn').style.display = p.custom ? 'inline-flex' : 'none'; openModal('discoverModal'); } // Grade a paper directly from its calendar assignment (via linked projectId) function gradeLinkedProject(projId){ const inStem = getAllStem().some(x => x.id === projId); window._currentDiscover = { type: inStem ? 'stem' : 'arts', id: projId }; closeModal(); gradeCurrentProject(); } /* Set how a lesson's work was completed: 'online' or 'offline' (or clear). */ function setAssignmentMode(dateKey, id, mode){ const list = state.assignments[currentGrade][dateKey] || []; const a = list.find(x => x.id === id); if (!a) return; a.mode = (a.mode === mode) ? null : mode; // tap again to clear save(); renderAssignments(); } /* Open the grade entry modal pre-filled for a specific assignment. The saved grade auto-links back to the row via findGradeForAssignment. */ function gradeAssignment(dateKey, id){ const list = state.assignments[currentGrade][dateKey] || []; const a = list.find(x => x.id === id); if (!a) return; // If a grade already exists, edit it; else open a fresh one prefilled const existing = findGradeForAssignment(a, dateKey); window._gradingProject = null; if (existing){ window._editingGrade = existing.id; openModal('addGradeModal'); setTimeout(()=>{ document.getElementById('ngModalTitle').textContent = 'Edit Grade'; document.getElementById('ngTitle').value = existing.title; document.getElementById('ngSubject').value = existing.subj; document.getElementById('ngDate').value = existing.date; document.getElementById('ngScore').value = existing.score; document.getElementById('ngTotal').value = existing.total; document.getElementById('ngScore').focus(); }, 30); return; } window._editingGrade = null; openModal('addGradeModal'); setTimeout(()=>{ document.getElementById('ngModalTitle').textContent = 'Grade Lesson'; document.getElementById('ngTitle').value = a.title; document.getElementById('ngSubject').value = a.subj; document.getElementById('ngDate').value = dateKey; document.getElementById('ngScore').value = ''; document.getElementById('ngTotal').value = a.isTest ? '' : '100'; document.getElementById('ngScore').focus(); }, 30); } function gradeCurrentProject(){ const {type, id} = window._currentDiscover || {}; if (!id) return; const list = type === 'stem' ? getAllStem() : getAllArts(); const p = list.find(x => x.id === id); if (!p) return; const st = (state.projectStatus && state.projectStatus[id]) || null; // If already graded, jump to editing that grade; else open a fresh grade prefilled for this project if (st && st.done && st.gradeId) { const g = (state.grades[currentGrade] || []).find(x => x.id === st.gradeId); if (g) { closeModal(); editGrade(st.gradeId); window._gradingProject = {id, type}; return; } } // Determine subject + label from the project's category let subj, prefix; if (p.category === 'History'){ subj='social'; prefix='History Project: '; } else if (p.category === 'English'){ subj='ela'; prefix='English: '; } else if (type==='stem'){ subj='science'; prefix='Science Project: '; } else { subj='art'; prefix='Art Project: '; } window._editingGrade = null; window._gradingProject = {id, type}; closeModal(); openModal('addGradeModal'); // Prefill after the modal's own reset runs setTimeout(()=>{ document.getElementById('ngModalTitle').textContent = 'Grade Project'; document.getElementById('ngTitle').value = prefix + p.title; document.getElementById('ngSubject').value = subj; document.getElementById('ngDate').value = isoDate(today()); document.getElementById('ngScore').value = ''; document.getElementById('ngTotal').value = ''; document.getElementById('ngScore').focus(); }, 30); } function editCurrentDiscover(){ const {type, id} = window._currentDiscover || {}; if (!id) return; const list = type === 'stem' ? getAllStem() : getAllArts(); const p = list.find(x => x.id === id); if (!p) return; closeModal(); openDiscoverEditor(type, p); } function deleteCurrentDiscover(){ const {type, id} = window._currentDiscover || {}; if (!id || !confirm('Delete this project?')) return; state.discoverCustom[type] = state.discoverCustom[type].filter(p => p.id !== id); save(); closeModal(); toast('Project deleted'); if (type === 'stem') renderStem(); else renderArts(); } function openDiscoverEditor(type, existing){ window._editingDiscover = existing ? {type, id:existing.id} : {type}; document.getElementById('discoverEditorTitle').textContent = existing ? 'Edit Project' : 'New Project'; document.getElementById('deType').value = type; document.getElementById('deCategory').value = existing?.category || (type==='stem' ? 'Experiment' : 'Craft'); document.getElementById('deTitle').value = existing?.title || ''; document.getElementById('deDesc').value = existing?.desc || ''; document.getElementById('deDifficulty').value = existing?.difficulty || 'easy'; document.getElementById('deTime').value = existing?.time || ''; document.getElementById('deMaterials').value = (existing?.materials || []).join('\n'); document.getElementById('deSteps').value = (existing?.steps || []).join('\n'); document.getElementById('deLearning').value = existing?.learning || ''; openModal('discoverEditorModal'); } function saveDiscover(){ const type = document.getElementById('deType').value; const data = { id: window._editingDiscover?.id || ('cust-'+uid()), category: document.getElementById('deCategory').value, title: document.getElementById('deTitle').value.trim(), desc: document.getElementById('deDesc').value.trim(), difficulty: document.getElementById('deDifficulty').value, time: document.getElementById('deTime').value.trim(), materials: document.getElementById('deMaterials').value.split('\n').map(s=>s.trim()).filter(Boolean), steps: document.getElementById('deSteps').value.split('\n').map(s=>s.trim()).filter(Boolean), learning: document.getElementById('deLearning').value.trim() }; if (!data.title) { toast('Title required'); return; } if (window._editingDiscover?.id) { const idx = state.discoverCustom[type].findIndex(p => p.id === data.id); if (idx >= 0) state.discoverCustom[type][idx] = data; else state.discoverCustom[type].push(data); } else { state.discoverCustom[type].push(data); } save(); closeModal(); toast('Project saved'); if (type === 'stem') renderStem(); else renderArts(); } /* ============================================================ RETROACTIVE ATTENDANCE + FILTERS ============================================================ */ let attFilter = 'all'; function markForDate(status){ const dateInput = document.getElementById('attDate').value; if (!dateInput) { toast('Pick a date first'); return; } const d = parseDate(dateInput); if (isWeekend(d)) { toast('That\'s a weekend โ€” no school'); return; } state.attendance[currentGrade][dateInput] = status; save(); toast(`Marked ${fmtShort(d)} as ${status}`); renderAttendance(); renderDashboard(); } function clearForDate(){ const dateInput = document.getElementById('attDate').value; if (!dateInput) { toast('Pick a date first'); return; } delete state.attendance[currentGrade][dateInput]; save(); toast(`Cleared ${fmtShort(parseDate(dateInput))}`); renderAttendance(); renderDashboard(); } /* ============================================================ BOOK LIST / WORKBOOKS (NEW) ============================================================ */ /* ============================================================ PRINTABLE TEMPLATES ============================================================ */ const TEMPLATES = [ {id:'lab', name:'Science Lab Report', desc:'Hypothesis, procedure, observations, conclusion', icon:'๐Ÿงช', color:'linear-gradient(135deg,#5dd5d5,#3ac0c0)'}, {id:'journal', name:'Journal Writing', desc:'Lined page with today\'s prompt', icon:'โœ๏ธ', color:'linear-gradient(135deg,#ff8aa8,#ff5c87)'}, {id:'math', name:'Math Facts Generator',desc:'Auto-generate +, โˆ’, ร—, รท problems', icon:'โž—', color:'linear-gradient(135deg,#ffc857,#ff9b7a)'}, {id:'book', name:'Book Report', desc:'Title, characters, plot, opinion', icon:'๐Ÿ“–', color:'linear-gradient(135deg,#9774d8,#6b3fc7)'}, {id:'spelling', name:'Spelling Test', desc:'Numbered sheet for dictation', icon:'๐Ÿ”ค', color:'linear-gradient(135deg,#ff9b7a,#ff7654)'}, {id:'vocab', name:'Vocabulary Practice', desc:'Word, definition, sentence', icon:'๐Ÿ“š', color:'linear-gradient(135deg,#ff8aa8,#ffc857)'}, {id:'cursive', name:'Handwriting Practice',desc:'Cursive guide lines', icon:'โœ’๏ธ', color:'linear-gradient(135deg,#c5f0ef,#5dd5d5)'}, {id:'reading', name:'Reading Log', desc:'Track daily reading minutes & pages', icon:'๐Ÿ“•', color:'linear-gradient(135deg,#7dd87d,#2a8a3e)'}, {id:'fieldtrip', name:'Field Trip Summary', desc:'Where, what learned, favorite part', icon:'๐ŸšŒ', color:'linear-gradient(135deg,#ffc857,#5dd5d5)'}, {id:'reflect', name:'Daily Reflection', desc:'What went well, what was hard, next steps', icon:'๐Ÿ’ญ', color:'linear-gradient(135deg,#5dd5d5,#ff8aa8)'}, {id:'behavior', name:'Behavior Chart', desc:'Weekly conduct tracking grid', icon:'โญ', color:'linear-gradient(135deg,#ffc857,#ff8aa8)'} ]; let activeTemplate = 'lab'; let templateConfig = { // math defaults mathOp:'add', mathMin:0, mathMax:12, mathCount:20, // spelling defaults spellingCount:20, // vocab defaults vocabCount:10, // reading defaults readingDays:7, // cursive defaults cursiveText:'', cursiveLines:14 }; function renderTemplates(){ const grid = document.getElementById('templateGrid'); if (!grid) return; grid.innerHTML = TEMPLATES.map(t => `
${t.icon}
${t.name}
${t.desc}
`).join(''); renderTemplateContent(); } function selectTemplate(id){ activeTemplate = id; renderTemplates(); // Scroll the preview into view so you can actually see the form setTimeout(() => { const preview = document.getElementById('printArea'); if (preview) preview.scrollIntoView({behavior:'smooth', block:'start'}); }, 50); } function renderTemplateContent(){ const config = document.getElementById('templateConfigArea'); const printArea = document.getElementById('printArea'); if (!config || !printArea) return; const studentName = state.student.name || 'Allegra'; const dateStr = fmtLong(today()); switch(activeTemplate){ case 'lab': renderLabTemplate(config, printArea, studentName, dateStr); break; case 'journal': renderJournalTemplate(config, printArea, studentName, dateStr); break; case 'math': renderMathTemplate(config, printArea, studentName, dateStr); break; case 'book': renderBookTemplate(config, printArea, studentName, dateStr); break; case 'spelling': renderSpellingTemplate(config, printArea, studentName, dateStr); break; case 'vocab': renderVocabTemplate(config, printArea, studentName, dateStr); break; case 'cursive': renderCursiveTemplate(config, printArea, studentName, dateStr); break; case 'reading': renderReadingTemplate(config, printArea, studentName, dateStr); break; case 'fieldtrip': renderFieldTripTemplate(config, printArea, studentName, dateStr); break; case 'reflect': renderReflectTemplate(config, printArea, studentName, dateStr); break; case 'behavior': renderBehaviorTemplate(config, printArea, studentName, dateStr); break; } } /* ---------- helpers for template body ---------- */ function headerRow(name, date, extra){ return `
Name:${escapeHtml(name)}
Date:${escapeHtml(date)}
${extra ? `
${extra}
` : ''}
`; } /* ---------- Science Lab Report ---------- */ function renderLabTemplate(config, printArea, name, date){ config.innerHTML = `

Lab Report Settings

`; const title = document.getElementById('cfgLabTitle')?.value || ''; const hyp = document.getElementById('cfgLabHyp')?.value || ''; printArea.innerHTML = `

Science Lab Report

Stella Maris Prep Academy ยท Grade ${currentGrade==='bridge'?'3โ†’4':currentGrade}
${headerRow(name, date)}

Experiment

${escapeHtml(title)}

Question / Purpose

Hypothesis (What I think will happen)

${hyp ? `${escapeHtml(hyp)}` : ''}

Materials

Procedure (Step by step)

Observations & Data

Conclusion (What I learned)

`; } /* ---------- Journal Writing Page ---------- */ function renderJournalTemplate(config, printArea, name, date){ config.innerHTML = `

Journal Settings

`; const prompt = document.getElementById('cfgJrnPrompt')?.value || getTodayPrompt(); const lines = parseInt(document.getElementById('cfgJrnLines')?.value) || 18; printArea.innerHTML = `

My Journal

${escapeHtml(date)}
${headerRow(name, date)}

Today's Prompt

${escapeHtml(prompt)}
`; } /* ---------- Math Facts Generator ---------- */ function renderMathTemplate(config, printArea, name, date){ config.innerHTML = `

Math Facts Settings

`; const op = document.getElementById('cfgMathOp')?.value || 'add'; const range = (document.getElementById('cfgMathRange')?.value || '0-12').split('-').map(Number); const count = parseInt(document.getElementById('cfgMathCount')?.value) || 20; const opSymbol = {add:'+', sub:'โˆ’', mul:'ร—', div:'รท'}; const problems = []; for (let i=0; i a) [a,b] = [b,a]; // Make division clean (a is a multiple of b) if (o === 'div') { if (b === 0) b = 1; const q = randInt(range[0], range[1]); a = b * q; } problems.push({a, b, op:o, symbol:opSymbol[o]}); } const opLabel = {add:'Addition', sub:'Subtraction', mul:'Multiplication', div:'Division', mix:'Mixed Practice'}[op]; printArea.innerHTML = `

${opLabel} Practice

Numbers ${range[0]}โ€“${range[1]} ยท ${count} problems
${headerRow(name, date, 'Score:/ ' + count + '')}
${problems.map((p,i) => `
${(i+1)}.
${p.a} ${p.symbol}${p.b}
`).join('')}
`; } function randInt(min, max){ return Math.floor(Math.random() * (max - min + 1)) + min; } /* ---------- Book Report ---------- */ function renderBookTemplate(config, printArea, name, date){ config.innerHTML = `

Book Report Settings

`; const title = document.getElementById('cfgBookTitle')?.value || ''; const author = document.getElementById('cfgBookAuthor')?.value || ''; printArea.innerHTML = `

Book Report

Stella Maris Prep Academy
${headerRow(name, date)}

Book Title

${escapeHtml(title)}

Author

${escapeHtml(author)}

Genre

# of Pages

Main Characters

Setting

Summary (What happened?)

My Favorite Part

Would I recommend it? Why?

My Rating: โ˜† โ˜† โ˜† โ˜† โ˜†
`; } /* ---------- Spelling Test ---------- */ function renderSpellingTemplate(config, printArea, name, date){ config.innerHTML = `

Spelling Test Settings

`; const count = parseInt(document.getElementById('cfgSpellCount')?.value) || 20; const wordList = (document.getElementById('cfgSpellList')?.value || '').split(/[,\n]/).map(s=>s.trim()).filter(Boolean); const half = Math.ceil(count/2); printArea.innerHTML = `

Spelling Test

Listen carefully, then write each word.
${headerRow(name, date, 'Score:/ ' + count + '')}
${Array.from({length:count}, (_,i) => `
${i+1}.
`).join('')}
${wordList.length ? `

Teacher's Word List

Read each word, use in a sentence, repeat.
    ${wordList.slice(0,count).map(w => `
  1. ${escapeHtml(w)}
  2. `).join('')}
` : ''}
`; } /* ---------- Vocabulary Practice ---------- */ function renderVocabTemplate(config, printArea, name, date){ config.innerHTML = `

Vocabulary Settings

`; const count = parseInt(document.getElementById('cfgVocabCount')?.value) || 10; const words = (document.getElementById('cfgVocabList')?.value || '').split(',').map(s=>s.trim()).filter(Boolean); printArea.innerHTML = `

Vocabulary Practice

Look up each word, write the meaning, then use it in a sentence.
${headerRow(name, date)} ${Array.from({length:count}, (_,i) => ` `).join('')}
WordMy DefinitionSentence
${words[i] ? escapeHtml(words[i]) : ''}
`; } /* ---------- Cursive / Handwriting Practice ---------- */ function renderCursiveTemplate(config, printArea, name, date){ config.innerHTML = `

Handwriting Practice Settings

`; const text = document.getElementById('cfgCursiveText')?.value || ''; const lines = parseInt(document.getElementById('cfgCursiveLines')?.value) || 14; printArea.innerHTML = `

Handwriting Practice

Trace lightly, then practice on your own.
${headerRow(name, date)}
${Array.from({length:lines}, () => `
${text ? `
${escapeHtml(text)}
` : ''}
`).join('')}
`; } /* ---------- Reading Log ---------- */ function renderReadingTemplate(config, printArea, name, date){ config.innerHTML = `

Reading Log Settings

`; const days = parseInt(document.getElementById('cfgReadingDays')?.value) || 7; const week = document.getElementById('cfgReadingWeek')?.value || ''; printArea.innerHTML = `

Reading Log

${week ? escapeHtml('Week of ' + week) : 'Daily reading tracker'}
${headerRow(name, date)} ${Array.from({length:days}, () => ``).join('')}
Date Book Title Pages Minutes Parent Signature

Favorite Part This Week

Total Minutes Read

`; } function printTemplate(){ if (!activeTemplate) { toast('Pick a template first'); return; } window.print(); } /* ---------- Field Trip Summary ---------- */ function renderFieldTripTemplate(config, printArea, name, date){ config.innerHTML = `

Field Trip Settings

`; const loc = document.getElementById('cfgFTLocation')?.value || ''; const tripDate = document.getElementById('cfgFTDate')?.value || ''; const subj = document.getElementById('cfgFTSubject')?.value || ''; printArea.innerHTML = `

Field Trip Summary

Stella Maris Prep Academy ยท Grade ${currentGrade==='bridge'?'3โ†’4':currentGrade}
${headerRow(name, date)}

Where We Went

${escapeHtml(loc)}

Trip Date

${tripDate ? escapeHtml(fmtLong(parseDate(tripDate))) : ''}
${subj ? `
Subject Area: ${escapeHtml(subj)}
` : ''}

What We Did

Three Things I Learned

1.
2.
3.

My Favorite Part

New Vocabulary

Questions I Still Have

Draw Something You Saw

Connection to studies:
`; } /* ---------- Permission Slip ---------- */ function renderPermissionTemplate(config, printArea, name, date){ config.innerHTML = `

Permission Slip Settings

`; const activity = document.getElementById('cfgPermActivity')?.value || ''; const dt = document.getElementById('cfgPermDate')?.value || ''; const depart = document.getElementById('cfgPermDepart')?.value || ''; const ret = document.getElementById('cfgPermReturn')?.value || ''; const cost = document.getElementById('cfgPermCost')?.value || ''; printArea.innerHTML = `

Permission Slip

Stella Maris Prep Academy

I, the parent/guardian of ${escapeHtml(name)}, give permission for my child to participate in the following activity:

Activity / Destination

${escapeHtml(activity)}

Date

${dt ? escapeHtml(fmtLong(parseDate(dt))) : ''}

Departure

${escapeHtml(depart)}

Return

${escapeHtml(ret)}

Cost

${escapeHtml(cost)}

Emergency Contact (Name & Phone)

Medical Conditions / Allergies / Medications

Special Instructions

I acknowledge that I have read the activity details above and consent to my child's participation. I release Stella Maris Prep Academy from liability for any reasonable risks associated with this activity, and agree to be reached at the emergency contact number listed.

Parent / Guardian Signature
Date
Printed Name
Relationship
`; } /* ---------- Daily Reflection ---------- */ function renderReflectTemplate(config, printArea, name, date){ config.innerHTML = `

Reflection Settings

`; const type = document.getElementById('cfgReflectType')?.value || 'daily'; const topic = document.getElementById('cfgReflectTopic')?.value || ''; const typeLabel = {daily:'Daily Reflection', weekly:'Weekly Reflection', topic:'Topic Reflection'}[type]; printArea.innerHTML = `

${typeLabel}

${topic ? escapeHtml(topic) : 'Take a moment to think about your learning.'}
${headerRow(name, date)}

What Went Well Today?

What Was Tricky or Hard?

Something New I Learned

Question I Still Have

One Thing I'll Try Tomorrow

How am I feeling about learning today?
๐Ÿ˜Ÿ   ๐Ÿ˜   ๐Ÿ™‚   ๐Ÿ˜Š   ๐Ÿคฉ
struggling okay good great amazing
`; } /* ---------- Behavior Chart ---------- */ function renderBehaviorTemplate(config, printArea, name, date){ config.innerHTML = `

Behavior Chart Settings

`; const type = document.getElementById('cfgBehavType')?.value || 'week'; const goals = (document.getElementById('cfgBehavGoals')?.value || '').split(',').map(s=>s.trim()).filter(Boolean).slice(0,6); const days = type === 'month' ? 30 : 5; const dayLabels = type === 'month' ? Array.from({length:30}, (_,i) => (i+1).toString()) : ['Monday','Tuesday','Wednesday','Thursday','Friday']; printArea.innerHTML = `

${type==='month'?'Monthly':'Weekly'} Behavior Chart

Color in a star for each goal met
${headerRow(name, date)} ${dayLabels.map(d => ``).join('')} ${goals.map(g => ` ${dayLabels.map(() => ``).join('')} `).join('')}
Goal${d}
${escapeHtml(g)}โ˜†

Stars Earned This ${type==='month'?'Month':'Week'}

Reward Earned

Notes & Encouragement

Student Signature
Parent Signature
`; } let activeBooksTab = 'workbooks'; let readingFilter = 'all'; function renderBooks(){ // Wire tab chips document.querySelectorAll('[data-books-tab]').forEach(c => { if (c.dataset.wired) return; c.dataset.wired = '1'; c.onclick = () => setBooksTab(c.dataset.booksTab); }); document.querySelectorAll('[data-reading-status]').forEach(c => { if (c.dataset.wired) return; c.dataset.wired = '1'; c.onclick = () => setReadingFilter(c.dataset.readingStatus); }); // Show the active tab document.getElementById('booksTabWorkbooks').style.display = activeBooksTab==='workbooks' ? '' : 'none'; document.getElementById('booksTabReading').style.display = activeBooksTab==='reading' ? '' : 'none'; document.getElementById('booksTabLog').style.display = activeBooksTab==='log' ? '' : 'none'; // Show right add button const addBookBtn = document.getElementById('addBookBtn'); const addReadingBtn = document.getElementById('addReadingBtn'); if (addBookBtn) addBookBtn.style.display = activeBooksTab==='workbooks' ? '' : 'none'; if (addReadingBtn) addReadingBtn.style.display = activeBooksTab==='reading' ? '' : 'none'; renderWorkbooks(); renderReadingList(); renderReadingLog(); } function setBooksTab(t){ activeBooksTab = t; document.querySelectorAll('[data-books-tab]').forEach(c => c.classList.toggle('active', c.dataset.booksTab===t)); renderBooks(); } function setReadingFilter(f){ readingFilter = f; document.querySelectorAll('[data-reading-status]').forEach(c => c.classList.toggle('active', c.dataset.readingStatus===f)); renderReadingList(); } function renderWorkbooks(){ const wrap = document.getElementById('booksList'); if (!wrap) return; const books = state.workbooks || []; if (!books.length) { wrap.innerHTML = `

No workbooks added yet. Click "Add Workbook" to get started.

`; return; } // Group by subject const grouped = {}; books.forEach(b => { const key = b.subject || 'general'; if (!grouped[key]) grouped[key] = []; grouped[key].push(b); }); const order = ['math','ela','science','social','religion','spanish','music','art','general']; wrap.innerHTML = order.filter(k => grouped[k]).map(subj => { const subjLabel = subj==='general' ? 'General / Cross-Curricular' : (SUBJECTS[subj]?.name || subj); const subjIcon = subj==='general' ? '๐Ÿ“š' : (SUBJECTS[subj]?.icon || '๐Ÿ“–'); return `

${subjIcon} ${subjLabel} (${grouped[subj].length})

${grouped[subj].map(b => `
๐Ÿ“–
${escapeHtml(b.title)}
${b.publisher ? `
${escapeHtml(b.publisher)}
` : ''} ${b.notes ? `
${escapeHtml(b.notes)}
` : ''}
`).join('')}
`; }).join(''); } function openBookEditor(id){ window._editingBook = id || null; const b = id ? state.workbooks.find(x => x.id === id) : {}; document.getElementById('bkTitle').value = b?.title || ''; document.getElementById('bkSubject').value = b?.subject || 'math'; document.getElementById('bkPublisher').value = b?.publisher || ''; document.getElementById('bkNotes').value = b?.notes || ''; document.getElementById('bkColor').value = b?.coverColor || '#ff8aa8'; document.getElementById('bookEditorTitle').textContent = id ? 'Edit Book' : 'Add Book'; openModal('bookEditorModal'); } function saveBook(){ const title = document.getElementById('bkTitle').value.trim(); if (!title) { toast('Title required'); return; } const book = { id: window._editingBook || uid(), title, subject: document.getElementById('bkSubject').value, publisher: document.getElementById('bkPublisher').value.trim(), notes: document.getElementById('bkNotes').value.trim(), coverColor: document.getElementById('bkColor').value }; if (window._editingBook) { const idx = state.workbooks.findIndex(x => x.id === book.id); if (idx >= 0) state.workbooks[idx] = book; else state.workbooks.push(book); } else { state.workbooks.push(book); } save(); closeModal(); toast('Book saved'); renderBooks(); } function editBook(id){ openBookEditor(id); } function deleteBook(id){ if (!confirm('Delete this book?')) return; state.workbooks = state.workbooks.filter(b => b.id !== id); save(); renderBooks(); } /* ============================================================ READING LIST ============================================================ */ const READING_STATUS_META = { assigned: {icon:'๐Ÿ“Œ', label:'Assigned', color:'linear-gradient(135deg,var(--pink),var(--coral))'}, free: {icon:'๐ŸŒŸ', label:'Free Reading', color:'linear-gradient(135deg,var(--aqua),var(--turq))'}, recommended: {icon:'๐Ÿ’ก', label:'Recommended', color:'linear-gradient(135deg,var(--gold),var(--coral))'}, reading: {icon:'๐Ÿ“–', label:'Reading Now', color:'linear-gradient(135deg,#9774d8,#6b3fc7)'}, finished: {icon:'โœ…', label:'Finished', color:'linear-gradient(135deg,#7dd87d,#2a8a3e)'} }; function renderReadingList(){ const wrap = document.getElementById('readingList'); if (!wrap) return; const books = state.readingList || []; if (!books.length) { wrap.innerHTML = `

No books in the reading list yet. Click "Add Book" to start building Allegra's reading library!

`; return; } const filtered = readingFilter === 'all' ? books : books.filter(b => b.status === readingFilter); if (!filtered.length) { wrap.innerHTML = `

No books in this category. Try another filter.

`; return; } // Group by status when showing all if (readingFilter === 'all') { const order = ['reading','assigned','free','recommended','finished']; const grouped = {}; filtered.forEach(b => { const s = b.status || 'recommended'; if (!grouped[s]) grouped[s] = []; grouped[s].push(b); }); wrap.innerHTML = order.filter(s => grouped[s]).map(s => `

${READING_STATUS_META[s].icon} ${READING_STATUS_META[s].label} (${grouped[s].length})

${grouped[s].map(b => readingBookCard(b)).join('')}
`).join(''); } else { wrap.innerHTML = `
${filtered.map(b => readingBookCard(b)).join('')}
`; } } function readingBookCard(b){ const meta = READING_STATUS_META[b.status] || READING_STATUS_META.recommended; const totalRead = (state.readingLog || []).filter(l => l.bookId === b.id).reduce((sum, l) => sum + (l.minutes || 0), 0); return `
${meta.icon}
${escapeHtml(b.title)}
${b.author ? `
by ${escapeHtml(b.author)}
` : ''}
${escapeHtml(b.category||'fiction')} ${b.ageMin ? `Ages ${b.ageMin}โ€“${b.ageMax}` : ''} ${b.pages ? `${b.pages} pp` : ''}
${b.rating ? `
${'โญ'.repeat(parseInt(b.rating))}
` : ''} ${b.review ? `
"${escapeHtml(b.review)}"
` : ''} ${totalRead > 0 ? `
๐Ÿ“Š ${totalRead} min logged
` : ''}
${b.status !== 'finished' ? `` : ''}
`; } function openReadingBookEditor(id){ window._editingReadingBook = id || null; const b = id ? state.readingList.find(x => x.id === id) : {}; document.getElementById('rbTitle').value = b?.title || ''; document.getElementById('rbAuthor').value = b?.author || ''; document.getElementById('rbPages').value = b?.pages || ''; document.getElementById('rbStatus').value = b?.status || 'recommended'; document.getElementById('rbCategory').value = b?.category || 'fiction'; document.getElementById('rbAgeRange').value = (b?.ageMin && b?.ageMax) ? `${b.ageMin}-${b.ageMax}` : '7-10'; document.getElementById('rbRating').value = b?.rating || ''; document.getElementById('rbReview').value = b?.review || ''; document.getElementById('readingBookEditorTitle').textContent = id ? 'Edit Book' : 'Add Book to Reading List'; openModal('readingBookEditorModal'); } function editReadingBook(id){ openReadingBookEditor(id); } function saveReadingBook(){ const title = document.getElementById('rbTitle').value.trim(); if (!title) { toast('Title required'); return; } const ageRange = document.getElementById('rbAgeRange').value.split('-'); const book = { id: window._editingReadingBook || uid(), title, author: document.getElementById('rbAuthor').value.trim(), pages: parseInt(document.getElementById('rbPages').value) || null, status: document.getElementById('rbStatus').value, category: document.getElementById('rbCategory').value, ageMin: parseInt(ageRange[0]), ageMax: parseInt(ageRange[1]), rating: document.getElementById('rbRating').value || null, review: document.getElementById('rbReview').value.trim(), addedAt: window._editingReadingBook ? null : new Date().toISOString() }; if (book.status === 'finished' && !state.readingList.find(b => b.id === book.id)?.finishedAt) { book.finishedAt = new Date().toISOString(); } if (window._editingReadingBook) { const idx = state.readingList.findIndex(x => x.id === book.id); if (idx >= 0) state.readingList[idx] = {...state.readingList[idx], ...book}; } else { state.readingList.push(book); } save(); closeModal(); toast('Book saved'); renderBooks(); } function markBookFinished(id){ const b = state.readingList.find(x => x.id === id); if (!b) return; b.status = 'finished'; b.finishedAt = new Date().toISOString(); save(); toast('Marked as finished ๐ŸŽ‰'); renderBooks(); } function deleteReadingBook(id){ if (!confirm('Delete this book from the reading list? Reading log entries will be kept.')) return; state.readingList = state.readingList.filter(b => b.id !== id); save(); renderBooks(); } /* ============================================================ READING LOG ============================================================ */ function renderReadingLog(){ const wrap = document.getElementById('readingLogList'); const statsWrap = document.getElementById('readingLogStats'); if (!wrap) return; const log = state.readingLog || []; // Stats const tNow = today(); const sevenAgo = addDays(tNow, -7); const thirtyAgo = addDays(tNow, -30); const minutes7 = log.filter(l => parseDate(l.date) >= sevenAgo).reduce((sum,l) => sum + (l.minutes||0), 0); const minutes30 = log.filter(l => parseDate(l.date) >= thirtyAgo).reduce((sum,l) => sum + (l.minutes||0), 0); const totalMinutes = log.reduce((sum,l) => sum + (l.minutes||0), 0); const totalSessions = log.length; const stats = [ {label:'This Week', value:minutes7 + ' min', icon:'๐Ÿ“…', bg:'linear-gradient(135deg,var(--pink),var(--coral))'}, {label:'Past 30 Days', value:minutes30 + ' min', icon:'๐Ÿ—“๏ธ', bg:'linear-gradient(135deg,var(--aqua),var(--turq))'}, {label:'Total Time', value:Math.round(totalMinutes/60) + ' hr', icon:'โฑ๏ธ', bg:'linear-gradient(135deg,var(--gold),var(--coral))'}, {label:'Sessions', value:totalSessions, icon:'๐Ÿ“Š', bg:'linear-gradient(135deg,#9774d8,#6b3fc7)'} ]; if (statsWrap) { statsWrap.innerHTML = stats.map(s => `
${s.icon}
${s.value}
${s.label}
`).join(''); } if (!log.length) { wrap.innerHTML = `

No reading sessions logged yet. Click "Log Reading Session" to track daily reading.

`; return; } const sorted = [...log].sort((a,b) => b.date.localeCompare(a.date)); wrap.innerHTML = `
${sorted.map(entry => { const book = state.readingList.find(b => b.id === entry.bookId); const bookName = book ? book.title : 'โ€” unknown โ€”'; const pages = (entry.pagesFrom && entry.pagesTo) ? `${entry.pagesFrom}โ€“${entry.pagesTo}` : (entry.pagesTo || 'โ€”'); return ` `; }).join('')}
Date Book Pages Minutes Notes
${fmtShort(parseDate(entry.date))} ${bookName} ${pages} ${entry.minutes || 'โ€”'} ${escapeHtml(entry.notes || '')}
`; } function openReadingLogEditor(){ window._loggingForBook = null; document.getElementById('rlDate').value = isoDate(today()); document.getElementById('rlMinutes').value = ''; document.getElementById('rlPagesFrom').value = ''; document.getElementById('rlPagesTo').value = ''; document.getElementById('rlNotes').value = ''; // Populate book dropdown const bookSelect = document.getElementById('rlBook'); const books = state.readingList || []; if (!books.length) { bookSelect.innerHTML = ''; } else { bookSelect.innerHTML = '' + books.map(b => ``).join(''); } openModal('readingLogEditorModal'); } function logReadingFor(bookId){ openReadingLogEditor(); document.getElementById('rlBook').value = bookId; } function saveReadingLog(){ const date = document.getElementById('rlDate').value; const bookId = document.getElementById('rlBook').value; const minutes = parseInt(document.getElementById('rlMinutes').value); if (!date || !minutes) { toast('Date and minutes required'); return; } const entry = { id: uid(), bookId, date, minutes, pagesFrom: parseInt(document.getElementById('rlPagesFrom').value) || null, pagesTo: parseInt(document.getElementById('rlPagesTo').value) || null, notes: document.getElementById('rlNotes').value.trim(), createdAt: new Date().toISOString() }; state.readingLog.push(entry); // If a book was picked and it was "assigned" or "recommended", auto-bump to "reading" if (bookId) { const book = state.readingList.find(b => b.id === bookId); if (book && (book.status === 'recommended' || book.status === 'assigned' || book.status === 'free')) { book.status = 'reading'; book.startedAt = book.startedAt || new Date().toISOString(); } } save(); closeModal(); toast('Reading session logged ๐Ÿ“š'); renderBooks(); } function deleteReadingLog(id){ if (!confirm('Delete this log entry?')) return; state.readingLog = state.readingLog.filter(l => l.id !== id); save(); renderBooks(); } /* ============================================================ LESSON DONE + TIMER + WORKSHEETS ============================================================ */ let activeLessonTimer = null; // {key, startTime, intervalId} function getLessonStatus(dateKey, subj){ const key = `${dateKey}:${subj}`; return state.lessonStatus[currentGrade]?.[key] || {done:false, durationMs:0, worksheets:[]}; } function setLessonStatus(dateKey, subj, updates){ if (!state.lessonStatus[currentGrade]) state.lessonStatus[currentGrade] = {}; const key = `${dateKey}:${subj}`; state.lessonStatus[currentGrade][key] = {...getLessonStatus(dateKey, subj), ...updates}; } function toggleLessonDone(dateKey, subj){ const cur = getLessonStatus(dateKey, subj); const done = !cur.done; setLessonStatus(dateKey, subj, {done, doneAt:done ? new Date().toISOString() : null}); if (done && activeLessonTimer && activeLessonTimer.key === `${dateKey}:${subj}`) { stopLessonTimer(); } save(); if (done) celebrate(); renderLessons(); renderDashboard(); } function startLessonTimer(dateKey, subj){ if (activeLessonTimer) stopLessonTimer(); const key = `${dateKey}:${subj}`; activeLessonTimer = {key, startTime:Date.now(), intervalId:null}; // Update UI every second activeLessonTimer.intervalId = setInterval(() => { const timerEls = document.querySelectorAll(`[data-timer-key="${key}"]`); timerEls.forEach(el => { const cur = getLessonStatus(dateKey, subj); const elapsed = (cur.durationMs || 0) + (Date.now() - activeLessonTimer.startTime); el.textContent = formatDuration(elapsed); }); }, 1000); toast('Timer started โฑ๏ธ'); renderLessons(); } function stopLessonTimer(){ if (!activeLessonTimer) return; const [dateKey, subj] = activeLessonTimer.key.split(':'); const cur = getLessonStatus(dateKey, subj); const elapsed = Date.now() - activeLessonTimer.startTime; setLessonStatus(dateKey, subj, {durationMs: (cur.durationMs || 0) + elapsed}); clearInterval(activeLessonTimer.intervalId); activeLessonTimer = null; save(); toast('Timer stopped โ€” time saved'); renderLessons(); } function formatDuration(ms){ const totalSec = Math.floor(ms / 1000); const h = Math.floor(totalSec / 3600); const m = Math.floor((totalSec % 3600) / 60); const s = totalSec % 60; if (h > 0) return `${h}:${String(m).padStart(2,'0')}:${String(s).padStart(2,'0')}`; return `${m}:${String(s).padStart(2,'0')}`; } function handleLessonWorksheetUpload(e){ const files = [...e.target.files]; if (!files.length) return; if (!window._pendingLessonWorksheets) window._pendingLessonWorksheets = []; Promise.all(files.map(async f => { if (f.size > 4 * 1024 * 1024) { toast(`"${f.name}" too large (4MB max)`); return null; } const data = await fileToDataURL(f); return {name:f.name, type:f.type, data, uploadedAt:new Date().toISOString()}; })).then(results => { const valid = results.filter(Boolean); window._pendingLessonWorksheets.push(...valid); renderPendingWorksheets(); if (valid.length) toast(`${valid.length} worksheet(s) ready to save`); }); e.target.value = ''; } function renderPendingWorksheets(){ const wrap = document.getElementById('leWorksheets'); if (!wrap) return; const btn = wrap.querySelector('.upload-btn'); wrap.querySelectorAll('.upload-thumb').forEach(t => t.remove()); // existing worksheets from editing lesson let existing = []; if (window._editingLesson) { const {dateKey, subj} = window._editingLesson; existing = getLessonStatus(dateKey, subj).worksheets || []; } [...existing, ...(window._pendingLessonWorksheets||[])].forEach(u => { const thumb = document.createElement('div'); thumb.className = 'upload-thumb'; thumb.innerHTML = u.type?.startsWith('image/') ? `` : '๐Ÿ“„'; thumb.title = u.name; wrap.insertBefore(thumb, btn); }); } function downloadLessonWorksheet(dateKey, subj, idx){ const cur = getLessonStatus(dateKey, subj); const w = cur.worksheets?.[idx]; if (!w) return; const a = document.createElement('a'); a.href = w.data; a.download = w.name; a.click(); } /* ============================================================ IXL IMPORT ============================================================ */ function importIxl(){ const text = document.getElementById('ixlPaste').value.trim(); const mastered = parseInt(document.getElementById('ixlMastered').value) || 0; const proficient = parseInt(document.getElementById('ixlProficient').value) || 0; if (!text && !mastered && !proficient) { toast('Paste IXL text or enter counts'); return; } // Parse lines โ€” flexible regex looking for "Subject | Skill | Code | Date" patterns // The IXL summary format alternates: Subject\nSkill name\nCode\nDate const lines = text.split('\n').map(s => s.trim()).filter(Boolean); const subjects = ['Math','ELA','Science','Social Studies','Spanish']; const subjMap = {'Math':'math','ELA':'ela','Science':'science','Social Studies':'social','Spanish':'spanish'}; const skillsBySubj = {math:[],ela:[],science:[],social:[],spanish:[]}; let i = 0; while (i < lines.length) { const subj = lines[i]; if (subjects.includes(subj)) { const skill = lines[i+1] || ''; const code = lines[i+2] || ''; const date = lines[i+3] || ''; if (skill && date) { skillsBySubj[subjMap[subj]].push({skill, code, date}); i += 4; continue; } } i++; } const totalSkills = Object.values(skillsBySubj).reduce((sum,arr) => sum + arr.length, 0); // Mark standards as mastered/in-progress based on keyword matching let markedMastered = 0, markedProgress = 0; const standards = state.standards[currentGrade] || []; standards.forEach(std => { // Try to find a matching skill in skillsBySubj[std.subj] const subjSkills = skillsBySubj[std.subj] || []; if (!subjSkills.length) return; const descLower = std.desc.toLowerCase(); const keywords = extractKeywords(descLower); const matched = subjSkills.find(s => { const sk = s.skill.toLowerCase(); return keywords.some(kw => sk.includes(kw)); }); if (matched) { // If we have enough mastered skills overall, prefer mastered status const curStatus = state.standardsStatus[currentGrade]?.[std.code]; if (!curStatus || curStatus === 'new') { state.standardsStatus[currentGrade][std.code] = 'mastered'; markedMastered++; } else if (curStatus === 'progress') { state.standardsStatus[currentGrade][std.code] = 'mastered'; markedMastered++; } } }); // Add summary grade entries โ€” one per subject with skills const today_ = isoDate(today()); Object.keys(skillsBySubj).forEach(subj => { const skills = skillsBySubj[subj]; if (!skills.length) return; state.grades[currentGrade].push({ id: uid(), title: `IXL Import: ${skills.length} skills (${skills.slice(0,3).map(s=>s.skill.slice(0,30)).join(', ')}${skills.length>3?'โ€ฆ':''})`, subj, date: today_, score: skills.length, total: skills.length, imported: true, importDate: today_ }); }); save(); closeModal(); toast(`Imported ${totalSkills} skills ยท Marked ${markedMastered} standards as mastered`); renderStandards(); renderGradebook(); renderProgress(); } function extractKeywords(text){ // Extract content words for keyword matching const stop = new Set(['and','or','of','the','a','an','to','in','on','with','for','from','by','as','at','is','are','be','it','that','this','these','those']); return text.split(/[^a-z0-9]+/).filter(w => w.length >= 4 && !stop.has(w)); } /* ============================================================ EDIT GRADE ============================================================ */ function editGrade(id){ const g = state.grades[currentGrade].find(x => x.id === id); if (!g) return; window._editingGrade = id; document.getElementById('ngModalTitle').textContent = 'Edit Grade'; document.getElementById('ngTitle').value = g.title; document.getElementById('ngSubject').value = g.subj; document.getElementById('ngDate').value = g.date; document.getElementById('ngScore').value = g.score; document.getElementById('ngTotal').value = g.total; openModal('addGradeModal'); } /* ============================================================ CALENDAR VIEW MODES (Month / Week / Agenda) ============================================================ */ let calView = 'month'; function setCalView(v){ calView = v; document.querySelectorAll('.cal-view-switch button').forEach(b => b.classList.toggle('active', b.dataset.view===v)); document.getElementById('calMonthWrap').style.display = v==='month' ? '' : 'none'; document.getElementById('calWeekWrap').style.display = v==='week' ? '' : 'none'; document.getElementById('calAgendaWrap').style.display = v==='agenda' ? '' : 'none'; renderCalendar(); } function calNav(n){ if (calView === 'week') { calCursor = addDays(calCursor, n*7); } else if (calView === 'agenda') { calCursor = addDays(calCursor, n*7); } else { calCursor = new Date(calCursor.getFullYear(), calCursor.getMonth()+n, 1); } renderCalendar(); } function renderWeekView(){ const wrap = document.getElementById('calWeekView'); if (!wrap) return; const monday = getWeekMonday(calCursor); const tKey = isoDate(today()); let html = ''; for (let d=0; d<5; d++){ const day = addDays(monday, d); const key = isoDate(day); const dayClasses = ['wv-day']; if (key === tKey) dayClasses.push('today'); const hol = holidayOn(key); const items = (state.assignments[currentGrade][key] || []); const events = (state.events[currentGrade] || []).filter(e => e.date===key); if (hol) dayClasses.push('holiday'); html += `
${day.toLocaleDateString('en-US',{weekday:'short'})}
${day.getDate()}
${hol ? `
๐ŸŽ‰ ${escapeHtml(hol.name)}
` : ''} ${items.slice(0,8).map(it => `
${SUBJECTS[it.subj]?.icon} ${SUBJECTS[it.subj]?.name}
${escapeHtml(it.title.slice(0,40))}${it.title.length>40?'โ€ฆ':''}
`).join('')} ${events.map(e => `
๐Ÿ“Œ ${escapeHtml(e.title)}
`).join('')}
`; } wrap.innerHTML = html; // Update label const friday = addDays(monday, 4); document.getElementById('calMonthLabel').textContent = `${fmtShort(monday)} โ€“ ${fmtShort(friday)}`; } function renderAgendaView(){ const wrap = document.getElementById('calAgendaWrap'); if (!wrap) return; // Show 7 days starting from calCursor const startD = new Date(calCursor); startD.setHours(0,0,0,0); const tKey = isoDate(today()); const milestones = getMilestones(currentGrade); let html = ''; let weekHasAnything = false; for (let i=0; i<7; i++){ const day = addDays(startD, i); const key = isoDate(day); const items = state.assignments[currentGrade][key] || []; const events = (state.events[currentGrade] || []).filter(e => e.date===key); const ms = milestones.filter(m => m.date===key); const hol = holidayOn(key); const isToday = key === tKey; if (items.length || events.length || ms.length) weekHasAnything = true; const dayClasses = ['agenda-date-box']; if (isToday) dayClasses.push('today'); if (hol) dayClasses.push('holiday'); html += `
${day.toLocaleDateString('en-US',{weekday:'short'})}
${day.getDate()}
${day.toLocaleDateString('en-US',{weekday:'long', month:'long', day:'numeric'})}${isToday?' ยท Today':''}
${items.length} lesson${items.length===1?'':'s'} ยท ${items.filter(x=>x.done).length} done${events.length?' ยท '+events.length+' event'+(events.length===1?'':'s'):''}
${hol ? `
๐ŸŽ‰ ${escapeHtml(hol.name)} โ€” No school
` : ''} ${ms.map(m => `
โญ ${escapeHtml(m.title)}
`).join('')} ${events.map(e => `
๐Ÿ“Œ ${escapeHtml(e.title)}
event
`).join('')} ${items.map(it => `
${SUBJECTS[it.subj]?.icon}
${escapeHtml(it.title)}
${it.done?'done':''}
`).join('')} ${(!items.length && !events.length && !ms.length && !hol) ? `

Nothing scheduled.

` : ''}
`; } wrap.innerHTML = html; // Helpful empty-week banner: explain when the visible week is outside the school year. if (!weekHasAnything){ const yr = { 3:'September 2025 โ€“ May 2026', 4:'August 17, 2026 โ€“ June 2027', bridge:'June โ€“ August 2026' }[currentGrade] || ''; const gLabel = currentGrade==='bridge'?'Bridge':`Grade ${currentGrade}`; const banner = `
๐Ÿ“… No lessons this week. The ${gLabel} school year runs ${yr}. Use Prev / Next above to move to a week within the year, or switch grade at the top-left.
`; wrap.innerHTML = banner + wrap.innerHTML; } document.getElementById('calMonthLabel').textContent = `${fmtShort(startD)} โ€“ ${fmtShort(addDays(startD, 6))}`; } /* ============================================================ MONTH-AT-A-GLANCE PRINT (agenda style, what's being taught) ============================================================ */ function printMonthAgenda(){ const base = new Date(calCursor); const year = base.getFullYear(); const month = base.getMonth(); const monthName = base.toLocaleDateString('en-US',{month:'long', year:'numeric'}); const studentName = (state.student && state.student.name) || 'Student'; const gradeLabel = currentGrade==='bridge' ? 'Bridge' : `Grade ${currentGrade}`; const milestones = getMilestones(currentGrade); const daysInMonth = new Date(year, month+1, 0).getDate(); let daysHtml = ''; for (let dnum=1; dnum<=daysInMonth; dnum++){ const day = new Date(year, month, dnum); const key = isoDate(day); const dow = day.getDay(); // 0 Sun .. 6 Sat const hol = holidayOn(key); const ms = milestones.filter(m => m.date===key); const events = (state.events[currentGrade] || []).filter(ev => ev.date===key); const items = (state.assignments[currentGrade][key] || []); // Skip weekends that have nothing on them if ((dow===0 || dow===6) && !items.length && !events.length && !ms.length && !hol) continue; const dowLabel = day.toLocaleDateString('en-US',{weekday:'long'}); let rows = ''; if (hol) rows += `
๐ŸŽ‰ ${escapeHtml(hol.name)} โ€” No school
`; ms.forEach(m => rows += `
โญ ${escapeHtml(m.title)}
`); events.forEach(ev => rows += `
๐Ÿ“Œ ${escapeHtml(ev.title)}
`); // Group lessons by subject, pulling detailed plan for assignment + standards items.forEach(it => { const detail = getLessonForDate(key, it.subj, currentGrade) || {}; const subjName = SUBJECTS[it.subj]?.name || it.subj; const stds = (detail.standards && detail.standards.length) ? detail.standards.join(', ') : ''; const assign = detail.assignment || ''; rows += `
${escapeHtml(subjName)}
${escapeHtml(it.title)}${it.isTest?' TEST':''}
${assign?`
${escapeHtml(assign)}
`:''} ${stds?`
NC: ${escapeHtml(stds)}
`:''}
`; }); if (!rows) rows = `
No lessons scheduled.
`; daysHtml += `
${dnum}${dowLabel}
${rows}
`; } const html = `${monthName} โ€” ${studentName}

${monthName}

${escapeHtml(studentName)} ยท ${gradeLabel} ยท Stella Maris Prep Academy โ€” Month at a Glance
${daysHtml || '

No school days scheduled this month.

'}
Generated ${new Date().toLocaleDateString()} ยท allegra.edenlord.com
`; const w = window.open('', '_blank'); if (!w){ toast('Pop-up blocked โ€” allow pop-ups to print'); return; } w.document.open(); w.document.write(html); w.document.close(); } /* ============================================================ AUTO BACKUPS ============================================================ */ function autoBackupCheck(){ const now = Date.now(); const last = state.lastAutoBackup ? new Date(state.lastAutoBackup).getTime() : 0; // 12 hours if (now - last < 12 * 3600 * 1000) return; performAutoBackup(); } function performAutoBackup(){ try { const snapshot = JSON.stringify(state); if (!state.autoBackups) state.autoBackups = []; state.autoBackups.unshift({ts:new Date().toISOString(), size:snapshot.length, blob:snapshot}); // Keep last 4 (covers 2 days at 2/day) state.autoBackups = state.autoBackups.slice(0, 4); state.lastAutoBackup = new Date().toISOString(); save(); console.log('Auto-backup completed at', state.lastAutoBackup); } catch(e) { console.warn('Auto-backup failed:', e); } } function restoreAutoBackup(idx){ const bk = state.autoBackups?.[idx]; if (!bk) return; if (!confirm(`Restore backup from ${new Date(bk.ts).toLocaleString()}? Current data will be replaced.`)) return; try { const restored = JSON.parse(bk.blob); state = Object.assign(defaultState(), restored); currentGrade = state.currentGrade || 3; save(); toast('Backup restored'); renderAll(); } catch(e) { toast('Could not restore backup'); } } /* ============================================================ TASK LIST SWITCHER (Shared vs Private) ============================================================ */ let activeTaskList = 'shared'; function setTaskList(t){ if (t === 'private' && state.currentMode !== 'mom') { toast('Private list is mom-only'); return; } activeTaskList = t; document.querySelectorAll('[data-task-list]').forEach(c => c.classList.toggle('active', c.dataset.taskList===t)); renderTasks(); } /* ============================================================ ATTENDANCE FILTER ============================================================ */ function setAttFilter(f){ attFilter = f; document.querySelectorAll('[data-att-filter]').forEach(c => c.classList.toggle('active', c.dataset.attFilter===f)); renderAttFilterResults(); } function renderAttFilterResults(){ const wrap = document.getElementById('attFilterResults'); const list = document.getElementById('attFilterList'); const title = document.getElementById('attFilterTitle'); if (!wrap) return; if (attFilter === 'all') { wrap.style.display = 'none'; return; } wrap.style.display = ''; const att = state.attendance[currentGrade] || {}; const tKey = isoDate(today()); let results = []; if (attFilter === 'absent') { title.textContent = 'Absent Days'; results = Object.keys(att).filter(k => att[k] === 'absent').sort(); } else if (attFilter === 'excused') { title.textContent = 'Excused Days'; results = Object.keys(att).filter(k => att[k] === 'excused').sort(); } else if (attFilter === 'unmarked') { title.textContent = 'Unmarked Past School Days'; // Walk back from today and find school days without attendance const yearStart = currentGrade==='bridge' ? new Date(2026,5,8) : (currentGrade===3 ? new Date(2025,8,1) : new Date(2026,7,1)); let d = new Date(yearStart); const tD = today(); while (d <= tD) { const k = isoDate(d); if (isSchoolDay(d, currentGrade) && !att[k]) results.push(k); d = addDays(d, 1); } } if (!results.length) { list.innerHTML = `

None found ๐ŸŒธ

`; return; } list.innerHTML = results.map(k => { const d = parseDate(k); const status = att[k]; return `
${d.toLocaleDateString('en-US',{weekday:'short'})}
${d.getDate()}
${fmtLong(d)}
${status ? `${status}` : 'unmarked'}
`; }).join(''); } /* ============================================================ PROGRESS TILE NAVIGATION (clickable) ============================================================ */ function progressTileToSubject(subj){ // Switch to lessons page with that subject tab lessonsTab = subj; showPage('lessons'); } function curriculumWeekToLessons(weekIdx){ lessonsWeekIdx = weekIdx; lessonsTab = 'math'; showPage('lessons'); } /* ============================================================ INIT ============================================================ */ document.querySelectorAll('.nav-item').forEach(b => b.onclick = () => showPage(b.dataset.page)); document.querySelectorAll('.modal-bg').forEach(m => m.addEventListener('click', e => { if (e.target===m) closeModal(); })); document.addEventListener('keydown', e => { if (e.key==='Escape') { closeModal(); closeSidebar(); }}); document.getElementById('menuToggle')?.addEventListener('click', toggleSidebar); // Mood picker delegate document.querySelectorAll('#moodPicker .mood-opt').forEach(m => { m.addEventListener('click', () => { document.querySelectorAll('#moodPicker .mood-opt').forEach(x => x.classList.remove('selected')); m.classList.add('selected'); window._selectedMood = m.dataset.mood; }); }); // Seed all grades seedCurriculum(3); seedCurriculum(4); seedCurriculum('bridge'); seedDefaultWorkbooks(); seedHistoryYear1(); seedScienceYear4(); seedMathYear4(); seedElaYear4(); seedSocialYear4(); seedSpanishYear4(); seedReligionYear4(); seedArtYear4(); seedMusicYear4(); seedFineArtsResources(); seedEnglishPapers(); seedSpellingSessions(); // Set initial calendar cursor to a useful month const t0 = today(); calCursor = new Date(t0.getFullYear(), t0.getMonth(), 1); // If we're before September 2025, jump to it so the 3rd grade calendar shows content if (currentGrade===3 && t0 < new Date(2025,8,1)) calCursor = new Date(2025,8,1); if (currentGrade===3 && t0 > new Date(2026,4,31)) calCursor = new Date(2025,8,1); // Apply active grade button state document.querySelectorAll('.grade-switch button').forEach(b => { const dg = b.dataset.grade; const matches = (dg === 'bridge' && currentGrade === 'bridge') || (+dg === +currentGrade); b.classList.toggle('active', matches); }); // Auto-backups: run check on load, then every hour autoBackupCheck(); setInterval(autoBackupCheck, 60 * 60 * 1000); // Restore sidebar collapsed preference (desktop) if (state.sidebarCollapsed && window.innerWidth > 760) { document.getElementById('app').classList.add('sidebar-collapsed'); document.getElementById('menuToggle')?.classList.add('show-collapsed'); } // Show login overlay on first load (or whenever no mode set) // Auto-login as mom if no passcode set yet (for first-time use) if (state.momPinHash) { // Default to kid mode while login overlay is up so mom-only stuff is hidden state.currentMode = state.currentMode || 'kid'; document.getElementById('loginOverlay').classList.add('show'); } else { // First-time use: auto-login as mom state.currentMode = 'mom'; save(); } // Apply current mode UI now that we know which mode applyModeUI(); renderAll();