better brackets, info types
This commit is contained in:
44
pb_migrations/1757211840_updated_matches.js
Normal file
44
pb_migrations/1757211840_updated_matches.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
/// <reference path="../pb_data/types.d.ts" />
|
||||||
|
migrate((app) => {
|
||||||
|
const collection = app.findCollectionByNameOrId("pbc_2541054544")
|
||||||
|
|
||||||
|
// add field
|
||||||
|
collection.fields.addAt(18, new Field({
|
||||||
|
"hidden": false,
|
||||||
|
"id": "number1705071305",
|
||||||
|
"max": null,
|
||||||
|
"min": null,
|
||||||
|
"name": "home_seed",
|
||||||
|
"onlyInt": false,
|
||||||
|
"presentable": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "number"
|
||||||
|
}))
|
||||||
|
|
||||||
|
// add field
|
||||||
|
collection.fields.addAt(19, new Field({
|
||||||
|
"hidden": false,
|
||||||
|
"id": "number3588777624",
|
||||||
|
"max": null,
|
||||||
|
"min": null,
|
||||||
|
"name": "away_seed",
|
||||||
|
"onlyInt": false,
|
||||||
|
"presentable": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "number"
|
||||||
|
}))
|
||||||
|
|
||||||
|
return app.save(collection)
|
||||||
|
}, (app) => {
|
||||||
|
const collection = app.findCollectionByNameOrId("pbc_2541054544")
|
||||||
|
|
||||||
|
// remove field
|
||||||
|
collection.fields.removeById("number1705071305")
|
||||||
|
|
||||||
|
// remove field
|
||||||
|
collection.fields.removeById("number3588777624")
|
||||||
|
|
||||||
|
return app.save(collection)
|
||||||
|
})
|
||||||
28
pb_migrations/1757211934_updated_tournaments.js
Normal file
28
pb_migrations/1757211934_updated_tournaments.js
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
/// <reference path="../pb_data/types.d.ts" />
|
||||||
|
migrate((app) => {
|
||||||
|
const collection = app.findCollectionByNameOrId("pbc_340646327")
|
||||||
|
|
||||||
|
// add field
|
||||||
|
collection.fields.addAt(10, new Field({
|
||||||
|
"cascadeDelete": false,
|
||||||
|
"collectionId": "pbc_2541054544",
|
||||||
|
"hidden": false,
|
||||||
|
"id": "relation103159226",
|
||||||
|
"maxSelect": 999,
|
||||||
|
"minSelect": 0,
|
||||||
|
"name": "matches",
|
||||||
|
"presentable": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "relation"
|
||||||
|
}))
|
||||||
|
|
||||||
|
return app.save(collection)
|
||||||
|
}, (app) => {
|
||||||
|
const collection = app.findCollectionByNameOrId("pbc_340646327")
|
||||||
|
|
||||||
|
// remove field
|
||||||
|
collection.fields.removeById("relation103159226")
|
||||||
|
|
||||||
|
return app.save(collection)
|
||||||
|
})
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import BracketPreview from "@/features/bracket/components/preview";
|
import BracketPreview from "@/features/admin/components/preview";
|
||||||
import { NumberInput } from "@mantine/core";
|
import { NumberInput } from "@mantine/core";
|
||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import { createFileRoute, redirect } from '@tanstack/react-router'
|
import { createFileRoute, redirect, useRouter } from '@tanstack/react-router'
|
||||||
import { tournamentQueries } from '@/features/tournaments/queries'
|
import { tournamentQueries } from '@/features/tournaments/queries'
|
||||||
import { ensureServerQueryData } from '@/lib/tanstack-query/utils/ensure'
|
import { ensureServerQueryData } from '@/lib/tanstack-query/utils/ensure'
|
||||||
|
import SeedTournament from '@/features/tournaments/components/seed-tournament'
|
||||||
|
import { Container, Alert, Text } from '@mantine/core'
|
||||||
|
import { Info } from '@phosphor-icons/react'
|
||||||
|
|
||||||
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 }) => {
|
||||||
@@ -25,6 +28,30 @@ export const Route = createFileRoute('/_authed/admin/tournaments/run/$id')({
|
|||||||
})
|
})
|
||||||
|
|
||||||
function RouteComponent() {
|
function RouteComponent() {
|
||||||
const { id } = Route.useParams()
|
const { tournament } = Route.useRouteContext()
|
||||||
return <p>Run tournament</p>
|
const router = useRouter()
|
||||||
|
|
||||||
|
const handleSuccess = () => {
|
||||||
|
router.navigate({
|
||||||
|
to: '/admin/tournaments/$id',
|
||||||
|
params: { id: tournament.id }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Tournament:', tournament)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container size="md">
|
||||||
|
{
|
||||||
|
tournament.matches?.length ?
|
||||||
|
<p>Matches</p>
|
||||||
|
: (
|
||||||
|
<SeedTournament
|
||||||
|
tournamentId={tournament.id}
|
||||||
|
teams={tournament.teams || []}
|
||||||
|
onSuccess={handleSuccess}
|
||||||
|
/>)
|
||||||
|
}
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,48 +0,0 @@
|
|||||||
import { Flex, Text } from '@mantine/core';
|
|
||||||
import React from 'react';
|
|
||||||
import { MatchCard } from './match-card';
|
|
||||||
import { Match } from '../types';
|
|
||||||
|
|
||||||
interface BracketRoundProps {
|
|
||||||
matches: Match[];
|
|
||||||
roundIndex: number;
|
|
||||||
getParentMatchOrder: (parentLid: number) => number | string;
|
|
||||||
onAnnounce?: (teamOne: any, teamTwo: any) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const BracketRound: React.FC<BracketRoundProps> = ({
|
|
||||||
matches,
|
|
||||||
roundIndex,
|
|
||||||
getParentMatchOrder,
|
|
||||||
onAnnounce,
|
|
||||||
}) => {
|
|
||||||
const isBye = (type: string) => type?.toLowerCase() === 'bye';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Flex direction="column" key={roundIndex} gap={24} justify="space-around">
|
|
||||||
{matches.map((match, matchIndex) => {
|
|
||||||
if (!match) return null;
|
|
||||||
if (isBye(match.type)) return <></>; // for spacing
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Flex
|
|
||||||
direction="row"
|
|
||||||
key={matchIndex}
|
|
||||||
align="center"
|
|
||||||
justify="end"
|
|
||||||
gap={8}
|
|
||||||
>
|
|
||||||
<Text c="dimmed" fw="bolder">
|
|
||||||
{match.order}
|
|
||||||
</Text>
|
|
||||||
<MatchCard
|
|
||||||
match={match}
|
|
||||||
getParentMatchOrder={getParentMatchOrder}
|
|
||||||
onAnnounce={onAnnounce}
|
|
||||||
/>
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import { Flex } from '@mantine/core';
|
|
||||||
import React, { useCallback } from 'react';
|
|
||||||
import { BracketMaps } from '../utils/bracket-maps';
|
|
||||||
import { BracketRound } from './bracket-round';
|
|
||||||
import { Match } from '../types';
|
|
||||||
|
|
||||||
interface BracketViewProps {
|
|
||||||
bracket: Match[][];
|
|
||||||
bracketMaps: BracketMaps;
|
|
||||||
onAnnounce?: (teamOne: any, teamTwo: any) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const BracketView: React.FC<BracketViewProps> = ({
|
|
||||||
bracket,
|
|
||||||
bracketMaps,
|
|
||||||
onAnnounce,
|
|
||||||
}) => {
|
|
||||||
|
|
||||||
const getParentMatchOrder = useCallback((parentLid: number): number | string => {
|
|
||||||
const parentMatch = bracketMaps.matchByLid.get(parentLid);
|
|
||||||
if (
|
|
||||||
parentMatch &&
|
|
||||||
parentMatch.order !== null &&
|
|
||||||
parentMatch.order !== undefined
|
|
||||||
) {
|
|
||||||
return parentMatch.order;
|
|
||||||
}
|
|
||||||
return `Match ${parentLid}`;
|
|
||||||
}, [bracketMaps]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Flex direction="row" gap={24} justify="left" pos="relative" p="xl">
|
|
||||||
{bracket.map((round, roundIndex) => (
|
|
||||||
<BracketRound
|
|
||||||
key={roundIndex}
|
|
||||||
matches={round}
|
|
||||||
roundIndex={roundIndex}
|
|
||||||
getParentMatchOrder={getParentMatchOrder}
|
|
||||||
onAnnounce={onAnnounce}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default BracketView;
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import { ScrollArea, Text } from "@mantine/core";
|
|
||||||
import BracketView from "./bracket-view";
|
|
||||||
import useAppShellHeight from "@/hooks/use-appshell-height";
|
|
||||||
import { BracketMaps } from "../utils/bracket-maps";
|
|
||||||
import { Match } from "@/features/matches/types";
|
|
||||||
|
|
||||||
interface BracketProps {
|
|
||||||
winners: Match[][];
|
|
||||||
losers?: Match[][];
|
|
||||||
bracketMaps: BracketMaps | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Bracket: React.FC<BracketProps> = ({ winners, losers, bracketMaps }) => {
|
|
||||||
const height = useAppShellHeight();
|
|
||||||
|
|
||||||
if (!bracketMaps) return <p>Bracket not available.</p>;
|
|
||||||
|
|
||||||
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>
|
|
||||||
<BracketView bracket={winners} bracketMaps={bracketMaps} />
|
|
||||||
</div>
|
|
||||||
{losers && (
|
|
||||||
<div>
|
|
||||||
<Text fw={600} size="md" m={16}>
|
|
||||||
Losers Bracket
|
|
||||||
</Text>
|
|
||||||
<BracketView bracket={losers} bracketMaps={bracketMaps} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</ScrollArea>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Bracket;
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
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';
|
|
||||||
|
|
||||||
interface MatchCardProps {
|
|
||||||
match: Match;
|
|
||||||
getParentMatchOrder: (parentLid: number) => number | string;
|
|
||||||
onAnnounce?: (teamOne: any, teamTwo: any) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const MatchCard: React.FC<MatchCardProps> = ({
|
|
||||||
match,
|
|
||||||
getParentMatchOrder,
|
|
||||||
onAnnounce
|
|
||||||
}) => {
|
|
||||||
|
|
||||||
const showAnnounce = useMemo(() =>
|
|
||||||
onAnnounce && match.home.team && match.away.team,
|
|
||||||
[onAnnounce, match.home.team, match.away.team]);
|
|
||||||
|
|
||||||
const handleAnnounce = useCallback(() =>
|
|
||||||
onAnnounce?.(match.home.team, match.away.team), [match.home.team, match.away.team]);
|
|
||||||
|
|
||||||
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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
import { Flex, Text } from "@mantine/core";
|
|
||||||
import React from "react";
|
|
||||||
import { SeedBadge } from "./seed-badge";
|
|
||||||
|
|
||||||
interface MatchSlotProps {
|
|
||||||
slot: any;
|
|
||||||
getParentMatchOrder: (parentLid: number) => number | string;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 (
|
|
||||||
<Text c="dimmed" size="xs">
|
|
||||||
{slot.loser ? "Loser" : "Winner"} of Match{" "}
|
|
||||||
{getParentMatchOrder(slot.parent_lid)}
|
|
||||||
</Text>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (slot) {
|
|
||||||
return (
|
|
||||||
<Text c="dimmed" size="xs" fs="italic">
|
|
||||||
TBD
|
|
||||||
</Text>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Flex align="stretch">
|
|
||||||
{slot?.seed && <SeedBadge seed={slot.seed} />}
|
|
||||||
<div style={{ flex: 1, padding: "4px 8px" }}>{renderSlotContent()}</div>
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
import {
|
|
||||||
Text,
|
|
||||||
Container,
|
|
||||||
Flex,
|
|
||||||
NumberInput,
|
|
||||||
Group,
|
|
||||||
Loader,
|
|
||||||
} from "@mantine/core";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { bracketQueries, useBracketPreview } from "../queries";
|
|
||||||
import { useQuery } from "@tanstack/react-query";
|
|
||||||
import { createBracketMaps, BracketMaps } from "../utils/bracket-maps";
|
|
||||||
import { BracketData, Match } from "../types";
|
|
||||||
import Bracket from "./bracket";
|
|
||||||
import "./styles.module.css";
|
|
||||||
|
|
||||||
interface PreviewTeam {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const PreviewBracket: React.FC = () => {
|
|
||||||
const [teamCount, setTeamCount] = useState(20);
|
|
||||||
const { data, isLoading, error } = useBracketPreview(teamCount);
|
|
||||||
|
|
||||||
const [teams, setTeams] = useState<PreviewTeam[]>([]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setTeams(
|
|
||||||
Array.from({ length: teamCount }, (_, i) => ({
|
|
||||||
id: `team-${i + 1}`,
|
|
||||||
name: `Team ${i + 1}`,
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
}, [teamCount]);
|
|
||||||
|
|
||||||
const [seededWinnersBracket, setSeededWinnersBracket] = useState<Match[][]>(
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
const [seededLosersBracket, setSeededLosersBracket] = useState<Match[][]>([]);
|
|
||||||
const [bracketMaps, setBracketMaps] = useState<BracketMaps | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!data || teams.length === 0) return;
|
|
||||||
|
|
||||||
const maps = createBracketMaps(data);
|
|
||||||
setBracketMaps(maps);
|
|
||||||
|
|
||||||
const mapBracket = (bracket: Match[][]) => {
|
|
||||||
return bracket.map((round) =>
|
|
||||||
round.map((match) => {
|
|
||||||
const mappedMatch = { ...match };
|
|
||||||
|
|
||||||
if (match.home?.seed && match.home.seed > 0) {
|
|
||||||
const teamIndex = match.home.seed - 1;
|
|
||||||
if (teams[teamIndex]) {
|
|
||||||
mappedMatch.home = {
|
|
||||||
...match.home,
|
|
||||||
team: teams[teamIndex],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (match.away?.seed && match.away.seed > 0) {
|
|
||||||
const teamIndex = match.away.seed - 1;
|
|
||||||
if (teams[teamIndex]) {
|
|
||||||
mappedMatch.away = {
|
|
||||||
...match.away,
|
|
||||||
team: teams[teamIndex],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return mappedMatch;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const bracketData = data as BracketData;
|
|
||||||
setSeededWinnersBracket(mapBracket(bracketData.winners));
|
|
||||||
setSeededLosersBracket(mapBracket(bracketData.losers));
|
|
||||||
}, [teams, data]);
|
|
||||||
|
|
||||||
if (error) return <p>Error loading bracket</p>;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Container p={0} w="100%" style={{ userSelect: "none" }}>
|
|
||||||
<Flex w="100%" justify="space-between" align="center" h="3rem">
|
|
||||||
<Group gap="sm" mx="auto">
|
|
||||||
<Text size="sm" c="dimmed">
|
|
||||||
Teams:
|
|
||||||
</Text>
|
|
||||||
<NumberInput
|
|
||||||
value={teamCount}
|
|
||||||
onChange={(value) => setTeamCount(Number(value) || 12)}
|
|
||||||
min={12}
|
|
||||||
max={20}
|
|
||||||
size="sm"
|
|
||||||
w={80}
|
|
||||||
allowDecimal={false}
|
|
||||||
clampBehavior="strict"
|
|
||||||
/>
|
|
||||||
</Group>
|
|
||||||
</Flex>
|
|
||||||
<Flex w="100%" gap={24}>
|
|
||||||
{isLoading ? (
|
|
||||||
<Flex justify="center" align="center" h="20vh" w="100%">
|
|
||||||
<Loader size="xl" />
|
|
||||||
</Flex>
|
|
||||||
) : (
|
|
||||||
<Bracket
|
|
||||||
winners={seededWinnersBracket}
|
|
||||||
losers={seededLosersBracket}
|
|
||||||
bracketMaps={bracketMaps}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Flex>
|
|
||||||
</Container>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import { Text } from "@mantine/core";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
interface SeedBadgeProps {
|
|
||||||
seed: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SeedBadge: React.FC<SeedBadgeProps> = ({ seed }) => {
|
|
||||||
return (
|
|
||||||
<Text
|
|
||||||
size="xs"
|
|
||||||
fw="bold"
|
|
||||||
py="4"
|
|
||||||
bg="var(--mantine-color-default-hover)"
|
|
||||||
style={{
|
|
||||||
width: "32px",
|
|
||||||
textAlign: "center",
|
|
||||||
color: "var(--mantine-color-text)",
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
borderTopLeftRadius: "var(--mantine-radius-default)",
|
|
||||||
borderBottomLeftRadius: "var(--mantine-radius-default)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{seed}
|
|
||||||
</Text>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
import { Flex, Text, Select, Card } from "@mantine/core";
|
|
||||||
|
|
||||||
interface Team {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SeedListProps {
|
|
||||||
teams: Team[];
|
|
||||||
onSeedChange: (currentIndex: number, newIndex: number) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SeedList({ teams, onSeedChange }: SeedListProps) {
|
|
||||||
const seedOptions = teams.map((_, index) => ({
|
|
||||||
value: index.toString(),
|
|
||||||
label: `Seed ${index + 1}`,
|
|
||||||
}));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Flex direction="column" gap={8}>
|
|
||||||
{teams.map((team, index) => (
|
|
||||||
<Card key={team.id} withBorder p="xs">
|
|
||||||
<Flex align="center" gap="xs" justify="space-between">
|
|
||||||
<Flex align="center" gap="xs">
|
|
||||||
<Select
|
|
||||||
value={index.toString()}
|
|
||||||
data={seedOptions}
|
|
||||||
onChange={(value) => {
|
|
||||||
if (value !== null) {
|
|
||||||
const newIndex = parseInt(value);
|
|
||||||
if (newIndex !== index) {
|
|
||||||
onSeedChange(index, newIndex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
size="xs"
|
|
||||||
w={100}
|
|
||||||
/>
|
|
||||||
<Text size="sm" fw={500}>
|
|
||||||
{team.name}
|
|
||||||
</Text>
|
|
||||||
</Flex>
|
|
||||||
</Flex>
|
|
||||||
</Card>
|
|
||||||
))}
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
.bracket-container::-webkit-scrollbar {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
.bracket-container {
|
|
||||||
box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.05);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import { queryOptions } from "@tanstack/react-query";
|
|
||||||
import { previewBracket } from "./server";
|
|
||||||
import { useServerSuspenseQuery } from "@/lib/tanstack-query/hooks";
|
|
||||||
import { BracketData } from "./types";
|
|
||||||
|
|
||||||
const bracketKeys = {
|
|
||||||
preview: (teams: number) => ["bracket", "preview", teams] as const,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const bracketQueries = {
|
|
||||||
preview: (teams: number) => ({
|
|
||||||
queryKey: bracketKeys.preview(teams),
|
|
||||||
queryFn: () => previewBracket({ data: teams }),
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useBracketPreview = (teams: number) =>
|
|
||||||
useServerSuspenseQuery<BracketData>(bracketQueries.preview(teams));
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import { superTokensFunctionMiddleware } from "@/utils/supertokens";
|
|
||||||
import { createServerFn } from "@tanstack/react-start";
|
|
||||||
import { z } from "zod";
|
|
||||||
import { Logger } from "@/lib/logger";
|
|
||||||
import brackets from "./utils";
|
|
||||||
import { BracketData } from "./types";
|
|
||||||
import { toServerResult } from "@/lib/tanstack-query/utils/to-server-result";
|
|
||||||
|
|
||||||
const logger = new Logger("Bracket Generation");
|
|
||||||
|
|
||||||
export const previewBracket = createServerFn()
|
|
||||||
.validator(z.number())
|
|
||||||
.middleware([superTokensFunctionMiddleware])
|
|
||||||
.handler(async ({ data: teams }) =>
|
|
||||||
toServerResult(async () => {
|
|
||||||
logger.info("Generating bracket", teams);
|
|
||||||
if (!Object.keys(brackets).includes(teams.toString()))
|
|
||||||
throw Error("Bracket not available");
|
|
||||||
return brackets[teams as keyof typeof brackets] as BracketData;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
/*export interface Slot {
|
|
||||||
seed?: number;
|
|
||||||
team?: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Match {
|
|
||||||
lid: number;
|
|
||||||
round: number;
|
|
||||||
order: number | null;
|
|
||||||
type: string;
|
|
||||||
home: Slot;
|
|
||||||
away: Slot;
|
|
||||||
reset?: boolean;
|
|
||||||
}*/
|
|
||||||
import { Match } from "../matches/types";
|
|
||||||
|
|
||||||
export interface BracketData {
|
|
||||||
winners: Match[][];
|
|
||||||
losers: Match[][];
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
import { BracketData, Match } from "../types";
|
|
||||||
|
|
||||||
export interface BracketMaps {
|
|
||||||
matchByLid: Map<number, Match>;
|
|
||||||
matchByOrder: Map<number, Match>;
|
|
||||||
allMatches: Match[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createBracketMaps(bracketData: BracketData): BracketMaps {
|
|
||||||
const matchByLid = new Map<number, Match>();
|
|
||||||
const matchByOrder = new Map<number, Match>();
|
|
||||||
const allMatches: Match[] = [];
|
|
||||||
|
|
||||||
[...bracketData.winners, ...bracketData.losers].forEach((round) => {
|
|
||||||
round.forEach((match) => {
|
|
||||||
matchByLid.set(match.lid, match);
|
|
||||||
|
|
||||||
if (match.order !== null && match.order !== undefined) {
|
|
||||||
matchByOrder.set(match.order, match);
|
|
||||||
}
|
|
||||||
|
|
||||||
allMatches.push(match);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
matchByLid,
|
|
||||||
matchByOrder,
|
|
||||||
allMatches,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getMatchByLid(
|
|
||||||
maps: BracketMaps,
|
|
||||||
lid: number
|
|
||||||
): Match | undefined {
|
|
||||||
return maps.matchByLid.get(lid);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getMatchByOrder(
|
|
||||||
maps: BracketMaps,
|
|
||||||
order: number
|
|
||||||
): Match | undefined {
|
|
||||||
return maps.matchByOrder.get(order);
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
/**
|
|
||||||
* Imports saved json dumps of bracket generation from a python script that I didn't prioritize converting to TS
|
|
||||||
*/
|
|
||||||
import b12 from "../../../../scripts/brackets/12.json";
|
|
||||||
import b13 from "../../../../scripts/brackets/13.json";
|
|
||||||
import b14 from "../../../../scripts/brackets/14.json";
|
|
||||||
import b15 from "../../../../scripts/brackets/15.json";
|
|
||||||
import b16 from "../../../../scripts/brackets/16.json";
|
|
||||||
import b17 from "../../../../scripts/brackets/17.json";
|
|
||||||
import b18 from "../../../../scripts/brackets/18.json";
|
|
||||||
import b19 from "../../../../scripts/brackets/19.json";
|
|
||||||
import b20 from "../../../../scripts/brackets/20.json";
|
|
||||||
|
|
||||||
export default {
|
|
||||||
12: b12,
|
|
||||||
13: b13,
|
|
||||||
14: b14,
|
|
||||||
15: b15,
|
|
||||||
16: b16,
|
|
||||||
17: b17,
|
|
||||||
18: b18,
|
|
||||||
19: b19,
|
|
||||||
20: b20,
|
|
||||||
};
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useBracketPreview } from "../queries";
|
import { useBracketPreview } from "../../bracket/queries";
|
||||||
import BracketView from "./bracket-view";
|
import BracketView from "../../bracket/components/bracket-view";
|
||||||
|
|
||||||
interface BracketPreviewProps {
|
interface BracketPreviewProps {
|
||||||
n: number;
|
n: number;
|
||||||
136
src/features/matches/server.ts
Normal file
136
src/features/matches/server.ts
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
import { superTokensAdminFunctionMiddleware } from "@/utils/supertokens";
|
||||||
|
import { createServerFn } from "@tanstack/react-start";
|
||||||
|
import { pbAdmin } from "@/lib/pocketbase/client";
|
||||||
|
import { logger } from "@/lib/logger";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { toServerResult } from "@/lib/tanstack-query/utils/to-server-result";
|
||||||
|
import brackets from "@/features/bracket/utils";
|
||||||
|
import { MatchInput } from "@/features/matches/types";
|
||||||
|
|
||||||
|
const orderedTeamsSchema = z.object({
|
||||||
|
tournamentId: z.string(),
|
||||||
|
orderedTeamIds: z.array(z.string()),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const generateTournamentBracket = createServerFn()
|
||||||
|
.validator(orderedTeamsSchema)
|
||||||
|
.middleware([superTokensAdminFunctionMiddleware])
|
||||||
|
.handler(async ({ data: { tournamentId, orderedTeamIds } }) =>
|
||||||
|
toServerResult(async () => {
|
||||||
|
logger.info('Generating tournament bracket', { tournamentId, teamCount: orderedTeamIds.length });
|
||||||
|
|
||||||
|
// Get tournament with teams
|
||||||
|
const tournament = await pbAdmin.getTournament(tournamentId);
|
||||||
|
if (!tournament) {
|
||||||
|
throw new Error('Tournament not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if tournament already has matches
|
||||||
|
if (tournament.matches && tournament.matches.length > 0) {
|
||||||
|
throw new Error('Tournament already has matches generated');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get bracket template based on team count
|
||||||
|
const teamCount = orderedTeamIds.length;
|
||||||
|
if (!Object.keys(brackets).includes(teamCount.toString())) {
|
||||||
|
throw new Error(`Bracket not available for ${teamCount} teams`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const bracketTemplate = brackets[teamCount as keyof typeof brackets] as any;
|
||||||
|
|
||||||
|
// Create seed to team mapping (index + 1 = seed)
|
||||||
|
const seedToTeamId = new Map<number, string>();
|
||||||
|
orderedTeamIds.forEach((teamId, index) => {
|
||||||
|
seedToTeamId.set(index + 1, teamId);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Convert bracket template to match records
|
||||||
|
const matchInputs: MatchInput[] = [];
|
||||||
|
|
||||||
|
// Process winners bracket
|
||||||
|
bracketTemplate.winners.forEach((round: any[]) => {
|
||||||
|
round.forEach((match: any) => {
|
||||||
|
const matchInput: MatchInput = {
|
||||||
|
lid: match.lid,
|
||||||
|
round: match.round,
|
||||||
|
order: match.order || 0,
|
||||||
|
reset: match.reset || false,
|
||||||
|
bye: match.bye || false,
|
||||||
|
home_cups: 0,
|
||||||
|
away_cups: 0,
|
||||||
|
ot_count: 0,
|
||||||
|
home_from_lid: match.home_from_lid,
|
||||||
|
away_from_lid: match.away_from_lid,
|
||||||
|
home_from_loser: match.home_from_loser || false,
|
||||||
|
away_from_loser: match.away_from_loser || false,
|
||||||
|
is_losers_bracket: false,
|
||||||
|
tournament: tournamentId,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Assign teams based on seeds
|
||||||
|
if (match.home_seed) {
|
||||||
|
const teamId = seedToTeamId.get(match.home_seed);
|
||||||
|
if (teamId) {
|
||||||
|
matchInput.home = teamId;
|
||||||
|
matchInput.home_seed = match.home_seed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (match.away_seed) {
|
||||||
|
const teamId = seedToTeamId.get(match.away_seed);
|
||||||
|
if (teamId) {
|
||||||
|
matchInput.away = teamId;
|
||||||
|
matchInput.away_seed = match.away_seed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
matchInputs.push(matchInput);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Process losers bracket
|
||||||
|
bracketTemplate.losers.forEach((round: any[]) => {
|
||||||
|
round.forEach((match: any) => {
|
||||||
|
const matchInput: MatchInput = {
|
||||||
|
lid: match.lid,
|
||||||
|
round: match.round,
|
||||||
|
order: match.order || 0,
|
||||||
|
reset: match.reset || false,
|
||||||
|
bye: match.bye || false,
|
||||||
|
home_cups: 0,
|
||||||
|
away_cups: 0,
|
||||||
|
ot_count: 0,
|
||||||
|
home_from_lid: match.home_from_lid,
|
||||||
|
away_from_lid: match.away_from_lid,
|
||||||
|
home_from_loser: match.home_from_loser || false,
|
||||||
|
away_from_loser: match.away_from_loser || false,
|
||||||
|
is_losers_bracket: true,
|
||||||
|
tournament: tournamentId,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Losers bracket matches don't start with teams
|
||||||
|
// Teams come from winners bracket losses
|
||||||
|
|
||||||
|
matchInputs.push(matchInput);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create all matches
|
||||||
|
const createdMatches = await pbAdmin.createMatches(matchInputs);
|
||||||
|
|
||||||
|
// Update tournament to include all match IDs in the matches relation
|
||||||
|
const matchIds = createdMatches.map(match => match.id);
|
||||||
|
await pbAdmin.updateTournamentMatches(tournamentId, matchIds);
|
||||||
|
|
||||||
|
logger.info('Tournament bracket generated', {
|
||||||
|
tournamentId,
|
||||||
|
matchCount: createdMatches.length
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
tournament,
|
||||||
|
matchCount: createdMatches.length,
|
||||||
|
matches: createdMatches,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { TeamInfo } from "../teams/types";
|
||||||
|
import { TournamentInfo } from "../tournaments/types";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* class TMatchSlot(BaseModel):
|
* class TMatchSlot(BaseModel):
|
||||||
@@ -50,9 +52,9 @@ export interface Match {
|
|||||||
home_from_loser: boolean;
|
home_from_loser: boolean;
|
||||||
away_from_loser: boolean;
|
away_from_loser: boolean;
|
||||||
is_losers_bracket: boolean;
|
is_losers_bracket: boolean;
|
||||||
tournament: string;
|
tournament: TournamentInfo;
|
||||||
home: string;
|
home?: TeamInfo;
|
||||||
away: string;
|
away?: TeamInfo;
|
||||||
created: string;
|
created: string;
|
||||||
updated: string;
|
updated: string;
|
||||||
home_seed?: number;
|
home_seed?: number;
|
||||||
@@ -78,7 +80,8 @@ export const matchInputSchema = z.object({
|
|||||||
tournament: z.string().min(1),
|
tournament: z.string().min(1),
|
||||||
home: z.string().min(1).optional(),
|
home: z.string().min(1).optional(),
|
||||||
away: z.string().min(1).optional(),
|
away: z.string().min(1).optional(),
|
||||||
seed: z.number().int().min(1).optional(),
|
home_seed: z.number().int().min(1).optional(),
|
||||||
|
away_seed: z.number().int().min(1).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type MatchInput = z.infer<typeof matchInputSchema>;
|
export type MatchInput = z.infer<typeof matchInputSchema>;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import Header from "./header";
|
|||||||
import { Player } from "@/features/players/types";
|
import { Player } from "@/features/players/types";
|
||||||
import SwipeableTabs from "@/components/swipeable-tabs";
|
import SwipeableTabs from "@/components/swipeable-tabs";
|
||||||
import { usePlayer } from "../../queries";
|
import { usePlayer } from "../../queries";
|
||||||
|
import TeamList from "@/features/teams/components/team-list";
|
||||||
|
|
||||||
interface ProfileProps {
|
interface ProfileProps {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -13,24 +14,26 @@ const Profile = ({ id }: ProfileProps) => {
|
|||||||
const tabs = [
|
const tabs = [
|
||||||
{
|
{
|
||||||
label: "Overview",
|
label: "Overview",
|
||||||
content: <Text p="md">Stats/Badges will go here</Text>
|
content: <Text p="md">Stats/Badges will go here</Text>,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Matches",
|
label: "Matches",
|
||||||
content: <Text p="md">Matches feed will go here</Text>
|
content: <Text p="md">Matches feed will go here</Text>,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Teams",
|
label: "Teams",
|
||||||
content: <Text p="md">Teams will go here</Text>
|
content: <TeamList teams={player.teams || []} />,
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return <>
|
return (
|
||||||
<Header player={player} />
|
<>
|
||||||
<Box m='sm' mt='lg'>
|
<Header player={player} />
|
||||||
<SwipeableTabs tabs={tabs} />
|
<Box m="sm" mt="lg">
|
||||||
</Box>
|
<SwipeableTabs tabs={tabs} />
|
||||||
</>;
|
</Box>
|
||||||
|
</>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Profile;
|
export default Profile;
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
import { Team } from "@/features/teams/types";
|
import { TeamInfo } from "@/features/teams/types";
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
export interface PlayerInfo {
|
||||||
|
id: string;
|
||||||
|
first_name?: string;
|
||||||
|
last_name?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Player {
|
export interface Player {
|
||||||
id: string;
|
id: string;
|
||||||
auth_id?: string;
|
auth_id?: string;
|
||||||
@@ -8,7 +14,7 @@ export interface Player {
|
|||||||
last_name?: string;
|
last_name?: string;
|
||||||
created?: string;
|
created?: string;
|
||||||
updated?: string;
|
updated?: string;
|
||||||
teams?: Team[];
|
teams?: TeamInfo[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const playerInputSchema = z.object({
|
export const playerInputSchema = z.object({
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { List, ListItem, Skeleton, Stack, Text } from "@mantine/core";
|
import { List, ListItem, Skeleton, Stack, Text } from "@mantine/core";
|
||||||
import Avatar from "@/components/avatar";
|
import Avatar from "@/components/avatar";
|
||||||
import { Team } from "@/features/teams/types";
|
import { TeamInfo } from "@/features/teams/types";
|
||||||
import { useNavigate } from "@tanstack/react-router";
|
import { useNavigate } from "@tanstack/react-router";
|
||||||
import { useCallback, useMemo } from "react";
|
import { useCallback, useMemo } from "react";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
interface TeamListItemProps {
|
interface TeamListItemProps {
|
||||||
team: Team;
|
team: TeamInfo;
|
||||||
}
|
}
|
||||||
const TeamListItem = React.memo(({ team }: TeamListItemProps) => {
|
const TeamListItem = React.memo(({ team }: TeamListItemProps) => {
|
||||||
const playerNames = useMemo(
|
const playerNames = useMemo(
|
||||||
@@ -29,7 +29,7 @@ const TeamListItem = React.memo(({ team }: TeamListItemProps) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
interface TeamListProps {
|
interface TeamListProps {
|
||||||
teams: Team[];
|
teams: TeamInfo[];
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Player } from "@/features/players/types";
|
import { PlayerInfo } from "@/features/players/types";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { Tournament } from "../tournaments/types";
|
import { TournamentInfo } from "../tournaments/types";
|
||||||
|
|
||||||
export interface Team {
|
export interface Team {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -18,8 +18,8 @@ export interface Team {
|
|||||||
song_image_url: string;
|
song_image_url: string;
|
||||||
created: string;
|
created: string;
|
||||||
updated: string;
|
updated: string;
|
||||||
players: Player[];
|
players: PlayerInfo[];
|
||||||
tournaments: Tournament[];
|
tournaments: TournamentInfo[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TeamInfo {
|
export interface TeamInfo {
|
||||||
@@ -27,6 +27,7 @@ export interface TeamInfo {
|
|||||||
name: string;
|
name: string;
|
||||||
primary_color: string;
|
primary_color: string;
|
||||||
accent_color: string;
|
accent_color: string;
|
||||||
|
players: PlayerInfo[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const teamInputSchema = z
|
export const teamInputSchema = z
|
||||||
|
|||||||
@@ -12,14 +12,14 @@ import { useTournament, useUnenrolledTeams } from "../queries";
|
|||||||
import useEnrollTeam from "../hooks/use-enroll-team";
|
import useEnrollTeam from "../hooks/use-enroll-team";
|
||||||
import useUnenrollTeam from "../hooks/use-unenroll-team";
|
import useUnenrollTeam from "../hooks/use-unenroll-team";
|
||||||
import Avatar from "@/components/avatar";
|
import Avatar from "@/components/avatar";
|
||||||
import { Team } from "@/features/teams/types";
|
import { Team, TeamInfo } from "@/features/teams/types";
|
||||||
|
|
||||||
interface EditEnrolledTeamsProps {
|
interface EditEnrolledTeamsProps {
|
||||||
tournamentId: string;
|
tournamentId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TeamItemProps {
|
interface TeamItemProps {
|
||||||
team: Team;
|
team: TeamInfo;
|
||||||
onUnenroll: (teamId: string) => void;
|
onUnenroll: (teamId: string) => void;
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
}
|
}
|
||||||
@@ -142,7 +142,7 @@ const EditEnrolledTeams = ({ tournamentId }: EditEnrolledTeamsProps) => {
|
|||||||
</Text>
|
</Text>
|
||||||
) : (
|
) : (
|
||||||
<Stack gap="xs" w="100%">
|
<Stack gap="xs" w="100%">
|
||||||
{enrolledTeams.map((team: Team) => (
|
{enrolledTeams.map((team: TeamInfo) => (
|
||||||
<TeamItem
|
<TeamItem
|
||||||
key={team.id}
|
key={team.id}
|
||||||
team={team}
|
team={team}
|
||||||
|
|||||||
205
src/features/tournaments/components/seed-tournament.tsx
Normal file
205
src/features/tournaments/components/seed-tournament.tsx
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
import React, { useState, useEffect, useCallback, useMemo } from "react";
|
||||||
|
import {
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
Group,
|
||||||
|
ActionIcon,
|
||||||
|
Button,
|
||||||
|
NumberInput,
|
||||||
|
LoadingOverlay,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { DragDropContext, Droppable, Draggable } from "@hello-pangea/dnd";
|
||||||
|
import { DotsNineIcon } from "@phosphor-icons/react";
|
||||||
|
import { useServerMutation } from "@/lib/tanstack-query/hooks/use-server-mutation";
|
||||||
|
import { generateTournamentBracket } from "../../matches/server";
|
||||||
|
import { TeamInfo } from "@/features/teams/types";
|
||||||
|
import Avatar from "@/components/avatar";
|
||||||
|
import { useBracketPreview } from "@/features/bracket/queries";
|
||||||
|
import { BracketData } from "@/features/bracket/types";
|
||||||
|
import BracketView from "@/features/bracket/components/bracket-view";
|
||||||
|
|
||||||
|
interface SeedTournamentProps {
|
||||||
|
tournamentId: string;
|
||||||
|
teams: TeamInfo[];
|
||||||
|
onSuccess?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SeedTournament: React.FC<SeedTournamentProps> = ({
|
||||||
|
tournamentId,
|
||||||
|
teams,
|
||||||
|
onSuccess,
|
||||||
|
}) => {
|
||||||
|
const [orderedTeams, setOrderedTeams] = useState<TeamInfo[]>(teams);
|
||||||
|
const { data: bracketPreview } = useBracketPreview(teams.length);
|
||||||
|
|
||||||
|
const bracket: BracketData = useMemo(
|
||||||
|
() => ({
|
||||||
|
winners: bracketPreview.winners.map((round) =>
|
||||||
|
round.map((match) => ({
|
||||||
|
...match,
|
||||||
|
away:
|
||||||
|
match.away_seed !== undefined
|
||||||
|
? orderedTeams[match.away_seed - 1]
|
||||||
|
: undefined,
|
||||||
|
home:
|
||||||
|
match.home_seed !== undefined
|
||||||
|
? orderedTeams[match.home_seed - 1]
|
||||||
|
: undefined,
|
||||||
|
}))
|
||||||
|
),
|
||||||
|
losers: bracketPreview.losers
|
||||||
|
}),
|
||||||
|
[bracketPreview, orderedTeams]
|
||||||
|
);
|
||||||
|
|
||||||
|
const generateBracket = useServerMutation({
|
||||||
|
mutationFn: generateTournamentBracket,
|
||||||
|
successMessage: "Tournament bracket generated successfully!",
|
||||||
|
onSuccess: () => {
|
||||||
|
onSuccess?.();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleDragEnd = (result: any) => {
|
||||||
|
if (!result.destination) return;
|
||||||
|
|
||||||
|
const items = Array.from(orderedTeams);
|
||||||
|
const [reorderedItem] = items.splice(result.source.index, 1);
|
||||||
|
items.splice(result.destination.index, 0, reorderedItem);
|
||||||
|
|
||||||
|
setOrderedTeams(items);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSeedChange = (teamId: string, newSeed: number) => {
|
||||||
|
if (newSeed < 1 || newSeed > orderedTeams.length) return;
|
||||||
|
|
||||||
|
const currentIndex = orderedTeams.findIndex((t) => t.id === teamId);
|
||||||
|
if (currentIndex === -1) return;
|
||||||
|
|
||||||
|
const targetIndex = newSeed - 1;
|
||||||
|
const items = Array.from(orderedTeams);
|
||||||
|
const [movedTeam] = items.splice(currentIndex, 1);
|
||||||
|
items.splice(targetIndex, 0, movedTeam);
|
||||||
|
|
||||||
|
setOrderedTeams(items);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleGenerateBracket = () => {
|
||||||
|
const orderedTeamIds = orderedTeams.map((team) => team.id);
|
||||||
|
|
||||||
|
generateBracket.mutate({
|
||||||
|
data: {
|
||||||
|
tournamentId,
|
||||||
|
orderedTeamIds,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ display: 'flex', gap: '2rem', alignItems: 'flex-start' }}>
|
||||||
|
<Stack gap="lg" style={{ flexShrink: 0 }}>
|
||||||
|
<Stack gap={0} pos="relative" w={400}>
|
||||||
|
<LoadingOverlay visible={generateBracket.isPending} />
|
||||||
|
|
||||||
|
<Group gap="xs" p="md" pb="sm" align="center">
|
||||||
|
<Text fw={600} size="lg">
|
||||||
|
Team Seeding
|
||||||
|
</Text>
|
||||||
|
<Text size="sm" c="dimmed" ml="auto">
|
||||||
|
{orderedTeams.length} teams
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
w={400}
|
||||||
|
onClick={handleGenerateBracket}
|
||||||
|
loading={generateBracket.isPending}
|
||||||
|
disabled={orderedTeams.length === 0}
|
||||||
|
>
|
||||||
|
Confirm Seeding
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<DragDropContext onDragEnd={handleDragEnd}>
|
||||||
|
<Droppable droppableId="teams">
|
||||||
|
{(provided) => (
|
||||||
|
<div {...provided.droppableProps} ref={provided.innerRef}>
|
||||||
|
{orderedTeams.map((team, index) => (
|
||||||
|
<Draggable
|
||||||
|
key={team.id}
|
||||||
|
draggableId={team.id}
|
||||||
|
index={index}
|
||||||
|
>
|
||||||
|
{(provided) => (
|
||||||
|
<div
|
||||||
|
ref={provided.innerRef}
|
||||||
|
{...provided.draggableProps}
|
||||||
|
style={{
|
||||||
|
...provided.draggableProps.style,
|
||||||
|
borderBottom:
|
||||||
|
"1px solid var(--mantine-color-dimmed)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Group align="center" gap="sm" p="sm" px="md">
|
||||||
|
<ActionIcon
|
||||||
|
{...provided.dragHandleProps}
|
||||||
|
variant="subtle"
|
||||||
|
color="gray"
|
||||||
|
size="xs"
|
||||||
|
>
|
||||||
|
<DotsNineIcon size={14} />
|
||||||
|
</ActionIcon>
|
||||||
|
|
||||||
|
<NumberInput
|
||||||
|
value={index + 1}
|
||||||
|
onChange={(value) =>
|
||||||
|
handleSeedChange(team.id, Number(value) || 1)
|
||||||
|
}
|
||||||
|
min={1}
|
||||||
|
max={orderedTeams.length}
|
||||||
|
size="xs"
|
||||||
|
w={50}
|
||||||
|
styles={{
|
||||||
|
input: {
|
||||||
|
textAlign: "center",
|
||||||
|
fontWeight: 600,
|
||||||
|
height: 28,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Avatar size={24} radius="sm" name={team.name} />
|
||||||
|
|
||||||
|
<Text fw={500} size="sm" style={{ flex: 1 }}>
|
||||||
|
{team.name}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Draggable>
|
||||||
|
))}
|
||||||
|
{provided.placeholder}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Droppable>
|
||||||
|
</DragDropContext>
|
||||||
|
</Stack>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
w={400}
|
||||||
|
onClick={handleGenerateBracket}
|
||||||
|
loading={generateBracket.isPending}
|
||||||
|
disabled={orderedTeams.length === 0}
|
||||||
|
>
|
||||||
|
Confirm Seeding
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<div style={{ flex: 1, overflow: 'auto' }}>
|
||||||
|
<BracketView bracket={bracket} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SeedTournament;
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
import { List, ListItem, Skeleton, Text } from "@mantine/core";
|
import { List, ListItem, Skeleton, Text } from "@mantine/core";
|
||||||
import { useNavigate } from "@tanstack/react-router";
|
import { useNavigate } from "@tanstack/react-router";
|
||||||
import Avatar from "@/components/avatar";
|
import Avatar from "@/components/avatar";
|
||||||
import { Tournament } from "../types";
|
import { TournamentInfo } from "../types";
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
|
|
||||||
interface TournamentListProps {
|
interface TournamentListProps {
|
||||||
tournaments: Tournament[];
|
tournaments: TournamentInfo[];
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,16 @@
|
|||||||
import { Team } from "@/features/teams/types";
|
import { TeamInfo } from "@/features/teams/types";
|
||||||
|
import { Match } from "@/features/matches/types";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export interface TournamentInfo {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
location?: string;
|
||||||
|
start_time?: string;
|
||||||
|
end_time?: string;
|
||||||
|
logo?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Tournament {
|
export interface Tournament {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -13,7 +23,8 @@ export interface Tournament {
|
|||||||
end_time?: string;
|
end_time?: string;
|
||||||
created: string;
|
created: string;
|
||||||
updated: string;
|
updated: string;
|
||||||
teams?: Team[];
|
teams?: TeamInfo[];
|
||||||
|
matches?: Match[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const tournamentInputSchema = z.object({
|
export const tournamentInputSchema = z.object({
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import PocketBase from "pocketbase";
|
|||||||
import { createPlayersService } from "./services/players";
|
import { createPlayersService } from "./services/players";
|
||||||
import { createTournamentsService } from "./services/tournaments";
|
import { createTournamentsService } from "./services/tournaments";
|
||||||
import { createTeamsService } from "./services/teams";
|
import { createTeamsService } from "./services/teams";
|
||||||
|
import { createMatchesService } from "./services/matches";
|
||||||
|
|
||||||
class PocketBaseAdminClient {
|
class PocketBaseAdminClient {
|
||||||
private pb: PocketBase;
|
private pb: PocketBase;
|
||||||
@@ -29,6 +30,7 @@ class PocketBaseAdminClient {
|
|||||||
Object.assign(this, createPlayersService(this.pb));
|
Object.assign(this, createPlayersService(this.pb));
|
||||||
Object.assign(this, createTeamsService(this.pb));
|
Object.assign(this, createTeamsService(this.pb));
|
||||||
Object.assign(this, createTournamentsService(this.pb));
|
Object.assign(this, createTournamentsService(this.pb));
|
||||||
|
Object.assign(this, createMatchesService(this.pb));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,7 +48,8 @@ interface AdminClient
|
|||||||
extends PocketBaseAdminClient,
|
extends PocketBaseAdminClient,
|
||||||
ReturnType<typeof createPlayersService>,
|
ReturnType<typeof createPlayersService>,
|
||||||
ReturnType<typeof createTeamsService>,
|
ReturnType<typeof createTeamsService>,
|
||||||
ReturnType<typeof createTournamentsService> {
|
ReturnType<typeof createTournamentsService>,
|
||||||
|
ReturnType<typeof createMatchesService> {
|
||||||
authPromise: Promise<void>;
|
authPromise: Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
4
src/lib/pocketbase/services/base.ts
Normal file
4
src/lib/pocketbase/services/base.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export interface DataFetchOptions {
|
||||||
|
includeRelations?: boolean;
|
||||||
|
expand?: string;
|
||||||
|
}
|
||||||
39
src/lib/pocketbase/services/matches.ts
Normal file
39
src/lib/pocketbase/services/matches.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { logger } from "@/lib/logger";
|
||||||
|
import type { Match, MatchInput } from "@/features/matches/types";
|
||||||
|
import type PocketBase from "pocketbase";
|
||||||
|
|
||||||
|
export function createMatchesService(pb: PocketBase) {
|
||||||
|
return {
|
||||||
|
async createMatch(data: MatchInput): Promise<Match> {
|
||||||
|
logger.info("PocketBase | Creating match", data);
|
||||||
|
const result = await pb.collection("matches").create<Match>(data);
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
|
||||||
|
async createMatches(matches: MatchInput[]): Promise<Match[]> {
|
||||||
|
logger.info("PocketBase | Creating multiple matches", { count: matches.length });
|
||||||
|
const results = await Promise.all(
|
||||||
|
matches.map(match => pb.collection("matches").create<Match>(match))
|
||||||
|
);
|
||||||
|
return results;
|
||||||
|
},
|
||||||
|
|
||||||
|
async updateMatch(id: string, data: Partial<MatchInput>): Promise<Match> {
|
||||||
|
logger.info("PocketBase | Updating match", { id, data });
|
||||||
|
const result = await pb.collection("matches").update<Match>(id, data);
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
|
||||||
|
async deleteMatchesByTournament(tournamentId: string): Promise<void> {
|
||||||
|
logger.info("PocketBase | Deleting matches for tournament", tournamentId);
|
||||||
|
const matches = await pb.collection("matches").getFullList({
|
||||||
|
filter: `tournament = "${tournamentId}"`,
|
||||||
|
fields: "id",
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
matches.map(match => pb.collection("matches").delete(match.id))
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,13 +1,28 @@
|
|||||||
import type {
|
import type {
|
||||||
Player,
|
Player,
|
||||||
|
PlayerInfo,
|
||||||
PlayerInput,
|
PlayerInput,
|
||||||
PlayerUpdateInput,
|
PlayerUpdateInput,
|
||||||
} from "@/features/players/types";
|
} from "@/features/players/types";
|
||||||
import { transformPlayer } from "@/lib/pocketbase/util/transform-types";
|
import { transformPlayer, transformPlayerInfo } from "@/lib/pocketbase/util/transform-types";
|
||||||
import PocketBase from "pocketbase";
|
import PocketBase from "pocketbase";
|
||||||
|
import { DataFetchOptions } from "./base";
|
||||||
|
|
||||||
export function createPlayersService(pb: PocketBase) {
|
export function createPlayersService(pb: PocketBase) {
|
||||||
return {
|
return {
|
||||||
|
async getPlayerInfo(id: string): Promise<PlayerInfo> {
|
||||||
|
const result = await pb.collection("players").getOne(id, {
|
||||||
|
fields: "id,first_name,last_name"
|
||||||
|
});
|
||||||
|
return transformPlayerInfo(result);
|
||||||
|
},
|
||||||
|
|
||||||
|
async listPlayerInfos(): Promise<PlayerInfo[]> {
|
||||||
|
const result = await pb.collection("players").getFullList({
|
||||||
|
fields: "id,first_name,last_name",
|
||||||
|
});
|
||||||
|
return result.map(transformPlayerInfo);
|
||||||
|
},
|
||||||
async getPlayerByAuthId(authId: string): Promise<Player | null> {
|
async getPlayerByAuthId(authId: string): Promise<Player | null> {
|
||||||
const result = await pb.collection("players").getList<Player>(1, 1, {
|
const result = await pb.collection("players").getList<Player>(1, 1, {
|
||||||
filter: `auth_id = "${authId}"`,
|
filter: `auth_id = "${authId}"`,
|
||||||
|
|||||||
@@ -1,10 +1,26 @@
|
|||||||
import { logger } from "@/lib/logger";
|
import { logger } from "@/lib/logger";
|
||||||
import PocketBase from "pocketbase";
|
import PocketBase from "pocketbase";
|
||||||
import { transformTeam } from "@/lib/pocketbase/util/transform-types";
|
import { transformTeam, transformTeamInfo } from "@/lib/pocketbase/util/transform-types";
|
||||||
import { Team } from "@/features/teams/types";
|
import { Team, TeamInfo } from "@/features/teams/types";
|
||||||
|
import { DataFetchOptions } from "./base";
|
||||||
|
|
||||||
export function createTeamsService(pb: PocketBase) {
|
export function createTeamsService(pb: PocketBase) {
|
||||||
return {
|
return {
|
||||||
|
async getTeamInfo(id: string): Promise<TeamInfo> {
|
||||||
|
logger.info("PocketBase | Getting team info", id);
|
||||||
|
const result = await pb.collection("teams").getOne(id, {
|
||||||
|
fields: "id,name,primary_color,accent_color"
|
||||||
|
});
|
||||||
|
return transformTeamInfo(result);
|
||||||
|
},
|
||||||
|
|
||||||
|
async listTeamInfos(): Promise<TeamInfo[]> {
|
||||||
|
logger.info("PocketBase | Listing team infos");
|
||||||
|
const result = await pb.collection("teams").getFullList({
|
||||||
|
fields: "id,name,primary_color,accent_color"
|
||||||
|
});
|
||||||
|
return result.map(transformTeamInfo);
|
||||||
|
},
|
||||||
async getTeam(id: string): Promise<Team | null> {
|
async getTeam(id: string): Promise<Team | null> {
|
||||||
logger.info("PocketBase | Getting team", id);
|
logger.info("PocketBase | Getting team", id);
|
||||||
const result = await pb.collection("teams").getOne(id, {
|
const result = await pb.collection("teams").getOne(id, {
|
||||||
|
|||||||
@@ -1,32 +1,32 @@
|
|||||||
import { logger } from "@/lib/logger";
|
import { logger } from "@/lib/logger";
|
||||||
import type {
|
import type {
|
||||||
Tournament,
|
Tournament,
|
||||||
|
TournamentInfo,
|
||||||
TournamentInput,
|
TournamentInput,
|
||||||
TournamentUpdateInput,
|
TournamentUpdateInput,
|
||||||
} from "@/features/tournaments/types";
|
} from "@/features/tournaments/types";
|
||||||
import type { Team } from "@/features/teams/types";
|
import type { Team } from "@/features/teams/types";
|
||||||
import PocketBase from "pocketbase";
|
import PocketBase from "pocketbase";
|
||||||
import { transformTournament } from "@/lib/pocketbase/util/transform-types";
|
import { transformTournament, transformTournamentInfo } from "@/lib/pocketbase/util/transform-types";
|
||||||
import { transformTeam } from "@/lib/pocketbase/util/transform-types";
|
import { transformTeam } from "@/lib/pocketbase/util/transform-types";
|
||||||
|
|
||||||
export function createTournamentsService(pb: PocketBase) {
|
export function createTournamentsService(pb: PocketBase) {
|
||||||
return {
|
return {
|
||||||
async getTournament(id: string): Promise<Tournament | null> {
|
async getTournament(id: string): Promise<Tournament> {
|
||||||
logger.info("PocketBase | Getting tournament", id);
|
|
||||||
const result = await pb.collection("tournaments").getOne(id, {
|
const result = await pb.collection("tournaments").getOne(id, {
|
||||||
expand: "teams, teams.players",
|
expand: "teams, teams.players, matches, matches.tournament",
|
||||||
});
|
});
|
||||||
return transformTournament(result);
|
return transformTournament(result);
|
||||||
},
|
},
|
||||||
async listTournaments(): Promise<Tournament[]> {
|
async listTournaments(): Promise<TournamentInfo[]> {
|
||||||
const result = await pb
|
const result = await pb
|
||||||
.collection("tournaments")
|
.collection("tournaments")
|
||||||
.getFullList<Tournament>({
|
.getFullList({
|
||||||
fields: "id,name,start_time,end_time,logo,created",
|
fields: "id,name,location,start_time,end_time,logo",
|
||||||
sort: "-created",
|
sort: "-created",
|
||||||
});
|
});
|
||||||
|
|
||||||
return result.map(transformTournament);
|
return result.map(transformTournamentInfo);
|
||||||
},
|
},
|
||||||
async createTournament(data: TournamentInput): Promise<Tournament> {
|
async createTournament(data: TournamentInput): Promise<Tournament> {
|
||||||
const result = await pb
|
const result = await pb
|
||||||
@@ -79,6 +79,18 @@ export function createTournamentsService(pb: PocketBase) {
|
|||||||
|
|
||||||
return transformTournament(result);
|
return transformTournament(result);
|
||||||
},
|
},
|
||||||
|
async updateTournamentMatches(
|
||||||
|
tournamentId: string,
|
||||||
|
matchIds: string[]
|
||||||
|
): Promise<Tournament> {
|
||||||
|
logger.info("PocketBase | Updating tournament matches", { tournamentId, matchCount: matchIds.length });
|
||||||
|
const result = await pb
|
||||||
|
.collection("tournaments")
|
||||||
|
.update<Tournament>(tournamentId, {
|
||||||
|
matches: matchIds
|
||||||
|
});
|
||||||
|
return transformTournament(result);
|
||||||
|
},
|
||||||
async getUnenrolledTeams(tournamentId: string): Promise<Team[]> {
|
async getUnenrolledTeams(tournamentId: string): Promise<Team[]> {
|
||||||
try {
|
try {
|
||||||
logger.info(
|
logger.info(
|
||||||
|
|||||||
@@ -1,18 +1,75 @@
|
|||||||
import { Player } from "@/features/players/types";
|
import { Match } from "@/features/matches/types";
|
||||||
import { Team } from "@/features/teams/types";
|
import { Player, PlayerInfo } from "@/features/players/types";
|
||||||
import { Tournament } from "@/features/tournaments/types";
|
import { Team, TeamInfo } from "@/features/teams/types";
|
||||||
|
import { Tournament, TournamentInfo } from "@/features/tournaments/types";
|
||||||
|
|
||||||
// pocketbase does this weird thing with relations where it puts them under a seperate "expand" field
|
// pocketbase does this weird thing with relations where it puts them under a seperate "expand" field
|
||||||
// this file transforms raw pocketbase results to our types
|
// this file transforms raw pocketbase results to our types
|
||||||
|
|
||||||
|
export function transformPlayerInfo(record: any): PlayerInfo {
|
||||||
|
return {
|
||||||
|
id: record.id,
|
||||||
|
first_name: record.first_name,
|
||||||
|
last_name: record.last_name,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function transformTeamInfo(record: any): TeamInfo {
|
||||||
|
const players = record.expand?.players?.map(transformPlayerInfo) ?? [];
|
||||||
|
return {
|
||||||
|
id: record.id,
|
||||||
|
name: record.name,
|
||||||
|
primary_color: record.primary_color,
|
||||||
|
accent_color: record.accent_color,
|
||||||
|
players,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const transformMatch = (record: any): Match => {
|
||||||
|
return {
|
||||||
|
id: record.id,
|
||||||
|
order: record.name,
|
||||||
|
lid: record.lid,
|
||||||
|
reset: record.reset,
|
||||||
|
round: record.round,
|
||||||
|
home_cups: record.home_cups,
|
||||||
|
away_cups: record.away_cups,
|
||||||
|
ot_count: record.ot_count,
|
||||||
|
start_time: record.start_time,
|
||||||
|
end_time: record.end_time,
|
||||||
|
bye: record.bye,
|
||||||
|
home_from_lid: record.home_from_lid,
|
||||||
|
away_from_lid: record.away_from_lid,
|
||||||
|
home_from_loser: record.home_from_loser,
|
||||||
|
away_from_loser: record.away_from_loser,
|
||||||
|
is_losers_bracket: record.is_losers_bracket,
|
||||||
|
tournament: transformTournamentInfo(record.expand?.tournament),
|
||||||
|
home: record.expand?.home ? transformTeamInfo(record.expand.home) : undefined,
|
||||||
|
away: record.expand?.away ? transformTeamInfo(record.expand.away) : undefined,
|
||||||
|
created: record.created,
|
||||||
|
updated: record.updated,
|
||||||
|
home_seed: record.home_seed,
|
||||||
|
away_seed: record.away_seed,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const transformTournamentInfo = (record: any): TournamentInfo => {
|
||||||
|
return {
|
||||||
|
id: record.id,
|
||||||
|
name: record.name,
|
||||||
|
location: record.location,
|
||||||
|
start_time: record.start_time,
|
||||||
|
logo: record.logo,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function transformPlayer(record: any): Player {
|
export function transformPlayer(record: any): Player {
|
||||||
const sadf: string[] = [];
|
|
||||||
const teams =
|
const teams =
|
||||||
record.expand?.teams
|
record.expand?.teams
|
||||||
?.sort((a: Team, b: Team) =>
|
?.sort((a: any, b: any) =>
|
||||||
new Date(a.created) < new Date(b.created) ? -1 : 0
|
new Date(a.created) < new Date(b.created) ? -1 : 0
|
||||||
)
|
)
|
||||||
?.map(transformTeam) ?? [];
|
?.map(transformTeamInfo) ?? [];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: record.id!,
|
id: record.id!,
|
||||||
@@ -28,16 +85,16 @@ export function transformPlayer(record: any): Player {
|
|||||||
export function transformTeam(record: any): Team {
|
export function transformTeam(record: any): Team {
|
||||||
const players =
|
const players =
|
||||||
record.expand?.players
|
record.expand?.players
|
||||||
?.sort((a: Player, b: Player) =>
|
?.sort((a: any, b: any) =>
|
||||||
new Date(a.created!) < new Date(b.created!) ? -1 : 0
|
new Date(a.created!) < new Date(b.created!) ? -1 : 0
|
||||||
)
|
)
|
||||||
?.map(transformPlayer) ?? [];
|
?.map(transformPlayerInfo) ?? [];
|
||||||
const tournaments =
|
const tournaments =
|
||||||
record.expand?.tournaments
|
record.expand?.tournaments
|
||||||
?.sort((a: Tournament, b: Tournament) =>
|
?.sort((a: any, b: any) =>
|
||||||
new Date(a.created!) < new Date(b.created!) ? -1 : 0
|
new Date(a.created!) < new Date(b.created!) ? -1 : 0
|
||||||
)
|
)
|
||||||
?.map(transformTournament) ?? [];
|
?.map(transformTournamentInfo) ?? [];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: record.id,
|
id: record.id,
|
||||||
@@ -63,11 +120,18 @@ export function transformTeam(record: any): Team {
|
|||||||
export function transformTournament(record: any): Tournament {
|
export function transformTournament(record: any): Tournament {
|
||||||
const teams =
|
const teams =
|
||||||
record.expand?.teams
|
record.expand?.teams
|
||||||
?.sort((a: Team, b: Team) =>
|
?.sort((a: any, b: any) =>
|
||||||
new Date(a.created) < new Date(b.created) ? -1 : 0
|
new Date(a.created) < new Date(b.created) ? -1 : 0
|
||||||
)
|
)
|
||||||
?.map(transformTeam) ?? [];
|
?.map(transformTeamInfo) ?? [];
|
||||||
|
|
||||||
|
const matches =
|
||||||
|
record.expand?.matches
|
||||||
|
?.sort((a: any, b: any) =>
|
||||||
|
a.lid - b.lid ? -1 : 0
|
||||||
|
)
|
||||||
|
?.map(transformMatch) ?? [];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: record.id,
|
id: record.id,
|
||||||
name: record.name,
|
name: record.name,
|
||||||
@@ -81,5 +145,6 @@ export function transformTournament(record: any): Tournament {
|
|||||||
created: record.created,
|
created: record.created,
|
||||||
updated: record.updated,
|
updated: record.updated,
|
||||||
teams,
|
teams,
|
||||||
|
matches
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user