mirror of
https://github.com/MetaFam/TheGame.git
synced 2026-02-10 22:14:53 -05:00
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:
7
packages/design-system/babel.config.js
Normal file
7
packages/design-system/babel.config.js
Normal file
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
env: {
|
||||
test: {
|
||||
presets: ['@babel/preset-env', '@babel/preset-react'],
|
||||
},
|
||||
},
|
||||
};
|
||||
3
packages/design-system/jest.config.js
Normal file
3
packages/design-system/jest.config.js
Normal file
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
transformIgnorePatterns: ['SelectTimezone'],
|
||||
};
|
||||
@@ -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": {
|
||||
|
||||
@@ -14,6 +14,7 @@ export const MetaButton: React.FC<
|
||||
letterSpacing="0.1em"
|
||||
size="lg"
|
||||
fontSize="sm"
|
||||
bg="purple.400"
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
|
||||
19
packages/design-system/src/MetaSelect.tsx
Normal file
19
packages/design-system/src/MetaSelect.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
@@ -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,
|
||||
|
||||
13
packages/design-system/src/icons/DropDownIcon.tsx
Normal file
13
packages/design-system/src/icons/DropDownIcon.tsx
Normal 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',
|
||||
});
|
||||
@@ -1,2 +1,3 @@
|
||||
export { BrightIdIcon } from './BrightIdIcon';
|
||||
export { DropDownIcon } from './DropDownIcon';
|
||||
export { Icon3box } from './Icon3box';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -14,6 +14,10 @@ export const theme: Theme = extendTheme({
|
||||
background: 'dark',
|
||||
color: 'white',
|
||||
minHeight: '100vh',
|
||||
option: {
|
||||
background: 'dark',
|
||||
color: 'white',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"noEmit": true,
|
||||
|
||||
222
packages/web/components/Player/PlayerFilter.tsx
Normal file
222
packages/web/components/Player/PlayerFilter.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
23
packages/web/components/Player/PlayerList.tsx
Normal file
23
packages/web/components/Player/PlayerList.tsx
Normal 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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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: [] },
|
||||
);
|
||||
};
|
||||
|
||||
@@ -43,14 +43,6 @@ gql`
|
||||
) {
|
||||
...QuestFragment
|
||||
}
|
||||
quest_aggregate(distinct_on: guild_id) {
|
||||
nodes {
|
||||
guild_id
|
||||
guild {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
${QuestFragment}
|
||||
|
||||
112
packages/web/lib/hooks/players.ts
Normal file
112
packages/web/lib/hooks/players.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
@@ -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,
|
||||
},
|
||||
];
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user