fix(hikes): sync tag filter to URL + re-fit overview map on filter change

Two bugs:

* Toggling a tag in the filter bar didn't update `?tag=` in the URL,
  so the page wasn't shareable / back-button-restorable past the
  initial deep-link state. Add a writer $effect that mirrors
  `filter.tags` into the URL via `replaceState` (no history churn).

* The overview map's polylines + camera were built once on mount and
  never refreshed when the `hikes` prop changed, so filtering left
  the map showing the full set and zoomed for it. Extract polyline
  rendering into a function called both on mount and from a
  prop-watching $effect; on change, smoothly fly to the new union.
This commit is contained in:
2026-05-19 10:19:08 +02:00
parent 2a8721fde0
commit 706dedbdc5
3 changed files with 105 additions and 53 deletions
+27 -4
View File
@@ -1,6 +1,7 @@
<script lang="ts">
import { SvelteSet } from 'svelte/reactivity';
import { page } from '$app/state';
import { replaceState } from '$app/navigation';
import HikeCard from '$lib/components/hikes/HikeCard.svelte';
import HikesFilterBar, { type HikesFilter } from '$lib/components/hikes/HikesFilterBar.svelte';
import HikesOverviewMap from '$lib/components/hikes/HikesOverviewMap.svelte';
@@ -60,14 +61,36 @@
});
// Tag deep-link: arrival from a detail-page tag chip (`/hikes?tag=winter`)
// or any saved URL with `?tag=...` pre-selects those tags. Repeated
// params accumulate (`?tag=winter&tag=easy`). Only runs on the client —
// SSR has no searchParams to read here.
// or any saved URL with `?tag=...` pre-selects those tags. Runs once on
// mount; thereafter the URL writer below is the source of truth.
let initialTagsApplied = false;
$effect(() => {
if (initialTagsApplied) return;
if (typeof window === 'undefined') return;
const params = page.url.searchParams.getAll('tag');
if (params.length === 0) return;
for (const t of params) if (t) filter.tags.add(t);
initialTagsApplied = true;
});
// Tag URL sync: every toggle in the filter bar reflects into the URL
// so the page is shareable / back-button-restorable. `replaceState`
// rather than `goto` keeps history clean — toggling four tags would
// otherwise leave four back-button stops.
$effect(() => {
if (typeof window === 'undefined' || !initialTagsApplied) return;
const url = new URL(window.location.href);
const wanted = [...filter.tags].sort();
const current = url.searchParams.getAll('tag').slice().sort();
// Skip the no-op rewrite path — `replaceState` would still touch
// history's state object and trigger downstream `page.url` effects
// for no UX benefit.
if (
wanted.length === current.length &&
wanted.every((t, i) => t === current[i])
) return;
url.searchParams.delete('tag');
for (const t of wanted) url.searchParams.append('tag', t);
replaceState(url, page.state);
});
// One-shot per mount: set the slider ceilings to the actual data maxes.