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