Switch to Solid.js
|
|
@ -1 +0,0 @@
|
|||
.next
|
||||
|
|
@ -1 +0,0 @@
|
|||
{}
|
||||
7
.vscode/shared.code-snippets
vendored
|
|
@ -1,7 +0,0 @@
|
|||
{
|
||||
"Styled component (Stitches)": {
|
||||
"prefix": "st",
|
||||
"body": ["const S_$1 = styled(\"$2\", {", " $0", "});"],
|
||||
"scope": "typescriptreact"
|
||||
}
|
||||
}
|
||||
13
App.tsx
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { Router } from "solid-app-router";
|
||||
import type { Component } from "solid-js";
|
||||
import Layout from "./scenes/layout";
|
||||
|
||||
const App: Component = () => {
|
||||
return (
|
||||
<Router>
|
||||
<Layout>routes here</Layout>
|
||||
</Router>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
57
README.md
|
|
@ -1,57 +0,0 @@
|
|||
# sendou.ink (Splatoon 3)
|
||||
|
||||
Next version of sendou.ink
|
||||
|
||||
⚠️ This branch is still experimental and likely to have very big changes quickly
|
||||
|
||||
## Getting started
|
||||
|
||||
> If these instructions are wrong please make an issue and let's fix them
|
||||
|
||||
Prerequisites: [Node.js](https://nodejs.org/en/) & PostgreSQL running locally ([guide](https://www.prisma.io/dataguide/postgresql/setting-up-a-local-postgresql-database))
|
||||
|
||||
You might be able to skip steps 2-4 and use a few pages but most pages need a server with database connected.
|
||||
|
||||
1. Install dependencies with running `npm install` command in the root folder
|
||||
2. Make a copy of the .env.sample file in the /server folder and name it .env
|
||||
|
||||
- See below for documentation about what the values mean and which are optional
|
||||
|
||||
3. In the /server folder seed the database with the `npm run seed` command
|
||||
4. In the /server folder run the development server with the `npm run dev` command
|
||||
5. Make a copy of the .env.sample file in the /frontend folder and name it .env.local
|
||||
|
||||
- See below for documentation about what the values mean and which are optional
|
||||
|
||||
6. In the /frontend folder run the app with the `npm run dev` command
|
||||
|
||||
## .env
|
||||
|
||||
### /frontend
|
||||
|
||||
| Name | Description | Required |
|
||||
| ----------------------- | ------------------------- | -------- |
|
||||
| NEXT_PUBLIC_BACKEND_URL | Where backend is located. | Yes |
|
||||
|
||||
## Folder structure
|
||||
|
||||
### /api
|
||||
|
||||
TypeScript types that acts as a pact between frontend and backend.
|
||||
|
||||
### /frontend
|
||||
|
||||
This folder contains frontend specific code.
|
||||
|
||||
```
|
||||
frontend/
|
||||
├── __mocks__/ mocks for tests
|
||||
├── assets/ images and other assets to be imported directly to code
|
||||
├── components/ react components
|
||||
├── hooks/ react hooks including data fetching logic
|
||||
└── utils/ frontend specific utility methods, constants etc.
|
||||
```
|
||||
|
||||
### /shared
|
||||
|
||||
Utilities, constants etc. that are shared between frontend and backend.
|
||||
|
|
@ -1 +0,0 @@
|
|||
export type Mode = "TW" | "SZ" | "TC" | "RM" | "CB";
|
||||
|
|
@ -1 +0,0 @@
|
|||
export * from "./tournaments.api";
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
{
|
||||
"name": "@sendou-ink/api",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
import type { Mode } from "./common";
|
||||
|
||||
export interface GetTournamentByOrganizationAndName {
|
||||
name: string;
|
||||
description: string | null;
|
||||
startTime: Date;
|
||||
checkInTime: Date;
|
||||
bannerBackground: string;
|
||||
bannerTextHSLArgs: string;
|
||||
organizer: {
|
||||
name: string;
|
||||
discordInvite: string;
|
||||
twitter: string | null;
|
||||
nameForUrl: string;
|
||||
};
|
||||
mapPool: {
|
||||
name: string;
|
||||
mode: Mode;
|
||||
}[];
|
||||
}
|
||||
18
components/icons/Search.tsx
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
export function SearchIcon(p: { class?: string }) {
|
||||
return (
|
||||
<svg
|
||||
class={p.class}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
NEXT_PUBLIC_BACKEND_URL=http://localhost:3001
|
||||
34
frontend/.gitignore
vendored
|
|
@ -1,34 +0,0 @@
|
|||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
|
@ -1 +0,0 @@
|
|||
module.exports = "test-file-stub";
|
||||
|
|
@ -1 +0,0 @@
|
|||
module.exports = {};
|
||||
|
Before Width: | Height: | Size: 10 KiB |
|
|
@ -1,17 +0,0 @@
|
|||
overwrite: true
|
||||
schema: "http://localhost:4000/graphql"
|
||||
documents: "**/*.graphql"
|
||||
config:
|
||||
scalars:
|
||||
Datetime: Date
|
||||
generates:
|
||||
generated/introspection.json:
|
||||
plugins:
|
||||
- "introspection"
|
||||
config:
|
||||
minify: true
|
||||
generated/graphql.ts:
|
||||
plugins:
|
||||
- "typescript"
|
||||
- "typescript-operations"
|
||||
- "typescript-urql"
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
import { styled } from "stitches.config";
|
||||
|
||||
export function Avatar({ src }: { src: string }) {
|
||||
return (
|
||||
<S_Container>
|
||||
<S_Avatar src={src} />
|
||||
</S_Container>
|
||||
);
|
||||
}
|
||||
|
||||
const S_Container = styled("div", {
|
||||
position: "relative",
|
||||
userSelect: "none",
|
||||
overflow: "hidden",
|
||||
width: "var(--item-size)",
|
||||
minWidth: "var(--item-size)",
|
||||
height: "var(--item-size)",
|
||||
borderRadius: "$rounded",
|
||||
});
|
||||
|
||||
const S_Avatar = styled("img", {
|
||||
objectFit: "cover",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
display: "block",
|
||||
});
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
import { HiSearch } from "react-icons/hi";
|
||||
import { styled } from "stitches.config";
|
||||
|
||||
export function SearchInput() {
|
||||
return (
|
||||
<S_Container>
|
||||
<S_Input type="text" placeholder="Search" />
|
||||
<HiSearch />
|
||||
</S_Container>
|
||||
);
|
||||
}
|
||||
|
||||
const S_Container = styled("div", {
|
||||
display: "flex",
|
||||
height: "1rem",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
paddingX: "$4",
|
||||
paddingY: "$5",
|
||||
backgroundColor: "$bgLighter",
|
||||
borderRadius: "$rounded",
|
||||
|
||||
"&:focus-within": {
|
||||
outline: "2px solid $theme",
|
||||
},
|
||||
|
||||
"&> svg": {
|
||||
height: "1.25rem",
|
||||
color: "$text",
|
||||
},
|
||||
});
|
||||
|
||||
const S_Input = styled("input", {
|
||||
width: "12rem",
|
||||
height: "2rem",
|
||||
border: "none",
|
||||
backgroundColor: "$bgLighter",
|
||||
fontSize: "$sm",
|
||||
outline: "none",
|
||||
color: "$text",
|
||||
flexGrow: 1,
|
||||
|
||||
"&::placeholder": {
|
||||
color: "$textLighter",
|
||||
letterSpacing: "0.5px",
|
||||
fontWeight: "$semiBold",
|
||||
},
|
||||
});
|
||||
|
||||
// .bigSearchInput {
|
||||
// width: 12rem;
|
||||
// border: none;
|
||||
// background-color: var(--input-bg);
|
||||
// font-size: var(--font-xs);
|
||||
// outline: none;
|
||||
// }
|
||||
|
||||
// .bigSearchInput::placeholder {
|
||||
// color: var(--input-placeholder-color);
|
||||
// letter-spacing: 0.5px;
|
||||
// }
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
import { styled } from "stitches.config";
|
||||
|
||||
export function Select({
|
||||
values,
|
||||
onChange,
|
||||
selected,
|
||||
}: {
|
||||
values: { id: string; name: string }[];
|
||||
onChange?: (e: React.ChangeEvent<HTMLSelectElement>) => void;
|
||||
selected: string;
|
||||
}) {
|
||||
return (
|
||||
<S_Select value={selected} onChange={onChange}>
|
||||
{values.map((value) => {
|
||||
return (
|
||||
<option key={value.id} value={value.id}>
|
||||
{value.name}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</S_Select>
|
||||
);
|
||||
}
|
||||
|
||||
const S_Select = styled("select", {
|
||||
all: "unset",
|
||||
width: "100%",
|
||||
padding: "$2 $4",
|
||||
backgroundColor: "$bgLighter",
|
||||
backgroundImage: `url('data:image/svg+xml;utf8,<svg width="1rem" color="white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" /></svg>')`,
|
||||
backgroundPositionX: "95%",
|
||||
backgroundPositionY: "50%",
|
||||
backgroundRepeat: "no-repeat",
|
||||
borderRadius: "$rounded",
|
||||
cursor: "pointer",
|
||||
fontWeight: "$body",
|
||||
fontSize: "$sm",
|
||||
|
||||
"&::selection": {
|
||||
fontWeight: "$bold",
|
||||
},
|
||||
|
||||
"&:focus": {
|
||||
outline: "2px solid $theme",
|
||||
},
|
||||
});
|
||||
|
|
@ -1,86 +0,0 @@
|
|||
import { Tab as HeadlessTab } from "@headlessui/react";
|
||||
import { ComponentProps } from "react";
|
||||
import { css } from "stitches.config";
|
||||
|
||||
export function Tab(props: any) {
|
||||
return (
|
||||
<HeadlessTab
|
||||
{...props}
|
||||
className={({ selected }) => (selected ? tab({ type: "active" }) : tab())}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function Group(props: ComponentProps<typeof HeadlessTab["Group"]>) {
|
||||
return <HeadlessTab.Group {...props} />;
|
||||
}
|
||||
|
||||
function List({
|
||||
tabsCount,
|
||||
...props
|
||||
}: ComponentProps<typeof HeadlessTab["List"]> & { tabsCount: number }) {
|
||||
return (
|
||||
<HeadlessTab.List
|
||||
{...props}
|
||||
className={container()}
|
||||
style={{ "--tabs-count": tabsCount }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function Panels(props: ComponentProps<typeof HeadlessTab["Panels"]>) {
|
||||
return <HeadlessTab.Panels {...props} />;
|
||||
}
|
||||
|
||||
function Panel(props: ComponentProps<typeof HeadlessTab["Panel"]>) {
|
||||
return <HeadlessTab.Panel {...props} />;
|
||||
}
|
||||
|
||||
Tab.Group = Group;
|
||||
Tab.List = List;
|
||||
Tab.Panels = Panels;
|
||||
Tab.Panel = Panel;
|
||||
|
||||
const container = css({
|
||||
display: "grid",
|
||||
justifyContent: "center",
|
||||
placeItems: "center",
|
||||
gap: "$10",
|
||||
gridTemplateColumns: "repeat(var(--tabs-count), 100px)",
|
||||
});
|
||||
|
||||
const tab = css({
|
||||
all: "unset",
|
||||
cursor: "pointer",
|
||||
borderRadius: "$rounded",
|
||||
|
||||
"&::after": {
|
||||
display: "block",
|
||||
width: "1.25rem",
|
||||
height: "3px",
|
||||
borderBottom: "3px solid",
|
||||
content: '""',
|
||||
borderColor: "transparent",
|
||||
},
|
||||
|
||||
"&:hover::after": {
|
||||
borderColor: "$themeTransparent",
|
||||
},
|
||||
|
||||
"&:focus-visible": {
|
||||
outline: "2px solid $themeTransparent",
|
||||
outlineOffset: "7px",
|
||||
},
|
||||
|
||||
variants: {
|
||||
type: {
|
||||
active: {
|
||||
fontWeight: "$bold",
|
||||
|
||||
"&::after": {
|
||||
borderColor: "$theme !important",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
@ -1,121 +0,0 @@
|
|||
import { styled } from "stitches.config";
|
||||
|
||||
export function HamburgerButton({
|
||||
isExpanded,
|
||||
onClick,
|
||||
}: {
|
||||
onClick: () => void;
|
||||
isExpanded: boolean;
|
||||
}) {
|
||||
return (
|
||||
<S_TransformingBurger
|
||||
aria-label="Toggle menu visibility"
|
||||
aria-expanded={isExpanded ? "true" : "false"}
|
||||
onClick={onClick}
|
||||
>
|
||||
<svg
|
||||
width="32"
|
||||
height="32"
|
||||
viewBox="0 0 32 32"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<S_TopLine
|
||||
x="6"
|
||||
y="9"
|
||||
width="20"
|
||||
height="2"
|
||||
rx="1"
|
||||
fill="currentColor"
|
||||
type={isExpanded ? "expanded" : undefined}
|
||||
/>
|
||||
<S_MiddleLine
|
||||
x="6"
|
||||
y="15"
|
||||
width="20"
|
||||
height="2"
|
||||
rx="1"
|
||||
fill="currentColor"
|
||||
type={isExpanded ? "expanded" : undefined}
|
||||
/>
|
||||
<S_BottomLine
|
||||
x="6"
|
||||
y="21"
|
||||
width="20"
|
||||
height="2"
|
||||
rx="1"
|
||||
fill="currentColor"
|
||||
type={isExpanded ? "expanded" : undefined}
|
||||
/>
|
||||
</svg>
|
||||
</S_TransformingBurger>
|
||||
);
|
||||
}
|
||||
|
||||
const S_TransformingBurger = styled("button", {
|
||||
display: "flex",
|
||||
width: "var(--item-size)",
|
||||
height: "var(--item-size)",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
padding: "$1",
|
||||
border: "3px solid",
|
||||
borderColor: "$bgLighter",
|
||||
backgroundColor: "transparent",
|
||||
borderRadius: "$rounded",
|
||||
color: "inherit",
|
||||
cursor: "pointer",
|
||||
gap: "2px",
|
||||
|
||||
"@sm": {
|
||||
display: "none",
|
||||
},
|
||||
});
|
||||
|
||||
const S_TopLine = styled("rect", {
|
||||
transform: "none",
|
||||
transformOrigin: "16px 10px",
|
||||
transitionDuration: "150ms",
|
||||
transitionProperty: "transform",
|
||||
transitionTimingFunction: "cubic-bezier(0.4, 0, 0.2, 1)",
|
||||
|
||||
variants: {
|
||||
type: {
|
||||
expanded: {
|
||||
transform: "translateY(7px) rotate(45deg)",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const S_MiddleLine = styled("rect", {
|
||||
opacity: 1,
|
||||
transitionDuration: "150ms",
|
||||
transitionProperty: "opacity",
|
||||
transitionTimingFunction: "cubic-bezier(0.4, 0, 0.2, 1)",
|
||||
|
||||
variants: {
|
||||
type: {
|
||||
expanded: {
|
||||
opacity: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const S_BottomLine = styled("rect", {
|
||||
transform: "none",
|
||||
transformOrigin: "16px 22px",
|
||||
transitionDuration: "150ms",
|
||||
transitionProperty: "transform",
|
||||
transitionTimingFunction: "cubic-bezier(0.4, 0, 0.2, 1)",
|
||||
|
||||
variants: {
|
||||
type: {
|
||||
expanded: {
|
||||
transform: "translateY(-5px) rotate(-45deg)",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
@ -1,154 +0,0 @@
|
|||
import { ReactNode, useState } from "react";
|
||||
import NextImage from "next/image";
|
||||
import NextLink from "next/link";
|
||||
import logo from "assets/img/logo.png";
|
||||
import { Avatar } from "components/common/Avatar";
|
||||
import { styled } from "stitches.config";
|
||||
import { SearchInput } from "components/common/SearchInput";
|
||||
import { HamburgerButton } from "./HamburgerButton";
|
||||
import { navItems } from "utils/constants";
|
||||
import { MobileNav } from "./MobileNav";
|
||||
|
||||
export function Layout({ children }: { children: ReactNode }) {
|
||||
const [menuExpanded, setMenuExpanded] = useState(false);
|
||||
return (
|
||||
<>
|
||||
<S_Header>
|
||||
<NextLink href="/">
|
||||
<S_LogoContainer>
|
||||
<NextImage src={logo} />
|
||||
</S_LogoContainer>
|
||||
</NextLink>
|
||||
<S_SearchContainer>
|
||||
<SearchInput />
|
||||
</S_SearchContainer>
|
||||
<S_RightContainer>
|
||||
<HamburgerButton
|
||||
isExpanded={menuExpanded}
|
||||
onClick={() => setMenuExpanded((expanded) => !expanded)}
|
||||
/>
|
||||
<Avatar src="https://cdn.discordapp.com/avatars/79237403620945920/fcfd65a3bea598905abb9ca25296816b.png?size=80" />
|
||||
</S_RightContainer>
|
||||
</S_Header>
|
||||
<MobileNav isExpanded={menuExpanded} />
|
||||
<S_Nav>
|
||||
<S_NavItems>
|
||||
{navItems.map((navItem) => (
|
||||
<S_NavItemColumn key={navItem.title}>
|
||||
<S_NavGroupTitle>{navItem.title}</S_NavGroupTitle>
|
||||
{navItem.items.map((item) => (
|
||||
<S_NavLink key={item} href="/">
|
||||
<NextImage
|
||||
src={`/img/nav-icons/${item.replace(" ", "")}.png`}
|
||||
width={32}
|
||||
height={32}
|
||||
/>
|
||||
{item}
|
||||
</S_NavLink>
|
||||
))}
|
||||
</S_NavItemColumn>
|
||||
))}
|
||||
</S_NavItems>
|
||||
</S_Nav>
|
||||
<S_Main>{children}</S_Main>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const S_Header = styled("header", {
|
||||
position: "relative",
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(2, 1fr)",
|
||||
alignItems: "center",
|
||||
padding: "$4",
|
||||
"--item-size": "3rem",
|
||||
zIndex: 10,
|
||||
backgroundColor: "$bg",
|
||||
|
||||
"@sm": {
|
||||
gridTemplateColumns: "repeat(3, 1fr)",
|
||||
"--item-size": "38px",
|
||||
},
|
||||
});
|
||||
|
||||
const S_LogoContainer = styled("div", {
|
||||
backgroundColor: "$bgLighter",
|
||||
backgroundImage: `url(/svg/background-pattern.svg)`,
|
||||
display: "grid",
|
||||
placeItems: "center",
|
||||
padding: "$1",
|
||||
borderRadius: "$rounded",
|
||||
justifySelf: "flex-start",
|
||||
cursor: "pointer",
|
||||
width: "var(--item-size)",
|
||||
minWidth: "var(--item-size)",
|
||||
height: "var(--item-size)",
|
||||
});
|
||||
|
||||
const S_SearchContainer = styled("div", {
|
||||
display: "none",
|
||||
|
||||
"@sm": {
|
||||
display: "block",
|
||||
},
|
||||
});
|
||||
|
||||
const S_RightContainer = styled("div", {
|
||||
display: "flex",
|
||||
gap: "$4",
|
||||
justifySelf: "flex-end",
|
||||
});
|
||||
|
||||
const S_Nav = styled("nav", {
|
||||
display: "none",
|
||||
justifyContent: "center",
|
||||
backgroundColor: "$bgLighter",
|
||||
backgroundImage: `url(/svg/background-pattern.svg)`,
|
||||
|
||||
"@sm": {
|
||||
display: "flex",
|
||||
},
|
||||
});
|
||||
|
||||
const S_NavItems = styled("div", {
|
||||
backgroundColor: "$bgLighter",
|
||||
display: "inline-flex",
|
||||
justifyContent: "center",
|
||||
gap: "$12",
|
||||
gridTemplateColumns: "repeat(4, 100px)",
|
||||
paddingY: "$4",
|
||||
paddingX: "$8",
|
||||
});
|
||||
|
||||
const S_NavItemColumn = styled("div", {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "$2",
|
||||
});
|
||||
|
||||
const S_NavLink = styled("a", {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "$2",
|
||||
color: "$text",
|
||||
textDecoration: "none",
|
||||
fontSize: "$sm",
|
||||
fontWeight: "$bold",
|
||||
textTransform: "capitalize",
|
||||
transition: "0.2s transform",
|
||||
|
||||
"&:hover": {
|
||||
transform: "translateX(2px)",
|
||||
},
|
||||
});
|
||||
|
||||
const S_NavGroupTitle = styled("div", {
|
||||
textTransform: "uppercase",
|
||||
fontWeight: "$bold",
|
||||
color: "$textLighter",
|
||||
fontSize: "$xxs",
|
||||
});
|
||||
|
||||
const S_Main = styled("main", {
|
||||
paddingTop: "$8",
|
||||
});
|
||||
|
|
@ -1,134 +0,0 @@
|
|||
import { SearchInput } from "components/common/SearchInput";
|
||||
import { styled } from "stitches.config";
|
||||
import { navItems } from "utils/constants";
|
||||
import NextLink from "next/link";
|
||||
import { Fragment } from "react";
|
||||
import NextImage from "next/image";
|
||||
|
||||
export function MobileNav({ isExpanded }: { isExpanded: boolean }) {
|
||||
return (
|
||||
<S_Container
|
||||
aria-hidden={isExpanded ? "false" : "true"}
|
||||
type={isExpanded ? "expanded" : undefined}
|
||||
>
|
||||
<S_TopAction>
|
||||
<SearchInput />
|
||||
</S_TopAction>
|
||||
<S_LinksContainer>
|
||||
{navItems.map((navItem) => {
|
||||
return (
|
||||
<Fragment key={navItem.title}>
|
||||
<S_GroupTitle>{navItem.title}</S_GroupTitle>
|
||||
{navItem.items.map((item, i, arr) => {
|
||||
return (
|
||||
<NextLink key={item} href="/">
|
||||
<S_NavLink
|
||||
type={
|
||||
i === 0
|
||||
? "first"
|
||||
: i + 1 === arr.length
|
||||
? "last"
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<NextImage
|
||||
src={`/img/nav-icons/${item.replace(" ", "")}.png`}
|
||||
width={38}
|
||||
height={38}
|
||||
/>
|
||||
<div>{item}</div>
|
||||
</S_NavLink>
|
||||
</NextLink>
|
||||
);
|
||||
})}
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</S_LinksContainer>
|
||||
</S_Container>
|
||||
);
|
||||
}
|
||||
|
||||
const S_Container = styled("nav", {
|
||||
position: "absolute",
|
||||
bottom: "100vh",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
transition: "bottom 0.5s",
|
||||
transitionTimingFunction: "cubic-bezier(0.4, 0, 0.2, 1)",
|
||||
backgroundImage: `url(/svg/background-pattern.svg)`,
|
||||
backgroundColor: "$bgLighter",
|
||||
zIndex: 5,
|
||||
overflowY: "auto",
|
||||
paddingBottom: "$6",
|
||||
// TODO: same for Safari
|
||||
overscrollBehavior: "contain",
|
||||
|
||||
"@sm": {
|
||||
display: "none",
|
||||
},
|
||||
|
||||
variants: {
|
||||
type: {
|
||||
expanded: {
|
||||
bottom: "0",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const S_TopAction = styled("div", {
|
||||
display: "grid",
|
||||
gap: "$4",
|
||||
placeItems: "center",
|
||||
backgroundColor: "$bg",
|
||||
paddingTop: "$24",
|
||||
paddingBottom: "$8",
|
||||
});
|
||||
|
||||
const S_LinksContainer = styled("div", {
|
||||
display: "grid",
|
||||
justifyContent: "center",
|
||||
gridAutoColumns: "1fr",
|
||||
gridAutoRows: "4rem",
|
||||
});
|
||||
|
||||
const S_GroupTitle = styled("div", {
|
||||
textTransform: "uppercase",
|
||||
alignSelf: "flex-end",
|
||||
justifySelf: "center",
|
||||
paddingX: "$3",
|
||||
paddingY: "$2",
|
||||
marginBottom: "$1",
|
||||
backgroundColor: "$bg",
|
||||
fontWeight: "$bold",
|
||||
borderRadius: "$rounded",
|
||||
});
|
||||
|
||||
const S_NavLink = styled("div", {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "$2",
|
||||
textTransform: "capitalize",
|
||||
borderTop: "3px solid $bgLighter",
|
||||
fontWeight: "$bold",
|
||||
backgroundColor: "$bg",
|
||||
paddingX: "$2",
|
||||
fontSize: "$sm",
|
||||
marginX: "$12",
|
||||
|
||||
"@xs": {
|
||||
marginX: "$24",
|
||||
},
|
||||
|
||||
variants: {
|
||||
type: {
|
||||
first: {
|
||||
borderRadius: "$rounded $rounded 0 0",
|
||||
},
|
||||
last: {
|
||||
borderRadius: "0 0 $rounded $rounded",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
@ -1,227 +0,0 @@
|
|||
import { styled } from "stitches.config";
|
||||
|
||||
const rounds = [
|
||||
{
|
||||
title: "Round 1",
|
||||
bestOf: 3,
|
||||
status: "DONE",
|
||||
},
|
||||
{
|
||||
title: "Round 2",
|
||||
bestOf: 5,
|
||||
status: "INPROGRESS",
|
||||
},
|
||||
{
|
||||
title: "Semifinals",
|
||||
bestOf: 5,
|
||||
status: "UPCOMING",
|
||||
},
|
||||
{
|
||||
title: "Finals",
|
||||
bestOf: 7,
|
||||
status: "UPCOMING",
|
||||
},
|
||||
] as const;
|
||||
|
||||
export function BracketTab() {
|
||||
return (
|
||||
<S_Container style={{ "--columns": rounds.length, "--matches": 4 } as any}>
|
||||
<S_Bracket>
|
||||
{rounds.map((round, i) => (
|
||||
<RoundInfo round={round} isLast={i === rounds.length - 1} />
|
||||
))}
|
||||
<S_Column style={{ "--column-matches": 4 } as any}>
|
||||
<S_MatchesContainer>
|
||||
<Match />
|
||||
<Match />
|
||||
<Match />
|
||||
<Match />
|
||||
</S_MatchesContainer>
|
||||
<S_LinesContainer>
|
||||
<div />
|
||||
<div />
|
||||
</S_LinesContainer>
|
||||
</S_Column>
|
||||
<S_Column style={{ "--column-matches": 2 } as any}>
|
||||
<S_MatchesContainer>
|
||||
<Match />
|
||||
<Match />
|
||||
</S_MatchesContainer>
|
||||
<S_LinesContainer>
|
||||
<div />
|
||||
</S_LinesContainer>
|
||||
</S_Column>
|
||||
<S_Column
|
||||
style={{ "--column-matches": 0, "--bottom-border-length": 0 } as any}
|
||||
>
|
||||
<S_MatchesContainer>
|
||||
<Match />
|
||||
</S_MatchesContainer>
|
||||
<S_LinesContainer>
|
||||
<div />
|
||||
</S_LinesContainer>
|
||||
</S_Column>
|
||||
<S_MatchesContainer>
|
||||
<Match />
|
||||
</S_MatchesContainer>
|
||||
</S_Bracket>
|
||||
</S_Container>
|
||||
);
|
||||
}
|
||||
|
||||
const S_Container = styled("div", {
|
||||
margin: "0 auto",
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
});
|
||||
|
||||
const S_Bracket = styled("div", {
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(var(--columns), 250px)",
|
||||
columnGap: "2px",
|
||||
"--match-height": "100px",
|
||||
// todo: fix overflow
|
||||
overflowX: "hidden",
|
||||
});
|
||||
|
||||
const S_Column = styled("div", {
|
||||
display: "flex",
|
||||
});
|
||||
|
||||
const S_MatchesContainer = styled("div", {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "space-around",
|
||||
height: "calc(var(--match-height) * var(--matches))",
|
||||
});
|
||||
|
||||
const S_LinesContainer = styled("div", {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "space-around",
|
||||
paddingLeft: "7px",
|
||||
|
||||
"&> div": {
|
||||
width: "calc(var(--match-height) / 4)",
|
||||
height:
|
||||
"calc(2px + var(--match-height) * (var(--matches) / var(--column-matches)))",
|
||||
borderTop: "2px solid",
|
||||
borderBottom: "var(--bottom-border-length, 2px) solid",
|
||||
borderInlineEnd: "2px solid",
|
||||
borderColor: "$theme",
|
||||
borderRadius: "0 var(--rounded) var(--rounded) 0",
|
||||
},
|
||||
});
|
||||
|
||||
function RoundInfo(props: {
|
||||
round: {
|
||||
title: string;
|
||||
bestOf: number;
|
||||
status: "DONE" | "INPROGRESS" | "UPCOMING";
|
||||
};
|
||||
isLast?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<S_RoundInfo
|
||||
status={props.round.status === "INPROGRESS" ? "active" : undefined}
|
||||
order={props.isLast ? "last" : undefined}
|
||||
>
|
||||
<S_RoundTitle>{props.round.title}</S_RoundTitle>
|
||||
{props.round.status !== "DONE" && (
|
||||
<S_BestOf>Bo{props.round.bestOf}</S_BestOf>
|
||||
)}
|
||||
</S_RoundInfo>
|
||||
);
|
||||
}
|
||||
|
||||
const S_RoundInfo = styled("div", {
|
||||
backgroundColor: "$bgLighter",
|
||||
padding: "1rem 1rem",
|
||||
|
||||
"&:first-of-type": {
|
||||
borderRadius: "$rounded 0 0 $rounded",
|
||||
},
|
||||
|
||||
variants: {
|
||||
status: {
|
||||
active: {
|
||||
backgroundColor: "$theme",
|
||||
},
|
||||
},
|
||||
order: {
|
||||
last: {
|
||||
borderRadius: "0 $rounded $rounded 0",
|
||||
},
|
||||
},
|
||||
},
|
||||
/* TODO: transition: background-color 0.5s; */
|
||||
});
|
||||
|
||||
const S_RoundTitle = styled("div", {
|
||||
fontWeight: 500,
|
||||
fontSize: "$sm",
|
||||
});
|
||||
|
||||
const S_BestOf = styled("div", {
|
||||
fontSize: "$xs",
|
||||
});
|
||||
|
||||
export function Match() {
|
||||
return (
|
||||
<S_Match>
|
||||
<S_RoundNumber>1</S_RoundNumber>
|
||||
<S_Team team="one">
|
||||
Team Olive <S_Score>1</S_Score>
|
||||
</S_Team>
|
||||
<S_Team team="two">
|
||||
Chimera <S_Score>1</S_Score>
|
||||
</S_Team>
|
||||
</S_Match>
|
||||
);
|
||||
}
|
||||
|
||||
const S_Match = styled("div", {
|
||||
margin: "$4 0",
|
||||
display: "grid",
|
||||
gridTemplateColumns: "10px 1fr",
|
||||
rowGap: "2px",
|
||||
columnGap: "5px",
|
||||
placeItems: "center",
|
||||
gridTemplateAreas: `
|
||||
"round-number team-one"
|
||||
"round-number team-two"
|
||||
`,
|
||||
});
|
||||
|
||||
const S_RoundNumber = styled("div", {
|
||||
gridArea: "round-number",
|
||||
fontSize: "$xs",
|
||||
});
|
||||
|
||||
const S_Team = styled("div", {
|
||||
placeSelf: "flex-start",
|
||||
backgroundColor: "$bgLighter",
|
||||
width: "200px",
|
||||
fontSize: "$sm",
|
||||
padding: "$1 $3",
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
|
||||
variants: {
|
||||
team: {
|
||||
one: {
|
||||
gridArea: "team-one",
|
||||
borderRadius: "$rounded $rounded 0 0",
|
||||
},
|
||||
two: {
|
||||
gridArea: "team-two",
|
||||
borderRadius: "0 0 $rounded $rounded",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const S_Score = styled("span", {
|
||||
fontWeight: "bold",
|
||||
});
|
||||
|
|
@ -1,184 +0,0 @@
|
|||
import { FaDiscord, FaTwitter } from "react-icons/fa";
|
||||
import { styled } from "stitches.config";
|
||||
import { useTournamentData } from "../../hooks/data/useTournamentData";
|
||||
|
||||
export function InfoBanner() {
|
||||
const { data } = useTournamentData();
|
||||
const tournament = data?.tournamentByIdentifier;
|
||||
|
||||
if (!tournament) return null;
|
||||
|
||||
return (
|
||||
<S_Container
|
||||
style={
|
||||
{
|
||||
"--background": tournament.bannerBackground,
|
||||
"--text": tournament.textColor,
|
||||
} as any
|
||||
}
|
||||
>
|
||||
<S_TopRow>
|
||||
<S_DateName>
|
||||
<S_MonthDate>
|
||||
<S_Month>{shortMonthName(tournament.startTime)}</S_Month>
|
||||
<S_Date>{dayNumber(tournament.startTime)}</S_Date>
|
||||
</S_MonthDate>
|
||||
<S_TournamentName>{tournament.name}</S_TournamentName>
|
||||
</S_DateName>
|
||||
<S_IconButtons>
|
||||
{tournament.organizationByOrganizationIdentifier.twitterUrl && (
|
||||
<S_IconButton
|
||||
href={tournament.organizationByOrganizationIdentifier.twitterUrl}
|
||||
>
|
||||
<FaTwitter />
|
||||
</S_IconButton>
|
||||
)}
|
||||
{tournament.organizationByOrganizationIdentifier.discordInviteUrl && (
|
||||
<S_IconButton
|
||||
href={
|
||||
tournament.organizationByOrganizationIdentifier.discordInviteUrl
|
||||
}
|
||||
>
|
||||
<FaDiscord />
|
||||
</S_IconButton>
|
||||
)}
|
||||
</S_IconButtons>
|
||||
</S_TopRow>
|
||||
<S_BottomRow>
|
||||
<S_Infos>
|
||||
<S_InfoContainer>
|
||||
<S_InfoLabel>Starting time</S_InfoLabel>
|
||||
<div>{weekdayAndStartTime(tournament.startTime)}</div>
|
||||
</S_InfoContainer>
|
||||
<S_InfoContainer>
|
||||
<S_InfoLabel>Format</S_InfoLabel>
|
||||
<div>Double Elimination</div>
|
||||
</S_InfoContainer>
|
||||
<S_InfoContainer>
|
||||
<S_InfoLabel>Organizer</S_InfoLabel>
|
||||
<div>{tournament.organizationByOrganizationIdentifier.name}</div>
|
||||
</S_InfoContainer>
|
||||
</S_Infos>
|
||||
</S_BottomRow>
|
||||
</S_Container>
|
||||
);
|
||||
}
|
||||
|
||||
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" });
|
||||
}
|
||||
|
||||
const S_Container = styled("div", {
|
||||
width: "100%",
|
||||
padding: "$6",
|
||||
background: "var(--background)",
|
||||
color: "var(--text)",
|
||||
|
||||
"@md": {
|
||||
borderRadius: "$rounded",
|
||||
width: "48rem",
|
||||
},
|
||||
});
|
||||
|
||||
const S_TopRow = styled("div", {
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
justifyContent: "space-between",
|
||||
gap: "$4",
|
||||
});
|
||||
|
||||
const S_DateName = styled("div", {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "$4",
|
||||
});
|
||||
|
||||
const S_MonthDate = styled("div", {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
lineHeight: 1.25,
|
||||
});
|
||||
|
||||
const S_Month = styled("div", {
|
||||
fontSize: "$xs",
|
||||
textTransform: "uppercase",
|
||||
});
|
||||
|
||||
const S_Date = styled("div", {
|
||||
fontSize: "$lg",
|
||||
fontWeight: "$bold",
|
||||
});
|
||||
|
||||
const S_TournamentName = styled("div", {
|
||||
paddingLeft: "$4",
|
||||
borderColor: "var(--text)",
|
||||
borderLeft: "1px solid",
|
||||
fontSize: "$xl",
|
||||
fontWeight: "$extraBold",
|
||||
});
|
||||
|
||||
const S_BottomRow = styled("div", {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "space-between",
|
||||
gap: "$4",
|
||||
|
||||
"@sm": {
|
||||
flexDirection: "row",
|
||||
alignItems: "flex-end",
|
||||
},
|
||||
});
|
||||
|
||||
const S_Infos = styled("div", {
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
marginTop: "$8",
|
||||
gap: "$4",
|
||||
|
||||
"@xs": {
|
||||
gap: "$8",
|
||||
},
|
||||
});
|
||||
|
||||
const S_IconButtons = styled("div", {
|
||||
display: "flex",
|
||||
gap: "$4",
|
||||
});
|
||||
|
||||
const S_InfoContainer = styled("div", {
|
||||
fontSize: "$xs",
|
||||
});
|
||||
|
||||
const S_InfoLabel = styled("label", {
|
||||
fontWeight: "$extraBold",
|
||||
});
|
||||
|
||||
const S_IconButton = styled("a", {
|
||||
display: "inline-flex",
|
||||
width: "2.25rem",
|
||||
height: "2.25rem",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
padding: "0.5rem",
|
||||
border: "1px solid",
|
||||
borderColor: "var(--text-transparent)",
|
||||
borderRadius: "50%",
|
||||
color: "inherit",
|
||||
transition: "background-color 0.3s",
|
||||
|
||||
"&:active": {
|
||||
transform: "translateY(1px)",
|
||||
},
|
||||
});
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
import { Mode } from "@sendou-ink/api/common";
|
||||
import { modesPerStage } from "./MapPoolTab";
|
||||
|
||||
describe("modesPerStage()", () => {
|
||||
test("lists are of right length", () => {
|
||||
const mapPool = [
|
||||
{ name: "a", mode: "1" as Mode },
|
||||
{ name: "b", mode: "2" as Mode },
|
||||
{ name: "c", mode: "3" as Mode },
|
||||
{ name: "c", mode: "1" as Mode },
|
||||
];
|
||||
const stages = modesPerStage(mapPool);
|
||||
|
||||
expect(stages.a.length).toBe(1);
|
||||
expect(stages.b.length).toBe(1);
|
||||
expect(stages.c.length).toBe(2);
|
||||
});
|
||||
|
||||
test("lists have right content", () => {
|
||||
const mapPool = [
|
||||
{ name: "a", mode: "1" as Mode },
|
||||
{ name: "b", mode: "2" as Mode },
|
||||
{ name: "c", mode: "3" as Mode },
|
||||
{ name: "c", mode: "1" as Mode },
|
||||
];
|
||||
const stages = modesPerStage(mapPool);
|
||||
|
||||
expect(stages.c).toContain("3");
|
||||
expect(stages.c).toContain("1");
|
||||
});
|
||||
});
|
||||
|
|
@ -1,126 +0,0 @@
|
|||
import { styled } from "stitches.config";
|
||||
import NextImage from "next/image";
|
||||
import { useTournamentData } from "hooks/data/useTournamentData";
|
||||
import { stages, modesShort } from "@sendou-ink/shared/constants";
|
||||
import type { Mode } from "@sendou-ink/api/common";
|
||||
import { ModeEnum } from "generated/graphql";
|
||||
|
||||
export function MapPoolTab() {
|
||||
const { data } = useTournamentData();
|
||||
const mapPool =
|
||||
data?.tournamentByIdentifier?.mapPoolsByTournamentIdentifier.nodes.map(
|
||||
(node) => node.mapModeByMapModeId
|
||||
);
|
||||
|
||||
// TODO: handle loading
|
||||
// TODO: handle error in parent
|
||||
if (!mapPool) return null;
|
||||
|
||||
return (
|
||||
<S_Container>
|
||||
<S_InfoSquare>
|
||||
<S_EmphasizedText>{mapPool.length} maps</S_EmphasizedText>
|
||||
</S_InfoSquare>
|
||||
{stages.map((stage) => (
|
||||
<S_StageImageContainer key={stage}>
|
||||
<S_StageImage
|
||||
alt={stage}
|
||||
src={`/img/stages/${stage.replaceAll(" ", "-").toLowerCase()}.png`}
|
||||
filter={modesPerStage(mapPool)[stage] ? undefined : "bw"}
|
||||
width={256}
|
||||
height={144}
|
||||
/>
|
||||
{modesPerStage(mapPool)[stage] && (
|
||||
<S_ModeImagesContainer>
|
||||
{modesShort.map((mode) => {
|
||||
if (!modesPerStage(mapPool)[stage]?.includes(mode as Mode)) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<NextImage
|
||||
key={mode}
|
||||
src={`/img/modes/${mode}.png`}
|
||||
alt={mode}
|
||||
width={28}
|
||||
height={28}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</S_ModeImagesContainer>
|
||||
)}
|
||||
</S_StageImageContainer>
|
||||
))}
|
||||
</S_Container>
|
||||
);
|
||||
}
|
||||
|
||||
export function modesPerStage(
|
||||
mapPool: {
|
||||
stage: string;
|
||||
gameMode: ModeEnum;
|
||||
}[]
|
||||
) {
|
||||
return mapPool.reduce((acc: Record<string, Mode[]>, { stage, gameMode }) => {
|
||||
if (!acc[stage]) {
|
||||
acc[stage] = [];
|
||||
}
|
||||
|
||||
acc[stage].push(gameMode);
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
const S_InfoSquare = styled("div", {
|
||||
display: "grid",
|
||||
placeItems: "center",
|
||||
fontWeight: "$semiBold",
|
||||
fontSize: "$xl",
|
||||
backgroundImage: `url(/svg/background-pattern.svg)`,
|
||||
backgroundColor: "$bgLighter",
|
||||
borderRadius: "$rounded",
|
||||
});
|
||||
|
||||
const S_Container = styled("div", {
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(2, 1fr)",
|
||||
gap: "1rem",
|
||||
|
||||
"@xs": {
|
||||
gridTemplateColumns: "repeat(2, 1fr)",
|
||||
},
|
||||
|
||||
"@sm": {
|
||||
gridTemplateColumns: "repeat(4, minmax(1px, 200px))",
|
||||
},
|
||||
});
|
||||
|
||||
const S_EmphasizedText = styled("span", {
|
||||
fontWeight: "$bold",
|
||||
});
|
||||
|
||||
const S_StageImageContainer = styled("div", {
|
||||
position: "relative",
|
||||
});
|
||||
|
||||
const S_StageImage = styled(NextImage, {
|
||||
borderRadius: "$rounded",
|
||||
|
||||
variants: {
|
||||
filter: {
|
||||
bw: {
|
||||
filter: "grayscale(100%)",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const S_ModeImagesContainer = styled("div", {
|
||||
display: "flex",
|
||||
position: "absolute",
|
||||
backdropFilter: "blur(5px) grayscale(25%)",
|
||||
top: 0,
|
||||
left: 0,
|
||||
borderRadius: "$rounded 0 $rounded 0",
|
||||
padding: "$1",
|
||||
gap: "$2",
|
||||
});
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
import { useTournamentData } from "hooks/data/useTournamentData";
|
||||
|
||||
export function OverviewTab() {
|
||||
const { data } = useTournamentData();
|
||||
|
||||
// TODO: handle loading
|
||||
// TODO: handle error in parent
|
||||
if (!data?.tournamentByIdentifier) return null;
|
||||
|
||||
return <div>{data.tournamentByIdentifier.description}</div>;
|
||||
}
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
query TournamentByIdentifier($identifier: String!) {
|
||||
tournamentByIdentifier(identifier: $identifier) {
|
||||
name
|
||||
startTime
|
||||
checkInTime
|
||||
bannerBackground
|
||||
description
|
||||
textColor
|
||||
organizationByOrganizationIdentifier {
|
||||
name
|
||||
discordInviteUrl
|
||||
twitterUrl
|
||||
}
|
||||
tournamentTeamsByTournamentIdentifier {
|
||||
nodes {
|
||||
name
|
||||
tournamentTeamRostersByTournamentTeamId {
|
||||
nodes {
|
||||
captain,
|
||||
accountByMemberId {
|
||||
discordFullUsername
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
mapPoolsByTournamentIdentifier {
|
||||
nodes {
|
||||
mapModeByMapModeId {
|
||||
stage
|
||||
gameMode
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
import { useTournamentByIdentifierQuery } from "generated/graphql";
|
||||
import { useRouter } from "next/dist/client/router";
|
||||
|
||||
export function useTournamentData() {
|
||||
const router = useRouter();
|
||||
const { tournament } = router.query;
|
||||
const [result] = useTournamentByIdentifierQuery({
|
||||
variables: { identifier: typeof tournament === "string" ? tournament : "" },
|
||||
pause: typeof tournament !== "string",
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
// jest.config.js
|
||||
|
||||
module.exports = {
|
||||
collectCoverageFrom: [
|
||||
"**/*.{js,jsx,ts,tsx}",
|
||||
"!**/*.d.ts",
|
||||
"!**/node_modules/**",
|
||||
],
|
||||
moduleNameMapper: {
|
||||
/* Handle CSS imports (with CSS modules)
|
||||
https://jestjs.io/docs/webpack#mocking-css-modules */
|
||||
"^.+\\.module\\.(css|sass|scss)$": "identity-obj-proxy",
|
||||
|
||||
// Handle CSS imports (without CSS modules)
|
||||
"^.+\\.(css|sass|scss)$": "<rootDir>/__mocks__/styleMock.js",
|
||||
|
||||
/* Handle image imports
|
||||
https://jestjs.io/docs/webpack#handling-static-assets */
|
||||
"^.+\\.(jpg|jpeg|png|gif|webp|avif|svg)$":
|
||||
"<rootDir>/__mocks__/fileMock.js",
|
||||
},
|
||||
testPathIgnorePatterns: ["<rootDir>/node_modules/", "<rootDir>/.next/"],
|
||||
testEnvironment: "jsdom",
|
||||
transform: {
|
||||
/* Use babel-jest to transpile tests with the next/babel preset
|
||||
https://jestjs.io/docs/configuration#transform-objectstring-pathtotransformer--pathtotransformer-object */
|
||||
"^.+\\.(js|jsx|ts|tsx)$": ["babel-jest", { presets: ["next/babel"] }],
|
||||
},
|
||||
transformIgnorePatterns: [
|
||||
"/node_modules/",
|
||||
"^.+\\.module\\.(css|sass|scss)$",
|
||||
],
|
||||
moduleDirectories: ["node_modules", "<rootDir>"],
|
||||
};
|
||||
6
frontend/next-env.d.ts
vendored
|
|
@ -1,6 +0,0 @@
|
|||
/// <reference types="next" />
|
||||
/// <reference types="next/types/global" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
module.exports = {
|
||||
images: {
|
||||
formats: ["image/avif", "image/webp"],
|
||||
},
|
||||
};
|
||||
28797
frontend/package-lock.json
generated
|
|
@ -1,33 +0,0 @@
|
|||
{
|
||||
"name": "@sendou.ink/frontend",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"test": "jest",
|
||||
"codegen": "graphql-codegen --config codegen.yml"
|
||||
},
|
||||
"dependencies": {
|
||||
"@headlessui/react": "^1.4.1",
|
||||
"@stitches/react": "^1.2.5",
|
||||
"graphql": "^15.7.2",
|
||||
"next": "^12.0.1",
|
||||
"normalize.css": "^8.0.1",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-icons": "^4.3.1",
|
||||
"urql": "^2.0.5",
|
||||
"urql-custom-scalars-exchange": "^0.1.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@graphql-codegen/cli": "2.2.2",
|
||||
"@graphql-codegen/introspection": "^2.1.0",
|
||||
"@graphql-codegen/typescript": "2.3.1",
|
||||
"@graphql-codegen/typescript-operations": "^2.2.0",
|
||||
"@graphql-codegen/typescript-urql": "^3.4.0",
|
||||
"@types/jest": "^27.0.2",
|
||||
"@types/react": "^17.0.33",
|
||||
"jest": "^27.3.1"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
:root {
|
||||
--colors-bg: hsl(237.3, 42.3%, 30.6%);
|
||||
--colors-bg-lighter: hsl(237.3, 42.3%, 35.6%);
|
||||
--colors-text: rgba(255, 255, 255, 0.95);
|
||||
--colors-text-lighter: rgba(255, 255, 255, 0.55);
|
||||
--colors-theme: rgba(103, 65, 217, 255);
|
||||
--colors-theme-transparent: rgba(103, 65, 217, 0.4);
|
||||
--radii-rounded: 16px;
|
||||
--fonts-xl: 1.5rem;
|
||||
--fonts-lg: 1.2rem;
|
||||
--fonts-md: 1rem;
|
||||
--fonts-sm: 0.9rem;
|
||||
--fonts-xs: 0.8rem;
|
||||
--text-transparent: hsla(231, 9%, 16%, 0.2);
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--colors-bg);
|
||||
color: var(--colors-text);
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial,
|
||||
sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
|
||||
line-height: 1.55;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: antialiased;
|
||||
}
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
import "normalize.css";
|
||||
import "./_app.css";
|
||||
|
||||
import { AppProps } from "next/app";
|
||||
import Head from "next/head";
|
||||
import { Layout } from "components/layout/Layout";
|
||||
import { globalCss } from "stitches.config";
|
||||
import { Provider, createClient, fetchExchange } from "urql";
|
||||
import customScalarsExchange from "urql-custom-scalars-exchange";
|
||||
import schema from "generated/introspection.json";
|
||||
|
||||
const globalStyles = globalCss({
|
||||
"*": { boxSizing: "border-box" },
|
||||
"*::before": { boxSizing: "border-box" },
|
||||
"*::after": { boxSizing: "border-box" },
|
||||
body: {
|
||||
backgroundColor: "$bg",
|
||||
color: "$text",
|
||||
fontFamily:
|
||||
'-apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"',
|
||||
lineHeight: 1.55,
|
||||
"-webkit-font-smoothing": "antialiased",
|
||||
"-moz-osx-font-smoothing": "antialiased",
|
||||
},
|
||||
});
|
||||
|
||||
const client = createClient({
|
||||
url: "http://localhost:4000/graphql",
|
||||
exchanges: [
|
||||
customScalarsExchange({
|
||||
schema: schema as any,
|
||||
scalars: {
|
||||
Datetime(value) {
|
||||
return new Date(value);
|
||||
},
|
||||
},
|
||||
}),
|
||||
fetchExchange,
|
||||
],
|
||||
});
|
||||
|
||||
export default function App(props: AppProps) {
|
||||
const { Component, pageProps } = props;
|
||||
|
||||
globalStyles();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>Page title</title>
|
||||
<meta
|
||||
name="viewport"
|
||||
content="minimum-scale=1, initial-scale=1, width=device-width"
|
||||
/>
|
||||
</Head>
|
||||
<Provider value={client}>
|
||||
<Layout>
|
||||
<Component {...pageProps} />
|
||||
</Layout>
|
||||
</Provider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
import NextDocument, { Html, Head, Main, NextScript } from "next/document";
|
||||
import { getCssText } from "stitches.config";
|
||||
|
||||
export default class Document extends NextDocument {
|
||||
render() {
|
||||
return (
|
||||
<Html lang="en">
|
||||
<Head>
|
||||
<style
|
||||
id="stitches"
|
||||
dangerouslySetInnerHTML={{ __html: getCssText() }}
|
||||
/>
|
||||
</Head>
|
||||
<body>
|
||||
<Main />
|
||||
<NextScript />
|
||||
</body>
|
||||
</Html>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
import Head from "next/head";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-screen py-2">
|
||||
<Head>
|
||||
<title>Create Next App</title>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
|
||||
<main className="flex flex-col items-center justify-center w-full flex-1 px-20 text-center">
|
||||
<h1 className="text-6xl font-bold">
|
||||
Welcome to{" "}
|
||||
<a className="text-blue-600" href="https://nextjs.org">
|
||||
Next.js!
|
||||
</a>
|
||||
</h1>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,79 +0,0 @@
|
|||
import { InfoBanner } from "components/tournament/InfoBanner";
|
||||
import { Tab } from "components/common/Tab";
|
||||
import { styled } from "stitches.config";
|
||||
import { MapPoolTab } from "components/tournament/MapPoolTab";
|
||||
import { OverviewTab } from "components/tournament/OverviewTab";
|
||||
import { Select } from "components/common/Select";
|
||||
import { useState } from "react";
|
||||
import { BracketTab } from "components/tournament/BracketTab";
|
||||
|
||||
const tabs = [
|
||||
{ name: "Overview", id: "info", component: <OverviewTab /> },
|
||||
{ name: "Map Pool", id: "map-pool", component: <MapPoolTab /> },
|
||||
{ name: "Bracket", id: "bracket", component: <BracketTab /> },
|
||||
{ name: "Teams (23)", id: "teams", component: null },
|
||||
{ name: "Streams (4)", id: "streams", component: null },
|
||||
];
|
||||
|
||||
export default function TournamentPage() {
|
||||
const [activeTab, setActiveTab] = useState(tabs[0].id);
|
||||
return (
|
||||
<S_Container>
|
||||
<InfoBanner />
|
||||
<S_TabsContainer>
|
||||
<Tab.Group>
|
||||
<Tab.List tabsCount={tabs.length}>
|
||||
{tabs.map((tab) => (
|
||||
<Tab key={tab.id}>{tab.name}</Tab>
|
||||
))}
|
||||
</Tab.List>
|
||||
<Tab.Panels>
|
||||
{tabs.map((tab) => (
|
||||
<Tab.Panel key={tab.id}>{tab.component}</Tab.Panel>
|
||||
))}
|
||||
</Tab.Panels>
|
||||
</Tab.Group>
|
||||
</S_TabsContainer>
|
||||
<S_MobileContent>
|
||||
<S_SelectContainer>
|
||||
<Select
|
||||
values={tabs}
|
||||
onChange={(e) => setActiveTab(e.target.value)}
|
||||
selected={activeTab}
|
||||
/>
|
||||
</S_SelectContainer>
|
||||
{tabs.find((tab) => tab.id === activeTab)!.component}
|
||||
</S_MobileContent>
|
||||
</S_Container>
|
||||
);
|
||||
}
|
||||
|
||||
const S_Container = styled("div", {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
gap: "$8",
|
||||
});
|
||||
|
||||
const S_TabsContainer = styled("div", {
|
||||
display: "none",
|
||||
|
||||
"@sm": {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
gap: "$8",
|
||||
},
|
||||
});
|
||||
|
||||
const S_SelectContainer = styled("div", {
|
||||
width: "12rem",
|
||||
margin: "0 auto",
|
||||
marginBottom: "$8",
|
||||
});
|
||||
|
||||
const S_MobileContent = styled("div", {
|
||||
"@sm": {
|
||||
display: "none",
|
||||
},
|
||||
});
|
||||
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
|
@ -1,85 +0,0 @@
|
|||
import { createStitches } from "@stitches/react";
|
||||
|
||||
export const {
|
||||
styled,
|
||||
css,
|
||||
globalCss,
|
||||
keyframes,
|
||||
getCssText,
|
||||
theme,
|
||||
createTheme,
|
||||
config,
|
||||
} = createStitches({
|
||||
theme: {
|
||||
colors: {
|
||||
bg: "hsl(237.3, 42.3%, 30.6%)",
|
||||
bgLighter: "hsl(237.3, 42.3%, 35.6%)",
|
||||
text: "rgba(255, 255, 255, 0.95)",
|
||||
textLighter: "rgba(255, 255, 255, 0.55)",
|
||||
theme: "rgba(103, 65, 217, 255)",
|
||||
themeTransparent: "rgba(103, 65, 217, 0.4)",
|
||||
},
|
||||
radii: {
|
||||
rounded: "16px",
|
||||
},
|
||||
fontSizes: {
|
||||
xl: "1.5rem",
|
||||
lg: "1.2rem",
|
||||
md: "1rem",
|
||||
sm: "0.9rem",
|
||||
xs: "0.8rem",
|
||||
xxs: "0.7rem",
|
||||
},
|
||||
fontWeights: {
|
||||
extraBold: 700,
|
||||
bold: 600,
|
||||
semiBold: 500,
|
||||
body: 400,
|
||||
},
|
||||
space: {
|
||||
// scale loaned from Tailwind CSS
|
||||
"1": "0.25rem",
|
||||
"2": "0.5rem",
|
||||
"3": "0.75rem",
|
||||
"4": "1rem",
|
||||
"5": "1.25rem",
|
||||
"6": "1.5rem",
|
||||
"7": "1.75rem",
|
||||
"8": "2rem",
|
||||
"9": "2.25rem",
|
||||
"10": "2.5rem",
|
||||
"11": "2.75rem",
|
||||
"12": "3rem",
|
||||
"14": "3.5rem",
|
||||
"16": "4rem",
|
||||
"20": "5rem",
|
||||
"24": "6rem",
|
||||
"28": "7rem",
|
||||
"32": "8rem",
|
||||
"36": "9rem",
|
||||
"40": "10rem",
|
||||
"44": "11rem",
|
||||
"48": "12rem",
|
||||
"52": "13rem",
|
||||
"56": "14rem",
|
||||
"60": "15rem",
|
||||
"64": "16rem",
|
||||
"72": "18rem",
|
||||
"80": "20rem",
|
||||
"96": "24rem",
|
||||
},
|
||||
},
|
||||
media: {
|
||||
xs: "(min-width: 480px)",
|
||||
sm: "(min-width: 640px)",
|
||||
md: "(min-width: 768px)",
|
||||
lg: "(min-width: 1024px)",
|
||||
xl: "(min-width: 1280px)",
|
||||
},
|
||||
utils: {
|
||||
marginX: (value: string) => ({ marginLeft: value, marginRight: value }),
|
||||
marginY: (value: string) => ({ marginTop: value, marginBottom: value }),
|
||||
paddingX: (value: string) => ({ paddingLeft: value, paddingRight: value }),
|
||||
paddingY: (value: string) => ({ paddingTop: value, paddingBottom: value }),
|
||||
},
|
||||
});
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"baseUrl": "."
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
// https://stackoverflow.com/a/52910586
|
||||
export type Serialized<T> = {
|
||||
[P in keyof T]: T[P] extends Date ? string : Serialized<T[P]>;
|
||||
};
|
||||
68
index.css
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
:root {
|
||||
--bg: hsl(237.3, 42.3%, 30.6%);
|
||||
--bg-lighter: hsl(237.3, 42.3%, 35.6%);
|
||||
--text: rgba(255, 255, 255, 0.95);
|
||||
--text-lighter: rgba(255, 255, 255, 0.55);
|
||||
--theme: rgba(103, 65, 217);
|
||||
--theme-transparent: rgba(103, 65, 217, 0.4);
|
||||
|
||||
--rounded: 16px;
|
||||
|
||||
--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-2: 0.5rem;
|
||||
--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;
|
||||
}
|
||||
|
||||
*,
|
||||
*::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";
|
||||
line-height: 1.55;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
*:focus:not(:focus-visible) {
|
||||
outline: none !important;
|
||||
}
|
||||
16
index.html
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<!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="/src/assets/favicon.ico" />
|
||||
<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>
|
||||
7
index.tsx
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import { render } from "solid-js/web";
|
||||
|
||||
import "normalize.css";
|
||||
import "./index.css";
|
||||
import App from "./App";
|
||||
|
||||
render(() => <App />, document.getElementById("root") as HTMLElement);
|
||||
21799
package-lock.json
generated
26
package.json
|
|
@ -1,16 +1,22 @@
|
|||
{
|
||||
"name": "sendou.ink",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"workspaces": [
|
||||
"frontend",
|
||||
"api",
|
||||
"shared"
|
||||
],
|
||||
"name": "vite-template-solid",
|
||||
"version": "0.0.0",
|
||||
"description": "",
|
||||
"scripts": {
|
||||
"format:check": "npx prettier --check ."
|
||||
"start": "vite",
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"serve": "vite preview"
|
||||
},
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"prettier": "2.4.1"
|
||||
"typescript": "^4.4.3",
|
||||
"vite": "^2.5.7",
|
||||
"vite-plugin-solid": "^2.0.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"normalize.css": "^8.0.1",
|
||||
"solid-app-router": "^0.1.11",
|
||||
"solid-js": "^1.1.3"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,136 +0,0 @@
|
|||
require("dotenv").config();
|
||||
const { Pool } = require("pg");
|
||||
|
||||
const TEST_DATABASE_URL = process.env.DATABASE_URL;
|
||||
|
||||
if (!TEST_DATABASE_URL) {
|
||||
throw new Error("Cannot run tests without a TEST_DATABASE_URL");
|
||||
}
|
||||
|
||||
const pools = {};
|
||||
|
||||
// Make sure we release those pgPools so that our tests exit!
|
||||
afterAll(() => {
|
||||
const keys = Object.keys(pools);
|
||||
return Promise.all(
|
||||
keys.map(async (key) => {
|
||||
try {
|
||||
const pool = pools[key];
|
||||
delete pools[key];
|
||||
pool.end();
|
||||
} catch (e) {
|
||||
console.error("Failed to release connection!");
|
||||
console.error(e);
|
||||
}
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
const poolFromUrl = (url) => {
|
||||
if (!pools[url]) {
|
||||
pools[url] = new Pool({ connectionString: url });
|
||||
}
|
||||
return pools[url];
|
||||
};
|
||||
|
||||
const withDbFromUrl = async (url, fn) => {
|
||||
/** @type {import ("pg").Pool} */
|
||||
const pool = poolFromUrl(url);
|
||||
const client = await pool.connect();
|
||||
await client.query("BEGIN ISOLATION LEVEL SERIALIZABLE;");
|
||||
|
||||
try {
|
||||
await fn(client);
|
||||
} catch (e) {
|
||||
// Error logging can be helpful:
|
||||
if (typeof e.code === "string" && e.code.match(/^[0-9A-Z]{5}$/)) {
|
||||
console.error([e.message, e.code, e.detail, e.hint, e.where].join("\n"));
|
||||
}
|
||||
throw e;
|
||||
} finally {
|
||||
await client.query("ROLLBACK;");
|
||||
await client.query("RESET ALL;"); // Shouldn't be necessary, but just in case...
|
||||
client.release();
|
||||
}
|
||||
};
|
||||
|
||||
const withRootDb = (fn) => withDbFromUrl(TEST_DATABASE_URL, fn);
|
||||
|
||||
// const becomeRoot = (client) => client.query("reset role");
|
||||
|
||||
/******************************************************************************
|
||||
** **
|
||||
** BELOW HERE, YOU'LL WANT TO CUSTOMISE FOR YOUR OWN DATABASE SCHEMA **
|
||||
** **
|
||||
******************************************************************************/
|
||||
|
||||
// export const becomeUser = async (
|
||||
// client: PoolClient,
|
||||
// userOrUserId: User | string | null
|
||||
// ) => {
|
||||
// await becomeRoot(client);
|
||||
// const session = userOrUserId
|
||||
// ? await createSession(
|
||||
// client,
|
||||
// typeof userOrUserId === "object" ? userOrUserId.id : userOrUserId
|
||||
// )
|
||||
// : null;
|
||||
// await client.query(
|
||||
// `select set_config('role', $1::text, true),
|
||||
// set_config('jwt.claims.session_id', $2::text, true)`,
|
||||
// [process.env.DATABASE_VISITOR, session ? session.uuid : ""]
|
||||
// );
|
||||
// };
|
||||
|
||||
// // Enables multiple calls to `createUsers` within the same test to still have
|
||||
// // deterministic results without conflicts.
|
||||
// let userCreationCounter = 0;
|
||||
// beforeEach(() => {
|
||||
// userCreationCounter = 0;
|
||||
// });
|
||||
|
||||
// export const createUsers = async function createUsers(
|
||||
// client: PoolClient,
|
||||
// count: number = 1,
|
||||
// verified: boolean = true
|
||||
// ) {
|
||||
// const users = [];
|
||||
// if (userCreationCounter > 25) {
|
||||
// throw new Error("Too many users created!");
|
||||
// }
|
||||
// for (let i = 0; i < count; i++) {
|
||||
// const userLetter = "abcdefghijklmnopqrstuvwxyz"[userCreationCounter];
|
||||
// userCreationCounter++;
|
||||
// const password = userLetter.repeat(12);
|
||||
// const email = `${userLetter}${i || ""}@b.c`;
|
||||
// const user: User = (
|
||||
// await client.query(
|
||||
// `SELECT * FROM app_private.really_create_user(
|
||||
// username := $1,
|
||||
// email := $2,
|
||||
// email_is_verified := $3,
|
||||
// name := $4,
|
||||
// avatar_url := $5,
|
||||
// password := $6
|
||||
// )`,
|
||||
// [
|
||||
// `testuser_${userLetter}`,
|
||||
// email,
|
||||
// verified,
|
||||
// `User ${userLetter}`,
|
||||
// null,
|
||||
// password,
|
||||
// ]
|
||||
// )
|
||||
// ).rows[0];
|
||||
// expect(user.id).not.toBeNull();
|
||||
// user._email = email;
|
||||
// user._password = password;
|
||||
// users.push(user);
|
||||
// }
|
||||
// return users;
|
||||
// };
|
||||
|
||||
module.exports = {
|
||||
withRootDb,
|
||||
};
|
||||
|
|
@ -1,64 +0,0 @@
|
|||
const { withRootDb } = require("./helpers");
|
||||
|
||||
describe("checks identifier", () => {
|
||||
test("rejects invalid characters", () =>
|
||||
withRootDb(async (pgClient) => {
|
||||
try {
|
||||
await pgClient.query(
|
||||
`
|
||||
insert into sendou_ink.organization (identifier, owner_id, name, discord_invite_code)
|
||||
values ('in-the-zone-å', 1, 'Sendou', '');
|
||||
`
|
||||
);
|
||||
throw new Error("query should not pass");
|
||||
} catch (e) {
|
||||
expect(e.message).toContain("violates check constraint");
|
||||
}
|
||||
}));
|
||||
|
||||
test("rejects too short", () =>
|
||||
withRootDb(async (pgClient) => {
|
||||
try {
|
||||
await pgClient.query(
|
||||
`
|
||||
insert into sendou_ink.organization (identifier, owner_id, name, discord_invite_code)
|
||||
values ('i', 1, 'Sendou', '');
|
||||
`
|
||||
);
|
||||
throw new Error("query should not pass");
|
||||
} catch (e) {
|
||||
expect(e.message).toContain("violates check constraint");
|
||||
}
|
||||
}));
|
||||
|
||||
test("accepts normal valid", () =>
|
||||
withRootDb(async (pgClient) => {
|
||||
const {
|
||||
rows: [organization],
|
||||
} = await pgClient.query(
|
||||
`
|
||||
insert into sendou_ink.organization (identifier, owner_id, name, discord_invite_code)
|
||||
values ('in-the-zone', 1, 'Sendou', '')
|
||||
returning *;
|
||||
`
|
||||
);
|
||||
|
||||
expect(organization.identifier).toBe("in-the-zone");
|
||||
}));
|
||||
|
||||
test("accepts long", () =>
|
||||
withRootDb(async (pgClient) => {
|
||||
const identifier = new Array(50).fill("a").join("");
|
||||
const {
|
||||
rows: [organization],
|
||||
} = await pgClient.query(
|
||||
`
|
||||
insert into sendou_ink.organization (identifier, owner_id, name, discord_invite_code)
|
||||
values ('${identifier}', 1, 'Sendou', '')
|
||||
returning *;
|
||||
`
|
||||
);
|
||||
|
||||
expect(organization.identifier).toBe(identifier);
|
||||
}));
|
||||
});
|
||||
|
|
@ -1,67 +0,0 @@
|
|||
const { withRootDb } = require("./helpers");
|
||||
|
||||
describe("checks hsl args", () => {
|
||||
test("rejects invalid hsl arg", () =>
|
||||
withRootDb(async (pgClient) => {
|
||||
try {
|
||||
await pgClient.query(
|
||||
`
|
||||
insert into sendou_ink.tournament (banner_text_hsl_args, identifier, name, description, start_time, banner_background, organization_identifier)
|
||||
values ('31 9% 16%;', 'in-the-zone-xi', 'In The Zone X', 'In The Zone eXtremeeeee', '2022-06-22 20:00:00', 'linear-gradient(to bottom, #9796f0, #fbc7d4)', 'sendous');
|
||||
`
|
||||
);
|
||||
throw new Error("query should not pass");
|
||||
} catch (e) {
|
||||
expect(e.message).toContain("violates check constraint");
|
||||
}
|
||||
}));
|
||||
|
||||
test("accepts normal valid", () =>
|
||||
withRootDb(async (pgClient) => {
|
||||
const {
|
||||
rows: [tournament],
|
||||
} = await pgClient.query(
|
||||
`
|
||||
insert into sendou_ink.tournament (banner_text_hsl_args, identifier, name, description, start_time, banner_background, organization_identifier)
|
||||
values ('31 9% 16%', 'in-the-zone-xi', 'In The Zone X', 'In The Zone eXtremeeeee', '2022-06-22 20:00:00', 'linear-gradient(to bottom, #9796f0, #fbc7d4)', 'sendous')
|
||||
returning *;
|
||||
`
|
||||
);
|
||||
|
||||
expect(tournament.banner_text_hsl_args).toBe("31 9% 16%");
|
||||
}));
|
||||
});
|
||||
|
||||
describe("checks dates", () => {
|
||||
test("rejects start time in the past", () =>
|
||||
withRootDb(async (pgClient) => {
|
||||
try {
|
||||
await pgClient.query(
|
||||
`
|
||||
insert into sendou_ink.tournament (start_time, check_in_time, banner_text_hsl_args, identifier, name, description, banner_background, organization_identifier)
|
||||
values ('2020-06-22 20:00:00', '2020-06-22 19:00:00', '31 9% 16%', 'in-the-zone-xi', 'In The Zone X', 'In The Zone eXtremeeeee', 'linear-gradient(to bottom, #9796f0, #fbc7d4)', 'sendous')
|
||||
returning *;
|
||||
`
|
||||
);
|
||||
throw new Error("query should not pass");
|
||||
} catch (e) {
|
||||
expect(e.message).toContain("violates check constraint");
|
||||
}
|
||||
}));
|
||||
|
||||
test("rejects check-in time after start time", () =>
|
||||
withRootDb(async (pgClient) => {
|
||||
try {
|
||||
await pgClient.query(
|
||||
`
|
||||
insert into sendou_ink.tournament (start_time, check_in_time, banner_text_hsl_args, identifier, name, description, banner_background, organization_identifier)
|
||||
values ('2022-06-22 20:00:00', '2022-06-22 21:00:00', '31 9% 16%', 'in-the-zone-xi', 'In The Zone X', 'In The Zone eXtremeeeee', 'linear-gradient(to bottom, #9796f0, #fbc7d4)', 'sendous')
|
||||
returning *;
|
||||
`
|
||||
);
|
||||
throw new Error("query should not pass");
|
||||
} catch (e) {
|
||||
expect(e.message).toContain("violates check constraint");
|
||||
}
|
||||
}));
|
||||
});
|
||||
|
|
@ -1,159 +0,0 @@
|
|||
import { config } from "dotenv";
|
||||
config();
|
||||
|
||||
import Fastify, { FastifyReply, FastifyRequest } from "fastify";
|
||||
import cors from "fastify-cors";
|
||||
import {
|
||||
postgraphile,
|
||||
PostGraphileResponseFastify3,
|
||||
PostGraphileResponse,
|
||||
PostGraphileOptions,
|
||||
} from "postgraphile";
|
||||
|
||||
const PORT = 4000;
|
||||
|
||||
function NonNullRelationsPlugin(builder) {
|
||||
builder.hook("GraphQLObjectType:fields:field", (field, build, context) => {
|
||||
if (!context.scope.isPgForwardRelationField) {
|
||||
return field;
|
||||
}
|
||||
return {
|
||||
...field,
|
||||
type: new build.graphql.GraphQLNonNull(field.type),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// PostGraphile options; see https://www.graphile.org/postgraphile/usage-library/#api-postgraphilepgconfig-schemaname-options
|
||||
const options: PostGraphileOptions = {
|
||||
// pluginHook,
|
||||
appendPlugins: [NonNullRelationsPlugin],
|
||||
// pgSettings(req) {
|
||||
// // Adding this to ensure that all servers pass through the request in a
|
||||
// // good enough way that we can extract headers.
|
||||
// // CREATE FUNCTION current_user_id() RETURNS text AS $$ SELECT current_setting('graphile.test.x-user-id', TRUE); $$ LANGUAGE sql STABLE;
|
||||
// return {
|
||||
// 'graphile.test.x-user-id':
|
||||
// req.headers['x-user-id'] ||
|
||||
// // `normalizedConnectionParams` comes from websocket connections, where
|
||||
// // the headers often cannot be customized by the client.
|
||||
// (req as any).normalizedConnectionParams?.['x-user-id'],
|
||||
// };
|
||||
// },
|
||||
watchPg: true,
|
||||
graphiql: true,
|
||||
enhanceGraphiql: true,
|
||||
subscriptions: true,
|
||||
dynamicJson: true,
|
||||
setofFunctionsContainNulls: false,
|
||||
ignoreRBAC: false,
|
||||
showErrorStack: "json",
|
||||
extendedErrors: ["hint", "detail", "errcode"],
|
||||
allowExplain: true,
|
||||
legacyRelations: "omit",
|
||||
// exportGqlSchemaPath: `${__dirname}/schema.graphql`,
|
||||
sortExport: true,
|
||||
};
|
||||
|
||||
const middleware = postgraphile(process.env.DATABASE_URL, "sendou_ink", {
|
||||
...options,
|
||||
// pgSettings(req) {
|
||||
// // Adding this to ensure that all servers pass through the request in a
|
||||
// // good enough way that we can extract headers.
|
||||
// // CREATE FUNCTION current_user_id() RETURNS text AS $$ SELECT current_setting('graphile.test.x-user-id', TRUE); $$ LANGUAGE sql STABLE;
|
||||
// return {
|
||||
// 'graphile.test.x-user-id':
|
||||
// // In GraphiQL, open console and enter `document.cookie = "userId=3"` to become user 3.
|
||||
// (req._fastifyRequest as FastifyRequest)?.cookies.userId ||
|
||||
// // `normalizedConnectionParams` comes from websocket connections, where
|
||||
// // the headers often cannot be customized by the client.
|
||||
// (req as any).normalizedConnectionParams?.['x-user-id'],
|
||||
// };
|
||||
// },
|
||||
});
|
||||
|
||||
/******************************************************************************/
|
||||
// These middlewares aren't needed; we just add them to make sure that
|
||||
// PostGraphile still works correctly with them in place.
|
||||
|
||||
// import fastifyCompression from 'fastify-compress';
|
||||
// fastify.register(fastifyCompression, { threshold: 0, inflateIfDeflated: false });
|
||||
|
||||
// import fastifyCookie from 'fastify-cookie';
|
||||
// fastify.register(fastifyCookie, { secret: 'USE_A_SECURE_SECRET!' });
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const fastify = Fastify({ logger: true });
|
||||
|
||||
fastify.register(cors);
|
||||
|
||||
/**
|
||||
* Converts a PostGraphile route handler into a Fastify request handler.
|
||||
*/
|
||||
const convertHandler =
|
||||
(handler: (res: PostGraphileResponse) => Promise<void>) =>
|
||||
(request: FastifyRequest, reply: FastifyReply) =>
|
||||
handler(new PostGraphileResponseFastify3(request, reply));
|
||||
|
||||
// IMPORTANT: do **NOT** change these routes here; if you want to change the
|
||||
// routes, do so in PostGraphile options. If you change the routes here only
|
||||
// then GraphiQL won't know where to find the GraphQL endpoint and the GraphQL
|
||||
// endpoint won't know where to indicate the EventStream for watch mode is.
|
||||
// (There may be other problems too.)
|
||||
|
||||
// OPTIONS requests, for CORS/etc
|
||||
fastify.options(
|
||||
middleware.graphqlRoute,
|
||||
convertHandler(middleware.graphqlRouteHandler)
|
||||
);
|
||||
|
||||
// This is the main middleware
|
||||
fastify.post(
|
||||
middleware.graphqlRoute,
|
||||
convertHandler(middleware.graphqlRouteHandler)
|
||||
);
|
||||
|
||||
// GraphiQL, if you need it
|
||||
if (middleware.options.graphiql) {
|
||||
if (middleware.graphiqlRouteHandler) {
|
||||
fastify.head(
|
||||
middleware.graphiqlRoute,
|
||||
convertHandler(middleware.graphiqlRouteHandler)
|
||||
);
|
||||
fastify.get(
|
||||
middleware.graphiqlRoute,
|
||||
convertHandler(middleware.graphiqlRouteHandler)
|
||||
);
|
||||
}
|
||||
// Remove this if you don't want the PostGraphile logo as your favicon!
|
||||
if (middleware.faviconRouteHandler) {
|
||||
fastify.get("/favicon.ico", convertHandler(middleware.faviconRouteHandler));
|
||||
}
|
||||
}
|
||||
|
||||
// If you need watch mode, this is the route served by the
|
||||
// X-GraphQL-Event-Stream header; see:
|
||||
// https://github.com/graphql/graphql-over-http/issues/48
|
||||
if (middleware.options.watchPg) {
|
||||
if (middleware.eventStreamRouteHandler) {
|
||||
fastify.options(
|
||||
middleware.eventStreamRoute,
|
||||
convertHandler(middleware.eventStreamRouteHandler)
|
||||
);
|
||||
fastify.get(
|
||||
middleware.eventStreamRoute,
|
||||
convertHandler(middleware.eventStreamRouteHandler)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fastify.listen(PORT, (err, address) => {
|
||||
if (err) {
|
||||
fastify.log.error(String(err));
|
||||
process.exit(1);
|
||||
}
|
||||
fastify.log.info(
|
||||
`PostGraphiQL available at ${address}${middleware.graphiqlRoute} 🚀`
|
||||
);
|
||||
});
|
||||
|
|
@ -1,155 +0,0 @@
|
|||
-- todo dev only
|
||||
drop schema if exists sendou_ink cascade;
|
||||
drop schema if exists sendou_ink_private cascade;
|
||||
|
||||
create extension if not exists "uuid-ossp";
|
||||
|
||||
create schema sendou_ink;
|
||||
create schema sendou_ink_private;
|
||||
|
||||
create function sendou_ink_private.set_updated_at() returns trigger as $$
|
||||
begin
|
||||
new.updated_at := current_timestamp;
|
||||
return new;
|
||||
end;
|
||||
$$ language plpgsql;
|
||||
|
||||
-- ACCOUNT
|
||||
|
||||
-- no checks because this data is not provided by user but with the log in event
|
||||
create table sendou_ink.account (
|
||||
id serial primary key,
|
||||
discord_id text not null,
|
||||
discord_username text not null,
|
||||
discord_discriminator text not null,
|
||||
discord_avatar text,
|
||||
twitch text,
|
||||
twitter text,
|
||||
youtube_id text,
|
||||
youtube_name text,
|
||||
created_at timestamp default now(),
|
||||
updated_at timestamp default now()
|
||||
);
|
||||
|
||||
create function sendou_ink.account_discord_full_username(account sendou_ink.account) returns text as $$
|
||||
select account.discord_username || '#' || account.discord_discriminator
|
||||
$$ language sql stable;
|
||||
|
||||
create trigger account_updated_at before update
|
||||
on sendou_ink.account
|
||||
for each row
|
||||
execute procedure sendou_ink_private.set_updated_at();
|
||||
|
||||
-- ORGANIZATION
|
||||
|
||||
create table sendou_ink.organization (
|
||||
identifier text primary key
|
||||
check (identifier ~ '^[a-z0-9-]{2,50}$'),
|
||||
name text not null
|
||||
check (char_length(name) < 51),
|
||||
discord_invite_code text not null
|
||||
check (char_length(discord_invite_code) < 51),
|
||||
twitter text
|
||||
check (twitter ~ '^[a-zA-Z0-9_]{4,15}$'),
|
||||
created_at timestamp default now(),
|
||||
updated_at timestamp default now(),
|
||||
owner_id integer not null references sendou_ink.account(id)
|
||||
);
|
||||
|
||||
create function sendou_ink.organization_twitter_url(organization sendou_ink.organization) returns text as $$
|
||||
select 'https://twitter.com/' || organization.twitter
|
||||
$$ language sql stable;
|
||||
|
||||
create function sendou_ink.organization_discord_invite_url(organization sendou_ink.organization) returns text as $$
|
||||
select 'https://discord.com/invite/' || organization.discord_invite_code
|
||||
$$ language sql stable;
|
||||
|
||||
create index organization_owner_id on sendou_ink.organization (owner_id);
|
||||
|
||||
create trigger organization_updated_at before update
|
||||
on sendou_ink.organization
|
||||
for each row
|
||||
execute procedure sendou_ink_private.set_updated_at();
|
||||
|
||||
-- TOURNAMENT
|
||||
|
||||
create table sendou_ink.tournament (
|
||||
identifier text primary key
|
||||
check (identifier ~ '^[a-z0-9-]{2,50}$'),
|
||||
name text not null
|
||||
check (char_length(name) < 51),
|
||||
description text not null
|
||||
check (char_length(description) < 5000),
|
||||
start_time timestamp not null
|
||||
check (start_time > now()),
|
||||
check_in_time timestamp,
|
||||
-- TODO check
|
||||
banner_background text not null,
|
||||
banner_text_hsl_args text not null
|
||||
check (banner_text_hsl_args ~ '^[0-9]{1,3} [0-9]{1,3}% [0-9]{1,3}%$'),
|
||||
created_at timestamp default now(),
|
||||
updated_at timestamp default now(),
|
||||
organization_identifier text not null references sendou_ink.organization(identifier),
|
||||
CHECK (check_in_time < start_time)
|
||||
);
|
||||
|
||||
create function sendou_ink.tournament_text_color(tournament sendou_ink.tournament) returns text as $$
|
||||
select 'hsl(' || tournament.banner_text_hsl_args || ')'
|
||||
$$ language sql stable;
|
||||
|
||||
create index tournament_organization_identifier on sendou_ink.tournament(organization_identifier);
|
||||
|
||||
create trigger tournament_updated_at before update
|
||||
on sendou_ink.tournament
|
||||
for each row
|
||||
execute procedure sendou_ink_private.set_updated_at();
|
||||
|
||||
create table sendou_ink.mode_enum (
|
||||
name text primary key
|
||||
);
|
||||
comment on table sendou_ink.mode_enum is E'@enum';
|
||||
insert into sendou_ink.mode_enum (name) values
|
||||
('TW'),
|
||||
('SZ'),
|
||||
('TC'),
|
||||
('RM'),
|
||||
('CB');
|
||||
|
||||
create table sendou_ink.map_mode (
|
||||
id serial primary key,
|
||||
stage text not null,
|
||||
game_mode text not null references sendou_ink.mode_enum(name),
|
||||
unique (stage, game_mode)
|
||||
);
|
||||
|
||||
create table sendou_ink.map_pool (
|
||||
tournament_identifier text not null references sendou_ink.tournament(identifier),
|
||||
map_mode_id integer not null references sendou_ink.map_mode(id),
|
||||
unique (tournament_identifier, map_mode_id)
|
||||
);
|
||||
|
||||
create index map_pool_tournament_identifier on sendou_ink.map_pool(tournament_identifier);
|
||||
create index map_pool_map_mode_id on sendou_ink.map_pool(map_mode_id);
|
||||
|
||||
create table sendou_ink.tournament_team (
|
||||
id serial primary key,
|
||||
name text not null
|
||||
check (char_length(name) < 51),
|
||||
checked_in boolean not null default false,
|
||||
tournament_identifier text not null references sendou_ink.tournament(identifier),
|
||||
-- todo restrict access
|
||||
invite_code uuid not null unique default uuid_generate_v1mc(),
|
||||
unique (name, tournament_identifier)
|
||||
);
|
||||
|
||||
create index tournament_team_tournament_identifier on sendou_ink.tournament_team(tournament_identifier);
|
||||
|
||||
create table sendou_ink.tournament_team_roster (
|
||||
member_id integer not null references sendou_ink.account(id),
|
||||
tournament_team_id integer not null references sendou_ink.tournament_team(id),
|
||||
captain boolean not null default false,
|
||||
unique (member_id, tournament_team_id)
|
||||
);
|
||||
|
||||
create index tournament_team_roster_member_id on sendou_ink.tournament_team_roster(member_id);
|
||||
create index tournament_team_roster_tournament_team_id on sendou_ink.tournament_team_roster(tournament_team_id);
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
import { migrate } from "postgres-migrations";
|
||||
|
||||
async function main() {
|
||||
const dbConfig = {
|
||||
// TODO: env vars
|
||||
database: "sendou_ink_postgraphile",
|
||||
user: "sendou",
|
||||
password: "password",
|
||||
host: "localhost",
|
||||
port: 5432,
|
||||
ensureDatabaseExists: true,
|
||||
defaultDatabase: "postgres",
|
||||
};
|
||||
|
||||
await migrate(dbConfig, "migrations");
|
||||
}
|
||||
|
||||
main().then(() => console.log("Migrations done"));
|
||||
12368
postgraphile/package-lock.json
generated
|
|
@ -1,31 +0,0 @@
|
|||
{
|
||||
"name": "@sendou.ink/postgraphile",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"fastify": "^3.22.1",
|
||||
"fastify-cors": "^6.0.2",
|
||||
"postgraphile": "^4.12.5",
|
||||
"postgres-migrations": "^5.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/faker": "^5.5.9",
|
||||
"dotenv": "^10.0.0",
|
||||
"faker": "^5.5.3",
|
||||
"jest": "^27.3.1",
|
||||
"pg": "^8.7.1",
|
||||
"ts-node-dev": "^1.1.8"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "ts-node-dev index.ts",
|
||||
"delete-migrations": "node scripts/delete-migrations.js",
|
||||
"migrate": "node migrations/index.mjs",
|
||||
"seed": "node scripts/seed.js",
|
||||
"migrate:reset": "npm run delete-migrations && npm run migrate && npm run seed",
|
||||
"test": "jest"
|
||||
},
|
||||
"jest": {
|
||||
"testPathIgnorePatterns": [
|
||||
"__tests__/helpers.js"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
require("dotenv").config();
|
||||
const { Client } = require("pg");
|
||||
|
||||
async function main() {
|
||||
const client = new Client({ connectionString: process.env.DATABASE_URL });
|
||||
|
||||
try {
|
||||
await client.connect();
|
||||
await client.query("drop table migrations;");
|
||||
} finally {
|
||||
client.end();
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
|
|
@ -1,161 +0,0 @@
|
|||
require("dotenv").config();
|
||||
const { Client } = require("pg");
|
||||
const faker = require("faker");
|
||||
|
||||
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",
|
||||
];
|
||||
|
||||
const modesShort = ["TW", "SZ", "TC", "RM", "CB"];
|
||||
|
||||
async function main() {
|
||||
const client = new Client({ connectionString: process.env.DATABASE_URL });
|
||||
|
||||
try {
|
||||
await client.connect();
|
||||
await client.query(`
|
||||
insert into sendou_ink.account (discord_username, discord_discriminator, discord_avatar, twitch, twitter, youtube_id, youtube_name, discord_id)
|
||||
values ('Sendou', '0043', 'fcfd65a3bea598905abb9ca25296816b', 'Sendou', 'Sendouc', 'UCWbJLXByvsfQvTcR4HLPs5Q', 'Sendou', '79237403620945920');
|
||||
`);
|
||||
await client.query(`
|
||||
insert into sendou_ink.organization (identifier, name, discord_invite_code, twitter, owner_id)
|
||||
values ('sendous', 'Sendou´s tournaments', 'sendou', 'sendouc', 1);
|
||||
`);
|
||||
await client.query(`
|
||||
insert into sendou_ink.tournament (identifier, name, description, start_time, banner_background, banner_text_hsl_args, organization_identifier)
|
||||
values ('in-the-zone-x', 'In The Zone X', 'In The Zone eXtremeeeee', '2022-06-22 20:00:00', 'linear-gradient(to bottom, #9796f0, #fbc7d4)', '31 9% 16%', 'sendous');
|
||||
`);
|
||||
|
||||
for (const stage of stages) {
|
||||
for (const mode of modesShort) {
|
||||
await client.query(
|
||||
`
|
||||
insert into sendou_ink.map_mode (stage, game_mode)
|
||||
values ($1, $2);
|
||||
`,
|
||||
[stage, mode]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
await mapPool(client);
|
||||
await fakeUsers(client);
|
||||
await fakeTournamentTeams(client);
|
||||
} finally {
|
||||
client.end();
|
||||
}
|
||||
}
|
||||
|
||||
async function mapPool(client) {
|
||||
const ids = Array.from(
|
||||
new Set(new Array(24).fill(null).map(() => getRandomInt(115)))
|
||||
);
|
||||
|
||||
for (const id of ids) {
|
||||
await client.query(
|
||||
`
|
||||
insert into sendou_ink.map_pool (tournament_identifier, map_mode_id)
|
||||
values ('in-the-zone-x', $1);
|
||||
`,
|
||||
[id]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function fakeUsers(client) {
|
||||
for (let index = 0; index < 200; index++) {
|
||||
const name = faker.name.firstName();
|
||||
const discordDiscriminator = Array(4)
|
||||
.fill(null)
|
||||
.map(() => faker.datatype.number(9))
|
||||
.join("");
|
||||
const discordId = Array(17)
|
||||
.fill(null)
|
||||
.map(() => faker.datatype.number(9))
|
||||
.join("");
|
||||
|
||||
await client.query(
|
||||
`
|
||||
insert into sendou_ink.account (discord_username, discord_discriminator, discord_id)
|
||||
values ($1, $2, $3);
|
||||
`,
|
||||
[name, discordDiscriminator, discordId]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function fakeTournamentTeams(client) {
|
||||
const randomIds = faker.helpers.shuffle(
|
||||
Array(201)
|
||||
.fill(null)
|
||||
.map((_, i) => i + 1)
|
||||
);
|
||||
|
||||
for (let index = 0; index < 24; index++) {
|
||||
const name = faker.address.cityName();
|
||||
const captainId = randomIds.pop();
|
||||
|
||||
const {
|
||||
rows: [tournamentTeam],
|
||||
} = await client.query(
|
||||
`
|
||||
insert into sendou_ink.tournament_team (name, tournament_identifier)
|
||||
values ($1, 'in-the-zone-x')
|
||||
returning *;
|
||||
`,
|
||||
[name]
|
||||
);
|
||||
|
||||
await client.query(
|
||||
`
|
||||
insert into sendou_ink.tournament_team_roster (member_id, tournament_team_id, captain)
|
||||
values ($1, $2, true);
|
||||
`,
|
||||
[captainId, tournamentTeam.id]
|
||||
);
|
||||
|
||||
for (let index = 0; index < faker.datatype.number(6); index++) {
|
||||
const memberId = randomIds.pop();
|
||||
await client.query(
|
||||
`
|
||||
insert into sendou_ink.tournament_team_roster (member_id, tournament_team_id)
|
||||
values ($1, $2);
|
||||
`,
|
||||
[memberId, tournamentTeam.id]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getRandomInt(maxInclusive) {
|
||||
let result = -1;
|
||||
|
||||
while (result < 24) {
|
||||
result = Math.floor(Math.random() * maxInclusive) + 1;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
main();
|
||||
BIN
public/img/layout/analyzer.webp
Normal file
|
After Width: | Height: | Size: 9.6 KiB |
BIN
public/img/layout/badges.webp
Normal file
|
After Width: | Height: | Size: 5.4 KiB |
BIN
public/img/layout/battle.webp
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
public/img/layout/browse.webp
Normal file
|
After Width: | Height: | Size: 7.7 KiB |
BIN
public/img/layout/calendar.webp
Normal file
|
After Width: | Height: | Size: 7.3 KiB |
BIN
public/img/layout/gear.webp
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
BIN
public/img/layout/leaderboards.webp
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
public/img/layout/links.webp
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
public/img/layout/logo.webp
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
BIN
public/img/layout/maps.webp
Normal file
|
After Width: | Height: | Size: 8.4 KiB |
BIN
public/img/layout/planner.webp
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
public/img/layout/rankings.webp
Normal file
|
After Width: | Height: | Size: 8.5 KiB |
BIN
public/img/layout/rotations.webp
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
public/img/layout/top500.webp
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
public/img/layout/u.webp
Normal file
|
After Width: | Height: | Size: 8.6 KiB |
BIN
public/img/layout/xtrends.webp
Normal file
|
After Width: | Height: | Size: 8.6 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 626 KiB After Width: | Height: | Size: 626 KiB |
|
Before Width: | Height: | Size: 634 KiB After Width: | Height: | Size: 634 KiB |
|
Before Width: | Height: | Size: 738 KiB After Width: | Height: | Size: 738 KiB |