From a63a1571ba946e691d88f06c021ea3b227a6d64e Mon Sep 17 00:00:00 2001 From: Mistral Vibe Date: Mon, 30 Mar 2026 19:40:33 +0200 Subject: [PATCH] feat: implement client-side image resizing for avatar uploads - Add resizeImage function to SettingsPage - Resize images larger than 2MB to max 800x800 pixels - Convert to JPEG with 80% quality to reduce file size - Add server-side validation for 10MB file size limit - Maintain aspect ratio during resizing - Log resizing details for debugging Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe --- api/src/rehearsalhub/routers/auth.py | 11 +++- web/src/pages/SettingsPage.tsx | 80 +++++++++++++++++++++++++++- 2 files changed, 89 insertions(+), 2 deletions(-) diff --git a/api/src/rehearsalhub/routers/auth.py b/api/src/rehearsalhub/routers/auth.py index 3b0fbfe..96c0500 100644 --- a/api/src/rehearsalhub/routers/auth.py +++ b/api/src/rehearsalhub/routers/auth.py @@ -83,7 +83,7 @@ async def upload_avatar( print(f"Avatar upload called for member {current_member.id}") print(f"File: {file.filename}, Content-Type: {file.content_type}") - # Validate file type and size + # Validate file type if not file.content_type.startswith("image/"): print("Invalid file type") raise HTTPException( @@ -91,6 +91,15 @@ async def upload_avatar( detail="Only image files are allowed" ) + # Validate file size (10MB limit) + max_size = 10 * 1024 * 1024 # 10MB + if file.size > max_size: + print(f"File too large: {file.size} bytes (max {max_size})") + raise HTTPException( + status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE, + detail=f"File too large. Maximum size is {max_size / 1024 / 1024}MB" + ) + # Create uploads directory if it doesn't exist upload_dir = "uploads/avatars" os.makedirs(upload_dir, exist_ok=True) diff --git a/web/src/pages/SettingsPage.tsx b/web/src/pages/SettingsPage.tsx index b37412e..d2404aa 100644 --- a/web/src/pages/SettingsPage.tsx +++ b/web/src/pages/SettingsPage.tsx @@ -44,6 +44,74 @@ function SettingsForm({ me, onBack }: { me: MemberRead; onBack: () => void }) { const [saved, setSaved] = useState(false); const [error, setError] = useState(null); + // Image resizing function + const resizeImage = (file: File, maxWidth: number, maxHeight: number): Promise => { + return new Promise((resolve, reject) => { + const img = new Image(); + const reader = new FileReader(); + + reader.onload = (event) => { + if (typeof event.target?.result !== 'string') { + reject(new Error('Failed to read file')); + return; + } + + img.onload = () => { + const canvas = document.createElement('canvas'); + let width = img.width; + let height = img.height; + + // Calculate new dimensions + if (width > height) { + if (width > maxWidth) { + height *= maxWidth / width; + width = maxWidth; + } + } else { + if (height > maxHeight) { + width *= maxHeight / height; + height = maxHeight; + } + } + + canvas.width = width; + canvas.height = height; + + const ctx = canvas.getContext('2d'); + if (!ctx) { + reject(new Error('Failed to get canvas context')); + return; + } + + ctx.drawImage(img, 0, 0, width, height); + + canvas.toBlob((blob) => { + if (!blob) { + reject(new Error('Failed to create blob')); + return; + } + + const resizedFile = new File([blob], file.name, { + type: 'image/jpeg', + lastModified: Date.now() + }); + + console.log(`Resized image from ${img.width}x${img.height} to ${width}x${height}`); + console.log(`File size reduced from ${file.size} to ${resizedFile.size} bytes`); + + resolve(resizedFile); + }, 'image/jpeg', 0.8); // JPEG quality 80% + }; + + img.onerror = reject; + img.src = event.target?.result; + }; + + reader.onerror = reject; + reader.readAsDataURL(file); + }); + }; + const saveMutation = useMutation({ mutationFn: () => updateSettings({ @@ -127,9 +195,19 @@ function SettingsForm({ me, onBack }: { me: MemberRead; onBack: () => void }) { if (file) { console.log("Selected file:", file.name, file.type, file.size); setUploading(true); + try { + // Check file size and resize if needed + const maxSize = 2 * 1024 * 1024; // 2MB + let processedFile = file; + + if (file.size > maxSize) { + console.log("File too large, resizing..."); + processedFile = await resizeImage(file, 800, 800); // Max 800x800 + } + const formData = new FormData(); - formData.append('file', file); + formData.append('file', processedFile, processedFile.name || file.name); console.log("Uploading file to /auth/me/avatar"); const response = await api.post('/auth/me/avatar', formData);