diff options
Diffstat (limited to 'src/frontend/app/routes/map.tsx')
| -rw-r--r-- | src/frontend/app/routes/map.tsx | 178 |
1 files changed, 141 insertions, 37 deletions
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" |
