UI: подсказки ников при приглашении игрока в группу

При вводе в поле приглашения («Список игроков») под инпутом всплывают
до 3 подходящих игроков — аватар + ник, в стилистике строк топов.
Клик подставляет ник в поле (приглашение отправляется кнопкой/Enter),
уже состоящие в группе не предлагаются.

Детали: компонент PlayerSuggestions (выбор по mousedown — до blur
инпута), хук useUserSearch с дебаунсом 250 мс и keepPreviousData,
классы .suggest-* в духе .notif-panel/.lb-row. В поиске общего списка
подсказки не нужны — таблица уже фильтруется вживую при вводе.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-17 02:30:55 +03:00
co-authored by Claude Fable 5
parent e5b7420bae
commit ddbcd74058
5 changed files with 147 additions and 8 deletions
+39 -8
View File
@@ -8,6 +8,7 @@ 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";
@@ -20,6 +21,7 @@ import {
useGroupStats,
useInviteMember,
} from "../hooks/groups";
import { useUserSearch } from "../hooks/users";
export function GroupPage() {
const { data: me, isLoading } = useMe();
@@ -33,6 +35,12 @@ export function GroupPage() {
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 <Spinner />;
@@ -48,6 +56,12 @@ export function GroupPage() {
}
};
// Подсказки: уже состоящих в группе не предлагаем, показываем до 3.
const memberIds = new Set((members ?? []).map((m) => m.user_id));
const suggestions = (foundUsers ?? []).filter((u) => !memberIds.has(u.user_id)).slice(0, 3);
const showSuggest =
nickFocused && !nickPicked && newNick.trim().length > 0 && suggestions.length > 0;
if (!groupId) {
return (
<div>
@@ -99,14 +113,31 @@ export function GroupPage() {
</p>
) : (
<div className="row" style={{ marginTop: 10 }}>
<input
placeholder="Ник игрока — отправить приглашение"
value={newNick}
onChange={(e) => setNewNick(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") invite();
}}
/>
<div className="suggest-wrap">
<input
placeholder="Ник игрока — отправить приглашение"
value={newNick}
onChange={(e) => {
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 && (
<PlayerSuggestions
items={suggestions}
onPick={(u) => {
setNewNick(u.nickname);
setNickPicked(true);
}}
/>
)}
</div>
<button className="btn btn-primary" onClick={invite} disabled={inviteMember.isPending}>
<UserPlus size={18} />
</button>