From 80bcf4a5f29ab926c2208d5efb4c19087c600323 Mon Sep 17 00:00:00 2001 From: Ariel Costas Guerrero Date: Sun, 7 Sep 2025 19:22:28 +0200 Subject: feat: Enhance StopSheet component with error handling and loading states - Added skeleton loading state to StopSheet for better UX during data fetch. - Implemented error handling with descriptive messages for network and server errors. - Introduced manual refresh functionality to reload stop estimates. - Updated styles for loading and error states. - Created StopSheetSkeleton and TimetableSkeleton components for consistent loading indicators. feat: Improve StopList component with loading indicators and network data fetching - Integrated loading state for StopList while fetching stops from the network. - Added skeleton loading indicators for favourite and recent stops. - Refactored data fetching logic to include favourite and recent stops with full data. - Enhanced user experience with better loading and error handling. feat: Update Timetable component with loading and error handling - Added loading skeletons to Timetable for improved user experience. - Implemented error handling for timetable data fetching. - Refactored data loading logic to handle errors gracefully and provide retry options. chore: Update package dependencies - Upgraded react-router, lucide-react, and other dependencies to their latest versions. - Updated types for TypeScript compatibility. --- src/frontend/app/routes/timetable-$id.tsx | 124 ++++++++++++++++++++++-------- 1 file changed, 92 insertions(+), 32 deletions(-) (limited to 'src/frontend/app/routes/timetable-$id.tsx') diff --git a/src/frontend/app/routes/timetable-$id.tsx b/src/frontend/app/routes/timetable-$id.tsx index 073dddb..cb55f53 100644 --- a/src/frontend/app/routes/timetable-$id.tsx +++ b/src/frontend/app/routes/timetable-$id.tsx @@ -3,26 +3,34 @@ import { useParams, Link } from "react-router"; import StopDataProvider from "../data/StopDataProvider"; import { ArrowLeft, Eye, EyeOff } from "lucide-react"; import { TimetableTable, type TimetableEntry } from "../components/TimetableTable"; +import { TimetableSkeleton } from "../components/TimetableSkeleton"; +import { ErrorDisplay } from "../components/ErrorDisplay"; import LineIcon from "../components/LineIcon"; import { useTranslation } from "react-i18next"; import "./timetable-$id.css"; -const loadTimetableData = async (stopId: string) => { +interface ErrorInfo { + type: 'network' | 'server' | 'unknown'; + status?: number; + message?: string; +} + +const loadTimetableData = async (stopId: string): Promise => { + // Add delay to see skeletons in action (remove in production) + await new Promise(resolve => setTimeout(resolve, 1000)); + const today = new Date().toISOString().split('T')[0]; // YYYY-MM-DD format - try { - const resp = await fetch(`/api/GetStopTimetable?date=${today}&stopId=${stopId}`, { - headers: { - Accept: "application/json", - }, - }); - if (!resp.ok) { - throw new Error(`HTTP error! status: ${resp.status}`); - } - return await resp.json(); - } catch (error) { - console.error('Error loading timetable data:', error); - return []; + 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(); }; // Utility function to compare times @@ -96,20 +104,40 @@ export default function Timetable() { const [timetableData, setTimetableData] = useState([]); const [customName, setCustomName] = useState(undefined); const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); + const [error, setError] = useState(null); const [showPastEntries, setShowPastEntries] = useState(false); const nextEntryRef = useRef(null); const currentTime = new Date().toTimeString().slice(0, 8); // HH:MM:SS const filteredData = filterTimetableData(timetableData, currentTime, showPastEntries); - useEffect(() => { - loadTimetableData(params.id!).then((timetableBody: TimetableEntry[]) => { + 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); + + const timetableBody = await loadTimetableData(params.id!); setTimetableData(timetableBody); - setLoading(false); - if (timetableBody.length === 0) { - setError(t("timetable.noDataAvailable", "No hay datos de horarios disponibles para hoy")); - } else { + + if (timetableBody.length > 0) { // Scroll to next entry after a short delay to allow rendering setTimeout(() => { const currentMinutes = timeToMinutes(currentTime); @@ -129,26 +157,50 @@ export default function Timetable() { } }, 500); } - }).catch((err) => { - setError(t("timetable.loadError", "Error al cargar los horarios")); + } catch (err) { + console.error('Error loading timetable data:', err); + setError(parseError(err)); + setTimetableData([]); + } finally { setLoading(false); - }); + } + }; + useEffect(() => { + loadData(); setCustomName(StopDataProvider.getCustomName(stopIdNum)); - }, [params.id, stopIdNum, t, currentTime]); + }, [params.id]); if (loading) { - return

{t("common.loading")}

; - } + return ( +
+
+

+ {t("timetable.fullTitle", "Horarios teóricos")} ({params.id}) +

+ + + {t("timetable.backToEstimates", "Volver a estimaciones")} + +
+ +
+
+ +
- // Get stop name from timetable data or use stop ID - const stopName = customName || - (timetableData.length > 0 ? `Parada ${params.id}` : `Parada ${params.id}`); + +
+
+ ); + } return (
-

{t("timetable.fullTitle", "Horarios teóricos")} ({params.id})

@@ -159,8 +211,16 @@ export default function Timetable() {
{error ? ( +
+ +
+ ) : timetableData.length === 0 ? (
-

{error}

+

{t("timetable.noDataAvailable", "No hay datos de horarios disponibles para hoy")}

{t("timetable.errorDetail", "Los horarios teóricos se actualizan diariamente. Inténtalo más tarde.")}

-- cgit v1.3