@burdenoff/microfe-buildmyiq 2026.1029.6 → 2026.1030.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/buildmyiq/pages/learning/LessonPlayerPage.d.ts.map +1 -1
- package/dist/buildmyiq/pages/learning/LessonPlayerPage.js +218 -200
- package/dist/buildmyiq/pages/learning/LessonPlayerPage.js.map +1 -1
- package/dist/stories/CourseCard.stories.d.ts +9 -0
- package/dist/stories/CourseCard.stories.d.ts.map +1 -0
- package/dist/stories/EmptyState.stories.d.ts.map +1 -1
- package/dist/stories/FilterTabs.stories.d.ts +13 -0
- package/dist/stories/FilterTabs.stories.d.ts.map +1 -0
- package/dist/stories/MetricsCard.stories.d.ts +1 -0
- package/dist/stories/MetricsCard.stories.d.ts.map +1 -1
- package/dist/stories/Pagination.stories.d.ts +9 -0
- package/dist/stories/Pagination.stories.d.ts.map +1 -0
- package/dist/stories/StateViews.stories.d.ts +8 -0
- package/dist/stories/StateViews.stories.d.ts.map +1 -0
- package/dist/stories/StatusBadge.stories.d.ts +1 -0
- package/dist/stories/StatusBadge.stories.d.ts.map +1 -1
- package/dist/stories/Tabs.stories.d.ts +12 -0
- package/dist/stories/Tabs.stories.d.ts.map +1 -0
- package/dist/stories/fixtures/courses.d.ts +6 -0
- package/dist/stories/fixtures/courses.d.ts.map +1 -0
- package/package.json +2 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LessonPlayerPage.d.ts","sourceRoot":"","sources":["../../../../src/buildmyiq/pages/learning/LessonPlayerPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC;AAgEhC,eAAO,MAAM,gBAAgB,EAAE,
|
|
1
|
+
{"version":3,"file":"LessonPlayerPage.d.ts","sourceRoot":"","sources":["../../../../src/buildmyiq/pages/learning/LessonPlayerPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC;AAgEhC,eAAO,MAAM,gBAAgB,EAAE,EAqjB9B,CAAC"}
|
|
@@ -9,215 +9,233 @@ import { EmptyState as s } from "../../components/EmptyState.js";
|
|
|
9
9
|
import { ErrorView as c, LoadingView as l } from "../../components/StateViews.js";
|
|
10
10
|
import { MiniBar as ee } from "../../components/MiniBar.js";
|
|
11
11
|
import { ProgressRing as te } from "../../components/Viz.js";
|
|
12
|
-
import { useBuiCompleteMyEnrollment as ne, useBuiCourse as re, useBuiMyEnrollment as
|
|
12
|
+
import { useBuiCompleteMyEnrollment as ne, useBuiCourse as re, useBuiMyEnrollment as ie, useBuiRecordMyLessonProgress as ae } from "../../hooks/useBuildmyiqApi.js";
|
|
13
13
|
import "../../hooks/index.js";
|
|
14
|
-
import { FileLink as
|
|
15
|
-
import { TutorPanel as
|
|
16
|
-
import { OfflineLessonControl as
|
|
14
|
+
import { FileLink as oe } from "../../components/FileLink.js";
|
|
15
|
+
import { TutorPanel as se } from "../../components/TutorPanel.js";
|
|
16
|
+
import { OfflineLessonControl as ce } from "../../components/OfflineLessonControl.js";
|
|
17
17
|
import "../../components/index.js";
|
|
18
|
-
import { useEffect as
|
|
19
|
-
import { ArrowLeft as
|
|
20
|
-
import { useParams as
|
|
21
|
-
import { Fragment as
|
|
22
|
-
import { Button as
|
|
18
|
+
import { useEffect as le, useMemo as u, useRef as ue, useState as d } from "react";
|
|
19
|
+
import { ArrowLeft as de, BookOpen as fe, Check as pe, CheckCircle2 as f, ChevronLeft as me, ChevronRight as he, FileText as p, HelpCircle as m, ListChecks as ge, Loader2 as h, MousePointerClick as g, PenLine as _, PlayCircle as v, Radio as y, Trophy as _e } from "lucide-react";
|
|
20
|
+
import { useParams as ve, useSearchParams as ye } from "react-router";
|
|
21
|
+
import { Fragment as b, jsx as x, jsxs as S } from "react/jsx-runtime";
|
|
22
|
+
import { Button as C, GlassCard as w } from "@burdenoff/fe-libs/ui";
|
|
23
23
|
//#region src/buildmyiq/pages/learning/LessonPlayerPage.tsx
|
|
24
|
-
var
|
|
25
|
-
ARTICLE:
|
|
26
|
-
VIDEO:
|
|
27
|
-
QUIZ:
|
|
28
|
-
ASSIGNMENT:
|
|
29
|
-
INTERACTIVE:
|
|
30
|
-
LIVE_SESSION:
|
|
31
|
-
},
|
|
32
|
-
let { enrollmentId:
|
|
33
|
-
if (!
|
|
34
|
-
let e = /* @__PURE__ */ new Set(), t =
|
|
24
|
+
var T = {
|
|
25
|
+
ARTICLE: p,
|
|
26
|
+
VIDEO: v,
|
|
27
|
+
QUIZ: m,
|
|
28
|
+
ASSIGNMENT: _,
|
|
29
|
+
INTERACTIVE: g,
|
|
30
|
+
LIVE_SESSION: y
|
|
31
|
+
}, E = (e) => [...e].sort((e, t) => e.position - t.position), D = () => {
|
|
32
|
+
let { enrollmentId: m } = ve(), [g] = ye(), _ = g.get("lesson"), { navigateTo: v } = e(), y = t(), D = ie(m), O = D.data ?? null, k = O?.courseId, A = re(k), j = A.data ?? null, M = ae(), N = ne(), P = M.mutate, F = ue(/* @__PURE__ */ new Set()), [I, L] = d(null), [R, z] = d(!1), [B, V] = d(null), H = u(() => {
|
|
33
|
+
if (!j) return [];
|
|
34
|
+
let e = /* @__PURE__ */ new Set(), t = E(j.modules ?? []), n = [];
|
|
35
35
|
for (let r of t) {
|
|
36
|
-
let t =
|
|
36
|
+
let t = E(r.lessons ?? []).filter((t) => e.has(t.id) ? !1 : (e.add(t.id), !0));
|
|
37
37
|
t.length > 0 && n.push({
|
|
38
38
|
id: r.id,
|
|
39
39
|
title: r.title,
|
|
40
40
|
lessons: t
|
|
41
41
|
});
|
|
42
42
|
}
|
|
43
|
-
let r =
|
|
43
|
+
let r = E(j.lessons ?? []).filter((t) => e.has(t.id) ? !1 : (e.add(t.id), !0));
|
|
44
44
|
return r.length > 0 && n.push({
|
|
45
45
|
id: "__standalone",
|
|
46
|
-
title:
|
|
46
|
+
title: y("common.lessons", "Lessons"),
|
|
47
47
|
lessons: r
|
|
48
48
|
}), n;
|
|
49
|
-
}, [
|
|
49
|
+
}, [j, y]), U = u(() => H.flatMap((e) => e.lessons), [H]), be = u(() => new Map(U.map((e) => [e.id, e.title])), [U]), W = u(() => {
|
|
50
50
|
let e = /* @__PURE__ */ new Map();
|
|
51
|
-
for (let t of
|
|
51
|
+
for (let t of O?.lessonProgress ?? []) e.set(t.lessonId, t.status);
|
|
52
52
|
return e;
|
|
53
|
-
}, [
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
53
|
+
}, [O]), G = u(() => U.length === 0 ? null : I && U.some((e) => e.id === I) ? I : _ && U.some((e) => e.id === _) ? _ : U[0].id, [
|
|
54
|
+
U,
|
|
55
|
+
I,
|
|
56
|
+
_
|
|
57
57
|
]);
|
|
58
|
-
|
|
59
|
-
if (!
|
|
60
|
-
let e =
|
|
61
|
-
e === "COMPLETED" || e === "IN_PROGRESS" ||
|
|
62
|
-
enrollmentId:
|
|
63
|
-
lessonId:
|
|
58
|
+
le(() => {
|
|
59
|
+
if (!m || !G) return;
|
|
60
|
+
let e = W.get(G);
|
|
61
|
+
e === "COMPLETED" || e === "IN_PROGRESS" || F.current.has(G) || (F.current.add(G), P({
|
|
62
|
+
enrollmentId: m,
|
|
63
|
+
lessonId: G,
|
|
64
64
|
status: "IN_PROGRESS"
|
|
65
65
|
}));
|
|
66
66
|
}, [
|
|
67
|
-
|
|
67
|
+
m,
|
|
68
|
+
G,
|
|
68
69
|
W,
|
|
69
|
-
|
|
70
|
-
F
|
|
70
|
+
P
|
|
71
71
|
]);
|
|
72
|
-
let
|
|
73
|
-
|
|
74
|
-
},
|
|
75
|
-
if (
|
|
76
|
-
let t =
|
|
77
|
-
t &&
|
|
78
|
-
},
|
|
79
|
-
if (!
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
72
|
+
let K = U.findIndex((e) => e.id === G), q = K >= 0 ? U[K] : null, J = (q ? W.get(q.id) : void 0) === "COMPLETED", Y = U.filter((e) => W.get(e.id) === "COMPLETED").length, xe = U.length > 0 && Y === U.length, Se = O?.status === "COMPLETED", X = (e) => {
|
|
73
|
+
L(e), z(!1), V(null);
|
|
74
|
+
}, Z = (e) => {
|
|
75
|
+
if (K < 0) return;
|
|
76
|
+
let t = U[K + e];
|
|
77
|
+
t && X(t.id);
|
|
78
|
+
}, Ce = async () => {
|
|
79
|
+
if (!m || !q) return;
|
|
80
|
+
V(null);
|
|
81
|
+
try {
|
|
82
|
+
await M.mutateAsync({
|
|
83
|
+
enrollmentId: m,
|
|
84
|
+
lessonId: q.id,
|
|
85
|
+
status: "COMPLETED"
|
|
86
|
+
});
|
|
87
|
+
} catch (e) {
|
|
88
|
+
V(e instanceof Error && e.message ? e.message : y("buildmyiq.lessonPlayer.markCompleteError", "Could not mark this lesson complete."));
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
let e = U[K + 1];
|
|
92
|
+
e && L(e.id);
|
|
93
|
+
}, we = async () => {
|
|
94
|
+
if (m) {
|
|
95
|
+
V(null);
|
|
96
|
+
try {
|
|
97
|
+
await N.mutateAsync({ id: m });
|
|
98
|
+
} catch (e) {
|
|
99
|
+
V(e instanceof Error && e.message ? e.message : y("buildmyiq.lessonPlayer.finishCourseError", "Could not finish this course."));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
89
102
|
};
|
|
90
|
-
if (!
|
|
91
|
-
title:
|
|
92
|
-
children: /* @__PURE__ */
|
|
103
|
+
if (!m) return /* @__PURE__ */ x(r, {
|
|
104
|
+
title: y("buildmyiq.lessonPlayer.pageTitle", "Lesson"),
|
|
105
|
+
children: /* @__PURE__ */ x(s, {
|
|
93
106
|
illustration: "empty-data",
|
|
94
|
-
title:
|
|
95
|
-
description:
|
|
96
|
-
action: /* @__PURE__ */
|
|
97
|
-
onClick: () =>
|
|
98
|
-
children:
|
|
107
|
+
title: y("buildmyiq.lessonPlayer.noEnrollmentTitle", "No enrollment selected"),
|
|
108
|
+
description: y("buildmyiq.lessonPlayer.noEnrollmentDescription", "Open a course from My Learning to start the lesson player."),
|
|
109
|
+
action: /* @__PURE__ */ x(C, {
|
|
110
|
+
onClick: () => v("learning"),
|
|
111
|
+
children: y("buildmyiq.lessonPlayer.backToLearning", "Back to My Learning")
|
|
99
112
|
})
|
|
100
113
|
})
|
|
101
114
|
});
|
|
102
|
-
let
|
|
115
|
+
let Te = D.isLoading || !!k && A.isLoading, Q = /* @__PURE__ */ S(C, {
|
|
103
116
|
variant: "ghost",
|
|
104
117
|
size: "sm",
|
|
105
|
-
onClick: () =>
|
|
118
|
+
onClick: () => v("learning"),
|
|
106
119
|
children: [
|
|
107
|
-
/* @__PURE__ */
|
|
120
|
+
/* @__PURE__ */ x(de, { className: "mr-1 h-4 w-4" }),
|
|
108
121
|
" ",
|
|
109
|
-
|
|
122
|
+
y("buildmyiq.nav.learning", "My Learning")
|
|
110
123
|
]
|
|
111
124
|
});
|
|
112
|
-
if (
|
|
113
|
-
title:
|
|
125
|
+
if (Te) return /* @__PURE__ */ x(r, {
|
|
126
|
+
title: y("buildmyiq.lessonPlayer.pageTitle", "Lesson"),
|
|
114
127
|
actions: Q,
|
|
115
|
-
children: /* @__PURE__ */
|
|
128
|
+
children: /* @__PURE__ */ x(l, { label: y("buildmyiq.lessonPlayer.loading", "Loading your lesson…") })
|
|
116
129
|
});
|
|
117
|
-
if (
|
|
118
|
-
title:
|
|
130
|
+
if (D.isError) return /* @__PURE__ */ x(r, {
|
|
131
|
+
title: y("buildmyiq.lessonPlayer.pageTitle", "Lesson"),
|
|
119
132
|
actions: Q,
|
|
120
|
-
children: /* @__PURE__ */
|
|
121
|
-
title:
|
|
122
|
-
message:
|
|
123
|
-
onRetry: () =>
|
|
133
|
+
children: /* @__PURE__ */ x(c, {
|
|
134
|
+
title: y("buildmyiq.lessonPlayer.enrollmentErrorTitle", "Couldn't load this enrollment"),
|
|
135
|
+
message: D.error instanceof Error ? D.error.message : void 0,
|
|
136
|
+
onRetry: () => D.refetch()
|
|
124
137
|
})
|
|
125
138
|
});
|
|
126
|
-
if (!
|
|
127
|
-
title:
|
|
139
|
+
if (!O) return /* @__PURE__ */ x(r, {
|
|
140
|
+
title: y("buildmyiq.lessonPlayer.pageTitle", "Lesson"),
|
|
128
141
|
actions: Q,
|
|
129
|
-
children: /* @__PURE__ */
|
|
142
|
+
children: /* @__PURE__ */ x(s, {
|
|
130
143
|
illustration: "empty-data",
|
|
131
|
-
title:
|
|
132
|
-
description:
|
|
133
|
-
action: /* @__PURE__ */
|
|
134
|
-
onClick: () =>
|
|
135
|
-
children:
|
|
144
|
+
title: y("buildmyiq.lessonPlayer.enrollmentNotFoundTitle", "Enrollment not found"),
|
|
145
|
+
description: y("buildmyiq.lessonPlayer.enrollmentNotFoundDescription", "This enrollment may have been removed or is not available to you."),
|
|
146
|
+
action: /* @__PURE__ */ x(C, {
|
|
147
|
+
onClick: () => v("learning"),
|
|
148
|
+
children: y("buildmyiq.lessonPlayer.backToLearning", "Back to My Learning")
|
|
136
149
|
})
|
|
137
150
|
})
|
|
138
151
|
});
|
|
139
|
-
if (
|
|
140
|
-
title:
|
|
152
|
+
if (A.isError) return /* @__PURE__ */ x(r, {
|
|
153
|
+
title: y("buildmyiq.lessonPlayer.pageTitle", "Lesson"),
|
|
141
154
|
actions: Q,
|
|
142
|
-
children: /* @__PURE__ */
|
|
143
|
-
title:
|
|
144
|
-
message:
|
|
145
|
-
onRetry: () =>
|
|
155
|
+
children: /* @__PURE__ */ x(c, {
|
|
156
|
+
title: y("buildmyiq.lessonPlayer.courseErrorTitle", "Couldn't load the course"),
|
|
157
|
+
message: A.error instanceof Error ? A.error.message : void 0,
|
|
158
|
+
onRetry: () => A.refetch()
|
|
146
159
|
})
|
|
147
160
|
});
|
|
148
|
-
let $ = Math.round(
|
|
149
|
-
return /* @__PURE__ */
|
|
150
|
-
title:
|
|
161
|
+
let $ = Math.round(O.progressPct ?? 0), Ee = q ? T[q.type] : p;
|
|
162
|
+
return /* @__PURE__ */ x(r, {
|
|
163
|
+
title: j?.title ?? y("buildmyiq.lessonPlayer.pageTitle", "Lesson"),
|
|
151
164
|
actions: Q,
|
|
152
|
-
children: /* @__PURE__ */
|
|
165
|
+
children: /* @__PURE__ */ S("div", {
|
|
153
166
|
className: "flex flex-col gap-5",
|
|
154
167
|
children: [
|
|
155
|
-
/* @__PURE__ */
|
|
168
|
+
/* @__PURE__ */ S(w, {
|
|
156
169
|
treatment: "glass",
|
|
157
170
|
glow: $ > 0,
|
|
158
171
|
className: "flex flex-col gap-4 p-4 sm:flex-row sm:items-center sm:gap-5",
|
|
159
172
|
children: [
|
|
160
|
-
/* @__PURE__ */
|
|
173
|
+
/* @__PURE__ */ x(te, {
|
|
161
174
|
value: $,
|
|
162
175
|
size: 64,
|
|
163
176
|
className: "shrink-0"
|
|
164
177
|
}),
|
|
165
|
-
/* @__PURE__ */
|
|
178
|
+
/* @__PURE__ */ S("div", {
|
|
166
179
|
className: "min-w-0 flex-1",
|
|
167
|
-
children: [/* @__PURE__ */
|
|
180
|
+
children: [/* @__PURE__ */ S("div", {
|
|
168
181
|
className: "flex flex-wrap items-center gap-2",
|
|
169
|
-
children: [/* @__PURE__ */
|
|
182
|
+
children: [/* @__PURE__ */ x(o, { status: O.status }), /* @__PURE__ */ x("span", {
|
|
170
183
|
className: "text-sm text-text-secondary tabular-nums",
|
|
171
|
-
children:
|
|
172
|
-
completed:
|
|
173
|
-
total:
|
|
184
|
+
children: y("buildmyiq.lessonPlayer.progressSummary", "{{completed}} of {{total}} lessons complete", {
|
|
185
|
+
completed: Y,
|
|
186
|
+
total: U.length
|
|
174
187
|
})
|
|
175
188
|
})]
|
|
176
|
-
}), /* @__PURE__ */
|
|
189
|
+
}), /* @__PURE__ */ x(ee, {
|
|
177
190
|
value: $,
|
|
178
191
|
className: "mt-3"
|
|
179
192
|
})]
|
|
180
193
|
}),
|
|
181
|
-
|
|
194
|
+
xe && !Se && /* @__PURE__ */ S(C, {
|
|
182
195
|
className: "shrink-0 max-sm:w-full",
|
|
183
|
-
onClick:
|
|
184
|
-
disabled:
|
|
185
|
-
children: [
|
|
196
|
+
onClick: we,
|
|
197
|
+
disabled: N.isPending,
|
|
198
|
+
children: [N.isPending ? /* @__PURE__ */ x(h, { className: "mr-1 h-4 w-4 animate-spin" }) : /* @__PURE__ */ x(_e, { className: "mr-1 h-4 w-4" }), y("buildmyiq.lessonPlayer.finishCourse", "Finish course")]
|
|
186
199
|
})
|
|
187
200
|
]
|
|
188
201
|
}),
|
|
189
|
-
/* @__PURE__ */
|
|
202
|
+
B && /* @__PURE__ */ x("p", {
|
|
203
|
+
role: "alert",
|
|
204
|
+
className: "text-sm text-status-error-text",
|
|
205
|
+
children: B
|
|
206
|
+
}),
|
|
207
|
+
/* @__PURE__ */ S(C, {
|
|
190
208
|
variant: "secondary",
|
|
191
209
|
size: "sm",
|
|
192
210
|
className: "w-full lg:hidden",
|
|
193
|
-
onClick: () =>
|
|
194
|
-
children: [/* @__PURE__ */
|
|
195
|
-
completed:
|
|
196
|
-
total:
|
|
211
|
+
onClick: () => z((e) => !e),
|
|
212
|
+
children: [/* @__PURE__ */ x(ge, { className: "mr-1 h-4 w-4" }), R ? y("buildmyiq.lessonPlayer.hideLessons", "Hide lessons") : y("buildmyiq.lessonPlayer.showLessons", "Lessons ({{completed}}/{{total}})", {
|
|
213
|
+
completed: Y,
|
|
214
|
+
total: U.length
|
|
197
215
|
})]
|
|
198
216
|
}),
|
|
199
|
-
/* @__PURE__ */
|
|
217
|
+
/* @__PURE__ */ S("div", {
|
|
200
218
|
className: "flex flex-col gap-5 lg:flex-row",
|
|
201
|
-
children: [/* @__PURE__ */
|
|
202
|
-
className: n("lg:block lg:w-80 lg:shrink-0",
|
|
203
|
-
children: /* @__PURE__ */
|
|
219
|
+
children: [/* @__PURE__ */ x("aside", {
|
|
220
|
+
className: n("lg:block lg:w-80 lg:shrink-0", R ? "block" : "hidden"),
|
|
221
|
+
children: /* @__PURE__ */ x(w, {
|
|
204
222
|
treatment: "plain",
|
|
205
223
|
className: "overflow-hidden p-0 lg:sticky lg:top-4",
|
|
206
|
-
children:
|
|
224
|
+
children: U.length === 0 ? /* @__PURE__ */ x("div", {
|
|
207
225
|
className: "p-4 text-sm text-text-secondary",
|
|
208
|
-
children:
|
|
209
|
-
}) : /* @__PURE__ */
|
|
226
|
+
children: y("buildmyiq.lessonPlayer.noLessons", "This course has no lessons yet.")
|
|
227
|
+
}) : /* @__PURE__ */ S(b, { children: [/* @__PURE__ */ x("nav", {
|
|
210
228
|
className: "max-h-[68vh] overflow-auto p-3",
|
|
211
|
-
children:
|
|
212
|
-
let t = e.lessons.filter((e) =>
|
|
213
|
-
return /* @__PURE__ */
|
|
229
|
+
children: H.map((e) => {
|
|
230
|
+
let t = e.lessons.filter((e) => W.get(e.id) === "COMPLETED").length;
|
|
231
|
+
return /* @__PURE__ */ S("div", {
|
|
214
232
|
className: "mb-4 last:mb-0",
|
|
215
|
-
children: [/* @__PURE__ */
|
|
233
|
+
children: [/* @__PURE__ */ S("div", {
|
|
216
234
|
className: "mb-2 flex items-center justify-between gap-2 px-1",
|
|
217
|
-
children: [/* @__PURE__ */
|
|
235
|
+
children: [/* @__PURE__ */ x("p", {
|
|
218
236
|
className: "min-w-0 truncate text-sm font-semibold text-text-primary",
|
|
219
237
|
children: e.title
|
|
220
|
-
}), /* @__PURE__ */
|
|
238
|
+
}), /* @__PURE__ */ S("span", {
|
|
221
239
|
className: "shrink-0 text-xs font-medium tabular-nums text-text-secondary",
|
|
222
240
|
children: [
|
|
223
241
|
t,
|
|
@@ -225,33 +243,33 @@ var E = {
|
|
|
225
243
|
e.lessons.length
|
|
226
244
|
]
|
|
227
245
|
})]
|
|
228
|
-
}), /* @__PURE__ */
|
|
246
|
+
}), /* @__PURE__ */ x("ul", {
|
|
229
247
|
className: "flex flex-col",
|
|
230
248
|
children: e.lessons.map((t, r) => {
|
|
231
|
-
let o =
|
|
232
|
-
return /* @__PURE__ */
|
|
249
|
+
let o = W.get(t.id) === "COMPLETED", s = t.id === G, c = r === e.lessons.length - 1, l = T[t.type];
|
|
250
|
+
return /* @__PURE__ */ S("li", {
|
|
233
251
|
className: "flex gap-3",
|
|
234
|
-
children: [/* @__PURE__ */
|
|
252
|
+
children: [/* @__PURE__ */ S("div", {
|
|
235
253
|
className: "flex flex-col items-center pt-1.5",
|
|
236
|
-
children: [o ? /* @__PURE__ */
|
|
254
|
+
children: [o ? /* @__PURE__ */ x("span", {
|
|
237
255
|
className: "flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-[var(--color-status-success-text)]",
|
|
238
|
-
children: /* @__PURE__ */
|
|
239
|
-
}) : s ? /* @__PURE__ */
|
|
256
|
+
children: /* @__PURE__ */ x(pe, { className: "h-3.5 w-3.5 text-status-success-bg-subtle" })
|
|
257
|
+
}) : s ? /* @__PURE__ */ x("span", {
|
|
240
258
|
className: "flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-[var(--color-accent-soft)] ring-2 ring-[var(--color-accent-teal)]",
|
|
241
|
-
children: /* @__PURE__ */
|
|
242
|
-
}) : /* @__PURE__ */
|
|
259
|
+
children: /* @__PURE__ */ x(l, { className: "h-3.5 w-3.5 text-[var(--color-accent-teal)]" })
|
|
260
|
+
}) : /* @__PURE__ */ x("span", {
|
|
243
261
|
className: "flex h-6 w-6 shrink-0 items-center justify-center rounded-full border border-border-seam bg-bg-sunken",
|
|
244
|
-
children: /* @__PURE__ */
|
|
245
|
-
}), !c && /* @__PURE__ */
|
|
246
|
-
}), /* @__PURE__ */
|
|
262
|
+
children: /* @__PURE__ */ x(l, { className: "h-3.5 w-3.5 text-text-secondary opacity-70" })
|
|
263
|
+
}), !c && /* @__PURE__ */ x("span", { className: n("my-1 w-px flex-1", o ? "bg-[color-mix(in_srgb,var(--color-status-success-text)_45%,transparent)]" : "bg-border-seam") })]
|
|
264
|
+
}), /* @__PURE__ */ S("button", {
|
|
247
265
|
type: "button",
|
|
248
|
-
onClick: () =>
|
|
266
|
+
onClick: () => X(t.id),
|
|
249
267
|
className: n("mb-1 min-w-0 flex-1 rounded-lg px-3 py-2 text-left transition-colors", s ? "bg-[var(--color-accent-soft)]" : "hover:bg-bg-sunken"),
|
|
250
268
|
"aria-current": s ? "true" : void 0,
|
|
251
|
-
children: [/* @__PURE__ */
|
|
269
|
+
children: [/* @__PURE__ */ x("span", {
|
|
252
270
|
className: n("block truncate text-sm", s ? "font-medium text-text-primary" : o ? "text-text-secondary" : "text-text-primary"),
|
|
253
271
|
children: t.title
|
|
254
|
-
}), /* @__PURE__ */
|
|
272
|
+
}), /* @__PURE__ */ S("span", {
|
|
255
273
|
className: "mt-0.5 block truncate text-xs text-text-secondary",
|
|
256
274
|
children: [a(t.type), t.durationMin > 0 ? ` · ${i(t.durationMin)}` : ""]
|
|
257
275
|
})]
|
|
@@ -261,131 +279,131 @@ var E = {
|
|
|
261
279
|
})]
|
|
262
280
|
}, e.id);
|
|
263
281
|
})
|
|
264
|
-
}),
|
|
282
|
+
}), k && /* @__PURE__ */ x("div", {
|
|
265
283
|
className: "border-t border-border-seam p-3",
|
|
266
|
-
children: /* @__PURE__ */ C
|
|
284
|
+
children: /* @__PURE__ */ S(C, {
|
|
267
285
|
variant: "outline",
|
|
268
286
|
size: "sm",
|
|
269
287
|
className: "w-full",
|
|
270
|
-
onClick: () =>
|
|
288
|
+
onClick: () => v(`catalog/${k}`),
|
|
271
289
|
children: [
|
|
272
|
-
/* @__PURE__ */
|
|
290
|
+
/* @__PURE__ */ x(fe, { className: "mr-1.5 h-4 w-4" }),
|
|
273
291
|
" ",
|
|
274
|
-
|
|
292
|
+
y("buildmyiq.lessonPlayer.courseOverview", "Course overview")
|
|
275
293
|
]
|
|
276
294
|
})
|
|
277
295
|
})] })
|
|
278
296
|
})
|
|
279
|
-
}), /* @__PURE__ */
|
|
297
|
+
}), /* @__PURE__ */ S("main", {
|
|
280
298
|
className: "min-w-0 flex-1",
|
|
281
|
-
children: [
|
|
299
|
+
children: [q ? /* @__PURE__ */ S(w, {
|
|
282
300
|
treatment: "plain",
|
|
283
301
|
className: "flex flex-col gap-4 p-5 sm:p-7",
|
|
284
302
|
children: [
|
|
285
|
-
/* @__PURE__ */
|
|
303
|
+
/* @__PURE__ */ S("div", {
|
|
286
304
|
className: "flex flex-wrap items-center gap-2",
|
|
287
305
|
children: [
|
|
288
|
-
/* @__PURE__ */
|
|
306
|
+
/* @__PURE__ */ S("span", {
|
|
289
307
|
className: "inline-flex items-center gap-1.5 rounded-full bg-[var(--color-accent-soft)] px-2.5 py-1 text-xs font-medium text-text-primary",
|
|
290
|
-
children: [/* @__PURE__ */
|
|
308
|
+
children: [/* @__PURE__ */ x(Ee, { className: "h-3.5 w-3.5 text-[var(--color-accent-teal)]" }), a(q.type)]
|
|
291
309
|
}),
|
|
292
|
-
|
|
310
|
+
q.durationMin > 0 && /* @__PURE__ */ x("span", {
|
|
293
311
|
className: "text-xs text-text-secondary",
|
|
294
|
-
children: i(
|
|
312
|
+
children: i(q.durationMin)
|
|
295
313
|
}),
|
|
296
|
-
|
|
314
|
+
J && /* @__PURE__ */ S("span", {
|
|
297
315
|
className: "inline-flex items-center gap-1 text-xs font-medium text-status-success-text",
|
|
298
316
|
children: [
|
|
299
|
-
/* @__PURE__ */
|
|
317
|
+
/* @__PURE__ */ x(f, { className: "h-3.5 w-3.5" }),
|
|
300
318
|
" ",
|
|
301
|
-
|
|
319
|
+
y("buildmyiq.lessonPlayer.completedLabel", "Completed")
|
|
302
320
|
]
|
|
303
321
|
}),
|
|
304
|
-
/* @__PURE__ */
|
|
305
|
-
/* @__PURE__ */
|
|
322
|
+
/* @__PURE__ */ x(ce, { lesson: q }, q.id),
|
|
323
|
+
/* @__PURE__ */ x("span", {
|
|
306
324
|
className: "ml-auto text-xs tabular-nums text-text-secondary",
|
|
307
|
-
children:
|
|
308
|
-
n:
|
|
309
|
-
total:
|
|
325
|
+
children: y("buildmyiq.lessonPlayer.lessonPosition", "Lesson {{n}} of {{total}}", {
|
|
326
|
+
n: K + 1,
|
|
327
|
+
total: U.length
|
|
310
328
|
})
|
|
311
329
|
})
|
|
312
330
|
]
|
|
313
331
|
}),
|
|
314
|
-
/* @__PURE__ */
|
|
332
|
+
/* @__PURE__ */ x("h2", {
|
|
315
333
|
className: "text-2xl font-semibold tracking-tight text-text-primary",
|
|
316
|
-
children:
|
|
334
|
+
children: q.title
|
|
317
335
|
}),
|
|
318
|
-
|
|
336
|
+
q.media ? /* @__PURE__ */ S("div", {
|
|
319
337
|
className: "rounded-xl border border-border-seam bg-bg-sunken p-3",
|
|
320
|
-
children: [/* @__PURE__ */
|
|
338
|
+
children: [/* @__PURE__ */ x("p", {
|
|
321
339
|
className: "mb-2 text-xs font-semibold uppercase tracking-wide text-text-secondary",
|
|
322
|
-
children:
|
|
323
|
-
}), /* @__PURE__ */
|
|
340
|
+
children: y("buildmyiq.lessonPlayer.resources", "Resources")
|
|
341
|
+
}), /* @__PURE__ */ x(oe, { fileId: q.media.id })]
|
|
324
342
|
}) : null,
|
|
325
|
-
|
|
343
|
+
q.content ? /* @__PURE__ */ x("p", {
|
|
326
344
|
className: "max-w-prose whitespace-pre-wrap text-[15px] leading-7 text-text-primary",
|
|
327
|
-
children:
|
|
328
|
-
}) : /* @__PURE__ */
|
|
345
|
+
children: q.content
|
|
346
|
+
}) : /* @__PURE__ */ x("p", {
|
|
329
347
|
className: "text-sm text-text-secondary",
|
|
330
|
-
children:
|
|
348
|
+
children: q.media ? y("buildmyiq.lessonPlayer.noContentWithMedia", "This lesson has no written content beyond the attached media.") : y("buildmyiq.lessonPlayer.noContent", "This lesson has no written content")
|
|
331
349
|
}),
|
|
332
|
-
/* @__PURE__ */
|
|
350
|
+
/* @__PURE__ */ S("div", {
|
|
333
351
|
className: "mt-2 flex flex-col gap-3 border-t border-border-seam pt-5 sm:flex-row sm:items-center sm:justify-between",
|
|
334
352
|
children: [
|
|
335
|
-
/* @__PURE__ */ C
|
|
353
|
+
/* @__PURE__ */ S(C, {
|
|
336
354
|
variant: "outline",
|
|
337
355
|
size: "sm",
|
|
338
356
|
className: "max-sm:w-full",
|
|
339
|
-
onClick: () =>
|
|
340
|
-
disabled:
|
|
357
|
+
onClick: () => Z(-1),
|
|
358
|
+
disabled: K <= 0,
|
|
341
359
|
children: [
|
|
342
|
-
/* @__PURE__ */
|
|
360
|
+
/* @__PURE__ */ x(me, { className: "mr-1 h-4 w-4" }),
|
|
343
361
|
" ",
|
|
344
|
-
|
|
362
|
+
y("buildmyiq.lessonPlayer.previous", "Previous")
|
|
345
363
|
]
|
|
346
364
|
}),
|
|
347
|
-
|
|
365
|
+
J ? /* @__PURE__ */ S("span", {
|
|
348
366
|
className: "inline-flex items-center justify-center gap-2 rounded-lg bg-status-success-bg-subtle px-4 py-2 text-sm font-medium text-status-success-text max-sm:w-full",
|
|
349
367
|
children: [
|
|
350
|
-
/* @__PURE__ */
|
|
368
|
+
/* @__PURE__ */ x(f, { className: "h-4 w-4" }),
|
|
351
369
|
" ",
|
|
352
|
-
|
|
370
|
+
y("buildmyiq.lessonPlayer.lessonComplete", "Lesson complete")
|
|
353
371
|
]
|
|
354
|
-
}) : /* @__PURE__ */ C
|
|
372
|
+
}) : /* @__PURE__ */ S(C, {
|
|
355
373
|
size: "lg",
|
|
356
374
|
className: "max-sm:w-full",
|
|
357
|
-
onClick:
|
|
358
|
-
disabled:
|
|
359
|
-
children: [
|
|
375
|
+
onClick: Ce,
|
|
376
|
+
disabled: M.isPending,
|
|
377
|
+
children: [M.isPending ? /* @__PURE__ */ x(h, { className: "mr-1.5 h-4 w-4 animate-spin" }) : /* @__PURE__ */ x(f, { className: "mr-1.5 h-4 w-4" }), M.isPending ? y("buildmyiq.lessonPlayer.markingComplete", "Marking complete…") : y("buildmyiq.lessonPlayer.markComplete", "Mark complete")]
|
|
360
378
|
}),
|
|
361
|
-
/* @__PURE__ */ C
|
|
379
|
+
/* @__PURE__ */ S(C, {
|
|
362
380
|
variant: "outline",
|
|
363
381
|
size: "sm",
|
|
364
382
|
className: "max-sm:w-full",
|
|
365
|
-
onClick: () =>
|
|
366
|
-
disabled:
|
|
383
|
+
onClick: () => Z(1),
|
|
384
|
+
disabled: K >= U.length - 1,
|
|
367
385
|
children: [
|
|
368
|
-
|
|
386
|
+
y("buildmyiq.lessonPlayer.next", "Next"),
|
|
369
387
|
" ",
|
|
370
|
-
/* @__PURE__ */
|
|
388
|
+
/* @__PURE__ */ x(he, { className: "ml-1 h-4 w-4" })
|
|
371
389
|
]
|
|
372
390
|
})
|
|
373
391
|
]
|
|
374
392
|
})
|
|
375
393
|
]
|
|
376
|
-
}) : /* @__PURE__ */
|
|
394
|
+
}) : /* @__PURE__ */ x(s, {
|
|
377
395
|
illustration: "empty-data",
|
|
378
|
-
title:
|
|
379
|
-
description:
|
|
380
|
-
}),
|
|
396
|
+
title: y("buildmyiq.lessonPlayer.noLessonTitle", "No lesson to show"),
|
|
397
|
+
description: y("buildmyiq.lessonPlayer.noLessonDescription", "This course doesn't have any lessons yet. Check back once the author adds content.")
|
|
398
|
+
}), q && k && m && /* @__PURE__ */ x("div", {
|
|
381
399
|
className: "mt-5",
|
|
382
|
-
children: /* @__PURE__ */
|
|
383
|
-
courseId:
|
|
384
|
-
enrollmentId:
|
|
385
|
-
lessonId:
|
|
386
|
-
lessonTitle:
|
|
387
|
-
lessonTitlesById:
|
|
388
|
-
},
|
|
400
|
+
children: /* @__PURE__ */ x(se, {
|
|
401
|
+
courseId: k,
|
|
402
|
+
enrollmentId: m,
|
|
403
|
+
lessonId: q.id,
|
|
404
|
+
lessonTitle: q.title,
|
|
405
|
+
lessonTitlesById: be
|
|
406
|
+
}, k)
|
|
389
407
|
})]
|
|
390
408
|
})]
|
|
391
409
|
})
|
|
@@ -394,6 +412,6 @@ var E = {
|
|
|
394
412
|
});
|
|
395
413
|
};
|
|
396
414
|
//#endregion
|
|
397
|
-
export {
|
|
415
|
+
export { D as LessonPlayerPage };
|
|
398
416
|
|
|
399
417
|
//# sourceMappingURL=LessonPlayerPage.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LessonPlayerPage.js","names":[],"sources":["../../../../src/buildmyiq/pages/learning/LessonPlayerPage.tsx"],"sourcesContent":["/**\n * LessonPlayerPage — the two-pane lesson player for a single enrollment.\n * @module buildmyiq/pages/learning\n *\n * Left pane: the course's module/lesson tree (from `buiCourse`) rendered as a\n * completion timeline driven by the enrollment's `lessonProgress`. Right pane:\n * the selected lesson (title, type, content text rendered safely, and a\n * `FileLink` for any attached media) with Prev / Mark-complete / Next controls.\n * Opening a lesson records IN_PROGRESS once; \"Mark complete\" records COMPLETED;\n * finishing every lesson transitions the enrollment to COMPLETED. All data is\n * live — no fabrication.\n */\n\nimport type { FC } from 'react';\nimport { useEffect, useMemo, useRef, useState } from 'react';\nimport { useParams, useSearchParams } from 'react-router';\nimport type { LucideIcon } from 'lucide-react';\nimport {\n ArrowLeft,\n BookOpen,\n Check,\n CheckCircle2,\n ChevronLeft,\n ChevronRight,\n FileText,\n HelpCircle,\n Loader2,\n ListChecks,\n MousePointerClick,\n PenLine,\n PlayCircle,\n Radio,\n Trophy,\n} from 'lucide-react';\nimport { Button, GlassCard } from '@burdenoff/fe-libs/ui';\nimport {\n PageLayout,\n StatusBadge,\n MiniBar,\n ProgressRing,\n LoadingView,\n ErrorView,\n EmptyState,\n FileLink,\n OfflineLessonControl,\n TutorPanel,\n} from '../../components';\nimport { useBuildmyiq } from '../../context';\nimport {\n useBuiMyEnrollment,\n useBuiCourse,\n useBuiRecordMyLessonProgress,\n useBuiCompleteMyEnrollment,\n} from '../../hooks';\nimport { useTr } from '../../shared/hooks/useTr';\nimport { cn } from '../../utils/cn';\nimport { formatDuration, formatEnumLabel } from '../../utils/formatters';\nimport type { BuiLesson, BuiLessonType, BuiModule, BuiProgressStatus } from '../../types';\n\ninterface LessonGroup {\n id: string;\n title: string;\n lessons: BuiLesson[];\n}\n\nconst LESSON_ICON: Record<BuiLessonType, LucideIcon> = {\n ARTICLE: FileText,\n VIDEO: PlayCircle,\n QUIZ: HelpCircle,\n ASSIGNMENT: PenLine,\n INTERACTIVE: MousePointerClick,\n LIVE_SESSION: Radio,\n};\n\nconst sortByPosition = <T extends { position: number }>(items: T[]): T[] =>\n [...items].sort((a, b) => a.position - b.position);\n\nexport const LessonPlayerPage: FC = () => {\n const { enrollmentId } = useParams<{ enrollmentId: string }>();\n // `?lesson=<id>` deep-links straight to one lesson (used by the \"what's next\"\n // suggestion on My Learning). Purely an INITIAL anchor — once the learner\n // picks a lesson themselves, their pick wins.\n const [searchParams] = useSearchParams();\n const requestedLessonId = searchParams.get('lesson');\n const { navigateTo } = useBuildmyiq();\n const tr = useTr();\n\n const enrollmentQuery = useBuiMyEnrollment(enrollmentId);\n const enrollment = enrollmentQuery.data ?? null;\n const courseId = enrollment?.courseId;\n const courseQuery = useBuiCourse(courseId);\n const course = courseQuery.data ?? null;\n\n const recordProgress = useBuiRecordMyLessonProgress();\n const completeEnrollment = useBuiCompleteMyEnrollment();\n\n // react-query's mutate is referentially stable across renders, so it can be\n // used directly in effects without a render-time ref write.\n const recordMutate = recordProgress.mutate;\n const autoRecordedRef = useRef<Set<string>>(new Set());\n\n const [userSelectedId, setUserSelectedId] = useState<string | null>(null);\n const [showTree, setShowTree] = useState(false);\n\n // Grouped tree (modules → lessons) plus any standalone lessons, de-duplicated.\n const groups = useMemo<LessonGroup[]>(() => {\n if (!course) return [];\n const seen = new Set<string>();\n const modules: BuiModule[] = sortByPosition(course.modules ?? []);\n const result: LessonGroup[] = [];\n for (const m of modules) {\n const lessons = sortByPosition(m.lessons ?? []).filter((l) => {\n if (seen.has(l.id)) return false;\n seen.add(l.id);\n return true;\n });\n if (lessons.length > 0) result.push({ id: m.id, title: m.title, lessons });\n }\n const standalone = sortByPosition(course.lessons ?? []).filter((l) => {\n if (seen.has(l.id)) return false;\n seen.add(l.id);\n return true;\n });\n if (standalone.length > 0) {\n result.push({\n id: '__standalone',\n title: tr('common.lessons', 'Lessons'),\n lessons: standalone,\n });\n }\n return result;\n }, [course, tr]);\n\n // Flat, ordered lesson list for prev/next + completion checks.\n const orderedLessons = useMemo<BuiLesson[]>(() => groups.flatMap((g) => g.lessons), [groups]);\n // Lesson id → title for this course, so the tutor panel can NAME the lessons\n // its answers were grounded in instead of showing opaque ids.\n const lessonTitlesById = useMemo<ReadonlyMap<string, string>>(\n () => new Map(orderedLessons.map((lesson) => [lesson.id, lesson.title])),\n [orderedLessons]\n );\n\n const progressByLesson = useMemo(() => {\n const map = new Map<string, BuiProgressStatus>();\n for (const p of enrollment?.lessonProgress ?? []) map.set(p.lessonId, p.status);\n return map;\n }, [enrollment]);\n\n // Resolve the active lesson as DERIVED state: the user's pick if it still\n // exists in the course, otherwise the first lesson. Avoids a setState-in-\n // effect re-anchor loop.\n const selectedLessonId = useMemo<string | null>(() => {\n if (orderedLessons.length === 0) return null;\n if (userSelectedId && orderedLessons.some((l) => l.id === userSelectedId)) {\n return userSelectedId;\n }\n if (requestedLessonId && orderedLessons.some((l) => l.id === requestedLessonId)) {\n return requestedLessonId;\n }\n return orderedLessons[0].id;\n }, [orderedLessons, userSelectedId, requestedLessonId]);\n\n // Record IN_PROGRESS once when a not-yet-started lesson is opened.\n useEffect(() => {\n if (!enrollmentId || !selectedLessonId) return;\n const status = progressByLesson.get(selectedLessonId);\n if (status === 'COMPLETED' || status === 'IN_PROGRESS') return;\n if (autoRecordedRef.current.has(selectedLessonId)) return;\n autoRecordedRef.current.add(selectedLessonId);\n recordMutate({\n enrollmentId,\n lessonId: selectedLessonId,\n status: 'IN_PROGRESS',\n });\n }, [enrollmentId, selectedLessonId, progressByLesson, recordMutate]);\n\n const selectedIndex = orderedLessons.findIndex((l) => l.id === selectedLessonId);\n const selectedLesson = selectedIndex >= 0 ? orderedLessons[selectedIndex] : null;\n const selectedStatus = selectedLesson ? progressByLesson.get(selectedLesson.id) : undefined;\n const isSelectedComplete = selectedStatus === 'COMPLETED';\n\n const completedCount = orderedLessons.filter(\n (l) => progressByLesson.get(l.id) === 'COMPLETED'\n ).length;\n const allComplete = orderedLessons.length > 0 && completedCount === orderedLessons.length;\n const alreadyFinished = enrollment?.status === 'COMPLETED';\n\n const handleSelect = (lessonId: string) => {\n setUserSelectedId(lessonId);\n setShowTree(false);\n };\n\n const goTo = (offset: number) => {\n if (selectedIndex < 0) return;\n const next = orderedLessons[selectedIndex + offset];\n if (next) handleSelect(next.id);\n };\n\n const handleMarkComplete = async () => {\n if (!enrollmentId || !selectedLesson) return;\n await recordProgress.mutateAsync({\n enrollmentId,\n lessonId: selectedLesson.id,\n status: 'COMPLETED',\n });\n // Advance to the next incomplete lesson for a natural flow.\n const next = orderedLessons[selectedIndex + 1];\n if (next) setUserSelectedId(next.id);\n };\n\n const handleFinishCourse = async () => {\n if (!enrollmentId) return;\n await completeEnrollment.mutateAsync({ id: enrollmentId });\n };\n\n // ---- top-level states ----------------------------------------------------\n if (!enrollmentId) {\n return (\n <PageLayout title={tr('buildmyiq.lessonPlayer.pageTitle', 'Lesson')}>\n <EmptyState\n illustration=\"empty-data\"\n title={tr('buildmyiq.lessonPlayer.noEnrollmentTitle', 'No enrollment selected')}\n description={tr(\n 'buildmyiq.lessonPlayer.noEnrollmentDescription',\n 'Open a course from My Learning to start the lesson player.'\n )}\n action={\n <Button onClick={() => navigateTo('learning')}>\n {tr('buildmyiq.lessonPlayer.backToLearning', 'Back to My Learning')}\n </Button>\n }\n />\n </PageLayout>\n );\n }\n\n const isLoading = enrollmentQuery.isLoading || (Boolean(courseId) && courseQuery.isLoading);\n\n const backAction = (\n <Button variant=\"ghost\" size=\"sm\" onClick={() => navigateTo('learning')}>\n <ArrowLeft className=\"mr-1 h-4 w-4\" /> {tr('buildmyiq.nav.learning', 'My Learning')}\n </Button>\n );\n\n if (isLoading) {\n return (\n <PageLayout title={tr('buildmyiq.lessonPlayer.pageTitle', 'Lesson')} actions={backAction}>\n <LoadingView label={tr('buildmyiq.lessonPlayer.loading', 'Loading your lesson…')} />\n </PageLayout>\n );\n }\n\n if (enrollmentQuery.isError) {\n return (\n <PageLayout title={tr('buildmyiq.lessonPlayer.pageTitle', 'Lesson')} actions={backAction}>\n <ErrorView\n title={tr('buildmyiq.lessonPlayer.enrollmentErrorTitle', \"Couldn't load this enrollment\")}\n message={\n enrollmentQuery.error instanceof Error ? enrollmentQuery.error.message : undefined\n }\n onRetry={() => enrollmentQuery.refetch()}\n />\n </PageLayout>\n );\n }\n\n if (!enrollment) {\n return (\n <PageLayout title={tr('buildmyiq.lessonPlayer.pageTitle', 'Lesson')} actions={backAction}>\n <EmptyState\n illustration=\"empty-data\"\n title={tr('buildmyiq.lessonPlayer.enrollmentNotFoundTitle', 'Enrollment not found')}\n description={tr(\n 'buildmyiq.lessonPlayer.enrollmentNotFoundDescription',\n 'This enrollment may have been removed or is not available to you.'\n )}\n action={\n <Button onClick={() => navigateTo('learning')}>\n {tr('buildmyiq.lessonPlayer.backToLearning', 'Back to My Learning')}\n </Button>\n }\n />\n </PageLayout>\n );\n }\n\n if (courseQuery.isError) {\n return (\n <PageLayout title={tr('buildmyiq.lessonPlayer.pageTitle', 'Lesson')} actions={backAction}>\n <ErrorView\n title={tr('buildmyiq.lessonPlayer.courseErrorTitle', \"Couldn't load the course\")}\n message={courseQuery.error instanceof Error ? courseQuery.error.message : undefined}\n onRetry={() => courseQuery.refetch()}\n />\n </PageLayout>\n );\n }\n\n const progressPct = Math.round(enrollment.progressPct ?? 0);\n const SelectedIcon = selectedLesson ? LESSON_ICON[selectedLesson.type] : FileText;\n\n return (\n <PageLayout\n title={course?.title ?? tr('buildmyiq.lessonPlayer.pageTitle', 'Lesson')}\n actions={backAction}\n >\n <div className=\"flex flex-col gap-5\">\n {/* Header progress — ProgressRing hero + completion bar */}\n <GlassCard\n treatment=\"glass\"\n glow={progressPct > 0}\n className=\"flex flex-col gap-4 p-4 sm:flex-row sm:items-center sm:gap-5\"\n >\n <ProgressRing value={progressPct} size={64} className=\"shrink-0\" />\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <StatusBadge status={enrollment.status} />\n <span className=\"text-sm text-text-secondary tabular-nums\">\n {tr(\n 'buildmyiq.lessonPlayer.progressSummary',\n '{{completed}} of {{total}} lessons complete',\n { completed: completedCount, total: orderedLessons.length }\n )}\n </span>\n </div>\n <MiniBar value={progressPct} className=\"mt-3\" />\n </div>\n {allComplete && !alreadyFinished && (\n <Button\n className=\"shrink-0 max-sm:w-full\"\n onClick={handleFinishCourse}\n disabled={completeEnrollment.isPending}\n >\n {completeEnrollment.isPending ? (\n <Loader2 className=\"mr-1 h-4 w-4 animate-spin\" />\n ) : (\n <Trophy className=\"mr-1 h-4 w-4\" />\n )}\n {tr('buildmyiq.lessonPlayer.finishCourse', 'Finish course')}\n </Button>\n )}\n </GlassCard>\n\n {/* Mobile tree toggle — collapses the outline into a top sheet */}\n <Button\n variant=\"secondary\"\n size=\"sm\"\n className=\"w-full lg:hidden\"\n onClick={() => setShowTree((v) => !v)}\n >\n <ListChecks className=\"mr-1 h-4 w-4\" />\n {showTree\n ? tr('buildmyiq.lessonPlayer.hideLessons', 'Hide lessons')\n : tr('buildmyiq.lessonPlayer.showLessons', 'Lessons ({{completed}}/{{total}})', {\n completed: completedCount,\n total: orderedLessons.length,\n })}\n </Button>\n\n <div className=\"flex flex-col gap-5 lg:flex-row\">\n {/* Left: module / lesson completion timeline */}\n <aside className={cn('lg:block lg:w-80 lg:shrink-0', showTree ? 'block' : 'hidden')}>\n <GlassCard treatment=\"plain\" className=\"overflow-hidden p-0 lg:sticky lg:top-4\">\n {orderedLessons.length === 0 ? (\n <div className=\"p-4 text-sm text-text-secondary\">\n {tr('buildmyiq.lessonPlayer.noLessons', 'This course has no lessons yet.')}\n </div>\n ) : (\n <>\n <nav className=\"max-h-[68vh] overflow-auto p-3\">\n {groups.map((group) => {\n const groupDone = group.lessons.filter(\n (l) => progressByLesson.get(l.id) === 'COMPLETED'\n ).length;\n return (\n <div key={group.id} className=\"mb-4 last:mb-0\">\n <div className=\"mb-2 flex items-center justify-between gap-2 px-1\">\n <p className=\"min-w-0 truncate text-sm font-semibold text-text-primary\">\n {group.title}\n </p>\n <span className=\"shrink-0 text-xs font-medium tabular-nums text-text-secondary\">\n {groupDone} / {group.lessons.length}\n </span>\n </div>\n <ul className=\"flex flex-col\">\n {group.lessons.map((lesson, i) => {\n const status = progressByLesson.get(lesson.id);\n const done = status === 'COMPLETED';\n const active = lesson.id === selectedLessonId;\n const isLast = i === group.lessons.length - 1;\n const Icon = LESSON_ICON[lesson.type];\n return (\n <li key={lesson.id} className=\"flex gap-3\">\n {/* Timeline node + connector */}\n <div className=\"flex flex-col items-center pt-1.5\">\n {done ? (\n <span className=\"flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-[var(--color-status-success-text)]\">\n <Check className=\"h-3.5 w-3.5 text-status-success-bg-subtle\" />\n </span>\n ) : active ? (\n <span className=\"flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-[var(--color-accent-soft)] ring-2 ring-[var(--color-accent-teal)]\">\n <Icon className=\"h-3.5 w-3.5 text-[var(--color-accent-teal)]\" />\n </span>\n ) : (\n <span className=\"flex h-6 w-6 shrink-0 items-center justify-center rounded-full border border-border-seam bg-bg-sunken\">\n <Icon className=\"h-3.5 w-3.5 text-text-secondary opacity-70\" />\n </span>\n )}\n {!isLast && (\n <span\n className={cn(\n 'my-1 w-px flex-1',\n done\n ? 'bg-[color-mix(in_srgb,var(--color-status-success-text)_45%,transparent)]'\n : 'bg-border-seam'\n )}\n />\n )}\n </div>\n <button\n type=\"button\"\n onClick={() => handleSelect(lesson.id)}\n className={cn(\n 'mb-1 min-w-0 flex-1 rounded-lg px-3 py-2 text-left transition-colors',\n active\n ? 'bg-[var(--color-accent-soft)]'\n : 'hover:bg-bg-sunken'\n )}\n aria-current={active ? 'true' : undefined}\n >\n <span\n className={cn(\n 'block truncate text-sm',\n active\n ? 'font-medium text-text-primary'\n : done\n ? 'text-text-secondary'\n : 'text-text-primary'\n )}\n >\n {lesson.title}\n </span>\n <span className=\"mt-0.5 block truncate text-xs text-text-secondary\">\n {formatEnumLabel(lesson.type)}\n {lesson.durationMin > 0\n ? ` · ${formatDuration(lesson.durationMin)}`\n : ''}\n </span>\n </button>\n </li>\n );\n })}\n </ul>\n </div>\n );\n })}\n </nav>\n {courseId && (\n <div className=\"border-t border-border-seam p-3\">\n <Button\n variant=\"outline\"\n size=\"sm\"\n className=\"w-full\"\n onClick={() => navigateTo(`catalog/${courseId}`)}\n >\n <BookOpen className=\"mr-1.5 h-4 w-4\" />{' '}\n {tr('buildmyiq.lessonPlayer.courseOverview', 'Course overview')}\n </Button>\n </div>\n )}\n </>\n )}\n </GlassCard>\n </aside>\n\n {/* Right: selected lesson */}\n <main className=\"min-w-0 flex-1\">\n {!selectedLesson ? (\n <EmptyState\n illustration=\"empty-data\"\n title={tr('buildmyiq.lessonPlayer.noLessonTitle', 'No lesson to show')}\n description={tr(\n 'buildmyiq.lessonPlayer.noLessonDescription',\n \"This course doesn't have any lessons yet. Check back once the author adds content.\"\n )}\n />\n ) : (\n <GlassCard treatment=\"plain\" className=\"flex flex-col gap-4 p-5 sm:p-7\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <span className=\"inline-flex items-center gap-1.5 rounded-full bg-[var(--color-accent-soft)] px-2.5 py-1 text-xs font-medium text-text-primary\">\n <SelectedIcon className=\"h-3.5 w-3.5 text-[var(--color-accent-teal)]\" />\n {formatEnumLabel(selectedLesson.type)}\n </span>\n {selectedLesson.durationMin > 0 && (\n <span className=\"text-xs text-text-secondary\">\n {formatDuration(selectedLesson.durationMin)}\n </span>\n )}\n {isSelectedComplete && (\n <span className=\"inline-flex items-center gap-1 text-xs font-medium text-status-success-text\">\n <CheckCircle2 className=\"h-3.5 w-3.5\" />{' '}\n {tr('buildmyiq.lessonPlayer.completedLabel', 'Completed')}\n </span>\n )}\n <OfflineLessonControl key={selectedLesson.id} lesson={selectedLesson} />\n <span className=\"ml-auto text-xs tabular-nums text-text-secondary\">\n {tr('buildmyiq.lessonPlayer.lessonPosition', 'Lesson {{n}} of {{total}}', {\n n: selectedIndex + 1,\n total: orderedLessons.length,\n })}\n </span>\n </div>\n\n <h2 className=\"text-2xl font-semibold tracking-tight text-text-primary\">\n {selectedLesson.title}\n </h2>\n\n {selectedLesson.media ? (\n <div className=\"rounded-xl border border-border-seam bg-bg-sunken p-3\">\n <p className=\"mb-2 text-xs font-semibold uppercase tracking-wide text-text-secondary\">\n {tr('buildmyiq.lessonPlayer.resources', 'Resources')}\n </p>\n <FileLink fileId={selectedLesson.media.id} />\n </div>\n ) : null}\n\n {selectedLesson.content ? (\n <p className=\"max-w-prose whitespace-pre-wrap text-[15px] leading-7 text-text-primary\">\n {selectedLesson.content}\n </p>\n ) : (\n <p className=\"text-sm text-text-secondary\">\n {selectedLesson.media\n ? tr(\n 'buildmyiq.lessonPlayer.noContentWithMedia',\n 'This lesson has no written content beyond the attached media.'\n )\n : tr(\n 'buildmyiq.lessonPlayer.noContent',\n 'This lesson has no written content'\n )}\n </p>\n )}\n\n {/* Actions — Prev · Mark complete · Next */}\n <div className=\"mt-2 flex flex-col gap-3 border-t border-border-seam pt-5 sm:flex-row sm:items-center sm:justify-between\">\n <Button\n variant=\"outline\"\n size=\"sm\"\n className=\"max-sm:w-full\"\n onClick={() => goTo(-1)}\n disabled={selectedIndex <= 0}\n >\n <ChevronLeft className=\"mr-1 h-4 w-4\" />{' '}\n {tr('buildmyiq.lessonPlayer.previous', 'Previous')}\n </Button>\n\n {isSelectedComplete ? (\n <span className=\"inline-flex items-center justify-center gap-2 rounded-lg bg-status-success-bg-subtle px-4 py-2 text-sm font-medium text-status-success-text max-sm:w-full\">\n <CheckCircle2 className=\"h-4 w-4\" />{' '}\n {tr('buildmyiq.lessonPlayer.lessonComplete', 'Lesson complete')}\n </span>\n ) : (\n <Button\n size=\"lg\"\n className=\"max-sm:w-full\"\n onClick={handleMarkComplete}\n disabled={recordProgress.isPending}\n >\n {recordProgress.isPending ? (\n <Loader2 className=\"mr-1.5 h-4 w-4 animate-spin\" />\n ) : (\n <CheckCircle2 className=\"mr-1.5 h-4 w-4\" />\n )}\n {recordProgress.isPending\n ? tr('buildmyiq.lessonPlayer.markingComplete', 'Marking complete…')\n : tr('buildmyiq.lessonPlayer.markComplete', 'Mark complete')}\n </Button>\n )}\n\n <Button\n variant=\"outline\"\n size=\"sm\"\n className=\"max-sm:w-full\"\n onClick={() => goTo(1)}\n disabled={selectedIndex >= orderedLessons.length - 1}\n >\n {tr('buildmyiq.lessonPlayer.next', 'Next')}{' '}\n <ChevronRight className=\"ml-1 h-4 w-4\" />\n </Button>\n </div>\n </GlassCard>\n )}\n\n {selectedLesson && courseId && enrollmentId && (\n <div className=\"mt-5\">\n {/* Course changes reset local state; lesson navigation keeps the course conversation. */}\n <TutorPanel\n key={courseId}\n courseId={courseId}\n enrollmentId={enrollmentId}\n lessonId={selectedLesson.id}\n lessonTitle={selectedLesson.title}\n lessonTitlesById={lessonTitlesById}\n />\n </div>\n )}\n </main>\n </div>\n </div>\n </PageLayout>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"LessonPlayerPage.js","names":[],"sources":["../../../../src/buildmyiq/pages/learning/LessonPlayerPage.tsx"],"sourcesContent":["/**\n * LessonPlayerPage — the two-pane lesson player for a single enrollment.\n * @module buildmyiq/pages/learning\n *\n * Left pane: the course's module/lesson tree (from `buiCourse`) rendered as a\n * completion timeline driven by the enrollment's `lessonProgress`. Right pane:\n * the selected lesson (title, type, content text rendered safely, and a\n * `FileLink` for any attached media) with Prev / Mark-complete / Next controls.\n * Opening a lesson records IN_PROGRESS once; \"Mark complete\" records COMPLETED;\n * finishing every lesson transitions the enrollment to COMPLETED. All data is\n * live — no fabrication.\n */\n\nimport type { FC } from 'react';\nimport { useEffect, useMemo, useRef, useState } from 'react';\nimport { useParams, useSearchParams } from 'react-router';\nimport type { LucideIcon } from 'lucide-react';\nimport {\n ArrowLeft,\n BookOpen,\n Check,\n CheckCircle2,\n ChevronLeft,\n ChevronRight,\n FileText,\n HelpCircle,\n Loader2,\n ListChecks,\n MousePointerClick,\n PenLine,\n PlayCircle,\n Radio,\n Trophy,\n} from 'lucide-react';\nimport { Button, GlassCard } from '@burdenoff/fe-libs/ui';\nimport {\n PageLayout,\n StatusBadge,\n MiniBar,\n ProgressRing,\n LoadingView,\n ErrorView,\n EmptyState,\n FileLink,\n OfflineLessonControl,\n TutorPanel,\n} from '../../components';\nimport { useBuildmyiq } from '../../context';\nimport {\n useBuiMyEnrollment,\n useBuiCourse,\n useBuiRecordMyLessonProgress,\n useBuiCompleteMyEnrollment,\n} from '../../hooks';\nimport { useTr } from '../../shared/hooks/useTr';\nimport { cn } from '../../utils/cn';\nimport { formatDuration, formatEnumLabel } from '../../utils/formatters';\nimport type { BuiLesson, BuiLessonType, BuiModule, BuiProgressStatus } from '../../types';\n\ninterface LessonGroup {\n id: string;\n title: string;\n lessons: BuiLesson[];\n}\n\nconst LESSON_ICON: Record<BuiLessonType, LucideIcon> = {\n ARTICLE: FileText,\n VIDEO: PlayCircle,\n QUIZ: HelpCircle,\n ASSIGNMENT: PenLine,\n INTERACTIVE: MousePointerClick,\n LIVE_SESSION: Radio,\n};\n\nconst sortByPosition = <T extends { position: number }>(items: T[]): T[] =>\n [...items].sort((a, b) => a.position - b.position);\n\nexport const LessonPlayerPage: FC = () => {\n const { enrollmentId } = useParams<{ enrollmentId: string }>();\n // `?lesson=<id>` deep-links straight to one lesson (used by the \"what's next\"\n // suggestion on My Learning). Purely an INITIAL anchor — once the learner\n // picks a lesson themselves, their pick wins.\n const [searchParams] = useSearchParams();\n const requestedLessonId = searchParams.get('lesson');\n const { navigateTo } = useBuildmyiq();\n const tr = useTr();\n\n const enrollmentQuery = useBuiMyEnrollment(enrollmentId);\n const enrollment = enrollmentQuery.data ?? null;\n const courseId = enrollment?.courseId;\n const courseQuery = useBuiCourse(courseId);\n const course = courseQuery.data ?? null;\n\n const recordProgress = useBuiRecordMyLessonProgress();\n const completeEnrollment = useBuiCompleteMyEnrollment();\n\n // react-query's mutate is referentially stable across renders, so it can be\n // used directly in effects without a render-time ref write.\n const recordMutate = recordProgress.mutate;\n const autoRecordedRef = useRef<Set<string>>(new Set());\n\n const [userSelectedId, setUserSelectedId] = useState<string | null>(null);\n const [showTree, setShowTree] = useState(false);\n // Error from the last \"Mark complete\" / \"Finish course\" attempt. Without it a\n // rejected mutation surfaced only as an unhandled promise rejection and the\n // button silently did nothing.\n const [actionError, setActionError] = useState<string | null>(null);\n\n // Grouped tree (modules → lessons) plus any standalone lessons, de-duplicated.\n const groups = useMemo<LessonGroup[]>(() => {\n if (!course) return [];\n const seen = new Set<string>();\n const modules: BuiModule[] = sortByPosition(course.modules ?? []);\n const result: LessonGroup[] = [];\n for (const m of modules) {\n const lessons = sortByPosition(m.lessons ?? []).filter((l) => {\n if (seen.has(l.id)) return false;\n seen.add(l.id);\n return true;\n });\n if (lessons.length > 0) result.push({ id: m.id, title: m.title, lessons });\n }\n const standalone = sortByPosition(course.lessons ?? []).filter((l) => {\n if (seen.has(l.id)) return false;\n seen.add(l.id);\n return true;\n });\n if (standalone.length > 0) {\n result.push({\n id: '__standalone',\n title: tr('common.lessons', 'Lessons'),\n lessons: standalone,\n });\n }\n return result;\n }, [course, tr]);\n\n // Flat, ordered lesson list for prev/next + completion checks.\n const orderedLessons = useMemo<BuiLesson[]>(() => groups.flatMap((g) => g.lessons), [groups]);\n // Lesson id → title for this course, so the tutor panel can NAME the lessons\n // its answers were grounded in instead of showing opaque ids.\n const lessonTitlesById = useMemo<ReadonlyMap<string, string>>(\n () => new Map(orderedLessons.map((lesson) => [lesson.id, lesson.title])),\n [orderedLessons]\n );\n\n const progressByLesson = useMemo(() => {\n const map = new Map<string, BuiProgressStatus>();\n for (const p of enrollment?.lessonProgress ?? []) map.set(p.lessonId, p.status);\n return map;\n }, [enrollment]);\n\n // Resolve the active lesson as DERIVED state: the user's pick if it still\n // exists in the course, otherwise the first lesson. Avoids a setState-in-\n // effect re-anchor loop.\n const selectedLessonId = useMemo<string | null>(() => {\n if (orderedLessons.length === 0) return null;\n if (userSelectedId && orderedLessons.some((l) => l.id === userSelectedId)) {\n return userSelectedId;\n }\n if (requestedLessonId && orderedLessons.some((l) => l.id === requestedLessonId)) {\n return requestedLessonId;\n }\n return orderedLessons[0].id;\n }, [orderedLessons, userSelectedId, requestedLessonId]);\n\n // Record IN_PROGRESS once when a not-yet-started lesson is opened.\n useEffect(() => {\n if (!enrollmentId || !selectedLessonId) return;\n const status = progressByLesson.get(selectedLessonId);\n if (status === 'COMPLETED' || status === 'IN_PROGRESS') return;\n if (autoRecordedRef.current.has(selectedLessonId)) return;\n autoRecordedRef.current.add(selectedLessonId);\n recordMutate({\n enrollmentId,\n lessonId: selectedLessonId,\n status: 'IN_PROGRESS',\n });\n }, [enrollmentId, selectedLessonId, progressByLesson, recordMutate]);\n\n const selectedIndex = orderedLessons.findIndex((l) => l.id === selectedLessonId);\n const selectedLesson = selectedIndex >= 0 ? orderedLessons[selectedIndex] : null;\n const selectedStatus = selectedLesson ? progressByLesson.get(selectedLesson.id) : undefined;\n const isSelectedComplete = selectedStatus === 'COMPLETED';\n\n const completedCount = orderedLessons.filter(\n (l) => progressByLesson.get(l.id) === 'COMPLETED'\n ).length;\n const allComplete = orderedLessons.length > 0 && completedCount === orderedLessons.length;\n const alreadyFinished = enrollment?.status === 'COMPLETED';\n\n const handleSelect = (lessonId: string) => {\n setUserSelectedId(lessonId);\n setShowTree(false);\n setActionError(null);\n };\n\n const goTo = (offset: number) => {\n if (selectedIndex < 0) return;\n const next = orderedLessons[selectedIndex + offset];\n if (next) handleSelect(next.id);\n };\n\n const handleMarkComplete = async () => {\n if (!enrollmentId || !selectedLesson) return;\n setActionError(null);\n try {\n await recordProgress.mutateAsync({\n enrollmentId,\n lessonId: selectedLesson.id,\n status: 'COMPLETED',\n });\n } catch (err) {\n setActionError(\n err instanceof Error && err.message\n ? err.message\n : tr('buildmyiq.lessonPlayer.markCompleteError', 'Could not mark this lesson complete.')\n );\n return;\n }\n // Advance to the next incomplete lesson for a natural flow.\n const next = orderedLessons[selectedIndex + 1];\n if (next) setUserSelectedId(next.id);\n };\n\n const handleFinishCourse = async () => {\n if (!enrollmentId) return;\n setActionError(null);\n try {\n await completeEnrollment.mutateAsync({ id: enrollmentId });\n } catch (err) {\n setActionError(\n err instanceof Error && err.message\n ? err.message\n : tr('buildmyiq.lessonPlayer.finishCourseError', 'Could not finish this course.')\n );\n }\n };\n\n // ---- top-level states ----------------------------------------------------\n if (!enrollmentId) {\n return (\n <PageLayout title={tr('buildmyiq.lessonPlayer.pageTitle', 'Lesson')}>\n <EmptyState\n illustration=\"empty-data\"\n title={tr('buildmyiq.lessonPlayer.noEnrollmentTitle', 'No enrollment selected')}\n description={tr(\n 'buildmyiq.lessonPlayer.noEnrollmentDescription',\n 'Open a course from My Learning to start the lesson player.'\n )}\n action={\n <Button onClick={() => navigateTo('learning')}>\n {tr('buildmyiq.lessonPlayer.backToLearning', 'Back to My Learning')}\n </Button>\n }\n />\n </PageLayout>\n );\n }\n\n const isLoading = enrollmentQuery.isLoading || (Boolean(courseId) && courseQuery.isLoading);\n\n const backAction = (\n <Button variant=\"ghost\" size=\"sm\" onClick={() => navigateTo('learning')}>\n <ArrowLeft className=\"mr-1 h-4 w-4\" /> {tr('buildmyiq.nav.learning', 'My Learning')}\n </Button>\n );\n\n if (isLoading) {\n return (\n <PageLayout title={tr('buildmyiq.lessonPlayer.pageTitle', 'Lesson')} actions={backAction}>\n <LoadingView label={tr('buildmyiq.lessonPlayer.loading', 'Loading your lesson…')} />\n </PageLayout>\n );\n }\n\n if (enrollmentQuery.isError) {\n return (\n <PageLayout title={tr('buildmyiq.lessonPlayer.pageTitle', 'Lesson')} actions={backAction}>\n <ErrorView\n title={tr('buildmyiq.lessonPlayer.enrollmentErrorTitle', \"Couldn't load this enrollment\")}\n message={\n enrollmentQuery.error instanceof Error ? enrollmentQuery.error.message : undefined\n }\n onRetry={() => enrollmentQuery.refetch()}\n />\n </PageLayout>\n );\n }\n\n if (!enrollment) {\n return (\n <PageLayout title={tr('buildmyiq.lessonPlayer.pageTitle', 'Lesson')} actions={backAction}>\n <EmptyState\n illustration=\"empty-data\"\n title={tr('buildmyiq.lessonPlayer.enrollmentNotFoundTitle', 'Enrollment not found')}\n description={tr(\n 'buildmyiq.lessonPlayer.enrollmentNotFoundDescription',\n 'This enrollment may have been removed or is not available to you.'\n )}\n action={\n <Button onClick={() => navigateTo('learning')}>\n {tr('buildmyiq.lessonPlayer.backToLearning', 'Back to My Learning')}\n </Button>\n }\n />\n </PageLayout>\n );\n }\n\n if (courseQuery.isError) {\n return (\n <PageLayout title={tr('buildmyiq.lessonPlayer.pageTitle', 'Lesson')} actions={backAction}>\n <ErrorView\n title={tr('buildmyiq.lessonPlayer.courseErrorTitle', \"Couldn't load the course\")}\n message={courseQuery.error instanceof Error ? courseQuery.error.message : undefined}\n onRetry={() => courseQuery.refetch()}\n />\n </PageLayout>\n );\n }\n\n const progressPct = Math.round(enrollment.progressPct ?? 0);\n const SelectedIcon = selectedLesson ? LESSON_ICON[selectedLesson.type] : FileText;\n\n return (\n <PageLayout\n title={course?.title ?? tr('buildmyiq.lessonPlayer.pageTitle', 'Lesson')}\n actions={backAction}\n >\n <div className=\"flex flex-col gap-5\">\n {/* Header progress — ProgressRing hero + completion bar */}\n <GlassCard\n treatment=\"glass\"\n glow={progressPct > 0}\n className=\"flex flex-col gap-4 p-4 sm:flex-row sm:items-center sm:gap-5\"\n >\n <ProgressRing value={progressPct} size={64} className=\"shrink-0\" />\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <StatusBadge status={enrollment.status} />\n <span className=\"text-sm text-text-secondary tabular-nums\">\n {tr(\n 'buildmyiq.lessonPlayer.progressSummary',\n '{{completed}} of {{total}} lessons complete',\n { completed: completedCount, total: orderedLessons.length }\n )}\n </span>\n </div>\n <MiniBar value={progressPct} className=\"mt-3\" />\n </div>\n {allComplete && !alreadyFinished && (\n <Button\n className=\"shrink-0 max-sm:w-full\"\n onClick={handleFinishCourse}\n disabled={completeEnrollment.isPending}\n >\n {completeEnrollment.isPending ? (\n <Loader2 className=\"mr-1 h-4 w-4 animate-spin\" />\n ) : (\n <Trophy className=\"mr-1 h-4 w-4\" />\n )}\n {tr('buildmyiq.lessonPlayer.finishCourse', 'Finish course')}\n </Button>\n )}\n </GlassCard>\n\n {actionError && (\n <p role=\"alert\" className=\"text-sm text-status-error-text\">\n {actionError}\n </p>\n )}\n\n {/* Mobile tree toggle — collapses the outline into a top sheet */}\n <Button\n variant=\"secondary\"\n size=\"sm\"\n className=\"w-full lg:hidden\"\n onClick={() => setShowTree((v) => !v)}\n >\n <ListChecks className=\"mr-1 h-4 w-4\" />\n {showTree\n ? tr('buildmyiq.lessonPlayer.hideLessons', 'Hide lessons')\n : tr('buildmyiq.lessonPlayer.showLessons', 'Lessons ({{completed}}/{{total}})', {\n completed: completedCount,\n total: orderedLessons.length,\n })}\n </Button>\n\n <div className=\"flex flex-col gap-5 lg:flex-row\">\n {/* Left: module / lesson completion timeline */}\n <aside className={cn('lg:block lg:w-80 lg:shrink-0', showTree ? 'block' : 'hidden')}>\n <GlassCard treatment=\"plain\" className=\"overflow-hidden p-0 lg:sticky lg:top-4\">\n {orderedLessons.length === 0 ? (\n <div className=\"p-4 text-sm text-text-secondary\">\n {tr('buildmyiq.lessonPlayer.noLessons', 'This course has no lessons yet.')}\n </div>\n ) : (\n <>\n <nav className=\"max-h-[68vh] overflow-auto p-3\">\n {groups.map((group) => {\n const groupDone = group.lessons.filter(\n (l) => progressByLesson.get(l.id) === 'COMPLETED'\n ).length;\n return (\n <div key={group.id} className=\"mb-4 last:mb-0\">\n <div className=\"mb-2 flex items-center justify-between gap-2 px-1\">\n <p className=\"min-w-0 truncate text-sm font-semibold text-text-primary\">\n {group.title}\n </p>\n <span className=\"shrink-0 text-xs font-medium tabular-nums text-text-secondary\">\n {groupDone} / {group.lessons.length}\n </span>\n </div>\n <ul className=\"flex flex-col\">\n {group.lessons.map((lesson, i) => {\n const status = progressByLesson.get(lesson.id);\n const done = status === 'COMPLETED';\n const active = lesson.id === selectedLessonId;\n const isLast = i === group.lessons.length - 1;\n const Icon = LESSON_ICON[lesson.type];\n return (\n <li key={lesson.id} className=\"flex gap-3\">\n {/* Timeline node + connector */}\n <div className=\"flex flex-col items-center pt-1.5\">\n {done ? (\n <span className=\"flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-[var(--color-status-success-text)]\">\n <Check className=\"h-3.5 w-3.5 text-status-success-bg-subtle\" />\n </span>\n ) : active ? (\n <span className=\"flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-[var(--color-accent-soft)] ring-2 ring-[var(--color-accent-teal)]\">\n <Icon className=\"h-3.5 w-3.5 text-[var(--color-accent-teal)]\" />\n </span>\n ) : (\n <span className=\"flex h-6 w-6 shrink-0 items-center justify-center rounded-full border border-border-seam bg-bg-sunken\">\n <Icon className=\"h-3.5 w-3.5 text-text-secondary opacity-70\" />\n </span>\n )}\n {!isLast && (\n <span\n className={cn(\n 'my-1 w-px flex-1',\n done\n ? 'bg-[color-mix(in_srgb,var(--color-status-success-text)_45%,transparent)]'\n : 'bg-border-seam'\n )}\n />\n )}\n </div>\n <button\n type=\"button\"\n onClick={() => handleSelect(lesson.id)}\n className={cn(\n 'mb-1 min-w-0 flex-1 rounded-lg px-3 py-2 text-left transition-colors',\n active\n ? 'bg-[var(--color-accent-soft)]'\n : 'hover:bg-bg-sunken'\n )}\n aria-current={active ? 'true' : undefined}\n >\n <span\n className={cn(\n 'block truncate text-sm',\n active\n ? 'font-medium text-text-primary'\n : done\n ? 'text-text-secondary'\n : 'text-text-primary'\n )}\n >\n {lesson.title}\n </span>\n <span className=\"mt-0.5 block truncate text-xs text-text-secondary\">\n {formatEnumLabel(lesson.type)}\n {lesson.durationMin > 0\n ? ` · ${formatDuration(lesson.durationMin)}`\n : ''}\n </span>\n </button>\n </li>\n );\n })}\n </ul>\n </div>\n );\n })}\n </nav>\n {courseId && (\n <div className=\"border-t border-border-seam p-3\">\n <Button\n variant=\"outline\"\n size=\"sm\"\n className=\"w-full\"\n onClick={() => navigateTo(`catalog/${courseId}`)}\n >\n <BookOpen className=\"mr-1.5 h-4 w-4\" />{' '}\n {tr('buildmyiq.lessonPlayer.courseOverview', 'Course overview')}\n </Button>\n </div>\n )}\n </>\n )}\n </GlassCard>\n </aside>\n\n {/* Right: selected lesson */}\n <main className=\"min-w-0 flex-1\">\n {!selectedLesson ? (\n <EmptyState\n illustration=\"empty-data\"\n title={tr('buildmyiq.lessonPlayer.noLessonTitle', 'No lesson to show')}\n description={tr(\n 'buildmyiq.lessonPlayer.noLessonDescription',\n \"This course doesn't have any lessons yet. Check back once the author adds content.\"\n )}\n />\n ) : (\n <GlassCard treatment=\"plain\" className=\"flex flex-col gap-4 p-5 sm:p-7\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <span className=\"inline-flex items-center gap-1.5 rounded-full bg-[var(--color-accent-soft)] px-2.5 py-1 text-xs font-medium text-text-primary\">\n <SelectedIcon className=\"h-3.5 w-3.5 text-[var(--color-accent-teal)]\" />\n {formatEnumLabel(selectedLesson.type)}\n </span>\n {selectedLesson.durationMin > 0 && (\n <span className=\"text-xs text-text-secondary\">\n {formatDuration(selectedLesson.durationMin)}\n </span>\n )}\n {isSelectedComplete && (\n <span className=\"inline-flex items-center gap-1 text-xs font-medium text-status-success-text\">\n <CheckCircle2 className=\"h-3.5 w-3.5\" />{' '}\n {tr('buildmyiq.lessonPlayer.completedLabel', 'Completed')}\n </span>\n )}\n <OfflineLessonControl key={selectedLesson.id} lesson={selectedLesson} />\n <span className=\"ml-auto text-xs tabular-nums text-text-secondary\">\n {tr('buildmyiq.lessonPlayer.lessonPosition', 'Lesson {{n}} of {{total}}', {\n n: selectedIndex + 1,\n total: orderedLessons.length,\n })}\n </span>\n </div>\n\n <h2 className=\"text-2xl font-semibold tracking-tight text-text-primary\">\n {selectedLesson.title}\n </h2>\n\n {selectedLesson.media ? (\n <div className=\"rounded-xl border border-border-seam bg-bg-sunken p-3\">\n <p className=\"mb-2 text-xs font-semibold uppercase tracking-wide text-text-secondary\">\n {tr('buildmyiq.lessonPlayer.resources', 'Resources')}\n </p>\n <FileLink fileId={selectedLesson.media.id} />\n </div>\n ) : null}\n\n {selectedLesson.content ? (\n <p className=\"max-w-prose whitespace-pre-wrap text-[15px] leading-7 text-text-primary\">\n {selectedLesson.content}\n </p>\n ) : (\n <p className=\"text-sm text-text-secondary\">\n {selectedLesson.media\n ? tr(\n 'buildmyiq.lessonPlayer.noContentWithMedia',\n 'This lesson has no written content beyond the attached media.'\n )\n : tr(\n 'buildmyiq.lessonPlayer.noContent',\n 'This lesson has no written content'\n )}\n </p>\n )}\n\n {/* Actions — Prev · Mark complete · Next */}\n <div className=\"mt-2 flex flex-col gap-3 border-t border-border-seam pt-5 sm:flex-row sm:items-center sm:justify-between\">\n <Button\n variant=\"outline\"\n size=\"sm\"\n className=\"max-sm:w-full\"\n onClick={() => goTo(-1)}\n disabled={selectedIndex <= 0}\n >\n <ChevronLeft className=\"mr-1 h-4 w-4\" />{' '}\n {tr('buildmyiq.lessonPlayer.previous', 'Previous')}\n </Button>\n\n {isSelectedComplete ? (\n <span className=\"inline-flex items-center justify-center gap-2 rounded-lg bg-status-success-bg-subtle px-4 py-2 text-sm font-medium text-status-success-text max-sm:w-full\">\n <CheckCircle2 className=\"h-4 w-4\" />{' '}\n {tr('buildmyiq.lessonPlayer.lessonComplete', 'Lesson complete')}\n </span>\n ) : (\n <Button\n size=\"lg\"\n className=\"max-sm:w-full\"\n onClick={handleMarkComplete}\n disabled={recordProgress.isPending}\n >\n {recordProgress.isPending ? (\n <Loader2 className=\"mr-1.5 h-4 w-4 animate-spin\" />\n ) : (\n <CheckCircle2 className=\"mr-1.5 h-4 w-4\" />\n )}\n {recordProgress.isPending\n ? tr('buildmyiq.lessonPlayer.markingComplete', 'Marking complete…')\n : tr('buildmyiq.lessonPlayer.markComplete', 'Mark complete')}\n </Button>\n )}\n\n <Button\n variant=\"outline\"\n size=\"sm\"\n className=\"max-sm:w-full\"\n onClick={() => goTo(1)}\n disabled={selectedIndex >= orderedLessons.length - 1}\n >\n {tr('buildmyiq.lessonPlayer.next', 'Next')}{' '}\n <ChevronRight className=\"ml-1 h-4 w-4\" />\n </Button>\n </div>\n </GlassCard>\n )}\n\n {selectedLesson && courseId && enrollmentId && (\n <div className=\"mt-5\">\n {/* Course changes reset local state; lesson navigation keeps the course conversation. */}\n <TutorPanel\n key={courseId}\n courseId={courseId}\n enrollmentId={enrollmentId}\n lessonId={selectedLesson.id}\n lessonTitle={selectedLesson.title}\n lessonTitlesById={lessonTitlesById}\n />\n </div>\n )}\n </main>\n </div>\n </div>\n </PageLayout>\n );\n};\n"],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { CourseCard } from '../buildmyiq/pages/_shared/CourseCard';
|
|
3
|
+
declare const meta: Meta<typeof CourseCard>;
|
|
4
|
+
export default meta;
|
|
5
|
+
type Story = StoryObj<typeof meta>;
|
|
6
|
+
export declare const Catalog: Story;
|
|
7
|
+
export declare const Authoring: Story;
|
|
8
|
+
export declare const SparseCourse: Story;
|
|
9
|
+
//# sourceMappingURL=CourseCard.stories.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CourseCard.stories.d.ts","sourceRoot":"","sources":["../../src/stories/CourseCard.stories.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAEvD,OAAO,EAAE,UAAU,EAAE,MAAM,uCAAuC,CAAC;AAGnE,QAAA,MAAM,IAAI,EAAE,IAAI,CAAC,OAAO,UAAU,CAajC,CAAC;AAEF,eAAe,IAAI,CAAC;AAEpB,KAAK,KAAK,GAAG,QAAQ,CAAC,OAAO,IAAI,CAAC,CAAC;AAEnC,eAAO,MAAM,OAAO,EAAE,KAmBrB,CAAC;AAEF,eAAO,MAAM,SAAS,EAAE,KAUvB,CAAC;AAEF,eAAO,MAAM,YAAY,EAAE,KAa1B,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EmptyState.stories.d.ts","sourceRoot":"","sources":["../../src/stories/EmptyState.stories.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"EmptyState.stories.d.ts","sourceRoot":"","sources":["../../src/stories/EmptyState.stories.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAGvD,OAAO,EAAE,UAAU,EAAE,MAAM,oCAAoC,CAAC;AAEhE,QAAA,MAAM,IAAI,EAAE,IAAI,CAAC,OAAO,UAAU,CAMjC,CAAC;AAEF,eAAe,IAAI,CAAC;AAEpB,KAAK,KAAK,GAAG,QAAQ,CAAC,OAAO,IAAI,CAAC,CAAC;AAEnC,eAAO,MAAM,QAAQ,EAAE,KActB,CAAC;AAEF,eAAO,MAAM,UAAU,EAAE,KAoBxB,CAAC;AAEF,eAAO,MAAM,WAAW,EAAE,KAazB,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
type EnrollmentFilter = 'ALL' | 'IN_PROGRESS' | 'COMPLETED';
|
|
3
|
+
interface StatefulFilterTabsProps {
|
|
4
|
+
initial: EnrollmentFilter;
|
|
5
|
+
onChange: (value: EnrollmentFilter) => void;
|
|
6
|
+
}
|
|
7
|
+
/** FilterTabs is controlled; this wrapper owns the state like a real page does. */
|
|
8
|
+
declare function StatefulFilterTabs({ initial, onChange }: StatefulFilterTabsProps): import("react").JSX.Element;
|
|
9
|
+
declare const meta: Meta<typeof StatefulFilterTabs>;
|
|
10
|
+
export default meta;
|
|
11
|
+
type Story = StoryObj<typeof meta>;
|
|
12
|
+
export declare const SwitchFilter: Story;
|
|
13
|
+
//# sourceMappingURL=FilterTabs.stories.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FilterTabs.stories.d.ts","sourceRoot":"","sources":["../../src/stories/FilterTabs.stories.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAIvD,KAAK,gBAAgB,GAAG,KAAK,GAAG,aAAa,GAAG,WAAW,CAAC;AAQ5D,UAAU,uBAAuB;IAC/B,OAAO,EAAE,gBAAgB,CAAC;IAC1B,QAAQ,EAAE,CAAC,KAAK,EAAE,gBAAgB,KAAK,IAAI,CAAC;CAC7C;AAED,mFAAmF;AACnF,iBAAS,kBAAkB,CAAC,EAAE,OAAO,EAAE,QAAQ,EAAE,EAAE,uBAAuB,+BAYzE;AAED,QAAA,MAAM,IAAI,EAAE,IAAI,CAAC,OAAO,kBAAkB,CAMzC,CAAC;AAEF,eAAe,IAAI,CAAC;AAEpB,KAAK,KAAK,GAAG,QAAQ,CAAC,OAAO,IAAI,CAAC,CAAC;AAEnC,eAAO,MAAM,YAAY,EAAE,KAmB1B,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MetricsCard.stories.d.ts","sourceRoot":"","sources":["../../src/stories/MetricsCard.stories.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"MetricsCard.stories.d.ts","sourceRoot":"","sources":["../../src/stories/MetricsCard.stories.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAGvD,OAAO,EAAE,WAAW,EAAE,MAAM,qCAAqC,CAAC;AAElE,QAAA,MAAM,IAAI,EAAE,IAAI,CAAC,OAAO,WAAW,CAMlC,CAAC;AAEF,eAAe,IAAI,CAAC;AAEpB,KAAK,KAAK,GAAG,QAAQ,CAAC,OAAO,IAAI,CAAC,CAAC;AAEnC,eAAO,MAAM,OAAO,EAAE,KAWrB,CAAC;AAEF,eAAO,MAAM,SAAS,EAAE,KAavB,CAAC;AAEF,eAAO,MAAM,QAAQ,EAAE,KAetB,CAAC;AAEF,eAAO,MAAM,QAAQ,EAAE,KAgBtB,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { Pagination } from '../buildmyiq/components/Pagination';
|
|
3
|
+
declare const meta: Meta<typeof Pagination>;
|
|
4
|
+
export default meta;
|
|
5
|
+
type Story = StoryObj<typeof meta>;
|
|
6
|
+
export declare const FirstPage: Story;
|
|
7
|
+
export declare const LastPage: Story;
|
|
8
|
+
export declare const SinglePageHidden: Story;
|
|
9
|
+
//# sourceMappingURL=Pagination.stories.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Pagination.stories.d.ts","sourceRoot":"","sources":["../../src/stories/Pagination.stories.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAEvD,OAAO,EAAE,UAAU,EAAE,MAAM,oCAAoC,CAAC;AAEhE,QAAA,MAAM,IAAI,EAAE,IAAI,CAAC,OAAO,UAAU,CAUjC,CAAC;AAEF,eAAe,IAAI,CAAC;AAEpB,KAAK,KAAK,GAAG,QAAQ,CAAC,OAAO,IAAI,CAAC,CAAC;AAEnC,eAAO,MAAM,SAAS,EAAE,KAevB,CAAC;AAEF,eAAO,MAAM,QAAQ,EAAE,KActB,CAAC;AAEF,eAAO,MAAM,gBAAgB,EAAE,KAW9B,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { ErrorView } from '../buildmyiq/components/StateViews';
|
|
3
|
+
declare const meta: Meta<typeof ErrorView>;
|
|
4
|
+
export default meta;
|
|
5
|
+
type Story = StoryObj<typeof meta>;
|
|
6
|
+
export declare const WithRetry: Story;
|
|
7
|
+
export declare const DefaultsWithoutRetry: Story;
|
|
8
|
+
//# sourceMappingURL=StateViews.stories.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"StateViews.stories.d.ts","sourceRoot":"","sources":["../../src/stories/StateViews.stories.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAEvD,OAAO,EAAE,SAAS,EAAE,MAAM,oCAAoC,CAAC;AAE/D,QAAA,MAAM,IAAI,EAAE,IAAI,CAAC,OAAO,SAAS,CAMhC,CAAC;AAEF,eAAe,IAAI,CAAC;AAEpB,KAAK,KAAK,GAAG,QAAQ,CAAC,OAAO,IAAI,CAAC,CAAC;AAEnC,eAAO,MAAM,SAAS,EAAE,KAcvB,CAAC;AAEF,eAAO,MAAM,oBAAoB,EAAE,KAUlC,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"StatusBadge.stories.d.ts","sourceRoot":"","sources":["../../src/stories/StatusBadge.stories.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"StatusBadge.stories.d.ts","sourceRoot":"","sources":["../../src/stories/StatusBadge.stories.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAEvD,OAAO,EAAE,WAAW,EAAE,MAAM,qCAAqC,CAAC;AAElE,QAAA,MAAM,IAAI,EAAE,IAAI,CAAC,OAAO,WAAW,CAYlC,CAAC;AAEF,eAAe,IAAI,CAAC;AAEpB,KAAK,KAAK,GAAG,QAAQ,CAAC,OAAO,IAAI,CAAC,CAAC;AAEnC,eAAO,MAAM,SAAS,EAAE,KAUvB,CAAC;AAEF,eAAO,MAAM,UAAU,EAAE,KAUxB,CAAC;AAEF,eAAO,MAAM,SAAS,EAAE,KAOvB,CAAC;AAEF,eAAO,MAAM,QAAQ,EAAE,KAOtB,CAAC;AAEF,eAAO,MAAM,SAAS,EAAE,KAOvB,CAAC;AAEF,eAAO,MAAM,sBAAsB,EAAE,KAQpC,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
type CohortSection = 'members' | 'schedule';
|
|
3
|
+
interface StatefulTabsProps {
|
|
4
|
+
onChange: (value: CohortSection) => void;
|
|
5
|
+
}
|
|
6
|
+
/** Tabs is presentational; the parent page owns the active section. */
|
|
7
|
+
declare function StatefulTabs({ onChange }: StatefulTabsProps): import("react").JSX.Element;
|
|
8
|
+
declare const meta: Meta<typeof StatefulTabs>;
|
|
9
|
+
export default meta;
|
|
10
|
+
type Story = StoryObj<typeof meta>;
|
|
11
|
+
export declare const SwitchSection: Story;
|
|
12
|
+
//# sourceMappingURL=Tabs.stories.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Tabs.stories.d.ts","sourceRoot":"","sources":["../../src/stories/Tabs.stories.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAKvD,KAAK,aAAa,GAAG,SAAS,GAAG,UAAU,CAAC;AAO5C,UAAU,iBAAiB;IACzB,QAAQ,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC;CAC1C;AAED,uEAAuE;AACvE,iBAAS,YAAY,CAAC,EAAE,QAAQ,EAAE,EAAE,iBAAiB,+BAepD;AAED,QAAA,MAAM,IAAI,EAAE,IAAI,CAAC,OAAO,YAAY,CAMnC,CAAC;AAEF,eAAe,IAAI,CAAC;AAEpB,KAAK,KAAK,GAAG,QAAQ,CAAC,OAAO,IAAI,CAAC,CAAC;AAEnC,eAAO,MAAM,aAAa,EAAE,KAgB3B,CAAC"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { BuiCourse, BuiSkill } from '../../buildmyiq/types';
|
|
2
|
+
export declare const skillFixture: BuiSkill;
|
|
3
|
+
export declare const courseFixture: BuiCourse;
|
|
4
|
+
/** A course with no modules, lessons, skill or duration — the sparse edge case. */
|
|
5
|
+
export declare const sparseCourseFixture: BuiCourse;
|
|
6
|
+
//# sourceMappingURL=courses.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"courses.d.ts","sourceRoot":"","sources":["../../../src/stories/fixtures/courses.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,KAAK,EAAE,SAAS,EAAwB,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAMvF,eAAO,MAAM,YAAY,EAAE,QAc1B,CAAC;AAuDF,eAAO,MAAM,aAAa,EAAE,SAsB3B,CAAC;AAEF,mFAAmF;AACnF,eAAO,MAAM,mBAAmB,EAAE,SAajC,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@burdenoff/microfe-buildmyiq",
|
|
3
|
-
"version": "2026.
|
|
3
|
+
"version": "2026.1030.1",
|
|
4
4
|
"description": "Burdenoff BuildMyIQ Microfrontend — education, learning & development",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
"build-storybook": "storybook build",
|
|
41
41
|
"test": "bun test",
|
|
42
42
|
"test:storybook": "test-storybook",
|
|
43
|
+
"test:storybook:ci": "bun scripts/test-storybook-static.ts",
|
|
43
44
|
"sanity": "bash scripts/build-limits.sh --gate bash -c 'bun run codegen && bun run lint && bun run format && bun run type:check && bun run build && bun run build-storybook'",
|
|
44
45
|
"schema:pull": "graphql-codegen --project schema-wspace && graphql-codegen --project schema-global",
|
|
45
46
|
"codegen": "graphql-codegen --project wspace && graphql-codegen --project global",
|