sendou.ink/app/features/tournament/routes/to.$id.join.tsx

211 lines
6.6 KiB
TypeScript

import type { ActionFunction, LoaderArgs } from "@remix-run/node";
import { redirect } from "@remix-run/node";
import { Form, useLoaderData, useOutletContext } from "@remix-run/react";
import invariant from "tiny-invariant";
import { SubmitButton } from "~/components/SubmitButton";
import { INVITE_CODE_LENGTH } from "~/constants";
import { useUser } from "~/modules/auth";
import { requireUserId } from "~/modules/auth/user.server";
import { notFoundIfFalsy, parseRequestFormData, validate } from "~/utils/remix";
import { assertUnreachable } from "~/utils/types";
import { tournamentPage } from "~/utils/urls";
import { findByInviteCode } from "../queries/findTeamByInviteCode.server";
import { findTeamsByTournamentId } from "../queries/findTeamsByTournamentId.server";
import { joinTeam } from "../queries/joinLeaveTeam.server";
import { TOURNAMENT } from "../tournament-constants";
import type { TournamentLoaderData, TournamentLoaderTeam } from "./to.$id";
import hasTournamentStarted from "../queries/hasTournamentStarted.server";
import React from "react";
import { discordFullName } from "~/utils/strings";
import { joinSchema } from "../tournament-schemas.server";
import { giveTrust } from "../queries/giveTrust.server";
import {
tournamentIdFromParams,
tournamentTeamMaxSize,
} from "../tournament-utils";
export const action: ActionFunction = async ({ request, params }) => {
const tournamentId = tournamentIdFromParams(params);
const user = await requireUserId(request);
const url = new URL(request.url);
const inviteCode = url.searchParams.get("code");
const data = await parseRequestFormData({ request, schema: joinSchema });
invariant(inviteCode, "code is missing");
const leanTeam = notFoundIfFalsy(findByInviteCode(inviteCode));
const teams = findTeamsByTournamentId(leanTeam.tournamentId);
const teamToJoin = teams.find((team) => team.id === leanTeam.id);
const previousTeam = teams.find((team) =>
team.members.some((member) => member.userId === user.id)
);
const tournamentHasStarted = hasTournamentStarted(tournamentId);
if (tournamentHasStarted) {
validate(
!previousTeam || !previousTeam.checkedInAt,
"Can't leave checked in team mid tournament"
);
}
validate(teamToJoin, "Not team of this tournament");
validate(
validateCanJoin({
inviteCode,
teamToJoin,
userId: user.id,
tournamentHasStarted,
}) === "VALID",
"Cannot join this team or invite code is invalid"
);
const whatToDoWithPreviousTeam = !previousTeam
? undefined
: previousTeam.members.some(
(member) => member.userId === user.id && member.isOwner
)
? "DELETE"
: "LEAVE";
joinTeam({
userId: user.id,
newTeamId: teamToJoin.id,
previousTeamId: previousTeam?.id,
// making sure they aren't unfilling one checking in condition i.e. having full roster
// and then having members leave without it affecting the checking in status
checkOutTeam:
whatToDoWithPreviousTeam === "LEAVE" &&
previousTeam &&
previousTeam.members.length <= TOURNAMENT.TEAM_MIN_MEMBERS_FOR_FULL,
whatToDoWithPreviousTeam,
});
if (data.trust) {
const inviterUserId = teamToJoin.members.find(
(member) => member.isOwner
)?.userId;
invariant(inviterUserId, "Inviter user could not be resolved");
giveTrust({
trustGiverUserId: user.id,
trustReceiverUserId: inviterUserId,
});
}
return redirect(tournamentPage(leanTeam.tournamentId));
};
export const loader = ({ request, params }: LoaderArgs) => {
const tournamentId = tournamentIdFromParams(params);
const url = new URL(request.url);
const inviteCode = url.searchParams.get("code");
return {
teamId: inviteCode ? findByInviteCode(inviteCode)?.id : null,
inviteCode,
tournamentHasStarted: hasTournamentStarted(tournamentId),
};
};
export default function JoinTeamPage() {
const id = React.useId();
const user = useUser();
const parentRouteData = useOutletContext<TournamentLoaderData>();
const data = useLoaderData<typeof loader>();
const teamToJoin = parentRouteData.teams.find(
(team) => team.id === data.teamId
);
const captain = teamToJoin?.members.find((member) => member.isOwner);
const validationStatus = validateCanJoin({
inviteCode: data.inviteCode,
teamToJoin,
userId: user?.id,
tournamentHasStarted: data.tournamentHasStarted,
});
const textPrompt = () => {
switch (validationStatus) {
case "MISSING_CODE": {
return "Invite code is missing. Was the full URL copied?";
}
case "SHORT_CODE": {
return "Invite code is not the right length. Was the full URL copied?";
}
case "NO_TEAM_MATCHING_CODE": {
return "No team matching the invite code.";
}
case "TEAM_FULL": {
return "Team you are trying to join is full.";
}
case "ALREADY_JOINED": {
return "You're already a member of this team.";
}
case "NOT_LOGGED_IN": {
return "You must be logged in to join a team.";
}
case "VALID": {
invariant(teamToJoin);
return `Join ${teamToJoin.name} for ${parentRouteData.event.name}?`;
}
default: {
assertUnreachable(validationStatus);
}
}
};
return (
<Form method="post" className="tournament__invite-container">
<div className="stack sm">
<div className="text-center">{textPrompt()}</div>
{validationStatus === "VALID" ? (
<div className="text-lighter text-sm stack horizontal sm items-center">
<input id={id} type="checkbox" name="trust" />{" "}
<label htmlFor={id} className="mb-0">
Trust {captain ? discordFullName(captain) : ""} to add you on
their own to future tournaments?
</label>
</div>
) : null}
</div>
{validationStatus === "VALID" ? (
<SubmitButton size="big">Join</SubmitButton>
) : null}
</Form>
);
}
function validateCanJoin({
inviteCode,
teamToJoin,
userId,
tournamentHasStarted,
}: {
inviteCode?: string | null;
teamToJoin?: TournamentLoaderTeam;
userId?: number;
tournamentHasStarted: boolean;
}) {
if (typeof inviteCode !== "string") {
return "MISSING_CODE";
}
if (typeof userId !== "number") {
return "NOT_LOGGED_IN";
}
if (!teamToJoin && inviteCode.length !== INVITE_CODE_LENGTH) {
return "SHORT_CODE";
}
if (!teamToJoin) {
return "NO_TEAM_MATCHING_CODE";
}
if (
teamToJoin.members.length >= tournamentTeamMaxSize(tournamentHasStarted)
) {
return "TEAM_FULL";
}
if (teamToJoin.members.some((member) => member.userId === userId)) {
return "ALREADY_JOINED";
}
return "VALID";
}