feat(hikes): in-season toggle + unified canton/country filter
Add two filters to the /hikes filter panel: - "Nur Touren in der aktuellen Saison" toggle — keeps only hikes whose recommended season window covers the current month (year-wrap aware; hikes without a window count as year-round). - "Kanton / Land" — a typeahead that abstracts the hike's area over the border: Swiss hikes group by canton (coat-of-arms), hikes abroad by country (flag). Generalised the tag typeahead into ChipTypeahead (optional icon + label mapping) and reused it for both tags and areas. Supporting bits: countries.ts (ISO/name → flag), hikeArea.ts (the canton-or-country resolver, namespaced so codes can't collide), prepared flag SVGs for CH/DE/IT/AT/FR, and an optional `country` field on the hike manifest type (populated by the build script; the app falls back to canton for Swiss hikes until a rebuild).
This commit is contained in:
@@ -8,7 +8,17 @@
|
||||
import Seo from '$lib/components/Seo.svelte';
|
||||
import { HIKES_OVERVIEW } from '$lib/data/hikes.generated';
|
||||
import { hikeFilterBounds } from '$lib/hikes/filterBounds';
|
||||
import { resolveHikeArea } from '$lib/hikes/hikeArea';
|
||||
import type { Difficulty } from '$types/hikes';
|
||||
|
||||
// True when the current month falls inside the hike's recommended season
|
||||
// window. Windows can wrap the new year (start > end, e.g. 11–3 for winter);
|
||||
// a missing/invalid window counts as year-round (always in season).
|
||||
function isInSeason(start: number | null | undefined, end: number | null | undefined, month: number): boolean {
|
||||
if (start == null || end == null) return true;
|
||||
if (start < 1 || start > 12 || end < 1 || end > 12) return true;
|
||||
return start <= end ? month >= start && month <= end : month >= start || month <= end;
|
||||
}
|
||||
import type { PageProps } from './$types';
|
||||
|
||||
const { data }: PageProps = $props();
|
||||
@@ -62,7 +72,9 @@
|
||||
maxLossM: Number.POSITIVE_INFINITY,
|
||||
difficulties: new SvelteSet<Difficulty>(),
|
||||
regions: new SvelteSet<string>(),
|
||||
tags: new SvelteSet<string>()
|
||||
areas: new SvelteSet<string>(),
|
||||
tags: new SvelteSet<string>(),
|
||||
inSeasonOnly: false
|
||||
});
|
||||
|
||||
// Tag deep-link: arrival from a detail-page tag chip (`/hikes?tag=winter`)
|
||||
@@ -121,6 +133,7 @@
|
||||
|
||||
const visible = $derived.by(() => {
|
||||
const out = [];
|
||||
const currentMonth = new Date().getMonth() + 1;
|
||||
for (const h of data.hikes) {
|
||||
if (h.distanceKm < filter.minDistanceKm || h.distanceKm > filter.maxDistanceKm) continue;
|
||||
const dur = h.durationMin ?? 0;
|
||||
@@ -129,6 +142,11 @@
|
||||
if (h.elevationLossM < filter.minLossM || h.elevationLossM > filter.maxLossM) continue;
|
||||
if (filter.difficulties.size > 0 && !filter.difficulties.has(h.difficulty)) continue;
|
||||
if (filter.regions.size > 0 && (!h.region || !filter.regions.has(h.region))) continue;
|
||||
if (filter.areas.size > 0) {
|
||||
const area = resolveHikeArea(h.canton, h.country);
|
||||
if (!area || !filter.areas.has(area.value)) continue;
|
||||
}
|
||||
if (filter.inSeasonOnly && !isInSeason(h.seasonStart, h.seasonEnd, currentMonth)) continue;
|
||||
// Multi-tag = OR (a hike matching ANY selected tag is shown). AND
|
||||
// would shrink the listing to ~zero quickly given how few tags
|
||||
// most hikes have; OR matches how detail-page chips feel like
|
||||
|
||||
Reference in New Issue
Block a user