refactor: migrate all Svelte components from Svelte 4 to Svelte 5 runes

- Replace `export let` with `$props()` and `$bindable()` across all components
- Replace `$:` reactive statements with `$derived()` and `$effect()`
- Replace `createEventDispatcher` with callback props (e.g. `onchange`)
- Replace `on:event` directives with inline event handlers (`onclick`, `oninput`, etc.)
- Replace `<slot />` with `{@render children()}` in layout
- Use `untrack()` for SSR-safe $state initialization from reactive props
- Replace `blur` + `setTimeout` anti-pattern in TagInput with `clickOutside` action
- Fix `page` store usage in layout to use `$app/state` directly
- 0 errors, 0 warnings after svelte-check

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-03-17 11:43:26 +01:00
parent 25e095ea47
commit 4417fc9828
14 changed files with 388 additions and 441 deletions

View File

@@ -1,24 +1,24 @@
<script lang="ts">
export let data;
$: doc = data.document;
let { data } = $props();
// Instead of a direct link, we use a reactive variable for the Blob URL
let fileUrl = '';
let isLoading = false;
let error = '';
const doc = $derived(data.document);
// Reactive statement: Whenever the document ID changes, load the file
$: if (doc?.id && doc?.filePath) {
loadFile(doc.id);
}
let fileUrl = $state('');
let isLoading = $state(false);
let error = $state('');
$effect(() => {
if (doc?.id && doc?.filePath) {
loadFile(doc.id);
}
});
async function loadFile(id: string) {
isLoading = true;
error = '';
fileUrl = ''; // Reset previous URL
fileUrl = '';
try {
// 1. Fetch with current authentication
const response = await fetch(`/api/documents/${id}/file`);
if (!response.ok) {
@@ -26,10 +26,7 @@
throw new Error('Fehler beim Laden der Datei');
}
// 2. Create a Blob from the data
const blob = await response.blob();
// 3. Create a temporary URL for this Blob
fileUrl = URL.createObjectURL(blob);
} catch (e) {
@@ -186,7 +183,6 @@
{#if doc.documentLocation}
<div class="flex items-start group">
<span class="text-brand-mint w-8 mt-0.5">
<!-- Archive Box Icon -->
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
@@ -209,7 +205,6 @@
{#if doc.tags && doc.tags.length > 0}
<div class="flex items-start group">
<span class="text-brand-mint w-8 mt-0.5">
<!-- Tag Icon -->
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
@@ -324,7 +319,7 @@
</div>
</div>
<!-- 3. INHALT GROUP (Merged Summary & Transcription) -->
<!-- 3. INHALT GROUP -->
{#if doc.summary || doc.transcription}
<div>
<h3
@@ -334,12 +329,9 @@
</h3>
<div class="space-y-6">
<!-- Summary Sub-Section -->
{#if doc.summary}
<div>
<span class="text-xs font-sans text-gray-400 block mb-2 uppercase"
>Zusammenfassung</span
>
<span class="text-xs font-sans text-gray-400 block mb-2 uppercase">Zusammenfassung</span>
<div
class="bg-brand-sand/30 p-5 rounded border border-brand-sand text-sm font-serif text-brand-navy leading-relaxed whitespace-pre-wrap"
>
@@ -348,12 +340,9 @@
</div>
{/if}
<!-- Transcription Sub-Section -->
{#if doc.transcription}
<div>
<span class="text-xs font-sans text-gray-400 block mb-2 uppercase"
>Transkription</span
>
<span class="text-xs font-sans text-gray-400 block mb-2 uppercase">Transkription</span>
<div
class="bg-brand-sand/30 p-5 rounded border border-brand-sand text-sm font-serif text-brand-navy leading-relaxed whitespace-pre-wrap"
>
@@ -376,7 +365,6 @@
<!-- RIGHT: PREVIEW AREA -->
<main class="flex-1 bg-[#2A2A2A] relative flex flex-col items-center justify-center">
{#if isLoading}
<!-- Loading Spinner -->
<div class="text-brand-mint flex flex-col items-center">
<svg
class="animate-spin h-8 w-8 mb-4"
@@ -398,7 +386,6 @@
<div class="text-gray-400 text-center px-4">
<p class="font-serif mb-2">{error}</p>
{#if doc.filePath}
<!-- Direct link as fallback -->
<a
href={`/api/documents/${doc.id}/file`}
target="_blank"
@@ -409,10 +396,8 @@
{/if}
</div>
{:else if !doc.filePath}
<!-- No File State -->
<div class="flex flex-col items-center text-gray-400">
<div class="bg-white/5 p-8 rounded-full mb-6">
<!-- Icon... -->
<svg class="w-12 h-12 opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24"
><path
stroke-linecap="round"
@@ -425,14 +410,12 @@
<p class="font-sans text-sm tracking-wide uppercase">Kein Scan vorhanden</p>
</div>
{:else if fileUrl && doc.originalFilename.toLowerCase().endsWith('.pdf')}
<!-- PDF Iframe with Blob URL -->
<iframe
src={fileUrl}
title="Document Preview"
class="w-full h-full border-none bg-white"
></iframe>
{:else if fileUrl}
<!-- Image with Blob URL -->
<div class="w-full h-full flex items-center justify-center overflow-auto p-8">
<img
src={fileUrl}

View File

@@ -3,14 +3,14 @@
import TagInput from '$lib/components/TagInput.svelte';
import PersonTypeahead from '$lib/components/PersonTypeahead.svelte';
import PersonMultiSelect from '$lib/components/PersonMultiSelect.svelte';
import { untrack } from 'svelte';
export let data;
export let form;
let { data, form } = $props();
let { document: doc } = data;
let tags = doc.tags ? doc.tags.map((t: any) => t.name) : [];
let senderId = doc.sender?.id ?? '';
let selectedReceivers = doc.receivers ?? [];
let { document: doc } = untrack(() => data);
let tags = $state(doc.tags ? doc.tags.map((t: { name: string }) => t.name) : []);
let senderId = $state(doc.sender?.id ?? '');
let selectedReceivers = $state(doc.receivers ?? []);
function isoToGerman(iso: string): string {
if (!iso || !/^\d{4}-\d{2}-\d{2}$/.test(iso)) return '';
@@ -25,9 +25,11 @@
return `${y}-${m}-${d}`;
}
let dateDisplay = isoToGerman(doc.documentDate ?? '');
let dateIso = doc.documentDate ?? '';
let dateDirty = false;
let dateDisplay = $state(isoToGerman(doc.documentDate ?? ''));
let dateIso = $state(doc.documentDate ?? '');
let dateDirty = $state(false);
const dateInvalid = $derived(dateDirty && dateDisplay.length > 0 && dateIso === '');
function handleDateInput(e: Event) {
const input = e.target as HTMLInputElement;
@@ -45,8 +47,6 @@
dateIso = germanToIso(formatted);
dateDirty = true;
}
$: dateInvalid = dateDirty && dateDisplay.length > 0 && dateIso === '';
</script>
<div class="max-w-4xl mx-auto py-8 px-4">
@@ -84,7 +84,7 @@
type="text"
inputmode="numeric"
value={dateDisplay}
on:input={handleDateInput}
oninput={handleDateInput}
placeholder="TT.MM.JJJJ"
maxlength="10"
class="block w-full rounded border-gray-300 shadow-sm p-2 border text-sm

View File

@@ -4,15 +4,17 @@ import TagInput from '$lib/components/TagInput.svelte';
import PersonTypeahead from '$lib/components/PersonTypeahead.svelte';
import PersonMultiSelect from '$lib/components/PersonMultiSelect.svelte';
export let form;
let { form } = $props();
let tags: string[] = [];
let senderId = '';
let selectedReceivers: { id: string; firstName: string; lastName: string }[] = [];
let tags: string[] = $state([]);
let senderId = $state('');
let selectedReceivers: { id: string; firstName: string; lastName: string }[] = $state([]);
let dateDisplay = '';
let dateIso = '';
let dateDirty = false;
let dateDisplay = $state('');
let dateIso = $state('');
let dateDirty = $state(false);
const dateInvalid = $derived(dateDirty && dateDisplay.length > 0 && dateIso === '');
function germanToIso(german: string): string {
const match = german.match(/^(\d{2})\.(\d{2})\.(\d{4})$/);
@@ -37,8 +39,6 @@ function handleDateInput(e: Event) {
dateIso = germanToIso(formatted);
dateDirty = true;
}
$: dateInvalid = dateDirty && dateDisplay.length > 0 && dateIso === '';
</script>
<div class="mx-auto max-w-4xl px-4 py-8">
@@ -86,7 +86,7 @@ $: dateInvalid = dateDirty && dateDisplay.length > 0 && dateIso === '';
type="text"
inputmode="numeric"
value={dateDisplay}
on:input={handleDateInput}
oninput={handleDateInput}
placeholder="TT.MM.JJJJ"
maxlength="10"
class="block w-full rounded border border-gray-300 p-2 text-sm shadow-sm