import { useQuery } from "@tanstack/react-query"; import { ChevronDown, ChevronRight, Star } from "lucide-react"; import { useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { Link } from "react-router"; import { fetchRoutes } from "~/api/transit"; import RouteIcon from "~/components/RouteIcon"; import { usePageTitle } from "~/contexts/PageTitleContext"; import { useFavorites } from "~/hooks/useFavorites"; import "../tailwind-full.css"; export default function RoutesPage() { const { t } = useTranslation(); usePageTitle(t("navbar.routes", "Rutas")); const [searchQuery, setSearchQuery] = useState(""); const [isFavoritesExpanded, setIsFavoritesExpanded] = useState(true); const { isFavorite: isFavoriteRoute } = useFavorites("favouriteRoutes"); const { toggleFavorite: toggleFavoriteAgency, isFavorite: isFavoriteAgency } = useFavorites("favouriteAgencies"); const [expandedAgencies, setExpandedAgencies] = useState< Record >({}); const toggleAgencyExpanded = (agency: string) => { setExpandedAgencies((prev) => ({ ...prev, [agency]: !prev[agency] })); }; const orderedAgencies = [ "vitrasa", "tranvias", "tussa", "ourense", "lugo", "feve", "shuttle", ]; const { data: routes, isLoading } = useQuery({ queryKey: ["routes"], queryFn: () => fetchRoutes(orderedAgencies), }); const filteredRoutes = useMemo(() => { return routes?.filter( (route) => route.shortName?.toLowerCase().includes(searchQuery.toLowerCase()) || route.longName?.toLowerCase().includes(searchQuery.toLowerCase()) ); }, [routes, searchQuery]); const routesByAgency = useMemo(() => { return filteredRoutes?.reduce( (acc, route) => { const agency = route.agencyName || t("routes.unknown_agency", "Otros"); if (!acc[agency]) acc[agency] = []; acc[agency].push(route); return acc; }, {} as Record ); }, [filteredRoutes, t]); const sortedAgencyEntries = useMemo(() => { if (!routesByAgency) return []; return Object.entries(routesByAgency).sort(([a, routesA], [b, routesB]) => { // Use the agency's own gtfsId (feedId:agencyId) as the stable key — this // matches the "agency#feedId:agencyId" alert selector format and correctly // handles feeds that contain multiple agencies. const agencyIdA = routesA?.[0]?.agencyId ?? routesA?.[0]?.id.split(":")[0] ?? a.toLowerCase(); const agencyIdB = routesB?.[0]?.agencyId ?? routesB?.[0]?.id.split(":")[0] ?? b.toLowerCase(); const feedIdA = agencyIdA.split(":")[0]; const feedIdB = agencyIdB.split(":")[0]; // First, sort by favorite status const isFavA = isFavoriteAgency(agencyIdA); const isFavB = isFavoriteAgency(agencyIdB); if (isFavA && !isFavB) return -1; if (!isFavA && isFavB) return 1; // Then by fixed order const indexA = orderedAgencies.indexOf(feedIdA); const indexB = orderedAgencies.indexOf(feedIdB); if (indexA === -1 && indexB === -1) { return a.localeCompare(b); } if (indexA === -1) return 1; if (indexB === -1) return -1; return indexA - indexB; }); }, [routesByAgency, orderedAgencies, isFavoriteAgency]); const favoriteRoutes = useMemo(() => { return filteredRoutes?.filter((route) => isFavoriteRoute(route.id)) || []; }, [filteredRoutes, isFavoriteRoute]); return (
setSearchQuery(e.target.value)} />
{isLoading && (
)}
{favoriteRoutes.length > 0 && !searchQuery && (
{isFavoritesExpanded && (
{favoriteRoutes.map((route) => (

{route.longName}

))}
)}
)} {sortedAgencyEntries.map(([agency, agencyRoutes]) => { // Use the agency's own gtfsId (feedId:agencyId) as the stable favourite key. const agencyId = agencyRoutes?.[0]?.agencyId ?? agencyRoutes?.[0]?.id.split(":")[0] ?? agency.toLowerCase(); const isFav = isFavoriteAgency(agencyId); const isExpanded = searchQuery ? true : (expandedAgencies[agency] ?? isFav); return (
{isExpanded && (
{agencyRoutes.map((route) => (

{route.longName}

))}
)}
); })}
); }