From 19a96cba5e42dc2c4730812bfa8d1def4b163dc0 Mon Sep 17 00:00:00 2001 From: Matt Date: Sat, 9 May 2026 03:37:05 +0000 Subject: [PATCH] feat: improve directions panel with route legend and place card below - Add route legend showing wilderness (dashed orange) vs road (solid blue) - Show place card below directions panel when clicking map during routing - Clean up error messages to be user-friendly (no offroute text) - Legend only appears when route has wilderness segments Co-Authored-By: Claude Opus 4.5 --- src/components/DirectionsPanel.jsx | 562 +++++++++++++++-------------- src/components/Panel.jsx | 21 +- 2 files changed, 316 insertions(+), 267 deletions(-) diff --git a/src/components/DirectionsPanel.jsx b/src/components/DirectionsPanel.jsx index d2f7db4..794cad6 100644 --- a/src/components/DirectionsPanel.jsx +++ b/src/components/DirectionsPanel.jsx @@ -1,263 +1,299 @@ -import { useEffect } from "react" -import { ArrowUpDown, Plus, X, Footprints, Bike, Car, Shield, AlertTriangle, Zap } from "lucide-react" -import { useStore } from "../store" -import LocationInput from "./LocationInput" -import ManeuverList from "./ManeuverList" - -const TRAVEL_MODES = [ - { id: "auto", label: "Drive", Icon: Car }, - { id: "foot", label: "Foot", Icon: Footprints }, - { id: "mtb", label: "MTB", Icon: Bike }, - { id: "atv", label: "ATV", Icon: Car }, - { id: "vehicle", label: "4x4", Icon: Car }, -] - -const BOUNDARY_MODES = [ - { id: "strict", label: "Strict", Icon: Shield, title: "Avoid barriers" }, - { id: "pragmatic", label: "Cross", Icon: AlertTriangle, title: "Cross with penalty" }, - { id: "emergency", label: "Ignore", Icon: Zap, title: "Ignore barriers" }, -] - -export default function DirectionsPanel({ onClose }) { - const routeStart = useStore((s) => s.routeStart) - const routeEnd = useStore((s) => s.routeEnd) - const routeMode = useStore((s) => s.routeMode) - const boundaryMode = useStore((s) => s.boundaryMode) - const routeResult = useStore((s) => s.routeResult) - const routeLoading = useStore((s) => s.routeLoading) - const routeError = useStore((s) => s.routeError) - const stops = useStore((s) => s.stops) - const userLocation = useStore((s) => s.userLocation) - const geoPermission = useStore((s) => s.geoPermission) - - const setRouteStart = useStore((s) => s.setRouteStart) - const setRouteEnd = useStore((s) => s.setRouteEnd) - const setRouteMode = useStore((s) => s.setRouteMode) - const setBoundaryMode = useStore((s) => s.setBoundaryMode) - const computeRoute = useStore((s) => s.computeRoute) - const clearRoute = useStore((s) => s.clearRoute) - const setDirectionsMode = useStore((s) => s.setDirectionsMode) - const addStop = useStore((s) => s.addStop) - const removeStop = useStore((s) => s.removeStop) - const reorderStops = useStore((s) => s.reorderStops) - - // Auto-fill origin with GPS if available and origin is empty - useEffect(() => { - if (!routeStart && geoPermission === "granted" && userLocation) { - setRouteStart({ - lat: userLocation.lat, - lon: userLocation.lon, - name: "Your location", - source: "gps", - }) - } - }, [routeStart, geoPermission, userLocation, setRouteStart]) - - // Auto-compute route when both endpoints are set - useEffect(() => { - if (routeStart && routeEnd) { - computeRoute() - } - }, [routeStart?.lat, routeStart?.lon, routeEnd?.lat, routeEnd?.lon]) - - const handleSwap = () => { - const tempStart = routeStart - const tempEnd = routeEnd - setRouteStart(tempEnd) - setRouteEnd(tempStart) - } - - const handleClose = () => { - clearRoute() - setDirectionsMode(false) - onClose?.() - } - - const handleAddStop = () => { - // Insert a stop between origin and destination - // For now, this adds to the stops array - // The UI will show intermediate stops - } - - // Multi-stop support: show intermediate stops from the stops array - const intermediateStops = stops.slice(1, -1) // Everything except first and last - - return ( -
- {/* Header */} -
- - Directions - - -
- - {/* Origin/Destination inputs with swap button */} -
- {/* Origin */} - - - {/* Swap button - positioned between inputs */} - - - {/* Intermediate stops (for multi-stop routes) */} - {intermediateStops.map((stop, idx) => ( -
- { - if (place) { - const newStops = [...stops] - newStops[idx + 1] = { ...newStops[idx + 1], ...place } - reorderStops(newStops) - } else { - removeStop(stop.id) - } - }} - placeholder="Stop" - icon="stop" - fieldId={`stop-${idx}`} - /> -
- ))} - - {/* Destination */} - - - {/* Add stop button */} - {routeStart && routeEnd && stops.length < 10 && ( - - )} -
- - {/* Travel mode selector */} -
- {TRAVEL_MODES.map((m) => { - const active = routeMode === m.id - return ( - - ) - })} -
- - {/* Boundary mode selector (only for non-auto modes) */} - {routeMode !== "auto" && ( -
- {BOUNDARY_MODES.map((m) => { - const active = boundaryMode === m.id - return ( - - ) - })} -
- )} - - {/* Loading indicator */} - {routeLoading && ( -
-
- - Finding route... - -
- )} - - {/* Error message */} - {routeError && ( -
- {routeError} -
- )} - - {/* Route summary and maneuvers */} - {routeResult && !routeLoading && ( -
- -
- )} - - {/* Hint when waiting for input */} - {!routeStart && !routeEnd && !routeLoading && ( -
-

- Enter addresses, paste coordinates, or click the map -

-
- )} -
- ) -} +import { useEffect } from "react" +import { ArrowUpDown, Plus, X, Footprints, Bike, Car, Shield, AlertTriangle, Zap } from "lucide-react" +import { useStore } from "../store" +import LocationInput from "./LocationInput" +import ManeuverList from "./ManeuverList" + +const TRAVEL_MODES = [ + { id: "auto", label: "Drive", Icon: Car }, + { id: "foot", label: "Foot", Icon: Footprints }, + { id: "mtb", label: "MTB", Icon: Bike }, + { id: "atv", label: "ATV", Icon: Car }, + { id: "vehicle", label: "4x4", Icon: Car }, +] + +const BOUNDARY_MODES = [ + { id: "strict", label: "Strict", Icon: Shield, title: "Avoid barriers" }, + { id: "pragmatic", label: "Cross", Icon: AlertTriangle, title: "Cross with penalty" }, + { id: "emergency", label: "Ignore", Icon: Zap, title: "Ignore barriers" }, +] + +export default function DirectionsPanel({ onClose }) { + const routeStart = useStore((s) => s.routeStart) + const routeEnd = useStore((s) => s.routeEnd) + const routeMode = useStore((s) => s.routeMode) + const boundaryMode = useStore((s) => s.boundaryMode) + const routeResult = useStore((s) => s.routeResult) + const routeLoading = useStore((s) => s.routeLoading) + const routeError = useStore((s) => s.routeError) + const stops = useStore((s) => s.stops) + const userLocation = useStore((s) => s.userLocation) + const geoPermission = useStore((s) => s.geoPermission) + + const setRouteStart = useStore((s) => s.setRouteStart) + const setRouteEnd = useStore((s) => s.setRouteEnd) + const setRouteMode = useStore((s) => s.setRouteMode) + const setBoundaryMode = useStore((s) => s.setBoundaryMode) + const computeRoute = useStore((s) => s.computeRoute) + const clearRoute = useStore((s) => s.clearRoute) + const setDirectionsMode = useStore((s) => s.setDirectionsMode) + const addStop = useStore((s) => s.addStop) + const removeStop = useStore((s) => s.removeStop) + const reorderStops = useStore((s) => s.reorderStops) + + // Auto-fill origin with GPS if available and origin is empty + useEffect(() => { + if (!routeStart && geoPermission === "granted" && userLocation) { + setRouteStart({ + lat: userLocation.lat, + lon: userLocation.lon, + name: "Your location", + source: "gps", + }) + } + }, [routeStart, geoPermission, userLocation, setRouteStart]) + + // Auto-compute route when both endpoints are set + useEffect(() => { + if (routeStart && routeEnd) { + computeRoute() + } + }, [routeStart?.lat, routeStart?.lon, routeEnd?.lat, routeEnd?.lon]) + + const handleSwap = () => { + const tempStart = routeStart + const tempEnd = routeEnd + setRouteStart(tempEnd) + setRouteEnd(tempStart) + } + + const handleClose = () => { + clearRoute() + setDirectionsMode(false) + onClose?.() + } + + const handleAddStop = () => { + // For now, show a message - multi-stop UI is complex + // TODO: Implement full multi-stop UI + } + + // Check if route has wilderness segments + const hasWilderness = routeResult?.summary?.wilderness_distance_km > 0 + + // Multi-stop support: show intermediate stops from the stops array + const intermediateStops = stops.slice(1, -1) + + return ( +
+ {/* Header */} +
+ + Directions + + +
+ + {/* Origin/Destination inputs with swap button */} +
+ {/* Origin */} + + + {/* Swap button - positioned between inputs */} + + + {/* Intermediate stops (for multi-stop routes) */} + {intermediateStops.map((stop, idx) => ( +
+ { + if (place) { + const newStops = [...stops] + newStops[idx + 1] = { ...newStops[idx + 1], ...place } + reorderStops(newStops) + } else { + removeStop(stop.id) + } + }} + placeholder="Stop" + icon="stop" + fieldId={`stop-${idx}`} + /> +
+ ))} + + {/* Destination */} + + + {/* Add stop button - only show when route exists */} + {routeStart && routeEnd && stops.length < 10 && ( + + )} +
+ + {/* Travel mode selector */} +
+ {TRAVEL_MODES.map((m) => { + const active = routeMode === m.id + return ( + + ) + })} +
+ + {/* Boundary mode selector (only for non-auto modes) */} + {routeMode !== "auto" && ( +
+ {BOUNDARY_MODES.map((m) => { + const active = boundaryMode === m.id + return ( + + ) + })} +
+ )} + + {/* Loading indicator */} + {routeLoading && ( +
+
+ + Finding route... + +
+ )} + + {/* Error message - friendly text, no "offroute" */} + {routeError && ( +
+ {routeError.includes("No route") || routeError.includes("not found") + ? "No route found. Try a different start point or mode." + : routeError.includes("entry point") + ? "No roads found nearby — try Foot mode for trails." + : routeError} +
+ )} + + {/* Route legend - only shown when route has wilderness segment */} + {routeResult && hasWilderness && !routeLoading && ( +
+
+ + + + Wilderness (on foot) +
+
+ + + + Road/Trail +
+
+ )} + + {/* Route summary and maneuvers */} + {routeResult && !routeLoading && ( +
+ +
+ )} + + {/* Hint when waiting for input */} + {!routeStart && !routeEnd && !routeLoading && ( +
+

+ Enter addresses, paste coordinates, or click the map +

+
+ )} +
+ ) +} diff --git a/src/components/Panel.jsx b/src/components/Panel.jsx index a708734..98e9f16 100644 --- a/src/components/Panel.jsx +++ b/src/components/Panel.jsx @@ -90,10 +90,23 @@ export default function Panel({ onClearRoute }) { const showEmptyState = panelState === 'IDLE' && !hasRoutePoints const routesContent = directionsMode ? ( - { - setDirectionsMode(false) - onClearRoute?.() - }} /> + <> + { + setDirectionsMode(false) + onClearRoute?.() + }} /> + {/* Show place card below directions when clicking map during routing */} + {selectedPlace && ( +
+ +
+ )} + ) : ( <>