SarahXia0405's picture
Update web/src/components/Onboarding.tsx
8300867 verified
import React, { useRef, useState } from "react";
import { Button } from "./ui/button";
import { Input } from "./ui/input";
import { Label } from "./ui/label";
import { Dialog, DialogContent, DialogTitle } from "./ui/dialog";
import type { User as UserType } from "../App";
import { toast } from "sonner";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
import { ChevronLeft, ChevronRight } from "lucide-react";
interface OnboardingProps {
user: UserType;
onComplete: (user: UserType) => void;
onSkip: () => void;
}
// ✅ Bio step removed. Total steps: 4
const TOTAL_STEPS = 4;
export function Onboarding({ user, onComplete, onSkip }: OnboardingProps) {
const [currentStep, setCurrentStep] = useState(1);
// Step 1: Basic
const [name, setName] = useState(user.name ?? "");
const [email, setEmail] = useState(user.email ?? "");
// Step 2: Academic
const [studentId, setStudentId] = useState(user.studentId ?? "");
const [department, setDepartment] = useState(user.department ?? "");
const [yearLevel, setYearLevel] = useState(user.yearLevel ?? "");
const [major, setMajor] = useState(user.major ?? "");
// Step 3: Preferences (controlled, not defaultValue-only)
const [learningStyle, setLearningStyle] = useState(user.learningStyle ?? "visual");
const [learningPace, setLearningPace] = useState(user.learningPace ?? "moderate");
// Step 4: Photo
const [photoPreview, setPhotoPreview] = useState<string | null>(user.avatarUrl ?? null);
const fileInputRef = useRef<HTMLInputElement>(null);
const handlePhotoSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
if (!file.type.startsWith("image/")) {
toast.error("Please select an image file");
return;
}
if (file.size > 2 * 1024 * 1024) {
toast.error("File size must be less than 2MB");
return;
}
const reader = new FileReader();
reader.onload = (ev) => setPhotoPreview(ev.target?.result as string);
reader.readAsDataURL(file);
};
const handleChangePhotoClick = () => fileInputRef.current?.click();
const handleNext = () => {
if (currentStep < TOTAL_STEPS) setCurrentStep((s) => s + 1);
else handleComplete();
};
const handlePrevious = () => {
if (currentStep > 1) setCurrentStep((s) => s - 1);
};
const handleSkip = () => onSkip();
const handleComplete = () => {
if (!name.trim() || !email.trim()) {
toast.error("Please fill in all required fields");
return;
}
// ✅ IMPORTANT: Onboarding must NOT overwrite bio.
// We merge ...user first to preserve any Clare-generated bio.
const next: UserType = {
...user,
name: name.trim(),
email: email.trim(),
studentId: studentId.trim() || undefined,
department: department.trim() || undefined,
yearLevel: yearLevel || undefined,
major: major.trim() || undefined,
learningStyle: learningStyle || undefined,
learningPace: learningPace || undefined,
avatarUrl: photoPreview || undefined,
onboardingCompleted: true,
// ❌ do NOT set bio here
};
onComplete(next);
toast.success("Profile setup completed!");
};
const renderStepContent = () => {
switch (currentStep) {
case 1:
return (
<div className="space-y-4">
<h3 className="text-lg font-medium">Basic Information</h3>
<p className="text-sm text-muted-foreground">Let's start with your basic information</p>
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="onboarding-name">Full Name *</Label>
<Input
id="onboarding-name"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Enter your full name"
/>
</div>
<div className="space-y-2">
<Label htmlFor="onboarding-email">Email *</Label>
<Input
id="onboarding-email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Enter your email"
/>
</div>
</div>
</div>
);
case 2:
return (
<div className="space-y-4">
<h3 className="text-lg font-medium">Academic Background</h3>
<p className="text-sm text-muted-foreground">Tell us about your academic information</p>
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="onboarding-student-id">Student ID</Label>
<Input
id="onboarding-student-id"
value={studentId}
onChange={(e) => setStudentId(e.target.value)}
placeholder="Enter your student ID"
/>
</div>
<div className="space-y-2">
<Label htmlFor="onboarding-department">Department</Label>
<Input
id="onboarding-department"
value={department}
onChange={(e) => setDepartment(e.target.value)}
placeholder="Enter your department"
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="onboarding-year">Year Level</Label>
<Select value={yearLevel} onValueChange={setYearLevel}>
<SelectTrigger id="onboarding-year">
<SelectValue placeholder="Select year level" />
</SelectTrigger>
<SelectContent>
<SelectItem value="1st Year">1st Year</SelectItem>
<SelectItem value="2nd Year">2nd Year</SelectItem>
<SelectItem value="3rd Year">3rd Year</SelectItem>
<SelectItem value="4th Year">4th Year</SelectItem>
<SelectItem value="Graduate">Graduate</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="onboarding-major">Major</Label>
<Input
id="onboarding-major"
value={major}
onChange={(e) => setMajor(e.target.value)}
placeholder="Enter your major"
/>
</div>
</div>
</div>
</div>
);
case 3:
return (
<div className="space-y-4">
<h3 className="text-lg font-medium">Learning Preferences</h3>
<p className="text-sm text-muted-foreground">Help us personalize your learning experience</p>
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="onboarding-learning-style">Preferred Learning Style</Label>
<Select value={learningStyle} onValueChange={setLearningStyle}>
<SelectTrigger id="onboarding-learning-style">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="visual">Visual</SelectItem>
<SelectItem value="auditory">Auditory</SelectItem>
<SelectItem value="reading">Reading/Writing</SelectItem>
<SelectItem value="kinesthetic">Kinesthetic</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="onboarding-pace">Learning Pace</Label>
<Select value={learningPace} onValueChange={setLearningPace}>
<SelectTrigger id="onboarding-pace">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="slow">Slow & Steady</SelectItem>
<SelectItem value="moderate">Moderate</SelectItem>
<SelectItem value="fast">Fast-paced</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</div>
);
case 4:
return (
<div className="space-y-4">
<h3 className="text-lg font-medium">Profile Picture</h3>
<p className="text-sm text-muted-foreground">Upload a photo to personalize your profile (optional)</p>
<div className="flex items-center gap-4">
<div className="w-24 h-24 rounded-full bg-gradient-to-br from-red-500 to-orange-500 flex items-center justify-center text-white text-3xl overflow-hidden">
{photoPreview ? (
<img src={photoPreview} alt="Profile" className="w-full h-full object-cover" />
) : (
(name?.charAt(0) || "U").toUpperCase()
)}
</div>
<div>
<input
ref={fileInputRef}
type="file"
accept="image/jpeg,image/png,image/gif,image/webp"
onChange={handlePhotoSelect}
className="hidden"
/>
<Button variant="outline" size="sm" onClick={handleChangePhotoClick}>
Change Photo
</Button>
<p className="text-xs text-muted-foreground mt-1">JPG, PNG or GIF. Max size 2MB</p>
</div>
</div>
</div>
);
default:
return null;
}
};
return (
<Dialog open onOpenChange={(open) => { if (!open) onSkip(); }}>
<DialogContent
className="sm:max-w-lg p-0 gap-0 max-h-[90vh] overflow-hidden"
style={{ zIndex: 1001 }}
overlayClassName="!inset-0 !z-[99]"
overlayStyle={{ top: 0, left: 0, right: 0, bottom: 0, zIndex: 99, position: "fixed" }}
>
<div className="flex flex-col max-h-[90vh]">
{/* Header */}
<div className="border-b border-border p-4 flex items-center justify-between flex-shrink-0">
<div className="flex-1">
<DialogTitle className="text-xl font-medium">Welcome! Let's set up your profile</DialogTitle>
<p className="text-sm text-muted-foreground mt-1">
Step {currentStep} of {TOTAL_STEPS}
</p>
</div>
{/* Progress indicator */}
<div className="flex gap-1">
{Array.from({ length: TOTAL_STEPS }).map((_, index) => (
<div
key={index}
className={`h-2 w-2 rounded-full transition-colors ${index + 1 <= currentStep ? "bg-primary" : "bg-muted"}`}
/>
))}
</div>
</div>
{/* Content */}
<div className="p-6 overflow-y-auto flex-1">{renderStepContent()}</div>
{/* Footer */}
<div className="border-t border-border p-4 flex justify-between gap-2 flex-shrink-0">
<div className="flex gap-2">
{currentStep > 1 && (
<Button variant="outline" onClick={handlePrevious}>
<ChevronLeft className="h-4 w-4 mr-1" />
Previous
</Button>
)}
</div>
<div className="flex gap-2">
<Button variant="outline" onClick={handleSkip}>
Skip all
</Button>
<Button onClick={handleNext}>
{currentStep === TOTAL_STEPS ? "Complete" : "Next Step"}
{currentStep < TOTAL_STEPS && <ChevronRight className="h-4 w-4 ml-1" />}
</Button>
</div>
</div>
</div>
</DialogContent>
</Dialog>
);
}