@burdenoff/microfe-technospam 2026.1031.5 → 2026.1031.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/generated/wspace-types.d.ts +1100 -147
- package/dist/generated/wspace-types.d.ts.map +1 -1
- package/dist/stories/fixtures/technospamFixtures.d.ts.map +1 -1
- package/dist/technospam/api/canonicalLearning.d.ts +53 -0
- package/dist/technospam/api/canonicalLearning.d.ts.map +1 -0
- package/dist/technospam/api/canonicalLearning.js +57 -0
- package/dist/technospam/api/canonicalLearning.js.map +1 -0
- package/dist/technospam/api/technospamApi.d.ts +448 -16
- package/dist/technospam/api/technospamApi.d.ts.map +1 -1
- package/dist/technospam/api/technospamApi.js +417 -287
- package/dist/technospam/api/technospamApi.js.map +1 -1
- package/dist/technospam/components/StatusBadge.d.ts.map +1 -1
- package/dist/technospam/components/StatusBadge.js +2 -0
- package/dist/technospam/components/StatusBadge.js.map +1 -1
- package/dist/technospam/components/admin/EnterpriseIntegrationsTab.d.ts +6 -0
- package/dist/technospam/components/admin/EnterpriseIntegrationsTab.d.ts.map +1 -0
- package/dist/technospam/components/admin/EnterpriseIntegrationsTab.js +256 -0
- package/dist/technospam/components/admin/EnterpriseIntegrationsTab.js.map +1 -0
- package/dist/technospam/lib/enterprise.d.ts +21 -0
- package/dist/technospam/lib/enterprise.d.ts.map +1 -0
- package/dist/technospam/lib/enterprise.js +36 -0
- package/dist/technospam/lib/enterprise.js.map +1 -0
- package/dist/technospam/pages/AdminAdminDashboardPagePage.d.ts.map +1 -1
- package/dist/technospam/pages/AdminAdminDashboardPagePage.js +206 -201
- package/dist/technospam/pages/AdminAdminDashboardPagePage.js.map +1 -1
- package/dist/technospam/pages/ApplicationsPortfolioPagePage.d.ts.map +1 -1
- package/dist/technospam/pages/ApplicationsPortfolioPagePage.js +26 -26
- package/dist/technospam/pages/ApplicationsPortfolioPagePage.js.map +1 -1
- package/dist/technospam/pages/CoursesCourseDetailsPagePage.d.ts.map +1 -1
- package/dist/technospam/pages/CoursesCourseDetailsPagePage.js +196 -192
- package/dist/technospam/pages/CoursesCourseDetailsPagePage.js.map +1 -1
- package/dist/technospam/pages/CoursesLessonPlayerPagePage.d.ts.map +1 -1
- package/dist/technospam/pages/CoursesLessonPlayerPagePage.js +125 -121
- package/dist/technospam/pages/CoursesLessonPlayerPagePage.js.map +1 -1
- package/dist/technospam/pages/CoursesMyLearningPagePage.d.ts.map +1 -1
- package/dist/technospam/pages/CoursesMyLearningPagePage.js +95 -95
- package/dist/technospam/pages/CoursesMyLearningPagePage.js.map +1 -1
- package/dist/technospam/pages/DashboardDashboardPagePage.d.ts.map +1 -1
- package/dist/technospam/pages/DashboardDashboardPagePage.js +30 -30
- package/dist/technospam/pages/DashboardDashboardPagePage.js.map +1 -1
- package/package.json +1 -1
|
@@ -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 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 const enrollment = data?.tecMyEnrollments?.items?.[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 };\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":";;;;;;;;;;;;;;;;;;;;;AAmCA,IAAM,IAA+C;CACnD,OAAO;CACP,SAAS;CACT,aAAa;CACb,MAAM;CACN,SAAS;CACV;AAGD,SAAS,EAAiB,GAA4B;AAGpD,QAFI,EAAO,SAAS,UAAgB,EAAO,eAAe,UAAU,KAAK,IACrE,EAAO,SAAS,gBAAsB,CAAC,CAAC,EAAO,iBAC5C;;AAGT,SAAS,EAAW,EAAE,WAAQ,iBAA6D;CACzF,IAAM,IAAQ,EAAO,UACnB,kBAAC,GAAD;EAAU,WAAU;YACjB,EAAO;EACC,CAAA,GACT;AAEJ,SAAQ,EAAO,MAAf;EACE,KAAK,SAAS;GACZ,IAAM,IAAM,EAAO,MAAM,aACnB,IAAU,CAAC,EAAO,MAAM,YAAY,EAAO,KAAK,SAAS,WAAW,SAAS;AACnF,UACE,kBAAC,OAAD;IAAK,WAAU;cAAf,CACG,KAAO,IACN,kBAAC,SAAD;KACE,UAAA;KACA,SAAQ;KACH;KACL,WAAU;KACV,eAAY;eAEZ,kBAAC,SAAD,EAAO,MAAK,YAAa,CAAA;KACnB,CAAA,GAER,kBAAC,KAAD;KAAG,WAAU;eAAuF;KAEhG,CAAA,EAEL,EACG;;;EAGV,KAAK,OACH,QACE,kBAAC,OAAD;GAAK,WAAU;aAAf,CACG,GACD,kBAAC,GAAD;IAAoC;IAAQ,UAAU;IAAc,EAAnD,EAAO,GAA4C,CAChE;;EAEV,KAAK,cACH,QACE,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,OAAD;IAAK,WAAU;cACZ,KACC,kBAAC,KAAD;KAAG,WAAU;eAA0B;KAEnC,CAAA;IAEF,CAAA,EACN,kBAAC,OAAD;IAAK,WAAU;cACb,kBAAC,GAAD;KAA8C;KAAQ,aAAa;KAAc,EAAtD,EAAO,GAA+C;IAC7E,CAAA,CACF;;EAIV,QACE,QACE,KACE,kBAAC,KAAD;GAAG,WAAU;aAAuF;GAEhG,CAAA;;;AAad,SAAgB,IAA8B;CAC5C,IAAM,CAAC,KAAU,IAAiB,EAC5B,IAAW,EAAO,IAAI,EAAa,GAAG,IAAI,IAC1C,IAAW,EAAO,IAAI,EAAa,OAAO,IAAI,IAC9C,IAAW,GAAuB,EAClC,EAAE,mBAAgB,GAAe,EACjC,EAAE,YAAS,GAAwB,EACnC,EAAE,SAAM,YAAS,UAAO,eAAY,EAAqB,GAAU,EAAS,EAC5E,CAAC,GAAgB,EAAE,SAAS,OAAgB,GAAmB,EAC/D,CAAC,GAAQ,EAAE,SAAS,OAAe,GAAW;AAEpD,UAAgB;AACd,EAAI,KACF,EAAK,4BAA4B;GAC/B;GACA,UAAU;GACV,YAAY;GACb,CAAC;IAEH;EAAC;EAAU;EAAa;EAAK,CAAC;CAEjC,IAAM,IAAS,GAAM,aAAa,MAC5B,IAAS,GAAM,aAAa,MAC5B,IAAa,GAAM,kBAAkB,QAAQ,MAAM,MACnD,IAAU,CAAC,GAAI,GAAQ,WAAW,EAAE,CAAE,CAAC,MAAM,GAAG,MAAM,EAAE,WAAW,EAAE,SAAS,EAC9E,IAAQ,EAAQ,WAAW,MAAM,EAAE,OAAO,EAAS,EACnD,IAAO,IAAQ,IAAI,EAAQ,IAAQ,KAAK,KAAA,GACxC,IAAO,KAAS,KAAK,IAAQ,EAAQ,SAAS,IAAI,EAAQ,IAAQ,KAAK,KAAA,GACvE,IAAW,EAAkB,EAAS,EAEtC,WAA8B;AAC7B,KAAS;IAGV,KAAiB,YAA2B;AAChD,MAAI,CAAC,EAAQ;EAEb,IAAM,KADM,MAAM,EAAe,EAAE,WAAW,EAAE,UAAU,EAAO,IAAI,EAAE,CAAC,EACpD,MAAM;AACrB,QACL,EAAK,+BAA+B;GAClC;GACA,UAAU,EAAO;GACjB,YAAY;GACb,CAAC,EACF,EAAO,QAAQ,oBAAoB,GAAG,EAAQ,YAAY,sBAAsB,EAC5E,IAAM,EAAS,EAAiB,GAAU,EAAK,GAAG,CAAC,GAClD,MAAM,GAAS;IAGhB,KAAe,YAA2B;AAE9C,GADY,MAAM,EAAO,EAAE,WAAW,EAAE,OAAO,EAAE,aAAU,EAAE,EAAE,CAAC,EACxD,MAAM,cACZ,EAAO,QACL,YACA,qBAAqB,GAAQ,SAAS,cAAc,mBACrD,EACD,MAAM,GAAS;;AAInB,KAAI,CAAC,KAAY,CAAC,EAChB,QACE,kBAAC,GAAD;EAAY,OAAM;EAAS,UAAS;YAClC,kBAAC,GAAD;GACE,OAAM;GACN,aAAY;GACZ,CAAA;EACS,CAAA;AAIjB,KAAI,KAAW,CAAC,EACd,QACE,kBAAC,GAAD;EAAY,OAAM;EAAmB;YAArC,CACE,kBAAC,GAAD,EAAU,WAAU,8BAA+B,CAAA,EACnD,kBAAC,GAAD,EAAU,WAAU,0BAA2B,CAAA,CACpC;;AAIjB,KAAI,KAAS,CAAC,KAAU,CAAC,KAAU,EAAO,aAAa,EAAO,GAC5D,QACE,kBAAC,GAAD;EAAY,OAAM;EAAmB;YAClC,IACC,kBAAC,GAAD;GAAW,OAAM;GAA8B;GAAS,CAAA,GAExD,kBAAC,GAAD;GAAW,OAAM;GAAmB,aAAY;GAAuC,CAAA;EAE9E,CAAA;CAIjB,IAAM,KAAO,EAAU,EAAO,SAAS,GACjC,IAAO,CAAC,CAAC,EAAO,eAChB,IAAU,EAAiB,EAAO;AAExC,QACE,kBAAC,GAAD;EACY;EACV,OAAO,EAAO;EACd,aAAa,GAAG,EAAO,MAAM,YAAY,IAAQ,EAAE,MAAM,EAAQ;EACjE,SACE,IACE,kBAAC,QAAD;GAAM,WAAU;aAAhB,CACE,kBAAC,GAAD,EAAc,MAAM,IAAM,CAAA,EAAA,aACrB;OACL;YAGN,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,OAAD;MAAK,WAAU;gBAAf;OACE,kBAAC,QAAD;QAAM,WAAU;kBAAhB;SACE,kBAAC,IAAD,EAAM,MAAM,IAAM,CAAA;;SAAE,EAAa,EAAO,KAAK;SACxC;;OACP,kBAAC,QAAD;QAAM,WAAU;kBAAhB;SACE,kBAAC,GAAD,EAAO,MAAM,IAAM,CAAA;;SAAE,EAAe,EAAO,iBAAiB;SACvD;;OACN,EAAO,YACN,kBAAC,QAAD;QAAM,WAAU;kBAA2D;QAEpE,CAAA,GACL;OACA;;KAEJ,IASE,OARF,kBAAC,GAAD;MAAW,WAAU;gBAArB,CACE,kBAAC,KAAD;OAAG,WAAU;iBAA8B;OAEvC,CAAA,EACJ,kBAAC,GAAD;OAAQ,MAAK;OAAK,eAAe,KAAK,IAAc;OAAE,UAAU;iBAAhE;QACE,kBAAC,GAAD,EAAU,MAAM,IAAM,CAAA;;QAAE,IAAY,eAAe;QAC5C;SACC;;KAGd,kBAAC,GAAD;MAAW,WAAU;MAAM,eAAY;gBACpC,EAAO,WACN,kBAAC,GAAD,EAAa,aAAY,oGAAqG,CAAA,GAE9H,kBAAC,GAAD;OAAoB;OAAQ,YAAY;OAAmB,CAAA;MAEnD,CAAA;KAEZ,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,GAAD;OACE,SAAQ;OACR,UAAU,CAAC;OACX,eAAe,KAAQ,EAAS,EAAiB,GAAU,EAAK,GAAG,CAAC;iBAHtE,CAKE,kBAAC,IAAD,EAAW,MAAM,IAAM,CAAA,EAAA,YAChB;UACT,kBAAC,OAAD;OAAK,WAAU;iBAAf;QACG,KAAc,CAAC,KAAQ,CAAC,KAAW,CAAC,EAAO,WAC1C,kBAAC,GAAD;SAAQ,eAAe,KAAK,IAAgB;SAAE,UAAU;mBAAxD;UACE,kBAAC,GAAD,EAAc,MAAM,IAAM,CAAA;;UAAE,IAAa,YAAY;UAC9C;aACP;QACH,KAAc,CAAC,KAAQ,KAAW,CAAC,EAAO,WACzC,kBAAC,QAAD;SAAM,WAAU;mBACb,EAAO,SAAS,SACb,2CACA;SACC,CAAA,GACL;QACJ,kBAAC,GAAD;SACE,SAAS,KAAQ,CAAC,IAAa,YAAY;SAC3C,UAAU,CAAC;SACX,eAAe,KAAQ,EAAS,EAAiB,GAAU,EAAK,GAAG,CAAC;mBAHtE,CAIC,SACM,kBAAC,GAAD,EAAY,MAAM,IAAM,CAAA,CACtB;;QACL;SACF;;KACF;OAEN,kBAAC,GAAD;IAAW,WAAU;cAArB;KACE,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,QAAD;OAAM,WAAU;iBAAgE;OAEzE,CAAA,EACN,IACC,kBAAC,QAAD;OAAM,WAAU;OAA8B,eAAY;iBAA1D,CACG,EAAW,aAAY,aACnB;WACL,KACA;;KACL,IACC,kBAAC,OAAD;MAAK,WAAU;gBACb,kBAAC,OAAD;OACE,WAAU;OACV,OAAO,EAAE,OAAO,GAAG,EAAW,YAAY,IAAI;OAC9C,CAAA;MACE,CAAA,GACJ;KACJ,kBAAC,MAAD;MAAI,WAAU;gBACX,EAAQ,KAAK,GAAG,MAAM;OACrB,IAAM,IAAQ,EAAU,EAAE,SAAS,GAC7B,IAAU,EAAE,OAAO,EAAO;AAChC,cACE,kBAAC,MAAD,EAAA,UACE,kBAAC,UAAD;QACE,MAAK;QACL,gBAAc,IAAU,SAAS,KAAA;QACjC,eAAe,EAAS,EAAiB,GAAU,EAAE,GAAG,CAAC;QACzD,WAAW,EACT,gHACA,KAAW,sDACZ;kBAPH;SASE,kBAAC,QAAD;UAAM,WAAU;oBACb,IAAI;UACA,CAAA;SACN,EAAE,gBACD,kBAAC,GAAD;UAAc,MAAM;UAAI,WAAU;UAAsC,CAAA,GACtE,EAAE,WACJ,kBAAC,IAAD;UAAM,MAAM;UAAI,WAAU;UAA6B,CAAA,GAEvD,kBAAC,GAAD;UAAO,MAAM;UAAI,WAAU;UAA6B,CAAA;SAE1D,kBAAC,QAAD;UAAM,WAAU;oBAA2B,EAAE;UAAa,CAAA;SACnD;WACN,EAtBI,EAAE,GAsBN;QAEP;MACC,CAAA;KACK;MACR;;EACK,CAAA"}
|
|
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":";;;;;;;;;;;;;;;;;;;;;AAoCA,IAAM,IAA+C;CACnD,OAAO;CACP,SAAS;CACT,aAAa;CACb,MAAM;CACN,SAAS;CACV;AAGD,SAAS,GAAiB,GAA4B;AAGpD,QAFI,EAAO,SAAS,UAAgB,EAAO,eAAe,UAAU,KAAK,IACrE,EAAO,SAAS,gBAAsB,CAAC,CAAC,EAAO,iBAC5C;;AAGT,SAAS,EAAW,EAAE,WAAQ,iBAA6D;CACzF,IAAM,IAAQ,EAAO,UACnB,kBAAC,GAAD;EAAU,WAAU;YACjB,EAAO;EACC,CAAA,GACT;AAEJ,SAAQ,EAAO,MAAf;EACE,KAAK,SAAS;GACZ,IAAM,IAAM,EAAO,MAAM,aACnB,IAAU,CAAC,EAAO,MAAM,YAAY,EAAO,KAAK,SAAS,WAAW,SAAS;AACnF,UACE,kBAAC,OAAD;IAAK,WAAU;cAAf,CACG,KAAO,IACN,kBAAC,SAAD;KACE,UAAA;KACA,SAAQ;KACH;KACL,WAAU;KACV,eAAY;eAEZ,kBAAC,SAAD,EAAO,MAAK,YAAa,CAAA;KACnB,CAAA,GAER,kBAAC,KAAD;KAAG,WAAU;eAAuF;KAEhG,CAAA,EAEL,EACG;;;EAGV,KAAK,OACH,QACE,kBAAC,OAAD;GAAK,WAAU;aAAf,CACG,GACD,kBAAC,GAAD;IAAoC;IAAQ,UAAU;IAAc,EAAnD,EAAO,GAA4C,CAChE;;EAEV,KAAK,cACH,QACE,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,OAAD;IAAK,WAAU;cACZ,KACC,kBAAC,KAAD;KAAG,WAAU;eAA0B;KAEnC,CAAA;IAEF,CAAA,EACN,kBAAC,OAAD;IAAK,WAAU;cACb,kBAAC,GAAD;KAA8C;KAAQ,aAAa;KAAc,EAAtD,EAAO,GAA+C;IAC7E,CAAA,CACF;;EAIV,QACE,QACE,KACE,kBAAC,KAAD;GAAG,WAAU;aAAuF;GAEhG,CAAA;;;AAad,SAAgB,IAA8B;CAC5C,IAAM,CAAC,KAAU,GAAiB,EAC5B,IAAW,EAAO,IAAI,EAAa,GAAG,IAAI,IAC1C,IAAW,EAAO,IAAI,EAAa,OAAO,IAAI,IAC9C,IAAW,GAAuB,EAClC,EAAE,mBAAgB,GAAe,EACjC,EAAE,YAAS,GAAwB,EACnC,EAAE,SAAM,YAAS,UAAO,eAAY,GAAqB,GAAU,EAAS,EAC5E,CAAC,GAAgB,EAAE,SAAS,OAAgB,GAAmB,EAC/D,CAAC,GAAQ,EAAE,SAAS,OAAe,GAAW;AAEpD,UAAgB;AACd,EAAI,KACF,EAAK,4BAA4B;GAC/B;GACA,UAAU;GACV,YAAY;GACb,CAAC;IAEH;EAAC;EAAU;EAAa;EAAK,CAAC;CAEjC,IAAM,IAAS,GAAM,aAAa,MAC5B,IAAS,GAAM,aAAa,MAI5B,EAAE,aAAa,GAAe,SAAS,MAAsB,EAAsB;EACvF,aAAa,GAAQ;EACrB,oBAAoB;EACpB,MAAM,CAAC;EACR,CAAC,EACI,IAAa,EAAc,MAAM,MACjC,IAAU,CAAC,GAAI,GAAQ,WAAW,EAAE,CAAE,CAAC,MAAM,GAAG,MAAM,EAAE,WAAW,EAAE,SAAS,EAC9E,IAAQ,EAAQ,WAAW,MAAM,EAAE,OAAO,EAAS,EACnD,IAAO,IAAQ,IAAI,EAAQ,IAAQ,KAAK,KAAA,GACxC,IAAO,KAAS,KAAK,IAAQ,EAAQ,SAAS,IAAI,EAAQ,IAAQ,KAAK,KAAA,GACvE,IAAW,EAAkB,EAAS,EAEtC,WAA8B;AAE7B,EADA,GAAS,EACT,GAAmB;IAGpB,KAAiB,YAA2B;AAChD,MAAI,CAAC,EAAQ;EAEb,IAAM,KADM,MAAM,EAAe,EAAE,WAAW,EAAE,UAAU,EAAO,IAAI,EAAE,CAAC,EACpD,MAAM;AACrB,QACL,EAAK,+BAA+B;GAClC;GACA,UAAU,EAAO;GACjB,YAAY;GACb,CAAC,EACF,EAAO,QAAQ,oBAAoB,GAAG,EAAQ,YAAY,sBAAsB,EAC5E,IAAM,EAAS,EAAiB,GAAU,EAAK,GAAG,CAAC,GAClD,MAAM,GAAS;IAGhB,KAAe,YAA2B;AAE9C,GADY,MAAM,EAAO,EAAE,WAAW,EAAE,OAAO,EAAE,aAAU,EAAE,EAAE,CAAC,EACxD,MAAM,cACZ,EAAO,QACL,YACA,qBAAqB,GAAQ,SAAS,cAAc,mBACrD,EACD,MAAM,GAAS;;AAInB,KAAI,CAAC,KAAY,CAAC,EAChB,QACE,kBAAC,GAAD;EAAY,OAAM;EAAS,UAAS;YAClC,kBAAC,GAAD;GACE,OAAM;GACN,aAAY;GACZ,CAAA;EACS,CAAA;AAIjB,KAAI,KAAW,CAAC,EACd,QACE,kBAAC,GAAD;EAAY,OAAM;EAAmB;YAArC,CACE,kBAAC,GAAD,EAAU,WAAU,8BAA+B,CAAA,EACnD,kBAAC,GAAD,EAAU,WAAU,0BAA2B,CAAA,CACpC;;AAIjB,KAAI,KAAS,CAAC,KAAU,CAAC,KAAU,EAAO,aAAa,EAAO,GAC5D,QACE,kBAAC,GAAD;EAAY,OAAM;EAAmB;YAClC,IACC,kBAAC,IAAD;GAAW,OAAM;GAA8B;GAAS,CAAA,GAExD,kBAAC,GAAD;GAAW,OAAM;GAAmB,aAAY;GAAuC,CAAA;EAE9E,CAAA;CAIjB,IAAM,KAAO,EAAU,EAAO,SAAS,GACjC,IAAO,CAAC,CAAC,EAAO,eAChB,IAAU,GAAiB,EAAO;AAExC,QACE,kBAAC,GAAD;EACY;EACV,OAAO,EAAO;EACd,aAAa,GAAG,EAAO,MAAM,YAAY,IAAQ,EAAE,MAAM,EAAQ;EACjE,SACE,IACE,kBAAC,QAAD;GAAM,WAAU;aAAhB,CACE,kBAAC,GAAD,EAAc,MAAM,IAAM,CAAA,EAAA,aACrB;OACL;YAGN,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,OAAD;MAAK,WAAU;gBAAf;OACE,kBAAC,QAAD;QAAM,WAAU;kBAAhB;SACE,kBAAC,IAAD,EAAM,MAAM,IAAM,CAAA;;SAAE,EAAa,EAAO,KAAK;SACxC;;OACP,kBAAC,QAAD;QAAM,WAAU;kBAAhB;SACE,kBAAC,GAAD,EAAO,MAAM,IAAM,CAAA;;SAAE,GAAe,EAAO,iBAAiB;SACvD;;OACN,EAAO,YACN,kBAAC,QAAD;QAAM,WAAU;kBAA2D;QAEpE,CAAA,GACL;OACA;;KAEJ,IASE,OARF,kBAAC,GAAD;MAAW,WAAU;gBAArB,CACE,kBAAC,KAAD;OAAG,WAAU;iBAA8B;OAEvC,CAAA,EACJ,kBAAC,GAAD;OAAQ,MAAK;OAAK,eAAe,KAAK,IAAc;OAAE,UAAU;iBAAhE;QACE,kBAAC,GAAD,EAAU,MAAM,IAAM,CAAA;;QAAE,IAAY,eAAe;QAC5C;SACC;;KAGd,kBAAC,GAAD;MAAW,WAAU;MAAM,eAAY;gBACpC,EAAO,WACN,kBAAC,GAAD,EAAa,aAAY,oGAAqG,CAAA,GAE9H,kBAAC,GAAD;OAAoB;OAAQ,YAAY;OAAmB,CAAA;MAEnD,CAAA;KAEZ,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,GAAD;OACE,SAAQ;OACR,UAAU,CAAC;OACX,eAAe,KAAQ,EAAS,EAAiB,GAAU,EAAK,GAAG,CAAC;iBAHtE,CAKE,kBAAC,IAAD,EAAW,MAAM,IAAM,CAAA,EAAA,YAChB;UACT,kBAAC,OAAD;OAAK,WAAU;iBAAf;QACG,KAAc,CAAC,KAAQ,CAAC,KAAW,CAAC,EAAO,WAC1C,kBAAC,GAAD;SAAQ,eAAe,KAAK,IAAgB;SAAE,UAAU;mBAAxD;UACE,kBAAC,GAAD,EAAc,MAAM,IAAM,CAAA;;UAAE,IAAa,YAAY;UAC9C;aACP;QACH,KAAc,CAAC,KAAQ,KAAW,CAAC,EAAO,WACzC,kBAAC,QAAD;SAAM,WAAU;mBACb,EAAO,SAAS,SACb,2CACA;SACC,CAAA,GACL;QACJ,kBAAC,GAAD;SACE,SAAS,KAAQ,CAAC,IAAa,YAAY;SAC3C,UAAU,CAAC;SACX,eAAe,KAAQ,EAAS,EAAiB,GAAU,EAAK,GAAG,CAAC;mBAHtE,CAIC,SACM,kBAAC,GAAD,EAAY,MAAM,IAAM,CAAA,CACtB;;QACL;SACF;;KACF;OAEN,kBAAC,GAAD;IAAW,WAAU;cAArB;KACE,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,QAAD;OAAM,WAAU;iBAAgE;OAEzE,CAAA,EACN,IACC,kBAAC,QAAD;OAAM,WAAU;OAA8B,eAAY;iBAA1D,CACG,EAAW,aAAY,aACnB;WACL,KACA;;KACL,IACC,kBAAC,OAAD;MAAK,WAAU;gBACb,kBAAC,OAAD;OACE,WAAU;OACV,OAAO,EAAE,OAAO,GAAG,EAAW,YAAY,IAAI;OAC9C,CAAA;MACE,CAAA,GACJ;KACJ,kBAAC,MAAD;MAAI,WAAU;gBACX,EAAQ,KAAK,GAAG,MAAM;OACrB,IAAM,IAAQ,EAAU,EAAE,SAAS,GAC7B,IAAU,EAAE,OAAO,EAAO;AAChC,cACE,kBAAC,MAAD,EAAA,UACE,kBAAC,UAAD;QACE,MAAK;QACL,gBAAc,IAAU,SAAS,KAAA;QACjC,eAAe,EAAS,EAAiB,GAAU,EAAE,GAAG,CAAC;QACzD,WAAW,EACT,gHACA,KAAW,sDACZ;kBAPH;SASE,kBAAC,QAAD;UAAM,WAAU;oBACb,IAAI;UACA,CAAA;SACN,EAAE,gBACD,kBAAC,GAAD;UAAc,MAAM;UAAI,WAAU;UAAsC,CAAA,GACtE,EAAE,WACJ,kBAAC,IAAD;UAAM,MAAM;UAAI,WAAU;UAA6B,CAAA,GAEvD,kBAAC,GAAD;UAAO,MAAM;UAAI,WAAU;UAA6B,CAAA;SAE1D,kBAAC,QAAD;UAAM,WAAU;oBAA2B,EAAE;UAAa,CAAA;SACnD;WACN,EAtBI,EAAE,GAsBN;QAEP;MACC,CAAA;KACK;MACR;;EACK,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CoursesMyLearningPagePage.d.ts","sourceRoot":"","sources":["../../../src/technospam/pages/CoursesMyLearningPagePage.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"CoursesMyLearningPagePage.d.ts","sourceRoot":"","sources":["../../../src/technospam/pages/CoursesMyLearningPagePage.tsx"],"names":[],"mappings":"AAgGA,wBAAgB,yBAAyB,gCAoJxC"}
|
|
@@ -1,195 +1,195 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { cn as
|
|
3
|
-
import { PageLayout as
|
|
4
|
-
import { StatCard as
|
|
5
|
-
import { coverGradient as
|
|
6
|
-
import { formatDate as
|
|
7
|
-
import { StatusBadge as
|
|
8
|
-
import { EmptyView as
|
|
9
|
-
import {
|
|
1
|
+
import { useTechnoSpamNavigate as e } from "../providers/TechnoSpamProvider.js";
|
|
2
|
+
import { cn as t } from "../utils/cn.js";
|
|
3
|
+
import { PageLayout as n } from "../components/PageLayout.js";
|
|
4
|
+
import { StatCard as r } from "../components/StatCard.js";
|
|
5
|
+
import { coverGradient as i, topicIcon as a } from "../components/MediaCover.js";
|
|
6
|
+
import { formatDate as o, relativeTime as s } from "../utils/format.js";
|
|
7
|
+
import { StatusBadge as c } from "../components/StatusBadge.js";
|
|
8
|
+
import { EmptyView as l, ErrorView as u, LoadingRows as d } from "../components/StateViews.js";
|
|
9
|
+
import { useMyCertificatesQuery as f, useMyEnrollmentsQuery as p } from "../api/technospamApi.js";
|
|
10
10
|
import "../components/index.js";
|
|
11
|
-
import { FOCUS_PARAMS as
|
|
12
|
-
import { resumePath as
|
|
13
|
-
import { focusableCardProps as
|
|
14
|
-
import { createElement as
|
|
15
|
-
import { Award as
|
|
16
|
-
import { jsx as
|
|
17
|
-
import { Button as
|
|
11
|
+
import { FOCUS_PARAMS as m } from "../utils/pageContext.js";
|
|
12
|
+
import { resumePath as h } from "../utils/lessonPath.js";
|
|
13
|
+
import { focusableCardProps as g, useFocusedRecordParam as _ } from "../hooks/useFocusedRecordParam.js";
|
|
14
|
+
import { createElement as v } from "react";
|
|
15
|
+
import { Award as y, BookOpen as b, GraduationCap as x, PlayCircle as S, ShieldCheck as C } from "lucide-react";
|
|
16
|
+
import { jsx as w, jsxs as T } from "react/jsx-runtime";
|
|
17
|
+
import { Button as E, GlassCard as D } from "@burdenoff/fe-libs/ui";
|
|
18
18
|
//#region src/technospam/pages/CoursesMyLearningPagePage.tsx
|
|
19
|
-
function
|
|
20
|
-
let
|
|
21
|
-
return /* @__PURE__ */ D
|
|
22
|
-
className:
|
|
23
|
-
...
|
|
19
|
+
function O({ e, onOpen: n, focused: r, onToggle: o }) {
|
|
20
|
+
let l = `${e.course?.topic ?? ""} ${e.course?.title ?? "Course"}`;
|
|
21
|
+
return /* @__PURE__ */ T(D, {
|
|
22
|
+
className: t("group flex cursor-pointer flex-col gap-3 p-4 transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md sm:flex-row sm:items-center sm:gap-4", r && "ring-2 ring-accent-primary"),
|
|
23
|
+
...g(r, o),
|
|
24
24
|
children: [
|
|
25
|
-
/* @__PURE__ */
|
|
26
|
-
className:
|
|
27
|
-
children:
|
|
25
|
+
/* @__PURE__ */ w("span", {
|
|
26
|
+
className: t("flex h-12 w-12 shrink-0 items-center justify-center rounded-xl bg-gradient-to-br text-white shadow-sm transition-transform duration-200 group-hover:scale-105", i(l)),
|
|
27
|
+
children: v(a(l), { size: 20 })
|
|
28
28
|
}),
|
|
29
|
-
/* @__PURE__ */
|
|
29
|
+
/* @__PURE__ */ T("div", {
|
|
30
30
|
className: "min-w-0 flex-1",
|
|
31
31
|
children: [
|
|
32
|
-
/* @__PURE__ */
|
|
32
|
+
/* @__PURE__ */ T("div", {
|
|
33
33
|
className: "flex flex-wrap items-center gap-2",
|
|
34
|
-
children: [/* @__PURE__ */
|
|
34
|
+
children: [/* @__PURE__ */ w("button", {
|
|
35
35
|
type: "button",
|
|
36
36
|
"aria-pressed": r,
|
|
37
|
-
onClick:
|
|
37
|
+
onClick: o,
|
|
38
38
|
className: "truncate text-left text-sm font-semibold text-text-primary group-hover:text-accent-primary",
|
|
39
39
|
children: e.course?.title ?? "Course"
|
|
40
|
-
}), /* @__PURE__ */
|
|
40
|
+
}), /* @__PURE__ */ w(c, { status: e.status })]
|
|
41
41
|
}),
|
|
42
|
-
/* @__PURE__ */
|
|
42
|
+
/* @__PURE__ */ T("div", {
|
|
43
43
|
className: "mt-1 flex flex-wrap items-center gap-2 text-xs text-text-muted",
|
|
44
44
|
children: [
|
|
45
|
-
/* @__PURE__ */
|
|
46
|
-
/* @__PURE__ */
|
|
45
|
+
/* @__PURE__ */ T("span", { children: [e.lessonsCompleted, " lessons done"] }),
|
|
46
|
+
/* @__PURE__ */ w("span", {
|
|
47
47
|
"aria-hidden": !0,
|
|
48
48
|
children: "·"
|
|
49
49
|
}),
|
|
50
|
-
/* @__PURE__ */
|
|
50
|
+
/* @__PURE__ */ T("span", { children: ["Active ", s(e.lastActiveAt ?? e.startedAt)] })
|
|
51
51
|
]
|
|
52
52
|
}),
|
|
53
|
-
/* @__PURE__ */
|
|
53
|
+
/* @__PURE__ */ w("div", {
|
|
54
54
|
className: "mt-2 h-1.5 w-full overflow-hidden rounded-full bg-bg-sunken",
|
|
55
|
-
children: /* @__PURE__ */
|
|
55
|
+
children: /* @__PURE__ */ w("div", {
|
|
56
56
|
className: "h-full rounded-full bg-gradient-to-r from-accent-primary via-accent-indigo to-accent-purple transition-all duration-500",
|
|
57
57
|
style: { width: `${e.progressPct}%` }
|
|
58
58
|
})
|
|
59
59
|
})
|
|
60
60
|
]
|
|
61
61
|
}),
|
|
62
|
-
/* @__PURE__ */
|
|
62
|
+
/* @__PURE__ */ T("div", {
|
|
63
63
|
className: "flex items-center justify-between gap-3 sm:flex-col sm:items-end sm:justify-center",
|
|
64
|
-
children: [/* @__PURE__ */
|
|
64
|
+
children: [/* @__PURE__ */ T("div", {
|
|
65
65
|
className: "text-lg font-semibold tabular-nums text-text-primary",
|
|
66
66
|
children: [e.progressPct, "%"]
|
|
67
|
-
}), /* @__PURE__ */
|
|
67
|
+
}), /* @__PURE__ */ T(E, {
|
|
68
68
|
variant: "ghost",
|
|
69
69
|
size: "sm",
|
|
70
|
-
onClick:
|
|
70
|
+
onClick: n,
|
|
71
71
|
"data-testid": `resume-${e.courseId}`,
|
|
72
|
-
children: [/* @__PURE__ */
|
|
72
|
+
children: [/* @__PURE__ */ w(S, { size: 15 }), " Resume"]
|
|
73
73
|
})]
|
|
74
74
|
})
|
|
75
75
|
]
|
|
76
76
|
});
|
|
77
77
|
}
|
|
78
|
-
var
|
|
79
|
-
function
|
|
80
|
-
let
|
|
81
|
-
return /* @__PURE__ */
|
|
78
|
+
var k = [m.certificate], A = [m.enrollment];
|
|
79
|
+
function j() {
|
|
80
|
+
let i = e(), { enrollments: a, loading: s, error: v } = p(), { certificates: j } = f(), [M, N] = _(m.enrollment, k), [P, F] = _(m.certificate, A), I = a.filter((e) => e.status === "COMPLETED").length, L = a.filter((e) => e.status === "IN_PROGRESS" || e.status === "ENROLLED").length;
|
|
81
|
+
return /* @__PURE__ */ T(n, {
|
|
82
82
|
hero: !0,
|
|
83
83
|
title: "My Learning",
|
|
84
84
|
description: "Track your enrolled courses, progress, and earned certificates.",
|
|
85
|
-
actions: /* @__PURE__ */
|
|
85
|
+
actions: /* @__PURE__ */ T(E, {
|
|
86
86
|
variant: "outline",
|
|
87
|
-
onClick: () =>
|
|
88
|
-
children: [/* @__PURE__ */
|
|
87
|
+
onClick: () => i("/courses/courses-page"),
|
|
88
|
+
children: [/* @__PURE__ */ w(b, { size: 16 }), " Browse catalog"]
|
|
89
89
|
}),
|
|
90
90
|
children: [
|
|
91
|
-
/* @__PURE__ */
|
|
91
|
+
/* @__PURE__ */ T("div", {
|
|
92
92
|
className: "mb-6 grid grid-cols-2 gap-3 sm:grid-cols-4",
|
|
93
93
|
children: [
|
|
94
|
-
/* @__PURE__ */
|
|
94
|
+
/* @__PURE__ */ w(r, {
|
|
95
95
|
label: "Enrolled",
|
|
96
|
-
value:
|
|
96
|
+
value: a.length,
|
|
97
97
|
hint: "Courses you've enrolled in",
|
|
98
|
-
icon:
|
|
98
|
+
icon: b,
|
|
99
99
|
tone: "blue"
|
|
100
100
|
}),
|
|
101
|
-
/* @__PURE__ */
|
|
101
|
+
/* @__PURE__ */ w(r, {
|
|
102
102
|
label: "In progress",
|
|
103
|
-
value:
|
|
103
|
+
value: L,
|
|
104
104
|
hint: "Courses in progress",
|
|
105
|
-
icon:
|
|
105
|
+
icon: S,
|
|
106
106
|
tone: "indigo"
|
|
107
107
|
}),
|
|
108
|
-
/* @__PURE__ */
|
|
108
|
+
/* @__PURE__ */ w(r, {
|
|
109
109
|
label: "Completed",
|
|
110
|
-
value:
|
|
110
|
+
value: I,
|
|
111
111
|
hint: "Courses completed",
|
|
112
|
-
icon:
|
|
112
|
+
icon: x,
|
|
113
113
|
tone: "green"
|
|
114
114
|
}),
|
|
115
|
-
/* @__PURE__ */
|
|
115
|
+
/* @__PURE__ */ w(r, {
|
|
116
116
|
label: "Certificates",
|
|
117
|
-
value:
|
|
117
|
+
value: j.length,
|
|
118
118
|
hint: "Certificates earned",
|
|
119
|
-
icon:
|
|
119
|
+
icon: y,
|
|
120
120
|
tone: "purple"
|
|
121
121
|
})
|
|
122
122
|
]
|
|
123
123
|
}),
|
|
124
|
-
|
|
124
|
+
s && !a.length ? /* @__PURE__ */ w(d, {
|
|
125
125
|
count: 3,
|
|
126
126
|
height: "h-24"
|
|
127
|
-
}) :
|
|
127
|
+
}) : v ? /* @__PURE__ */ w(u, {
|
|
128
128
|
title: "Couldn't load your learning",
|
|
129
|
-
error:
|
|
130
|
-
}) :
|
|
131
|
-
icon: /* @__PURE__ */
|
|
129
|
+
error: v
|
|
130
|
+
}) : a.length === 0 ? /* @__PURE__ */ w(l, {
|
|
131
|
+
icon: /* @__PURE__ */ w(b, { size: 26 }),
|
|
132
132
|
title: "You haven't enrolled yet",
|
|
133
133
|
description: "Browse the catalog and enroll in a course to start building skill by doing.",
|
|
134
|
-
action: /* @__PURE__ */ E
|
|
135
|
-
onClick: () =>
|
|
134
|
+
action: /* @__PURE__ */ w(E, {
|
|
135
|
+
onClick: () => i("/courses/courses-page"),
|
|
136
136
|
children: "Browse courses"
|
|
137
137
|
})
|
|
138
|
-
}) : /* @__PURE__ */
|
|
138
|
+
}) : /* @__PURE__ */ w("div", {
|
|
139
139
|
className: "space-y-3",
|
|
140
|
-
children:
|
|
140
|
+
children: a.map((e) => /* @__PURE__ */ w(O, {
|
|
141
141
|
e,
|
|
142
|
-
onOpen: () =>
|
|
143
|
-
focused:
|
|
144
|
-
onToggle: () =>
|
|
142
|
+
onOpen: () => i(h(e.courseId, e.nextLessonId)),
|
|
143
|
+
focused: M === e.id,
|
|
144
|
+
onToggle: () => N(M === e.id ? null : e.id)
|
|
145
145
|
}, e.id))
|
|
146
146
|
}),
|
|
147
|
-
|
|
147
|
+
j.length > 0 ? /* @__PURE__ */ T("div", {
|
|
148
148
|
className: "mt-8",
|
|
149
|
-
children: [/* @__PURE__ */
|
|
149
|
+
children: [/* @__PURE__ */ T("h2", {
|
|
150
150
|
className: "mb-3 flex items-center gap-2 text-base font-semibold text-text-primary",
|
|
151
|
-
children: [/* @__PURE__ */
|
|
151
|
+
children: [/* @__PURE__ */ w(y, {
|
|
152
152
|
size: 18,
|
|
153
153
|
className: "text-accent-teal"
|
|
154
154
|
}), " Certificates"]
|
|
155
|
-
}), /* @__PURE__ */
|
|
155
|
+
}), /* @__PURE__ */ w("div", {
|
|
156
156
|
className: "grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3",
|
|
157
|
-
children:
|
|
158
|
-
className:
|
|
159
|
-
...
|
|
160
|
-
children: [/* @__PURE__ */
|
|
157
|
+
children: j.map((e) => /* @__PURE__ */ T(D, {
|
|
158
|
+
className: t("group flex cursor-pointer flex-col gap-3 p-4 transition-all duration-200 hover:-translate-y-1 hover:shadow-lg", P === e.id && "ring-2 ring-accent-primary"),
|
|
159
|
+
...g(P === e.id, () => F(P === e.id ? null : e.id)),
|
|
160
|
+
children: [/* @__PURE__ */ T("div", {
|
|
161
161
|
className: "flex items-center gap-3",
|
|
162
|
-
children: [/* @__PURE__ */
|
|
162
|
+
children: [/* @__PURE__ */ w("span", {
|
|
163
163
|
className: "flex h-11 w-11 shrink-0 items-center justify-center rounded-xl bg-status-warning-bg-subtle text-status-warning-text ring-1 ring-status-warning-border/30 transition-transform duration-200 group-hover:scale-105",
|
|
164
|
-
children: /* @__PURE__ */
|
|
165
|
-
}), /* @__PURE__ */
|
|
164
|
+
children: /* @__PURE__ */ w(y, { size: 22 })
|
|
165
|
+
}), /* @__PURE__ */ T("div", {
|
|
166
166
|
className: "min-w-0",
|
|
167
|
-
children: [/* @__PURE__ */
|
|
167
|
+
children: [/* @__PURE__ */ w("button", {
|
|
168
168
|
type: "button",
|
|
169
|
-
"aria-pressed":
|
|
170
|
-
onClick: () =>
|
|
169
|
+
"aria-pressed": P === e.id,
|
|
170
|
+
onClick: () => F(P === e.id ? null : e.id),
|
|
171
171
|
className: "block max-w-full truncate text-left text-sm font-medium text-text-primary",
|
|
172
172
|
children: e.course?.title ?? "Certificate"
|
|
173
|
-
}), /* @__PURE__ */
|
|
173
|
+
}), /* @__PURE__ */ T("div", {
|
|
174
174
|
className: "truncate text-xs text-text-muted",
|
|
175
175
|
children: ["Serial ", e.serial]
|
|
176
176
|
})]
|
|
177
177
|
})]
|
|
178
|
-
}), /* @__PURE__ */
|
|
178
|
+
}), /* @__PURE__ */ T("div", {
|
|
179
179
|
className: "mt-auto flex items-center justify-between border-t border-border-seam pt-3 text-xs text-text-muted",
|
|
180
|
-
children: [/* @__PURE__ */
|
|
180
|
+
children: [/* @__PURE__ */ T("span", { children: [
|
|
181
181
|
"Score ",
|
|
182
|
-
/* @__PURE__ */
|
|
182
|
+
/* @__PURE__ */ w("span", {
|
|
183
183
|
className: "font-semibold text-text-primary",
|
|
184
184
|
children: e.score
|
|
185
185
|
}),
|
|
186
186
|
" ·",
|
|
187
187
|
" ",
|
|
188
|
-
|
|
189
|
-
] }), e.status === "ISSUED" ? /* @__PURE__ */
|
|
188
|
+
o(e.issuedAt)
|
|
189
|
+
] }), e.status === "ISSUED" ? /* @__PURE__ */ T("span", {
|
|
190
190
|
className: "inline-flex items-center gap-1 rounded-full border border-status-success-border bg-status-success-bg-subtle px-2 py-0.5 font-medium text-status-success-text",
|
|
191
|
-
children: [/* @__PURE__ */
|
|
192
|
-
}) : /* @__PURE__ */
|
|
191
|
+
children: [/* @__PURE__ */ w(C, { size: 12 }), " Verified"]
|
|
192
|
+
}) : /* @__PURE__ */ w(c, { status: e.status })]
|
|
193
193
|
})]
|
|
194
194
|
}, e.id))
|
|
195
195
|
})]
|
|
@@ -198,6 +198,6 @@ function N() {
|
|
|
198
198
|
});
|
|
199
199
|
}
|
|
200
200
|
//#endregion
|
|
201
|
-
export {
|
|
201
|
+
export { j as CoursesMyLearningPagePage };
|
|
202
202
|
|
|
203
203
|
//# sourceMappingURL=CoursesMyLearningPagePage.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CoursesMyLearningPagePage.js","names":[],"sources":["../../../src/technospam/pages/CoursesMyLearningPagePage.tsx"],"sourcesContent":["import { createElement } from 'react';\nimport { Award, BookOpen, GraduationCap, PlayCircle, ShieldCheck } from 'lucide-react';\nimport { Button, GlassCard } from '@burdenoff/fe-libs/ui';\nimport {\n EmptyView,\n ErrorView,\n LoadingRows,\n PageLayout,\n StatCard,\n StatusBadge,\n coverGradient,\n topicIcon,\n} from '../components';\nimport { useTechnoSpam, useTechnoSpamNavigate } from '../providers/TechnoSpamProvider';\nimport {\n useEnrollmentsQuery,\n useMyCertificatesQuery,\n useMyNextLessonsQuery,\n type TecEnrollment,\n} from '../api/technospamApi';\nimport { formatDate, relativeTime } from '../utils/format';\nimport { cn } from '../utils/cn';\nimport {\n FOCUSED_CARD_CLASS,\n focusableCardProps,\n useFocusedRecordParam,\n} from '../hooks/useFocusedRecordParam';\nimport { FOCUS_PARAMS } from '../utils/pageContext';\nimport { resumePath } from '../utils/lessonPath';\n\nfunction EnrollmentRow({\n e,\n onOpen,\n focused,\n onToggle,\n}: {\n e: TecEnrollment;\n onOpen: () => void;\n /** The focused enrollment (`?enrollmentId=`) is the one the assistant acts on. */\n focused: boolean;\n onToggle: () => void;\n}) {\n const seed = `${e.course?.topic ?? ''} ${e.course?.title ?? 'Course'}`;\n return (\n <GlassCard\n className={cn(\n 'group flex cursor-pointer flex-col gap-3 p-4 transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md sm:flex-row sm:items-center sm:gap-4',\n focused && FOCUSED_CARD_CLASS\n )}\n {...focusableCardProps(focused, onToggle)}\n >\n <span\n className={cn(\n 'flex h-12 w-12 shrink-0 items-center justify-center rounded-xl bg-gradient-to-br text-white shadow-sm transition-transform duration-200 group-hover:scale-105',\n coverGradient(seed)\n )}\n >\n {createElement(topicIcon(seed), { size: 20 })}\n </span>\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <button\n type=\"button\"\n aria-pressed={focused}\n onClick={onToggle}\n className=\"truncate text-left text-sm font-semibold text-text-primary group-hover:text-accent-primary\"\n >\n {e.course?.title ?? 'Course'}\n </button>\n <StatusBadge status={e.status} />\n </div>\n <div className=\"mt-1 flex flex-wrap items-center gap-2 text-xs text-text-muted\">\n <span>{e.lessonsCompleted} lessons done</span>\n <span aria-hidden>·</span>\n <span>Active {relativeTime(e.lastActiveAt ?? e.startedAt)}</span>\n </div>\n <div className=\"mt-2 h-1.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: `${e.progressPct}%` }}\n />\n </div>\n </div>\n <div className=\"flex items-center justify-between gap-3 sm:flex-col sm:items-end sm:justify-center\">\n <div className=\"text-lg font-semibold tabular-nums text-text-primary\">{e.progressPct}%</div>\n <Button variant=\"ghost\" size=\"sm\" onClick={onOpen} data-testid={`resume-${e.courseId}`}>\n <PlayCircle size={15} /> Resume\n </Button>\n </div>\n </GlassCard>\n );\n}\n\n// Stable sibling lists: focusing an enrollment clears a focused certificate and vice versa.\nconst CERTIFICATE_SIBLING = [FOCUS_PARAMS.certificate] as const;\nconst ENROLLMENT_SIBLING = [FOCUS_PARAMS.enrollment] as const;\n\nexport function CoursesMyLearningPagePage() {\n const navigate = useTechnoSpamNavigate();\n const { currentUser } = useTechnoSpam();\n const learnerId = currentUser?.id;\n\n const { data, loading, error } = useEnrollmentsQuery(learnerId ? { learnerId } : {});\n // Self-scoped read — `tecListCertificates` needs the cross-learner\n // \"list-all\" action a plain learner does not hold, so their own earned\n // certificates never arrived here.\n const { data: certData } = useMyCertificatesQuery();\n // BOFF-4107: \"Resume\" deep-links into the next incomplete lesson.\n const { data: nextData } = useMyNextLessonsQuery();\n const nextLessonByCourse = new Map(\n (nextData?.tecMyEnrollments?.items ?? []).map((n) => [n.courseId, n.nextLessonId ?? null])\n );\n // The focused enrollment / certificate live in the URL so the assistant's\n // page context names them (\"how far am I in this course?\").\n const [focusedEnrollmentId, setFocusedEnrollmentId] = useFocusedRecordParam(\n FOCUS_PARAMS.enrollment,\n CERTIFICATE_SIBLING\n );\n const [focusedCertificateId, setFocusedCertificateId] = useFocusedRecordParam(\n FOCUS_PARAMS.certificate,\n ENROLLMENT_SIBLING\n );\n\n const enrollments = data?.tecListEnrollments?.items ?? [];\n const certificates = certData?.tecMyCertificates ?? [];\n const completed = enrollments.filter((e) => e.status === 'COMPLETED').length;\n const inProgress = enrollments.filter(\n (e) => e.status === 'IN_PROGRESS' || e.status === 'ENROLLED'\n ).length;\n\n return (\n <PageLayout\n hero\n title=\"My Learning\"\n description=\"Track your enrolled courses, progress, and earned certificates.\"\n actions={\n <Button variant=\"outline\" onClick={() => navigate('/courses/courses-page')}>\n <BookOpen size={16} /> Browse catalog\n </Button>\n }\n >\n <div className=\"mb-6 grid grid-cols-2 gap-3 sm:grid-cols-4\">\n <StatCard\n label=\"Enrolled\"\n value={enrollments.length}\n hint=\"Courses you've enrolled in\"\n icon={BookOpen}\n tone=\"blue\"\n />\n <StatCard\n label=\"In progress\"\n value={inProgress}\n hint=\"Courses in progress\"\n icon={PlayCircle}\n tone=\"indigo\"\n />\n <StatCard\n label=\"Completed\"\n value={completed}\n hint=\"Courses completed\"\n icon={GraduationCap}\n tone=\"green\"\n />\n <StatCard\n label=\"Certificates\"\n value={certificates.length}\n hint=\"Certificates earned\"\n icon={Award}\n tone=\"purple\"\n />\n </div>\n\n {loading && !enrollments.length ? (\n <LoadingRows count={3} height=\"h-24\" />\n ) : error ? (\n <ErrorView title=\"Couldn't load your learning\" error={error} />\n ) : enrollments.length === 0 ? (\n <EmptyView\n icon={<BookOpen size={26} />}\n title=\"You haven't enrolled yet\"\n description=\"Browse the catalog and enroll in a course to start building skill by doing.\"\n action={<Button onClick={() => navigate('/courses/courses-page')}>Browse courses</Button>}\n />\n ) : (\n <div className=\"space-y-3\">\n {enrollments.map((e) => (\n <EnrollmentRow\n key={e.id}\n e={e}\n onOpen={() => navigate(resumePath(e.courseId, nextLessonByCourse.get(e.courseId)))}\n focused={focusedEnrollmentId === e.id}\n onToggle={() => setFocusedEnrollmentId(focusedEnrollmentId === e.id ? null : e.id)}\n />\n ))}\n </div>\n )}\n\n {certificates.length > 0 ? (\n <div className=\"mt-8\">\n <h2 className=\"mb-3 flex items-center gap-2 text-base font-semibold text-text-primary\">\n <Award size={18} className=\"text-accent-teal\" /> Certificates\n </h2>\n <div className=\"grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3\">\n {certificates.map((c) => (\n <GlassCard\n key={c.id}\n className={cn(\n 'group flex cursor-pointer flex-col gap-3 p-4 transition-all duration-200 hover:-translate-y-1 hover:shadow-lg',\n focusedCertificateId === c.id && FOCUSED_CARD_CLASS\n )}\n {...focusableCardProps(focusedCertificateId === c.id, () =>\n setFocusedCertificateId(focusedCertificateId === c.id ? null : c.id)\n )}\n >\n <div className=\"flex items-center gap-3\">\n <span className=\"flex h-11 w-11 shrink-0 items-center justify-center rounded-xl bg-status-warning-bg-subtle text-status-warning-text ring-1 ring-status-warning-border/30 transition-transform duration-200 group-hover:scale-105\">\n <Award size={22} />\n </span>\n <div className=\"min-w-0\">\n <button\n type=\"button\"\n aria-pressed={focusedCertificateId === c.id}\n onClick={() =>\n setFocusedCertificateId(focusedCertificateId === c.id ? null : c.id)\n }\n className=\"block max-w-full truncate text-left text-sm font-medium text-text-primary\"\n >\n {c.course?.title ?? 'Certificate'}\n </button>\n <div className=\"truncate text-xs text-text-muted\">Serial {c.serial}</div>\n </div>\n </div>\n <div className=\"mt-auto flex items-center justify-between border-t border-border-seam pt-3 text-xs text-text-muted\">\n <span>\n Score <span className=\"font-semibold text-text-primary\">{c.score}</span> ·{' '}\n {formatDate(c.issuedAt)}\n </span>\n {c.status === 'ISSUED' ? (\n <span className=\"inline-flex items-center gap-1 rounded-full border border-status-success-border bg-status-success-bg-subtle px-2 py-0.5 font-medium text-status-success-text\">\n <ShieldCheck size={12} /> Verified\n </span>\n ) : (\n <StatusBadge status={c.status} />\n )}\n </div>\n </GlassCard>\n ))}\n </div>\n </div>\n ) : null}\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CoursesMyLearningPagePage.js","names":[],"sources":["../../../src/technospam/pages/CoursesMyLearningPagePage.tsx"],"sourcesContent":["import { createElement } from 'react';\nimport { Award, BookOpen, GraduationCap, PlayCircle, ShieldCheck } from 'lucide-react';\nimport { Button, GlassCard } from '@burdenoff/fe-libs/ui';\nimport {\n EmptyView,\n ErrorView,\n LoadingRows,\n PageLayout,\n StatCard,\n StatusBadge,\n coverGradient,\n topicIcon,\n} from '../components';\nimport { useTechnoSpamNavigate } from '../providers/TechnoSpamProvider';\nimport {\n useMyCertificatesQuery,\n useMyEnrollmentsQuery,\n type TecEnrollment,\n} from '../api/technospamApi';\nimport { formatDate, relativeTime } from '../utils/format';\nimport { cn } from '../utils/cn';\nimport {\n FOCUSED_CARD_CLASS,\n focusableCardProps,\n useFocusedRecordParam,\n} from '../hooks/useFocusedRecordParam';\nimport { FOCUS_PARAMS } from '../utils/pageContext';\nimport { resumePath } from '../utils/lessonPath';\n\nfunction EnrollmentRow({\n e,\n onOpen,\n focused,\n onToggle,\n}: {\n e: TecEnrollment;\n onOpen: () => void;\n /** The focused enrollment (`?enrollmentId=`) is the one the assistant acts on. */\n focused: boolean;\n onToggle: () => void;\n}) {\n const seed = `${e.course?.topic ?? ''} ${e.course?.title ?? 'Course'}`;\n return (\n <GlassCard\n className={cn(\n 'group flex cursor-pointer flex-col gap-3 p-4 transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md sm:flex-row sm:items-center sm:gap-4',\n focused && FOCUSED_CARD_CLASS\n )}\n {...focusableCardProps(focused, onToggle)}\n >\n <span\n className={cn(\n 'flex h-12 w-12 shrink-0 items-center justify-center rounded-xl bg-gradient-to-br text-white shadow-sm transition-transform duration-200 group-hover:scale-105',\n coverGradient(seed)\n )}\n >\n {createElement(topicIcon(seed), { size: 20 })}\n </span>\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <button\n type=\"button\"\n aria-pressed={focused}\n onClick={onToggle}\n className=\"truncate text-left text-sm font-semibold text-text-primary group-hover:text-accent-primary\"\n >\n {e.course?.title ?? 'Course'}\n </button>\n <StatusBadge status={e.status} />\n </div>\n <div className=\"mt-1 flex flex-wrap items-center gap-2 text-xs text-text-muted\">\n <span>{e.lessonsCompleted} lessons done</span>\n <span aria-hidden>·</span>\n <span>Active {relativeTime(e.lastActiveAt ?? e.startedAt)}</span>\n </div>\n <div className=\"mt-2 h-1.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: `${e.progressPct}%` }}\n />\n </div>\n </div>\n <div className=\"flex items-center justify-between gap-3 sm:flex-col sm:items-end sm:justify-center\">\n <div className=\"text-lg font-semibold tabular-nums text-text-primary\">{e.progressPct}%</div>\n <Button variant=\"ghost\" size=\"sm\" onClick={onOpen} data-testid={`resume-${e.courseId}`}>\n <PlayCircle size={15} /> Resume\n </Button>\n </div>\n </GlassCard>\n );\n}\n\n// Stable sibling lists: focusing an enrollment clears a focused certificate and vice versa.\nconst CERTIFICATE_SIBLING = [FOCUS_PARAMS.certificate] as const;\nconst ENROLLMENT_SIBLING = [FOCUS_PARAMS.enrollment] as const;\n\nexport function CoursesMyLearningPagePage() {\n const navigate = useTechnoSpamNavigate();\n\n // BOFF-10481: the learner's own enrollments and certificates come from the\n // canonical, self-scoped Buildmyiq fields. The previous cross-learner\n // `tecListEnrollments` / list-all reads needed an instructor permission a\n // plain learner does not hold. Each enrollment carries its next lesson, so\n // \"Resume\" (BOFF-4107) deep-links into the next incomplete lesson.\n const { enrollments, loading, error } = useMyEnrollmentsQuery();\n const { certificates } = useMyCertificatesQuery();\n // The focused enrollment / certificate live in the URL so the assistant's\n // page context names them (\"how far am I in this course?\").\n const [focusedEnrollmentId, setFocusedEnrollmentId] = useFocusedRecordParam(\n FOCUS_PARAMS.enrollment,\n CERTIFICATE_SIBLING\n );\n const [focusedCertificateId, setFocusedCertificateId] = useFocusedRecordParam(\n FOCUS_PARAMS.certificate,\n ENROLLMENT_SIBLING\n );\n\n const completed = enrollments.filter((e) => e.status === 'COMPLETED').length;\n const inProgress = enrollments.filter(\n (e) => e.status === 'IN_PROGRESS' || e.status === 'ENROLLED'\n ).length;\n\n return (\n <PageLayout\n hero\n title=\"My Learning\"\n description=\"Track your enrolled courses, progress, and earned certificates.\"\n actions={\n <Button variant=\"outline\" onClick={() => navigate('/courses/courses-page')}>\n <BookOpen size={16} /> Browse catalog\n </Button>\n }\n >\n <div className=\"mb-6 grid grid-cols-2 gap-3 sm:grid-cols-4\">\n <StatCard\n label=\"Enrolled\"\n value={enrollments.length}\n hint=\"Courses you've enrolled in\"\n icon={BookOpen}\n tone=\"blue\"\n />\n <StatCard\n label=\"In progress\"\n value={inProgress}\n hint=\"Courses in progress\"\n icon={PlayCircle}\n tone=\"indigo\"\n />\n <StatCard\n label=\"Completed\"\n value={completed}\n hint=\"Courses completed\"\n icon={GraduationCap}\n tone=\"green\"\n />\n <StatCard\n label=\"Certificates\"\n value={certificates.length}\n hint=\"Certificates earned\"\n icon={Award}\n tone=\"purple\"\n />\n </div>\n\n {loading && !enrollments.length ? (\n <LoadingRows count={3} height=\"h-24\" />\n ) : error ? (\n <ErrorView title=\"Couldn't load your learning\" error={error} />\n ) : enrollments.length === 0 ? (\n <EmptyView\n icon={<BookOpen size={26} />}\n title=\"You haven't enrolled yet\"\n description=\"Browse the catalog and enroll in a course to start building skill by doing.\"\n action={<Button onClick={() => navigate('/courses/courses-page')}>Browse courses</Button>}\n />\n ) : (\n <div className=\"space-y-3\">\n {enrollments.map((e) => (\n <EnrollmentRow\n key={e.id}\n e={e}\n onOpen={() => navigate(resumePath(e.courseId, e.nextLessonId))}\n focused={focusedEnrollmentId === e.id}\n onToggle={() => setFocusedEnrollmentId(focusedEnrollmentId === e.id ? null : e.id)}\n />\n ))}\n </div>\n )}\n\n {certificates.length > 0 ? (\n <div className=\"mt-8\">\n <h2 className=\"mb-3 flex items-center gap-2 text-base font-semibold text-text-primary\">\n <Award size={18} className=\"text-accent-teal\" /> Certificates\n </h2>\n <div className=\"grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3\">\n {certificates.map((c) => (\n <GlassCard\n key={c.id}\n className={cn(\n 'group flex cursor-pointer flex-col gap-3 p-4 transition-all duration-200 hover:-translate-y-1 hover:shadow-lg',\n focusedCertificateId === c.id && FOCUSED_CARD_CLASS\n )}\n {...focusableCardProps(focusedCertificateId === c.id, () =>\n setFocusedCertificateId(focusedCertificateId === c.id ? null : c.id)\n )}\n >\n <div className=\"flex items-center gap-3\">\n <span className=\"flex h-11 w-11 shrink-0 items-center justify-center rounded-xl bg-status-warning-bg-subtle text-status-warning-text ring-1 ring-status-warning-border/30 transition-transform duration-200 group-hover:scale-105\">\n <Award size={22} />\n </span>\n <div className=\"min-w-0\">\n <button\n type=\"button\"\n aria-pressed={focusedCertificateId === c.id}\n onClick={() =>\n setFocusedCertificateId(focusedCertificateId === c.id ? null : c.id)\n }\n className=\"block max-w-full truncate text-left text-sm font-medium text-text-primary\"\n >\n {c.course?.title ?? 'Certificate'}\n </button>\n <div className=\"truncate text-xs text-text-muted\">Serial {c.serial}</div>\n </div>\n </div>\n <div className=\"mt-auto flex items-center justify-between border-t border-border-seam pt-3 text-xs text-text-muted\">\n <span>\n Score <span className=\"font-semibold text-text-primary\">{c.score}</span> ·{' '}\n {formatDate(c.issuedAt)}\n </span>\n {c.status === 'ISSUED' ? (\n <span className=\"inline-flex items-center gap-1 rounded-full border border-status-success-border bg-status-success-bg-subtle px-2 py-0.5 font-medium text-status-success-text\">\n <ShieldCheck size={12} /> Verified\n </span>\n ) : (\n <StatusBadge status={c.status} />\n )}\n </div>\n </GlassCard>\n ))}\n </div>\n </div>\n ) : null}\n </PageLayout>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AA6BA,SAAS,EAAc,EACrB,GACA,WACA,YACA,eAOC;CACD,IAAM,IAAO,GAAG,EAAE,QAAQ,SAAS,GAAG,GAAG,EAAE,QAAQ,SAAS;AAC5D,QACE,kBAAC,GAAD;EACE,WAAW,EACT,wJACA,KAAA,6BACD;EACD,GAAI,EAAmB,GAAS,EAAS;YAL3C;GAOE,kBAAC,QAAD;IACE,WAAW,EACT,iKACA,EAAc,EAAK,CACpB;cAEA,EAAc,EAAU,EAAK,EAAE,EAAE,MAAM,IAAI,CAAC;IACxC,CAAA;GACP,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,UAAD;OACE,MAAK;OACL,gBAAc;OACd,SAAS;OACT,WAAU;iBAET,EAAE,QAAQ,SAAS;OACb,CAAA,EACT,kBAAC,GAAD,EAAa,QAAQ,EAAE,QAAU,CAAA,CAC7B;;KACN,kBAAC,OAAD;MAAK,WAAU;gBAAf;OACE,kBAAC,QAAD,EAAA,UAAA,CAAO,EAAE,kBAAiB,gBAAoB,EAAA,CAAA;OAC9C,kBAAC,QAAD;QAAM,eAAA;kBAAY;QAAQ,CAAA;OAC1B,kBAAC,QAAD,EAAA,UAAA,CAAM,WAAQ,EAAa,EAAE,gBAAgB,EAAE,UAAU,CAAQ,EAAA,CAAA;OAC7D;;KACN,kBAAC,OAAD;MAAK,WAAU;gBACb,kBAAC,OAAD;OACE,WAAU;OACV,OAAO,EAAE,OAAO,GAAG,EAAE,YAAY,IAAI;OACrC,CAAA;MACE,CAAA;KACF;;GACN,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,OAAD;KAAK,WAAU;eAAf,CAAuE,EAAE,aAAY,IAAO;QAC5F,kBAAC,GAAD;KAAQ,SAAQ;KAAQ,MAAK;KAAK,SAAS;KAAQ,eAAa,UAAU,EAAE;eAA5E,CACE,kBAAC,GAAD,EAAY,MAAM,IAAM,CAAA,EAAA,UACjB;OACL;;GACI;;;AAKhB,IAAM,IAAsB,CAAC,EAAa,YAAY,EAChD,IAAqB,CAAC,EAAa,WAAW;AAEpD,SAAgB,IAA4B;CAC1C,IAAM,IAAW,GAAuB,EAOlC,EAAE,gBAAa,YAAS,aAAU,GAAuB,EACzD,EAAE,oBAAiB,GAAwB,EAG3C,CAAC,GAAqB,KAA0B,EACpD,EAAa,YACb,EACD,EACK,CAAC,GAAsB,KAA2B,EACtD,EAAa,aACb,EACD,EAEK,IAAY,EAAY,QAAQ,MAAM,EAAE,WAAW,YAAY,CAAC,QAChE,IAAa,EAAY,QAC5B,MAAM,EAAE,WAAW,iBAAiB,EAAE,WAAW,WACnD,CAAC;AAEF,QACE,kBAAC,GAAD;EACE,MAAA;EACA,OAAM;EACN,aAAY;EACZ,SACE,kBAAC,GAAD;GAAQ,SAAQ;GAAU,eAAe,EAAS,wBAAwB;aAA1E,CACE,kBAAC,GAAD,EAAU,MAAM,IAAM,CAAA,EAAA,kBACf;;YAPb;GAUE,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,GAAD;MACE,OAAM;MACN,OAAO,EAAY;MACnB,MAAK;MACL,MAAM;MACN,MAAK;MACL,CAAA;KACF,kBAAC,GAAD;MACE,OAAM;MACN,OAAO;MACP,MAAK;MACL,MAAM;MACN,MAAK;MACL,CAAA;KACF,kBAAC,GAAD;MACE,OAAM;MACN,OAAO;MACP,MAAK;MACL,MAAM;MACN,MAAK;MACL,CAAA;KACF,kBAAC,GAAD;MACE,OAAM;MACN,OAAO,EAAa;MACpB,MAAK;MACL,MAAM;MACN,MAAK;MACL,CAAA;KACE;;GAEL,KAAW,CAAC,EAAY,SACvB,kBAAC,GAAD;IAAa,OAAO;IAAG,QAAO;IAAS,CAAA,GACrC,IACF,kBAAC,GAAD;IAAW,OAAM;IAAqC;IAAS,CAAA,GAC7D,EAAY,WAAW,IACzB,kBAAC,GAAD;IACE,MAAM,kBAAC,GAAD,EAAU,MAAM,IAAM,CAAA;IAC5B,OAAM;IACN,aAAY;IACZ,QAAQ,kBAAC,GAAD;KAAQ,eAAe,EAAS,wBAAwB;eAAE;KAAuB,CAAA;IACzF,CAAA,GAEF,kBAAC,OAAD;IAAK,WAAU;cACZ,EAAY,KAAK,MAChB,kBAAC,GAAD;KAEK;KACH,cAAc,EAAS,EAAW,EAAE,UAAU,EAAE,aAAa,CAAC;KAC9D,SAAS,MAAwB,EAAE;KACnC,gBAAgB,EAAuB,MAAwB,EAAE,KAAK,OAAO,EAAE,GAAG;KAClF,EALK,EAAE,GAKP,CACF;IACE,CAAA;GAGP,EAAa,SAAS,IACrB,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,MAAD;KAAI,WAAU;eAAd,CACE,kBAAC,GAAD;MAAO,MAAM;MAAI,WAAU;MAAqB,CAAA,EAAA,gBAC7C;QACL,kBAAC,OAAD;KAAK,WAAU;eACZ,EAAa,KAAK,MACjB,kBAAC,GAAD;MAEE,WAAW,EACT,iHACA,MAAyB,EAAE,MAAA,6BAC5B;MACD,GAAI,EAAmB,MAAyB,EAAE,UAChD,EAAwB,MAAyB,EAAE,KAAK,OAAO,EAAE,GAAG,CACrE;gBARH,CAUE,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,QAAD;QAAM,WAAU;kBACd,kBAAC,GAAD,EAAO,MAAM,IAAM,CAAA;QACd,CAAA,EACP,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,UAAD;SACE,MAAK;SACL,gBAAc,MAAyB,EAAE;SACzC,eACE,EAAwB,MAAyB,EAAE,KAAK,OAAO,EAAE,GAAG;SAEtE,WAAU;mBAET,EAAE,QAAQ,SAAS;SACb,CAAA,EACT,kBAAC,OAAD;SAAK,WAAU;mBAAf,CAAkD,WAAQ,EAAE,OAAa;WACrE;UACF;UACN,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,QAAD,EAAA,UAAA;QAAM;QACE,kBAAC,QAAD;SAAM,WAAU;mBAAmC,EAAE;SAAa,CAAA;;QAAG;QAC1E,EAAW,EAAE,SAAS;QAClB,EAAA,CAAA,EACN,EAAE,WAAW,WACZ,kBAAC,QAAD;QAAM,WAAU;kBAAhB,CACE,kBAAC,GAAD,EAAa,MAAM,IAAM,CAAA,EAAA,YACpB;YAEP,kBAAC,GAAD,EAAa,QAAQ,EAAE,QAAU,CAAA,CAE/B;SACI;QAxCL,EAAE,GAwCG,CACZ;KACE,CAAA,CACF;QACJ;GACO"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DashboardDashboardPagePage.d.ts","sourceRoot":"","sources":["../../../src/technospam/pages/DashboardDashboardPagePage.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"DashboardDashboardPagePage.d.ts","sourceRoot":"","sources":["../../../src/technospam/pages/DashboardDashboardPagePage.tsx"],"names":[],"mappings":"AAqNA,wBAAgB,0BAA0B,gCAmVzC"}
|