@burdenoff/microfe-buildmyiq 2026.715.2 → 2026.720.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/buildmyiq/BuildmyiqAdminRoot.d.ts.map +1 -1
- package/dist/buildmyiq/BuildmyiqAdminRoot.js +149 -135
- package/dist/buildmyiq/BuildmyiqAdminRoot.js.map +1 -1
- package/dist/buildmyiq/BuildmyiqRoot.d.ts.map +1 -1
- package/dist/buildmyiq/BuildmyiqRoot.js +59 -54
- package/dist/buildmyiq/BuildmyiqRoot.js.map +1 -1
- package/dist/buildmyiq/BuildmyiqRoutes.js.map +1 -1
- package/dist/buildmyiq/components/Avatar.js.map +1 -1
- package/dist/buildmyiq/components/BuiDiscussion.js.map +1 -1
- package/dist/buildmyiq/components/BuiPermissionButton.d.ts +25 -0
- package/dist/buildmyiq/components/BuiPermissionButton.d.ts.map +1 -0
- package/dist/buildmyiq/components/BuiPermissionButton.js +15 -0
- package/dist/buildmyiq/components/BuiPermissionButton.js.map +1 -0
- package/dist/buildmyiq/components/BuiTags.js.map +1 -1
- package/dist/buildmyiq/components/DataTable.js.map +1 -1
- package/dist/buildmyiq/components/EmptyState.js.map +1 -1
- package/dist/buildmyiq/components/EvidenceUpload.js.map +1 -1
- package/dist/buildmyiq/components/FileLink.js.map +1 -1
- package/dist/buildmyiq/components/FilterTabs.js.map +1 -1
- package/dist/buildmyiq/components/MetricsCard.js.map +1 -1
- package/dist/buildmyiq/components/MiniBar.js.map +1 -1
- package/dist/buildmyiq/components/PageLayout.js.map +1 -1
- package/dist/buildmyiq/components/Pagination.js.map +1 -1
- package/dist/buildmyiq/components/StateViews.js.map +1 -1
- package/dist/buildmyiq/components/StatusBadge.js.map +1 -1
- package/dist/buildmyiq/components/Tabs.js.map +1 -1
- package/dist/buildmyiq/components/Viz.js.map +1 -1
- package/dist/buildmyiq/components/index.d.ts +1 -0
- package/dist/buildmyiq/components/index.d.ts.map +1 -1
- package/dist/buildmyiq/components/index.js +1 -0
- package/dist/buildmyiq/context/BuildmyiqContext.js.map +1 -1
- package/dist/buildmyiq/hooks/index.d.ts +1 -0
- package/dist/buildmyiq/hooks/index.d.ts.map +1 -1
- package/dist/buildmyiq/hooks/index.js +1 -0
- package/dist/buildmyiq/hooks/useBuiComments.js.map +1 -1
- package/dist/buildmyiq/hooks/useBuiFiles.js.map +1 -1
- package/dist/buildmyiq/hooks/useBuiPermissions.d.ts +54 -0
- package/dist/buildmyiq/hooks/useBuiPermissions.d.ts.map +1 -0
- package/dist/buildmyiq/hooks/useBuiPermissions.js +2 -0
- package/dist/buildmyiq/hooks/useBuiTags.js.map +1 -1
- package/dist/buildmyiq/hooks/useBuildmyiqApi.js.map +1 -1
- package/dist/buildmyiq/hooks/useCursorPager.js.map +1 -1
- package/dist/buildmyiq/pages/DashboardPage.js.map +1 -1
- package/dist/buildmyiq/pages/_shared/CourseCard.js.map +1 -1
- package/dist/buildmyiq/pages/assessments/AssessmentDetailPage.js.map +1 -1
- package/dist/buildmyiq/pages/assessments/AssessmentsPage.js.map +1 -1
- package/dist/buildmyiq/pages/assessments/TakeAssessmentPage.js.map +1 -1
- package/dist/buildmyiq/pages/authoring/AuthoringPage.d.ts.map +1 -1
- package/dist/buildmyiq/pages/authoring/AuthoringPage.js +97 -86
- package/dist/buildmyiq/pages/authoring/AuthoringPage.js.map +1 -1
- package/dist/buildmyiq/pages/authoring/CourseBuilderPage.d.ts.map +1 -1
- package/dist/buildmyiq/pages/authoring/CourseBuilderPage.js +159 -118
- package/dist/buildmyiq/pages/authoring/CourseBuilderPage.js.map +1 -1
- package/dist/buildmyiq/pages/authoring/_forms.js.map +1 -1
- package/dist/buildmyiq/pages/catalog/CatalogPage.js.map +1 -1
- package/dist/buildmyiq/pages/catalog/CourseDetailPage.d.ts.map +1 -1
- package/dist/buildmyiq/pages/catalog/CourseDetailPage.js +75 -69
- package/dist/buildmyiq/pages/catalog/CourseDetailPage.js.map +1 -1
- package/dist/buildmyiq/pages/certificates/CertificatesPage.d.ts.map +1 -1
- package/dist/buildmyiq/pages/certificates/CertificatesPage.js +186 -170
- package/dist/buildmyiq/pages/certificates/CertificatesPage.js.map +1 -1
- package/dist/buildmyiq/pages/cohorts/CohortsPage.d.ts.map +1 -1
- package/dist/buildmyiq/pages/cohorts/CohortsPage.js +177 -166
- package/dist/buildmyiq/pages/cohorts/CohortsPage.js.map +1 -1
- package/dist/buildmyiq/pages/insights/InsightsPage.js.map +1 -1
- package/dist/buildmyiq/pages/learning/LessonPlayerPage.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.d.ts.map +1 -1
- package/dist/buildmyiq/pages/submissions/SubmissionsPage.js +160 -154
- package/dist/buildmyiq/pages/submissions/SubmissionsPage.js.map +1 -1
- package/dist/buildmyiq/shared/hooks/useTr.js.map +1 -1
- package/dist/buildmyiq/types/index.d.ts.map +1 -1
- package/dist/buildmyiq/types/index.js.map +1 -1
- package/dist/buildmyiq/utils/cn.js.map +1 -1
- package/dist/buildmyiq/utils/formatters.js.map +1 -1
- package/dist/buildmyiq/utils/navigation.js.map +1 -1
- package/dist/node_modules/clsx/dist/clsx.js.map +1 -1
- package/dist/node_modules/tailwind-merge/dist/bundle-mjs.js +1 -1
- package/dist/node_modules/tailwind-merge/dist/bundle-mjs.js.map +1 -1
- package/dist/services/createSandboxAssistantTransport.js.map +1 -1
- package/dist/stories/EmptyState.stories.d.ts +10 -0
- package/dist/stories/EmptyState.stories.d.ts.map +1 -0
- package/dist/stories/MetricsCard.stories.d.ts +10 -0
- package/dist/stories/MetricsCard.stories.d.ts.map +1 -0
- package/dist/stories/StatusBadge.stories.d.ts +12 -0
- package/dist/stories/StatusBadge.stories.d.ts.map +1 -0
- package/dist/utils/pageContext.js.map +1 -1
- package/package.json +14 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CourseBuilderPage.js","names":[],"sources":["../../../../src/buildmyiq/pages/authoring/CourseBuilderPage.tsx"],"sourcesContent":["/**\n * CourseBuilderPage — the per-course authoring workspace.\n * @module buildmyiq/pages/authoring\n *\n * Loads one course (`useBuiCourse`) and exposes four tabs:\n * - Details — editable course meta (`useBuiUpdateCourse`).\n * - Curriculum — the module/lesson builder (`useBuiModules`, `useBuiCreateModule`,\n * `useBuiCreateLesson`) plus a right rail with the cover-image dropzone\n * (EvidenceUpload → coverFileId) and a live course-details summary.\n * - Assessments — course assessments (`useBuiAssessments`) linking out to the\n * assessment editor.\n * - Discussion — the shared wspace-conversations thread (`BuiDiscussion`).\n *\n * Publish (`useBuiPublishCourse`) and Archive (`useBuiArchiveCourse`) live in the\n * page header so they are reachable from every tab. Every list carries loading /\n * error / empty states; all data is live `bui*` data — no fabrication.\n */\n\nimport type { FC, ReactNode } from 'react';\nimport { useState } from 'react';\nimport { useParams } from 'react-router-dom';\nimport {\n Archive,\n ArrowLeft,\n BarChart3,\n BookOpen,\n Calendar,\n ClipboardList,\n Clock,\n Eye,\n FileText,\n Globe,\n Image as ImageIcon,\n Link2,\n ListChecks,\n MessageSquare,\n MousePointerClick,\n PlayCircle,\n Plus,\n Radio,\n Send,\n type LucideIcon,\n} from 'lucide-react';\nimport { Button, GlassCard, Input, Switch, Textarea } from '@burdenoff/fe-libs/ui';\nimport {\n PageLayout,\n Tabs,\n type TabItem,\n StatusBadge,\n EmptyState,\n LoadingView,\n ErrorView,\n Section,\n EvidenceUpload,\n BuiDiscussion,\n} from '../../components';\nimport { Modal, Field, SelectField, NO_SKILL } from './_forms';\nimport { useBuildmyiq } from '../../context';\nimport {\n useBuiCourse,\n useBuiUpdateCourse,\n useBuiPublishCourse,\n useBuiArchiveCourse,\n useBuiModules,\n useBuiCreateModule,\n useBuiCreateLesson,\n useBuiAssessments,\n useBuiSkills,\n type UploadedBuiFile,\n} from '../../hooks';\nimport { COURSE_LEVELS, LESSON_TYPES } from '../../types';\nimport type {\n BuiCourse,\n BuiCourseLevel,\n BuiCreateLessonInput,\n BuiCreateModuleInput,\n BuiLesson,\n BuiLessonType,\n BuiModule,\n BuiUpdateCourseInput,\n} from '../../types';\nimport { cn } from '../../utils/cn';\nimport { formatDate, formatDuration, formatEnumLabel } from '../../utils/formatters';\n\ntype BuilderTab = 'details' | 'curriculum' | 'assessments' | 'discussion';\n\n/** Per-lesson-type icon + theme-safe tint (semantic tokens only). */\nconst LESSON_META: Record<BuiLessonType, { icon: LucideIcon; tint: string }> = {\n ARTICLE: { icon: FileText, tint: 'bg-bg-sunken text-text-secondary' },\n VIDEO: { icon: PlayCircle, tint: 'bg-[var(--color-accent-soft)] text-text-primary' },\n QUIZ: { icon: ListChecks, tint: 'bg-status-warning-bg-subtle text-status-warning-text' },\n ASSIGNMENT: {\n icon: ClipboardList,\n tint: 'bg-status-success-bg-subtle text-status-success-text',\n },\n INTERACTIVE: {\n icon: MousePointerClick,\n tint: 'bg-[color-mix(in_srgb,var(--color-accent-teal)_18%,transparent)] text-[var(--color-accent-teal)]',\n },\n LIVE_SESSION: {\n icon: Radio,\n tint: 'bg-[color-mix(in_srgb,var(--color-accent-primary)_16%,transparent)] text-[var(--color-accent-primary)]',\n },\n};\n\n// ============================================================================\n// Details tab — pure metadata form (publish/archive live in the page header,\n// cover upload lives in the Curriculum right rail).\n// ============================================================================\n\nconst DetailsTab: FC<{ course: BuiCourse }> = ({ course }) => {\n const updateCourse = useBuiUpdateCourse();\n const skillsQuery = useBuiSkills();\n const skills = skillsQuery.data ?? [];\n\n const [title, setTitle] = useState(course.title);\n const [summary, setSummary] = useState(course.summary ?? '');\n const [description, setDescription] = useState(course.description ?? '');\n const [level, setLevel] = useState<BuiCourseLevel>(course.level);\n const [language, setLanguage] = useState(course.language);\n const [estimatedHours, setEstimatedHours] = useState(String(course.estimatedHours ?? 0));\n const [skillId, setSkillId] = useState<string>(course.skillId ?? NO_SKILL);\n const [isPublic, setIsPublic] = useState(course.isPublic);\n const [saveError, setSaveError] = useState<string | null>(null);\n\n const handleSave = async () => {\n setSaveError(null);\n const input: BuiUpdateCourseInput = {\n title: title.trim() || course.title,\n summary: summary.trim() || undefined,\n description: description.trim() || undefined,\n level,\n language: language.trim() || 'en',\n estimatedHours: estimatedHours.trim() ? Number(estimatedHours) : 0,\n skillId: skillId !== NO_SKILL ? skillId : undefined,\n isPublic,\n };\n try {\n await updateCourse.mutateAsync({ id: course.id, input });\n } catch (e) {\n setSaveError(e instanceof Error ? e.message : 'Failed to save changes.');\n }\n };\n\n return (\n <div className=\"mx-auto max-w-3xl\">\n <Section\n title=\"Course details\"\n description=\"Metadata learners see in the catalog.\"\n action={\n <Button onClick={handleSave} disabled={updateCourse.isPending}>\n {updateCourse.isPending ? 'Saving…' : 'Save changes'}\n </Button>\n }\n >\n <GlassCard treatment=\"plain\" className=\"p-5\">\n <div className=\"grid gap-4\">\n <Field label=\"Title\" htmlFor=\"detail-title\" required>\n <Input id=\"detail-title\" value={title} onChange={(e) => setTitle(e.target.value)} />\n </Field>\n <Field label=\"Summary\" htmlFor=\"detail-summary\">\n <Textarea\n id=\"detail-summary\"\n rows={2}\n value={summary}\n onChange={(e) => setSummary(e.target.value)}\n placeholder=\"A short description shown on the catalog card.\"\n />\n </Field>\n <Field label=\"Description\" htmlFor=\"detail-description\">\n <Textarea\n id=\"detail-description\"\n rows={5}\n value={description}\n onChange={(e) => setDescription(e.target.value)}\n placeholder=\"The full course overview.\"\n />\n </Field>\n <div className=\"grid gap-4 sm:grid-cols-2\">\n <Field label=\"Level\">\n <SelectField\n value={level}\n onChange={(v) => setLevel(v as BuiCourseLevel)}\n options={COURSE_LEVELS.map((l) => ({ value: l, label: formatEnumLabel(l) }))}\n />\n </Field>\n <Field label=\"Language\" htmlFor=\"detail-language\">\n <Input\n id=\"detail-language\"\n value={language}\n onChange={(e) => setLanguage(e.target.value)}\n placeholder=\"en\"\n />\n </Field>\n <Field label=\"Estimated hours\" htmlFor=\"detail-hours\">\n <Input\n id=\"detail-hours\"\n type=\"number\"\n min={0}\n value={estimatedHours}\n onChange={(e) => setEstimatedHours(e.target.value)}\n />\n </Field>\n <Field label=\"Skill\">\n <SelectField\n value={skillId}\n onChange={setSkillId}\n options={[\n { value: NO_SKILL, label: skillsQuery.isLoading ? 'Loading…' : 'No skill' },\n ...skills.map((s) => ({ value: s.id, label: s.name })),\n ]}\n />\n </Field>\n </div>\n <label className=\"inline-flex cursor-pointer items-center gap-2 text-sm text-text-secondary\">\n <Switch checked={isPublic} onCheckedChange={setIsPublic} />\n Publicly visible in the catalog\n </label>\n {saveError && <p className=\"text-sm text-status-error-text\">{saveError}</p>}\n </div>\n </GlassCard>\n </Section>\n </div>\n );\n};\n\n// ============================================================================\n// Curriculum tab\n// ============================================================================\n\nconst AddModuleModal: FC<{ courseId: string; position: number; onClose: () => void }> = ({\n courseId,\n position,\n onClose,\n}) => {\n const createModule = useBuiCreateModule();\n const [title, setTitle] = useState('');\n const [description, setDescription] = useState('');\n const [error, setError] = useState<string | null>(null);\n\n const canSubmit = title.trim().length > 0 && !createModule.isPending;\n\n const handleSubmit = async () => {\n if (!canSubmit) return;\n setError(null);\n const input: BuiCreateModuleInput = {\n courseId,\n title: title.trim(),\n description: description.trim() || undefined,\n position,\n };\n try {\n await createModule.mutateAsync(input);\n onClose();\n } catch (e) {\n setError(e instanceof Error ? e.message : 'Failed to add the module.');\n }\n };\n\n return (\n <Modal\n open\n onClose={onClose}\n title=\"Add module\"\n footer={\n <>\n <Button variant=\"ghost\" onClick={onClose} disabled={createModule.isPending}>\n Cancel\n </Button>\n <Button onClick={handleSubmit} disabled={!canSubmit}>\n {createModule.isPending ? 'Adding…' : 'Add module'}\n </Button>\n </>\n }\n >\n <div className=\"grid gap-4\">\n <Field label=\"Module title\" htmlFor=\"module-title\" required>\n <Input\n id=\"module-title\"\n value={title}\n onChange={(e) => setTitle(e.target.value)}\n placeholder=\"e.g. Getting started\"\n />\n </Field>\n <Field label=\"Description\" htmlFor=\"module-description\">\n <Textarea\n id=\"module-description\"\n rows={3}\n value={description}\n onChange={(e) => setDescription(e.target.value)}\n />\n </Field>\n {error && <p className=\"text-sm text-status-error-text\">{error}</p>}\n </div>\n </Modal>\n );\n};\n\nconst AddLessonModal: FC<{\n courseId: string;\n moduleId: string;\n position: number;\n onClose: () => void;\n}> = ({ courseId, moduleId, position, onClose }) => {\n const createLesson = useBuiCreateLesson();\n const [title, setTitle] = useState('');\n const [type, setType] = useState<BuiLessonType>('ARTICLE');\n const [content, setContent] = useState('');\n const [durationMin, setDurationMin] = useState('');\n const [isPreview, setIsPreview] = useState(false);\n const [media, setMedia] = useState<UploadedBuiFile | null>(null);\n const [error, setError] = useState<string | null>(null);\n\n const canSubmit = title.trim().length > 0 && !createLesson.isPending;\n\n const handleSubmit = async () => {\n if (!canSubmit) return;\n setError(null);\n const input: BuiCreateLessonInput = {\n courseId,\n moduleId,\n title: title.trim(),\n type,\n content: content.trim() || undefined,\n durationMin: durationMin.trim() ? Number(durationMin) : undefined,\n position,\n isPreview,\n mediaFileId: media?.fileId,\n };\n try {\n await createLesson.mutateAsync(input);\n onClose();\n } catch (e) {\n setError(e instanceof Error ? e.message : 'Failed to add the lesson.');\n }\n };\n\n return (\n <Modal\n open\n onClose={onClose}\n title=\"Add lesson\"\n className=\"sm:max-w-xl\"\n footer={\n <>\n <Button variant=\"ghost\" onClick={onClose} disabled={createLesson.isPending}>\n Cancel\n </Button>\n <Button onClick={handleSubmit} disabled={!canSubmit}>\n {createLesson.isPending ? 'Adding…' : 'Add lesson'}\n </Button>\n </>\n }\n >\n <div className=\"grid gap-4\">\n <Field label=\"Lesson title\" htmlFor=\"lesson-title\" required>\n <Input\n id=\"lesson-title\"\n value={title}\n onChange={(e) => setTitle(e.target.value)}\n placeholder=\"e.g. What is data literacy?\"\n />\n </Field>\n <div className=\"grid gap-4 sm:grid-cols-2\">\n <Field label=\"Type\">\n <SelectField\n value={type}\n onChange={(v) => setType(v as BuiLessonType)}\n options={LESSON_TYPES.map((t) => ({ value: t, label: formatEnumLabel(t) }))}\n />\n </Field>\n <Field label=\"Duration (minutes)\" htmlFor=\"lesson-duration\">\n <Input\n id=\"lesson-duration\"\n type=\"number\"\n min={0}\n value={durationMin}\n onChange={(e) => setDurationMin(e.target.value)}\n placeholder=\"0\"\n />\n </Field>\n </div>\n <Field\n label=\"Content\"\n htmlFor=\"lesson-content\"\n hint=\"Lesson body — markdown or plain text.\"\n >\n <Textarea\n id=\"lesson-content\"\n rows={5}\n value={content}\n onChange={(e) => setContent(e.target.value)}\n />\n </Field>\n <EvidenceUpload\n id=\"lesson-media\"\n label=\"Media (optional)\"\n value={media}\n onChange={setMedia}\n hint=\"Attach a video, audio file or PDF for this lesson.\"\n />\n <label className=\"inline-flex cursor-pointer items-center gap-2 text-sm text-text-secondary\">\n <Switch checked={isPreview} onCheckedChange={setIsPreview} />\n Free preview lesson\n </label>\n {error && <p className=\"text-sm text-status-error-text\">{error}</p>}\n </div>\n </Modal>\n );\n};\n\nconst LessonRow: FC<{ lesson: BuiLesson }> = ({ lesson }) => {\n const meta = LESSON_META[lesson.type];\n const Icon = meta.icon;\n return (\n <li className=\"flex items-center gap-3 rounded-lg border border-border-seam bg-bg-surface px-3 py-2.5\">\n <span\n className={cn('flex h-8 w-8 shrink-0 items-center justify-center rounded-lg', meta.tint)}\n aria-hidden=\"true\"\n >\n <Icon className=\"h-4 w-4\" />\n </span>\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate text-sm font-medium text-text-primary\">{lesson.title}</p>\n <p className=\"text-xs text-text-tertiary\">{formatEnumLabel(lesson.type)}</p>\n </div>\n {lesson.durationMin > 0 && (\n <span className=\"inline-flex shrink-0 items-center gap-1 text-xs text-text-secondary\">\n <Clock className=\"h-3.5 w-3.5\" /> {formatDuration(lesson.durationMin)}\n </span>\n )}\n {lesson.isPreview && (\n <span className=\"shrink-0 rounded-full bg-[var(--color-accent-soft)] px-2 py-0.5 text-xs font-medium text-text-primary\">\n Preview\n </span>\n )}\n </li>\n );\n};\n\nconst ModuleCard: FC<{ module: BuiModule; index: number; onAddLesson: () => void }> = ({\n module,\n index,\n onAddLesson,\n}) => {\n const lessons = [...(module.lessons ?? [])].sort((a, b) => a.position - b.position);\n const totalMin = lessons.reduce((sum, l) => sum + (l.durationMin || 0), 0);\n return (\n <GlassCard treatment=\"plain\" className=\"overflow-hidden\">\n <div className=\"flex flex-wrap items-center gap-3 border-b border-border-seam bg-bg-sunken/40 px-4 py-3\">\n <span\n className=\"flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-action-primary-bg text-sm font-semibold tabular-nums text-action-primary-text\"\n aria-hidden=\"true\"\n >\n {index + 1}\n </span>\n <div className=\"min-w-0 flex-1\">\n <h3 className=\"truncate font-semibold text-text-primary\">\n <span className=\"text-text-tertiary\">Module {index + 1} · </span>\n {module.title}\n </h3>\n <p className=\"text-xs text-text-secondary\">\n {lessons.length} {lessons.length === 1 ? 'lesson' : 'lessons'}\n {totalMin > 0 && ` · ${formatDuration(totalMin)}`}\n </p>\n </div>\n <Button variant=\"outline\" size=\"sm\" onClick={onAddLesson} className=\"shrink-0\">\n <Plus className=\"mr-1.5 h-4 w-4\" /> Add lesson\n </Button>\n </div>\n {module.description && (\n <p className=\"border-b border-border-seam px-4 py-2.5 text-sm text-text-secondary\">\n {module.description}\n </p>\n )}\n <div className=\"p-3\">\n {lessons.length === 0 ? (\n <button\n type=\"button\"\n onClick={onAddLesson}\n className=\"flex w-full items-center justify-center gap-2 rounded-lg border border-dashed border-border-seam px-3 py-3 text-sm text-text-secondary transition-colors hover:border-[var(--color-action-primary-bg)] hover:text-text-primary\"\n >\n <Plus className=\"h-4 w-4\" /> Add the first lesson\n </button>\n ) : (\n <ul className=\"flex flex-col gap-2\">\n {lessons.map((lesson) => (\n <LessonRow key={lesson.id} lesson={lesson} />\n ))}\n </ul>\n )}\n </div>\n </GlassCard>\n );\n};\n\nconst DetailRow: FC<{ icon: ReactNode; label: string; value: string }> = ({\n icon,\n label,\n value,\n}) => (\n <div className=\"flex items-center gap-3 py-2.5\">\n <span className=\"shrink-0 text-text-tertiary\" aria-hidden=\"true\">\n {icon}\n </span>\n <span className=\"text-sm text-text-secondary\">{label}</span>\n <span className=\"ml-auto min-w-0 truncate text-right text-sm font-medium text-text-primary\">\n {value}\n </span>\n </div>\n);\n\nconst CurriculumTab: FC<{ course: BuiCourse }> = ({ course }) => {\n const courseId = course.id;\n const modulesQuery = useBuiModules(courseId);\n const updateCourse = useBuiUpdateCourse();\n const [showAddModule, setShowAddModule] = useState(false);\n const [lessonModuleId, setLessonModuleId] = useState<string | null>(null);\n const [coverFile, setCoverFile] = useState<UploadedBuiFile | null>(null);\n const [coverError, setCoverError] = useState<string | null>(null);\n\n const modules = [...(modulesQuery.data ?? [])].sort((a, b) => a.position - b.position);\n const lessonModule = modules.find((m) => m.id === lessonModuleId) ?? null;\n\n const handleCoverChange = async (file: UploadedBuiFile | null) => {\n setCoverFile(file);\n if (!file) return;\n setCoverError(null);\n try {\n await updateCourse.mutateAsync({ id: courseId, input: { coverFileId: file.fileId } });\n } catch (e) {\n setCoverError(e instanceof Error ? e.message : 'Failed to update the cover.');\n }\n };\n\n return (\n <div className=\"grid gap-6 lg:grid-cols-3\">\n {/* Builder */}\n <div className=\"lg:col-span-2\">\n <Section\n title=\"Curriculum\"\n description=\"Organise the course into modules and lessons.\"\n action={\n <Button onClick={() => setShowAddModule(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> Add module\n </Button>\n }\n >\n {modulesQuery.isLoading ? (\n <LoadingView label=\"Loading curriculum…\" />\n ) : modulesQuery.isError ? (\n <ErrorView\n message=\"We couldn't load the curriculum.\"\n onRetry={() => modulesQuery.refetch()}\n />\n ) : modules.length === 0 ? (\n <EmptyState\n illustration=\"onboarding\"\n title=\"No modules yet\"\n description=\"Add your first module, then fill it with lessons.\"\n action={\n <Button onClick={() => setShowAddModule(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> Add module\n </Button>\n }\n />\n ) : (\n <div className=\"flex flex-col gap-4\">\n {modules.map((mod, idx) => (\n <ModuleCard\n key={mod.id}\n module={mod}\n index={idx}\n onAddLesson={() => setLessonModuleId(mod.id)}\n />\n ))}\n <button\n type=\"button\"\n onClick={() => setShowAddModule(true)}\n className=\"flex w-full items-center justify-center gap-2 rounded-xl border-2 border-dashed border-border-seam px-4 py-4 text-sm font-medium text-text-secondary transition-colors hover:border-[var(--color-action-primary-bg)] hover:bg-bg-sunken/40 hover:text-text-primary\"\n >\n <Plus className=\"h-4 w-4\" /> Add module\n </button>\n </div>\n )}\n </Section>\n </div>\n\n {/* Right rail: cover + details */}\n <div className=\"flex flex-col gap-6\">\n <GlassCard treatment=\"plain\" className=\"p-5\">\n <h3 className=\"text-base font-semibold text-text-primary\">Course cover</h3>\n <p className=\"mt-0.5 text-sm text-text-secondary\">\n Upload an image to represent your course.\n </p>\n <div className=\"mt-3 aspect-video w-full overflow-hidden rounded-xl border border-border-seam bg-bg-sunken\">\n {course.cover?.downloadUrl ? (\n <img src={course.cover.downloadUrl} alt=\"\" className=\"h-full w-full object-cover\" />\n ) : (\n <div className=\"flex h-full items-center justify-center gap-2 text-sm text-text-tertiary\">\n <ImageIcon className=\"h-5 w-5\" /> No cover yet\n </div>\n )}\n </div>\n <div className=\"mt-3\">\n <EvidenceUpload\n id=\"course-cover\"\n label={course.cover?.downloadUrl ? 'Replace cover' : 'Upload cover'}\n value={coverFile}\n onChange={handleCoverChange}\n accept=\"image/*\"\n hint=\"PNG or JPG · 16:9 ratio recommended.\"\n />\n {coverError && <p className=\"mt-1 text-xs text-status-error-text\">{coverError}</p>}\n </div>\n </GlassCard>\n\n <GlassCard treatment=\"plain\" className=\"p-5\">\n <h3 className=\"text-base font-semibold text-text-primary\">Course details</h3>\n <dl className=\"mt-2 flex flex-col divide-y divide-border-seam\">\n <DetailRow icon={<Link2 className=\"h-4 w-4\" />} label=\"Slug\" value={course.slug} />\n <DetailRow\n icon={<Globe className=\"h-4 w-4\" />}\n label=\"Language\"\n value={course.language ? course.language.toUpperCase() : '—'}\n />\n <DetailRow\n icon={<BarChart3 className=\"h-4 w-4\" />}\n label=\"Level\"\n value={formatEnumLabel(course.level)}\n />\n <DetailRow\n icon={<Eye className=\"h-4 w-4\" />}\n label=\"Visibility\"\n value={course.isPublic ? 'Public' : 'Private'}\n />\n <DetailRow\n icon={<Calendar className=\"h-4 w-4\" />}\n label=\"Created\"\n value={formatDate(course.createdAt)}\n />\n </dl>\n </GlassCard>\n </div>\n\n {showAddModule && (\n <AddModuleModal\n courseId={courseId}\n position={modules.length}\n onClose={() => setShowAddModule(false)}\n />\n )}\n {lessonModuleId && (\n <AddLessonModal\n courseId={courseId}\n moduleId={lessonModuleId}\n position={lessonModule?.lessons?.length ?? 0}\n onClose={() => setLessonModuleId(null)}\n />\n )}\n </div>\n );\n};\n\n// ============================================================================\n// Assessments tab\n// ============================================================================\n\nconst AssessmentsTab: FC<{ courseId: string }> = ({ courseId }) => {\n const { navigateTo } = useBuildmyiq();\n const assessmentsQuery = useBuiAssessments(courseId);\n const assessments = assessmentsQuery.data ?? [];\n\n return (\n <Section\n title=\"Assessments\"\n description=\"Quizzes, exams and assignments attached to this course.\"\n >\n {assessmentsQuery.isLoading ? (\n <LoadingView label=\"Loading assessments…\" />\n ) : assessmentsQuery.isError ? (\n <ErrorView\n message=\"We couldn't load the assessments.\"\n onRetry={() => assessmentsQuery.refetch()}\n />\n ) : assessments.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n title=\"No assessments yet\"\n description=\"Create an assessment in the Assessments workspace to test learners on this course.\"\n action={\n <Button variant=\"outline\" onClick={() => navigateTo('assessments')}>\n Open assessments\n </Button>\n }\n />\n ) : (\n <div className=\"grid grid-cols-1 gap-3 sm:grid-cols-2\">\n {assessments.map((a) => (\n <GlassCard\n key={a.id}\n treatment=\"plain\"\n className=\"flex cursor-pointer flex-col gap-2 p-4 transition-shadow hover:shadow-lg\"\n onClick={() => navigateTo(`assessments/${a.id}`)}\n >\n <div className=\"flex items-start justify-between gap-2\">\n <h3 className=\"min-w-0 font-semibold text-text-primary\">{a.title}</h3>\n <StatusBadge status={a.status} />\n </div>\n {a.description && (\n <p className=\"line-clamp-2 text-sm text-text-secondary\">{a.description}</p>\n )}\n <div className=\"mt-auto flex flex-wrap items-center gap-3 pt-1 text-xs text-text-secondary\">\n <span className=\"rounded-full bg-bg-sunken px-2 py-0.5\">\n {formatEnumLabel(a.type)}\n </span>\n <span>Pass ≥ {a.passingScore ?? 0}%</span>\n {a.timeLimitMin > 0 && (\n <span className=\"inline-flex items-center gap-1\">\n <Clock className=\"h-3.5 w-3.5\" /> {formatDuration(a.timeLimitMin)}\n </span>\n )}\n </div>\n </GlassCard>\n ))}\n </div>\n )}\n </Section>\n );\n};\n\n// ============================================================================\n// Page\n// ============================================================================\n\nexport const CourseBuilderPage: FC = () => {\n const { courseId } = useParams<{ courseId: string }>();\n const { navigateTo } = useBuildmyiq();\n const [tab, setTab] = useState<BuilderTab>('details');\n const courseQuery = useBuiCourse(courseId);\n const course = courseQuery.data ?? null;\n\n const publishCourse = useBuiPublishCourse();\n const archiveCourse = useBuiArchiveCourse();\n const [actionError, setActionError] = useState<string | null>(null);\n const [confirmArchive, setConfirmArchive] = useState(false);\n\n const handlePublish = async () => {\n if (!course) return;\n setActionError(null);\n try {\n await publishCourse.mutateAsync(course.id);\n } catch (e) {\n setActionError(e instanceof Error ? e.message : 'Failed to publish the course.');\n }\n };\n\n const handleArchive = async () => {\n if (!course) return;\n setActionError(null);\n try {\n await archiveCourse.mutateAsync(course.id);\n setConfirmArchive(false);\n } catch (e) {\n setActionError(e instanceof Error ? e.message : 'Failed to archive the course.');\n }\n };\n\n if (courseQuery.isLoading) {\n return (\n <PageLayout title=\"Course builder\">\n <LoadingView label=\"Loading course…\" />\n </PageLayout>\n );\n }\n\n if (courseQuery.isError) {\n return (\n <PageLayout title=\"Course builder\">\n <ErrorView message=\"We couldn't load this course.\" onRetry={() => courseQuery.refetch()} />\n </PageLayout>\n );\n }\n\n if (!courseId || !course) {\n return (\n <PageLayout title=\"Course builder\">\n <EmptyState\n illustration=\"empty-search\"\n title=\"Course not found\"\n description=\"This course may have been archived or removed.\"\n action={<Button onClick={() => navigateTo('authoring')}>Back to authoring</Button>}\n />\n </PageLayout>\n );\n }\n\n const canPublish = course.status === 'DRAFT' || course.status === 'IN_REVIEW';\n const canArchive = course.status !== 'ARCHIVED';\n\n const tabs: TabItem<BuilderTab>[] = [\n { value: 'details', label: 'Details', icon: <FileText className=\"h-4 w-4\" /> },\n {\n value: 'curriculum',\n label: 'Curriculum',\n icon: <BookOpen className=\"h-4 w-4\" />,\n count: course.modules?.length ?? 0,\n },\n { value: 'assessments', label: 'Assessments', icon: <ListChecks className=\"h-4 w-4\" /> },\n { value: 'discussion', label: 'Discussion', icon: <MessageSquare className=\"h-4 w-4\" /> },\n ];\n\n return (\n <PageLayout\n title={\n <span className=\"flex flex-wrap items-center gap-3\">\n <span className=\"min-w-0\">{course.title}</span>\n <StatusBadge status={course.status} />\n <StatusBadge status={course.level} />\n </span>\n }\n description={course.summary ?? undefined}\n actions={\n <>\n <Button variant=\"ghost\" onClick={() => navigateTo('authoring')}>\n <ArrowLeft className=\"mr-1.5 h-4 w-4\" /> Back\n </Button>\n {canArchive && (\n <Button variant=\"outline\" onClick={() => setConfirmArchive(true)}>\n <Archive className=\"mr-1.5 h-4 w-4\" /> Archive\n </Button>\n )}\n {canPublish && (\n <Button onClick={handlePublish} disabled={publishCourse.isPending}>\n <Send className=\"mr-1.5 h-4 w-4\" />\n {publishCourse.isPending ? 'Publishing…' : 'Publish'}\n </Button>\n )}\n </>\n }\n >\n <div className=\"flex flex-col gap-5\">\n {actionError && (\n <p className=\"rounded-lg border border-status-error-bg-subtle bg-status-error-bg-subtle px-3 py-2 text-sm text-status-error-text\">\n {actionError}\n </p>\n )}\n\n <Tabs items={tabs} value={tab} onChange={setTab} />\n\n {tab === 'details' && <DetailsTab key={course.id} course={course} />}\n {tab === 'curriculum' && <CurriculumTab course={course} />}\n {tab === 'assessments' && <AssessmentsTab courseId={courseId} />}\n {tab === 'discussion' && (\n <BuiDiscussion entityType=\"buildmyiq-course\" entityId={courseId} />\n )}\n </div>\n\n {confirmArchive && (\n <Modal\n open\n onClose={() => setConfirmArchive(false)}\n title=\"Archive this course?\"\n description=\"Archived courses are hidden from the catalog and cannot be enrolled in.\"\n footer={\n <>\n <Button\n variant=\"ghost\"\n onClick={() => setConfirmArchive(false)}\n disabled={archiveCourse.isPending}\n >\n Cancel\n </Button>\n <Button\n variant=\"destructive\"\n onClick={handleArchive}\n disabled={archiveCourse.isPending}\n >\n {archiveCourse.isPending ? 'Archiving…' : 'Archive course'}\n </Button>\n </>\n }\n >\n <p className=\"text-sm text-text-secondary\">\n You can still find it in Authoring under the Archived filter.\n </p>\n </Modal>\n )}\n </PageLayout>\n );\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAuFA,IAAM,KAAyE;CAC7E,SAAS;EAAE,MAAM;EAAU,MAAM;EAAoC;CACrE,OAAO;EAAE,MAAM;EAAY,MAAM;EAAmD;CACpF,MAAM;EAAE,MAAM;EAAY,MAAM;EAAwD;CACxF,YAAY;EACV,MAAM;EACN,MAAM;EACP;CACD,aAAa;EACX,MAAM;EACN,MAAM;EACP;CACD,cAAc;EACZ,MAAM;EACN,MAAM;EACP;CACF,EAOK,MAAyC,EAAE,gBAAa;CAC5D,IAAM,IAAe,GAAoB,EACnC,IAAc,GAAc,EAC5B,IAAS,EAAY,QAAQ,EAAE,EAE/B,CAAC,GAAO,KAAY,EAAS,EAAO,MAAM,EAC1C,CAAC,GAAS,KAAc,EAAS,EAAO,WAAW,GAAG,EACtD,CAAC,GAAa,KAAkB,EAAS,EAAO,eAAe,GAAG,EAClE,CAAC,GAAO,KAAY,EAAyB,EAAO,MAAM,EAC1D,CAAC,GAAU,KAAe,EAAS,EAAO,SAAS,EACnD,CAAC,GAAgB,KAAqB,EAAS,OAAO,EAAO,kBAAkB,EAAE,CAAC,EAClF,CAAC,GAAS,KAAc,EAAiB,EAAO,WAAA,WAAoB,EACpE,CAAC,GAAU,KAAe,EAAS,EAAO,SAAS,EACnD,CAAC,GAAW,KAAgB,EAAwB,KAAK;AAqB/D,QACE,kBAAC,OAAD;EAAK,WAAU;YACb,kBAAC,GAAD;GACE,OAAM;GACN,aAAY;GACZ,QACE,kBAAC,GAAD;IAAQ,SAzBG,YAAY;AAC7B,OAAa,KAAK;KAClB,IAAM,IAA8B;MAClC,OAAO,EAAM,MAAM,IAAI,EAAO;MAC9B,SAAS,EAAQ,MAAM,IAAI,KAAA;MAC3B,aAAa,EAAY,MAAM,IAAI,KAAA;MACnC;MACA,UAAU,EAAS,MAAM,IAAI;MAC7B,gBAAgB,EAAe,MAAM,GAAG,OAAO,EAAe,GAAG;MACjE,SAAS,MAAA,aAAiC,KAAA,IAAV;MAChC;MACD;AACD,SAAI;AACF,YAAM,EAAa,YAAY;OAAE,IAAI,EAAO;OAAI;OAAO,CAAC;cACjD,GAAG;AACV,QAAa,aAAa,QAAQ,EAAE,UAAU,0BAA0B;;;IAUvC,UAAU,EAAa;cACjD,EAAa,YAAY,YAAY;IAC/B,CAAA;aAGX,kBAAC,GAAD;IAAW,WAAU;IAAQ,WAAU;cACrC,kBAAC,OAAD;KAAK,WAAU;eAAf;MACE,kBAAC,GAAD;OAAO,OAAM;OAAQ,SAAQ;OAAe,UAAA;iBAC1C,kBAAC,GAAD;QAAO,IAAG;QAAe,OAAO;QAAO,WAAW,MAAM,EAAS,EAAE,OAAO,MAAM;QAAI,CAAA;OAC9E,CAAA;MACR,kBAAC,GAAD;OAAO,OAAM;OAAU,SAAQ;iBAC7B,kBAAC,GAAD;QACE,IAAG;QACH,MAAM;QACN,OAAO;QACP,WAAW,MAAM,EAAW,EAAE,OAAO,MAAM;QAC3C,aAAY;QACZ,CAAA;OACI,CAAA;MACR,kBAAC,GAAD;OAAO,OAAM;OAAc,SAAQ;iBACjC,kBAAC,GAAD;QACE,IAAG;QACH,MAAM;QACN,OAAO;QACP,WAAW,MAAM,EAAe,EAAE,OAAO,MAAM;QAC/C,aAAY;QACZ,CAAA;OACI,CAAA;MACR,kBAAC,OAAD;OAAK,WAAU;iBAAf;QACE,kBAAC,GAAD;SAAO,OAAM;mBACX,kBAAC,GAAD;UACE,OAAO;UACP,WAAW,MAAM,EAAS,EAAoB;UAC9C,SAAS,EAAc,KAAK,OAAO;WAAE,OAAO;WAAG,OAAO,EAAgB,EAAE;WAAE,EAAE;UAC5E,CAAA;SACI,CAAA;QACR,kBAAC,GAAD;SAAO,OAAM;SAAW,SAAQ;mBAC9B,kBAAC,GAAD;UACE,IAAG;UACH,OAAO;UACP,WAAW,MAAM,EAAY,EAAE,OAAO,MAAM;UAC5C,aAAY;UACZ,CAAA;SACI,CAAA;QACR,kBAAC,GAAD;SAAO,OAAM;SAAkB,SAAQ;mBACrC,kBAAC,GAAD;UACE,IAAG;UACH,MAAK;UACL,KAAK;UACL,OAAO;UACP,WAAW,MAAM,EAAkB,EAAE,OAAO,MAAM;UAClD,CAAA;SACI,CAAA;QACR,kBAAC,GAAD;SAAO,OAAM;mBACX,kBAAC,GAAD;UACE,OAAO;UACP,UAAU;UACV,SAAS,CACP;WAAE,OAAO;WAAU,OAAO,EAAY,YAAY,aAAa;WAAY,EAC3E,GAAG,EAAO,KAAK,OAAO;WAAE,OAAO,EAAE;WAAI,OAAO,EAAE;WAAM,EAAE,CACvD;UACD,CAAA;SACI,CAAA;QACJ;;MACN,kBAAC,SAAD;OAAO,WAAU;iBAAjB,CACE,kBAAC,GAAD;QAAQ,SAAS;QAAU,iBAAiB;QAAe,CAAA,EAAA,kCAErD;;MACP,KAAa,kBAAC,KAAD;OAAG,WAAU;iBAAkC;OAAc,CAAA;MACvE;;IACI,CAAA;GACJ,CAAA;EACN,CAAA;GAQJ,MAAmF,EACvF,aACA,aACA,iBACI;CACJ,IAAM,IAAe,GAAoB,EACnC,CAAC,GAAO,KAAY,EAAS,GAAG,EAChC,CAAC,GAAa,KAAkB,EAAS,GAAG,EAC5C,CAAC,GAAO,KAAY,EAAwB,KAAK,EAEjD,IAAY,EAAM,MAAM,CAAC,SAAS,KAAK,CAAC,EAAa;AAmB3D,QACE,kBAAC,GAAD;EACE,MAAA;EACS;EACT,OAAM;EACN,QACE,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,GAAD;GAAQ,SAAQ;GAAQ,SAAS;GAAS,UAAU,EAAa;aAAW;GAEnE,CAAA,EACT,kBAAC,GAAD;GAAQ,SA3BK,YAAY;AAC/B,QAAI,CAAC,EAAW;AAChB,MAAS,KAAK;IACd,IAAM,IAA8B;KAClC;KACA,OAAO,EAAM,MAAM;KACnB,aAAa,EAAY,MAAM,IAAI,KAAA;KACnC;KACD;AACD,QAAI;AAEF,KADA,MAAM,EAAa,YAAY,EAAM,EACrC,GAAS;aACF,GAAG;AACV,OAAS,aAAa,QAAQ,EAAE,UAAU,4BAA4B;;;GAcnC,UAAU,CAAC;aACvC,EAAa,YAAY,YAAY;GAC/B,CAAA,CACR,EAAA,CAAA;YAGL,kBAAC,OAAD;GAAK,WAAU;aAAf;IACE,kBAAC,GAAD;KAAO,OAAM;KAAe,SAAQ;KAAe,UAAA;eACjD,kBAAC,GAAD;MACE,IAAG;MACH,OAAO;MACP,WAAW,MAAM,EAAS,EAAE,OAAO,MAAM;MACzC,aAAY;MACZ,CAAA;KACI,CAAA;IACR,kBAAC,GAAD;KAAO,OAAM;KAAc,SAAQ;eACjC,kBAAC,GAAD;MACE,IAAG;MACH,MAAM;MACN,OAAO;MACP,WAAW,MAAM,EAAe,EAAE,OAAO,MAAM;MAC/C,CAAA;KACI,CAAA;IACP,KAAS,kBAAC,KAAD;KAAG,WAAU;eAAkC;KAAU,CAAA;IAC/D;;EACA,CAAA;GAIN,MAKA,EAAE,aAAU,aAAU,aAAU,iBAAc;CAClD,IAAM,IAAe,GAAoB,EACnC,CAAC,GAAO,KAAY,EAAS,GAAG,EAChC,CAAC,GAAM,KAAW,EAAwB,UAAU,EACpD,CAAC,GAAS,KAAc,EAAS,GAAG,EACpC,CAAC,GAAa,KAAkB,EAAS,GAAG,EAC5C,CAAC,GAAW,KAAgB,EAAS,GAAM,EAC3C,CAAC,GAAO,KAAY,EAAiC,KAAK,EAC1D,CAAC,GAAO,KAAY,EAAwB,KAAK,EAEjD,IAAY,EAAM,MAAM,CAAC,SAAS,KAAK,CAAC,EAAa;AAwB3D,QACE,kBAAC,GAAD;EACE,MAAA;EACS;EACT,OAAM;EACN,WAAU;EACV,QACE,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,GAAD;GAAQ,SAAQ;GAAQ,SAAS;GAAS,UAAU,EAAa;aAAW;GAEnE,CAAA,EACT,kBAAC,GAAD;GAAQ,SAjCK,YAAY;AAC/B,QAAI,CAAC,EAAW;AAChB,MAAS,KAAK;IACd,IAAM,IAA8B;KAClC;KACA;KACA,OAAO,EAAM,MAAM;KACnB;KACA,SAAS,EAAQ,MAAM,IAAI,KAAA;KAC3B,aAAa,EAAY,MAAM,GAAG,OAAO,EAAY,GAAG,KAAA;KACxD;KACA;KACA,aAAa,GAAO;KACrB;AACD,QAAI;AAEF,KADA,MAAM,EAAa,YAAY,EAAM,EACrC,GAAS;aACF,GAAG;AACV,OAAS,aAAa,QAAQ,EAAE,UAAU,4BAA4B;;;GAenC,UAAU,CAAC;aACvC,EAAa,YAAY,YAAY;GAC/B,CAAA,CACR,EAAA,CAAA;YAGL,kBAAC,OAAD;GAAK,WAAU;aAAf;IACE,kBAAC,GAAD;KAAO,OAAM;KAAe,SAAQ;KAAe,UAAA;eACjD,kBAAC,GAAD;MACE,IAAG;MACH,OAAO;MACP,WAAW,MAAM,EAAS,EAAE,OAAO,MAAM;MACzC,aAAY;MACZ,CAAA;KACI,CAAA;IACR,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,GAAD;MAAO,OAAM;gBACX,kBAAC,GAAD;OACE,OAAO;OACP,WAAW,MAAM,EAAQ,EAAmB;OAC5C,SAAS,EAAa,KAAK,OAAO;QAAE,OAAO;QAAG,OAAO,EAAgB,EAAE;QAAE,EAAE;OAC3E,CAAA;MACI,CAAA,EACR,kBAAC,GAAD;MAAO,OAAM;MAAqB,SAAQ;gBACxC,kBAAC,GAAD;OACE,IAAG;OACH,MAAK;OACL,KAAK;OACL,OAAO;OACP,WAAW,MAAM,EAAe,EAAE,OAAO,MAAM;OAC/C,aAAY;OACZ,CAAA;MACI,CAAA,CACJ;;IACN,kBAAC,GAAD;KACE,OAAM;KACN,SAAQ;KACR,MAAK;eAEL,kBAAC,GAAD;MACE,IAAG;MACH,MAAM;MACN,OAAO;MACP,WAAW,MAAM,EAAW,EAAE,OAAO,MAAM;MAC3C,CAAA;KACI,CAAA;IACR,kBAAC,GAAD;KACE,IAAG;KACH,OAAM;KACN,OAAO;KACP,UAAU;KACV,MAAK;KACL,CAAA;IACF,kBAAC,SAAD;KAAO,WAAU;eAAjB,CACE,kBAAC,GAAD;MAAQ,SAAS;MAAW,iBAAiB;MAAgB,CAAA,EAAA,sBAEvD;;IACP,KAAS,kBAAC,KAAD;KAAG,WAAU;eAAkC;KAAU,CAAA;IAC/D;;EACA,CAAA;GAIN,MAAwC,EAAE,gBAAa;CAC3D,IAAM,IAAO,GAAY,EAAO,OAC1B,IAAO,EAAK;AAClB,QACE,kBAAC,MAAD;EAAI,WAAU;YAAd;GACE,kBAAC,QAAD;IACE,WAAW,EAAG,gEAAgE,EAAK,KAAK;IACxF,eAAY;cAEZ,kBAAC,GAAD,EAAM,WAAU,WAAY,CAAA;IACvB,CAAA;GACP,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,KAAD;KAAG,WAAU;eAAkD,EAAO;KAAU,CAAA,EAChF,kBAAC,KAAD;KAAG,WAAU;eAA8B,EAAgB,EAAO,KAAK;KAAK,CAAA,CACxE;;GACL,EAAO,cAAc,KACpB,kBAAC,QAAD;IAAM,WAAU;cAAhB;KACE,kBAAC,GAAD,EAAO,WAAU,eAAgB,CAAA;;KAAE,EAAe,EAAO,YAAY;KAChE;;GAER,EAAO,aACN,kBAAC,QAAD;IAAM,WAAU;cAAwG;IAEjH,CAAA;GAEN;;GAIH,KAAiF,EACrF,WACA,UACA,qBACI;CACJ,IAAM,IAAU,CAAC,GAAI,EAAO,WAAW,EAAE,CAAE,CAAC,MAAM,GAAG,MAAM,EAAE,WAAW,EAAE,SAAS,EAC7E,IAAW,EAAQ,QAAQ,GAAK,MAAM,KAAO,EAAE,eAAe,IAAI,EAAE;AAC1E,QACE,kBAAC,GAAD;EAAW,WAAU;EAAQ,WAAU;YAAvC;GACE,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,QAAD;MACE,WAAU;MACV,eAAY;gBAEX,IAAQ;MACJ,CAAA;KACP,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,MAAD;OAAI,WAAU;iBAAd,CACE,kBAAC,QAAD;QAAM,WAAU;kBAAhB;SAAqC;SAAQ,IAAQ;SAAE;SAAU;WAChE,EAAO,MACL;UACL,kBAAC,KAAD;OAAG,WAAU;iBAAb;QACG,EAAQ;QAAO;QAAE,EAAQ,WAAW,IAAI,WAAW;QACnD,IAAW,KAAK,MAAM,EAAe,EAAS;QAC7C;SACA;;KACN,kBAAC,GAAD;MAAQ,SAAQ;MAAU,MAAK;MAAK,SAAS;MAAa,WAAU;gBAApE,CACE,kBAAC,GAAD,EAAM,WAAU,kBAAmB,CAAA,EAAA,cAC5B;;KACL;;GACL,EAAO,eACN,kBAAC,KAAD;IAAG,WAAU;cACV,EAAO;IACN,CAAA;GAEN,kBAAC,OAAD;IAAK,WAAU;cACZ,EAAQ,WAAW,IAClB,kBAAC,UAAD;KACE,MAAK;KACL,SAAS;KACT,WAAU;eAHZ,CAKE,kBAAC,GAAD,EAAM,WAAU,WAAY,CAAA,EAAA,wBACrB;SAET,kBAAC,MAAD;KAAI,WAAU;eACX,EAAQ,KAAK,MACZ,kBAAC,IAAD,EAAmC,WAAU,EAA7B,EAAO,GAAsB,CAC7C;KACC,CAAA;IAEH,CAAA;GACI;;GAIV,KAAoE,EACxE,SACA,UACA,eAEA,kBAAC,OAAD;CAAK,WAAU;WAAf;EACE,kBAAC,QAAD;GAAM,WAAU;GAA8B,eAAY;aACvD;GACI,CAAA;EACP,kBAAC,QAAD;GAAM,WAAU;aAA+B;GAAa,CAAA;EAC5D,kBAAC,QAAD;GAAM,WAAU;aACb;GACI,CAAA;EACH;IAGF,MAA4C,EAAE,gBAAa;CAC/D,IAAM,IAAW,EAAO,IAClB,IAAe,EAAc,EAAS,EACtC,IAAe,GAAoB,EACnC,CAAC,GAAe,KAAoB,EAAS,GAAM,EACnD,CAAC,GAAgB,KAAqB,EAAwB,KAAK,EACnE,CAAC,GAAW,KAAgB,EAAiC,KAAK,EAClE,CAAC,GAAY,KAAiB,EAAwB,KAAK,EAE3D,IAAU,CAAC,GAAI,EAAa,QAAQ,EAAE,CAAE,CAAC,MAAM,GAAG,MAAM,EAAE,WAAW,EAAE,SAAS,EAChF,IAAe,EAAQ,MAAM,MAAM,EAAE,OAAO,EAAe,IAAI;AAarE,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf;GAEE,kBAAC,OAAD;IAAK,WAAU;cACb,kBAAC,GAAD;KACE,OAAM;KACN,aAAY;KACZ,QACE,kBAAC,GAAD;MAAQ,eAAe,EAAiB,GAAK;gBAA7C,CACE,kBAAC,GAAD,EAAM,WAAU,kBAAmB,CAAA,EAAA,cAC5B;;eAGV,EAAa,YACZ,kBAAC,GAAD,EAAa,OAAM,uBAAwB,CAAA,GACzC,EAAa,UACf,kBAAC,GAAD;MACE,SAAQ;MACR,eAAe,EAAa,SAAS;MACrC,CAAA,GACA,EAAQ,WAAW,IACrB,kBAAC,GAAD;MACE,cAAa;MACb,OAAM;MACN,aAAY;MACZ,QACE,kBAAC,GAAD;OAAQ,eAAe,EAAiB,GAAK;iBAA7C,CACE,kBAAC,GAAD,EAAM,WAAU,kBAAmB,CAAA,EAAA,cAC5B;;MAEX,CAAA,GAEF,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACG,EAAQ,KAAK,GAAK,MACjB,kBAAC,GAAD;OAEE,QAAQ;OACR,OAAO;OACP,mBAAmB,EAAkB,EAAI,GAAG;OAC5C,EAJK,EAAI,GAIT,CACF,EACF,kBAAC,UAAD;OACE,MAAK;OACL,eAAe,EAAiB,GAAK;OACrC,WAAU;iBAHZ,CAKE,kBAAC,GAAD,EAAM,WAAU,WAAY,CAAA,EAAA,cACrB;SACL;;KAEA,CAAA;IACN,CAAA;GAGN,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,GAAD;KAAW,WAAU;KAAQ,WAAU;eAAvC;MACE,kBAAC,MAAD;OAAI,WAAU;iBAA4C;OAAiB,CAAA;MAC3E,kBAAC,KAAD;OAAG,WAAU;iBAAqC;OAE9C,CAAA;MACJ,kBAAC,OAAD;OAAK,WAAU;iBACZ,EAAO,OAAO,cACb,kBAAC,OAAD;QAAK,KAAK,EAAO,MAAM;QAAa,KAAI;QAAG,WAAU;QAA+B,CAAA,GAEpF,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,IAAD,EAAW,WAAU,WAAY,CAAA,EAAA,gBAC7B;;OAEJ,CAAA;MACN,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,GAAD;QACE,IAAG;QACH,OAAO,EAAO,OAAO,cAAc,kBAAkB;QACrD,OAAO;QACP,UArFc,OAAO,MAAiC;AAChE,eAAa,EAAK,EACb,GACL;YAAc,KAAK;AACnB,cAAI;AACF,iBAAM,EAAa,YAAY;YAAE,IAAI;YAAU,OAAO,EAAE,aAAa,EAAK,QAAQ;YAAE,CAAC;mBAC9E,GAAG;AACV,aAAc,aAAa,QAAQ,EAAE,UAAU,8BAA8B;;;;QA+ErE,QAAO;QACP,MAAK;QACL,CAAA,EACD,KAAc,kBAAC,KAAD;QAAG,WAAU;kBAAuC;QAAe,CAAA,CAC9E;;MACI;QAEZ,kBAAC,GAAD;KAAW,WAAU;KAAQ,WAAU;eAAvC,CACE,kBAAC,MAAD;MAAI,WAAU;gBAA4C;MAAmB,CAAA,EAC7E,kBAAC,MAAD;MAAI,WAAU;gBAAd;OACE,kBAAC,GAAD;QAAW,MAAM,kBAAC,IAAD,EAAO,WAAU,WAAY,CAAA;QAAE,OAAM;QAAO,OAAO,EAAO;QAAQ,CAAA;OACnF,kBAAC,GAAD;QACE,MAAM,kBAAC,IAAD,EAAO,WAAU,WAAY,CAAA;QACnC,OAAM;QACN,OAAO,EAAO,WAAW,EAAO,SAAS,aAAa,GAAG;QACzD,CAAA;OACF,kBAAC,GAAD;QACE,MAAM,kBAAC,GAAD,EAAW,WAAU,WAAY,CAAA;QACvC,OAAM;QACN,OAAO,EAAgB,EAAO,MAAM;QACpC,CAAA;OACF,kBAAC,GAAD;QACE,MAAM,kBAAC,GAAD,EAAK,WAAU,WAAY,CAAA;QACjC,OAAM;QACN,OAAO,EAAO,WAAW,WAAW;QACpC,CAAA;OACF,kBAAC,GAAD;QACE,MAAM,kBAAC,GAAD,EAAU,WAAU,WAAY,CAAA;QACtC,OAAM;QACN,OAAO,EAAW,EAAO,UAAU;QACnC,CAAA;OACC;QACK;OACR;;GAEL,KACC,kBAAC,IAAD;IACY;IACV,UAAU,EAAQ;IAClB,eAAe,EAAiB,GAAM;IACtC,CAAA;GAEH,KACC,kBAAC,IAAD;IACY;IACV,UAAU;IACV,UAAU,GAAc,SAAS,UAAU;IAC3C,eAAe,EAAkB,KAAK;IACtC,CAAA;GAEA;;GAQJ,MAA4C,EAAE,kBAAe;CACjE,IAAM,EAAE,kBAAe,GAAc,EAC/B,IAAmB,EAAkB,EAAS,EAC9C,IAAc,EAAiB,QAAQ,EAAE;AAE/C,QACE,kBAAC,GAAD;EACE,OAAM;EACN,aAAY;YAEX,EAAiB,YAChB,kBAAC,GAAD,EAAa,OAAM,wBAAyB,CAAA,GAC1C,EAAiB,UACnB,kBAAC,GAAD;GACE,SAAQ;GACR,eAAe,EAAiB,SAAS;GACzC,CAAA,GACA,EAAY,WAAW,IACzB,kBAAC,GAAD;GACE,cAAa;GACb,OAAM;GACN,aAAY;GACZ,QACE,kBAAC,GAAD;IAAQ,SAAQ;IAAU,eAAe,EAAW,cAAc;cAAE;IAE3D,CAAA;GAEX,CAAA,GAEF,kBAAC,OAAD;GAAK,WAAU;aACZ,EAAY,KAAK,MAChB,kBAAC,GAAD;IAEE,WAAU;IACV,WAAU;IACV,eAAe,EAAW,eAAe,EAAE,KAAK;cAJlD;KAME,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,MAAD;OAAI,WAAU;iBAA2C,EAAE;OAAW,CAAA,EACtE,kBAAC,GAAD,EAAa,QAAQ,EAAE,QAAU,CAAA,CAC7B;;KACL,EAAE,eACD,kBAAC,KAAD;MAAG,WAAU;gBAA4C,EAAE;MAAgB,CAAA;KAE7E,kBAAC,OAAD;MAAK,WAAU;gBAAf;OACE,kBAAC,QAAD;QAAM,WAAU;kBACb,EAAgB,EAAE,KAAK;QACnB,CAAA;OACP,kBAAC,QAAD,EAAA,UAAA;QAAM;QAAQ,EAAE,gBAAgB;QAAE;QAAQ,EAAA,CAAA;OACzC,EAAE,eAAe,KAChB,kBAAC,QAAD;QAAM,WAAU;kBAAhB;SACE,kBAAC,GAAD,EAAO,WAAU,eAAgB,CAAA;;SAAE,EAAe,EAAE,aAAa;SAC5D;;OAEL;;KACI;MAvBL,EAAE,GAuBG,CACZ;GACE,CAAA;EAEA,CAAA;GAQD,WAA8B;CACzC,IAAM,EAAE,gBAAa,GAAiC,EAChD,EAAE,kBAAe,GAAc,EAC/B,CAAC,GAAK,KAAU,EAAqB,UAAU,EAC/C,IAAc,EAAa,EAAS,EACpC,IAAS,EAAY,QAAQ,MAE7B,IAAgB,GAAqB,EACrC,IAAgB,GAAqB,EACrC,CAAC,GAAa,KAAkB,EAAwB,KAAK,EAC7D,CAAC,GAAgB,KAAqB,EAAS,GAAM,EAErD,IAAgB,YAAY;AAC3B,SACL;KAAe,KAAK;AACpB,OAAI;AACF,UAAM,EAAc,YAAY,EAAO,GAAG;YACnC,GAAG;AACV,MAAe,aAAa,QAAQ,EAAE,UAAU,gCAAgC;;;IAI9E,IAAgB,YAAY;AAC3B,SACL;KAAe,KAAK;AACpB,OAAI;AAEF,IADA,MAAM,EAAc,YAAY,EAAO,GAAG,EAC1C,EAAkB,GAAM;YACjB,GAAG;AACV,MAAe,aAAa,QAAQ,EAAE,UAAU,gCAAgC;;;;AAIpF,KAAI,EAAY,UACd,QACE,kBAAC,GAAD;EAAY,OAAM;YAChB,kBAAC,GAAD,EAAa,OAAM,mBAAoB,CAAA;EAC5B,CAAA;AAIjB,KAAI,EAAY,QACd,QACE,kBAAC,GAAD;EAAY,OAAM;YAChB,kBAAC,GAAD;GAAW,SAAQ;GAAgC,eAAe,EAAY,SAAS;GAAI,CAAA;EAChF,CAAA;AAIjB,KAAI,CAAC,KAAY,CAAC,EAChB,QACE,kBAAC,GAAD;EAAY,OAAM;YAChB,kBAAC,GAAD;GACE,cAAa;GACb,OAAM;GACN,aAAY;GACZ,QAAQ,kBAAC,GAAD;IAAQ,eAAe,EAAW,YAAY;cAAE;IAA0B,CAAA;GAClF,CAAA;EACS,CAAA;CAIjB,IAAM,IAAa,EAAO,WAAW,WAAW,EAAO,WAAW,aAC5D,IAAa,EAAO,WAAW,YAE/B,IAA8B;EAClC;GAAE,OAAO;GAAW,OAAO;GAAW,MAAM,kBAAC,GAAD,EAAU,WAAU,WAAY,CAAA;GAAE;EAC9E;GACE,OAAO;GACP,OAAO;GACP,MAAM,kBAAC,IAAD,EAAU,WAAU,WAAY,CAAA;GACtC,OAAO,EAAO,SAAS,UAAU;GAClC;EACD;GAAE,OAAO;GAAe,OAAO;GAAe,MAAM,kBAAC,GAAD,EAAY,WAAU,WAAY,CAAA;GAAE;EACxF;GAAE,OAAO;GAAc,OAAO;GAAc,MAAM,kBAAC,IAAD,EAAe,WAAU,WAAY,CAAA;GAAE;EAC1F;AAED,QACE,kBAAC,GAAD;EACE,OACE,kBAAC,QAAD;GAAM,WAAU;aAAhB;IACE,kBAAC,QAAD;KAAM,WAAU;eAAW,EAAO;KAAa,CAAA;IAC/C,kBAAC,GAAD,EAAa,QAAQ,EAAO,QAAU,CAAA;IACtC,kBAAC,GAAD,EAAa,QAAQ,EAAO,OAAS,CAAA;IAChC;;EAET,aAAa,EAAO,WAAW,KAAA;EAC/B,SACE,kBAAA,GAAA,EAAA,UAAA;GACE,kBAAC,GAAD;IAAQ,SAAQ;IAAQ,eAAe,EAAW,YAAY;cAA9D,CACE,kBAAC,GAAD,EAAW,WAAU,kBAAmB,CAAA,EAAA,QACjC;;GACR,KACC,kBAAC,GAAD;IAAQ,SAAQ;IAAU,eAAe,EAAkB,GAAK;cAAhE,CACE,kBAAC,GAAD,EAAS,WAAU,kBAAmB,CAAA,EAAA,WAC/B;;GAEV,KACC,kBAAC,GAAD;IAAQ,SAAS;IAAe,UAAU,EAAc;cAAxD,CACE,kBAAC,GAAD,EAAM,WAAU,kBAAmB,CAAA,EAClC,EAAc,YAAY,gBAAgB,UACpC;;GAEV,EAAA,CAAA;YAzBP,CA4BE,kBAAC,OAAD;GAAK,WAAU;aAAf;IACG,KACC,kBAAC,KAAD;KAAG,WAAU;eACV;KACC,CAAA;IAGN,kBAAC,GAAD;KAAM,OAAO;KAAM,OAAO;KAAK,UAAU;KAAU,CAAA;IAElD,MAAQ,aAAa,kBAAC,IAAD,EAAoC,WAAU,EAA7B,EAAO,GAAsB;IACnE,MAAQ,gBAAgB,kBAAC,IAAD,EAAuB,WAAU,CAAA;IACzD,MAAQ,iBAAiB,kBAAC,IAAD,EAA0B,aAAY,CAAA;IAC/D,MAAQ,gBACP,kBAAC,GAAD;KAAe,YAAW;KAAmB,UAAU;KAAY,CAAA;IAEjE;MAEL,KACC,kBAAC,GAAD;GACE,MAAA;GACA,eAAe,EAAkB,GAAM;GACvC,OAAM;GACN,aAAY;GACZ,QACE,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,GAAD;IACE,SAAQ;IACR,eAAe,EAAkB,GAAM;IACvC,UAAU,EAAc;cACzB;IAEQ,CAAA,EACT,kBAAC,GAAD;IACE,SAAQ;IACR,SAAS;IACT,UAAU,EAAc;cAEvB,EAAc,YAAY,eAAe;IACnC,CAAA,CACR,EAAA,CAAA;aAGL,kBAAC,KAAD;IAAG,WAAU;cAA8B;IAEvC,CAAA;GACE,CAAA,CAEC"}
|
|
1
|
+
{"version":3,"file":"CourseBuilderPage.js","names":[],"sources":["../../../../src/buildmyiq/pages/authoring/CourseBuilderPage.tsx"],"sourcesContent":["/**\n * CourseBuilderPage — the per-course authoring workspace.\n * @module buildmyiq/pages/authoring\n *\n * Loads one course (`useBuiCourse`) and exposes four tabs:\n * - Details — editable course meta (`useBuiUpdateCourse`).\n * - Curriculum — the module/lesson builder (`useBuiModules`, `useBuiCreateModule`,\n * `useBuiCreateLesson`) plus a right rail with the cover-image dropzone\n * (EvidenceUpload → coverFileId) and a live course-details summary.\n * - Assessments — course assessments (`useBuiAssessments`) linking out to the\n * assessment editor.\n * - Discussion — the shared wspace-conversations thread (`BuiDiscussion`).\n *\n * Publish (`useBuiPublishCourse`) and Archive (`useBuiArchiveCourse`) live in the\n * page header so they are reachable from every tab. Every list carries loading /\n * error / empty states; all data is live `bui*` data — no fabrication.\n */\n\nimport type { FC, ReactNode } from 'react';\nimport { useState } from 'react';\nimport { useParams } from 'react-router-dom';\nimport {\n Archive,\n ArrowLeft,\n BarChart3,\n BookOpen,\n Calendar,\n ClipboardList,\n Clock,\n Eye,\n FileText,\n Globe,\n Image as ImageIcon,\n Link2,\n ListChecks,\n MessageSquare,\n MousePointerClick,\n PlayCircle,\n Plus,\n Radio,\n Send,\n type LucideIcon,\n} from 'lucide-react';\nimport { Button, GlassCard, Input, Switch, Textarea } from '@burdenoff/fe-libs/ui';\nimport {\n PageLayout,\n Tabs,\n type TabItem,\n StatusBadge,\n EmptyState,\n LoadingView,\n ErrorView,\n Section,\n EvidenceUpload,\n BuiDiscussion,\n} from '../../components';\nimport { Modal, Field, SelectField, NO_SKILL } from './_forms';\nimport { useBuildmyiq } from '../../context';\nimport {\n useBuiCourse,\n useBuiUpdateCourse,\n useBuiPublishCourse,\n useBuiArchiveCourse,\n useBuiModules,\n useBuiCreateModule,\n useBuiCreateLesson,\n useBuiAssessments,\n useBuiSkills,\n type UploadedBuiFile,\n} from '../../hooks';\nimport { COURSE_LEVELS, LESSON_TYPES } from '../../types';\nimport type {\n BuiCourse,\n BuiCourseLevel,\n BuiCreateLessonInput,\n BuiCreateModuleInput,\n BuiLesson,\n BuiLessonType,\n BuiModule,\n BuiUpdateCourseInput,\n} from '../../types';\nimport { cn } from '../../utils/cn';\nimport { formatDate, formatDuration, formatEnumLabel } from '../../utils/formatters';\nimport { BuiPermissionButton } from '../../components/BuiPermissionButton';\n\ntype BuilderTab = 'details' | 'curriculum' | 'assessments' | 'discussion';\n\n/** Per-lesson-type icon + theme-safe tint (semantic tokens only). */\nconst LESSON_META: Record<BuiLessonType, { icon: LucideIcon; tint: string }> = {\n ARTICLE: { icon: FileText, tint: 'bg-bg-sunken text-text-secondary' },\n VIDEO: { icon: PlayCircle, tint: 'bg-[var(--color-accent-soft)] text-text-primary' },\n QUIZ: { icon: ListChecks, tint: 'bg-status-warning-bg-subtle text-status-warning-text' },\n ASSIGNMENT: {\n icon: ClipboardList,\n tint: 'bg-status-success-bg-subtle text-status-success-text',\n },\n INTERACTIVE: {\n icon: MousePointerClick,\n tint: 'bg-[color-mix(in_srgb,var(--color-accent-teal)_18%,transparent)] text-[var(--color-accent-teal)]',\n },\n LIVE_SESSION: {\n icon: Radio,\n tint: 'bg-[color-mix(in_srgb,var(--color-accent-primary)_16%,transparent)] text-[var(--color-accent-primary)]',\n },\n};\n\n// ============================================================================\n// Details tab — pure metadata form (publish/archive live in the page header,\n// cover upload lives in the Curriculum right rail).\n// ============================================================================\n\nconst DetailsTab: FC<{ course: BuiCourse }> = ({ course }) => {\n const updateCourse = useBuiUpdateCourse();\n const skillsQuery = useBuiSkills();\n const skills = skillsQuery.data ?? [];\n\n const [title, setTitle] = useState(course.title);\n const [summary, setSummary] = useState(course.summary ?? '');\n const [description, setDescription] = useState(course.description ?? '');\n const [level, setLevel] = useState<BuiCourseLevel>(course.level);\n const [language, setLanguage] = useState(course.language);\n const [estimatedHours, setEstimatedHours] = useState(String(course.estimatedHours ?? 0));\n const [skillId, setSkillId] = useState<string>(course.skillId ?? NO_SKILL);\n const [isPublic, setIsPublic] = useState(course.isPublic);\n const [saveError, setSaveError] = useState<string | null>(null);\n\n const handleSave = async () => {\n setSaveError(null);\n const input: BuiUpdateCourseInput = {\n title: title.trim() || course.title,\n summary: summary.trim() || undefined,\n description: description.trim() || undefined,\n level,\n language: language.trim() || 'en',\n estimatedHours: estimatedHours.trim() ? Number(estimatedHours) : 0,\n skillId: skillId !== NO_SKILL ? skillId : undefined,\n isPublic,\n };\n try {\n await updateCourse.mutateAsync({ id: course.id, input });\n } catch (e) {\n setSaveError(e instanceof Error ? e.message : 'Failed to save changes.');\n }\n };\n\n return (\n <div className=\"mx-auto max-w-3xl\">\n <Section\n title=\"Course details\"\n description=\"Metadata learners see in the catalog.\"\n action={\n <BuiPermissionButton resource=\"buildmyiq-course\" action=\"update\" deniedBehavior=\"tooltip\">\n <Button onClick={handleSave} disabled={updateCourse.isPending}>\n {updateCourse.isPending ? 'Saving…' : 'Save changes'}\n </Button>\n </BuiPermissionButton>\n }\n >\n <GlassCard treatment=\"plain\" className=\"p-5\">\n <div className=\"grid gap-4\">\n <Field label=\"Title\" htmlFor=\"detail-title\" required>\n <Input id=\"detail-title\" value={title} onChange={(e) => setTitle(e.target.value)} />\n </Field>\n <Field label=\"Summary\" htmlFor=\"detail-summary\">\n <Textarea\n id=\"detail-summary\"\n rows={2}\n value={summary}\n onChange={(e) => setSummary(e.target.value)}\n placeholder=\"A short description shown on the catalog card.\"\n />\n </Field>\n <Field label=\"Description\" htmlFor=\"detail-description\">\n <Textarea\n id=\"detail-description\"\n rows={5}\n value={description}\n onChange={(e) => setDescription(e.target.value)}\n placeholder=\"The full course overview.\"\n />\n </Field>\n <div className=\"grid gap-4 sm:grid-cols-2\">\n <Field label=\"Level\">\n <SelectField\n value={level}\n onChange={(v) => setLevel(v as BuiCourseLevel)}\n options={COURSE_LEVELS.map((l) => ({ value: l, label: formatEnumLabel(l) }))}\n />\n </Field>\n <Field label=\"Language\" htmlFor=\"detail-language\">\n <Input\n id=\"detail-language\"\n value={language}\n onChange={(e) => setLanguage(e.target.value)}\n placeholder=\"en\"\n />\n </Field>\n <Field label=\"Estimated hours\" htmlFor=\"detail-hours\">\n <Input\n id=\"detail-hours\"\n type=\"number\"\n min={0}\n value={estimatedHours}\n onChange={(e) => setEstimatedHours(e.target.value)}\n />\n </Field>\n <Field label=\"Skill\">\n <SelectField\n value={skillId}\n onChange={setSkillId}\n options={[\n { value: NO_SKILL, label: skillsQuery.isLoading ? 'Loading…' : 'No skill' },\n ...skills.map((s) => ({ value: s.id, label: s.name })),\n ]}\n />\n </Field>\n </div>\n <label className=\"inline-flex cursor-pointer items-center gap-2 text-sm text-text-secondary\">\n <Switch checked={isPublic} onCheckedChange={setIsPublic} />\n Publicly visible in the catalog\n </label>\n {saveError && <p className=\"text-sm text-status-error-text\">{saveError}</p>}\n </div>\n </GlassCard>\n </Section>\n </div>\n );\n};\n\n// ============================================================================\n// Curriculum tab\n// ============================================================================\n\nconst AddModuleModal: FC<{ courseId: string; position: number; onClose: () => void }> = ({\n courseId,\n position,\n onClose,\n}) => {\n const createModule = useBuiCreateModule();\n const [title, setTitle] = useState('');\n const [description, setDescription] = useState('');\n const [error, setError] = useState<string | null>(null);\n\n const canSubmit = title.trim().length > 0 && !createModule.isPending;\n\n const handleSubmit = async () => {\n if (!canSubmit) return;\n setError(null);\n const input: BuiCreateModuleInput = {\n courseId,\n title: title.trim(),\n description: description.trim() || undefined,\n position,\n };\n try {\n await createModule.mutateAsync(input);\n onClose();\n } catch (e) {\n setError(e instanceof Error ? e.message : 'Failed to add the module.');\n }\n };\n\n return (\n <Modal\n open\n onClose={onClose}\n title=\"Add module\"\n footer={\n <>\n <Button variant=\"ghost\" onClick={onClose} disabled={createModule.isPending}>\n Cancel\n </Button>\n <Button onClick={handleSubmit} disabled={!canSubmit}>\n {createModule.isPending ? 'Adding…' : 'Add module'}\n </Button>\n </>\n }\n >\n <div className=\"grid gap-4\">\n <Field label=\"Module title\" htmlFor=\"module-title\" required>\n <Input\n id=\"module-title\"\n value={title}\n onChange={(e) => setTitle(e.target.value)}\n placeholder=\"e.g. Getting started\"\n />\n </Field>\n <Field label=\"Description\" htmlFor=\"module-description\">\n <Textarea\n id=\"module-description\"\n rows={3}\n value={description}\n onChange={(e) => setDescription(e.target.value)}\n />\n </Field>\n {error && <p className=\"text-sm text-status-error-text\">{error}</p>}\n </div>\n </Modal>\n );\n};\n\nconst AddLessonModal: FC<{\n courseId: string;\n moduleId: string;\n position: number;\n onClose: () => void;\n}> = ({ courseId, moduleId, position, onClose }) => {\n const createLesson = useBuiCreateLesson();\n const [title, setTitle] = useState('');\n const [type, setType] = useState<BuiLessonType>('ARTICLE');\n const [content, setContent] = useState('');\n const [durationMin, setDurationMin] = useState('');\n const [isPreview, setIsPreview] = useState(false);\n const [media, setMedia] = useState<UploadedBuiFile | null>(null);\n const [error, setError] = useState<string | null>(null);\n\n const canSubmit = title.trim().length > 0 && !createLesson.isPending;\n\n const handleSubmit = async () => {\n if (!canSubmit) return;\n setError(null);\n const input: BuiCreateLessonInput = {\n courseId,\n moduleId,\n title: title.trim(),\n type,\n content: content.trim() || undefined,\n durationMin: durationMin.trim() ? Number(durationMin) : undefined,\n position,\n isPreview,\n mediaFileId: media?.fileId,\n };\n try {\n await createLesson.mutateAsync(input);\n onClose();\n } catch (e) {\n setError(e instanceof Error ? e.message : 'Failed to add the lesson.');\n }\n };\n\n return (\n <Modal\n open\n onClose={onClose}\n title=\"Add lesson\"\n className=\"sm:max-w-xl\"\n footer={\n <>\n <Button variant=\"ghost\" onClick={onClose} disabled={createLesson.isPending}>\n Cancel\n </Button>\n <Button onClick={handleSubmit} disabled={!canSubmit}>\n {createLesson.isPending ? 'Adding…' : 'Add lesson'}\n </Button>\n </>\n }\n >\n <div className=\"grid gap-4\">\n <Field label=\"Lesson title\" htmlFor=\"lesson-title\" required>\n <Input\n id=\"lesson-title\"\n value={title}\n onChange={(e) => setTitle(e.target.value)}\n placeholder=\"e.g. What is data literacy?\"\n />\n </Field>\n <div className=\"grid gap-4 sm:grid-cols-2\">\n <Field label=\"Type\">\n <SelectField\n value={type}\n onChange={(v) => setType(v as BuiLessonType)}\n options={LESSON_TYPES.map((t) => ({ value: t, label: formatEnumLabel(t) }))}\n />\n </Field>\n <Field label=\"Duration (minutes)\" htmlFor=\"lesson-duration\">\n <Input\n id=\"lesson-duration\"\n type=\"number\"\n min={0}\n value={durationMin}\n onChange={(e) => setDurationMin(e.target.value)}\n placeholder=\"0\"\n />\n </Field>\n </div>\n <Field\n label=\"Content\"\n htmlFor=\"lesson-content\"\n hint=\"Lesson body — markdown or plain text.\"\n >\n <Textarea\n id=\"lesson-content\"\n rows={5}\n value={content}\n onChange={(e) => setContent(e.target.value)}\n />\n </Field>\n <EvidenceUpload\n id=\"lesson-media\"\n label=\"Media (optional)\"\n value={media}\n onChange={setMedia}\n hint=\"Attach a video, audio file or PDF for this lesson.\"\n />\n <label className=\"inline-flex cursor-pointer items-center gap-2 text-sm text-text-secondary\">\n <Switch checked={isPreview} onCheckedChange={setIsPreview} />\n Free preview lesson\n </label>\n {error && <p className=\"text-sm text-status-error-text\">{error}</p>}\n </div>\n </Modal>\n );\n};\n\nconst LessonRow: FC<{ lesson: BuiLesson }> = ({ lesson }) => {\n const meta = LESSON_META[lesson.type];\n const Icon = meta.icon;\n return (\n <li className=\"flex items-center gap-3 rounded-lg border border-border-seam bg-bg-surface px-3 py-2.5\">\n <span\n className={cn('flex h-8 w-8 shrink-0 items-center justify-center rounded-lg', meta.tint)}\n aria-hidden=\"true\"\n >\n <Icon className=\"h-4 w-4\" />\n </span>\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate text-sm font-medium text-text-primary\">{lesson.title}</p>\n <p className=\"text-xs text-text-tertiary\">{formatEnumLabel(lesson.type)}</p>\n </div>\n {lesson.durationMin > 0 && (\n <span className=\"inline-flex shrink-0 items-center gap-1 text-xs text-text-secondary\">\n <Clock className=\"h-3.5 w-3.5\" /> {formatDuration(lesson.durationMin)}\n </span>\n )}\n {lesson.isPreview && (\n <span className=\"shrink-0 rounded-full bg-[var(--color-accent-soft)] px-2 py-0.5 text-xs font-medium text-text-primary\">\n Preview\n </span>\n )}\n </li>\n );\n};\n\nconst ModuleCard: FC<{ module: BuiModule; index: number; onAddLesson: () => void }> = ({\n module,\n index,\n onAddLesson,\n}) => {\n const lessons = [...(module.lessons ?? [])].sort((a, b) => a.position - b.position);\n const totalMin = lessons.reduce((sum, l) => sum + (l.durationMin || 0), 0);\n return (\n <GlassCard treatment=\"plain\" className=\"overflow-hidden\">\n <div className=\"flex flex-wrap items-center gap-3 border-b border-border-seam bg-bg-sunken/40 px-4 py-3\">\n <span\n className=\"flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-action-primary-bg text-sm font-semibold tabular-nums text-action-primary-text\"\n aria-hidden=\"true\"\n >\n {index + 1}\n </span>\n <div className=\"min-w-0 flex-1\">\n <h3 className=\"truncate font-semibold text-text-primary\">\n <span className=\"text-text-tertiary\">Module {index + 1} · </span>\n {module.title}\n </h3>\n <p className=\"text-xs text-text-secondary\">\n {lessons.length} {lessons.length === 1 ? 'lesson' : 'lessons'}\n {totalMin > 0 && ` · ${formatDuration(totalMin)}`}\n </p>\n </div>\n <BuiPermissionButton resource=\"buildmyiq-lesson\" action=\"create\" deniedBehavior=\"hide\">\n <Button variant=\"outline\" size=\"sm\" onClick={onAddLesson} className=\"shrink-0\">\n <Plus className=\"mr-1.5 h-4 w-4\" /> Add lesson\n </Button>\n </BuiPermissionButton>\n </div>\n {module.description && (\n <p className=\"border-b border-border-seam px-4 py-2.5 text-sm text-text-secondary\">\n {module.description}\n </p>\n )}\n <div className=\"p-3\">\n {lessons.length === 0 ? (\n <BuiPermissionButton resource=\"buildmyiq-lesson\" action=\"create\" deniedBehavior=\"hide\">\n <button\n type=\"button\"\n onClick={onAddLesson}\n className=\"flex w-full items-center justify-center gap-2 rounded-lg border border-dashed border-border-seam px-3 py-3 text-sm text-text-secondary transition-colors hover:border-[var(--color-action-primary-bg)] hover:text-text-primary\"\n >\n <Plus className=\"h-4 w-4\" /> Add the first lesson\n </button>\n </BuiPermissionButton>\n ) : (\n <ul className=\"flex flex-col gap-2\">\n {lessons.map((lesson) => (\n <LessonRow key={lesson.id} lesson={lesson} />\n ))}\n </ul>\n )}\n </div>\n </GlassCard>\n );\n};\n\nconst DetailRow: FC<{ icon: ReactNode; label: string; value: string }> = ({\n icon,\n label,\n value,\n}) => (\n <div className=\"flex items-center gap-3 py-2.5\">\n <span className=\"shrink-0 text-text-tertiary\" aria-hidden=\"true\">\n {icon}\n </span>\n <span className=\"text-sm text-text-secondary\">{label}</span>\n <span className=\"ml-auto min-w-0 truncate text-right text-sm font-medium text-text-primary\">\n {value}\n </span>\n </div>\n);\n\nconst CurriculumTab: FC<{ course: BuiCourse }> = ({ course }) => {\n const courseId = course.id;\n const modulesQuery = useBuiModules(courseId);\n const updateCourse = useBuiUpdateCourse();\n const [showAddModule, setShowAddModule] = useState(false);\n const [lessonModuleId, setLessonModuleId] = useState<string | null>(null);\n const [coverFile, setCoverFile] = useState<UploadedBuiFile | null>(null);\n const [coverError, setCoverError] = useState<string | null>(null);\n\n const modules = [...(modulesQuery.data ?? [])].sort((a, b) => a.position - b.position);\n const lessonModule = modules.find((m) => m.id === lessonModuleId) ?? null;\n\n const handleCoverChange = async (file: UploadedBuiFile | null) => {\n setCoverFile(file);\n if (!file) return;\n setCoverError(null);\n try {\n await updateCourse.mutateAsync({ id: courseId, input: { coverFileId: file.fileId } });\n } catch (e) {\n setCoverError(e instanceof Error ? e.message : 'Failed to update the cover.');\n }\n };\n\n return (\n <div className=\"grid gap-6 lg:grid-cols-3\">\n {/* Builder */}\n <div className=\"lg:col-span-2\">\n <Section\n title=\"Curriculum\"\n description=\"Organise the course into modules and lessons.\"\n action={\n <BuiPermissionButton resource=\"buildmyiq-module\" action=\"create\" deniedBehavior=\"hide\">\n <Button onClick={() => setShowAddModule(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> Add module\n </Button>\n </BuiPermissionButton>\n }\n >\n {modulesQuery.isLoading ? (\n <LoadingView label=\"Loading curriculum…\" />\n ) : modulesQuery.isError ? (\n <ErrorView\n message=\"We couldn't load the curriculum.\"\n onRetry={() => modulesQuery.refetch()}\n />\n ) : modules.length === 0 ? (\n <EmptyState\n illustration=\"onboarding\"\n title=\"No modules yet\"\n description=\"Add your first module, then fill it with lessons.\"\n action={\n <BuiPermissionButton\n resource=\"buildmyiq-module\"\n action=\"create\"\n deniedBehavior=\"hide\"\n >\n <Button onClick={() => setShowAddModule(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> Add module\n </Button>\n </BuiPermissionButton>\n }\n />\n ) : (\n <div className=\"flex flex-col gap-4\">\n {modules.map((mod, idx) => (\n <ModuleCard\n key={mod.id}\n module={mod}\n index={idx}\n onAddLesson={() => setLessonModuleId(mod.id)}\n />\n ))}\n <BuiPermissionButton\n resource=\"buildmyiq-module\"\n action=\"create\"\n deniedBehavior=\"hide\"\n >\n <button\n type=\"button\"\n onClick={() => setShowAddModule(true)}\n className=\"flex w-full items-center justify-center gap-2 rounded-xl border-2 border-dashed border-border-seam px-4 py-4 text-sm font-medium text-text-secondary transition-colors hover:border-[var(--color-action-primary-bg)] hover:bg-bg-sunken/40 hover:text-text-primary\"\n >\n <Plus className=\"h-4 w-4\" /> Add module\n </button>\n </BuiPermissionButton>\n </div>\n )}\n </Section>\n </div>\n\n {/* Right rail: cover + details */}\n <div className=\"flex flex-col gap-6\">\n <GlassCard treatment=\"plain\" className=\"p-5\">\n <h3 className=\"text-base font-semibold text-text-primary\">Course cover</h3>\n <p className=\"mt-0.5 text-sm text-text-secondary\">\n Upload an image to represent your course.\n </p>\n <div className=\"mt-3 aspect-video w-full overflow-hidden rounded-xl border border-border-seam bg-bg-sunken\">\n {course.cover?.downloadUrl ? (\n <img src={course.cover.downloadUrl} alt=\"\" className=\"h-full w-full object-cover\" />\n ) : (\n <div className=\"flex h-full items-center justify-center gap-2 text-sm text-text-tertiary\">\n <ImageIcon className=\"h-5 w-5\" /> No cover yet\n </div>\n )}\n </div>\n <div className=\"mt-3\">\n <EvidenceUpload\n id=\"course-cover\"\n label={course.cover?.downloadUrl ? 'Replace cover' : 'Upload cover'}\n value={coverFile}\n onChange={handleCoverChange}\n accept=\"image/*\"\n hint=\"PNG or JPG · 16:9 ratio recommended.\"\n />\n {coverError && <p className=\"mt-1 text-xs text-status-error-text\">{coverError}</p>}\n </div>\n </GlassCard>\n\n <GlassCard treatment=\"plain\" className=\"p-5\">\n <h3 className=\"text-base font-semibold text-text-primary\">Course details</h3>\n <dl className=\"mt-2 flex flex-col divide-y divide-border-seam\">\n <DetailRow icon={<Link2 className=\"h-4 w-4\" />} label=\"Slug\" value={course.slug} />\n <DetailRow\n icon={<Globe className=\"h-4 w-4\" />}\n label=\"Language\"\n value={course.language ? course.language.toUpperCase() : '—'}\n />\n <DetailRow\n icon={<BarChart3 className=\"h-4 w-4\" />}\n label=\"Level\"\n value={formatEnumLabel(course.level)}\n />\n <DetailRow\n icon={<Eye className=\"h-4 w-4\" />}\n label=\"Visibility\"\n value={course.isPublic ? 'Public' : 'Private'}\n />\n <DetailRow\n icon={<Calendar className=\"h-4 w-4\" />}\n label=\"Created\"\n value={formatDate(course.createdAt)}\n />\n </dl>\n </GlassCard>\n </div>\n\n {showAddModule && (\n <AddModuleModal\n courseId={courseId}\n position={modules.length}\n onClose={() => setShowAddModule(false)}\n />\n )}\n {lessonModuleId && (\n <AddLessonModal\n courseId={courseId}\n moduleId={lessonModuleId}\n position={lessonModule?.lessons?.length ?? 0}\n onClose={() => setLessonModuleId(null)}\n />\n )}\n </div>\n );\n};\n\n// ============================================================================\n// Assessments tab\n// ============================================================================\n\nconst AssessmentsTab: FC<{ courseId: string }> = ({ courseId }) => {\n const { navigateTo } = useBuildmyiq();\n const assessmentsQuery = useBuiAssessments(courseId);\n const assessments = assessmentsQuery.data ?? [];\n\n return (\n <Section\n title=\"Assessments\"\n description=\"Quizzes, exams and assignments attached to this course.\"\n >\n {assessmentsQuery.isLoading ? (\n <LoadingView label=\"Loading assessments…\" />\n ) : assessmentsQuery.isError ? (\n <ErrorView\n message=\"We couldn't load the assessments.\"\n onRetry={() => assessmentsQuery.refetch()}\n />\n ) : assessments.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n title=\"No assessments yet\"\n description=\"Create an assessment in the Assessments workspace to test learners on this course.\"\n action={\n <Button variant=\"outline\" onClick={() => navigateTo('assessments')}>\n Open assessments\n </Button>\n }\n />\n ) : (\n <div className=\"grid grid-cols-1 gap-3 sm:grid-cols-2\">\n {assessments.map((a) => (\n <GlassCard\n key={a.id}\n treatment=\"plain\"\n className=\"flex cursor-pointer flex-col gap-2 p-4 transition-shadow hover:shadow-lg\"\n onClick={() => navigateTo(`assessments/${a.id}`)}\n >\n <div className=\"flex items-start justify-between gap-2\">\n <h3 className=\"min-w-0 font-semibold text-text-primary\">{a.title}</h3>\n <StatusBadge status={a.status} />\n </div>\n {a.description && (\n <p className=\"line-clamp-2 text-sm text-text-secondary\">{a.description}</p>\n )}\n <div className=\"mt-auto flex flex-wrap items-center gap-3 pt-1 text-xs text-text-secondary\">\n <span className=\"rounded-full bg-bg-sunken px-2 py-0.5\">\n {formatEnumLabel(a.type)}\n </span>\n <span>Pass ≥ {a.passingScore ?? 0}%</span>\n {a.timeLimitMin > 0 && (\n <span className=\"inline-flex items-center gap-1\">\n <Clock className=\"h-3.5 w-3.5\" /> {formatDuration(a.timeLimitMin)}\n </span>\n )}\n </div>\n </GlassCard>\n ))}\n </div>\n )}\n </Section>\n );\n};\n\n// ============================================================================\n// Page\n// ============================================================================\n\nexport const CourseBuilderPage: FC = () => {\n const { courseId } = useParams<{ courseId: string }>();\n const { navigateTo } = useBuildmyiq();\n const [tab, setTab] = useState<BuilderTab>('details');\n const courseQuery = useBuiCourse(courseId);\n const course = courseQuery.data ?? null;\n\n const publishCourse = useBuiPublishCourse();\n const archiveCourse = useBuiArchiveCourse();\n const [actionError, setActionError] = useState<string | null>(null);\n const [confirmArchive, setConfirmArchive] = useState(false);\n\n const handlePublish = async () => {\n if (!course) return;\n setActionError(null);\n try {\n await publishCourse.mutateAsync(course.id);\n } catch (e) {\n setActionError(e instanceof Error ? e.message : 'Failed to publish the course.');\n }\n };\n\n const handleArchive = async () => {\n if (!course) return;\n setActionError(null);\n try {\n await archiveCourse.mutateAsync(course.id);\n setConfirmArchive(false);\n } catch (e) {\n setActionError(e instanceof Error ? e.message : 'Failed to archive the course.');\n }\n };\n\n if (courseQuery.isLoading) {\n return (\n <PageLayout title=\"Course builder\">\n <LoadingView label=\"Loading course…\" />\n </PageLayout>\n );\n }\n\n if (courseQuery.isError) {\n return (\n <PageLayout title=\"Course builder\">\n <ErrorView message=\"We couldn't load this course.\" onRetry={() => courseQuery.refetch()} />\n </PageLayout>\n );\n }\n\n if (!courseId || !course) {\n return (\n <PageLayout title=\"Course builder\">\n <EmptyState\n illustration=\"empty-search\"\n title=\"Course not found\"\n description=\"This course may have been archived or removed.\"\n action={<Button onClick={() => navigateTo('authoring')}>Back to authoring</Button>}\n />\n </PageLayout>\n );\n }\n\n const canPublish = course.status === 'DRAFT' || course.status === 'IN_REVIEW';\n const canArchive = course.status !== 'ARCHIVED';\n\n const tabs: TabItem<BuilderTab>[] = [\n { value: 'details', label: 'Details', icon: <FileText className=\"h-4 w-4\" /> },\n {\n value: 'curriculum',\n label: 'Curriculum',\n icon: <BookOpen className=\"h-4 w-4\" />,\n count: course.modules?.length ?? 0,\n },\n { value: 'assessments', label: 'Assessments', icon: <ListChecks className=\"h-4 w-4\" /> },\n { value: 'discussion', label: 'Discussion', icon: <MessageSquare className=\"h-4 w-4\" /> },\n ];\n\n return (\n <PageLayout\n title={\n <span className=\"flex flex-wrap items-center gap-3\">\n <span className=\"min-w-0\">{course.title}</span>\n <StatusBadge status={course.status} />\n <StatusBadge status={course.level} />\n </span>\n }\n description={course.summary ?? undefined}\n actions={\n <>\n <Button variant=\"ghost\" onClick={() => navigateTo('authoring')}>\n <ArrowLeft className=\"mr-1.5 h-4 w-4\" /> Back\n </Button>\n {canArchive && (\n <BuiPermissionButton resource=\"buildmyiq-course\" action=\"delete\" deniedBehavior=\"hide\">\n <Button variant=\"outline\" onClick={() => setConfirmArchive(true)}>\n <Archive className=\"mr-1.5 h-4 w-4\" /> Archive\n </Button>\n </BuiPermissionButton>\n )}\n {canPublish && (\n <BuiPermissionButton resource=\"buildmyiq-course\" action=\"update\" deniedBehavior=\"hide\">\n <Button onClick={handlePublish} disabled={publishCourse.isPending}>\n <Send className=\"mr-1.5 h-4 w-4\" />\n {publishCourse.isPending ? 'Publishing…' : 'Publish'}\n </Button>\n </BuiPermissionButton>\n )}\n </>\n }\n >\n <div className=\"flex flex-col gap-5\">\n {actionError && (\n <p className=\"rounded-lg border border-status-error-bg-subtle bg-status-error-bg-subtle px-3 py-2 text-sm text-status-error-text\">\n {actionError}\n </p>\n )}\n\n <Tabs items={tabs} value={tab} onChange={setTab} />\n\n {tab === 'details' && <DetailsTab key={course.id} course={course} />}\n {tab === 'curriculum' && <CurriculumTab course={course} />}\n {tab === 'assessments' && <AssessmentsTab courseId={courseId} />}\n {tab === 'discussion' && (\n <BuiDiscussion entityType=\"buildmyiq-course\" entityId={courseId} />\n )}\n </div>\n\n {confirmArchive && (\n <Modal\n open\n onClose={() => setConfirmArchive(false)}\n title=\"Archive this course?\"\n description=\"Archived courses are hidden from the catalog and cannot be enrolled in.\"\n footer={\n <>\n <Button\n variant=\"ghost\"\n onClick={() => setConfirmArchive(false)}\n disabled={archiveCourse.isPending}\n >\n Cancel\n </Button>\n <Button\n variant=\"destructive\"\n onClick={handleArchive}\n disabled={archiveCourse.isPending}\n >\n {archiveCourse.isPending ? 'Archiving…' : 'Archive course'}\n </Button>\n </>\n }\n >\n <p className=\"text-sm text-text-secondary\">\n You can still find it in Authoring under the Archived filter.\n </p>\n </Modal>\n )}\n </PageLayout>\n );\n};\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"_forms.js","names":[],"sources":["../../../../src/buildmyiq/pages/authoring/_forms.tsx"],"sourcesContent":["/**\n * Authoring form primitives — co-located helpers shared by AuthoringPage and\n * CourseBuilderPage only.\n * @module buildmyiq/pages/authoring\n *\n * Thin, token-only wrappers over the fe-libs ResponsiveDialog + Select primitives so the\n * authoring create/edit modals stay consistent and free of duplication. These\n * are intentionally NOT exported from the components barrel — they are private\n * to the authoring section.\n */\n\nimport type { FC, ReactNode } from 'react';\nimport {\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogDescription,\n ResponsiveDialogFooter,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n Label,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '@burdenoff/fe-libs/ui';\nimport { cn } from '../../utils/cn';\n\n/** Sentinel value for an \"unset\" optional Select (radix forbids an empty string value). */\nexport const NO_SKILL = '__none__';\n\n/** Turn a free-text title into a URL-safe slug. */\nexport const slugify = (text: string): string =>\n text\n .toLowerCase()\n .trim()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '')\n .slice(0, 80);\n\ninterface ModalProps {\n open: boolean;\n onClose: () => void;\n title: string;\n description?: string;\n footer?: ReactNode;\n className?: string;\n children: ReactNode;\n}\n\n/** Controlled modal built on the fe-libs ResponsiveDialog. Closes on ESC / overlay / X. */\nexport const Modal: FC<ModalProps> = ({\n open,\n onClose,\n title,\n description,\n footer,\n className,\n children,\n}) => (\n <ResponsiveDialog\n open={open}\n onOpenChange={(next) => {\n if (!next) onClose();\n }}\n >\n <ResponsiveDialogContent className={cn('max-h-[90vh] overflow-y-auto', className)}>\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>{title}</ResponsiveDialogTitle>\n {description && <ResponsiveDialogDescription>{description}</ResponsiveDialogDescription>}\n </ResponsiveDialogHeader>\n {children}\n {footer && <ResponsiveDialogFooter>{footer}</ResponsiveDialogFooter>}\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n);\n\ninterface FieldProps {\n label: string;\n htmlFor?: string;\n required?: boolean;\n hint?: string;\n error?: string;\n className?: string;\n children: ReactNode;\n}\n\n/** Label + control + hint/error wrapper for form rows. */\nexport const Field: FC<FieldProps> = ({\n label,\n htmlFor,\n required,\n hint,\n error,\n className,\n children,\n}) => (\n <div className={cn('grid gap-1.5', className)}>\n <Label htmlFor={htmlFor}>\n {label}\n {required && <span className=\"text-status-error-text\"> *</span>}\n </Label>\n {children}\n {hint && !error && <p className=\"text-xs text-text-tertiary\">{hint}</p>}\n {error && <p className=\"text-xs text-status-error-text\">{error}</p>}\n </div>\n);\n\nexport interface SelectOption {\n value: string;\n label: string;\n}\n\ninterface SelectFieldProps {\n id?: string;\n value: string;\n onChange: (value: string) => void;\n options: SelectOption[];\n placeholder?: string;\n disabled?: boolean;\n}\n\n/** A single-select control over the fe-libs Select primitive. */\nexport const SelectField: FC<SelectFieldProps> = ({\n id,\n value,\n onChange,\n options,\n placeholder,\n disabled,\n}) => (\n <Select value={value} onValueChange={onChange} disabled={disabled}>\n <SelectTrigger id={id} className=\"w-full\">\n <SelectValue placeholder={placeholder} />\n </SelectTrigger>\n <SelectContent>\n {options.map((opt) => (\n <SelectItem key={opt.value} value={opt.value}>\n {opt.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n);\n"],"mappings":";;;;AA6BA,IAAa,IAAW,YAGX,KAAW,MACtB,EACG,
|
|
1
|
+
{"version":3,"file":"_forms.js","names":[],"sources":["../../../../src/buildmyiq/pages/authoring/_forms.tsx"],"sourcesContent":["/**\n * Authoring form primitives — co-located helpers shared by AuthoringPage and\n * CourseBuilderPage only.\n * @module buildmyiq/pages/authoring\n *\n * Thin, token-only wrappers over the fe-libs ResponsiveDialog + Select primitives so the\n * authoring create/edit modals stay consistent and free of duplication. These\n * are intentionally NOT exported from the components barrel — they are private\n * to the authoring section.\n */\n\nimport type { FC, ReactNode } from 'react';\nimport {\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogDescription,\n ResponsiveDialogFooter,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n Label,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '@burdenoff/fe-libs/ui';\nimport { cn } from '../../utils/cn';\n\n/** Sentinel value for an \"unset\" optional Select (radix forbids an empty string value). */\nexport const NO_SKILL = '__none__';\n\n/** Turn a free-text title into a URL-safe slug. */\nexport const slugify = (text: string): string =>\n text\n .toLowerCase()\n .trim()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '')\n .slice(0, 80);\n\ninterface ModalProps {\n open: boolean;\n onClose: () => void;\n title: string;\n description?: string;\n footer?: ReactNode;\n className?: string;\n children: ReactNode;\n}\n\n/** Controlled modal built on the fe-libs ResponsiveDialog. Closes on ESC / overlay / X. */\nexport const Modal: FC<ModalProps> = ({\n open,\n onClose,\n title,\n description,\n footer,\n className,\n children,\n}) => (\n <ResponsiveDialog\n open={open}\n onOpenChange={(next) => {\n if (!next) onClose();\n }}\n >\n <ResponsiveDialogContent className={cn('max-h-[90vh] overflow-y-auto', className)}>\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>{title}</ResponsiveDialogTitle>\n {description && <ResponsiveDialogDescription>{description}</ResponsiveDialogDescription>}\n </ResponsiveDialogHeader>\n {children}\n {footer && <ResponsiveDialogFooter>{footer}</ResponsiveDialogFooter>}\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n);\n\ninterface FieldProps {\n label: string;\n htmlFor?: string;\n required?: boolean;\n hint?: string;\n error?: string;\n className?: string;\n children: ReactNode;\n}\n\n/** Label + control + hint/error wrapper for form rows. */\nexport const Field: FC<FieldProps> = ({\n label,\n htmlFor,\n required,\n hint,\n error,\n className,\n children,\n}) => (\n <div className={cn('grid gap-1.5', className)}>\n <Label htmlFor={htmlFor}>\n {label}\n {required && <span className=\"text-status-error-text\"> *</span>}\n </Label>\n {children}\n {hint && !error && <p className=\"text-xs text-text-tertiary\">{hint}</p>}\n {error && <p className=\"text-xs text-status-error-text\">{error}</p>}\n </div>\n);\n\nexport interface SelectOption {\n value: string;\n label: string;\n}\n\ninterface SelectFieldProps {\n id?: string;\n value: string;\n onChange: (value: string) => void;\n options: SelectOption[];\n placeholder?: string;\n disabled?: boolean;\n}\n\n/** A single-select control over the fe-libs Select primitive. */\nexport const SelectField: FC<SelectFieldProps> = ({\n id,\n value,\n onChange,\n options,\n placeholder,\n disabled,\n}) => (\n <Select value={value} onValueChange={onChange} disabled={disabled}>\n <SelectTrigger id={id} className=\"w-full\">\n <SelectValue placeholder={placeholder} />\n </SelectTrigger>\n <SelectContent>\n {options.map((opt) => (\n <SelectItem key={opt.value} value={opt.value}>\n {opt.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n);\n"],"mappings":";;;;AA6BA,IAAa,IAAW,YAGX,KAAW,MACtB,EACG,YAAY,EACZ,KAAK,EACL,QAAQ,eAAe,GAAG,EAC1B,QAAQ,YAAY,EAAE,EACtB,MAAM,GAAG,EAAE,GAaH,KAAyB,EACpC,SACA,YACA,UACA,gBACA,WACA,cACA,kBAEA,kBAAC,GAAD;CACQ;CACN,eAAe,MAAS;EACtB,AAAK,KAAM,EAAQ;CACrB;WAEA,kBAAC,GAAD;EAAyB,WAAW,EAAG,gCAAgC,CAAS;YAAhF;GACE,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD,EAAA,UAAwB,EAA6B,CAAA,GACpD,KAAe,kBAAC,GAAD,EAAA,UAA8B,EAAyC,CAAA,CACjE,EAAA,CAAA;GACvB;GACA,KAAU,kBAAC,GAAD,EAAA,UAAyB,EAA+B,CAAA;EAC5C;;AACT,CAAA,GAcP,KAAyB,EACpC,UACA,YACA,aACA,SACA,UACA,cACA,kBAEA,kBAAC,OAAD;CAAK,WAAW,EAAG,gBAAgB,CAAS;WAA5C;EACE,kBAAC,GAAD;GAAgB;aAAhB,CACG,GACA,KAAY,kBAAC,QAAD;IAAM,WAAU;cAAyB;GAAQ,CAAA,CACzD;;EACN;EACA,KAAQ,CAAC,KAAS,kBAAC,KAAD;GAAG,WAAU;aAA8B;EAAQ,CAAA;EACrE,KAAS,kBAAC,KAAD;GAAG,WAAU;aAAkC;EAAS,CAAA;CAC/D;IAkBM,KAAqC,EAChD,OACA,UACA,aACA,YACA,gBACA,kBAEA,kBAAC,GAAD;CAAe;CAAO,eAAe;CAAoB;WAAzD,CACE,kBAAC,GAAD;EAAmB;EAAI,WAAU;YAC/B,kBAAC,GAAD,EAA0B,eAAc,CAAA;CAC3B,CAAA,GACf,kBAAC,GAAD,EAAA,UACG,EAAQ,KAAK,MACZ,kBAAC,GAAD;EAA4B,OAAO,EAAI;YACpC,EAAI;CACK,GAFK,EAAI,KAET,CACb,EACY,CAAA,CACT"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CatalogPage.js","names":[],"sources":["../../../../src/buildmyiq/pages/catalog/CatalogPage.tsx"],"sourcesContent":["/**\n * CatalogPage — learner-facing course discovery.\n * @module buildmyiq/pages/catalog\n *\n * The primary page of the Catalog section. Browses PUBLISHED courses from the\n * live `buiListCourses` connection, filtered server-side by level and paged by\n * cursor. A client-side search box narrows the current page by title/summary.\n * Every course tile links to its detail/enrol page. Honest empty states only.\n */\n\nimport type { FC } from 'react';\nimport { useMemo, useState } from 'react';\nimport { LibraryBig, Search } from 'lucide-react';\nimport { Input } from '@burdenoff/fe-libs/ui';\nimport {\n PageLayout,\n FilterTabs,\n Pagination,\n EmptyState,\n LoadingView,\n ErrorView,\n type FilterOption,\n} from '../../components';\nimport { CourseCard } from '../_shared';\nimport { useBuildmyiq } from '../../context';\nimport { useBuiCourses, useCursorPager } from '../../hooks';\nimport { COURSE_LEVELS } from '../../types';\nimport type { BuiCourse, BuiCourseLevel } from '../../types';\nimport { formatEnumLabel } from '../../utils/formatters';\n\ntype LevelFilter = 'ALL' | BuiCourseLevel;\n\nconst PAGE_SIZE = 24;\n\nexport const CatalogPage: FC = () => {\n const { navigateTo } = useBuildmyiq();\n const pager = useCursorPager();\n\n const [level, setLevel] = useState<LevelFilter>('ALL');\n const [search, setSearch] = useState('');\n\n const coursesQuery = useBuiCourses({\n status: 'PUBLISHED',\n level: level === 'ALL' ? undefined : level,\n cursor: pager.cursor,\n limit: PAGE_SIZE,\n });\n\n const courses: BuiCourse[] = coursesQuery.data?.items ?? [];\n\n const levelOptions: FilterOption<LevelFilter>[] = useMemo(\n () => [\n { value: 'ALL', label: 'All levels' },\n ...COURSE_LEVELS.map((lvl) => ({ value: lvl, label: formatEnumLabel(lvl) })),\n ],\n []\n );\n\n const term = search.trim().toLowerCase();\n const filtered = useMemo(() => {\n if (!term) return courses;\n return courses.filter((course) =>\n `${course.title} ${course.summary ?? ''}`.toLowerCase().includes(term)\n );\n }, [courses, term]);\n\n const handleLevelChange = (next: LevelFilter): void => {\n if (next === level) return;\n setLevel(next);\n setSearch('');\n pager.reset();\n };\n\n const hasMore = coursesQuery.data?.pageInfo.hasMore ?? false;\n const hasResults =\n !coursesQuery.isLoading && !coursesQuery.isError && courses.length > 0 && filtered.length > 0;\n\n // Honest, page-scoped result summary (we only ever hold the current page).\n const resultSummary = (() => {\n const n = filtered.length;\n const noun = `course${n === 1 ? '' : 's'}`;\n if (term) return `${n} ${noun} matching “${search.trim()}” on this page`;\n if (hasMore) return `Showing ${n} ${noun} — more on the next page`;\n return `${n} ${noun}`;\n })();\n\n return (\n <PageLayout\n title=\"Course Catalog\"\n description=\"Browse and enrol in published courses across your workspace.\"\n titleGradient\n aurora\n >\n <div className=\"flex flex-col gap-6\">\n {/* Toolbar: level filter + client-side search */}\n <div className=\"flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between\">\n <FilterTabs options={levelOptions} value={level} onChange={handleLevelChange} />\n <div className=\"relative w-full min-w-0 lg:max-w-xs\">\n <Search\n className=\"pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-tertiary\"\n aria-hidden\n />\n <Input\n value={search}\n onChange={(event) => setSearch(event.target.value)}\n placeholder=\"Search this page…\"\n className=\"pl-9\"\n aria-label=\"Search courses on this page\"\n />\n </div>\n </div>\n\n {/* Result summary — only when a data grid is on screen */}\n {hasResults && (\n <p className=\"-mt-2 inline-flex items-center gap-1.5 text-sm text-text-secondary\">\n <LibraryBig className=\"h-4 w-4 text-text-tertiary\" aria-hidden />\n {resultSummary}\n </p>\n )}\n\n {/* List: loading → error → empty → data */}\n {coursesQuery.isLoading ? (\n <LoadingView label=\"Loading the catalog…\" />\n ) : coursesQuery.isError ? (\n <ErrorView\n title=\"Couldn't load courses\"\n message={coursesQuery.error instanceof Error ? coursesQuery.error.message : undefined}\n onRetry={() => coursesQuery.refetch()}\n />\n ) : courses.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n title={\n level === 'ALL'\n ? 'No published courses yet'\n : `No ${formatEnumLabel(level).toLowerCase()} courses yet`\n }\n description=\"Published courses appear here as instructors publish them. Check back soon or browse another level.\"\n />\n ) : filtered.length === 0 ? (\n <EmptyState\n illustration=\"empty-search\"\n title=\"No courses match your search\"\n description={`Nothing on this page matches “${search.trim()}”. Try a different term or clear the search.`}\n />\n ) : (\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4\">\n {filtered.map((course) => (\n <CourseCard\n key={course.id}\n course={course}\n onClick={() => navigateTo(`catalog/${course.id}`)}\n />\n ))}\n </div>\n )}\n\n {/* Server pagination — hidden while a search term narrows the current page */}\n {!coursesQuery.isLoading && !coursesQuery.isError && courses.length > 0 && !term && (\n <Pagination\n page={pager.page}\n pageSize={PAGE_SIZE}\n itemCount={courses.length}\n hasMore={hasMore}\n onPageChange={(next) => pager.onPageChange(next, coursesQuery.data?.pageInfo.cursor)}\n />\n )}\n </div>\n </PageLayout>\n );\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAgCA,IAAM,IAAY,IAEL,UAAwB;CACnC,IAAM,EAAE,kBAAe,
|
|
1
|
+
{"version":3,"file":"CatalogPage.js","names":[],"sources":["../../../../src/buildmyiq/pages/catalog/CatalogPage.tsx"],"sourcesContent":["/**\n * CatalogPage — learner-facing course discovery.\n * @module buildmyiq/pages/catalog\n *\n * The primary page of the Catalog section. Browses PUBLISHED courses from the\n * live `buiListCourses` connection, filtered server-side by level and paged by\n * cursor. A client-side search box narrows the current page by title/summary.\n * Every course tile links to its detail/enrol page. Honest empty states only.\n */\n\nimport type { FC } from 'react';\nimport { useMemo, useState } from 'react';\nimport { LibraryBig, Search } from 'lucide-react';\nimport { Input } from '@burdenoff/fe-libs/ui';\nimport {\n PageLayout,\n FilterTabs,\n Pagination,\n EmptyState,\n LoadingView,\n ErrorView,\n type FilterOption,\n} from '../../components';\nimport { CourseCard } from '../_shared';\nimport { useBuildmyiq } from '../../context';\nimport { useBuiCourses, useCursorPager } from '../../hooks';\nimport { COURSE_LEVELS } from '../../types';\nimport type { BuiCourse, BuiCourseLevel } from '../../types';\nimport { formatEnumLabel } from '../../utils/formatters';\n\ntype LevelFilter = 'ALL' | BuiCourseLevel;\n\nconst PAGE_SIZE = 24;\n\nexport const CatalogPage: FC = () => {\n const { navigateTo } = useBuildmyiq();\n const pager = useCursorPager();\n\n const [level, setLevel] = useState<LevelFilter>('ALL');\n const [search, setSearch] = useState('');\n\n const coursesQuery = useBuiCourses({\n status: 'PUBLISHED',\n level: level === 'ALL' ? undefined : level,\n cursor: pager.cursor,\n limit: PAGE_SIZE,\n });\n\n const courses: BuiCourse[] = coursesQuery.data?.items ?? [];\n\n const levelOptions: FilterOption<LevelFilter>[] = useMemo(\n () => [\n { value: 'ALL', label: 'All levels' },\n ...COURSE_LEVELS.map((lvl) => ({ value: lvl, label: formatEnumLabel(lvl) })),\n ],\n []\n );\n\n const term = search.trim().toLowerCase();\n const filtered = useMemo(() => {\n if (!term) return courses;\n return courses.filter((course) =>\n `${course.title} ${course.summary ?? ''}`.toLowerCase().includes(term)\n );\n }, [courses, term]);\n\n const handleLevelChange = (next: LevelFilter): void => {\n if (next === level) return;\n setLevel(next);\n setSearch('');\n pager.reset();\n };\n\n const hasMore = coursesQuery.data?.pageInfo.hasMore ?? false;\n const hasResults =\n !coursesQuery.isLoading && !coursesQuery.isError && courses.length > 0 && filtered.length > 0;\n\n // Honest, page-scoped result summary (we only ever hold the current page).\n const resultSummary = (() => {\n const n = filtered.length;\n const noun = `course${n === 1 ? '' : 's'}`;\n if (term) return `${n} ${noun} matching “${search.trim()}” on this page`;\n if (hasMore) return `Showing ${n} ${noun} — more on the next page`;\n return `${n} ${noun}`;\n })();\n\n return (\n <PageLayout\n title=\"Course Catalog\"\n description=\"Browse and enrol in published courses across your workspace.\"\n titleGradient\n aurora\n >\n <div className=\"flex flex-col gap-6\">\n {/* Toolbar: level filter + client-side search */}\n <div className=\"flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between\">\n <FilterTabs options={levelOptions} value={level} onChange={handleLevelChange} />\n <div className=\"relative w-full min-w-0 lg:max-w-xs\">\n <Search\n className=\"pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-tertiary\"\n aria-hidden\n />\n <Input\n value={search}\n onChange={(event) => setSearch(event.target.value)}\n placeholder=\"Search this page…\"\n className=\"pl-9\"\n aria-label=\"Search courses on this page\"\n />\n </div>\n </div>\n\n {/* Result summary — only when a data grid is on screen */}\n {hasResults && (\n <p className=\"-mt-2 inline-flex items-center gap-1.5 text-sm text-text-secondary\">\n <LibraryBig className=\"h-4 w-4 text-text-tertiary\" aria-hidden />\n {resultSummary}\n </p>\n )}\n\n {/* List: loading → error → empty → data */}\n {coursesQuery.isLoading ? (\n <LoadingView label=\"Loading the catalog…\" />\n ) : coursesQuery.isError ? (\n <ErrorView\n title=\"Couldn't load courses\"\n message={coursesQuery.error instanceof Error ? coursesQuery.error.message : undefined}\n onRetry={() => coursesQuery.refetch()}\n />\n ) : courses.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n title={\n level === 'ALL'\n ? 'No published courses yet'\n : `No ${formatEnumLabel(level).toLowerCase()} courses yet`\n }\n description=\"Published courses appear here as instructors publish them. Check back soon or browse another level.\"\n />\n ) : filtered.length === 0 ? (\n <EmptyState\n illustration=\"empty-search\"\n title=\"No courses match your search\"\n description={`Nothing on this page matches “${search.trim()}”. Try a different term or clear the search.`}\n />\n ) : (\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4\">\n {filtered.map((course) => (\n <CourseCard\n key={course.id}\n course={course}\n onClick={() => navigateTo(`catalog/${course.id}`)}\n />\n ))}\n </div>\n )}\n\n {/* Server pagination — hidden while a search term narrows the current page */}\n {!coursesQuery.isLoading && !coursesQuery.isError && courses.length > 0 && !term && (\n <Pagination\n page={pager.page}\n pageSize={PAGE_SIZE}\n itemCount={courses.length}\n hasMore={hasMore}\n onPageChange={(next) => pager.onPageChange(next, coursesQuery.data?.pageInfo.cursor)}\n />\n )}\n </div>\n </PageLayout>\n );\n};\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CourseDetailPage.d.ts","sourceRoot":"","sources":["../../../../src/buildmyiq/pages/catalog/CourseDetailPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,EAAE,EAAa,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"CourseDetailPage.d.ts","sourceRoot":"","sources":["../../../../src/buildmyiq/pages/catalog/CourseDetailPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,EAAE,EAAa,MAAM,OAAO,CAAC;AAwG3C,eAAO,MAAM,gBAAgB,EAAE,EAse9B,CAAC"}
|