fix: use simple event handler for file selection instead of $effect
All checks were successful
CI / update (push) Successful in 1m21s

Replace $effect + bind:files approach with straightforward onchange handler:
- Use event.currentTarget.files[0] to get selected file
- Avoid reactive complexity that caused infinite loops
- Add bind:this reference to file input for clearing
- Clean implementation that works reliably in Svelte 5
This commit is contained in:
2026-01-20 17:29:39 +01:00
parent 3fb0a72014
commit 0459ef26bc

View File

@@ -17,23 +17,21 @@ let {
short_name: string short_name: string
} = $props(); } = $props();
// Local state for file input binding (Svelte 5 best practice)
let files = $state<FileList | null>(null);
let upload_error = $state("");
// Constants for validation // Constants for validation
const ALLOWED_MIME_TYPES = ['image/webp', 'image/jpeg', 'image/jpg', 'image/png']; const ALLOWED_MIME_TYPES = ['image/webp', 'image/jpeg', 'image/jpg', 'image/png'];
const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
// React to file selection using $effect (Svelte 5 best practice) // Handle file selection via onchange event
// This effect intentionally has side effects - it's reacting to user file selection function handleFileSelect(event: Event) {
$effect(() => { const input = event.currentTarget as HTMLInputElement;
const file = files?.[0]; const file = input.files?.[0];
// Only process when there's an actual file selected if (!file) {
if (!file) return; console.log('[CardAdd] No file selected');
return;
}
console.log('[CardAdd] File selected via bind:files:', { console.log('[CardAdd] File selected:', {
name: file.name, name: file.name,
size: file.size, size: file.size,
type: file.type type: file.type
@@ -41,39 +39,35 @@ $effect(() => {
// Validate MIME type // Validate MIME type
if (!ALLOWED_MIME_TYPES.includes(file.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); console.error('[CardAdd] Invalid MIME type:', file.type);
alert(upload_error); alert('Invalid file type. Please upload a JPEG, PNG, or WebP image.');
files = null; input.value = '';
return; return;
} }
console.log('[CardAdd] MIME type valid:', file.type); console.log('[CardAdd] MIME type valid:', file.type);
// Validate file size // Validate file size
if (file.size > MAX_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); console.error('[CardAdd] File too large:', file.size);
alert(upload_error); alert(`File too large. Maximum size is 5MB. Your file is ${(file.size / 1024 / 1024).toFixed(2)}MB.`);
files = null; input.value = '';
return; return;
} }
console.log('[CardAdd] File size valid:', file.size, 'bytes'); 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 // Clean up old preview URL if exists
if (image_preview_url && image_preview_url.startsWith('blob:')) { if (image_preview_url && image_preview_url.startsWith('blob:')) {
URL.revokeObjectURL(image_preview_url); URL.revokeObjectURL(image_preview_url);
} }
// Create preview and store file
image_preview_url = URL.createObjectURL(file); image_preview_url = URL.createObjectURL(file);
selected_image_file = file; selected_image_file = file;
console.log('[CardAdd] Image preview created, file stored for upload:', { console.log('[CardAdd] Image preview created, file stored for upload:', {
previewUrl: image_preview_url, previewUrl: image_preview_url,
fileName: selected_image_file.name fileName: file.name
}); });
}); }
// Check if initial image_preview_url redirects to placeholder // Check if initial image_preview_url redirects to placeholder
onMount(() => { onMount(() => {
@@ -111,6 +105,9 @@ if (!card_data.tags) {
// Tag management // Tag management
let new_tag = $state(""); let new_tag = $state("");
// Reference to file input for clearing
let fileInput: HTMLInputElement;
function remove_selected_images() { function remove_selected_images() {
console.log('[CardAdd] Removing selected image'); console.log('[CardAdd] Removing selected image');
if (image_preview_url && image_preview_url.startsWith('blob:')) { if (image_preview_url && image_preview_url.startsWith('blob:')) {
@@ -118,8 +115,10 @@ function remove_selected_images() {
} }
image_preview_url = ""; image_preview_url = "";
selected_image_file = null; selected_image_file = null;
files = null; // Reset the file input
upload_error = ""; if (fileInput) {
fileInput.value = '';
}
} }
@@ -440,7 +439,7 @@ input::placeholder{
<svg class="upload over_img" xmlns="http://www.w3.org/2000/svg" height="1em" viewBox="0 0 512 512"><path d="M288 109.3V352c0 17.7-14.3 32-32 32s-32-14.3-32-32V109.3l-73.4 73.4c-12.5 12.5-32.8 12.5-45.3 0s-12.5-32.8 0-45.3l128-128c12.5-12.5 32.8-12.5 45.3 0l128 128c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0L288 109.3zM64 352H192c0 35.3 28.7 64 64 64s64-28.7 64-64H448c35.3 0 64 28.7 64 64v32c0 35.3-28.7 64-64 64H64c-35.3 0-64-28.7-64-64V416c0-35.3 28.7-64 64-64zM432 456a24 24 0 1 0 0-48 24 24 0 1 0 0 48z"/></svg> <svg class="upload over_img" xmlns="http://www.w3.org/2000/svg" height="1em" viewBox="0 0 512 512"><path d="M288 109.3V352c0 17.7-14.3 32-32 32s-32-14.3-32-32V109.3l-73.4 73.4c-12.5 12.5-32.8 12.5-45.3 0s-12.5-32.8 0-45.3l128-128c12.5-12.5 32.8-12.5 45.3 0l128 128c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0L288 109.3zM64 352H192c0 35.3 28.7 64 64 64s64-28.7 64-64H448c35.3 0 64 28.7 64 64v32c0 35.3-28.7 64-64 64H64c-35.3 0-64-28.7-64-64V416c0-35.3 28.7-64 64-64zM432 456a24 24 0 1 0 0-48 24 24 0 1 0 0 48z"/></svg>
</label> </label>
</div> </div>
<input type="file" id="img_picker" accept="image/webp,image/jpeg,image/jpg,image/png" bind:files> <input type="file" id="img_picker" accept="image/webp,image/jpeg,image/jpg,image/png" onchange={handleFileSelect} bind:this={fileInput}>
<div class=title> <div class=title>
<input class=category placeholder=Kategorie... bind:value={card_data.category}/> <input class=category placeholder=Kategorie... bind:value={card_data.category}/>
<div> <div>