Files
flxn-app/src/features/tournaments/components/enrollment/team-section.tsx
T

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;