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,58 +1,39 @@
<script lang="ts">
import { goto } from '$app/navigation';
import PersonTypeahead from '$lib/components/PersonTypeahead.svelte';
import { untrack } from 'svelte';
export let data;
let { data } = $props();
// Data & State
let documents: typeof data.documents = [];
let initialValues = { senderName: '', receiverName: '' };
let senderId = $state(untrack(() => data.filters.senderId));
let receiverId = $state(untrack(() => data.filters.receiverId));
let fromDate = $state(untrack(() => data.filters.from));
let toDate = $state(untrack(() => data.filters.to));
let sortDir = $state(untrack(() => data.filters.dir));
// Filter State
let senderId = '';
let receiverId = '';
let fromDate = '';
let toDate = '';
let sortDir = 'DESC';
// Reactive Update
$: {
documents = data.documents;
initialValues = data.initialValues;
// Sync with server data after navigation
$effect(() => {
senderId = data.filters.senderId;
receiverId = data.filters.receiverId;
fromDate = data.filters.from;
toDate = data.filters.to;
sortDir = data.filters.dir;
}
});
// Filter Logic
function applyFilters() {
setTimeout(() => {
const params = new URLSearchParams();
if (senderId) params.set('senderId', senderId);
if (receiverId) params.set('receiverId', receiverId);
if (fromDate) params.set('from', fromDate);
if (toDate) params.set('to', toDate);
params.set('dir', sortDir);
goto(`?${params.toString()}`, { keepFocus: true });
}, 0);
const params = new URLSearchParams();
if (senderId) params.set('senderId', senderId);
if (receiverId) params.set('receiverId', receiverId);
if (fromDate) params.set('from', fromDate);
if (toDate) params.set('to', toDate);
params.set('dir', sortDir);
goto(`?${params.toString()}`, { keepFocus: true });
}
function toggleSort() {
sortDir = sortDir === 'DESC' ? 'ASC' : 'DESC';
applyFilters();
}
function handleSenderChange(event: CustomEvent) {
senderId = event.detail.value;
applyFilters();
}
function handleReceiverChange(event: CustomEvent) {
receiverId = event.detail.value;
applyFilters();
}
</script>
<div class="max-w-5xl mx-auto py-10 px-4">
@@ -74,9 +55,9 @@
<PersonTypeahead
name="senderId"
label="Person A (Absender)"
value={senderId}
initialName={initialValues.senderName}
on:change={handleSenderChange}
bind:value={senderId}
initialName={data.initialValues.senderName}
onchange={() => applyFilters()}
/>
</div>
@@ -87,9 +68,9 @@
<PersonTypeahead
name="receiverId"
label="Person B (Empfänger)"
value={receiverId}
initialName={initialValues.receiverName}
on:change={handleReceiverChange}
bind:value={receiverId}
initialName={data.initialValues.receiverName}
onchange={() => applyFilters()}
/>
</div>
</div>
@@ -106,7 +87,7 @@
id="dateFrom"
type="date"
bind:value={fromDate}
on:change={() => applyFilters()}
onchange={() => applyFilters()}
class="block w-full border-gray-300 shadow-sm focus:border-brand-navy focus:ring-brand-navy text-sm py-2.5"
/>
</div>
@@ -122,7 +103,7 @@
id="dateTo"
type="date"
bind:value={toDate}
on:change={() => applyFilters()}
onchange={() => applyFilters()}
class="block w-full border-gray-300 shadow-sm focus:border-brand-navy focus:ring-brand-navy text-sm py-2.5"
/>
</div>
@@ -130,7 +111,7 @@
<!-- Sort Toggle -->
<div>
<button
on:click={toggleSort}
onclick={toggleSort}
class="w-full flex items-center justify-center h-[42px] border border-brand-sand text-xs font-bold uppercase tracking-wide text-brand-navy hover:bg-brand-navy hover:text-white transition-colors"
>
<span class="mr-2">Sortierung:</span>
@@ -169,7 +150,7 @@
<p class="text-brand-navy font-serif text-lg">Wählen Sie zwei Personen aus</p>
<p class="text-gray-500 font-sans text-sm mt-1">Die Korrespondenz wird hier angezeigt.</p>
</div>
{:else if documents.length === 0}
{:else if data.documents.length === 0}
<div
class="flex flex-col items-center justify-center py-24 bg-white border border-brand-sand rounded-sm text-center shadow-sm"
>
@@ -178,18 +159,15 @@
</div>
{:else}
<!-- CHAT CONTAINER -->
<!-- Added: White background, Border, Shadow to separate from page -->
<div class="bg-white border border-brand-sand shadow-sm rounded-sm relative overflow-hidden">
<!-- Decoration: Central Timeline Line -->
<div
class="absolute left-1/2 top-0 bottom-0 w-px bg-brand-sand/30 transform -translate-x-1/2 hidden md:block"
></div>
<!-- Scrollable Area (optional, if you want max-height) -->
<div class="p-6 md:p-8">
<!-- TIGHTER GAP: Changed from gap-8 to gap-4 -->
<div class="flex flex-col gap-4 relative z-10">
{#each documents as doc}
{#each data.documents as doc}
{@const isRight = doc.sender?.id === senderId}
<!-- Message Row -->
@@ -200,7 +178,7 @@
? 'flex-row-reverse'
: 'flex-row'}"
>
<!-- AVATAR (Small) -->
<!-- AVATAR -->
<div class="flex-shrink-0 mt-auto mb-1 hidden sm:block">
<div
class="w-8 h-8 rounded-full flex items-center justify-center font-serif text-xs border shadow-sm
@@ -217,7 +195,6 @@
</div>
<!-- BUBBLE CARD -->
<!-- Adjusted padding (p-4) and added light bg to left bubbles for contrast -->
<a
href="/documents/{doc.id}"
class="group block p-4 rounded shadow-sm transition-all duration-200 transform hover:-translate-y-0.5 hover:shadow-md border