@burdenoff/microfe-buildmyiq 2026.1030.5 → 2026.1030.7
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/pages/learning/LessonPlayerPage.d.ts.map +1 -1
- package/dist/buildmyiq/pages/learning/LessonPlayerPage.js +2 -1
- 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 +2 -2
- package/dist/buildmyiq/pages/learning/MyLearningPage.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LessonPlayerPage.d.ts","sourceRoot":"","sources":["../../../../src/buildmyiq/pages/learning/LessonPlayerPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC;AAiEhC,eAAO,MAAM,gBAAgB,EAAE,
|
|
1
|
+
{"version":3,"file":"LessonPlayerPage.d.ts","sourceRoot":"","sources":["../../../../src/buildmyiq/pages/learning/LessonPlayerPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC;AAiEhC,eAAO,MAAM,gBAAgB,EAAE,EAmlB9B,CAAC"}
|
|
@@ -317,7 +317,8 @@ var w = {
|
|
|
317
317
|
})
|
|
318
318
|
})] })
|
|
319
319
|
})
|
|
320
|
-
}), /* @__PURE__ */ x("
|
|
320
|
+
}), /* @__PURE__ */ x("section", {
|
|
321
|
+
"aria-label": y("buildmyiq.lessonPlayer.lessonRegion", "Lesson"),
|
|
321
322
|
className: "min-w-0 flex-1",
|
|
322
323
|
children: [q ? /* @__PURE__ */ x(C, {
|
|
323
324
|
treatment: "plain",
|
|
@@ -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`) 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, useSearchParams } from 'react-router';\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 OfflineLessonControl,\n TutorPanel,\n} from '../../components';\nimport { useBuildmyiqAnalytics } from '../../analytics';\nimport { useBuildmyiq } from '../../context';\nimport {\n useBuiMyEnrollment,\n useBuiCourse,\n useBuiRecordMyLessonProgress,\n useBuiCompleteMyEnrollment,\n} from '../../hooks';\nimport { useTr } from '../../shared/hooks/useTr';\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 // `?lesson=<id>` deep-links straight to one lesson (used by the \"what's next\"\n // suggestion on My Learning). Purely an INITIAL anchor — once the learner\n // picks a lesson themselves, their pick wins.\n const [searchParams] = useSearchParams();\n const requestedLessonId = searchParams.get('lesson');\n const { navigateTo } = useBuildmyiq();\n const tr = useTr();\n\n const enrollmentQuery = useBuiMyEnrollment(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 = useBuiRecordMyLessonProgress();\n const completeEnrollment = useBuiCompleteMyEnrollment();\n const { trackLessonStarted, trackLessonCompleted, trackCourseCompleted } =\n useBuildmyiqAnalytics();\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 // Error from the last \"Mark complete\" / \"Finish course\" attempt. Without it a\n // rejected mutation surfaced only as an unhandled promise rejection and the\n // button silently did nothing.\n const [actionError, setActionError] = useState<string | null>(null);\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({\n id: '__standalone',\n title: tr('common.lessons', 'Lessons'),\n lessons: standalone,\n });\n }\n return result;\n }, [course, tr]);\n\n // Flat, ordered lesson list for prev/next + completion checks.\n const orderedLessons = useMemo<BuiLesson[]>(() => groups.flatMap((g) => g.lessons), [groups]);\n // Lesson id → title for this course, so the tutor panel can NAME the lessons\n // its answers were grounded in instead of showing opaque ids.\n const lessonTitlesById = useMemo<ReadonlyMap<string, string>>(\n () => new Map(orderedLessons.map((lesson) => [lesson.id, lesson.title])),\n [orderedLessons]\n );\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 if (requestedLessonId && orderedLessons.some((l) => l.id === requestedLessonId)) {\n return requestedLessonId;\n }\n return orderedLessons[0].id;\n }, [orderedLessons, userSelectedId, requestedLessonId]);\n\n // Record IN_PROGRESS once when a not-yet-started lesson is opened. That\n // first successful write is also the `buildmyiq.lesson.started` fact, so a\n // re-opened lesson is never counted as a second start.\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 const lessonType = orderedLessons.find((l) => l.id === selectedLessonId)?.type;\n recordMutate(\n {\n enrollmentId,\n lessonId: selectedLessonId,\n status: 'IN_PROGRESS',\n },\n {\n onSuccess: () =>\n trackLessonStarted({ courseId, enrollmentId, lessonId: selectedLessonId, lessonType }),\n }\n );\n }, [\n enrollmentId,\n selectedLessonId,\n progressByLesson,\n recordMutate,\n orderedLessons,\n courseId,\n trackLessonStarted,\n ]);\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 setActionError(null);\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 setActionError(null);\n try {\n await recordProgress.mutateAsync({\n enrollmentId,\n lessonId: selectedLesson.id,\n status: 'COMPLETED',\n });\n } catch (err) {\n setActionError(\n err instanceof Error && err.message\n ? err.message\n : tr('buildmyiq.lessonPlayer.markCompleteError', 'Could not mark this lesson complete.')\n );\n return;\n }\n trackLessonCompleted({\n courseId,\n enrollmentId,\n lessonId: selectedLesson.id,\n lessonType: selectedLesson.type,\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 setActionError(null);\n try {\n const completed = await completeEnrollment.mutateAsync({ id: enrollmentId });\n trackCourseCompleted({ courseId: completed.courseId, enrollmentId: completed.id });\n } catch (err) {\n setActionError(\n err instanceof Error && err.message\n ? err.message\n : tr('buildmyiq.lessonPlayer.finishCourseError', 'Could not finish this course.')\n );\n }\n };\n\n // ---- top-level states ----------------------------------------------------\n if (!enrollmentId) {\n return (\n <PageLayout title={tr('buildmyiq.lessonPlayer.pageTitle', 'Lesson')}>\n <EmptyState\n illustration=\"empty-data\"\n title={tr('buildmyiq.lessonPlayer.noEnrollmentTitle', 'No enrollment selected')}\n description={tr(\n 'buildmyiq.lessonPlayer.noEnrollmentDescription',\n 'Open a course from My Learning to start the lesson player.'\n )}\n action={\n <Button onClick={() => navigateTo('learning')}>\n {tr('buildmyiq.lessonPlayer.backToLearning', 'Back to My Learning')}\n </Button>\n }\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=\"me-1 h-4 w-4 rtl:-scale-x-100\" />{' '}\n {tr('buildmyiq.nav.learning', 'My Learning')}\n </Button>\n );\n\n if (isLoading) {\n return (\n <PageLayout title={tr('buildmyiq.lessonPlayer.pageTitle', 'Lesson')} actions={backAction}>\n <LoadingView label={tr('buildmyiq.lessonPlayer.loading', 'Loading your lesson…')} />\n </PageLayout>\n );\n }\n\n if (enrollmentQuery.isError) {\n return (\n <PageLayout title={tr('buildmyiq.lessonPlayer.pageTitle', 'Lesson')} actions={backAction}>\n <ErrorView\n title={tr('buildmyiq.lessonPlayer.enrollmentErrorTitle', \"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={tr('buildmyiq.lessonPlayer.pageTitle', 'Lesson')} actions={backAction}>\n <EmptyState\n illustration=\"empty-data\"\n title={tr('buildmyiq.lessonPlayer.enrollmentNotFoundTitle', 'Enrollment not found')}\n description={tr(\n 'buildmyiq.lessonPlayer.enrollmentNotFoundDescription',\n 'This enrollment may have been removed or is not available to you.'\n )}\n action={\n <Button onClick={() => navigateTo('learning')}>\n {tr('buildmyiq.lessonPlayer.backToLearning', 'Back to My Learning')}\n </Button>\n }\n />\n </PageLayout>\n );\n }\n\n if (courseQuery.isError) {\n return (\n <PageLayout title={tr('buildmyiq.lessonPlayer.pageTitle', 'Lesson')} actions={backAction}>\n <ErrorView\n title={tr('buildmyiq.lessonPlayer.courseErrorTitle', \"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\n title={course?.title ?? tr('buildmyiq.lessonPlayer.pageTitle', 'Lesson')}\n actions={backAction}\n >\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 {tr(\n 'buildmyiq.lessonPlayer.progressSummary',\n '{{completed}} of {{total}} lessons complete',\n { completed: completedCount, total: orderedLessons.length }\n )}\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={completeEnrollment.isPending}\n >\n {completeEnrollment.isPending ? (\n <Loader2 className=\"me-1 h-4 w-4 animate-spin\" />\n ) : (\n <Trophy className=\"me-1 h-4 w-4\" />\n )}\n {tr('buildmyiq.lessonPlayer.finishCourse', 'Finish course')}\n </Button>\n )}\n </GlassCard>\n\n {actionError && (\n <p role=\"alert\" className=\"text-sm text-status-error-text\">\n {actionError}\n </p>\n )}\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=\"me-1 h-4 w-4\" />\n {showTree\n ? tr('buildmyiq.lessonPlayer.hideLessons', 'Hide lessons')\n : tr('buildmyiq.lessonPlayer.showLessons', 'Lessons ({{completed}}/{{total}})', {\n completed: completedCount,\n total: orderedLessons.length,\n })}\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 {tr('buildmyiq.lessonPlayer.noLessons', '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-start 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=\"me-1.5 h-4 w-4\" />{' '}\n {tr('buildmyiq.lessonPlayer.courseOverview', '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={tr('buildmyiq.lessonPlayer.noLessonTitle', 'No lesson to show')}\n description={tr(\n 'buildmyiq.lessonPlayer.noLessonDescription',\n \"This course doesn't have any lessons yet. Check back once the author adds content.\"\n )}\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\" />{' '}\n {tr('buildmyiq.lessonPlayer.completedLabel', 'Completed')}\n </span>\n )}\n <OfflineLessonControl key={selectedLesson.id} lesson={selectedLesson} />\n <span className=\"ms-auto text-xs tabular-nums text-text-secondary\">\n {tr('buildmyiq.lessonPlayer.lessonPosition', 'Lesson {{n}} of {{total}}', {\n n: selectedIndex + 1,\n total: orderedLessons.length,\n })}\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 {tr('buildmyiq.lessonPlayer.resources', '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 {selectedLesson.media\n ? tr(\n 'buildmyiq.lessonPlayer.noContentWithMedia',\n 'This lesson has no written content beyond the attached media.'\n )\n : tr(\n 'buildmyiq.lessonPlayer.noContent',\n 'This lesson has no written content'\n )}\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=\"me-1 h-4 w-4 rtl:-scale-x-100\" />{' '}\n {tr('buildmyiq.lessonPlayer.previous', '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\" />{' '}\n {tr('buildmyiq.lessonPlayer.lessonComplete', '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=\"me-1.5 h-4 w-4 animate-spin\" />\n ) : (\n <CheckCircle2 className=\"me-1.5 h-4 w-4\" />\n )}\n {recordProgress.isPending\n ? tr('buildmyiq.lessonPlayer.markingComplete', 'Marking complete…')\n : tr('buildmyiq.lessonPlayer.markComplete', '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 {tr('buildmyiq.lessonPlayer.next', 'Next')}{' '}\n <ChevronRight className=\"ms-1 h-4 w-4 rtl:-scale-x-100\" />\n </Button>\n </div>\n </GlassCard>\n )}\n\n {selectedLesson && courseId && enrollmentId && (\n <div className=\"mt-5\">\n {/* Course changes reset local state; lesson navigation keeps the course conversation. */}\n <TutorPanel\n key={courseId}\n courseId={courseId}\n enrollmentId={enrollmentId}\n lessonId={selectedLesson.id}\n lessonTitle={selectedLesson.title}\n lessonTitlesById={lessonTitlesById}\n />\n </div>\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, useSearchParams } from 'react-router';\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 OfflineLessonControl,\n TutorPanel,\n} from '../../components';\nimport { useBuildmyiqAnalytics } from '../../analytics';\nimport { useBuildmyiq } from '../../context';\nimport {\n useBuiMyEnrollment,\n useBuiCourse,\n useBuiRecordMyLessonProgress,\n useBuiCompleteMyEnrollment,\n} from '../../hooks';\nimport { useTr } from '../../shared/hooks/useTr';\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 // `?lesson=<id>` deep-links straight to one lesson (used by the \"what's next\"\n // suggestion on My Learning). Purely an INITIAL anchor — once the learner\n // picks a lesson themselves, their pick wins.\n const [searchParams] = useSearchParams();\n const requestedLessonId = searchParams.get('lesson');\n const { navigateTo } = useBuildmyiq();\n const tr = useTr();\n\n const enrollmentQuery = useBuiMyEnrollment(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 = useBuiRecordMyLessonProgress();\n const completeEnrollment = useBuiCompleteMyEnrollment();\n const { trackLessonStarted, trackLessonCompleted, trackCourseCompleted } =\n useBuildmyiqAnalytics();\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 // Error from the last \"Mark complete\" / \"Finish course\" attempt. Without it a\n // rejected mutation surfaced only as an unhandled promise rejection and the\n // button silently did nothing.\n const [actionError, setActionError] = useState<string | null>(null);\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({\n id: '__standalone',\n title: tr('common.lessons', 'Lessons'),\n lessons: standalone,\n });\n }\n return result;\n }, [course, tr]);\n\n // Flat, ordered lesson list for prev/next + completion checks.\n const orderedLessons = useMemo<BuiLesson[]>(() => groups.flatMap((g) => g.lessons), [groups]);\n // Lesson id → title for this course, so the tutor panel can NAME the lessons\n // its answers were grounded in instead of showing opaque ids.\n const lessonTitlesById = useMemo<ReadonlyMap<string, string>>(\n () => new Map(orderedLessons.map((lesson) => [lesson.id, lesson.title])),\n [orderedLessons]\n );\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 if (requestedLessonId && orderedLessons.some((l) => l.id === requestedLessonId)) {\n return requestedLessonId;\n }\n return orderedLessons[0].id;\n }, [orderedLessons, userSelectedId, requestedLessonId]);\n\n // Record IN_PROGRESS once when a not-yet-started lesson is opened. That\n // first successful write is also the `buildmyiq.lesson.started` fact, so a\n // re-opened lesson is never counted as a second start.\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 const lessonType = orderedLessons.find((l) => l.id === selectedLessonId)?.type;\n recordMutate(\n {\n enrollmentId,\n lessonId: selectedLessonId,\n status: 'IN_PROGRESS',\n },\n {\n onSuccess: () =>\n trackLessonStarted({ courseId, enrollmentId, lessonId: selectedLessonId, lessonType }),\n }\n );\n }, [\n enrollmentId,\n selectedLessonId,\n progressByLesson,\n recordMutate,\n orderedLessons,\n courseId,\n trackLessonStarted,\n ]);\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 setActionError(null);\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 setActionError(null);\n try {\n await recordProgress.mutateAsync({\n enrollmentId,\n lessonId: selectedLesson.id,\n status: 'COMPLETED',\n });\n } catch (err) {\n setActionError(\n err instanceof Error && err.message\n ? err.message\n : tr('buildmyiq.lessonPlayer.markCompleteError', 'Could not mark this lesson complete.')\n );\n return;\n }\n trackLessonCompleted({\n courseId,\n enrollmentId,\n lessonId: selectedLesson.id,\n lessonType: selectedLesson.type,\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 setActionError(null);\n try {\n const completed = await completeEnrollment.mutateAsync({ id: enrollmentId });\n trackCourseCompleted({ courseId: completed.courseId, enrollmentId: completed.id });\n } catch (err) {\n setActionError(\n err instanceof Error && err.message\n ? err.message\n : tr('buildmyiq.lessonPlayer.finishCourseError', 'Could not finish this course.')\n );\n }\n };\n\n // ---- top-level states ----------------------------------------------------\n if (!enrollmentId) {\n return (\n <PageLayout title={tr('buildmyiq.lessonPlayer.pageTitle', 'Lesson')}>\n <EmptyState\n illustration=\"empty-data\"\n title={tr('buildmyiq.lessonPlayer.noEnrollmentTitle', 'No enrollment selected')}\n description={tr(\n 'buildmyiq.lessonPlayer.noEnrollmentDescription',\n 'Open a course from My Learning to start the lesson player.'\n )}\n action={\n <Button onClick={() => navigateTo('learning')}>\n {tr('buildmyiq.lessonPlayer.backToLearning', 'Back to My Learning')}\n </Button>\n }\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=\"me-1 h-4 w-4 rtl:-scale-x-100\" />{' '}\n {tr('buildmyiq.nav.learning', 'My Learning')}\n </Button>\n );\n\n if (isLoading) {\n return (\n <PageLayout title={tr('buildmyiq.lessonPlayer.pageTitle', 'Lesson')} actions={backAction}>\n <LoadingView label={tr('buildmyiq.lessonPlayer.loading', 'Loading your lesson…')} />\n </PageLayout>\n );\n }\n\n if (enrollmentQuery.isError) {\n return (\n <PageLayout title={tr('buildmyiq.lessonPlayer.pageTitle', 'Lesson')} actions={backAction}>\n <ErrorView\n title={tr('buildmyiq.lessonPlayer.enrollmentErrorTitle', \"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={tr('buildmyiq.lessonPlayer.pageTitle', 'Lesson')} actions={backAction}>\n <EmptyState\n illustration=\"empty-data\"\n title={tr('buildmyiq.lessonPlayer.enrollmentNotFoundTitle', 'Enrollment not found')}\n description={tr(\n 'buildmyiq.lessonPlayer.enrollmentNotFoundDescription',\n 'This enrollment may have been removed or is not available to you.'\n )}\n action={\n <Button onClick={() => navigateTo('learning')}>\n {tr('buildmyiq.lessonPlayer.backToLearning', 'Back to My Learning')}\n </Button>\n }\n />\n </PageLayout>\n );\n }\n\n if (courseQuery.isError) {\n return (\n <PageLayout title={tr('buildmyiq.lessonPlayer.pageTitle', 'Lesson')} actions={backAction}>\n <ErrorView\n title={tr('buildmyiq.lessonPlayer.courseErrorTitle', \"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\n title={course?.title ?? tr('buildmyiq.lessonPlayer.pageTitle', 'Lesson')}\n actions={backAction}\n >\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 {tr(\n 'buildmyiq.lessonPlayer.progressSummary',\n '{{completed}} of {{total}} lessons complete',\n { completed: completedCount, total: orderedLessons.length }\n )}\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={completeEnrollment.isPending}\n >\n {completeEnrollment.isPending ? (\n <Loader2 className=\"me-1 h-4 w-4 animate-spin\" />\n ) : (\n <Trophy className=\"me-1 h-4 w-4\" />\n )}\n {tr('buildmyiq.lessonPlayer.finishCourse', 'Finish course')}\n </Button>\n )}\n </GlassCard>\n\n {actionError && (\n <p role=\"alert\" className=\"text-sm text-status-error-text\">\n {actionError}\n </p>\n )}\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=\"me-1 h-4 w-4\" />\n {showTree\n ? tr('buildmyiq.lessonPlayer.hideLessons', 'Hide lessons')\n : tr('buildmyiq.lessonPlayer.showLessons', 'Lessons ({{completed}}/{{total}})', {\n completed: completedCount,\n total: orderedLessons.length,\n })}\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 {tr('buildmyiq.lessonPlayer.noLessons', '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-start 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=\"me-1.5 h-4 w-4\" />{' '}\n {tr('buildmyiq.lessonPlayer.courseOverview', 'Course overview')}\n </Button>\n </div>\n )}\n </>\n )}\n </GlassCard>\n </aside>\n\n {/* Right: selected lesson */}\n <section\n aria-label={tr('buildmyiq.lessonPlayer.lessonRegion', 'Lesson')}\n className=\"min-w-0 flex-1\"\n >\n {!selectedLesson ? (\n <EmptyState\n illustration=\"empty-data\"\n title={tr('buildmyiq.lessonPlayer.noLessonTitle', 'No lesson to show')}\n description={tr(\n 'buildmyiq.lessonPlayer.noLessonDescription',\n \"This course doesn't have any lessons yet. Check back once the author adds content.\"\n )}\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\" />{' '}\n {tr('buildmyiq.lessonPlayer.completedLabel', 'Completed')}\n </span>\n )}\n <OfflineLessonControl key={selectedLesson.id} lesson={selectedLesson} />\n <span className=\"ms-auto text-xs tabular-nums text-text-secondary\">\n {tr('buildmyiq.lessonPlayer.lessonPosition', 'Lesson {{n}} of {{total}}', {\n n: selectedIndex + 1,\n total: orderedLessons.length,\n })}\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 {tr('buildmyiq.lessonPlayer.resources', '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 {selectedLesson.media\n ? tr(\n 'buildmyiq.lessonPlayer.noContentWithMedia',\n 'This lesson has no written content beyond the attached media.'\n )\n : tr(\n 'buildmyiq.lessonPlayer.noContent',\n 'This lesson has no written content'\n )}\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=\"me-1 h-4 w-4 rtl:-scale-x-100\" />{' '}\n {tr('buildmyiq.lessonPlayer.previous', '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\" />{' '}\n {tr('buildmyiq.lessonPlayer.lessonComplete', '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=\"me-1.5 h-4 w-4 animate-spin\" />\n ) : (\n <CheckCircle2 className=\"me-1.5 h-4 w-4\" />\n )}\n {recordProgress.isPending\n ? tr('buildmyiq.lessonPlayer.markingComplete', 'Marking complete…')\n : tr('buildmyiq.lessonPlayer.markComplete', '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 {tr('buildmyiq.lessonPlayer.next', 'Next')}{' '}\n <ChevronRight className=\"ms-1 h-4 w-4 rtl:-scale-x-100\" />\n </Button>\n </div>\n </GlassCard>\n )}\n\n {selectedLesson && courseId && enrollmentId && (\n <div className=\"mt-5\">\n {/* Course changes reset local state; lesson navigation keeps the course conversation. */}\n <TutorPanel\n key={courseId}\n courseId={courseId}\n enrollmentId={enrollmentId}\n lessonId={selectedLesson.id}\n lessonTitle={selectedLesson.title}\n lessonTitlesById={lessonTitlesById}\n />\n </div>\n )}\n </section>\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;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC;AAsOhC,eAAO,MAAM,cAAc,EAAE,
|
|
1
|
+
{"version":3,"file":"MyLearningPage.d.ts","sourceRoot":"","sources":["../../../../src/buildmyiq/pages/learning/MyLearningPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC;AAsOhC,eAAO,MAAM,cAAc,EAAE,EAymB5B,CAAC"}
|
|
@@ -164,9 +164,9 @@ var le = new Set(["ENROLLED", "IN_PROGRESS"]), P = .7, ue = ({ enrollment: e, co
|
|
|
164
164
|
let e = /* @__PURE__ */ new Map();
|
|
165
165
|
for (let t of V) e.set(t.id, t);
|
|
166
166
|
return e;
|
|
167
|
-
}, [V]), G = (e) => W.get(e)?.title ?? E("
|
|
167
|
+
}, [V]), G = (e) => W.get(e)?.title ?? E("buildmyiq.learning.unavailableCourse", "Course no longer available"), pe = (e) => {
|
|
168
168
|
let t = e.enrollmentId ? fe.get(e.enrollmentId) : void 0;
|
|
169
|
-
return t ? G(t.courseId) : E("buildmyiq.
|
|
169
|
+
return t ? G(t.courseId) : E("buildmyiq.learning.unlinkedAssessment", "Assessment");
|
|
170
170
|
}, K = x(() => V.filter((e) => le.has(e.status)), [V]), q = x(() => V.filter((e) => e.status === "COMPLETED"), [V]), J = x(() => K.length === 0 ? null : [...K].sort((e, t) => (t.lastActiveAt ?? t.enrolledAt).localeCompare(e.lastActiveAt ?? e.enrolledAt))[0], [K]), Y = x(() => [...P === "in_progress" ? K : P === "completed" ? q : V].sort((e, t) => (t.lastActiveAt ?? t.enrolledAt).localeCompare(e.lastActiveAt ?? e.enrolledAt)), [
|
|
171
171
|
P,
|
|
172
172
|
K,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MyLearningPage.js","names":[],"sources":["../../../../src/buildmyiq/pages/learning/MyLearningPage.tsx"],"sourcesContent":["/**\n * MyLearningPage — the caller's personal learning surface.\n * @module buildmyiq/pages/learning\n *\n * Renders the current learner's real enrollments (`buiListEnrollments` filtered\n * by `learnerId`) grouped by progress, their assessment results\n * (`buiListSubmissions`), and the credentials they have earned\n * (`buiListCertificates`). Course titles + covers are resolved from\n * `buiListCourses` (id → course map, falling back to \"Course\"). Every list\n * carries an honest loading / error / empty state — no fabricated rows.\n *\n * Also hosts the \"what should I learn next\" suggestion (`buiSuggestNextLesson`)\n * for the learner's most recently active course — see NextLessonSuggestion.\n */\n\nimport type { FC } from 'react';\nimport { useMemo, useState } from 'react';\nimport {\n GraduationCap,\n Award,\n ArrowRight,\n PlayCircle,\n ClipboardCheck,\n CheckCircle2,\n XCircle,\n Users,\n Calendar,\n TrendingUp,\n Compass,\n Gauge,\n} from 'lucide-react';\nimport { Button, GlassCard } from '@burdenoff/fe-libs/ui';\nimport {\n PageLayout,\n Tabs,\n StatusBadge,\n MiniBar,\n EmptyState,\n LoadingView,\n ErrorView,\n Section,\n FileLink,\n DataTable,\n ProgressRing,\n Sparkline,\n} from '../../components';\nimport type { TabItem, Column } from '../../components';\nimport { cn } from '../../utils/cn';\nimport { useBuildmyiq } from '../../context';\nimport {\n useBuiMyEnrollments,\n useBuiCourses,\n useBuiMyCertificates,\n useBuiMySubmissions,\n useBuiSuggestNextLesson,\n useBuiReviewNextLessonSuggestion,\n} from '../../hooks';\nimport { useTr } from '../../shared/hooks/useTr';\nimport { formatRelativeTime, formatDate, formatDateTime } from '../../utils/formatters';\nimport { dueLabel } from '../automations/automationModel';\nimport type {\n BuiEnrollment,\n BuiCourse,\n BuiCertificate,\n BuiSubmission,\n BuiNextLessonSuggestion,\n BuiPacingHint,\n} from '../../types';\n\ntype LearningTab = 'in_progress' | 'completed' | 'all';\n\nconst IN_PROGRESS_STATUSES = new Set(['ENROLLED', 'IN_PROGRESS']);\n\n/**\n * A next-lesson suggestion is worth a second look below this confidence — the\n * same \"low confidence makes the review obligation explicit\" convention the AI\n * grading panel uses (see pages/submissions/aiGrading.ts).\n */\nconst LOW_SUGGESTION_CONFIDENCE = 0.7;\n\n/**\n * \"What should I learn next\" for the learner's most recently active course\n * (BOFF-4434). `buiSuggestNextLesson` is a deterministic curriculum-order walk\n * plus a pacing hint derived from their recent graded scores — it persists an\n * auditable record but NEVER advances progress on its own, so the learner has\n * to act on it. Asking is an explicit click (it is a mutation), mirroring the\n * educator-side AI grading panel rather than firing on every page view.\n */\nconst NextLessonSuggestion: FC<{\n enrollment: BuiEnrollment;\n courseTitle: string;\n onOpenLesson: (enrollmentId: string, lessonId: string) => void;\n}> = ({ enrollment, courseTitle, onOpenLesson }) => {\n const tr = useTr();\n const suggestNext = useBuiSuggestNextLesson();\n const reviewSuggestion = useBuiReviewNextLessonSuggestion();\n const [suggestion, setSuggestion] = useState<BuiNextLessonSuggestion | null>(null);\n const [dismissed, setDismissed] = useState(false);\n\n const PACING_LABEL: Record<BuiPacingHint, string> = {\n SLOW_DOWN: tr('buildmyiq.learning.pacing.slowDown', 'Slow down'),\n STEADY: tr('buildmyiq.learning.pacing.steady', 'Steady'),\n ACCELERATE: tr('buildmyiq.learning.pacing.accelerate', 'Speed up'),\n };\n\n const handleSuggest = async (): Promise<void> => {\n setDismissed(false);\n try {\n setSuggestion(await suggestNext.mutateAsync(enrollment.id));\n } catch {\n // Surfaced below via suggestNext.isError / suggestNext.error.\n }\n };\n\n const handleOpen = (): void => {\n if (!suggestion) return;\n reviewSuggestion.mutate({ id: suggestion.id, outcome: 'ACCEPTED' });\n onOpenLesson(suggestion.enrollmentId, suggestion.lessonId);\n };\n\n const handleDismiss = (): void => {\n if (suggestion) reviewSuggestion.mutate({ id: suggestion.id, outcome: 'REJECTED' });\n setSuggestion(null);\n setDismissed(true);\n };\n\n if (dismissed && !suggestion) return null;\n\n if (!suggestion) {\n return (\n <GlassCard\n treatment=\"plain\"\n className=\"flex flex-wrap items-center gap-x-4 gap-y-3 border-dashed p-4\"\n >\n <span className=\"flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-[var(--color-accent-soft)]\">\n <Compass className=\"h-5 w-5 text-[var(--color-accent-teal)]\" />\n </span>\n <div className=\"min-w-0 flex-1\">\n <p className=\"text-sm font-semibold text-text-primary\">\n {tr('buildmyiq.learning.next.prompt', 'Not sure what to pick up next?')}\n </p>\n <p className=\"text-xs text-text-secondary\">\n {tr(\n 'buildmyiq.learning.next.promptHelp',\n 'Get the next lesson in {{course}}, paced to your recent scores.',\n { course: courseTitle }\n )}\n </p>\n {suggestNext.isError && (\n <p className=\"mt-1.5 text-xs text-status-error-text\">\n {suggestNext.error instanceof Error\n ? suggestNext.error.message\n : tr('buildmyiq.learning.next.error', \"Couldn't work out a next lesson right now.\")}\n </p>\n )}\n </div>\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={handleSuggest}\n disabled={suggestNext.isPending}\n >\n {suggestNext.isPending\n ? tr('buildmyiq.learning.next.asking', 'Working it out…')\n : tr('buildmyiq.learning.next.ask', 'Suggest my next lesson')}\n </Button>\n </GlassCard>\n );\n }\n\n const lowConfidence = suggestion.confidence < LOW_SUGGESTION_CONFIDENCE;\n\n return (\n <GlassCard treatment=\"glass\" glow className=\"flex flex-col gap-3 p-5\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <span className=\"flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-[var(--color-accent-soft)]\">\n <Compass className=\"h-4 w-4 text-[var(--color-accent-teal)]\" />\n </span>\n <span className=\"rounded-full border border-[var(--color-accent-teal)] px-2.5 py-0.5 text-xs font-medium text-[var(--color-accent-teal)]\">\n {tr('buildmyiq.learning.next.badge', 'Suggestion')}\n </span>\n <span className=\"truncate text-xs text-text-tertiary\">{courseTitle}</span>\n </div>\n\n <div className=\"min-w-0\">\n <p className=\"text-lg font-semibold text-text-primary\">\n {suggestion.lessonTitle}\n {suggestion.moduleTitle ? (\n <span className=\"font-normal text-text-secondary\"> · {suggestion.moduleTitle}</span>\n ) : null}\n </p>\n <p className=\"mt-1 text-sm text-text-secondary\">{suggestion.rationale}</p>\n </div>\n\n <div className=\"flex flex-wrap items-center gap-x-3 gap-y-2 text-xs text-text-secondary\">\n <span className=\"inline-flex items-center gap-1.5 rounded-full bg-bg-sunken px-2.5 py-1 font-medium text-text-primary\">\n <Gauge className=\"h-3.5 w-3.5\" />\n {PACING_LABEL[suggestion.pacingHint]}\n </span>\n <span\n className={\n lowConfidence ? 'font-semibold text-status-warning-text' : 'text-text-tertiary'\n }\n >\n {tr('buildmyiq.learning.next.confidence', '{{pct}}% confidence', {\n pct: Math.round(suggestion.confidence * 100),\n })}\n </span>\n </div>\n\n {lowConfidence && (\n <p className=\"rounded-lg border border-status-warning-border bg-status-warning-bg-subtle p-2 text-xs text-status-warning-text\">\n {tr(\n 'buildmyiq.learning.next.lowConfidence',\n \"There isn't much recent activity to go on — treat this as a starting point, not a verdict.\"\n )}\n </p>\n )}\n\n <div className=\"flex flex-wrap items-center gap-2\">\n <Button size=\"sm\" onClick={handleOpen}>\n {tr('buildmyiq.learning.next.open', 'Open lesson')}\n <ArrowRight className=\"ms-1 h-4 w-4 rtl:-scale-x-100\" />\n </Button>\n <Button variant=\"ghost\" size=\"sm\" onClick={handleDismiss}>\n {tr('buildmyiq.learning.next.dismiss', 'Not now')}\n </Button>\n <span className=\"ms-auto text-xs text-text-tertiary\">\n {tr(\n 'buildmyiq.learning.next.noWriteNote',\n 'This suggestion never changes your progress on its own.'\n )}\n </span>\n </div>\n </GlassCard>\n );\n};\n\nconst scoreClass = (s: BuiSubmission): string =>\n s.passed == null\n ? 'text-text-primary'\n : s.passed\n ? 'text-status-success-text'\n : 'text-status-error-text';\n\nexport const MyLearningPage: FC = () => {\n const { currentUser, navigateTo } = useBuildmyiq();\n const tr = useTr();\n const learnerId = currentUser?.id;\n\n const [tab, setTab] = useState<LearningTab>('in_progress');\n\n // Self-scoped `buiMy*` reads — the learner's own records, identity from the\n // authenticated actor (FERPA/IDOR hardening removed the learnerId arg).\n const enrollmentsQuery = useBuiMyEnrollments({\n limit: 100,\n enabled: Boolean(learnerId),\n });\n const coursesQuery = useBuiCourses({ limit: 100 });\n const certsQuery = useBuiMyCertificates(undefined, Boolean(learnerId));\n const submissionsQuery = useBuiMySubmissions({\n limit: 50,\n enabled: Boolean(learnerId),\n });\n const submissions = useMemo(\n () =>\n [...(submissionsQuery.data?.items ?? [])].sort((a, b) =>\n (b.submittedAt ?? b.createdAt).localeCompare(a.submittedAt ?? a.createdAt)\n ),\n [submissionsQuery.data]\n );\n\n const enrollments: BuiEnrollment[] = useMemo(\n () => enrollmentsQuery.data?.items ?? [],\n [enrollmentsQuery.data]\n );\n const courses: BuiCourse[] = useMemo(() => coursesQuery.data?.items ?? [], [coursesQuery.data]);\n const certificates: BuiCertificate[] = useMemo(() => certsQuery.data ?? [], [certsQuery.data]);\n\n const courseById = useMemo(() => {\n const map = new Map<string, BuiCourse>();\n for (const c of courses) map.set(c.id, c);\n return map;\n }, [courses]);\n\n const enrollmentById = useMemo(() => {\n const map = new Map<string, BuiEnrollment>();\n for (const e of enrollments) map.set(e.id, e);\n return map;\n }, [enrollments]);\n\n const titleFor = (courseId: string): string =>\n courseById.get(courseId)?.title ?? tr('common.title', 'Course');\n\n /** Resolve a submission's course via its enrollment (falls back honestly). */\n const courseTitleForSubmission = (s: BuiSubmission): string => {\n const enr = s.enrollmentId ? enrollmentById.get(s.enrollmentId) : undefined;\n return enr ? titleFor(enr.courseId) : tr('buildmyiq.assessments.title', 'Assessment');\n };\n\n const inProgress = useMemo(\n () => enrollments.filter((e) => IN_PROGRESS_STATUSES.has(e.status)),\n [enrollments]\n );\n const completed = useMemo(\n () => enrollments.filter((e) => e.status === 'COMPLETED'),\n [enrollments]\n );\n\n // The course the learner touched most recently — the one a \"what's next\"\n // suggestion is actually about.\n const mostRecentlyActive = useMemo<BuiEnrollment | null>(() => {\n if (inProgress.length === 0) return null;\n return [...inProgress].sort((a, b) =>\n (b.lastActiveAt ?? b.enrolledAt).localeCompare(a.lastActiveAt ?? a.enrolledAt)\n )[0];\n }, [inProgress]);\n\n const visible = useMemo(() => {\n const rows = tab === 'in_progress' ? inProgress : tab === 'completed' ? completed : enrollments;\n return [...rows].sort((a, b) =>\n (b.lastActiveAt ?? b.enrolledAt).localeCompare(a.lastActiveAt ?? a.enrolledAt)\n );\n }, [tab, inProgress, completed, enrollments]);\n\n const sortedCertificates = useMemo(\n () => [...certificates].sort((a, b) => b.issuedAt.localeCompare(a.issuedAt)),\n [certificates]\n );\n\n // Derived score summary (real graded submissions only) — powers the trend strip.\n const graded = useMemo(() => submissions.filter((s) => s.score != null), [submissions]);\n const scoreTrend = useMemo(() => [...graded].reverse().map((s) => s.score as number), [graded]);\n const avgScore =\n graded.length > 0\n ? Math.round(graded.reduce((sum, s) => sum + (s.score ?? 0), 0) / graded.length)\n : null;\n const passedCount = useMemo(\n () => submissions.filter((s) => s.passed === true).length,\n [submissions]\n );\n\n const tabs: TabItem<LearningTab>[] = [\n {\n value: 'in_progress',\n label: tr('buildmyiq.learning.tabInProgress', 'In progress'),\n count: inProgress.length,\n },\n {\n value: 'completed',\n label: tr('buildmyiq.learning.tabCompleted', 'Completed'),\n count: completed.length,\n },\n { value: 'all', label: tr('buildmyiq.learning.tabAll', 'All'), count: enrollments.length },\n ];\n\n const isLoading = Boolean(learnerId) && (enrollmentsQuery.isLoading || coursesQuery.isLoading);\n\n const renderEnrollments = () => {\n if (!learnerId) {\n return (\n <EmptyState\n illustration=\"empty-people\"\n title={tr('buildmyiq.learning.signInTitle', 'Sign in to see your learning')}\n description={tr(\n 'buildmyiq.learning.signInDescription',\n 'Your enrollments and progress appear here once you are signed in to the workspace.'\n )}\n />\n );\n }\n if (isLoading)\n return (\n <LoadingView\n label={tr('buildmyiq.learning.loadingEnrollments', 'Loading your enrollments…')}\n />\n );\n if (enrollmentsQuery.isError) {\n return (\n <ErrorView\n title={tr('buildmyiq.learning.errorTitle', \"Couldn't load your learning\")}\n message={\n enrollmentsQuery.error instanceof Error ? enrollmentsQuery.error.message : undefined\n }\n onRetry={() => enrollmentsQuery.refetch()}\n />\n );\n }\n if (enrollments.length === 0) {\n return (\n <EmptyState\n illustration=\"onboarding\"\n title={tr('buildmyiq.learning.notEnrolledTitle', \"You're not enrolled in anything yet\")}\n description={tr(\n 'buildmyiq.learning.notEnrolledDescription',\n 'Browse the catalog and enrol in a course to start learning.'\n )}\n action={\n <Button onClick={() => navigateTo('catalog')}>\n {tr('buildmyiq.learning.exploreCatalog', 'Explore the catalog')}\n </Button>\n }\n />\n );\n }\n if (visible.length === 0) {\n const label =\n tab === 'in_progress'\n ? tr('buildmyiq.learning.noInProgressTitle', 'No courses in progress')\n : tab === 'completed'\n ? tr('buildmyiq.learning.noCompletedTitle', 'No completed courses yet')\n : tr('buildmyiq.learning.noCoursesTitle', 'No courses');\n return (\n <EmptyState\n illustration=\"empty-data\"\n title={label}\n description={\n tab === 'completed'\n ? tr(\n 'buildmyiq.learning.noCompletedDescription',\n 'Finish a course to see it here — and earn a certificate.'\n )\n : tr(\n 'buildmyiq.learning.noFilteredDescription',\n 'Switch tabs or enrol in a new course to keep learning.'\n )\n }\n />\n );\n }\n return (\n <div\n className=\"grid grid-cols-1 gap-4 xl:grid-cols-2\"\n data-tour=\"buildmyiq-learning-enrollment-grid\"\n >\n {visible.map((e) => {\n const course = courseById.get(e.courseId);\n const cover = course?.cover?.downloadUrl ?? null;\n const isCompleted = e.status === 'COMPLETED';\n const pct = Math.round(e.progressPct ?? 0);\n const due = dueLabel(e.dueAt, e.status);\n return (\n <GlassCard\n key={e.id}\n treatment=\"plain\"\n className=\"group flex flex-col overflow-hidden transition-shadow hover:shadow-lg sm:flex-row\"\n >\n {/* Media tile — real cover when available, soft accent fallback; play\n affordance + a live progress ring badge. */}\n <div className=\"relative aspect-video w-full shrink-0 overflow-hidden bg-[var(--color-accent-soft)] sm:aspect-auto sm:w-44\">\n {cover ? (\n <img\n src={cover}\n alt=\"\"\n className=\"h-full w-full object-cover transition-transform duration-500 group-hover:scale-105\"\n />\n ) : null}\n <div className=\"absolute inset-0 flex items-center justify-center\">\n <span className=\"flex h-11 w-11 items-center justify-center rounded-full bg-bg-surface/85 text-text-primary shadow-sm backdrop-blur transition-transform group-hover:scale-110\">\n <PlayCircle className=\"h-6 w-6\" />\n </span>\n </div>\n <div className=\"absolute bottom-2 start-2 rounded-full bg-bg-surface/85 p-0.5 shadow-sm backdrop-blur\">\n <ProgressRing\n value={pct}\n size={38}\n stroke={5}\n color={\n isCompleted ? 'var(--color-status-success-text)' : 'var(--color-accent-teal)'\n }\n />\n </div>\n </div>\n\n {/* Content */}\n <div className=\"flex min-w-0 flex-1 flex-col gap-2 p-4\">\n <div className=\"flex flex-wrap items-start gap-2\">\n <p className=\"min-w-0 flex-1 truncate font-semibold text-text-primary\">\n {titleFor(e.courseId)}\n </p>\n <StatusBadge status={e.status} />\n </div>\n\n {(course?.level || course?.skill?.name || e.cohort?.name) && (\n <div className=\"flex flex-wrap items-center gap-2 text-xs text-text-secondary\">\n {course?.level && <StatusBadge status={course.level} />}\n {course?.skill?.name && (\n <span className=\"rounded-full bg-bg-sunken px-2 py-0.5\">\n {course.skill.name}\n </span>\n )}\n {e.cohort?.name && (\n <span className=\"inline-flex items-center gap-1\">\n <Users className=\"h-3.5 w-3.5\" /> {e.cohort.name}\n </span>\n )}\n </div>\n )}\n\n <div className=\"mt-1 flex items-center gap-2\">\n <MiniBar\n value={e.progressPct}\n barClassName={\n isCompleted\n ? 'bg-[var(--color-status-success-text)]'\n : 'bg-[var(--color-accent-primary)]'\n }\n className=\"flex-1\"\n />\n <span className=\"shrink-0 text-xs font-semibold tabular-nums text-text-secondary\">\n {pct}%\n </span>\n </div>\n\n {due && (\n <p\n className={\n due.overdue\n ? 'text-xs font-medium text-status-warning-text'\n : 'text-xs text-text-secondary'\n }\n >\n {due.text}\n </p>\n )}\n <p className=\"text-xs text-text-secondary\">\n {isCompleted && e.completedAt\n ? tr('common.completed', 'Completed {{time}}', {\n time: formatRelativeTime(e.completedAt),\n })\n : tr('common.active', 'Active {{time}}', {\n time: formatRelativeTime(e.lastActiveAt ?? e.enrolledAt),\n })}\n </p>\n\n <div className=\"mt-auto flex flex-wrap gap-2 pt-2\">\n <Button\n size=\"sm\"\n className=\"max-sm:flex-1\"\n onClick={() => navigateTo(`learning/${e.id}`)}\n >\n {isCompleted\n ? tr('common.review', 'Review')\n : tr('common.continue', 'Continue')}\n <ArrowRight className=\"ms-1 h-4 w-4 rtl:-scale-x-100\" />\n </Button>\n <Button\n variant=\"secondary\"\n size=\"sm\"\n className=\"max-sm:flex-1\"\n onClick={() => navigateTo(`catalog/${e.courseId}`)}\n >\n {tr('common.overview', 'Overview')}\n </Button>\n </div>\n </div>\n </GlassCard>\n );\n })}\n </div>\n );\n };\n\n const resultColumns: Column<BuiSubmission>[] = [\n {\n key: 'assessment',\n header: tr('buildmyiq.learning.columnAssessment', 'Assessment'),\n render: (s) => (\n <div className=\"flex min-w-0 items-center gap-3\">\n <span className=\"flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-[var(--color-accent-soft)]\">\n <ClipboardCheck className=\"h-4 w-4 text-text-primary\" />\n </span>\n <div className=\"min-w-0\">\n <p className=\"truncate font-medium text-text-primary\">{courseTitleForSubmission(s)}</p>\n <p className=\"text-xs text-text-secondary\">\n {tr('buildmyiq.learning.attempt', 'Attempt {{n}}', { n: s.attempt })}\n </p>\n </div>\n </div>\n ),\n },\n {\n key: 'status',\n header: tr('buildmyiq.learning.columnStatus', 'Status'),\n render: (s) => <StatusBadge status={s.status} />,\n },\n {\n key: 'result',\n header: tr('buildmyiq.learning.columnResult', 'Result'),\n render: (s) =>\n s.passed == null ? (\n <span className=\"text-text-tertiary\">—</span>\n ) : (\n <span\n className={cn(\n 'inline-flex items-center gap-1.5 text-sm font-medium',\n s.passed ? 'text-status-success-text' : 'text-status-error-text'\n )}\n >\n {s.passed ? <CheckCircle2 className=\"h-4 w-4\" /> : <XCircle className=\"h-4 w-4\" />}\n {s.passed\n ? tr('buildmyiq.learning.passed', 'Passed')\n : tr('buildmyiq.learning.notPassed', 'Not passed')}\n </span>\n ),\n },\n {\n key: 'feedback',\n header: tr('buildmyiq.learning.columnFeedback', 'Feedback'),\n hideOnMobile: true,\n render: (s) =>\n s.feedback ? (\n <p className=\"line-clamp-2 max-w-[18rem] text-sm text-text-secondary\">{s.feedback}</p>\n ) : (\n <span className=\"text-text-tertiary\">—</span>\n ),\n },\n {\n key: 'submitted',\n header: tr('buildmyiq.learning.columnSubmitted', 'Submitted'),\n hideOnMobile: true,\n sortValue: (s) => s.submittedAt ?? s.createdAt,\n render: (s) => (\n <span className=\"whitespace-nowrap text-sm text-text-secondary\">\n {formatDateTime(s.submittedAt ?? s.createdAt)}\n </span>\n ),\n },\n {\n key: 'score',\n header: tr('buildmyiq.learning.columnScore', 'Score'),\n align: 'right',\n sortValue: (s) => s.score ?? -1,\n render: (s) => (\n <span className={cn('text-lg font-bold tabular-nums', scoreClass(s))}>\n {s.score != null ? `${s.score}%` : '—'}\n </span>\n ),\n },\n ];\n\n const renderResults = () => {\n if (!learnerId) return null;\n if (submissionsQuery.isLoading)\n return (\n <LoadingView label={tr('buildmyiq.learning.loadingResults', 'Loading your results…')} />\n );\n if (submissionsQuery.isError)\n return (\n <ErrorView\n title={tr('buildmyiq.learning.resultsErrorTitle', \"Couldn't load your results\")}\n message={\n submissionsQuery.error instanceof Error ? submissionsQuery.error.message : undefined\n }\n onRetry={() => submissionsQuery.refetch()}\n />\n );\n if (submissions.length === 0)\n return (\n <EmptyState\n illustration=\"empty-data\"\n title={tr('buildmyiq.learning.noResultsTitle', 'No assessment results yet')}\n description={tr(\n 'buildmyiq.learning.noResultsDescription',\n 'Take a course assessment and your score and feedback will appear here.'\n )}\n />\n );\n return (\n <div className=\"flex flex-col gap-3\">\n {avgScore != null && (\n <GlassCard\n treatment=\"metric\"\n className=\"flex flex-wrap items-center justify-between gap-4 p-4\"\n >\n <div className=\"flex items-center gap-3\">\n <ProgressRing value={avgScore} size={54} />\n <div>\n <p className=\"text-sm font-semibold text-text-primary\">\n {tr('buildmyiq.learning.averageScore', 'Average score')}\n </p>\n <p className=\"text-xs text-text-secondary\">\n {tr(\n 'buildmyiq.learning.passedSummary',\n '{{passed}} of {{graded}} passed · {{graded}} graded',\n {\n passed: passedCount,\n graded: graded.length,\n }\n )}\n </p>\n </div>\n </div>\n {scoreTrend.length >= 2 && (\n <div className=\"flex items-center gap-2 text-xs text-text-secondary\">\n <TrendingUp className=\"h-4 w-4 text-status-success-text\" />\n <Sparkline data={scoreTrend} width={128} height={36} />\n <span>{tr('buildmyiq.learning.scoreTrend', 'score trend')}</span>\n </div>\n )}\n </GlassCard>\n )}\n <DataTable\n columns={resultColumns}\n rows={submissions}\n rowKey={(s) => s.id}\n defaultSort={{ key: 'submitted', dir: 'desc' }}\n />\n </div>\n );\n };\n\n const renderCertificates = () => {\n if (!learnerId) return null;\n if (certsQuery.isLoading)\n return (\n <LoadingView\n label={tr('buildmyiq.learning.loadingCertificates', 'Loading your certificates…')}\n />\n );\n if (certsQuery.isError) {\n return (\n <ErrorView\n title={tr('buildmyiq.learning.certificatesErrorTitle', \"Couldn't load your certificates\")}\n message={certsQuery.error instanceof Error ? certsQuery.error.message : undefined}\n onRetry={() => certsQuery.refetch()}\n />\n );\n }\n if (sortedCertificates.length === 0) {\n return (\n <EmptyState\n illustration=\"empty-data\"\n title={tr('buildmyiq.learning.noCertificatesTitle', 'No certificates yet')}\n description={tr(\n 'buildmyiq.learning.noCertificatesDescription',\n 'Complete a credentialed course to earn a verifiable certificate.'\n )}\n />\n );\n }\n return (\n <div className=\"grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3\">\n {sortedCertificates.map((cert) => (\n <GlassCard\n key={cert.id}\n treatment=\"plain\"\n className=\"flex flex-col gap-3 p-4 transition-shadow hover:shadow-lg\"\n >\n <div className=\"flex items-start justify-between gap-3\">\n <div className=\"flex h-12 w-12 shrink-0 items-center justify-center rounded-xl bg-[color-mix(in_srgb,var(--color-status-warning-text)_16%,transparent)]\">\n <Award className=\"h-6 w-6 text-status-warning-text\" />\n </div>\n <StatusBadge status={cert.status} />\n </div>\n <div className=\"min-w-0\">\n <p className=\"truncate font-semibold text-text-primary\">{titleFor(cert.courseId)}</p>\n <div className=\"mt-1 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-text-secondary\">\n <span className=\"font-mono\">{cert.serial}</span>\n {cert.finalScore != null && (\n <span className=\"inline-flex items-center gap-1 text-status-success-text\">\n <CheckCircle2 className=\"h-3.5 w-3.5\" /> {cert.finalScore}%\n </span>\n )}\n </div>\n </div>\n <div className=\"mt-auto flex flex-wrap items-center justify-between gap-2 border-t border-border-seam/60 pt-3 text-xs text-text-secondary\">\n <span className=\"inline-flex items-center gap-1.5\">\n <Calendar className=\"h-3.5 w-3.5\" />\n {tr('buildmyiq.learning.issued', 'Issued {{date}}', {\n date: formatDate(cert.issuedAt),\n })}\n </span>\n {cert.file ? <FileLink fileId={cert.file.id} /> : null}\n </div>\n </GlassCard>\n ))}\n </div>\n );\n };\n\n return (\n <PageLayout\n title={tr('buildmyiq.learning.title', 'My Learning')}\n description={tr(\n 'buildmyiq.learning.description',\n \"Your enrollments, progress and the credentials you've earned.\"\n )}\n titleGradient\n aurora\n >\n <div className=\"flex flex-col gap-6\" data-tour=\"buildmyiq-learning-content\">\n {mostRecentlyActive && (\n <NextLessonSuggestion\n key={mostRecentlyActive.id}\n enrollment={mostRecentlyActive}\n courseTitle={titleFor(mostRecentlyActive.courseId)}\n onOpenLesson={(enrollmentId, lessonId) =>\n navigateTo(`learning/${enrollmentId}?lesson=${encodeURIComponent(lessonId)}`)\n }\n />\n )}\n\n <div data-tour=\"buildmyiq-learning-courses\">\n <Section\n title={tr('buildmyiq.learning.sectionCourses', 'Courses')}\n description={tr(\n 'buildmyiq.learning.sectionCoursesDescription',\n \"Everything you're enrolled in.\"\n )}\n >\n {learnerId && enrollments.length > 0 && (\n <div className=\"mb-4\" data-tour=\"buildmyiq-learning-tabs\">\n <Tabs items={tabs} value={tab} onChange={setTab} />\n </div>\n )}\n {renderEnrollments()}\n </Section>\n </div>\n\n <div data-tour=\"buildmyiq-learning-results\">\n <Section\n title={tr('buildmyiq.learning.sectionAssessments', 'Assessment results')}\n description={tr(\n 'buildmyiq.learning.sectionAssessmentsDescription',\n 'Your submitted assessments, scores and educator feedback.'\n )}\n >\n {renderResults()}\n </Section>\n </div>\n\n <div data-tour=\"buildmyiq-learning-certificates\">\n <Section\n title={tr('buildmyiq.learning.sectionCertificates', 'Certificates')}\n description={tr(\n 'buildmyiq.learning.sectionCertificatesDescription',\n \"Verifiable credentials issued for the courses you've completed.\"\n )}\n action={\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={() => navigateTo('certificates')}\n data-tour=\"buildmyiq-learning-all-certificates\"\n >\n <GraduationCap className=\"me-1 h-4 w-4\" />{' '}\n {tr('buildmyiq.learning.allCertificates', 'All certificates')}\n </Button>\n }\n >\n {renderCertificates()}\n </Section>\n </div>\n </div>\n </PageLayout>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"MyLearningPage.js","names":[],"sources":["../../../../src/buildmyiq/pages/learning/MyLearningPage.tsx"],"sourcesContent":["/**\n * MyLearningPage — the caller's personal learning surface.\n * @module buildmyiq/pages/learning\n *\n * Renders the current learner's real enrollments (`buiListEnrollments` filtered\n * by `learnerId`) grouped by progress, their assessment results\n * (`buiListSubmissions`), and the credentials they have earned\n * (`buiListCertificates`). Course titles + covers are resolved from\n * `buiListCourses` (id → course map, falling back to \"Course\"). Every list\n * carries an honest loading / error / empty state — no fabricated rows.\n *\n * Also hosts the \"what should I learn next\" suggestion (`buiSuggestNextLesson`)\n * for the learner's most recently active course — see NextLessonSuggestion.\n */\n\nimport type { FC } from 'react';\nimport { useMemo, useState } from 'react';\nimport {\n GraduationCap,\n Award,\n ArrowRight,\n PlayCircle,\n ClipboardCheck,\n CheckCircle2,\n XCircle,\n Users,\n Calendar,\n TrendingUp,\n Compass,\n Gauge,\n} from 'lucide-react';\nimport { Button, GlassCard } from '@burdenoff/fe-libs/ui';\nimport {\n PageLayout,\n Tabs,\n StatusBadge,\n MiniBar,\n EmptyState,\n LoadingView,\n ErrorView,\n Section,\n FileLink,\n DataTable,\n ProgressRing,\n Sparkline,\n} from '../../components';\nimport type { TabItem, Column } from '../../components';\nimport { cn } from '../../utils/cn';\nimport { useBuildmyiq } from '../../context';\nimport {\n useBuiMyEnrollments,\n useBuiCourses,\n useBuiMyCertificates,\n useBuiMySubmissions,\n useBuiSuggestNextLesson,\n useBuiReviewNextLessonSuggestion,\n} from '../../hooks';\nimport { useTr } from '../../shared/hooks/useTr';\nimport { formatRelativeTime, formatDate, formatDateTime } from '../../utils/formatters';\nimport { dueLabel } from '../automations/automationModel';\nimport type {\n BuiEnrollment,\n BuiCourse,\n BuiCertificate,\n BuiSubmission,\n BuiNextLessonSuggestion,\n BuiPacingHint,\n} from '../../types';\n\ntype LearningTab = 'in_progress' | 'completed' | 'all';\n\nconst IN_PROGRESS_STATUSES = new Set(['ENROLLED', 'IN_PROGRESS']);\n\n/**\n * A next-lesson suggestion is worth a second look below this confidence — the\n * same \"low confidence makes the review obligation explicit\" convention the AI\n * grading panel uses (see pages/submissions/aiGrading.ts).\n */\nconst LOW_SUGGESTION_CONFIDENCE = 0.7;\n\n/**\n * \"What should I learn next\" for the learner's most recently active course\n * (BOFF-4434). `buiSuggestNextLesson` is a deterministic curriculum-order walk\n * plus a pacing hint derived from their recent graded scores — it persists an\n * auditable record but NEVER advances progress on its own, so the learner has\n * to act on it. Asking is an explicit click (it is a mutation), mirroring the\n * educator-side AI grading panel rather than firing on every page view.\n */\nconst NextLessonSuggestion: FC<{\n enrollment: BuiEnrollment;\n courseTitle: string;\n onOpenLesson: (enrollmentId: string, lessonId: string) => void;\n}> = ({ enrollment, courseTitle, onOpenLesson }) => {\n const tr = useTr();\n const suggestNext = useBuiSuggestNextLesson();\n const reviewSuggestion = useBuiReviewNextLessonSuggestion();\n const [suggestion, setSuggestion] = useState<BuiNextLessonSuggestion | null>(null);\n const [dismissed, setDismissed] = useState(false);\n\n const PACING_LABEL: Record<BuiPacingHint, string> = {\n SLOW_DOWN: tr('buildmyiq.learning.pacing.slowDown', 'Slow down'),\n STEADY: tr('buildmyiq.learning.pacing.steady', 'Steady'),\n ACCELERATE: tr('buildmyiq.learning.pacing.accelerate', 'Speed up'),\n };\n\n const handleSuggest = async (): Promise<void> => {\n setDismissed(false);\n try {\n setSuggestion(await suggestNext.mutateAsync(enrollment.id));\n } catch {\n // Surfaced below via suggestNext.isError / suggestNext.error.\n }\n };\n\n const handleOpen = (): void => {\n if (!suggestion) return;\n reviewSuggestion.mutate({ id: suggestion.id, outcome: 'ACCEPTED' });\n onOpenLesson(suggestion.enrollmentId, suggestion.lessonId);\n };\n\n const handleDismiss = (): void => {\n if (suggestion) reviewSuggestion.mutate({ id: suggestion.id, outcome: 'REJECTED' });\n setSuggestion(null);\n setDismissed(true);\n };\n\n if (dismissed && !suggestion) return null;\n\n if (!suggestion) {\n return (\n <GlassCard\n treatment=\"plain\"\n className=\"flex flex-wrap items-center gap-x-4 gap-y-3 border-dashed p-4\"\n >\n <span className=\"flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-[var(--color-accent-soft)]\">\n <Compass className=\"h-5 w-5 text-[var(--color-accent-teal)]\" />\n </span>\n <div className=\"min-w-0 flex-1\">\n <p className=\"text-sm font-semibold text-text-primary\">\n {tr('buildmyiq.learning.next.prompt', 'Not sure what to pick up next?')}\n </p>\n <p className=\"text-xs text-text-secondary\">\n {tr(\n 'buildmyiq.learning.next.promptHelp',\n 'Get the next lesson in {{course}}, paced to your recent scores.',\n { course: courseTitle }\n )}\n </p>\n {suggestNext.isError && (\n <p className=\"mt-1.5 text-xs text-status-error-text\">\n {suggestNext.error instanceof Error\n ? suggestNext.error.message\n : tr('buildmyiq.learning.next.error', \"Couldn't work out a next lesson right now.\")}\n </p>\n )}\n </div>\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={handleSuggest}\n disabled={suggestNext.isPending}\n >\n {suggestNext.isPending\n ? tr('buildmyiq.learning.next.asking', 'Working it out…')\n : tr('buildmyiq.learning.next.ask', 'Suggest my next lesson')}\n </Button>\n </GlassCard>\n );\n }\n\n const lowConfidence = suggestion.confidence < LOW_SUGGESTION_CONFIDENCE;\n\n return (\n <GlassCard treatment=\"glass\" glow className=\"flex flex-col gap-3 p-5\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <span className=\"flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-[var(--color-accent-soft)]\">\n <Compass className=\"h-4 w-4 text-[var(--color-accent-teal)]\" />\n </span>\n <span className=\"rounded-full border border-[var(--color-accent-teal)] px-2.5 py-0.5 text-xs font-medium text-[var(--color-accent-teal)]\">\n {tr('buildmyiq.learning.next.badge', 'Suggestion')}\n </span>\n <span className=\"truncate text-xs text-text-tertiary\">{courseTitle}</span>\n </div>\n\n <div className=\"min-w-0\">\n <p className=\"text-lg font-semibold text-text-primary\">\n {suggestion.lessonTitle}\n {suggestion.moduleTitle ? (\n <span className=\"font-normal text-text-secondary\"> · {suggestion.moduleTitle}</span>\n ) : null}\n </p>\n <p className=\"mt-1 text-sm text-text-secondary\">{suggestion.rationale}</p>\n </div>\n\n <div className=\"flex flex-wrap items-center gap-x-3 gap-y-2 text-xs text-text-secondary\">\n <span className=\"inline-flex items-center gap-1.5 rounded-full bg-bg-sunken px-2.5 py-1 font-medium text-text-primary\">\n <Gauge className=\"h-3.5 w-3.5\" />\n {PACING_LABEL[suggestion.pacingHint]}\n </span>\n <span\n className={\n lowConfidence ? 'font-semibold text-status-warning-text' : 'text-text-tertiary'\n }\n >\n {tr('buildmyiq.learning.next.confidence', '{{pct}}% confidence', {\n pct: Math.round(suggestion.confidence * 100),\n })}\n </span>\n </div>\n\n {lowConfidence && (\n <p className=\"rounded-lg border border-status-warning-border bg-status-warning-bg-subtle p-2 text-xs text-status-warning-text\">\n {tr(\n 'buildmyiq.learning.next.lowConfidence',\n \"There isn't much recent activity to go on — treat this as a starting point, not a verdict.\"\n )}\n </p>\n )}\n\n <div className=\"flex flex-wrap items-center gap-2\">\n <Button size=\"sm\" onClick={handleOpen}>\n {tr('buildmyiq.learning.next.open', 'Open lesson')}\n <ArrowRight className=\"ms-1 h-4 w-4 rtl:-scale-x-100\" />\n </Button>\n <Button variant=\"ghost\" size=\"sm\" onClick={handleDismiss}>\n {tr('buildmyiq.learning.next.dismiss', 'Not now')}\n </Button>\n <span className=\"ms-auto text-xs text-text-tertiary\">\n {tr(\n 'buildmyiq.learning.next.noWriteNote',\n 'This suggestion never changes your progress on its own.'\n )}\n </span>\n </div>\n </GlassCard>\n );\n};\n\nconst scoreClass = (s: BuiSubmission): string =>\n s.passed == null\n ? 'text-text-primary'\n : s.passed\n ? 'text-status-success-text'\n : 'text-status-error-text';\n\nexport const MyLearningPage: FC = () => {\n const { currentUser, navigateTo } = useBuildmyiq();\n const tr = useTr();\n const learnerId = currentUser?.id;\n\n const [tab, setTab] = useState<LearningTab>('in_progress');\n\n // Self-scoped `buiMy*` reads — the learner's own records, identity from the\n // authenticated actor (FERPA/IDOR hardening removed the learnerId arg).\n const enrollmentsQuery = useBuiMyEnrollments({\n limit: 100,\n enabled: Boolean(learnerId),\n });\n const coursesQuery = useBuiCourses({ limit: 100 });\n const certsQuery = useBuiMyCertificates(undefined, Boolean(learnerId));\n const submissionsQuery = useBuiMySubmissions({\n limit: 50,\n enabled: Boolean(learnerId),\n });\n const submissions = useMemo(\n () =>\n [...(submissionsQuery.data?.items ?? [])].sort((a, b) =>\n (b.submittedAt ?? b.createdAt).localeCompare(a.submittedAt ?? a.createdAt)\n ),\n [submissionsQuery.data]\n );\n\n const enrollments: BuiEnrollment[] = useMemo(\n () => enrollmentsQuery.data?.items ?? [],\n [enrollmentsQuery.data]\n );\n const courses: BuiCourse[] = useMemo(() => coursesQuery.data?.items ?? [], [coursesQuery.data]);\n const certificates: BuiCertificate[] = useMemo(() => certsQuery.data ?? [], [certsQuery.data]);\n\n const courseById = useMemo(() => {\n const map = new Map<string, BuiCourse>();\n for (const c of courses) map.set(c.id, c);\n return map;\n }, [courses]);\n\n const enrollmentById = useMemo(() => {\n const map = new Map<string, BuiEnrollment>();\n for (const e of enrollments) map.set(e.id, e);\n return map;\n }, [enrollments]);\n\n // Own keys for both fallbacks: the shared `common.title` and\n // `buildmyiq.assessments.title` keys translate to the column header \"Title\"\n // and the page heading \"Assessments\", which is what an archived course's\n // enrollment and an unlinked submission used to be labelled with.\n const titleFor = (courseId: string): string =>\n courseById.get(courseId)?.title ??\n tr('buildmyiq.learning.unavailableCourse', 'Course no longer available');\n\n /** Resolve a submission's course via its enrollment (falls back honestly). */\n const courseTitleForSubmission = (s: BuiSubmission): string => {\n const enr = s.enrollmentId ? enrollmentById.get(s.enrollmentId) : undefined;\n return enr ? titleFor(enr.courseId) : tr('buildmyiq.learning.unlinkedAssessment', 'Assessment');\n };\n\n const inProgress = useMemo(\n () => enrollments.filter((e) => IN_PROGRESS_STATUSES.has(e.status)),\n [enrollments]\n );\n const completed = useMemo(\n () => enrollments.filter((e) => e.status === 'COMPLETED'),\n [enrollments]\n );\n\n // The course the learner touched most recently — the one a \"what's next\"\n // suggestion is actually about.\n const mostRecentlyActive = useMemo<BuiEnrollment | null>(() => {\n if (inProgress.length === 0) return null;\n return [...inProgress].sort((a, b) =>\n (b.lastActiveAt ?? b.enrolledAt).localeCompare(a.lastActiveAt ?? a.enrolledAt)\n )[0];\n }, [inProgress]);\n\n const visible = useMemo(() => {\n const rows = tab === 'in_progress' ? inProgress : tab === 'completed' ? completed : enrollments;\n return [...rows].sort((a, b) =>\n (b.lastActiveAt ?? b.enrolledAt).localeCompare(a.lastActiveAt ?? a.enrolledAt)\n );\n }, [tab, inProgress, completed, enrollments]);\n\n const sortedCertificates = useMemo(\n () => [...certificates].sort((a, b) => b.issuedAt.localeCompare(a.issuedAt)),\n [certificates]\n );\n\n // Derived score summary (real graded submissions only) — powers the trend strip.\n const graded = useMemo(() => submissions.filter((s) => s.score != null), [submissions]);\n const scoreTrend = useMemo(() => [...graded].reverse().map((s) => s.score as number), [graded]);\n const avgScore =\n graded.length > 0\n ? Math.round(graded.reduce((sum, s) => sum + (s.score ?? 0), 0) / graded.length)\n : null;\n const passedCount = useMemo(\n () => submissions.filter((s) => s.passed === true).length,\n [submissions]\n );\n\n const tabs: TabItem<LearningTab>[] = [\n {\n value: 'in_progress',\n label: tr('buildmyiq.learning.tabInProgress', 'In progress'),\n count: inProgress.length,\n },\n {\n value: 'completed',\n label: tr('buildmyiq.learning.tabCompleted', 'Completed'),\n count: completed.length,\n },\n { value: 'all', label: tr('buildmyiq.learning.tabAll', 'All'), count: enrollments.length },\n ];\n\n const isLoading = Boolean(learnerId) && (enrollmentsQuery.isLoading || coursesQuery.isLoading);\n\n const renderEnrollments = () => {\n if (!learnerId) {\n return (\n <EmptyState\n illustration=\"empty-people\"\n title={tr('buildmyiq.learning.signInTitle', 'Sign in to see your learning')}\n description={tr(\n 'buildmyiq.learning.signInDescription',\n 'Your enrollments and progress appear here once you are signed in to the workspace.'\n )}\n />\n );\n }\n if (isLoading)\n return (\n <LoadingView\n label={tr('buildmyiq.learning.loadingEnrollments', 'Loading your enrollments…')}\n />\n );\n if (enrollmentsQuery.isError) {\n return (\n <ErrorView\n title={tr('buildmyiq.learning.errorTitle', \"Couldn't load your learning\")}\n message={\n enrollmentsQuery.error instanceof Error ? enrollmentsQuery.error.message : undefined\n }\n onRetry={() => enrollmentsQuery.refetch()}\n />\n );\n }\n if (enrollments.length === 0) {\n return (\n <EmptyState\n illustration=\"onboarding\"\n title={tr('buildmyiq.learning.notEnrolledTitle', \"You're not enrolled in anything yet\")}\n description={tr(\n 'buildmyiq.learning.notEnrolledDescription',\n 'Browse the catalog and enrol in a course to start learning.'\n )}\n action={\n <Button onClick={() => navigateTo('catalog')}>\n {tr('buildmyiq.learning.exploreCatalog', 'Explore the catalog')}\n </Button>\n }\n />\n );\n }\n if (visible.length === 0) {\n const label =\n tab === 'in_progress'\n ? tr('buildmyiq.learning.noInProgressTitle', 'No courses in progress')\n : tab === 'completed'\n ? tr('buildmyiq.learning.noCompletedTitle', 'No completed courses yet')\n : tr('buildmyiq.learning.noCoursesTitle', 'No courses');\n return (\n <EmptyState\n illustration=\"empty-data\"\n title={label}\n description={\n tab === 'completed'\n ? tr(\n 'buildmyiq.learning.noCompletedDescription',\n 'Finish a course to see it here — and earn a certificate.'\n )\n : tr(\n 'buildmyiq.learning.noFilteredDescription',\n 'Switch tabs or enrol in a new course to keep learning.'\n )\n }\n />\n );\n }\n return (\n <div\n className=\"grid grid-cols-1 gap-4 xl:grid-cols-2\"\n data-tour=\"buildmyiq-learning-enrollment-grid\"\n >\n {visible.map((e) => {\n const course = courseById.get(e.courseId);\n const cover = course?.cover?.downloadUrl ?? null;\n const isCompleted = e.status === 'COMPLETED';\n const pct = Math.round(e.progressPct ?? 0);\n const due = dueLabel(e.dueAt, e.status);\n return (\n <GlassCard\n key={e.id}\n treatment=\"plain\"\n className=\"group flex flex-col overflow-hidden transition-shadow hover:shadow-lg sm:flex-row\"\n >\n {/* Media tile — real cover when available, soft accent fallback; play\n affordance + a live progress ring badge. */}\n <div className=\"relative aspect-video w-full shrink-0 overflow-hidden bg-[var(--color-accent-soft)] sm:aspect-auto sm:w-44\">\n {cover ? (\n <img\n src={cover}\n alt=\"\"\n className=\"h-full w-full object-cover transition-transform duration-500 group-hover:scale-105\"\n />\n ) : null}\n <div className=\"absolute inset-0 flex items-center justify-center\">\n <span className=\"flex h-11 w-11 items-center justify-center rounded-full bg-bg-surface/85 text-text-primary shadow-sm backdrop-blur transition-transform group-hover:scale-110\">\n <PlayCircle className=\"h-6 w-6\" />\n </span>\n </div>\n <div className=\"absolute bottom-2 start-2 rounded-full bg-bg-surface/85 p-0.5 shadow-sm backdrop-blur\">\n <ProgressRing\n value={pct}\n size={38}\n stroke={5}\n color={\n isCompleted ? 'var(--color-status-success-text)' : 'var(--color-accent-teal)'\n }\n />\n </div>\n </div>\n\n {/* Content */}\n <div className=\"flex min-w-0 flex-1 flex-col gap-2 p-4\">\n <div className=\"flex flex-wrap items-start gap-2\">\n <p className=\"min-w-0 flex-1 truncate font-semibold text-text-primary\">\n {titleFor(e.courseId)}\n </p>\n <StatusBadge status={e.status} />\n </div>\n\n {(course?.level || course?.skill?.name || e.cohort?.name) && (\n <div className=\"flex flex-wrap items-center gap-2 text-xs text-text-secondary\">\n {course?.level && <StatusBadge status={course.level} />}\n {course?.skill?.name && (\n <span className=\"rounded-full bg-bg-sunken px-2 py-0.5\">\n {course.skill.name}\n </span>\n )}\n {e.cohort?.name && (\n <span className=\"inline-flex items-center gap-1\">\n <Users className=\"h-3.5 w-3.5\" /> {e.cohort.name}\n </span>\n )}\n </div>\n )}\n\n <div className=\"mt-1 flex items-center gap-2\">\n <MiniBar\n value={e.progressPct}\n barClassName={\n isCompleted\n ? 'bg-[var(--color-status-success-text)]'\n : 'bg-[var(--color-accent-primary)]'\n }\n className=\"flex-1\"\n />\n <span className=\"shrink-0 text-xs font-semibold tabular-nums text-text-secondary\">\n {pct}%\n </span>\n </div>\n\n {due && (\n <p\n className={\n due.overdue\n ? 'text-xs font-medium text-status-warning-text'\n : 'text-xs text-text-secondary'\n }\n >\n {due.text}\n </p>\n )}\n <p className=\"text-xs text-text-secondary\">\n {isCompleted && e.completedAt\n ? tr('common.completed', 'Completed {{time}}', {\n time: formatRelativeTime(e.completedAt),\n })\n : tr('common.active', 'Active {{time}}', {\n time: formatRelativeTime(e.lastActiveAt ?? e.enrolledAt),\n })}\n </p>\n\n <div className=\"mt-auto flex flex-wrap gap-2 pt-2\">\n <Button\n size=\"sm\"\n className=\"max-sm:flex-1\"\n onClick={() => navigateTo(`learning/${e.id}`)}\n >\n {isCompleted\n ? tr('common.review', 'Review')\n : tr('common.continue', 'Continue')}\n <ArrowRight className=\"ms-1 h-4 w-4 rtl:-scale-x-100\" />\n </Button>\n <Button\n variant=\"secondary\"\n size=\"sm\"\n className=\"max-sm:flex-1\"\n onClick={() => navigateTo(`catalog/${e.courseId}`)}\n >\n {tr('common.overview', 'Overview')}\n </Button>\n </div>\n </div>\n </GlassCard>\n );\n })}\n </div>\n );\n };\n\n const resultColumns: Column<BuiSubmission>[] = [\n {\n key: 'assessment',\n header: tr('buildmyiq.learning.columnAssessment', 'Assessment'),\n render: (s) => (\n <div className=\"flex min-w-0 items-center gap-3\">\n <span className=\"flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-[var(--color-accent-soft)]\">\n <ClipboardCheck className=\"h-4 w-4 text-text-primary\" />\n </span>\n <div className=\"min-w-0\">\n <p className=\"truncate font-medium text-text-primary\">{courseTitleForSubmission(s)}</p>\n <p className=\"text-xs text-text-secondary\">\n {tr('buildmyiq.learning.attempt', 'Attempt {{n}}', { n: s.attempt })}\n </p>\n </div>\n </div>\n ),\n },\n {\n key: 'status',\n header: tr('buildmyiq.learning.columnStatus', 'Status'),\n render: (s) => <StatusBadge status={s.status} />,\n },\n {\n key: 'result',\n header: tr('buildmyiq.learning.columnResult', 'Result'),\n render: (s) =>\n s.passed == null ? (\n <span className=\"text-text-tertiary\">—</span>\n ) : (\n <span\n className={cn(\n 'inline-flex items-center gap-1.5 text-sm font-medium',\n s.passed ? 'text-status-success-text' : 'text-status-error-text'\n )}\n >\n {s.passed ? <CheckCircle2 className=\"h-4 w-4\" /> : <XCircle className=\"h-4 w-4\" />}\n {s.passed\n ? tr('buildmyiq.learning.passed', 'Passed')\n : tr('buildmyiq.learning.notPassed', 'Not passed')}\n </span>\n ),\n },\n {\n key: 'feedback',\n header: tr('buildmyiq.learning.columnFeedback', 'Feedback'),\n hideOnMobile: true,\n render: (s) =>\n s.feedback ? (\n <p className=\"line-clamp-2 max-w-[18rem] text-sm text-text-secondary\">{s.feedback}</p>\n ) : (\n <span className=\"text-text-tertiary\">—</span>\n ),\n },\n {\n key: 'submitted',\n header: tr('buildmyiq.learning.columnSubmitted', 'Submitted'),\n hideOnMobile: true,\n sortValue: (s) => s.submittedAt ?? s.createdAt,\n render: (s) => (\n <span className=\"whitespace-nowrap text-sm text-text-secondary\">\n {formatDateTime(s.submittedAt ?? s.createdAt)}\n </span>\n ),\n },\n {\n key: 'score',\n header: tr('buildmyiq.learning.columnScore', 'Score'),\n align: 'right',\n sortValue: (s) => s.score ?? -1,\n render: (s) => (\n <span className={cn('text-lg font-bold tabular-nums', scoreClass(s))}>\n {s.score != null ? `${s.score}%` : '—'}\n </span>\n ),\n },\n ];\n\n const renderResults = () => {\n if (!learnerId) return null;\n if (submissionsQuery.isLoading)\n return (\n <LoadingView label={tr('buildmyiq.learning.loadingResults', 'Loading your results…')} />\n );\n if (submissionsQuery.isError)\n return (\n <ErrorView\n title={tr('buildmyiq.learning.resultsErrorTitle', \"Couldn't load your results\")}\n message={\n submissionsQuery.error instanceof Error ? submissionsQuery.error.message : undefined\n }\n onRetry={() => submissionsQuery.refetch()}\n />\n );\n if (submissions.length === 0)\n return (\n <EmptyState\n illustration=\"empty-data\"\n title={tr('buildmyiq.learning.noResultsTitle', 'No assessment results yet')}\n description={tr(\n 'buildmyiq.learning.noResultsDescription',\n 'Take a course assessment and your score and feedback will appear here.'\n )}\n />\n );\n return (\n <div className=\"flex flex-col gap-3\">\n {avgScore != null && (\n <GlassCard\n treatment=\"metric\"\n className=\"flex flex-wrap items-center justify-between gap-4 p-4\"\n >\n <div className=\"flex items-center gap-3\">\n <ProgressRing value={avgScore} size={54} />\n <div>\n <p className=\"text-sm font-semibold text-text-primary\">\n {tr('buildmyiq.learning.averageScore', 'Average score')}\n </p>\n <p className=\"text-xs text-text-secondary\">\n {tr(\n 'buildmyiq.learning.passedSummary',\n '{{passed}} of {{graded}} passed · {{graded}} graded',\n {\n passed: passedCount,\n graded: graded.length,\n }\n )}\n </p>\n </div>\n </div>\n {scoreTrend.length >= 2 && (\n <div className=\"flex items-center gap-2 text-xs text-text-secondary\">\n <TrendingUp className=\"h-4 w-4 text-status-success-text\" />\n <Sparkline data={scoreTrend} width={128} height={36} />\n <span>{tr('buildmyiq.learning.scoreTrend', 'score trend')}</span>\n </div>\n )}\n </GlassCard>\n )}\n <DataTable\n columns={resultColumns}\n rows={submissions}\n rowKey={(s) => s.id}\n defaultSort={{ key: 'submitted', dir: 'desc' }}\n />\n </div>\n );\n };\n\n const renderCertificates = () => {\n if (!learnerId) return null;\n if (certsQuery.isLoading)\n return (\n <LoadingView\n label={tr('buildmyiq.learning.loadingCertificates', 'Loading your certificates…')}\n />\n );\n if (certsQuery.isError) {\n return (\n <ErrorView\n title={tr('buildmyiq.learning.certificatesErrorTitle', \"Couldn't load your certificates\")}\n message={certsQuery.error instanceof Error ? certsQuery.error.message : undefined}\n onRetry={() => certsQuery.refetch()}\n />\n );\n }\n if (sortedCertificates.length === 0) {\n return (\n <EmptyState\n illustration=\"empty-data\"\n title={tr('buildmyiq.learning.noCertificatesTitle', 'No certificates yet')}\n description={tr(\n 'buildmyiq.learning.noCertificatesDescription',\n 'Complete a credentialed course to earn a verifiable certificate.'\n )}\n />\n );\n }\n return (\n <div className=\"grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3\">\n {sortedCertificates.map((cert) => (\n <GlassCard\n key={cert.id}\n treatment=\"plain\"\n className=\"flex flex-col gap-3 p-4 transition-shadow hover:shadow-lg\"\n >\n <div className=\"flex items-start justify-between gap-3\">\n <div className=\"flex h-12 w-12 shrink-0 items-center justify-center rounded-xl bg-[color-mix(in_srgb,var(--color-status-warning-text)_16%,transparent)]\">\n <Award className=\"h-6 w-6 text-status-warning-text\" />\n </div>\n <StatusBadge status={cert.status} />\n </div>\n <div className=\"min-w-0\">\n <p className=\"truncate font-semibold text-text-primary\">{titleFor(cert.courseId)}</p>\n <div className=\"mt-1 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-text-secondary\">\n <span className=\"font-mono\">{cert.serial}</span>\n {cert.finalScore != null && (\n <span className=\"inline-flex items-center gap-1 text-status-success-text\">\n <CheckCircle2 className=\"h-3.5 w-3.5\" /> {cert.finalScore}%\n </span>\n )}\n </div>\n </div>\n <div className=\"mt-auto flex flex-wrap items-center justify-between gap-2 border-t border-border-seam/60 pt-3 text-xs text-text-secondary\">\n <span className=\"inline-flex items-center gap-1.5\">\n <Calendar className=\"h-3.5 w-3.5\" />\n {tr('buildmyiq.learning.issued', 'Issued {{date}}', {\n date: formatDate(cert.issuedAt),\n })}\n </span>\n {cert.file ? <FileLink fileId={cert.file.id} /> : null}\n </div>\n </GlassCard>\n ))}\n </div>\n );\n };\n\n return (\n <PageLayout\n title={tr('buildmyiq.learning.title', 'My Learning')}\n description={tr(\n 'buildmyiq.learning.description',\n \"Your enrollments, progress and the credentials you've earned.\"\n )}\n titleGradient\n aurora\n >\n <div className=\"flex flex-col gap-6\" data-tour=\"buildmyiq-learning-content\">\n {mostRecentlyActive && (\n <NextLessonSuggestion\n key={mostRecentlyActive.id}\n enrollment={mostRecentlyActive}\n courseTitle={titleFor(mostRecentlyActive.courseId)}\n onOpenLesson={(enrollmentId, lessonId) =>\n navigateTo(`learning/${enrollmentId}?lesson=${encodeURIComponent(lessonId)}`)\n }\n />\n )}\n\n <div data-tour=\"buildmyiq-learning-courses\">\n <Section\n title={tr('buildmyiq.learning.sectionCourses', 'Courses')}\n description={tr(\n 'buildmyiq.learning.sectionCoursesDescription',\n \"Everything you're enrolled in.\"\n )}\n >\n {learnerId && enrollments.length > 0 && (\n <div className=\"mb-4\" data-tour=\"buildmyiq-learning-tabs\">\n <Tabs items={tabs} value={tab} onChange={setTab} />\n </div>\n )}\n {renderEnrollments()}\n </Section>\n </div>\n\n <div data-tour=\"buildmyiq-learning-results\">\n <Section\n title={tr('buildmyiq.learning.sectionAssessments', 'Assessment results')}\n description={tr(\n 'buildmyiq.learning.sectionAssessmentsDescription',\n 'Your submitted assessments, scores and educator feedback.'\n )}\n >\n {renderResults()}\n </Section>\n </div>\n\n <div data-tour=\"buildmyiq-learning-certificates\">\n <Section\n title={tr('buildmyiq.learning.sectionCertificates', 'Certificates')}\n description={tr(\n 'buildmyiq.learning.sectionCertificatesDescription',\n \"Verifiable credentials issued for the courses you've completed.\"\n )}\n action={\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={() => navigateTo('certificates')}\n data-tour=\"buildmyiq-learning-all-certificates\"\n >\n <GraduationCap className=\"me-1 h-4 w-4\" />{' '}\n {tr('buildmyiq.learning.allCertificates', 'All certificates')}\n </Button>\n }\n >\n {renderCertificates()}\n </Section>\n </div>\n </div>\n </PageLayout>\n );\n};\n"],"mappings":"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"}
|