Fetch data from GraphQL

This commit is contained in:
Kalle (Sendou) 2021-11-09 23:23:43 +02:00
parent 4e0e86e362
commit 1c05643de6
14 changed files with 10736 additions and 342 deletions

9
frontend/codegen.yml Normal file
View File

@ -0,0 +1,9 @@
overwrite: true
schema: "http://localhost:4000/graphql"
documents: "**/*.graphql"
generates:
generated/graphql.ts:
plugins:
- "typescript"
- "typescript-operations"
- "typescript-urql"

View File

@ -4,45 +4,53 @@ import { useTournamentData } from "../../hooks/data/useTournamentData";
export function InfoBanner() {
const { data } = useTournamentData();
const tournament = data?.tournamentByIdentifier;
// TODO: handle loading
// TODO: handle error in parent
if (!data) return null;
if (!tournament) return null;
return (
<S_Container
style={
{
"--background": data.bannerBackground,
"--text": `hsl(${data.bannerTextHSLArgs})`,
"--text-transparent": `hsla(${data.bannerTextHSLArgs}, 0.2)`,
"--background": tournament.bannerBackground,
"--text": tournament.textColor,
} as any
}
>
<S_TopRow>
<S_DateName>
<S_MonthDate>
<S_Month>{shortMonthName(data.startTime)}</S_Month>
<S_Date>{dayNumber(data.startTime)}</S_Date>
<S_Month>{shortMonthName(tournament.startTime)}</S_Month>
<S_Date>{dayNumber(tournament.startTime)}</S_Date>
</S_MonthDate>
<S_TournamentName>{data.name}</S_TournamentName>
<S_TournamentName>{tournament.name}</S_TournamentName>
</S_DateName>
<S_IconButtons>
{data.organizer.twitter && (
<S_IconButton href={data.organizer.twitter}>
{tournament.organizationByOrganizationIdentifier.twitterUrl && (
<S_IconButton
href={tournament.organizationByOrganizationIdentifier.twitterUrl}
>
<FaTwitter />
</S_IconButton>
)}
<S_IconButton href={data.organizer.discordInvite}>
<FaDiscord />
</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(data.startTime)}</div>
<div>{weekdayAndStartTime(tournament.startTime)}</div>
</S_InfoContainer>
<S_InfoContainer>
<S_InfoLabel>Format</S_InfoLabel>
@ -50,7 +58,7 @@ export function InfoBanner() {
</S_InfoContainer>
<S_InfoContainer>
<S_InfoLabel>Organizer</S_InfoLabel>
<div>{data.organizer.name}</div>
<div>{tournament.organizationByOrganizationIdentifier.name}</div>
</S_InfoContainer>
</S_Infos>
</S_BottomRow>

View File

@ -3,35 +3,37 @@ 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 { GetTournamentByOrganizationAndName } from "@sendou-ink/api";
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 (!data) return null;
if (!mapPool) return null;
return (
<S_Container>
<S_InfoSquare>
<S_EmphasizedText>{data.mapPool.length} maps</S_EmphasizedText>
<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(data.mapPool)[stage] ? undefined : "bw"}
filter={modesPerStage(mapPool)[stage] ? undefined : "bw"}
width={256}
height={144}
/>
{modesPerStage(data.mapPool)[stage] && (
{modesPerStage(mapPool)[stage] && (
<S_ModeImagesContainer>
{modesShort.map((mode) => {
if (
!modesPerStage(data.mapPool)[stage]?.includes(mode as Mode)
) {
if (!modesPerStage(mapPool)[stage]?.includes(mode as Mode)) {
return null;
}
return (
@ -53,14 +55,17 @@ export function MapPoolTab() {
}
export function modesPerStage(
mapPool: GetTournamentByOrganizationAndName["mapPool"]
mapPool: {
stage: string;
gameMode: ModeEnum;
}[]
) {
return mapPool.reduce((acc: Record<string, Mode[]>, { name, mode }) => {
if (!acc[name]) {
acc[name] = [];
return mapPool.reduce((acc: Record<string, Mode[]>, { stage, gameMode }) => {
if (!acc[stage]) {
acc[stage] = [];
}
acc[name].push(mode);
acc[stage].push(gameMode);
return acc;
}, {});
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,36 @@
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
}
}
}
}
}

View File

@ -1,15 +1,13 @@
import type { GetTournamentByOrganizationAndName } from "@sendou-ink/api";
import { useTournamentByIdentifierQuery } from "generated/graphql";
import { useRouter } from "next/dist/client/router";
import { useMySWR } from "hooks/useMySWR";
export function useTournamentData() {
const router = useRouter();
const { organization, tournament } = router.query;
const result = useMySWR<GetTournamentByOrganizationAndName>(
typeof organization === "string" && typeof tournament === "string"
? `/tournaments/${organization}/${tournament}`
: null
);
const { tournament } = router.query;
// TODO: as
const [result] = useTournamentByIdentifierQuery({
variables: { identifier: tournament as string },
});
return result;
}

File diff suppressed because it is too large Load Diff

View File

@ -5,19 +5,26 @@
"dev": "next dev",
"build": "next build",
"start": "next start",
"test": "jest"
"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",
"swr": "^1.0.1"
"swr": "^1.0.1",
"urql": "^2.0.5"
},
"devDependencies": {
"@graphql-codegen/cli": "2.2.2",
"@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"

View File

@ -6,6 +6,7 @@ import Head from "next/head";
import { SWRConfig } from "swr";
import { Layout } from "components/layout/Layout";
import { globalCss } from "stitches.config";
import { Provider, createClient } from "urql";
const globalStyles = globalCss({
"*": { boxSizing: "border-box" },
@ -22,6 +23,10 @@ const globalStyles = globalCss({
},
});
const client = createClient({
url: "http://localhost:4000/graphql",
});
export default function App(props: AppProps) {
const { Component, pageProps } = props;
@ -36,18 +41,20 @@ export default function App(props: AppProps) {
content="minimum-scale=1, initial-scale=1, width=device-width"
/>
</Head>
<SWRConfig
value={{
fetcher: (resource, init) =>
fetch(process.env.NEXT_PUBLIC_BACKEND_URL + resource, init).then(
(res) => res.json()
),
}}
>
<Layout>
<Component {...pageProps} />
</Layout>
</SWRConfig>
<Provider value={client}>
<SWRConfig
value={{
fetcher: (resource, init) =>
fetch(process.env.NEXT_PUBLIC_BACKEND_URL + resource, init).then(
(res) => res.json()
),
}}
>
<Layout>
<Component {...pageProps} />
</Layout>
</SWRConfig>
</Provider>
</>
);
}

View File

@ -2,6 +2,7 @@ import { config } from "dotenv";
config();
import Fastify, { FastifyReply, FastifyRequest } from "fastify";
import cors from "fastify-cors";
import {
postgraphile,
PostGraphileResponseFastify3,
@ -11,10 +12,22 @@ import {
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: [MySubscriptionPlugin],
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.
@ -73,6 +86,8 @@ const middleware = postgraphile(process.env.DATABASE_URL, "sendou_ink", {
const fastify = Fastify({ logger: true });
fastify.register(cors);
/**
* Converts a PostGraphile route handler into a Fastify request handler.
*/

View File

@ -93,9 +93,9 @@ create table sendou_ink.tournament (
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 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);

View File

@ -9,6 +9,7 @@
"version": "1.0.0",
"dependencies": {
"fastify": "^3.22.1",
"fastify-cors": "^6.0.2",
"postgraphile": "^4.12.5",
"postgres-migrations": "^5.3.0"
},
@ -2533,11 +2534,25 @@
"tiny-lru": "^7.0.0"
}
},
"node_modules/fastify-cors": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/fastify-cors/-/fastify-cors-6.0.2.tgz",
"integrity": "sha512-sE0AOyzmj5hLLRRVgenjA6G2iOGX35/1S3QGYB9rr9TXelMZB3lFrXy4CzwYVOMiujJeMiLgO4J7eRm8sQSv8Q==",
"dependencies": {
"fastify-plugin": "^3.0.0",
"vary": "^1.1.2"
}
},
"node_modules/fastify-error": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/fastify-error/-/fastify-error-0.3.1.tgz",
"integrity": "sha512-oCfpcsDndgnDVgiI7bwFKAun2dO+4h84vBlkWsWnz/OUK9Reff5UFoFl241xTiLeHWX/vU9zkDVXqYUxjOwHcQ=="
},
"node_modules/fastify-plugin": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-3.0.0.tgz",
"integrity": "sha512-ZdCvKEEd92DNLps5n0v231Bha8bkz1DjnPP/aEz37rz/q42Z5JVLmgnqR4DYuNn3NXAO3IDCPyRvgvxtJ4Ym4w=="
},
"node_modules/fastify-warning": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/fastify-warning/-/fastify-warning-0.2.0.tgz",
@ -6734,6 +6749,14 @@
"node": ">= 8"
}
},
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/w3c-hr-time": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz",
@ -8964,11 +8987,25 @@
"tiny-lru": "^7.0.0"
}
},
"fastify-cors": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/fastify-cors/-/fastify-cors-6.0.2.tgz",
"integrity": "sha512-sE0AOyzmj5hLLRRVgenjA6G2iOGX35/1S3QGYB9rr9TXelMZB3lFrXy4CzwYVOMiujJeMiLgO4J7eRm8sQSv8Q==",
"requires": {
"fastify-plugin": "^3.0.0",
"vary": "^1.1.2"
}
},
"fastify-error": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/fastify-error/-/fastify-error-0.3.1.tgz",
"integrity": "sha512-oCfpcsDndgnDVgiI7bwFKAun2dO+4h84vBlkWsWnz/OUK9Reff5UFoFl241xTiLeHWX/vU9zkDVXqYUxjOwHcQ=="
},
"fastify-plugin": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-3.0.0.tgz",
"integrity": "sha512-ZdCvKEEd92DNLps5n0v231Bha8bkz1DjnPP/aEz37rz/q42Z5JVLmgnqR4DYuNn3NXAO3IDCPyRvgvxtJ4Ym4w=="
},
"fastify-warning": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/fastify-warning/-/fastify-warning-0.2.0.tgz",
@ -12132,6 +12169,11 @@
}
}
},
"vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw="
},
"w3c-hr-time": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz",

View File

@ -3,6 +3,7 @@
"version": "1.0.0",
"dependencies": {
"fastify": "^3.22.1",
"fastify-cors": "^6.0.2",
"postgraphile": "^4.12.5",
"postgres-migrations": "^5.3.0"
},

View File

@ -50,10 +50,13 @@ async function main() {
for (const stage of stages) {
for (const mode of modesShort) {
await client.query(`
await client.query(
`
insert into sendou_ink.map_mode (stage, game_mode)
values ('${stage}', '${mode}');
`);
values ($1, $2);
`,
[stage, mode]
);
}
}
@ -71,10 +74,13 @@ async function mapPool(client) {
);
for (const id of ids) {
await client.query(`
await client.query(
`
insert into sendou_ink.map_pool (tournament_identifier, map_mode_id)
values ('in-the-zone-x', '${id}');
`);
values ('in-the-zone-x', $1);
`,
[id]
);
}
}
@ -90,10 +96,13 @@ async function fakeUsers(client) {
.map(() => faker.datatype.number(9))
.join("");
await client.query(`
await client.query(
`
insert into sendou_ink.account (discord_username, discord_discriminator, discord_id)
values ('${name}', '${discordDiscriminator}', '${discordId}');
`);
values ($1, $2, $3);
`,
[name, discordDiscriminator, discordId]
);
}
}
@ -101,7 +110,7 @@ async function fakeTournamentTeams(client) {
const randomIds = faker.helpers.shuffle(
Array(201)
.fill(null)
.map((_, i) => i)
.map((_, i) => i + 1)
);
for (let index = 0; index < 24; index++) {
@ -110,25 +119,32 @@ async function fakeTournamentTeams(client) {
const {
rows: [tournamentTeam],
} = await client.query(`
} = await client.query(
`
insert into sendou_ink.tournament_team (name, tournament_identifier)
values ('${name}', 'in-the-zone-x')
values ($1, 'in-the-zone-x')
returning *;
`);
`,
[name]
);
await client.query(`
await client.query(
`
insert into sendou_ink.tournament_team_roster (member_id, tournament_team_id, captain)
values ('${captainId}', '${tournamentTeam.id}', true)
returning *;
`);
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 ('${memberId}', '${tournamentTeam.id}')
returning *;
`);
await client.query(
`
insert into sendou_ink.tournament_team_roster (member_id, tournament_team_id)
values ($1, $2);
`,
[memberId, tournamentTeam.id]
);
}
}
}