From 120a3c6bddd0fb8d9fa05df4763596956554c025 Mon Sep 17 00:00:00 2001
From: Ariel Costas Guerrero <ariel@costas.dev>
Date: Sun, 28 Dec 2025 23:08:25 +0100
Subject: Improve planning widget

---
 src/frontend/app/routes/map.tsx | 178 +++++++++++++++++++++++++++++++---------
 1 file changed, 141 insertions(+), 37 deletions(-)

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

diff --git a/src/frontend/app/routes/map.tsx b/src/frontend/app/routes/map.tsx
index b8f179c..a651893 100644
--- a/src/frontend/app/routes/map.tsx
+++ b/src/frontend/app/routes/map.tsx
@@ -1,6 +1,4 @@
-import StopDataProvider from "../data/StopDataProvider";
-import "./map.css";
-
+import { Check, X } from "lucide-react";
 import type { FilterSpecification } from "maplibre-gl";
 import { useMemo, useRef, useState } from "react";
 import { useTranslation } from "react-i18next";
@@ -19,8 +17,11 @@ import {
 import { PlannerOverlay } from "~/components/PlannerOverlay";
 import { AppMap } from "~/components/shared/AppMap";
 import { usePageTitle } from "~/contexts/PageTitleContext";
+import { reverseGeocode } from "~/data/PlannerApi";
 import { usePlanner } from "~/hooks/usePlanner";
+import StopDataProvider from "../data/StopDataProvider";
 import "../tailwind-full.css";
+import "./map.css";
 
 // Componente principal del mapa
 export default function StopMap() {
@@ -38,7 +39,52 @@ export default function StopMap() {
   const [isSheetOpen, setIsSheetOpen] = useState(false);
   const mapRef = useRef<MapRef>(null);
 
-  const { searchRoute } = usePlanner({ autoLoad: false });
+  const {
+    searchRoute,
+    pickingMode,
+    setPickingMode,
+    setOrigin,
+    setDestination,
+    addRecentPlace,
+  } = usePlanner({ autoLoad: false });
+
+  const [isConfirming, setIsConfirming] = useState(false);
+
+  const handleConfirmPick = async () => {
+    if (!mapRef.current || !pickingMode) return;
+    const center = mapRef.current.getCenter();
+    setIsConfirming(true);
+
+    try {
+      const result = await reverseGeocode(center.lat, center.lng);
+      const finalResult = {
+        name:
+          result?.name || `${center.lat.toFixed(5)}, ${center.lng.toFixed(5)}`,
+        label: result?.label || "Map location",
+        lat: center.lat,
+        lon: center.lng,
+        layer: "map-pick",
+      };
+
+      if (pickingMode === "origin") {
+        setOrigin(finalResult);
+      } else {
+        setDestination(finalResult);
+      }
+      addRecentPlace(finalResult);
+      setPickingMode(null);
+    } catch (err) {
+      console.error("Failed to reverse geocode:", err);
+    } finally {
+      setIsConfirming(false);
+    }
+  };
+
+  const onMapInteraction = () => {
+    if (!pickingMode) {
+      window.dispatchEvent(new CustomEvent("plannerOverlay:collapse"));
+    }
+  };
 
   const favouriteIds = useMemo(() => StopDataProvider.getFavouriteIds(), []);
 
@@ -120,22 +166,78 @@ export default function StopMap() {
 
   return (
     <div className="relative h-full">
-      <PlannerOverlay
-        onSearch={(o, d, time, arriveBy) => searchRoute(o, d, time, arriveBy)}
-        onNavigateToPlanner={() => navigate("/planner")}
-        clearPickerOnOpen={true}
-        showLastDestinationWhenCollapsed={false}
-        cardBackground="bg-white/95 dark:bg-slate-900/90"
-        autoLoad={false}
-      />
+      {!pickingMode && (
+        <PlannerOverlay
+          onSearch={(o, d, time, arriveBy) => searchRoute(o, d, time, arriveBy)}
+          onNavigateToPlanner={() => navigate("/planner")}
+          clearPickerOnOpen={true}
+          showLastDestinationWhenCollapsed={false}
+          cardBackground="bg-white/95 dark:bg-slate-900/90"
+          autoLoad={false}
+        />
+      )}
+
+      {pickingMode && (
+        <div className="absolute top-4 left-0 right-0 z-20 flex justify-center px-4 pointer-events-none">
+          <div className="bg-white/95 dark:bg-slate-900/90 backdrop-blur p-4 rounded-2xl shadow-2xl border border-slate-200 dark:border-slate-700 w-full max-w-md pointer-events-auto">
+            <div className="flex items-center justify-between mb-4">
+              <h3 className="font-bold text-slate-900 dark:text-slate-100">
+                {pickingMode === "origin"
+                  ? t("planner.pick_origin", "Select origin")
+                  : t("planner.pick_destination", "Select destination")}
+              </h3>
+              <button
+                onClick={() => setPickingMode(null)}
+                className="p-1 hover:bg-slate-100 dark:hover:bg-slate-800 rounded-full transition-colors"
+              >
+                <X className="w-5 h-5 text-slate-500" />
+              </button>
+            </div>
+            <p className="text-sm text-slate-600 dark:text-slate-400 mb-4">
+              {t(
+                "planner.pick_instruction",
+                "Move the map to place the target on the desired location"
+              )}
+            </p>
+            <button
+              onClick={handleConfirmPick}
+              disabled={isConfirming}
+              className="w-full bg-primary-600 hover:bg-primary-700 text-white font-bold py-3 rounded-xl flex items-center justify-center gap-2 transition-colors disabled:opacity-50"
+            >
+              {isConfirming ? (
+                <div className="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin" />
+              ) : (
+                <>
+                  <Check className="w-5 h-5" />
+                  {t("planner.confirm_location", "Confirm location")}
+                </>
+              )}
+            </button>
+          </div>
+        </div>
+      )}
+
+      {pickingMode && (
+        <div className="absolute inset-0 pointer-events-none z-10 flex items-center justify-center">
+          <div className="relative flex items-center justify-center">
+            {/* Modern discrete target */}
+            <div className="w-1 h-1 bg-primary-600 rounded-full shadow-[0_0_0_4px_rgba(37,99,235,0.1)]" />
+            <div className="absolute w-6 h-[1px] bg-primary-600/30" />
+            <div className="absolute w-[1px] h-6 bg-primary-600/30" />
+          </div>
+        </div>
+      )}
 
       <AppMap
         ref={mapRef}
         syncState={true}
         showNavigation={true}
         showGeolocate={true}
+        showTraffic={pickingMode ? false : undefined}
         interactiveLayerIds={["stops", "stops-label"]}
         onClick={onMapClick}
+        onDragStart={onMapInteraction}
+        onZoomStart={onMapInteraction}
         attributionControl={{ compact: false }}
       >
         <Source
@@ -146,31 +248,33 @@ export default function StopMap() {
           maxzoom={20}
         />
 
-        <Layer
-          id="stops-favourite-highlight"
-          type="circle"
-          minzoom={11}
-          source="stops-source"
-          source-layer="stops"
-          filter={["all", stopLayerFilter, favouriteFilter]}
-          paint={{
-            "circle-color": "#FFD700",
-            "circle-radius": [
-              "interpolate",
-              ["linear"],
-              ["zoom"],
-              13,
-              10,
-              16,
-              12,
-              18,
-              16,
-            ],
-            "circle-opacity": 0.4,
-            "circle-stroke-color": "#FFD700",
-            "circle-stroke-width": 2,
-          }}
-        />
+        {!pickingMode && (
+          <Layer
+            id="stops-favourite-highlight"
+            type="circle"
+            minzoom={11}
+            source="stops-source"
+            source-layer="stops"
+            filter={["all", stopLayerFilter, favouriteFilter]}
+            paint={{
+              "circle-color": "#FFD700",
+              "circle-radius": [
+                "interpolate",
+                ["linear"],
+                ["zoom"],
+                13,
+                10,
+                16,
+                12,
+                18,
+                16,
+              ],
+              "circle-opacity": 0.4,
+              "circle-stroke-color": "#FFD700",
+              "circle-stroke-width": 2,
+            }}
+          />
+        )}
 
         <Layer
           id="stops"
-- 
cgit v1.3

