Merge pull request #2031 from blockscout/dapp-card-redesign
Combine default and security score view on the Marketplace page
@@ -41,11 +41,6 @@ export enum ContractListTypes {
|
||||
VERIFIED = 'Verified',
|
||||
}
|
||||
|
||||
export enum MarketplaceDisplayType {
|
||||
DEFAULT = 'default',
|
||||
SCORES = 'scores',
|
||||
}
|
||||
|
||||
export type MarketplaceAppSecurityReport = {
|
||||
overallInfo: {
|
||||
verifiedNumber: number;
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import { Box, Text, Link, Popover, PopoverTrigger, PopoverBody, PopoverContent, useDisclosure } from '@chakra-ui/react';
|
||||
import { Box, Text, Link, Popover, PopoverTrigger, PopoverBody, PopoverContent, useDisclosure, chakra, Flex, Divider, Icon } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import type { MarketplaceAppSecurityReport } from 'types/client/marketplace';
|
||||
import { ContractListTypes } from 'types/client/marketplace';
|
||||
|
||||
import config from 'configs/app';
|
||||
// This icon doesn't work properly when it is in the sprite
|
||||
// Probably because of the gradient
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import solidityScanIcon from 'icons/brands/solidity_scan.svg';
|
||||
import { apos } from 'lib/html-entities';
|
||||
import * as mixpanel from 'lib/mixpanel/index';
|
||||
import IconSvg from 'ui/shared/IconSvg';
|
||||
@@ -14,13 +19,17 @@ import SolidityscanReportScore from 'ui/shared/solidityscanReport/SolidityscanRe
|
||||
type Props = {
|
||||
id: string;
|
||||
securityReport?: MarketplaceAppSecurityReport;
|
||||
showContractList: () => void;
|
||||
showContractList: (id: string, type: ContractListTypes) => void;
|
||||
isLoading?: boolean;
|
||||
onlyIcon?: boolean;
|
||||
source: 'Security view' | 'App modal' | 'App page';
|
||||
source: 'Discovery view' | 'App modal' | 'App page';
|
||||
className?: string;
|
||||
popoverPlacement?: 'bottom-start' | 'bottom-end' | 'left';
|
||||
}
|
||||
|
||||
const AppSecurityReport = ({ id, securityReport, showContractList, isLoading, onlyIcon, source }: Props) => {
|
||||
const AppSecurityReport = ({
|
||||
id, securityReport, showContractList, isLoading, onlyIcon, source, className, popoverPlacement = 'bottom-start',
|
||||
}: Props) => {
|
||||
const { isOpen, onToggle, onClose } = useDisclosure();
|
||||
|
||||
const handleButtonClick = React.useCallback(() => {
|
||||
@@ -28,14 +37,15 @@ const AppSecurityReport = ({ id, securityReport, showContractList, isLoading, on
|
||||
onToggle();
|
||||
}, [ id, source, onToggle ]);
|
||||
|
||||
const handleLinkClick = React.useCallback(() => {
|
||||
const showAnalyzedContracts = React.useCallback(() => {
|
||||
mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'Analyzed contracts', Info: id, Source: 'Security score popup' });
|
||||
showContractList();
|
||||
}, [ id, showContractList ]);
|
||||
showContractList(id, ContractListTypes.ANALYZED);
|
||||
}, [ showContractList, id ]);
|
||||
|
||||
if (!securityReport && !isLoading) {
|
||||
return null;
|
||||
}
|
||||
const showAllContracts = React.useCallback(() => {
|
||||
mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'Total contracts', Info: id, Source: 'Security score popup' });
|
||||
showContractList(id, ContractListTypes.ALL);
|
||||
}, [ showContractList, id ]);
|
||||
|
||||
const {
|
||||
securityScore = 0,
|
||||
@@ -44,8 +54,12 @@ const AppSecurityReport = ({ id, securityReport, showContractList, isLoading, on
|
||||
totalIssues = 0,
|
||||
} = securityReport?.overallInfo || {};
|
||||
|
||||
if ((!securityReport || !securityScore) && !isLoading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover isOpen={ isOpen } onClose={ onClose } placement="bottom-start" isLazy>
|
||||
<Popover isOpen={ isOpen } onClose={ onClose } placement={ popoverPlacement } isLazy>
|
||||
<PopoverTrigger>
|
||||
<SolidityscanReportButton
|
||||
score={ securityScore }
|
||||
@@ -53,14 +67,30 @@ const AppSecurityReport = ({ id, securityReport, showContractList, isLoading, on
|
||||
onClick={ handleButtonClick }
|
||||
isActive={ isOpen }
|
||||
onlyIcon={ onlyIcon }
|
||||
label="The security score is based on analysis of a DApp's smart contracts."
|
||||
label={ <>The security score is based on analysis<br/>of a DApp{ apos }s smart contracts.</> }
|
||||
className={ className }
|
||||
/>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent w={{ base: '100vw', lg: '328px' }}>
|
||||
<PopoverBody px="26px" py="20px" fontSize="sm">
|
||||
<Text fontWeight="500" fontSize="xs" mb={ 2 } variant="secondary">Smart contracts info</Text>
|
||||
<Flex alignItems="center" justifyContent="space-between" py={ 1.5 }>
|
||||
<Flex alignItems="center">
|
||||
<IconSvg name="contracts_verified" boxSize={ 5 } color="green.500" mr={ 1 }/>
|
||||
<Text>Verified contracts</Text>
|
||||
</Flex>
|
||||
<Link fontSize="sm" fontWeight="500" onClick={ showAllContracts }>
|
||||
{ securityReport?.overallInfo.verifiedNumber ?? 0 } of { securityReport?.overallInfo.totalContractsNumber ?? 0 }
|
||||
</Link>
|
||||
</Flex>
|
||||
<Divider my={ 3 }/>
|
||||
<Box mb={ 5 }>
|
||||
{ solidityScanContractsNumber } smart contract{ solidityScanContractsNumber === 1 ? ' was' : 's were' } evaluated to determine
|
||||
this protocol{ apos }s overall security score on the { config.chain.name } network.
|
||||
this protocol{ apos }s overall security score on the { config.chain.name } network by { ' ' }
|
||||
<Box>
|
||||
<Icon as={ solidityScanIcon } mr={ 1 } w="23px" h="20px" display="inline-block" verticalAlign="middle"/>
|
||||
<Text fontWeight={ 600 } display="inline-block">SolidityScan</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
<SolidityscanReportScore score={ securityScore } mb={ 5 }/>
|
||||
{ issueSeverityDistribution && totalIssues > 0 && (
|
||||
@@ -69,9 +99,8 @@ const AppSecurityReport = ({ id, securityReport, showContractList, isLoading, on
|
||||
<SolidityscanReportDetails vulnerabilities={ issueSeverityDistribution } vulnerabilitiesCount={ totalIssues }/>
|
||||
</Box>
|
||||
) }
|
||||
<Link onClick={ handleLinkClick } display="inline-flex" alignItems="center">
|
||||
<Link onClick={ showAnalyzedContracts } display="inline-flex" alignItems="center">
|
||||
Analyzed contracts
|
||||
<IconSvg name="arrows/north-east" boxSize={ 5 } color="gray.400"/>
|
||||
</Link>
|
||||
</PopoverBody>
|
||||
</PopoverContent>
|
||||
@@ -79,4 +108,4 @@ const AppSecurityReport = ({ id, securityReport, showContractList, isLoading, on
|
||||
);
|
||||
};
|
||||
|
||||
export default AppSecurityReport;
|
||||
export default chakra(AppSecurityReport);
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
import { Link, Tooltip, Skeleton } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
import type { MouseEvent } from 'react';
|
||||
|
||||
import config from 'configs/app';
|
||||
import IconSvg from 'ui/shared/IconSvg';
|
||||
|
||||
export enum ContractListButtonVariants {
|
||||
ALL_CONTRACTS = 'all contracts',
|
||||
VERIFIED_CONTRACTS = 'verified contracts',
|
||||
}
|
||||
|
||||
const values = {
|
||||
[ContractListButtonVariants.ALL_CONTRACTS]: {
|
||||
icon: 'contracts' as const,
|
||||
iconColor: 'gray.500',
|
||||
tooltip: `Total number of contracts deployed by the protocol on ${ config.chain.name }`,
|
||||
},
|
||||
[ContractListButtonVariants.VERIFIED_CONTRACTS]: {
|
||||
icon: 'contracts_verified' as const,
|
||||
iconColor: 'green.500',
|
||||
tooltip: `Number of verified contracts on ${ config.chain.name }`,
|
||||
},
|
||||
};
|
||||
|
||||
interface Props {
|
||||
children: string | number;
|
||||
onClick: (event: MouseEvent) => void;
|
||||
variant: ContractListButtonVariants;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
const ContractListButton = ({ children, onClick, variant, isLoading }: Props) => {
|
||||
const { icon, iconColor, tooltip } = values[variant];
|
||||
return (
|
||||
<Tooltip
|
||||
label={ tooltip }
|
||||
textAlign="center"
|
||||
padding={ 2 }
|
||||
isDisabled={ !tooltip }
|
||||
openDelay={ 500 }
|
||||
width="250px"
|
||||
>
|
||||
<Skeleton
|
||||
isLoaded={ !isLoading }
|
||||
display="inline-flex"
|
||||
alignItems="center"
|
||||
width={ isLoading ? '40px' : 'auto' }
|
||||
height="30px"
|
||||
borderRadius="base"
|
||||
>
|
||||
<Link
|
||||
fontSize="sm"
|
||||
onClick={ onClick }
|
||||
fontWeight="500"
|
||||
display="inline-flex"
|
||||
>
|
||||
{ icon && <IconSvg name={ icon } boxSize={ 5 } color={ iconColor } mr={ 1 }/> }
|
||||
{ children }
|
||||
</Link>
|
||||
</Skeleton>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export default ContractListButton;
|
||||
@@ -37,7 +37,7 @@ const ContractListModal = ({ onClose, onBack, type, contracts }: Props) => {
|
||||
switch (type) {
|
||||
default:
|
||||
case ContractListTypes.ALL:
|
||||
return contracts;
|
||||
return contracts.sort((a) => a.isVerified ? -1 : 1);
|
||||
case ContractListTypes.ANALYZED:
|
||||
return contracts
|
||||
.filter((contract) => Boolean(contract.solidityScanReport))
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import { Box, Text, Popover, PopoverTrigger, PopoverBody, PopoverContent, useDisclosure } from '@chakra-ui/react';
|
||||
import { Box, Text, Popover, PopoverTrigger, PopoverBody, PopoverContent, useDisclosure, Icon } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import type { SolidityscanReport } from 'types/api/contract';
|
||||
|
||||
import config from 'configs/app';
|
||||
// This icon doesn't work properly when it is in the sprite
|
||||
// Probably because of the gradient
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import solidityScanIcon from 'icons/brands/solidity_scan.svg';
|
||||
import * as mixpanel from 'lib/mixpanel/index';
|
||||
import LinkExternal from 'ui/shared/links/LinkExternal';
|
||||
import SolidityscanReportButton from 'ui/shared/solidityscanReport/SolidityscanReportButton';
|
||||
@@ -46,7 +50,11 @@ const ContractSecurityReport = ({ securityReport }: Props) => {
|
||||
<PopoverContent w={{ base: '100vw', lg: '328px' }}>
|
||||
<PopoverBody px="26px" py="20px" fontSize="sm">
|
||||
<Box mb={ 5 }>
|
||||
The security score was derived from evaluating the smart contracts of a protocol on the { config.chain.name } network.
|
||||
The security score was derived from evaluating the smart contracts of a protocol on the { config.chain.name } network by { ' ' }
|
||||
<Box>
|
||||
<Icon as={ solidityScanIcon } mr={ 1 } w="23px" h="20px" display="inline-block" verticalAlign="middle"/>
|
||||
<Text fontWeight={ 600 } display="inline-block">SolidityScan</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
<SolidityscanReportScore score={ parseFloat(securityScore) } mb={ 5 }/>
|
||||
{ issueSeverityDistribution && totalIssues > 0 && (
|
||||
|
||||
@@ -2,20 +2,23 @@ import { Box, IconButton, Image, Link, LinkBox, Skeleton, useColorModeValue, cha
|
||||
import type { MouseEvent } from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import type { MarketplaceAppPreview } from 'types/client/marketplace';
|
||||
import type { MarketplaceAppWithSecurityReport, ContractListTypes } from 'types/client/marketplace';
|
||||
|
||||
import useIsMobile from 'lib/hooks/useIsMobile';
|
||||
import IconSvg from 'ui/shared/IconSvg';
|
||||
|
||||
import AppSecurityReport from './AppSecurityReport';
|
||||
import MarketplaceAppCardLink from './MarketplaceAppCardLink';
|
||||
import MarketplaceAppIntegrationIcon from './MarketplaceAppIntegrationIcon';
|
||||
|
||||
interface Props extends MarketplaceAppPreview {
|
||||
interface Props extends MarketplaceAppWithSecurityReport {
|
||||
onInfoClick: (id: string) => void;
|
||||
isFavorite: boolean;
|
||||
onFavoriteClick: (id: string, isFavorite: boolean) => void;
|
||||
isLoading: boolean;
|
||||
onAppClick: (event: MouseEvent, id: string) => void;
|
||||
className?: string;
|
||||
showContractList: (id: string, type: ContractListTypes) => void;
|
||||
}
|
||||
|
||||
const MarketplaceAppCard = ({
|
||||
@@ -33,8 +36,11 @@ const MarketplaceAppCard = ({
|
||||
isLoading,
|
||||
internalWallet,
|
||||
onAppClick,
|
||||
securityReport,
|
||||
className,
|
||||
showContractList,
|
||||
}: Props) => {
|
||||
const isMobile = useIsMobile();
|
||||
const categoriesLabel = categories.join(', ');
|
||||
|
||||
const handleInfoClick = useCallback((event: MouseEvent) => {
|
||||
@@ -58,32 +64,29 @@ const MarketplaceAppCard = ({
|
||||
boxShadow: isLoading ? 'none' : 'md',
|
||||
}}
|
||||
borderRadius="md"
|
||||
padding={{ base: 3, sm: '20px' }}
|
||||
padding={{ base: 3, md: '20px' }}
|
||||
border="1px"
|
||||
borderColor={ useColorModeValue('gray.200', 'gray.600') }
|
||||
role="group"
|
||||
>
|
||||
<Flex
|
||||
flexDirection={{ base: 'row', sm: 'column' }}
|
||||
flexDirection="column"
|
||||
height="100%"
|
||||
alignContent="start"
|
||||
gap={{ base: 4, sm: 0 }}
|
||||
gap={ 2 }
|
||||
>
|
||||
<Flex
|
||||
display={{ base: 'flex', sm: 'contents' }}
|
||||
flexDirection="column"
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
display={{ base: 'flex', md: 'contents' }}
|
||||
gap={ 4 }
|
||||
>
|
||||
<Skeleton
|
||||
isLoaded={ !isLoading }
|
||||
marginBottom={ 4 }
|
||||
w={{ base: '64px', sm: '96px' }}
|
||||
h={{ base: '64px', sm: '96px' }}
|
||||
w={{ base: '64px', md: '96px' }}
|
||||
h={{ base: '64px', md: '96px' }}
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
order={{ base: 'auto', sm: 1 }}
|
||||
mb={{ base: 0, md: 2 }}
|
||||
>
|
||||
<Image
|
||||
src={ isLoading ? undefined : logoUrl }
|
||||
@@ -92,93 +95,96 @@ const MarketplaceAppCard = ({
|
||||
/>
|
||||
</Skeleton>
|
||||
|
||||
{ !isLoading && (
|
||||
<Box
|
||||
display="flex"
|
||||
marginTop={{ base: 0, sm: 'auto' }}
|
||||
paddingTop={{ base: 0, sm: 4 }}
|
||||
order={{ base: 'auto', sm: 5 }}
|
||||
<Flex
|
||||
display={{ base: 'flex', md: 'contents' }}
|
||||
flexDirection="column"
|
||||
gap={ 2 }
|
||||
pt={ 1 }
|
||||
>
|
||||
<Skeleton
|
||||
isLoaded={ !isLoading }
|
||||
fontSize={{ base: 'sm', md: 'lg' }}
|
||||
lineHeight={{ base: '20px', md: '28px' }}
|
||||
paddingRight={{ base: '40px', md: 0 }}
|
||||
fontWeight="semibold"
|
||||
fontFamily="heading"
|
||||
display="inline-block"
|
||||
>
|
||||
<Link
|
||||
fontSize={{ base: 'xs', sm: 'sm' }}
|
||||
fontWeight="500"
|
||||
paddingRight={{ sm: 2 }}
|
||||
href="#"
|
||||
onClick={ handleInfoClick }
|
||||
>
|
||||
More info
|
||||
</Link>
|
||||
</Box>
|
||||
) }
|
||||
<MarketplaceAppCardLink
|
||||
id={ id }
|
||||
url={ url }
|
||||
external={ external }
|
||||
title={ title }
|
||||
onClick={ onAppClick }
|
||||
/>
|
||||
<MarketplaceAppIntegrationIcon external={ external } internalWallet={ internalWallet }/>
|
||||
</Skeleton>
|
||||
|
||||
<Skeleton
|
||||
isLoaded={ !isLoading }
|
||||
color="text_secondary"
|
||||
fontSize="xs"
|
||||
lineHeight="16px"
|
||||
>
|
||||
<span>{ categoriesLabel }</span>
|
||||
</Skeleton>
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
<Flex
|
||||
display={{ base: 'flex', sm: 'contents' }}
|
||||
flexDirection="column"
|
||||
gap={ 2 }
|
||||
<Skeleton
|
||||
isLoaded={ !isLoading }
|
||||
fontSize="sm"
|
||||
lineHeight="20px"
|
||||
noOfLines={{ base: 2, md: 3 }}
|
||||
>
|
||||
<Skeleton
|
||||
isLoaded={ !isLoading }
|
||||
marginBottom={{ base: 0, sm: 2 }}
|
||||
fontSize={{ base: 'sm', sm: 'lg' }}
|
||||
lineHeight={{ base: '20px', sm: '28px' }}
|
||||
paddingRight={{ base: '25px', sm: 0 }}
|
||||
fontWeight="semibold"
|
||||
fontFamily="heading"
|
||||
display="inline-block"
|
||||
order={{ base: 'auto', sm: 2 }}
|
||||
>
|
||||
<MarketplaceAppCardLink
|
||||
id={ id }
|
||||
url={ url }
|
||||
external={ external }
|
||||
title={ title }
|
||||
onClick={ onAppClick }
|
||||
/>
|
||||
<MarketplaceAppIntegrationIcon external={ external } internalWallet={ internalWallet }/>
|
||||
</Skeleton>
|
||||
|
||||
<Skeleton
|
||||
isLoaded={ !isLoading }
|
||||
marginBottom={{ base: 0, sm: 2 }}
|
||||
color="text_secondary"
|
||||
fontSize="xs"
|
||||
lineHeight="16px"
|
||||
order={{ base: 'auto', sm: 3 }}
|
||||
>
|
||||
<span>{ categoriesLabel }</span>
|
||||
</Skeleton>
|
||||
|
||||
<Skeleton
|
||||
isLoaded={ !isLoading }
|
||||
fontSize={{ base: 'xs', sm: 'sm' }}
|
||||
lineHeight="20px"
|
||||
noOfLines={ 3 }
|
||||
order={{ base: 'auto', sm: 4 }}
|
||||
>
|
||||
{ shortDescription }
|
||||
</Skeleton>
|
||||
</Flex>
|
||||
{ shortDescription }
|
||||
</Skeleton>
|
||||
|
||||
{ !isLoading && (
|
||||
<IconButton
|
||||
<Box
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
justifyContent="space-between"
|
||||
marginTop="auto"
|
||||
>
|
||||
<Link
|
||||
fontSize="sm"
|
||||
fontWeight="500"
|
||||
paddingRight={{ md: 2 }}
|
||||
href="#"
|
||||
onClick={ handleInfoClick }
|
||||
>
|
||||
More info
|
||||
</Link>
|
||||
<IconButton
|
||||
aria-label="Mark as favorite"
|
||||
title="Mark as favorite"
|
||||
variant="ghost"
|
||||
colorScheme="gray"
|
||||
w={{ base: 6, md: '30px' }}
|
||||
h={{ base: 6, md: '30px' }}
|
||||
onClick={ handleFavoriteClick }
|
||||
icon={ isFavorite ?
|
||||
<IconSvg name="star_filled" w={ 5 } h={ 5 } color="yellow.400"/> :
|
||||
<IconSvg name="star_outline" w={ 5 } h={ 5 } color="gray.400"/>
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
) }
|
||||
|
||||
{ securityReport && (
|
||||
<AppSecurityReport
|
||||
id={ id }
|
||||
securityReport={ securityReport }
|
||||
showContractList={ showContractList }
|
||||
isLoading={ isLoading }
|
||||
source="Discovery view"
|
||||
popoverPlacement={ isMobile ? 'bottom-end' : 'bottom-start' }
|
||||
position="absolute"
|
||||
right={{ base: 1, sm: '10px' }}
|
||||
top={{ base: 1, sm: '10px' }}
|
||||
aria-label="Mark as favorite"
|
||||
title="Mark as favorite"
|
||||
variant="ghost"
|
||||
colorScheme="gray"
|
||||
w={ 9 }
|
||||
h={ 8 }
|
||||
onClick={ handleFavoriteClick }
|
||||
icon={ isFavorite ?
|
||||
<IconSvg name="star_filled" w={ 5 } h={ 5 } color="yellow.400"/> :
|
||||
<IconSvg name="star_outline" w={ 5 } h={ 5 } color="gray.400"/>
|
||||
}
|
||||
right={{ base: 3, md: 5 }}
|
||||
top={{ base: '10px', md: 5 }}
|
||||
border={ 0 }
|
||||
padding={ 0 }
|
||||
/>
|
||||
) }
|
||||
</Flex>
|
||||
|
||||
@@ -42,7 +42,7 @@ const MarketplaceAppIntegrationIcon = ({ external, internalWallet }: Props) => {
|
||||
position="relative"
|
||||
cursor="pointer"
|
||||
verticalAlign="middle"
|
||||
mb={ 1 }
|
||||
mb={{ base: 0, md: 1 }}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
@@ -3,6 +3,7 @@ import React from 'react';
|
||||
import type { MarketplaceAppWithSecurityReport } from 'types/client/marketplace';
|
||||
|
||||
import { apps as appsMock } from 'mocks/apps/apps';
|
||||
import { securityReports as securityReportsMock } from 'mocks/apps/securityReports';
|
||||
import { test, expect, devices } from 'playwright/lib';
|
||||
|
||||
import MarketplaceAppModal from './MarketplaceAppModal';
|
||||
@@ -11,7 +12,10 @@ const props = {
|
||||
onClose: () => {},
|
||||
onFavoriteClick: () => {},
|
||||
showContractList: () => {},
|
||||
data: appsMock[0] as MarketplaceAppWithSecurityReport,
|
||||
data: {
|
||||
...appsMock[0],
|
||||
securityReport: securityReportsMock[0].chainsData['1'],
|
||||
} as MarketplaceAppWithSecurityReport,
|
||||
isFavorite: false,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {
|
||||
Box, Flex, Heading, IconButton, Image, Link, List, Modal, ModalBody,
|
||||
Box, Flex, Heading, IconButton, Image, Link, Modal, ModalBody,
|
||||
ModalCloseButton, ModalContent, ModalFooter, ModalOverlay, Tag, Text, useColorModeValue,
|
||||
} from '@chakra-ui/react';
|
||||
import React, { useCallback } from 'react';
|
||||
@@ -14,7 +14,6 @@ import type { IconName } from 'ui/shared/IconSvg';
|
||||
import IconSvg from 'ui/shared/IconSvg';
|
||||
|
||||
import AppSecurityReport from './AppSecurityReport';
|
||||
import ContractListButton, { ContractListButtonVariants } from './ContractListButton';
|
||||
import MarketplaceAppModalLink from './MarketplaceAppModalLink';
|
||||
|
||||
type Props = {
|
||||
@@ -79,25 +78,16 @@ const MarketplaceAppModal = ({
|
||||
onFavoriteClick(id, isFavorite, 'App modal');
|
||||
}, [ onFavoriteClick, id, isFavorite ]);
|
||||
|
||||
const showContractList = useCallback((type: ContractListTypes) => {
|
||||
const showContractList = useCallback((id: string, type: ContractListTypes) => {
|
||||
onClose();
|
||||
showContractListProp(id, type, true);
|
||||
}, [ onClose, showContractListProp, id ]);
|
||||
}, [ onClose, showContractListProp ]);
|
||||
|
||||
const showAllContracts = React.useCallback(() => {
|
||||
mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'Total contracts', Info: id, Source: 'App modal' });
|
||||
showContractList(ContractListTypes.ALL);
|
||||
showContractList(id, ContractListTypes.ALL);
|
||||
}, [ showContractList, id ]);
|
||||
|
||||
const showVerifiedContracts = React.useCallback(() => {
|
||||
mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'Verified contracts', Info: id, Source: 'App modal' });
|
||||
showContractList(ContractListTypes.VERIFIED);
|
||||
}, [ showContractList, id ]);
|
||||
|
||||
const showAnalyzedContracts = React.useCallback(() => {
|
||||
showContractList(ContractListTypes.ANALYZED);
|
||||
}, [ showContractList ]);
|
||||
|
||||
const isMobile = useIsMobile();
|
||||
const logoUrl = useColorModeValue(logo, logoDarkMode || logo);
|
||||
|
||||
@@ -120,16 +110,16 @@ const MarketplaceAppModal = ({
|
||||
<Box
|
||||
display="grid"
|
||||
gridTemplateColumns={{ base: 'auto 1fr' }}
|
||||
paddingRight={{ sm: 12 }}
|
||||
marginBottom={{ base: 6, sm: 8 }}
|
||||
paddingRight={{ md: 12 }}
|
||||
marginBottom={{ base: 6, md: 8 }}
|
||||
>
|
||||
<Flex
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
w={{ base: '72px', sm: '144px' }}
|
||||
h={{ base: '72px', sm: '144px' }}
|
||||
marginRight={{ base: 6, sm: 8 }}
|
||||
gridRow={{ base: '1 / 3', sm: '1 / 4' }}
|
||||
w={{ base: '72px', md: '144px' }}
|
||||
h={{ base: '72px', md: '144px' }}
|
||||
marginRight={{ base: 6, md: 8 }}
|
||||
gridRow={{ base: '1 / 3', md: '1 / 4' }}
|
||||
>
|
||||
<Image
|
||||
src={ logoUrl }
|
||||
@@ -141,7 +131,7 @@ const MarketplaceAppModal = ({
|
||||
<Heading
|
||||
as="h2"
|
||||
gridColumn={ 2 }
|
||||
fontSize={{ base: '2xl', sm: '3xl' }}
|
||||
fontSize={{ base: '2xl', md: '3xl' }}
|
||||
fontWeight="medium"
|
||||
lineHeight={ 1 }
|
||||
color="blue.600"
|
||||
@@ -160,11 +150,11 @@ const MarketplaceAppModal = ({
|
||||
</Text>
|
||||
|
||||
<Box
|
||||
gridColumn={{ base: '1 / 3', sm: 2 }}
|
||||
marginTop={{ base: 6, sm: 0 }}
|
||||
gridColumn={{ base: '1 / 3', md: 2 }}
|
||||
marginTop={{ base: 6, md: 0 }}
|
||||
>
|
||||
<Flex flexWrap="wrap" gap={ 6 }>
|
||||
<Flex width={{ base: '100%', sm: 'auto' }}>
|
||||
<Flex width={{ base: '100%', md: 'auto' }}>
|
||||
<MarketplaceAppModalLink
|
||||
id={ data.id }
|
||||
url={ url }
|
||||
@@ -185,126 +175,115 @@ const MarketplaceAppModal = ({
|
||||
<IconSvg name="star_outline" w={ 5 } h={ 5 } color={ starOutlineIconColor }/> }
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
{ securityReport && (
|
||||
<Flex alignItems="center" gap={ 3 }>
|
||||
<AppSecurityReport
|
||||
id={ id }
|
||||
securityReport={ securityReport }
|
||||
showContractList={ showAnalyzedContracts }
|
||||
source="App modal"
|
||||
/>
|
||||
<ContractListButton
|
||||
onClick={ showAllContracts }
|
||||
variant={ ContractListButtonVariants.ALL_CONTRACTS }
|
||||
>
|
||||
{ securityReport.overallInfo.totalContractsNumber }
|
||||
</ContractListButton>
|
||||
<ContractListButton
|
||||
onClick={ showVerifiedContracts }
|
||||
variant={ ContractListButtonVariants.VERIFIED_CONTRACTS }
|
||||
>
|
||||
{ securityReport.overallInfo.verifiedNumber }
|
||||
</ContractListButton>
|
||||
</Flex>
|
||||
) }
|
||||
</Flex>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<ModalCloseButton/>
|
||||
|
||||
<ModalBody>
|
||||
<Heading
|
||||
as="h3"
|
||||
fontSize="2xl"
|
||||
marginBottom={ 4 }
|
||||
>
|
||||
Overview
|
||||
</Heading>
|
||||
|
||||
<Box marginBottom={ 2 }>
|
||||
{ categories.map((category) => (
|
||||
<Tag
|
||||
colorScheme="blue"
|
||||
marginRight={ 2 }
|
||||
marginBottom={ 2 }
|
||||
key={ category }
|
||||
>
|
||||
{ category }
|
||||
</Tag>
|
||||
)) }
|
||||
</Box>
|
||||
|
||||
<ModalBody mb={ 6 }>
|
||||
{ securityReport && (
|
||||
<Flex
|
||||
direction={{ base: 'column', md: 'row' }}
|
||||
justifyContent={{ base: 'flex-start', md: 'space-between' }}
|
||||
gap={ 3 }
|
||||
fontSize="sm"
|
||||
mb={ 6 }
|
||||
>
|
||||
<Flex alignItems="center" gap={ 2 } flexWrap="wrap">
|
||||
<IconSvg name="contracts_verified" boxSize={ 5 } color="green.500"/>
|
||||
<Text>Verified contracts</Text>
|
||||
<Text fontWeight="500">
|
||||
{ securityReport?.overallInfo.verifiedNumber ?? 0 } of { securityReport?.overallInfo.totalContractsNumber ?? 0 }
|
||||
</Text>
|
||||
<Link onClick={ showAllContracts } ml={ 1 }>
|
||||
View all contracts
|
||||
</Link>
|
||||
</Flex>
|
||||
<Flex alignItems="center" gap={ 2 }>
|
||||
<Text>Security level</Text>
|
||||
<AppSecurityReport
|
||||
id={ id }
|
||||
securityReport={ securityReport }
|
||||
showContractList={ showContractList }
|
||||
source="App modal"
|
||||
popoverPlacement={ isMobile ? 'bottom-start' : 'left' }
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
) }
|
||||
<Text>{ description }</Text>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter
|
||||
display="flex"
|
||||
flexDirection={{ base: 'column', sm: 'row' }}
|
||||
alignItems={{ base: 'flex-start', sm: 'center' }}
|
||||
flexDirection={{ base: 'column', md: 'row' }}
|
||||
justifyContent={{ base: 'flex-start', md: 'space-between' }}
|
||||
alignItems={{ base: 'flex-start', md: 'center' }}
|
||||
gap={ 3 }
|
||||
>
|
||||
{ site && (
|
||||
<Link
|
||||
isExternal
|
||||
href={ site }
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
paddingRight={{ sm: 2 }}
|
||||
marginBottom={{ base: 3, sm: 0 }}
|
||||
maxW="100%"
|
||||
overflow="hidden"
|
||||
>
|
||||
<IconSvg
|
||||
name="link"
|
||||
display="inline"
|
||||
verticalAlign="baseline"
|
||||
boxSize="18px"
|
||||
marginRight={ 2 }
|
||||
/>
|
||||
|
||||
<Text
|
||||
color="inherit"
|
||||
whiteSpace="nowrap"
|
||||
overflow="hidden"
|
||||
textOverflow="ellipsis"
|
||||
<Flex gap={ 2 }>
|
||||
{ categories.map((category) => (
|
||||
<Tag
|
||||
colorScheme="blue"
|
||||
key={ category }
|
||||
>
|
||||
{ getHostname(site) }
|
||||
</Text>
|
||||
</Link>
|
||||
) }
|
||||
{ category }
|
||||
</Tag>
|
||||
)) }
|
||||
</Flex>
|
||||
|
||||
{ socialLinks.length > 0 && (
|
||||
<List
|
||||
marginLeft={{ sm: 'auto' }}
|
||||
display="grid"
|
||||
gridAutoFlow="column"
|
||||
columnGap={ 2 }
|
||||
>
|
||||
{ socialLinks.map(({ icon, url }) => (
|
||||
<Link
|
||||
aria-label={ `Link to ${ url }` }
|
||||
title={ url }
|
||||
key={ url }
|
||||
href={ url }
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
isExternal
|
||||
w={ 10 }
|
||||
h={ 10 }
|
||||
<Flex alignItems="center" gap={ 3 }>
|
||||
{ site && (
|
||||
<Link
|
||||
isExternal
|
||||
href={ site }
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
fontSize="sm"
|
||||
>
|
||||
<IconSvg
|
||||
name="link"
|
||||
display="inline"
|
||||
verticalAlign="baseline"
|
||||
boxSize="18px"
|
||||
marginRight={ 2 }
|
||||
/>
|
||||
|
||||
<Text
|
||||
color="inherit"
|
||||
whiteSpace="nowrap"
|
||||
overflow="hidden"
|
||||
textOverflow="ellipsis"
|
||||
>
|
||||
<IconSvg
|
||||
name={ icon }
|
||||
w="20px"
|
||||
h="20px"
|
||||
display="block"
|
||||
color="text_secondary"
|
||||
/>
|
||||
</Link>
|
||||
)) }
|
||||
</List>
|
||||
) }
|
||||
{ getHostname(site) }
|
||||
</Text>
|
||||
</Link>
|
||||
) }
|
||||
|
||||
{ socialLinks.map(({ icon, url }) => (
|
||||
<Link
|
||||
aria-label={ `Link to ${ url }` }
|
||||
title={ url }
|
||||
key={ url }
|
||||
href={ url }
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
isExternal
|
||||
w={ 5 }
|
||||
h={ 5 }
|
||||
>
|
||||
<IconSvg
|
||||
name={ icon }
|
||||
w="20px"
|
||||
h="20px"
|
||||
display="block"
|
||||
color="text_secondary"
|
||||
/>
|
||||
</Link>
|
||||
)) }
|
||||
</Flex>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { chakra, Flex, Tooltip, Skeleton, useBoolean } from '@chakra-ui/react';
|
||||
import { chakra, Flex, Tooltip, Skeleton } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import type { MarketplaceAppOverview, MarketplaceAppSecurityReport } from 'types/client/marketplace';
|
||||
import { ContractListTypes } from 'types/client/marketplace';
|
||||
import type { MarketplaceAppOverview, MarketplaceAppSecurityReport, ContractListTypes } from 'types/client/marketplace';
|
||||
|
||||
import { route } from 'nextjs-routes';
|
||||
|
||||
@@ -27,7 +26,7 @@ type Props = {
|
||||
}
|
||||
|
||||
const MarketplaceAppTopBar = ({ data, isLoading, securityReport }: Props) => {
|
||||
const [ showContractList, setShowContractList ] = useBoolean(false);
|
||||
const [ contractListType, setContractListType ] = React.useState<ContractListTypes>();
|
||||
const appProps = useAppContext();
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
@@ -44,6 +43,9 @@ const MarketplaceAppTopBar = ({ data, isLoading, securityReport }: Props) => {
|
||||
} catch (err) {}
|
||||
}
|
||||
|
||||
const showContractList = React.useCallback((id: string, type: ContractListTypes) => setContractListType(type), []);
|
||||
const hideContractList = React.useCallback(() => setContractListType(undefined), []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flex alignItems="center" flexWrap="wrap" mb={{ base: 3, md: 2 }} rowGap={ 3 } columnGap={ 2 }>
|
||||
@@ -74,7 +76,7 @@ const MarketplaceAppTopBar = ({ data, isLoading, securityReport }: Props) => {
|
||||
<AppSecurityReport
|
||||
id={ data?.id || '' }
|
||||
securityReport={ securityReport }
|
||||
showContractList={ setShowContractList.on }
|
||||
showContractList={ showContractList }
|
||||
isLoading={ isLoading }
|
||||
onlyIcon={ isMobile }
|
||||
source="App page"
|
||||
@@ -87,11 +89,11 @@ const MarketplaceAppTopBar = ({ data, isLoading, securityReport }: Props) => {
|
||||
</Flex>
|
||||
) }
|
||||
</Flex>
|
||||
{ showContractList && (
|
||||
{ contractListType && (
|
||||
<ContractListModal
|
||||
type={ ContractListTypes.ANALYZED }
|
||||
type={ contractListType }
|
||||
contracts={ securityReport?.contractsData }
|
||||
onClose={ setShowContractList.off }
|
||||
onClose={ hideContractList }
|
||||
/>
|
||||
) }
|
||||
</>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Grid } from '@chakra-ui/react';
|
||||
import React, { useCallback } from 'react';
|
||||
import type { MouseEvent } from 'react';
|
||||
|
||||
import type { MarketplaceAppPreview } from 'types/client/marketplace';
|
||||
import type { MarketplaceAppWithSecurityReport, ContractListTypes } from 'types/client/marketplace';
|
||||
|
||||
import * as mixpanel from 'lib/mixpanel/index';
|
||||
|
||||
@@ -10,16 +10,17 @@ import EmptySearchResult from './EmptySearchResult';
|
||||
import MarketplaceAppCard from './MarketplaceAppCard';
|
||||
|
||||
type Props = {
|
||||
apps: Array<MarketplaceAppPreview>;
|
||||
apps: Array<MarketplaceAppWithSecurityReport>;
|
||||
showAppInfo: (id: string) => void;
|
||||
favoriteApps: Array<string>;
|
||||
onFavoriteClick: (id: string, isFavorite: boolean, source: 'Discovery view') => void;
|
||||
isLoading: boolean;
|
||||
selectedCategoryId?: string;
|
||||
onAppClick: (event: MouseEvent, id: string) => void;
|
||||
showContractList: (id: string, type: ContractListTypes) => void;
|
||||
}
|
||||
|
||||
const MarketplaceList = ({ apps, showAppInfo, favoriteApps, onFavoriteClick, isLoading, selectedCategoryId, onAppClick }: Props) => {
|
||||
const MarketplaceList = ({ apps, showAppInfo, favoriteApps, onFavoriteClick, isLoading, selectedCategoryId, onAppClick, showContractList }: Props) => {
|
||||
const handleInfoClick = useCallback((id: string) => {
|
||||
mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'More button', Info: id, Source: 'Discovery view' });
|
||||
showAppInfo(id);
|
||||
@@ -32,11 +33,11 @@ const MarketplaceList = ({ apps, showAppInfo, favoriteApps, onFavoriteClick, isL
|
||||
return apps.length > 0 ? (
|
||||
<Grid
|
||||
templateColumns={{
|
||||
sm: 'repeat(auto-fill, minmax(178px, 1fr))',
|
||||
md: 'repeat(auto-fill, minmax(230px, 1fr))',
|
||||
lg: 'repeat(auto-fill, minmax(260px, 1fr))',
|
||||
}}
|
||||
autoRows="1fr"
|
||||
gap={{ base: '16px', sm: '24px' }}
|
||||
gap={{ base: '16px', md: '24px' }}
|
||||
>
|
||||
{ apps.map((app, index) => (
|
||||
<MarketplaceAppCard
|
||||
@@ -55,6 +56,8 @@ const MarketplaceList = ({ apps, showAppInfo, favoriteApps, onFavoriteClick, isL
|
||||
isLoading={ isLoading }
|
||||
internalWallet={ app.internalWallet }
|
||||
onAppClick={ onAppClick }
|
||||
securityReport={ app.securityReport }
|
||||
showContractList={ showContractList }
|
||||
/>
|
||||
)) }
|
||||
</Grid>
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
import { Hide, Show } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
import type { MouseEvent } from 'react';
|
||||
|
||||
import type { MarketplaceAppWithSecurityReport, ContractListTypes } from 'types/client/marketplace';
|
||||
|
||||
import DataListDisplay from 'ui/shared/DataListDisplay';
|
||||
|
||||
import EmptySearchResult from './EmptySearchResult';
|
||||
import ListItem from './MarketplaceListWithScores/ListItem';
|
||||
import Table from './MarketplaceListWithScores/Table';
|
||||
|
||||
interface Props {
|
||||
apps: Array<MarketplaceAppWithSecurityReport>;
|
||||
showAppInfo: (id: string) => void;
|
||||
favoriteApps: Array<string>;
|
||||
onFavoriteClick: (id: string, isFavorite: boolean, source: 'Security view') => void;
|
||||
isLoading: boolean;
|
||||
selectedCategoryId?: string;
|
||||
onAppClick: (event: MouseEvent, id: string) => void;
|
||||
showContractList: (id: string, type: ContractListTypes) => void;
|
||||
}
|
||||
|
||||
const MarketplaceListWithScores = ({
|
||||
apps,
|
||||
showAppInfo,
|
||||
favoriteApps,
|
||||
onFavoriteClick,
|
||||
isLoading,
|
||||
selectedCategoryId,
|
||||
onAppClick,
|
||||
showContractList,
|
||||
}: Props) => {
|
||||
|
||||
const displayedApps = React.useMemo(() => [ ...apps ].sort((a, b) => {
|
||||
if (!a.securityReport) {
|
||||
return 1;
|
||||
} else if (!b.securityReport) {
|
||||
return -1;
|
||||
}
|
||||
return b.securityReport.overallInfo.securityScore - a.securityReport.overallInfo.securityScore;
|
||||
}), [ apps ]);
|
||||
|
||||
const content = apps.length > 0 ? (
|
||||
<>
|
||||
<Show below="lg" ssr={ false }>
|
||||
{ displayedApps.map((app, index) => (
|
||||
<ListItem
|
||||
key={ app.id + (isLoading ? index : '') }
|
||||
app={ app }
|
||||
onInfoClick={ showAppInfo }
|
||||
isFavorite={ favoriteApps.includes(app.id) }
|
||||
onFavoriteClick={ onFavoriteClick }
|
||||
isLoading={ isLoading }
|
||||
onAppClick={ onAppClick }
|
||||
showContractList={ showContractList }
|
||||
/>
|
||||
)) }
|
||||
</Show>
|
||||
<Hide below="lg" ssr={ false }>
|
||||
<Table
|
||||
apps={ displayedApps }
|
||||
isLoading={ isLoading }
|
||||
onAppClick={ onAppClick }
|
||||
favoriteApps={ favoriteApps }
|
||||
onFavoriteClick={ onFavoriteClick }
|
||||
onInfoClick={ showAppInfo }
|
||||
showContractList={ showContractList }
|
||||
/>
|
||||
</Hide>
|
||||
</>
|
||||
) : null;
|
||||
|
||||
return apps.length > 0 ? (
|
||||
<DataListDisplay
|
||||
isError={ false }
|
||||
items={ apps }
|
||||
emptyText="No apps found."
|
||||
content={ content }
|
||||
/>
|
||||
) : (
|
||||
<EmptySearchResult selectedCategoryId={ selectedCategoryId } favoriteApps={ favoriteApps }/>
|
||||
);
|
||||
};
|
||||
|
||||
export default MarketplaceListWithScores;
|
||||
@@ -1,74 +0,0 @@
|
||||
import { Flex, Skeleton, LinkBox, Image, useColorModeValue } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
import type { MouseEvent } from 'react';
|
||||
|
||||
import type { MarketplaceAppPreview } from 'types/client/marketplace';
|
||||
|
||||
import MarketplaceAppCardLink from '../MarketplaceAppCardLink';
|
||||
import MarketplaceAppIntegrationIcon from '../MarketplaceAppIntegrationIcon';
|
||||
|
||||
interface Props {
|
||||
app: MarketplaceAppPreview;
|
||||
isLoading: boolean | undefined;
|
||||
onAppClick: (event: MouseEvent, id: string) => void;
|
||||
isLarge?: boolean;
|
||||
}
|
||||
|
||||
const AppLink = ({ app, isLoading, onAppClick, isLarge = false }: Props) => {
|
||||
const { id, url, external, title, logo, logoDarkMode, internalWallet, categories } = app;
|
||||
|
||||
const logoUrl = useColorModeValue(logo, logoDarkMode || logo);
|
||||
|
||||
const categoriesLabel = categories.join(', ');
|
||||
|
||||
return (
|
||||
<LinkBox display="flex" height="100%" width="100%" role="group" alignItems="center" mb={ isLarge ? 0 : 4 }>
|
||||
<Skeleton
|
||||
isLoaded={ !isLoading }
|
||||
w={ isLarge ? '56px' : '48px' }
|
||||
h={ isLarge ? '56px' : '48px' }
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
mr={ isLarge ? 3 : 4 }
|
||||
flexShrink={ 0 }
|
||||
>
|
||||
<Image
|
||||
src={ isLoading ? undefined : logoUrl }
|
||||
alt={ `${ title } app icon` }
|
||||
borderRadius="8px"
|
||||
/>
|
||||
</Skeleton>
|
||||
|
||||
<Flex direction="column">
|
||||
<Skeleton
|
||||
isLoaded={ !isLoading }
|
||||
marginBottom={ 0 }
|
||||
fontSize="sm"
|
||||
fontWeight="semibold"
|
||||
fontFamily="heading"
|
||||
display="inline-block"
|
||||
mb={ 1 }
|
||||
>
|
||||
<MarketplaceAppCardLink
|
||||
id={ id }
|
||||
url={ url }
|
||||
external={ external }
|
||||
title={ title }
|
||||
onClick={ onAppClick }
|
||||
/>
|
||||
<MarketplaceAppIntegrationIcon external={ external } internalWallet={ internalWallet }/>
|
||||
</Skeleton>
|
||||
<Skeleton
|
||||
isLoaded={ !isLoading }
|
||||
color="text_secondary"
|
||||
fontSize={ isLarge ? 'sm' : 'xs' }
|
||||
>
|
||||
<span>{ categoriesLabel }</span>
|
||||
</Skeleton>
|
||||
</Flex>
|
||||
</LinkBox>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppLink;
|
||||
@@ -1,125 +0,0 @@
|
||||
import { Flex, IconButton, Text } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
import type { MouseEvent } from 'react';
|
||||
|
||||
import type { MarketplaceAppWithSecurityReport } from 'types/client/marketplace';
|
||||
import { ContractListTypes } from 'types/client/marketplace';
|
||||
|
||||
import * as mixpanel from 'lib/mixpanel/index';
|
||||
import IconSvg from 'ui/shared/IconSvg';
|
||||
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
|
||||
|
||||
import AppSecurityReport from '../AppSecurityReport';
|
||||
import ContractListButton, { ContractListButtonVariants } from '../ContractListButton';
|
||||
import AppLink from './AppLink';
|
||||
import MoreInfoButton from './MoreInfoButton';
|
||||
|
||||
type Props = {
|
||||
app: MarketplaceAppWithSecurityReport;
|
||||
onInfoClick: (id: string) => void;
|
||||
isFavorite: boolean;
|
||||
onFavoriteClick: (id: string, isFavorite: boolean, source: 'Security view') => void;
|
||||
isLoading: boolean;
|
||||
onAppClick: (event: MouseEvent, id: string) => void;
|
||||
showContractList: (id: string, type: ContractListTypes) => void;
|
||||
}
|
||||
|
||||
const ListItem = ({ app, onInfoClick, isFavorite, onFavoriteClick, isLoading, onAppClick, showContractList }: Props) => {
|
||||
const { id, securityReport } = app;
|
||||
|
||||
const handleInfoClick = React.useCallback((event: MouseEvent) => {
|
||||
event.preventDefault();
|
||||
mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'More button', Info: id, Source: 'Security view' });
|
||||
onInfoClick(id);
|
||||
}, [ onInfoClick, id ]);
|
||||
|
||||
const handleFavoriteClick = React.useCallback(() => {
|
||||
onFavoriteClick(id, isFavorite, 'Security view');
|
||||
}, [ onFavoriteClick, id, isFavorite ]);
|
||||
|
||||
const showAllContracts = React.useCallback(() => {
|
||||
mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'Total contracts', Info: id, Source: 'Security view' });
|
||||
showContractList(id, ContractListTypes.ALL);
|
||||
}, [ showContractList, id ]);
|
||||
|
||||
const showVerifiedContracts = React.useCallback(() => {
|
||||
mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'Verified contracts', Info: id, Source: 'Security view' });
|
||||
showContractList(id, ContractListTypes.VERIFIED);
|
||||
}, [ showContractList, id ]);
|
||||
|
||||
const showAnalyzedContracts = React.useCallback(() => {
|
||||
showContractList(id, ContractListTypes.ANALYZED);
|
||||
}, [ showContractList, id ]);
|
||||
|
||||
return (
|
||||
<ListItemMobile
|
||||
rowGap={ 3 }
|
||||
py={ 3 }
|
||||
_first={{ borderTop: 'none', paddingTop: 0 }}
|
||||
_last={{ borderBottom: 'none', paddingBottom: 0 }}
|
||||
>
|
||||
<Flex
|
||||
direction="column"
|
||||
justifyContent="stretch"
|
||||
padding={ 3 }
|
||||
width="100%"
|
||||
>
|
||||
<Flex position="relative">
|
||||
<AppLink app={ app } isLoading={ isLoading } onAppClick={ onAppClick }/>
|
||||
{ !isLoading && (
|
||||
<IconButton
|
||||
position="absolute"
|
||||
right={ -1 }
|
||||
top={ -1 }
|
||||
aria-label="Mark as favorite"
|
||||
title="Mark as favorite"
|
||||
variant="ghost"
|
||||
colorScheme="gray"
|
||||
w={ 9 }
|
||||
h={ 8 }
|
||||
onClick={ handleFavoriteClick }
|
||||
icon={ isFavorite ?
|
||||
<IconSvg name="star_filled" w={ 5 } h={ 5 } color="yellow.400"/> :
|
||||
<IconSvg name="star_outline" w={ 5 } h={ 5 } color="gray.400"/>
|
||||
}
|
||||
/>
|
||||
) }
|
||||
</Flex>
|
||||
<Flex alignItems="center">
|
||||
<Flex flex={ 1 } gap={ 3 } alignItems="center">
|
||||
{ (securityReport || isLoading) ? (
|
||||
<>
|
||||
<AppSecurityReport
|
||||
id={ id }
|
||||
isLoading={ isLoading }
|
||||
securityReport={ securityReport }
|
||||
showContractList={ showAnalyzedContracts }
|
||||
source="Security view"
|
||||
/>
|
||||
<ContractListButton
|
||||
onClick={ showAllContracts }
|
||||
variant={ ContractListButtonVariants.ALL_CONTRACTS }
|
||||
isLoading={ isLoading }
|
||||
>
|
||||
{ securityReport?.overallInfo.totalContractsNumber ?? 0 }
|
||||
</ContractListButton>
|
||||
<ContractListButton
|
||||
onClick={ showVerifiedContracts }
|
||||
variant={ ContractListButtonVariants.VERIFIED_CONTRACTS }
|
||||
isLoading={ isLoading }
|
||||
>
|
||||
{ securityReport?.overallInfo.verifiedNumber ?? 0 }
|
||||
</ContractListButton>
|
||||
</>
|
||||
) : (
|
||||
<Text variant="secondary" fontSize="sm" fontWeight={ 500 }>Data will be available soon</Text>
|
||||
) }
|
||||
</Flex>
|
||||
<MoreInfoButton onClick={ handleInfoClick } isLoading={ isLoading }/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</ListItemMobile>
|
||||
);
|
||||
};
|
||||
|
||||
export default ListItem;
|
||||
@@ -1,29 +0,0 @@
|
||||
import { Link, Skeleton } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
import type { MouseEvent } from 'react';
|
||||
|
||||
interface Props {
|
||||
onClick: (event: MouseEvent) => void;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
const MoreInfoButton = ({ onClick, isLoading }: Props) => (
|
||||
<Skeleton
|
||||
isLoaded={ !isLoading }
|
||||
display="inline-flex"
|
||||
alignItems="center"
|
||||
height="30px"
|
||||
borderRadius="base"
|
||||
>
|
||||
<Link
|
||||
fontSize="sm"
|
||||
onClick={ onClick }
|
||||
fontWeight="500"
|
||||
display="inline-flex"
|
||||
>
|
||||
More info
|
||||
</Link>
|
||||
</Skeleton>
|
||||
);
|
||||
|
||||
export default MoreInfoButton;
|
||||
@@ -1,52 +0,0 @@
|
||||
import { Table as ChakraTable, Tbody, Th, Tr } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
import type { MouseEvent } from 'react';
|
||||
|
||||
import type { MarketplaceAppWithSecurityReport, ContractListTypes } from 'types/client/marketplace';
|
||||
|
||||
import { default as Thead } from 'ui/shared/TheadSticky';
|
||||
|
||||
import TableItem from './TableItem';
|
||||
|
||||
type Props = {
|
||||
apps: Array<MarketplaceAppWithSecurityReport>;
|
||||
isLoading?: boolean;
|
||||
favoriteApps: Array<string>;
|
||||
onFavoriteClick: (id: string, isFavorite: boolean, source: 'Security view') => void;
|
||||
onAppClick: (event: MouseEvent, id: string) => void;
|
||||
onInfoClick: (id: string) => void;
|
||||
showContractList: (id: string, type: ContractListTypes) => void;
|
||||
}
|
||||
|
||||
const Table = ({ apps, isLoading, favoriteApps, onFavoriteClick, onAppClick, onInfoClick, showContractList }: Props) => {
|
||||
return (
|
||||
<ChakraTable>
|
||||
<Thead top={ 0 }>
|
||||
<Tr>
|
||||
<Th w="5%"></Th>
|
||||
<Th w="40%">App</Th>
|
||||
<Th w="15%">Contracts score</Th>
|
||||
<Th w="10%">Total</Th>
|
||||
<Th w="10%">Verified</Th>
|
||||
<Th w="20%"></Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{ apps.map((app, index) => (
|
||||
<TableItem
|
||||
key={ app.id + (isLoading ? index : '') }
|
||||
app={ app }
|
||||
isLoading={ isLoading }
|
||||
isFavorite={ favoriteApps.includes(app.id) }
|
||||
onFavoriteClick={ onFavoriteClick }
|
||||
onAppClick={ onAppClick }
|
||||
onInfoClick={ onInfoClick }
|
||||
showContractList={ showContractList }
|
||||
/>
|
||||
)) }
|
||||
</Tbody>
|
||||
</ChakraTable>
|
||||
);
|
||||
};
|
||||
|
||||
export default Table;
|
||||
@@ -1,126 +0,0 @@
|
||||
import { Td, Tr, IconButton, Skeleton, Text } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
import type { MouseEvent } from 'react';
|
||||
|
||||
import type { MarketplaceAppWithSecurityReport } from 'types/client/marketplace';
|
||||
import { ContractListTypes } from 'types/client/marketplace';
|
||||
|
||||
import * as mixpanel from 'lib/mixpanel/index';
|
||||
import IconSvg from 'ui/shared/IconSvg';
|
||||
|
||||
import AppSecurityReport from '../AppSecurityReport';
|
||||
import ContractListButton, { ContractListButtonVariants } from '../ContractListButton';
|
||||
import AppLink from './AppLink';
|
||||
import MoreInfoButton from './MoreInfoButton';
|
||||
|
||||
type Props = {
|
||||
app: MarketplaceAppWithSecurityReport;
|
||||
isLoading?: boolean;
|
||||
isFavorite: boolean;
|
||||
onFavoriteClick: (id: string, isFavorite: boolean, source: 'Security view') => void;
|
||||
onAppClick: (event: MouseEvent, id: string) => void;
|
||||
onInfoClick: (id: string) => void;
|
||||
showContractList: (id: string, type: ContractListTypes) => void;
|
||||
}
|
||||
|
||||
const TableItem = ({
|
||||
app,
|
||||
isLoading,
|
||||
isFavorite,
|
||||
onFavoriteClick,
|
||||
onAppClick,
|
||||
onInfoClick,
|
||||
showContractList,
|
||||
}: Props) => {
|
||||
|
||||
const { id, securityReport } = app;
|
||||
|
||||
const handleInfoClick = React.useCallback((event: MouseEvent) => {
|
||||
event.preventDefault();
|
||||
mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'More button', Info: id, Source: 'Security view' });
|
||||
onInfoClick(id);
|
||||
}, [ onInfoClick, id ]);
|
||||
|
||||
const handleFavoriteClick = React.useCallback(() => {
|
||||
onFavoriteClick(id, isFavorite, 'Security view');
|
||||
}, [ onFavoriteClick, id, isFavorite ]);
|
||||
|
||||
const showAllContracts = React.useCallback(() => {
|
||||
mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'Total contracts', Info: id, Source: 'Security view' });
|
||||
showContractList(id, ContractListTypes.ALL);
|
||||
}, [ showContractList, id ]);
|
||||
|
||||
const showVerifiedContracts = React.useCallback(() => {
|
||||
mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'Verified contracts', Info: id, Source: 'Security view' });
|
||||
showContractList(id, ContractListTypes.VERIFIED);
|
||||
}, [ showContractList, id ]);
|
||||
|
||||
const showAnalyzedContracts = React.useCallback(() => {
|
||||
showContractList(id, ContractListTypes.ANALYZED);
|
||||
}, [ showContractList, id ]);
|
||||
|
||||
return (
|
||||
<Tr>
|
||||
<Td verticalAlign="middle" px={ 2 }>
|
||||
<Skeleton isLoaded={ !isLoading }>
|
||||
<IconButton
|
||||
aria-label="Mark as favorite"
|
||||
title="Mark as favorite"
|
||||
variant="ghost"
|
||||
colorScheme="gray"
|
||||
w={ 9 }
|
||||
h={ 8 }
|
||||
onClick={ handleFavoriteClick }
|
||||
icon={ isFavorite ?
|
||||
<IconSvg name="star_filled" w={ 5 } h={ 5 } color="yellow.400"/> :
|
||||
<IconSvg name="star_outline" w={ 5 } h={ 5 } color="gray.400"/>
|
||||
}
|
||||
/>
|
||||
</Skeleton>
|
||||
</Td>
|
||||
<Td verticalAlign="middle">
|
||||
<AppLink app={ app } isLoading={ isLoading } onAppClick={ onAppClick } isLarge/>
|
||||
</Td>
|
||||
{ (securityReport || isLoading) ? (
|
||||
<>
|
||||
<Td verticalAlign="middle">
|
||||
<AppSecurityReport
|
||||
id={ id }
|
||||
securityReport={ securityReport }
|
||||
showContractList={ showAnalyzedContracts }
|
||||
isLoading={ isLoading }
|
||||
source="Security view"
|
||||
/>
|
||||
</Td>
|
||||
<Td verticalAlign="middle">
|
||||
<ContractListButton
|
||||
onClick={ showAllContracts }
|
||||
variant={ ContractListButtonVariants.ALL_CONTRACTS }
|
||||
isLoading={ isLoading }
|
||||
>
|
||||
{ securityReport?.overallInfo.totalContractsNumber ?? 0 }
|
||||
</ContractListButton>
|
||||
</Td>
|
||||
<Td verticalAlign="middle">
|
||||
<ContractListButton
|
||||
onClick={ showVerifiedContracts }
|
||||
variant={ ContractListButtonVariants.VERIFIED_CONTRACTS }
|
||||
isLoading={ isLoading }
|
||||
>
|
||||
{ securityReport?.overallInfo.verifiedNumber ?? 0 }
|
||||
</ContractListButton>
|
||||
</Td>
|
||||
</>
|
||||
) : (
|
||||
<Td verticalAlign="middle" colSpan={ 3 }>
|
||||
<Text variant="secondary" fontSize="sm" fontWeight={ 500 }>Data will be available soon</Text>
|
||||
</Td>
|
||||
) }
|
||||
<Td verticalAlign="middle" isNumeric>
|
||||
<MoreInfoButton onClick={ handleInfoClick } isLoading={ isLoading }/>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
};
|
||||
|
||||
export default TableItem;
|
||||
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 42 KiB |
@@ -3,7 +3,7 @@ import { useRouter } from 'next/router';
|
||||
import React from 'react';
|
||||
|
||||
import type { ContractListTypes } from 'types/client/marketplace';
|
||||
import { MarketplaceCategory, MarketplaceDisplayType } from 'types/client/marketplace';
|
||||
import { MarketplaceCategory } from 'types/client/marketplace';
|
||||
|
||||
import useDebounce from 'lib/hooks/useDebounce';
|
||||
import * as mixpanel from 'lib/mixpanel/index';
|
||||
@@ -26,15 +26,9 @@ export default function useMarketplace() {
|
||||
const router = useRouter();
|
||||
const defaultCategoryId = getQueryParamString(router.query.category);
|
||||
const defaultFilterQuery = getQueryParamString(router.query.filter);
|
||||
const defaultDisplayType = getQueryParamString(router.query.tab);
|
||||
|
||||
const [ selectedAppId, setSelectedAppId ] = React.useState<string | null>(null);
|
||||
const [ selectedCategoryId, setSelectedCategoryId ] = React.useState<string>(MarketplaceCategory.ALL);
|
||||
const [ selectedDisplayType, setSelectedDisplayType ] = React.useState<string>(
|
||||
Object.values(MarketplaceDisplayType).includes(defaultDisplayType as MarketplaceDisplayType) ?
|
||||
defaultDisplayType :
|
||||
MarketplaceDisplayType.DEFAULT,
|
||||
);
|
||||
const [ filterQuery, setFilterQuery ] = React.useState(defaultFilterQuery);
|
||||
const [ favoriteApps, setFavoriteApps ] = React.useState<Array<string>>([]);
|
||||
const [ isFavoriteAppsLoaded, setIsFavoriteAppsLoaded ] = React.useState<boolean>(false);
|
||||
@@ -91,12 +85,8 @@ export default function useMarketplace() {
|
||||
setSelectedCategoryId(newCategory);
|
||||
}, []);
|
||||
|
||||
const handleDisplayTypeChange = React.useCallback((newDisplayType: MarketplaceDisplayType) => {
|
||||
setSelectedDisplayType(newDisplayType);
|
||||
}, []);
|
||||
|
||||
const {
|
||||
isPlaceholderData, isError, error, data, displayedApps,
|
||||
isPlaceholderData, isError, error, data, displayedApps, setSorting,
|
||||
} = useMarketplaceApps(debouncedFilterQuery, selectedCategoryId, favoriteApps, isFavoriteAppsLoaded);
|
||||
const {
|
||||
isPlaceholderData: isCategoriesPlaceholderData, data: categories,
|
||||
@@ -120,7 +110,6 @@ export default function useMarketplace() {
|
||||
const query = _pickBy({
|
||||
category: selectedCategoryId === MarketplaceCategory.ALL ? undefined : selectedCategoryId,
|
||||
filter: debouncedFilterQuery,
|
||||
tab: selectedDisplayType === MarketplaceDisplayType.DEFAULT ? undefined : selectedDisplayType,
|
||||
}, Boolean);
|
||||
|
||||
if (debouncedFilterQuery.length > 0) {
|
||||
@@ -135,7 +124,7 @@ export default function useMarketplace() {
|
||||
// omit router in the deps because router.push() somehow modifies it
|
||||
// and we get infinite re-renders then
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [ debouncedFilterQuery, selectedCategoryId, selectedDisplayType ]);
|
||||
}, [ debouncedFilterQuery, selectedCategoryId ]);
|
||||
|
||||
return React.useMemo(() => ({
|
||||
selectedCategoryId,
|
||||
@@ -160,9 +149,8 @@ export default function useMarketplace() {
|
||||
isCategoriesPlaceholderData,
|
||||
showContractList,
|
||||
contractListModalType,
|
||||
selectedDisplayType,
|
||||
onDisplayTypeChange: handleDisplayTypeChange,
|
||||
hasPreviousStep,
|
||||
setSorting,
|
||||
}), [
|
||||
selectedCategoryId,
|
||||
categories,
|
||||
@@ -184,8 +172,7 @@ export default function useMarketplace() {
|
||||
isCategoriesPlaceholderData,
|
||||
showContractList,
|
||||
contractListModalType,
|
||||
selectedDisplayType,
|
||||
handleDisplayTypeChange,
|
||||
hasPreviousStep,
|
||||
setSorting,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import useFetch from 'lib/hooks/useFetch';
|
||||
import { MARKETPLACE_APP } from 'stubs/marketplace';
|
||||
|
||||
import useSecurityReports from './useSecurityReports';
|
||||
import type { SortValue } from './utils';
|
||||
|
||||
const feature = config.features.marketplace;
|
||||
|
||||
@@ -88,13 +89,22 @@ export default function useMarketplaceApps(
|
||||
enabled: feature.isEnabled && Boolean(snapshotFavoriteApps),
|
||||
});
|
||||
|
||||
const [ sorting, setSorting ] = React.useState<SortValue>();
|
||||
|
||||
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 +112,7 @@ export default function useMarketplaceApps(
|
||||
error,
|
||||
isError,
|
||||
isPlaceholderData: isPlaceholderData || isSecurityReportsPlaceholderData,
|
||||
setSorting,
|
||||
}), [
|
||||
data,
|
||||
displayedApps,
|
||||
@@ -109,5 +120,6 @@ export default function useMarketplaceApps(
|
||||
isError,
|
||||
isPlaceholderData,
|
||||
isSecurityReportsPlaceholderData,
|
||||
setSorting,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,14 @@ import type { NextRouter } from 'next/router';
|
||||
|
||||
import getQueryParamString from 'lib/router/getQueryParamString';
|
||||
import removeQueryParam from 'lib/router/removeQueryParam';
|
||||
import type { TOption } from 'ui/shared/sort/Option';
|
||||
|
||||
export type SortValue = 'security_score';
|
||||
|
||||
export const SORT_OPTIONS: Array<TOption<SortValue>> = [
|
||||
{ title: 'Default', id: undefined },
|
||||
{ title: 'Security score', id: 'security_score' },
|
||||
];
|
||||
|
||||
export function getAppUrl(url: string | undefined, router: NextRouter) {
|
||||
if (!url) {
|
||||
|
||||
@@ -136,9 +136,10 @@ const NameDomainsActionBar = ({
|
||||
|
||||
const sortButton = (
|
||||
<Sort
|
||||
name="name_domains_sorting"
|
||||
defaultValue={ sort }
|
||||
options={ SORT_OPTIONS }
|
||||
sort={ sort }
|
||||
setSort={ onSortChange }
|
||||
onChange={ onSortChange }
|
||||
isLoading={ isInitialLoading }
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import type { EnsLookupSorting } from 'types/api/ens';
|
||||
|
||||
import getNextSortValueShared from 'ui/shared/sort/getNextSortValue';
|
||||
import type { Option } from 'ui/shared/sort/Sort';
|
||||
import type { TOption } from 'ui/shared/sort/Option';
|
||||
|
||||
export type SortField = EnsLookupSorting['sort'];
|
||||
export type Sort = `${ EnsLookupSorting['sort'] }-${ EnsLookupSorting['order'] }`;
|
||||
|
||||
export const SORT_OPTIONS: Array<Option<Sort>> = [
|
||||
export const SORT_OPTIONS: Array<TOption<Sort>> = [
|
||||
{ title: 'Default', id: undefined },
|
||||
{ title: 'Registered on descending', id: 'registration_date-DESC' },
|
||||
{ title: 'Registered on ascending', id: 'registration_date-ASC' },
|
||||
|
||||
@@ -7,13 +7,16 @@ import { test, expect, devices } from 'playwright/lib';
|
||||
import Marketplace from './Marketplace';
|
||||
|
||||
const MARKETPLACE_CONFIG_URL = 'http://localhost/marketplace-config.json';
|
||||
const MARKETPLACE_SECURITY_REPORTS_URL = 'https://marketplace-security-reports.json';
|
||||
|
||||
test.beforeEach(async({ mockConfigResponse, mockEnvs, mockAssetResponse }) => {
|
||||
await mockEnvs([
|
||||
[ 'NEXT_PUBLIC_MARKETPLACE_ENABLED', 'true' ],
|
||||
[ 'NEXT_PUBLIC_MARKETPLACE_CONFIG_URL', MARKETPLACE_CONFIG_URL ],
|
||||
[ 'NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL', MARKETPLACE_SECURITY_REPORTS_URL ],
|
||||
]);
|
||||
await mockConfigResponse('NEXT_PUBLIC_MARKETPLACE_CONFIG_URL', MARKETPLACE_CONFIG_URL, JSON.stringify(appsMock));
|
||||
await mockConfigResponse('NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL', MARKETPLACE_SECURITY_REPORTS_URL, JSON.stringify(securityReportsMock));
|
||||
await Promise.all(appsMock.map(app => mockAssetResponse(app.logo, './playwright/mocks/image_s.jpg')));
|
||||
});
|
||||
|
||||
@@ -46,18 +49,6 @@ test('with banner +@dark-mode', async({ render, mockEnvs, mockConfigResponse })
|
||||
await expect(component).toHaveScreenshot();
|
||||
});
|
||||
|
||||
test('with scores +@dark-mode', async({ render, mockConfigResponse, mockEnvs }) => {
|
||||
const MARKETPLACE_SECURITY_REPORTS_URL = 'https://marketplace-security-reports.json';
|
||||
await mockEnvs([
|
||||
[ 'NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL', MARKETPLACE_SECURITY_REPORTS_URL ],
|
||||
]);
|
||||
await mockConfigResponse('NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL', MARKETPLACE_SECURITY_REPORTS_URL, JSON.stringify(securityReportsMock));
|
||||
const component = await render(<Marketplace/>);
|
||||
await component.getByText('Apps scores').click();
|
||||
|
||||
await expect(component).toHaveScreenshot();
|
||||
});
|
||||
|
||||
// I had a memory error while running tests in GH actions
|
||||
// separate run for mobile tests fixes it
|
||||
test.describe('mobile', () => {
|
||||
@@ -91,16 +82,4 @@ test.describe('mobile', () => {
|
||||
|
||||
await expect(component).toHaveScreenshot();
|
||||
});
|
||||
|
||||
test('with scores', async({ render, mockConfigResponse, mockEnvs }) => {
|
||||
const MARKETPLACE_SECURITY_REPORTS_URL = 'https://marketplace-security-reports.json';
|
||||
await mockEnvs([
|
||||
[ 'NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL', MARKETPLACE_SECURITY_REPORTS_URL ],
|
||||
]);
|
||||
await mockConfigResponse('NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL', MARKETPLACE_SECURITY_REPORTS_URL, JSON.stringify(securityReportsMock));
|
||||
const component = await render(<Marketplace/>);
|
||||
await component.getByText('Apps scores').click();
|
||||
|
||||
await expect(component).toHaveScreenshot();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Box, Menu, MenuButton, MenuItem, MenuList, Flex, IconButton, Skeleton } from '@chakra-ui/react';
|
||||
import { Box, Menu, MenuButton, MenuItem, MenuList, Flex, IconButton } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
import type { MouseEvent } from 'react';
|
||||
|
||||
import { MarketplaceCategory, MarketplaceDisplayType } from 'types/client/marketplace';
|
||||
import { MarketplaceCategory } from 'types/client/marketplace';
|
||||
import type { TabItem } from 'ui/shared/Tabs/types';
|
||||
|
||||
import config from 'configs/app';
|
||||
@@ -13,13 +13,13 @@ 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 MarketplaceListWithScores from 'ui/marketplace/MarketplaceListWithScores';
|
||||
import { SORT_OPTIONS } from 'ui/marketplace/utils';
|
||||
import FilterInput from 'ui/shared/filters/FilterInput';
|
||||
import IconSvg from 'ui/shared/IconSvg';
|
||||
import type { IconName } from 'ui/shared/IconSvg';
|
||||
import LinkExternal from 'ui/shared/links/LinkExternal';
|
||||
import PageTitle from 'ui/shared/Page/PageTitle';
|
||||
import RadioButtonGroup from 'ui/shared/radioButtonGroup/RadioButtonGroup';
|
||||
import Sort from 'ui/shared/sort/Sort';
|
||||
import TabsWithScroll from 'ui/shared/Tabs/TabsWithScroll';
|
||||
|
||||
import useMarketplace from '../marketplace/useMarketplace';
|
||||
@@ -67,9 +67,8 @@ const Marketplace = () => {
|
||||
isCategoriesPlaceholderData,
|
||||
showContractList,
|
||||
contractListModalType,
|
||||
selectedDisplayType,
|
||||
onDisplayTypeChange,
|
||||
hasPreviousStep,
|
||||
setSorting,
|
||||
} = useMarketplace();
|
||||
|
||||
const isMobile = useIsMobile();
|
||||
@@ -186,38 +185,14 @@ 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 && (
|
||||
<Skeleton isLoaded={ !isPlaceholderData }>
|
||||
<RadioButtonGroup<MarketplaceDisplayType>
|
||||
onChange={ onDisplayTypeChange }
|
||||
defaultValue={ selectedDisplayType }
|
||||
name="type"
|
||||
options={ [
|
||||
{
|
||||
title: 'Discovery',
|
||||
value: MarketplaceDisplayType.DEFAULT,
|
||||
icon: 'apps_xs',
|
||||
onlyIcon: false,
|
||||
},
|
||||
{
|
||||
title: 'Apps scores',
|
||||
value: MarketplaceDisplayType.SCORES,
|
||||
icon: 'apps_list',
|
||||
onlyIcon: false,
|
||||
contentAfter: (
|
||||
<IconSvg
|
||||
name={ isMobile ? 'beta_xs' : 'beta' }
|
||||
h={ 3 }
|
||||
w={ isMobile ? 3 : 7 }
|
||||
ml={ 1 }
|
||||
/>
|
||||
),
|
||||
},
|
||||
] }
|
||||
autoWidth
|
||||
/>
|
||||
</Skeleton>
|
||||
<Sort
|
||||
name="dapps_sorting"
|
||||
options={ SORT_OPTIONS }
|
||||
onChange={ setSorting }
|
||||
isLoading={ isPlaceholderData }
|
||||
/>
|
||||
) }
|
||||
<FilterInput
|
||||
initialValue={ filterQuery }
|
||||
@@ -229,28 +204,16 @@ const Marketplace = () => {
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
{ (selectedDisplayType === MarketplaceDisplayType.SCORES && feature.securityReportsUrl) ? (
|
||||
<MarketplaceListWithScores
|
||||
apps={ displayedApps }
|
||||
showAppInfo={ showAppInfo }
|
||||
favoriteApps={ favoriteApps }
|
||||
onFavoriteClick={ onFavoriteClick }
|
||||
isLoading={ isPlaceholderData }
|
||||
selectedCategoryId={ selectedCategoryId }
|
||||
onAppClick={ handleAppClick }
|
||||
showContractList={ showContractList }
|
||||
/>
|
||||
) : (
|
||||
<MarketplaceList
|
||||
apps={ displayedApps }
|
||||
showAppInfo={ showAppInfo }
|
||||
favoriteApps={ favoriteApps }
|
||||
onFavoriteClick={ onFavoriteClick }
|
||||
isLoading={ isPlaceholderData }
|
||||
selectedCategoryId={ selectedCategoryId }
|
||||
onAppClick={ handleAppClick }
|
||||
/>
|
||||
) }
|
||||
<MarketplaceList
|
||||
apps={ displayedApps }
|
||||
showAppInfo={ showAppInfo }
|
||||
favoriteApps={ favoriteApps }
|
||||
onFavoriteClick={ onFavoriteClick }
|
||||
isLoading={ isPlaceholderData }
|
||||
selectedCategoryId={ selectedCategoryId }
|
||||
onAppClick={ handleAppClick }
|
||||
showContractList={ showContractList }
|
||||
/>
|
||||
|
||||
{ (selectedApp && isAppInfoModalOpen) && (
|
||||
<MarketplaceAppModal
|
||||
|
||||
@@ -97,9 +97,10 @@ const Validators = () => {
|
||||
|
||||
const sortButton = (
|
||||
<Sort
|
||||
name="validators_sorting"
|
||||
defaultValue={ sort }
|
||||
options={ SORT_OPTIONS }
|
||||
sort={ sort }
|
||||
setSort={ handleSortChange }
|
||||
onChange={ handleSortChange }
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
@@ -97,9 +97,11 @@ const VerifiedContracts = () => {
|
||||
|
||||
const sortButton = (
|
||||
<Sort
|
||||
name="verified_contracts_sorting"
|
||||
defaultValue={ sort }
|
||||
options={ SORT_OPTIONS }
|
||||
sort={ sort }
|
||||
setSort={ handleSortChange }
|
||||
onChange={ handleSortChange }
|
||||
isLoading={ isPlaceholderData }
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 51 KiB |
@@ -1,7 +1,8 @@
|
||||
import { Button, Spinner, Tooltip, useColorModeValue } from '@chakra-ui/react';
|
||||
import { Button, Spinner, Tooltip, useColorModeValue, chakra } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import useIsMobile from 'lib/hooks/useIsMobile';
|
||||
import usePreventFocusAfterModalClosing from 'lib/hooks/usePreventFocusAfterModalClosing';
|
||||
import IconSvg from 'ui/shared/IconSvg';
|
||||
|
||||
import useScoreLevelAndColor from './useScoreLevelAndColor';
|
||||
@@ -11,21 +12,24 @@ interface Props {
|
||||
isLoading?: boolean;
|
||||
onlyIcon?: boolean;
|
||||
onClick?: () => void;
|
||||
label?: string;
|
||||
label?: string | React.ReactElement;
|
||||
isActive: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const SolidityscanReportButton = (
|
||||
{ score, isLoading, onlyIcon, onClick, label = 'Security score', isActive }: Props,
|
||||
{ score, isLoading, onlyIcon, onClick, label = 'Security score', isActive, className }: Props,
|
||||
ref: React.ForwardedRef<HTMLButtonElement>,
|
||||
) => {
|
||||
const { scoreColor } = useScoreLevelAndColor(score);
|
||||
const colorLoading = useColorModeValue('gray.300', 'gray.600');
|
||||
const isMobile = useIsMobile();
|
||||
const onFocusCapture = usePreventFocusAfterModalClosing();
|
||||
|
||||
return (
|
||||
<Tooltip label={ label } isDisabled={ isMobile } openDelay={ 100 }>
|
||||
<Tooltip label={ label } isDisabled={ isMobile } openDelay={ 100 } textAlign="center">
|
||||
<Button
|
||||
className={ className }
|
||||
ref={ ref }
|
||||
color={ isLoading ? colorLoading : scoreColor }
|
||||
size="sm"
|
||||
@@ -45,6 +49,7 @@ const SolidityscanReportButton = (
|
||||
color: colorLoading,
|
||||
},
|
||||
}}
|
||||
onFocusCapture={ onFocusCapture }
|
||||
>
|
||||
<IconSvg name={ score < 80 ? 'score/score-not-ok' : 'score/score-ok' } boxSize={ 5 }/>
|
||||
{ isLoading && <Spinner size="sm"/> }
|
||||
@@ -54,4 +59,4 @@ const SolidityscanReportButton = (
|
||||
);
|
||||
};
|
||||
|
||||
export default React.forwardRef(SolidityscanReportButton);
|
||||
export default chakra(React.forwardRef(SolidityscanReportButton));
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import {
|
||||
Box,
|
||||
useColorModeValue,
|
||||
Button,
|
||||
Skeleton,
|
||||
chakra,
|
||||
} from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import IconSvg from 'ui/shared/IconSvg';
|
||||
|
||||
type ButtonProps = {
|
||||
isActive: boolean;
|
||||
onClick: () => void;
|
||||
isLoading?: boolean;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const ButtonDesktop = ({ children, isActive, onClick, isLoading, className }: ButtonProps, ref: React.ForwardedRef<HTMLButtonElement>) => {
|
||||
const primaryColor = useColorModeValue('blackAlpha.800', 'whiteAlpha.800');
|
||||
const secondaryColor = useColorModeValue('blackAlpha.600', 'whiteAlpha.600');
|
||||
|
||||
return (
|
||||
<Skeleton isLoaded={ !isLoading }>
|
||||
<Button
|
||||
className={ className }
|
||||
ref={ ref }
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={ onClick }
|
||||
color={ primaryColor }
|
||||
fontWeight="600"
|
||||
borderColor="transparent"
|
||||
px={ 2 }
|
||||
data-selected={ isActive }
|
||||
>
|
||||
<Box
|
||||
as={ isActive ? 'div' : 'span' }
|
||||
color={ isActive ? 'inherit' : secondaryColor }
|
||||
fontWeight="400"
|
||||
mr={ 1 }
|
||||
transition={ isActive ? 'none' : 'inherit' }
|
||||
>Sort by</Box>
|
||||
{ children }
|
||||
<IconSvg
|
||||
name="arrows/east-mini"
|
||||
boxSize={ 5 }
|
||||
ml={ 1 }
|
||||
transform={ isActive ? 'rotate(90deg)' : 'rotate(-90deg)' }
|
||||
/>
|
||||
</Button>
|
||||
</Skeleton>
|
||||
);
|
||||
};
|
||||
|
||||
export default chakra(React.forwardRef(ButtonDesktop));
|
||||
@@ -10,13 +10,14 @@ type Props = {
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
const SortButton = ({ onClick, isActive, className, isLoading }: Props) => {
|
||||
const ButtonMobile = ({ 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(ButtonMobile));
|
||||
@@ -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<Sort extends string> {
|
||||
id: Sort | undefined;
|
||||
title: 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;
|
||||
@@ -1,58 +1,70 @@
|
||||
import {
|
||||
chakra,
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuList,
|
||||
MenuOptionGroup,
|
||||
MenuItemOption,
|
||||
Popover,
|
||||
PopoverTrigger,
|
||||
PopoverContent,
|
||||
PopoverBody,
|
||||
useDisclosure,
|
||||
useRadioGroup,
|
||||
chakra,
|
||||
} from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import SortButton from './SortButton';
|
||||
import useIsMobile from 'lib/hooks/useIsMobile';
|
||||
|
||||
export interface Option<Sort extends string> {
|
||||
title: string;
|
||||
id: Sort | undefined;
|
||||
}
|
||||
import SortButtonDesktop from './ButtonDesktop';
|
||||
import SortButtonMobile from './ButtonMobile';
|
||||
import Option from './Option';
|
||||
import type { TOption } from './Option';
|
||||
|
||||
interface Props<Sort extends string> {
|
||||
options: Array<Option<Sort>>;
|
||||
sort: Sort | undefined;
|
||||
setSort: (value: Sort | undefined) => void;
|
||||
name: string;
|
||||
options: Array<TOption<Sort>>;
|
||||
defaultValue?: Sort;
|
||||
isLoading?: boolean;
|
||||
onChange: (value: Sort | undefined) => void;
|
||||
}
|
||||
|
||||
const Sort = <Sort extends string>({ sort, setSort, options, isLoading }: Props<Sort>) => {
|
||||
const { isOpen, onToggle } = useDisclosure();
|
||||
const Sort = <Sort extends string>({ name, options, isLoading, onChange, defaultValue }: Props<Sort>) => {
|
||||
const isMobile = useIsMobile(false);
|
||||
const { isOpen, onToggle, onClose } = useDisclosure();
|
||||
|
||||
const setSortingFromMenu = React.useCallback((val: string | Array<string>) => {
|
||||
const value = val as Sort | Array<Sort>;
|
||||
setSort(Array.isArray(value) ? value[0] : value);
|
||||
}, [ setSort ]);
|
||||
const handleChange = (value: Sort) => {
|
||||
onChange(value);
|
||||
onClose();
|
||||
};
|
||||
|
||||
const { value, getRootProps, getRadioProps } = useRadioGroup({
|
||||
name,
|
||||
defaultValue,
|
||||
onChange: handleChange,
|
||||
});
|
||||
|
||||
const root = getRootProps();
|
||||
|
||||
return (
|
||||
<Menu>
|
||||
<MenuButton as="div">
|
||||
<SortButton
|
||||
isActive={ isOpen || Boolean(sort) }
|
||||
onClick={ onToggle }
|
||||
isLoading={ isLoading }
|
||||
/>
|
||||
</MenuButton>
|
||||
<MenuList minWidth="240px" zIndex="popover">
|
||||
<MenuOptionGroup value={ sort } title="Sort by" type="radio" onChange={ setSortingFromMenu }>
|
||||
{ options.map((option) => (
|
||||
<MenuItemOption
|
||||
key={ option.id || 'default' }
|
||||
value={ option.id }
|
||||
>
|
||||
{ option.title }
|
||||
</MenuItemOption>
|
||||
)) }
|
||||
</MenuOptionGroup>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
<Popover isOpen={ isOpen } onClose={ onClose } placement="bottom-start" isLazy>
|
||||
<PopoverTrigger>
|
||||
{ isMobile ? (
|
||||
<SortButtonMobile isActive={ isOpen || Boolean(value) } onClick={ onToggle } isLoading={ isLoading }/>
|
||||
) : (
|
||||
<SortButtonDesktop isActive={ isOpen } isLoading={ isLoading } onClick={ onToggle }>
|
||||
{ options.find((option: TOption<Sort>) => option.id === value || (!option.id && !value))?.title }
|
||||
</SortButtonDesktop>
|
||||
) }
|
||||
</PopoverTrigger>
|
||||
<PopoverContent w="fit-content" minW="165px">
|
||||
<PopoverBody { ...root } py={ 2 } px={ 0 } display="flex" flexDir="column">
|
||||
{ options.map((option, index) => {
|
||||
const radio = getRadioProps({ value: option.id });
|
||||
return (
|
||||
<Option key={ index } { ...radio } isChecked={ radio.isChecked || (!option.id && !value) }>
|
||||
{ option.title }
|
||||
</Option>
|
||||
);
|
||||
}) }
|
||||
</PopoverBody>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { Query } from 'nextjs-routes';
|
||||
|
||||
import type { Option } from 'ui/shared/sort/Sort';
|
||||
import type { TOption } from 'ui/shared/sort/Option';
|
||||
|
||||
export default function getSortValueFromQuery<SortValue extends string>(query: Query, sortOptions: Array<Option<SortValue>>) {
|
||||
export default function getSortValueFromQuery<SortValue extends string>(query: Query, sortOptions: Array<TOption<SortValue>>) {
|
||||
if (!query.sort || !query.order) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -45,9 +45,10 @@ const TokensActionBar = ({
|
||||
<HStack spacing={ 3 } mb={ 6 } display={{ base: 'flex', lg: 'none' }}>
|
||||
{ filter }
|
||||
<Sort
|
||||
name="tokens_sorting"
|
||||
defaultValue={ sort }
|
||||
options={ SORT_OPTIONS }
|
||||
setSort={ onSortChange }
|
||||
sort={ sort }
|
||||
onChange={ onSortChange }
|
||||
/>
|
||||
{ searchInput }
|
||||
</HStack>
|
||||
|
||||
@@ -4,9 +4,9 @@ import type { TokensSortingValue } from 'types/api/tokens';
|
||||
import config from 'configs/app';
|
||||
import getFilterValuesFromQuery from 'lib/getFilterValuesFromQuery';
|
||||
import { TOKEN_TYPE_IDS } from 'lib/token/tokenTypes';
|
||||
import type { Option } from 'ui/shared/sort/Sort';
|
||||
import type { TOption } from 'ui/shared/sort/Option';
|
||||
|
||||
export const SORT_OPTIONS: Array<Option<TokensSortingValue>> = [
|
||||
export const SORT_OPTIONS: Array<TOption<TokensSortingValue>> = [
|
||||
{ title: 'Default', id: undefined },
|
||||
{ title: 'Price ascending', id: 'fiat_value-asc' },
|
||||
{ title: 'Price descending', id: 'fiat_value-desc' },
|
||||
|
||||
@@ -30,9 +30,10 @@ const TxsHeaderMobile = ({ filterComponent, sorting, setSorting, paginationProps
|
||||
<HStack>
|
||||
{ filterComponent }
|
||||
<Sort
|
||||
name="transactions_sorting"
|
||||
defaultValue={ sorting }
|
||||
options={ SORT_OPTIONS }
|
||||
setSort={ setSorting }
|
||||
sort={ sorting }
|
||||
onChange={ setSorting }
|
||||
isLoading={ paginationProps.isLoading }
|
||||
/>
|
||||
{ /* api is not implemented */ }
|
||||
|
||||
@@ -5,11 +5,11 @@ import type { TransactionsSortingValue, TxsResponse } from 'types/api/transactio
|
||||
|
||||
import type { ResourceError } from 'lib/api/resources';
|
||||
import * as cookies from 'lib/cookies';
|
||||
import type { Option } from 'ui/shared/sort/Sort';
|
||||
import type { TOption } from 'ui/shared/sort/Option';
|
||||
|
||||
import sortTxs from './sortTxs';
|
||||
|
||||
export const SORT_OPTIONS: Array<Option<TransactionsSortingValue>> = [
|
||||
export const SORT_OPTIONS: Array<TOption<TransactionsSortingValue>> = [
|
||||
{ title: 'Default', id: undefined },
|
||||
{ title: 'Value ascending', id: 'value-asc' },
|
||||
{ title: 'Value descending', id: 'value-desc' },
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { ValidatorsSortingValue, ValidatorsSortingField } from 'types/api/validators';
|
||||
|
||||
import type { Option } from 'ui/shared/sort/Sort';
|
||||
import type { TOption } from 'ui/shared/sort/Option';
|
||||
|
||||
export const SORT_OPTIONS: Array<Option<ValidatorsSortingValue>> = [
|
||||
export const SORT_OPTIONS: Array<TOption<ValidatorsSortingValue>> = [
|
||||
{ title: 'Default', id: undefined },
|
||||
{ title: 'Status descending', id: 'state-desc' },
|
||||
{ title: 'Status ascending', id: 'state-asc' },
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { VerifiedContractsSortingValue, VerifiedContractsSortingField } from 'types/api/verifiedContracts';
|
||||
|
||||
import type { Option } from 'ui/shared/sort/Sort';
|
||||
import type { TOption } from 'ui/shared/sort/Option';
|
||||
|
||||
export const SORT_OPTIONS: Array<Option<VerifiedContractsSortingValue>> = [
|
||||
export const SORT_OPTIONS: Array<TOption<VerifiedContractsSortingValue>> = [
|
||||
{ title: 'Default', id: undefined },
|
||||
{ title: 'Balance descending', id: 'balance-desc' },
|
||||
{ title: 'Balance ascending', id: 'balance-asc' },
|
||||
|
||||