@burdenoff/microfe-buildmyiq 2026.1030.5 → 2026.1030.6
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.
|
@@ -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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"}
|