From b647e23f9121b9beba3ccdc703fc1c4690413b8f Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Sun, 12 Apr 2026 23:02:45 +0200 Subject: [PATCH] fix: use presigned S3 URLs for avatar images instead of /uploads/ paths Avatar images were rendered via /uploads/ which doesn't exist since the S3 migration. Now the server enriches profile responses with avatarImageUrl (presigned S3 URL) and the frontend uses it directly. Also fixed the public profile page at /users/:id. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/client/components/ProfileSection.tsx | 22 ++++++++++++++-------- src/client/hooks/useProfile.ts | 1 + src/client/routes/users/$userId.tsx | 4 ++-- src/server/routes/auth.ts | 8 +++++++- src/server/routes/profiles.ts | 14 +++++++++++++- 5 files changed, 37 insertions(+), 12 deletions(-) diff --git a/src/client/components/ProfileSection.tsx b/src/client/components/ProfileSection.tsx index 98e4191..37b50d8 100644 --- a/src/client/components/ProfileSection.tsx +++ b/src/client/components/ProfileSection.tsx @@ -11,7 +11,8 @@ export function ProfileSection() { const [displayName, setDisplayName] = useState(""); const [bio, setBio] = useState(""); - const [avatarUrl, setAvatarUrl] = useState(null); + const [avatarFilename, setAvatarFilename] = useState(null); + const [avatarDisplayUrl, setAvatarDisplayUrl] = useState(null); const [dirty, setDirty] = useState(false); const [message, setMessage] = useState<{ type: "success" | "error"; @@ -24,7 +25,8 @@ export function ProfileSection() { if (profile && !dirty) { setDisplayName(profile.displayName ?? ""); setBio(profile.bio ?? ""); - setAvatarUrl(profile.avatarUrl ?? null); + setAvatarFilename(profile.avatarUrl ?? null); + setAvatarDisplayUrl(profile.avatarImageUrl ?? null); } }, [profile, dirty]); @@ -34,7 +36,7 @@ export function ProfileSection() { try { await updateProfile.mutateAsync({ displayName: displayName.trim() || undefined, - avatarUrl, + avatarUrl: avatarFilename, bio: bio.trim() || undefined, }); setDirty(false); @@ -63,13 +65,16 @@ export function ProfileSection() { return; } + const localPreview = URL.createObjectURL(file); + setAvatarDisplayUrl(localPreview); setUploading(true); setMessage(null); try { const result = await apiUpload<{ filename: string }>("/api/images", file); - setAvatarUrl(result.filename); + setAvatarFilename(result.filename); setDirty(true); } catch { + setAvatarDisplayUrl(null); setMessage({ type: "error", text: "Avatar upload failed." }); } finally { setUploading(false); @@ -92,9 +97,9 @@ export function ProfileSection() { onClick={() => fileInputRef.current?.click()} className="relative w-16 h-16 rounded-full overflow-hidden cursor-pointer group shrink-0" > - {avatarUrl ? ( + {avatarDisplayUrl ? ( Avatar @@ -145,11 +150,12 @@ export function ProfileSection() { > {uploading ? "Uploading..." : "Change avatar"} - {avatarUrl && ( + {avatarFilename && (