Spaces:
Sleeping
Sleeping
| import React, { useState, useRef } from 'react'; | |
| import { Button } from './ui/button'; | |
| import { Input } from './ui/input'; | |
| import { Label } from './ui/label'; | |
| import { Textarea } from './ui/textarea'; | |
| 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; | |
| } | |
| const TOTAL_STEPS = 5; | |
| export function Onboarding({ user, onComplete, onSkip }: OnboardingProps) { | |
| const [currentStep, setCurrentStep] = useState(1); | |
| const [name, setName] = useState(user.name); | |
| const [email, setEmail] = useState(user.email); | |
| const [studentId, setStudentId] = useState(''); | |
| const [department, setDepartment] = useState(''); | |
| const [year, setYear] = useState(''); | |
| const [major, setMajor] = useState(''); | |
| const [bio, setBio] = useState(''); | |
| const [photoPreview, setPhotoPreview] = useState<string | null>(null); | |
| const fileInputRef = useRef<HTMLInputElement>(null); | |
| const handlePhotoSelect = (e: React.ChangeEvent<HTMLInputElement>) => { | |
| const file = e.target.files?.[0]; | |
| if (file) { | |
| // Validate file type | |
| if (!file.type.startsWith('image/')) { | |
| toast.error('Please select an image file'); | |
| return; | |
| } | |
| // Validate file size (2MB) | |
| if (file.size > 2 * 1024 * 1024) { | |
| toast.error('File size must be less than 2MB'); | |
| return; | |
| } | |
| // Create preview | |
| const reader = new FileReader(); | |
| reader.onload = (e) => { | |
| setPhotoPreview(e.target?.result as string); | |
| }; | |
| reader.readAsDataURL(file); | |
| } | |
| }; | |
| const handleChangePhotoClick = () => { | |
| fileInputRef.current?.click(); | |
| }; | |
| const handleNext = () => { | |
| if (currentStep < TOTAL_STEPS) { | |
| setCurrentStep(currentStep + 1); | |
| } else { | |
| handleComplete(); | |
| } | |
| }; | |
| const handlePrevious = () => { | |
| if (currentStep > 1) { | |
| setCurrentStep(currentStep - 1); | |
| } | |
| }; | |
| const handleSkip = () => { | |
| // Skip all remaining steps and complete onboarding | |
| onSkip(); | |
| }; | |
| const handleComplete = () => { | |
| if (name.trim() && email.trim()) { | |
| onComplete({ name: name.trim(), email: email.trim() }); | |
| toast.success('Profile setup completed!'); | |
| } else { | |
| toast.error('Please fill in all required fields'); | |
| } | |
| }; | |
| 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={year} onValueChange={setYear}> | |
| <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">About You</h3> | |
| <p className="text-sm text-muted-foreground"> | |
| Share a brief introduction about yourself | |
| </p> | |
| <div className="space-y-2"> | |
| <Label htmlFor="onboarding-bio">Bio</Label> | |
| <Textarea | |
| id="onboarding-bio" | |
| value={bio} | |
| onChange={(e) => setBio(e.target.value)} | |
| placeholder="Tell us about yourself..." | |
| className="min-h-[120px] resize-none" | |
| /> | |
| <p className="text-xs text-muted-foreground"> | |
| Brief description for your profile. Max 200 characters. | |
| </p> | |
| </div> | |
| </div> | |
| ); | |
| case 4: | |
| 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 defaultValue="visual"> | |
| <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 defaultValue="moderate"> | |
| <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 5: | |
| 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).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> | |
| ); | |
| } | |