fix(F-01): fix avatar upload persistence on profile page

Replaced the one-shot initialized flag with a dirty flag that allows
the useEffect to re-sync local state from server data after a
successful save. Previously, once initialized was set to true, the
effect never ran again so avatar changes were lost on refetch.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-12 22:35:35 +02:00
parent 957d661567
commit 452928760a

View File

@@ -12,7 +12,7 @@ export function ProfileSection() {
const [displayName, setDisplayName] = useState(""); const [displayName, setDisplayName] = useState("");
const [bio, setBio] = useState(""); const [bio, setBio] = useState("");
const [avatarUrl, setAvatarUrl] = useState<string | null>(null); const [avatarUrl, setAvatarUrl] = useState<string | null>(null);
const [initialized, setInitialized] = useState(false); const [dirty, setDirty] = useState(false);
const [message, setMessage] = useState<{ const [message, setMessage] = useState<{
type: "success" | "error"; type: "success" | "error";
text: string; text: string;
@@ -21,13 +21,12 @@ export function ProfileSection() {
const fileInputRef = useRef<HTMLInputElement>(null); const fileInputRef = useRef<HTMLInputElement>(null);
useEffect(() => { useEffect(() => {
if (profile && !initialized) { if (profile && !dirty) {
setDisplayName(profile.displayName ?? ""); setDisplayName(profile.displayName ?? "");
setBio(profile.bio ?? ""); setBio(profile.bio ?? "");
setAvatarUrl(profile.avatarUrl ?? null); setAvatarUrl(profile.avatarUrl ?? null);
setInitialized(true);
} }
}, [profile, initialized]); }, [profile, dirty]);
async function handleSave(e: React.FormEvent) { async function handleSave(e: React.FormEvent) {
e.preventDefault(); e.preventDefault();
@@ -38,6 +37,7 @@ export function ProfileSection() {
avatarUrl, avatarUrl,
bio: bio.trim() || undefined, bio: bio.trim() || undefined,
}); });
setDirty(false);
setMessage({ type: "success", text: "Profile updated" }); setMessage({ type: "success", text: "Profile updated" });
} catch (err) { } catch (err) {
setMessage({ type: "error", text: (err as Error).message }); setMessage({ type: "error", text: (err as Error).message });
@@ -68,6 +68,7 @@ export function ProfileSection() {
try { try {
const result = await apiUpload<{ filename: string }>("/api/images", file); const result = await apiUpload<{ filename: string }>("/api/images", file);
setAvatarUrl(result.filename); setAvatarUrl(result.filename);
setDirty(true);
} catch { } catch {
setMessage({ type: "error", text: "Avatar upload failed." }); setMessage({ type: "error", text: "Avatar upload failed." });
} finally { } finally {
@@ -147,7 +148,7 @@ export function ProfileSection() {
{avatarUrl && ( {avatarUrl && (
<button <button
type="button" type="button"
onClick={() => setAvatarUrl(null)} onClick={() => { setAvatarUrl(null); setDirty(true); }}
className="block text-xs text-red-500 hover:text-red-700 mt-0.5" className="block text-xs text-red-500 hover:text-red-700 mt-0.5"
> >
Remove Remove
@@ -175,7 +176,7 @@ export function ProfileSection() {
id="displayName" id="displayName"
type="text" type="text"
value={displayName} value={displayName}
onChange={(e) => setDisplayName(e.target.value)} onChange={(e) => { setDisplayName(e.target.value); setDirty(true); }}
maxLength={100} maxLength={100}
placeholder="Your display name" placeholder="Your display name"
className="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-gray-200" className="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-gray-200"
@@ -193,7 +194,7 @@ export function ProfileSection() {
<textarea <textarea
id="bio" id="bio"
value={bio} value={bio}
onChange={(e) => setBio(e.target.value)} onChange={(e) => { setBio(e.target.value); setDirty(true); }}
maxLength={500} maxLength={500}
rows={3} rows={3}
placeholder="Tell others about yourself and your gear interests" placeholder="Tell others about yourself and your gear interests"