mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-09-11 21:56:11 -05:00
Create tournaments rounds almost™️ edition
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -50,6 +50,7 @@ export async function findByNameForUrl({
|
||||
},
|
||||
brackets: {
|
||||
select: {
|
||||
id: true,
|
||||
type: true,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -216,7 +216,7 @@
|
||||
outline: 2px solid var(--tournaments-text-transparent);
|
||||
}
|
||||
|
||||
.tournament__outlet-container {
|
||||
.tournament__outlet-spacer {
|
||||
padding-block-start: var(--s-8);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user