diff --git a/src/lib/components/CardAdd.svelte b/src/lib/components/CardAdd.svelte index b1cf476..7a51519 100644 --- a/src/lib/components/CardAdd.svelte +++ b/src/lib/components/CardAdd.svelte @@ -7,28 +7,86 @@ import { onMount } from 'svelte' let { card_data = $bindable(), - image_preview_url = $bindable(), - selected_image_file = $bindable(null), + image_preview_url = $bindable(''), + selected_image_file = $bindable(null), short_name = '' -} = $props<{ +}: { card_data: any, image_preview_url: string, - selected_image_file?: File | null, + selected_image_file: File | null, short_name: string -}>(); +} = $props(); -// Check if image redirects to placeholder by attempting to load it +// Local state for file input binding (Svelte 5 best practice) +let files = $state(null); +let upload_error = $state(""); + +// Constants for validation +const ALLOWED_MIME_TYPES = ['image/webp', 'image/jpeg', 'image/jpg', 'image/png']; +const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB + +// React to file selection using $effect (Svelte 5 best practice) +// This effect intentionally has side effects - it's reacting to user file selection +$effect(() => { + const file = files?.[0]; + + // Only process when there's an actual file selected + if (!file) return; + + console.log('[CardAdd] File selected via bind:files:', { + name: file.name, + size: file.size, + type: file.type + }); + + // Validate MIME type + if (!ALLOWED_MIME_TYPES.includes(file.type)) { + upload_error = 'Invalid file type. Please upload a JPEG, PNG, or WebP image.'; + console.error('[CardAdd] Invalid MIME type:', file.type); + alert(upload_error); + files = null; + return; + } + console.log('[CardAdd] MIME type valid:', file.type); + + // Validate file size + if (file.size > MAX_FILE_SIZE) { + upload_error = `File too large. Maximum size is 5MB. Your file is ${(file.size / 1024 / 1024).toFixed(2)}MB.`; + console.error('[CardAdd] File too large:', file.size); + alert(upload_error); + files = null; + return; + } + console.log('[CardAdd] File size valid:', file.size, 'bytes'); + + // Validation passed - create preview and update bindable prop + upload_error = ""; + + // Clean up old preview URL if exists + if (image_preview_url && image_preview_url.startsWith('blob:')) { + URL.revokeObjectURL(image_preview_url); + } + + image_preview_url = URL.createObjectURL(file); + selected_image_file = file; + console.log('[CardAdd] Image preview created, file stored for upload:', { + previewUrl: image_preview_url, + fileName: selected_image_file.name + }); +}); + +// Check if initial image_preview_url redirects to placeholder onMount(() => { - if (image_preview_url) { + if (image_preview_url && !image_preview_url.startsWith('blob:')) { const img = new Image(); img.onload = () => { // Check if this is the placeholder image (150x150) if (img.naturalWidth === 150 && img.naturalHeight === 150) { - console.log('Detected placeholder image (150x150), clearing preview'); + console.log('[CardAdd] Detected placeholder image (150x150), clearing preview'); image_preview_url = "" } else { - console.log('Real image loaded:', { + console.log('[CardAdd] Real image loaded:', { url: image_preview_url, naturalWidth: img.naturalWidth, naturalHeight: img.naturalHeight @@ -37,98 +95,55 @@ onMount(() => { }; img.onerror = () => { - // Image failed to load - could be 404 or network error - console.log('Image failed to load, clearing preview'); + console.log('[CardAdd] Image failed to load, clearing preview'); image_preview_url = "" }; img.src = image_preview_url; } -}) +}); -if(!card_data.tags){ +// Initialize tags if needed +if (!card_data.tags) { card_data.tags = [] } -//locals +// Tag management let new_tag = $state(""); -let upload_error = $state(""); -/** - * Handles image file selection and preview - * The actual upload will happen when the form is submitted - */ -export async function show_local_image(event: Event){ - const input = event.target as HTMLInputElement; - const file = input.files?.[0]; - if (!file) { - console.log('[CardAdd] No file selected'); - return; - } - - console.log('[CardAdd] File selected:', { - name: file.name, - size: file.size, - type: file.type - }); - - // Client-side validation - const allowed_mime_types = ['image/webp', 'image/jpeg', 'image/jpg', 'image/png']; - const max_size = 5 * 1024 * 1024; // 5MB - - // Validate MIME type - if(!allowed_mime_types.includes(file.type)) { - upload_error = 'Invalid file type. Please upload a JPEG, PNG, or WebP image.'; - console.error('[CardAdd] Invalid MIME type:', file.type); - alert(upload_error); - return; - } - console.log('[CardAdd] MIME type valid:', file.type); - - // Validate file size - if(file.size > max_size) { - upload_error = `File too large. Maximum size is 5MB. Your file is ${(file.size / 1024 / 1024).toFixed(2)}MB.`; - console.error('[CardAdd] File too large:', file.size); - alert(upload_error); - return; - } - console.log('[CardAdd] File size valid:', file.size, 'bytes'); - - // Show preview and store file for later upload - image_preview_url = URL.createObjectURL(file); - selected_image_file = file; - upload_error = ""; - console.log('[CardAdd] Image preview created and file stored for upload'); -} - -export function remove_selected_images(){ +function remove_selected_images() { console.log('[CardAdd] Removing selected image'); + if (image_preview_url && image_preview_url.startsWith('blob:')) { + URL.revokeObjectURL(image_preview_url); + } image_preview_url = ""; selected_image_file = null; + files = null; upload_error = ""; } -export function add_to_tags(){ - if(new_tag){ - if(! card_data.tags.includes(new_tag)){ - card_data.tags.push(new_tag) - card_data.tags = card_data.tags; - } +function add_to_tags() { + if (new_tag && !card_data.tags.includes(new_tag)) { + card_data.tags = [...card_data.tags, new_tag]; } - new_tag = "" + new_tag = ""; } -export function remove_from_tags(tag){ - card_data.tags = card_data.tags.filter(item => item !== tag) + +function remove_from_tags(tag: string) { + card_data.tags = card_data.tags.filter((item: string) => item !== tag); } -export function add_on_enter(event){ - if(event.key === 'Enter'){ - add_to_tags() + +function add_on_enter(event: KeyboardEvent) { + if (event.key === 'Enter') { + event.preventDefault(); + add_to_tags(); } } -export function remove_on_enter(event, tag){ - if(event.key === 'Enter'){ - card_data.tags = card_data.tags.filter(item => item !== tag) + +function remove_on_enter(event: KeyboardEvent, tag: string) { + if (event.key === 'Enter') { + card_data.tags = card_data.tags.filter((item: string) => item !== tag); } } @@ -425,7 +440,7 @@ input::placeholder{ - +
@@ -433,7 +448,7 @@ input::placeholder{

- {#each card_data.tags as tag} + {#each card_data.tags as tag (tag)}
remove_on_enter(event, tag)} onclick={() => remove_from_tags(tag)} aria-label="Tag {tag} entfernen">{tag}
{/each}