248 lines
8.1 KiB
TypeScript
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">
|
|
“{example.text}”
|
|
</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>
|
|
);
|
|
}
|