Кастомизация профиля (о себе, аватар) и просмотр профилей других игроков + общая статистика
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -8,12 +8,15 @@ const TITLES: Record<string, string> = {
|
||||
"/group/settings": "Настройки группы",
|
||||
"/account": "Аккаунт",
|
||||
"/match/new": "Новая партия",
|
||||
"/stats": "Общая статистика",
|
||||
};
|
||||
|
||||
export function AppShell() {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const title = TITLES[location.pathname] ?? "Forbidden Stars";
|
||||
const title =
|
||||
TITLES[location.pathname] ??
|
||||
(location.pathname.startsWith("/u/") ? "Профиль игрока" : "Forbidden Stars");
|
||||
const isHome = location.pathname === "/";
|
||||
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import type { CSSProperties } from "react";
|
||||
|
||||
/**
|
||||
* Чисто презентационный аватар: картинка по url или инициал ника как заглушка.
|
||||
* Используется и в своём, и в чужом профиле, и в списках (общий примитив без логики).
|
||||
*/
|
||||
export function Avatar({
|
||||
url,
|
||||
nickname,
|
||||
size = 40,
|
||||
}: {
|
||||
url?: string | null;
|
||||
nickname: string;
|
||||
size?: number;
|
||||
}) {
|
||||
const box: CSSProperties = {
|
||||
width: size,
|
||||
height: size,
|
||||
borderRadius: "50%",
|
||||
flexShrink: 0,
|
||||
};
|
||||
if (url) {
|
||||
return (
|
||||
<img
|
||||
src={url}
|
||||
alt={nickname}
|
||||
width={size}
|
||||
height={size}
|
||||
style={{ ...box, objectFit: "cover", border: "1px solid var(--border)" }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
const initial = (nickname.trim()[0] ?? "?").toUpperCase();
|
||||
return (
|
||||
<div
|
||||
aria-hidden
|
||||
style={{
|
||||
...box,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
background: "var(--surface-2)",
|
||||
border: "1px solid var(--border)",
|
||||
color: "var(--text-muted)",
|
||||
fontWeight: 700,
|
||||
fontSize: size * 0.42,
|
||||
}}
|
||||
>
|
||||
{initial}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
import type { LeaderboardEntry } from "../domain/types";
|
||||
import { Avatar } from "./Avatar";
|
||||
|
||||
function pct(v: number | null | undefined): string {
|
||||
return v == null ? "—" : `${Math.round(v * 100)}%`;
|
||||
@@ -10,8 +13,16 @@ function Row({ entry }: { entry: LeaderboardEntry }) {
|
||||
<div className={"lb-rank" + (entry.rank && entry.rank <= 3 ? " top" : "")}>
|
||||
{entry.rank ?? "—"}
|
||||
</div>
|
||||
<Avatar url={entry.avatar_url} nickname={entry.nickname} size={32} />
|
||||
<div>
|
||||
<div style={{ fontWeight: 600 }}>{entry.nickname}</div>
|
||||
<Link
|
||||
to={`/u/${entry.user_id}`}
|
||||
// stopPropagation: ник кликабелен даже когда вся карточка-обёртка кликабельна.
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
style={{ fontWeight: 600, color: "inherit" }}
|
||||
>
|
||||
{entry.nickname}
|
||||
</Link>
|
||||
<div className="lb-metrics">
|
||||
<span>Игр: {entry.games}</span>
|
||||
<span>Побед: {entry.wins}</span>
|
||||
|
||||
@@ -4,11 +4,17 @@ function pct(v: number | null | undefined): string {
|
||||
return v == null ? "—" : `${Math.round(v * 100)}%`;
|
||||
}
|
||||
|
||||
export function ProfileStatsCard({ stats }: { stats: ProfileStats }) {
|
||||
export function ProfileStatsCard({
|
||||
stats,
|
||||
title = "Моя статистика",
|
||||
}: {
|
||||
stats: ProfileStats;
|
||||
title?: string;
|
||||
}) {
|
||||
const o = stats.overall;
|
||||
return (
|
||||
<div className="card">
|
||||
<h3>Моя статистика</h3>
|
||||
<h3>{title}</h3>
|
||||
<div className="row-between">
|
||||
<div>Игр</div>
|
||||
<b>{o.games}</b>
|
||||
|
||||
Reference in New Issue
Block a user