@burdenoff/microfe-buildmyiq 2026.731.2 → 2026.731.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/buildmyiq/components/OfflineLessonControl.d.ts +28 -0
- package/dist/buildmyiq/components/OfflineLessonControl.d.ts.map +1 -0
- package/dist/buildmyiq/components/OfflineLessonControl.js +100 -0
- package/dist/buildmyiq/components/OfflineLessonControl.js.map +1 -0
- package/dist/buildmyiq/components/index.d.ts +1 -0
- package/dist/buildmyiq/components/index.d.ts.map +1 -1
- package/dist/buildmyiq/components/index.js +1 -0
- package/dist/buildmyiq/pages/learning/LessonPlayerPage.d.ts.map +1 -1
- package/dist/buildmyiq/pages/learning/LessonPlayerPage.js +160 -158
- package/dist/buildmyiq/pages/learning/LessonPlayerPage.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { BuiLesson } from '../types';
|
|
2
|
+
|
|
3
|
+
interface OfflineLessonInput {
|
|
4
|
+
id: string;
|
|
5
|
+
courseId: string;
|
|
6
|
+
title: string;
|
|
7
|
+
type: string;
|
|
8
|
+
content: string | null;
|
|
9
|
+
mediaUrl?: string | null;
|
|
10
|
+
}
|
|
11
|
+
interface BuildmyiqOfflineLessonsBridge {
|
|
12
|
+
cache(lesson: OfflineLessonInput): Promise<boolean>;
|
|
13
|
+
isCached(lessonId: string): Promise<boolean>;
|
|
14
|
+
remove(lessonId: string): Promise<boolean>;
|
|
15
|
+
}
|
|
16
|
+
declare global {
|
|
17
|
+
interface Window {
|
|
18
|
+
__buildmyiqNative?: {
|
|
19
|
+
offlineLessons: BuildmyiqOfflineLessonsBridge;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
interface OfflineLessonControlProps {
|
|
24
|
+
lesson: BuiLesson;
|
|
25
|
+
}
|
|
26
|
+
export declare function OfflineLessonControl({ lesson }: OfflineLessonControlProps): import("react").JSX.Element | null;
|
|
27
|
+
export {};
|
|
28
|
+
//# sourceMappingURL=OfflineLessonControl.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"OfflineLessonControl.d.ts","sourceRoot":"","sources":["../../../src/buildmyiq/components/OfflineLessonControl.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAMH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAE1C,UAAU,kBAAkB;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED,UAAU,6BAA6B;IACrC,KAAK,CAAC,MAAM,EAAE,kBAAkB,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACpD,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC7C,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CAC5C;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,MAAM;QACd,iBAAiB,CAAC,EAAE;YAClB,cAAc,EAAE,6BAA6B,CAAC;SAC/C,CAAC;KACH;CACF;AASD,UAAU,yBAAyB;IACjC,MAAM,EAAE,SAAS,CAAC;CACnB;AAED,wBAAgB,oBAAoB,CAAC,EAAE,MAAM,EAAE,EAAE,yBAAyB,sCAgHzE"}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { useEcoFile as e } from "../hooks/useBuiFiles.js";
|
|
2
|
+
import "../hooks/index.js";
|
|
3
|
+
import { useEffect as t, useState as n } from "react";
|
|
4
|
+
import { CloudDownload as r, CloudOff as i, HardDriveDownload as a, Loader2 as o, Trash2 as s } from "lucide-react";
|
|
5
|
+
import { jsx as c, jsxs as l } from "react/jsx-runtime";
|
|
6
|
+
import { Button as u } from "@burdenoff/fe-libs/ui";
|
|
7
|
+
//#region src/buildmyiq/components/OfflineLessonControl.tsx
|
|
8
|
+
function d() {
|
|
9
|
+
if (!(typeof window > "u")) return window.__buildmyiqNative?.offlineLessons;
|
|
10
|
+
}
|
|
11
|
+
function f({ lesson: f }) {
|
|
12
|
+
let [p, m] = n(() => d() ? "checking" : "unavailable"), h = e(f.media?.id);
|
|
13
|
+
if (t(() => {
|
|
14
|
+
let e = d();
|
|
15
|
+
if (!e) return;
|
|
16
|
+
let t = !1;
|
|
17
|
+
return e.isCached(f.id).then((e) => {
|
|
18
|
+
t || m(e ? "cached" : "idle");
|
|
19
|
+
}).catch(() => {
|
|
20
|
+
t || m("idle");
|
|
21
|
+
}), () => {
|
|
22
|
+
t = !0;
|
|
23
|
+
};
|
|
24
|
+
}, [f.id]), p === "unavailable" || p === "checking") return null;
|
|
25
|
+
let g = async () => {
|
|
26
|
+
let e = d();
|
|
27
|
+
if (!e) {
|
|
28
|
+
m("unavailable");
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
m("downloading");
|
|
32
|
+
try {
|
|
33
|
+
m(await e.cache({
|
|
34
|
+
id: f.id,
|
|
35
|
+
courseId: f.courseId,
|
|
36
|
+
title: f.title,
|
|
37
|
+
type: f.type,
|
|
38
|
+
content: f.content ?? null,
|
|
39
|
+
mediaUrl: h.data?.downloadUrl ?? null
|
|
40
|
+
}) ? "cached" : "error");
|
|
41
|
+
} catch {
|
|
42
|
+
m("error");
|
|
43
|
+
}
|
|
44
|
+
}, _ = async () => {
|
|
45
|
+
let e = d();
|
|
46
|
+
if (!e) {
|
|
47
|
+
m("unavailable");
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
m("removing");
|
|
51
|
+
try {
|
|
52
|
+
m(await e.remove(f.id) ? "idle" : "error");
|
|
53
|
+
} catch {
|
|
54
|
+
m("error");
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
return p === "cached" ? /* @__PURE__ */ l("div", {
|
|
58
|
+
className: "inline-flex items-center gap-1.5",
|
|
59
|
+
children: [/* @__PURE__ */ l("span", {
|
|
60
|
+
className: "inline-flex items-center gap-1.5 rounded-full bg-[var(--color-accent-soft)] px-2.5 py-1 text-xs font-medium text-text-primary",
|
|
61
|
+
children: [/* @__PURE__ */ c(a, { className: "h-3.5 w-3.5 text-[var(--color-accent-teal)]" }), "Available offline"]
|
|
62
|
+
}), /* @__PURE__ */ c(u, {
|
|
63
|
+
type: "button",
|
|
64
|
+
variant: "ghost",
|
|
65
|
+
size: "icon-sm",
|
|
66
|
+
onClick: () => void _(),
|
|
67
|
+
"aria-label": "Remove offline copy of this lesson",
|
|
68
|
+
title: "Remove offline copy",
|
|
69
|
+
children: /* @__PURE__ */ c(s, { className: "h-3.5 w-3.5" })
|
|
70
|
+
})]
|
|
71
|
+
}) : p === "downloading" || p === "removing" ? /* @__PURE__ */ l(u, {
|
|
72
|
+
type: "button",
|
|
73
|
+
variant: "outline",
|
|
74
|
+
size: "sm",
|
|
75
|
+
disabled: !0,
|
|
76
|
+
children: [/* @__PURE__ */ c(o, { className: "mr-1.5 h-3.5 w-3.5 animate-spin" }), p === "downloading" ? "Downloading…" : "Removing…"]
|
|
77
|
+
}) : p === "error" ? /* @__PURE__ */ l("div", {
|
|
78
|
+
className: "inline-flex items-center gap-1.5",
|
|
79
|
+
children: [/* @__PURE__ */ l("span", {
|
|
80
|
+
className: "inline-flex items-center gap-1 text-xs text-status-error-text",
|
|
81
|
+
children: [/* @__PURE__ */ c(i, { className: "h-3.5 w-3.5" }), " Could not save offline"]
|
|
82
|
+
}), /* @__PURE__ */ l(u, {
|
|
83
|
+
type: "button",
|
|
84
|
+
variant: "outline",
|
|
85
|
+
size: "sm",
|
|
86
|
+
onClick: () => void g(),
|
|
87
|
+
children: [/* @__PURE__ */ c(r, { className: "mr-1.5 h-3.5 w-3.5" }), " Retry"]
|
|
88
|
+
})]
|
|
89
|
+
}) : /* @__PURE__ */ l(u, {
|
|
90
|
+
type: "button",
|
|
91
|
+
variant: "outline",
|
|
92
|
+
size: "sm",
|
|
93
|
+
onClick: () => void g(),
|
|
94
|
+
children: [/* @__PURE__ */ c(r, { className: "mr-1.5 h-3.5 w-3.5" }), " Download for offline"]
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
//#endregion
|
|
98
|
+
export { f as OfflineLessonControl };
|
|
99
|
+
|
|
100
|
+
//# sourceMappingURL=OfflineLessonControl.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"OfflineLessonControl.js","names":[],"sources":["../../../src/buildmyiq/components/OfflineLessonControl.tsx"],"sourcesContent":["/**\n * OfflineLessonControl — \"Download for offline\" control for the lesson player (BOFF-4421).\n * @module buildmyiq/components/OfflineLessonControl\n *\n * Wires the lesson player to the app shell's offline-lesson cache. The\n * caching logic itself lives in `buildmyiq-app` (`src/lib/native/offline-lessons.ts`,\n * Capacitor Filesystem on native / `localStorage` fallback on web) and is\n * exposed as `window.__buildmyiqNative.offlineLessons` by that repo's\n * `installBuildmyiqNativeBridge()` (`src/lib/native/buildmyiq-bridge.ts`).\n *\n * This MFE never imports from `buildmyiq-app` directly — no MFE-to-shell npm\n * dependency, matching this monorepo's build-time MFE composition rules — it\n * only ever talks to the plain `window` global the shell installs. If that\n * bridge isn't present (e.g. this component somehow rendered outside the\n * buildmyiq app shell), the control quietly renders nothing: offline caching\n * is a UX nicety, never a hard dependency of the lesson player.\n */\n\nimport { useEffect, useState } from 'react';\nimport { CloudDownload, CloudOff, HardDriveDownload, Loader2, Trash2 } from 'lucide-react';\nimport { Button } from '@burdenoff/fe-libs/ui';\nimport { useEcoFile } from '../hooks';\nimport type { BuiLesson } from '../types';\n\ninterface OfflineLessonInput {\n id: string;\n courseId: string;\n title: string;\n type: string;\n content: string | null;\n mediaUrl?: string | null;\n}\n\ninterface BuildmyiqOfflineLessonsBridge {\n cache(lesson: OfflineLessonInput): Promise<boolean>;\n isCached(lessonId: string): Promise<boolean>;\n remove(lessonId: string): Promise<boolean>;\n}\n\ndeclare global {\n interface Window {\n __buildmyiqNative?: {\n offlineLessons: BuildmyiqOfflineLessonsBridge;\n };\n }\n}\n\nfunction getBridge(): BuildmyiqOfflineLessonsBridge | undefined {\n if (typeof window === 'undefined') return undefined;\n return window.__buildmyiqNative?.offlineLessons;\n}\n\ntype Status = 'checking' | 'unavailable' | 'idle' | 'downloading' | 'cached' | 'removing' | 'error';\n\ninterface OfflineLessonControlProps {\n lesson: BuiLesson;\n}\n\nexport function OfflineLessonControl({ lesson }: OfflineLessonControlProps) {\n // Lazy initializer keeps the \"no bridge installed\" branch out of the\n // effect body (only promise-callback setState calls happen there).\n const [status, setStatus] = useState<Status>(() => (getBridge() ? 'checking' : 'unavailable'));\n // Reuses the same React Query cache entry `FileLink` populates for this\n // lesson's `media` (same queryKey) — no extra network round trip.\n const mediaFile = useEcoFile(lesson.media?.id);\n\n useEffect(() => {\n const bridge = getBridge();\n if (!bridge) return;\n let cancelled = false;\n bridge\n .isCached(lesson.id)\n .then((cached) => {\n if (!cancelled) setStatus(cached ? 'cached' : 'idle');\n })\n .catch(() => {\n if (!cancelled) setStatus('idle');\n });\n return () => {\n cancelled = true;\n };\n }, [lesson.id]);\n\n if (status === 'unavailable' || status === 'checking') return null;\n\n const handleDownload = async () => {\n const bridge = getBridge();\n if (!bridge) {\n setStatus('unavailable');\n return;\n }\n setStatus('downloading');\n try {\n const ok = await bridge.cache({\n id: lesson.id,\n courseId: lesson.courseId,\n title: lesson.title,\n type: lesson.type,\n content: lesson.content ?? null,\n mediaUrl: mediaFile.data?.downloadUrl ?? null,\n });\n setStatus(ok ? 'cached' : 'error');\n } catch {\n setStatus('error');\n }\n };\n\n const handleRemove = async () => {\n const bridge = getBridge();\n if (!bridge) {\n setStatus('unavailable');\n return;\n }\n setStatus('removing');\n try {\n const ok = await bridge.remove(lesson.id);\n setStatus(ok ? 'idle' : 'error');\n } catch {\n setStatus('error');\n }\n };\n\n if (status === 'cached') {\n return (\n <div className=\"inline-flex items-center gap-1.5\">\n <span className=\"inline-flex items-center gap-1.5 rounded-full bg-[var(--color-accent-soft)] px-2.5 py-1 text-xs font-medium text-text-primary\">\n <HardDriveDownload className=\"h-3.5 w-3.5 text-[var(--color-accent-teal)]\" />\n Available offline\n </span>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-sm\"\n onClick={() => void handleRemove()}\n aria-label=\"Remove offline copy of this lesson\"\n title=\"Remove offline copy\"\n >\n <Trash2 className=\"h-3.5 w-3.5\" />\n </Button>\n </div>\n );\n }\n\n if (status === 'downloading' || status === 'removing') {\n return (\n <Button type=\"button\" variant=\"outline\" size=\"sm\" disabled>\n <Loader2 className=\"mr-1.5 h-3.5 w-3.5 animate-spin\" />\n {status === 'downloading' ? 'Downloading…' : 'Removing…'}\n </Button>\n );\n }\n\n if (status === 'error') {\n return (\n <div className=\"inline-flex items-center gap-1.5\">\n <span className=\"inline-flex items-center gap-1 text-xs text-status-error-text\">\n <CloudOff className=\"h-3.5 w-3.5\" /> Could not save offline\n </span>\n <Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={() => void handleDownload()}>\n <CloudDownload className=\"mr-1.5 h-3.5 w-3.5\" /> Retry\n </Button>\n </div>\n );\n }\n\n return (\n <Button type=\"button\" variant=\"outline\" size=\"sm\" onClick={() => void handleDownload()}>\n <CloudDownload className=\"mr-1.5 h-3.5 w-3.5\" /> Download for offline\n </Button>\n );\n}\n"],"mappings":";;;;;;;AA+CA,SAAS,IAAuD;CAC1D,aAAO,SAAW,MACtB,OAAO,OAAO,mBAAmB;AACnC;AAQA,SAAgB,EAAqB,EAAE,aAAqC;CAG1E,IAAM,CAAC,GAAQ,KAAa,QAAwB,EAAU,IAAI,aAAa,aAAc,GAGvF,IAAY,EAAW,EAAO,OAAO,EAAE;CAmB7C,IAjBA,QAAgB;EACd,IAAM,IAAS,EAAU;EACzB,IAAI,CAAC,GAAQ;EACb,IAAI,IAAY;EAShB,OARA,EACG,SAAS,EAAO,EAAE,EAClB,MAAM,MAAW;GAChB,AAAK,KAAW,EAAU,IAAS,WAAW,MAAM;EACtD,CAAC,EACA,YAAY;GACX,AAAK,KAAW,EAAU,MAAM;EAClC,CAAC,SACU;GACX,IAAY;EACd;CACF,GAAG,CAAC,EAAO,EAAE,CAAC,GAEV,MAAW,iBAAiB,MAAW,YAAY,OAAO;CAE9D,IAAM,IAAiB,YAAY;EACjC,IAAM,IAAS,EAAU;EACzB,IAAI,CAAC,GAAQ;GACX,EAAU,aAAa;GACvB;EACF;EACA,EAAU,aAAa;EACvB,IAAI;GASF,EAAU,MARO,EAAO,MAAM;IAC5B,IAAI,EAAO;IACX,UAAU,EAAO;IACjB,OAAO,EAAO;IACd,MAAM,EAAO;IACb,SAAS,EAAO,WAAW;IAC3B,UAAU,EAAU,MAAM,eAAe;GAC3C,CAAC,IACc,WAAW,OAAO;EACnC,QAAQ;GACN,EAAU,OAAO;EACnB;CACF,GAEM,IAAe,YAAY;EAC/B,IAAM,IAAS,EAAU;EACzB,IAAI,CAAC,GAAQ;GACX,EAAU,aAAa;GACvB;EACF;EACA,EAAU,UAAU;EACpB,IAAI;GAEF,EAAU,MADO,EAAO,OAAO,EAAO,EAAE,IACzB,SAAS,OAAO;EACjC,QAAQ;GACN,EAAU,OAAO;EACnB;CACF;CA6CA,OA3CI,MAAW,WAEX,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,QAAD;GAAM,WAAU;aAAhB,CACE,kBAAC,GAAD,EAAmB,WAAU,8CAA+C,CAAA,GAAC,mBAEzE;MACN,kBAAC,GAAD;GACE,MAAK;GACL,SAAQ;GACR,MAAK;GACL,eAAe,KAAK,EAAa;GACjC,cAAW;GACX,OAAM;aAEN,kBAAC,GAAD,EAAQ,WAAU,cAAe,CAAA;EAC3B,CAAA,CACL;MAIL,MAAW,iBAAiB,MAAW,aAEvC,kBAAC,GAAD;EAAQ,MAAK;EAAS,SAAQ;EAAU,MAAK;EAAK,UAAA;YAAlD,CACE,kBAAC,GAAD,EAAS,WAAU,kCAAmC,CAAA,GACrD,MAAW,gBAAgB,iBAAiB,WACvC;MAIR,MAAW,UAEX,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,QAAD;GAAM,WAAU;aAAhB,CACE,kBAAC,GAAD,EAAU,WAAU,cAAe,CAAA,GAAC,yBAChC;MACN,kBAAC,GAAD;GAAQ,MAAK;GAAS,SAAQ;GAAU,MAAK;GAAK,eAAe,KAAK,EAAe;aAArF,CACE,kBAAC,GAAD,EAAe,WAAU,qBAAsB,CAAA,GAAC,QAC1C;IACL;MAKP,kBAAC,GAAD;EAAQ,MAAK;EAAS,SAAQ;EAAU,MAAK;EAAK,eAAe,KAAK,EAAe;YAArF,CACE,kBAAC,GAAD,EAAe,WAAU,qBAAsB,CAAA,GAAC,uBAC1C;;AAEZ"}
|
|
@@ -20,4 +20,5 @@ export { BuiDiscussion } from './BuiDiscussion';
|
|
|
20
20
|
export { TutorPanel } from './TutorPanel';
|
|
21
21
|
export { BuiTags } from './BuiTags';
|
|
22
22
|
export { BuiPermissionButton, type BuiPermissionButtonProps } from './BuiPermissionButton';
|
|
23
|
+
export { OfflineLessonControl } from './OfflineLessonControl';
|
|
23
24
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/buildmyiq/components/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAC/D,OAAO,EAAE,SAAS,EAAE,KAAK,MAAM,EAAE,MAAM,aAAa,CAAC;AACrD,OAAO,EAAE,UAAU,EAAE,KAAK,YAAY,EAAE,MAAM,cAAc,CAAC;AAC7D,OAAO,EAAE,IAAI,EAAE,KAAK,OAAO,EAAE,MAAM,QAAQ,CAAC;AAC5C,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAClC,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAChD,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,mBAAmB,EAAE,KAAK,wBAAwB,EAAE,MAAM,uBAAuB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/buildmyiq/components/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAC/D,OAAO,EAAE,SAAS,EAAE,KAAK,MAAM,EAAE,MAAM,aAAa,CAAC;AACrD,OAAO,EAAE,UAAU,EAAE,KAAK,YAAY,EAAE,MAAM,cAAc,CAAC;AAC7D,OAAO,EAAE,IAAI,EAAE,KAAK,OAAO,EAAE,MAAM,QAAQ,CAAC;AAC5C,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAClC,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAChD,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,mBAAmB,EAAE,KAAK,wBAAwB,EAAE,MAAM,uBAAuB,CAAC;AAC3F,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LessonPlayerPage.d.ts","sourceRoot":"","sources":["../../../../src/buildmyiq/pages/learning/LessonPlayerPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"LessonPlayerPage.d.ts","sourceRoot":"","sources":["../../../../src/buildmyiq/pages/learning/LessonPlayerPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC;AA+DhC,eAAO,MAAM,gBAAgB,EAAE,EAod9B,CAAC"}
|
|
@@ -8,208 +8,209 @@ import { EmptyState as o } from "../../components/EmptyState.js";
|
|
|
8
8
|
import { ErrorView as s, LoadingView as c } from "../../components/StateViews.js";
|
|
9
9
|
import { MiniBar as l } from "../../components/MiniBar.js";
|
|
10
10
|
import { ProgressRing as ee } from "../../components/Viz.js";
|
|
11
|
-
import { useBuiCourse as te, useBuiEnrollment as
|
|
11
|
+
import { useBuiCourse as te, useBuiEnrollment as u, useBuiRecordLessonProgress as d, useBuiTransitionEnrollment as f } from "../../hooks/useBuildmyiqApi.js";
|
|
12
12
|
import "../../hooks/index.js";
|
|
13
|
-
import { FileLink as
|
|
14
|
-
import { TutorPanel as
|
|
13
|
+
import { FileLink as p } from "../../components/FileLink.js";
|
|
14
|
+
import { TutorPanel as ne } from "../../components/TutorPanel.js";
|
|
15
|
+
import { OfflineLessonControl as re } from "../../components/OfflineLessonControl.js";
|
|
15
16
|
import "../../components/index.js";
|
|
16
|
-
import { useEffect as
|
|
17
|
-
import { ArrowLeft as
|
|
18
|
-
import { useParams as
|
|
19
|
-
import { Fragment as
|
|
20
|
-
import { Button as
|
|
17
|
+
import { useEffect as ie, useMemo as m, useRef as ae, useState as h } from "react";
|
|
18
|
+
import { ArrowLeft as oe, BookOpen as se, Check as g, CheckCircle2 as _, ChevronLeft as ce, ChevronRight as le, FileText as v, HelpCircle as y, ListChecks as ue, Loader2 as b, MousePointerClick as x, PenLine as S, PlayCircle as C, Radio as w, Trophy as de } from "lucide-react";
|
|
19
|
+
import { useParams as fe } from "react-router-dom";
|
|
20
|
+
import { Fragment as T, jsx as E, jsxs as D } from "react/jsx-runtime";
|
|
21
|
+
import { Button as O, GlassCard as k } from "@burdenoff/fe-libs/ui";
|
|
21
22
|
//#region src/buildmyiq/pages/learning/LessonPlayerPage.tsx
|
|
22
|
-
var
|
|
23
|
-
ARTICLE:
|
|
24
|
-
VIDEO:
|
|
25
|
-
QUIZ:
|
|
26
|
-
ASSIGNMENT:
|
|
27
|
-
INTERACTIVE:
|
|
28
|
-
LIVE_SESSION:
|
|
29
|
-
},
|
|
30
|
-
let { enrollmentId:
|
|
31
|
-
if (!
|
|
32
|
-
let e = /* @__PURE__ */ new Set(), t =
|
|
23
|
+
var A = {
|
|
24
|
+
ARTICLE: v,
|
|
25
|
+
VIDEO: C,
|
|
26
|
+
QUIZ: y,
|
|
27
|
+
ASSIGNMENT: S,
|
|
28
|
+
INTERACTIVE: x,
|
|
29
|
+
LIVE_SESSION: w
|
|
30
|
+
}, j = (e) => [...e].sort((e, t) => e.position - t.position), M = () => {
|
|
31
|
+
let { enrollmentId: y } = fe(), { navigateTo: x } = e(), S = u(y), C = S.data ?? null, w = C?.courseId, M = te(w), N = M.data ?? null, P = d(), F = f(), I = P.mutate, L = ae(/* @__PURE__ */ new Set()), [R, z] = h(null), [B, V] = h(!1), H = m(() => {
|
|
32
|
+
if (!N) return [];
|
|
33
|
+
let e = /* @__PURE__ */ new Set(), t = j(N.modules ?? []), n = [];
|
|
33
34
|
for (let r of t) {
|
|
34
|
-
let t =
|
|
35
|
+
let t = j(r.lessons ?? []).filter((t) => e.has(t.id) ? !1 : (e.add(t.id), !0));
|
|
35
36
|
t.length > 0 && n.push({
|
|
36
37
|
id: r.id,
|
|
37
38
|
title: r.title,
|
|
38
39
|
lessons: t
|
|
39
40
|
});
|
|
40
41
|
}
|
|
41
|
-
let r =
|
|
42
|
+
let r = j(N.lessons ?? []).filter((t) => e.has(t.id) ? !1 : (e.add(t.id), !0));
|
|
42
43
|
return r.length > 0 && n.push({
|
|
43
44
|
id: "__standalone",
|
|
44
45
|
title: "Lessons",
|
|
45
46
|
lessons: r
|
|
46
47
|
}), n;
|
|
47
|
-
}, [
|
|
48
|
+
}, [N]), U = m(() => H.flatMap((e) => e.lessons), [H]), W = m(() => {
|
|
48
49
|
let e = /* @__PURE__ */ new Map();
|
|
49
|
-
for (let t of
|
|
50
|
+
for (let t of C?.lessonProgress ?? []) e.set(t.lessonId, t.status);
|
|
50
51
|
return e;
|
|
51
|
-
}, [
|
|
52
|
-
|
|
53
|
-
if (!
|
|
54
|
-
let e =
|
|
55
|
-
e === "COMPLETED" || e === "IN_PROGRESS" ||
|
|
56
|
-
enrollmentId:
|
|
57
|
-
lessonId:
|
|
52
|
+
}, [C]), G = m(() => U.length === 0 ? null : R && U.some((e) => e.id === R) ? R : U[0].id, [U, R]);
|
|
53
|
+
ie(() => {
|
|
54
|
+
if (!y || !G) return;
|
|
55
|
+
let e = W.get(G);
|
|
56
|
+
e === "COMPLETED" || e === "IN_PROGRESS" || L.current.has(G) || (L.current.add(G), I({
|
|
57
|
+
enrollmentId: y,
|
|
58
|
+
lessonId: G,
|
|
58
59
|
status: "IN_PROGRESS"
|
|
59
60
|
}));
|
|
60
61
|
}, [
|
|
61
|
-
|
|
62
|
+
y,
|
|
63
|
+
G,
|
|
62
64
|
W,
|
|
63
|
-
|
|
64
|
-
F
|
|
65
|
+
I
|
|
65
66
|
]);
|
|
66
|
-
let
|
|
67
|
-
|
|
67
|
+
let K = U.findIndex((e) => e.id === G), q = K >= 0 ? U[K] : null, J = (q ? W.get(q.id) : void 0) === "COMPLETED", Y = U.filter((e) => W.get(e.id) === "COMPLETED").length, pe = U.length > 0 && Y === U.length, me = C?.status === "COMPLETED", X = (e) => {
|
|
68
|
+
z(e), V(!1);
|
|
68
69
|
}, Z = (e) => {
|
|
69
|
-
if (
|
|
70
|
-
let t =
|
|
70
|
+
if (K < 0) return;
|
|
71
|
+
let t = U[K + e];
|
|
71
72
|
t && X(t.id);
|
|
72
|
-
},
|
|
73
|
-
if (!
|
|
74
|
-
await
|
|
75
|
-
enrollmentId:
|
|
76
|
-
lessonId:
|
|
73
|
+
}, he = async () => {
|
|
74
|
+
if (!y || !q) return;
|
|
75
|
+
await P.mutateAsync({
|
|
76
|
+
enrollmentId: y,
|
|
77
|
+
lessonId: q.id,
|
|
77
78
|
status: "COMPLETED"
|
|
78
79
|
});
|
|
79
|
-
let e =
|
|
80
|
-
e &&
|
|
81
|
-
},
|
|
82
|
-
|
|
83
|
-
id:
|
|
80
|
+
let e = U[K + 1];
|
|
81
|
+
e && z(e.id);
|
|
82
|
+
}, ge = async () => {
|
|
83
|
+
y && await F.mutateAsync({
|
|
84
|
+
id: y,
|
|
84
85
|
status: "COMPLETED"
|
|
85
86
|
});
|
|
86
87
|
};
|
|
87
|
-
if (!
|
|
88
|
+
if (!y) return /* @__PURE__ */ E(n, {
|
|
88
89
|
title: "Lesson",
|
|
89
|
-
children: /* @__PURE__ */
|
|
90
|
+
children: /* @__PURE__ */ E(o, {
|
|
90
91
|
illustration: "empty-data",
|
|
91
92
|
title: "No enrollment selected",
|
|
92
93
|
description: "Open a course from My Learning to start the lesson player.",
|
|
93
|
-
action: /* @__PURE__ */
|
|
94
|
-
onClick: () =>
|
|
94
|
+
action: /* @__PURE__ */ E(O, {
|
|
95
|
+
onClick: () => x("learning"),
|
|
95
96
|
children: "Back to My Learning"
|
|
96
97
|
})
|
|
97
98
|
})
|
|
98
99
|
});
|
|
99
|
-
let
|
|
100
|
+
let _e = S.isLoading || !!w && M.isLoading, Q = /* @__PURE__ */ D(O, {
|
|
100
101
|
variant: "ghost",
|
|
101
102
|
size: "sm",
|
|
102
|
-
onClick: () =>
|
|
103
|
-
children: [/* @__PURE__ */
|
|
103
|
+
onClick: () => x("learning"),
|
|
104
|
+
children: [/* @__PURE__ */ E(oe, { className: "mr-1 h-4 w-4" }), " My Learning"]
|
|
104
105
|
});
|
|
105
|
-
if (
|
|
106
|
+
if (_e) return /* @__PURE__ */ E(n, {
|
|
106
107
|
title: "Lesson",
|
|
107
108
|
actions: Q,
|
|
108
|
-
children: /* @__PURE__ */
|
|
109
|
+
children: /* @__PURE__ */ E(c, { label: "Loading your lesson…" })
|
|
109
110
|
});
|
|
110
|
-
if (
|
|
111
|
+
if (S.isError) return /* @__PURE__ */ E(n, {
|
|
111
112
|
title: "Lesson",
|
|
112
113
|
actions: Q,
|
|
113
|
-
children: /* @__PURE__ */
|
|
114
|
+
children: /* @__PURE__ */ E(s, {
|
|
114
115
|
title: "Couldn't load this enrollment",
|
|
115
|
-
message:
|
|
116
|
-
onRetry: () =>
|
|
116
|
+
message: S.error instanceof Error ? S.error.message : void 0,
|
|
117
|
+
onRetry: () => S.refetch()
|
|
117
118
|
})
|
|
118
119
|
});
|
|
119
|
-
if (!
|
|
120
|
+
if (!C) return /* @__PURE__ */ E(n, {
|
|
120
121
|
title: "Lesson",
|
|
121
122
|
actions: Q,
|
|
122
|
-
children: /* @__PURE__ */
|
|
123
|
+
children: /* @__PURE__ */ E(o, {
|
|
123
124
|
illustration: "empty-data",
|
|
124
125
|
title: "Enrollment not found",
|
|
125
126
|
description: "This enrollment may have been removed or is not available to you.",
|
|
126
|
-
action: /* @__PURE__ */
|
|
127
|
-
onClick: () =>
|
|
127
|
+
action: /* @__PURE__ */ E(O, {
|
|
128
|
+
onClick: () => x("learning"),
|
|
128
129
|
children: "Back to My Learning"
|
|
129
130
|
})
|
|
130
131
|
})
|
|
131
132
|
});
|
|
132
|
-
if (
|
|
133
|
+
if (M.isError) return /* @__PURE__ */ E(n, {
|
|
133
134
|
title: "Lesson",
|
|
134
135
|
actions: Q,
|
|
135
|
-
children: /* @__PURE__ */
|
|
136
|
+
children: /* @__PURE__ */ E(s, {
|
|
136
137
|
title: "Couldn't load the course",
|
|
137
|
-
message:
|
|
138
|
-
onRetry: () =>
|
|
138
|
+
message: M.error instanceof Error ? M.error.message : void 0,
|
|
139
|
+
onRetry: () => M.refetch()
|
|
139
140
|
})
|
|
140
141
|
});
|
|
141
|
-
let $ = Math.round(
|
|
142
|
-
return /* @__PURE__ */
|
|
143
|
-
title:
|
|
142
|
+
let $ = Math.round(C.progressPct ?? 0), ve = q ? A[q.type] : v;
|
|
143
|
+
return /* @__PURE__ */ E(n, {
|
|
144
|
+
title: N?.title ?? "Lesson",
|
|
144
145
|
actions: Q,
|
|
145
|
-
children: /* @__PURE__ */
|
|
146
|
+
children: /* @__PURE__ */ D("div", {
|
|
146
147
|
className: "flex flex-col gap-5",
|
|
147
148
|
children: [
|
|
148
|
-
/* @__PURE__ */
|
|
149
|
+
/* @__PURE__ */ D(k, {
|
|
149
150
|
treatment: "glass",
|
|
150
151
|
glow: $ > 0,
|
|
151
152
|
className: "flex flex-col gap-4 p-4 sm:flex-row sm:items-center sm:gap-5",
|
|
152
153
|
children: [
|
|
153
|
-
/* @__PURE__ */
|
|
154
|
+
/* @__PURE__ */ E(ee, {
|
|
154
155
|
value: $,
|
|
155
156
|
size: 64,
|
|
156
157
|
className: "shrink-0"
|
|
157
158
|
}),
|
|
158
|
-
/* @__PURE__ */
|
|
159
|
+
/* @__PURE__ */ D("div", {
|
|
159
160
|
className: "min-w-0 flex-1",
|
|
160
|
-
children: [/* @__PURE__ */
|
|
161
|
+
children: [/* @__PURE__ */ D("div", {
|
|
161
162
|
className: "flex flex-wrap items-center gap-2",
|
|
162
|
-
children: [/* @__PURE__ */
|
|
163
|
+
children: [/* @__PURE__ */ E(a, { status: C.status }), /* @__PURE__ */ D("span", {
|
|
163
164
|
className: "text-sm text-text-secondary tabular-nums",
|
|
164
165
|
children: [
|
|
165
|
-
|
|
166
|
+
Y,
|
|
166
167
|
" of ",
|
|
167
|
-
|
|
168
|
+
U.length,
|
|
168
169
|
" lessons complete"
|
|
169
170
|
]
|
|
170
171
|
})]
|
|
171
|
-
}), /* @__PURE__ */
|
|
172
|
+
}), /* @__PURE__ */ E(l, {
|
|
172
173
|
value: $,
|
|
173
174
|
className: "mt-3"
|
|
174
175
|
})]
|
|
175
176
|
}),
|
|
176
|
-
|
|
177
|
+
pe && !me && /* @__PURE__ */ D(O, {
|
|
177
178
|
className: "shrink-0 max-sm:w-full",
|
|
178
|
-
onClick:
|
|
179
|
-
disabled:
|
|
180
|
-
children: [
|
|
179
|
+
onClick: ge,
|
|
180
|
+
disabled: F.isPending,
|
|
181
|
+
children: [F.isPending ? /* @__PURE__ */ E(b, { className: "mr-1 h-4 w-4 animate-spin" }) : /* @__PURE__ */ E(de, { className: "mr-1 h-4 w-4" }), "Finish course"]
|
|
181
182
|
})
|
|
182
183
|
]
|
|
183
184
|
}),
|
|
184
|
-
/* @__PURE__ */
|
|
185
|
+
/* @__PURE__ */ D(O, {
|
|
185
186
|
variant: "secondary",
|
|
186
187
|
size: "sm",
|
|
187
188
|
className: "w-full lg:hidden",
|
|
188
|
-
onClick: () =>
|
|
189
|
-
children: [/* @__PURE__ */
|
|
189
|
+
onClick: () => V((e) => !e),
|
|
190
|
+
children: [/* @__PURE__ */ E(ue, { className: "mr-1 h-4 w-4" }), B ? "Hide lessons" : `Lessons (${Y}/${U.length})`]
|
|
190
191
|
}),
|
|
191
|
-
/* @__PURE__ */
|
|
192
|
+
/* @__PURE__ */ D("div", {
|
|
192
193
|
className: "flex flex-col gap-5 lg:flex-row",
|
|
193
|
-
children: [/* @__PURE__ */
|
|
194
|
-
className: t("lg:block lg:w-80 lg:shrink-0",
|
|
195
|
-
children: /* @__PURE__ */
|
|
194
|
+
children: [/* @__PURE__ */ E("aside", {
|
|
195
|
+
className: t("lg:block lg:w-80 lg:shrink-0", B ? "block" : "hidden"),
|
|
196
|
+
children: /* @__PURE__ */ E(k, {
|
|
196
197
|
treatment: "plain",
|
|
197
198
|
className: "overflow-hidden p-0 lg:sticky lg:top-4",
|
|
198
|
-
children:
|
|
199
|
+
children: U.length === 0 ? /* @__PURE__ */ E("div", {
|
|
199
200
|
className: "p-4 text-sm text-text-secondary",
|
|
200
201
|
children: "This course has no lessons yet."
|
|
201
|
-
}) : /* @__PURE__ */
|
|
202
|
+
}) : /* @__PURE__ */ D(T, { children: [/* @__PURE__ */ E("nav", {
|
|
202
203
|
className: "max-h-[68vh] overflow-auto p-3",
|
|
203
|
-
children:
|
|
204
|
-
let n = e.lessons.filter((e) =>
|
|
205
|
-
return /* @__PURE__ */
|
|
204
|
+
children: H.map((e) => {
|
|
205
|
+
let n = e.lessons.filter((e) => W.get(e.id) === "COMPLETED").length;
|
|
206
|
+
return /* @__PURE__ */ D("div", {
|
|
206
207
|
className: "mb-4 last:mb-0",
|
|
207
|
-
children: [/* @__PURE__ */
|
|
208
|
+
children: [/* @__PURE__ */ D("div", {
|
|
208
209
|
className: "mb-2 flex items-center justify-between gap-2 px-1",
|
|
209
|
-
children: [/* @__PURE__ */
|
|
210
|
+
children: [/* @__PURE__ */ E("p", {
|
|
210
211
|
className: "min-w-0 truncate text-sm font-semibold text-text-primary",
|
|
211
212
|
children: e.title
|
|
212
|
-
}), /* @__PURE__ */
|
|
213
|
+
}), /* @__PURE__ */ D("span", {
|
|
213
214
|
className: "shrink-0 text-xs font-medium tabular-nums text-text-secondary",
|
|
214
215
|
children: [
|
|
215
216
|
n,
|
|
@@ -217,33 +218,33 @@ var k = {
|
|
|
217
218
|
e.lessons.length
|
|
218
219
|
]
|
|
219
220
|
})]
|
|
220
|
-
}), /* @__PURE__ */
|
|
221
|
+
}), /* @__PURE__ */ E("ul", {
|
|
221
222
|
className: "flex flex-col",
|
|
222
223
|
children: e.lessons.map((n, a) => {
|
|
223
|
-
let o =
|
|
224
|
-
return /* @__PURE__ */
|
|
224
|
+
let o = W.get(n.id) === "COMPLETED", s = n.id === G, c = a === e.lessons.length - 1, l = A[n.type];
|
|
225
|
+
return /* @__PURE__ */ D("li", {
|
|
225
226
|
className: "flex gap-3",
|
|
226
|
-
children: [/* @__PURE__ */
|
|
227
|
+
children: [/* @__PURE__ */ D("div", {
|
|
227
228
|
className: "flex flex-col items-center pt-1.5",
|
|
228
|
-
children: [o ? /* @__PURE__ */
|
|
229
|
+
children: [o ? /* @__PURE__ */ E("span", {
|
|
229
230
|
className: "flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-[var(--color-status-success-text)]",
|
|
230
|
-
children: /* @__PURE__ */
|
|
231
|
-
}) : s ? /* @__PURE__ */
|
|
231
|
+
children: /* @__PURE__ */ E(g, { className: "h-3.5 w-3.5 text-status-success-bg-subtle" })
|
|
232
|
+
}) : s ? /* @__PURE__ */ E("span", {
|
|
232
233
|
className: "flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-[var(--color-accent-soft)] ring-2 ring-[var(--color-accent-teal)]",
|
|
233
|
-
children: /* @__PURE__ */
|
|
234
|
-
}) : /* @__PURE__ */
|
|
234
|
+
children: /* @__PURE__ */ E(l, { className: "h-3.5 w-3.5 text-[var(--color-accent-teal)]" })
|
|
235
|
+
}) : /* @__PURE__ */ E("span", {
|
|
235
236
|
className: "flex h-6 w-6 shrink-0 items-center justify-center rounded-full border border-border-seam bg-bg-sunken",
|
|
236
|
-
children: /* @__PURE__ */
|
|
237
|
-
}), !c && /* @__PURE__ */
|
|
238
|
-
}), /* @__PURE__ */
|
|
237
|
+
children: /* @__PURE__ */ E(l, { className: "h-3.5 w-3.5 text-text-secondary opacity-70" })
|
|
238
|
+
}), !c && /* @__PURE__ */ E("span", { className: t("my-1 w-px flex-1", o ? "bg-[color-mix(in_srgb,var(--color-status-success-text)_45%,transparent)]" : "bg-border-seam") })]
|
|
239
|
+
}), /* @__PURE__ */ D("button", {
|
|
239
240
|
type: "button",
|
|
240
241
|
onClick: () => X(n.id),
|
|
241
242
|
className: t("mb-1 min-w-0 flex-1 rounded-lg px-3 py-2 text-left transition-colors", s ? "bg-[var(--color-accent-soft)]" : "hover:bg-bg-sunken"),
|
|
242
243
|
"aria-current": s ? "true" : void 0,
|
|
243
|
-
children: [/* @__PURE__ */
|
|
244
|
+
children: [/* @__PURE__ */ E("span", {
|
|
244
245
|
className: t("block truncate text-sm", s ? "font-medium text-text-primary" : o ? "text-text-secondary" : "text-text-primary"),
|
|
245
246
|
children: n.title
|
|
246
|
-
}), /* @__PURE__ */
|
|
247
|
+
}), /* @__PURE__ */ D("span", {
|
|
247
248
|
className: "mt-0.5 block truncate text-xs text-text-secondary",
|
|
248
249
|
children: [i(n.type), n.durationMin > 0 ? ` · ${r(n.durationMin)}` : ""]
|
|
249
250
|
})]
|
|
@@ -253,113 +254,114 @@ var k = {
|
|
|
253
254
|
})]
|
|
254
255
|
}, e.id);
|
|
255
256
|
})
|
|
256
|
-
}),
|
|
257
|
+
}), w && /* @__PURE__ */ E("div", {
|
|
257
258
|
className: "border-t border-border-seam p-3",
|
|
258
|
-
children: /* @__PURE__ */
|
|
259
|
+
children: /* @__PURE__ */ D(O, {
|
|
259
260
|
variant: "outline",
|
|
260
261
|
size: "sm",
|
|
261
262
|
className: "w-full",
|
|
262
|
-
onClick: () =>
|
|
263
|
-
children: [/* @__PURE__ */
|
|
263
|
+
onClick: () => x(`catalog/${w}`),
|
|
264
|
+
children: [/* @__PURE__ */ E(se, { className: "mr-1.5 h-4 w-4" }), " Course overview"]
|
|
264
265
|
})
|
|
265
266
|
})] })
|
|
266
267
|
})
|
|
267
|
-
}), /* @__PURE__ */
|
|
268
|
+
}), /* @__PURE__ */ D("main", {
|
|
268
269
|
className: "min-w-0 flex-1",
|
|
269
|
-
children: [
|
|
270
|
+
children: [q ? /* @__PURE__ */ D(k, {
|
|
270
271
|
treatment: "plain",
|
|
271
272
|
className: "flex flex-col gap-4 p-5 sm:p-7",
|
|
272
273
|
children: [
|
|
273
|
-
/* @__PURE__ */
|
|
274
|
+
/* @__PURE__ */ D("div", {
|
|
274
275
|
className: "flex flex-wrap items-center gap-2",
|
|
275
276
|
children: [
|
|
276
|
-
/* @__PURE__ */
|
|
277
|
+
/* @__PURE__ */ D("span", {
|
|
277
278
|
className: "inline-flex items-center gap-1.5 rounded-full bg-[var(--color-accent-soft)] px-2.5 py-1 text-xs font-medium text-text-primary",
|
|
278
|
-
children: [/* @__PURE__ */
|
|
279
|
+
children: [/* @__PURE__ */ E(ve, { className: "h-3.5 w-3.5 text-[var(--color-accent-teal)]" }), i(q.type)]
|
|
279
280
|
}),
|
|
280
|
-
|
|
281
|
+
q.durationMin > 0 && /* @__PURE__ */ E("span", {
|
|
281
282
|
className: "text-xs text-text-secondary",
|
|
282
|
-
children: r(
|
|
283
|
+
children: r(q.durationMin)
|
|
283
284
|
}),
|
|
284
|
-
|
|
285
|
+
J && /* @__PURE__ */ D("span", {
|
|
285
286
|
className: "inline-flex items-center gap-1 text-xs font-medium text-status-success-text",
|
|
286
|
-
children: [/* @__PURE__ */
|
|
287
|
+
children: [/* @__PURE__ */ E(_, { className: "h-3.5 w-3.5" }), " Completed"]
|
|
287
288
|
}),
|
|
288
|
-
/* @__PURE__ */ E(
|
|
289
|
+
/* @__PURE__ */ E(re, { lesson: q }, q.id),
|
|
290
|
+
/* @__PURE__ */ D("span", {
|
|
289
291
|
className: "ml-auto text-xs tabular-nums text-text-secondary",
|
|
290
292
|
children: [
|
|
291
293
|
"Lesson ",
|
|
292
|
-
|
|
294
|
+
K + 1,
|
|
293
295
|
" of ",
|
|
294
|
-
|
|
296
|
+
U.length
|
|
295
297
|
]
|
|
296
298
|
})
|
|
297
299
|
]
|
|
298
300
|
}),
|
|
299
|
-
/* @__PURE__ */
|
|
301
|
+
/* @__PURE__ */ E("h2", {
|
|
300
302
|
className: "text-2xl font-semibold tracking-tight text-text-primary",
|
|
301
|
-
children:
|
|
303
|
+
children: q.title
|
|
302
304
|
}),
|
|
303
|
-
|
|
305
|
+
q.media ? /* @__PURE__ */ D("div", {
|
|
304
306
|
className: "rounded-xl border border-border-seam bg-bg-sunken p-3",
|
|
305
|
-
children: [/* @__PURE__ */
|
|
307
|
+
children: [/* @__PURE__ */ E("p", {
|
|
306
308
|
className: "mb-2 text-xs font-semibold uppercase tracking-wide text-text-secondary",
|
|
307
309
|
children: "Resources"
|
|
308
|
-
}), /* @__PURE__ */
|
|
310
|
+
}), /* @__PURE__ */ E(p, { fileId: q.media.id })]
|
|
309
311
|
}) : null,
|
|
310
|
-
|
|
312
|
+
q.content ? /* @__PURE__ */ E("p", {
|
|
311
313
|
className: "max-w-prose whitespace-pre-wrap text-[15px] leading-7 text-text-primary",
|
|
312
|
-
children:
|
|
313
|
-
}) : /* @__PURE__ */
|
|
314
|
+
children: q.content
|
|
315
|
+
}) : /* @__PURE__ */ D("p", {
|
|
314
316
|
className: "text-sm text-text-secondary",
|
|
315
317
|
children: [
|
|
316
318
|
"This lesson has no written content",
|
|
317
|
-
|
|
319
|
+
q.media ? " beyond the attached media" : "",
|
|
318
320
|
"."
|
|
319
321
|
]
|
|
320
322
|
}),
|
|
321
|
-
/* @__PURE__ */
|
|
323
|
+
/* @__PURE__ */ D("div", {
|
|
322
324
|
className: "mt-2 flex flex-col gap-3 border-t border-border-seam pt-5 sm:flex-row sm:items-center sm:justify-between",
|
|
323
325
|
children: [
|
|
324
|
-
/* @__PURE__ */
|
|
326
|
+
/* @__PURE__ */ D(O, {
|
|
325
327
|
variant: "outline",
|
|
326
328
|
size: "sm",
|
|
327
329
|
className: "max-sm:w-full",
|
|
328
330
|
onClick: () => Z(-1),
|
|
329
|
-
disabled:
|
|
330
|
-
children: [/* @__PURE__ */
|
|
331
|
+
disabled: K <= 0,
|
|
332
|
+
children: [/* @__PURE__ */ E(ce, { className: "mr-1 h-4 w-4" }), " Previous"]
|
|
331
333
|
}),
|
|
332
|
-
|
|
334
|
+
J ? /* @__PURE__ */ D("span", {
|
|
333
335
|
className: "inline-flex items-center justify-center gap-2 rounded-lg bg-status-success-bg-subtle px-4 py-2 text-sm font-medium text-status-success-text max-sm:w-full",
|
|
334
|
-
children: [/* @__PURE__ */
|
|
335
|
-
}) : /* @__PURE__ */
|
|
336
|
+
children: [/* @__PURE__ */ E(_, { className: "h-4 w-4" }), " Lesson complete"]
|
|
337
|
+
}) : /* @__PURE__ */ D(O, {
|
|
336
338
|
size: "lg",
|
|
337
339
|
className: "max-sm:w-full",
|
|
338
|
-
onClick:
|
|
339
|
-
disabled:
|
|
340
|
-
children: [
|
|
340
|
+
onClick: he,
|
|
341
|
+
disabled: P.isPending,
|
|
342
|
+
children: [P.isPending ? /* @__PURE__ */ E(b, { className: "mr-1.5 h-4 w-4 animate-spin" }) : /* @__PURE__ */ E(_, { className: "mr-1.5 h-4 w-4" }), "Mark complete"]
|
|
341
343
|
}),
|
|
342
|
-
/* @__PURE__ */
|
|
344
|
+
/* @__PURE__ */ D(O, {
|
|
343
345
|
variant: "outline",
|
|
344
346
|
size: "sm",
|
|
345
347
|
className: "max-sm:w-full",
|
|
346
348
|
onClick: () => Z(1),
|
|
347
|
-
disabled:
|
|
348
|
-
children: ["Next ", /* @__PURE__ */
|
|
349
|
+
disabled: K >= U.length - 1,
|
|
350
|
+
children: ["Next ", /* @__PURE__ */ E(le, { className: "ml-1 h-4 w-4" })]
|
|
349
351
|
})
|
|
350
352
|
]
|
|
351
353
|
})
|
|
352
354
|
]
|
|
353
|
-
}) : /* @__PURE__ */
|
|
355
|
+
}) : /* @__PURE__ */ E(o, {
|
|
354
356
|
illustration: "empty-data",
|
|
355
357
|
title: "No lesson to show",
|
|
356
358
|
description: "This course doesn't have any lessons yet. Check back once the author adds content."
|
|
357
|
-
}),
|
|
359
|
+
}), q && /* @__PURE__ */ E("div", {
|
|
358
360
|
className: "mt-5",
|
|
359
|
-
children: /* @__PURE__ */
|
|
360
|
-
lessonId:
|
|
361
|
-
lessonTitle:
|
|
362
|
-
},
|
|
361
|
+
children: /* @__PURE__ */ E(ne, {
|
|
362
|
+
lessonId: q.id,
|
|
363
|
+
lessonTitle: q.title
|
|
364
|
+
}, q.id)
|
|
363
365
|
})]
|
|
364
366
|
})]
|
|
365
367
|
})
|
|
@@ -368,6 +370,6 @@ var k = {
|
|
|
368
370
|
});
|
|
369
371
|
};
|
|
370
372
|
//#endregion
|
|
371
|
-
export {
|
|
373
|
+
export { M as LessonPlayerPage };
|
|
372
374
|
|
|
373
375
|
//# sourceMappingURL=LessonPlayerPage.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LessonPlayerPage.js","names":[],"sources":["../../../../src/buildmyiq/pages/learning/LessonPlayerPage.tsx"],"sourcesContent":["/**\n * LessonPlayerPage — the two-pane lesson player for a single enrollment.\n * @module buildmyiq/pages/learning\n *\n * Left pane: the course's module/lesson tree (from `buiCourse`) rendered as a\n * completion timeline driven by the enrollment's `lessonProgress`. Right pane:\n * the selected lesson (title, type, content text rendered safely, and a\n * `FileLink` for any attached media) with Prev / Mark-complete / Next controls.\n * Opening a lesson records IN_PROGRESS once; \"Mark complete\" records COMPLETED;\n * finishing every lesson transitions the enrollment to COMPLETED. All data is\n * live — no fabrication.\n */\n\nimport type { FC } from 'react';\nimport { useEffect, useMemo, useRef, useState } from 'react';\nimport { useParams } from 'react-router-dom';\nimport type { LucideIcon } from 'lucide-react';\nimport {\n ArrowLeft,\n BookOpen,\n Check,\n CheckCircle2,\n ChevronLeft,\n ChevronRight,\n FileText,\n HelpCircle,\n Loader2,\n ListChecks,\n MousePointerClick,\n PenLine,\n PlayCircle,\n Radio,\n Trophy,\n} from 'lucide-react';\nimport { Button, GlassCard } from '@burdenoff/fe-libs/ui';\nimport {\n PageLayout,\n StatusBadge,\n MiniBar,\n ProgressRing,\n LoadingView,\n ErrorView,\n EmptyState,\n FileLink,\n TutorPanel,\n} from '../../components';\nimport { useBuildmyiq } from '../../context';\nimport {\n useBuiEnrollment,\n useBuiCourse,\n useBuiRecordLessonProgress,\n useBuiTransitionEnrollment,\n} from '../../hooks';\nimport { cn } from '../../utils/cn';\nimport { formatDuration, formatEnumLabel } from '../../utils/formatters';\nimport type { BuiLesson, BuiLessonType, BuiModule, BuiProgressStatus } from '../../types';\n\ninterface LessonGroup {\n id: string;\n title: string;\n lessons: BuiLesson[];\n}\n\nconst LESSON_ICON: Record<BuiLessonType, LucideIcon> = {\n ARTICLE: FileText,\n VIDEO: PlayCircle,\n QUIZ: HelpCircle,\n ASSIGNMENT: PenLine,\n INTERACTIVE: MousePointerClick,\n LIVE_SESSION: Radio,\n};\n\nconst sortByPosition = <T extends { position: number }>(items: T[]): T[] =>\n [...items].sort((a, b) => a.position - b.position);\n\nexport const LessonPlayerPage: FC = () => {\n const { enrollmentId } = useParams<{ enrollmentId: string }>();\n const { navigateTo } = useBuildmyiq();\n\n const enrollmentQuery = useBuiEnrollment(enrollmentId);\n const enrollment = enrollmentQuery.data ?? null;\n const courseId = enrollment?.courseId;\n const courseQuery = useBuiCourse(courseId);\n const course = courseQuery.data ?? null;\n\n const recordProgress = useBuiRecordLessonProgress();\n const transitionEnrollment = useBuiTransitionEnrollment();\n\n // react-query's mutate is referentially stable across renders, so it can be\n // used directly in effects without a render-time ref write.\n const recordMutate = recordProgress.mutate;\n const autoRecordedRef = useRef<Set<string>>(new Set());\n\n const [userSelectedId, setUserSelectedId] = useState<string | null>(null);\n const [showTree, setShowTree] = useState(false);\n\n // Grouped tree (modules → lessons) plus any standalone lessons, de-duplicated.\n const groups = useMemo<LessonGroup[]>(() => {\n if (!course) return [];\n const seen = new Set<string>();\n const modules: BuiModule[] = sortByPosition(course.modules ?? []);\n const result: LessonGroup[] = [];\n for (const m of modules) {\n const lessons = sortByPosition(m.lessons ?? []).filter((l) => {\n if (seen.has(l.id)) return false;\n seen.add(l.id);\n return true;\n });\n if (lessons.length > 0) result.push({ id: m.id, title: m.title, lessons });\n }\n const standalone = sortByPosition(course.lessons ?? []).filter((l) => {\n if (seen.has(l.id)) return false;\n seen.add(l.id);\n return true;\n });\n if (standalone.length > 0) {\n result.push({ id: '__standalone', title: 'Lessons', lessons: standalone });\n }\n return result;\n }, [course]);\n\n // Flat, ordered lesson list for prev/next + completion checks.\n const orderedLessons = useMemo<BuiLesson[]>(() => groups.flatMap((g) => g.lessons), [groups]);\n\n const progressByLesson = useMemo(() => {\n const map = new Map<string, BuiProgressStatus>();\n for (const p of enrollment?.lessonProgress ?? []) map.set(p.lessonId, p.status);\n return map;\n }, [enrollment]);\n\n // Resolve the active lesson as DERIVED state: the user's pick if it still\n // exists in the course, otherwise the first lesson. Avoids a setState-in-\n // effect re-anchor loop.\n const selectedLessonId = useMemo<string | null>(() => {\n if (orderedLessons.length === 0) return null;\n if (userSelectedId && orderedLessons.some((l) => l.id === userSelectedId)) {\n return userSelectedId;\n }\n return orderedLessons[0].id;\n }, [orderedLessons, userSelectedId]);\n\n // Record IN_PROGRESS once when a not-yet-started lesson is opened.\n useEffect(() => {\n if (!enrollmentId || !selectedLessonId) return;\n const status = progressByLesson.get(selectedLessonId);\n if (status === 'COMPLETED' || status === 'IN_PROGRESS') return;\n if (autoRecordedRef.current.has(selectedLessonId)) return;\n autoRecordedRef.current.add(selectedLessonId);\n recordMutate({\n enrollmentId,\n lessonId: selectedLessonId,\n status: 'IN_PROGRESS',\n });\n }, [enrollmentId, selectedLessonId, progressByLesson, recordMutate]);\n\n const selectedIndex = orderedLessons.findIndex((l) => l.id === selectedLessonId);\n const selectedLesson = selectedIndex >= 0 ? orderedLessons[selectedIndex] : null;\n const selectedStatus = selectedLesson ? progressByLesson.get(selectedLesson.id) : undefined;\n const isSelectedComplete = selectedStatus === 'COMPLETED';\n\n const completedCount = orderedLessons.filter(\n (l) => progressByLesson.get(l.id) === 'COMPLETED'\n ).length;\n const allComplete = orderedLessons.length > 0 && completedCount === orderedLessons.length;\n const alreadyFinished = enrollment?.status === 'COMPLETED';\n\n const handleSelect = (lessonId: string) => {\n setUserSelectedId(lessonId);\n setShowTree(false);\n };\n\n const goTo = (offset: number) => {\n if (selectedIndex < 0) return;\n const next = orderedLessons[selectedIndex + offset];\n if (next) handleSelect(next.id);\n };\n\n const handleMarkComplete = async () => {\n if (!enrollmentId || !selectedLesson) return;\n await recordProgress.mutateAsync({\n enrollmentId,\n lessonId: selectedLesson.id,\n status: 'COMPLETED',\n });\n // Advance to the next incomplete lesson for a natural flow.\n const next = orderedLessons[selectedIndex + 1];\n if (next) setUserSelectedId(next.id);\n };\n\n const handleFinishCourse = async () => {\n if (!enrollmentId) return;\n await transitionEnrollment.mutateAsync({ id: enrollmentId, status: 'COMPLETED' });\n };\n\n // ---- top-level states ----------------------------------------------------\n if (!enrollmentId) {\n return (\n <PageLayout title=\"Lesson\">\n <EmptyState\n illustration=\"empty-data\"\n title=\"No enrollment selected\"\n description=\"Open a course from My Learning to start the lesson player.\"\n action={<Button onClick={() => navigateTo('learning')}>Back to My Learning</Button>}\n />\n </PageLayout>\n );\n }\n\n const isLoading = enrollmentQuery.isLoading || (Boolean(courseId) && courseQuery.isLoading);\n\n const backAction = (\n <Button variant=\"ghost\" size=\"sm\" onClick={() => navigateTo('learning')}>\n <ArrowLeft className=\"mr-1 h-4 w-4\" /> My Learning\n </Button>\n );\n\n if (isLoading) {\n return (\n <PageLayout title=\"Lesson\" actions={backAction}>\n <LoadingView label=\"Loading your lesson…\" />\n </PageLayout>\n );\n }\n\n if (enrollmentQuery.isError) {\n return (\n <PageLayout title=\"Lesson\" actions={backAction}>\n <ErrorView\n title=\"Couldn't load this enrollment\"\n message={\n enrollmentQuery.error instanceof Error ? enrollmentQuery.error.message : undefined\n }\n onRetry={() => enrollmentQuery.refetch()}\n />\n </PageLayout>\n );\n }\n\n if (!enrollment) {\n return (\n <PageLayout title=\"Lesson\" actions={backAction}>\n <EmptyState\n illustration=\"empty-data\"\n title=\"Enrollment not found\"\n description=\"This enrollment may have been removed or is not available to you.\"\n action={<Button onClick={() => navigateTo('learning')}>Back to My Learning</Button>}\n />\n </PageLayout>\n );\n }\n\n if (courseQuery.isError) {\n return (\n <PageLayout title=\"Lesson\" actions={backAction}>\n <ErrorView\n title=\"Couldn't load the course\"\n message={courseQuery.error instanceof Error ? courseQuery.error.message : undefined}\n onRetry={() => courseQuery.refetch()}\n />\n </PageLayout>\n );\n }\n\n const progressPct = Math.round(enrollment.progressPct ?? 0);\n const SelectedIcon = selectedLesson ? LESSON_ICON[selectedLesson.type] : FileText;\n\n return (\n <PageLayout title={course?.title ?? 'Lesson'} actions={backAction}>\n <div className=\"flex flex-col gap-5\">\n {/* Header progress — ProgressRing hero + completion bar */}\n <GlassCard\n treatment=\"glass\"\n glow={progressPct > 0}\n className=\"flex flex-col gap-4 p-4 sm:flex-row sm:items-center sm:gap-5\"\n >\n <ProgressRing value={progressPct} size={64} className=\"shrink-0\" />\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <StatusBadge status={enrollment.status} />\n <span className=\"text-sm text-text-secondary tabular-nums\">\n {completedCount} of {orderedLessons.length} lessons complete\n </span>\n </div>\n <MiniBar value={progressPct} className=\"mt-3\" />\n </div>\n {allComplete && !alreadyFinished && (\n <Button\n className=\"shrink-0 max-sm:w-full\"\n onClick={handleFinishCourse}\n disabled={transitionEnrollment.isPending}\n >\n {transitionEnrollment.isPending ? (\n <Loader2 className=\"mr-1 h-4 w-4 animate-spin\" />\n ) : (\n <Trophy className=\"mr-1 h-4 w-4\" />\n )}\n Finish course\n </Button>\n )}\n </GlassCard>\n\n {/* Mobile tree toggle — collapses the outline into a top sheet */}\n <Button\n variant=\"secondary\"\n size=\"sm\"\n className=\"w-full lg:hidden\"\n onClick={() => setShowTree((v) => !v)}\n >\n <ListChecks className=\"mr-1 h-4 w-4\" />\n {showTree ? 'Hide lessons' : `Lessons (${completedCount}/${orderedLessons.length})`}\n </Button>\n\n <div className=\"flex flex-col gap-5 lg:flex-row\">\n {/* Left: module / lesson completion timeline */}\n <aside className={cn('lg:block lg:w-80 lg:shrink-0', showTree ? 'block' : 'hidden')}>\n <GlassCard treatment=\"plain\" className=\"overflow-hidden p-0 lg:sticky lg:top-4\">\n {orderedLessons.length === 0 ? (\n <div className=\"p-4 text-sm text-text-secondary\">\n This course has no lessons yet.\n </div>\n ) : (\n <>\n <nav className=\"max-h-[68vh] overflow-auto p-3\">\n {groups.map((group) => {\n const groupDone = group.lessons.filter(\n (l) => progressByLesson.get(l.id) === 'COMPLETED'\n ).length;\n return (\n <div key={group.id} className=\"mb-4 last:mb-0\">\n <div className=\"mb-2 flex items-center justify-between gap-2 px-1\">\n <p className=\"min-w-0 truncate text-sm font-semibold text-text-primary\">\n {group.title}\n </p>\n <span className=\"shrink-0 text-xs font-medium tabular-nums text-text-secondary\">\n {groupDone} / {group.lessons.length}\n </span>\n </div>\n <ul className=\"flex flex-col\">\n {group.lessons.map((lesson, i) => {\n const status = progressByLesson.get(lesson.id);\n const done = status === 'COMPLETED';\n const active = lesson.id === selectedLessonId;\n const isLast = i === group.lessons.length - 1;\n const Icon = LESSON_ICON[lesson.type];\n return (\n <li key={lesson.id} className=\"flex gap-3\">\n {/* Timeline node + connector */}\n <div className=\"flex flex-col items-center pt-1.5\">\n {done ? (\n <span className=\"flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-[var(--color-status-success-text)]\">\n <Check className=\"h-3.5 w-3.5 text-status-success-bg-subtle\" />\n </span>\n ) : active ? (\n <span className=\"flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-[var(--color-accent-soft)] ring-2 ring-[var(--color-accent-teal)]\">\n <Icon className=\"h-3.5 w-3.5 text-[var(--color-accent-teal)]\" />\n </span>\n ) : (\n <span className=\"flex h-6 w-6 shrink-0 items-center justify-center rounded-full border border-border-seam bg-bg-sunken\">\n <Icon className=\"h-3.5 w-3.5 text-text-secondary opacity-70\" />\n </span>\n )}\n {!isLast && (\n <span\n className={cn(\n 'my-1 w-px flex-1',\n done\n ? 'bg-[color-mix(in_srgb,var(--color-status-success-text)_45%,transparent)]'\n : 'bg-border-seam'\n )}\n />\n )}\n </div>\n <button\n type=\"button\"\n onClick={() => handleSelect(lesson.id)}\n className={cn(\n 'mb-1 min-w-0 flex-1 rounded-lg px-3 py-2 text-left transition-colors',\n active\n ? 'bg-[var(--color-accent-soft)]'\n : 'hover:bg-bg-sunken'\n )}\n aria-current={active ? 'true' : undefined}\n >\n <span\n className={cn(\n 'block truncate text-sm',\n active\n ? 'font-medium text-text-primary'\n : done\n ? 'text-text-secondary'\n : 'text-text-primary'\n )}\n >\n {lesson.title}\n </span>\n <span className=\"mt-0.5 block truncate text-xs text-text-secondary\">\n {formatEnumLabel(lesson.type)}\n {lesson.durationMin > 0\n ? ` · ${formatDuration(lesson.durationMin)}`\n : ''}\n </span>\n </button>\n </li>\n );\n })}\n </ul>\n </div>\n );\n })}\n </nav>\n {courseId && (\n <div className=\"border-t border-border-seam p-3\">\n <Button\n variant=\"outline\"\n size=\"sm\"\n className=\"w-full\"\n onClick={() => navigateTo(`catalog/${courseId}`)}\n >\n <BookOpen className=\"mr-1.5 h-4 w-4\" /> Course overview\n </Button>\n </div>\n )}\n </>\n )}\n </GlassCard>\n </aside>\n\n {/* Right: selected lesson */}\n <main className=\"min-w-0 flex-1\">\n {!selectedLesson ? (\n <EmptyState\n illustration=\"empty-data\"\n title=\"No lesson to show\"\n description=\"This course doesn't have any lessons yet. Check back once the author adds content.\"\n />\n ) : (\n <GlassCard treatment=\"plain\" className=\"flex flex-col gap-4 p-5 sm:p-7\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <span className=\"inline-flex items-center gap-1.5 rounded-full bg-[var(--color-accent-soft)] px-2.5 py-1 text-xs font-medium text-text-primary\">\n <SelectedIcon className=\"h-3.5 w-3.5 text-[var(--color-accent-teal)]\" />\n {formatEnumLabel(selectedLesson.type)}\n </span>\n {selectedLesson.durationMin > 0 && (\n <span className=\"text-xs text-text-secondary\">\n {formatDuration(selectedLesson.durationMin)}\n </span>\n )}\n {isSelectedComplete && (\n <span className=\"inline-flex items-center gap-1 text-xs font-medium text-status-success-text\">\n <CheckCircle2 className=\"h-3.5 w-3.5\" /> Completed\n </span>\n )}\n <span className=\"ml-auto text-xs tabular-nums text-text-secondary\">\n Lesson {selectedIndex + 1} of {orderedLessons.length}\n </span>\n </div>\n\n <h2 className=\"text-2xl font-semibold tracking-tight text-text-primary\">\n {selectedLesson.title}\n </h2>\n\n {selectedLesson.media ? (\n <div className=\"rounded-xl border border-border-seam bg-bg-sunken p-3\">\n <p className=\"mb-2 text-xs font-semibold uppercase tracking-wide text-text-secondary\">\n Resources\n </p>\n <FileLink fileId={selectedLesson.media.id} />\n </div>\n ) : null}\n\n {selectedLesson.content ? (\n <p className=\"max-w-prose whitespace-pre-wrap text-[15px] leading-7 text-text-primary\">\n {selectedLesson.content}\n </p>\n ) : (\n <p className=\"text-sm text-text-secondary\">\n This lesson has no written content\n {selectedLesson.media ? ' beyond the attached media' : ''}.\n </p>\n )}\n\n {/* Actions — Prev · Mark complete · Next */}\n <div className=\"mt-2 flex flex-col gap-3 border-t border-border-seam pt-5 sm:flex-row sm:items-center sm:justify-between\">\n <Button\n variant=\"outline\"\n size=\"sm\"\n className=\"max-sm:w-full\"\n onClick={() => goTo(-1)}\n disabled={selectedIndex <= 0}\n >\n <ChevronLeft className=\"mr-1 h-4 w-4\" /> Previous\n </Button>\n\n {isSelectedComplete ? (\n <span className=\"inline-flex items-center justify-center gap-2 rounded-lg bg-status-success-bg-subtle px-4 py-2 text-sm font-medium text-status-success-text max-sm:w-full\">\n <CheckCircle2 className=\"h-4 w-4\" /> Lesson complete\n </span>\n ) : (\n <Button\n size=\"lg\"\n className=\"max-sm:w-full\"\n onClick={handleMarkComplete}\n disabled={recordProgress.isPending}\n >\n {recordProgress.isPending ? (\n <Loader2 className=\"mr-1.5 h-4 w-4 animate-spin\" />\n ) : (\n <CheckCircle2 className=\"mr-1.5 h-4 w-4\" />\n )}\n Mark complete\n </Button>\n )}\n\n <Button\n variant=\"outline\"\n size=\"sm\"\n className=\"max-sm:w-full\"\n onClick={() => goTo(1)}\n disabled={selectedIndex >= orderedLessons.length - 1}\n >\n Next <ChevronRight className=\"ml-1 h-4 w-4\" />\n </Button>\n </div>\n </GlassCard>\n )}\n\n {selectedLesson && (\n <div className=\"mt-5\">\n {/* `key` remounts the panel per lesson so its local\n session/draft/error state resets cleanly on switch. */}\n <TutorPanel\n key={selectedLesson.id}\n lessonId={selectedLesson.id}\n lessonTitle={selectedLesson.title}\n />\n </div>\n )}\n </main>\n </div>\n </div>\n </PageLayout>\n );\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AA+DA,IAAM,IAAiD;CACrD,SAAS;CACT,OAAO;CACP,MAAM;CACN,YAAY;CACZ,aAAa;CACb,cAAc;AAChB,GAEM,KAAkD,MACtD,CAAC,GAAG,CAAK,EAAE,MAAM,GAAG,MAAM,EAAE,WAAW,EAAE,QAAQ,GAEtC,UAA6B;CACxC,IAAM,EAAE,oBAAiB,EAAoC,GACvD,EAAE,kBAAe,EAAa,GAE9B,IAAkB,GAAiB,CAAY,GAC/C,IAAa,EAAgB,QAAQ,MACrC,IAAW,GAAY,UACvB,IAAc,GAAa,CAAQ,GACnC,IAAS,EAAY,QAAQ,MAE7B,IAAiB,GAA2B,GAC5C,IAAuB,EAA2B,GAIlD,IAAe,EAAe,QAC9B,IAAkB,kBAAoB,IAAI,IAAI,CAAC,GAE/C,CAAC,GAAgB,KAAqB,EAAwB,IAAI,GAClE,CAAC,GAAU,KAAe,EAAS,EAAK,GAGxC,IAAS,QAA6B;EAC1C,IAAI,CAAC,GAAQ,OAAO,CAAC;EACrB,IAAM,oBAAO,IAAI,IAAY,GACvB,IAAuB,EAAe,EAAO,WAAW,CAAC,CAAC,GAC1D,IAAwB,CAAC;EAC/B,KAAK,IAAM,KAAK,GAAS;GACvB,IAAM,IAAU,EAAe,EAAE,WAAW,CAAC,CAAC,EAAE,QAAQ,MAClD,EAAK,IAAI,EAAE,EAAE,IAAU,MAC3B,EAAK,IAAI,EAAE,EAAE,GACN,GACR;GACD,AAAI,EAAQ,SAAS,KAAG,EAAO,KAAK;IAAE,IAAI,EAAE;IAAI,OAAO,EAAE;IAAO;GAAQ,CAAC;EAC3E;EACA,IAAM,IAAa,EAAe,EAAO,WAAW,CAAC,CAAC,EAAE,QAAQ,MAC1D,EAAK,IAAI,EAAE,EAAE,IAAU,MAC3B,EAAK,IAAI,EAAE,EAAE,GACN,GACR;EAID,OAHI,EAAW,SAAS,KACtB,EAAO,KAAK;GAAE,IAAI;GAAgB,OAAO;GAAW,SAAS;EAAW,CAAC,GAEpE;CACT,GAAG,CAAC,CAAM,CAAC,GAGL,IAAiB,QAA2B,EAAO,SAAS,MAAM,EAAE,OAAO,GAAG,CAAC,CAAM,CAAC,GAEtF,IAAmB,QAAc;EACrC,IAAM,oBAAM,IAAI,IAA+B;EAC/C,KAAK,IAAM,KAAK,GAAY,kBAAkB,CAAC,GAAG,EAAI,IAAI,EAAE,UAAU,EAAE,MAAM;EAC9E,OAAO;CACT,GAAG,CAAC,CAAU,CAAC,GAKT,IAAmB,QACnB,EAAe,WAAW,IAAU,OACpC,KAAkB,EAAe,MAAM,MAAM,EAAE,OAAO,CAAc,IAC/D,IAEF,EAAe,GAAG,IACxB,CAAC,GAAgB,CAAc,CAAC;CAGnC,SAAgB;EACd,IAAI,CAAC,KAAgB,CAAC,GAAkB;EACxC,IAAM,IAAS,EAAiB,IAAI,CAAgB;EAChD,MAAW,eAAe,MAAW,iBACrC,EAAgB,QAAQ,IAAI,CAAgB,MAChD,EAAgB,QAAQ,IAAI,CAAgB,GAC5C,EAAa;GACX;GACA,UAAU;GACV,QAAQ;EACV,CAAC;CACH,GAAG;EAAC;EAAc;EAAkB;EAAkB;CAAY,CAAC;CAEnE,IAAM,IAAgB,EAAe,WAAW,MAAM,EAAE,OAAO,CAAgB,GACzE,IAAiB,KAAiB,IAAI,EAAe,KAAiB,MAEtE,KADiB,IAAiB,EAAiB,IAAI,EAAe,EAAE,IAAI,KAAA,OACpC,aAExC,IAAiB,EAAe,QACnC,MAAM,EAAiB,IAAI,EAAE,EAAE,MAAM,WACxC,EAAE,QACI,IAAc,EAAe,SAAS,KAAK,MAAmB,EAAe,QAC7E,KAAkB,GAAY,WAAW,aAEzC,KAAgB,MAAqB;EAEzC,AADA,EAAkB,CAAQ,GAC1B,EAAY,EAAK;CACnB,GAEM,KAAQ,MAAmB;EAC/B,IAAI,IAAgB,GAAG;EACvB,IAAM,IAAO,EAAe,IAAgB;EAC5C,AAAI,KAAM,EAAa,EAAK,EAAE;CAChC,GAEM,KAAqB,YAAY;EACrC,IAAI,CAAC,KAAgB,CAAC,GAAgB;EACtC,MAAM,EAAe,YAAY;GAC/B;GACA,UAAU,EAAe;GACzB,QAAQ;EACV,CAAC;EAED,IAAM,IAAO,EAAe,IAAgB;EAC5C,AAAI,KAAM,EAAkB,EAAK,EAAE;CACrC,GAEM,KAAqB,YAAY;EAChC,KACL,MAAM,EAAqB,YAAY;GAAE,IAAI;GAAc,QAAQ;EAAY,CAAC;CAClF;CAGA,IAAI,CAAC,GACH,OACE,kBAAC,GAAD;EAAY,OAAM;YAChB,kBAAC,GAAD;GACE,cAAa;GACb,OAAM;GACN,aAAY;GACZ,QAAQ,kBAAC,GAAD;IAAQ,eAAe,EAAW,UAAU;cAAG;GAA2B,CAAA;EACnF,CAAA;CACS,CAAA;CAIhB,IAAM,KAAY,EAAgB,aAAc,EAAQ,KAAa,EAAY,WAE3E,IACJ,kBAAC,GAAD;EAAQ,SAAQ;EAAQ,MAAK;EAAK,eAAe,EAAW,UAAU;YAAtE,CACE,kBAAC,IAAD,EAAW,WAAU,eAAgB,CAAA,GAAC,cAChC;;CAGV,IAAI,IACF,OACE,kBAAC,GAAD;EAAY,OAAM;EAAS,SAAS;YAClC,kBAAC,GAAD,EAAa,OAAM,uBAAwB,CAAA;CACjC,CAAA;CAIhB,IAAI,EAAgB,SAClB,OACE,kBAAC,GAAD;EAAY,OAAM;EAAS,SAAS;YAClC,kBAAC,GAAD;GACE,OAAM;GACN,SACE,EAAgB,iBAAiB,QAAQ,EAAgB,MAAM,UAAU,KAAA;GAE3E,eAAe,EAAgB,QAAQ;EACxC,CAAA;CACS,CAAA;CAIhB,IAAI,CAAC,GACH,OACE,kBAAC,GAAD;EAAY,OAAM;EAAS,SAAS;YAClC,kBAAC,GAAD;GACE,cAAa;GACb,OAAM;GACN,aAAY;GACZ,QAAQ,kBAAC,GAAD;IAAQ,eAAe,EAAW,UAAU;cAAG;GAA2B,CAAA;EACnF,CAAA;CACS,CAAA;CAIhB,IAAI,EAAY,SACd,OACE,kBAAC,GAAD;EAAY,OAAM;EAAS,SAAS;YAClC,kBAAC,GAAD;GACE,OAAM;GACN,SAAS,EAAY,iBAAiB,QAAQ,EAAY,MAAM,UAAU,KAAA;GAC1E,eAAe,EAAY,QAAQ;EACpC,CAAA;CACS,CAAA;CAIhB,IAAM,IAAc,KAAK,MAAM,EAAW,eAAe,CAAC,GACpD,KAAe,IAAiB,EAAY,EAAe,QAAQ;CAEzE,OACE,kBAAC,GAAD;EAAY,OAAO,GAAQ,SAAS;EAAU,SAAS;YACrD,kBAAC,OAAD;GAAK,WAAU;aAAf;IAEE,kBAAC,GAAD;KACE,WAAU;KACV,MAAM,IAAc;KACpB,WAAU;eAHZ;MAKE,kBAAC,IAAD;OAAc,OAAO;OAAa,MAAM;OAAI,WAAU;MAAY,CAAA;MAClE,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,GAAD,EAAa,QAAQ,EAAW,OAAS,CAAA,GACzC,kBAAC,QAAD;SAAM,WAAU;mBAAhB;UACG;UAAe;UAAK,EAAe;UAAO;SACvC;UACH;WACL,kBAAC,GAAD;QAAS,OAAO;QAAa,WAAU;OAAQ,CAAA,CAC5C;;MACJ,KAAe,CAAC,MACf,kBAAC,GAAD;OACE,WAAU;OACV,SAAS;OACT,UAAU,EAAqB;iBAHjC,CAKG,EAAqB,YACpB,kBAAC,GAAD,EAAS,WAAU,4BAA6B,CAAA,IAEhD,kBAAC,GAAD,EAAQ,WAAU,eAAgB,CAAA,GAClC,eAEI;;KAED;;IAGX,kBAAC,GAAD;KACE,SAAQ;KACR,MAAK;KACL,WAAU;KACV,eAAe,GAAa,MAAM,CAAC,CAAC;eAJtC,CAME,kBAAC,GAAD,EAAY,WAAU,eAAgB,CAAA,GACrC,IAAW,iBAAiB,YAAY,EAAe,GAAG,EAAe,OAAO,EAC3E;;IAER,kBAAC,OAAD;KAAK,WAAU;eAAf,CAEE,kBAAC,SAAD;MAAO,WAAW,EAAG,gCAAgC,IAAW,UAAU,QAAQ;gBAChF,kBAAC,GAAD;OAAW,WAAU;OAAQ,WAAU;iBACpC,EAAe,WAAW,IACzB,kBAAC,OAAD;QAAK,WAAU;kBAAkC;OAE5C,CAAA,IAEL,kBAAA,IAAA,EAAA,UAAA,CACE,kBAAC,OAAD;QAAK,WAAU;kBACZ,EAAO,KAAK,MAAU;SACrB,IAAM,IAAY,EAAM,QAAQ,QAC7B,MAAM,EAAiB,IAAI,EAAE,EAAE,MAAM,WACxC,EAAE;SACF,OACE,kBAAC,OAAD;UAAoB,WAAU;oBAA9B,CACE,kBAAC,OAAD;WAAK,WAAU;qBAAf,CACE,kBAAC,KAAD;YAAG,WAAU;sBACV,EAAM;WACN,CAAA,GACH,kBAAC,QAAD;YAAM,WAAU;sBAAhB;aACG;aAAU;aAAI,EAAM,QAAQ;YACzB;aACH;cACL,kBAAC,MAAD;WAAI,WAAU;qBACX,EAAM,QAAQ,KAAK,GAAQ,MAAM;YAEhC,IAAM,IADS,EAAiB,IAAI,EAAO,EAC9B,MAAW,aAClB,IAAS,EAAO,OAAO,GACvB,IAAS,MAAM,EAAM,QAAQ,SAAS,GACtC,IAAO,EAAY,EAAO;YAChC,OACE,kBAAC,MAAD;aAAoB,WAAU;uBAA9B,CAEE,kBAAC,OAAD;cAAK,WAAU;wBAAf,CACG,IACC,kBAAC,QAAD;eAAM,WAAU;yBACd,kBAAC,IAAD,EAAO,WAAU,4CAA6C,CAAA;cAC1D,CAAA,IACJ,IACF,kBAAC,QAAD;eAAM,WAAU;yBACd,kBAAC,GAAD,EAAM,WAAU,8CAA+C,CAAA;cAC3D,CAAA,IAEN,kBAAC,QAAD;eAAM,WAAU;yBACd,kBAAC,GAAD,EAAM,WAAU,6CAA8C,CAAA;cAC1D,CAAA,GAEP,CAAC,KACA,kBAAC,QAAD,EACE,WAAW,EACT,oBACA,IACI,6EACA,gBACN,EACD,CAAA,CAEA;iBACL,kBAAC,UAAD;cACE,MAAK;cACL,eAAe,EAAa,EAAO,EAAE;cACrC,WAAW,EACT,wEACA,IACI,kCACA,oBACN;cACA,gBAAc,IAAS,SAAS,KAAA;wBATlC,CAWE,kBAAC,QAAD;eACE,WAAW,EACT,0BACA,IACI,kCACA,IACE,wBACA,mBACR;yBAEC,EAAO;cACJ,CAAA,GACN,kBAAC,QAAD;eAAM,WAAU;yBAAhB,CACG,EAAgB,EAAO,IAAI,GAC3B,EAAO,cAAc,IAClB,MAAM,EAAe,EAAO,WAAW,MACvC,EACA;gBACA;eACN;eAzDK,EAAO,EAyDZ;WAER,CAAC;UACC,CAAA,CACD;YA9EK,EAAM,EA8EX;QAET,CAAC;OACE,CAAA,GACJ,KACC,kBAAC,OAAD;QAAK,WAAU;kBACb,kBAAC,GAAD;SACE,SAAQ;SACR,MAAK;SACL,WAAU;SACV,eAAe,EAAW,WAAW,GAAU;mBAJjD,CAME,kBAAC,IAAD,EAAU,WAAU,iBAAkB,CAAA,GAAC,kBACjC;;OACL,CAAA,CAEP,EAAA,CAAA;MAEK,CAAA;KACN,CAAA,GAGP,kBAAC,QAAD;MAAM,WAAU;gBAAhB,CACI,IAOA,kBAAC,GAAD;OAAW,WAAU;OAAQ,WAAU;iBAAvC;QACE,kBAAC,OAAD;SAAK,WAAU;mBAAf;UACE,kBAAC,QAAD;WAAM,WAAU;qBAAhB,CACE,kBAAC,IAAD,EAAc,WAAU,8CAA+C,CAAA,GACtE,EAAgB,EAAe,IAAI,CAChC;;UACL,EAAe,cAAc,KAC5B,kBAAC,QAAD;WAAM,WAAU;qBACb,EAAe,EAAe,WAAW;UACtC,CAAA;UAEP,KACC,kBAAC,QAAD;WAAM,WAAU;qBAAhB,CACE,kBAAC,GAAD,EAAc,WAAU,cAAe,CAAA,GAAC,YACpC;;UAER,kBAAC,QAAD;WAAM,WAAU;qBAAhB;YAAmE;YACzD,IAAgB;YAAE;YAAK,EAAe;WAC1C;;SACH;;QAEL,kBAAC,MAAD;SAAI,WAAU;mBACX,EAAe;QACd,CAAA;QAEH,EAAe,QACd,kBAAC,OAAD;SAAK,WAAU;mBAAf,CACE,kBAAC,KAAD;UAAG,WAAU;oBAAyE;SAEnF,CAAA,GACH,kBAAC,IAAD,EAAU,QAAQ,EAAe,MAAM,GAAK,CAAA,CACzC;aACH;QAEH,EAAe,UACd,kBAAC,KAAD;SAAG,WAAU;mBACV,EAAe;QACf,CAAA,IAEH,kBAAC,KAAD;SAAG,WAAU;mBAAb;UAA2C;UAExC,EAAe,QAAQ,+BAA+B;UAAG;SACzD;;QAIL,kBAAC,OAAD;SAAK,WAAU;mBAAf;UACE,kBAAC,GAAD;WACE,SAAQ;WACR,MAAK;WACL,WAAU;WACV,eAAe,EAAK,EAAE;WACtB,UAAU,KAAiB;qBAL7B,CAOE,kBAAC,IAAD,EAAa,WAAU,eAAgB,CAAA,GAAC,WAClC;;UAEP,IACC,kBAAC,QAAD;WAAM,WAAU;qBAAhB,CACE,kBAAC,GAAD,EAAc,WAAU,UAAW,CAAA,GAAC,kBAChC;eAEN,kBAAC,GAAD;WACE,MAAK;WACL,WAAU;WACV,SAAS;WACT,UAAU,EAAe;qBAJ3B,CAMG,EAAe,YACd,kBAAC,GAAD,EAAS,WAAU,8BAA+B,CAAA,IAElD,kBAAC,GAAD,EAAc,WAAU,iBAAkB,CAAA,GAC1C,eAEI;;UAGV,kBAAC,GAAD;WACE,SAAQ;WACR,MAAK;WACL,WAAU;WACV,eAAe,EAAK,CAAC;WACrB,UAAU,KAAiB,EAAe,SAAS;qBALrD,CAMC,SACM,kBAAC,IAAD,EAAc,WAAU,eAAgB,CAAA,CACvC;;SACL;;OACI;WA7FX,kBAAC,GAAD;OACE,cAAa;OACb,OAAM;OACN,aAAY;MACb,CAAA,GA4FF,KACC,kBAAC,OAAD;OAAK,WAAU;iBAGb,kBAAC,IAAD;QAEE,UAAU,EAAe;QACzB,aAAa,EAAe;OAC7B,GAHM,EAAe,EAGrB;MACE,CAAA,CAEH;OACH;;GACF;;CACK,CAAA;AAEhB"}
|
|
1
|
+
{"version":3,"file":"LessonPlayerPage.js","names":[],"sources":["../../../../src/buildmyiq/pages/learning/LessonPlayerPage.tsx"],"sourcesContent":["/**\n * LessonPlayerPage — the two-pane lesson player for a single enrollment.\n * @module buildmyiq/pages/learning\n *\n * Left pane: the course's module/lesson tree (from `buiCourse`) rendered as a\n * completion timeline driven by the enrollment's `lessonProgress`. Right pane:\n * the selected lesson (title, type, content text rendered safely, and a\n * `FileLink` for any attached media) with Prev / Mark-complete / Next controls.\n * Opening a lesson records IN_PROGRESS once; \"Mark complete\" records COMPLETED;\n * finishing every lesson transitions the enrollment to COMPLETED. All data is\n * live — no fabrication.\n */\n\nimport type { FC } from 'react';\nimport { useEffect, useMemo, useRef, useState } from 'react';\nimport { useParams } from 'react-router-dom';\nimport type { LucideIcon } from 'lucide-react';\nimport {\n ArrowLeft,\n BookOpen,\n Check,\n CheckCircle2,\n ChevronLeft,\n ChevronRight,\n FileText,\n HelpCircle,\n Loader2,\n ListChecks,\n MousePointerClick,\n PenLine,\n PlayCircle,\n Radio,\n Trophy,\n} from 'lucide-react';\nimport { Button, GlassCard } from '@burdenoff/fe-libs/ui';\nimport {\n PageLayout,\n StatusBadge,\n MiniBar,\n ProgressRing,\n LoadingView,\n ErrorView,\n EmptyState,\n FileLink,\n OfflineLessonControl,\n TutorPanel,\n} from '../../components';\nimport { useBuildmyiq } from '../../context';\nimport {\n useBuiEnrollment,\n useBuiCourse,\n useBuiRecordLessonProgress,\n useBuiTransitionEnrollment,\n} from '../../hooks';\nimport { cn } from '../../utils/cn';\nimport { formatDuration, formatEnumLabel } from '../../utils/formatters';\nimport type { BuiLesson, BuiLessonType, BuiModule, BuiProgressStatus } from '../../types';\n\ninterface LessonGroup {\n id: string;\n title: string;\n lessons: BuiLesson[];\n}\n\nconst LESSON_ICON: Record<BuiLessonType, LucideIcon> = {\n ARTICLE: FileText,\n VIDEO: PlayCircle,\n QUIZ: HelpCircle,\n ASSIGNMENT: PenLine,\n INTERACTIVE: MousePointerClick,\n LIVE_SESSION: Radio,\n};\n\nconst sortByPosition = <T extends { position: number }>(items: T[]): T[] =>\n [...items].sort((a, b) => a.position - b.position);\n\nexport const LessonPlayerPage: FC = () => {\n const { enrollmentId } = useParams<{ enrollmentId: string }>();\n const { navigateTo } = useBuildmyiq();\n\n const enrollmentQuery = useBuiEnrollment(enrollmentId);\n const enrollment = enrollmentQuery.data ?? null;\n const courseId = enrollment?.courseId;\n const courseQuery = useBuiCourse(courseId);\n const course = courseQuery.data ?? null;\n\n const recordProgress = useBuiRecordLessonProgress();\n const transitionEnrollment = useBuiTransitionEnrollment();\n\n // react-query's mutate is referentially stable across renders, so it can be\n // used directly in effects without a render-time ref write.\n const recordMutate = recordProgress.mutate;\n const autoRecordedRef = useRef<Set<string>>(new Set());\n\n const [userSelectedId, setUserSelectedId] = useState<string | null>(null);\n const [showTree, setShowTree] = useState(false);\n\n // Grouped tree (modules → lessons) plus any standalone lessons, de-duplicated.\n const groups = useMemo<LessonGroup[]>(() => {\n if (!course) return [];\n const seen = new Set<string>();\n const modules: BuiModule[] = sortByPosition(course.modules ?? []);\n const result: LessonGroup[] = [];\n for (const m of modules) {\n const lessons = sortByPosition(m.lessons ?? []).filter((l) => {\n if (seen.has(l.id)) return false;\n seen.add(l.id);\n return true;\n });\n if (lessons.length > 0) result.push({ id: m.id, title: m.title, lessons });\n }\n const standalone = sortByPosition(course.lessons ?? []).filter((l) => {\n if (seen.has(l.id)) return false;\n seen.add(l.id);\n return true;\n });\n if (standalone.length > 0) {\n result.push({ id: '__standalone', title: 'Lessons', lessons: standalone });\n }\n return result;\n }, [course]);\n\n // Flat, ordered lesson list for prev/next + completion checks.\n const orderedLessons = useMemo<BuiLesson[]>(() => groups.flatMap((g) => g.lessons), [groups]);\n\n const progressByLesson = useMemo(() => {\n const map = new Map<string, BuiProgressStatus>();\n for (const p of enrollment?.lessonProgress ?? []) map.set(p.lessonId, p.status);\n return map;\n }, [enrollment]);\n\n // Resolve the active lesson as DERIVED state: the user's pick if it still\n // exists in the course, otherwise the first lesson. Avoids a setState-in-\n // effect re-anchor loop.\n const selectedLessonId = useMemo<string | null>(() => {\n if (orderedLessons.length === 0) return null;\n if (userSelectedId && orderedLessons.some((l) => l.id === userSelectedId)) {\n return userSelectedId;\n }\n return orderedLessons[0].id;\n }, [orderedLessons, userSelectedId]);\n\n // Record IN_PROGRESS once when a not-yet-started lesson is opened.\n useEffect(() => {\n if (!enrollmentId || !selectedLessonId) return;\n const status = progressByLesson.get(selectedLessonId);\n if (status === 'COMPLETED' || status === 'IN_PROGRESS') return;\n if (autoRecordedRef.current.has(selectedLessonId)) return;\n autoRecordedRef.current.add(selectedLessonId);\n recordMutate({\n enrollmentId,\n lessonId: selectedLessonId,\n status: 'IN_PROGRESS',\n });\n }, [enrollmentId, selectedLessonId, progressByLesson, recordMutate]);\n\n const selectedIndex = orderedLessons.findIndex((l) => l.id === selectedLessonId);\n const selectedLesson = selectedIndex >= 0 ? orderedLessons[selectedIndex] : null;\n const selectedStatus = selectedLesson ? progressByLesson.get(selectedLesson.id) : undefined;\n const isSelectedComplete = selectedStatus === 'COMPLETED';\n\n const completedCount = orderedLessons.filter(\n (l) => progressByLesson.get(l.id) === 'COMPLETED'\n ).length;\n const allComplete = orderedLessons.length > 0 && completedCount === orderedLessons.length;\n const alreadyFinished = enrollment?.status === 'COMPLETED';\n\n const handleSelect = (lessonId: string) => {\n setUserSelectedId(lessonId);\n setShowTree(false);\n };\n\n const goTo = (offset: number) => {\n if (selectedIndex < 0) return;\n const next = orderedLessons[selectedIndex + offset];\n if (next) handleSelect(next.id);\n };\n\n const handleMarkComplete = async () => {\n if (!enrollmentId || !selectedLesson) return;\n await recordProgress.mutateAsync({\n enrollmentId,\n lessonId: selectedLesson.id,\n status: 'COMPLETED',\n });\n // Advance to the next incomplete lesson for a natural flow.\n const next = orderedLessons[selectedIndex + 1];\n if (next) setUserSelectedId(next.id);\n };\n\n const handleFinishCourse = async () => {\n if (!enrollmentId) return;\n await transitionEnrollment.mutateAsync({ id: enrollmentId, status: 'COMPLETED' });\n };\n\n // ---- top-level states ----------------------------------------------------\n if (!enrollmentId) {\n return (\n <PageLayout title=\"Lesson\">\n <EmptyState\n illustration=\"empty-data\"\n title=\"No enrollment selected\"\n description=\"Open a course from My Learning to start the lesson player.\"\n action={<Button onClick={() => navigateTo('learning')}>Back to My Learning</Button>}\n />\n </PageLayout>\n );\n }\n\n const isLoading = enrollmentQuery.isLoading || (Boolean(courseId) && courseQuery.isLoading);\n\n const backAction = (\n <Button variant=\"ghost\" size=\"sm\" onClick={() => navigateTo('learning')}>\n <ArrowLeft className=\"mr-1 h-4 w-4\" /> My Learning\n </Button>\n );\n\n if (isLoading) {\n return (\n <PageLayout title=\"Lesson\" actions={backAction}>\n <LoadingView label=\"Loading your lesson…\" />\n </PageLayout>\n );\n }\n\n if (enrollmentQuery.isError) {\n return (\n <PageLayout title=\"Lesson\" actions={backAction}>\n <ErrorView\n title=\"Couldn't load this enrollment\"\n message={\n enrollmentQuery.error instanceof Error ? enrollmentQuery.error.message : undefined\n }\n onRetry={() => enrollmentQuery.refetch()}\n />\n </PageLayout>\n );\n }\n\n if (!enrollment) {\n return (\n <PageLayout title=\"Lesson\" actions={backAction}>\n <EmptyState\n illustration=\"empty-data\"\n title=\"Enrollment not found\"\n description=\"This enrollment may have been removed or is not available to you.\"\n action={<Button onClick={() => navigateTo('learning')}>Back to My Learning</Button>}\n />\n </PageLayout>\n );\n }\n\n if (courseQuery.isError) {\n return (\n <PageLayout title=\"Lesson\" actions={backAction}>\n <ErrorView\n title=\"Couldn't load the course\"\n message={courseQuery.error instanceof Error ? courseQuery.error.message : undefined}\n onRetry={() => courseQuery.refetch()}\n />\n </PageLayout>\n );\n }\n\n const progressPct = Math.round(enrollment.progressPct ?? 0);\n const SelectedIcon = selectedLesson ? LESSON_ICON[selectedLesson.type] : FileText;\n\n return (\n <PageLayout title={course?.title ?? 'Lesson'} actions={backAction}>\n <div className=\"flex flex-col gap-5\">\n {/* Header progress — ProgressRing hero + completion bar */}\n <GlassCard\n treatment=\"glass\"\n glow={progressPct > 0}\n className=\"flex flex-col gap-4 p-4 sm:flex-row sm:items-center sm:gap-5\"\n >\n <ProgressRing value={progressPct} size={64} className=\"shrink-0\" />\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <StatusBadge status={enrollment.status} />\n <span className=\"text-sm text-text-secondary tabular-nums\">\n {completedCount} of {orderedLessons.length} lessons complete\n </span>\n </div>\n <MiniBar value={progressPct} className=\"mt-3\" />\n </div>\n {allComplete && !alreadyFinished && (\n <Button\n className=\"shrink-0 max-sm:w-full\"\n onClick={handleFinishCourse}\n disabled={transitionEnrollment.isPending}\n >\n {transitionEnrollment.isPending ? (\n <Loader2 className=\"mr-1 h-4 w-4 animate-spin\" />\n ) : (\n <Trophy className=\"mr-1 h-4 w-4\" />\n )}\n Finish course\n </Button>\n )}\n </GlassCard>\n\n {/* Mobile tree toggle — collapses the outline into a top sheet */}\n <Button\n variant=\"secondary\"\n size=\"sm\"\n className=\"w-full lg:hidden\"\n onClick={() => setShowTree((v) => !v)}\n >\n <ListChecks className=\"mr-1 h-4 w-4\" />\n {showTree ? 'Hide lessons' : `Lessons (${completedCount}/${orderedLessons.length})`}\n </Button>\n\n <div className=\"flex flex-col gap-5 lg:flex-row\">\n {/* Left: module / lesson completion timeline */}\n <aside className={cn('lg:block lg:w-80 lg:shrink-0', showTree ? 'block' : 'hidden')}>\n <GlassCard treatment=\"plain\" className=\"overflow-hidden p-0 lg:sticky lg:top-4\">\n {orderedLessons.length === 0 ? (\n <div className=\"p-4 text-sm text-text-secondary\">\n This course has no lessons yet.\n </div>\n ) : (\n <>\n <nav className=\"max-h-[68vh] overflow-auto p-3\">\n {groups.map((group) => {\n const groupDone = group.lessons.filter(\n (l) => progressByLesson.get(l.id) === 'COMPLETED'\n ).length;\n return (\n <div key={group.id} className=\"mb-4 last:mb-0\">\n <div className=\"mb-2 flex items-center justify-between gap-2 px-1\">\n <p className=\"min-w-0 truncate text-sm font-semibold text-text-primary\">\n {group.title}\n </p>\n <span className=\"shrink-0 text-xs font-medium tabular-nums text-text-secondary\">\n {groupDone} / {group.lessons.length}\n </span>\n </div>\n <ul className=\"flex flex-col\">\n {group.lessons.map((lesson, i) => {\n const status = progressByLesson.get(lesson.id);\n const done = status === 'COMPLETED';\n const active = lesson.id === selectedLessonId;\n const isLast = i === group.lessons.length - 1;\n const Icon = LESSON_ICON[lesson.type];\n return (\n <li key={lesson.id} className=\"flex gap-3\">\n {/* Timeline node + connector */}\n <div className=\"flex flex-col items-center pt-1.5\">\n {done ? (\n <span className=\"flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-[var(--color-status-success-text)]\">\n <Check className=\"h-3.5 w-3.5 text-status-success-bg-subtle\" />\n </span>\n ) : active ? (\n <span className=\"flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-[var(--color-accent-soft)] ring-2 ring-[var(--color-accent-teal)]\">\n <Icon className=\"h-3.5 w-3.5 text-[var(--color-accent-teal)]\" />\n </span>\n ) : (\n <span className=\"flex h-6 w-6 shrink-0 items-center justify-center rounded-full border border-border-seam bg-bg-sunken\">\n <Icon className=\"h-3.5 w-3.5 text-text-secondary opacity-70\" />\n </span>\n )}\n {!isLast && (\n <span\n className={cn(\n 'my-1 w-px flex-1',\n done\n ? 'bg-[color-mix(in_srgb,var(--color-status-success-text)_45%,transparent)]'\n : 'bg-border-seam'\n )}\n />\n )}\n </div>\n <button\n type=\"button\"\n onClick={() => handleSelect(lesson.id)}\n className={cn(\n 'mb-1 min-w-0 flex-1 rounded-lg px-3 py-2 text-left transition-colors',\n active\n ? 'bg-[var(--color-accent-soft)]'\n : 'hover:bg-bg-sunken'\n )}\n aria-current={active ? 'true' : undefined}\n >\n <span\n className={cn(\n 'block truncate text-sm',\n active\n ? 'font-medium text-text-primary'\n : done\n ? 'text-text-secondary'\n : 'text-text-primary'\n )}\n >\n {lesson.title}\n </span>\n <span className=\"mt-0.5 block truncate text-xs text-text-secondary\">\n {formatEnumLabel(lesson.type)}\n {lesson.durationMin > 0\n ? ` · ${formatDuration(lesson.durationMin)}`\n : ''}\n </span>\n </button>\n </li>\n );\n })}\n </ul>\n </div>\n );\n })}\n </nav>\n {courseId && (\n <div className=\"border-t border-border-seam p-3\">\n <Button\n variant=\"outline\"\n size=\"sm\"\n className=\"w-full\"\n onClick={() => navigateTo(`catalog/${courseId}`)}\n >\n <BookOpen className=\"mr-1.5 h-4 w-4\" /> Course overview\n </Button>\n </div>\n )}\n </>\n )}\n </GlassCard>\n </aside>\n\n {/* Right: selected lesson */}\n <main className=\"min-w-0 flex-1\">\n {!selectedLesson ? (\n <EmptyState\n illustration=\"empty-data\"\n title=\"No lesson to show\"\n description=\"This course doesn't have any lessons yet. Check back once the author adds content.\"\n />\n ) : (\n <GlassCard treatment=\"plain\" className=\"flex flex-col gap-4 p-5 sm:p-7\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <span className=\"inline-flex items-center gap-1.5 rounded-full bg-[var(--color-accent-soft)] px-2.5 py-1 text-xs font-medium text-text-primary\">\n <SelectedIcon className=\"h-3.5 w-3.5 text-[var(--color-accent-teal)]\" />\n {formatEnumLabel(selectedLesson.type)}\n </span>\n {selectedLesson.durationMin > 0 && (\n <span className=\"text-xs text-text-secondary\">\n {formatDuration(selectedLesson.durationMin)}\n </span>\n )}\n {isSelectedComplete && (\n <span className=\"inline-flex items-center gap-1 text-xs font-medium text-status-success-text\">\n <CheckCircle2 className=\"h-3.5 w-3.5\" /> Completed\n </span>\n )}\n <OfflineLessonControl key={selectedLesson.id} lesson={selectedLesson} />\n <span className=\"ml-auto text-xs tabular-nums text-text-secondary\">\n Lesson {selectedIndex + 1} of {orderedLessons.length}\n </span>\n </div>\n\n <h2 className=\"text-2xl font-semibold tracking-tight text-text-primary\">\n {selectedLesson.title}\n </h2>\n\n {selectedLesson.media ? (\n <div className=\"rounded-xl border border-border-seam bg-bg-sunken p-3\">\n <p className=\"mb-2 text-xs font-semibold uppercase tracking-wide text-text-secondary\">\n Resources\n </p>\n <FileLink fileId={selectedLesson.media.id} />\n </div>\n ) : null}\n\n {selectedLesson.content ? (\n <p className=\"max-w-prose whitespace-pre-wrap text-[15px] leading-7 text-text-primary\">\n {selectedLesson.content}\n </p>\n ) : (\n <p className=\"text-sm text-text-secondary\">\n This lesson has no written content\n {selectedLesson.media ? ' beyond the attached media' : ''}.\n </p>\n )}\n\n {/* Actions — Prev · Mark complete · Next */}\n <div className=\"mt-2 flex flex-col gap-3 border-t border-border-seam pt-5 sm:flex-row sm:items-center sm:justify-between\">\n <Button\n variant=\"outline\"\n size=\"sm\"\n className=\"max-sm:w-full\"\n onClick={() => goTo(-1)}\n disabled={selectedIndex <= 0}\n >\n <ChevronLeft className=\"mr-1 h-4 w-4\" /> Previous\n </Button>\n\n {isSelectedComplete ? (\n <span className=\"inline-flex items-center justify-center gap-2 rounded-lg bg-status-success-bg-subtle px-4 py-2 text-sm font-medium text-status-success-text max-sm:w-full\">\n <CheckCircle2 className=\"h-4 w-4\" /> Lesson complete\n </span>\n ) : (\n <Button\n size=\"lg\"\n className=\"max-sm:w-full\"\n onClick={handleMarkComplete}\n disabled={recordProgress.isPending}\n >\n {recordProgress.isPending ? (\n <Loader2 className=\"mr-1.5 h-4 w-4 animate-spin\" />\n ) : (\n <CheckCircle2 className=\"mr-1.5 h-4 w-4\" />\n )}\n Mark complete\n </Button>\n )}\n\n <Button\n variant=\"outline\"\n size=\"sm\"\n className=\"max-sm:w-full\"\n onClick={() => goTo(1)}\n disabled={selectedIndex >= orderedLessons.length - 1}\n >\n Next <ChevronRight className=\"ml-1 h-4 w-4\" />\n </Button>\n </div>\n </GlassCard>\n )}\n\n {selectedLesson && (\n <div className=\"mt-5\">\n {/* `key` remounts the panel per lesson so its local\n session/draft/error state resets cleanly on switch. */}\n <TutorPanel\n key={selectedLesson.id}\n lessonId={selectedLesson.id}\n lessonTitle={selectedLesson.title}\n />\n </div>\n )}\n </main>\n </div>\n </div>\n </PageLayout>\n );\n};\n"],"mappings":"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"}
|