Learner Experience¶
Audience: Anyone designing or supporting the learner-facing surfaces of the LMS.
Scope: The end-to-end learner journey — discovery, account creation, enrolment, sequential entry playback, quiz attempts, dashboards. Authoritative behaviour lives in
reference/course-enrollment-system.md,reference/entry-pagination.md,reference/quiz-system.md, andreference/toggleable-navigation.md.
Discovery (Public)¶
Public learners (no account, no session) can:
- Browse the course catalogue at
/learn. - Open a public course landing at
/learn/course/\[slug\]— published metadata, preview-safe summary, sign-in / sign-up CTA. - Read CMS-authored marketing pages under
/\[slug\]and the blog under/blog.
generateStaticParams includes published courses only. Drafts, archived, and unlisted courses are not enumerated.
Account Creation¶
/learn/sign-up— email + password. Tenant is resolved from the request host.- A verification email is sent immediately. Account remains unverified until the recipient clicks the token link at
/learn/verify/\[token\]. - Magic-link sign-in (when the tenant
magicLinkfeature flag is enabled): from/learn/sign-in, follow Sign in with a magic link to/learn/magic, submit the account email, then open the one-time link at/learn/magic/\[token\](emailed in production; logged to the dev server terminal when using the console email driver — see Email Delivery). Details:development/auth.md. - Google OAuth (when enabled) auto-links by
(tenant_id, email)perdevelopment/auth.md.
Forgot-password is at /learn/forgot; reset is at /learn/reset/\[token\]. Both are rate-limited and never reveal whether an account exists.
Enrolment¶
- Authenticated learners click Enrol on the course landing page.
- The server action
enrollInCourse(courseSlug):- Validates the slug and verifies the course is published.
- Upserts the
enrollmentsrow scoped bytenant_id+user_id. - Sets
status = 'active'for free courses. - Returns
501 Not Implementedwith user-safe copy for paid courses (until a payment phase ships). - Re-enrolment from
cancelledupdates the same row, bumpsenrolled_at, clearscompleted_at, and writes an audit event.
- After success the learner is redirected to
/learn/course/\[slug\](the enrolled course home).
Full state machine and error paths: reference/course-enrollment-system.md.
Course Home¶
Enrolled view of /learn/course/\[slug\]:
- Module/entry list with status badges (completed / locked / available).
- Continue CTA pointing to the resume target (first incomplete entry, or most recently viewed entry).
- Toggleable course sidebar — see
reference/toggleable-navigation.md.[toggles visibility from anywhere on the course shell.
Sequential Entry Playback¶
/learn/course/\[slug\]/entry/\[entrySlug\]:
- Renders the entry body (Portable Text + widgets) and prev/next buttons.
- Sequential gating: by default an entry is accessible if all prior entries in the same module are complete; cross-module gating is off.
- Deep-linking to a locked entry redirects to the first available entry with
?notice=<reasonKey>; a translation-keyed toast announces the reason. - Server actions
markEntryCompleteandrecordEntryViewwriteentry_progress. Preview mode (?preview=1, available to assignedcourse_admin,tenant_admin,super_admin) is read-only. - Keyboard:
←/→for prev/next,[for sidebar toggle.
Full gating and resume contract: reference/entry-pagination.md.
Quizzes¶
When an entry's _type is quiz:
- The CMS supplies the question shape; the LMS strips correct answers from the client payload (
Quizserver-only vsQuizPublicclient-safe). QuizPlayer(client) handles per-question validation and one-pass submission.submitQuizAttemptre-fetches the quiz from the CMS, grades server-side, persists aquiz_attemptsrow with sequentialattempt_number, and writesentry_progresson pass or attempts exhaustion.- Retry policy reads
quiz.maxAttemptsfrom the CMS. After exhaustion the entry enters review-only mode.
Full contract: reference/quiz-system.md.
Assignments¶
_type: 'assignment' entries are routable in V1 but submission and grading are placeholders. Learners see translation-keyed copy explaining that uploads are not yet implemented and a manual Mark complete button. Real submission/grading is F9 in roadmap.md.
Dashboard¶
/learn/dashboard:
- Enrolled courses with progress bars and continue CTAs.
- In-progress entries.
- Recent quiz scores.
Tenant-scoped queries via Kysely; analytics shapes documented in reference/analytics.md.
Site header preferences¶
Every visitor sees three preference controls in the top-right site header on public and learner routes (inside data-testid="site-header-actions"):
| Control | Location | Options | Persistence |
|---|---|---|---|
| Language | Header dropdown | en, es, fr, de, pt, zh | Cookie; authenticated users also store preference in users.locale via the account page |
| Font size | Header dropdown (text-size icon) | Small, Medium (default), Large, Extra large | Browser localStorage (font-size); same device and browser only |
| Theme | Header dropdown | Light, Dark, System | Browser localStorage (theme); same device and browser only |
- Font size scales all text proportionally — navigation, headings, body copy, course widgets, and footer — so size relationships stay consistent.
- Medium keeps the site's normal reading size, including any CMS-authored base font size when the tenant site theme defines one.
- Choices apply immediately and persist across visits on the same browser. They are not synced to the learner account or other devices (same model as theme).
Account¶
/learn/account:
- Edit display name and preferred language (stored on your account).
- Request an email change (sends a confirmation token to the new address).
- Sign out everywhere (bumps
users.token_version).
For light/dark theme and font size, use the site header controls described in Site header preferences above.
Accessibility¶
- Every interactive element has a visible focus indicator.
- Modals and sheets trap focus via
useFocusTrap. - Keyboard shortcuts (
←,→,[) ignore form /contenteditablefocus and respectaria-disabled. - Live-region announcements (toasts, prev/next) are translation-keyed.
- Learners can increase or decrease global text size from the site header without breaking layout or heading hierarchy.