Epic: Member Profiles Type: Feature Status: Completed
Allow members to upload a custom profile picture from the edit profile page. Users can select or drag-and-drop an image file, see a preview before saving, and on save the image is uploaded to Supabase Storage. The resulting public URL is saved to the avatar_url column on the profiles table, replacing any OAuth-provided avatar.
- The edit profile page includes a profile picture section with the current avatar displayed
- Users can select an image file via a file input or drag-and-drop
- Only image files are accepted (JPEG, PNG, WebP) with a maximum size of 2 MB
- A preview of the selected image is shown before saving
- On save, the image is uploaded to a Supabase Storage bucket and the public URL is stored in
avatar_url - The previous uploaded avatar file is overwritten via upsert on re-upload
- Validation errors (wrong file type, file too large) are displayed to the user
- The updated avatar is reflected immediately on the profile page and navbar after saving
- Public Supabase Storage bucket named
avatars - File path convention:
{user_id}/avatar.{ext}(one file per user, overwritten on re-upload viaupsert: true) - Cache-busting query parameter (
?t=timestamp) appended to the public URL to avoid stale CDN caches
- SELECT (read): Public — anyone can view avatar images
- INSERT: Authenticated users can upload to their own folder (
auth.uid()::text = (storage.foldername(name))[1]) - UPDATE: Authenticated users can overwrite files in their own folder
- DELETE: Authenticated users can delete files in their own folder
- Creates the
avatarsbucket withpublic = true - Creates RLS policies for SELECT (public), INSERT, UPDATE, and DELETE (scoped to user's folder)
[storage.buckets.avatars]section withpublic = true,file_size_limit = "2MiB", allowed MIME types
A reusable 'use client' component for selecting and previewing an image before upload.
- Props:
currentImageUrl,displayName,onFileSelect,error - Renders the current avatar via the
Avatarcomponent with a hover overlay "Change" button - Supports click-to-select (hidden file input) and drag-and-drop with visual feedback
- Client-side validation: JPEG/PNG/WebP only, max 2 MB
- Generates a local preview URL via
URL.createObjectURL, cleaned up on unmount/change - "Remove new photo" button to clear the selection
- Inline error display for rejected files
ImageUploadcomponent rendered above the form fieldsuseStatefor selectedFile | nulland avatar upload error- Async
handleSubmit:- Runs existing client-side validation
- If a file is selected, uploads via
supabase.storage.from('avatars').upload(...)withupsert: true - Gets the public URL via
getPublicUrl()with cache-bust suffix - Sets
avatar_urlon the FormData - Calls
formAction(formData)insidestartTransition
- Passes
userId={auth.user.id}toEditProfileFormfor the storage upload path
- Reads optional
avatar_urlfrom FormData - If present, includes it in the
.update()call alongsidedisplay_nameandbio - Calls
revalidatePath('/', 'layout')after successful update to refresh the navbar avatar