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 <vibe@mistral.ai>
This commit is contained in:
@@ -83,7 +83,7 @@ async def upload_avatar(
|
|||||||
print(f"Avatar upload called for member {current_member.id}")
|
print(f"Avatar upload called for member {current_member.id}")
|
||||||
print(f"File: {file.filename}, Content-Type: {file.content_type}")
|
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/"):
|
if not file.content_type.startswith("image/"):
|
||||||
print("Invalid file type")
|
print("Invalid file type")
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@@ -91,6 +91,15 @@ async def upload_avatar(
|
|||||||
detail="Only image files are allowed"
|
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
|
# Create uploads directory if it doesn't exist
|
||||||
upload_dir = "uploads/avatars"
|
upload_dir = "uploads/avatars"
|
||||||
os.makedirs(upload_dir, exist_ok=True)
|
os.makedirs(upload_dir, exist_ok=True)
|
||||||
|
|||||||
@@ -44,6 +44,74 @@ function SettingsForm({ me, onBack }: { me: MemberRead; onBack: () => void }) {
|
|||||||
const [saved, setSaved] = useState(false);
|
const [saved, setSaved] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// Image resizing function
|
||||||
|
const resizeImage = (file: File, maxWidth: number, maxHeight: number): Promise<File> => {
|
||||||
|
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({
|
const saveMutation = useMutation({
|
||||||
mutationFn: () =>
|
mutationFn: () =>
|
||||||
updateSettings({
|
updateSettings({
|
||||||
@@ -127,9 +195,19 @@ function SettingsForm({ me, onBack }: { me: MemberRead; onBack: () => void }) {
|
|||||||
if (file) {
|
if (file) {
|
||||||
console.log("Selected file:", file.name, file.type, file.size);
|
console.log("Selected file:", file.name, file.type, file.size);
|
||||||
setUploading(true);
|
setUploading(true);
|
||||||
|
|
||||||
try {
|
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();
|
const formData = new FormData();
|
||||||
formData.append('file', file);
|
formData.append('file', processedFile, processedFile.name || file.name);
|
||||||
|
|
||||||
console.log("Uploading file to /auth/me/avatar");
|
console.log("Uploading file to /auth/me/avatar");
|
||||||
const response = await api.post<MemberRead>('/auth/me/avatar', formData);
|
const response = await api.post<MemberRead>('/auth/me/avatar', formData);
|
||||||
|
|||||||
Reference in New Issue
Block a user