feat(recipes): client-side image editor + modernize /add to match /edit
CI / update (push) Has been cancelled

Add a from-scratch photo editor (crop, max-resolution scale-to-fit,
WebP quality with live final-size + dimensions readout) that opens on
image pick in the recipe add/edit flow. Conversion uses the browser's
canvas WebP encoder (sharp can't run client-side); crop, scale and the
size readout are built by hand.

Server now stores the client WebP full image byte-for-byte (passthrough)
so the on-disk file matches the user's chosen quality/size; sharp still
derives the 800px thumb and OKLAB colour. Non-WebP uploads keep the old
q90 re-encode fallback.

Rework /add to reuse EditTitleImgParallax (parallax hero +
titleExtras/below-hero layout, shape-tile Backform, SaveFab + optional
translation), replacing the antiquated CardAdd card. Move the edit/remove
image controls into the hero, below the fixed header. Delete now-dead
CardAdd and RecipeEditor.
This commit is contained in:
2026-05-30 15:54:28 +02:00
parent fb54f6907f
commit cd7912fa8f
9 changed files with 1478 additions and 782 deletions
+18 -11
View File
@@ -1,4 +1,5 @@
import path from 'path';
import { writeFile } from 'fs/promises';
import sharp from 'sharp';
import { generateImageHashFromBuffer, getHashedFilename } from '$utils/imageHash';
import { validateImageFile } from '$utils/imageValidation';
@@ -132,21 +133,27 @@ export async function processAndSaveRecipeImage(
unhashed: unhashedFilename
});
// Process image with Sharp - convert to WebP format
// Save full size - both hashed and unhashed versions
console.log('[ImageProcessing] Converting to WebP and generating full size...');
const fullBuffer = await sharp(buffer)
.toFormat('webp')
.webp({ quality: 90 }) // High quality for full size
.toBuffer();
console.log('[ImageProcessing] Full size buffer created, size:', fullBuffer.length, 'bytes');
// Full size: the client photo editor already crops, scales and encodes WebP at
// the user's chosen quality. Store that byte-for-byte so the on-disk file matches
// the size the user saw in the editor — re-encoding through sharp would silently
// re-compress and discard their quality/size choice.
// Fallback (non-webp upload, e.g. editor bypassed): re-encode to WebP q90 as before.
const fullHashedPath = path.join(imageDir, 'rezepte', 'full', hashedFilename);
const fullUnhashedPath = path.join(imageDir, 'rezepte', 'full', unhashedFilename);
let fullBuffer: Buffer;
if (file.type === 'image/webp') {
console.log('[ImageProcessing] Client WebP detected — storing full size as-is (passthrough)');
fullBuffer = buffer;
} else {
console.log('[ImageProcessing] Non-WebP upload — re-encoding full size to WebP q90...');
fullBuffer = await sharp(buffer).toFormat('webp').webp({ quality: 90 }).toBuffer();
}
console.log('[ImageProcessing] Full size buffer ready, size:', fullBuffer.length, 'bytes');
console.log('[ImageProcessing] Saving full size to:', { fullHashedPath, fullUnhashedPath });
await sharp(fullBuffer).toFile(fullHashedPath);
await sharp(fullBuffer).toFile(fullUnhashedPath);
await writeFile(fullHashedPath, fullBuffer);
await writeFile(fullUnhashedPath, fullBuffer);
console.log('[ImageProcessing] Full size images saved');
// Save thumbnail (800px width) - both hashed and unhashed versions