mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-08-25 12:35:18 -05:00
Tournament require IGNs option (#1760)
* Initial * Edit IGN from profile * Admin action * Show IGN * Toggle visibility
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { useFetcher } from "@remix-run/react";
|
||||
import clsx from "clsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Input } from "~/components/Input";
|
||||
import { Label } from "~/components/Label";
|
||||
@@ -12,9 +13,15 @@ export function FriendCodeInput({ friendCode }: { friendCode?: string }) {
|
||||
|
||||
return (
|
||||
<fetcher.Form method="post" action={SENDOUQ_PAGE}>
|
||||
<div className="stack sm horizontal items-end">
|
||||
<div
|
||||
className={clsx("stack sm horizontal items-end", {
|
||||
"justify-center": friendCode,
|
||||
})}
|
||||
>
|
||||
<div>
|
||||
<Label htmlFor="friendCode">{t("common:fc.title")}</Label>
|
||||
{!friendCode ? (
|
||||
<Label htmlFor="friendCode">{t("common:fc.title")}</Label>
|
||||
) : null}
|
||||
{friendCode ? (
|
||||
<div className="font-bold">SW-{friendCode}</div>
|
||||
) : (
|
||||
|
||||
@@ -428,6 +428,7 @@ export interface TournamentSettings {
|
||||
autoCheckInAll?: boolean;
|
||||
enableNoScreenToggle?: boolean;
|
||||
deadlines?: "STRICT" | "DEFAULT";
|
||||
requireInGameNames?: boolean;
|
||||
isInvitational?: boolean;
|
||||
/** Can teams add subs on their own while tournament is in progress? */
|
||||
autonomousSubs?: boolean;
|
||||
@@ -616,6 +617,7 @@ export interface TournamentTeamCheckIn {
|
||||
export interface TournamentTeamMember {
|
||||
createdAt: Generated<number>;
|
||||
isOwner: Generated<number>;
|
||||
inGameName: string | null;
|
||||
tournamentTeamId: number;
|
||||
userId: number;
|
||||
}
|
||||
|
||||
@@ -421,6 +421,7 @@ type CreateArgs = Pick<
|
||||
teamsPerGroup?: number;
|
||||
thirdPlaceMatch?: boolean;
|
||||
autoCheckInAll?: boolean;
|
||||
requireInGameNames?: boolean;
|
||||
isRanked?: boolean;
|
||||
isInvitational?: boolean;
|
||||
deadlines: TournamentSettings["deadlines"];
|
||||
@@ -461,6 +462,7 @@ export async function create(args: CreateArgs) {
|
||||
autonomousSubs: args.autonomousSubs,
|
||||
regClosesAt: args.regClosesAt,
|
||||
autoCheckInAll: args.autoCheckInAll,
|
||||
requireInGameNames: args.requireInGameNames,
|
||||
swiss:
|
||||
args.swissGroupCount && args.swissRoundCount
|
||||
? {
|
||||
@@ -613,6 +615,7 @@ export async function update(args: UpdateArgs) {
|
||||
autonomousSubs: args.autonomousSubs,
|
||||
regClosesAt: args.regClosesAt,
|
||||
autoCheckInAll: args.autoCheckInAll,
|
||||
requireInGameNames: args.requireInGameNames,
|
||||
swiss:
|
||||
args.swissGroupCount && args.swissRoundCount
|
||||
? {
|
||||
|
||||
@@ -99,6 +99,7 @@ export const action: ActionFunction = async ({ request }) => {
|
||||
isInvitational: data.isInvitational ?? false,
|
||||
deadlines: data.strictDeadline ? ("STRICT" as const) : ("DEFAULT" as const),
|
||||
enableNoScreenToggle: data.enableNoScreenToggle ?? undefined,
|
||||
requireInGameNames: data.requireInGameNames ?? undefined,
|
||||
autoCheckInAll: data.autoCheckInAll ?? undefined,
|
||||
autonomousSubs: data.autonomousSubs ?? undefined,
|
||||
swissGroupCount: data.swissGroupCount ?? undefined,
|
||||
@@ -266,6 +267,10 @@ export const newCalendarEventActionSchema = z
|
||||
autonomousSubs: z.preprocess(checkboxValueToBoolean, z.boolean().nullish()),
|
||||
strictDeadline: z.preprocess(checkboxValueToBoolean, z.boolean().nullish()),
|
||||
isInvitational: z.preprocess(checkboxValueToBoolean, z.boolean().nullish()),
|
||||
requireInGameNames: z.preprocess(
|
||||
checkboxValueToBoolean,
|
||||
z.boolean().nullish(),
|
||||
),
|
||||
//
|
||||
// tournament format related fields
|
||||
//
|
||||
|
||||
@@ -203,6 +203,7 @@ function EventForm() {
|
||||
<RankedToggle />
|
||||
<EnableNoScreenToggle />
|
||||
<AutonomousSubsToggle />
|
||||
<RequireIGNToggle />
|
||||
<InvitationalToggle />
|
||||
<StrictDeadlinesToggle />
|
||||
</>
|
||||
@@ -863,6 +864,34 @@ function AutonomousSubsToggle() {
|
||||
);
|
||||
}
|
||||
|
||||
function RequireIGNToggle() {
|
||||
const baseEvent = useBaseEvent();
|
||||
const [requireIGNs, setRequireIGNs] = React.useState(
|
||||
baseEvent?.tournamentCtx?.settings.requireInGameNames ?? false,
|
||||
);
|
||||
const id = React.useId();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<label htmlFor={id} className="w-max">
|
||||
Require in-game names
|
||||
</label>
|
||||
<Toggle
|
||||
name="requireInGameNames"
|
||||
id={id}
|
||||
tiny
|
||||
checked={requireIGNs}
|
||||
setChecked={setRequireIGNs}
|
||||
/>
|
||||
<FormMessage type="info">
|
||||
If enabled players can't join the tournament without an in-game
|
||||
name (e.g. Sendou#1234). Players can't change the IGNs after the
|
||||
registration closes.
|
||||
</FormMessage>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function InvitationalToggle() {
|
||||
const baseEvent = useBaseEvent();
|
||||
const [isInvitational, setIsInvitational] = React.useState(
|
||||
@@ -1194,7 +1223,7 @@ function TournamentFormatSelector() {
|
||||
? baseEvent.tournamentCtx.settings.bracketProgression.some(
|
||||
(b) => b.name === BRACKET_NAMES.UNDERGROUND,
|
||||
)
|
||||
: true,
|
||||
: false,
|
||||
);
|
||||
const [thirdPlaceMatch, setThirdPlaceMatch] = React.useState(
|
||||
baseEvent?.tournamentCtx?.settings.thirdPlaceMatch ?? true,
|
||||
|
||||
@@ -282,7 +282,7 @@ export async function usersThatTrusted(userId: number) {
|
||||
.selectFrom("TeamMember")
|
||||
.innerJoin("User", "User.id", "TeamMember.userId")
|
||||
.innerJoin("UserFriendCode", "UserFriendCode.userId", "User.id")
|
||||
.select(COMMON_USER_FIELDS)
|
||||
.select([...COMMON_USER_FIELDS, "User.inGameName"])
|
||||
.where((eb) =>
|
||||
eb(
|
||||
"TeamMember.teamId",
|
||||
@@ -298,7 +298,7 @@ export async function usersThatTrusted(userId: number) {
|
||||
.selectFrom("TrustRelationship")
|
||||
.innerJoin("User", "User.id", "TrustRelationship.trustGiverUserId")
|
||||
.innerJoin("UserFriendCode", "UserFriendCode.userId", "User.id")
|
||||
.select(COMMON_USER_FIELDS)
|
||||
.select([...COMMON_USER_FIELDS, "User.inGameName"])
|
||||
.where("TrustRelationship.trustReceiverUserId", "=", userId),
|
||||
)
|
||||
.orderBy("User.username asc")
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { add } from "date-fns";
|
||||
import type { Insertable, NotNull, Transaction } from "kysely";
|
||||
import { sql, type Insertable, type NotNull, type Transaction } from "kysely";
|
||||
import { jsonArrayFrom, jsonObjectFrom } from "kysely/helpers/sqlite";
|
||||
import { nanoid } from "nanoid";
|
||||
import { db } from "~/db/sql";
|
||||
@@ -98,11 +98,14 @@ export async function findById(id: number) {
|
||||
"User.discordId",
|
||||
"User.discordAvatar",
|
||||
"User.customUrl",
|
||||
"User.inGameName",
|
||||
"User.country",
|
||||
"PlusTier.tier as plusTier",
|
||||
"TournamentTeamMember.isOwner",
|
||||
"TournamentTeamMember.createdAt",
|
||||
sql<string | null>/*sql*/ `coalesce(
|
||||
"TournamentTeamMember"."inGameName",
|
||||
"User"."inGameName"
|
||||
)`.as("inGameName"),
|
||||
])
|
||||
.whereRef(
|
||||
"TournamentTeamMember.tournamentTeamId",
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// TODO: add rest of the functions here that relate more to tournament teams than tournament/bracket
|
||||
|
||||
import { sql } from "kysely";
|
||||
import { db } from "~/db/sql";
|
||||
import { databaseTimestampNow } from "~/utils/dates";
|
||||
|
||||
export function setActiveRoster({
|
||||
teamId,
|
||||
@@ -15,3 +17,71 @@ export function setActiveRoster({
|
||||
.where("TournamentTeam.id", "=", teamId)
|
||||
.execute();
|
||||
}
|
||||
|
||||
const regOpenTournamentTeamIdsByJoinedUserId = (userId: number) =>
|
||||
db
|
||||
.selectFrom("TournamentTeamMember")
|
||||
.innerJoin(
|
||||
"TournamentTeam",
|
||||
"TournamentTeam.id",
|
||||
"TournamentTeamMember.tournamentTeamId",
|
||||
)
|
||||
.innerJoin("Tournament", "Tournament.id", "TournamentTeam.tournamentId")
|
||||
.innerJoin("CalendarEvent", "CalendarEvent.tournamentId", "Tournament.id")
|
||||
.innerJoin(
|
||||
"CalendarEventDate",
|
||||
"CalendarEventDate.eventId",
|
||||
"CalendarEvent.id",
|
||||
)
|
||||
.select("TournamentTeamMember.tournamentTeamId")
|
||||
.where("TournamentTeamMember.userId", "=", userId)
|
||||
.where(
|
||||
sql`coalesce(
|
||||
"Tournament"."settings" ->> 'regClosesAt',
|
||||
"CalendarEventDate"."startTime"
|
||||
)`,
|
||||
">",
|
||||
databaseTimestampNow(),
|
||||
)
|
||||
.execute()
|
||||
.then((rows) => rows.map((row) => row.tournamentTeamId));
|
||||
|
||||
export async function updateMemberInGameName({
|
||||
userId,
|
||||
inGameName,
|
||||
tournamentTeamId,
|
||||
}: {
|
||||
userId: number;
|
||||
inGameName: string;
|
||||
tournamentTeamId: number;
|
||||
}) {
|
||||
return db
|
||||
.updateTable("TournamentTeamMember")
|
||||
.set({ inGameName })
|
||||
.where("TournamentTeamMember.userId", "=", userId)
|
||||
.where("TournamentTeamMember.tournamentTeamId", "=", tournamentTeamId)
|
||||
.execute();
|
||||
}
|
||||
|
||||
export async function updateMemberInGameNameForNonStarted({
|
||||
userId,
|
||||
inGameName,
|
||||
}: {
|
||||
userId: number;
|
||||
inGameName: string;
|
||||
}) {
|
||||
const tournamentTeamIds =
|
||||
await regOpenTournamentTeamIdsByJoinedUserId(userId);
|
||||
|
||||
return (
|
||||
db
|
||||
.updateTable("TournamentTeamMember")
|
||||
.set({ inGameName })
|
||||
.where("TournamentTeamMember.userId", "=", userId)
|
||||
// after they have checked in no longer can update their IGN from here
|
||||
.where("TournamentTeamMember.tournamentTeamId", "in", tournamentTeamIds)
|
||||
// if the tournament doesn't have the setting to require IGN, ignore
|
||||
.where("TournamentTeamMember.inGameName", "is not", null)
|
||||
.execute()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -59,6 +59,14 @@ export function TeamWithRoster({
|
||||
databaseTimestampToDate(member.createdAt) >
|
||||
tournament.ctx.startTime;
|
||||
|
||||
const name = () => {
|
||||
if (!tournament.ctx.settings.requireInGameNames) {
|
||||
return member.username;
|
||||
}
|
||||
|
||||
return member.inGameName ?? member.username;
|
||||
};
|
||||
|
||||
return (
|
||||
<li key={member.userId} className="tournament__team-member-row">
|
||||
{member.isOwner ? (
|
||||
@@ -89,7 +97,7 @@ export function TeamWithRoster({
|
||||
to={userPage(member)}
|
||||
className="tournament__team-member-name"
|
||||
>
|
||||
{member.username}{" "}
|
||||
{name()}
|
||||
</Link>
|
||||
</div>
|
||||
{friendCode ? (
|
||||
|
||||
@@ -25,10 +25,12 @@ const createMemberStm = sql.prepare(/*sql*/ `
|
||||
insert into "TournamentTeamMember" (
|
||||
"tournamentTeamId",
|
||||
"userId",
|
||||
"inGameName",
|
||||
"isOwner"
|
||||
) values (
|
||||
@tournamentTeamId,
|
||||
@userId,
|
||||
@inGameName,
|
||||
1
|
||||
)
|
||||
`);
|
||||
@@ -38,6 +40,7 @@ export const createTeam = sql.transaction(
|
||||
tournamentId,
|
||||
name,
|
||||
ownerId,
|
||||
ownerInGameName,
|
||||
prefersNotToHost,
|
||||
noScreen,
|
||||
teamId,
|
||||
@@ -45,6 +48,7 @@ export const createTeam = sql.transaction(
|
||||
tournamentId: TournamentTeam["tournamentId"];
|
||||
name: TournamentTeam["name"];
|
||||
ownerId: User["id"];
|
||||
ownerInGameName: string | null;
|
||||
prefersNotToHost: TournamentTeam["prefersNotToHost"];
|
||||
noScreen: number;
|
||||
teamId: number | null;
|
||||
@@ -58,6 +62,10 @@ export const createTeam = sql.transaction(
|
||||
teamId,
|
||||
}) as TournamentTeam;
|
||||
|
||||
createMemberStm.run({ tournamentTeamId: team.id, userId: ownerId });
|
||||
createMemberStm.run({
|
||||
tournamentTeamId: team.id,
|
||||
inGameName: ownerInGameName,
|
||||
userId: ownerId,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
@@ -6,9 +6,11 @@ import { deleteSub } from "~/features/tournament-subs";
|
||||
const createTeamMemberStm = sql.prepare(/*sql*/ `
|
||||
insert into "TournamentTeamMember" (
|
||||
"tournamentTeamId",
|
||||
"inGameName",
|
||||
"userId"
|
||||
) values (
|
||||
@tournamentTeamId,
|
||||
@inGameName,
|
||||
@userId
|
||||
)
|
||||
`);
|
||||
@@ -31,6 +33,7 @@ export const joinTeam = sql.transaction(
|
||||
whatToDoWithPreviousTeam,
|
||||
newTeamId,
|
||||
userId,
|
||||
inGameName,
|
||||
tournamentId,
|
||||
checkOutTeam = false,
|
||||
}: {
|
||||
@@ -38,6 +41,7 @@ export const joinTeam = sql.transaction(
|
||||
whatToDoWithPreviousTeam?: "LEAVE" | "DELETE";
|
||||
newTeamId: number;
|
||||
userId: number;
|
||||
inGameName: string | null;
|
||||
tournamentId: number;
|
||||
checkOutTeam?: boolean;
|
||||
}) => {
|
||||
@@ -59,7 +63,11 @@ export const joinTeam = sql.transaction(
|
||||
checkOut(previousTeamId);
|
||||
}
|
||||
|
||||
createTeamMemberStm.run({ tournamentTeamId: newTeamId, userId });
|
||||
createTeamMemberStm.run({
|
||||
tournamentTeamId: newTeamId,
|
||||
userId,
|
||||
inGameName,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -20,7 +20,11 @@ import type { TournamentData } from "~/features/tournament-bracket/core/Tourname
|
||||
import { tournamentFromDB } from "~/features/tournament-bracket/core/Tournament.server";
|
||||
import { isAdmin } from "~/permissions";
|
||||
import { databaseTimestampToDate } from "~/utils/dates";
|
||||
import { parseRequestFormData, validate } from "~/utils/remix";
|
||||
import {
|
||||
badRequestIfFalsy,
|
||||
parseRequestFormData,
|
||||
validate,
|
||||
} from "~/utils/remix";
|
||||
import { assertUnreachable } from "~/utils/types";
|
||||
import {
|
||||
calendarEditPage,
|
||||
@@ -40,6 +44,9 @@ import { findMapPoolByTeamId } from "~/features/tournament-bracket/queries/findM
|
||||
import { Input } from "~/components/Input";
|
||||
import { logger } from "~/utils/logger";
|
||||
import { userIsBanned } from "~/features/ban/core/banned.server";
|
||||
import { inGameNameIfNeeded } from "../tournament-utils.server";
|
||||
import * as TournamentTeamRepository from "~/features/tournament/TournamentTeamRepository.server";
|
||||
import { USER } from "~/constants";
|
||||
|
||||
export const action: ActionFunction = async ({ request, params }) => {
|
||||
const user = await requireUserId(request);
|
||||
@@ -75,6 +82,10 @@ export const action: ActionFunction = async ({ request, params }) => {
|
||||
ownerId: data.userId,
|
||||
prefersNotToHost: 0,
|
||||
noScreen: 0,
|
||||
ownerInGameName: await inGameNameIfNeeded({
|
||||
tournament,
|
||||
userId: data.userId,
|
||||
}),
|
||||
});
|
||||
|
||||
break;
|
||||
@@ -224,6 +235,10 @@ export const action: ActionFunction = async ({ request, params }) => {
|
||||
// this team is not checked in so we can simply delete it
|
||||
whatToDoWithPreviousTeam: previousTeam ? "DELETE" : undefined,
|
||||
tournamentId,
|
||||
inGameName: await inGameNameIfNeeded({
|
||||
tournament,
|
||||
userId: data.userId,
|
||||
}),
|
||||
});
|
||||
break;
|
||||
}
|
||||
@@ -302,6 +317,20 @@ export const action: ActionFunction = async ({ request, params }) => {
|
||||
|
||||
break;
|
||||
}
|
||||
case "UPDATE_IN_GAME_NAME": {
|
||||
validateIsTournamentOrganizer();
|
||||
|
||||
const teamMemberOf = badRequestIfFalsy(
|
||||
tournament.teamMemberOfByUser({ id: data.memberId }),
|
||||
);
|
||||
|
||||
await TournamentTeamRepository.updateMemberInGameName({
|
||||
userId: data.memberId,
|
||||
inGameName: `${data.inGameNameText}#${data.inGameNameDiscriminator}`,
|
||||
tournamentTeamId: teamMemberOf.id,
|
||||
});
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
assertUnreachable(data);
|
||||
}
|
||||
@@ -375,7 +404,8 @@ type Input =
|
||||
| "REGISTERED_TEAM"
|
||||
| "USER"
|
||||
| "ROSTER_MEMBER"
|
||||
| "BRACKET";
|
||||
| "BRACKET"
|
||||
| "IN_GAME_NAME";
|
||||
const actions = [
|
||||
{
|
||||
type: "ADD_TEAM",
|
||||
@@ -427,6 +457,11 @@ const actions = [
|
||||
inputs: ["REGISTERED_TEAM"] as Input[],
|
||||
when: ["TOURNAMENT_AFTER_START", "IS_SWISS"],
|
||||
},
|
||||
{
|
||||
type: "UPDATE_IN_GAME_NAME",
|
||||
inputs: ["ROSTER_MEMBER", "REGISTERED_TEAM", "IN_GAME_NAME"] as Input[],
|
||||
when: ["IN_GAME_NAME_REQUIRED"],
|
||||
},
|
||||
] as const;
|
||||
|
||||
function TeamActions() {
|
||||
@@ -461,12 +496,14 @@ function TeamActions() {
|
||||
if (tournament.hasStarted) {
|
||||
return false;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case "TOURNAMENT_AFTER_START": {
|
||||
if (!tournament.hasStarted) {
|
||||
return false;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case "IS_SWISS": {
|
||||
@@ -476,6 +513,13 @@ function TeamActions() {
|
||||
|
||||
break;
|
||||
}
|
||||
case "IN_GAME_NAME_REQUIRED": {
|
||||
if (!tournament.ctx.settings.requireInGameNames) {
|
||||
return false;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
assertUnreachable(when);
|
||||
}
|
||||
@@ -563,6 +607,25 @@ function TeamActions() {
|
||||
</select>
|
||||
</div>
|
||||
) : null}
|
||||
{selectedTeam && selectedAction.inputs.includes("IN_GAME_NAME") ? (
|
||||
<div className="stack items-start">
|
||||
<Label>New IGN</Label>
|
||||
<div className="stack horizontal sm items-center">
|
||||
<Input
|
||||
name="inGameNameText"
|
||||
aria-label="In game name"
|
||||
maxLength={USER.IN_GAME_NAME_TEXT_MAX_LENGTH}
|
||||
/>
|
||||
<div className="u-edit__in-game-name-hashtag">#</div>
|
||||
<Input
|
||||
name="inGameNameDiscriminator"
|
||||
aria-label="In game name discriminator"
|
||||
maxLength={USER.IN_GAME_NAME_DISCRIMINATOR_MAX_LENGTH}
|
||||
pattern="[0-9a-z]{4,5}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
<SubmitButton
|
||||
_action={selectedAction.type}
|
||||
state={fetcher.state}
|
||||
|
||||
@@ -10,7 +10,7 @@ import { useUser } from "~/features/auth/core/user";
|
||||
import { requireUserId } from "~/features/auth/core/user.server";
|
||||
import { notFoundIfFalsy, parseRequestFormData, validate } from "~/utils/remix";
|
||||
import { assertUnreachable } from "~/utils/types";
|
||||
import { tournamentPage } from "~/utils/urls";
|
||||
import { tournamentPage, userEditProfilePage } from "~/utils/urls";
|
||||
import { findByInviteCode } from "../queries/findTeamByInviteCode.server";
|
||||
import { giveTrust } from "../queries/giveTrust.server";
|
||||
import { joinTeam } from "../queries/joinLeaveTeam.server";
|
||||
@@ -21,6 +21,9 @@ import { useTournamentFriendCode, useTournament } from "./to.$id";
|
||||
import { FriendCodeInput } from "~/components/FriendCodeInput";
|
||||
import * as UserRepository from "~/features/user-page/UserRepository.server";
|
||||
import { tournamentFromDB } from "~/features/tournament-bracket/core/Tournament.server";
|
||||
import { Alert } from "~/components/Alert";
|
||||
import { LinkButton } from "~/components/Button";
|
||||
import { inGameNameIfNeeded } from "../tournament-utils.server";
|
||||
|
||||
export const action: ActionFunction = async ({ request, params }) => {
|
||||
const tournamentId = tournamentIdFromParams(params);
|
||||
@@ -85,6 +88,10 @@ export const action: ActionFunction = async ({ request, params }) => {
|
||||
previousTeam.members.length <= TOURNAMENT.TEAM_MIN_MEMBERS_FOR_FULL,
|
||||
whatToDoWithPreviousTeam,
|
||||
tournamentId,
|
||||
inGameName: await inGameNameIfNeeded({
|
||||
tournament,
|
||||
userId: user.id,
|
||||
}),
|
||||
});
|
||||
if (data.trust) {
|
||||
const inviterUserId = teamToJoin.members.find(
|
||||
@@ -150,12 +157,32 @@ export default function JoinTeamPage() {
|
||||
}
|
||||
};
|
||||
|
||||
if (tournament.ctx.settings.requireInGameNames && user && !user.inGameName) {
|
||||
return (
|
||||
<Alert variation="WARNING" alertClassName="w-max">
|
||||
<div className="stack horizontal sm items-center flex-wrap justify-center text-center">
|
||||
This tournament requires you to have an in-game name set{" "}
|
||||
<LinkButton to={userEditProfilePage(user)} size="tiny">
|
||||
Edit profile
|
||||
</LinkButton>
|
||||
</div>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="stack lg items-center">
|
||||
<div className="text-center text-lg font-semi-bold">{textPrompt()}</div>
|
||||
{validationStatus === "VALID" ? (
|
||||
<FriendCodeInput friendCode={friendCode} />
|
||||
) : null}
|
||||
<div className="stack sm items-center">
|
||||
{validationStatus === "VALID" ? (
|
||||
<FriendCodeInput friendCode={friendCode} />
|
||||
) : null}
|
||||
{user?.inGameName ? (
|
||||
<div className="font-bold">
|
||||
<span className="text-lighter">IGN</span> {user.inGameName}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<Form method="post" className="tournament__invite-container">
|
||||
{validationStatus === "VALID" ? (
|
||||
<div className="stack md items-center">
|
||||
|
||||
@@ -51,6 +51,7 @@ import {
|
||||
readonlyMapsPage,
|
||||
tournamentJoinPage,
|
||||
tournamentSubsPage,
|
||||
userEditProfilePage,
|
||||
userPage,
|
||||
} from "~/utils/urls";
|
||||
import { checkIn } from "../queries/checkIn.server";
|
||||
@@ -79,6 +80,7 @@ import { useSearchParamState } from "~/hooks/useSearchParamState";
|
||||
import * as TeamRepository from "~/features/team/TeamRepository.server";
|
||||
import { Toggle } from "~/components/Toggle";
|
||||
import { DiscordIcon } from "~/components/icons/Discord";
|
||||
import { inGameNameIfNeeded } from "../tournament-utils.server";
|
||||
|
||||
export const action: ActionFunction = async ({ request, params }) => {
|
||||
const user = await requireUser(request);
|
||||
@@ -135,6 +137,10 @@ export const action: ActionFunction = async ({ request, params }) => {
|
||||
ownerId: user.id,
|
||||
prefersNotToHost: booleanToInt(data.prefersNotToHost),
|
||||
noScreen: booleanToInt(data.noScreen),
|
||||
ownerInGameName: await inGameNameIfNeeded({
|
||||
tournament,
|
||||
userId: user.id,
|
||||
}),
|
||||
teamId: data.teamId ?? null,
|
||||
});
|
||||
}
|
||||
@@ -239,6 +245,10 @@ export const action: ActionFunction = async ({ request, params }) => {
|
||||
userId: data.userId,
|
||||
newTeamId: ownTeam.id,
|
||||
tournamentId,
|
||||
inGameName: await inGameNameIfNeeded({
|
||||
tournament,
|
||||
userId: data.userId,
|
||||
}),
|
||||
});
|
||||
break;
|
||||
}
|
||||
@@ -377,6 +387,12 @@ function TournamentRegisterInfoTabs() {
|
||||
revive: Number,
|
||||
});
|
||||
|
||||
const showAddIGNAlert =
|
||||
tournament.ctx.settings.requireInGameNames &&
|
||||
!teamOwned &&
|
||||
user &&
|
||||
!user?.inGameName;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<NewTabs
|
||||
@@ -461,12 +477,24 @@ function TournamentRegisterInfoTabs() {
|
||||
</FormWithConfirm>
|
||||
) : null}
|
||||
</div>
|
||||
) : showAddIGNAlert ? (
|
||||
<div>
|
||||
<Alert variation="WARNING">
|
||||
<div className="stack horizontal sm items-center flex-wrap justify-center text-center">
|
||||
This tournament requires you to have an in-game name set{" "}
|
||||
<LinkButton to={userEditProfilePage(user)} size="tiny">
|
||||
Edit profile
|
||||
</LinkButton>
|
||||
</div>
|
||||
</Alert>
|
||||
</div>
|
||||
) : (
|
||||
<RegistrationForms />
|
||||
)}
|
||||
{user &&
|
||||
!tournament.teamMemberOfByUser(user) &&
|
||||
tournament.canAddNewSubPost &&
|
||||
!showAddIGNAlert &&
|
||||
!tournament.hasStarted ? (
|
||||
<Link
|
||||
to={tournamentSubsPage(tournament.ctx.id)}
|
||||
@@ -813,7 +841,7 @@ function TeamInfo({
|
||||
<input type="hidden" name="teamId" value={data.team.id} />
|
||||
) : null}
|
||||
<div className="stack sm items-center">
|
||||
{data?.team ? (
|
||||
{data?.team && tournament.registrationOpen ? (
|
||||
<div className="tournament__section__input-container">
|
||||
<Label htmlFor="signUpAsTeam">
|
||||
Sign up as {data.team.name}
|
||||
@@ -891,6 +919,11 @@ function FriendCode() {
|
||||
<FriendCodeInput friendCode={friendCode} />
|
||||
</div>
|
||||
</section>
|
||||
{friendCode ? (
|
||||
<div className="tournament__section__warning">
|
||||
Is the friend code above wrong? Contact Sendou directly to change it.
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -933,9 +966,14 @@ function FillRoster({
|
||||
|
||||
const playersAvailableToDirectlyAdd = (() => {
|
||||
return (data!.trusterPlayers ?? []).filter((user) => {
|
||||
return tournament.ctx.teams.every((team) =>
|
||||
const isNotInTeam = tournament.ctx.teams.every((team) =>
|
||||
team.members.every((member) => member.userId !== user.id),
|
||||
);
|
||||
|
||||
const hasInGameNameIfNeeded =
|
||||
!tournament.ctx.settings.requireInGameNames || user.inGameName;
|
||||
|
||||
return isNotInTeam && hasInGameNameIfNeeded;
|
||||
});
|
||||
})();
|
||||
|
||||
@@ -979,7 +1017,20 @@ function FillRoster({
|
||||
data-testid={`member-num-${i + 1}`}
|
||||
>
|
||||
<Avatar size="xsm" user={member} />
|
||||
{member.username}
|
||||
{tournament.ctx.settings.requireInGameNames ? (
|
||||
<div>
|
||||
<div className="text-center">
|
||||
{member.inGameName ?? member.username}
|
||||
</div>
|
||||
{member.inGameName ? (
|
||||
<div className="text-lighter text-xs font-bold text-center">
|
||||
{member.username}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : (
|
||||
member.username
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
@@ -1005,12 +1056,20 @@ function FillRoster({
|
||||
<DeleteMember members={ownTeamMembers} />
|
||||
) : null}
|
||||
</section>
|
||||
<div className="tournament__section__warning">
|
||||
{t("tournament:pre.roster.footer", {
|
||||
atLeastCount: TOURNAMENT.TEAM_MIN_MEMBERS_FOR_FULL,
|
||||
maxCount: tournament.maxTeamMemberCount,
|
||||
})}
|
||||
</div>
|
||||
{tournament.ctx.settings.requireInGameNames ? (
|
||||
<div className="tournament__section__warning text-warning-important">
|
||||
Note that you are expected to use the in-game names as listed above.
|
||||
Playing in the event with a different name or using the alias feature
|
||||
might result in disqualification.
|
||||
</div>
|
||||
) : (
|
||||
<div className="tournament__section__warning">
|
||||
{t("tournament:pre.roster.footer", {
|
||||
atLeastCount: TOURNAMENT.TEAM_MIN_MEMBERS_FOR_FULL,
|
||||
maxCount: tournament.maxTeamMemberCount,
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@ export const meta: MetaFunction = (args) => {
|
||||
const title = makeTitle(data.tournament.ctx.name);
|
||||
|
||||
return [
|
||||
{ title },
|
||||
{
|
||||
property: "og:title",
|
||||
content: title,
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
} from "~/utils/zod";
|
||||
import { TOURNAMENT } from "./tournament-constants";
|
||||
import { bracketIdx } from "../tournament-bracket/tournament-bracket-schemas.server";
|
||||
import { USER } from "~/constants";
|
||||
|
||||
const teamName = z.string().trim().min(1).max(TOURNAMENT.TEAM_NAME_MAX_LENGTH);
|
||||
|
||||
@@ -129,6 +130,14 @@ export const adminActionSchema = z.union([
|
||||
_action: _action("RESET_BRACKET"),
|
||||
stageId: id,
|
||||
}),
|
||||
z.object({
|
||||
_action: _action("UPDATE_IN_GAME_NAME"),
|
||||
inGameNameText: z.string().max(USER.IN_GAME_NAME_TEXT_MAX_LENGTH),
|
||||
inGameNameDiscriminator: z
|
||||
.string()
|
||||
.refine((val) => /^[0-9a-z]{4,5}$/.test(val)),
|
||||
memberId: id,
|
||||
}),
|
||||
]);
|
||||
|
||||
export const joinSchema = z.object({
|
||||
|
||||
19
app/features/tournament/tournament-utils.server.ts
Normal file
19
app/features/tournament/tournament-utils.server.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { validate } from "~/utils/remix";
|
||||
import * as UserRepository from "~/features/user-page/UserRepository.server";
|
||||
import type { Tournament } from "../tournament-bracket/core/Tournament";
|
||||
|
||||
export const inGameNameIfNeeded = async ({
|
||||
tournament,
|
||||
userId,
|
||||
}: {
|
||||
tournament: Tournament;
|
||||
userId: number;
|
||||
}) => {
|
||||
if (!tournament.ctx.settings.requireInGameNames) return null;
|
||||
|
||||
const inGameName = await UserRepository.inGameNameByUserId(userId);
|
||||
|
||||
validate(inGameName, "No in-game name");
|
||||
|
||||
return inGameName;
|
||||
};
|
||||
@@ -186,7 +186,7 @@
|
||||
color: var(--text);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 100px;
|
||||
max-width: 150px;
|
||||
}
|
||||
|
||||
.tournament__team-member-name__role {
|
||||
|
||||
@@ -100,6 +100,7 @@ export function findLeanById(id: number) {
|
||||
"User.patronTier",
|
||||
"User.favoriteBadgeId",
|
||||
"User.languages",
|
||||
"User.inGameName",
|
||||
"PlusTier.tier as plusTier",
|
||||
])
|
||||
.executeTakeFirst();
|
||||
@@ -354,6 +355,16 @@ export async function currentFriendCodeByUserId(userId: number) {
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
export async function inGameNameByUserId(userId: number) {
|
||||
return (
|
||||
await db
|
||||
.selectFrom("User")
|
||||
.select("User.inGameName")
|
||||
.where("id", "=", userId)
|
||||
.executeTakeFirst()
|
||||
)?.inGameName;
|
||||
}
|
||||
|
||||
export function insertFriendCode(args: TablesInsertable["UserFriendCode"]) {
|
||||
return db.insertInto("UserFriendCode").values(args).execute();
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ import { StarIcon } from "~/components/icons/Star";
|
||||
import { StarFilledIcon } from "~/components/icons/StarFilled";
|
||||
import { TrashIcon } from "~/components/icons/Trash";
|
||||
import { USER } from "~/constants";
|
||||
import type { User, UserWeapon } from "~/db/types";
|
||||
import type { User } from "~/db/types";
|
||||
import { useUser } from "~/features/auth/core/user";
|
||||
import { requireUser, requireUserId } from "~/features/auth/core/user.server";
|
||||
import * as UserRepository from "~/features/user-page/UserRepository.server";
|
||||
@@ -50,6 +50,7 @@ import {
|
||||
} from "~/utils/zod";
|
||||
import { type UserPageLoaderData } from "./u.$identifier";
|
||||
import { userParamsSchema } from "../user-page-schemas.server";
|
||||
import * as TournamentTeamRepository from "~/features/tournament/TournamentTeamRepository.server";
|
||||
|
||||
import "~/styles/u-edit.css";
|
||||
|
||||
@@ -166,21 +167,26 @@ export const action: ActionFunction = async ({ request }) => {
|
||||
const { inGameNameText, inGameNameDiscriminator, ...data } = parsedInput.data;
|
||||
|
||||
const user = await requireUserId(request);
|
||||
const inGameName =
|
||||
inGameNameText && inGameNameDiscriminator
|
||||
? `${inGameNameText}#${inGameNameDiscriminator}`
|
||||
: null;
|
||||
|
||||
try {
|
||||
const editedUser = await UserRepository.updateProfile({
|
||||
...data,
|
||||
weapons: data.weapons as Array<
|
||||
Pick<UserWeapon, "weaponSplId" | "isFavorite">
|
||||
>,
|
||||
inGameName:
|
||||
inGameNameText && inGameNameDiscriminator
|
||||
? `${inGameNameText}#${inGameNameDiscriminator}`
|
||||
: null,
|
||||
inGameName,
|
||||
userId: user.id,
|
||||
showDiscordUniqueName: data.showDiscordUniqueName,
|
||||
});
|
||||
|
||||
// TODO: to transaction
|
||||
if (inGameName) {
|
||||
await TournamentTeamRepository.updateMemberInGameNameForNonStarted({
|
||||
inGameName,
|
||||
userId: user.id,
|
||||
});
|
||||
}
|
||||
|
||||
throw redirect(userPage(editedUser));
|
||||
} catch (e) {
|
||||
if (!errorIsSqliteUniqueConstraintFailure(e)) {
|
||||
|
||||
@@ -121,6 +121,7 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
|
||||
patronTier: user.patronTier,
|
||||
isArtist: user.isArtist,
|
||||
isVideoAdder: user.isVideoAdder,
|
||||
inGameName: user.inGameName,
|
||||
languages: user.languages ? user.languages.split(",") : [],
|
||||
}
|
||||
: undefined,
|
||||
|
||||
@@ -58,6 +58,10 @@
|
||||
color: var(--theme-warning);
|
||||
}
|
||||
|
||||
.text-warning-important {
|
||||
color: var(--theme-warning) !important;
|
||||
}
|
||||
|
||||
.text-theme {
|
||||
color: var(--theme);
|
||||
}
|
||||
|
||||
@@ -77,6 +77,7 @@
|
||||
"admin.actions.ADD_TEAM": "Register team",
|
||||
"admin.actions.DROP_TEAM_OUT": "Drop out team",
|
||||
"admin.actions.UNDO_DROP_TEAM_OUT": "Undo drop out",
|
||||
"admin.actions.UPDATE_IN_GAME_NAME": "Update player IGN",
|
||||
|
||||
"staff.role.ORGANIZER": "organizer",
|
||||
"staff.role.STREAMER": "streamer",
|
||||
|
||||
7
migrations/060-tournament-team-member-ign.js
Normal file
7
migrations/060-tournament-team-member-ign.js
Normal file
@@ -0,0 +1,7 @@
|
||||
export function up(db) {
|
||||
db.transaction(() => {
|
||||
db.prepare(
|
||||
/* sql */ `alter table "tournamentTeamMember" add "inGameName" text`,
|
||||
).run();
|
||||
})();
|
||||
}
|
||||
Reference in New Issue
Block a user