From 00ae9d575c9ea7848a48e1634b05657cd557e6b1 Mon Sep 17 00:00:00 2001 From: Kalle <38327916+Sendouc@users.noreply.github.com> Date: Fri, 1 Apr 2022 18:34:20 +0300 Subject: [PATCH] Unregister teams from tournament --- app/components/icons/Trash.tsx | 18 +++ app/models/TournamentTeam.server.ts | 12 ++ .../to/$organization.$tournament/manage.tsx | 122 ++++++++++++------ app/styles/global.css | 4 + app/styles/tournament-manage.css | 7 +- 5 files changed, 124 insertions(+), 39 deletions(-) create mode 100644 app/components/icons/Trash.tsx diff --git a/app/components/icons/Trash.tsx b/app/components/icons/Trash.tsx new file mode 100644 index 000000000..eb45707cb --- /dev/null +++ b/app/components/icons/Trash.tsx @@ -0,0 +1,18 @@ +export function TrashIcon({ className }: { className?: string }) { + return ( + + + + ); +} diff --git a/app/models/TournamentTeam.server.ts b/app/models/TournamentTeam.server.ts index dcbf0bb56..b4a856c52 100644 --- a/app/models/TournamentTeam.server.ts +++ b/app/models/TournamentTeam.server.ts @@ -57,3 +57,15 @@ export function checkOut(id: string) { }, }); } + +export function unregister(id: string) { + // TODO: this should use cascades instead + return db.$transaction([ + db.tournamentTeamMember.deleteMany({ where: { teamId: id } }), + db.tournamentTeam.delete({ + where: { + id, + }, + }), + ]); +} diff --git a/app/routes/to/$organization.$tournament/manage.tsx b/app/routes/to/$organization.$tournament/manage.tsx index f9034fe1f..76ff3cfaa 100644 --- a/app/routes/to/$organization.$tournament/manage.tsx +++ b/app/routes/to/$organization.$tournament/manage.tsx @@ -1,3 +1,4 @@ +import { useRef } from "react"; import { ActionFunction, Form, @@ -5,38 +6,49 @@ import { useMatches, useTransition, } from "remix"; +import invariant from "tiny-invariant"; import { z } from "zod"; import { Button } from "~/components/Button"; import { Catcher } from "~/components/Catcher"; +import { TrashIcon } from "~/components/icons/Trash"; import { TOURNAMENT_TEAM_ROSTER_MIN_SIZE } from "~/constants"; import { checkInHasStarted } from "~/core/tournament/utils"; +import { + isTournamentAdmin, + tournamentHasNotStarted, +} from "~/core/tournament/validators"; +import * as TournamentTeam from "~/models/TournamentTeam.server"; import { checkIn, checkOut, FindTournamentByNameForUrlI, } from "~/services/tournament"; import manageStylesUrl from "~/styles/tournament-manage.css"; -import { parseRequestFormData, requireUser, Unpacked } from "~/utils"; +import { parseRequestFormData, requireUser, Unpacked, validate } from "~/utils"; -// TODO: for consistency upper case this schema and all others like in /validators -const manageSchema = z.union([ - z.object({ - _action: z.literal("CHECK_IN"), - teamId: z.string().uuid(), - }), - z.object({ - _action: z.literal("CHECK_OUT"), - teamId: z.string().uuid(), - }), -]); +const manageActionSchema = z.object({ + _action: z.enum(["CHECK_OUT", "CHECK_IN", "UNREGISTER"]), + teamId: z.string().uuid(), +}); export const action: ActionFunction = async ({ context, request }) => { const data = await parseRequestFormData({ request, - schema: manageSchema, + schema: manageActionSchema, }); const user = requireUser(context); + const tournamentTeam = await TournamentTeam.findById(data.teamId); + validate(tournamentTeam, "Invalid team id"); + validate( + isTournamentAdmin({ + userId: user.id, + organization: tournamentTeam?.tournament.organizer, + }), + "Not tournament admin" + ); + // TODO: validate tournament has not started + switch (data._action) { case "CHECK_IN": { await checkIn({ teamId: data.teamId, userId: user.id }); @@ -46,8 +58,12 @@ export const action: ActionFunction = async ({ context, request }) => { await checkOut({ teamId: data.teamId, userId: user.id }); break; } + case "UNREGISTER": { + await TournamentTeam.unregister(data.teamId); + break; + } default: { - const exhaustive: never = data; + const exhaustive: never = data._action; throw new Response(`Unknown action: ${JSON.stringify(exhaustive)}`, { status: 400, }); @@ -88,7 +104,8 @@ function RowContents({ team: Unpacked; }) { const [, parentRoute] = useMatches(); - const { checkInStartTime } = parentRoute.data as FindTournamentByNameForUrlI; + const tournament = parentRoute.data as FindTournamentByNameForUrlI; + const unregisterFormRef = useRef(null); const playersLacking = (() => { if (team.members.length >= TOURNAMENT_TEAM_ROSTER_MIN_SIZE) return; @@ -96,32 +113,61 @@ function RowContents({ return TOURNAMENT_TEAM_ROSTER_MIN_SIZE - team.members.length; })(); + const handleUnregisterButtonClick = () => { + invariant(unregisterFormRef.current, "!unregisterFormRef.current"); + + if (window.confirm(`Delete ${team.name} from the tournament? (No undo)`)) { + unregisterFormRef.current.submit(); + } + }; + return ( -
-
{team.name}
-
- {new Date(team.createdAt).toLocaleString("en-US", { - month: "numeric", - day: "numeric", - hour: "numeric", - minute: "numeric", - })} -
-
- {!checkInHasStarted(checkInStartTime) ? null : team.checkedInTime ? ( - - ) : !playersLacking ? ( - - ) : ( -
- - {playersLacking} more {playersLacking > 1 ? "players" : "player"}{" "} - required - -
+ <> +
+ + +
+
+ {tournamentHasNotStarted(tournament) && ( + )} +
{team.name}
+
+ {new Date(team.createdAt).toLocaleString("en-US", { + month: "numeric", + day: "numeric", + hour: "numeric", + minute: "numeric", + })} +
+
+ {!checkInHasStarted( + tournament.checkInStartTime + ) ? null : team.checkedInTime ? ( + + ) : !playersLacking ? ( + + ) : ( +
+ + {playersLacking} more{" "} + {playersLacking > 1 ? "players" : "player"} required + +
+ )} +
-
+ ); } diff --git a/app/styles/global.css b/app/styles/global.css index 76b3a0021..6adc56ca9 100644 --- a/app/styles/global.css +++ b/app/styles/global.css @@ -764,6 +764,10 @@ hr { visibility: hidden; } +.hidden { + display: none; +} + .width-full { width: 100%; } diff --git a/app/styles/tournament-manage.css b/app/styles/tournament-manage.css index 3c7db7889..6a1be23ac 100644 --- a/app/styles/tournament-manage.css +++ b/app/styles/tournament-manage.css @@ -12,11 +12,16 @@ border-radius: var(--rounded); column-gap: var(--s-1); font-size: var(--fonts-sm); - grid-template-columns: 4fr 3fr 3fr; + grid-template-columns: 0.5fr 4fr 3fr 3fr; list-style: none; row-gap: var(--s-1-5); } +.tournament__manage__trash-icon { + width: 1rem; + margin-block-end: 1px; +} + /* TODO: this does not work correctly when variation = check-in */ .tournament__manage__teams-list-row:nth-child(even) { background-color: var(--bg-lighter-transparent);