From 4e72f6761572b64502604019720b303aa21fb79e Mon Sep 17 00:00:00 2001
From: "Kalle (Sendou)" <38327916+Sendouc@users.noreply.github.com>
Date: Tue, 7 Dec 2021 00:57:06 +0200
Subject: [PATCH] Tournament admin page initial
---
README.md | 1 +
app/components/Alert.tsx | 8 +-
.../tournament/ActionSectionWrapper.tsx | 1 +
app/routes/to/$organization.$tournament.tsx | 8 ++
.../to/$organization.$tournament/admin.tsx | 75 +++++++++++++++++++
.../to/$organization.$tournament/index.tsx | 1 +
app/services/tournament.ts | 2 +
app/styles/tournament-admin.css | 33 ++++++++
prisma/seed/script.ts | 2 +-
9 files changed, 129 insertions(+), 2 deletions(-)
create mode 100644 app/routes/to/$organization.$tournament/admin.tsx
create mode 100644 app/styles/tournament-admin.css
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 (
-
+
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: {