Профиль: догрузка истории и счётчик партий в шапке

История запрашивалась без 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
This commit is contained in:
2026-09-09 20:27:38 +03:00
co-authored by Claude Opus 5
parent f3067b1f89
commit f2aaa2a15e
4 changed files with 76 additions and 15 deletions
+15 -2
View File
@@ -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() {
<Avatar url={me.avatar_url} nickname={me.nickname} size={64} />
<div style={{ flex: 1 }}>
<b style={{ fontSize: 18 }}>{me.nickname}</b>
{stats && <div className="muted small">Партий: {stats.overall.games}</div>}
<div className="row" style={{ gap: 8, marginTop: 8 }}>
<button
className="btn small"
@@ -238,7 +244,14 @@ export function AccountPage() {
</div>
<div style={{ marginTop: 12 }}>
{history && (
<MatchHistory items={history.items} userId={me.id} detail={history.detail} />
<MatchHistory
items={history.pages.flatMap((p) => p.items)}
userId={me.id}
detail={history.pages[0].detail}
hasMore={hasNextPage}
loadingMore={isFetchingNextPage}
onMore={() => fetchNextPage()}
/>
)}
</div>
</div>
+15 -5
View File
@@ -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 <Spinner />;
if (isError || !data) return <div className="muted">Профиль не найден.</div>;
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() {
<Avatar url={data.avatar_url} nickname={data.nickname} size={64} />
<div style={{ flex: 1 }}>
<b style={{ fontSize: 18 }}>{data.nickname}</b>
<div className="muted small">Партий: {data.stats.overall.games}</div>
{data.bio ? (
<div className="muted" style={{ marginTop: 6, whiteSpace: "pre-wrap" }}>
{data.bio}
@@ -103,13 +110,16 @@ export function PublicProfilePage() {
<ProfileStatsCard stats={data.stats} title="Статистика" />
{/* История в том виде, который выбрал владелец профиля; контролов здесь нет. */}
{history && history.items.length > 0 && (
{historyItems.length > 0 && (
<div className="card">
<h3>{history.mode === "best" ? "Лучшая партия" : "История игр"}</h3>
<h3>{history!.pages[0].mode === "best" ? "Лучшая партия" : "История игр"}</h3>
<MatchHistory
items={history.items}
items={historyItems}
userId={data.user_id}
detail={history.detail}
detail={history!.pages[0].detail}
hasMore={hasNextPage}
loadingMore={isFetchingNextPage}
onMore={() => fetchNextPage()}
/>
</div>
)}