@burdenoff/microfe-buildmyiq 2026.705.13 → 2026.706.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/buildmyiq/BuildmyiqRoot.d.ts.map +1 -1
- package/dist/buildmyiq/BuildmyiqRoot.js +57 -49
- package/dist/buildmyiq/BuildmyiqRoot.js.map +1 -1
- package/dist/buildmyiq/BuildmyiqRoutes.d.ts.map +1 -1
- package/dist/buildmyiq/BuildmyiqRoutes.js +15 -11
- package/dist/buildmyiq/BuildmyiqRoutes.js.map +1 -1
- package/dist/buildmyiq/components/Avatar.d.ts +15 -0
- package/dist/buildmyiq/components/Avatar.d.ts.map +1 -0
- package/dist/buildmyiq/components/Avatar.js +42 -0
- package/dist/buildmyiq/components/Avatar.js.map +1 -0
- package/dist/buildmyiq/components/Viz.d.ts +25 -0
- package/dist/buildmyiq/components/Viz.d.ts.map +1 -0
- package/dist/buildmyiq/components/Viz.js +78 -0
- package/dist/buildmyiq/components/Viz.js.map +1 -0
- package/dist/buildmyiq/components/index.d.ts +2 -0
- package/dist/buildmyiq/components/index.d.ts.map +1 -1
- package/dist/buildmyiq/components/index.js +2 -0
- package/dist/buildmyiq/pages/DashboardPage.d.ts.map +1 -1
- package/dist/buildmyiq/pages/DashboardPage.js +200 -98
- package/dist/buildmyiq/pages/DashboardPage.js.map +1 -1
- package/dist/buildmyiq/pages/assessments/AssessmentDetailPage.d.ts.map +1 -1
- package/dist/buildmyiq/pages/assessments/AssessmentDetailPage.js +262 -241
- package/dist/buildmyiq/pages/assessments/AssessmentDetailPage.js.map +1 -1
- package/dist/buildmyiq/pages/assessments/AssessmentsPage.d.ts.map +1 -1
- package/dist/buildmyiq/pages/assessments/AssessmentsPage.js +142 -115
- package/dist/buildmyiq/pages/assessments/AssessmentsPage.js.map +1 -1
- package/dist/buildmyiq/pages/assessments/TakeAssessmentPage.d.ts.map +1 -1
- package/dist/buildmyiq/pages/assessments/TakeAssessmentPage.js +220 -156
- package/dist/buildmyiq/pages/assessments/TakeAssessmentPage.js.map +1 -1
- package/dist/buildmyiq/pages/authoring/AuthoringPage.d.ts.map +1 -1
- package/dist/buildmyiq/pages/authoring/AuthoringPage.js +51 -46
- package/dist/buildmyiq/pages/authoring/AuthoringPage.js.map +1 -1
- package/dist/buildmyiq/pages/authoring/CourseBuilderPage.d.ts.map +1 -1
- package/dist/buildmyiq/pages/authoring/CourseBuilderPage.js +581 -448
- package/dist/buildmyiq/pages/authoring/CourseBuilderPage.js.map +1 -1
- package/dist/buildmyiq/pages/catalog/CatalogPage.d.ts.map +1 -1
- package/dist/buildmyiq/pages/catalog/CatalogPage.js +52 -42
- package/dist/buildmyiq/pages/catalog/CatalogPage.js.map +1 -1
- package/dist/buildmyiq/pages/catalog/CourseDetailPage.d.ts.map +1 -1
- package/dist/buildmyiq/pages/catalog/CourseDetailPage.js +422 -301
- package/dist/buildmyiq/pages/catalog/CourseDetailPage.js.map +1 -1
- package/dist/buildmyiq/pages/certificates/CertificatesPage.d.ts.map +1 -1
- package/dist/buildmyiq/pages/certificates/CertificatesPage.js +237 -151
- package/dist/buildmyiq/pages/certificates/CertificatesPage.js.map +1 -1
- package/dist/buildmyiq/pages/cohorts/CohortsPage.d.ts.map +1 -1
- package/dist/buildmyiq/pages/cohorts/CohortsPage.js +219 -138
- package/dist/buildmyiq/pages/cohorts/CohortsPage.js.map +1 -1
- package/dist/buildmyiq/pages/insights/InsightsPage.d.ts.map +1 -1
- package/dist/buildmyiq/pages/insights/InsightsPage.js +276 -152
- package/dist/buildmyiq/pages/insights/InsightsPage.js.map +1 -1
- package/dist/buildmyiq/pages/learning/LessonPlayerPage.d.ts.map +1 -1
- package/dist/buildmyiq/pages/learning/LessonPlayerPage.js +237 -174
- package/dist/buildmyiq/pages/learning/LessonPlayerPage.js.map +1 -1
- package/dist/buildmyiq/pages/learning/MyLearningPage.d.ts.map +1 -1
- package/dist/buildmyiq/pages/learning/MyLearningPage.js +307 -169
- package/dist/buildmyiq/pages/learning/MyLearningPage.js.map +1 -1
- package/dist/buildmyiq/pages/paths/PathsPage.d.ts +4 -0
- package/dist/buildmyiq/pages/paths/PathsPage.d.ts.map +1 -0
- package/dist/buildmyiq/pages/paths/PathsPage.js +187 -0
- package/dist/buildmyiq/pages/paths/PathsPage.js.map +1 -0
- package/dist/buildmyiq/pages/skills/SkillsPage.d.ts.map +1 -1
- package/dist/buildmyiq/pages/skills/SkillsPage.js +233 -140
- package/dist/buildmyiq/pages/skills/SkillsPage.js.map +1 -1
- package/dist/buildmyiq/pages/submissions/SubmissionsPage.d.ts.map +1 -1
- package/dist/buildmyiq/pages/submissions/SubmissionsPage.js +379 -275
- package/dist/buildmyiq/pages/submissions/SubmissionsPage.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LessonPlayerPage.js","names":[],"sources":["../../../../src/buildmyiq/pages/learning/LessonPlayerPage.tsx"],"sourcesContent":["/**\n * LessonPlayerPage — the two-pane lesson player for a single enrollment.\n * @module buildmyiq/pages/learning\n *\n * Left pane: the course's module/lesson tree (from `buiCourse`) with a completed\n * check driven by the enrollment's `lessonProgress`. Right pane: the selected\n * lesson (title, type, content text rendered safely, and a `FileLink` for any\n * attached media). Opening a lesson records IN_PROGRESS once; \"Mark complete\"\n * records COMPLETED; finishing every lesson transitions the enrollment to\n * COMPLETED. All data is live — no fabrication.\n */\n\nimport type { FC } from 'react';\nimport { useEffect, useMemo, useRef, useState } from 'react';\nimport { useParams } from 'react-router-dom';\nimport {\n ArrowLeft,\n ChevronLeft,\n ChevronRight,\n CheckCircle2,\n Circle,\n ListChecks,\n Trophy,\n Loader2,\n} from 'lucide-react';\nimport { Button, GlassCard } from '@burdenoff/fe-libs/ui';\nimport {\n PageLayout,\n StatusBadge,\n MiniBar,\n LoadingView,\n ErrorView,\n EmptyState,\n FileLink,\n} from '../../components';\nimport { useBuildmyiq } from '../../context';\nimport {\n useBuiEnrollment,\n useBuiCourse,\n useBuiRecordLessonProgress,\n useBuiTransitionEnrollment,\n} from '../../hooks';\nimport { cn } from '../../utils/cn';\nimport { formatDuration } from '../../utils/formatters';\nimport type { BuiLesson, BuiModule, BuiProgressStatus } from '../../types';\n\ninterface LessonGroup {\n id: string;\n title: string;\n lessons: BuiLesson[];\n}\n\nconst sortByPosition = <T extends { position: number }>(items: T[]): T[] =>\n [...items].sort((a, b) => a.position - b.position);\n\nexport const LessonPlayerPage: FC = () => {\n const { enrollmentId } = useParams<{ enrollmentId: string }>();\n const { navigateTo } = useBuildmyiq();\n\n const enrollmentQuery = useBuiEnrollment(enrollmentId);\n const enrollment = enrollmentQuery.data ?? null;\n const courseId = enrollment?.courseId;\n const courseQuery = useBuiCourse(courseId);\n const course = courseQuery.data ?? null;\n\n const recordProgress = useBuiRecordLessonProgress();\n const transitionEnrollment = useBuiTransitionEnrollment();\n\n // react-query's mutate is referentially stable across renders, so it can be\n // used directly in effects without a render-time ref write.\n const recordMutate = recordProgress.mutate;\n const autoRecordedRef = useRef<Set<string>>(new Set());\n\n const [userSelectedId, setUserSelectedId] = useState<string | null>(null);\n const [showTree, setShowTree] = useState(false);\n\n // Grouped tree (modules → lessons) plus any standalone lessons, de-duplicated.\n const groups = useMemo<LessonGroup[]>(() => {\n if (!course) return [];\n const seen = new Set<string>();\n const modules: BuiModule[] = sortByPosition(course.modules ?? []);\n const result: LessonGroup[] = [];\n for (const m of modules) {\n const lessons = sortByPosition(m.lessons ?? []).filter((l) => {\n if (seen.has(l.id)) return false;\n seen.add(l.id);\n return true;\n });\n if (lessons.length > 0) result.push({ id: m.id, title: m.title, lessons });\n }\n const standalone = sortByPosition(course.lessons ?? []).filter((l) => {\n if (seen.has(l.id)) return false;\n seen.add(l.id);\n return true;\n });\n if (standalone.length > 0) {\n result.push({ id: '__standalone', title: 'Lessons', lessons: standalone });\n }\n return result;\n }, [course]);\n\n // Flat, ordered lesson list for prev/next + completion checks.\n const orderedLessons = useMemo<BuiLesson[]>(() => groups.flatMap((g) => g.lessons), [groups]);\n\n const progressByLesson = useMemo(() => {\n const map = new Map<string, BuiProgressStatus>();\n for (const p of enrollment?.lessonProgress ?? []) map.set(p.lessonId, p.status);\n return map;\n }, [enrollment]);\n\n // Resolve the active lesson as DERIVED state: the user's pick if it still\n // exists in the course, otherwise the first lesson. Avoids a setState-in-\n // effect re-anchor loop.\n const selectedLessonId = useMemo<string | null>(() => {\n if (orderedLessons.length === 0) return null;\n if (userSelectedId && orderedLessons.some((l) => l.id === userSelectedId)) {\n return userSelectedId;\n }\n return orderedLessons[0].id;\n }, [orderedLessons, userSelectedId]);\n\n // Record IN_PROGRESS once when a not-yet-started lesson is opened.\n useEffect(() => {\n if (!enrollmentId || !selectedLessonId) return;\n const status = progressByLesson.get(selectedLessonId);\n if (status === 'COMPLETED' || status === 'IN_PROGRESS') return;\n if (autoRecordedRef.current.has(selectedLessonId)) return;\n autoRecordedRef.current.add(selectedLessonId);\n recordMutate({\n enrollmentId,\n lessonId: selectedLessonId,\n status: 'IN_PROGRESS',\n });\n }, [enrollmentId, selectedLessonId, progressByLesson, recordMutate]);\n\n const selectedIndex = orderedLessons.findIndex((l) => l.id === selectedLessonId);\n const selectedLesson = selectedIndex >= 0 ? orderedLessons[selectedIndex] : null;\n const selectedStatus = selectedLesson ? progressByLesson.get(selectedLesson.id) : undefined;\n const isSelectedComplete = selectedStatus === 'COMPLETED';\n\n const allComplete =\n orderedLessons.length > 0 &&\n orderedLessons.every((l) => progressByLesson.get(l.id) === 'COMPLETED');\n const alreadyFinished = enrollment?.status === 'COMPLETED';\n\n const handleSelect = (lessonId: string) => {\n setUserSelectedId(lessonId);\n setShowTree(false);\n };\n\n const goTo = (offset: number) => {\n if (selectedIndex < 0) return;\n const next = orderedLessons[selectedIndex + offset];\n if (next) handleSelect(next.id);\n };\n\n const handleMarkComplete = async () => {\n if (!enrollmentId || !selectedLesson) return;\n await recordProgress.mutateAsync({\n enrollmentId,\n lessonId: selectedLesson.id,\n status: 'COMPLETED',\n });\n // Advance to the next incomplete lesson for a natural flow.\n const next = orderedLessons[selectedIndex + 1];\n if (next) setUserSelectedId(next.id);\n };\n\n const handleFinishCourse = async () => {\n if (!enrollmentId) return;\n await transitionEnrollment.mutateAsync({ id: enrollmentId, status: 'COMPLETED' });\n };\n\n // ---- top-level states ----------------------------------------------------\n if (!enrollmentId) {\n return (\n <PageLayout title=\"Lesson\">\n <EmptyState\n illustration=\"empty-data\"\n title=\"No enrollment selected\"\n description=\"Open a course from My Learning to start the lesson player.\"\n action={<Button onClick={() => navigateTo('learning')}>Back to My Learning</Button>}\n />\n </PageLayout>\n );\n }\n\n const isLoading = enrollmentQuery.isLoading || (Boolean(courseId) && courseQuery.isLoading);\n\n const backAction = (\n <Button variant=\"ghost\" size=\"sm\" onClick={() => navigateTo('learning')}>\n <ArrowLeft className=\"mr-1 h-4 w-4\" /> My Learning\n </Button>\n );\n\n if (isLoading) {\n return (\n <PageLayout title=\"Lesson\" actions={backAction}>\n <LoadingView label=\"Loading your lesson…\" />\n </PageLayout>\n );\n }\n\n if (enrollmentQuery.isError) {\n return (\n <PageLayout title=\"Lesson\" actions={backAction}>\n <ErrorView\n title=\"Couldn't load this enrollment\"\n message={\n enrollmentQuery.error instanceof Error ? enrollmentQuery.error.message : undefined\n }\n onRetry={() => enrollmentQuery.refetch()}\n />\n </PageLayout>\n );\n }\n\n if (!enrollment) {\n return (\n <PageLayout title=\"Lesson\" actions={backAction}>\n <EmptyState\n illustration=\"empty-data\"\n title=\"Enrollment not found\"\n description=\"This enrollment may have been removed or is not available to you.\"\n action={<Button onClick={() => navigateTo('learning')}>Back to My Learning</Button>}\n />\n </PageLayout>\n );\n }\n\n if (courseQuery.isError) {\n return (\n <PageLayout title=\"Lesson\" actions={backAction}>\n <ErrorView\n title=\"Couldn't load the course\"\n message={courseQuery.error instanceof Error ? courseQuery.error.message : undefined}\n onRetry={() => courseQuery.refetch()}\n />\n </PageLayout>\n );\n }\n\n const progressPct = Math.round(enrollment.progressPct ?? 0);\n\n return (\n <PageLayout title={course?.title ?? 'Lesson'} actions={backAction}>\n <div className=\"flex flex-col gap-5\">\n {/* Header progress */}\n <div className=\"flex flex-col gap-2 rounded-2xl border border-border-seam bg-bg-surface p-4 sm:flex-row sm:items-center sm:gap-4\">\n <div className=\"flex items-center gap-2\">\n <StatusBadge status={enrollment.status} />\n <span className=\"text-sm text-text-secondary\">\n {orderedLessons.filter((l) => progressByLesson.get(l.id) === 'COMPLETED').length} /{' '}\n {orderedLessons.length} lessons complete\n </span>\n </div>\n <div className=\"flex flex-1 items-center gap-3\">\n <MiniBar value={progressPct} className=\"flex-1\" />\n <span className=\"shrink-0 text-sm font-medium tabular-nums text-text-primary\">\n {progressPct}%\n </span>\n </div>\n {allComplete && !alreadyFinished && (\n <Button\n size=\"sm\"\n className=\"shrink-0 max-sm:w-full\"\n onClick={handleFinishCourse}\n disabled={transitionEnrollment.isPending}\n >\n {transitionEnrollment.isPending ? (\n <Loader2 className=\"mr-1 h-4 w-4 animate-spin\" />\n ) : (\n <Trophy className=\"mr-1 h-4 w-4\" />\n )}\n Finish course\n </Button>\n )}\n </div>\n\n {/* Mobile tree toggle */}\n <Button\n variant=\"secondary\"\n size=\"sm\"\n className=\"w-full lg:hidden\"\n onClick={() => setShowTree((v) => !v)}\n >\n <ListChecks className=\"mr-1 h-4 w-4\" />\n {showTree ? 'Hide lessons' : 'Show lessons'}\n </Button>\n\n <div className=\"flex flex-col gap-5 lg:flex-row\">\n {/* Left: module / lesson tree */}\n <aside className={cn('lg:block lg:w-80 lg:shrink-0', showTree ? 'block' : 'hidden')}>\n <GlassCard treatment=\"plain\" className=\"overflow-hidden p-0 lg:sticky lg:top-4\">\n {orderedLessons.length === 0 ? (\n <div className=\"p-4 text-sm text-text-secondary\">\n This course has no lessons yet.\n </div>\n ) : (\n <nav className=\"max-h-[70vh] overflow-auto p-2\">\n {groups.map((group) => (\n <div key={group.id} className=\"mb-2 last:mb-0\">\n <p className=\"px-3 py-2 text-xs font-semibold uppercase tracking-wide text-text-secondary\">\n {group.title}\n </p>\n <ul className=\"flex flex-col gap-0.5\">\n {group.lessons.map((lesson) => {\n const done = progressByLesson.get(lesson.id) === 'COMPLETED';\n const active = lesson.id === selectedLessonId;\n return (\n <li key={lesson.id}>\n <button\n type=\"button\"\n onClick={() => handleSelect(lesson.id)}\n className={cn(\n 'flex w-full items-center gap-2.5 rounded-lg px-3 py-2 text-left text-sm transition-colors',\n active\n ? 'bg-[var(--color-accent-soft)] text-text-primary'\n : 'text-text-secondary hover:bg-bg-sunken hover:text-text-primary'\n )}\n aria-current={active ? 'true' : undefined}\n >\n {done ? (\n <CheckCircle2 className=\"h-4 w-4 shrink-0 text-status-success-text\" />\n ) : (\n <Circle className=\"h-4 w-4 shrink-0 opacity-50\" />\n )}\n <span className=\"min-w-0 flex-1 truncate\">{lesson.title}</span>\n {lesson.durationMin > 0 && (\n <span className=\"shrink-0 text-xs tabular-nums text-text-secondary\">\n {formatDuration(lesson.durationMin)}\n </span>\n )}\n </button>\n </li>\n );\n })}\n </ul>\n </div>\n ))}\n </nav>\n )}\n </GlassCard>\n </aside>\n\n {/* Right: selected lesson */}\n <main className=\"min-w-0 flex-1\">\n {!selectedLesson ? (\n <EmptyState\n illustration=\"empty-data\"\n title=\"No lesson to show\"\n description=\"This course doesn't have any lessons yet. Check back once the author adds content.\"\n />\n ) : (\n <GlassCard treatment=\"plain\" className=\"flex flex-col gap-4 p-5 sm:p-6\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <StatusBadge status={selectedLesson.type} />\n {selectedLesson.durationMin > 0 && (\n <span className=\"text-xs text-text-secondary\">\n {formatDuration(selectedLesson.durationMin)}\n </span>\n )}\n {isSelectedComplete && (\n <span className=\"inline-flex items-center gap-1 text-xs font-medium text-status-success-text\">\n <CheckCircle2 className=\"h-3.5 w-3.5\" /> Completed\n </span>\n )}\n </div>\n\n <h2 className=\"text-xl font-semibold text-text-primary\">{selectedLesson.title}</h2>\n\n {selectedLesson.media ? (\n <div className=\"rounded-xl border border-border-seam bg-bg-sunken p-3\">\n <FileLink fileId={selectedLesson.media.id} />\n </div>\n ) : null}\n\n {selectedLesson.content ? (\n <p className=\"whitespace-pre-wrap text-sm leading-relaxed text-text-primary\">\n {selectedLesson.content}\n </p>\n ) : (\n <p className=\"text-sm text-text-secondary\">\n This lesson has no written content\n {selectedLesson.media ? ' beyond the attached media' : ''}.\n </p>\n )}\n\n {/* Actions */}\n <div className=\"mt-2 flex flex-col gap-3 border-t border-border-seam pt-4 sm:flex-row sm:items-center sm:justify-between\">\n <div className=\"flex items-center gap-2\">\n <Button\n variant=\"secondary\"\n size=\"sm\"\n onClick={() => goTo(-1)}\n disabled={selectedIndex <= 0}\n >\n <ChevronLeft className=\"mr-1 h-4 w-4\" /> Prev\n </Button>\n <Button\n variant=\"secondary\"\n size=\"sm\"\n onClick={() => goTo(1)}\n disabled={selectedIndex >= orderedLessons.length - 1}\n >\n Next <ChevronRight className=\"ml-1 h-4 w-4\" />\n </Button>\n </div>\n {!isSelectedComplete && (\n <Button\n size=\"sm\"\n className=\"max-sm:w-full\"\n onClick={handleMarkComplete}\n disabled={recordProgress.isPending}\n >\n {recordProgress.isPending ? (\n <Loader2 className=\"mr-1 h-4 w-4 animate-spin\" />\n ) : (\n <CheckCircle2 className=\"mr-1 h-4 w-4\" />\n )}\n Mark complete\n </Button>\n )}\n </div>\n </GlassCard>\n )}\n </main>\n </div>\n </div>\n </PageLayout>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"LessonPlayerPage.js","names":[],"sources":["../../../../src/buildmyiq/pages/learning/LessonPlayerPage.tsx"],"sourcesContent":["/**\n * LessonPlayerPage — the two-pane lesson player for a single enrollment.\n * @module buildmyiq/pages/learning\n *\n * Left pane: the course's module/lesson tree (from `buiCourse`) rendered as a\n * completion timeline driven by the enrollment's `lessonProgress`. Right pane:\n * the selected lesson (title, type, content text rendered safely, and a\n * `FileLink` for any attached media) with Prev / Mark-complete / Next controls.\n * Opening a lesson records IN_PROGRESS once; \"Mark complete\" records COMPLETED;\n * finishing every lesson transitions the enrollment to COMPLETED. All data is\n * live — no fabrication.\n */\n\nimport type { FC } from 'react';\nimport { useEffect, useMemo, useRef, useState } from 'react';\nimport { useParams } from 'react-router-dom';\nimport type { LucideIcon } from 'lucide-react';\nimport {\n ArrowLeft,\n BookOpen,\n Check,\n CheckCircle2,\n ChevronLeft,\n ChevronRight,\n FileText,\n HelpCircle,\n Loader2,\n ListChecks,\n MousePointerClick,\n PenLine,\n PlayCircle,\n Radio,\n Trophy,\n} from 'lucide-react';\nimport { Button, GlassCard } from '@burdenoff/fe-libs/ui';\nimport {\n PageLayout,\n StatusBadge,\n MiniBar,\n ProgressRing,\n LoadingView,\n ErrorView,\n EmptyState,\n FileLink,\n} from '../../components';\nimport { useBuildmyiq } from '../../context';\nimport {\n useBuiEnrollment,\n useBuiCourse,\n useBuiRecordLessonProgress,\n useBuiTransitionEnrollment,\n} from '../../hooks';\nimport { cn } from '../../utils/cn';\nimport { formatDuration, formatEnumLabel } from '../../utils/formatters';\nimport type { BuiLesson, BuiLessonType, BuiModule, BuiProgressStatus } from '../../types';\n\ninterface LessonGroup {\n id: string;\n title: string;\n lessons: BuiLesson[];\n}\n\nconst LESSON_ICON: Record<BuiLessonType, LucideIcon> = {\n ARTICLE: FileText,\n VIDEO: PlayCircle,\n QUIZ: HelpCircle,\n ASSIGNMENT: PenLine,\n INTERACTIVE: MousePointerClick,\n LIVE_SESSION: Radio,\n};\n\nconst sortByPosition = <T extends { position: number }>(items: T[]): T[] =>\n [...items].sort((a, b) => a.position - b.position);\n\nexport const LessonPlayerPage: FC = () => {\n const { enrollmentId } = useParams<{ enrollmentId: string }>();\n const { navigateTo } = useBuildmyiq();\n\n const enrollmentQuery = useBuiEnrollment(enrollmentId);\n const enrollment = enrollmentQuery.data ?? null;\n const courseId = enrollment?.courseId;\n const courseQuery = useBuiCourse(courseId);\n const course = courseQuery.data ?? null;\n\n const recordProgress = useBuiRecordLessonProgress();\n const transitionEnrollment = useBuiTransitionEnrollment();\n\n // react-query's mutate is referentially stable across renders, so it can be\n // used directly in effects without a render-time ref write.\n const recordMutate = recordProgress.mutate;\n const autoRecordedRef = useRef<Set<string>>(new Set());\n\n const [userSelectedId, setUserSelectedId] = useState<string | null>(null);\n const [showTree, setShowTree] = useState(false);\n\n // Grouped tree (modules → lessons) plus any standalone lessons, de-duplicated.\n const groups = useMemo<LessonGroup[]>(() => {\n if (!course) return [];\n const seen = new Set<string>();\n const modules: BuiModule[] = sortByPosition(course.modules ?? []);\n const result: LessonGroup[] = [];\n for (const m of modules) {\n const lessons = sortByPosition(m.lessons ?? []).filter((l) => {\n if (seen.has(l.id)) return false;\n seen.add(l.id);\n return true;\n });\n if (lessons.length > 0) result.push({ id: m.id, title: m.title, lessons });\n }\n const standalone = sortByPosition(course.lessons ?? []).filter((l) => {\n if (seen.has(l.id)) return false;\n seen.add(l.id);\n return true;\n });\n if (standalone.length > 0) {\n result.push({ id: '__standalone', title: 'Lessons', lessons: standalone });\n }\n return result;\n }, [course]);\n\n // Flat, ordered lesson list for prev/next + completion checks.\n const orderedLessons = useMemo<BuiLesson[]>(() => groups.flatMap((g) => g.lessons), [groups]);\n\n const progressByLesson = useMemo(() => {\n const map = new Map<string, BuiProgressStatus>();\n for (const p of enrollment?.lessonProgress ?? []) map.set(p.lessonId, p.status);\n return map;\n }, [enrollment]);\n\n // Resolve the active lesson as DERIVED state: the user's pick if it still\n // exists in the course, otherwise the first lesson. Avoids a setState-in-\n // effect re-anchor loop.\n const selectedLessonId = useMemo<string | null>(() => {\n if (orderedLessons.length === 0) return null;\n if (userSelectedId && orderedLessons.some((l) => l.id === userSelectedId)) {\n return userSelectedId;\n }\n return orderedLessons[0].id;\n }, [orderedLessons, userSelectedId]);\n\n // Record IN_PROGRESS once when a not-yet-started lesson is opened.\n useEffect(() => {\n if (!enrollmentId || !selectedLessonId) return;\n const status = progressByLesson.get(selectedLessonId);\n if (status === 'COMPLETED' || status === 'IN_PROGRESS') return;\n if (autoRecordedRef.current.has(selectedLessonId)) return;\n autoRecordedRef.current.add(selectedLessonId);\n recordMutate({\n enrollmentId,\n lessonId: selectedLessonId,\n status: 'IN_PROGRESS',\n });\n }, [enrollmentId, selectedLessonId, progressByLesson, recordMutate]);\n\n const selectedIndex = orderedLessons.findIndex((l) => l.id === selectedLessonId);\n const selectedLesson = selectedIndex >= 0 ? orderedLessons[selectedIndex] : null;\n const selectedStatus = selectedLesson ? progressByLesson.get(selectedLesson.id) : undefined;\n const isSelectedComplete = selectedStatus === 'COMPLETED';\n\n const completedCount = orderedLessons.filter(\n (l) => progressByLesson.get(l.id) === 'COMPLETED'\n ).length;\n const allComplete = orderedLessons.length > 0 && completedCount === orderedLessons.length;\n const alreadyFinished = enrollment?.status === 'COMPLETED';\n\n const handleSelect = (lessonId: string) => {\n setUserSelectedId(lessonId);\n setShowTree(false);\n };\n\n const goTo = (offset: number) => {\n if (selectedIndex < 0) return;\n const next = orderedLessons[selectedIndex + offset];\n if (next) handleSelect(next.id);\n };\n\n const handleMarkComplete = async () => {\n if (!enrollmentId || !selectedLesson) return;\n await recordProgress.mutateAsync({\n enrollmentId,\n lessonId: selectedLesson.id,\n status: 'COMPLETED',\n });\n // Advance to the next incomplete lesson for a natural flow.\n const next = orderedLessons[selectedIndex + 1];\n if (next) setUserSelectedId(next.id);\n };\n\n const handleFinishCourse = async () => {\n if (!enrollmentId) return;\n await transitionEnrollment.mutateAsync({ id: enrollmentId, status: 'COMPLETED' });\n };\n\n // ---- top-level states ----------------------------------------------------\n if (!enrollmentId) {\n return (\n <PageLayout title=\"Lesson\">\n <EmptyState\n illustration=\"empty-data\"\n title=\"No enrollment selected\"\n description=\"Open a course from My Learning to start the lesson player.\"\n action={<Button onClick={() => navigateTo('learning')}>Back to My Learning</Button>}\n />\n </PageLayout>\n );\n }\n\n const isLoading = enrollmentQuery.isLoading || (Boolean(courseId) && courseQuery.isLoading);\n\n const backAction = (\n <Button variant=\"ghost\" size=\"sm\" onClick={() => navigateTo('learning')}>\n <ArrowLeft className=\"mr-1 h-4 w-4\" /> My Learning\n </Button>\n );\n\n if (isLoading) {\n return (\n <PageLayout title=\"Lesson\" actions={backAction}>\n <LoadingView label=\"Loading your lesson…\" />\n </PageLayout>\n );\n }\n\n if (enrollmentQuery.isError) {\n return (\n <PageLayout title=\"Lesson\" actions={backAction}>\n <ErrorView\n title=\"Couldn't load this enrollment\"\n message={\n enrollmentQuery.error instanceof Error ? enrollmentQuery.error.message : undefined\n }\n onRetry={() => enrollmentQuery.refetch()}\n />\n </PageLayout>\n );\n }\n\n if (!enrollment) {\n return (\n <PageLayout title=\"Lesson\" actions={backAction}>\n <EmptyState\n illustration=\"empty-data\"\n title=\"Enrollment not found\"\n description=\"This enrollment may have been removed or is not available to you.\"\n action={<Button onClick={() => navigateTo('learning')}>Back to My Learning</Button>}\n />\n </PageLayout>\n );\n }\n\n if (courseQuery.isError) {\n return (\n <PageLayout title=\"Lesson\" actions={backAction}>\n <ErrorView\n title=\"Couldn't load the course\"\n message={courseQuery.error instanceof Error ? courseQuery.error.message : undefined}\n onRetry={() => courseQuery.refetch()}\n />\n </PageLayout>\n );\n }\n\n const progressPct = Math.round(enrollment.progressPct ?? 0);\n const SelectedIcon = selectedLesson ? LESSON_ICON[selectedLesson.type] : FileText;\n\n return (\n <PageLayout title={course?.title ?? 'Lesson'} actions={backAction}>\n <div className=\"flex flex-col gap-5\">\n {/* Header progress — ProgressRing hero + completion bar */}\n <GlassCard\n treatment=\"glass\"\n glow={progressPct > 0}\n className=\"flex flex-col gap-4 p-4 sm:flex-row sm:items-center sm:gap-5\"\n >\n <ProgressRing value={progressPct} size={64} className=\"shrink-0\" />\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <StatusBadge status={enrollment.status} />\n <span className=\"text-sm text-text-secondary tabular-nums\">\n {completedCount} of {orderedLessons.length} lessons complete\n </span>\n </div>\n <MiniBar value={progressPct} className=\"mt-3\" />\n </div>\n {allComplete && !alreadyFinished && (\n <Button\n className=\"shrink-0 max-sm:w-full\"\n onClick={handleFinishCourse}\n disabled={transitionEnrollment.isPending}\n >\n {transitionEnrollment.isPending ? (\n <Loader2 className=\"mr-1 h-4 w-4 animate-spin\" />\n ) : (\n <Trophy className=\"mr-1 h-4 w-4\" />\n )}\n Finish course\n </Button>\n )}\n </GlassCard>\n\n {/* Mobile tree toggle — collapses the outline into a top sheet */}\n <Button\n variant=\"secondary\"\n size=\"sm\"\n className=\"w-full lg:hidden\"\n onClick={() => setShowTree((v) => !v)}\n >\n <ListChecks className=\"mr-1 h-4 w-4\" />\n {showTree ? 'Hide lessons' : `Lessons (${completedCount}/${orderedLessons.length})`}\n </Button>\n\n <div className=\"flex flex-col gap-5 lg:flex-row\">\n {/* Left: module / lesson completion timeline */}\n <aside className={cn('lg:block lg:w-80 lg:shrink-0', showTree ? 'block' : 'hidden')}>\n <GlassCard treatment=\"plain\" className=\"overflow-hidden p-0 lg:sticky lg:top-4\">\n {orderedLessons.length === 0 ? (\n <div className=\"p-4 text-sm text-text-secondary\">\n This course has no lessons yet.\n </div>\n ) : (\n <>\n <nav className=\"max-h-[68vh] overflow-auto p-3\">\n {groups.map((group) => {\n const groupDone = group.lessons.filter(\n (l) => progressByLesson.get(l.id) === 'COMPLETED'\n ).length;\n return (\n <div key={group.id} className=\"mb-4 last:mb-0\">\n <div className=\"mb-2 flex items-center justify-between gap-2 px-1\">\n <p className=\"min-w-0 truncate text-sm font-semibold text-text-primary\">\n {group.title}\n </p>\n <span className=\"shrink-0 text-xs font-medium tabular-nums text-text-secondary\">\n {groupDone} / {group.lessons.length}\n </span>\n </div>\n <ul className=\"flex flex-col\">\n {group.lessons.map((lesson, i) => {\n const status = progressByLesson.get(lesson.id);\n const done = status === 'COMPLETED';\n const active = lesson.id === selectedLessonId;\n const isLast = i === group.lessons.length - 1;\n const Icon = LESSON_ICON[lesson.type];\n return (\n <li key={lesson.id} className=\"flex gap-3\">\n {/* Timeline node + connector */}\n <div className=\"flex flex-col items-center pt-1.5\">\n {done ? (\n <span className=\"flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-[var(--color-status-success-text)]\">\n <Check className=\"h-3.5 w-3.5 text-status-success-bg-subtle\" />\n </span>\n ) : active ? (\n <span className=\"flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-[var(--color-accent-soft)] ring-2 ring-[var(--color-accent-teal)]\">\n <Icon className=\"h-3.5 w-3.5 text-[var(--color-accent-teal)]\" />\n </span>\n ) : (\n <span className=\"flex h-6 w-6 shrink-0 items-center justify-center rounded-full border border-border-seam bg-bg-sunken\">\n <Icon className=\"h-3.5 w-3.5 text-text-secondary opacity-70\" />\n </span>\n )}\n {!isLast && (\n <span\n className={cn(\n 'my-1 w-px flex-1',\n done\n ? 'bg-[color-mix(in_srgb,var(--color-status-success-text)_45%,transparent)]'\n : 'bg-border-seam'\n )}\n />\n )}\n </div>\n <button\n type=\"button\"\n onClick={() => handleSelect(lesson.id)}\n className={cn(\n 'mb-1 min-w-0 flex-1 rounded-lg px-3 py-2 text-left transition-colors',\n active\n ? 'bg-[var(--color-accent-soft)]'\n : 'hover:bg-bg-sunken'\n )}\n aria-current={active ? 'true' : undefined}\n >\n <span\n className={cn(\n 'block truncate text-sm',\n active\n ? 'font-medium text-text-primary'\n : done\n ? 'text-text-secondary'\n : 'text-text-primary'\n )}\n >\n {lesson.title}\n </span>\n <span className=\"mt-0.5 block truncate text-xs text-text-secondary\">\n {formatEnumLabel(lesson.type)}\n {lesson.durationMin > 0\n ? ` · ${formatDuration(lesson.durationMin)}`\n : ''}\n </span>\n </button>\n </li>\n );\n })}\n </ul>\n </div>\n );\n })}\n </nav>\n {courseId && (\n <div className=\"border-t border-border-seam p-3\">\n <Button\n variant=\"outline\"\n size=\"sm\"\n className=\"w-full\"\n onClick={() => navigateTo(`catalog/${courseId}`)}\n >\n <BookOpen className=\"mr-1.5 h-4 w-4\" /> Course overview\n </Button>\n </div>\n )}\n </>\n )}\n </GlassCard>\n </aside>\n\n {/* Right: selected lesson */}\n <main className=\"min-w-0 flex-1\">\n {!selectedLesson ? (\n <EmptyState\n illustration=\"empty-data\"\n title=\"No lesson to show\"\n description=\"This course doesn't have any lessons yet. Check back once the author adds content.\"\n />\n ) : (\n <GlassCard treatment=\"plain\" className=\"flex flex-col gap-4 p-5 sm:p-7\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <span className=\"inline-flex items-center gap-1.5 rounded-full bg-[var(--color-accent-soft)] px-2.5 py-1 text-xs font-medium text-text-primary\">\n <SelectedIcon className=\"h-3.5 w-3.5 text-[var(--color-accent-teal)]\" />\n {formatEnumLabel(selectedLesson.type)}\n </span>\n {selectedLesson.durationMin > 0 && (\n <span className=\"text-xs text-text-secondary\">\n {formatDuration(selectedLesson.durationMin)}\n </span>\n )}\n {isSelectedComplete && (\n <span className=\"inline-flex items-center gap-1 text-xs font-medium text-status-success-text\">\n <CheckCircle2 className=\"h-3.5 w-3.5\" /> Completed\n </span>\n )}\n <span className=\"ml-auto text-xs tabular-nums text-text-secondary\">\n Lesson {selectedIndex + 1} of {orderedLessons.length}\n </span>\n </div>\n\n <h2 className=\"text-2xl font-semibold tracking-tight text-text-primary\">\n {selectedLesson.title}\n </h2>\n\n {selectedLesson.media ? (\n <div className=\"rounded-xl border border-border-seam bg-bg-sunken p-3\">\n <p className=\"mb-2 text-xs font-semibold uppercase tracking-wide text-text-secondary\">\n Resources\n </p>\n <FileLink fileId={selectedLesson.media.id} />\n </div>\n ) : null}\n\n {selectedLesson.content ? (\n <p className=\"max-w-prose whitespace-pre-wrap text-[15px] leading-7 text-text-primary\">\n {selectedLesson.content}\n </p>\n ) : (\n <p className=\"text-sm text-text-secondary\">\n This lesson has no written content\n {selectedLesson.media ? ' beyond the attached media' : ''}.\n </p>\n )}\n\n {/* Actions — Prev · Mark complete · Next */}\n <div className=\"mt-2 flex flex-col gap-3 border-t border-border-seam pt-5 sm:flex-row sm:items-center sm:justify-between\">\n <Button\n variant=\"outline\"\n size=\"sm\"\n className=\"max-sm:w-full\"\n onClick={() => goTo(-1)}\n disabled={selectedIndex <= 0}\n >\n <ChevronLeft className=\"mr-1 h-4 w-4\" /> Previous\n </Button>\n\n {isSelectedComplete ? (\n <span className=\"inline-flex items-center justify-center gap-2 rounded-lg bg-status-success-bg-subtle px-4 py-2 text-sm font-medium text-status-success-text max-sm:w-full\">\n <CheckCircle2 className=\"h-4 w-4\" /> Lesson complete\n </span>\n ) : (\n <Button\n size=\"lg\"\n className=\"max-sm:w-full\"\n onClick={handleMarkComplete}\n disabled={recordProgress.isPending}\n >\n {recordProgress.isPending ? (\n <Loader2 className=\"mr-1.5 h-4 w-4 animate-spin\" />\n ) : (\n <CheckCircle2 className=\"mr-1.5 h-4 w-4\" />\n )}\n Mark complete\n </Button>\n )}\n\n <Button\n variant=\"outline\"\n size=\"sm\"\n className=\"max-sm:w-full\"\n onClick={() => goTo(1)}\n disabled={selectedIndex >= orderedLessons.length - 1}\n >\n Next <ChevronRight className=\"ml-1 h-4 w-4\" />\n </Button>\n </div>\n </GlassCard>\n )}\n </main>\n </div>\n </div>\n </PageLayout>\n );\n};\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MyLearningPage.d.ts","sourceRoot":"","sources":["../../../../src/buildmyiq/pages/learning/MyLearningPage.tsx"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"MyLearningPage.d.ts","sourceRoot":"","sources":["../../../../src/buildmyiq/pages/learning/MyLearningPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC;AAoDhC,eAAO,MAAM,cAAc,EAAE,EA2e5B,CAAC"}
|