aboutsummaryrefslogtreecommitdiff
path: root/src/frontend/app/routes/planner.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/frontend/app/routes/planner.tsx')
-rw-r--r--src/frontend/app/routes/planner.tsx637
1 files changed, 404 insertions, 233 deletions
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"