mirror of
https://github.com/luxfi/explore.git
synced 2026-09-24 00:28:18 +00:00
add dapps sorting
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
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);
|
||||
@@ -0,0 +1,62 @@
|
||||
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);
|
||||
@@ -0,0 +1,47 @@
|
||||
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() {
|
||||
}, []);
|
||||
|
||||
const {
|
||||
isPlaceholderData, isError, error, data, displayedApps,
|
||||
isPlaceholderData, isError, error, data, displayedApps, setSorting,
|
||||
} = useMarketplaceApps(debouncedFilterQuery, selectedCategoryId, favoriteApps, isFavoriteAppsLoaded);
|
||||
const {
|
||||
isPlaceholderData: isCategoriesPlaceholderData, data: categories,
|
||||
@@ -150,6 +150,7 @@ export default function useMarketplace() {
|
||||
showContractList,
|
||||
contractListModalType,
|
||||
hasPreviousStep,
|
||||
setSorting,
|
||||
}), [
|
||||
selectedCategoryId,
|
||||
categories,
|
||||
@@ -172,5 +173,6 @@ export default function useMarketplace() {
|
||||
showContractList,
|
||||
contractListModalType,
|
||||
hasPreviousStep,
|
||||
setSorting,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -88,13 +88,22 @@ export default function useMarketplaceApps(
|
||||
enabled: feature.isEnabled && Boolean(snapshotFavoriteApps),
|
||||
});
|
||||
|
||||
const [ sorting, setSorting ] = React.useState<'default' | 'security_score'>('default');
|
||||
|
||||
const appsWithSecurityReports = React.useMemo(() =>
|
||||
data?.map((app) => ({ ...app, securityReport: securityReports?.[app.id] })),
|
||||
[ data, securityReports ]);
|
||||
|
||||
const displayedApps = React.useMemo(() => {
|
||||
return appsWithSecurityReports?.filter(app => isAppNameMatches(filter, app) && isAppCategoryMatches(selectedCategoryId, app, favoriteApps)) || [];
|
||||
}, [ selectedCategoryId, appsWithSecurityReports, filter, favoriteApps ]);
|
||||
return appsWithSecurityReports
|
||||
?.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(() => ({
|
||||
data,
|
||||
@@ -102,6 +111,7 @@ export default function useMarketplaceApps(
|
||||
error,
|
||||
isError,
|
||||
isPlaceholderData: isPlaceholderData || isSecurityReportsPlaceholderData,
|
||||
setSorting,
|
||||
}), [
|
||||
data,
|
||||
displayedApps,
|
||||
@@ -109,5 +119,6 @@ export default function useMarketplaceApps(
|
||||
isError,
|
||||
isPlaceholderData,
|
||||
isSecurityReportsPlaceholderData,
|
||||
setSorting,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import ContractListModal from 'ui/marketplace/ContractListModal';
|
||||
import MarketplaceAppModal from 'ui/marketplace/MarketplaceAppModal';
|
||||
import MarketplaceDisclaimerModal from 'ui/marketplace/MarketplaceDisclaimerModal';
|
||||
import MarketplaceList from 'ui/marketplace/MarketplaceList';
|
||||
import SortMenu from 'ui/marketplace/SortMenu/Menu';
|
||||
import FilterInput from 'ui/shared/filters/FilterInput';
|
||||
import IconSvg from 'ui/shared/IconSvg';
|
||||
import type { IconName } from 'ui/shared/IconSvg';
|
||||
@@ -66,6 +67,7 @@ const Marketplace = () => {
|
||||
showContractList,
|
||||
contractListModalType,
|
||||
hasPreviousStep,
|
||||
setSorting,
|
||||
} = useMarketplace();
|
||||
|
||||
const isMobile = useIsMobile();
|
||||
@@ -182,7 +184,19 @@ const Marketplace = () => {
|
||||
/>
|
||||
</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
|
||||
initialValue={ filterQuery }
|
||||
onChange={ onSearchInputChange }
|
||||
|
||||
@@ -10,13 +10,14 @@ type Props = {
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
const SortButton = ({ onClick, isActive, className, isLoading }: Props) => {
|
||||
const SortButton = ({ onClick, isActive, className, isLoading }: Props, ref: React.ForwardedRef<HTMLButtonElement>) => {
|
||||
if (isLoading) {
|
||||
return <Skeleton className={ className } w="36px" h="32px" borderRadius="base"/>;
|
||||
}
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
ref={ ref }
|
||||
icon={ <IconSvg name="arrows/up-down" boxSize={ 5 }/> }
|
||||
aria-label="sort"
|
||||
size="sm"
|
||||
@@ -31,4 +32,4 @@ const SortButton = ({ onClick, isActive, className, isLoading }: Props) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default chakra(SortButton);
|
||||
export default chakra(React.forwardRef(SortButton));
|
||||
|
||||
Reference in New Issue
Block a user