История запрашивалась без limit/offset, поэтому бэкенд отдавал первые 20 партий, а страницы рисовали их и молча игнорировали total: у игрока с 40 партиями половина истории просто не существовала — ни кнопки, ни намёка. Теперь история грузится страницами по 20, под списком — «Показать ещё». Постранично, а не одним большим запросом: у эндпоинта потолок limit=100, и на 101-й партии обрезка вернулась бы. Кнопка живёт в самом компоненте истории, поэтому появилась сразу и в своём профиле, и в чужом, и в обоих режимах подробности. Общее число партий вынесено в шапку профиля, к нику. Берём его из статистики, а не из total истории: в режиме «только лучшая партия» total равен единице, и счётчик показывал бы «Партий: 1». #30 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BoiJK9ux8peeyjLb8TYjFf
60 lines
2.6 KiB
TypeScript
60 lines
2.6 KiB
TypeScript
import { keepPreviousData, useInfiniteQuery, useQuery } from "@tanstack/react-query";
|
|
import { useEffect, useState } from "react";
|
|
|
|
import { api, unwrap } from "../api/client";
|
|
import { qk } from "../api/queryKeys";
|
|
|
|
/** Значение с задержкой: не дёргаем поиск на каждый введённый символ.
|
|
* 100 мс — почти незаметно глазу, но склеивает быструю печать в один запрос. */
|
|
function useDebounced<T>(value: T, ms = 100): T {
|
|
const [debounced, setDebounced] = useState(value);
|
|
useEffect(() => {
|
|
const t = setTimeout(() => setDebounced(value), ms);
|
|
return () => clearTimeout(t);
|
|
}, [value, ms]);
|
|
return debounced;
|
|
}
|
|
|
|
const HISTORY_PAGE = 20;
|
|
|
|
/** История партий игрока страницами по 20 («Показать ещё»).
|
|
*
|
|
* Режим и подробность приходят вместе со списком: это витрина ВЛАДЕЛЬЦА профиля,
|
|
* её видят и гости. Постранично, а не одним большим запросом: у эндпоинта потолок
|
|
* limit=100, и на 101-й партии обрезка вернулась бы. */
|
|
export function useUserMatches(userId: number | null) {
|
|
return useInfiniteQuery({
|
|
queryKey: userId != null ? qk.userMatches(userId) : ["userMatches", "none"],
|
|
enabled: userId != null,
|
|
initialPageParam: 0,
|
|
queryFn: async ({ pageParam }) =>
|
|
unwrap(
|
|
await api.GET("/api/users/{user_id}/matches", {
|
|
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;
|
|
},
|
|
});
|
|
}
|
|
|
|
/** Подсказки по нику среди всех активных игроков (GET /api/users/search). */
|
|
export function useUserSearch(q: string, limit = 3) {
|
|
const dq = useDebounced(q.trim());
|
|
return useQuery({
|
|
queryKey: qk.userSearch(dq, limit),
|
|
enabled: dq.length > 0,
|
|
// Список не мигает пустым между буквами; при пустом q данные остаются —
|
|
// видимость выпадашки родитель гейтит по q, а не по data.
|
|
placeholderData: keepPreviousData,
|
|
staleTime: 30_000,
|
|
queryFn: async () =>
|
|
unwrap(await api.GET("/api/users/search", { params: { query: { q: dq, limit } } })),
|
|
});
|
|
}
|