diff options
| author | Ariel Costas Guerrero <ariel@costas.dev> | 2026-03-08 23:12:05 +0100 |
|---|---|---|
| committer | Ariel Costas Guerrero <ariel@costas.dev> | 2026-03-08 23:12:13 +0100 |
| commit | 854be328986a09460249a55dbac3af26530c7b29 (patch) | |
| tree | dc5d9d63a4e54cb08a5520230b3343882594c097 | |
| parent | 2063f8101b1c887e079e11c96755a2441aa1b57b (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.tsx | 119 | ||||
| -rw-r--r-- | src/frontend/app/routes/home.tsx | 82 |
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}' + </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 />*/} </> )} |
