@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
@@ -1 +1 @@
1
- {"version":3,"file":"CoursesCourseDetailsPagePage.js","names":[],"sources":["../../../src/technospam/pages/CoursesCourseDetailsPagePage.tsx"],"sourcesContent":["import type { ReactNode } from 'react';\nimport { useEffect } from 'react';\nimport { useSearchParams } from 'react-router';\nimport {\n ArrowRight,\n Award,\n BookOpen,\n CheckCircle2,\n ChevronDown,\n Clock,\n Eye,\n FileText,\n Lock,\n PlayCircle,\n Layers,\n ShieldCheck,\n Sparkles,\n Trophy,\n TrendingUp,\n Users,\n type LucideIcon,\n} from 'lucide-react';\nimport { Button, GlassCard, Skeleton } from '@burdenoff/fe-libs/ui';\nimport {\n EmptyView,\n ErrorView,\n MediaCover,\n PageLayout,\n PremiumBadge,\n StatCard,\n TagStrip,\n UpgradeGate,\n topicIcon,\n} from '../components';\nimport { useTechnoSpam, useTechnoSpamNavigate } from '../providers/TechnoSpamProvider';\nimport {\n useCourseQuery,\n useEnroll,\n useIssueCertificate,\n useMyCertificatesQuery,\n type TecCertificate,\n type TecLesson,\n type TecEnrollment,\n} from '../api/technospamApi';\nimport { formatDate, formatDuration, humanizeEnum } from '../utils/format';\nimport { notify } from '../lib/toast';\nimport { cn } from '../utils/cn';\nimport { useTechnoSpamAnalytics } from '../hooks/useTechnoSpamAnalytics';\nimport { useFocusedRecordParam } from '../hooks/useFocusedRecordParam';\nimport { FOCUS_PARAMS } from '../utils/pageContext';\nimport { lessonPlayerPath } from '../utils/lessonPath';\n\n/** Per-lesson-kind icon + on-theme tint (chip) and pill (badge) classes. */\nconst KIND_META: Record<string, { icon: LucideIcon; chip: string; badge: string }> = {\n VIDEO: {\n icon: PlayCircle,\n chip: 'bg-accent-primary/15 text-accent-primary',\n badge: 'border-accent-primary/25 bg-accent-primary/6 text-accent-primary',\n },\n ARTICLE: {\n icon: FileText,\n chip: 'bg-accent-indigo/15 text-accent-indigo',\n badge: 'border-accent-indigo/25 bg-accent-indigo/10 text-accent-indigo',\n },\n INTERACTIVE: {\n icon: Sparkles,\n chip: 'bg-accent-teal/15 text-accent-teal',\n badge: 'border-accent-teal/25 bg-accent-teal/10 text-accent-teal',\n },\n QUIZ: {\n icon: CheckCircle2,\n chip: 'bg-accent-purple/15 text-accent-purple',\n badge: 'border-accent-purple/25 bg-accent-purple/10 text-accent-purple',\n },\n PROJECT: {\n icon: Layers,\n chip: 'bg-status-warning-bg-subtle text-status-warning-text',\n badge: 'border-status-warning-border bg-status-warning-bg-subtle text-status-warning-text',\n },\n};\n\nfunction HeroBadge({ icon: Icon, children }: { icon?: LucideIcon; children: ReactNode }) {\n return (\n <span className=\"inline-flex items-center gap-1 rounded-full bg-white/15 px-2.5 py-1 text-xs font-medium text-white ring-1 ring-white/25 backdrop-blur-sm\">\n {Icon ? <Icon size={12} /> : null}\n {children}\n </span>\n );\n}\n\n/**\n * One curriculum row. Expands in place to reveal the lesson body.\n *\n * `TecLesson.content` is the only lesson body this service exposes to a\n * learner, and the API nulls it server-side whenever `isLocked` is true\n * (BOFF-4416), so the three states below are exhaustive and never guessed:\n * locked → upgrade note, content present → the body, content null on an\n * unlocked lesson → the author simply hasn't written it yet.\n */\nfunction LessonRow({\n lesson,\n index,\n done,\n open,\n onToggle,\n onStart,\n}: {\n lesson: TecLesson;\n index: number;\n done: boolean;\n /** The open lesson is the focused one (`?lessonId=`), so the assistant can name it. */\n open: boolean;\n onToggle: () => void;\n /** Opens the lesson in the lesson player (BOFF-4107). */\n onStart: () => void;\n}) {\n const meta = KIND_META[lesson.kind] ?? KIND_META.ARTICLE!;\n const Icon = meta.icon;\n const bodyId = `tec-lesson-body-${lesson.id}`;\n\n return (\n <div className=\"border-b border-border-seam last:border-b-0\">\n <button\n type=\"button\"\n onClick={onToggle}\n aria-expanded={open}\n aria-controls={bodyId}\n className=\"group flex w-full items-center gap-3 rounded-xl px-2 py-3 text-left transition-colors hover:bg-bg-sunken/60\"\n >\n <span className=\"hidden w-6 shrink-0 text-center text-sm font-medium tabular-nums text-text-muted sm:block\">\n {index + 1}\n </span>\n <span\n className={cn(\n 'flex h-9 w-9 shrink-0 items-center justify-center rounded-lg',\n done ? 'bg-status-success-bg-subtle text-status-success-text' : meta.chip\n )}\n >\n {done ? <CheckCircle2 size={17} /> : <Icon size={17} />}\n </span>\n <div className=\"min-w-0 flex-1\">\n <span className=\"block truncate text-sm font-medium text-text-primary\">\n {lesson.title}\n </span>\n <div className=\"mt-1 flex flex-wrap items-center gap-2\">\n <span\n className={cn(\n 'inline-flex items-center rounded-full border px-2 py-0.5 text-[11px] font-medium',\n meta.badge\n )}\n >\n {humanizeEnum(lesson.kind)}\n </span>\n <span className=\"inline-flex items-center gap-1 text-xs text-text-muted\">\n <Clock size={12} /> {formatDuration(lesson.estimatedMinutes)}\n </span>\n </div>\n </div>\n <div className=\"flex shrink-0 items-center gap-2\">\n {done ? (\n <CheckCircle2 size={18} className=\"text-status-success-text\" />\n ) : lesson.isPreview ? (\n <span className=\"inline-flex items-center gap-1 rounded-full border border-accent-primary/30 bg-accent-primary/6 px-2 py-0.5 text-[11px] font-medium text-accent-primary\">\n <Eye size={12} /> Preview\n </span>\n ) : lesson.isLocked ? (\n // BOFF-4416: distinct from the plain \"not completed yet\" Lock icon\n // below — this lesson's content is gated behind a paid plan, not\n // just not-yet-watched. The API already nulled `content`/`file`.\n <span className=\"inline-flex items-center gap-1 rounded-full border border-status-warning-border/60 bg-status-warning-bg-subtle px-2 py-0.5 text-[11px] font-medium text-status-warning-text\">\n <Lock size={12} /> Upgrade to unlock\n </span>\n ) : (\n <Lock size={15} className=\"text-text-muted\" />\n )}\n <ChevronDown\n size={16}\n aria-hidden\n className={cn(\n 'text-text-muted transition-transform duration-200',\n open ? 'rotate-180' : ''\n )}\n />\n </div>\n </button>\n\n {open ? (\n <div id={bodyId} className=\"px-2 pb-4 sm:pl-11\">\n {lesson.isLocked ? (\n <UpgradeGate\n compact\n description=\"This lesson is part of a paid TechnoSpam plan — its content unlocks once your workspace upgrades.\"\n />\n ) : (\n <div className=\"space-y-3\">\n {lesson.content ? (\n <div className=\"rounded-xl border border-border-seam bg-bg-sunken p-4\">\n <p className=\"line-clamp-6 whitespace-pre-wrap text-sm leading-relaxed text-text-secondary\">\n {lesson.content}\n </p>\n </div>\n ) : null}\n <Button size=\"sm\" onClick={onStart} data-testid={`open-lesson-${lesson.id}`}>\n <PlayCircle size={15} /> {done ? 'Review lesson' : 'Start lesson'}\n </Button>\n </div>\n )}\n </div>\n ) : null}\n </div>\n );\n}\n\n/**\n * Verifiable certificate face for an already-issued certificate. Every value\n * shown is a real field on the TecCertificate row (`serial`, `score`,\n * `issuedAt`, `status`) plus the course title it was issued against — the\n * learner name comes from the signed-in actor the shell handed us, and is\n * simply omitted when the shell did not supply one.\n */\nfunction CertificatePreview({\n certificate,\n learnerName,\n courseTitle,\n}: {\n certificate: TecCertificate;\n learnerName?: string;\n courseTitle: string;\n}) {\n const revoked = certificate.status === 'REVOKED';\n return (\n <GlassCard className=\"overflow-hidden p-0\">\n <div className=\"border-b border-border-seam px-5 py-3\">\n <span className=\"text-xs font-semibold uppercase tracking-wide text-text-muted\">\n Certificate preview\n </span>\n </div>\n <div className=\"space-y-4 bg-bg-sunken/50 px-5 py-6 text-center\">\n <div className=\"flex items-center justify-center gap-2 text-xs font-semibold uppercase tracking-[0.2em] text-text-muted\">\n <Award size={14} className=\"text-status-warning-text\" /> Certificate of completion\n </div>\n <h3 className=\"text-lg font-bold text-text-primary\">{courseTitle}</h3>\n {learnerName ? (\n <p className=\"text-sm text-text-secondary\">\n Awarded to <span className=\"font-semibold text-text-primary\">{learnerName}</span>\n </p>\n ) : null}\n <div className=\"grid grid-cols-1 gap-3 border-t border-border-seam pt-4 text-left sm:grid-cols-3\">\n <div>\n <div className=\"text-[11px] font-medium uppercase tracking-wide text-text-muted\">\n Serial number\n </div>\n <div className=\"truncate font-mono text-sm text-text-primary\">{certificate.serial}</div>\n </div>\n <div>\n <div className=\"text-[11px] font-medium uppercase tracking-wide text-text-muted\">\n Score\n </div>\n <div className=\"text-sm font-semibold text-text-primary\">{certificate.score}</div>\n </div>\n <div>\n <div className=\"text-[11px] font-medium uppercase tracking-wide text-text-muted\">\n Issued on\n </div>\n <div className=\"text-sm text-text-primary\">{formatDate(certificate.issuedAt)}</div>\n </div>\n </div>\n {revoked ? (\n <div className=\"rounded-lg border border-status-error-border bg-status-error-bg-subtle px-3 py-2 text-left text-xs text-status-error-text\">\n This certificate was revoked\n {certificate.revokedAt ? ` on ${formatDate(certificate.revokedAt)}` : ''}\n {certificate.revokeReason ? ` — ${certificate.revokeReason}` : '.'}\n </div>\n ) : (\n <span className=\"inline-flex items-center gap-1 rounded-full border border-status-success-border bg-status-success-bg-subtle px-2.5 py-1 text-xs font-medium text-status-success-text\">\n <ShieldCheck size={13} /> Verified\n </span>\n )}\n </div>\n </GlassCard>\n );\n}\n\nexport function CoursesCourseDetailsPagePage() {\n const [params] = useSearchParams();\n const id = params.get('id') ?? '';\n // The expanded lesson lives in `?lessonId=` so the assistant's page context\n // names it (\"explain this lesson\") and a reload reopens it.\n const [openLessonId, setOpenLessonId] = useFocusedRecordParam(FOCUS_PARAMS.lesson);\n const { currentUser, workspaceId } = useTechnoSpam();\n const navigate = useTechnoSpamNavigate();\n const { emit } = useTechnoSpamAnalytics();\n const { data, loading, error, refetch } = useCourseQuery(id);\n\n useEffect(() => {\n if (id) {\n emit('technospam.course.viewed', {\n workspaceId,\n entityId: id,\n entityType: 'TEC_COURSE',\n });\n }\n }, [id, workspaceId, emit]);\n\n const [enroll, { loading: enrolling }] = useEnroll();\n const course = data?.tecCourse ?? null;\n // Self-scoped read (BOFF-10481): the learner's own certificates come from the\n // canonical Buildmyiq field, which filters by the authenticated actor\n // server-side. It is narrowed to this course by its canonical id once known\n // and by its Technospam id on the client. A cross-learner list-all read is\n // not granted to a plain learner, so an already-claimed certificate would\n // come back empty and the page would keep offering \"Claim certificate\".\n const { certificates: myCertificates, refetch: refetchCerts } = useMyCertificatesQuery({\n buiCourseId: course?.buiCourseId,\n technospamCourseId: id,\n skip: !id,\n });\n const [issueCertificate, { loading: claiming }] = useIssueCertificate();\n\n const lessons = [...(course?.lessons ?? [])].sort((a, b) => a.position - b.position);\n const myEnrollment: TecEnrollment | undefined = (course?.enrollments ?? []).find(\n (e) => e.learnerId === currentUser?.id\n );\n const myCertificate = myCertificates.find((c) => c.status === 'ISSUED') ?? myCertificates[0];\n const isComplete = !!myEnrollment && myEnrollment.progressPct >= 100;\n\n const handleClaimCertificate = async () => {\n if (!id || !currentUser?.id) return;\n emit('technospam.course.certificate_claimed', {\n workspaceId,\n entityId: id,\n entityType: 'TEC_COURSE',\n });\n const res = await issueCertificate({\n variables: { courseId: id, learnerId: currentUser.id, score: 100 },\n });\n if (res.data?.tecIssueCertificate) {\n notify.success('Certificate issued', 'Your verifiable certificate is ready in My Learning.');\n await refetchCerts();\n }\n };\n\n const handleEnroll = async () => {\n if (!id) return;\n emit('technospam.course.enroll_clicked', {\n workspaceId,\n entityId: id,\n entityType: 'TEC_COURSE',\n });\n const res = await enroll({ variables: { input: { courseId: id } } });\n if (res.data?.tecEnroll) {\n emit('technospam.course.enrolled', {\n workspaceId,\n entityId: id,\n entityType: 'TEC_COURSE',\n });\n notify.success('Enrolled', `You're enrolled in \"${course?.title}\".`);\n await refetch();\n }\n };\n\n if (loading && !course) {\n return (\n <PageLayout title=\"Course\" backPath=\"/courses/courses-page\">\n <Skeleton className=\"mb-4 h-40 w-full rounded-2xl\" />\n <Skeleton className=\"h-64 w-full rounded-xl\" />\n </PageLayout>\n );\n }\n\n if (error || !course) {\n return (\n <PageLayout title=\"Course\" backPath=\"/courses/courses-page\">\n {error ? (\n <ErrorView title=\"Couldn't load course\" error={error} />\n ) : (\n <EmptyView\n title=\"Course not found\"\n description=\"This course may have been unpublished.\"\n />\n )}\n </PageLayout>\n );\n }\n\n const completedLessons = myEnrollment?.lessonsCompleted ?? 0;\n // BOFF-4107: \"Continue\" lands on the first lesson the learner has not\n // finished (per-lesson completions from the API), or the first lesson to\n // review once everything is done.\n const nextLesson = lessons.find((l) => !l.completedByMe) ?? lessons[0];\n const allDone = lessons.length > 0 && lessons.every((l) => l.completedByMe);\n const openLesson = (lessonId: string) => navigate(lessonPlayerPath(id, lessonId));\n const remaining = Math.max(0, lessons.length - completedLessons);\n const milestone =\n myEnrollment && myEnrollment.progressPct >= 100\n ? 'Course complete — claim your certificate below.'\n : remaining === 1\n ? '1 lesson to go — almost there!'\n : `${remaining} lessons to go — keep it up!`;\n const heroSeed = `${course.topic ?? ''} ${course.title}`;\n\n return (\n <PageLayout\n backPath=\"/courses/courses-page\"\n title={course.title}\n description={course.summary ?? undefined}\n actions={\n myEnrollment ? (\n nextLesson ? (\n <Button onClick={() => openLesson(nextLesson.id)} data-testid=\"continue-learning\">\n {allDone ? <CheckCircle2 size={16} /> : <ArrowRight size={16} />}\n {allDone\n ? 'Review course'\n : completedLessons > 0\n ? 'Continue learning'\n : 'Start course'}\n </Button>\n ) : null\n ) : (\n <Button onClick={handleEnroll} disabled={enrolling}>\n <BookOpen size={16} /> {enrolling ? 'Enrolling…' : 'Enroll'}\n </Button>\n )\n }\n >\n {/* Bold hero banner */}\n <MediaCover\n height=\"h-40\"\n seed={heroSeed}\n icon={topicIcon(course.topic ?? course.title)}\n className=\"mb-5 rounded-2xl\"\n >\n <div className=\"flex w-full flex-col gap-2\">\n <span className=\"text-xs font-medium uppercase tracking-wide text-white/80\">\n {course.topic ?? 'Course'}\n </span>\n <h2 className=\"text-2xl font-bold leading-tight text-white drop-shadow-sm sm:text-3xl\">\n {course.title}\n </h2>\n <div className=\"flex flex-wrap items-center gap-2\">\n <HeroBadge icon={TrendingUp}>{humanizeEnum(course.level)}</HeroBadge>\n <HeroBadge icon={CheckCircle2}>{humanizeEnum(course.status)}</HeroBadge>\n {course.topic ? <HeroBadge icon={Layers}>{course.topic}</HeroBadge> : null}\n {course.isPremiumOnly ? <HeroBadge icon={Lock}>Premium</HeroBadge> : null}\n </div>\n </div>\n </MediaCover>\n\n <TagStrip tags={course.tags} className=\"mb-5\" />\n\n <div className=\"mb-6 grid grid-cols-2 gap-3 sm:grid-cols-4\">\n <StatCard label=\"Lessons\" value={lessons.length} icon={FileText} tone=\"blue\" />\n <StatCard\n label=\"Duration\"\n value={formatDuration(course.estimatedMinutes)}\n icon={Clock}\n tone=\"teal\"\n />\n <StatCard\n label=\"Enrolled\"\n value={course.enrollments?.length ?? 0}\n icon={Users}\n tone=\"indigo\"\n />\n <StatCard\n label=\"Your progress\"\n value={myEnrollment ? `${myEnrollment.progressPct}%` : 'Not started'}\n icon={TrendingUp}\n tone=\"purple\"\n />\n </div>\n\n {myEnrollment ? (\n <GlassCard className=\"mb-6 p-5\">\n <div className=\"mb-3 flex items-center gap-2\">\n <span className=\"flex h-8 w-8 items-center justify-center rounded-lg bg-accent-primary/15 text-accent-primary\">\n <TrendingUp size={16} />\n </span>\n <span className=\"text-sm font-semibold text-text-primary\">Your progress</span>\n <span className=\"ml-auto text-sm text-text-secondary\">\n {completedLessons}/{lessons.length} lessons · {myEnrollment.progressPct}%\n </span>\n </div>\n <div className=\"h-2.5 w-full overflow-hidden rounded-full bg-bg-sunken\">\n <div\n className=\"h-full rounded-full bg-gradient-to-r from-accent-primary via-accent-indigo to-accent-purple transition-all duration-500\"\n style={{ width: `${myEnrollment.progressPct}%` }}\n />\n </div>\n <p className=\"mt-2 text-xs text-text-muted\">{milestone}</p>\n </GlassCard>\n ) : null}\n\n {myCertificate ? (\n <div className=\"mb-6 grid grid-cols-1 gap-4 lg:grid-cols-2\">\n <GlassCard\n className={cn(\n 'flex items-center gap-4 border p-5',\n myCertificate.status === 'REVOKED'\n ? 'border-status-error-border/60 bg-status-error-bg-subtle/20'\n : 'border-status-success-border/60 bg-status-success-bg-subtle/30'\n )}\n >\n <span className=\"flex h-14 w-14 shrink-0 items-center justify-center rounded-2xl bg-status-warning-bg-subtle text-status-warning-text ring-1 ring-status-warning-border/40\">\n <Trophy size={26} />\n </span>\n <div className=\"min-w-0\">\n <div className=\"text-base font-semibold text-text-primary\">\n {myCertificate.status === 'REVOKED'\n ? 'Your certificate was revoked'\n : 'You finished this course'}\n </div>\n <div className=\"text-sm text-text-muted\">\n {myCertificate.status === 'REVOKED'\n ? 'Contact a workspace admin if you believe this was a mistake.'\n : 'Your verifiable certificate of completion is issued — it also appears in My Learning.'}\n </div>\n </div>\n </GlassCard>\n <CertificatePreview\n certificate={myCertificate}\n learnerName={currentUser?.name ?? undefined}\n courseTitle={myCertificate.course?.title ?? course.title}\n />\n </div>\n ) : isComplete ? (\n <GlassCard className=\"mb-6 flex flex-col items-start gap-4 border border-status-success-border/60 bg-status-success-bg-subtle/40 p-5 sm:flex-row sm:items-center sm:justify-between\">\n <div className=\"flex items-center gap-4\">\n <span className=\"flex h-14 w-14 shrink-0 items-center justify-center rounded-2xl bg-status-warning-bg-subtle text-status-warning-text ring-1 ring-status-warning-border/40\">\n <Trophy size={26} />\n </span>\n <div>\n <div className=\"text-base font-semibold text-text-primary\">\n You finished this course\n </div>\n <div className=\"text-sm text-text-muted\">\n Claim your verifiable certificate of completion.\n </div>\n </div>\n </div>\n <Button onClick={handleClaimCertificate} disabled={claiming}>\n <Award size={16} /> {claiming ? 'Issuing…' : 'Claim certificate'}\n </Button>\n </GlassCard>\n ) : null}\n\n <GlassCard className=\"p-5\">\n <div className=\"mb-3 flex items-center justify-between gap-2\">\n <h2 className=\"flex items-center gap-2 text-base font-semibold text-text-primary\">\n <BookOpen size={18} className=\"text-accent-primary\" /> Curriculum\n {course.isPremiumOnly ? <PremiumBadge locked={course.isLocked} /> : null}\n </h2>\n {lessons.length ? (\n <span className=\"rounded-full bg-status-success-bg-subtle px-2.5 py-1 text-xs font-medium text-status-success-text\">\n {completedLessons}/{lessons.length} completed\n </span>\n ) : null}\n </div>\n {course.isLocked ? (\n <div className=\"mb-4\">\n <UpgradeGate\n compact\n description=\"This course is on a paid TechnoSpam plan. Preview lessons below stay free — the rest unlock once your workspace upgrades.\"\n />\n </div>\n ) : null}\n {lessons.length === 0 ? (\n <p className=\"py-6 text-center text-sm text-text-muted\">\n Lessons for this course are being prepared.\n </p>\n ) : (\n <div className=\"flex flex-col\">\n {lessons.map((lesson, i) => (\n <LessonRow\n key={lesson.id}\n lesson={lesson}\n index={i}\n done={!!lesson.completedByMe}\n open={openLessonId === lesson.id}\n onToggle={() => setOpenLessonId(openLessonId === lesson.id ? null : lesson.id)}\n onStart={() => openLesson(lesson.id)}\n />\n ))}\n </div>\n )}\n </GlassCard>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"CoursesCourseDetailsPagePage.js","names":[],"sources":["../../../src/technospam/pages/CoursesCourseDetailsPagePage.tsx"],"sourcesContent":["import type { ReactNode } from 'react';\nimport { useEffect } from 'react';\nimport { useSearchParams } from 'react-router';\nimport {\n ArrowRight,\n Award,\n BookOpen,\n CheckCircle2,\n ChevronDown,\n Clock,\n Eye,\n FileText,\n Lock,\n PlayCircle,\n Layers,\n ShieldCheck,\n Sparkles,\n Trophy,\n TrendingUp,\n Users,\n type LucideIcon,\n} from 'lucide-react';\nimport { Button, GlassCard, Skeleton } from '@burdenoff/fe-libs/ui';\nimport {\n EmptyView,\n ErrorView,\n MediaCover,\n PageLayout,\n PremiumBadge,\n StatCard,\n TagStrip,\n UpgradeGate,\n topicIcon,\n} from '../components';\nimport { useTechnoSpam, useTechnoSpamNavigate } from '../providers/TechnoSpamProvider';\nimport {\n useCourseQuery,\n useEnroll,\n useIssueCertificate,\n useMyCertificatesQuery,\n type TecCertificate,\n type TecLesson,\n type TecEnrollment,\n} from '../api/technospamApi';\nimport { formatDate, formatDuration, humanizeEnum } from '../utils/format';\nimport { notify } from '../lib/toast';\nimport { cn } from '../utils/cn';\nimport { useTechnoSpamAnalytics } from '../hooks/useTechnoSpamAnalytics';\nimport { useFocusedRecordParam } from '../hooks/useFocusedRecordParam';\nimport { FOCUS_PARAMS } from '../utils/pageContext';\nimport { lessonPlayerPath } from '../utils/lessonPath';\n\n/** Per-lesson-kind icon + on-theme tint (chip) and pill (badge) classes. */\nconst KIND_META: Record<string, { icon: LucideIcon; chip: string; badge: string }> = {\n VIDEO: {\n icon: PlayCircle,\n chip: 'bg-accent-primary/15 text-accent-primary',\n badge: 'border-accent-primary/25 bg-accent-primary/6 text-accent-primary',\n },\n ARTICLE: {\n icon: FileText,\n chip: 'bg-accent-indigo/15 text-accent-indigo',\n badge: 'border-accent-indigo/25 bg-accent-indigo/10 text-accent-indigo',\n },\n INTERACTIVE: {\n icon: Sparkles,\n chip: 'bg-accent-teal/15 text-accent-teal',\n badge: 'border-accent-teal/25 bg-accent-teal/10 text-accent-teal',\n },\n QUIZ: {\n icon: CheckCircle2,\n chip: 'bg-accent-purple/15 text-accent-purple',\n badge: 'border-accent-purple/25 bg-accent-purple/10 text-accent-purple',\n },\n PROJECT: {\n icon: Layers,\n chip: 'bg-status-warning-bg-subtle text-status-warning-text',\n badge: 'border-status-warning-border bg-status-warning-bg-subtle text-status-warning-text',\n },\n};\n\nfunction HeroBadge({ icon: Icon, children }: { icon?: LucideIcon; children: ReactNode }) {\n return (\n <span className=\"inline-flex items-center gap-1 rounded-full bg-white/15 px-2.5 py-1 text-xs font-medium text-white ring-1 ring-white/25 backdrop-blur-sm\">\n {Icon ? <Icon size={12} /> : null}\n {children}\n </span>\n );\n}\n\n/**\n * One curriculum row. Expands in place to reveal the lesson body.\n *\n * `TecLesson.content` is the only lesson body this service exposes to a\n * learner, and the API nulls it server-side whenever `isLocked` is true\n * (BOFF-4416), so the three states below are exhaustive and never guessed:\n * locked → upgrade note, content present → the body, content null on an\n * unlocked lesson → the author simply hasn't written it yet.\n */\nfunction LessonRow({\n lesson,\n index,\n done,\n open,\n onToggle,\n onStart,\n}: {\n lesson: TecLesson;\n index: number;\n done: boolean;\n /** The open lesson is the focused one (`?lessonId=`), so the assistant can name it. */\n open: boolean;\n onToggle: () => void;\n /** Opens the lesson in the lesson player (BOFF-4107). */\n onStart: () => void;\n}) {\n const meta = KIND_META[lesson.kind] ?? KIND_META.ARTICLE!;\n const Icon = meta.icon;\n const bodyId = `tec-lesson-body-${lesson.id}`;\n\n return (\n <div className=\"border-b border-border-seam last:border-b-0\">\n <button\n type=\"button\"\n onClick={onToggle}\n aria-expanded={open}\n aria-controls={bodyId}\n className=\"group flex w-full items-center gap-3 rounded-xl px-2 py-3 text-left transition-colors hover:bg-bg-sunken/60\"\n >\n <span className=\"hidden w-6 shrink-0 text-center text-sm font-medium tabular-nums text-text-muted sm:block\">\n {index + 1}\n </span>\n <span\n className={cn(\n 'flex h-9 w-9 shrink-0 items-center justify-center rounded-lg',\n done ? 'bg-status-success-bg-subtle text-status-success-text' : meta.chip\n )}\n >\n {done ? <CheckCircle2 size={17} /> : <Icon size={17} />}\n </span>\n <div className=\"min-w-0 flex-1\">\n <span className=\"block truncate text-sm font-medium text-text-primary\">\n {lesson.title}\n </span>\n <div className=\"mt-1 flex flex-wrap items-center gap-2\">\n <span\n className={cn(\n 'inline-flex items-center rounded-full border px-2 py-0.5 text-[11px] font-medium',\n meta.badge\n )}\n >\n {humanizeEnum(lesson.kind)}\n </span>\n <span className=\"inline-flex items-center gap-1 text-xs text-text-muted\">\n <Clock size={12} /> {formatDuration(lesson.estimatedMinutes)}\n </span>\n </div>\n </div>\n <div className=\"flex shrink-0 items-center gap-2\">\n {done ? (\n <CheckCircle2 size={18} className=\"text-status-success-text\" />\n ) : lesson.isPreview ? (\n <span className=\"inline-flex items-center gap-1 rounded-full border border-accent-primary/30 bg-accent-primary/6 px-2 py-0.5 text-[11px] font-medium text-accent-primary\">\n <Eye size={12} /> Preview\n </span>\n ) : lesson.isLocked ? (\n // BOFF-4416: distinct from the plain \"not completed yet\" Lock icon\n // below — this lesson's content is gated behind a paid plan, not\n // just not-yet-watched. The API already nulled `content`/`file`.\n <span className=\"inline-flex items-center gap-1 rounded-full border border-status-warning-border/60 bg-status-warning-bg-subtle px-2 py-0.5 text-[11px] font-medium text-status-warning-text\">\n <Lock size={12} /> Upgrade to unlock\n </span>\n ) : (\n <Lock size={15} className=\"text-text-muted\" />\n )}\n <ChevronDown\n size={16}\n aria-hidden\n className={cn(\n 'text-text-muted transition-transform duration-200',\n open ? 'rotate-180' : ''\n )}\n />\n </div>\n </button>\n\n {open ? (\n <div id={bodyId} className=\"px-2 pb-4 sm:pl-11\">\n {lesson.isLocked ? (\n <UpgradeGate\n compact\n description=\"This lesson is part of a paid TechnoSpam plan — its content unlocks once your workspace upgrades.\"\n />\n ) : (\n <div className=\"space-y-3\">\n {lesson.content ? (\n <div className=\"rounded-xl border border-border-seam bg-bg-sunken p-4\">\n <p className=\"line-clamp-6 whitespace-pre-wrap text-sm leading-relaxed text-text-secondary\">\n {lesson.content}\n </p>\n </div>\n ) : null}\n <Button size=\"sm\" onClick={onStart} data-testid={`open-lesson-${lesson.id}`}>\n <PlayCircle size={15} /> {done ? 'Review lesson' : 'Start lesson'}\n </Button>\n </div>\n )}\n </div>\n ) : null}\n </div>\n );\n}\n\n/**\n * Verifiable certificate face for an already-issued certificate. Every value\n * shown is a real field on the TecCertificate row (`serial`, `score`,\n * `issuedAt`, `status`) plus the course title it was issued against — the\n * learner name comes from the signed-in actor the shell handed us, and is\n * simply omitted when the shell did not supply one.\n */\nfunction CertificatePreview({\n certificate,\n learnerName,\n courseTitle,\n}: {\n certificate: TecCertificate;\n learnerName?: string;\n courseTitle: string;\n}) {\n const revoked = certificate.status === 'REVOKED';\n return (\n <GlassCard className=\"overflow-hidden p-0\">\n <div className=\"border-b border-border-seam px-5 py-3\">\n <span className=\"text-xs font-semibold uppercase tracking-wide text-text-muted\">\n Certificate preview\n </span>\n </div>\n <div className=\"space-y-4 bg-bg-sunken/50 px-5 py-6 text-center\">\n <div className=\"flex items-center justify-center gap-2 text-xs font-semibold uppercase tracking-[0.2em] text-text-muted\">\n <Award size={14} className=\"text-status-warning-text\" /> Certificate of completion\n </div>\n <h3 className=\"text-lg font-bold text-text-primary\">{courseTitle}</h3>\n {learnerName ? (\n <p className=\"text-sm text-text-secondary\">\n Awarded to <span className=\"font-semibold text-text-primary\">{learnerName}</span>\n </p>\n ) : null}\n <div className=\"grid grid-cols-1 gap-3 border-t border-border-seam pt-4 text-left sm:grid-cols-3\">\n <div>\n <div className=\"text-[11px] font-medium uppercase tracking-wide text-text-muted\">\n Serial number\n </div>\n <div className=\"truncate font-mono text-sm text-text-primary\">{certificate.serial}</div>\n </div>\n <div>\n <div className=\"text-[11px] font-medium uppercase tracking-wide text-text-muted\">\n Score\n </div>\n <div className=\"text-sm font-semibold text-text-primary\">{certificate.score}</div>\n </div>\n <div>\n <div className=\"text-[11px] font-medium uppercase tracking-wide text-text-muted\">\n Issued on\n </div>\n <div className=\"text-sm text-text-primary\">{formatDate(certificate.issuedAt)}</div>\n </div>\n </div>\n {revoked ? (\n <div className=\"rounded-lg border border-status-error-border bg-status-error-bg-subtle px-3 py-2 text-left text-xs text-status-error-text\">\n This certificate was revoked\n {certificate.revokedAt ? ` on ${formatDate(certificate.revokedAt)}` : ''}\n {certificate.revokeReason ? ` — ${certificate.revokeReason}` : '.'}\n </div>\n ) : (\n <span className=\"inline-flex items-center gap-1 rounded-full border border-status-success-border bg-status-success-bg-subtle px-2.5 py-1 text-xs font-medium text-status-success-text\">\n <ShieldCheck size={13} /> Verified\n </span>\n )}\n </div>\n </GlassCard>\n );\n}\n\nexport function CoursesCourseDetailsPagePage() {\n const [params] = useSearchParams();\n const id = params.get('id') ?? '';\n // The expanded lesson lives in `?lessonId=` so the assistant's page context\n // names it (\"explain this lesson\") and a reload reopens it.\n const [openLessonId, setOpenLessonId] = useFocusedRecordParam(FOCUS_PARAMS.lesson);\n const { currentUser, workspaceId } = useTechnoSpam();\n const navigate = useTechnoSpamNavigate();\n const { emit } = useTechnoSpamAnalytics();\n const { data, loading, error, refetch } = useCourseQuery(id);\n\n useEffect(() => {\n if (id) {\n emit('technospam.course.viewed', {\n workspaceId,\n entityId: id,\n entityType: 'TEC_COURSE',\n });\n }\n }, [id, workspaceId, emit]);\n\n const [enroll, { loading: enrolling }] = useEnroll();\n const course = data?.tecCourse ?? null;\n // Self-scoped read (BOFF-10481): the learner's own certificates come from the\n // canonical Buildmyiq field, which filters by the authenticated actor\n // server-side. It is narrowed to this course by its canonical id once known\n // and by its Technospam id on the client. A cross-learner list-all read is\n // not granted to a plain learner, so an already-claimed certificate would\n // come back empty and the page would keep offering \"Claim certificate\".\n const { certificates: myCertificates, refetch: refetchCerts } = useMyCertificatesQuery({\n buiCourseId: course?.buiCourseId,\n technospamCourseId: id,\n skip: !id,\n });\n const [issueCertificate, { loading: claiming }] = useIssueCertificate();\n\n const lessons = [...(course?.lessons ?? [])].sort((a, b) => a.position - b.position);\n const myEnrollment: TecEnrollment | undefined = (course?.enrollments ?? []).find(\n (e) => e.learnerId === currentUser?.id\n );\n const myCertificate = myCertificates.find((c) => c.status === 'ISSUED') ?? myCertificates[0];\n const isComplete = !!myEnrollment && myEnrollment.progressPct >= 100;\n\n const handleClaimCertificate = async () => {\n if (!id || !currentUser?.id) return;\n emit('technospam.course.certificate_claimed', {\n workspaceId,\n entityId: id,\n entityType: 'TEC_COURSE',\n });\n const res = await issueCertificate({\n variables: { courseId: id, learnerId: currentUser.id, score: 100 },\n });\n if (res.data?.tecIssueCertificate) {\n notify.success('Certificate issued', 'Your verifiable certificate is ready in My Learning.');\n await refetchCerts();\n }\n };\n\n const handleEnroll = async () => {\n if (!id) return;\n emit('technospam.course.enroll_clicked', {\n workspaceId,\n entityId: id,\n entityType: 'TEC_COURSE',\n });\n const res = await enroll({ variables: { input: { courseId: id } } });\n if (res.data?.tecEnroll) {\n emit('technospam.course.enrolled', {\n workspaceId,\n entityId: id,\n entityType: 'TEC_COURSE',\n });\n notify.success('Enrolled', `You're enrolled in \"${course?.title}\".`);\n await refetch();\n }\n };\n\n if (loading && !course) {\n return (\n <PageLayout title=\"Course\" backPath=\"/courses/courses-page\">\n <Skeleton className=\"mb-4 h-40 w-full rounded-2xl\" />\n <Skeleton className=\"h-64 w-full rounded-xl\" />\n </PageLayout>\n );\n }\n\n if (error || !course) {\n return (\n <PageLayout title=\"Course\" backPath=\"/courses/courses-page\">\n {error ? (\n <ErrorView title=\"Couldn't load course\" error={error} />\n ) : (\n <EmptyView\n title=\"Course not found\"\n description=\"This course may have been unpublished.\"\n />\n )}\n </PageLayout>\n );\n }\n\n const completedLessons = myEnrollment?.lessonsCompleted ?? 0;\n // BOFF-4107: \"Continue\" lands on the first lesson the learner has not\n // finished (per-lesson completions from the API), or the first lesson to\n // review once everything is done.\n const nextLesson = lessons.find((l) => !l.completedByMe) ?? lessons[0];\n const allDone = lessons.length > 0 && lessons.every((l) => l.completedByMe);\n const openLesson = (lessonId: string) => navigate(lessonPlayerPath(id, lessonId));\n const remaining = Math.max(0, lessons.length - completedLessons);\n const milestone =\n myEnrollment && myEnrollment.progressPct >= 100\n ? 'Course complete — claim your certificate below.'\n : remaining === 1\n ? '1 lesson to go — almost there!'\n : `${remaining} lessons to go — keep it up!`;\n const heroSeed = `${course.topic ?? ''} ${course.title}`;\n\n return (\n <PageLayout\n backPath=\"/courses/courses-page\"\n title={course.title}\n description={course.summary ?? undefined}\n actions={\n myEnrollment ? (\n nextLesson ? (\n <Button onClick={() => openLesson(nextLesson.id)} data-testid=\"continue-learning\">\n {allDone ? <CheckCircle2 size={16} /> : <ArrowRight size={16} />}\n {allDone\n ? 'Review course'\n : completedLessons > 0\n ? 'Continue learning'\n : 'Start course'}\n </Button>\n ) : null\n ) : (\n <Button onClick={handleEnroll} disabled={enrolling}>\n <BookOpen size={16} /> {enrolling ? 'Enrolling…' : 'Enroll'}\n </Button>\n )\n }\n >\n {/* Bold hero banner */}\n <MediaCover\n height=\"h-40\"\n seed={heroSeed}\n icon={topicIcon(course.topic ?? course.title)}\n className=\"mb-5 rounded-2xl\"\n >\n <div className=\"flex w-full flex-col gap-2\">\n <span className=\"text-xs font-medium uppercase tracking-wide text-white/80\">\n {course.topic ?? 'Course'}\n </span>\n <h2 className=\"text-2xl font-bold leading-tight text-white drop-shadow-sm sm:text-3xl\">\n {course.title}\n </h2>\n <div className=\"flex flex-wrap items-center gap-2\">\n <HeroBadge icon={TrendingUp}>{humanizeEnum(course.level)}</HeroBadge>\n <HeroBadge icon={CheckCircle2}>{humanizeEnum(course.status)}</HeroBadge>\n {course.topic ? <HeroBadge icon={Layers}>{course.topic}</HeroBadge> : null}\n {course.isPremiumOnly ? <HeroBadge icon={Lock}>Premium</HeroBadge> : null}\n </div>\n </div>\n </MediaCover>\n\n <TagStrip tags={course.tags} className=\"mb-5\" />\n\n <div className=\"mb-6 grid grid-cols-2 gap-3 sm:grid-cols-4\">\n <StatCard label=\"Lessons\" value={lessons.length} icon={FileText} tone=\"blue\" />\n <StatCard\n label=\"Duration\"\n value={formatDuration(course.estimatedMinutes)}\n icon={Clock}\n tone=\"teal\"\n />\n <StatCard\n label=\"Enrolled\"\n value={course.enrollments?.length ?? 0}\n icon={Users}\n tone=\"indigo\"\n />\n <StatCard\n label=\"Your progress\"\n value={myEnrollment ? `${myEnrollment.progressPct}%` : 'Not started'}\n icon={TrendingUp}\n tone=\"purple\"\n />\n </div>\n\n {myEnrollment ? (\n <GlassCard className=\"mb-6 p-5\">\n <div className=\"mb-3 flex items-center gap-2\">\n <span className=\"flex h-8 w-8 items-center justify-center rounded-lg bg-accent-primary/15 text-accent-primary\">\n <TrendingUp size={16} />\n </span>\n <span className=\"text-sm font-semibold text-text-primary\">Your progress</span>\n <span className=\"ml-auto text-sm text-text-secondary\">\n {completedLessons}/{lessons.length} lessons · {myEnrollment.progressPct}%\n </span>\n </div>\n <div className=\"h-2.5 w-full overflow-hidden rounded-full bg-bg-sunken\">\n <div\n className=\"h-full rounded-full bg-gradient-to-r from-accent-primary via-accent-indigo to-accent-purple transition-all duration-500\"\n style={{ width: `${myEnrollment.progressPct}%` }}\n />\n </div>\n <p className=\"mt-2 text-xs text-text-muted\">{milestone}</p>\n </GlassCard>\n ) : null}\n\n {myCertificate ? (\n <div className=\"mb-6 grid grid-cols-1 gap-4 lg:grid-cols-2\">\n <GlassCard\n className={cn(\n 'flex items-center gap-4 border p-5',\n myCertificate.status === 'REVOKED'\n ? 'border-status-error-border/60 bg-status-error-bg-subtle/20'\n : 'border-status-success-border/60 bg-status-success-bg-subtle/30'\n )}\n >\n <span className=\"flex h-14 w-14 shrink-0 items-center justify-center rounded-2xl bg-status-warning-bg-subtle text-status-warning-text ring-1 ring-status-warning-border/40\">\n <Trophy size={26} />\n </span>\n <div className=\"min-w-0\">\n <div className=\"text-base font-semibold text-text-primary\">\n {myCertificate.status === 'REVOKED'\n ? 'Your certificate was revoked'\n : 'You finished this course'}\n </div>\n <div className=\"text-sm text-text-muted\">\n {myCertificate.status === 'REVOKED'\n ? 'Contact a workspace admin if you believe this was a mistake.'\n : 'Your verifiable certificate of completion is issued — it also appears in My Learning.'}\n </div>\n </div>\n </GlassCard>\n <CertificatePreview\n certificate={myCertificate}\n learnerName={currentUser?.name ?? undefined}\n courseTitle={myCertificate.course?.title ?? course.title}\n />\n </div>\n ) : isComplete ? (\n <GlassCard className=\"mb-6 flex flex-col items-start gap-4 border border-status-success-border/60 bg-status-success-bg-subtle/40 p-5 sm:flex-row sm:items-center sm:justify-between\">\n <div className=\"flex items-center gap-4\">\n <span className=\"flex h-14 w-14 shrink-0 items-center justify-center rounded-2xl bg-status-warning-bg-subtle text-status-warning-text ring-1 ring-status-warning-border/40\">\n <Trophy size={26} />\n </span>\n <div>\n <div className=\"text-base font-semibold text-text-primary\">\n You finished this course\n </div>\n <div className=\"text-sm text-text-muted\">\n Claim your verifiable certificate of completion.\n </div>\n </div>\n </div>\n <Button onClick={handleClaimCertificate} disabled={claiming}>\n <Award size={16} /> {claiming ? 'Issuing…' : 'Claim certificate'}\n </Button>\n </GlassCard>\n ) : null}\n\n <GlassCard className=\"p-5\">\n <div className=\"mb-3 flex items-center justify-between gap-2\">\n <h2 className=\"flex items-center gap-2 text-base font-semibold text-text-primary\">\n <BookOpen size={18} className=\"text-accent-primary\" /> Curriculum\n {course.isPremiumOnly ? <PremiumBadge locked={course.isLocked} /> : null}\n </h2>\n {lessons.length ? (\n <span className=\"rounded-full bg-status-success-bg-subtle px-2.5 py-1 text-xs font-medium text-status-success-text\">\n {completedLessons}/{lessons.length} completed\n </span>\n ) : null}\n </div>\n {course.isLocked ? (\n <div className=\"mb-4\">\n <UpgradeGate\n compact\n description=\"This course is on a paid TechnoSpam plan. Preview lessons below stay free — the rest unlock once your workspace upgrades.\"\n />\n </div>\n ) : null}\n {lessons.length === 0 ? (\n <p className=\"py-6 text-center text-sm text-text-muted\">\n Lessons for this course are being prepared.\n </p>\n ) : (\n <div className=\"flex flex-col\">\n {lessons.map((lesson, i) => (\n <LessonRow\n key={lesson.id}\n lesson={lesson}\n index={i}\n done={!!lesson.completedByMe}\n open={openLessonId === lesson.id}\n onToggle={() => setOpenLessonId(openLessonId === lesson.id ? null : lesson.id)}\n onStart={() => openLesson(lesson.id)}\n />\n ))}\n </div>\n )}\n </GlassCard>\n </PageLayout>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAqDA,IAAM,IAA+E;CACnF,OAAO;EACL,MAAM;EACN,MAAM;EACN,OAAO;EACR;CACD,SAAS;EACP,MAAM;EACN,MAAM;EACN,OAAO;EACR;CACD,aAAa;EACX,MAAM;EACN,MAAM;EACN,OAAO;EACR;CACD,MAAM;EACJ,MAAM;EACN,MAAM;EACN,OAAO;EACR;CACD,SAAS;EACP,MAAM;EACN,MAAM;EACN,OAAO;EACR;CACF;AAED,SAAS,EAAU,EAAE,MAAM,GAAM,eAAwD;AACvF,QACE,kBAAC,QAAD;EAAM,WAAU;YAAhB,CACG,IAAO,kBAAC,GAAD,EAAM,MAAM,IAAM,CAAA,GAAG,MAC5B,EACI;;;AAaX,SAAS,GAAU,EACjB,WACA,UACA,SACA,SACA,aACA,cAUC;CACD,IAAM,IAAO,EAAU,EAAO,SAAS,EAAU,SAC3C,IAAO,EAAK,MACZ,IAAS,mBAAmB,EAAO;AAEzC,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,UAAD;GACE,MAAK;GACL,SAAS;GACT,iBAAe;GACf,iBAAe;GACf,WAAU;aALZ;IAOE,kBAAC,QAAD;KAAM,WAAU;eACb,IAAQ;KACJ,CAAA;IACP,kBAAC,QAAD;KACE,WAAW,EACT,gEACA,IAAO,yDAAyD,EAAK,KACtE;eAEO,EAAP,IAAQ,IAA6B,GAA9B,EAAc,MAAM,IAAM,CAAqB;KAClD,CAAA;IACP,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,QAAD;MAAM,WAAU;gBACb,EAAO;MACH,CAAA,EACP,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,QAAD;OACE,WAAW,EACT,oFACA,EAAK,MACN;iBAEA,EAAa,EAAO,KAAK;OACrB,CAAA,EACP,kBAAC,QAAD;OAAM,WAAU;iBAAhB;QACE,kBAAC,GAAD,EAAO,MAAM,IAAM,CAAA;;QAAE,EAAe,EAAO,iBAAiB;QACvD;SACH;QACF;;IACN,kBAAC,OAAD;KAAK,WAAU;eAAf,CACG,IACC,kBAAC,GAAD;MAAc,MAAM;MAAI,WAAU;MAA6B,CAAA,GAC7D,EAAO,YACT,kBAAC,QAAD;MAAM,WAAU;gBAAhB,CACE,kBAAC,GAAD,EAAK,MAAM,IAAM,CAAA,EAAA,WACZ;UACL,EAAO,WAIT,kBAAC,QAAD;MAAM,WAAU;gBAAhB,CACE,kBAAC,GAAD,EAAM,MAAM,IAAM,CAAA,EAAA,qBACb;UAEP,kBAAC,GAAD;MAAM,MAAM;MAAI,WAAU;MAAoB,CAAA,EAEhD,kBAAC,GAAD;MACE,MAAM;MACN,eAAA;MACA,WAAW,EACT,qDACA,IAAO,eAAe,GACvB;MACD,CAAA,CACE;;IACC;MAER,IACC,kBAAC,OAAD;GAAK,IAAI;GAAQ,WAAU;aACxB,EAAO,WACN,kBAAC,GAAD;IACE,SAAA;IACA,aAAY;IACZ,CAAA,GAEF,kBAAC,OAAD;IAAK,WAAU;cAAf,CACG,EAAO,UACN,kBAAC,OAAD;KAAK,WAAU;eACb,kBAAC,KAAD;MAAG,WAAU;gBACV,EAAO;MACN,CAAA;KACA,CAAA,GACJ,MACJ,kBAAC,GAAD;KAAQ,MAAK;KAAK,SAAS;KAAS,eAAa,eAAe,EAAO;eAAvE;MACE,kBAAC,GAAD,EAAY,MAAM,IAAM,CAAA;;MAAE,IAAO,kBAAkB;MAC5C;OACL;;GAEJ,CAAA,GACJ,KACA;;;AAWV,SAAS,GAAmB,EAC1B,gBACA,gBACA,kBAKC;CACD,IAAM,IAAU,EAAY,WAAW;AACvC,QACE,kBAAC,GAAD;EAAW,WAAU;YAArB,CACE,kBAAC,OAAD;GAAK,WAAU;aACb,kBAAC,QAAD;IAAM,WAAU;cAAgE;IAEzE,CAAA;GACH,CAAA,EACN,kBAAC,OAAD;GAAK,WAAU;aAAf;IACE,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,GAAD;MAAO,MAAM;MAAI,WAAU;MAA6B,CAAA,EAAA,6BACpD;;IACN,kBAAC,MAAD;KAAI,WAAU;eAAuC;KAAiB,CAAA;IACrE,IACC,kBAAC,KAAD;KAAG,WAAU;eAAb,CAA2C,eAC9B,kBAAC,QAAD;MAAM,WAAU;gBAAmC;MAAmB,CAAA,CAC/E;SACF;IACJ,kBAAC,OAAD;KAAK,WAAU;eAAf;MACE,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,OAAD;OAAK,WAAU;iBAAkE;OAE3E,CAAA,EACN,kBAAC,OAAD;OAAK,WAAU;iBAAgD,EAAY;OAAa,CAAA,CACpF,EAAA,CAAA;MACN,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,OAAD;OAAK,WAAU;iBAAkE;OAE3E,CAAA,EACN,kBAAC,OAAD;OAAK,WAAU;iBAA2C,EAAY;OAAY,CAAA,CAC9E,EAAA,CAAA;MACN,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,OAAD;OAAK,WAAU;iBAAkE;OAE3E,CAAA,EACN,kBAAC,OAAD;OAAK,WAAU;iBAA6B,EAAW,EAAY,SAAS;OAAO,CAAA,CAC/E,EAAA,CAAA;MACF;;IACL,IACC,kBAAC,OAAD;KAAK,WAAU;eAAf;MAA2I;MAExI,EAAY,YAAY,OAAO,EAAW,EAAY,UAAU,KAAK;MACrE,EAAY,eAAe,MAAM,EAAY,iBAAiB;MAC3D;SAEN,kBAAC,QAAD;KAAM,WAAU;eAAhB,CACE,kBAAC,GAAD,EAAa,MAAM,IAAM,CAAA,EAAA,YACpB;;IAEL;KACI;;;AAIhB,SAAgB,IAA+B;CAC7C,IAAM,CAAC,KAAU,IAAiB,EAC5B,IAAK,EAAO,IAAI,KAAK,IAAI,IAGzB,CAAC,GAAc,KAAmB,EAAsB,GAAa,OAAO,EAC5E,EAAE,gBAAa,mBAAgB,GAAe,EAC9C,IAAW,GAAuB,EAClC,EAAE,YAAS,GAAwB,EACnC,EAAE,SAAM,YAAS,UAAO,gBAAY,GAAe,EAAG;AAE5D,UAAgB;AACd,EAAI,KACF,EAAK,4BAA4B;GAC/B;GACA,UAAU;GACV,YAAY;GACb,CAAC;IAEH;EAAC;EAAI;EAAa;EAAK,CAAC;CAE3B,IAAM,CAAC,IAAQ,EAAE,SAAS,OAAe,IAAW,EAC9C,IAAS,GAAM,aAAa,MAO5B,EAAE,cAAc,GAAgB,SAAS,OAAiB,GAAuB;EACrF,aAAa,GAAQ;EACrB,oBAAoB;EACpB,MAAM,CAAC;EACR,CAAC,EACI,CAAC,IAAkB,EAAE,SAAS,OAAc,IAAqB,EAEjE,IAAU,CAAC,GAAI,GAAQ,WAAW,EAAE,CAAE,CAAC,MAAM,GAAG,MAAM,EAAE,WAAW,EAAE,SAAS,EAC9E,KAA2C,GAAQ,eAAe,EAAE,EAAE,MACzE,MAAM,EAAE,cAAc,GAAa,GACrC,EACK,IAAgB,EAAe,MAAM,MAAM,EAAE,WAAW,SAAS,IAAI,EAAe,IACpF,KAAa,CAAC,CAAC,KAAgB,EAAa,eAAe,KAE3D,KAAyB,YAAY;AACrC,GAAC,KAAM,CAAC,GAAa,OACzB,EAAK,yCAAyC;GAC5C;GACA,UAAU;GACV,YAAY;GACb,CAAC,GACU,MAAM,GAAiB,EACjC,WAAW;GAAE,UAAU;GAAI,WAAW,EAAY;GAAI,OAAO;GAAK,EACnE,CAAC,EACM,MAAM,wBACZ,EAAO,QAAQ,sBAAsB,uDAAuD,EAC5F,MAAM,IAAc;IAIlB,KAAe,YAAY;AAC1B,QACL,EAAK,oCAAoC;GACvC;GACA,UAAU;GACV,YAAY;GACb,CAAC,GACU,MAAM,GAAO,EAAE,WAAW,EAAE,OAAO,EAAE,UAAU,GAAI,EAAE,EAAE,CAAC,EAC5D,MAAM,cACZ,EAAK,8BAA8B;GACjC;GACA,UAAU;GACV,YAAY;GACb,CAAC,EACF,EAAO,QAAQ,YAAY,uBAAuB,GAAQ,MAAM,IAAI,EACpE,MAAM,IAAS;;AAInB,KAAI,KAAW,CAAC,EACd,QACE,kBAAC,GAAD;EAAY,OAAM;EAAS,UAAS;YAApC,CACE,kBAAC,GAAD,EAAU,WAAU,gCAAiC,CAAA,EACrD,kBAAC,GAAD,EAAU,WAAU,0BAA2B,CAAA,CACpC;;AAIjB,KAAI,KAAS,CAAC,EACZ,QACE,kBAAC,GAAD;EAAY,OAAM;EAAS,UAAS;YACjC,IACC,kBAAC,IAAD;GAAW,OAAM;GAA8B;GAAS,CAAA,GAExD,kBAAC,IAAD;GACE,OAAM;GACN,aAAY;GACZ,CAAA;EAEO,CAAA;CAIjB,IAAM,IAAmB,GAAc,oBAAoB,GAIrD,IAAa,EAAQ,MAAM,MAAM,CAAC,EAAE,cAAc,IAAI,EAAQ,IAC9D,IAAU,EAAQ,SAAS,KAAK,EAAQ,OAAO,MAAM,EAAE,cAAc,EACrE,KAAc,MAAqB,EAAS,GAAiB,GAAI,EAAS,CAAC,EAC3E,IAAY,KAAK,IAAI,GAAG,EAAQ,SAAS,EAAiB,EAC1D,KACJ,KAAgB,EAAa,eAAe,MACxC,oDACA,MAAc,IACZ,mCACA,GAAG,EAAU,+BACf,KAAW,GAAG,EAAO,SAAS,GAAG,GAAG,EAAO;AAEjD,QACE,kBAAC,GAAD;EACE,UAAS;EACT,OAAO,EAAO;EACd,aAAa,EAAO,WAAW,KAAA;EAC/B,SACE,IACE,IACE,kBAAC,GAAD;GAAQ,eAAe,EAAW,EAAW,GAAG;GAAE,eAAY;aAA9D,CACa,EAAV,IAAW,IAA6B,GAA9B,EAAc,MAAM,IAAM,CAA2B,EAC/D,IACG,kBACA,IAAmB,IACjB,sBACA,eACC;OACP,OAEJ,kBAAC,GAAD;GAAQ,SAAS;GAAc,UAAU;aAAzC;IACE,kBAAC,GAAD,EAAU,MAAM,IAAM,CAAA;;IAAE,IAAY,eAAe;IAC5C;;YAnBf;GAwBE,kBAAC,GAAD;IACE,QAAO;IACP,MAAM;IACN,MAAM,EAAU,EAAO,SAAS,EAAO,MAAM;IAC7C,WAAU;cAEV,kBAAC,OAAD;KAAK,WAAU;eAAf;MACE,kBAAC,QAAD;OAAM,WAAU;iBACb,EAAO,SAAS;OACZ,CAAA;MACP,kBAAC,MAAD;OAAI,WAAU;iBACX,EAAO;OACL,CAAA;MACL,kBAAC,OAAD;OAAK,WAAU;iBAAf;QACE,kBAAC,GAAD;SAAW,MAAM;mBAAa,EAAa,EAAO,MAAM;SAAa,CAAA;QACrE,kBAAC,GAAD;SAAW,MAAM;mBAAe,EAAa,EAAO,OAAO;SAAa,CAAA;QACvE,EAAO,QAAQ,kBAAC,GAAD;SAAW,MAAM;mBAAS,EAAO;SAAkB,CAAA,GAAG;QACrE,EAAO,gBAAgB,kBAAC,GAAD;SAAW,MAAM;mBAAM;SAAmB,CAAA,GAAG;QACjE;;MACF;;IACK,CAAA;GAEb,kBAAC,IAAD;IAAU,MAAM,EAAO;IAAM,WAAU;IAAS,CAAA;GAEhD,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,GAAD;MAAU,OAAM;MAAU,OAAO,EAAQ;MAAQ,MAAM;MAAU,MAAK;MAAS,CAAA;KAC/E,kBAAC,GAAD;MACE,OAAM;MACN,OAAO,EAAe,EAAO,iBAAiB;MAC9C,MAAM;MACN,MAAK;MACL,CAAA;KACF,kBAAC,GAAD;MACE,OAAM;MACN,OAAO,EAAO,aAAa,UAAU;MACrC,MAAM;MACN,MAAK;MACL,CAAA;KACF,kBAAC,GAAD;MACE,OAAM;MACN,OAAO,IAAe,GAAG,EAAa,YAAY,KAAK;MACvD,MAAM;MACN,MAAK;MACL,CAAA;KACE;;GAEL,IACC,kBAAC,GAAD;IAAW,WAAU;cAArB;KACE,kBAAC,OAAD;MAAK,WAAU;gBAAf;OACE,kBAAC,QAAD;QAAM,WAAU;kBACd,kBAAC,GAAD,EAAY,MAAM,IAAM,CAAA;QACnB,CAAA;OACP,kBAAC,QAAD;QAAM,WAAU;kBAA0C;QAAoB,CAAA;OAC9E,kBAAC,QAAD;QAAM,WAAU;kBAAhB;SACG;SAAiB;SAAE,EAAQ;SAAO;SAAY,EAAa;SAAY;SACnE;;OACH;;KACN,kBAAC,OAAD;MAAK,WAAU;gBACb,kBAAC,OAAD;OACE,WAAU;OACV,OAAO,EAAE,OAAO,GAAG,EAAa,YAAY,IAAI;OAChD,CAAA;MACE,CAAA;KACN,kBAAC,KAAD;MAAG,WAAU;gBAAgC;MAAc,CAAA;KACjD;QACV;GAEH,IACC,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,GAAD;KACE,WAAW,EACT,sCACA,EAAc,WAAW,YACrB,+DACA,iEACL;eANH,CAQE,kBAAC,QAAD;MAAM,WAAU;gBACd,kBAAC,GAAD,EAAQ,MAAM,IAAM,CAAA;MACf,CAAA,EACP,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,OAAD;OAAK,WAAU;iBACZ,EAAc,WAAW,YACtB,iCACA;OACA,CAAA,EACN,kBAAC,OAAD;OAAK,WAAU;iBACZ,EAAc,WAAW,YACtB,iEACA;OACA,CAAA,CACF;QACI;QACZ,kBAAC,IAAD;KACE,aAAa;KACb,aAAa,GAAa,QAAQ,KAAA;KAClC,aAAa,EAAc,QAAQ,SAAS,EAAO;KACnD,CAAA,CACE;QACJ,KACF,kBAAC,GAAD;IAAW,WAAU;cAArB,CACE,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,QAAD;MAAM,WAAU;gBACd,kBAAC,GAAD,EAAQ,MAAM,IAAM,CAAA;MACf,CAAA,EACP,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,OAAD;MAAK,WAAU;gBAA4C;MAErD,CAAA,EACN,kBAAC,OAAD;MAAK,WAAU;gBAA0B;MAEnC,CAAA,CACF,EAAA,CAAA,CACF;QACN,kBAAC,GAAD;KAAQ,SAAS;KAAwB,UAAU;eAAnD;MACE,kBAAC,GAAD,EAAO,MAAM,IAAM,CAAA;;MAAE,IAAW,aAAa;MACtC;OACC;QACV;GAEJ,kBAAC,GAAD;IAAW,WAAU;cAArB;KACE,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,MAAD;OAAI,WAAU;iBAAd;QACE,kBAAC,GAAD;SAAU,MAAM;SAAI,WAAU;SAAwB,CAAA;;QACrD,EAAO,gBAAgB,kBAAC,GAAD,EAAc,QAAQ,EAAO,UAAY,CAAA,GAAG;QACjE;UACJ,EAAQ,SACP,kBAAC,QAAD;OAAM,WAAU;iBAAhB;QACG;QAAiB;QAAE,EAAQ;QAAO;QAC9B;WACL,KACA;;KACL,EAAO,WACN,kBAAC,OAAD;MAAK,WAAU;gBACb,kBAAC,GAAD;OACE,SAAA;OACA,aAAY;OACZ,CAAA;MACE,CAAA,GACJ;KACH,EAAQ,WAAW,IAClB,kBAAC,KAAD;MAAG,WAAU;gBAA2C;MAEpD,CAAA,GAEJ,kBAAC,OAAD;MAAK,WAAU;gBACZ,EAAQ,KAAK,GAAQ,MACpB,kBAAC,IAAD;OAEU;OACR,OAAO;OACP,MAAM,CAAC,CAAC,EAAO;OACf,MAAM,MAAiB,EAAO;OAC9B,gBAAgB,EAAgB,MAAiB,EAAO,KAAK,OAAO,EAAO,GAAG;OAC9E,eAAe,EAAW,EAAO,GAAG;OACpC,EAPK,EAAO,GAOZ,CACF;MACE,CAAA;KAEE;;GACD"}
@@ -1 +1 @@
1
- {"version":3,"file":"CoursesCoursesPagePage.d.ts","sourceRoot":"","sources":["../../../src/technospam/pages/CoursesCoursesPagePage.tsx"],"names":[],"mappings":"AAqFA,wBAAgB,sBAAsB,gCAqLrC"}
1
+ {"version":3,"file":"CoursesCoursesPagePage.d.ts","sourceRoot":"","sources":["../../../src/technospam/pages/CoursesCoursesPagePage.tsx"],"names":[],"mappings":"AAmFA,wBAAgB,sBAAsB,gCAqLrC"}
@@ -6,65 +6,68 @@ import { StatCard as a } from "../components/StatCard.js";
6
6
  import { MediaCover as o, topicIcon as s } from "../components/MediaCover.js";
7
7
  import { formatDuration as c } from "../utils/format.js";
8
8
  import { StatusBadge as l } from "../components/StatusBadge.js";
9
- import { LoadMore as u } from "../components/LoadMore.js";
10
- import { EmptyView as d, ErrorView as f, LoadingGrid as p } from "../components/StateViews.js";
11
- import { PremiumBadge as m } from "../components/UpgradeGate.js";
9
+ import { PremiumBadge as u } from "../components/UpgradeGate.js";
10
+ import { LoadMore as d } from "../components/LoadMore.js";
11
+ import { EmptyView as f, ErrorView as p, LoadingGrid as m } from "../components/StateViews.js";
12
12
  import { pageArg as h, useCoursesQuery as g, usePageLimit as _ } from "../api/technospamApi.js";
13
+ import { CardOpenButton as v } from "../components/CardOpenButton.js";
13
14
  import "../components/index.js";
14
- import { TAB_PANEL_FOCUS_CLASS as v } from "../lib/a11y.js";
15
- import { useMemo as y, useState as b } from "react";
16
- import { BookOpen as x, Clock as S, GraduationCap as C, Layers as w, Search as T, Sparkles as E } from "lucide-react";
17
- import { jsx as D, jsxs as O } from "react/jsx-runtime";
18
- import { Badge as k, Button as A, GlassCard as j, Input as M, Tabs as N, TabsContent as P, TabsList as F, TabsTrigger as I } from "@burdenoff/fe-libs/ui";
15
+ import { TAB_PANEL_FOCUS_CLASS as y } from "../lib/a11y.js";
16
+ import { useMemo as b, useState as x } from "react";
17
+ import { BookOpen as S, Clock as C, GraduationCap as w, Layers as T, Search as E, Sparkles as D } from "lucide-react";
18
+ import { jsx as O, jsxs as k } from "react/jsx-runtime";
19
+ import { Badge as A, Button as j, GlassCard as M, Input as N, Tabs as P, TabsContent as F, TabsList as I, TabsTrigger as L } from "@burdenoff/fe-libs/ui";
19
20
  //#region src/technospam/pages/CoursesCoursesPagePage.tsx
20
- function L({ course: e, onOpen: t }) {
21
- return /* @__PURE__ */ O(j, {
21
+ function R({ course: e, onOpen: t }) {
22
+ return /* @__PURE__ */ k(M, {
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__ */ D(o, {
24
+ children: [/* @__PURE__ */ O(o, {
25
25
  seed: `${e.topic ?? ""} ${e.title}`,
26
26
  icon: s(e.topic ?? e.title),
27
- children: /* @__PURE__ */ O("div", {
27
+ children: /* @__PURE__ */ k("div", {
28
28
  className: "flex flex-wrap items-center gap-1.5",
29
- children: [/* @__PURE__ */ D(l, {
29
+ children: [/* @__PURE__ */ O(l, {
30
30
  status: e.level,
31
31
  kind: "level"
32
- }), e.isPremiumOnly ? /* @__PURE__ */ D(m, { locked: e.isLocked }) : null]
32
+ }), e.isPremiumOnly ? /* @__PURE__ */ O(u, { locked: e.isLocked }) : null]
33
33
  })
34
- }), /* @__PURE__ */ O("div", {
34
+ }), /* @__PURE__ */ k("div", {
35
35
  className: "flex flex-1 flex-col gap-3 p-4",
36
36
  children: [
37
- /* @__PURE__ */ O("div", {
37
+ /* @__PURE__ */ k("div", {
38
38
  className: "min-w-0",
39
- children: [/* @__PURE__ */ D("h3", {
39
+ children: [/* @__PURE__ */ O("h3", {
40
40
  className: "truncate text-base font-semibold text-text-primary group-hover:text-accent-primary",
41
- children: e.title
42
- }), e.summary ? /* @__PURE__ */ D("p", {
41
+ children: /* @__PURE__ */ O(v, {
42
+ onOpen: t,
43
+ children: e.title
44
+ })
45
+ }), e.summary ? /* @__PURE__ */ O("p", {
43
46
  className: "mt-1 line-clamp-2 text-sm text-text-secondary",
44
47
  children: e.summary
45
48
  }) : null]
46
49
  }),
47
- /* @__PURE__ */ O("div", {
50
+ /* @__PURE__ */ k("div", {
48
51
  className: "mt-auto flex flex-wrap items-center gap-3 text-xs text-text-muted",
49
- children: [e.topic ? /* @__PURE__ */ O("span", {
52
+ children: [e.topic ? /* @__PURE__ */ k("span", {
50
53
  className: "inline-flex items-center gap-1",
51
54
  children: [
52
- /* @__PURE__ */ D(w, { size: 13 }),
55
+ /* @__PURE__ */ O(T, { size: 13 }),
53
56
  " ",
54
57
  e.topic
55
58
  ]
56
- }) : null, /* @__PURE__ */ O("span", {
59
+ }) : null, /* @__PURE__ */ k("span", {
57
60
  className: "inline-flex items-center gap-1",
58
61
  children: [
59
- /* @__PURE__ */ D(S, { size: 13 }),
62
+ /* @__PURE__ */ O(C, { size: 13 }),
60
63
  " ",
61
64
  c(e.estimatedMinutes)
62
65
  ]
63
66
  })]
64
67
  }),
65
- e.tags.length ? /* @__PURE__ */ D("div", {
68
+ e.tags.length ? /* @__PURE__ */ O("div", {
66
69
  className: "flex flex-wrap gap-1.5",
67
- children: e.tags.slice(0, 3).map((e) => /* @__PURE__ */ D(k, {
70
+ children: e.tags.slice(0, 3).map((e) => /* @__PURE__ */ O(A, {
68
71
  variant: "secondary",
69
72
  className: "text-[11px]",
70
73
  children: e
@@ -74,99 +77,99 @@ function L({ course: e, onOpen: t }) {
74
77
  })]
75
78
  });
76
79
  }
77
- function R() {
78
- let o = t(), { workspaceId: s } = e(), { emit: l } = n(), m = r(), [w, k] = b("ALL"), [j, R] = b(""), z = y(() => [
80
+ function z() {
81
+ let o = t(), { workspaceId: s } = e(), { emit: l } = n(), u = r(), [v, T] = x("ALL"), [A, M] = x(""), z = b(() => [
79
82
  {
80
83
  value: "ALL",
81
- label: m("technospam.courses.filterAllLevels", "All levels")
84
+ label: u("technospam.courses.filterAllLevels", "All levels")
82
85
  },
83
86
  {
84
87
  value: "BEGINNER",
85
- label: m("technospam.courses.filterBeginner", "Beginner")
88
+ label: u("technospam.courses.filterBeginner", "Beginner")
86
89
  },
87
90
  {
88
91
  value: "INTERMEDIATE",
89
- label: m("technospam.courses.filterIntermediate", "Intermediate")
92
+ label: u("technospam.courses.filterIntermediate", "Intermediate")
90
93
  },
91
94
  {
92
95
  value: "ADVANCED",
93
- label: m("technospam.courses.filterAdvanced", "Advanced")
96
+ label: u("technospam.courses.filterAdvanced", "Advanced")
94
97
  },
95
98
  {
96
99
  value: "EXPERT",
97
- label: m("technospam.courses.filterExpert", "Expert")
100
+ label: u("technospam.courses.filterExpert", "Expert")
98
101
  }
99
- ], [m]), { limit: B, more: V } = _(), { data: H, loading: U, error: W } = g({
102
+ ], [u]), { limit: B, more: V } = _(), { data: H, loading: U, error: W } = g({
100
103
  status: "PUBLISHED",
101
104
  pagination: h(B),
102
- ...w === "ALL" ? {} : { level: w }
103
- }), G = H?.tecListCourses?.items ?? [], K = H?.tecListCourses?.pageInfo?.hasMore ?? !1, q = y(() => {
104
- let e = j.trim().toLowerCase();
105
+ ...v === "ALL" ? {} : { level: v }
106
+ }), G = H?.tecListCourses?.items ?? [], K = H?.tecListCourses?.pageInfo?.hasMore ?? !1, q = b(() => {
107
+ let e = A.trim().toLowerCase();
105
108
  return e ? G.filter((t) => t.title.toLowerCase().includes(e) || (t.topic ?? "").toLowerCase().includes(e) || t.tags.some((t) => t.toLowerCase().includes(e))) : G;
106
- }, [G, j]);
107
- return /* @__PURE__ */ O(i, {
109
+ }, [G, A]);
110
+ return /* @__PURE__ */ k(i, {
108
111
  hero: !0,
109
- title: m("technospam.courses.title", "Course Catalog"),
110
- description: m("technospam.courses.description", "Learn technology by doing — practical, project-based courses across the stack."),
111
- actions: /* @__PURE__ */ O(A, {
112
+ title: u("technospam.courses.title", "Course Catalog"),
113
+ description: u("technospam.courses.description", "Learn technology by doing — practical, project-based courses across the stack."),
114
+ actions: /* @__PURE__ */ k(j, {
112
115
  variant: "outline",
113
116
  onClick: () => o("/courses/my-learning-page"),
114
117
  children: [
115
- /* @__PURE__ */ D(C, { size: 16 }),
118
+ /* @__PURE__ */ O(w, { size: 16 }),
116
119
  " ",
117
- m("technospam.common.myLearning", "My Learning")
120
+ u("technospam.common.myLearning", "My Learning")
118
121
  ]
119
122
  }),
120
- children: [/* @__PURE__ */ O("div", {
123
+ children: [/* @__PURE__ */ k("div", {
121
124
  className: "mb-5 grid grid-cols-2 gap-3 sm:grid-cols-4",
122
125
  children: [
123
- /* @__PURE__ */ D(a, {
124
- label: m("technospam.courses.statCourses", "Courses"),
126
+ /* @__PURE__ */ O(a, {
127
+ label: u("technospam.courses.statCourses", "Courses"),
125
128
  value: G.length,
126
- icon: x,
129
+ icon: S,
127
130
  tone: "blue"
128
131
  }),
129
- /* @__PURE__ */ D(a, {
130
- label: m("technospam.courses.statBeginnerFriendly", "Beginner-friendly"),
132
+ /* @__PURE__ */ O(a, {
133
+ label: u("technospam.courses.statBeginnerFriendly", "Beginner-friendly"),
131
134
  value: G.filter((e) => e.level === "BEGINNER").length,
132
- icon: E,
135
+ icon: D,
133
136
  tone: "green"
134
137
  }),
135
- /* @__PURE__ */ D(a, {
136
- label: m("technospam.courses.statAdvancedPlus", "Advanced+"),
138
+ /* @__PURE__ */ O(a, {
139
+ label: u("technospam.courses.statAdvancedPlus", "Advanced+"),
137
140
  value: G.filter((e) => e.level === "ADVANCED" || e.level === "EXPERT").length,
138
- icon: C,
141
+ icon: w,
139
142
  tone: "purple"
140
143
  }),
141
- /* @__PURE__ */ D(a, {
142
- label: m("technospam.courses.statTotalContent", "Total content"),
144
+ /* @__PURE__ */ O(a, {
145
+ label: u("technospam.courses.statTotalContent", "Total content"),
143
146
  value: c(G.reduce((e, t) => e + t.estimatedMinutes, 0)),
144
- icon: S,
147
+ icon: C,
145
148
  tone: "teal"
146
149
  })
147
150
  ]
148
- }), /* @__PURE__ */ O(N, {
151
+ }), /* @__PURE__ */ k(P, {
149
152
  className: "gap-0",
150
- value: w,
151
- onValueChange: (e) => k(e),
152
- children: [/* @__PURE__ */ O("div", {
153
+ value: v,
154
+ onValueChange: (e) => T(e),
155
+ children: [/* @__PURE__ */ k("div", {
153
156
  className: "mb-4 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between",
154
- children: [/* @__PURE__ */ D(F, { children: z.map((e) => /* @__PURE__ */ D(I, {
157
+ children: [/* @__PURE__ */ O(I, { children: z.map((e) => /* @__PURE__ */ O(L, {
155
158
  value: e.value,
156
159
  children: e.label
157
- }, e.value)) }), /* @__PURE__ */ O("div", {
160
+ }, e.value)) }), /* @__PURE__ */ k("div", {
158
161
  className: "relative w-full sm:w-72",
159
- children: [/* @__PURE__ */ D(T, {
162
+ children: [/* @__PURE__ */ O(E, {
160
163
  className: "pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-text-muted",
161
164
  size: 16
162
- }), /* @__PURE__ */ D(M, {
165
+ }), /* @__PURE__ */ O(N, {
163
166
  className: "pl-9",
164
- "aria-label": m("technospam.courses.searchLabel", "Search courses"),
165
- placeholder: m("technospam.courses.searchPlaceholder", "Search courses…"),
166
- value: j,
167
- onChange: (e) => R(e.target.value),
167
+ "aria-label": u("technospam.courses.searchLabel", "Search courses"),
168
+ placeholder: u("technospam.courses.searchPlaceholder", "Search courses…"),
169
+ value: A,
170
+ onChange: (e) => M(e.target.value),
168
171
  onBlur: () => {
169
- let e = j.trim();
172
+ let e = A.trim();
170
173
  e && l("technospam.search.performed", {
171
174
  workspaceId: s,
172
175
  entityType: "course",
@@ -175,19 +178,19 @@ function R() {
175
178
  }
176
179
  })]
177
180
  })]
178
- }), /* @__PURE__ */ O(P, {
179
- value: w,
180
- className: v,
181
- children: [U && !G.length ? /* @__PURE__ */ D(p, {}) : W ? /* @__PURE__ */ D(f, {
182
- title: m("technospam.common.errorLoadCourses", "Couldn't load courses"),
181
+ }), /* @__PURE__ */ k(F, {
182
+ value: v,
183
+ className: y,
184
+ children: [U && !G.length ? /* @__PURE__ */ O(m, {}) : W ? /* @__PURE__ */ O(p, {
185
+ title: u("technospam.common.errorLoadCourses", "Couldn't load courses"),
183
186
  error: W
184
- }) : q.length === 0 ? /* @__PURE__ */ D(d, {
185
- icon: /* @__PURE__ */ D(x, { size: 26 }),
186
- title: j || w !== "ALL" ? m("technospam.courses.emptySearchTitle", "No matching courses") : m("technospam.courses.emptyDefaultTitle", "No courses published yet"),
187
- description: j || w !== "ALL" ? m("technospam.courses.emptySearchDesc", "Try a different search or level filter.") : m("technospam.courses.emptyDefaultDesc", "Published courses will appear here. Check back soon — TechnoSpam is pre-launch.")
188
- }) : /* @__PURE__ */ D("div", {
187
+ }) : q.length === 0 ? /* @__PURE__ */ O(f, {
188
+ icon: /* @__PURE__ */ O(S, { size: 26 }),
189
+ title: A || v !== "ALL" ? u("technospam.courses.emptySearchTitle", "No matching courses") : u("technospam.courses.emptyDefaultTitle", "No courses published yet"),
190
+ description: A || v !== "ALL" ? u("technospam.courses.emptySearchDesc", "Try a different search or level filter.") : u("technospam.courses.emptyDefaultDesc", "Published courses will appear here. Check back soon — TechnoSpam is pre-launch.")
191
+ }) : /* @__PURE__ */ O("div", {
189
192
  className: "grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3",
190
- children: q.map((e) => /* @__PURE__ */ D(L, {
193
+ children: q.map((e) => /* @__PURE__ */ O(R, {
191
194
  course: e,
192
195
  onOpen: () => {
193
196
  l("technospam.course.viewed", {
@@ -197,7 +200,7 @@ function R() {
197
200
  }), o(`/courses/course-details-page?id=${e.id}`);
198
201
  }
199
202
  }, e.id))
200
- }), /* @__PURE__ */ D(u, {
203
+ }), /* @__PURE__ */ O(d, {
201
204
  hasMore: K,
202
205
  loading: U,
203
206
  onClick: V
@@ -207,6 +210,6 @@ function R() {
207
210
  });
208
211
  }
209
212
  //#endregion
210
- export { R as CoursesCoursesPagePage };
213
+ export { z as CoursesCoursesPagePage };
211
214
 
212
215
  //# sourceMappingURL=CoursesCoursesPagePage.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"CoursesCoursesPagePage.js","names":[],"sources":["../../../src/technospam/pages/CoursesCoursesPagePage.tsx"],"sourcesContent":["import { useMemo, useState } from 'react';\nimport { BookOpen, Clock, GraduationCap, Layers, Search, Sparkles } from 'lucide-react';\nimport {\n Badge,\n Button,\n GlassCard,\n Input,\n Tabs,\n TabsContent,\n TabsList,\n TabsTrigger,\n} from '@burdenoff/fe-libs/ui';\nimport { TAB_PANEL_FOCUS_CLASS } from '../lib/a11y';\nimport {\n EmptyView,\n ErrorView,\n LoadMore,\n LoadingGrid,\n MediaCover,\n PageLayout,\n PremiumBadge,\n StatCard,\n StatusBadge,\n topicIcon,\n} from '../components';\nimport { useTechnoSpam, useTechnoSpamNavigate } from '../providers/TechnoSpamProvider';\nimport {\n pageArg,\n useCoursesQuery,\n usePageLimit,\n type TecCourse,\n type TecCourseLevel,\n} from '../api/technospamApi';\nimport { formatDuration } from '../utils/format';\nimport { useTechnoSpamAnalytics } from '../hooks/useTechnoSpamAnalytics';\nimport { useTr } from '../hooks/useTr';\n\nfunction CourseCard({ course, onOpen }: { course: TecCourse; onOpen: () => void }) {\n return (\n <GlassCard\n className=\"group flex cursor-pointer flex-col overflow-hidden transition-all duration-200 hover:-translate-y-1 hover:shadow-lg\"\n onClick={onOpen}\n >\n <MediaCover\n seed={`${course.topic ?? ''} ${course.title}`}\n icon={topicIcon(course.topic ?? course.title)}\n >\n <div className=\"flex flex-wrap items-center gap-1.5\">\n <StatusBadge status={course.level} kind=\"level\" />\n {course.isPremiumOnly ? <PremiumBadge locked={course.isLocked} /> : null}\n </div>\n </MediaCover>\n <div className=\"flex flex-1 flex-col gap-3 p-4\">\n <div className=\"min-w-0\">\n <h3 className=\"truncate text-base font-semibold text-text-primary group-hover:text-accent-primary\">\n {course.title}\n </h3>\n {course.summary ? (\n <p className=\"mt-1 line-clamp-2 text-sm text-text-secondary\">{course.summary}</p>\n ) : null}\n </div>\n <div className=\"mt-auto flex flex-wrap items-center gap-3 text-xs text-text-muted\">\n {course.topic ? (\n <span className=\"inline-flex items-center gap-1\">\n <Layers size={13} /> {course.topic}\n </span>\n ) : null}\n <span className=\"inline-flex items-center gap-1\">\n <Clock size={13} /> {formatDuration(course.estimatedMinutes)}\n </span>\n </div>\n {course.tags.length ? (\n <div className=\"flex flex-wrap gap-1.5\">\n {course.tags.slice(0, 3).map((t) => (\n <Badge key={t} variant=\"secondary\" className=\"text-[11px]\">\n {t}\n </Badge>\n ))}\n </div>\n ) : null}\n </div>\n </GlassCard>\n );\n}\n\nexport function CoursesCoursesPagePage() {\n const navigate = useTechnoSpamNavigate();\n const { workspaceId } = useTechnoSpam();\n const { emit } = useTechnoSpamAnalytics();\n const tr = useTr();\n const [level, setLevel] = useState<'ALL' | TecCourseLevel>('ALL');\n const [search, setSearch] = useState('');\n\n const levels = useMemo(\n () => [\n { value: 'ALL' as const, label: tr('technospam.courses.filterAllLevels', 'All levels') },\n {\n value: 'BEGINNER' as TecCourseLevel,\n label: tr('technospam.courses.filterBeginner', 'Beginner'),\n },\n {\n value: 'INTERMEDIATE' as TecCourseLevel,\n label: tr('technospam.courses.filterIntermediate', 'Intermediate'),\n },\n {\n value: 'ADVANCED' as TecCourseLevel,\n label: tr('technospam.courses.filterAdvanced', 'Advanced'),\n },\n { value: 'EXPERT' as TecCourseLevel, label: tr('technospam.courses.filterExpert', 'Expert') },\n ],\n [tr]\n );\n\n const { limit, more } = usePageLimit();\n const { data, loading, error } = useCoursesQuery({\n status: 'PUBLISHED',\n pagination: pageArg(limit),\n ...(level !== 'ALL' ? { level } : {}),\n });\n\n const courses = data?.tecListCourses?.items ?? [];\n const hasMore = data?.tecListCourses?.pageInfo?.hasMore ?? false;\n const filtered = useMemo(() => {\n const q = search.trim().toLowerCase();\n if (!q) return courses;\n return courses.filter(\n (c) =>\n c.title.toLowerCase().includes(q) ||\n (c.topic ?? '').toLowerCase().includes(q) ||\n c.tags.some((t) => t.toLowerCase().includes(q))\n );\n }, [courses, search]);\n\n return (\n <PageLayout\n hero\n title={tr('technospam.courses.title', 'Course Catalog')}\n description={tr(\n 'technospam.courses.description',\n 'Learn technology by doing — practical, project-based courses across the stack.'\n )}\n actions={\n <Button variant=\"outline\" onClick={() => navigate('/courses/my-learning-page')}>\n <GraduationCap size={16} /> {tr('technospam.common.myLearning', 'My Learning')}\n </Button>\n }\n >\n <div className=\"mb-5 grid grid-cols-2 gap-3 sm:grid-cols-4\">\n <StatCard\n label={tr('technospam.courses.statCourses', 'Courses')}\n value={courses.length}\n icon={BookOpen}\n tone=\"blue\"\n />\n <StatCard\n label={tr('technospam.courses.statBeginnerFriendly', 'Beginner-friendly')}\n value={courses.filter((c) => c.level === 'BEGINNER').length}\n icon={Sparkles}\n tone=\"green\"\n />\n <StatCard\n label={tr('technospam.courses.statAdvancedPlus', 'Advanced+')}\n value={courses.filter((c) => c.level === 'ADVANCED' || c.level === 'EXPERT').length}\n icon={GraduationCap}\n tone=\"purple\"\n />\n <StatCard\n label={tr('technospam.courses.statTotalContent', 'Total content')}\n value={formatDuration(courses.reduce((s, c) => s + c.estimatedMinutes, 0))}\n icon={Clock}\n tone=\"teal\"\n />\n </div>\n\n {/* The tabs filter one list, so that list is their tab panel: Radix points every\n trigger's aria-controls at a panel id, and without a TabsContent the id\n resolved to nothing (axe aria-valid-attr-value, critical; BOFF-3157). */}\n <Tabs\n className=\"gap-0\"\n value={level}\n onValueChange={(v) => setLevel(v as 'ALL' | TecCourseLevel)}\n >\n <div className=\"mb-4 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between\">\n <TabsList>\n {levels.map((l) => (\n <TabsTrigger key={l.value} value={l.value}>\n {l.label}\n </TabsTrigger>\n ))}\n </TabsList>\n <div className=\"relative w-full sm:w-72\">\n <Search\n className=\"pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-text-muted\"\n size={16}\n />\n <Input\n className=\"pl-9\"\n aria-label={tr('technospam.courses.searchLabel', 'Search courses')}\n placeholder={tr('technospam.courses.searchPlaceholder', 'Search courses…')}\n value={search}\n onChange={(e) => setSearch(e.target.value)}\n onBlur={() => {\n const value = search.trim();\n if (value) {\n emit('technospam.search.performed', {\n workspaceId,\n entityType: 'course',\n queryLength: value.length,\n });\n }\n }}\n />\n </div>\n </div>\n\n <TabsContent value={level} className={TAB_PANEL_FOCUS_CLASS}>\n {loading && !courses.length ? (\n <LoadingGrid />\n ) : error ? (\n <ErrorView\n title={tr('technospam.common.errorLoadCourses', \"Couldn't load courses\")}\n error={error}\n />\n ) : filtered.length === 0 ? (\n <EmptyView\n icon={<BookOpen size={26} />}\n title={\n search || level !== 'ALL'\n ? tr('technospam.courses.emptySearchTitle', 'No matching courses')\n : tr('technospam.courses.emptyDefaultTitle', 'No courses published yet')\n }\n description={\n search || level !== 'ALL'\n ? tr(\n 'technospam.courses.emptySearchDesc',\n 'Try a different search or level filter.'\n )\n : tr(\n 'technospam.courses.emptyDefaultDesc',\n 'Published courses will appear here. Check back soon — TechnoSpam is pre-launch.'\n )\n }\n />\n ) : (\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n {filtered.map((course) => (\n <CourseCard\n key={course.id}\n course={course}\n onOpen={() => {\n emit('technospam.course.viewed', {\n workspaceId,\n entityId: course.id,\n entityType: 'TEC_COURSE',\n });\n navigate(`/courses/course-details-page?id=${course.id}`);\n }}\n />\n ))}\n </div>\n )}\n <LoadMore hasMore={hasMore} loading={loading} onClick={more} />\n </TabsContent>\n </Tabs>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"CoursesCoursesPagePage.js","names":[],"sources":["../../../src/technospam/pages/CoursesCoursesPagePage.tsx"],"sourcesContent":["import { useMemo, useState } from 'react';\nimport { BookOpen, Clock, GraduationCap, Layers, Search, Sparkles } from 'lucide-react';\nimport {\n Badge,\n Button,\n GlassCard,\n Input,\n Tabs,\n TabsContent,\n TabsList,\n TabsTrigger,\n} from '@burdenoff/fe-libs/ui';\nimport { TAB_PANEL_FOCUS_CLASS } from '../lib/a11y';\nimport {\n CardOpenButton,\n EmptyView,\n ErrorView,\n LoadMore,\n LoadingGrid,\n MediaCover,\n PageLayout,\n PremiumBadge,\n StatCard,\n StatusBadge,\n topicIcon,\n} from '../components';\nimport { useTechnoSpam, useTechnoSpamNavigate } from '../providers/TechnoSpamProvider';\nimport {\n pageArg,\n useCoursesQuery,\n usePageLimit,\n type TecCourse,\n type TecCourseLevel,\n} from '../api/technospamApi';\nimport { formatDuration } from '../utils/format';\nimport { useTechnoSpamAnalytics } from '../hooks/useTechnoSpamAnalytics';\nimport { useTr } from '../hooks/useTr';\n\nfunction CourseCard({ course, onOpen }: { course: TecCourse; onOpen: () => void }) {\n return (\n <GlassCard className=\"group flex cursor-pointer flex-col overflow-hidden transition-all duration-200 hover:-translate-y-1 hover:shadow-lg\">\n <MediaCover\n seed={`${course.topic ?? ''} ${course.title}`}\n icon={topicIcon(course.topic ?? course.title)}\n >\n <div className=\"flex flex-wrap items-center gap-1.5\">\n <StatusBadge status={course.level} kind=\"level\" />\n {course.isPremiumOnly ? <PremiumBadge locked={course.isLocked} /> : null}\n </div>\n </MediaCover>\n <div className=\"flex flex-1 flex-col gap-3 p-4\">\n <div className=\"min-w-0\">\n <h3 className=\"truncate text-base font-semibold text-text-primary group-hover:text-accent-primary\">\n <CardOpenButton onOpen={onOpen}>{course.title}</CardOpenButton>\n </h3>\n {course.summary ? (\n <p className=\"mt-1 line-clamp-2 text-sm text-text-secondary\">{course.summary}</p>\n ) : null}\n </div>\n <div className=\"mt-auto flex flex-wrap items-center gap-3 text-xs text-text-muted\">\n {course.topic ? (\n <span className=\"inline-flex items-center gap-1\">\n <Layers size={13} /> {course.topic}\n </span>\n ) : null}\n <span className=\"inline-flex items-center gap-1\">\n <Clock size={13} /> {formatDuration(course.estimatedMinutes)}\n </span>\n </div>\n {course.tags.length ? (\n <div className=\"flex flex-wrap gap-1.5\">\n {course.tags.slice(0, 3).map((t) => (\n <Badge key={t} variant=\"secondary\" className=\"text-[11px]\">\n {t}\n </Badge>\n ))}\n </div>\n ) : null}\n </div>\n </GlassCard>\n );\n}\n\nexport function CoursesCoursesPagePage() {\n const navigate = useTechnoSpamNavigate();\n const { workspaceId } = useTechnoSpam();\n const { emit } = useTechnoSpamAnalytics();\n const tr = useTr();\n const [level, setLevel] = useState<'ALL' | TecCourseLevel>('ALL');\n const [search, setSearch] = useState('');\n\n const levels = useMemo(\n () => [\n { value: 'ALL' as const, label: tr('technospam.courses.filterAllLevels', 'All levels') },\n {\n value: 'BEGINNER' as TecCourseLevel,\n label: tr('technospam.courses.filterBeginner', 'Beginner'),\n },\n {\n value: 'INTERMEDIATE' as TecCourseLevel,\n label: tr('technospam.courses.filterIntermediate', 'Intermediate'),\n },\n {\n value: 'ADVANCED' as TecCourseLevel,\n label: tr('technospam.courses.filterAdvanced', 'Advanced'),\n },\n { value: 'EXPERT' as TecCourseLevel, label: tr('technospam.courses.filterExpert', 'Expert') },\n ],\n [tr]\n );\n\n const { limit, more } = usePageLimit();\n const { data, loading, error } = useCoursesQuery({\n status: 'PUBLISHED',\n pagination: pageArg(limit),\n ...(level !== 'ALL' ? { level } : {}),\n });\n\n const courses = data?.tecListCourses?.items ?? [];\n const hasMore = data?.tecListCourses?.pageInfo?.hasMore ?? false;\n const filtered = useMemo(() => {\n const q = search.trim().toLowerCase();\n if (!q) return courses;\n return courses.filter(\n (c) =>\n c.title.toLowerCase().includes(q) ||\n (c.topic ?? '').toLowerCase().includes(q) ||\n c.tags.some((t) => t.toLowerCase().includes(q))\n );\n }, [courses, search]);\n\n return (\n <PageLayout\n hero\n title={tr('technospam.courses.title', 'Course Catalog')}\n description={tr(\n 'technospam.courses.description',\n 'Learn technology by doing — practical, project-based courses across the stack.'\n )}\n actions={\n <Button variant=\"outline\" onClick={() => navigate('/courses/my-learning-page')}>\n <GraduationCap size={16} /> {tr('technospam.common.myLearning', 'My Learning')}\n </Button>\n }\n >\n <div className=\"mb-5 grid grid-cols-2 gap-3 sm:grid-cols-4\">\n <StatCard\n label={tr('technospam.courses.statCourses', 'Courses')}\n value={courses.length}\n icon={BookOpen}\n tone=\"blue\"\n />\n <StatCard\n label={tr('technospam.courses.statBeginnerFriendly', 'Beginner-friendly')}\n value={courses.filter((c) => c.level === 'BEGINNER').length}\n icon={Sparkles}\n tone=\"green\"\n />\n <StatCard\n label={tr('technospam.courses.statAdvancedPlus', 'Advanced+')}\n value={courses.filter((c) => c.level === 'ADVANCED' || c.level === 'EXPERT').length}\n icon={GraduationCap}\n tone=\"purple\"\n />\n <StatCard\n label={tr('technospam.courses.statTotalContent', 'Total content')}\n value={formatDuration(courses.reduce((s, c) => s + c.estimatedMinutes, 0))}\n icon={Clock}\n tone=\"teal\"\n />\n </div>\n\n {/* The tabs filter one list, so that list is their tab panel: Radix points every\n trigger's aria-controls at a panel id, and without a TabsContent the id\n resolved to nothing (axe aria-valid-attr-value, critical; BOFF-3157). */}\n <Tabs\n className=\"gap-0\"\n value={level}\n onValueChange={(v) => setLevel(v as 'ALL' | TecCourseLevel)}\n >\n <div className=\"mb-4 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between\">\n <TabsList>\n {levels.map((l) => (\n <TabsTrigger key={l.value} value={l.value}>\n {l.label}\n </TabsTrigger>\n ))}\n </TabsList>\n <div className=\"relative w-full sm:w-72\">\n <Search\n className=\"pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-text-muted\"\n size={16}\n />\n <Input\n className=\"pl-9\"\n aria-label={tr('technospam.courses.searchLabel', 'Search courses')}\n placeholder={tr('technospam.courses.searchPlaceholder', 'Search courses…')}\n value={search}\n onChange={(e) => setSearch(e.target.value)}\n onBlur={() => {\n const value = search.trim();\n if (value) {\n emit('technospam.search.performed', {\n workspaceId,\n entityType: 'course',\n queryLength: value.length,\n });\n }\n }}\n />\n </div>\n </div>\n\n <TabsContent value={level} className={TAB_PANEL_FOCUS_CLASS}>\n {loading && !courses.length ? (\n <LoadingGrid />\n ) : error ? (\n <ErrorView\n title={tr('technospam.common.errorLoadCourses', \"Couldn't load courses\")}\n error={error}\n />\n ) : filtered.length === 0 ? (\n <EmptyView\n icon={<BookOpen size={26} />}\n title={\n search || level !== 'ALL'\n ? tr('technospam.courses.emptySearchTitle', 'No matching courses')\n : tr('technospam.courses.emptyDefaultTitle', 'No courses published yet')\n }\n description={\n search || level !== 'ALL'\n ? tr(\n 'technospam.courses.emptySearchDesc',\n 'Try a different search or level filter.'\n )\n : tr(\n 'technospam.courses.emptyDefaultDesc',\n 'Published courses will appear here. Check back soon — TechnoSpam is pre-launch.'\n )\n }\n />\n ) : (\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n {filtered.map((course) => (\n <CourseCard\n key={course.id}\n course={course}\n onOpen={() => {\n emit('technospam.course.viewed', {\n workspaceId,\n entityId: course.id,\n entityType: 'TEC_COURSE',\n });\n navigate(`/courses/course-details-page?id=${course.id}`);\n }}\n />\n ))}\n </div>\n )}\n <LoadMore hasMore={hasMore} loading={loading} onClick={more} />\n </TabsContent>\n </Tabs>\n </PageLayout>\n );\n}\n"],"mappings":"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"}