Commit 50d50376 authored by Max Alekseenko's avatar Max Alekseenko

add dapps sorting

parent ca4e12d6
import {
Box,
useColorModeValue,
Button,
Skeleton,
} from '@chakra-ui/react';
import React from 'react';
import useIsMobile from 'lib/hooks/useIsMobile';
import IconSvg from 'ui/shared/IconSvg';
import SortButtonMobile from 'ui/shared/sort/SortButton';
type ButtonProps = {
isMenuOpen: boolean;
onClick: () => void;
isLoading?: boolean;
children: React.ReactNode;
};
const SortButton = ({ children, isMenuOpen, onClick, isLoading }: ButtonProps, ref: React.ForwardedRef<HTMLButtonElement>) => {
const isMobile = useIsMobile();
const primaryColor = useColorModeValue('blackAlpha.800', 'whiteAlpha.800');
const secondaryColor = useColorModeValue('blackAlpha.600', 'whiteAlpha.600');
return (
<Skeleton isLoaded={ !isLoading }>
{ isMobile ? (
<SortButtonMobile ref={ ref } isActive={ isMenuOpen } onClick={ onClick }/>
) : (
<Button
ref={ ref }
size="sm"
variant="outline"
onClick={ onClick }
color={ primaryColor }
fontWeight="600"
borderColor="transparent"
px={ 2 }
data-selected={ isMenuOpen }
>
<Box
as={ isMenuOpen ? 'div' : 'span' }
color={ isMenuOpen ? 'inherit' : secondaryColor }
fontWeight="400"
mr={ 1 }
transition={ isMenuOpen ? 'none' : 'inherit' }
>Sort by</Box>
{ children }
<IconSvg
name="arrows/east-mini"
boxSize={ 5 }
ml={ 1 }
transform={ isMenuOpen ? 'rotate(90deg)' : 'rotate(-90deg)' }
/>
</Button>
) }
</Skeleton>
);
};
export default React.forwardRef(SortButton);
import {
Popover,
PopoverTrigger,
PopoverContent,
PopoverBody,
useDisclosure,
useRadioGroup,
} from '@chakra-ui/react';
import React from 'react';
import SortButton from './Button';
import Option from './Option';
import type { TOption } from './Option';
interface Props {
name: string;
options: Array<TOption>;
defaultValue?: string;
isLoading?: boolean;
onChange: (nextValue: 'default' | 'security_score') => void;
}
const SortMenu = ({ name, options, isLoading, onChange, defaultValue }: Props) => {
const { isOpen, onToggle, onClose } = useDisclosure();
const handleChange = (nextValue: 'default' | 'security_score') => {
onChange(nextValue);
onClose();
};
const { value, getRootProps, getRadioProps } = useRadioGroup({
name,
defaultValue,
onChange: handleChange,
});
const root = getRootProps();
return (
<Popover isOpen={ isOpen } onClose={ onClose } placement="bottom-start" isLazy>
<PopoverTrigger>
<SortButton isMenuOpen={ isOpen } isLoading={ isLoading } onClick={ onToggle }>
{ options.find(option => option.value === value)?.label }
</SortButton>
</PopoverTrigger>
<PopoverContent w="fit-content" minW="165px">
<PopoverBody { ...root } py={ 2 } px={ 0 } display="flex" flexDir="column">
{ options.map((option) => {
const radio = getRadioProps({ value: option.value });
return (
<Option key={ option.value } { ...radio }>
{ option.label }
</Option>
);
}) }
</PopoverBody>
</PopoverContent>
</Popover>
);
};
export default React.memo(SortMenu);
import {
useRadio,
Box,
useColorModeValue,
} from '@chakra-ui/react';
import type { useRadioGroup } from '@chakra-ui/react';
import React from 'react';
import IconSvg from 'ui/shared/IconSvg';
export interface TOption {
value: string;
label: string;
}
type OptionProps = ReturnType<ReturnType<typeof useRadioGroup>['getRadioProps']>;
const Option = (props: OptionProps) => {
const { getInputProps, getRadioProps } = useRadio(props);
const input = getInputProps();
const checkbox = getRadioProps();
const bgColorHover = useColorModeValue('blue.50', 'whiteAlpha.100');
return (
<Box
as="label"
px={ 4 }
py={ 2 }
cursor="pointer"
display="flex"
columnGap={ 2 }
alignItems="center"
_hover={{
bgColor: bgColorHover,
}}
>
<input { ...input }/>
<Box { ...checkbox }>
{ props.children }
</Box>
{ props.isChecked && <IconSvg name="check" boxSize={ 4 } color="blue.600"/> }
</Box>
);
};
export default Option;
...@@ -86,7 +86,7 @@ export default function useMarketplace() { ...@@ -86,7 +86,7 @@ export default function useMarketplace() {
}, []); }, []);
const { const {
isPlaceholderData, isError, error, data, displayedApps, isPlaceholderData, isError, error, data, displayedApps, setSorting,
} = useMarketplaceApps(debouncedFilterQuery, selectedCategoryId, favoriteApps, isFavoriteAppsLoaded); } = useMarketplaceApps(debouncedFilterQuery, selectedCategoryId, favoriteApps, isFavoriteAppsLoaded);
const { const {
isPlaceholderData: isCategoriesPlaceholderData, data: categories, isPlaceholderData: isCategoriesPlaceholderData, data: categories,
...@@ -150,6 +150,7 @@ export default function useMarketplace() { ...@@ -150,6 +150,7 @@ export default function useMarketplace() {
showContractList, showContractList,
contractListModalType, contractListModalType,
hasPreviousStep, hasPreviousStep,
setSorting,
}), [ }), [
selectedCategoryId, selectedCategoryId,
categories, categories,
...@@ -172,5 +173,6 @@ export default function useMarketplace() { ...@@ -172,5 +173,6 @@ export default function useMarketplace() {
showContractList, showContractList,
contractListModalType, contractListModalType,
hasPreviousStep, hasPreviousStep,
setSorting,
]); ]);
} }
...@@ -88,13 +88,22 @@ export default function useMarketplaceApps( ...@@ -88,13 +88,22 @@ export default function useMarketplaceApps(
enabled: feature.isEnabled && Boolean(snapshotFavoriteApps), enabled: feature.isEnabled && Boolean(snapshotFavoriteApps),
}); });
const [ sorting, setSorting ] = React.useState<'default' | 'security_score'>('default');
const appsWithSecurityReports = React.useMemo(() => const appsWithSecurityReports = React.useMemo(() =>
data?.map((app) => ({ ...app, securityReport: securityReports?.[app.id] })), data?.map((app) => ({ ...app, securityReport: securityReports?.[app.id] })),
[ data, securityReports ]); [ data, securityReports ]);
const displayedApps = React.useMemo(() => { const displayedApps = React.useMemo(() => {
return appsWithSecurityReports?.filter(app => isAppNameMatches(filter, app) && isAppCategoryMatches(selectedCategoryId, app, favoriteApps)) || []; return appsWithSecurityReports
}, [ selectedCategoryId, appsWithSecurityReports, filter, favoriteApps ]); ?.filter(app => isAppNameMatches(filter, app) && isAppCategoryMatches(selectedCategoryId, app, favoriteApps))
.sort((a, b) => {
if (sorting === 'security_score') {
return (b.securityReport?.overallInfo.securityScore || 0) - (a.securityReport?.overallInfo.securityScore || 0);
}
return 0;
}) || [];
}, [ selectedCategoryId, appsWithSecurityReports, filter, favoriteApps, sorting ]);
return React.useMemo(() => ({ return React.useMemo(() => ({
data, data,
...@@ -102,6 +111,7 @@ export default function useMarketplaceApps( ...@@ -102,6 +111,7 @@ export default function useMarketplaceApps(
error, error,
isError, isError,
isPlaceholderData: isPlaceholderData || isSecurityReportsPlaceholderData, isPlaceholderData: isPlaceholderData || isSecurityReportsPlaceholderData,
setSorting,
}), [ }), [
data, data,
displayedApps, displayedApps,
...@@ -109,5 +119,6 @@ export default function useMarketplaceApps( ...@@ -109,5 +119,6 @@ export default function useMarketplaceApps(
isError, isError,
isPlaceholderData, isPlaceholderData,
isSecurityReportsPlaceholderData, isSecurityReportsPlaceholderData,
setSorting,
]); ]);
} }
...@@ -13,6 +13,7 @@ import ContractListModal from 'ui/marketplace/ContractListModal'; ...@@ -13,6 +13,7 @@ import ContractListModal from 'ui/marketplace/ContractListModal';
import MarketplaceAppModal from 'ui/marketplace/MarketplaceAppModal'; import MarketplaceAppModal from 'ui/marketplace/MarketplaceAppModal';
import MarketplaceDisclaimerModal from 'ui/marketplace/MarketplaceDisclaimerModal'; import MarketplaceDisclaimerModal from 'ui/marketplace/MarketplaceDisclaimerModal';
import MarketplaceList from 'ui/marketplace/MarketplaceList'; import MarketplaceList from 'ui/marketplace/MarketplaceList';
import SortMenu from 'ui/marketplace/SortMenu/Menu';
import FilterInput from 'ui/shared/filters/FilterInput'; import FilterInput from 'ui/shared/filters/FilterInput';
import IconSvg from 'ui/shared/IconSvg'; import IconSvg from 'ui/shared/IconSvg';
import type { IconName } from 'ui/shared/IconSvg'; import type { IconName } from 'ui/shared/IconSvg';
...@@ -66,6 +67,7 @@ const Marketplace = () => { ...@@ -66,6 +67,7 @@ const Marketplace = () => {
showContractList, showContractList,
contractListModalType, contractListModalType,
hasPreviousStep, hasPreviousStep,
setSorting,
} = useMarketplace(); } = useMarketplace();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
...@@ -182,7 +184,19 @@ const Marketplace = () => { ...@@ -182,7 +184,19 @@ const Marketplace = () => {
/> />
</Box> </Box>
<Flex direction={{ base: 'column', lg: 'row' }} mb={{ base: 4, lg: 6 }} gap={{ base: 4, lg: 3 }}> <Flex mb={{ base: 4, lg: 6 }} gap={{ base: 2, lg: 3 }}>
{ feature.securityReportsUrl && (
<SortMenu
name="dapps_sorting"
defaultValue="default"
options={ [
{ value: 'default', label: 'Default' },
{ value: 'security_score', label: 'Security score' },
] }
onChange={ setSorting }
isLoading={ isPlaceholderData }
/>
) }
<FilterInput <FilterInput
initialValue={ filterQuery } initialValue={ filterQuery }
onChange={ onSearchInputChange } onChange={ onSearchInputChange }
......
...@@ -10,13 +10,14 @@ type Props = { ...@@ -10,13 +10,14 @@ type Props = {
isLoading?: boolean; isLoading?: boolean;
} }
const SortButton = ({ onClick, isActive, className, isLoading }: Props) => { const SortButton = ({ onClick, isActive, className, isLoading }: Props, ref: React.ForwardedRef<HTMLButtonElement>) => {
if (isLoading) { if (isLoading) {
return <Skeleton className={ className } w="36px" h="32px" borderRadius="base"/>; return <Skeleton className={ className } w="36px" h="32px" borderRadius="base"/>;
} }
return ( return (
<IconButton <IconButton
ref={ ref }
icon={ <IconSvg name="arrows/up-down" boxSize={ 5 }/> } icon={ <IconSvg name="arrows/up-down" boxSize={ 5 }/> }
aria-label="sort" aria-label="sort"
size="sm" size="sm"
...@@ -31,4 +32,4 @@ const SortButton = ({ onClick, isActive, className, isLoading }: Props) => { ...@@ -31,4 +32,4 @@ const SortButton = ({ onClick, isActive, className, isLoading }: Props) => {
); );
}; };
export default chakra(SortButton); export default chakra(React.forwardRef(SortButton));
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment