chore(frontend): add ESLint + Prettier; fix real findings; format the tree

The frontend had no JS/TS linter — only svelte-check. Add flat-config ESLint (typescript-eslint
+ eslint-plugin-svelte) and Prettier (tabs/single-quote, matching the existing style).

Rules encode "catch bugs, not enforce taste":
  - svelte/require-each-key KEPT — it is the exact bug class as the feed mis-tap fix. Fixed every
    flagged block: keyed activeFilters, filteredUsers, stagedFiles (by previewUrl), captionTags,
    admin tabs/jobs/users, the export-viewer suggestions/filters/comments, and the static skeleton
    loops.
  - svelte/prefer-svelte-reactivity KEPT — inline-disabled only the verified-safe sites (a local
    freq Map in a $derived.by, throwaway URLSearchParams query builders), with a reason each.
  - svelte/no-navigation-without-resolve OFF — wants resolve() around every goto()/href; taste, not
    a bug, and pure churn.
  - svelte/no-unused-svelte-ignore OFF — those comments are consumed by svelte-check, which ESLint
    can't see, so it wrongly calls them unused; removing them would reintroduce a11y warnings.
  - no-explicit-any OFF for *.test.ts only (partial fixtures legitimately use any).

Real code fixes beyond keys: removed a dead jobLabel(), an unused ViewerComment import and unused
catch binding, an unused scroll-lock arg, and replaced an empty interface with a type alias.

Then `prettier --write` (54 files). Formatting only. Verified: eslint clean, svelte-check 0 errors,
vitest 46 passed, vite build succeeds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
fabi
2026-07-15 20:45:42 +02:00
parent 4d14df18d0
commit f8cba95e49
59 changed files with 4046 additions and 998 deletions

View File

@@ -67,17 +67,18 @@
}
}
function jobLabel(type: 'zip' | 'html'): string {
return type === 'zip' ? 'ZIP-Archiv (Gallery.zip)' : 'HTML-Viewer (Memories.zip)';
}
function statusText(job: JobStatus): string {
switch (job.status) {
case 'locked': return 'Noch nicht freigegeben';
case 'pending': return 'Wird vorbereitet…';
case 'running': return `Wird erstellt (${job.progress_pct} %)`;
case 'done': return 'Bereit zum Download';
case 'failed': return 'Fehlgeschlagen';
case 'locked':
return 'Noch nicht freigegeben';
case 'pending':
return 'Wird vorbereitet…';
case 'running':
return `Wird erstellt (${job.progress_pct} %)`;
case 'done':
return 'Bereit zum Download';
case 'failed':
return 'Fehlgeschlagen';
}
}
@@ -142,13 +143,26 @@
aria-labelledby="html-guide-title"
use:focusTrap={{ onclose: () => (showHtmlGuide = false) }}
>
<h2 id="html-guide-title" class="mb-3 text-lg font-bold text-gray-900 dark:text-gray-100">Hinweis zum HTML-Viewer</h2>
<h2 id="html-guide-title" class="mb-3 text-lg font-bold text-gray-900 dark:text-gray-100">
Hinweis zum HTML-Viewer
</h2>
<ol class="mb-4 space-y-2 text-sm text-gray-700 dark:text-gray-300">
<li class="flex gap-2"><span class="font-bold text-blue-600 dark:text-blue-400">1.</span> ZIP-Datei entpacken (Windows: Rechtsklick → "Alle extrahieren"; Mac: Doppelklick).</li>
<li class="flex gap-2"><span class="font-bold text-blue-600 dark:text-blue-400">2.</span> <strong>index.html</strong> im Browser öffnen.</li>
<li class="flex gap-2"><span class="font-bold text-blue-600 dark:text-blue-400">3.</span> Kein Internet nötig — alles ist lokal gespeichert.</li>
<li class="flex gap-2">
<span class="font-bold text-blue-600 dark:text-blue-400">1.</span> ZIP-Datei entpacken (Windows:
Rechtsklick → "Alle extrahieren"; Mac: Doppelklick).
</li>
<li class="flex gap-2">
<span class="font-bold text-blue-600 dark:text-blue-400">2.</span>
<strong>index.html</strong> im Browser öffnen.
</li>
<li class="flex gap-2">
<span class="font-bold text-blue-600 dark:text-blue-400">3.</span> Kein Internet nötig — alles
ist lokal gespeichert.
</li>
</ol>
<p class="mb-4 rounded-lg bg-amber-50 px-3 py-2 text-xs text-amber-700 dark:bg-amber-950/30 dark:text-amber-300">
<p
class="mb-4 rounded-lg bg-amber-50 px-3 py-2 text-xs text-amber-700 dark:bg-amber-950/30 dark:text-amber-300"
>
Tipp: Am besten im WLAN herunterladen — die Datei kann mehrere GB groß sein.
</p>
<div class="flex gap-2">
@@ -170,9 +184,16 @@
{/if}
<div class="min-h-screen bg-gray-50 pb-24 dark:bg-gray-950">
<div class="border-b border-gray-200 bg-white pt-[env(safe-area-inset-top)] dark:border-gray-800 dark:bg-gray-900">
<div
class="border-b border-gray-200 bg-white pt-[env(safe-area-inset-top)] dark:border-gray-800 dark:bg-gray-900"
>
<div class="mx-auto flex max-w-lg items-center gap-2 px-4 py-4">
<IconButton label="Zurück" onclick={() => goto('/feed')} data-testid="export-back" class="-ml-2">
<IconButton
label="Zurück"
onclick={() => goto('/feed')}
data-testid="export-back"
class="-ml-2"
>
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 19.5 8.25 12l7.5-7.5" />
</svg>
@@ -185,30 +206,56 @@
{#if loading}
<div class="py-16 text-center text-gray-400 dark:text-gray-500">Laden…</div>
{:else if !status?.released}
<div class="rounded-xl border border-gray-200 bg-white p-6 text-center dark:border-gray-700 dark:bg-gray-800">
<svg class="mx-auto mb-3 h-12 w-12 text-gray-300 dark:text-gray-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
<div
class="rounded-xl border border-gray-200 bg-white p-6 text-center dark:border-gray-700 dark:bg-gray-800"
>
<svg
class="mx-auto mb-3 h-12 w-12 text-gray-300 dark:text-gray-600"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.5"
d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"
/>
</svg>
<p class="font-medium text-gray-700 dark:text-gray-300">Export noch nicht verfügbar</p>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Schau nach der Veranstaltung noch einmal vorbei.</p>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
Schau nach der Veranstaltung noch einmal vorbei.
</p>
</div>
{:else if status}
<p class="text-sm text-gray-500 dark:text-gray-400">Wähle dein bevorzugtes Format:</p>
<!-- ZIP card -->
<div class="rounded-xl border border-gray-200 bg-white p-5 dark:border-gray-700 dark:bg-gray-800">
<div
class="rounded-xl border border-gray-200 bg-white p-5 dark:border-gray-700 dark:bg-gray-800"
>
<div class="flex items-start justify-between gap-4">
<div class="min-w-0">
<h2 class="font-semibold text-gray-900 dark:text-gray-100">ZIP-Archiv</h2>
<p class="mt-0.5 text-sm text-gray-500 dark:text-gray-400">Alle Original-Fotos und Videos in strukturierten Ordnern.</p>
<p class="mt-1 text-xs {status.zip.status === 'done' ? 'text-green-600 dark:text-green-400' : status.zip.status === 'failed' ? 'text-red-500 dark:text-red-400' : 'text-gray-400 dark:text-gray-500'}">
<p class="mt-0.5 text-sm text-gray-500 dark:text-gray-400">
Alle Original-Fotos und Videos in strukturierten Ordnern.
</p>
<p
class="mt-1 text-xs {status.zip.status === 'done'
? 'text-green-600 dark:text-green-400'
: status.zip.status === 'failed'
? 'text-red-500 dark:text-red-400'
: 'text-gray-400 dark:text-gray-500'}"
>
{statusText(status.zip)}
</p>
</div>
<button
onclick={downloadZip}
disabled={status.zip.status !== 'done'}
class="shrink-0 rounded-lg px-4 py-2 text-sm font-medium {status.zip.status === 'done' ? 'bg-blue-600 text-white hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-400' : 'bg-gray-100 text-gray-400 cursor-not-allowed dark:bg-gray-700 dark:text-gray-500'}"
class="shrink-0 rounded-lg px-4 py-2 text-sm font-medium {status.zip.status === 'done'
? 'bg-blue-600 text-white hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-400'
: 'bg-gray-100 text-gray-400 cursor-not-allowed dark:bg-gray-700 dark:text-gray-500'}"
>
Download
</button>
@@ -216,26 +263,41 @@
{#if status.zip.status === 'running'}
<div class="mt-3">
<div class="h-1.5 overflow-hidden rounded-full bg-gray-200 dark:bg-gray-700">
<div class="h-full rounded-full bg-blue-500 transition-all" style="width: {status.zip.progress_pct}%"></div>
<div
class="h-full rounded-full bg-blue-500 transition-all"
style="width: {status.zip.progress_pct}%"
></div>
</div>
</div>
{/if}
</div>
<!-- HTML card -->
<div class="rounded-xl border border-gray-200 bg-white p-5 dark:border-gray-700 dark:bg-gray-800">
<div
class="rounded-xl border border-gray-200 bg-white p-5 dark:border-gray-700 dark:bg-gray-800"
>
<div class="flex items-start justify-between gap-4">
<div class="min-w-0">
<h2 class="font-semibold text-gray-900 dark:text-gray-100">HTML-Viewer</h2>
<p class="mt-0.5 text-sm text-gray-500 dark:text-gray-400">Schöne Offline-Galerie mit Filterung, Kommentaren und Likes — kein Internet nötig.</p>
<p class="mt-1 text-xs {status.html.status === 'done' ? 'text-green-600 dark:text-green-400' : status.html.status === 'failed' ? 'text-red-500 dark:text-red-400' : 'text-gray-400 dark:text-gray-500'}">
<p class="mt-0.5 text-sm text-gray-500 dark:text-gray-400">
Schöne Offline-Galerie mit Filterung, Kommentaren und Likes — kein Internet nötig.
</p>
<p
class="mt-1 text-xs {status.html.status === 'done'
? 'text-green-600 dark:text-green-400'
: status.html.status === 'failed'
? 'text-red-500 dark:text-red-400'
: 'text-gray-400 dark:text-gray-500'}"
>
{statusText(status.html)}
</p>
</div>
<button
onclick={downloadHtml}
disabled={status.html.status !== 'done'}
class="shrink-0 rounded-lg px-4 py-2 text-sm font-medium {status.html.status === 'done' ? 'bg-blue-600 text-white hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-400' : 'bg-gray-100 text-gray-400 cursor-not-allowed dark:bg-gray-700 dark:text-gray-500'}"
class="shrink-0 rounded-lg px-4 py-2 text-sm font-medium {status.html.status === 'done'
? 'bg-blue-600 text-white hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-400'
: 'bg-gray-100 text-gray-400 cursor-not-allowed dark:bg-gray-700 dark:text-gray-500'}"
>
Download
</button>
@@ -243,7 +305,10 @@
{#if status.html.status === 'running'}
<div class="mt-3">
<div class="h-1.5 overflow-hidden rounded-full bg-gray-200 dark:bg-gray-700">
<div class="h-full rounded-full bg-blue-500 transition-all" style="width: {status.html.progress_pct}%"></div>
<div
class="h-full rounded-full bg-blue-500 transition-all"
style="width: {status.html.progress_pct}%"
></div>
</div>
</div>
{/if}