From 7b8594debceb93a1fa400d48fe1dcff943bd5af6 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 26 Jun 2025 23:44:25 +0200 Subject: Implement stop sheet modal for map stop interactions (#27) Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: arielcostas <94913521+arielcostas@users.noreply.github.com> Co-authored-by: Ariel Costas Guerrero --- src/frontend/app/components/StopSheet.tsx | 154 ++++++++++++++++++++++++++++++ 1 file changed, 154 insertions(+) create mode 100644 src/frontend/app/components/StopSheet.tsx (limited to 'src/frontend/app/components/StopSheet.tsx') diff --git a/src/frontend/app/components/StopSheet.tsx b/src/frontend/app/components/StopSheet.tsx new file mode 100644 index 0000000..8075e9d --- /dev/null +++ b/src/frontend/app/components/StopSheet.tsx @@ -0,0 +1,154 @@ +import React, { useEffect, useState } from "react"; +import { Sheet } from "react-modal-sheet"; +import { Link } from "react-router"; +import { useTranslation } from "react-i18next"; +import LineIcon from "./LineIcon"; +import { type StopDetails } from "../routes/estimates-$id"; +import "./StopSheet.css"; + +interface StopSheetProps { + isOpen: boolean; + onClose: () => void; + stopId: number; + stopName: string; +} + +const loadStopData = async (stopId: number): Promise => { + const resp = await fetch(`/api/GetStopEstimates?id=${stopId}`, { + headers: { + Accept: "application/json", + }, + }); + return await resp.json(); +}; + +export const StopSheet: React.FC = ({ + isOpen, + onClose, + stopId, + stopName, +}) => { + const { t } = useTranslation(); + const [data, setData] = useState(null); + const [loading, setLoading] = useState(false); + + useEffect(() => { + if (isOpen && stopId) { + setLoading(true); + setData(null); + loadStopData(stopId) + .then((stopData) => { + setData(stopData); + }) + .catch((error) => { + console.error("Failed to load stop data:", error); + }) + .finally(() => { + setLoading(false); + }); + } + }, [isOpen, stopId]); + + const formatTime = (minutes: number) => { + if (minutes > 15) { + const now = new Date(); + const arrival = new Date(now.getTime() + minutes * 60000); + return Intl.DateTimeFormat( + typeof navigator !== "undefined" ? navigator.language : "en", + { + hour: "2-digit", + minute: "2-digit", + } + ).format(arrival); + } else { + return `${minutes} ${t("estimates.minutes", "min")}`; + } + }; + + const formatDistance = (meters: number) => { + if (meters > 1024) { + return `${(meters / 1000).toFixed(1)} km`; + } else { + return `${meters} ${t("estimates.meters", "m")}`; + } + }; + + // Show only the next 4 arrivals + const limitedEstimates = + data?.estimates.sort((a, b) => a.minutes - b.minutes).slice(0, 4) || []; + + return ( + + + + +
+
+

{stopName}

+ ({stopId}) +
+ + {loading && ( +
+ {t("common.loading", "Loading...")} +
+ )} + + {data && !loading && ( + <> +
+

+ {t("estimates.next_arrivals", "Next arrivals")} +

+ + {limitedEstimates.length === 0 ? ( +
+ {t("estimates.none", "No hay estimaciones disponibles")} +
+ ) : ( +
+ {limitedEstimates.map((estimate, idx) => ( +
+
+ +
+
+
+ {estimate.route} +
+
+ {formatTime(estimate.minutes)} + {estimate.meters > -1 && ( + + {" • "} + {formatDistance(estimate.meters)} + + )} +
+
+
+ ))} +
+ )} +
+ + + {t("map.view_all_estimates", "Ver todas las estimaciones")} + + + )} +
+
+
+ +
+ ); +}; -- cgit v1.3