mirror of
https://github.com/zvx-echo6/navi.git
synced 2026-05-20 22:54:42 +02: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>
This commit is contained in:
parent
ce32014896
commit
e7b08a7dc9
16 changed files with 1364 additions and 44 deletions
89
src/api.js
Normal file
89
src/api.js
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
const GEOCODE_URL = '/api/geocode'
|
||||
const VALHALLA_URL = '/valhalla/route'
|
||||
const VALHALLA_OPTIMIZED_URL = '/valhalla/optimized_route'
|
||||
|
||||
/**
|
||||
* Search geocode API with abort support.
|
||||
* @param {string} query
|
||||
* @param {number} limit
|
||||
* @param {AbortSignal} signal
|
||||
* @returns {Promise<{query, results, count}>}
|
||||
*/
|
||||
export async function searchGeocode(query, limit = 6, signal) {
|
||||
const params = new URLSearchParams({ q: query, limit: String(limit) })
|
||||
const resp = await fetch(`${GEOCODE_URL}?${params}`, { signal, timeout: 5000 })
|
||||
if (!resp.ok) throw new Error(`Geocode error: ${resp.status}`)
|
||||
return resp.json()
|
||||
}
|
||||
|
||||
/**
|
||||
* Request a route from Valhalla.
|
||||
* @param {Array<{lat, lon}>} locations
|
||||
* @param {string} costing - 'auto' | 'pedestrian' | 'bicycle'
|
||||
* @returns {Promise<object>} Valhalla trip response
|
||||
*/
|
||||
export async function requestRoute(locations, costing = 'auto') {
|
||||
const body = {
|
||||
locations: locations.map((l) => ({ lat: l.lat, lon: l.lon })),
|
||||
costing,
|
||||
units: 'miles',
|
||||
directions_options: { units: 'miles' },
|
||||
}
|
||||
|
||||
const controller = new AbortController()
|
||||
const timeout = setTimeout(() => controller.abort(), 30000)
|
||||
|
||||
try {
|
||||
const resp = await fetch(VALHALLA_URL, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
signal: controller.signal,
|
||||
})
|
||||
|
||||
if (!resp.ok) {
|
||||
const errBody = await resp.json().catch(() => ({}))
|
||||
throw new Error(errBody.error || errBody.status_message || `Route error: ${resp.status}`)
|
||||
}
|
||||
|
||||
return resp.json()
|
||||
} finally {
|
||||
clearTimeout(timeout)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Request an optimized route from Valhalla.
|
||||
* @param {Array<{lat, lon}>} locations
|
||||
* @param {string} costing
|
||||
* @returns {Promise<object>} Valhalla optimized trip response
|
||||
*/
|
||||
export async function requestOptimizedRoute(locations, costing = 'auto') {
|
||||
const body = {
|
||||
locations: locations.map((l) => ({ lat: l.lat, lon: l.lon })),
|
||||
costing,
|
||||
units: 'miles',
|
||||
directions_options: { units: 'miles' },
|
||||
}
|
||||
|
||||
const controller = new AbortController()
|
||||
const timeout = setTimeout(() => controller.abort(), 30000)
|
||||
|
||||
try {
|
||||
const resp = await fetch(VALHALLA_OPTIMIZED_URL, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
signal: controller.signal,
|
||||
})
|
||||
|
||||
if (!resp.ok) {
|
||||
const errBody = await resp.json().catch(() => ({}))
|
||||
throw new Error(errBody.error || errBody.status_message || `Optimize error: ${resp.status}`)
|
||||
}
|
||||
|
||||
return resp.json()
|
||||
} finally {
|
||||
clearTimeout(timeout)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue