|
| 1 | +'use client'; |
| 2 | + |
| 3 | +import { useEffect, useRef } from 'react'; |
| 4 | +import { useUIStore } from '../stores/ui-store'; |
| 5 | +import { TIME_WINDOWS, type TimeWindowKey } from '../constants/time-windows'; |
| 6 | + |
| 7 | +/** |
| 8 | + * Sync UI state to/from URL hash parameters. |
| 9 | + * Enables sharing links like: /map#mode=hexgrid&window=1h&lat=51.5&lon=-0.1&zoom=8 |
| 10 | + */ |
| 11 | +export function useUrlState() { |
| 12 | + const mode = useUIStore((s) => s.mode); |
| 13 | + const timeWindow = useUIStore((s) => s.timeWindow); |
| 14 | + const transportFilter = useUIStore((s) => s.transportFilter); |
| 15 | + const viewState = useUIStore((s) => s.viewState); |
| 16 | + const setMode = useUIStore((s) => s.setMode); |
| 17 | + const setTimeWindow = useUIStore((s) => s.setTimeWindow); |
| 18 | + const setTransportFilter = useUIStore((s) => s.setTransportFilter); |
| 19 | + const flyTo = useUIStore((s) => s.flyTo); |
| 20 | + |
| 21 | + const initialized = useRef(false); |
| 22 | + |
| 23 | + // On mount: read from URL hash |
| 24 | + useEffect(() => { |
| 25 | + if (initialized.current) return; |
| 26 | + initialized.current = true; |
| 27 | + |
| 28 | + const hash = window.location.hash.slice(1); |
| 29 | + if (!hash) return; |
| 30 | + |
| 31 | + const params = new URLSearchParams(hash); |
| 32 | + |
| 33 | + const m = params.get('mode'); |
| 34 | + if (m === 'hexgrid' || m === 'polygon') setMode(m); |
| 35 | + |
| 36 | + const w = params.get('window'); |
| 37 | + if (w && TIME_WINDOWS.some((tw) => tw.key === w)) setTimeWindow(w as TimeWindowKey); |
| 38 | + |
| 39 | + const t = params.get('transport'); |
| 40 | + if (t === 'all' || t === 'aircraft' || t === 'marine') setTransportFilter(t); |
| 41 | + |
| 42 | + const lat = params.get('lat'); |
| 43 | + const lon = params.get('lon'); |
| 44 | + const zoom = params.get('zoom'); |
| 45 | + if (lat && lon) { |
| 46 | + flyTo({ |
| 47 | + latitude: parseFloat(lat), |
| 48 | + longitude: parseFloat(lon), |
| 49 | + zoom: zoom ? parseFloat(zoom) : 6, |
| 50 | + }); |
| 51 | + } |
| 52 | + }, [setMode, setTimeWindow, setTransportFilter, flyTo]); |
| 53 | + |
| 54 | + // Debounced write to URL hash |
| 55 | + const writeTimer = useRef<ReturnType<typeof setTimeout>>(undefined); |
| 56 | + |
| 57 | + useEffect(() => { |
| 58 | + clearTimeout(writeTimer.current); |
| 59 | + writeTimer.current = setTimeout(() => { |
| 60 | + const params = new URLSearchParams(); |
| 61 | + params.set('mode', mode); |
| 62 | + params.set('window', timeWindow); |
| 63 | + if (transportFilter !== 'all') params.set('transport', transportFilter); |
| 64 | + params.set('lat', viewState.latitude.toFixed(2)); |
| 65 | + params.set('lon', viewState.longitude.toFixed(2)); |
| 66 | + params.set('zoom', viewState.zoom.toFixed(1)); |
| 67 | + |
| 68 | + const newHash = params.toString(); |
| 69 | + if (window.location.hash.slice(1) !== newHash) { |
| 70 | + history.replaceState(null, '', `#${newHash}`); |
| 71 | + } |
| 72 | + }, 1000); |
| 73 | + }, [mode, timeWindow, transportFilter, viewState]); |
| 74 | +} |
0 commit comments