mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-10-03 00:27:30 -05:00
Tournament bracket working with data
This commit is contained in:
@@ -1,126 +1,96 @@
|
||||
import classNames from "classnames";
|
||||
import type { BracketModified } from "~/services/tournament";
|
||||
import { Unpacked } from "~/utils";
|
||||
|
||||
const rounds = [
|
||||
{
|
||||
title: "Round 1",
|
||||
bestOf: 3,
|
||||
status: "DONE",
|
||||
},
|
||||
{
|
||||
title: "Round 2",
|
||||
bestOf: 5,
|
||||
status: "INPROGRESS",
|
||||
},
|
||||
{
|
||||
title: "Semifinals",
|
||||
bestOf: 5,
|
||||
status: "UPCOMING",
|
||||
},
|
||||
{
|
||||
title: "Finals",
|
||||
bestOf: 7,
|
||||
status: "UPCOMING",
|
||||
},
|
||||
] as const;
|
||||
|
||||
export function EliminationBracket() {
|
||||
export function EliminationBracket({
|
||||
bracketSide,
|
||||
}: {
|
||||
bracketSide: BracketModified["winners"];
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className="tournament-bracket__elim__container"
|
||||
style={
|
||||
{
|
||||
"--brackets-columns": rounds.length,
|
||||
"--brackets-matches": 4,
|
||||
"--brackets-columns": bracketSide.length,
|
||||
"--brackets-max-matches": bracketSide[0].matches.length,
|
||||
} as any
|
||||
}
|
||||
>
|
||||
<div className="tournament-bracket__elim__bracket">
|
||||
{rounds.map((round, i) => (
|
||||
{bracketSide.map((round, i) => (
|
||||
<RoundInfo
|
||||
key={round.title}
|
||||
round={round}
|
||||
isLast={i === rounds.length - 1}
|
||||
key={round.id}
|
||||
title={round.name}
|
||||
isLast={i === bracketSide.length - 1}
|
||||
bestOf={round.bestOf}
|
||||
status="UPCOMING"
|
||||
/>
|
||||
))}
|
||||
<div
|
||||
className="tournament-bracket__elim__column"
|
||||
style={{ "--brackets-column-matches": 4 } as any}
|
||||
>
|
||||
<div className="tournament-bracket__elim__matches">
|
||||
<Match />
|
||||
<Match />
|
||||
<Match />
|
||||
<Match />
|
||||
</div>
|
||||
<div className="tournament-bracket__elim__lines">
|
||||
<div />
|
||||
<div />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="tournament-bracket__elim__column"
|
||||
style={{ "--brackets-column-matches": 2 } as any}
|
||||
>
|
||||
<div className="tournament-bracket__elim__matches">
|
||||
<Match />
|
||||
<Match />
|
||||
</div>
|
||||
<div className="tournament-bracket__elim__lines">
|
||||
<div />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="tournament-bracket__elim__column"
|
||||
style={
|
||||
{
|
||||
"--brackets-column-matches": 0,
|
||||
"--brackets-bottom-border-length": 0,
|
||||
} as any
|
||||
}
|
||||
>
|
||||
<div className="tournament-bracket__elim__matches">
|
||||
<Match />
|
||||
</div>
|
||||
<div className="tournament-bracket__elim__lines">
|
||||
<div />
|
||||
</div>
|
||||
</div>
|
||||
<div className="tournament-bracket__elim__matches">
|
||||
<Match />
|
||||
</div>
|
||||
{bracketSide.map((round, i) => {
|
||||
return (
|
||||
<div
|
||||
key={round.id}
|
||||
className="tournament-bracket__elim__column"
|
||||
style={
|
||||
{
|
||||
"--brackets-bottom-border-length":
|
||||
round.matches.length === 1 ? 0 : undefined,
|
||||
"--brackets-column-matches":
|
||||
round.matches.length === 1 ? 0 : round.matches.length,
|
||||
} as any
|
||||
}
|
||||
>
|
||||
<div className="tournament-bracket__elim__matches">
|
||||
{round.matches.map((match) => {
|
||||
return <Match key={match.id} match={match} />;
|
||||
})}
|
||||
</div>
|
||||
<div className="tournament-bracket__elim__lines">
|
||||
{i !== bracketSide.length - 1 &&
|
||||
new Array(Math.ceil(round.matches.length / 2))
|
||||
.fill(null)
|
||||
.map((_, i) => <div key={i} />)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function RoundInfo(props: {
|
||||
round: {
|
||||
title: string;
|
||||
bestOf: number;
|
||||
status: "DONE" | "INPROGRESS" | "UPCOMING";
|
||||
};
|
||||
function RoundInfo({
|
||||
title,
|
||||
bestOf,
|
||||
status,
|
||||
isLast,
|
||||
}: {
|
||||
title: string;
|
||||
bestOf: number;
|
||||
status: "DONE" | "INPROGRESS" | "UPCOMING";
|
||||
isLast?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={classNames("tournament-bracket__elim__roundInfo", {
|
||||
highlighted: props.round.status === "INPROGRESS",
|
||||
last: props.isLast,
|
||||
highlighted: status === "INPROGRESS",
|
||||
last: isLast,
|
||||
})}
|
||||
>
|
||||
<div className="tournament-bracket__elim__roundTitle">
|
||||
{props.round.title}
|
||||
</div>
|
||||
{props.round.status !== "DONE" && (
|
||||
<div className="tournament-bracket__elim__bestOf">
|
||||
Bo{props.round.bestOf}
|
||||
</div>
|
||||
<div className="tournament-bracket__elim__roundTitle">{title}</div>
|
||||
{status !== "DONE" && (
|
||||
<div className="tournament-bracket__elim__bestOf">Bo{bestOf}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function Match() {
|
||||
export function Match({
|
||||
match,
|
||||
}: {
|
||||
match: Unpacked<Unpacked<BracketModified["winners"]>["matches"]>;
|
||||
}) {
|
||||
return (
|
||||
<div className="tournament-bracket__elim__match">
|
||||
<div className="tournament-bracket__elim__roundNumber">1</div>
|
||||
@@ -130,7 +100,14 @@ export function Match() {
|
||||
"tournament-bracket__elim__teamOne"
|
||||
)}
|
||||
>
|
||||
Team Olive <span className="tournament-bracket__elim__score">1</span>
|
||||
{match.participants?.[0]}{" "}
|
||||
<span
|
||||
className={classNames("tournament-bracket__elim__score", {
|
||||
"visibility-hidden": typeof match.score?.[0] !== "number",
|
||||
})}
|
||||
>
|
||||
{match.score?.[0] ?? 0}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className={classNames(
|
||||
@@ -138,7 +115,14 @@ export function Match() {
|
||||
"tournament-bracket__elim__teamTwo"
|
||||
)}
|
||||
>
|
||||
Chimera <span className="tournament-bracket__elim__score">1</span>
|
||||
{match.participants?.[1]}{" "}
|
||||
<span
|
||||
className={classNames("tournament-bracket__elim__score", {
|
||||
"visibility-hidden": typeof match.score?.[0] !== "number",
|
||||
})}
|
||||
>
|
||||
{match.score?.[1] ?? 0}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -89,26 +89,33 @@ export function getRoundsDefaultBestOf(
|
||||
};
|
||||
}
|
||||
|
||||
export function winnersRoundNames(count: number, isSE: boolean) {
|
||||
return new Array(count).fill(null).map((_, i) => {
|
||||
if (i === count - 4 + Number(isSE)) {
|
||||
return "Winners' Semifinals";
|
||||
}
|
||||
if (i === count - 3 + Number(isSE)) return "Winners' Finals";
|
||||
if (i === count - 2 + Number(isSE)) return "Grand Finals";
|
||||
if (!isSE && i === count - 1) return "Bracket Reset";
|
||||
return `Winners' Round ${i + 1}`;
|
||||
});
|
||||
}
|
||||
|
||||
export function losersRoundNames(count: number) {
|
||||
return new Array(count)
|
||||
.fill(null)
|
||||
.map((_, i) =>
|
||||
i === count - 1 ? "Losers' Finals" : `Losers' Round ${i + 1}`
|
||||
);
|
||||
}
|
||||
|
||||
export function getRoundNames(bracket: Bracket): EliminationBracket<string[]> {
|
||||
const { winners: winnersRoundCount, losers: losersRoundCount } =
|
||||
countRounds(bracket);
|
||||
|
||||
return {
|
||||
winners: new Array(winnersRoundCount).fill(null).map((_, i) => {
|
||||
const isSE = bracket.losers.length === 0;
|
||||
if (i === winnersRoundCount - 4 + Number(isSE)) {
|
||||
return "Winners' Semifinals";
|
||||
}
|
||||
if (i === winnersRoundCount - 3 + Number(isSE)) return "Winners' Finals";
|
||||
if (i === winnersRoundCount - 2 + Number(isSE)) return "Grand Finals";
|
||||
if (!isSE && i === winnersRoundCount - 1) return "Bracket Reset";
|
||||
return `Winners' Round ${i + 1}`;
|
||||
}),
|
||||
losers: new Array(losersRoundCount)
|
||||
.fill(null)
|
||||
.map((_, i) =>
|
||||
i === losersRoundCount - 1 ? "Losers' Finals" : `Losers' Round ${i + 1}`
|
||||
),
|
||||
winners: winnersRoundNames(winnersRoundCount, losersRoundCount === 0),
|
||||
losers: losersRoundNames(losersRoundCount),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ export function findById(bracketId: string) {
|
||||
select: {
|
||||
rounds: {
|
||||
select: {
|
||||
id: true,
|
||||
position: true,
|
||||
_count: {
|
||||
select: {
|
||||
@@ -16,6 +17,7 @@ export function findById(bracketId: string) {
|
||||
},
|
||||
matches: {
|
||||
select: {
|
||||
id: true,
|
||||
participants: {
|
||||
select: {
|
||||
team: {
|
||||
|
||||
@@ -64,6 +64,7 @@ export default function TournamentPage() {
|
||||
const tournamentIsOver = false;
|
||||
|
||||
if (tournamentHasStarted(data.brackets)) {
|
||||
// TODO: link broken
|
||||
result.push({ code: "bracket", text: "Bracket" });
|
||||
if (!tournamentIsOver) {
|
||||
result.push({ code: "streams", text: "Streams (4)" });
|
||||
|
||||
@@ -19,14 +19,17 @@ export const loader: LoaderFunction = async ({ params }) => {
|
||||
return typedJson(bracket);
|
||||
};
|
||||
|
||||
// TODO: top seed is at the bottom of the tournament
|
||||
// TODO: bottom bracket lines are wrong
|
||||
// TODO: match numbers are off
|
||||
// TODO: make bracket a bit smaller
|
||||
export default function BracketTabWrapper() {
|
||||
const data = useLoaderData<BracketModified>();
|
||||
|
||||
console.log("data", data);
|
||||
|
||||
return (
|
||||
<div className="tournament-bracket__container">
|
||||
<EliminationBracket />
|
||||
<EliminationBracket bracketSide={data.winners} />
|
||||
<EliminationBracket bracketSide={data.losers} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,16 +5,19 @@ import {
|
||||
} from "~/constants";
|
||||
import {
|
||||
EliminationBracket,
|
||||
EliminationBracketSide,
|
||||
losersRoundNames,
|
||||
MapListIds,
|
||||
tournamentRoundsForDB,
|
||||
winnersRoundNames,
|
||||
} from "~/core/tournament/bracket";
|
||||
import { isTournamentAdmin } from "~/core/tournament/permissions";
|
||||
import { sortTeamsBySeed } from "~/core/tournament/utils";
|
||||
import * as Tournament from "~/models/Tournament";
|
||||
import * as TournamentBracket from "~/models/TournamentBracket";
|
||||
import * as TournamentTeam from "~/models/TournamentTeam";
|
||||
import * as TournamentTeamMember from "~/models/TournamentTeamMember";
|
||||
import * as TrustRelationship from "~/models/TrustRelationship";
|
||||
import * as TournamentBracket from "~/models/TournamentBracket";
|
||||
import { Serialized, Unpacked } from "~/utils";
|
||||
import { db } from "~/utils/db.server";
|
||||
|
||||
@@ -127,8 +130,11 @@ export async function findTournamentWithInviteCodes({
|
||||
|
||||
export type BracketModified = EliminationBracket<BracketModifiedSide>;
|
||||
type BracketModifiedSide = {
|
||||
id: string;
|
||||
name: string;
|
||||
bestOf: number;
|
||||
matches: {
|
||||
id: string;
|
||||
score?: [upperTeamScore: number, lowerTeamScore: number];
|
||||
participants?: [upperTeamName: string | null, lowerTeamName: string | null];
|
||||
}[];
|
||||
@@ -139,29 +145,39 @@ export async function bracketById(bracketId: string): Promise<BracketModified> {
|
||||
|
||||
if (!bracket) throw new Response("No bracket found", { status: 404 });
|
||||
|
||||
const winnersRounds = bracket.rounds
|
||||
.filter((round) => round.position > 0)
|
||||
.sort((a, b) => a.position - b.position);
|
||||
|
||||
const losersRounds = bracket.rounds
|
||||
.filter((round) => round.position < 0)
|
||||
.sort((a, b) => b.position - a.position);
|
||||
|
||||
console.log("losersRounds.length", losersRounds.length);
|
||||
|
||||
return {
|
||||
winners: modifyRounds(
|
||||
bracket.rounds
|
||||
.filter((round) => round.position > 0)
|
||||
.sort((a, b) => a.position - b.position)
|
||||
),
|
||||
losers: modifyRounds(
|
||||
bracket.rounds
|
||||
.filter((round) => round.position < 0)
|
||||
.sort((a, b) => b.position - a.position)
|
||||
),
|
||||
winners: modifyRounds(winnersRounds, "winners", losersRounds.length === 0),
|
||||
losers: modifyRounds(losersRounds, "losers", false),
|
||||
};
|
||||
}
|
||||
|
||||
function modifyRounds(
|
||||
rounds: NonNullable<TournamentBracket.FindById>["rounds"]
|
||||
rounds: NonNullable<TournamentBracket.FindById>["rounds"],
|
||||
side: EliminationBracketSide,
|
||||
isSE: boolean
|
||||
): BracketModifiedSide {
|
||||
return rounds.map((round) => {
|
||||
const roundNames =
|
||||
side === "winners"
|
||||
? winnersRoundNames(rounds.length, isSE)
|
||||
: losersRoundNames(rounds.length);
|
||||
return rounds.map((round, i) => {
|
||||
return {
|
||||
id: round.id,
|
||||
name: roundNames[i],
|
||||
bestOf: round._count.stages,
|
||||
matches: round.matches.map((match) => {
|
||||
const score =
|
||||
match.results.length > 0
|
||||
match.participants.length === 2
|
||||
? match.results.reduce(
|
||||
(scores: [number, number], result) => {
|
||||
if (result.winner === "UPPER") scores[0]++;
|
||||
@@ -182,6 +198,7 @@ function modifyRounds(
|
||||
]
|
||||
: undefined;
|
||||
return {
|
||||
id: match.id,
|
||||
score,
|
||||
participants,
|
||||
};
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
/* TODO: should be able to go wider than container https://stackoverflow.com/questions/5581034/is-there-are-way-to-make-a-child-divs-width-wider-than-the-parent-div-using-css */
|
||||
.tournament-bracket__container {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
gap: var(--s-6);
|
||||
}
|
||||
|
||||
.tournament-bracket__elim__container {
|
||||
@@ -95,7 +98,7 @@
|
||||
|
||||
.tournament-bracket__elim__matches {
|
||||
display: flex;
|
||||
height: calc(var(--brackets-match-height) * var(--brackets-matches));
|
||||
height: calc(var(--brackets-match-height) * var(--brackets-max-matches));
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
}
|
||||
@@ -111,7 +114,7 @@
|
||||
width: calc(var(--brackets-match-height) / 4);
|
||||
height: calc(
|
||||
2px + var(--brackets-match-height) *
|
||||
(var(--brackets-matches) / var(--brackets-column-matches))
|
||||
(var(--brackets-max-matches) / var(--brackets-column-matches))
|
||||
);
|
||||
border-top: 2px solid var(--theme-transparent);
|
||||
border-bottom: var(--brackets-bottom-border-length, 2px) solid
|
||||
|
||||
Reference in New Issue
Block a user