mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-09-14 15:16:09 -05:00
Register to tournament action
This commit is contained in:
@@ -15,6 +15,7 @@ import normalizeStylesUrl from "~/styles/normalize.css";
|
||||
import globalStylesUrl from "~/styles/global.css";
|
||||
import layoutStylesUrl from "~/styles/layout.css";
|
||||
import { Layout } from "./components/Layout";
|
||||
import type { LoggedInUser } from "./utils";
|
||||
|
||||
export const links: LinksFunction = () => {
|
||||
return [
|
||||
@@ -30,11 +31,7 @@ export const loader: LoaderFunction = ({ context }) => {
|
||||
return user ?? null;
|
||||
};
|
||||
|
||||
type LoggedInUser = {
|
||||
id: number;
|
||||
discordId: string;
|
||||
discordAvatar: string;
|
||||
} | null;
|
||||
export let unstable_shouldReload = () => false;
|
||||
|
||||
const UserContext = React.createContext<LoggedInUser>(null);
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
NavLink,
|
||||
Link,
|
||||
} from "remix";
|
||||
import { useLoaderData, Outlet } from "remix";
|
||||
import { useLoaderData, Outlet, useLocation } from "remix";
|
||||
import invariant from "tiny-invariant";
|
||||
import { DiscordIcon } from "~/components/icons/Discord";
|
||||
import { TwitterIcon } from "~/components/icons/Twitter";
|
||||
@@ -49,30 +49,35 @@ export const meta: MetaFunction = (props) => {
|
||||
|
||||
export default function TournamentPage() {
|
||||
const data = useLoaderData<FindTournamentByNameForUrlI>();
|
||||
const location = useLocation();
|
||||
|
||||
const displayNavLinks = !location.pathname.endsWith("register");
|
||||
|
||||
return (
|
||||
<div className="tournament__container">
|
||||
<InfoBanner />
|
||||
<div
|
||||
style={{ "--tabs-count": 5 } as Record<string, number>}
|
||||
className="tournament__links-container"
|
||||
>
|
||||
<NavLink className="tournament__nav-link" to="overview">
|
||||
Overview
|
||||
</NavLink>
|
||||
<NavLink className="tournament__nav-link" to="map-pool">
|
||||
Map Pool
|
||||
</NavLink>
|
||||
<NavLink className="tournament__nav-link" to="bracket">
|
||||
Bracket
|
||||
</NavLink>
|
||||
<NavLink className="tournament__nav-link" to="teams">
|
||||
Teams ({data.teams.length})
|
||||
</NavLink>
|
||||
<NavLink className="tournament__nav-link" to="streams">
|
||||
Streams (4)
|
||||
</NavLink>
|
||||
</div>
|
||||
{displayNavLinks && (
|
||||
<div
|
||||
style={{ "--tabs-count": 5 } as Record<string, number>}
|
||||
className="tournament__links-container"
|
||||
>
|
||||
<NavLink className="tournament__nav-link" to="overview">
|
||||
Overview
|
||||
</NavLink>
|
||||
<NavLink className="tournament__nav-link" to="map-pool">
|
||||
Map Pool
|
||||
</NavLink>
|
||||
<NavLink className="tournament__nav-link" to="bracket">
|
||||
Bracket
|
||||
</NavLink>
|
||||
<NavLink className="tournament__nav-link" to="teams">
|
||||
Teams ({data.teams.length})
|
||||
</NavLink>
|
||||
<NavLink className="tournament__nav-link" to="streams">
|
||||
Streams (4)
|
||||
</NavLink>
|
||||
</div>
|
||||
)}
|
||||
<Outlet />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import stylesUrl from "~/styles/map-pool.css";
|
||||
import styles from "~/styles/tournament-map-pool.css";
|
||||
import type { Mode } from ".prisma/client";
|
||||
import classNames from "classnames";
|
||||
import { modesShort, stages } from "~/constants";
|
||||
@@ -6,7 +6,7 @@ import { LinksFunction, useMatches } from "remix";
|
||||
import { FindTournamentByNameForUrlI } from "~/services/tournament";
|
||||
|
||||
export const links: LinksFunction = () => {
|
||||
return [{ rel: "stylesheet", href: stylesUrl }];
|
||||
return [{ rel: "stylesheet", href: styles }];
|
||||
};
|
||||
|
||||
export default function MapPoolTab() {
|
||||
|
||||
87
app/routes/to/$organization.$tournament/register.tsx
Normal file
87
app/routes/to/$organization.$tournament/register.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
import styles from "~/styles/tournament-register.css";
|
||||
import {
|
||||
Form,
|
||||
redirect,
|
||||
LinksFunction,
|
||||
ActionFunction,
|
||||
useMatches,
|
||||
} from "remix";
|
||||
import invariant from "tiny-invariant";
|
||||
import {
|
||||
createTournamentTeam,
|
||||
FindTournamentByNameForUrlI,
|
||||
} from "~/services/tournament";
|
||||
import { requireUser } from "~/utils";
|
||||
|
||||
export const links: LinksFunction = () => {
|
||||
return [{ rel: "stylesheet", href: styles }];
|
||||
};
|
||||
|
||||
const TEAM_NAME_MIN_LENGTH = 2;
|
||||
const TEAM_NAME_MAX_LENGTH = 40;
|
||||
|
||||
export const action: ActionFunction = async ({ request, context }) => {
|
||||
const formData = await request.formData();
|
||||
const teamName = formData.get("team-name");
|
||||
const tournamentId = Number(formData.get("tournament-id"));
|
||||
invariant(typeof teamName === "string", "Invalid type for team name.");
|
||||
invariant(
|
||||
typeof tournamentId === "number",
|
||||
"Invalid type for tournament id."
|
||||
);
|
||||
|
||||
if (
|
||||
teamName.length < TEAM_NAME_MIN_LENGTH ||
|
||||
teamName.length > TEAM_NAME_MAX_LENGTH
|
||||
) {
|
||||
throw new Error(
|
||||
`Team name length has to be between min ${TEAM_NAME_MIN_LENGTH} and max ${TEAM_NAME_MAX_LENGTH}. Was ${teamName.length}.`
|
||||
);
|
||||
}
|
||||
|
||||
const user = requireUser(context);
|
||||
|
||||
// TODO: validate can register for tournament
|
||||
|
||||
await createTournamentTeam({
|
||||
teamName,
|
||||
tournamentId,
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
// TODO: redirect to add players to page
|
||||
return redirect("/");
|
||||
};
|
||||
|
||||
// TODO: redirect if not logged in
|
||||
|
||||
export default function RegisterPage() {
|
||||
const [, parentRoute] = useMatches();
|
||||
const { id } = parentRoute.data as FindTournamentByNameForUrlI;
|
||||
|
||||
return (
|
||||
<div className="tournament__register__container">
|
||||
<h2 className="tournament__register__header">Register now</h2>
|
||||
<div className="tournament__register__content">
|
||||
<Form method="post">
|
||||
<label htmlFor="team-name">Team name</label>
|
||||
<input type="hidden" name="tournament-id" value={id} />
|
||||
<input
|
||||
name="team-name"
|
||||
id="team-name"
|
||||
required
|
||||
minLength={TEAM_NAME_MIN_LENGTH}
|
||||
maxLength={TEAM_NAME_MAX_LENGTH}
|
||||
/>
|
||||
{/* <ErrorMessage error={errors["team-name"]} /> */}
|
||||
<div className="tournament__register__buttons-container">
|
||||
<button type="submit">Submit</button>
|
||||
<button className="outlined" type="button">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useMatches, LinksFunction } from "remix";
|
||||
import type { FindTournamentByNameForUrlI } from "~/services/tournament";
|
||||
import stylesUrl from "~/styles/teams.css";
|
||||
import styles from "~/styles/tournament-teams.css";
|
||||
import { Fragment } from "react";
|
||||
|
||||
export const links: LinksFunction = () => {
|
||||
return [{ rel: "stylesheet", href: stylesUrl }];
|
||||
return [{ rel: "stylesheet", href: styles }];
|
||||
};
|
||||
|
||||
export default function TeamsTab() {
|
||||
|
||||
@@ -91,16 +91,16 @@ function discordInviteToUrl(discordInvite: string) {
|
||||
|
||||
export function createTournamentTeam({
|
||||
userId,
|
||||
name,
|
||||
teamName,
|
||||
tournamentId,
|
||||
}: {
|
||||
userId: number;
|
||||
name: string;
|
||||
teamName: string;
|
||||
tournamentId: number;
|
||||
}) {
|
||||
return prisma.tournamentTeam.create({
|
||||
data: {
|
||||
name: name.trim(),
|
||||
name: teamName.trim(),
|
||||
tournamentId,
|
||||
members: {
|
||||
create: {
|
||||
|
||||
@@ -110,9 +110,8 @@ button.outlined {
|
||||
input:not(.plain) {
|
||||
height: 1rem;
|
||||
padding: var(--s-4) var(--s-3);
|
||||
border: none;
|
||||
border: 1px solid var(--border);
|
||||
background-color: var(--bg-lighter);
|
||||
background-color: transparent;
|
||||
border-radius: var(--rounded);
|
||||
color: var(--text);
|
||||
outline: none;
|
||||
|
||||
22
app/styles/tournament-register.css
Normal file
22
app/styles/tournament-register.css
Normal file
@@ -0,0 +1,22 @@
|
||||
.tournament__register__container {
|
||||
padding-block-start: var(--s-4);
|
||||
}
|
||||
|
||||
.tournament__register__header {
|
||||
margin-top: 0;
|
||||
font-size: var(--fonts-lg);
|
||||
font-weight: var(--bold);
|
||||
list-style: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.tournament__register__content {
|
||||
width: min(24rem, 100%);
|
||||
margin-block-start: var(--s-4);
|
||||
}
|
||||
|
||||
.tournament__register__buttons-container {
|
||||
display: flex;
|
||||
gap: var(--s-2);
|
||||
margin-block-start: var(--s-6);
|
||||
}
|
||||
18
app/utils.ts
18
app/utils.ts
@@ -1,5 +1,23 @@
|
||||
import { json } from "remix";
|
||||
|
||||
export const makeTitle = (endOfTitle: string) => `sendou.ink | ${endOfTitle}`;
|
||||
|
||||
export const requireUser = (ctx: any) => {
|
||||
const user = ctx.user;
|
||||
|
||||
if (!user) {
|
||||
throw json("Log in required", { status: 401 });
|
||||
}
|
||||
|
||||
return user as NonNullable<LoggedInUser>;
|
||||
};
|
||||
|
||||
export type LoggedInUser = {
|
||||
id: number;
|
||||
discordId: string;
|
||||
discordAvatar: string;
|
||||
} | null;
|
||||
|
||||
export type Serialized<T> = {
|
||||
[P in keyof T]: T[P] extends Date ? string : Serialized<T[P]>;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user