diff options
| author | Ariel Costas Guerrero <ariel@costas.dev> | 2025-09-07 19:22:28 +0200 |
|---|---|---|
| committer | Ariel Costas Guerrero <ariel@costas.dev> | 2025-09-07 19:22:28 +0200 |
| commit | 80bcf4a5f29ab926c2208d5efb4c19087c600323 (patch) | |
| tree | 1e5826b29d8a22e057616e16069232f95788f3ba /src/frontend/app/routes/stoplist.tsx | |
| parent | 8182a08f60e88595984ba80b472f29ccf53c19bd (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.tsx | 100 |
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> |
