diff --git a/frontend/src/components/MatchHistory.tsx b/frontend/src/components/MatchHistory.tsx index b03c6d5..5d0b8f3 100644 --- a/frontend/src/components/MatchHistory.tsx +++ b/frontend/src/components/MatchHistory.tsx @@ -13,14 +13,38 @@ export function MatchHistory({ items, userId, detail, + hasMore = false, + loadingMore = false, + onMore, }: { items: MatchListItem[]; userId: number; detail: string; + hasMore?: boolean; + loadingMore?: boolean; + onMore?: () => void; }) { const navigate = useNavigate(); + + const more = hasMore && onMore && ( + + ); + if (items.length === 0) return
Партий пока нет.
; - if (detail === "full") return ; + if (detail === "full") + return ( + <> + + {more} + + ); return (
@@ -55,6 +79,7 @@ export function MatchHistory({ ); })} + {more}
); } diff --git a/frontend/src/hooks/users.ts b/frontend/src/hooks/users.ts index 4502ecd..b687ada 100644 --- a/frontend/src/hooks/users.ts +++ b/frontend/src/hooks/users.ts @@ -1,4 +1,4 @@ -import { keepPreviousData, useQuery } from "@tanstack/react-query"; +import { keepPreviousData, useInfiniteQuery, useQuery } from "@tanstack/react-query"; import { useEffect, useState } from "react"; import { api, unwrap } from "../api/client"; @@ -15,18 +15,31 @@ function useDebounced(value: T, ms = 100): T { return debounced; } -/** История партий игрока. Режим и подробность приходят вместе со списком: - * это витрина ВЛАДЕЛЬЦА профиля, её видят и гости. */ +const HISTORY_PAGE = 20; + +/** История партий игрока страницами по 20 («Показать ещё»). + * + * Режим и подробность приходят вместе со списком: это витрина ВЛАДЕЛЬЦА профиля, + * её видят и гости. Постранично, а не одним большим запросом: у эндпоинта потолок + * limit=100, и на 101-й партии обрезка вернулась бы. */ export function useUserMatches(userId: number | null) { - return useQuery({ - queryKey: userId ? qk.userMatches(userId) : ["userMatches", "none"], + return useInfiniteQuery({ + queryKey: userId != null ? qk.userMatches(userId) : ["userMatches", "none"], enabled: userId != null, - queryFn: async () => + initialPageParam: 0, + queryFn: async ({ pageParam }) => unwrap( await api.GET("/api/users/{user_id}/matches", { - params: { path: { user_id: userId as number } }, + params: { + path: { user_id: userId as number }, + query: { limit: HISTORY_PAGE, offset: pageParam as number }, + }, }), ), + getNextPageParam: (last, pages) => { + const loaded = pages.reduce((n, p) => n + p.items.length, 0); + return loaded < last.total ? loaded : undefined; + }, }); } diff --git a/frontend/src/pages/AccountPage.tsx b/frontend/src/pages/AccountPage.tsx index 871d573..e4e8aa9 100644 --- a/frontend/src/pages/AccountPage.tsx +++ b/frontend/src/pages/AccountPage.tsx @@ -32,7 +32,12 @@ export function AccountPage() { const updateHistory = useUpdateHistoryPrefs(); const { data: factions } = useFactions(); const { data: expansions } = useExpansions(); - const { data: history } = useUserMatches(me?.id ?? null); + const { + data: history, + fetchNextPage, + hasNextPage, + isFetchingNextPage, + } = useUserMatches(me?.id ?? null); const uploadAvatar = useUploadAvatar(); const deleteAvatar = useDeleteAvatar(); const logout = useLogout(); @@ -124,6 +129,7 @@ export function AccountPage() {
{me.nickname} + {stats &&
Партий: {stats.overall.games}
}
diff --git a/frontend/src/pages/PublicProfilePage.tsx b/frontend/src/pages/PublicProfilePage.tsx index 9c8c0dc..438936f 100644 --- a/frontend/src/pages/PublicProfilePage.tsx +++ b/frontend/src/pages/PublicProfilePage.tsx @@ -21,7 +21,12 @@ export function PublicProfilePage() { const id = Number(userId); const { data, isLoading, isError } = usePublicProfile(id); const { data: me } = useMe(); - const { data: history } = useUserMatches(Number.isFinite(id) ? id : null); + const { + data: history, + fetchNextPage, + hasNextPage, + isFetchingNextPage, + } = useUserMatches(Number.isFinite(id) ? id : null); const invite = useInviteMember(); const toast = useToast(); const [pickOpen, setPickOpen] = useState(false); @@ -29,6 +34,7 @@ export function PublicProfilePage() { if (isLoading) return ; if (isError || !data) return
Профиль не найден.
; + const historyItems = history?.pages.flatMap((p) => p.items) ?? []; const groups = me?.groups ?? []; const isSelf = me?.id === data.user_id; @@ -49,6 +55,7 @@ export function PublicProfilePage() {
{data.nickname} +
Партий: {data.stats.overall.games}
{data.bio ? (
{data.bio} @@ -103,13 +110,16 @@ export function PublicProfilePage() { {/* История в том виде, который выбрал владелец профиля; контролов здесь нет. */} - {history && history.items.length > 0 && ( + {historyItems.length > 0 && (
-

{history.mode === "best" ? "Лучшая партия" : "История игр"}

+

{history!.pages[0].mode === "best" ? "Лучшая партия" : "История игр"}

fetchNextPage()} />
)}