From ece17875d4e454423f55f0623a456c0433ecd502 Mon Sep 17 00:00:00 2001
From: Ariel Costas Guerrero <ariel@costas.dev>
Date: Fri, 13 Mar 2026 17:12:12 +0100
Subject: feat: integrate geolocation functionality and enhance map
 interactions

- Added useGeolocation hook to manage user location and permissions.
- Updated PlannerOverlay to utilize geolocation for setting origin.
- Enhanced NavBar with a new planner route.
- Introduced context menu for map interactions to set routes from current location.
- Improved search functionality in the map with a dedicated search bar.
- Updated localization files with new strings for routing and search features.
---
 src/frontend/app/routes/planner.tsx | 637 +++++++++++++++++++++++-------------
 1 file changed, 404 insertions(+), 233 deletions(-)

(limited to 'src/frontend/app/routes/planner.tsx')

diff --git a/src/frontend/app/routes/planner.tsx b/src/frontend/app/routes/planner.tsx
index 4038ef7..c2fc648 100644
--- a/src/frontend/app/routes/planner.tsx
+++ b/src/frontend/app/routes/planner.tsx
@@ -1,4 +1,12 @@
-import { AlertTriangle, Coins, CreditCard, Footprints } from "lucide-react";
+import {
+  AlertTriangle,
+  Coins,
+  CreditCard,
+  Footprints,
+  LayoutGrid,
+  List,
+  Map as MapIcon,
+} from "lucide-react";
 import maplibregl from "maplibre-gl";
 import "maplibre-gl/dist/maplibre-gl.css";
 import React, { useEffect, useMemo, useRef, useState } from "react";
@@ -14,6 +22,7 @@ import { AppMap } from "~/components/shared/AppMap";
 import { APP_CONSTANTS } from "~/config/constants";
 import { useBackButton, usePageTitle } from "~/contexts/PageTitleContext";
 import { type Itinerary } from "~/data/PlannerApi";
+import { useGeolocation } from "~/hooks/useGeolocation";
 import { usePlanner } from "~/hooks/usePlanner";
 import "../tailwind-full.css";
 
@@ -45,6 +54,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 +146,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
-            )
-          );
+        {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
+              )
+            );
 
-          const isFirstBusLeg =
-            !isWalk &&
-            itinerary.legs.findIndex((l) => l.mode !== "WALK") === idx;
-
-          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 || ""}
+                      textColour={leg.routeTextColor || ""}
+                    />
+                  </div>
+                )}
+              </React.Fragment>
+            );
+          })}
       </div>
 
       <div className="flex items-center justify-between text-sm text-muted mt-1">
@@ -211,6 +228,40 @@ const ItineraryDetail = ({
   const [nextArrivals, setNextArrivals] = useState<
     Record<string, StopEstimatesResponse>
   >({});
+  const [selectedLegIndex, setSelectedLegIndex] = useState<number | null>(null);
+  const [layoutMode, setLayoutMode] = useState<"balanced" | "map" | "list">(
+    "balanced"
+  );
+
+  const focusLegOnMap = (leg: Itinerary["legs"][number]) => {
+    if (!mapRef.current) return;
+
+    const bounds = new maplibregl.LngLatBounds();
+    leg.geometry?.coordinates?.forEach((coord) =>
+      bounds.extend([coord[0], coord[1]])
+    );
+
+    if (leg.from?.lon && leg.from?.lat) {
+      bounds.extend([leg.from.lon, leg.from.lat]);
+    }
+
+    if (leg.to?.lon && leg.to?.lat) {
+      bounds.extend([leg.to.lon, leg.to.lat]);
+    }
+
+    if (!bounds.isEmpty()) {
+      mapRef.current.fitBounds(bounds, { padding: 90, duration: 800 });
+      return;
+    }
+
+    if (leg.from?.lon && leg.from?.lat) {
+      mapRef.current.flyTo({
+        center: [leg.from.lon, leg.from.lat],
+        zoom: 15,
+        duration: 800,
+      });
+    }
+  };
 
   const routeGeoJson = {
     type: "FeatureCollection",
@@ -283,10 +334,41 @@ const ItineraryDetail = ({
     return { type: "FeatureCollection", features };
   }, [itinerary]);
 
-  // Get origin and destination coordinates
   const origin = itinerary.legs[0]?.from;
   const destination = itinerary.legs[itinerary.legs.length - 1]?.to;
 
+  const mapHeightClass =
+    layoutMode === "map"
+      ? "h-[78%]"
+      : layoutMode === "list"
+        ? "h-[35%]"
+        : "h-[50%]";
+
+  const detailHeightClass =
+    layoutMode === "map"
+      ? "h-[22%]"
+      : layoutMode === "list"
+        ? "h-[65%]"
+        : "h-[50%]";
+
+  const layoutOptions = [
+    {
+      id: "map",
+      label: t("routes.layout_map", "Mapa"),
+      icon: MapIcon,
+    },
+    {
+      id: "balanced",
+      label: t("routes.layout_balanced", "Equilibrada"),
+      icon: LayoutGrid,
+    },
+    {
+      id: "list",
+      label: t("routes.layout_list", "Paradas"),
+      icon: List,
+    },
+  ] as const;
+
   useEffect(() => {
     if (!mapRef.current) return;
 
@@ -362,7 +444,7 @@ const ItineraryDetail = ({
   return (
     <div className="flex flex-col md:flex-row h-full">
       {/* Map Section */}
-      <div className="relative h-2/3 md:h-full md:flex-1">
+      <div className={`${mapHeightClass} relative md:h-full md:flex-1`}>
         <AppMap
           ref={mapRef}
           initialViewState={{
@@ -465,7 +547,7 @@ const ItineraryDetail = ({
               ]}
               layout={{
                 "text-field": ["get", "index"],
-                "text-font": ["Open Sans Bold", "Arial Unicode MS Bold"],
+                "text-font": ["Noto Sans Bold"],
                 "text-size": [
                   "interpolate",
                   ["linear"],
@@ -485,204 +567,303 @@ const ItineraryDetail = ({
             />
           </Source>
         </AppMap>
+
+        <div className="absolute top-3 left-3 z-10 flex items-center gap-1 rounded-full border border-border bg-background/90 p-1 shadow-sm backdrop-blur">
+          {layoutOptions.map((option) => {
+            const Icon = option.icon;
+            const isActive = layoutMode === option.id;
+            return (
+              <button
+                key={option.id}
+                type="button"
+                onClick={() => setLayoutMode(option.id)}
+                className={`h-8 w-8 rounded-full flex items-center justify-center transition-colors ${
+                  isActive
+                    ? "bg-primary text-white"
+                    : "text-muted hover:text-text"
+                }`}
+                aria-label={option.label}
+                title={option.label}
+              >
+                <Icon size={16} />
+              </button>
+            );
+          })}
+        </div>
       </div>
 
       {/* Details Panel */}
-      <div className="h-1/3 md:h-full md:w-96 lg:w-lg overflow-y-auto bg-white dark:bg-slate-900 border-t md:border-t-0 md:border-l border-slate-200 dark:border-slate-700">
+      <div
+        className={`${detailHeightClass} md:h-full md:w-96 lg:w-lg overflow-y-auto bg-white dark:bg-slate-900 border-t md:border-t-0 md:border-l border-slate-200 dark:border-slate-700`}
+      >
         <div className="px-4 py-4">
           <h2 className="text-xl font-bold mb-4 text-slate-900 dark:text-slate-100">
             {t("planner.itinerary_details")}
           </h2>
 
           <div>
-            {itinerary.legs.map((leg, idx) => (
-              <div key={idx} className="flex gap-3">
-                <div className="flex flex-col items-center w-20 shrink-0">
-                  {leg.mode === "WALK" ? (
-                    <div
-                      className="w-8 h-8 rounded-full flex items-center justify-center text-xs font-bold shadow-sm"
-                      style={{ backgroundColor: "#e5e7eb", color: "#374151" }}
-                    >
-                      <Footprints className="w-4 h-4" />
-                    </div>
-                  ) : (
-                    <RouteIcon
-                      line={leg.routeShortName || leg.routeName || ""}
-                      mode="rounded"
-                      colour={leg.routeColor || undefined}
-                      textColour={leg.routeTextColor || undefined}
-                    />
-                  )}
-                  {idx < itinerary.legs.length - 1 && (
-                    <div className="w-0.5 flex-1 bg-gray-300 dark:bg-gray-600 my-1"></div>
-                  )}
-                </div>
-                <div className="flex-1 pb-4">
-                  <div className="font-bold flex items-center gap-2">
+            {itinerary.legs.map((leg, idx) => {
+              const arrivalsForLeg =
+                leg.mode !== "WALK" && leg.from?.stopId && leg.to?.stopId
+                  ? (
+                      nextArrivals[`${leg.from.stopId}::${leg.to.stopId}`]
+                        ?.arrivals ?? []
+                    )
+                      .map((arrival) => ({
+                        arrival,
+                        minutes: arrival.estimate.minutes,
+                        delay: arrival.delay,
+                      }))
+                      .slice(0, 4)
+                  : [];
+
+              const legDestinationLabel = (() => {
+                if (leg.mode !== "WALK") {
+                  return (
+                    leg.to?.name || t("planner.unknown_stop", "Unknown stop")
+                  );
+                }
+
+                const enteredDest = userDestination?.name || "";
+                const finalDest =
+                  enteredDest ||
+                  itinerary.legs[itinerary.legs.length - 1]?.to?.name ||
+                  "";
+                const raw = leg.to?.name || finalDest || "";
+                const cleaned = raw.trim();
+                const placeholder = cleaned.toLowerCase();
+
+                if (
+                  placeholder === "destination" ||
+                  placeholder === "destino" ||
+                  placeholder === "destinación" ||
+                  placeholder === "destinatario"
+                ) {
+                  return enteredDest || finalDest;
+                }
+
+                return cleaned || finalDest;
+              })();
+
+              return (
+                <div key={idx} className="flex gap-3 mb-3">
+                  <div className="flex flex-col items-center w-12 shrink-0 pt-1">
                     {leg.mode === "WALK" ? (
-                      t("planner.walk")
-                    ) : (
-                      <div className="flex flex-col">
-                        <span className="text-[10px] uppercase text-muted font-bold leading-none mb-1">
-                          {t("planner.direction")}
-                        </span>
-                        <span className="leading-tight">
-                          {leg.headsign ||
-                            leg.routeLongName ||
-                            leg.routeName ||
-                            ""}
-                        </span>
+                      <div
+                        className="w-8 h-8 rounded-full flex items-center justify-center text-xs font-bold shadow-sm"
+                        style={{ backgroundColor: "#e5e7eb", color: "#374151" }}
+                      >
+                        <Footprints className="w-4 h-4" />
                       </div>
+                    ) : (
+                      <RouteIcon
+                        line={leg.routeShortName || leg.routeName || ""}
+                        mode="rounded"
+                        colour={leg.routeColor || ""}
+                        textColour={leg.routeTextColor || ""}
+                      />
                     )}
-                  </div>
-                  <div className="text-sm text-gray-600 dark:text-gray-400 flex flex-wrap gap-x-3 gap-y-1 mt-1">
-                    <span>
-                      {new Date(leg.startTime).toLocaleTimeString([], {
-                        hour: "2-digit",
-                        minute: "2-digit",
-                        timeZone: "Europe/Madrid",
-                      })}{" "}
-                    </span>
-                    <span>•</span>
-                    <span>
-                      {formatDuration(
-                        Math.round(
-                          (new Date(leg.endTime).getTime() -
-                            new Date(leg.startTime).getTime()) /
-                            60000
-                        ),
-                        t
-                      )}
-                    </span>
-                    <span>•</span>
-                    <span>{formatDistance(leg.distanceMeters)}</span>
-                    {leg.agencyName && (
-                      <>
-                        <span>•</span>
-                        <span className="italic">{leg.agencyName}</span>
-                      </>
+                    {idx < itinerary.legs.length - 1 && (
+                      <div className="w-0.5 flex-1 bg-gray-300 dark:bg-gray-600 my-1 min-h-6"></div>
                     )}
                   </div>
-                  {leg.mode !== "WALK" &&
-                    leg.from?.stopId &&
-                    leg.to?.stopId &&
-                    nextArrivals[`${leg.from.stopId}::${leg.to.stopId}`] && (
-                      <div className="mt-2 text-xs text-gray-600 dark:text-gray-400">
-                        <div className="font-semibold mb-1">
-                          {t("planner.next_arrivals", "Next arrivals")}:
+                  <button
+                    type="button"
+                    onClick={() => {
+                      setSelectedLegIndex(idx);
+                      focusLegOnMap(leg);
+                    }}
+                    className={`flex-1 rounded-xl border p-3 text-left transition-colors ${
+                      selectedLegIndex === idx
+                        ? "border-primary bg-primary/5"
+                        : "border-border bg-surface hover:border-primary/50"
+                    }`}
+                  >
+                    <div className="font-bold flex items-center gap-2">
+                      {leg.mode === "WALK" ? (
+                        t("planner.walk")
+                      ) : (
+                        <div className="flex flex-col">
+                          <span className="text-[10px] uppercase text-muted font-bold leading-none mb-1">
+                            {t("planner.direction")}
+                          </span>
+                          <span className="leading-tight">
+                            {leg.headsign ||
+                              leg.routeLongName ||
+                              leg.routeName ||
+                              ""}
+                          </span>
                         </div>
-                        {nextArrivals[
-                          `${leg.from.stopId}::${leg.to.stopId}`
-                        ].arrivals
-                          .slice(0, 3)
-                          .map((arrival, i) => (
-                            <div
-                              key={`${arrival.tripId}-${i}`}
-                              className="flex items-center gap-2 py-0.5"
-                            >
-                              <span className="font-semibold text-primary-600 dark:text-primary-400">
-                                {formatDuration(arrival.estimate.minutes, t)}
-                              </span>
-                              {arrival.estimate.precision !== "scheduled" && (
-                                <span className="text-green-600 dark:text-green-400">
-                                  🟢
-                                </span>
-                              )}
-                              {arrival.delay?.minutes !== undefined &&
-                                arrival.delay.minutes !== 0 && (
+                      )}
+                    </div>
+                    <div className="text-sm text-gray-600 dark:text-gray-400 flex flex-wrap gap-x-3 gap-y-1 mt-1">
+                      <span>
+                        {new Date(leg.startTime).toLocaleTimeString([], {
+                          hour: "2-digit",
+                          minute: "2-digit",
+                          timeZone: "Europe/Madrid",
+                        })}{" "}
+                      </span>
+                      <span>•</span>
+                      <span>
+                        {formatDuration(
+                          Math.round(
+                            (new Date(leg.endTime).getTime() -
+                              new Date(leg.startTime).getTime()) /
+                              60000
+                          ),
+                          t
+                        )}
+                      </span>
+                      <span>•</span>
+                      <span>{formatDistance(leg.distanceMeters)}</span>
+                      {leg.agencyName && (
+                        <>
+                          <span>•</span>
+                          <span className="italic">{leg.agencyName}</span>
+                        </>
+                      )}
+                    </div>
+                    {leg.mode !== "WALK" && arrivalsForLeg.length > 0 && (
+                      <div className="mt-2">
+                        <div className="text-[10px] uppercase tracking-wide text-muted mb-1">
+                          {t("planner.next_arrivals", "Next arrivals")}
+                        </div>
+                        <div className="flex items-center justify-between gap-2 rounded-lg border border-green-500/30 bg-green-500/10 px-2.5 py-2">
+                          <span className="text-[11px] font-semibold uppercase tracking-wide text-green-700 dark:text-green-300">
+                            {t("planner.next_arrival", "Next")}
+                          </span>
+                          <span className="inline-flex min-w-16 items-center justify-center rounded-xl bg-green-600 px-3 py-1.5 text-base font-bold leading-none text-white">
+                            {arrivalsForLeg[0].minutes}′
+                            {arrivalsForLeg[0].delay?.minutes
+                              ? arrivalsForLeg[0].delay.minutes > 0
+                                ? ` (R${Math.abs(arrivalsForLeg[0].delay.minutes)})`
+                                : ` (A${Math.abs(arrivalsForLeg[0].delay.minutes)})`
+                              : ""}
+                          </span>
+                        </div>
+
+                        {arrivalsForLeg.length > 1 && (
+                          <div className="mt-2 flex flex-wrap justify-end gap-1">
+                            {arrivalsForLeg
+                              .slice(1)
+                              .map(
+                                ({ arrival, minutes, delay }, arrivalIdx) => (
                                   <span
-                                    className={
-                                      arrival.delay.minutes > 0
-                                        ? "text-red-500"
-                                        : "text-green-500"
-                                    }
+                                    key={`${arrival.tripId}-${arrivalIdx}`}
+                                    className="text-[11px] px-2 py-0.5 bg-primary/10 text-primary rounded"
                                   >
-                                    {arrival.delay.minutes > 0
-                                      ? `+${arrival.delay.minutes}′`
-                                      : `${arrival.delay.minutes}′`}
+                                    {minutes}′
+                                    {delay?.minutes
+                                      ? delay.minutes > 0
+                                        ? ` (R${Math.abs(delay.minutes)})`
+                                        : ` (A${Math.abs(delay.minutes)})`
+                                      : ""}
                                   </span>
-                                )}
-                            </div>
-                          ))}
+                                )
+                              )}
+                          </div>
+                        )}
                       </div>
                     )}
-                  <div className="text-sm mt-1">
-                    {leg.mode === "WALK" ? (
-                      <span>
-                        {t("planner.walk_to", {
-                          distance: Math.round(leg.distanceMeters) + "m",
-                          destination: (() => {
-                            const enteredDest = userDestination?.name || "";
-                            const finalDest =
-                              enteredDest ||
-                              itinerary.legs[itinerary.legs.length - 1]?.to
-                                ?.name ||
-                              "";
-                            const raw = leg.to?.name || finalDest || "";
-                            const cleaned = raw.trim();
-                            const placeholder = cleaned.toLowerCase();
-                            // If OTP provided a generic placeholder, use the user's entered destination
-                            if (
-                              placeholder === "destination" ||
-                              placeholder === "destino" ||
-                              placeholder === "destinación" ||
-                              placeholder === "destinatario"
-                            ) {
-                              return enteredDest || finalDest;
-                            }
-                            return cleaned || finalDest;
-                          })(),
-                        })}
-                      </span>
-                    ) : (
-                      <>
+                    <div className="text-sm mt-2">
+                      {leg.mode === "WALK" ? (
                         <span>
-                          {t("planner.from_to", {
-                            from: leg.from?.name,
-                            to: leg.to?.name,
+                          {t("planner.walk_to", {
+                            distance: Math.round(leg.distanceMeters) + "m",
+                            destination: legDestinationLabel,
                           })}
                         </span>
-                        {leg.intermediateStops &&
-                          leg.intermediateStops.length > 0 && (
-                            <details className="mt-2">
-                              <summary className="text-xs text-gray-600 dark:text-gray-400 cursor-pointer hover:text-gray-800 dark:hover:text-gray-200">
-                                {leg.intermediateStops.length}{" "}
-                                {leg.intermediateStops.length === 1
-                                  ? "stop"
-                                  : "stops"}
-                              </summary>
-                              <ul className="mt-1 ml-4 text-xs text-gray-500 dark:text-gray-400 space-y-0.5">
-                                {leg.intermediateStops.map((stop, idx) => (
-                                  <li key={idx}>• {stop.name}</li>
-                                ))}
-                              </ul>
-                            </details>
-                          )}
-                        {(() => {
-                          const municipality = getUrbanMunicipalityWarning(leg);
-                          if (!municipality) return null;
-                          return (
-                            <div className="mt-2 flex items-start gap-2 rounded-md bg-yellow-50 dark:bg-yellow-900/30 border border-yellow-300 dark:border-yellow-700 px-3 py-2 text-xs text-yellow-800 dark:text-yellow-200">
-                              <AlertTriangle className="w-4 h-4 shrink-0 mt-0.5 text-yellow-600 dark:text-yellow-400" />
-                              <div>
-                                <div className="font-semibold">
-                                  {t("planner.urban_traffic_warning")}
-                                </div>
-                                <div>
-                                  {t("planner.urban_traffic_warning_desc", {
-                                    municipality,
+                      ) : (
+                        <>
+                          <span>
+                            {t("planner.from_to", {
+                              from: leg.from?.name,
+                              to: leg.to?.name,
+                            })}
+                          </span>
+
+                          {leg.intermediateStops &&
+                            leg.intermediateStops.length > 0 && (
+                              <details className="mt-2">
+                                <summary className="text-xs text-gray-600 dark:text-gray-400 cursor-pointer hover:text-gray-800 dark:hover:text-gray-200">
+                                  {t("planner.intermediate_stops", {
+                                    count: leg.intermediateStops.length,
                                   })}
+                                </summary>
+                                <ul className="mt-1 text-xs space-y-0.5">
+                                  {/* Boarding stop */}
+                                  <li className="flex items-center gap-1.5 py-0.5 px-1.5 rounded bg-primary/8 font-semibold text-primary">
+                                    <span className="w-1.5 h-1.5 rounded-full bg-primary inline-block shrink-0" />
+                                    <span className="flex-1">
+                                      {leg.from?.name}
+                                    </span>
+                                    {leg.from?.stopCode && (
+                                      <span className="text-[10px] text-primary/60 shrink-0">
+                                        {leg.from.stopCode}
+                                      </span>
+                                    )}
+                                  </li>
+                                  {/* Intermediate stops */}
+                                  {leg.intermediateStops.map((stop, sIdx) => (
+                                    <li
+                                      key={sIdx}
+                                      className="flex items-center gap-1.5 py-0.5 px-1.5 text-gray-500 dark:text-gray-400"
+                                    >
+                                      <span className="w-1 h-1 rounded-full bg-gray-400 dark:bg-gray-500 inline-block shrink-0 ml-0.5" />
+                                      <span className="flex-1">
+                                        {stop.name}
+                                      </span>
+                                      {stop.stopCode && (
+                                        <span className="text-[10px] text-gray-400 dark:text-gray-500 shrink-0">
+                                          {stop.stopCode}
+                                        </span>
+                                      )}
+                                    </li>
+                                  ))}
+                                  {/* Alighting stop */}
+                                  <li className="flex items-center gap-1.5 py-0.5 px-1.5 rounded bg-primary/8 font-semibold text-primary">
+                                    <span className="w-1.5 h-1.5 rounded-full bg-primary inline-block shrink-0" />
+                                    <span className="flex-1">
+                                      {leg.to?.name}
+                                    </span>
+                                    {leg.to?.stopCode && (
+                                      <span className="text-[10px] text-primary/60 shrink-0">
+                                        {leg.to.stopCode}
+                                      </span>
+                                    )}
+                                  </li>
+                                </ul>
+                              </details>
+                            )}
+
+                          {(() => {
+                            const municipality =
+                              getUrbanMunicipalityWarning(leg);
+                            if (!municipality) return null;
+                            return (
+                              <div className="mt-2 flex items-start gap-2 rounded-md bg-yellow-50 dark:bg-yellow-900/30 border border-yellow-300 dark:border-yellow-700 px-3 py-2 text-xs text-yellow-800 dark:text-yellow-200">
+                                <AlertTriangle className="w-4 h-4 shrink-0 mt-0.5 text-yellow-600 dark:text-yellow-400" />
+                                <div>
+                                  <div className="font-semibold">
+                                    {t("planner.urban_traffic_warning")}
+                                  </div>
+                                  <div>
+                                    {t("planner.urban_traffic_warning_desc", {
+                                      municipality,
+                                    })}
+                                  </div>
                                 </div>
                               </div>
-                            </div>
-                          );
-                        })()}
-                      </>
-                    )}
-                  </div>
+                            );
+                          })()}
+                        </>
+                      )}
+                    </div>
+                  </button>
                 </div>
-              </div>
-            ))}
+              );
+            })}
           </div>
         </div>
       </div>
@@ -707,6 +888,7 @@ export default function PlannerPage() {
     setOrigin,
     setDestination,
   } = usePlanner();
+  const { userLocation } = useGeolocation();
   const [selectedItinerary, setSelectedItinerary] = useState<Itinerary | null>(
     null
   );
@@ -802,27 +984,16 @@ export default function PlannerPage() {
               onClick={() => {
                 clearRoute();
                 setDestination(null);
-                if (navigator.geolocation) {
-                  navigator.geolocation.getCurrentPosition(
-                    async (pos) => {
-                      const initial = {
-                        name: t("planner.current_location"),
-                        label: "GPS",
-                        lat: pos.coords.latitude,
-                        lon: pos.coords.longitude,
-                        layer: "current-location",
-                      } as any;
-                      setOrigin(initial);
-                    },
-                    () => {
-                      // If geolocation fails, just keep origin empty
-                    },
-                    {
-                      enableHighAccuracy: true,
-                      timeout: 10000,
-                      maximumAge: 60 * 60 * 1000, // 1 hour in milliseconds
-                    }
-                  );
+                if (userLocation) {
+                  setOrigin({
+                    name: t("planner.current_location"),
+                    label: "GPS",
+                    lat: userLocation.latitude,
+                    lon: userLocation.longitude,
+                    layer: "current-location",
+                  });
+                } else {
+                  setOrigin(null);
                 }
               }}
               className="text-sm text-red-500"
-- 
cgit v1.3

