214 lines
8.6 KiB
TypeScript
214 lines
8.6 KiB
TypeScript
import { useMemo, useState } from "react";
|
||
import { POSITIONS } from "@/lib/game-engine";
|
||
import type { Dictionary } from "@/lib/i18n";
|
||
import type { Difficulty, Player, Position } from "@/lib/types";
|
||
|
||
type PlayerSearchProps = {
|
||
disabled: boolean;
|
||
difficulty: Difficulty;
|
||
fixedPosition: Position;
|
||
players: Player[];
|
||
selectedPosition: Position;
|
||
t: Dictionary;
|
||
onSelectPosition: (position: Position) => void;
|
||
onSelect: (playerId: string) => void;
|
||
};
|
||
|
||
export default function PlayerSearch({
|
||
disabled,
|
||
difficulty,
|
||
fixedPosition,
|
||
players,
|
||
selectedPosition,
|
||
t,
|
||
onSelectPosition,
|
||
onSelect,
|
||
}: PlayerSearchProps) {
|
||
const [query, setQuery] = useState("");
|
||
const [modalOpen, setModalOpen] = useState(false);
|
||
const revealMetadata = difficulty === "easy";
|
||
const positionPlayers = useMemo(
|
||
() => players.filter((player) => player.position === selectedPosition),
|
||
[players, selectedPosition],
|
||
);
|
||
const matches = useMemo(() => {
|
||
const normalized = query.trim().toLowerCase();
|
||
const source = normalized
|
||
? positionPlayers.filter((player) => player.name.toLowerCase().includes(normalized))
|
||
: positionPlayers;
|
||
|
||
return source.slice(0, normalized ? 6 : 4);
|
||
}, [positionPlayers, query]);
|
||
const preview = query.trim() ? matches[0] : null;
|
||
|
||
function selectPosition(position: Position) {
|
||
onSelectPosition(position);
|
||
setQuery("");
|
||
setModalOpen(false);
|
||
}
|
||
|
||
function choosePlayer(playerId: string) {
|
||
onSelect(playerId);
|
||
setQuery("");
|
||
setModalOpen(false);
|
||
}
|
||
|
||
return (
|
||
<section className="rounded-lg border border-white/10 bg-white/[0.06] p-2 sm:p-3">
|
||
<div className="flex flex-col gap-2 lg:flex-row lg:items-center">
|
||
<div className="hidden gap-2 overflow-x-auto pb-1 lg:flex lg:pb-0" aria-label="Choose position">
|
||
{POSITIONS.map((position) => (
|
||
<button
|
||
key={position}
|
||
type="button"
|
||
disabled={disabled || position === fixedPosition}
|
||
onClick={() => selectPosition(position)}
|
||
className={`h-11 min-w-14 rounded-lg border px-3 text-sm font-black transition ${
|
||
selectedPosition === position
|
||
? "border-white bg-white text-pitch-950"
|
||
: "border-white/15 bg-pitch-950 text-white hover:bg-white/10"
|
||
} ${position === fixedPosition ? "cursor-not-allowed opacity-45" : ""}`}
|
||
title={position === fixedPosition ? t.fixed : t.pickAndValidate(position)}
|
||
>
|
||
{position}
|
||
</button>
|
||
))}
|
||
</div>
|
||
|
||
<div className="relative min-w-0 flex-1">
|
||
<select
|
||
value={selectedPosition}
|
||
disabled={disabled}
|
||
onChange={(event) => selectPosition(event.target.value as Position)}
|
||
className="absolute left-1.5 top-1/2 z-10 h-8 -translate-y-1/2 rounded-md border border-white/10 bg-white/10 px-2 text-xs font-black text-white outline-none disabled:cursor-not-allowed disabled:opacity-40 lg:hidden"
|
||
aria-label="Choose position"
|
||
>
|
||
{POSITIONS.map((position) => (
|
||
<option key={position} value={position} disabled={position === fixedPosition}>
|
||
{position}
|
||
</option>
|
||
))}
|
||
</select>
|
||
<label htmlFor="player-search" className="sr-only">
|
||
{t.playerSearch}
|
||
</label>
|
||
<input
|
||
id="player-search"
|
||
type="search"
|
||
value={query}
|
||
disabled={disabled}
|
||
onChange={(event) => setQuery(event.target.value)}
|
||
placeholder={t.searchPlaceholder(selectedPosition)}
|
||
className="h-11 w-full rounded-lg border border-white/15 bg-pitch-950 py-0 pl-[74px] pr-12 text-sm text-white outline-none transition placeholder:text-white/35 focus:border-white disabled:cursor-not-allowed disabled:opacity-40 lg:pl-3"
|
||
/>
|
||
<button
|
||
type="button"
|
||
disabled={disabled}
|
||
onClick={() => setModalOpen(true)}
|
||
className="absolute right-1.5 top-1/2 flex h-8 w-8 -translate-y-1/2 items-center justify-center rounded-md border border-white/10 bg-white/10 text-white transition hover:bg-white/20 disabled:cursor-not-allowed disabled:opacity-40"
|
||
aria-label={t.browsePlayers(selectedPosition)}
|
||
title={t.browsePlayers(selectedPosition)}
|
||
>
|
||
<span className="grid gap-0.5" aria-hidden="true">
|
||
<span className="block h-0.5 w-4 rounded bg-current" />
|
||
<span className="block h-0.5 w-4 rounded bg-current" />
|
||
<span className="block h-0.5 w-4 rounded bg-current" />
|
||
</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{preview ? (
|
||
<button
|
||
type="button"
|
||
disabled={disabled}
|
||
onClick={() => choosePlayer(preview.id)}
|
||
className="mt-2 flex w-full items-center justify-between gap-3 rounded-md border border-emerald-300/30 bg-emerald-300/10 px-3 py-2 text-left transition hover:bg-emerald-300/15 lg:hidden"
|
||
>
|
||
<span className="min-w-0">
|
||
<span className="block truncate text-sm font-black text-white">{preview.name}</span>
|
||
{revealMetadata ? (
|
||
<span className="block truncate text-xs text-white/55">
|
||
{preview.club} · {preview.nationality}
|
||
</span>
|
||
) : null}
|
||
</span>
|
||
<span className="shrink-0 rounded bg-emerald-300 px-2 py-1 text-[10px] font-black text-pitch-950">
|
||
{selectedPosition}
|
||
</span>
|
||
</button>
|
||
) : null}
|
||
|
||
<div className="mt-3 hidden gap-2 overflow-x-auto pb-1 lg:flex">
|
||
{matches.map((player) => (
|
||
<button
|
||
key={player.id}
|
||
type="button"
|
||
disabled={disabled}
|
||
onClick={() => choosePlayer(player.id)}
|
||
className="min-w-[210px] rounded-lg border border-white/10 bg-pitch-950/80 px-3 py-2 text-left transition hover:bg-white/10 disabled:cursor-not-allowed disabled:opacity-40"
|
||
>
|
||
<span className="flex items-center justify-between gap-3 text-sm font-semibold">
|
||
<span className="truncate">{player.name}</span>
|
||
<span className="rounded bg-white/10 px-1.5 py-0.5 text-[10px]">{player.position}</span>
|
||
</span>
|
||
{revealMetadata ? (
|
||
<span className="mt-0.5 block text-xs text-white/55">
|
||
{player.club} · {player.nationality}
|
||
</span>
|
||
) : null}
|
||
</button>
|
||
))}
|
||
</div>
|
||
|
||
{modalOpen ? (
|
||
<div
|
||
className="fixed inset-0 z-50 flex items-end bg-black/65 p-3 backdrop-blur-sm sm:items-center sm:justify-center"
|
||
role="dialog"
|
||
aria-modal="true"
|
||
aria-labelledby="player-list-title"
|
||
>
|
||
<div className="max-h-[82vh] w-full overflow-hidden rounded-lg border border-white/10 bg-pitch-950 shadow-2xl sm:max-w-lg">
|
||
<div className="flex items-center justify-between gap-3 border-b border-white/10 p-4">
|
||
<div>
|
||
<h2 id="player-list-title" className="text-lg font-black text-white">
|
||
{t.playerListTitle(selectedPosition)}
|
||
</h2>
|
||
<p className="mt-0.5 text-xs text-white/50">{t.playersAvailable(positionPlayers.length)}</p>
|
||
</div>
|
||
<button
|
||
type="button"
|
||
onClick={() => setModalOpen(false)}
|
||
className="h-9 w-9 rounded-full border border-white/15 text-lg font-black text-white transition hover:bg-white/10"
|
||
aria-label={t.closeModal}
|
||
>
|
||
×
|
||
</button>
|
||
</div>
|
||
|
||
<div className="max-h-[62vh] overflow-y-auto p-3">
|
||
<div className="grid gap-2">
|
||
{positionPlayers.map((player) => (
|
||
<button
|
||
key={player.id}
|
||
type="button"
|
||
onClick={() => choosePlayer(player.id)}
|
||
className="rounded-lg border border-white/10 bg-white/[0.06] px-3 py-3 text-left transition hover:bg-white/10"
|
||
>
|
||
<span className="block text-sm font-bold text-white">{player.name}</span>
|
||
{revealMetadata ? (
|
||
<span className="mt-0.5 block text-xs text-white/55">
|
||
{player.club} · {player.league} · {player.nationality}
|
||
</span>
|
||
) : null}
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
) : null}
|
||
</section>
|
||
);
|
||
}
|