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/components/StopSheet.tsx | 127 +++++++++++++++++++++++------- 1 file changed, 99 insertions(+), 28 deletions(-) (limited to 'src/frontend/app/components/StopSheet.tsx') diff --git a/src/frontend/app/components/StopSheet.tsx b/src/frontend/app/components/StopSheet.tsx index 8075e9d..e8000d1 100644 --- a/src/frontend/app/components/StopSheet.tsx +++ b/src/frontend/app/components/StopSheet.tsx @@ -2,7 +2,10 @@ import React, { useEffect, useState } from "react"; import { Sheet } from "react-modal-sheet"; import { Link } from "react-router"; import { useTranslation } from "react-i18next"; +import { RefreshCw } from "lucide-react"; import LineIcon from "./LineIcon"; +import { StopSheetSkeleton } from "./StopSheetSkeleton"; +import { ErrorDisplay } from "./ErrorDisplay"; import { type StopDetails } from "../routes/estimates-$id"; import "./StopSheet.css"; @@ -13,12 +16,26 @@ interface StopSheetProps { stopName: string; } +interface ErrorInfo { + type: 'network' | 'server' | 'unknown'; + status?: number; + message?: string; +} + const loadStopData = async (stopId: number): 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(); }; @@ -31,21 +48,47 @@ export const StopSheet: React.FC = ({ const { t } = useTranslation(); const [data, setData] = useState(null); const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [lastUpdated, setLastUpdated] = useState(null); - useEffect(() => { - if (isOpen && stopId) { + 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); - loadStopData(stopId) - .then((stopData) => { - setData(stopData); - }) - .catch((error) => { - console.error("Failed to load stop data:", error); - }) - .finally(() => { - setLoading(false); - }); + + const stopData = await loadStopData(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 && stopId) { + loadData(); } }, [isOpen, stopId]); @@ -81,7 +124,7 @@ export const StopSheet: React.FC = ({ @@ -92,13 +135,16 @@ export const StopSheet: React.FC = ({ ({stopId}) - {loading && ( -
- {t("common.loading", "Loading...")} -
- )} - - {data && !loading && ( + {loading ? ( + + ) : error ? ( + + ) : data ? ( <>

@@ -136,15 +182,40 @@ export const StopSheet: React.FC = ({ )}

- - {t("map.view_all_estimates", "Ver todas las estimaciones")} - +
+ {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}
-- cgit v1.3