Edit team info: friend code, room pass & canHost

This commit is contained in:
Kalle (Sendou)
2021-12-26 19:23:35 +02:00
parent 26c8838f66
commit 3e14bb4320
8 changed files with 237 additions and 25 deletions

View File

@@ -38,3 +38,6 @@ export function tournamentHasStarted(
export const friendCodeRegExpString = "^(SW-)?[0-9]{4}-?[0-9]{4}-?[0-9]{4}$";
export const friendCodeRegExp = new RegExp(friendCodeRegExpString, "i");
export const roompassRegExpString = "^[0-9]{4}$";
export const roompassRegExp = new RegExp(roompassRegExpString, "i");

View File

@@ -113,6 +113,9 @@ export function findByNameForUrlWithInviteCodes(tournamentNameForUrl: string) {
name: true,
inviteCode: true,
checkedInTime: true,
friendCode: true,
roomPass: true,
canHost: true,
members: {
select: {
captain: true,

View File

@@ -60,3 +60,27 @@ export function checkOut(id: string) {
},
});
}
export type EditTeam = Prisma.PromiseReturnType<typeof editTeam>;
export function editTeam({
id,
friendCode,
roomPass,
canHost,
}: {
id: string;
friendCode: string;
roomPass: string | null;
canHost: boolean;
}) {
return db.tournamentTeam.update({
where: {
id,
},
data: {
friendCode,
roomPass,
canHost,
},
});
}

View File

@@ -8,30 +8,39 @@ import {
useActionData,
useLoaderData,
useLocation,
useTransition,
} from "remix";
import invariant from "tiny-invariant";
import { Alert } from "~/components/Alert";
import { Button } from "~/components/Button";
import { Catcher } from "~/components/Catcher";
import { FormErrorMessage } from "~/components/FormErrorMessage";
import { FormInfoText } from "~/components/FormInfoText";
import { TeamRoster } from "~/components/tournament/TeamRoster";
import { TOURNAMENT_TEAM_ROSTER_MAX_SIZE } from "~/constants";
import {
friendCodeRegExpString,
roompassRegExp,
roompassRegExpString,
} from "~/core/tournament/utils";
import type { FindManyByTrustReceiverId } from "~/models/TrustRelationship";
import {
editTeam,
ownTeamWithInviteCode,
putPlayerToTeam,
removePlayerFromTeam,
} from "~/services/tournament";
import { getTrustingUsers } from "~/services/user";
import type { FindManyByTrustReceiverId } from "~/models/TrustRelationship";
import styles from "~/styles/tournament-manage-roster.css";
import { requireUser } from "~/utils";
import { useBaseURL, useIsSubmitting, useTimeoutState } from "~/utils/hooks";
import { Catcher } from "~/components/Catcher";
export const links: LinksFunction = () => {
return [{ rel: "stylesheet", href: styles }];
};
export enum ManageRosterAction {
EDIT_TEAM = "EDIT_TEAM",
ADD_PLAYER = "ADD_PLAYER",
DELETE_PLAYER = "DELETE_PLAYER",
}
@@ -48,7 +57,6 @@ export const action: ActionFunction = async ({
context,
}): Promise<Response | ActionData> => {
const data = Object.fromEntries(await request.formData());
invariant(typeof data.userId === "string", "Invalid type for userId");
invariant(typeof data.teamId === "string", "Invalid type for teamId");
invariant(typeof data._action === "string", "Invalid type for _action");
@@ -56,6 +64,8 @@ export const action: ActionFunction = async ({
switch (data._action) {
case ManageRosterAction.ADD_PLAYER: {
invariant(typeof data.userId === "string", "Invalid type for userId");
try {
await putPlayerToTeam({
teamId: data.teamId,
@@ -77,13 +87,39 @@ export const action: ActionFunction = async ({
return new Response("Added player to team", { status: 200 });
}
case ManageRosterAction.DELETE_PLAYER: {
invariant(typeof data.userId === "string", "Invalid type for userId");
await removePlayerFromTeam({
captainId: user.id,
userId: user.id,
playerId: data.userId,
teamId: data.teamId,
});
return new Response("Player deleted from team", { status: 200 });
}
case ManageRosterAction.EDIT_TEAM: {
invariant(
typeof data.friendCode === "string",
"Invalid type for friendCode"
);
invariant(typeof data.roomPass === "string", "Invalid type for roomPass");
invariant(
["yes", "no"].includes(data.canHost as string),
"Invalid type for canHost"
);
if (!roompassRegExp.test(data.roomPass)) {
return new Response("Invalid room pass", { status: 400 });
}
await editTeam({
friendCode: data.friendCode,
roomPass: data.roomPass,
teamId: data.teamId,
canHost: data.canHost === "yes" ? true : false,
userId: user.id,
});
return new Response("Tournament team edited", { status: 200 });
}
default: {
throw new Response("Bad Request", { status: 400 });
}
@@ -130,6 +166,7 @@ export default function ManageRosterPage() {
const { ownTeam, trustingUsers } = useLoaderData<Data>();
const baseURL = useBaseURL();
const location = useLocation();
const transition = useTransition();
const isSubmitting = useIsSubmitting("POST");
const urlWithInviteCode = `${baseURL}${location.pathname.replace(
@@ -147,12 +184,85 @@ export default function ManageRosterPage() {
<div className="tournament__manage-roster__roster-container">
<TeamRoster team={ownTeam} deleteMode={!ownTeam.checkedInTime} />
</div>
{ownTeam.members.length < TOURNAMENT_TEAM_ROSTER_MAX_SIZE && (
<div className="tournament__manage-roster__actions">
<div className="tournament__manage-roster__actions__section">
<label htmlFor="inviteCodeInput">
Share this URL to invite players to your team
<Form method="post">
<input
type="hidden"
name="_action"
value={ManageRosterAction.EDIT_TEAM}
/>
<input type="hidden" name="teamId" value={ownTeam.id} />
<fieldset>
<legend>Edit team info</legend>
<label htmlFor="friendCode">
Friend code for your opponents to add
</label>
<input
name="friendCode"
id="friendCode"
defaultValue={ownTeam.friendCode}
required
pattern={friendCodeRegExpString}
/>
<label className="mt-3" htmlFor="roomPass">
Room password
</label>
<input
name="roomPass"
id="roomPass"
defaultValue={ownTeam.roomPass ?? undefined}
placeholder="1234"
pattern={roompassRegExpString}
/>
<FormInfoText>
If blank the password will be randomly generated whenever you host
</FormInfoText>
<label className="mt-3" htmlFor="canHost">
Does your team want to host?
</label>
<div className="flex align-center">
<input
type="radio"
id="yes"
name="canHost"
value="yes"
defaultChecked={ownTeam.canHost}
/>
<label className="mb-0 ml-2 " htmlFor="yes">
Yes
</label>
</div>
<div className="mt-1 flex align-center">
<input
type="radio"
id="no"
name="canHost"
value="no"
defaultChecked={!ownTeam.canHost}
/>
<label className="mb-0 ml-2" htmlFor="no">
No
</label>
</div>
<FormInfoText>
You might still have to host if both teams prefer not to
</FormInfoText>
<Button
className="mt-4"
type="submit"
loading={transition.state !== "idle"}
>
Save
</Button>
</fieldset>
</Form>
{ownTeam.members.length < TOURNAMENT_TEAM_ROSTER_MAX_SIZE && (
<fieldset className="tournament__manage-roster__actions">
<legend>Add players to your team</legend>
<div className="tournament__manage-roster__actions__section">
<label htmlFor="inviteCodeInput">Share this URL</label>
<input
id="inviteCodeInput"
className="tournament__manage-roster__input"
@@ -196,7 +306,7 @@ export default function ManageRosterPage() {
</Form>
</div>
)}
</div>
</fieldset>
)}
</div>
);

View File

@@ -148,10 +148,7 @@ export default function RegisterPage() {
data-cy="team-name-input"
/>
<FormErrorMessage errorMsg={actionData?.fieldErrors?.teamName} />
<label
className="tournament__register__friend-code-label"
htmlFor="friendCode"
>
<label className="mt-3" htmlFor="friendCode">
Friend code for your opponents to add
</label>
<input

View File

@@ -369,17 +369,49 @@ export async function putPlayerToTeam({
});
}
export async function editTeam({
teamId,
userId,
friendCode,
roomPass,
canHost,
}: {
teamId: string;
userId: string;
friendCode: string;
roomPass: string | null;
canHost: boolean;
}) {
const tournamentTeam = await TournamentTeam.findById(teamId);
if (!tournamentTeam) throw new Response("Invalid team id", { status: 400 });
if (
!tournamentTeam.members.some(
({ memberId, captain }) => captain && memberId === userId
)
) {
throw new Response("Not captain of the team", { status: 401 });
}
return TournamentTeam.editTeam({
id: teamId,
canHost,
friendCode,
roomPass,
});
}
export async function removePlayerFromTeam({
teamId,
captainId,
userId,
playerId,
}: {
teamId: string;
captainId: string;
userId: string;
playerId: string;
}) {
if (captainId === playerId) {
throw new Response("Can't remove captain", { status: 400 });
if (userId === playerId) {
throw new Response("Can't remove self", { status: 400 });
}
const tournamentTeam = await TournamentTeam.findById(teamId);
@@ -392,7 +424,7 @@ export async function removePlayerFromTeam({
}
if (
!tournamentTeam.members.some(
({ memberId, captain }) => captain && memberId === captainId
({ memberId, captain }) => captain && memberId === userId
)
) {
throw new Response("Not captain of the team", { status: 401 });

View File

@@ -171,7 +171,7 @@ input:not(.plain) {
height: 1rem;
padding: var(--s-4) var(--s-3);
border: 1px solid var(--border);
accent-color: var(--theme);
accent-color: var(--theme-secondary);
background-color: transparent;
border-radius: var(--rounded);
color: var(--text);
@@ -215,7 +215,21 @@ label {
}
fieldset {
all: inherit;
border: none;
background-color: var(--bg-lighter);
border-radius: var(--rounded);
padding-block: var(--s-6);
padding-inline: var(--s-7);
}
legend {
padding: var(--s-2);
background: url("/svg/background-pattern.svg");
background-color: var(--bg);
border-radius: 2px;
border-radius: var(--rounded-sm);
font-size: var(--fonts-sm);
font-weight: var(--bold);
}
article {
@@ -236,6 +250,7 @@ select {
background-repeat: no-repeat;
border-radius: var(--rounded);
cursor: pointer;
font-size: var(--fonts-sm);
font-weight: 500;
padding-block: 3.5px;
padding-inline: var(--s-3);
@@ -313,6 +328,26 @@ select::selection {
/* Utility classes */
.ml-2 {
margin-inline-start: var(--s-2);
}
.mt-1 {
margin-block-start: var(--s-1);
}
.mt-3 {
margin-block-start: var(--s-3);
}
.mt-4 {
margin-block-start: var(--s-4);
}
.mb-0 {
margin-block-end: 0;
}
.visibility-hidden {
visibility: hidden;
}
@@ -320,3 +355,15 @@ select::selection {
.width-100 {
width: 100%;
}
.flex {
display: flex;
}
.align-center {
align-items: center;
}
.justify-center {
justify-content: center;
}

View File

@@ -16,7 +16,3 @@
gap: var(--s-2);
margin-block-start: var(--s-6);
}
.tournament__register__friend-code-label {
margin-block-start: var(--s-3);
}