Files
flxn-app/src/features/tournaments/components/seed-tournament.tsx
2026-02-09 23:36:04 -06:00

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;