Tournament admin page initial

This commit is contained in:
Kalle (Sendou)
2021-12-07 00:57:06 +02:00
parent cc7e1d6428
commit 4e72f67615
9 changed files with 129 additions and 2 deletions

View File

@@ -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)

View File

@@ -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 (
<div data-type={props.type} className="alert" data-cy={props["data-cy"]}>
<div
data-type={props.type}
className={classNames("alert", props.className)}
data-cy={props["data-cy"]}
>
<AlertIcon />
{props.children}
</div>

View File

@@ -5,6 +5,7 @@ export function ActionSectionWrapper({
children: React.ReactNode;
icon: "warning" | "info" | "success" | "error";
}) {
// todo: flex-dir: column on mobile
return (
<section
className="tournament__action-section"

View File

@@ -17,6 +17,7 @@ import {
FindTournamentByNameForUrlI,
} from "~/services/tournament";
import { getUser, makeTitle } from "~/utils";
import { useUser } from "~/utils/hooks";
import tournamentStylesUrl from "../../styles/tournament.css";
export const links: LinksFunction = () => {
@@ -54,6 +55,7 @@ export const meta: MetaFunction = (props) => {
export default function TournamentPage() {
const data = useLoaderData<FindTournamentByNameForUrlI>();
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;
})();

View File

@@ -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 (
<>
<div className="tournament__admin__buttons-container">
<Button>Start tournament</Button>
<Button variant="outlined">Edit tournament</Button>
</div>
<Alert type="info" className="tournament__admin__alert">
Drag teams to adjust their seeding
</Alert>
<div className="tournament__admin__teams-container">
<div className="tournament__admin__teams-container__header">Seed</div>
<div className="tournament__admin__teams-container__header">Name</div>
<div className="tournament__admin__teams-container__header">
Registered at
</div>
<div className="tournament__admin__teams-container__header">
Check-in time
</div>
<div className="tournament__admin__teams-container__header">
Roster size
</div>
{teams.map((team, i) => (
<React.Fragment key={team.id}>
<div>{i + 1}</div>
<div>{team.name}</div>
<div>
{new Date(team.createdAt).toLocaleString("en-US", {
month: "numeric",
day: "numeric",
hour: "numeric",
minute: "numeric",
})}
</div>
<div>
{team.checkedInTime
? new Date(team.checkedInTime).toLocaleString("en-US", {
hour: "numeric",
minute: "numeric",
})
: null}
</div>
<div
className={classNames({
tournament__admin__ok:
team.members.length >= TOURNAMENT_TEAM_ROSTER_MIN_SIZE,
tournament__admin__problem:
team.members.length < TOURNAMENT_TEAM_ROSTER_MIN_SIZE,
})}
>
{team.members.length}
</div>
</React.Fragment>
))}
</div>
</>
);
}

View File

@@ -5,5 +5,6 @@ export default function DefaultTab() {
const [, parentRoute] = useMatches();
const { description } = parentRoute.data as FindTournamentByNameForUrlI;
// TODO: margin on mobile
return <article style={{ whiteSpace: "pre-line" }}>{description}</article>;
}

View File

@@ -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,

View File

@@ -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);
}

View File

@@ -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: {