summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAriel Costas Guerrero <ariel@costas.dev>2026-03-08 23:12:05 +0100
committerAriel Costas Guerrero <ariel@costas.dev>2026-03-08 23:12:13 +0100
commit854be328986a09460249a55dbac3af26530c7b29 (patch)
treedc5d9d63a4e54cb08a5520230b3343882594c097
parent2063f8101b1c887e079e11c96755a2441aa1b57b (diff)
Enhance StopItem component to display next arrivals and improve layout; update StopList to show arrivals for the first three stops
-rw-r--r--src/frontend/app/components/StopItem.tsx119
-rw-r--r--src/frontend/app/routes/home.tsx82
2 files changed, 161 insertions, 40 deletions
diff --git a/src/frontend/app/components/StopItem.tsx b/src/frontend/app/components/StopItem.tsx
index 35ccf6d..362798e 100644
--- a/src/frontend/app/components/StopItem.tsx
+++ b/src/frontend/app/components/StopItem.tsx
@@ -1,37 +1,112 @@
-import React from "react";
+import { Clock } from "lucide-react";
+import React, { useEffect, useState } from "react";
+import { useTranslation } from "react-i18next";
import { Link } from "react-router";
+import { fetchArrivals } from "../api/arrivals";
+import { type Arrival } from "../api/schema";
import StopDataProvider, { type Stop } from "../data/StopDataProvider";
import RouteIcon from "./RouteIcon";
interface StopItemProps {
stop: Stop;
+ showArrivals?: boolean;
}
-const StopItem: React.FC<StopItemProps> = ({ stop }) => {
+const StopItem: React.FC<StopItemProps> = ({ stop, showArrivals }) => {
+ const { t } = useTranslation();
+ const [arrivals, setArrivals] = useState<Arrival[] | null>(null);
+
+ useEffect(() => {
+ let mounted = true;
+ if (showArrivals) {
+ fetchArrivals(stop.stopId, true)
+ .then((res) => {
+ if (mounted) {
+ setArrivals(res.arrivals.slice(0, 3));
+ }
+ })
+ .catch(console.error);
+ }
+ return () => {
+ mounted = false;
+ };
+ }, [showArrivals, stop.stopId]);
+
return (
- <li className="pb-3 border-b border-gray-200 dark:border-gray-700 md:border md:border-gray-300 dark:md:border-gray-700 md:rounded-lg md:p-3 md:pb-3">
+ <li>
<Link
- className="block text-gray-900 dark:text-gray-100 no-underline hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-200"
to={`/stops/${stop.stopId}`}
+ className="flex items-center gap-x-4 gap-y-3 rounded-xl p-3 transition-all bg-slate-50 dark:bg-slate-800 border border-gray-200 dark:border-gray-700 shadow-sm hover:border-blue-400 dark:hover:border-blue-500 active:scale-[0.98] cursor-pointer"
>
- <div className="flex justify-between items-baseline">
- <span className="font-semibold">
- {stop.favourite && <span className="text-yellow-500 mr-1">★</span>}
- {StopDataProvider.getDisplayName(stop)}
- </span>
- <span className="text-sm text-gray-600 dark:text-gray-400 ml-2">
- ({stop.stopCode || stop.stopId})
- </span>
- </div>
- <div className="flex flex-wrap gap-1 mt-1">
- {stop.lines?.map((lineObj) => (
- <RouteIcon
- key={lineObj.line}
- line={lineObj.line}
- colour={lineObj.colour}
- textColour={lineObj.textColour}
- />
- ))}
+ <div className="flex-1 min-w-0 flex flex-col gap-1">
+ <div className="flex justify-between items-start gap-2">
+ <span className="text-base font-bold overflow-hidden text-ellipsis line-clamp-2 leading-tight text-slate-900 dark:text-slate-100">
+ {stop.favourite && (
+ <span className="text-yellow-500 mr-2">★</span>
+ )}
+ {StopDataProvider.getDisplayName(stop)}
+ </span>
+ </div>
+
+ <div className="text-xs flex items-center gap-1.5 text-slate-500 dark:text-slate-400 font-mono uppercase">
+ <span className="px-1.5 py-0.5 rounded flex items-center justify-center bg-slate-200 dark:bg-slate-700 text-[10px] font-bold text-slate-700 dark:text-slate-300 leading-none">
+ {stop.stopId.split(":")[0]}
+ </span>
+ <span>
+ {stop.stopCode || stop.stopId.split(":")[1] || stop.stopId}
+ </span>
+ </div>
+
+ {stop.lines && stop.lines.length > 0 && (
+ <div className="flex flex-wrap gap-1 mt-1">
+ {stop.lines.map((lineObj) => (
+ <RouteIcon
+ key={lineObj.line}
+ line={lineObj.line}
+ colour={lineObj.colour}
+ textColour={lineObj.textColour}
+ />
+ ))}
+ </div>
+ )}
+
+ {showArrivals && arrivals && arrivals.length > 0 && (
+ <div className="flex flex-col gap-1 mt-2 p-2 bg-slate-100 dark:bg-slate-900 rounded-lg border border-slate-200 dark:border-slate-800">
+ <div className="flex items-center gap-1.5 mb-1 opacity-70">
+ <Clock className="w-3 h-3" />
+ <span className="text-[10px] font-bold uppercase tracking-wider">
+ {t("estimates.next_arrivals", "Próximas llegadas")}
+ </span>
+ </div>
+ {arrivals.map((arr, i) => (
+ <div key={i} className="flex items-center gap-2 text-sm">
+ <div className="shrink-0">
+ <RouteIcon
+ line={arr.route.shortName}
+ colour={arr.route.colour}
+ textColour={arr.route.textColour}
+ />
+ </div>
+ <span className="flex-1 truncate text-xs font-medium text-slate-700 dark:text-slate-300">
+ {arr.headsign.destination}
+ </span>
+ <span
+ className={`text-xs pr-1 font-bold ${
+ arr.estimate.precision === "confident"
+ ? "text-green-600 dark:text-green-500"
+ : arr.estimate.precision === "unsure"
+ ? "text-orange-600 dark:text-orange-500"
+ : arr.estimate.precision === "past"
+ ? "text-gray-500 line-through"
+ : "text-blue-600 dark:text-blue-400"
+ }`}
+ >
+ {arr.estimate.minutes}&apos;
+ </span>
+ </div>
+ ))}
+ </div>
+ )}
</div>
</Link>
</li>
diff --git a/src/frontend/app/routes/home.tsx b/src/frontend/app/routes/home.tsx
index 973a534..0229ad5 100644
--- a/src/frontend/app/routes/home.tsx
+++ b/src/frontend/app/routes/home.tsx
@@ -1,11 +1,10 @@
-import { History } from "lucide-react";
+import { Clock, History, Star } from "lucide-react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router";
import { PlannerOverlay } from "~/components/PlannerOverlay";
import { usePageTitle } from "~/contexts/PageTitleContext";
import { usePlanner } from "~/hooks/usePlanner";
-import StopGallery from "../components/StopGallery";
import StopItem from "../components/StopItem";
import StopDataProvider, { type Stop } from "../data/StopDataProvider";
import "../tailwind-full.css";
@@ -246,8 +245,12 @@ export default function StopList() {
{t("stoplist.search_results", "Resultados de la búsqueda")}
</h2>
<ul className="list-none p-0 m-0 flex flex-col gap-2 md:grid md:grid-cols-[repeat(auto-fill,minmax(300px,1fr))] lg:grid-cols-[repeat(auto-fill,minmax(320px,1fr))]">
- {searchResults.map((stop: Stop) => (
- <StopItem key={stop.stopId} stop={stop} />
+ {searchResults.map((stop: Stop, index) => (
+ <StopItem
+ key={stop.stopId}
+ stop={stop}
+ showArrivals={index < 3}
+ />
))}
</ul>
</div>
@@ -259,25 +262,68 @@ export default function StopList() {
</div>
) : (
<>
- {/* Favourites Gallery */}
- {!loading && (
- <StopGallery
- stops={favouriteStops.sort((a, b) =>
- a.stopId.localeCompare(b.stopId)
- )}
- title={t("stoplist.favourites")}
- emptyMessage={t("stoplist.no_favourites")}
- />
+ {/* Favourites List */}
+ {!loading && favouriteStops.length > 0 && (
+ <div className="w-full px-4 flex flex-col gap-2">
+ <div className="flex items-center gap-2 mb-1 pl-1">
+ <Star className="text-yellow-500 w-4 h-4" />
+ <h3 className="text-xs font-bold uppercase tracking-wider text-muted m-0">
+ {t("stoplist.favourites")}
+ </h3>
+ </div>
+ <ul className="list-none p-0 m-0 flex flex-col gap-2 md:grid md:grid-cols-[repeat(auto-fill,minmax(300px,1fr))] lg:grid-cols-[repeat(auto-fill,minmax(320px,1fr))]">
+ {favouriteStops
+ .sort((a, b) => a.stopId.localeCompare(b.stopId))
+ .map((stop, index) => (
+ <StopItem
+ key={stop.stopId}
+ stop={stop}
+ showArrivals={index < 3}
+ />
+ ))}
+ </ul>
+ </div>
)}
- {/* Recent Stops Gallery - only show if no favourites */}
{!loading && favouriteStops.length === 0 && (
- <StopGallery
- stops={recentStops.slice(0, 5)}
- title={t("stoplist.recents")}
- />
+ <div className="w-full px-4 flex flex-col gap-2">
+ <div className="flex items-center gap-2 mb-1 pl-1">
+ <Star className="text-yellow-500 w-4 h-4" />
+ <h3 className="text-xs font-bold uppercase tracking-wider text-muted m-0">
+ {t("stoplist.favourites")}
+ </h3>
+ </div>
+ <div className="text-center bg-surface border border-slate-200 dark:border-slate-700 shadow-sm rounded-xl p-4">
+ <p className="text-sm text-muted">
+ {t("stoplist.no_favourites")}
+ </p>
+ </div>
+ </div>
)}
+ {/* Recent Stops List - only show if no favourites */}
+ {!loading &&
+ favouriteStops.length === 0 &&
+ recentStops.length > 0 && (
+ <div className="w-full px-4 flex flex-col gap-2 mt-4">
+ <div className="flex items-center gap-2 mb-1 pl-1">
+ <Clock className="text-blue-500 w-4 h-4" />
+ <h3 className="text-xs font-bold uppercase tracking-wider text-muted m-0">
+ {t("stoplist.recents")}
+ </h3>
+ </div>
+ <ul className="list-none p-0 m-0 flex flex-col gap-2 md:grid md:grid-cols-[repeat(auto-fill,minmax(300px,1fr))] lg:grid-cols-[repeat(auto-fill,minmax(320px,1fr))]">
+ {recentStops.slice(0, 5).map((stop, index) => (
+ <StopItem
+ key={stop.stopId}
+ stop={stop}
+ showArrivals={index < 3}
+ />
+ ))}
+ </ul>
+ </div>
+ )}
+
{/*<ServiceAlerts />*/}
</>
)}