aboutsummaryrefslogtreecommitdiff
path: root/src/frontend/app/routes
diff options
context:
space:
mode:
authorAriel Costas Guerrero <ariel@costas.dev>2025-12-28 23:08:25 +0100
committerAriel Costas Guerrero <ariel@costas.dev>2025-12-28 23:10:24 +0100
commit120a3c6bddd0fb8d9fa05df4763596956554c025 (patch)
tree3ed99935b58b1a269030aa2a638f35c0aa989f55 /src/frontend/app/routes
parent9618229477439d1604869aa68fc21d4eae7d8bb1 (diff)
Improve planning widget
Diffstat (limited to 'src/frontend/app/routes')
-rw-r--r--src/frontend/app/routes/map.tsx178
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"