@burdenoff/microfe-technospam 2026.827.1 → 2026.829.1
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/index.js +4 -3
- package/dist/technospam/TechnoSpamRoot.js +4 -4
- package/dist/technospam/TechnoSpamRoutes.d.ts.map +1 -1
- package/dist/technospam/TechnoSpamRoutes.js +89 -88
- package/dist/technospam/TechnoSpamRoutes.js.map +1 -1
- package/dist/technospam/components/PageLayout.d.ts.map +1 -1
- package/dist/technospam/components/PageLayout.js +24 -23
- package/dist/technospam/components/PageLayout.js.map +1 -1
- package/dist/technospam/components/StateViews.d.ts +1 -1
- package/dist/technospam/components/StateViews.d.ts.map +1 -1
- package/dist/technospam/components/StateViews.js +28 -26
- package/dist/technospam/components/StateViews.js.map +1 -1
- package/dist/technospam/components/TagInput.d.ts +1 -1
- package/dist/technospam/components/TagInput.d.ts.map +1 -1
- package/dist/technospam/components/TagInput.js +39 -34
- package/dist/technospam/components/TagInput.js.map +1 -1
- package/dist/technospam/components/TechnoSpamErrorBoundary.d.ts +25 -0
- package/dist/technospam/components/TechnoSpamErrorBoundary.d.ts.map +1 -0
- package/dist/technospam/components/TechnoSpamErrorBoundary.js +31 -0
- package/dist/technospam/components/TechnoSpamErrorBoundary.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/useTr.d.ts +17 -0
- package/dist/technospam/hooks/useTr.d.ts.map +1 -0
- package/dist/technospam/hooks/useTr.js +12 -0
- package/dist/technospam/hooks/useTr.js.map +1 -0
- package/dist/technospam/index.d.ts +1 -0
- package/dist/technospam/index.d.ts.map +1 -1
- package/dist/technospam/index.js +1 -0
- package/dist/technospam/pages/AdminAdminDashboardPagePage.js +7 -7
- package/dist/technospam/pages/AdminAdminDashboardPagePage.js.map +1 -1
- package/dist/technospam/pages/AnalyticsAnalyticsPagePage.d.ts.map +1 -1
- package/dist/technospam/pages/AnalyticsAnalyticsPagePage.js +198 -189
- package/dist/technospam/pages/AnalyticsAnalyticsPagePage.js.map +1 -1
- package/dist/technospam/pages/ApplicationsPortfolioPagePage.js +2 -2
- package/dist/technospam/pages/ApplicationsPortfolioPagePage.js.map +1 -1
- package/dist/technospam/pages/ApplicationsProblemDetailsPagePage.js +1 -1
- package/dist/technospam/pages/ApplicationsProblemDetailsPagePage.js.map +1 -1
- package/dist/technospam/pages/ApplicationsProblemsPagePage.d.ts.map +1 -1
- package/dist/technospam/pages/ApplicationsProblemsPagePage.js +165 -163
- package/dist/technospam/pages/ApplicationsProblemsPagePage.js.map +1 -1
- package/dist/technospam/pages/CoursesCoursesPagePage.d.ts.map +1 -1
- package/dist/technospam/pages/CoursesCoursesPagePage.js +117 -113
- package/dist/technospam/pages/CoursesCoursesPagePage.js.map +1 -1
- package/dist/technospam/pages/CoursesMyLearningPagePage.js +1 -1
- package/dist/technospam/pages/CoursesMyLearningPagePage.js.map +1 -1
- package/dist/technospam/pages/DashboardDashboardPagePage.d.ts.map +1 -1
- package/dist/technospam/pages/DashboardDashboardPagePage.js +211 -193
- package/dist/technospam/pages/DashboardDashboardPagePage.js.map +1 -1
- package/dist/technospam/pages/HackathonsHackathonDetailsPagePage.js +1 -1
- package/dist/technospam/pages/HackathonsHackathonDetailsPagePage.js.map +1 -1
- package/dist/technospam/pages/HackathonsHackathonsPagePage.d.ts.map +1 -1
- package/dist/technospam/pages/HackathonsHackathonsPagePage.js +110 -110
- package/dist/technospam/pages/HackathonsHackathonsPagePage.js.map +1 -1
- package/dist/technospam/pages/MentorshipMentorsPagePage.d.ts.map +1 -1
- package/dist/technospam/pages/MentorshipMentorsPagePage.js +174 -160
- package/dist/technospam/pages/MentorshipMentorsPagePage.js.map +1 -1
- package/dist/technospam/pages/MentorshipSessionsPagePage.d.ts.map +1 -1
- package/dist/technospam/pages/MentorshipSessionsPagePage.js +156 -151
- package/dist/technospam/pages/MentorshipSessionsPagePage.js.map +1 -1
- package/dist/technospam/pages/StudyPlannerStudyPlannerPagePage.js +1 -1
- package/dist/technospam/pages/StudyPlannerStudyPlannerPagePage.js.map +1 -1
- package/dist/technospam/utils/i18n.js +1 -1
- package/dist/technospam/utils/i18n.js.map +1 -1
- package/dist/test/setup.d.ts +2 -0
- package/dist/test/setup.d.ts.map +1 -0
- package/package.json +8 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AnalyticsAnalyticsPagePage.js","names":[],"sources":["../../../src/technospam/pages/AnalyticsAnalyticsPagePage.tsx"],"sourcesContent":["import { useMemo, useState, type CSSProperties, type SVGProps } from 'react';\nimport {\n Activity,\n Award,\n BarChart3,\n BookOpen,\n CalendarRange,\n Code2,\n GraduationCap,\n Library,\n Percent,\n Target,\n TrendingUp,\n Trophy,\n Users,\n X,\n type LucideIcon,\n} from 'lucide-react';\nimport { GlassCard, Button, Input } from '@burdenoff/fe-libs/ui';\nimport { cn } from '../utils/cn';\nimport {\n Area,\n AreaChart,\n Bar,\n BarChart,\n CartesianGrid,\n Cell,\n Legend,\n Pie,\n PieChart,\n ResponsiveContainer,\n Tooltip as ReTooltip,\n XAxis,\n YAxis,\n} from 'recharts';\nimport {\n EmptyView,\n ErrorView,\n LoadingGrid,\n LoadingRows,\n PageLayout,\n StatCard,\n} from '../components';\nimport { useTechnoSpam, useTechnoSpamNavigate } from '../providers/TechnoSpamProvider';\nimport {\n useCertificatesQuery,\n useCoursesQuery,\n useEnrollmentsQuery,\n useHackathonsQuery,\n useProblemsQuery,\n useSessionsQuery,\n useSubmissionsQuery,\n type TecEnrollmentStatus,\n type TecSubmissionVerdict,\n} from '../api/technospamApi';\nimport { humanizeEnum } from '../utils/format';\n\n/* ----------------------------------------------------------- chart config */\n\nconst ENROLL_STATUS_ORDER: TecEnrollmentStatus[] = [\n 'ENROLLED',\n 'IN_PROGRESS',\n 'COMPLETED',\n 'DROPPED',\n];\n\nconst ENROLL_STATUS_COLOR: Record<TecEnrollmentStatus, string> = {\n ENROLLED: 'var(--color-accent-primary)',\n IN_PROGRESS: 'var(--color-accent-teal)',\n COMPLETED: 'var(--color-status-success)',\n DROPPED: 'var(--color-status-warning)',\n};\n\nconst VERDICT_ORDER: TecSubmissionVerdict[] = [\n 'ACCEPTED',\n 'WRONG_ANSWER',\n 'TIME_LIMIT_EXCEEDED',\n 'RUNTIME_ERROR',\n 'COMPILE_ERROR',\n 'PENDING',\n];\n\nconst VERDICT_LABEL: Record<TecSubmissionVerdict, string> = {\n ACCEPTED: 'Accepted',\n WRONG_ANSWER: 'Wrong',\n TIME_LIMIT_EXCEEDED: 'TLE',\n RUNTIME_ERROR: 'Runtime',\n COMPILE_ERROR: 'Compile',\n PENDING: 'Pending',\n};\n\nconst VERDICT_COLOR: Record<TecSubmissionVerdict, string> = {\n ACCEPTED: 'var(--color-status-success)',\n WRONG_ANSWER: 'var(--color-status-error)',\n TIME_LIMIT_EXCEEDED: 'var(--color-status-warning)',\n RUNTIME_ERROR: 'var(--color-status-error)',\n COMPILE_ERROR: 'var(--color-status-error)',\n PENDING: 'var(--color-text-muted)',\n};\n\nconst TOOLTIP_STYLE: CSSProperties = {\n background: 'var(--color-bg-surface)',\n border: '1px solid var(--color-border-seam)',\n borderRadius: 12,\n color: 'var(--color-text-primary)',\n fontSize: 12,\n};\nconst TOOLTIP_LABEL_STYLE: CSSProperties = { color: 'var(--color-text-secondary)' };\nconst AXIS_TICK: SVGProps<SVGTextElement> = { fill: 'var(--color-text-muted)', fontSize: 12 };\n\ninterface StatusDatum {\n name: string;\n value: number;\n color: string;\n}\ninterface VerdictDatum {\n label: string;\n count: number;\n color: string;\n}\ninterface ActivityDatum {\n label: string;\n submissions: number;\n enrollments: number;\n}\ninterface TopicRateDatum {\n topic: string;\n accepted: number;\n attempted: number;\n rate: number;\n}\n\n/** Normalize an ISO timestamp to the start of its (Sunday-based) week. */\nfunction weekBucket(iso: string): { key: number; label: string } {\n const d = new Date(iso);\n if (Number.isNaN(d.getTime())) return { key: 0, label: '—' };\n const start = new Date(d.getFullYear(), d.getMonth(), d.getDate() - d.getDay());\n return {\n key: start.getTime(),\n label: start.toLocaleDateString(undefined, { month: 'short', day: 'numeric' }),\n };\n}\n\n/* -------------------------------------------------------- chart wrappers */\n\ntype ChipTone = 'blue' | 'teal' | 'purple' | 'indigo';\n\nconst CHIP: Record<ChipTone, string> = {\n blue: 'bg-accent-primary/15 text-accent-primary ring-accent-primary/20',\n teal: 'bg-accent-teal/15 text-accent-teal ring-accent-teal/20',\n purple: 'bg-accent-purple/15 text-accent-purple ring-accent-purple/20',\n indigo: 'bg-accent-indigo/15 text-accent-indigo ring-accent-indigo/20',\n};\n\nfunction ChartHeader({\n icon: Icon,\n title,\n subtitle,\n tone,\n}: {\n icon: LucideIcon;\n title: string;\n subtitle?: string;\n tone: ChipTone;\n}) {\n return (\n <div className=\"mb-4 flex items-center gap-3\">\n <span\n className={cn(\n 'flex h-9 w-9 shrink-0 items-center justify-center rounded-xl ring-1',\n CHIP[tone]\n )}\n >\n <Icon size={17} />\n </span>\n <div className=\"min-w-0\">\n <h3 className=\"truncate text-sm font-semibold text-text-primary\">{title}</h3>\n {subtitle ? <p className=\"truncate text-xs text-text-muted\">{subtitle}</p> : null}\n </div>\n </div>\n );\n}\n\nfunction ChartCard({\n title,\n subtitle,\n icon,\n tone = 'teal',\n isEmpty,\n emptyLabel,\n children,\n}: {\n title: string;\n subtitle?: string;\n icon: LucideIcon;\n tone?: ChipTone;\n isEmpty: boolean;\n emptyLabel: string;\n children: React.ReactElement;\n}) {\n return (\n <GlassCard className=\"p-5 transition-shadow duration-200 hover:shadow-lg\">\n <ChartHeader icon={icon} title={title} subtitle={subtitle} tone={tone} />\n {isEmpty ? (\n <div className=\"flex h-[240px] items-center justify-center text-center text-sm text-text-muted\">\n {emptyLabel}\n </div>\n ) : (\n <ResponsiveContainer width=\"100%\" height={260}>\n {children}\n </ResponsiveContainer>\n )}\n </GlassCard>\n );\n}\n\n/**\n * Accepted rate per problem topic.\n *\n * Built entirely from the submissions this page already loads: each\n * TecSubmission carries its `problem { topic }` and its `verdict`, so the\n * rate is `ACCEPTED / attempts` per topic -- a count, not an inference. The\n * lowest row is called out as exactly that (the lowest measured rate), never\n * as a diagnosis or a recommendation.\n */\nfunction TopicAcceptanceCard({ data }: { data: TopicRateDatum[] }) {\n const weakest = data.length > 1 ? data[data.length - 1] : undefined;\n return (\n <GlassCard className=\"p-5 transition-shadow duration-200 hover:shadow-lg\">\n <ChartHeader\n icon={Target}\n title=\"Accepted rate by topic\"\n subtitle=\"Where your solutions land, and where they don't\"\n tone=\"indigo\"\n />\n {data.length === 0 ? (\n <div className=\"flex h-[240px] items-center justify-center text-center text-sm text-text-muted\">\n Solve a practice problem and your per-topic rate builds here\n </div>\n ) : (\n <div className=\"space-y-3\">\n {data.map((row) => {\n const lowest = weakest?.topic === row.topic;\n return (\n <div key={row.topic}>\n <div className=\"mb-1 flex flex-wrap items-baseline gap-x-2\">\n <span className=\"truncate text-sm font-medium text-text-primary\">\n {row.topic}\n </span>\n <span\n className={cn(\n 'text-sm font-semibold tabular-nums',\n lowest ? 'text-status-warning-text' : 'text-text-secondary'\n )}\n >\n {row.rate}%\n </span>\n <span className=\"ml-auto shrink-0 rounded-full bg-bg-sunken px-2 py-0.5 text-xs text-text-muted\">\n {row.accepted} of {row.attempted} accepted\n </span>\n </div>\n <div className=\"h-2.5 w-full overflow-hidden rounded-full bg-bg-sunken\">\n <div\n className={cn(\n 'h-full rounded-full transition-all duration-500',\n lowest ? 'bg-status-warning-text' : 'bg-accent-primary'\n )}\n style={{ width: `${row.rate}%` }}\n />\n </div>\n </div>\n );\n })}\n {weakest ? (\n <p className=\"pt-1 text-xs text-text-muted\">\n Lowest measured rate: <span className=\"text-text-secondary\">{weakest.topic}</span> —{' '}\n {weakest.accepted} of {weakest.attempted} attempts accepted in the loaded range.\n </p>\n ) : null}\n </div>\n )}\n </GlassCard>\n );\n}\n\n/* ---------------------------------------------------------- date range */\n\n/** Convert a native `<input type=\"date\">` value (YYYY-MM-DD) into inclusive\n * ISO bounds so the server-side createdAt filter covers the whole day. */\nfunction dayStartIso(date: string): string | undefined {\n return date ? `${date}T00:00:00.000Z` : undefined;\n}\nfunction dayEndIso(date: string): string | undefined {\n return date ? `${date}T23:59:59.999Z` : undefined;\n}\n\nfunction DateRangeControls({\n start,\n end,\n onStart,\n onEnd,\n onPreset,\n onClear,\n}: {\n start: string;\n end: string;\n onStart: (v: string) => void;\n onEnd: (v: string) => void;\n onPreset: (days: number) => void;\n onClear: () => void;\n}) {\n const active = Boolean(start || end);\n return (\n <GlassCard className=\"mb-6 flex flex-col gap-3 p-4 lg:flex-row lg:items-end lg:justify-between\">\n <div className=\"flex flex-wrap items-end gap-3\">\n <div className=\"flex items-center gap-2 self-center text-sm font-medium text-text-secondary\">\n <CalendarRange size={16} className=\"text-accent-teal\" />\n Date range\n </div>\n <div>\n <label\n className=\"mb-1 block text-xs font-medium text-text-muted\"\n htmlFor=\"tec-range-from\"\n >\n From\n </label>\n <Input\n id=\"tec-range-from\"\n type=\"date\"\n value={start}\n max={end || undefined}\n onChange={(e) => onStart(e.target.value)}\n className=\"w-40\"\n />\n </div>\n <div>\n <label className=\"mb-1 block text-xs font-medium text-text-muted\" htmlFor=\"tec-range-to\">\n To\n </label>\n <Input\n id=\"tec-range-to\"\n type=\"date\"\n value={end}\n min={start || undefined}\n onChange={(e) => onEnd(e.target.value)}\n className=\"w-40\"\n />\n </div>\n {active ? (\n <Button variant=\"ghost\" size=\"sm\" onClick={onClear}>\n <X size={14} /> Clear\n </Button>\n ) : null}\n </div>\n <div className=\"flex flex-wrap gap-2\">\n <Button variant=\"outline\" size=\"sm\" onClick={() => onPreset(7)}>\n Last 7 days\n </Button>\n <Button variant=\"outline\" size=\"sm\" onClick={() => onPreset(30)}>\n Last 30 days\n </Button>\n <Button variant=\"outline\" size=\"sm\" onClick={() => onPreset(90)}>\n Last 90 days\n </Button>\n </div>\n </GlassCard>\n );\n}\n\n/* --------------------------------------------------------------- page */\n\nexport function AnalyticsAnalyticsPagePage() {\n const navigate = useTechnoSpamNavigate();\n const { currentUser } = useTechnoSpam();\n const learnerId = currentUser?.id;\n\n const [rangeStart, setRangeStart] = useState('');\n const [rangeEnd, setRangeEnd] = useState('');\n const startDate = dayStartIso(rangeStart);\n const endDate = dayEndIso(rangeEnd);\n\n const applyPreset = (days: number) => {\n const now = new Date();\n const from = new Date();\n from.setDate(from.getDate() - days);\n setRangeStart(from.toISOString().slice(0, 10));\n setRangeEnd(now.toISOString().slice(0, 10));\n };\n const clearRange = () => {\n setRangeStart('');\n setRangeEnd('');\n };\n\n const dateVars = startDate || endDate ? { startDate, endDate } : {};\n const enrollmentsQ = useEnrollmentsQuery(learnerId ? { learnerId, ...dateVars } : {});\n const submissionsQ = useSubmissionsQuery(learnerId ? { learnerId, ...dateVars } : {});\n const sessionsQ = useSessionsQuery(learnerId ? { menteeId: learnerId } : {});\n const certificatesQ = useCertificatesQuery(learnerId ? { learnerId } : {});\n const coursesQ = useCoursesQuery({ status: 'PUBLISHED' });\n const problemsQ = useProblemsQuery({ status: 'PUBLISHED' });\n const hackathonsQ = useHackathonsQuery({});\n\n const enrollments = useMemo(\n () => enrollmentsQ.data?.tecListEnrollments.items ?? [],\n [enrollmentsQ.data]\n );\n const submissions = useMemo(\n () => submissionsQ.data?.tecListSubmissions.items ?? [],\n [submissionsQ.data]\n );\n const sessions = sessionsQ.data?.tecListSessions.items ?? [];\n const certificates = certificatesQ.data?.tecListCertificates ?? [];\n const coursesTotal = coursesQ.data?.tecListCourses.items.length ?? 0;\n const problemsTotal = problemsQ.data?.tecListProblems.items.length ?? 0;\n const hackathonsTotal = hackathonsQ.data?.tecListHackathons.items.length ?? 0;\n\n const metrics = useMemo(() => {\n const coursesCompleted = enrollments.filter((e) => e.status === 'COMPLETED').length;\n const avgProgress = enrollments.length\n ? Math.round(enrollments.reduce((s, e) => s + e.progressPct, 0) / enrollments.length)\n : 0;\n const acceptedSubs = submissions.filter((s) => s.verdict === 'ACCEPTED');\n const problemsSolved = new Set(acceptedSubs.map((s) => s.problemId)).size;\n const acceptanceRate = submissions.length\n ? Math.round((acceptedSubs.length / submissions.length) * 100)\n : 0;\n return { coursesCompleted, avgProgress, problemsSolved, acceptanceRate };\n }, [enrollments, submissions]);\n\n const enrollmentsByStatus = useMemo<StatusDatum[]>(\n () =>\n ENROLL_STATUS_ORDER.map((s) => ({\n name: humanizeEnum(s),\n value: enrollments.filter((e) => e.status === s).length,\n color: ENROLL_STATUS_COLOR[s],\n })).filter((d) => d.value > 0),\n [enrollments]\n );\n\n const submissionsByVerdict = useMemo<VerdictDatum[]>(\n () =>\n VERDICT_ORDER.map((v) => ({\n label: VERDICT_LABEL[v],\n count: submissions.filter((s) => s.verdict === v).length,\n color: VERDICT_COLOR[v],\n })).filter((d) => d.count > 0),\n [submissions]\n );\n\n const acceptedRateByTopic = useMemo<TopicRateDatum[]>(() => {\n const buckets = new Map<string, { accepted: number; attempted: number }>();\n submissions.forEach((s) => {\n const topic = s.problem?.topic?.trim() || 'Untagged';\n const bucket = buckets.get(topic) ?? { accepted: 0, attempted: 0 };\n bucket.attempted += 1;\n if (s.verdict === 'ACCEPTED') bucket.accepted += 1;\n buckets.set(topic, bucket);\n });\n return [...buckets.entries()]\n .map(([topic, bucket]) => ({\n topic,\n accepted: bucket.accepted,\n attempted: bucket.attempted,\n rate: Math.round((bucket.accepted / bucket.attempted) * 100),\n }))\n .sort((a, b) => b.rate - a.rate || b.attempted - a.attempted);\n }, [submissions]);\n\n const activityData = useMemo<ActivityDatum[]>(() => {\n const buckets = new Map<number, ActivityDatum>();\n const add = (iso: string, key: 'submissions' | 'enrollments') => {\n const { key: bucketKey, label } = weekBucket(iso);\n const existing = buckets.get(bucketKey) ?? { label, submissions: 0, enrollments: 0 };\n existing[key] += 1;\n buckets.set(bucketKey, existing);\n };\n submissions.forEach((s) => add(s.createdAt, 'submissions'));\n enrollments.forEach((e) => add(e.createdAt, 'enrollments'));\n return [...buckets.entries()]\n .sort((a, b) => a[0] - b[0])\n .slice(-8)\n .map(([, v]) => v);\n }, [submissions, enrollments]);\n\n const hasActivity =\n enrollments.length > 0 ||\n submissions.length > 0 ||\n sessions.length > 0 ||\n certificates.length > 0;\n\n const learnerLoading =\n enrollmentsQ.loading || submissionsQ.loading || sessionsQ.loading || certificatesQ.loading;\n const learnerError =\n enrollmentsQ.error || submissionsQ.error || sessionsQ.error || certificatesQ.error;\n const showLoading = learnerLoading && !hasActivity;\n\n const workspaceOverview = (\n <div>\n <h2 className=\"mb-3 text-base font-semibold text-text-primary\">Workspace overview</h2>\n <p className=\"mb-4 max-w-2xl text-sm text-text-secondary\">\n Live content available across the TechnoSpam learning workspace right now.\n </p>\n <div className=\"grid grid-cols-1 gap-3 sm:grid-cols-3\">\n <StatCard label=\"Published courses\" value={coursesTotal} icon={Library} tone=\"blue\" />\n <StatCard label=\"Published problems\" value={problemsTotal} icon={Code2} tone=\"indigo\" />\n <StatCard label=\"Hackathons\" value={hackathonsTotal} icon={Trophy} tone=\"purple\" />\n </div>\n </div>\n );\n\n return (\n <PageLayout\n hero\n title=\"Analytics\"\n description=\"Your real learning performance across courses, problems, mentoring and certificates.\"\n actions={\n <Button variant=\"outline\" onClick={() => navigate('/courses/my-learning-page')}>\n <GraduationCap size={16} /> My Learning\n </Button>\n }\n >\n {!learnerId ? (\n <div className=\"space-y-8\">\n <EmptyView\n icon={<BarChart3 size={26} />}\n title=\"Sign in to view your analytics\"\n description=\"Your personal learning metrics appear here once you're signed in.\"\n />\n {workspaceOverview}\n </div>\n ) : showLoading ? (\n <div className=\"space-y-6\">\n <LoadingGrid count={4} height=\"h-24\" />\n <LoadingRows count={2} height=\"h-72\" />\n </div>\n ) : learnerError && !hasActivity ? (\n <ErrorView title=\"Couldn't load your analytics\" error={learnerError} />\n ) : (\n <div className=\"space-y-8\">\n <DateRangeControls\n start={rangeStart}\n end={rangeEnd}\n onStart={setRangeStart}\n onEnd={setRangeEnd}\n onPreset={applyPreset}\n onClear={clearRange}\n />\n <div className=\"grid grid-cols-2 gap-3 sm:grid-cols-3 xl:grid-cols-4\">\n <StatCard\n label=\"Courses enrolled\"\n value={enrollments.length}\n icon={BookOpen}\n tone=\"blue\"\n />\n <StatCard\n label=\"Courses completed\"\n value={metrics.coursesCompleted}\n icon={GraduationCap}\n tone=\"green\"\n />\n <StatCard\n label=\"Avg progress\"\n value={`${metrics.avgProgress}%`}\n icon={TrendingUp}\n tone=\"purple\"\n />\n <StatCard\n label=\"Problems solved\"\n value={metrics.problemsSolved}\n icon={Code2}\n tone=\"indigo\"\n />\n <StatCard\n label=\"Acceptance rate\"\n value={`${metrics.acceptanceRate}%`}\n icon={Percent}\n tone=\"teal\"\n />\n <StatCard label=\"Mentor sessions\" value={sessions.length} icon={Users} tone=\"blue\" />\n <StatCard label=\"Certificates\" value={certificates.length} icon={Award} tone=\"amber\" />\n </div>\n\n {!hasActivity ? (\n <EmptyView\n icon={<Activity size={26} />}\n title=\"No learning activity yet\"\n description=\"Enroll in a course or solve a problem and your progress charts will build here. TechnoSpam is pre-launch — start learning to see your data.\"\n action={\n <Button onClick={() => navigate('/courses/courses-page')}>Browse courses</Button>\n }\n />\n ) : (\n <div className=\"space-y-4\">\n <div className=\"grid grid-cols-1 gap-4 lg:grid-cols-2\">\n <ChartCard\n title=\"Enrollments by status\"\n subtitle=\"How your courses are progressing\"\n icon={BookOpen}\n tone=\"blue\"\n isEmpty={enrollmentsByStatus.length === 0}\n emptyLabel=\"No enrollments yet\"\n >\n <PieChart>\n <Pie\n data={enrollmentsByStatus}\n dataKey=\"value\"\n nameKey=\"name\"\n innerRadius={60}\n outerRadius={92}\n paddingAngle={2}\n stroke=\"var(--color-bg-surface)\"\n strokeWidth={2}\n >\n {enrollmentsByStatus.map((d) => (\n <Cell key={d.name} fill={d.color} />\n ))}\n </Pie>\n <ReTooltip contentStyle={TOOLTIP_STYLE} labelStyle={TOOLTIP_LABEL_STYLE} />\n <Legend iconType=\"circle\" wrapperStyle={{ fontSize: 12 }} />\n </PieChart>\n </ChartCard>\n\n <ChartCard\n title=\"Submissions by verdict\"\n subtitle=\"Outcomes across your attempts\"\n icon={Code2}\n tone=\"purple\"\n isEmpty={submissionsByVerdict.length === 0}\n emptyLabel=\"No submissions yet\"\n >\n <BarChart\n data={submissionsByVerdict}\n margin={{ top: 8, right: 8, left: -18, bottom: 0 }}\n >\n <CartesianGrid\n strokeDasharray=\"3 3\"\n stroke=\"var(--color-border-seam)\"\n vertical={false}\n />\n <XAxis\n dataKey=\"label\"\n tick={AXIS_TICK}\n axisLine={{ stroke: 'var(--color-border-seam)' }}\n tickLine={false}\n />\n <YAxis\n allowDecimals={false}\n tick={AXIS_TICK}\n axisLine={false}\n tickLine={false}\n />\n <ReTooltip\n contentStyle={TOOLTIP_STYLE}\n labelStyle={TOOLTIP_LABEL_STYLE}\n cursor={{ fill: 'var(--color-accent-primary)', fillOpacity: 0.08 }}\n />\n <Bar dataKey=\"count\" name=\"Submissions\" radius={[6, 6, 0, 0]} maxBarSize={56}>\n {submissionsByVerdict.map((d) => (\n <Cell key={d.label} fill={d.color} />\n ))}\n </Bar>\n </BarChart>\n </ChartCard>\n </div>\n\n <TopicAcceptanceCard data={acceptedRateByTopic} />\n\n <GlassCard className=\"p-5 transition-shadow duration-200 hover:shadow-lg\">\n <ChartHeader\n icon={Activity}\n title=\"Learning activity over time\"\n subtitle=\"Submissions and enrollments per week\"\n tone=\"teal\"\n />\n {activityData.length === 0 ? (\n <div className=\"flex h-[240px] items-center justify-center text-sm text-text-muted\">\n No dated activity yet\n </div>\n ) : (\n <ResponsiveContainer width=\"100%\" height={280}>\n <AreaChart\n data={activityData}\n margin={{ top: 8, right: 12, left: -18, bottom: 0 }}\n >\n <defs>\n <linearGradient id=\"tec-grad-submissions\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">\n <stop\n offset=\"0%\"\n stopColor=\"var(--color-accent-primary)\"\n stopOpacity={0.35}\n />\n <stop\n offset=\"100%\"\n stopColor=\"var(--color-accent-primary)\"\n stopOpacity={0}\n />\n </linearGradient>\n <linearGradient id=\"tec-grad-enrollments\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">\n <stop\n offset=\"0%\"\n stopColor=\"var(--color-accent-teal)\"\n stopOpacity={0.35}\n />\n <stop\n offset=\"100%\"\n stopColor=\"var(--color-accent-teal)\"\n stopOpacity={0}\n />\n </linearGradient>\n </defs>\n <CartesianGrid\n strokeDasharray=\"3 3\"\n stroke=\"var(--color-border-seam)\"\n vertical={false}\n />\n <XAxis\n dataKey=\"label\"\n tick={AXIS_TICK}\n axisLine={{ stroke: 'var(--color-border-seam)' }}\n tickLine={false}\n />\n <YAxis\n allowDecimals={false}\n tick={AXIS_TICK}\n axisLine={false}\n tickLine={false}\n />\n <ReTooltip contentStyle={TOOLTIP_STYLE} labelStyle={TOOLTIP_LABEL_STYLE} />\n <Legend iconType=\"circle\" wrapperStyle={{ fontSize: 12 }} />\n <Area\n type=\"monotone\"\n dataKey=\"submissions\"\n name=\"Submissions\"\n stroke=\"var(--color-accent-primary)\"\n strokeWidth={2}\n fill=\"url(#tec-grad-submissions)\"\n />\n <Area\n type=\"monotone\"\n dataKey=\"enrollments\"\n name=\"Enrollments\"\n stroke=\"var(--color-accent-teal)\"\n strokeWidth={2}\n fill=\"url(#tec-grad-enrollments)\"\n />\n </AreaChart>\n </ResponsiveContainer>\n )}\n </GlassCard>\n </div>\n )}\n\n {workspaceOverview}\n </div>\n )}\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AnalyticsAnalyticsPagePage.js","names":[],"sources":["../../../src/technospam/pages/AnalyticsAnalyticsPagePage.tsx"],"sourcesContent":["import { useMemo, useState, type CSSProperties, type SVGProps } from 'react';\nimport {\n Activity,\n Award,\n BarChart3,\n BookOpen,\n CalendarRange,\n Code2,\n GraduationCap,\n Library,\n Percent,\n Target,\n TrendingUp,\n Trophy,\n Users,\n X,\n type LucideIcon,\n} from 'lucide-react';\nimport { GlassCard, Button, Input } from '@burdenoff/fe-libs/ui';\nimport { cn } from '../utils/cn';\nimport {\n Area,\n AreaChart,\n Bar,\n BarChart,\n CartesianGrid,\n Cell,\n Legend,\n Pie,\n PieChart,\n ResponsiveContainer,\n Tooltip as ReTooltip,\n XAxis,\n YAxis,\n} from 'recharts';\nimport {\n EmptyView,\n ErrorView,\n LoadingGrid,\n LoadingRows,\n PageLayout,\n StatCard,\n} from '../components';\nimport { useTechnoSpam, useTechnoSpamNavigate } from '../providers/TechnoSpamProvider';\nimport {\n useCertificatesQuery,\n useCoursesQuery,\n useEnrollmentsQuery,\n useHackathonsQuery,\n useProblemsQuery,\n useSessionsQuery,\n useSubmissionsQuery,\n type TecEnrollmentStatus,\n type TecSubmissionVerdict,\n} from '../api/technospamApi';\nimport { humanizeEnum } from '../utils/format';\nimport { useTr } from '../hooks/useTr';\n\n/* ----------------------------------------------------------- chart config */\n\nconst ENROLL_STATUS_ORDER: TecEnrollmentStatus[] = [\n 'ENROLLED',\n 'IN_PROGRESS',\n 'COMPLETED',\n 'DROPPED',\n];\n\nconst ENROLL_STATUS_COLOR: Record<TecEnrollmentStatus, string> = {\n ENROLLED: 'var(--color-accent-primary)',\n IN_PROGRESS: 'var(--color-accent-teal)',\n COMPLETED: 'var(--color-status-success)',\n DROPPED: 'var(--color-status-warning)',\n};\n\nconst VERDICT_ORDER: TecSubmissionVerdict[] = [\n 'ACCEPTED',\n 'WRONG_ANSWER',\n 'TIME_LIMIT_EXCEEDED',\n 'RUNTIME_ERROR',\n 'COMPILE_ERROR',\n 'PENDING',\n];\n\nconst VERDICT_LABEL: Record<TecSubmissionVerdict, string> = {\n ACCEPTED: 'Accepted',\n WRONG_ANSWER: 'Wrong',\n TIME_LIMIT_EXCEEDED: 'TLE',\n RUNTIME_ERROR: 'Runtime',\n COMPILE_ERROR: 'Compile',\n PENDING: 'Pending',\n};\n\nconst VERDICT_COLOR: Record<TecSubmissionVerdict, string> = {\n ACCEPTED: 'var(--color-status-success)',\n WRONG_ANSWER: 'var(--color-status-error)',\n TIME_LIMIT_EXCEEDED: 'var(--color-status-warning)',\n RUNTIME_ERROR: 'var(--color-status-error)',\n COMPILE_ERROR: 'var(--color-status-error)',\n PENDING: 'var(--color-text-muted)',\n};\n\nconst TOOLTIP_STYLE: CSSProperties = {\n background: 'var(--color-bg-surface)',\n border: '1px solid var(--color-border-seam)',\n borderRadius: 12,\n color: 'var(--color-text-primary)',\n fontSize: 12,\n};\nconst TOOLTIP_LABEL_STYLE: CSSProperties = { color: 'var(--color-text-secondary)' };\nconst AXIS_TICK: SVGProps<SVGTextElement> = { fill: 'var(--color-text-muted)', fontSize: 12 };\n\ninterface StatusDatum {\n name: string;\n value: number;\n color: string;\n}\ninterface VerdictDatum {\n label: string;\n count: number;\n color: string;\n}\ninterface ActivityDatum {\n label: string;\n submissions: number;\n enrollments: number;\n}\ninterface TopicRateDatum {\n topic: string;\n accepted: number;\n attempted: number;\n rate: number;\n}\n\n/** Normalize an ISO timestamp to the start of its (Sunday-based) week. */\nfunction weekBucket(iso: string): { key: number; label: string } {\n const d = new Date(iso);\n if (Number.isNaN(d.getTime())) return { key: 0, label: '—' };\n const start = new Date(d.getFullYear(), d.getMonth(), d.getDate() - d.getDay());\n return {\n key: start.getTime(),\n label: start.toLocaleDateString(undefined, { month: 'short', day: 'numeric' }),\n };\n}\n\n/* -------------------------------------------------------- chart wrappers */\n\ntype ChipTone = 'blue' | 'teal' | 'purple' | 'indigo';\n\nconst CHIP: Record<ChipTone, string> = {\n blue: 'bg-accent-primary/15 text-accent-primary ring-accent-primary/20',\n teal: 'bg-accent-teal/15 text-accent-teal ring-accent-teal/20',\n purple: 'bg-accent-purple/15 text-accent-purple ring-accent-purple/20',\n indigo: 'bg-accent-indigo/15 text-accent-indigo ring-accent-indigo/20',\n};\n\nfunction ChartHeader({\n icon: Icon,\n title,\n subtitle,\n tone,\n}: {\n icon: LucideIcon;\n title: string;\n subtitle?: string;\n tone: ChipTone;\n}) {\n return (\n <div className=\"mb-4 flex items-center gap-3\">\n <span\n className={cn(\n 'flex h-9 w-9 shrink-0 items-center justify-center rounded-xl ring-1',\n CHIP[tone]\n )}\n >\n <Icon size={17} />\n </span>\n <div className=\"min-w-0\">\n <h3 className=\"truncate text-sm font-semibold text-text-primary\">{title}</h3>\n {subtitle ? <p className=\"truncate text-xs text-text-muted\">{subtitle}</p> : null}\n </div>\n </div>\n );\n}\n\nfunction ChartCard({\n title,\n subtitle,\n icon,\n tone = 'teal',\n isEmpty,\n emptyLabel,\n children,\n}: {\n title: string;\n subtitle?: string;\n icon: LucideIcon;\n tone?: ChipTone;\n isEmpty: boolean;\n emptyLabel: string;\n children: React.ReactElement;\n}) {\n return (\n <GlassCard className=\"p-5 transition-shadow duration-200 hover:shadow-lg\">\n <ChartHeader icon={icon} title={title} subtitle={subtitle} tone={tone} />\n {isEmpty ? (\n <div className=\"flex h-[240px] items-center justify-center text-center text-sm text-text-muted\">\n {emptyLabel}\n </div>\n ) : (\n <ResponsiveContainer width=\"100%\" height={260}>\n {children}\n </ResponsiveContainer>\n )}\n </GlassCard>\n );\n}\n\n/**\n * Accepted rate per problem topic.\n *\n * Built entirely from the submissions this page already loads: each\n * TecSubmission carries its `problem { topic }` and its `verdict`, so the\n * rate is `ACCEPTED / attempts` per topic -- a count, not an inference. The\n * lowest row is called out as exactly that (the lowest measured rate), never\n * as a diagnosis or a recommendation.\n */\nfunction TopicAcceptanceCard({\n data,\n title,\n subtitle,\n noData,\n lowestRateLabel,\n}: {\n data: TopicRateDatum[];\n title: string;\n subtitle: string;\n noData: string;\n lowestRateLabel: (topic: string, accepted: number, attempted: number) => string;\n}) {\n const weakest = data.length > 1 ? data[data.length - 1] : undefined;\n return (\n <GlassCard className=\"p-5 transition-shadow duration-200 hover:shadow-lg\">\n <ChartHeader icon={Target} title={title} subtitle={subtitle} tone=\"indigo\" />\n {data.length === 0 ? (\n <div className=\"flex h-[240px] items-center justify-center text-center text-sm text-text-muted\">\n {noData}\n </div>\n ) : (\n <div className=\"space-y-3\">\n {data.map((row) => {\n const lowest = weakest?.topic === row.topic;\n return (\n <div key={row.topic}>\n <div className=\"mb-1 flex flex-wrap items-baseline gap-x-2\">\n <span className=\"truncate text-sm font-medium text-text-primary\">\n {row.topic}\n </span>\n <span\n className={cn(\n 'text-sm font-semibold tabular-nums',\n lowest ? 'text-status-warning-text' : 'text-text-secondary'\n )}\n >\n {row.rate}%\n </span>\n <span className=\"ml-auto shrink-0 rounded-full bg-bg-sunken px-2 py-0.5 text-xs text-text-muted\">\n {row.accepted} of {row.attempted} accepted\n </span>\n </div>\n <div className=\"h-2.5 w-full overflow-hidden rounded-full bg-bg-sunken\">\n <div\n className={cn(\n 'h-full rounded-full transition-all duration-500',\n lowest ? 'bg-status-warning-text' : 'bg-accent-primary'\n )}\n style={{ width: `${row.rate}%` }}\n />\n </div>\n </div>\n );\n })}\n {weakest ? (\n <p className=\"pt-1 text-xs text-text-muted\">\n {lowestRateLabel(weakest.topic, weakest.accepted, weakest.attempted)}\n </p>\n ) : null}\n </div>\n )}\n </GlassCard>\n );\n}\n\n/* ---------------------------------------------------------- date range */\n\n/** Convert a native `<input type=\"date\">` value (YYYY-MM-DD) into inclusive\n * ISO bounds so the server-side createdAt filter covers the whole day. */\nfunction dayStartIso(date: string): string | undefined {\n return date ? `${date}T00:00:00.000Z` : undefined;\n}\nfunction dayEndIso(date: string): string | undefined {\n return date ? `${date}T23:59:59.999Z` : undefined;\n}\n\nfunction DateRangeControls({\n start,\n end,\n onStart,\n onEnd,\n onPreset,\n onClear,\n tr,\n}: {\n start: string;\n end: string;\n onStart: (v: string) => void;\n onEnd: (v: string) => void;\n onPreset: (days: number) => void;\n onClear: () => void;\n tr: ReturnType<typeof useTr>;\n}) {\n const active = Boolean(start || end);\n return (\n <GlassCard className=\"mb-6 flex flex-col gap-3 p-4 lg:flex-row lg:items-end lg:justify-between\">\n <div className=\"flex flex-wrap items-end gap-3\">\n <div className=\"flex items-center gap-2 self-center text-sm font-medium text-text-secondary\">\n <CalendarRange size={16} className=\"text-accent-teal\" />\n {tr('technospam.analytics.dateRangeTitle', 'Date range')}\n </div>\n <div>\n <label\n className=\"mb-1 block text-xs font-medium text-text-muted\"\n htmlFor=\"tec-range-from\"\n >\n {tr('technospam.analytics.from', 'From')}\n </label>\n <Input\n id=\"tec-range-from\"\n type=\"date\"\n value={start}\n max={end || undefined}\n onChange={(e) => onStart(e.target.value)}\n className=\"w-40\"\n />\n </div>\n <div>\n <label className=\"mb-1 block text-xs font-medium text-text-muted\" htmlFor=\"tec-range-to\">\n {tr('technospam.analytics.to', 'To')}\n </label>\n <Input\n id=\"tec-range-to\"\n type=\"date\"\n value={end}\n min={start || undefined}\n onChange={(e) => onEnd(e.target.value)}\n className=\"w-40\"\n />\n </div>\n {active ? (\n <Button variant=\"ghost\" size=\"sm\" onClick={onClear}>\n <X size={14} /> {tr('technospam.analytics.clear', 'Clear')}\n </Button>\n ) : null}\n </div>\n <div className=\"flex flex-wrap gap-2\">\n <Button variant=\"outline\" size=\"sm\" onClick={() => onPreset(7)}>\n {tr('technospam.analytics.last7Days', 'Last 7 days')}\n </Button>\n <Button variant=\"outline\" size=\"sm\" onClick={() => onPreset(30)}>\n {tr('technospam.analytics.last30Days', 'Last 30 days')}\n </Button>\n <Button variant=\"outline\" size=\"sm\" onClick={() => onPreset(90)}>\n {tr('technospam.analytics.last90Days', 'Last 90 days')}\n </Button>\n </div>\n </GlassCard>\n );\n}\n\n/* --------------------------------------------------------------- page */\n\nexport function AnalyticsAnalyticsPagePage() {\n const navigate = useTechnoSpamNavigate();\n const { currentUser } = useTechnoSpam();\n const tr = useTr();\n const learnerId = currentUser?.id;\n\n const [rangeStart, setRangeStart] = useState('');\n const [rangeEnd, setRangeEnd] = useState('');\n const startDate = dayStartIso(rangeStart);\n const endDate = dayEndIso(rangeEnd);\n\n const applyPreset = (days: number) => {\n const now = new Date();\n const from = new Date();\n from.setDate(from.getDate() - days);\n setRangeStart(from.toISOString().slice(0, 10));\n setRangeEnd(now.toISOString().slice(0, 10));\n };\n const clearRange = () => {\n setRangeStart('');\n setRangeEnd('');\n };\n\n const dateVars = startDate || endDate ? { startDate, endDate } : {};\n const enrollmentsQ = useEnrollmentsQuery(learnerId ? { learnerId, ...dateVars } : {});\n const submissionsQ = useSubmissionsQuery(learnerId ? { learnerId, ...dateVars } : {});\n const sessionsQ = useSessionsQuery(learnerId ? { menteeId: learnerId } : {});\n const certificatesQ = useCertificatesQuery(learnerId ? { learnerId } : {});\n const coursesQ = useCoursesQuery({ status: 'PUBLISHED' });\n const problemsQ = useProblemsQuery({ status: 'PUBLISHED' });\n const hackathonsQ = useHackathonsQuery({});\n\n const enrollments = useMemo(\n () => enrollmentsQ.data?.tecListEnrollments?.items ?? [],\n [enrollmentsQ.data]\n );\n const submissions = useMemo(\n () => submissionsQ.data?.tecListSubmissions?.items ?? [],\n [submissionsQ.data]\n );\n const sessions = sessionsQ.data?.tecListSessions?.items ?? [];\n const certificates = certificatesQ.data?.tecListCertificates ?? [];\n const coursesTotal = coursesQ.data?.tecListCourses?.items.length ?? 0;\n const problemsTotal = problemsQ.data?.tecListProblems?.items.length ?? 0;\n const hackathonsTotal = hackathonsQ.data?.tecListHackathons?.items.length ?? 0;\n\n const metrics = useMemo(() => {\n const coursesCompleted = enrollments.filter((e) => e.status === 'COMPLETED').length;\n const avgProgress = enrollments.length\n ? Math.round(enrollments.reduce((s, e) => s + e.progressPct, 0) / enrollments.length)\n : 0;\n const acceptedSubs = submissions.filter((s) => s.verdict === 'ACCEPTED');\n const problemsSolved = new Set(acceptedSubs.map((s) => s.problemId)).size;\n const acceptanceRate = submissions.length\n ? Math.round((acceptedSubs.length / submissions.length) * 100)\n : 0;\n return { coursesCompleted, avgProgress, problemsSolved, acceptanceRate };\n }, [enrollments, submissions]);\n\n const enrollmentsByStatus = useMemo<StatusDatum[]>(\n () =>\n ENROLL_STATUS_ORDER.map((s) => ({\n name: humanizeEnum(s),\n value: enrollments.filter((e) => e.status === s).length,\n color: ENROLL_STATUS_COLOR[s],\n })).filter((d) => d.value > 0),\n [enrollments]\n );\n\n const submissionsByVerdict = useMemo<VerdictDatum[]>(\n () =>\n VERDICT_ORDER.map((v) => ({\n label: VERDICT_LABEL[v],\n count: submissions.filter((s) => s.verdict === v).length,\n color: VERDICT_COLOR[v],\n })).filter((d) => d.count > 0),\n [submissions]\n );\n\n const acceptedRateByTopic = useMemo<TopicRateDatum[]>(() => {\n const buckets = new Map<string, { accepted: number; attempted: number }>();\n submissions.forEach((s) => {\n const topic = s.problem?.topic?.trim() || 'Untagged';\n const bucket = buckets.get(topic) ?? { accepted: 0, attempted: 0 };\n bucket.attempted += 1;\n if (s.verdict === 'ACCEPTED') bucket.accepted += 1;\n buckets.set(topic, bucket);\n });\n return [...buckets.entries()]\n .map(([topic, bucket]) => ({\n topic,\n accepted: bucket.accepted,\n attempted: bucket.attempted,\n rate: Math.round((bucket.accepted / bucket.attempted) * 100),\n }))\n .sort((a, b) => b.rate - a.rate || b.attempted - a.attempted);\n }, [submissions]);\n\n const activityData = useMemo<ActivityDatum[]>(() => {\n const buckets = new Map<number, ActivityDatum>();\n const add = (iso: string, key: 'submissions' | 'enrollments') => {\n const { key: bucketKey, label } = weekBucket(iso);\n const existing = buckets.get(bucketKey) ?? { label, submissions: 0, enrollments: 0 };\n existing[key] += 1;\n buckets.set(bucketKey, existing);\n };\n submissions.forEach((s) => add(s.createdAt, 'submissions'));\n enrollments.forEach((e) => add(e.createdAt, 'enrollments'));\n return [...buckets.entries()]\n .sort((a, b) => a[0] - b[0])\n .slice(-8)\n .map(([, v]) => v);\n }, [submissions, enrollments]);\n\n const hasActivity =\n enrollments.length > 0 ||\n submissions.length > 0 ||\n sessions.length > 0 ||\n certificates.length > 0;\n\n const learnerLoading =\n enrollmentsQ.loading || submissionsQ.loading || sessionsQ.loading || certificatesQ.loading;\n const learnerError =\n enrollmentsQ.error || submissionsQ.error || sessionsQ.error || certificatesQ.error;\n const showLoading = learnerLoading && !hasActivity;\n\n const workspaceOverview = (\n <div>\n <h2 className=\"mb-3 text-base font-semibold text-text-primary\">\n {tr('technospam.analytics.workspaceOverviewTitle', 'Workspace overview')}\n </h2>\n <p className=\"mb-4 max-w-2xl text-sm text-text-secondary\">\n {tr(\n 'technospam.analytics.workspaceOverviewDesc',\n 'Live content available across the TechnoSpam learning workspace right now.'\n )}\n </p>\n <div className=\"grid grid-cols-1 gap-3 sm:grid-cols-3\">\n <StatCard\n label={tr('technospam.analytics.statPublishedCourses', 'Published courses')}\n value={coursesTotal}\n icon={Library}\n tone=\"blue\"\n />\n <StatCard\n label={tr('technospam.analytics.statPublishedProblems', 'Published problems')}\n value={problemsTotal}\n icon={Code2}\n tone=\"indigo\"\n />\n <StatCard\n label={tr('technospam.analytics.statHackathons', 'Hackathons')}\n value={hackathonsTotal}\n icon={Trophy}\n tone=\"purple\"\n />\n </div>\n </div>\n );\n\n return (\n <PageLayout\n hero\n title={tr('technospam.analytics.title', 'Analytics')}\n description={tr(\n 'technospam.analytics.description',\n 'Your real learning performance across courses, problems, mentoring and certificates.'\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 {!learnerId ? (\n <div className=\"space-y-8\">\n <EmptyView\n icon={<BarChart3 size={26} />}\n title={tr('technospam.analytics.signInTitle', 'Sign in to view your analytics')}\n description={tr(\n 'technospam.analytics.signInDesc',\n \"Your personal learning metrics appear here once you're signed in.\"\n )}\n />\n {workspaceOverview}\n </div>\n ) : showLoading ? (\n <div className=\"space-y-6\">\n <LoadingGrid count={4} height=\"h-24\" />\n <LoadingRows count={2} height=\"h-72\" />\n </div>\n ) : learnerError && !hasActivity ? (\n <ErrorView\n title={tr('technospam.common.errorLoadAnalytics', \"Couldn't load your analytics\")}\n error={learnerError}\n />\n ) : (\n <div className=\"space-y-8\">\n <DateRangeControls\n start={rangeStart}\n end={rangeEnd}\n onStart={setRangeStart}\n onEnd={setRangeEnd}\n onPreset={applyPreset}\n onClear={clearRange}\n tr={tr}\n />\n <div className=\"grid grid-cols-2 gap-3 sm:grid-cols-3 xl:grid-cols-4\">\n <StatCard\n label={tr('technospam.analytics.statCoursesEnrolled', 'Courses enrolled')}\n value={enrollments.length}\n icon={BookOpen}\n tone=\"blue\"\n />\n <StatCard\n label={tr('technospam.analytics.statCoursesCompleted', 'Courses completed')}\n value={metrics.coursesCompleted}\n icon={GraduationCap}\n tone=\"green\"\n />\n <StatCard\n label={tr('technospam.analytics.statAvgProgress', 'Avg progress')}\n value={`${metrics.avgProgress}%`}\n icon={TrendingUp}\n tone=\"purple\"\n />\n <StatCard\n label={tr('technospam.analytics.statProblemsSolved', 'Problems solved')}\n value={metrics.problemsSolved}\n icon={Code2}\n tone=\"indigo\"\n />\n <StatCard\n label={tr('technospam.analytics.statAcceptanceRate', 'Acceptance rate')}\n value={`${metrics.acceptanceRate}%`}\n icon={Percent}\n tone=\"teal\"\n />\n <StatCard\n label={tr('technospam.analytics.statMentorSessions', 'Mentor sessions')}\n value={sessions.length}\n icon={Users}\n tone=\"blue\"\n />\n <StatCard\n label={tr('technospam.analytics.statCertificates', 'Certificates')}\n value={certificates.length}\n icon={Award}\n tone=\"amber\"\n />\n </div>\n\n {!hasActivity ? (\n <EmptyView\n icon={<Activity size={26} />}\n title={tr('technospam.analytics.noActivityTitle', 'No learning activity yet')}\n description={tr(\n 'technospam.analytics.noActivityDesc',\n 'Enroll in a course or solve a problem and your progress charts will build here. TechnoSpam is pre-launch — start learning to see your data.'\n )}\n action={\n <Button onClick={() => navigate('/courses/courses-page')}>\n {tr('technospam.common.browseCourses', 'Browse courses')}\n </Button>\n }\n />\n ) : (\n <div className=\"space-y-4\">\n <div className=\"grid grid-cols-1 gap-4 lg:grid-cols-2\">\n <ChartCard\n title={tr(\n 'technospam.analytics.enrollmentsByStatusTitle',\n 'Enrollments by status'\n )}\n subtitle={tr(\n 'technospam.analytics.enrollmentsByStatusSubtitle',\n 'How your courses are progressing'\n )}\n icon={BookOpen}\n tone=\"blue\"\n isEmpty={enrollmentsByStatus.length === 0}\n emptyLabel={tr('technospam.analytics.noEnrollments', 'No enrollments yet')}\n >\n <PieChart>\n <Pie\n data={enrollmentsByStatus}\n dataKey=\"value\"\n nameKey=\"name\"\n innerRadius={60}\n outerRadius={92}\n paddingAngle={2}\n stroke=\"var(--color-bg-surface)\"\n strokeWidth={2}\n >\n {enrollmentsByStatus.map((d) => (\n <Cell key={d.name} fill={d.color} />\n ))}\n </Pie>\n <ReTooltip contentStyle={TOOLTIP_STYLE} labelStyle={TOOLTIP_LABEL_STYLE} />\n <Legend iconType=\"circle\" wrapperStyle={{ fontSize: 12 }} />\n </PieChart>\n </ChartCard>\n\n <ChartCard\n title={tr(\n 'technospam.analytics.submissionsByVerdictTitle',\n 'Submissions by verdict'\n )}\n subtitle={tr(\n 'technospam.analytics.submissionsByVerdictSubtitle',\n 'Outcomes across your attempts'\n )}\n icon={Code2}\n tone=\"purple\"\n isEmpty={submissionsByVerdict.length === 0}\n emptyLabel={tr('technospam.analytics.noSubmissions', 'No submissions yet')}\n >\n <BarChart\n data={submissionsByVerdict}\n margin={{ top: 8, right: 8, left: -18, bottom: 0 }}\n >\n <CartesianGrid\n strokeDasharray=\"3 3\"\n stroke=\"var(--color-border-seam)\"\n vertical={false}\n />\n <XAxis\n dataKey=\"label\"\n tick={AXIS_TICK}\n axisLine={{ stroke: 'var(--color-border-seam)' }}\n tickLine={false}\n />\n <YAxis\n allowDecimals={false}\n tick={AXIS_TICK}\n axisLine={false}\n tickLine={false}\n />\n <ReTooltip\n contentStyle={TOOLTIP_STYLE}\n labelStyle={TOOLTIP_LABEL_STYLE}\n cursor={{ fill: 'var(--color-accent-primary)', fillOpacity: 0.08 }}\n />\n <Bar dataKey=\"count\" name=\"Submissions\" radius={[6, 6, 0, 0]} maxBarSize={56}>\n {submissionsByVerdict.map((d) => (\n <Cell key={d.label} fill={d.color} />\n ))}\n </Bar>\n </BarChart>\n </ChartCard>\n </div>\n\n <TopicAcceptanceCard\n data={acceptedRateByTopic}\n title={tr('technospam.analytics.topicAcceptanceTitle', 'Accepted rate by topic')}\n subtitle={tr(\n 'technospam.analytics.topicAcceptanceSubtitle',\n \"Where your solutions land, and where they don't\"\n )}\n noData={tr(\n 'technospam.analytics.noTopicData',\n 'Solve a practice problem and your per-topic rate builds here'\n )}\n lowestRateLabel={(topic, accepted, attempted) =>\n tr(\n 'technospam.analytics.lowestMeasuredRate',\n 'Lowest measured rate: {{topic}} — {{accepted}} of {{attempted}} attempts accepted in the loaded range.',\n { topic, accepted, attempted }\n )\n }\n />\n\n <GlassCard className=\"p-5 transition-shadow duration-200 hover:shadow-lg\">\n <ChartHeader\n icon={Activity}\n title={tr(\n 'technospam.analytics.activityOverTimeTitle',\n 'Learning activity over time'\n )}\n subtitle={tr(\n 'technospam.analytics.activityOverTimeSubtitle',\n 'Submissions and enrollments per week'\n )}\n tone=\"teal\"\n />\n {activityData.length === 0 ? (\n <div className=\"flex h-[240px] items-center justify-center text-sm text-text-muted\">\n {tr('technospam.analytics.noDatedActivity', 'No dated activity yet')}\n </div>\n ) : (\n <ResponsiveContainer width=\"100%\" height={280}>\n <AreaChart\n data={activityData}\n margin={{ top: 8, right: 12, left: -18, bottom: 0 }}\n >\n <defs>\n <linearGradient id=\"tec-grad-submissions\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">\n <stop\n offset=\"0%\"\n stopColor=\"var(--color-accent-primary)\"\n stopOpacity={0.35}\n />\n <stop\n offset=\"100%\"\n stopColor=\"var(--color-accent-primary)\"\n stopOpacity={0}\n />\n </linearGradient>\n <linearGradient id=\"tec-grad-enrollments\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">\n <stop\n offset=\"0%\"\n stopColor=\"var(--color-accent-teal)\"\n stopOpacity={0.35}\n />\n <stop\n offset=\"100%\"\n stopColor=\"var(--color-accent-teal)\"\n stopOpacity={0}\n />\n </linearGradient>\n </defs>\n <CartesianGrid\n strokeDasharray=\"3 3\"\n stroke=\"var(--color-border-seam)\"\n vertical={false}\n />\n <XAxis\n dataKey=\"label\"\n tick={AXIS_TICK}\n axisLine={{ stroke: 'var(--color-border-seam)' }}\n tickLine={false}\n />\n <YAxis\n allowDecimals={false}\n tick={AXIS_TICK}\n axisLine={false}\n tickLine={false}\n />\n <ReTooltip contentStyle={TOOLTIP_STYLE} labelStyle={TOOLTIP_LABEL_STYLE} />\n <Legend iconType=\"circle\" wrapperStyle={{ fontSize: 12 }} />\n <Area\n type=\"monotone\"\n dataKey=\"submissions\"\n name=\"Submissions\"\n stroke=\"var(--color-accent-primary)\"\n strokeWidth={2}\n fill=\"url(#tec-grad-submissions)\"\n />\n <Area\n type=\"monotone\"\n dataKey=\"enrollments\"\n name=\"Enrollments\"\n stroke=\"var(--color-accent-teal)\"\n strokeWidth={2}\n fill=\"url(#tec-grad-enrollments)\"\n />\n </AreaChart>\n </ResponsiveContainer>\n )}\n </GlassCard>\n </div>\n )}\n\n {workspaceOverview}\n </div>\n )}\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
@@ -99,7 +99,7 @@ function V({ onAdded: t }) {
|
|
|
99
99
|
});
|
|
100
100
|
}
|
|
101
101
|
function H({ onAdded: t }) {
|
|
102
|
-
let { currentUser: n } = e(), r = n?.id, { data: i } = l(r ? { learnerId: r } : {}), [a, { loading: c }] = s(), u = (i?.tecListEnrollments
|
|
102
|
+
let { currentUser: n } = e(), r = n?.id, { data: i } = l(r ? { learnerId: r } : {}), [a, { loading: c }] = s(), u = (i?.tecListEnrollments?.items ?? []).filter((e) => e.status === "COMPLETED");
|
|
103
103
|
return u.length === 0 ? /* @__PURE__ */ O("p", {
|
|
104
104
|
className: "text-xs text-text-muted",
|
|
105
105
|
children: "No completed courses yet."
|
|
@@ -126,7 +126,7 @@ function H({ onAdded: t }) {
|
|
|
126
126
|
});
|
|
127
127
|
}
|
|
128
128
|
function U({ onAdded: t }) {
|
|
129
|
-
let { currentUser: n } = e(), r = n?.id, { data: i } = u({}), a = i?.tecListHackathons
|
|
129
|
+
let { currentUser: n } = e(), r = n?.id, { data: i } = u({}), a = i?.tecListHackathons?.items ?? [], [c, l] = g(""), { data: d } = m(c), [f, { loading: p }] = s(), h = (d?.tecListTeams?.items ?? []).filter((e) => (e.status === "SUBMITTED" || e.status === "WINNER") && (e.captainId === r || e.memberIds.includes(r ?? "")));
|
|
130
130
|
return /* @__PURE__ */ k("div", {
|
|
131
131
|
className: "space-y-2",
|
|
132
132
|
children: [
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ApplicationsPortfolioPagePage.js","names":[],"sources":["../../../src/technospam/pages/ApplicationsPortfolioPagePage.tsx"],"sourcesContent":["/**\n * Learner Portfolio (BOFF-4432).\n *\n * Aggregates real completed work — issued certificates, completed courses,\n * and submitted hackathon projects — into a single showcase the learner\n * controls. Every item's title/description/link/completion date is copied\n * server-side from the real source record at add-time; nothing here is\n * free-form/fabricated.\n *\n * DEFERRED SCOPE (documented, not silently dropped): the backend already\n * exposes a public, unauthenticated `tecPublicPortfolio(slug)` field\n * (@rbac public: true) that returns real published-portfolio data. This\n * page generates the slug and lets the learner copy the public link, but\n * there is no unauthenticated *page* yet that renders it — the MFE only\n * mounts inside the authenticated app shell, and alpha gates every web\n * hostname behind Cloudflare Access. Standing up a real public viewer\n * needs app-shell + CF Access allow-list work coordinated with\n * infra/tf-cloudflare, which is out of scope for this pass.\n */\nimport { useState } from 'react';\nimport {\n Award,\n Briefcase,\n Check,\n Copy,\n ExternalLink,\n GraduationCap,\n Link2,\n Plus,\n Trash2,\n Trophy,\n X,\n} from 'lucide-react';\nimport {\n Button,\n GlassCard,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '@burdenoff/fe-libs/ui';\nimport { EmptyView, ErrorView, LoadingRows, PageLayout } from '../components';\nimport { useTechnoSpam } from '../providers/TechnoSpamProvider';\nimport {\n type TecPortfolioItem,\n type TecPortfolioItemKind,\n useAddPortfolioItem,\n useCertificatesQuery,\n useEnrollmentsQuery,\n useHackathonsQuery,\n useMyPortfolioQuery,\n usePublishPortfolio,\n useRemovePortfolioItem,\n useTeamsQuery,\n useUnpublishPortfolio,\n} from '../api/technospamApi';\nimport { formatDate } from '../utils/format';\nimport { notify } from '../lib/toast';\nimport { nativeConfirm } from '@burdenoff/fe-libs/shared/native';\n\nconst KIND_ICON: Record<TecPortfolioItemKind, typeof Award> = {\n HACKATHON_PROJECT: Trophy,\n CERTIFICATE: Award,\n COURSE_COMPLETION: GraduationCap,\n};\n\nconst KIND_LABEL: Record<TecPortfolioItemKind, string> = {\n HACKATHON_PROJECT: 'Hackathon project',\n CERTIFICATE: 'Certificate',\n COURSE_COMPLETION: 'Completed course',\n};\n\nfunction ItemCard({ item, onRemove }: { item: TecPortfolioItem; onRemove: () => void }) {\n const Icon = KIND_ICON[item.kind];\n return (\n <GlassCard className=\"group flex flex-col gap-3 p-4 transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md sm:flex-row sm:items-start sm:justify-between\">\n <div className=\"flex min-w-0 gap-3\">\n <span className=\"flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-accent-purple/15 text-accent-purple ring-1 ring-accent-purple/20\">\n <Icon size={18} />\n </span>\n <div className=\"min-w-0\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <span className=\"truncate text-sm font-semibold text-text-primary\">{item.title}</span>\n <span className=\"rounded-full border border-border-seam px-1.5 py-0.5 text-[10px] font-medium uppercase tracking-wide text-text-muted\">\n {KIND_LABEL[item.kind]}\n </span>\n </div>\n {item.description ? (\n <p className=\"mt-1 line-clamp-2 text-xs text-text-secondary\">{item.description}</p>\n ) : null}\n <div className=\"mt-2 flex flex-wrap items-center gap-3 text-xs text-text-muted\">\n <span>Completed {formatDate(item.completionDate)}</span>\n {item.link ? (\n <a\n href={item.link}\n target=\"_blank\"\n rel=\"noreferrer\"\n className=\"inline-flex items-center gap-1 text-accent-primary hover:underline\"\n >\n <ExternalLink size={12} /> View\n </a>\n ) : null}\n </div>\n </div>\n </div>\n <Button\n variant=\"ghost\"\n size=\"sm\"\n className=\"shrink-0 text-status-error-text hover:text-status-error-text\"\n onClick={onRemove}\n >\n <Trash2 size={14} />\n </Button>\n </GlassCard>\n );\n}\n\nfunction AddFromCertificates({ onAdded }: { onAdded: () => void }) {\n const { currentUser } = useTechnoSpam();\n const learnerId = currentUser?.id;\n const { data } = useCertificatesQuery(learnerId ? { learnerId } : {});\n const [addItem, { loading }] = useAddPortfolioItem();\n const certificates = (data?.tecListCertificates ?? []).filter((c) => c.status === 'ISSUED');\n\n if (certificates.length === 0) {\n return <p className=\"text-xs text-text-muted\">No issued certificates yet.</p>;\n }\n return (\n <div className=\"space-y-2\">\n {certificates.map((c) => (\n <div\n key={c.id}\n className=\"flex items-center justify-between gap-2 rounded-lg border border-border-seam p-2\"\n >\n <span className=\"min-w-0 truncate text-xs text-text-primary\">\n {c.course?.title ?? 'Certificate'} · Serial {c.serial}\n </span>\n <Button\n size=\"sm\"\n variant=\"outline\"\n disabled={loading}\n onClick={async () => {\n const res = await addItem({\n variables: { input: { kind: 'CERTIFICATE', sourceId: c.id } },\n });\n if (res.data?.tecAddPortfolioItem) {\n notify.success('Added to portfolio');\n onAdded();\n }\n }}\n >\n <Plus size={12} /> Add\n </Button>\n </div>\n ))}\n </div>\n );\n}\n\nfunction AddFromCourses({ onAdded }: { onAdded: () => void }) {\n const { currentUser } = useTechnoSpam();\n const learnerId = currentUser?.id;\n const { data } = useEnrollmentsQuery(learnerId ? { learnerId } : {});\n const [addItem, { loading }] = useAddPortfolioItem();\n const completed = (data?.tecListEnrollments.items ?? []).filter((e) => e.status === 'COMPLETED');\n\n if (completed.length === 0) {\n return <p className=\"text-xs text-text-muted\">No completed courses yet.</p>;\n }\n return (\n <div className=\"space-y-2\">\n {completed.map((e) => (\n <div\n key={e.id}\n className=\"flex items-center justify-between gap-2 rounded-lg border border-border-seam p-2\"\n >\n <span className=\"min-w-0 truncate text-xs text-text-primary\">\n {e.course?.title ?? 'Course'}\n </span>\n <Button\n size=\"sm\"\n variant=\"outline\"\n disabled={loading}\n onClick={async () => {\n const res = await addItem({\n variables: { input: { kind: 'COURSE_COMPLETION', sourceId: e.id } },\n });\n if (res.data?.tecAddPortfolioItem) {\n notify.success('Added to portfolio');\n onAdded();\n }\n }}\n >\n <Plus size={12} /> Add\n </Button>\n </div>\n ))}\n </div>\n );\n}\n\nfunction AddFromHackathons({ onAdded }: { onAdded: () => void }) {\n const { currentUser } = useTechnoSpam();\n const learnerId = currentUser?.id;\n const { data: hackathonData } = useHackathonsQuery({});\n const hackathons = hackathonData?.tecListHackathons.items ?? [];\n const [hackathonId, setHackathonId] = useState<string>('');\n const { data: teamsData } = useTeamsQuery(hackathonId);\n const [addItem, { loading }] = useAddPortfolioItem();\n\n const myTeams = (teamsData?.tecListTeams.items ?? []).filter(\n (t) =>\n (t.status === 'SUBMITTED' || t.status === 'WINNER') &&\n (t.captainId === learnerId || t.memberIds.includes(learnerId ?? ''))\n );\n\n return (\n <div className=\"space-y-2\">\n <Select value={hackathonId} onValueChange={setHackathonId}>\n <SelectTrigger className=\"w-full\">\n <SelectValue placeholder=\"Choose a hackathon\" />\n </SelectTrigger>\n <SelectContent>\n {hackathons.map((h) => (\n <SelectItem key={h.id} value={h.id}>\n {h.title}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n {hackathonId && myTeams.length === 0 ? (\n <p className=\"text-xs text-text-muted\">No submitted team of yours in this hackathon.</p>\n ) : null}\n {myTeams.map((t) => (\n <div\n key={t.id}\n className=\"flex items-center justify-between gap-2 rounded-lg border border-border-seam p-2\"\n >\n <span className=\"min-w-0 truncate text-xs text-text-primary\">\n {t.projectName || t.name}\n </span>\n <Button\n size=\"sm\"\n variant=\"outline\"\n disabled={loading}\n onClick={async () => {\n const res = await addItem({\n variables: { input: { kind: 'HACKATHON_PROJECT', sourceId: t.id } },\n });\n if (res.data?.tecAddPortfolioItem) {\n notify.success('Added to portfolio');\n onAdded();\n }\n }}\n >\n <Plus size={12} /> Add\n </Button>\n </div>\n ))}\n </div>\n );\n}\n\nexport function ApplicationsPortfolioPagePage() {\n const { data, loading, error, refetch } = useMyPortfolioQuery();\n const [removeItem] = useRemovePortfolioItem();\n const [publish, { loading: publishing }] = usePublishPortfolio();\n const [unpublish, { loading: unpublishing }] = useUnpublishPortfolio();\n const [showAdd, setShowAdd] = useState(false);\n const [copied, setCopied] = useState(false);\n\n const portfolio = data?.tecMyPortfolio;\n const items = portfolio?.items ?? [];\n\n const publicUrl = portfolio?.publicSlug\n ? `${typeof window !== 'undefined' ? window.location.origin : ''}/technospam/portfolio/${portfolio.publicSlug}`\n : null;\n\n const handleRemove = async (id: string, title: string) => {\n const confirmed = await nativeConfirm({\n title: 'Remove from portfolio',\n message: `Remove \"${title}\" from your portfolio? This does not delete the underlying record.`,\n okButtonTitle: 'Remove',\n cancelButtonTitle: 'Cancel',\n });\n if (!confirmed) return;\n const res = await removeItem({ variables: { id } });\n if (res.data?.tecRemovePortfolioItem) notify.success('Removed from portfolio');\n };\n\n const handleTogglePublish = async () => {\n if (!portfolio) return;\n if (portfolio.isPublished) {\n const res = await unpublish();\n if (res.data?.tecUnpublishPortfolio) notify.success('Portfolio unpublished');\n } else {\n const res = await publish();\n if (res.data?.tecPublishPortfolio) notify.success('Portfolio published');\n }\n };\n\n const handleCopyLink = async () => {\n if (!publicUrl) return;\n await navigator.clipboard.writeText(publicUrl);\n setCopied(true);\n setTimeout(() => setCopied(false), 1500);\n };\n\n return (\n <PageLayout\n hero\n title=\"Portfolio\"\n description=\"Showcase your real completed courses, certificates, and hackathon projects.\"\n actions={\n portfolio ? (\n <Button\n variant={portfolio.isPublished ? 'outline' : 'default'}\n onClick={() => void handleTogglePublish()}\n disabled={publishing || unpublishing || (!portfolio.isPublished && items.length === 0)}\n >\n {portfolio.isPublished ? 'Unpublish' : 'Publish'}\n </Button>\n ) : null\n }\n >\n {portfolio?.isPublished && publicUrl ? (\n <GlassCard className=\"mb-6 flex flex-wrap items-center justify-between gap-3 p-3\">\n <div className=\"flex min-w-0 items-center gap-2 text-xs text-text-secondary\">\n <Link2 size={14} className=\"shrink-0 text-accent-teal\" />\n <span className=\"truncate\">{publicUrl}</span>\n </div>\n <Button variant=\"ghost\" size=\"sm\" onClick={() => void handleCopyLink()}>\n {copied ? <Check size={14} /> : <Copy size={14} />} {copied ? 'Copied' : 'Copy link'}\n </Button>\n </GlassCard>\n ) : null}\n\n <div className=\"mb-4 flex items-center justify-between\">\n <h2 className=\"text-sm font-semibold text-text-primary\">Your items</h2>\n <Button\n size=\"sm\"\n variant={showAdd ? 'outline' : 'default'}\n onClick={() => setShowAdd((v) => !v)}\n >\n {showAdd ? <X size={14} /> : <Plus size={14} />} {showAdd ? 'Close' : 'Add item'}\n </Button>\n </div>\n\n {showAdd ? (\n <GlassCard className=\"mb-6 space-y-4 p-4\">\n <div>\n <h3 className=\"mb-2 text-xs font-semibold uppercase tracking-wide text-text-muted\">\n From certificates\n </h3>\n <AddFromCertificates onAdded={() => void refetch()} />\n </div>\n <div>\n <h3 className=\"mb-2 text-xs font-semibold uppercase tracking-wide text-text-muted\">\n From completed courses\n </h3>\n <AddFromCourses onAdded={() => void refetch()} />\n </div>\n <div>\n <h3 className=\"mb-2 text-xs font-semibold uppercase tracking-wide text-text-muted\">\n From hackathon projects\n </h3>\n <AddFromHackathons onAdded={() => void refetch()} />\n </div>\n </GlassCard>\n ) : null}\n\n {loading && !portfolio ? (\n <LoadingRows count={3} height=\"h-24\" />\n ) : error ? (\n <ErrorView title=\"Couldn't load your portfolio\" error={error} />\n ) : items.length === 0 ? (\n <EmptyView\n icon={<Briefcase size={26} />}\n title=\"Your portfolio is empty\"\n description=\"Add a certificate, completed course, or submitted hackathon project to start building your showcase.\"\n action={\n <Button onClick={() => setShowAdd(true)}>\n <Plus size={15} /> Add your first item\n </Button>\n }\n />\n ) : (\n <div className=\"space-y-3\">\n {items.map((item) => (\n <ItemCard\n key={item.id}\n item={item}\n onRemove={() => void handleRemove(item.id, item.title)}\n />\n ))}\n </div>\n )}\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ApplicationsPortfolioPagePage.js","names":[],"sources":["../../../src/technospam/pages/ApplicationsPortfolioPagePage.tsx"],"sourcesContent":["/**\n * Learner Portfolio (BOFF-4432).\n *\n * Aggregates real completed work — issued certificates, completed courses,\n * and submitted hackathon projects — into a single showcase the learner\n * controls. Every item's title/description/link/completion date is copied\n * server-side from the real source record at add-time; nothing here is\n * free-form/fabricated.\n *\n * DEFERRED SCOPE (documented, not silently dropped): the backend already\n * exposes a public, unauthenticated `tecPublicPortfolio(slug)` field\n * (@rbac public: true) that returns real published-portfolio data. This\n * page generates the slug and lets the learner copy the public link, but\n * there is no unauthenticated *page* yet that renders it — the MFE only\n * mounts inside the authenticated app shell, and alpha gates every web\n * hostname behind Cloudflare Access. Standing up a real public viewer\n * needs app-shell + CF Access allow-list work coordinated with\n * infra/tf-cloudflare, which is out of scope for this pass.\n */\nimport { useState } from 'react';\nimport {\n Award,\n Briefcase,\n Check,\n Copy,\n ExternalLink,\n GraduationCap,\n Link2,\n Plus,\n Trash2,\n Trophy,\n X,\n} from 'lucide-react';\nimport {\n Button,\n GlassCard,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '@burdenoff/fe-libs/ui';\nimport { EmptyView, ErrorView, LoadingRows, PageLayout } from '../components';\nimport { useTechnoSpam } from '../providers/TechnoSpamProvider';\nimport {\n type TecPortfolioItem,\n type TecPortfolioItemKind,\n useAddPortfolioItem,\n useCertificatesQuery,\n useEnrollmentsQuery,\n useHackathonsQuery,\n useMyPortfolioQuery,\n usePublishPortfolio,\n useRemovePortfolioItem,\n useTeamsQuery,\n useUnpublishPortfolio,\n} from '../api/technospamApi';\nimport { formatDate } from '../utils/format';\nimport { notify } from '../lib/toast';\nimport { nativeConfirm } from '@burdenoff/fe-libs/shared/native';\n\nconst KIND_ICON: Record<TecPortfolioItemKind, typeof Award> = {\n HACKATHON_PROJECT: Trophy,\n CERTIFICATE: Award,\n COURSE_COMPLETION: GraduationCap,\n};\n\nconst KIND_LABEL: Record<TecPortfolioItemKind, string> = {\n HACKATHON_PROJECT: 'Hackathon project',\n CERTIFICATE: 'Certificate',\n COURSE_COMPLETION: 'Completed course',\n};\n\nfunction ItemCard({ item, onRemove }: { item: TecPortfolioItem; onRemove: () => void }) {\n const Icon = KIND_ICON[item.kind];\n return (\n <GlassCard className=\"group flex flex-col gap-3 p-4 transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md sm:flex-row sm:items-start sm:justify-between\">\n <div className=\"flex min-w-0 gap-3\">\n <span className=\"flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-accent-purple/15 text-accent-purple ring-1 ring-accent-purple/20\">\n <Icon size={18} />\n </span>\n <div className=\"min-w-0\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <span className=\"truncate text-sm font-semibold text-text-primary\">{item.title}</span>\n <span className=\"rounded-full border border-border-seam px-1.5 py-0.5 text-[10px] font-medium uppercase tracking-wide text-text-muted\">\n {KIND_LABEL[item.kind]}\n </span>\n </div>\n {item.description ? (\n <p className=\"mt-1 line-clamp-2 text-xs text-text-secondary\">{item.description}</p>\n ) : null}\n <div className=\"mt-2 flex flex-wrap items-center gap-3 text-xs text-text-muted\">\n <span>Completed {formatDate(item.completionDate)}</span>\n {item.link ? (\n <a\n href={item.link}\n target=\"_blank\"\n rel=\"noreferrer\"\n className=\"inline-flex items-center gap-1 text-accent-primary hover:underline\"\n >\n <ExternalLink size={12} /> View\n </a>\n ) : null}\n </div>\n </div>\n </div>\n <Button\n variant=\"ghost\"\n size=\"sm\"\n className=\"shrink-0 text-status-error-text hover:text-status-error-text\"\n onClick={onRemove}\n >\n <Trash2 size={14} />\n </Button>\n </GlassCard>\n );\n}\n\nfunction AddFromCertificates({ onAdded }: { onAdded: () => void }) {\n const { currentUser } = useTechnoSpam();\n const learnerId = currentUser?.id;\n const { data } = useCertificatesQuery(learnerId ? { learnerId } : {});\n const [addItem, { loading }] = useAddPortfolioItem();\n const certificates = (data?.tecListCertificates ?? []).filter((c) => c.status === 'ISSUED');\n\n if (certificates.length === 0) {\n return <p className=\"text-xs text-text-muted\">No issued certificates yet.</p>;\n }\n return (\n <div className=\"space-y-2\">\n {certificates.map((c) => (\n <div\n key={c.id}\n className=\"flex items-center justify-between gap-2 rounded-lg border border-border-seam p-2\"\n >\n <span className=\"min-w-0 truncate text-xs text-text-primary\">\n {c.course?.title ?? 'Certificate'} · Serial {c.serial}\n </span>\n <Button\n size=\"sm\"\n variant=\"outline\"\n disabled={loading}\n onClick={async () => {\n const res = await addItem({\n variables: { input: { kind: 'CERTIFICATE', sourceId: c.id } },\n });\n if (res.data?.tecAddPortfolioItem) {\n notify.success('Added to portfolio');\n onAdded();\n }\n }}\n >\n <Plus size={12} /> Add\n </Button>\n </div>\n ))}\n </div>\n );\n}\n\nfunction AddFromCourses({ onAdded }: { onAdded: () => void }) {\n const { currentUser } = useTechnoSpam();\n const learnerId = currentUser?.id;\n const { data } = useEnrollmentsQuery(learnerId ? { learnerId } : {});\n const [addItem, { loading }] = useAddPortfolioItem();\n const completed = (data?.tecListEnrollments?.items ?? []).filter((e) => e.status === 'COMPLETED');\n\n if (completed.length === 0) {\n return <p className=\"text-xs text-text-muted\">No completed courses yet.</p>;\n }\n return (\n <div className=\"space-y-2\">\n {completed.map((e) => (\n <div\n key={e.id}\n className=\"flex items-center justify-between gap-2 rounded-lg border border-border-seam p-2\"\n >\n <span className=\"min-w-0 truncate text-xs text-text-primary\">\n {e.course?.title ?? 'Course'}\n </span>\n <Button\n size=\"sm\"\n variant=\"outline\"\n disabled={loading}\n onClick={async () => {\n const res = await addItem({\n variables: { input: { kind: 'COURSE_COMPLETION', sourceId: e.id } },\n });\n if (res.data?.tecAddPortfolioItem) {\n notify.success('Added to portfolio');\n onAdded();\n }\n }}\n >\n <Plus size={12} /> Add\n </Button>\n </div>\n ))}\n </div>\n );\n}\n\nfunction AddFromHackathons({ onAdded }: { onAdded: () => void }) {\n const { currentUser } = useTechnoSpam();\n const learnerId = currentUser?.id;\n const { data: hackathonData } = useHackathonsQuery({});\n const hackathons = hackathonData?.tecListHackathons?.items ?? [];\n const [hackathonId, setHackathonId] = useState<string>('');\n const { data: teamsData } = useTeamsQuery(hackathonId);\n const [addItem, { loading }] = useAddPortfolioItem();\n\n const myTeams = (teamsData?.tecListTeams?.items ?? []).filter(\n (t) =>\n (t.status === 'SUBMITTED' || t.status === 'WINNER') &&\n (t.captainId === learnerId || t.memberIds.includes(learnerId ?? ''))\n );\n\n return (\n <div className=\"space-y-2\">\n <Select value={hackathonId} onValueChange={setHackathonId}>\n <SelectTrigger className=\"w-full\">\n <SelectValue placeholder=\"Choose a hackathon\" />\n </SelectTrigger>\n <SelectContent>\n {hackathons.map((h) => (\n <SelectItem key={h.id} value={h.id}>\n {h.title}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n {hackathonId && myTeams.length === 0 ? (\n <p className=\"text-xs text-text-muted\">No submitted team of yours in this hackathon.</p>\n ) : null}\n {myTeams.map((t) => (\n <div\n key={t.id}\n className=\"flex items-center justify-between gap-2 rounded-lg border border-border-seam p-2\"\n >\n <span className=\"min-w-0 truncate text-xs text-text-primary\">\n {t.projectName || t.name}\n </span>\n <Button\n size=\"sm\"\n variant=\"outline\"\n disabled={loading}\n onClick={async () => {\n const res = await addItem({\n variables: { input: { kind: 'HACKATHON_PROJECT', sourceId: t.id } },\n });\n if (res.data?.tecAddPortfolioItem) {\n notify.success('Added to portfolio');\n onAdded();\n }\n }}\n >\n <Plus size={12} /> Add\n </Button>\n </div>\n ))}\n </div>\n );\n}\n\nexport function ApplicationsPortfolioPagePage() {\n const { data, loading, error, refetch } = useMyPortfolioQuery();\n const [removeItem] = useRemovePortfolioItem();\n const [publish, { loading: publishing }] = usePublishPortfolio();\n const [unpublish, { loading: unpublishing }] = useUnpublishPortfolio();\n const [showAdd, setShowAdd] = useState(false);\n const [copied, setCopied] = useState(false);\n\n const portfolio = data?.tecMyPortfolio;\n const items = portfolio?.items ?? [];\n\n const publicUrl = portfolio?.publicSlug\n ? `${typeof window !== 'undefined' ? window.location.origin : ''}/technospam/portfolio/${portfolio.publicSlug}`\n : null;\n\n const handleRemove = async (id: string, title: string) => {\n const confirmed = await nativeConfirm({\n title: 'Remove from portfolio',\n message: `Remove \"${title}\" from your portfolio? This does not delete the underlying record.`,\n okButtonTitle: 'Remove',\n cancelButtonTitle: 'Cancel',\n });\n if (!confirmed) return;\n const res = await removeItem({ variables: { id } });\n if (res.data?.tecRemovePortfolioItem) notify.success('Removed from portfolio');\n };\n\n const handleTogglePublish = async () => {\n if (!portfolio) return;\n if (portfolio.isPublished) {\n const res = await unpublish();\n if (res.data?.tecUnpublishPortfolio) notify.success('Portfolio unpublished');\n } else {\n const res = await publish();\n if (res.data?.tecPublishPortfolio) notify.success('Portfolio published');\n }\n };\n\n const handleCopyLink = async () => {\n if (!publicUrl) return;\n await navigator.clipboard.writeText(publicUrl);\n setCopied(true);\n setTimeout(() => setCopied(false), 1500);\n };\n\n return (\n <PageLayout\n hero\n title=\"Portfolio\"\n description=\"Showcase your real completed courses, certificates, and hackathon projects.\"\n actions={\n portfolio ? (\n <Button\n variant={portfolio.isPublished ? 'outline' : 'default'}\n onClick={() => void handleTogglePublish()}\n disabled={publishing || unpublishing || (!portfolio.isPublished && items.length === 0)}\n >\n {portfolio.isPublished ? 'Unpublish' : 'Publish'}\n </Button>\n ) : null\n }\n >\n {portfolio?.isPublished && publicUrl ? (\n <GlassCard className=\"mb-6 flex flex-wrap items-center justify-between gap-3 p-3\">\n <div className=\"flex min-w-0 items-center gap-2 text-xs text-text-secondary\">\n <Link2 size={14} className=\"shrink-0 text-accent-teal\" />\n <span className=\"truncate\">{publicUrl}</span>\n </div>\n <Button variant=\"ghost\" size=\"sm\" onClick={() => void handleCopyLink()}>\n {copied ? <Check size={14} /> : <Copy size={14} />} {copied ? 'Copied' : 'Copy link'}\n </Button>\n </GlassCard>\n ) : null}\n\n <div className=\"mb-4 flex items-center justify-between\">\n <h2 className=\"text-sm font-semibold text-text-primary\">Your items</h2>\n <Button\n size=\"sm\"\n variant={showAdd ? 'outline' : 'default'}\n onClick={() => setShowAdd((v) => !v)}\n >\n {showAdd ? <X size={14} /> : <Plus size={14} />} {showAdd ? 'Close' : 'Add item'}\n </Button>\n </div>\n\n {showAdd ? (\n <GlassCard className=\"mb-6 space-y-4 p-4\">\n <div>\n <h3 className=\"mb-2 text-xs font-semibold uppercase tracking-wide text-text-muted\">\n From certificates\n </h3>\n <AddFromCertificates onAdded={() => void refetch()} />\n </div>\n <div>\n <h3 className=\"mb-2 text-xs font-semibold uppercase tracking-wide text-text-muted\">\n From completed courses\n </h3>\n <AddFromCourses onAdded={() => void refetch()} />\n </div>\n <div>\n <h3 className=\"mb-2 text-xs font-semibold uppercase tracking-wide text-text-muted\">\n From hackathon projects\n </h3>\n <AddFromHackathons onAdded={() => void refetch()} />\n </div>\n </GlassCard>\n ) : null}\n\n {loading && !portfolio ? (\n <LoadingRows count={3} height=\"h-24\" />\n ) : error ? (\n <ErrorView title=\"Couldn't load your portfolio\" error={error} />\n ) : items.length === 0 ? (\n <EmptyView\n icon={<Briefcase size={26} />}\n title=\"Your portfolio is empty\"\n description=\"Add a certificate, completed course, or submitted hackathon project to start building your showcase.\"\n action={\n <Button onClick={() => setShowAdd(true)}>\n <Plus size={15} /> Add your first item\n </Button>\n }\n />\n ) : (\n <div className=\"space-y-3\">\n {items.map((item) => (\n <ItemCard\n key={item.id}\n item={item}\n onRemove={() => void handleRemove(item.id, item.title)}\n />\n ))}\n </div>\n )}\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
@@ -92,7 +92,7 @@ function j() {
|
|
|
92
92
|
h(() => {
|
|
93
93
|
W && !X.current && (q(W.languages[0] ?? A[0]), Y(W.starterCode ?? ""), X.current = !0);
|
|
94
94
|
}, [W]);
|
|
95
|
-
let Z = g(() => [...z?.tecListSubmissions
|
|
95
|
+
let Z = g(() => [...z?.tecListSubmissions?.items ?? []].sort((e, t) => new Date(t.createdAt).getTime() - new Date(e.createdAt).getTime()), [z]), xe = async () => {
|
|
96
96
|
if (!j) return;
|
|
97
97
|
if (!K) {
|
|
98
98
|
m.error("Pick a language", "Choose a language before submitting.");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ApplicationsProblemDetailsPagePage.js","names":[],"sources":["../../../src/technospam/pages/ApplicationsProblemDetailsPagePage.tsx"],"sourcesContent":["import { useEffect, useMemo, useRef, useState } from 'react';\nimport { useSearchParams } from 'react-router';\nimport {\n Clock,\n Code2,\n FileText,\n FlaskConical,\n Layers,\n RefreshCw,\n Send,\n Tag,\n Trophy,\n} from 'lucide-react';\nimport {\n Badge,\n Button,\n GlassCard,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n Skeleton,\n Textarea,\n} from '@burdenoff/fe-libs/ui';\nimport {\n CodePanel,\n EmptyView,\n ErrorView,\n IoBlock,\n LoadingRows,\n PageLayout,\n PremiumBadge,\n StatCard,\n StatusBadge,\n TagStrip,\n TestResultsPanel,\n UpgradeGate,\n} from '../components';\nimport { useTechnoSpam } from '../providers/TechnoSpamProvider';\nimport {\n useProblemQuery,\n useRegradeSubmission,\n useSubmissionsQuery,\n useSubmitSolution,\n type TecSubmission,\n} from '../api/technospamApi';\nimport { formatDateTime, humanizeEnum } from '../utils/format';\nimport { notify } from '../lib/toast';\nimport { useTechnoSpamAnalytics } from '../hooks/useTechnoSpamAnalytics';\n\nconst FALLBACK_LANGUAGES = ['javascript', 'python', 'typescript'];\n\nfunction SubmissionRow({ s }: { s: TecSubmission }) {\n return (\n <div className=\"rounded-lg border border-transparent px-2 py-3 transition-colors hover:border-border-seam hover:bg-bg-sunken\">\n <div className=\"flex flex-wrap items-center gap-x-4 gap-y-1\">\n <StatusBadge status={s.verdict} />\n <span className=\"inline-flex items-center gap-1 text-sm font-medium text-text-primary\">\n <Code2 size={13} /> {humanizeEnum(s.language)}\n </span>\n <span className=\"inline-flex items-center gap-1 text-xs text-text-muted\">\n <Clock size={12} /> {s.runtimeMs} ms\n </span>\n <span className=\"inline-flex items-center gap-1 text-xs text-text-muted\">\n <Trophy size={12} /> {s.pointsAwarded} pts\n </span>\n <span className=\"ml-auto text-xs text-text-muted\">{formatDateTime(s.createdAt)}</span>\n </div>\n {s.totalCount > 0 ? (\n <p className=\"mt-1 px-0.5 text-xs text-text-muted\">\n Passed {s.passedCount} of {s.totalCount} test cases\n </p>\n ) : null}\n </div>\n );\n}\n\nexport function ApplicationsProblemDetailsPagePage() {\n const [params] = useSearchParams();\n const id = params.get('id') ?? '';\n const { currentUser, workspaceId } = useTechnoSpam();\n const learnerId = currentUser?.id;\n const { emit } = useTechnoSpamAnalytics();\n\n const { data, loading, error, refetch } = useProblemQuery(id);\n\n useEffect(() => {\n if (id) {\n emit('technospam.problem.viewed', {\n workspaceId,\n entityId: id,\n entityType: 'TEC_PROBLEM',\n });\n }\n }, [id, workspaceId, emit]);\n const {\n data: subsData,\n loading: subsLoading,\n error: subsError,\n refetch: refetchSubs,\n } = useSubmissionsQuery({ ...(learnerId ? { learnerId } : {}), problemId: id });\n const [submit, { loading: submitting }] = useSubmitSolution();\n const [regrade, { loading: regrading }] = useRegradeSubmission();\n\n const problem = data?.tecProblem ?? null;\n\n const languages = useMemo(\n () => (problem?.languages?.length ? problem.languages : FALLBACK_LANGUAGES),\n [problem]\n );\n\n const [language, setLanguage] = useState('');\n const [code, setCode] = useState('');\n const initedRef = useRef(false);\n\n useEffect(() => {\n if (problem && !initedRef.current) {\n setLanguage(problem.languages[0] ?? FALLBACK_LANGUAGES[0]!);\n setCode(problem.starterCode ?? '');\n initedRef.current = true;\n }\n }, [problem]);\n\n const submissions = useMemo(\n () =>\n [...(subsData?.tecListSubmissions.items ?? [])].sort(\n (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()\n ),\n [subsData]\n );\n\n const handleSubmit = async () => {\n if (!id) return;\n if (!language) {\n notify.error('Pick a language', 'Choose a language before submitting.');\n return;\n }\n if (!code.trim()) {\n notify.error('Add your solution', 'Write some code before submitting.');\n return;\n }\n emit('technospam.problem.submit_clicked', {\n workspaceId,\n entityId: id,\n entityType: 'TEC_PROBLEM',\n language,\n });\n const res = await submit({ variables: { input: { problemId: id, language, code } } });\n if (res.data?.tecSubmitSolution) {\n emit('technospam.problem.submitted', {\n workspaceId,\n entityId: id,\n entityType: 'TEC_PROBLEM',\n submissionId: res.data.tecSubmitSolution.id,\n });\n const graded = res.data.tecSubmitSolution;\n if (graded.verdict === 'PENDING') {\n notify.info(\n 'Solution submitted',\n 'The grader is not available right now — your submission was saved and can be re-run.'\n );\n } else if (graded.verdict === 'ACCEPTED') {\n notify.success('Accepted', `Passed all ${graded.totalCount} test cases.`);\n } else {\n notify.error(\n humanizeEnum(graded.verdict),\n `Passed ${graded.passedCount} of ${graded.totalCount} test cases.`\n );\n }\n await Promise.all([refetch(), refetchSubs()]);\n }\n };\n\n const handleRegrade = async (submissionId: string) => {\n const res = await regrade({ variables: { id: submissionId } });\n const graded = res.data?.tecRegradeSubmission;\n if (!graded) return;\n if (graded.verdict === 'PENDING') {\n notify.error('Still pending', 'The grader is not available yet. Please try again shortly.');\n } else {\n notify.success('Grading complete', `Verdict: ${humanizeEnum(graded.verdict)}.`);\n }\n await Promise.all([refetch(), refetchSubs()]);\n };\n\n /** Most recent submission that actually carries grading detail. */\n const latestGraded = submissions.find((s) => (s.testResults?.length ?? 0) > 0) ?? null;\n const latestSubmission = submissions[0] ?? null;\n\n if (loading && !problem) {\n return (\n <PageLayout title=\"Problem\" backPath=\"/applications/problems-page\">\n <Skeleton className=\"mb-4 h-28 w-full rounded-xl\" />\n <Skeleton className=\"h-72 w-full rounded-xl\" />\n </PageLayout>\n );\n }\n\n if (error || !problem) {\n return (\n <PageLayout title=\"Problem\" backPath=\"/applications/problems-page\">\n {error ? (\n <ErrorView title=\"Couldn't load problem\" error={error} />\n ) : (\n <EmptyView\n title=\"Problem not found\"\n description=\"This problem may have been unpublished.\"\n />\n )}\n </PageLayout>\n );\n }\n\n return (\n <PageLayout\n hero\n backPath=\"/applications/problems-page\"\n title={problem.title}\n description={problem.topic ?? undefined}\n >\n <div className=\"mb-5 flex flex-wrap items-center gap-2\">\n <StatusBadge status={problem.difficulty} kind=\"level\" />\n <StatusBadge status={problem.status} />\n {problem.topic ? (\n <Badge variant=\"outline\" className=\"gap-1\">\n <Layers size={12} /> {problem.topic}\n </Badge>\n ) : null}\n {problem.isPremiumOnly ? <PremiumBadge locked={problem.isLocked} /> : null}\n <TagStrip tags={problem.tags} />\n </div>\n\n <div className=\"mb-6 grid grid-cols-2 gap-3 sm:grid-cols-4\">\n <StatCard label=\"Points\" value={problem.points} icon={Trophy} tone=\"amber\" />\n <StatCard label=\"Languages\" value={languages.length} icon={Code2} tone=\"blue\" />\n <StatCard\n label=\"Difficulty\"\n value={humanizeEnum(problem.difficulty)}\n icon={Layers}\n tone=\"purple\"\n />\n <StatCard label=\"My submissions\" value={submissions.length} icon={FileText} tone=\"teal\" />\n </div>\n\n <div className=\"grid grid-cols-1 gap-5 lg:grid-cols-3\">\n <div className=\"space-y-5 lg:col-span-2\">\n <GlassCard className=\"p-5\">\n <h2 className=\"mb-2 text-base font-semibold text-text-primary\">Problem statement</h2>\n {problem.isLocked ? (\n // BOFF-4416: the API already nulled `statement` for a locked\n // premium problem — this explains why, rather than showing the\n // generic \"being prepared\" message used for a real content gap.\n <UpgradeGate description=\"This problem's statement is part of a paid TechnoSpam plan. Ask a workspace admin to upgrade to unlock it.\" />\n ) : problem.statement ? (\n <p className=\"whitespace-pre-wrap text-sm leading-relaxed text-text-secondary\">\n {problem.statement}\n </p>\n ) : (\n <p className=\"py-4 text-center text-sm text-text-muted\">\n The statement for this problem is being prepared.\n </p>\n )}\n </GlassCard>\n\n {problem.isLocked ? (\n <UpgradeGate\n compact\n description=\"Starter code and sample test cases unlock with a paid TechnoSpam plan.\"\n />\n ) : problem.starterCode ? (\n <GlassCard className=\"p-5\">\n <h2 className=\"mb-3 text-base font-semibold text-text-primary\">Starter code</h2>\n <CodePanel label={humanizeEnum(languages[0] ?? 'code')} bodyClassName=\"max-h-72\">\n {problem.starterCode}\n </CodePanel>\n </GlassCard>\n ) : null}\n\n <GlassCard className=\"p-5\">\n <div className=\"mb-3 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between\">\n <h2 className=\"text-base font-semibold text-text-primary\">Submit a solution</h2>\n <div className=\"flex items-center gap-2\">\n <span className=\"text-sm text-text-secondary\">Language</span>\n <Select value={language} onValueChange={setLanguage}>\n <SelectTrigger className=\"w-full sm:w-48\">\n <SelectValue placeholder=\"Choose a language\" />\n </SelectTrigger>\n <SelectContent>\n {languages.map((l) => (\n <SelectItem key={l} value={l}>\n {humanizeEnum(l)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n </div>\n\n <div className=\"overflow-hidden rounded-xl border border-border-strong bg-bg-sunken shadow-sm\">\n <div className=\"flex items-center justify-between gap-2 border-b border-border-seam bg-bg-canvas/40 px-4 py-2\">\n <div className=\"flex items-center gap-1.5\">\n <span className=\"h-2.5 w-2.5 rounded-full bg-status-error-border\" />\n <span className=\"h-2.5 w-2.5 rounded-full bg-status-warning-border\" />\n <span className=\"h-2.5 w-2.5 rounded-full bg-status-success-border\" />\n </div>\n <span className=\"inline-flex items-center gap-1.5 font-mono text-xs text-text-muted\">\n <Code2 size={13} /> {language ? humanizeEnum(language) : 'solution'}\n </span>\n </div>\n <Textarea\n value={code}\n onChange={(e) => setCode(e.target.value)}\n placeholder=\"Write your solution…\"\n className=\"min-h-[280px] resize-y rounded-none border-0 bg-transparent font-mono text-xs leading-relaxed focus-visible:ring-0 focus-visible:ring-offset-0\"\n spellCheck={false}\n />\n </div>\n\n <Button\n className=\"mt-4 w-full\"\n onClick={handleSubmit}\n disabled={submitting || !code.trim()}\n >\n <Send size={16} /> {submitting ? 'Running test cases…' : 'Run & submit'}\n </Button>\n <p className=\"mt-2 text-center text-xs text-text-muted\">\n Your code runs against this problem's test cases in an isolated sandbox.\n </p>\n </GlassCard>\n\n {problem.publicTestCases?.length ? (\n <GlassCard className=\"p-5\">\n <h2 className=\"mb-1 flex items-center gap-2 text-base font-semibold text-text-primary\">\n <FlaskConical size={16} /> Sample test cases\n </h2>\n <p className=\"mb-3 text-sm text-text-secondary\">\n Your solution reads each input from stdin and must print the expected output. Hidden\n test cases are also run when you submit.\n </p>\n <div className=\"space-y-3\">\n {problem.publicTestCases.map((testCase) => (\n <div key={testCase.id} className=\"rounded-xl border border-border-seam p-3\">\n <div className=\"mb-2 flex flex-wrap items-center gap-x-3 gap-y-1\">\n <span className=\"text-sm font-medium text-text-primary\">{testCase.name}</span>\n <span className=\"inline-flex items-center gap-1 text-xs text-text-muted\">\n <Clock size={12} /> {testCase.timeLimitMs} ms limit\n </span>\n </div>\n <div className=\"grid grid-cols-1 gap-2 sm:grid-cols-2\">\n <IoBlock label=\"Input\" value={testCase.input} />\n <IoBlock label=\"Expected output\" value={testCase.expectedOutput} />\n </div>\n </div>\n ))}\n </div>\n </GlassCard>\n ) : null}\n\n {latestGraded ? (\n <GlassCard className=\"p-5\">\n <h2 className=\"mb-3 text-base font-semibold text-text-primary\">Latest run</h2>\n <TestResultsPanel submission={latestGraded} />\n </GlassCard>\n ) : latestSubmission && latestSubmission.verdict === 'PENDING' ? (\n <GlassCard className=\"p-5\">\n <h2 className=\"mb-2 text-base font-semibold text-text-primary\">Latest run</h2>\n <p className=\"mb-3 text-sm text-text-secondary\">\n Your most recent submission has not been graded yet — the grader was unavailable\n when you submitted. You can re-run it without resubmitting your code.\n </p>\n <Button\n variant=\"outline\"\n onClick={() => void handleRegrade(latestSubmission.id)}\n disabled={regrading}\n >\n <RefreshCw size={16} /> {regrading ? 'Re-running…' : 'Re-run grader'}\n </Button>\n </GlassCard>\n ) : null}\n </div>\n\n <div className=\"lg:col-span-1\">\n <GlassCard className=\"p-5\">\n <h2 className=\"mb-3 text-base font-semibold text-text-primary\">My submissions</h2>\n {subsLoading && !submissions.length ? (\n <LoadingRows count={3} height=\"h-12\" />\n ) : subsError ? (\n <ErrorView title=\"Couldn't load submissions\" error={subsError} />\n ) : submissions.length === 0 ? (\n <EmptyView\n icon={<FileText size={24} />}\n title=\"No submissions yet\"\n description=\"Submit a solution above to see your verdicts and points here.\"\n />\n ) : (\n <div className=\"space-y-1\">\n {submissions.map((s) => (\n <SubmissionRow key={s.id} s={s} />\n ))}\n </div>\n )}\n </GlassCard>\n </div>\n </div>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ApplicationsProblemDetailsPagePage.js","names":[],"sources":["../../../src/technospam/pages/ApplicationsProblemDetailsPagePage.tsx"],"sourcesContent":["import { useEffect, useMemo, useRef, useState } from 'react';\nimport { useSearchParams } from 'react-router';\nimport {\n Clock,\n Code2,\n FileText,\n FlaskConical,\n Layers,\n RefreshCw,\n Send,\n Tag,\n Trophy,\n} from 'lucide-react';\nimport {\n Badge,\n Button,\n GlassCard,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n Skeleton,\n Textarea,\n} from '@burdenoff/fe-libs/ui';\nimport {\n CodePanel,\n EmptyView,\n ErrorView,\n IoBlock,\n LoadingRows,\n PageLayout,\n PremiumBadge,\n StatCard,\n StatusBadge,\n TagStrip,\n TestResultsPanel,\n UpgradeGate,\n} from '../components';\nimport { useTechnoSpam } from '../providers/TechnoSpamProvider';\nimport {\n useProblemQuery,\n useRegradeSubmission,\n useSubmissionsQuery,\n useSubmitSolution,\n type TecSubmission,\n} from '../api/technospamApi';\nimport { formatDateTime, humanizeEnum } from '../utils/format';\nimport { notify } from '../lib/toast';\nimport { useTechnoSpamAnalytics } from '../hooks/useTechnoSpamAnalytics';\n\nconst FALLBACK_LANGUAGES = ['javascript', 'python', 'typescript'];\n\nfunction SubmissionRow({ s }: { s: TecSubmission }) {\n return (\n <div className=\"rounded-lg border border-transparent px-2 py-3 transition-colors hover:border-border-seam hover:bg-bg-sunken\">\n <div className=\"flex flex-wrap items-center gap-x-4 gap-y-1\">\n <StatusBadge status={s.verdict} />\n <span className=\"inline-flex items-center gap-1 text-sm font-medium text-text-primary\">\n <Code2 size={13} /> {humanizeEnum(s.language)}\n </span>\n <span className=\"inline-flex items-center gap-1 text-xs text-text-muted\">\n <Clock size={12} /> {s.runtimeMs} ms\n </span>\n <span className=\"inline-flex items-center gap-1 text-xs text-text-muted\">\n <Trophy size={12} /> {s.pointsAwarded} pts\n </span>\n <span className=\"ml-auto text-xs text-text-muted\">{formatDateTime(s.createdAt)}</span>\n </div>\n {s.totalCount > 0 ? (\n <p className=\"mt-1 px-0.5 text-xs text-text-muted\">\n Passed {s.passedCount} of {s.totalCount} test cases\n </p>\n ) : null}\n </div>\n );\n}\n\nexport function ApplicationsProblemDetailsPagePage() {\n const [params] = useSearchParams();\n const id = params.get('id') ?? '';\n const { currentUser, workspaceId } = useTechnoSpam();\n const learnerId = currentUser?.id;\n const { emit } = useTechnoSpamAnalytics();\n\n const { data, loading, error, refetch } = useProblemQuery(id);\n\n useEffect(() => {\n if (id) {\n emit('technospam.problem.viewed', {\n workspaceId,\n entityId: id,\n entityType: 'TEC_PROBLEM',\n });\n }\n }, [id, workspaceId, emit]);\n const {\n data: subsData,\n loading: subsLoading,\n error: subsError,\n refetch: refetchSubs,\n } = useSubmissionsQuery({ ...(learnerId ? { learnerId } : {}), problemId: id });\n const [submit, { loading: submitting }] = useSubmitSolution();\n const [regrade, { loading: regrading }] = useRegradeSubmission();\n\n const problem = data?.tecProblem ?? null;\n\n const languages = useMemo(\n () => (problem?.languages?.length ? problem.languages : FALLBACK_LANGUAGES),\n [problem]\n );\n\n const [language, setLanguage] = useState('');\n const [code, setCode] = useState('');\n const initedRef = useRef(false);\n\n useEffect(() => {\n if (problem && !initedRef.current) {\n setLanguage(problem.languages[0] ?? FALLBACK_LANGUAGES[0]!);\n setCode(problem.starterCode ?? '');\n initedRef.current = true;\n }\n }, [problem]);\n\n const submissions = useMemo(\n () =>\n [...(subsData?.tecListSubmissions?.items ?? [])].sort(\n (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()\n ),\n [subsData]\n );\n\n const handleSubmit = async () => {\n if (!id) return;\n if (!language) {\n notify.error('Pick a language', 'Choose a language before submitting.');\n return;\n }\n if (!code.trim()) {\n notify.error('Add your solution', 'Write some code before submitting.');\n return;\n }\n emit('technospam.problem.submit_clicked', {\n workspaceId,\n entityId: id,\n entityType: 'TEC_PROBLEM',\n language,\n });\n const res = await submit({ variables: { input: { problemId: id, language, code } } });\n if (res.data?.tecSubmitSolution) {\n emit('technospam.problem.submitted', {\n workspaceId,\n entityId: id,\n entityType: 'TEC_PROBLEM',\n submissionId: res.data.tecSubmitSolution.id,\n });\n const graded = res.data.tecSubmitSolution;\n if (graded.verdict === 'PENDING') {\n notify.info(\n 'Solution submitted',\n 'The grader is not available right now — your submission was saved and can be re-run.'\n );\n } else if (graded.verdict === 'ACCEPTED') {\n notify.success('Accepted', `Passed all ${graded.totalCount} test cases.`);\n } else {\n notify.error(\n humanizeEnum(graded.verdict),\n `Passed ${graded.passedCount} of ${graded.totalCount} test cases.`\n );\n }\n await Promise.all([refetch(), refetchSubs()]);\n }\n };\n\n const handleRegrade = async (submissionId: string) => {\n const res = await regrade({ variables: { id: submissionId } });\n const graded = res.data?.tecRegradeSubmission;\n if (!graded) return;\n if (graded.verdict === 'PENDING') {\n notify.error('Still pending', 'The grader is not available yet. Please try again shortly.');\n } else {\n notify.success('Grading complete', `Verdict: ${humanizeEnum(graded.verdict)}.`);\n }\n await Promise.all([refetch(), refetchSubs()]);\n };\n\n /** Most recent submission that actually carries grading detail. */\n const latestGraded = submissions.find((s) => (s.testResults?.length ?? 0) > 0) ?? null;\n const latestSubmission = submissions[0] ?? null;\n\n if (loading && !problem) {\n return (\n <PageLayout title=\"Problem\" backPath=\"/applications/problems-page\">\n <Skeleton className=\"mb-4 h-28 w-full rounded-xl\" />\n <Skeleton className=\"h-72 w-full rounded-xl\" />\n </PageLayout>\n );\n }\n\n if (error || !problem) {\n return (\n <PageLayout title=\"Problem\" backPath=\"/applications/problems-page\">\n {error ? (\n <ErrorView title=\"Couldn't load problem\" error={error} />\n ) : (\n <EmptyView\n title=\"Problem not found\"\n description=\"This problem may have been unpublished.\"\n />\n )}\n </PageLayout>\n );\n }\n\n return (\n <PageLayout\n hero\n backPath=\"/applications/problems-page\"\n title={problem.title}\n description={problem.topic ?? undefined}\n >\n <div className=\"mb-5 flex flex-wrap items-center gap-2\">\n <StatusBadge status={problem.difficulty} kind=\"level\" />\n <StatusBadge status={problem.status} />\n {problem.topic ? (\n <Badge variant=\"outline\" className=\"gap-1\">\n <Layers size={12} /> {problem.topic}\n </Badge>\n ) : null}\n {problem.isPremiumOnly ? <PremiumBadge locked={problem.isLocked} /> : null}\n <TagStrip tags={problem.tags} />\n </div>\n\n <div className=\"mb-6 grid grid-cols-2 gap-3 sm:grid-cols-4\">\n <StatCard label=\"Points\" value={problem.points} icon={Trophy} tone=\"amber\" />\n <StatCard label=\"Languages\" value={languages.length} icon={Code2} tone=\"blue\" />\n <StatCard\n label=\"Difficulty\"\n value={humanizeEnum(problem.difficulty)}\n icon={Layers}\n tone=\"purple\"\n />\n <StatCard label=\"My submissions\" value={submissions.length} icon={FileText} tone=\"teal\" />\n </div>\n\n <div className=\"grid grid-cols-1 gap-5 lg:grid-cols-3\">\n <div className=\"space-y-5 lg:col-span-2\">\n <GlassCard className=\"p-5\">\n <h2 className=\"mb-2 text-base font-semibold text-text-primary\">Problem statement</h2>\n {problem.isLocked ? (\n // BOFF-4416: the API already nulled `statement` for a locked\n // premium problem — this explains why, rather than showing the\n // generic \"being prepared\" message used for a real content gap.\n <UpgradeGate description=\"This problem's statement is part of a paid TechnoSpam plan. Ask a workspace admin to upgrade to unlock it.\" />\n ) : problem.statement ? (\n <p className=\"whitespace-pre-wrap text-sm leading-relaxed text-text-secondary\">\n {problem.statement}\n </p>\n ) : (\n <p className=\"py-4 text-center text-sm text-text-muted\">\n The statement for this problem is being prepared.\n </p>\n )}\n </GlassCard>\n\n {problem.isLocked ? (\n <UpgradeGate\n compact\n description=\"Starter code and sample test cases unlock with a paid TechnoSpam plan.\"\n />\n ) : problem.starterCode ? (\n <GlassCard className=\"p-5\">\n <h2 className=\"mb-3 text-base font-semibold text-text-primary\">Starter code</h2>\n <CodePanel label={humanizeEnum(languages[0] ?? 'code')} bodyClassName=\"max-h-72\">\n {problem.starterCode}\n </CodePanel>\n </GlassCard>\n ) : null}\n\n <GlassCard className=\"p-5\">\n <div className=\"mb-3 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between\">\n <h2 className=\"text-base font-semibold text-text-primary\">Submit a solution</h2>\n <div className=\"flex items-center gap-2\">\n <span className=\"text-sm text-text-secondary\">Language</span>\n <Select value={language} onValueChange={setLanguage}>\n <SelectTrigger className=\"w-full sm:w-48\">\n <SelectValue placeholder=\"Choose a language\" />\n </SelectTrigger>\n <SelectContent>\n {languages.map((l) => (\n <SelectItem key={l} value={l}>\n {humanizeEnum(l)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n </div>\n\n <div className=\"overflow-hidden rounded-xl border border-border-strong bg-bg-sunken shadow-sm\">\n <div className=\"flex items-center justify-between gap-2 border-b border-border-seam bg-bg-canvas/40 px-4 py-2\">\n <div className=\"flex items-center gap-1.5\">\n <span className=\"h-2.5 w-2.5 rounded-full bg-status-error-border\" />\n <span className=\"h-2.5 w-2.5 rounded-full bg-status-warning-border\" />\n <span className=\"h-2.5 w-2.5 rounded-full bg-status-success-border\" />\n </div>\n <span className=\"inline-flex items-center gap-1.5 font-mono text-xs text-text-muted\">\n <Code2 size={13} /> {language ? humanizeEnum(language) : 'solution'}\n </span>\n </div>\n <Textarea\n value={code}\n onChange={(e) => setCode(e.target.value)}\n placeholder=\"Write your solution…\"\n className=\"min-h-[280px] resize-y rounded-none border-0 bg-transparent font-mono text-xs leading-relaxed focus-visible:ring-0 focus-visible:ring-offset-0\"\n spellCheck={false}\n />\n </div>\n\n <Button\n className=\"mt-4 w-full\"\n onClick={handleSubmit}\n disabled={submitting || !code.trim()}\n >\n <Send size={16} /> {submitting ? 'Running test cases…' : 'Run & submit'}\n </Button>\n <p className=\"mt-2 text-center text-xs text-text-muted\">\n Your code runs against this problem's test cases in an isolated sandbox.\n </p>\n </GlassCard>\n\n {problem.publicTestCases?.length ? (\n <GlassCard className=\"p-5\">\n <h2 className=\"mb-1 flex items-center gap-2 text-base font-semibold text-text-primary\">\n <FlaskConical size={16} /> Sample test cases\n </h2>\n <p className=\"mb-3 text-sm text-text-secondary\">\n Your solution reads each input from stdin and must print the expected output. Hidden\n test cases are also run when you submit.\n </p>\n <div className=\"space-y-3\">\n {problem.publicTestCases.map((testCase) => (\n <div key={testCase.id} className=\"rounded-xl border border-border-seam p-3\">\n <div className=\"mb-2 flex flex-wrap items-center gap-x-3 gap-y-1\">\n <span className=\"text-sm font-medium text-text-primary\">{testCase.name}</span>\n <span className=\"inline-flex items-center gap-1 text-xs text-text-muted\">\n <Clock size={12} /> {testCase.timeLimitMs} ms limit\n </span>\n </div>\n <div className=\"grid grid-cols-1 gap-2 sm:grid-cols-2\">\n <IoBlock label=\"Input\" value={testCase.input} />\n <IoBlock label=\"Expected output\" value={testCase.expectedOutput} />\n </div>\n </div>\n ))}\n </div>\n </GlassCard>\n ) : null}\n\n {latestGraded ? (\n <GlassCard className=\"p-5\">\n <h2 className=\"mb-3 text-base font-semibold text-text-primary\">Latest run</h2>\n <TestResultsPanel submission={latestGraded} />\n </GlassCard>\n ) : latestSubmission && latestSubmission.verdict === 'PENDING' ? (\n <GlassCard className=\"p-5\">\n <h2 className=\"mb-2 text-base font-semibold text-text-primary\">Latest run</h2>\n <p className=\"mb-3 text-sm text-text-secondary\">\n Your most recent submission has not been graded yet — the grader was unavailable\n when you submitted. You can re-run it without resubmitting your code.\n </p>\n <Button\n variant=\"outline\"\n onClick={() => void handleRegrade(latestSubmission.id)}\n disabled={regrading}\n >\n <RefreshCw size={16} /> {regrading ? 'Re-running…' : 'Re-run grader'}\n </Button>\n </GlassCard>\n ) : null}\n </div>\n\n <div className=\"lg:col-span-1\">\n <GlassCard className=\"p-5\">\n <h2 className=\"mb-3 text-base font-semibold text-text-primary\">My submissions</h2>\n {subsLoading && !submissions.length ? (\n <LoadingRows count={3} height=\"h-12\" />\n ) : subsError ? (\n <ErrorView title=\"Couldn't load submissions\" error={subsError} />\n ) : submissions.length === 0 ? (\n <EmptyView\n icon={<FileText size={24} />}\n title=\"No submissions yet\"\n description=\"Submit a solution above to see your verdicts and points here.\"\n />\n ) : (\n <div className=\"space-y-1\">\n {submissions.map((s) => (\n <SubmissionRow key={s.id} s={s} />\n ))}\n </div>\n )}\n </GlassCard>\n </div>\n </div>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ApplicationsProblemsPagePage.d.ts","sourceRoot":"","sources":["../../../src/technospam/pages/ApplicationsProblemsPagePage.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"ApplicationsProblemsPagePage.d.ts","sourceRoot":"","sources":["../../../src/technospam/pages/ApplicationsProblemsPagePage.tsx"],"names":[],"mappings":"AA+IA,wBAAgB,4BAA4B,gCAuP3C"}
|