/** * @file components/settings/SettingsProfileEditCard.tsx * @description Card component for changing user details like email, username, and password with guest protection. */ "use client"; import { useState } from "react"; import { useUser } from "@/lib/context/UserContext"; import { User, Mail, Lock, AlertTriangle, ChevronDown, Save, } from "lucide-react"; import { ActionButton } from "@/components/ui/ActionButton"; import { profileUpdateSchema } from "@/lib/schemas/auth.schema"; /** Props for the SettingsProfileEditCard component. */ interface SettingsProfileEditCardProps { defaultOpen?: boolean; } /** Renders a collapsible card allowing users to update username, email, and password. */ export function SettingsProfileEditCard({ defaultOpen = false, }: SettingsProfileEditCardProps) { const [isOpen, setIsOpen] = useState(defaultOpen); const { currentUser, updateCurrentUser } = useUser(); const [username, setUsername] = useState(currentUser?.username || ""); const [email, setEmail] = useState(currentUser?.email || ""); const [password, setPassword] = useState(""); const [confirmPassword, setConfirmPassword] = useState(""); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const [success, setSuccess] = useState(null); if (!currentUser) return null; const guestEmail = process.env.NEXT_PUBLIC_GUEST_EMAIL; const isGuest = Boolean( guestEmail && currentUser.email?.toLowerCase() === guestEmail.toLowerCase(), ); // Simple check for changes & password match const hasChanges = username !== currentUser.username || email !== currentUser.email || password.length > 0; const isPasswordMatch = !password || password === confirmPassword; const isSaveDisabled = !hasChanges || !isPasswordMatch || isLoading; /** Validates form inputs and sends account detail updates to the server. */ const handleSubmit = async (e: React.SubmitEvent) => { e.preventDefault(); if (isGuest || isSaveDisabled) return; setError(null); setSuccess(null); // Zod Validation const validationResult = profileUpdateSchema.safeParse({ username, email, password, confirmPassword, }); if (!validationResult.success) { const firstError = validationResult.error.issues[0]?.message; setError(firstError || "Validation failed."); return; } try { setIsLoading(true); const payload: Record = {}; if (username !== currentUser.username) payload.username = username; if (email !== currentUser.email) payload.email = email; if (password) payload.password = password; const res = await fetch("/api/users/profile", { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); if (!res.ok) { const data = await res.json(); throw new Error(data.error || "Failed to update profile."); } updateCurrentUser({ ...(payload.username && { username: payload.username }), ...(payload.email && { email: payload.email }), }); setPassword(""); setConfirmPassword(""); setSuccess("Profile updated successfully!"); } catch (err) { setError( err instanceof Error ? err.message : "An unexpected error occurred.", ); } finally { setIsLoading(false); } }; return (
{/* Header / Toggle Button */} {/* Collapsible Body */} {isOpen && (
{isGuest ? (
Guest accounts cannot modify their account details.
) : (
{error &&

{error}

} {success &&

{success}

} {/* Username Input */}
setUsername(e.target.value)} disabled={isLoading} className="w-full bg-background border border-surface/80 rounded-xl pl-10 pr-3.5 py-2.5 text-sm text-white placeholder:text-muted focus:outline-none focus:ring-2 focus:ring-accent transition-all disabled:opacity-50" />
{/* Email Input */}
setEmail(e.target.value)} disabled={isLoading} className="w-full bg-background border border-surface/80 rounded-xl pl-10 pr-3.5 py-2.5 text-sm text-white placeholder:text-muted focus:outline-none focus:ring-2 focus:ring-accent transition-all disabled:opacity-50" />
{/* Password Inputs */}
setPassword(e.target.value)} disabled={isLoading} className="w-full bg-background border border-surface/80 rounded-xl pl-10 pr-3.5 py-2.5 text-sm text-white placeholder:text-muted focus:outline-none focus:ring-2 focus:ring-accent transition-all disabled:opacity-50" />
setConfirmPassword(e.target.value)} disabled={isLoading || !password} className="w-full bg-background border border-surface/80 rounded-xl pl-10 pr-3.5 py-2.5 text-sm text-white placeholder:text-muted focus:outline-none focus:ring-2 focus:ring-accent transition-all disabled:opacity-50" />
Save Changes
)}
)}
); }