Files
ForbiddenStarsApp/frontend/src/hooks/users.ts
T
NotBigGhostandClaude Opus 5 f2aaa2a15e Профиль: догрузка истории и счётчик партий в шапке
История запрашивалась без 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
2026-09-09 20:27:38 +03:00

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 } } })),
});
}