feat: image upload optimization with sharp, thumbnails, and structured error codes (#304)

- Add sharp for server-side image processing (WebP conversion + thumbnails)
- New shared backend utility for image upload, optimization, and cleanup
- Return structured error codes from upload endpoints (IMAGE_TOO_LARGE, INVALID_TYPE, etc.)
- Frontend error code mapping with i18n support (EN + DE)
- MedicationAvatar tries thumbnail first, falls back to full image
- Error display in MedicationsPage, MobileEditModal, and Auth avatar upload

Closes #302
This commit is contained in:
Daniel Volz
2026-02-24 23:52:59 +01:00
committed by GitHub
parent 7a32b2045e
commit 96b2a0c96f
15 changed files with 916 additions and 93 deletions
+28 -1
View File
@@ -2,6 +2,8 @@
// MedicationAvatar Component
// =============================================================================
import { useEffect, useState } from "react";
export type MedicationAvatarProps = {
name: string;
imageUrl?: string | null;
@@ -9,6 +11,12 @@ export type MedicationAvatarProps = {
};
export function MedicationAvatar({ name, imageUrl, size = "sm" }: MedicationAvatarProps) {
const [thumbFailed, setThumbFailed] = useState(false);
useEffect(() => {
setThumbFailed(false);
}, [imageUrl]);
const initials =
name
.split(" ")
@@ -19,7 +27,26 @@ export function MedicationAvatar({ name, imageUrl, size = "sm" }: MedicationAvat
const sizeClass = `med-avatar med-avatar-${size}`;
if (imageUrl) {
return <img src={`/api/images/${imageUrl}`} alt={name} className={sizeClass} />;
const normalizedImageUrl = imageUrl.toLowerCase();
const shouldUseThumbFirst = normalizedImageUrl.endsWith(".webp");
const extIndex = imageUrl.lastIndexOf(".");
const baseName = extIndex > 0 ? imageUrl.slice(0, extIndex) : imageUrl;
const thumbSrc = `/api/images/${baseName}-thumb.webp`;
const fullSrc = `/api/images/${imageUrl}`;
const resolvedSrc = shouldUseThumbFirst && !thumbFailed ? thumbSrc : fullSrc;
return (
<img
src={resolvedSrc}
alt={name}
className={sizeClass}
loading="lazy"
decoding="async"
onError={() => {
if (shouldUseThumbFirst && !thumbFailed) setThumbFailed(true);
}}
/>
);
}
return <div className={`${sizeClass} med-avatar-initials`}>{initials}</div>;
}