f3d16d5187
Build pipeline (scripts/build-hikes.ts) parses per-hike GPX, encodes images via sharp, reverse-geocodes the centroid against Swisstopo and emits a typed manifest under src/lib/data/hikes.generated.ts (gitignored). Track JSON + image binaries live outside /static; served in dev by a small hike-images plugin in vite.config.ts, in prod by nginx (private/ images proxied through Node + X-Accel-Redirect for auth-gating). /hikes overview: full-bleed Swisstopo hero map (HikesOverviewMap) sits under the sticky nav, drawing one polyline per route coloured by SAC tier (T1 yellow Wegweiser, T2/T3 white-red-white, T4-T6 white-blue- white). Click navigates, hover thickens + tooltips. Layer toggle, recenter, GPS controls mirror the detail map (minus images toggle). Cards drop the trail SVG, gain a per-route icon + SAC marker pictogram on the cover, altitude range, season label, and "Neu" badge for recently-published hikes. Filter bar + totals strip recompute over the currently-visible set. /hikes/[slug]: hero map with elevation profile, photo strip with map sync, scroll-position pin, GPX download, SAC marker stats + min/max altitude + season. Route-builder (/hikes/route-builder): client-side draft persisted to localStorage, EXIF-driven image placement, snap-to-route via BRouter (OSRM + linear fallback) and Swisstopo profile.json elevation enrichment that handles degenerate same-coord segments via the height endpoint. Filter init switched from a script-time snapshot of data.hikes (which sporadically returned a one-hike subset during dev hydration and locked the page to that single hike) to a post-mount \$effect. Content under src/content/hikes/ intentionally not included (WIP).
43 lines
1.6 KiB
TypeScript
43 lines
1.6 KiB
TypeScript
/**
|
|
* Shared focus state for a hike detail page's photo strip + map.
|
|
*
|
|
* Writing to `focused.index` from the strip (source='strip') makes the map fly
|
|
* to that photo and pulse a focus ring; writing from the map (source='map')
|
|
* makes the strip scroll the matching card into view and highlight it. Each
|
|
* side ignores its own writes via the `source` field so the two never feed
|
|
* back into each other.
|
|
*
|
|
* Indexes are positions in the visibility-filtered `ImagePoint[]` that both
|
|
* components share — the page filters once and hands the same array down.
|
|
*/
|
|
|
|
/**
|
|
* Sources of focus-store writes:
|
|
* - `'strip'`: the user clicked a thumbnail or used a chevron / arrow key.
|
|
* Full sync: map flies to the marker, strip centres the card.
|
|
* - `'map'`: the user clicked a map marker. Strip scrolls + highlights,
|
|
* but the map doesn't fly to itself.
|
|
* - `'map-hover'`: the user is hovering a map marker. Strip skips scroll
|
|
* (would jerk across dense clusters), and the map skips its own flyTo +
|
|
* focus ring (the user is already looking at it).
|
|
* - `'inline'`: an inline `<HikeImage>` scrolled into the viewport's middle
|
|
* band. Full sync: map flies to the marker, strip centres the card. This
|
|
* is the desktop scrollytelling driver.
|
|
*/
|
|
export type FocusSource = 'map' | 'map-hover' | 'strip' | 'inline' | null;
|
|
|
|
export const focused = $state<{ index: number | null; source: FocusSource }>({
|
|
index: null,
|
|
source: null
|
|
});
|
|
|
|
export function setFocused(index: number | null, source: FocusSource): void {
|
|
focused.index = index;
|
|
focused.source = source;
|
|
}
|
|
|
|
export function clearFocused(): void {
|
|
focused.index = null;
|
|
focused.source = null;
|
|
}
|