@burdenoff/microfe-technospam 2026.1101.4 → 2026.1102.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/dist/index.js +5 -3
  2. package/dist/technospam/TechnoSpamRoutes.d.ts.map +1 -1
  3. package/dist/technospam/TechnoSpamRoutes.js +91 -90
  4. package/dist/technospam/TechnoSpamRoutes.js.map +1 -1
  5. package/dist/technospam/api/technospamApi.d.ts +21 -7
  6. package/dist/technospam/api/technospamApi.d.ts.map +1 -1
  7. package/dist/technospam/api/technospamApi.js +317 -284
  8. package/dist/technospam/api/technospamApi.js.map +1 -1
  9. package/dist/technospam/components/CertificateVerifyLink.d.ts +11 -0
  10. package/dist/technospam/components/CertificateVerifyLink.d.ts.map +1 -0
  11. package/dist/technospam/components/CertificateVerifyLink.js +63 -0
  12. package/dist/technospam/components/CertificateVerifyLink.js.map +1 -0
  13. package/dist/technospam/components/index.d.ts +1 -0
  14. package/dist/technospam/components/index.d.ts.map +1 -1
  15. package/dist/technospam/components/index.js +1 -0
  16. package/dist/technospam/components/lesson/OfflineProgressSync.d.ts +9 -0
  17. package/dist/technospam/components/lesson/OfflineProgressSync.d.ts.map +1 -0
  18. package/dist/technospam/components/lesson/OfflineProgressSync.js +20 -0
  19. package/dist/technospam/components/lesson/OfflineProgressSync.js.map +1 -0
  20. package/dist/technospam/components/lesson/SaveCourseOffline.d.ts +17 -0
  21. package/dist/technospam/components/lesson/SaveCourseOffline.d.ts.map +1 -0
  22. package/dist/technospam/components/lesson/SaveCourseOffline.js +73 -0
  23. package/dist/technospam/components/lesson/SaveCourseOffline.js.map +1 -0
  24. package/dist/technospam/components/lesson/SavedCourseOutline.d.ts +17 -0
  25. package/dist/technospam/components/lesson/SavedCourseOutline.d.ts.map +1 -0
  26. package/dist/technospam/components/lesson/SavedCourseOutline.js +77 -0
  27. package/dist/technospam/components/lesson/SavedCourseOutline.js.map +1 -0
  28. package/dist/technospam/hooks/useOfflineLessons.d.ts +26 -0
  29. package/dist/technospam/hooks/useOfflineLessons.d.ts.map +1 -0
  30. package/dist/technospam/hooks/useOfflineLessons.js +70 -0
  31. package/dist/technospam/hooks/useOfflineLessons.js.map +1 -0
  32. package/dist/technospam/index.d.ts +2 -0
  33. package/dist/technospam/index.d.ts.map +1 -1
  34. package/dist/technospam/index.js +2 -0
  35. package/dist/technospam/lib/certificateVerify.d.ts +71 -0
  36. package/dist/technospam/lib/certificateVerify.d.ts.map +1 -0
  37. package/dist/technospam/lib/certificateVerify.js +55 -0
  38. package/dist/technospam/lib/certificateVerify.js.map +1 -0
  39. package/dist/technospam/lib/offlineLessons.d.ts +97 -0
  40. package/dist/technospam/lib/offlineLessons.d.ts.map +1 -0
  41. package/dist/technospam/lib/offlineLessons.js +168 -0
  42. package/dist/technospam/lib/offlineLessons.js.map +1 -0
  43. package/dist/technospam/pages/CoursesCourseDetailsPagePage.d.ts.map +1 -1
  44. package/dist/technospam/pages/CoursesCourseDetailsPagePage.js +241 -198
  45. package/dist/technospam/pages/CoursesCourseDetailsPagePage.js.map +1 -1
  46. package/dist/technospam/pages/CoursesLessonPlayerPagePage.d.ts.map +1 -1
  47. package/dist/technospam/pages/CoursesLessonPlayerPagePage.js +216 -138
  48. package/dist/technospam/pages/CoursesLessonPlayerPagePage.js.map +1 -1
  49. package/dist/technospam/pages/CoursesMyLearningPagePage.d.ts.map +1 -1
  50. package/dist/technospam/pages/CoursesMyLearningPagePage.js +104 -96
  51. package/dist/technospam/pages/CoursesMyLearningPagePage.js.map +1 -1
  52. package/dist/technospam/pages/public/CertificateVerifyPage.d.ts +4 -0
  53. package/dist/technospam/pages/public/CertificateVerifyPage.d.ts.map +1 -0
  54. package/dist/technospam/pages/public/CertificateVerifyPage.js +257 -0
  55. package/dist/technospam/pages/public/CertificateVerifyPage.js.map +1 -0
  56. package/package.json +1 -1
@@ -2,281 +2,359 @@ import { useTechnoSpam as e, useTechnoSpamNavigate as t } from "../providers/Tec
2
2
  import { useTechnoSpamAnalytics as n } from "../hooks/useTechnoSpamAnalytics.js";
3
3
  import { cn as r } from "../utils/cn.js";
4
4
  import { PageLayout as i } from "../components/PageLayout.js";
5
- import { formatDuration as ee, humanizeEnum as a } from "../utils/format.js";
6
- import { UpgradeGate as o } from "../components/UpgradeGate.js";
7
- import { EmptyView as s, ErrorView as c } from "../components/StateViews.js";
8
- import { notify as l } from "../lib/toast.js";
9
- import { useCompleteLesson as u, useEnroll as d, useLessonPlayerQuery as te, useMyEnrollmentsQuery as f } from "../api/technospamApi.js";
5
+ import { formatDuration as ee, humanizeEnum as te } from "../utils/format.js";
6
+ import { UpgradeGate as ne } from "../components/UpgradeGate.js";
7
+ import { EmptyView as a, ErrorView as re } from "../components/StateViews.js";
8
+ import { notify as o } from "../lib/toast.js";
9
+ import { useCompleteLesson as ie, useEnroll as ae, useLessonPlayerQuery as oe, useMyEnrollmentsQuery as se } from "../api/technospamApi.js";
10
10
  import "../components/index.js";
11
- import { FOCUS_PARAMS as p } from "../utils/pageContext.js";
12
- import { courseDetailsPath as m, lessonPlayerPath as h } from "../utils/lessonPath.js";
13
- import { InteractiveWorkspace as g } from "../components/lesson/InteractiveWorkspace.js";
14
- import { QuizLesson as _ } from "../components/lesson/QuizLesson.js";
15
- import { useEffect as ne } from "react";
16
- import { ArrowLeft as re, ArrowRight as v, BookOpen as y, CheckCircle2 as b, Clock as ie, FileText as x, Layers as S, Lock as ae, PlayCircle as C, Sparkles as w } from "lucide-react";
17
- import { useSearchParams as T } from "react-router";
18
- import { jsx as E, jsxs as D } from "react/jsx-runtime";
19
- import { Button as O, GlassCard as k, Skeleton as A } from "@burdenoff/fe-libs/ui";
20
- import { Markdown as j } from "@burdenoff/fe-libs/shared/components/Markdown";
11
+ import { offlineScope as ce, readLessonSnapshot as le, saveLessonSnapshot as ue } from "../lib/offlineLessons.js";
12
+ import { useOfflineProgressSync as s, useOnlineStatus as de, usePurgeOtherUsersLessons as fe } from "../hooks/useOfflineLessons.js";
13
+ import { FOCUS_PARAMS as c } from "../utils/pageContext.js";
14
+ import { courseDetailsPath as l, lessonPlayerPath as u } from "../utils/lessonPath.js";
15
+ import { InteractiveWorkspace as d } from "../components/lesson/InteractiveWorkspace.js";
16
+ import { QuizLesson as f } from "../components/lesson/QuizLesson.js";
17
+ import { useEffect as p, useMemo as pe } from "react";
18
+ import { ArrowLeft as me, ArrowRight as he, BookOpen as ge, CheckCircle2 as m, Clock as _e, CloudOff as h, FileText as g, Layers as _, Lock as ve, PlayCircle as v, Sparkles as y } from "lucide-react";
19
+ import { useSearchParams as ye } from "react-router";
20
+ import { jsx as b, jsxs as x } from "react/jsx-runtime";
21
+ import { Button as S, GlassCard as C, Skeleton as w } from "@burdenoff/fe-libs/ui";
22
+ import { Markdown as T } from "@burdenoff/fe-libs/shared/components/Markdown";
21
23
  //#region src/technospam/pages/CoursesLessonPlayerPagePage.tsx
22
- var M = {
23
- VIDEO: C,
24
- ARTICLE: x,
25
- INTERACTIVE: w,
26
- QUIZ: b,
27
- PROJECT: S
24
+ var E = {
25
+ VIDEO: v,
26
+ ARTICLE: g,
27
+ INTERACTIVE: y,
28
+ QUIZ: m,
29
+ PROJECT: _
28
30
  };
29
- function oe(e) {
31
+ function be(e) {
30
32
  return e.kind === "QUIZ" ? (e.quizQuestions?.length ?? 0) > 0 : e.kind === "INTERACTIVE" ? !!e.expectedOutput : !1;
31
33
  }
32
- function N({ lesson: e, onProgress: t }) {
33
- let n = e.content ? /* @__PURE__ */ E(j, {
34
+ function xe(e) {
35
+ let t = new Date(e);
36
+ return Number.isNaN(t.getTime()) ? "" : ` on ${t.toLocaleDateString()}`;
37
+ }
38
+ function D({ children: e }) {
39
+ return /* @__PURE__ */ x("p", {
40
+ className: "flex items-center gap-2 rounded-xl border border-dashed border-border-seam px-4 py-3 text-sm text-text-muted",
41
+ "data-testid": "lesson-needs-connection",
42
+ children: [
43
+ /* @__PURE__ */ b(h, {
44
+ size: 15,
45
+ className: "shrink-0"
46
+ }),
47
+ " ",
48
+ e
49
+ ]
50
+ });
51
+ }
52
+ function Se({ lesson: e, onProgress: t, offline: n }) {
53
+ let r = e.content ? /* @__PURE__ */ b(T, {
34
54
  className: "prose prose-sm max-w-none text-text-secondary dark:prose-invert",
35
55
  children: e.content
36
56
  }) : null;
37
57
  switch (e.kind) {
38
58
  case "VIDEO": {
39
- let t = e.file?.downloadUrl, r = !e.file?.mimeType || e.file.mimeType.startsWith("video/");
40
- return /* @__PURE__ */ D("div", {
59
+ let t = e.file?.downloadUrl, i = !e.file?.mimeType || e.file.mimeType.startsWith("video/");
60
+ return /* @__PURE__ */ x("div", {
41
61
  className: "space-y-4",
42
- children: [t && r ? /* @__PURE__ */ E("video", {
62
+ children: [n ? /* @__PURE__ */ b(D, { children: "The video plays once you are back online. The lesson notes are below." }) : t && i ? /* @__PURE__ */ b("video", {
43
63
  controls: !0,
44
64
  preload: "metadata",
45
65
  src: t,
46
66
  className: "aspect-video w-full rounded-xl bg-bg-sunken",
47
67
  "data-testid": "lesson-video",
48
- children: /* @__PURE__ */ E("track", { kind: "captions" })
49
- }) : /* @__PURE__ */ E("p", {
68
+ children: /* @__PURE__ */ b("track", { kind: "captions" })
69
+ }) : /* @__PURE__ */ b("p", {
50
70
  className: "rounded-xl border border-dashed border-border-seam px-4 py-3 text-sm text-text-muted",
51
71
  children: "The video for this lesson has not been uploaded yet."
52
- }), n]
72
+ }), r]
53
73
  });
54
74
  }
55
- case "QUIZ": return /* @__PURE__ */ D("div", {
75
+ case "QUIZ": return /* @__PURE__ */ x("div", {
56
76
  className: "space-y-4",
57
- children: [n, /* @__PURE__ */ E(_, {
77
+ children: [r, n ? /* @__PURE__ */ b(D, { children: "Quizzes are graded online. Reconnect to take this one." }) : /* @__PURE__ */ b(f, {
58
78
  lesson: e,
59
79
  onPassed: t
60
80
  }, e.id)]
61
81
  });
62
- case "INTERACTIVE": return /* @__PURE__ */ D("div", {
82
+ case "INTERACTIVE": return /* @__PURE__ */ x("div", {
63
83
  className: "grid grid-cols-1 gap-5 xl:grid-cols-2",
64
- children: [/* @__PURE__ */ E("div", {
84
+ children: [/* @__PURE__ */ b("div", {
65
85
  className: "min-w-0",
66
- children: n ?? /* @__PURE__ */ E("p", {
86
+ children: r ?? /* @__PURE__ */ b("p", {
67
87
  className: "text-sm text-text-muted",
68
88
  children: "Edit the code, run it, and compare the output with what the lesson expects."
69
89
  })
70
- }), /* @__PURE__ */ E("div", {
90
+ }), /* @__PURE__ */ b("div", {
71
91
  className: "min-w-0",
72
- children: /* @__PURE__ */ E(g, {
92
+ children: n ? /* @__PURE__ */ b(D, { children: "Code runs in an online sandbox. Reconnect to run this lesson." }) : /* @__PURE__ */ b(d, {
73
93
  lesson: e,
74
94
  onCompleted: t
75
95
  }, e.id)
76
96
  })]
77
97
  });
78
- default: return n ?? /* @__PURE__ */ E("p", {
98
+ default: return r ?? /* @__PURE__ */ b("p", {
79
99
  className: "rounded-xl border border-dashed border-border-seam px-4 py-3 text-sm text-text-muted",
80
100
  children: "No content published for this lesson yet."
81
101
  });
82
102
  }
83
103
  }
84
- function P() {
85
- let [g] = T(), _ = g.get(p.id) ?? "", S = g.get(p.lesson) ?? "", C = t(), { workspaceId: w } = e(), { emit: j } = n(), { data: P, loading: F, error: I, refetch: L } = te(_, S), [R, { loading: z }] = u(), [B, { loading: V }] = d();
86
- ne(() => {
87
- S && j("technospam.lesson.viewed", {
88
- workspaceId: w,
89
- entityId: S,
104
+ function O() {
105
+ let [d] = ye(), f = d.get(c.id) ?? "", _ = d.get(c.lesson) ?? "", v = t(), { workspaceId: y, currentUser: T } = e(), { emit: D } = n(), { data: O, loading: Ce, error: k, refetch: A } = oe(f, _), [we, { loading: j }] = ie(), [Te, { loading: M }] = ae();
106
+ p(() => {
107
+ _ && D("technospam.lesson.viewed", {
108
+ workspaceId: y,
109
+ entityId: _,
90
110
  entityType: "TEC_LESSON"
91
111
  });
92
112
  }, [
93
- S,
94
- w,
95
- j
113
+ _,
114
+ y,
115
+ D
116
+ ]);
117
+ let N = de(), P = ce(y, T?.id);
118
+ fe(T?.id);
119
+ let { pendingLessonIds: Ee, queueCompletion: De } = s(P), F = O?.tecLesson ?? null, I = pe(() => F || N && !k ? null : le(P, f, _), [
120
+ F,
121
+ N,
122
+ k,
123
+ P,
124
+ f,
125
+ _
126
+ ]), L = F ?? I?.lesson ?? null, R = O?.tecCourse ?? I?.course ?? null, { enrollments: z, refetch: B } = se({
127
+ buiCourseId: R?.buiCourseId,
128
+ technospamCourseId: f,
129
+ skip: !f
130
+ }), V = z[0] ?? I?.enrollment ?? null, H = !F && !!I;
131
+ p(() => {
132
+ !N || !H || (A(), B());
133
+ }, [
134
+ N,
135
+ H,
136
+ A,
137
+ B
138
+ ]);
139
+ let U = z[0]?.id, W = z[0]?.progressPct;
140
+ p(() => {
141
+ !O || !F || !P || ue(P, O, U !== void 0 && W !== void 0 ? {
142
+ id: U,
143
+ progressPct: W
144
+ } : null);
145
+ }, [
146
+ O,
147
+ F,
148
+ P,
149
+ U,
150
+ W
96
151
  ]);
97
- let H = P?.tecLesson ?? null, U = P?.tecCourse ?? null, { enrollments: W, refetch: G } = f({
98
- buiCourseId: U?.buiCourseId,
99
- technospamCourseId: _,
100
- skip: !_
101
- }), K = W[0] ?? null, q = [...U?.lessons ?? []].sort((e, t) => e.position - t.position), J = q.findIndex((e) => e.id === S), Y = J > 0 ? q[J - 1] : void 0, X = J >= 0 && J < q.length - 1 ? q[J + 1] : void 0, Z = m(_), se = () => {
102
- L(), G();
103
- }, ce = async () => {
104
- if (!H) return;
105
- let e = (await R({ variables: { lessonId: H.id } })).data?.tecCompleteLesson;
106
- e && (j("technospam.lesson.completed", {
107
- workspaceId: w,
108
- entityId: H.id,
152
+ let G = [...R?.lessons ?? []].sort((e, t) => e.position - t.position), K = G.findIndex((e) => e.id === _), q = K > 0 ? G[K - 1] : void 0, J = K >= 0 && K < G.length - 1 ? G[K + 1] : void 0, Y = l(f), Oe = () => {
153
+ A(), B();
154
+ }, ke = async () => {
155
+ if (!L) return;
156
+ if (!N) {
157
+ if (!De(f, L.id)) {
158
+ o.error("Could not save progress", "This device has no room to keep your progress offline. Reconnect to mark the lesson complete.");
159
+ return;
160
+ }
161
+ o.info("Saved on this device", "Your progress syncs as soon as you are back online."), J && v(u(f, J.id));
162
+ return;
163
+ }
164
+ let e = (await we({ variables: { lessonId: L.id } })).data?.tecCompleteLesson;
165
+ e && (D("technospam.lesson.completed", {
166
+ workspaceId: y,
167
+ entityId: L.id,
109
168
  entityType: "TEC_LESSON"
110
- }), l.success("Lesson completed", `${e.progressPct}% of the course done`), X ? C(h(_, X.id)) : await L());
111
- }, le = async () => {
112
- (await B({ variables: { input: { courseId: _ } } })).data?.tecEnroll && (l.success("Enrolled", `Your progress in "${U?.title ?? "this course"}" is now tracked.`), await L());
169
+ }), o.success("Lesson completed", `${e.progressPct}% of the course done`), J ? v(u(f, J.id)) : await A());
170
+ }, Ae = async () => {
171
+ (await Te({ variables: { input: { courseId: f } } })).data?.tecEnroll && (o.success("Enrolled", `Your progress in "${R?.title ?? "this course"}" is now tracked.`), await A());
113
172
  };
114
- if (!_ || !S) return /* @__PURE__ */ E(i, {
173
+ if (!f || !_) return /* @__PURE__ */ b(i, {
115
174
  title: "Lesson",
116
175
  backPath: "/courses/courses-page",
117
- children: /* @__PURE__ */ E(s, {
176
+ children: /* @__PURE__ */ b(a, {
118
177
  title: "No lesson selected",
119
178
  description: "Open a lesson from a course curriculum."
120
179
  })
121
180
  });
122
- if (F && !H) return /* @__PURE__ */ D(i, {
181
+ if (Ce && !L) return /* @__PURE__ */ x(i, {
123
182
  title: "Lesson",
124
- backPath: Z,
125
- children: [/* @__PURE__ */ E(A, { className: "mb-4 h-10 w-1/2 rounded-xl" }), /* @__PURE__ */ E(A, { className: "h-80 w-full rounded-xl" })]
183
+ backPath: Y,
184
+ children: [/* @__PURE__ */ b(w, { className: "mb-4 h-10 w-1/2 rounded-xl" }), /* @__PURE__ */ b(w, { className: "h-80 w-full rounded-xl" })]
126
185
  });
127
- if (I || !H || !U || H.courseId !== U.id) return /* @__PURE__ */ E(i, {
186
+ if (k && !I || !L || !R || L.courseId !== R.id) return /* @__PURE__ */ b(i, {
128
187
  title: "Lesson",
129
- backPath: Z,
130
- children: I ? /* @__PURE__ */ E(c, {
188
+ backPath: Y,
189
+ children: k ? /* @__PURE__ */ b(re, {
131
190
  title: "Couldn't load lesson",
132
- error: I
133
- }) : /* @__PURE__ */ E(s, {
191
+ error: k
192
+ }) : /* @__PURE__ */ b(a, {
134
193
  title: "Lesson not found",
135
194
  description: "This lesson may have been removed."
136
195
  })
137
196
  });
138
- let ue = M[H.kind] ?? x, Q = !!H.completedByMe, $ = oe(H);
139
- return /* @__PURE__ */ E(i, {
140
- backPath: Z,
141
- title: H.title,
142
- description: `${U.title} · Lesson ${J + 1} of ${q.length}`,
143
- actions: Q ? /* @__PURE__ */ D("span", {
197
+ let je = E[L.kind] ?? g, X = Ee.includes(L.id), Z = !!L.completedByMe || X, Q = H, $ = be(L);
198
+ return /* @__PURE__ */ b(i, {
199
+ backPath: Y,
200
+ title: L.title,
201
+ description: `${R.title} · Lesson ${K + 1} of ${G.length}`,
202
+ actions: Z ? /* @__PURE__ */ x("span", {
144
203
  className: "inline-flex items-center gap-1.5 rounded-full border border-status-success-border bg-status-success-bg-subtle px-3 py-1.5 text-sm font-medium text-status-success-text",
145
- children: [/* @__PURE__ */ E(b, { size: 15 }), " Completed"]
204
+ children: [
205
+ /* @__PURE__ */ b(m, { size: 15 }),
206
+ " ",
207
+ X && !L.completedByMe ? "Completed · syncs when online" : "Completed"
208
+ ]
146
209
  }) : null,
147
- children: /* @__PURE__ */ D("div", {
210
+ children: /* @__PURE__ */ x("div", {
148
211
  className: "grid grid-cols-1 gap-5 lg:grid-cols-[minmax(0,1fr)_280px]",
149
- children: [/* @__PURE__ */ D("div", {
212
+ children: [/* @__PURE__ */ x("div", {
150
213
  className: "min-w-0 space-y-4",
151
214
  children: [
152
- /* @__PURE__ */ D("div", {
215
+ /* @__PURE__ */ x("div", {
153
216
  className: "flex flex-wrap items-center gap-2 text-xs text-text-muted",
154
217
  children: [
155
- /* @__PURE__ */ D("span", {
218
+ /* @__PURE__ */ x("span", {
156
219
  className: "inline-flex items-center gap-1 rounded-full bg-accent-primary/6 px-2 py-0.5 font-medium text-accent-primary",
157
220
  children: [
158
- /* @__PURE__ */ E(ue, { size: 12 }),
221
+ /* @__PURE__ */ b(je, { size: 12 }),
159
222
  " ",
160
- a(H.kind)
223
+ te(L.kind)
161
224
  ]
162
225
  }),
163
- /* @__PURE__ */ D("span", {
226
+ /* @__PURE__ */ x("span", {
164
227
  className: "inline-flex items-center gap-1",
165
228
  children: [
166
- /* @__PURE__ */ E(ie, { size: 12 }),
229
+ /* @__PURE__ */ b(_e, { size: 12 }),
167
230
  " ",
168
- ee(H.estimatedMinutes)
231
+ ee(L.estimatedMinutes)
169
232
  ]
170
233
  }),
171
- H.isPreview ? /* @__PURE__ */ E("span", {
234
+ L.isPreview ? /* @__PURE__ */ b("span", {
172
235
  className: "rounded-full border border-accent-primary/30 px-2 py-0.5",
173
236
  children: "Free preview"
174
237
  }) : null
175
238
  ]
176
239
  }),
177
- K ? null : /* @__PURE__ */ D(k, {
240
+ Q && I ? /* @__PURE__ */ x("div", {
241
+ className: "flex items-center gap-2 rounded-xl border border-status-warning-border bg-status-warning-bg-subtle px-4 py-2.5 text-sm text-status-warning-text",
242
+ "data-testid": "lesson-offline-banner",
243
+ role: "status",
244
+ children: [
245
+ /* @__PURE__ */ b(h, {
246
+ size: 15,
247
+ className: "shrink-0"
248
+ }),
249
+ N ? "Couldn't reach TechnoSpam. Showing the copy saved on this device" : "You are offline. Showing the copy saved on this device",
250
+ xe(I.savedAt),
251
+ "."
252
+ ]
253
+ }) : null,
254
+ !V && !Q ? /* @__PURE__ */ x(C, {
178
255
  className: "flex flex-col items-start gap-3 p-4 sm:flex-row sm:items-center sm:justify-between",
179
- children: [/* @__PURE__ */ E("p", {
256
+ children: [/* @__PURE__ */ b("p", {
180
257
  className: "text-sm text-text-secondary",
181
258
  children: "Enroll in this course to save your progress and earn a certificate."
182
- }), /* @__PURE__ */ D(O, {
259
+ }), /* @__PURE__ */ x(S, {
183
260
  size: "sm",
184
- onClick: () => void le(),
185
- disabled: V,
261
+ onClick: () => void Ae(),
262
+ disabled: M,
186
263
  children: [
187
- /* @__PURE__ */ E(y, { size: 14 }),
264
+ /* @__PURE__ */ b(ge, { size: 14 }),
188
265
  " ",
189
- V ? "Enrolling…" : "Enroll"
266
+ M ? "Enrolling…" : "Enroll"
190
267
  ]
191
268
  })]
192
- }),
193
- /* @__PURE__ */ E(k, {
269
+ }) : null,
270
+ /* @__PURE__ */ b(C, {
194
271
  className: "p-5",
195
272
  "data-testid": "lesson-player-body",
196
- children: H.isLocked ? /* @__PURE__ */ E(o, { description: "This lesson is part of a paid TechnoSpam plan. Its content unlocks once your workspace upgrades." }) : /* @__PURE__ */ E(N, {
197
- lesson: H,
198
- onProgress: se
273
+ children: L.isLocked ? /* @__PURE__ */ b(ne, { description: "This lesson is part of a paid TechnoSpam plan. Its content unlocks once your workspace upgrades." }) : /* @__PURE__ */ b(Se, {
274
+ lesson: L,
275
+ onProgress: Oe,
276
+ offline: !N || Q
199
277
  })
200
278
  }),
201
- /* @__PURE__ */ D("div", {
279
+ /* @__PURE__ */ x("div", {
202
280
  className: "flex flex-wrap items-center justify-between gap-2",
203
- children: [/* @__PURE__ */ D(O, {
281
+ children: [/* @__PURE__ */ x(S, {
204
282
  variant: "outline",
205
- disabled: !Y,
206
- onClick: () => Y && C(h(_, Y.id)),
207
- children: [/* @__PURE__ */ E(re, { size: 15 }), " Previous"]
208
- }), /* @__PURE__ */ D("div", {
283
+ disabled: !q,
284
+ onClick: () => q && v(u(f, q.id)),
285
+ children: [/* @__PURE__ */ b(me, { size: 15 }), " Previous"]
286
+ }), /* @__PURE__ */ x("div", {
209
287
  className: "flex flex-wrap items-center gap-2",
210
288
  children: [
211
- K && !Q && !$ && !H.isLocked ? /* @__PURE__ */ D(O, {
212
- onClick: () => void ce(),
213
- disabled: z,
289
+ V && !Z && !$ && !L.isLocked ? /* @__PURE__ */ x(S, {
290
+ onClick: () => void ke(),
291
+ disabled: j,
214
292
  children: [
215
- /* @__PURE__ */ E(b, { size: 15 }),
293
+ /* @__PURE__ */ b(m, { size: 15 }),
216
294
  " ",
217
- z ? "Saving…" : "Mark complete"
295
+ j ? "Saving…" : "Mark complete"
218
296
  ]
219
297
  }) : null,
220
- K && !Q && $ && !H.isLocked ? /* @__PURE__ */ E("span", {
298
+ V && !Z && $ && !L.isLocked ? /* @__PURE__ */ b("span", {
221
299
  className: "text-xs text-text-muted",
222
- children: H.kind === "QUIZ" ? "Pass the quiz to complete this lesson." : "Run code that prints the expected output to complete this lesson."
300
+ children: L.kind === "QUIZ" ? "Pass the quiz to complete this lesson." : "Run code that prints the expected output to complete this lesson."
223
301
  }) : null,
224
- /* @__PURE__ */ D(O, {
225
- variant: Q || !K ? "default" : "outline",
226
- disabled: !X,
227
- onClick: () => X && C(h(_, X.id)),
228
- children: ["Next ", /* @__PURE__ */ E(v, { size: 15 })]
302
+ /* @__PURE__ */ x(S, {
303
+ variant: Z || !V ? "default" : "outline",
304
+ disabled: !J,
305
+ onClick: () => J && v(u(f, J.id)),
306
+ children: ["Next ", /* @__PURE__ */ b(he, { size: 15 })]
229
307
  })
230
308
  ]
231
309
  })]
232
310
  })
233
311
  ]
234
- }), /* @__PURE__ */ D(k, {
312
+ }), /* @__PURE__ */ x(C, {
235
313
  className: "h-fit p-4",
236
314
  children: [
237
- /* @__PURE__ */ D("div", {
315
+ /* @__PURE__ */ x("div", {
238
316
  className: "mb-2 flex items-center justify-between",
239
- children: [/* @__PURE__ */ E("span", {
317
+ children: [/* @__PURE__ */ b("span", {
240
318
  className: "text-xs font-semibold uppercase tracking-wide text-text-muted",
241
319
  children: "Curriculum"
242
- }), K ? /* @__PURE__ */ D("span", {
320
+ }), V ? /* @__PURE__ */ x("span", {
243
321
  className: "text-xs text-text-secondary",
244
322
  "data-testid": "lesson-player-progress",
245
- children: [K.progressPct, "% complete"]
323
+ children: [V.progressPct, "% complete"]
246
324
  }) : null]
247
325
  }),
248
- K ? /* @__PURE__ */ E("div", {
326
+ V ? /* @__PURE__ */ b("div", {
249
327
  className: "mb-3 h-1.5 w-full overflow-hidden rounded-full bg-bg-sunken",
250
- children: /* @__PURE__ */ E("div", {
328
+ children: /* @__PURE__ */ b("div", {
251
329
  className: "h-full rounded-full bg-accent-primary transition-all duration-500",
252
- style: { width: `${K.progressPct}%` }
330
+ style: { width: `${V.progressPct}%` }
253
331
  })
254
332
  }) : null,
255
- /* @__PURE__ */ E("ol", {
333
+ /* @__PURE__ */ b("ol", {
256
334
  className: "space-y-1",
257
- children: q.map((e, t) => {
258
- let n = M[e.kind] ?? x, i = e.id === H.id;
259
- return /* @__PURE__ */ E("li", { children: /* @__PURE__ */ D("button", {
335
+ children: G.map((e, t) => {
336
+ let n = E[e.kind] ?? g, i = e.id === L.id;
337
+ return /* @__PURE__ */ b("li", { children: /* @__PURE__ */ x("button", {
260
338
  type: "button",
261
339
  "aria-current": i ? "page" : void 0,
262
- onClick: () => C(h(_, e.id)),
340
+ onClick: () => v(u(f, e.id)),
263
341
  className: r("flex w-full items-center gap-2 rounded-lg px-2 py-1.5 text-left text-sm transition-colors hover:bg-bg-sunken", i && "bg-accent-primary/6 font-medium text-accent-primary"),
264
342
  children: [
265
- /* @__PURE__ */ E("span", {
343
+ /* @__PURE__ */ b("span", {
266
344
  className: "w-5 shrink-0 text-center text-xs tabular-nums text-text-muted",
267
345
  children: t + 1
268
346
  }),
269
- e.completedByMe ? /* @__PURE__ */ E(b, {
347
+ e.completedByMe ? /* @__PURE__ */ b(m, {
270
348
  size: 14,
271
349
  className: "shrink-0 text-status-success-text"
272
- }) : e.isLocked ? /* @__PURE__ */ E(ae, {
350
+ }) : e.isLocked ? /* @__PURE__ */ b(ve, {
273
351
  size: 14,
274
352
  className: "shrink-0 text-text-muted"
275
- }) : /* @__PURE__ */ E(n, {
353
+ }) : /* @__PURE__ */ b(n, {
276
354
  size: 14,
277
355
  className: "shrink-0 text-text-muted"
278
356
  }),
279
- /* @__PURE__ */ E("span", {
357
+ /* @__PURE__ */ b("span", {
280
358
  className: "min-w-0 flex-1 truncate",
281
359
  children: e.title
282
360
  })
@@ -290,6 +368,6 @@ function P() {
290
368
  });
291
369
  }
292
370
  //#endregion
293
- export { P as CoursesLessonPlayerPagePage };
371
+ export { O as CoursesLessonPlayerPagePage };
294
372
 
295
373
  //# sourceMappingURL=CoursesLessonPlayerPagePage.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"CoursesLessonPlayerPagePage.js","names":[],"sources":["../../../src/technospam/pages/CoursesLessonPlayerPagePage.tsx"],"sourcesContent":["import { useEffect } from 'react';\nimport { useSearchParams } from 'react-router';\nimport { Markdown } from '@burdenoff/fe-libs/shared/components/Markdown';\nimport {\n ArrowLeft,\n ArrowRight,\n BookOpen,\n CheckCircle2,\n Clock,\n FileText,\n Layers,\n Lock,\n PlayCircle,\n Sparkles,\n type LucideIcon,\n} from 'lucide-react';\nimport { Button, GlassCard, Skeleton } from '@burdenoff/fe-libs/ui';\nimport { EmptyView, ErrorView, PageLayout, UpgradeGate } from '../components';\nimport { InteractiveWorkspace } from '../components/lesson/InteractiveWorkspace';\nimport { QuizLesson } from '../components/lesson/QuizLesson';\nimport { useTechnoSpamNavigate, useTechnoSpam } from '../providers/TechnoSpamProvider';\nimport {\n useCompleteLesson,\n useEnroll,\n useLessonPlayerQuery,\n useMyEnrollmentsQuery,\n type TecLesson,\n type TecLessonKind,\n} from '../api/technospamApi';\nimport { formatDuration, humanizeEnum } from '../utils/format';\nimport { notify } from '../lib/toast';\nimport { cn } from '../utils/cn';\nimport { useTechnoSpamAnalytics } from '../hooks/useTechnoSpamAnalytics';\nimport { FOCUS_PARAMS } from '../utils/pageContext';\nimport { courseDetailsPath, lessonPlayerPath } from '../utils/lessonPath';\n\nconst KIND_ICON: Record<TecLessonKind, LucideIcon> = {\n VIDEO: PlayCircle,\n ARTICLE: FileText,\n INTERACTIVE: Sparkles,\n QUIZ: CheckCircle2,\n PROJECT: Layers,\n};\n\n/** Lessons whose completion is earned (a passed quiz, a matching run) rather than self-reported. */\nfunction completesByCheck(lesson: TecLesson): boolean {\n if (lesson.kind === 'QUIZ') return (lesson.quizQuestions?.length ?? 0) > 0;\n if (lesson.kind === 'INTERACTIVE') return !!lesson.expectedOutput;\n return false;\n}\n\nfunction LessonBody({ lesson, onProgress }: { lesson: TecLesson; onProgress: () => void }) {\n const notes = lesson.content ? (\n <Markdown className=\"prose prose-sm max-w-none text-text-secondary dark:prose-invert\">\n {lesson.content}\n </Markdown>\n ) : null;\n\n switch (lesson.kind) {\n case 'VIDEO': {\n const src = lesson.file?.downloadUrl;\n const isVideo = !lesson.file?.mimeType || lesson.file.mimeType.startsWith('video/');\n return (\n <div className=\"space-y-4\">\n {src && isVideo ? (\n <video\n controls\n preload=\"metadata\"\n src={src}\n className=\"aspect-video w-full rounded-xl bg-bg-sunken\"\n data-testid=\"lesson-video\"\n >\n <track kind=\"captions\" />\n </video>\n ) : (\n <p className=\"rounded-xl border border-dashed border-border-seam px-4 py-3 text-sm text-text-muted\">\n The video for this lesson has not been uploaded yet.\n </p>\n )}\n {notes}\n </div>\n );\n }\n case 'QUIZ':\n return (\n <div className=\"space-y-4\">\n {notes}\n <QuizLesson key={lesson.id} lesson={lesson} onPassed={onProgress} />\n </div>\n );\n case 'INTERACTIVE':\n return (\n <div className=\"grid grid-cols-1 gap-5 xl:grid-cols-2\">\n <div className=\"min-w-0\">\n {notes ?? (\n <p className=\"text-sm text-text-muted\">\n Edit the code, run it, and compare the output with what the lesson expects.\n </p>\n )}\n </div>\n <div className=\"min-w-0\">\n <InteractiveWorkspace key={lesson.id} lesson={lesson} onCompleted={onProgress} />\n </div>\n </div>\n );\n case 'PROJECT':\n case 'ARTICLE':\n default:\n return (\n notes ?? (\n <p className=\"rounded-xl border border-dashed border-border-seam px-4 py-3 text-sm text-text-muted\">\n No content published for this lesson yet.\n </p>\n )\n );\n }\n}\n\n/**\n * Lesson player (BOFF-4107): renders one lesson of a course by kind — article\n * and project briefs as markdown, video through the federated File, quizzes\n * graded server-side, and a runnable sandbox workspace for INTERACTIVE\n * lessons — with the curriculum alongside and per-lesson progress tracking.\n * URL: `/courses/lesson-player-page?id=<courseId>&lessonId=<lessonId>`.\n */\nexport function CoursesLessonPlayerPagePage() {\n const [params] = useSearchParams();\n const courseId = params.get(FOCUS_PARAMS.id) ?? '';\n const lessonId = params.get(FOCUS_PARAMS.lesson) ?? '';\n const navigate = useTechnoSpamNavigate();\n const { workspaceId } = useTechnoSpam();\n const { emit } = useTechnoSpamAnalytics();\n const { data, loading, error, refetch } = useLessonPlayerQuery(courseId, lessonId);\n const [completeLesson, { loading: completing }] = useCompleteLesson();\n const [enroll, { loading: enrolling }] = useEnroll();\n\n useEffect(() => {\n if (lessonId) {\n emit('technospam.lesson.viewed', {\n workspaceId,\n entityId: lessonId,\n entityType: 'TEC_LESSON',\n });\n }\n }, [lessonId, workspaceId, emit]);\n\n const lesson = data?.tecLesson ?? null;\n const course = data?.tecCourse ?? null;\n // BOFF-10481: the learner's own enrollment in this course, from the\n // canonical self-scoped Buildmyiq field (narrowed by the course's canonical\n // id on the server and its Technospam id on the client).\n const { enrollments: myEnrollments, refetch: refetchEnrollment } = useMyEnrollmentsQuery({\n buiCourseId: course?.buiCourseId,\n technospamCourseId: courseId,\n skip: !courseId,\n });\n const enrollment = myEnrollments[0] ?? null;\n const lessons = [...(course?.lessons ?? [])].sort((a, b) => a.position - b.position);\n const index = lessons.findIndex((l) => l.id === lessonId);\n const prev = index > 0 ? lessons[index - 1] : undefined;\n const next = index >= 0 && index < lessons.length - 1 ? lessons[index + 1] : undefined;\n const backPath = courseDetailsPath(courseId);\n\n const refreshProgress = (): void => {\n void refetch();\n void refetchEnrollment();\n };\n\n const handleComplete = async (): Promise<void> => {\n if (!lesson) return;\n const res = await completeLesson({ variables: { lessonId: lesson.id } });\n const updated = res.data?.tecCompleteLesson;\n if (!updated) return;\n emit('technospam.lesson.completed', {\n workspaceId,\n entityId: lesson.id,\n entityType: 'TEC_LESSON',\n });\n notify.success('Lesson completed', `${updated.progressPct}% of the course done`);\n if (next) navigate(lessonPlayerPath(courseId, next.id));\n else await refetch();\n };\n\n const handleEnroll = async (): Promise<void> => {\n const res = await enroll({ variables: { input: { courseId } } });\n if (res.data?.tecEnroll) {\n notify.success(\n 'Enrolled',\n `Your progress in \"${course?.title ?? 'this course'}\" is now tracked.`\n );\n await refetch();\n }\n };\n\n if (!courseId || !lessonId) {\n return (\n <PageLayout title=\"Lesson\" backPath=\"/courses/courses-page\">\n <EmptyView\n title=\"No lesson selected\"\n description=\"Open a lesson from a course curriculum.\"\n />\n </PageLayout>\n );\n }\n\n if (loading && !lesson) {\n return (\n <PageLayout title=\"Lesson\" backPath={backPath}>\n <Skeleton className=\"mb-4 h-10 w-1/2 rounded-xl\" />\n <Skeleton className=\"h-80 w-full rounded-xl\" />\n </PageLayout>\n );\n }\n\n if (error || !lesson || !course || lesson.courseId !== course.id) {\n return (\n <PageLayout title=\"Lesson\" backPath={backPath}>\n {error ? (\n <ErrorView title=\"Couldn't load lesson\" error={error} />\n ) : (\n <EmptyView title=\"Lesson not found\" description=\"This lesson may have been removed.\" />\n )}\n </PageLayout>\n );\n }\n\n const Icon = KIND_ICON[lesson.kind] ?? FileText;\n const done = !!lesson.completedByMe;\n const byCheck = completesByCheck(lesson);\n\n return (\n <PageLayout\n backPath={backPath}\n title={lesson.title}\n description={`${course.title} · Lesson ${index + 1} of ${lessons.length}`}\n actions={\n done ? (\n <span className=\"inline-flex items-center gap-1.5 rounded-full border border-status-success-border bg-status-success-bg-subtle px-3 py-1.5 text-sm font-medium text-status-success-text\">\n <CheckCircle2 size={15} /> Completed\n </span>\n ) : null\n }\n >\n <div className=\"grid grid-cols-1 gap-5 lg:grid-cols-[minmax(0,1fr)_280px]\">\n <div className=\"min-w-0 space-y-4\">\n <div className=\"flex flex-wrap items-center gap-2 text-xs text-text-muted\">\n <span className=\"inline-flex items-center gap-1 rounded-full bg-accent-primary/6 px-2 py-0.5 font-medium text-accent-primary\">\n <Icon size={12} /> {humanizeEnum(lesson.kind)}\n </span>\n <span className=\"inline-flex items-center gap-1\">\n <Clock size={12} /> {formatDuration(lesson.estimatedMinutes)}\n </span>\n {lesson.isPreview ? (\n <span className=\"rounded-full border border-accent-primary/30 px-2 py-0.5\">\n Free preview\n </span>\n ) : null}\n </div>\n\n {!enrollment ? (\n <GlassCard className=\"flex flex-col items-start gap-3 p-4 sm:flex-row sm:items-center sm:justify-between\">\n <p className=\"text-sm text-text-secondary\">\n Enroll in this course to save your progress and earn a certificate.\n </p>\n <Button size=\"sm\" onClick={() => void handleEnroll()} disabled={enrolling}>\n <BookOpen size={14} /> {enrolling ? 'Enrolling…' : 'Enroll'}\n </Button>\n </GlassCard>\n ) : null}\n\n <GlassCard className=\"p-5\" data-testid=\"lesson-player-body\">\n {lesson.isLocked ? (\n <UpgradeGate description=\"This lesson is part of a paid TechnoSpam plan. Its content unlocks once your workspace upgrades.\" />\n ) : (\n <LessonBody lesson={lesson} onProgress={refreshProgress} />\n )}\n </GlassCard>\n\n <div className=\"flex flex-wrap items-center justify-between gap-2\">\n <Button\n variant=\"outline\"\n disabled={!prev}\n onClick={() => prev && navigate(lessonPlayerPath(courseId, prev.id))}\n >\n <ArrowLeft size={15} /> Previous\n </Button>\n <div className=\"flex flex-wrap items-center gap-2\">\n {enrollment && !done && !byCheck && !lesson.isLocked ? (\n <Button onClick={() => void handleComplete()} disabled={completing}>\n <CheckCircle2 size={15} /> {completing ? 'Saving…' : 'Mark complete'}\n </Button>\n ) : null}\n {enrollment && !done && byCheck && !lesson.isLocked ? (\n <span className=\"text-xs text-text-muted\">\n {lesson.kind === 'QUIZ'\n ? 'Pass the quiz to complete this lesson.'\n : 'Run code that prints the expected output to complete this lesson.'}\n </span>\n ) : null}\n <Button\n variant={done || !enrollment ? 'default' : 'outline'}\n disabled={!next}\n onClick={() => next && navigate(lessonPlayerPath(courseId, next.id))}\n >\n Next <ArrowRight size={15} />\n </Button>\n </div>\n </div>\n </div>\n\n <GlassCard className=\"h-fit p-4\">\n <div className=\"mb-2 flex items-center justify-between\">\n <span className=\"text-xs font-semibold uppercase tracking-wide text-text-muted\">\n Curriculum\n </span>\n {enrollment ? (\n <span className=\"text-xs text-text-secondary\" data-testid=\"lesson-player-progress\">\n {enrollment.progressPct}% complete\n </span>\n ) : null}\n </div>\n {enrollment ? (\n <div className=\"mb-3 h-1.5 w-full overflow-hidden rounded-full bg-bg-sunken\">\n <div\n className=\"h-full rounded-full bg-accent-primary transition-all duration-500\"\n style={{ width: `${enrollment.progressPct}%` }}\n />\n </div>\n ) : null}\n <ol className=\"space-y-1\">\n {lessons.map((l, i) => {\n const LIcon = KIND_ICON[l.kind] ?? FileText;\n const current = l.id === lesson.id;\n return (\n <li key={l.id}>\n <button\n type=\"button\"\n aria-current={current ? 'page' : undefined}\n onClick={() => navigate(lessonPlayerPath(courseId, l.id))}\n className={cn(\n 'flex w-full items-center gap-2 rounded-lg px-2 py-1.5 text-left text-sm transition-colors hover:bg-bg-sunken',\n current && 'bg-accent-primary/6 font-medium text-accent-primary'\n )}\n >\n <span className=\"w-5 shrink-0 text-center text-xs tabular-nums text-text-muted\">\n {i + 1}\n </span>\n {l.completedByMe ? (\n <CheckCircle2 size={14} className=\"shrink-0 text-status-success-text\" />\n ) : l.isLocked ? (\n <Lock size={14} className=\"shrink-0 text-text-muted\" />\n ) : (\n <LIcon size={14} className=\"shrink-0 text-text-muted\" />\n )}\n <span className=\"min-w-0 flex-1 truncate\">{l.title}</span>\n </button>\n </li>\n );\n })}\n </ol>\n </GlassCard>\n </div>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"CoursesLessonPlayerPagePage.js","names":[],"sources":["../../../src/technospam/pages/CoursesLessonPlayerPagePage.tsx"],"sourcesContent":["import { useEffect, useMemo } 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 CloudOff,\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';\nimport {\n useOfflineProgressSync,\n useOnlineStatus,\n usePurgeOtherUsersLessons,\n} from '../hooks/useOfflineLessons';\nimport { offlineScope, readLessonSnapshot, saveLessonSnapshot } from '../lib/offlineLessons';\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\n/** \" on <date>\" for a snapshot's save time, or nothing when the stored value is not a valid date. */\nfunction savedOnLabel(savedAt: string): string {\n const saved = new Date(savedAt);\n return Number.isNaN(saved.getTime()) ? '' : ` on ${saved.toLocaleDateString()}`;\n}\n\n/** Shown in place of the parts of a lesson that need the network when it is read offline. */\nfunction NeedsConnection({ children }: { children: string }) {\n return (\n <p\n className=\"flex items-center gap-2 rounded-xl border border-dashed border-border-seam px-4 py-3 text-sm text-text-muted\"\n data-testid=\"lesson-needs-connection\"\n >\n <CloudOff size={15} className=\"shrink-0\" /> {children}\n </p>\n );\n}\n\nfunction LessonBody({\n lesson,\n onProgress,\n offline,\n}: {\n lesson: TecLesson;\n onProgress: () => void;\n /** Rendering a saved copy without a connection: network-only parts are replaced by a notice. */\n offline: boolean;\n}) {\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 {offline ? (\n <NeedsConnection>\n The video plays once you are back online. The lesson notes are below.\n </NeedsConnection>\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 {offline ? (\n <NeedsConnection>\n Quizzes are graded online. Reconnect to take this one.\n </NeedsConnection>\n ) : (\n <QuizLesson key={lesson.id} lesson={lesson} onPassed={onProgress} />\n )}\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 {offline ? (\n <NeedsConnection>\n Code runs in an online sandbox. Reconnect to run this lesson.\n </NeedsConnection>\n ) : (\n <InteractiveWorkspace key={lesson.id} lesson={lesson} onCompleted={onProgress} />\n )}\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, currentUser } = 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 online = useOnlineStatus();\n // Offline data belongs to this learner in this workspace (BOFF-4418).\n const scope = offlineScope(workspaceId, currentUser?.id);\n usePurgeOtherUsersLessons(currentUser?.id);\n const { pendingLessonIds, queueCompletion } = useOfflineProgressSync(scope);\n // BOFF-4418: with no live lesson (offline, or the request failed) fall back\n // to the copy saved on this device the last time the lesson was opened.\n const liveLesson = data?.tecLesson ?? null;\n const offlineCopy = useMemo(\n () => (liveLesson || (online && !error) ? null : readLessonSnapshot(scope, courseId, lessonId)),\n [liveLesson, online, error, scope, courseId, lessonId]\n );\n const lesson = liveLesson ?? offlineCopy?.lesson ?? null;\n const course = data?.tecCourse ?? offlineCopy?.course ?? 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] ?? offlineCopy?.enrollment ?? null;\n // Back online while showing the saved copy: load the live lesson and\n // enrollment again so video, quizzes and code runs work without leaving the page.\n const showingSavedCopy = !liveLesson && !!offlineCopy;\n useEffect(() => {\n if (!online || !showingSavedCopy) return;\n void refetch();\n void refetchEnrollment();\n }, [online, showingSavedCopy, refetch, refetchEnrollment]);\n\n // Keep every lesson the learner opens readable offline.\n // Depends on the enrollment's id and progress, not the array identity, so a\n // re-render never rewrites storage.\n const liveEnrollmentId = myEnrollments[0]?.id;\n const liveProgressPct = myEnrollments[0]?.progressPct;\n useEffect(() => {\n if (!data || !liveLesson || !scope) return;\n saveLessonSnapshot(\n scope,\n data,\n liveEnrollmentId !== undefined && liveProgressPct !== undefined\n ? { id: liveEnrollmentId, progressPct: liveProgressPct }\n : null\n );\n }, [data, liveLesson, scope, liveEnrollmentId, liveProgressPct]);\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 if (!online) {\n if (!queueCompletion(courseId, lesson.id)) {\n notify.error(\n 'Could not save progress',\n 'This device has no room to keep your progress offline. Reconnect to mark the lesson complete.'\n );\n return;\n }\n notify.info('Saved on this device', 'Your progress syncs as soon as you are back online.');\n if (next) navigate(lessonPlayerPath(courseId, next.id));\n return;\n }\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 && !offlineCopy) || !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 pendingSync = pendingLessonIds.includes(lesson.id);\n const done = !!lesson.completedByMe || pendingSync;\n const readingOffline = showingSavedCopy;\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} />{' '}\n {pendingSync && !lesson.completedByMe ? 'Completed · syncs when online' : '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 {readingOffline && offlineCopy ? (\n <div\n className=\"flex items-center gap-2 rounded-xl border border-status-warning-border bg-status-warning-bg-subtle px-4 py-2.5 text-sm text-status-warning-text\"\n data-testid=\"lesson-offline-banner\"\n role=\"status\"\n >\n <CloudOff size={15} className=\"shrink-0\" />\n {online\n ? \"Couldn't reach TechnoSpam. Showing the copy saved on this device\"\n : 'You are offline. Showing the copy saved on this device'}\n {savedOnLabel(offlineCopy.savedAt)}.\n </div>\n ) : null}\n\n {!enrollment && !readingOffline ? (\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\n lesson={lesson}\n onProgress={refreshProgress}\n offline={!online || readingOffline}\n />\n )}\n </GlassCard>\n\n <div className=\"flex flex-wrap items-center justify-between gap-2\">\n <Button\n variant=\"outline\"\n disabled={!prev}\n onClick={() => prev && navigate(lessonPlayerPath(courseId, prev.id))}\n >\n <ArrowLeft size={15} /> Previous\n </Button>\n <div className=\"flex flex-wrap items-center gap-2\">\n {enrollment && !done && !byCheck && !lesson.isLocked ? (\n <Button onClick={() => void handleComplete()} disabled={completing}>\n <CheckCircle2 size={15} /> {completing ? 'Saving…' : 'Mark complete'}\n </Button>\n ) : null}\n {enrollment && !done && byCheck && !lesson.isLocked ? (\n <span className=\"text-xs text-text-muted\">\n {lesson.kind === 'QUIZ'\n ? 'Pass the quiz to complete this lesson.'\n : 'Run code that prints the expected output to complete this lesson.'}\n </span>\n ) : null}\n <Button\n variant={done || !enrollment ? 'default' : 'outline'}\n disabled={!next}\n onClick={() => next && navigate(lessonPlayerPath(courseId, next.id))}\n >\n Next <ArrowRight size={15} />\n </Button>\n </div>\n </div>\n </div>\n\n <GlassCard className=\"h-fit p-4\">\n <div className=\"mb-2 flex items-center justify-between\">\n <span className=\"text-xs font-semibold uppercase tracking-wide text-text-muted\">\n Curriculum\n </span>\n {enrollment ? (\n <span className=\"text-xs text-text-secondary\" data-testid=\"lesson-player-progress\">\n {enrollment.progressPct}% complete\n </span>\n ) : null}\n </div>\n {enrollment ? (\n <div className=\"mb-3 h-1.5 w-full overflow-hidden rounded-full bg-bg-sunken\">\n <div\n className=\"h-full rounded-full bg-accent-primary transition-all duration-500\"\n style={{ width: `${enrollment.progressPct}%` }}\n />\n </div>\n ) : null}\n <ol className=\"space-y-1\">\n {lessons.map((l, i) => {\n const LIcon = KIND_ICON[l.kind] ?? FileText;\n const current = l.id === lesson.id;\n return (\n <li key={l.id}>\n <button\n type=\"button\"\n aria-current={current ? 'page' : undefined}\n onClick={() => navigate(lessonPlayerPath(courseId, l.id))}\n className={cn(\n 'flex w-full items-center gap-2 rounded-lg px-2 py-1.5 text-left text-sm transition-colors hover:bg-bg-sunken',\n current && 'bg-accent-primary/6 font-medium text-accent-primary'\n )}\n >\n <span className=\"w-5 shrink-0 text-center text-xs tabular-nums text-text-muted\">\n {i + 1}\n </span>\n {l.completedByMe ? (\n <CheckCircle2 size={14} className=\"shrink-0 text-status-success-text\" />\n ) : l.isLocked ? (\n <Lock size={14} className=\"shrink-0 text-text-muted\" />\n ) : (\n <LIcon size={14} className=\"shrink-0 text-text-muted\" />\n )}\n <span className=\"min-w-0 flex-1 truncate\">{l.title}</span>\n </button>\n </li>\n );\n })}\n </ol>\n </GlassCard>\n </div>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"CoursesMyLearningPagePage.d.ts","sourceRoot":"","sources":["../../../src/technospam/pages/CoursesMyLearningPagePage.tsx"],"names":[],"mappings":"AAgGA,wBAAgB,yBAAyB,gCAoJxC"}
1
+ {"version":3,"file":"CoursesMyLearningPagePage.d.ts","sourceRoot":"","sources":["../../../src/technospam/pages/CoursesMyLearningPagePage.tsx"],"names":[],"mappings":"AAiGA,wBAAgB,yBAAyB,gCAqJxC"}