mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-08-28 06:08:13 -05:00
Edit team info: friend code, room pass & canHost
This commit is contained in:
@@ -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");
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user