feat: 5-day theme-level onboarding track from the "New here?" card (#30)
Some checks failed
On Pull Request to Main / test (pull_request) Failing after 24s
On Pull Request to Main / publish (pull_request) Has been skipped
On Pull Request to Main / deploy-dev (pull_request) Has been skipped

A self-paced onboarding track that introduces a new employee to every KB
theme in breadth (not depth), so they grasp how Respellion works day to
day and week to week. Offered as a CTA inside the Dashboard "New here?"
explainer card; always available regardless of enrollment.

Design:
- Theme is the trackable unit; the 5 "days" are a read-time presentation
  grouping, so re-chunking never loses progress. Completion is stored per
  theme in onboarding_completions.
- Per-theme overview generated lazily on first open (fast-tier
  emit_onboarding_overview tool), cached in onboarding_overviews keyed by
  theme + a topics_fingerprint that triggers regeneration when the theme's
  topic set changes.
- Reachable via /onboarding-track using the existing skipEnrollmentGate
  prop, decoupled from the 26-week curriculum (distinct from /onboarding,
  the enrollment page).

Backend:
- pb_migrations/1781200000_created_onboarding.js: two collections with
  authenticated-only rules and unique indexes; TEXT team_member_id (no
  relation) per the post-#18/#27 convention. Mirrored in
  scripts/setup-pb-collections.mjs.
- src/lib/onboardingService.js: pure helpers (orderThemes,
  distributeThemesIntoDays, computeTopicsFingerprint,
  computeOnboardingProgress, buildOnboardingPlan) + generation + I/O.
- db.js onboarding helpers use pb.filter() bindings (theme is free text).
- LLM tool + Zod schema + registry + simulation stub.

Frontend:
- src/pages/OnboardingTrack.jsx (day list, per-theme overview, completion
  banner, progress ring/day bar).
- Dashboard "New here?" card CTA + X/5-days progress chip (hidden when the
  KB has no themes).

Docs: data-model, generation-spec (§D), frontend-spec updated.

Verified: 22 new unit tests (npm test 134/134), eslint clean on changed
files, npm run build OK, PocketBase v0.30.4 boot applies the migration
(collections + unique indexes + authed rules confirmed), and a backend
contract check (upsert idempotency, unique-index guard, special-char
theme filtering).

Closes #30

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
RaymondVerhoef
2026-07-13 09:08:38 +02:00
parent 48715df147
commit 5214c9db3b
14 changed files with 1144 additions and 5 deletions

View File

@@ -1,6 +1,6 @@
import { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import { BookOpen, CheckSquare, Trophy, Sparkles, X, HelpCircle } from 'lucide-react';
import { BookOpen, CheckSquare, Trophy, Sparkles, X, HelpCircle, Rocket, ArrowRight } from 'lucide-react';
import { useApp } from '../store/AppContext';
import Card from '../components/ui/Card';
import Button from '../components/ui/Button';
@@ -9,6 +9,7 @@ import * as db from '../lib/db';
import { storage } from '../lib/storage';
import { getAssignedTopic } from '../lib/learningService';
import { getYearProgress, getCurriculumCycle, getCurriculumWeek, getActiveVersion } from '../lib/curriculumService';
import { getOnboardingSummary } from '../lib/onboardingService';
const Dashboard = () => {
const { state } = useApp();
@@ -25,6 +26,7 @@ const Dashboard = () => {
yearProgress: null,
hasCurriculum: false,
theme: '',
onboarding: null,
});
useEffect(() => {
@@ -69,6 +71,15 @@ const Dashboard = () => {
console.warn('[Dashboard] Could not load curriculum data:', e.message);
}
// Onboarding-track progress (independent of enrollment/curriculum). Guarded
// so a missing collection or empty KB never breaks the dashboard.
let onboarding = null;
try {
onboarding = await getOnboardingSummary(currentUser.id);
} catch (e) {
console.warn('[Dashboard] Could not load onboarding summary:', e.message);
}
setDashData({
topic,
learnDone,
@@ -80,13 +91,19 @@ const Dashboard = () => {
yearProgress,
hasCurriculum: curriculumExists,
theme: topic?.theme || '',
onboarding,
});
};
load();
}, [currentUser, weekNumber]);
const { topic, learnDone, testResult, top3, myRank, myPoints, activity, yearProgress, hasCurriculum: curriculumActive, theme } = dashData;
const { topic, learnDone, testResult, top3, myRank, myPoints, activity, yearProgress, hasCurriculum: curriculumActive, onboarding } = dashData;
const onboardingLabel = onboarding
? (onboarding.allDone
? 'Review onboarding'
: onboarding.daysCompleted > 0 ? 'Continue onboarding' : 'Start onboarding')
: 'Start onboarding';
const currentCycle = getCurriculumCycle(weekNumber);
const currWeek = getCurriculumWeek(weekNumber);
@@ -174,6 +191,31 @@ const Dashboard = () => {
</div>
))}
</div>
{/* Onboarding track CTA — only when there are themes to introduce. */}
{onboarding && onboarding.themesTotal > 0 && (
<div className="mt-5 pt-5 border-t border-bg-warm flex flex-col sm:flex-row sm:items-center justify-between gap-3">
<div className="flex items-center gap-3">
<div className="w-9 h-9 rounded-[var(--r-org)] bg-teal/10 text-teal flex items-center justify-center shrink-0">
<Rocket size={18} />
</div>
<div>
<p className="font-bold flex items-center gap-2">
New here? Take the 5-day onboarding
<Tag variant={onboarding.allDone ? 'success' : 'accent'} className="text-[10px]">
{onboarding.allDone ? 'Onboarding complete' : `${onboarding.daysCompleted}/${onboarding.dayCount} days`}
</Tag>
</p>
<p className="text-sm text-fg-muted">A light tour of every theme how Respellion works day to day and week to week.</p>
</div>
</div>
<Link to="/onboarding-track" className="shrink-0">
<Button variant="outline" className="whitespace-nowrap">
{onboardingLabel} <ArrowRight size={16} className="ml-1" />
</Button>
</Link>
</div>
)}
</Card>
)}