diff --git a/frontend/src/components/PickerSelect.tsx b/frontend/src/components/PickerSelect.tsx new file mode 100644 index 0000000..a042c00 --- /dev/null +++ b/frontend/src/components/PickerSelect.tsx @@ -0,0 +1,69 @@ +import { ChevronDown } from "lucide-react"; +import { type ReactNode, useState } from "react"; + +/** + * Выпадающий выбор «как select» в стиле подсказок ников (.suggest-*): + * кнопка-триггер с выбранным значением, под ней список опций. Родитель сам + * фильтрует options (например, убирает выбранное в других строках) и передаёт + * selected, найденный в ПОЛНОМ списке: выбранное значение может отсутствовать + * в options (например, фракция занята другой строкой после «Рандом всем»). + */ +export function PickerSelect({ + selected, + options, + placeholder, + renderOption, + renderValue, + onPick, +}: { + selected: T | null; + options: T[]; + placeholder: string; + renderOption: (option: T) => ReactNode; + /** Отрисовка выбранного в триггере; по умолчанию — как renderOption. */ + renderValue?: (option: T) => ReactNode; + onPick: (option: T) => void; +}) { + const [open, setOpen] = useState(false); + return ( +
+ + {open && options.length > 0 && ( +
+ {options.map((o) => ( + + ))} +
+ )} +
+ ); +} diff --git a/frontend/src/components/PlayerSelect.tsx b/frontend/src/components/PlayerSelect.tsx deleted file mode 100644 index 8c9612e..0000000 --- a/frontend/src/components/PlayerSelect.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { ChevronDown } from "lucide-react"; -import { useState } from "react"; - -import { Avatar } from "./Avatar"; -import { PlayerSuggestions, type PlayerSuggestion } from "./PlayerSuggestions"; - -/** - * Выбор игрока «как select», но в стиле выпадающих подсказок ников: - * кнопка-триггер с аватаром и ником, под ней список options. Родитель сам - * фильтрует options (например, убирает выбранных в других строках). - */ -export function PlayerSelect({ - value, - options, - onPick, -}: { - value: number | ""; - options: PlayerSuggestion[]; - onPick: (userId: number) => void; -}) { - const [open, setOpen] = useState(false); - const selected = value === "" ? null : options.find((o) => o.user_id === value) ?? null; - return ( -
- - {open && ( - { - onPick(u.user_id); - setOpen(false); - }} - /> - )} -
- ); -} diff --git a/frontend/src/pages/CreateMatchPage.tsx b/frontend/src/pages/CreateMatchPage.tsx index 128b962..e6aa1e9 100644 --- a/frontend/src/pages/CreateMatchPage.tsx +++ b/frontend/src/pages/CreateMatchPage.tsx @@ -3,7 +3,8 @@ import { useEffect, useRef, useState } from "react"; import { useNavigate } from "react-router-dom"; import { ApiError } from "../api/client"; -import { PlayerSelect } from "../components/PlayerSelect"; +import { Avatar } from "../components/Avatar"; +import { PickerSelect } from "../components/PickerSelect"; import { Spinner } from "../components/Spinner"; import { MAX_MATCH_PLAYERS } from "../domain/constants"; import { useToast } from "../context/ToastContext"; @@ -168,6 +169,20 @@ export function CreateMatchPage() { const memberOptions = (members ?? []) as MemberRead[]; const spinning = Object.values(spin).some((v) => v != null); + // Опции выпадашек. Игрок — аватар + ник (в триггере аватар мельче, чтобы + // поле не разъезжалось по высоте с соседними). + const playerOptions = memberOptions.map((m) => ({ + id: m.user_id, + nickname: m.nickname, + avatar_url: m.avatar_url, + })); + const playerLabel = (m: { nickname: string; avatar_url?: string | null }, size: number) => ( + <> + + {m.nickname} + + ); + return (
@@ -189,43 +204,26 @@ export function CreateMatchPage() { )}
- !takenUserIds(idx).includes(m.user_id)) - .map((m) => ({ - user_id: m.user_id, - nickname: m.nickname, - avatar_url: m.avatar_url, - }))} - onPick={(id) => update(idx, { user_id: id })} + m.id === row.user_id) ?? null} + options={playerOptions.filter((m) => !takenUserIds(idx).includes(m.id))} + placeholder="— игрок —" + renderOption={(m) => playerLabel(m, 28)} + renderValue={(m) => playerLabel(m, 24)} + onPick={(m) => update(idx, { user_id: m.id })} />
{spin[idx] != null ? (
{spin[idx]}
) : ( - + f.id === row.faction_id) ?? null} + options={(factions ?? []).filter((f) => !takenFactionIds(idx).includes(f.id))} + placeholder="— фракция —" + renderOption={(f) => f.name_ru} + onPick={(f) => update(idx, { faction_id: f.id, was_random: false })} + /> )}