2026-03-29 00:59:58 -04:00

248 lines
8.1 KiB
TypeScript

"use client";
import { useCallback, useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Progress } from "@/components/ui/progress";
import { Separator } from "@/components/ui/separator";
import { ONBOARDING_STEPS } from "@/lib/onboarding-content";
import { ChevronLeft, ChevronRight, GraduationCap } from "lucide-react";
export default function OnboardingPage() {
const router = useRouter();
const [currentStep, setCurrentStep] = useState(0);
const [maxVisited, setMaxVisited] = useState(0);
const [completing, setCompleting] = useState(false);
const step = ONBOARDING_STEPS[currentStep];
const isLastStep = currentStep === ONBOARDING_STEPS.length - 1;
const progress = ((currentStep + 1) / ONBOARDING_STEPS.length) * 100;
// Check if already onboarded
useEffect(() => {
fetch("/api/onboarding")
.then((res) => res.json())
.then((data) => {
if (data.onboarded) router.push("/dashboard");
});
}, [router]);
const goNext = useCallback(() => {
if (isLastStep) return;
const next = currentStep + 1;
setCurrentStep(next);
setMaxVisited((prev) => Math.max(prev, next));
window.scrollTo(0, 0);
}, [currentStep, isLastStep]);
const goBack = useCallback(() => {
if (currentStep === 0) return;
setCurrentStep(currentStep - 1);
window.scrollTo(0, 0);
}, [currentStep]);
const completeOnboarding = useCallback(async () => {
setCompleting(true);
try {
const res = await fetch("/api/onboarding", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ action: "complete" }),
});
if (res.ok) {
router.push("/dashboard");
}
} finally {
setCompleting(false);
}
}, [router]);
// Keyboard navigation
useEffect(() => {
function handleKeyDown(e: KeyboardEvent) {
if (e.key === "ArrowRight" && !isLastStep) {
e.preventDefault();
goNext();
} else if (e.key === "ArrowLeft" && currentStep > 0) {
e.preventDefault();
goBack();
}
}
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [currentStep, isLastStep, goNext, goBack]);
return (
<div className="bg-background">
{/* Progress header */}
<header className="sticky top-0 z-10 border-b bg-card px-6 py-3">
<div className="mx-auto flex max-w-3xl items-center justify-between gap-4">
<span className="text-sm text-muted-foreground">
Step {currentStep + 1} of {ONBOARDING_STEPS.length}
</span>
<Progress value={progress} className="w-48" />
<a
href="/codebook"
target="_blank"
rel="noopener noreferrer"
className="text-sm text-muted-foreground underline-offset-4 hover:underline"
>
Full Codebook
</a>
</div>
</header>
{/* Step nav dots */}
<div className="mx-auto mt-4 flex max-w-3xl flex-wrap gap-1.5 px-6">
{ONBOARDING_STEPS.map((s, i) => (
<button
key={s.id}
onClick={() => {
if (i <= maxVisited) {
setCurrentStep(i);
window.scrollTo(0, 0);
}
}}
disabled={i > maxVisited}
className={`h-2 rounded-full transition-all ${
i === currentStep
? "w-6 bg-primary"
: i <= maxVisited
? "w-2 bg-primary/40 hover:bg-primary/60"
: "w-2 bg-muted"
}`}
aria-label={`Step ${i + 1}: ${s.title}`}
/>
))}
</div>
{/* Main content */}
<main className="mx-auto w-full max-w-3xl p-6">
<Card>
<CardHeader>
<div className="flex items-center gap-3">
<Badge variant="outline" className="font-mono">
{currentStep + 1}
</Badge>
<div>
<CardTitle className="text-xl">{step.title}</CardTitle>
<CardDescription className="mt-1">
{step.subtitle}
</CardDescription>
</div>
</div>
</CardHeader>
<CardContent className="space-y-6">
{/* Main text content */}
<div className="space-y-3">
{step.content.map((paragraph, i) => (
<p key={i} className="leading-relaxed text-sm">
{paragraph}
</p>
))}
</div>
{/* Examples */}
{step.examples && step.examples.length > 0 && (
<>
<Separator />
<div className="space-y-4">
<h4 className="text-sm font-semibold uppercase tracking-wider text-muted-foreground">
Examples
</h4>
{step.examples.map((example, i) => (
<div key={i} className="space-y-2 rounded-lg border p-4">
<p className="font-serif text-sm italic leading-relaxed text-foreground/90">
&ldquo;{example.text}&rdquo;
</p>
<div className="flex flex-wrap gap-2">
{example.category && (
<Badge variant="secondary">{example.category}</Badge>
)}
{example.specificity && (
<Badge variant="outline">{example.specificity}</Badge>
)}
</div>
<p className="text-sm text-muted-foreground">
{example.explanation}
</p>
</div>
))}
</div>
</>
)}
{/* Key points */}
{step.keyPoints && step.keyPoints.length > 0 && (
<>
<Separator />
<div className="space-y-2">
<h4 className="text-sm font-semibold uppercase tracking-wider text-muted-foreground">
Key Points
</h4>
<ul className="space-y-1.5">
{step.keyPoints.map((point, i) => (
<li
key={i}
className="flex items-start gap-2 text-sm"
>
<span className="mt-1.5 block h-1.5 w-1.5 shrink-0 rounded-full bg-primary" />
{point}
</li>
))}
</ul>
</div>
</>
)}
{/* Tip */}
{step.tip && (
<div className="rounded-lg border-l-4 border-amber-500 bg-amber-50 p-4 dark:bg-amber-950/30">
<p className="text-sm">
<span className="font-semibold">Tip: </span>
{step.tip}
</p>
</div>
)}
</CardContent>
<CardFooter className="flex justify-between gap-4 pt-6">
<Button
variant="outline"
onClick={goBack}
disabled={currentStep === 0}
>
<ChevronLeft className="mr-1 size-4" />
Back
</Button>
{isLastStep ? (
<Button
onClick={completeOnboarding}
disabled={completing}
size="lg"
>
<GraduationCap className="mr-2 size-4" />
{completing ? "Completing..." : "I'm Ready — Start the Quiz"}
</Button>
) : (
<Button onClick={goNext}>
Continue
<ChevronRight className="ml-1 size-4" />
</Button>
)}
</CardFooter>
</Card>
</main>
</div>
);
}