diff options
| author | Ariel Costas Guerrero <ariel@costas.dev> | 2026-03-13 12:51:01 +0100 |
|---|---|---|
| committer | Ariel Costas Guerrero <ariel@costas.dev> | 2026-03-13 12:51:11 +0100 |
| commit | b3b20bc1360ea67de6a1c837bb24c2b55541d3ac (patch) | |
| tree | 6dd2ca9e5750b490011fad4df94f497a9b32cf3b | |
| parent | c8d7ab720004ab4a10bf6feb98f7cf4ef450c1e0 (diff) | |
feat: enhance navigation and planner functionality in NavBar and map components
| -rw-r--r-- | src/frontend/app/components/layout/NavBar.tsx | 7 | ||||
| -rw-r--r-- | src/frontend/app/routes/map.tsx | 22 | ||||
| -rw-r--r-- | src/frontend/app/routes/planner.tsx | 90 |
3 files changed, 63 insertions, 56 deletions
diff --git a/src/frontend/app/components/layout/NavBar.tsx b/src/frontend/app/components/layout/NavBar.tsx index 5822ce7..e66c388 100644 --- a/src/frontend/app/components/layout/NavBar.tsx +++ b/src/frontend/app/components/layout/NavBar.tsx @@ -1,4 +1,4 @@ -import { Home, Map, Route } from "lucide-react"; +import { Home, Map, Navigation, Route } from "lucide-react"; import type { LngLatLike } from "maplibre-gl"; import { useTranslation } from "react-i18next"; import { Link, useLocation, useNavigate } from "react-router"; @@ -53,6 +53,11 @@ export default function NavBar({ orientation = "horizontal" }: NavBarProps) { }, }, { + name: t("navbar.planner", "Planificador"), + icon: Navigation, + path: "/planner", + }, + { name: t("navbar.routes", "Rutas"), icon: Route, path: "/routes", diff --git a/src/frontend/app/routes/map.tsx b/src/frontend/app/routes/map.tsx index 6d1fc9f..efc97e4 100644 --- a/src/frontend/app/routes/map.tsx +++ b/src/frontend/app/routes/map.tsx @@ -1,6 +1,6 @@ import { Check, MapPin, Navigation, Search, X } from "lucide-react"; import type { FilterSpecification } from "maplibre-gl"; -import { useEffect, useRef, useState, useMemo } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { Layer, @@ -16,7 +16,11 @@ import { } from "~/components/map/StopSummarySheet"; import { AppMap } from "~/components/shared/AppMap"; import { usePageTitle } from "~/contexts/PageTitleContext"; -import { reverseGeocode, searchPlaces, type PlannerSearchResult } from "~/data/PlannerApi"; +import { + reverseGeocode, + searchPlaces, + type PlannerSearchResult, +} from "~/data/PlannerApi"; import { usePlanner } from "~/hooks/usePlanner"; import StopDataProvider from "../data/StopDataProvider"; import "../tailwind-full.css"; @@ -170,15 +174,6 @@ function MapSearchBar({ mapRef }: MapSearchBarProps) { </div> </div> )} - - {/* Plan a trip – always visible */} - <button - onClick={() => navigate("/planner")} - className="flex items-center justify-center gap-2 px-4 py-2.5 rounded-xl bg-white/90 dark:bg-slate-900/80 backdrop-blur border border-slate-200 dark:border-slate-700 shadow-sm text-sm font-medium text-primary-600 dark:text-primary-400 hover:bg-primary-50 dark:hover:bg-primary-900/20 transition-colors" - > - <Navigation className="w-4 h-4 shrink-0" /> - {t("map.plan_trip", "Planificar ruta")} - </button> </div> </div> ); @@ -667,10 +662,7 @@ export default function StopMap() { {contextMenu && ( <> {/* Dismiss backdrop */} - <div - className="absolute inset-0 z-30" - onClick={closeContextMenu} - /> + <div className="absolute inset-0 z-30" onClick={closeContextMenu} /> {/* Context menu */} <div className="absolute z-40 min-w-[180px] rounded-xl bg-white dark:bg-slate-900 shadow-2xl border border-slate-200 dark:border-slate-700 overflow-hidden" diff --git a/src/frontend/app/routes/planner.tsx b/src/frontend/app/routes/planner.tsx index ff13225..0cd5efb 100644 --- a/src/frontend/app/routes/planner.tsx +++ b/src/frontend/app/routes/planner.tsx @@ -45,6 +45,14 @@ const haversineMeters = (a: [number, number], b: [number, number]) => { return 2 * R * Math.asin(Math.sqrt(h)); }; +const shouldSkipWalkLeg = (leg: Itinerary["legs"][number]): boolean => { + if (leg.mode !== "WALK") return false; + const durationMinutes = + (new Date(leg.endTime).getTime() - new Date(leg.startTime).getTime()) / + 60000; + return durationMinutes <= 2 || leg.distanceMeters < 50; +}; + const sumWalkMetrics = (legs: Itinerary["legs"]) => { let meters = 0; let minutes = 0; @@ -129,44 +137,44 @@ const ItinerarySummary = ({ </div> <div className="flex items-center gap-2 overflow-x-auto pb-2"> - {itinerary.legs.map((leg, idx) => { - const isWalk = leg.mode === "WALK"; - const legDurationMinutes = Math.max( - 1, - Math.round( - (new Date(leg.endTime).getTime() - - new Date(leg.startTime).getTime()) / - 60000 - ) - ); - - const isFirstBusLeg = - !isWalk && - itinerary.legs.findIndex((l) => l.mode !== "WALK") === idx; + {itinerary.legs + .filter((leg) => !shouldSkipWalkLeg(leg)) + .map((leg, idx) => { + const isWalk = leg.mode === "WALK"; + const legDurationMinutes = Math.max( + 1, + Math.round( + (new Date(leg.endTime).getTime() - + new Date(leg.startTime).getTime()) / + 60000 + ) + ); - return ( - <React.Fragment key={idx}> - {idx > 0 && <span className="text-muted/50">›</span>} - {isWalk ? ( - <div className="flex items-center gap-2 rounded-full bg-surface px-3 py-1.5 text-sm text-text whitespace-nowrap border border-border"> - <Footprints className="w-4 h-4 text-muted" /> - <span className="font-semibold"> - {formatDuration(legDurationMinutes, t)} - </span> - </div> - ) : ( - <div className="flex items-center gap-2"> - <RouteIcon - line={leg.routeShortName || leg.routeName || leg.mode || ""} - mode="pill" - colour={leg.routeColor || undefined} - textColour={leg.routeTextColor || undefined} - /> - </div> - )} - </React.Fragment> - ); - })} + return ( + <React.Fragment key={idx}> + {idx > 0 && <span className="text-muted/50">›</span>} + {isWalk ? ( + <div className="flex items-center gap-2 rounded-full bg-surface px-3 py-1.5 text-sm text-text whitespace-nowrap border border-border"> + <Footprints className="w-4 h-4 text-muted" /> + <span className="font-semibold"> + {formatDuration(legDurationMinutes, t)} + </span> + </div> + ) : ( + <div className="flex items-center gap-2"> + <RouteIcon + line={ + leg.routeShortName || leg.routeName || leg.mode || "" + } + mode="pill" + colour={leg.routeColor || undefined} + textColour={leg.routeTextColor || undefined} + /> + </div> + )} + </React.Fragment> + ); + })} </div> <div className="flex items-center justify-between text-sm text-muted mt-1"> @@ -284,6 +292,8 @@ const ItineraryDetail = ({ }, [itinerary]); // Get origin and destination coordinates + const visibleLegs = itinerary.legs.filter((leg) => !shouldSkipWalkLeg(leg)); + const origin = itinerary.legs[0]?.from; const destination = itinerary.legs[itinerary.legs.length - 1]?.to; @@ -495,7 +505,7 @@ const ItineraryDetail = ({ </h2> <div> - {itinerary.legs.map((leg, idx) => ( + {visibleLegs.map((leg, idx) => ( <div key={idx} className="flex gap-3"> <div className="flex flex-col items-center w-20 shrink-0"> {leg.mode === "WALK" ? ( @@ -513,7 +523,7 @@ const ItineraryDetail = ({ textColour={leg.routeTextColor || undefined} /> )} - {idx < itinerary.legs.length - 1 && ( + {idx < visibleLegs.length - 1 && ( <div className="w-0.5 flex-1 bg-gray-300 dark:bg-gray-600 my-1"></div> )} </div> @@ -574,7 +584,7 @@ const ItineraryDetail = ({ const currentLine = leg.routeShortName || leg.routeName; const previousLeg = - idx > 0 ? itinerary.legs[idx - 1] : null; + idx > 0 ? visibleLegs[idx - 1] : null; const previousLine = previousLeg?.mode !== "WALK" ? previousLeg?.routeShortName || |
