From 92ad9dc3944137e3fc46b90dd804974161c4e718 Mon Sep 17 00:00:00 2001 From: Tsiry Sandratraina Date: Sat, 24 Jan 2026 23:32:24 +0300 Subject: [PATCH] Add infinite scroll for weekly top artists --- apps/web/src/hooks/useLibrary.tsx | 30 +++ apps/web/src/pages/charts/weekly/Weekly.tsx | 241 ++++++++++++-------- 2 files changed, 171 insertions(+), 100 deletions(-) diff --git a/apps/web/src/hooks/useLibrary.tsx b/apps/web/src/hooks/useLibrary.tsx index cb9431c9..df676d46 100644 --- a/apps/web/src/hooks/useLibrary.tsx +++ b/apps/web/src/hooks/useLibrary.tsx @@ -256,3 +256,33 @@ export const useTopArtistsQuery = ( scrobbles: x.playCount, })), }); + +export const useTopArtistsInfiniteQuery = ( + limit = 20, + startDate?: Date, + endDate?: Date, +) => + useInfiniteQuery({ + queryKey: ["infiniteTopArtists", limit, startDate, endDate], + queryFn: async ({ pageParam = 0 }) => { + const data = await getTopArtists( + pageParam * limit, + limit, + startDate, + endDate, + ); + return { + artists: data?.artists.map((x) => ({ + ...x, + scrobbles: x.playCount, + })), + nextOffset: pageParam + 1, + }; + }, + getNextPageParam: (lastPage) => { + if (!lastPage.artists || lastPage.artists.length < limit) + return undefined; + return lastPage.nextOffset; + }, + initialPageParam: 0, + }); diff --git a/apps/web/src/pages/charts/weekly/Weekly.tsx b/apps/web/src/pages/charts/weekly/Weekly.tsx index 10c9f318..15a91cfa 100644 --- a/apps/web/src/pages/charts/weekly/Weekly.tsx +++ b/apps/web/src/pages/charts/weekly/Weekly.tsx @@ -1,9 +1,11 @@ import { TableBuilder, TableBuilderColumn } from "baseui/table-semantic"; -import { useTopArtistsQuery } from "../../../hooks/useLibrary"; +import { useTopArtistsInfiniteQuery } from "../../../hooks/useLibrary"; import Artist from "../../../components/Icons/Artist"; import { getLastDays } from "../../../lib/date"; import dayjs from "dayjs"; import numeral from "numeral"; +import { useEffect, useRef } from "react"; +import { LabelSmall } from "baseui/typography"; type ArtistRow = { id: string; @@ -16,119 +18,158 @@ type ArtistRow = { }; function Weekly() { - const { data: artists } = useTopArtistsQuery(0, 20, ...getLastDays(7)); + const [startDate, endDate] = getLastDays(7); + const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading } = + useTopArtistsInfiniteQuery(20, startDate, endDate); const end = dayjs(); const start = end.subtract(7, "day"); const range = `${start.format("DD MMM YYYY")} — ${end.format("DD MMM YYYY")}`; + const loadMoreRef = useRef(null); + + // Intersection Observer for infinite scroll + useEffect(() => { + if (!loadMoreRef.current || !hasNextPage || isFetchingNextPage) return; + + const observer = new IntersectionObserver( + (entries) => { + if (entries[0]?.isIntersecting) { + fetchNextPage(); + } + }, + { threshold: 0.1 }, + ); + + observer.observe(loadMoreRef.current); + + return () => observer.disconnect(); + }, [hasNextPage, isFetchingNextPage, fetchNextPage]); + + const allArtists = data?.pages.flatMap((page) => page.artists) ?? []; + return ( <>
{range}
- ({ - ...x, - index, - }))} - divider="clean" - overrides={{ - TableBodyCell: { - style: { - verticalAlign: "middle", - }, - }, - TableHead: { - style: { - backgroundColor: "var(--color-background) !important", - }, - }, - TableHeadRow: { - style: { - backgroundColor: "var(--color-background) !important", - }, - }, - TableHeadCell: { - style: { - backgroundColor: "var(--color-background) !important", - color: "var(--color-text) !important", - opacity: "85%", - }, - }, - TableBodyRow: { - style: { - backgroundColor: "var(--color-background)", - ":hover": { - backgroundColor: "var(--color-menu-hover)", + {!isLoading && ( + <> + ({ + ...x, + index, + }))} + divider="clean" + overrides={{ + TableBodyCell: { + style: { + verticalAlign: "middle", + }, }, - }, - }, - TableEmptyMessage: { - style: { - backgroundColor: "var(--color-background)", - }, - }, - Table: { - style: { - backgroundColor: "var(--color-background)", - }, - }, - }} - > - - {(row: ArtistRow) => ( -
-
-
- {row.index + 1} -
-
- - {!!row.picture && ( - {row.name} - )} - {!row.picture && ( -
-
- + TableHead: { + style: { + backgroundColor: "var(--color-background) !important", + }, + }, + TableHeadRow: { + style: { + backgroundColor: "var(--color-background) !important", + }, + }, + TableHeadCell: { + style: { + backgroundColor: "var(--color-background) !important", + color: "var(--color-text) !important", + opacity: "85%", + }, + }, + TableBodyRow: { + style: { + backgroundColor: "var(--color-background)", + ":hover": { + backgroundColor: "var(--color-menu-hover)", + }, + }, + }, + TableEmptyMessage: { + style: { + backgroundColor: "var(--color-background)", + }, + }, + Table: { + style: { + backgroundColor: "var(--color-background)", + }, + }, + }} + > + + {(row: ArtistRow) => ( + - )} - - - {(row: ArtistRow) => ( -
- {numeral(row.uniqueListeners).format("0,0")} -
- )} -
- - {(row: ArtistRow) => ( - + )} + + + {(row: ArtistRow) => ( +
+ {numeral(row.uniqueListeners).format("0,0")} +
+ )} +
+ + {(row: ArtistRow) => ( +
+ {numeral(row.scrobbles).format("0,0")} +
+ )} +
+ + + {/* Infinite scroll trigger */} +
+ + {isFetchingNextPage && ( +
+ + Loading more... +
)} - - + + )} ); } -- 2.51.2