feat(timeline): wire the layer filter into /zeitstrahl
The route holds the three layer toggles in $state, binds them into TimelineFilters, and derives a client-side filtered view of the SSR-loaded timeline that it passes to TimelineView — no goto, no URL param, no extra fetch. When the active toggles leave nothing visible it renders a calm filtered-empty message plus a one-click reset below the still-open filter bar, never a blank page and never the generic "no events" state. The meta-line keeps counting the unfiltered timeline (D1 known limitation). Refs #780 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
<script lang="ts">
|
||||
import * as m from '$lib/paraglide/messages.js';
|
||||
import TimelineView from '$lib/timeline/TimelineView.svelte';
|
||||
import TimelineFilters from '$lib/timeline/TimelineFilters.svelte';
|
||||
import { timelineMeta } from '$lib/timeline/timelineMeta';
|
||||
import { filterTimeline } from '$lib/timeline/timelineFilter';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
let { data }: { data: PageData } = $props();
|
||||
@@ -9,6 +11,28 @@ let { data }: { data: PageData } = $props();
|
||||
const meta = $derived(timelineMeta(data.timeline));
|
||||
const hasContent = $derived(data.timeline.years.length > 0 || data.timeline.undated.length > 0);
|
||||
|
||||
// Layer-filter state (#780). Layer hiding is client-side only — the whole
|
||||
// timeline is loaded once by #779's SSR load and we derive a filtered view of
|
||||
// it here; there is no goto, no URL param, and no extra fetch. Known limitation
|
||||
// (D1): the meta-line counts above stay on the unfiltered timeline, so they
|
||||
// include entries the active toggles hide.
|
||||
let personalOn = $state(true);
|
||||
let historicalOn = $state(true);
|
||||
let lettersOn = $state(true);
|
||||
|
||||
const filteredTimeline = $derived(
|
||||
filterTimeline(data.timeline, { personalOn, historicalOn, lettersOn })
|
||||
);
|
||||
const filteredEmpty = $derived(
|
||||
filteredTimeline.years.length === 0 && filteredTimeline.undated.length === 0
|
||||
);
|
||||
|
||||
function resetFilters() {
|
||||
personalOn = true;
|
||||
historicalOn = true;
|
||||
lettersOn = true;
|
||||
}
|
||||
|
||||
// Compose the sub-line from segments joined by " · " so the range drops out
|
||||
// cleanly when there are no year bands; the whole line is absent when the
|
||||
// timeline is empty (REQ-002). Counts come from the route alone, never from
|
||||
@@ -51,7 +75,29 @@ const metaLine = $derived.by(() => {
|
||||
<h1 class="font-serif text-2xl font-bold text-brand-navy">{m.timeline_heading()}</h1>
|
||||
{#if hasContent}
|
||||
<p data-testid="timeline-meta" class="mt-1 mb-6 font-sans text-xs text-ink-3">{metaLine}</p>
|
||||
<TimelineFilters
|
||||
bind:personalOn={personalOn}
|
||||
bind:historicalOn={historicalOn}
|
||||
bind:lettersOn={lettersOn}
|
||||
/>
|
||||
{/if}
|
||||
{#if hasContent && filteredEmpty}
|
||||
<!-- Filtered-empty: a calm message + one-click reset below the still-open
|
||||
filter bar — never a blank page, and never the generic "no events"
|
||||
state (which would imply the archive itself is empty). REQ-006. -->
|
||||
<div data-testid="timeline-filter-empty" class="py-12 text-center">
|
||||
<p class="font-serif text-base text-ink-2">{m.timeline_filter_empty_state()}</p>
|
||||
<button
|
||||
type="button"
|
||||
data-testid="timeline-filter-empty-reset"
|
||||
onclick={resetFilters}
|
||||
class="mt-3 inline-flex min-h-[44px] items-center font-sans text-sm text-primary underline-offset-2 hover:underline"
|
||||
>
|
||||
{m.timeline_filter_reset()}
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
<TimelineView timeline={filteredTimeline} />
|
||||
{/if}
|
||||
<TimelineView timeline={data.timeline} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user