HTML & CSS Foundations
+Semantic markup, the box model, flexbox, and responsive layout basics.
+diff --git a/prd/learning-paths/README.md b/prd/learning-paths/README.md new file mode 100644 index 00000000..1a9e9ed6 --- /dev/null +++ b/prd/learning-paths/README.md @@ -0,0 +1,277 @@ +# Learning Paths PRD + +## Status + +- Draft (prototyped, ready for engineering review) + +## Prototypes — the UX source of truth + +**The UX of this feature must be taken from the prototype folder.** Interactive HTML prototypes for every surface live in [`prototypes/learning-paths/`](../../prototypes/learning-paths/) and are the approved design reference for layout, states (done / in progress / locked), copy, and navigation flow. When this document and a prototype disagree on a UI detail, the prototype wins. + +**Start here:** + +``` +prototypes/learning-paths/index.html +``` + +Open it in a browser (`open prototypes/learning-paths/index.html`) — it maps all three journeys (visitor → learner → teacher) and every page cross-links so you can click through each flow end to end. Each page has a light/dark toggle. + +The prototypes are built on the real design tokens (`packages/ui/src/index.css` for app pages via `app-theme.css`, the `minimal` org-landing theme for public pages via `landing-theme.css`) and encode every decision below. + +| Surface | Files | +| --- | --- | +| Public (org site) | `org-landing.html`, `public-paths.html`, `public-path.html` | +| Learner (LMS) | `lms-home.html`, `paths.html`, `path-detail.html`, `course-in-path.html` | +| Teacher (admin) | `teacher-paths.html`, `teacher-path-setup.html`, `teacher-path-builder.html`, `teacher-path-people.html`, `teacher-path-analytics.html`, `teacher-path-landing-editor.html`, `teacher-path-certificate.html`, `teacher-path-settings.html` | + +## Purpose + +Let orgs package multiple courses into an **ordered, sequentially unlocked, bundle-priced learning path** that is sold on the org's public site, completed step-by-step in the LMS, and finished with an optional path certificate. Reference experience: Coursera Professional Certificates (e.g. IBM Data Science). + +## Problem Statement + +- Courses can only be sold and taken individually; there is no way to sell a curriculum as one product with one price. +- Programs (shipped) group courses for cohort admin purposes but are explicitly **unordered**, have no gating, no public page, no pricing, and no path-level certificate. +- Instructors who run structured curricula cannot enforce prerequisites ("finish JS before React"). +- The org landing page has no product above the individual course, so multi-course offerings are invisible to visitors. + +## Confirmed Decisions + +1. **New entity, separate from Programs.** `learning_path` is its own table set. Programs remain unchanged (cohort hub + newsfeed). No migration. +2. **Terminology**: "Learning Path" everywhere (admin, LMS, public site). +3. **Self-enrollment with bundle pricing.** A path is public on the org landing site with one price. Enrolling (buy or free) auto-enrolls the learner in every course in the path. +4. **Show savings.** The public page shows the bundle price against the summed individual course prices ("$199 ~~$280~~ · Save $81 (29%)"). Toggleable per path. +5. **Unlock rule = lessons + exercises.** Course N+1 unlocks when course N has every lesson complete AND every exercise submitted/passed. Sequential unlocking is a per-path toggle (off = take in any order). +6. **Path certificate is a teacher toggle** with its own admin tab; issued automatically on full completion; appears in the learner's LMS Certificates page. +7. **Teacher workspace tabs**: Courses (ordering), People, Analytics, Landing page, Certificate, Settings. **No path newsfeed** (Programs covers cohort communication). +8. **Visitor access is teacher-configurable**: teaser only / full syllabus / syllabus + preview lessons. +9. **Public path page follows the Coursera professional-certificate shape**: hero with stats + enroll CTA, what-you'll-learn, skills tags, sequential course cards with per-course outcomes and lesson outlines, certificate block, instructors, testimonials, FAQ accordion, pricing card. +10. **Org landing page: Learning Paths section sits ABOVE the Courses section.** Plus a `/paths` catalog page and `/path/[slug]` detail page mirroring how `/courses` and `/course/[slug]` work today. +11. **Course independence**: a course can be in multiple paths and remain individually sellable. Removing a course from a path or a learner from a path never deletes `groupmember` rows or progress. +12. **Statuses**: `ACTIVE` (public + LMS), `DRAFT` (hidden while building), `ARCHIVED` (hidden everywhere, restorable). + +## Current-State Audit + +| Capability | Current state | Notes | +| --- | --- | --- | +| Grouping above course | `program` tables (unordered, admin-enrolled, newsfeed) | Keep as-is; do not extend | +| Enrollment | `groupmember` per course group; `enrollInCourse()` in `apps/api/src/services/course/invite.ts` | Reuse for auto-enrollment | +| Course completion | lesson completion + exercise submissions tracked per course | Source data for the unlock rule | +| Public org site | `(org-site)` routes + `packages/ui/src/custom/org-landing-page/` (10 themes, default `minimal`) | Mirror for paths section/list/detail | +| Course landing page | theme `course.svelte` composer: hero → anchor nav → social proof → info blocks → curriculum → chips → instructor → reviews → pricing | Path page reuses these shared components where possible | +| Payments | course payment flow on paid enroll CTA (`course-landing-page.svelte`) | Path enroll reuses the same flow with the path as the product | +| Certificates | course certificate system + LMS Certificates page | Add path-level issuance | +| Setup checklist pattern | `routes/(app)/org/[slug]/setup/+page.svelte` + `PercentRingProgress` + `Item.*` | Reuse for "Set up your path" and learner checklist view | + +## Product Goals + +1. A visitor can discover a path on the org landing page, read a full Coursera-style path page, and self-enroll (paid or free). +2. Enrolling in a path creates path membership and `groupmember` rows for every course in it. +3. A learner sees paths in the LMS (home section + Learning Paths nav), opens a path hub with per-course progress and locked/unlocked states, and moves through courses in order. +4. Inside a course that belongs to a path, the learner sees path context (position stepper, "next course unlocks when…"). +5. Teachers create a path via a setup checklist, order courses by drag, price the bundle, customize the public page, configure the certificate, and track a per-course funnel. +6. Completing all courses issues the path certificate (when enabled). +7. Programs, standalone courses, and existing enrollments are untouched (zero regression). + +## Non-Goals (v1) + +- Branching / non-linear paths (graphs, optional courses, parallel tracks). +- Path-level newsfeed or community. +- Subscriptions or installment pricing; coupon codes beyond what courses support. +- Path templates, cloning, or cross-org sharing. +- Per-learner overrides of the unlock rule (admin "skip ahead" can be a fast-follow). +- Team/B2B seat purchasing. + +--- + +## Functional Requirements + +### 1. Public — org landing site + +**Landing page section** (above Courses; see `org-landing.html`): +- Section heading "Learning Paths" + lead line; grid of path cards. +- Path card: "Learning Path" chip, name, description, ordered course preview list (first 3 + "+N more"), meta row (course count, total hours, certificate), bundle price with struck-through sum. +- "View all learning paths" → `/paths`. +- Rendered per landing-page theme like the courses section (start with `minimal`; other themes can fall back to base styling). + +**Catalog page `/paths`** (see `public-paths.html`): compact hero with search, filter sidebar (topic/tags, pricing, length), path card list, pagination. Mirrors `(org-site)/courses`. + +**Detail page `/path/[slug]`** (see `public-path.html`), section order: +1. Hero: chip, headline, subheadline (teacher-editable), instructor avatar stack, Enroll CTA with price + savings chip, sticky enroll box (price, savings, feature list, CTA, enrolled/rating counts). +2. Stats row: courses, total hours, level, rating. +3. Sticky anchor nav: About / Courses / Certificate / Instructors / Testimonials / FAQ. +4. What you'll learn (outcome bullets) + Skills tags. +5. Course series: one card per course in order — number, title, lesson count + duration, per-course outcome bullets, expandable lesson outline (lessons marked preview are openable when the teacher allows). +6. Certificate block (when enabled). +7. Instructors (auto-derived from course tutors, hideable). +8. Testimonials (teacher-managed, hideable). +9. FAQ accordion (teacher-managed, hideable). +10. Pricing card + footer. +- Enroll: free path → enroll flow like `/course/{slug}/enroll`; paid path → payment flow with the path as product. Requires auth like course enrollment. +- Visitor access level (teaser / syllabus / syllabus+preview) gates sections 5's lesson detail. +- JSON-LD `Course`/`LearningResource` schema like the course page. + +### 2. Learner — LMS + +**Home** (`lms-home.html`): "Learning Paths" section above Courses when `enrolledPaths.length > 0`; path cards show segmented per-course progress (one segment per course: done/current/locked) + overall %, and "Next: {course}". Course cards belonging to a path carry a "{path} · i/N" chip. + +**Nav**: "Learning Paths" item in the LMS sidebar with enrolled count badge, shown only when enrolled in ≥1 path. + +**List `/lms/paths`** (`paths.html`): tabs In progress / Completed / All; rows with segment bar, next course, Continue / View certificate. + +**Path hub `/lms/paths/[id]`** (`path-detail.html`): the journey spine — a vertical rail of course nodes that fills as courses complete. Per course: state (Completed / In progress / Locked), progress bar, lesson+exercise counts, unlock note ("Unlocks after Course N"), certificate end-row. + +**Course in path** (`course-in-path.html`): when the visited course belongs to a path the learner is in, wrap the course view with a path ribbon — back pill to the path, "Course 3 of 5", overall path %, horizontal stepper (done/current/locked) — and a "Next in your path" callout under the lesson showing what completing this course unlocks. + +**Unlock enforcement**: locked courses are not openable from path surfaces; direct navigation to a locked course (URL) shows a "locked — finish {course} first" state. Learners enrolled in a course independently (outside the path) keep their normal access. + +### 3. Teacher — admin dashboard + +**Org listing `/org/[slug]/paths`** (`teacher-paths.html`): nav item "Learning Paths" after Courses; stat cards (active paths, enrolled learners, completions); table (name, courses, learners, status); Create path → creation modal → setup checklist. + +**Setup checklist `/paths/[id]/setup`** (`teacher-path-setup.html`): Get Started pattern (ring + dots + item rows): name/describe ✓ auto, add courses, set order, set price, customize landing page, activate. Each step deep-links to its tab. Shown until complete; "Finish setup · N%" chip in the workspace top bar. + +**Path workspace `/paths/[id]/*`** — course-style sidebar (back link, path identity + status, tabs): + +| Tab | File | Contents | +| --- | --- | --- | +| Courses | `teacher-path-builder.html` | "Unlock courses in order" toggle; drag-to-reorder course rows (order, thumb, lessons/exercises/price, open/remove); add-course picker (org courses not in path); certificate end-row. Remove-course confirmation notes nobody is unenrolled. | +| People | `teacher-path-people.html` | Auto-enroll info banner; search + progress filter; table: learner, path progress bar, current course, enrolled date, actions (view, remove — preserves course access). Add learners (batch email, like course invites). | +| Analytics | `teacher-path-analytics.html` | Stat cards (enrolled, active, completion rate, avg time); per-course funnel with drop-off callouts; "stuck" list (lessons/exercises blocking most learners). | +| Landing page | `teacher-path-landing-editor.html` | Hero headline/subheadline; visitor access radio (teaser / syllabus / syllabus+previews); what-you'll-learn bullets; skills tags; instructors toggle (auto from course tutors); testimonials CRUD + toggle; FAQ CRUD + toggle; "View live page". | +| Certificate | `teacher-path-certificate.html` | Award toggle; requirements summary (all lessons + all exercises); title + issuer fields; live preview; awarded count. | +| Settings | `teacher-path-settings.html` | General (name, description, cover); Pricing (bundle price, currency, show-savings toggle with computed sum); Enrollment & flow (self-enrollment toggle, unlock-in-order toggle, auto-enroll toggle); Status radio (Active / Draft / Archived); Danger zone (delete path — courses & progress preserved). | + +### 4. Access control + +- Org admins manage all paths; path-level TUTOR role can manage content but not delete (mirror Programs roles: ADMIN/TUTOR/STUDENT via `roleId`). +- Students: read path data they're members of; public endpoints serve ACTIVE paths only. +- DRAFT paths: admin/tutor only. ARCHIVED: hidden everywhere, data preserved. + +--- + +## Technical Design + +### Data model (`packages/db/src/schema.ts`) + +``` +learning_path + id uuid PK · organizationId FK(organization, cascade) · name varchar · slug varchar (unique per org) + description text · coverImage text · status LEARNING_PATH_STATUS default 'DRAFT' + cost numeric · currency varchar default 'USD' · showSavings boolean default true + sequentialUnlock boolean default true · selfEnrollment boolean default true · autoEnroll boolean default true + certificateEnabled boolean default true · certificateTitle text · certificateIssuer text + landingPage jsonb -- { headline, subheadline, visitorAccess: 'teaser'|'syllabus'|'preview', + -- outcomes: string[], skills: string[], showInstructors, testimonials: [...], + -- showTestimonials, faqs: [...], showFaqs } + visitorAccess is inside landingPage; no relational IDs inside the jsonb (per repo rule) + createdByProfileId FK(profile) · createdAt / updatedAt + +learning_path_course + id uuid PK · pathId FK(learning_path, cascade) · courseId FK(course, cascade) + order integer NOT NULL -- 1-based position; the gating sequence + unique(pathId, courseId) · unique(pathId, order) · index(pathId) + +learning_path_member + id uuid PK · pathId FK(learning_path, cascade) · profileId FK(profile) · roleId FK(role) + enrolledAt timestamptz · completedAt timestamptz nullable + certificateIssuedAt timestamptz nullable · certificateId varchar nullable + unique(pathId, profileId) · index(pathId) · index(profileId) + +enum LEARNING_PATH_STATUS: ACTIVE | DRAFT | ARCHIVED +``` + +Notes: +- No denormalized progress on `learning_path_member` beyond `completedAt`; per-course progress is computed from existing lesson-completion + exercise-submission data (already queried for course cards). +- Savings figure is computed at read time from the sum of `course.cost` over path courses — never stored. +- Schema work stops at a passing `@cio/db` build; migrations are handled outside this workflow. + +### Completion / unlock logic (service layer) + +``` +courseCompleteForPath(profileId, courseId): + lessonsComplete = completedLessons == totalLessons + exercisesComplete = submittedOrPassedExercises == totalExercises + return lessonsComplete && exercisesComplete + +unlockedCourses(path, profileId): + if !path.sequentialUnlock → all + else → courses[0..k] where k = first index whose course is not complete +``` + +Path completion: all courses complete → set `completedAt`; if `certificateEnabled`, issue certificate (id `LP-XXXX-XXXX`), set `certificateIssuedAt`. Evaluate on lesson/exercise completion events for members of paths containing that course (reuse the pattern used by course-completion side effects). + +### API routes (`apps/api/src/routes/learning-path/`) + +Follow the standard layering (validation in `packages/utils/src/validation/learning-path/`, queries in `packages/db/src/queries/learning-path/`, services in `apps/api/src/services/learning-path/`). Mount as a single root segment: `.route('/learning-path', learningPathRouter)`. + +| Method | Path | Auth | Description | +| --- | --- | --- | --- | +| GET | `/learning-path` | org admin | List org paths (admin) | +| POST | `/learning-path` | org admin | Create (name, description) → DRAFT | +| GET | `/learning-path/:pathId` | member/admin | Detail incl. ordered courses + caller progress | +| PUT | `/learning-path/:pathId` | admin/tutor | Update settings/pricing/landingPage/status | +| DELETE | `/learning-path/:pathId` | org admin | Delete (courses/progress preserved) | +| POST | `/learning-path/:pathId/courses` | admin/tutor | Add course (+ auto-enroll existing students) | +| PUT | `/learning-path/:pathId/courses/order` | admin/tutor | Reorder (array of courseIds) | +| DELETE | `/learning-path/:pathId/courses/:courseId` | admin/tutor | Remove (no unenrollment) | +| GET | `/learning-path/:pathId/members` | admin/tutor | Members + per-course progress | +| POST | `/learning-path/:pathId/members` | admin/tutor | Batch add (email+role) + auto-enroll | +| DELETE | `/learning-path/:pathId/members/:memberId` | admin/tutor | Remove (course access preserved) | +| GET | `/learning-path/:pathId/analytics` | admin/tutor | Funnel + stuck items | +| POST | `/learning-path/:pathId/enroll` | auth user | Self-enroll (free) / post-payment callback (paid) | +| GET | `/organization/learning-paths/enrolled` | auth user | LMS: caller's paths with progress + unlock states | + +Public (org-site loaders, no auth): list ACTIVE paths for landing/catalog; get path by slug with landingPage content filtered by `visitorAccess`. + +### Frontend plan (dashboard) + +Follow CLAUDE.md conventions: types in `features/learning-path/utils/types.ts` inferred from the API, API classes in `features/learning-path/api/*.svelte.ts`, thin components, all copy in `en.json` under `"learningPath"`, `ui:` prefix for theme colors. + +- **Admin**: nav item in `org-navigation.ts` after Courses; routes `org/[slug]/paths/+page.svelte` and `paths/[id]/{setup,courses,people,analytics,landing,certificate,settings}` with a `PathSidebar` mirroring the course sidebar pattern. Reuse `Item.*`, `PercentRingProgress`, `Field.*`, existing drag-reorder approach from lesson ordering if present. +- **LMS**: `lms/paths` + `lms/paths/[id]`; path ribbon injected in the course layout when course ∈ caller's path; reuse `course-progress-card` math for per-course %. +- **Public**: paths section added to org landing themes (start `minimal`), `(org-site)/paths` and `(org-site)/path/[slug]` mirroring the courses equivalents; reuse `CourseSectionNav`, `CourseSocialProof`, `CourseCurriculum`-style rows, `CoursePricing` card, `LandingButton`, footer. + +### Build verification + +```bash +pnpm --filter @cio/utils build && pnpm --filter @cio/db build && pnpm --filter @cio/api build +pnpm --filter @cio/dashboard^... build && pnpm --filter @cio/dashboard build +pnpm format:check +``` + +--- + +## Implementation Order + +1. **Schema + validation + queries** (`learning_path*` tables, Zod schemas, query layer). +2. **API**: CRUD + course ordering + members + enroll service (auto-enroll transaction, `ON CONFLICT DO NOTHING` idempotency) + unlock/completion service + enrolled-paths endpoint. +3. **Admin UI**: listing → workspace shell + Courses tab → Settings → People → setup checklist → Landing editor → Certificate → Analytics. +4. **LMS**: enrolled section on home + nav, list, path hub (pick journey or checklist), course-in-path ribbon + lock enforcement. +5. **Public**: landing section (minimal theme) → catalog → detail page → enroll flow (free first, then paid via existing payment flow). +6. **Certificate issuance** + LMS Certificates page integration. + +## Acceptance Criteria + +1. Teacher can create a path, add + reorder courses, set a bundle price, and activate it; the setup checklist reflects real completion. +2. ACTIVE paths appear on the org landing page above courses, in `/paths`, and at `/path/[slug]` with all enabled sections; DRAFT/ARCHIVED paths do not. +3. Savings displays as bundle price vs summed course prices and updates when course prices change. +4. Visitor access level correctly gates lesson outlines/previews for non-enrolled visitors. +5. Enrolling (free or paid) creates the member row and `groupmember` rows for all path courses, idempotently. +6. With sequential unlock on, course N+1 is locked until course N's lessons AND exercises are complete — enforced in UI and API; toggle off restores free order. +7. Learner LMS shows path progress (segments + %), locked states, and the in-course path ribbon; direct URL access to a locked course is blocked with an explanatory state. +8. Completing all courses sets `completedAt` and (when enabled) issues the path certificate, visible in LMS Certificates. +9. Removing a course from a path or a member from a path never deletes course enrollments or progress. +10. Adding a course to a path with existing students auto-enrolls them in that course. +11. Analytics funnel counts match member course-completion data. +12. Programs and standalone courses behave exactly as before. +13. All user-facing strings use translation keys; all builds and `pnpm format:check` pass. + +## Risks and Mitigations + +- **Unlock-rule evaluation cost** (checked on every lesson/exercise completion): scope the check to paths containing that course with the learner as member; index `learning_path_course.courseId`. +- **Auto-enroll transaction size** for large paths/cohorts: batch inserts with `ON CONFLICT DO NOTHING`; >500 members → background job (same mitigation as Programs). +- **Price drift**: savings computed at read time, so course price changes are always reflected; if a course in an active path becomes free/pricier, surface a notice in the Settings pricing section. +- **Learner already enrolled in a member course independently**: idempotent enrollment; their existing progress counts toward the unlock rule immediately (decide: acceptable and desirable). +- **Locked-course deep links** shared between learners: API must enforce the lock (not just UI), returning a typed error the frontend renders as the locked state. +- **Landing theme sprawl** (10 themes): v1 ships the paths section/pages for the base + `minimal` theme with a neutral fallback for others, mirroring how course landing tokens are layered. diff --git a/prototypes/learning-paths/app-theme.css b/prototypes/learning-paths/app-theme.css new file mode 100644 index 00000000..50449607 --- /dev/null +++ b/prototypes/learning-paths/app-theme.css @@ -0,0 +1,1178 @@ +/* + * app-theme.css — shared theme for the learning-paths prototypes. + * + * Tokens mirror packages/ui/src/index.css (shadcn-svelte) exactly: + * OKLCH palette, Geist, --radius: 0.625rem, shadow-2xs, and the component + * recipes from @cio/ui base components (Button, Badge, Item, Progress, Sidebar). + * Dark mode uses the same `.dark` class convention as the app. + */ + +:root { + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.488 0.243 264.376); + --primary-foreground: oklch(0.97 0.014 254.604); + --secondary: oklch(0.967 0.001 286.375); + --secondary-foreground: oklch(0.21 0.006 285.885); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); + --radius: 0.625rem; + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + + /* Semantic states from @cio/ui Badge variants (emerald / amber) */ + --success: oklch(0.596 0.145 163.225); /* emerald-600 */ + --success-foreground: #fff; + --success-soft: oklch(0.596 0.145 163.225 / 0.12); + --warning: oklch(0.666 0.179 58.318); /* amber-600 */ + --warning-foreground: #fff; + --warning-soft: oklch(0.666 0.179 58.318 / 0.14); + + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + + --shadow-2xs: 0 1px 2px 0 rgb(0 0 0 / 0.05); + --shadow-sm: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); + --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); +} + +.dark { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.205 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.424 0.199 265.638); + --primary-foreground: oklch(0.97 0.014 254.604); + --secondary: oklch(0.274 0.006 286.033); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.556 0 0); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + + --success: oklch(0.696 0.17 162.48); /* emerald-500 */ + --success-foreground: oklch(0.262 0.051 172.552); /* emerald-950 */ + --success-soft: oklch(0.696 0.17 162.48 / 0.16); + --warning: oklch(0.769 0.188 70.08); /* amber-500 */ + --warning-foreground: oklch(0.279 0.077 45.635); /* amber-950 */ + --warning-soft: oklch(0.769 0.188 70.08 / 0.16); +} + +/* ============ base ============ */ +* { + box-sizing: border-box; + border-color: var(--border); +} +html, +body { + margin: 0; + padding: 0; +} +body { + font-family: + 'Geist', + ui-sans-serif, + system-ui, + -apple-system, + 'Segoe UI', + Roboto, + sans-serif; + background: var(--background); + color: var(--foreground); + font-size: 14px; + line-height: 1.5; + -webkit-font-smoothing: antialiased; +} +a { + color: inherit; + text-decoration: none; +} +button { + font: inherit; +} +.tnum { + font-variant-numeric: tabular-nums; +} +.text-muted { + color: var(--muted-foreground); +} +.text-primary { + color: var(--primary); +} + +/* ============ app shell ============ */ +.app { + display: grid; + grid-template-columns: 256px 1fr; + min-height: 100vh; +} +.main { + display: flex; + flex-direction: column; + min-width: 0; +} +.content { + padding: 28px 28px 64px; +} +.wrap { + max-width: 960px; + margin: 0 auto; +} +.wrap-narrow { + max-width: 760px; + margin: 0 auto; +} + +/* sidebar (mirrors @cio/ui base/sidebar) */ +.sidebar { + background: var(--sidebar); + border-right: 1px solid var(--sidebar-border); + padding: 16px 12px; + display: flex; + flex-direction: column; + gap: 2px; + position: sticky; + top: 0; + height: 100vh; +} +.brand { + display: flex; + align-items: center; + gap: 8px; + padding: 4px 8px 14px; +} +.brand-mark { + width: 28px; + height: 28px; + border-radius: var(--radius-md); + background: var(--primary); + color: var(--primary-foreground); + display: grid; + place-items: center; + font-weight: 700; + font-size: 14px; +} +.brand-name { + font-weight: 600; + font-size: 14px; + letter-spacing: -0.01em; +} +.nav-label { + font-size: 11px; + color: var(--muted-foreground); + font-weight: 500; + padding: 12px 8px 4px; +} +.nav-item { + display: flex; + align-items: center; + gap: 10px; + padding: 8px; + border-radius: var(--radius-md); + color: var(--sidebar-foreground); + font-weight: 400; + font-size: 14px; + transition: background 0.1s; +} +.nav-item svg { + width: 16px; + height: 16px; + stroke-width: 2; + color: var(--muted-foreground); + flex-shrink: 0; +} +.nav-item:hover { + background: var(--sidebar-accent); +} +.nav-item.active { + background: var(--sidebar-accent); + color: var(--sidebar-accent-foreground); + font-weight: 500; +} +.nav-item.active svg { + color: var(--sidebar-accent-foreground); +} +.nav-tail { + margin-left: auto; + font-size: 12px; + color: var(--muted-foreground); +} +.nav-badge { + margin-left: auto; + font-size: 11px; + font-weight: 600; + background: var(--primary); + color: var(--primary-foreground); + border-radius: 999px; + padding: 1px 7px; +} +.sidebar-foot { + margin-top: auto; + display: flex; + align-items: center; + gap: 10px; + padding: 8px; + border-top: 1px solid var(--sidebar-border); +} +.avatar { + width: 30px; + height: 30px; + border-radius: 999px; + background: var(--secondary); + color: var(--secondary-foreground); + display: grid; + place-items: center; + font-weight: 600; + font-size: 12px; + flex-shrink: 0; +} +.who { + min-width: 0; +} +.who .nm { + font-size: 13px; + font-weight: 500; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.who .rl { + font-size: 11.5px; + color: var(--muted-foreground); +} + +/* path identity block (teacher path sidebar) */ +.back-link { + display: flex; + align-items: center; + gap: 8px; + font-size: 13px; + color: var(--muted-foreground); + padding: 6px 8px; + border-radius: var(--radius-md); + margin-bottom: 4px; +} +.back-link:hover { + background: var(--sidebar-accent); + color: var(--foreground); +} +.back-link svg { + width: 15px; + height: 15px; + stroke-width: 2; +} +.path-id { + display: flex; + align-items: center; + gap: 10px; + padding: 8px 8px 14px; + border-bottom: 1px solid var(--sidebar-border); + margin-bottom: 6px; +} +.path-id .ico { + width: 32px; + height: 32px; + border-radius: var(--radius-md); + background: var(--primary); + color: var(--primary-foreground); + display: grid; + place-items: center; + flex-shrink: 0; +} +.path-id .ico svg { + width: 16px; + height: 16px; + stroke-width: 2; +} +.path-id .nm { + font-size: 13px; + font-weight: 600; + line-height: 1.3; + letter-spacing: -0.01em; +} +.path-id .st { + font-size: 11.5px; + font-weight: 500; + display: flex; + align-items: center; + gap: 5px; + margin-top: 2px; +} +.path-id .st .dot { + width: 6px; + height: 6px; + border-radius: 999px; +} +.st-active { + color: var(--success); +} +.st-active .dot { + background: var(--success); +} +.st-draft { + color: var(--warning); +} +.st-draft .dot { + background: var(--warning); +} + +/* topbar */ +.topbar { + height: 56px; + border-bottom: 1px solid var(--border); + background: color-mix(in oklab, var(--background) 90%, transparent); + backdrop-filter: blur(8px); + position: sticky; + top: 0; + z-index: 100; + display: flex; + align-items: center; + gap: 12px; + padding: 0 24px; +} +.crumbs { + display: flex; + align-items: center; + gap: 8px; + font-size: 13.5px; + color: var(--muted-foreground); + min-width: 0; +} +.crumbs a:hover { + color: var(--foreground); +} +.crumbs .sep { + color: var(--border); +} +.crumbs .here { + color: var(--foreground); + font-weight: 500; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.top-title { + font-weight: 600; + font-size: 15px; + letter-spacing: -0.01em; +} +.top-actions { + margin-left: auto; + display: flex; + align-items: center; + gap: 8px; +} + +/* ============ buttons (mirrors @cio/ui base/button) ============ */ +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + border-radius: var(--radius-md); + font-size: 14px; + font-weight: 500; + white-space: nowrap; + height: 36px; + padding: 0 16px; + border: 1px solid transparent; + cursor: pointer; + transition: + background 0.12s, + border-color 0.12s, + color 0.12s; + box-shadow: var(--shadow-2xs); + user-select: none; +} +.btn svg { + width: 16px; + height: 16px; + stroke-width: 2; + flex-shrink: 0; +} +.btn-primary { + background: var(--primary); + color: var(--primary-foreground); +} +.btn-primary:hover { + background: color-mix(in oklab, var(--primary) 90%, transparent); +} +.btn-outline { + background: var(--background); + border-color: var(--input); + color: var(--foreground); +} +.btn-outline:hover { + background: var(--accent); + color: var(--accent-foreground); +} +.btn-secondary { + background: var(--secondary); + color: var(--secondary-foreground); +} +.btn-secondary:hover { + background: color-mix(in oklab, var(--secondary) 80%, transparent); +} +.btn-ghost { + background: transparent; + box-shadow: none; + color: var(--foreground); +} +.btn-ghost:hover { + background: var(--accent); +} +.btn-destructive { + background: var(--destructive); + color: #fff; +} +.btn-success { + background: var(--success); + color: var(--success-foreground); +} +.btn-sm { + height: 32px; + padding: 0 12px; + font-size: 13px; + gap: 6px; +} +.btn-sm svg { + width: 15px; + height: 15px; +} +.btn-lg { + height: 40px; + padding: 0 24px; +} +.btn[disabled], +.btn.disabled { + opacity: 0.5; + pointer-events: none; + cursor: not-allowed; +} +.btn-icon { + width: 36px; + padding: 0; +} +.btn-icon.btn-sm { + width: 32px; +} + +/* ============ badges (mirrors @cio/ui base/badge) ============ */ +.badge { + display: inline-flex; + align-items: center; + gap: 4px; + width: fit-content; + border-radius: var(--radius-md); + border: 1px solid transparent; + padding: 2px 8px; + font-size: 12px; + font-weight: 500; + white-space: nowrap; +} +.badge svg { + width: 12px; + height: 12px; + stroke-width: 2.4; +} +.badge-default { + background: var(--primary); + color: var(--primary-foreground); +} +.badge-secondary { + background: var(--secondary); + color: var(--secondary-foreground); +} +.badge-success { + background: var(--success); + color: var(--success-foreground); +} +.badge-warning { + background: var(--warning); + color: var(--warning-foreground); +} +.badge-outline { + border-color: var(--border); + color: var(--foreground); + background: transparent; +} +.badge-success-soft { + background: var(--success-soft); + color: var(--success); +} +.badge-primary-soft { + background: color-mix(in oklab, var(--primary) 12%, transparent); + color: var(--primary); +} +.badge-muted { + background: var(--muted); + color: var(--muted-foreground); +} + +/* ============ cards ============ */ +.card { + background: var(--card); + color: var(--card-foreground); + border: 1px solid var(--border); + border-radius: var(--radius-xl); + box-shadow: var(--shadow-2xs); +} +.card-pad { + padding: 20px; +} + +/* ============ item rows (mirrors @cio/ui base/item) ============ */ +.item { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 16px; + border-radius: var(--radius-md); + border: 1px solid transparent; + padding: 16px; + font-size: 14px; + transition: + background 0.1s, + border-color 0.1s; +} +.item-muted { + background: color-mix(in oklab, var(--muted) 50%, transparent); +} +.item-outline { + border-color: var(--border); +} +.item-done { + opacity: 0.6; +} +.item-done .item-title, +.item-done .item-desc { + color: var(--muted-foreground); + text-decoration: line-through; +} +.item-current { + background: var(--card); + border-color: var(--primary); + box-shadow: var(--shadow-sm); +} +.item-dashed { + border: 1px dashed var(--border); + background: transparent; +} +a.item:hover { + background: color-mix(in oklab, var(--accent) 50%, transparent); +} + +.item-media-icon { + width: 32px; + height: 32px; + border-radius: var(--radius-sm); + background: var(--muted); + border: 1px solid var(--border); + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + align-self: flex-start; + margin-top: 2px; + color: var(--muted-foreground); +} +.item-media-icon svg { + width: 16px; + height: 16px; + stroke-width: 2; +} +.item-media-icon.is-done { + background: var(--success); + border-color: var(--success); + color: var(--success-foreground); +} +.item-media-icon.is-current { + background: color-mix(in oklab, var(--primary) 10%, transparent); + border-color: color-mix(in oklab, var(--primary) 30%, transparent); + color: var(--primary); +} +.item-media-lg { + width: 40px; + height: 40px; +} +.item-media-lg svg { + width: 19px; + height: 19px; +} +.item-content { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + gap: 2px; +} +.item-title { + font-size: 14px; + font-weight: 500; + line-height: 1.4; + letter-spacing: -0.01em; +} +.item-title-lg { + font-size: 15px; + font-weight: 600; +} +.item-desc { + font-size: 13px; + color: var(--muted-foreground); + line-height: 1.45; +} +.item-actions { + display: flex; + align-items: center; + gap: 8px; + flex-shrink: 0; +} +.item-eyebrow { + font-size: 11px; + font-weight: 600; + letter-spacing: 0.05em; + text-transform: uppercase; + color: var(--muted-foreground); +} + +/* ============ progress (mirrors @cio/ui base/progress) ============ */ +.progress { + height: 8px; + border-radius: var(--radius-md); + overflow: hidden; + background: color-mix(in oklab, var(--primary) 20%, transparent); + flex: 1; +} +.progress > span { + display: block; + height: 100%; + background: var(--primary); + border-radius: var(--radius-md); + transition: width 0.3s; +} +.progress-success > span { + background: var(--success); +} +.progress-thin { + height: 6px; +} + +/* progress ring (mirrors @cio/ui custom/percent-ring-progress) */ +.ring { + position: relative; + width: 56px; + height: 56px; + flex-shrink: 0; +} +.ring-sm { + width: 40px; + height: 40px; +} +.ring svg { + width: 100%; + height: 100%; + transform: rotate(-90deg); + display: block; +} +.ring .track { + stroke: var(--border); +} +.ring .arc { + stroke: var(--success); + transition: stroke-dashoffset 0.7s ease-out; +} +.ring .lbl { + position: absolute; + inset: 0; + display: grid; + place-items: center; + font-size: 12.5px; + font-weight: 600; + color: var(--foreground); +} +.ring-sm .lbl { + font-size: 10px; +} + +/* completion dots (setup page pattern) */ +.dots { + display: flex; + gap: 4px; +} +.dot { + width: 8px; + height: 8px; + border-radius: 999px; + background: var(--border); +} +.dot.on { + background: var(--success); +} + +/* ============ forms ============ */ +.input, +.textarea, +.select { + width: 100%; + font: inherit; + font-size: 14px; + color: var(--foreground); + background: transparent; + border: 1px solid var(--input); + border-radius: var(--radius-md); + padding: 7px 12px; + outline: none; + box-shadow: var(--shadow-2xs); + transition: + border-color 0.12s, + box-shadow 0.12s; +} +.input:focus, +.textarea:focus { + border-color: var(--ring); + box-shadow: 0 0 0 3px color-mix(in oklab, var(--ring) 50%, transparent); +} +.textarea { + resize: vertical; + min-height: 76px; + line-height: 1.55; +} +.label { + display: block; + font-size: 13px; + font-weight: 500; + margin-bottom: 6px; +} +.hint { + font-size: 12px; + color: var(--muted-foreground); + margin-top: 5px; +} +.field { + margin-bottom: 18px; +} +.field:last-child { + margin-bottom: 0; +} + +/* switch */ +.switch { + position: relative; + width: 36px; + height: 20px; + flex-shrink: 0; + cursor: pointer; + display: inline-block; +} +.switch input { + position: absolute; + opacity: 0; + inset: 0; + margin: 0; + cursor: pointer; + z-index: 1; +} +.switch .track { + position: absolute; + inset: 0; + background: var(--input); + border-radius: 999px; + transition: background 0.15s; +} +.switch .knob { + position: absolute; + top: 2px; + left: 2px; + width: 16px; + height: 16px; + border-radius: 999px; + background: var(--background); + box-shadow: var(--shadow-sm); + transition: transform 0.15s; +} +.switch input:checked + .track { + background: var(--primary); +} +.switch input:checked ~ .knob { + transform: translateX(16px); +} + +/* ============ tables ============ */ +.thead, +.trow { + display: grid; + align-items: center; + gap: 14px; + padding: 12px 16px; +} +.thead { + font-size: 12px; + color: var(--muted-foreground); + font-weight: 500; + border-bottom: 1px solid var(--border); +} +.trow { + border-bottom: 1px solid var(--border); + font-size: 14px; +} +.trow:last-child { + border-bottom: none; +} +a.trow:hover { + background: color-mix(in oklab, var(--accent) 50%, transparent); +} + +/* ============ misc components ============ */ +.icon-btn { + width: 32px; + height: 32px; + border-radius: var(--radius-md); + border: 1px solid var(--input); + background: var(--background); + display: grid; + place-items: center; + color: var(--muted-foreground); + cursor: pointer; + box-shadow: var(--shadow-2xs); +} +.icon-btn:hover { + color: var(--foreground); + background: var(--accent); +} +.icon-btn svg { + width: 15px; + height: 15px; + stroke-width: 2; +} + +.seg-toggle { + display: inline-flex; + padding: 3px; + background: var(--muted); + border-radius: var(--radius-lg); + gap: 2px; +} +.seg-toggle a, +.seg-toggle button { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 5px 12px; + border-radius: var(--radius-md); + border: none; + background: transparent; + font-size: 13px; + font-weight: 500; + color: var(--muted-foreground); + cursor: pointer; +} +.seg-toggle .on { + background: var(--background); + color: var(--foreground); + box-shadow: var(--shadow-2xs); +} +.seg-toggle svg { + width: 14px; + height: 14px; + stroke-width: 2; +} + +.stat-card { + padding: 16px 18px; +} +.stat-card .k { + font-size: 13px; + color: var(--muted-foreground); + font-weight: 500; + display: flex; + align-items: center; + gap: 7px; +} +.stat-card .k svg { + width: 15px; + height: 15px; + stroke-width: 2; +} +.stat-card .v { + font-size: 26px; + font-weight: 600; + letter-spacing: -0.02em; + margin-top: 4px; +} +.stat-card .s { + font-size: 12px; + color: var(--muted-foreground); + margin-top: 2px; +} + +/* page headers */ +.page-head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 20px; + margin: 8px 0 20px; +} +.page-title { + font-size: 24px; + font-weight: 400; + letter-spacing: -0.025em; + margin: 0 0 4px; +} +.page-sub { + color: var(--muted-foreground); + font-size: 14px; + margin: 0; +} +.sec-title { + font-size: 16px; + font-weight: 600; + letter-spacing: -0.01em; + margin: 0; +} +.sec-head { + display: flex; + align-items: center; + gap: 10px; + margin: 28px 0 14px; +} +.sec-head .count { + font-size: 12px; + font-weight: 500; + color: var(--muted-foreground); + background: var(--muted); + padding: 1px 8px; + border-radius: 999px; +} +.sec-head .more { + margin-left: auto; + font-size: 13.5px; + color: var(--primary); + font-weight: 500; + display: flex; + align-items: center; + gap: 4px; +} +.sec-head .more svg { + width: 14px; + height: 14px; + stroke-width: 2; +} + +/* segmented path progress (course-count segments) */ +.segs { + display: flex; + gap: 4px; + flex: 1; +} +.seg { + flex: 1; + height: 6px; + border-radius: 999px; + background: color-mix(in oklab, var(--primary) 15%, transparent); + position: relative; + overflow: hidden; +} +.seg.done { + background: var(--success); +} +.seg.half::after { + content: ''; + position: absolute; + inset: 0; + width: 45%; + background: var(--primary); + border-radius: 999px; +} + +/* lock veil for thumbnails */ +.lock-veil { + position: absolute; + inset: 0; + background: oklch(0.145 0 0 / 0.35); + display: grid; + place-items: center; +} +.lock-veil svg { + width: 18px; + height: 18px; + color: #fff; + stroke-width: 2.2; +} + +/* ============ journey spine (learner path detail) ============ */ +.journey { + position: relative; + margin-top: 16px; +} +.step { + position: relative; + display: grid; + grid-template-columns: 36px 1fr; + gap: 16px; + padding-bottom: 16px; +} +.step:last-child { + padding-bottom: 0; +} +.rail { + position: relative; + display: flex; + justify-content: center; +} +.rail::before { + content: ''; + position: absolute; + top: 38px; + bottom: -16px; + width: 2px; + background: var(--border); + border-radius: 2px; +} +.step:last-child .rail::before, +.step.no-rail .rail::before { + display: none; +} +.step.s-done .rail::before { + background: var(--success); +} +.step.s-current .rail::before { + background: linear-gradient(180deg, var(--success) 0%, var(--border) 70%); +} +.node { + width: 36px; + height: 36px; + border-radius: 999px; + z-index: 1; + display: grid; + place-items: center; + font-weight: 600; + font-size: 13px; + background: var(--muted); + color: var(--muted-foreground); + border: 1px solid var(--border); +} +.node svg { + width: 17px; + height: 17px; + stroke-width: 2.4; +} +.step.s-done .node { + background: var(--success); + border-color: var(--success); + color: var(--success-foreground); +} +.step.s-current .node { + background: var(--primary); + border-color: var(--primary); + color: var(--primary-foreground); + box-shadow: 0 0 0 4px color-mix(in oklab, var(--primary) 15%, transparent); +} + +/* horizontal stepper (in-course ribbon) */ +.stepper { + display: flex; + align-items: center; +} +.snode { + display: flex; + flex-direction: column; + align-items: center; + gap: 5px; + flex-shrink: 0; + width: 110px; + text-align: center; +} +.sdot { + width: 28px; + height: 28px; + border-radius: 999px; + display: grid; + place-items: center; + font-weight: 600; + font-size: 12px; + background: var(--muted); + color: var(--muted-foreground); + border: 1px solid var(--border); +} +.sdot svg { + width: 14px; + height: 14px; + stroke-width: 2.6; +} +.snode.done .sdot { + background: var(--success); + border-color: var(--success); + color: var(--success-foreground); +} +.snode.current .sdot { + background: var(--primary); + border-color: var(--primary); + color: var(--primary-foreground); + box-shadow: 0 0 0 3px color-mix(in oklab, var(--primary) 15%, transparent); +} +.slabel { + font-size: 11px; + font-weight: 500; + color: var(--muted-foreground); + line-height: 1.25; +} +.snode.current .slabel { + color: var(--foreground); + font-weight: 600; +} +.sbar { + flex: 1; + height: 2px; + background: var(--border); + min-width: 12px; + margin: 0 -14px 18px; +} +.sbar.filled { + background: var(--success); +} + +/* ============ responsive ============ */ +@media (max-width: 860px) { + .app { + grid-template-columns: 1fr; + } + .sidebar { + display: none; + } + .item { + flex-wrap: wrap; + } + .item-actions { + width: 100%; + } + .item-actions .btn { + flex: 1; + } +} diff --git a/prototypes/learning-paths/course-in-path.html b/prototypes/learning-paths/course-in-path.html new file mode 100644 index 00000000..442fb921 --- /dev/null +++ b/prototypes/learning-paths/course-in-path.html @@ -0,0 +1,304 @@ + + +
+ + +In React you respond to user interaction by passing event handlers as props to elements. Handlers are regular functions, and React normalizes the underlying browser event for you.
+onClick, onChange, and onSubmit handlersBy the end of this lesson you'll wire up a small form that updates the screen as the user types — the foundation for everything interactive you'll build in this path.
+
+ A learning path is an ordered bundle of courses sold and taken as one:
+ courses unlock sequentially (lessons + exercises complete → next course), one bundle price with savings,
+ and an optional path certificate at the end. Built on the real packages/ui tokens.
+ The engineering PRD lives at prd/learning-paths/README.md.
+
Every page has a theme toggle and cross-links so you can click through each journey end to end.
+ + +Learning Paths section above Courses — mirrors the "minimal" landing theme.
/paths — search, filters, and path cards with savings pricing (like /courses).
Hero + enroll box with savings, stats, outcomes, skills, course series with lesson outlines, certificate, instructors, testimonials, FAQ.
Paths above courses, segmented cross-course progress bars.
In progress / Completed tabs; each row shows segment progress + next course.
The journey spine — Done / Continue / Locked courses with per-course progress and a certificate end-row.
Lesson view wrapped in path chrome: stepper ribbon + "next course unlocks" callout.
Org-level listing: stats, statuses, Create path → setup checklist.
"Get Started"-style checklist: courses → order → price → landing page → activate.
Drag-to-reorder course list + sequential-unlock toggle. Try dragging rows.
Per-learner path progress, current course, auto-enroll note, add learners.
Course-by-course funnel, drop-off callouts, stuck lessons/exercises.
Hero copy, visitor access level, outcomes, skills, instructors, testimonials, FAQ.
Award toggle, completion requirements, live certificate preview.
General, bundle pricing + savings, enrollment & unlock rules, status, danger zone.
learning_path, learning_path_course (ordered), learning_path_member.packages/ui (shadcn-svelte, Geist, OKLCH tokens).
+ Pick up where you left off — you're 49% through your bootcamp.
+ + + +From HTML foundations to shipping a production React app.
+Pandas, visualization, and applied machine-learning basics.
+Components, hooks, and building interactive UIs.
+Semantic markup, the box model, and responsive layout.
+A standalone course on the model-view-controller pattern.
+Syntax, data types, and your first scripts.
+Courses and guided learning paths in engineering, data, and product — built by practitioners, finished with certificates.
+ +Multi-course tracks you complete in order — one enrollment, one price, one certificate at the end.
+ +Five courses, one path — from your first HTML tag to a deployed React capstone. Learn in order, build real projects, earn a certificate.
+From Python basics to applied machine learning — pandas, visualization, and a real analysis project, in a fixed order that builds week over week.
+Standalone courses you can take in any order.
+ +Semantic markup, the box model, flexbox, and responsive layout basics for absolute beginners.
+ + + +Types, functions, the DOM, async/await, and fetching data from APIs — production JavaScript from day one.
+ + + +Components, props & state, hooks, and building interactive UIs with the most-used frontend library.
+ + + +A standalone deep dive on the model-view-controller pattern and where it still shines today.
+ + +A guided, five-course track that takes you from markup fundamentals to shipping a production React app. Each course unlocks the next — finish in order to earn your certificate.
+ +Semantic markup, the box model, flexbox, and responsive layout basics.
+Types, functions, the DOM, async/await, and fetching data from APIs.
+Components, props & state, hooks, and building interactive UIs.
+Context, reducers, caching, and connecting a React app to a backend.
+Plan, build, and deploy a full project — your portfolio piece.
+Structured tracks you're enrolled in. Complete the courses in order to finish a path and earn its certificate.
+From HTML foundations to shipping a production React app.
+Pandas, visualization, and applied machine-learning basics.
+Security awareness, data handling, and code of conduct.
+Five courses, one path — from your first HTML tag to a deployed React capstone. Learn in order, build real projects, earn a certificate.
+Practical, project-first skills — every course ends with something you built.
+Each course unlocks the next once you finish its lessons and exercises.
+ +Complete all five courses — every lesson and exercise — and receive the Frontend Engineering Bootcamp certificate, ready to add to LinkedIn and your CV.
+The tutors behind the courses in this path.
+From people who finished the path.
+"I went from zero to deploying my own React app in ten weeks. The order of the courses just makes sense — nothing assumed I knew things I didn't."
+"The locked courses kept me honest — no skipping ahead, and everything built on what came before. The certificate helped me land interviews."
+Guided multi-course tracks with one enrollment, ordered courses, and a certificate at the end.
+Five courses, one path — from your first HTML tag to a deployed React capstone. Learn in order, build real projects, earn a certificate.
+ + + + + Learning Path +From Python basics to applied machine learning — pandas, visualization, and a real analysis project, in a fixed order that builds week over week.
+ + + + + Learning Path +Discovery, prioritization, delivery, and stakeholder craft — a five-course track for new and aspiring PMs.
+ + +How learners are moving through the path.
+How many learners have reached and completed each course, in order.
+ +Lessons and exercises blocking the most learners right now.
+Add courses and drag to set the order students move through them.
+Each course stays locked until a student completes the previous one's lessons and exercises. Turn off to allow any order.
+Award a path-level certificate when a learner completes every course.
+Issued automatically on completion and shown in the learner's Certificates page.
Completion rules follow the path's unlock rule (lessons + exercises). 13 certificates awarded so far.
+What appears on the issued certificate.
This certifies that
+has successfully completed all 5 courses in the Frontend Engineering Bootcamp learning path.
+ +Control what visitors see on the public path page at /path/frontend-engineering-bootcamp.
+The headline visitors see first.
How much of the path a non-enrolled visitor can explore.
Outcome bullets shown near the top of the page.
Tags shown as "Skills you'll gain".
Pulled from the tutors of the courses in this path.
Learner quotes shown on the public page.
Collapsible questions at the bottom of the page.
48 learners enrolled in this path.
+Manage how this path appears, is priced, and behaves for learners.
+The name, summary, and cover learners see across the LMS.
One bundle price for the whole path. Enrolling unlocks every course.
How learners join and move through the path.
Controls whether learners can see and join this path.
Finish these steps to launch Frontend Engineering Bootcamp to your learners.
+Order the courses, set a price, polish the landing page, and activate.
+Group your courses into ordered tracks your learners complete step by step.
+