feat(admin): dashboard home, telemetry section, enrollment ops screen
- mission-control dashboard: now strip, tournament quick actions, usage sparklines, error feed, live activity, tools list - /admin/telemetry with functions/events/errors/players/usage child routes, hand-rolled svg chart primitives, decomposed activities table - error browser with grouping and mark-resolved - consolidated per-tournament enrollment screen; hub simplified with run tournament promoted - /admin/activities redirects to /admin/telemetry/functions
This commit is contained in:
@@ -1,184 +0,0 @@
|
||||
import {
|
||||
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 { Trans, Plural, useLingui } from "@lingui/react/macro";
|
||||
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";
|
||||
|
||||
interface EditEnrolledTeamsProps {
|
||||
tournamentId: string;
|
||||
}
|
||||
|
||||
interface TeamItemProps {
|
||||
team: TeamInfo;
|
||||
onUnenroll: (teamId: string) => void;
|
||||
disabled: boolean;
|
||||
isRegional?: boolean;
|
||||
}
|
||||
|
||||
const TeamItem = memo(({ team, onUnenroll, 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"
|
||||
color="red"
|
||||
onClick={() => onUnenroll(team.id)}
|
||||
disabled={disabled}
|
||||
size="sm"
|
||||
>
|
||||
<TrashIcon size={14} />
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
);
|
||||
});
|
||||
|
||||
const EditEnrolledTeams = ({ tournamentId }: EditEnrolledTeamsProps) => {
|
||||
const { t } = useLingui();
|
||||
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 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%">
|
||||
<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>
|
||||
|
||||
<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}
|
||||
disabled={isUnenrolling}
|
||||
isRegional={isRegional}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditEnrolledTeams;
|
||||
@@ -0,0 +1,48 @@
|
||||
import { Alert, Group, Stack, Text } from "@mantine/core";
|
||||
import { WarningCircleIcon } from "@phosphor-icons/react";
|
||||
import { Trans, Plural } from "@lingui/react/macro";
|
||||
|
||||
interface EnrollmentStatsProps {
|
||||
count: number;
|
||||
regional?: boolean;
|
||||
}
|
||||
|
||||
const EnrollmentStats = (props: EnrollmentStatsProps) => {
|
||||
const hasOddPlayers = props.count % 2 !== 0;
|
||||
const hasEnoughPlayers = props.count >= 2;
|
||||
|
||||
return (
|
||||
<Stack gap="xs">
|
||||
<Group gap="xs" align="baseline">
|
||||
<Text size="xl" fw={700}>
|
||||
{props.count}
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
{props.regional ? (
|
||||
<Plural
|
||||
value={props.count}
|
||||
one="player enrolled"
|
||||
other="players enrolled"
|
||||
/>
|
||||
) : (
|
||||
<Trans>Enrolled Teams</Trans>
|
||||
)}
|
||||
</Text>
|
||||
</Group>
|
||||
|
||||
{props.regional && !hasEnoughPlayers && (
|
||||
<Alert color="yellow" icon={<WarningCircleIcon size={16} />}>
|
||||
<Trans>Need at least 2 players to create teams</Trans>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{props.regional && hasOddPlayers && (
|
||||
<Alert color="red" icon={<WarningCircleIcon size={16} />}>
|
||||
<Trans>Cannot create teams with an odd number of players. Please have one player unenroll.</Trans>
|
||||
</Alert>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default EnrollmentStats;
|
||||
+43
-20
@@ -4,19 +4,23 @@ import {
|
||||
Text,
|
||||
Group,
|
||||
Loader,
|
||||
Box,
|
||||
} from "@mantine/core";
|
||||
import { TrashIcon } from "@phosphor-icons/react";
|
||||
import { TrashIcon, ShuffleIcon } from "@phosphor-icons/react";
|
||||
import { useCallback, memo } from "react";
|
||||
import { Trans, Plural, useLingui } from "@lingui/react/macro";
|
||||
import { useFreeAgents } from "../queries";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import Button from "@/components/button";
|
||||
import { useFreeAgents } from "../../queries";
|
||||
import PlayerAvatar from "@/components/player-avatar";
|
||||
import { PlayerInfo, Player } from "@/features/players/types";
|
||||
import Typeahead, { TypeaheadOption } from "@/components/typeahead";
|
||||
import { usePlayers } from "@/features/players/queries";
|
||||
import useAdminEnrollPlayer from "../hooks/use-admin-enroll-player";
|
||||
import useAdminUnenrollPlayer from "../hooks/use-admin-unenroll-player";
|
||||
import useAdminEnrollPlayer from "../../hooks/use-admin-enroll-player";
|
||||
import useAdminUnenrollPlayer from "../../hooks/use-admin-unenroll-player";
|
||||
import EnrollmentStats from "./enrollment-stats";
|
||||
|
||||
interface EditEnrolledPlayersProps {
|
||||
interface FreeAgentSectionProps {
|
||||
tournamentId: string;
|
||||
}
|
||||
|
||||
@@ -51,8 +55,9 @@ const PlayerItem = memo(({ player, onRemove, disabled }: PlayerItemProps) => {
|
||||
);
|
||||
});
|
||||
|
||||
const EditEnrolledPlayers = ({ tournamentId }: EditEnrolledPlayersProps) => {
|
||||
const FreeAgentSection = ({ tournamentId }: FreeAgentSectionProps) => {
|
||||
const { t } = useLingui();
|
||||
const navigate = useNavigate();
|
||||
const { data: freeAgents = [], isLoading } = useFreeAgents(tournamentId);
|
||||
const { data: allPlayers = [] } = usePlayers();
|
||||
|
||||
@@ -112,19 +117,37 @@ const EditEnrolledPlayers = ({ tournamentId }: EditEnrolledPlayersProps) => {
|
||||
|
||||
return (
|
||||
<Stack gap="lg" w="100%">
|
||||
<Stack gap="xs">
|
||||
<Text fw={600} size="sm">
|
||||
<Trans>Add Player</Trans>
|
||||
</Text>
|
||||
<Typeahead
|
||||
placeholder={t`Search for players to enroll...`}
|
||||
onSelect={handleEnrollPlayer}
|
||||
searchFn={searchPlayers}
|
||||
renderOption={renderPlayerOption}
|
||||
format={formatPlayer}
|
||||
disabled={isEnrolling}
|
||||
/>
|
||||
</Stack>
|
||||
<EnrollmentStats count={enrolledPlayers.length} regional />
|
||||
|
||||
<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 Player</Trans>
|
||||
</Text>
|
||||
<Typeahead
|
||||
placeholder={t`Search for players to enroll...`}
|
||||
onSelect={handleEnrollPlayer}
|
||||
searchFn={searchPlayers}
|
||||
renderOption={renderPlayerOption}
|
||||
format={formatPlayer}
|
||||
disabled={isEnrolling}
|
||||
/>
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
<Button
|
||||
leftSection={<ShuffleIcon size={18} />}
|
||||
onClick={() =>
|
||||
navigate({ to: `/admin/tournaments/${tournamentId}/assign-partners` })
|
||||
}
|
||||
>
|
||||
<Trans>Assign Partners</Trans>
|
||||
</Button>
|
||||
|
||||
<Stack gap="xs">
|
||||
<Group justify="space-between">
|
||||
@@ -161,4 +184,4 @@ const EditEnrolledPlayers = ({ tournamentId }: EditEnrolledPlayersProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default EditEnrolledPlayers;
|
||||
export default FreeAgentSection;
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Tournament } from "../../types";
|
||||
import FreeAgentSection from "./free-agent-section";
|
||||
import TeamSection from "./team-section";
|
||||
|
||||
interface EnrollmentProps {
|
||||
tournament: Tournament;
|
||||
}
|
||||
|
||||
const Enrollment = ({ tournament }: EnrollmentProps) => {
|
||||
return tournament.regional === true ? (
|
||||
<FreeAgentSection tournamentId={tournament.id} />
|
||||
) : (
|
||||
<TeamSection tournamentId={tournament.id} />
|
||||
);
|
||||
};
|
||||
|
||||
export default Enrollment;
|
||||
@@ -0,0 +1,283 @@
|
||||
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";
|
||||
import EnrollmentStats from "./enrollment-stats";
|
||||
|
||||
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%">
|
||||
<EnrollmentStats count={enrolledTeams.length} />
|
||||
|
||||
<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;
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useTournament } from "../queries";
|
||||
import { List } from "@mantine/core";
|
||||
import { Box, List } from "@mantine/core";
|
||||
import ListButton from "@/components/list-button";
|
||||
import Sheet from "@/components/sheet/sheet";
|
||||
import TournamentForm from "./tournament-form";
|
||||
@@ -8,16 +8,13 @@ import {
|
||||
PencilLineIcon,
|
||||
TreeStructureIcon,
|
||||
UsersThreeIcon,
|
||||
UsersIcon,
|
||||
ShuffleIcon,
|
||||
} from "@phosphor-icons/react";
|
||||
import { useSheet } from "@/hooks/use-sheet";
|
||||
import EditEnrolledTeams from "./edit-enrolled-teams";
|
||||
import EditEnrolledPlayers from "./edit-enrolled-players";
|
||||
import ListLink from "@/components/list-link";
|
||||
import React from "react";
|
||||
import EditRules from "./edit-rules";
|
||||
import { useLingui } from "@lingui/react/macro";
|
||||
import Button from "@/components/button";
|
||||
import { Trans, useLingui } from "@lingui/react/macro";
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
|
||||
interface ManageTournamentProps {
|
||||
tournamentId: string;
|
||||
@@ -25,6 +22,7 @@ interface ManageTournamentProps {
|
||||
|
||||
const ManageTournament = ({ tournamentId }: ManageTournamentProps) => {
|
||||
const { t } = useLingui();
|
||||
const navigate = useNavigate();
|
||||
const { data: tournament } = useTournament(tournamentId);
|
||||
|
||||
if (!tournament) throw new Error("Tournament not found.");
|
||||
@@ -38,17 +36,26 @@ const ManageTournament = ({ tournamentId }: ManageTournamentProps) => {
|
||||
open: openEditRules,
|
||||
close: closeEditRules,
|
||||
} = useSheet();
|
||||
const {
|
||||
isOpen: editTeamsOpened,
|
||||
open: openEditTeams,
|
||||
close: closeEditTeams,
|
||||
} = useSheet();
|
||||
|
||||
const [v, setV] = React.useState("");
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box px="md" py="md">
|
||||
<Button
|
||||
leftSection={<TreeStructureIcon weight="bold" size={20} />}
|
||||
onClick={() =>
|
||||
navigate({ to: `/admin/tournaments/run/${tournamentId}` })
|
||||
}
|
||||
>
|
||||
<Trans>Run Tournament</Trans>
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<List p="0">
|
||||
<ListLink
|
||||
label={t`Enrollment & Teams`}
|
||||
Icon={UsersThreeIcon}
|
||||
to={`/admin/tournaments/${tournamentId}/enrollment`}
|
||||
/>
|
||||
<ListButton
|
||||
label={t`Edit Tournament`}
|
||||
Icon={HardDrivesIcon}
|
||||
@@ -59,30 +66,6 @@ const ManageTournament = ({ tournamentId }: ManageTournamentProps) => {
|
||||
Icon={PencilLineIcon}
|
||||
onClick={openEditRules}
|
||||
/>
|
||||
<ListButton
|
||||
label={t`Edit Enrollments`}
|
||||
Icon={UsersThreeIcon}
|
||||
onClick={openEditTeams}
|
||||
/>
|
||||
{tournament.regional && (
|
||||
<ListLink
|
||||
label={t`Assign Partners`}
|
||||
Icon={ShuffleIcon}
|
||||
to={`/admin/tournaments/${tournamentId}/assign-partners`}
|
||||
/>
|
||||
)}
|
||||
{!tournament.regional && (
|
||||
<ListLink
|
||||
label={t`Manage Team Songs/Logos`}
|
||||
Icon={UsersIcon}
|
||||
to={`/admin/tournaments/${tournamentId}/teams`}
|
||||
/>
|
||||
)}
|
||||
<ListLink
|
||||
label={t`Run Tournament`}
|
||||
Icon={TreeStructureIcon}
|
||||
to={`/admin/tournaments/run/${tournamentId}`}
|
||||
/>
|
||||
</List>
|
||||
|
||||
<Sheet
|
||||
@@ -112,18 +95,6 @@ const ManageTournament = ({ tournamentId }: ManageTournamentProps) => {
|
||||
>
|
||||
<EditRules tournamentId={tournamentId} onClose={closeEditRules} />
|
||||
</Sheet>
|
||||
|
||||
<Sheet
|
||||
title={tournament.regional === true ? t`Manage Enrollments` : t`Edit Enrolled Teams`}
|
||||
opened={editTeamsOpened}
|
||||
onChange={closeEditTeams}
|
||||
>
|
||||
{tournament.regional === true ? (
|
||||
<EditEnrolledPlayers tournamentId={tournamentId} />
|
||||
) : (
|
||||
<EditEnrolledTeams tournamentId={tournamentId} />
|
||||
)}
|
||||
</Sheet>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user