sendou.ink/app/features/tournament/routes/to.$id.teams.$tid.tsx
Kalle 144da5d158
Tournament groups->SE, underground bracket etc. (#1628)
* Renders groups

* Bracket data refactoring

* Starting bracket working (first bracket only)

* TODOs + crash fix

* Source bracket logic initial

* Bracket progression (DE underground bracket)

* Preview working for second bracket

* Bracket nav initial

* Check-in to bracket feature

* Start Underground bracket

* Team/teams pages tweaks to support underground bracket

* Underground bracket finalization progress

* Tournament class

* id -> userId + more useOutletContext removed

* Bracket loader refactored out

* Migrate admin to useTournament

* Bracket.settings

* Slim tournament loader

* Fix useEffect infinite loop

* Adjust waiting for teams text

* Refactor old tournament DB call from to admin

* Admin action: check in/out from specific bracket

* Standings work

* Back button from match page -> correct bracket

* Standings logic for DE grand finals

* Standings + finalize bracket

* Dev log

* Unit tests utils etc.

* Adjust TODOs

* Fix round robin issues

* Add RR tests

* Round robin standings initial

* Wins against tied + points tiebreaker progress

* Fix losing state when switching between tabs

* Add check-in indications to seeding page

* Link to user page on seed tool

* Submit points

* Total points from bracket manager

* findById gonezino

* Ahead of time check-in

* Couple todos

* Reopen logic refactor

* Tournament format settings

* RR->SE placements, skipping underground bracket

* Fix tournament team page round names

* More teams to UG bracket if first round of DE only byes

* Fix graphics bug

* Fixes

* Fix some E2E tests

* Fix E2E tests
2024-01-30 00:32:13 +02:00

262 lines
8.0 KiB
TypeScript

import type { LoaderFunctionArgs } from "@remix-run/node";
import { Link, useLoaderData } from "@remix-run/react";
import clsx from "clsx";
import { useTranslation } from "react-i18next";
import { Avatar } from "~/components/Avatar";
import { ModeImage, StageImage } from "~/components/Image";
import { Placement } from "~/components/Placement";
import { Popover } from "~/components/Popover";
import { Redirect } from "~/components/Redirect";
import type { TournamentDataTeam } from "~/features/tournament-bracket/core/Tournament.server";
import type { TournamentMaplistSource } from "~/modules/tournament-map-list-generator";
import {
tournamentMatchPage,
tournamentPage,
tournamentTeamPage,
userPage,
} from "~/utils/urls";
import { TeamWithRoster } from "../components/TeamWithRoster";
import {
tournamentTeamSets,
winCounts,
type PlayedSet,
} from "../core/sets.server";
import {
tournamentIdFromParams,
tournamentRoundI18nKey,
tournamentTeamIdFromParams,
} from "../tournament-utils";
import { useTournament } from "./to.$id";
export const loader = ({ params }: LoaderFunctionArgs) => {
const tournamentId = tournamentIdFromParams(params);
const tournamentTeamId = tournamentTeamIdFromParams(params);
const sets = tournamentTeamSets({ tournamentTeamId, tournamentId });
return {
tournamentTeamId,
sets,
// TODO: could be inferred from tournament data
winCounts: winCounts(sets),
};
};
export default function TournamentTeamPage() {
const data = useLoaderData<typeof loader>();
const tournament = useTournament();
const teamIndex = tournament.ctx.teams.findIndex(
(t) => t.id === data.tournamentTeamId,
);
const team = tournament.teamById(data.tournamentTeamId);
if (!team) {
return <Redirect to={tournamentPage(tournament.ctx.id)} />;
}
return (
<div className="stack lg">
<TeamWithRoster
team={team}
mapPool={team.mapPool}
activePlayers={
data.sets.length > 0
? tournament
.participatedPlayersByTeamId(team.id)
.map((p) => p.userId)
: undefined
}
/>
{data.winCounts.sets.total > 0 ? (
<StatSquares
seed={teamIndex + 1}
teamsCount={tournament.ctx.teams.length}
/>
) : null}
<div className="tournament__team__sets">
{data.sets.map((set) => {
return <SetInfo key={set.tournamentMatchId} set={set} team={team} />;
})}
</div>
</div>
);
}
function StatSquares({
seed,
teamsCount,
}: {
seed: number;
teamsCount: number;
}) {
const { t } = useTranslation(["tournament"]);
const data = useLoaderData<typeof loader>();
const tournament = useTournament();
const placement = tournament.standings.find(
(s) => s.team.id === data.tournamentTeamId,
)?.placement;
const undergroundBracket = tournament.brackets.find((b) => b.isUnderground);
const undergroundPlacement = undergroundBracket?.standings.find(
(s) => s.team.id === data.tournamentTeamId,
)?.placement;
return (
<div className="tournament__team__stats">
<div className="tournament__team__stat">
<div className="tournament__team__stat__title">
{t("tournament:team.setWins")}
</div>
<div className="tournament__team__stat__main">
{data.winCounts.sets.won} / {data.winCounts.sets.total}
</div>
<div className="tournament__team__stat__sub">
{data.winCounts.sets.percentage}%
</div>
</div>
<div className="tournament__team__stat">
<div className="tournament__team__stat__title">
{t("tournament:team.mapWins")}
</div>
<div className="tournament__team__stat__main">
{data.winCounts.maps.won} / {data.winCounts.maps.total}
</div>
<div className="tournament__team__stat__sub">
{data.winCounts.maps.percentage}%
</div>
</div>
<div className="tournament__team__stat">
<div className="tournament__team__stat__title">
{t("tournament:team.seed")}
</div>
<div className="tournament__team__stat__main">{seed}</div>
<div className="tournament__team__stat__sub">
{t("tournament:team.seed.footer", { count: teamsCount })}
</div>
</div>
<div className="tournament__team__stat">
<div className="tournament__team__stat__title">
{t("tournament:team.placement")}
</div>
<div className="tournament__team__stat__main">
{placement ? <Placement placement={placement} textOnly /> : "-"}
{undergroundPlacement ? (
<>
{" "}
/ <Placement placement={undergroundPlacement} textOnly />
</>
) : null}
</div>
{undergroundPlacement ? (
<div className="tournament__team__stat__sub">
{t("tournament:team.placement.footer")}
</div>
) : null}
</div>
</div>
);
}
function SetInfo({ set, team }: { set: PlayedSet; team: TournamentDataTeam }) {
const { t } = useTranslation(["tournament"]);
const tournament = useTournament();
const sourceToText = (source: TournamentMaplistSource) => {
switch (source) {
case "BOTH":
return t("tournament:pickInfo.both");
case "DEFAULT":
return t("tournament:pickInfo.default");
case "TIEBREAKER":
return t("tournament:pickInfo.tiebreaker");
default: {
const teamName =
source === set.opponent.id ? set.opponent.name : team.name;
return t("tournament:pickInfo.team.specific", { team: teamName });
}
}
};
return (
<div className="tournament__team__set">
<div className="tournament__team__set__top-container">
<div className="tournament__team__set__score">
{set.score.join("-")}
</div>
<Link
to={tournamentMatchPage({
matchId: set.tournamentMatchId,
eventId: tournament.ctx.id,
})}
className="tournament__team__set__round-name"
>
{t(`tournament:${tournamentRoundI18nKey(set.round)}`, {
round: set.round.round,
})}{" "}
- {set.stageName}
</Link>
</div>
<div className="overlap-divider">
<div className="stack horizontal sm">
{set.maps.map(({ stageId, modeShort, result, source }, i) => {
return (
<Popover
key={i}
buttonChildren={
<ModeImage
mode={modeShort}
size={20}
containerClassName={clsx("tournament__team__set__mode", {
tournament__team__set__mode__loss: result === "loss",
})}
/>
}
placement="top"
>
<div className="tournament__team__set__stage-container">
<StageImage
stageId={stageId}
width={125}
className="rounded-sm"
/>
{sourceToText(source)}
</div>
</Popover>
);
})}
</div>
</div>
<div className="tournament__team__set__opponent">
<div className="tournament__team__set__opponent__vs">vs.</div>
<Link
to={tournamentTeamPage({
tournamentTeamId: set.opponent.id,
eventId: tournament.ctx.id,
})}
className="tournament__team__set__opponent__team"
>
{set.opponent.name}
</Link>
<div className="tournament__team__set__opponent__members">
{set.opponent.roster.map((user) => {
return (
<Link
to={userPage(user)}
key={user.id}
className="tournament__team__set__opponent__member"
>
<Avatar user={user} size="xxs" />
{user.discordName}
</Link>
);
})}
</div>
</div>
</div>
);
}