Spaces:
Sleeping
Sleeping
| import React, { useEffect, useRef, useState } 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"; | |
| interface ProfileEditorProps { | |
| user: UserType; | |
| onSave: (user: UserType) => void; | |
| onClose: () => void; | |
| } | |
| export function ProfileEditor({ user, onSave, onClose }: ProfileEditorProps) { | |
| const [name, setName] = useState(user.name ?? ""); | |
| const [email, setEmail] = useState(user.email ?? ""); | |
| const [studentId, setStudentId] = useState(user.studentId ?? ""); | |
| const [department, setDepartment] = useState(user.department ?? ""); | |
| const [yearLevel, setYearLevel] = useState(user.yearLevel ?? ""); | |
| const [major, setMajor] = useState(user.major ?? ""); | |
| // ✅ bio is editable here (user can override Clare-generated bio) | |
| const [bio, setBio] = useState(user.bio ?? ""); | |
| const [learningStyle, setLearningStyle] = useState(user.learningStyle ?? "visual"); | |
| const [learningPace, setLearningPace] = useState(user.learningPace ?? "moderate"); | |
| const [photoPreview, setPhotoPreview] = useState<string | null>(user.avatarUrl ?? null); | |
| const fileInputRef = useRef<HTMLInputElement>(null); | |
| // ✅ Keep fields in sync if user changes while dialog is open | |
| useEffect(() => { | |
| setName(user.name ?? ""); | |
| setEmail(user.email ?? ""); | |
| setStudentId(user.studentId ?? ""); | |
| setDepartment(user.department ?? ""); | |
| setYearLevel(user.yearLevel ?? ""); | |
| setMajor(user.major ?? ""); | |
| setBio(user.bio ?? ""); | |
| setLearningStyle(user.learningStyle ?? "visual"); | |
| setLearningPace(user.learningPace ?? "moderate"); | |
| setPhotoPreview(user.avatarUrl ?? null); | |
| }, [ | |
| user.name, | |
| user.email, | |
| user.studentId, | |
| user.department, | |
| user.yearLevel, | |
| user.major, | |
| user.bio, | |
| user.learningStyle, | |
| user.learningPace, | |
| user.avatarUrl, | |
| ]); | |
| const handleSave = () => { | |
| if (!name.trim() || !email.trim()) { | |
| toast.error("Please fill in all required fields"); | |
| return; | |
| } | |
| 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, | |
| // ✅ allow user edit | |
| bio: (bio ?? "").slice(0, 200), | |
| learningStyle: learningStyle || undefined, | |
| learningPace: learningPace || undefined, | |
| avatarUrl: photoPreview || undefined, | |
| }; | |
| onSave(next); | |
| toast.success("Profile updated successfully!"); | |
| onClose(); | |
| }; | |
| 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); | |
| toast.success("Photo updated successfully!"); | |
| }; | |
| reader.readAsDataURL(file); | |
| }; | |
| const handleChangePhotoClick = () => fileInputRef.current?.click(); | |
| return ( | |
| <Dialog | |
| open | |
| onOpenChange={(open) => { | |
| if (!open) onClose(); | |
| }} | |
| > | |
| <DialogContent | |
| className="sm:max-w-[800px] p-0 gap-0 max-h-[90vh] overflow-hidden" | |
| style={{ zIndex: 1001, maxWidth: "800px", width: "800px" }} | |
| overlayClassName="!top-16 !left-0 !right-0 !bottom-0 !z-[99]" | |
| overlayStyle={{ top: "64px", left: 0, right: 0, bottom: 0, zIndex: 99, position: "fixed" }} | |
| > | |
| <div className="flex flex-col max-h-[90vh]"> | |
| {/* Header */} | |
| <div className="p-4 flex items-center justify-between flex-shrink-0"> | |
| <DialogTitle className="text-xl font-medium">Edit Profile</DialogTitle> | |
| </div> | |
| {/* Content */} | |
| <div className="p-6 space-y-6 overflow-y-auto flex-1"> | |
| {/* Profile Picture */} | |
| <div className="flex items-center gap-4"> | |
| <div className="w-20 h-20 rounded-full bg-gradient-to-br from-red-500 to-orange-500 flex items-center justify-center text-white text-2xl 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> | |
| {/* Basic Information */} | |
| <div className="space-y-4"> | |
| <h3 className="text-sm font-medium">Basic Information</h3> | |
| <div className="grid grid-cols-1 md:grid-cols-2 gap-4"> | |
| <div className="space-y-2"> | |
| <Label htmlFor="edit-name">Full Name *</Label> | |
| <Input | |
| id="edit-name" | |
| value={name} | |
| onChange={(e) => setName(e.target.value)} | |
| placeholder="Enter your full name" | |
| /> | |
| </div> | |
| <div className="space-y-2"> | |
| <Label htmlFor="edit-email">Email *</Label> | |
| <Input | |
| id="edit-email" | |
| type="email" | |
| value={email} | |
| onChange={(e) => setEmail(e.target.value)} | |
| placeholder="Enter your email" | |
| /> | |
| </div> | |
| <div className="space-y-2"> | |
| <Label htmlFor="edit-student-id">Student ID</Label> | |
| <Input | |
| id="edit-student-id" | |
| value={studentId} | |
| onChange={(e) => setStudentId(e.target.value)} | |
| placeholder="Enter your student ID" | |
| /> | |
| </div> | |
| <div className="space-y-2"> | |
| <Label htmlFor="edit-department">Department</Label> | |
| <Input | |
| id="edit-department" | |
| value={department} | |
| onChange={(e) => setDepartment(e.target.value)} | |
| placeholder="Enter your department" | |
| /> | |
| </div> | |
| </div> | |
| </div> | |
| {/* Academic Background */} | |
| <div className="space-y-4"> | |
| <h3 className="text-sm font-medium">Academic Background</h3> | |
| <div className="grid grid-cols-1 md:grid-cols-2 gap-4"> | |
| <div className="space-y-2"> | |
| <Label htmlFor="edit-year">Year Level</Label> | |
| <Select value={yearLevel} onValueChange={setYearLevel}> | |
| <SelectTrigger id="edit-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="edit-major">Major</Label> | |
| <Input | |
| id="edit-major" | |
| value={major} | |
| onChange={(e) => setMajor(e.target.value)} | |
| placeholder="Enter your major" | |
| /> | |
| </div> | |
| </div> | |
| </div> | |
| {/* Bio (Editable) */} | |
| <div className="space-y-2"> | |
| <Label htmlFor="edit-bio">Bio</Label> | |
| <Textarea | |
| id="edit-bio" | |
| value={bio} | |
| onChange={(e) => setBio(e.target.value)} | |
| placeholder="Tell us about yourself..." | |
| className="min-h-[100px] resize-none" | |
| maxLength={200} | |
| /> | |
| <p className="text-xs text-muted-foreground">Max 200 characters. You can edit this anytime.</p> | |
| </div> | |
| {/* Learning Preferences */} | |
| <div className="space-y-4"> | |
| <h3 className="text-sm font-medium">Learning Preferences</h3> | |
| <div className="grid grid-cols-1 md:grid-cols-2 gap-4"> | |
| <div className="space-y-2"> | |
| <Label htmlFor="edit-learning-style">Preferred Learning Style</Label> | |
| <Select value={learningStyle} onValueChange={setLearningStyle}> | |
| <SelectTrigger id="edit-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="edit-pace">Learning Pace</Label> | |
| <Select value={learningPace} onValueChange={setLearningPace}> | |
| <SelectTrigger id="edit-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> | |
| </div> | |
| {/* Footer */} | |
| <div className="border-t border-border p-4 flex justify-end gap-2 flex-shrink-0"> | |
| <Button variant="outline" onClick={onClose}> | |
| Cancel | |
| </Button> | |
| <Button onClick={handleSave}>Save Changes</Button> | |
| </div> | |
| </div> | |
| </DialogContent> | |
| </Dialog> | |
| ); | |
| } | |