better brackets, info types
This commit is contained in:
205
src/features/tournaments/components/seed-tournament.tsx
Normal file
205
src/features/tournaments/components/seed-tournament.tsx
Normal file
@@ -0,0 +1,205 @@
|
||||
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 Avatar from "@/components/avatar";
|
||||
import { useBracketPreview } from "@/features/bracket/queries";
|
||||
import { BracketData } from "@/features/bracket/types";
|
||||
import BracketView from "@/features/bracket/components/bracket-view";
|
||||
|
||||
interface SeedTournamentProps {
|
||||
tournamentId: string;
|
||||
teams: TeamInfo[];
|
||||
onSuccess?: () => void;
|
||||
}
|
||||
|
||||
const SeedTournament: React.FC<SeedTournamentProps> = ({
|
||||
tournamentId,
|
||||
teams,
|
||||
onSuccess,
|
||||
}) => {
|
||||
const [orderedTeams, setOrderedTeams] = useState<TeamInfo[]>(teams);
|
||||
const { data: bracketPreview } = useBracketPreview(teams.length);
|
||||
|
||||
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: () => {
|
||||
onSuccess?.();
|
||||
},
|
||||
});
|
||||
|
||||
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,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<Avatar size={24} radius="sm" name={team.name} />
|
||||
|
||||
<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;
|
||||
Reference in New Issue
Block a user