sendou.ink/app/features/tournament-bracket/components/TeamRosterInputs.tsx
Kalle ef78d3a2c2
Tournament full (#1373)
* Got something going

* Style overwrites

* width != height

* More playing with lines

* Migrations

* Start bracket initial

* Unhardcode stage generation params

* Link to match page

* Matches page initial

* Support directly adding seed to map list generator

* Add docs

* Maps in matches page

* Add invariant about tie breaker map pool

* Fix PICNIC lacking tie breaker maps

* Only link in bracket when tournament has started

* Styled tournament roster inputs

* Prefer IGN in tournament match page

* ModeProgressIndicator

* Some conditional rendering

* Match action initial + better error display

* Persist bestOf in DB

* Resolve best of ahead of time

* Move brackets-manager to core

* Score reporting works

* Clear winner on score report

* ModeProgressIndicator: highlight winners

* Fix inconsistent input

* Better text when submitting match

* mapCountPlayedInSetWithCertainty that works

* UNDO_REPORT_SCORE implemented

* Permission check when starting tournament

* Remove IGN from upsert

* View match results page

* Source in DB

* Match page waiting for teams

* Move tournament bracket to feature folder

* REOPEN_MATCH initial

* Handle proper resetting of match

* Inline bracket-manager

* Syncify

* Transactions

* Handle match is locked gracefully

* Match page auto refresh

* Fix match refresh called "globally"

* Bracket autoupdate

* Move fillWithNullTillPowerOfTwo to utils with testing

* Fix map lists not visible after tournament started

* Optimize match events

* Show UI while in progress to members

* Fix start tournament alert not being responsive

* Teams can check in

* Fix map list 400

* xxx -> TODO

* Seeds page

* Remove map icons for team page

* Don't display link to seeds after tournament has started

* Admin actions initial

* Change captain admin action

* Make all hooks ts

* Admin actions functioning

* Fix validate error not displaying in CatchBoundary

* Adjust validate args order

* Remove admin loader

* Make delete team button menancing

* Only include checked in teams to bracket

* Optimize to.id route loads

* Working show map list generator toggle

* Update full tournaments flow

* Make full tournaments work with many start times

* Handle undefined in crud

* Dynamic stage banner

* Handle default strat if map list generation fails

* Fix crash on brackets if less than 2 teams

* Add commented out test for reference

* Add TODO

* Add players from team during register

* TrustRelationship

* Prefers not to host feature

* Last before merge

* Rename some vars

* More renames
2023-05-15 22:37:43 +03:00

201 lines
6.0 KiB
TypeScript

import clsx from "clsx";
import clone from "just-clone";
import * as React from "react";
import { TOURNAMENT } from "../../tournament/tournament-constants";
import { Label } from "~/components/Label";
import type {
TournamentLoaderData,
TournamentLoaderTeam,
} from "../../tournament/routes/to.$id";
import type { Unpacked } from "~/utils/types";
import { inGameNameWithoutDiscriminator } from "~/utils/strings";
import { useLoaderData } from "@remix-run/react";
import type { TournamentMatchLoaderData } from "../routes/to.$id.matches.$mid";
import type { Result } from "./ScoreReporter";
export type TeamRosterInputsType = "DEFAULT" | "DISABLED" | "PRESENTATIONAL";
/** Inputs to select who played for teams in a match as well as the winner. Can also be used in a presentational way. */
export function TeamRosterInputs({
teams,
winnerId,
setWinnerId,
checkedPlayers,
setCheckedPlayers,
result,
}: {
teams: [TournamentLoaderTeam, TournamentLoaderTeam];
winnerId?: number | null;
setWinnerId: (newId?: number) => void;
checkedPlayers: [number[], number[]];
setCheckedPlayers?: (newPlayerIds: [number[], number[]]) => void;
result?: Result;
}) {
const presentational = Boolean(result);
const data = useLoaderData<TournamentMatchLoaderData>();
const inputMode = (
team: Unpacked<TournamentLoaderData["teams"]>
): TeamRosterInputsType => {
if (presentational) return "PRESENTATIONAL";
// Disabled in this case because we expect a result to have exactly
// TOURNAMENT_TEAM_ROSTER_MIN_SIZE members per team when reporting it
// so there is no point to let user to change them around
if (team.members.length <= TOURNAMENT.TEAM_MIN_MEMBERS_FOR_FULL) {
return "DISABLED";
}
return "DEFAULT";
};
React.useEffect(() => {
setWinnerId(undefined);
}, [data, setWinnerId]);
return (
<div className="tournament-bracket__during-match-actions__rosters">
{teams.map((team, teamI) => (
<div key={team.id}>
<div className="text-xs text-lighter font-semi-bold stack horizontal xs items-center justify-center">
<div
className={
teamI === 0
? "tournament-bracket__team-one-dot"
: "tournament-bracket__team-two-dot"
}
/>
Team {teamI + 1}
</div>
<h4>{team.name}</h4>
<WinnerRadio
presentational={presentational}
checked={
result ? result.winnerTeamId === team.id : winnerId === team.id
}
teamId={team.id}
onChange={() => setWinnerId?.(team.id)}
team={teamI + 1}
/>
<TeamRosterInputsCheckboxes
team={team}
checkedPlayers={result?.participantIds ?? checkedPlayers[teamI]!}
mode={inputMode(team)}
handlePlayerClick={(playerId: number) => {
const newCheckedPlayers = () => {
const newPlayers = clone(checkedPlayers);
if (checkedPlayers.flat().includes(playerId)) {
newPlayers[teamI] = newPlayers[teamI]!.filter(
(id) => id !== playerId
);
} else {
newPlayers[teamI]!.push(playerId);
}
return newPlayers;
};
setCheckedPlayers?.(newCheckedPlayers());
}}
/>
</div>
))}
</div>
);
}
/** Renders radio button to select winner, or in presentational mode just display the text "Winner" */
function WinnerRadio({
presentational,
teamId,
checked,
onChange,
team,
}: {
presentational: boolean;
teamId: number;
checked: boolean;
onChange: () => void;
team: number;
}) {
const id = React.useId();
if (presentational) {
return (
<div
className={clsx("text-xs font-bold", {
invisible: !checked,
"text-theme": team === 1,
"text-theme-secondary": team === 2,
})}
>
Winner
</div>
);
}
return (
<div className="tournament-bracket__during-match-actions__radio-container">
<input
type="radio"
id={`${teamId}-${id}`}
onChange={onChange}
checked={checked}
/>
<Label className="mb-0 ml-2" htmlFor={`${teamId}-${id}`}>
Winner
</Label>
</div>
);
}
function TeamRosterInputsCheckboxes({
team,
checkedPlayers,
handlePlayerClick,
mode,
}: {
team: Unpacked<TournamentLoaderData["teams"]>;
checkedPlayers: number[];
handlePlayerClick: (playerId: number) => void;
/** DEFAULT = inputs work, DISABLED = inputs disabled and look disabled, PRESENTATION = inputs disabled but look like in DEFAULT (without hover styles) */
mode: TeamRosterInputsType;
}) {
const id = React.useId();
return (
<div className="tournament-bracket__during-match-actions__team-players">
{team.members.map((member) => {
return (
<div
key={member.userId}
className={clsx(
"tournament-bracket__during-match-actions__checkbox-name",
{ "disabled-opaque": mode === "DISABLED" },
{ presentational: mode === "PRESENTATIONAL" }
)}
>
<input
className="plain tournament-bracket__during-match-actions__checkbox"
type="checkbox"
id={`${member.userId}-${id}`}
name="playerName"
disabled={mode === "DISABLED" || mode === "PRESENTATIONAL"}
value={member.userId}
checked={checkedPlayers.flat().includes(member.userId)}
onChange={() => handlePlayerClick(member.userId)}
/>{" "}
<label
className="tournament-bracket__during-match-actions__player-name"
htmlFor={`${member.userId}-${id}`}
>
{member.inGameName
? inGameNameWithoutDiscriminator(member.inGameName)
: member.discordName}
</label>
</div>
);
})}
</div>
);
}