more bracket work
This commit is contained in:
48
src/features/bracket/components/bracket-view.tsx
Normal file
48
src/features/bracket/components/bracket-view.tsx
Normal 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;
|
||||
44
src/features/bracket/components/bracket.tsx
Normal file
44
src/features/bracket/components/bracket.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user