2026-04-20 20:59:18 +00:00
|
|
|
import { useEffect, useRef, forwardRef, useImperativeHandle } from 'react'
|
feat: search, multi-stop routing, and route display
Full navigation UI with:
- Search bar with 150ms debounced autocomplete from /api/geocode
- Keyboard navigation (arrow keys, Enter, Escape)
- Exact match badge for verified address results
- Multi-stop list with drag-to-reorder (dnd-kit)
- 10-stop cap with disabled state
- Mode selector (drive/walk/bike)
- Valhalla route display with per-leg color polyline
- Maneuver list with instructions, distance, time remaining
- Click maneuver to fly map to that point
- Optimize stops button (3+ stops, uses /optimized_route)
- Responsive: side panel (desktop ≥768px), bottom sheet (mobile)
- Stop pins: green origin, red destination, blue intermediate
- Pin popup with remove button
- Geolocation permission requested on first route, not on load
- Error handling for unroutable pairs
- nginx proxy for /api/ and /valhalla/ endpoints
Dependencies added: zustand, @dnd-kit/core, @dnd-kit/sortable,
@dnd-kit/utilities
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-20 16:50:53 +00:00
|
|
|
import maplibregl from 'maplibre-gl'
|
|
|
|
|
import 'maplibre-gl/dist/maplibre-gl.css'
|
|
|
|
|
import { Protocol } from 'pmtiles'
|
|
|
|
|
import { layers, namedTheme } from 'protomaps-themes-base'
|
|
|
|
|
import { useStore } from '../store'
|
|
|
|
|
import { decodePolyline } from '../utils/decode'
|
|
|
|
|
|
|
|
|
|
const ROUTE_SOURCE = 'route-source'
|
|
|
|
|
const ROUTE_LAYER_PREFIX = 'route-layer-'
|
|
|
|
|
|
2026-04-20 20:59:18 +00:00
|
|
|
/** Build a full MapLibre style object for the given theme */
|
|
|
|
|
function buildStyle(themeName) {
|
|
|
|
|
return {
|
|
|
|
|
version: 8,
|
|
|
|
|
glyphs: 'https://protomaps.github.io/basemaps-assets/fonts/{fontstack}/{range}.pbf',
|
|
|
|
|
sprite: `https://protomaps.github.io/basemaps-assets/sprites/v4/${themeName}`,
|
|
|
|
|
sources: {
|
|
|
|
|
protomaps: {
|
|
|
|
|
type: 'vector',
|
|
|
|
|
url: 'pmtiles:///tiles/na.pmtiles',
|
|
|
|
|
attribution:
|
|
|
|
|
'<a href="https://protomaps.com">Protomaps</a> | <a href="https://openstreetmap.org">OSM</a>',
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
layers: layers('protomaps', namedTheme(themeName), { lang: 'en' }),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** SVG for ATAK-style chevron pointing up (will be rotated via CSS) */
|
|
|
|
|
const CHEVRON_SVG = `<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
|
|
|
|
<path d="M8 1 L14 13 L8 10 L2 13 Z" fill="var(--accent)" stroke="var(--bg-raised)" stroke-width="1.5" stroke-linejoin="round"/>
|
|
|
|
|
</svg>`
|
|
|
|
|
|
|
|
|
|
const MapView = forwardRef(function MapView(_, ref) {
|
feat: search, multi-stop routing, and route display
Full navigation UI with:
- Search bar with 150ms debounced autocomplete from /api/geocode
- Keyboard navigation (arrow keys, Enter, Escape)
- Exact match badge for verified address results
- Multi-stop list with drag-to-reorder (dnd-kit)
- 10-stop cap with disabled state
- Mode selector (drive/walk/bike)
- Valhalla route display with per-leg color polyline
- Maneuver list with instructions, distance, time remaining
- Click maneuver to fly map to that point
- Optimize stops button (3+ stops, uses /optimized_route)
- Responsive: side panel (desktop ≥768px), bottom sheet (mobile)
- Stop pins: green origin, red destination, blue intermediate
- Pin popup with remove button
- Geolocation permission requested on first route, not on load
- Error handling for unroutable pairs
- nginx proxy for /api/ and /valhalla/ endpoints
Dependencies added: zustand, @dnd-kit/core, @dnd-kit/sortable,
@dnd-kit/utilities
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-20 16:50:53 +00:00
|
|
|
const mapRef = useRef(null)
|
|
|
|
|
const mapInstance = useRef(null)
|
|
|
|
|
const markersRef = useRef([])
|
|
|
|
|
const popupRef = useRef(null)
|
2026-04-20 20:59:18 +00:00
|
|
|
const gpsMarkerRef = useRef(null)
|
|
|
|
|
const previewMarkerRef = useRef(null)
|
|
|
|
|
const watchIdRef = useRef(null)
|
|
|
|
|
const currentThemeRef = useRef('dark')
|
feat: search, multi-stop routing, and route display
Full navigation UI with:
- Search bar with 150ms debounced autocomplete from /api/geocode
- Keyboard navigation (arrow keys, Enter, Escape)
- Exact match badge for verified address results
- Multi-stop list with drag-to-reorder (dnd-kit)
- 10-stop cap with disabled state
- Mode selector (drive/walk/bike)
- Valhalla route display with per-leg color polyline
- Maneuver list with instructions, distance, time remaining
- Click maneuver to fly map to that point
- Optimize stops button (3+ stops, uses /optimized_route)
- Responsive: side panel (desktop ≥768px), bottom sheet (mobile)
- Stop pins: green origin, red destination, blue intermediate
- Pin popup with remove button
- Geolocation permission requested on first route, not on load
- Error handling for unroutable pairs
- nginx proxy for /api/ and /valhalla/ endpoints
Dependencies added: zustand, @dnd-kit/core, @dnd-kit/sortable,
@dnd-kit/utilities
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-20 16:50:53 +00:00
|
|
|
|
|
|
|
|
const stops = useStore((s) => s.stops)
|
|
|
|
|
const route = useStore((s) => s.route)
|
2026-04-20 20:59:18 +00:00
|
|
|
const theme = useStore((s) => s.theme)
|
|
|
|
|
const selectedPlace = useStore((s) => s.selectedPlace)
|
|
|
|
|
const gpsOrigin = useStore((s) => s.gpsOrigin)
|
|
|
|
|
const geoPermission = useStore((s) => s.geoPermission)
|
feat: search, multi-stop routing, and route display
Full navigation UI with:
- Search bar with 150ms debounced autocomplete from /api/geocode
- Keyboard navigation (arrow keys, Enter, Escape)
- Exact match badge for verified address results
- Multi-stop list with drag-to-reorder (dnd-kit)
- 10-stop cap with disabled state
- Mode selector (drive/walk/bike)
- Valhalla route display with per-leg color polyline
- Maneuver list with instructions, distance, time remaining
- Click maneuver to fly map to that point
- Optimize stops button (3+ stops, uses /optimized_route)
- Responsive: side panel (desktop ≥768px), bottom sheet (mobile)
- Stop pins: green origin, red destination, blue intermediate
- Pin popup with remove button
- Geolocation permission requested on first route, not on load
- Error handling for unroutable pairs
- nginx proxy for /api/ and /valhalla/ endpoints
Dependencies added: zustand, @dnd-kit/core, @dnd-kit/sortable,
@dnd-kit/utilities
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-20 16:50:53 +00:00
|
|
|
const setSheetState = useStore((s) => s.setSheetState)
|
|
|
|
|
|
|
|
|
|
// Expose map methods to parent
|
|
|
|
|
useImperativeHandle(ref, () => ({
|
|
|
|
|
flyTo(lat, lon, zoom = 14) {
|
|
|
|
|
mapInstance.current?.flyTo({ center: [lon, lat], zoom })
|
|
|
|
|
},
|
|
|
|
|
getMap() {
|
|
|
|
|
return mapInstance.current
|
|
|
|
|
},
|
|
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
// Initialize map
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const protocol = new Protocol()
|
|
|
|
|
maplibregl.addProtocol('pmtiles', protocol.tile)
|
|
|
|
|
|
2026-04-20 18:43:36 +00:00
|
|
|
const DEFAULT_CENTER = [-114.6066, 42.5736]
|
|
|
|
|
const DEFAULT_ZOOM = 10
|
|
|
|
|
|
2026-04-20 20:59:18 +00:00
|
|
|
const initialTheme = document.documentElement.getAttribute('data-theme') || 'dark'
|
|
|
|
|
currentThemeRef.current = initialTheme
|
|
|
|
|
|
feat: search, multi-stop routing, and route display
Full navigation UI with:
- Search bar with 150ms debounced autocomplete from /api/geocode
- Keyboard navigation (arrow keys, Enter, Escape)
- Exact match badge for verified address results
- Multi-stop list with drag-to-reorder (dnd-kit)
- 10-stop cap with disabled state
- Mode selector (drive/walk/bike)
- Valhalla route display with per-leg color polyline
- Maneuver list with instructions, distance, time remaining
- Click maneuver to fly map to that point
- Optimize stops button (3+ stops, uses /optimized_route)
- Responsive: side panel (desktop ≥768px), bottom sheet (mobile)
- Stop pins: green origin, red destination, blue intermediate
- Pin popup with remove button
- Geolocation permission requested on first route, not on load
- Error handling for unroutable pairs
- nginx proxy for /api/ and /valhalla/ endpoints
Dependencies added: zustand, @dnd-kit/core, @dnd-kit/sortable,
@dnd-kit/utilities
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-20 16:50:53 +00:00
|
|
|
const map = new maplibregl.Map({
|
|
|
|
|
container: mapRef.current,
|
2026-04-20 20:59:18 +00:00
|
|
|
style: buildStyle(initialTheme),
|
2026-04-20 18:43:36 +00:00
|
|
|
center: DEFAULT_CENTER,
|
|
|
|
|
zoom: DEFAULT_ZOOM,
|
feat: search, multi-stop routing, and route display
Full navigation UI with:
- Search bar with 150ms debounced autocomplete from /api/geocode
- Keyboard navigation (arrow keys, Enter, Escape)
- Exact match badge for verified address results
- Multi-stop list with drag-to-reorder (dnd-kit)
- 10-stop cap with disabled state
- Mode selector (drive/walk/bike)
- Valhalla route display with per-leg color polyline
- Maneuver list with instructions, distance, time remaining
- Click maneuver to fly map to that point
- Optimize stops button (3+ stops, uses /optimized_route)
- Responsive: side panel (desktop ≥768px), bottom sheet (mobile)
- Stop pins: green origin, red destination, blue intermediate
- Pin popup with remove button
- Geolocation permission requested on first route, not on load
- Error handling for unroutable pairs
- nginx proxy for /api/ and /valhalla/ endpoints
Dependencies added: zustand, @dnd-kit/core, @dnd-kit/sortable,
@dnd-kit/utilities
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-20 16:50:53 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
map.addControl(new maplibregl.NavigationControl(), 'top-right')
|
2026-04-20 18:43:36 +00:00
|
|
|
|
2026-04-20 20:59:18 +00:00
|
|
|
// GPS tracking — creates chevron or dot marker
|
2026-04-20 18:43:36 +00:00
|
|
|
if (navigator.geolocation) {
|
|
|
|
|
navigator.geolocation.getCurrentPosition(
|
|
|
|
|
(pos) => {
|
|
|
|
|
const { latitude, longitude } = pos.coords
|
|
|
|
|
if (useStore.getState().stops.length === 0) {
|
|
|
|
|
map.flyTo({ center: [longitude, latitude], zoom: 12, duration: 1500 })
|
|
|
|
|
}
|
|
|
|
|
useStore.getState().setUserLocation({ lat: latitude, lon: longitude })
|
|
|
|
|
useStore.getState().setGeoPermission('granted')
|
2026-04-20 20:59:18 +00:00
|
|
|
createOrUpdateGpsMarker(map, latitude, longitude, null)
|
2026-04-20 18:43:36 +00:00
|
|
|
},
|
|
|
|
|
() => {
|
|
|
|
|
useStore.getState().setGeoPermission('denied')
|
|
|
|
|
},
|
|
|
|
|
{ enableHighAccuracy: false, timeout: 8000, maximumAge: 300000 }
|
|
|
|
|
)
|
2026-04-20 20:59:18 +00:00
|
|
|
|
|
|
|
|
// Watch for heading changes
|
|
|
|
|
watchIdRef.current = navigator.geolocation.watchPosition(
|
|
|
|
|
(pos) => {
|
|
|
|
|
const { latitude, longitude, heading } = pos.coords
|
|
|
|
|
useStore.getState().setUserLocation({ lat: latitude, lon: longitude })
|
|
|
|
|
createOrUpdateGpsMarker(map, latitude, longitude, heading)
|
|
|
|
|
},
|
|
|
|
|
() => {},
|
|
|
|
|
{ enableHighAccuracy: true, maximumAge: 5000 }
|
|
|
|
|
)
|
2026-04-20 18:43:36 +00:00
|
|
|
}
|
feat: search, multi-stop routing, and route display
Full navigation UI with:
- Search bar with 150ms debounced autocomplete from /api/geocode
- Keyboard navigation (arrow keys, Enter, Escape)
- Exact match badge for verified address results
- Multi-stop list with drag-to-reorder (dnd-kit)
- 10-stop cap with disabled state
- Mode selector (drive/walk/bike)
- Valhalla route display with per-leg color polyline
- Maneuver list with instructions, distance, time remaining
- Click maneuver to fly map to that point
- Optimize stops button (3+ stops, uses /optimized_route)
- Responsive: side panel (desktop ≥768px), bottom sheet (mobile)
- Stop pins: green origin, red destination, blue intermediate
- Pin popup with remove button
- Geolocation permission requested on first route, not on load
- Error handling for unroutable pairs
- nginx proxy for /api/ and /valhalla/ endpoints
Dependencies added: zustand, @dnd-kit/core, @dnd-kit/sortable,
@dnd-kit/utilities
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-20 16:50:53 +00:00
|
|
|
|
|
|
|
|
map.on('click', () => {
|
2026-04-20 20:59:18 +00:00
|
|
|
if (window.innerWidth < 768) setSheetState('collapsed')
|
|
|
|
|
useStore.getState().clearSelectedPlace()
|
feat: search, multi-stop routing, and route display
Full navigation UI with:
- Search bar with 150ms debounced autocomplete from /api/geocode
- Keyboard navigation (arrow keys, Enter, Escape)
- Exact match badge for verified address results
- Multi-stop list with drag-to-reorder (dnd-kit)
- 10-stop cap with disabled state
- Mode selector (drive/walk/bike)
- Valhalla route display with per-leg color polyline
- Maneuver list with instructions, distance, time remaining
- Click maneuver to fly map to that point
- Optimize stops button (3+ stops, uses /optimized_route)
- Responsive: side panel (desktop ≥768px), bottom sheet (mobile)
- Stop pins: green origin, red destination, blue intermediate
- Pin popup with remove button
- Geolocation permission requested on first route, not on load
- Error handling for unroutable pairs
- nginx proxy for /api/ and /valhalla/ endpoints
Dependencies added: zustand, @dnd-kit/core, @dnd-kit/sortable,
@dnd-kit/utilities
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-20 16:50:53 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
map.on('load', () => {
|
|
|
|
|
map.addSource(ROUTE_SOURCE, {
|
|
|
|
|
type: 'geojson',
|
|
|
|
|
data: { type: 'FeatureCollection', features: [] },
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
mapInstance.current = map
|
|
|
|
|
|
|
|
|
|
return () => {
|
2026-04-20 20:59:18 +00:00
|
|
|
if (watchIdRef.current != null) navigator.geolocation.clearWatch(watchIdRef.current)
|
|
|
|
|
if (gpsMarkerRef.current) gpsMarkerRef.current.remove()
|
feat: search, multi-stop routing, and route display
Full navigation UI with:
- Search bar with 150ms debounced autocomplete from /api/geocode
- Keyboard navigation (arrow keys, Enter, Escape)
- Exact match badge for verified address results
- Multi-stop list with drag-to-reorder (dnd-kit)
- 10-stop cap with disabled state
- Mode selector (drive/walk/bike)
- Valhalla route display with per-leg color polyline
- Maneuver list with instructions, distance, time remaining
- Click maneuver to fly map to that point
- Optimize stops button (3+ stops, uses /optimized_route)
- Responsive: side panel (desktop ≥768px), bottom sheet (mobile)
- Stop pins: green origin, red destination, blue intermediate
- Pin popup with remove button
- Geolocation permission requested on first route, not on load
- Error handling for unroutable pairs
- nginx proxy for /api/ and /valhalla/ endpoints
Dependencies added: zustand, @dnd-kit/core, @dnd-kit/sortable,
@dnd-kit/utilities
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-20 16:50:53 +00:00
|
|
|
maplibregl.removeProtocol('pmtiles')
|
|
|
|
|
map.remove()
|
|
|
|
|
}
|
|
|
|
|
}, [setSheetState])
|
|
|
|
|
|
2026-04-20 20:59:18 +00:00
|
|
|
/** Create or update the GPS chevron/dot marker */
|
|
|
|
|
function createOrUpdateGpsMarker(map, lat, lon, heading) {
|
|
|
|
|
if (!gpsMarkerRef.current) {
|
|
|
|
|
const el = document.createElement('div')
|
|
|
|
|
if (heading != null && !isNaN(heading)) {
|
|
|
|
|
el.className = 'navi-chevron'
|
|
|
|
|
el.innerHTML = CHEVRON_SVG
|
|
|
|
|
el.style.transform = `rotate(${heading}deg)`
|
|
|
|
|
} else {
|
|
|
|
|
el.className = 'navi-gps-dot'
|
|
|
|
|
}
|
|
|
|
|
gpsMarkerRef.current = new maplibregl.Marker({ element: el })
|
|
|
|
|
.setLngLat([lon, lat])
|
|
|
|
|
.addTo(map)
|
|
|
|
|
} else {
|
|
|
|
|
gpsMarkerRef.current.setLngLat([lon, lat])
|
|
|
|
|
const el = gpsMarkerRef.current.getElement()
|
|
|
|
|
if (heading != null && !isNaN(heading)) {
|
|
|
|
|
if (!el.classList.contains('navi-chevron')) {
|
|
|
|
|
el.className = 'navi-chevron'
|
|
|
|
|
el.innerHTML = CHEVRON_SVG
|
|
|
|
|
}
|
|
|
|
|
el.style.transform = `rotate(${heading}deg)`
|
|
|
|
|
} else {
|
|
|
|
|
if (!el.classList.contains('navi-gps-dot')) {
|
|
|
|
|
el.className = 'navi-gps-dot'
|
|
|
|
|
el.innerHTML = ''
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Swap map theme when store.theme changes
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const map = mapInstance.current
|
|
|
|
|
if (!map || currentThemeRef.current === theme) return
|
|
|
|
|
|
|
|
|
|
currentThemeRef.current = theme
|
|
|
|
|
const center = map.getCenter()
|
|
|
|
|
const zoom = map.getZoom()
|
|
|
|
|
const bearing = map.getBearing()
|
|
|
|
|
const pitch = map.getPitch()
|
|
|
|
|
|
|
|
|
|
map.setStyle(buildStyle(theme), { diff: false })
|
|
|
|
|
|
|
|
|
|
// Re-add route source after style swap
|
|
|
|
|
map.once('style.load', () => {
|
|
|
|
|
map.addSource(ROUTE_SOURCE, {
|
|
|
|
|
type: 'geojson',
|
|
|
|
|
data: { type: 'FeatureCollection', features: [] },
|
|
|
|
|
})
|
|
|
|
|
// Restore view
|
|
|
|
|
map.jumpTo({ center, zoom, bearing, pitch })
|
|
|
|
|
// Re-render route if exists
|
|
|
|
|
const currentRoute = useStore.getState().route
|
|
|
|
|
if (currentRoute) updateRoute(map, currentRoute)
|
|
|
|
|
})
|
|
|
|
|
}, [theme])
|
|
|
|
|
|
|
|
|
|
// Preview pin for selected place
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const map = mapInstance.current
|
|
|
|
|
if (!map) return
|
|
|
|
|
|
|
|
|
|
// Remove old preview marker
|
|
|
|
|
if (previewMarkerRef.current) {
|
|
|
|
|
previewMarkerRef.current.remove()
|
|
|
|
|
previewMarkerRef.current = null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!selectedPlace) return
|
|
|
|
|
|
|
|
|
|
// Fly to selected place
|
|
|
|
|
map.flyTo({ center: [selectedPlace.lon, selectedPlace.lat], zoom: 14, duration: 800 })
|
|
|
|
|
|
|
|
|
|
// Create preview marker
|
|
|
|
|
const el = document.createElement('div')
|
|
|
|
|
el.className = 'navi-pin-preview'
|
|
|
|
|
previewMarkerRef.current = new maplibregl.Marker({ element: el })
|
|
|
|
|
.setLngLat([selectedPlace.lon, selectedPlace.lat])
|
|
|
|
|
.addTo(map)
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
if (previewMarkerRef.current) {
|
|
|
|
|
previewMarkerRef.current.remove()
|
|
|
|
|
previewMarkerRef.current = null
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}, [selectedPlace])
|
|
|
|
|
|
feat: search, multi-stop routing, and route display
Full navigation UI with:
- Search bar with 150ms debounced autocomplete from /api/geocode
- Keyboard navigation (arrow keys, Enter, Escape)
- Exact match badge for verified address results
- Multi-stop list with drag-to-reorder (dnd-kit)
- 10-stop cap with disabled state
- Mode selector (drive/walk/bike)
- Valhalla route display with per-leg color polyline
- Maneuver list with instructions, distance, time remaining
- Click maneuver to fly map to that point
- Optimize stops button (3+ stops, uses /optimized_route)
- Responsive: side panel (desktop ≥768px), bottom sheet (mobile)
- Stop pins: green origin, red destination, blue intermediate
- Pin popup with remove button
- Geolocation permission requested on first route, not on load
- Error handling for unroutable pairs
- nginx proxy for /api/ and /valhalla/ endpoints
Dependencies added: zustand, @dnd-kit/core, @dnd-kit/sortable,
@dnd-kit/utilities
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-20 16:50:53 +00:00
|
|
|
// Update route polyline when route changes
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const map = mapInstance.current
|
2026-04-20 20:59:18 +00:00
|
|
|
if (!map) return
|
|
|
|
|
if (!map.isStyleLoaded()) {
|
|
|
|
|
const handler = () => updateRoute(map, route)
|
|
|
|
|
map.once('load', handler)
|
|
|
|
|
return () => map.off('load', handler)
|
feat: search, multi-stop routing, and route display
Full navigation UI with:
- Search bar with 150ms debounced autocomplete from /api/geocode
- Keyboard navigation (arrow keys, Enter, Escape)
- Exact match badge for verified address results
- Multi-stop list with drag-to-reorder (dnd-kit)
- 10-stop cap with disabled state
- Mode selector (drive/walk/bike)
- Valhalla route display with per-leg color polyline
- Maneuver list with instructions, distance, time remaining
- Click maneuver to fly map to that point
- Optimize stops button (3+ stops, uses /optimized_route)
- Responsive: side panel (desktop ≥768px), bottom sheet (mobile)
- Stop pins: green origin, red destination, blue intermediate
- Pin popup with remove button
- Geolocation permission requested on first route, not on load
- Error handling for unroutable pairs
- nginx proxy for /api/ and /valhalla/ endpoints
Dependencies added: zustand, @dnd-kit/core, @dnd-kit/sortable,
@dnd-kit/utilities
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-20 16:50:53 +00:00
|
|
|
}
|
2026-04-20 20:59:18 +00:00
|
|
|
updateRoute(map, route)
|
feat: search, multi-stop routing, and route display
Full navigation UI with:
- Search bar with 150ms debounced autocomplete from /api/geocode
- Keyboard navigation (arrow keys, Enter, Escape)
- Exact match badge for verified address results
- Multi-stop list with drag-to-reorder (dnd-kit)
- 10-stop cap with disabled state
- Mode selector (drive/walk/bike)
- Valhalla route display with per-leg color polyline
- Maneuver list with instructions, distance, time remaining
- Click maneuver to fly map to that point
- Optimize stops button (3+ stops, uses /optimized_route)
- Responsive: side panel (desktop ≥768px), bottom sheet (mobile)
- Stop pins: green origin, red destination, blue intermediate
- Pin popup with remove button
- Geolocation permission requested on first route, not on load
- Error handling for unroutable pairs
- nginx proxy for /api/ and /valhalla/ endpoints
Dependencies added: zustand, @dnd-kit/core, @dnd-kit/sortable,
@dnd-kit/utilities
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-20 16:50:53 +00:00
|
|
|
}, [route])
|
|
|
|
|
|
2026-04-20 20:59:18 +00:00
|
|
|
function updateRoute(map, routeData) {
|
feat: search, multi-stop routing, and route display
Full navigation UI with:
- Search bar with 150ms debounced autocomplete from /api/geocode
- Keyboard navigation (arrow keys, Enter, Escape)
- Exact match badge for verified address results
- Multi-stop list with drag-to-reorder (dnd-kit)
- 10-stop cap with disabled state
- Mode selector (drive/walk/bike)
- Valhalla route display with per-leg color polyline
- Maneuver list with instructions, distance, time remaining
- Click maneuver to fly map to that point
- Optimize stops button (3+ stops, uses /optimized_route)
- Responsive: side panel (desktop ≥768px), bottom sheet (mobile)
- Stop pins: green origin, red destination, blue intermediate
- Pin popup with remove button
- Geolocation permission requested on first route, not on load
- Error handling for unroutable pairs
- nginx proxy for /api/ and /valhalla/ endpoints
Dependencies added: zustand, @dnd-kit/core, @dnd-kit/sortable,
@dnd-kit/utilities
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-20 16:50:53 +00:00
|
|
|
if (!map) return
|
|
|
|
|
|
|
|
|
|
// Remove old route layers
|
|
|
|
|
const style = map.getStyle()
|
|
|
|
|
if (style) {
|
|
|
|
|
for (const layer of style.layers) {
|
|
|
|
|
if (layer.id.startsWith(ROUTE_LAYER_PREFIX)) {
|
|
|
|
|
map.removeLayer(layer.id)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-20 20:59:18 +00:00
|
|
|
if (!routeData || !routeData.legs) {
|
feat: search, multi-stop routing, and route display
Full navigation UI with:
- Search bar with 150ms debounced autocomplete from /api/geocode
- Keyboard navigation (arrow keys, Enter, Escape)
- Exact match badge for verified address results
- Multi-stop list with drag-to-reorder (dnd-kit)
- 10-stop cap with disabled state
- Mode selector (drive/walk/bike)
- Valhalla route display with per-leg color polyline
- Maneuver list with instructions, distance, time remaining
- Click maneuver to fly map to that point
- Optimize stops button (3+ stops, uses /optimized_route)
- Responsive: side panel (desktop ≥768px), bottom sheet (mobile)
- Stop pins: green origin, red destination, blue intermediate
- Pin popup with remove button
- Geolocation permission requested on first route, not on load
- Error handling for unroutable pairs
- nginx proxy for /api/ and /valhalla/ endpoints
Dependencies added: zustand, @dnd-kit/core, @dnd-kit/sortable,
@dnd-kit/utilities
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-20 16:50:53 +00:00
|
|
|
if (map.getSource(ROUTE_SOURCE)) {
|
|
|
|
|
map.getSource(ROUTE_SOURCE).setData({ type: 'FeatureCollection', features: [] })
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const features = []
|
2026-04-20 20:59:18 +00:00
|
|
|
for (let i = 0; i < routeData.legs.length; i++) {
|
|
|
|
|
const leg = routeData.legs[i]
|
feat: search, multi-stop routing, and route display
Full navigation UI with:
- Search bar with 150ms debounced autocomplete from /api/geocode
- Keyboard navigation (arrow keys, Enter, Escape)
- Exact match badge for verified address results
- Multi-stop list with drag-to-reorder (dnd-kit)
- 10-stop cap with disabled state
- Mode selector (drive/walk/bike)
- Valhalla route display with per-leg color polyline
- Maneuver list with instructions, distance, time remaining
- Click maneuver to fly map to that point
- Optimize stops button (3+ stops, uses /optimized_route)
- Responsive: side panel (desktop ≥768px), bottom sheet (mobile)
- Stop pins: green origin, red destination, blue intermediate
- Pin popup with remove button
- Geolocation permission requested on first route, not on load
- Error handling for unroutable pairs
- nginx proxy for /api/ and /valhalla/ endpoints
Dependencies added: zustand, @dnd-kit/core, @dnd-kit/sortable,
@dnd-kit/utilities
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-20 16:50:53 +00:00
|
|
|
if (!leg.shape) continue
|
|
|
|
|
const coords = decodePolyline(leg.shape, 6)
|
|
|
|
|
features.push({
|
|
|
|
|
type: 'Feature',
|
|
|
|
|
properties: { legIndex: i },
|
|
|
|
|
geometry: { type: 'LineString', coordinates: coords },
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const source = map.getSource(ROUTE_SOURCE)
|
|
|
|
|
if (source) {
|
|
|
|
|
source.setData({ type: 'FeatureCollection', features })
|
|
|
|
|
} else {
|
|
|
|
|
map.addSource(ROUTE_SOURCE, {
|
|
|
|
|
type: 'geojson',
|
|
|
|
|
data: { type: 'FeatureCollection', features },
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-20 20:59:18 +00:00
|
|
|
// Use CSS variable for route color (read computed value)
|
|
|
|
|
const routeColor = getComputedStyle(document.documentElement).getPropertyValue('--route-line').trim()
|
|
|
|
|
|
feat: search, multi-stop routing, and route display
Full navigation UI with:
- Search bar with 150ms debounced autocomplete from /api/geocode
- Keyboard navigation (arrow keys, Enter, Escape)
- Exact match badge for verified address results
- Multi-stop list with drag-to-reorder (dnd-kit)
- 10-stop cap with disabled state
- Mode selector (drive/walk/bike)
- Valhalla route display with per-leg color polyline
- Maneuver list with instructions, distance, time remaining
- Click maneuver to fly map to that point
- Optimize stops button (3+ stops, uses /optimized_route)
- Responsive: side panel (desktop ≥768px), bottom sheet (mobile)
- Stop pins: green origin, red destination, blue intermediate
- Pin popup with remove button
- Geolocation permission requested on first route, not on load
- Error handling for unroutable pairs
- nginx proxy for /api/ and /valhalla/ endpoints
Dependencies added: zustand, @dnd-kit/core, @dnd-kit/sortable,
@dnd-kit/utilities
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-20 16:50:53 +00:00
|
|
|
for (let i = 0; i < features.length; i++) {
|
|
|
|
|
const layerId = `${ROUTE_LAYER_PREFIX}${i}`
|
|
|
|
|
if (!map.getLayer(layerId)) {
|
|
|
|
|
map.addLayer({
|
|
|
|
|
id: layerId,
|
|
|
|
|
type: 'line',
|
|
|
|
|
source: ROUTE_SOURCE,
|
|
|
|
|
filter: ['==', ['get', 'legIndex'], i],
|
2026-04-20 20:59:18 +00:00
|
|
|
layout: { 'line-join': 'round', 'line-cap': 'round' },
|
feat: search, multi-stop routing, and route display
Full navigation UI with:
- Search bar with 150ms debounced autocomplete from /api/geocode
- Keyboard navigation (arrow keys, Enter, Escape)
- Exact match badge for verified address results
- Multi-stop list with drag-to-reorder (dnd-kit)
- 10-stop cap with disabled state
- Mode selector (drive/walk/bike)
- Valhalla route display with per-leg color polyline
- Maneuver list with instructions, distance, time remaining
- Click maneuver to fly map to that point
- Optimize stops button (3+ stops, uses /optimized_route)
- Responsive: side panel (desktop ≥768px), bottom sheet (mobile)
- Stop pins: green origin, red destination, blue intermediate
- Pin popup with remove button
- Geolocation permission requested on first route, not on load
- Error handling for unroutable pairs
- nginx proxy for /api/ and /valhalla/ endpoints
Dependencies added: zustand, @dnd-kit/core, @dnd-kit/sortable,
@dnd-kit/utilities
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-20 16:50:53 +00:00
|
|
|
paint: {
|
2026-04-20 20:59:18 +00:00
|
|
|
'line-color': routeColor || '#7a9a6b',
|
feat: search, multi-stop routing, and route display
Full navigation UI with:
- Search bar with 150ms debounced autocomplete from /api/geocode
- Keyboard navigation (arrow keys, Enter, Escape)
- Exact match badge for verified address results
- Multi-stop list with drag-to-reorder (dnd-kit)
- 10-stop cap with disabled state
- Mode selector (drive/walk/bike)
- Valhalla route display with per-leg color polyline
- Maneuver list with instructions, distance, time remaining
- Click maneuver to fly map to that point
- Optimize stops button (3+ stops, uses /optimized_route)
- Responsive: side panel (desktop ≥768px), bottom sheet (mobile)
- Stop pins: green origin, red destination, blue intermediate
- Pin popup with remove button
- Geolocation permission requested on first route, not on load
- Error handling for unroutable pairs
- nginx proxy for /api/ and /valhalla/ endpoints
Dependencies added: zustand, @dnd-kit/core, @dnd-kit/sortable,
@dnd-kit/utilities
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-20 16:50:53 +00:00
|
|
|
'line-width': 5,
|
|
|
|
|
'line-opacity': 0.85,
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Fit bounds to route
|
|
|
|
|
if (features.length > 0) {
|
|
|
|
|
const allCoords = features.flatMap((f) => f.geometry.coordinates)
|
|
|
|
|
const bounds = allCoords.reduce(
|
|
|
|
|
(b, c) => b.extend(c),
|
|
|
|
|
new maplibregl.LngLatBounds(allCoords[0], allCoords[0])
|
|
|
|
|
)
|
2026-04-20 20:59:18 +00:00
|
|
|
const hasDetail = useStore.getState().selectedPlace != null
|
|
|
|
|
const leftPad = hasDetail ? 700 : 340
|
|
|
|
|
map.fitBounds(bounds, { padding: { top: 60, bottom: 60, left: leftPad, right: 60 } })
|
feat: search, multi-stop routing, and route display
Full navigation UI with:
- Search bar with 150ms debounced autocomplete from /api/geocode
- Keyboard navigation (arrow keys, Enter, Escape)
- Exact match badge for verified address results
- Multi-stop list with drag-to-reorder (dnd-kit)
- 10-stop cap with disabled state
- Mode selector (drive/walk/bike)
- Valhalla route display with per-leg color polyline
- Maneuver list with instructions, distance, time remaining
- Click maneuver to fly map to that point
- Optimize stops button (3+ stops, uses /optimized_route)
- Responsive: side panel (desktop ≥768px), bottom sheet (mobile)
- Stop pins: green origin, red destination, blue intermediate
- Pin popup with remove button
- Geolocation permission requested on first route, not on load
- Error handling for unroutable pairs
- nginx proxy for /api/ and /valhalla/ endpoints
Dependencies added: zustand, @dnd-kit/core, @dnd-kit/sortable,
@dnd-kit/utilities
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-20 16:50:53 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Update stop markers when stops change
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const map = mapInstance.current
|
|
|
|
|
if (!map) return
|
|
|
|
|
|
|
|
|
|
// Remove old markers
|
|
|
|
|
for (const m of markersRef.current) m.remove()
|
|
|
|
|
markersRef.current = []
|
|
|
|
|
if (popupRef.current) {
|
|
|
|
|
popupRef.current.remove()
|
|
|
|
|
popupRef.current = null
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-20 20:59:18 +00:00
|
|
|
const hasGpsOrigin = gpsOrigin && geoPermission === 'granted'
|
|
|
|
|
const indexOffset = hasGpsOrigin ? 1 : 0
|
|
|
|
|
|
feat: search, multi-stop routing, and route display
Full navigation UI with:
- Search bar with 150ms debounced autocomplete from /api/geocode
- Keyboard navigation (arrow keys, Enter, Escape)
- Exact match badge for verified address results
- Multi-stop list with drag-to-reorder (dnd-kit)
- 10-stop cap with disabled state
- Mode selector (drive/walk/bike)
- Valhalla route display with per-leg color polyline
- Maneuver list with instructions, distance, time remaining
- Click maneuver to fly map to that point
- Optimize stops button (3+ stops, uses /optimized_route)
- Responsive: side panel (desktop ≥768px), bottom sheet (mobile)
- Stop pins: green origin, red destination, blue intermediate
- Pin popup with remove button
- Geolocation permission requested on first route, not on load
- Error handling for unroutable pairs
- nginx proxy for /api/ and /valhalla/ endpoints
Dependencies added: zustand, @dnd-kit/core, @dnd-kit/sortable,
@dnd-kit/utilities
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-20 16:50:53 +00:00
|
|
|
stops.forEach((stop, i) => {
|
2026-04-20 20:59:18 +00:00
|
|
|
const displayIndex = i + indexOffset
|
|
|
|
|
const effectiveTotal = stops.length + indexOffset
|
|
|
|
|
|
|
|
|
|
let pinClass = 'navi-pin navi-pin--intermediate'
|
|
|
|
|
if (displayIndex === 0) pinClass = 'navi-pin navi-pin--origin'
|
|
|
|
|
else if (displayIndex === effectiveTotal - 1 && effectiveTotal > 1) pinClass = 'navi-pin navi-pin--destination'
|
feat: search, multi-stop routing, and route display
Full navigation UI with:
- Search bar with 150ms debounced autocomplete from /api/geocode
- Keyboard navigation (arrow keys, Enter, Escape)
- Exact match badge for verified address results
- Multi-stop list with drag-to-reorder (dnd-kit)
- 10-stop cap with disabled state
- Mode selector (drive/walk/bike)
- Valhalla route display with per-leg color polyline
- Maneuver list with instructions, distance, time remaining
- Click maneuver to fly map to that point
- Optimize stops button (3+ stops, uses /optimized_route)
- Responsive: side panel (desktop ≥768px), bottom sheet (mobile)
- Stop pins: green origin, red destination, blue intermediate
- Pin popup with remove button
- Geolocation permission requested on first route, not on load
- Error handling for unroutable pairs
- nginx proxy for /api/ and /valhalla/ endpoints
Dependencies added: zustand, @dnd-kit/core, @dnd-kit/sortable,
@dnd-kit/utilities
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-20 16:50:53 +00:00
|
|
|
|
2026-04-20 20:59:18 +00:00
|
|
|
const label = String.fromCharCode(65 + Math.min(displayIndex, 25))
|
feat: search, multi-stop routing, and route display
Full navigation UI with:
- Search bar with 150ms debounced autocomplete from /api/geocode
- Keyboard navigation (arrow keys, Enter, Escape)
- Exact match badge for verified address results
- Multi-stop list with drag-to-reorder (dnd-kit)
- 10-stop cap with disabled state
- Mode selector (drive/walk/bike)
- Valhalla route display with per-leg color polyline
- Maneuver list with instructions, distance, time remaining
- Click maneuver to fly map to that point
- Optimize stops button (3+ stops, uses /optimized_route)
- Responsive: side panel (desktop ≥768px), bottom sheet (mobile)
- Stop pins: green origin, red destination, blue intermediate
- Pin popup with remove button
- Geolocation permission requested on first route, not on load
- Error handling for unroutable pairs
- nginx proxy for /api/ and /valhalla/ endpoints
Dependencies added: zustand, @dnd-kit/core, @dnd-kit/sortable,
@dnd-kit/utilities
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-20 16:50:53 +00:00
|
|
|
|
|
|
|
|
const el = document.createElement('div')
|
2026-04-20 20:59:18 +00:00
|
|
|
el.className = pinClass
|
feat: search, multi-stop routing, and route display
Full navigation UI with:
- Search bar with 150ms debounced autocomplete from /api/geocode
- Keyboard navigation (arrow keys, Enter, Escape)
- Exact match badge for verified address results
- Multi-stop list with drag-to-reorder (dnd-kit)
- 10-stop cap with disabled state
- Mode selector (drive/walk/bike)
- Valhalla route display with per-leg color polyline
- Maneuver list with instructions, distance, time remaining
- Click maneuver to fly map to that point
- Optimize stops button (3+ stops, uses /optimized_route)
- Responsive: side panel (desktop ≥768px), bottom sheet (mobile)
- Stop pins: green origin, red destination, blue intermediate
- Pin popup with remove button
- Geolocation permission requested on first route, not on load
- Error handling for unroutable pairs
- nginx proxy for /api/ and /valhalla/ endpoints
Dependencies added: zustand, @dnd-kit/core, @dnd-kit/sortable,
@dnd-kit/utilities
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-20 16:50:53 +00:00
|
|
|
el.textContent = label
|
|
|
|
|
|
|
|
|
|
el.addEventListener('click', (e) => {
|
|
|
|
|
e.stopPropagation()
|
|
|
|
|
if (popupRef.current) popupRef.current.remove()
|
|
|
|
|
const popup = new maplibregl.Popup({ offset: 20, closeButton: true })
|
|
|
|
|
.setLngLat([stop.lon, stop.lat])
|
|
|
|
|
.setHTML(
|
2026-04-20 20:59:18 +00:00
|
|
|
`<div style="font-size:12px;max-width:200px">
|
feat: search, multi-stop routing, and route display
Full navigation UI with:
- Search bar with 150ms debounced autocomplete from /api/geocode
- Keyboard navigation (arrow keys, Enter, Escape)
- Exact match badge for verified address results
- Multi-stop list with drag-to-reorder (dnd-kit)
- 10-stop cap with disabled state
- Mode selector (drive/walk/bike)
- Valhalla route display with per-leg color polyline
- Maneuver list with instructions, distance, time remaining
- Click maneuver to fly map to that point
- Optimize stops button (3+ stops, uses /optimized_route)
- Responsive: side panel (desktop ≥768px), bottom sheet (mobile)
- Stop pins: green origin, red destination, blue intermediate
- Pin popup with remove button
- Geolocation permission requested on first route, not on load
- Error handling for unroutable pairs
- nginx proxy for /api/ and /valhalla/ endpoints
Dependencies added: zustand, @dnd-kit/core, @dnd-kit/sortable,
@dnd-kit/utilities
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-20 16:50:53 +00:00
|
|
|
<strong>${stop.name}</strong>
|
2026-04-20 20:59:18 +00:00
|
|
|
<br/><button id="remove-stop-${stop.id}" style="margin-top:4px;padding:2px 8px;background:var(--status-danger);border:none;border-radius:4px;color:white;cursor:pointer;font-size:11px">Remove</button>
|
feat: search, multi-stop routing, and route display
Full navigation UI with:
- Search bar with 150ms debounced autocomplete from /api/geocode
- Keyboard navigation (arrow keys, Enter, Escape)
- Exact match badge for verified address results
- Multi-stop list with drag-to-reorder (dnd-kit)
- 10-stop cap with disabled state
- Mode selector (drive/walk/bike)
- Valhalla route display with per-leg color polyline
- Maneuver list with instructions, distance, time remaining
- Click maneuver to fly map to that point
- Optimize stops button (3+ stops, uses /optimized_route)
- Responsive: side panel (desktop ≥768px), bottom sheet (mobile)
- Stop pins: green origin, red destination, blue intermediate
- Pin popup with remove button
- Geolocation permission requested on first route, not on load
- Error handling for unroutable pairs
- nginx proxy for /api/ and /valhalla/ endpoints
Dependencies added: zustand, @dnd-kit/core, @dnd-kit/sortable,
@dnd-kit/utilities
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-20 16:50:53 +00:00
|
|
|
</div>`
|
|
|
|
|
)
|
|
|
|
|
.addTo(map)
|
|
|
|
|
|
|
|
|
|
popup.getElement().querySelector(`#remove-stop-${stop.id}`)?.addEventListener('click', () => {
|
|
|
|
|
useStore.getState().removeStop(stop.id)
|
|
|
|
|
popup.remove()
|
|
|
|
|
})
|
|
|
|
|
popupRef.current = popup
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const marker = new maplibregl.Marker({ element: el })
|
|
|
|
|
.setLngLat([stop.lon, stop.lat])
|
|
|
|
|
.addTo(map)
|
|
|
|
|
|
|
|
|
|
markersRef.current.push(marker)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// If stops but no route yet, fit to stops
|
|
|
|
|
if (stops.length > 0 && !route) {
|
|
|
|
|
if (stops.length === 1) {
|
|
|
|
|
map.flyTo({ center: [stops[0].lon, stops[0].lat], zoom: 13 })
|
|
|
|
|
} else {
|
|
|
|
|
const bounds = stops.reduce(
|
|
|
|
|
(b, s) => b.extend([s.lon, s.lat]),
|
|
|
|
|
new maplibregl.LngLatBounds([stops[0].lon, stops[0].lat], [stops[0].lon, stops[0].lat])
|
|
|
|
|
)
|
|
|
|
|
map.fitBounds(bounds, { padding: { top: 60, bottom: 60, left: 340, right: 60 } })
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-20 20:59:18 +00:00
|
|
|
}, [stops, route, gpsOrigin, geoPermission])
|
feat: search, multi-stop routing, and route display
Full navigation UI with:
- Search bar with 150ms debounced autocomplete from /api/geocode
- Keyboard navigation (arrow keys, Enter, Escape)
- Exact match badge for verified address results
- Multi-stop list with drag-to-reorder (dnd-kit)
- 10-stop cap with disabled state
- Mode selector (drive/walk/bike)
- Valhalla route display with per-leg color polyline
- Maneuver list with instructions, distance, time remaining
- Click maneuver to fly map to that point
- Optimize stops button (3+ stops, uses /optimized_route)
- Responsive: side panel (desktop ≥768px), bottom sheet (mobile)
- Stop pins: green origin, red destination, blue intermediate
- Pin popup with remove button
- Geolocation permission requested on first route, not on load
- Error handling for unroutable pairs
- nginx proxy for /api/ and /valhalla/ endpoints
Dependencies added: zustand, @dnd-kit/core, @dnd-kit/sortable,
@dnd-kit/utilities
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-20 16:50:53 +00:00
|
|
|
|
|
|
|
|
return <div ref={mapRef} className="w-full h-full" />
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
export default MapView
|