281 lines
7.5 KiB
TypeScript
281 lines
7.5 KiB
TypeScript
import {
|
|
Stack,
|
|
ActionIcon,
|
|
Text,
|
|
Group,
|
|
Loader,
|
|
Box,
|
|
Button as MantineButton,
|
|
} from "@mantine/core";
|
|
import Typeahead, { TypeaheadOption } from "@/components/typeahead";
|
|
import { TrashIcon, PencilIcon, MusicNotesIcon } from "@phosphor-icons/react";
|
|
import { useState, useCallback, useMemo, memo } from "react";
|
|
import { Trans, Plural, useLingui } from "@lingui/react/macro";
|
|
import { useNavigate } from "@tanstack/react-router";
|
|
import { useTournament, useUnenrolledTeams } from "../../queries";
|
|
import useEnrollTeam from "../../hooks/use-enroll-team";
|
|
import useUnenrollTeam from "../../hooks/use-unenroll-team";
|
|
import TeamAvatar from "@/components/team-avatar";
|
|
import { Team, TeamInfo } from "@/features/teams/types";
|
|
import Sheet from "@/components/sheet/sheet";
|
|
import TeamForm from "@/features/teams/components/team-form";
|
|
import { useTeam } from "@/features/teams/queries";
|
|
import { useSheet } from "@/hooks/use-sheet";
|
|
|
|
interface TeamSectionProps {
|
|
tournamentId: string;
|
|
}
|
|
|
|
interface TeamItemProps {
|
|
team: TeamInfo;
|
|
onUnenroll: (teamId: string) => void;
|
|
onEdit: (teamId: string) => void;
|
|
disabled: boolean;
|
|
isRegional?: boolean;
|
|
}
|
|
|
|
const TeamItem = memo(({ team, onUnenroll, onEdit, disabled, isRegional }: TeamItemProps) => {
|
|
const playerNames = useMemo(
|
|
() =>
|
|
team.players?.map((p) => `${p.first_name} ${p.last_name}`).join(", ") ||
|
|
"",
|
|
[team.players]
|
|
);
|
|
|
|
return (
|
|
<Group py="xs" px="sm" w="100%" gap="sm" align="center">
|
|
<TeamAvatar
|
|
team={team}
|
|
size={32}
|
|
radius="sm"
|
|
isRegional={isRegional}
|
|
/>
|
|
<Stack gap={0} style={{ flex: 1, minWidth: 0 }}>
|
|
<Text fw={500} truncate>
|
|
{team.name}
|
|
</Text>
|
|
{playerNames && (
|
|
<Text size="xs" c="dimmed" truncate>
|
|
{playerNames}
|
|
</Text>
|
|
)}
|
|
</Stack>
|
|
<ActionIcon
|
|
variant="subtle"
|
|
onClick={() => onEdit(team.id)}
|
|
size="sm"
|
|
>
|
|
<PencilIcon size={14} />
|
|
</ActionIcon>
|
|
<ActionIcon
|
|
variant="subtle"
|
|
color="red"
|
|
onClick={() => onUnenroll(team.id)}
|
|
disabled={disabled}
|
|
size="sm"
|
|
>
|
|
<TrashIcon size={14} />
|
|
</ActionIcon>
|
|
</Group>
|
|
);
|
|
});
|
|
|
|
interface TeamEditSheetProps {
|
|
teamId: string;
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
}
|
|
|
|
const TeamEditSheet = ({ teamId, isOpen, onClose }: TeamEditSheetProps) => {
|
|
const { data: team } = useTeam(teamId);
|
|
const { t } = useLingui();
|
|
|
|
return (
|
|
<Sheet
|
|
title={team ? t`Edit ${team.name}` : t`Edit Team`}
|
|
opened={isOpen}
|
|
onChange={onClose}
|
|
>
|
|
{team && (
|
|
<TeamForm
|
|
teamId={team.id}
|
|
initialValues={{
|
|
...team,
|
|
players: team.players ? team.players.map((p) => p.id) : [],
|
|
logo: typeof team.logo === "string" ? undefined : team.logo,
|
|
}}
|
|
close={onClose}
|
|
/>
|
|
)}
|
|
</Sheet>
|
|
);
|
|
};
|
|
|
|
const TeamSection = ({ tournamentId }: TeamSectionProps) => {
|
|
const { t } = useLingui();
|
|
const navigate = useNavigate();
|
|
const { data: tournament, isLoading: tournamentLoading } =
|
|
useTournament(tournamentId);
|
|
const { data: unenrolledTeams = [], isLoading: unenrolledLoading } =
|
|
useUnenrolledTeams(tournamentId);
|
|
|
|
const isRegional = tournament?.regional;
|
|
|
|
const { mutate: enrollTeam, isPending: isEnrolling } = useEnrollTeam();
|
|
const { mutate: unenrollTeam, isPending: isUnenrolling } = useUnenrollTeam();
|
|
|
|
const [selectedTeamId, setSelectedTeamId] = useState<string | null>(null);
|
|
const {
|
|
isOpen: editTeamOpened,
|
|
open: openEditTeam,
|
|
close: closeEditTeam,
|
|
} = useSheet();
|
|
|
|
const handleEditTeam = useCallback(
|
|
(teamId: string) => {
|
|
setSelectedTeamId(teamId);
|
|
openEditTeam();
|
|
},
|
|
[openEditTeam]
|
|
);
|
|
|
|
const handleCloseEditTeam = useCallback(() => {
|
|
setSelectedTeamId(null);
|
|
closeEditTeam();
|
|
}, [closeEditTeam]);
|
|
|
|
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(
|
|
(option: TypeaheadOption<Team>) => {
|
|
enrollTeam({ tournamentId, teamId: option.data.id });
|
|
},
|
|
[enrollTeam, tournamentId]
|
|
);
|
|
|
|
const handleUnenrollTeam = useCallback(
|
|
(teamId: string) => {
|
|
unenrollTeam({ tournamentId, teamId });
|
|
},
|
|
[unenrollTeam, tournamentId]
|
|
);
|
|
|
|
const renderTeamOption = (option: TypeaheadOption<Team>) => {
|
|
const team = option.data;
|
|
return (
|
|
<Group py="xs" px="sm" gap="sm" align="center">
|
|
<TeamAvatar
|
|
team={team as any}
|
|
size={32}
|
|
radius="sm"
|
|
isRegional={isRegional}
|
|
/>
|
|
<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;
|
|
|
|
return (
|
|
<>
|
|
<Stack gap="lg" w="100%">
|
|
<Box
|
|
pos="sticky"
|
|
top={0}
|
|
py="xs"
|
|
style={{ zIndex: 5, backgroundColor: "var(--mantine-color-body)" }}
|
|
>
|
|
<Stack gap="xs">
|
|
<Text fw={600} size="sm">
|
|
<Trans>Add Team</Trans>
|
|
</Text>
|
|
<Typeahead
|
|
placeholder={t`Search for teams to enroll...`}
|
|
onSelect={handleEnrollTeam}
|
|
searchFn={searchTeams}
|
|
renderOption={renderTeamOption}
|
|
format={formatTeam}
|
|
disabled={isEnrolling || unenrolledLoading}
|
|
/>
|
|
</Stack>
|
|
</Box>
|
|
|
|
<MantineButton
|
|
fullWidth
|
|
variant="light"
|
|
leftSection={<MusicNotesIcon size={18} />}
|
|
onClick={() =>
|
|
navigate({ to: `/admin/tournaments/${tournamentId}/teams` })
|
|
}
|
|
>
|
|
<Trans>Manage Team Songs/Logos</Trans>
|
|
</MantineButton>
|
|
|
|
<Stack gap="xs">
|
|
<Group justify="space-between">
|
|
<Text fw={600} size="sm">
|
|
<Trans>Enrolled Teams</Trans>
|
|
</Text>
|
|
<Text size="xs" c="dimmed">
|
|
<Plural value={enrolledTeams.length} one="# team" other="# teams" />
|
|
</Text>
|
|
</Group>
|
|
|
|
{isLoading ? (
|
|
<Group justify="center" py="md">
|
|
<Loader size="sm" />
|
|
</Group>
|
|
) : !hasEnrolledTeams ? (
|
|
<Text size="sm" c="dimmed" ta="center" py="lg">
|
|
<Trans>No teams enrolled yet</Trans>
|
|
</Text>
|
|
) : (
|
|
<Stack gap="xs" w="100%">
|
|
{enrolledTeams.map((team: TeamInfo) => (
|
|
<TeamItem
|
|
key={team.id}
|
|
team={team}
|
|
onUnenroll={handleUnenrollTeam}
|
|
onEdit={handleEditTeam}
|
|
disabled={isUnenrolling}
|
|
isRegional={isRegional}
|
|
/>
|
|
))}
|
|
</Stack>
|
|
)}
|
|
</Stack>
|
|
</Stack>
|
|
|
|
{selectedTeamId && (
|
|
<TeamEditSheet
|
|
teamId={selectedTeamId}
|
|
isOpen={editTeamOpened}
|
|
onClose={handleCloseEditTeam}
|
|
/>
|
|
)}
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default TeamSection;
|