i think working bracket runner
This commit is contained in:
@@ -7,10 +7,10 @@ import { Match } from "@/features/matches/types";
|
||||
|
||||
interface BracketViewProps {
|
||||
bracket: BracketData;
|
||||
onStartMatch?: (match: Match) => void;
|
||||
showControls?: boolean
|
||||
}
|
||||
|
||||
const BracketView: React.FC<BracketViewProps> = ({ bracket, onStartMatch }) => {
|
||||
const BracketView: React.FC<BracketViewProps> = ({ bracket, showControls }) => {
|
||||
const height = useAppShellHeight();
|
||||
const orders = useMemo(() => {
|
||||
const map: Record<number, number> = {};
|
||||
@@ -32,14 +32,14 @@ const BracketView: React.FC<BracketViewProps> = ({ bracket, onStartMatch }) => {
|
||||
<Text fw={600} size="md" m={16}>
|
||||
Winners Bracket
|
||||
</Text>
|
||||
<Bracket rounds={bracket.winners} orders={orders} onStartMatch={onStartMatch} />
|
||||
<Bracket rounds={bracket.winners} orders={orders} showControls={showControls} />
|
||||
</div>
|
||||
{bracket.losers && (
|
||||
<div>
|
||||
<Text fw={600} size="md" m={16}>
|
||||
Losers Bracket
|
||||
</Text>
|
||||
<Bracket rounds={bracket.losers} orders={orders} onStartMatch={onStartMatch} />
|
||||
<Bracket rounds={bracket.losers} orders={orders} showControls={showControls} />
|
||||
</div>
|
||||
)}
|
||||
</ScrollArea>
|
||||
|
||||
@@ -5,13 +5,13 @@ import { MatchCard } from "./match-card";
|
||||
interface BracketProps {
|
||||
rounds: Match[][];
|
||||
orders: Record<number, number>;
|
||||
onStartMatch?: (match: Match) => void;
|
||||
showControls?: boolean;
|
||||
}
|
||||
|
||||
export const Bracket: React.FC<BracketProps> = ({
|
||||
rounds,
|
||||
orders,
|
||||
onStartMatch,
|
||||
showControls,
|
||||
}) => {
|
||||
return (
|
||||
<Flex direction="row" gap={24} justify="left" p="xl">
|
||||
@@ -32,7 +32,7 @@ export const Bracket: React.FC<BracketProps> = ({
|
||||
<MatchCard
|
||||
match={match}
|
||||
orders={orders}
|
||||
onStartMatch={onStartMatch}
|
||||
showControls={showControls}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,26 +1,36 @@
|
||||
import { ActionIcon, Card, Flex, Text, Stack } from "@mantine/core";
|
||||
import { ActionIcon, Card, Flex, Text, Stack, Indicator } from "@mantine/core";
|
||||
import { PlayIcon, PencilIcon } from "@phosphor-icons/react";
|
||||
import React, { useCallback, useMemo } from "react";
|
||||
import { MatchSlot } from "./match-slot";
|
||||
import { Match } from "@/features/matches/types";
|
||||
import { useSheet } from "@/hooks/use-sheet";
|
||||
import { MatchForm } from "./match-form";
|
||||
import Sheet from "@/components/sheet/sheet";
|
||||
import { useServerMutation } from "@/lib/tanstack-query/hooks";
|
||||
import { endMatch, startMatch } from "@/features/matches/server";
|
||||
import { tournamentKeys } from "@/features/tournaments/queries";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
interface MatchCardProps {
|
||||
match: Match;
|
||||
orders: Record<number, number>;
|
||||
onStartMatch?: (match: Match) => void;
|
||||
showControls?: boolean;
|
||||
}
|
||||
|
||||
export const MatchCard: React.FC<MatchCardProps> = ({
|
||||
match,
|
||||
orders,
|
||||
onStartMatch,
|
||||
showControls,
|
||||
}) => {
|
||||
const queryClient = useQueryClient();
|
||||
const editSheet = useSheet();
|
||||
const homeSlot = useMemo(
|
||||
() => ({
|
||||
from: orders[match.home_from_lid],
|
||||
from_loser: match.home_from_loser,
|
||||
team: match.home,
|
||||
seed: match.home_seed,
|
||||
cups: match.status === "ended" ? match.home_cups : undefined
|
||||
}),
|
||||
[match]
|
||||
);
|
||||
@@ -30,72 +40,110 @@ export const MatchCard: React.FC<MatchCardProps> = ({
|
||||
from_loser: match.away_from_loser,
|
||||
team: match.away,
|
||||
seed: match.away_seed,
|
||||
cups: match.status === "ended" ? match.away_cups : undefined
|
||||
}),
|
||||
[match]
|
||||
);
|
||||
|
||||
const showToolbar = useMemo(
|
||||
() => match.status === "ready" && onStartMatch,
|
||||
[match.status, onStartMatch]
|
||||
() => match.status === "ready" && showControls,
|
||||
[match.status, showControls]
|
||||
);
|
||||
|
||||
const handleAnnounce = useCallback(
|
||||
() => onStartMatch?.(match),
|
||||
[onStartMatch, match]
|
||||
const showEditButton = useMemo(
|
||||
() => showControls && match.status === "started",
|
||||
[showControls, match.status]
|
||||
);
|
||||
|
||||
const handleEdit = useCallback(() => {
|
||||
// TODO: implement edit functionality
|
||||
console.log('Edit match:', match);
|
||||
const start = useServerMutation({
|
||||
mutationFn: startMatch,
|
||||
successMessage: "Match started!",
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: tournamentKeys.details(match.tournament.id),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const end = useServerMutation({
|
||||
mutationFn: endMatch,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: tournamentKeys.details(match.tournament.id),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const handleStart = useCallback(async () => {
|
||||
await start.mutate({
|
||||
data: match.id,
|
||||
});
|
||||
}, [match]);
|
||||
|
||||
const handleFormSubmit = useCallback(
|
||||
async (data: { home_cups: number; away_cups: number; ot_count: number }) => {
|
||||
await end.mutate({ data: {
|
||||
...data,
|
||||
matchId: match.id
|
||||
}});
|
||||
editSheet.close();
|
||||
},
|
||||
[match.id, editSheet]
|
||||
);
|
||||
|
||||
console.log(homeSlot, awaySlot)
|
||||
|
||||
return (
|
||||
<Flex direction="row" align="center" justify="end" gap={8}>
|
||||
<Text c="dimmed" fw="bolder">
|
||||
{match.order}
|
||||
</Text>
|
||||
<Flex align="stretch">
|
||||
<Card
|
||||
withBorder
|
||||
pos="relative"
|
||||
w={200}
|
||||
style={{ overflow: "visible" }}
|
||||
data-match-lid={match.lid}
|
||||
<Indicator
|
||||
inline
|
||||
processing={match.status === "started"}
|
||||
color="red"
|
||||
size={12}
|
||||
disabled={match.status !== "started" || showEditButton}
|
||||
>
|
||||
<Card.Section withBorder p={0}>
|
||||
<MatchSlot {...homeSlot} />
|
||||
</Card.Section>
|
||||
<Card
|
||||
w={showToolbar || showEditButton ? 200 : 220}
|
||||
withBorder
|
||||
pos="relative"
|
||||
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>
|
||||
<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>
|
||||
)}
|
||||
</Card>
|
||||
{match.reset && (
|
||||
<Text
|
||||
pos="absolute"
|
||||
top={-20}
|
||||
left={8}
|
||||
size="xs"
|
||||
c="dimmed"
|
||||
fw="bold"
|
||||
>
|
||||
* If necessary
|
||||
</Text>
|
||||
)}
|
||||
</Card>
|
||||
</Indicator>
|
||||
|
||||
{showToolbar && (
|
||||
<Flex
|
||||
direction="column"
|
||||
justify="center"
|
||||
align="center"
|
||||
>
|
||||
<Flex direction="column" justify="center" align="center">
|
||||
<ActionIcon
|
||||
color="green"
|
||||
onClick={handleAnnounce}
|
||||
onClick={handleStart}
|
||||
size="sm"
|
||||
h='100%'
|
||||
radius='sm'
|
||||
h="100%"
|
||||
radius="sm"
|
||||
ml={-4}
|
||||
style={{
|
||||
borderTopLeftRadius: 0,
|
||||
@@ -106,7 +154,34 @@ export const MatchCard: React.FC<MatchCardProps> = ({
|
||||
</ActionIcon>
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
{showEditButton && (
|
||||
<Flex direction="column" justify="center" align="center">
|
||||
<ActionIcon
|
||||
color="blue"
|
||||
onClick={editSheet.open}
|
||||
size="sm"
|
||||
h="100%"
|
||||
radius="sm"
|
||||
ml={-4}
|
||||
style={{
|
||||
borderTopLeftRadius: 0,
|
||||
borderBottomLeftRadius: 0,
|
||||
}}
|
||||
>
|
||||
<PencilIcon size={14} />
|
||||
</ActionIcon>
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
<Sheet title="Edit Match" {...editSheet.props}>
|
||||
<MatchForm
|
||||
match={match}
|
||||
onSubmit={handleFormSubmit}
|
||||
onCancel={editSheet.close}
|
||||
/>
|
||||
</Sheet>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
160
src/features/bracket/components/match-form.tsx
Normal file
160
src/features/bracket/components/match-form.tsx
Normal file
@@ -0,0 +1,160 @@
|
||||
import { Button, TextInput, Stack, Group, Text, Flex, Divider } from "@mantine/core";
|
||||
import { useForm } from "@mantine/form";
|
||||
import { Match } from "@/features/matches/types";
|
||||
|
||||
interface MatchFormProps {
|
||||
match: Match;
|
||||
onSubmit: (data: {
|
||||
home_cups: number;
|
||||
away_cups: number;
|
||||
ot_count: number;
|
||||
}) => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
export const MatchForm: React.FC<MatchFormProps> = ({
|
||||
match,
|
||||
onSubmit,
|
||||
onCancel,
|
||||
}) => {
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
home_cups: match.home_cups || 10,
|
||||
away_cups: match.away_cups || 10,
|
||||
ot_count: match.ot_count || 0,
|
||||
},
|
||||
validate: {
|
||||
home_cups: (value, values) => {
|
||||
if (value === null || value === undefined) return "Home cups is required";
|
||||
if (values.ot_count > 0) return null;
|
||||
|
||||
const homeCups = Number(value);
|
||||
const awayCups = Number(values.away_cups);
|
||||
|
||||
if (homeCups !== 10 && awayCups !== 10) {
|
||||
return "At least one team must have 10 cups";
|
||||
}
|
||||
|
||||
// Both teams can't have 10 cups
|
||||
if (homeCups === 10 && awayCups === 10) {
|
||||
return "Both teams cannot have 10 cups";
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
away_cups: (value, values) => {
|
||||
if (value === null || value === undefined) return "Away cups is required";
|
||||
if (values.ot_count > 0) return null;
|
||||
|
||||
const awayCups = Number(value);
|
||||
const homeCups = Number(values.home_cups);
|
||||
|
||||
if (homeCups !== 10 && awayCups !== 10) {
|
||||
return "At least one team must have 10 cups";
|
||||
}
|
||||
|
||||
if (homeCups === 10 && awayCups === 10) {
|
||||
return "Both teams cannot have 10 cups";
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
ot_count: (value) =>
|
||||
value === null || value === undefined
|
||||
? "Overtime count is required"
|
||||
: null,
|
||||
},
|
||||
transformValues: (values) => ({
|
||||
home_cups: Number(values.home_cups),
|
||||
away_cups: Number(values.away_cups),
|
||||
ot_count: Number(values.ot_count),
|
||||
}),
|
||||
});
|
||||
|
||||
const handleSubmit = form.onSubmit(() => {
|
||||
const transformedValues = form.getTransformedValues();
|
||||
onSubmit(transformedValues);
|
||||
});
|
||||
|
||||
console.log(match)
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Stack gap="md">
|
||||
<Flex mx='auto' direction='column' gap='md' miw={250}>
|
||||
|
||||
<Group gap="xs">
|
||||
<Stack gap={0}>
|
||||
<Text fw={500} size="sm">
|
||||
{match.home?.name} Cups
|
||||
</Text>
|
||||
{
|
||||
match.home?.players.map(p => (<Text size='xs' c='dimmed'>
|
||||
{p.first_name} {p.last_name}
|
||||
</Text>))
|
||||
}
|
||||
</Stack>
|
||||
<TextInput
|
||||
ml='auto'
|
||||
type="number"
|
||||
min={0}
|
||||
w={50}
|
||||
ta="center"
|
||||
key={form.key("home_cups")}
|
||||
{...form.getInputProps("home_cups")}
|
||||
/>
|
||||
</Group>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Group gap="xs">
|
||||
<Stack gap={0}>
|
||||
<Text fw={500} size="sm">
|
||||
{match.away?.name} Cups
|
||||
</Text>
|
||||
{
|
||||
match.away?.players.map(p => (<Text size='xs' c='dimmed'>
|
||||
{p.first_name} {p.last_name}
|
||||
</Text>))
|
||||
}
|
||||
</Stack>
|
||||
<TextInput
|
||||
ml='auto'
|
||||
ta="center"
|
||||
w={50}
|
||||
type="number"
|
||||
min={0}
|
||||
key={form.key("away_cups")}
|
||||
{...form.getInputProps("away_cups")}
|
||||
/>
|
||||
</Group>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Group gap="xs">
|
||||
<Text fw={500} size="sm">
|
||||
OT Count
|
||||
</Text>
|
||||
<TextInput
|
||||
ml='auto'
|
||||
ta="center"
|
||||
w={50}
|
||||
type="number"
|
||||
min={0}
|
||||
key={form.key("ot_count")}
|
||||
{...form.getInputProps("ot_count")}
|
||||
/>
|
||||
</Group>
|
||||
|
||||
</Flex>
|
||||
|
||||
<Stack mt="md">
|
||||
<Button type="submit">Update Match</Button>
|
||||
<Button variant="subtle" color="red" onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
@@ -8,6 +8,7 @@ interface MatchSlotProps {
|
||||
from_loser?: boolean;
|
||||
team?: TeamInfo;
|
||||
seed?: number;
|
||||
cups?: number;
|
||||
}
|
||||
|
||||
export const MatchSlot: React.FC<MatchSlotProps> = ({
|
||||
@@ -15,10 +16,11 @@ export const MatchSlot: React.FC<MatchSlotProps> = ({
|
||||
from_loser,
|
||||
team,
|
||||
seed,
|
||||
cups
|
||||
}) => (
|
||||
<Flex align="stretch">
|
||||
{(seed && seed > 0) ? <SeedBadge seed={seed} /> : undefined}
|
||||
<Flex p="4px 8px">
|
||||
<Flex p="4px 8px" w='100%'>
|
||||
{team ? (
|
||||
<Text size="xs">{team.name}</Text>
|
||||
) : from ? (
|
||||
@@ -30,6 +32,11 @@ export const MatchSlot: React.FC<MatchSlotProps> = ({
|
||||
TBD
|
||||
</Text>
|
||||
)}
|
||||
{
|
||||
cups !== undefined ? (
|
||||
<Text ta='center' w={15} fw="800" ml='auto' size="xs">{cups}</Text>
|
||||
) : undefined
|
||||
}
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user