mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-09-12 22:26:12 -05:00
Unregister teams from tournament
This commit is contained in:
18
app/components/icons/Trash.tsx
Normal file
18
app/components/icons/Trash.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
export function TrashIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -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<FindTournamentByNameForUrlI["teams"]>;
|
||||
}) {
|
||||
const [, parentRoute] = useMatches();
|
||||
const { checkInStartTime } = parentRoute.data as FindTournamentByNameForUrlI;
|
||||
const tournament = parentRoute.data as FindTournamentByNameForUrlI;
|
||||
const unregisterFormRef = useRef<HTMLFormElement>(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 (
|
||||
<div className="tournament__manage__teams-list-row">
|
||||
<div>{team.name}</div>
|
||||
<div>
|
||||
{new Date(team.createdAt).toLocaleString("en-US", {
|
||||
month: "numeric",
|
||||
day: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
})}
|
||||
</div>
|
||||
<div>
|
||||
{!checkInHasStarted(checkInStartTime) ? null : team.checkedInTime ? (
|
||||
<CheckOutButton teamId={team.id} />
|
||||
) : !playersLacking ? (
|
||||
<CheckInButton teamId={team.id} />
|
||||
) : (
|
||||
<div className="text-xs">
|
||||
<i>
|
||||
{playersLacking} more {playersLacking > 1 ? "players" : "player"}{" "}
|
||||
required
|
||||
</i>
|
||||
</div>
|
||||
<>
|
||||
<Form className="hidden" ref={unregisterFormRef} method="post">
|
||||
<input type="hidden" name="_action" value="UNREGISTER" />
|
||||
<input type="hidden" name="teamId" value={team.id} />
|
||||
</Form>
|
||||
<div className="tournament__manage__teams-list-row">
|
||||
{tournamentHasNotStarted(tournament) && (
|
||||
<Button
|
||||
type="button"
|
||||
name="_action"
|
||||
value="UNREGISTER"
|
||||
variant="minimal-destructive"
|
||||
title="Delete team from the tournament"
|
||||
aria-label="Delete team from the tournament"
|
||||
onClick={handleUnregisterButtonClick}
|
||||
>
|
||||
<TrashIcon className="tournament__manage__trash-icon" />
|
||||
</Button>
|
||||
)}
|
||||
<div>{team.name}</div>
|
||||
<div>
|
||||
{new Date(team.createdAt).toLocaleString("en-US", {
|
||||
month: "numeric",
|
||||
day: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
})}
|
||||
</div>
|
||||
<div>
|
||||
{!checkInHasStarted(
|
||||
tournament.checkInStartTime
|
||||
) ? null : team.checkedInTime ? (
|
||||
<CheckOutButton teamId={team.id} />
|
||||
) : !playersLacking ? (
|
||||
<CheckInButton teamId={team.id} />
|
||||
) : (
|
||||
<div className="text-xs">
|
||||
<i>
|
||||
{playersLacking} more{" "}
|
||||
{playersLacking > 1 ? "players" : "player"} required
|
||||
</i>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -764,6 +764,10 @@ hr {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.width-full {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user