@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.
- package/dist/index.js +5 -3
- package/dist/technospam/TechnoSpamRoutes.d.ts.map +1 -1
- package/dist/technospam/TechnoSpamRoutes.js +91 -90
- package/dist/technospam/TechnoSpamRoutes.js.map +1 -1
- package/dist/technospam/api/technospamApi.d.ts +21 -7
- package/dist/technospam/api/technospamApi.d.ts.map +1 -1
- package/dist/technospam/api/technospamApi.js +317 -284
- package/dist/technospam/api/technospamApi.js.map +1 -1
- package/dist/technospam/components/CertificateVerifyLink.d.ts +11 -0
- package/dist/technospam/components/CertificateVerifyLink.d.ts.map +1 -0
- package/dist/technospam/components/CertificateVerifyLink.js +63 -0
- package/dist/technospam/components/CertificateVerifyLink.js.map +1 -0
- package/dist/technospam/components/index.d.ts +1 -0
- package/dist/technospam/components/index.d.ts.map +1 -1
- package/dist/technospam/components/index.js +1 -0
- package/dist/technospam/components/lesson/OfflineProgressSync.d.ts +9 -0
- package/dist/technospam/components/lesson/OfflineProgressSync.d.ts.map +1 -0
- package/dist/technospam/components/lesson/OfflineProgressSync.js +20 -0
- package/dist/technospam/components/lesson/OfflineProgressSync.js.map +1 -0
- package/dist/technospam/components/lesson/SaveCourseOffline.d.ts +17 -0
- package/dist/technospam/components/lesson/SaveCourseOffline.d.ts.map +1 -0
- package/dist/technospam/components/lesson/SaveCourseOffline.js +73 -0
- package/dist/technospam/components/lesson/SaveCourseOffline.js.map +1 -0
- package/dist/technospam/components/lesson/SavedCourseOutline.d.ts +17 -0
- package/dist/technospam/components/lesson/SavedCourseOutline.d.ts.map +1 -0
- package/dist/technospam/components/lesson/SavedCourseOutline.js +77 -0
- package/dist/technospam/components/lesson/SavedCourseOutline.js.map +1 -0
- package/dist/technospam/hooks/useOfflineLessons.d.ts +26 -0
- package/dist/technospam/hooks/useOfflineLessons.d.ts.map +1 -0
- package/dist/technospam/hooks/useOfflineLessons.js +70 -0
- package/dist/technospam/hooks/useOfflineLessons.js.map +1 -0
- package/dist/technospam/index.d.ts +2 -0
- package/dist/technospam/index.d.ts.map +1 -1
- package/dist/technospam/index.js +2 -0
- package/dist/technospam/lib/certificateVerify.d.ts +71 -0
- package/dist/technospam/lib/certificateVerify.d.ts.map +1 -0
- package/dist/technospam/lib/certificateVerify.js +55 -0
- package/dist/technospam/lib/certificateVerify.js.map +1 -0
- package/dist/technospam/lib/offlineLessons.d.ts +97 -0
- package/dist/technospam/lib/offlineLessons.d.ts.map +1 -0
- package/dist/technospam/lib/offlineLessons.js +168 -0
- package/dist/technospam/lib/offlineLessons.js.map +1 -0
- package/dist/technospam/pages/CoursesCourseDetailsPagePage.d.ts.map +1 -1
- package/dist/technospam/pages/CoursesCourseDetailsPagePage.js +241 -198
- package/dist/technospam/pages/CoursesCourseDetailsPagePage.js.map +1 -1
- package/dist/technospam/pages/CoursesLessonPlayerPagePage.d.ts.map +1 -1
- package/dist/technospam/pages/CoursesLessonPlayerPagePage.js +216 -138
- package/dist/technospam/pages/CoursesLessonPlayerPagePage.js.map +1 -1
- package/dist/technospam/pages/CoursesMyLearningPagePage.d.ts.map +1 -1
- package/dist/technospam/pages/CoursesMyLearningPagePage.js +104 -96
- package/dist/technospam/pages/CoursesMyLearningPagePage.js.map +1 -1
- package/dist/technospam/pages/public/CertificateVerifyPage.d.ts +4 -0
- package/dist/technospam/pages/public/CertificateVerifyPage.d.ts.map +1 -0
- package/dist/technospam/pages/public/CertificateVerifyPage.js +257 -0
- package/dist/technospam/pages/public/CertificateVerifyPage.js.map +1 -0
- 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
|
|
6
|
-
import { UpgradeGate as
|
|
7
|
-
import { EmptyView as
|
|
8
|
-
import { notify as
|
|
9
|
-
import { useCompleteLesson as
|
|
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 {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
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
|
|
23
|
-
VIDEO:
|
|
24
|
-
ARTICLE:
|
|
25
|
-
INTERACTIVE:
|
|
26
|
-
QUIZ:
|
|
27
|
-
PROJECT:
|
|
24
|
+
var E = {
|
|
25
|
+
VIDEO: v,
|
|
26
|
+
ARTICLE: g,
|
|
27
|
+
INTERACTIVE: y,
|
|
28
|
+
QUIZ: m,
|
|
29
|
+
PROJECT: _
|
|
28
30
|
};
|
|
29
|
-
function
|
|
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
|
|
33
|
-
let
|
|
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,
|
|
40
|
-
return /* @__PURE__ */
|
|
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 &&
|
|
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__ */
|
|
49
|
-
}) : /* @__PURE__ */
|
|
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
|
-
}),
|
|
72
|
+
}), r]
|
|
53
73
|
});
|
|
54
74
|
}
|
|
55
|
-
case "QUIZ": return /* @__PURE__ */
|
|
75
|
+
case "QUIZ": return /* @__PURE__ */ x("div", {
|
|
56
76
|
className: "space-y-4",
|
|
57
|
-
children: [n, /* @__PURE__ */
|
|
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__ */
|
|
82
|
+
case "INTERACTIVE": return /* @__PURE__ */ x("div", {
|
|
63
83
|
className: "grid grid-cols-1 gap-5 xl:grid-cols-2",
|
|
64
|
-
children: [/* @__PURE__ */
|
|
84
|
+
children: [/* @__PURE__ */ b("div", {
|
|
65
85
|
className: "min-w-0",
|
|
66
|
-
children:
|
|
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__ */
|
|
90
|
+
}), /* @__PURE__ */ b("div", {
|
|
71
91
|
className: "min-w-0",
|
|
72
|
-
children: /* @__PURE__ */
|
|
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
|
|
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
|
|
85
|
-
let [
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
workspaceId:
|
|
89
|
-
entityId:
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
}),
|
|
111
|
-
},
|
|
112
|
-
(await
|
|
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 (!
|
|
173
|
+
if (!f || !_) return /* @__PURE__ */ b(i, {
|
|
115
174
|
title: "Lesson",
|
|
116
175
|
backPath: "/courses/courses-page",
|
|
117
|
-
children: /* @__PURE__ */
|
|
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 (
|
|
181
|
+
if (Ce && !L) return /* @__PURE__ */ x(i, {
|
|
123
182
|
title: "Lesson",
|
|
124
|
-
backPath:
|
|
125
|
-
children: [/* @__PURE__ */
|
|
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 || !
|
|
186
|
+
if (k && !I || !L || !R || L.courseId !== R.id) return /* @__PURE__ */ b(i, {
|
|
128
187
|
title: "Lesson",
|
|
129
|
-
backPath:
|
|
130
|
-
children:
|
|
188
|
+
backPath: Y,
|
|
189
|
+
children: k ? /* @__PURE__ */ b(re, {
|
|
131
190
|
title: "Couldn't load lesson",
|
|
132
|
-
error:
|
|
133
|
-
}) : /* @__PURE__ */
|
|
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
|
|
139
|
-
return /* @__PURE__ */
|
|
140
|
-
backPath:
|
|
141
|
-
title:
|
|
142
|
-
description: `${
|
|
143
|
-
actions:
|
|
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: [
|
|
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__ */
|
|
210
|
+
children: /* @__PURE__ */ x("div", {
|
|
148
211
|
className: "grid grid-cols-1 gap-5 lg:grid-cols-[minmax(0,1fr)_280px]",
|
|
149
|
-
children: [/* @__PURE__ */
|
|
212
|
+
children: [/* @__PURE__ */ x("div", {
|
|
150
213
|
className: "min-w-0 space-y-4",
|
|
151
214
|
children: [
|
|
152
|
-
/* @__PURE__ */
|
|
215
|
+
/* @__PURE__ */ x("div", {
|
|
153
216
|
className: "flex flex-wrap items-center gap-2 text-xs text-text-muted",
|
|
154
217
|
children: [
|
|
155
|
-
/* @__PURE__ */
|
|
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__ */
|
|
221
|
+
/* @__PURE__ */ b(je, { size: 12 }),
|
|
159
222
|
" ",
|
|
160
|
-
|
|
223
|
+
te(L.kind)
|
|
161
224
|
]
|
|
162
225
|
}),
|
|
163
|
-
/* @__PURE__ */
|
|
226
|
+
/* @__PURE__ */ x("span", {
|
|
164
227
|
className: "inline-flex items-center gap-1",
|
|
165
228
|
children: [
|
|
166
|
-
/* @__PURE__ */
|
|
229
|
+
/* @__PURE__ */ b(_e, { size: 12 }),
|
|
167
230
|
" ",
|
|
168
|
-
ee(
|
|
231
|
+
ee(L.estimatedMinutes)
|
|
169
232
|
]
|
|
170
233
|
}),
|
|
171
|
-
|
|
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
|
-
|
|
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__ */
|
|
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__ */
|
|
259
|
+
}), /* @__PURE__ */ x(S, {
|
|
183
260
|
size: "sm",
|
|
184
|
-
onClick: () => void
|
|
185
|
-
disabled:
|
|
261
|
+
onClick: () => void Ae(),
|
|
262
|
+
disabled: M,
|
|
186
263
|
children: [
|
|
187
|
-
/* @__PURE__ */
|
|
264
|
+
/* @__PURE__ */ b(ge, { size: 14 }),
|
|
188
265
|
" ",
|
|
189
|
-
|
|
266
|
+
M ? "Enrolling…" : "Enroll"
|
|
190
267
|
]
|
|
191
268
|
})]
|
|
192
|
-
}),
|
|
193
|
-
/* @__PURE__ */
|
|
269
|
+
}) : null,
|
|
270
|
+
/* @__PURE__ */ b(C, {
|
|
194
271
|
className: "p-5",
|
|
195
272
|
"data-testid": "lesson-player-body",
|
|
196
|
-
children:
|
|
197
|
-
lesson:
|
|
198
|
-
onProgress:
|
|
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__ */
|
|
279
|
+
/* @__PURE__ */ x("div", {
|
|
202
280
|
className: "flex flex-wrap items-center justify-between gap-2",
|
|
203
|
-
children: [/* @__PURE__ */
|
|
281
|
+
children: [/* @__PURE__ */ x(S, {
|
|
204
282
|
variant: "outline",
|
|
205
|
-
disabled: !
|
|
206
|
-
onClick: () =>
|
|
207
|
-
children: [/* @__PURE__ */
|
|
208
|
-
}), /* @__PURE__ */
|
|
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
|
-
|
|
212
|
-
onClick: () => void
|
|
213
|
-
disabled:
|
|
289
|
+
V && !Z && !$ && !L.isLocked ? /* @__PURE__ */ x(S, {
|
|
290
|
+
onClick: () => void ke(),
|
|
291
|
+
disabled: j,
|
|
214
292
|
children: [
|
|
215
|
-
/* @__PURE__ */
|
|
293
|
+
/* @__PURE__ */ b(m, { size: 15 }),
|
|
216
294
|
" ",
|
|
217
|
-
|
|
295
|
+
j ? "Saving…" : "Mark complete"
|
|
218
296
|
]
|
|
219
297
|
}) : null,
|
|
220
|
-
|
|
298
|
+
V && !Z && $ && !L.isLocked ? /* @__PURE__ */ b("span", {
|
|
221
299
|
className: "text-xs text-text-muted",
|
|
222
|
-
children:
|
|
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__ */
|
|
225
|
-
variant:
|
|
226
|
-
disabled: !
|
|
227
|
-
onClick: () =>
|
|
228
|
-
children: ["Next ", /* @__PURE__ */
|
|
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__ */
|
|
312
|
+
}), /* @__PURE__ */ x(C, {
|
|
235
313
|
className: "h-fit p-4",
|
|
236
314
|
children: [
|
|
237
|
-
/* @__PURE__ */
|
|
315
|
+
/* @__PURE__ */ x("div", {
|
|
238
316
|
className: "mb-2 flex items-center justify-between",
|
|
239
|
-
children: [/* @__PURE__ */
|
|
317
|
+
children: [/* @__PURE__ */ b("span", {
|
|
240
318
|
className: "text-xs font-semibold uppercase tracking-wide text-text-muted",
|
|
241
319
|
children: "Curriculum"
|
|
242
|
-
}),
|
|
320
|
+
}), V ? /* @__PURE__ */ x("span", {
|
|
243
321
|
className: "text-xs text-text-secondary",
|
|
244
322
|
"data-testid": "lesson-player-progress",
|
|
245
|
-
children: [
|
|
323
|
+
children: [V.progressPct, "% complete"]
|
|
246
324
|
}) : null]
|
|
247
325
|
}),
|
|
248
|
-
|
|
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__ */
|
|
328
|
+
children: /* @__PURE__ */ b("div", {
|
|
251
329
|
className: "h-full rounded-full bg-accent-primary transition-all duration-500",
|
|
252
|
-
style: { width: `${
|
|
330
|
+
style: { width: `${V.progressPct}%` }
|
|
253
331
|
})
|
|
254
332
|
}) : null,
|
|
255
|
-
/* @__PURE__ */
|
|
333
|
+
/* @__PURE__ */ b("ol", {
|
|
256
334
|
className: "space-y-1",
|
|
257
|
-
children:
|
|
258
|
-
let n =
|
|
259
|
-
return /* @__PURE__ */
|
|
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: () =>
|
|
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__ */
|
|
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__ */
|
|
347
|
+
e.completedByMe ? /* @__PURE__ */ b(m, {
|
|
270
348
|
size: 14,
|
|
271
349
|
className: "shrink-0 text-status-success-text"
|
|
272
|
-
}) : e.isLocked ? /* @__PURE__ */
|
|
350
|
+
}) : e.isLocked ? /* @__PURE__ */ b(ve, {
|
|
273
351
|
size: 14,
|
|
274
352
|
className: "shrink-0 text-text-muted"
|
|
275
|
-
}) : /* @__PURE__ */
|
|
353
|
+
}) : /* @__PURE__ */ b(n, {
|
|
276
354
|
size: 14,
|
|
277
355
|
className: "shrink-0 text-text-muted"
|
|
278
356
|
}),
|
|
279
|
-
/* @__PURE__ */
|
|
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 {
|
|
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":"
|
|
1
|
+
{"version":3,"file":"CoursesMyLearningPagePage.d.ts","sourceRoot":"","sources":["../../../src/technospam/pages/CoursesMyLearningPagePage.tsx"],"names":[],"mappings":"AAiGA,wBAAgB,yBAAyB,gCAqJxC"}
|