From 80bcf4a5f29ab926c2208d5efb4c19087c600323 Mon Sep 17 00:00:00 2001
From: Ariel Costas Guerrero <ariel@costas.dev>
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<StopDetails> => {
+  // 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<StopSheetProps> = ({
   const { t } = useTranslation();
   const [data, setData] = useState<StopDetails | null>(null);
   const [loading, setLoading] = useState(false);
+  const [error, setError] = useState<ErrorInfo | null>(null);
+  const [lastUpdated, setLastUpdated] = useState<Date | null>(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<StopSheetProps> = ({
     <Sheet
       isOpen={isOpen}
       onClose={onClose}
-      detent="content-height"
+      detent={"content-height" as any}
     >
       <Sheet.Container>
         <Sheet.Header />
@@ -92,13 +135,16 @@ export const StopSheet: React.FC<StopSheetProps> = ({
               <span className="stop-sheet-id">({stopId})</span>
             </div>
 
-            {loading && (
-              <div className="stop-sheet-loading">
-                {t("common.loading", "Loading...")}
-              </div>
-            )}
-
-            {data && !loading && (
+            {loading ? (
+              <StopSheetSkeleton />
+            ) : error ? (
+              <ErrorDisplay 
+                error={error} 
+                onRetry={loadData}
+                title={t("errors.estimates_title", "Error al cargar estimaciones")}
+                className="compact"
+              />
+            ) : data ? (
               <>
                 <div className="stop-sheet-estimates">
                   <h3 className="stop-sheet-subtitle">
@@ -136,15 +182,40 @@ export const StopSheet: React.FC<StopSheetProps> = ({
                   )}
                 </div>
 
-                <Link
-                  to={`/estimates/${stopId}`}
-                  className="stop-sheet-view-all"
-                  onClick={onClose}
-                >
-                  {t("map.view_all_estimates", "Ver todas las estimaciones")}
-                </Link>
+                <div className="stop-sheet-footer">
+                  {lastUpdated && (
+                    <div className="stop-sheet-timestamp">
+                      {t("estimates.last_updated", "Actualizado a las")}{" "}
+                      {lastUpdated.toLocaleTimeString(undefined, {
+                        hour: "2-digit",
+                        minute: "2-digit",
+                        second: "2-digit"
+                      })}
+                    </div>
+                  )}
+                  
+                  <div className="stop-sheet-actions">
+                    <button 
+                      className="stop-sheet-reload"
+                      onClick={loadData}
+                      disabled={loading}
+                      title={t("estimates.reload", "Recargar estimaciones")}
+                    >
+                      <RefreshCw className={`reload-icon ${loading ? 'spinning' : ''}`} />
+                      {t("estimates.reload", "Recargar")}
+                    </button>
+                    
+                    <Link
+                      to={`/estimates/${stopId}`}
+                      className="stop-sheet-view-all"
+                      onClick={onClose}
+                    >
+                      {t("map.view_all_estimates", "Ver todas las estimaciones")}
+                    </Link>
+                  </div>
+                </div>
               </>
-            )}
+            ) : null}
           </div>
         </Sheet.Content>
       </Sheet.Container>
-- 
cgit v1.3

