@burdenoff/microfe-buildmyiq 2026.709.1 → 2026.709.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/buildmyiq/components/Avatar.js.map +1 -1
- package/dist/buildmyiq/components/Viz.js.map +1 -1
- package/dist/buildmyiq/pages/assessments/AssessmentDetailPage.js.map +1 -1
- package/dist/buildmyiq/pages/assessments/TakeAssessmentPage.js.map +1 -1
- package/dist/buildmyiq/pages/authoring/CourseBuilderPage.js.map +1 -1
- package/dist/buildmyiq/pages/certificates/CertificatesPage.js.map +1 -1
- package/dist/buildmyiq/pages/insights/InsightsPage.js.map +1 -1
- package/dist/buildmyiq/pages/learning/MyLearningPage.js.map +1 -1
- package/dist/buildmyiq/pages/paths/PathsPage.js.map +1 -1
- package/dist/buildmyiq/pages/skills/SkillsPage.js.map +1 -1
- package/dist/buildmyiq/pages/submissions/SubmissionsPage.js +4 -8
- package/dist/buildmyiq/pages/submissions/SubmissionsPage.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Avatar.js","names":[],"sources":["../../../src/buildmyiq/components/Avatar.tsx"],"sourcesContent":["/**\n * Avatar — a deterministic initials avatar (with optional real image).\n * @module buildmyiq/components\n *\n * HONEST BY DESIGN: when we have a real name/photo (the signed-in user) we show\n * it; for anyone we can only identify by id we render initials derived from the\n * id on a deterministic token-based tint — never a fabricated name or stock\n * photo. The colour is picked from a fixed set of semantic accent tints so it is\n * theme-safe and reproducible for the same seed.\n */\n\nimport type { FC } from 'react';\nimport { cn } from '../utils/cn';\n\ninterface AvatarProps {\n /** Display name — its initials are shown when no image is provided. */\n name?: string | null;\n /** Stable seed (e.g. a user id) used for the tint + fallback initials. */\n seed?: string | null;\n /** Real image URL (only for the signed-in user / resolved members). */\n src?: string | null;\n size?: 'xs' | 'sm' | 'md' | 'lg';\n className?: string;\n}\n\nconst SIZE: Record<NonNullable<AvatarProps['size']>, string> = {\n xs: 'h-6 w-6 text-[10px]',\n sm: 'h-8 w-8 text-xs',\n md: 'h-10 w-10 text-sm',\n lg: 'h-12 w-12 text-base',\n};\n\n// Theme-safe tint set (semantic CSS vars → always resolve in light/dark).\nconst TINTS = [\n 'bg-[var(--color-accent-soft)] text-text-primary',\n 'bg-status-success-bg-subtle text-status-success-text',\n 'bg-status-warning-bg-subtle text-status-warning-text',\n 'bg-[color-mix(in_srgb,var(--color-accent-teal)_18%,transparent)] text-[var(--color-accent-teal)]',\n 'bg-[color-mix(in_srgb,var(--color-accent-primary)_16%,transparent)] text-[var(--color-accent-primary)]',\n 'bg-bg-sunken text-text-secondary',\n];\n\nconst hash = (s: string): number => {\n let h = 0;\n for (let i = 0; i < s.length; i += 1) h = (h * 31 + s.charCodeAt(i)) | 0;\n return Math.abs(h);\n};\n\nconst initialsOf = (name?: string | null, seed?: string | null): string => {\n const src = (name ?? '').trim();\n if (src) {\n const parts = src.split(/\\s+/).filter(Boolean);\n if (parts.length >= 2) return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();\n return src.slice(0, 2).toUpperCase();\n }\n return (seed ?? '?').replace(/[^a-zA-Z0-9]/g, '').slice(0, 2).toUpperCase() || '?';\n};\n\nexport const Avatar: FC<AvatarProps> = ({ name, seed, src, size = 'md', className }) => {\n const tint = TINTS[hash(seed ?? name ?? '?') % TINTS.length];\n if (src) {\n return (\n <img\n src={src}\n alt=\"\"\n className={cn('shrink-0 rounded-full object-cover', SIZE[size], className)}\n />\n );\n }\n return (\n <span\n aria-hidden=\"true\"\n className={cn(\n 'inline-flex shrink-0 select-none items-center justify-center rounded-full font-semibold',\n SIZE[size],\n tint,\n className\n )}\n >\n {initialsOf(name, seed)}\n </span>\n );\n};\n"],"mappings":";;;AAyBA,IAAM,IAAyD;CAC7D,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,IAAI;CACL,EAGK,IAAQ;CACZ;CACA;CACA;CACA;CACA;CACA;CACD,EAEK,KAAQ,MAAsB;CAClC,IAAI,IAAI;AACR,MAAK,IAAI,IAAI,GAAG,IAAI,EAAE,QAAQ,KAAK,EAAG,KAAK,IAAI,KAAK,EAAE,WAAW,EAAE,GAAI;AACvE,QAAO,KAAK,IAAI,EAAE;GAGd,KAAc,GAAsB,MAAiC;CACzE,IAAM,KAAO,KAAQ,IAAI,MAAM;AAC/B,KAAI,GAAK;EACP,IAAM,IAAQ,EAAI,MAAM,MAAM,CAAC,OAAO,QAAQ;AAE9C,SADI,EAAM,UAAU,KAAW,EAAM,GAAG,KAAK,EAAM,EAAM,SAAS,GAAG,IAAI,aAAa,GAC/E,EAAI,MAAM,GAAG,EAAE,CAAC,aAAa;;AAEtC,
|
|
1
|
+
{"version":3,"file":"Avatar.js","names":[],"sources":["../../../src/buildmyiq/components/Avatar.tsx"],"sourcesContent":["/**\n * Avatar — a deterministic initials avatar (with optional real image).\n * @module buildmyiq/components\n *\n * HONEST BY DESIGN: when we have a real name/photo (the signed-in user) we show\n * it; for anyone we can only identify by id we render initials derived from the\n * id on a deterministic token-based tint — never a fabricated name or stock\n * photo. The colour is picked from a fixed set of semantic accent tints so it is\n * theme-safe and reproducible for the same seed.\n */\n\nimport type { FC } from 'react';\nimport { cn } from '../utils/cn';\n\ninterface AvatarProps {\n /** Display name — its initials are shown when no image is provided. */\n name?: string | null;\n /** Stable seed (e.g. a user id) used for the tint + fallback initials. */\n seed?: string | null;\n /** Real image URL (only for the signed-in user / resolved members). */\n src?: string | null;\n size?: 'xs' | 'sm' | 'md' | 'lg';\n className?: string;\n}\n\nconst SIZE: Record<NonNullable<AvatarProps['size']>, string> = {\n xs: 'h-6 w-6 text-[10px]',\n sm: 'h-8 w-8 text-xs',\n md: 'h-10 w-10 text-sm',\n lg: 'h-12 w-12 text-base',\n};\n\n// Theme-safe tint set (semantic CSS vars → always resolve in light/dark).\nconst TINTS = [\n 'bg-[var(--color-accent-soft)] text-text-primary',\n 'bg-status-success-bg-subtle text-status-success-text',\n 'bg-status-warning-bg-subtle text-status-warning-text',\n 'bg-[color-mix(in_srgb,var(--color-accent-teal)_18%,transparent)] text-[var(--color-accent-teal)]',\n 'bg-[color-mix(in_srgb,var(--color-accent-primary)_16%,transparent)] text-[var(--color-accent-primary)]',\n 'bg-bg-sunken text-text-secondary',\n];\n\nconst hash = (s: string): number => {\n let h = 0;\n for (let i = 0; i < s.length; i += 1) h = (h * 31 + s.charCodeAt(i)) | 0;\n return Math.abs(h);\n};\n\nconst initialsOf = (name?: string | null, seed?: string | null): string => {\n const src = (name ?? '').trim();\n if (src) {\n const parts = src.split(/\\s+/).filter(Boolean);\n if (parts.length >= 2) return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();\n return src.slice(0, 2).toUpperCase();\n }\n return (\n (seed ?? '?')\n .replace(/[^a-zA-Z0-9]/g, '')\n .slice(0, 2)\n .toUpperCase() || '?'\n );\n};\n\nexport const Avatar: FC<AvatarProps> = ({ name, seed, src, size = 'md', className }) => {\n const tint = TINTS[hash(seed ?? name ?? '?') % TINTS.length];\n if (src) {\n return (\n <img\n src={src}\n alt=\"\"\n className={cn('shrink-0 rounded-full object-cover', SIZE[size], className)}\n />\n );\n }\n return (\n <span\n aria-hidden=\"true\"\n className={cn(\n 'inline-flex shrink-0 select-none items-center justify-center rounded-full font-semibold',\n SIZE[size],\n tint,\n className\n )}\n >\n {initialsOf(name, seed)}\n </span>\n );\n};\n"],"mappings":";;;AAyBA,IAAM,IAAyD;CAC7D,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,IAAI;CACL,EAGK,IAAQ;CACZ;CACA;CACA;CACA;CACA;CACA;CACD,EAEK,KAAQ,MAAsB;CAClC,IAAI,IAAI;AACR,MAAK,IAAI,IAAI,GAAG,IAAI,EAAE,QAAQ,KAAK,EAAG,KAAK,IAAI,KAAK,EAAE,WAAW,EAAE,GAAI;AACvE,QAAO,KAAK,IAAI,EAAE;GAGd,KAAc,GAAsB,MAAiC;CACzE,IAAM,KAAO,KAAQ,IAAI,MAAM;AAC/B,KAAI,GAAK;EACP,IAAM,IAAQ,EAAI,MAAM,MAAM,CAAC,OAAO,QAAQ;AAE9C,SADI,EAAM,UAAU,KAAW,EAAM,GAAG,KAAK,EAAM,EAAM,SAAS,GAAG,IAAI,aAAa,GAC/E,EAAI,MAAM,GAAG,EAAE,CAAC,aAAa;;AAEtC,SACG,KAAQ,KACN,QAAQ,iBAAiB,GAAG,CAC5B,MAAM,GAAG,EAAE,CACX,aAAa,IAAI;GAIX,KAA2B,EAAE,SAAM,SAAM,QAAK,UAAO,MAAM,mBAAgB;CACtF,IAAM,IAAO,EAAM,EAAK,KAAQ,KAAQ,IAAI,GAAG,EAAM;AAUrD,QATI,IAEA,kBAAC,OAAD;EACO;EACL,KAAI;EACJ,WAAW,EAAG,sCAAsC,EAAK,IAAO,EAAU;EAC1E,CAAA,GAIJ,kBAAC,QAAD;EACE,eAAY;EACZ,WAAW,EACT,2FACA,EAAK,IACL,GACA,EACD;YAEA,EAAW,GAAM,EAAK;EAClB,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Viz.js","names":[],"sources":["../../../src/buildmyiq/components/Viz.tsx"],"sourcesContent":["/**\n * Viz — tiny self-contained tokenized SVG chart primitives.\n * @module buildmyiq/components\n *\n * ProgressRing (circular % meter) and Sparkline (trend line). Both are pure SVG,\n * driven by semantic CSS-variable colours (no raw hex), and take numeric data\n * only — the caller always passes REAL derived values.\n */\n\nimport type { FC } from 'react';\nimport { cn } from '../utils/cn';\n\ninterface ProgressRingProps {\n /** 0–100. */\n value: number;\n size?: number;\n stroke?: number;\n /** CSS colour for the arc (defaults to the teal accent). */\n color?: string;\n /** Center label; when omitted the rounded percent is shown. */\n label?: string;\n className?: string;\n}\n\nexport const ProgressRing: FC<ProgressRingProps> = ({\n value,\n size = 64,\n stroke = 6,\n color = 'var(--color-accent-teal)',\n label,\n className,\n}) => {\n const clamped = Math.max(0, Math.min(100, value));\n const r = (size - stroke) / 2;\n const c = 2 * Math.PI * r;\n const offset = c - (clamped / 100) * c;\n return (\n <div
|
|
1
|
+
{"version":3,"file":"Viz.js","names":[],"sources":["../../../src/buildmyiq/components/Viz.tsx"],"sourcesContent":["/**\n * Viz — tiny self-contained tokenized SVG chart primitives.\n * @module buildmyiq/components\n *\n * ProgressRing (circular % meter) and Sparkline (trend line). Both are pure SVG,\n * driven by semantic CSS-variable colours (no raw hex), and take numeric data\n * only — the caller always passes REAL derived values.\n */\n\nimport type { FC } from 'react';\nimport { cn } from '../utils/cn';\n\ninterface ProgressRingProps {\n /** 0–100. */\n value: number;\n size?: number;\n stroke?: number;\n /** CSS colour for the arc (defaults to the teal accent). */\n color?: string;\n /** Center label; when omitted the rounded percent is shown. */\n label?: string;\n className?: string;\n}\n\nexport const ProgressRing: FC<ProgressRingProps> = ({\n value,\n size = 64,\n stroke = 6,\n color = 'var(--color-accent-teal)',\n label,\n className,\n}) => {\n const clamped = Math.max(0, Math.min(100, value));\n const r = (size - stroke) / 2;\n const c = 2 * Math.PI * r;\n const offset = c - (clamped / 100) * c;\n return (\n <div\n className={cn('relative inline-flex items-center justify-center', className)}\n style={{ width: size, height: size }}\n >\n <svg width={size} height={size} className=\"-rotate-90\">\n <circle\n cx={size / 2}\n cy={size / 2}\n r={r}\n fill=\"none\"\n stroke=\"var(--color-bg-sunken)\"\n strokeWidth={stroke}\n />\n <circle\n cx={size / 2}\n cy={size / 2}\n r={r}\n fill=\"none\"\n stroke={color}\n strokeWidth={stroke}\n strokeLinecap=\"round\"\n strokeDasharray={c}\n strokeDashoffset={offset}\n className=\"transition-[stroke-dashoffset] duration-700 ease-out\"\n />\n </svg>\n <span className=\"absolute text-xs font-semibold tabular-nums text-text-primary\">\n {label ?? `${Math.round(clamped)}%`}\n </span>\n </div>\n );\n};\n\ninterface SparklineProps {\n /** Series of numbers; rendered as a smooth trend line. */\n data: number[];\n width?: number;\n height?: number;\n color?: string;\n className?: string;\n}\n\nexport const Sparkline: FC<SparklineProps> = ({\n data,\n width = 96,\n height = 28,\n color = 'var(--color-accent-teal)',\n className,\n}) => {\n if (data.length < 2) {\n return (\n <svg width={width} height={height} className={className} aria-hidden=\"true\">\n <line\n x1=\"0\"\n y1={height - 2}\n x2={width}\n y2={height - 2}\n stroke=\"var(--color-border-seam)\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n />\n </svg>\n );\n }\n const min = Math.min(...data);\n const max = Math.max(...data);\n const span = max - min || 1;\n const stepX = width / (data.length - 1);\n const pts = data.map((v, i) => {\n const x = i * stepX;\n const y = height - 3 - ((v - min) / span) * (height - 6);\n return `${x.toFixed(1)},${y.toFixed(1)}`;\n });\n return (\n <svg width={width} height={height} className={className} aria-hidden=\"true\">\n <polyline\n points={pts.join(' ')}\n fill=\"none\"\n stroke={color}\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n );\n};\n"],"mappings":";;;AAwBA,IAAa,KAAuC,EAClD,UACA,UAAO,IACP,YAAS,GACT,WAAQ,4BACR,UACA,mBACI;CACJ,IAAM,IAAU,KAAK,IAAI,GAAG,KAAK,IAAI,KAAK,EAAM,CAAC,EAC3C,KAAK,IAAO,KAAU,GACtB,IAAI,IAAI,KAAK,KAAK,GAClB,IAAS,IAAK,IAAU,MAAO;AACrC,QACE,kBAAC,OAAD;EACE,WAAW,EAAG,oDAAoD,EAAU;EAC5E,OAAO;GAAE,OAAO;GAAM,QAAQ;GAAM;YAFtC,CAIE,kBAAC,OAAD;GAAK,OAAO;GAAM,QAAQ;GAAM,WAAU;aAA1C,CACE,kBAAC,UAAD;IACE,IAAI,IAAO;IACX,IAAI,IAAO;IACR;IACH,MAAK;IACL,QAAO;IACP,aAAa;IACb,CAAA,EACF,kBAAC,UAAD;IACE,IAAI,IAAO;IACX,IAAI,IAAO;IACR;IACH,MAAK;IACL,QAAQ;IACR,aAAa;IACb,eAAc;IACd,iBAAiB;IACjB,kBAAkB;IAClB,WAAU;IACV,CAAA,CACE;MACN,kBAAC,QAAD;GAAM,WAAU;aACb,KAAS,GAAG,KAAK,MAAM,EAAQ,CAAC;GAC5B,CAAA,CACH;;GAaG,KAAiC,EAC5C,SACA,WAAQ,IACR,YAAS,IACT,WAAQ,4BACR,mBACI;AACJ,KAAI,EAAK,SAAS,EAChB,QACE,kBAAC,OAAD;EAAY;EAAe;EAAmB;EAAW,eAAY;YACnE,kBAAC,QAAD;GACE,IAAG;GACH,IAAI,IAAS;GACb,IAAI;GACJ,IAAI,IAAS;GACb,QAAO;GACP,aAAY;GACZ,eAAc;GACd,CAAA;EACE,CAAA;CAGV,IAAM,IAAM,KAAK,IAAI,GAAG,EAAK,EAEvB,IADM,KAAK,IAAI,GAAG,EAAK,GACV,KAAO,GACpB,IAAQ,KAAS,EAAK,SAAS;AAMrC,QACE,kBAAC,OAAD;EAAY;EAAe;EAAmB;EAAW,eAAY;YACnE,kBAAC,YAAD;GACE,QARM,EAAK,KAAK,GAAG,MAAM;IAC7B,IAAM,IAAI,IAAI,GACR,IAAI,IAAS,KAAM,IAAI,KAAO,KAAS,IAAS;AACtD,WAAO,GAAG,EAAE,QAAQ,EAAE,CAAC,GAAG,EAAE,QAAQ,EAAE;KACtC,CAIgB,KAAK,IAAI;GACrB,MAAK;GACL,QAAQ;GACR,aAAY;GACZ,eAAc;GACd,gBAAe;GACf,CAAA;EACE,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AssessmentDetailPage.js","names":[],"sources":["../../../../src/buildmyiq/pages/assessments/AssessmentDetailPage.tsx"],"sourcesContent":["/**\n * AssessmentDetailPage — a single assessment + its question builder.\n * @module buildmyiq/pages/assessments\n *\n * Reads `buiAssessment` (which includes `questions[]`). Educators can publish a\n * DRAFT assessment (`useBuiPublishAssessment`) and add questions\n * (`useBuiCreateQuestion`). The question builder writes an `options` JSON array\n * of `{ label }` for choice questions and an optional input-only `answerKey`\n * (correct label(s)) — the latter is never read back (not exposed on the type).\n */\n\nimport { useMemo, useState, type FC, type ReactNode } from 'react';\nimport { useParams } from 'react-router-dom';\nimport {\n ArrowLeft,\n Plus,\n Send,\n ClipboardCheck,\n ClipboardEdit,\n Circle,\n Square,\n Clock,\n Trophy,\n Repeat,\n Trash2,\n Lightbulb,\n} from 'lucide-react';\nimport {\n Button,\n GlassCard,\n Input,\n Label,\n Textarea,\n Checkbox,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogDescription,\n ResponsiveDialogFooter,\n} from '@burdenoff/fe-libs/ui';\nimport {\n PageLayout,\n StatusBadge,\n Section,\n EmptyState,\n LoadingView,\n ErrorView,\n} from '../../components';\nimport { useBuildmyiq } from '../../context';\nimport { useBuiAssessment, useBuiPublishAssessment, useBuiCreateQuestion } from '../../hooks';\nimport { formatDuration, formatEnumLabel } from '../../utils/formatters';\nimport { cn } from '../../utils/cn';\nimport { QUESTION_TYPES } from '../../types';\nimport type { BuiQuestion, BuiQuestionType } from '../../types';\n\n// ---------------------------------------------------------------------------\n// Safe narrowing for the untyped `options` JSON blob (array of { id?, label }).\n// ---------------------------------------------------------------------------\ninterface QuestionOption {\n id?: string;\n label: string;\n}\n\nconst parseOptions = (raw: unknown): QuestionOption[] => {\n if (!Array.isArray(raw)) return [];\n const out: QuestionOption[] = [];\n for (const item of raw) {\n if (item && typeof item === 'object' && 'label' in item) {\n const label = (item as { label: unknown }).label;\n if (typeof label === 'string') {\n const idVal = (item as { id?: unknown }).id;\n out.push({ label, id: typeof idVal === 'string' ? idVal : undefined });\n }\n }\n }\n return out;\n};\n\nconst PROMPT_MAX = 1000;\n\nconst CHOICE_TYPES = new Set<BuiQuestionType>(['SINGLE_CHOICE', 'MULTIPLE_CHOICE']);\nconst isExclusive = (type: BuiQuestionType): boolean =>\n type === 'SINGLE_CHOICE' || type === 'TRUE_FALSE';\n\ninterface OptionDraft {\n label: string;\n correct: boolean;\n}\n\ninterface QuestionFormState {\n prompt: string;\n type: BuiQuestionType;\n points: string;\n explanation: string;\n options: OptionDraft[];\n shortAnswer: string;\n}\n\nconst blankChoiceOptions = (): OptionDraft[] => [\n { label: '', correct: false },\n { label: '', correct: false },\n];\n\nconst trueFalseOptions = (): OptionDraft[] => [\n { label: 'True', correct: false },\n { label: 'False', correct: false },\n];\n\nconst makeEmptyForm = (): QuestionFormState => ({\n prompt: '',\n type: 'SINGLE_CHOICE',\n points: '1',\n explanation: '',\n options: blankChoiceOptions(),\n shortAnswer: '',\n});\n\nexport const AssessmentDetailPage: FC = () => {\n const { assessmentId } = useParams<{ assessmentId: string }>();\n const { navigateTo } = useBuildmyiq();\n\n const assessmentQuery = useBuiAssessment(assessmentId);\n const publishAssessment = useBuiPublishAssessment();\n const createQuestion = useBuiCreateQuestion();\n\n const [dialogOpen, setDialogOpen] = useState(false);\n const [form, setForm] = useState<QuestionFormState>(makeEmptyForm);\n const [formError, setFormError] = useState<string | null>(null);\n\n const assessment = assessmentQuery.data ?? null;\n const questions = useMemo(\n () => [...(assessment?.questions ?? [])].sort((a, b) => a.position - b.position),\n [assessment]\n );\n\n const nextPosition = useMemo(\n () => (questions.length ? Math.max(...questions.map((q) => q.position)) + 1 : 1),\n [questions]\n );\n\n const totalPoints = useMemo(\n () => questions.reduce((sum, q) => sum + q.points, 0),\n [questions]\n );\n\n const usesOptions = form.type === 'TRUE_FALSE' || CHOICE_TYPES.has(form.type);\n\n const openDialog = () => {\n setForm(makeEmptyForm());\n setFormError(null);\n setDialogOpen(true);\n };\n\n const closeDialog = () => {\n setDialogOpen(false);\n setForm(makeEmptyForm());\n setFormError(null);\n };\n\n const changeType = (type: BuiQuestionType) => {\n setForm((prev) => {\n let options = prev.options;\n if (type === 'TRUE_FALSE') options = trueFalseOptions();\n else if (CHOICE_TYPES.has(type)) {\n options = prev.options.some((o) => o.label === 'True' || o.label === 'False')\n ? blankChoiceOptions()\n : prev.options;\n }\n return { ...prev, type, options };\n });\n };\n\n const setOptionLabel = (index: number, label: string) => {\n setForm((prev) => ({\n ...prev,\n options: prev.options.map((o, i) => (i === index ? { ...o, label } : o)),\n }));\n };\n\n const toggleCorrect = (index: number, value: boolean) => {\n setForm((prev) => ({\n ...prev,\n options: prev.options.map((o, i) =>\n i === index\n ? { ...o, correct: value }\n : isExclusive(prev.type)\n ? { ...o, correct: false }\n : o\n ),\n }));\n };\n\n const addOption = () => {\n setForm((prev) => ({ ...prev, options: [...prev.options, { label: '', correct: false }] }));\n };\n\n const removeOption = (index: number) => {\n setForm((prev) => ({ ...prev, options: prev.options.filter((_, i) => i !== index) }));\n };\n\n const handlePublish = async () => {\n if (!assessment) return;\n try {\n await publishAssessment.mutateAsync(assessment.id);\n } catch {\n // The mutation surfaces its own error state; nothing else to do here.\n }\n };\n\n const handleAddQuestion = async () => {\n if (!assessmentId) return;\n if (!form.prompt.trim()) {\n setFormError('Question prompt is required.');\n return;\n }\n\n let options: { label: string }[] | undefined;\n let answerKey: string[] | string | undefined;\n\n if (usesOptions) {\n const cleaned = form.options\n .map((o) => ({ label: o.label.trim(), correct: o.correct }))\n .filter((o) => o.label.length > 0);\n if (CHOICE_TYPES.has(form.type) && cleaned.length < 2) {\n setFormError('Add at least two options.');\n return;\n }\n options = cleaned.map((o) => ({ label: o.label }));\n const correct = cleaned.filter((o) => o.correct).map((o) => o.label);\n if (correct.length) answerKey = correct;\n } else if (form.type === 'SHORT_ANSWER') {\n answerKey = form.shortAnswer.trim() || undefined;\n }\n\n try {\n await createQuestion.mutateAsync({\n assessmentId,\n prompt: form.prompt.trim(),\n type: form.type,\n points: form.points ? Number(form.points) : undefined,\n position: nextPosition,\n explanation: form.explanation.trim() || undefined,\n options,\n answerKey,\n });\n closeDialog();\n } catch (err) {\n setFormError(err instanceof Error ? err.message : 'Failed to add question.');\n }\n };\n\n // ----- Loading / error / not-found -----\n if (assessmentQuery.isLoading) {\n return (\n <PageLayout title=\"Assessment\">\n <LoadingView label=\"Loading assessment…\" />\n </PageLayout>\n );\n }\n\n if (assessmentQuery.isError) {\n return (\n <PageLayout title=\"Assessment\">\n <ErrorView\n message=\"Couldn't load this assessment.\"\n onRetry={() => assessmentQuery.refetch()}\n />\n </PageLayout>\n );\n }\n\n if (!assessment) {\n return (\n <PageLayout title=\"Assessment\">\n <EmptyState\n illustration=\"empty-search\"\n title=\"Assessment not found\"\n description=\"This assessment may have been removed or you don't have access to it.\"\n action={<Button onClick={() => navigateTo('assessments')}>Back to assessments</Button>}\n />\n </PageLayout>\n );\n }\n\n const isDraft = assessment.status === 'DRAFT';\n\n return (\n <PageLayout\n title={\n <span className=\"flex min-w-0 items-center gap-3\">\n <span className=\"flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-[var(--color-accent-soft)] text-[var(--color-accent-teal)]\">\n <ClipboardEdit className=\"h-5 w-5\" />\n </span>\n <span className=\"truncate\">{assessment.title}</span>\n </span>\n }\n description={assessment.description ?? undefined}\n actions={\n <>\n <Button variant=\"ghost\" onClick={() => navigateTo('submissions')}>\n <ClipboardCheck className=\"mr-1.5 h-4 w-4\" /> Submissions\n </Button>\n {isDraft ? (\n <Button onClick={handlePublish} disabled={publishAssessment.isPending}>\n <Send className=\"mr-1.5 h-4 w-4\" />\n {publishAssessment.isPending ? 'Publishing…' : 'Publish'}\n </Button>\n ) : (\n <StatusBadge status={assessment.status} className=\"px-3 py-1.5\" />\n )}\n </>\n }\n >\n <button\n type=\"button\"\n onClick={() => navigateTo('assessments')}\n className=\"mb-4 inline-flex items-center gap-1.5 text-sm text-text-secondary transition-colors hover:text-text-primary\"\n >\n <ArrowLeft className=\"h-4 w-4\" /> All assessments\n </button>\n\n {/* Meta */}\n <GlassCard treatment=\"glass\" className=\"mb-6 p-5 sm:p-6\">\n <div className=\"mb-5 flex flex-wrap items-center gap-2\">\n <StatusBadge status={assessment.type} />\n <StatusBadge status={assessment.status} />\n {assessment.isGraded ? (\n <StatusBadge status=\"GRADED\" />\n ) : (\n <span className=\"inline-flex items-center gap-1.5 rounded-full bg-bg-sunken px-2.5 py-0.5 text-xs font-medium text-text-secondary\">\n Not graded\n </span>\n )}\n </div>\n <div className=\"grid grid-cols-2 gap-y-6 sm:grid-cols-4\">\n <MetaTile\n icon={<Trophy className=\"h-5 w-5\" />}\n label=\"Passing score\"\n value={`${assessment.passingScore}%`}\n />\n <MetaTile\n icon={<Clock className=\"h-5 w-5\" />}\n label=\"Time limit\"\n value={formatDuration(assessment.timeLimitMin)}\n divider\n />\n <MetaTile\n icon={<Repeat className=\"h-5 w-5\" />}\n label=\"Max attempts\"\n value={assessment.maxAttempts}\n divider\n />\n <MetaTile\n icon={<ClipboardCheck className=\"h-5 w-5\" />}\n label=\"Graded\"\n value={assessment.isGraded ? 'Yes' : 'No'}\n divider\n />\n </div>\n </GlassCard>\n\n {/* Questions */}\n <Section\n title={`Questions (${questions.length})`}\n description={\n questions.length > 0\n ? `${totalPoints} point${totalPoints === 1 ? '' : 's'} across ${questions.length} question${\n questions.length === 1 ? '' : 's'\n }`\n : undefined\n }\n action={\n <Button variant=\"outline\" onClick={openDialog}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> Add question\n </Button>\n }\n >\n {questions.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n title=\"No questions yet\"\n description=\"Build this assessment by adding its first question.\"\n action={\n <Button onClick={openDialog}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> Add question\n </Button>\n }\n />\n ) : (\n <ol className=\"flex flex-col gap-3\">\n {questions.map((q, idx) => (\n <QuestionRow key={q.id} index={idx + 1} question={q} />\n ))}\n </ol>\n )}\n </Section>\n\n {/* Add-question dialog */}\n <ResponsiveDialog\n open={dialogOpen}\n onOpenChange={(open) => {\n if (!open) closeDialog();\n else setDialogOpen(true);\n }}\n >\n <ResponsiveDialogContent>\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>Add question</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n Question {nextPosition} · pick a type, write the prompt and mark the correct\n answer(s).\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n\n <div className=\"grid gap-4 py-2\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"prompt\">Prompt *</Label>\n <Textarea\n id=\"prompt\"\n value={form.prompt}\n onChange={(e) => setForm({ ...form, prompt: e.target.value.slice(0, PROMPT_MAX) })}\n placeholder=\"Enter your question prompt…\"\n rows={3}\n maxLength={PROMPT_MAX}\n />\n <p className=\"text-right text-xs tabular-nums text-text-secondary\">\n {form.prompt.length} / {PROMPT_MAX}\n </p>\n </div>\n\n <div className=\"grid gap-2 sm:grid-cols-2\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"questionType\">Type</Label>\n <Select value={form.type} onValueChange={(v) => changeType(v as BuiQuestionType)}>\n <SelectTrigger id=\"questionType\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {QUESTION_TYPES.map((t) => (\n <SelectItem key={t} value={t}>\n {formatEnumLabel(t)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"points\">Points</Label>\n <Input\n id=\"points\"\n type=\"number\"\n min=\"0\"\n step=\"1\"\n value={form.points}\n onChange={(e) => setForm({ ...form, points: e.target.value })}\n placeholder=\"1\"\n />\n </div>\n </div>\n\n {usesOptions && (\n <div className=\"grid gap-2\">\n <Label>Options{form.type !== 'TRUE_FALSE' && ' *'}</Label>\n <p className=\"-mt-1 text-xs text-text-secondary\">\n Tick the {isExclusive(form.type) ? 'single correct answer' : 'correct answers'}.\n </p>\n <div className=\"flex flex-col gap-2\">\n {form.options.map((opt, i) => (\n <div\n key={i}\n className={cn(\n 'flex items-center gap-2.5 rounded-lg border px-2.5 py-1.5 transition-colors',\n opt.correct\n ? 'border-status-success-text bg-status-success-bg-subtle'\n : 'border-border-seam bg-bg-surface'\n )}\n >\n <Checkbox\n checked={opt.correct}\n onCheckedChange={(c) => toggleCorrect(i, c === true)}\n aria-label=\"Mark option correct\"\n />\n <Input\n value={opt.label}\n readOnly={form.type === 'TRUE_FALSE'}\n onChange={(e) => setOptionLabel(i, e.target.value)}\n placeholder={`Option ${i + 1}`}\n className=\"flex-1 border-0 bg-transparent px-1 shadow-none focus-visible:ring-0\"\n />\n {opt.correct && (\n <span className=\"shrink-0 text-xs font-medium text-status-success-text\">\n Correct\n </span>\n )}\n {form.type !== 'TRUE_FALSE' && form.options.length > 2 && (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon\"\n onClick={() => removeOption(i)}\n aria-label=\"Remove option\"\n >\n <Trash2 className=\"h-4 w-4\" />\n </Button>\n )}\n </div>\n ))}\n </div>\n {form.type !== 'TRUE_FALSE' && (\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" onClick={addOption}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> Add option\n </Button>\n )}\n </div>\n )}\n\n {form.type === 'SHORT_ANSWER' && (\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"shortAnswer\">Expected answer</Label>\n <Input\n id=\"shortAnswer\"\n value={form.shortAnswer}\n onChange={(e) => setForm({ ...form, shortAnswer: e.target.value })}\n placeholder=\"The accepted answer (used as the grading key).\"\n />\n </div>\n )}\n\n {(form.type === 'ESSAY' || form.type === 'CODE') && (\n <p className=\"rounded-lg bg-bg-sunken px-3 py-2 text-xs text-text-secondary\">\n {formatEnumLabel(form.type)} responses are graded manually from the Submissions\n view.\n </p>\n )}\n\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"explanation\">Explanation</Label>\n <Textarea\n id=\"explanation\"\n value={form.explanation}\n onChange={(e) => setForm({ ...form, explanation: e.target.value })}\n placeholder=\"Shown to learners after grading (optional).\"\n rows={2}\n />\n </div>\n\n {formError && <p className=\"text-sm text-status-error-text\">{formError}</p>}\n </div>\n\n <ResponsiveDialogFooter>\n <Button variant=\"outline\" onClick={closeDialog}>\n Cancel\n </Button>\n <Button onClick={handleAddQuestion} disabled={createQuestion.isPending}>\n {createQuestion.isPending ? 'Adding…' : 'Add question'}\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n </PageLayout>\n );\n};\n\n// ---------------------------------------------------------------------------\n// Meta tile — an icon-circle stat used in the assessment summary card.\n// ---------------------------------------------------------------------------\ninterface MetaTileProps {\n icon: ReactNode;\n label: string;\n value: ReactNode;\n /** Draw a subtle left divider on ≥sm (all tiles except the first in the row). */\n divider?: boolean;\n}\n\nconst MetaTile: FC<MetaTileProps> = ({ icon, label, value, divider }) => (\n <div\n className={cn(\n 'flex items-center gap-3',\n divider && 'sm:border-l sm:border-border-seam sm:pl-5'\n )}\n >\n <span className=\"flex h-11 w-11 shrink-0 items-center justify-center rounded-full bg-[var(--color-accent-soft)] text-[var(--color-accent-teal)]\">\n {icon}\n </span>\n <div className=\"min-w-0\">\n <p className=\"text-xs font-medium uppercase tracking-wide text-text-secondary\">{label}</p>\n <p className=\"mt-0.5 text-lg font-semibold tabular-nums text-text-primary\">{value}</p>\n </div>\n </div>\n);\n\n// ---------------------------------------------------------------------------\n// Question row\n// ---------------------------------------------------------------------------\nconst freeTextHint = (type: BuiQuestionType): string => {\n switch (type) {\n case 'SHORT_ANSWER':\n return 'Learners type a short answer — auto-graded against the answer key.';\n case 'ESSAY':\n return 'Open long-form response — graded manually from Submissions.';\n case 'CODE':\n return 'Code response — graded manually from Submissions.';\n default:\n return 'Free-text response.';\n }\n};\n\ninterface QuestionRowProps {\n index: number;\n question: BuiQuestion;\n}\n\nconst QuestionRow: FC<QuestionRowProps> = ({ index, question }) => {\n const options = parseOptions(question.options);\n const isChoice = CHOICE_TYPES.has(question.type) || question.type === 'TRUE_FALSE';\n const isMulti = question.type === 'MULTIPLE_CHOICE';\n\n return (\n <li>\n <GlassCard treatment=\"plain\" className=\"p-4 sm:p-5\">\n <div className=\"flex items-start gap-3.5\">\n <span className=\"flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-[var(--color-accent-soft)] text-sm font-bold tabular-nums text-[var(--color-accent-teal)]\">\n {index}\n </span>\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <StatusBadge status={question.type} />\n <span className=\"rounded-full bg-bg-sunken px-2 py-0.5 text-xs font-medium tabular-nums text-text-secondary\">\n {question.points} pt{question.points === 1 ? '' : 's'}\n </span>\n </div>\n\n <p className=\"mt-2.5 font-medium text-text-primary\">{question.prompt}</p>\n\n {isChoice && options.length > 0 && (\n <ul className=\"mt-3 flex flex-col gap-2\">\n {options.map((opt, i) => (\n <li\n key={opt.id ?? `${i}-${opt.label}`}\n className=\"flex items-center gap-2.5 rounded-lg border border-border-seam bg-bg-surface px-3 py-2 text-sm text-text-primary\"\n >\n {isMulti ? (\n <Square className=\"h-4 w-4 shrink-0 text-text-secondary opacity-60\" />\n ) : (\n <Circle className=\"h-4 w-4 shrink-0 text-text-secondary opacity-60\" />\n )}\n <span className=\"min-w-0\">{opt.label}</span>\n </li>\n ))}\n </ul>\n )}\n\n {!isChoice && (\n <p className=\"mt-3 rounded-lg border border-dashed border-border-seam bg-bg-sunken px-3 py-2 text-xs text-text-secondary\">\n {freeTextHint(question.type)}\n </p>\n )}\n\n {question.explanation && (\n <p className=\"mt-3 flex items-start gap-1.5 rounded-lg bg-bg-sunken px-3 py-2 text-xs text-text-secondary\">\n <Lightbulb className=\"mt-0.5 h-3.5 w-3.5 shrink-0 text-status-warning-text\" />\n <span>{question.explanation}</span>\n </p>\n )}\n </div>\n </div>\n </GlassCard>\n </li>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AssessmentDetailPage.js","names":[],"sources":["../../../../src/buildmyiq/pages/assessments/AssessmentDetailPage.tsx"],"sourcesContent":["/**\n * AssessmentDetailPage — a single assessment + its question builder.\n * @module buildmyiq/pages/assessments\n *\n * Reads `buiAssessment` (which includes `questions[]`). Educators can publish a\n * DRAFT assessment (`useBuiPublishAssessment`) and add questions\n * (`useBuiCreateQuestion`). The question builder writes an `options` JSON array\n * of `{ label }` for choice questions and an optional input-only `answerKey`\n * (correct label(s)) — the latter is never read back (not exposed on the type).\n */\n\nimport { useMemo, useState, type FC, type ReactNode } from 'react';\nimport { useParams } from 'react-router-dom';\nimport {\n ArrowLeft,\n Plus,\n Send,\n ClipboardCheck,\n ClipboardEdit,\n Circle,\n Square,\n Clock,\n Trophy,\n Repeat,\n Trash2,\n Lightbulb,\n} from 'lucide-react';\nimport {\n Button,\n GlassCard,\n Input,\n Label,\n Textarea,\n Checkbox,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogDescription,\n ResponsiveDialogFooter,\n} from '@burdenoff/fe-libs/ui';\nimport {\n PageLayout,\n StatusBadge,\n Section,\n EmptyState,\n LoadingView,\n ErrorView,\n} from '../../components';\nimport { useBuildmyiq } from '../../context';\nimport { useBuiAssessment, useBuiPublishAssessment, useBuiCreateQuestion } from '../../hooks';\nimport { formatDuration, formatEnumLabel } from '../../utils/formatters';\nimport { cn } from '../../utils/cn';\nimport { QUESTION_TYPES } from '../../types';\nimport type { BuiQuestion, BuiQuestionType } from '../../types';\n\n// ---------------------------------------------------------------------------\n// Safe narrowing for the untyped `options` JSON blob (array of { id?, label }).\n// ---------------------------------------------------------------------------\ninterface QuestionOption {\n id?: string;\n label: string;\n}\n\nconst parseOptions = (raw: unknown): QuestionOption[] => {\n if (!Array.isArray(raw)) return [];\n const out: QuestionOption[] = [];\n for (const item of raw) {\n if (item && typeof item === 'object' && 'label' in item) {\n const label = (item as { label: unknown }).label;\n if (typeof label === 'string') {\n const idVal = (item as { id?: unknown }).id;\n out.push({ label, id: typeof idVal === 'string' ? idVal : undefined });\n }\n }\n }\n return out;\n};\n\nconst PROMPT_MAX = 1000;\n\nconst CHOICE_TYPES = new Set<BuiQuestionType>(['SINGLE_CHOICE', 'MULTIPLE_CHOICE']);\nconst isExclusive = (type: BuiQuestionType): boolean =>\n type === 'SINGLE_CHOICE' || type === 'TRUE_FALSE';\n\ninterface OptionDraft {\n label: string;\n correct: boolean;\n}\n\ninterface QuestionFormState {\n prompt: string;\n type: BuiQuestionType;\n points: string;\n explanation: string;\n options: OptionDraft[];\n shortAnswer: string;\n}\n\nconst blankChoiceOptions = (): OptionDraft[] => [\n { label: '', correct: false },\n { label: '', correct: false },\n];\n\nconst trueFalseOptions = (): OptionDraft[] => [\n { label: 'True', correct: false },\n { label: 'False', correct: false },\n];\n\nconst makeEmptyForm = (): QuestionFormState => ({\n prompt: '',\n type: 'SINGLE_CHOICE',\n points: '1',\n explanation: '',\n options: blankChoiceOptions(),\n shortAnswer: '',\n});\n\nexport const AssessmentDetailPage: FC = () => {\n const { assessmentId } = useParams<{ assessmentId: string }>();\n const { navigateTo } = useBuildmyiq();\n\n const assessmentQuery = useBuiAssessment(assessmentId);\n const publishAssessment = useBuiPublishAssessment();\n const createQuestion = useBuiCreateQuestion();\n\n const [dialogOpen, setDialogOpen] = useState(false);\n const [form, setForm] = useState<QuestionFormState>(makeEmptyForm);\n const [formError, setFormError] = useState<string | null>(null);\n\n const assessment = assessmentQuery.data ?? null;\n const questions = useMemo(\n () => [...(assessment?.questions ?? [])].sort((a, b) => a.position - b.position),\n [assessment]\n );\n\n const nextPosition = useMemo(\n () => (questions.length ? Math.max(...questions.map((q) => q.position)) + 1 : 1),\n [questions]\n );\n\n const totalPoints = useMemo(() => questions.reduce((sum, q) => sum + q.points, 0), [questions]);\n\n const usesOptions = form.type === 'TRUE_FALSE' || CHOICE_TYPES.has(form.type);\n\n const openDialog = () => {\n setForm(makeEmptyForm());\n setFormError(null);\n setDialogOpen(true);\n };\n\n const closeDialog = () => {\n setDialogOpen(false);\n setForm(makeEmptyForm());\n setFormError(null);\n };\n\n const changeType = (type: BuiQuestionType) => {\n setForm((prev) => {\n let options = prev.options;\n if (type === 'TRUE_FALSE') options = trueFalseOptions();\n else if (CHOICE_TYPES.has(type)) {\n options = prev.options.some((o) => o.label === 'True' || o.label === 'False')\n ? blankChoiceOptions()\n : prev.options;\n }\n return { ...prev, type, options };\n });\n };\n\n const setOptionLabel = (index: number, label: string) => {\n setForm((prev) => ({\n ...prev,\n options: prev.options.map((o, i) => (i === index ? { ...o, label } : o)),\n }));\n };\n\n const toggleCorrect = (index: number, value: boolean) => {\n setForm((prev) => ({\n ...prev,\n options: prev.options.map((o, i) =>\n i === index\n ? { ...o, correct: value }\n : isExclusive(prev.type)\n ? { ...o, correct: false }\n : o\n ),\n }));\n };\n\n const addOption = () => {\n setForm((prev) => ({ ...prev, options: [...prev.options, { label: '', correct: false }] }));\n };\n\n const removeOption = (index: number) => {\n setForm((prev) => ({ ...prev, options: prev.options.filter((_, i) => i !== index) }));\n };\n\n const handlePublish = async () => {\n if (!assessment) return;\n try {\n await publishAssessment.mutateAsync(assessment.id);\n } catch {\n // The mutation surfaces its own error state; nothing else to do here.\n }\n };\n\n const handleAddQuestion = async () => {\n if (!assessmentId) return;\n if (!form.prompt.trim()) {\n setFormError('Question prompt is required.');\n return;\n }\n\n let options: { label: string }[] | undefined;\n let answerKey: string[] | string | undefined;\n\n if (usesOptions) {\n const cleaned = form.options\n .map((o) => ({ label: o.label.trim(), correct: o.correct }))\n .filter((o) => o.label.length > 0);\n if (CHOICE_TYPES.has(form.type) && cleaned.length < 2) {\n setFormError('Add at least two options.');\n return;\n }\n options = cleaned.map((o) => ({ label: o.label }));\n const correct = cleaned.filter((o) => o.correct).map((o) => o.label);\n if (correct.length) answerKey = correct;\n } else if (form.type === 'SHORT_ANSWER') {\n answerKey = form.shortAnswer.trim() || undefined;\n }\n\n try {\n await createQuestion.mutateAsync({\n assessmentId,\n prompt: form.prompt.trim(),\n type: form.type,\n points: form.points ? Number(form.points) : undefined,\n position: nextPosition,\n explanation: form.explanation.trim() || undefined,\n options,\n answerKey,\n });\n closeDialog();\n } catch (err) {\n setFormError(err instanceof Error ? err.message : 'Failed to add question.');\n }\n };\n\n // ----- Loading / error / not-found -----\n if (assessmentQuery.isLoading) {\n return (\n <PageLayout title=\"Assessment\">\n <LoadingView label=\"Loading assessment…\" />\n </PageLayout>\n );\n }\n\n if (assessmentQuery.isError) {\n return (\n <PageLayout title=\"Assessment\">\n <ErrorView\n message=\"Couldn't load this assessment.\"\n onRetry={() => assessmentQuery.refetch()}\n />\n </PageLayout>\n );\n }\n\n if (!assessment) {\n return (\n <PageLayout title=\"Assessment\">\n <EmptyState\n illustration=\"empty-search\"\n title=\"Assessment not found\"\n description=\"This assessment may have been removed or you don't have access to it.\"\n action={<Button onClick={() => navigateTo('assessments')}>Back to assessments</Button>}\n />\n </PageLayout>\n );\n }\n\n const isDraft = assessment.status === 'DRAFT';\n\n return (\n <PageLayout\n title={\n <span className=\"flex min-w-0 items-center gap-3\">\n <span className=\"flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-[var(--color-accent-soft)] text-[var(--color-accent-teal)]\">\n <ClipboardEdit className=\"h-5 w-5\" />\n </span>\n <span className=\"truncate\">{assessment.title}</span>\n </span>\n }\n description={assessment.description ?? undefined}\n actions={\n <>\n <Button variant=\"ghost\" onClick={() => navigateTo('submissions')}>\n <ClipboardCheck className=\"mr-1.5 h-4 w-4\" /> Submissions\n </Button>\n {isDraft ? (\n <Button onClick={handlePublish} disabled={publishAssessment.isPending}>\n <Send className=\"mr-1.5 h-4 w-4\" />\n {publishAssessment.isPending ? 'Publishing…' : 'Publish'}\n </Button>\n ) : (\n <StatusBadge status={assessment.status} className=\"px-3 py-1.5\" />\n )}\n </>\n }\n >\n <button\n type=\"button\"\n onClick={() => navigateTo('assessments')}\n className=\"mb-4 inline-flex items-center gap-1.5 text-sm text-text-secondary transition-colors hover:text-text-primary\"\n >\n <ArrowLeft className=\"h-4 w-4\" /> All assessments\n </button>\n\n {/* Meta */}\n <GlassCard treatment=\"glass\" className=\"mb-6 p-5 sm:p-6\">\n <div className=\"mb-5 flex flex-wrap items-center gap-2\">\n <StatusBadge status={assessment.type} />\n <StatusBadge status={assessment.status} />\n {assessment.isGraded ? (\n <StatusBadge status=\"GRADED\" />\n ) : (\n <span className=\"inline-flex items-center gap-1.5 rounded-full bg-bg-sunken px-2.5 py-0.5 text-xs font-medium text-text-secondary\">\n Not graded\n </span>\n )}\n </div>\n <div className=\"grid grid-cols-2 gap-y-6 sm:grid-cols-4\">\n <MetaTile\n icon={<Trophy className=\"h-5 w-5\" />}\n label=\"Passing score\"\n value={`${assessment.passingScore}%`}\n />\n <MetaTile\n icon={<Clock className=\"h-5 w-5\" />}\n label=\"Time limit\"\n value={formatDuration(assessment.timeLimitMin)}\n divider\n />\n <MetaTile\n icon={<Repeat className=\"h-5 w-5\" />}\n label=\"Max attempts\"\n value={assessment.maxAttempts}\n divider\n />\n <MetaTile\n icon={<ClipboardCheck className=\"h-5 w-5\" />}\n label=\"Graded\"\n value={assessment.isGraded ? 'Yes' : 'No'}\n divider\n />\n </div>\n </GlassCard>\n\n {/* Questions */}\n <Section\n title={`Questions (${questions.length})`}\n description={\n questions.length > 0\n ? `${totalPoints} point${totalPoints === 1 ? '' : 's'} across ${questions.length} question${\n questions.length === 1 ? '' : 's'\n }`\n : undefined\n }\n action={\n <Button variant=\"outline\" onClick={openDialog}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> Add question\n </Button>\n }\n >\n {questions.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n title=\"No questions yet\"\n description=\"Build this assessment by adding its first question.\"\n action={\n <Button onClick={openDialog}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> Add question\n </Button>\n }\n />\n ) : (\n <ol className=\"flex flex-col gap-3\">\n {questions.map((q, idx) => (\n <QuestionRow key={q.id} index={idx + 1} question={q} />\n ))}\n </ol>\n )}\n </Section>\n\n {/* Add-question dialog */}\n <ResponsiveDialog\n open={dialogOpen}\n onOpenChange={(open) => {\n if (!open) closeDialog();\n else setDialogOpen(true);\n }}\n >\n <ResponsiveDialogContent>\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>Add question</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n Question {nextPosition} · pick a type, write the prompt and mark the correct\n answer(s).\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n\n <div className=\"grid gap-4 py-2\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"prompt\">Prompt *</Label>\n <Textarea\n id=\"prompt\"\n value={form.prompt}\n onChange={(e) => setForm({ ...form, prompt: e.target.value.slice(0, PROMPT_MAX) })}\n placeholder=\"Enter your question prompt…\"\n rows={3}\n maxLength={PROMPT_MAX}\n />\n <p className=\"text-right text-xs tabular-nums text-text-secondary\">\n {form.prompt.length} / {PROMPT_MAX}\n </p>\n </div>\n\n <div className=\"grid gap-2 sm:grid-cols-2\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"questionType\">Type</Label>\n <Select value={form.type} onValueChange={(v) => changeType(v as BuiQuestionType)}>\n <SelectTrigger id=\"questionType\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {QUESTION_TYPES.map((t) => (\n <SelectItem key={t} value={t}>\n {formatEnumLabel(t)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"points\">Points</Label>\n <Input\n id=\"points\"\n type=\"number\"\n min=\"0\"\n step=\"1\"\n value={form.points}\n onChange={(e) => setForm({ ...form, points: e.target.value })}\n placeholder=\"1\"\n />\n </div>\n </div>\n\n {usesOptions && (\n <div className=\"grid gap-2\">\n <Label>Options{form.type !== 'TRUE_FALSE' && ' *'}</Label>\n <p className=\"-mt-1 text-xs text-text-secondary\">\n Tick the {isExclusive(form.type) ? 'single correct answer' : 'correct answers'}.\n </p>\n <div className=\"flex flex-col gap-2\">\n {form.options.map((opt, i) => (\n <div\n key={i}\n className={cn(\n 'flex items-center gap-2.5 rounded-lg border px-2.5 py-1.5 transition-colors',\n opt.correct\n ? 'border-status-success-text bg-status-success-bg-subtle'\n : 'border-border-seam bg-bg-surface'\n )}\n >\n <Checkbox\n checked={opt.correct}\n onCheckedChange={(c) => toggleCorrect(i, c === true)}\n aria-label=\"Mark option correct\"\n />\n <Input\n value={opt.label}\n readOnly={form.type === 'TRUE_FALSE'}\n onChange={(e) => setOptionLabel(i, e.target.value)}\n placeholder={`Option ${i + 1}`}\n className=\"flex-1 border-0 bg-transparent px-1 shadow-none focus-visible:ring-0\"\n />\n {opt.correct && (\n <span className=\"shrink-0 text-xs font-medium text-status-success-text\">\n Correct\n </span>\n )}\n {form.type !== 'TRUE_FALSE' && form.options.length > 2 && (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon\"\n onClick={() => removeOption(i)}\n aria-label=\"Remove option\"\n >\n <Trash2 className=\"h-4 w-4\" />\n </Button>\n )}\n </div>\n ))}\n </div>\n {form.type !== 'TRUE_FALSE' && (\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" onClick={addOption}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> Add option\n </Button>\n )}\n </div>\n )}\n\n {form.type === 'SHORT_ANSWER' && (\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"shortAnswer\">Expected answer</Label>\n <Input\n id=\"shortAnswer\"\n value={form.shortAnswer}\n onChange={(e) => setForm({ ...form, shortAnswer: e.target.value })}\n placeholder=\"The accepted answer (used as the grading key).\"\n />\n </div>\n )}\n\n {(form.type === 'ESSAY' || form.type === 'CODE') && (\n <p className=\"rounded-lg bg-bg-sunken px-3 py-2 text-xs text-text-secondary\">\n {formatEnumLabel(form.type)} responses are graded manually from the Submissions\n view.\n </p>\n )}\n\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"explanation\">Explanation</Label>\n <Textarea\n id=\"explanation\"\n value={form.explanation}\n onChange={(e) => setForm({ ...form, explanation: e.target.value })}\n placeholder=\"Shown to learners after grading (optional).\"\n rows={2}\n />\n </div>\n\n {formError && <p className=\"text-sm text-status-error-text\">{formError}</p>}\n </div>\n\n <ResponsiveDialogFooter>\n <Button variant=\"outline\" onClick={closeDialog}>\n Cancel\n </Button>\n <Button onClick={handleAddQuestion} disabled={createQuestion.isPending}>\n {createQuestion.isPending ? 'Adding…' : 'Add question'}\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n </PageLayout>\n );\n};\n\n// ---------------------------------------------------------------------------\n// Meta tile — an icon-circle stat used in the assessment summary card.\n// ---------------------------------------------------------------------------\ninterface MetaTileProps {\n icon: ReactNode;\n label: string;\n value: ReactNode;\n /** Draw a subtle left divider on ≥sm (all tiles except the first in the row). */\n divider?: boolean;\n}\n\nconst MetaTile: FC<MetaTileProps> = ({ icon, label, value, divider }) => (\n <div\n className={cn(\n 'flex items-center gap-3',\n divider && 'sm:border-l sm:border-border-seam sm:pl-5'\n )}\n >\n <span className=\"flex h-11 w-11 shrink-0 items-center justify-center rounded-full bg-[var(--color-accent-soft)] text-[var(--color-accent-teal)]\">\n {icon}\n </span>\n <div className=\"min-w-0\">\n <p className=\"text-xs font-medium uppercase tracking-wide text-text-secondary\">{label}</p>\n <p className=\"mt-0.5 text-lg font-semibold tabular-nums text-text-primary\">{value}</p>\n </div>\n </div>\n);\n\n// ---------------------------------------------------------------------------\n// Question row\n// ---------------------------------------------------------------------------\nconst freeTextHint = (type: BuiQuestionType): string => {\n switch (type) {\n case 'SHORT_ANSWER':\n return 'Learners type a short answer — auto-graded against the answer key.';\n case 'ESSAY':\n return 'Open long-form response — graded manually from Submissions.';\n case 'CODE':\n return 'Code response — graded manually from Submissions.';\n default:\n return 'Free-text response.';\n }\n};\n\ninterface QuestionRowProps {\n index: number;\n question: BuiQuestion;\n}\n\nconst QuestionRow: FC<QuestionRowProps> = ({ index, question }) => {\n const options = parseOptions(question.options);\n const isChoice = CHOICE_TYPES.has(question.type) || question.type === 'TRUE_FALSE';\n const isMulti = question.type === 'MULTIPLE_CHOICE';\n\n return (\n <li>\n <GlassCard treatment=\"plain\" className=\"p-4 sm:p-5\">\n <div className=\"flex items-start gap-3.5\">\n <span className=\"flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-[var(--color-accent-soft)] text-sm font-bold tabular-nums text-[var(--color-accent-teal)]\">\n {index}\n </span>\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <StatusBadge status={question.type} />\n <span className=\"rounded-full bg-bg-sunken px-2 py-0.5 text-xs font-medium tabular-nums text-text-secondary\">\n {question.points} pt{question.points === 1 ? '' : 's'}\n </span>\n </div>\n\n <p className=\"mt-2.5 font-medium text-text-primary\">{question.prompt}</p>\n\n {isChoice && options.length > 0 && (\n <ul className=\"mt-3 flex flex-col gap-2\">\n {options.map((opt, i) => (\n <li\n key={opt.id ?? `${i}-${opt.label}`}\n className=\"flex items-center gap-2.5 rounded-lg border border-border-seam bg-bg-surface px-3 py-2 text-sm text-text-primary\"\n >\n {isMulti ? (\n <Square className=\"h-4 w-4 shrink-0 text-text-secondary opacity-60\" />\n ) : (\n <Circle className=\"h-4 w-4 shrink-0 text-text-secondary opacity-60\" />\n )}\n <span className=\"min-w-0\">{opt.label}</span>\n </li>\n ))}\n </ul>\n )}\n\n {!isChoice && (\n <p className=\"mt-3 rounded-lg border border-dashed border-border-seam bg-bg-sunken px-3 py-2 text-xs text-text-secondary\">\n {freeTextHint(question.type)}\n </p>\n )}\n\n {question.explanation && (\n <p className=\"mt-3 flex items-start gap-1.5 rounded-lg bg-bg-sunken px-3 py-2 text-xs text-text-secondary\">\n <Lightbulb className=\"mt-0.5 h-3.5 w-3.5 shrink-0 text-status-warning-text\" />\n <span>{question.explanation}</span>\n </p>\n )}\n </div>\n </div>\n </GlassCard>\n </li>\n );\n};\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TakeAssessmentPage.js","names":[],"sources":["../../../../src/buildmyiq/pages/assessments/TakeAssessmentPage.tsx"],"sourcesContent":["/**\n * TakeAssessmentPage — the LEARNER-facing surface to attempt a published\n * assessment and submit answers.\n * @module buildmyiq/pages/assessments\n *\n * Loads the assessment + its questions, collects an answer per question (typed\n * to the question kind), resolves the caller's enrollment for the assessment's\n * course, and submits via `buiSubmitAssessment` ({ assessmentId, enrollmentId?,\n * answers }). Grading is done by an educator in the Grading queue — this page is\n * honest about that (\"submitted, awaiting grading\"). All data is live.\n */\n\nimport type { FC, ReactNode } from 'react';\nimport { useMemo, useState } from 'react';\nimport { useParams } from 'react-router-dom';\nimport {\n ArrowLeft,\n CheckCircle2,\n ClipboardCheck,\n Clock,\n FileText,\n RefreshCw,\n Send,\n Target,\n} from 'lucide-react';\nimport { Button, GlassCard, Input, Textarea } from '@burdenoff/fe-libs/ui';\nimport {\n PageLayout,\n StatusBadge,\n ProgressRing,\n LoadingView,\n ErrorView,\n EmptyState,\n} from '../../components';\nimport { useBuildmyiq } from '../../context';\nimport { useBuiAssessment, useBuiEnrollments, useBuiSubmitAssessment } from '../../hooks';\nimport { cn } from '../../utils/cn';\nimport { formatDuration, formatEnumLabel } from '../../utils/formatters';\nimport type { BuiQuestion } from '../../types';\n\ninterface Choice {\n label: string;\n}\n\n/** Narrow the untyped `options` JSON blob to a list of `{ label }` choices. */\nfunction parseChoices(options: unknown): Choice[] {\n if (!Array.isArray(options)) return [];\n return options\n .map((o) => (o && typeof o === 'object' && 'label' in o ? { label: String((o as { label: unknown }).label) } : null))\n .filter((c): c is Choice => c !== null && c.label.length > 0);\n}\n\ntype AnswerValue = string | string[];\n\nconst TRUE_FALSE_CHOICES: Choice[] = [{ label: 'True' }, { label: 'False' }];\n\ninterface MetaCellProps {\n icon: ReactNode;\n label: string;\n value: ReactNode;\n}\n\nconst MetaCell: FC<MetaCellProps> = ({ icon, label, value }) => (\n <div className=\"flex items-center gap-3 py-3 first:pt-0 last:pb-0 sm:px-5 sm:py-0 sm:first:pl-0 sm:last:pr-0\">\n <span className=\"flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-[var(--color-accent-soft)] text-[var(--color-accent-teal)]\">\n {icon}\n </span>\n <div className=\"min-w-0\">\n <p className=\"text-xs text-text-secondary\">{label}</p>\n <p className=\"text-sm font-semibold text-text-primary\">{value}</p>\n </div>\n </div>\n);\n\nexport const TakeAssessmentPage: FC = () => {\n const { assessmentId } = useParams<{ assessmentId: string }>();\n const { currentUser, navigateTo } = useBuildmyiq();\n\n const assessmentQuery = useBuiAssessment(assessmentId);\n const assessment = assessmentQuery.data ?? null;\n\n const enrollmentsQuery = useBuiEnrollments({\n learnerId: currentUser?.id,\n courseId: assessment?.courseId,\n enabled: Boolean(currentUser?.id && assessment?.courseId),\n });\n const enrollmentId = enrollmentsQuery.data?.items?.[0]?.id;\n\n const submit = useBuiSubmitAssessment();\n const [answers, setAnswers] = useState<Record<string, AnswerValue>>({});\n const [submittedId, setSubmittedId] = useState<string | null>(null);\n\n const questions = useMemo<BuiQuestion[]>(\n () => [...(assessment?.questions ?? [])].sort((a, b) => a.position - b.position),\n [assessment]\n );\n\n const setAnswer = (qid: string, value: AnswerValue): void => {\n setAnswers((prev) => ({ ...prev, [qid]: value }));\n };\n\n const toggleMulti = (qid: string, label: string): void => {\n setAnswers((prev) => {\n const cur = Array.isArray(prev[qid]) ? (prev[qid] as string[]) : [];\n return { ...prev, [qid]: cur.includes(label) ? cur.filter((l) => l !== label) : [...cur, label] };\n });\n };\n\n const isAnswered = (q: BuiQuestion): boolean => {\n const a = answers[q.id];\n return Array.isArray(a) ? a.length > 0 : Boolean(a && String(a).trim());\n };\n\n const answeredCount = questions.filter(isAnswered).length;\n const answeredPct = questions.length > 0 ? (answeredCount / questions.length) * 100 : 0;\n const remaining = questions.length - answeredCount;\n const encouragement =\n answeredCount === 0\n ? 'Answer a question to get started.'\n : remaining === 0\n ? 'All questions answered — ready to submit.'\n : `${remaining} question${remaining === 1 ? '' : 's'} left to answer.`;\n\n const handleSubmit = async (): Promise<void> => {\n if (!assessmentId) return;\n const result = await submit.mutateAsync({\n assessmentId,\n enrollmentId,\n attempt: 1,\n answers,\n });\n setSubmittedId(result.id);\n };\n\n const backToCourse = assessment\n ? (\n <Button variant=\"ghost\" size=\"sm\" onClick={() => navigateTo(`catalog/${assessment.courseId}`)}>\n <ArrowLeft className=\"mr-1 h-4 w-4\" /> Back to course\n </Button>\n )\n : undefined;\n\n if (!assessmentId) {\n return (\n <PageLayout title=\"Assessment\">\n <EmptyState illustration=\"empty-data\" title=\"No assessment selected\" />\n </PageLayout>\n );\n }\n\n if (assessmentQuery.isLoading) {\n return (\n <PageLayout title=\"Assessment\">\n <LoadingView label=\"Loading the assessment…\" />\n </PageLayout>\n );\n }\n\n if (assessmentQuery.isError || !assessment) {\n return (\n <PageLayout title=\"Assessment\" actions={backToCourse}>\n <ErrorView\n title=\"Couldn't load this assessment\"\n message={assessmentQuery.error instanceof Error ? assessmentQuery.error.message : undefined}\n onRetry={() => assessmentQuery.refetch()}\n />\n </PageLayout>\n );\n }\n\n if (assessment.status !== 'PUBLISHED') {\n return (\n <PageLayout title={assessment.title} actions={backToCourse}>\n <EmptyState\n illustration=\"empty-data\"\n title=\"This assessment isn't open yet\"\n description=\"It has to be published by an educator before you can take it.\"\n />\n </PageLayout>\n );\n }\n\n // Submitted confirmation\n if (submittedId) {\n return (\n <PageLayout title={assessment.title} actions={backToCourse}>\n <div className=\"mx-auto max-w-lg\">\n <GlassCard treatment=\"glass\" glow className=\"flex flex-col items-center gap-4 p-8 text-center\">\n <div className=\"flex h-14 w-14 items-center justify-center rounded-full bg-status-success-bg-subtle\">\n <CheckCircle2 className=\"h-7 w-7 text-status-success-text\" />\n </div>\n <div>\n <h2 className=\"text-lg font-semibold text-text-primary\">Submitted</h2>\n <p className=\"mt-1 text-sm text-text-secondary\">\n Your answers for “{assessment.title}” were submitted.\n {assessment.isGraded\n ? ' An educator will grade them — you’ll see your score and feedback in My Learning.'\n : ' Thanks for completing it.'}\n </p>\n </div>\n <div className=\"flex flex-wrap justify-center gap-2\">\n <Button variant=\"secondary\" onClick={() => navigateTo('learning')}>\n Go to My Learning\n </Button>\n <Button variant=\"ghost\" onClick={() => navigateTo(`catalog/${assessment.courseId}`)}>\n Back to course\n </Button>\n </div>\n </GlassCard>\n </div>\n </PageLayout>\n );\n }\n\n return (\n <PageLayout\n title={assessment.title}\n description={assessment.description ?? undefined}\n actions={backToCourse}\n >\n <div className=\"mx-auto max-w-3xl pb-4\">\n {/* Meta strip — type · passing score · time limit · attempts · answered */}\n <GlassCard treatment=\"glass\" className=\"mb-6 p-4 sm:px-5\">\n <div className=\"flex flex-col divide-y divide-border-seam sm:flex-row sm:flex-wrap sm:items-center sm:divide-x sm:divide-y-0\">\n <MetaCell\n icon={<FileText className=\"h-4 w-4\" />}\n label=\"Type\"\n value={<StatusBadge status={assessment.type} />}\n />\n <MetaCell\n icon={<Target className=\"h-4 w-4\" />}\n label=\"Passing score\"\n value={`${assessment.passingScore}%`}\n />\n {assessment.timeLimitMin > 0 && (\n <MetaCell\n icon={<Clock className=\"h-4 w-4\" />}\n label=\"Time limit\"\n value={formatDuration(assessment.timeLimitMin)}\n />\n )}\n {assessment.maxAttempts > 0 && (\n <MetaCell\n icon={<RefreshCw className=\"h-4 w-4\" />}\n label=\"Attempts\"\n value={assessment.maxAttempts}\n />\n )}\n <div className=\"flex items-center gap-3 py-3 sm:ml-auto sm:py-0 sm:pl-5\">\n <ProgressRing\n value={answeredPct}\n size={44}\n stroke={5}\n label={`${answeredCount}/${questions.length}`}\n />\n <div className=\"min-w-0\">\n <p className=\"text-sm font-semibold text-text-primary tabular-nums\">\n {answeredCount} of {questions.length}\n </p>\n <p className=\"text-xs text-text-secondary\">answered</p>\n </div>\n </div>\n </div>\n </GlassCard>\n\n {questions.length === 0 ? (\n <EmptyState\n icon={<ClipboardCheck className=\"h-8 w-8\" />}\n title=\"No questions yet\"\n description=\"This assessment doesn't have any questions to answer.\"\n />\n ) : (\n <div className=\"flex flex-col gap-4\">\n {questions.map((q, idx) => {\n const choices =\n q.type === 'TRUE_FALSE' ? TRUE_FALSE_CHOICES : parseChoices(q.options);\n const answer = answers[q.id];\n const answered = isAnswered(q);\n return (\n <GlassCard\n key={q.id}\n treatment={answered ? 'insight' : 'plain'}\n className={cn(\n 'p-5 transition-shadow',\n answered && 'ring-1 ring-[var(--color-accent-soft)]'\n )}\n >\n <div className=\"flex items-start gap-3 sm:gap-4\">\n <span\n className={cn(\n 'mt-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-sm font-semibold tabular-nums transition-colors',\n answered\n ? 'bg-status-success-bg-subtle text-status-success-text'\n : 'bg-[var(--color-accent-soft)] text-text-primary'\n )}\n >\n {answered ? <CheckCircle2 className=\"h-4 w-4\" /> : idx + 1}\n </span>\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <span className=\"rounded-md bg-bg-sunken px-2 py-0.5 text-xs font-medium text-text-secondary\">\n {formatEnumLabel(q.type)}\n </span>\n <span className=\"ml-auto text-xs text-text-secondary tabular-nums\">\n {q.points} {q.points === 1 ? 'point' : 'points'}\n </span>\n </div>\n <p className=\"mt-2.5 font-medium text-text-primary\">{q.prompt}</p>\n\n <div className=\"mt-4\">\n {(q.type === 'SINGLE_CHOICE' || q.type === 'TRUE_FALSE') && (\n <div className=\"flex flex-col gap-2\">\n {choices.map((c) => (\n <label\n key={c.label}\n className={cn(\n 'flex min-h-[44px] cursor-pointer items-center gap-3 rounded-xl border px-3.5 py-2.5 text-sm transition-colors',\n answer === c.label\n ? 'border-[var(--color-accent-teal)] bg-[var(--color-accent-soft)] text-text-primary'\n : 'border-border-seam text-text-secondary hover:border-border-strong hover:bg-bg-sunken'\n )}\n >\n <input\n type=\"radio\"\n name={q.id}\n className=\"h-4 w-4 accent-[var(--color-accent-teal)]\"\n checked={answer === c.label}\n onChange={() => setAnswer(q.id, c.label)}\n />\n {c.label}\n </label>\n ))}\n </div>\n )}\n\n {q.type === 'MULTIPLE_CHOICE' && (\n <div className=\"flex flex-col gap-2\">\n {choices.map((c) => {\n const checked = Array.isArray(answer) && answer.includes(c.label);\n return (\n <label\n key={c.label}\n className={cn(\n 'flex min-h-[44px] cursor-pointer items-center gap-3 rounded-xl border px-3.5 py-2.5 text-sm transition-colors',\n checked\n ? 'border-[var(--color-accent-teal)] bg-[var(--color-accent-soft)] text-text-primary'\n : 'border-border-seam text-text-secondary hover:border-border-strong hover:bg-bg-sunken'\n )}\n >\n <input\n type=\"checkbox\"\n className=\"h-4 w-4 rounded accent-[var(--color-accent-teal)]\"\n checked={checked}\n onChange={() => toggleMulti(q.id, c.label)}\n />\n {c.label}\n </label>\n );\n })}\n </div>\n )}\n\n {q.type === 'SHORT_ANSWER' && (\n <Input\n value={typeof answer === 'string' ? answer : ''}\n onChange={(e) => setAnswer(q.id, e.target.value)}\n placeholder=\"Type your answer here…\"\n />\n )}\n\n {(q.type === 'ESSAY' || q.type === 'CODE') && (\n <Textarea\n rows={q.type === 'CODE' ? 6 : 4}\n value={typeof answer === 'string' ? answer : ''}\n onChange={(e) => setAnswer(q.id, e.target.value)}\n placeholder={q.type === 'CODE' ? 'Write your code…' : 'Write your answer…'}\n className={q.type === 'CODE' ? 'font-mono text-sm' : undefined}\n />\n )}\n </div>\n </div>\n </div>\n </GlassCard>\n );\n })}\n\n {submit.isError && (\n <p className=\"text-sm text-status-error-text\">\n {submit.error instanceof Error ? submit.error.message : 'Could not submit your answers.'}\n </p>\n )}\n\n {/* Sticky submit bar */}\n <div className=\"sticky bottom-0 z-10 pt-2\">\n <div className=\"flex flex-col gap-3 rounded-2xl border border-border-seam bg-bg-surface/90 p-4 shadow-elevation-2 backdrop-blur sm:flex-row sm:items-center sm:justify-between\">\n <div className=\"flex items-center gap-3\">\n <ProgressRing\n value={answeredPct}\n size={44}\n stroke={5}\n label={`${answeredCount}/${questions.length}`}\n />\n <div className=\"min-w-0\">\n <p className=\"text-sm font-semibold text-text-primary tabular-nums\">\n {answeredCount} of {questions.length} answered\n </p>\n <p className=\"text-xs text-text-secondary\">{encouragement}</p>\n </div>\n </div>\n <Button\n size=\"lg\"\n className=\"max-sm:w-full\"\n onClick={handleSubmit}\n disabled={submit.isPending || answeredCount === 0}\n >\n <Send className=\"mr-1.5 h-4 w-4\" />\n {submit.isPending ? 'Submitting…' : 'Submit assessment'}\n </Button>\n </div>\n </div>\n </div>\n )}\n </div>\n </PageLayout>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"TakeAssessmentPage.js","names":[],"sources":["../../../../src/buildmyiq/pages/assessments/TakeAssessmentPage.tsx"],"sourcesContent":["/**\n * TakeAssessmentPage — the LEARNER-facing surface to attempt a published\n * assessment and submit answers.\n * @module buildmyiq/pages/assessments\n *\n * Loads the assessment + its questions, collects an answer per question (typed\n * to the question kind), resolves the caller's enrollment for the assessment's\n * course, and submits via `buiSubmitAssessment` ({ assessmentId, enrollmentId?,\n * answers }). Grading is done by an educator in the Grading queue — this page is\n * honest about that (\"submitted, awaiting grading\"). All data is live.\n */\n\nimport type { FC, ReactNode } from 'react';\nimport { useMemo, useState } from 'react';\nimport { useParams } from 'react-router-dom';\nimport {\n ArrowLeft,\n CheckCircle2,\n ClipboardCheck,\n Clock,\n FileText,\n RefreshCw,\n Send,\n Target,\n} from 'lucide-react';\nimport { Button, GlassCard, Input, Textarea } from '@burdenoff/fe-libs/ui';\nimport {\n PageLayout,\n StatusBadge,\n ProgressRing,\n LoadingView,\n ErrorView,\n EmptyState,\n} from '../../components';\nimport { useBuildmyiq } from '../../context';\nimport { useBuiAssessment, useBuiEnrollments, useBuiSubmitAssessment } from '../../hooks';\nimport { cn } from '../../utils/cn';\nimport { formatDuration, formatEnumLabel } from '../../utils/formatters';\nimport type { BuiQuestion } from '../../types';\n\ninterface Choice {\n label: string;\n}\n\n/** Narrow the untyped `options` JSON blob to a list of `{ label }` choices. */\nfunction parseChoices(options: unknown): Choice[] {\n if (!Array.isArray(options)) return [];\n return options\n .map((o) =>\n o && typeof o === 'object' && 'label' in o\n ? { label: String((o as { label: unknown }).label) }\n : null\n )\n .filter((c): c is Choice => c !== null && c.label.length > 0);\n}\n\ntype AnswerValue = string | string[];\n\nconst TRUE_FALSE_CHOICES: Choice[] = [{ label: 'True' }, { label: 'False' }];\n\ninterface MetaCellProps {\n icon: ReactNode;\n label: string;\n value: ReactNode;\n}\n\nconst MetaCell: FC<MetaCellProps> = ({ icon, label, value }) => (\n <div className=\"flex items-center gap-3 py-3 first:pt-0 last:pb-0 sm:px-5 sm:py-0 sm:first:pl-0 sm:last:pr-0\">\n <span className=\"flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-[var(--color-accent-soft)] text-[var(--color-accent-teal)]\">\n {icon}\n </span>\n <div className=\"min-w-0\">\n <p className=\"text-xs text-text-secondary\">{label}</p>\n <p className=\"text-sm font-semibold text-text-primary\">{value}</p>\n </div>\n </div>\n);\n\nexport const TakeAssessmentPage: FC = () => {\n const { assessmentId } = useParams<{ assessmentId: string }>();\n const { currentUser, navigateTo } = useBuildmyiq();\n\n const assessmentQuery = useBuiAssessment(assessmentId);\n const assessment = assessmentQuery.data ?? null;\n\n const enrollmentsQuery = useBuiEnrollments({\n learnerId: currentUser?.id,\n courseId: assessment?.courseId,\n enabled: Boolean(currentUser?.id && assessment?.courseId),\n });\n const enrollmentId = enrollmentsQuery.data?.items?.[0]?.id;\n\n const submit = useBuiSubmitAssessment();\n const [answers, setAnswers] = useState<Record<string, AnswerValue>>({});\n const [submittedId, setSubmittedId] = useState<string | null>(null);\n\n const questions = useMemo<BuiQuestion[]>(\n () => [...(assessment?.questions ?? [])].sort((a, b) => a.position - b.position),\n [assessment]\n );\n\n const setAnswer = (qid: string, value: AnswerValue): void => {\n setAnswers((prev) => ({ ...prev, [qid]: value }));\n };\n\n const toggleMulti = (qid: string, label: string): void => {\n setAnswers((prev) => {\n const cur = Array.isArray(prev[qid]) ? (prev[qid] as string[]) : [];\n return {\n ...prev,\n [qid]: cur.includes(label) ? cur.filter((l) => l !== label) : [...cur, label],\n };\n });\n };\n\n const isAnswered = (q: BuiQuestion): boolean => {\n const a = answers[q.id];\n return Array.isArray(a) ? a.length > 0 : Boolean(a && String(a).trim());\n };\n\n const answeredCount = questions.filter(isAnswered).length;\n const answeredPct = questions.length > 0 ? (answeredCount / questions.length) * 100 : 0;\n const remaining = questions.length - answeredCount;\n const encouragement =\n answeredCount === 0\n ? 'Answer a question to get started.'\n : remaining === 0\n ? 'All questions answered — ready to submit.'\n : `${remaining} question${remaining === 1 ? '' : 's'} left to answer.`;\n\n const handleSubmit = async (): Promise<void> => {\n if (!assessmentId) return;\n const result = await submit.mutateAsync({\n assessmentId,\n enrollmentId,\n attempt: 1,\n answers,\n });\n setSubmittedId(result.id);\n };\n\n const backToCourse = assessment ? (\n <Button variant=\"ghost\" size=\"sm\" onClick={() => navigateTo(`catalog/${assessment.courseId}`)}>\n <ArrowLeft className=\"mr-1 h-4 w-4\" /> Back to course\n </Button>\n ) : undefined;\n\n if (!assessmentId) {\n return (\n <PageLayout title=\"Assessment\">\n <EmptyState illustration=\"empty-data\" title=\"No assessment selected\" />\n </PageLayout>\n );\n }\n\n if (assessmentQuery.isLoading) {\n return (\n <PageLayout title=\"Assessment\">\n <LoadingView label=\"Loading the assessment…\" />\n </PageLayout>\n );\n }\n\n if (assessmentQuery.isError || !assessment) {\n return (\n <PageLayout title=\"Assessment\" actions={backToCourse}>\n <ErrorView\n title=\"Couldn't load this assessment\"\n message={\n assessmentQuery.error instanceof Error ? assessmentQuery.error.message : undefined\n }\n onRetry={() => assessmentQuery.refetch()}\n />\n </PageLayout>\n );\n }\n\n if (assessment.status !== 'PUBLISHED') {\n return (\n <PageLayout title={assessment.title} actions={backToCourse}>\n <EmptyState\n illustration=\"empty-data\"\n title=\"This assessment isn't open yet\"\n description=\"It has to be published by an educator before you can take it.\"\n />\n </PageLayout>\n );\n }\n\n // Submitted confirmation\n if (submittedId) {\n return (\n <PageLayout title={assessment.title} actions={backToCourse}>\n <div className=\"mx-auto max-w-lg\">\n <GlassCard\n treatment=\"glass\"\n glow\n className=\"flex flex-col items-center gap-4 p-8 text-center\"\n >\n <div className=\"flex h-14 w-14 items-center justify-center rounded-full bg-status-success-bg-subtle\">\n <CheckCircle2 className=\"h-7 w-7 text-status-success-text\" />\n </div>\n <div>\n <h2 className=\"text-lg font-semibold text-text-primary\">Submitted</h2>\n <p className=\"mt-1 text-sm text-text-secondary\">\n Your answers for “{assessment.title}” were submitted.\n {assessment.isGraded\n ? ' An educator will grade them — you’ll see your score and feedback in My Learning.'\n : ' Thanks for completing it.'}\n </p>\n </div>\n <div className=\"flex flex-wrap justify-center gap-2\">\n <Button variant=\"secondary\" onClick={() => navigateTo('learning')}>\n Go to My Learning\n </Button>\n <Button variant=\"ghost\" onClick={() => navigateTo(`catalog/${assessment.courseId}`)}>\n Back to course\n </Button>\n </div>\n </GlassCard>\n </div>\n </PageLayout>\n );\n }\n\n return (\n <PageLayout\n title={assessment.title}\n description={assessment.description ?? undefined}\n actions={backToCourse}\n >\n <div className=\"mx-auto max-w-3xl pb-4\">\n {/* Meta strip — type · passing score · time limit · attempts · answered */}\n <GlassCard treatment=\"glass\" className=\"mb-6 p-4 sm:px-5\">\n <div className=\"flex flex-col divide-y divide-border-seam sm:flex-row sm:flex-wrap sm:items-center sm:divide-x sm:divide-y-0\">\n <MetaCell\n icon={<FileText className=\"h-4 w-4\" />}\n label=\"Type\"\n value={<StatusBadge status={assessment.type} />}\n />\n <MetaCell\n icon={<Target className=\"h-4 w-4\" />}\n label=\"Passing score\"\n value={`${assessment.passingScore}%`}\n />\n {assessment.timeLimitMin > 0 && (\n <MetaCell\n icon={<Clock className=\"h-4 w-4\" />}\n label=\"Time limit\"\n value={formatDuration(assessment.timeLimitMin)}\n />\n )}\n {assessment.maxAttempts > 0 && (\n <MetaCell\n icon={<RefreshCw className=\"h-4 w-4\" />}\n label=\"Attempts\"\n value={assessment.maxAttempts}\n />\n )}\n <div className=\"flex items-center gap-3 py-3 sm:ml-auto sm:py-0 sm:pl-5\">\n <ProgressRing\n value={answeredPct}\n size={44}\n stroke={5}\n label={`${answeredCount}/${questions.length}`}\n />\n <div className=\"min-w-0\">\n <p className=\"text-sm font-semibold text-text-primary tabular-nums\">\n {answeredCount} of {questions.length}\n </p>\n <p className=\"text-xs text-text-secondary\">answered</p>\n </div>\n </div>\n </div>\n </GlassCard>\n\n {questions.length === 0 ? (\n <EmptyState\n icon={<ClipboardCheck className=\"h-8 w-8\" />}\n title=\"No questions yet\"\n description=\"This assessment doesn't have any questions to answer.\"\n />\n ) : (\n <div className=\"flex flex-col gap-4\">\n {questions.map((q, idx) => {\n const choices =\n q.type === 'TRUE_FALSE' ? TRUE_FALSE_CHOICES : parseChoices(q.options);\n const answer = answers[q.id];\n const answered = isAnswered(q);\n return (\n <GlassCard\n key={q.id}\n treatment={answered ? 'insight' : 'plain'}\n className={cn(\n 'p-5 transition-shadow',\n answered && 'ring-1 ring-[var(--color-accent-soft)]'\n )}\n >\n <div className=\"flex items-start gap-3 sm:gap-4\">\n <span\n className={cn(\n 'mt-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-sm font-semibold tabular-nums transition-colors',\n answered\n ? 'bg-status-success-bg-subtle text-status-success-text'\n : 'bg-[var(--color-accent-soft)] text-text-primary'\n )}\n >\n {answered ? <CheckCircle2 className=\"h-4 w-4\" /> : idx + 1}\n </span>\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <span className=\"rounded-md bg-bg-sunken px-2 py-0.5 text-xs font-medium text-text-secondary\">\n {formatEnumLabel(q.type)}\n </span>\n <span className=\"ml-auto text-xs text-text-secondary tabular-nums\">\n {q.points} {q.points === 1 ? 'point' : 'points'}\n </span>\n </div>\n <p className=\"mt-2.5 font-medium text-text-primary\">{q.prompt}</p>\n\n <div className=\"mt-4\">\n {(q.type === 'SINGLE_CHOICE' || q.type === 'TRUE_FALSE') && (\n <div className=\"flex flex-col gap-2\">\n {choices.map((c) => (\n <label\n key={c.label}\n className={cn(\n 'flex min-h-[44px] cursor-pointer items-center gap-3 rounded-xl border px-3.5 py-2.5 text-sm transition-colors',\n answer === c.label\n ? 'border-[var(--color-accent-teal)] bg-[var(--color-accent-soft)] text-text-primary'\n : 'border-border-seam text-text-secondary hover:border-border-strong hover:bg-bg-sunken'\n )}\n >\n <input\n type=\"radio\"\n name={q.id}\n className=\"h-4 w-4 accent-[var(--color-accent-teal)]\"\n checked={answer === c.label}\n onChange={() => setAnswer(q.id, c.label)}\n />\n {c.label}\n </label>\n ))}\n </div>\n )}\n\n {q.type === 'MULTIPLE_CHOICE' && (\n <div className=\"flex flex-col gap-2\">\n {choices.map((c) => {\n const checked = Array.isArray(answer) && answer.includes(c.label);\n return (\n <label\n key={c.label}\n className={cn(\n 'flex min-h-[44px] cursor-pointer items-center gap-3 rounded-xl border px-3.5 py-2.5 text-sm transition-colors',\n checked\n ? 'border-[var(--color-accent-teal)] bg-[var(--color-accent-soft)] text-text-primary'\n : 'border-border-seam text-text-secondary hover:border-border-strong hover:bg-bg-sunken'\n )}\n >\n <input\n type=\"checkbox\"\n className=\"h-4 w-4 rounded accent-[var(--color-accent-teal)]\"\n checked={checked}\n onChange={() => toggleMulti(q.id, c.label)}\n />\n {c.label}\n </label>\n );\n })}\n </div>\n )}\n\n {q.type === 'SHORT_ANSWER' && (\n <Input\n value={typeof answer === 'string' ? answer : ''}\n onChange={(e) => setAnswer(q.id, e.target.value)}\n placeholder=\"Type your answer here…\"\n />\n )}\n\n {(q.type === 'ESSAY' || q.type === 'CODE') && (\n <Textarea\n rows={q.type === 'CODE' ? 6 : 4}\n value={typeof answer === 'string' ? answer : ''}\n onChange={(e) => setAnswer(q.id, e.target.value)}\n placeholder={\n q.type === 'CODE' ? 'Write your code…' : 'Write your answer…'\n }\n className={q.type === 'CODE' ? 'font-mono text-sm' : undefined}\n />\n )}\n </div>\n </div>\n </div>\n </GlassCard>\n );\n })}\n\n {submit.isError && (\n <p className=\"text-sm text-status-error-text\">\n {submit.error instanceof Error\n ? submit.error.message\n : 'Could not submit your answers.'}\n </p>\n )}\n\n {/* Sticky submit bar */}\n <div className=\"sticky bottom-0 z-10 pt-2\">\n <div className=\"flex flex-col gap-3 rounded-2xl border border-border-seam bg-bg-surface/90 p-4 shadow-elevation-2 backdrop-blur sm:flex-row sm:items-center sm:justify-between\">\n <div className=\"flex items-center gap-3\">\n <ProgressRing\n value={answeredPct}\n size={44}\n stroke={5}\n label={`${answeredCount}/${questions.length}`}\n />\n <div className=\"min-w-0\">\n <p className=\"text-sm font-semibold text-text-primary tabular-nums\">\n {answeredCount} of {questions.length} answered\n </p>\n <p className=\"text-xs text-text-secondary\">{encouragement}</p>\n </div>\n </div>\n <Button\n size=\"lg\"\n className=\"max-sm:w-full\"\n onClick={handleSubmit}\n disabled={submit.isPending || answeredCount === 0}\n >\n <Send className=\"mr-1.5 h-4 w-4\" />\n {submit.isPending ? 'Submitting…' : 'Submit assessment'}\n </Button>\n </div>\n </div>\n </div>\n )}\n </div>\n </PageLayout>\n );\n};\n"],"mappings":"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"}
|