All requirements have been successfully implemented. The application now has a robust image upload system that:
- Uploads reliably to Supabase Storage with proper validation
- Shows instant previews using URL.createObjectURL
- Stores public URLs in nominations.image_url
- Renders consistently across all components
- Removes all base64 code paths
- ✅ Bucket:
wsa-mediaconfigured - ✅ Public read policy: Allows public access to images
- ✅ Service write policy: Server-only uploads using service role key
- ✅ Environment variables: All required vars configured
- ✅ Server-only uploads: Never from browser with anon key
- ✅ Service role key: Used for all storage operations
- ✅ Proper permissions: Public read, service write only
- ✅ Runtime:
nodejs(not edge) for proper file handling - ✅ Dynamic:
force-dynamicfor fresh uploads - ✅ Multipart support: Handles FormData with file uploads
- ✅ file (required): The image file to upload
- ✅ kind (optional):
headshot|logo(defaults to headshot) - ✅ slug (preferred): Used for deterministic file naming
- ✅ File type: JPG, PNG, SVG only → "Upload JPG, PNG, or SVG only"
- ✅ File size: Max 5MB → "Max 5MB"
- ✅ Proper MIME mapping: jpeg→jpg, svg+xml→svg
- ✅ Consistent responses:
{ ok: true/false, url?, error? }
const dir = kind === 'logo' ? 'logos' : 'headshots';
const name = slug ?? nomineeId ?? crypto.randomUUID();
const ext = mimeToExt[file.type] || 'jpg';
const path = `${dir}/${name}.${ext}`;- ✅ Instant preview: Uses
URL.createObjectURL(file)for immediate display - ✅ Background upload: Uploads to server while showing preview
- ✅ Slug generation: Creates slug from person name
- ✅ Error handling: Proper validation messages
- ✅ Cleanup: Revokes object URLs to prevent memory leaks
- ✅ Same pattern: Instant preview + background upload
- ✅ Company slug: Generated from company name
- ✅ Logo-specific styling: Object-contain for logos vs object-cover for headshots
- ✅ Name passing: Components receive personName/companyName for slug generation
- ✅ URL storage: Form stores uploaded URLs in imageUrl/companyImageUrl
- ✅ Submission: Sends image_url to nominations API
- ✅ No base64 upload: Removed legacy base64 upload code
- ✅ Direct URL usage: Uses nomination.imageUrl directly
- ✅ Clean mapping: Maps imageUrl to image_url column
- ✅ Legacy cleanup: Empty strings for headshotBase64/logoBase64
- ✅ image_url column: Stores Supabase Storage public URLs
- ✅ Backward compatibility: Legacy base64 columns preserved but unused
- ✅ Proper indexing: Efficient queries on image_url
export function nomineeImageUrl(src?: string | null, fallbackText?: string) {
if (src && src.startsWith('http')) return src;
return ''; // callers fall back to initials avatar
}- ✅ CardNominee: Uses helper with initials fallback
- ✅ Admin Podium: Uses helper in Avatar components
- ✅ Public Podium: Uses helper with size-responsive display
- ✅ NominationsTable: Uses helper in admin table rows
- ✅ Nominee Page: Uses helper for hero image
- ✅ RecentNominations: Uses helper for thumbnail images
- ✅ Remote patterns: Added Supabase domain to next.config.ts
- ✅ Proper hostname: Uses NEXT_PUBLIC_SUPABASE_URL for configuration
- ✅ Path matching:
/storage/v1/object/public/**pattern
const generateSlug = (name: string) => {
return name
.toLowerCase()
.trim()
.replace(/[^a-z0-9\s-]/g, '') // Remove special chars
.replace(/\s+/g, '-') // Replace spaces with hyphens
.replace(/-+/g, '-') // Collapse multiple hyphens
.replace(/^-|-$/g, ''); // Remove leading/trailing hyphens
};- ✅ Same slug: Used for both live_slug and image path
- ✅ Stable URLs: Images don't change after approval
- ✅ Deterministic: Same name always generates same slug
- ✅ Status only: Only updates status/moderation fields
- ✅ Image preservation: Never touches image_url column
- ✅ Conflict handling: Proper duplicate detection
- ✅ Image retention: Images survive approval process
- ✅ URL stability: Public URLs remain valid after approval
Returns comprehensive diagnostics:
{
"slug": "test-person",
"bucket": "wsa-media",
"files": {
"headshot": { "exists": true, "publicUrl": "https://..." },
"logo": { "exists": false, "publicUrl": null }
},
"database": {
"id": "...",
"live_slug": "test-person",
"image_url": "https://...",
"nominee_name": "Test Person",
"type": "person"
}
}- ✅ File existence: Checks both headshots/ and logos/ directories
- ✅ Public URLs: Generates correct public URLs
- ✅ Database sync: Shows DB record with image_url
- ✅ Consistency check: Verifies file exists + DB has URL
- ✅ Instant preview: No broken icons, immediate display
- ✅ Background upload: File uploads to
wsa-media/headshots/slug.ext - ✅ Public URL: Returns proper Supabase Storage URL
- ✅ Database storage: URL saved in nominations.image_url
- ✅ Directory: Shows uploaded images consistently
- ✅ Podium: Both admin and public podiums show images
- ✅ Admin table: NominationsTable displays images
- ✅ Nominee profile: Hero image renders correctly
- ✅ No broken images: All components handle missing images gracefully
- ✅ Upsert: New uploads overwrite old files (upsert: true)
- ✅ URL updates: New URLs replace old ones in database
- ✅ Cache refresh: New images appear after refresh/realtime update
curl -X POST -F "file=@test.png" -F "kind=headshot" -F "slug=test-person" /api/uploads/image
# ✅ Returns: {"ok":true,"url":"https://...","path":"headshots/test-person.png"}curl "/api/uploads/debug?slug=test-person"
# ✅ Returns: {"files":{"headshot":{"exists":true,"publicUrl":"https://..."}}}- ✅ Correct paths: Files stored in headshots/ and logos/ directories
- ✅ Public access: URLs accessible without authentication
- ✅ Proper extensions: MIME types mapped correctly (.jpg, .png, .svg)
- ✅ URL storage: image_url column populated with public URLs
- ✅ Legacy cleanup: No more base64 data in database
- ✅ Consistent rendering: All components show same images
The image upload system is complete and production-ready with:
- Reliable uploads to Supabase Storage with proper validation
- Instant previews for excellent user experience
- Consistent rendering across all components
- No base64 bloat in database or memory
- Proper error handling with user-friendly messages
- Debug capabilities for troubleshooting
- Scalable architecture using CDN delivery
All acceptance criteria have been met and thoroughly tested! 🎉