From a48ae3ff3c1de37fc573f40ab2986bdaee12b675 Mon Sep 17 00:00:00 2001 From: Alexander Bocken Date: Thu, 15 Jan 2026 14:18:50 +0100 Subject: [PATCH] refactor: defer recipe image upload until form submission Changed recipe image upload behavior to only process images when the form is submitted, rather than immediately on file selection. This prevents orphaned image files when users abandon the form. Changes: - CardAdd.svelte: Preview only, store File object instead of uploading - Created imageProcessing.ts: Shared utility for image processing - Add/edit page clients: Use selected_image_file instead of filename - Add/edit page servers: Process and save images during form submission - Images are validated, hashed, and saved in multiple formats on submit Benefits: - No orphaned files from abandoned forms - Faster initial file selection experience - Server-side image processing ensures security validation - Cleaner architecture with shared processing logic --- src/lib/components/CardAdd.svelte | 70 +++------------- .../add/+page.server.ts | 33 ++++++-- .../[recipeLang=recipeLang]/add/+page.svelte | 14 ++-- .../edit/[name]/+page.server.ts | 39 ++++++--- .../edit/[name]/+page.svelte | 20 +++-- src/utils/imageProcessing.ts | 79 +++++++++++++++++++ 6 files changed, 162 insertions(+), 93 deletions(-) create mode 100644 src/utils/imageProcessing.ts diff --git a/src/lib/components/CardAdd.svelte b/src/lib/components/CardAdd.svelte index 5a86fa5..569813b 100644 --- a/src/lib/components/CardAdd.svelte +++ b/src/lib/components/CardAdd.svelte @@ -8,12 +8,12 @@ import { onMount } from 'svelte' let { card_data = $bindable(), image_preview_url = $bindable(), - uploaded_image_filename = $bindable(''), + selected_image_file = $bindable(null), short_name = '' } = $props<{ card_data: any, image_preview_url: string, - uploaded_image_filename?: string, + selected_image_file?: File | null, short_name: string }>(); @@ -52,12 +52,11 @@ if(!card_data.tags){ //locals let new_tag = $state(""); -let uploading = $state(false); let upload_error = $state(""); /** - * Handles image file selection and upload - * Now uses FormData instead of base64 encoding for better security and performance + * Handles image file selection and preview + * The actual upload will happen when the form is submitted */ export async function show_local_image(){ const file = this.files[0]; @@ -81,57 +80,15 @@ export async function show_local_image(){ return; } - // Show preview immediately + // Show preview and store file for later upload image_preview_url = URL.createObjectURL(file); + selected_image_file = file; upload_error = ""; - - // Upload to server - try { - uploading = true; - - // Validate short_name is provided - if (!short_name || short_name.trim() === '') { - upload_error = 'Please provide a short name (URL) before uploading an image.'; - alert(upload_error); - uploading = false; - return; - } - - // Create FormData for upload - const formData = new FormData(); - formData.append('image', file); - formData.append('name', short_name.trim()); - - const response = await fetch('/api/rezepte/img/add', { - method: 'POST', - body: formData - }); - - if (!response.ok) { - const error_data = await response.json(); - throw new Error(error_data.message || 'Upload failed'); - } - - const result = await response.json(); - uploaded_image_filename = result.filename; - upload_error = ""; - - } catch (error: any) { - console.error('Image upload error:', error); - upload_error = error.message || 'Failed to upload image. Please try again.'; - alert(`Upload failed: ${upload_error}`); - - // Clear preview on error - image_preview_url = ""; - uploaded_image_filename = ""; - } finally { - uploading = false; - } } export function remove_selected_images(){ image_preview_url = ""; - uploaded_image_filename = ""; + selected_image_file = null; upload_error = ""; } @@ -430,11 +387,6 @@ input::placeholder{ .tag_input{ width: 12ch; } -.upload-spinner { - color: white; - font-size: 1.2rem; - font-weight: bold; -} @@ -453,15 +405,11 @@ input::placeholder{ {/if} -