import { useState, useCallback, useEffect, useRef } from "react"; import AppBreadcrumb from "@/components/generic/Breadcrumb/AppBreadcrumb"; import { useParams, useNavigate, useLocation, useBlocker } from "react-router-dom"; import { House, TableOfContents, ArrowRight, ClipboardList, GraduationCap, Trophy, Lock, CheckCircle2, Circle, Zap, ListChecks, ChevronsLeft, ChevronsRight, Hourglass } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, } from "@/components/ui/accordion"; import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"; import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet"; import ResponsiveModal from "@/components/generic/ResponsiveModal"; import { InfoDialog } from "@/components/generic/Dialogs/Client/InfoDialog"; import LessonBlock from "../components/LessonBlock.jsx"; import QuizBlock from "../components/blocks/QuizBlock.jsx"; import CourseCompleteBlock from "../components/blocks/CourseCompleteBlock.jsx"; import MarkCompleteButton from "../components/MarkCompleteButton.jsx"; import { useClientCourses } from "@/contexts/ClientCoursesContext"; import { PageMeta } from "@/contexts/MetadataContext"; import { useCourseReadingProgress } from "@/contexts/ClientCourseReadingProgressContext"; import { useProfile } from "@/contexts/ProfileProvider"; import { Skeleton } from "@/components/ui/skeleton"; import { toast } from "sonner"; import api from "@/utils/api.util"; // ─── Quiz Prerequisite Gate ──────────────────────────────────────────────── const QuizPrerequisiteGate = ({ previousQuizzes, units, onQuizClick }) => { const [open, setOpen] = useState(false); const passed = previousQuizzes.filter((q) => q.has_passed).length; const useModal = previousQuizzes.length > QUIZ_MODAL_THRESHOLD; return (

Complete Previous {previousQuizzes.length > 1 ? "Quizzes" : "Quiz"} First

You must pass the required {previousQuizzes.length > 1 ? "quizzes" : "quiz"} before you can take this one.

{useModal ? ( <>

{passed} of {previousQuizzes.length} quizzes passed

setOpen(false)} /> ) : (
)}
); }; // ─── Assessment Gate ─────────────────────────────────────────────────────── const QUIZ_MODAL_THRESHOLD = 1; const QuizGateList = ({ requiredQuizzes, units, onQuizClick, onItemClick }) => (
{requiredQuizzes.map((q) => (
{ onQuizClick({ unit: units.find((u) => u.unit_id === q.unitId), quiz: { quiz_id: q.quizId } }); onItemClick?.(); }} className={`flex items-center gap-3 p-3 rounded-lg border transition-colors ${ q.has_passed ? "bg-emerald-50 dark:bg-emerald-900/20 border-emerald-200 dark:border-emerald-800" : "border-border hover:bg-muted cursor-pointer" }`} > {q.has_passed ? : } {q.title} {!q.has_passed && }
))}
); const AssessmentGate = ({ requiredQuizzes, units, onQuizClick }) => { const [open, setOpen] = useState(false); const passed = requiredQuizzes.filter((q) => q.has_passed).length; const useModal = requiredQuizzes.length > QUIZ_MODAL_THRESHOLD; return (

Complete All Unit Quizzes First

You must pass every required unit quiz before you can attempt the Final Assessment.

{useModal ? ( <>

{passed} of {requiredQuizzes.length} quizzes passed

setOpen(false)} /> ) : (
)}
); }; // ─── Sidebar ─────────────────────────────────────────────────────────────── const SidebarContent = ({ units, selectedLessonId, selectedQuizId, onLessonClick, onQuizClick, courseAssessment, selectedAssessment, onAssessmentClick, assessmentLocked, isCompleted, selectedCompletion, onCompletionClick, getLessonCompleted, getUnitCompleted, isQuizLocked, loading, }) => (

Course Content

{loading ? (
{Array.from({ length: 4 }).map((_, i) => ( ))}
) : ( <> {units.map((unit, index) => ( {getUnitCompleted?.(unit) ? : } Unit {index + 1}: {unit.title}
    {(unit.lessons ?? []).map((lesson) => { const lessonDone = getLessonCompleted?.(lesson); return (
  • onLessonClick({ unit, lesson })} className={`flex items-center gap-2 pl-4 pr-3 py-1.5 text-sm rounded-md hover:bg-muted-foreground/10 hover:text-foreground cursor-pointer transition-colors ${selectedLessonId === lesson.lesson_id ? "bg-muted-foreground/10 text-foreground font-medium" : "text-muted-foreground" }`} > {lessonDone ? : } {lesson.title}
  • ); })} {unit.quiz && (() => { const quizLocked = isQuizLocked?.(unit); return (
  • onQuizClick({ unit, quiz: unit.quiz })} className={`flex items-center gap-2 pl-6 pr-3 py-1.5 text-sm rounded-md cursor-pointer transition-colors ${ selectedQuizId === unit.quiz.quiz_id ? "bg-muted-foreground/10 text-foreground font-medium" : unit.quiz.has_passed ? "text-emerald-600 dark:text-emerald-400 hover:bg-muted-foreground/10" : quizLocked ? "text-muted-foreground/50 hover:bg-muted-foreground/5" : "text-muted-foreground hover:bg-muted-foreground/10 hover:text-foreground" }`} > {unit.quiz.has_passed ? : quizLocked ? : } {"Quiz"}
  • ); })()}
))}
{courseAssessment && (
{assessmentLocked ? : } Assessment
)} {isCompleted && (
Course Complete
)} )}
); // ─── UnitList ───────────────────────────────────────────────────────────── const UnitList = () => { const { id: courseId } = useParams(); const navigate = useNavigate(); const location = useLocation(); const { course, courseLoading, courseBlocked, getCourse, lesson, lessonLoading, getLesson, resetLesson, quiz, quizLoading, getUnitQuiz, resetQuiz, submitUnitQuiz, saveQuizDraft, assessment, assessmentLoading, getCourseAssessment, resetAssessment, startCourseAssessment, saveDraft, refreshAssessmentSession, submitCourseAssessment, } = useClientCourses(); const { fetchCourseProgress, upsertLessonProgress, upsertWatchProgress, markComplete, isCompleted: isProgressCompleted, isRead: isProgressRead, completedTasks, clearCompletedTasks, resetProgress, } = useCourseReadingProgress(); const { profile, getProfile } = useProfile(); // Tracks which lessons have been marked completed this session to avoid duplicate calls const completedSessionRef = useRef(new Set()); // ── Task context — populated from navigation state or backend fallback ── const [taskCtx, setTaskCtx] = useState(null); // ── Local UI state ────────────────────────────────────────────────────── const [selectedLessonId, setSelectedLessonId] = useState(null); const [selectedQuizId, setSelectedQuizId] = useState(null); const [selectedAssessment, setSelectedAssessment] = useState(false); const [selectedCompletion, setSelectedCompletion] = useState(false); const [selectedUnitId, setSelectedUnitId] = useState(null); const [sidebarOpen, setSidebarOpen] = useState(false); const [desktopSidebarOpen, setDesktopSidebarOpen] = useState(true); const resetCompletion = () => setSelectedCompletion(false); // ── No-assessment notice — shown once per course visit, informational only ── // Gated behind the "Course notices" toggle in Account Settings. const [noAssessmentDialogOpen, setNoAssessmentDialogOpen] = useState(false); const noAssessmentNoticeShown = useRef(false); useEffect(() => { getProfile(); }, []); // eslint-disable-line react-hooks/exhaustive-deps useEffect(() => { noAssessmentNoticeShown.current = false; }, [courseId]); useEffect(() => { const shouldShow = course && !courseBlocked && course.duration_seconds && !course.assessment && !noAssessmentNoticeShown.current && profile && (profile.personal_info?.show_course_notices ?? true); if (!shouldShow) return; noAssessmentNoticeShown.current = true; setNoAssessmentDialogOpen(true); }, [course, courseBlocked, profile]); // ── Session guard — block navigation while a quiz/assessment is in progress ─ const quizActiveRef = useRef(false); // sync check inside handlers const [quizSessionActive, setQuizSessionActive] = useState(false); const [pendingNav, setPendingNav] = useState(null); // deferred nav fn const setQuizActive = useCallback((active) => { quizActiveRef.current = active; setQuizSessionActive(active); }, []); // ── Quiz gate — all required unit quizzes must be passed before assessment ─ const requiredQuizzes = (course?.units ?? []) .filter((u) => u.quiz?.is_required) .map((u) => ({ unit: u, unitId: u.unit_id, quizId: u.quiz.quiz_id, title: u.quiz.title || "Quiz", has_passed: u.quiz.has_passed ?? false, })); const allRequiredQuizzesPassed = requiredQuizzes.length === 0 || requiredQuizzes.every((q) => q.has_passed); // ── Quiz sequential lock — unit N's quiz is locked until all previous required quizzes are passed ─ const lockedQuizUnitIds = new Set( (course?.units ?? []) .filter((u, i, arr) => u.quiz && arr.slice(0, i).some(prev => prev.quiz?.is_required && !prev.quiz.has_passed) ) .map(u => u.unit_id) ); const selectedUnitIndex = (course?.units ?? []).findIndex(u => u.unit_id === selectedUnitId); const previousRequiredQuizzes = selectedUnitIndex > 0 ? (course?.units ?? []).slice(0, selectedUnitIndex) .filter(u => u.quiz?.is_required) .map(u => ({ unit: u, unitId: u.unit_id, quizId: u.quiz.quiz_id, title: u.quiz.title || "Quiz", has_passed: u.quiz.has_passed ?? false, })) : []; // ── Block React Router navigation (back button / programmatic navigate) ── const blocker = useBlocker( ({ currentLocation, nextLocation }) => quizSessionActive && currentLocation.pathname !== nextLocation.pathname ); useEffect(() => { if (blocker.state !== "blocked") return; setPendingNav(() => () => blocker.proceed()); }, [blocker.state]); // eslint-disable-line react-hooks/exhaustive-deps // ── Block browser tab-close / hard navigation during active session ─────── useEffect(() => { if (!quizSessionActive) return; const handler = (e) => { e.preventDefault(); e.returnValue = ''; }; window.addEventListener('beforeunload', handler); return () => window.removeEventListener('beforeunload', handler); }, [quizSessionActive]); // ── Flatten all content (lessons + quiz + final assessment) ────────────── const allContent = (course?.units ?? []).flatMap((u) => [ ...(u.lessons ?? []).map((l) => ({ type: "lesson", unit: u, lesson: l })), ...(u.quiz ? [{ type: "quiz", unit: u, quiz: u.quiz }] : []), ]); if (course?.assessment && allRequiredQuizzesPassed) { allContent.push({ type: "assessment", assessment: course.assessment }); } // ── Scroll progress ──────────────────────────────────────────────────── const [scrollProgress, setScrollProgress] = useState(0); useEffect(() => { setScrollProgress(0); window.scrollTo(0, 0); }, [selectedLessonId, selectedQuizId, selectedAssessment, selectedCompletion]); useEffect(() => { const handleScroll = () => { const scrollTop = window.scrollY; const scrollHeight = document.documentElement.scrollHeight - window.innerHeight; if (scrollHeight <= 0) { setScrollProgress(100); return; } setScrollProgress(Math.round((scrollTop / scrollHeight) * 100)); }; window.addEventListener("scroll", handleScroll, { passive: true }); return () => window.removeEventListener("scroll", handleScroll); }, []); // ── Lesson completion-trigger dispatch ───────────────────────────────── // read_all_content (or unconfigured/default) → scroll-to-bottom, below. // watch_percent / manual_complete → their own dedicated triggers further down // (video/audio onWatchProgress callback, MarkCompleteButton) — the scroll // trigger must NOT also fire completion for those, so it's gated here. const selectedUnitStub = course?.units?.find((u) => u.unit_id === selectedUnitId); const selectedLessonStub = selectedUnitStub?.lessons?.find((l) => l.lesson_id === selectedLessonId) ?? null; const selectedLessonCompletionType = selectedLessonStub?.completion?.type ?? 'read_all_content'; // Quiz/assessment submits go through ClientCoursesContext, not // ClientCourseReadingProgressContext, so their completed_tasks (pass_quiz can complete a // read_unit/read_course task requirement with no lesson ever read) don't flow through the // shared completedTasks toast effect above — surface them directly here instead. const notifyCompletedTasks = useCallback((tasks) => { (tasks ?? []).forEach((t) => toast(`"${t.task_name}" automatically turned in!`)); }, []); // ── watch_percent / watch_video / listen_audio trigger: video/audio block // reports playback progress. meta.blockId/blockType (attached by PreviewBlock) // let the backend drive the per-block watch_video/listen_audio types alongside // the aggregate watch_percent one — safe to always pass through, the backend // no-ops whichever type isn't configured on the lesson. const handleWatchProgress = useCallback((percent, meta) => { if (!selectedLessonId || !selectedUnitId) return; upsertWatchProgress(courseId, selectedUnitId, selectedLessonId, percent, meta); }, [courseId, selectedUnitId, selectedLessonId, upsertWatchProgress]); // ── manual_complete trigger: learner clicks the Mark Complete button ──── const handleMarkComplete = useCallback(async () => { if (!selectedLessonId || !selectedUnitId) return; const result = await markComplete(courseId, selectedUnitId, selectedLessonId); if (result?.course?.status === 'completed') { toast.success('All lessons read! Finish the quizzes & assessment to get certified.', { duration: 5000 }); } }, [courseId, selectedUnitId, selectedLessonId, markComplete]); // ── Mark lesson completed when user scrolls to the bottom ───────────── useEffect(() => { if (selectedLessonCompletionType !== 'read_all_content') return; if (scrollProgress < 100 || !selectedLessonId || !selectedUnitId || !lesson?.uuid) return; if (completedSessionRef.current.has(selectedLessonId)) return; if (isProgressCompleted(lesson.uuid)) return; completedSessionRef.current.add(selectedLessonId); (async () => { const result = await upsertLessonProgress(courseId, selectedUnitId, selectedLessonId, lesson.uuid, 'completed'); if (!result) return; // toast.success(`"${lesson.title}" marked as read.`); // if (result.unit?.status === 'completed') { // const unitTitle = currentUnit?.title; // toast.success( // unitTitle ? `Unit "${unitTitle}" complete!` : 'Unit complete!', // { duration: 4000 } // ); // } // if (result.course?.status === 'completed') { // toast.success( // 'All lessons read! Finish the quizzes & assessment to get certified.', // { duration: 5000 } // ); // } })(); }, [scrollProgress]); // ── Fetch course + progress on mount ───────────────────────────────── useEffect(() => { getCourse(courseId); fetchCourseProgress(courseId); return () => { resetLesson(); resetQuiz(); resetAssessment(); resetProgress(); completedSessionRef.current.clear(); }; }, [courseId]); // ── Task context: state-first, endpoint fallback ────────────────────── // If navigated from ReadCourse the taskCtx is in location.state; // if the user opened this URL directly, fetch from the backend. useEffect(() => { const stateCtx = location.state?.taskCtx; if (stateCtx) { setTaskCtx(stateCtx); return; } api.get(`/client/courses/${courseId}/task-context`) .then(({ data }) => { if (data?.data?.has_task) setTaskCtx(data.data); }) .catch(() => {}); // non-critical — silently swallow }, [courseId]); // ── Toast when a task's read requirements are all done ──────────────── useEffect(() => { if (!completedTasks.length) return; completedTasks.forEach((t) => { toast(`"${t.task_name}" automatically turned in!`); }); clearCompletedTasks(); }, [completedTasks]); // ── Auto-load lesson once course data is available ──────────────────── // If navigated from CourseDetails with a specific lesson, open that one; // otherwise fall back to the first lesson. useEffect(() => { if (!course || selectedLessonId || selectedQuizId || selectedAssessment || selectedCompletion) return; const { lessonId, unitId, seekFirstIncomplete, quizUnitId, seekAssessment } = location.state ?? {}; if (seekAssessment && course.assessment) { setSelectedAssessment(true); if (allRequiredQuizzesPassed) getCourseAssessment(courseId); return; } if (quizUnitId) { const targetUnit = (course.units ?? []).find((u) => String(u.unit_id) === String(quizUnitId)); if (targetUnit?.quiz) { setSelectedQuizId(targetUnit.quiz.quiz_id); setSelectedUnitId(targetUnit.unit_id); if (!lockedQuizUnitIds.has(targetUnit.unit_id)) getUnitQuiz(courseId, targetUnit.unit_id); return; } } if (seekFirstIncomplete) { const firstIncompleteUnit = (course.units ?? []).find((u) => u.quiz && !u.quiz.has_passed); if (firstIncompleteUnit) { setSelectedQuizId(firstIncompleteUnit.quiz.quiz_id); setSelectedUnitId(firstIncompleteUnit.unit_id); getUnitQuiz(courseId, firstIncompleteUnit.unit_id); return; } if (course.assessment) { setSelectedAssessment(true); if (allRequiredQuizzesPassed) getCourseAssessment(courseId); return; } } if (lessonId && unitId) { setSelectedLessonId(lessonId); setSelectedUnitId(unitId); getLesson(courseId, unitId, lessonId); } else { const firstUnit = course.units?.[0]; const firstLesson = firstUnit?.lessons?.[0]; if (firstUnit && firstLesson) { setSelectedLessonId(firstLesson.lesson_id); setSelectedUnitId(firstUnit.unit_id); getLesson(courseId, firstUnit.unit_id, firstLesson.lesson_id); } } }, [course]); // ── Breadcrumbs ───────────────────────────────────────────────────────── const currentUnit = course?.units?.find((u) => u.unit_id === selectedUnitId); const currentLesson = lesson; const currentLessonStub = currentUnit?.lessons?.find((l) => l.lesson_id === selectedLessonId) ?? null; const pageTitle = (() => { if (!course) return undefined; if (selectedCompletion) return `${course.title} – Completed - STARR`; if (selectedAssessment) return `${course.title} – Assessment - STARR`; if (selectedQuizId && currentUnit) return `${currentUnit.title} – Quiz - STARR`; const lessonTitle = lesson?.title ?? currentLessonStub?.title; if (selectedLessonId && lessonTitle) return `${lessonTitle} - STARR`; return `${course.title} - STARR`; })(); const breadcrumbItemsFull = [ { label: "Home", icon: , to: `/dashboard` }, { label: "Courses", to: `/course` }, { label: course?.title ?? "Course", to: `/course/${courseId}` }, { label: selectedCompletion ? "Course Complete" : selectedAssessment ? "Course Assessment" : currentUnit ? `Unit ${(course?.units?.indexOf(currentUnit) ?? 0) + 1}: ${currentUnit.title}` : "Select a lesson" }, ]; const breadcrumbItemsMobile = [ { label: "Home", icon: , to: `/dashboard` }, { label: selectedCompletion ? "Course Complete" : selectedAssessment ? "Course Assessment" : (currentUnit?.title ?? "Select a lesson") }, ]; // ── Session guard helpers ────────────────────────────────────────────── const handleConfirmNav = useCallback(() => { const fn = pendingNav; setPendingNav(null); quizActiveRef.current = false; setQuizSessionActive(false); if (blocker.state === "blocked") blocker.proceed(); else fn?.(); }, [pendingNav, blocker]); const handleCancelNav = useCallback(() => { setPendingNav(null); if (blocker.state === "blocked") blocker.reset(); }, [blocker]); // ── Lesson click ─────────────────────────────────────────────────────── const handleLessonClick = useCallback(async ({ unit, lesson: lessonStub }) => { if (lessonStub.lesson_id === selectedLessonId) { setSidebarOpen(false); return; } const doNav = async () => { setSelectedLessonId(lessonStub.lesson_id); setSelectedQuizId(null); setSelectedAssessment(false); resetCompletion(); resetQuiz(); resetAssessment(); setSelectedUnitId(unit.unit_id); setSidebarOpen(false); await getLesson(courseId, unit.unit_id, lessonStub.lesson_id); if (!isProgressRead(lessonStub.uuid)) { upsertLessonProgress(courseId, unit.unit_id, lessonStub.lesson_id, lessonStub.uuid, 'in_progress'); } }; if (quizActiveRef.current) { setPendingNav(() => doNav); return; } await doNav(); }, [selectedLessonId, courseId, getLesson, resetQuiz, resetAssessment, isProgressRead, upsertLessonProgress]); // ── Quiz click ───────────────────────────────────────────────────────── const handleQuizClick = useCallback(async ({ unit, quiz: quizStub }) => { if (quizStub.quiz_id === selectedQuizId) { setSidebarOpen(false); return; } const doNav = async () => { setSelectedQuizId(quizStub.quiz_id); setSelectedLessonId(null); setSelectedAssessment(false); resetCompletion(); resetLesson(); resetAssessment(); setSelectedUnitId(unit.unit_id); setSidebarOpen(false); if (!lockedQuizUnitIds.has(unit.unit_id)) { await getUnitQuiz(courseId, unit.unit_id); } }; if (quizActiveRef.current) { setPendingNav(() => doNav); return; } await doNav(); }, [selectedQuizId, courseId, getUnitQuiz, resetLesson, resetAssessment, lockedQuizUnitIds]); // ── Course assessment click ───────────────────────────────────────────── const handleAssessmentClick = useCallback(async () => { if (selectedAssessment) { setSidebarOpen(false); return; } const doNav = async () => { setSelectedAssessment(true); setSelectedLessonId(null); setSelectedQuizId(null); resetCompletion(); resetLesson(); resetQuiz(); setSidebarOpen(false); if (allRequiredQuizzesPassed) await getCourseAssessment(courseId); }; if (quizActiveRef.current) { setPendingNav(() => doNav); return; } await doNav(); }, [selectedAssessment, courseId, getCourseAssessment, resetLesson, resetQuiz, allRequiredQuizzesPassed]); // ── Course complete click ─────────────────────────────────────────────── const handleCompletionClick = useCallback(() => { if (selectedCompletion) { setSidebarOpen(false); return; } const doNav = () => { setSelectedLessonId(null); setSelectedQuizId(null); setSelectedAssessment(false); resetLesson(); resetQuiz(); resetAssessment(); setSelectedCompletion(true); setSidebarOpen(false); }; if (quizActiveRef.current) { setPendingNav(() => doNav); return; } doNav(); }, [selectedCompletion, resetLesson, resetQuiz, resetAssessment]); // Stable draft callbacks — avoids recreating on every render (which would re-trigger QuizBlock's onDraft effect) const handleQuizDraft = useCallback((answers) => { saveQuizDraft(courseId, selectedUnitId, selectedQuizId, answers); }, [courseId, selectedUnitId, selectedQuizId, saveQuizDraft]); const handleAssessmentDraft = useCallback((answers) => { if (!assessment?.assessment_id) return; saveDraft(courseId, assessment.assessment_id, answers); }, [courseId, assessment?.assessment_id, saveDraft]); // ── Next content item (lesson, quiz, or final assessment) ───────────── const getNextContent = useCallback(() => { const idx = allContent.findIndex((item) => (item.type === "lesson" && item.lesson.lesson_id === selectedLessonId) || (item.type === "quiz" && item.quiz.quiz_id === selectedQuizId) || (item.type === "assessment" && selectedAssessment) ); return idx !== -1 && idx < allContent.length - 1 ? allContent[idx + 1] : null; }, [selectedLessonId, selectedQuizId, selectedAssessment, allContent]); const units = course?.units ?? []; const nextContent = getNextContent(); const nextLabel = nextContent ? nextContent.type === "lesson" ? nextContent.lesson.title : nextContent.type === "quiz" ? (nextContent.quiz.title || "Quiz") : (nextContent.assessment?.title || "Course Assessment") : null; const showSidebarContent = desktopSidebarOpen && !quizSessionActive; const handleNextContentClick = () => { if (!nextContent) return; if (nextContent.type === "lesson") handleLessonClick(nextContent); else if (nextContent.type === "quiz") handleQuizClick(nextContent); else handleAssessmentClick(); }; // ── Access blocked (403 from getCourse) ────────────────────────────────── if (courseBlocked) { return (

Premium / Exclusive Content

To take this course, we advise you to subscribe to one of our available tier plans and unlock access to this content.

Already subscribed? Your plan may not cover this tier.

); } // ── Content not ready (duration_seconds === 0 — no lesson content authored yet) ── if (!courseLoading && course && !course.duration_seconds) { return (

Course Not Yet Available

This course is currently being prepared. Please check back later.

); } return ( <> {/* ── Session-guard dialog — shown when user tries to navigate away mid-quiz ── */} !open && handleCancelNav()} title={`Leave ${selectedAssessment ? "Assessment" : "Quiz"}?`} description="" footer={ <> } >

You have an ongoing {selectedAssessment ? "assessment" : "quiz"} session in progress. Leaving now will not submit your answers — your session will remain open and the administrator can see it.

{selectedAssessment && (

Your assessment timer will keep counting while you're away.

)}
{/* ── No-assessment notice — informational, shown once per course visit ── */} {/* ── Task-mode banner ─────────────────────────────────────────── */} {taskCtx?.has_task && (
{course?.is_completed ? 'Course complete — tracking finished' : 'Task mode — reading progress is being tracked automatically' }
)} {/* ── Up next floating button (lesson only — quizzes/assessments have their own bottom controls) ── */} {scrollProgress >= 100 && nextContent && !selectedQuizId && !selectedAssessment && (
Up next {nextContent.type === "lesson" ? nextContent.lesson.title : nextContent.type === "quiz" ? (nextContent.quiz.title || "Quiz") : (nextContent.assessment?.title || "Course Assessment")}
)} {/* ── Up next after passing a quiz ── */} {selectedQuizId && quiz?.has_passed && !quizSessionActive && nextContent && (
Up next {nextContent.type === "lesson" ? nextContent.lesson.title : nextContent.type === "quiz" ? (nextContent.quiz.title || "Quiz") : (nextContent.assessment?.title || "Course Assessment")}
)} {/* ── Top sticky bar ── */}
{/* Breadcrumb */}
{/* Mobile hamburger */} isProgressCompleted(l.uuid)} getUnitCompleted={(u) => isProgressCompleted(u.uuid)} isQuizLocked={(u) => lockedQuizUnitIds.has(u.unit_id)} loading={courseLoading} />
{/* Scroll progress bar */} {lesson && (
)}
{/* ── Desktop sidebar: rail + collapsible content panel (desktop only) ── */}
{/* Rail — always visible */}
{/* Content panel — hidden during quiz / assessment session */} {showSidebarContent && (
isProgressCompleted(l.uuid)} getUnitCompleted={(u) => isProgressCompleted(u.uuid)} isQuizLocked={(u) => lockedQuizUnitIds.has(u.unit_id)} loading={courseLoading} />
)}
{/* ── Main content ── */}
{selectedCompletion ? ( ) : selectedAssessment ? ( !allRequiredQuizzesPassed ? ( ) : ( startCourseAssessment(courseId, assessment.assessment_id)} onDraft={handleAssessmentDraft} onRefreshSession={() => refreshAssessmentSession(courseId, assessment.assessment_id)} onSubmit={async (answers, sessionId) => { const result = await submitCourseAssessment(courseId, assessment.assessment_id, answers, sessionId); notifyCompletedTasks(result?.completed_tasks); await getCourse(courseId); return result; }} onRetake={() => getCourseAssessment(courseId)} onActiveChange={setQuizActive} onNextContent={nextContent ? handleNextContentClick : undefined} nextLabel={nextLabel} /> ) ) : selectedQuizId ? ( lockedQuizUnitIds.has(selectedUnitId) ? ( ) : ( { const result = await submitUnitQuiz(courseId, selectedUnitId, selectedQuizId, answers); notifyCompletedTasks(result?.completed_tasks); await getCourse(courseId); return result; }} onRetake={() => getUnitQuiz(courseId, selectedUnitId)} onActiveChange={setQuizActive} onNextContent={nextContent ? handleNextContentClick : undefined} nextLabel={nextLabel} /> ) ) : ( <> {!lessonLoading && lesson && selectedLessonCompletionType === 'manual_complete' && ( )} )}
); }; export default UnitList;