mirror of
https://github.com/zvx-echo6/navi.git
synced 2026-05-20 14:44:51 +02:00
- Increase touch targets from 36px to 44px (meets accessibility guidelines) - Wrap Locate and Layers buttons in unified .map-controls-br container - Layer popover now opens LEFT of buttons (avoids collision with Locate) - Add hover and active states with theme-aware styling - Proper spacing for scale control below the cluster - Increased icon sizes from 18px to 20px - Mobile-responsive with proper max-height on layer popover Layout: [Locate] 44x44 [Layers] 44x44 ────────────── Scale: 0.5 mi Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
107 lines
3.3 KiB
JavaScript
107 lines
3.3 KiB
JavaScript
import { useEffect, useRef, useCallback } from 'react'
|
|
import { useStore } from './store'
|
|
import { useTheme } from './hooks/useTheme'
|
|
import { requestRoute, fetchAuthState } from './api'
|
|
import { decodePolyline } from './utils/decode'
|
|
import MapView from './components/MapView'
|
|
import Panel from './components/Panel'
|
|
|
|
import ContactModal from './components/ContactModal'
|
|
import LayerControl from './components/LayerControl'
|
|
import LocateButton from './components/LocateButton'
|
|
|
|
export default function App() {
|
|
const mapViewRef = useRef(null)
|
|
const routeDebounceRef = useRef(null)
|
|
|
|
// Initialize theme system
|
|
useTheme()
|
|
|
|
const stops = useStore((s) => s.stops)
|
|
const mode = useStore((s) => s.mode)
|
|
const route = useStore((s) => s.route)
|
|
const gpsOrigin = useStore((s) => s.gpsOrigin)
|
|
const geoPermission = useStore((s) => s.geoPermission)
|
|
const setRoute = useStore((s) => s.setRoute)
|
|
const setRouteLoading = useStore((s) => s.setRouteLoading)
|
|
const setRouteError = useStore((s) => s.setRouteError)
|
|
const clearRoute = useStore((s) => s.clearRoute)
|
|
const setAuth = useStore((s) => s.setAuth)
|
|
|
|
// Initialize auth state on app load (single fetch, no polling)
|
|
useEffect(() => {
|
|
fetchAuthState().then(setAuth)
|
|
}, [setAuth])
|
|
|
|
// Fetch route when stops, mode, gpsOrigin, or geoPermission change (debounced 500ms)
|
|
useEffect(() => {
|
|
if (routeDebounceRef.current) clearTimeout(routeDebounceRef.current)
|
|
|
|
routeDebounceRef.current = setTimeout(async () => {
|
|
const { userLocation } = useStore.getState()
|
|
|
|
let effective = stops.map((s) => ({ lat: s.lat, lon: s.lon }))
|
|
if (gpsOrigin && geoPermission === 'granted' && userLocation) {
|
|
effective = [{ lat: userLocation.lat, lon: userLocation.lon }, ...effective]
|
|
}
|
|
|
|
if (effective.length < 2) {
|
|
clearRoute()
|
|
return
|
|
}
|
|
|
|
setRouteLoading(true)
|
|
|
|
try {
|
|
const data = await requestRoute(effective, mode)
|
|
if (data.trip) {
|
|
setRoute(data.trip)
|
|
} else {
|
|
setRouteError('No route returned')
|
|
}
|
|
} catch (e) {
|
|
setRouteError(e.message || 'Route request failed')
|
|
} finally {
|
|
setRouteLoading(false)
|
|
}
|
|
}, 500)
|
|
|
|
return () => {
|
|
if (routeDebounceRef.current) clearTimeout(routeDebounceRef.current)
|
|
}
|
|
}, [stops, mode, gpsOrigin, geoPermission, clearRoute, setRoute, setRouteLoading, setRouteError])
|
|
|
|
// Handle maneuver click
|
|
const handleManeuverClick = useCallback(
|
|
(maneuver) => {
|
|
if (!route || !route.legs) return
|
|
|
|
const legIdx = maneuver._legIndex || 0
|
|
const leg = route.legs[legIdx]
|
|
if (!leg || !leg.shape) return
|
|
|
|
const coords = decodePolyline(leg.shape, 6)
|
|
const idx = maneuver.begin_shape_index
|
|
if (idx >= 0 && idx < coords.length) {
|
|
const [lng, lat] = coords[idx]
|
|
mapViewRef.current?.flyTo(lat, lng, 15)
|
|
}
|
|
},
|
|
[route]
|
|
)
|
|
|
|
return (
|
|
<div className="relative w-screen h-screen overflow-hidden" style={{ background: 'var(--bg-base)' }}>
|
|
<MapView ref={mapViewRef} />
|
|
<Panel onManeuverClick={handleManeuverClick} />
|
|
|
|
<ContactModal />
|
|
|
|
{/* Bottom-right map controls */}
|
|
<div className="map-controls-br">
|
|
<LocateButton mapRef={mapViewRef} />
|
|
<LayerControl mapRef={mapViewRef} />
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|