Files
hidden11/components/PlayerSearch.tsx
T
alexandrev-tibco fa04b20e30 Initial hidden11 MVP
2026-05-02 20:50:36 +02:00

205 lines
8.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useMemo, useState } from "react";
import { POSITIONS } from "@/lib/game-engine";
import type { Dictionary } from "@/lib/i18n";
import type { Player, Position } from "@/lib/types";
type PlayerSearchProps = {
disabled: boolean;
fixedPosition: Position;
players: Player[];
selectedPosition: Position;
t: Dictionary;
onSelectPosition: (position: Position) => void;
onSelect: (playerId: string) => void;
};
export default function PlayerSearch({
disabled,
fixedPosition,
players,
selectedPosition,
t,
onSelectPosition,
onSelect,
}: PlayerSearchProps) {
const [query, setQuery] = useState("");
const [modalOpen, setModalOpen] = useState(false);
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>
<span className="block truncate text-xs text-white/55">
{preview.club} · {preview.nationality}
</span>
</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>
<span className="mt-0.5 block text-xs text-white/55">
{player.club} · {player.nationality}
</span>
</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>
<span className="mt-0.5 block text-xs text-white/55">
{player.club} · {player.league} · {player.nationality}
</span>
</button>
))}
</div>
</div>
</div>
</div>
) : null}
</section>
);
}