From 5e9f1094a50bbcdd514e958dcd67d0f0a844589d Mon Sep 17 00:00:00 2001 From: Ariel Costas Guerrero Date: Fri, 12 Dec 2025 20:30:44 +0100 Subject: feat: implement favourites management, add recent places functionality, and enhance planner features --- src/frontend/app/components/PlannerOverlay.tsx | 163 +++++++++++++++++++------ 1 file changed, 125 insertions(+), 38 deletions(-) (limited to 'src/frontend/app/components/PlannerOverlay.tsx') diff --git a/src/frontend/app/components/PlannerOverlay.tsx b/src/frontend/app/components/PlannerOverlay.tsx index 8046ab2..12cfb0f 100644 --- a/src/frontend/app/components/PlannerOverlay.tsx +++ b/src/frontend/app/components/PlannerOverlay.tsx @@ -1,3 +1,4 @@ +import { MapPin } from "lucide-react"; import React, { useCallback, useEffect, @@ -6,6 +7,8 @@ import React, { useState, } from "react"; import { useTranslation } from "react-i18next"; +import PlaceListItem from "~/components/PlaceListItem"; +import { REGION_DATA } from "~/config/RegionConfig"; import { reverseGeocode, searchPlaces, @@ -55,6 +58,14 @@ export const PlannerOverlay: React.FC = ({ const [favouriteStops, setFavouriteStops] = useState( [] ); + const [recentPlaces, setRecentPlaces] = useState([]); + const RECENT_KEY = `recentPlaces_${REGION_DATA.id}`; + const clearRecentPlaces = useCallback(() => { + setRecentPlaces([]); + try { + localStorage.removeItem(RECENT_KEY); + } catch {} + }, []); const pickerInputRef = useRef(null); @@ -100,6 +111,43 @@ export const PlannerOverlay: React.FC = ({ .catch(() => setFavouriteStops([])); }, []); + // Load recent places from localStorage + useEffect(() => { + try { + const raw = localStorage.getItem(RECENT_KEY); + if (raw) { + const parsed = JSON.parse(raw) as PlannerSearchResult[]; + setRecentPlaces(parsed.slice(0, 20)); + } + } catch { + setRecentPlaces([]); + } + }, []); + + const addRecentPlace = useCallback( + (p: PlannerSearchResult) => { + const key = `${p.lat.toFixed(5)},${p.lon.toFixed(5)}`; + const existing = recentPlaces.filter( + (rp) => `${rp.lat.toFixed(5)},${rp.lon.toFixed(5)}` !== key + ); + const updated = [ + { + name: p.name, + label: p.label, + lat: p.lat, + lon: p.lon, + layer: p.layer, + }, + ...existing, + ].slice(0, 20); + setRecentPlaces(updated); + try { + localStorage.setItem(RECENT_KEY, JSON.stringify(updated)); + } catch {} + }, + [recentPlaces] + ); + const filteredFavouriteStops = useMemo(() => { const q = pickerQuery.trim().toLowerCase(); if (!q) return favouriteStops; @@ -110,6 +158,28 @@ export const PlannerOverlay: React.FC = ({ ); }, [favouriteStops, pickerQuery]); + const sortedRemoteResults = useMemo(() => { + const order: Record = { venue: 0, address: 1, street: 2 }; + const q = pickerQuery.trim().toLowerCase(); + const base = q + ? remoteResults.filter( + (s) => + (s.name || "").toLowerCase().includes(q) || + (s.label || "").toLowerCase().includes(q) + ) + : remoteResults; + return [...base].sort((a, b) => { + const oa = order[a.layer || ""] ?? 99; + const ob = order[b.layer || ""] ?? 99; + if (oa !== ob) return oa - ob; + // Secondary: shorter label first, then name alpha + const la = (a.label || "").length; + const lb = (b.label || "").length; + if (la !== lb) return la - lb; + return (a.name || "").localeCompare(b.name || ""); + }); + }, [remoteResults, pickerQuery]); + const openPicker = (field: PickerField) => { setPickerField(field); setPickerQuery( @@ -134,6 +204,7 @@ export const PlannerOverlay: React.FC = ({ setDestination(result); setDestQuery(result.name || ""); } + addRecentPlace(result); setPickerOpen(false); }; @@ -484,15 +555,17 @@ export const PlannerOverlay: React.FC = ({ /> @@ -506,60 +579,74 @@ export const PlannerOverlay: React.FC = ({ onClick={setOriginFromCurrentLocation} disabled={locationLoading} > -
-
- {t("planner.current_location")} -
-
- {t("planner.gps")} +
+ + + +
+
+ {t("planner.current_location")} +
+
+ {t("planner.gps")} +
- {locationLoading ? "…" : "📍"} + {locationLoading ? "…" : ""}
)} + {(remoteLoading || sortedRemoteResults.length > 0) && ( +
  • + {remoteLoading + ? t("planner.searching_ellipsis") + : t("planner.results", "Results")} +
  • + )} + + {sortedRemoteResults.map((r, i) => ( + + ))} + {filteredFavouriteStops.length > 0 && ( <>
  • {t("planner.favourite_stops")}
  • {filteredFavouriteStops.map((r, i) => ( -
  • - -
  • + place={r} + onClick={applyPickedResult} + /> ))} )} - {(remoteLoading || remoteResults.length > 0) && ( -
  • - {remoteLoading - ? t("planner.searching_ellipsis") - : t("planner.results", "Results")} + {recentPlaces.length > 0 && ( +
  • + + {t("planner.recent_locations", "Recent locations")} + +
  • )} - {remoteResults.map((r, i) => ( + {recentPlaces.map((r, i) => (