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:
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user