"use client"; /** * ProfileEditForm — the editor block used in /u/me's edit mode. * * Three editable fields: * • Display name (PATCH /users/me { name }) * • Handle (PATCH /users/me { handle }) — 409 if taken * • Avatar (R2 presign + PATCH /users/me { avatar_url }) * * `onDone` is invoked after a successful save OR when the user clicks * "Cancel" — the parent flips back to view mode. * * No log-out button here — that's handled by the sidebar footer so it lives * outside the edit context. */ import { useRef, useState } from "react"; import Image from "next/image"; import { ImagePlus, Save, X } from "lucide-react"; import { useAuth } from "@/components/auth/auth-provider"; import { auth, AuthError, type User } from "@/lib/auth"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; export function ProfileEditForm({ user, onDone, }: { user: User; onDone: () => void; }) { const { setUser } = useAuth(); const fileInput = useRef(null); const [name, setName] = useState(user.name); const [handle, setHandle] = useState(user.handle); const [avatarUrl, setAvatarUrl] = useState(user.avatar_url); const [saving, setSaving] = useState(false); const [uploading, setUploading] = useState(false); const [error, setError] = useState(null); const dirty = name.trim() !== user.name || handle.trim() !== user.handle || avatarUrl !== user.avatar_url; async function onSubmit(e: React.SyntheticEvent) { e.preventDefault(); setError(null); setSaving(true); try { // Patch only what's changed — keeps the audit log tidy and avoids // bumping updated_at when the user just clicked "Save" without edits. const updated = await auth.updateProfile( name.trim() !== user.name ? name.trim() : undefined, avatarUrl === user.avatar_url ? undefined : (avatarUrl ?? ""), handle.trim() !== user.handle ? handle.trim() : undefined, ); setUser(updated); onDone(); } catch (err) { setError(err instanceof AuthError ? err.message : "Could not save."); } finally { setSaving(false); } } async function onAvatarChange(e: React.ChangeEvent) { const file = e.target.files?.[0]; if (fileInput.current) fileInput.current.value = ""; if (!file) return; setError(null); setUploading(true); try { const publicURL = await auth.uploadAvatar(file); setAvatarUrl(publicURL); } catch (err) { setError(err instanceof AuthError ? err.message : "Upload failed."); } finally { setUploading(false); } } return (

Edit profile

{/* Avatar — same widget pattern as the old /profile form */}
{avatarUrl ? ( ) : null}
setName(e.target.value)} maxLength={120} required />
@ setHandle(e.target.value)} maxLength={32} pattern="[a-zA-Z0-9._-]+" required />

Letters, digits, dashes. Your public profile lives at /u/{handle || user.handle}.

Email changes aren't supported yet.

{error ?

{error}

: null}
); } function AvatarPreview({ src, name }: { src: string | null; name: string }) { if (src) { return ( {name} ); } const initials = name .split(/\s+/) .map((p) => p[0]) .slice(0, 2) .join("") .toUpperCase() || "?"; return (
{initials}
); }