@burdenoff/microfe-technospam 2026.1101.1 → 2026.1101.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/stories/journeys/Courses.stories.d.ts +5 -0
- package/dist/stories/journeys/Courses.stories.d.ts.map +1 -1
- package/dist/stories/journeys/Mentorship.stories.d.ts +5 -0
- package/dist/stories/journeys/Mentorship.stories.d.ts.map +1 -1
- package/dist/stories/journeys/Problems.stories.d.ts.map +1 -1
- package/dist/technospam/api/technospamApi.d.ts +15 -0
- package/dist/technospam/api/technospamApi.d.ts.map +1 -1
- package/dist/technospam/api/technospamApi.js +200 -151
- package/dist/technospam/api/technospamApi.js.map +1 -1
- package/dist/technospam/components/CardOpenButton.d.ts +26 -0
- package/dist/technospam/components/CardOpenButton.d.ts.map +1 -0
- package/dist/technospam/components/CardOpenButton.js +15 -0
- package/dist/technospam/components/CardOpenButton.js.map +1 -0
- package/dist/technospam/components/ProblemsTable.d.ts +19 -0
- package/dist/technospam/components/ProblemsTable.d.ts.map +1 -0
- package/dist/technospam/components/ProblemsTable.js +117 -0
- package/dist/technospam/components/ProblemsTable.js.map +1 -0
- package/dist/technospam/components/index.d.ts +2 -0
- package/dist/technospam/components/index.d.ts.map +1 -1
- package/dist/technospam/components/index.js +3 -1
- package/dist/technospam/pages/ApplicationsProblemDetailsPagePage.js +38 -38
- 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 +197 -144
- package/dist/technospam/pages/ApplicationsProblemsPagePage.js.map +1 -1
- package/dist/technospam/pages/CoursesCourseDetailsPagePage.js +195 -195
- 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 +87 -84
- package/dist/technospam/pages/CoursesCoursesPagePage.js.map +1 -1
- package/dist/technospam/pages/CoursesLessonPlayerPagePage.js +36 -36
- package/dist/technospam/pages/CoursesLessonPlayerPagePage.js.map +1 -1
- package/dist/technospam/pages/HackathonsHackathonsPagePage.d.ts.map +1 -1
- package/dist/technospam/pages/HackathonsHackathonsPagePage.js +75 -72
- package/dist/technospam/pages/HackathonsHackathonsPagePage.js.map +1 -1
- package/dist/technospam/pages/MentorshipMentorsPagePage.d.ts.map +1 -1
- package/dist/technospam/pages/MentorshipMentorsPagePage.js +1 -0
- package/dist/technospam/pages/MentorshipMentorsPagePage.js.map +1 -1
- package/dist/technospam/utils/problemCatalog.d.ts +27 -0
- package/dist/technospam/utils/problemCatalog.d.ts.map +1 -0
- package/dist/technospam/utils/problemCatalog.js +68 -0
- package/dist/technospam/utils/problemCatalog.js.map +1 -0
- package/package.json +1 -1
|
@@ -3,17 +3,17 @@ import { useTechnoSpamAnalytics as n } from "../hooks/useTechnoSpamAnalytics.js"
|
|
|
3
3
|
import { cn as r } from "../utils/cn.js";
|
|
4
4
|
import { PageLayout as i } from "../components/PageLayout.js";
|
|
5
5
|
import { formatDuration as ee, humanizeEnum as a } from "../utils/format.js";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import { notify as
|
|
9
|
-
import { useCompleteLesson as
|
|
6
|
+
import { UpgradeGate as o } from "../components/UpgradeGate.js";
|
|
7
|
+
import { EmptyView as s, ErrorView as c } from "../components/StateViews.js";
|
|
8
|
+
import { notify as l } from "../lib/toast.js";
|
|
9
|
+
import { useCompleteLesson as u, useEnroll as d, useLessonPlayerQuery as te, useMyEnrollmentsQuery as f } from "../api/technospamApi.js";
|
|
10
10
|
import "../components/index.js";
|
|
11
|
-
import { FOCUS_PARAMS as
|
|
12
|
-
import { courseDetailsPath as
|
|
13
|
-
import { InteractiveWorkspace as
|
|
14
|
-
import { QuizLesson as
|
|
15
|
-
import { useEffect as
|
|
16
|
-
import { ArrowLeft as
|
|
11
|
+
import { FOCUS_PARAMS as p } from "../utils/pageContext.js";
|
|
12
|
+
import { courseDetailsPath as m, lessonPlayerPath as h } from "../utils/lessonPath.js";
|
|
13
|
+
import { InteractiveWorkspace as g } from "../components/lesson/InteractiveWorkspace.js";
|
|
14
|
+
import { QuizLesson as _ } from "../components/lesson/QuizLesson.js";
|
|
15
|
+
import { useEffect as ne } from "react";
|
|
16
|
+
import { ArrowLeft as re, ArrowRight as v, BookOpen as y, CheckCircle2 as b, Clock as ie, FileText as x, Layers as S, Lock as ae, PlayCircle as C, Sparkles as w } from "lucide-react";
|
|
17
17
|
import { useSearchParams as T } from "react-router";
|
|
18
18
|
import { jsx as E, jsxs as D } from "react/jsx-runtime";
|
|
19
19
|
import { Button as O, GlassCard as k, Skeleton as A } from "@burdenoff/fe-libs/ui";
|
|
@@ -23,7 +23,7 @@ var M = {
|
|
|
23
23
|
VIDEO: C,
|
|
24
24
|
ARTICLE: x,
|
|
25
25
|
INTERACTIVE: w,
|
|
26
|
-
QUIZ:
|
|
26
|
+
QUIZ: b,
|
|
27
27
|
PROJECT: S
|
|
28
28
|
};
|
|
29
29
|
function oe(e) {
|
|
@@ -54,7 +54,7 @@ function N({ lesson: e, onProgress: t }) {
|
|
|
54
54
|
}
|
|
55
55
|
case "QUIZ": return /* @__PURE__ */ D("div", {
|
|
56
56
|
className: "space-y-4",
|
|
57
|
-
children: [n, /* @__PURE__ */ E(
|
|
57
|
+
children: [n, /* @__PURE__ */ E(_, {
|
|
58
58
|
lesson: e,
|
|
59
59
|
onPassed: t
|
|
60
60
|
}, e.id)]
|
|
@@ -69,7 +69,7 @@ function N({ lesson: e, onProgress: t }) {
|
|
|
69
69
|
})
|
|
70
70
|
}), /* @__PURE__ */ E("div", {
|
|
71
71
|
className: "min-w-0",
|
|
72
|
-
children: /* @__PURE__ */ E(
|
|
72
|
+
children: /* @__PURE__ */ E(g, {
|
|
73
73
|
lesson: e,
|
|
74
74
|
onCompleted: t
|
|
75
75
|
}, e.id)
|
|
@@ -82,8 +82,8 @@ function N({ lesson: e, onProgress: t }) {
|
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
84
|
function P() {
|
|
85
|
-
let [
|
|
86
|
-
|
|
85
|
+
let [g] = T(), _ = g.get(p.id) ?? "", S = g.get(p.lesson) ?? "", C = t(), { workspaceId: w } = e(), { emit: j } = n(), { data: P, loading: F, error: I, refetch: L } = te(_, S), [R, { loading: z }] = u(), [B, { loading: V }] = d();
|
|
86
|
+
ne(() => {
|
|
87
87
|
S && j("technospam.lesson.viewed", {
|
|
88
88
|
workspaceId: w,
|
|
89
89
|
entityId: S,
|
|
@@ -94,11 +94,11 @@ function P() {
|
|
|
94
94
|
w,
|
|
95
95
|
j
|
|
96
96
|
]);
|
|
97
|
-
let H = P?.tecLesson ?? null, U = P?.tecCourse ?? null, { enrollments: W, refetch: G } =
|
|
97
|
+
let H = P?.tecLesson ?? null, U = P?.tecCourse ?? null, { enrollments: W, refetch: G } = f({
|
|
98
98
|
buiCourseId: U?.buiCourseId,
|
|
99
|
-
technospamCourseId:
|
|
100
|
-
skip: !
|
|
101
|
-
}), K = W[0] ?? null, q = [...U?.lessons ?? []].sort((e, t) => e.position - t.position), J = q.findIndex((e) => e.id === S), Y = J > 0 ? q[J - 1] : void 0, X = J >= 0 && J < q.length - 1 ? q[J + 1] : void 0, Z =
|
|
99
|
+
technospamCourseId: _,
|
|
100
|
+
skip: !_
|
|
101
|
+
}), K = W[0] ?? null, q = [...U?.lessons ?? []].sort((e, t) => e.position - t.position), J = q.findIndex((e) => e.id === S), Y = J > 0 ? q[J - 1] : void 0, X = J >= 0 && J < q.length - 1 ? q[J + 1] : void 0, Z = m(_), se = () => {
|
|
102
102
|
L(), G();
|
|
103
103
|
}, ce = async () => {
|
|
104
104
|
if (!H) return;
|
|
@@ -107,14 +107,14 @@ function P() {
|
|
|
107
107
|
workspaceId: w,
|
|
108
108
|
entityId: H.id,
|
|
109
109
|
entityType: "TEC_LESSON"
|
|
110
|
-
}),
|
|
110
|
+
}), l.success("Lesson completed", `${e.progressPct}% of the course done`), X ? C(h(_, X.id)) : await L());
|
|
111
111
|
}, le = async () => {
|
|
112
|
-
(await B({ variables: { input: { courseId:
|
|
112
|
+
(await B({ variables: { input: { courseId: _ } } })).data?.tecEnroll && (l.success("Enrolled", `Your progress in "${U?.title ?? "this course"}" is now tracked.`), await L());
|
|
113
113
|
};
|
|
114
|
-
if (!
|
|
114
|
+
if (!_ || !S) return /* @__PURE__ */ E(i, {
|
|
115
115
|
title: "Lesson",
|
|
116
116
|
backPath: "/courses/courses-page",
|
|
117
|
-
children: /* @__PURE__ */ E(
|
|
117
|
+
children: /* @__PURE__ */ E(s, {
|
|
118
118
|
title: "No lesson selected",
|
|
119
119
|
description: "Open a lesson from a course curriculum."
|
|
120
120
|
})
|
|
@@ -127,10 +127,10 @@ function P() {
|
|
|
127
127
|
if (I || !H || !U || H.courseId !== U.id) return /* @__PURE__ */ E(i, {
|
|
128
128
|
title: "Lesson",
|
|
129
129
|
backPath: Z,
|
|
130
|
-
children: I ? /* @__PURE__ */ E(
|
|
130
|
+
children: I ? /* @__PURE__ */ E(c, {
|
|
131
131
|
title: "Couldn't load lesson",
|
|
132
132
|
error: I
|
|
133
|
-
}) : /* @__PURE__ */ E(
|
|
133
|
+
}) : /* @__PURE__ */ E(s, {
|
|
134
134
|
title: "Lesson not found",
|
|
135
135
|
description: "This lesson may have been removed."
|
|
136
136
|
})
|
|
@@ -142,7 +142,7 @@ function P() {
|
|
|
142
142
|
description: `${U.title} · Lesson ${J + 1} of ${q.length}`,
|
|
143
143
|
actions: Q ? /* @__PURE__ */ D("span", {
|
|
144
144
|
className: "inline-flex items-center gap-1.5 rounded-full border border-status-success-border bg-status-success-bg-subtle px-3 py-1.5 text-sm font-medium text-status-success-text",
|
|
145
|
-
children: [/* @__PURE__ */ E(
|
|
145
|
+
children: [/* @__PURE__ */ E(b, { size: 15 }), " Completed"]
|
|
146
146
|
}) : null,
|
|
147
147
|
children: /* @__PURE__ */ D("div", {
|
|
148
148
|
className: "grid grid-cols-1 gap-5 lg:grid-cols-[minmax(0,1fr)_280px]",
|
|
@@ -163,7 +163,7 @@ function P() {
|
|
|
163
163
|
/* @__PURE__ */ D("span", {
|
|
164
164
|
className: "inline-flex items-center gap-1",
|
|
165
165
|
children: [
|
|
166
|
-
/* @__PURE__ */ E(
|
|
166
|
+
/* @__PURE__ */ E(ie, { size: 12 }),
|
|
167
167
|
" ",
|
|
168
168
|
ee(H.estimatedMinutes)
|
|
169
169
|
]
|
|
@@ -184,7 +184,7 @@ function P() {
|
|
|
184
184
|
onClick: () => void le(),
|
|
185
185
|
disabled: V,
|
|
186
186
|
children: [
|
|
187
|
-
/* @__PURE__ */ E(
|
|
187
|
+
/* @__PURE__ */ E(y, { size: 14 }),
|
|
188
188
|
" ",
|
|
189
189
|
V ? "Enrolling…" : "Enroll"
|
|
190
190
|
]
|
|
@@ -193,7 +193,7 @@ function P() {
|
|
|
193
193
|
/* @__PURE__ */ E(k, {
|
|
194
194
|
className: "p-5",
|
|
195
195
|
"data-testid": "lesson-player-body",
|
|
196
|
-
children: H.isLocked ? /* @__PURE__ */ E(
|
|
196
|
+
children: H.isLocked ? /* @__PURE__ */ E(o, { description: "This lesson is part of a paid TechnoSpam plan. Its content unlocks once your workspace upgrades." }) : /* @__PURE__ */ E(N, {
|
|
197
197
|
lesson: H,
|
|
198
198
|
onProgress: se
|
|
199
199
|
})
|
|
@@ -203,8 +203,8 @@ function P() {
|
|
|
203
203
|
children: [/* @__PURE__ */ D(O, {
|
|
204
204
|
variant: "outline",
|
|
205
205
|
disabled: !Y,
|
|
206
|
-
onClick: () => Y && C(
|
|
207
|
-
children: [/* @__PURE__ */ E(
|
|
206
|
+
onClick: () => Y && C(h(_, Y.id)),
|
|
207
|
+
children: [/* @__PURE__ */ E(re, { size: 15 }), " Previous"]
|
|
208
208
|
}), /* @__PURE__ */ D("div", {
|
|
209
209
|
className: "flex flex-wrap items-center gap-2",
|
|
210
210
|
children: [
|
|
@@ -212,7 +212,7 @@ function P() {
|
|
|
212
212
|
onClick: () => void ce(),
|
|
213
213
|
disabled: z,
|
|
214
214
|
children: [
|
|
215
|
-
/* @__PURE__ */ E(
|
|
215
|
+
/* @__PURE__ */ E(b, { size: 15 }),
|
|
216
216
|
" ",
|
|
217
217
|
z ? "Saving…" : "Mark complete"
|
|
218
218
|
]
|
|
@@ -224,8 +224,8 @@ function P() {
|
|
|
224
224
|
/* @__PURE__ */ D(O, {
|
|
225
225
|
variant: Q || !K ? "default" : "outline",
|
|
226
226
|
disabled: !X,
|
|
227
|
-
onClick: () => X && C(
|
|
228
|
-
children: ["Next ", /* @__PURE__ */ E(
|
|
227
|
+
onClick: () => X && C(h(_, X.id)),
|
|
228
|
+
children: ["Next ", /* @__PURE__ */ E(v, { size: 15 })]
|
|
229
229
|
})
|
|
230
230
|
]
|
|
231
231
|
})]
|
|
@@ -259,14 +259,14 @@ function P() {
|
|
|
259
259
|
return /* @__PURE__ */ E("li", { children: /* @__PURE__ */ D("button", {
|
|
260
260
|
type: "button",
|
|
261
261
|
"aria-current": i ? "page" : void 0,
|
|
262
|
-
onClick: () => C(
|
|
262
|
+
onClick: () => C(h(_, e.id)),
|
|
263
263
|
className: r("flex w-full items-center gap-2 rounded-lg px-2 py-1.5 text-left text-sm transition-colors hover:bg-bg-sunken", i && "bg-accent-primary/6 font-medium text-accent-primary"),
|
|
264
264
|
children: [
|
|
265
265
|
/* @__PURE__ */ E("span", {
|
|
266
266
|
className: "w-5 shrink-0 text-center text-xs tabular-nums text-text-muted",
|
|
267
267
|
children: t + 1
|
|
268
268
|
}),
|
|
269
|
-
e.completedByMe ? /* @__PURE__ */ E(
|
|
269
|
+
e.completedByMe ? /* @__PURE__ */ E(b, {
|
|
270
270
|
size: 14,
|
|
271
271
|
className: "shrink-0 text-status-success-text"
|
|
272
272
|
}) : e.isLocked ? /* @__PURE__ */ E(ae, {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CoursesLessonPlayerPagePage.js","names":[],"sources":["../../../src/technospam/pages/CoursesLessonPlayerPagePage.tsx"],"sourcesContent":["import { useEffect } from 'react';\nimport { useSearchParams } from 'react-router';\nimport { Markdown } from '@burdenoff/fe-libs/shared/components/Markdown';\nimport {\n ArrowLeft,\n ArrowRight,\n BookOpen,\n CheckCircle2,\n Clock,\n FileText,\n Layers,\n Lock,\n PlayCircle,\n Sparkles,\n type LucideIcon,\n} from 'lucide-react';\nimport { Button, GlassCard, Skeleton } from '@burdenoff/fe-libs/ui';\nimport { EmptyView, ErrorView, PageLayout, UpgradeGate } from '../components';\nimport { InteractiveWorkspace } from '../components/lesson/InteractiveWorkspace';\nimport { QuizLesson } from '../components/lesson/QuizLesson';\nimport { useTechnoSpamNavigate, useTechnoSpam } from '../providers/TechnoSpamProvider';\nimport {\n useCompleteLesson,\n useEnroll,\n useLessonPlayerQuery,\n useMyEnrollmentsQuery,\n type TecLesson,\n type TecLessonKind,\n} from '../api/technospamApi';\nimport { formatDuration, humanizeEnum } from '../utils/format';\nimport { notify } from '../lib/toast';\nimport { cn } from '../utils/cn';\nimport { useTechnoSpamAnalytics } from '../hooks/useTechnoSpamAnalytics';\nimport { FOCUS_PARAMS } from '../utils/pageContext';\nimport { courseDetailsPath, lessonPlayerPath } from '../utils/lessonPath';\n\nconst KIND_ICON: Record<TecLessonKind, LucideIcon> = {\n VIDEO: PlayCircle,\n ARTICLE: FileText,\n INTERACTIVE: Sparkles,\n QUIZ: CheckCircle2,\n PROJECT: Layers,\n};\n\n/** Lessons whose completion is earned (a passed quiz, a matching run) rather than self-reported. */\nfunction completesByCheck(lesson: TecLesson): boolean {\n if (lesson.kind === 'QUIZ') return (lesson.quizQuestions?.length ?? 0) > 0;\n if (lesson.kind === 'INTERACTIVE') return !!lesson.expectedOutput;\n return false;\n}\n\nfunction LessonBody({ lesson, onProgress }: { lesson: TecLesson; onProgress: () => void }) {\n const notes = lesson.content ? (\n <Markdown className=\"prose prose-sm max-w-none text-text-secondary dark:prose-invert\">\n {lesson.content}\n </Markdown>\n ) : null;\n\n switch (lesson.kind) {\n case 'VIDEO': {\n const src = lesson.file?.downloadUrl;\n const isVideo = !lesson.file?.mimeType || lesson.file.mimeType.startsWith('video/');\n return (\n <div className=\"space-y-4\">\n {src && isVideo ? (\n <video\n controls\n preload=\"metadata\"\n src={src}\n className=\"aspect-video w-full rounded-xl bg-bg-sunken\"\n data-testid=\"lesson-video\"\n >\n <track kind=\"captions\" />\n </video>\n ) : (\n <p className=\"rounded-xl border border-dashed border-border-seam px-4 py-3 text-sm text-text-muted\">\n The video for this lesson has not been uploaded yet.\n </p>\n )}\n {notes}\n </div>\n );\n }\n case 'QUIZ':\n return (\n <div className=\"space-y-4\">\n {notes}\n <QuizLesson key={lesson.id} lesson={lesson} onPassed={onProgress} />\n </div>\n );\n case 'INTERACTIVE':\n return (\n <div className=\"grid grid-cols-1 gap-5 xl:grid-cols-2\">\n <div className=\"min-w-0\">\n {notes ?? (\n <p className=\"text-sm text-text-muted\">\n Edit the code, run it, and compare the output with what the lesson expects.\n </p>\n )}\n </div>\n <div className=\"min-w-0\">\n <InteractiveWorkspace key={lesson.id} lesson={lesson} onCompleted={onProgress} />\n </div>\n </div>\n );\n case 'PROJECT':\n case 'ARTICLE':\n default:\n return (\n notes ?? (\n <p className=\"rounded-xl border border-dashed border-border-seam px-4 py-3 text-sm text-text-muted\">\n No content published for this lesson yet.\n </p>\n )\n );\n }\n}\n\n/**\n * Lesson player (BOFF-4107): renders one lesson of a course by kind — article\n * and project briefs as markdown, video through the federated File, quizzes\n * graded server-side, and a runnable sandbox workspace for INTERACTIVE\n * lessons — with the curriculum alongside and per-lesson progress tracking.\n * URL: `/courses/lesson-player-page?id=<courseId>&lessonId=<lessonId>`.\n */\nexport function CoursesLessonPlayerPagePage() {\n const [params] = useSearchParams();\n const courseId = params.get(FOCUS_PARAMS.id) ?? '';\n const lessonId = params.get(FOCUS_PARAMS.lesson) ?? '';\n const navigate = useTechnoSpamNavigate();\n const { workspaceId } = useTechnoSpam();\n const { emit } = useTechnoSpamAnalytics();\n const { data, loading, error, refetch } = useLessonPlayerQuery(courseId, lessonId);\n const [completeLesson, { loading: completing }] = useCompleteLesson();\n const [enroll, { loading: enrolling }] = useEnroll();\n\n useEffect(() => {\n if (lessonId) {\n emit('technospam.lesson.viewed', {\n workspaceId,\n entityId: lessonId,\n entityType: 'TEC_LESSON',\n });\n }\n }, [lessonId, workspaceId, emit]);\n\n const lesson = data?.tecLesson ?? null;\n const course = data?.tecCourse ?? null;\n // BOFF-10481: the learner's own enrollment in this course, from the\n // canonical self-scoped Buildmyiq field (narrowed by the course's canonical\n // id on the server and its Technospam id on the client).\n const { enrollments: myEnrollments, refetch: refetchEnrollment } = useMyEnrollmentsQuery({\n buiCourseId: course?.buiCourseId,\n technospamCourseId: courseId,\n skip: !courseId,\n });\n const enrollment = myEnrollments[0] ?? null;\n const lessons = [...(course?.lessons ?? [])].sort((a, b) => a.position - b.position);\n const index = lessons.findIndex((l) => l.id === lessonId);\n const prev = index > 0 ? lessons[index - 1] : undefined;\n const next = index >= 0 && index < lessons.length - 1 ? lessons[index + 1] : undefined;\n const backPath = courseDetailsPath(courseId);\n\n const refreshProgress = (): void => {\n void refetch();\n void refetchEnrollment();\n };\n\n const handleComplete = async (): Promise<void> => {\n if (!lesson) return;\n const res = await completeLesson({ variables: { lessonId: lesson.id } });\n const updated = res.data?.tecCompleteLesson;\n if (!updated) return;\n emit('technospam.lesson.completed', {\n workspaceId,\n entityId: lesson.id,\n entityType: 'TEC_LESSON',\n });\n notify.success('Lesson completed', `${updated.progressPct}% of the course done`);\n if (next) navigate(lessonPlayerPath(courseId, next.id));\n else await refetch();\n };\n\n const handleEnroll = async (): Promise<void> => {\n const res = await enroll({ variables: { input: { courseId } } });\n if (res.data?.tecEnroll) {\n notify.success(\n 'Enrolled',\n `Your progress in \"${course?.title ?? 'this course'}\" is now tracked.`\n );\n await refetch();\n }\n };\n\n if (!courseId || !lessonId) {\n return (\n <PageLayout title=\"Lesson\" backPath=\"/courses/courses-page\">\n <EmptyView\n title=\"No lesson selected\"\n description=\"Open a lesson from a course curriculum.\"\n />\n </PageLayout>\n );\n }\n\n if (loading && !lesson) {\n return (\n <PageLayout title=\"Lesson\" backPath={backPath}>\n <Skeleton className=\"mb-4 h-10 w-1/2 rounded-xl\" />\n <Skeleton className=\"h-80 w-full rounded-xl\" />\n </PageLayout>\n );\n }\n\n if (error || !lesson || !course || lesson.courseId !== course.id) {\n return (\n <PageLayout title=\"Lesson\" backPath={backPath}>\n {error ? (\n <ErrorView title=\"Couldn't load lesson\" error={error} />\n ) : (\n <EmptyView title=\"Lesson not found\" description=\"This lesson may have been removed.\" />\n )}\n </PageLayout>\n );\n }\n\n const Icon = KIND_ICON[lesson.kind] ?? FileText;\n const done = !!lesson.completedByMe;\n const byCheck = completesByCheck(lesson);\n\n return (\n <PageLayout\n backPath={backPath}\n title={lesson.title}\n description={`${course.title} · Lesson ${index + 1} of ${lessons.length}`}\n actions={\n done ? (\n <span className=\"inline-flex items-center gap-1.5 rounded-full border border-status-success-border bg-status-success-bg-subtle px-3 py-1.5 text-sm font-medium text-status-success-text\">\n <CheckCircle2 size={15} /> Completed\n </span>\n ) : null\n }\n >\n <div className=\"grid grid-cols-1 gap-5 lg:grid-cols-[minmax(0,1fr)_280px]\">\n <div className=\"min-w-0 space-y-4\">\n <div className=\"flex flex-wrap items-center gap-2 text-xs text-text-muted\">\n <span className=\"inline-flex items-center gap-1 rounded-full bg-accent-primary/6 px-2 py-0.5 font-medium text-accent-primary\">\n <Icon size={12} /> {humanizeEnum(lesson.kind)}\n </span>\n <span className=\"inline-flex items-center gap-1\">\n <Clock size={12} /> {formatDuration(lesson.estimatedMinutes)}\n </span>\n {lesson.isPreview ? (\n <span className=\"rounded-full border border-accent-primary/30 px-2 py-0.5\">\n Free preview\n </span>\n ) : null}\n </div>\n\n {!enrollment ? (\n <GlassCard className=\"flex flex-col items-start gap-3 p-4 sm:flex-row sm:items-center sm:justify-between\">\n <p className=\"text-sm text-text-secondary\">\n Enroll in this course to save your progress and earn a certificate.\n </p>\n <Button size=\"sm\" onClick={() => void handleEnroll()} disabled={enrolling}>\n <BookOpen size={14} /> {enrolling ? 'Enrolling…' : 'Enroll'}\n </Button>\n </GlassCard>\n ) : null}\n\n <GlassCard className=\"p-5\" data-testid=\"lesson-player-body\">\n {lesson.isLocked ? (\n <UpgradeGate description=\"This lesson is part of a paid TechnoSpam plan. Its content unlocks once your workspace upgrades.\" />\n ) : (\n <LessonBody lesson={lesson} onProgress={refreshProgress} />\n )}\n </GlassCard>\n\n <div className=\"flex flex-wrap items-center justify-between gap-2\">\n <Button\n variant=\"outline\"\n disabled={!prev}\n onClick={() => prev && navigate(lessonPlayerPath(courseId, prev.id))}\n >\n <ArrowLeft size={15} /> Previous\n </Button>\n <div className=\"flex flex-wrap items-center gap-2\">\n {enrollment && !done && !byCheck && !lesson.isLocked ? (\n <Button onClick={() => void handleComplete()} disabled={completing}>\n <CheckCircle2 size={15} /> {completing ? 'Saving…' : 'Mark complete'}\n </Button>\n ) : null}\n {enrollment && !done && byCheck && !lesson.isLocked ? (\n <span className=\"text-xs text-text-muted\">\n {lesson.kind === 'QUIZ'\n ? 'Pass the quiz to complete this lesson.'\n : 'Run code that prints the expected output to complete this lesson.'}\n </span>\n ) : null}\n <Button\n variant={done || !enrollment ? 'default' : 'outline'}\n disabled={!next}\n onClick={() => next && navigate(lessonPlayerPath(courseId, next.id))}\n >\n Next <ArrowRight size={15} />\n </Button>\n </div>\n </div>\n </div>\n\n <GlassCard className=\"h-fit p-4\">\n <div className=\"mb-2 flex items-center justify-between\">\n <span className=\"text-xs font-semibold uppercase tracking-wide text-text-muted\">\n Curriculum\n </span>\n {enrollment ? (\n <span className=\"text-xs text-text-secondary\" data-testid=\"lesson-player-progress\">\n {enrollment.progressPct}% complete\n </span>\n ) : null}\n </div>\n {enrollment ? (\n <div className=\"mb-3 h-1.5 w-full overflow-hidden rounded-full bg-bg-sunken\">\n <div\n className=\"h-full rounded-full bg-accent-primary transition-all duration-500\"\n style={{ width: `${enrollment.progressPct}%` }}\n />\n </div>\n ) : null}\n <ol className=\"space-y-1\">\n {lessons.map((l, i) => {\n const LIcon = KIND_ICON[l.kind] ?? FileText;\n const current = l.id === lesson.id;\n return (\n <li key={l.id}>\n <button\n type=\"button\"\n aria-current={current ? 'page' : undefined}\n onClick={() => navigate(lessonPlayerPath(courseId, l.id))}\n className={cn(\n 'flex w-full items-center gap-2 rounded-lg px-2 py-1.5 text-left text-sm transition-colors hover:bg-bg-sunken',\n current && 'bg-accent-primary/6 font-medium text-accent-primary'\n )}\n >\n <span className=\"w-5 shrink-0 text-center text-xs tabular-nums text-text-muted\">\n {i + 1}\n </span>\n {l.completedByMe ? (\n <CheckCircle2 size={14} className=\"shrink-0 text-status-success-text\" />\n ) : l.isLocked ? (\n <Lock size={14} className=\"shrink-0 text-text-muted\" />\n ) : (\n <LIcon size={14} className=\"shrink-0 text-text-muted\" />\n )}\n <span className=\"min-w-0 flex-1 truncate\">{l.title}</span>\n </button>\n </li>\n );\n })}\n </ol>\n </GlassCard>\n </div>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CoursesLessonPlayerPagePage.js","names":[],"sources":["../../../src/technospam/pages/CoursesLessonPlayerPagePage.tsx"],"sourcesContent":["import { useEffect } from 'react';\nimport { useSearchParams } from 'react-router';\nimport { Markdown } from '@burdenoff/fe-libs/shared/components/Markdown';\nimport {\n ArrowLeft,\n ArrowRight,\n BookOpen,\n CheckCircle2,\n Clock,\n FileText,\n Layers,\n Lock,\n PlayCircle,\n Sparkles,\n type LucideIcon,\n} from 'lucide-react';\nimport { Button, GlassCard, Skeleton } from '@burdenoff/fe-libs/ui';\nimport { EmptyView, ErrorView, PageLayout, UpgradeGate } from '../components';\nimport { InteractiveWorkspace } from '../components/lesson/InteractiveWorkspace';\nimport { QuizLesson } from '../components/lesson/QuizLesson';\nimport { useTechnoSpamNavigate, useTechnoSpam } from '../providers/TechnoSpamProvider';\nimport {\n useCompleteLesson,\n useEnroll,\n useLessonPlayerQuery,\n useMyEnrollmentsQuery,\n type TecLesson,\n type TecLessonKind,\n} from '../api/technospamApi';\nimport { formatDuration, humanizeEnum } from '../utils/format';\nimport { notify } from '../lib/toast';\nimport { cn } from '../utils/cn';\nimport { useTechnoSpamAnalytics } from '../hooks/useTechnoSpamAnalytics';\nimport { FOCUS_PARAMS } from '../utils/pageContext';\nimport { courseDetailsPath, lessonPlayerPath } from '../utils/lessonPath';\n\nconst KIND_ICON: Record<TecLessonKind, LucideIcon> = {\n VIDEO: PlayCircle,\n ARTICLE: FileText,\n INTERACTIVE: Sparkles,\n QUIZ: CheckCircle2,\n PROJECT: Layers,\n};\n\n/** Lessons whose completion is earned (a passed quiz, a matching run) rather than self-reported. */\nfunction completesByCheck(lesson: TecLesson): boolean {\n if (lesson.kind === 'QUIZ') return (lesson.quizQuestions?.length ?? 0) > 0;\n if (lesson.kind === 'INTERACTIVE') return !!lesson.expectedOutput;\n return false;\n}\n\nfunction LessonBody({ lesson, onProgress }: { lesson: TecLesson; onProgress: () => void }) {\n const notes = lesson.content ? (\n <Markdown className=\"prose prose-sm max-w-none text-text-secondary dark:prose-invert\">\n {lesson.content}\n </Markdown>\n ) : null;\n\n switch (lesson.kind) {\n case 'VIDEO': {\n const src = lesson.file?.downloadUrl;\n const isVideo = !lesson.file?.mimeType || lesson.file.mimeType.startsWith('video/');\n return (\n <div className=\"space-y-4\">\n {src && isVideo ? (\n <video\n controls\n preload=\"metadata\"\n src={src}\n className=\"aspect-video w-full rounded-xl bg-bg-sunken\"\n data-testid=\"lesson-video\"\n >\n <track kind=\"captions\" />\n </video>\n ) : (\n <p className=\"rounded-xl border border-dashed border-border-seam px-4 py-3 text-sm text-text-muted\">\n The video for this lesson has not been uploaded yet.\n </p>\n )}\n {notes}\n </div>\n );\n }\n case 'QUIZ':\n return (\n <div className=\"space-y-4\">\n {notes}\n <QuizLesson key={lesson.id} lesson={lesson} onPassed={onProgress} />\n </div>\n );\n case 'INTERACTIVE':\n return (\n <div className=\"grid grid-cols-1 gap-5 xl:grid-cols-2\">\n <div className=\"min-w-0\">\n {notes ?? (\n <p className=\"text-sm text-text-muted\">\n Edit the code, run it, and compare the output with what the lesson expects.\n </p>\n )}\n </div>\n <div className=\"min-w-0\">\n <InteractiveWorkspace key={lesson.id} lesson={lesson} onCompleted={onProgress} />\n </div>\n </div>\n );\n case 'PROJECT':\n case 'ARTICLE':\n default:\n return (\n notes ?? (\n <p className=\"rounded-xl border border-dashed border-border-seam px-4 py-3 text-sm text-text-muted\">\n No content published for this lesson yet.\n </p>\n )\n );\n }\n}\n\n/**\n * Lesson player (BOFF-4107): renders one lesson of a course by kind — article\n * and project briefs as markdown, video through the federated File, quizzes\n * graded server-side, and a runnable sandbox workspace for INTERACTIVE\n * lessons — with the curriculum alongside and per-lesson progress tracking.\n * URL: `/courses/lesson-player-page?id=<courseId>&lessonId=<lessonId>`.\n */\nexport function CoursesLessonPlayerPagePage() {\n const [params] = useSearchParams();\n const courseId = params.get(FOCUS_PARAMS.id) ?? '';\n const lessonId = params.get(FOCUS_PARAMS.lesson) ?? '';\n const navigate = useTechnoSpamNavigate();\n const { workspaceId } = useTechnoSpam();\n const { emit } = useTechnoSpamAnalytics();\n const { data, loading, error, refetch } = useLessonPlayerQuery(courseId, lessonId);\n const [completeLesson, { loading: completing }] = useCompleteLesson();\n const [enroll, { loading: enrolling }] = useEnroll();\n\n useEffect(() => {\n if (lessonId) {\n emit('technospam.lesson.viewed', {\n workspaceId,\n entityId: lessonId,\n entityType: 'TEC_LESSON',\n });\n }\n }, [lessonId, workspaceId, emit]);\n\n const lesson = data?.tecLesson ?? null;\n const course = data?.tecCourse ?? null;\n // BOFF-10481: the learner's own enrollment in this course, from the\n // canonical self-scoped Buildmyiq field (narrowed by the course's canonical\n // id on the server and its Technospam id on the client).\n const { enrollments: myEnrollments, refetch: refetchEnrollment } = useMyEnrollmentsQuery({\n buiCourseId: course?.buiCourseId,\n technospamCourseId: courseId,\n skip: !courseId,\n });\n const enrollment = myEnrollments[0] ?? null;\n const lessons = [...(course?.lessons ?? [])].sort((a, b) => a.position - b.position);\n const index = lessons.findIndex((l) => l.id === lessonId);\n const prev = index > 0 ? lessons[index - 1] : undefined;\n const next = index >= 0 && index < lessons.length - 1 ? lessons[index + 1] : undefined;\n const backPath = courseDetailsPath(courseId);\n\n const refreshProgress = (): void => {\n void refetch();\n void refetchEnrollment();\n };\n\n const handleComplete = async (): Promise<void> => {\n if (!lesson) return;\n const res = await completeLesson({ variables: { lessonId: lesson.id } });\n const updated = res.data?.tecCompleteLesson;\n if (!updated) return;\n emit('technospam.lesson.completed', {\n workspaceId,\n entityId: lesson.id,\n entityType: 'TEC_LESSON',\n });\n notify.success('Lesson completed', `${updated.progressPct}% of the course done`);\n if (next) navigate(lessonPlayerPath(courseId, next.id));\n else await refetch();\n };\n\n const handleEnroll = async (): Promise<void> => {\n const res = await enroll({ variables: { input: { courseId } } });\n if (res.data?.tecEnroll) {\n notify.success(\n 'Enrolled',\n `Your progress in \"${course?.title ?? 'this course'}\" is now tracked.`\n );\n await refetch();\n }\n };\n\n if (!courseId || !lessonId) {\n return (\n <PageLayout title=\"Lesson\" backPath=\"/courses/courses-page\">\n <EmptyView\n title=\"No lesson selected\"\n description=\"Open a lesson from a course curriculum.\"\n />\n </PageLayout>\n );\n }\n\n if (loading && !lesson) {\n return (\n <PageLayout title=\"Lesson\" backPath={backPath}>\n <Skeleton className=\"mb-4 h-10 w-1/2 rounded-xl\" />\n <Skeleton className=\"h-80 w-full rounded-xl\" />\n </PageLayout>\n );\n }\n\n if (error || !lesson || !course || lesson.courseId !== course.id) {\n return (\n <PageLayout title=\"Lesson\" backPath={backPath}>\n {error ? (\n <ErrorView title=\"Couldn't load lesson\" error={error} />\n ) : (\n <EmptyView title=\"Lesson not found\" description=\"This lesson may have been removed.\" />\n )}\n </PageLayout>\n );\n }\n\n const Icon = KIND_ICON[lesson.kind] ?? FileText;\n const done = !!lesson.completedByMe;\n const byCheck = completesByCheck(lesson);\n\n return (\n <PageLayout\n backPath={backPath}\n title={lesson.title}\n description={`${course.title} · Lesson ${index + 1} of ${lessons.length}`}\n actions={\n done ? (\n <span className=\"inline-flex items-center gap-1.5 rounded-full border border-status-success-border bg-status-success-bg-subtle px-3 py-1.5 text-sm font-medium text-status-success-text\">\n <CheckCircle2 size={15} /> Completed\n </span>\n ) : null\n }\n >\n <div className=\"grid grid-cols-1 gap-5 lg:grid-cols-[minmax(0,1fr)_280px]\">\n <div className=\"min-w-0 space-y-4\">\n <div className=\"flex flex-wrap items-center gap-2 text-xs text-text-muted\">\n <span className=\"inline-flex items-center gap-1 rounded-full bg-accent-primary/6 px-2 py-0.5 font-medium text-accent-primary\">\n <Icon size={12} /> {humanizeEnum(lesson.kind)}\n </span>\n <span className=\"inline-flex items-center gap-1\">\n <Clock size={12} /> {formatDuration(lesson.estimatedMinutes)}\n </span>\n {lesson.isPreview ? (\n <span className=\"rounded-full border border-accent-primary/30 px-2 py-0.5\">\n Free preview\n </span>\n ) : null}\n </div>\n\n {!enrollment ? (\n <GlassCard className=\"flex flex-col items-start gap-3 p-4 sm:flex-row sm:items-center sm:justify-between\">\n <p className=\"text-sm text-text-secondary\">\n Enroll in this course to save your progress and earn a certificate.\n </p>\n <Button size=\"sm\" onClick={() => void handleEnroll()} disabled={enrolling}>\n <BookOpen size={14} /> {enrolling ? 'Enrolling…' : 'Enroll'}\n </Button>\n </GlassCard>\n ) : null}\n\n <GlassCard className=\"p-5\" data-testid=\"lesson-player-body\">\n {lesson.isLocked ? (\n <UpgradeGate description=\"This lesson is part of a paid TechnoSpam plan. Its content unlocks once your workspace upgrades.\" />\n ) : (\n <LessonBody lesson={lesson} onProgress={refreshProgress} />\n )}\n </GlassCard>\n\n <div className=\"flex flex-wrap items-center justify-between gap-2\">\n <Button\n variant=\"outline\"\n disabled={!prev}\n onClick={() => prev && navigate(lessonPlayerPath(courseId, prev.id))}\n >\n <ArrowLeft size={15} /> Previous\n </Button>\n <div className=\"flex flex-wrap items-center gap-2\">\n {enrollment && !done && !byCheck && !lesson.isLocked ? (\n <Button onClick={() => void handleComplete()} disabled={completing}>\n <CheckCircle2 size={15} /> {completing ? 'Saving…' : 'Mark complete'}\n </Button>\n ) : null}\n {enrollment && !done && byCheck && !lesson.isLocked ? (\n <span className=\"text-xs text-text-muted\">\n {lesson.kind === 'QUIZ'\n ? 'Pass the quiz to complete this lesson.'\n : 'Run code that prints the expected output to complete this lesson.'}\n </span>\n ) : null}\n <Button\n variant={done || !enrollment ? 'default' : 'outline'}\n disabled={!next}\n onClick={() => next && navigate(lessonPlayerPath(courseId, next.id))}\n >\n Next <ArrowRight size={15} />\n </Button>\n </div>\n </div>\n </div>\n\n <GlassCard className=\"h-fit p-4\">\n <div className=\"mb-2 flex items-center justify-between\">\n <span className=\"text-xs font-semibold uppercase tracking-wide text-text-muted\">\n Curriculum\n </span>\n {enrollment ? (\n <span className=\"text-xs text-text-secondary\" data-testid=\"lesson-player-progress\">\n {enrollment.progressPct}% complete\n </span>\n ) : null}\n </div>\n {enrollment ? (\n <div className=\"mb-3 h-1.5 w-full overflow-hidden rounded-full bg-bg-sunken\">\n <div\n className=\"h-full rounded-full bg-accent-primary transition-all duration-500\"\n style={{ width: `${enrollment.progressPct}%` }}\n />\n </div>\n ) : null}\n <ol className=\"space-y-1\">\n {lessons.map((l, i) => {\n const LIcon = KIND_ICON[l.kind] ?? FileText;\n const current = l.id === lesson.id;\n return (\n <li key={l.id}>\n <button\n type=\"button\"\n aria-current={current ? 'page' : undefined}\n onClick={() => navigate(lessonPlayerPath(courseId, l.id))}\n className={cn(\n 'flex w-full items-center gap-2 rounded-lg px-2 py-1.5 text-left text-sm transition-colors hover:bg-bg-sunken',\n current && 'bg-accent-primary/6 font-medium text-accent-primary'\n )}\n >\n <span className=\"w-5 shrink-0 text-center text-xs tabular-nums text-text-muted\">\n {i + 1}\n </span>\n {l.completedByMe ? (\n <CheckCircle2 size={14} className=\"shrink-0 text-status-success-text\" />\n ) : l.isLocked ? (\n <Lock size={14} className=\"shrink-0 text-text-muted\" />\n ) : (\n <LIcon size={14} className=\"shrink-0 text-text-muted\" />\n )}\n <span className=\"min-w-0 flex-1 truncate\">{l.title}</span>\n </button>\n </li>\n );\n })}\n </ol>\n </GlassCard>\n </div>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"HackathonsHackathonsPagePage.d.ts","sourceRoot":"","sources":["../../../src/technospam/pages/HackathonsHackathonsPagePage.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"HackathonsHackathonsPagePage.d.ts","sourceRoot":"","sources":["../../../src/technospam/pages/HackathonsHackathonsPagePage.tsx"],"names":[],"mappings":"AAsFA,wBAAgB,4BAA4B,gCA4K3C"}
|
|
@@ -9,68 +9,71 @@ import { StatusBadge as l } from "../components/StatusBadge.js";
|
|
|
9
9
|
import { LoadMore as u } from "../components/LoadMore.js";
|
|
10
10
|
import { EmptyView as d, ErrorView as f, LoadingGrid as p } from "../components/StateViews.js";
|
|
11
11
|
import { pageArg as m, useHackathonsQuery as h, usePageLimit as g } from "../api/technospamApi.js";
|
|
12
|
+
import { CardOpenButton as _ } from "../components/CardOpenButton.js";
|
|
12
13
|
import "../components/index.js";
|
|
13
|
-
import { TAB_PANEL_FOCUS_CLASS as
|
|
14
|
-
import { useMemo as
|
|
15
|
-
import { Award as
|
|
16
|
-
import { jsx as
|
|
17
|
-
import { Badge as
|
|
14
|
+
import { TAB_PANEL_FOCUS_CLASS as v } from "../lib/a11y.js";
|
|
15
|
+
import { useMemo as y, useState as b } from "react";
|
|
16
|
+
import { Award as x, CalendarDays as S, Search as C, Sparkles as w, Trophy as T, Users as E } from "lucide-react";
|
|
17
|
+
import { jsx as D, jsxs as O } from "react/jsx-runtime";
|
|
18
|
+
import { Badge as k, GlassCard as A, Input as j, Tabs as M, TabsContent as N, TabsList as P, TabsTrigger as F } from "@burdenoff/fe-libs/ui";
|
|
18
19
|
//#region src/technospam/pages/HackathonsHackathonsPagePage.tsx
|
|
19
|
-
function
|
|
20
|
+
function I({ hackathon: e, onOpen: t }) {
|
|
20
21
|
let n = r();
|
|
21
|
-
return /* @__PURE__ */
|
|
22
|
+
return /* @__PURE__ */ O(A, {
|
|
22
23
|
className: "group flex cursor-pointer flex-col overflow-hidden transition-all duration-200 hover:-translate-y-1 hover:shadow-lg",
|
|
23
|
-
|
|
24
|
-
children: [/* @__PURE__ */ E(o, {
|
|
24
|
+
children: [/* @__PURE__ */ D(o, {
|
|
25
25
|
height: "h-32",
|
|
26
26
|
seed: e.theme ?? e.id,
|
|
27
|
-
icon:
|
|
28
|
-
children: /* @__PURE__ */
|
|
29
|
-
}), /* @__PURE__ */
|
|
27
|
+
icon: T,
|
|
28
|
+
children: /* @__PURE__ */ D(l, { status: e.status })
|
|
29
|
+
}), /* @__PURE__ */ O("div", {
|
|
30
30
|
className: "flex flex-1 flex-col gap-3 p-4",
|
|
31
31
|
children: [
|
|
32
|
-
/* @__PURE__ */
|
|
32
|
+
/* @__PURE__ */ O("div", {
|
|
33
33
|
className: "min-w-0",
|
|
34
|
-
children: [/* @__PURE__ */
|
|
34
|
+
children: [/* @__PURE__ */ D("h3", {
|
|
35
35
|
className: "truncate text-base font-semibold text-text-primary group-hover:text-accent-primary",
|
|
36
|
-
children:
|
|
37
|
-
|
|
36
|
+
children: /* @__PURE__ */ D(_, {
|
|
37
|
+
onOpen: t,
|
|
38
|
+
children: e.title
|
|
39
|
+
})
|
|
40
|
+
}), e.theme ? /* @__PURE__ */ D("p", {
|
|
38
41
|
className: "mt-1 line-clamp-2 text-sm text-text-secondary",
|
|
39
42
|
children: e.theme
|
|
40
43
|
}) : null]
|
|
41
44
|
}),
|
|
42
|
-
/* @__PURE__ */
|
|
45
|
+
/* @__PURE__ */ O("div", {
|
|
43
46
|
className: "mt-auto flex flex-wrap items-center gap-x-4 gap-y-1.5 text-xs text-text-muted",
|
|
44
|
-
children: [/* @__PURE__ */
|
|
47
|
+
children: [/* @__PURE__ */ O("span", {
|
|
45
48
|
className: "inline-flex items-center gap-1",
|
|
46
49
|
children: [
|
|
47
|
-
/* @__PURE__ */
|
|
50
|
+
/* @__PURE__ */ D(S, { size: 13 }),
|
|
48
51
|
" ",
|
|
49
52
|
s(e.startAt),
|
|
50
53
|
" –",
|
|
51
54
|
" ",
|
|
52
55
|
s(e.endAt)
|
|
53
56
|
]
|
|
54
|
-
}), /* @__PURE__ */
|
|
57
|
+
}), /* @__PURE__ */ O("span", {
|
|
55
58
|
className: "inline-flex items-center gap-1",
|
|
56
59
|
children: [
|
|
57
|
-
/* @__PURE__ */ E
|
|
60
|
+
/* @__PURE__ */ D(E, { size: 13 }),
|
|
58
61
|
" ",
|
|
59
62
|
n("technospam.hackathons.teamSize", "Team of {{size}}", { size: e.maxTeamSize })
|
|
60
63
|
]
|
|
61
64
|
})]
|
|
62
65
|
}),
|
|
63
|
-
/* @__PURE__ */
|
|
66
|
+
/* @__PURE__ */ O("div", {
|
|
64
67
|
className: "flex items-center gap-1.5 text-sm font-medium text-accent-teal",
|
|
65
68
|
children: [
|
|
66
|
-
/* @__PURE__ */
|
|
69
|
+
/* @__PURE__ */ D(x, { size: 15 }),
|
|
67
70
|
" ",
|
|
68
71
|
n("technospam.hackathons.prizePool", "{{prize}} prize pool", { prize: c(e.prizePool, e.currency) })
|
|
69
72
|
]
|
|
70
73
|
}),
|
|
71
|
-
e.tags.length ? /* @__PURE__ */
|
|
74
|
+
e.tags.length ? /* @__PURE__ */ D("div", {
|
|
72
75
|
className: "flex flex-wrap gap-1.5",
|
|
73
|
-
children: e.tags.slice(0, 4).map((e) => /* @__PURE__ */
|
|
76
|
+
children: e.tags.slice(0, 4).map((e) => /* @__PURE__ */ D(k, {
|
|
74
77
|
variant: "secondary",
|
|
75
78
|
className: "text-[11px]",
|
|
76
79
|
children: e
|
|
@@ -80,81 +83,81 @@ function F({ hackathon: e, onOpen: t }) {
|
|
|
80
83
|
})]
|
|
81
84
|
});
|
|
82
85
|
}
|
|
83
|
-
function
|
|
84
|
-
let o = t(), { workspaceId: s } = e(), { emit: l } = n(),
|
|
86
|
+
function L() {
|
|
87
|
+
let o = t(), { workspaceId: s } = e(), { emit: l } = n(), _ = r(), [S, E] = b("ALL"), [k, A] = b(""), { limit: L, more: R } = g(), z = y(() => [
|
|
85
88
|
{
|
|
86
89
|
value: "ALL",
|
|
87
|
-
label:
|
|
90
|
+
label: _("technospam.hackathons.filterAll", "All")
|
|
88
91
|
},
|
|
89
92
|
{
|
|
90
93
|
value: "REGISTRATION_OPEN",
|
|
91
|
-
label:
|
|
94
|
+
label: _("technospam.hackathons.filterRegistrationOpen", "Registration open")
|
|
92
95
|
},
|
|
93
96
|
{
|
|
94
97
|
value: "IN_PROGRESS",
|
|
95
|
-
label:
|
|
98
|
+
label: _("technospam.hackathons.filterInProgress", "In progress")
|
|
96
99
|
},
|
|
97
100
|
{
|
|
98
101
|
value: "JUDGING",
|
|
99
|
-
label:
|
|
102
|
+
label: _("technospam.hackathons.filterJudging", "Judging")
|
|
100
103
|
},
|
|
101
104
|
{
|
|
102
105
|
value: "COMPLETED",
|
|
103
|
-
label:
|
|
106
|
+
label: _("technospam.hackathons.filterCompleted", "Completed")
|
|
104
107
|
}
|
|
105
|
-
], [
|
|
108
|
+
], [_]), { data: B, loading: V, error: H } = h({
|
|
106
109
|
pagination: m(L),
|
|
107
|
-
...
|
|
108
|
-
}), U = B?.tecListHackathons?.items ?? [], W = B?.tecListHackathons?.pageInfo?.hasMore ?? !1, G =
|
|
110
|
+
...S === "ALL" ? {} : { status: S }
|
|
111
|
+
}), U = B?.tecListHackathons?.items ?? [], W = B?.tecListHackathons?.pageInfo?.hasMore ?? !1, G = y(() => {
|
|
109
112
|
let e = k.trim().toLowerCase();
|
|
110
113
|
return e ? U.filter((t) => t.title.toLowerCase().includes(e) || (t.theme ?? "").toLowerCase().includes(e) || t.tags.some((t) => t.toLowerCase().includes(e))) : U;
|
|
111
114
|
}, [U, k]), K = G.filter((e) => e.status === "REGISTRATION_OPEN").length, q = G.reduce((e, t) => e + t.prizePool, 0), J = G[0]?.currency ?? "USD";
|
|
112
|
-
return /* @__PURE__ */
|
|
115
|
+
return /* @__PURE__ */ O(i, {
|
|
113
116
|
hero: !0,
|
|
114
|
-
title:
|
|
115
|
-
description:
|
|
116
|
-
children: [/* @__PURE__ */
|
|
117
|
+
title: _("technospam.hackathons.title", "Hackathons"),
|
|
118
|
+
description: _("technospam.hackathons.description", "Compete, collaborate, and ship real projects with a team in a fixed window."),
|
|
119
|
+
children: [/* @__PURE__ */ O("div", {
|
|
117
120
|
className: "mb-5 grid grid-cols-1 gap-3 sm:grid-cols-3",
|
|
118
121
|
children: [
|
|
119
|
-
/* @__PURE__ */
|
|
120
|
-
label:
|
|
122
|
+
/* @__PURE__ */ D(a, {
|
|
123
|
+
label: _("technospam.hackathons.statHackathons", "Hackathons"),
|
|
121
124
|
value: G.length,
|
|
122
|
-
icon:
|
|
125
|
+
icon: T,
|
|
123
126
|
tone: "purple"
|
|
124
127
|
}),
|
|
125
|
-
/* @__PURE__ */
|
|
126
|
-
label:
|
|
128
|
+
/* @__PURE__ */ D(a, {
|
|
129
|
+
label: _("technospam.hackathons.statRegistrationOpen", "Registration open"),
|
|
127
130
|
value: K,
|
|
128
|
-
icon:
|
|
131
|
+
icon: w,
|
|
129
132
|
tone: "green"
|
|
130
133
|
}),
|
|
131
|
-
/* @__PURE__ */
|
|
132
|
-
label:
|
|
134
|
+
/* @__PURE__ */ D(a, {
|
|
135
|
+
label: _("technospam.hackathons.statTotalPrizePool", "Total prize pool"),
|
|
133
136
|
value: c(q, J),
|
|
134
|
-
icon:
|
|
137
|
+
icon: x,
|
|
135
138
|
tone: "amber"
|
|
136
139
|
})
|
|
137
140
|
]
|
|
138
|
-
}), /* @__PURE__ */
|
|
141
|
+
}), /* @__PURE__ */ O(M, {
|
|
139
142
|
className: "gap-0",
|
|
140
|
-
value:
|
|
141
|
-
onValueChange: (e) =>
|
|
142
|
-
children: [/* @__PURE__ */
|
|
143
|
+
value: S,
|
|
144
|
+
onValueChange: (e) => E(e),
|
|
145
|
+
children: [/* @__PURE__ */ O("div", {
|
|
143
146
|
className: "mb-4 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between",
|
|
144
|
-
children: [/* @__PURE__ */
|
|
147
|
+
children: [/* @__PURE__ */ D(P, { children: z.map((e) => /* @__PURE__ */ D(F, {
|
|
145
148
|
value: e.value,
|
|
146
149
|
children: e.label
|
|
147
|
-
}, e.value)) }), /* @__PURE__ */
|
|
150
|
+
}, e.value)) }), /* @__PURE__ */ O("div", {
|
|
148
151
|
className: "relative w-full sm:w-72",
|
|
149
|
-
children: [/* @__PURE__ */
|
|
152
|
+
children: [/* @__PURE__ */ D(C, {
|
|
150
153
|
className: "pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-text-muted",
|
|
151
154
|
size: 16
|
|
152
|
-
}), /* @__PURE__ */
|
|
155
|
+
}), /* @__PURE__ */ D(j, {
|
|
153
156
|
className: "pl-9",
|
|
154
|
-
"aria-label":
|
|
155
|
-
placeholder:
|
|
157
|
+
"aria-label": _("technospam.hackathons.searchLabel", "Search hackathons"),
|
|
158
|
+
placeholder: _("technospam.hackathons.searchPlaceholder", "Search hackathons…"),
|
|
156
159
|
value: k,
|
|
157
|
-
onChange: (e) =>
|
|
160
|
+
onChange: (e) => A(e.target.value),
|
|
158
161
|
onBlur: () => {
|
|
159
162
|
let e = k.trim();
|
|
160
163
|
e && l("technospam.search.performed", {
|
|
@@ -165,19 +168,19 @@ function I() {
|
|
|
165
168
|
}
|
|
166
169
|
})]
|
|
167
170
|
})]
|
|
168
|
-
}), /* @__PURE__ */
|
|
169
|
-
value:
|
|
170
|
-
className:
|
|
171
|
-
children: [V && !G.length ? /* @__PURE__ */
|
|
172
|
-
title:
|
|
171
|
+
}), /* @__PURE__ */ O(N, {
|
|
172
|
+
value: S,
|
|
173
|
+
className: v,
|
|
174
|
+
children: [V && !G.length ? /* @__PURE__ */ D(p, {}) : H ? /* @__PURE__ */ D(f, {
|
|
175
|
+
title: _("technospam.common.errorLoadHackathons", "Couldn't load hackathons"),
|
|
173
176
|
error: H
|
|
174
|
-
}) : G.length === 0 ? /* @__PURE__ */
|
|
175
|
-
icon: /* @__PURE__ */
|
|
176
|
-
title:
|
|
177
|
-
description:
|
|
178
|
-
}) : /* @__PURE__ */
|
|
177
|
+
}) : G.length === 0 ? /* @__PURE__ */ D(d, {
|
|
178
|
+
icon: /* @__PURE__ */ D(T, { size: 26 }),
|
|
179
|
+
title: S === "ALL" ? _("technospam.hackathons.emptyDefaultTitle", "No hackathons yet") : _("technospam.hackathons.emptyFilteredTitle", "No hackathons in this stage"),
|
|
180
|
+
description: S === "ALL" ? _("technospam.hackathons.emptyDefaultDesc", "Upcoming hackathons will appear here. Check back soon — TechnoSpam is pre-launch.") : _("technospam.hackathons.emptyFilteredDesc", "Try a different stage filter.")
|
|
181
|
+
}) : /* @__PURE__ */ D("div", {
|
|
179
182
|
className: "grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3",
|
|
180
|
-
children: G.map((e) => /* @__PURE__ */
|
|
183
|
+
children: G.map((e) => /* @__PURE__ */ D(I, {
|
|
181
184
|
hackathon: e,
|
|
182
185
|
onOpen: () => {
|
|
183
186
|
l("technospam.hackathon.viewed", {
|
|
@@ -187,7 +190,7 @@ function I() {
|
|
|
187
190
|
}), o(`/hackathons/hackathon-details-page?id=${e.id}`);
|
|
188
191
|
}
|
|
189
192
|
}, e.id))
|
|
190
|
-
}), /* @__PURE__ */
|
|
193
|
+
}), /* @__PURE__ */ D(u, {
|
|
191
194
|
hasMore: W,
|
|
192
195
|
loading: V,
|
|
193
196
|
onClick: R
|
|
@@ -197,6 +200,6 @@ function I() {
|
|
|
197
200
|
});
|
|
198
201
|
}
|
|
199
202
|
//#endregion
|
|
200
|
-
export {
|
|
203
|
+
export { L as HackathonsHackathonsPagePage };
|
|
201
204
|
|
|
202
205
|
//# sourceMappingURL=HackathonsHackathonsPagePage.js.map
|