User configurable tournament max member count (#2565)

This commit is contained in:
Kalle
2025-10-11 14:03:53 +03:00
committed by GitHub
parent f3e6a6c04c
commit 98a5ccbe3a
12 changed files with 74 additions and 32 deletions

View File

@@ -456,6 +456,8 @@ export interface TournamentSettings {
roundCount: number;
};
minMembersPerTeam?: number;
/** Maximum number of team members that can be registered (only applies to 4v4 tournaments) */
maxMembersPerTeam?: number;
isTest?: boolean;
}

View File

@@ -477,6 +477,7 @@ type CreateArgs = Pick<
mapPickingStyle: Tables["Tournament"]["mapPickingStyle"];
bracketProgression: TournamentSettings["bracketProgression"] | null;
minMembersPerTeam?: number;
maxMembersPerTeam?: number;
teamsPerGroup?: number;
thirdPlaceMatch?: boolean;
requireInGameNames?: boolean;
@@ -525,6 +526,7 @@ export async function create(args: CreateArgs) {
regClosesAt: args.regClosesAt,
requireInGameNames: args.requireInGameNames,
minMembersPerTeam: args.minMembersPerTeam,
maxMembersPerTeam: args.maxMembersPerTeam,
swiss:
args.swissGroupCount && args.swissRoundCount
? {
@@ -725,6 +727,7 @@ async function updateTournamentTables(
regClosesAt: args.regClosesAt,
requireInGameNames: args.requireInGameNames,
minMembersPerTeam: args.minMembersPerTeam,
maxMembersPerTeam: args.maxMembersPerTeam,
swiss:
args.swissGroupCount && args.swissRoundCount
? {

View File

@@ -79,6 +79,7 @@ export const action: ActionFunction = async ({ request }) => {
rankedModesShort.find((mode) => mode === data.toToolsMode) ?? null,
bracketProgression: data.bracketProgression ?? null,
minMembersPerTeam: data.minMembersPerTeam ?? undefined,
maxMembersPerTeam: data.maxMembersPerTeam ?? undefined,
isRanked: data.isRanked ?? undefined,
isTest: data.isTest ?? undefined,
isInvitational: data.isInvitational ?? false,

View File

@@ -86,7 +86,14 @@ export const newCalendarEventActionSchema = z
checkboxValueToBoolean,
z.boolean().nullish(),
),
minMembersPerTeam: z.coerce.number().int().min(1).max(4).nullish(),
minMembersPerTeam: z.preprocess(
actualNumber,
z.number().int().min(1).max(4).nullish(),
),
maxMembersPerTeam: z.preprocess(
actualNumber,
z.number().int().min(4).max(10).nullish(),
),
bracketProgression: bracketProgressionSchema.nullish(),
})
.refine(

View File

@@ -1257,25 +1257,59 @@ function MapPoolValidationStatusMessage({
function MemberCountSelect() {
const baseEvent = useBaseEvent();
const id = React.useId();
const [memberCount, setMemberCount] = React.useState(
baseEvent?.tournament?.ctx.settings.minMembersPerTeam ?? 4,
);
return (
<>
<div>
<label htmlFor={id} className="w-max">
Players count
</label>
<select
id={id}
name="minMembersPerTeam"
value={memberCount}
onChange={(e) => setMemberCount(Number(e.target.value))}
className="input__extra-small"
>
{[4, 3, 2, 1].map((count) => (
<option key={count} value={count}>
{`${count}v${count}`}
</option>
))}
</select>
</div>
{memberCount === 4 ? <MaxTeamMemberCountInput /> : null}
</>
);
}
function MaxTeamMemberCountInput() {
const baseEvent = useBaseEvent();
const id = React.useId();
return (
<div>
<label htmlFor={id} className="w-max">
Players count
Max team size
</label>
<select
name="minMembersPerTeam"
<input
type="number"
id={id}
name="maxMembersPerTeam"
min={4}
max={10}
defaultValue={
baseEvent?.tournament?.ctx.settings.minMembersPerTeam ?? 4
baseEvent?.tournament?.ctx.settings.maxMembersPerTeam ?? undefined
}
className="w-max"
>
{[4, 3, 2, 1].map((count) => (
<option key={count} value={count}>
{`${count}v${count}`}
</option>
))}
</select>
className="input__extra-small"
/>
<FormMessage type="info">
Maximum number of players that can be registered per team. Doesn't apply
to tournament organizers. Default is 6.
</FormMessage>
</div>
);
}

View File

@@ -863,24 +863,15 @@ export class Tournament {
}
/** what is the max amount of members teams can add in total? This limit doesn't apply to the organizer adding members to a team. */
get maxTeamMemberCount() {
get maxMembersPerTeam() {
// special format
if (this.minMembersPerTeam !== 4) return this.minMembersPerTeam;
if (this.isLeagueSignup || this.isLeagueDivision) return 8;
// TODO: retire this hack by making it user configurable
if (this.ctx.organization?.id === 19 && this.ctx.name.includes("FLUTI")) {
return 8;
if (this.ctx.settings.maxMembersPerTeam) {
return this.ctx.settings.maxMembersPerTeam;
}
const maxMembersBeforeStart = 6;
if (this.hasStarted) {
return maxMembersBeforeStart + 1;
}
return maxMembersBeforeStart;
return 6;
}
/** Is the regular check-in (check-in for the whole tournament) open at this time? */

View File

@@ -386,7 +386,7 @@ function AddSubsPopOver() {
}
const subsAvailableToAdd =
tournament.maxTeamMemberCount - ownedTeam.members.length;
tournament.maxMembersPerTeam - ownedTeam.members.length;
const inviteLink = `${SENDOU_INK_BASE_URL}${tournamentJoinPage({
tournamentId: tournament.ctx.id,

View File

@@ -68,7 +68,7 @@ export const action: ActionFunction = async ({ request, params }) => {
inviteCode,
teamToJoin,
userId: user.id,
maxTeamSize: tournament.maxTeamMemberCount,
maxTeamSize: tournament.maxMembersPerTeam,
}) === "VALID",
"Cannot join this team or invite code is invalid",
);

View File

@@ -28,7 +28,7 @@ export default function JoinTeamPage() {
inviteCode: data.inviteCode,
teamToJoin,
userId: user?.id,
maxTeamSize: tournament.maxTeamMemberCount,
maxTeamSize: tournament.maxMembersPerTeam,
});
const textPrompt = () => {

View File

@@ -971,7 +971,7 @@ function FillRoster({
);
const optionalMembers = Math.max(
tournament.maxTeamMemberCount - ownTeamMembers.length - missingMembers,
tournament.maxMembersPerTeam - ownTeamMembers.length - missingMembers,
0,
);
@@ -992,7 +992,7 @@ function FillRoster({
});
})();
const teamIsFull = ownTeamMembers.length >= tournament.maxTeamMemberCount;
const teamIsFull = ownTeamMembers.length >= tournament.maxMembersPerTeam;
const canAddMembers = !teamIsFull && tournament.registrationOpen;
return (
@@ -1085,7 +1085,7 @@ function FillRoster({
<div className="tournament__section__warning">
{t("tournament:pre.roster.footer", {
atLeastCount: tournament.minMembersPerTeam,
maxCount: tournament.maxTeamMemberCount,
maxCount: tournament.maxMembersPerTeam,
})}
</div>
)}

View File

@@ -81,14 +81,17 @@ input:user-invalid {
.input__extra-small {
min-width: var(--input-width-extra-small);
max-width: var(--input-width-extra-small);
}
.input__small {
min-width: var(--input-width-small);
max-width: var(--input-width-small);
}
.input__medium {
min-width: var(--input-width-medium);
max-width: var(--input-width-medium);
}
input:not(.plain, .combobox-input):focus-within {

View File

@@ -93,6 +93,7 @@ async function main() {
autonomousSubs: false,
isRanked: tournament.ctx.settings.isRanked,
minMembersPerTeam: tournament.ctx.settings.minMembersPerTeam,
maxMembersPerTeam: tournament.ctx.settings.maxMembersPerTeam,
regClosesAt: tournament.ctx.settings.regClosesAt,
requireInGameNames: tournament.ctx.settings.requireInGameNames,
bracketUrl: "https://sendou.ink",