aboutsummaryrefslogtreecommitdiff
path: root/src/frontend/app/routes/stoplist.tsx
diff options
context:
space:
mode:
authorAriel Costas Guerrero <ariel@costas.dev>2025-09-07 19:22:28 +0200
committerAriel Costas Guerrero <ariel@costas.dev>2025-09-07 19:22:28 +0200
commit80bcf4a5f29ab926c2208d5efb4c19087c600323 (patch)
tree1e5826b29d8a22e057616e16069232f95788f3ba /src/frontend/app/routes/stoplist.tsx
parent8182a08f60e88595984ba80b472f29ccf53c19bd (diff)
feat: Enhance StopSheet component with error handling and loading states
- Added skeleton loading state to StopSheet for better UX during data fetch. - Implemented error handling with descriptive messages for network and server errors. - Introduced manual refresh functionality to reload stop estimates. - Updated styles for loading and error states. - Created StopSheetSkeleton and TimetableSkeleton components for consistent loading indicators. feat: Improve StopList component with loading indicators and network data fetching - Integrated loading state for StopList while fetching stops from the network. - Added skeleton loading indicators for favourite and recent stops. - Refactored data fetching logic to include favourite and recent stops with full data. - Enhanced user experience with better loading and error handling. feat: Update Timetable component with loading and error handling - Added loading skeletons to Timetable for improved user experience. - Implemented error handling for timetable data fetching. - Refactored data loading logic to handle errors gracefully and provide retry options. chore: Update package dependencies - Upgraded react-router, lucide-react, and other dependencies to their latest versions. - Updated types for TypeScript compatibility.
Diffstat (limited to 'src/frontend/app/routes/stoplist.tsx')
-rw-r--r--src/frontend/app/routes/stoplist.tsx100
1 files changed, 70 insertions, 30 deletions
diff --git a/src/frontend/app/routes/stoplist.tsx b/src/frontend/app/routes/stoplist.tsx
index 885a0da..8b0ebe2 100644
--- a/src/frontend/app/routes/stoplist.tsx
+++ b/src/frontend/app/routes/stoplist.tsx
@@ -1,6 +1,7 @@
import { useEffect, useMemo, useRef, useState, useCallback } from "react";
import StopDataProvider, { type Stop } from "../data/StopDataProvider";
import StopItem from "../components/StopItem";
+import StopItemSkeleton from "../components/StopItemSkeleton";
import Fuse from "fuse.js";
import "./stoplist.css";
import { useTranslation } from "react-i18next";
@@ -8,21 +9,63 @@ import { useTranslation } from "react-i18next";
export default function StopList() {
const { t } = useTranslation();
const [data, setData] = useState<Stop[] | null>(null);
+ const [loading, setLoading] = useState(true);
const [searchResults, setSearchResults] = useState<Stop[] | null>(null);
+ const [favouriteIds, setFavouriteIds] = useState<number[]>([]);
+ const [recentIds, setRecentIds] = useState<number[]>([]);
+ const [favouriteStops, setFavouriteStops] = useState<Stop[]>([]);
+ const [recentStops, setRecentStops] = useState<Stop[]>([]);
const searchTimeout = useRef<NodeJS.Timeout | null>(null);
const randomPlaceholder = useMemo(
() => t("stoplist.search_placeholder"),
[t],
);
+
const fuse = useMemo(
() => new Fuse(data || [], { threshold: 0.3, keys: ["name.original"] }),
[data],
);
+ // Load favourite and recent IDs immediately from localStorage
+ useEffect(() => {
+ setFavouriteIds(StopDataProvider.getFavouriteIds());
+ setRecentIds(StopDataProvider.getRecent());
+ }, []);
+
+ // Load stops from network
const loadStops = useCallback(async () => {
- const stops = await StopDataProvider.getStops();
- setData(stops);
+ try {
+ setLoading(true);
+
+ const stops = await StopDataProvider.loadStopsFromNetwork();
+
+ // Add favourite flags to stops
+ const favouriteStopsIds = StopDataProvider.getFavouriteIds();
+ const stopsWithFavourites = stops.map(stop => ({
+ ...stop,
+ favourite: favouriteStopsIds.includes(stop.stopId)
+ }));
+
+ setData(stopsWithFavourites);
+
+ // Update favourite and recent stops with full data
+ const favStops = stopsWithFavourites.filter(stop =>
+ favouriteStopsIds.includes(stop.stopId)
+ );
+ setFavouriteStops(favStops);
+
+ const recIds = StopDataProvider.getRecent();
+ const recStops = recIds
+ .map(id => stopsWithFavourites.find(stop => stop.stopId === id))
+ .filter(Boolean) as Stop[];
+ setRecentStops(recStops.reverse());
+
+ } catch (error) {
+ console.error("Failed to load stops:", error);
+ } finally {
+ setLoading(false);
+ }
}, []);
useEffect(() => {
@@ -53,26 +96,6 @@ export default function StopList() {
}, 300);
};
- const favouritedStops = useMemo(() => {
- return data?.filter((stop) => stop.favourite) ?? [];
- }, [data]);
-
- const recentStops = useMemo(() => {
- // no recent items if data not loaded
- if (!data) return null;
- const recentIds = StopDataProvider.getRecent();
- if (recentIds.length === 0) return null;
- // map and filter out missing entries
- const stopsList = recentIds
- .map((id) => data.find((stop) => stop.stopId === id))
- .filter((s): s is Stop => Boolean(s));
- return stopsList.reverse();
- }, [data]);
-
- if (data === null) {
- return <h1 className="page-title">{t("common.loading")}</h1>;
- }
-
return (
<div className="page-container stoplist-page">
<h1 className="page-title">UrbanoVigo Web</h1>
@@ -108,7 +131,7 @@ export default function StopList() {
<div className="list-container">
<h2 className="page-subtitle">{t("stoplist.favourites")}</h2>
- {favouritedStops?.length === 0 && (
+ {favouriteIds.length === 0 && (
<p className="message">
{t(
"stoplist.no_favourites",
@@ -118,18 +141,28 @@ export default function StopList() {
)}
<ul className="list">
- {favouritedStops
- ?.sort((a, b) => a.stopId - b.stopId)
- .map((stop: Stop) => <StopItem key={stop.stopId} stop={stop} />)}
+ {loading && favouriteIds.length > 0 &&
+ favouriteIds.map((id) => (
+ <StopItemSkeleton key={id} showId={true} stopId={id} />
+ ))
+ }
+ {!loading && favouriteStops
+ .sort((a, b) => a.stopId - b.stopId)
+ .map((stop) => <StopItem key={stop.stopId} stop={stop} />)}
</ul>
</div>
- {recentStops && recentStops.length > 0 && (
+ {(recentIds.length > 0 || (!loading && recentStops.length > 0)) && (
<div className="list-container">
<h2 className="page-subtitle">{t("stoplist.recents")}</h2>
<ul className="list">
- {recentStops.map((stop: Stop) => (
+ {loading && recentIds.length > 0 &&
+ recentIds.map((id) => (
+ <StopItemSkeleton key={id} showId={true} stopId={id} />
+ ))
+ }
+ {!loading && recentStops.map((stop) => (
<StopItem key={stop.stopId} stop={stop} />
))}
</ul>
@@ -140,9 +173,16 @@ export default function StopList() {
<h2 className="page-subtitle">{t("stoplist.all_stops", "Paradas")}</h2>
<ul className="list">
- {data
+ {loading && (
+ <>
+ {Array.from({ length: 8 }, (_, index) => (
+ <StopItemSkeleton key={`skeleton-${index}`} />
+ ))}
+ </>
+ )}
+ {!loading && data
?.sort((a, b) => a.stopId - b.stopId)
- .map((stop: Stop) => <StopItem key={stop.stopId} stop={stop} />)}
+ .map((stop) => <StopItem key={stop.stopId} stop={stop} />)}
</ul>
</div>
</div>