@burdenoff/microfe-technospam 2026.1024.1 → 2026.1030.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/node_modules/recharts/es6/cartesian/Brush.js.map +1 -1
- package/dist/node_modules/recharts/es6/cartesian/ErrorBar.js.map +1 -1
- package/dist/node_modules/recharts/es6/cartesian/ReferenceArea.js.map +1 -1
- package/dist/node_modules/recharts/es6/cartesian/ReferenceDot.js.map +1 -1
- package/dist/node_modules/recharts/es6/cartesian/ReferenceLine.js.map +1 -1
- package/dist/node_modules/recharts/es6/cartesian/XAxis.js.map +1 -1
- package/dist/node_modules/recharts/es6/cartesian/YAxis.js.map +1 -1
- package/dist/node_modules/recharts/es6/component/ResponsiveContainer.js.map +1 -1
- package/dist/node_modules/sonner/dist/index.js.map +1 -1
- package/dist/technospam/api/technospamApi.d.ts +28 -0
- package/dist/technospam/api/technospamApi.d.ts.map +1 -1
- package/dist/technospam/api/technospamApi.js +193 -153
- package/dist/technospam/api/technospamApi.js.map +1 -1
- package/dist/technospam/components/SubmissionReviewPanel.d.ts +15 -0
- package/dist/technospam/components/SubmissionReviewPanel.d.ts.map +1 -0
- package/dist/technospam/components/SubmissionReviewPanel.js +85 -0
- package/dist/technospam/components/SubmissionReviewPanel.js.map +1 -0
- package/dist/technospam/components/index.d.ts +1 -0
- package/dist/technospam/components/index.d.ts.map +1 -1
- package/dist/technospam/components/index.js +1 -0
- package/dist/technospam/hooks/useFocusedRecordParam.d.ts +41 -0
- package/dist/technospam/hooks/useFocusedRecordParam.d.ts.map +1 -0
- package/dist/technospam/hooks/useFocusedRecordParam.js +41 -0
- package/dist/technospam/hooks/useFocusedRecordParam.js.map +1 -0
- package/dist/technospam/pages/AdminAdminDashboardPagePage.js +1 -1
- package/dist/technospam/pages/AnalyticsAnalyticsPagePage.js +1 -1
- package/dist/technospam/pages/ApplicationsPortfolioPagePage.js +1 -1
- package/dist/technospam/pages/ApplicationsProblemDetailsPagePage.d.ts.map +1 -1
- package/dist/technospam/pages/ApplicationsProblemDetailsPagePage.js +190 -165
- package/dist/technospam/pages/ApplicationsProblemDetailsPagePage.js.map +1 -1
- package/dist/technospam/pages/ApplicationsProblemsPagePage.js +1 -1
- package/dist/technospam/pages/CoursesCourseDetailsPagePage.d.ts.map +1 -1
- package/dist/technospam/pages/CoursesCourseDetailsPagePage.js +92 -88
- package/dist/technospam/pages/CoursesCourseDetailsPagePage.js.map +1 -1
- package/dist/technospam/pages/CoursesCoursesPagePage.js +1 -1
- package/dist/technospam/pages/CoursesMyLearningPagePage.d.ts.map +1 -1
- package/dist/technospam/pages/CoursesMyLearningPagePage.js +90 -80
- package/dist/technospam/pages/CoursesMyLearningPagePage.js.map +1 -1
- package/dist/technospam/pages/DashboardDashboardPagePage.js +1 -1
- package/dist/technospam/pages/HackathonsHackathonDetailsPagePage.d.ts.map +1 -1
- package/dist/technospam/pages/HackathonsHackathonDetailsPagePage.js +155 -147
- package/dist/technospam/pages/HackathonsHackathonDetailsPagePage.js.map +1 -1
- package/dist/technospam/pages/HackathonsHackathonsPagePage.js +1 -1
- package/dist/technospam/pages/MentorshipMentorProfilePagePage.js +1 -1
- package/dist/technospam/pages/MentorshipMentorsPagePage.js +1 -1
- package/dist/technospam/pages/MentorshipSessionsPagePage.d.ts.map +1 -1
- package/dist/technospam/pages/MentorshipSessionsPagePage.js +186 -167
- package/dist/technospam/pages/MentorshipSessionsPagePage.js.map +1 -1
- package/dist/technospam/pages/StudyPlannerStudyPlannerPagePage.js +1 -1
- package/dist/technospam/utils/pageContext.d.ts +54 -5
- package/dist/technospam/utils/pageContext.d.ts.map +1 -1
- package/dist/technospam/utils/pageContext.js +99 -35
- package/dist/technospam/utils/pageContext.js.map +1 -1
- 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, useState } from 'react';\nimport { useSearchParams } from 'react-router';\nimport {\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 } from '../providers/TechnoSpamProvider';\nimport {\n useCourseQuery,\n useEnroll,\n useIssueCertificate,\n useMyCertificatesQuery,\n useUpdateProgress,\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';\n\n/** Per-lesson-kind icon + on-theme tint (chip) and pill (badge) classes. */\nconst KIND_META: Record<string, { icon: LucideIcon; chip: string; badge: string }> = {\n VIDEO: {\n icon: PlayCircle,\n chip: 'bg-accent-primary/15 text-accent-primary',\n badge: 'border-accent-primary/25 bg-accent-primary/10 text-accent-primary',\n },\n ARTICLE: {\n icon: FileText,\n chip: 'bg-accent-indigo/15 text-accent-indigo',\n badge: 'border-accent-indigo/25 bg-accent-indigo/10 text-accent-indigo',\n },\n INTERACTIVE: {\n icon: Sparkles,\n chip: 'bg-accent-teal/15 text-accent-teal',\n badge: 'border-accent-teal/25 bg-accent-teal/10 text-accent-teal',\n },\n QUIZ: {\n icon: CheckCircle2,\n chip: 'bg-accent-purple/15 text-accent-purple',\n badge: 'border-accent-purple/25 bg-accent-purple/10 text-accent-purple',\n },\n PROJECT: {\n icon: Layers,\n chip: 'bg-status-warning-bg-subtle text-status-warning-text',\n badge: 'border-status-warning-border bg-status-warning-bg-subtle text-status-warning-text',\n },\n};\n\nfunction HeroBadge({ icon: Icon, children }: { icon?: LucideIcon; children: ReactNode }) {\n return (\n <span className=\"inline-flex items-center gap-1 rounded-full bg-white/15 px-2.5 py-1 text-xs font-medium text-white ring-1 ring-white/25 backdrop-blur-sm\">\n {Icon ? <Icon size={12} /> : null}\n {children}\n </span>\n );\n}\n\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({ lesson, index, done }: { lesson: TecLesson; index: number; done: boolean }) {\n const [open, setOpen] = useState(false);\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={() => setOpen((v) => !v)}\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/10 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 ) : lesson.content ? (\n <div className=\"rounded-xl border border-border-seam bg-bg-sunken p-4\">\n <p className=\"whitespace-pre-wrap text-sm leading-relaxed text-text-secondary\">\n {lesson.content}\n </p>\n </div>\n ) : (\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 </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 const { currentUser, workspaceId } = useTechnoSpam();\n const { emit } = useTechnoSpamAnalytics();\n const { data, loading, error, refetch } = useCourseQuery(id);\n\n useEffect(() => {\n if (id) {\n emit('technospam.course.viewed', {\n workspaceId,\n entityId: id,\n entityType: 'TEC_COURSE',\n });\n }\n }, [id, workspaceId, emit]);\n\n const [enroll, { loading: enrolling }] = useEnroll();\n const [updateProgress, { loading: savingProgress }] = useUpdateProgress();\n // Self-scoped read: `tecMyCertificates` filters by the authenticated actor\n // server-side. `tecListCertificates` (the previous call) is gated behind the\n // cross-learner \"list-all\" action, which a plain learner does not hold — so\n // an already-claimed certificate came back empty and the page kept offering\n // \"Claim certificate\" instead of showing it.\n const { data: certData, refetch: refetchCerts } = useMyCertificatesQuery(\n id ? { courseId: id } : {}\n );\n const [issueCertificate, { loading: claiming }] = useIssueCertificate();\n\n const course = data?.tecCourse ?? null;\n const lessons = [...(course?.lessons ?? [])].sort((a, b) => a.position - b.position);\n const myEnrollment: TecEnrollment | undefined = (course?.enrollments ?? []).find(\n (e) => e.learnerId === currentUser?.id\n );\n const myCertificates = certData?.tecMyCertificates ?? [];\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 const handleProgress = async () => {\n if (!myEnrollment) return;\n const nextCompleted = Math.min(lessons.length, myEnrollment.lessonsCompleted + 1);\n const pct = lessons.length ? Math.round((nextCompleted / lessons.length) * 100) : 100;\n const res = await updateProgress({\n variables: {\n input: { enrollmentId: myEnrollment.id, lessonsCompleted: nextCompleted, progressPct: pct },\n },\n });\n if (res.data?.tecUpdateProgress) {\n emit('technospam.course.progress_updated', {\n workspaceId,\n entityId: myEnrollment.id,\n entityType: 'TEC_ENROLLMENT',\n progressPct: pct,\n });\n notify.success('Progress saved', `${pct}% complete`);\n await refetch();\n }\n };\n\n if (loading && !course) {\n return (\n <PageLayout title=\"Course\" backPath=\"/courses/courses-page\">\n <Skeleton className=\"mb-4 h-40 w-full rounded-2xl\" />\n <Skeleton className=\"h-64 w-full rounded-xl\" />\n </PageLayout>\n );\n }\n\n if (error || !course) {\n return (\n <PageLayout title=\"Course\" backPath=\"/courses/courses-page\">\n {error ? (\n <ErrorView title=\"Couldn't load course\" error={error} />\n ) : (\n <EmptyView\n title=\"Course not found\"\n description=\"This course may have been unpublished.\"\n />\n )}\n </PageLayout>\n );\n }\n\n const completedLessons = myEnrollment?.lessonsCompleted ?? 0;\n const remaining = Math.max(0, lessons.length - completedLessons);\n const milestone =\n myEnrollment && myEnrollment.progressPct >= 100\n ? 'Course complete — claim your certificate below.'\n : remaining === 1\n ? '1 lesson to go — almost there!'\n : `${remaining} lessons to go — keep it up!`;\n const heroSeed = `${course.topic ?? ''} ${course.title}`;\n\n return (\n <PageLayout\n backPath=\"/courses/courses-page\"\n title={course.title}\n description={course.summary ?? undefined}\n actions={\n myEnrollment ? (\n <Button\n onClick={handleProgress}\n disabled={savingProgress || completedLessons >= lessons.length}\n >\n <CheckCircle2 size={16} />\n {completedLessons >= lessons.length ? 'Completed' : 'Complete next lesson'}\n </Button>\n ) : (\n <Button onClick={handleEnroll} disabled={enrolling}>\n <BookOpen size={16} /> {enrolling ? 'Enrolling…' : 'Enroll'}\n </Button>\n )\n }\n >\n {/* Bold hero banner */}\n <MediaCover\n height=\"h-40\"\n seed={heroSeed}\n icon={topicIcon(course.topic ?? course.title)}\n className=\"mb-5 rounded-2xl\"\n >\n <div className=\"flex w-full flex-col gap-2\">\n <span className=\"text-xs font-medium uppercase tracking-wide text-white/80\">\n {course.topic ?? 'Course'}\n </span>\n <h2 className=\"text-2xl font-bold leading-tight text-white drop-shadow-sm sm:text-3xl\">\n {course.title}\n </h2>\n <div className=\"flex flex-wrap items-center gap-2\">\n <HeroBadge icon={TrendingUp}>{humanizeEnum(course.level)}</HeroBadge>\n <HeroBadge icon={CheckCircle2}>{humanizeEnum(course.status)}</HeroBadge>\n {course.topic ? <HeroBadge icon={Layers}>{course.topic}</HeroBadge> : null}\n {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 key={lesson.id} lesson={lesson} index={i} done={i < completedLessons} />\n ))}\n </div>\n )}\n </GlassCard>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CoursesCourseDetailsPagePage.js","names":[],"sources":["../../../src/technospam/pages/CoursesCourseDetailsPagePage.tsx"],"sourcesContent":["import type { ReactNode } from 'react';\nimport { useEffect } from 'react';\nimport { useSearchParams } from 'react-router';\nimport {\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 } from '../providers/TechnoSpamProvider';\nimport {\n useCourseQuery,\n useEnroll,\n useIssueCertificate,\n useMyCertificatesQuery,\n useUpdateProgress,\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';\n\n/** Per-lesson-kind icon + on-theme tint (chip) and pill (badge) classes. */\nconst KIND_META: Record<string, { icon: LucideIcon; chip: string; badge: string }> = {\n VIDEO: {\n icon: PlayCircle,\n chip: 'bg-accent-primary/15 text-accent-primary',\n badge: 'border-accent-primary/25 bg-accent-primary/10 text-accent-primary',\n },\n ARTICLE: {\n icon: FileText,\n chip: 'bg-accent-indigo/15 text-accent-indigo',\n badge: 'border-accent-indigo/25 bg-accent-indigo/10 text-accent-indigo',\n },\n INTERACTIVE: {\n icon: Sparkles,\n chip: 'bg-accent-teal/15 text-accent-teal',\n badge: 'border-accent-teal/25 bg-accent-teal/10 text-accent-teal',\n },\n QUIZ: {\n icon: CheckCircle2,\n chip: 'bg-accent-purple/15 text-accent-purple',\n badge: 'border-accent-purple/25 bg-accent-purple/10 text-accent-purple',\n },\n PROJECT: {\n icon: Layers,\n chip: 'bg-status-warning-bg-subtle text-status-warning-text',\n badge: 'border-status-warning-border bg-status-warning-bg-subtle text-status-warning-text',\n },\n};\n\nfunction HeroBadge({ icon: Icon, children }: { icon?: LucideIcon; children: ReactNode }) {\n return (\n <span className=\"inline-flex items-center gap-1 rounded-full bg-white/15 px-2.5 py-1 text-xs font-medium text-white ring-1 ring-white/25 backdrop-blur-sm\">\n {Icon ? <Icon size={12} /> : null}\n {children}\n </span>\n );\n}\n\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}: {\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}) {\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/10 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 ) : lesson.content ? (\n <div className=\"rounded-xl border border-border-seam bg-bg-sunken p-4\">\n <p className=\"whitespace-pre-wrap text-sm leading-relaxed text-text-secondary\">\n {lesson.content}\n </p>\n </div>\n ) : (\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 </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 { emit } = useTechnoSpamAnalytics();\n const { data, loading, error, refetch } = useCourseQuery(id);\n\n useEffect(() => {\n if (id) {\n emit('technospam.course.viewed', {\n workspaceId,\n entityId: id,\n entityType: 'TEC_COURSE',\n });\n }\n }, [id, workspaceId, emit]);\n\n const [enroll, { loading: enrolling }] = useEnroll();\n const [updateProgress, { loading: savingProgress }] = useUpdateProgress();\n // Self-scoped read: `tecMyCertificates` filters by the authenticated actor\n // server-side. `tecListCertificates` (the previous call) is gated behind the\n // cross-learner \"list-all\" action, which a plain learner does not hold — so\n // an already-claimed certificate came back empty and the page kept offering\n // \"Claim certificate\" instead of showing it.\n const { data: certData, refetch: refetchCerts } = useMyCertificatesQuery(\n id ? { courseId: id } : {}\n );\n const [issueCertificate, { loading: claiming }] = useIssueCertificate();\n\n const course = data?.tecCourse ?? null;\n const lessons = [...(course?.lessons ?? [])].sort((a, b) => a.position - b.position);\n const myEnrollment: TecEnrollment | undefined = (course?.enrollments ?? []).find(\n (e) => e.learnerId === currentUser?.id\n );\n const myCertificates = certData?.tecMyCertificates ?? [];\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 const handleProgress = async () => {\n if (!myEnrollment) return;\n const nextCompleted = Math.min(lessons.length, myEnrollment.lessonsCompleted + 1);\n const pct = lessons.length ? Math.round((nextCompleted / lessons.length) * 100) : 100;\n const res = await updateProgress({\n variables: {\n input: { enrollmentId: myEnrollment.id, lessonsCompleted: nextCompleted, progressPct: pct },\n },\n });\n if (res.data?.tecUpdateProgress) {\n emit('technospam.course.progress_updated', {\n workspaceId,\n entityId: myEnrollment.id,\n entityType: 'TEC_ENROLLMENT',\n progressPct: pct,\n });\n notify.success('Progress saved', `${pct}% complete`);\n await refetch();\n }\n };\n\n if (loading && !course) {\n return (\n <PageLayout title=\"Course\" backPath=\"/courses/courses-page\">\n <Skeleton className=\"mb-4 h-40 w-full rounded-2xl\" />\n <Skeleton className=\"h-64 w-full rounded-xl\" />\n </PageLayout>\n );\n }\n\n if (error || !course) {\n return (\n <PageLayout title=\"Course\" backPath=\"/courses/courses-page\">\n {error ? (\n <ErrorView title=\"Couldn't load course\" error={error} />\n ) : (\n <EmptyView\n title=\"Course not found\"\n description=\"This course may have been unpublished.\"\n />\n )}\n </PageLayout>\n );\n }\n\n const completedLessons = myEnrollment?.lessonsCompleted ?? 0;\n const remaining = Math.max(0, lessons.length - completedLessons);\n const milestone =\n myEnrollment && myEnrollment.progressPct >= 100\n ? 'Course complete — claim your certificate below.'\n : remaining === 1\n ? '1 lesson to go — almost there!'\n : `${remaining} lessons to go — keep it up!`;\n const heroSeed = `${course.topic ?? ''} ${course.title}`;\n\n return (\n <PageLayout\n backPath=\"/courses/courses-page\"\n title={course.title}\n description={course.summary ?? undefined}\n actions={\n myEnrollment ? (\n <Button\n onClick={handleProgress}\n disabled={savingProgress || completedLessons >= lessons.length}\n >\n <CheckCircle2 size={16} />\n {completedLessons >= lessons.length ? 'Completed' : 'Complete next lesson'}\n </Button>\n ) : (\n <Button onClick={handleEnroll} disabled={enrolling}>\n <BookOpen size={16} /> {enrolling ? 'Enrolling…' : 'Enroll'}\n </Button>\n )\n }\n >\n {/* Bold hero banner */}\n <MediaCover\n height=\"h-40\"\n seed={heroSeed}\n icon={topicIcon(course.topic ?? course.title)}\n className=\"mb-5 rounded-2xl\"\n >\n <div className=\"flex w-full flex-col gap-2\">\n <span className=\"text-xs font-medium uppercase tracking-wide text-white/80\">\n {course.topic ?? 'Course'}\n </span>\n <h2 className=\"text-2xl font-bold leading-tight text-white drop-shadow-sm sm:text-3xl\">\n {course.title}\n </h2>\n <div className=\"flex flex-wrap items-center gap-2\">\n <HeroBadge icon={TrendingUp}>{humanizeEnum(course.level)}</HeroBadge>\n <HeroBadge icon={CheckCircle2}>{humanizeEnum(course.status)}</HeroBadge>\n {course.topic ? <HeroBadge icon={Layers}>{course.topic}</HeroBadge> : null}\n {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={i < completedLessons}\n open={openLessonId === lesson.id}\n onToggle={() => setOpenLessonId(openLessonId === lesson.id ? null : lesson.id)}\n />\n ))}\n </div>\n )}\n </GlassCard>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
@@ -9,8 +9,8 @@ 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 { PremiumBadge as m } from "../components/UpgradeGate.js";
|
|
12
|
-
import "../components/index.js";
|
|
13
12
|
import { pageArg as h, useCoursesQuery as g, usePageLimit as _ } from "../api/technospamApi.js";
|
|
13
|
+
import "../components/index.js";
|
|
14
14
|
import { useMemo as v, useState as y } from "react";
|
|
15
15
|
import { BookOpen as b, Clock as x, GraduationCap as S, Layers as C, Search as w, Sparkles as T } from "lucide-react";
|
|
16
16
|
import { jsx as E, jsxs as D } from "react/jsx-runtime";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CoursesMyLearningPagePage.d.ts","sourceRoot":"","sources":["../../../src/technospam/pages/CoursesMyLearningPagePage.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"CoursesMyLearningPagePage.d.ts","sourceRoot":"","sources":["../../../src/technospam/pages/CoursesMyLearningPagePage.tsx"],"names":[],"mappings":"AA+FA,wBAAgB,yBAAyB,gCAsJxC"}
|
|
@@ -6,178 +6,188 @@ import { coverGradient as a, topicIcon as o } from "../components/MediaCover.js"
|
|
|
6
6
|
import { formatDate as s, relativeTime as c } from "../utils/format.js";
|
|
7
7
|
import { StatusBadge as l } from "../components/StatusBadge.js";
|
|
8
8
|
import { EmptyView as u, ErrorView as d, LoadingRows as f } from "../components/StateViews.js";
|
|
9
|
-
import "../components/index.js";
|
|
10
9
|
import { useEnrollmentsQuery as p, useMyCertificatesQuery as m } from "../api/technospamApi.js";
|
|
11
|
-
import
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
10
|
+
import "../components/index.js";
|
|
11
|
+
import { FOCUS_PARAMS as h } from "../utils/pageContext.js";
|
|
12
|
+
import { focusableCardProps as g, useFocusedRecordParam as _ } from "../hooks/useFocusedRecordParam.js";
|
|
13
|
+
import { createElement as v } from "react";
|
|
14
|
+
import { Award as y, BookOpen as b, GraduationCap as x, PlayCircle as S, ShieldCheck as C } from "lucide-react";
|
|
15
|
+
import { jsx as w, jsxs as T } from "react/jsx-runtime";
|
|
16
|
+
import { Button as E, GlassCard as D } from "@burdenoff/fe-libs/ui";
|
|
15
17
|
//#region src/technospam/pages/CoursesMyLearningPagePage.tsx
|
|
16
|
-
function
|
|
17
|
-
let
|
|
18
|
-
return /* @__PURE__ */
|
|
19
|
-
className: "group flex cursor-pointer flex-col gap-3 p-4 transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md sm:flex-row sm:items-center sm:gap-4",
|
|
20
|
-
|
|
18
|
+
function O({ e, onOpen: t, focused: r, onToggle: i }) {
|
|
19
|
+
let s = `${e.course?.topic ?? ""} ${e.course?.title ?? "Course"}`;
|
|
20
|
+
return /* @__PURE__ */ T(D, {
|
|
21
|
+
className: n("group flex cursor-pointer flex-col gap-3 p-4 transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md sm:flex-row sm:items-center sm:gap-4", r && "ring-2 ring-accent-primary"),
|
|
22
|
+
...g(r, i),
|
|
21
23
|
children: [
|
|
22
|
-
/* @__PURE__ */
|
|
23
|
-
className: n("flex h-12 w-12 shrink-0 items-center justify-center rounded-xl bg-gradient-to-br text-white shadow-sm transition-transform duration-200 group-hover:scale-105", a(
|
|
24
|
-
children:
|
|
24
|
+
/* @__PURE__ */ w("span", {
|
|
25
|
+
className: n("flex h-12 w-12 shrink-0 items-center justify-center rounded-xl bg-gradient-to-br text-white shadow-sm transition-transform duration-200 group-hover:scale-105", a(s)),
|
|
26
|
+
children: v(o(s), { size: 20 })
|
|
25
27
|
}),
|
|
26
|
-
/* @__PURE__ */
|
|
28
|
+
/* @__PURE__ */ T("div", {
|
|
27
29
|
className: "min-w-0 flex-1",
|
|
28
30
|
children: [
|
|
29
|
-
/* @__PURE__ */
|
|
31
|
+
/* @__PURE__ */ T("div", {
|
|
30
32
|
className: "flex flex-wrap items-center gap-2",
|
|
31
|
-
children: [/* @__PURE__ */
|
|
32
|
-
|
|
33
|
+
children: [/* @__PURE__ */ w("button", {
|
|
34
|
+
type: "button",
|
|
35
|
+
"aria-pressed": r,
|
|
36
|
+
onClick: i,
|
|
37
|
+
className: "truncate text-left text-sm font-semibold text-text-primary group-hover:text-accent-primary",
|
|
33
38
|
children: e.course?.title ?? "Course"
|
|
34
|
-
}), /* @__PURE__ */
|
|
39
|
+
}), /* @__PURE__ */ w(l, { status: e.status })]
|
|
35
40
|
}),
|
|
36
|
-
/* @__PURE__ */
|
|
41
|
+
/* @__PURE__ */ T("div", {
|
|
37
42
|
className: "mt-1 flex flex-wrap items-center gap-2 text-xs text-text-muted",
|
|
38
43
|
children: [
|
|
39
|
-
/* @__PURE__ */
|
|
40
|
-
/* @__PURE__ */
|
|
44
|
+
/* @__PURE__ */ T("span", { children: [e.lessonsCompleted, " lessons done"] }),
|
|
45
|
+
/* @__PURE__ */ w("span", {
|
|
41
46
|
"aria-hidden": !0,
|
|
42
47
|
children: "·"
|
|
43
48
|
}),
|
|
44
|
-
/* @__PURE__ */
|
|
49
|
+
/* @__PURE__ */ T("span", { children: ["Active ", c(e.lastActiveAt ?? e.startedAt)] })
|
|
45
50
|
]
|
|
46
51
|
}),
|
|
47
|
-
/* @__PURE__ */
|
|
52
|
+
/* @__PURE__ */ w("div", {
|
|
48
53
|
className: "mt-2 h-1.5 w-full overflow-hidden rounded-full bg-bg-sunken",
|
|
49
|
-
children: /* @__PURE__ */
|
|
54
|
+
children: /* @__PURE__ */ w("div", {
|
|
50
55
|
className: "h-full rounded-full bg-gradient-to-r from-accent-primary via-accent-indigo to-accent-purple transition-all duration-500",
|
|
51
56
|
style: { width: `${e.progressPct}%` }
|
|
52
57
|
})
|
|
53
58
|
})
|
|
54
59
|
]
|
|
55
60
|
}),
|
|
56
|
-
/* @__PURE__ */
|
|
61
|
+
/* @__PURE__ */ T("div", {
|
|
57
62
|
className: "flex items-center justify-between gap-3 sm:flex-col sm:items-end sm:justify-center",
|
|
58
|
-
children: [/* @__PURE__ */
|
|
63
|
+
children: [/* @__PURE__ */ T("div", {
|
|
59
64
|
className: "text-lg font-semibold tabular-nums text-text-primary",
|
|
60
65
|
children: [e.progressPct, "%"]
|
|
61
|
-
}), /* @__PURE__ */
|
|
66
|
+
}), /* @__PURE__ */ T(E, {
|
|
62
67
|
variant: "ghost",
|
|
63
68
|
size: "sm",
|
|
64
|
-
onClick:
|
|
65
|
-
|
|
66
|
-
},
|
|
67
|
-
children: [/* @__PURE__ */ x(y, { size: 15 }), " Resume"]
|
|
69
|
+
onClick: t,
|
|
70
|
+
children: [/* @__PURE__ */ w(S, { size: 15 }), " Resume"]
|
|
68
71
|
})]
|
|
69
72
|
})
|
|
70
73
|
]
|
|
71
74
|
});
|
|
72
75
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
+
var k = [h.certificate], A = [h.enrollment];
|
|
77
|
+
function j() {
|
|
78
|
+
let a = t(), { currentUser: o } = e(), c = o?.id, { data: v, loading: j, error: M } = p(c ? { learnerId: c } : {}), { data: N } = m(), [P, F] = _(h.enrollment, k), [I, L] = _(h.certificate, A), R = v?.tecListEnrollments?.items ?? [], z = N?.tecMyCertificates ?? [], B = R.filter((e) => e.status === "COMPLETED").length, V = R.filter((e) => e.status === "IN_PROGRESS" || e.status === "ENROLLED").length;
|
|
79
|
+
return /* @__PURE__ */ T(r, {
|
|
76
80
|
hero: !0,
|
|
77
81
|
title: "My Learning",
|
|
78
82
|
description: "Track your enrolled courses, progress, and earned certificates.",
|
|
79
|
-
actions: /* @__PURE__ */
|
|
83
|
+
actions: /* @__PURE__ */ T(E, {
|
|
80
84
|
variant: "outline",
|
|
81
|
-
onClick: () =>
|
|
82
|
-
children: [/* @__PURE__ */
|
|
85
|
+
onClick: () => a("/courses/courses-page"),
|
|
86
|
+
children: [/* @__PURE__ */ w(b, { size: 16 }), " Browse catalog"]
|
|
83
87
|
}),
|
|
84
88
|
children: [
|
|
85
|
-
/* @__PURE__ */
|
|
89
|
+
/* @__PURE__ */ T("div", {
|
|
86
90
|
className: "mb-6 grid grid-cols-2 gap-3 sm:grid-cols-4",
|
|
87
91
|
children: [
|
|
88
|
-
/* @__PURE__ */
|
|
92
|
+
/* @__PURE__ */ w(i, {
|
|
89
93
|
label: "Enrolled",
|
|
90
|
-
value:
|
|
94
|
+
value: R.length,
|
|
91
95
|
hint: "Courses you've enrolled in",
|
|
92
|
-
icon:
|
|
96
|
+
icon: b,
|
|
93
97
|
tone: "blue"
|
|
94
98
|
}),
|
|
95
|
-
/* @__PURE__ */
|
|
99
|
+
/* @__PURE__ */ w(i, {
|
|
96
100
|
label: "In progress",
|
|
97
|
-
value:
|
|
101
|
+
value: V,
|
|
98
102
|
hint: "Courses in progress",
|
|
99
|
-
icon:
|
|
103
|
+
icon: S,
|
|
100
104
|
tone: "indigo"
|
|
101
105
|
}),
|
|
102
|
-
/* @__PURE__ */
|
|
106
|
+
/* @__PURE__ */ w(i, {
|
|
103
107
|
label: "Completed",
|
|
104
|
-
value:
|
|
108
|
+
value: B,
|
|
105
109
|
hint: "Courses completed",
|
|
106
|
-
icon:
|
|
110
|
+
icon: x,
|
|
107
111
|
tone: "green"
|
|
108
112
|
}),
|
|
109
|
-
/* @__PURE__ */
|
|
113
|
+
/* @__PURE__ */ w(i, {
|
|
110
114
|
label: "Certificates",
|
|
111
|
-
value:
|
|
115
|
+
value: z.length,
|
|
112
116
|
hint: "Certificates earned",
|
|
113
|
-
icon:
|
|
117
|
+
icon: y,
|
|
114
118
|
tone: "purple"
|
|
115
119
|
})
|
|
116
120
|
]
|
|
117
121
|
}),
|
|
118
|
-
|
|
122
|
+
j && !R.length ? /* @__PURE__ */ w(f, {
|
|
119
123
|
count: 3,
|
|
120
124
|
height: "h-24"
|
|
121
|
-
}) :
|
|
125
|
+
}) : M ? /* @__PURE__ */ w(d, {
|
|
122
126
|
title: "Couldn't load your learning",
|
|
123
|
-
error:
|
|
124
|
-
}) :
|
|
125
|
-
icon: /* @__PURE__ */
|
|
127
|
+
error: M
|
|
128
|
+
}) : R.length === 0 ? /* @__PURE__ */ w(u, {
|
|
129
|
+
icon: /* @__PURE__ */ w(b, { size: 26 }),
|
|
126
130
|
title: "You haven't enrolled yet",
|
|
127
131
|
description: "Browse the catalog and enroll in a course to start building skill by doing.",
|
|
128
|
-
action: /* @__PURE__ */
|
|
129
|
-
onClick: () =>
|
|
132
|
+
action: /* @__PURE__ */ w(E, {
|
|
133
|
+
onClick: () => a("/courses/courses-page"),
|
|
130
134
|
children: "Browse courses"
|
|
131
135
|
})
|
|
132
|
-
}) : /* @__PURE__ */
|
|
136
|
+
}) : /* @__PURE__ */ w("div", {
|
|
133
137
|
className: "space-y-3",
|
|
134
|
-
children:
|
|
138
|
+
children: R.map((e) => /* @__PURE__ */ w(O, {
|
|
135
139
|
e,
|
|
136
|
-
onOpen: () =>
|
|
140
|
+
onOpen: () => a(`/courses/course-details-page?id=${e.courseId}`),
|
|
141
|
+
focused: P === e.id,
|
|
142
|
+
onToggle: () => F(P === e.id ? null : e.id)
|
|
137
143
|
}, e.id))
|
|
138
144
|
}),
|
|
139
|
-
|
|
145
|
+
z.length > 0 ? /* @__PURE__ */ T("div", {
|
|
140
146
|
className: "mt-8",
|
|
141
|
-
children: [/* @__PURE__ */
|
|
147
|
+
children: [/* @__PURE__ */ T("h2", {
|
|
142
148
|
className: "mb-3 flex items-center gap-2 text-base font-semibold text-text-primary",
|
|
143
|
-
children: [/* @__PURE__ */
|
|
149
|
+
children: [/* @__PURE__ */ w(y, {
|
|
144
150
|
size: 18,
|
|
145
151
|
className: "text-accent-teal"
|
|
146
152
|
}), " Certificates"]
|
|
147
|
-
}), /* @__PURE__ */
|
|
153
|
+
}), /* @__PURE__ */ w("div", {
|
|
148
154
|
className: "grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3",
|
|
149
|
-
children:
|
|
150
|
-
className: "group flex flex-col gap-3 p-4 transition-all duration-200 hover:-translate-y-1 hover:shadow-lg",
|
|
151
|
-
|
|
155
|
+
children: z.map((e) => /* @__PURE__ */ T(D, {
|
|
156
|
+
className: n("group flex cursor-pointer flex-col gap-3 p-4 transition-all duration-200 hover:-translate-y-1 hover:shadow-lg", I === e.id && "ring-2 ring-accent-primary"),
|
|
157
|
+
...g(I === e.id, () => L(I === e.id ? null : e.id)),
|
|
158
|
+
children: [/* @__PURE__ */ T("div", {
|
|
152
159
|
className: "flex items-center gap-3",
|
|
153
|
-
children: [/* @__PURE__ */
|
|
160
|
+
children: [/* @__PURE__ */ w("span", {
|
|
154
161
|
className: "flex h-11 w-11 shrink-0 items-center justify-center rounded-xl bg-status-warning-bg-subtle text-status-warning-text ring-1 ring-status-warning-border/30 transition-transform duration-200 group-hover:scale-105",
|
|
155
|
-
children: /* @__PURE__ */
|
|
156
|
-
}), /* @__PURE__ */
|
|
162
|
+
children: /* @__PURE__ */ w(y, { size: 22 })
|
|
163
|
+
}), /* @__PURE__ */ T("div", {
|
|
157
164
|
className: "min-w-0",
|
|
158
|
-
children: [/* @__PURE__ */
|
|
159
|
-
|
|
165
|
+
children: [/* @__PURE__ */ w("button", {
|
|
166
|
+
type: "button",
|
|
167
|
+
"aria-pressed": I === e.id,
|
|
168
|
+
onClick: () => L(I === e.id ? null : e.id),
|
|
169
|
+
className: "block max-w-full truncate text-left text-sm font-medium text-text-primary",
|
|
160
170
|
children: e.course?.title ?? "Certificate"
|
|
161
|
-
}), /* @__PURE__ */
|
|
171
|
+
}), /* @__PURE__ */ T("div", {
|
|
162
172
|
className: "truncate text-xs text-text-muted",
|
|
163
173
|
children: ["Serial ", e.serial]
|
|
164
174
|
})]
|
|
165
175
|
})]
|
|
166
|
-
}), /* @__PURE__ */
|
|
176
|
+
}), /* @__PURE__ */ T("div", {
|
|
167
177
|
className: "mt-auto flex items-center justify-between border-t border-border-seam pt-3 text-xs text-text-muted",
|
|
168
|
-
children: [/* @__PURE__ */
|
|
178
|
+
children: [/* @__PURE__ */ T("span", { children: [
|
|
169
179
|
"Score ",
|
|
170
|
-
/* @__PURE__ */
|
|
180
|
+
/* @__PURE__ */ w("span", {
|
|
171
181
|
className: "font-semibold text-text-primary",
|
|
172
182
|
children: e.score
|
|
173
183
|
}),
|
|
174
184
|
" ·",
|
|
175
185
|
" ",
|
|
176
186
|
s(e.issuedAt)
|
|
177
|
-
] }), e.status === "ISSUED" ? /* @__PURE__ */
|
|
187
|
+
] }), e.status === "ISSUED" ? /* @__PURE__ */ T("span", {
|
|
178
188
|
className: "inline-flex items-center gap-1 rounded-full border border-status-success-border bg-status-success-bg-subtle px-2 py-0.5 font-medium text-status-success-text",
|
|
179
|
-
children: [/* @__PURE__ */
|
|
180
|
-
}) : /* @__PURE__ */
|
|
189
|
+
children: [/* @__PURE__ */ w(C, { size: 12 }), " Verified"]
|
|
190
|
+
}) : /* @__PURE__ */ w(l, { status: e.status })]
|
|
181
191
|
})]
|
|
182
192
|
}, e.id))
|
|
183
193
|
})]
|
|
@@ -186,6 +196,6 @@ function E() {
|
|
|
186
196
|
});
|
|
187
197
|
}
|
|
188
198
|
//#endregion
|
|
189
|
-
export {
|
|
199
|
+
export { j as CoursesMyLearningPagePage };
|
|
190
200
|
|
|
191
201
|
//# sourceMappingURL=CoursesMyLearningPagePage.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CoursesMyLearningPagePage.js","names":[],"sources":["../../../src/technospam/pages/CoursesMyLearningPagePage.tsx"],"sourcesContent":["import { createElement } from 'react';\nimport { Award, BookOpen, GraduationCap, PlayCircle, ShieldCheck } from 'lucide-react';\nimport { Button, GlassCard } from '@burdenoff/fe-libs/ui';\nimport {\n EmptyView,\n ErrorView,\n LoadingRows,\n PageLayout,\n StatCard,\n StatusBadge,\n coverGradient,\n topicIcon,\n} from '../components';\nimport { useTechnoSpam, useTechnoSpamNavigate } from '../providers/TechnoSpamProvider';\nimport {\n useEnrollmentsQuery,\n useMyCertificatesQuery,\n type TecEnrollment,\n} from '../api/technospamApi';\nimport { formatDate, relativeTime } from '../utils/format';\nimport { cn } from '../utils/cn';\n\nfunction EnrollmentRow({ e, onOpen }: { e: TecEnrollment; onOpen: () => void }) {\n const seed = `${e.course?.topic ?? ''} ${e.course?.title ?? 'Course'}`;\n return (\n <GlassCard\n className=\"group flex cursor-pointer flex-col gap-3 p-4 transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md sm:flex-row sm:items-center sm:gap-4\"\n onClick={onOpen}\n >\n <span\n className={cn(\n 'flex h-12 w-12 shrink-0 items-center justify-center rounded-xl bg-gradient-to-br text-white shadow-sm transition-transform duration-200 group-hover:scale-105',\n coverGradient(seed)\n )}\n >\n {createElement(topicIcon(seed), { size: 20 })}\n </span>\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <span className=\"truncate text-sm font-semibold text-text-primary group-hover:text-accent-primary\">\n {e.course?.title ?? 'Course'}\n </span>\n <StatusBadge status={e.status} />\n </div>\n <div className=\"mt-1 flex flex-wrap items-center gap-2 text-xs text-text-muted\">\n <span>{e.lessonsCompleted} lessons done</span>\n <span aria-hidden>·</span>\n <span>Active {relativeTime(e.lastActiveAt ?? e.startedAt)}</span>\n </div>\n <div className=\"mt-2 h-1.5 w-full overflow-hidden rounded-full bg-bg-sunken\">\n <div\n className=\"h-full rounded-full bg-gradient-to-r from-accent-primary via-accent-indigo to-accent-purple transition-all duration-500\"\n style={{ width: `${e.progressPct}%` }}\n />\n </div>\n </div>\n <div className=\"flex items-center justify-between gap-3 sm:flex-col sm:items-end sm:justify-center\">\n <div className=\"text-lg font-semibold tabular-nums text-text-primary\">{e.progressPct}%</div>\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={(ev) => {\n ev.stopPropagation();\n onOpen();\n }}\n >\n <PlayCircle size={15} /> Resume\n </Button>\n </div>\n </GlassCard>\n );\n}\n\nexport function CoursesMyLearningPagePage() {\n const navigate = useTechnoSpamNavigate();\n const { currentUser } = useTechnoSpam();\n const learnerId = currentUser?.id;\n\n const { data, loading, error } = useEnrollmentsQuery(learnerId ? { learnerId } : {});\n // Self-scoped read — `tecListCertificates` needs the cross-learner\n // \"list-all\" action a plain learner does not hold, so their own earned\n // certificates never arrived here.\n const { data: certData } = useMyCertificatesQuery();\n\n const enrollments = data?.tecListEnrollments?.items ?? [];\n const certificates = certData?.tecMyCertificates ?? [];\n const completed = enrollments.filter((e) => e.status === 'COMPLETED').length;\n const inProgress = enrollments.filter(\n (e) => e.status === 'IN_PROGRESS' || e.status === 'ENROLLED'\n ).length;\n\n return (\n <PageLayout\n hero\n title=\"My Learning\"\n description=\"Track your enrolled courses, progress, and earned certificates.\"\n actions={\n <Button variant=\"outline\" onClick={() => navigate('/courses/courses-page')}>\n <BookOpen size={16} /> Browse catalog\n </Button>\n }\n >\n <div className=\"mb-6 grid grid-cols-2 gap-3 sm:grid-cols-4\">\n <StatCard\n label=\"Enrolled\"\n value={enrollments.length}\n hint=\"Courses you've enrolled in\"\n icon={BookOpen}\n tone=\"blue\"\n />\n <StatCard\n label=\"In progress\"\n value={inProgress}\n hint=\"Courses in progress\"\n icon={PlayCircle}\n tone=\"indigo\"\n />\n <StatCard\n label=\"Completed\"\n value={completed}\n hint=\"Courses completed\"\n icon={GraduationCap}\n tone=\"green\"\n />\n <StatCard\n label=\"Certificates\"\n value={certificates.length}\n hint=\"Certificates earned\"\n icon={Award}\n tone=\"purple\"\n />\n </div>\n\n {loading && !enrollments.length ? (\n <LoadingRows count={3} height=\"h-24\" />\n ) : error ? (\n <ErrorView title=\"Couldn't load your learning\" error={error} />\n ) : enrollments.length === 0 ? (\n <EmptyView\n icon={<BookOpen size={26} />}\n title=\"You haven't enrolled yet\"\n description=\"Browse the catalog and enroll in a course to start building skill by doing.\"\n action={<Button onClick={() => navigate('/courses/courses-page')}>Browse courses</Button>}\n />\n ) : (\n <div className=\"space-y-3\">\n {enrollments.map((e) => (\n <EnrollmentRow\n key={e.id}\n e={e}\n onOpen={() => navigate(`/courses/course-details-page?id=${e.courseId}`)}\n />\n ))}\n </div>\n )}\n\n {certificates.length > 0 ? (\n <div className=\"mt-8\">\n <h2 className=\"mb-3 flex items-center gap-2 text-base font-semibold text-text-primary\">\n <Award size={18} className=\"text-accent-teal\" /> Certificates\n </h2>\n <div className=\"grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3\">\n {certificates.map((c) => (\n <GlassCard\n key={c.id}\n className=\"group flex flex-col gap-3 p-4 transition-all duration-200 hover:-translate-y-1 hover:shadow-lg\"\n >\n <div className=\"flex items-center gap-3\">\n <span className=\"flex h-11 w-11 shrink-0 items-center justify-center rounded-xl bg-status-warning-bg-subtle text-status-warning-text ring-1 ring-status-warning-border/30 transition-transform duration-200 group-hover:scale-105\">\n <Award size={22} />\n </span>\n <div className=\"min-w-0\">\n <div className=\"truncate text-sm font-medium text-text-primary\">\n {c.course?.title ?? 'Certificate'}\n </div>\n <div className=\"truncate text-xs text-text-muted\">Serial {c.serial}</div>\n </div>\n </div>\n <div className=\"mt-auto flex items-center justify-between border-t border-border-seam pt-3 text-xs text-text-muted\">\n <span>\n Score <span className=\"font-semibold text-text-primary\">{c.score}</span> ·{' '}\n {formatDate(c.issuedAt)}\n </span>\n {c.status === 'ISSUED' ? (\n <span className=\"inline-flex items-center gap-1 rounded-full border border-status-success-border bg-status-success-bg-subtle px-2 py-0.5 font-medium text-status-success-text\">\n <ShieldCheck size={12} /> Verified\n </span>\n ) : (\n <StatusBadge status={c.status} />\n )}\n </div>\n </GlassCard>\n ))}\n </div>\n </div>\n ) : null}\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CoursesMyLearningPagePage.js","names":[],"sources":["../../../src/technospam/pages/CoursesMyLearningPagePage.tsx"],"sourcesContent":["import { createElement } from 'react';\nimport { Award, BookOpen, GraduationCap, PlayCircle, ShieldCheck } from 'lucide-react';\nimport { Button, GlassCard } from '@burdenoff/fe-libs/ui';\nimport {\n EmptyView,\n ErrorView,\n LoadingRows,\n PageLayout,\n StatCard,\n StatusBadge,\n coverGradient,\n topicIcon,\n} from '../components';\nimport { useTechnoSpam, useTechnoSpamNavigate } from '../providers/TechnoSpamProvider';\nimport {\n useEnrollmentsQuery,\n useMyCertificatesQuery,\n type TecEnrollment,\n} from '../api/technospamApi';\nimport { formatDate, relativeTime } from '../utils/format';\nimport { cn } from '../utils/cn';\nimport {\n FOCUSED_CARD_CLASS,\n focusableCardProps,\n useFocusedRecordParam,\n} from '../hooks/useFocusedRecordParam';\nimport { FOCUS_PARAMS } from '../utils/pageContext';\n\nfunction EnrollmentRow({\n e,\n onOpen,\n focused,\n onToggle,\n}: {\n e: TecEnrollment;\n onOpen: () => void;\n /** The focused enrollment (`?enrollmentId=`) is the one the assistant acts on. */\n focused: boolean;\n onToggle: () => void;\n}) {\n const seed = `${e.course?.topic ?? ''} ${e.course?.title ?? 'Course'}`;\n return (\n <GlassCard\n className={cn(\n 'group flex cursor-pointer flex-col gap-3 p-4 transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md sm:flex-row sm:items-center sm:gap-4',\n focused && FOCUSED_CARD_CLASS\n )}\n {...focusableCardProps(focused, onToggle)}\n >\n <span\n className={cn(\n 'flex h-12 w-12 shrink-0 items-center justify-center rounded-xl bg-gradient-to-br text-white shadow-sm transition-transform duration-200 group-hover:scale-105',\n coverGradient(seed)\n )}\n >\n {createElement(topicIcon(seed), { size: 20 })}\n </span>\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <button\n type=\"button\"\n aria-pressed={focused}\n onClick={onToggle}\n className=\"truncate text-left text-sm font-semibold text-text-primary group-hover:text-accent-primary\"\n >\n {e.course?.title ?? 'Course'}\n </button>\n <StatusBadge status={e.status} />\n </div>\n <div className=\"mt-1 flex flex-wrap items-center gap-2 text-xs text-text-muted\">\n <span>{e.lessonsCompleted} lessons done</span>\n <span aria-hidden>·</span>\n <span>Active {relativeTime(e.lastActiveAt ?? e.startedAt)}</span>\n </div>\n <div className=\"mt-2 h-1.5 w-full overflow-hidden rounded-full bg-bg-sunken\">\n <div\n className=\"h-full rounded-full bg-gradient-to-r from-accent-primary via-accent-indigo to-accent-purple transition-all duration-500\"\n style={{ width: `${e.progressPct}%` }}\n />\n </div>\n </div>\n <div className=\"flex items-center justify-between gap-3 sm:flex-col sm:items-end sm:justify-center\">\n <div className=\"text-lg font-semibold tabular-nums text-text-primary\">{e.progressPct}%</div>\n <Button variant=\"ghost\" size=\"sm\" onClick={onOpen}>\n <PlayCircle size={15} /> Resume\n </Button>\n </div>\n </GlassCard>\n );\n}\n\n// Stable sibling lists: focusing an enrollment clears a focused certificate and vice versa.\nconst CERTIFICATE_SIBLING = [FOCUS_PARAMS.certificate] as const;\nconst ENROLLMENT_SIBLING = [FOCUS_PARAMS.enrollment] as const;\n\nexport function CoursesMyLearningPagePage() {\n const navigate = useTechnoSpamNavigate();\n const { currentUser } = useTechnoSpam();\n const learnerId = currentUser?.id;\n\n const { data, loading, error } = useEnrollmentsQuery(learnerId ? { learnerId } : {});\n // Self-scoped read — `tecListCertificates` needs the cross-learner\n // \"list-all\" action a plain learner does not hold, so their own earned\n // certificates never arrived here.\n const { data: certData } = useMyCertificatesQuery();\n // The focused enrollment / certificate live in the URL so the assistant's\n // page context names them (\"how far am I in this course?\").\n const [focusedEnrollmentId, setFocusedEnrollmentId] = useFocusedRecordParam(\n FOCUS_PARAMS.enrollment,\n CERTIFICATE_SIBLING\n );\n const [focusedCertificateId, setFocusedCertificateId] = useFocusedRecordParam(\n FOCUS_PARAMS.certificate,\n ENROLLMENT_SIBLING\n );\n\n const enrollments = data?.tecListEnrollments?.items ?? [];\n const certificates = certData?.tecMyCertificates ?? [];\n const completed = enrollments.filter((e) => e.status === 'COMPLETED').length;\n const inProgress = enrollments.filter(\n (e) => e.status === 'IN_PROGRESS' || e.status === 'ENROLLED'\n ).length;\n\n return (\n <PageLayout\n hero\n title=\"My Learning\"\n description=\"Track your enrolled courses, progress, and earned certificates.\"\n actions={\n <Button variant=\"outline\" onClick={() => navigate('/courses/courses-page')}>\n <BookOpen size={16} /> Browse catalog\n </Button>\n }\n >\n <div className=\"mb-6 grid grid-cols-2 gap-3 sm:grid-cols-4\">\n <StatCard\n label=\"Enrolled\"\n value={enrollments.length}\n hint=\"Courses you've enrolled in\"\n icon={BookOpen}\n tone=\"blue\"\n />\n <StatCard\n label=\"In progress\"\n value={inProgress}\n hint=\"Courses in progress\"\n icon={PlayCircle}\n tone=\"indigo\"\n />\n <StatCard\n label=\"Completed\"\n value={completed}\n hint=\"Courses completed\"\n icon={GraduationCap}\n tone=\"green\"\n />\n <StatCard\n label=\"Certificates\"\n value={certificates.length}\n hint=\"Certificates earned\"\n icon={Award}\n tone=\"purple\"\n />\n </div>\n\n {loading && !enrollments.length ? (\n <LoadingRows count={3} height=\"h-24\" />\n ) : error ? (\n <ErrorView title=\"Couldn't load your learning\" error={error} />\n ) : enrollments.length === 0 ? (\n <EmptyView\n icon={<BookOpen size={26} />}\n title=\"You haven't enrolled yet\"\n description=\"Browse the catalog and enroll in a course to start building skill by doing.\"\n action={<Button onClick={() => navigate('/courses/courses-page')}>Browse courses</Button>}\n />\n ) : (\n <div className=\"space-y-3\">\n {enrollments.map((e) => (\n <EnrollmentRow\n key={e.id}\n e={e}\n onOpen={() => navigate(`/courses/course-details-page?id=${e.courseId}`)}\n focused={focusedEnrollmentId === e.id}\n onToggle={() => setFocusedEnrollmentId(focusedEnrollmentId === e.id ? null : e.id)}\n />\n ))}\n </div>\n )}\n\n {certificates.length > 0 ? (\n <div className=\"mt-8\">\n <h2 className=\"mb-3 flex items-center gap-2 text-base font-semibold text-text-primary\">\n <Award size={18} className=\"text-accent-teal\" /> Certificates\n </h2>\n <div className=\"grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3\">\n {certificates.map((c) => (\n <GlassCard\n key={c.id}\n className={cn(\n 'group flex cursor-pointer flex-col gap-3 p-4 transition-all duration-200 hover:-translate-y-1 hover:shadow-lg',\n focusedCertificateId === c.id && FOCUSED_CARD_CLASS\n )}\n {...focusableCardProps(focusedCertificateId === c.id, () =>\n setFocusedCertificateId(focusedCertificateId === c.id ? null : c.id)\n )}\n >\n <div className=\"flex items-center gap-3\">\n <span className=\"flex h-11 w-11 shrink-0 items-center justify-center rounded-xl bg-status-warning-bg-subtle text-status-warning-text ring-1 ring-status-warning-border/30 transition-transform duration-200 group-hover:scale-105\">\n <Award size={22} />\n </span>\n <div className=\"min-w-0\">\n <button\n type=\"button\"\n aria-pressed={focusedCertificateId === c.id}\n onClick={() =>\n setFocusedCertificateId(focusedCertificateId === c.id ? null : c.id)\n }\n className=\"block max-w-full truncate text-left text-sm font-medium text-text-primary\"\n >\n {c.course?.title ?? 'Certificate'}\n </button>\n <div className=\"truncate text-xs text-text-muted\">Serial {c.serial}</div>\n </div>\n </div>\n <div className=\"mt-auto flex items-center justify-between border-t border-border-seam pt-3 text-xs text-text-muted\">\n <span>\n Score <span className=\"font-semibold text-text-primary\">{c.score}</span> ·{' '}\n {formatDate(c.issuedAt)}\n </span>\n {c.status === 'ISSUED' ? (\n <span className=\"inline-flex items-center gap-1 rounded-full border border-status-success-border bg-status-success-bg-subtle px-2 py-0.5 font-medium text-status-success-text\">\n <ShieldCheck size={12} /> Verified\n </span>\n ) : (\n <StatusBadge status={c.status} />\n )}\n </div>\n </GlassCard>\n ))}\n </div>\n </div>\n ) : null}\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
@@ -6,8 +6,8 @@ import { MediaCover as a, topicIcon as o } from "../components/MediaCover.js";
|
|
|
6
6
|
import { formatDate as s, formatDateTime as c, formatMoney as l } from "../utils/format.js";
|
|
7
7
|
import { StatusBadge as u } from "../components/StatusBadge.js";
|
|
8
8
|
import { EmptyView as d, ErrorView as f, LoadingRows as p } from "../components/StateViews.js";
|
|
9
|
-
import "../components/index.js";
|
|
10
9
|
import { useCertificatesQuery as ee, useCoursesQuery as m, useEnrollmentsQuery as h, useHackathonsQuery as g, useSessionsQuery as _, useSubmissionsQuery as v } from "../api/technospamApi.js";
|
|
10
|
+
import "../components/index.js";
|
|
11
11
|
import { useMemo as y, useState as b } from "react";
|
|
12
12
|
import { ArrowRight as x, Award as S, BookOpen as C, Calendar as w, CalendarClock as T, CheckCircle2 as E, Clock as D, Code2 as O, GraduationCap as k, PlayCircle as A, Trophy as j, Users as te } from "lucide-react";
|
|
13
13
|
import { jsx as M, jsxs as N } from "react/jsx-runtime";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"HackathonsHackathonDetailsPagePage.d.ts","sourceRoot":"","sources":["../../../src/technospam/pages/HackathonsHackathonDetailsPagePage.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"HackathonsHackathonDetailsPagePage.d.ts","sourceRoot":"","sources":["../../../src/technospam/pages/HackathonsHackathonDetailsPagePage.tsx"],"names":[],"mappings":"AAgKA,wBAAgB,kCAAkC,gCA0VjD"}
|