История запрашивалась без 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
86 lines
2.6 KiB
TypeScript
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>
|
|
);
|
|
}
|