@burdenoff/microfe-technospam 2026.709.3 → 2026.709.4
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 +4 -4
- package/dist/technospam/TechnoSpamRoutes.d.ts.map +1 -1
- package/dist/technospam/TechnoSpamRoutes.js +100 -74
- package/dist/technospam/TechnoSpamRoutes.js.map +1 -1
- package/dist/technospam/components/PageLayout.d.ts.map +1 -1
- package/dist/technospam/components/PageLayout.js +26 -23
- package/dist/technospam/components/PageLayout.js.map +1 -1
- package/dist/technospam/components/TagInput.d.ts +18 -0
- package/dist/technospam/components/TagInput.d.ts.map +1 -0
- package/dist/technospam/components/TagInput.js +58 -0
- package/dist/technospam/components/TagInput.js.map +1 -0
- package/dist/technospam/components/TagStrip.d.ts +12 -0
- package/dist/technospam/components/TagStrip.d.ts.map +1 -0
- package/dist/technospam/components/TagStrip.js +20 -0
- package/dist/technospam/components/TagStrip.js.map +1 -0
- package/dist/technospam/components/index.d.ts +2 -0
- package/dist/technospam/components/index.d.ts.map +1 -1
- package/dist/technospam/components/index.js +2 -0
- package/dist/technospam/hooks/useNativeControls.d.ts +27 -0
- package/dist/technospam/hooks/useNativeControls.d.ts.map +1 -0
- package/dist/technospam/hooks/useNativeControls.js +18 -0
- package/dist/technospam/hooks/useNativeControls.js.map +1 -0
- package/dist/technospam/hooks/useTechnoSpamAnalytics.d.ts +1 -1
- package/dist/technospam/hooks/useTechnoSpamAnalytics.d.ts.map +1 -1
- package/dist/technospam/hooks/useTechnoSpamAnalytics.js.map +1 -1
- package/dist/technospam/hooks/useTechnoSpamPageView.d.ts +14 -0
- package/dist/technospam/hooks/useTechnoSpamPageView.d.ts.map +1 -0
- package/dist/technospam/hooks/useTechnoSpamPageView.js +26 -0
- package/dist/technospam/hooks/useTechnoSpamPageView.js.map +1 -0
- package/dist/technospam/index.js +1 -1
- package/dist/technospam/pages/AdminAdminDashboardPagePage.d.ts.map +1 -1
- package/dist/technospam/pages/AdminAdminDashboardPagePage.js +408 -399
- package/dist/technospam/pages/AdminAdminDashboardPagePage.js.map +1 -1
- package/dist/technospam/pages/ApplicationsProblemDetailsPagePage.d.ts.map +1 -1
- package/dist/technospam/pages/ApplicationsProblemDetailsPagePage.js +50 -55
- package/dist/technospam/pages/ApplicationsProblemDetailsPagePage.js.map +1 -1
- package/dist/technospam/pages/ApplicationsProblemsPagePage.js +25 -25
- package/dist/technospam/pages/CoursesCourseDetailsPagePage.d.ts.map +1 -1
- package/dist/technospam/pages/CoursesCourseDetailsPagePage.js +142 -144
- package/dist/technospam/pages/CoursesCourseDetailsPagePage.js.map +1 -1
- package/dist/technospam/pages/CoursesCoursesPagePage.js +30 -30
- package/dist/technospam/pages/HackathonsHackathonsPagePage.js +29 -29
- package/dist/technospam/pages/MentorshipMentorProfilePagePage.js +54 -54
- package/dist/technospam/pages/MentorshipMentorsPagePage.js +61 -61
- package/dist/technospam/pages/MentorshipSessionsPagePage.js +42 -42
- package/package.json +2 -2
|
@@ -1,95 +1,96 @@
|
|
|
1
1
|
import { useTechnoSpam as e } from "../providers/TechnoSpamProvider.js";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
2
|
+
import { useTechnoSpamAnalytics as t } from "../hooks/useTechnoSpamAnalytics.js";
|
|
3
|
+
import { cn as n } from "../utils/cn.js";
|
|
4
|
+
import { PageLayout as r } from "../components/PageLayout.js";
|
|
5
|
+
import { StatCard as i } from "../components/StatCard.js";
|
|
6
|
+
import { MediaCover as a, topicIcon as o } from "../components/MediaCover.js";
|
|
7
|
+
import { formatDuration as s, humanizeEnum as c } from "../utils/format.js";
|
|
8
|
+
import { EmptyView as l, ErrorView as u } from "../components/StateViews.js";
|
|
9
|
+
import { TagStrip as d } from "../components/TagStrip.js";
|
|
8
10
|
import "../components/index.js";
|
|
9
|
-
import { notify as
|
|
10
|
-
import { useCertificatesQuery as
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import { Badge as A, Button as j, GlassCard as M, Skeleton as N } from "@burdenoff/fe-libs/ui";
|
|
11
|
+
import { notify as f } from "../lib/toast.js";
|
|
12
|
+
import { useCertificatesQuery as p, useCourseQuery as ee, useEnroll as te, useIssueCertificate as ne, useUpdateProgress as m } from "../api/technospamApi.js";
|
|
13
|
+
import { useEffect as h } from "react";
|
|
14
|
+
import { Award as g, BookOpen as _, CheckCircle2 as v, Clock as y, Eye as b, FileText as x, Layers as S, Lock as C, PlayCircle as w, ShieldCheck as re, Sparkles as T, TrendingUp as E, Trophy as ie, Users as D } from "lucide-react";
|
|
15
|
+
import { useSearchParams as O } from "react-router-dom";
|
|
16
|
+
import { jsx as k, jsxs as A } from "react/jsx-runtime";
|
|
17
|
+
import { Button as j, GlassCard as M, Skeleton as N } from "@burdenoff/fe-libs/ui";
|
|
17
18
|
//#region src/technospam/pages/CoursesCourseDetailsPagePage.tsx
|
|
18
19
|
var P = {
|
|
19
20
|
VIDEO: {
|
|
20
|
-
icon:
|
|
21
|
+
icon: w,
|
|
21
22
|
chip: "bg-accent-primary/15 text-accent-primary",
|
|
22
23
|
badge: "border-accent-primary/25 bg-accent-primary/10 text-accent-primary"
|
|
23
24
|
},
|
|
24
25
|
ARTICLE: {
|
|
25
|
-
icon:
|
|
26
|
+
icon: x,
|
|
26
27
|
chip: "bg-accent-indigo/15 text-accent-indigo",
|
|
27
28
|
badge: "border-accent-indigo/25 bg-accent-indigo/10 text-accent-indigo"
|
|
28
29
|
},
|
|
29
30
|
INTERACTIVE: {
|
|
30
|
-
icon:
|
|
31
|
+
icon: T,
|
|
31
32
|
chip: "bg-accent-teal/15 text-accent-teal",
|
|
32
33
|
badge: "border-accent-teal/25 bg-accent-teal/10 text-accent-teal"
|
|
33
34
|
},
|
|
34
35
|
QUIZ: {
|
|
35
|
-
icon:
|
|
36
|
+
icon: v,
|
|
36
37
|
chip: "bg-accent-purple/15 text-accent-purple",
|
|
37
38
|
badge: "border-accent-purple/25 bg-accent-purple/10 text-accent-purple"
|
|
38
39
|
},
|
|
39
40
|
PROJECT: {
|
|
40
|
-
icon:
|
|
41
|
+
icon: S,
|
|
41
42
|
chip: "bg-status-warning-bg-subtle text-status-warning-text",
|
|
42
43
|
badge: "border-status-warning-border bg-status-warning-bg-subtle text-status-warning-text"
|
|
43
44
|
}
|
|
44
45
|
};
|
|
45
46
|
function F({ icon: e, children: t }) {
|
|
46
|
-
return /* @__PURE__ */
|
|
47
|
+
return /* @__PURE__ */ A("span", {
|
|
47
48
|
className: "inline-flex items-center gap-1 rounded-full bg-white/15 px-2.5 py-1 text-xs font-medium text-white ring-1 ring-white/25 backdrop-blur-sm",
|
|
48
|
-
children: [e ? /* @__PURE__ */
|
|
49
|
+
children: [e ? /* @__PURE__ */ k(e, { size: 12 }) : null, t]
|
|
49
50
|
});
|
|
50
51
|
}
|
|
51
|
-
function I({ lesson: e, index:
|
|
52
|
+
function I({ lesson: e, index: t, done: r }) {
|
|
52
53
|
let i = P[e.kind] ?? P.ARTICLE, a = i.icon;
|
|
53
|
-
return /* @__PURE__ */
|
|
54
|
+
return /* @__PURE__ */ A("div", {
|
|
54
55
|
className: "group flex items-center gap-3 rounded-xl px-2 py-3 transition-colors hover:bg-bg-sunken/60",
|
|
55
56
|
children: [
|
|
56
|
-
/* @__PURE__ */
|
|
57
|
+
/* @__PURE__ */ k("span", {
|
|
57
58
|
className: "hidden w-6 shrink-0 text-center text-sm font-medium tabular-nums text-text-muted sm:block",
|
|
58
|
-
children:
|
|
59
|
+
children: t + 1
|
|
59
60
|
}),
|
|
60
|
-
/* @__PURE__ */
|
|
61
|
-
className:
|
|
62
|
-
children:
|
|
61
|
+
/* @__PURE__ */ k("span", {
|
|
62
|
+
className: n("flex h-9 w-9 shrink-0 items-center justify-center rounded-lg", r ? "bg-status-success-bg-subtle text-status-success-text" : i.chip),
|
|
63
|
+
children: k(r ? v : a, { size: 17 })
|
|
63
64
|
}),
|
|
64
|
-
/* @__PURE__ */
|
|
65
|
+
/* @__PURE__ */ A("div", {
|
|
65
66
|
className: "min-w-0 flex-1",
|
|
66
|
-
children: [/* @__PURE__ */
|
|
67
|
+
children: [/* @__PURE__ */ k("span", {
|
|
67
68
|
className: "block truncate text-sm font-medium text-text-primary",
|
|
68
69
|
children: e.title
|
|
69
|
-
}), /* @__PURE__ */
|
|
70
|
+
}), /* @__PURE__ */ A("div", {
|
|
70
71
|
className: "mt-1 flex flex-wrap items-center gap-2",
|
|
71
|
-
children: [/* @__PURE__ */
|
|
72
|
-
className:
|
|
73
|
-
children:
|
|
74
|
-
}), /* @__PURE__ */
|
|
72
|
+
children: [/* @__PURE__ */ k("span", {
|
|
73
|
+
className: n("inline-flex items-center rounded-full border px-2 py-0.5 text-[11px] font-medium", i.badge),
|
|
74
|
+
children: c(e.kind)
|
|
75
|
+
}), /* @__PURE__ */ A("span", {
|
|
75
76
|
className: "inline-flex items-center gap-1 text-xs text-text-muted",
|
|
76
77
|
children: [
|
|
77
|
-
/* @__PURE__ */
|
|
78
|
+
/* @__PURE__ */ k(y, { size: 12 }),
|
|
78
79
|
" ",
|
|
79
|
-
|
|
80
|
+
s(e.estimatedMinutes)
|
|
80
81
|
]
|
|
81
82
|
})]
|
|
82
83
|
})]
|
|
83
84
|
}),
|
|
84
|
-
/* @__PURE__ */
|
|
85
|
+
/* @__PURE__ */ k("div", {
|
|
85
86
|
className: "shrink-0",
|
|
86
|
-
children: r ? /* @__PURE__ */
|
|
87
|
+
children: r ? /* @__PURE__ */ k(v, {
|
|
87
88
|
size: 18,
|
|
88
89
|
className: "text-status-success-text"
|
|
89
|
-
}) : e.isPreview ? /* @__PURE__ */
|
|
90
|
+
}) : e.isPreview ? /* @__PURE__ */ A("span", {
|
|
90
91
|
className: "inline-flex items-center gap-1 rounded-full border border-accent-primary/30 bg-accent-primary/10 px-2 py-0.5 text-[11px] font-medium text-accent-primary",
|
|
91
|
-
children: [/* @__PURE__ */
|
|
92
|
-
}) : /* @__PURE__ */
|
|
92
|
+
children: [/* @__PURE__ */ k(b, { size: 12 }), " Preview"]
|
|
93
|
+
}) : /* @__PURE__ */ k(C, {
|
|
93
94
|
size: 15,
|
|
94
95
|
className: "text-text-muted"
|
|
95
96
|
})
|
|
@@ -98,41 +99,41 @@ function I({ lesson: e, index: n, done: r }) {
|
|
|
98
99
|
});
|
|
99
100
|
}
|
|
100
101
|
function L() {
|
|
101
|
-
let [
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
workspaceId:
|
|
105
|
-
entityId:
|
|
102
|
+
let [n] = O(), b = n.get("id") ?? "", { currentUser: C, workspaceId: w } = e(), { emit: T } = t(), { data: P, loading: L, error: R, refetch: z } = ee(b);
|
|
103
|
+
h(() => {
|
|
104
|
+
b && T("technospam.course.viewed", {
|
|
105
|
+
workspaceId: w,
|
|
106
|
+
entityId: b,
|
|
106
107
|
entityType: "TEC_COURSE"
|
|
107
108
|
});
|
|
108
109
|
}, [
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
110
|
+
b,
|
|
111
|
+
w,
|
|
112
|
+
T
|
|
112
113
|
]);
|
|
113
|
-
let [B, { loading: V }] =
|
|
114
|
-
learnerId:
|
|
115
|
-
courseId:
|
|
116
|
-
} : {}), [K, { loading: q }] =
|
|
117
|
-
!
|
|
118
|
-
workspaceId:
|
|
119
|
-
entityId:
|
|
114
|
+
let [B, { loading: V }] = te(), [H, { loading: U }] = m(), { data: W, refetch: G } = p(C?.id && b ? {
|
|
115
|
+
learnerId: C.id,
|
|
116
|
+
courseId: b
|
|
117
|
+
} : {}), [K, { loading: q }] = ne(), J = P?.tecCourse ?? null, Y = [...J?.lessons ?? []].sort((e, t) => e.position - t.position), X = (J?.enrollments ?? []).find((e) => e.learnerId === C?.id), Z = (W?.tecListCertificates ?? []).find((e) => e.status === "ISSUED"), ae = !!X && X.progressPct >= 100, oe = async () => {
|
|
118
|
+
!b || !C?.id || (T("technospam.course.certificate_claimed", {
|
|
119
|
+
workspaceId: w,
|
|
120
|
+
entityId: b,
|
|
120
121
|
entityType: "TEC_COURSE"
|
|
121
122
|
}), (await K({ variables: {
|
|
122
|
-
courseId:
|
|
123
|
-
learnerId:
|
|
123
|
+
courseId: b,
|
|
124
|
+
learnerId: C.id,
|
|
124
125
|
score: 100
|
|
125
|
-
} })).data?.tecIssueCertificate && (
|
|
126
|
+
} })).data?.tecIssueCertificate && (f.success("Certificate issued", "Your verifiable certificate is ready in My Learning."), await G()));
|
|
126
127
|
}, se = async () => {
|
|
127
|
-
|
|
128
|
-
workspaceId:
|
|
129
|
-
entityId:
|
|
128
|
+
b && (T("technospam.course.enroll_clicked", {
|
|
129
|
+
workspaceId: w,
|
|
130
|
+
entityId: b,
|
|
130
131
|
entityType: "TEC_COURSE"
|
|
131
|
-
}), (await B({ variables: { input: { courseId:
|
|
132
|
-
workspaceId:
|
|
133
|
-
entityId:
|
|
132
|
+
}), (await B({ variables: { input: { courseId: b } } })).data?.tecEnroll && (T("technospam.course.enrolled", {
|
|
133
|
+
workspaceId: w,
|
|
134
|
+
entityId: b,
|
|
134
135
|
entityType: "TEC_COURSE"
|
|
135
|
-
}),
|
|
136
|
+
}), f.success("Enrolled", `You're enrolled in "${J?.title}".`), await z()));
|
|
136
137
|
}, ce = async () => {
|
|
137
138
|
if (!X) return;
|
|
138
139
|
let e = Math.min(Y.length, X.lessonsCompleted + 1), t = Y.length ? Math.round(e / Y.length * 100) : 100;
|
|
@@ -140,77 +141,77 @@ function L() {
|
|
|
140
141
|
enrollmentId: X.id,
|
|
141
142
|
lessonsCompleted: e,
|
|
142
143
|
progressPct: t
|
|
143
|
-
} } })).data?.tecUpdateProgress && (
|
|
144
|
-
workspaceId:
|
|
144
|
+
} } })).data?.tecUpdateProgress && (T("technospam.course.progress_updated", {
|
|
145
|
+
workspaceId: w,
|
|
145
146
|
entityId: X.id,
|
|
146
147
|
entityType: "TEC_ENROLLMENT",
|
|
147
148
|
progressPct: t
|
|
148
|
-
}),
|
|
149
|
+
}), f.success("Progress saved", `${t}% complete`), await z());
|
|
149
150
|
};
|
|
150
|
-
if (L && !J) return /* @__PURE__ */
|
|
151
|
+
if (L && !J) return /* @__PURE__ */ A(r, {
|
|
151
152
|
title: "Course",
|
|
152
153
|
backPath: "/courses/courses-page",
|
|
153
|
-
children: [/* @__PURE__ */
|
|
154
|
+
children: [/* @__PURE__ */ k(N, { className: "mb-4 h-40 w-full rounded-2xl" }), /* @__PURE__ */ k(N, { className: "h-64 w-full rounded-xl" })]
|
|
154
155
|
});
|
|
155
|
-
if (R || !J) return /* @__PURE__ */
|
|
156
|
+
if (R || !J) return /* @__PURE__ */ k(r, {
|
|
156
157
|
title: "Course",
|
|
157
158
|
backPath: "/courses/courses-page",
|
|
158
|
-
children: R ? /* @__PURE__ */
|
|
159
|
+
children: R ? /* @__PURE__ */ k(u, {
|
|
159
160
|
title: "Couldn't load course",
|
|
160
161
|
error: R
|
|
161
|
-
}) : /* @__PURE__ */
|
|
162
|
+
}) : /* @__PURE__ */ k(l, {
|
|
162
163
|
title: "Course not found",
|
|
163
164
|
description: "This course may have been unpublished."
|
|
164
165
|
})
|
|
165
166
|
});
|
|
166
167
|
let Q = X?.lessonsCompleted ?? 0, $ = Math.max(0, Y.length - Q), le = X && X.progressPct >= 100 ? "Course complete — claim your certificate below." : $ === 1 ? "1 lesson to go — almost there!" : `${$} lessons to go — keep it up!`, ue = `${J.topic ?? ""} ${J.title}`;
|
|
167
|
-
return /* @__PURE__ */
|
|
168
|
+
return /* @__PURE__ */ A(r, {
|
|
168
169
|
backPath: "/courses/courses-page",
|
|
169
170
|
title: J.title,
|
|
170
171
|
description: J.summary ?? void 0,
|
|
171
|
-
actions: X ? /* @__PURE__ */
|
|
172
|
+
actions: X ? /* @__PURE__ */ A(j, {
|
|
172
173
|
onClick: ce,
|
|
173
174
|
disabled: U || Q >= Y.length,
|
|
174
|
-
children: [/* @__PURE__ */
|
|
175
|
-
}) : /* @__PURE__ */
|
|
175
|
+
children: [/* @__PURE__ */ k(v, { size: 16 }), Q >= Y.length ? "Completed" : "Complete next lesson"]
|
|
176
|
+
}) : /* @__PURE__ */ A(j, {
|
|
176
177
|
onClick: se,
|
|
177
178
|
disabled: V,
|
|
178
179
|
children: [
|
|
179
|
-
/* @__PURE__ */
|
|
180
|
+
/* @__PURE__ */ k(_, { size: 16 }),
|
|
180
181
|
" ",
|
|
181
182
|
V ? "Enrolling…" : "Enroll"
|
|
182
183
|
]
|
|
183
184
|
}),
|
|
184
185
|
children: [
|
|
185
|
-
/* @__PURE__ */
|
|
186
|
+
/* @__PURE__ */ k(a, {
|
|
186
187
|
height: "h-40",
|
|
187
188
|
seed: ue,
|
|
188
|
-
icon:
|
|
189
|
+
icon: o(J.topic ?? J.title),
|
|
189
190
|
className: "mb-5 rounded-2xl",
|
|
190
|
-
children: /* @__PURE__ */
|
|
191
|
+
children: /* @__PURE__ */ A("div", {
|
|
191
192
|
className: "flex w-full flex-col gap-2",
|
|
192
193
|
children: [
|
|
193
|
-
/* @__PURE__ */
|
|
194
|
+
/* @__PURE__ */ k("span", {
|
|
194
195
|
className: "text-xs font-medium uppercase tracking-wide text-white/80",
|
|
195
196
|
children: J.topic ?? "Course"
|
|
196
197
|
}),
|
|
197
|
-
/* @__PURE__ */
|
|
198
|
+
/* @__PURE__ */ k("h2", {
|
|
198
199
|
className: "text-2xl font-bold leading-tight text-white drop-shadow-sm sm:text-3xl",
|
|
199
200
|
children: J.title
|
|
200
201
|
}),
|
|
201
|
-
/* @__PURE__ */
|
|
202
|
+
/* @__PURE__ */ A("div", {
|
|
202
203
|
className: "flex flex-wrap items-center gap-2",
|
|
203
204
|
children: [
|
|
204
|
-
/* @__PURE__ */
|
|
205
|
-
icon:
|
|
206
|
-
children:
|
|
205
|
+
/* @__PURE__ */ k(F, {
|
|
206
|
+
icon: E,
|
|
207
|
+
children: c(J.level)
|
|
207
208
|
}),
|
|
208
|
-
/* @__PURE__ */
|
|
209
|
-
icon:
|
|
210
|
-
children:
|
|
209
|
+
/* @__PURE__ */ k(F, {
|
|
210
|
+
icon: v,
|
|
211
|
+
children: c(J.status)
|
|
211
212
|
}),
|
|
212
|
-
J.topic ? /* @__PURE__ */
|
|
213
|
-
icon:
|
|
213
|
+
J.topic ? /* @__PURE__ */ k(F, {
|
|
214
|
+
icon: S,
|
|
214
215
|
children: J.topic
|
|
215
216
|
}) : null
|
|
216
217
|
]
|
|
@@ -218,57 +219,54 @@ function L() {
|
|
|
218
219
|
]
|
|
219
220
|
})
|
|
220
221
|
}),
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
}, e))
|
|
227
|
-
}) : null,
|
|
228
|
-
/* @__PURE__ */ k("div", {
|
|
222
|
+
/* @__PURE__ */ k(d, {
|
|
223
|
+
tags: J.tags,
|
|
224
|
+
className: "mb-5"
|
|
225
|
+
}),
|
|
226
|
+
/* @__PURE__ */ A("div", {
|
|
229
227
|
className: "mb-6 grid grid-cols-2 gap-3 sm:grid-cols-4",
|
|
230
228
|
children: [
|
|
231
|
-
/* @__PURE__ */
|
|
229
|
+
/* @__PURE__ */ k(i, {
|
|
232
230
|
label: "Lessons",
|
|
233
231
|
value: Y.length,
|
|
234
|
-
icon:
|
|
232
|
+
icon: x,
|
|
235
233
|
tone: "blue"
|
|
236
234
|
}),
|
|
237
|
-
/* @__PURE__ */
|
|
235
|
+
/* @__PURE__ */ k(i, {
|
|
238
236
|
label: "Duration",
|
|
239
|
-
value:
|
|
240
|
-
icon:
|
|
237
|
+
value: s(J.estimatedMinutes),
|
|
238
|
+
icon: y,
|
|
241
239
|
tone: "teal"
|
|
242
240
|
}),
|
|
243
|
-
/* @__PURE__ */
|
|
241
|
+
/* @__PURE__ */ k(i, {
|
|
244
242
|
label: "Enrolled",
|
|
245
243
|
value: J.enrollments?.length ?? 0,
|
|
246
|
-
icon:
|
|
244
|
+
icon: D,
|
|
247
245
|
tone: "indigo"
|
|
248
246
|
}),
|
|
249
|
-
/* @__PURE__ */
|
|
247
|
+
/* @__PURE__ */ k(i, {
|
|
250
248
|
label: "Your progress",
|
|
251
249
|
value: X ? `${X.progressPct}%` : "Not started",
|
|
252
|
-
icon:
|
|
250
|
+
icon: E,
|
|
253
251
|
tone: "purple"
|
|
254
252
|
})
|
|
255
253
|
]
|
|
256
254
|
}),
|
|
257
|
-
X ? /* @__PURE__ */
|
|
255
|
+
X ? /* @__PURE__ */ A(M, {
|
|
258
256
|
className: "mb-6 p-5",
|
|
259
257
|
children: [
|
|
260
|
-
/* @__PURE__ */
|
|
258
|
+
/* @__PURE__ */ A("div", {
|
|
261
259
|
className: "mb-3 flex items-center gap-2",
|
|
262
260
|
children: [
|
|
263
|
-
/* @__PURE__ */
|
|
261
|
+
/* @__PURE__ */ k("span", {
|
|
264
262
|
className: "flex h-8 w-8 items-center justify-center rounded-lg bg-accent-primary/15 text-accent-primary",
|
|
265
|
-
children: /* @__PURE__ */
|
|
263
|
+
children: /* @__PURE__ */ k(E, { size: 16 })
|
|
266
264
|
}),
|
|
267
|
-
/* @__PURE__ */
|
|
265
|
+
/* @__PURE__ */ k("span", {
|
|
268
266
|
className: "text-sm font-semibold text-text-primary",
|
|
269
267
|
children: "Your progress"
|
|
270
268
|
}),
|
|
271
|
-
/* @__PURE__ */
|
|
269
|
+
/* @__PURE__ */ A("span", {
|
|
272
270
|
className: "ml-auto text-sm text-text-secondary",
|
|
273
271
|
children: [
|
|
274
272
|
Q,
|
|
@@ -281,32 +279,32 @@ function L() {
|
|
|
281
279
|
})
|
|
282
280
|
]
|
|
283
281
|
}),
|
|
284
|
-
/* @__PURE__ */
|
|
282
|
+
/* @__PURE__ */ k("div", {
|
|
285
283
|
className: "h-2.5 w-full overflow-hidden rounded-full bg-bg-sunken",
|
|
286
|
-
children: /* @__PURE__ */
|
|
284
|
+
children: /* @__PURE__ */ k("div", {
|
|
287
285
|
className: "h-full rounded-full bg-gradient-to-r from-accent-primary via-accent-indigo to-accent-purple transition-all duration-500",
|
|
288
286
|
style: { width: `${X.progressPct}%` }
|
|
289
287
|
})
|
|
290
288
|
}),
|
|
291
|
-
/* @__PURE__ */
|
|
289
|
+
/* @__PURE__ */ k("p", {
|
|
292
290
|
className: "mt-2 text-xs text-text-muted",
|
|
293
291
|
children: le
|
|
294
292
|
})
|
|
295
293
|
]
|
|
296
294
|
}) : null,
|
|
297
|
-
Z ? /* @__PURE__ */
|
|
295
|
+
Z ? /* @__PURE__ */ A(M, {
|
|
298
296
|
className: "mb-6 flex flex-wrap items-center gap-3 border border-status-success-border/60 bg-status-success-bg-subtle/30 p-4",
|
|
299
297
|
children: [
|
|
300
|
-
/* @__PURE__ */
|
|
298
|
+
/* @__PURE__ */ k("span", {
|
|
301
299
|
className: "flex h-11 w-11 shrink-0 items-center justify-center rounded-xl bg-status-warning-bg-subtle text-status-warning-text ring-1 ring-status-warning-border/30",
|
|
302
|
-
children: /* @__PURE__ */
|
|
300
|
+
children: /* @__PURE__ */ k(g, { size: 22 })
|
|
303
301
|
}),
|
|
304
|
-
/* @__PURE__ */
|
|
302
|
+
/* @__PURE__ */ A("div", {
|
|
305
303
|
className: "min-w-0 flex-1",
|
|
306
|
-
children: [/* @__PURE__ */
|
|
304
|
+
children: [/* @__PURE__ */ k("div", {
|
|
307
305
|
className: "text-sm font-semibold text-text-primary",
|
|
308
306
|
children: "Certificate earned"
|
|
309
|
-
}), /* @__PURE__ */
|
|
307
|
+
}), /* @__PURE__ */ A("div", {
|
|
310
308
|
className: "text-xs text-text-muted",
|
|
311
309
|
children: [
|
|
312
310
|
"Serial ",
|
|
@@ -316,46 +314,46 @@ function L() {
|
|
|
316
314
|
]
|
|
317
315
|
})]
|
|
318
316
|
}),
|
|
319
|
-
/* @__PURE__ */
|
|
317
|
+
/* @__PURE__ */ A("span", {
|
|
320
318
|
className: "inline-flex items-center gap-1 rounded-full border border-status-success-border bg-status-success-bg-subtle px-2.5 py-1 text-xs font-medium text-status-success-text",
|
|
321
|
-
children: [/* @__PURE__ */
|
|
319
|
+
children: [/* @__PURE__ */ k(re, { size: 13 }), " Verified"]
|
|
322
320
|
})
|
|
323
321
|
]
|
|
324
|
-
}) : ae ? /* @__PURE__ */
|
|
322
|
+
}) : ae ? /* @__PURE__ */ A(M, {
|
|
325
323
|
className: "mb-6 flex flex-col items-start gap-4 border border-status-success-border/60 bg-status-success-bg-subtle/40 p-5 sm:flex-row sm:items-center sm:justify-between",
|
|
326
|
-
children: [/* @__PURE__ */
|
|
324
|
+
children: [/* @__PURE__ */ A("div", {
|
|
327
325
|
className: "flex items-center gap-4",
|
|
328
|
-
children: [/* @__PURE__ */
|
|
326
|
+
children: [/* @__PURE__ */ k("span", {
|
|
329
327
|
className: "flex h-14 w-14 shrink-0 items-center justify-center rounded-2xl bg-status-warning-bg-subtle text-status-warning-text ring-1 ring-status-warning-border/40",
|
|
330
|
-
children: /* @__PURE__ */
|
|
331
|
-
}), /* @__PURE__ */
|
|
328
|
+
children: /* @__PURE__ */ k(ie, { size: 26 })
|
|
329
|
+
}), /* @__PURE__ */ A("div", { children: [/* @__PURE__ */ k("div", {
|
|
332
330
|
className: "text-base font-semibold text-text-primary",
|
|
333
331
|
children: "You finished this course"
|
|
334
|
-
}), /* @__PURE__ */
|
|
332
|
+
}), /* @__PURE__ */ k("div", {
|
|
335
333
|
className: "text-sm text-text-muted",
|
|
336
334
|
children: "Claim your verifiable certificate of completion."
|
|
337
335
|
})] })]
|
|
338
|
-
}), /* @__PURE__ */
|
|
336
|
+
}), /* @__PURE__ */ A(j, {
|
|
339
337
|
onClick: oe,
|
|
340
338
|
disabled: q,
|
|
341
339
|
children: [
|
|
342
|
-
/* @__PURE__ */
|
|
340
|
+
/* @__PURE__ */ k(g, { size: 16 }),
|
|
343
341
|
" ",
|
|
344
342
|
q ? "Issuing…" : "Claim certificate"
|
|
345
343
|
]
|
|
346
344
|
})]
|
|
347
345
|
}) : null,
|
|
348
|
-
/* @__PURE__ */
|
|
346
|
+
/* @__PURE__ */ A(M, {
|
|
349
347
|
className: "p-5",
|
|
350
|
-
children: [/* @__PURE__ */
|
|
348
|
+
children: [/* @__PURE__ */ A("div", {
|
|
351
349
|
className: "mb-3 flex items-center justify-between gap-2",
|
|
352
|
-
children: [/* @__PURE__ */
|
|
350
|
+
children: [/* @__PURE__ */ A("h2", {
|
|
353
351
|
className: "flex items-center gap-2 text-base font-semibold text-text-primary",
|
|
354
|
-
children: [/* @__PURE__ */
|
|
352
|
+
children: [/* @__PURE__ */ k(_, {
|
|
355
353
|
size: 18,
|
|
356
354
|
className: "text-accent-primary"
|
|
357
355
|
}), " Curriculum"]
|
|
358
|
-
}), Y.length ? /* @__PURE__ */
|
|
356
|
+
}), Y.length ? /* @__PURE__ */ A("span", {
|
|
359
357
|
className: "rounded-full bg-status-success-bg-subtle px-2.5 py-1 text-xs font-medium text-status-success-text",
|
|
360
358
|
children: [
|
|
361
359
|
Q,
|
|
@@ -364,12 +362,12 @@ function L() {
|
|
|
364
362
|
" completed"
|
|
365
363
|
]
|
|
366
364
|
}) : null]
|
|
367
|
-
}), Y.length === 0 ? /* @__PURE__ */
|
|
365
|
+
}), Y.length === 0 ? /* @__PURE__ */ k("p", {
|
|
368
366
|
className: "py-6 text-center text-sm text-text-muted",
|
|
369
367
|
children: "Lessons for this course are being prepared."
|
|
370
|
-
}) : /* @__PURE__ */
|
|
368
|
+
}) : /* @__PURE__ */ k("div", {
|
|
371
369
|
className: "flex flex-col",
|
|
372
|
-
children: Y.map((e, t) => /* @__PURE__ */
|
|
370
|
+
children: Y.map((e, t) => /* @__PURE__ */ k(I, {
|
|
373
371
|
lesson: e,
|
|
374
372
|
index: t,
|
|
375
373
|
done: t < Q
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CoursesCourseDetailsPagePage.js","names":[],"sources":["../../../src/technospam/pages/CoursesCourseDetailsPagePage.tsx"],"sourcesContent":["import type { ReactNode } from 'react';\nimport { useEffect } from 'react';\nimport { useSearchParams } from 'react-router-dom';\nimport {\n Award,\n BookOpen,\n CheckCircle2,\n Clock,\n Eye,\n FileText,\n Lock,\n PlayCircle,\n Layers,\n ShieldCheck,\n Sparkles,\n Trophy,\n TrendingUp,\n Users,\n type LucideIcon,\n} from 'lucide-react';\nimport { Badge, Button, GlassCard, Skeleton } from '@burdenoff/fe-libs/ui';\nimport {\n EmptyView,\n ErrorView,\n MediaCover,\n PageLayout,\n StatCard,\n StatusBadge,\n topicIcon,\n} from '../components';\nimport { useTechnoSpam } from '../providers/TechnoSpamProvider';\nimport {\n useCertificatesQuery,\n useCourseQuery,\n useEnroll,\n useIssueCertificate,\n useUpdateProgress,\n type TecLesson,\n type TecEnrollment,\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';\n\n/** Per-lesson-kind icon + on-theme tint (chip) and pill (badge) classes. */\nconst KIND_META: Record<string, { icon: LucideIcon; chip: string; badge: string }> = {\n VIDEO: {\n icon: PlayCircle,\n chip: 'bg-accent-primary/15 text-accent-primary',\n badge: 'border-accent-primary/25 bg-accent-primary/10 text-accent-primary',\n },\n ARTICLE: {\n icon: FileText,\n chip: 'bg-accent-indigo/15 text-accent-indigo',\n badge: 'border-accent-indigo/25 bg-accent-indigo/10 text-accent-indigo',\n },\n INTERACTIVE: {\n icon: Sparkles,\n chip: 'bg-accent-teal/15 text-accent-teal',\n badge: 'border-accent-teal/25 bg-accent-teal/10 text-accent-teal',\n },\n QUIZ: {\n icon: CheckCircle2,\n chip: 'bg-accent-purple/15 text-accent-purple',\n badge: 'border-accent-purple/25 bg-accent-purple/10 text-accent-purple',\n },\n PROJECT: {\n icon: Layers,\n chip: 'bg-status-warning-bg-subtle text-status-warning-text',\n badge: 'border-status-warning-border bg-status-warning-bg-subtle text-status-warning-text',\n },\n};\n\nfunction HeroBadge({ icon: Icon, children }: { icon?: LucideIcon; children: ReactNode }) {\n return (\n <span className=\"inline-flex items-center gap-1 rounded-full bg-white/15 px-2.5 py-1 text-xs font-medium text-white ring-1 ring-white/25 backdrop-blur-sm\">\n {Icon ? <Icon size={12} /> : null}\n {children}\n </span>\n );\n}\n\nfunction LessonRow({ lesson, index, done }: { lesson: TecLesson; index: number; done: boolean }) {\n const meta = KIND_META[lesson.kind] ?? KIND_META.ARTICLE!;\n const Icon = meta.icon;\n return (\n <div className=\"group flex items-center gap-3 rounded-xl px-2 py-3 transition-colors hover:bg-bg-sunken/60\">\n <span className=\"hidden w-6 shrink-0 text-center text-sm font-medium tabular-nums text-text-muted sm:block\">\n {index + 1}\n </span>\n <span\n className={cn(\n 'flex h-9 w-9 shrink-0 items-center justify-center rounded-lg',\n done ? 'bg-status-success-bg-subtle text-status-success-text' : meta.chip\n )}\n >\n {done ? <CheckCircle2 size={17} /> : <Icon size={17} />}\n </span>\n <div className=\"min-w-0 flex-1\">\n <span className=\"block truncate text-sm font-medium text-text-primary\">{lesson.title}</span>\n <div className=\"mt-1 flex flex-wrap items-center gap-2\">\n <span\n className={cn(\n 'inline-flex items-center rounded-full border px-2 py-0.5 text-[11px] font-medium',\n meta.badge\n )}\n >\n {humanizeEnum(lesson.kind)}\n </span>\n <span className=\"inline-flex items-center gap-1 text-xs text-text-muted\">\n <Clock size={12} /> {formatDuration(lesson.estimatedMinutes)}\n </span>\n </div>\n </div>\n <div className=\"shrink-0\">\n {done ? (\n <CheckCircle2 size={18} className=\"text-status-success-text\" />\n ) : lesson.isPreview ? (\n <span className=\"inline-flex items-center gap-1 rounded-full border border-accent-primary/30 bg-accent-primary/10 px-2 py-0.5 text-[11px] font-medium text-accent-primary\">\n <Eye size={12} /> Preview\n </span>\n ) : (\n <Lock size={15} className=\"text-text-muted\" />\n )}\n </div>\n </div>\n );\n}\n\nexport function CoursesCourseDetailsPagePage() {\n const [params] = useSearchParams();\n const id = params.get('id') ?? '';\n const { currentUser, workspaceId } = useTechnoSpam();\n const { emit } = useTechnoSpamAnalytics();\n const { data, loading, error, refetch } = useCourseQuery(id);\n\n useEffect(() => {\n if (id) {\n emit('technospam.course.viewed', {\n workspaceId,\n entityId: id,\n entityType: 'TEC_COURSE',\n });\n }\n }, [id, workspaceId, emit]);\n\n const [enroll, { loading: enrolling }] = useEnroll();\n const [updateProgress, { loading: savingProgress }] = useUpdateProgress();\n const { data: certData, refetch: refetchCerts } = useCertificatesQuery(\n currentUser?.id && id ? { learnerId: currentUser.id, courseId: id } : {}\n );\n const [issueCertificate, { loading: claiming }] = useIssueCertificate();\n\n const course = data?.tecCourse ?? null;\n const lessons = [...(course?.lessons ?? [])].sort((a, b) => a.position - b.position);\n const myEnrollment: TecEnrollment | undefined = (course?.enrollments ?? []).find(\n (e) => e.learnerId === currentUser?.id\n );\n const myCertificate = (certData?.tecListCertificates ?? []).find((c) => c.status === 'ISSUED');\n const isComplete = !!myEnrollment && myEnrollment.progressPct >= 100;\n\n const handleClaimCertificate = async () => {\n if (!id || !currentUser?.id) return;\n emit('technospam.course.certificate_claimed', {\n workspaceId,\n entityId: id,\n entityType: 'TEC_COURSE',\n });\n const res = await issueCertificate({\n variables: { courseId: id, learnerId: currentUser.id, score: 100 },\n });\n if (res.data?.tecIssueCertificate) {\n notify.success('Certificate issued', 'Your verifiable certificate is ready in My Learning.');\n await refetchCerts();\n }\n };\n\n const handleEnroll = async () => {\n if (!id) return;\n emit('technospam.course.enroll_clicked', {\n workspaceId,\n entityId: id,\n entityType: 'TEC_COURSE',\n });\n const res = await enroll({ variables: { input: { courseId: id } } });\n if (res.data?.tecEnroll) {\n emit('technospam.course.enrolled', {\n workspaceId,\n entityId: id,\n entityType: 'TEC_COURSE',\n });\n notify.success('Enrolled', `You're enrolled in \"${course?.title}\".`);\n await refetch();\n }\n };\n\n const handleProgress = async () => {\n if (!myEnrollment) return;\n const nextCompleted = Math.min(lessons.length, myEnrollment.lessonsCompleted + 1);\n const pct = lessons.length ? Math.round((nextCompleted / lessons.length) * 100) : 100;\n const res = await updateProgress({\n variables: {\n input: { enrollmentId: myEnrollment.id, lessonsCompleted: nextCompleted, progressPct: pct },\n },\n });\n if (res.data?.tecUpdateProgress) {\n emit('technospam.course.progress_updated', {\n workspaceId,\n entityId: myEnrollment.id,\n entityType: 'TEC_ENROLLMENT',\n progressPct: pct,\n });\n notify.success('Progress saved', `${pct}% complete`);\n await refetch();\n }\n };\n\n if (loading && !course) {\n return (\n <PageLayout title=\"Course\" backPath=\"/courses/courses-page\">\n <Skeleton className=\"mb-4 h-40 w-full rounded-2xl\" />\n <Skeleton className=\"h-64 w-full rounded-xl\" />\n </PageLayout>\n );\n }\n\n if (error || !course) {\n return (\n <PageLayout title=\"Course\" backPath=\"/courses/courses-page\">\n {error ? (\n <ErrorView title=\"Couldn't load course\" error={error} />\n ) : (\n <EmptyView\n title=\"Course not found\"\n description=\"This course may have been unpublished.\"\n />\n )}\n </PageLayout>\n );\n }\n\n const completedLessons = myEnrollment?.lessonsCompleted ?? 0;\n const remaining = Math.max(0, lessons.length - completedLessons);\n const milestone =\n myEnrollment && myEnrollment.progressPct >= 100\n ? 'Course complete — claim your certificate below.'\n : remaining === 1\n ? '1 lesson to go — almost there!'\n : `${remaining} lessons to go — keep it up!`;\n const heroSeed = `${course.topic ?? ''} ${course.title}`;\n\n return (\n <PageLayout\n backPath=\"/courses/courses-page\"\n title={course.title}\n description={course.summary ?? undefined}\n actions={\n myEnrollment ? (\n <Button\n onClick={handleProgress}\n disabled={savingProgress || completedLessons >= lessons.length}\n >\n <CheckCircle2 size={16} />\n {completedLessons >= lessons.length ? 'Completed' : 'Complete next lesson'}\n </Button>\n ) : (\n <Button onClick={handleEnroll} disabled={enrolling}>\n <BookOpen size={16} /> {enrolling ? 'Enrolling…' : 'Enroll'}\n </Button>\n )\n }\n >\n {/* Bold hero banner */}\n <MediaCover\n height=\"h-40\"\n seed={heroSeed}\n icon={topicIcon(course.topic ?? course.title)}\n className=\"mb-5 rounded-2xl\"\n >\n <div className=\"flex w-full flex-col gap-2\">\n <span className=\"text-xs font-medium uppercase tracking-wide text-white/80\">\n {course.topic ?? 'Course'}\n </span>\n <h2 className=\"text-2xl font-bold leading-tight text-white drop-shadow-sm sm:text-3xl\">\n {course.title}\n </h2>\n <div className=\"flex flex-wrap items-center gap-2\">\n <HeroBadge icon={TrendingUp}>{humanizeEnum(course.level)}</HeroBadge>\n <HeroBadge icon={CheckCircle2}>{humanizeEnum(course.status)}</HeroBadge>\n {course.topic ? <HeroBadge icon={Layers}>{course.topic}</HeroBadge> : null}\n </div>\n </div>\n </MediaCover>\n\n {course.tags.length ? (\n <div className=\"mb-5 flex flex-wrap items-center gap-2\">\n {course.tags.map((t) => (\n <Badge key={t} variant=\"secondary\">\n {t}\n </Badge>\n ))}\n </div>\n ) : null}\n\n <div className=\"mb-6 grid grid-cols-2 gap-3 sm:grid-cols-4\">\n <StatCard label=\"Lessons\" value={lessons.length} icon={FileText} tone=\"blue\" />\n <StatCard\n label=\"Duration\"\n value={formatDuration(course.estimatedMinutes)}\n icon={Clock}\n tone=\"teal\"\n />\n <StatCard\n label=\"Enrolled\"\n value={course.enrollments?.length ?? 0}\n icon={Users}\n tone=\"indigo\"\n />\n <StatCard\n label=\"Your progress\"\n value={myEnrollment ? `${myEnrollment.progressPct}%` : 'Not started'}\n icon={TrendingUp}\n tone=\"purple\"\n />\n </div>\n\n {myEnrollment ? (\n <GlassCard className=\"mb-6 p-5\">\n <div className=\"mb-3 flex items-center gap-2\">\n <span className=\"flex h-8 w-8 items-center justify-center rounded-lg bg-accent-primary/15 text-accent-primary\">\n <TrendingUp size={16} />\n </span>\n <span className=\"text-sm font-semibold text-text-primary\">Your progress</span>\n <span className=\"ml-auto text-sm text-text-secondary\">\n {completedLessons}/{lessons.length} lessons · {myEnrollment.progressPct}%\n </span>\n </div>\n <div className=\"h-2.5 w-full overflow-hidden rounded-full bg-bg-sunken\">\n <div\n className=\"h-full rounded-full bg-gradient-to-r from-accent-primary via-accent-indigo to-accent-purple transition-all duration-500\"\n style={{ width: `${myEnrollment.progressPct}%` }}\n />\n </div>\n <p className=\"mt-2 text-xs text-text-muted\">{milestone}</p>\n </GlassCard>\n ) : null}\n\n {myCertificate ? (\n <GlassCard className=\"mb-6 flex flex-wrap items-center gap-3 border border-status-success-border/60 bg-status-success-bg-subtle/30 p-4\">\n <span className=\"flex h-11 w-11 shrink-0 items-center justify-center rounded-xl bg-status-warning-bg-subtle text-status-warning-text ring-1 ring-status-warning-border/30\">\n <Award size={22} />\n </span>\n <div className=\"min-w-0 flex-1\">\n <div className=\"text-sm font-semibold text-text-primary\">Certificate earned</div>\n <div className=\"text-xs text-text-muted\">\n Serial {myCertificate.serial} · Score {myCertificate.score}\n </div>\n </div>\n <span className=\"inline-flex items-center gap-1 rounded-full border border-status-success-border bg-status-success-bg-subtle px-2.5 py-1 text-xs font-medium text-status-success-text\">\n <ShieldCheck size={13} /> Verified\n </span>\n </GlassCard>\n ) : isComplete ? (\n <GlassCard className=\"mb-6 flex flex-col items-start gap-4 border border-status-success-border/60 bg-status-success-bg-subtle/40 p-5 sm:flex-row sm:items-center sm:justify-between\">\n <div className=\"flex items-center gap-4\">\n <span className=\"flex h-14 w-14 shrink-0 items-center justify-center rounded-2xl bg-status-warning-bg-subtle text-status-warning-text ring-1 ring-status-warning-border/40\">\n <Trophy size={26} />\n </span>\n <div>\n <div className=\"text-base font-semibold text-text-primary\">\n You finished this course\n </div>\n <div className=\"text-sm text-text-muted\">\n Claim your verifiable certificate of completion.\n </div>\n </div>\n </div>\n <Button onClick={handleClaimCertificate} disabled={claiming}>\n <Award size={16} /> {claiming ? 'Issuing…' : 'Claim certificate'}\n </Button>\n </GlassCard>\n ) : null}\n\n <GlassCard className=\"p-5\">\n <div className=\"mb-3 flex items-center justify-between gap-2\">\n <h2 className=\"flex items-center gap-2 text-base font-semibold text-text-primary\">\n <BookOpen size={18} className=\"text-accent-primary\" /> Curriculum\n </h2>\n {lessons.length ? (\n <span className=\"rounded-full bg-status-success-bg-subtle px-2.5 py-1 text-xs font-medium text-status-success-text\">\n {completedLessons}/{lessons.length} completed\n </span>\n ) : null}\n </div>\n {lessons.length === 0 ? (\n <p className=\"py-6 text-center text-sm text-text-muted\">\n Lessons for this course are being prepared.\n </p>\n ) : (\n <div className=\"flex flex-col\">\n {lessons.map((lesson, i) => (\n <LessonRow key={lesson.id} lesson={lesson} index={i} done={i < completedLessons} />\n ))}\n </div>\n )}\n </GlassCard>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CoursesCourseDetailsPagePage.js","names":[],"sources":["../../../src/technospam/pages/CoursesCourseDetailsPagePage.tsx"],"sourcesContent":["import type { ReactNode } from 'react';\nimport { useEffect } from 'react';\nimport { useSearchParams } from 'react-router-dom';\nimport {\n Award,\n BookOpen,\n CheckCircle2,\n Clock,\n Eye,\n FileText,\n Lock,\n PlayCircle,\n Layers,\n ShieldCheck,\n Sparkles,\n Trophy,\n TrendingUp,\n Users,\n type LucideIcon,\n} from 'lucide-react';\nimport { Badge, Button, GlassCard, Skeleton } from '@burdenoff/fe-libs/ui';\nimport {\n EmptyView,\n ErrorView,\n MediaCover,\n PageLayout,\n StatCard,\n StatusBadge,\n TagStrip,\n topicIcon,\n} from '../components';\nimport { useTechnoSpam } from '../providers/TechnoSpamProvider';\nimport {\n useCertificatesQuery,\n useCourseQuery,\n useEnroll,\n useIssueCertificate,\n useUpdateProgress,\n type TecLesson,\n type TecEnrollment,\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';\n\n/** Per-lesson-kind icon + on-theme tint (chip) and pill (badge) classes. */\nconst KIND_META: Record<string, { icon: LucideIcon; chip: string; badge: string }> = {\n VIDEO: {\n icon: PlayCircle,\n chip: 'bg-accent-primary/15 text-accent-primary',\n badge: 'border-accent-primary/25 bg-accent-primary/10 text-accent-primary',\n },\n ARTICLE: {\n icon: FileText,\n chip: 'bg-accent-indigo/15 text-accent-indigo',\n badge: 'border-accent-indigo/25 bg-accent-indigo/10 text-accent-indigo',\n },\n INTERACTIVE: {\n icon: Sparkles,\n chip: 'bg-accent-teal/15 text-accent-teal',\n badge: 'border-accent-teal/25 bg-accent-teal/10 text-accent-teal',\n },\n QUIZ: {\n icon: CheckCircle2,\n chip: 'bg-accent-purple/15 text-accent-purple',\n badge: 'border-accent-purple/25 bg-accent-purple/10 text-accent-purple',\n },\n PROJECT: {\n icon: Layers,\n chip: 'bg-status-warning-bg-subtle text-status-warning-text',\n badge: 'border-status-warning-border bg-status-warning-bg-subtle text-status-warning-text',\n },\n};\n\nfunction HeroBadge({ icon: Icon, children }: { icon?: LucideIcon; children: ReactNode }) {\n return (\n <span className=\"inline-flex items-center gap-1 rounded-full bg-white/15 px-2.5 py-1 text-xs font-medium text-white ring-1 ring-white/25 backdrop-blur-sm\">\n {Icon ? <Icon size={12} /> : null}\n {children}\n </span>\n );\n}\n\nfunction LessonRow({ lesson, index, done }: { lesson: TecLesson; index: number; done: boolean }) {\n const meta = KIND_META[lesson.kind] ?? KIND_META.ARTICLE!;\n const Icon = meta.icon;\n return (\n <div className=\"group flex items-center gap-3 rounded-xl px-2 py-3 transition-colors hover:bg-bg-sunken/60\">\n <span className=\"hidden w-6 shrink-0 text-center text-sm font-medium tabular-nums text-text-muted sm:block\">\n {index + 1}\n </span>\n <span\n className={cn(\n 'flex h-9 w-9 shrink-0 items-center justify-center rounded-lg',\n done ? 'bg-status-success-bg-subtle text-status-success-text' : meta.chip\n )}\n >\n {done ? <CheckCircle2 size={17} /> : <Icon size={17} />}\n </span>\n <div className=\"min-w-0 flex-1\">\n <span className=\"block truncate text-sm font-medium text-text-primary\">{lesson.title}</span>\n <div className=\"mt-1 flex flex-wrap items-center gap-2\">\n <span\n className={cn(\n 'inline-flex items-center rounded-full border px-2 py-0.5 text-[11px] font-medium',\n meta.badge\n )}\n >\n {humanizeEnum(lesson.kind)}\n </span>\n <span className=\"inline-flex items-center gap-1 text-xs text-text-muted\">\n <Clock size={12} /> {formatDuration(lesson.estimatedMinutes)}\n </span>\n </div>\n </div>\n <div className=\"shrink-0\">\n {done ? (\n <CheckCircle2 size={18} className=\"text-status-success-text\" />\n ) : lesson.isPreview ? (\n <span className=\"inline-flex items-center gap-1 rounded-full border border-accent-primary/30 bg-accent-primary/10 px-2 py-0.5 text-[11px] font-medium text-accent-primary\">\n <Eye size={12} /> Preview\n </span>\n ) : (\n <Lock size={15} className=\"text-text-muted\" />\n )}\n </div>\n </div>\n );\n}\n\nexport function CoursesCourseDetailsPagePage() {\n const [params] = useSearchParams();\n const id = params.get('id') ?? '';\n const { currentUser, workspaceId } = useTechnoSpam();\n const { emit } = useTechnoSpamAnalytics();\n const { data, loading, error, refetch } = useCourseQuery(id);\n\n useEffect(() => {\n if (id) {\n emit('technospam.course.viewed', {\n workspaceId,\n entityId: id,\n entityType: 'TEC_COURSE',\n });\n }\n }, [id, workspaceId, emit]);\n\n const [enroll, { loading: enrolling }] = useEnroll();\n const [updateProgress, { loading: savingProgress }] = useUpdateProgress();\n const { data: certData, refetch: refetchCerts } = useCertificatesQuery(\n currentUser?.id && id ? { learnerId: currentUser.id, courseId: id } : {}\n );\n const [issueCertificate, { loading: claiming }] = useIssueCertificate();\n\n const course = data?.tecCourse ?? null;\n const lessons = [...(course?.lessons ?? [])].sort((a, b) => a.position - b.position);\n const myEnrollment: TecEnrollment | undefined = (course?.enrollments ?? []).find(\n (e) => e.learnerId === currentUser?.id\n );\n const myCertificate = (certData?.tecListCertificates ?? []).find((c) => c.status === 'ISSUED');\n const isComplete = !!myEnrollment && myEnrollment.progressPct >= 100;\n\n const handleClaimCertificate = async () => {\n if (!id || !currentUser?.id) return;\n emit('technospam.course.certificate_claimed', {\n workspaceId,\n entityId: id,\n entityType: 'TEC_COURSE',\n });\n const res = await issueCertificate({\n variables: { courseId: id, learnerId: currentUser.id, score: 100 },\n });\n if (res.data?.tecIssueCertificate) {\n notify.success('Certificate issued', 'Your verifiable certificate is ready in My Learning.');\n await refetchCerts();\n }\n };\n\n const handleEnroll = async () => {\n if (!id) return;\n emit('technospam.course.enroll_clicked', {\n workspaceId,\n entityId: id,\n entityType: 'TEC_COURSE',\n });\n const res = await enroll({ variables: { input: { courseId: id } } });\n if (res.data?.tecEnroll) {\n emit('technospam.course.enrolled', {\n workspaceId,\n entityId: id,\n entityType: 'TEC_COURSE',\n });\n notify.success('Enrolled', `You're enrolled in \"${course?.title}\".`);\n await refetch();\n }\n };\n\n const handleProgress = async () => {\n if (!myEnrollment) return;\n const nextCompleted = Math.min(lessons.length, myEnrollment.lessonsCompleted + 1);\n const pct = lessons.length ? Math.round((nextCompleted / lessons.length) * 100) : 100;\n const res = await updateProgress({\n variables: {\n input: { enrollmentId: myEnrollment.id, lessonsCompleted: nextCompleted, progressPct: pct },\n },\n });\n if (res.data?.tecUpdateProgress) {\n emit('technospam.course.progress_updated', {\n workspaceId,\n entityId: myEnrollment.id,\n entityType: 'TEC_ENROLLMENT',\n progressPct: pct,\n });\n notify.success('Progress saved', `${pct}% complete`);\n await refetch();\n }\n };\n\n if (loading && !course) {\n return (\n <PageLayout title=\"Course\" backPath=\"/courses/courses-page\">\n <Skeleton className=\"mb-4 h-40 w-full rounded-2xl\" />\n <Skeleton className=\"h-64 w-full rounded-xl\" />\n </PageLayout>\n );\n }\n\n if (error || !course) {\n return (\n <PageLayout title=\"Course\" backPath=\"/courses/courses-page\">\n {error ? (\n <ErrorView title=\"Couldn't load course\" error={error} />\n ) : (\n <EmptyView\n title=\"Course not found\"\n description=\"This course may have been unpublished.\"\n />\n )}\n </PageLayout>\n );\n }\n\n const completedLessons = myEnrollment?.lessonsCompleted ?? 0;\n const remaining = Math.max(0, lessons.length - completedLessons);\n const milestone =\n myEnrollment && myEnrollment.progressPct >= 100\n ? 'Course complete — claim your certificate below.'\n : remaining === 1\n ? '1 lesson to go — almost there!'\n : `${remaining} lessons to go — keep it up!`;\n const heroSeed = `${course.topic ?? ''} ${course.title}`;\n\n return (\n <PageLayout\n backPath=\"/courses/courses-page\"\n title={course.title}\n description={course.summary ?? undefined}\n actions={\n myEnrollment ? (\n <Button\n onClick={handleProgress}\n disabled={savingProgress || completedLessons >= lessons.length}\n >\n <CheckCircle2 size={16} />\n {completedLessons >= lessons.length ? 'Completed' : 'Complete next lesson'}\n </Button>\n ) : (\n <Button onClick={handleEnroll} disabled={enrolling}>\n <BookOpen size={16} /> {enrolling ? 'Enrolling…' : 'Enroll'}\n </Button>\n )\n }\n >\n {/* Bold hero banner */}\n <MediaCover\n height=\"h-40\"\n seed={heroSeed}\n icon={topicIcon(course.topic ?? course.title)}\n className=\"mb-5 rounded-2xl\"\n >\n <div className=\"flex w-full flex-col gap-2\">\n <span className=\"text-xs font-medium uppercase tracking-wide text-white/80\">\n {course.topic ?? 'Course'}\n </span>\n <h2 className=\"text-2xl font-bold leading-tight text-white drop-shadow-sm sm:text-3xl\">\n {course.title}\n </h2>\n <div className=\"flex flex-wrap items-center gap-2\">\n <HeroBadge icon={TrendingUp}>{humanizeEnum(course.level)}</HeroBadge>\n <HeroBadge icon={CheckCircle2}>{humanizeEnum(course.status)}</HeroBadge>\n {course.topic ? <HeroBadge icon={Layers}>{course.topic}</HeroBadge> : null}\n </div>\n </div>\n </MediaCover>\n\n <TagStrip tags={course.tags} className=\"mb-5\" />\n\n <div className=\"mb-6 grid grid-cols-2 gap-3 sm:grid-cols-4\">\n <StatCard label=\"Lessons\" value={lessons.length} icon={FileText} tone=\"blue\" />\n <StatCard\n label=\"Duration\"\n value={formatDuration(course.estimatedMinutes)}\n icon={Clock}\n tone=\"teal\"\n />\n <StatCard\n label=\"Enrolled\"\n value={course.enrollments?.length ?? 0}\n icon={Users}\n tone=\"indigo\"\n />\n <StatCard\n label=\"Your progress\"\n value={myEnrollment ? `${myEnrollment.progressPct}%` : 'Not started'}\n icon={TrendingUp}\n tone=\"purple\"\n />\n </div>\n\n {myEnrollment ? (\n <GlassCard className=\"mb-6 p-5\">\n <div className=\"mb-3 flex items-center gap-2\">\n <span className=\"flex h-8 w-8 items-center justify-center rounded-lg bg-accent-primary/15 text-accent-primary\">\n <TrendingUp size={16} />\n </span>\n <span className=\"text-sm font-semibold text-text-primary\">Your progress</span>\n <span className=\"ml-auto text-sm text-text-secondary\">\n {completedLessons}/{lessons.length} lessons · {myEnrollment.progressPct}%\n </span>\n </div>\n <div className=\"h-2.5 w-full overflow-hidden rounded-full bg-bg-sunken\">\n <div\n className=\"h-full rounded-full bg-gradient-to-r from-accent-primary via-accent-indigo to-accent-purple transition-all duration-500\"\n style={{ width: `${myEnrollment.progressPct}%` }}\n />\n </div>\n <p className=\"mt-2 text-xs text-text-muted\">{milestone}</p>\n </GlassCard>\n ) : null}\n\n {myCertificate ? (\n <GlassCard className=\"mb-6 flex flex-wrap items-center gap-3 border border-status-success-border/60 bg-status-success-bg-subtle/30 p-4\">\n <span className=\"flex h-11 w-11 shrink-0 items-center justify-center rounded-xl bg-status-warning-bg-subtle text-status-warning-text ring-1 ring-status-warning-border/30\">\n <Award size={22} />\n </span>\n <div className=\"min-w-0 flex-1\">\n <div className=\"text-sm font-semibold text-text-primary\">Certificate earned</div>\n <div className=\"text-xs text-text-muted\">\n Serial {myCertificate.serial} · Score {myCertificate.score}\n </div>\n </div>\n <span className=\"inline-flex items-center gap-1 rounded-full border border-status-success-border bg-status-success-bg-subtle px-2.5 py-1 text-xs font-medium text-status-success-text\">\n <ShieldCheck size={13} /> Verified\n </span>\n </GlassCard>\n ) : isComplete ? (\n <GlassCard className=\"mb-6 flex flex-col items-start gap-4 border border-status-success-border/60 bg-status-success-bg-subtle/40 p-5 sm:flex-row sm:items-center sm:justify-between\">\n <div className=\"flex items-center gap-4\">\n <span className=\"flex h-14 w-14 shrink-0 items-center justify-center rounded-2xl bg-status-warning-bg-subtle text-status-warning-text ring-1 ring-status-warning-border/40\">\n <Trophy size={26} />\n </span>\n <div>\n <div className=\"text-base font-semibold text-text-primary\">\n You finished this course\n </div>\n <div className=\"text-sm text-text-muted\">\n Claim your verifiable certificate of completion.\n </div>\n </div>\n </div>\n <Button onClick={handleClaimCertificate} disabled={claiming}>\n <Award size={16} /> {claiming ? 'Issuing…' : 'Claim certificate'}\n </Button>\n </GlassCard>\n ) : null}\n\n <GlassCard className=\"p-5\">\n <div className=\"mb-3 flex items-center justify-between gap-2\">\n <h2 className=\"flex items-center gap-2 text-base font-semibold text-text-primary\">\n <BookOpen size={18} className=\"text-accent-primary\" /> Curriculum\n </h2>\n {lessons.length ? (\n <span className=\"rounded-full bg-status-success-bg-subtle px-2.5 py-1 text-xs font-medium text-status-success-text\">\n {completedLessons}/{lessons.length} completed\n </span>\n ) : null}\n </div>\n {lessons.length === 0 ? (\n <p className=\"py-6 text-center text-sm text-text-muted\">\n Lessons for this course are being prepared.\n </p>\n ) : (\n <div className=\"flex flex-col\">\n {lessons.map((lesson, i) => (\n <LessonRow key={lesson.id} lesson={lesson} index={i} done={i < completedLessons} />\n ))}\n </div>\n )}\n </GlassCard>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|