--- name: "" overview: "" todos: [] isProject: false --- # Course Page UI Extraction and Storybook Integration (Detailed Plan) ## Overview Extract the course page UI (sidebar + content shell) into a reusable `CoursePage` component in `packages/ui`, add a Storybook story under `Pages/CoursePage` that accepts JSON to form the whole page, and wire the dashboard layout to use it. **Content rendering in Storybook is view-mode only** (lesson and exercise). Edit mode stays in the dashboard and is out of scope. --- ## Part 0: Prerequisites and Reference Files to Read Before starting, a junior dev should read these files to understand the patterns: 1. **Exercise Question pattern** (how JSON fixtures drive UI): - `[packages/storybook/src/molecules/exercise-question/exercise-question.stories.svelte](packages/storybook/src/molecules/exercise-question/exercise-question.stories.svelte)` - `[packages/storybook/src/molecules/exercise-question/question-fixtures.ts](packages/storybook/src/molecules/exercise-question/question-fixtures.ts)` - `[packages/ui/src/custom/exercise-question/question-renderer.svelte](packages/ui/src/custom/exercise-question/question-renderer.svelte)` 2. **Current course layout** (what we are extracting): - `[apps/dashboard/src/routes/courses/[id]/+layout.svelte](apps/dashboard/src/routes/courses/[id]/+layout.svelte)` - `[apps/dashboard/src/lib/features/course/components/sidebar/course-sidebar-navigation.svelte](apps/dashboard/src/lib/features/course/components/sidebar/course-sidebar-navigation.svelte)` 3. **View vs Edit mode** (view mode only in Storybook): - Exercise view: `[apps/dashboard/src/lib/features/course/components/exercise/view-mode.svelte](apps/dashboard/src/lib/features/course/components/exercise/view-mode.svelte)` uses `ExerciseQuestion.QuestionList` with `mode: 'view'` - Lesson view: `[apps/dashboard/src/lib/features/course/components/lesson/utils.ts](apps/dashboard/src/lib/features/course/components/lesson/utils.ts)` – Note, Slide, Video, Document in view mode --- ## Part 1: Define the JSON Config Schema ### Step 1.1: Create the types file **Create file**: `packages/ui/src/custom/course-page/course-page-types.ts` **Purpose**: Define the shape of the JSON that drives the CoursePage. No imports from dashboard or API. **Contents** (copy and adapt): ```typescript /** * JSON config for CoursePage. Used by Storybook (fixtures) and Dashboard (built from course/org state). */ export interface CoursePageConfig { course: { id: string; title: string }; sidebar: CourseSidebarConfig; /** Used by Storybook for view-mode content. Dashboard uses content snippet instead. */ content?: CourseContentViewConfig; } export interface CourseSidebarConfig { backUrl: string; backLabel: string; navItems: CourseNavItem[]; } export interface CourseNavItem { id: string; title: string; url: string; isActive: boolean; isLesson?: boolean; icon?: CourseNavIconKey; contentTree?: CourseContentTreeSection[]; contentCount?: { lessons: number; exercises: number }; } export type CourseNavIconKey = | 'newsfeed' | 'content' | 'analytics' | 'attendance' | 'submissions' | 'marks' | 'certificates' | 'landingpage' | 'people' | 'settings' | 'section'; export interface CourseContentTreeSection { id: string; title: string; items: CourseContentTreeItem[]; } export interface CourseContentTreeItem { id: string; title: string; type: 'LESSON' | 'EXERCISE'; url?: string; isComplete?: boolean; isUnlocked?: boolean; } /** * View-mode content config for Storybook. Dashboard does NOT use this – it renders route children. */ export type CourseContentViewConfig = | { type: 'lesson'; data: LessonViewData } | { type: 'exercise'; data: ExerciseViewData } | { type: 'placeholder'; data: { label: string } }; export interface LessonViewData { title: string; note?: string; // HTML string slideUrl?: string; videos?: { link: string; type: string; metadata?: Record }[]; documents?: { title: string; url: string }[]; } export interface ExerciseViewData { title: string; questions: Record[]; answersByKey?: Record; } ``` **Check**: File compiles with `pnpm exec tsc --noEmit` in `packages/ui`. --- ## Part 2: Create the CoursePage UI Components ### Step 2.1: Icon lookup map **Create file**: `packages/ui/src/custom/course-page/icon-map.ts` **Purpose**: Map string keys from config to the actual icon components. **Contents**: ```typescript import type { Component } from 'svelte'; import TableOfContentsIcon from '@lucide/svelte/icons/table-of-contents'; import { NewsFeedIcon, ContentIcon, AnalyticsIcon, AttendanceIcon, SubmissionIcon, MarksIcon, CertificateIcon, LandingPageIcon, PeopleIcon, SettingsIcon } from '@cio/ui/custom/moving-icons'; import type { CourseNavIconKey } from './course-page-types'; export const COURSE_NAV_ICON_MAP: Record = { section: TableOfContentsIcon, newsfeed: NewsFeedIcon, content: ContentIcon, analytics: AnalyticsIcon, attendance: AttendanceIcon, submissions: SubmissionIcon, marks: MarksIcon, certificates: CertificateIcon, landingpage: LandingPageIcon, people: PeopleIcon, settings: SettingsIcon }; export function getCourseNavIcon(key: CourseNavIconKey | undefined): Component | null { if (!key) return null; return COURSE_NAV_ICON_MAP[key] ?? null; } ``` **Check**: Imports resolve. Moving-icons live at `packages/ui/src/custom/moving-icons`. --- ### Step 2.2: Course page sidebar **Create file**: `packages/ui/src/custom/course-page/course-page-sidebar.svelte` **Purpose**: Renders the sidebar from `CourseSidebarConfig`. No dashboard/API deps. **Logic**: - Use `@cio/ui/base/sidebar` primitives: `Sidebar.Root`, `Sidebar.Header`, `Sidebar.Content`, `Sidebar.Group`, `Sidebar.Menu`, `Sidebar.MenuItem`, `Sidebar.MenuButton`, `Sidebar.MenuSub`, `Sidebar.MenuSubItem`, `Sidebar.MenuSubButton`, `Sidebar.Rail`. - For each `navItem`: if `isLesson` and `contentTree` exists and item `isActive`, render the collapsible tree; otherwise render a simple link. - Use `getCourseNavIcon(navItem.icon)` for icons. - Back button: link to `config.sidebar.backUrl` with label `config.sidebar.backLabel`. - Sidebar header: render a simple "Course" placeholder (or accept a `sidebarHeader` snippet later). - Use `resolve` from `$app/paths` for URLs – but CoursePage is in packages/ui which may not have SvelteKit. **Alternative**: pass `basePath` or use plain `href={item.url}` since URLs come from config. **Note**: `packages/ui` typically does NOT have `$app/paths` (SvelteKit). Use `item.url` directly as `href={item.url}` since config supplies full URLs. **Content tree for Lessons**: Use `Collapsible.Root` + `Collapsible.Trigger` + `Collapsible.Content` from `@cio/ui/base/collapsible`. Each section maps to a collapsible; items inside link to `item.url`. Reuse `LessonIcon` and `ExerciseIcon` from `@cio/ui/custom/moving-icons` for item types. **Structure** (pseudocode): ``` Sidebar.Root collapsible="icon" Sidebar.Header slot or "Course" Sidebar.Content Sidebar.Group Button/link back: backLabel -> backUrl Sidebar.Menu for each navItem if isLesson && isActive && contentTree render link + CourseContentTree (collapsible sections) else render link only Sidebar.Rail ``` **CourseContentTree for config**: Create a small component that renders sections/items from `contentTree` (from config, not courseApi). Each item links to `item.url` or builds path from `courseId` + `item.id` + `item.type`. For Storybook we can use `item.url` from the fixture. --- ### Step 2.3: Course page header **Create file**: `packages/ui/src/custom/course-page/course-page-header.svelte` **Purpose**: Simple header: Sidebar.Trigger + course title. No Search/notifications (dashboard keeps those). **Props**: `title: string` **Contents**: ```svelte

{title}

``` Copy classes from `[CourseHeader](apps/dashboard/src/lib/features/course/components/course-header.svelte)`. --- ### Step 2.4: View-mode content renderer (Lesson + Exercise) **Create file**: `packages/ui/src/custom/course-page/course-content-view.svelte` **Purpose**: Renders view-mode content from `CourseContentViewConfig`. Used only in Storybook. **Props**: `config: CourseContentViewConfig` **Logic**: - If `config.type === 'lesson'`: render `LessonViewContent` with `config.data` - If `config.type === 'exercise'`: render `ExerciseQuestion.QuestionList` (or `QuestionRenderer` per question) with `mode: 'view'`, `questions`, `answersByKey`, `disabled: true` - If `config.type === 'placeholder'`: render a simple card with `config.data.label` **Create file**: `packages/ui/src/custom/course-page/lesson-view-content.svelte` **Purpose**: Read-only lesson view. Accepts `LessonViewData` as props. **Rendering** (copy structure from dashboard view mode, but data-driven): - **Note**: Use `sanitizeHtml` from `@cio/ui/tools/sanitize` + `
{@html sanitized}
`. If `!data.note`, render nothing. - **Slide**: If `data.slideUrl`, render `