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

@@ -30,11 +30,17 @@
<a
href="/feed"
class="flex flex-col items-center gap-0.5 px-4 py-1 text-xs font-medium transition-colors
{isActive('/feed') ? 'text-blue-600 dark:text-blue-400' : 'text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300'}"
{isActive('/feed')
? 'text-blue-600 dark:text-blue-400'
: 'text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300'}"
aria-label="Galerie"
>
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d="m2.25 15.75 5.159-5.159a2.25 2.25 0 0 1 3.182 0l5.159 5.159m-1.5-1.5 1.409-1.409a2.25 2.25 0 0 1 3.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 0 0 1.5-1.5V6a1.5 1.5 0 0 0-1.5-1.5H3.75A1.5 1.5 0 0 0 2.25 6v12a1.5 1.5 0 0 0 1.5 1.5Zm10.5-11.25h.008v.008h-.008V8.25Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z" />
<path
stroke-linecap="round"
stroke-linejoin="round"
d="m2.25 15.75 5.159-5.159a2.25 2.25 0 0 1 3.182 0l5.159 5.159m-1.5-1.5 1.409-1.409a2.25 2.25 0 0 1 3.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 0 0 1.5-1.5V6a1.5 1.5 0 0 0-1.5-1.5H3.75A1.5 1.5 0 0 0 2.25 6v12a1.5 1.5 0 0 0 1.5 1.5Zm10.5-11.25h.008v.008h-.008V8.25Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z"
/>
</svg>
<span>Galerie</span>
</a>
@@ -47,9 +53,23 @@
aria-label="Hochladen"
>
<!-- Camera + plus icon -->
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M6.827 6.175A2.31 2.31 0 0 1 5.186 7.23c-.38.054-.757.112-1.134.175C2.999 7.58 2.25 8.507 2.25 9.574V18a2.25 2.25 0 0 0 2.25 2.25h15A2.25 2.25 0 0 0 21.75 18V9.574c0-1.067-.75-1.994-1.802-2.169a47.865 47.865 0 0 0-1.134-.175 2.31 2.31 0 0 1-1.64-1.055l-.822-1.316a2.192 2.192 0 0 0-1.736-1.039 48.774 48.774 0 0 0-5.232 0 2.192 2.192 0 0 0-1.736 1.039l-.821 1.316Z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M16.5 12.75a4.5 4.5 0 1 1-9 0 4.5 4.5 0 0 1 9 0ZM18.75 10.5h.008v.008h-.008V10.5Z" />
<svg
class="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="1.5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M6.827 6.175A2.31 2.31 0 0 1 5.186 7.23c-.38.054-.757.112-1.134.175C2.999 7.58 2.25 8.507 2.25 9.574V18a2.25 2.25 0 0 0 2.25 2.25h15A2.25 2.25 0 0 0 21.75 18V9.574c0-1.067-.75-1.994-1.802-2.169a47.865 47.865 0 0 0-1.134-.175 2.31 2.31 0 0 1-1.64-1.055l-.822-1.316a2.192 2.192 0 0 0-1.736-1.039 48.774 48.774 0 0 0-5.232 0 2.192 2.192 0 0 0-1.736 1.039l-.821 1.316Z"
/>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M16.5 12.75a4.5 4.5 0 1 1-9 0 4.5 4.5 0 0 1 9 0ZM18.75 10.5h.008v.008h-.008V10.5Z"
/>
</svg>
<!-- Badge -->
{#if $uploadBadgeCount > 0}
@@ -68,15 +88,28 @@
href="/export"
data-testid="bottom-nav-export"
class="relative flex flex-col items-center gap-0.5 px-4 py-1 text-xs font-medium transition-colors
{isActive('/export') ? 'text-blue-600 dark:text-blue-400' : 'text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300'}"
{isActive('/export')
? 'text-blue-600 dark:text-blue-400'
: 'text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300'}"
aria-label="Export"
>
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3" />
<svg
class="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="1.5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3"
/>
</svg>
<span>Export</span>
{#if zipReady}
<span class="absolute right-2 top-0 h-2 w-2 rounded-full bg-blue-500" aria-hidden="true"></span>
<span class="absolute right-2 top-0 h-2 w-2 rounded-full bg-blue-500" aria-hidden="true"
></span>
{/if}
</a>
{/if}
@@ -85,11 +118,17 @@
<a
href="/account"
class="flex flex-col items-center gap-0.5 px-4 py-1 text-xs font-medium transition-colors
{isActive('/account') ? 'text-blue-600 dark:text-blue-400' : 'text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300'}"
{isActive('/account')
? 'text-blue-600 dark:text-blue-400'
: 'text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300'}"
aria-label="Konto"
>
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 6a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0ZM4.501 20.118a7.5 7.5 0 0 1 14.998 0A17.933 17.933 0 0 1 12 21.75c-2.676 0-5.216-.584-7.499-1.632Z" />
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15.75 6a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0ZM4.501 20.118a7.5 7.5 0 0 1 14.998 0A17.933 17.933 0 0 1 12 21.75c-2.676 0-5.216-.584-7.499-1.632Z"
/>
</svg>
<span>Konto</span>
</a>

View File

@@ -57,7 +57,8 @@
}
} catch (err) {
if (err instanceof DOMException && err.name === 'NotAllowedError') {
error = 'Kamerazugriff wurde verweigert. Bitte erlaube den Zugriff in den Browsereinstellungen.';
error =
'Kamerazugriff wurde verweigert. Bitte erlaube den Zugriff in den Browsereinstellungen.';
} else if (err instanceof DOMException && err.name === 'NotFoundError') {
error = 'Keine Kamera gefunden.';
} else {
@@ -162,8 +163,18 @@
{#if error}
<div class="flex h-full items-center justify-center p-8">
<div class="rounded-lg bg-gray-900 p-6 text-center">
<svg class="mx-auto mb-3 h-12 w-12 text-red-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M15 10l-4 4m0-4l4 4m6-4a9 9 0 11-18 0 9 9 0 0118 0z" />
<svg
class="mx-auto mb-3 h-12 w-12 text-red-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.5"
d="M15 10l-4 4m0-4l4 4m6-4a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<p class="text-sm text-white">{error}</p>
<div class="mt-4 flex justify-center gap-2">
@@ -173,10 +184,7 @@
>
Erneut versuchen
</button>
<button
onclick={onclose}
class="rounded-lg bg-white/20 px-4 py-2 text-sm text-white"
>
<button onclick={onclose} class="rounded-lg bg-white/20 px-4 py-2 text-sm text-white">
Schließen
</button>
</div>
@@ -193,7 +201,9 @@
></video>
{#if recording}
<div class="absolute left-4 top-4 flex items-center gap-2 rounded-full bg-red-600 px-3 py-1">
<div
class="absolute left-4 top-4 flex items-center gap-2 rounded-full bg-red-600 px-3 py-1"
>
<div class="h-2 w-2 animate-pulse rounded-full bg-white"></div>
<span class="text-sm font-medium text-white">{formatRecordingTime(recordingTime)}</span>
</div>
@@ -218,7 +228,9 @@
aria-selected={mode === 'photo'}
onclick={() => setMode('photo')}
data-testid="camera-mode-photo"
class="rounded-full px-4 py-1 text-sm font-medium transition {mode === 'photo' ? 'bg-white text-gray-900' : 'text-white/70 hover:text-white active:text-white'}"
class="rounded-full px-4 py-1 text-sm font-medium transition {mode === 'photo'
? 'bg-white text-gray-900'
: 'text-white/70 hover:text-white active:text-white'}"
>
Foto
</button>
@@ -228,7 +240,9 @@
aria-selected={mode === 'video'}
onclick={() => setMode('video')}
data-testid="camera-mode-video"
class="rounded-full px-4 py-1 text-sm font-medium transition {mode === 'video' ? 'bg-white text-gray-900' : 'text-white/70 hover:text-white active:text-white'}"
class="rounded-full px-4 py-1 text-sm font-medium transition {mode === 'video'
? 'bg-white text-gray-900'
: 'text-white/70 hover:text-white active:text-white'}"
>
Video
</button>
@@ -236,7 +250,10 @@
</div>
{/if}
<div class="flex items-center justify-center gap-8 bg-black/80 px-4 pt-4 pb-6" style="padding-bottom: calc(env(safe-area-inset-bottom) + 1.5rem)">
<div
class="flex items-center justify-center gap-8 bg-black/80 px-4 pt-4 pb-6"
style="padding-bottom: calc(env(safe-area-inset-bottom) + 1.5rem)"
>
<!-- Close -->
<button
onclick={onclose}
@@ -244,7 +261,12 @@
aria-label="Schließen"
>
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
@@ -252,8 +274,14 @@
<button
onclick={handleShutter}
data-testid="camera-shutter"
class="flex h-16 w-16 items-center justify-center rounded-full border-4 border-white transition active:scale-95 {recording ? 'bg-red-600' : 'bg-white/20'}"
aria-label={mode === 'photo' ? 'Foto aufnehmen' : recording ? 'Aufnahme stoppen' : 'Video aufnehmen'}
class="flex h-16 w-16 items-center justify-center rounded-full border-4 border-white transition active:scale-95 {recording
? 'bg-red-600'
: 'bg-white/20'}"
aria-label={mode === 'photo'
? 'Foto aufnehmen'
: recording
? 'Aufnahme stoppen'
: 'Video aufnehmen'}
>
{#if mode === 'photo'}
<div class="h-12 w-12 rounded-full bg-white"></div>
@@ -274,7 +302,12 @@
aria-label="Kamera wechseln"
>
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
/>
</svg>
</button>
{/if}

View File

@@ -80,12 +80,16 @@
onclick={handleConfirm}
disabled={busy}
data-testid="confirm-sheet-confirm"
class="mb-3 flex w-full items-center justify-center gap-2 rounded-xl py-3 text-sm font-semibold text-white transition disabled:opacity-60 {tone === 'danger'
class="mb-3 flex w-full items-center justify-center gap-2 rounded-xl py-3 text-sm font-semibold text-white transition disabled:opacity-60 {tone ===
'danger'
? 'bg-red-600 hover:bg-red-700 active:bg-red-700 dark:bg-red-500 dark:hover:bg-red-400 dark:active:bg-red-400'
: 'bg-blue-600 hover:bg-blue-700 active:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-400 dark:active:bg-blue-400'}"
>
{#if busy}
<span class="inline-block h-4 w-4 animate-spin rounded-full border-2 border-white/40 border-t-white" aria-hidden="true"></span>
<span
class="inline-block h-4 w-4 animate-spin rounded-full border-2 border-white/40 border-t-white"
aria-hidden="true"
></span>
{/if}
{confirmLabel}
</button>

View File

@@ -63,7 +63,11 @@
window.addEventListener('keydown', onKeyDown);
return () => window.removeEventListener('keydown', onKeyDown);
} else if (returnFocus) {
try { returnFocus.focus({ preventScroll: true }); } catch { /* element gone */ }
try {
returnFocus.focus({ preventScroll: true });
} catch {
/* element gone */
}
returnFocus = null;
}
});
@@ -115,7 +119,9 @@
</div>
{#if title}
<p class="px-5 pt-1 pb-2 text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
<p
class="px-5 pt-1 pb-2 text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400"
>
{title}
</p>
{/if}

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>

View File

@@ -18,11 +18,10 @@
<button
onclick={() => onselect(null)}
aria-pressed={selected === null}
class="inline-flex min-h-11 shrink-0 items-center rounded-full px-4 py-2 text-sm font-medium transition {
selected === null
? 'bg-blue-600 text-white dark:bg-blue-500'
: 'bg-gray-200 text-gray-700 hover:bg-gray-300 active:bg-gray-300 dark:bg-gray-800 dark:text-gray-200 dark:hover:bg-gray-700 dark:active:bg-gray-700'
}"
class="inline-flex min-h-11 shrink-0 items-center rounded-full px-4 py-2 text-sm font-medium transition {selected ===
null
? 'bg-blue-600 text-white dark:bg-blue-500'
: 'bg-gray-200 text-gray-700 hover:bg-gray-300 active:bg-gray-300 dark:bg-gray-800 dark:text-gray-200 dark:hover:bg-gray-700 dark:active:bg-gray-700'}"
>
Alle
</button>
@@ -30,11 +29,10 @@
<button
onclick={() => onselect(h.tag)}
aria-pressed={selected === h.tag}
class="inline-flex min-h-11 shrink-0 items-center rounded-full px-4 py-2 text-sm font-medium transition {
selected === h.tag
? 'bg-blue-600 text-white dark:bg-blue-500'
: 'bg-gray-200 text-gray-700 hover:bg-gray-300 active:bg-gray-300 dark:bg-gray-800 dark:text-gray-200 dark:hover:bg-gray-700 dark:active:bg-gray-700'
}"
class="inline-flex min-h-11 shrink-0 items-center rounded-full px-4 py-2 text-sm font-medium transition {selected ===
h.tag
? 'bg-blue-600 text-white dark:bg-blue-500'
: 'bg-gray-200 text-gray-700 hover:bg-gray-300 active:bg-gray-300 dark:bg-gray-800 dark:text-gray-200 dark:hover:bg-gray-700 dark:active:bg-gray-700'}"
>
#{h.tag}
<span class="ml-1 text-xs opacity-70">{h.count}</span>

View File

@@ -5,15 +5,6 @@
let { active }: Props = $props();
</script>
<style>
@keyframes heart-burst {
0% { transform: scale(0.5); opacity: 0; }
30% { transform: scale(1.2); opacity: 1; }
70% { transform: scale(1); opacity: 1; }
100% { transform: scale(1.4); opacity: 0; }
}
</style>
{#if active}
<span
class="pointer-events-none absolute inset-0 flex items-center justify-center"
@@ -26,7 +17,30 @@
fill="currentColor"
viewBox="0 0 24 24"
>
<path d="M12 21s-7-4.534-9.5-9.034C.5 9.466 2.5 5 7 5c2.09 0 3.534 1.083 5 3 1.466-1.917 2.91-3 5-3 4.5 0 6.5 4.466 4.5 6.966C19 16.466 12 21 12 21z" />
<path
d="M12 21s-7-4.534-9.5-9.034C.5 9.466 2.5 5 7 5c2.09 0 3.534 1.083 5 3 1.466-1.917 2.91-3 5-3 4.5 0 6.5 4.466 4.5 6.966C19 16.466 12 21 12 21z"
/>
</svg>
</span>
{/if}
<style>
@keyframes heart-burst {
0% {
transform: scale(0.5);
opacity: 0;
}
30% {
transform: scale(1.2);
opacity: 1;
}
70% {
transform: scale(1);
opacity: 1;
}
100% {
transform: scale(1.4);
opacity: 0;
}
}
</style>

View File

@@ -55,7 +55,9 @@
try {
const { comment_id } = JSON.parse(data) as { comment_id: string };
comments = comments.filter((c) => c.id !== comment_id);
} catch { /* ignore */ }
} catch {
/* ignore */
}
});
// Pull in a comment posted from another device on the CURRENTLY-open upload, so the
@@ -66,7 +68,9 @@
try {
const { upload_id } = JSON.parse(data) as { upload_id: string };
if (upload_id === upload.id) void loadComments(upload.id);
} catch { /* ignore */ }
} catch {
/* ignore */
}
});
onDestroy(() => {
@@ -124,7 +128,10 @@
function formatTime(iso: string): string {
return new Date(iso).toLocaleString('de-DE', {
day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit'
day: '2-digit',
month: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
}
</script>
@@ -138,7 +145,9 @@
use:scrollLock
use:modalInert
>
<div class="flex max-h-[90vh] w-full max-w-2xl flex-col overflow-hidden rounded-xl bg-white dark:bg-gray-900">
<div
class="flex max-h-[90vh] w-full max-w-2xl flex-col overflow-hidden rounded-xl bg-white dark:bg-gray-900"
>
<!-- Media -->
<div class="relative bg-black">
<button
@@ -147,14 +156,15 @@
class="absolute right-2 top-2 z-10 inline-flex min-h-11 min-w-11 items-center justify-center rounded-full bg-black/50 text-white hover:bg-black/70 active:bg-black/70"
>
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
<div
class="relative"
use:doubletap
ondoubletap={triggerHeartBurst}
>
<div class="relative" use:doubletap ondoubletap={triggerHeartBurst}>
{#if isVideo(upload.mime_type)}
<video
src={mediaSrc}
@@ -180,19 +190,31 @@
<div class="border-b border-gray-100 p-3 dark:border-gray-800">
<div class="flex items-center justify-between">
<div>
<span id="lightbox-title" class="font-medium text-gray-900 dark:text-gray-100">{upload.uploader_name}</span>
<span class="ml-2 text-xs text-gray-400 dark:text-gray-500">{formatTime(upload.created_at)}</span>
<span id="lightbox-title" class="font-medium text-gray-900 dark:text-gray-100"
>{upload.uploader_name}</span
>
<span class="ml-2 text-xs text-gray-400 dark:text-gray-500"
>{formatTime(upload.created_at)}</span
>
</div>
<button
onclick={() => onlike(upload.id)}
class="flex items-center gap-1 rounded-full px-2.5 py-1 text-sm transition {
upload.liked_by_me
? 'bg-red-50 text-red-600 dark:bg-red-950/40 dark:text-red-300'
: 'bg-gray-100 text-gray-600 hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700'
}"
class="flex items-center gap-1 rounded-full px-2.5 py-1 text-sm transition {upload.liked_by_me
? 'bg-red-50 text-red-600 dark:bg-red-950/40 dark:text-red-300'
: 'bg-gray-100 text-gray-600 hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700'}"
>
<svg class="h-4 w-4 {upload.liked_by_me ? 'fill-current' : ''}" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" 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
class="h-4 w-4 {upload.liked_by_me ? 'fill-current' : ''}"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
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>
@@ -211,9 +233,13 @@
{#each comments as comment (comment.id)}
<div class="flex items-start gap-2">
<div class="flex-1">
<span class="text-sm font-medium text-gray-900 dark:text-gray-100">{comment.uploader_name}</span>
<span class="text-sm font-medium text-gray-900 dark:text-gray-100"
>{comment.uploader_name}</span
>
<span class="ml-1 text-sm text-gray-700 dark:text-gray-300">{comment.body}</span>
<div class="mt-0.5 text-xs text-gray-400 dark:text-gray-500">{formatTime(comment.created_at)}</div>
<div class="mt-0.5 text-xs text-gray-400 dark:text-gray-500">
{formatTime(comment.created_at)}
</div>
</div>
{#if comment.user_id === userId}
<button
@@ -222,7 +248,12 @@
aria-label="Löschen"
>
<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
{/if}
@@ -234,7 +265,10 @@
<!-- Comment input -->
<form
onsubmit={(e) => { e.preventDefault(); submitComment(); }}
onsubmit={(e) => {
e.preventDefault();
submitComment();
}}
class="border-t border-gray-100 p-3 dark:border-gray-800"
>
<div class="flex gap-2">

View File

@@ -19,14 +19,7 @@
children: Snippet;
}
let {
open,
titleId,
ariaLabel,
onClose,
closeOnBackdrop = true,
children
}: Props = $props();
let { open, titleId, ariaLabel, onClose, closeOnBackdrop = true, children }: Props = $props();
$effect(() => {
if (open && !titleId && !ariaLabel && typeof console !== 'undefined') {
@@ -56,7 +49,9 @@
use:focusTrap={{ onclose: onClose }}
use:scrollLock
>
<div class="pointer-events-auto w-full max-w-sm rounded-2xl bg-white p-6 shadow-xl dark:bg-gray-900">
<div
class="pointer-events-auto w-full max-w-sm rounded-2xl bg-white p-6 shadow-xl dark:bg-gray-900"
>
{@render children()}
</div>
</div>

View File

@@ -63,10 +63,15 @@
// Derived in the script so TypeScript can narrow on `.kind` inside the template.
let currentStep = $derived(steps[step]);
const THEME_OPTIONS: Array<{ value: ThemePreference; label: string; hint: string; icon: string }> = [
const THEME_OPTIONS: Array<{
value: ThemePreference;
label: string;
hint: string;
icon: string;
}> = [
{ value: 'system', label: 'System', hint: 'Folgt der Geräteeinstellung', icon: '🖥️' },
{ value: 'light', label: 'Hell', hint: 'Heller Hintergrund', icon: '☀️' },
{ value: 'dark', label: 'Dunkel', hint: 'Dunkler Hintergrund', icon: '🌙' }
{ value: 'light', label: 'Hell', hint: 'Heller Hintergrund', icon: '☀️' },
{ value: 'dark', label: 'Dunkel', hint: 'Dunkler Hintergrund', icon: '🌙' }
];
if (browser && !localStorage.getItem(GUIDE_SEEN_KEY)) {
@@ -106,7 +111,7 @@
<!-- Step indicator — tap a pip to jump back. The visible dot is small but
the touch target is padded to ~44 px so it remains tappable on mobile. -->
<div class="mb-3 flex justify-center">
{#each steps as _, i}
{#each steps as _, i (i)}
<button
type="button"
onclick={() => goToStep(i)}
@@ -148,8 +153,8 @@
onclick={() => themePreference.set(opt.value)}
class="flex w-full cursor-pointer items-center gap-3 rounded-xl border-2 p-3 transition focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-200
{selected
? 'border-blue-600 bg-blue-50 dark:border-blue-500 dark:bg-blue-950/40'
: 'border-gray-200 hover:bg-gray-50 dark:border-gray-700 dark:hover:bg-gray-800'}"
? 'border-blue-600 bg-blue-50 dark:border-blue-500 dark:bg-blue-950/40'
: 'border-gray-200 hover:bg-gray-50 dark:border-gray-700 dark:hover:bg-gray-800'}"
>
<span class="text-2xl leading-none">{opt.icon}</span>
<div class="flex-1">
@@ -159,11 +164,17 @@
<span
class="flex h-5 w-5 shrink-0 items-center justify-center rounded-full border-2
{selected
? 'border-blue-600 bg-blue-600 dark:border-blue-500 dark:bg-blue-500'
: 'border-gray-300 dark:border-gray-600'}"
? 'border-blue-600 bg-blue-600 dark:border-blue-500 dark:bg-blue-500'
: 'border-gray-300 dark:border-gray-600'}"
>
{#if selected}
<svg class="h-3 w-3 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="3">
<svg
class="h-3 w-3 text-white"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="3"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
</svg>
{/if}
@@ -186,7 +197,7 @@
onclick={next}
class="flex-1 rounded-xl bg-blue-600 py-3 text-sm font-semibold text-white hover:bg-blue-700 active:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-400 dark:active:bg-blue-400"
>
{step < steps.length - 1 ? 'Weiter' : 'Los geht\'s!'}
{step < steps.length - 1 ? 'Weiter' : "Los geht's!"}
</button>
</div>
</div>

View File

@@ -27,7 +27,9 @@
<button
type="button"
onclick={() => dismissToast(t.id)}
class="pointer-events-auto w-full max-w-sm rounded-xl px-4 py-3 text-left text-sm font-medium shadow-lg transition active:scale-[0.98] {toneClasses(t.tone)}"
class="pointer-events-auto w-full max-w-sm rounded-xl px-4 py-3 text-left text-sm font-medium shadow-lg transition active:scale-[0.98] {toneClasses(
t.tone
)}"
role={t.tone === 'error' || t.tone === 'warning' ? 'alert' : 'status'}
aria-live={t.tone === 'error' || t.tone === 'warning' ? 'assertive' : 'polite'}
data-testid="toast"

View File

@@ -1,5 +1,12 @@
<script lang="ts">
import { queueItems, isProcessing, retryItem, removeItem, clearCompleted, rateLimitRetryAt } from '$lib/upload-queue';
import {
queueItems,
isProcessing,
retryItem,
removeItem,
clearCompleted,
rateLimitRetryAt
} from '$lib/upload-queue';
import type { QueueItem } from '$lib/upload-queue';
function formatSize(bytes: number): string {
@@ -10,21 +17,31 @@
function statusLabel(status: QueueItem['status']): string {
switch (status) {
case 'pending': return 'Wartend';
case 'uploading': return 'Wird hochgeladen';
case 'done': return 'Fertig';
case 'error': return 'Fehler';
case 'blocked': return 'Gesperrt';
case 'pending':
return 'Wartend';
case 'uploading':
return 'Wird hochgeladen';
case 'done':
return 'Fertig';
case 'error':
return 'Fehler';
case 'blocked':
return 'Gesperrt';
}
}
function statusColor(status: QueueItem['status']): string {
switch (status) {
case 'pending': return 'text-gray-500 dark:text-gray-400';
case 'uploading': return 'text-blue-600 dark:text-blue-400';
case 'done': return 'text-green-600 dark:text-green-400';
case 'error': return 'text-red-600 dark:text-red-400';
case 'blocked': return 'text-red-600 dark:text-red-400';
case 'pending':
return 'text-gray-500 dark:text-gray-400';
case 'uploading':
return 'text-blue-600 dark:text-blue-400';
case 'done':
return 'text-green-600 dark:text-green-400';
case 'error':
return 'text-red-600 dark:text-red-400';
case 'blocked':
return 'text-red-600 dark:text-red-400';
}
}
@@ -52,8 +69,12 @@
</script>
{#if items.length > 0}
<div class="mt-4 rounded-lg border border-gray-200 bg-white dark:border-gray-800 dark:bg-gray-900">
<div class="flex items-center justify-between border-b border-gray-100 px-4 py-3 dark:border-gray-800">
<div
class="mt-4 rounded-lg border border-gray-200 bg-white dark:border-gray-800 dark:bg-gray-900"
>
<div
class="flex items-center justify-between border-b border-gray-100 px-4 py-3 dark:border-gray-800"
>
<h3 class="text-sm font-semibold text-gray-900 dark:text-gray-100">
Upload-Warteschlange
{#if $isProcessing}
@@ -71,7 +92,9 @@
</div>
{#if $rateLimitRetryAt && countdown > 0}
<div class="border-b border-amber-100 bg-amber-50 px-4 py-2 text-sm text-amber-800 dark:border-amber-900/40 dark:bg-amber-950/30 dark:text-amber-300">
<div
class="border-b border-amber-100 bg-amber-50 px-4 py-2 text-sm text-amber-800 dark:border-amber-900/40 dark:bg-amber-950/30 dark:text-amber-300"
>
Upload-Limit erreicht. Wird in {countdown} Sek. automatisch fortgesetzt.
</div>
{/if}
@@ -81,7 +104,9 @@
<li class="px-4 py-3">
<div class="flex items-center justify-between">
<div class="min-w-0 flex-1">
<p class="truncate text-sm font-medium text-gray-900 dark:text-gray-100">{item.fileName}</p>
<p class="truncate text-sm font-medium text-gray-900 dark:text-gray-100">
{item.fileName}
</p>
<p class="text-xs text-gray-500 dark:text-gray-400">{formatSize(item.fileSize)}</p>
</div>
<div class="ml-3 flex items-center gap-2">
@@ -103,7 +128,12 @@
aria-label="Entfernen"
>
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
{/if}
@@ -111,7 +141,9 @@
</div>
{#if item.status === 'uploading'}
<div class="mt-2 h-1.5 w-full overflow-hidden rounded-full bg-gray-200 dark:bg-gray-700">
<div
class="mt-2 h-1.5 w-full overflow-hidden rounded-full bg-gray-200 dark:bg-gray-700"
>
<div
class="h-full rounded-full bg-blue-500 transition-all duration-300"
style="width: {item.progress}%"

View File

@@ -59,7 +59,11 @@
window.addEventListener('keydown', onKeyDown);
return () => window.removeEventListener('keydown', onKeyDown);
} else if (returnFocus) {
try { returnFocus.focus({ preventScroll: true }); } catch { /* element gone */ }
try {
returnFocus.focus({ preventScroll: true });
} catch {
/* element gone */
}
returnFocus = null;
}
});
@@ -170,46 +174,74 @@
Schließen
</button>
{:else}
<!-- Gallery option -->
<button
onclick={openGallery}
class="flex w-full items-center gap-4 rounded-xl bg-gray-50 px-5 py-4 text-left transition hover:bg-gray-100 active:bg-gray-200 dark:bg-gray-800 dark:hover:bg-gray-700 dark:active:bg-gray-600"
>
<span class="flex h-11 w-11 items-center justify-center rounded-full bg-blue-100 text-blue-600 dark:bg-blue-900/40 dark:text-blue-300">
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d="m2.25 15.75 5.159-5.159a2.25 2.25 0 0 1 3.182 0l5.159 5.159m-1.5-1.5 1.409-1.409a2.25 2.25 0 0 1 3.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 0 0 1.5-1.5V6a1.5 1.5 0 0 0-1.5-1.5H3.75A1.5 1.5 0 0 0 2.25 6v12a1.5 1.5 0 0 0 1.5 1.5Zm10.5-11.25h.008v.008h-.008V8.25Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z" />
</svg>
</span>
<div>
<p class="font-semibold text-gray-900 dark:text-gray-100">Galerie</p>
<p class="text-sm text-gray-500 dark:text-gray-400">Foto oder Video wählen</p>
</div>
</button>
<!-- Gallery option -->
<button
onclick={openGallery}
class="flex w-full items-center gap-4 rounded-xl bg-gray-50 px-5 py-4 text-left transition hover:bg-gray-100 active:bg-gray-200 dark:bg-gray-800 dark:hover:bg-gray-700 dark:active:bg-gray-600"
>
<span
class="flex h-11 w-11 items-center justify-center rounded-full bg-blue-100 text-blue-600 dark:bg-blue-900/40 dark:text-blue-300"
>
<svg
class="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="1.5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="m2.25 15.75 5.159-5.159a2.25 2.25 0 0 1 3.182 0l5.159 5.159m-1.5-1.5 1.409-1.409a2.25 2.25 0 0 1 3.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 0 0 1.5-1.5V6a1.5 1.5 0 0 0-1.5-1.5H3.75A1.5 1.5 0 0 0 2.25 6v12a1.5 1.5 0 0 0 1.5 1.5Zm10.5-11.25h.008v.008h-.008V8.25Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z"
/>
</svg>
</span>
<div>
<p class="font-semibold text-gray-900 dark:text-gray-100">Galerie</p>
<p class="text-sm text-gray-500 dark:text-gray-400">Foto oder Video wählen</p>
</div>
</button>
<!-- Camera option -->
<button
onclick={openCamera}
class="flex w-full items-center gap-4 rounded-xl bg-gray-50 px-5 py-4 text-left transition hover:bg-gray-100 active:bg-gray-200 dark:bg-gray-800 dark:hover:bg-gray-700 dark:active:bg-gray-600"
>
<span class="flex h-11 w-11 items-center justify-center rounded-full bg-purple-100 text-purple-600 dark:bg-purple-900/40 dark:text-purple-300">
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M6.827 6.175A2.31 2.31 0 0 1 5.186 7.23c-.38.054-.757.112-1.134.175C2.999 7.58 2.25 8.507 2.25 9.574V18a2.25 2.25 0 0 0 2.25 2.25h15A2.25 2.25 0 0 0 21.75 18V9.574c0-1.067-.75-1.994-1.802-2.169a47.865 47.865 0 0 0-1.134-.175 2.31 2.31 0 0 1-1.64-1.055l-.822-1.316a2.192 2.192 0 0 0-1.736-1.039 48.774 48.774 0 0 0-5.232 0 2.192 2.192 0 0 0-1.736 1.039l-.821 1.316Z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M16.5 12.75a4.5 4.5 0 1 1-9 0 4.5 4.5 0 0 1 9 0ZM18.75 10.5h.008v.008h-.008V10.5Z" />
</svg>
</span>
<div>
<p class="font-semibold text-gray-900 dark:text-gray-100">Kamera</p>
<p class="text-sm text-gray-500 dark:text-gray-400">Jetzt aufnehmen</p>
</div>
</button>
<!-- Camera option -->
<button
onclick={openCamera}
class="flex w-full items-center gap-4 rounded-xl bg-gray-50 px-5 py-4 text-left transition hover:bg-gray-100 active:bg-gray-200 dark:bg-gray-800 dark:hover:bg-gray-700 dark:active:bg-gray-600"
>
<span
class="flex h-11 w-11 items-center justify-center rounded-full bg-purple-100 text-purple-600 dark:bg-purple-900/40 dark:text-purple-300"
>
<svg
class="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="1.5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M6.827 6.175A2.31 2.31 0 0 1 5.186 7.23c-.38.054-.757.112-1.134.175C2.999 7.58 2.25 8.507 2.25 9.574V18a2.25 2.25 0 0 0 2.25 2.25h15A2.25 2.25 0 0 0 21.75 18V9.574c0-1.067-.75-1.994-1.802-2.169a47.865 47.865 0 0 0-1.134-.175 2.31 2.31 0 0 1-1.64-1.055l-.822-1.316a2.192 2.192 0 0 0-1.736-1.039 48.774 48.774 0 0 0-5.232 0 2.192 2.192 0 0 0-1.736 1.039l-.821 1.316Z"
/>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M16.5 12.75a4.5 4.5 0 1 1-9 0 4.5 4.5 0 0 1 9 0ZM18.75 10.5h.008v.008h-.008V10.5Z"
/>
</svg>
</span>
<div>
<p class="font-semibold text-gray-900 dark:text-gray-100">Kamera</p>
<p class="text-sm text-gray-500 dark:text-gray-400">Jetzt aufnehmen</p>
</div>
</button>
<!-- Cancel -->
<button
onclick={close}
class="w-full rounded-xl border border-gray-200 py-3 text-sm font-medium text-gray-600 transition hover:bg-gray-50 dark:border-gray-700 dark:text-gray-300 dark:hover:bg-gray-800"
>
Abbrechen
</button>
<!-- Cancel -->
<button
onclick={close}
class="w-full rounded-xl border border-gray-200 py-3 text-sm font-medium text-gray-600 transition hover:bg-gray-50 dark:border-gray-700 dark:text-gray-300 dark:hover:bg-gray-800"
>
Abbrechen
</button>
{/if}
</div>
</div>

View File

@@ -35,8 +35,7 @@
oncontextmenu?: (upload: FeedUpload) => void;
}
let { uploads, mode, myUserId, onlike, oncomment, onselect, oncontextmenu }: Props =
$props();
let { uploads, mode, myUserId, onlike, oncomment, onselect, oncontextmenu }: Props = $props();
const COLS = 3;
const GRID_GAP = 2; // px — matches the `gap-0.5` the non-virtual grid used.
@@ -218,7 +217,11 @@
/>
{/if}
<div class="absolute inset-0 flex items-center justify-center">
<svg class="h-10 w-10 text-white/80" fill="currentColor" viewBox="0 0 24 24">
<svg
class="h-10 w-10 text-white/80"
fill="currentColor"
viewBox="0 0 24 24"
>
<path d="M8 5v14l11-7z" />
</svg>
</div>