From 1e9271eac47ac30ed3521abf9bc847dae3a7cdb0 Mon Sep 17 00:00:00 2001 From: che-pj Date: Wed, 27 Aug 2025 16:30:00 +0200 Subject: [PATCH] fix(ui): improve watchlist loading with batching and skeletons --- spotizerr-ui/src/routes/watchlist.tsx | 100 ++++++++++++++++++++------ 1 file changed, 79 insertions(+), 21 deletions(-) diff --git a/spotizerr-ui/src/routes/watchlist.tsx b/spotizerr-ui/src/routes/watchlist.tsx index ddbbffb..88ab4ff 100644 --- a/spotizerr-ui/src/routes/watchlist.tsx +++ b/spotizerr-ui/src/routes/watchlist.tsx @@ -20,39 +20,77 @@ export const Watchlist = () => { const { settings, isLoading: settingsLoading } = useSettings(); const [items, setItems] = useState([]); const [isLoading, setIsLoading] = useState(true); + const [expectedCount, setExpectedCount] = useState(null); + + // Utility to batch fetch details + async function batchFetch( + ids: string[], + fetchFn: (id: string) => Promise, + batchSize: number, + onBatch: (results: T[]) => void + ) { + for (let i = 0; i < ids.length; i += batchSize) { + const batchIds = ids.slice(i, i + batchSize); + const batchResults = await Promise.all( + batchIds.map((id) => fetchFn(id).catch(() => null)) + ); + onBatch(batchResults.filter(Boolean) as T[]); + } + } const fetchWatchlist = useCallback(async () => { setIsLoading(true); + setItems([]); // Clear previous items + setExpectedCount(null); try { const [artistsRes, playlistsRes] = await Promise.all([ apiClient.get("/artist/watch/list"), apiClient.get("/playlist/watch/list"), ]); - const artistDetailsPromises = artistsRes.data.map((artist) => - apiClient.get(`/artist/info?id=${artist.spotify_id}`), - ); - const playlistDetailsPromises = playlistsRes.data.map((playlist) => - apiClient.get(`/playlist/info?id=${playlist.spotify_id}`), + // Prepare lists of IDs + const artistIds = artistsRes.data.map((artist) => artist.spotify_id); + const playlistIds = playlistsRes.data.map((playlist) => playlist.spotify_id); + setExpectedCount(artistIds.length + playlistIds.length); + + // Allow UI to render grid and skeletons immediately + setIsLoading(false); + + // Helper to update state incrementally + const appendItems = (newItems: WatchedItem[]) => { + setItems((prev) => [...prev, ...newItems]); + }; + + // Fetch artist details in batches + await batchFetch( + artistIds, + (id) => apiClient.get(`/artist/info?id=${id}`).then(res => res.data), + 5, // batch size + (results) => { + const items: WatchedArtist[] = results.map((data) => ({ + ...data, + itemType: "artist", + })); + appendItems(items); + } ); - const [artistDetailsRes, playlistDetailsRes] = await Promise.all([ - Promise.all(artistDetailsPromises), - Promise.all(playlistDetailsPromises), - ]); - - const artists: WatchedItem[] = artistDetailsRes.map((res) => ({ ...res.data, itemType: "artist" })); - const playlists: WatchedItem[] = playlistDetailsRes.map((res) => ({ - ...res.data, - itemType: "playlist", - spotify_id: res.data.id, - })); - - setItems([...artists, ...playlists]); + // Fetch playlist details in batches + await batchFetch( + playlistIds, + (id) => apiClient.get(`/playlist/info?id=${id}`).then(res => res.data), + 5, // batch size + (results) => { + const items: WatchedPlaylist[] = results.map((data) => ({ + ...data, + itemType: "playlist", + spotify_id: data.id, + })); + appendItems(items); + } + ); } catch { toast.error("Failed to load watchlist."); - } finally { - setIsLoading(false); } }, []); @@ -110,7 +148,8 @@ export const Watchlist = () => { ); } - if (items.length === 0) { + // Show "empty" only if not loading and nothing expected + if (!isLoading && items.length === 0 && (!expectedCount || expectedCount === 0)) { return (

Watchlist is Empty

@@ -158,6 +197,25 @@ export const Watchlist = () => {
))} + {/* Skeletons for loading items */} + {isLoading && expectedCount && items.length < expectedCount && + Array.from({ length: expectedCount - items.length }).map((_, idx) => ( +
+
+
+
+
+
+
+
+
+
+
+ )) + }
);