UI: единый PlayerLink — ники игроков кликабельны во всех местах просмотра
Новый компонент PlayerLink (ссылка на /u/:id, кликабельно всё поле строки, stopPropagation для вложенности в кликабельные карточки). Применён: - Leaderboard и общая статистика (рефакторинг ручных Link); - страница партии: результаты и состав; - настройки группы: список участников; - список партий и незавершённые на главной (карточки button -> div, т.к. ссылки внутри button — невалидная разметка). «пригласил: X» в приглашениях остаётся текстом — в API нет id пригласившего. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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() {
|
||||
<div className="stack">
|
||||
{(members ?? []).map((m) => (
|
||||
<div key={m.user_id} className="row-between">
|
||||
<span>
|
||||
{/* Кликабельна вся левая часть строки, не только текст ника. */}
|
||||
<PlayerLink userId={m.user_id} style={{ flex: 1 }}>
|
||||
{m.nickname} <span className="muted small">· {m.role}</span>
|
||||
</span>
|
||||
</PlayerLink>
|
||||
{isOwner && m.role !== "owner" && (
|
||||
<button className="btn btn-ghost btn-danger small" onClick={() => remove(m.user_id)}>
|
||||
<Trash2 size={16} />
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useNavigate, useParams } from "react-router-dom";
|
||||
import { ApiError } from "../api/client";
|
||||
import { ConfirmDialog } from "../components/ConfirmDialog";
|
||||
import { MatchMedia } from "../components/MatchMedia";
|
||||
import { PlayerLink } from "../components/PlayerLink";
|
||||
import { Spinner } from "../components/Spinner";
|
||||
import { formatDuration, formatTime } from "../domain/format";
|
||||
import { WIN_REASONS, type WinReason, winReasonLabel } from "../domain/winReasons";
|
||||
@@ -145,7 +146,7 @@ export function MatchDetailPage() {
|
||||
<div className="stack">
|
||||
{sorted.map((p) => (
|
||||
<div key={p.user_id}>
|
||||
<div className="row" style={{ gap: 8 }}>
|
||||
<PlayerLink userId={p.user_id} className="row" style={{ gap: 8 }}>
|
||||
<span
|
||||
className={
|
||||
"place-badge" +
|
||||
@@ -158,7 +159,7 @@ export function MatchDetailPage() {
|
||||
<b>{p.nickname}</b>
|
||||
<span className="muted small">· {p.faction_name}</span>
|
||||
{p.was_random && <span className="badge">🎲</span>}
|
||||
</div>
|
||||
</PlayerLink>
|
||||
{p.comment && <div className="small muted" style={{ marginLeft: 32 }}>{p.comment}</div>}
|
||||
</div>
|
||||
))}
|
||||
@@ -181,11 +182,11 @@ export function MatchDetailPage() {
|
||||
<h3>Состав</h3>
|
||||
<div className="stack">
|
||||
{match.participants.map((p) => (
|
||||
<div key={p.user_id} className="row" style={{ gap: 8 }}>
|
||||
<PlayerLink key={p.user_id} userId={p.user_id} className="row" style={{ gap: 8 }}>
|
||||
<b>{p.nickname}</b>
|
||||
<span className="muted small">· {p.faction_name}</span>
|
||||
{p.was_random && <span className="badge">🎲</span>}
|
||||
</div>
|
||||
</PlayerLink>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
import { Avatar } from "../components/Avatar";
|
||||
import { PlayerLink } from "../components/PlayerLink";
|
||||
import { Spinner } from "../components/Spinner";
|
||||
import type { LeaderboardEntry } from "../domain/types";
|
||||
import { useLeaderboard } from "../hooks/stats";
|
||||
@@ -43,7 +43,7 @@ export function OverallStatsPage() {
|
||||
const arrow = (k: SortKey) => (k === sortKey ? (dir === 1 ? " ↑" : " ↓") : "");
|
||||
|
||||
const row = (e: LeaderboardEntry) => (
|
||||
<Link key={e.user_id} to={`/u/${e.user_id}`} className="st-row" style={{ color: "inherit" }}>
|
||||
<PlayerLink key={e.user_id} userId={e.user_id} className="st-row">
|
||||
<span className={"st-num" + (e.rank && e.rank <= 3 ? " gold" : "")}>{e.rank ?? "—"}</span>
|
||||
<span className="st-player">
|
||||
<Avatar url={e.avatar_url} nickname={e.nickname} size={28} />
|
||||
@@ -53,7 +53,7 @@ export function OverallStatsPage() {
|
||||
<span className="st-num">{e.wins}</span>
|
||||
<span className="st-num">{pct(e.win_rate)}</span>
|
||||
<span className="st-num lb-score">{e.score ?? "—"}</span>
|
||||
</Link>
|
||||
</PlayerLink>
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user