more bracket work

This commit is contained in:
2025-09-04 11:37:33 -05:00
parent 2f6950ee9e
commit d2e6849bca
25 changed files with 8459 additions and 115 deletions

View File

@@ -0,0 +1,48 @@
import React, { useCallback, useMemo } from "react";
import { Text, ScrollArea } from "@mantine/core";
import { MatchCard } from "./match-card";
import { BracketData } from "../types";
import { Bracket } from "./bracket";
import useAppShellHeight from "@/hooks/use-appshell-height";
interface BracketViewProps {
bracket: BracketData;
onAnnounce?: (teamOne: any, teamTwo: any) => void;
}
const BracketView: React.FC<BracketViewProps> = ({ bracket, onAnnounce }) => {
const height = useAppShellHeight();
const orders = useMemo(() => {
const map: Record<number, number> = {};
bracket.winners.flat().forEach(match => map[match.lid] = match.order);
bracket.losers.flat().forEach(match => map[match.lid] = match.order);
return map;
}, [bracket.winners, bracket.losers]);
return <ScrollArea
h={`calc(${height} - 4rem)`}
className="bracket-container"
style={{
backgroundImage: `radial-gradient(circle, var(--mantine-color-default-border) 1px, transparent 1px)`,
backgroundSize: "16px 16px",
backgroundPosition: "0 0, 8px 8px",
}}
>
<div>
<Text fw={600} size="md" m={16}>
Winners Bracket
</Text>
<Bracket rounds={bracket.winners} orders={orders} onAnnounce={onAnnounce} />
</div>
{bracket.losers && (
<div>
<Text fw={600} size="md" m={16}>
Losers Bracket
</Text>
<Bracket rounds={bracket.losers} orders={orders} onAnnounce={onAnnounce} />
</div>
)}
</ScrollArea>
};
export default BracketView;

View File

@@ -0,0 +1,44 @@
import { Flex } from "@mantine/core";
import { Match } from "@/features/matches/types";
import { MatchCard } from "./match-card";
interface BracketProps {
rounds: Match[][];
orders: Record<number, number>;
onAnnounce?: (teamOne: any, teamTwo: any) => void;
}
export const Bracket: React.FC<BracketProps> = ({
rounds,
orders,
onAnnounce,
}) => {
return (
<Flex direction="row" gap={24} justify="left" p="xl">
{rounds.map((round, roundIndex) => (
<Flex
key={roundIndex}
direction="column"
align="center"
pos="relative"
gap={24}
justify="space-around"
>
{round
.filter((match) => !match.bye)
.map((match) => {
return (
<div key={match.lid}>
<MatchCard
match={match}
orders={orders}
onAnnounce={onAnnounce}
/>
</div>
);
})}
</Flex>
))}
</Flex>
);
};

View File

@@ -1,72 +1,98 @@
import { ActionIcon, Card, Text } from '@mantine/core';
import { PlayIcon } from '@phosphor-icons/react';
import React, { useCallback, useMemo } from 'react';
import { MatchSlot } from './match-slot';
import { Match } from '../types';
import { ActionIcon, Card, Flex, Text } from "@mantine/core";
import { PlayIcon } from "@phosphor-icons/react";
import React, { useCallback, useMemo } from "react";
import { MatchSlot } from "./match-slot";
import { Match } from "@/features/matches/types";
interface MatchCardProps {
match: Match;
getParentMatchOrder: (parentLid: number) => number | string;
orders: Record<number, number>;
onAnnounce?: (teamOne: any, teamTwo: any) => void;
}
export const MatchCard: React.FC<MatchCardProps> = ({
match,
getParentMatchOrder,
onAnnounce
export const MatchCard: React.FC<MatchCardProps> = ({
match,
orders,
onAnnounce,
}) => {
const homeSlot = useMemo(
() => ({
from: orders[match.home_from_lid],
from_loser: match.home_from_loser,
team: match.home,
seed: match.home_seed,
}),
[match]
);
const awaySlot = useMemo(
() => ({
from: orders[match.away_from_lid],
from_loser: match.away_from_loser,
team: match.away,
seed: match.away_seed,
}),
[match]
);
const showAnnounce = useMemo(() =>
onAnnounce && match.home.team && match.away.team,
[onAnnounce, match.home.team, match.away.team]);
const showAnnounce = useMemo(
() => onAnnounce && match.home && match.away,
[onAnnounce, match.home, match.away]
);
const handleAnnounce = useCallback(() =>
onAnnounce?.(match.home.team, match.away.team), [match.home.team, match.away.team]);
const handleAnnounce = useCallback(
() => onAnnounce?.(match.home, match.away),
[match.home, match.away]
);
return (
<Card
withBorder
pos="relative"
w={200}
style={{ overflow: 'visible' }}
data-match-lid={match.lid}
>
<Card.Section withBorder p={0}>
<MatchSlot slot={match.home} getParentMatchOrder={getParentMatchOrder} />
</Card.Section>
<Card.Section p={0} mb={-16}>
<MatchSlot slot={match.away} getParentMatchOrder={getParentMatchOrder} />
</Card.Section>
{match.reset && (
<Text
pos="absolute"
top={-20}
left={8}
size="xs"
c="dimmed"
fw="bold"
>
* If necessary
</Text>
)}
{showAnnounce && (
<ActionIcon
pos="absolute"
variant="filled"
color="green"
top={-20}
right={-12}
onClick={handleAnnounce}
bd="none"
style={{ boxShadow: 'none' }}
size="xs"
>
<PlayIcon size={12} />
</ActionIcon>
)}
</Card>
<Flex direction="row" align="center" justify="end" gap={8}>
<Text c="dimmed" fw="bolder">
{match.order}
</Text>
<Card
withBorder
pos="relative"
w={200}
style={{ overflow: "visible" }}
data-match-lid={match.lid}
>
<Card.Section withBorder p={0}>
<MatchSlot {...homeSlot} />
</Card.Section>
<Card.Section p={0} mb={-16}>
<MatchSlot {...awaySlot} />
</Card.Section>
{match.reset && (
<Text
pos="absolute"
top={-20}
left={8}
size="xs"
c="dimmed"
fw="bold"
>
* If necessary
</Text>
)}
{showAnnounce && (
<ActionIcon
pos="absolute"
variant="filled"
color="green"
top={-20}
right={-12}
onClick={handleAnnounce}
bd="none"
style={{ boxShadow: "none" }}
size="xs"
>
<PlayIcon size={12} />
</ActionIcon>
)}
</Card>
</Flex>
);
};
};

View File

@@ -1,51 +1,35 @@
import { Flex, Text } from "@mantine/core";
import React from "react";
import { SeedBadge } from "./seed-badge";
import { TeamInfo } from "@/features/teams/types";
interface MatchSlotProps {
slot: any;
getParentMatchOrder: (parentLid: number) => number | string;
from?: number;
from_loser?: boolean;
team?: TeamInfo;
seed?: number;
}
export const MatchSlot: React.FC<MatchSlotProps> = ({
slot,
getParentMatchOrder,
}) => {
const renderSlotContent = () => {
if (slot?.seed) {
return slot.team ? (
<Text size="xs">{slot.team.name}</Text>
) : (
<Text size="xs" c="dimmed">
Team {slot.seed}
</Text>
);
}
if (slot?.parent_lid !== null && slot?.parent_lid !== undefined) {
return (
from,
from_loser,
team,
seed,
}) => (
<Flex align="stretch">
{seed && <SeedBadge seed={seed} />}
<Flex p="4px 8px">
{team ? (
<Text size="xs">{team.name}</Text>
) : from ? (
<Text c="dimmed" size="xs">
{slot.loser ? "Loser" : "Winner"} of Match{" "}
{getParentMatchOrder(slot.parent_lid)}
{from_loser ? "Loser" : "Winner"} of Match {from}
</Text>
);
}
if (slot) {
return (
<Text c="dimmed" size="xs" fs="italic">
) : (
<Text c="dimmed" size="xs">
TBD
</Text>
);
}
return null;
};
return (
<Flex align="stretch">
{slot?.seed && <SeedBadge seed={slot.seed} />}
<div style={{ flex: 1, padding: "4px 8px" }}>{renderSlotContent()}</div>
)}
</Flex>
);
};
</Flex>
);