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:
@@ -1,3 +1,3 @@
|
||||
/* Pulls the live app's design tokens so the offline keepsake matches visually.
|
||||
* See ../../src/tailwind-theme.css for the source of truth. */
|
||||
@import "../../src/tailwind-theme.css";
|
||||
@import '../../src/tailwind-theme.css';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import type { ViewerData, ViewerPost, ViewerComment } from '$lib/types';
|
||||
import type { ViewerData, ViewerPost } from '$lib/types';
|
||||
|
||||
let data = $state<ViewerData | null>(null);
|
||||
let loading = $state(true);
|
||||
@@ -13,7 +13,10 @@
|
||||
let searchQuery = $state('');
|
||||
let showAutocomplete = $state(false);
|
||||
|
||||
interface Filter { type: 'tag' | 'user'; value: string }
|
||||
interface Filter {
|
||||
type: 'tag' | 'user';
|
||||
value: string;
|
||||
}
|
||||
let activeFilters = $state<Filter[]>([]);
|
||||
|
||||
// Lightbox state
|
||||
@@ -29,6 +32,7 @@
|
||||
let posts = $derived(data?.posts ?? []);
|
||||
|
||||
let allTags = $derived.by(() => {
|
||||
// eslint-disable-next-line svelte/prefer-svelte-reactivity -- local throwaway counter inside a $derived.by; never stored in $state.
|
||||
const freq = new Map<string, number>();
|
||||
for (const p of posts) {
|
||||
for (const t of p.tags) {
|
||||
@@ -47,7 +51,7 @@
|
||||
if (!showAutocomplete) return [];
|
||||
return [
|
||||
...allUploaders.slice(0, 3).map((u) => ({ type: 'user' as const, value: u })),
|
||||
...allTags.slice(0, 3).map(([t]) => ({ type: 'tag' as const, value: t })),
|
||||
...allTags.slice(0, 3).map(([t]) => ({ type: 'tag' as const, value: t }))
|
||||
];
|
||||
}
|
||||
if (q.startsWith('#')) {
|
||||
@@ -66,7 +70,7 @@
|
||||
...allTags
|
||||
.filter(([t]) => t.includes(lower))
|
||||
.slice(0, 4)
|
||||
.map(([t]) => ({ type: 'tag' as const, value: t })),
|
||||
.map(([t]) => ({ type: 'tag' as const, value: t }))
|
||||
];
|
||||
});
|
||||
|
||||
@@ -98,8 +102,9 @@
|
||||
const res = await fetch('./data.json');
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
data = await res.json();
|
||||
} catch (e) {
|
||||
error = 'Daten konnten nicht geladen werden. Stelle sicher, dass data.json im selben Ordner liegt.';
|
||||
} catch {
|
||||
error =
|
||||
'Daten konnten nicht geladen werden. Stelle sicher, dass data.json im selben Ordner liegt.';
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
@@ -110,14 +115,20 @@
|
||||
function formatDate(iso: string): string {
|
||||
const d = new Date(iso);
|
||||
return d.toLocaleString('de-DE', {
|
||||
day: '2-digit', month: '2-digit', year: 'numeric',
|
||||
hour: '2-digit', minute: '2-digit',
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
}
|
||||
|
||||
function formatShortDate(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'
|
||||
});
|
||||
}
|
||||
|
||||
@@ -131,7 +142,7 @@
|
||||
'bg-green-100 text-green-700',
|
||||
'bg-amber-100 text-amber-700',
|
||||
'bg-rose-100 text-rose-700',
|
||||
'bg-teal-100 text-teal-700',
|
||||
'bg-teal-100 text-teal-700'
|
||||
];
|
||||
function avatarColor(name: string): string {
|
||||
let hash = 0;
|
||||
@@ -214,7 +225,9 @@
|
||||
|
||||
{#if loading}
|
||||
<div class="flex min-h-screen items-center justify-center bg-gray-50">
|
||||
<div class="inline-block h-8 w-8 animate-spin rounded-full border-2 border-gray-300 border-t-blue-600"></div>
|
||||
<div
|
||||
class="inline-block h-8 w-8 animate-spin rounded-full border-2 border-gray-300 border-t-blue-600"
|
||||
></div>
|
||||
</div>
|
||||
{:else if error}
|
||||
<div class="flex min-h-screen items-center justify-center bg-gray-50 p-4">
|
||||
@@ -233,20 +246,44 @@
|
||||
<div class="flex items-center gap-1 rounded-lg bg-gray-100 p-1">
|
||||
<button
|
||||
onclick={() => switchView('list')}
|
||||
class="rounded-md p-1.5 transition-colors {viewMode === 'list' ? 'bg-white text-gray-900 shadow-sm' : 'text-gray-400 hover:text-gray-600'}"
|
||||
class="rounded-md p-1.5 transition-colors {viewMode === 'list'
|
||||
? 'bg-white text-gray-900 shadow-sm'
|
||||
: 'text-gray-400 hover:text-gray-600'}"
|
||||
aria-label="Listenansicht"
|
||||
>
|
||||
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
|
||||
<svg
|
||||
class="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
onclick={() => switchView('grid')}
|
||||
class="rounded-md p-1.5 transition-colors {viewMode === 'grid' ? 'bg-white text-gray-900 shadow-sm' : 'text-gray-400 hover:text-gray-600'}"
|
||||
class="rounded-md p-1.5 transition-colors {viewMode === 'grid'
|
||||
? 'bg-white text-gray-900 shadow-sm'
|
||||
: 'text-gray-400 hover:text-gray-600'}"
|
||||
aria-label="Rasteransicht"
|
||||
>
|
||||
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6A2.25 2.25 0 0 1 6 3.75h2.25A2.25 2.25 0 0 1 10.5 6v2.25a2.25 2.25 0 0 1-2.25 2.25H6a2.25 2.25 0 0 1-2.25-2.25V6ZM3.75 15.75A2.25 2.25 0 0 1 6 13.5h2.25a2.25 2.25 0 0 1 2.25 2.25V18a2.25 2.25 0 0 1-2.25 2.25H6A2.25 2.25 0 0 1 3.75 18v-2.25ZM13.5 6a2.25 2.25 0 0 1 2.25-2.25H18A2.25 2.25 0 0 1 20.25 6v2.25A2.25 2.25 0 0 1 18 10.5h-2.25a2.25 2.25 0 0 1-2.25-2.25V6ZM13.5 15.75a2.25 2.25 0 0 1 2.25-2.25H18a2.25 2.25 0 0 1 2.25 2.25V18A2.25 2.25 0 0 1 18 20.25h-2.25A2.25 2.25 0 0 1 13.5 18v-2.25Z" />
|
||||
<svg
|
||||
class="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M3.75 6A2.25 2.25 0 0 1 6 3.75h2.25A2.25 2.25 0 0 1 10.5 6v2.25a2.25 2.25 0 0 1-2.25 2.25H6a2.25 2.25 0 0 1-2.25-2.25V6ZM3.75 15.75A2.25 2.25 0 0 1 6 13.5h2.25a2.25 2.25 0 0 1 2.25 2.25V18a2.25 2.25 0 0 1-2.25 2.25H6A2.25 2.25 0 0 1 3.75 18v-2.25ZM13.5 6a2.25 2.25 0 0 1 2.25-2.25H18A2.25 2.25 0 0 1 20.25 6v2.25A2.25 2.25 0 0 1 18 10.5h-2.25a2.25 2.25 0 0 1-2.25-2.25V6ZM13.5 15.75a2.25 2.25 0 0 1 2.25-2.25H18a2.25 2.25 0 0 1 2.25 2.25V18A2.25 2.25 0 0 1 18 20.25h-2.25A2.25 2.25 0 0 1 13.5 18v-2.25Z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
@@ -258,22 +295,20 @@
|
||||
<div class="flex gap-2 overflow-x-auto pb-2">
|
||||
<button
|
||||
onclick={() => selectHashtag(null)}
|
||||
class="shrink-0 rounded-full px-3 py-1 text-sm font-medium transition {
|
||||
selectedHashtag === null
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
|
||||
}"
|
||||
class="shrink-0 rounded-full px-3 py-1 text-sm font-medium transition {selectedHashtag ===
|
||||
null
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'}"
|
||||
>
|
||||
Alle
|
||||
</button>
|
||||
{#each allTags as [tag, count] (tag)}
|
||||
<button
|
||||
onclick={() => selectHashtag(tag)}
|
||||
class="shrink-0 rounded-full px-3 py-1 text-sm font-medium transition {
|
||||
selectedHashtag === tag
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
|
||||
}"
|
||||
class="shrink-0 rounded-full px-3 py-1 text-sm font-medium transition {selectedHashtag ===
|
||||
tag
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'}"
|
||||
>
|
||||
#{tag}
|
||||
<span class="ml-1 text-xs opacity-70">{count}</span>
|
||||
@@ -287,9 +322,21 @@
|
||||
{#if viewMode === 'grid'}
|
||||
<div class="mx-auto max-w-2xl px-4 pb-3">
|
||||
<div class="relative">
|
||||
<div class="flex items-center gap-2 rounded-xl border border-gray-200 bg-gray-50 px-3 py-2 focus-within:border-blue-400 focus-within:bg-white focus-within:ring-1 focus-within:ring-blue-200">
|
||||
<svg class="h-4 w-4 shrink-0 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z" />
|
||||
<div
|
||||
class="flex items-center gap-2 rounded-xl border border-gray-200 bg-gray-50 px-3 py-2 focus-within:border-blue-400 focus-within:bg-white focus-within:ring-1 focus-within:ring-blue-200"
|
||||
>
|
||||
<svg
|
||||
class="h-4 w-4 shrink-0 text-gray-400"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z"
|
||||
/>
|
||||
</svg>
|
||||
<input
|
||||
type="search"
|
||||
@@ -301,11 +348,19 @@
|
||||
/>
|
||||
{#if searchQuery}
|
||||
<button
|
||||
onclick={() => { searchQuery = ''; }}
|
||||
onclick={() => {
|
||||
searchQuery = '';
|
||||
}}
|
||||
class="shrink-0 text-gray-400 hover:text-gray-600"
|
||||
aria-label="Suche loschen"
|
||||
>
|
||||
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<svg
|
||||
class="h-4 w-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
@@ -314,15 +369,27 @@
|
||||
|
||||
<!-- Autocomplete dropdown -->
|
||||
{#if showAutocomplete && suggestions.length > 0}
|
||||
<div class="absolute left-0 right-0 top-full z-50 mt-1 overflow-hidden rounded-xl border border-gray-200 bg-white shadow-lg">
|
||||
{#each suggestions as item}
|
||||
<div
|
||||
class="absolute left-0 right-0 top-full z-50 mt-1 overflow-hidden rounded-xl border border-gray-200 bg-white shadow-lg"
|
||||
>
|
||||
{#each suggestions as item (item.type + ':' + item.value)}
|
||||
<button
|
||||
class="flex w-full items-center gap-3 px-4 py-2.5 text-left text-sm hover:bg-gray-50"
|
||||
onmousedown={() => selectSuggestion(item)}
|
||||
>
|
||||
{#if item.type === 'user'}
|
||||
<svg class="h-4 w-4 shrink-0 text-gray-400" 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" />
|
||||
<svg
|
||||
class="h-4 w-4 shrink-0 text-gray-400"
|
||||
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"
|
||||
/>
|
||||
</svg>
|
||||
<span class="font-medium text-gray-900">{item.value}</span>
|
||||
{:else}
|
||||
@@ -338,12 +405,28 @@
|
||||
<!-- Active filter chips -->
|
||||
{#if activeFilters.length > 0}
|
||||
<div class="mt-2 flex flex-wrap items-center gap-1.5">
|
||||
{#each activeFilters as filter}
|
||||
<span class="flex items-center gap-1 rounded-full bg-blue-100 px-2.5 py-0.5 text-xs font-medium text-blue-700">
|
||||
{#each activeFilters as filter (filter.type + ':' + filter.value)}
|
||||
<span
|
||||
class="flex items-center gap-1 rounded-full bg-blue-100 px-2.5 py-0.5 text-xs font-medium text-blue-700"
|
||||
>
|
||||
{filter.type === 'tag' ? '#' : ''}{filter.value}
|
||||
<button onclick={() => removeFilter(filter)} class="ml-0.5 hover:text-blue-900" aria-label="Filter entfernen">
|
||||
<svg class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
|
||||
<button
|
||||
onclick={() => removeFilter(filter)}
|
||||
class="ml-0.5 hover:text-blue-900"
|
||||
aria-label="Filter entfernen"
|
||||
>
|
||||
<svg
|
||||
class="h-3 w-3"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="2.5"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M6 18 18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</span>
|
||||
@@ -364,7 +447,9 @@
|
||||
<div class="py-20 text-center">
|
||||
<p class="text-lg text-gray-400">Noch keine Fotos.</p>
|
||||
{#if activeFilters.length > 0}
|
||||
<button onclick={clearFilters} class="mt-2 text-sm text-blue-600 hover:underline">Filter zurucksetzen</button>
|
||||
<button onclick={clearFilters} class="mt-2 text-sm text-blue-600 hover:underline"
|
||||
>Filter zurucksetzen</button
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if viewMode === 'list'}
|
||||
@@ -395,10 +480,16 @@
|
||||
{#if post.media.type === 'video'}
|
||||
<div class="relative aspect-video w-full bg-gray-900">
|
||||
{#if post.media.thumb}
|
||||
<img src={post.media.thumb} alt="" class="h-full w-full object-cover opacity-80" />
|
||||
<img
|
||||
src={post.media.thumb}
|
||||
alt=""
|
||||
class="h-full w-full object-cover opacity-80"
|
||||
/>
|
||||
{/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>
|
||||
@@ -415,8 +506,18 @@
|
||||
/>
|
||||
{:else}
|
||||
<div class="flex aspect-square w-full items-center justify-center bg-gray-100">
|
||||
<svg class="h-12 w-12 text-gray-300" 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
|
||||
class="h-12 w-12 text-gray-300"
|
||||
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}
|
||||
@@ -425,14 +526,34 @@
|
||||
<!-- Stats row (read-only) -->
|
||||
<div class="flex items-center gap-4 px-4 py-2">
|
||||
<span class="flex items-center gap-1.5 text-sm font-medium text-gray-500">
|
||||
<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="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-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
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"
|
||||
/>
|
||||
</svg>
|
||||
{post.likes}
|
||||
</span>
|
||||
<span class="flex items-center gap-1.5 text-sm font-medium text-gray-500">
|
||||
<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" />
|
||||
<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"
|
||||
/>
|
||||
</svg>
|
||||
{post.comments.length}
|
||||
</span>
|
||||
@@ -454,7 +575,9 @@
|
||||
<div class="mx-auto max-w-2xl">
|
||||
<div class="grid grid-cols-3 gap-0.5">
|
||||
{#each displayPosts as post (post.id)}
|
||||
<div class="group relative aspect-square cursor-pointer overflow-hidden rounded-lg bg-gray-100">
|
||||
<div
|
||||
class="group relative aspect-square cursor-pointer overflow-hidden rounded-lg bg-gray-100"
|
||||
>
|
||||
<button
|
||||
onclick={() => openLightbox(post)}
|
||||
class="block h-full w-full"
|
||||
@@ -472,29 +595,51 @@
|
||||
</div>
|
||||
</div>
|
||||
{:else if post.media.thumb}
|
||||
<img src={post.media.thumb} alt="" class="h-full w-full object-cover" loading="lazy" />
|
||||
<img
|
||||
src={post.media.thumb}
|
||||
alt=""
|
||||
class="h-full w-full object-cover"
|
||||
loading="lazy"
|
||||
/>
|
||||
{:else}
|
||||
<div class="flex h-full items-center justify-center text-gray-400">
|
||||
<svg class="h-8 w-8" 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" />
|
||||
<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}
|
||||
</button>
|
||||
|
||||
<!-- Overlay with name and stats -->
|
||||
<div class="pointer-events-none absolute inset-x-0 bottom-0 bg-gradient-to-t from-black/60 to-transparent p-2">
|
||||
<div
|
||||
class="pointer-events-none absolute inset-x-0 bottom-0 bg-gradient-to-t from-black/60 to-transparent p-2"
|
||||
>
|
||||
<p class="truncate text-xs font-medium text-white">{post.uploader}</p>
|
||||
<div class="mt-0.5 flex items-center gap-3 text-xs text-white/80">
|
||||
<span class="flex items-center gap-0.5">
|
||||
<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="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"
|
||||
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>
|
||||
{post.likes}
|
||||
</span>
|
||||
<span class="flex items-center gap-0.5">
|
||||
<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="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"
|
||||
stroke-width="2"
|
||||
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>
|
||||
{post.comments.length}
|
||||
</span>
|
||||
@@ -519,16 +664,28 @@
|
||||
class="fixed inset-0 z-50 flex items-center justify-center bg-black/80"
|
||||
role="dialog"
|
||||
tabindex="-1"
|
||||
onclick={(e) => { if (e.target === e.currentTarget) closeLightbox(); }}
|
||||
onclick={(e) => {
|
||||
if (e.target === e.currentTarget) closeLightbox();
|
||||
}}
|
||||
ontouchstart={handleTouchStart}
|
||||
ontouchend={handleTouchEnd}
|
||||
>
|
||||
<div class="flex max-h-[95vh] w-full max-w-2xl flex-col overflow-hidden rounded-xl bg-white sm:m-4">
|
||||
<div
|
||||
class="flex max-h-[95vh] w-full max-w-2xl flex-col overflow-hidden rounded-xl bg-white sm:m-4"
|
||||
>
|
||||
<!-- Media -->
|
||||
<div class="relative bg-black">
|
||||
<button onclick={closeLightbox} class="absolute right-2 top-2 z-10 rounded-full bg-black/50 p-1.5 text-white hover:bg-black/70">
|
||||
<button
|
||||
onclick={closeLightbox}
|
||||
class="absolute right-2 top-2 z-10 rounded-full bg-black/50 p-1.5 text-white hover: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>
|
||||
|
||||
@@ -539,8 +696,18 @@
|
||||
class="absolute left-2 top-1/2 z-10 -translate-y-1/2 rounded-full bg-black/50 p-1.5 text-white hover:bg-black/70"
|
||||
aria-label="Vorheriges"
|
||||
>
|
||||
<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
|
||||
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>
|
||||
</button>
|
||||
<button
|
||||
@@ -548,8 +715,18 @@
|
||||
class="absolute right-2 top-1/2 z-10 -translate-y-1/2 rounded-full bg-black/50 p-1.5 text-white hover:bg-black/70"
|
||||
aria-label="Nachstes"
|
||||
>
|
||||
<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.25 4.5 7.5 7.5-7.5 7.5" />
|
||||
<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.25 4.5 7.5 7.5-7.5 7.5"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
{/if}
|
||||
@@ -562,11 +739,7 @@
|
||||
poster={selectedPost.media.thumb || undefined}
|
||||
></video>
|
||||
{:else}
|
||||
<img
|
||||
src={selectedPost.media.full}
|
||||
alt=""
|
||||
class="max-h-[50vh] w-full object-contain"
|
||||
/>
|
||||
<img src={selectedPost.media.full} alt="" class="max-h-[50vh] w-full object-contain" />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -576,17 +749,28 @@
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<span class="font-medium text-gray-900">{selectedPost.uploader}</span>
|
||||
<span class="ml-2 text-xs text-gray-400">{formatShortDate(selectedPost.timestamp)}</span>
|
||||
<span class="ml-2 text-xs text-gray-400"
|
||||
>{formatShortDate(selectedPost.timestamp)}</span
|
||||
>
|
||||
</div>
|
||||
<span class="flex items-center gap-1 rounded-full bg-gray-100 px-2.5 py-1 text-sm text-gray-600">
|
||||
<span
|
||||
class="flex items-center gap-1 rounded-full bg-gray-100 px-2.5 py-1 text-sm text-gray-600"
|
||||
>
|
||||
<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="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"
|
||||
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>
|
||||
{selectedPost.likes}
|
||||
</span>
|
||||
</div>
|
||||
{#if selectedPost.caption}
|
||||
<p class="mt-1 text-sm text-gray-700 [overflow-wrap:anywhere]">{selectedPost.caption}</p>
|
||||
<p class="mt-1 text-sm text-gray-700 [overflow-wrap:anywhere]">
|
||||
{selectedPost.caption}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -596,11 +780,13 @@
|
||||
<p class="text-center text-sm text-gray-400">Keine Kommentare.</p>
|
||||
{:else}
|
||||
<div class="space-y-3">
|
||||
{#each selectedPost.comments as comment}
|
||||
{#each selectedPost.comments as comment, i (i)}
|
||||
<div>
|
||||
<span class="text-sm font-medium text-gray-900">{comment.author}</span>
|
||||
<span class="ml-1 text-sm text-gray-700">{comment.text}</span>
|
||||
<div class="mt-0.5 text-xs text-gray-400">{formatShortDate(comment.timestamp)}</div>
|
||||
<div class="mt-0.5 text-xs text-gray-400">
|
||||
{formatShortDate(comment.timestamp)}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user