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

@@ -18,14 +18,7 @@
oncontextmenu?: (upload: FeedUpload) => void;
}
let {
upload,
isOwn = false,
onlike,
oncomment,
onselect,
oncontextmenu
}: Props = $props();
let { upload, isOwn = false, onlike, oncomment, onselect, oncontextmenu }: Props = $props();
function isVideo(mime: string): boolean {
return mime.startsWith('video/');
@@ -88,7 +81,9 @@
{initials(upload.uploader_name)}
</div>
<div class="min-w-0">
<p class="truncate text-sm font-semibold text-gray-900 dark:text-gray-100">{upload.uploader_name}</p>
<p class="truncate text-sm font-semibold text-gray-900 dark:text-gray-100">
{upload.uploader_name}
</p>
<p class="text-xs text-gray-400 dark:text-gray-500">{relTime}</p>
</div>
</div>
@@ -97,11 +92,20 @@
<!-- Desktop kebab — same actions as the mobile long-press context sheet. -->
<button
type="button"
onclick={(e) => { e.stopPropagation(); openContext(); }}
onclick={(e) => {
e.stopPropagation();
openContext();
}}
class="rounded-full p-1 text-gray-400 hover:bg-gray-100 active:bg-gray-200 hover:text-gray-700 dark:text-gray-500 dark:hover:bg-gray-800 dark:active:bg-gray-800 dark:hover:text-gray-200"
aria-label="Mehr Aktionen"
>
<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 24 24"><circle cx="5" cy="12" r="2"/><circle cx="12" cy="12" r="2"/><circle cx="19" cy="12" r="2"/></svg>
<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 24 24"
><circle cx="5" cy="12" r="2" /><circle cx="12" cy="12" r="2" /><circle
cx="19"
cy="12"
r="2"
/></svg
>
</button>
{/if}
</div>
@@ -112,7 +116,9 @@
use:doubletap
onsingletap={() => onselect(upload)}
ondoubletap={handleDoubleTap}
onclick={(e) => { if (e.detail === 0) onselect(upload); }}
onclick={(e) => {
if (e.detail === 0) onselect(upload);
}}
aria-label="Bild vergrößern"
>
<HeartBurst active={heartBurst} />
@@ -128,7 +134,9 @@
/>
{/if}
<div class="absolute inset-0 flex items-center justify-center">
<span class="flex h-14 w-14 items-center justify-center rounded-full bg-black/50 text-white">
<span
class="flex h-14 w-14 items-center justify-center rounded-full bg-black/50 text-white"
>
<svg class="h-7 w-7 pl-0.5" fill="currentColor" viewBox="0 0 24 24">
<path d="M8 5v14l11-7z" />
</svg>
@@ -149,9 +157,21 @@
/>
</div>
{:else}
<div class="flex aspect-square w-full items-center justify-center bg-gray-100 dark:bg-gray-800">
<svg class="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="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
<div
class="flex aspect-square w-full items-center justify-center bg-gray-100 dark:bg-gray-800"
>
<svg
class="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="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
</div>
{/if}
@@ -160,11 +180,16 @@
<!-- Actions row -->
<div class="flex items-center gap-4 px-4 py-2">
<button
onclick={() => { vibrate(10); onlike(upload.id); }}
onclick={() => {
vibrate(10);
onlike(upload.id);
}}
aria-pressed={upload.liked_by_me}
aria-label={upload.liked_by_me ? 'Gefällt mir nicht mehr' : 'Gefällt mir'}
class="flex items-center gap-1.5 text-sm font-medium transition-colors
{upload.liked_by_me ? 'text-red-500 dark:text-red-400' : 'text-gray-500 hover:text-red-400 active:text-red-400 dark:text-gray-400 dark:hover:text-red-400 dark:active:text-red-400'}"
{upload.liked_by_me
? 'text-red-500 dark:text-red-400'
: 'text-gray-500 hover:text-red-400 active:text-red-400 dark:text-gray-400 dark:hover:text-red-400 dark:active:text-red-400'}"
>
<svg
class="h-5 w-5 {upload.liked_by_me ? 'fill-red-500' : ''}"
@@ -173,7 +198,11 @@
stroke="currentColor"
stroke-width="2"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z" />
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"
/>
</svg>
{upload.like_count}
</button>
@@ -182,7 +211,11 @@
class="flex items-center gap-1.5 text-sm font-medium text-gray-500 transition-colors hover:text-blue-500 active:text-blue-500 dark:text-gray-400 dark:hover:text-blue-400 dark:active:text-blue-400"
>
<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="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"
/>
</svg>
{upload.comment_count}
</button>