diff --git a/app/db/tables.ts b/app/db/tables.ts
index 8b4bf89a8..1ebd17ae9 100644
--- a/app/db/tables.ts
+++ b/app/db/tables.ts
@@ -389,6 +389,8 @@ export interface TournamentSettings {
isRanked?: boolean;
autoCheckInAll?: boolean;
enableNoScreenToggle?: boolean;
+ deadlines?: "STRICT" | "DEFAULT";
+ isInvitational?: boolean;
/** Can teams add subs on their own while tournament is in progress? */
autonomousSubs?: boolean;
/** Timestamp (SQLite format) when reg closes, if missing then means closes at start time */
diff --git a/app/features/calendar/CalendarRepository.server.ts b/app/features/calendar/CalendarRepository.server.ts
index 0c33f078a..ddd8bec97 100644
--- a/app/features/calendar/CalendarRepository.server.ts
+++ b/app/features/calendar/CalendarRepository.server.ts
@@ -408,6 +408,8 @@ type CreateArgs = Pick<
thirdPlaceMatch?: boolean;
autoCheckInAll?: boolean;
isRanked?: boolean;
+ isInvitational?: boolean;
+ deadlines: TournamentSettings["deadlines"];
enableNoScreenToggle?: boolean;
autonomousSubs?: boolean;
regClosesAt?: number;
@@ -435,6 +437,8 @@ export async function create(args: CreateArgs) {
teamsPerGroup: args.teamsPerGroup,
thirdPlaceMatch: args.thirdPlaceMatch,
isRanked: args.isRanked,
+ deadlines: args.deadlines,
+ isInvitational: args.isInvitational,
enableNoScreenToggle: args.enableNoScreenToggle,
autonomousSubs: args.autonomousSubs,
regClosesAt: args.regClosesAt,
@@ -535,6 +539,8 @@ export async function update(args: UpdateArgs) {
teamsPerGroup: args.teamsPerGroup,
thirdPlaceMatch: args.thirdPlaceMatch,
isRanked: args.isRanked,
+ deadlines: args.deadlines,
+ isInvitational: args.isInvitational,
enableNoScreenToggle: args.enableNoScreenToggle,
autonomousSubs: args.autonomousSubs,
regClosesAt: args.regClosesAt,
diff --git a/app/features/calendar/actions/calendar.new.server.ts b/app/features/calendar/actions/calendar.new.server.ts
index 8b0c40d7f..f604840ad 100644
--- a/app/features/calendar/actions/calendar.new.server.ts
+++ b/app/features/calendar/actions/calendar.new.server.ts
@@ -77,6 +77,8 @@ export const action: ActionFunction = async ({ request }) => {
teamsPerGroup: data.teamsPerGroup ?? undefined,
thirdPlaceMatch: data.thirdPlaceMatch ?? undefined,
isRanked: data.isRanked ?? undefined,
+ isInvitational: data.isInvitational ?? false,
+ deadlines: data.strictDeadline ? ("STRICT" as const) : ("DEFAULT" as const),
enableNoScreenToggle: data.enableNoScreenToggle ?? undefined,
autoCheckInAll: data.autoCheckInAll ?? undefined,
autonomousSubs: data.autonomousSubs ?? undefined,
@@ -209,6 +211,8 @@ export const newCalendarEventActionSchema = z
z.boolean().nullish(),
),
autonomousSubs: z.preprocess(checkboxValueToBoolean, z.boolean().nullish()),
+ strictDeadline: z.preprocess(checkboxValueToBoolean, z.boolean().nullish()),
+ isInvitational: z.preprocess(checkboxValueToBoolean, z.boolean().nullish()),
//
// tournament format related fields
//
diff --git a/app/features/calendar/routes/calendar.new.tsx b/app/features/calendar/routes/calendar.new.tsx
index 724b37e8e..e583892fd 100644
--- a/app/features/calendar/routes/calendar.new.tsx
+++ b/app/features/calendar/routes/calendar.new.tsx
@@ -170,6 +170,8 @@ function EventForm() {
+
+
>
) : null}
{isTournament ? : }
@@ -640,6 +642,60 @@ function AutonomousSubsToggle() {
);
}
+function InvitationalToggle() {
+ const baseEvent = useBaseEvent();
+ const [isInvitational, setIsInvitational] = React.useState(
+ baseEvent?.tournamentCtx?.settings.isInvitational ?? false,
+ );
+ const id = React.useId();
+
+ return (
+
+
+
+
+ No open registration or subs list. All teams must be added by the
+ organizer.
+
+
+ );
+}
+
+function StrictDeadlinesToggle() {
+ const baseEvent = useBaseEvent();
+ const [strictDeadlines, setStrictDeadlines] = React.useState(
+ baseEvent?.tournamentCtx?.settings.deadlines === "STRICT" ? true : false,
+ );
+ const id = React.useId();
+
+ return (
+
+
+
+
+ Strict deadlines has 5 minutes less for the target time of each round
+ (25min Bo3, 35min Bo5 compared to 30min Bo3, 40min Bo5 normal).
+
+
+ );
+}
+
function RegClosesAtSelect() {
const baseEvent = useBaseEvent();
const [regClosesAt, setRegClosesAt] = React.useState(
diff --git a/app/features/tournament-bracket/components/Bracket/useDeadline.ts b/app/features/tournament-bracket/components/Bracket/useDeadline.ts
index bba60d164..8907f8d15 100644
--- a/app/features/tournament-bracket/components/Bracket/useDeadline.ts
+++ b/app/features/tournament-bracket/components/Bracket/useDeadline.ts
@@ -13,10 +13,16 @@ const MINUTES = {
BO7: 50,
};
-const minutesToPlay = (count: number) => {
- if (count === 3) return MINUTES.BO3;
- if (count === 5) return MINUTES.BO5;
- if (count === 7) return MINUTES.BO7;
+const STRICT_MINUTES = {
+ BO3: 25,
+ BO5: 35,
+ BO7: 45,
+};
+
+const minutesToPlay = (count: number, strict: boolean) => {
+ if (count === 3) return strict ? STRICT_MINUTES.BO3 : MINUTES.BO3;
+ if (count === 5) return strict ? STRICT_MINUTES.BO5 : MINUTES.BO5;
+ if (count === 7) return strict ? STRICT_MINUTES.BO7 : MINUTES.BO7;
logger.warn("Unknown best of count", { count });
return MINUTES.BO5;
@@ -73,7 +79,10 @@ export function useDeadline(roundId: number, bestOf: number) {
if (!dl) return null;
- dl.setMinutes(dl.getMinutes() + minutesToPlay(bestOf));
+ dl.setMinutes(
+ dl.getMinutes() +
+ minutesToPlay(bestOf, tournament.ctx.settings.deadlines === "STRICT"),
+ );
return dl;
} catch (e) {
diff --git a/app/features/tournament-bracket/core/Tournament.ts b/app/features/tournament-bracket/core/Tournament.ts
index 986afe9fb..efd12eee6 100644
--- a/app/features/tournament-bracket/core/Tournament.ts
+++ b/app/features/tournament-bracket/core/Tournament.ts
@@ -9,10 +9,7 @@ import { assertUnreachable } from "~/utils/types";
import { isAdmin } from "~/permissions";
import { TOURNAMENT } from "~/features/tournament";
import type { TournamentData, TournamentDataTeam } from "./Tournament.server";
-import {
- HACKY_isInviteOnlyEvent,
- HACKY_resolvePicture,
-} from "~/features/tournament/tournament-utils";
+import { HACKY_resolvePicture } from "~/features/tournament/tournament-utils";
import { rankedModesShort } from "~/modules/in-game-lists/modes";
import type { ModeShort } from "~/modules/in-game-lists";
import {
@@ -547,10 +544,6 @@ export class Tournament {
: TOURNAMENT.COUNTERPICK_MAPS_PER_MODE;
}
- get hasOpenRegistration() {
- return !HACKY_isInviteOnlyEvent(this.ctx);
- }
-
get hasStarted() {
return this.brackets.some((bracket) => !bracket.preview);
}
@@ -717,13 +710,12 @@ export class Tournament {
return true;
}
- // TODO: get from settings
- private isInvitational() {
- return this.ctx.name.includes("Finale");
+ get isInvitational() {
+ return this.ctx.settings.isInvitational ?? false;
}
get subsFeatureEnabled() {
- return !this.isInvitational();
+ return !this.isInvitational;
}
get canAddNewSubPost() {
@@ -738,7 +730,7 @@ export class Tournament {
}
get maxTeamMemberCount() {
- const maxMembersBeforeStart = this.isInvitational()
+ const maxMembersBeforeStart = this.isInvitational
? 5
: TOURNAMENT.DEFAULT_TEAM_MAX_MEMBERS_BEFORE_START;
diff --git a/app/features/tournament/routes/to.$id.join.tsx b/app/features/tournament/routes/to.$id.join.tsx
index 865b26114..9503f3270 100644
--- a/app/features/tournament/routes/to.$id.join.tsx
+++ b/app/features/tournament/routes/to.$id.join.tsx
@@ -13,14 +13,10 @@ import { assertUnreachable } from "~/utils/types";
import { tournamentPage } from "~/utils/urls";
import { findByInviteCode } from "../queries/findTeamByInviteCode.server";
import { giveTrust } from "../queries/giveTrust.server";
-import hasTournamentStarted from "../queries/hasTournamentStarted.server";
import { joinTeam } from "../queries/joinLeaveTeam.server";
import { TOURNAMENT } from "../tournament-constants";
import { joinSchema } from "../tournament-schemas.server";
-import {
- tournamentIdFromParams,
- tournamentTeamMaxSize,
-} from "../tournament-utils";
+import { tournamentIdFromParams } from "../tournament-utils";
import { useTournamentFriendCode, useTournament } from "./to.$id";
import { FriendCodeInput } from "~/components/FriendCodeInput";
import * as UserRepository from "~/features/user-page/UserRepository.server";
@@ -60,8 +56,7 @@ export const action: ActionFunction = async ({ request, params }) => {
inviteCode,
teamToJoin,
userId: user.id,
- tournamentHasStarted: tournament.hasStarted,
- tournament: tournament.ctx,
+ maxTeamSize: tournament.maxTeamMemberCount,
}) === "VALID",
"Cannot join this team or invite code is invalid",
);
@@ -105,15 +100,13 @@ export const action: ActionFunction = async ({ request, params }) => {
throw redirect(tournamentPage(leanTeam.tournamentId));
};
-export const loader = ({ request, params }: LoaderFunctionArgs) => {
- const tournamentId = tournamentIdFromParams(params);
+export const loader = ({ request }: LoaderFunctionArgs) => {
const url = new URL(request.url);
const inviteCode = url.searchParams.get("code");
return {
teamId: inviteCode ? findByInviteCode(inviteCode)?.id : null,
inviteCode,
- tournamentHasStarted: hasTournamentStarted(tournamentId),
};
};
@@ -128,11 +121,10 @@ export default function JoinTeamPage() {
const teamToJoin = data.teamId ? tournament.teamById(data.teamId) : undefined;
const captain = teamToJoin?.members.find((member) => member.isOwner);
const validationStatus = validateCanJoin({
- tournament: tournament.ctx,
inviteCode: data.inviteCode,
teamToJoin,
userId: user?.id,
- tournamentHasStarted: data.tournamentHasStarted,
+ maxTeamSize: tournament.maxTeamMemberCount,
});
const textPrompt = () => {
@@ -195,14 +187,12 @@ function validateCanJoin({
inviteCode,
teamToJoin,
userId,
- tournamentHasStarted,
- tournament,
+ maxTeamSize,
}: {
inviteCode?: string | null;
teamToJoin?: { members: { userId: number }[] };
userId?: number;
- tournamentHasStarted: boolean;
- tournament: { name: string };
+ maxTeamSize: number;
}) {
if (typeof inviteCode !== "string") {
return "MISSING_CODE";
@@ -216,10 +206,7 @@ function validateCanJoin({
if (!teamToJoin) {
return "NO_TEAM_MATCHING_CODE";
}
- if (
- teamToJoin.members.length >=
- tournamentTeamMaxSize({ tournament, tournamentHasStarted })
- ) {
+ if (teamToJoin.members.length >= maxTeamSize) {
return "TEAM_FULL";
}
if (teamToJoin.members.some((member) => member.userId === userId)) {
diff --git a/app/features/tournament/routes/to.$id.register.tsx b/app/features/tournament/routes/to.$id.register.tsx
index ef02efa78..13df6f069 100644
--- a/app/features/tournament/routes/to.$id.register.tsx
+++ b/app/features/tournament/routes/to.$id.register.tsx
@@ -64,7 +64,6 @@ import { upsertCounterpickMaps } from "../queries/upsertCounterpickMaps.server";
import { TOURNAMENT } from "../tournament-constants";
import { registerSchema } from "../tournament-schemas.server";
import {
- HACKY_isInviteOnlyEvent,
isOneModeTournamentOf,
tournamentIdFromParams,
} from "../tournament-utils";
@@ -118,7 +117,7 @@ export const action: ActionFunction = async ({ request, params }) => {
teamId: data.teamId ?? null,
});
} else {
- validate(!HACKY_isInviteOnlyEvent(event), "Event is invite only");
+ validate(!tournament.isInvitational, "Event is invite only");
validate(
await UserRepository.currentFriendCodeByUserId(user.id),
"No friend code",
@@ -489,19 +488,19 @@ function RegistrationForms() {
const ownTeam = tournament.ownedTeamByUser(user);
const ownTeamCheckedIn = Boolean(ownTeam && ownTeam.checkIns.length > 0);
- if (!user && tournament.hasOpenRegistration) {
+ if (!user && !tournament.isInvitational) {
return ;
}
const showRegistrationProgress = () => {
if (ownTeam) return true;
- return tournament.hasOpenRegistration;
+ return !tournament.isInvitational;
};
const showRegisterNewTeam = () => {
if (ownTeam) return true;
- if (!tournament.hasOpenRegistration) return false;
+ if (tournament.isInvitational) return false;
if (!tournament.registrationOpen) return false;
return !tournament.regularCheckInHasEnded;
@@ -516,7 +515,11 @@ function RegistrationForms() {
mapPool={data?.mapPool ?? undefined}
members={ownTeam?.members}
/>
- ) : null}
+ ) : (
+
+ This tournament is invitational. Tournament organizer adds all teams.
+
+ )}
{showRegisterNewTeam() ? (
<>
diff --git a/app/features/tournament/tournament-utils.ts b/app/features/tournament/tournament-utils.ts
index 13cece2f2..ff99f7f8a 100644
--- a/app/features/tournament/tournament-utils.ts
+++ b/app/features/tournament/tournament-utils.ts
@@ -5,7 +5,6 @@ import type { ModeShort } from "~/modules/in-game-lists";
import { rankedModesShort } from "~/modules/in-game-lists/modes";
import { tournamentLogoUrl } from "~/utils/urls";
import type { PlayedSet } from "./core/sets.server";
-import { TOURNAMENT } from "./tournament-constants";
export function tournamentIdFromParams(params: Params) {
const result = Number(params["id"]);
@@ -54,7 +53,7 @@ export function isOneModeTournamentOf(
export function HACKY_resolvePicture(event: { name: string }) {
const normalizedEventName = event.name.toLowerCase();
- if (HACKY_isInviteOnlyEvent(event)) {
+ if (normalizedEventName.includes("sendouq")) {
return tournamentLogoUrl("sf");
}
@@ -166,7 +165,7 @@ const WHITE = "#fffcfc";
export function HACKY_resolveThemeColors(event: { name: string }) {
const normalizedEventName = event.name.toLowerCase();
- if (HACKY_isInviteOnlyEvent(event)) {
+ if (normalizedEventName.includes("sendouq")) {
return { bg: "#1e1e1e", text: WHITE };
}
@@ -273,19 +272,6 @@ export function HACKY_resolveThemeColors(event: { name: string }) {
return { bg: "#3430ad", text: WHITE };
}
-const HACKY_isSendouQSeasonFinale = (event: { name: string }) =>
- event.name.includes("Finale");
-
-export function HACKY_isInviteOnlyEvent(event: { name: string }) {
- return HACKY_isSendouQSeasonFinale(event);
-}
-
-export function HACKY_maxRosterSizeBeforeStart(event: { name: string }) {
- if (HACKY_isSendouQSeasonFinale(event)) return 5;
-
- return TOURNAMENT.DEFAULT_TEAM_MAX_MEMBERS_BEFORE_START;
-}
-
export function tournamentRoundI18nKey(round: PlayedSet["round"]) {
if (round.round === "grand_finals") return `bracket.grand_finals`;
if (round.round === "bracket_reset") {
@@ -295,16 +281,3 @@ export function tournamentRoundI18nKey(round: PlayedSet["round"]) {
return `bracket.${round.type}` as const;
}
-
-export function tournamentTeamMaxSize({
- tournament,
- tournamentHasStarted,
-}: {
- tournament: { name: string };
- tournamentHasStarted: boolean;
-}) {
- // ensuring every team can add at least one sub while the tournament is ongoing
- return (
- HACKY_maxRosterSizeBeforeStart(tournament) + Number(tournamentHasStarted)
- );
-}