mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-03-21 18:04:39 -05:00
Port tournaments with map pool
This commit is contained in:
parent
7463c2a02c
commit
dff33033b3
|
|
@ -1,13 +1,20 @@
|
|||
// TODO: 404 page that shows other tournaments by the organization
|
||||
|
||||
import type { MetaFunction, LoaderFunction } from "remix";
|
||||
import { useLoaderData, json, Link } from "remix";
|
||||
import { MetaFunction, LoaderFunction, LinksFunction, NavLink } from "remix";
|
||||
import { useLoaderData, Outlet } from "remix";
|
||||
import invariant from "tiny-invariant";
|
||||
import { DiscordIcon } from "~/components/icons/Discord";
|
||||
import { TwitterIcon } from "~/components/icons/Twitter";
|
||||
import { makeTitle } from "~/utils";
|
||||
import {
|
||||
findTournamentByNameForUrl,
|
||||
FindTournamentByNameForUrlI,
|
||||
} from "../../../services/tournament";
|
||||
import tournamentStylesUrl from "../../styles/tournament.css";
|
||||
|
||||
export const links: LinksFunction = () => {
|
||||
return [{ rel: "stylesheet", href: tournamentStylesUrl }];
|
||||
};
|
||||
|
||||
export const loader: LoaderFunction = ({ params }) => {
|
||||
invariant(
|
||||
|
|
@ -34,9 +41,124 @@ export const meta: MetaFunction = (props) => {
|
|||
};
|
||||
};
|
||||
|
||||
// https://remix.run/guides/routing#index-routes
|
||||
export default function Index() {
|
||||
export default function TournamentPage() {
|
||||
const data = useLoaderData<FindTournamentByNameForUrlI>();
|
||||
|
||||
return <div className="remix__page">hello</div>;
|
||||
return (
|
||||
// <div className={s.container}>
|
||||
<div className="tournament__container">
|
||||
<InfoBanner />
|
||||
{/* <ActionSection /> */}
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
export function InfoBanner() {
|
||||
const data = useLoaderData<FindTournamentByNameForUrlI>();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="info-banner"
|
||||
style={
|
||||
{
|
||||
"--background": data.bannerBackground,
|
||||
// TODO: do this on backend
|
||||
"--text": `hsl(${data.bannerTextHSLArgs})`,
|
||||
// TODO: and this
|
||||
"--text-transparent": `hsla(${data.bannerTextHSLArgs}, 0.3)`,
|
||||
} as Record<string, string>
|
||||
}
|
||||
>
|
||||
<div className="info-banner__top-row">
|
||||
<div className="info-banner__top-row__date-name">
|
||||
<div className="info-banner__top-row__month-date">
|
||||
<div className="info-banner__top-row__month-date__month">
|
||||
{shortMonthName(data.startTime)}
|
||||
</div>
|
||||
<div className="info-banner__top-row__month-date__date">
|
||||
{dayNumber(data.startTime)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="info-banner__top-row__tournament-name">
|
||||
{data.name}
|
||||
</div>
|
||||
</div>
|
||||
<div className="info-banner__icon-buttons-container">
|
||||
{data.organizer.twitter && (
|
||||
<a
|
||||
className="info-banner__icon-button"
|
||||
href={data.organizer.twitter}
|
||||
>
|
||||
<TwitterIcon />
|
||||
</a>
|
||||
)}
|
||||
<a
|
||||
className="info-banner__icon-button"
|
||||
href={data.organizer.discordInvite}
|
||||
>
|
||||
<DiscordIcon />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="info-banner__bottom-row">
|
||||
<div className="info-banner__bottom-row__infos">
|
||||
<div className="info-banner__bottom-row__info-container">
|
||||
<div className="info-banner__bottom-row__info-label">
|
||||
Starting time
|
||||
</div>
|
||||
<div>{weekdayAndStartTime(data.startTime)}</div>
|
||||
</div>
|
||||
<div className="info-banner__bottom-row__info-container">
|
||||
<div className="info-banner__bottom-row__info-label">Format</div>
|
||||
<div>Double Elimination</div>
|
||||
</div>
|
||||
<div className="info-banner__bottom-row__info-container">
|
||||
<div className="info-banner__bottom-row__info-label">
|
||||
Organizer
|
||||
</div>
|
||||
<div>{data.organizer.name}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: https://github.com/remix-run/remix/issues/656
|
||||
function weekdayAndStartTime(date: string) {
|
||||
return new Date(date).toLocaleString("en-US", {
|
||||
weekday: "long",
|
||||
hour: "numeric",
|
||||
});
|
||||
}
|
||||
|
||||
function shortMonthName(date: string) {
|
||||
return new Date(date).toLocaleString("en-US", { month: "short" });
|
||||
}
|
||||
|
||||
function dayNumber(date: string) {
|
||||
return new Date(date).toLocaleString("en-US", { day: "numeric" });
|
||||
}
|
||||
|
|
|
|||
68
app/routes/to/$organization.$tournament/map-pool.tsx
Normal file
68
app/routes/to/$organization.$tournament/map-pool.tsx
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
import mapPoolStylesUrl from "~/styles/map-pool.css";
|
||||
import type { Mode } from ".prisma/client";
|
||||
import classNames from "classnames";
|
||||
import { modesShort, stages } from "~/utils";
|
||||
import { LinksFunction, useMatches } from "remix";
|
||||
import { FindTournamentByNameForUrlI } from "../../../../services/tournament";
|
||||
|
||||
export const links: LinksFunction = () => {
|
||||
return [{ rel: "stylesheet", href: mapPoolStylesUrl }];
|
||||
};
|
||||
|
||||
export default function MapPoolTab() {
|
||||
const [, parentRoute] = useMatches();
|
||||
const { mapPool } = parentRoute.data as FindTournamentByNameForUrlI;
|
||||
|
||||
return (
|
||||
<div className="map-pool">
|
||||
<div className="map-pool__info-square">
|
||||
<span className="map-pool__info-square__text">
|
||||
{mapPool.length} maps
|
||||
</span>
|
||||
</div>
|
||||
{stages.map((stage) => (
|
||||
<div key={stage} className="map-pool__stage-images-container">
|
||||
<img
|
||||
className={classNames("map-pool__stage-image", {
|
||||
"map-pool__stage-image-disabled": !modesPerStage(mapPool)[stage],
|
||||
})}
|
||||
loading="lazy"
|
||||
alt={stage}
|
||||
src={`/img/stages/${stage.replaceAll(" ", "-").toLowerCase()}.webp`}
|
||||
/>
|
||||
{modesPerStage(mapPool)[stage] && (
|
||||
<div className="map-pool__mode-images-container">
|
||||
{modesShort.map(
|
||||
(mode) =>
|
||||
modesPerStage(mapPool)[stage]?.includes(mode as Mode) && (
|
||||
<img
|
||||
key={mode}
|
||||
className="map-pool__mode-image"
|
||||
src={`/img/modes/${mode}.webp`}
|
||||
alt={mode}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function modesPerStage(
|
||||
mapPool: {
|
||||
name: string;
|
||||
mode: Mode;
|
||||
}[]
|
||||
) {
|
||||
return mapPool.reduce((acc: Record<string, Mode[]>, { name, mode }) => {
|
||||
if (!acc[name]) {
|
||||
acc[name] = [];
|
||||
}
|
||||
|
||||
acc[name].push(mode);
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
62
app/styles/map-pool.css
Normal file
62
app/styles/map-pool.css
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
.map-pool__info-square {
|
||||
display: grid;
|
||||
background-color: var(--bg-lighter);
|
||||
background-image: url("/svg/background-pattern.svg");
|
||||
border-radius: var(--rounded);
|
||||
font-size: var(--fonts-xl);
|
||||
font-weight: var(--semi-bold);
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
.map-pool {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
.map-pool__info-square__text {
|
||||
font-weight: var(--bold);
|
||||
}
|
||||
|
||||
.map-pool__stage-images-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.map-pool__stage-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: var(--rounded);
|
||||
}
|
||||
|
||||
.map-pool__mode-image {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
}
|
||||
|
||||
.map-pool__stage-image-disabled {
|
||||
filter: grayscale(100%);
|
||||
}
|
||||
|
||||
.map-pool__mode-images-container {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
padding: var(--s-1);
|
||||
backdrop-filter: blur(5px) grayscale(25%);
|
||||
border-end-end-radius: var(--rounded);
|
||||
border-start-start-radius: var(--rounded);
|
||||
gap: var(--s-2);
|
||||
inset-block-start: 0;
|
||||
inset-inline-start: 0;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 480px) {
|
||||
.map-pool {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 640px) {
|
||||
.map-pool {
|
||||
grid-template-columns: repeat(4, minmax(1px, 200px));
|
||||
}
|
||||
}
|
||||
160
app/styles/tournament.css
Normal file
160
app/styles/tournament.css
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
.tournament__container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tournament__links-container {
|
||||
display: grid;
|
||||
justify-content: center;
|
||||
gap: var(--s-10);
|
||||
grid-template-columns: repeat(2, 100px);
|
||||
padding-block: var(--s-8);
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
.tournament__nav-link {
|
||||
all: unset;
|
||||
border-radius: var(--rounded);
|
||||
cursor: pointer;
|
||||
font-size: var(--fonts-sm);
|
||||
}
|
||||
|
||||
.tournament__nav-link::after {
|
||||
display: block;
|
||||
width: 1.25rem;
|
||||
height: 3px;
|
||||
border-bottom: 3px solid transparent;
|
||||
content: "";
|
||||
}
|
||||
|
||||
.tournament__nav-link:hover::after {
|
||||
border-color: var(--theme-transparent);
|
||||
}
|
||||
|
||||
.tournament__nav-link.active {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.tournament__nav-link.active::after {
|
||||
width: 1.25rem;
|
||||
border-color: var(--theme);
|
||||
}
|
||||
|
||||
.info-banner {
|
||||
width: 100%;
|
||||
padding: var(--s-6);
|
||||
background: var(--background);
|
||||
border-radius: var(--rounded);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.info-banner__top-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
gap: var(--s-4);
|
||||
}
|
||||
|
||||
.info-banner__top-row__date-name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--s-4);
|
||||
}
|
||||
|
||||
.info-banner__top-row__month-date {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.info-banner__top-row__month-date__month {
|
||||
font-size: var(--fonts-xs);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.info-banner__top-row__month-date__date {
|
||||
font-size: var(--fonts-lg);
|
||||
font-weight: var(--bold);
|
||||
}
|
||||
|
||||
.info-banner__top-row__tournament-name {
|
||||
border-color: var(--text);
|
||||
border-inline-start: 1px solid;
|
||||
font-size: var(--fonts-xl);
|
||||
font-weight: var(--extra-bold);
|
||||
padding-inline-start: var(--s-4);
|
||||
}
|
||||
|
||||
.info-banner__bottom-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
gap: var(--s-4);
|
||||
}
|
||||
|
||||
.info-banner__bottom-row__infos {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--s-4);
|
||||
margin-block-start: var(--s-8);
|
||||
}
|
||||
|
||||
.info-banner__icon-buttons-container {
|
||||
display: flex;
|
||||
gap: var(--s-4);
|
||||
}
|
||||
|
||||
.info-banner__bottom-row__info-container {
|
||||
font-size: var(--fonts-xs);
|
||||
}
|
||||
|
||||
.info-banner__bottom-row__info-label {
|
||||
font-weight: var(--extra-bold);
|
||||
}
|
||||
|
||||
.info-banner__icon-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid;
|
||||
border-color: var(--text-transparent);
|
||||
block-size: 2.25rem;
|
||||
border-radius: 50%;
|
||||
color: inherit;
|
||||
inline-size: 2.25rem;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
.info-banner__icon-button:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
@media screen and (min-width: 480px) {
|
||||
.tournament__links-container {
|
||||
grid-template-columns: repeat(3, 100px);
|
||||
}
|
||||
|
||||
.info-banner__bottom-row__infos {
|
||||
gap: var(--s-8);
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 640px) {
|
||||
.tournament__links-container {
|
||||
grid-template-columns: repeat(var(--tabs-count), 100px);
|
||||
}
|
||||
|
||||
.info-banner__bottom-row {
|
||||
flex-direction: row;
|
||||
align-items: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.info-banner {
|
||||
width: 48rem;
|
||||
}
|
||||
}
|
||||
35
app/utils.ts
35
app/utils.ts
|
|
@ -1,3 +1,31 @@
|
|||
export const stages = [
|
||||
"The Reef",
|
||||
"Musselforge Fitness",
|
||||
"Starfish Mainstage",
|
||||
"Humpback Pump Track",
|
||||
"Inkblot Art Academy",
|
||||
"Sturgeon Shipyard",
|
||||
"Moray Towers",
|
||||
"Port Mackerel",
|
||||
"Manta Maria",
|
||||
"Kelp Dome",
|
||||
"Snapper Canal",
|
||||
"Blackbelly Skatepark",
|
||||
"MakoMart",
|
||||
"Walleye Warehouse",
|
||||
"Shellendorf Institute",
|
||||
"Arowana Mall",
|
||||
"Goby Arena",
|
||||
"Piranha Pit",
|
||||
"Camp Triggerfish",
|
||||
"Wahoo World",
|
||||
"New Albacore Hotel",
|
||||
"Ancho-V Games",
|
||||
"Skipper Pavilion",
|
||||
];
|
||||
|
||||
export const modesShort = ["TW", "SZ", "TC", "RM", "CB"];
|
||||
|
||||
export const navItems = [
|
||||
{
|
||||
title: "builds",
|
||||
|
|
@ -18,3 +46,10 @@ export const navItems = [
|
|||
];
|
||||
|
||||
export const makeTitle = (endOfTitle: string) => `sendou.ink | ${endOfTitle}`;
|
||||
|
||||
export type Serialized<T> = {
|
||||
[P in keyof T]: T[P] extends Date ? string : Serialized<T[P]>;
|
||||
};
|
||||
|
||||
// TODO:
|
||||
// export type InferredSerializedAPI<T> = Serialized<Prisma.PromiseReturnType<T>>;
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import { Prisma } from ".prisma/client";
|
||||
import { json } from "@remix-run/server-runtime";
|
||||
import { json } from "remix";
|
||||
import { Serialized } from "~/utils";
|
||||
import prisma from "../prisma/client";
|
||||
|
||||
export type FindTournamentByNameForUrlI = Prisma.PromiseReturnType<
|
||||
typeof findTournamentByNameForUrl
|
||||
export type FindTournamentByNameForUrlI = Serialized<
|
||||
Prisma.PromiseReturnType<typeof findTournamentByNameForUrl>
|
||||
>;
|
||||
|
||||
export async function findTournamentByNameForUrl({
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"include": ["remix.env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||
"compilerOptions": {
|
||||
"lib": ["DOM", "DOM.Iterable", "ES2019"],
|
||||
"lib": ["DOM", "DOM.Iterable", "ES2019", "es2021"],
|
||||
"esModuleInterop": true,
|
||||
"jsx": "react-jsx",
|
||||
"moduleResolution": "node",
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user