i think working bracket runner
This commit is contained in:
@@ -1,25 +1,31 @@
|
|||||||
import { createFileRoute, redirect, useRouter } from '@tanstack/react-router'
|
import { createFileRoute, redirect, useRouter } from "@tanstack/react-router";
|
||||||
import { tournamentQueries, useTournament } from '@/features/tournaments/queries'
|
import {
|
||||||
import { ensureServerQueryData } from '@/lib/tanstack-query/utils/ensure'
|
tournamentKeys,
|
||||||
import SeedTournament from '@/features/tournaments/components/seed-tournament'
|
tournamentQueries,
|
||||||
import { Container, Alert, Text } from '@mantine/core'
|
useTournament,
|
||||||
import { Info } from '@phosphor-icons/react'
|
} from "@/features/tournaments/queries";
|
||||||
import { useMemo } from 'react'
|
import { ensureServerQueryData } from "@/lib/tanstack-query/utils/ensure";
|
||||||
import { BracketData } from '@/features/bracket/types'
|
import SeedTournament from "@/features/tournaments/components/seed-tournament";
|
||||||
import { Match } from '@/features/matches/types'
|
import { Container } from "@mantine/core";
|
||||||
import BracketView from '@/features/bracket/components/bracket-view'
|
import { useMemo } from "react";
|
||||||
|
import { BracketData } from "@/features/bracket/types";
|
||||||
|
import { Match } from "@/features/matches/types";
|
||||||
|
import BracketView from "@/features/bracket/components/bracket-view";
|
||||||
|
import { startMatch } from "@/features/matches/server";
|
||||||
|
import { useServerMutation } from "@/lib/tanstack-query/hooks";
|
||||||
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
export const Route = createFileRoute('/_authed/admin/tournaments/run/$id')({
|
export const Route = createFileRoute("/_authed/admin/tournaments/run/$id")({
|
||||||
beforeLoad: async ({ context, params }) => {
|
beforeLoad: async ({ context, params }) => {
|
||||||
const { queryClient } = context
|
const { queryClient } = context;
|
||||||
const tournament = await ensureServerQueryData(
|
const tournament = await ensureServerQueryData(
|
||||||
queryClient,
|
queryClient,
|
||||||
tournamentQueries.details(params.id)
|
tournamentQueries.details(params.id)
|
||||||
)
|
);
|
||||||
if (!tournament) throw redirect({ to: '/admin/tournaments' })
|
if (!tournament) throw redirect({ to: "/admin/tournaments" });
|
||||||
return {
|
return {
|
||||||
tournament,
|
tournament,
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
loader: ({ context }) => ({
|
loader: ({ context }) => ({
|
||||||
fullWidth: true,
|
fullWidth: true,
|
||||||
@@ -29,61 +35,75 @@ export const Route = createFileRoute('/_authed/admin/tournaments/run/$id')({
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
component: RouteComponent,
|
component: RouteComponent,
|
||||||
})
|
});
|
||||||
|
|
||||||
function RouteComponent() {
|
function RouteComponent() {
|
||||||
const { id } = Route.useParams();
|
const { id } = Route.useParams();
|
||||||
const { data: tournament } = useTournament(id)
|
const { data: tournament } = useTournament(id);
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const start = useServerMutation({
|
||||||
|
mutationFn: startMatch,
|
||||||
|
successMessage: "Match started!",
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: tournamentKeys.details(tournament.id),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const bracket: BracketData = useMemo(() => {
|
const bracket: BracketData = useMemo(() => {
|
||||||
if (!tournament.matches || tournament.matches.length === 0) {
|
if (!tournament.matches || tournament.matches.length === 0) {
|
||||||
return { winners: [], losers: [] }
|
return { winners: [], losers: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
const winnersMap = new Map<number, Match[]>()
|
const winnersMap = new Map<number, Match[]>();
|
||||||
const losersMap = new Map<number, Match[]>()
|
const losersMap = new Map<number, Match[]>();
|
||||||
|
|
||||||
tournament.matches.sort((a, b) => a.lid - b.lid).forEach((match) => {
|
tournament.matches
|
||||||
if (!match.is_losers_bracket) {
|
.sort((a, b) => a.lid - b.lid)
|
||||||
if (!winnersMap.has(match.round)) {
|
.forEach((match) => {
|
||||||
winnersMap.set(match.round, [])
|
if (!match.is_losers_bracket) {
|
||||||
|
if (!winnersMap.has(match.round)) {
|
||||||
|
winnersMap.set(match.round, []);
|
||||||
|
}
|
||||||
|
winnersMap.get(match.round)!.push(match);
|
||||||
|
} else {
|
||||||
|
if (!losersMap.has(match.round)) {
|
||||||
|
losersMap.set(match.round, []);
|
||||||
|
}
|
||||||
|
losersMap.get(match.round)!.push(match);
|
||||||
}
|
}
|
||||||
winnersMap.get(match.round)!.push(match)
|
});
|
||||||
} else {
|
|
||||||
if (!losersMap.has(match.round)) {
|
|
||||||
losersMap.set(match.round, [])
|
|
||||||
}
|
|
||||||
losersMap.get(match.round)!.push(match)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const winners = Array.from(winnersMap.entries())
|
const winners = Array.from(winnersMap.entries())
|
||||||
.sort(([a], [b]) => a - b)
|
.sort(([a], [b]) => a - b)
|
||||||
.map(([, matches]) => matches)
|
.map(([, matches]) => matches);
|
||||||
|
|
||||||
const losers = Array.from(losersMap.entries())
|
const losers = Array.from(losersMap.entries())
|
||||||
.sort(([a], [b]) => a - b)
|
.sort(([a], [b]) => a - b)
|
||||||
.map(([, matches]) => matches)
|
.map(([, matches]) => matches);
|
||||||
return { winners, losers }
|
return { winners, losers };
|
||||||
}, [tournament.matches])
|
}, [tournament.matches]);
|
||||||
|
|
||||||
const handleStartMatch = (match: Match) => {
|
const handleStartMatch = async (match: Match) => {
|
||||||
|
await start.mutate({
|
||||||
|
data: match.id
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
}
|
console.log(tournament.matches);
|
||||||
|
|
||||||
console.log(tournament.matches)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container size="md">
|
<Container size="md">
|
||||||
{
|
{tournament.matches?.length ? (
|
||||||
tournament.matches?.length ?
|
<BracketView bracket={bracket} showControls />
|
||||||
<BracketView bracket={bracket} onStartMatch={console.log} />
|
) : (
|
||||||
: (
|
<SeedTournament
|
||||||
<SeedTournament
|
tournamentId={tournament.id}
|
||||||
tournamentId={tournament.id}
|
teams={tournament.teams || []}
|
||||||
teams={tournament.teams || []}
|
/>
|
||||||
/>)
|
)}
|
||||||
}
|
|
||||||
</Container>
|
</Container>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ import { Match } from "@/features/matches/types";
|
|||||||
|
|
||||||
interface BracketViewProps {
|
interface BracketViewProps {
|
||||||
bracket: BracketData;
|
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 height = useAppShellHeight();
|
||||||
const orders = useMemo(() => {
|
const orders = useMemo(() => {
|
||||||
const map: Record<number, number> = {};
|
const map: Record<number, number> = {};
|
||||||
@@ -32,14 +32,14 @@ const BracketView: React.FC<BracketViewProps> = ({ bracket, onStartMatch }) => {
|
|||||||
<Text fw={600} size="md" m={16}>
|
<Text fw={600} size="md" m={16}>
|
||||||
Winners Bracket
|
Winners Bracket
|
||||||
</Text>
|
</Text>
|
||||||
<Bracket rounds={bracket.winners} orders={orders} onStartMatch={onStartMatch} />
|
<Bracket rounds={bracket.winners} orders={orders} showControls={showControls} />
|
||||||
</div>
|
</div>
|
||||||
{bracket.losers && (
|
{bracket.losers && (
|
||||||
<div>
|
<div>
|
||||||
<Text fw={600} size="md" m={16}>
|
<Text fw={600} size="md" m={16}>
|
||||||
Losers Bracket
|
Losers Bracket
|
||||||
</Text>
|
</Text>
|
||||||
<Bracket rounds={bracket.losers} orders={orders} onStartMatch={onStartMatch} />
|
<Bracket rounds={bracket.losers} orders={orders} showControls={showControls} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
|
|||||||
@@ -5,13 +5,13 @@ import { MatchCard } from "./match-card";
|
|||||||
interface BracketProps {
|
interface BracketProps {
|
||||||
rounds: Match[][];
|
rounds: Match[][];
|
||||||
orders: Record<number, number>;
|
orders: Record<number, number>;
|
||||||
onStartMatch?: (match: Match) => void;
|
showControls?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Bracket: React.FC<BracketProps> = ({
|
export const Bracket: React.FC<BracketProps> = ({
|
||||||
rounds,
|
rounds,
|
||||||
orders,
|
orders,
|
||||||
onStartMatch,
|
showControls,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<Flex direction="row" gap={24} justify="left" p="xl">
|
<Flex direction="row" gap={24} justify="left" p="xl">
|
||||||
@@ -32,7 +32,7 @@ export const Bracket: React.FC<BracketProps> = ({
|
|||||||
<MatchCard
|
<MatchCard
|
||||||
match={match}
|
match={match}
|
||||||
orders={orders}
|
orders={orders}
|
||||||
onStartMatch={onStartMatch}
|
showControls={showControls}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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 { PlayIcon, PencilIcon } from "@phosphor-icons/react";
|
||||||
import React, { useCallback, useMemo } from "react";
|
import React, { useCallback, useMemo } from "react";
|
||||||
import { MatchSlot } from "./match-slot";
|
import { MatchSlot } from "./match-slot";
|
||||||
import { Match } from "@/features/matches/types";
|
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 {
|
interface MatchCardProps {
|
||||||
match: Match;
|
match: Match;
|
||||||
orders: Record<number, number>;
|
orders: Record<number, number>;
|
||||||
onStartMatch?: (match: Match) => void;
|
showControls?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MatchCard: React.FC<MatchCardProps> = ({
|
export const MatchCard: React.FC<MatchCardProps> = ({
|
||||||
match,
|
match,
|
||||||
orders,
|
orders,
|
||||||
onStartMatch,
|
showControls,
|
||||||
}) => {
|
}) => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const editSheet = useSheet();
|
||||||
const homeSlot = useMemo(
|
const homeSlot = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
from: orders[match.home_from_lid],
|
from: orders[match.home_from_lid],
|
||||||
from_loser: match.home_from_loser,
|
from_loser: match.home_from_loser,
|
||||||
team: match.home,
|
team: match.home,
|
||||||
seed: match.home_seed,
|
seed: match.home_seed,
|
||||||
|
cups: match.status === "ended" ? match.home_cups : undefined
|
||||||
}),
|
}),
|
||||||
[match]
|
[match]
|
||||||
);
|
);
|
||||||
@@ -30,72 +40,110 @@ export const MatchCard: React.FC<MatchCardProps> = ({
|
|||||||
from_loser: match.away_from_loser,
|
from_loser: match.away_from_loser,
|
||||||
team: match.away,
|
team: match.away,
|
||||||
seed: match.away_seed,
|
seed: match.away_seed,
|
||||||
|
cups: match.status === "ended" ? match.away_cups : undefined
|
||||||
}),
|
}),
|
||||||
[match]
|
[match]
|
||||||
);
|
);
|
||||||
|
|
||||||
const showToolbar = useMemo(
|
const showToolbar = useMemo(
|
||||||
() => match.status === "ready" && onStartMatch,
|
() => match.status === "ready" && showControls,
|
||||||
[match.status, onStartMatch]
|
[match.status, showControls]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleAnnounce = useCallback(
|
const showEditButton = useMemo(
|
||||||
() => onStartMatch?.(match),
|
() => showControls && match.status === "started",
|
||||||
[onStartMatch, match]
|
[showControls, match.status]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleEdit = useCallback(() => {
|
const start = useServerMutation({
|
||||||
// TODO: implement edit functionality
|
mutationFn: startMatch,
|
||||||
console.log('Edit match:', match);
|
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]);
|
}, [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 (
|
return (
|
||||||
<Flex direction="row" align="center" justify="end" gap={8}>
|
<Flex direction="row" align="center" justify="end" gap={8}>
|
||||||
<Text c="dimmed" fw="bolder">
|
<Text c="dimmed" fw="bolder">
|
||||||
{match.order}
|
{match.order}
|
||||||
</Text>
|
</Text>
|
||||||
<Flex align="stretch">
|
<Flex align="stretch">
|
||||||
<Card
|
<Indicator
|
||||||
withBorder
|
inline
|
||||||
pos="relative"
|
processing={match.status === "started"}
|
||||||
w={200}
|
color="red"
|
||||||
style={{ overflow: "visible" }}
|
size={12}
|
||||||
data-match-lid={match.lid}
|
disabled={match.status !== "started" || showEditButton}
|
||||||
>
|
>
|
||||||
<Card.Section withBorder p={0}>
|
<Card
|
||||||
<MatchSlot {...homeSlot} />
|
w={showToolbar || showEditButton ? 200 : 220}
|
||||||
</Card.Section>
|
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}>
|
<Card.Section p={0} mb={-16}>
|
||||||
<MatchSlot {...awaySlot} />
|
<MatchSlot {...awaySlot} />
|
||||||
</Card.Section>
|
</Card.Section>
|
||||||
|
|
||||||
{match.reset && (
|
{match.reset && (
|
||||||
<Text
|
<Text
|
||||||
pos="absolute"
|
pos="absolute"
|
||||||
top={-20}
|
top={-20}
|
||||||
left={8}
|
left={8}
|
||||||
size="xs"
|
size="xs"
|
||||||
c="dimmed"
|
c="dimmed"
|
||||||
fw="bold"
|
fw="bold"
|
||||||
>
|
>
|
||||||
* If necessary
|
* If necessary
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
|
</Indicator>
|
||||||
|
|
||||||
{showToolbar && (
|
{showToolbar && (
|
||||||
<Flex
|
<Flex direction="column" justify="center" align="center">
|
||||||
direction="column"
|
|
||||||
justify="center"
|
|
||||||
align="center"
|
|
||||||
>
|
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
color="green"
|
color="green"
|
||||||
onClick={handleAnnounce}
|
onClick={handleStart}
|
||||||
size="sm"
|
size="sm"
|
||||||
h='100%'
|
h="100%"
|
||||||
radius='sm'
|
radius="sm"
|
||||||
ml={-4}
|
ml={-4}
|
||||||
style={{
|
style={{
|
||||||
borderTopLeftRadius: 0,
|
borderTopLeftRadius: 0,
|
||||||
@@ -106,7 +154,34 @@ export const MatchCard: React.FC<MatchCardProps> = ({
|
|||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Flex>
|
</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>
|
</Flex>
|
||||||
|
|
||||||
|
<Sheet title="Edit Match" {...editSheet.props}>
|
||||||
|
<MatchForm
|
||||||
|
match={match}
|
||||||
|
onSubmit={handleFormSubmit}
|
||||||
|
onCancel={editSheet.close}
|
||||||
|
/>
|
||||||
|
</Sheet>
|
||||||
</Flex>
|
</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;
|
from_loser?: boolean;
|
||||||
team?: TeamInfo;
|
team?: TeamInfo;
|
||||||
seed?: number;
|
seed?: number;
|
||||||
|
cups?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MatchSlot: React.FC<MatchSlotProps> = ({
|
export const MatchSlot: React.FC<MatchSlotProps> = ({
|
||||||
@@ -15,10 +16,11 @@ export const MatchSlot: React.FC<MatchSlotProps> = ({
|
|||||||
from_loser,
|
from_loser,
|
||||||
team,
|
team,
|
||||||
seed,
|
seed,
|
||||||
|
cups
|
||||||
}) => (
|
}) => (
|
||||||
<Flex align="stretch">
|
<Flex align="stretch">
|
||||||
{(seed && seed > 0) ? <SeedBadge seed={seed} /> : undefined}
|
{(seed && seed > 0) ? <SeedBadge seed={seed} /> : undefined}
|
||||||
<Flex p="4px 8px">
|
<Flex p="4px 8px" w='100%'>
|
||||||
{team ? (
|
{team ? (
|
||||||
<Text size="xs">{team.name}</Text>
|
<Text size="xs">{team.name}</Text>
|
||||||
) : from ? (
|
) : from ? (
|
||||||
@@ -30,6 +32,11 @@ export const MatchSlot: React.FC<MatchSlotProps> = ({
|
|||||||
TBD
|
TBD
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
|
{
|
||||||
|
cups !== undefined ? (
|
||||||
|
<Text ta='center' w={15} fw="800" ml='auto' size="xs">{cups}</Text>
|
||||||
|
) : undefined
|
||||||
|
}
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -174,5 +174,39 @@ export const endMatch = createServerFn()
|
|||||||
away_cups,
|
away_cups,
|
||||||
ot_count
|
ot_count
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const matchWinner = home_cups > away_cups ? match.home : match.away;
|
||||||
|
const matchLoser = home_cups < away_cups ? match.home : match.away;
|
||||||
|
if (!matchWinner || !matchLoser) throw new Error("Something went wrong")
|
||||||
|
|
||||||
|
// winner -> where to send match winner to, loser same
|
||||||
|
const { winner, loser } = await pbAdmin.getChildMatches(matchId)
|
||||||
|
|
||||||
|
console.log('match winner:', matchWinner)
|
||||||
|
console.log('match loser:', matchLoser)
|
||||||
|
|
||||||
|
if (winner) {
|
||||||
|
await pbAdmin.updateMatch(winner.id, {
|
||||||
|
[winner.home_from_lid === match.lid ? "home" : "away"]: matchWinner,
|
||||||
|
status:
|
||||||
|
((winner.home_from_lid === match.lid) && winner.away)
|
||||||
|
|| ((winner.away_from_lid === match.lid) && winner.home)
|
||||||
|
? 'ready' : 'tbd'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loser) {
|
||||||
|
await pbAdmin.updateMatch(loser.id, {
|
||||||
|
[loser.home_from_lid === match.lid ? "home" : "away"]: matchLoser,
|
||||||
|
status:
|
||||||
|
((loser.home_from_lid === match.lid) && loser.away)
|
||||||
|
|| ((loser.away_from_lid === match.lid) && loser.home)
|
||||||
|
? 'ready' : 'tbd'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: send SSE
|
||||||
|
|
||||||
|
return match;
|
||||||
}
|
}
|
||||||
));
|
));
|
||||||
@@ -4,39 +4,6 @@ import { TournamentInfo } from "../tournaments/types";
|
|||||||
|
|
||||||
export type MatchStatus = "tbd" | "ready" | "started" | "ended";
|
export type MatchStatus = "tbd" | "ready" | "started" | "ended";
|
||||||
|
|
||||||
/**
|
|
||||||
* class TMatchSlot(BaseModel):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class Seed(TMatchSlot):
|
|
||||||
seed: int
|
|
||||||
|
|
||||||
|
|
||||||
class TBD(TMatchSlot):
|
|
||||||
parent: "TMatchBase"
|
|
||||||
loser: bool
|
|
||||||
|
|
||||||
|
|
||||||
class TMatchBase(BaseModel):
|
|
||||||
lid: int # local id
|
|
||||||
round: int
|
|
||||||
order: Optional[int] = None
|
|
||||||
|
|
||||||
|
|
||||||
class TMatch(TMatchBase):
|
|
||||||
home: Seed | TBD
|
|
||||||
away: Seed | TBD
|
|
||||||
reset: bool = False
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return f'{self.order}'
|
|
||||||
|
|
||||||
|
|
||||||
class TBye(TMatchBase):
|
|
||||||
home: Seed | TBD
|
|
||||||
*/
|
|
||||||
|
|
||||||
export interface Match {
|
export interface Match {
|
||||||
id: string;
|
id: string;
|
||||||
order: number;
|
order: number;
|
||||||
|
|||||||
@@ -13,6 +13,28 @@ export function createMatchesService(pb: PocketBase) {
|
|||||||
return transformMatch(result);
|
return transformMatch(result);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// match Ids where the current lid is home_from_lid or away_from_lid
|
||||||
|
async getChildMatches(matchId: string): Promise<{ winner: Match | undefined, loser: Match | undefined }> {
|
||||||
|
logger.info("PocketBase | Getting child matches", matchId);
|
||||||
|
const match = await this.getMatch(matchId);
|
||||||
|
if (!match) throw new Error("Match not found")
|
||||||
|
|
||||||
|
const result = await pb.collection("matches").getFullList({
|
||||||
|
filter: `tournament="${match.tournament.id}" && (home_from_lid = ${match.lid} || away_from_lid = ${match.lid})`,
|
||||||
|
expand: "tournament, home, away",
|
||||||
|
});
|
||||||
|
|
||||||
|
const winnerMatch = result.find(m => (m.home_from_lid === match.lid && !m.home_from_loser) || (m.away_from_lid === match.lid && !m.away_from_loser));
|
||||||
|
const loserMatch = result.find(m => (m.home_from_lid === match.lid && m.home_from_loser) || (m.away_from_lid === match.lid && m.away_from_loser));
|
||||||
|
|
||||||
|
console.log(winnerMatch, loserMatch)
|
||||||
|
|
||||||
|
return {
|
||||||
|
winner: winnerMatch ? transformMatch(winnerMatch) : undefined,
|
||||||
|
loser: loserMatch ? transformMatch(loserMatch) : undefined
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
async createMatch(data: MatchInput): Promise<Match> {
|
async createMatch(data: MatchInput): Promise<Match> {
|
||||||
logger.info("PocketBase | Creating match", data);
|
logger.info("PocketBase | Creating match", data);
|
||||||
const result = await pb.collection("matches").create<Match>(data);
|
const result = await pb.collection("matches").create<Match>(data);
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export function createTournamentsService(pb: PocketBase) {
|
|||||||
return {
|
return {
|
||||||
async getTournament(id: string): Promise<Tournament> {
|
async getTournament(id: string): Promise<Tournament> {
|
||||||
const result = await pb.collection("tournaments").getOne(id, {
|
const result = await pb.collection("tournaments").getOne(id, {
|
||||||
expand: "teams, teams.players, matches, matches.tournament, matches.home, matches.away",
|
expand: "teams, teams.players, matches, matches.tournament, matches.home, matches.away, matches.home.players, matches.away.players",
|
||||||
});
|
});
|
||||||
return transformTournament(result);
|
return transformTournament(result);
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user