"use client"; import { useRef, useState } from "react"; import { useRouter } from "next/navigation"; import Image from "next/image"; 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"; /** * Profile edit form. Two responsibilities: * * 1. Inline name edit (debounced save on form submit). * 2. Avatar upload via R2 presigned PUT: * click [Change] → presign → PUT to R2 → PATCH /users/me with new URL. * * The new avatar URL appended with a `?v=` cache-buster forces the * browser to fetch the new image even if the key happened to collide * (extremely unlikely given our random suffix, but cheap insurance). */ export function ProfileForm({ user }: { user: User }) { const router = useRouter(); const { logOut, setUser } = useAuth(); const fileInput = useRef(null); const [name, setName] = useState(user.name); const [avatarUrl, setAvatarUrl] = useState(user.avatar_url); const [saving, setSaving] = useState(false); const [uploading, setUploading] = useState(false); const [error, setError] = useState(null); const [success, setSuccess] = useState(null); async function onSave(e: React.SyntheticEvent) { e.preventDefault(); setError(null); setSuccess(null); setSaving(true); try { const updated = await auth.updateProfile(name); setUser(updated); setSuccess("Profile saved."); } 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 (!file) return; setError(null); setSuccess(null); setUploading(true); try { const publicURL = await auth.uploadAvatar(file); const updated = await auth.updateProfile(undefined, publicURL); setUser(updated); setAvatarUrl(updated.avatar_url); setSuccess("Avatar updated."); } catch (err) { setError(err instanceof AuthError ? err.message : "Upload failed."); } finally { setUploading(false); if (fileInput.current) fileInput.current.value = ""; } } async function onRemoveAvatar() { setError(null); setUploading(true); try { const updated = await auth.updateProfile(undefined, ""); setUser(updated); setAvatarUrl(null); } catch (err) { setError(err instanceof AuthError ? err.message : "Could not remove avatar."); } finally { setUploading(false); } } async function onLogout() { await logOut(); router.push("/"); router.refresh(); } return (

Avatar

{avatarUrl ? ( ) : null}

Details

Email changes aren't supported yet — drop us a note if you need this.

setName(e.target.value)} maxLength={120} required />
{error ?

{error}

: null} {success ?

{success}

: 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}
); }