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/routes/favourites.tsx | 309 ++++++++++++++++++++++++++++++++- src/frontend/app/routes/planner.tsx | 28 ++- 2 files changed, 333 insertions(+), 4 deletions(-) (limited to 'src/frontend/app/routes') diff --git a/src/frontend/app/routes/favourites.tsx b/src/frontend/app/routes/favourites.tsx index 5b74391..ff229b2 100644 --- a/src/frontend/app/routes/favourites.tsx +++ b/src/frontend/app/routes/favourites.tsx @@ -1,13 +1,316 @@ +import { useCallback, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; +import { Link } from "react-router"; +import LineIcon from "~/components/LineIcon"; import { usePageTitle } from "~/contexts/PageTitleContext"; +import SpecialPlacesProvider, { + type SpecialPlace, +} from "~/data/SpecialPlacesProvider"; +import StopDataProvider, { type Stop } from "~/data/StopDataProvider"; export default function Favourites() { const { t } = useTranslation(); - usePageTitle(t("navbar.favourites", "Favoritos")); + usePageTitle(t("favourites.title", "Favourites")); + + const [home, setHome] = useState(null); + const [work, setWork] = useState(null); + const [favouriteStops, setFavouriteStops] = useState([]); + const [loading, setLoading] = useState(true); + + const loadData = useCallback(async () => { + try { + setLoading(true); + + // Load special places + setHome(SpecialPlacesProvider.getHome()); + setWork(SpecialPlacesProvider.getWork()); + + // Load favourite stops + const favouriteIds = StopDataProvider.getFavouriteIds(); + const allStops = await StopDataProvider.getStops(); + const favStops = allStops.filter((stop) => + favouriteIds.includes(stop.stopId) + ); + setFavouriteStops(favStops); + } catch (error) { + console.error("Error loading favourites:", error); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + loadData(); + }, [loadData]); + + const handleRemoveFavourite = (stopId: string) => { + StopDataProvider.removeFavourite(stopId); + setFavouriteStops((prev) => prev.filter((s) => s.stopId !== stopId)); + }; + + const handleRemoveHome = () => { + SpecialPlacesProvider.removeHome(); + setHome(null); + }; + + const handleRemoveWork = () => { + SpecialPlacesProvider.removeWork(); + setWork(null); + }; + + const isEmpty = !home && !work && favouriteStops.length === 0; + + if (loading) { + return ( +
+
+
+ {t("common.loading", "Loading...")} +
+
+
+ ); + } + + if (isEmpty) { + return ( +
+
+ + + +

+ {t("favourites.empty", "You don't have any favourite stops yet.")} +

+

+ {t( + "favourites.empty_description", + "Go to a stop and mark it as favourite to see it here." + )} +

+
+
+ ); + } return ( -
-

{t("favourites.empty", "No tienes paradas favoritas.")}

+
+ {/* Special Places Section */} + {(home || work) && ( +
+

+ {t("favourites.special_places", "Special Places")} +

+
+ {/* Home */} + + {/* Work */} + +
+
+ )} + + {/* Favourite Stops Section */} + {favouriteStops.length > 0 && ( +
+

+ {t("favourites.favourite_stops", "Favourite Stops")} +

+
    + {favouriteStops.map((stop) => ( + + ))} +
+
+ )}
); } + +interface SpecialPlaceCardProps { + icon: string; + label: string; + place: SpecialPlace | null; + onRemove: () => void; + editLabel: string; + removeLabel: string; + notSetLabel: string; + setLabel: string; +} + +function SpecialPlaceCard({ + icon, + label, + place, + onRemove, + editLabel, + removeLabel, + notSetLabel, + setLabel, +}: SpecialPlaceCardProps) { + return ( +
+
+
+ +
+

+ {label} +

+ {place ? ( +
+

+ {place.name} +

+ {place.type === "stop" && place.stopId && ( +

({place.stopId})

+ )} + {place.type === "address" && place.address && ( +

{place.address}

+ )} +
+ ) : ( +

+ {notSetLabel} +

+ )} +
+
+
+ {place ? ( + <> + {place.type === "stop" && place.stopId && ( + + {editLabel} + + )} + + + ) : ( + + )} +
+
+
+ ); +} + +interface FavouriteStopItemProps { + stop: Stop; + onRemove: (stopId: string) => void; + removeLabel: string; + viewLabel: string; +} + +function FavouriteStopItem({ + stop, + onRemove, + removeLabel, + viewLabel, +}: FavouriteStopItemProps) { + const { t } = useTranslation(); + const confirmAndRemove = () => { + const ok = window.confirm( + t("favourites.confirm_remove", "Remove this favourite?") + ); + if (!ok) return; + onRemove(stop.stopId); + }; + + return ( +
  • +
    + +
    + + ★ + + + ({stop.stopId}) + +
    +
    + {StopDataProvider.getDisplayName(stop)} +
    +
    + {stop.lines?.slice(0, 6).map((line) => ( + + ))} + {stop.lines && stop.lines.length > 6 && ( + + +{stop.lines.length - 6} + + )} +
    + +
    + +
    +
    +
  • + ); +} diff --git a/src/frontend/app/routes/planner.tsx b/src/frontend/app/routes/planner.tsx index 5121dce..55d1553 100644 --- a/src/frontend/app/routes/planner.tsx +++ b/src/frontend/app/routes/planner.tsx @@ -754,6 +754,8 @@ export default function PlannerPage() { selectedItineraryIndex, selectItinerary, deselectItinerary, + setOrigin, + setDestination, } = usePlanner(); const [selectedItinerary, setSelectedItinerary] = useState( null @@ -839,7 +841,31 @@ export default function PlannerPage() {

    )}
    - -- cgit v1.3