diff --git a/app/components/tournament/TeamRoster.tsx b/app/components/tournament/TeamRoster.tsx index 006584a1c..ae3cb1444 100644 --- a/app/components/tournament/TeamRoster.tsx +++ b/app/components/tournament/TeamRoster.tsx @@ -1,11 +1,15 @@ -import { Form, useTransition } from "@remix-run/react"; +import { Form, useMatches, useTransition } from "@remix-run/react"; +import { tournamentHasNotStarted } from "~/core/tournament/validators"; import { useUser } from "~/hooks/common"; +import { FindTournamentByNameForUrlI } from "~/services/tournament"; import { Avatar } from "../Avatar"; import { Button } from "../Button"; +import { SubmitButton } from "../SubmitButton"; export function TeamRoster({ team, - deleteMode: deleteMode, + showUnregister = false, + deleteMode = false, }: { team: { id: string; @@ -21,19 +25,51 @@ export function TeamRoster({ }[]; }; deleteMode?: boolean; + showUnregister?: boolean; }) { + const [, parentRoute] = useMatches(); + const tournament = parentRoute.data as FindTournamentByNameForUrlI; const user = useUser(); const showDeleteButtons = (userToDeleteId: string) => { - // TODO: - const tournamentHasStarted = false; + return ( + tournamentHasNotStarted(tournament) && + deleteMode && + userToDeleteId !== user?.id + ); + }; - return !tournamentHasStarted && deleteMode && userToDeleteId !== user?.id; + const showUnregisterButton = () => { + return tournamentHasNotStarted(tournament) && showUnregister; }; return (
-
{team.name}
+
+ {team.name} + {showUnregisterButton() ? ( +
+ + + { + if ( + !confirm(`Unregister ${team.name} from ${tournament.name}?`) + ) { + e.preventDefault(); + } + }} + data-cy="unregister-button" + > + Unregister + +
+ ) : null} +
{team.members .sort((a, b) => Number(b.captain) - Number(a.captain)) diff --git a/app/routes/to/$organization.$tournament/manage-team.tsx b/app/routes/to/$organization.$tournament/manage-team.tsx index 567971629..fa2125967 100644 --- a/app/routes/to/$organization.$tournament/manage-team.tsx +++ b/app/routes/to/$organization.$tournament/manage-team.tsx @@ -7,6 +7,7 @@ import { redirect, } from "@remix-run/node"; import { useActionData, useLoaderData, useLocation } from "@remix-run/react"; +import invariant from "tiny-invariant"; import { z } from "zod"; import { AddPlayers } from "~/components/AddPlayers"; import { Alert } from "~/components/Alert"; @@ -16,6 +17,7 @@ import { TOURNAMENT_TEAM_ROSTER_MAX_SIZE } from "~/constants"; import { isCaptainOfTheTeam, teamHasNotCheckedIn, + tournamentHasNotStarted, tournamentTeamIsNotFull, } from "~/core/tournament/validators"; import * as Tournament from "~/models/Tournament.server"; @@ -42,6 +44,10 @@ const actionSchema = z.union([ userId: z.string().uuid(), teamId: z.string().uuid(), }), + z.object({ + _action: z.literal("UNREGISTER"), + teamId: z.string().uuid(), + }), ]); type ActionData = { @@ -59,18 +65,14 @@ export const action: ActionFunction = async ({ }); const user = requireUser(context); + const tournamentTeam = await TournamentTeam.findById(data.teamId); + validate(tournamentTeam, "Invalid tournament team id"); + validate(isCaptainOfTheTeam(user, tournamentTeam), "Not captain of the team"); + switch (data._action) { case "ADD_PLAYER": { try { - const tournamentTeam = await TournamentTeam.findById(data.teamId); - - // TODO: Validate if tournament already started / concluded (depending on if tournament allows mid-event roster additions) - validate(tournamentTeam, "Invalid tournament team id"); validate(tournamentTeamIsNotFull(tournamentTeam), "Team is full"); - validate( - isCaptainOfTheTeam(user, tournamentTeam), - "Not captain of the team" - ); await TournamentTeamMember.joinTeam({ tournamentId: tournamentTeam.tournament.id, @@ -91,14 +93,7 @@ export const action: ActionFunction = async ({ return { ok: "ADD_PLAYER" }; } case "DELETE_PLAYER": { - const tournamentTeam = await TournamentTeam.findById(data.teamId); - - validate(tournamentTeam, "Invalid team id"); validate(data.userId !== user.id, "Can't remove self"); - validate( - isCaptainOfTheTeam(user, tournamentTeam), - "Not captain of the team" - ); validate( teamHasNotCheckedIn(tournamentTeam), "Can't remove players after checking in" @@ -111,6 +106,16 @@ export const action: ActionFunction = async ({ return { ok: "DELETE_PLAYER" }; } + case "UNREGISTER": { + const tournament = await Tournament.findById(tournamentTeam.tournamentId); + invariant(tournament, "!tournament"); + + validate(tournamentHasNotStarted(tournament), "Tournament is ongoing"); + + await TournamentTeam.unregister(data.teamId); + + return { ok: "UNREGISTER" }; + } default: { const exhaustive: never = data; throw new Response(`Unknown action: ${JSON.stringify(exhaustive)}`, { @@ -173,7 +178,11 @@ export default function ManageTeamPage() { )}
- +
{ownTeam.members.length < TOURNAMENT_TEAM_ROSTER_MAX_SIZE && ( { ).as("tournaments"); }); - it("Registers a new team", () => { + function registerToTournament(teamName: string) { cy.logIn("sendou"); cy.visit("/to/sendou/in-the-zone-x"); cy.title().should("include", "In The Zone X"); cy.getCy("register-button").click(); - cy.getCy("team-name-input").type("Team Olive"); + cy.getCy("team-name-input").type(teamName); cy.getCy("register-submit-button").click(); - cy.contains("Team name already taken"); + } + + it("Registers a new team", () => { + registerToTournament("Team Olive"); cy.wait("@tournaments"); cy.getCy("team-name-input").clear().type("Team Olive V2"); @@ -33,4 +36,12 @@ describe("Before tournament starts", () => { "map-pool__stage-image-disabled" ); }); + + it("Can unregister from tournament", () => { + registerToTournament("Team Olive V2"); + + cy.on("window:confirm", () => true); + cy.getCy("unregister-button").click(); + cy.getCy("register-button"); + }); });