aboutsummaryrefslogtreecommitdiff
path: root/src/frontend/app/components/StopSummarySheet.tsx
diff options
context:
space:
mode:
authorAriel Costas Guerrero <ariel@costas.dev>2025-12-01 22:25:56 +0100
committerAriel Costas Guerrero <ariel@costas.dev>2025-12-01 22:25:56 +0100
commit3227c7bc6bd233c92b1cf54bec689f0582dca547 (patch)
tree66405a8f51a4abe826f268009f2ed461e434df83 /src/frontend/app/components/StopSummarySheet.tsx
parent6d15a6113d1c467d1a6113eea052882f4037dcf2 (diff)
refactor: replace StopSheet with StopSummarySheet and update related components
- Deleted StopSheet and StopSheetSkeleton components. - Introduced StopSummarySheet and StopSummarySheetSkeleton components. - Updated ConsolidatedCirculationCard to support a reduced view. - Modified ConsolidatedCirculationList to accept a reduced prop. - Adjusted map route to use StopSummarySheet. - Cleaned up CSS styles related to the stop sheet components. - Enhanced error handling and loading states in the new summary sheet. - Updated stop report logic to filter out empty next streets.
Diffstat (limited to 'src/frontend/app/components/StopSummarySheet.tsx')
-rw-r--r--src/frontend/app/components/StopSummarySheet.tsx208
1 files changed, 208 insertions, 0 deletions
diff --git a/src/frontend/app/components/StopSummarySheet.tsx b/src/frontend/app/components/StopSummarySheet.tsx
new file mode 100644
index 0000000..17c0afd
--- /dev/null
+++ b/src/frontend/app/components/StopSummarySheet.tsx
@@ -0,0 +1,208 @@
+import { RefreshCw } from "lucide-react";
+import React, { useEffect, useState } from "react";
+import { useTranslation } from "react-i18next";
+import { Sheet } from "react-modal-sheet";
+import { Link } from "react-router";
+import { ConsolidatedCirculationList } from "~/components/Stops/ConsolidatedCirculationList";
+import { REGION_DATA } from "~/config/RegionConfig";
+import type { Stop } from "~/data/StopDataProvider";
+import { type ConsolidatedCirculation } from "../routes/stops-$id";
+import { ErrorDisplay } from "./ErrorDisplay";
+import LineIcon from "./LineIcon";
+import { StopAlert } from "./StopAlert";
+import "./StopSummarySheet.css";
+import { StopSummarySheetSkeleton } from "./StopSummarySheetSkeleton";
+
+interface StopSheetProps {
+ isOpen: boolean;
+ onClose: () => void;
+ stop: Stop;
+}
+
+interface ErrorInfo {
+ type: "network" | "server" | "unknown";
+ status?: number;
+ message?: string;
+}
+
+const loadConsolidatedData = async (
+ stopId: number
+): Promise<ConsolidatedCirculation[]> => {
+ const resp = await fetch(
+ `${REGION_DATA.consolidatedCirculationsEndpoint}?stopId=${stopId}`,
+ {
+ headers: {
+ Accept: "application/json",
+ },
+ }
+ );
+
+ if (!resp.ok) {
+ throw new Error(`HTTP ${resp.status}: ${resp.statusText}`);
+ }
+
+ return await resp.json();
+};
+
+export const StopSheet: React.FC<StopSheetProps> = ({
+ isOpen,
+ onClose,
+ stop,
+}) => {
+ const { t } = useTranslation();
+ const [data, setData] = useState<ConsolidatedCirculation[] | null>(null);
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState<ErrorInfo | null>(null);
+ const [lastUpdated, setLastUpdated] = useState<Date | null>(null);
+
+ 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);
+
+ const stopData = await loadConsolidatedData(stop.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 && stop.stopId) {
+ loadData();
+ }
+ }, [isOpen, stop.stopId]);
+
+ // Show only the next 4 arrivals
+ const sortedData = data
+ ? [...data].sort(
+ (a, b) =>
+ (a.realTime?.minutes ?? a.schedule?.minutes ?? 999) -
+ (b.realTime?.minutes ?? b.schedule?.minutes ?? 999)
+ )
+ : [];
+ const limitedEstimates = sortedData.slice(0, 4);
+
+ return (
+ <Sheet isOpen={isOpen} onClose={onClose} detent="content">
+ <Sheet.Container>
+ <Sheet.Header />
+ <Sheet.Content drag="y">
+ <div className="stop-sheet-content">
+ <div className="stop-sheet-header">
+ <h2 className="stop-sheet-title">{stop.name.original}</h2>
+ <span className="stop-sheet-id">({stop.stopId})</span>
+ </div>
+
+ <div
+ className={`stop-sheet-lines-container ${stop.lines.length >= 10 ? "scrollable" : ""}`}
+ >
+ {stop.lines.map((line) => (
+ <div key={line} className="stop-sheet-line-icon">
+ <LineIcon line={line} mode="rounded" />
+ </div>
+ ))}
+ </div>
+
+ <StopAlert stop={stop} compact />
+
+ {loading ? (
+ <StopSummarySheetSkeleton />
+ ) : 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">
+ {t("estimates.next_arrivals", "Next arrivals")}
+ </h3>
+
+ {limitedEstimates.length === 0 ? (
+ <div className="stop-sheet-no-estimates">
+ {t("estimates.none", "No hay estimaciones disponibles")}
+ </div>
+ ) : (
+ <ConsolidatedCirculationList
+ data={data.slice(0, 4)}
+ reduced
+ />
+ )}
+ </div>
+ </>
+ ) : null}
+ </div>
+ </Sheet.Content>
+
+ <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={`/stops/${stop.stopId}`}
+ className="stop-sheet-view-all"
+ onClick={onClose}
+ >
+ {t("map.view_all_estimates", "Ver todas las estimaciones")}
+ </Link>
+ </div>
+ </div>
+ </Sheet.Container>
+ <Sheet.Backdrop onTap={onClose} />
+ </Sheet>
+ );
+};