diff --git "a/index.html" "b/index.html" --- "a/index.html" +++ "b/index.html" @@ -106,6 +106,10 @@ h1.hero-title span{color:var(--accent)} .path-title{font-family:var(--font-head);font-size:1.2rem;font-weight:800} .streak-badge{display:flex;align-items:center;gap:6px;background:#fef3c7;border:1px solid #fbbf24;color:#92400e;padding:5px 12px;border-radius:99px;font-size:.8rem;font-weight:600} [data-theme="dark"] .streak-badge{background:#2d1f00;border-color:#92400e;color:#fbbf24} +/* FIX: path complete badge */ +.path-complete-badge{display:none;align-items:center;gap:6px;background:#d1fae5;border:1px solid #a7f3d0;color:#065f46;padding:5px 14px;border-radius:99px;font-size:.82rem;font-weight:700} +.path-complete-badge.show{display:flex} +[data-theme="dark"] .path-complete-badge{background:#064e3b;border-color:#065f46;color:#6ee7b7} .path-progress-bar-wrap{flex:1;min-width:100px;max-width:190px;background:var(--border);border-radius:99px;height:7px;overflow:hidden} .path-progress-bar-fill{height:100%;background:var(--accent3);border-radius:99px;transition:width .4s} .path-progress-label{font-size:.78rem;color:var(--muted);white-space:nowrap} @@ -121,6 +125,7 @@ h1.hero-title span{color:var(--accent)} .timeline-dot{flex-shrink:0;width:28px;height:28px;border-radius:50%;background:var(--surface);border:2px solid var(--border);display:flex;align-items:center;justify-content:center;font-size:.62rem;font-weight:700;color:var(--muted);cursor:pointer;transition:all .15s} .timeline-dot.done{background:#d1fae5;border-color:var(--accent3);color:#065f46} .timeline-dot.today{background:#dbeafe;border-color:var(--accent);color:#1e40af} +.timeline-dot.active-dot{border-color:var(--accent2);box-shadow:0 0 0 2px var(--accent2)} [data-theme="dark"] .timeline-dot.done{background:#064e3b;color:#6ee7b7} [data-theme="dark"] .timeline-dot.today{background:#1e3a5f;color:#93c5fd} .timeline-dot:hover{border-color:var(--accent)} @@ -186,10 +191,15 @@ h1.hero-title span{color:var(--accent)} .page-title{font-family:var(--font-head);font-size:2rem;font-weight:800;margin-bottom:24px;margin-top:20px} .path-list-card{background:var(--card);border:1px solid var(--border);border-radius:10px;padding:18px 22px;margin-bottom:12px;cursor:pointer;transition:border-color .15s} .path-list-card:hover{border-color:var(--accent)} +/* FIX: completed path gets gold border */ +.path-list-card.path-done{border-color:var(--gold)} .path-list-top{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px} .path-list-topic{font-family:var(--font-head);font-size:1.05rem;font-weight:700} .path-list-meta{font-size:.8rem;color:var(--muted);margin-top:3px} .path-list-arrow{color:var(--accent);font-size:1.2rem} +/* FIX: completed badge in path list */ +.path-done-pill{display:inline-flex;align-items:center;gap:4px;background:#fef3c7;border:1px solid #fbbf24;color:#92400e;padding:3px 10px;border-radius:99px;font-size:.72rem;font-weight:700;margin-left:8px} +[data-theme="dark"] .path-done-pill{background:#2d1f00;border-color:#92400e;color:#fbbf24} .path-card-progress{margin-top:8px} .path-card-prog-label{display:flex;justify-content:space-between;font-size:.75rem;color:var(--muted);margin-bottom:5px} .path-card-prog-bar{background:var(--border);border-radius:99px;height:7px;overflow:hidden} @@ -223,7 +233,6 @@ h1.hero-title span{color:var(--accent)} .tpd.current{border-color:var(--accent2);box-shadow:0 0 0 2px var(--accent2)} .tpd.correct-dot{background:var(--accent3);border-color:var(--accent3);color:#fff} .tpd.wrong-dot{background:var(--accent2);border-color:var(--accent2);color:#fff} - .question-card{background:var(--card);border:1px solid var(--border);border-radius:10px;padding:24px;margin-bottom:16px} .question-num{font-size:.75rem;font-weight:700;color:var(--accent);text-transform:uppercase;letter-spacing:1px;margin-bottom:10px} .question-text{font-family:var(--font-head);font-size:1rem;font-weight:700;margin-bottom:16px;line-height:1.4} @@ -238,7 +247,6 @@ h1.hero-title span{color:var(--accent)} [data-theme="dark"] .option-btn.wrong{background:#450a0a;color:#fca5a5} .explanation-box{background:var(--bg);border-radius:6px;padding:10px 14px;margin-top:10px;font-size:.83rem;color:var(--muted);line-height:1.6;border-left:3px solid var(--accent)} .test-nav-bar{display:flex;justify-content:space-between;align-items:center;margin-top:20px;gap:10px} - .test-score-card{background:var(--card);border:2px solid var(--accent);border-radius:12px;padding:30px;text-align:center;margin-bottom:22px} .test-score-num{font-family:var(--font-head);font-size:3.5rem;font-weight:800;color:var(--accent)} .test-score-label{color:var(--muted);font-size:1rem;margin-top:4px} @@ -261,7 +269,9 @@ h1.hero-title span{color:var(--accent)} .parent-student-card{background:linear-gradient(135deg,var(--accent) 0%,var(--accent4) 100%);border-radius:12px;padding:22px 26px;color:#fff;margin-bottom:22px} .parent-student-card h2{font-family:var(--font-head);font-size:1.4rem;font-weight:800;margin-bottom:4px} .parent-path-card{background:var(--card);border:1px solid var(--border);border-radius:10px;padding:20px 22px;margin-bottom:12px} -.parent-path-title{font-family:var(--font-head);font-size:1rem;font-weight:700;margin-bottom:10px} +/* FIX: completed path in parent dashboard */ +.parent-path-card.parent-done{border-color:var(--gold)} +.parent-path-title{font-family:var(--font-head);font-size:1rem;font-weight:700;margin-bottom:10px;display:flex;align-items:center;gap:8px} .parent-stats{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-bottom:12px} .parent-stat{background:var(--bg);border-radius:8px;padding:12px;text-align:center} .parent-stat-val{font-family:var(--font-head);font-size:1.4rem;font-weight:800;color:var(--accent)} @@ -360,6 +370,8 @@ h1.hero-title span{color:var(--accent)}
πŸ”₯ 0 Day Streak
+ +
πŸ† Path Complete!
0% @@ -383,14 +395,11 @@ h1.hero-title span{color:var(--accent)}
-
πŸ“ Tests

10-question MCQ test for each learning path. Up to 3 attempts. Same questions per attempt.

- -
@@ -399,28 +408,20 @@ h1.hero-title span{color:var(--accent)}
- - - -
- -
- -
Question 1 / 10
- +
@@ -594,6 +595,7 @@ h1.hero-title span{color:var(--accent)} let currentUser = null; let currentPathId = null; let currentPathData = null; +let currentPathMeta = {}; // FIX: store is_completed, streak, etc. let currentDayIndex = 0; let selectedDuration = '7 days'; let pendingGenerate = false; @@ -604,14 +606,14 @@ let ytPlayer = null, accumulatedWatchTime = 0, lastCurrentTime = 0; let videoTotalDuration = 0, videoWatchInterval = null; let currentModalDayNum = null, videoCompleted = false; -// Test β€” ALL state in one object for clarity +// Test state const TEST = { id: null, pathId: null, pathName: '', - questions: [], answers: {}, // answers: { questionId(number) -> optionString } + questions: [], answers: {}, qIndex: 0, submitted: false, score: 0 }; -// Notes +// Notes state let notesState = { dayNum: null, pathId: null, dayTopic: '', pathTopic: '' }; // ─── Init ───────────────────────────────────────────────────────────────────── @@ -690,13 +692,13 @@ async function doLogin() { async function doSignup() { const d = { - full_name: document.getElementById('su-name').value.trim(), - student_number: document.getElementById('su-student-no').value.trim(), - parent_number: document.getElementById('su-parent-no').value.trim(), - parent_email: document.getElementById('su-parent-email').value.trim(), - email: document.getElementById('su-email').value.trim(), - username: document.getElementById('su-username').value.trim(), - password: document.getElementById('su-pass').value, + full_name: document.getElementById('su-name').value.trim(), + student_number: document.getElementById('su-student-no').value.trim(), + parent_number: document.getElementById('su-parent-no').value.trim(), + parent_email: document.getElementById('su-parent-email').value.trim(), + email: document.getElementById('su-email').value.trim(), + username: document.getElementById('su-username').value.trim(), + password: document.getElementById('su-pass').value, confirm_password: document.getElementById('su-confirm').value }; const e = document.getElementById('signup-err'); e.style.display = 'none'; @@ -727,11 +729,11 @@ async function doParentLogin() { async function doParentSignup() { const d = { - full_name: document.getElementById('ps-name').value.trim(), + full_name: document.getElementById('ps-name').value.trim(), student_username: document.getElementById('ps-student-username').value.trim(), - email: document.getElementById('ps-email').value.trim(), - username: document.getElementById('ps-username').value.trim(), - password: document.getElementById('ps-pass').value, + email: document.getElementById('ps-email').value.trim(), + username: document.getElementById('ps-username').value.trim(), + password: document.getElementById('ps-pass').value, confirm_password: document.getElementById('ps-confirm').value }; const e = document.getElementById('parent-signup-err'); e.style.display = 'none'; @@ -743,7 +745,7 @@ async function doParentSignup() { async function logout() { await api('/api/auth/logout','POST'); - setUser(null); currentPathData = null; currentPathId = null; + setUser(null); currentPathData = null; currentPathId = null; currentPathMeta = {}; goPage('home-page'); toast('Logged out.','success'); } @@ -757,53 +759,78 @@ async function generate() { } if (!currentUser) { pendingGenerate = true; showModal('login-modal'); return; } showLoading(true, 'Generating path…', 'Building your ' + selectedDuration + ' plan for "' + topic + '"'); - const qt = queueTimer(); + const qt = queueTimer(); const res = await api('/api/generate','POST',{ topic, start_date: start, duration: selectedDuration }); clearTimeout(qt); showLoading(false); if (res.success) { - currentPathId = res.path_id; currentPathData = res.path_data; progressData = {}; - renderPathPage(res.path_data, {}, 0, null, 0, res.path_data.days.length); + currentPathId = res.path_id; + currentPathData = res.path_data; + currentPathMeta = { is_completed: 0 }; + progressData = {}; + renderPathPage(res.path_data, {}, 0, null, 0, res.path_data.days.length, 0); goPage('path-page'); toast('Learning path created! πŸŽ‰','success'); } else { toast(res.error || 'Generation failed.','error'); } } // ─── Path Rendering ─────────────────────────────────────────────────────────── -function renderPathPage(pd, prog, focusIdx, resetTo, doneDays, totalDays) { +function renderPathPage(pd, prog, focusIdx, resetTo, doneDays, totalDays, streak, isCompleted) { const days = pd.days; const today = new Date().toISOString().split('T')[0]; + document.getElementById('path-title').textContent = pd.topic; document.getElementById('path-meta').textContent = days.length + ' days Β· Starts ' + fmtDate(days[0].date); + const pct = totalDays ? Math.round(doneDays / totalDays * 100) : 0; document.getElementById('path-prog-bar').style.width = pct + '%'; document.getElementById('path-prog-label').textContent = pct + '%'; + + // FIX: show/hide path complete badge in header + const compBadge = document.getElementById('path-complete-badge'); + if (isCompleted) { + compBadge.classList.add('show'); + } else { + compBadge.classList.remove('show'); + } + + // Build timeline const tl = document.getElementById('day-timeline'); tl.innerHTML = ''; days.forEach((day, i) => { - const dot = document.createElement('div'); + const dot = document.createElement('div'); dot.className = 'timeline-dot'; dot.textContent = i + 1; const done = prog[day.day_number]?.completed == 1; const isTd = day.date === today; if (done) dot.classList.add('done'); else if (isTd) dot.classList.add('today'); - dot.onclick = () => { currentDayIndex = i; renderDay(pd, prog, i, resetTo); }; + if (i === focusIdx) dot.classList.add('active-dot'); + dot.onclick = () => { + // Remove active-dot from all, add to clicked + document.querySelectorAll('.timeline-dot').forEach(d => d.classList.remove('active-dot')); + dot.classList.add('active-dot'); + currentDayIndex = i; + renderDay(pd, prog, i, resetTo); + }; tl.appendChild(dot); }); + currentDayIndex = focusIdx; - renderDay(pd, prog, focusIdx, resetTo); + renderDay(pd, prog, focusIdx, resetTo, isCompleted); } -function renderDay(pd, prog, idx, resetTo) { +function renderDay(pd, prog, idx, resetTo, isCompleted) { const day = pd.days[idx]; const today = new Date().toISOString().split('T')[0]; const dp = prog[day.day_number] || {}; const sec = document.getElementById('day-section'); sec.innerHTML = ''; - // Certificate banner if path complete const total = pd.days.length; const done = Object.values(prog).filter(p => p.completed == 1).length; - if (done >= total && total > 0) { + + // FIX: Show cert banner when path is completed (all days done OR is_completed flag) + const pathDone = isCompleted || done >= total; + if (pathDone && total > 0) { sec.appendChild(mkEl('div','cert-banner',`

πŸ† Path Complete!

@@ -814,7 +841,8 @@ function renderDay(pd, prog, idx, resetTo) { } if (resetTo && idx + 1 === resetTo) { - sec.appendChild(mkEl('div','reset-notice',`⚑ Streak Reset! Missed 2 days. Starting again from Day ${resetTo}.`)); + sec.appendChild(mkEl('div','reset-notice', + `⚑ Streak Reset! Missed 2 days. Starting again from Day ${resetTo}.`)); } const isToday = day.date === today; @@ -824,15 +852,16 @@ function renderDay(pd, prog, idx, resetTo) { ? Math.round(dp.watch_time_seconds / dp.total_duration_seconds * 100) : 0; const card = mkEl('div', - 'day-card' + (isToday ? ' current-day' : '') + (isDone ? ' completed' : '') + (isFuture ? ' locked' : ''), ''); + 'day-card' + + (isToday && !isDone ? ' current-day' : '') + + (isDone ? ' completed' : '') + + (isFuture ? ' locked' : ''), ''); let videoHTML = ''; if (day.video) { const v = day.video; const vJ = JSON.stringify(v); const tJ = JSON.stringify(day.topic); - const vSafe = vJ.replace(/'/g, "\\'"); - const tSafe = tJ.replace(/'/g, "\\'"); videoHTML = `
@@ -850,7 +879,6 @@ function renderDay(pd, prog, idx, resetTo) { const statusLabel = isDone ? 'done' : isToday ? 'today' : 'upcoming'; const statusText = isDone ? 'βœ“ Completed' : isToday ? '● Today' : fmtDate(day.date); - // Use data-* for the notes button to avoid quote injection card.innerHTML = `
@@ -863,18 +891,18 @@ function renderDay(pd, prog, idx, resetTo) {
${esc(day.explanation)}
${videoHTML} ${!isFuture ? `
- +
` : `
πŸ“… Available on ${fmtDate(day.date)}
`} `; sec.appendChild(card); } function openNotesBtn(btn) { - openNotes( - parseInt(btn.dataset.daynum), - btn.dataset.daytopic, - btn.dataset.pathtopic - ); + openNotes(parseInt(btn.dataset.daynum), btn.dataset.daytopic, btn.dataset.pathtopic); } function mkEl(tag, className, html) { @@ -890,10 +918,15 @@ function esc(s) { function navDay(dir) { const days = currentPathData?.days || []; - const n = currentDayIndex + dir; + const n = currentDayIndex + dir; if (n < 0 || n >= days.length) return; + // FIX: update active dot on navigation + document.querySelectorAll('.timeline-dot').forEach((d,i) => { + d.classList.remove('active-dot'); + if (i === n) d.classList.add('active-dot'); + }); currentDayIndex = n; - renderDay(currentPathData, progressData, n, null); + renderDay(currentPathData, progressData, n, null, currentPathMeta.is_completed); const dots = document.querySelectorAll('.timeline-dot'); if (dots[n]) dots[n].scrollIntoView({ behavior:'smooth', inline:'center', block:'nearest' }); } @@ -901,8 +934,20 @@ function navDay(dir) { function goToday() { if (!currentPathData) return; const td = new Date().toISOString().split('T')[0]; - const i = currentPathData.days.findIndex(d => d.date === td); - if (i >= 0) { currentDayIndex = i; renderDay(currentPathData, progressData, i, null); } + let i = currentPathData.days.findIndex(d => d.date === td); + // FIX: if today not in path, go to last available day + if (i < 0) { + const past = currentPathData.days.filter(d => d.date <= td); + i = past.length > 0 ? past.length - 1 : 0; + } + document.querySelectorAll('.timeline-dot').forEach((d, idx) => { + d.classList.remove('active-dot'); + if (idx === i) d.classList.add('active-dot'); + }); + currentDayIndex = i; + renderDay(currentPathData, progressData, i, null, currentPathMeta.is_completed); + const dots = document.querySelectorAll('.timeline-dot'); + if (dots[i]) dots[i].scrollIntoView({ behavior:'smooth', inline:'center', block:'nearest' }); } async function openPath(pathId) { @@ -910,11 +955,28 @@ async function openPath(pathId) { const res = await api('/api/paths/' + pathId); showLoading(false); if (!res.path) return; - currentPathId = pathId; currentPathData = res.path_data; progressData = res.progress || {}; + currentPathId = pathId; + currentPathData = res.path_data; + progressData = res.progress || {}; + // FIX: store full meta including is_completed + currentPathMeta = { + is_completed: res.is_completed || 0, + completed_at: res.completed_at || null, + streak: res.streak || 0, + }; const td = new Date().toISOString().split('T')[0]; - let si = res.reset_to ? res.reset_to - 1 : Math.max(0, currentPathData.days.findIndex(d => d.date === td)); + let si = 0; + if (res.reset_to) { + si = res.reset_to - 1; + } else { + const todayIdx = currentPathData.days.findIndex(d => d.date === td); + si = todayIdx >= 0 ? todayIdx : Math.max(0, currentPathData.days.filter(d => d.date <= td).length - 1); + } document.getElementById('streak-badge').textContent = 'πŸ”₯ ' + (res.streak || 0) + ' Day Streak'; - renderPathPage(currentPathData, progressData, si, res.reset_to, res.completed_days || 0, res.total_days || 0); + renderPathPage( + currentPathData, progressData, si, res.reset_to, + res.completed_days || 0, res.total_days || 0, res.streak || 0, res.is_completed || 0 + ); goPage('path-page'); } @@ -924,14 +986,18 @@ function confirmResetPath() { if (!confirm('⚠️ Reset ALL progress?\n\nThis clears all completed days and watch times. Cannot be undone.')) return; doResetPath(); } + async function doResetPath() { const res = await api('/api/paths/' + currentPathId + '/reset','POST'); if (res.success) { - progressData = {}; - document.getElementById('path-prog-bar').style.width = '0%'; - document.getElementById('path-prog-label').textContent = '0%'; - document.getElementById('streak-badge').textContent = 'πŸ”₯ 0 Day Streak'; - renderPathPage(currentPathData, {}, 0, null, 0, currentPathData.days.length); + progressData = {}; + currentPathMeta = { is_completed: 0 }; + document.getElementById('path-prog-bar').style.width = '0%'; + document.getElementById('path-prog-label').textContent = '0%'; + document.getElementById('streak-badge').textContent = 'πŸ”₯ 0 Day Streak'; + // FIX: hide path complete badge on reset + document.getElementById('path-complete-badge').classList.remove('show'); + renderPathPage(currentPathData, {}, 0, null, 0, currentPathData.days.length, 0, 0); toast('Path progress reset!','info'); } } @@ -950,11 +1016,19 @@ async function loadPaths() { const done = Math.round((p.completed_days / p.duration_days) * dots); let dotsHtml = ''; for (let i = 0; i < dots; i++) dotsHtml += `
`; + const card = document.createElement('div'); - card.className = 'path-list-card'; + // FIX: add .path-done class if completed + card.className = 'path-list-card' + (p.is_completed ? ' path-done' : ''); card.innerHTML = `
-
${esc(p.topic)}
${p.duration_days} days Β· Started ${fmtDate(p.start_date)}
+
+
+ ${esc(p.topic)} + ${p.is_completed ? 'πŸ† Completed' : ''} +
+
${p.duration_days} days Β· Started ${fmtDate(p.start_date)}
+
β†’
@@ -1027,7 +1101,8 @@ function startWatchTracking(dayNum) { document.getElementById('next-day-btn').classList.add('show'); if (!progressData[dayNum]) progressData[dayNum] = {}; progressData[dayNum].completed = 1; - refreshStreak(); + // FIX: refresh streak AND check for path completion + refreshPathStatus(); } } }, 1000); @@ -1050,25 +1125,60 @@ async function saveProgress(dayNum, wt, td, lp, comp) { if (!currentPathId) return; if (!progressData[dayNum]) progressData[dayNum] = {}; Object.assign(progressData[dayNum], { - watch_time_seconds: wt, total_duration_seconds: td, - last_position_seconds: lp, completed: comp + watch_time_seconds: wt, + total_duration_seconds:td, + last_position_seconds: lp, + completed: comp }); await api('/api/progress','POST',{ - path_id: currentPathId, day_number: dayNum, - watch_time_seconds: wt, total_duration_seconds: td, - last_position_seconds: lp, completed: comp + path_id: currentPathId, + day_number: dayNum, + watch_time_seconds: wt, + total_duration_seconds:td, + last_position_seconds: lp, + completed: comp }); } -async function refreshStreak() { +// FIX: unified refresh β€” updates streak, progress bar, AND path complete badge +async function refreshPathStatus() { if (!currentPathId) return; const res = await api('/api/paths/' + currentPathId); - if (res.streak !== undefined) { - document.getElementById('streak-badge').textContent = 'πŸ”₯ ' + res.streak + ' Day Streak'; - document.getElementById('path-prog-bar').style.width = (res.progress_pct || 0) + '%'; - document.getElementById('path-prog-label').textContent = (res.progress_pct || 0) + '%'; - progressData = res.progress || {}; + if (!res.path_data) return; + + currentPathMeta = { + is_completed: res.is_completed || 0, + completed_at: res.completed_at || null, + streak: res.streak || 0, + }; + progressData = res.progress || {}; + + document.getElementById('streak-badge').textContent = 'πŸ”₯ ' + (res.streak || 0) + ' Day Streak'; + document.getElementById('path-prog-bar').style.width = (res.progress_pct || 0) + '%'; + document.getElementById('path-prog-label').textContent = (res.progress_pct || 0) + '%'; + + // FIX: update path complete badge + const compBadge = document.getElementById('path-complete-badge'); + if (res.is_completed) { + compBadge.classList.add('show'); + toast('πŸ† Congratulations! Path complete! Download your certificate!', 'success'); + } else { + compBadge.classList.remove('show'); } + + // FIX: re-render timeline dots to reflect new completion states + const days = currentPathData?.days || []; + const today = new Date().toISOString().split('T')[0]; + const dots = document.querySelectorAll('.timeline-dot'); + days.forEach((day, i) => { + const dot = dots[i]; + if (!dot) return; + const done = progressData[day.day_number]?.completed == 1; + const isTd = day.date === today; + dot.classList.remove('done','today'); + if (done) dot.classList.add('done'); + else if (isTd) dot.classList.add('today'); + }); } async function closeVideoModal() { @@ -1081,12 +1191,16 @@ async function closeVideoModal() { if (ytPlayer) { ytPlayer.destroy(); ytPlayer = null; } document.getElementById('video-modal').classList.remove('active'); document.body.style.overflow = ''; - if (currentPathData) renderDay(currentPathData, progressData, currentDayIndex, null); + // FIX: re-render current day AND refresh path status after close + if (currentPathData) { + await refreshPathStatus(); + renderDay(currentPathData, progressData, currentDayIndex, null, currentPathMeta.is_completed); + } } function goNextDayFromModal() { closeVideoModal(); navDay(1); } -// ─── TESTS β€” FULLY FIXED ────────────────────────────────────────────────────── +// ─── TESTS ──────────────────────────────────────────────────────────────────── async function loadTestPaths() { const res = await api('/api/paths'); const ctr = document.getElementById('test-paths-list'); @@ -1102,12 +1216,12 @@ async function loadTestPaths() { for (let i = 1; i <= 3; i++) { const t = tests[i-1]; const cls = t?.completed ? (t.score >= 7 ? 'passed' : 'failed') : ''; - badges += `
${t?.completed ? t.score : 'β€”'}
`; + badges += `
${t?.completed ? t.score : 'β€”'}
`; } const card = document.createElement('div'); card.className = 'test-path-card'; - const dis = used >= 3 ? 'disabled' : ''; - const txt = used >= 3 ? 'βœ“ All 3 Used' : `Take Test (${3-used} left)`; + const dis = used >= 3 ? 'disabled' : ''; + const txt = used >= 3 ? 'βœ“ All 3 Used' : `Take Test (${3-used} left)`; card.innerHTML = `
${esc(p.topic)}
@@ -1124,7 +1238,6 @@ async function loadTestPaths() { } async function startTest(pathId, pathName) { - // Reset test state Object.assign(TEST, { id:null, pathId, pathName, questions:[], answers:{}, qIndex:0, submitted:false, score:0 }); showLoading(true,'Creating test…','Please wait, your test is being created…'); const qt = queueTimer(); @@ -1136,15 +1249,12 @@ async function startTest(pathId, pathName) { TEST.questions = res.questions; document.getElementById('test-path-title').textContent = esc(pathName) + ' β€” Test'; document.getElementById('test-attempt-label').textContent = 'Attempt ' + res.attempt_number + ' of 3'; - - // Switch views - document.getElementById('test-list-view').style.display = 'none'; - document.getElementById('test-taking-view').style.display = 'block'; - document.getElementById('test-score-card').style.display = 'none'; + document.getElementById('test-list-view').style.display = 'none'; + document.getElementById('test-taking-view').style.display = 'block'; + document.getElementById('test-score-card').style.display = 'none'; if (res.is_existing) toast('Resuming existing test','info'); else toast('Test created! Good luck! 🎯','success'); - renderTestQuestion(0); } @@ -1154,15 +1264,16 @@ function renderTestQuestion(idx) { const total = TEST.questions.length; const answered = Object.keys(TEST.answers).length; - // Progress label document.getElementById('test-progress-label').textContent = `Question ${idx+1} / ${total} Β· ${answered} answered`; - // Nav buttons - document.getElementById('test-prev-btn').style.opacity = idx === 0 ? '.35' : '1'; - document.getElementById('test-next-btn').style.display = idx < total - 1 ? '' : 'none'; - document.getElementById('test-submit-btn').style.display = (!TEST.submitted && idx === total - 1) ? '' : 'none'; + // FIX: correct button visibility logic + document.getElementById('test-prev-btn').style.opacity = idx === 0 ? '.35' : '1'; + document.getElementById('test-next-btn').style.display = idx < total - 1 ? 'inline-block' : 'none'; + // Submit button: only on last question, only when not submitted + const submitBtn = document.getElementById('test-submit-btn'); + submitBtn.style.display = (!TEST.submitted && idx === total - 1) ? 'inline-block' : 'none'; - // Question dots + // Question navigation dots const dotsEl = document.getElementById('test-progress-dots'); dotsEl.innerHTML = ''; TEST.questions.forEach((qq, i) => { @@ -1185,21 +1296,20 @@ function renderTestQuestion(idx) { // Question card const disp = document.getElementById('question-display'); disp.innerHTML = ''; - const card = document.createElement('div'); + const card = document.createElement('div'); card.className = 'question-card'; - const selected = TEST.answers[q.id] || null; + const selected = TEST.answers[q.id] !== undefined ? TEST.answers[q.id] : null; let optsHtml = ''; q.options.forEach((opt, oi) => { let cls = 'option-btn'; if (TEST.submitted) { - if (opt === q.correct) cls += ' correct'; + if (opt === q.correct) cls += ' correct'; else if (opt === selected && opt !== q.correct) cls += ' wrong'; } else if (opt === selected) { cls += ' selected'; } - // Use data-idx to avoid quote injection issues - optsHtml += ``; + optsHtml += ``; }); card.innerHTML = ` @@ -1210,19 +1320,18 @@ function renderTestQuestion(idx) { `; disp.appendChild(card); - // Show score card at top when submitted if (TEST.submitted) { document.getElementById('test-score-card').style.display = 'block'; } } -// ── FIX: store answer by question id (number), look up by number ────────────── function selectTestOption(questionId, optionIndex) { if (TEST.submitted) return; const q = TEST.questions.find(qq => qq.id === questionId); if (!q) return; const opt = q.options[optionIndex]; - TEST.answers[questionId] = opt; // store as number key β†’ string value + // FIX: store as numeric key (consistent with server) + TEST.answers[questionId] = opt; renderTestQuestion(TEST.qIndex); } @@ -1238,23 +1347,19 @@ async function submitTest() { if (answered < total) { if (!confirm(`You've answered ${answered}/${total} questions. Submit anyway?`)) return; } - - // Build answers object: send BOTH str and number keys for server safety + // FIX: send both string and numeric keys to handle server-side key parsing const answersPayload = {}; for (const [k, v] of Object.entries(TEST.answers)) { - answersPayload[k] = v; // number key - answersPayload[String(k)] = v; // string key + answersPayload[k] = v; + answersPayload[String(k)] = v; } - const res = await api('/api/test/' + TEST.id + '/submit','POST',{ answers: answersPayload }); if (!res.success) { toast(res.error || 'Failed to submit.','error'); return; } TEST.submitted = true; TEST.score = res.score; - // Merge server results (has correct/explanation) back into questions TEST.questions = res.results; - // Show score card const pct = Math.round(res.score / res.total * 100); const msg = pct >= 80 ? 'πŸ† Excellent!' : pct >= 60 ? 'πŸ‘ Good job!' : 'πŸ’ͺ Keep practicing!'; document.getElementById('score-num').textContent = res.score + '/' + res.total; @@ -1272,7 +1377,7 @@ function exitTest() { document.getElementById('test-taking-view').style.display = 'none'; document.getElementById('test-list-view').style.display = 'block'; document.getElementById('test-score-card').style.display = 'none'; - document.getElementById('question-display').innerHTML = ''; + document.getElementById('question-display').innerHTML = ''; document.getElementById('test-progress-dots').innerHTML = ''; loadTestPaths(); } @@ -1282,43 +1387,58 @@ async function openNotes(dayNum, dayTopic, pathTopic) { Object.assign(notesState, { dayNum, pathId: currentPathId, dayTopic, pathTopic }); showModal('notes-modal'); document.getElementById('notes-modal-sub').textContent = dayTopic; - document.getElementById('notes-content').style.display = 'none'; - document.getElementById('notes-actions').style.display = 'none'; - document.getElementById('notes-loading').style.display = 'block'; + document.getElementById('notes-content').style.display = 'none'; + document.getElementById('notes-actions').style.display = 'none'; + document.getElementById('notes-loading').style.display = 'block'; const res = await api('/api/notes/generate','POST',{ - path_id: currentPathId, day_number: dayNum, day_topic: dayTopic, path_topic: pathTopic + path_id: currentPathId, + day_number: dayNum, + day_topic: dayTopic, + path_topic: pathTopic }); document.getElementById('notes-loading').style.display = 'none'; if (res.success) { - document.getElementById('notes-content').textContent = res.notes; + document.getElementById('notes-content').textContent = res.notes; document.getElementById('notes-content').style.display = 'block'; document.getElementById('notes-actions').style.display = 'flex'; toast(res.cached ? 'Loaded cached notes' : 'Notes generated! πŸ“', 'success'); - } else { toast(res.error || 'Failed to generate notes','error'); closeAllModals(); } + } else { + // FIX: close modal cleanly on error + closeAllModals(); + toast(res.error || 'Failed to generate notes','error'); + } } async function downloadNotesPDF() { toast('Downloading PDF…','info'); const res = await fetch('/api/notes/pdf',{ method:'POST', headers:{'Content-Type':'application/json'}, credentials:'include', - body: JSON.stringify({ path_id: notesState.pathId, day_number: notesState.dayNum, - day_topic: notesState.dayTopic, path_topic: notesState.pathTopic }) + body: JSON.stringify({ + path_id: notesState.pathId, + day_number: notesState.dayNum, + day_topic: notesState.dayTopic, + path_topic: notesState.pathTopic + }) }); if (res.ok) { const blob = await res.blob(); - const a = document.createElement('a'); - a.href = URL.createObjectURL(blob); a.download = 'study_notes.pdf'; a.click(); + const a = document.createElement('a'); + a.href = URL.createObjectURL(blob); + a.download = 'study_notes.pdf'; + a.click(); } else { toast('Failed to download PDF','error'); } } // ─── Certificate ────────────────────────────────────────────────────────────── async function downloadCertificate(pathId) { toast('Generating certificate…','info'); - const res = await fetch('/api/certificate/'+pathId,{ credentials:'include' }); + const res = await fetch('/api/certificate/' + pathId, { credentials:'include' }); if (res.ok) { const blob = await res.blob(); - const a = document.createElement('a'); - a.href = URL.createObjectURL(blob); a.download = 'certificate.pdf'; a.click(); + const a = document.createElement('a'); + a.href = URL.createObjectURL(blob); + a.download = 'certificate.pdf'; + a.click(); toast('Certificate downloaded! πŸŽ“','success'); } else { const d = await res.json().catch(()=>({})); @@ -1367,6 +1487,7 @@ async function changePassword() { const s = document.getElementById('pw-success'); e.style.display = 'none'; s.style.display = 'none'; if (d.new_password !== confirm_pw) { e.textContent = 'New passwords do not match.'; e.style.display = 'block'; return; } + if (!d.new_password) { e.textContent = 'New password is required.'; e.style.display = 'block'; return; } const res = await api('/api/profile','PUT',d); if (res.success) { s.style.display = 'block'; @@ -1380,7 +1501,7 @@ let chartInst = null; async function loadActivityChart() { const res = await api('/api/activity'); const actMap = {}; - (res.activity || []).forEach(a => { actMap[a.logged_at] = (actMap[a.logged_at]||0) + a.count; }); + (res.activity || []).forEach(a => { actMap[a.logged_at] = (actMap[a.logged_at]||0) + (a.count||1); }); const today = new Date(); const labels = [], data = []; for (let i = 29; i >= 0; i--) { @@ -1410,19 +1531,33 @@ async function loadActivityChart() { async function loadParentDashboard() { const res = await api('/api/parent/dashboard'); const cont = document.getElementById('parent-dashboard-content'); - if (res.error) { cont.innerHTML = `
${esc(res.error)}
`; return; } + if (res.error) { + cont.innerHTML = `
${esc(res.error)}
`; return; + } const s = res.student; let html = `

πŸ‘€ ${esc(s.full_name)}

${esc(s.email)}

`; - if (!res.paths?.length) { html += '
No learning paths yet.
'; } + if (!res.paths?.length) { + html += '
No learning paths yet.
'; + } (res.paths||[]).forEach(p => { const testRows = (p.tests||[]).map(t => - `Attempt ${t.attempt_number}${t.score}/10 (${t.score*10}%)${new Date(t.created_at).toLocaleDateString()}` - ).join('') || 'No tests taken yet'; + ` + Attempt ${t.attempt_number} + ${t.score}/10 (${t.score*10}%) + ${new Date(t.created_at).toLocaleDateString()} + ${t.score >= 7 ? 'βœ… Passed' : '❌ Failed'} + ` + ).join('') || 'No tests taken yet'; + + // FIX: gold border for completed paths in parent dashboard html += ` -
-
${esc(p.topic)}
+
+
+ ${esc(p.topic)} + ${p.is_completed ? 'πŸ† Completed' : ''} +
-
${p.progress_pct}%
Completed
+
${p.progress_pct}%
Progress
${p.completed_days}/${p.total_days}
Days Done
${p.streak}πŸ”₯
Streak
@@ -1430,9 +1565,12 @@ async function loadParentDashboard() {
Progress
+ ${p.is_completed ? `
+ πŸ† Completed on ${p.completed_at ? new Date(p.completed_at).toLocaleDateString() : 'N/A'} +
` : ''}
Test Scores
- + ${testRows}
AttemptScoreDate
AttemptScoreDateResult
`; @@ -1482,10 +1620,12 @@ function queueTimer() { }, 9000); } +let _toastTimer = null; function toast(msg, type='success') { - const t = document.getElementById('toast'); + const t = document.getElementById('toast'); t.textContent = msg; t.className = 'show ' + type; - setTimeout(() => { t.className = ''; }, 3500); + if (_toastTimer) clearTimeout(_toastTimer); + _toastTimer = setTimeout(() => { t.className = ''; }, 3500); } function fmtDate(s) { @@ -1495,4 +1635,1097 @@ function fmtDate(s) { } - \ No newline at end of file + + + + + + +
+
+
πŸ“„ Resume Builder
+
+ + +
+
+

Build a professional resume with AI assistance. Download as PDF in 3 styles.

+ + +
+
Loading…
+
+ + + +
+ + +
+
+
βœ… To-Do List
+ +
+

Track tasks, assignments, and goals with priority management.

+ +
+
0
Total
+
0
Done
+
0
High Priority
+
0
Overdue
+
+ +
+ + + + + + + + +
+ +
+
+ + + + + + +
+
+
πŸ€– LearnPath AI
Your AI study companion
+
+ + +
+
+
+
πŸ‘‹ Hi! I'm your AI study assistant. Ask me anything β€” concepts, code, math, or study tips!
+
+
+ + +
+
+ + +
+
πŸ“‘ PDF Summarizer
+

Upload any PDF β€” textbooks, articles, notes β€” and get an AI summary with key points. Then ask questions about it.

+ +
+
πŸ“„
+
Drop your PDF here or click to browse
+
Max 10MB Β· Text-based PDFs only
+ +
+ + + + + +
+
Recent Summaries
+
+
+
+ + + \ No newline at end of file