Remove old files

This commit is contained in:
Kalle (Sendou)
2021-11-27 16:10:06 +02:00
parent 4dbc56e98c
commit 76acdb2515
43 changed files with 0 additions and 12389 deletions

View File

@@ -26,7 +26,6 @@ export const links: LinksFunction = () => {
export const loader: LoaderFunction = ({ context }) => {
const { user } = context;
console.log({ user });
return user ?? null;
};

3
old/.gitignore vendored
View File

@@ -1,3 +0,0 @@
node_modules
.env
cypress/videos/*

View File

@@ -1,19 +0,0 @@
import { Router } from "solid-app-router";
import type { Component } from "solid-js";
import { Routes } from "./Routes";
import { Layout } from "./old/scenes/layout/components/Layout";
import { UserProvider } from "./old/utils/UserContext";
const App: Component = () => {
return (
<Router>
<UserProvider>
<Layout>
<Routes />
</Layout>
</UserProvider>
</Router>
);
};
export default App;

View File

@@ -1,31 +0,0 @@
import { Routes as SolidAppRoutes, Route } from "solid-app-router";
import { lazy } from "solid-js";
import { MapPoolTab } from "./old/scenes/tournament/components/MapPoolTab";
import { TeamsTab } from "./old/scenes/tournament/components/TeamsTab";
import TournamentData from "./old/scenes/tournament/TournamentPage.data";
const TournamentsPage = lazy(
() => import("./old/scenes/tournament/components/TournamentPage")
);
export function Routes() {
return (
<SolidAppRoutes>
<Route
path="/to/:organization/:tournament"
element={<TournamentsPage />}
// TODO: fix type error
data={TournamentData as any}
>
<Route path="/map-pool" element={<MapPoolTab />} />
<Route path="/bracket" element={() => <>bracket</>} />
<Route path="/teams" element={<TeamsTab />} />
<Route path="/streams" element={() => <>streams</>} />
<Route path="/*all" element={() => <>overview</>} />
</Route>
<Route path="/links" element={() => <div>links page</div>} />
<Route path="/" element={() => <>home!</>} />
<Route path="/*all" element={() => <>Not found</>} />
</SolidAppRoutes>
);
}

View File

@@ -1,23 +0,0 @@
import s from "../styles/AvatarWithName.module.css";
export function AvatarWithName(p: {
discordName: string;
discordAvatar: string | null;
discordId: string;
}) {
return (
<div class={s.container}>
<div class={s.placeholder}>
{p.discordAvatar && (
<img
alt=""
class={s.avatar}
loading="lazy"
src={`https://cdn.discordapp.com/avatars/${p.discordId}/${p.discordAvatar}.png?size=80`}
/>
)}
</div>
<div>{p.discordName}</div>
</div>
);
}

View File

@@ -1,10 +0,0 @@
import { Show } from "solid-js";
import s from "../styles/ErrorMessage.module.css";
export function ErrorMessage(p: { error?: string }) {
return (
<Show when={p.error}>
<div class={s.container}>{p.error}</div>
</Show>
);
}

View File

@@ -1,16 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<link rel="shortcut icon" type="image/ico" href="/img/layout/logo.webp" />
<title>sendou.ink</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script src="/index.tsx" type="module"></script>
</body>
</html>

View File

@@ -1,7 +0,0 @@
import { render } from "solid-js/web";
import "normalize.css";
import "./styles/global.css";
import App from "./App";
render(() => <App />, document.getElementById("root") as HTMLElement);

10419
old/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,63 +0,0 @@
{
"name": "sendou.ink",
"type": "module",
"version": "0.0.0",
"scripts": {
"dev": "concurrently \"npm run dev:frontend\" \"npm run dev:server\"",
"dev:frontend": "vite",
"dev:server": "node --experimental-specifier-resolution=node --loader ts-node/esm server.ts",
"build:frontend": "vite build",
"migration:create": "npx prisma migrate dev --create-only",
"migration:apply:dev": "npx prisma migrate dev",
"migration:apply:prod": "npx prisma migrate deploy",
"seed": "npx prisma db seed",
"seed:reset": "npx prisma migrate reset --force",
"lint:styles": "stylelint \"**/*.css\"",
"cy:open": "npx cypress open",
"cy:run": "npx cypress run"
},
"license": "MIT",
"dependencies": {
"@prisma/client": "^3.5.0",
"@trpc/client": "^9.14.0",
"@trpc/server": "^9.14.0",
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",
"dotenv": "^10.0.0",
"express": "^4.17.1",
"express-session": "^1.17.2",
"normalize.css": "^8.0.1",
"passport": "^0.5.0",
"passport-discord": "^0.1.4",
"solid-app-router": "^0.1.12",
"solid-js": "^1.2.5",
"superjson": "^1.8.0",
"zod": "^3.11.6"
},
"devDependencies": {
"@types/cookie-parser": "^1.4.2",
"@types/cors": "^2.8.12",
"@types/express": "^4.17.13",
"@types/express-session": "^1.17.4",
"@types/faker": "^5.5.9",
"@types/node": "^16.11.9",
"@types/passport": "^1.0.7",
"@types/passport-discord": "^0.1.5",
"concurrently": "^6.4.0",
"cypress": "^9.0.0",
"faker": "^5.5.3",
"prisma": "^3.5.0",
"stylelint": "^14.1.0",
"stylelint-config-idiomatic-order": "^8.1.0",
"stylelint-config-prettier": "^9.0.3",
"stylelint-config-standard": "^24.0.0",
"stylelint-order": "^5.0.0",
"ts-node": "^10.4.0",
"typescript": "^4.5.2",
"vite": "^2.6.14",
"vite-plugin-solid": "^2.1.2"
},
"prisma": {
"seed": "node --experimental-specifier-resolution=node --loader ts-node/esm prisma/seed.ts"
}
}

View File

@@ -1,5 +0,0 @@
import { createRouter } from "../../utils/trpc-server";
export const layout = createRouter().query("getLoggedInUser", {
resolve: ({ ctx }) => ctx.user,
});

View File

@@ -1,62 +0,0 @@
import prisma from "../../../prisma/client";
import type { Strategy as DiscordStrategy } from "passport-discord";
export async function upsertUser({
loggedInUser,
refreshToken,
}: {
loggedInUser: DiscordStrategy.Profile;
refreshToken: string;
}) {
return prisma.user.upsert({
create: {
discordId: loggedInUser.id,
discordName: loggedInUser.username,
discordDiscriminator: loggedInUser.discriminator,
discordAvatar: loggedInUser.avatar,
discordRefreshToken: refreshToken,
...parseConnections(loggedInUser.connections),
},
update: {
discordName: loggedInUser.username,
discordDiscriminator: loggedInUser.discriminator,
discordAvatar: loggedInUser.avatar,
discordRefreshToken: refreshToken,
...parseConnections(loggedInUser.connections),
},
where: {
discordId: loggedInUser.id,
},
});
}
function parseConnections(
connections: DiscordStrategy.ConnectionInfo[] | undefined
) {
if (!connections) return null;
const result: {
twitch?: string;
twitter?: string;
youtubeId?: string;
youtubeName?: string;
} = {};
for (const connection of connections) {
if (connection.visibility !== 1 || !connection.verified) continue;
switch (connection.type) {
case "twitch":
result.twitch = connection.name;
break;
case "twitter":
result.twitter = connection.name;
break;
case "youtube":
result.youtubeId = connection.id;
result.youtubeName = connection.name;
}
}
return result;
}

View File

@@ -1,57 +0,0 @@
.transformingBurger {
display: flex;
width: 3rem;
height: 3rem;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 0.5rem;
border: 2px solid;
border-color: var(--bg-lighter);
background-color: transparent;
border-radius: var(--rounded);
color: inherit;
cursor: pointer;
gap: 2px;
}
.topLine {
transform: none;
transform-origin: 16px 10px;
transition-duration: 150ms;
transition-property: transform;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}
.topLine[data-expanded="true"] {
transform: translateY(7px) rotate(45deg);
}
.middleLine {
opacity: 1;
transition-duration: 150ms;
transition-property: opacity;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}
.middleLine[data-expanded="true"] {
opacity: 0;
}
.bottomLine {
transform: none;
transform-origin: 16px 22px;
transition-duration: 150ms;
transition-property: transform;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}
.bottomLine[data-expanded="true"] {
transform: translateY(-5px) rotate(-45deg);
}
@media only screen and (min-width: 640px) {
.transformingBurger {
display: none;
}
}

View File

@@ -1,111 +0,0 @@
.layout__header {
--item-size: 3rem;
position: relative;
z-index: 501;
display: grid;
align-items: center;
padding: var(--s-4);
background-color: var(--bg);
grid-template-columns: 1fr 2fr;
}
.layout__header__logo-container {
display: grid;
width: var(--item-size);
min-width: var(--item-size);
height: var(--item-size);
min-height: var(--item-size);
padding: var(--s-1);
background-color: var(--bg-lighter);
background-image: url("/svg/background-pattern.svg");
border-radius: var(--rounded);
cursor: pointer;
justify-self: flex-start;
place-items: center;
}
.layout__logo {
max-width: 100%;
max-height: 100%;
}
.layout__header__search-container {
display: none;
}
.layout__header__right-container {
display: flex;
gap: var(--s-4);
justify-self: flex-end;
}
.layout__nav {
display: none;
justify-content: center;
background-color: var(--bg-lighter);
background-image: url("/svg/background-pattern.svg");
}
.layout__nav__items {
display: inline-flex;
justify-content: center;
padding: var(--s-4) var(--s-8);
background-color: var(--bg-lighter);
gap: var(--s-12);
grid-template-columns: repeat(4, 100px);
}
.layout__nav__items__column {
display: flex;
flex-direction: column;
gap: var(--s-2);
}
.layout__nav__link {
display: flex;
align-items: center;
color: var(--text);
font-size: var(--fonts-sm);
font-weight: var(--bold);
gap: var(--s-2);
text-decoration: none;
text-transform: capitalize;
transition: 0.2s transform;
}
.layout__nav__link:hover {
transform: translateX(2px);
}
.layout__nav__column__title {
color: var(--text-lighter);
font-size: var(--fonts-xxs);
font-weight: var(--bold);
text-transform: uppercase;
}
.layout__nav__link__icon {
width: 2rem;
height: 2rem;
}
.layout__main {
padding-top: var(--s-8);
}
@media screen and (min-width: 640px) {
.header {
--item-size: 38px;
grid-template-columns: 1fr 1fr 1fr;
}
.searchContainer {
display: block;
}
.nav {
display: flex;
}
}

View File

@@ -1,87 +0,0 @@
.container {
position: absolute;
z-index: 500;
bottom: 100vh;
width: 100%;
height: 100%;
padding-bottom: var(--s-6);
background-color: var(--bg-lighter);
background-image: url("/svg/background-pattern.svg");
overflow-y: auto;
/* TODO: same for Safari */
overscroll-behavior: contain;
transition: bottom 0.5s;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}
.container[data-expanded="true"] {
bottom: 0;
}
.mobileNavTopAction {
display: grid;
padding-top: var(--s-24);
padding-bottom: var(--s-8);
background-color: var(--bg);
gap: var(--s-4);
place-items: center;
}
.navIcon {
width: 2rem;
height: 2rem;
}
.mobileNavLinksContainer {
display: grid;
justify-content: center;
grid-auto-columns: 1fr;
grid-auto-rows: 4rem;
}
.mobileNavGroupTitle {
align-self: flex-end;
padding: var(--s-2) var(--s-3);
margin-bottom: var(--s-1);
background-color: var(--bg);
border-radius: var(--rounded);
font-weight: var(--bold);
justify-self: center;
text-transform: uppercase;
}
.mobileNavLink {
display: flex;
align-items: center;
padding: 0 var(--s-2);
border-top: 3px solid var(--bg-lighter);
margin: 0 var(--s-12);
background-color: var(--bg);
color: var(--text);
font-size: var(--fonts-sm);
font-weight: var(--bold);
gap: var(--s-2);
text-decoration: none;
text-transform: capitalize;
}
.mobileNavLink[data-order="first"] {
border-radius: var(--rounded) var(--rounded) 0 0;
}
.mobileNavLink[data-order="last"] {
border-radius: 0 0 var(--rounded) var(--rounded);
}
@media screen and (min-width: 640px) {
.container {
display: none;
}
}
@media screen and (min-width: 480px) {
.mobileNavLink {
margin: 0 var(--s-24);
}
}

View File

@@ -1,35 +0,0 @@
.searchInputContainer {
display: flex;
height: 1rem;
align-items: center;
justify-content: center;
padding: var(--s-5) var(--s-4);
background-color: var(--bg-lighter);
border-radius: var(--rounded);
}
.searchInputContainer:focus-within {
outline: 2px solid var(--theme);
}
.searchInput {
width: 12rem;
height: 2rem;
flex-grow: 1;
border: none;
background-color: var(--bg-lighter);
color: var(---text);
font-size: var(--fonts-sm);
outline: none;
}
.searchInput::placeholder {
color: var(--text-lighter);
font-weight: var(--semi-bold);
letter-spacing: 0.5px;
}
.searchInputIcon {
height: 1.25rem;
color: var(--text);
}

View File

@@ -1,32 +0,0 @@
.logInButton {
display: flex;
height: var(--item-size);
align-items: center;
justify-content: center;
padding: 0.5rem;
border: 2px solid;
border-color: var(--bg-lighter);
background-color: transparent;
border-radius: var(--rounded);
color: inherit;
cursor: pointer;
font-size: var(--fonts-xs);
font-weight: var(--bold);
gap: var(--s-2);
text-decoration: none;
}
.logInButton > svg {
width: 1rem;
}
.logInButton:active {
transform: translateY(1px);
}
.avatar {
width: var(--item-size);
min-width: var(--item-size);
height: var(--item-size);
border-radius: var(--rounded);
}

View File

@@ -1,18 +0,0 @@
export const navItems = [
{
title: "builds",
items: ["browse", "gear", "analyzer"],
},
{
title: "play",
items: ["calendar", "battle", "Rankings"],
},
{
title: "tools",
items: ["planner", "rotations", "top 500"],
},
{
title: "misc",
items: ["badges", "links"],
},
];

View File

@@ -1,19 +0,0 @@
import { createResource } from "solid-js";
import { trpcClient } from "../../utils/trpc-client";
export default function TournamentData({
params,
}: {
params: { organization: string; tournament: string };
}) {
return createResource(
() => [params.organization, params.tournament],
([organization, tournament]) =>
trpcClient.query("tournament.get", {
organization,
tournament,
})
);
}
export type ITournamentData = ReturnType<typeof TournamentData>;

View File

@@ -1,104 +0,0 @@
import { useData } from "solid-app-router";
import { createMemo, createSignal } from "solid-js";
import { ErrorMessage } from "../../../components/ErrorMessage";
import { trpcClient } from "../../../utils/trpc-client";
import { useForm } from "../../../utils/useForm";
import s from "../styles/ActionSection.module.css";
import type { ITournamentData } from "../TournamentPage.data";
const lengthIsCorrect = ({ value }: { value: string }) => {
if (value.length >= 2 && value.length <= 40) {
return;
}
return "Team name has to be between 2 and 40 characters long.";
};
const uniqueName =
(registeredTeamNames: string[]) =>
({ value }: { value: string }) => {
if (!registeredTeamNames.includes(value)) return;
return `There is already a team called "${value}" registered`;
};
// TODO: show login action when not logged in
// TODO: show add members action when captain and not started
// TODO: show leave member action when not captain and not started
// TODO: show checkin action when captain and check-in started
// TODO: show report score and other in-game actions when there is a match ongoing
// TODO: outlined when no crucial action, filled when there is?
export function ActionSection() {
const [expanded, setExpanded] = createSignal(false);
const [teamName, setTeamName] = createSignal("");
const [tournament, { refetch }] = useData<ITournamentData>();
const { validate: _validate, formSubmit: _formSubmit, errors } = useForm();
const fn = async () => {
try {
await trpcClient.mutation("tournament.createTournamentTeam", {
name: teamName(),
tournamentId: tournament()!.id,
});
// TODO: mutate here?
refetch();
setExpanded(false);
} catch (e) {
console.error(e);
// TODO: deal with error
}
};
const registeredTeamNames = createMemo(() =>
(tournament()?.teams ?? []).map((t) => t.name.trim())
);
return (
<div
class={s.container}
classList={{ [s.active]: expanded() }}
onClick={() => setExpanded(true)}
tabindex={!expanded() ? "0" : undefined}
role={!expanded() ? "button" : undefined}
onKeyDown={(e: KeyboardEvent) => {
if (e.key === " " || e.key === "Enter") {
e.preventDefault();
setExpanded(true);
}
}}
>
<div class={s.header}>Register now</div>
{expanded() && (
<div class={s.content} onKeyDown={(e) => e.stopPropagation()}>
{/* @ts-expect-error */}
<form use:_formSubmit={fn}>
<label for="team-name">Team name</label>
{/* TODO: auto focus when opening ActionSection */}
<input
name="team-name"
id="team-name"
required
value={teamName()}
onInput={(e) => setTeamName(e.currentTarget.value)}
// @ts-expect-error
use:_validate={[
lengthIsCorrect,
uniqueName(registeredTeamNames()),
]}
/>
<ErrorMessage error={errors["team-name"]} />
<div class={s.buttonsContainer}>
<button type="submit">Submit</button>
<button
class="outlined"
type="button"
onClick={() => setExpanded(false)}
>
Cancel
</button>
</div>
</form>
</div>
)}
</div>
);
}

View File

@@ -1,84 +0,0 @@
import { Show } from "solid-js";
import { DiscordIcon } from "../../../components/icons/Discord";
import { TwitterIcon } from "../../../components/icons/Twitter";
import type { ITournamentData } from "../TournamentPage.data";
import s from "../styles/InfoBanner.module.css";
import { useData } from "solid-app-router";
export function InfoBanner() {
const [tournament] = useData<ITournamentData>();
// TODO: skeleton
return (
<>
<Show when={tournament()} fallback={null}>
{(tournament) => (
<div
class={s.container}
style={{
"--background": tournament.bannerBackground,
"--text": `hsl(${tournament.bannerTextHSLArgs})`,
"--text-transparent": `hsla(${tournament.bannerTextHSLArgs}, 0.3)`,
}}
>
<div class={s.topRow}>
<div class={s.dateName}>
<div class={s.monthDate}>
<div class={s.month}>
{shortMonthName(tournament.startTime)}
</div>
<div class={s.date}>{dayNumber(tournament.startTime)}</div>
</div>
<div class={s.tournamentName}>{tournament.name}</div>
</div>
<div class={s.iconButtons}>
{tournament.organizer.twitter && (
<a class={s.iconButton} href={tournament.organizer.twitter}>
<TwitterIcon />
</a>
)}
<a
class={s.iconButton}
href={tournament.organizer.discordInvite}
>
<DiscordIcon />
</a>
</div>
</div>
<div class={s.bottomRow}>
<div class={s.infos}>
<div class={s.infoContainer}>
<div class={s.infoLabel}>Starting time</div>
<div>{weekdayAndStartTime(tournament.startTime)}</div>
</div>
<div class={s.infoContainer}>
<div class={s.infoLabel}>Format</div>
<div>Double Elimination</div>
</div>
<div class={s.infoContainer}>
<div class={s.infoLabel}>Organizer</div>
<div>{tournament.organizer.name}</div>
</div>
</div>
</div>
</div>
)}
</Show>
</>
);
}
function weekdayAndStartTime(date: Date) {
return date.toLocaleString("en-US", {
weekday: "long",
hour: "numeric",
});
}
function shortMonthName(date: Date) {
return date.toLocaleString("en-US", { month: "short" });
}
function dayNumber(date: Date) {
return date.toLocaleString("en-US", { day: "numeric" });
}

View File

@@ -1,72 +0,0 @@
import { For, Show } from "solid-js";
import { modesShort, stages } from "../../../utils/constants";
import type { ITournamentData } from "../TournamentPage.data";
import s from "../styles/MapPoolTab.module.css";
import type { Mode } from ".prisma/client";
import { useData } from "solid-app-router";
export function MapPoolTab() {
const [tournament] = useData<ITournamentData>(1);
return (
<Show when={tournament()}>
{({ mapPool }) => (
<div class={s.container}>
<div class={s.infoSquare}>
<span class={s.emphasizedText}>{mapPool.length} maps</span>
</div>
<For each={stages}>
{(stage) => (
<div class={s.stageImageContainer}>
<img
class={s.stageImage}
classList={{ [s.disabled]: !modesPerStage(mapPool)[stage] }}
loading="lazy"
alt={stage}
src={`/img/stages/${stage
.replaceAll(" ", "-")
.toLowerCase()}.webp`}
/>
{modesPerStage(mapPool)[stage] && (
<div class={s.modeImagesContainer}>
<For each={modesShort}>
{(mode) => (
<Show
when={modesPerStage(mapPool)[stage]?.includes(
mode as Mode
)}
>
<img
class={s.modeImage}
src={`/img/modes/${mode}.webp`}
alt={mode}
/>
</Show>
)}
</For>
</div>
)}
</div>
)}
</For>
</div>
)}
</Show>
);
}
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;
}, {});
}

View File

@@ -1,89 +0,0 @@
import { createMemo, For, Show } from "solid-js";
import type { ITournamentData } from "../TournamentPage.data";
import s from "../styles/TeamsTab.module.css";
import { AvatarWithName } from "../../../components/AvatarWithName";
import type { InferQueryOutput } from "../../../utils/trpc-client";
import { useUser } from "../../../utils/UserContext";
import { useData } from "solid-app-router";
const sortCaptainFirst = (a: { captain: boolean }, b: { captain: boolean }) => {
return Number(b.captain) - Number(a.captain);
};
const sortOwnTeamsAndFullTeamsFirst =
(userId?: number) =>
(
a: { members: { member: { id: number } }[] },
b: { members: { member: { id: number } }[] }
) => {
if (userId) {
const aSortValue = Number(
a.members.some(({ member }) => userId === member.id)
);
const bSortValue = Number(
b.members.some(({ member }) => userId === member.id)
);
if (aSortValue !== bSortValue) return bSortValue - aSortValue;
}
const aSortValue = a.members.length >= 4 ? 1 : 0;
const bSortValue = b.members.length >= 4 ? 1 : 0;
return bSortValue - aSortValue;
};
export function TeamsTab() {
const [tournament] = useData<ITournamentData>(1);
const user = useUser();
const sortedTeams = createMemo(() =>
tournament()
?.teams.sort(sortOwnTeamsAndFullTeamsFirst(user()?.id))
.map((team) => {
return {
...team,
members: team.members.sort(sortCaptainFirst),
};
})
);
// TODO: looks kinda bad on mobile
return (
<div class={s.container}>
<Show when={sortedTeams()}>{(teams) => <TeamsList teams={teams} />}</Show>
</div>
);
}
function TeamsList(p: {
teams: NonNullable<InferQueryOutput<"tournament.get">>["teams"];
}) {
if (!p.teams.length) return null;
return (
<div>
<div class={s.teamsContainer}>
<For each={p.teams}>
{(team) => (
<>
<div class={s.teamName}>{team.name}</div>
<div class={s.membersContainer}>
<For each={team.members}>
{(data, i) => (
<div class={s.member}>
<div class={s.orderNumber}>
{data.captain ? "C" : i() + 1}
</div>
<AvatarWithName {...data.member} />
</div>
)}
</For>
</div>
</>
)}
</For>
</div>
</div>
);
}

View File

@@ -1,40 +0,0 @@
import { NavLink, Outlet, useData } from "solid-app-router";
import { InfoBanner } from "./InfoBanner";
import s from "../styles/TournamentPage.module.css";
import type { ITournamentData } from "../TournamentPage.data";
import { Show } from "solid-js";
import { ActionSection } from "./ActionSection";
export default function TournamentsPage() {
const [tournament] = useData<ITournamentData>();
return (
<div class={s.container}>
<InfoBanner />
<ActionSection />
<Show when={tournament()}>
{(tournament) => (
// TODO: don't scroll to top on click
<div style={{ "--tabs-count": 5 }} class={s.linksContainer}>
<NavLink class={s.navLink} href="overview">
Overview
</NavLink>
<NavLink class={s.navLink} href="map-pool">
Map Pool
</NavLink>
<NavLink class={s.navLink} href="bracket">
Bracket
</NavLink>
<NavLink class={s.navLink} href="teams">
Teams ({tournament.teams.length})
</NavLink>
<NavLink class={s.navLink} href="streams">
Streams (4)
</NavLink>
</div>
)}
</Show>
<Outlet />
</div>
);
}

View File

@@ -1,30 +0,0 @@
import { z } from "zod";
import { createRouter } from "../../utils/trpc-server";
import { createTournamentTeam, findTournamentByNameForUrl } from "./service";
export const tournament = createRouter()
.query("get", {
input: z.object({
organization: z.string(),
tournament: z.string(),
}),
resolve: ({ input }) =>
findTournamentByNameForUrl({
organizationNameForUrl: input.organization,
tournamentNameForUrl: input.tournament,
}),
})
.mutation("createTournamentTeam", {
input: z.object({
// TODO: validator
name: z.string(),
tournamentId: z.number(),
}),
// TODO: auth middleware
resolve: ({ input, ctx }) =>
createTournamentTeam({
userId: ctx.user!.id,
name: input.name,
tournamentId: input.tournamentId,
}),
});

View File

@@ -1,107 +0,0 @@
import prisma from "../../../prisma/client";
export async function findTournamentByNameForUrl({
organizationNameForUrl,
tournamentNameForUrl,
}: {
organizationNameForUrl: string;
tournamentNameForUrl: string;
}) {
const tournaments = await prisma.tournament.findMany({
where: {
nameForUrl: tournamentNameForUrl.toLowerCase(),
},
select: {
id: true,
name: true,
description: true,
startTime: true,
checkInTime: true,
bannerBackground: true,
bannerTextHSLArgs: true,
organizer: {
select: {
name: true,
discordInvite: true,
twitter: true,
nameForUrl: true,
},
},
mapPool: {
select: {
mode: true,
name: true,
},
},
teams: {
select: {
checkedIn: true,
id: true,
name: true,
members: {
select: {
captain: true,
member: {
select: {
id: true,
discordAvatar: true,
discordName: true,
discordId: true,
discordDiscriminator: true,
},
},
},
},
},
},
},
});
const result = tournaments.find(
(tournament) =>
tournament.organizer.nameForUrl === organizationNameForUrl.toLowerCase()
);
if (!result) return result;
result.organizer.twitter = twitterToUrl(result.organizer.twitter);
result.organizer.discordInvite = discordInviteToUrl(
result.organizer.discordInvite
);
return result;
}
function twitterToUrl(twitter: string | null) {
if (!twitter) return twitter;
return `https://twitter.com/${twitter}`;
}
function discordInviteToUrl(discordInvite: string) {
return `https://discord.com/invite/${discordInvite}`;
}
export function createTournamentTeam({
userId,
name,
tournamentId,
}: {
userId: number;
name: string;
tournamentId: number;
}) {
return prisma.tournamentTeam.create({
data: {
name: name.trim(),
tournamentId,
members: {
create: {
memberId: userId,
tournamentId,
captain: true,
},
},
},
});
}

View File

@@ -1,58 +0,0 @@
.container {
z-index: 50;
display: flex;
width: 100%;
flex-direction: column;
align-items: center;
padding: var(--s-4);
background-color: var(--theme);
border-radius: var(--rounded);
cursor: pointer;
margin-block-start: var(--s-4);
transition: background-color 0.2s;
}
.container:hover,
.container:focus {
background-color: var(--bg-lighter);
}
.active {
background-color: var(--bg-lighter);
cursor: auto;
}
.header {
font-size: var(--fonts-lg);
font-weight: var(--bold);
list-style: none;
user-select: none;
}
.content {
width: min(24rem, 100%);
margin-block-start: var(--s-4);
}
.buttonsContainer {
display: flex;
gap: var(--s-2);
margin-block-start: var(--s-4);
}
.searchInput {
width: 12rem;
height: 2rem;
flex-grow: 1;
border: none;
background-color: var(--bg-lighter);
color: var(---text);
font-size: var(--fonts-sm);
outline: none;
}
@media screen and (min-width: 768px) {
.container {
width: 48rem;
}
}

View File

@@ -1,109 +0,0 @@
.container {
width: 100%;
padding: var(--s-6);
background: var(--background);
border-radius: var(--rounded);
color: var(--text);
}
.topRow {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
gap: var(--s-4);
}
.dateName {
display: flex;
align-items: center;
gap: var(--s-4);
}
.monthDate {
display: flex;
flex-direction: column;
align-items: center;
line-height: 1.25;
}
.month {
font-size: var(--fonts-xs);
text-transform: uppercase;
}
.date {
font-size: var(--fonts-lg);
font-weight: var(--bold);
}
.tournamentName {
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);
}
.bottomRow {
display: flex;
flex-direction: column;
justify-content: space-between;
gap: var(--s-4);
}
.infos {
display: flex;
flex-wrap: wrap;
gap: var(--s-4);
margin-block-start: var(--s-8);
}
.iconButtons {
display: flex;
gap: var(--s-4);
}
.infoContainer {
font-size: var(--fonts-xs);
}
.infoLabel {
font-weight: var(--extra-bold);
}
.iconButton {
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;
}
.iconButton:active {
transform: translateY(1px);
}
@media screen and (min-width: 480px) {
.infos {
gap: var(--s-8);
}
}
@media screen and (min-width: 640px) {
.bottomRow {
flex-direction: row;
align-items: flex-end;
}
}
@media screen and (min-width: 768px) {
.container {
width: 48rem;
}
}

View File

@@ -1,62 +0,0 @@
.infoSquare {
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;
}
.container {
display: grid;
gap: 1rem;
grid-template-columns: 1fr 1fr;
}
.emphasizedText {
font-weight: var(--bold);
}
.stageImageContainer {
position: relative;
}
.stageImage {
width: 100%;
height: 100%;
border-radius: var(--rounded);
}
.modeImage {
width: 1.5rem;
height: 1.5rem;
}
.disabled {
filter: grayscale(100%);
}
.modeImagesContainer {
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) {
.container {
grid-template-columns: 1fr 1fr;
}
}
@media screen and (min-width: 640px) {
.container {
grid-template-columns: repeat(4, minmax(1px, 200px));
}
}

View File

@@ -1,61 +0,0 @@
.container {
display: flex;
flex-direction: column;
gap: var(--s-10);
}
.teamsContainer {
display: grid;
column-gap: var(--s-6);
grid-template-columns: 1fr;
row-gap: var(--s-10);
}
.teamName {
display: flex;
align-self: center;
border-block-end: 2px solid var(--theme);
font-size: var(--fonts-lg);
font-weight: var(--extra-bold);
justify-self: center;
}
.membersContainer {
display: flex;
flex-direction: column;
gap: var(--s-4);
}
.member {
display: flex;
align-items: center;
gap: var(--s-3);
}
.orderNumber {
color: var(--theme-secondary);
font-size: var(--fonts-sm);
font-weight: bold;
}
.teamListTitle {
margin-block-end: var(--s-8);
margin-block-start: 0;
text-align: center;
}
@media screen and (min-width: 640px) {
.teamsContainer {
grid-template-columns: 1fr 1fr;
}
.teamName {
border-bottom: none;
justify-self: flex-end;
}
.membersContainer {
border-inline-start: 2px solid var(--theme);
padding-inline-start: var(--s-6);
}
}

View File

@@ -1,54 +0,0 @@
.container {
display: flex;
flex-direction: column;
align-items: center;
}
.linksContainer {
display: grid;
justify-content: center;
gap: var(--s-10);
grid-template-columns: repeat(2, 100px);
padding-block: var(--s-8);
place-items: center;
}
.navLink {
all: unset;
border-radius: var(--rounded);
cursor: pointer;
font-size: var(--fonts-sm);
}
.navLink::after {
display: block;
width: 1.25rem;
height: 3px;
border-bottom: 3px solid transparent;
content: "";
}
.navLink:hover::after {
border-color: var(--theme-transparent);
}
.navLink:global(.active) {
font-weight: bold;
}
.navLink:global(.active)::after {
width: 1.25rem;
border-color: var(--theme);
}
@media screen and (min-width: 480px) {
.linksContainer {
grid-template-columns: repeat(3, 100px);
}
}
@media screen and (min-width: 640px) {
.linksContainer {
grid-template-columns: repeat(var(--tabs-count), 100px);
}
}

View File

@@ -1,106 +0,0 @@
import { config } from "dotenv";
config();
import cors from "cors";
import express from "express";
import session from "express-session";
import cookieParser from "cookie-parser";
import passport from "passport";
import { Strategy as DiscordStrategy } from "passport-discord";
import { tournament as tournamentRouter } from "./old/scenes/tournament/router";
import { layout as layoutRouter } from "./old/scenes/layout/router";
import { createContext, createRouter } from "./old/utils/trpc-server";
import * as trpcExpress from "@trpc/server/adapters/express/dist/trpc-server-adapters-express.cjs";
import { upsertUser } from "./old/scenes/layout/service";
const PORT = 3001;
export const appRouter = createRouter()
.merge("tournament.", tournamentRouter)
.merge("layout.", layoutRouter);
export type AppRouter = typeof appRouter;
async function main() {
const app = express();
if (
process.env.DISCORD_CLIENT_ID &&
process.env.DISCORD_CLIENT_SECRET &&
process.env.DISCORD_CALLBACK_URL &&
process.env.COOKIE_SECRET
) {
passport.use(
new DiscordStrategy(
{
clientID: process.env.DISCORD_CLIENT_ID,
clientSecret: process.env.DISCORD_CLIENT_SECRET,
callbackURL: process.env.DISCORD_CALLBACK_URL,
scope: ["identify", "connections"],
},
function (_accessToken, refreshToken, loggedInUser, cb) {
upsertUser({ loggedInUser, refreshToken })
.then((user) => {
return cb(null, {
id: user.id,
discordId: user.discordId,
discordAvatar: user.discordAvatar,
});
})
.catch((err) => cb(err));
}
)
);
passport.serializeUser(function (user, done) {
done(null, user);
});
passport.deserializeUser(function (user, done) {
// @ts-expect-error it is guaranteed it's of a certain shape without an extra check
done(null, user);
});
app.use(cookieParser());
app.use(
session({
secret: process.env.COOKIE_SECRET,
resave: true,
saveUninitialized: true,
})
);
app.use(passport.initialize());
app.use(passport.session());
app.post("/auth/discord", passport.authenticate("discord"));
app.get(
"/auth/discord/callback",
passport.authenticate("discord", {
failureRedirect: "/login",
successRedirect: process.env.FRONTEND_URL,
})
);
} else {
console.warn("Logging in not set up because of missing env vars");
}
app.use(cors({ origin: process.env.FRONTEND_URL, credentials: true }));
app.use((req, _res, next) => {
console.log("⬅️ ", req.method, req.path, req.body ?? req.query);
next();
});
app.use(
"/trpc",
trpcExpress.createExpressMiddleware({
router: appRouter,
createContext,
})
);
app.listen(PORT, () => {
console.log(`listening on port ${PORT}`);
});
}
main();

View File

@@ -1,21 +0,0 @@
.container {
display: flex;
align-items: center;
font-weight: var(--semi-bold);
gap: var(--s-3);
}
.avatar {
block-size: 2.5rem;
border-radius: var(--rounded);
filter: none;
inline-size: 2.5rem;
text-indent: -10000px;
}
.placeholder {
background-image: url("/svg/background-pattern.svg");
block-size: 2.5rem;
border-radius: var(--rounded);
inline-size: 2.5rem;
}

View File

@@ -1,5 +0,0 @@
.container {
margin-top: var(--s-1);
color: var(--theme-error);
font-size: var(--fonts-xs);
}

View File

@@ -1,143 +0,0 @@
:root {
--bg: hsl(237.3deg 42.3% 30.6%);
--bg-lighter: hsl(237.3deg 42.3% 35.6%);
--border: hsl(237.3deg 42.3% 45.6%);
--text: rgb(255 255 255 / 95%);
--text-lighter: rgb(255 255 255 / 55%);
--theme-error: rgb(219 70 65);
--theme: hsl(255deg 66.7% 55.3%);
--theme-transparent: hsl(255deg 66.7% 55.3% / 40%);
--theme-secondary: hsl(85deg 66.7% 55.3%);
--rounded: 16px;
--rounded-sm: 10px;
--fonts-xl: 1.5rem;
--fonts-lg: 1.2rem;
--fonts-md: 1rem;
--fonts-sm: 0.9rem;
--fonts-xs: 0.8rem;
--fonts-xxs: 0.7rem;
--extra-bold: 700;
--bold: 600;
--semi-bold: 500;
--body: 400;
--s-1: 0.25rem;
--s-1-5: 0.375rem;
--s-2: 0.5rem;
--s-2-5: 0.625rem;
--s-3: 0.75rem;
--s-4: 1rem;
--s-5: 1.25rem;
--s-6: 1.5rem;
--s-7: 1.75rem;
--s-8: 2rem;
--s-9: 2.25rem;
--s-10: 2.5rem;
--s-11: 2.75rem;
--s-12: 3rem;
--s-14: 3.5rem;
--s-16: 4rem;
--s-20: 5rem;
--s-24: 6rem;
--s-28: 7rem;
--s-32: 8rem;
--s-40: 10rem;
--s-48: 12rem;
--s-56: 14rem;
--s-64: 16rem;
--s-72: 18rem;
--s-80: 20rem;
--s-96: 2rem;
}
/* xs: "(min-width: 480px)",
sm: "(min-width: 640px)",
md: "(min-width: 768px)",
lg: "(min-width: 1024px)",
xl: "(min-width: 1280px)", */
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
background-color: var(--bg);
color: var(--text);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial,
sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: antialiased;
line-height: 1.55;
}
*:focus:not(:focus-visible) {
outline: none !important;
}
button {
display: inline-flex;
width: auto;
align-items: center;
justify-content: center;
border: 2px solid var(--theme);
appearance: none;
background: var(--theme);
border-radius: var(--rounded-sm);
color: var(--text);
cursor: pointer;
font-size: var(--fonts-sm);
font-weight: var(--bold);
line-height: 1.2;
outline-offset: 2px;
padding-block: var(--s-1-5);
padding-inline: var(--s-2-5);
user-select: none;
}
button:focus-visible {
outline: 2px solid var(--theme);
}
button:active {
transform: translateY(1px);
}
button.outlined {
background-color: transparent;
}
input:not(.plain) {
height: 1rem;
padding: var(--s-4) var(--s-3);
border: none;
border: 1px solid var(--border);
background-color: var(--bg-lighter);
border-radius: var(--rounded);
color: var(--text);
outline: none;
}
input:not(.plain):focus-within {
border-color: transparent;
outline: 2px solid var(--theme);
}
input:not(.plain)::placeholder {
color: var(--text-lighter);
font-weight: var(--semi-bold);
letter-spacing: 0.5px;
}
input:not(.plain).error {
border-color: transparent;
outline: 2px solid var(--theme-error);
}
label {
display: block;
color: var(--text-lighter);
font-size: var(--fonts-xs);
font-weight: var(--bold);
margin-block-end: var(--s-1);
}

View File

@@ -1,17 +0,0 @@
{
"compilerOptions": {
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"jsx": "preserve",
"jsxImportSource": "solid-js",
"types": ["vite/client", "cypress"]
}
}

View File

@@ -1,35 +0,0 @@
import {
createContext,
useContext,
createResource,
JSXElement,
Resource,
} from "solid-js";
import { trpcClient } from "./trpc-client";
const fetchLoggedInUser = async () => {
return trpcClient.query("layout.getLoggedInUser");
};
type LoggedInUserResource = Resource<
| {
id: number;
discordId: string;
discordAvatar?: string | undefined;
}
| undefined
>;
const UserContext = createContext<LoggedInUserResource>();
export function UserProvider(props: { children: JSXElement }) {
const [user] = createResource(fetchLoggedInUser);
return (
<UserContext.Provider value={user}>{props.children}</UserContext.Provider>
);
}
export function useUser(): LoggedInUserResource {
return useContext(UserContext)!;
}

View File

@@ -1,27 +0,0 @@
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"];

View File

@@ -1,24 +0,0 @@
import { createTRPCClient } from "@trpc/client";
import type { inferProcedureOutput } from "@trpc/server";
import type { AppRouter } from "../../server";
import superjson from "superjson";
// TODO: transformer superjson
export const trpcClient = createTRPCClient<AppRouter>({
url: `${import.meta.env.VITE_BACKEND_URL}/trpc`,
fetch: (input, init) => fetch(input, { ...init, credentials: "include" }),
transformer: superjson,
});
// export const trpcClient = createTRPCClient<AppRouter>({
// fetch: (input, init) => fetch(input, { ...init, credentials: "include" }),
// links: [
// httpLink({
// url: `${import.meta.env.VITE_BACKEND_URL}/trpc`,
// }),
// ],
// });
export type InferQueryOutput<
TRouteKey extends keyof AppRouter["_def"]["queries"]
> = inferProcedureOutput<AppRouter["_def"]["queries"][TRouteKey]>;

View File

@@ -1,30 +0,0 @@
import * as trpc from "@trpc/server";
import * as trpcExpress from "@trpc/server/adapters/express/dist/trpc-server-adapters-express.cjs";
import superjson from "superjson";
export const createContext = ({
req,
res,
}: trpcExpress.CreateExpressContextOptions) => {
const getUser = () => {
if (!req.user) {
return;
}
return req.user as {
id: number;
discordId: string;
discordAvatar?: string;
};
};
return {
req,
res,
user: getUser(),
};
};
type Context = trpc.inferAsyncReturnType<typeof createContext>;
export function createRouter() {
return trpc.router<Context>().transformer(superjson);
}

View File

@@ -1 +0,0 @@
export type UseDataResult<Data> = () => Data;

View File

@@ -1,82 +0,0 @@
// https://www.solidjs.com/examples/forms
import { createStore, SetStoreFunction } from "solid-js/store";
type FormFieldElement = HTMLInputElement;
type Validator = (el: FormFieldElement) => string | undefined;
type FormFieldAccessor = () => Validator | undefined;
type FormAccessor = () => (el: HTMLFormElement) => void;
type ElementAndValidators = {
element: FormFieldElement;
validators: Validator[];
};
const INPUT_ERROR_CLASSNAME = "error";
function checkValid(
{
element,
validators = [],
}: { element: FormFieldElement; validators: Array<Validator> },
setErrors: SetStoreFunction<{}>
) {
return async () => {
element.setCustomValidity("");
element.checkValidity();
let message = element.validationMessage;
if (!message) {
for (const validator of validators) {
const text = validator(element);
if (text) {
element.setCustomValidity(text);
break;
}
}
message = element.validationMessage;
}
if (message) {
element.classList.toggle(INPUT_ERROR_CLASSNAME, true);
setErrors({ [element.name]: message });
}
};
}
export function useForm() {
const [errors, setErrors] = createStore<Record<string, string>>({});
const fields: Record<
string,
{ element: FormFieldElement; validators: Validator[] }
> = {};
const validate = (ref: FormFieldElement, accessor: FormFieldAccessor) => {
const validators = (accessor() || []) as Validator[];
let config: ElementAndValidators;
fields[ref.name] = config = { element: ref, validators };
ref.onblur = checkValid(config, setErrors);
ref.oninput = () => {
if (!errors[ref.name]) return;
setErrors({ [ref.name]: undefined });
ref.classList.toggle(INPUT_ERROR_CLASSNAME, false);
};
};
const formSubmit = (ref: HTMLFormElement, accessor: FormAccessor) => {
const callback = accessor() || (() => {});
ref.setAttribute("novalidate", "");
ref.onsubmit = async (e) => {
e.preventDefault();
let errored = false;
for (const k in fields) {
const field = fields[k];
await checkValid(field, setErrors)();
if (!errored && field.element.validationMessage) {
field.element.focus();
errored = true;
}
}
!errored && callback(ref);
};
};
return { validate, formSubmit, errors };
}

View File

@@ -1,10 +0,0 @@
import { defineConfig } from "vite";
import solidPlugin from "vite-plugin-solid";
export default defineConfig({
plugins: [solidPlugin()],
build: {
target: "esnext",
polyfillDynamicImport: false,
},
});