Commit bde9906e authored by Max Alekseenko's avatar Max Alekseenko

add security score to dapp card

parent 11fe8c6f
......@@ -41,11 +41,6 @@ export enum ContractListTypes {
VERIFIED = 'Verified',
}
export enum MarketplaceDisplayType {
DEFAULT = 'default',
SCORES = 'scores',
}
export type MarketplaceAppSecurityReport = {
overallInfo: {
verifiedNumber: number;
......
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);
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))
......
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"
>
<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"
>
<Link
fontSize={{ base: 'xs', sm: 'sm' }}
fontWeight="500"
paddingRight={{ sm: 2 }}
href="#"
onClick={ handleInfoClick }
>
More info
</Link>
</Box>
) }
<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>
);
......
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>
<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>
<Box marginBottom={ 2 }>
<ModalFooter
display="flex"
flexDirection={{ base: 'column', sm: 'row' }}
justifyContent={{ base: 'flex-start', sm: 'space-between' }}
alignItems={{ base: 'flex-start', sm: 'center' }}
gap={ 3 }
>
<Flex gap={ 2 }>
{ categories.map((category) => (
<Tag
colorScheme="blue"
marginRight={ 2 }
marginBottom={ 2 }
key={ category }
>
{ category }
</Tag>
)) }
</Box>
<Text>{ description }</Text>
</ModalBody>
<ModalFooter
display="flex"
flexDirection={{ base: 'column', sm: 'row' }}
alignItems={{ base: 'flex-start', sm: 'center' }}
>
{ 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 }
/>
</Flex>
<Text
color="inherit"
whiteSpace="nowrap"
overflow="hidden"
textOverflow="ellipsis"
<Flex alignItems="center" gap={ 3 }>
{ site && (
<Link
isExternal
href={ site }
display="flex"
alignItems="center"
fontSize="sm"
>
{ getHostname(site) }
</Text>
</Link>
) }
<IconSvg
name="link"
display="inline"
verticalAlign="baseline"
boxSize="18px"
marginRight={ 2 }
/>
{ 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 }
<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>
......
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;
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;
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;
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;
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;
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,
]);
}
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
......
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));
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment