import { useEffect, useMemo, useState, type CSSProperties, type ReactNode } from "react"; import { Link, useLocation } from "react-router-dom"; import { CourseCover, ProgressBar } from "@stc/ui"; import type { CoursePayload, OutlineLesson, OutlineModule } from "../../shared/types"; import { courseHref, ensureStyles, fmtMinutes, fmtTime, lessonHref, useSearch, type Look } from "../api"; import { Check, Chev, Lock } from "../icons"; import { HeaderExtras } from "./HeaderExtras"; import { TitleText } from "../lesson/TitleText"; const fold = (s: string) => s.normalize("NFKD").replace(/[̀-ͯ]/g, "").toLowerCase(); export function lessonKind(l: OutlineLesson): string { if (l.type === "video") return l.minutes ? (l.minutes < 60 ? fmtTime(l.minutes * 60) : fmtMinutes(l.minutes)) : "Video"; if (l.type === "exercise") return "Exercise"; return l.hasDeck ? "Slides" : "Text"; } export function courseDone(c: CoursePayload): { done: number; total: number } { const all = c.modules.flatMap((m) => m.lessons); return { done: all.filter((l) => c.progress[l.id]?.state === "done").length, total: all.length }; } /** Display number of a module: courses whose first module id ends in 0 ("m00") count from 0. */ export function moduleNumber(course: { modules: { id: string }[] }, index: number): number { return /(^|\D)0+$/.test(course.modules[0]?.id ?? "") ? index : index + 1; } export function accentStyle(accent: string | null | undefined): CSSProperties | undefined { return accent ? ({ "--accent": accent } as CSSProperties) : undefined; } /** The course page frame: outline sidebar (an overlay on narrow screens) and the main column. */ export function CourseFrame({ course, currentLessonId, look, lookMode, children }: { course: CoursePayload; currentLessonId: string | null; look: Look; lookMode: "toggle" | "work" | null; children: ReactNode; }) { ensureStyles(); const [open, setOpen] = useState(false); const loc = useLocation(); useEffect(() => { setOpen(false); }, [loc.pathname]); useEffect(() => { if (!open) return; const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") setOpen(false); }; document.addEventListener("keydown", onKey); return () => document.removeEventListener("keydown", onKey); }, [open]); return (
No lessons match “{q.trim()}”.
: null} > ); } function ModuleBlock({ course, m, lessons, open, current, currentLessonId, onToggle }: { course: CoursePayload; m: OutlineModule; lessons: OutlineLesson[]; open: boolean; current: boolean; currentLessonId: string | null; onToggle: () => void; }) { const d = m.lessons.filter((l) => course.progress[l.id]?.state === "done").length; const pct = m.lessons.length ? d / m.lessons.length : 0; const number = String(moduleNumber(course, m.index)); return (