diff options
Diffstat (limited to 'src/frontend/app/components/StopSheet.tsx')
| -rw-r--r-- | src/frontend/app/components/StopSheet.tsx | 127 |
1 files changed, 99 insertions, 28 deletions
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> |
