From 80bcf4a5f29ab926c2208d5efb4c19087c600323 Mon Sep 17 00:00:00 2001 From: Ariel Costas Guerrero Date: Sun, 7 Sep 2025 19:22:28 +0200 Subject: 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. --- src/frontend/app/routes/stoplist.tsx | 100 ++++++++++++++++++++++++----------- 1 file changed, 70 insertions(+), 30 deletions(-) (limited to 'src/frontend/app/routes/stoplist.tsx') 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(null); + const [loading, setLoading] = useState(true); const [searchResults, setSearchResults] = useState(null); + const [favouriteIds, setFavouriteIds] = useState([]); + const [recentIds, setRecentIds] = useState([]); + const [favouriteStops, setFavouriteStops] = useState([]); + const [recentStops, setRecentStops] = useState([]); const searchTimeout = useRef(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

{t("common.loading")}

; - } - return (

UrbanoVigo Web

@@ -108,7 +131,7 @@ export default function StopList() {

{t("stoplist.favourites")}

- {favouritedStops?.length === 0 && ( + {favouriteIds.length === 0 && (

{t( "stoplist.no_favourites", @@ -118,18 +141,28 @@ export default function StopList() { )}

    - {favouritedStops - ?.sort((a, b) => a.stopId - b.stopId) - .map((stop: Stop) => )} + {loading && favouriteIds.length > 0 && + favouriteIds.map((id) => ( + + )) + } + {!loading && favouriteStops + .sort((a, b) => a.stopId - b.stopId) + .map((stop) => )}
- {recentStops && recentStops.length > 0 && ( + {(recentIds.length > 0 || (!loading && recentStops.length > 0)) && (

{t("stoplist.recents")}

    - {recentStops.map((stop: Stop) => ( + {loading && recentIds.length > 0 && + recentIds.map((id) => ( + + )) + } + {!loading && recentStops.map((stop) => ( ))}
@@ -140,9 +173,16 @@ export default function StopList() {

{t("stoplist.all_stops", "Paradas")}

    - {data + {loading && ( + <> + {Array.from({ length: 8 }, (_, index) => ( + + ))} + + )} + {!loading && data ?.sort((a, b) => a.stopId - b.stopId) - .map((stop: Stop) => )} + .map((stop) => )}
-- cgit v1.3