Files
familienarchiv/frontend/src/routes/geschichten/+page.svelte
marcel 38a6d6b0fc
Some checks failed
CI / Unit & Component Tests (push) Failing after 3m48s
CI / OCR Service Tests (push) Successful in 22s
CI / Backend Unit Tests (push) Successful in 5m24s
CI / fail2ban Regex (push) Successful in 53s
CI / Semgrep Security Scan (push) Successful in 23s
CI / Compose Bucket Idempotency (push) Successful in 1m9s
feat(geschichten): show blog writers' own drafts on the Geschichten overview (#807) (#813)
2026-06-12 19:46:03 +02:00

178 lines
5.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 PersonTypeahead from '$lib/person/PersonTypeahead.svelte';
import GeschichteListRow from '$lib/geschichte/GeschichteListRow.svelte';
import DocumentFilterChip from './DocumentFilterChip.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 || data.documentFilter !== null);
const emptyMessage = $derived.by(() => {
if (data.personFilters.length > 0) {
return m.geschichten_empty_for_persons({
names: data.personFilters.map((p) => p.displayName).join(' & ')
});
}
if (data.documentFilter) {
return m.geschichten_empty_for_document();
}
return m.geschichten_empty_no_filter();
});
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() {
const url = new URL(window.location.href);
url.searchParams.delete('personId');
url.searchParams.delete('documentId');
goto(url.pathname + url.search, { 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 removeDocument() {
const url = new URL(window.location.href);
url.searchParams.delete('documentId');
goto(url.pathname + url.search);
}
</script>
<div class="mx-auto max-w-7xl px-4 py-8">
<header class="mb-4 flex flex-wrap items-center justify-between gap-3">
<h1 class="font-serif text-2xl 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>
<!-- Editorial list card: filter pills + rows share one surface -->
<div class="overflow-hidden rounded-sm border border-line bg-surface shadow-sm">
<!-- Filter pills -->
<div class="flex flex-wrap items-center gap-2 border-b border-line-2 px-3 py-2.5">
<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-semibold tracking-wider text-ink-2 uppercase hover:bg-muted aria-pressed:border-primary aria-pressed:bg-primary 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-1.5 rounded-full border border-primary bg-primary px-3 font-sans text-xs font-semibold tracking-wider text-primary-fg uppercase"
>
{p.displayName}
<span aria-hidden="true">×</span>
</button>
{/each}
{#if data.documentFilter}
<DocumentFilterChip
id={data.documentFilter.id}
title={data.documentFilter.title}
onremove={removeDocument}
/>
{/if}
<button
type="button"
aria-expanded={showPersonPicker}
onclick={() => (showPersonPicker = !showPersonPicker)}
class="inline-flex h-11 items-center rounded-full border border-dashed border-line px-3 font-sans text-xs font-semibold text-ink-3 hover:bg-muted"
>
+ {m.geschichten_filter_choose_person()}
</button>
</div>
{#if showPersonPicker}
<div class="border-b border-line-2 px-3 py-3">
<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}
<!-- Entwürfe section (blog writers only, unfiltered) -->
{#if data.drafts.length > 0}
<div class="border-b-2 border-line">
<h2 class="px-3 pt-3 pb-1 font-sans text-xs font-bold tracking-widest text-ink-3 uppercase">
{m.geschichten_drafts_heading()}
<span class="ml-1 font-normal text-ink-3 normal-case"
>{m.geschichten_drafts_unfiltered_caption()}</span
>
</h2>
<ul>
{#each data.drafts as g (g.id)}
<li class="border-b border-line-2 last:border-b-0">
<GeschichteListRow geschichte={g} />
</li>
{/each}
</ul>
</div>
{/if}
<!-- Published rows -->
{#if data.drafts.length > 0}
<!-- Heading only when the Entwürfe section is present, to keep the h2 outline balanced -->
<h2 class="px-3 pt-3 pb-1 font-sans text-xs font-bold tracking-widest text-ink-3 uppercase">
{m.geschichten_published_heading()}
</h2>
{/if}
{#if data.geschichten.length === 0}
<div class="px-4 py-12 text-center font-serif text-sm text-ink-3 italic">
{emptyMessage}
</div>
{:else}
<ul>
{#each data.geschichten as g (g.id)}
<li class="border-b border-line-2 last:border-b-0">
<!-- plaintext for JOURNEY, sanitised-HTML→text for STORY; never {@html} -->
<GeschichteListRow geschichte={g} />
</li>
{/each}
</ul>
{/if}
</div>
</div>