@burdenoff/microfe-technospam 2026.1102.1 → 2026.1102.3
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/generated/wspace-types.d.ts +7468 -141
- package/dist/generated/wspace-types.d.ts.map +1 -1
- package/dist/node_modules/@codemirror/autocomplete/dist/index.js +527 -0
- package/dist/node_modules/@codemirror/autocomplete/dist/index.js.map +1 -0
- package/dist/node_modules/@codemirror/commands/dist/index.js +1108 -0
- package/dist/node_modules/@codemirror/commands/dist/index.js.map +1 -0
- package/dist/node_modules/@codemirror/lang-go/dist/index.js +206 -0
- package/dist/node_modules/@codemirror/lang-go/dist/index.js.map +1 -0
- package/dist/node_modules/@codemirror/lang-javascript/dist/index.js +321 -0
- package/dist/node_modules/@codemirror/lang-javascript/dist/index.js.map +1 -0
- package/dist/node_modules/@codemirror/lang-python/dist/index.js +265 -0
- package/dist/node_modules/@codemirror/lang-python/dist/index.js.map +1 -0
- package/dist/node_modules/@codemirror/language/dist/index.js +957 -0
- package/dist/node_modules/@codemirror/language/dist/index.js.map +1 -0
- package/dist/node_modules/@codemirror/legacy-modes/mode/ruby.js +164 -0
- package/dist/node_modules/@codemirror/legacy-modes/mode/ruby.js.map +1 -0
- package/dist/node_modules/@codemirror/legacy-modes/mode/shell.js +109 -0
- package/dist/node_modules/@codemirror/legacy-modes/mode/shell.js.map +1 -0
- package/dist/node_modules/@codemirror/state/dist/index.js +1739 -0
- package/dist/node_modules/@codemirror/state/dist/index.js.map +1 -0
- package/dist/node_modules/@codemirror/streamparser/dist/index.js +70 -0
- package/dist/node_modules/@codemirror/streamparser/dist/index.js.map +1 -0
- package/dist/node_modules/@codemirror/view/dist/index.js +5427 -0
- package/dist/node_modules/@codemirror/view/dist/index.js.map +1 -0
- package/dist/node_modules/@lezer/common/dist/index.js +896 -0
- package/dist/node_modules/@lezer/common/dist/index.js.map +1 -0
- package/dist/node_modules/@lezer/go/dist/index.js +215 -0
- package/dist/node_modules/@lezer/go/dist/index.js.map +1 -0
- package/dist/node_modules/@lezer/highlight/dist/index.js +395 -0
- package/dist/node_modules/@lezer/highlight/dist/index.js.map +1 -0
- package/dist/node_modules/@lezer/javascript/dist/index.js +437 -0
- package/dist/node_modules/@lezer/javascript/dist/index.js.map +1 -0
- package/dist/node_modules/@lezer/lr/dist/index.js +842 -0
- package/dist/node_modules/@lezer/lr/dist/index.js.map +1 -0
- package/dist/node_modules/@lezer/python/dist/index.js +352 -0
- package/dist/node_modules/@lezer/python/dist/index.js.map +1 -0
- package/dist/node_modules/@marijn/find-cluster-break/src/index.js +66 -0
- package/dist/node_modules/@marijn/find-cluster-break/src/index.js.map +1 -0
- package/dist/node_modules/style-mod/src/style-mod.js +74 -0
- package/dist/node_modules/style-mod/src/style-mod.js.map +1 -0
- package/dist/node_modules/w3c-keyname/index.js +90 -0
- package/dist/node_modules/w3c-keyname/index.js.map +1 -0
- package/dist/stories/fixtures/technospamFixtures.d.ts.map +1 -1
- package/dist/stories/journeys/Problems.stories.d.ts.map +1 -1
- package/dist/technospam/TechnoSpamRoot.d.ts.map +1 -1
- package/dist/technospam/TechnoSpamRoot.js +187 -178
- package/dist/technospam/TechnoSpamRoot.js.map +1 -1
- package/dist/technospam/api/technospamApi.d.ts +39 -0
- package/dist/technospam/api/technospamApi.d.ts.map +1 -1
- package/dist/technospam/api/technospamApi.js +184 -150
- package/dist/technospam/api/technospamApi.js.map +1 -1
- package/dist/technospam/components/MediaCover.js +2 -2
- package/dist/technospam/components/MediaCover.js.map +1 -1
- package/dist/technospam/components/MobileSectionTabs.d.ts +9 -0
- package/dist/technospam/components/MobileSectionTabs.d.ts.map +1 -0
- package/dist/technospam/components/MobileSectionTabs.js +43 -0
- package/dist/technospam/components/MobileSectionTabs.js.map +1 -0
- package/dist/technospam/components/PageLayout.d.ts +7 -1
- package/dist/technospam/components/PageLayout.d.ts.map +1 -1
- package/dist/technospam/components/PageLayout.js +40 -28
- package/dist/technospam/components/PageLayout.js.map +1 -1
- package/dist/technospam/components/SubmissionViews.d.ts +6 -2
- package/dist/technospam/components/SubmissionViews.d.ts.map +1 -1
- package/dist/technospam/components/SubmissionViews.js +6 -5
- package/dist/technospam/components/SubmissionViews.js.map +1 -1
- package/dist/technospam/components/TechnoSpamErrorBoundary.d.ts +6 -0
- package/dist/technospam/components/TechnoSpamErrorBoundary.d.ts.map +1 -1
- package/dist/technospam/components/TechnoSpamErrorBoundary.js +2 -2
- package/dist/technospam/components/TechnoSpamErrorBoundary.js.map +1 -1
- package/dist/technospam/components/editor/CodeEditor.d.ts +26 -0
- package/dist/technospam/components/editor/CodeEditor.d.ts.map +1 -0
- package/dist/technospam/components/editor/CodeEditor.js +102 -0
- package/dist/technospam/components/editor/CodeEditor.js.map +1 -0
- package/dist/technospam/components/editor/RunOutputPanel.d.ts +14 -0
- package/dist/technospam/components/editor/RunOutputPanel.d.ts.map +1 -0
- package/dist/technospam/components/editor/RunOutputPanel.js +127 -0
- package/dist/technospam/components/editor/RunOutputPanel.js.map +1 -0
- package/dist/technospam/components/editor/editorLanguages.d.ts +11 -0
- package/dist/technospam/components/editor/editorLanguages.d.ts.map +1 -0
- package/dist/technospam/components/editor/editorLanguages.js +30 -0
- package/dist/technospam/components/editor/editorLanguages.js.map +1 -0
- package/dist/technospam/components/editor/editorTheme.d.ts +11 -0
- package/dist/technospam/components/editor/editorTheme.d.ts.map +1 -0
- package/dist/technospam/components/editor/editorTheme.js +120 -0
- package/dist/technospam/components/editor/editorTheme.js.map +1 -0
- package/dist/technospam/components/lesson/SaveCourseOffline.js +6 -6
- package/dist/technospam/hooks/usePhoneLayout.d.ts +18 -0
- package/dist/technospam/hooks/usePhoneLayout.d.ts.map +1 -0
- package/dist/technospam/hooks/usePhoneLayout.js +14 -0
- package/dist/technospam/hooks/usePhoneLayout.js.map +1 -0
- package/dist/technospam/hooks/useSolutionRunner.d.ts +35 -0
- package/dist/technospam/hooks/useSolutionRunner.d.ts.map +1 -0
- package/dist/technospam/hooks/useSolutionRunner.js +66 -0
- package/dist/technospam/hooks/useSolutionRunner.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/pages/AdminAdminDashboardPagePage.js +1 -1
- 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 +416 -263
- package/dist/technospam/pages/ApplicationsProblemDetailsPagePage.js.map +1 -1
- package/dist/technospam/pages/ApplicationsProblemsPagePage.d.ts.map +1 -1
- package/dist/technospam/pages/ApplicationsProblemsPagePage.js +89 -87
- package/dist/technospam/pages/ApplicationsProblemsPagePage.js.map +1 -1
- package/dist/technospam/pages/CoursesCourseDetailsPagePage.js +3 -3
- package/dist/technospam/pages/CoursesCourseDetailsPagePage.js.map +1 -1
- package/dist/technospam/pages/CoursesCoursesPagePage.d.ts.map +1 -1
- package/dist/technospam/pages/CoursesCoursesPagePage.js +117 -81
- package/dist/technospam/pages/CoursesCoursesPagePage.js.map +1 -1
- package/dist/technospam/pages/CoursesMyLearningPagePage.js +1 -1
- package/dist/technospam/pages/CoursesMyLearningPagePage.js.map +1 -1
- package/dist/technospam/pages/DashboardDashboardPagePage.d.ts.map +1 -1
- package/dist/technospam/pages/DashboardDashboardPagePage.js +21 -17
- package/dist/technospam/pages/DashboardDashboardPagePage.js.map +1 -1
- package/dist/technospam/pages/HackathonsHackathonDetailsPagePage.js +3 -3
- package/dist/technospam/pages/HackathonsHackathonDetailsPagePage.js.map +1 -1
- package/dist/technospam/pages/MentorshipMentorProfilePagePage.js +4 -4
- package/dist/technospam/pages/MentorshipMentorProfilePagePage.js.map +1 -1
- package/dist/technospam/pages/MentorshipMentorsPagePage.d.ts.map +1 -1
- package/dist/technospam/pages/MentorshipMentorsPagePage.js +6 -4
- package/dist/technospam/pages/MentorshipMentorsPagePage.js.map +1 -1
- package/dist/technospam/pages/MentorshipSessionsPagePage.js +1 -1
- package/dist/technospam/pages/MentorshipSessionsPagePage.js.map +1 -1
- package/dist/technospam/types/index.d.ts +7 -0
- package/dist/technospam/types/index.d.ts.map +1 -1
- package/dist/technospam/utils/mobileTabs.d.ts +45 -0
- package/dist/technospam/utils/mobileTabs.d.ts.map +1 -0
- package/dist/technospam/utils/mobileTabs.js +70 -0
- package/dist/technospam/utils/mobileTabs.js.map +1 -0
- package/dist/test/matchMedia.d.ts +6 -0
- package/dist/test/matchMedia.d.ts.map +1 -0
- package/package.json +16 -6
|
@@ -14,60 +14,85 @@ import { CardOpenButton as v } from "../components/CardOpenButton.js";
|
|
|
14
14
|
import "../components/index.js";
|
|
15
15
|
import { TAB_PANEL_FOCUS_CLASS as y } from "../lib/a11y.js";
|
|
16
16
|
import { useMemo as b, useState as x } from "react";
|
|
17
|
-
import { BookOpen as S,
|
|
18
|
-
import { jsx as
|
|
19
|
-
import { Badge as
|
|
17
|
+
import { BookOpen as S, ChevronRight as C, Clock as w, GraduationCap as T, Layers as E, Search as D, Sparkles as O } from "lucide-react";
|
|
18
|
+
import { jsx as k, jsxs as A } from "react/jsx-runtime";
|
|
19
|
+
import { Badge as j, Button as M, GlassCard as N, Input as P, Tabs as F, TabsContent as I, TabsList as L, TabsTrigger as R } from "@burdenoff/fe-libs/ui";
|
|
20
20
|
//#region src/technospam/pages/CoursesCoursesPagePage.tsx
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
var z = {
|
|
22
|
+
BEGINNER: "bg-status-success-text",
|
|
23
|
+
INTERMEDIATE: "bg-accent-primary",
|
|
24
|
+
ADVANCED: "bg-status-warning-text",
|
|
25
|
+
EXPERT: "bg-status-error-text"
|
|
26
|
+
};
|
|
27
|
+
function B({ course: e, onOpen: t }) {
|
|
28
|
+
return /* @__PURE__ */ A(N, {
|
|
29
|
+
"data-testid": "course-card",
|
|
30
|
+
className: "group flex cursor-pointer flex-row overflow-hidden transition-all duration-200 hover:-translate-y-1 hover:shadow-lg sm:flex-col",
|
|
31
|
+
children: [/* @__PURE__ */ k(o, {
|
|
25
32
|
seed: `${e.topic ?? ""} ${e.title}`,
|
|
26
33
|
icon: s(e.topic ?? e.title),
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
34
|
+
className: "h-auto min-h-28 w-24 shrink-0 p-2 sm:h-28 sm:min-h-0 sm:w-auto sm:p-4",
|
|
35
|
+
children: /* @__PURE__ */ A("div", {
|
|
36
|
+
className: "hidden flex-wrap items-center gap-1.5 sm:flex",
|
|
37
|
+
children: [/* @__PURE__ */ k(l, {
|
|
30
38
|
status: e.level,
|
|
31
39
|
kind: "level"
|
|
32
|
-
}), e.isPremiumOnly ? /* @__PURE__ */
|
|
40
|
+
}), e.isPremiumOnly ? /* @__PURE__ */ k(u, { locked: e.isLocked }) : null]
|
|
33
41
|
})
|
|
34
|
-
}), /* @__PURE__ */
|
|
35
|
-
className: "flex flex-1 flex-col gap-3 p-4",
|
|
42
|
+
}), /* @__PURE__ */ A("div", {
|
|
43
|
+
className: "flex min-w-0 flex-1 flex-col gap-2 p-3 sm:gap-3 sm:p-4",
|
|
36
44
|
children: [
|
|
37
|
-
/* @__PURE__ */
|
|
45
|
+
/* @__PURE__ */ A("div", {
|
|
38
46
|
className: "min-w-0",
|
|
39
|
-
children: [
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
children: [
|
|
48
|
+
/* @__PURE__ */ A("div", {
|
|
49
|
+
className: "flex items-start gap-2",
|
|
50
|
+
children: [/* @__PURE__ */ k("h3", {
|
|
51
|
+
className: "min-w-0 flex-1 truncate text-base font-semibold text-text-primary group-hover:text-accent-primary",
|
|
52
|
+
children: /* @__PURE__ */ k(v, {
|
|
53
|
+
onOpen: t,
|
|
54
|
+
children: e.title
|
|
55
|
+
})
|
|
56
|
+
}), /* @__PURE__ */ k(C, {
|
|
57
|
+
size: 18,
|
|
58
|
+
"aria-hidden": "true",
|
|
59
|
+
className: "mt-0.5 shrink-0 text-accent-primary sm:hidden"
|
|
60
|
+
})]
|
|
61
|
+
}),
|
|
62
|
+
/* @__PURE__ */ A("div", {
|
|
63
|
+
className: "mt-1 flex flex-wrap items-center gap-1.5 sm:hidden",
|
|
64
|
+
children: [/* @__PURE__ */ k(l, {
|
|
65
|
+
status: e.level,
|
|
66
|
+
kind: "level"
|
|
67
|
+
}), e.isPremiumOnly ? /* @__PURE__ */ k(u, { locked: e.isLocked }) : null]
|
|
68
|
+
}),
|
|
69
|
+
e.summary ? /* @__PURE__ */ k("p", {
|
|
70
|
+
className: "mt-1 line-clamp-2 text-sm text-text-secondary",
|
|
71
|
+
children: e.summary
|
|
72
|
+
}) : null
|
|
73
|
+
]
|
|
49
74
|
}),
|
|
50
|
-
/* @__PURE__ */
|
|
75
|
+
/* @__PURE__ */ A("div", {
|
|
51
76
|
className: "mt-auto flex flex-wrap items-center gap-3 text-xs text-text-muted",
|
|
52
|
-
children: [e.topic ? /* @__PURE__ */
|
|
77
|
+
children: [e.topic ? /* @__PURE__ */ A("span", {
|
|
53
78
|
className: "inline-flex items-center gap-1",
|
|
54
79
|
children: [
|
|
55
|
-
/* @__PURE__ */
|
|
80
|
+
/* @__PURE__ */ k(E, { size: 13 }),
|
|
56
81
|
" ",
|
|
57
82
|
e.topic
|
|
58
83
|
]
|
|
59
|
-
}) : null, /* @__PURE__ */
|
|
84
|
+
}) : null, /* @__PURE__ */ A("span", {
|
|
60
85
|
className: "inline-flex items-center gap-1",
|
|
61
86
|
children: [
|
|
62
|
-
/* @__PURE__ */
|
|
87
|
+
/* @__PURE__ */ k(w, { size: 13 }),
|
|
63
88
|
" ",
|
|
64
89
|
c(e.estimatedMinutes)
|
|
65
90
|
]
|
|
66
91
|
})]
|
|
67
92
|
}),
|
|
68
|
-
e.tags.length ? /* @__PURE__ */
|
|
93
|
+
e.tags.length ? /* @__PURE__ */ k("div", {
|
|
69
94
|
className: "flex flex-wrap gap-1.5",
|
|
70
|
-
children: e.tags.slice(0, 3).map((e) => /* @__PURE__ */
|
|
95
|
+
children: e.tags.slice(0, 3).map((e) => /* @__PURE__ */ k(j, {
|
|
71
96
|
variant: "secondary",
|
|
72
97
|
className: "text-[11px]",
|
|
73
98
|
children: e
|
|
@@ -77,8 +102,8 @@ function R({ course: e, onOpen: t }) {
|
|
|
77
102
|
})]
|
|
78
103
|
});
|
|
79
104
|
}
|
|
80
|
-
function
|
|
81
|
-
let o = t(), { workspaceId: s } = e(), { emit: l } = n(), u = r(), [v,
|
|
105
|
+
function V() {
|
|
106
|
+
let o = t(), { workspaceId: s } = e(), { emit: l } = n(), u = r(), [v, C] = x("ALL"), [E, j] = x(""), N = b(() => [
|
|
82
107
|
{
|
|
83
108
|
value: "ALL",
|
|
84
109
|
label: u("technospam.courses.filterAllLevels", "All levels")
|
|
@@ -99,77 +124,88 @@ function z() {
|
|
|
99
124
|
value: "EXPERT",
|
|
100
125
|
label: u("technospam.courses.filterExpert", "Expert")
|
|
101
126
|
}
|
|
102
|
-
], [u]), { limit:
|
|
127
|
+
], [u]), { limit: V, more: H } = _(), { data: U, loading: W, error: G } = g({
|
|
103
128
|
status: "PUBLISHED",
|
|
104
|
-
pagination: h(
|
|
129
|
+
pagination: h(V),
|
|
105
130
|
...v === "ALL" ? {} : { level: v }
|
|
106
|
-
}),
|
|
107
|
-
let e =
|
|
108
|
-
return e ?
|
|
109
|
-
}, [
|
|
110
|
-
return /* @__PURE__ */
|
|
131
|
+
}), K = U?.tecListCourses?.items ?? [], q = U?.tecListCourses?.pageInfo?.hasMore ?? !1, J = b(() => {
|
|
132
|
+
let e = E.trim().toLowerCase();
|
|
133
|
+
return e ? K.filter((t) => t.title.toLowerCase().includes(e) || (t.topic ?? "").toLowerCase().includes(e) || t.tags.some((t) => t.toLowerCase().includes(e))) : K;
|
|
134
|
+
}, [K, E]);
|
|
135
|
+
return /* @__PURE__ */ A(i, {
|
|
111
136
|
hero: !0,
|
|
137
|
+
mobileSection: !0,
|
|
112
138
|
title: u("technospam.courses.title", "Course Catalog"),
|
|
113
139
|
description: u("technospam.courses.description", "Learn technology by doing — practical, project-based courses across the stack."),
|
|
114
|
-
actions: /* @__PURE__ */
|
|
140
|
+
actions: /* @__PURE__ */ A(M, {
|
|
115
141
|
variant: "outline",
|
|
116
142
|
onClick: () => o("/courses/my-learning-page"),
|
|
117
143
|
children: [
|
|
118
|
-
/* @__PURE__ */
|
|
144
|
+
/* @__PURE__ */ k(T, { size: 16 }),
|
|
119
145
|
" ",
|
|
120
146
|
u("technospam.common.myLearning", "My Learning")
|
|
121
147
|
]
|
|
122
148
|
}),
|
|
123
|
-
children: [/* @__PURE__ */
|
|
124
|
-
className: "mb-5
|
|
149
|
+
children: [/* @__PURE__ */ A("div", {
|
|
150
|
+
className: "mb-5 hidden grid-cols-2 gap-3 sm:grid sm:grid-cols-4",
|
|
125
151
|
children: [
|
|
126
|
-
/* @__PURE__ */
|
|
152
|
+
/* @__PURE__ */ k(a, {
|
|
127
153
|
label: u("technospam.courses.statCourses", "Courses"),
|
|
128
|
-
value:
|
|
154
|
+
value: K.length,
|
|
129
155
|
icon: S,
|
|
130
156
|
tone: "blue"
|
|
131
157
|
}),
|
|
132
|
-
/* @__PURE__ */
|
|
158
|
+
/* @__PURE__ */ k(a, {
|
|
133
159
|
label: u("technospam.courses.statBeginnerFriendly", "Beginner-friendly"),
|
|
134
|
-
value:
|
|
135
|
-
icon:
|
|
160
|
+
value: K.filter((e) => e.level === "BEGINNER").length,
|
|
161
|
+
icon: O,
|
|
136
162
|
tone: "green"
|
|
137
163
|
}),
|
|
138
|
-
/* @__PURE__ */
|
|
164
|
+
/* @__PURE__ */ k(a, {
|
|
139
165
|
label: u("technospam.courses.statAdvancedPlus", "Advanced+"),
|
|
140
|
-
value:
|
|
141
|
-
icon:
|
|
166
|
+
value: K.filter((e) => e.level === "ADVANCED" || e.level === "EXPERT").length,
|
|
167
|
+
icon: T,
|
|
142
168
|
tone: "purple"
|
|
143
169
|
}),
|
|
144
|
-
/* @__PURE__ */
|
|
170
|
+
/* @__PURE__ */ k(a, {
|
|
145
171
|
label: u("technospam.courses.statTotalContent", "Total content"),
|
|
146
|
-
value: c(
|
|
147
|
-
icon:
|
|
172
|
+
value: c(K.reduce((e, t) => e + t.estimatedMinutes, 0)),
|
|
173
|
+
icon: w,
|
|
148
174
|
tone: "teal"
|
|
149
175
|
})
|
|
150
176
|
]
|
|
151
|
-
}), /* @__PURE__ */
|
|
177
|
+
}), /* @__PURE__ */ A(F, {
|
|
152
178
|
className: "gap-0",
|
|
153
179
|
value: v,
|
|
154
|
-
onValueChange: (e) =>
|
|
155
|
-
children: [/* @__PURE__ */
|
|
180
|
+
onValueChange: (e) => C(e),
|
|
181
|
+
children: [/* @__PURE__ */ A("div", {
|
|
156
182
|
className: "mb-4 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between",
|
|
157
|
-
children: [/* @__PURE__ */
|
|
158
|
-
|
|
159
|
-
children:
|
|
160
|
-
|
|
183
|
+
children: [/* @__PURE__ */ k("div", {
|
|
184
|
+
className: "-mx-4 overflow-x-auto px-4 sm:mx-0 sm:overflow-visible sm:px-0",
|
|
185
|
+
children: /* @__PURE__ */ k(L, {
|
|
186
|
+
className: "h-auto w-max gap-1.5 bg-transparent p-0 sm:h-9 sm:gap-0 sm:bg-bg-sunken sm:p-[3px]",
|
|
187
|
+
children: N.map((e) => /* @__PURE__ */ A(R, {
|
|
188
|
+
value: e.value,
|
|
189
|
+
className: "min-h-11 flex-none gap-1.5 rounded-full border-border-subtle px-4 data-[state=active]:border-accent-primary sm:min-h-0 sm:flex-1 sm:rounded-button sm:border-transparent sm:px-3 sm:data-[state=active]:border-transparent",
|
|
190
|
+
children: [z[e.value] ? /* @__PURE__ */ k("span", {
|
|
191
|
+
"aria-hidden": "true",
|
|
192
|
+
className: `h-2 w-2 rounded-full sm:hidden ${z[e.value]}`
|
|
193
|
+
}) : null, e.label]
|
|
194
|
+
}, e.value))
|
|
195
|
+
})
|
|
196
|
+
}), /* @__PURE__ */ A("div", {
|
|
161
197
|
className: "relative w-full sm:w-72",
|
|
162
|
-
children: [/* @__PURE__ */
|
|
198
|
+
children: [/* @__PURE__ */ k(D, {
|
|
163
199
|
className: "pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-text-muted",
|
|
164
200
|
size: 16
|
|
165
|
-
}), /* @__PURE__ */
|
|
201
|
+
}), /* @__PURE__ */ k(P, {
|
|
166
202
|
className: "pl-9",
|
|
167
203
|
"aria-label": u("technospam.courses.searchLabel", "Search courses"),
|
|
168
204
|
placeholder: u("technospam.courses.searchPlaceholder", "Search courses…"),
|
|
169
|
-
value:
|
|
170
|
-
onChange: (e) =>
|
|
205
|
+
value: E,
|
|
206
|
+
onChange: (e) => j(e.target.value),
|
|
171
207
|
onBlur: () => {
|
|
172
|
-
let e =
|
|
208
|
+
let e = E.trim();
|
|
173
209
|
e && l("technospam.search.performed", {
|
|
174
210
|
workspaceId: s,
|
|
175
211
|
entityType: "course",
|
|
@@ -178,19 +214,19 @@ function z() {
|
|
|
178
214
|
}
|
|
179
215
|
})]
|
|
180
216
|
})]
|
|
181
|
-
}), /* @__PURE__ */
|
|
217
|
+
}), /* @__PURE__ */ A(I, {
|
|
182
218
|
value: v,
|
|
183
219
|
className: y,
|
|
184
|
-
children: [
|
|
220
|
+
children: [W && !K.length ? /* @__PURE__ */ k(m, {}) : G ? /* @__PURE__ */ k(p, {
|
|
185
221
|
title: u("technospam.common.errorLoadCourses", "Couldn't load courses"),
|
|
186
|
-
error:
|
|
187
|
-
}) :
|
|
188
|
-
icon: /* @__PURE__ */
|
|
189
|
-
title:
|
|
190
|
-
description:
|
|
191
|
-
}) : /* @__PURE__ */
|
|
222
|
+
error: G
|
|
223
|
+
}) : J.length === 0 ? /* @__PURE__ */ k(f, {
|
|
224
|
+
icon: /* @__PURE__ */ k(S, { size: 26 }),
|
|
225
|
+
title: E || v !== "ALL" ? u("technospam.courses.emptySearchTitle", "No matching courses") : u("technospam.courses.emptyDefaultTitle", "No courses published yet"),
|
|
226
|
+
description: E || v !== "ALL" ? u("technospam.courses.emptySearchDesc", "Try a different search or level filter.") : u("technospam.courses.emptyDefaultDesc", "Published courses will appear here. Check back soon — TechnoSpam is pre-launch.")
|
|
227
|
+
}) : /* @__PURE__ */ k("div", {
|
|
192
228
|
className: "grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3",
|
|
193
|
-
children:
|
|
229
|
+
children: J.map((e) => /* @__PURE__ */ k(B, {
|
|
194
230
|
course: e,
|
|
195
231
|
onOpen: () => {
|
|
196
232
|
l("technospam.course.viewed", {
|
|
@@ -200,16 +236,16 @@ function z() {
|
|
|
200
236
|
}), o(`/courses/course-details-page?id=${e.id}`);
|
|
201
237
|
}
|
|
202
238
|
}, e.id))
|
|
203
|
-
}), /* @__PURE__ */
|
|
204
|
-
hasMore:
|
|
205
|
-
loading:
|
|
206
|
-
onClick:
|
|
239
|
+
}), /* @__PURE__ */ k(d, {
|
|
240
|
+
hasMore: q,
|
|
241
|
+
loading: W,
|
|
242
|
+
onClick: H
|
|
207
243
|
})]
|
|
208
244
|
})]
|
|
209
245
|
})]
|
|
210
246
|
});
|
|
211
247
|
}
|
|
212
248
|
//#endregion
|
|
213
|
-
export {
|
|
249
|
+
export { V as CoursesCoursesPagePage };
|
|
214
250
|
|
|
215
251
|
//# sourceMappingURL=CoursesCoursesPagePage.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CoursesCoursesPagePage.js","names":[],"sources":["../../../src/technospam/pages/CoursesCoursesPagePage.tsx"],"sourcesContent":["import { useMemo, useState } from 'react';\nimport { BookOpen, Clock, GraduationCap, Layers, Search, Sparkles } from 'lucide-react';\nimport {\n Badge,\n Button,\n GlassCard,\n Input,\n Tabs,\n TabsContent,\n TabsList,\n TabsTrigger,\n} from '@burdenoff/fe-libs/ui';\nimport { TAB_PANEL_FOCUS_CLASS } from '../lib/a11y';\nimport {\n CardOpenButton,\n EmptyView,\n ErrorView,\n LoadMore,\n LoadingGrid,\n MediaCover,\n PageLayout,\n PremiumBadge,\n StatCard,\n StatusBadge,\n topicIcon,\n} from '../components';\nimport { useTechnoSpam, useTechnoSpamNavigate } from '../providers/TechnoSpamProvider';\nimport {\n pageArg,\n useCoursesQuery,\n usePageLimit,\n type TecCourse,\n type TecCourseLevel,\n} from '../api/technospamApi';\nimport { formatDuration } from '../utils/format';\nimport { useTechnoSpamAnalytics } from '../hooks/useTechnoSpamAnalytics';\nimport { useTr } from '../hooks/useTr';\n\nfunction CourseCard({ course, onOpen }: { course: TecCourse; onOpen: () => void }) {\n return (\n <GlassCard className=\"group flex cursor-pointer flex-col overflow-hidden transition-all duration-200 hover:-translate-y-1 hover:shadow-lg\">\n <MediaCover\n seed={`${course.topic ?? ''} ${course.title}`}\n icon={topicIcon(course.topic ?? course.title)}\n >\n <div className=\"flex flex-wrap items-center gap-1.5\">\n <StatusBadge status={course.level} kind=\"level\" />\n {course.isPremiumOnly ? <PremiumBadge locked={course.isLocked} /> : null}\n </div>\n </MediaCover>\n <div className=\"flex flex-1 flex-col gap-3 p-4\">\n <div className=\"min-w-0\">\n <h3 className=\"truncate text-base font-semibold text-text-primary group-hover:text-accent-primary\">\n <CardOpenButton onOpen={onOpen}>{course.title}</CardOpenButton>\n </h3>\n {course.summary ? (\n <p className=\"mt-1 line-clamp-2 text-sm text-text-secondary\">{course.summary}</p>\n ) : null}\n </div>\n <div className=\"mt-auto flex flex-wrap items-center gap-3 text-xs text-text-muted\">\n {course.topic ? (\n <span className=\"inline-flex items-center gap-1\">\n <Layers size={13} /> {course.topic}\n </span>\n ) : null}\n <span className=\"inline-flex items-center gap-1\">\n <Clock size={13} /> {formatDuration(course.estimatedMinutes)}\n </span>\n </div>\n {course.tags.length ? (\n <div className=\"flex flex-wrap gap-1.5\">\n {course.tags.slice(0, 3).map((t) => (\n <Badge key={t} variant=\"secondary\" className=\"text-[11px]\">\n {t}\n </Badge>\n ))}\n </div>\n ) : null}\n </div>\n </GlassCard>\n );\n}\n\nexport function CoursesCoursesPagePage() {\n const navigate = useTechnoSpamNavigate();\n const { workspaceId } = useTechnoSpam();\n const { emit } = useTechnoSpamAnalytics();\n const tr = useTr();\n const [level, setLevel] = useState<'ALL' | TecCourseLevel>('ALL');\n const [search, setSearch] = useState('');\n\n const levels = useMemo(\n () => [\n { value: 'ALL' as const, label: tr('technospam.courses.filterAllLevels', 'All levels') },\n {\n value: 'BEGINNER' as TecCourseLevel,\n label: tr('technospam.courses.filterBeginner', 'Beginner'),\n },\n {\n value: 'INTERMEDIATE' as TecCourseLevel,\n label: tr('technospam.courses.filterIntermediate', 'Intermediate'),\n },\n {\n value: 'ADVANCED' as TecCourseLevel,\n label: tr('technospam.courses.filterAdvanced', 'Advanced'),\n },\n { value: 'EXPERT' as TecCourseLevel, label: tr('technospam.courses.filterExpert', 'Expert') },\n ],\n [tr]\n );\n\n const { limit, more } = usePageLimit();\n const { data, loading, error } = useCoursesQuery({\n status: 'PUBLISHED',\n pagination: pageArg(limit),\n ...(level !== 'ALL' ? { level } : {}),\n });\n\n const courses = data?.tecListCourses?.items ?? [];\n const hasMore = data?.tecListCourses?.pageInfo?.hasMore ?? false;\n const filtered = useMemo(() => {\n const q = search.trim().toLowerCase();\n if (!q) return courses;\n return courses.filter(\n (c) =>\n c.title.toLowerCase().includes(q) ||\n (c.topic ?? '').toLowerCase().includes(q) ||\n c.tags.some((t) => t.toLowerCase().includes(q))\n );\n }, [courses, search]);\n\n return (\n <PageLayout\n hero\n title={tr('technospam.courses.title', 'Course Catalog')}\n description={tr(\n 'technospam.courses.description',\n 'Learn technology by doing — practical, project-based courses across the stack.'\n )}\n actions={\n <Button variant=\"outline\" onClick={() => navigate('/courses/my-learning-page')}>\n <GraduationCap size={16} /> {tr('technospam.common.myLearning', 'My Learning')}\n </Button>\n }\n >\n <div className=\"mb-5 grid grid-cols-2 gap-3 sm:grid-cols-4\">\n <StatCard\n label={tr('technospam.courses.statCourses', 'Courses')}\n value={courses.length}\n icon={BookOpen}\n tone=\"blue\"\n />\n <StatCard\n label={tr('technospam.courses.statBeginnerFriendly', 'Beginner-friendly')}\n value={courses.filter((c) => c.level === 'BEGINNER').length}\n icon={Sparkles}\n tone=\"green\"\n />\n <StatCard\n label={tr('technospam.courses.statAdvancedPlus', 'Advanced+')}\n value={courses.filter((c) => c.level === 'ADVANCED' || c.level === 'EXPERT').length}\n icon={GraduationCap}\n tone=\"purple\"\n />\n <StatCard\n label={tr('technospam.courses.statTotalContent', 'Total content')}\n value={formatDuration(courses.reduce((s, c) => s + c.estimatedMinutes, 0))}\n icon={Clock}\n tone=\"teal\"\n />\n </div>\n\n {/* The tabs filter one list, so that list is their tab panel: Radix points every\n trigger's aria-controls at a panel id, and without a TabsContent the id\n resolved to nothing (axe aria-valid-attr-value, critical; BOFF-3157). */}\n <Tabs\n className=\"gap-0\"\n value={level}\n onValueChange={(v) => setLevel(v as 'ALL' | TecCourseLevel)}\n >\n <div className=\"mb-4 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between\">\n <TabsList>\n {levels.map((l) => (\n <TabsTrigger key={l.value} value={l.value}>\n {l.label}\n </TabsTrigger>\n ))}\n </TabsList>\n <div className=\"relative w-full sm:w-72\">\n <Search\n className=\"pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-text-muted\"\n size={16}\n />\n <Input\n className=\"pl-9\"\n aria-label={tr('technospam.courses.searchLabel', 'Search courses')}\n placeholder={tr('technospam.courses.searchPlaceholder', 'Search courses…')}\n value={search}\n onChange={(e) => setSearch(e.target.value)}\n onBlur={() => {\n const value = search.trim();\n if (value) {\n emit('technospam.search.performed', {\n workspaceId,\n entityType: 'course',\n queryLength: value.length,\n });\n }\n }}\n />\n </div>\n </div>\n\n <TabsContent value={level} className={TAB_PANEL_FOCUS_CLASS}>\n {loading && !courses.length ? (\n <LoadingGrid />\n ) : error ? (\n <ErrorView\n title={tr('technospam.common.errorLoadCourses', \"Couldn't load courses\")}\n error={error}\n />\n ) : filtered.length === 0 ? (\n <EmptyView\n icon={<BookOpen size={26} />}\n title={\n search || level !== 'ALL'\n ? tr('technospam.courses.emptySearchTitle', 'No matching courses')\n : tr('technospam.courses.emptyDefaultTitle', 'No courses published yet')\n }\n description={\n search || level !== 'ALL'\n ? tr(\n 'technospam.courses.emptySearchDesc',\n 'Try a different search or level filter.'\n )\n : tr(\n 'technospam.courses.emptyDefaultDesc',\n 'Published courses will appear here. Check back soon — TechnoSpam is pre-launch.'\n )\n }\n />\n ) : (\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n {filtered.map((course) => (\n <CourseCard\n key={course.id}\n course={course}\n onOpen={() => {\n emit('technospam.course.viewed', {\n workspaceId,\n entityId: course.id,\n entityType: 'TEC_COURSE',\n });\n navigate(`/courses/course-details-page?id=${course.id}`);\n }}\n />\n ))}\n </div>\n )}\n <LoadMore hasMore={hasMore} loading={loading} onClick={more} />\n </TabsContent>\n </Tabs>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CoursesCoursesPagePage.js","names":[],"sources":["../../../src/technospam/pages/CoursesCoursesPagePage.tsx"],"sourcesContent":["import { useMemo, useState } from 'react';\nimport {\n BookOpen,\n ChevronRight,\n Clock,\n GraduationCap,\n Layers,\n Search,\n Sparkles,\n} from 'lucide-react';\nimport {\n Badge,\n Button,\n GlassCard,\n Input,\n Tabs,\n TabsContent,\n TabsList,\n TabsTrigger,\n} from '@burdenoff/fe-libs/ui';\nimport { TAB_PANEL_FOCUS_CLASS } from '../lib/a11y';\nimport {\n CardOpenButton,\n EmptyView,\n ErrorView,\n LoadMore,\n LoadingGrid,\n MediaCover,\n PageLayout,\n PremiumBadge,\n StatCard,\n StatusBadge,\n topicIcon,\n} from '../components';\nimport { useTechnoSpam, useTechnoSpamNavigate } from '../providers/TechnoSpamProvider';\nimport {\n pageArg,\n useCoursesQuery,\n usePageLimit,\n type TecCourse,\n type TecCourseLevel,\n} from '../api/technospamApi';\nimport { formatDuration } from '../utils/format';\nimport { useTechnoSpamAnalytics } from '../hooks/useTechnoSpamAnalytics';\nimport { useTr } from '../hooks/useTr';\n\n/** Dot colour per level for the phone filter chips (mock technospam-19). */\nconst LEVEL_DOT: Partial<Record<'ALL' | TecCourseLevel, string>> = {\n BEGINNER: 'bg-status-success-text',\n INTERMEDIATE: 'bg-accent-primary',\n ADVANCED: 'bg-status-warning-text',\n EXPERT: 'bg-status-error-text',\n};\n\n/**\n * Catalog card. Phones get the mock's compact row (cover tile on the left,\n * level chip, summary, duration and tags on the right, a chevron affordance);\n * from `sm` up it is the vertical grid card.\n */\nfunction CourseCard({ course, onOpen }: { course: TecCourse; onOpen: () => void }) {\n return (\n <GlassCard\n data-testid=\"course-card\"\n className=\"group flex cursor-pointer flex-row overflow-hidden transition-all duration-200 hover:-translate-y-1 hover:shadow-lg sm:flex-col\"\n >\n <MediaCover\n seed={`${course.topic ?? ''} ${course.title}`}\n icon={topicIcon(course.topic ?? course.title)}\n className=\"h-auto min-h-28 w-24 shrink-0 p-2 sm:h-28 sm:min-h-0 sm:w-auto sm:p-4\"\n >\n <div className=\"hidden flex-wrap items-center gap-1.5 sm:flex\">\n <StatusBadge status={course.level} kind=\"level\" />\n {course.isPremiumOnly ? <PremiumBadge locked={course.isLocked} /> : null}\n </div>\n </MediaCover>\n <div className=\"flex min-w-0 flex-1 flex-col gap-2 p-3 sm:gap-3 sm:p-4\">\n <div className=\"min-w-0\">\n <div className=\"flex items-start gap-2\">\n <h3 className=\"min-w-0 flex-1 truncate text-base font-semibold text-text-primary group-hover:text-accent-primary\">\n <CardOpenButton onOpen={onOpen}>{course.title}</CardOpenButton>\n </h3>\n <ChevronRight\n size={18}\n aria-hidden=\"true\"\n className=\"mt-0.5 shrink-0 text-accent-primary sm:hidden\"\n />\n </div>\n <div className=\"mt-1 flex flex-wrap items-center gap-1.5 sm:hidden\">\n <StatusBadge status={course.level} kind=\"level\" />\n {course.isPremiumOnly ? <PremiumBadge locked={course.isLocked} /> : null}\n </div>\n {course.summary ? (\n <p className=\"mt-1 line-clamp-2 text-sm text-text-secondary\">{course.summary}</p>\n ) : null}\n </div>\n <div className=\"mt-auto flex flex-wrap items-center gap-3 text-xs text-text-muted\">\n {course.topic ? (\n <span className=\"inline-flex items-center gap-1\">\n <Layers size={13} /> {course.topic}\n </span>\n ) : null}\n <span className=\"inline-flex items-center gap-1\">\n <Clock size={13} /> {formatDuration(course.estimatedMinutes)}\n </span>\n </div>\n {course.tags.length ? (\n <div className=\"flex flex-wrap gap-1.5\">\n {course.tags.slice(0, 3).map((t) => (\n <Badge key={t} variant=\"secondary\" className=\"text-[11px]\">\n {t}\n </Badge>\n ))}\n </div>\n ) : null}\n </div>\n </GlassCard>\n );\n}\n\nexport function CoursesCoursesPagePage() {\n const navigate = useTechnoSpamNavigate();\n const { workspaceId } = useTechnoSpam();\n const { emit } = useTechnoSpamAnalytics();\n const tr = useTr();\n const [level, setLevel] = useState<'ALL' | TecCourseLevel>('ALL');\n const [search, setSearch] = useState('');\n\n const levels = useMemo(\n () => [\n { value: 'ALL' as const, label: tr('technospam.courses.filterAllLevels', 'All levels') },\n {\n value: 'BEGINNER' as TecCourseLevel,\n label: tr('technospam.courses.filterBeginner', 'Beginner'),\n },\n {\n value: 'INTERMEDIATE' as TecCourseLevel,\n label: tr('technospam.courses.filterIntermediate', 'Intermediate'),\n },\n {\n value: 'ADVANCED' as TecCourseLevel,\n label: tr('technospam.courses.filterAdvanced', 'Advanced'),\n },\n { value: 'EXPERT' as TecCourseLevel, label: tr('technospam.courses.filterExpert', 'Expert') },\n ],\n [tr]\n );\n\n const { limit, more } = usePageLimit();\n const { data, loading, error } = useCoursesQuery({\n status: 'PUBLISHED',\n pagination: pageArg(limit),\n ...(level !== 'ALL' ? { level } : {}),\n });\n\n const courses = data?.tecListCourses?.items ?? [];\n const hasMore = data?.tecListCourses?.pageInfo?.hasMore ?? false;\n const filtered = useMemo(() => {\n const q = search.trim().toLowerCase();\n if (!q) return courses;\n return courses.filter(\n (c) =>\n c.title.toLowerCase().includes(q) ||\n (c.topic ?? '').toLowerCase().includes(q) ||\n c.tags.some((t) => t.toLowerCase().includes(q))\n );\n }, [courses, search]);\n\n return (\n <PageLayout\n hero\n mobileSection\n title={tr('technospam.courses.title', 'Course Catalog')}\n description={tr(\n 'technospam.courses.description',\n 'Learn technology by doing — practical, project-based courses across the stack.'\n )}\n actions={\n <Button variant=\"outline\" onClick={() => navigate('/courses/my-learning-page')}>\n <GraduationCap size={16} /> {tr('technospam.common.myLearning', 'My Learning')}\n </Button>\n }\n >\n {/* Catalog stats are a desktop summary; phones go straight to the list. */}\n <div className=\"mb-5 hidden grid-cols-2 gap-3 sm:grid sm:grid-cols-4\">\n <StatCard\n label={tr('technospam.courses.statCourses', 'Courses')}\n value={courses.length}\n icon={BookOpen}\n tone=\"blue\"\n />\n <StatCard\n label={tr('technospam.courses.statBeginnerFriendly', 'Beginner-friendly')}\n value={courses.filter((c) => c.level === 'BEGINNER').length}\n icon={Sparkles}\n tone=\"green\"\n />\n <StatCard\n label={tr('technospam.courses.statAdvancedPlus', 'Advanced+')}\n value={courses.filter((c) => c.level === 'ADVANCED' || c.level === 'EXPERT').length}\n icon={GraduationCap}\n tone=\"purple\"\n />\n <StatCard\n label={tr('technospam.courses.statTotalContent', 'Total content')}\n value={formatDuration(courses.reduce((s, c) => s + c.estimatedMinutes, 0))}\n icon={Clock}\n tone=\"teal\"\n />\n </div>\n\n {/* The tabs filter one list, so that list is their tab panel: Radix points every\n trigger's aria-controls at a panel id, and without a TabsContent the id\n resolved to nothing (axe aria-valid-attr-value, critical; BOFF-3157). */}\n <Tabs\n className=\"gap-0\"\n value={level}\n onValueChange={(v) => setLevel(v as 'ALL' | TecCourseLevel)}\n >\n <div className=\"mb-4 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between\">\n {/* Phones: a horizontally scrolling row of level chips with 44px\n touch targets; from sm up the regular segmented tabs. */}\n <div className=\"-mx-4 overflow-x-auto px-4 sm:mx-0 sm:overflow-visible sm:px-0\">\n <TabsList className=\"h-auto w-max gap-1.5 bg-transparent p-0 sm:h-9 sm:gap-0 sm:bg-bg-sunken sm:p-[3px]\">\n {levels.map((l) => (\n <TabsTrigger\n key={l.value}\n value={l.value}\n className=\"min-h-11 flex-none gap-1.5 rounded-full border-border-subtle px-4 data-[state=active]:border-accent-primary sm:min-h-0 sm:flex-1 sm:rounded-button sm:border-transparent sm:px-3 sm:data-[state=active]:border-transparent\"\n >\n {LEVEL_DOT[l.value] ? (\n <span\n aria-hidden=\"true\"\n className={`h-2 w-2 rounded-full sm:hidden ${LEVEL_DOT[l.value]}`}\n />\n ) : null}\n {l.label}\n </TabsTrigger>\n ))}\n </TabsList>\n </div>\n <div className=\"relative w-full sm:w-72\">\n <Search\n className=\"pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-text-muted\"\n size={16}\n />\n <Input\n className=\"pl-9\"\n aria-label={tr('technospam.courses.searchLabel', 'Search courses')}\n placeholder={tr('technospam.courses.searchPlaceholder', 'Search courses…')}\n value={search}\n onChange={(e) => setSearch(e.target.value)}\n onBlur={() => {\n const value = search.trim();\n if (value) {\n emit('technospam.search.performed', {\n workspaceId,\n entityType: 'course',\n queryLength: value.length,\n });\n }\n }}\n />\n </div>\n </div>\n\n <TabsContent value={level} className={TAB_PANEL_FOCUS_CLASS}>\n {loading && !courses.length ? (\n <LoadingGrid />\n ) : error ? (\n <ErrorView\n title={tr('technospam.common.errorLoadCourses', \"Couldn't load courses\")}\n error={error}\n />\n ) : filtered.length === 0 ? (\n <EmptyView\n icon={<BookOpen size={26} />}\n title={\n search || level !== 'ALL'\n ? tr('technospam.courses.emptySearchTitle', 'No matching courses')\n : tr('technospam.courses.emptyDefaultTitle', 'No courses published yet')\n }\n description={\n search || level !== 'ALL'\n ? tr(\n 'technospam.courses.emptySearchDesc',\n 'Try a different search or level filter.'\n )\n : tr(\n 'technospam.courses.emptyDefaultDesc',\n 'Published courses will appear here. Check back soon — TechnoSpam is pre-launch.'\n )\n }\n />\n ) : (\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n {filtered.map((course) => (\n <CourseCard\n key={course.id}\n course={course}\n onOpen={() => {\n emit('technospam.course.viewed', {\n workspaceId,\n entityId: course.id,\n entityType: 'TEC_COURSE',\n });\n navigate(`/courses/course-details-page?id=${course.id}`);\n }}\n />\n ))}\n </div>\n )}\n <LoadMore hasMore={hasMore} loading={loading} onClick={more} />\n </TabsContent>\n </Tabs>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
@@ -24,7 +24,7 @@ function k({ e, onOpen: n, focused: r, onToggle: o }) {
|
|
|
24
24
|
..._(r, o),
|
|
25
25
|
children: [
|
|
26
26
|
/* @__PURE__ */ T("span", {
|
|
27
|
-
className: t("flex h-12 w-12 shrink-0 items-center justify-center rounded-xl bg-gradient-to-br text-
|
|
27
|
+
className: t("flex h-12 w-12 shrink-0 items-center justify-center rounded-xl bg-gradient-to-br text-action-primary-text shadow-sm transition-transform duration-200 group-hover:scale-105", i(l)),
|
|
28
28
|
children: y(a(l), { size: 20 })
|
|
29
29
|
}),
|
|
30
30
|
/* @__PURE__ */ E("div", {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CoursesMyLearningPagePage.js","names":[],"sources":["../../../src/technospam/pages/CoursesMyLearningPagePage.tsx"],"sourcesContent":["import { createElement } from 'react';\nimport { Award, BookOpen, GraduationCap, PlayCircle, ShieldCheck } from 'lucide-react';\nimport { Button, GlassCard } from '@burdenoff/fe-libs/ui';\nimport {\n CertificateVerifyLink,\n EmptyView,\n ErrorView,\n LoadingRows,\n PageLayout,\n StatCard,\n StatusBadge,\n coverGradient,\n topicIcon,\n} from '../components';\nimport { useTechnoSpamNavigate } from '../providers/TechnoSpamProvider';\nimport {\n useMyCertificatesQuery,\n useMyEnrollmentsQuery,\n type TecEnrollment,\n} from '../api/technospamApi';\nimport { formatDate, relativeTime } from '../utils/format';\nimport { cn } from '../utils/cn';\nimport {\n FOCUSED_CARD_CLASS,\n focusableCardProps,\n useFocusedRecordParam,\n} from '../hooks/useFocusedRecordParam';\nimport { FOCUS_PARAMS } from '../utils/pageContext';\nimport { resumePath } from '../utils/lessonPath';\n\nfunction EnrollmentRow({\n e,\n onOpen,\n focused,\n onToggle,\n}: {\n e: TecEnrollment;\n onOpen: () => void;\n /** The focused enrollment (`?enrollmentId=`) is the one the assistant acts on. */\n focused: boolean;\n onToggle: () => void;\n}) {\n const seed = `${e.course?.topic ?? ''} ${e.course?.title ?? 'Course'}`;\n return (\n <GlassCard\n className={cn(\n 'group flex cursor-pointer flex-col gap-3 p-4 transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md sm:flex-row sm:items-center sm:gap-4',\n focused && FOCUSED_CARD_CLASS\n )}\n {...focusableCardProps(focused, onToggle)}\n >\n <span\n className={cn(\n 'flex h-12 w-12 shrink-0 items-center justify-center rounded-xl bg-gradient-to-br text-white shadow-sm transition-transform duration-200 group-hover:scale-105',\n coverGradient(seed)\n )}\n >\n {createElement(topicIcon(seed), { size: 20 })}\n </span>\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <button\n type=\"button\"\n aria-pressed={focused}\n onClick={onToggle}\n className=\"truncate text-left text-sm font-semibold text-text-primary group-hover:text-accent-primary\"\n >\n {e.course?.title ?? 'Course'}\n </button>\n <StatusBadge status={e.status} />\n </div>\n <div className=\"mt-1 flex flex-wrap items-center gap-2 text-xs text-text-muted\">\n <span>{e.lessonsCompleted} lessons done</span>\n <span aria-hidden>·</span>\n <span>Active {relativeTime(e.lastActiveAt ?? e.startedAt)}</span>\n </div>\n <div className=\"mt-2 h-1.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: `${e.progressPct}%` }}\n />\n </div>\n </div>\n <div className=\"flex items-center justify-between gap-3 sm:flex-col sm:items-end sm:justify-center\">\n <div className=\"text-lg font-semibold tabular-nums text-text-primary\">{e.progressPct}%</div>\n <Button variant=\"ghost\" size=\"sm\" onClick={onOpen} data-testid={`resume-${e.courseId}`}>\n <PlayCircle size={15} /> Resume\n </Button>\n </div>\n </GlassCard>\n );\n}\n\n// Stable sibling lists: focusing an enrollment clears a focused certificate and vice versa.\nconst CERTIFICATE_SIBLING = [FOCUS_PARAMS.certificate] as const;\nconst ENROLLMENT_SIBLING = [FOCUS_PARAMS.enrollment] as const;\n\nexport function CoursesMyLearningPagePage() {\n const navigate = useTechnoSpamNavigate();\n\n // BOFF-10481: the learner's own enrollments and certificates come from the\n // canonical, self-scoped Buildmyiq fields. The previous cross-learner\n // `tecListEnrollments` / list-all reads needed an instructor permission a\n // plain learner does not hold. Each enrollment carries its next lesson, so\n // \"Resume\" (BOFF-4107) deep-links into the next incomplete lesson.\n const { enrollments, loading, error } = useMyEnrollmentsQuery();\n const { certificates } = useMyCertificatesQuery();\n // The focused enrollment / certificate live in the URL so the assistant's\n // page context names them (\"how far am I in this course?\").\n const [focusedEnrollmentId, setFocusedEnrollmentId] = useFocusedRecordParam(\n FOCUS_PARAMS.enrollment,\n CERTIFICATE_SIBLING\n );\n const [focusedCertificateId, setFocusedCertificateId] = useFocusedRecordParam(\n FOCUS_PARAMS.certificate,\n ENROLLMENT_SIBLING\n );\n\n const completed = enrollments.filter((e) => e.status === 'COMPLETED').length;\n const inProgress = enrollments.filter(\n (e) => e.status === 'IN_PROGRESS' || e.status === 'ENROLLED'\n ).length;\n\n return (\n <PageLayout\n hero\n title=\"My Learning\"\n description=\"Track your enrolled courses, progress, and earned certificates.\"\n actions={\n <Button variant=\"outline\" onClick={() => navigate('/courses/courses-page')}>\n <BookOpen size={16} /> Browse catalog\n </Button>\n }\n >\n <div className=\"mb-6 grid grid-cols-2 gap-3 sm:grid-cols-4\">\n <StatCard\n label=\"Enrolled\"\n value={enrollments.length}\n hint=\"Courses you've enrolled in\"\n icon={BookOpen}\n tone=\"blue\"\n />\n <StatCard\n label=\"In progress\"\n value={inProgress}\n hint=\"Courses in progress\"\n icon={PlayCircle}\n tone=\"indigo\"\n />\n <StatCard\n label=\"Completed\"\n value={completed}\n hint=\"Courses completed\"\n icon={GraduationCap}\n tone=\"green\"\n />\n <StatCard\n label=\"Certificates\"\n value={certificates.length}\n hint=\"Certificates earned\"\n icon={Award}\n tone=\"purple\"\n />\n </div>\n\n {loading && !enrollments.length ? (\n <LoadingRows count={3} height=\"h-24\" />\n ) : error ? (\n <ErrorView title=\"Couldn't load your learning\" error={error} />\n ) : enrollments.length === 0 ? (\n <EmptyView\n icon={<BookOpen size={26} />}\n title=\"You haven't enrolled yet\"\n description=\"Browse the catalog and enroll in a course to start building skill by doing.\"\n action={<Button onClick={() => navigate('/courses/courses-page')}>Browse courses</Button>}\n />\n ) : (\n <div className=\"space-y-3\">\n {enrollments.map((e) => (\n <EnrollmentRow\n key={e.id}\n e={e}\n onOpen={() => navigate(resumePath(e.courseId, e.nextLessonId))}\n focused={focusedEnrollmentId === e.id}\n onToggle={() => setFocusedEnrollmentId(focusedEnrollmentId === e.id ? null : e.id)}\n />\n ))}\n </div>\n )}\n\n {certificates.length > 0 ? (\n <div className=\"mt-8\">\n <h2 className=\"mb-3 flex items-center gap-2 text-base font-semibold text-text-primary\">\n <Award size={18} className=\"text-accent-teal\" /> Certificates\n </h2>\n <div className=\"grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3\">\n {certificates.map((c) => (\n <GlassCard\n key={c.id}\n className={cn(\n 'group flex cursor-pointer flex-col gap-3 p-4 transition-all duration-200 hover:-translate-y-1 hover:shadow-lg',\n focusedCertificateId === c.id && FOCUSED_CARD_CLASS\n )}\n {...focusableCardProps(focusedCertificateId === c.id, () =>\n setFocusedCertificateId(focusedCertificateId === c.id ? null : c.id)\n )}\n >\n <div className=\"flex items-center gap-3\">\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 transition-transform duration-200 group-hover:scale-105\">\n <Award size={22} />\n </span>\n <div className=\"min-w-0\">\n <button\n type=\"button\"\n aria-pressed={focusedCertificateId === c.id}\n onClick={() =>\n setFocusedCertificateId(focusedCertificateId === c.id ? null : c.id)\n }\n className=\"block max-w-full truncate text-left text-sm font-medium text-text-primary\"\n >\n {c.course?.title ?? 'Certificate'}\n </button>\n <div className=\"truncate text-xs text-text-muted\">Serial {c.serial}</div>\n </div>\n </div>\n <div className=\"mt-auto flex items-center justify-between border-t border-border-seam pt-3 text-xs text-text-muted\">\n <span>\n Score <span className=\"font-semibold text-text-primary\">{c.score}</span> ·{' '}\n {formatDate(c.issuedAt)}\n </span>\n {c.status === 'ISSUED' ? (\n <span className=\"inline-flex items-center gap-1 rounded-full border border-status-success-border bg-status-success-bg-subtle px-2 py-0.5 font-medium text-status-success-text\">\n <ShieldCheck size={12} /> Verified\n </span>\n ) : (\n <StatusBadge status={c.status} />\n )}\n </div>\n <CertificateVerifyLink serial={c.serial} compact />\n </GlassCard>\n ))}\n </div>\n </div>\n ) : null}\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CoursesMyLearningPagePage.js","names":[],"sources":["../../../src/technospam/pages/CoursesMyLearningPagePage.tsx"],"sourcesContent":["import { createElement } from 'react';\nimport { Award, BookOpen, GraduationCap, PlayCircle, ShieldCheck } from 'lucide-react';\nimport { Button, GlassCard } from '@burdenoff/fe-libs/ui';\nimport {\n CertificateVerifyLink,\n EmptyView,\n ErrorView,\n LoadingRows,\n PageLayout,\n StatCard,\n StatusBadge,\n coverGradient,\n topicIcon,\n} from '../components';\nimport { useTechnoSpamNavigate } from '../providers/TechnoSpamProvider';\nimport {\n useMyCertificatesQuery,\n useMyEnrollmentsQuery,\n type TecEnrollment,\n} from '../api/technospamApi';\nimport { formatDate, relativeTime } from '../utils/format';\nimport { cn } from '../utils/cn';\nimport {\n FOCUSED_CARD_CLASS,\n focusableCardProps,\n useFocusedRecordParam,\n} from '../hooks/useFocusedRecordParam';\nimport { FOCUS_PARAMS } from '../utils/pageContext';\nimport { resumePath } from '../utils/lessonPath';\n\nfunction EnrollmentRow({\n e,\n onOpen,\n focused,\n onToggle,\n}: {\n e: TecEnrollment;\n onOpen: () => void;\n /** The focused enrollment (`?enrollmentId=`) is the one the assistant acts on. */\n focused: boolean;\n onToggle: () => void;\n}) {\n const seed = `${e.course?.topic ?? ''} ${e.course?.title ?? 'Course'}`;\n return (\n <GlassCard\n className={cn(\n 'group flex cursor-pointer flex-col gap-3 p-4 transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md sm:flex-row sm:items-center sm:gap-4',\n focused && FOCUSED_CARD_CLASS\n )}\n {...focusableCardProps(focused, onToggle)}\n >\n <span\n className={cn(\n 'flex h-12 w-12 shrink-0 items-center justify-center rounded-xl bg-gradient-to-br text-action-primary-text shadow-sm transition-transform duration-200 group-hover:scale-105',\n coverGradient(seed)\n )}\n >\n {createElement(topicIcon(seed), { size: 20 })}\n </span>\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <button\n type=\"button\"\n aria-pressed={focused}\n onClick={onToggle}\n className=\"truncate text-left text-sm font-semibold text-text-primary group-hover:text-accent-primary\"\n >\n {e.course?.title ?? 'Course'}\n </button>\n <StatusBadge status={e.status} />\n </div>\n <div className=\"mt-1 flex flex-wrap items-center gap-2 text-xs text-text-muted\">\n <span>{e.lessonsCompleted} lessons done</span>\n <span aria-hidden>·</span>\n <span>Active {relativeTime(e.lastActiveAt ?? e.startedAt)}</span>\n </div>\n <div className=\"mt-2 h-1.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: `${e.progressPct}%` }}\n />\n </div>\n </div>\n <div className=\"flex items-center justify-between gap-3 sm:flex-col sm:items-end sm:justify-center\">\n <div className=\"text-lg font-semibold tabular-nums text-text-primary\">{e.progressPct}%</div>\n <Button variant=\"ghost\" size=\"sm\" onClick={onOpen} data-testid={`resume-${e.courseId}`}>\n <PlayCircle size={15} /> Resume\n </Button>\n </div>\n </GlassCard>\n );\n}\n\n// Stable sibling lists: focusing an enrollment clears a focused certificate and vice versa.\nconst CERTIFICATE_SIBLING = [FOCUS_PARAMS.certificate] as const;\nconst ENROLLMENT_SIBLING = [FOCUS_PARAMS.enrollment] as const;\n\nexport function CoursesMyLearningPagePage() {\n const navigate = useTechnoSpamNavigate();\n\n // BOFF-10481: the learner's own enrollments and certificates come from the\n // canonical, self-scoped Buildmyiq fields. The previous cross-learner\n // `tecListEnrollments` / list-all reads needed an instructor permission a\n // plain learner does not hold. Each enrollment carries its next lesson, so\n // \"Resume\" (BOFF-4107) deep-links into the next incomplete lesson.\n const { enrollments, loading, error } = useMyEnrollmentsQuery();\n const { certificates } = useMyCertificatesQuery();\n // The focused enrollment / certificate live in the URL so the assistant's\n // page context names them (\"how far am I in this course?\").\n const [focusedEnrollmentId, setFocusedEnrollmentId] = useFocusedRecordParam(\n FOCUS_PARAMS.enrollment,\n CERTIFICATE_SIBLING\n );\n const [focusedCertificateId, setFocusedCertificateId] = useFocusedRecordParam(\n FOCUS_PARAMS.certificate,\n ENROLLMENT_SIBLING\n );\n\n const completed = enrollments.filter((e) => e.status === 'COMPLETED').length;\n const inProgress = enrollments.filter(\n (e) => e.status === 'IN_PROGRESS' || e.status === 'ENROLLED'\n ).length;\n\n return (\n <PageLayout\n hero\n title=\"My Learning\"\n description=\"Track your enrolled courses, progress, and earned certificates.\"\n actions={\n <Button variant=\"outline\" onClick={() => navigate('/courses/courses-page')}>\n <BookOpen size={16} /> Browse catalog\n </Button>\n }\n >\n <div className=\"mb-6 grid grid-cols-2 gap-3 sm:grid-cols-4\">\n <StatCard\n label=\"Enrolled\"\n value={enrollments.length}\n hint=\"Courses you've enrolled in\"\n icon={BookOpen}\n tone=\"blue\"\n />\n <StatCard\n label=\"In progress\"\n value={inProgress}\n hint=\"Courses in progress\"\n icon={PlayCircle}\n tone=\"indigo\"\n />\n <StatCard\n label=\"Completed\"\n value={completed}\n hint=\"Courses completed\"\n icon={GraduationCap}\n tone=\"green\"\n />\n <StatCard\n label=\"Certificates\"\n value={certificates.length}\n hint=\"Certificates earned\"\n icon={Award}\n tone=\"purple\"\n />\n </div>\n\n {loading && !enrollments.length ? (\n <LoadingRows count={3} height=\"h-24\" />\n ) : error ? (\n <ErrorView title=\"Couldn't load your learning\" error={error} />\n ) : enrollments.length === 0 ? (\n <EmptyView\n icon={<BookOpen size={26} />}\n title=\"You haven't enrolled yet\"\n description=\"Browse the catalog and enroll in a course to start building skill by doing.\"\n action={<Button onClick={() => navigate('/courses/courses-page')}>Browse courses</Button>}\n />\n ) : (\n <div className=\"space-y-3\">\n {enrollments.map((e) => (\n <EnrollmentRow\n key={e.id}\n e={e}\n onOpen={() => navigate(resumePath(e.courseId, e.nextLessonId))}\n focused={focusedEnrollmentId === e.id}\n onToggle={() => setFocusedEnrollmentId(focusedEnrollmentId === e.id ? null : e.id)}\n />\n ))}\n </div>\n )}\n\n {certificates.length > 0 ? (\n <div className=\"mt-8\">\n <h2 className=\"mb-3 flex items-center gap-2 text-base font-semibold text-text-primary\">\n <Award size={18} className=\"text-accent-teal\" /> Certificates\n </h2>\n <div className=\"grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3\">\n {certificates.map((c) => (\n <GlassCard\n key={c.id}\n className={cn(\n 'group flex cursor-pointer flex-col gap-3 p-4 transition-all duration-200 hover:-translate-y-1 hover:shadow-lg',\n focusedCertificateId === c.id && FOCUSED_CARD_CLASS\n )}\n {...focusableCardProps(focusedCertificateId === c.id, () =>\n setFocusedCertificateId(focusedCertificateId === c.id ? null : c.id)\n )}\n >\n <div className=\"flex items-center gap-3\">\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 transition-transform duration-200 group-hover:scale-105\">\n <Award size={22} />\n </span>\n <div className=\"min-w-0\">\n <button\n type=\"button\"\n aria-pressed={focusedCertificateId === c.id}\n onClick={() =>\n setFocusedCertificateId(focusedCertificateId === c.id ? null : c.id)\n }\n className=\"block max-w-full truncate text-left text-sm font-medium text-text-primary\"\n >\n {c.course?.title ?? 'Certificate'}\n </button>\n <div className=\"truncate text-xs text-text-muted\">Serial {c.serial}</div>\n </div>\n </div>\n <div className=\"mt-auto flex items-center justify-between border-t border-border-seam pt-3 text-xs text-text-muted\">\n <span>\n Score <span className=\"font-semibold text-text-primary\">{c.score}</span> ·{' '}\n {formatDate(c.issuedAt)}\n </span>\n {c.status === 'ISSUED' ? (\n <span className=\"inline-flex items-center gap-1 rounded-full border border-status-success-border bg-status-success-bg-subtle px-2 py-0.5 font-medium text-status-success-text\">\n <ShieldCheck size={12} /> Verified\n </span>\n ) : (\n <StatusBadge status={c.status} />\n )}\n </div>\n <CertificateVerifyLink serial={c.serial} compact />\n </GlassCard>\n ))}\n </div>\n </div>\n ) : null}\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DashboardDashboardPagePage.d.ts","sourceRoot":"","sources":["../../../src/technospam/pages/DashboardDashboardPagePage.tsx"],"names":[],"mappings":"AAqNA,wBAAgB,0BAA0B,
|
|
1
|
+
{"version":3,"file":"DashboardDashboardPagePage.d.ts","sourceRoot":"","sources":["../../../src/technospam/pages/DashboardDashboardPagePage.tsx"],"names":[],"mappings":"AAqNA,wBAAgB,0BAA0B,gCA8VzC"}
|
|
@@ -48,7 +48,7 @@ function z({ e, resumeLessonId: t, onOpen: r }) {
|
|
|
48
48
|
type: "button",
|
|
49
49
|
onClick: r,
|
|
50
50
|
"data-resume-lesson": t ?? void 0,
|
|
51
|
-
className: "group flex flex-col overflow-hidden rounded-xl border border-border-seam bg-bg-surface text-left transition-all duration-200 hover:-translate-y-1 hover:shadow-lg focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-[var(--color-focus-ring)]",
|
|
51
|
+
className: "group flex w-[72%] shrink-0 snap-start flex-col overflow-hidden rounded-xl sm:w-auto sm:shrink border border-border-seam bg-bg-surface text-left transition-all duration-200 hover:-translate-y-1 hover:shadow-lg focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-[var(--color-focus-ring)]",
|
|
52
52
|
children: [/* @__PURE__ */ P(a, {
|
|
53
53
|
seed: c,
|
|
54
54
|
icon: o(e.course?.topic ?? s),
|
|
@@ -57,7 +57,7 @@ function z({ e, resumeLessonId: t, onOpen: r }) {
|
|
|
57
57
|
status: e.course.level,
|
|
58
58
|
kind: "level"
|
|
59
59
|
}) : /* @__PURE__ */ N("span", {}), /* @__PURE__ */ N("span", {
|
|
60
|
-
className: "flex h-7 w-7 items-center justify-center rounded-full bg-
|
|
60
|
+
className: "flex h-7 w-7 items-center justify-center rounded-full bg-action-primary-text/15 text-action-primary-text backdrop-blur transition-transform duration-200 group-hover:scale-110",
|
|
61
61
|
children: /* @__PURE__ */ N(A, { size: 16 })
|
|
62
62
|
})]
|
|
63
63
|
}), /* @__PURE__ */ P("div", {
|
|
@@ -157,18 +157,18 @@ function ne({ h: e, onOpen: t }) {
|
|
|
157
157
|
children: [/* @__PURE__ */ P("div", {
|
|
158
158
|
className: "min-w-0",
|
|
159
159
|
children: [/* @__PURE__ */ P("div", {
|
|
160
|
-
className: "flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wide text-
|
|
160
|
+
className: "flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wide text-action-primary-text/80",
|
|
161
161
|
children: [
|
|
162
162
|
/* @__PURE__ */ N(j, { size: 12 }),
|
|
163
163
|
" ",
|
|
164
164
|
r("technospam.common.openHackathon", "Open hackathon")
|
|
165
165
|
]
|
|
166
166
|
}), /* @__PURE__ */ N("h3", {
|
|
167
|
-
className: "mt-0.5 truncate text-sm font-semibold text-
|
|
167
|
+
className: "mt-0.5 truncate text-sm font-semibold text-action-primary-text drop-shadow-sm",
|
|
168
168
|
children: e.title
|
|
169
169
|
})]
|
|
170
170
|
}), e.prizePool > 0 ? /* @__PURE__ */ N("span", {
|
|
171
|
-
className: "shrink-0 rounded-full bg-
|
|
171
|
+
className: "shrink-0 rounded-full bg-action-primary-text/20 px-2.5 py-1 text-xs font-semibold text-action-primary-text backdrop-blur",
|
|
172
172
|
children: l(e.prizePool, e.currency)
|
|
173
173
|
}) : null]
|
|
174
174
|
}), /* @__PURE__ */ P("div", {
|
|
@@ -199,6 +199,7 @@ function H() {
|
|
|
199
199
|
}, [K, H]), $ = q.slice(0, 4);
|
|
200
200
|
return /* @__PURE__ */ P(r, {
|
|
201
201
|
hero: !0,
|
|
202
|
+
mobileSection: !0,
|
|
202
203
|
title: c("technospam.dashboard.title", "Welcome back, {{name}}", { name: re }),
|
|
203
204
|
description: c("technospam.dashboard.description", "Your learning hub — pick up where you left off, keep your skills sharp, and jump into what's next."),
|
|
204
205
|
actions: /* @__PURE__ */ P(F, {
|
|
@@ -230,18 +231,20 @@ function H() {
|
|
|
230
231
|
label: c("technospam.dashboard.upcomingSessions", "Upcoming sessions"),
|
|
231
232
|
value: Z.length,
|
|
232
233
|
icon: T,
|
|
233
|
-
tone: "amber"
|
|
234
|
+
tone: "amber",
|
|
235
|
+
className: "hidden sm:flex"
|
|
234
236
|
}),
|
|
235
237
|
/* @__PURE__ */ N(i, {
|
|
236
238
|
label: c("technospam.dashboard.certificates", "Certificates"),
|
|
237
239
|
value: G.length,
|
|
238
240
|
icon: te,
|
|
239
|
-
tone: "purple"
|
|
241
|
+
tone: "purple",
|
|
242
|
+
className: "hidden sm:flex"
|
|
240
243
|
})
|
|
241
244
|
]
|
|
242
245
|
}),
|
|
243
246
|
/* @__PURE__ */ P("div", {
|
|
244
|
-
className: "mb-6
|
|
247
|
+
className: "mb-6 hidden grid-cols-2 gap-3 sm:grid sm:grid-cols-4",
|
|
245
248
|
children: [
|
|
246
249
|
/* @__PURE__ */ P(F, {
|
|
247
250
|
variant: "outline",
|
|
@@ -282,11 +285,11 @@ function H() {
|
|
|
282
285
|
]
|
|
283
286
|
}),
|
|
284
287
|
/* @__PURE__ */ P("div", {
|
|
285
|
-
className: "
|
|
288
|
+
className: "flex flex-col gap-6 lg:grid lg:grid-cols-3",
|
|
286
289
|
children: [/* @__PURE__ */ P("div", {
|
|
287
|
-
className: "
|
|
290
|
+
className: "contents lg:col-span-2 lg:block lg:space-y-6",
|
|
288
291
|
children: [/* @__PURE__ */ P(I, {
|
|
289
|
-
className: "p-5",
|
|
292
|
+
className: "order-1 p-4 sm:p-5",
|
|
290
293
|
children: [/* @__PURE__ */ N(L, {
|
|
291
294
|
title: c("technospam.dashboard.continueLearning", "Continue learning"),
|
|
292
295
|
actionLabel: c("technospam.dashboard.myLearning", "My Learning"),
|
|
@@ -306,7 +309,8 @@ function H() {
|
|
|
306
309
|
children: c("technospam.common.browseCourses", "Browse courses")
|
|
307
310
|
})
|
|
308
311
|
}) : /* @__PURE__ */ N("div", {
|
|
309
|
-
|
|
312
|
+
"data-testid": "continue-learning-list",
|
|
313
|
+
className: "-mx-1 flex snap-x snap-mandatory gap-3 overflow-x-auto px-1 pb-1 sm:mx-0 sm:grid sm:snap-none sm:grid-cols-2 sm:gap-4 sm:overflow-visible sm:px-0 sm:pb-0 lg:grid-cols-3",
|
|
310
314
|
children: Y.map((e) => /* @__PURE__ */ N(z, {
|
|
311
315
|
e,
|
|
312
316
|
resumeLessonId: e.nextLessonId,
|
|
@@ -314,7 +318,7 @@ function H() {
|
|
|
314
318
|
}, e.id))
|
|
315
319
|
})]
|
|
316
320
|
}), /* @__PURE__ */ P(I, {
|
|
317
|
-
className: "p-5",
|
|
321
|
+
className: "order-3 p-4 sm:p-5",
|
|
318
322
|
children: [/* @__PURE__ */ N(L, {
|
|
319
323
|
title: c("technospam.dashboard.recommendedForYou", "Recommended for you"),
|
|
320
324
|
actionLabel: c("technospam.common.viewAll", "View all"),
|
|
@@ -338,10 +342,10 @@ function H() {
|
|
|
338
342
|
})]
|
|
339
343
|
})]
|
|
340
344
|
}), /* @__PURE__ */ P("div", {
|
|
341
|
-
className: "space-y-6",
|
|
345
|
+
className: "contents lg:block lg:space-y-6",
|
|
342
346
|
children: [
|
|
343
347
|
/* @__PURE__ */ P(I, {
|
|
344
|
-
className: "p-5",
|
|
348
|
+
className: "order-2 p-4 sm:p-5",
|
|
345
349
|
children: [/* @__PURE__ */ N(L, {
|
|
346
350
|
title: c("technospam.dashboard.upcomingSessionsTitle", "Upcoming sessions"),
|
|
347
351
|
actionLabel: c("technospam.common.allSessions", "All sessions"),
|
|
@@ -364,7 +368,7 @@ function H() {
|
|
|
364
368
|
}) : /* @__PURE__ */ N("div", { children: Z.slice(0, 5).map((e) => /* @__PURE__ */ N(V, { s: e }, e.id)) })]
|
|
365
369
|
}),
|
|
366
370
|
/* @__PURE__ */ P(I, {
|
|
367
|
-
className: "p-5",
|
|
371
|
+
className: "order-2 p-4 sm:p-5",
|
|
368
372
|
children: [/* @__PURE__ */ N(L, {
|
|
369
373
|
title: c("technospam.dashboard.openHackathons", "Open hackathons"),
|
|
370
374
|
actionLabel: c("technospam.common.viewAll", "View all"),
|
|
@@ -388,7 +392,7 @@ function H() {
|
|
|
388
392
|
})]
|
|
389
393
|
}),
|
|
390
394
|
G.length > 0 ? /* @__PURE__ */ P(I, {
|
|
391
|
-
className: "p-5",
|
|
395
|
+
className: "order-4 p-4 sm:p-5",
|
|
392
396
|
children: [/* @__PURE__ */ N(L, { title: c("technospam.common.latestCertificate", "Latest certificate") }), /* @__PURE__ */ P("div", {
|
|
393
397
|
className: "flex items-center gap-3",
|
|
394
398
|
children: [/* @__PURE__ */ N("span", {
|