Профиль: блок истории игр с переключателями режима и подробности

В «Аккаунте» появился блок «История игр» с двумя тумблерами — «Только
лучшая партия» и «Подробные карточки»; выбор уходит в профиль, поэтому
переживает перезаход и другое устройство.

Новый компонент MatchHistory: подробный режим переиспользует список партий
группы (MatchListView со всеми участниками), компактный рисует строку с
результатом самого игрока — место, фракция, дата и длительность.

Публичный профиль показывает ту же историю в режиме владельца и без
контролов: страница намеренно read-only.

schema.d.ts пересобран с живого бэкенда (npm run gen:api).

#1

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-07 19:56:15 +03:00
co-authored by Claude Opus 5
parent be80891449
commit 9dda7de067
7 changed files with 250 additions and 0 deletions
+40
View File
@@ -3,9 +3,11 @@ import { useNavigate } from "react-router-dom";
import { ApiError } from "../api/client";
import { Avatar } from "../components/Avatar";
import { MatchHistory } from "../components/MatchHistory";
import { PickerSelect } from "../components/PickerSelect";
import { ProfileStatsCard } from "../components/ProfileStatsCard";
import { Spinner } from "../components/Spinner";
import { Switch } from "../components/Switch";
import { useToast } from "../context/ToastContext";
import {
useDeleteAvatar,
@@ -13,11 +15,13 @@ import {
useMe,
useUpdateBio,
useUpdateFavoriteFaction,
useUpdateHistoryPrefs,
useUpdateNickname,
useUploadAvatar,
} from "../hooks/auth";
import { useExpansions, useFactions } from "../hooks/reference";
import { useMyStats } from "../hooks/stats";
import { useUserMatches } from "../hooks/users";
export function AccountPage() {
const { data: me, isLoading } = useMe();
@@ -25,8 +29,10 @@ export function AccountPage() {
const updateNick = useUpdateNickname();
const updateBio = useUpdateBio();
const updateFavorite = useUpdateFavoriteFaction();
const updateHistory = useUpdateHistoryPrefs();
const { data: factions } = useFactions();
const { data: expansions } = useExpansions();
const { data: history } = useUserMatches(me?.id ?? null);
const uploadAvatar = useUploadAvatar();
const deleteAvatar = useDeleteAvatar();
const logout = useLogout();
@@ -55,6 +61,14 @@ export function AccountPage() {
}
};
const saveHistory = async (prefs: { history_mode?: string; history_detail?: string }) => {
try {
await updateHistory.mutateAsync(prefs);
} catch (e) {
toast.show(e instanceof ApiError ? e.message : "Ошибка");
}
};
const saveNick = async () => {
const nick = nickname.trim();
if (!nick) return;
@@ -203,6 +217,32 @@ export function AccountPage() {
{stats && <ProfileStatsCard stats={stats} />}
<div className="card">
<h3>История игр</h3>
{/* Настройки — витрина профиля: в этом же виде историю видят и гости. */}
<div className="row-between">
<div>Только лучшая партия</div>
<Switch
checked={me.history_mode === "best"}
disabled={updateHistory.isPending}
onChange={(v) => saveHistory({ history_mode: v ? "best" : "all" })}
/>
</div>
<div className="row-between" style={{ marginTop: 10 }}>
<div>Подробные карточки</div>
<Switch
checked={me.history_detail === "full"}
disabled={updateHistory.isPending}
onChange={(v) => saveHistory({ history_detail: v ? "full" : "compact" })}
/>
</div>
<div style={{ marginTop: 12 }}>
{history && (
<MatchHistory items={history.items} userId={me.id} detail={history.detail} />
)}
</div>
</div>
<button className="btn btn-danger btn-block" onClick={doLogout}>
Выйти
</button>
+15
View File
@@ -3,12 +3,14 @@ import { useParams } from "react-router-dom";
import { ApiError } from "../api/client";
import { Avatar } from "../components/Avatar";
import { MatchHistory } from "../components/MatchHistory";
import { ProfileStatsCard } from "../components/ProfileStatsCard";
import { Spinner } from "../components/Spinner";
import { useToast } from "../context/ToastContext";
import { useMe } from "../hooks/auth";
import { useInviteMember } from "../hooks/groups";
import { usePublicProfile } from "../hooks/stats";
import { useUserMatches } from "../hooks/users";
/**
* Просмотр ЧУЖОГО профиля — отдельный read-only компонент (намеренно изолирован от
@@ -19,6 +21,7 @@ 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 invite = useInviteMember();
const toast = useToast();
const [pickOpen, setPickOpen] = useState(false);
@@ -98,6 +101,18 @@ export function PublicProfilePage() {
)}
<ProfileStatsCard stats={data.stats} title="Статистика" />
{/* История в том виде, который выбрал владелец профиля; контролов здесь нет. */}
{history && history.items.length > 0 && (
<div className="card">
<h3>{history.mode === "best" ? "Лучшая партия" : "История игр"}</h3>
<MatchHistory
items={history.items}
userId={data.user_id}
detail={history.detail}
/>
</div>
)}
</div>
);
}