mirror of
https://github.com/luxfi/explore.git
synced 2026-09-24 00:28:18 +00:00
add security score to dapp card
This commit is contained in:
@@ -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';
|
||||
}
|
||||
|
||||
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,10 +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 ]);
|
||||
|
||||
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 ]);
|
||||
|
||||
if (!securityReport && !isLoading) {
|
||||
return null;
|
||||
@@ -45,7 +59,7 @@ const AppSecurityReport = ({ id, securityReport, showContractList, isLoading, on
|
||||
} = securityReport?.overallInfo || {};
|
||||
|
||||
return (
|
||||
<Popover isOpen={ isOpen } onClose={ onClose } placement="bottom-start" isLazy>
|
||||
<Popover isOpen={ isOpen } onClose={ onClose } placement={ popoverPlacement } isLazy>
|
||||
<PopoverTrigger>
|
||||
<SolidityscanReportButton
|
||||
score={ securityScore }
|
||||
@@ -54,13 +68,29 @@ const AppSecurityReport = ({ id, securityReport, showContractList, isLoading, on
|
||||
isActive={ isOpen }
|
||||
onlyIcon={ onlyIcon }
|
||||
label="The security score is based on analysis of a DApp'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" h="32px">
|
||||
<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) => {
|
||||
@@ -64,26 +70,23 @@ const MarketplaceAppCard = ({
|
||||
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"
|
||||
gap={ 4 }
|
||||
>
|
||||
<Skeleton
|
||||
isLoaded={ !isLoading }
|
||||
marginBottom={ 4 }
|
||||
w={{ base: '64px', sm: '96px' }}
|
||||
h={{ base: '64px', sm: '96px' }}
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
order={{ base: 'auto', sm: 1 }}
|
||||
mb={{ base: 0, sm: 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', sm: 'contents' }}
|
||||
flexDirection="column"
|
||||
gap={ 2 }
|
||||
pt={ 1 }
|
||||
>
|
||||
<Skeleton
|
||||
isLoaded={ !isLoading }
|
||||
fontSize={{ base: 'sm', sm: 'lg' }}
|
||||
lineHeight={{ base: '20px', sm: '28px' }}
|
||||
paddingRight={{ base: '40px', sm: 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, sm: 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={{ sm: 2 }}
|
||||
href="#"
|
||||
onClick={ handleInfoClick }
|
||||
>
|
||||
More info
|
||||
</Link>
|
||||
<IconButton
|
||||
aria-label="Mark as favorite"
|
||||
title="Mark as favorite"
|
||||
variant="ghost"
|
||||
colorScheme="gray"
|
||||
w={{ base: 6, sm: '30px' }}
|
||||
h={{ base: 6, sm: '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, sm: 5 }}
|
||||
top={{ base: '10px', sm: 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, sm: 1 }}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -185,126 +175,114 @@ 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', sm: 'row' }}
|
||||
justifyContent={{ base: 'flex-start', sm: '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"
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
) }
|
||||
<Text>{ description }</Text>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter
|
||||
display="flex"
|
||||
flexDirection={{ base: 'column', sm: 'row' }}
|
||||
justifyContent={{ base: 'flex-start', sm: 'space-between' }}
|
||||
alignItems={{ base: 'flex-start', sm: '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>
|
||||
|
||||
@@ -27,7 +27,8 @@ type Props = {
|
||||
}
|
||||
|
||||
const MarketplaceAppTopBar = ({ data, isLoading, securityReport }: Props) => {
|
||||
const [ showContractList, setShowContractList ] = useBoolean(false);
|
||||
const [ isContractListShown, setIsContractListShown ] = useBoolean(false);
|
||||
const [ contractListType, setContractListType ] = React.useState(ContractListTypes.ALL);
|
||||
const appProps = useAppContext();
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
@@ -44,6 +45,11 @@ const MarketplaceAppTopBar = ({ data, isLoading, securityReport }: Props) => {
|
||||
} catch (err) {}
|
||||
}
|
||||
|
||||
const showContractList = React.useCallback((id: string, type: ContractListTypes) => {
|
||||
setIsContractListShown.on();
|
||||
setContractListType(type);
|
||||
}, [ setIsContractListShown ]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flex alignItems="center" flexWrap="wrap" mb={{ base: 3, md: 2 }} rowGap={ 3 } columnGap={ 2 }>
|
||||
@@ -74,7 +80,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 +93,11 @@ const MarketplaceAppTopBar = ({ data, isLoading, securityReport }: Props) => {
|
||||
</Flex>
|
||||
) }
|
||||
</Flex>
|
||||
{ showContractList && (
|
||||
{ isContractListShown && (
|
||||
<ContractListModal
|
||||
type={ ContractListTypes.ANALYZED }
|
||||
type={ contractListType }
|
||||
contracts={ securityReport?.contractsData }
|
||||
onClose={ setShowContractList.off }
|
||||
onClose={ setIsContractListShown.off }
|
||||
/>
|
||||
) }
|
||||
</>
|
||||
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
@@ -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,10 +85,6 @@ export default function useMarketplace() {
|
||||
setSelectedCategoryId(newCategory);
|
||||
}, []);
|
||||
|
||||
const handleDisplayTypeChange = React.useCallback((newDisplayType: MarketplaceDisplayType) => {
|
||||
setSelectedDisplayType(newDisplayType);
|
||||
}, []);
|
||||
|
||||
const {
|
||||
isPlaceholderData, isError, error, data, displayedApps,
|
||||
} = useMarketplaceApps(debouncedFilterQuery, selectedCategoryId, favoriteApps, isFavoriteAppsLoaded);
|
||||
@@ -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,8 +149,6 @@ export default function useMarketplace() {
|
||||
isCategoriesPlaceholderData,
|
||||
showContractList,
|
||||
contractListModalType,
|
||||
selectedDisplayType,
|
||||
onDisplayTypeChange: handleDisplayTypeChange,
|
||||
hasPreviousStep,
|
||||
}), [
|
||||
selectedCategoryId,
|
||||
@@ -184,8 +171,6 @@ export default function useMarketplace() {
|
||||
isCategoriesPlaceholderData,
|
||||
showContractList,
|
||||
contractListModalType,
|
||||
selectedDisplayType,
|
||||
handleDisplayTypeChange,
|
||||
hasPreviousStep,
|
||||
]);
|
||||
}
|
||||
|
||||
+12
-60
@@ -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,11 @@ 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 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 TabsWithScroll from 'ui/shared/Tabs/TabsWithScroll';
|
||||
|
||||
import useMarketplace from '../marketplace/useMarketplace';
|
||||
@@ -67,8 +65,6 @@ const Marketplace = () => {
|
||||
isCategoriesPlaceholderData,
|
||||
showContractList,
|
||||
contractListModalType,
|
||||
selectedDisplayType,
|
||||
onDisplayTypeChange,
|
||||
hasPreviousStep,
|
||||
} = useMarketplace();
|
||||
|
||||
@@ -187,38 +183,6 @@ const Marketplace = () => {
|
||||
</Box>
|
||||
|
||||
<Flex direction={{ base: 'column', lg: 'row' }} mb={{ base: 4, lg: 6 }} gap={{ base: 4, 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>
|
||||
) }
|
||||
<FilterInput
|
||||
initialValue={ filterQuery }
|
||||
onChange={ onSearchInputChange }
|
||||
@@ -229,28 +193,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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
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';
|
||||
@@ -13,10 +13,11 @@ interface Props {
|
||||
onClick?: () => void;
|
||||
label?: string;
|
||||
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);
|
||||
@@ -26,6 +27,7 @@ const SolidityscanReportButton = (
|
||||
return (
|
||||
<Tooltip label={ label } isDisabled={ isMobile } openDelay={ 100 }>
|
||||
<Button
|
||||
className={ className }
|
||||
ref={ ref }
|
||||
color={ isLoading ? colorLoading : scoreColor }
|
||||
size="sm"
|
||||
@@ -54,4 +56,4 @@ const SolidityscanReportButton = (
|
||||
);
|
||||
};
|
||||
|
||||
export default React.forwardRef(SolidityscanReportButton);
|
||||
export default chakra(React.forwardRef(SolidityscanReportButton));
|
||||
|
||||
Reference in New Issue
Block a user