import { protectedFetch } from "./api.js?v=__ASSET_VERSION__"; import { initSideNav, loadNavUser } from "./nav.js?v=__ASSET_VERSION__"; import { ACADEMY_TRACKS } from "./academy-content.js?v=__ASSET_VERSION__"; import { SCENES, sceneDefs, sceneFallbackText } from "./academy-scenes.js?v=__ASSET_VERSION__"; // ── Progress ─────────────────────────────────────────────────────────────── const PROGRESS_KEY = "academyProgress.v1"; function loadProgress() { try { return JSON.parse(localStorage.getItem(PROGRESS_KEY)) ?? {}; } catch { return {}; } } function saveProgress(lessonId, patch) { const all = loadProgress(); all[lessonId] = { ...all[lessonId], ...patch }; try { localStorage.setItem(PROGRESS_KEY, JSON.stringify(all)); } catch { /* storage unavailable */ } } // ── Player ───────────────────────────────────────────────────────────────── let current = null; // {track, lesson, index} const audio = new Audio(); let autoplay = true; const $ = (id) => document.getElementById(id); function renderMenu() { const progress = loadProgress(); const mount = $("academy-menu"); mount.replaceChildren( ...ACADEMY_TRACKS.map((track) => { const wrap = document.createElement("section"); wrap.className = "panel-card"; const head = document.createElement("div"); head.className = "panel-head"; const h2 = document.createElement("h2"); h2.textContent = track.name; head.append(h2); const body = document.createElement("div"); body.className = "panel-body"; const blurb = document.createElement("p"); blurb.className = "muted"; blurb.style.marginTop = "0"; blurb.textContent = track.blurb; body.append(blurb); const list = document.createElement("div"); list.className = "academy-lessons"; track.lessons.forEach((lesson, li) => { const state = progress[lesson.id] ?? {}; const resumeAt = state.done ? 0 : (state.slide ?? 0); const btn = document.createElement("button"); btn.type = "button"; btn.className = "academy-lesson"; const num = document.createElement("span"); num.className = "academy-lesson-num"; num.textContent = state.done ? "✓" : String(li + 1); if (state.done) num.classList.add("done"); const mid = document.createElement("span"); mid.className = "academy-lesson-mid"; const label = document.createElement("span"); label.className = "academy-lesson-label"; label.textContent = lesson.title; const meta = document.createElement("span"); meta.className = "academy-lesson-meta"; meta.textContent = state.done ? `completed · ${lesson.slides.length} slides · ~${lesson.minutes} min` : resumeAt > 0 ? `resume at slide ${resumeAt + 1} of ${lesson.slides.length}` : `${lesson.slides.length} slides · ~${lesson.minutes} min`; const bar = document.createElement("span"); bar.className = "academy-progress"; const fill = document.createElement("span"); fill.className = "academy-progress-fill"; fill.style.width = state.done ? "100%" : `${Math.round((resumeAt / lesson.slides.length) * 100)}%`; bar.append(fill); mid.append(label, meta, bar); btn.append(num, mid); btn.addEventListener("click", () => openLesson(track, lesson, resumeAt)); list.append(btn); }); body.append(list); wrap.append(head, body); return wrap; }), ); } function openLesson(track, lesson, index) { current = { track, lesson, index }; $("academy-menu").classList.add("hidden"); $("academy-films")?.classList.add("hidden"); // Pause any playing film so it doesn't run (and download) behind the lesson. for (const video of document.querySelectorAll("#academy-films video")) video.pause(); $("academy-player").classList.remove("hidden"); renderSlide(); } function closeLesson() { audio.pause(); current = null; $("academy-player").classList.add("hidden"); $("academy-menu").classList.remove("hidden"); $("academy-films")?.classList.remove("hidden"); renderMenu(); // reflect fresh progress } function renderSlide() { const { lesson, index, track } = current; const slide = lesson.slides[index]; saveProgress(lesson.id, { slide: index }); $("player-lesson").textContent = `${track.name} — ${lesson.title}`; $("player-slide-title").textContent = slide.title; $("player-count").textContent = `${index + 1} / ${lesson.slides.length}`; // Rebuilt per slide so CSS animations restart with it. const svg = $("player-scene"); svg.replaceChildren(sceneDefs()); try { svg.append(...SCENES[slide.scene[0]](slide.scene[1] ?? {})); } catch { svg.append(sceneFallbackText(slide.title)); } $("player-caption").textContent = slide.transcript; const dots = $("player-dots"); dots.replaceChildren( ...lesson.slides.map((s, i) => { const dot = document.createElement("button"); dot.type = "button"; dot.className = `academy-dot${i === index ? " active" : ""}`; dot.setAttribute("aria-label", `Slide ${i + 1}: ${s.title}`); dot.addEventListener("click", () => { current.index = i; renderSlide(); }); return dot; }), ); $("player-prev").disabled = index === 0; $("player-next").textContent = index === lesson.slides.length - 1 ? "Finish lesson" : "Next →"; audio.src = `/academy-audio/${slide.id}.mp3?v=__ASSET_VERSION__`; if (autoplay) audio.play().catch(() => setPlayState(false)); setPlayState(autoplay); } function setPlayState(playing) { $("player-play").textContent = playing ? "❚❚" : "▶"; $("player-play").setAttribute("aria-label", playing ? "Pause narration" : "Play narration"); } function next() { const { lesson, index } = current; if (index >= lesson.slides.length - 1) { saveProgress(lesson.id, { done: true, slide: 0 }); closeLesson(); return; } current.index++; renderSlide(); } function wirePlayer() { $("player-close").addEventListener("click", closeLesson); $("player-prev").addEventListener("click", () => { if (current.index > 0) { current.index--; renderSlide(); } }); $("player-next").addEventListener("click", next); $("player-play").addEventListener("click", () => { if (audio.paused) { autoplay = true; audio.play().catch(() => {}); setPlayState(true); } else { audio.pause(); setPlayState(false); } }); audio.addEventListener("ended", () => { setPlayState(false); // Give the animation a beat, then advance — hands-free lesson flow. if (autoplay && current) setTimeout(() => current && next(), 1200); }); document.addEventListener("keydown", (event) => { if (!current) return; if (event.key === "ArrowRight") next(); if (event.key === "ArrowLeft" && current.index > 0) { current.index--; renderSlide(); } if (event.key === "Escape") closeLesson(); if (event.key === " " && event.target === document.body) { event.preventDefault(); $("player-play").click(); } }); } document.getElementById("btn-logout").addEventListener("click", async () => { await protectedFetch("/api/auth/logout", { method: "POST" }); location.assign("/"); }); async function init() { initSideNav(); const user = await loadNavUser(); if (!user) return; renderMenu(); wirePlayer(); } init();