added filters for /players page (#566)

* player filters for type and skills

* fetching multiple players in parallel

* removed tile fragment

* search by username or address

* better spacing

* availability filter

* timezone filter

* passing tests in ds

* submit form in search bar

* added better labels for timezone

* fixed test issue

* searching only if search >= 2 char

* meta select ds

* updated metabutton bg color

* parallel for > 50 only

* fix reset search filter
This commit is contained in:
dan13ram
2021-05-19 20:49:06 +05:30
committed by GitHub
parent cc8326361e
commit c2e3782d28
29 changed files with 686 additions and 180 deletions

View File

@@ -0,0 +1,7 @@
module.exports = {
env: {
test: {
presets: ['@babel/preset-env', '@babel/preset-react'],
},
},
};

View File

@@ -0,0 +1,3 @@
module.exports = {
transformIgnorePatterns: ['SelectTimezone'],
};

View File

@@ -24,18 +24,20 @@
"react": ">=16"
},
"dependencies": {
"@types/react": "17.0.5",
"@types/react-dom": "17.0.5",
"@types/react-select": "3.0.22",
"@chakra-ui/icons": "1.0.13",
"@chakra-ui/react": "1.6.1",
"@chakra-ui/theme-tools": "1.1.7",
"@emotion/react": "11.4.0",
"@emotion/styled": "11.3.0",
"framer-motion": "4.1.16",
"@types/react": "17.0.5",
"@types/react-dom": "17.0.5",
"@types/react-select": "3.0.22",
"framer-motion": "4.1.17",
"next": "10.2.0",
"react-select": "4.3.1",
"react-timezone-select": "0.9.8",
"react-timezone-select": "1.0.3",
"spacetime": "6.16.1",
"spacetime-informal": "0.6.1",
"storybook-addon-performance": "0.15.2"
},
"devDependencies": {

View File

@@ -14,6 +14,7 @@ export const MetaButton: React.FC<
letterSpacing="0.1em"
size="lg"
fontSize="sm"
bg="purple.400"
ref={ref}
{...props}
>

View File

@@ -0,0 +1,19 @@
import { Select, SelectProps } from '@chakra-ui/react';
import React from 'react';
import { DropDownIcon } from './icons/DropDownIcon';
export const MetaSelect: React.FC<SelectProps> = (props) => (
<Select
textTransform="uppercase"
maxW="48"
bg="dark"
iconColor="purple.400"
iconSize="xs"
icon={<DropDownIcon boxSize={2} />}
borderColor="purple.400"
borderWidth="2px"
borderRadius="4px"
{...props}
/>
);

View File

@@ -1,10 +1,46 @@
/* istanbul ignore file */
import React from 'react';
import { Styles } from 'react-select';
import TimezoneSelect, { TimezoneSelectProps } from 'react-timezone-select';
import { i18nTimezones } from 'react-timezone-select/dist/index.js';
import spacetime from 'spacetime';
import informal from 'spacetime-informal';
import { theme } from './theme';
export const selectStyles: Styles = {
export type TimezoneType = {
id: string;
label: string;
};
export const TimezoneOptions: TimezoneType[] = Object.entries(
i18nTimezones,
).map((zone) => {
const now = spacetime.now().goto(zone[0]);
const tz = now.timezone();
const tzStrings = informal.display(zone[0]);
let abbrev = zone[0];
if (tzStrings && tzStrings.daylight && tzStrings.standard) {
abbrev = now.isDST()
? tzStrings.daylight.abbrev
: tzStrings.standard.abbrev;
}
const min = tz.current.offset * 60;
const hr = `${(min / 60) ^ 0}:${min % 60 === 0 ? '00' : Math.abs(min % 60)}`;
const prefix = `(GMT${hr.includes('-') ? hr : `+${hr}`}) ${zone[1]}`;
const label = `${prefix} ${abbrev.length < 5 ? `(${abbrev})` : ''}`;
return {
id: zone[0],
label,
};
});
const selectStyles: Styles = {
menu: (styles) => ({
...styles,
background: theme.colors.dark,

View File

@@ -0,0 +1,13 @@
import { createIcon } from '@chakra-ui/icons';
import * as React from 'react';
export const DropDownIcon = createIcon({
displayName: 'DropDownIcon',
path: (
<path
d="M10 0H2C1.17595 0 0.705573 0.940764 1.2 1.6L5.2 6.93333C5.6 7.46667 6.4 7.46667 6.8 6.93333L10.8 1.6C11.2944 0.940764 10.824 0 10 0Z"
fill="currentColor"
/>
),
viewBox: '0 0 12 8',
});

View File

@@ -1,2 +1,3 @@
export { BrightIdIcon } from './BrightIdIcon';
export { DropDownIcon } from './DropDownIcon';
export { Icon3box } from './Icon3box';

View File

@@ -1,15 +1,20 @@
export { BoxedNextImage } from './BoxedNextImage';
export { ConfirmModal } from './ConfirmModal';
export { BrightIdIcon, Icon3box } from './icons';
export * from './icons';
export { LoadingState } from './LoadingState';
export { MetaBox } from './MetaBox';
export { MetaButton } from './MetaButton';
export { MetaHeading } from './MetaHeading';
export { MetaSelect } from './MetaSelect';
export { MetaTag } from './MetaTag';
export { MetaTile, MetaTileBody, MetaTileHeader } from './MetaTile';
export { ResponsiveText } from './ResponsiveText';
export { SelectSearch, selectStyles } from './SelectSearch';
export { SelectTimeZone } from './SelectTimeZone';
export {
SelectTimeZone,
TimezoneOptions,
TimezoneType,
} from './SelectTimeZone';
export { SVG } from './SVG';
export { theme as MetaTheme } from './theme';
export { H1, P } from './typography';

View File

@@ -9,6 +9,7 @@ export type MetaColors = ChakraTheme['colors'] & {
purpleBoxDark: string;
purpleBoxLight: string;
purpleTag: string;
purpleTag30: string;
blueLight: string;
cyanText: string;
diamond: string;
@@ -36,6 +37,7 @@ export const colors: MetaColors = {
purpleBoxDark: '#261943',
purpleBoxLight: '#392373',
purpleTag: '#40347C',
purpleTag30: 'rgba(64, 52, 124, 0.3)',
blueLight: '#A5B9F6',
cyanText: '#79F8FB',
discord: '#7289da',

View File

@@ -14,6 +14,10 @@ export const theme: Theme = extendTheme({
background: 'dark',
color: 'white',
minHeight: '100vh',
option: {
background: 'dark',
color: 'white',
},
},
},
},

View File

@@ -1,6 +1,7 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"noEmit": true,

View File

@@ -0,0 +1,222 @@
import {
Input,
MetaButton,
MetaSelect,
Stack,
Text,
TimezoneOptions,
TimezoneType,
VStack,
Wrap,
WrapItem,
} from '@metafam/ds';
import {
GetPlayersQueryVariables,
PlayerFragmentFragment,
} from 'graphql/autogen/types';
import { PlayerAggregates, QueryVariableSetter } from 'lib/hooks/players';
import React, { useState } from 'react';
type Props = {
fetching: boolean;
players: PlayerFragmentFragment[];
aggregates: PlayerAggregates;
queryVariables: GetPlayersQueryVariables;
setQueryVariable: QueryVariableSetter;
};
export const PlayerFilter: React.FC<Props> = ({
fetching,
players,
aggregates,
queryVariables,
setQueryVariable,
}) => {
const [search, setSearch] = useState<string>('');
const onSearch = (e: React.ChangeEvent<HTMLFormElement>) => {
e.preventDefault();
if (search.length >= 2) {
setQueryVariable('search', `%${search}%`);
} else {
setQueryVariable('search', `%%`);
}
};
return (
<>
<form onSubmit={onSearch}>
<Stack
spacing="4"
w="100%"
maxW="2xl"
direction={{ base: 'column', md: 'row' }}
align="center"
>
<Input
background="dark"
w="100%"
type="text"
minW={{ base: 'sm', sm: 'md', md: 'lg', lg: 'xl' }}
placeholder="SEARCH PLAYERS BY USERNAME OR ETHEREUM ADDRESS"
_placeholder={{ color: 'whiteAlpha.500' }}
value={search}
onChange={(e) => setSearch(e.target.value)}
size="lg"
borderRadius="0"
borderColor="purple.400"
fontSize="md"
borderWidth="2px"
/>
<MetaButton type="submit" size="lg" isLoading={fetching} px="16">
SEARCH
</MetaButton>
</Stack>
</form>
<Wrap
justify="space-between"
w="100%"
bg="whiteAlpha.200"
style={{ backdropFilter: 'blur(7px)' }}
p="6"
borderRadius="6px"
maxW="79rem"
>
<WrapItem>
<Wrap spacing="4">
<WrapItem>
<VStack spacing="2" w="100%">
<Text
textTransform="uppercase"
color="blueLight"
w="100%"
fontSize="xs"
>
Show
</Text>
<MetaSelect
value={queryVariables.limit as number}
onChange={(e) =>
setQueryVariable('limit', Number(e.target.value))
}
minW="3rem"
>
<option value={10}>10</option>
<option value={20}>20</option>
<option value={50}>50</option>
<option value={150}>150</option>
</MetaSelect>
</VStack>
</WrapItem>
<WrapItem>
<VStack spacing="2" w="100%">
<Text
textTransform="uppercase"
color="blueLight"
w="100%"
fontSize="xs"
>
Player Type
</Text>
<MetaSelect
value={(queryVariables.playerType as number) || ''}
onChange={(e) =>
setQueryVariable('playerType', e.target.value)
}
>
<option value="">All Types</option>
{aggregates.playerTypes &&
aggregates.playerTypes.map(({ id, title }) => (
<option key={id} value={id}>
{title}
</option>
))}
</MetaSelect>
</VStack>
</WrapItem>
<WrapItem>
<VStack spacing="2" w="100%">
<Text
textTransform="uppercase"
color="blueLight"
w="100%"
fontSize="xs"
>
Skills
</Text>
<MetaSelect
value={(queryVariables.skillCategory as string) || ''}
onChange={(e) =>
setQueryVariable('skillCategory', e.target.value)
}
>
<option value="">All Skills</option>
{aggregates.skillCategories &&
aggregates.skillCategories.map(({ name }) => (
<option key={name} value={name}>
{name}
</option>
))}
</MetaSelect>
</VStack>
</WrapItem>
<WrapItem>
<VStack spacing="2" w="100%">
<Text
textTransform="uppercase"
color="blueLight"
w="100%"
fontSize="xs"
>
Availability
</Text>
<MetaSelect
value={queryVariables.availability as number}
onChange={(e) =>
setQueryVariable('availability', e.target.value)
}
>
<option value={0}>Any h/week</option>
<option value={1}>{'> 1 h/week'}</option>
<option value={5}>{'> 5 h/week'}</option>
<option value={10}>{'> 10 h/week'}</option>
<option value={20}>{'> 20 h/week'}</option>
<option value={30}>{'> 30 h/week'}</option>
<option value={40}>{'> 40 h/week'}</option>
</MetaSelect>
</VStack>
</WrapItem>
<WrapItem>
<VStack spacing="2" w="100%">
<Text
textTransform="uppercase"
color="blueLight"
w="100%"
fontSize="xs"
>
Timezone
</Text>
<MetaSelect
value={(queryVariables.timezone as string) || ''}
onChange={(e) => setQueryVariable('timezone', e.target.value)}
>
<option value="">All timezones</option>
{TimezoneOptions.map((z: TimezoneType) => (
<option key={z.id} value={z.id}>
{z.label}
</option>
))}
</MetaSelect>
</VStack>
</WrapItem>
</Wrap>
</WrapItem>
{players && !fetching && (
<WrapItem>
<Text align="center" fontWeight="bold">
{players.length} players
</Text>
</WrapItem>
)}
</Wrap>
</>
);
};

View File

@@ -0,0 +1,23 @@
import { SimpleGrid, Text } from '@metafam/ds';
import { PlayerTile } from 'components/Player/PlayerTile';
import { PlayerFragmentFragment } from 'graphql/autogen/types';
import React from 'react';
type Props = {
players: PlayerFragmentFragment[];
};
export const PlayerList: React.FC<Props> = ({ players }) =>
players.length > 0 ? (
<SimpleGrid
columns={[1, null, 2, 3]}
spacing="8"
autoRows="minmax(35rem, auto)"
>
{players.map((p) => (
<PlayerTile key={p.username} player={p} />
))}
</SimpleGrid>
) : (
<Text>No players found</Text>
);

View File

@@ -1,20 +0,0 @@
import { SimpleGrid } from '@metafam/ds';
import { PlayerTile } from 'components/Player/PlayerTile';
import { PlayerFragmentFragment } from 'graphql/autogen/types';
import React from 'react';
type Props = {
players: PlayerFragmentFragment[];
};
export const PlayerList: React.FC<Props> = ({ players }) => (
<SimpleGrid
columns={[1, null, 2, 3]}
spacing="8"
autoRows="minmax(35rem, auto)"
>
{players.map((p) => (
<PlayerTile key={p.id} player={p} />
))}
</SimpleGrid>
);

View File

@@ -1,7 +1,7 @@
import {
Flex,
MetaButton,
Select,
MetaSelect,
Switch,
Text,
Wrap,
@@ -43,7 +43,7 @@ export const QuestFilter: React.FC<Props> = ({
<WrapItem>
<Wrap>
<WrapItem>
<Select
<MetaSelect
value={queryVariables.limit as number}
onChange={(e) =>
setQueryVariable('limit', Number(e.target.value))
@@ -52,28 +52,28 @@ export const QuestFilter: React.FC<Props> = ({
<option value={10}>10</option>
<option value={20}>20</option>
<option value={50}>50</option>
</Select>
</MetaSelect>
</WrapItem>
<WrapItem>
<Select
<MetaSelect
value={queryVariables.order as string}
onChange={(e) => setQueryVariable('order', e.target.value)}
>
<option value={Order_By.Desc}>Newest</option>
<option value={Order_By.Asc}>Oldest</option>
</Select>
</MetaSelect>
</WrapItem>
<WrapItem>
<Select
<MetaSelect
value={queryVariables.status as string}
onChange={(e) => setQueryVariable('status', e.target.value)}
>
<option value={QuestStatus_Enum.Open}>Open</option>
<option value={QuestStatus_Enum.Closed}>Closed</option>
</Select>
</MetaSelect>
</WrapItem>
<WrapItem>
<Select
<MetaSelect
value={(queryVariables.guild_id as string) || ''}
onChange={(e) => setQueryVariable('guild_id', e.target.value)}
>
@@ -84,7 +84,7 @@ export const QuestFilter: React.FC<Props> = ({
{g.name}
</option>
))}
</Select>
</MetaSelect>
</WrapItem>
{myId && (
@@ -94,6 +94,8 @@ export const QuestFilter: React.FC<Props> = ({
size="md"
colorScheme="cyan"
variant="outline"
borderWidth="2px"
borderRadius="4px"
px={4}
onClick={() =>
setQueryVariable(
@@ -107,13 +109,6 @@ export const QuestFilter: React.FC<Props> = ({
isChecked={
myId && queryVariables.created_by_player_id === myId
}
onClick={(e) => {
setQueryVariable(
'created_by_player_id',
queryVariables.created_by_player_id ? '' : myId,
);
e.preventDefault();
}}
/>
</MetaButton>
</Flex>
@@ -123,7 +118,9 @@ export const QuestFilter: React.FC<Props> = ({
</WrapItem>
{quests && (
<WrapItem>
<Text align="center">{quests.length} quests</Text>
<Text align="center" fontWeight="bold">
{quests.length} quests
</Text>
</WrapItem>
)}
</Wrap>

View File

@@ -23,18 +23,14 @@ const errorHasResponseTimeout = (err: CombinedError): boolean =>
err.graphQLErrors.length > 0 &&
!!err.graphQLErrors.find((_err) => _err.message === 'ResponseTimeout');
const retryExchangeFunc = retryExchange({
retryIf: (error) => !!(errorHasResponseTimeout(error) || error.networkError),
});
export const client = createClient({
url: CONFIG.graphqlURL,
suspense: false,
exchanges: [
dedupExchange,
cacheExchange,
retryExchange({
retryIf: (error) =>
!!(errorHasResponseTimeout(error) || error.networkError),
}),
fetchExchange,
],
exchanges: [dedupExchange, cacheExchange, retryExchangeFunc, fetchExchange],
});
export const getSsrClient = (): [Client, ReturnType<typeof ssrExchange>] => {
@@ -43,7 +39,13 @@ export const getSsrClient = (): [Client, ReturnType<typeof ssrExchange>] => {
const ssrClient = initUrqlClient(
{
url: CONFIG.graphqlURL,
exchanges: [dedupExchange, cacheExchange, ssrCache, fetchExchange],
exchanges: [
dedupExchange,
cacheExchange,
ssrCache,
retryExchangeFunc,
fetchExchange,
],
},
false,
);

View File

@@ -1,77 +1,92 @@
import gql from 'fake-tag';
import { Client } from 'urql';
import {
GetPlayersDocument,
GetPlayersQuery,
GetPlayersQueryVariables,
GetPlayerUsernamesQuery,
GetPlayerUsernamesQueryVariables,
PlayerFragmentFragment,
} from './autogen/types';
import { client } from './client';
import { client as defaultClient } from './client';
import { PlayerFragment } from './fragments';
const playersQuery = gql`
query GetPlayers($limit: Int, $offset: Int) {
player(order_by: { total_xp: desc }, limit: $limit, offset: $offset) {
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
gql`
query GetPlayers(
$offset: Int
$limit: Int
$skillCategory: SkillCategory_enum
$playerType: Int
$availability: Int
$timezone: String
$search: String
) {
player(
order_by: { total_xp: desc }
offset: $offset
limit: $limit
where: {
Player_Skills: { Skill: { category: { _eq: $skillCategory } } }
playerType: { id: { _eq: $playerType } }
availability_hours: { _gte: $availability }
timezone: { _eq: $timezone }
_or: [
{ username: { _ilike: $search } }
{ ethereum_address: { _ilike: $search } }
]
}
) {
...PlayerFragment
}
}
${PlayerFragment}
`;
export const getPlayers = async (
limit = 50,
offset = 0,
): Promise<PlayerFragmentFragment[]> => {
const { data, error } = await client
.query<GetPlayersQuery, GetPlayersQueryVariables>(playersQuery, {
limit,
offset,
})
.toPromise();
if (!data) {
if (error) {
throw error;
}
return [];
}
return data.player;
export const defaultQueryVariables: GetPlayersQueryVariables = {
offset: 0,
limit: 50,
skillCategory: undefined,
playerType: undefined,
availability: 0,
timezone: undefined,
search: '%%',
};
const LIMIT = 50;
const TOTAL_PLAYERS = 150;
export type PlayersResponse = {
error: Error | undefined;
players: PlayerFragmentFragment[];
};
export const getTopPlayers = async (): Promise<PlayerFragmentFragment[]> => {
const promises: Promise<PlayerFragmentFragment[]>[] = new Array(
TOTAL_PLAYERS / LIMIT,
)
.fill(false)
.map((_, i) => getPlayers(LIMIT, i * LIMIT));
const playersArr = await Promise.all(promises);
return playersArr.reduce((_total, _players) => [..._total, ..._players], []);
export const getPlayers = async (
queryVariables = defaultQueryVariables,
client: Client = defaultClient,
): Promise<PlayersResponse> => {
const { data, error } = await client
.query<GetPlayersQuery, GetPlayersQueryVariables>(
GetPlayersDocument,
queryVariables,
)
.toPromise();
return { players: data?.player || [], error };
};
const playerUsernamesQuery = gql`
query GetPlayerUsernames($limit: Int, $offset: Int) {
player(order_by: { total_xp: desc }, limit: $limit, offset: $offset) {
query GetPlayerUsernames($limit: Int) {
player(order_by: { total_xp: desc }, limit: $limit) {
username
}
}
`;
export const getPlayerUsernames = async (
limit = 50,
offset = 0,
): Promise<string[]> => {
const { data, error } = await client
export const getPlayerUsernames = async (limit = 150): Promise<string[]> => {
const { data, error } = await defaultClient
.query<GetPlayerUsernamesQuery, GetPlayerUsernamesQueryVariables>(
playerUsernamesQuery,
{
limit,
offset,
},
)
.toPromise();
@@ -87,10 +102,47 @@ export const getPlayerUsernames = async (
return data.player.map((p) => p.username);
};
export const getTopPlayerUsernames = async (): Promise<string[]> => {
const promises: Promise<string[]>[] = new Array(TOTAL_PLAYERS / LIMIT)
export const getTopPlayerUsernames = getPlayerUsernames;
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
gql`
query GetPlayerFilters {
skill_aggregate(distinct_on: category) {
nodes {
name: category
}
}
player_type(distinct_on: id) {
id
title
}
}
`;
export const getPlayersInParallel = async (
variables: GetPlayersQueryVariables,
): Promise<PlayersResponse> => {
const limit = 50;
const total = variables?.limit as number;
if (total <= limit) {
return getPlayers(variables);
}
const len = Math.ceil(total / limit);
const variablesArr: GetPlayersQueryVariables[] = new Array<boolean>(len)
.fill(false)
.map((_, i) => getPlayerUsernames(LIMIT, i * LIMIT));
const playersArr = await Promise.all(promises);
return playersArr.reduce((_total, _players) => [..._total, ..._players], []);
.map((_, i) => ({
...variables,
offset: i * limit,
limit: i < len - 1 ? limit : total - limit * (len - 1),
}));
const promises = variablesArr.map((vars) => getPlayers(vars));
const playersRespArr = await Promise.all(promises);
return playersRespArr.reduce(
(totalRes, response) => ({
error: totalRes.error || response.error,
players: [...totalRes.players, ...response.players],
}),
{ error: undefined, players: [] },
);
};

View File

@@ -43,14 +43,6 @@ gql`
) {
...QuestFragment
}
quest_aggregate(distinct_on: guild_id) {
nodes {
guild_id
guild {
name
}
}
}
}
${QuestFragment}

View File

@@ -0,0 +1,112 @@
import {
GetPlayersQueryVariables,
PlayerFragmentFragment,
useGetPlayerFiltersQuery,
useGetPlayersQuery,
} from 'graphql/autogen/types';
import {
defaultQueryVariables,
getPlayersInParallel,
PlayersResponse,
} from 'graphql/getPlayers';
import { useCallback, useEffect, useState } from 'react';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type QueryVariableSetter = (key: string, value: any) => void;
export interface PlayerAggregates {
skillCategories: { name: string }[];
playerTypes: { id: number; title: string }[];
}
interface PlayerFilter {
players: PlayerFragmentFragment[];
fetching: boolean;
aggregates: PlayerAggregates;
queryVariables: GetPlayersQueryVariables;
setQueryVariable: QueryVariableSetter;
error?: Error;
}
const usePlayerAggregates = () => {
const [{ data }] = useGetPlayerFiltersQuery();
return {
skillCategories: data?.skill_aggregate.nodes || [],
playerTypes: data?.player_type || [],
};
};
const usePlayersSingle = (
run: boolean,
variables: GetPlayersQueryVariables,
) => {
const [{ fetching, data, error }] = useGetPlayersQuery({
variables,
pause: !run,
});
const players = data?.player || [];
return { fetching, players, error };
};
const usePlayersParallel = (
run: boolean,
variables: GetPlayersQueryVariables,
) => {
const [fetching, setFetching] = useState(true);
const [{ players, error }, setResponse] = useState<PlayersResponse>({
error: undefined,
players: [],
});
useEffect(() => {
const load = async () => {
if (run) {
setFetching(true);
const response = await getPlayersInParallel(variables);
setResponse(response);
setFetching(false);
}
};
load();
}, [run, variables]);
return { fetching, players, error };
};
const useFilteredPlayers = (variables: GetPlayersQueryVariables) => {
const runParallel = (variables.limit as number) > 50; // if limit is 150 then hasura is unable to handle in one query
const playersParallel = usePlayersParallel(runParallel, variables);
const playersSingle = usePlayersSingle(!runParallel, variables);
return runParallel ? playersParallel : playersSingle;
};
export const usePlayerFilter = (): PlayerFilter => {
const [
queryVariables,
setQueryVariables,
] = useState<GetPlayersQueryVariables>(defaultQueryVariables);
const aggregates = usePlayerAggregates();
const setQueryVariable: QueryVariableSetter = useCallback(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(key: string, value: any) => {
setQueryVariables((oldQueryVariables) => ({
...oldQueryVariables,
[key]: value !== '' ? value : null,
}));
},
[],
);
const { fetching, players, error } = useFilteredPlayers(queryVariables);
return {
players,
aggregates,
fetching,
error,
queryVariables,
setQueryVariable,
};
};

View File

@@ -1,14 +1,17 @@
// eslint-disable-next-line
/* eslint-disable @typescript-eslint/no-var-requires */
const withTM = require('next-transpile-modules')(['react-timezone-select']);
const withImages = require('next-images');
module.exports = withImages({
async redirects() {
return [
{
source: '/',
destination: '/players',
permanent: false,
},
];
},
});
module.exports = withTM(
withImages({
async redirects() {
return [
{
source: '/',
destination: '/players',
permanent: false,
},
];
},
}),
);

View File

@@ -25,6 +25,7 @@
"moment": "2.29.1",
"next": "10.2.0",
"next-images": "1.7.0",
"next-transpile-modules": "7.0.0",
"next-urql": "3.1.0",
"node-fetch": "2.6.1",
"opensea-js": "1.1.11",
@@ -33,9 +34,9 @@
"react-hook-form": "6.15.5",
"react-icons": "4.2.0",
"react-qr-svg": "2.3.0",
"spacetime": "6.16.0",
"spacetime-informal": "0.6.1",
"urql": "2.0.2",
"web3modal": "1.9.3"
"web3modal": "1.9.3",
"spacetime": "6.16.1",
"spacetime-informal": "0.6.1"
}
}

View File

@@ -1,5 +1,5 @@
import { PageContainer } from 'components/Container';
import { GuildList } from 'components/GuildList';
import { GuildList } from 'components/Guild/GuildList';
import { getGuilds } from 'graphql/getGuilds';
import { InferGetStaticPropsType } from 'next';
import React from 'react';

View File

@@ -1,5 +1,5 @@
import { PageContainer } from 'components/Container';
import { PatronList } from 'components/PatronList';
import { PatronList } from 'components/Patron/PatronList';
import { getPatrons } from 'graphql/getPatrons';
import { InferGetStaticPropsType } from 'next';
import React from 'react';

View File

@@ -1,25 +1,52 @@
import { LoadingState, Text, VStack } from '@metafam/ds';
import { PageContainer } from 'components/Container';
import { PlayerList } from 'components/PlayerList';
import { getTopPlayers } from 'graphql/getPlayers';
import { PlayerFilter } from 'components/Player/PlayerFilter';
import { PlayerList } from 'components/Player/PlayerList';
import { getSsrClient } from 'graphql/client';
import { getPlayers } from 'graphql/getPlayers';
import { usePlayerFilter } from 'lib/hooks/players';
import { InferGetStaticPropsType } from 'next';
import React from 'react';
type Props = InferGetStaticPropsType<typeof getStaticProps>;
export const getStaticProps = async () => {
const players = await getTopPlayers();
const [ssrClient, ssrCache] = getSsrClient();
// This populate the cache server-side
await getPlayers(undefined, ssrClient);
return {
props: {
players,
urqlState: ssrCache.extractData(),
},
revalidate: 1,
};
};
const Players: React.FC<Props> = ({ players }) => (
<PageContainer>
<PlayerList players={players} />
</PageContainer>
);
const Players: React.FC<Props> = () => {
const {
players,
aggregates,
fetching,
error,
queryVariables,
setQueryVariable,
} = usePlayerFilter();
return (
<PageContainer>
<VStack w="100%" spacing="8">
<PlayerFilter
fetching={fetching}
aggregates={aggregates}
queryVariables={queryVariables}
setQueryVariable={setQueryVariable}
players={players || []}
/>
{error && <Text>{`Error: ${error.message}`}</Text>}
{fetching && <LoadingState />}
{players && !fetching && !error && <PlayerList players={players} />}
</VStack>
</PageContainer>
);
};
export default Players;

View File

@@ -10,15 +10,14 @@ import {
import { PageContainer } from 'components/Container';
import { QuestFilter } from 'components/Quest/QuestFilter';
import { QuestList } from 'components/Quest/QuestList';
import { getSsrClient } from 'graphql/client';
import { getQuests } from 'graphql/getQuests';
import { usePSeedBalance } from 'lib/hooks/balances';
import { useQuestFilter } from 'lib/hooks/quests';
import { InferGetStaticPropsType } from 'next';
import { useRouter } from 'next/router';
import React, { useMemo } from 'react';
import { getSsrClient } from '../graphql/client';
import { usePSeedBalance } from '../lib/hooks/balances';
import { useQuestFilter } from '../lib/hooks/quests';
import { isAllowedToCreateQuest } from '../utils/questHelpers';
import { isAllowedToCreateQuest } from 'utils/questHelpers';
type Props = InferGetStaticPropsType<typeof getStaticProps>;

View File

@@ -2026,7 +2026,7 @@
source-map "^0.5.7"
stylis "^4.0.3"
"@emotion/cache@^10.0.27", "@emotion/cache@^10.0.9":
"@emotion/cache@^10.0.27":
version "10.0.29"
resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-10.0.29.tgz#87e7e64f412c060102d589fe7c6dc042e6f9d1e0"
integrity sha512-fU2VtSVlHiF27empSbxi1O2JFdNWZO+2NFHfwO0pxgTep6Xa3uGb+3pVKfLww2l/IBGLNEZl5Xf/++A4wAYDYQ==
@@ -2047,7 +2047,7 @@
"@emotion/weak-memoize" "^0.2.5"
stylis "^4.0.3"
"@emotion/core@^10.0.9", "@emotion/core@^10.1.1":
"@emotion/core@^10.1.1":
version "10.1.1"
resolved "https://registry.yarnpkg.com/@emotion/core/-/core-10.1.1.tgz#c956c1365f2f2481960064bcb8c4732e5fb612c3"
integrity sha512-ZMLG6qpXR8x031NXD8HJqugy/AZSkAuMxxqB46pmAR7ze47MhNJ56cdoX243QPZdGctrdfo+s08yZTiwaUcRKA==
@@ -2059,7 +2059,7 @@
"@emotion/sheet" "0.9.4"
"@emotion/utils" "0.11.3"
"@emotion/css@^10.0.27", "@emotion/css@^10.0.9":
"@emotion/css@^10.0.27":
version "10.0.27"
resolved "https://registry.yarnpkg.com/@emotion/css/-/css-10.0.27.tgz#3a7458198fbbebb53b01b2b87f64e5e21241e14c"
integrity sha512-6wZjsvYeBhyZQYNrGoR5yPMYbMBNEnanDrqmsqS1mzDm1cOTu12shvl2j4QHNS36UaTE0USIJawCH9C8oW34Zw==
@@ -12009,6 +12009,14 @@ enhanced-resolve@^4.0.0, enhanced-resolve@^4.5.0:
memory-fs "^0.5.0"
tapable "^1.0.0"
enhanced-resolve@^5.7.0:
version "5.8.2"
resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.8.2.tgz#15ddc779345cbb73e97c611cd00c01c1e7bf4d8b"
integrity sha512-F27oB3WuHDzvR2DOGNTaYy0D5o0cnrv8TeI482VM4kYgQd/FT9lUQwuNsJ0oOHtBUq7eiW5ytqzp7nBFknL+GA==
dependencies:
graceful-fs "^4.2.4"
tapable "^2.2.0"
enquirer@^2.3.4, enquirer@^2.3.5, enquirer@^2.3.6:
version "2.3.6"
resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d"
@@ -13804,10 +13812,10 @@ fragment-cache@^0.2.1:
dependencies:
map-cache "^0.2.2"
framer-motion@4.1.16:
version "4.1.16"
resolved "https://registry.yarnpkg.com/framer-motion/-/framer-motion-4.1.16.tgz#dc715334847d0a146acf47f61019222d0d1c46c9"
integrity sha512-sEc3UI3oncwE+RUzdd86TxbmpEaX/Ki/T0AmFYSsbxEqGZ3feLvzGL7BJlkhERIyyuAC9+OzI4BnhJM0GSUAMA==
framer-motion@4.1.17:
version "4.1.17"
resolved "https://registry.yarnpkg.com/framer-motion/-/framer-motion-4.1.17.tgz#4029469252a62ea599902e5a92b537120cc89721"
integrity sha512-thx1wvKzblzbs0XaK2X0G1JuwIdARcoNOW7VVwjO8BUltzXPyONGAElLu6CiCScsOQRI7FIk/45YTFtJw5Yozw==
dependencies:
framesync "5.3.0"
hey-listen "^1.0.8"
@@ -20658,6 +20666,14 @@ next-tick@~1.0.0:
resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c"
integrity sha1-yobR/ogoFpsBICCOPchCS524NCw=
next-transpile-modules@7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/next-transpile-modules/-/next-transpile-modules-7.0.0.tgz#5a48988cede89bc5920defb15378093d0d8562eb"
integrity sha512-HgVczU5ajXKvE7HO3ZLmBmxXj79aq8jSZNYpCttim+MZ+b0GIsdk7AV2w7Ax/tIM1/dJA+vV/6loXCRYlbsGGA==
dependencies:
enhanced-resolve "^5.7.0"
escalade "^3.1.1"
next-urql@3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/next-urql/-/next-urql-3.1.0.tgz#9c354cfd54cdf886988a2cb1ea0fd89f48ee7555"
@@ -23613,7 +23629,7 @@ react-router@5.2.0, react-router@^5.1.0, react-router@^5.2.0:
tiny-invariant "^1.0.2"
tiny-warning "^1.0.0"
react-select@4.3.1:
react-select@4.3.1, react-select@^4.2.1:
version "4.3.1"
resolved "https://registry.yarnpkg.com/react-select/-/react-select-4.3.1.tgz#389fc07c9bc7cf7d3c377b7a05ea18cd7399cb81"
integrity sha512-HBBd0dYwkF5aZk1zP81Wx5UsLIIT2lSvAY2JiJo199LjoLHoivjn9//KsmvQMEFGNhe58xyuOITjfxKCcGc62Q==
@@ -23626,20 +23642,6 @@ react-select@4.3.1:
react-input-autosize "^3.0.0"
react-transition-group "^4.3.0"
react-select@^3.1.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/react-select/-/react-select-3.2.0.tgz#de9284700196f5f9b5277c5d850a9ce85f5c72fe"
integrity sha512-B/q3TnCZXEKItO0fFN/I0tWOX3WJvi/X2wtdffmwSQVRwg5BpValScTO1vdic9AxlUgmeSzib2hAZAwIUQUZGQ==
dependencies:
"@babel/runtime" "^7.4.4"
"@emotion/cache" "^10.0.9"
"@emotion/core" "^10.0.9"
"@emotion/css" "^10.0.9"
memoize-one "^5.0.0"
prop-types "^15.6.0"
react-input-autosize "^3.0.0"
react-transition-group "^4.3.0"
react-sizeme@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/react-sizeme/-/react-sizeme-3.0.1.tgz#4d12f4244e0e6a0fb97253e7af0314dc7c83a5a0"
@@ -23684,14 +23686,14 @@ react-textarea-autosize@^8.3.0:
use-composed-ref "^1.0.0"
use-latest "^1.0.0"
react-timezone-select@0.9.8:
version "0.9.8"
resolved "https://registry.yarnpkg.com/react-timezone-select/-/react-timezone-select-0.9.8.tgz#a5d9ead1fa0b40dab1b3a77d9cdcc20354fe2ec4"
integrity sha512-CzIOs9IwAb5hmjs5jnB1uShQvw9zJGUVeH5dIpRVy1dgVnwkasvXc2fGwnggI6nYXAmN7uOCt5kFIcQOpZmUNw==
react-timezone-select@1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/react-timezone-select/-/react-timezone-select-1.0.3.tgz#e257f7204c8ee74c30ab033f34a1d2d3a45963b9"
integrity sha512-wpsu2jhEkn+/G91m24oqSlT/iAmcE4pQot9tOTUcV9kI+uA1kc+0YpvJFL5aT2jMVJ/uwpQkxUWAhRwa8U9YKg==
dependencies:
react-select "^3.1.0"
spacetime "^6.6.2"
spacetime-informal "^0.3.0"
react-select "^4.2.1"
spacetime "^6.14.0"
spacetime-informal "^0.6.1"
react-transition-group@^4.3.0, react-transition-group@^4.4.0, react-transition-group@^4.4.1:
version "4.4.1"
@@ -25409,22 +25411,17 @@ space-separated-tokens@^1.0.0:
resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz#85f32c3d10d9682007e917414ddc5c26d1aa6899"
integrity sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==
spacetime-informal@0.6.1:
spacetime-informal@0.6.1, spacetime-informal@^0.6.1:
version "0.6.1"
resolved "https://registry.yarnpkg.com/spacetime-informal/-/spacetime-informal-0.6.1.tgz#621b4156a98f20f5c3f0151475d2ea886e1c19c1"
integrity sha512-fOA+RMi2mpCbkLkjBIzWQttmkYE/v9VfdbbrvE2Pus/WoiQNmHO20YGNEqJOuFADsrrZOd/hJVuBwAkV3s6BHg==
dependencies:
efrt-unpack "2.2.0"
spacetime-informal@^0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/spacetime-informal/-/spacetime-informal-0.3.0.tgz#6d0feffe291697f686b737b678b59ee2dcef3297"
integrity sha512-HtFTwtkzDl7gswCfeWPQAxvQ+Fmrdt7WqoDT0Lq2FX7RVZKZ/+zmgBOAVAuH8oVcf4uXza9NvdX9QppGRiG8oQ==
spacetime@6.16.0, spacetime@^6.6.2:
version "6.16.0"
resolved "https://registry.yarnpkg.com/spacetime/-/spacetime-6.16.0.tgz#f213963392eafc380716c3857b23251de87db97f"
integrity sha512-mkuniNOp6ssfPyJidj81tb54zKxK4vEKPTmcUsC/NEGIF8S07ppoSotdg6numT1/26rthQYmdxMY/M5a9WeJVQ==
spacetime@6.16.1, spacetime@^6.14.0:
version "6.16.1"
resolved "https://registry.yarnpkg.com/spacetime/-/spacetime-6.16.1.tgz#e032dc77bea494367a7a321a03458ebbf8f625b0"
integrity sha512-+aK5oTrCCfXZsg73NEoy3V2cLa0xY3obddFNbWSJ8UcbuPb7f8zWOV4vH3hEAncznUJQqA7tGHAuVR4TysfgIA==
sparse-array@^1.3.1:
version "1.3.2"
@@ -26171,6 +26168,11 @@ tapable@^1.0.0, tapable@^1.1.3:
resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2"
integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==
tapable@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.0.tgz#5c373d281d9c672848213d0e037d1c4165ab426b"
integrity sha512-FBk4IesMV1rBxX2tfiK8RAmogtWn53puLOQlvO8XuwlgxcYbP4mVPS9Ph4aeamSyyVjOl24aYWAuc8U5kCVwMw==
tape@^4.4.0, tape@^4.6.3:
version "4.13.3"
resolved "https://registry.yarnpkg.com/tape/-/tape-4.13.3.tgz#51b3d91c83668c7a45b1a594b607dee0a0b46278"