mirror of
https://github.com/MetaFam/TheGame.git
synced 2026-04-02 03:00:32 -04:00
updated filters with new design
This commit is contained in:
359
packages/design-system/src/MetaFilterSelect.tsx
Normal file
359
packages/design-system/src/MetaFilterSelect.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
@@ -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,
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -160,3 +160,11 @@ export const TokenBalancesFragment = gql`
|
||||
pSeedBalance
|
||||
}
|
||||
`;
|
||||
|
||||
export const PlayerSkillFragment = gql`
|
||||
fragment PlayerSkillFragment on skill {
|
||||
id
|
||||
name
|
||||
category
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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 />}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user