'use client'; // Complete Avatar Upload Flow for Vercel Blob // Includes image preview, crop preview (optional), upload, and update import { useState, useRef } from 'react'; import { put, del } from '@vercel/blob'; interface AvatarUploadProps { userId: string; currentAvatarUrl?: string; onAvatarUpdate?: (url: string) => void; } export function AvatarUploadFlow({ userId, currentAvatarUrl, onAvatarUpdate }: AvatarUploadProps) { const [avatarUrl, setAvatarUrl] = useState(currentAvatarUrl || ''); const [previewUrl, setPreviewUrl] = useState(''); const [file, setFile] = useState(null); const [isUploading, setIsUploading] = useState(false); const [error, setError] = useState(''); const fileInputRef = useRef(null); // Validate image file const validateImage = (file: File): string | null => { const maxSize = 5 * 1024 * 1024; // 5MB const allowedTypes = ['image/jpeg', 'image/png', 'image/webp']; if (!allowedTypes.includes(file.type)) { return 'Only JPEG, PNG, and WebP images are allowed'; } if (file.size > maxSize) { return 'Image must be smaller than 5MB'; } return null; }; // Handle file selection const handleFileSelect = (e: React.ChangeEvent) => { const selectedFile = e.target.files?.[0]; if (!selectedFile) return; const validationError = validateImage(selectedFile); if (validationError) { setError(validationError); return; } setFile(selectedFile); setError(''); // Create preview const reader = new FileReader(); reader.onloadend = () => { setPreviewUrl(reader.result as string); }; reader.readAsDataURL(selectedFile); }; // Handle upload const handleUpload = async () => { if (!file) return; setIsUploading(true); setError(''); try { // Delete old avatar if exists if (avatarUrl) { try { await del(avatarUrl); } catch (err) { console.error('Failed to delete old avatar:', err); // Continue with upload even if delete fails } } // Upload new avatar const blob = await put(`avatars/${userId}.jpg`, file, { access: 'public', contentType: file.type, addRandomSuffix: false // Always use same filename for user }); setAvatarUrl(blob.url); setPreviewUrl(''); setFile(null); // Reset file input if (fileInputRef.current) { fileInputRef.current.value = ''; } // Callback to parent if (onAvatarUpdate) { onAvatarUpdate(blob.url); } } catch (err) { console.error('Upload failed:', err); setError(err instanceof Error ? err.message : 'Upload failed'); } finally { setIsUploading(false); } }; // Cancel preview const handleCancel = () => { setFile(null); setPreviewUrl(''); setError(''); if (fileInputRef.current) { fileInputRef.current.value = ''; } }; // Trigger file input const triggerFileInput = () => { fileInputRef.current?.click(); }; return (

Profile Picture

{/* Current or Preview Avatar */}
{previewUrl || avatarUrl ? ( Avatar ) : ( )}
{/* Edit overlay */} {!previewUrl && ( )}
{/* Hidden file input */} {/* Error message */} {error && (
{error}
)} {/* File info */} {file && (

{file.name}

{(file.size / 1024).toFixed(1)} KB · {file.type}

)} {/* Action buttons */} {previewUrl ? (
) : ( )} {/* Current avatar URL */} {avatarUrl && !previewUrl && ( )}
); } // ============================================================================ // SERVER ACTION VERSION (Next.js Server Actions) // ============================================================================ /* 'use server'; import { put, del } from '@vercel/blob'; import { revalidatePath } from 'next/cache'; export async function updateAvatar(userId: string, formData: FormData) { const file = formData.get('avatar') as File; const currentAvatarUrl = formData.get('currentAvatarUrl') as string; if (!file) { throw new Error('No file provided'); } // Validate const allowedTypes = ['image/jpeg', 'image/png', 'image/webp']; if (!allowedTypes.includes(file.type)) { throw new Error('Invalid file type'); } if (file.size > 5 * 1024 * 1024) { throw new Error('File too large (max 5MB)'); } // Delete old avatar if (currentAvatarUrl) { try { await del(currentAvatarUrl); } catch (error) { console.error('Failed to delete old avatar:', error); } } // Upload new avatar const blob = await put(`avatars/${userId}.jpg`, file, { access: 'public', contentType: file.type, addRandomSuffix: false }); // Update database await db.update(users).set({ avatarUrl: blob.url }).where(eq(users.id, userId)); revalidatePath('/profile'); return blob.url; } */ // ============================================================================ // CLIENT COMPONENT USING SERVER ACTION // ============================================================================ /* 'use client'; import { useState } from 'react'; import { updateAvatar } from './actions'; export function AvatarUploadWithServerAction({ userId, currentAvatarUrl }: Props) { const [avatarUrl, setAvatarUrl] = useState(currentAvatarUrl || ''); const [previewUrl, setPreviewUrl] = useState(''); const [isUploading, setIsUploading] = useState(false); const [error, setError] = useState(''); async function handleSubmit(formData: FormData) { setIsUploading(true); setError(''); try { formData.append('currentAvatarUrl', avatarUrl); const newUrl = await updateAvatar(userId, formData); setAvatarUrl(newUrl); setPreviewUrl(''); } catch (err) { setError(err instanceof Error ? err.message : 'Upload failed'); } finally { setIsUploading(false); } } return (
); } */ // ============================================================================ // WITH IMAGE OPTIMIZATION (Sharp) // ============================================================================ /* // Server-side optimization before upload import sharp from 'sharp'; export async function uploadOptimizedAvatar(file: File) { const buffer = Buffer.from(await file.arrayBuffer()); // Resize and optimize const optimized = await sharp(buffer) .resize(400, 400, { fit: 'cover', position: 'center' }) .jpeg({ quality: 85 }) .toBuffer(); // Upload optimized version const blob = await put(`avatars/${userId}.jpg`, optimized, { access: 'public', contentType: 'image/jpeg' }); return blob.url; } */