import { UserPlus } from "lucide-react"; import { useState } from "react"; import { useNavigate } from "react-router-dom"; import { ApiError } from "../api/client"; import { CreateGroupForm } from "../components/CreateGroupForm"; import { GroupInvitations } from "../components/GroupInvitations"; import { GroupSwitcher } from "../components/GroupSwitcher"; import { Leaderboard } from "../components/Leaderboard"; import { MatchListView } from "../components/MatchList"; import { PlayerSuggestions } from "../components/PlayerSuggestions"; import { Spinner } from "../components/Spinner"; import { MAX_GROUP_SIZE } from "../domain/constants"; import { formatDate } from "../domain/format"; import { useToast } from "../context/ToastContext"; import { useMe } from "../hooks/auth"; import { useGroup, useGroupMatches, useGroupMembers, useGroupStats, useInviteMember, } from "../hooks/groups"; import { useUserSearch } from "../hooks/users"; export function GroupPage() { const { data: me, isLoading } = useMe(); const navigate = useNavigate(); const groupId = me?.active_group_id ?? null; const { data: group } = useGroup(groupId); const { data: stats } = useGroupStats(groupId); const { data: matches } = useGroupMatches(groupId); const { data: members } = useGroupMembers(groupId); const inviteMember = useInviteMember(); const toast = useToast(); const [newNick, setNewNick] = useState(""); const [nickFocused, setNickFocused] = useState(false); // После подстановки ника из подсказки список прячем: подставленный ник сам // совпадает с собой и без флага выпадашка вернулась бы сразу. const [nickPicked, setNickPicked] = useState(false); // limit с запасом: уже состоящих в группе отфильтруем ниже, до 3 подсказок. const { data: foundUsers } = useUserSearch(newNick, 10); if (isLoading) return ; 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 : "Ошибка"); } }; // Подсказки: уже состоящих в группе не предлагаем, показываем до 3. // Дофильтровка по текущему вводу — на клиенте: пока сервер отвечает на новую // букву, показанный список (keepPreviousData) сужается мгновенно. const memberIds = new Set((members ?? []).map((m) => m.user_id)); const needle = newNick.trim().toLowerCase(); const suggestions = (foundUsers ?? []) .filter((u) => !memberIds.has(u.user_id) && u.nickname.toLowerCase().includes(needle)) .slice(0, 3); const showSuggest = nickFocused && !nickPicked && newNick.trim().length > 0 && suggestions.length > 0; if (!groupId) { return (

Активная группа не выбрана

Создайте группу, выберите существующую или примите приглашение, чтобы вести учёт партий.

); } return (

{group?.name ?? "Группа"}

{group && ( )}

Партий: {stats?.total_matches ?? 0} {stats?.last_match_at ? ` · последняя: ${formatDate(stats.last_match_at)}` : ""}

Список игроков

{stats && ( )} {/* Приглашение нового игрока — здесь, рядом со списком (не в настройках). */} {(members ?? []).length >= MAX_GROUP_SIZE ? (

Достигнут максимум участников ({MAX_GROUP_SIZE}).

) : (
{ setNewNick(e.target.value); setNickPicked(false); }} onFocus={() => setNickFocused(true)} onBlur={() => setNickFocused(false)} onKeyDown={(e) => { if (e.key === "Enter") invite(); if (e.key === "Escape") e.currentTarget.blur(); }} /> {showSuggest && ( { setNewNick(u.nickname); setNickPicked(true); }} /> )}
)}

Партии

{matches && }
); }