diff --git a/README.md b/README.md index 0e1a50429..831216adc 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,7 @@ Prerequisites: [Node.js 16.13](https://nodejs.org/en/) - [ ] Routine to calculate Trueskill - [ ] Markdown support for tournament description - [ ] Can make a new tournament as org admin +- [ ] Can delete team before checking in / tournament start - [ ] Rules tab with a good default, highlight overrides - [ ] Friend code to add that is easily editable even before tournament starts - [ ] Admin can see how long each match has been in progress (with descriptive colors) diff --git a/app/components/Alert.tsx b/app/components/Alert.tsx index 3e05634ac..1c68000e1 100644 --- a/app/components/Alert.tsx +++ b/app/components/Alert.tsx @@ -1,12 +1,18 @@ +import classNames from "classnames"; import { AlertIcon } from "./icons/Alert"; export function Alert(props: { children: React.ReactNode; type: "warning" | "info"; + className?: string; "data-cy"?: string; }) { return ( -
+
{props.children}
diff --git a/app/components/tournament/ActionSectionWrapper.tsx b/app/components/tournament/ActionSectionWrapper.tsx index 2c572342c..061c99cec 100644 --- a/app/components/tournament/ActionSectionWrapper.tsx +++ b/app/components/tournament/ActionSectionWrapper.tsx @@ -5,6 +5,7 @@ export function ActionSectionWrapper({ children: React.ReactNode; icon: "warning" | "info" | "success" | "error"; }) { + // todo: flex-dir: column on mobile return (
{ @@ -54,6 +55,7 @@ export const meta: MetaFunction = (props) => { export default function TournamentPage() { const data = useLoaderData(); const location = useLocation(); + const user = useUser(); const displayNavLinks = ["register", "manage-roster", "join-team"].every( (urlPart) => !location.pathname.endsWith(urlPart) @@ -75,6 +77,12 @@ export default function TournamentPage() { } } + const isAdmin = data.organizer.ownerId === user?.id; + + if (isAdmin) { + result.push({ code: "admin", text: "Admin" }); + } + return result; })(); diff --git a/app/routes/to/$organization.$tournament/admin.tsx b/app/routes/to/$organization.$tournament/admin.tsx new file mode 100644 index 000000000..b7c7a079c --- /dev/null +++ b/app/routes/to/$organization.$tournament/admin.tsx @@ -0,0 +1,75 @@ +import { useMatches } from "remix"; +import type { LinksFunction } from "remix"; +import { Button } from "~/components/Button"; +import type { FindTournamentByNameForUrlI } from "~/services/tournament"; +import styles from "~/styles/tournament-admin.css"; +import * as React from "react"; +import classNames from "classnames"; +import { TOURNAMENT_TEAM_ROSTER_MIN_SIZE } from "~/constants"; +import { Alert } from "~/components/Alert"; + +export const links: LinksFunction = () => { + return [{ rel: "stylesheet", href: styles }]; +}; + +export default function AdminPage() { + const [, parentRoute] = useMatches(); + const { teams } = parentRoute.data as FindTournamentByNameForUrlI; + + return ( + <> +
+ + +
+ + Drag teams to adjust their seeding + +
+
Seed
+
Name
+
+ Registered at +
+
+ Check-in time +
+
+ Roster size +
+ {teams.map((team, i) => ( + +
{i + 1}
+
{team.name}
+
+ {new Date(team.createdAt).toLocaleString("en-US", { + month: "numeric", + day: "numeric", + hour: "numeric", + minute: "numeric", + })} +
+
+ {team.checkedInTime + ? new Date(team.checkedInTime).toLocaleString("en-US", { + hour: "numeric", + minute: "numeric", + }) + : null} +
+
= TOURNAMENT_TEAM_ROSTER_MIN_SIZE, + tournament__admin__problem: + team.members.length < TOURNAMENT_TEAM_ROSTER_MIN_SIZE, + })} + > + {team.members.length} +
+
+ ))} +
+ + ); +} diff --git a/app/routes/to/$organization.$tournament/index.tsx b/app/routes/to/$organization.$tournament/index.tsx index ac9b96512..0199694f8 100644 --- a/app/routes/to/$organization.$tournament/index.tsx +++ b/app/routes/to/$organization.$tournament/index.tsx @@ -5,5 +5,6 @@ export default function DefaultTab() { const [, parentRoute] = useMatches(); const { description } = parentRoute.data as FindTournamentByNameForUrlI; + // TODO: margin on mobile return
{description}
; } diff --git a/app/services/tournament.ts b/app/services/tournament.ts index 14ca9b3e6..856174df2 100644 --- a/app/services/tournament.ts +++ b/app/services/tournament.ts @@ -38,6 +38,7 @@ export async function findTournamentByNameForUrl({ discordInvite: true, twitter: true, nameForUrl: true, + ownerId: true, }, }, mapPool: { @@ -51,6 +52,7 @@ export async function findTournamentByNameForUrl({ checkedInTime: true, id: true, name: true, + createdAt: true, members: { select: { captain: true, diff --git a/app/styles/tournament-admin.css b/app/styles/tournament-admin.css new file mode 100644 index 000000000..6704248ad --- /dev/null +++ b/app/styles/tournament-admin.css @@ -0,0 +1,33 @@ +.tournament__admin__buttons-container { + display: flex; + justify-content: space-evenly; + gap: var(--s-6); +} + +.tournament__admin__alert { + width: 100%; + margin-block-start: var(--s-4); +} + +/* TODO: overflow-x scroll */ +.tournament__admin__teams-container { + display: grid; + width: 100%; + column-gap: var(--s-1); + font-size: var(--fonts-sm); + grid-template-columns: 1fr 3fr 2fr 2fr 1fr; + margin-block-start: var(--s-4); + row-gap: var(--s-1-5); +} + +.tournament__admin__teams-container__header { + font-weight: var(--bold); +} + +.tournament__admin__ok { + color: var(--theme-success); +} + +.tournament__admin__problem { + color: var(--theme-error); +} diff --git a/prisma/seed/script.ts b/prisma/seed/script.ts index a37b102b9..ab669d70e 100644 --- a/prisma/seed/script.ts +++ b/prisma/seed/script.ts @@ -172,7 +172,7 @@ export async function seed(variation?: "check-in") { }, }); - for (let memberI = 0; memberI < (mockTeamI % 6) + 2; memberI++) { + for (let memberI = 0; memberI < (mockTeamI % 5) + 2; memberI++) { const memberId = userIdsCopy.shift()!; await prisma.tournamentTeamMember.create({ data: {