mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-10-01 07:37:55 -05:00
Tournament admin page initial
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
})();
|
||||
|
||||
|
||||
75
app/routes/to/$organization.$tournament/admin.tsx
Normal file
75
app/routes/to/$organization.$tournament/admin.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
33
app/styles/tournament-admin.css
Normal file
33
app/styles/tournament-admin.css
Normal 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);
|
||||
}
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user