updated filters with new design

This commit is contained in:
dan13ram
2021-05-26 18:51:03 +05:30
committed by dan13ram
parent 3cfaa8f083
commit 591b7d1e9f
13 changed files with 841 additions and 305 deletions

View File

@@ -0,0 +1,359 @@
import { CheckIcon, CloseIcon } from '@chakra-ui/icons';
import {
Button,
Flex,
FlexProps,
IconButton,
Input,
Select,
SelectProps,
Text,
} from '@chakra-ui/react';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { DropDownIcon } from './icons/DropDownIcon';
import { MetaTag } from './MetaTag';
import { SelectComponents, SelectSearch } from './SelectSearch';
export const MetaSelect: React.FC<SelectProps> = (props) => (
<Select
textTransform="uppercase"
maxW="48"
bg="dark"
iconColor="purple.400"
iconSize="xs"
icon={<DropDownIcon boxSize={2} />}
borderColor="borderPurple"
borderWidth="2px"
borderRadius="4px"
{...props}
/>
);
type FilterTagProps = {
label: string;
onRemove: () => void;
};
export const FilterTag: React.FC<FilterTagProps> = ({ label, onRemove }) => (
<MetaTag
backgroundColor="black"
size="lg"
fontSize="normal"
borderRadius="1rem"
py="1"
px="4"
fontWeight="normal"
>
{label}
<IconButton
ml="3"
minW="4"
variant="unstyled"
size="xs"
color="silver"
icon={<CloseIcon />}
_hover={{ color: 'white' }}
aria-label={`Remove filter ${label}`}
onClick={onRemove}
/>
</MetaTag>
);
const SelectedTag: React.FC<FlexProps> = (props) => (
<Flex
color="black"
bg="#E839B7"
borderRadius="2px"
justify="center"
align="center"
px="1"
ml="2"
fontSize="sm"
fontWeight="bold"
{...props}
/>
);
const SelectOption: React.FC<
React.ComponentProps<typeof SelectComponents.Option>
> = (props) => {
const {
isSelected,
data: { value: optionValue },
selectProps: { onChange, value: selectValue },
} = props;
const clearValue = useCallback(() => {
if (onChange) {
const newSelectValue = selectValue
? selectValue.filter(
({ value }: { value: string }) => !(value === optionValue),
)
: [];
onChange(newSelectValue, {
action: 'remove-value',
removedValue: { optionValue },
});
}
}, [optionValue, selectValue, onChange]);
return (
<Flex
fontWeight="normal"
w="100%"
justify="space-between"
cursor="pointer"
align="center"
borderBottomWidth="1px"
borderBottomStyle="solid"
borderBottomColor="borderPurple"
_hover={{ backgroundColor: 'whiteAlpha.100' }}
onClick={isSelected ? clearValue : undefined}
css={{ div: { cursor: 'pointer' } }}
>
<SelectComponents.Option {...props} />
{isSelected && <CheckIcon color="white" mx="2" />}
</Flex>
);
};
const ValueDisplay: React.FC<{
menuIsOpen: boolean | undefined;
title: string;
tagLabel: string;
}> = ({ menuIsOpen, tagLabel, title }) => (
<>
<Text ml="2" textTransform="uppercase">
{title}
</Text>
{tagLabel ? <SelectedTag>{tagLabel}</SelectedTag> : null}
<DropDownIcon
boxSize={3}
color="purple.400"
mx="2"
transition="transform 0.1s"
transform={menuIsOpen ? 'rotate(180deg) translateY(10%)' : 'none'}
/>
</>
);
const SelectValueContainer: React.FC<
React.ComponentProps<typeof SelectComponents.ValueContainer>
> = (props) => {
const {
selectProps: { value, title, menuIsOpen },
} = props;
let tagLabel = '';
if (Array.isArray(value) && value.length > 0) {
tagLabel = value.length.toString();
}
if (value && !Array.isArray(value)) {
tagLabel =
title.toLowerCase() === 'availability' ? `>${value.value}` : value.value;
}
return (
<Flex mr="-1rem" py="1" align="center" cursor="pointer">
<ValueDisplay title={title} menuIsOpen={menuIsOpen} tagLabel={tagLabel} />
<SelectComponents.ValueContainer {...props} />
</Flex>
);
};
const SelectControl: React.FC<
React.ComponentProps<typeof SelectComponents.Control>
> = (props) => {
const {
hasValue,
selectProps: { menuIsOpen, onMenuClose, onMenuOpen },
} = props;
const buttonRef = useRef<HTMLButtonElement>(null);
return (
<Button
fontWeight="normal"
variant="unstyled"
boxShadow={menuIsOpen ? '0px 10px 20px rgba(0, 0, 0, 0.4)' : 'none'}
cursor="pointer"
ref={buttonRef}
onClick={menuIsOpen ? onMenuClose : onMenuOpen}
onMouseDown={() => (menuIsOpen ? undefined : buttonRef.current?.focus())}
align="center"
borderTopRadius="4px"
borderBottomRadius={menuIsOpen ? '0' : '4px'}
borderColor="borderPurple"
borderStyle="solid"
borderWidth={hasValue && !menuIsOpen ? '4px' : '2px'}
borderBottom={menuIsOpen ? '0' : undefined}
height="auto"
bg="dark"
_hover={{
borderColor: menuIsOpen ? 'borderPurple' : 'whiteAlpha.800',
}}
transform={menuIsOpen ? 'translateY(-1px)' : undefined}
transition="transform 0s"
>
<SelectComponents.Control {...props} />
</Button>
);
};
const SelectMenu: React.FC<
React.ComponentProps<typeof SelectComponents.Menu>
> = (props) => {
const {
selectProps: { onInputChange, title, value, placement, showSearch },
} = props;
const [input, setInput] = useState('');
let tagLabel = '';
if (Array.isArray(value) && value.length > 0) {
tagLabel = value.length.toString();
}
if (value && !Array.isArray(value)) {
tagLabel =
title.toLowerCase() === 'availability' ? `>${value.value}` : value.value;
}
const placeRight = placement === 'right';
return (
<Flex
position="absolute"
top="calc(100% - 1px)"
minWidth="15rem"
left={placeRight ? 'auto' : '0'}
right={placeRight ? '0' : 'auto'}
zIndex="1"
direction="column"
>
<Flex w="100%" direction={placeRight ? 'row-reverse' : 'row'}>
<Flex
height="3"
p="0"
bg="dark"
borderLeftColor="borderPurple"
borderLeftStyle="solid"
borderLeftWidth="2px"
borderRightColor="borderPurple"
borderRightStyle="solid"
borderRightWidth="2px"
overflow="hidden"
whiteSpace="nowrap"
boxShadow="0px 10px 20px rgba(0, 0, 0, 0.4)"
>
<Flex visibility="hidden">
<ValueDisplay title={title} tagLabel={tagLabel} menuIsOpen />
</Flex>
</Flex>
<Flex
borderBottomColor="borderPurple"
borderBottomStyle="solid"
borderBottomWidth="2px"
flex={1}
pointerEvents="none"
/>
</Flex>
<Flex
w="100%"
boxShadow="0px 10px 20px rgba(0, 0, 0, 0.4)"
bg="dark"
borderWidth="2px"
borderColor="borderPurple"
borderStyle="solid"
borderBottomWidth={showSearch ? '2px' : '1px'}
borderTop="none"
borderBottomRadius="4px"
direction="column"
>
{showSearch && (
<Flex
w="100%"
borderBottomWidth="1px"
borderBottomColor="borderPurple"
borderBottomStyle="solid"
>
<Input
autoFocus
width="calc(100% - 2rem)"
placeholder="Search..."
_placeholder={{ color: 'whiteAlpha.500' }}
borderRadius="0"
borderWidth="2px"
mx="4"
my="2"
borderColor="borderPurple"
onChange={(e) => {
const inputValue = e.target.value;
setInput(inputValue);
if (onInputChange) {
onInputChange(inputValue, { action: 'input-change' });
}
}}
value={input}
/>
</Flex>
)}
<SelectComponents.Menu {...props} />
</Flex>
</Flex>
);
};
const SelectContainer: React.FC<
React.ComponentProps<typeof SelectComponents.SelectContainer>
> = (props) => {
const {
selectProps: { menuIsOpen, onMenuClose },
} = props;
const selectRef = useRef<HTMLDivElement>(null);
const onOutsideFocus = useCallback(() => {
if (onMenuClose && menuIsOpen) {
onMenuClose();
}
}, [menuIsOpen, onMenuClose]);
useEffect(() => {
const selectedRef = selectRef.current;
selectedRef?.addEventListener('focusout', onOutsideFocus);
return () => {
selectedRef?.removeEventListener('focusout', onOutsideFocus);
};
}, [selectRef, onOutsideFocus]);
return (
<Flex ref={selectRef} position="relative">
<SelectComponents.SelectContainer
{...props}
innerProps={{ onKeyDown: () => undefined }}
/>
</Flex>
);
};
export const MetaFilterSelectSearch: React.FC<
React.ComponentProps<typeof SelectSearch> & { showSearch?: boolean }
> = ({ showSearch = false, ...props }) => (
<SelectSearch
isMulti
closeMenuOnSelect={false}
placeholder=" "
components={{
MultiValueContainer: () => null,
SingleValue: () => null,
IndicatorSeparator: () => null,
DropdownIndicator: () => null,
IndicatorsContainer: () => null,
Input: () => null,
ValueContainer: SelectValueContainer,
Option: SelectOption,
Menu: SelectMenu,
Control: SelectControl,
SelectContainer,
}}
isClearable={false}
hideSelectedOptions={false}
showSearch={showSearch}
{...props}
/>
);

View File

@@ -1,19 +0,0 @@
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,16 +1,24 @@
import React from 'react';
import Select, { Props as SelectProps, Styles } from 'react-select';
import Select, { components, Props as SelectProps, Styles } from 'react-select';
import { theme } from './theme';
export const SelectComponents = components;
export const selectStyles: Styles = {
menuPortal: (styles) => ({
...styles,
borderRadius: theme.radii.md,
}),
menu: (styles) => ({
...styles,
background: theme.colors.dark,
minWidth: '15rem',
border: `2px solid ${theme.colors.borderPurple}`,
}),
menuList: (styles) => ({
...styles,
paddingTop: 0,
padding: 0,
}),
noOptionsMessage: (styles) => ({
...styles,
@@ -33,20 +41,23 @@ export const selectStyles: Styles = {
paddingBottom: theme.space['3'],
position: 'sticky',
top: 0,
borderRadius: theme.radii.md,
}),
option: (styles) => ({
...styles,
background: theme.colors.dark,
backgroundColor: 'transparent',
':hover': {
backgroundColor: theme.colors.purpleTag,
backgroundColor: theme.colors.whiteAlpha[100],
color: theme.colors.white,
},
}),
control: (styles) => ({
...styles,
minWidth: '6rem',
background: theme.colors.dark,
border: theme.colors.dark,
border: `2px solid ${theme.colors.borderPurple}`,
':hover': {
borderColor: theme.colors.white,
},
}),
multiValue: (styles) => ({
...styles,

View File

@@ -4,12 +4,16 @@ export * from './icons';
export { LoadingState } from './LoadingState';
export { MetaBox } from './MetaBox';
export { MetaButton } from './MetaButton';
export {
FilterTag,
MetaFilterSelectSearch,
MetaSelect,
} from './MetaFilterSelect';
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 { SelectComponents, SelectSearch, selectStyles } from './SelectSearch';
export {
SelectTimeZone,
TimezoneOptions,
@@ -52,6 +56,7 @@ export {
InputGroup,
InputLeftElement,
InputRightAddon,
InputRightElement,
Link,
List,
ListIcon,
@@ -70,6 +75,7 @@ export {
Text,
Textarea,
Tooltip,
useBreakpointValue,
useDisclosure,
useTheme,
useToast,
@@ -77,4 +83,5 @@ export {
Wrap,
WrapItem,
} from '@chakra-ui/react';
export { default as styled } from '@emotion/styled';
export { motion } from 'framer-motion';

View File

@@ -10,6 +10,7 @@ export type MetaColors = ChakraTheme['colors'] & {
purpleBoxLight: string;
purpleTag: string;
purpleTag30: string;
purpleTag70: string;
blueLight: string;
cyanText: string;
diamond: string;
@@ -21,6 +22,7 @@ export type MetaColors = ChakraTheme['colors'] & {
bronze: string;
purple80: string;
brightIdOrange: ColorHues;
borderPurple: string;
};
export const colors: MetaColors = {
@@ -38,10 +40,12 @@ export const colors: MetaColors = {
purpleBoxLight: '#392373',
purpleTag: '#40347C',
purpleTag30: 'rgba(64, 52, 124, 0.3)',
purpleTag70: 'rgba(64, 52, 124, 0.7)',
blueLight: '#A5B9F6',
cyanText: '#79F8FB',
discord: '#7289da',
discordDark: '#5d6eb3',
borderPurple: '#5946BC',
cyan: {
50: '#dbffff',
100: '#b1fcfe',

View File

@@ -1,21 +1,91 @@
import {
Button,
CloseIcon,
FilterTag,
Flex,
IconButton,
Input,
InputGroup,
InputRightElement,
MetaButton,
MetaSelect,
MetaFilterSelectSearch,
MetaTheme,
selectStyles,
Stack,
styled,
Text,
TimezoneOptions,
TimezoneType,
VStack,
useBreakpointValue,
Wrap,
WrapItem,
} from '@metafam/ds';
import {
GetPlayersQueryVariables,
PlayerFragmentFragment,
SkillCategory_Enum,
} from 'graphql/autogen/types';
import { PlayerAggregates, QueryVariableSetter } from 'lib/hooks/players';
import React, { useState } from 'react';
import { SkillColors } from 'graphql/types';
import {
PlayerAggregates,
QueryVariableSetter,
useFiltersUsed,
} from 'lib/hooks/players';
import React, { useEffect, useRef, useState } from 'react';
import { SkillOption } from 'utils/skillHelpers';
const Form = styled.form({
width: '100%',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
});
type ValueType = { value: string; label: string };
const styles: typeof selectStyles = {
...selectStyles,
multiValue: (s, { data }) => ({
...s,
background: SkillColors[data.category as SkillCategory_Enum],
color: MetaTheme.colors.white,
}),
multiValueLabel: (s, { data }) => ({
...s,
background: SkillColors[data.category as SkillCategory_Enum],
color: MetaTheme.colors.white,
}),
groupHeading: (s, { children }) => ({
...s,
...(selectStyles.groupHeading &&
selectStyles.groupHeading(s, { children })),
background: SkillColors[children as SkillCategory_Enum],
borderTop: `1px solid ${MetaTheme.colors.borderPurple}`,
margin: 0,
}),
option: (s, { isSelected }) => ({
...s,
backgroundColor: 'transparent',
fontWeight: isSelected ? 'bold' : 'normal',
':hover': {
backgroundColor: 'transparent',
color: MetaTheme.colors.white,
},
':focus': {
boxShadow: '0 0 0 3px rgba(66, 153, 225, 0.6)',
},
}),
menu: () => ({}),
control: (s) => ({
...s,
background: MetaTheme.colors.dark,
border: 'none',
':hover': {},
}),
noOptionsMessage: (s) => ({
...s,
borderTop: `1px solid ${MetaTheme.colors.borderPurple}`,
}),
};
type Props = {
fetching: boolean;
@@ -23,6 +93,7 @@ type Props = {
aggregates: PlayerAggregates;
queryVariables: GetPlayersQueryVariables;
setQueryVariable: QueryVariableSetter;
resetFilter: () => void;
};
export const PlayerFilter: React.FC<Props> = ({
@@ -31,8 +102,15 @@ export const PlayerFilter: React.FC<Props> = ({
aggregates,
queryVariables,
setQueryVariable,
resetFilter,
}) => {
const [search, setSearch] = useState<string>('');
const [skills, setSkills] = useState<SkillOption[]>([]);
const [playerTypes, setPlayerTypes] = useState<ValueType[]>([]);
const [timezones, setTimezones] = useState<ValueType[]>([]);
const [availability, setAvailability] = useState<ValueType | null>(null);
const onSearch = (e: React.ChangeEvent<HTMLFormElement>) => {
e.preventDefault();
if (search.length >= 2) {
@@ -41,9 +119,60 @@ export const PlayerFilter: React.FC<Props> = ({
setQueryVariable('search', `%%`);
}
};
const { filtersUsed } = useFiltersUsed(queryVariables);
const [isElementSticky, setIsSticky] = useState<boolean>(false);
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
const cachedRef = ref.current as Element;
const observer = new IntersectionObserver(
([e]) => setIsSticky(e.intersectionRatio < 1),
{ threshold: [1] },
);
observer.observe(cachedRef);
return () => observer.unobserve(cachedRef);
}, []);
const isSmallScreen = useBreakpointValue({ base: true, md: false });
const isSticky = !isSmallScreen && isElementSticky;
useEffect(() => {
setQueryVariable(
'playerTypeIds',
playerTypes.length > 0
? playerTypes.map((pT) => Number.parseInt(pT.value, 10))
: null,
);
}, [setQueryVariable, playerTypes]);
useEffect(() => {
setQueryVariable(
'skillIds',
skills.length > 0 ? skills.map((s) => s.id) : null,
);
}, [setQueryVariable, skills]);
useEffect(() => {
setQueryVariable(
'timezones',
timezones.length > 0 ? timezones.map((t) => t.value) : null,
);
}, [setQueryVariable, timezones]);
useEffect(() => {
setQueryVariable(
'availability',
availability ? parseInt(availability.value, 10) : 0,
);
}, [setQueryVariable, availability]);
return (
<>
<form onSubmit={onSearch}>
<Form onSubmit={onSearch}>
<Stack
spacing="4"
w="100%"
@@ -51,172 +180,195 @@ export const PlayerFilter: React.FC<Props> = ({
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">
<InputGroup size="lg">
<Input
background="dark"
w="100%"
type="text"
minW={{ base: '18rem', 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="borderPurple"
fontSize="md"
borderWidth="2px"
/>
{search.length > 0 && (
<InputRightElement>
<IconButton
variant="link"
colorScheme="cyan"
icon={<CloseIcon />}
onClick={() => {
setSearch('');
setQueryVariable('search', `%%`);
}}
aria-label="Clear Search"
/>
</InputRightElement>
)}
</InputGroup>
<MetaButton type="submit" size="lg" isDisabled={fetching} px="16">
SEARCH
</MetaButton>
</Stack>
</form>
</Form>
<Wrap
justify="space-between"
w="100%"
bg="whiteAlpha.200"
spacing="4"
justify={{ base: 'flex-start', md: 'center' }}
w={isSticky ? 'calc(100% + 6rem)' : '100%'}
maxW={isSticky ? 'auto' : '79rem'}
transition="all 0.25s"
bg={isElementSticky ? 'purpleTag70' : 'whiteAlpha.200'}
py="6"
px={isSticky ? '4.5rem' : '1.5rem'}
style={{ backdropFilter: 'blur(7px)' }}
p="6"
borderRadius="6px"
maxW="79rem"
borderRadius={isSticky ? '0px' : '6px'}
ref={ref}
position={isSmallScreen ? 'relative' : 'sticky'}
top="-1px"
borderTop="1px solid transparent"
zIndex="1"
align="center"
>
<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>
<MetaFilterSelectSearch
title="Type Of Player"
styles={styles}
value={playerTypes}
onChange={(value) => {
setPlayerTypes(value as ValueType[]);
}}
options={aggregates.playerTypes.map(({ id, title }) => ({
value: id.toString(),
label: title,
}))}
/>
</WrapItem>
<WrapItem>
<MetaFilterSelectSearch
title="Skills"
styles={styles}
value={skills}
onChange={(value) => {
setSkills(value as SkillOption[]);
}}
options={aggregates.skillChoices}
showSearch
/>
</WrapItem>
<WrapItem>
<MetaFilterSelectSearch
title="Availability"
styles={styles}
value={availability}
onChange={(value) => {
const values = value as ValueType[];
setAvailability(values[values.length - 1]);
}}
options={[1, 5, 10, 20, 30, 40].map((value) => ({
value: value.toString(),
label: `> ${value.toString()} h/week`,
}))}
/>
</WrapItem>
<WrapItem>
<MetaFilterSelectSearch
title="Time Zone"
styles={styles}
value={timezones}
onChange={(value) => {
setTimezones(value as ValueType[]);
}}
options={TimezoneOptions.map(({ id, label }) => ({
value: id.toString(),
label,
}))}
showSearch
/>
</WrapItem>
{players && !fetching && (
<WrapItem>
<Text align="center" fontWeight="bold">
{players.length} players
</Text>
</WrapItem>
)}
</Wrap>
{filtersUsed && (
<Flex w="100%" maxW="79rem" justify="space-between">
<Wrap flex="1">
<WrapItem>
<Flex w="100%" h="100%" justify="center" align="center">
<Text> {`Selected Filters: `}</Text>
</Flex>
</WrapItem>
{playerTypes.map(({ value, label }, index) => (
<WrapItem key={value}>
<FilterTag
label={label}
onRemove={() => {
const newPlayerTypes = playerTypes.slice();
newPlayerTypes.splice(index, 1);
setPlayerTypes(newPlayerTypes);
}}
/>
</WrapItem>
))}
{skills.map(({ value, label }, index) => (
<WrapItem key={value}>
<FilterTag
label={label}
onRemove={() => {
const newSkills = skills.slice();
newSkills.splice(index, 1);
setSkills(newSkills);
}}
/>
</WrapItem>
))}
{timezones.map(({ value, label }, index) => (
<WrapItem key={value}>
<FilterTag
label={label}
onRemove={() => {
const newTimezones = timezones.slice();
newTimezones.splice(index, 1);
setTimezones(newTimezones);
}}
/>
</WrapItem>
))}
{availability && (
<WrapItem>
<FilterTag
label={`Available >${availability.value} h/week`}
onRemove={() => {
setAvailability(null);
}}
/>
</WrapItem>
)}
</Wrap>
<Button
variant="link"
color="cyan.400"
onClick={() => {
resetFilter();
setSkills([]);
setPlayerTypes([]);
setTimezones([]);
setAvailability(null);
}}
minH="2.5rem"
>
RESET ALL FILTERS
</Button>
</Flex>
)}
<Flex justify="space-between" w="100%" maxW="80rem" px="4">
<Text fontWeight="bold" fontSize="xl" w="100%" maxW="79rem">
{players && !fetching
? `${players.length} player${players.length > 1 ? 's' : ''}`
: ''}
</Text>
</Flex>
</>
);
};

View File

@@ -23,6 +23,26 @@ export type SetupSkillsProps = {
setSkills: React.Dispatch<React.SetStateAction<Array<SkillOption>>>;
};
const styles: typeof selectStyles = {
...selectStyles,
multiValue: (s, { data }) => ({
...s,
background: SkillColors[data.category as SkillCategory_Enum],
color: MetaTheme.colors.white,
}),
multiValueLabel: (s, { data }) => ({
...s,
background: SkillColors[data.category as SkillCategory_Enum],
color: MetaTheme.colors.white,
}),
groupHeading: (s, { children }) => ({
...s,
...(selectStyles.groupHeading &&
selectStyles.groupHeading(s, { children })),
background: SkillColors[children as SkillCategory_Enum],
}),
};
export const SetupSkills: React.FC<SetupSkillsProps> = ({
skillChoices,
skills,
@@ -58,26 +78,6 @@ export const SetupSkills: React.FC<SetupSkillsProps> = ({
onNextPress();
};
const styles: typeof selectStyles = {
...selectStyles,
multiValue: (s, { data }) => ({
...s,
background: SkillColors[data.category as SkillCategory_Enum],
color: MetaTheme.colors.white,
}),
multiValueLabel: (s, { data }) => ({
...s,
background: SkillColors[data.category as SkillCategory_Enum],
color: MetaTheme.colors.white,
}),
groupHeading: (s, { children }) => ({
...s,
...(selectStyles.groupHeading &&
selectStyles.groupHeading(s, { children })),
background: SkillColors[children as SkillCategory_Enum],
}),
};
return (
<FlexContainer>
<MetaHeading mb={10} mt={-64} textAlign="center">

View File

@@ -160,3 +160,11 @@ export const TokenBalancesFragment = gql`
pSeedBalance
}
`;
export const PlayerSkillFragment = gql`
fragment PlayerSkillFragment on skill {
id
name
category
}
`;

View File

@@ -2,6 +2,9 @@ import gql from 'fake-tag';
import { Client } from 'urql';
import {
GetPlayerFiltersDocument,
GetPlayerFiltersQuery,
GetPlayerFiltersQueryVariables,
GetPlayersDocument,
GetPlayersQuery,
GetPlayersQueryVariables,
@@ -10,17 +13,17 @@ import {
PlayerFragmentFragment,
} from './autogen/types';
import { client as defaultClient } from './client';
import { PlayerFragment } from './fragments';
import { PlayerFragment, PlayerSkillFragment } from './fragments';
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
gql`
query GetPlayers(
$offset: Int
$limit: Int
$skillCategory: SkillCategory_enum
$playerType: Int
$skillIds: [uuid!]
$playerTypeIds: [Int!]
$availability: Int
$timezone: String
$timezones: [String!]
$search: String
) {
player(
@@ -28,10 +31,10 @@ gql`
offset: $offset
limit: $limit
where: {
Player_Skills: { Skill: { category: { _eq: $skillCategory } } }
playerType: { id: { _eq: $playerType } }
availability_hours: { _gte: $availability }
timezone: { _eq: $timezone }
timezone: { _in: $timezones }
playerType: { id: { _in: $playerTypeIds } }
Player_Skills: { Skill: { id: { _in: $skillIds } } }
_or: [
{ username: { _ilike: $search } }
{ ethereum_address: { _ilike: $search } }
@@ -44,13 +47,15 @@ gql`
${PlayerFragment}
`;
export const PLAYER_LIMIT = 56;
export const defaultQueryVariables: GetPlayersQueryVariables = {
offset: 0,
limit: 50,
skillCategory: undefined,
playerType: undefined,
limit: PLAYER_LIMIT,
availability: 0,
timezone: undefined,
skillIds: null,
playerTypeIds: null,
timezones: null,
search: '%%',
};
@@ -112,37 +117,27 @@ gql`
name: category
}
}
skill(
order_by: { Player_Skills_aggregate: { count: desc }, category: asc }
) {
...PlayerSkillFragment
}
player_type(distinct_on: id) {
id
title
}
}
${PlayerSkillFragment}
`;
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) => ({
...variables,
offset: i * limit,
limit: i < len - 1 ? limit : total - limit * (len - 1),
}));
export const getPlayerFilters = async (client: Client = defaultClient) => {
const { data, error } = await client
.query<GetPlayerFiltersQuery, GetPlayerFiltersQueryVariables>(
GetPlayerFiltersDocument,
)
.toPromise();
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: [] },
);
if (error) throw error;
return data;
};

View File

@@ -1,24 +1,24 @@
import gql from 'fake-tag';
import { GetSkillsQuery, PlayerSkillFragment } from 'graphql/autogen/types';
import {
GetSkillsQuery,
PlayerSkillFragmentFragment,
} from 'graphql/autogen/types';
import { client } from 'graphql/client';
import { PlayerSkillFragment } from './fragments';
const skillsQuery = gql`
query GetSkills {
skill(
order_by: { Player_Skills_aggregate: { count: desc }, category: asc }
) {
...PlayerSkill
...PlayerSkillFragment
}
}
fragment PlayerSkill on skill {
id
name
category
}
${PlayerSkillFragment}
`;
export const getSkills = async (): Promise<PlayerSkillFragment[]> => {
export const getSkills = async (): Promise<PlayerSkillFragmentFragment[]> => {
const { data, error } = await client
.query<GetSkillsQuery>(skillsQuery)
.toPromise();

View File

@@ -4,12 +4,9 @@ import {
useGetPlayerFiltersQuery,
useGetPlayersQuery,
} from 'graphql/autogen/types';
import {
defaultQueryVariables,
getPlayersInParallel,
PlayersResponse,
} from 'graphql/getPlayers';
import { useCallback, useEffect, useState } from 'react';
import { defaultQueryVariables } from 'graphql/getPlayers';
import { useCallback, useMemo, useState } from 'react';
import { CategoryOption, parseSkills } from 'utils/skillHelpers';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type QueryVariableSetter = (key: string, value: any) => void;
@@ -17,6 +14,7 @@ export type QueryVariableSetter = (key: string, value: any) => void;
export interface PlayerAggregates {
skillCategories: { name: string }[];
playerTypes: { id: number; title: string }[];
skillChoices: CategoryOption[];
}
interface PlayerFilter {
@@ -26,60 +24,27 @@ interface PlayerFilter {
queryVariables: GetPlayersQueryVariables;
setQueryVariable: QueryVariableSetter;
error?: Error;
resetFilter: () => void;
}
const usePlayerAggregates = () => {
const [{ data }] = useGetPlayerFiltersQuery();
const skillChoices = useMemo(() => parseSkills(data?.skill || []), [data]);
return {
skillCategories: data?.skill_aggregate.nodes || [],
playerTypes: data?.player_type || [],
skillChoices,
};
};
const usePlayersSingle = (
run: boolean,
variables: GetPlayersQueryVariables,
) => {
const useFilteredPlayers = (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,
@@ -99,14 +64,63 @@ export const usePlayerFilter = (): PlayerFilter => {
[],
);
const { fetching, players, error } = useFilteredPlayers(queryVariables);
const resetFilter = () => setQueryVariables(defaultQueryVariables);
const {
fetching: fetchingPlayers,
players,
error: errorPlayers,
} = useFilteredPlayers(queryVariables);
return {
players,
aggregates,
fetching,
error,
fetching: fetchingPlayers,
error: errorPlayers,
queryVariables,
setQueryVariable,
resetFilter,
};
};
export const useFiltersUsed = (
queryVariables: GetPlayersQueryVariables,
): { filtersUsed: boolean } => {
const playerTypesFilterUsed = useMemo(
() => (queryVariables.playerTypeIds as number[])?.length > 0,
[queryVariables.playerTypeIds],
);
const searchFilterUsed = useMemo(() => queryVariables.search !== '%%', [
queryVariables.search,
]);
const availabilityFilterUsed = useMemo(
() => (queryVariables.availability as number) > 0,
[queryVariables.availability],
);
const skillIdsFilterUsed = useMemo(
() => (queryVariables.skillIds as string[])?.length > 0,
[queryVariables.skillIds],
);
const timezonesFilterUsed = useMemo(
() => (queryVariables.timezones as string[])?.length > 0,
[queryVariables.timezones],
);
const filtersUsed = useMemo(
() =>
playerTypesFilterUsed ||
searchFilterUsed ||
availabilityFilterUsed ||
skillIdsFilterUsed ||
timezonesFilterUsed,
[
playerTypesFilterUsed,
searchFilterUsed,
availabilityFilterUsed,
skillIdsFilterUsed,
timezonesFilterUsed,
],
);
return {
filtersUsed,
};
};

View File

@@ -4,7 +4,7 @@ import { PlayerFilter } from 'components/Player/PlayerFilter';
import { PlayerList } from 'components/Player/PlayerList';
import { HeadComponent } from 'components/Seo';
import { getSsrClient } from 'graphql/client';
import { getPlayers } from 'graphql/getPlayers';
import { getPlayerFilters, getPlayers } from 'graphql/getPlayers';
import { usePlayerFilter } from 'lib/hooks/players';
import { InferGetStaticPropsType } from 'next';
import React from 'react';
@@ -13,8 +13,11 @@ type Props = InferGetStaticPropsType<typeof getStaticProps>;
export const getStaticProps = async () => {
const [ssrClient, ssrCache] = getSsrClient();
// This populate the cache server-side
await getPlayers(undefined, ssrClient);
await getPlayerFilters(ssrClient);
return {
props: {
urqlState: ssrCache.extractData(),
@@ -31,6 +34,7 @@ const Players: React.FC<Props> = () => {
error,
queryVariables,
setQueryVariable,
resetFilter,
} = usePlayerFilter();
return (
<PageContainer>
@@ -42,6 +46,7 @@ const Players: React.FC<Props> = () => {
queryVariables={queryVariables}
setQueryVariable={setQueryVariable}
players={players || []}
resetFilter={resetFilter}
/>
{error && <Text>{`Error: ${error.message}`}</Text>}
{fetching && <LoadingState />}

View File

@@ -1,10 +1,10 @@
import { PlayerSkillFragment } from '../graphql/autogen/types';
import { PlayerSkillFragmentFragment } from '../graphql/autogen/types';
export type SkillMap = {
[category: string]: CategoryOption;
};
export type SkillOption = PlayerSkillFragment & {
export type SkillOption = PlayerSkillFragmentFragment & {
value: string;
label: string;
};
@@ -15,7 +15,7 @@ export type CategoryOption = {
};
export const parseSkills = (
skills: Array<PlayerSkillFragment>,
skills: Array<PlayerSkillFragmentFragment>,
): Array<CategoryOption> => {
const skillsMap: SkillMap = {};
skills.forEach((skill) => {