Setup URQL with own backend and create basic player list page

This commit is contained in:
Hammad Jutt
2020-08-17 01:17:14 -06:00
parent 78ee9e156c
commit b2a114bbdd
21 changed files with 264 additions and 129 deletions

7
packages/@types/fake-tag/index.d.ts vendored Normal file
View File

@@ -0,0 +1,7 @@
declare module 'fake-tag' {
function gql(
literals: TemplateStringsArray,
...placeholders: string[]
): string;
export = gql;
}

View File

@@ -9,7 +9,7 @@
"get-schema": "env-cmd -f ../../.env -x get-graphql-schema -h x-hasura-admin-secret=\\$HASURA_GRAPHQL_ADMIN_SECRET http://localhost:8080/v1/graphql > schema.graphql",
"update-schema": "yarn get-schema && yarn generate",
"generate": "graphql-codegen --config=graphql-codegen-gql.yaml && graphql-codegen --config=graphql-codegen-typescript.yaml",
"typecheck": "yarn generate"
"prepare": "yarn generate"
},
"dependencies": {
"graphql-tag": "^2.10.4"

View File

@@ -5,6 +5,7 @@ export const MetaTag: React.FC<TagProps> = ({ children, ...props }) => (
<Tag
fontFamily="body"
fontSize="sm"
fontWeight="bold"
backgroundColor="purpleTag"
color="white"
{...props}

View File

@@ -21,6 +21,7 @@ export {
Spinner,
Stack,
Text,
Container,
useTheme,
useToast,
} from '@chakra-ui/core';

View File

@@ -9,6 +9,12 @@ interface MetaColors {
purpleTag: string;
blueLight: string;
cyanText: string;
dark60: string;
diamond: string;
platinum: string;
gold: string;
silver: string;
bronze: string;
}
interface MetaTheme {
@@ -53,8 +59,14 @@ export const theme: Theme = {
800: '#150747',
900: '#07021d',
},
diamond: '#40e8ec',
platinum: '#81b6e3',
gold: '#d0a757',
silver: '#b0b0b0',
bronze: '#a97142',
offwhite: '#F6F8F9',
blue02: 'rgba(79, 105, 205, 0.2)',
dark60: 'rgba(0,0,0, 0.6)',
dark: '#1B0D2A',
purpleBoxDark: '#261943',
purpleBoxLight: '#392373',

17
packages/web/codegen.yml Normal file
View File

@@ -0,0 +1,17 @@
overwrite: true
require:
- ts-node/register
generates:
./graphql/autogen/types.tsx:
schema: '../codegen/schema.graphql'
documents:
- ./graphql/**/(!(*.d)).ts
plugins:
- typescript
- typescript-operations
- typescript-urql
- add:
content: '/* eslint-disable */'
config:
gqlImport: fake-tag
skipTypename: true

View File

@@ -1,29 +1,24 @@
import { Flex } from '@metafam/ds';
import React from 'react';
type Props = React.ComponentProps<typeof Flex>
type Props = React.ComponentProps<typeof Flex>;
export const PageContainer: React.FC<Props> = ({
children,
...props
}) => (
export const PageContainer: React.FC<Props> = ({ children, ...props }) => (
<Flex
bgSize="cover"
w="100vw"
w="100%"
h="100vh"
p={12}
direction="column"
align="center"
backgroundSize="cover"
{...props}
>
{children}
</Flex>
);
export const FlexContainer: React.FC<Props> = ({
children,
...props
}) => (
export const FlexContainer: React.FC<Props> = ({ children, ...props }) => (
<Flex align="center" justify="center" direction="column" {...props}>
{children}
</Flex>

View File

@@ -1,5 +1,7 @@
import { createClient } from 'urql';
import { CONFIG } from '../config';
export const client = createClient({
url: 'https://graphql-pokemon.now.sh/',
url: CONFIG.graphqlURL,
});

View File

@@ -0,0 +1,20 @@
import gql from 'fake-tag';
export const PlayerFragment = gql`
fragment PlayerFragment on Player {
id
username
totalXp
rank
ethereum_address
box_profile {
description
emoji
ethereumAddress
imageUrl
job
location
name
}
}
`;

View File

@@ -0,0 +1,23 @@
import gql from 'fake-tag';
import { GetPlayerQuery, GetPlayerQueryVariables } from './autogen/types';
import { client } from './client';
import { PlayerFragment } from './fragments';
const playerQuery = gql`
query GetPlayer($username: String!) {
Player(where: { username: { _eq: $username } }) {
...PlayerFragment
}
}
${PlayerFragment}
`;
export const getPlayer = async (username: string | undefined) => {
if (!username) return null;
const { data } = await client
.query<GetPlayerQuery, GetPlayerQueryVariables>(playerQuery, { username })
.toPromise();
return data?.Player[0];
};

View File

@@ -0,0 +1,32 @@
import gql from 'fake-tag';
import { GetPlayersQuery, GetPlayersQueryVariables } from './autogen/types';
import { client } from './client';
import { PlayerFragment } from './fragments';
const playersQuery = gql`
query GetPlayers($limit: Int) {
Player(order_by: { totalXp: desc }, limit: $limit) {
...PlayerFragment
}
}
${PlayerFragment}
`;
export const getPlayers = async (limit = 50) => {
const { data, error } = await client
.query<GetPlayersQuery, GetPlayersQueryVariables>(playersQuery, { limit })
.toPromise();
if (!data) {
if (error) {
throw new Error(
`${error.message}${JSON.stringify(error.graphQLErrors, null, 2)}`,
);
}
return [];
}
return data.Player;
};

View File

@@ -1,22 +0,0 @@
import { Pokemon } from '../types/pokemon';
import { client } from './client';
const pokemonQuery = `
query firstTwentyPokemons($name: String!) {
pokemon(name: $name) {
name
image
}
}
`;
export const getPokemon = async (
name: string | undefined,
): Promise<Pokemon | null> => {
if (!name) return null;
const {
data: { pokemon },
} = await client.query(pokemonQuery, { name }).toPromise();
return pokemon;
};

View File

@@ -1,22 +0,0 @@
import { Pokemon } from '../types/pokemon';
import { client } from './client';
const firstTwentyPokemonsQuery = `
query firstTwentyPokemons {
pokemons(first: 20) {
image
name
}
}
`;
export const getPokemons = async (): Promise<Array<Pokemon>> => {
const {
data: { pokemons },
} = await client.query(firstTwentyPokemonsQuery).toPromise();
return pokemons.map((pokemon: Pokemon) => ({
...pokemon,
name: pokemon.name.toLowerCase(),
}));
};

View File

@@ -7,10 +7,13 @@
"build": "next build && next export",
"start": "next start",
"typecheck": "tsc",
"precommit": "yarn lint-staged"
"precommit": "yarn lint-staged",
"generate": "graphql-codegen --config=codegen.yml",
"prepare": "yarn generate"
},
"dependencies": {
"@metafam/ds": "0.1.0",
"fake-tag": "2.0.0",
"graphql": "^15.0.0",
"isomorphic-unfetch": "^3.0.0",
"next": "latest",

View File

@@ -1,34 +1,60 @@
import { Box, Heading, Image, SimpleGrid } from '@metafam/ds';
import { Avatar, Box, Flex, Heading, MetaTag, Stack } from '@metafam/ds';
import { PageContainer } from 'components/Container';
import { MetaLink } from 'components/Link';
import { getPokemons } from 'graphql/getPokemons';
import { getPlayers } from 'graphql/getPlayers';
import { InferGetStaticPropsType } from 'next';
import React from 'react';
import BackgroundImage from '../public/images/login-background.jpg';
import { getPlayerImage, getPlayerName } from '../utils/playerHelpers';
type Props = InferGetStaticPropsType<typeof getStaticProps>;
export const getStaticProps = async () => {
const pokemon = await getPokemons();
const players = await getPlayers();
return {
props: {
pokemon,
players,
},
};
};
const Home: React.FC<Props> = ({ pokemon }) => (
<SimpleGrid columns={{ sm: 2, lg: 3 }} spacing={6}>
{pokemon.map((p, index) => (
<MetaLink
as={`/pokemon/${p.name}`}
href="pokemon/[name]"
key={index.toString()}
>
<Box key={p.name}>
<Heading style={{ textTransform: 'capitalize' }}>{p.name}</Heading>
<Image src={p.image} alt={p.name} />
</Box>
</MetaLink>
))}
</SimpleGrid>
const Home: React.FC<Props> = ({ players }) => (
<PageContainer backgroundImage={`url(${BackgroundImage})`}>
<Stack direction="column" spacing="8">
{players.map((p) => (
<MetaLink
as={`/player/${p.username}`}
href="player/[username]"
key={p.id}
flex={1}
>
<Flex key={p.id} dir="row" align="center">
<Avatar
size="lg"
src={getPlayerImage(p)}
name={getPlayerName(p)}
mr="6"
/>
<Box>
<Heading size="xs">{getPlayerName(p)}</Heading>
<Box mt="4">
<MetaTag
backgroundColor={p.rank?.toLowerCase()}
mr="3"
size="md"
color="dark60"
>
{p.rank}
</MetaTag>
<MetaTag size="md">XP: {Math.floor(p.totalXp)}</MetaTag>
</Box>
</Box>
</Flex>
</MetaLink>
))}
</Stack>
</PageContainer>
);
export default Home;

View File

@@ -11,7 +11,10 @@ import MetaGameImage from '../public/images/metagame.png';
const Login: React.FC = () => {
const [step, setStep] = useState(0);
return (
<PageContainer backgroundImage={`url(${BackgroundImage})`}>
<PageContainer
backgroundImage={`url(${BackgroundImage})`}
backgroundSize="cover"
>
<SimpleGrid
columns={3}
alignItems="center"

View File

@@ -0,0 +1,62 @@
import { Avatar, Box, Heading } from '@metafam/ds';
import {
GetStaticPaths,
GetStaticPropsContext,
InferGetStaticPropsType,
} from 'next';
import Error from 'next/error';
import React from 'react';
import { PageContainer } from '../../components/Container';
import { getPlayer } from '../../graphql/getPlayer';
import { getPlayers } from '../../graphql/getPlayers';
import { getPlayerImage, getPlayerName } from '../../utils/playerHelpers';
type Props = InferGetStaticPropsType<typeof getStaticProps>;
const PlayerPage: React.FC<Props> = ({ player }) => {
if (!player) {
return <Error statusCode={404} />;
}
return (
<PageContainer>
<Box>
<Heading size="md" textAlign="center">
{player.username}
</Heading>
<Avatar
size="xl"
src={getPlayerImage(player)}
name={getPlayerName(player)}
/>
</Box>
</PageContainer>
);
};
export default PlayerPage;
export const getStaticPaths: GetStaticPaths = async () => {
const players = await getPlayers();
return {
paths: players.map(({ username }) => ({
params: { username },
})),
fallback: false,
};
};
export const getStaticProps = async (
context: GetStaticPropsContext<{ username: string }>,
) => {
const username = context.params?.username;
const player = await getPlayer(username);
return {
props: {
player,
},
};
};

View File

@@ -1,52 +0,0 @@
import { Box, Flex, Heading, Image } from '@metafam/ds';
import { GetStaticPaths, GetStaticProps } from 'next';
import Error from 'next/error';
import { getPokemon } from '../../graphql/getPokemon';
import { getPokemons } from '../../graphql/getPokemons';
import { Pokemon } from '../../types/pokemon';
type Props = {
pokemon: Pokemon | null;
};
const PokemonPage: React.FC<Props> = ({ pokemon }) => {
if (!pokemon) {
return <Error statusCode={404} />;
}
return (
<Flex align="center" justify="center">
<Box>
<Heading textAlign="center">{pokemon.name}</Heading>
<Image src={pokemon.image} alt={pokemon.name} />
</Box>
</Flex>
);
};
export default PokemonPage;
export const getStaticPaths: GetStaticPaths = async () => {
const pokemons = await getPokemons();
return {
paths: pokemons.map(({ name }) => ({
params: { name },
})),
fallback: false,
};
};
export const getStaticProps: GetStaticProps<Props, { name: string }> = async (
context,
) => {
const name = context.params?.name;
const pokemon = await getPokemon(name);
return {
props: {
pokemon,
},
};
};

View File

@@ -0,0 +1,8 @@
import { PlayerFragmentFragment } from '../graphql/autogen/types';
export const getPlayerImage = (player: PlayerFragmentFragment): string =>
player.box_profile?.imageUrl ||
`https://avatars.dicebear.com/api/jdenticon/${player.username}.svg`;
export const getPlayerName = (player: PlayerFragmentFragment): string =>
player.box_profile?.name || player.username;