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