@burdenoff/microfe-technospam 2026.1101.1 → 2026.1101.2

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.
Files changed (27) hide show
  1. package/dist/stories/journeys/Problems.stories.d.ts.map +1 -1
  2. package/dist/technospam/api/technospamApi.d.ts +15 -0
  3. package/dist/technospam/api/technospamApi.d.ts.map +1 -1
  4. package/dist/technospam/api/technospamApi.js +200 -151
  5. package/dist/technospam/api/technospamApi.js.map +1 -1
  6. package/dist/technospam/components/ProblemsTable.d.ts +19 -0
  7. package/dist/technospam/components/ProblemsTable.d.ts.map +1 -0
  8. package/dist/technospam/components/ProblemsTable.js +117 -0
  9. package/dist/technospam/components/ProblemsTable.js.map +1 -0
  10. package/dist/technospam/components/index.d.ts +1 -0
  11. package/dist/technospam/components/index.d.ts.map +1 -1
  12. package/dist/technospam/components/index.js +2 -1
  13. package/dist/technospam/pages/ApplicationsProblemDetailsPagePage.js +38 -38
  14. package/dist/technospam/pages/ApplicationsProblemDetailsPagePage.js.map +1 -1
  15. package/dist/technospam/pages/ApplicationsProblemsPagePage.d.ts.map +1 -1
  16. package/dist/technospam/pages/ApplicationsProblemsPagePage.js +197 -144
  17. package/dist/technospam/pages/ApplicationsProblemsPagePage.js.map +1 -1
  18. package/dist/technospam/pages/CoursesCourseDetailsPagePage.js +195 -195
  19. package/dist/technospam/pages/CoursesCourseDetailsPagePage.js.map +1 -1
  20. package/dist/technospam/pages/CoursesCoursesPagePage.js +26 -26
  21. package/dist/technospam/pages/CoursesLessonPlayerPagePage.js +36 -36
  22. package/dist/technospam/pages/CoursesLessonPlayerPagePage.js.map +1 -1
  23. package/dist/technospam/utils/problemCatalog.d.ts +27 -0
  24. package/dist/technospam/utils/problemCatalog.d.ts.map +1 -0
  25. package/dist/technospam/utils/problemCatalog.js +68 -0
  26. package/dist/technospam/utils/problemCatalog.js.map +1 -0
  27. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"CoursesCourseDetailsPagePage.js","names":[],"sources":["../../../src/technospam/pages/CoursesCourseDetailsPagePage.tsx"],"sourcesContent":["import type { ReactNode } from 'react';\nimport { useEffect } from 'react';\nimport { useSearchParams } from 'react-router';\nimport {\n ArrowRight,\n Award,\n BookOpen,\n CheckCircle2,\n ChevronDown,\n Clock,\n Eye,\n FileText,\n Lock,\n PlayCircle,\n Layers,\n ShieldCheck,\n Sparkles,\n Trophy,\n TrendingUp,\n Users,\n type LucideIcon,\n} from 'lucide-react';\nimport { Button, GlassCard, Skeleton } from '@burdenoff/fe-libs/ui';\nimport {\n EmptyView,\n ErrorView,\n MediaCover,\n PageLayout,\n PremiumBadge,\n StatCard,\n TagStrip,\n UpgradeGate,\n topicIcon,\n} from '../components';\nimport { useTechnoSpam, useTechnoSpamNavigate } from '../providers/TechnoSpamProvider';\nimport {\n useCourseQuery,\n useEnroll,\n useIssueCertificate,\n useMyCertificatesQuery,\n type TecCertificate,\n type TecLesson,\n type TecEnrollment,\n} from '../api/technospamApi';\nimport { formatDate, formatDuration, humanizeEnum } from '../utils/format';\nimport { notify } from '../lib/toast';\nimport { cn } from '../utils/cn';\nimport { useTechnoSpamAnalytics } from '../hooks/useTechnoSpamAnalytics';\nimport { useFocusedRecordParam } from '../hooks/useFocusedRecordParam';\nimport { FOCUS_PARAMS } from '../utils/pageContext';\nimport { lessonPlayerPath } from '../utils/lessonPath';\n\n/** Per-lesson-kind icon + on-theme tint (chip) and pill (badge) classes. */\nconst KIND_META: Record<string, { icon: LucideIcon; chip: string; badge: string }> = {\n VIDEO: {\n icon: PlayCircle,\n chip: 'bg-accent-primary/15 text-accent-primary',\n badge: 'border-accent-primary/25 bg-accent-primary/6 text-accent-primary',\n },\n ARTICLE: {\n icon: FileText,\n chip: 'bg-accent-indigo/15 text-accent-indigo',\n badge: 'border-accent-indigo/25 bg-accent-indigo/10 text-accent-indigo',\n },\n INTERACTIVE: {\n icon: Sparkles,\n chip: 'bg-accent-teal/15 text-accent-teal',\n badge: 'border-accent-teal/25 bg-accent-teal/10 text-accent-teal',\n },\n QUIZ: {\n icon: CheckCircle2,\n chip: 'bg-accent-purple/15 text-accent-purple',\n badge: 'border-accent-purple/25 bg-accent-purple/10 text-accent-purple',\n },\n PROJECT: {\n icon: Layers,\n chip: 'bg-status-warning-bg-subtle text-status-warning-text',\n badge: 'border-status-warning-border bg-status-warning-bg-subtle text-status-warning-text',\n },\n};\n\nfunction HeroBadge({ icon: Icon, children }: { icon?: LucideIcon; children: ReactNode }) {\n return (\n <span className=\"inline-flex items-center gap-1 rounded-full bg-white/15 px-2.5 py-1 text-xs font-medium text-white ring-1 ring-white/25 backdrop-blur-sm\">\n {Icon ? <Icon size={12} /> : null}\n {children}\n </span>\n );\n}\n\n/**\n * One curriculum row. Expands in place to reveal the lesson body.\n *\n * `TecLesson.content` is the only lesson body this service exposes to a\n * learner, and the API nulls it server-side whenever `isLocked` is true\n * (BOFF-4416), so the three states below are exhaustive and never guessed:\n * locked → upgrade note, content present → the body, content null on an\n * unlocked lesson → the author simply hasn't written it yet.\n */\nfunction LessonRow({\n lesson,\n index,\n done,\n open,\n onToggle,\n onStart,\n}: {\n lesson: TecLesson;\n index: number;\n done: boolean;\n /** The open lesson is the focused one (`?lessonId=`), so the assistant can name it. */\n open: boolean;\n onToggle: () => void;\n /** Opens the lesson in the lesson player (BOFF-4107). */\n onStart: () => void;\n}) {\n const meta = KIND_META[lesson.kind] ?? KIND_META.ARTICLE!;\n const Icon = meta.icon;\n const bodyId = `tec-lesson-body-${lesson.id}`;\n\n return (\n <div className=\"border-b border-border-seam last:border-b-0\">\n <button\n type=\"button\"\n onClick={onToggle}\n aria-expanded={open}\n aria-controls={bodyId}\n className=\"group flex w-full items-center gap-3 rounded-xl px-2 py-3 text-left transition-colors hover:bg-bg-sunken/60\"\n >\n <span className=\"hidden w-6 shrink-0 text-center text-sm font-medium tabular-nums text-text-muted sm:block\">\n {index + 1}\n </span>\n <span\n className={cn(\n 'flex h-9 w-9 shrink-0 items-center justify-center rounded-lg',\n done ? 'bg-status-success-bg-subtle text-status-success-text' : meta.chip\n )}\n >\n {done ? <CheckCircle2 size={17} /> : <Icon size={17} />}\n </span>\n <div className=\"min-w-0 flex-1\">\n <span className=\"block truncate text-sm font-medium text-text-primary\">\n {lesson.title}\n </span>\n <div className=\"mt-1 flex flex-wrap items-center gap-2\">\n <span\n className={cn(\n 'inline-flex items-center rounded-full border px-2 py-0.5 text-[11px] font-medium',\n meta.badge\n )}\n >\n {humanizeEnum(lesson.kind)}\n </span>\n <span className=\"inline-flex items-center gap-1 text-xs text-text-muted\">\n <Clock size={12} /> {formatDuration(lesson.estimatedMinutes)}\n </span>\n </div>\n </div>\n <div className=\"flex shrink-0 items-center gap-2\">\n {done ? (\n <CheckCircle2 size={18} className=\"text-status-success-text\" />\n ) : lesson.isPreview ? (\n <span className=\"inline-flex items-center gap-1 rounded-full border border-accent-primary/30 bg-accent-primary/6 px-2 py-0.5 text-[11px] font-medium text-accent-primary\">\n <Eye size={12} /> Preview\n </span>\n ) : lesson.isLocked ? (\n // BOFF-4416: distinct from the plain \"not completed yet\" Lock icon\n // below — this lesson's content is gated behind a paid plan, not\n // just not-yet-watched. The API already nulled `content`/`file`.\n <span className=\"inline-flex items-center gap-1 rounded-full border border-status-warning-border/60 bg-status-warning-bg-subtle px-2 py-0.5 text-[11px] font-medium text-status-warning-text\">\n <Lock size={12} /> Upgrade to unlock\n </span>\n ) : (\n <Lock size={15} className=\"text-text-muted\" />\n )}\n <ChevronDown\n size={16}\n aria-hidden\n className={cn(\n 'text-text-muted transition-transform duration-200',\n open ? 'rotate-180' : ''\n )}\n />\n </div>\n </button>\n\n {open ? (\n <div id={bodyId} className=\"px-2 pb-4 sm:pl-11\">\n {lesson.isLocked ? (\n <UpgradeGate\n compact\n description=\"This lesson is part of a paid TechnoSpam plan — its content unlocks once your workspace upgrades.\"\n />\n ) : (\n <div className=\"space-y-3\">\n {lesson.content ? (\n <div className=\"rounded-xl border border-border-seam bg-bg-sunken p-4\">\n <p className=\"line-clamp-6 whitespace-pre-wrap text-sm leading-relaxed text-text-secondary\">\n {lesson.content}\n </p>\n </div>\n ) : null}\n <Button size=\"sm\" onClick={onStart} data-testid={`open-lesson-${lesson.id}`}>\n <PlayCircle size={15} /> {done ? 'Review lesson' : 'Start lesson'}\n </Button>\n </div>\n )}\n </div>\n ) : null}\n </div>\n );\n}\n\n/**\n * Verifiable certificate face for an already-issued certificate. Every value\n * shown is a real field on the TecCertificate row (`serial`, `score`,\n * `issuedAt`, `status`) plus the course title it was issued against — the\n * learner name comes from the signed-in actor the shell handed us, and is\n * simply omitted when the shell did not supply one.\n */\nfunction CertificatePreview({\n certificate,\n learnerName,\n courseTitle,\n}: {\n certificate: TecCertificate;\n learnerName?: string;\n courseTitle: string;\n}) {\n const revoked = certificate.status === 'REVOKED';\n return (\n <GlassCard className=\"overflow-hidden p-0\">\n <div className=\"border-b border-border-seam px-5 py-3\">\n <span className=\"text-xs font-semibold uppercase tracking-wide text-text-muted\">\n Certificate preview\n </span>\n </div>\n <div className=\"space-y-4 bg-bg-sunken/50 px-5 py-6 text-center\">\n <div className=\"flex items-center justify-center gap-2 text-xs font-semibold uppercase tracking-[0.2em] text-text-muted\">\n <Award size={14} className=\"text-status-warning-text\" /> Certificate of completion\n </div>\n <h3 className=\"text-lg font-bold text-text-primary\">{courseTitle}</h3>\n {learnerName ? (\n <p className=\"text-sm text-text-secondary\">\n Awarded to <span className=\"font-semibold text-text-primary\">{learnerName}</span>\n </p>\n ) : null}\n <div className=\"grid grid-cols-1 gap-3 border-t border-border-seam pt-4 text-left sm:grid-cols-3\">\n <div>\n <div className=\"text-[11px] font-medium uppercase tracking-wide text-text-muted\">\n Serial number\n </div>\n <div className=\"truncate font-mono text-sm text-text-primary\">{certificate.serial}</div>\n </div>\n <div>\n <div className=\"text-[11px] font-medium uppercase tracking-wide text-text-muted\">\n Score\n </div>\n <div className=\"text-sm font-semibold text-text-primary\">{certificate.score}</div>\n </div>\n <div>\n <div className=\"text-[11px] font-medium uppercase tracking-wide text-text-muted\">\n Issued on\n </div>\n <div className=\"text-sm text-text-primary\">{formatDate(certificate.issuedAt)}</div>\n </div>\n </div>\n {revoked ? (\n <div className=\"rounded-lg border border-status-error-border bg-status-error-bg-subtle px-3 py-2 text-left text-xs text-status-error-text\">\n This certificate was revoked\n {certificate.revokedAt ? ` on ${formatDate(certificate.revokedAt)}` : ''}\n {certificate.revokeReason ? ` — ${certificate.revokeReason}` : '.'}\n </div>\n ) : (\n <span className=\"inline-flex items-center gap-1 rounded-full border border-status-success-border bg-status-success-bg-subtle px-2.5 py-1 text-xs font-medium text-status-success-text\">\n <ShieldCheck size={13} /> Verified\n </span>\n )}\n </div>\n </GlassCard>\n );\n}\n\nexport function CoursesCourseDetailsPagePage() {\n const [params] = useSearchParams();\n const id = params.get('id') ?? '';\n // The expanded lesson lives in `?lessonId=` so the assistant's page context\n // names it (\"explain this lesson\") and a reload reopens it.\n const [openLessonId, setOpenLessonId] = useFocusedRecordParam(FOCUS_PARAMS.lesson);\n const { currentUser, workspaceId } = useTechnoSpam();\n const navigate = useTechnoSpamNavigate();\n const { emit } = useTechnoSpamAnalytics();\n const { data, loading, error, refetch } = useCourseQuery(id);\n\n useEffect(() => {\n if (id) {\n emit('technospam.course.viewed', {\n workspaceId,\n entityId: id,\n entityType: 'TEC_COURSE',\n });\n }\n }, [id, workspaceId, emit]);\n\n const [enroll, { loading: enrolling }] = useEnroll();\n const course = data?.tecCourse ?? null;\n // Self-scoped read (BOFF-10481): the learner's own certificates come from the\n // canonical Buildmyiq field, which filters by the authenticated actor\n // server-side. It is narrowed to this course by its canonical id once known\n // and by its Technospam id on the client. A cross-learner list-all read is\n // not granted to a plain learner, so an already-claimed certificate would\n // come back empty and the page would keep offering \"Claim certificate\".\n const { certificates: myCertificates, refetch: refetchCerts } = useMyCertificatesQuery({\n buiCourseId: course?.buiCourseId,\n technospamCourseId: id,\n skip: !id,\n });\n const [issueCertificate, { loading: claiming }] = useIssueCertificate();\n\n const lessons = [...(course?.lessons ?? [])].sort((a, b) => a.position - b.position);\n const myEnrollment: TecEnrollment | undefined = (course?.enrollments ?? []).find(\n (e) => e.learnerId === currentUser?.id\n );\n const myCertificate = myCertificates.find((c) => c.status === 'ISSUED') ?? myCertificates[0];\n const isComplete = !!myEnrollment && myEnrollment.progressPct >= 100;\n\n const handleClaimCertificate = async () => {\n if (!id || !currentUser?.id) return;\n emit('technospam.course.certificate_claimed', {\n workspaceId,\n entityId: id,\n entityType: 'TEC_COURSE',\n });\n const res = await issueCertificate({\n variables: { courseId: id, learnerId: currentUser.id, score: 100 },\n });\n if (res.data?.tecIssueCertificate) {\n notify.success('Certificate issued', 'Your verifiable certificate is ready in My Learning.');\n await refetchCerts();\n }\n };\n\n const handleEnroll = async () => {\n if (!id) return;\n emit('technospam.course.enroll_clicked', {\n workspaceId,\n entityId: id,\n entityType: 'TEC_COURSE',\n });\n const res = await enroll({ variables: { input: { courseId: id } } });\n if (res.data?.tecEnroll) {\n emit('technospam.course.enrolled', {\n workspaceId,\n entityId: id,\n entityType: 'TEC_COURSE',\n });\n notify.success('Enrolled', `You're enrolled in \"${course?.title}\".`);\n await refetch();\n }\n };\n\n if (loading && !course) {\n return (\n <PageLayout title=\"Course\" backPath=\"/courses/courses-page\">\n <Skeleton className=\"mb-4 h-40 w-full rounded-2xl\" />\n <Skeleton className=\"h-64 w-full rounded-xl\" />\n </PageLayout>\n );\n }\n\n if (error || !course) {\n return (\n <PageLayout title=\"Course\" backPath=\"/courses/courses-page\">\n {error ? (\n <ErrorView title=\"Couldn't load course\" error={error} />\n ) : (\n <EmptyView\n title=\"Course not found\"\n description=\"This course may have been unpublished.\"\n />\n )}\n </PageLayout>\n );\n }\n\n const completedLessons = myEnrollment?.lessonsCompleted ?? 0;\n // BOFF-4107: \"Continue\" lands on the first lesson the learner has not\n // finished (per-lesson completions from the API), or the first lesson to\n // review once everything is done.\n const nextLesson = lessons.find((l) => !l.completedByMe) ?? lessons[0];\n const allDone = lessons.length > 0 && lessons.every((l) => l.completedByMe);\n const openLesson = (lessonId: string) => navigate(lessonPlayerPath(id, lessonId));\n const remaining = Math.max(0, lessons.length - completedLessons);\n const milestone =\n myEnrollment && myEnrollment.progressPct >= 100\n ? 'Course complete — claim your certificate below.'\n : remaining === 1\n ? '1 lesson to go — almost there!'\n : `${remaining} lessons to go — keep it up!`;\n const heroSeed = `${course.topic ?? ''} ${course.title}`;\n\n return (\n <PageLayout\n backPath=\"/courses/courses-page\"\n title={course.title}\n description={course.summary ?? undefined}\n actions={\n myEnrollment ? (\n nextLesson ? (\n <Button onClick={() => openLesson(nextLesson.id)} data-testid=\"continue-learning\">\n {allDone ? <CheckCircle2 size={16} /> : <ArrowRight size={16} />}\n {allDone\n ? 'Review course'\n : completedLessons > 0\n ? 'Continue learning'\n : 'Start course'}\n </Button>\n ) : null\n ) : (\n <Button onClick={handleEnroll} disabled={enrolling}>\n <BookOpen size={16} /> {enrolling ? 'Enrolling…' : 'Enroll'}\n </Button>\n )\n }\n >\n {/* Bold hero banner */}\n <MediaCover\n height=\"h-40\"\n seed={heroSeed}\n icon={topicIcon(course.topic ?? course.title)}\n className=\"mb-5 rounded-2xl\"\n >\n <div className=\"flex w-full flex-col gap-2\">\n <span className=\"text-xs font-medium uppercase tracking-wide text-white/80\">\n {course.topic ?? 'Course'}\n </span>\n <h2 className=\"text-2xl font-bold leading-tight text-white drop-shadow-sm sm:text-3xl\">\n {course.title}\n </h2>\n <div className=\"flex flex-wrap items-center gap-2\">\n <HeroBadge icon={TrendingUp}>{humanizeEnum(course.level)}</HeroBadge>\n <HeroBadge icon={CheckCircle2}>{humanizeEnum(course.status)}</HeroBadge>\n {course.topic ? <HeroBadge icon={Layers}>{course.topic}</HeroBadge> : null}\n {course.isPremiumOnly ? <HeroBadge icon={Lock}>Premium</HeroBadge> : null}\n </div>\n </div>\n </MediaCover>\n\n <TagStrip tags={course.tags} className=\"mb-5\" />\n\n <div className=\"mb-6 grid grid-cols-2 gap-3 sm:grid-cols-4\">\n <StatCard label=\"Lessons\" value={lessons.length} icon={FileText} tone=\"blue\" />\n <StatCard\n label=\"Duration\"\n value={formatDuration(course.estimatedMinutes)}\n icon={Clock}\n tone=\"teal\"\n />\n <StatCard\n label=\"Enrolled\"\n value={course.enrollments?.length ?? 0}\n icon={Users}\n tone=\"indigo\"\n />\n <StatCard\n label=\"Your progress\"\n value={myEnrollment ? `${myEnrollment.progressPct}%` : 'Not started'}\n icon={TrendingUp}\n tone=\"purple\"\n />\n </div>\n\n {myEnrollment ? (\n <GlassCard className=\"mb-6 p-5\">\n <div className=\"mb-3 flex items-center gap-2\">\n <span className=\"flex h-8 w-8 items-center justify-center rounded-lg bg-accent-primary/15 text-accent-primary\">\n <TrendingUp size={16} />\n </span>\n <span className=\"text-sm font-semibold text-text-primary\">Your progress</span>\n <span className=\"ml-auto text-sm text-text-secondary\">\n {completedLessons}/{lessons.length} lessons · {myEnrollment.progressPct}%\n </span>\n </div>\n <div className=\"h-2.5 w-full overflow-hidden rounded-full bg-bg-sunken\">\n <div\n className=\"h-full rounded-full bg-gradient-to-r from-accent-primary via-accent-indigo to-accent-purple transition-all duration-500\"\n style={{ width: `${myEnrollment.progressPct}%` }}\n />\n </div>\n <p className=\"mt-2 text-xs text-text-muted\">{milestone}</p>\n </GlassCard>\n ) : null}\n\n {myCertificate ? (\n <div className=\"mb-6 grid grid-cols-1 gap-4 lg:grid-cols-2\">\n <GlassCard\n className={cn(\n 'flex items-center gap-4 border p-5',\n myCertificate.status === 'REVOKED'\n ? 'border-status-error-border/60 bg-status-error-bg-subtle/20'\n : 'border-status-success-border/60 bg-status-success-bg-subtle/30'\n )}\n >\n <span className=\"flex h-14 w-14 shrink-0 items-center justify-center rounded-2xl bg-status-warning-bg-subtle text-status-warning-text ring-1 ring-status-warning-border/40\">\n <Trophy size={26} />\n </span>\n <div className=\"min-w-0\">\n <div className=\"text-base font-semibold text-text-primary\">\n {myCertificate.status === 'REVOKED'\n ? 'Your certificate was revoked'\n : 'You finished this course'}\n </div>\n <div className=\"text-sm text-text-muted\">\n {myCertificate.status === 'REVOKED'\n ? 'Contact a workspace admin if you believe this was a mistake.'\n : 'Your verifiable certificate of completion is issued — it also appears in My Learning.'}\n </div>\n </div>\n </GlassCard>\n <CertificatePreview\n certificate={myCertificate}\n learnerName={currentUser?.name ?? undefined}\n courseTitle={myCertificate.course?.title ?? course.title}\n />\n </div>\n ) : isComplete ? (\n <GlassCard className=\"mb-6 flex flex-col items-start gap-4 border border-status-success-border/60 bg-status-success-bg-subtle/40 p-5 sm:flex-row sm:items-center sm:justify-between\">\n <div className=\"flex items-center gap-4\">\n <span className=\"flex h-14 w-14 shrink-0 items-center justify-center rounded-2xl bg-status-warning-bg-subtle text-status-warning-text ring-1 ring-status-warning-border/40\">\n <Trophy size={26} />\n </span>\n <div>\n <div className=\"text-base font-semibold text-text-primary\">\n You finished this course\n </div>\n <div className=\"text-sm text-text-muted\">\n Claim your verifiable certificate of completion.\n </div>\n </div>\n </div>\n <Button onClick={handleClaimCertificate} disabled={claiming}>\n <Award size={16} /> {claiming ? 'Issuing…' : 'Claim certificate'}\n </Button>\n </GlassCard>\n ) : null}\n\n <GlassCard className=\"p-5\">\n <div className=\"mb-3 flex items-center justify-between gap-2\">\n <h2 className=\"flex items-center gap-2 text-base font-semibold text-text-primary\">\n <BookOpen size={18} className=\"text-accent-primary\" /> Curriculum\n {course.isPremiumOnly ? <PremiumBadge locked={course.isLocked} /> : null}\n </h2>\n {lessons.length ? (\n <span className=\"rounded-full bg-status-success-bg-subtle px-2.5 py-1 text-xs font-medium text-status-success-text\">\n {completedLessons}/{lessons.length} completed\n </span>\n ) : null}\n </div>\n {course.isLocked ? (\n <div className=\"mb-4\">\n <UpgradeGate\n compact\n description=\"This course is on a paid TechnoSpam plan. Preview lessons below stay free — the rest unlock once your workspace upgrades.\"\n />\n </div>\n ) : null}\n {lessons.length === 0 ? (\n <p className=\"py-6 text-center text-sm text-text-muted\">\n Lessons for this course are being prepared.\n </p>\n ) : (\n <div className=\"flex flex-col\">\n {lessons.map((lesson, i) => (\n <LessonRow\n key={lesson.id}\n lesson={lesson}\n index={i}\n done={!!lesson.completedByMe}\n open={openLessonId === lesson.id}\n onToggle={() => setOpenLessonId(openLessonId === lesson.id ? null : lesson.id)}\n onStart={() => openLesson(lesson.id)}\n />\n ))}\n </div>\n )}\n </GlassCard>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"CoursesCourseDetailsPagePage.js","names":[],"sources":["../../../src/technospam/pages/CoursesCourseDetailsPagePage.tsx"],"sourcesContent":["import type { ReactNode } from 'react';\nimport { useEffect } from 'react';\nimport { useSearchParams } from 'react-router';\nimport {\n ArrowRight,\n Award,\n BookOpen,\n CheckCircle2,\n ChevronDown,\n Clock,\n Eye,\n FileText,\n Lock,\n PlayCircle,\n Layers,\n ShieldCheck,\n Sparkles,\n Trophy,\n TrendingUp,\n Users,\n type LucideIcon,\n} from 'lucide-react';\nimport { Button, GlassCard, Skeleton } from '@burdenoff/fe-libs/ui';\nimport {\n EmptyView,\n ErrorView,\n MediaCover,\n PageLayout,\n PremiumBadge,\n StatCard,\n TagStrip,\n UpgradeGate,\n topicIcon,\n} from '../components';\nimport { useTechnoSpam, useTechnoSpamNavigate } from '../providers/TechnoSpamProvider';\nimport {\n useCourseQuery,\n useEnroll,\n useIssueCertificate,\n useMyCertificatesQuery,\n type TecCertificate,\n type TecLesson,\n type TecEnrollment,\n} from '../api/technospamApi';\nimport { formatDate, formatDuration, humanizeEnum } from '../utils/format';\nimport { notify } from '../lib/toast';\nimport { cn } from '../utils/cn';\nimport { useTechnoSpamAnalytics } from '../hooks/useTechnoSpamAnalytics';\nimport { useFocusedRecordParam } from '../hooks/useFocusedRecordParam';\nimport { FOCUS_PARAMS } from '../utils/pageContext';\nimport { lessonPlayerPath } from '../utils/lessonPath';\n\n/** Per-lesson-kind icon + on-theme tint (chip) and pill (badge) classes. */\nconst KIND_META: Record<string, { icon: LucideIcon; chip: string; badge: string }> = {\n VIDEO: {\n icon: PlayCircle,\n chip: 'bg-accent-primary/15 text-accent-primary',\n badge: 'border-accent-primary/25 bg-accent-primary/6 text-accent-primary',\n },\n ARTICLE: {\n icon: FileText,\n chip: 'bg-accent-indigo/15 text-accent-indigo',\n badge: 'border-accent-indigo/25 bg-accent-indigo/10 text-accent-indigo',\n },\n INTERACTIVE: {\n icon: Sparkles,\n chip: 'bg-accent-teal/15 text-accent-teal',\n badge: 'border-accent-teal/25 bg-accent-teal/10 text-accent-teal',\n },\n QUIZ: {\n icon: CheckCircle2,\n chip: 'bg-accent-purple/15 text-accent-purple',\n badge: 'border-accent-purple/25 bg-accent-purple/10 text-accent-purple',\n },\n PROJECT: {\n icon: Layers,\n chip: 'bg-status-warning-bg-subtle text-status-warning-text',\n badge: 'border-status-warning-border bg-status-warning-bg-subtle text-status-warning-text',\n },\n};\n\nfunction HeroBadge({ icon: Icon, children }: { icon?: LucideIcon; children: ReactNode }) {\n return (\n <span className=\"inline-flex items-center gap-1 rounded-full bg-white/15 px-2.5 py-1 text-xs font-medium text-white ring-1 ring-white/25 backdrop-blur-sm\">\n {Icon ? <Icon size={12} /> : null}\n {children}\n </span>\n );\n}\n\n/**\n * One curriculum row. Expands in place to reveal the lesson body.\n *\n * `TecLesson.content` is the only lesson body this service exposes to a\n * learner, and the API nulls it server-side whenever `isLocked` is true\n * (BOFF-4416), so the three states below are exhaustive and never guessed:\n * locked → upgrade note, content present → the body, content null on an\n * unlocked lesson → the author simply hasn't written it yet.\n */\nfunction LessonRow({\n lesson,\n index,\n done,\n open,\n onToggle,\n onStart,\n}: {\n lesson: TecLesson;\n index: number;\n done: boolean;\n /** The open lesson is the focused one (`?lessonId=`), so the assistant can name it. */\n open: boolean;\n onToggle: () => void;\n /** Opens the lesson in the lesson player (BOFF-4107). */\n onStart: () => void;\n}) {\n const meta = KIND_META[lesson.kind] ?? KIND_META.ARTICLE!;\n const Icon = meta.icon;\n const bodyId = `tec-lesson-body-${lesson.id}`;\n\n return (\n <div className=\"border-b border-border-seam last:border-b-0\">\n <button\n type=\"button\"\n onClick={onToggle}\n aria-expanded={open}\n aria-controls={bodyId}\n className=\"group flex w-full items-center gap-3 rounded-xl px-2 py-3 text-left transition-colors hover:bg-bg-sunken/60\"\n >\n <span className=\"hidden w-6 shrink-0 text-center text-sm font-medium tabular-nums text-text-muted sm:block\">\n {index + 1}\n </span>\n <span\n className={cn(\n 'flex h-9 w-9 shrink-0 items-center justify-center rounded-lg',\n done ? 'bg-status-success-bg-subtle text-status-success-text' : meta.chip\n )}\n >\n {done ? <CheckCircle2 size={17} /> : <Icon size={17} />}\n </span>\n <div className=\"min-w-0 flex-1\">\n <span className=\"block truncate text-sm font-medium text-text-primary\">\n {lesson.title}\n </span>\n <div className=\"mt-1 flex flex-wrap items-center gap-2\">\n <span\n className={cn(\n 'inline-flex items-center rounded-full border px-2 py-0.5 text-[11px] font-medium',\n meta.badge\n )}\n >\n {humanizeEnum(lesson.kind)}\n </span>\n <span className=\"inline-flex items-center gap-1 text-xs text-text-muted\">\n <Clock size={12} /> {formatDuration(lesson.estimatedMinutes)}\n </span>\n </div>\n </div>\n <div className=\"flex shrink-0 items-center gap-2\">\n {done ? (\n <CheckCircle2 size={18} className=\"text-status-success-text\" />\n ) : lesson.isPreview ? (\n <span className=\"inline-flex items-center gap-1 rounded-full border border-accent-primary/30 bg-accent-primary/6 px-2 py-0.5 text-[11px] font-medium text-accent-primary\">\n <Eye size={12} /> Preview\n </span>\n ) : lesson.isLocked ? (\n // BOFF-4416: distinct from the plain \"not completed yet\" Lock icon\n // below — this lesson's content is gated behind a paid plan, not\n // just not-yet-watched. The API already nulled `content`/`file`.\n <span className=\"inline-flex items-center gap-1 rounded-full border border-status-warning-border/60 bg-status-warning-bg-subtle px-2 py-0.5 text-[11px] font-medium text-status-warning-text\">\n <Lock size={12} /> Upgrade to unlock\n </span>\n ) : (\n <Lock size={15} className=\"text-text-muted\" />\n )}\n <ChevronDown\n size={16}\n aria-hidden\n className={cn(\n 'text-text-muted transition-transform duration-200',\n open ? 'rotate-180' : ''\n )}\n />\n </div>\n </button>\n\n {open ? (\n <div id={bodyId} className=\"px-2 pb-4 sm:pl-11\">\n {lesson.isLocked ? (\n <UpgradeGate\n compact\n description=\"This lesson is part of a paid TechnoSpam plan — its content unlocks once your workspace upgrades.\"\n />\n ) : (\n <div className=\"space-y-3\">\n {lesson.content ? (\n <div className=\"rounded-xl border border-border-seam bg-bg-sunken p-4\">\n <p className=\"line-clamp-6 whitespace-pre-wrap text-sm leading-relaxed text-text-secondary\">\n {lesson.content}\n </p>\n </div>\n ) : null}\n <Button size=\"sm\" onClick={onStart} data-testid={`open-lesson-${lesson.id}`}>\n <PlayCircle size={15} /> {done ? 'Review lesson' : 'Start lesson'}\n </Button>\n </div>\n )}\n </div>\n ) : null}\n </div>\n );\n}\n\n/**\n * Verifiable certificate face for an already-issued certificate. Every value\n * shown is a real field on the TecCertificate row (`serial`, `score`,\n * `issuedAt`, `status`) plus the course title it was issued against — the\n * learner name comes from the signed-in actor the shell handed us, and is\n * simply omitted when the shell did not supply one.\n */\nfunction CertificatePreview({\n certificate,\n learnerName,\n courseTitle,\n}: {\n certificate: TecCertificate;\n learnerName?: string;\n courseTitle: string;\n}) {\n const revoked = certificate.status === 'REVOKED';\n return (\n <GlassCard className=\"overflow-hidden p-0\">\n <div className=\"border-b border-border-seam px-5 py-3\">\n <span className=\"text-xs font-semibold uppercase tracking-wide text-text-muted\">\n Certificate preview\n </span>\n </div>\n <div className=\"space-y-4 bg-bg-sunken/50 px-5 py-6 text-center\">\n <div className=\"flex items-center justify-center gap-2 text-xs font-semibold uppercase tracking-[0.2em] text-text-muted\">\n <Award size={14} className=\"text-status-warning-text\" /> Certificate of completion\n </div>\n <h3 className=\"text-lg font-bold text-text-primary\">{courseTitle}</h3>\n {learnerName ? (\n <p className=\"text-sm text-text-secondary\">\n Awarded to <span className=\"font-semibold text-text-primary\">{learnerName}</span>\n </p>\n ) : null}\n <div className=\"grid grid-cols-1 gap-3 border-t border-border-seam pt-4 text-left sm:grid-cols-3\">\n <div>\n <div className=\"text-[11px] font-medium uppercase tracking-wide text-text-muted\">\n Serial number\n </div>\n <div className=\"truncate font-mono text-sm text-text-primary\">{certificate.serial}</div>\n </div>\n <div>\n <div className=\"text-[11px] font-medium uppercase tracking-wide text-text-muted\">\n Score\n </div>\n <div className=\"text-sm font-semibold text-text-primary\">{certificate.score}</div>\n </div>\n <div>\n <div className=\"text-[11px] font-medium uppercase tracking-wide text-text-muted\">\n Issued on\n </div>\n <div className=\"text-sm text-text-primary\">{formatDate(certificate.issuedAt)}</div>\n </div>\n </div>\n {revoked ? (\n <div className=\"rounded-lg border border-status-error-border bg-status-error-bg-subtle px-3 py-2 text-left text-xs text-status-error-text\">\n This certificate was revoked\n {certificate.revokedAt ? ` on ${formatDate(certificate.revokedAt)}` : ''}\n {certificate.revokeReason ? ` — ${certificate.revokeReason}` : '.'}\n </div>\n ) : (\n <span className=\"inline-flex items-center gap-1 rounded-full border border-status-success-border bg-status-success-bg-subtle px-2.5 py-1 text-xs font-medium text-status-success-text\">\n <ShieldCheck size={13} /> Verified\n </span>\n )}\n </div>\n </GlassCard>\n );\n}\n\nexport function CoursesCourseDetailsPagePage() {\n const [params] = useSearchParams();\n const id = params.get('id') ?? '';\n // The expanded lesson lives in `?lessonId=` so the assistant's page context\n // names it (\"explain this lesson\") and a reload reopens it.\n const [openLessonId, setOpenLessonId] = useFocusedRecordParam(FOCUS_PARAMS.lesson);\n const { currentUser, workspaceId } = useTechnoSpam();\n const navigate = useTechnoSpamNavigate();\n const { emit } = useTechnoSpamAnalytics();\n const { data, loading, error, refetch } = useCourseQuery(id);\n\n useEffect(() => {\n if (id) {\n emit('technospam.course.viewed', {\n workspaceId,\n entityId: id,\n entityType: 'TEC_COURSE',\n });\n }\n }, [id, workspaceId, emit]);\n\n const [enroll, { loading: enrolling }] = useEnroll();\n const course = data?.tecCourse ?? null;\n // Self-scoped read (BOFF-10481): the learner's own certificates come from the\n // canonical Buildmyiq field, which filters by the authenticated actor\n // server-side. It is narrowed to this course by its canonical id once known\n // and by its Technospam id on the client. A cross-learner list-all read is\n // not granted to a plain learner, so an already-claimed certificate would\n // come back empty and the page would keep offering \"Claim certificate\".\n const { certificates: myCertificates, refetch: refetchCerts } = useMyCertificatesQuery({\n buiCourseId: course?.buiCourseId,\n technospamCourseId: id,\n skip: !id,\n });\n const [issueCertificate, { loading: claiming }] = useIssueCertificate();\n\n const lessons = [...(course?.lessons ?? [])].sort((a, b) => a.position - b.position);\n const myEnrollment: TecEnrollment | undefined = (course?.enrollments ?? []).find(\n (e) => e.learnerId === currentUser?.id\n );\n const myCertificate = myCertificates.find((c) => c.status === 'ISSUED') ?? myCertificates[0];\n const isComplete = !!myEnrollment && myEnrollment.progressPct >= 100;\n\n const handleClaimCertificate = async () => {\n if (!id || !currentUser?.id) return;\n emit('technospam.course.certificate_claimed', {\n workspaceId,\n entityId: id,\n entityType: 'TEC_COURSE',\n });\n const res = await issueCertificate({\n variables: { courseId: id, learnerId: currentUser.id, score: 100 },\n });\n if (res.data?.tecIssueCertificate) {\n notify.success('Certificate issued', 'Your verifiable certificate is ready in My Learning.');\n await refetchCerts();\n }\n };\n\n const handleEnroll = async () => {\n if (!id) return;\n emit('technospam.course.enroll_clicked', {\n workspaceId,\n entityId: id,\n entityType: 'TEC_COURSE',\n });\n const res = await enroll({ variables: { input: { courseId: id } } });\n if (res.data?.tecEnroll) {\n emit('technospam.course.enrolled', {\n workspaceId,\n entityId: id,\n entityType: 'TEC_COURSE',\n });\n notify.success('Enrolled', `You're enrolled in \"${course?.title}\".`);\n await refetch();\n }\n };\n\n if (loading && !course) {\n return (\n <PageLayout title=\"Course\" backPath=\"/courses/courses-page\">\n <Skeleton className=\"mb-4 h-40 w-full rounded-2xl\" />\n <Skeleton className=\"h-64 w-full rounded-xl\" />\n </PageLayout>\n );\n }\n\n if (error || !course) {\n return (\n <PageLayout title=\"Course\" backPath=\"/courses/courses-page\">\n {error ? (\n <ErrorView title=\"Couldn't load course\" error={error} />\n ) : (\n <EmptyView\n title=\"Course not found\"\n description=\"This course may have been unpublished.\"\n />\n )}\n </PageLayout>\n );\n }\n\n const completedLessons = myEnrollment?.lessonsCompleted ?? 0;\n // BOFF-4107: \"Continue\" lands on the first lesson the learner has not\n // finished (per-lesson completions from the API), or the first lesson to\n // review once everything is done.\n const nextLesson = lessons.find((l) => !l.completedByMe) ?? lessons[0];\n const allDone = lessons.length > 0 && lessons.every((l) => l.completedByMe);\n const openLesson = (lessonId: string) => navigate(lessonPlayerPath(id, lessonId));\n const remaining = Math.max(0, lessons.length - completedLessons);\n const milestone =\n myEnrollment && myEnrollment.progressPct >= 100\n ? 'Course complete — claim your certificate below.'\n : remaining === 1\n ? '1 lesson to go — almost there!'\n : `${remaining} lessons to go — keep it up!`;\n const heroSeed = `${course.topic ?? ''} ${course.title}`;\n\n return (\n <PageLayout\n backPath=\"/courses/courses-page\"\n title={course.title}\n description={course.summary ?? undefined}\n actions={\n myEnrollment ? (\n nextLesson ? (\n <Button onClick={() => openLesson(nextLesson.id)} data-testid=\"continue-learning\">\n {allDone ? <CheckCircle2 size={16} /> : <ArrowRight size={16} />}\n {allDone\n ? 'Review course'\n : completedLessons > 0\n ? 'Continue learning'\n : 'Start course'}\n </Button>\n ) : null\n ) : (\n <Button onClick={handleEnroll} disabled={enrolling}>\n <BookOpen size={16} /> {enrolling ? 'Enrolling…' : 'Enroll'}\n </Button>\n )\n }\n >\n {/* Bold hero banner */}\n <MediaCover\n height=\"h-40\"\n seed={heroSeed}\n icon={topicIcon(course.topic ?? course.title)}\n className=\"mb-5 rounded-2xl\"\n >\n <div className=\"flex w-full flex-col gap-2\">\n <span className=\"text-xs font-medium uppercase tracking-wide text-white/80\">\n {course.topic ?? 'Course'}\n </span>\n <h2 className=\"text-2xl font-bold leading-tight text-white drop-shadow-sm sm:text-3xl\">\n {course.title}\n </h2>\n <div className=\"flex flex-wrap items-center gap-2\">\n <HeroBadge icon={TrendingUp}>{humanizeEnum(course.level)}</HeroBadge>\n <HeroBadge icon={CheckCircle2}>{humanizeEnum(course.status)}</HeroBadge>\n {course.topic ? <HeroBadge icon={Layers}>{course.topic}</HeroBadge> : null}\n {course.isPremiumOnly ? <HeroBadge icon={Lock}>Premium</HeroBadge> : null}\n </div>\n </div>\n </MediaCover>\n\n <TagStrip tags={course.tags} className=\"mb-5\" />\n\n <div className=\"mb-6 grid grid-cols-2 gap-3 sm:grid-cols-4\">\n <StatCard label=\"Lessons\" value={lessons.length} icon={FileText} tone=\"blue\" />\n <StatCard\n label=\"Duration\"\n value={formatDuration(course.estimatedMinutes)}\n icon={Clock}\n tone=\"teal\"\n />\n <StatCard\n label=\"Enrolled\"\n value={course.enrollments?.length ?? 0}\n icon={Users}\n tone=\"indigo\"\n />\n <StatCard\n label=\"Your progress\"\n value={myEnrollment ? `${myEnrollment.progressPct}%` : 'Not started'}\n icon={TrendingUp}\n tone=\"purple\"\n />\n </div>\n\n {myEnrollment ? (\n <GlassCard className=\"mb-6 p-5\">\n <div className=\"mb-3 flex items-center gap-2\">\n <span className=\"flex h-8 w-8 items-center justify-center rounded-lg bg-accent-primary/15 text-accent-primary\">\n <TrendingUp size={16} />\n </span>\n <span className=\"text-sm font-semibold text-text-primary\">Your progress</span>\n <span className=\"ml-auto text-sm text-text-secondary\">\n {completedLessons}/{lessons.length} lessons · {myEnrollment.progressPct}%\n </span>\n </div>\n <div className=\"h-2.5 w-full overflow-hidden rounded-full bg-bg-sunken\">\n <div\n className=\"h-full rounded-full bg-gradient-to-r from-accent-primary via-accent-indigo to-accent-purple transition-all duration-500\"\n style={{ width: `${myEnrollment.progressPct}%` }}\n />\n </div>\n <p className=\"mt-2 text-xs text-text-muted\">{milestone}</p>\n </GlassCard>\n ) : null}\n\n {myCertificate ? (\n <div className=\"mb-6 grid grid-cols-1 gap-4 lg:grid-cols-2\">\n <GlassCard\n className={cn(\n 'flex items-center gap-4 border p-5',\n myCertificate.status === 'REVOKED'\n ? 'border-status-error-border/60 bg-status-error-bg-subtle/20'\n : 'border-status-success-border/60 bg-status-success-bg-subtle/30'\n )}\n >\n <span className=\"flex h-14 w-14 shrink-0 items-center justify-center rounded-2xl bg-status-warning-bg-subtle text-status-warning-text ring-1 ring-status-warning-border/40\">\n <Trophy size={26} />\n </span>\n <div className=\"min-w-0\">\n <div className=\"text-base font-semibold text-text-primary\">\n {myCertificate.status === 'REVOKED'\n ? 'Your certificate was revoked'\n : 'You finished this course'}\n </div>\n <div className=\"text-sm text-text-muted\">\n {myCertificate.status === 'REVOKED'\n ? 'Contact a workspace admin if you believe this was a mistake.'\n : 'Your verifiable certificate of completion is issued — it also appears in My Learning.'}\n </div>\n </div>\n </GlassCard>\n <CertificatePreview\n certificate={myCertificate}\n learnerName={currentUser?.name ?? undefined}\n courseTitle={myCertificate.course?.title ?? course.title}\n />\n </div>\n ) : isComplete ? (\n <GlassCard className=\"mb-6 flex flex-col items-start gap-4 border border-status-success-border/60 bg-status-success-bg-subtle/40 p-5 sm:flex-row sm:items-center sm:justify-between\">\n <div className=\"flex items-center gap-4\">\n <span className=\"flex h-14 w-14 shrink-0 items-center justify-center rounded-2xl bg-status-warning-bg-subtle text-status-warning-text ring-1 ring-status-warning-border/40\">\n <Trophy size={26} />\n </span>\n <div>\n <div className=\"text-base font-semibold text-text-primary\">\n You finished this course\n </div>\n <div className=\"text-sm text-text-muted\">\n Claim your verifiable certificate of completion.\n </div>\n </div>\n </div>\n <Button onClick={handleClaimCertificate} disabled={claiming}>\n <Award size={16} /> {claiming ? 'Issuing…' : 'Claim certificate'}\n </Button>\n </GlassCard>\n ) : null}\n\n <GlassCard className=\"p-5\">\n <div className=\"mb-3 flex items-center justify-between gap-2\">\n <h2 className=\"flex items-center gap-2 text-base font-semibold text-text-primary\">\n <BookOpen size={18} className=\"text-accent-primary\" /> Curriculum\n {course.isPremiumOnly ? <PremiumBadge locked={course.isLocked} /> : null}\n </h2>\n {lessons.length ? (\n <span className=\"rounded-full bg-status-success-bg-subtle px-2.5 py-1 text-xs font-medium text-status-success-text\">\n {completedLessons}/{lessons.length} completed\n </span>\n ) : null}\n </div>\n {course.isLocked ? (\n <div className=\"mb-4\">\n <UpgradeGate\n compact\n description=\"This course is on a paid TechnoSpam plan. Preview lessons below stay free — the rest unlock once your workspace upgrades.\"\n />\n </div>\n ) : null}\n {lessons.length === 0 ? (\n <p className=\"py-6 text-center text-sm text-text-muted\">\n Lessons for this course are being prepared.\n </p>\n ) : (\n <div className=\"flex flex-col\">\n {lessons.map((lesson, i) => (\n <LessonRow\n key={lesson.id}\n lesson={lesson}\n index={i}\n done={!!lesson.completedByMe}\n open={openLessonId === lesson.id}\n onToggle={() => setOpenLessonId(openLessonId === lesson.id ? null : lesson.id)}\n onStart={() => openLesson(lesson.id)}\n />\n ))}\n </div>\n )}\n </GlassCard>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
@@ -6,9 +6,9 @@ import { StatCard as a } from "../components/StatCard.js";
6
6
  import { MediaCover as o, topicIcon as s } from "../components/MediaCover.js";
7
7
  import { formatDuration as c } from "../utils/format.js";
8
8
  import { StatusBadge as l } from "../components/StatusBadge.js";
9
- import { LoadMore as u } from "../components/LoadMore.js";
10
- import { EmptyView as d, ErrorView as f, LoadingGrid as p } from "../components/StateViews.js";
11
- import { PremiumBadge as m } from "../components/UpgradeGate.js";
9
+ import { PremiumBadge as u } from "../components/UpgradeGate.js";
10
+ import { LoadMore as d } from "../components/LoadMore.js";
11
+ import { EmptyView as f, ErrorView as p, LoadingGrid as m } from "../components/StateViews.js";
12
12
  import { pageArg as h, useCoursesQuery as g, usePageLimit as _ } from "../api/technospamApi.js";
13
13
  import "../components/index.js";
14
14
  import { TAB_PANEL_FOCUS_CLASS as v } from "../lib/a11y.js";
@@ -29,7 +29,7 @@ function L({ course: e, onOpen: t }) {
29
29
  children: [/* @__PURE__ */ D(l, {
30
30
  status: e.level,
31
31
  kind: "level"
32
- }), e.isPremiumOnly ? /* @__PURE__ */ D(m, { locked: e.isLocked }) : null]
32
+ }), e.isPremiumOnly ? /* @__PURE__ */ D(u, { locked: e.isLocked }) : null]
33
33
  })
34
34
  }), /* @__PURE__ */ O("div", {
35
35
  className: "flex flex-1 flex-col gap-3 p-4",
@@ -75,28 +75,28 @@ function L({ course: e, onOpen: t }) {
75
75
  });
76
76
  }
77
77
  function R() {
78
- let o = t(), { workspaceId: s } = e(), { emit: l } = n(), m = r(), [w, k] = b("ALL"), [j, R] = b(""), z = y(() => [
78
+ let o = t(), { workspaceId: s } = e(), { emit: l } = n(), u = r(), [w, k] = b("ALL"), [j, R] = b(""), z = y(() => [
79
79
  {
80
80
  value: "ALL",
81
- label: m("technospam.courses.filterAllLevels", "All levels")
81
+ label: u("technospam.courses.filterAllLevels", "All levels")
82
82
  },
83
83
  {
84
84
  value: "BEGINNER",
85
- label: m("technospam.courses.filterBeginner", "Beginner")
85
+ label: u("technospam.courses.filterBeginner", "Beginner")
86
86
  },
87
87
  {
88
88
  value: "INTERMEDIATE",
89
- label: m("technospam.courses.filterIntermediate", "Intermediate")
89
+ label: u("technospam.courses.filterIntermediate", "Intermediate")
90
90
  },
91
91
  {
92
92
  value: "ADVANCED",
93
- label: m("technospam.courses.filterAdvanced", "Advanced")
93
+ label: u("technospam.courses.filterAdvanced", "Advanced")
94
94
  },
95
95
  {
96
96
  value: "EXPERT",
97
- label: m("technospam.courses.filterExpert", "Expert")
97
+ label: u("technospam.courses.filterExpert", "Expert")
98
98
  }
99
- ], [m]), { limit: B, more: V } = _(), { data: H, loading: U, error: W } = g({
99
+ ], [u]), { limit: B, more: V } = _(), { data: H, loading: U, error: W } = g({
100
100
  status: "PUBLISHED",
101
101
  pagination: h(B),
102
102
  ...w === "ALL" ? {} : { level: w }
@@ -106,40 +106,40 @@ function R() {
106
106
  }, [G, j]);
107
107
  return /* @__PURE__ */ O(i, {
108
108
  hero: !0,
109
- title: m("technospam.courses.title", "Course Catalog"),
110
- description: m("technospam.courses.description", "Learn technology by doing — practical, project-based courses across the stack."),
109
+ title: u("technospam.courses.title", "Course Catalog"),
110
+ description: u("technospam.courses.description", "Learn technology by doing — practical, project-based courses across the stack."),
111
111
  actions: /* @__PURE__ */ O(A, {
112
112
  variant: "outline",
113
113
  onClick: () => o("/courses/my-learning-page"),
114
114
  children: [
115
115
  /* @__PURE__ */ D(C, { size: 16 }),
116
116
  " ",
117
- m("technospam.common.myLearning", "My Learning")
117
+ u("technospam.common.myLearning", "My Learning")
118
118
  ]
119
119
  }),
120
120
  children: [/* @__PURE__ */ O("div", {
121
121
  className: "mb-5 grid grid-cols-2 gap-3 sm:grid-cols-4",
122
122
  children: [
123
123
  /* @__PURE__ */ D(a, {
124
- label: m("technospam.courses.statCourses", "Courses"),
124
+ label: u("technospam.courses.statCourses", "Courses"),
125
125
  value: G.length,
126
126
  icon: x,
127
127
  tone: "blue"
128
128
  }),
129
129
  /* @__PURE__ */ D(a, {
130
- label: m("technospam.courses.statBeginnerFriendly", "Beginner-friendly"),
130
+ label: u("technospam.courses.statBeginnerFriendly", "Beginner-friendly"),
131
131
  value: G.filter((e) => e.level === "BEGINNER").length,
132
132
  icon: E,
133
133
  tone: "green"
134
134
  }),
135
135
  /* @__PURE__ */ D(a, {
136
- label: m("technospam.courses.statAdvancedPlus", "Advanced+"),
136
+ label: u("technospam.courses.statAdvancedPlus", "Advanced+"),
137
137
  value: G.filter((e) => e.level === "ADVANCED" || e.level === "EXPERT").length,
138
138
  icon: C,
139
139
  tone: "purple"
140
140
  }),
141
141
  /* @__PURE__ */ D(a, {
142
- label: m("technospam.courses.statTotalContent", "Total content"),
142
+ label: u("technospam.courses.statTotalContent", "Total content"),
143
143
  value: c(G.reduce((e, t) => e + t.estimatedMinutes, 0)),
144
144
  icon: S,
145
145
  tone: "teal"
@@ -161,8 +161,8 @@ function R() {
161
161
  size: 16
162
162
  }), /* @__PURE__ */ D(M, {
163
163
  className: "pl-9",
164
- "aria-label": m("technospam.courses.searchLabel", "Search courses"),
165
- placeholder: m("technospam.courses.searchPlaceholder", "Search courses…"),
164
+ "aria-label": u("technospam.courses.searchLabel", "Search courses"),
165
+ placeholder: u("technospam.courses.searchPlaceholder", "Search courses…"),
166
166
  value: j,
167
167
  onChange: (e) => R(e.target.value),
168
168
  onBlur: () => {
@@ -178,13 +178,13 @@ function R() {
178
178
  }), /* @__PURE__ */ O(P, {
179
179
  value: w,
180
180
  className: v,
181
- children: [U && !G.length ? /* @__PURE__ */ D(p, {}) : W ? /* @__PURE__ */ D(f, {
182
- title: m("technospam.common.errorLoadCourses", "Couldn't load courses"),
181
+ children: [U && !G.length ? /* @__PURE__ */ D(m, {}) : W ? /* @__PURE__ */ D(p, {
182
+ title: u("technospam.common.errorLoadCourses", "Couldn't load courses"),
183
183
  error: W
184
- }) : q.length === 0 ? /* @__PURE__ */ D(d, {
184
+ }) : q.length === 0 ? /* @__PURE__ */ D(f, {
185
185
  icon: /* @__PURE__ */ D(x, { size: 26 }),
186
- title: j || w !== "ALL" ? m("technospam.courses.emptySearchTitle", "No matching courses") : m("technospam.courses.emptyDefaultTitle", "No courses published yet"),
187
- description: j || w !== "ALL" ? m("technospam.courses.emptySearchDesc", "Try a different search or level filter.") : m("technospam.courses.emptyDefaultDesc", "Published courses will appear here. Check back soon — TechnoSpam is pre-launch.")
186
+ title: j || w !== "ALL" ? u("technospam.courses.emptySearchTitle", "No matching courses") : u("technospam.courses.emptyDefaultTitle", "No courses published yet"),
187
+ description: j || w !== "ALL" ? u("technospam.courses.emptySearchDesc", "Try a different search or level filter.") : u("technospam.courses.emptyDefaultDesc", "Published courses will appear here. Check back soon — TechnoSpam is pre-launch.")
188
188
  }) : /* @__PURE__ */ D("div", {
189
189
  className: "grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3",
190
190
  children: q.map((e) => /* @__PURE__ */ D(L, {
@@ -197,7 +197,7 @@ function R() {
197
197
  }), o(`/courses/course-details-page?id=${e.id}`);
198
198
  }
199
199
  }, e.id))
200
- }), /* @__PURE__ */ D(u, {
200
+ }), /* @__PURE__ */ D(d, {
201
201
  hasMore: K,
202
202
  loading: U,
203
203
  onClick: V
@@ -3,17 +3,17 @@ import { useTechnoSpamAnalytics as n } from "../hooks/useTechnoSpamAnalytics.js"
3
3
  import { cn as r } from "../utils/cn.js";
4
4
  import { PageLayout as i } from "../components/PageLayout.js";
5
5
  import { formatDuration as ee, humanizeEnum as a } from "../utils/format.js";
6
- import { EmptyView as o, ErrorView as te } from "../components/StateViews.js";
7
- import { UpgradeGate as s } from "../components/UpgradeGate.js";
8
- import { notify as c } from "../lib/toast.js";
9
- import { useCompleteLesson as l, useEnroll as u, useLessonPlayerQuery as ne, useMyEnrollmentsQuery as d } from "../api/technospamApi.js";
6
+ import { UpgradeGate as o } from "../components/UpgradeGate.js";
7
+ import { EmptyView as s, ErrorView as c } from "../components/StateViews.js";
8
+ import { notify as l } from "../lib/toast.js";
9
+ import { useCompleteLesson as u, useEnroll as d, useLessonPlayerQuery as te, useMyEnrollmentsQuery as f } from "../api/technospamApi.js";
10
10
  import "../components/index.js";
11
- import { FOCUS_PARAMS as f } from "../utils/pageContext.js";
12
- import { courseDetailsPath as p, lessonPlayerPath as m } from "../utils/lessonPath.js";
13
- import { InteractiveWorkspace as h } from "../components/lesson/InteractiveWorkspace.js";
14
- import { QuizLesson as g } from "../components/lesson/QuizLesson.js";
15
- import { useEffect as re } from "react";
16
- import { ArrowLeft as ie, ArrowRight as _, BookOpen as v, CheckCircle2 as y, Clock as b, FileText as x, Layers as S, Lock as ae, PlayCircle as C, Sparkles as w } from "lucide-react";
11
+ import { FOCUS_PARAMS as p } from "../utils/pageContext.js";
12
+ import { courseDetailsPath as m, lessonPlayerPath as h } from "../utils/lessonPath.js";
13
+ import { InteractiveWorkspace as g } from "../components/lesson/InteractiveWorkspace.js";
14
+ import { QuizLesson as _ } from "../components/lesson/QuizLesson.js";
15
+ import { useEffect as ne } from "react";
16
+ import { ArrowLeft as re, ArrowRight as v, BookOpen as y, CheckCircle2 as b, Clock as ie, FileText as x, Layers as S, Lock as ae, PlayCircle as C, Sparkles as w } from "lucide-react";
17
17
  import { useSearchParams as T } from "react-router";
18
18
  import { jsx as E, jsxs as D } from "react/jsx-runtime";
19
19
  import { Button as O, GlassCard as k, Skeleton as A } from "@burdenoff/fe-libs/ui";
@@ -23,7 +23,7 @@ var M = {
23
23
  VIDEO: C,
24
24
  ARTICLE: x,
25
25
  INTERACTIVE: w,
26
- QUIZ: y,
26
+ QUIZ: b,
27
27
  PROJECT: S
28
28
  };
29
29
  function oe(e) {
@@ -54,7 +54,7 @@ function N({ lesson: e, onProgress: t }) {
54
54
  }
55
55
  case "QUIZ": return /* @__PURE__ */ D("div", {
56
56
  className: "space-y-4",
57
- children: [n, /* @__PURE__ */ E(g, {
57
+ children: [n, /* @__PURE__ */ E(_, {
58
58
  lesson: e,
59
59
  onPassed: t
60
60
  }, e.id)]
@@ -69,7 +69,7 @@ function N({ lesson: e, onProgress: t }) {
69
69
  })
70
70
  }), /* @__PURE__ */ E("div", {
71
71
  className: "min-w-0",
72
- children: /* @__PURE__ */ E(h, {
72
+ children: /* @__PURE__ */ E(g, {
73
73
  lesson: e,
74
74
  onCompleted: t
75
75
  }, e.id)
@@ -82,8 +82,8 @@ function N({ lesson: e, onProgress: t }) {
82
82
  }
83
83
  }
84
84
  function P() {
85
- let [h] = T(), g = h.get(f.id) ?? "", S = h.get(f.lesson) ?? "", C = t(), { workspaceId: w } = e(), { emit: j } = n(), { data: P, loading: F, error: I, refetch: L } = ne(g, S), [R, { loading: z }] = l(), [B, { loading: V }] = u();
86
- re(() => {
85
+ let [g] = T(), _ = g.get(p.id) ?? "", S = g.get(p.lesson) ?? "", C = t(), { workspaceId: w } = e(), { emit: j } = n(), { data: P, loading: F, error: I, refetch: L } = te(_, S), [R, { loading: z }] = u(), [B, { loading: V }] = d();
86
+ ne(() => {
87
87
  S && j("technospam.lesson.viewed", {
88
88
  workspaceId: w,
89
89
  entityId: S,
@@ -94,11 +94,11 @@ function P() {
94
94
  w,
95
95
  j
96
96
  ]);
97
- let H = P?.tecLesson ?? null, U = P?.tecCourse ?? null, { enrollments: W, refetch: G } = d({
97
+ let H = P?.tecLesson ?? null, U = P?.tecCourse ?? null, { enrollments: W, refetch: G } = f({
98
98
  buiCourseId: U?.buiCourseId,
99
- technospamCourseId: g,
100
- skip: !g
101
- }), K = W[0] ?? null, q = [...U?.lessons ?? []].sort((e, t) => e.position - t.position), J = q.findIndex((e) => e.id === S), Y = J > 0 ? q[J - 1] : void 0, X = J >= 0 && J < q.length - 1 ? q[J + 1] : void 0, Z = p(g), se = () => {
99
+ technospamCourseId: _,
100
+ skip: !_
101
+ }), K = W[0] ?? null, q = [...U?.lessons ?? []].sort((e, t) => e.position - t.position), J = q.findIndex((e) => e.id === S), Y = J > 0 ? q[J - 1] : void 0, X = J >= 0 && J < q.length - 1 ? q[J + 1] : void 0, Z = m(_), se = () => {
102
102
  L(), G();
103
103
  }, ce = async () => {
104
104
  if (!H) return;
@@ -107,14 +107,14 @@ function P() {
107
107
  workspaceId: w,
108
108
  entityId: H.id,
109
109
  entityType: "TEC_LESSON"
110
- }), c.success("Lesson completed", `${e.progressPct}% of the course done`), X ? C(m(g, X.id)) : await L());
110
+ }), l.success("Lesson completed", `${e.progressPct}% of the course done`), X ? C(h(_, X.id)) : await L());
111
111
  }, le = async () => {
112
- (await B({ variables: { input: { courseId: g } } })).data?.tecEnroll && (c.success("Enrolled", `Your progress in "${U?.title ?? "this course"}" is now tracked.`), await L());
112
+ (await B({ variables: { input: { courseId: _ } } })).data?.tecEnroll && (l.success("Enrolled", `Your progress in "${U?.title ?? "this course"}" is now tracked.`), await L());
113
113
  };
114
- if (!g || !S) return /* @__PURE__ */ E(i, {
114
+ if (!_ || !S) return /* @__PURE__ */ E(i, {
115
115
  title: "Lesson",
116
116
  backPath: "/courses/courses-page",
117
- children: /* @__PURE__ */ E(o, {
117
+ children: /* @__PURE__ */ E(s, {
118
118
  title: "No lesson selected",
119
119
  description: "Open a lesson from a course curriculum."
120
120
  })
@@ -127,10 +127,10 @@ function P() {
127
127
  if (I || !H || !U || H.courseId !== U.id) return /* @__PURE__ */ E(i, {
128
128
  title: "Lesson",
129
129
  backPath: Z,
130
- children: I ? /* @__PURE__ */ E(te, {
130
+ children: I ? /* @__PURE__ */ E(c, {
131
131
  title: "Couldn't load lesson",
132
132
  error: I
133
- }) : /* @__PURE__ */ E(o, {
133
+ }) : /* @__PURE__ */ E(s, {
134
134
  title: "Lesson not found",
135
135
  description: "This lesson may have been removed."
136
136
  })
@@ -142,7 +142,7 @@ function P() {
142
142
  description: `${U.title} · Lesson ${J + 1} of ${q.length}`,
143
143
  actions: Q ? /* @__PURE__ */ D("span", {
144
144
  className: "inline-flex items-center gap-1.5 rounded-full border border-status-success-border bg-status-success-bg-subtle px-3 py-1.5 text-sm font-medium text-status-success-text",
145
- children: [/* @__PURE__ */ E(y, { size: 15 }), " Completed"]
145
+ children: [/* @__PURE__ */ E(b, { size: 15 }), " Completed"]
146
146
  }) : null,
147
147
  children: /* @__PURE__ */ D("div", {
148
148
  className: "grid grid-cols-1 gap-5 lg:grid-cols-[minmax(0,1fr)_280px]",
@@ -163,7 +163,7 @@ function P() {
163
163
  /* @__PURE__ */ D("span", {
164
164
  className: "inline-flex items-center gap-1",
165
165
  children: [
166
- /* @__PURE__ */ E(b, { size: 12 }),
166
+ /* @__PURE__ */ E(ie, { size: 12 }),
167
167
  " ",
168
168
  ee(H.estimatedMinutes)
169
169
  ]
@@ -184,7 +184,7 @@ function P() {
184
184
  onClick: () => void le(),
185
185
  disabled: V,
186
186
  children: [
187
- /* @__PURE__ */ E(v, { size: 14 }),
187
+ /* @__PURE__ */ E(y, { size: 14 }),
188
188
  " ",
189
189
  V ? "Enrolling…" : "Enroll"
190
190
  ]
@@ -193,7 +193,7 @@ function P() {
193
193
  /* @__PURE__ */ E(k, {
194
194
  className: "p-5",
195
195
  "data-testid": "lesson-player-body",
196
- children: H.isLocked ? /* @__PURE__ */ E(s, { description: "This lesson is part of a paid TechnoSpam plan. Its content unlocks once your workspace upgrades." }) : /* @__PURE__ */ E(N, {
196
+ children: H.isLocked ? /* @__PURE__ */ E(o, { description: "This lesson is part of a paid TechnoSpam plan. Its content unlocks once your workspace upgrades." }) : /* @__PURE__ */ E(N, {
197
197
  lesson: H,
198
198
  onProgress: se
199
199
  })
@@ -203,8 +203,8 @@ function P() {
203
203
  children: [/* @__PURE__ */ D(O, {
204
204
  variant: "outline",
205
205
  disabled: !Y,
206
- onClick: () => Y && C(m(g, Y.id)),
207
- children: [/* @__PURE__ */ E(ie, { size: 15 }), " Previous"]
206
+ onClick: () => Y && C(h(_, Y.id)),
207
+ children: [/* @__PURE__ */ E(re, { size: 15 }), " Previous"]
208
208
  }), /* @__PURE__ */ D("div", {
209
209
  className: "flex flex-wrap items-center gap-2",
210
210
  children: [
@@ -212,7 +212,7 @@ function P() {
212
212
  onClick: () => void ce(),
213
213
  disabled: z,
214
214
  children: [
215
- /* @__PURE__ */ E(y, { size: 15 }),
215
+ /* @__PURE__ */ E(b, { size: 15 }),
216
216
  " ",
217
217
  z ? "Saving…" : "Mark complete"
218
218
  ]
@@ -224,8 +224,8 @@ function P() {
224
224
  /* @__PURE__ */ D(O, {
225
225
  variant: Q || !K ? "default" : "outline",
226
226
  disabled: !X,
227
- onClick: () => X && C(m(g, X.id)),
228
- children: ["Next ", /* @__PURE__ */ E(_, { size: 15 })]
227
+ onClick: () => X && C(h(_, X.id)),
228
+ children: ["Next ", /* @__PURE__ */ E(v, { size: 15 })]
229
229
  })
230
230
  ]
231
231
  })]
@@ -259,14 +259,14 @@ function P() {
259
259
  return /* @__PURE__ */ E("li", { children: /* @__PURE__ */ D("button", {
260
260
  type: "button",
261
261
  "aria-current": i ? "page" : void 0,
262
- onClick: () => C(m(g, e.id)),
262
+ onClick: () => C(h(_, e.id)),
263
263
  className: r("flex w-full items-center gap-2 rounded-lg px-2 py-1.5 text-left text-sm transition-colors hover:bg-bg-sunken", i && "bg-accent-primary/6 font-medium text-accent-primary"),
264
264
  children: [
265
265
  /* @__PURE__ */ E("span", {
266
266
  className: "w-5 shrink-0 text-center text-xs tabular-nums text-text-muted",
267
267
  children: t + 1
268
268
  }),
269
- e.completedByMe ? /* @__PURE__ */ E(y, {
269
+ e.completedByMe ? /* @__PURE__ */ E(b, {
270
270
  size: 14,
271
271
  className: "shrink-0 text-status-success-text"
272
272
  }) : e.isLocked ? /* @__PURE__ */ E(ae, {
@@ -1 +1 @@
1
- {"version":3,"file":"CoursesLessonPlayerPagePage.js","names":[],"sources":["../../../src/technospam/pages/CoursesLessonPlayerPagePage.tsx"],"sourcesContent":["import { useEffect } from 'react';\nimport { useSearchParams } from 'react-router';\nimport { Markdown } from '@burdenoff/fe-libs/shared/components/Markdown';\nimport {\n ArrowLeft,\n ArrowRight,\n BookOpen,\n CheckCircle2,\n Clock,\n FileText,\n Layers,\n Lock,\n PlayCircle,\n Sparkles,\n type LucideIcon,\n} from 'lucide-react';\nimport { Button, GlassCard, Skeleton } from '@burdenoff/fe-libs/ui';\nimport { EmptyView, ErrorView, PageLayout, UpgradeGate } from '../components';\nimport { InteractiveWorkspace } from '../components/lesson/InteractiveWorkspace';\nimport { QuizLesson } from '../components/lesson/QuizLesson';\nimport { useTechnoSpamNavigate, useTechnoSpam } from '../providers/TechnoSpamProvider';\nimport {\n useCompleteLesson,\n useEnroll,\n useLessonPlayerQuery,\n useMyEnrollmentsQuery,\n type TecLesson,\n type TecLessonKind,\n} from '../api/technospamApi';\nimport { formatDuration, humanizeEnum } from '../utils/format';\nimport { notify } from '../lib/toast';\nimport { cn } from '../utils/cn';\nimport { useTechnoSpamAnalytics } from '../hooks/useTechnoSpamAnalytics';\nimport { FOCUS_PARAMS } from '../utils/pageContext';\nimport { courseDetailsPath, lessonPlayerPath } from '../utils/lessonPath';\n\nconst KIND_ICON: Record<TecLessonKind, LucideIcon> = {\n VIDEO: PlayCircle,\n ARTICLE: FileText,\n INTERACTIVE: Sparkles,\n QUIZ: CheckCircle2,\n PROJECT: Layers,\n};\n\n/** Lessons whose completion is earned (a passed quiz, a matching run) rather than self-reported. */\nfunction completesByCheck(lesson: TecLesson): boolean {\n if (lesson.kind === 'QUIZ') return (lesson.quizQuestions?.length ?? 0) > 0;\n if (lesson.kind === 'INTERACTIVE') return !!lesson.expectedOutput;\n return false;\n}\n\nfunction LessonBody({ lesson, onProgress }: { lesson: TecLesson; onProgress: () => void }) {\n const notes = lesson.content ? (\n <Markdown className=\"prose prose-sm max-w-none text-text-secondary dark:prose-invert\">\n {lesson.content}\n </Markdown>\n ) : null;\n\n switch (lesson.kind) {\n case 'VIDEO': {\n const src = lesson.file?.downloadUrl;\n const isVideo = !lesson.file?.mimeType || lesson.file.mimeType.startsWith('video/');\n return (\n <div className=\"space-y-4\">\n {src && isVideo ? (\n <video\n controls\n preload=\"metadata\"\n src={src}\n className=\"aspect-video w-full rounded-xl bg-bg-sunken\"\n data-testid=\"lesson-video\"\n >\n <track kind=\"captions\" />\n </video>\n ) : (\n <p className=\"rounded-xl border border-dashed border-border-seam px-4 py-3 text-sm text-text-muted\">\n The video for this lesson has not been uploaded yet.\n </p>\n )}\n {notes}\n </div>\n );\n }\n case 'QUIZ':\n return (\n <div className=\"space-y-4\">\n {notes}\n <QuizLesson key={lesson.id} lesson={lesson} onPassed={onProgress} />\n </div>\n );\n case 'INTERACTIVE':\n return (\n <div className=\"grid grid-cols-1 gap-5 xl:grid-cols-2\">\n <div className=\"min-w-0\">\n {notes ?? (\n <p className=\"text-sm text-text-muted\">\n Edit the code, run it, and compare the output with what the lesson expects.\n </p>\n )}\n </div>\n <div className=\"min-w-0\">\n <InteractiveWorkspace key={lesson.id} lesson={lesson} onCompleted={onProgress} />\n </div>\n </div>\n );\n case 'PROJECT':\n case 'ARTICLE':\n default:\n return (\n notes ?? (\n <p className=\"rounded-xl border border-dashed border-border-seam px-4 py-3 text-sm text-text-muted\">\n No content published for this lesson yet.\n </p>\n )\n );\n }\n}\n\n/**\n * Lesson player (BOFF-4107): renders one lesson of a course by kind — article\n * and project briefs as markdown, video through the federated File, quizzes\n * graded server-side, and a runnable sandbox workspace for INTERACTIVE\n * lessons — with the curriculum alongside and per-lesson progress tracking.\n * URL: `/courses/lesson-player-page?id=<courseId>&lessonId=<lessonId>`.\n */\nexport function CoursesLessonPlayerPagePage() {\n const [params] = useSearchParams();\n const courseId = params.get(FOCUS_PARAMS.id) ?? '';\n const lessonId = params.get(FOCUS_PARAMS.lesson) ?? '';\n const navigate = useTechnoSpamNavigate();\n const { workspaceId } = useTechnoSpam();\n const { emit } = useTechnoSpamAnalytics();\n const { data, loading, error, refetch } = useLessonPlayerQuery(courseId, lessonId);\n const [completeLesson, { loading: completing }] = useCompleteLesson();\n const [enroll, { loading: enrolling }] = useEnroll();\n\n useEffect(() => {\n if (lessonId) {\n emit('technospam.lesson.viewed', {\n workspaceId,\n entityId: lessonId,\n entityType: 'TEC_LESSON',\n });\n }\n }, [lessonId, workspaceId, emit]);\n\n const lesson = data?.tecLesson ?? null;\n const course = data?.tecCourse ?? null;\n // BOFF-10481: the learner's own enrollment in this course, from the\n // canonical self-scoped Buildmyiq field (narrowed by the course's canonical\n // id on the server and its Technospam id on the client).\n const { enrollments: myEnrollments, refetch: refetchEnrollment } = useMyEnrollmentsQuery({\n buiCourseId: course?.buiCourseId,\n technospamCourseId: courseId,\n skip: !courseId,\n });\n const enrollment = myEnrollments[0] ?? null;\n const lessons = [...(course?.lessons ?? [])].sort((a, b) => a.position - b.position);\n const index = lessons.findIndex((l) => l.id === lessonId);\n const prev = index > 0 ? lessons[index - 1] : undefined;\n const next = index >= 0 && index < lessons.length - 1 ? lessons[index + 1] : undefined;\n const backPath = courseDetailsPath(courseId);\n\n const refreshProgress = (): void => {\n void refetch();\n void refetchEnrollment();\n };\n\n const handleComplete = async (): Promise<void> => {\n if (!lesson) return;\n const res = await completeLesson({ variables: { lessonId: lesson.id } });\n const updated = res.data?.tecCompleteLesson;\n if (!updated) return;\n emit('technospam.lesson.completed', {\n workspaceId,\n entityId: lesson.id,\n entityType: 'TEC_LESSON',\n });\n notify.success('Lesson completed', `${updated.progressPct}% of the course done`);\n if (next) navigate(lessonPlayerPath(courseId, next.id));\n else await refetch();\n };\n\n const handleEnroll = async (): Promise<void> => {\n const res = await enroll({ variables: { input: { courseId } } });\n if (res.data?.tecEnroll) {\n notify.success(\n 'Enrolled',\n `Your progress in \"${course?.title ?? 'this course'}\" is now tracked.`\n );\n await refetch();\n }\n };\n\n if (!courseId || !lessonId) {\n return (\n <PageLayout title=\"Lesson\" backPath=\"/courses/courses-page\">\n <EmptyView\n title=\"No lesson selected\"\n description=\"Open a lesson from a course curriculum.\"\n />\n </PageLayout>\n );\n }\n\n if (loading && !lesson) {\n return (\n <PageLayout title=\"Lesson\" backPath={backPath}>\n <Skeleton className=\"mb-4 h-10 w-1/2 rounded-xl\" />\n <Skeleton className=\"h-80 w-full rounded-xl\" />\n </PageLayout>\n );\n }\n\n if (error || !lesson || !course || lesson.courseId !== course.id) {\n return (\n <PageLayout title=\"Lesson\" backPath={backPath}>\n {error ? (\n <ErrorView title=\"Couldn't load lesson\" error={error} />\n ) : (\n <EmptyView title=\"Lesson not found\" description=\"This lesson may have been removed.\" />\n )}\n </PageLayout>\n );\n }\n\n const Icon = KIND_ICON[lesson.kind] ?? FileText;\n const done = !!lesson.completedByMe;\n const byCheck = completesByCheck(lesson);\n\n return (\n <PageLayout\n backPath={backPath}\n title={lesson.title}\n description={`${course.title} · Lesson ${index + 1} of ${lessons.length}`}\n actions={\n done ? (\n <span className=\"inline-flex items-center gap-1.5 rounded-full border border-status-success-border bg-status-success-bg-subtle px-3 py-1.5 text-sm font-medium text-status-success-text\">\n <CheckCircle2 size={15} /> Completed\n </span>\n ) : null\n }\n >\n <div className=\"grid grid-cols-1 gap-5 lg:grid-cols-[minmax(0,1fr)_280px]\">\n <div className=\"min-w-0 space-y-4\">\n <div className=\"flex flex-wrap items-center gap-2 text-xs text-text-muted\">\n <span className=\"inline-flex items-center gap-1 rounded-full bg-accent-primary/6 px-2 py-0.5 font-medium text-accent-primary\">\n <Icon size={12} /> {humanizeEnum(lesson.kind)}\n </span>\n <span className=\"inline-flex items-center gap-1\">\n <Clock size={12} /> {formatDuration(lesson.estimatedMinutes)}\n </span>\n {lesson.isPreview ? (\n <span className=\"rounded-full border border-accent-primary/30 px-2 py-0.5\">\n Free preview\n </span>\n ) : null}\n </div>\n\n {!enrollment ? (\n <GlassCard className=\"flex flex-col items-start gap-3 p-4 sm:flex-row sm:items-center sm:justify-between\">\n <p className=\"text-sm text-text-secondary\">\n Enroll in this course to save your progress and earn a certificate.\n </p>\n <Button size=\"sm\" onClick={() => void handleEnroll()} disabled={enrolling}>\n <BookOpen size={14} /> {enrolling ? 'Enrolling…' : 'Enroll'}\n </Button>\n </GlassCard>\n ) : null}\n\n <GlassCard className=\"p-5\" data-testid=\"lesson-player-body\">\n {lesson.isLocked ? (\n <UpgradeGate description=\"This lesson is part of a paid TechnoSpam plan. Its content unlocks once your workspace upgrades.\" />\n ) : (\n <LessonBody lesson={lesson} onProgress={refreshProgress} />\n )}\n </GlassCard>\n\n <div className=\"flex flex-wrap items-center justify-between gap-2\">\n <Button\n variant=\"outline\"\n disabled={!prev}\n onClick={() => prev && navigate(lessonPlayerPath(courseId, prev.id))}\n >\n <ArrowLeft size={15} /> Previous\n </Button>\n <div className=\"flex flex-wrap items-center gap-2\">\n {enrollment && !done && !byCheck && !lesson.isLocked ? (\n <Button onClick={() => void handleComplete()} disabled={completing}>\n <CheckCircle2 size={15} /> {completing ? 'Saving…' : 'Mark complete'}\n </Button>\n ) : null}\n {enrollment && !done && byCheck && !lesson.isLocked ? (\n <span className=\"text-xs text-text-muted\">\n {lesson.kind === 'QUIZ'\n ? 'Pass the quiz to complete this lesson.'\n : 'Run code that prints the expected output to complete this lesson.'}\n </span>\n ) : null}\n <Button\n variant={done || !enrollment ? 'default' : 'outline'}\n disabled={!next}\n onClick={() => next && navigate(lessonPlayerPath(courseId, next.id))}\n >\n Next <ArrowRight size={15} />\n </Button>\n </div>\n </div>\n </div>\n\n <GlassCard className=\"h-fit p-4\">\n <div className=\"mb-2 flex items-center justify-between\">\n <span className=\"text-xs font-semibold uppercase tracking-wide text-text-muted\">\n Curriculum\n </span>\n {enrollment ? (\n <span className=\"text-xs text-text-secondary\" data-testid=\"lesson-player-progress\">\n {enrollment.progressPct}% complete\n </span>\n ) : null}\n </div>\n {enrollment ? (\n <div className=\"mb-3 h-1.5 w-full overflow-hidden rounded-full bg-bg-sunken\">\n <div\n className=\"h-full rounded-full bg-accent-primary transition-all duration-500\"\n style={{ width: `${enrollment.progressPct}%` }}\n />\n </div>\n ) : null}\n <ol className=\"space-y-1\">\n {lessons.map((l, i) => {\n const LIcon = KIND_ICON[l.kind] ?? FileText;\n const current = l.id === lesson.id;\n return (\n <li key={l.id}>\n <button\n type=\"button\"\n aria-current={current ? 'page' : undefined}\n onClick={() => navigate(lessonPlayerPath(courseId, l.id))}\n className={cn(\n 'flex w-full items-center gap-2 rounded-lg px-2 py-1.5 text-left text-sm transition-colors hover:bg-bg-sunken',\n current && 'bg-accent-primary/6 font-medium text-accent-primary'\n )}\n >\n <span className=\"w-5 shrink-0 text-center text-xs tabular-nums text-text-muted\">\n {i + 1}\n </span>\n {l.completedByMe ? (\n <CheckCircle2 size={14} className=\"shrink-0 text-status-success-text\" />\n ) : l.isLocked ? (\n <Lock size={14} className=\"shrink-0 text-text-muted\" />\n ) : (\n <LIcon size={14} className=\"shrink-0 text-text-muted\" />\n )}\n <span className=\"min-w-0 flex-1 truncate\">{l.title}</span>\n </button>\n </li>\n );\n })}\n </ol>\n </GlassCard>\n </div>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"CoursesLessonPlayerPagePage.js","names":[],"sources":["../../../src/technospam/pages/CoursesLessonPlayerPagePage.tsx"],"sourcesContent":["import { useEffect } from 'react';\nimport { useSearchParams } from 'react-router';\nimport { Markdown } from '@burdenoff/fe-libs/shared/components/Markdown';\nimport {\n ArrowLeft,\n ArrowRight,\n BookOpen,\n CheckCircle2,\n Clock,\n FileText,\n Layers,\n Lock,\n PlayCircle,\n Sparkles,\n type LucideIcon,\n} from 'lucide-react';\nimport { Button, GlassCard, Skeleton } from '@burdenoff/fe-libs/ui';\nimport { EmptyView, ErrorView, PageLayout, UpgradeGate } from '../components';\nimport { InteractiveWorkspace } from '../components/lesson/InteractiveWorkspace';\nimport { QuizLesson } from '../components/lesson/QuizLesson';\nimport { useTechnoSpamNavigate, useTechnoSpam } from '../providers/TechnoSpamProvider';\nimport {\n useCompleteLesson,\n useEnroll,\n useLessonPlayerQuery,\n useMyEnrollmentsQuery,\n type TecLesson,\n type TecLessonKind,\n} from '../api/technospamApi';\nimport { formatDuration, humanizeEnum } from '../utils/format';\nimport { notify } from '../lib/toast';\nimport { cn } from '../utils/cn';\nimport { useTechnoSpamAnalytics } from '../hooks/useTechnoSpamAnalytics';\nimport { FOCUS_PARAMS } from '../utils/pageContext';\nimport { courseDetailsPath, lessonPlayerPath } from '../utils/lessonPath';\n\nconst KIND_ICON: Record<TecLessonKind, LucideIcon> = {\n VIDEO: PlayCircle,\n ARTICLE: FileText,\n INTERACTIVE: Sparkles,\n QUIZ: CheckCircle2,\n PROJECT: Layers,\n};\n\n/** Lessons whose completion is earned (a passed quiz, a matching run) rather than self-reported. */\nfunction completesByCheck(lesson: TecLesson): boolean {\n if (lesson.kind === 'QUIZ') return (lesson.quizQuestions?.length ?? 0) > 0;\n if (lesson.kind === 'INTERACTIVE') return !!lesson.expectedOutput;\n return false;\n}\n\nfunction LessonBody({ lesson, onProgress }: { lesson: TecLesson; onProgress: () => void }) {\n const notes = lesson.content ? (\n <Markdown className=\"prose prose-sm max-w-none text-text-secondary dark:prose-invert\">\n {lesson.content}\n </Markdown>\n ) : null;\n\n switch (lesson.kind) {\n case 'VIDEO': {\n const src = lesson.file?.downloadUrl;\n const isVideo = !lesson.file?.mimeType || lesson.file.mimeType.startsWith('video/');\n return (\n <div className=\"space-y-4\">\n {src && isVideo ? (\n <video\n controls\n preload=\"metadata\"\n src={src}\n className=\"aspect-video w-full rounded-xl bg-bg-sunken\"\n data-testid=\"lesson-video\"\n >\n <track kind=\"captions\" />\n </video>\n ) : (\n <p className=\"rounded-xl border border-dashed border-border-seam px-4 py-3 text-sm text-text-muted\">\n The video for this lesson has not been uploaded yet.\n </p>\n )}\n {notes}\n </div>\n );\n }\n case 'QUIZ':\n return (\n <div className=\"space-y-4\">\n {notes}\n <QuizLesson key={lesson.id} lesson={lesson} onPassed={onProgress} />\n </div>\n );\n case 'INTERACTIVE':\n return (\n <div className=\"grid grid-cols-1 gap-5 xl:grid-cols-2\">\n <div className=\"min-w-0\">\n {notes ?? (\n <p className=\"text-sm text-text-muted\">\n Edit the code, run it, and compare the output with what the lesson expects.\n </p>\n )}\n </div>\n <div className=\"min-w-0\">\n <InteractiveWorkspace key={lesson.id} lesson={lesson} onCompleted={onProgress} />\n </div>\n </div>\n );\n case 'PROJECT':\n case 'ARTICLE':\n default:\n return (\n notes ?? (\n <p className=\"rounded-xl border border-dashed border-border-seam px-4 py-3 text-sm text-text-muted\">\n No content published for this lesson yet.\n </p>\n )\n );\n }\n}\n\n/**\n * Lesson player (BOFF-4107): renders one lesson of a course by kind — article\n * and project briefs as markdown, video through the federated File, quizzes\n * graded server-side, and a runnable sandbox workspace for INTERACTIVE\n * lessons — with the curriculum alongside and per-lesson progress tracking.\n * URL: `/courses/lesson-player-page?id=<courseId>&lessonId=<lessonId>`.\n */\nexport function CoursesLessonPlayerPagePage() {\n const [params] = useSearchParams();\n const courseId = params.get(FOCUS_PARAMS.id) ?? '';\n const lessonId = params.get(FOCUS_PARAMS.lesson) ?? '';\n const navigate = useTechnoSpamNavigate();\n const { workspaceId } = useTechnoSpam();\n const { emit } = useTechnoSpamAnalytics();\n const { data, loading, error, refetch } = useLessonPlayerQuery(courseId, lessonId);\n const [completeLesson, { loading: completing }] = useCompleteLesson();\n const [enroll, { loading: enrolling }] = useEnroll();\n\n useEffect(() => {\n if (lessonId) {\n emit('technospam.lesson.viewed', {\n workspaceId,\n entityId: lessonId,\n entityType: 'TEC_LESSON',\n });\n }\n }, [lessonId, workspaceId, emit]);\n\n const lesson = data?.tecLesson ?? null;\n const course = data?.tecCourse ?? null;\n // BOFF-10481: the learner's own enrollment in this course, from the\n // canonical self-scoped Buildmyiq field (narrowed by the course's canonical\n // id on the server and its Technospam id on the client).\n const { enrollments: myEnrollments, refetch: refetchEnrollment } = useMyEnrollmentsQuery({\n buiCourseId: course?.buiCourseId,\n technospamCourseId: courseId,\n skip: !courseId,\n });\n const enrollment = myEnrollments[0] ?? null;\n const lessons = [...(course?.lessons ?? [])].sort((a, b) => a.position - b.position);\n const index = lessons.findIndex((l) => l.id === lessonId);\n const prev = index > 0 ? lessons[index - 1] : undefined;\n const next = index >= 0 && index < lessons.length - 1 ? lessons[index + 1] : undefined;\n const backPath = courseDetailsPath(courseId);\n\n const refreshProgress = (): void => {\n void refetch();\n void refetchEnrollment();\n };\n\n const handleComplete = async (): Promise<void> => {\n if (!lesson) return;\n const res = await completeLesson({ variables: { lessonId: lesson.id } });\n const updated = res.data?.tecCompleteLesson;\n if (!updated) return;\n emit('technospam.lesson.completed', {\n workspaceId,\n entityId: lesson.id,\n entityType: 'TEC_LESSON',\n });\n notify.success('Lesson completed', `${updated.progressPct}% of the course done`);\n if (next) navigate(lessonPlayerPath(courseId, next.id));\n else await refetch();\n };\n\n const handleEnroll = async (): Promise<void> => {\n const res = await enroll({ variables: { input: { courseId } } });\n if (res.data?.tecEnroll) {\n notify.success(\n 'Enrolled',\n `Your progress in \"${course?.title ?? 'this course'}\" is now tracked.`\n );\n await refetch();\n }\n };\n\n if (!courseId || !lessonId) {\n return (\n <PageLayout title=\"Lesson\" backPath=\"/courses/courses-page\">\n <EmptyView\n title=\"No lesson selected\"\n description=\"Open a lesson from a course curriculum.\"\n />\n </PageLayout>\n );\n }\n\n if (loading && !lesson) {\n return (\n <PageLayout title=\"Lesson\" backPath={backPath}>\n <Skeleton className=\"mb-4 h-10 w-1/2 rounded-xl\" />\n <Skeleton className=\"h-80 w-full rounded-xl\" />\n </PageLayout>\n );\n }\n\n if (error || !lesson || !course || lesson.courseId !== course.id) {\n return (\n <PageLayout title=\"Lesson\" backPath={backPath}>\n {error ? (\n <ErrorView title=\"Couldn't load lesson\" error={error} />\n ) : (\n <EmptyView title=\"Lesson not found\" description=\"This lesson may have been removed.\" />\n )}\n </PageLayout>\n );\n }\n\n const Icon = KIND_ICON[lesson.kind] ?? FileText;\n const done = !!lesson.completedByMe;\n const byCheck = completesByCheck(lesson);\n\n return (\n <PageLayout\n backPath={backPath}\n title={lesson.title}\n description={`${course.title} · Lesson ${index + 1} of ${lessons.length}`}\n actions={\n done ? (\n <span className=\"inline-flex items-center gap-1.5 rounded-full border border-status-success-border bg-status-success-bg-subtle px-3 py-1.5 text-sm font-medium text-status-success-text\">\n <CheckCircle2 size={15} /> Completed\n </span>\n ) : null\n }\n >\n <div className=\"grid grid-cols-1 gap-5 lg:grid-cols-[minmax(0,1fr)_280px]\">\n <div className=\"min-w-0 space-y-4\">\n <div className=\"flex flex-wrap items-center gap-2 text-xs text-text-muted\">\n <span className=\"inline-flex items-center gap-1 rounded-full bg-accent-primary/6 px-2 py-0.5 font-medium text-accent-primary\">\n <Icon size={12} /> {humanizeEnum(lesson.kind)}\n </span>\n <span className=\"inline-flex items-center gap-1\">\n <Clock size={12} /> {formatDuration(lesson.estimatedMinutes)}\n </span>\n {lesson.isPreview ? (\n <span className=\"rounded-full border border-accent-primary/30 px-2 py-0.5\">\n Free preview\n </span>\n ) : null}\n </div>\n\n {!enrollment ? (\n <GlassCard className=\"flex flex-col items-start gap-3 p-4 sm:flex-row sm:items-center sm:justify-between\">\n <p className=\"text-sm text-text-secondary\">\n Enroll in this course to save your progress and earn a certificate.\n </p>\n <Button size=\"sm\" onClick={() => void handleEnroll()} disabled={enrolling}>\n <BookOpen size={14} /> {enrolling ? 'Enrolling…' : 'Enroll'}\n </Button>\n </GlassCard>\n ) : null}\n\n <GlassCard className=\"p-5\" data-testid=\"lesson-player-body\">\n {lesson.isLocked ? (\n <UpgradeGate description=\"This lesson is part of a paid TechnoSpam plan. Its content unlocks once your workspace upgrades.\" />\n ) : (\n <LessonBody lesson={lesson} onProgress={refreshProgress} />\n )}\n </GlassCard>\n\n <div className=\"flex flex-wrap items-center justify-between gap-2\">\n <Button\n variant=\"outline\"\n disabled={!prev}\n onClick={() => prev && navigate(lessonPlayerPath(courseId, prev.id))}\n >\n <ArrowLeft size={15} /> Previous\n </Button>\n <div className=\"flex flex-wrap items-center gap-2\">\n {enrollment && !done && !byCheck && !lesson.isLocked ? (\n <Button onClick={() => void handleComplete()} disabled={completing}>\n <CheckCircle2 size={15} /> {completing ? 'Saving…' : 'Mark complete'}\n </Button>\n ) : null}\n {enrollment && !done && byCheck && !lesson.isLocked ? (\n <span className=\"text-xs text-text-muted\">\n {lesson.kind === 'QUIZ'\n ? 'Pass the quiz to complete this lesson.'\n : 'Run code that prints the expected output to complete this lesson.'}\n </span>\n ) : null}\n <Button\n variant={done || !enrollment ? 'default' : 'outline'}\n disabled={!next}\n onClick={() => next && navigate(lessonPlayerPath(courseId, next.id))}\n >\n Next <ArrowRight size={15} />\n </Button>\n </div>\n </div>\n </div>\n\n <GlassCard className=\"h-fit p-4\">\n <div className=\"mb-2 flex items-center justify-between\">\n <span className=\"text-xs font-semibold uppercase tracking-wide text-text-muted\">\n Curriculum\n </span>\n {enrollment ? (\n <span className=\"text-xs text-text-secondary\" data-testid=\"lesson-player-progress\">\n {enrollment.progressPct}% complete\n </span>\n ) : null}\n </div>\n {enrollment ? (\n <div className=\"mb-3 h-1.5 w-full overflow-hidden rounded-full bg-bg-sunken\">\n <div\n className=\"h-full rounded-full bg-accent-primary transition-all duration-500\"\n style={{ width: `${enrollment.progressPct}%` }}\n />\n </div>\n ) : null}\n <ol className=\"space-y-1\">\n {lessons.map((l, i) => {\n const LIcon = KIND_ICON[l.kind] ?? FileText;\n const current = l.id === lesson.id;\n return (\n <li key={l.id}>\n <button\n type=\"button\"\n aria-current={current ? 'page' : undefined}\n onClick={() => navigate(lessonPlayerPath(courseId, l.id))}\n className={cn(\n 'flex w-full items-center gap-2 rounded-lg px-2 py-1.5 text-left text-sm transition-colors hover:bg-bg-sunken',\n current && 'bg-accent-primary/6 font-medium text-accent-primary'\n )}\n >\n <span className=\"w-5 shrink-0 text-center text-xs tabular-nums text-text-muted\">\n {i + 1}\n </span>\n {l.completedByMe ? (\n <CheckCircle2 size={14} className=\"shrink-0 text-status-success-text\" />\n ) : l.isLocked ? (\n <Lock size={14} className=\"shrink-0 text-text-muted\" />\n ) : (\n <LIcon size={14} className=\"shrink-0 text-text-muted\" />\n )}\n <span className=\"min-w-0 flex-1 truncate\">{l.title}</span>\n </button>\n </li>\n );\n })}\n </ol>\n </GlassCard>\n </div>\n </PageLayout>\n );\n}\n"],"mappings":"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"}