From cbf3cfc6e613cb33fc51a45864c0561850fe8b91 Mon Sep 17 00:00:00 2001 From: NotBigGhost Date: Mon, 13 Jul 2026 17:37:19 +0300 Subject: [PATCH] =?UTF-8?q?UI:=20=D0=B5=D0=B4=D0=B8=D0=BD=D1=8B=D0=B9=20Pl?= =?UTF-8?q?ayerLink=20=E2=80=94=20=D0=BD=D0=B8=D0=BA=D0=B8=20=D0=B8=D0=B3?= =?UTF-8?q?=D1=80=D0=BE=D0=BA=D0=BE=D0=B2=20=D0=BA=D0=BB=D0=B8=D0=BA=D0=B0?= =?UTF-8?q?=D0=B1=D0=B5=D0=BB=D1=8C=D0=BD=D1=8B=20=D0=B2=D0=BE=20=D0=B2?= =?UTF-8?q?=D1=81=D0=B5=D1=85=20=D0=BC=D0=B5=D1=81=D1=82=D0=B0=D1=85=20?= =?UTF-8?q?=D0=BF=D1=80=D0=BE=D1=81=D0=BC=D0=BE=D1=82=D1=80=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Новый компонент PlayerLink (ссылка на /u/:id, кликабельно всё поле строки, stopPropagation для вложенности в кликабельные карточки). Применён: - Leaderboard и общая статистика (рефакторинг ручных Link); - страница партии: результаты и состав; - настройки группы: список участников; - список партий и незавершённые на главной (карточки button -> div, т.к. ссылки внутри button — невалидная разметка). «пригласил: X» в приглашениях остаётся текстом — в API нет id пригласившего. Co-Authored-By: Claude Fable 5 --- frontend/src/components/InProgressMatches.tsx | 15 +++++++-- frontend/src/components/Leaderboard.tsx | 16 +++------- frontend/src/components/MatchList.tsx | 13 +++++--- frontend/src/components/PlayerLink.tsx | 31 +++++++++++++++++++ frontend/src/pages/GroupSettingsPage.tsx | 6 ++-- frontend/src/pages/MatchDetailPage.tsx | 9 +++--- frontend/src/pages/OverallStatsPage.tsx | 6 ++-- 7 files changed, 68 insertions(+), 28 deletions(-) create mode 100644 frontend/src/components/PlayerLink.tsx diff --git a/frontend/src/components/InProgressMatches.tsx b/frontend/src/components/InProgressMatches.tsx index bb9917b..e6044dc 100644 --- a/frontend/src/components/InProgressMatches.tsx +++ b/frontend/src/components/InProgressMatches.tsx @@ -3,6 +3,7 @@ import { useNavigate } from "react-router-dom"; import { formatTime } from "../domain/format"; import type { HomeInProgressMatch } from "../domain/types"; +import { PlayerLink } from "./PlayerLink"; export function InProgressMatches({ items }: { items: HomeInProgressMatch[] }) { const navigate = useNavigate(); @@ -13,7 +14,9 @@ export function InProgressMatches({ items }: { items: HomeInProgressMatch[] }) {

Незавершённые партии

{items.map((m) => ( - +
))} diff --git a/frontend/src/components/Leaderboard.tsx b/frontend/src/components/Leaderboard.tsx index e5e13f2..94dfeb1 100644 --- a/frontend/src/components/Leaderboard.tsx +++ b/frontend/src/components/Leaderboard.tsx @@ -1,7 +1,6 @@ -import { Link } from "react-router-dom"; - import type { LeaderboardEntry } from "../domain/types"; import { Avatar } from "./Avatar"; +import { PlayerLink } from "./PlayerLink"; function pct(v: number | null | undefined): string { return v == null ? "—" : `${Math.round(v * 100)}%`; @@ -9,14 +8,9 @@ function pct(v: number | null | undefined): string { function Row({ entry }: { entry: LeaderboardEntry }) { return ( - // Вся строка — ссылка на профиль. stopPropagation: на главной карточка-обёртка - // ведёт в общую статистику, а клик по строке игрока — в его профиль. - e.stopPropagation()} - > + // Вся строка — ссылка на профиль (на главной карточка-обёртка ведёт в общую + // статистику, а клик по строке игрока — в его профиль). +
{entry.rank ?? "—"}
@@ -30,7 +24,7 @@ function Row({ entry }: { entry: LeaderboardEntry }) {
{entry.score ?? "—"}
- +
); } diff --git a/frontend/src/components/MatchList.tsx b/frontend/src/components/MatchList.tsx index 7aac38b..f55a90b 100644 --- a/frontend/src/components/MatchList.tsx +++ b/frontend/src/components/MatchList.tsx @@ -3,6 +3,7 @@ import { useNavigate } from "react-router-dom"; import { formatDuration } from "../domain/format"; import { winReasonLabel } from "../domain/winReasons"; import type { MatchListItem } from "../domain/types"; +import { PlayerLink } from "./PlayerLink"; export function MatchListView({ items }: { items: MatchListItem[] }) { const navigate = useNavigate(); @@ -14,10 +15,12 @@ export function MatchListView({ items }: { items: MatchListItem[] }) { const inProgress = m.status === "in_progress"; const sorted = [...m.participants].sort((a, b) => (a.place ?? 99) - (b.place ?? 99)); return ( - + ); })} diff --git a/frontend/src/components/PlayerLink.tsx b/frontend/src/components/PlayerLink.tsx new file mode 100644 index 0000000..51ad8cd --- /dev/null +++ b/frontend/src/components/PlayerLink.tsx @@ -0,0 +1,31 @@ +import type { CSSProperties, ReactNode } from "react"; +import { Link } from "react-router-dom"; + +/** + * Ссылка на профиль игрока (/u/:id). Оборачивает всё поле с ником (строку с аватаром, + * местом, фракцией…), а не голый текст. stopPropagation — строки часто лежат внутри + * кликабельных карточек (список партий, топ на главной), и клик по игроку должен + * вести в профиль, а не срабатывать навигацией карточки. + */ +export function PlayerLink({ + userId, + className, + style, + children, +}: { + userId: number; + className?: string; + style?: CSSProperties; + children: ReactNode; +}) { + return ( + e.stopPropagation()} + > + {children} + + ); +} diff --git a/frontend/src/pages/GroupSettingsPage.tsx b/frontend/src/pages/GroupSettingsPage.tsx index 31a4f5f..6e82776 100644 --- a/frontend/src/pages/GroupSettingsPage.tsx +++ b/frontend/src/pages/GroupSettingsPage.tsx @@ -2,6 +2,7 @@ import { Trash2, UserPlus } from "lucide-react"; import { useEffect, useState } from "react"; import { ApiError } from "../api/client"; +import { PlayerLink } from "../components/PlayerLink"; import { Spinner } from "../components/Spinner"; import { MAX_GROUP_SIZE } from "../domain/constants"; import { useToast } from "../context/ToastContext"; @@ -118,9 +119,10 @@ export function GroupSettingsPage() {
{(members ?? []).map((m) => (
- + {/* Кликабельна вся левая часть строки, не только текст ника. */} + {m.nickname} · {m.role} - + {isOwner && m.role !== "owner" && (