init matches, tournament runner
This commit is contained in:
@@ -11,7 +11,7 @@ const Layout: React.FC<PropsWithChildren> = ({ children }) => {
|
||||
const { header } = useRouterConfig();
|
||||
const viewport = useVisualViewportSize();
|
||||
const [scrollPosition, setScrollPosition] = useState({ x: 0, y: 0 });
|
||||
const { withPadding } = useRouterConfig();
|
||||
const { withPadding, fullWidth } = useRouterConfig();
|
||||
|
||||
return (
|
||||
<AppShell
|
||||
@@ -43,7 +43,7 @@ const Layout: React.FC<PropsWithChildren> = ({ children }) => {
|
||||
style={{ transition: 'none' }}
|
||||
>
|
||||
<Pullable scrollPosition={scrollPosition} onScrollPositionChange={setScrollPosition}>
|
||||
<Page noPadding={!withPadding}>
|
||||
<Page noPadding={!withPadding} fullWidth={fullWidth}>
|
||||
{children}
|
||||
</Page>
|
||||
</Pullable>
|
||||
|
||||
@@ -33,7 +33,8 @@ const useRouterConfig = () => {
|
||||
return {
|
||||
header: headerConfig,
|
||||
refresh: current && typeof current === 'object' && 'refresh' in current ? current.refresh : [],
|
||||
withPadding: current && typeof current === 'object' && 'withPadding' in current ? current.withPadding : true
|
||||
withPadding: current && typeof current === 'object' && 'withPadding' in current ? current.withPadding : true,
|
||||
fullWidth: current && typeof current === 'object' && 'fullWidth' in current ? current.fullWidth : false,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ export interface Match {
|
||||
away_from_lid: number;
|
||||
home_from_loser: boolean;
|
||||
away_from_loser: boolean;
|
||||
bracket_type: 'winners' | 'losers';
|
||||
is_losers_bracket: 'winners' | 'losers';
|
||||
tournament_id: string;
|
||||
home_id: string;
|
||||
away_id: string;
|
||||
@@ -39,7 +39,7 @@ export const matchInputSchema = z.object({
|
||||
away_from_lid: z.number().int().min(1).optional(),
|
||||
home_from_loser: z.boolean().optional().default(false),
|
||||
away_from_loser: z.boolean().optional().default(false),
|
||||
losers_bracket: z.boolean().optional().default(false),
|
||||
is_losers_bracket: z.boolean().optional().default(false),
|
||||
tournament_id: z.string().min(1),
|
||||
home_id: z.string().min(1).optional(),
|
||||
away_id: z.string().min(1).optional(),
|
||||
|
||||
@@ -6,10 +6,12 @@ import TournamentForm from "./tournament-form";
|
||||
import {
|
||||
HardDrivesIcon,
|
||||
PencilLineIcon,
|
||||
TreeStructureIcon,
|
||||
UsersThreeIcon,
|
||||
} from "@phosphor-icons/react";
|
||||
import { useSheet } from "@/hooks/use-sheet";
|
||||
import EditEnrolledTeams from "./edit-enrolled-teams";
|
||||
import ListLink from "@/components/list-link";
|
||||
|
||||
interface ManageTournamentProps {
|
||||
tournamentId: string;
|
||||
@@ -53,6 +55,11 @@ const ManageTournament = ({ tournamentId }: ManageTournamentProps) => {
|
||||
Icon={UsersThreeIcon}
|
||||
onClick={openEditTeams}
|
||||
/>
|
||||
<ListLink
|
||||
label="Run Tournament"
|
||||
Icon={TreeStructureIcon}
|
||||
to={`/admin/tournaments/run/${tournamentId}`}
|
||||
/>
|
||||
</List>
|
||||
|
||||
<Sheet
|
||||
|
||||
181
src/features/tournaments/components/run-tournament.tsx
Normal file
181
src/features/tournaments/components/run-tournament.tsx
Normal file
@@ -0,0 +1,181 @@
|
||||
import { useTournament } from '../queries'
|
||||
import { Box, Grid, NumberInput, Stack, Text, Title, Group, Flex, Divider, ScrollArea, Button } from '@mantine/core'
|
||||
import { useState, useMemo, useEffect } from 'react'
|
||||
import Avatar from '@/components/avatar'
|
||||
import { useBracketPreview } from '@/features/bracket/queries'
|
||||
import Bracket from '@/features/bracket/components/bracket'
|
||||
import { createBracketMaps, BracketMaps } from '@/features/bracket/utils/bracket-maps'
|
||||
import { Match, BracketData } from '@/features/bracket/types'
|
||||
|
||||
interface RunTournamentProps {
|
||||
tournamentId: string
|
||||
}
|
||||
|
||||
interface TeamWithSeed {
|
||||
id: string
|
||||
name: string
|
||||
seed: number
|
||||
}
|
||||
|
||||
const RunTournament = ({ tournamentId }: RunTournamentProps) => {
|
||||
const { data: tournament } = useTournament(tournamentId)
|
||||
const teamCount = tournament?.teams?.length || 0
|
||||
const { data: bracketData, isLoading } = useBracketPreview(teamCount)
|
||||
|
||||
const [teamSeeds, setTeamSeeds] = useState<Record<string, number>>(() => {
|
||||
if (!tournament?.teams) return {}
|
||||
return tournament.teams.reduce((acc, team, index) => {
|
||||
acc[team.id] = index + 1
|
||||
return acc
|
||||
}, {} as Record<string, number>)
|
||||
})
|
||||
|
||||
const [seededWinnersBracket, setSeededWinnersBracket] = useState<Match[][]>([])
|
||||
const [seededLosersBracket, setSeededLosersBracket] = useState<Match[][]>([])
|
||||
const [bracketMaps, setBracketMaps] = useState<BracketMaps | null>(null)
|
||||
|
||||
const sortedTeams = useMemo(() => {
|
||||
if (!tournament?.teams) return []
|
||||
|
||||
return tournament.teams
|
||||
.map(team => ({
|
||||
...team,
|
||||
seed: teamSeeds[team.id] || 1,
|
||||
}))
|
||||
.sort((a, b) => a.seed - b.seed)
|
||||
}, [tournament?.teams, teamSeeds])
|
||||
|
||||
const handleSeedChange = (teamId: string, newSeed: number) => {
|
||||
if (newSeed < 1 || !tournament?.teams) return
|
||||
|
||||
setTeamSeeds(prev => {
|
||||
const currSeed = prev[teamId]
|
||||
const newSeeds = { ...prev }
|
||||
|
||||
const otherTeams = tournament.teams!.filter(team => team.id !== teamId)
|
||||
|
||||
if (newSeed !== currSeed) {
|
||||
const currTeam = otherTeams.find(team => prev[team.id] === newSeed)
|
||||
|
||||
if (currTeam) {
|
||||
const affectedTeams = otherTeams.filter(team => {
|
||||
const teamSeed = prev[team.id]
|
||||
return newSeed < currSeed
|
||||
? teamSeed >= newSeed && teamSeed < currSeed
|
||||
: teamSeed > currSeed && teamSeed <= newSeed
|
||||
})
|
||||
|
||||
affectedTeams.forEach(team => {
|
||||
const teamSeed = prev[team.id]
|
||||
if (newSeed < currSeed) {
|
||||
newSeeds[team.id] = teamSeed + 1
|
||||
} else {
|
||||
newSeeds[team.id] = teamSeed - 1
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
newSeeds[teamId] = newSeed
|
||||
}
|
||||
|
||||
return newSeeds
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!bracketData || !tournament?.teams || sortedTeams.length === 0) return
|
||||
|
||||
const maps = createBracketMaps(bracketData)
|
||||
setBracketMaps(maps)
|
||||
|
||||
const mapBracket = (bracket: Match[][]) => {
|
||||
return bracket.map((round) =>
|
||||
round.map((match) => {
|
||||
const mappedMatch = { ...match }
|
||||
|
||||
if (match.home?.seed && match.home.seed > 0) {
|
||||
const team = sortedTeams.find(t => t.seed === match.home.seed)
|
||||
if (team) {
|
||||
mappedMatch.home = {
|
||||
...match.home,
|
||||
team: team,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (match.away?.seed && match.away.seed > 0) {
|
||||
const team = sortedTeams.find(t => t.seed === match.away.seed)
|
||||
if (team) {
|
||||
mappedMatch.away = {
|
||||
...match.away,
|
||||
team: team,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return mappedMatch
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
setSeededWinnersBracket(mapBracket(bracketData.winners))
|
||||
setSeededLosersBracket(mapBracket(bracketData.losers))
|
||||
}, [bracketData, sortedTeams])
|
||||
|
||||
if (!tournament) throw new Error('Tournament not found.')
|
||||
|
||||
return (
|
||||
<Grid>
|
||||
<Grid.Col span={3}>
|
||||
<ScrollArea offsetScrollbars type='always' h={700}>
|
||||
<Title order={3}>Team Seeds</Title>
|
||||
{sortedTeams.map((team) => (
|
||||
<>
|
||||
<Group
|
||||
key={team.id}
|
||||
justify="space-between"
|
||||
p={4}
|
||||
>
|
||||
<Group gap="xs" style={{ flex: 1 }}>
|
||||
<Avatar size={24} name={team.name} />
|
||||
<Text fw={500} size="sm" truncate>{team.name}</Text>
|
||||
</Group>
|
||||
<NumberInput
|
||||
value={teamSeeds[team.id]}
|
||||
onChange={(value) => handleSeedChange(team.id, Number(value) || 1)}
|
||||
min={1}
|
||||
max={tournament.teams?.length || 1}
|
||||
size="xs"
|
||||
w={50}
|
||||
styles={{ input: { textAlign: 'center' } }}
|
||||
step={-1}
|
||||
/>
|
||||
</Group>
|
||||
<Divider />
|
||||
</>
|
||||
))}
|
||||
</ScrollArea>
|
||||
<Button fullWidth mt='md' onClick={() => console.log(sortedTeams)}>Start Tournament</Button>
|
||||
</Grid.Col>
|
||||
|
||||
<Grid.Col span={9}>
|
||||
<Stack gap="md">
|
||||
<Title order={3}>Tournament Bracket</Title>
|
||||
{isLoading ? (
|
||||
<Flex justify="center" align="center" h="400px">
|
||||
<Text c="dimmed">Loading bracket...</Text>
|
||||
</Flex>
|
||||
) : (
|
||||
<Bracket
|
||||
winners={seededWinnersBracket}
|
||||
losers={seededLosersBracket}
|
||||
bracketMaps={bracketMaps}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
|
||||
export default RunTournament
|
||||
Reference in New Issue
Block a user