import { CircleHelp, Eye, EyeClosed, Star } from "lucide-react"; import { useCallback, useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { useParams } from "react-router"; import { fetchArrivals } from "~/api/arrivals"; import { type Arrival, type Position, type RouteInfo, type StopArrivalsResponse, } from "~/api/schema"; import { ArrivalList } from "~/components/arrivals/ArrivalList"; import { ErrorDisplay } from "~/components/ErrorDisplay"; import { PullToRefresh } from "~/components/PullToRefresh"; import RouteIcon from "~/components/RouteIcon"; import { StopHelpModal } from "~/components/stop/StopHelpModal"; import { StopMapModal } from "~/components/stop/StopMapModal"; import { StopUsageChart } from "~/components/stop/StopUsageChart"; import { usePageRightNode, usePageTitle } from "~/contexts/PageTitleContext"; import { formatHex } from "~/utils/colours"; import StopDataProvider from "../data/StopDataProvider"; import "../tailwind-full.css"; import "./stops-$id.css"; function StopFavouriteButton({ stopId }: { stopId: string }) { const { t } = useTranslation(); const [favourited, setFavourited] = useState(() => StopDataProvider.isFavourite(stopId) ); const toggle = () => { if (favourited) { StopDataProvider.removeFavourite(stopId); setFavourited(false); } else { StopDataProvider.addFavourite(stopId); setFavourited(true); } }; return ( ); } export const getArrivalId = (a: Arrival): string => { return a.tripId; }; interface ErrorInfo { type: "network" | "server" | "unknown"; status?: number; message?: string; } export default function Estimates() { const { t } = useTranslation(); const params = useParams(); const stopId = params.id ?? ""; const stopFeedId = stopId.split(":")[0] || stopId; const fallbackStopCode = stopId.split(":")[1] || stopId; const [stopName, setStopName] = useState(undefined); const [apiRoutes, setApiRoutes] = useState([]); const [apiLocation, setApiLocation] = useState( undefined ); // Data state const [data, setData] = useState(null); const [dataDate, setDataDate] = useState(null); const [dataLoading, setDataLoading] = useState(true); const [dataError, setDataError] = useState(null); const [isManualRefreshing, setIsManualRefreshing] = useState(false); const [isMapModalOpen, setIsMapModalOpen] = useState(false); const [isHelpModalOpen, setIsHelpModalOpen] = useState(false); const [isReducedView, setIsReducedView] = useState(false); const [selectedArrivalId, setSelectedArrivalId] = useState< string | undefined >(undefined); // Helper function to get the display name for the stop const getStopDisplayName = useCallback(() => { if (stopName) return stopName; return `Parada ${stopId}`; }, [stopId, stopName]); usePageTitle(getStopDisplayName()); const rightNode = useMemo( () => , [stopId] ); usePageRightNode(rightNode); const parseError = (error: any): ErrorInfo => { if (!navigator.onLine) { return { type: "network", message: "No internet connection" }; } if ( error.message?.includes("Failed to fetch") || error.message?.includes("NetworkError") ) { return { type: "network" }; } if (error.message?.includes("HTTP")) { const statusMatch = error.message.match(/HTTP (\d+):/); const status = statusMatch ? parseInt(statusMatch[1]) : undefined; return { type: "server", status }; } return { type: "unknown", message: error.message }; }; const loadData = useCallback(async () => { try { setDataError(null); const response = await fetchArrivals(stopId, false); setData(response); setStopName(response.stopName); setApiRoutes(response.routes); if (response.stopLocation) { setApiLocation(response.stopLocation); } setDataDate(new Date()); } catch (error) { console.error("Error loading arrivals data:", error); setDataError(parseError(error)); setData(null); setDataDate(null); } }, [stopId]); const refreshData = useCallback(async () => { await Promise.all([loadData()]); }, [loadData]); const handleManualRefresh = useCallback(async () => { try { setDataLoading(true); setIsManualRefreshing(true); await refreshData(); } finally { setIsManualRefreshing(false); setDataLoading(false); } }, [refreshData]); useEffect(() => { // Initial load setDataLoading(true); loadData(); StopDataProvider.pushRecent(stopId); setDataLoading(false); }, [stopId, loadData]); return ( {apiRoutes.length > 0 && ( {apiRoutes.map((line) => ( ))} )} {dataLoading ? ( <>{/*TODO: New loading skeleton*/}> ) : dataError ? ( ) : data ? ( <> {t("estimates.caption", "Estimaciones a las {{time}}", { time: dataDate?.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", }), })} {stopFeedId} {data.stopCode || fallbackStopCode} setIsHelpModalOpen(true)} > {isReducedView ? ( setIsReducedView(false)} > ) : ( setIsReducedView(true)} > )} { setSelectedArrivalId(getArrivalId(arrival)); setIsMapModalOpen(true); }} /> {data.usage && data.usage.length > 0 && ( )} > ) : null} {apiLocation && ( ({ id: getArrivalId(a), currentPosition: a.currentPosition ?? undefined, colour: formatHex(a.route.colour), textColour: formatHex(a.route.textColour), shape: a.shape, }))} isOpen={isMapModalOpen} onClose={() => setIsMapModalOpen(false)} selectedCirculationId={selectedArrivalId} /> )} setIsHelpModalOpen(false)} /> ); }