From b3f5bfad9c2d1ac92debb389fd7a774a6cdb5109 Mon Sep 17 00:00:00 2001 From: Ariel Costas Guerrero Date: Mon, 9 Mar 2026 00:12:27 +0100 Subject: Enhance Favourites and Routes components; add agency favorites handling and improve layout for favorites display --- src/frontend/app/routes/favourites.tsx | 287 ++++++++++++++++++++++++++------- src/frontend/app/routes/routes.tsx | 82 ++++++---- 2 files changed, 281 insertions(+), 88 deletions(-) (limited to 'src') diff --git a/src/frontend/app/routes/favourites.tsx b/src/frontend/app/routes/favourites.tsx index 6b57256..9632123 100644 --- a/src/frontend/app/routes/favourites.tsx +++ b/src/frontend/app/routes/favourites.tsx @@ -1,12 +1,18 @@ -import { useCallback, useEffect, useState } from "react"; +import { useQuery } from "@tanstack/react-query"; +import { Clock, Star } from "lucide-react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { Link } from "react-router"; +import { fetchArrivals } from "~/api/arrivals"; +import { type Arrival } from "~/api/schema"; +import { fetchRoutes } from "~/api/transit"; import RouteIcon from "~/components/RouteIcon"; import { usePageTitle } from "~/contexts/PageTitleContext"; import SpecialPlacesProvider, { type SpecialPlace, } from "~/data/SpecialPlacesProvider"; import StopDataProvider, { type Stop } from "~/data/StopDataProvider"; +import { useFavorites } from "~/hooks/useFavorites"; export default function Favourites() { const { t } = useTranslation(); @@ -16,6 +22,27 @@ export default function Favourites() { const [work, setWork] = useState(null); const [favouriteStops, setFavouriteStops] = useState([]); const [loading, setLoading] = useState(true); + const [expandedAgencies, setExpandedAgencies] = useState< + Record + >({}); + const { favorites: favouriteRouteIds, isFavorite: isFavoriteRoute } = + useFavorites("favouriteRoutes"); + const { favorites: favouriteAgencyIds, isFavorite: isFavoriteAgency } = + useFavorites("favouriteAgencies"); + + const orderedAgencies = [ + "vitrasa", + "tranvias", + "tussa", + "ourense", + "feve", + "shuttle", + ]; + + const { data: routes = [], isLoading: routesLoading } = useQuery({ + queryKey: ["routes", "favourites"], + queryFn: () => fetchRoutes(orderedAgencies), + }); const loadData = useCallback(async () => { try { @@ -59,9 +86,55 @@ export default function Favourites() { setWork(null); }; - const isEmpty = !home && !work && favouriteStops.length === 0; + const toggleAgencyExpanded = (agency: string) => { + setExpandedAgencies((prev) => ({ ...prev, [agency]: !prev[agency] })); + }; + + const favouriteRoutes = useMemo(() => { + return routes.filter((route) => isFavoriteRoute(route.id)); + }, [routes, isFavoriteRoute]); + + const favouriteAgencies = useMemo(() => { + return routes.reduce( + (acc, route) => { + const agency = route.agencyName || t("routes.unknown_agency", "Otros"); + if (!isFavoriteAgency(agency)) { + return acc; + } + + if (!acc[agency]) { + acc[agency] = []; + } + + acc[agency].push(route); + return acc; + }, + {} as Record + ); + }, [routes, isFavoriteAgency, t]); + + const sortedFavouriteAgencyEntries = useMemo(() => { + return Object.entries(favouriteAgencies).sort(([a], [b]) => { + const indexA = orderedAgencies.indexOf(a.toLowerCase()); + const indexB = orderedAgencies.indexOf(b.toLowerCase()); + + if (indexA === -1 && indexB === -1) { + return a.localeCompare(b); + } + if (indexA === -1) return 1; + if (indexB === -1) return -1; + return indexA - indexB; + }); + }, [favouriteAgencies]); + + const isEmpty = + !home && + !work && + favouriteStops.length === 0 && + favouriteRouteIds.length === 0 && + favouriteAgencyIds.length === 0; - if (loading) { + if (loading || routesLoading) { return (
@@ -105,13 +178,54 @@ export default function Favourites() { } return ( -
+
+ {favouriteRoutes.length > 0 && ( +
+
+ +

+ {t("routes.favorites", "Rutas favoritas")} +

+
+
    + {favouriteRoutes.map((route) => ( +
  • + + +
    +

    + {route.longName} +

    + {route.agencyName && ( +

    + {route.agencyName} +

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

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

+
+
+ +

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

+
{/* Home */} 0 && ( -
-

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

-
    - {favouriteStops.map((stop) => ( +
    +
    + +

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

    +
    +
      + {favouriteStops.map((stop, index) => ( ))}
    @@ -251,15 +369,35 @@ interface FavouriteStopItemProps { onRemove: (stopId: string) => void; removeLabel: string; viewLabel: string; + showArrivals?: boolean; } function FavouriteStopItem({ stop, onRemove, removeLabel, - viewLabel, + viewLabel: _viewLabel, + showArrivals, }: FavouriteStopItemProps) { const { t } = useTranslation(); + const [arrivals, setArrivals] = useState(null); + + useEffect(() => { + let mounted = true; + if (showArrivals) { + fetchArrivals(stop.stopId, true) + .then((res) => { + if (mounted) { + setArrivals(res.arrivals.slice(0, 3)); + } + }) + .catch(console.error); + } + return () => { + mounted = false; + }; + }, [showArrivals, stop.stopId]); + const confirmAndRemove = () => { const ok = window.confirm( t("favourites.confirm_remove", "Remove this favourite?") @@ -269,50 +407,89 @@ function FavouriteStopItem({ }; return ( -
  • -
    - -
    - - ★ - - - ({stop.stopCode || stop.stopId}) +
  • + + + +
    +
    + + {StopDataProvider.getDisplayName(stop)}
    -
    - {StopDataProvider.getDisplayName(stop)} -
    -
    - {stop.lines?.slice(0, 6).map((lineObj) => ( - - ))} - {stop.lines && stop.lines.length > 6 && ( - - +{stop.lines.length - 6} - - )} + +
    + + {stop.stopId.split(":")[0]} + + + {stop.stopCode || stop.stopId.split(":")[1] || stop.stopId} +
    - -
    - + + {stop.lines && stop.lines.length > 0 && ( +
    + {stop.lines.map((lineObj) => ( + + ))} +
    + )} + + {showArrivals && arrivals && arrivals.length > 0 && ( +
    +
    + + + {t("estimates.next_arrivals", "Próximas llegadas")} + +
    + {arrivals.map((arr, i) => ( +
    +
    + +
    + + {arr.headsign.destination} + + + {arr.estimate.minutes}' + +
    + ))} +
    + )}
    -
    +
  • ); } diff --git a/src/frontend/app/routes/routes.tsx b/src/frontend/app/routes/routes.tsx index 128bbc4..6f07571 100644 --- a/src/frontend/app/routes/routes.tsx +++ b/src/frontend/app/routes/routes.tsx @@ -13,8 +13,8 @@ export default function RoutesPage() { const { t } = useTranslation(); usePageTitle(t("navbar.routes", "Rutas")); const [searchQuery, setSearchQuery] = useState(""); - const { toggleFavorite: toggleFavoriteRoute, isFavorite: isFavoriteRoute } = - useFavorites("favouriteRoutes"); + const [isFavoritesExpanded, setIsFavoritesExpanded] = useState(true); + const { isFavorite: isFavoriteRoute } = useFavorites("favouriteRoutes"); const { toggleFavorite: toggleFavoriteAgency, isFavorite: isFavoriteAgency } = useFavorites("favouriteAgencies"); @@ -105,36 +105,52 @@ export default function RoutesPage() {
    {favoriteRoutes.length > 0 && !searchQuery && ( -
    -

    +
    +

    -
    - {favoriteRoutes.map((route) => ( -
    - - -
    -

    - {route.longName} -

    -
    - -
    - ))} -
    +

    + {t("routes.favorites", "Favoritas")} +

    + + {favoriteRoutes.length} + + + + {isFavoritesExpanded && ( +
    + {favoriteRoutes.map((route) => ( +
    + + +
    +

    + {route.longName} +

    +
    + +
    + ))} +
    + )}
    )} @@ -199,8 +215,8 @@ export default function RoutesPage() {

    -- cgit v1.3