From 4fb2fe683b75464917dec4b1a0aaee63830f3b9a Mon Sep 17 00:00:00 2001 From: Ariel Costas Guerrero Date: Sun, 28 Dec 2025 15:59:32 +0100 Subject: feat: Refactor NavBar and Planner components; update geocoding services - Removed unused Navigation2 icon from NavBar. - Updated usePlanner hook to manage route history and improve local storage handling. - Enhanced PlannerApi with new fare properties and improved itinerary handling. - Added recent routes feature in StopList with navigation to planner. - Implemented NominatimGeocodingService for autocomplete and reverse geocoding. - Updated UI components for better user experience and accessibility. - Added translations for recent routes in multiple languages. - Improved CSS styles for map controls and overall layout. --- src/frontend/app/hooks/usePlanQuery.ts | 4 +- src/frontend/app/hooks/usePlanner.ts | 178 ++++++++++++++++++++++++++------- 2 files changed, 143 insertions(+), 39 deletions(-) (limited to 'src/frontend/app/hooks') diff --git a/src/frontend/app/hooks/usePlanQuery.ts b/src/frontend/app/hooks/usePlanQuery.ts index 103f5f4..8c81073 100644 --- a/src/frontend/app/hooks/usePlanQuery.ts +++ b/src/frontend/app/hooks/usePlanQuery.ts @@ -8,7 +8,8 @@ export const usePlanQuery = ( toLon: number | undefined, time?: Date, arriveBy: boolean = false, - enabled: boolean = true + enabled: boolean = true, + initialData?: any ) => { return useQuery({ queryKey: [ @@ -25,5 +26,6 @@ export const usePlanQuery = ( enabled: !!(fromLat && fromLon && toLat && toLon) && enabled, staleTime: 60000, // 1 minute retry: false, + initialData, }); }; diff --git a/src/frontend/app/hooks/usePlanner.ts b/src/frontend/app/hooks/usePlanner.ts index a28167a..445a426 100644 --- a/src/frontend/app/hooks/usePlanner.ts +++ b/src/frontend/app/hooks/usePlanner.ts @@ -1,21 +1,23 @@ +import { useQueryClient } from "@tanstack/react-query"; import { useCallback, useEffect, useState } from "react"; import { type PlannerSearchResult, type RoutePlan } from "../data/PlannerApi"; import { usePlanQuery } from "./usePlanQuery"; -const STORAGE_KEY = "planner_last_route"; +const STORAGE_KEY = "planner_route_history"; const EXPIRY_MS = 2 * 60 * 60 * 1000; // 2 hours interface StoredRoute { timestamp: number; origin: PlannerSearchResult; destination: PlannerSearchResult; - plan: RoutePlan; + plan?: RoutePlan; searchTime?: Date; arriveBy?: boolean; selectedItineraryIndex?: number; } -export function usePlanner() { +export function usePlanner(options: { autoLoad?: boolean } = {}) { + const { autoLoad = true } = options; const [origin, setOrigin] = useState(null); const [destination, setDestination] = useState( null @@ -28,6 +30,8 @@ export function usePlanner() { const [selectedItineraryIndex, setSelectedItineraryIndex] = useState< number | null >(null); + const [history, setHistory] = useState([]); + const queryClient = useQueryClient(); const { data: queryPlan, @@ -41,13 +45,13 @@ export function usePlanner() { destination?.lon, searchTime ?? undefined, arriveBy, - !!(origin && destination) + !!(origin && destination && searchTime) ); // Sync query result to local state and storage useEffect(() => { if (queryPlan) { - setPlan(queryPlan as any); // Cast because of slight type differences if any, but they should match now + setPlan(queryPlan as any); if (origin && destination) { const toStore: StoredRoute = { @@ -59,7 +63,21 @@ export function usePlanner() { arriveBy, selectedItineraryIndex: selectedItineraryIndex ?? undefined, }; - localStorage.setItem(STORAGE_KEY, JSON.stringify(toStore)); + + setHistory((prev) => { + const filtered = prev.filter( + (r) => + !( + r.origin.lat === origin.lat && + r.origin.lon === origin.lon && + r.destination.lat === destination.lat && + r.destination.lon === destination.lon + ) + ); + const updated = [toStore, ...filtered].slice(0, 3); + localStorage.setItem(STORAGE_KEY, JSON.stringify(updated)); + return updated; + }); } } }, [ @@ -76,22 +94,40 @@ export function usePlanner() { const stored = localStorage.getItem(STORAGE_KEY); if (stored) { try { - const data: StoredRoute = JSON.parse(stored); - if (Date.now() - data.timestamp < EXPIRY_MS) { - setOrigin(data.origin); - setDestination(data.destination); - setPlan(data.plan); - setSearchTime(data.searchTime ? new Date(data.searchTime) : null); - setArriveBy(data.arriveBy ?? false); - setSelectedItineraryIndex(data.selectedItineraryIndex ?? null); - } else { - localStorage.removeItem(STORAGE_KEY); + const data: StoredRoute[] = JSON.parse(stored); + const valid = data.filter((r) => Date.now() - r.timestamp < EXPIRY_MS); + setHistory(valid); + + if (autoLoad && valid.length > 0) { + const last = valid[0]; + if (last.plan) { + queryClient.setQueryData( + [ + "plan", + last.origin.lat, + last.origin.lon, + last.destination.lat, + last.destination.lon, + last.searchTime + ? new Date(last.searchTime).toISOString() + : undefined, + last.arriveBy ?? false, + ], + last.plan + ); + setPlan(last.plan); + } + setOrigin(last.origin); + setDestination(last.destination); + setSearchTime(last.searchTime ? new Date(last.searchTime) : null); + setArriveBy(last.arriveBy ?? false); + setSelectedItineraryIndex(last.selectedItineraryIndex ?? null); } } catch (e) { localStorage.removeItem(STORAGE_KEY); } } - }, []); + }, [autoLoad]); const searchRoute = async ( from: PlannerSearchResult, @@ -101,9 +137,78 @@ export function usePlanner() { ) => { setOrigin(from); setDestination(to); - setSearchTime(time ?? new Date()); + const finalTime = time ?? new Date(); + setSearchTime(finalTime); setArriveBy(arriveByParam); setSelectedItineraryIndex(null); + + // Save to history immediately so other pages can pick it up + const toStore: StoredRoute = { + timestamp: Date.now(), + origin: from, + destination: to, + searchTime: finalTime, + arriveBy: arriveByParam, + }; + + setHistory((prev) => { + const filtered = prev.filter( + (r) => + !( + r.origin.lat === from.lat && + r.origin.lon === from.lon && + r.destination.lat === to.lat && + r.destination.lon === to.lon + ) + ); + const updated = [toStore, ...filtered].slice(0, 3); + localStorage.setItem(STORAGE_KEY, JSON.stringify(updated)); + return updated; + }); + }; + + const loadRoute = (route: StoredRoute) => { + if (route.plan) { + queryClient.setQueryData( + [ + "plan", + route.origin.lat, + route.origin.lon, + route.destination.lat, + route.destination.lon, + route.searchTime + ? new Date(route.searchTime).toISOString() + : undefined, + route.arriveBy ?? false, + ], + route.plan + ); + setPlan(route.plan); + } + setOrigin(route.origin); + setDestination(route.destination); + setSearchTime(route.searchTime ? new Date(route.searchTime) : null); + setArriveBy(route.arriveBy ?? false); + setSelectedItineraryIndex(route.selectedItineraryIndex ?? null); + + // Move to top of history + setHistory((prev) => { + const filtered = prev.filter( + (r) => + !( + r.origin.lat === route.origin.lat && + r.origin.lon === route.origin.lon && + r.destination.lat === route.destination.lat && + r.destination.lon === route.destination.lon + ) + ); + const updated = [{ ...route, timestamp: Date.now() }, ...filtered].slice( + 0, + 3 + ); + localStorage.setItem(STORAGE_KEY, JSON.stringify(updated)); + return updated; + }); }; const clearRoute = () => { @@ -113,6 +218,7 @@ export function usePlanner() { setSearchTime(null); setArriveBy(false); setSelectedItineraryIndex(null); + setHistory([]); localStorage.removeItem(STORAGE_KEY); }; @@ -120,32 +226,26 @@ export function usePlanner() { setSelectedItineraryIndex(index); // Update storage - const stored = localStorage.getItem(STORAGE_KEY); - if (stored) { - try { - const data: StoredRoute = JSON.parse(stored); - data.selectedItineraryIndex = index; - localStorage.setItem(STORAGE_KEY, JSON.stringify(data)); - } catch (e) { - // Ignore - } - } + setHistory((prev) => { + if (prev.length === 0) return prev; + const updated = [...prev]; + updated[0] = { ...updated[0], selectedItineraryIndex: index }; + localStorage.setItem(STORAGE_KEY, JSON.stringify(updated)); + return updated; + }); }, []); const deselectItinerary = useCallback(() => { setSelectedItineraryIndex(null); // Update storage - const stored = localStorage.getItem(STORAGE_KEY); - if (stored) { - try { - const data: StoredRoute = JSON.parse(stored); - data.selectedItineraryIndex = undefined; - localStorage.setItem(STORAGE_KEY, JSON.stringify(data)); - } catch (e) { - // Ignore - } - } + setHistory((prev) => { + if (prev.length === 0) return prev; + const updated = [...prev]; + updated[0] = { ...updated[0], selectedItineraryIndex: undefined }; + localStorage.setItem(STORAGE_KEY, JSON.stringify(updated)); + return updated; + }); }, []); return { @@ -159,7 +259,9 @@ export function usePlanner() { searchTime, arriveBy, selectedItineraryIndex, + history, searchRoute, + loadRoute, clearRoute, selectItinerary, deselectItinerary, -- cgit v1.3