mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-09-29 14:50:13 -05:00
Remove old files
This commit is contained in:
@@ -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
3
old/.gitignore
vendored
@@ -1,3 +0,0 @@
|
||||
node_modules
|
||||
.env
|
||||
cypress/videos/*
|
||||
19
old/App.tsx
19
old/App.tsx
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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
10419
old/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
import { createRouter } from "../../utils/trpc-server";
|
||||
|
||||
export const layout = createRouter().query("getLoggedInUser", {
|
||||
resolve: ({ ctx }) => ctx.user,
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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"],
|
||||
},
|
||||
];
|
||||
@@ -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>;
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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" });
|
||||
}
|
||||
@@ -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;
|
||||
}, {});
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
}),
|
||||
});
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
106
old/server.ts
106
old/server.ts
@@ -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();
|
||||
@@ -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;
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
.container {
|
||||
margin-top: var(--s-1);
|
||||
color: var(--theme-error);
|
||||
font-size: var(--fonts-xs);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
}
|
||||
@@ -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)!;
|
||||
}
|
||||
@@ -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"];
|
||||
@@ -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]>;
|
||||
@@ -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);
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export type UseDataResult<Data> = () => Data;
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import { defineConfig } from "vite";
|
||||
import solidPlugin from "vite-plugin-solid";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [solidPlugin()],
|
||||
build: {
|
||||
target: "esnext",
|
||||
polyfillDynamicImport: false,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user