mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-05-10 04:40:46 -05:00
* 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
201 lines
6.0 KiB
TypeScript
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>
|
|
);
|
|
}
|