aboutsummaryrefslogtreecommitdiff
path: root/src/frontend/app/routes/timetable-$id.tsx
diff options
context:
space:
mode:
authorAriel Costas Guerrero <ariel@costas.dev>2025-09-07 19:22:28 +0200
committerAriel Costas Guerrero <ariel@costas.dev>2025-09-07 19:22:28 +0200
commit80bcf4a5f29ab926c2208d5efb4c19087c600323 (patch)
tree1e5826b29d8a22e057616e16069232f95788f3ba /src/frontend/app/routes/timetable-$id.tsx
parent8182a08f60e88595984ba80b472f29ccf53c19bd (diff)
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.
Diffstat (limited to 'src/frontend/app/routes/timetable-$id.tsx')
-rw-r--r--src/frontend/app/routes/timetable-$id.tsx124
1 files changed, 92 insertions, 32 deletions
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<TimetableEntry[]> => {
+ // 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<TimetableEntry[]>([]);
const [customName, setCustomName] = useState<string | undefined>(undefined);
const [loading, setLoading] = useState(true);
- const [error, setError] = useState<string | null>(null);
+ const [error, setError] = useState<ErrorInfo | null>(null);
const [showPastEntries, setShowPastEntries] = useState(false);
const nextEntryRef = useRef<HTMLDivElement>(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 <h1 className="page-title">{t("common.loading")}</h1>;
- }
+ return (
+ <div className="page-container">
+ <div className="timetable-full-header">
+ <h1 className="page-title">
+ {t("timetable.fullTitle", "Horarios teóricos")} ({params.id})
+ </h1>
+ <Link to={`/estimates/${params.id}`} className="back-link">
+ <ArrowLeft className="back-icon" />
+ {t("timetable.backToEstimates", "Volver a estimaciones")}
+ </Link>
+ </div>
+
+ <div className="timetable-full-content">
+ <div className="timetable-controls">
+ <button className="past-toggle" disabled>
+ <Eye className="toggle-icon" />
+ {t("timetable.showPast", "Mostrar todos")}
+ </button>
+ </div>
- // Get stop name from timetable data or use stop ID
- const stopName = customName ||
- (timetableData.length > 0 ? `Parada ${params.id}` : `Parada ${params.id}`);
+ <TimetableSkeleton rows={8} />
+ </div>
+ </div>
+ );
+ }
return (
<div className="page-container">
<div className="timetable-full-header">
-
<h1 className="page-title">
{t("timetable.fullTitle", "Horarios teóricos")} ({params.id})
</h1>
@@ -159,8 +211,16 @@ export default function Timetable() {
</div>
{error ? (
+ <div className="timetable-full-content">
+ <ErrorDisplay
+ error={error}
+ onRetry={loadData}
+ title={t("errors.timetable_title", "Error al cargar horarios")}
+ />
+ </div>
+ ) : timetableData.length === 0 ? (
<div className="error-message">
- <p>{error}</p>
+ <p>{t("timetable.noDataAvailable", "No hay datos de horarios disponibles para hoy")}</p>
<p className="error-detail">
{t("timetable.errorDetail", "Los horarios teóricos se actualizan diariamente. Inténtalo más tarde.")}
</p>