@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.
Files changed (43) hide show
  1. package/dist/stories/journeys/Courses.stories.d.ts +5 -0
  2. package/dist/stories/journeys/Courses.stories.d.ts.map +1 -1
  3. package/dist/stories/journeys/Mentorship.stories.d.ts +5 -0
  4. package/dist/stories/journeys/Mentorship.stories.d.ts.map +1 -1
  5. package/dist/stories/journeys/Problems.stories.d.ts.map +1 -1
  6. package/dist/technospam/api/technospamApi.d.ts +15 -0
  7. package/dist/technospam/api/technospamApi.d.ts.map +1 -1
  8. package/dist/technospam/api/technospamApi.js +200 -151
  9. package/dist/technospam/api/technospamApi.js.map +1 -1
  10. package/dist/technospam/components/CardOpenButton.d.ts +26 -0
  11. package/dist/technospam/components/CardOpenButton.d.ts.map +1 -0
  12. package/dist/technospam/components/CardOpenButton.js +15 -0
  13. package/dist/technospam/components/CardOpenButton.js.map +1 -0
  14. package/dist/technospam/components/ProblemsTable.d.ts +19 -0
  15. package/dist/technospam/components/ProblemsTable.d.ts.map +1 -0
  16. package/dist/technospam/components/ProblemsTable.js +117 -0
  17. package/dist/technospam/components/ProblemsTable.js.map +1 -0
  18. package/dist/technospam/components/index.d.ts +2 -0
  19. package/dist/technospam/components/index.d.ts.map +1 -1
  20. package/dist/technospam/components/index.js +3 -1
  21. package/dist/technospam/pages/ApplicationsProblemDetailsPagePage.js +38 -38
  22. package/dist/technospam/pages/ApplicationsProblemDetailsPagePage.js.map +1 -1
  23. package/dist/technospam/pages/ApplicationsProblemsPagePage.d.ts.map +1 -1
  24. package/dist/technospam/pages/ApplicationsProblemsPagePage.js +197 -144
  25. package/dist/technospam/pages/ApplicationsProblemsPagePage.js.map +1 -1
  26. package/dist/technospam/pages/CoursesCourseDetailsPagePage.js +195 -195
  27. package/dist/technospam/pages/CoursesCourseDetailsPagePage.js.map +1 -1
  28. package/dist/technospam/pages/CoursesCoursesPagePage.d.ts.map +1 -1
  29. package/dist/technospam/pages/CoursesCoursesPagePage.js +87 -84
  30. package/dist/technospam/pages/CoursesCoursesPagePage.js.map +1 -1
  31. package/dist/technospam/pages/CoursesLessonPlayerPagePage.js +36 -36
  32. package/dist/technospam/pages/CoursesLessonPlayerPagePage.js.map +1 -1
  33. package/dist/technospam/pages/HackathonsHackathonsPagePage.d.ts.map +1 -1
  34. package/dist/technospam/pages/HackathonsHackathonsPagePage.js +75 -72
  35. package/dist/technospam/pages/HackathonsHackathonsPagePage.js.map +1 -1
  36. package/dist/technospam/pages/MentorshipMentorsPagePage.d.ts.map +1 -1
  37. package/dist/technospam/pages/MentorshipMentorsPagePage.js +1 -0
  38. package/dist/technospam/pages/MentorshipMentorsPagePage.js.map +1 -1
  39. package/dist/technospam/utils/problemCatalog.d.ts +27 -0
  40. package/dist/technospam/utils/problemCatalog.d.ts.map +1 -0
  41. package/dist/technospam/utils/problemCatalog.js +68 -0
  42. package/dist/technospam/utils/problemCatalog.js.map +1 -0
  43. 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 { EmptyView as o, ErrorView as te } from "../components/StateViews.js";
7
- import { UpgradeGate as s } from "../components/UpgradeGate.js";
8
- import { notify as c } from "../lib/toast.js";
9
- import { useCompleteLesson as l, useEnroll as u, useLessonPlayerQuery as ne, useMyEnrollmentsQuery as d } from "../api/technospamApi.js";
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 f } from "../utils/pageContext.js";
12
- import { courseDetailsPath as p, lessonPlayerPath as m } from "../utils/lessonPath.js";
13
- import { InteractiveWorkspace as h } from "../components/lesson/InteractiveWorkspace.js";
14
- import { QuizLesson as g } from "../components/lesson/QuizLesson.js";
15
- import { useEffect as re } from "react";
16
- import { ArrowLeft as ie, ArrowRight as _, BookOpen as v, CheckCircle2 as y, Clock as b, FileText as x, Layers as S, Lock as ae, PlayCircle as C, Sparkles as w } from "lucide-react";
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: y,
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(g, {
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(h, {
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 [h] = T(), g = h.get(f.id) ?? "", S = h.get(f.lesson) ?? "", C = t(), { workspaceId: w } = e(), { emit: j } = n(), { data: P, loading: F, error: I, refetch: L } = ne(g, S), [R, { loading: z }] = l(), [B, { loading: V }] = u();
86
- re(() => {
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 } = d({
97
+ let H = P?.tecLesson ?? null, U = P?.tecCourse ?? null, { enrollments: W, refetch: G } = f({
98
98
  buiCourseId: U?.buiCourseId,
99
- technospamCourseId: g,
100
- skip: !g
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 = p(g), se = () => {
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
- }), c.success("Lesson completed", `${e.progressPct}% of the course done`), X ? C(m(g, X.id)) : await L());
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: g } } })).data?.tecEnroll && (c.success("Enrolled", `Your progress in "${U?.title ?? "this course"}" is now tracked.`), await L());
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 (!g || !S) return /* @__PURE__ */ E(i, {
114
+ if (!_ || !S) return /* @__PURE__ */ E(i, {
115
115
  title: "Lesson",
116
116
  backPath: "/courses/courses-page",
117
- children: /* @__PURE__ */ E(o, {
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(te, {
130
+ children: I ? /* @__PURE__ */ E(c, {
131
131
  title: "Couldn't load lesson",
132
132
  error: I
133
- }) : /* @__PURE__ */ E(o, {
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(y, { size: 15 }), " Completed"]
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(b, { size: 12 }),
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(v, { size: 14 }),
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(s, { description: "This lesson is part of a paid TechnoSpam plan. Its content unlocks once your workspace upgrades." }) : /* @__PURE__ */ E(N, {
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(m(g, Y.id)),
207
- children: [/* @__PURE__ */ E(ie, { size: 15 }), " Previous"]
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(y, { size: 15 }),
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(m(g, X.id)),
228
- children: ["Next ", /* @__PURE__ */ E(_, { size: 15 })]
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(m(g, e.id)),
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(y, {
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":"AAwFA,wBAAgB,4BAA4B,gCA4K3C"}
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 _ } from "../lib/a11y.js";
14
- import { useMemo as v, useState as y } from "react";
15
- import { Award as b, CalendarDays as x, Search as S, Sparkles as C, Trophy as w, Users as T } from "lucide-react";
16
- import { jsx as E, jsxs as D } from "react/jsx-runtime";
17
- import { Badge as O, GlassCard as k, Input as A, Tabs as j, TabsContent as M, TabsList as N, TabsTrigger as P } from "@burdenoff/fe-libs/ui";
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 F({ hackathon: e, onOpen: t }) {
20
+ function I({ hackathon: e, onOpen: t }) {
20
21
  let n = r();
21
- return /* @__PURE__ */ D(k, {
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
- onClick: t,
24
- children: [/* @__PURE__ */ E(o, {
24
+ children: [/* @__PURE__ */ D(o, {
25
25
  height: "h-32",
26
26
  seed: e.theme ?? e.id,
27
- icon: w,
28
- children: /* @__PURE__ */ E(l, { status: e.status })
29
- }), /* @__PURE__ */ D("div", {
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__ */ D("div", {
32
+ /* @__PURE__ */ O("div", {
33
33
  className: "min-w-0",
34
- children: [/* @__PURE__ */ E("h3", {
34
+ children: [/* @__PURE__ */ D("h3", {
35
35
  className: "truncate text-base font-semibold text-text-primary group-hover:text-accent-primary",
36
- children: e.title
37
- }), e.theme ? /* @__PURE__ */ E("p", {
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__ */ D("div", {
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__ */ D("span", {
47
+ children: [/* @__PURE__ */ O("span", {
45
48
  className: "inline-flex items-center gap-1",
46
49
  children: [
47
- /* @__PURE__ */ E(x, { size: 13 }),
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__ */ D("span", {
57
+ }), /* @__PURE__ */ O("span", {
55
58
  className: "inline-flex items-center gap-1",
56
59
  children: [
57
- /* @__PURE__ */ E(T, { size: 13 }),
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__ */ D("div", {
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__ */ E(b, { size: 15 }),
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__ */ E("div", {
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__ */ E(O, {
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 I() {
84
- let o = t(), { workspaceId: s } = e(), { emit: l } = n(), x = r(), [T, O] = y("ALL"), [k, I] = y(""), { limit: L, more: R } = g(), z = v(() => [
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: x("technospam.hackathons.filterAll", "All")
90
+ label: _("technospam.hackathons.filterAll", "All")
88
91
  },
89
92
  {
90
93
  value: "REGISTRATION_OPEN",
91
- label: x("technospam.hackathons.filterRegistrationOpen", "Registration open")
94
+ label: _("technospam.hackathons.filterRegistrationOpen", "Registration open")
92
95
  },
93
96
  {
94
97
  value: "IN_PROGRESS",
95
- label: x("technospam.hackathons.filterInProgress", "In progress")
98
+ label: _("technospam.hackathons.filterInProgress", "In progress")
96
99
  },
97
100
  {
98
101
  value: "JUDGING",
99
- label: x("technospam.hackathons.filterJudging", "Judging")
102
+ label: _("technospam.hackathons.filterJudging", "Judging")
100
103
  },
101
104
  {
102
105
  value: "COMPLETED",
103
- label: x("technospam.hackathons.filterCompleted", "Completed")
106
+ label: _("technospam.hackathons.filterCompleted", "Completed")
104
107
  }
105
- ], [x]), { data: B, loading: V, error: H } = h({
108
+ ], [_]), { data: B, loading: V, error: H } = h({
106
109
  pagination: m(L),
107
- ...T === "ALL" ? {} : { status: T }
108
- }), U = B?.tecListHackathons?.items ?? [], W = B?.tecListHackathons?.pageInfo?.hasMore ?? !1, G = v(() => {
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__ */ D(i, {
115
+ return /* @__PURE__ */ O(i, {
113
116
  hero: !0,
114
- title: x("technospam.hackathons.title", "Hackathons"),
115
- description: x("technospam.hackathons.description", "Compete, collaborate, and ship real projects with a team in a fixed window."),
116
- children: [/* @__PURE__ */ D("div", {
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__ */ E(a, {
120
- label: x("technospam.hackathons.statHackathons", "Hackathons"),
122
+ /* @__PURE__ */ D(a, {
123
+ label: _("technospam.hackathons.statHackathons", "Hackathons"),
121
124
  value: G.length,
122
- icon: w,
125
+ icon: T,
123
126
  tone: "purple"
124
127
  }),
125
- /* @__PURE__ */ E(a, {
126
- label: x("technospam.hackathons.statRegistrationOpen", "Registration open"),
128
+ /* @__PURE__ */ D(a, {
129
+ label: _("technospam.hackathons.statRegistrationOpen", "Registration open"),
127
130
  value: K,
128
- icon: C,
131
+ icon: w,
129
132
  tone: "green"
130
133
  }),
131
- /* @__PURE__ */ E(a, {
132
- label: x("technospam.hackathons.statTotalPrizePool", "Total prize pool"),
134
+ /* @__PURE__ */ D(a, {
135
+ label: _("technospam.hackathons.statTotalPrizePool", "Total prize pool"),
133
136
  value: c(q, J),
134
- icon: b,
137
+ icon: x,
135
138
  tone: "amber"
136
139
  })
137
140
  ]
138
- }), /* @__PURE__ */ D(j, {
141
+ }), /* @__PURE__ */ O(M, {
139
142
  className: "gap-0",
140
- value: T,
141
- onValueChange: (e) => O(e),
142
- children: [/* @__PURE__ */ D("div", {
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__ */ E(N, { children: z.map((e) => /* @__PURE__ */ E(P, {
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__ */ D("div", {
150
+ }, e.value)) }), /* @__PURE__ */ O("div", {
148
151
  className: "relative w-full sm:w-72",
149
- children: [/* @__PURE__ */ E(S, {
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__ */ E(A, {
155
+ }), /* @__PURE__ */ D(j, {
153
156
  className: "pl-9",
154
- "aria-label": x("technospam.hackathons.searchLabel", "Search hackathons"),
155
- placeholder: x("technospam.hackathons.searchPlaceholder", "Search hackathons…"),
157
+ "aria-label": _("technospam.hackathons.searchLabel", "Search hackathons"),
158
+ placeholder: _("technospam.hackathons.searchPlaceholder", "Search hackathons…"),
156
159
  value: k,
157
- onChange: (e) => I(e.target.value),
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__ */ D(M, {
169
- value: T,
170
- className: _,
171
- children: [V && !G.length ? /* @__PURE__ */ E(p, {}) : H ? /* @__PURE__ */ E(f, {
172
- title: x("technospam.common.errorLoadHackathons", "Couldn't load hackathons"),
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__ */ E(d, {
175
- icon: /* @__PURE__ */ E(w, { size: 26 }),
176
- title: T === "ALL" ? x("technospam.hackathons.emptyDefaultTitle", "No hackathons yet") : x("technospam.hackathons.emptyFilteredTitle", "No hackathons in this stage"),
177
- description: T === "ALL" ? x("technospam.hackathons.emptyDefaultDesc", "Upcoming hackathons will appear here. Check back soon — TechnoSpam is pre-launch.") : x("technospam.hackathons.emptyFilteredDesc", "Try a different stage filter.")
178
- }) : /* @__PURE__ */ E("div", {
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__ */ E(F, {
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__ */ E(u, {
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 { I as HackathonsHackathonsPagePage };
203
+ export { L as HackathonsHackathonsPagePage };
201
204
 
202
205
  //# sourceMappingURL=HackathonsHackathonsPagePage.js.map