123 lines
4.3 KiB
Svelte
123 lines
4.3 KiB
Svelte
<script lang="ts">
|
|
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@components/ui/command";
|
|
import { Popover, PopoverContent, PopoverTrigger } from "@components/ui/popover";
|
|
import { Button } from "@components/ui/button";
|
|
import { Check, ChevronsUpDown } from "lucide-svelte";
|
|
import { cn } from "@components/utils";
|
|
import { dataRepo } from "@repo/data";
|
|
import type { Player } from "@type/data";
|
|
|
|
let {
|
|
value = $bindable(null),
|
|
multiple = false,
|
|
placeholder = "Select player...",
|
|
onSelect,
|
|
}: {
|
|
value?: number | number[] | null;
|
|
multiple?: boolean;
|
|
placeholder?: string;
|
|
onSelect?: (player: Player) => void;
|
|
} = $props();
|
|
|
|
let open = $state(false);
|
|
let search = $state("");
|
|
let players: Player[] = $state([]);
|
|
let loading = $state(false);
|
|
|
|
let debounceTimer: NodeJS.Timeout;
|
|
|
|
function fetchPlayers(searchTerm: string) {
|
|
clearTimeout(debounceTimer);
|
|
debounceTimer = setTimeout(async () => {
|
|
loading = true;
|
|
try {
|
|
const res = await $dataRepo.queryPlayers(searchTerm || undefined, undefined, undefined, 50, 0, false, true);
|
|
players = res.entries;
|
|
} finally {
|
|
loading = false;
|
|
}
|
|
}, 300);
|
|
}
|
|
|
|
$effect(() => {
|
|
fetchPlayers(search);
|
|
});
|
|
|
|
function handleSelect(player: Player) {
|
|
if (onSelect) {
|
|
onSelect(player);
|
|
}
|
|
|
|
if (multiple) {
|
|
if (Array.isArray(value)) {
|
|
if (value.includes(player.id!)) {
|
|
value = value.filter((v) => v !== player.id);
|
|
} else {
|
|
value = [...value, player.id!];
|
|
}
|
|
} else {
|
|
value = [player.id!];
|
|
}
|
|
} else {
|
|
if (value === player.id) {
|
|
value = null; // Deselect
|
|
} else {
|
|
value = player.id;
|
|
open = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
function isSelected(id: number) {
|
|
if (multiple) {
|
|
return Array.isArray(value) && value.includes(id);
|
|
}
|
|
return value === id;
|
|
}
|
|
|
|
let triggerLabel = $derived.by(() => {
|
|
if (multiple) {
|
|
if (Array.isArray(value) && value.length > 0) {
|
|
return `${placeholder} (${value.length})`;
|
|
}
|
|
return placeholder;
|
|
} else {
|
|
// We might need to fetch the selected player's name if it's not in the current list
|
|
// For now, let's just show the placeholder or "Selected"
|
|
// Ideally we would have a way to resolve the name from the UUID if it's not in `players`
|
|
// But `players` only contains search results.
|
|
// If we want to show the name, we might need to fetch it or pass it in.
|
|
// Given the context of AuditLog, it shows "Spieler Filter (count)".
|
|
// Given RefereesList, it's a button "Hinzufügen".
|
|
return placeholder;
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<Popover bind:open>
|
|
<PopoverTrigger>
|
|
{#snippet child({ props })}
|
|
<Button variant="outline" class={cn("justify-between", Array.isArray(value) && !value?.length && "text-muted-foreground")} {...props} role="combobox" aria-expanded={open}>
|
|
{triggerLabel}
|
|
<ChevronsUpDown class="ml-2 size-4 shrink-0 opacity-50" />
|
|
</Button>
|
|
{/snippet}
|
|
</PopoverTrigger>
|
|
<PopoverContent class="p-0">
|
|
<Command shouldFilter={false}>
|
|
<CommandInput bind:value={search} placeholder="Search players..." />
|
|
<CommandList>
|
|
<CommandEmpty>No players found.</CommandEmpty>
|
|
<CommandGroup>
|
|
{#each players as player (player.uuid)}
|
|
<CommandItem value={player.id?.toString()} onSelect={() => handleSelect(player)}>
|
|
<Check class={cn("mr-2 size-4", isSelected(player.id!) ? "opacity-100" : "opacity-0")} />
|
|
{player.name}
|
|
</CommandItem>
|
|
{/each}
|
|
</CommandGroup>
|
|
</CommandList>
|
|
</Command>
|
|
</PopoverContent>
|
|
</Popover>
|