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(); 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 [expandedAgencies, setExpandedAgencies] = useState< Record >({}); const { favorites: favouriteRouteIds, isFavorite: isFavoriteRoute } = useFavorites("favouriteRoutes"); const { favorites: favouriteAgencyIds, isFavorite: isFavoriteAgency } = useFavorites("favouriteAgencies"); const orderedAgencies = [ "vitrasa", "tranvias", "tussa", "ourense", "lugo", "feve", "shuttle", ]; const { data: routes = [], isLoading: routesLoading } = useQuery({ queryKey: ["routes", "favourites"], queryFn: () => fetchRoutes(orderedAgencies), }); const loadData = useCallback(async () => { try { setLoading(true); // Load special places setHome(SpecialPlacesProvider.getHome()); setWork(SpecialPlacesProvider.getWork()); // Load favourite stops const favouriteIds = StopDataProvider.getFavouriteIds(); const stopsMap = await StopDataProvider.fetchStopsByIds(favouriteIds); const favStops = favouriteIds .map((id) => stopsMap[id]) .filter(Boolean) .map((stop) => ({ ...stop, favourite: true })); 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 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 || routesLoading) { 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 (
{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")}

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

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

    {favouriteStops.map((stop, index) => ( ))}
)}
); } 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; showArrivals?: boolean; } function FavouriteStopItem({ stop, onRemove, removeLabel, 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?") ); if (!ok) return; onRemove(stop.stopId); }; return (
  • {StopDataProvider.getDisplayName(stop)}
    {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}'
    ))}
    )}
  • ); }