Files
ForbiddenStarsApp/frontend/src/components/MatchHistory.tsx
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

86 lines
2.6 KiB
TypeScript

import { useNavigate } from "react-router-dom";
import { formatDate, formatDuration } from "../domain/format";
import type { MatchListItem } from "../domain/types";
import { MatchListView } from "./MatchList";
/**
* История партий игрока в профиле. Подробный режим — тот же список, что у группы
* (MatchListView со всеми участниками); компактный — строка с результатом самого
* владельца профиля, поэтому компонент и принимает его userId.
*/
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 && (
<button
className="btn btn-ghost btn-block"
style={{ marginTop: 10 }}
onClick={onMore}
disabled={loadingMore}
>
{loadingMore ? "Загружаю…" : "Показать ещё"}
</button>
);
if (items.length === 0) return <div className="muted small">Партий пока нет.</div>;
if (detail === "full")
return (
<>
<MatchListView items={items} />
{more}
</>
);
return (
<div className="stack">
{items.map((m) => {
const mine = m.participants.find((p) => p.user_id === userId);
return (
<button
key={m.id}
className="card row-between"
style={{ margin: 0, width: "100%", textAlign: "left" }}
onClick={() => navigate(`/match/${m.id}`)}
>
<div className="row" style={{ gap: 10, minWidth: 0 }}>
<span
className={
"place-badge" +
(mine?.place === 1 && !mine?.eliminated ? " first" : "") +
(mine?.eliminated ? " eliminated" : "")
}
>
{mine?.eliminated ? "✕" : (mine?.place ?? "—")}
</span>
<span
style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}
>
{mine?.faction_name ?? "—"}
</span>
</div>
<span className="muted small" style={{ flexShrink: 0 }}>
{formatDate(m.played_at)} · {formatDuration(m.duration_minutes)}
</span>
</button>
);
})}
{more}
</div>
);
}