ready for regionals
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
import React, { useMemo, useState } from "react";
|
||||
import { Stack, Text, Card, Group as MantineGroup, Box, SimpleGrid, Tabs, Collapse, ActionIcon, Button, Alert } from "@mantine/core";
|
||||
import { Stack, Text, Card, Group as MantineGroup, Box, SimpleGrid, Tabs, Collapse, ActionIcon, Button, Alert, Badge } from "@mantine/core";
|
||||
import { CaretCircleDownIcon, CaretCircleUpIcon } from "@phosphor-icons/react";
|
||||
import { Match } from "@/features/matches/types";
|
||||
import { Group } from "../types";
|
||||
import { Group, GroupConfig } from "../types";
|
||||
import GroupMatchCard from "./group-match-card";
|
||||
import TeamAvatar from "@/components/team-avatar";
|
||||
import { useServerMutation } from "@/lib/tanstack-query/hooks/use-server-mutation";
|
||||
@@ -17,6 +17,7 @@ interface GroupStageViewProps {
|
||||
tournamentId?: string;
|
||||
hasKnockoutBracket?: boolean;
|
||||
isRegional?: boolean;
|
||||
groupConfig?: GroupConfig;
|
||||
}
|
||||
|
||||
interface TeamStanding {
|
||||
@@ -37,6 +38,7 @@ const GroupStageView: React.FC<GroupStageViewProps> = ({
|
||||
tournamentId,
|
||||
hasKnockoutBracket,
|
||||
isRegional,
|
||||
groupConfig,
|
||||
}) => {
|
||||
const queryClient = useQueryClient();
|
||||
const [expandedTeams, setExpandedTeams] = useState<Record<string, boolean>>({});
|
||||
@@ -239,6 +241,57 @@ const GroupStageView: React.FC<GroupStageViewProps> = ({
|
||||
});
|
||||
};
|
||||
|
||||
const allGroupStandings = useMemo(() => {
|
||||
return sortedGroups.map((group) => ({
|
||||
groupId: group.id,
|
||||
groupOrder: group.order,
|
||||
standings: getTeamStandings(group.id, group.teams || []),
|
||||
}));
|
||||
}, [sortedGroups, matchesByGroup]);
|
||||
|
||||
const advancingTeams = useMemo(() => {
|
||||
if (!groupConfig) return { qualifiedTeams: new Set<string>(), wildcardTeams: new Set<string>() };
|
||||
|
||||
const advancePerGroup = groupConfig.advance_per_group;
|
||||
const qualifiedTeams = new Set<string>();
|
||||
const wildcardTeams = new Set<string>();
|
||||
|
||||
allGroupStandings.forEach(({ standings }) => {
|
||||
standings.slice(0, advancePerGroup).forEach((standing) => {
|
||||
qualifiedTeams.add(standing.teamId);
|
||||
});
|
||||
});
|
||||
|
||||
const totalQualified = qualifiedTeams.size;
|
||||
const knockoutTeamCount = totalQualified;
|
||||
|
||||
const nextPowerOf2 = Math.pow(2, Math.ceil(Math.log2(knockoutTeamCount)));
|
||||
const wildcardsNeeded = nextPowerOf2 - knockoutTeamCount;
|
||||
|
||||
if (wildcardsNeeded > 0) {
|
||||
const allNonQualifiedTeams: TeamStanding[] = [];
|
||||
|
||||
allGroupStandings.forEach(({ standings }) => {
|
||||
standings.slice(advancePerGroup).forEach((standing) => {
|
||||
allNonQualifiedTeams.push(standing);
|
||||
});
|
||||
});
|
||||
|
||||
allNonQualifiedTeams.sort((a, b) => {
|
||||
if (b.wins !== a.wins) return b.wins - a.wins;
|
||||
if (b.cupDifference !== a.cupDifference) return b.cupDifference - a.cupDifference;
|
||||
if (b.cupsFor !== a.cupsFor) return b.cupsFor - a.cupsFor;
|
||||
return a.teamId.localeCompare(b.teamId);
|
||||
});
|
||||
|
||||
allNonQualifiedTeams.slice(0, wildcardsNeeded).forEach((standing) => {
|
||||
wildcardTeams.add(standing.teamId);
|
||||
});
|
||||
}
|
||||
|
||||
return { qualifiedTeams, wildcardTeams };
|
||||
}, [allGroupStandings, groupConfig]);
|
||||
|
||||
if (sortedGroups.length === 0) {
|
||||
return (
|
||||
<Box p="md">
|
||||
@@ -321,44 +374,64 @@ const GroupStageView: React.FC<GroupStageViewProps> = ({
|
||||
<Collapse in={expandedTeams[group.id]}>
|
||||
<Stack gap={0}>
|
||||
{standings.length > 0 ? (
|
||||
standings.map((standing, index) => (
|
||||
<MantineGroup
|
||||
key={standing.teamId}
|
||||
gap="sm"
|
||||
align="center"
|
||||
wrap="nowrap"
|
||||
px="md"
|
||||
py="xs"
|
||||
style={{
|
||||
borderTop: index > 0 ? '1px solid var(--mantine-color-default-border)' : 'none',
|
||||
}}
|
||||
>
|
||||
<Text size="sm" fw={700} c="dimmed" w={24} ta="center">
|
||||
{index + 1}
|
||||
</Text>
|
||||
<TeamAvatar team={standing.team} size={28} radius="sm" isRegional={isRegional} />
|
||||
<Text size="sm" fw={500} style={{ flex: 1 }} lineClamp={1}>
|
||||
{standing.teamName}
|
||||
</Text>
|
||||
<MantineGroup gap="xs" wrap="nowrap">
|
||||
<Text size="xs" c="dimmed" fw={500} miw={35} ta="center">
|
||||
{standing.wins}-{standing.losses}
|
||||
</Text>
|
||||
<Text
|
||||
size="xs"
|
||||
c={standing.cupDifference > 0 ? "green" : standing.cupDifference < 0 ? "red" : "dimmed"}
|
||||
fw={600}
|
||||
miw={30}
|
||||
ta="center"
|
||||
>
|
||||
{standing.cupDifference > 0 ? '+' : ''}{standing.cupDifference}
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed" fw={400} miw={40} ta="center">
|
||||
{standing.cupsFor}/{standing.cupsAgainst}
|
||||
standings.map((standing, index) => {
|
||||
const isQualified = advancingTeams.qualifiedTeams.has(standing.teamId);
|
||||
const isWildcard = advancingTeams.wildcardTeams.has(standing.teamId);
|
||||
const isAdvancing = isQualified || isWildcard;
|
||||
|
||||
return (
|
||||
<MantineGroup
|
||||
key={standing.teamId}
|
||||
gap="sm"
|
||||
align="center"
|
||||
wrap="nowrap"
|
||||
px="md"
|
||||
py="xs"
|
||||
style={{
|
||||
borderTop: index > 0 ? '1px solid var(--mantine-color-default-border)' : 'none',
|
||||
backgroundColor: isAdvancing ? 'var(--mantine-primary-color-light)' : undefined,
|
||||
borderLeft: isAdvancing ? '3px solid var(--mantine-primary-color-filled)' : '3px solid transparent',
|
||||
}}
|
||||
>
|
||||
<Text size="sm" fw={700} c="dimmed" w={24} ta="center">
|
||||
{index + 1}
|
||||
</Text>
|
||||
<TeamAvatar team={standing.team} size={28} radius="sm" isRegional={isRegional} />
|
||||
<Box style={{ flex: 1, minWidth: 0 }}>
|
||||
<Text size="sm" fw={500} lineClamp={1}>
|
||||
{standing.teamName}
|
||||
</Text>
|
||||
</Box>
|
||||
<MantineGroup gap="xs" wrap="nowrap">
|
||||
{isWildcard && (
|
||||
<Badge size="xs" color="yellow" variant="light">
|
||||
WC
|
||||
</Badge>
|
||||
)}
|
||||
{isQualified && (
|
||||
<Badge size="xs" variant="light">
|
||||
Q
|
||||
</Badge>
|
||||
)}
|
||||
<Text size="xs" c="dimmed" fw={500} miw={35} ta="center">
|
||||
{standing.wins}-{standing.losses}
|
||||
</Text>
|
||||
<Text
|
||||
size="xs"
|
||||
c={standing.cupDifference > 0 ? "green" : standing.cupDifference < 0 ? "red" : "dimmed"}
|
||||
fw={600}
|
||||
miw={30}
|
||||
ta="center"
|
||||
>
|
||||
{standing.cupDifference > 0 ? '+' : ''}{standing.cupDifference}
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed" fw={400} miw={40} ta="center">
|
||||
{standing.cupsFor}/{standing.cupsAgainst}
|
||||
</Text>
|
||||
</MantineGroup>
|
||||
</MantineGroup>
|
||||
</MantineGroup>
|
||||
))
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<Text size="sm" c="dimmed" ta="center" py="md">
|
||||
No teams assigned
|
||||
|
||||
@@ -80,12 +80,16 @@ const SetupGroupStage: React.FC<SetupGroupStageProps> = ({
|
||||
return null;
|
||||
}
|
||||
|
||||
const nextPowerOf2 = Math.pow(2, Math.ceil(Math.log2(knockoutTeamCount)));
|
||||
const bracketSize = nextPowerOf2;
|
||||
const wildcardsNeeded = bracketSize - knockoutTeamCount;
|
||||
|
||||
let bracketTemplate: any;
|
||||
if (Object.keys(brackets).includes(knockoutTeamCount.toString())) {
|
||||
bracketTemplate = brackets[knockoutTeamCount as keyof typeof brackets];
|
||||
if (Object.keys(brackets).includes(bracketSize.toString())) {
|
||||
bracketTemplate = brackets[bracketSize as keyof typeof brackets];
|
||||
} else {
|
||||
try {
|
||||
bracketTemplate = generateSingleEliminationBracket(knockoutTeamCount);
|
||||
bracketTemplate = generateSingleEliminationBracket(bracketSize);
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
@@ -113,6 +117,10 @@ const SetupGroupStage: React.FC<SetupGroupStageProps> = ({
|
||||
seedLabels[seedIndex++] = `${groupName2} ${rankSuffix2}`;
|
||||
}
|
||||
|
||||
for (let i = 0; i < wildcardsNeeded; i++) {
|
||||
seedLabels[seedIndex++] = `Wildcard ${i + 1}`;
|
||||
}
|
||||
|
||||
const ordersMap: Record<number, number> = {};
|
||||
bracketTemplate.winners.forEach((round: any[]) => {
|
||||
round.forEach((match: any) => {
|
||||
@@ -265,10 +273,11 @@ const SetupGroupStage: React.FC<SetupGroupStageProps> = ({
|
||||
<Box>
|
||||
<Divider mb="lg" />
|
||||
<Title order={3} ta="center" mb="md">
|
||||
Knockout Bracket Preview ({knockoutTeamCount} Teams)
|
||||
Knockout Bracket Preview ({selectedConfig?.knockout_size} Teams)
|
||||
</Title>
|
||||
<Text size="sm" c="dimmed" ta="center" mb="lg">
|
||||
Top {selectedConfig?.advance_per_group} team{selectedConfig?.advance_per_group !== 1 ? 's' : ''} from each group will advance
|
||||
{selectedConfig?.wildcards_needed ? ` + ${selectedConfig.wildcards_needed} wildcard${selectedConfig.wildcards_needed > 1 ? 's' : ''}` : ''}
|
||||
</Text>
|
||||
<Box
|
||||
style={{
|
||||
|
||||
Reference in New Issue
Block a user