import { useEffect, useMemo, useState } from 'react'; import { Link } from 'react-router-dom'; import { Loader, Rocket, ArrowLeft, ChevronRight, CheckCircle2, Circle, Sparkles, ListChecks, PartyPopper, } from 'lucide-react'; import Card from '../components/ui/Card'; import Button from '../components/ui/Button'; import Tag from '../components/ui/Tag'; import { useApp } from '../store/AppContext'; import { getOnboardingPlan, getCompletedThemes, getOrGenerateOnboardingOverview, markThemeCompleted, computeOnboardingProgress, } from '../lib/onboardingService'; function normalizeContent(raw) { if (!raw) return null; if (typeof raw === 'string') { try { return JSON.parse(raw); } catch { return null; } } return raw; } // ── Per-theme overview view ────────────────────────────────────────────────── function OnboardingThemeView({ theme, topics, done, onBack, onDone }) { const [record, setRecord] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [marked, setMarked] = useState(done); useEffect(() => { let cancelled = false; setLoading(true); setError(null); getOrGenerateOnboardingOverview(theme, topics) .then((rec) => { if (!cancelled) setRecord(rec); }) .catch((err) => { if (!cancelled) setError(err.message || 'Failed to load the overview.'); }) .finally(() => { if (!cancelled) setLoading(false); }); return () => { cancelled = true; }; }, [theme]); // eslint-disable-line react-hooks/exhaustive-deps const content = normalizeContent(record?.content); const handleDone = async () => { if (marked) return; setMarked(true); await onDone(theme); }; return (
{loading && (

Preparing this theme…

This may take 10–30 seconds the first time — the result is cached.

)} {!loading && error && (

Could not load this theme

{error}

)} {!loading && !error && content && ( <>
Theme {marked && Done}

{content.title}

{content.what_it_is}

Why it matters here

{content.why_it_matters}

Key points

{Array.isArray(content.topics_covered) && content.topics_covered.length > 0 && (

Topics in this theme

{content.topics_covered.map((t) => ( {t.label} ))}
)}
)}
); } // ── Progress ring ───────────────────────────────────────────────────────────── function ProgressRing({ percentage }) { return (
{percentage}%
); } // ── Page ────────────────────────────────────────────────────────────────────── export default function OnboardingTrack() { const { state } = useApp(); const { currentUser } = state; const [plan, setPlan] = useState(null); const [completed, setCompleted] = useState(new Set()); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [selectedTheme, setSelectedTheme] = useState(null); useEffect(() => { if (!currentUser) return; let cancelled = false; setLoading(true); Promise.all([getOnboardingPlan(), getCompletedThemes(currentUser.id)]) .then(([p, done]) => { if (!cancelled) { setPlan(p); setCompleted(done); } }) .catch((err) => { if (!cancelled) setError(err.message || 'Failed to load the onboarding track.'); }) .finally(() => { if (!cancelled) setLoading(false); }); return () => { cancelled = true; }; }, [currentUser]); const progress = useMemo( () => computeOnboardingProgress(plan?.days || [], completed), [plan, completed], ); const handleThemeDone = async (theme) => { await markThemeCompleted(currentUser.id, theme); setCompleted((prev) => new Set(prev).add(theme)); setSelectedTheme(null); }; if (loading) { return (

Loading your onboarding track…

); } if (error) { return (

Could not load the onboarding track

{error}

); } // Empty KB → friendly empty state. if (!plan || plan.themes.length === 0) { return (

Onboarding

There are no themes to introduce yet. Once the knowledge base has content, your 5-day onboarding track will appear here.

); } const themeTopics = (theme) => plan.themeTopicMap.get(theme) || []; if (selectedTheme) { return (
setSelectedTheme(null)} onDone={handleThemeDone} />
); } return (

Onboarding

A light, self-paced tour of every theme at Respellion, spread over about five days. It gives you the breadth you need to understand how we work day to day and week to week — you can go faster if you like.

{progress.allDone ? 'Onboarding complete' : `${progress.daysCompleted}/${progress.dayCount} days complete`}

{progress.themesDone} of {progress.themesTotal} themes done

{plan.days.map((d) => { const dayDone = d.themes.every((t) => completed.has(t)); return (
); })}
{progress.allDone && (

You've completed the onboarding track 🎉

You've been introduced to every theme. You're ready to pick up a first assignment — and you can revisit any theme below whenever you want a refresher.

)}
{plan.days.map((d) => { const doneCount = d.themes.filter((t) => completed.has(t)).length; return (
Day {d.day}
{doneCount}/{d.themes.length} done
{d.themes.map((theme) => { const isDone = completed.has(theme); const count = themeTopics(theme).length; return ( ); })}
); })}
); }