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:
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user