"use client"; import { useEffect, useState, useRef } from "react"; import Link from "next/link"; import { AcademyPackageSchema } from "@/lib/schemas/academy"; import type { AcademyPackage } from "@/lib/schemas/academy"; import { getVideoObjectUrl, getVideoMeta, getYoutubeId, getVideoUrl } from "@/lib/video-store"; import { getTheme } from "@/lib/theme"; type Lesson = AcademyPackage["curriculum"][number]["lessons"][number]; type DraftLesson = { title: string; description: string; notes: string; keyTakeaways: string; // newline-separated actionItems: string; // newline-separated }; const TYPE_COLORS: Record = { video: "bg-violet-500/20 text-violet-300 border-violet-500/30", reading: "bg-sky-500/20 text-sky-300 border-sky-500/30", quiz: "bg-amber-500/20 text-amber-300 border-amber-500/30", exercise: "bg-orange-500/20 text-orange-300 border-orange-500/30", }; const TYPE_ICONS: Record = { video: "▶", reading: "📄", quiz: "✏", exercise: "🏋", }; // ── Inline markdown renderer (bold / italic) ────────────────────────────────── function InlineContent({ text }: { text: string }) { const parts: Array<{ t: "text" | "bold" | "italic"; v: string }> = []; const re = /(\*\*([^*\n]+?)\*\*|\*([^*\n]+?)\*)/g; let last = 0; let m: RegExpExecArray | null; while ((m = re.exec(text)) !== null) { if (m.index > last) parts.push({ t: "text", v: text.slice(last, m.index) }); if (m[0].startsWith("**")) parts.push({ t: "bold", v: m[2] }); else parts.push({ t: "italic", v: m[3] }); last = m.index + m[0].length; } if (last < text.length) parts.push({ t: "text", v: text.slice(last) }); return ( <> {parts.map((p, i) => p.t === "bold" ? {p.v} : p.t === "italic" ? {p.v} : {p.v} )} ); } // ── Block-level markdown renderer ──────────────────────────────────────────── function MarkdownNotes({ text }: { text: string }) { const lines = text.split("\n"); const blocks: React.ReactNode[] = []; let i = 0; while (i < lines.length) { const raw = lines[i]; const line = raw.trim(); if (!line) { i++; continue; } // H1 if (/^# /.test(line)) { blocks.push(

); } // H2 else if (/^## /.test(line)) { blocks.push(

); } // H3 else if (/^### /.test(line)) { blocks.push(

); } // Horizontal rule else if (/^---+$/.test(line)) { blocks.push(
); } // Blockquote else if (/^> /.test(line)) { blocks.push(
); } // Numbered list — collect consecutive items else if (/^\d+\. /.test(line)) { const items: React.ReactNode[] = []; const startI = i; while (i < lines.length && /^\d+\. /.test(lines[i].trim())) { items.push(
  • ); i++; } blocks.push(
      {items}
    ); continue; } // Bullet list — collect consecutive items else if (/^[-*•] /.test(line)) { const items: React.ReactNode[] = []; const startI = i; while (i < lines.length && /^[-*•] /.test(lines[i].trim())) { items.push(
  • ); i++; } blocks.push(); continue; } // Regular paragraph else { blocks.push(

    ); } i++; } return
    {blocks}
    ; } export default function LearnPage() { const [academy, setAcademy] = useState(null); const [missing, setMissing] = useState(false); const [activeModule, setActiveModule] = useState(0); const [activeLesson, setActiveLesson] = useState(null); const [videoUrl, setVideoUrl] = useState(null); const [youtubeId, setYoutubeIdState] = useState(null); const [cloudVideoUrl, setCloudVideoUrl] = useState(null); const [totalDuration, setTotalDuration] = useState(0); const [lessonEndTime, setLessonEndTime] = useState(0); const [selectedAnswers, setSelectedAnswers] = useState([]); const [completedLessons, setCompletedLessons] = useState>(new Set()); const [isEditing, setIsEditing] = useState(false); const [draft, setDraft] = useState(null); // Mobile: toggle between lesson list and lesson content const [mobileView, setMobileView] = useState<"list" | "content">("list"); const videoRef = useRef(null); // Build a flat ordered list of all lessons with start + end times in seconds function buildTimestamps(ac: AcademyPackage, knownTotalSecs: number) { const allLessons = ac.curriculum.flatMap((m) => m.lessons); const totalAiMins = allLessons.reduce((s, l) => s + l.durationMinutes, 0); const scale = knownTotalSecs > 0 && totalAiMins > 0 ? knownTotalSecs / (totalAiMins * 60) : 1; const map = new Map(); let cursor = 0; for (let i = 0; i < allLessons.length; i++) { const start = cursor; cursor += allLessons[i].durationMinutes * 60 * scale; const end = i < allLessons.length - 1 ? cursor : knownTotalSecs; map.set(allLessons[i].title, { start, end }); } return map; } useEffect(() => { try { const raw = localStorage.getItem("nexus_academy_preview"); if (!raw) { setMissing(true); return; } const parsed = AcademyPackageSchema.parse(JSON.parse(raw)); setAcademy(parsed); setActiveLesson(parsed.curriculum[0]?.lessons[0] ?? null); } catch { setMissing(true); } getVideoObjectUrl().then((url) => { if (url) setVideoUrl(url); }); // YouTube embed takes priority over local blob const ytId = getYoutubeId(); if (ytId) setYoutubeIdState(ytId); // R2 / cloud URL const cloudUrl = getVideoUrl(); if (cloudUrl) setCloudVideoUrl(cloudUrl); const meta = getVideoMeta(); if (meta?.durationSecs) setTotalDuration(meta.durationSecs); const savedProgress = localStorage.getItem("nexus_completed_lessons"); if (savedProgress) { try { setCompletedLessons(new Set(JSON.parse(savedProgress) as string[])); } catch { /* ignore */ } } return () => { if (videoUrl) URL.revokeObjectURL(videoUrl); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); function openEdit() { if (!activeLesson) return; setDraft({ title: activeLesson.title, description: activeLesson.description, notes: activeLesson.notes ?? "", keyTakeaways: (activeLesson.keyTakeaways ?? []).join("\n"), actionItems: (activeLesson.actionItems ?? []).join("\n"), }); setIsEditing(true); } function cancelEdit() { setIsEditing(false); setDraft(null); } function saveEdit() { if (!academy || !activeLesson || !draft) return; const oldTitle = activeLesson.title; const newCurriculum = academy.curriculum.map((mod) => ({ ...mod, lessons: mod.lessons.map((l) => l.title === oldTitle ? { ...l, title: draft.title.trim() || oldTitle, description: draft.description, notes: draft.notes, keyTakeaways: draft.keyTakeaways.split("\n").map((s) => s.trim()).filter(Boolean), actionItems: draft.actionItems.split("\n").map((s) => s.trim()).filter(Boolean), } : l ), })); const updated = { ...academy, curriculum: newCurriculum }; const newTitle = draft.title.trim() || oldTitle; // Preserve completion state if title changed if (oldTitle !== newTitle && completedLessons.has(oldTitle)) { const next = new Set(completedLessons); next.delete(oldTitle); next.add(newTitle); setCompletedLessons(next); localStorage.setItem("nexus_completed_lessons", JSON.stringify([...next])); } setAcademy(updated); setActiveLesson(newCurriculum.flatMap((m) => m.lessons).find((l) => l.title === newTitle) ?? null); localStorage.setItem("nexus_academy_preview", JSON.stringify(updated)); setIsEditing(false); setDraft(null); } function toggleComplete(title: string) { setCompletedLessons((prev) => { const next = new Set(prev); if (next.has(title)) next.delete(title); else next.add(title); localStorage.setItem("nexus_completed_lessons", JSON.stringify([...next])); return next; }); } // G: Keyboard navigation — ← / → to move between lessons, Space to mark complete useEffect(() => { function onKey(e: KeyboardEvent) { if (!academy || !activeLesson) return; const tag = (e.target as HTMLElement).tagName; if (["INPUT", "TEXTAREA", "BUTTON"].includes(tag)) return; const flatLessons = academy.curriculum.flatMap((m) => m.lessons); const idx = flatLessons.findIndex((l) => l.title === activeLesson.title); if (e.key === "ArrowRight" && idx < flatLessons.length - 1) { const next = flatLessons[idx + 1]; const mi = academy.curriculum.findIndex((m) => m.lessons.some((l) => l.title === next.title)); setActiveModule(mi); setActiveLesson(next); setSelectedAnswers([]); if (videoRef.current && next.type === "video") { const map = buildTimestamps(academy, totalDuration); const seg = map.get(next.title); if (seg) { setLessonEndTime(seg.end); videoRef.current.currentTime = seg.start; videoRef.current.play().catch(() => {}); } } } else if (e.key === "ArrowLeft" && idx > 0) { const prev = flatLessons[idx - 1]; const mi = academy.curriculum.findIndex((m) => m.lessons.some((l) => l.title === prev.title)); setActiveModule(mi); setActiveLesson(prev); setSelectedAnswers([]); if (videoRef.current && prev.type === "video") { const map = buildTimestamps(academy, totalDuration); const seg = map.get(prev.title); if (seg) { setLessonEndTime(seg.end); videoRef.current.currentTime = seg.start; videoRef.current.play().catch(() => {}); } } } else if (e.key === " ") { e.preventDefault(); toggleComplete(activeLesson.title); } } window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); // eslint-disable-next-line react-hooks/exhaustive-deps }, [academy, activeLesson, totalDuration]); if (missing || !academy) { return (

    No academy data found —{" "} go back and run the pipeline first.

    ); } const mod = academy.curriculum[activeModule]; const allLessons = academy.curriculum.flatMap((m) => m.lessons); const totalLessons = allLessons.length; const completedCount = completedLessons.size; const progressPct = totalLessons > 0 ? Math.round((completedCount / totalLessons) * 100) : 0; const allDone = completedCount === totalLessons && totalLessons > 0; const timestamps = buildTimestamps(academy, totalDuration); const t = getTheme(academy.themeVariant); function selectLesson(lesson: Lesson) { setActiveLesson(lesson); setSelectedAnswers([]); setMobileView("content"); if (videoRef.current && lesson.type === "video") { const seg = timestamps.get(lesson.title); if (seg) { setLessonEndTime(seg.end); videoRef.current.currentTime = seg.start; videoRef.current.play().catch(() => {/* autoplay blocked */}); } } } return ( <>
    {/* ── Top nav ── */} {/* E: Certificate banner — shown when all lessons are complete */} {allDone && (

    Course Complete

    {academy.certificateTitle || academy.academyName}

    You have completed all {totalLessons} lessons. Congratulations.

    )}
    {/* ── Module sidebar ── */} {/* ── Content column (mobile tabs + lesson list/viewer stacked) ── */}
    {/* ── Mobile module tabs ── */}
    {academy.curriculum.map((m, mi) => ( ))}
    {/* ── Lesson list + viewer ── */}

    {mod.moduleTitle}

    {mod.moduleDescription}

    {mod.learningObjectives && mod.learningObjectives.length > 0 && (

    Learning Objectives

      {mod.learningObjectives.map((obj, i) => (
    • {obj}
    • ))}
    )}
      {mod.lessons.map((lesson, li) => { const isActive = activeLesson?.title === lesson.title; return (
    1. ); })}
    {/* Lesson viewer */} {activeLesson && (
    {/* Mobile back-to-lessons button */} {/* Lesson type badge */} {TYPE_ICONS[activeLesson.type]} {activeLesson.type}

    {activeLesson.title}

    {activeLesson.durationMinutes} min

    {/* Video player */} {activeLesson.type === "video" && (
    {youtubeId ? ( /* YouTube nocookie embed — no ad tracking */