216 lines
6.8 KiB
TypeScript
216 lines
6.8 KiB
TypeScript
import React, { useState, useEffect, useCallback, useMemo } from "react";
|
|
import {
|
|
Stack,
|
|
Text,
|
|
Group,
|
|
ActionIcon,
|
|
Button,
|
|
NumberInput,
|
|
LoadingOverlay,
|
|
} from "@mantine/core";
|
|
import { DragDropContext, Droppable, Draggable } from "@hello-pangea/dnd";
|
|
import { DotsNineIcon } from "@phosphor-icons/react";
|
|
import { useServerMutation } from "@/lib/tanstack-query/hooks/use-server-mutation";
|
|
import { generateTournamentBracket } from "../../matches/server";
|
|
import { TeamInfo } from "@/features/teams/types";
|
|
import TeamAvatar from "@/components/team-avatar";
|
|
import { useBracketPreview } from "@/features/bracket/queries";
|
|
import { BracketData } from "@/features/bracket/types";
|
|
import BracketView from "@/features/bracket/components/bracket-view";
|
|
import { useQueryClient } from "@tanstack/react-query";
|
|
import { tournamentKeys } from "../queries";
|
|
|
|
interface SeedTournamentProps {
|
|
tournamentId: string;
|
|
teams: TeamInfo[];
|
|
isRegional?: boolean;
|
|
}
|
|
|
|
const SeedTournament: React.FC<SeedTournamentProps> = ({
|
|
tournamentId,
|
|
teams,
|
|
isRegional,
|
|
}) => {
|
|
const [orderedTeams, setOrderedTeams] = useState<TeamInfo[]>(teams);
|
|
const { data: bracketPreview } = useBracketPreview(teams.length);
|
|
const queryClient = useQueryClient();
|
|
|
|
const bracket: BracketData = useMemo(
|
|
() => ({
|
|
winners: bracketPreview.winners.map((round) =>
|
|
round.map((match) => ({
|
|
...match,
|
|
away:
|
|
match.away_seed !== undefined
|
|
? orderedTeams[match.away_seed - 1]
|
|
: undefined,
|
|
home:
|
|
match.home_seed !== undefined
|
|
? orderedTeams[match.home_seed - 1]
|
|
: undefined,
|
|
}))
|
|
),
|
|
losers: bracketPreview.losers,
|
|
}),
|
|
[bracketPreview, orderedTeams]
|
|
);
|
|
|
|
const generateBracket = useServerMutation({
|
|
mutationFn: generateTournamentBracket,
|
|
successMessage: "Tournament bracket generated successfully!",
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({
|
|
queryKey: tournamentKeys.details(tournamentId),
|
|
});
|
|
},
|
|
});
|
|
|
|
const handleDragEnd = (result: any) => {
|
|
if (!result.destination) return;
|
|
|
|
const items = Array.from(orderedTeams);
|
|
const [reorderedItem] = items.splice(result.source.index, 1);
|
|
items.splice(result.destination.index, 0, reorderedItem);
|
|
|
|
setOrderedTeams(items);
|
|
};
|
|
|
|
const handleSeedChange = (teamId: string, newSeed: number) => {
|
|
if (newSeed < 1 || newSeed > orderedTeams.length) return;
|
|
|
|
const currentIndex = orderedTeams.findIndex((t) => t.id === teamId);
|
|
if (currentIndex === -1) return;
|
|
|
|
const targetIndex = newSeed - 1;
|
|
const items = Array.from(orderedTeams);
|
|
const [movedTeam] = items.splice(currentIndex, 1);
|
|
items.splice(targetIndex, 0, movedTeam);
|
|
|
|
setOrderedTeams(items);
|
|
};
|
|
|
|
const handleGenerateBracket = () => {
|
|
const orderedTeamIds = orderedTeams.map((team) => team.id);
|
|
|
|
generateBracket.mutate({
|
|
data: {
|
|
tournamentId,
|
|
orderedTeamIds,
|
|
},
|
|
});
|
|
};
|
|
|
|
return (
|
|
<div style={{ display: "flex", gap: "2rem", alignItems: "flex-start" }}>
|
|
<Stack gap="lg" style={{ flexShrink: 0 }}>
|
|
<Stack gap={0} pos="relative" w={400}>
|
|
<LoadingOverlay visible={generateBracket.isPending} />
|
|
|
|
<Group gap="xs" p="md" pb="sm" align="center">
|
|
<Text fw={600} size="lg">
|
|
Team Seeding
|
|
</Text>
|
|
<Text size="sm" c="dimmed" ml="auto">
|
|
{orderedTeams.length} teams
|
|
</Text>
|
|
</Group>
|
|
|
|
<Button
|
|
size="sm"
|
|
w={400}
|
|
onClick={handleGenerateBracket}
|
|
loading={generateBracket.isPending}
|
|
disabled={orderedTeams.length === 0}
|
|
>
|
|
Confirm Seeding
|
|
</Button>
|
|
|
|
<DragDropContext onDragEnd={handleDragEnd}>
|
|
<Droppable droppableId="teams">
|
|
{(provided) => (
|
|
<div {...provided.droppableProps} ref={provided.innerRef}>
|
|
{orderedTeams.map((team, index) => (
|
|
<Draggable
|
|
key={team.id}
|
|
draggableId={team.id}
|
|
index={index}
|
|
>
|
|
{(provided) => (
|
|
<div
|
|
ref={provided.innerRef}
|
|
{...provided.draggableProps}
|
|
style={{
|
|
...provided.draggableProps.style,
|
|
borderBottom:
|
|
"1px solid var(--mantine-color-dimmed)",
|
|
}}
|
|
>
|
|
<Group align="center" gap="sm" p="sm" px="md">
|
|
<ActionIcon
|
|
{...provided.dragHandleProps}
|
|
variant="subtle"
|
|
color="gray"
|
|
size="xs"
|
|
>
|
|
<DotsNineIcon size={14} />
|
|
</ActionIcon>
|
|
|
|
<NumberInput
|
|
value={index + 1}
|
|
onChange={(value) =>
|
|
handleSeedChange(team.id, Number(value) || 1)
|
|
}
|
|
min={1}
|
|
max={orderedTeams.length}
|
|
size="xs"
|
|
w={50}
|
|
styles={{
|
|
input: {
|
|
textAlign: "center",
|
|
fontWeight: 600,
|
|
height: 28,
|
|
},
|
|
}}
|
|
/>
|
|
|
|
<TeamAvatar
|
|
team={team}
|
|
size={24}
|
|
radius="sm"
|
|
isRegional={isRegional}
|
|
/>
|
|
|
|
<Text fw={500} size="sm" style={{ flex: 1 }}>
|
|
{team.name}
|
|
</Text>
|
|
</Group>
|
|
</div>
|
|
)}
|
|
</Draggable>
|
|
))}
|
|
{provided.placeholder}
|
|
</div>
|
|
)}
|
|
</Droppable>
|
|
</DragDropContext>
|
|
</Stack>
|
|
<Button
|
|
size="sm"
|
|
w={400}
|
|
onClick={handleGenerateBracket}
|
|
loading={generateBracket.isPending}
|
|
disabled={orderedTeams.length === 0}
|
|
>
|
|
Confirm Seeding
|
|
</Button>
|
|
</Stack>
|
|
|
|
<div style={{ flex: 1, overflow: "auto" }}>
|
|
<BracketView bracket={bracket} />
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default SeedTournament;
|