Files
familienarchiv/frontend/src/routes/geschichten/+page.svelte
Marcel e6c890c61e feat(frontend): update generated API types and Geschichte routes for JourneyItem model
- api.ts: add GeschichteType, JourneyItem, GeschichteSummary schemas;
  remove documentId param from list endpoint; change list response to
  GeschichteSummary[]; add type + items to Geschichte; remove documents field
- GeschichteEditor: remove DocumentMultiSelect + documentIds from payload
  (journey items are managed via the future Lesereisen editor, not here)
- GET /geschichten page: remove documentId filter from server load + URL logic
- geschichten/new: remove documentId pre-population from server load
- geschichten/[id]: replace g.documents with g.items (document-backed JourneyItems)
- geschichten/new + [id]/edit: remove documentIds from submit payload type

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 12:39:53 +02:00

149 lines
4.6 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script lang="ts">
import { goto } from '$app/navigation';
import { m } from '$lib/paraglide/messages.js';
import { plainExcerpt } from '$lib/shared/utils/extractText';
import { formatDate } from '$lib/shared/utils/date';
import PersonTypeahead from '$lib/person/PersonTypeahead.svelte';
import type { PageData } from './$types';
let { data }: { data: PageData } = $props();
let showPersonPicker = $state(false);
const selectedPersonIds = $derived(data.personFilters.map((p) => p.id!));
const hasFilters = $derived(data.personFilters.length > 0);
function rebuildUrl(personIds: string[]) {
const url = new URL(window.location.href);
url.searchParams.delete('personId');
for (const id of personIds) url.searchParams.append('personId', id);
return url.pathname + url.search;
}
function clearAll() {
goto(rebuildUrl([]), { replaceState: true });
}
function addPerson(personId: string) {
if (!personId || selectedPersonIds.includes(personId)) {
showPersonPicker = false;
return;
}
showPersonPicker = false;
goto(rebuildUrl([...selectedPersonIds, personId]));
}
function removePerson(personId: string) {
goto(rebuildUrl(selectedPersonIds.filter((id) => id !== personId)));
}
function authorName(g: { author?: { firstName?: string; lastName?: string; email: string } }) {
const a = g.author;
if (!a) return '';
const full = [a.firstName, a.lastName].filter(Boolean).join(' ').trim();
return full || a.email || '';
}
function publishedAt(g: { publishedAt?: string }): string | null {
if (!g.publishedAt) return null;
return formatDate(g.publishedAt.slice(0, 10), 'short');
}
</script>
<div class="mx-auto max-w-4xl px-4 py-8">
<header class="mb-6 flex flex-wrap items-center justify-between gap-3">
<h1 class="font-serif text-3xl font-bold text-ink">{m.geschichten_index_title()}</h1>
{#if data.canBlogWrite}
<a
href="/geschichten/new"
class="inline-flex h-11 items-center rounded bg-primary px-4 font-sans text-sm font-medium text-primary-fg hover:opacity-90 focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring"
>
{m.geschichten_new_button()}
</a>
{/if}
</header>
<!-- Filter pills -->
<div class="mb-6 flex flex-wrap items-center gap-2">
<button
type="button"
aria-pressed={!hasFilters}
onclick={clearAll}
class="inline-flex h-11 items-center rounded-full border border-line px-3 font-sans text-xs font-bold tracking-wider text-ink-2 uppercase hover:bg-muted aria-pressed:bg-ink aria-pressed:text-primary-fg"
>
{m.geschichten_filter_all_pill()}
</button>
{#each data.personFilters as p (p.id)}
<button
type="button"
aria-pressed="true"
aria-label={m.geschichten_filter_remove_chip({ name: p.displayName })}
onclick={() => removePerson(p.id!)}
class="inline-flex h-11 items-center gap-2 rounded-full bg-ink px-3 font-sans text-xs font-bold tracking-wider text-primary-fg uppercase"
>
{p.displayName}
<span aria-hidden="true">×</span>
</button>
{/each}
<button
type="button"
aria-expanded={showPersonPicker}
onclick={() => (showPersonPicker = !showPersonPicker)}
class="inline-flex h-11 items-center rounded-full border border-line px-3 font-sans text-xs font-bold tracking-wider text-ink-2 uppercase hover:bg-muted"
>
+ {m.geschichten_filter_choose_person()}
</button>
</div>
{#if showPersonPicker}
<div class="mb-4">
<PersonTypeahead
name="filter-person"
label={m.geschichten_filter_choose_person()}
compact
autofocus
onchange={addPerson}
/>
{#if selectedPersonIds.length > 1}
<p class="mt-1 font-sans text-xs text-ink-3">
{m.geschichten_filter_and_hint()}
</p>
{/if}
</div>
{/if}
<!-- Card list -->
{#if data.geschichten.length === 0}
<div class="rounded border border-line bg-surface p-6 text-center font-sans text-sm text-ink-3">
{#if data.personFilters.length > 0}
{m.geschichten_empty_for_persons({
names: data.personFilters.map((p) => p.displayName).join(' & ')
})}
{:else}
{m.geschichten_empty_no_filter()}
{/if}
</div>
{:else}
<ul class="flex flex-col gap-4">
{#each data.geschichten as g (g.id)}
<li
class="rounded border border-line bg-surface p-5 shadow-sm transition-shadow hover:shadow-md"
>
<a href="/geschichten/{g.id}" class="block">
<h2 class="mb-1 font-serif text-xl font-bold text-ink">{g.title}</h2>
<p class="mb-3 font-sans text-xs text-ink-3">
{authorName(g)}
{#if publishedAt(g)}· {m.geschichten_published_on({ date: publishedAt(g)! })}{/if}
</p>
{#if g.body}
<p class="font-serif text-base text-ink-2">{plainExcerpt(g.body, 150)}</p>
{/if}
</a>
</li>
{/each}
</ul>
{/if}
</div>