From a9384ca099d8f7ce9c9b908abef1c2aa36fdb9e5 Mon Sep 17 00:00:00 2001 From: "Kalle (Sendou)" <38327916+Sendouc@users.noreply.github.com> Date: Wed, 8 Dec 2021 01:11:31 +0200 Subject: [PATCH] Admin can check in and check out teams --- README.md | 4 +- app/components/Button.tsx | 4 +- .../ActionSectionBeforeStartContent.tsx | 5 +- app/components/tournament/InfoBanner.tsx | 5 -- app/core/tournament/permissions.ts | 11 +++ app/core/tournament/utils.ts | 2 + .../api/tournament/$teamId/check-out.ts | 12 +++ .../to/$organization.$tournament/admin.tsx | 79 ++++++++++++++----- app/services/tournament.ts | 51 +++++++++++- app/styles/global.css | 17 ++++ app/styles/tournament-admin.css | 3 +- 11 files changed, 160 insertions(+), 33 deletions(-) create mode 100644 app/core/tournament/permissions.ts create mode 100644 app/core/tournament/utils.ts create mode 100644 app/routes/api/tournament/$teamId/check-out.ts diff --git a/README.md b/README.md index 831216adc..a8133a6a6 100644 --- a/README.md +++ b/README.md @@ -24,10 +24,12 @@ Prerequisites: [Node.js 16.13](https://nodejs.org/en/) - [x] Captain can remove players from roster - [ ] Add info about architecture to README +- [ ] Move away from MyForm - [x] Make description mandatory + overview tab - [x] Captain can check in -- [ ] Admin can check teams in (and out) +- [x] Admin can check teams in / out - [ ] Admin can drop people out (show on bracket somehow that they dropped) +- [ ] Admin can change team captain - [ ] Admin can randomize and rerandomize maps - [ ] Admin can change seeding - [ ] Admin can start the tournament diff --git a/app/components/Button.tsx b/app/components/Button.tsx index e50506aaa..47512a185 100644 --- a/app/components/Button.tsx +++ b/app/components/Button.tsx @@ -2,7 +2,7 @@ import classNames from "classnames"; import * as React from "react"; interface ButtonProps extends React.ButtonHTMLAttributes { - variant?: "outlined" | "destructive"; + variant?: "outlined" | "destructive" | "minimal" | "minimal-destructive"; tiny?: boolean; loading?: boolean; loadingText?: string; @@ -17,6 +17,8 @@ export function Button(props: ButtonProps) { className={classNames(className, { outlined: variant === "outlined", destructive: variant === "destructive", + "minimal-destructive": variant === "minimal-destructive", + minimal: variant === "minimal", loading: loading, tiny, })} diff --git a/app/components/tournament/ActionSectionBeforeStartContent.tsx b/app/components/tournament/ActionSectionBeforeStartContent.tsx index 3683b146a..8f9c01756 100644 --- a/app/components/tournament/ActionSectionBeforeStartContent.tsx +++ b/app/components/tournament/ActionSectionBeforeStartContent.tsx @@ -1,5 +1,5 @@ import * as React from "react"; -import { useFetcher, useLoaderData, useLocation } from "remix"; +import { useFetcher, useLoaderData } from "remix"; import { checkInClosesDate, TOURNAMENT_TEAM_ROSTER_MIN_SIZE, @@ -20,7 +20,6 @@ export function ActionSectionBeforeStartContent({ }) { const fetcher = useFetcher(); const tournament = useLoaderData(); - const location = useLocation(); const timeInMinutesBeforeCheckInCloses = () => { return Math.floor( @@ -113,7 +112,7 @@ export function ActionSectionBeforeStartContent({ > + + ); +} + +function CheckInButton({ teamId }: { teamId: string }) { + const fetcher = useFetcher(); + return ( + + + + ); +} diff --git a/app/services/tournament.ts b/app/services/tournament.ts index 856174df2..f3663f1a6 100644 --- a/app/services/tournament.ts +++ b/app/services/tournament.ts @@ -4,6 +4,7 @@ import { TOURNAMENT_TEAM_ROSTER_MAX_SIZE, TOURNAMENT_TEAM_ROSTER_MIN_SIZE, } from "~/constants"; +import { isTournamentAdmin } from "~/core/tournament/permissions"; import { Serialized } from "~/utils"; import { db } from "~/utils/db.server"; @@ -408,13 +409,16 @@ export async function checkIn({ }) { const tournamentTeam = await db.tournamentTeam.findUnique({ where: { id: teamId }, - include: { tournament: true, members: true }, + include: { tournament: { include: { organizer: true } }, members: true }, }); if (!tournamentTeam) throw new Response("Invalid team id", { status: 400 }); - if (tournamentTeam.checkedInTime) - throw new Response("Already checked in", { status: 400 }); + if ( + !isTournamentAdmin({ + userId, + organization: tournamentTeam.tournament.organizer, + }) && !tournamentTeam.members.some( ({ memberId, captain }) => captain && memberId === userId ) @@ -424,12 +428,18 @@ export async function checkIn({ // cut them some slack so UI never shows you can check in when you can't const checkInCutOff = TOURNAMENT_CHECK_IN_CLOSING_MINUTES_FROM_START - 2; if ( + !isTournamentAdmin({ + userId, + organization: tournamentTeam.tournament.organizer, + }) && tournamentTeam.tournament.startTime.getTime() - checkInCutOff * 60000 < - new Date().getTime() + new Date().getTime() ) { throw new Response("Check in time has passed", { status: 400 }); } + // TODO: fail if tournament has started + return db.tournamentTeam.update({ where: { id: teamId, @@ -439,3 +449,36 @@ export async function checkIn({ }, }); } + +export async function checkOut({ + teamId, + userId, +}: { + teamId: string; + userId: string; +}) { + const tournamentTeam = await db.tournamentTeam.findUnique({ + where: { id: teamId }, + include: { tournament: { include: { organizer: true } }, members: true }, + }); + if (!tournamentTeam) throw new Response("Invalid team id", { status: 400 }); + if ( + !isTournamentAdmin({ + organization: tournamentTeam.tournament.organizer, + userId, + }) + ) { + throw new Response("Not tournament admin", { status: 401 }); + } + + // TODO: fail if tournament has started + + return db.tournamentTeam.update({ + where: { + id: teamId, + }, + data: { + checkedInTime: null, + }, + }); +} diff --git a/app/styles/global.css b/app/styles/global.css index d328cea94..770e3904a 100644 --- a/app/styles/global.css +++ b/app/styles/global.css @@ -116,6 +116,15 @@ button.tiny { padding-inline: var(--s-2); } +button.minimal { + padding: 0; + border: none; + background-color: transparent; + + /* TODO: fix bad default */ + color: var(--theme-success); +} + button.destructive { border-color: var(--theme-error); background-color: transparent; @@ -123,6 +132,14 @@ button.destructive { outline-color: var(--theme-error); } +button.minimal-destructive { + padding: 0; + border: none; + background-color: transparent; + color: var(--theme-error); + outline-color: var(--theme-error); +} + button.loading { cursor: not-allowed; opacity: 0.6; diff --git a/app/styles/tournament-admin.css b/app/styles/tournament-admin.css index 6704248ad..87596c39e 100644 --- a/app/styles/tournament-admin.css +++ b/app/styles/tournament-admin.css @@ -2,6 +2,7 @@ display: flex; justify-content: space-evenly; gap: var(--s-6); + margin-block-end: var(--s-8); } .tournament__admin__alert { @@ -15,7 +16,7 @@ width: 100%; column-gap: var(--s-1); font-size: var(--fonts-sm); - grid-template-columns: 1fr 3fr 2fr 2fr 1fr; + grid-template-columns: 1fr 3fr 3fr 1fr; margin-block-start: var(--s-4); row-gap: var(--s-1-5); }