@burdenoff/microfe-technospam 2026.1031.6 → 2026.1031.8

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/generated/wspace-types.d.ts +1100 -147
  2. package/dist/generated/wspace-types.d.ts.map +1 -1
  3. package/dist/stories/fixtures/technospamFixtures.d.ts.map +1 -1
  4. package/dist/technospam/api/canonicalLearning.d.ts +53 -0
  5. package/dist/technospam/api/canonicalLearning.d.ts.map +1 -0
  6. package/dist/technospam/api/canonicalLearning.js +57 -0
  7. package/dist/technospam/api/canonicalLearning.js.map +1 -0
  8. package/dist/technospam/api/technospamApi.d.ts +392 -16
  9. package/dist/technospam/api/technospamApi.d.ts.map +1 -1
  10. package/dist/technospam/api/technospamApi.js +367 -304
  11. package/dist/technospam/api/technospamApi.js.map +1 -1
  12. package/dist/technospam/pages/ApplicationsPortfolioPagePage.d.ts.map +1 -1
  13. package/dist/technospam/pages/ApplicationsPortfolioPagePage.js +26 -26
  14. package/dist/technospam/pages/ApplicationsPortfolioPagePage.js.map +1 -1
  15. package/dist/technospam/pages/CoursesCourseDetailsPagePage.d.ts.map +1 -1
  16. package/dist/technospam/pages/CoursesCourseDetailsPagePage.js +196 -192
  17. package/dist/technospam/pages/CoursesCourseDetailsPagePage.js.map +1 -1
  18. package/dist/technospam/pages/CoursesLessonPlayerPagePage.d.ts.map +1 -1
  19. package/dist/technospam/pages/CoursesLessonPlayerPagePage.js +125 -121
  20. package/dist/technospam/pages/CoursesLessonPlayerPagePage.js.map +1 -1
  21. package/dist/technospam/pages/CoursesMyLearningPagePage.d.ts.map +1 -1
  22. package/dist/technospam/pages/CoursesMyLearningPagePage.js +95 -95
  23. package/dist/technospam/pages/CoursesMyLearningPagePage.js.map +1 -1
  24. package/dist/technospam/pages/DashboardDashboardPagePage.d.ts.map +1 -1
  25. package/dist/technospam/pages/DashboardDashboardPagePage.js +30 -30
  26. package/dist/technospam/pages/DashboardDashboardPagePage.js.map +1 -1
  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 // Self-scoped read: `tecMyCertificates` filters by the authenticated actor\n // server-side. `tecListCertificates` (the previous call) is gated behind the\n // cross-learner \"list-all\" action, which a plain learner does not hold — so\n // an already-claimed certificate came back empty and the page kept offering\n // \"Claim certificate\" instead of showing it.\n const { data: certData, refetch: refetchCerts } = useMyCertificatesQuery(\n id ? { courseId: id } : {}\n );\n const [issueCertificate, { loading: claiming }] = useIssueCertificate();\n\n const course = data?.tecCourse ?? null;\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 myCertificates = certData?.tecMyCertificates ?? [];\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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"}
@@ -1 +1 @@
1
- {"version":3,"file":"CoursesLessonPlayerPagePage.d.ts","sourceRoot":"","sources":["../../../src/technospam/pages/CoursesLessonPlayerPagePage.tsx"],"names":[],"mappings":"AAqHA;;;;;;GAMG;AACH,wBAAgB,2BAA2B,gCAsO1C"}
1
+ {"version":3,"file":"CoursesLessonPlayerPagePage.d.ts","sourceRoot":"","sources":["../../../src/technospam/pages/CoursesLessonPlayerPagePage.tsx"],"names":[],"mappings":"AAsHA;;;;;;GAMG;AACH,wBAAgB,2BAA2B,gCA+O1C"}
@@ -2,277 +2,281 @@ import { useTechnoSpam as e, useTechnoSpamNavigate as t } from "../providers/Tec
2
2
  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
- import { formatDuration as a, humanizeEnum as o } from "../utils/format.js";
6
- import { EmptyView as s, ErrorView as c } from "../components/StateViews.js";
7
- import { UpgradeGate as l } from "../components/UpgradeGate.js";
8
- import { notify as u } from "../lib/toast.js";
9
- import { useCompleteLesson as d, useEnroll as f, useLessonPlayerQuery as p } from "../api/technospamApi.js";
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";
10
10
  import "../components/index.js";
11
- import { FOCUS_PARAMS as m } from "../utils/pageContext.js";
12
- import { courseDetailsPath as h, lessonPlayerPath as g } from "../utils/lessonPath.js";
13
- import { InteractiveWorkspace as _ } from "../components/lesson/InteractiveWorkspace.js";
14
- import { QuizLesson as v } from "../components/lesson/QuizLesson.js";
15
- import { useEffect as ee } from "react";
16
- import { ArrowLeft as te, ArrowRight as y, BookOpen as b, CheckCircle2 as x, Clock as S, FileText as C, Layers as w, Lock as ne, PlayCircle as T, Sparkles as E } from "lucide-react";
17
- import { useSearchParams as re } from "react-router";
18
- import { jsx as D, jsxs as O } from "react/jsx-runtime";
19
- import { Button as k, GlassCard as A, Skeleton as j } from "@burdenoff/fe-libs/ui";
20
- import { Markdown as M } from "@burdenoff/fe-libs/shared/components/Markdown";
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";
17
+ import { useSearchParams as T } from "react-router";
18
+ import { jsx as E, jsxs as D } from "react/jsx-runtime";
19
+ import { Button as O, GlassCard as k, Skeleton as A } from "@burdenoff/fe-libs/ui";
20
+ import { Markdown as j } from "@burdenoff/fe-libs/shared/components/Markdown";
21
21
  //#region src/technospam/pages/CoursesLessonPlayerPagePage.tsx
22
- var N = {
23
- VIDEO: T,
24
- ARTICLE: C,
25
- INTERACTIVE: E,
26
- QUIZ: x,
27
- PROJECT: w
22
+ var M = {
23
+ VIDEO: C,
24
+ ARTICLE: x,
25
+ INTERACTIVE: w,
26
+ QUIZ: y,
27
+ PROJECT: S
28
28
  };
29
- function P(e) {
29
+ function oe(e) {
30
30
  return e.kind === "QUIZ" ? (e.quizQuestions?.length ?? 0) > 0 : e.kind === "INTERACTIVE" ? !!e.expectedOutput : !1;
31
31
  }
32
- function F({ lesson: e, onProgress: t }) {
33
- let n = e.content ? /* @__PURE__ */ D(M, {
32
+ function N({ lesson: e, onProgress: t }) {
33
+ let n = e.content ? /* @__PURE__ */ E(j, {
34
34
  className: "prose prose-sm max-w-none text-text-secondary dark:prose-invert",
35
35
  children: e.content
36
36
  }) : null;
37
37
  switch (e.kind) {
38
38
  case "VIDEO": {
39
39
  let t = e.file?.downloadUrl, r = !e.file?.mimeType || e.file.mimeType.startsWith("video/");
40
- return /* @__PURE__ */ O("div", {
40
+ return /* @__PURE__ */ D("div", {
41
41
  className: "space-y-4",
42
- children: [t && r ? /* @__PURE__ */ D("video", {
42
+ children: [t && r ? /* @__PURE__ */ E("video", {
43
43
  controls: !0,
44
44
  preload: "metadata",
45
45
  src: t,
46
46
  className: "aspect-video w-full rounded-xl bg-bg-sunken",
47
47
  "data-testid": "lesson-video",
48
- children: /* @__PURE__ */ D("track", { kind: "captions" })
49
- }) : /* @__PURE__ */ D("p", {
48
+ children: /* @__PURE__ */ E("track", { kind: "captions" })
49
+ }) : /* @__PURE__ */ E("p", {
50
50
  className: "rounded-xl border border-dashed border-border-seam px-4 py-3 text-sm text-text-muted",
51
51
  children: "The video for this lesson has not been uploaded yet."
52
52
  }), n]
53
53
  });
54
54
  }
55
- case "QUIZ": return /* @__PURE__ */ O("div", {
55
+ case "QUIZ": return /* @__PURE__ */ D("div", {
56
56
  className: "space-y-4",
57
- children: [n, /* @__PURE__ */ D(v, {
57
+ children: [n, /* @__PURE__ */ E(g, {
58
58
  lesson: e,
59
59
  onPassed: t
60
60
  }, e.id)]
61
61
  });
62
- case "INTERACTIVE": return /* @__PURE__ */ O("div", {
62
+ case "INTERACTIVE": return /* @__PURE__ */ D("div", {
63
63
  className: "grid grid-cols-1 gap-5 xl:grid-cols-2",
64
- children: [/* @__PURE__ */ D("div", {
64
+ children: [/* @__PURE__ */ E("div", {
65
65
  className: "min-w-0",
66
- children: n ?? /* @__PURE__ */ D("p", {
66
+ children: n ?? /* @__PURE__ */ E("p", {
67
67
  className: "text-sm text-text-muted",
68
68
  children: "Edit the code, run it, and compare the output with what the lesson expects."
69
69
  })
70
- }), /* @__PURE__ */ D("div", {
70
+ }), /* @__PURE__ */ E("div", {
71
71
  className: "min-w-0",
72
- children: /* @__PURE__ */ D(_, {
72
+ children: /* @__PURE__ */ E(h, {
73
73
  lesson: e,
74
74
  onCompleted: t
75
75
  }, e.id)
76
76
  })]
77
77
  });
78
- default: return n ?? /* @__PURE__ */ D("p", {
78
+ default: return n ?? /* @__PURE__ */ E("p", {
79
79
  className: "rounded-xl border border-dashed border-border-seam px-4 py-3 text-sm text-text-muted",
80
80
  children: "No content published for this lesson yet."
81
81
  });
82
82
  }
83
83
  }
84
- function I() {
85
- let [_] = re(), v = _.get(m.id) ?? "", w = _.get(m.lesson) ?? "", T = t(), { workspaceId: E } = e(), { emit: M } = n(), { data: I, loading: L, error: R, refetch: z } = p(v, w), [B, { loading: V }] = d(), [H, { loading: U }] = f();
86
- ee(() => {
87
- w && M("technospam.lesson.viewed", {
88
- workspaceId: E,
89
- entityId: w,
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(() => {
87
+ S && j("technospam.lesson.viewed", {
88
+ workspaceId: w,
89
+ entityId: S,
90
90
  entityType: "TEC_LESSON"
91
91
  });
92
92
  }, [
93
+ S,
93
94
  w,
94
- E,
95
- M
95
+ j
96
96
  ]);
97
- let W = I?.tecLesson ?? null, G = I?.tecCourse ?? null, K = I?.tecMyEnrollments?.items?.[0] ?? null, q = [...G?.lessons ?? []].sort((e, t) => e.position - t.position), J = q.findIndex((e) => e.id === w), Y = J > 0 ? q[J - 1] : void 0, X = J >= 0 && J < q.length - 1 ? q[J + 1] : void 0, Z = h(v), ie = () => {
98
- z();
99
- }, ae = async () => {
100
- if (!W) return;
101
- let e = (await B({ variables: { lessonId: W.id } })).data?.tecCompleteLesson;
102
- e && (M("technospam.lesson.completed", {
103
- workspaceId: E,
104
- entityId: W.id,
97
+ let H = P?.tecLesson ?? null, U = P?.tecCourse ?? null, { enrollments: W, refetch: G } = d({
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 = () => {
102
+ L(), G();
103
+ }, ce = async () => {
104
+ if (!H) return;
105
+ let e = (await R({ variables: { lessonId: H.id } })).data?.tecCompleteLesson;
106
+ e && (j("technospam.lesson.completed", {
107
+ workspaceId: w,
108
+ entityId: H.id,
105
109
  entityType: "TEC_LESSON"
106
- }), u.success("Lesson completed", `${e.progressPct}% of the course done`), X ? T(g(v, X.id)) : await z());
107
- }, oe = async () => {
108
- (await H({ variables: { input: { courseId: v } } })).data?.tecEnroll && (u.success("Enrolled", `Your progress in "${G?.title ?? "this course"}" is now tracked.`), await z());
110
+ }), c.success("Lesson completed", `${e.progressPct}% of the course done`), X ? C(m(g, X.id)) : await L());
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());
109
113
  };
110
- if (!v || !w) return /* @__PURE__ */ D(i, {
114
+ if (!g || !S) return /* @__PURE__ */ E(i, {
111
115
  title: "Lesson",
112
116
  backPath: "/courses/courses-page",
113
- children: /* @__PURE__ */ D(s, {
117
+ children: /* @__PURE__ */ E(o, {
114
118
  title: "No lesson selected",
115
119
  description: "Open a lesson from a course curriculum."
116
120
  })
117
121
  });
118
- if (L && !W) return /* @__PURE__ */ O(i, {
122
+ if (F && !H) return /* @__PURE__ */ D(i, {
119
123
  title: "Lesson",
120
124
  backPath: Z,
121
- children: [/* @__PURE__ */ D(j, { className: "mb-4 h-10 w-1/2 rounded-xl" }), /* @__PURE__ */ D(j, { className: "h-80 w-full rounded-xl" })]
125
+ children: [/* @__PURE__ */ E(A, { className: "mb-4 h-10 w-1/2 rounded-xl" }), /* @__PURE__ */ E(A, { className: "h-80 w-full rounded-xl" })]
122
126
  });
123
- if (R || !W || !G || W.courseId !== G.id) return /* @__PURE__ */ D(i, {
127
+ if (I || !H || !U || H.courseId !== U.id) return /* @__PURE__ */ E(i, {
124
128
  title: "Lesson",
125
129
  backPath: Z,
126
- children: R ? /* @__PURE__ */ D(c, {
130
+ children: I ? /* @__PURE__ */ E(te, {
127
131
  title: "Couldn't load lesson",
128
- error: R
129
- }) : /* @__PURE__ */ D(s, {
132
+ error: I
133
+ }) : /* @__PURE__ */ E(o, {
130
134
  title: "Lesson not found",
131
135
  description: "This lesson may have been removed."
132
136
  })
133
137
  });
134
- let se = N[W.kind] ?? C, Q = !!W.completedByMe, $ = P(W);
135
- return /* @__PURE__ */ D(i, {
138
+ let ue = M[H.kind] ?? x, Q = !!H.completedByMe, $ = oe(H);
139
+ return /* @__PURE__ */ E(i, {
136
140
  backPath: Z,
137
- title: W.title,
138
- description: `${G.title} · Lesson ${J + 1} of ${q.length}`,
139
- actions: Q ? /* @__PURE__ */ O("span", {
141
+ title: H.title,
142
+ description: `${U.title} · Lesson ${J + 1} of ${q.length}`,
143
+ actions: Q ? /* @__PURE__ */ D("span", {
140
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",
141
- children: [/* @__PURE__ */ D(x, { size: 15 }), " Completed"]
145
+ children: [/* @__PURE__ */ E(y, { size: 15 }), " Completed"]
142
146
  }) : null,
143
- children: /* @__PURE__ */ O("div", {
147
+ children: /* @__PURE__ */ D("div", {
144
148
  className: "grid grid-cols-1 gap-5 lg:grid-cols-[minmax(0,1fr)_280px]",
145
- children: [/* @__PURE__ */ O("div", {
149
+ children: [/* @__PURE__ */ D("div", {
146
150
  className: "min-w-0 space-y-4",
147
151
  children: [
148
- /* @__PURE__ */ O("div", {
152
+ /* @__PURE__ */ D("div", {
149
153
  className: "flex flex-wrap items-center gap-2 text-xs text-text-muted",
150
154
  children: [
151
- /* @__PURE__ */ O("span", {
155
+ /* @__PURE__ */ D("span", {
152
156
  className: "inline-flex items-center gap-1 rounded-full bg-accent-primary/6 px-2 py-0.5 font-medium text-accent-primary",
153
157
  children: [
154
- /* @__PURE__ */ D(se, { size: 12 }),
158
+ /* @__PURE__ */ E(ue, { size: 12 }),
155
159
  " ",
156
- o(W.kind)
160
+ a(H.kind)
157
161
  ]
158
162
  }),
159
- /* @__PURE__ */ O("span", {
163
+ /* @__PURE__ */ D("span", {
160
164
  className: "inline-flex items-center gap-1",
161
165
  children: [
162
- /* @__PURE__ */ D(S, { size: 12 }),
166
+ /* @__PURE__ */ E(b, { size: 12 }),
163
167
  " ",
164
- a(W.estimatedMinutes)
168
+ ee(H.estimatedMinutes)
165
169
  ]
166
170
  }),
167
- W.isPreview ? /* @__PURE__ */ D("span", {
171
+ H.isPreview ? /* @__PURE__ */ E("span", {
168
172
  className: "rounded-full border border-accent-primary/30 px-2 py-0.5",
169
173
  children: "Free preview"
170
174
  }) : null
171
175
  ]
172
176
  }),
173
- K ? null : /* @__PURE__ */ O(A, {
177
+ K ? null : /* @__PURE__ */ D(k, {
174
178
  className: "flex flex-col items-start gap-3 p-4 sm:flex-row sm:items-center sm:justify-between",
175
- children: [/* @__PURE__ */ D("p", {
179
+ children: [/* @__PURE__ */ E("p", {
176
180
  className: "text-sm text-text-secondary",
177
181
  children: "Enroll in this course to save your progress and earn a certificate."
178
- }), /* @__PURE__ */ O(k, {
182
+ }), /* @__PURE__ */ D(O, {
179
183
  size: "sm",
180
- onClick: () => void oe(),
181
- disabled: U,
184
+ onClick: () => void le(),
185
+ disabled: V,
182
186
  children: [
183
- /* @__PURE__ */ D(b, { size: 14 }),
187
+ /* @__PURE__ */ E(v, { size: 14 }),
184
188
  " ",
185
- U ? "Enrolling…" : "Enroll"
189
+ V ? "Enrolling…" : "Enroll"
186
190
  ]
187
191
  })]
188
192
  }),
189
- /* @__PURE__ */ D(A, {
193
+ /* @__PURE__ */ E(k, {
190
194
  className: "p-5",
191
195
  "data-testid": "lesson-player-body",
192
- children: W.isLocked ? /* @__PURE__ */ D(l, { description: "This lesson is part of a paid TechnoSpam plan. Its content unlocks once your workspace upgrades." }) : /* @__PURE__ */ D(F, {
193
- lesson: W,
194
- onProgress: ie
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, {
197
+ lesson: H,
198
+ onProgress: se
195
199
  })
196
200
  }),
197
- /* @__PURE__ */ O("div", {
201
+ /* @__PURE__ */ D("div", {
198
202
  className: "flex flex-wrap items-center justify-between gap-2",
199
- children: [/* @__PURE__ */ O(k, {
203
+ children: [/* @__PURE__ */ D(O, {
200
204
  variant: "outline",
201
205
  disabled: !Y,
202
- onClick: () => Y && T(g(v, Y.id)),
203
- children: [/* @__PURE__ */ D(te, { size: 15 }), " Previous"]
204
- }), /* @__PURE__ */ O("div", {
206
+ onClick: () => Y && C(m(g, Y.id)),
207
+ children: [/* @__PURE__ */ E(ie, { size: 15 }), " Previous"]
208
+ }), /* @__PURE__ */ D("div", {
205
209
  className: "flex flex-wrap items-center gap-2",
206
210
  children: [
207
- K && !Q && !$ && !W.isLocked ? /* @__PURE__ */ O(k, {
208
- onClick: () => void ae(),
209
- disabled: V,
211
+ K && !Q && !$ && !H.isLocked ? /* @__PURE__ */ D(O, {
212
+ onClick: () => void ce(),
213
+ disabled: z,
210
214
  children: [
211
- /* @__PURE__ */ D(x, { size: 15 }),
215
+ /* @__PURE__ */ E(y, { size: 15 }),
212
216
  " ",
213
- V ? "Saving…" : "Mark complete"
217
+ z ? "Saving…" : "Mark complete"
214
218
  ]
215
219
  }) : null,
216
- K && !Q && $ && !W.isLocked ? /* @__PURE__ */ D("span", {
220
+ K && !Q && $ && !H.isLocked ? /* @__PURE__ */ E("span", {
217
221
  className: "text-xs text-text-muted",
218
- children: W.kind === "QUIZ" ? "Pass the quiz to complete this lesson." : "Run code that prints the expected output to complete this lesson."
222
+ children: H.kind === "QUIZ" ? "Pass the quiz to complete this lesson." : "Run code that prints the expected output to complete this lesson."
219
223
  }) : null,
220
- /* @__PURE__ */ O(k, {
224
+ /* @__PURE__ */ D(O, {
221
225
  variant: Q || !K ? "default" : "outline",
222
226
  disabled: !X,
223
- onClick: () => X && T(g(v, X.id)),
224
- children: ["Next ", /* @__PURE__ */ D(y, { size: 15 })]
227
+ onClick: () => X && C(m(g, X.id)),
228
+ children: ["Next ", /* @__PURE__ */ E(_, { size: 15 })]
225
229
  })
226
230
  ]
227
231
  })]
228
232
  })
229
233
  ]
230
- }), /* @__PURE__ */ O(A, {
234
+ }), /* @__PURE__ */ D(k, {
231
235
  className: "h-fit p-4",
232
236
  children: [
233
- /* @__PURE__ */ O("div", {
237
+ /* @__PURE__ */ D("div", {
234
238
  className: "mb-2 flex items-center justify-between",
235
- children: [/* @__PURE__ */ D("span", {
239
+ children: [/* @__PURE__ */ E("span", {
236
240
  className: "text-xs font-semibold uppercase tracking-wide text-text-muted",
237
241
  children: "Curriculum"
238
- }), K ? /* @__PURE__ */ O("span", {
242
+ }), K ? /* @__PURE__ */ D("span", {
239
243
  className: "text-xs text-text-secondary",
240
244
  "data-testid": "lesson-player-progress",
241
245
  children: [K.progressPct, "% complete"]
242
246
  }) : null]
243
247
  }),
244
- K ? /* @__PURE__ */ D("div", {
248
+ K ? /* @__PURE__ */ E("div", {
245
249
  className: "mb-3 h-1.5 w-full overflow-hidden rounded-full bg-bg-sunken",
246
- children: /* @__PURE__ */ D("div", {
250
+ children: /* @__PURE__ */ E("div", {
247
251
  className: "h-full rounded-full bg-accent-primary transition-all duration-500",
248
252
  style: { width: `${K.progressPct}%` }
249
253
  })
250
254
  }) : null,
251
- /* @__PURE__ */ D("ol", {
255
+ /* @__PURE__ */ E("ol", {
252
256
  className: "space-y-1",
253
257
  children: q.map((e, t) => {
254
- let n = N[e.kind] ?? C, i = e.id === W.id;
255
- return /* @__PURE__ */ D("li", { children: /* @__PURE__ */ O("button", {
258
+ let n = M[e.kind] ?? x, i = e.id === H.id;
259
+ return /* @__PURE__ */ E("li", { children: /* @__PURE__ */ D("button", {
256
260
  type: "button",
257
261
  "aria-current": i ? "page" : void 0,
258
- onClick: () => T(g(v, e.id)),
262
+ onClick: () => C(m(g, e.id)),
259
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"),
260
264
  children: [
261
- /* @__PURE__ */ D("span", {
265
+ /* @__PURE__ */ E("span", {
262
266
  className: "w-5 shrink-0 text-center text-xs tabular-nums text-text-muted",
263
267
  children: t + 1
264
268
  }),
265
- e.completedByMe ? /* @__PURE__ */ D(x, {
269
+ e.completedByMe ? /* @__PURE__ */ E(y, {
266
270
  size: 14,
267
271
  className: "shrink-0 text-status-success-text"
268
- }) : e.isLocked ? /* @__PURE__ */ D(ne, {
272
+ }) : e.isLocked ? /* @__PURE__ */ E(ae, {
269
273
  size: 14,
270
274
  className: "shrink-0 text-text-muted"
271
- }) : /* @__PURE__ */ D(n, {
275
+ }) : /* @__PURE__ */ E(n, {
272
276
  size: 14,
273
277
  className: "shrink-0 text-text-muted"
274
278
  }),
275
- /* @__PURE__ */ D("span", {
279
+ /* @__PURE__ */ E("span", {
276
280
  className: "min-w-0 flex-1 truncate",
277
281
  children: e.title
278
282
  })
@@ -286,6 +290,6 @@ function I() {
286
290
  });
287
291
  }
288
292
  //#endregion
289
- export { I as CoursesLessonPlayerPagePage };
293
+ export { P as CoursesLessonPlayerPagePage };
290
294
 
291
295
  //# sourceMappingURL=CoursesLessonPlayerPagePage.js.map