better brackets, info types

This commit is contained in:
yohlo
2025-09-07 00:52:28 -05:00
parent cb83ea06fa
commit 2396464a19
36 changed files with 678 additions and 657 deletions

View File

@@ -12,14 +12,14 @@ import { useTournament, useUnenrolledTeams } from "../queries";
import useEnrollTeam from "../hooks/use-enroll-team";
import useUnenrollTeam from "../hooks/use-unenroll-team";
import Avatar from "@/components/avatar";
import { Team } from "@/features/teams/types";
import { Team, TeamInfo } from "@/features/teams/types";
interface EditEnrolledTeamsProps {
tournamentId: string;
}
interface TeamItemProps {
team: Team;
team: TeamInfo;
onUnenroll: (teamId: string) => void;
disabled: boolean;
}
@@ -142,7 +142,7 @@ const EditEnrolledTeams = ({ tournamentId }: EditEnrolledTeamsProps) => {
</Text>
) : (
<Stack gap="xs" w="100%">
{enrolledTeams.map((team: Team) => (
{enrolledTeams.map((team: TeamInfo) => (
<TeamItem
key={team.id}
team={team}

View 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;

View File

@@ -1,11 +1,11 @@
import { List, ListItem, Skeleton, Text } from "@mantine/core";
import { useNavigate } from "@tanstack/react-router";
import Avatar from "@/components/avatar";
import { Tournament } from "../types";
import { TournamentInfo } from "../types";
import { useCallback } from "react";
interface TournamentListProps {
tournaments: Tournament[];
tournaments: TournamentInfo[];
loading?: boolean;
}

View File

@@ -1,6 +1,16 @@
import { Team } from "@/features/teams/types";
import { TeamInfo } from "@/features/teams/types";
import { Match } from "@/features/matches/types";
import { z } from "zod";
export interface TournamentInfo {
id: string;
name: string;
location?: string;
start_time?: string;
end_time?: string;
logo?: string;
}
export interface Tournament {
id: string;
name: string;
@@ -13,7 +23,8 @@ export interface Tournament {
end_time?: string;
created: string;
updated: string;
teams?: Team[];
teams?: TeamInfo[];
matches?: Match[];
}
export const tournamentInputSchema = z.object({