Create tournaments rounds almost™️ edition

This commit is contained in:
Kalle (Sendou)
2021-12-22 18:19:41 +02:00
parent 8adf0b9f15
commit 88dc19f450
7 changed files with 100 additions and 17 deletions

View File

@@ -1,4 +1,5 @@
import type { BracketType, Stage, TeamOrder } from ".prisma/client";
import { v4 as uuidv4 } from "uuid";
import invariant from "tiny-invariant";
import { TOURNAMENT_TEAM_ROSTER_MIN_SIZE } from "../../constants";
import { FindTournamentByNameForUrlI } from "../../services/tournament";
@@ -182,7 +183,8 @@ export function countParticipants(teams: FindTournamentByNameForUrlI["teams"]) {
}, 0);
}
interface TournamentRoundForDB {
export interface TournamentRoundForDB {
id: string;
position: number;
stages: {
position: number;
@@ -256,6 +258,7 @@ export function tournamentRoundsForDB({
});
result.push({
id: uuidv4(),
position,
stages,
matches,

View File

@@ -50,6 +50,7 @@ export async function findByNameForUrl({
},
brackets: {
select: {
id: true,
type: true,
},
},

View File

@@ -117,7 +117,8 @@ export default function TournamentPage() {
</div>
<div className="tournament__container__spacer" />
<ActionSection />
<div className="tournament__outlet-container"></div>
<div className="tournament__outlet-spacer" />
{/* TODO: pass context instead of useMatches */}
<Outlet />
</div>
);

View File

@@ -1,6 +1,11 @@
import type { Mode, Stage } from ".prisma/client";
import classNames from "classnames";
import type { ActionFunction, LinksFunction, LoaderFunction } from "remix";
import {
ActionFunction,
LinksFunction,
LoaderFunction,
useMatches,
} from "remix";
import { Form, json, redirect, useLoaderData } from "remix";
import invariant from "tiny-invariant";
import { Alert } from "~/components/Alert";
@@ -9,6 +14,7 @@ import { Catcher } from "~/components/Catcher";
import { modesShort, modesShortToLong } from "~/constants";
import { eliminationBracket } from "~/core/tournament/algorithms";
import {
EliminationBracket,
EliminationBracketSide,
participantCountToRoundsInfo,
} from "~/core/tournament/bracket";
@@ -21,6 +27,7 @@ import type {
import {
createTournamentRounds,
findTournamentByNameForUrl,
FindTournamentByNameForUrlI,
} from "~/services/tournament";
import startBracketTabStylesUrl from "~/styles/tournament-start.css";
import { requireUser } from "~/utils";
@@ -32,11 +39,13 @@ export const links: LinksFunction = () => {
};
export const action: ActionFunction = async ({ request, params, context }) => {
const mapListString = (await request.formData()).get("map-list");
const formData = await request.formData();
const mapListString = formData.get("map-list");
const bracketId = formData.get("bracket-id");
invariant(typeof mapListString === "string", "Type of map list not string");
const mapList = JSON.parse(
mapListString
) as UseTournamentRoundsState["bracket"];
invariant(typeof bracketId === "string", "Type of bracket id not string");
// TODO: could use Zod here
const mapList = JSON.parse(mapListString) as EliminationBracket<Stage[][]>;
const organizationNameForUrl = params.organization;
const tournamentNameForUrl = params.tournament;
@@ -50,10 +59,9 @@ export const action: ActionFunction = async ({ request, params, context }) => {
organizationNameForUrl,
tournamentNameForUrl,
userId: user.id,
bracketId,
});
return null;
return redirect(
`/to/${organizationNameForUrl}/${tournamentNameForUrl}/bracket`
);
@@ -94,18 +102,26 @@ export const loader: LoaderFunction = async ({ params }) => {
// TODO: component that shows a table of map, counts in the map pool, which rounds
// TODO: handle warning if check-in has not concluded
export default function StartBracketTab() {
const [, parentRoute] = useMatches();
const { brackets } = parentRoute.data as FindTournamentByNameForUrlI;
const args = useLoaderData<UseTournamentRoundsArgs>();
const [{ bracket, showAlert, actionButtonsDisabled }, dispatch] =
useTournamentRounds(args);
// TODO: dropdown to select this
const bracketId = brackets[0].id;
return (
<Form method="post" className="width-100">
<input type="hidden" name="map-list" value={JSON.stringify(bracket)} />
<input
type="hidden"
name="tournament-id"
value={JSON.stringify(bracket)}
name="map-list"
value={JSON.stringify({
losers: bracket.winners.map((round) => round.mapList),
winners: bracket.winners.map((round) => round.mapList),
})}
/>
<input type="hidden" name="bracket-id" value={bracketId} />
<div className="tournament__start__container">
<ActionButtons
dispatch={dispatch}

View File

@@ -1,16 +1,20 @@
import { Prisma } from ".prisma/client";
import type { Prisma, Stage } from ".prisma/client";
import {
TOURNAMENT_CHECK_IN_CLOSING_MINUTES_FROM_START,
TOURNAMENT_TEAM_ROSTER_MAX_SIZE,
} from "~/constants";
import {
EliminationBracket,
tournamentRoundsForDB,
} from "~/core/tournament/bracket";
import { isTournamentAdmin } from "~/core/tournament/permissions";
import { sortTeamsBySeed } from "~/core/tournament/utils";
import type { UseTournamentRoundsState } from "~/hooks/useTournamentRounds/types";
import * as Tournament from "~/models/Tournament";
import * as TournamentTeam from "~/models/TournamentTeam";
import * as TournamentTeamMember from "~/models/TournamentTeamMember";
import * as TrustRelationship from "~/models/TrustRelationship";
import { Serialized, Unpacked } from "~/utils";
import { db } from "~/utils/db.server";
export type FindTournamentByNameForUrlI = Serialized<
Prisma.PromiseReturnType<typeof findTournamentByNameForUrl>
@@ -126,11 +130,13 @@ export async function createTournamentRounds({
tournamentNameForUrl,
mapList,
userId,
bracketId,
}: {
organizationNameForUrl: string;
tournamentNameForUrl: string;
mapList: UseTournamentRoundsState["bracket"];
mapList: EliminationBracket<Stage[][]>;
userId: string;
bracketId: string;
}) {
const tournament = await Tournament.findByNameForUrl({
organizationNameForUrl,
@@ -141,6 +147,61 @@ export async function createTournamentRounds({
if (!isTournamentAdmin({ organization: tournament.organizer, userId })) {
throw new Response("Not tournament admin", { status: 401 });
}
const bracket = tournament.brackets.find(
(bracket) => bracket.id === bracketId
);
// TODO: OR rounds i.e. bracket was already started
if (!bracket) {
throw new Response("Invalid bracket id provided", { status: 400 });
}
const participantsSeeded = tournament.teams.sort(
sortTeamsBySeed(tournament.seeds)
);
const rounds = tournamentRoundsForDB({
mapList,
bracketType: bracket.type,
participantsSeeded,
});
return db.$transaction([
db.tournamentRound.createMany({
data: rounds.map((round) => ({
bracketId: bracket.id,
id: round.id,
position: round.position,
})),
}),
db.tournamentRoundStage.createMany({
data: rounds.flatMap((round) => {
return round.stages.map((stage) => ({ ...stage, roundId: round.id }));
}),
}),
db.tournamentMatch.createMany({
data: rounds.flatMap((round) => {
return round.matches.map((match) => ({
...match,
roundId: round.id,
}));
}),
}),
db.tournamentMatchParticipant.createMany({
data: rounds.flatMap((round) => {
return round.matches.flatMap((match) => {
return match.participants.flatMap((participant) => {
if (participant.team === "BYE") return [];
return {
teamId: participant.team.id,
matchId: match.id,
order: participant.order,
};
});
});
}),
}),
]);
}
export async function joinTeamViaInviteCode({

View File

@@ -3,7 +3,7 @@
--bg-lighter: hsl(237.3deg 42.3% 35.6%);
--bg-lighter-transparent: hsla(237.3deg 42.3% 35.6% / 50%);
--border: hsl(237.3deg 42.3% 45.6%);
--button-text: rgba(0, 0, 0, 0.85);
--button-text: rgb(0 0 0 / 85%);
--text: rgb(255 255 255 / 95%);
--text-lighter: rgb(255 255 255 / 55%);
--theme-error: rgb(219 70 65);
@@ -13,6 +13,7 @@
--theme: hsl(255deg 66.7% 75%);
--theme-transparent: hsl(255deg 66.7% 75% / 40%);
--theme-secondary: hsl(85deg 66.7% 55.3%);
/* background-pattern.svg not using this currently */
--theme-pattern: #6741d9;
--rounded: 16px;

View File

@@ -216,7 +216,7 @@
outline: 2px solid var(--tournaments-text-transparent);
}
.tournament__outlet-container {
.tournament__outlet-spacer {
padding-block-start: var(--s-8);
}