new typeahead

This commit is contained in:
yohlo
2025-09-25 16:11:54 -05:00
parent c0ef535001
commit b3ebf46afa
7 changed files with 314 additions and 195 deletions

View File

@@ -1,11 +1,11 @@
import {
Autocomplete,
Stack,
ActionIcon,
Text,
Group,
Loader,
} from "@mantine/core";
import Typeahead, { TypeaheadOption } from "@/components/typeahead";
import { TrashIcon } from "@phosphor-icons/react";
import { useState, useCallback, useMemo, memo } from "react";
import { useTournament, useUnenrolledTeams } from "../queries";
@@ -68,8 +68,6 @@ const TeamItem = memo(({ team, onUnenroll, disabled }: TeamItemProps) => {
});
const EditEnrolledTeams = ({ tournamentId }: EditEnrolledTeamsProps) => {
const [search, setSearch] = useState("");
const { data: tournament, isLoading: tournamentLoading } =
useTournament(tournamentId);
const { data: unenrolledTeams = [], isLoading: unenrolledLoading } =
@@ -78,27 +76,24 @@ const EditEnrolledTeams = ({ tournamentId }: EditEnrolledTeamsProps) => {
const { mutate: enrollTeam, isPending: isEnrolling } = useEnrollTeam();
const { mutate: unenrollTeam, isPending: isUnenrolling } = useUnenrollTeam();
const autocompleteData = useMemo(
() =>
unenrolledTeams.map((team: Team) => ({
value: team.id,
label: team.name,
})),
[unenrolledTeams]
);
const searchTeams = async (query: string): Promise<TypeaheadOption<Team>[]> => {
if (!query.trim()) return [];
const filtered = unenrolledTeams.filter((team: Team) =>
team.name.toLowerCase().includes(query.toLowerCase())
);
return filtered.map((team: Team) => ({
id: team.id,
data: team
}));
};
const handleEnrollTeam = useCallback(
(teamId: string) => {
enrollTeam(
{ tournamentId, teamId },
{
onSuccess: () => {
setSearch("");
},
}
);
(option: TypeaheadOption<Team>) => {
enrollTeam({ tournamentId, teamId: option.data.id });
},
[enrollTeam, tournamentId, setSearch]
[enrollTeam, tournamentId]
);
const handleUnenrollTeam = useCallback(
@@ -108,6 +103,31 @@ const EditEnrolledTeams = ({ tournamentId }: EditEnrolledTeamsProps) => {
[unenrollTeam, tournamentId]
);
const renderTeamOption = (option: TypeaheadOption<Team>) => {
const team = option.data;
return (
<Group py="xs" px="sm" gap="sm" align="center">
<Avatar
size={32}
radius="sm"
name={team.name}
src={
team.logo
? `/api/files/teams/${team.id}/${team.logo}`
: undefined
}
/>
<Text fw={500} truncate>
{team.name}
</Text>
</Group>
);
};
const formatTeam = (option: TypeaheadOption<Team>) => {
return option.data.name;
};
const isLoading = tournamentLoading || unenrolledLoading;
const enrolledTeams = tournament?.teams || [];
const hasEnrolledTeams = enrolledTeams.length > 0;
@@ -118,16 +138,13 @@ const EditEnrolledTeams = ({ tournamentId }: EditEnrolledTeamsProps) => {
<Text fw={600} size="sm">
Add Team
</Text>
<Autocomplete
<Typeahead
placeholder="Search for teams to enroll..."
data={autocompleteData}
value={search}
onChange={setSearch}
onOptionSubmit={handleEnrollTeam}
onSelect={handleEnrollTeam}
searchFn={searchTeams}
renderOption={renderTeamOption}
format={formatTeam}
disabled={isEnrolling || unenrolledLoading}
rightSection={isEnrolling ? <Loader size="xs" /> : null}
maxDropdownHeight={200}
limit={10}
/>
</Stack>