feat: 5-day theme-level onboarding track from the "New here?" card (#30)
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:
@@ -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>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user