import React, { useEffect, useState } from "react"; import { Sheet } from "react-modal-sheet"; import { Link } from "react-router"; import { useTranslation } from "react-i18next"; import { Clock, RefreshCw } from "lucide-react"; import LineIcon from "./LineIcon"; import { StopSheetSkeleton } from "./StopSheetSkeleton"; import { ErrorDisplay } from "./ErrorDisplay"; import { StopAlert } from "./StopAlert"; import { type Estimate } from "../routes/estimates-$id"; import { REGIONS, type RegionId, getRegionConfig } from "../data/RegionConfig"; import { useApp } from "../AppContext"; import "./StopSheet.css"; import type { Stop } from "~/data/StopDataProvider"; interface StopSheetProps { isOpen: boolean; onClose: () => void; stop: Stop; } interface ErrorInfo { type: "network" | "server" | "unknown"; status?: number; message?: string; } const loadStopData = async ( region: RegionId, stopId: number, ): Promise => { const regionConfig = getRegionConfig(region); const resp = await fetch(`${regionConfig.estimatesEndpoint}?id=${stopId}`, { headers: { Accept: "application/json", }, }); if (!resp.ok) { throw new Error(`HTTP ${resp.status}: ${resp.statusText}`); } return await resp.json(); }; export const StopSheet: React.FC = ({ isOpen, onClose, stop, }) => { const { t } = useTranslation(); const { region } = useApp(); const regionConfig = getRegionConfig(region); const [data, setData] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [lastUpdated, setLastUpdated] = useState(null); 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 = async () => { try { setLoading(true); setError(null); setData(null); const stopData = await loadStopData(region, stop.stopId); setData(stopData); setLastUpdated(new Date()); } catch (err) { console.error("Failed to load stop data:", err); setError(parseError(err)); } finally { setLoading(false); } }; useEffect(() => { if (isOpen && stop.stopId) { loadData(); } }, [isOpen, stop.stopId, region]); const formatTime = (minutes: number) => { if (minutes > 15) { const now = new Date(); const arrival = new Date(now.getTime() + minutes * 60000); return Intl.DateTimeFormat( typeof navigator !== "undefined" ? navigator.language : "en", { hour: "2-digit", minute: "2-digit", }, ).format(arrival); } else { return `${minutes} ${t("estimates.minutes", "min")}`; } }; const formatDistance = (meters: number) => { if (meters > 1024) { return `${(meters / 1000).toFixed(1)} km`; } else { return `${meters} ${t("estimates.meters", "m")}`; } }; // Show only the next 4 arrivals const limitedEstimates = data?.sort((a, b) => a.minutes - b.minutes).slice(0, 4) || []; return (

{stop.name.original}

({stop.stopId})
= 10 ? "scrollable" : ""}`} > {stop.lines.map((line) => (
))}
{loading ? ( ) : error ? ( ) : data ? ( <>

{t("estimates.next_arrivals", "Next arrivals")}

{limitedEstimates.length === 0 ? (
{t("estimates.none", "No hay estimaciones disponibles")}
) : (
{limitedEstimates.map((estimate, idx) => (
{estimate.route}
{formatTime(estimate.minutes)}
{REGIONS[region].showMeters && estimate.meters >= 0 && (
{formatDistance(estimate.meters)}
)}
))}
)}
{lastUpdated && (
{t("estimates.last_updated", "Actualizado a las")}{" "} {lastUpdated.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit", second: "2-digit", })}
)}
{t( "map.view_all_estimates", "Ver todas las estimaciones", )}
) : null}
); };