UI: приглашение игрока перенесено из настроек в «Список игроков»
Карточка «Статистика игроков» на странице группы переименована в «Список игроков», под списком — поле приглашения по нику (с проверкой максимума участников и отправкой по Enter). В настройках группы остался только состав с удалением участников. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,14 +1,25 @@
|
|||||||
|
import { UserPlus } from "lucide-react";
|
||||||
|
import { useState } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
|
import { ApiError } from "../api/client";
|
||||||
import { CreateGroupForm } from "../components/CreateGroupForm";
|
import { CreateGroupForm } from "../components/CreateGroupForm";
|
||||||
import { GroupInvitations } from "../components/GroupInvitations";
|
import { GroupInvitations } from "../components/GroupInvitations";
|
||||||
import { GroupSwitcher } from "../components/GroupSwitcher";
|
import { GroupSwitcher } from "../components/GroupSwitcher";
|
||||||
import { Leaderboard } from "../components/Leaderboard";
|
import { Leaderboard } from "../components/Leaderboard";
|
||||||
import { MatchListView } from "../components/MatchList";
|
import { MatchListView } from "../components/MatchList";
|
||||||
import { Spinner } from "../components/Spinner";
|
import { Spinner } from "../components/Spinner";
|
||||||
|
import { MAX_GROUP_SIZE } from "../domain/constants";
|
||||||
import { formatDate } from "../domain/format";
|
import { formatDate } from "../domain/format";
|
||||||
|
import { useToast } from "../context/ToastContext";
|
||||||
import { useMe } from "../hooks/auth";
|
import { useMe } from "../hooks/auth";
|
||||||
import { useGroup, useGroupMatches, useGroupStats } from "../hooks/groups";
|
import {
|
||||||
|
useGroup,
|
||||||
|
useGroupMatches,
|
||||||
|
useGroupMembers,
|
||||||
|
useGroupStats,
|
||||||
|
useInviteMember,
|
||||||
|
} from "../hooks/groups";
|
||||||
|
|
||||||
export function GroupPage() {
|
export function GroupPage() {
|
||||||
const { data: me, isLoading } = useMe();
|
const { data: me, isLoading } = useMe();
|
||||||
@@ -18,9 +29,25 @@ export function GroupPage() {
|
|||||||
const { data: group } = useGroup(groupId);
|
const { data: group } = useGroup(groupId);
|
||||||
const { data: stats } = useGroupStats(groupId);
|
const { data: stats } = useGroupStats(groupId);
|
||||||
const { data: matches } = useGroupMatches(groupId);
|
const { data: matches } = useGroupMatches(groupId);
|
||||||
|
const { data: members } = useGroupMembers(groupId);
|
||||||
|
const inviteMember = useInviteMember();
|
||||||
|
const toast = useToast();
|
||||||
|
const [newNick, setNewNick] = useState("");
|
||||||
|
|
||||||
if (isLoading) return <Spinner />;
|
if (isLoading) return <Spinner />;
|
||||||
|
|
||||||
|
const invite = async () => {
|
||||||
|
const nick = newNick.trim();
|
||||||
|
if (!nick || !groupId) return;
|
||||||
|
try {
|
||||||
|
await inviteMember.mutateAsync({ groupId, nickname: nick });
|
||||||
|
setNewNick("");
|
||||||
|
toast.show("Приглашение отправлено");
|
||||||
|
} catch (e) {
|
||||||
|
toast.show(e instanceof ApiError ? e.message : "Ошибка");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (!groupId) {
|
if (!groupId) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -57,7 +84,7 @@ export function GroupPage() {
|
|||||||
<GroupInvitations />
|
<GroupInvitations />
|
||||||
|
|
||||||
<div className="card">
|
<div className="card">
|
||||||
<h3>Статистика игроков</h3>
|
<h3>Список игроков</h3>
|
||||||
{stats && (
|
{stats && (
|
||||||
<Leaderboard
|
<Leaderboard
|
||||||
entries={stats.leaderboard}
|
entries={stats.leaderboard}
|
||||||
@@ -65,6 +92,26 @@ export function GroupPage() {
|
|||||||
inactive={stats.inactive}
|
inactive={stats.inactive}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{/* Приглашение нового игрока — здесь, рядом со списком (не в настройках). */}
|
||||||
|
{(members ?? []).length >= MAX_GROUP_SIZE ? (
|
||||||
|
<p className="muted small" style={{ margin: "10px 0 0" }}>
|
||||||
|
Достигнут максимум участников ({MAX_GROUP_SIZE}).
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<div className="row" style={{ marginTop: 10 }}>
|
||||||
|
<input
|
||||||
|
placeholder="Ник игрока — отправить приглашение"
|
||||||
|
value={newNick}
|
||||||
|
onChange={(e) => setNewNick(e.target.value)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter") invite();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<button className="btn btn-primary" onClick={invite} disabled={inviteMember.isPending}>
|
||||||
|
<UserPlus size={18} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<GroupSwitcher />
|
<GroupSwitcher />
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Trash2, UserPlus } from "lucide-react";
|
import { Trash2 } from "lucide-react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
import { ApiError } from "../api/client";
|
import { ApiError } from "../api/client";
|
||||||
@@ -9,7 +9,7 @@ import { Switch } from "../components/Switch";
|
|||||||
import { MAX_GROUP_SIZE } from "../domain/constants";
|
import { MAX_GROUP_SIZE } from "../domain/constants";
|
||||||
import { useToast } from "../context/ToastContext";
|
import { useToast } from "../context/ToastContext";
|
||||||
import { useMe } from "../hooks/auth";
|
import { useMe } from "../hooks/auth";
|
||||||
import { useGroup, useGroupMembers, useInviteMember, useRemoveMember, useSetGroupExpansions } from "../hooks/groups";
|
import { useGroup, useGroupMembers, useRemoveMember, useSetGroupExpansions } from "../hooks/groups";
|
||||||
import { useExpansions } from "../hooks/reference";
|
import { useExpansions } from "../hooks/reference";
|
||||||
|
|
||||||
export function GroupSettingsPage() {
|
export function GroupSettingsPage() {
|
||||||
@@ -19,12 +19,10 @@ export function GroupSettingsPage() {
|
|||||||
const { data: expansions } = useExpansions();
|
const { data: expansions } = useExpansions();
|
||||||
const { data: members } = useGroupMembers(groupId);
|
const { data: members } = useGroupMembers(groupId);
|
||||||
const setExpansions = useSetGroupExpansions(groupId ?? 0);
|
const setExpansions = useSetGroupExpansions(groupId ?? 0);
|
||||||
const inviteMember = useInviteMember();
|
|
||||||
const removeMember = useRemoveMember(groupId ?? 0);
|
const removeMember = useRemoveMember(groupId ?? 0);
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
|
||||||
const [selected, setSelected] = useState<Set<number>>(new Set());
|
const [selected, setSelected] = useState<Set<number>>(new Set());
|
||||||
const [newNick, setNewNick] = useState("");
|
|
||||||
const [removeTarget, setRemoveTarget] = useState<{ userId: number; nickname: string } | null>(null);
|
const [removeTarget, setRemoveTarget] = useState<{ userId: number; nickname: string } | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -51,18 +49,6 @@ export function GroupSettingsPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const add = async () => {
|
|
||||||
const nick = newNick.trim();
|
|
||||||
if (!nick) return;
|
|
||||||
try {
|
|
||||||
await inviteMember.mutateAsync({ groupId: group.id, nickname: nick });
|
|
||||||
setNewNick("");
|
|
||||||
toast.show("Приглашение отправлено");
|
|
||||||
} catch (e) {
|
|
||||||
toast.show(e instanceof ApiError ? e.message : "Ошибка");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const remove = async (userId: number) => {
|
const remove = async (userId: number) => {
|
||||||
try {
|
try {
|
||||||
await removeMember.mutateAsync(userId);
|
await removeMember.mutateAsync(userId);
|
||||||
@@ -99,22 +85,8 @@ export function GroupSettingsPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="card">
|
<div className="card">
|
||||||
|
{/* Приглашение нового игрока — на странице группы («Список игроков»). */}
|
||||||
<h3>Участники ({(members ?? []).length}/{MAX_GROUP_SIZE})</h3>
|
<h3>Участники ({(members ?? []).length}/{MAX_GROUP_SIZE})</h3>
|
||||||
{isOwner && (members ?? []).length >= MAX_GROUP_SIZE && (
|
|
||||||
<p className="muted small">Достигнут максимум участников ({MAX_GROUP_SIZE}).</p>
|
|
||||||
)}
|
|
||||||
{isOwner && (members ?? []).length < MAX_GROUP_SIZE && (
|
|
||||||
<div className="row" style={{ marginBottom: 10 }}>
|
|
||||||
<input
|
|
||||||
placeholder="Ник игрока — отправить приглашение"
|
|
||||||
value={newNick}
|
|
||||||
onChange={(e) => setNewNick(e.target.value)}
|
|
||||||
/>
|
|
||||||
<button className="btn btn-primary" onClick={add} disabled={inviteMember.isPending}>
|
|
||||||
<UserPlus size={18} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="stack">
|
<div className="stack">
|
||||||
{(members ?? []).map((m) => (
|
{(members ?? []).map((m) => (
|
||||||
<div key={m.user_id} className="row-between">
|
<div key={m.user_id} className="row-between">
|
||||||
|
|||||||
Reference in New Issue
Block a user