Профиль: блок истории игр с переключателями режима и подробности
В «Аккаунте» появился блок «История игр» с двумя тумблерами — «Только лучшая партия» и «Подробные карточки»; выбор уходит в профиль, поэтому переживает перезаход и другое устройство. Новый компонент 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:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user