import { type JSX, useEffect, useState, useCallback } from "react"; import { useParams, Link } from "react-router"; import StopDataProvider from "../data/StopDataProvider"; import { Star, Edit2, ExternalLink, RefreshCw } from "lucide-react"; import "./estimates-$id.css"; import { RegularTable } from "../components/RegularTable"; import { useApp } from "../AppContext"; import { GroupedTable } from "../components/GroupedTable"; import { useTranslation } from "react-i18next"; import { TimetableTable, type TimetableEntry } from "../components/TimetableTable"; import { EstimatesTableSkeleton, EstimatesGroupedSkeleton } from "../components/EstimatesTableSkeleton"; import { TimetableSkeleton } from "../components/TimetableSkeleton"; import { ErrorDisplay } from "../components/ErrorDisplay"; import { PullToRefresh } from "../components/PullToRefresh"; import { useAutoRefresh } from "../hooks/useAutoRefresh"; export interface StopDetails { stop: { id: number; name: string; latitude: number; longitude: number; }; estimates: { line: string; route: string; minutes: number; meters: number; }[]; } interface ErrorInfo { type: 'network' | 'server' | 'unknown'; status?: number; message?: string; } const loadData = async (stopId: string): Promise => { // Add delay to see skeletons in action (remove in production) await new Promise(resolve => setTimeout(resolve, 1000)); const resp = await fetch(`/api/GetStopEstimates?id=${stopId}`, { headers: { Accept: "application/json", }, }); if (!resp.ok) { throw new Error(`HTTP ${resp.status}: ${resp.statusText}`); } return await resp.json(); }; const loadTimetableData = async (stopId: string): Promise => { // Add delay to see skeletons in action (remove in production) await new Promise(resolve => setTimeout(resolve, 1500)); const today = new Date().toISOString().split('T')[0]; // YYYY-MM-DD format const resp = await fetch(`/api/GetStopTimetable?date=${today}&stopId=${stopId}`, { headers: { Accept: "application/json", }, }); if (!resp.ok) { throw new Error(`HTTP ${resp.status}: ${resp.statusText}`); } return await resp.json(); }; export default function Estimates() { const { t } = useTranslation(); const params = useParams(); const stopIdNum = parseInt(params.id ?? ""); const [customName, setCustomName] = useState(undefined); // Estimates data state const [data, setData] = useState(null); const [dataDate, setDataDate] = useState(null); const [estimatesLoading, setEstimatesLoading] = useState(true); const [estimatesError, setEstimatesError] = useState(null); // Timetable data state const [timetableData, setTimetableData] = useState([]); const [timetableLoading, setTimetableLoading] = useState(true); const [timetableError, setTimetableError] = useState(null); const [favourited, setFavourited] = useState(false); const [isManualRefreshing, setIsManualRefreshing] = useState(false); const { tableStyle } = useApp(); 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 loadEstimatesData = useCallback(async () => { try { setEstimatesLoading(true); setEstimatesError(null); const body = await loadData(params.id!); setData(body); setDataDate(new Date()); setCustomName(StopDataProvider.getCustomName(stopIdNum)); } catch (error) { console.error('Error loading estimates data:', error); setEstimatesError(parseError(error)); setData(null); setDataDate(null); } finally { setEstimatesLoading(false); } }, [params.id, stopIdNum]); const loadTimetableDataAsync = useCallback(async () => { try { setTimetableLoading(true); setTimetableError(null); const timetableBody = await loadTimetableData(params.id!); setTimetableData(timetableBody); } catch (error) { console.error('Error loading timetable data:', error); setTimetableError(parseError(error)); setTimetableData([]); } finally { setTimetableLoading(false); } }, [params.id]); const refreshData = useCallback(async () => { await Promise.all([ loadEstimatesData(), loadTimetableDataAsync() ]); }, [loadEstimatesData, loadTimetableDataAsync]); // Manual refresh function for pull-to-refresh and button const handleManualRefresh = useCallback(async () => { try { setIsManualRefreshing(true); // Only reload real-time estimates data, not timetable await loadEstimatesData(); } finally { setIsManualRefreshing(false); } }, [loadEstimatesData]); // Auto-refresh estimates data every 30 seconds (only if not in error state) useAutoRefresh({ onRefresh: loadEstimatesData, interval: 30000, enabled: !estimatesError, }); useEffect(() => { // Initial load loadEstimatesData(); loadTimetableDataAsync(); StopDataProvider.pushRecent(parseInt(params.id ?? "")); setFavourited(StopDataProvider.isFavourite(parseInt(params.id ?? ""))); }, [params.id, loadEstimatesData, loadTimetableDataAsync]); const toggleFavourite = () => { if (favourited) { StopDataProvider.removeFavourite(stopIdNum); setFavourited(false); } else { StopDataProvider.addFavourite(stopIdNum); setFavourited(true); } }; const handleRename = () => { const current = customName ?? data?.stop.name; const input = window.prompt("Custom name for this stop:", current); if (input === null) return; // cancelled const trimmed = input.trim(); if (trimmed === "") { StopDataProvider.removeCustomName(stopIdNum); setCustomName(undefined); } else { StopDataProvider.setCustomName(stopIdNum, trimmed); setCustomName(trimmed); } }; // Show loading skeleton while initial data is loading if (estimatesLoading && !data) { return (

{t("common.loading")}...

{tableStyle === "grouped" ? ( ) : ( )}
); } return (

{customName ?? data?.stop.name ?? `Parada ${stopIdNum}`}{" "} ({data?.stop.id ?? stopIdNum})

{estimatesLoading ? ( tableStyle === "grouped" ? ( ) : ( ) ) : estimatesError ? ( ) : data ? ( tableStyle === "grouped" ? ( ) : ( ) ) : null}
{timetableLoading ? ( ) : timetableError ? ( ) : timetableData.length > 0 ? ( <>
{t("timetable.viewAll", "Ver todos los horarios")}
) : null}
); }