Commit 6aba61a4 authored by tom's avatar tom

marketplace apps page

parent 4aaf1f1a
...@@ -9,7 +9,7 @@ const feature = config.features.marketplace; ...@@ -9,7 +9,7 @@ const feature = config.features.marketplace;
export default function useGraphLinks() { export default function useGraphLinks() {
const fetch = useFetch(); const fetch = useFetch();
return useQuery<unknown, ResourceError<unknown>, Record<string, Array<{ text: string; url: string }>>>({ return useQuery<unknown, ResourceError<unknown>, Record<string, Array<{ title: string; url: string }>>>({
queryKey: [ 'graph-links' ], queryKey: [ 'graph-links' ],
queryFn: async() => fetch((feature.isEnabled && feature.graphLinksUrl) ? feature.graphLinksUrl : '', undefined, { resource: 'graph-links' }), queryFn: async() => fetch((feature.isEnabled && feature.graphLinksUrl) ? feature.graphLinksUrl : '', undefined, { resource: 'graph-links' }),
enabled: feature.isEnabled && Boolean(feature.graphLinksUrl), enabled: feature.isEnabled && Boolean(feature.graphLinksUrl),
......
...@@ -4,11 +4,11 @@ import React from 'react'; ...@@ -4,11 +4,11 @@ import React from 'react';
import PageNextJs from 'nextjs/PageNextJs'; import PageNextJs from 'nextjs/PageNextJs';
// const Marketplace = dynamic(() => import('ui/pages/Marketplace'), { ssr: false }); const Marketplace = dynamic(() => import('ui/pages/Marketplace'), { ssr: false });
const Page: NextPage = () => ( const Page: NextPage = () => (
<PageNextJs pathname="/apps"> <PageNextJs pathname="/apps">
{ /* <Marketplace/> */ } <Marketplace/>
</PageNextJs> </PageNextJs>
); );
......
import { Box, Text, Link, PopoverTrigger, PopoverBody, PopoverContent, useDisclosure, chakra, Flex, Divider, Icon } from '@chakra-ui/react'; import type { BoxProps, ButtonProps } from '@chakra-ui/react';
import { Box, Text, Flex, Separator, Icon } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { MarketplaceAppSecurityReport } from 'types/client/marketplace'; import type { MarketplaceAppSecurityReport } from 'types/client/marketplace';
...@@ -11,7 +12,9 @@ import config from 'configs/app'; ...@@ -11,7 +12,9 @@ import config from 'configs/app';
import solidityScanIcon from 'icons/brands/solidity_scan.svg'; import solidityScanIcon from 'icons/brands/solidity_scan.svg';
import { apos } from 'lib/html-entities'; import { apos } from 'lib/html-entities';
import * as mixpanel from 'lib/mixpanel/index'; import * as mixpanel from 'lib/mixpanel/index';
import Popover from 'ui/shared/chakra/Popover'; import { Link } from 'toolkit/chakra/link';
import { PopoverBody, PopoverContent, PopoverRoot } from 'toolkit/chakra/popover';
import { useDisclosure } from 'toolkit/hooks/useDisclosure';
import IconSvg from 'ui/shared/IconSvg'; import IconSvg from 'ui/shared/IconSvg';
import SolidityscanReportButton from 'ui/shared/solidityscanReport/SolidityscanReportButton'; import SolidityscanReportButton from 'ui/shared/solidityscanReport/SolidityscanReportButton';
import SolidityscanReportDetails from 'ui/shared/solidityscanReport/SolidityscanReportDetails'; import SolidityscanReportDetails from 'ui/shared/solidityscanReport/SolidityscanReportDetails';
...@@ -24,29 +27,32 @@ type Props = { ...@@ -24,29 +27,32 @@ type Props = {
isLoading?: boolean; isLoading?: boolean;
onlyIcon?: boolean; onlyIcon?: boolean;
source: 'Discovery view' | 'App modal' | 'App page'; source: 'Discovery view' | 'App modal' | 'App page';
className?: string;
popoverPlacement?: 'bottom-start' | 'bottom-end' | 'left'; popoverPlacement?: 'bottom-start' | 'bottom-end' | 'left';
buttonProps?: ButtonProps;
triggerWrapperProps?: BoxProps;
}; };
const AppSecurityReport = ({ const AppSecurityReport = ({
id, securityReport, showContractList, isLoading, onlyIcon, source, className, popoverPlacement = 'bottom-start', id, securityReport, showContractList, isLoading, onlyIcon, source, triggerWrapperProps, buttonProps, popoverPlacement = 'bottom-start',
}: Props) => { }: Props) => {
const { isOpen, onToggle, onClose } = useDisclosure();
const { open, onOpenChange } = useDisclosure();
const handleButtonClick = React.useCallback(() => { const handleButtonClick = React.useCallback(() => {
mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'Security score', Info: id, Source: source }); mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'Security score', Info: id, Source: source });
onToggle(); }, [ id, source ]);
}, [ id, source, onToggle ]);
const showAnalyzedContracts = React.useCallback(() => { const showAnalyzedContracts = React.useCallback(() => {
mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'Analyzed contracts', Info: id, Source: 'Security score popup' }); mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'Analyzed contracts', Info: id, Source: 'Security score popup' });
showContractList(id, ContractListTypes.ANALYZED); showContractList(id, ContractListTypes.ANALYZED);
}, [ showContractList, id ]); onOpenChange({ open: false });
}, [ showContractList, id, onOpenChange ]);
const showAllContracts = React.useCallback(() => { const showAllContracts = React.useCallback(() => {
mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'Total contracts', Info: id, Source: 'Security score popup' }); mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'Total contracts', Info: id, Source: 'Security score popup' });
showContractList(id, ContractListTypes.ALL); showContractList(id, ContractListTypes.ALL);
}, [ showContractList, id ]); onOpenChange({ open: false });
}, [ showContractList, id, onOpenChange ]);
const { const {
securityScore = 0, securityScore = 0,
...@@ -60,31 +66,29 @@ const AppSecurityReport = ({ ...@@ -60,31 +66,29 @@ const AppSecurityReport = ({
} }
return ( return (
<Popover isOpen={ isOpen } onClose={ onClose } placement={ popoverPlacement } isLazy> <PopoverRoot open={ open } onOpenChange={ onOpenChange } positioning={{ placement: popoverPlacement }}>
<PopoverTrigger> <SolidityscanReportButton
<SolidityscanReportButton score={ securityScore }
score={ securityScore } isLoading={ isLoading }
isLoading={ isLoading } onClick={ handleButtonClick }
onClick={ handleButtonClick } onlyIcon={ onlyIcon }
isActive={ isOpen } label={ <>The security score is based on analysis<br/>of a DApp{ apos }s smart contracts.</> }
onlyIcon={ onlyIcon } wrapperProps={ triggerWrapperProps }
label={ <>The security score is based on analysis<br/>of a DApp{ apos }s smart contracts.</> } { ...buttonProps }
className={ className } />
/> <PopoverContent w={{ base: 'calc(100vw - 48px)', lg: '328px' }} mx={{ base: 3, lg: 0 }}>
</PopoverTrigger> <PopoverBody px="26px" py="20px" textStyle="sm">
<PopoverContent w={{ base: 'calc(100vw - 24px)', lg: '328px' }} mx={{ base: 3, lg: 0 }}> <Text fontWeight="500" textStyle="xs" mb={ 2 } color="text.secondary">Smart contracts info</Text>
<PopoverBody px="26px" py="20px" fontSize="sm">
<Text fontWeight="500" fontSize="xs" mb={ 2 } variant="secondary">Smart contracts info</Text>
<Flex alignItems="center" justifyContent="space-between" py={ 1.5 }> <Flex alignItems="center" justifyContent="space-between" py={ 1.5 }>
<Flex alignItems="center"> <Flex alignItems="center">
<IconSvg name="contracts/verified_many" boxSize={ 5 } color="green.500" mr={ 1 }/> <IconSvg name="contracts/verified_many" boxSize={ 5 } color="green.500" mr={ 1 }/>
<Text>Verified contracts</Text> <Text>Verified contracts</Text>
</Flex> </Flex>
<Link fontSize="sm" fontWeight="500" onClick={ showAllContracts }> <Link textStyle="sm" fontWeight="500" onClick={ showAllContracts }>
{ securityReport?.overallInfo.verifiedNumber ?? 0 } of { securityReport?.overallInfo.totalContractsNumber ?? 0 } { securityReport?.overallInfo.verifiedNumber ?? 0 } of { securityReport?.overallInfo.totalContractsNumber ?? 0 }
</Link> </Link>
</Flex> </Flex>
<Divider my={ 3 }/> <Separator my={ 3 }/>
<Box mb={ 5 }> <Box mb={ 5 }>
{ solidityScanContractsNumber } smart contract{ solidityScanContractsNumber === 1 ? ' was' : 's were' } evaluated to determine { solidityScanContractsNumber } smart contract{ solidityScanContractsNumber === 1 ? ' was' : 's were' } evaluated to determine
this protocol{ apos }s overall security score on the { config.chain.name } network by { ' ' } this protocol{ apos }s overall security score on the { config.chain.name } network by { ' ' }
...@@ -96,7 +100,7 @@ const AppSecurityReport = ({ ...@@ -96,7 +100,7 @@ const AppSecurityReport = ({
<SolidityscanReportScore score={ securityScore } mb={ 5 }/> <SolidityscanReportScore score={ securityScore } mb={ 5 }/>
{ issueSeverityDistribution && totalIssues > 0 && ( { issueSeverityDistribution && totalIssues > 0 && (
<Box mb={ 5 }> <Box mb={ 5 }>
<Text py="7px" variant="secondary" fontSize="xs" fontWeight={ 500 }>Threat score & vulnerabilities</Text> <Text py="7px" color="text.secondary" textStyle="xs" fontWeight={ 500 }>Threat score & vulnerabilities</Text>
<SolidityscanReportDetails vulnerabilities={ issueSeverityDistribution } vulnerabilitiesCount={ totalIssues }/> <SolidityscanReportDetails vulnerabilities={ issueSeverityDistribution } vulnerabilitiesCount={ totalIssues }/>
</Box> </Box>
) } ) }
...@@ -105,8 +109,8 @@ const AppSecurityReport = ({ ...@@ -105,8 +109,8 @@ const AppSecurityReport = ({
</Link> </Link>
</PopoverBody> </PopoverBody>
</PopoverContent> </PopoverContent>
</Popover> </PopoverRoot>
); );
}; };
export default chakra(AppSecurityReport); export default AppSecurityReport;
import { Link, useColorModeValue, LinkBox, Flex, Image, LinkOverlay, IconButton } from '@chakra-ui/react'; import { LinkBox, Flex, LinkOverlay } from '@chakra-ui/react';
import type { MouseEvent } from 'react'; import type { MouseEvent } from 'react';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
...@@ -6,7 +6,11 @@ import type { MarketplaceAppPreview } from 'types/client/marketplace'; ...@@ -6,7 +6,11 @@ import type { MarketplaceAppPreview } from 'types/client/marketplace';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import * as mixpanel from 'lib/mixpanel/index'; import * as mixpanel from 'lib/mixpanel/index';
import Skeleton from 'ui/shared/chakra/Skeleton'; import { useColorModeValue } from 'toolkit/chakra/color-mode';
import { IconButton } from 'toolkit/chakra/icon-button';
import { Image } from 'toolkit/chakra/image';
import { Link } from 'toolkit/chakra/link';
import { Skeleton } from 'toolkit/chakra/skeleton';
import NextLink from 'ui/shared/NextLink'; import NextLink from 'ui/shared/NextLink';
import FavoriteIcon from '../FavoriteIcon'; import FavoriteIcon from '../FavoriteIcon';
...@@ -32,8 +36,6 @@ const FeaturedApp = ({ ...@@ -32,8 +36,6 @@ const FeaturedApp = ({
const logoUrl = useColorModeValue(logo, logoDarkMode || logo); const logoUrl = useColorModeValue(logo, logoDarkMode || logo);
const categoriesLabel = categories.join(', '); const categoriesLabel = categories.join(', ');
const backgroundColor = useColorModeValue('purple.50', 'whiteAlpha.100');
const handleInfoClick = useCallback((event: MouseEvent) => { const handleInfoClick = useCallback((event: MouseEvent) => {
event.preventDefault(); event.preventDefault();
mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'More button', Info: id, Source: 'Banner' }); mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'More button', Info: id, Source: 'Banner' });
...@@ -64,12 +66,12 @@ const FeaturedApp = ({ ...@@ -64,12 +66,12 @@ const FeaturedApp = ({
borderRadius="md" borderRadius="md"
height="136px" height="136px"
padding={ 5 } padding={ 5 }
background={ backgroundColor } background={{ _light: 'purple.50', _dark: 'whiteAlpha.100' }}
mb={ 2 } mb={ 2 }
mt={ 6 } mt={ 6 }
> >
<Skeleton <Skeleton
isLoaded={ !isLoading } loading={ isLoading }
w="96px" w="96px"
h="96px" h="96px"
display="flex" display="flex"
...@@ -86,14 +88,14 @@ const FeaturedApp = ({ ...@@ -86,14 +88,14 @@ const FeaturedApp = ({
<Flex flexDirection="column" flex={ 1 } gap={ 2 }> <Flex flexDirection="column" flex={ 1 } gap={ 2 }>
<Flex alignItems="center" gap={ 3 }> <Flex alignItems="center" gap={ 3 }>
<Skeleton <Skeleton
isLoaded={ !isLoading } loading={ isLoading }
fontSize="30px" fontSize="30px"
fontWeight="semibold" fontWeight="semibold"
fontFamily="heading" fontFamily="heading"
lineHeight="36px" lineHeight="36px"
> >
{ external ? ( { external ? (
<LinkOverlay href={ url } isExternal={ true } marginRight={ 2 }> <LinkOverlay href={ url } target="_blank" marginRight={ 2 }>
{ title } { title }
</LinkOverlay> </LinkOverlay>
) : ( ) : (
...@@ -107,7 +109,7 @@ const FeaturedApp = ({ ...@@ -107,7 +109,7 @@ const FeaturedApp = ({
</Skeleton> </Skeleton>
<Skeleton <Skeleton
isLoaded={ !isLoading } loading={ isLoading }
color="text_secondary" color="text_secondary"
fontSize="xs" fontSize="xs"
flex={ 1 } flex={ 1 }
...@@ -128,25 +130,26 @@ const FeaturedApp = ({ ...@@ -128,25 +130,26 @@ const FeaturedApp = ({
{ !isLoading && ( { !isLoading && (
<IconButton <IconButton
display="flex"
alignItems="center"
aria-label="Mark as favorite" aria-label="Mark as favorite"
title="Mark as favorite" title="Mark as favorite"
variant="ghost" // TODO @tom2drum fix this button
colorScheme="gray" boxSize={ 8 }
w={ 9 }
h={ 8 }
onClick={ handleFavoriteClick } onClick={ handleFavoriteClick }
icon={ <FavoriteIcon isFavorite={ isFavorite }/> } >
/> <FavoriteIcon isFavorite={ isFavorite }/>
</IconButton>
) } ) }
</Flex> </Flex>
<Skeleton <Skeleton
isLoaded={ !isLoading } loading={ isLoading }
fontSize="sm" textStyle="sm"
lineHeight="20px" WebkitLineClamp={ 2 }
noOfLines={ 2 } style={{
WebkitBoxOrient: 'vertical',
}}
display="-webkit-box"
overflow="hidden"
> >
{ shortDescription } { shortDescription }
</Skeleton> </Skeleton>
......
import { IconButton, Image, Link, LinkBox, useColorModeValue, Flex } from '@chakra-ui/react'; import { LinkBox, Flex } from '@chakra-ui/react';
import type { MouseEvent } from 'react'; import type { MouseEvent } from 'react';
import React from 'react'; import React from 'react';
import type { MarketplaceAppPreview } from 'types/client/marketplace'; import type { MarketplaceAppPreview } from 'types/client/marketplace';
import Skeleton from 'ui/shared/chakra/Skeleton'; import { useColorModeValue } from 'toolkit/chakra/color-mode';
import { IconButton } from 'toolkit/chakra/icon-button';
import { Image } from 'toolkit/chakra/image';
import { Link } from 'toolkit/chakra/link';
import { Skeleton } from 'toolkit/chakra/skeleton';
import FavoriteIcon from '../FavoriteIcon'; import FavoriteIcon from '../FavoriteIcon';
import MarketplaceAppCardLink from '../MarketplaceAppCardLink'; import MarketplaceAppCardLink from '../MarketplaceAppCardLink';
...@@ -43,7 +47,7 @@ const FeaturedAppMobile = ({ ...@@ -43,7 +47,7 @@ const FeaturedAppMobile = ({
borderRadius="md" borderRadius="md"
padding={{ base: 3, sm: '20px' }} padding={{ base: 3, sm: '20px' }}
role="group" role="group"
background={ useColorModeValue('purple.50', 'whiteAlpha.100') } background={{ base: 'purple.50', sm: 'whiteAlpha.100' }}
mt={ 6 } mt={ 6 }
> >
<Flex <Flex
...@@ -58,7 +62,7 @@ const FeaturedAppMobile = ({ ...@@ -58,7 +62,7 @@ const FeaturedAppMobile = ({
justifyContent="space-between" justifyContent="space-between"
> >
<Skeleton <Skeleton
isLoaded={ !isLoading } loading={ isLoading }
w={{ base: '64px', sm: '96px' }} w={{ base: '64px', sm: '96px' }}
h={{ base: '64px', sm: '96px' }} h={{ base: '64px', sm: '96px' }}
display="flex" display="flex"
...@@ -93,7 +97,7 @@ const FeaturedAppMobile = ({ ...@@ -93,7 +97,7 @@ const FeaturedAppMobile = ({
<Flex flexDirection="column" gap={ 2 }> <Flex flexDirection="column" gap={ 2 }>
<Skeleton <Skeleton
isLoaded={ !isLoading } loading={ isLoading }
fontSize={{ base: 'sm', sm: 'lg' }} fontSize={{ base: 'sm', sm: 'lg' }}
lineHeight={{ base: '20px', sm: '28px' }} lineHeight={{ base: '20px', sm: '28px' }}
paddingRight={{ base: '25px', sm: '110px' }} paddingRight={{ base: '25px', sm: '110px' }}
...@@ -112,19 +116,22 @@ const FeaturedAppMobile = ({ ...@@ -112,19 +116,22 @@ const FeaturedAppMobile = ({
</Skeleton> </Skeleton>
<Skeleton <Skeleton
isLoaded={ !isLoading } loading={ isLoading }
color="text_secondary" color="text.secondary"
fontSize="xs" textStyle="xs"
lineHeight="16px"
> >
<span>{ categoriesLabel }</span> <span>{ categoriesLabel }</span>
</Skeleton> </Skeleton>
<Skeleton <Skeleton
isLoaded={ !isLoading } loading={ isLoading }
fontSize={{ base: 'xs', sm: 'sm' }} textStyle="xs"
lineHeight="20px" WebkitLineClamp={ 3 }
noOfLines={ 3 } style={{
WebkitBoxOrient: 'vertical',
}}
display="-webkit-box"
overflow="hidden"
> >
{ shortDescription } { shortDescription }
</Skeleton> </Skeleton>
...@@ -140,13 +147,12 @@ const FeaturedAppMobile = ({ ...@@ -140,13 +147,12 @@ const FeaturedAppMobile = ({
top={{ base: 1, sm: '18px' }} top={{ base: 1, sm: '18px' }}
aria-label="Mark as favorite" aria-label="Mark as favorite"
title="Mark as favorite" title="Mark as favorite"
variant="ghost" // TODO @tom2drum fix this button
colorScheme="gray" boxSize={ 8 }
w={ 9 }
h={ 8 }
onClick={ onFavoriteClick } onClick={ onFavoriteClick }
icon={ <FavoriteIcon isFavorite={ isFavorite }/> } >
/> <FavoriteIcon isFavorite={ isFavorite }/>
</IconButton>
) } ) }
</Flex> </Flex>
</LinkBox> </LinkBox>
......
import { Link, Box } from '@chakra-ui/react'; import { chakra } from '@chakra-ui/react';
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
import * as mixpanel from 'lib/mixpanel/index'; import * as mixpanel from 'lib/mixpanel/index';
import Skeleton from 'ui/shared/chakra/Skeleton'; import { Link } from 'toolkit/chakra/link';
import { Skeleton } from 'toolkit/chakra/skeleton';
const IframeBanner = ({ contentUrl, linkUrl }: { contentUrl: string; linkUrl: string }) => { const IframeBanner = ({ contentUrl, linkUrl }: { contentUrl: string; linkUrl: string }) => {
const [ isFrameLoading, setIsFrameLoading ] = useState(true); const [ isFrameLoading, setIsFrameLoading ] = useState(true);
...@@ -17,7 +18,7 @@ const IframeBanner = ({ contentUrl, linkUrl }: { contentUrl: string; linkUrl: st ...@@ -17,7 +18,7 @@ const IframeBanner = ({ contentUrl, linkUrl }: { contentUrl: string; linkUrl: st
return ( return (
<Skeleton <Skeleton
isLoaded={ !isFrameLoading } loading={ isFrameLoading }
position="relative" position="relative"
h="136px" h="136px"
w="100%" w="100%"
...@@ -28,8 +29,8 @@ const IframeBanner = ({ contentUrl, linkUrl }: { contentUrl: string; linkUrl: st ...@@ -28,8 +29,8 @@ const IframeBanner = ({ contentUrl, linkUrl }: { contentUrl: string; linkUrl: st
> >
<Link <Link
href={ linkUrl } href={ linkUrl }
target="_blank" external
rel="noopener noreferrer" noIcon
onClick={ handleClick } onClick={ handleClick }
position="absolute" position="absolute"
w="100%" w="100%"
...@@ -38,8 +39,7 @@ const IframeBanner = ({ contentUrl, linkUrl }: { contentUrl: string; linkUrl: st ...@@ -38,8 +39,7 @@ const IframeBanner = ({ contentUrl, linkUrl }: { contentUrl: string; linkUrl: st
left={ 0 } left={ 0 }
zIndex={ 1 } zIndex={ 1 }
/> />
<Box <chakra.iframe
as="iframe"
h="100%" h="100%"
w="100%" w="100%"
src={ contentUrl } src={ contentUrl }
......
import { import { Box } from '@chakra-ui/react';
Box, Modal, Text, ModalBody,
ModalCloseButton, ModalContent, ModalHeader, ModalOverlay,
} from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { MarketplaceAppSecurityReport } from 'types/client/marketplace'; import type { MarketplaceAppSecurityReport } from 'types/client/marketplace';
import { ContractListTypes } from 'types/client/marketplace'; import { ContractListTypes } from 'types/client/marketplace';
import useIsMobile from 'lib/hooks/useIsMobile';
import { apos } from 'lib/html-entities'; import { apos } from 'lib/html-entities';
import { DialogBody, DialogContent, DialogHeader, DialogRoot } from 'toolkit/chakra/dialog';
import AddressEntity from 'ui/shared/entities/address/AddressEntity'; import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import IconSvg from 'ui/shared/IconSvg';
import ContractSecurityReport from './ContractSecurityReport'; import ContractSecurityReport from './ContractSecurityReport';
...@@ -28,7 +24,11 @@ const titles = { ...@@ -28,7 +24,11 @@ const titles = {
}; };
const ContractListModal = ({ onClose, onBack, type, contracts }: Props) => { const ContractListModal = ({ onClose, onBack, type, contracts }: Props) => {
const isMobile = useIsMobile(); const handleOpenChange = React.useCallback(({ open }: { open: boolean }) => {
if (!open) {
onClose();
}
}, [ onClose ]);
const displayedContracts = React.useMemo(() => { const displayedContracts = React.useMemo(() => {
if (!contracts) { if (!contracts) {
...@@ -54,35 +54,18 @@ const ContractListModal = ({ onClose, onBack, type, contracts }: Props) => { ...@@ -54,35 +54,18 @@ const ContractListModal = ({ onClose, onBack, type, contracts }: Props) => {
} }
return ( return (
<Modal <DialogRoot
isOpen={ Boolean(type) } open={ Boolean(type) }
onClose={ onClose } onOpenChange={ handleOpenChange }
size={ isMobile ? 'full' : 'md' } size={{ lgDown: 'full', lg: 'md' }}
isCentered placement="center"
> >
<ModalOverlay/> <DialogContent>
<ModalContent> <DialogHeader display="flex" alignItems="center" mb={ 4 } onBackToClick={ onBack }>
<ModalHeader display="flex" alignItems="center" mb={ 4 }> { titles[type] }
{ onBack && ( </DialogHeader>
<IconSvg <DialogBody
name="arrows/east" maxH={{ base: 'max-content', lg: '352px' }}
w={ 6 }
h={ 10 }
transform="rotate(180deg)"
verticalAlign="middle"
color="gray.400"
mr={ 3 }
cursor="pointer"
onClick={ onBack }
/>
) }
<Text fontWeight="500" textStyle="h3">
{ titles[type] }
</Text>
</ModalHeader>
<ModalCloseButton/>
<ModalBody
maxH={ isMobile ? 'auto' : '352px' }
overflow="scroll" overflow="scroll"
mb={ 0 } mb={ 0 }
display="grid" display="grid"
...@@ -110,9 +93,9 @@ const ContractListModal = ({ onClose, onBack, type, contracts }: Props) => { ...@@ -110,9 +93,9 @@ const ContractListModal = ({ onClose, onBack, type, contracts }: Props) => {
/> />
</React.Fragment> </React.Fragment>
)) } )) }
</ModalBody> </DialogBody>
</ModalContent> </DialogContent>
</Modal> </DialogRoot>
); );
}; };
......
import { Box, Text, PopoverTrigger, PopoverBody, PopoverContent, useDisclosure, Icon } from '@chakra-ui/react'; import { Box, Text, Icon } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import config from 'configs/app'; import config from 'configs/app';
...@@ -8,8 +8,8 @@ import config from 'configs/app'; ...@@ -8,8 +8,8 @@ import config from 'configs/app';
import solidityScanIcon from 'icons/brands/solidity_scan.svg'; import solidityScanIcon from 'icons/brands/solidity_scan.svg';
import * as mixpanel from 'lib/mixpanel/index'; import * as mixpanel from 'lib/mixpanel/index';
import type { SolidityScanReport } from 'lib/solidityScan/schema'; import type { SolidityScanReport } from 'lib/solidityScan/schema';
import Popover from 'ui/shared/chakra/Popover'; import { Link } from 'toolkit/chakra/link';
import LinkExternal from 'ui/shared/links/LinkExternal'; import { PopoverBody, PopoverContent, PopoverRoot } from 'toolkit/chakra/popover';
import SolidityscanReportButton from 'ui/shared/solidityscanReport/SolidityscanReportButton'; import SolidityscanReportButton from 'ui/shared/solidityscanReport/SolidityscanReportButton';
import SolidityscanReportDetails from 'ui/shared/solidityscanReport/SolidityscanReportDetails'; import SolidityscanReportDetails from 'ui/shared/solidityscanReport/SolidityscanReportDetails';
import SolidityscanReportScore from 'ui/shared/solidityscanReport/SolidityscanReportScore'; import SolidityscanReportScore from 'ui/shared/solidityscanReport/SolidityscanReportScore';
...@@ -19,12 +19,9 @@ type Props = { ...@@ -19,12 +19,9 @@ type Props = {
}; };
const ContractSecurityReport = ({ securityReport }: Props) => { const ContractSecurityReport = ({ securityReport }: Props) => {
const { isOpen, onToggle, onClose } = useDisclosure();
const handleClick = React.useCallback(() => { const handleClick = React.useCallback(() => {
mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'Security score', Source: 'Analyzed contracts popup' }); mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'Security score', Source: 'Analyzed contracts popup' });
onToggle(); }, [ ]);
}, [ onToggle ]);
if (!securityReport) { if (!securityReport) {
return null; return null;
...@@ -39,16 +36,13 @@ const ContractSecurityReport = ({ securityReport }: Props) => { ...@@ -39,16 +36,13 @@ const ContractSecurityReport = ({ securityReport }: Props) => {
const totalIssues = Object.values(issueSeverityDistribution as Record<string, number>).reduce((acc, val) => acc + val, 0); const totalIssues = Object.values(issueSeverityDistribution as Record<string, number>).reduce((acc, val) => acc + val, 0);
return ( return (
<Popover isOpen={ isOpen } onClose={ onClose } placement="bottom-start" isLazy> <PopoverRoot>
<PopoverTrigger> <SolidityscanReportButton
<SolidityscanReportButton score={ parseFloat(securityScore) }
score={ parseFloat(securityScore) } onClick={ handleClick }
onClick={ handleClick } />
isActive={ isOpen } <PopoverContent>
/> <PopoverBody>
</PopoverTrigger>
<PopoverContent w={{ base: '100vw', lg: '328px' }}>
<PopoverBody px="26px" py="20px" fontSize="sm">
<Box mb={ 5 }> <Box mb={ 5 }>
The security score was derived from evaluating the smart contracts of a protocol on the { config.chain.name } network by { ' ' } The security score was derived from evaluating the smart contracts of a protocol on the { config.chain.name } network by { ' ' }
<Box> <Box>
...@@ -59,14 +53,14 @@ const ContractSecurityReport = ({ securityReport }: Props) => { ...@@ -59,14 +53,14 @@ const ContractSecurityReport = ({ securityReport }: Props) => {
<SolidityscanReportScore score={ parseFloat(securityScore) } mb={ 5 }/> <SolidityscanReportScore score={ parseFloat(securityScore) } mb={ 5 }/>
{ issueSeverityDistribution && totalIssues > 0 && ( { issueSeverityDistribution && totalIssues > 0 && (
<Box mb={ 5 }> <Box mb={ 5 }>
<Text py="7px" variant="secondary" fontSize="xs" fontWeight={ 500 }>Threat score & vulnerabilities</Text> <Text py="7px" color="text.secondary" textStyle="xs" fontWeight={ 500 }>Threat score & vulnerabilities</Text>
<SolidityscanReportDetails vulnerabilities={ issueSeverityDistribution } vulnerabilitiesCount={ totalIssues }/> <SolidityscanReportDetails vulnerabilities={ issueSeverityDistribution } vulnerabilitiesCount={ totalIssues }/>
</Box> </Box>
) } ) }
<LinkExternal href={ url }>View full report</LinkExternal> <Link external href={ url }>View full report</Link>
</PopoverBody> </PopoverBody>
</PopoverContent> </PopoverContent>
</Popover> </PopoverRoot>
); );
}; };
......
...@@ -4,9 +4,9 @@ import { MarketplaceCategory } from 'types/client/marketplace'; ...@@ -4,9 +4,9 @@ import { MarketplaceCategory } from 'types/client/marketplace';
import config from 'configs/app'; import config from 'configs/app';
import { apos } from 'lib/html-entities'; import { apos } from 'lib/html-entities';
import { Link } from 'toolkit/chakra/link';
import EmptySearchResultDefault from 'ui/shared/EmptySearchResult'; import EmptySearchResultDefault from 'ui/shared/EmptySearchResult';
import IconSvg from 'ui/shared/IconSvg'; import IconSvg from 'ui/shared/IconSvg';
import LinkExternal from 'ui/shared/links/LinkExternal';
const feature = config.features.marketplace; const feature = config.features.marketplace;
...@@ -29,7 +29,7 @@ const EmptySearchResult = ({ favoriteApps, selectedCategoryId }: Props) => ( ...@@ -29,7 +29,7 @@ const EmptySearchResult = ({ favoriteApps, selectedCategoryId }: Props) => (
{ 'suggestIdeasFormUrl' in feature && ( { 'suggestIdeasFormUrl' in feature && (
<> <>
{ ' ' }Have a groundbreaking idea or app suggestion?<br/> { ' ' }Have a groundbreaking idea or app suggestion?<br/>
<LinkExternal href={ feature.suggestIdeasFormUrl }>Share it with us</LinkExternal> <Link external href={ feature.suggestIdeasFormUrl }>Share it with us</Link>
</> </>
) } ) }
</> </>
......
import { useColorModeValue } from '@chakra-ui/react'; import type { HTMLChakraProps } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import IconSvg from 'ui/shared/IconSvg'; import IconSvg from 'ui/shared/IconSvg';
type Props = { interface Props extends HTMLChakraProps<'div'> {
isFavorite: boolean; isFavorite: boolean;
color?: string;
}; };
const FavoriteIcon = ({ isFavorite, color }: Props) => { const FavoriteIcon = ({ isFavorite, color, ...rest }: Props) => {
const heartFilledColor = useColorModeValue('blue.600', 'blue.300'); const heartFilledColor = { _light: 'blue.600', _dark: 'blue.300' };
const defaultColor = isFavorite ? heartFilledColor : (color || 'gray.400'); const defaultColor = isFavorite ? heartFilledColor : (color || 'gray.400');
return ( return (
...@@ -17,6 +16,7 @@ const FavoriteIcon = ({ isFavorite, color }: Props) => { ...@@ -17,6 +16,7 @@ const FavoriteIcon = ({ isFavorite, color }: Props) => {
name={ isFavorite ? 'heart_filled' : 'heart_outline' } name={ isFavorite ? 'heart_filled' : 'heart_outline' }
color={ defaultColor } color={ defaultColor }
boxSize={ 5 } boxSize={ 5 }
{ ...rest }
/> />
); );
}; };
......
import { IconButton, Image, Link, LinkBox, useColorModeValue, chakra, Flex } from '@chakra-ui/react'; import { LinkBox, chakra, Flex } from '@chakra-ui/react';
import type { MouseEvent } from 'react'; import type { MouseEvent } from 'react';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
...@@ -6,7 +6,11 @@ import type { MarketplaceAppWithSecurityReport, ContractListTypes, AppRating } f ...@@ -6,7 +6,11 @@ import type { MarketplaceAppWithSecurityReport, ContractListTypes, AppRating } f
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import isBrowser from 'lib/isBrowser'; import isBrowser from 'lib/isBrowser';
import Skeleton from 'ui/shared/chakra/Skeleton'; import { useColorModeValue } from 'toolkit/chakra/color-mode';
import { IconButton } from 'toolkit/chakra/icon-button';
import { Image } from 'toolkit/chakra/image';
import { Link } from 'toolkit/chakra/link';
import { Skeleton } from 'toolkit/chakra/skeleton';
import CopyToClipboard from 'ui/shared/CopyToClipboard'; import CopyToClipboard from 'ui/shared/CopyToClipboard';
import AppSecurityReport from './AppSecurityReport'; import AppSecurityReport from './AppSecurityReport';
...@@ -30,7 +34,7 @@ interface Props extends MarketplaceAppWithSecurityReport { ...@@ -30,7 +34,7 @@ interface Props extends MarketplaceAppWithSecurityReport {
isRatingSending: boolean; isRatingSending: boolean;
isRatingLoading: boolean; isRatingLoading: boolean;
canRate: boolean | undefined; canRate: boolean | undefined;
graphLinks: Array<{ text: string; url: string }>; graphLinks?: Array<{ title: string; url: string }>;
} }
const MarketplaceAppCard = ({ const MarketplaceAppCard = ({
...@@ -84,9 +88,8 @@ const MarketplaceAppCard = ({ ...@@ -84,9 +88,8 @@ const MarketplaceAppCard = ({
}} }}
borderRadius="md" borderRadius="md"
padding={{ base: 3, md: '20px' }} padding={{ base: 3, md: '20px' }}
border="1px" borderWidth="1px"
borderColor={ useColorModeValue('gray.200', 'gray.600') } borderColor={{ _light: 'gray.200', _dark: 'gray.600' }}
role="group"
> >
<Flex <Flex
flexDirection="column" flexDirection="column"
...@@ -99,7 +102,7 @@ const MarketplaceAppCard = ({ ...@@ -99,7 +102,7 @@ const MarketplaceAppCard = ({
gap={ 4 } gap={ 4 }
> >
<Skeleton <Skeleton
isLoaded={ !isLoading } loading={ isLoading }
w={{ base: '64px', md: '96px' }} w={{ base: '64px', md: '96px' }}
h={{ base: '64px', md: '96px' }} h={{ base: '64px', md: '96px' }}
display="flex" display="flex"
...@@ -121,9 +124,10 @@ const MarketplaceAppCard = ({ ...@@ -121,9 +124,10 @@ const MarketplaceAppCard = ({
pt={ 1 } pt={ 1 }
> >
<Skeleton <Skeleton
isLoaded={ !isLoading } loading={ isLoading }
paddingRight={{ base: '40px', md: 0 }} paddingRight={{ base: '40px', md: 0 }}
display="inline-block" display="inline-flex"
alignItems="center"
> >
<MarketplaceAppCardLink <MarketplaceAppCardLink
id={ id } id={ id }
...@@ -141,12 +145,11 @@ const MarketplaceAppCard = ({ ...@@ -141,12 +145,11 @@ const MarketplaceAppCard = ({
links={ graphLinks } links={ graphLinks }
ml={ 2 } ml={ 2 }
verticalAlign="middle" verticalAlign="middle"
mb={{ base: 0, md: 1 }}
/> />
</Skeleton> </Skeleton>
<Skeleton <Skeleton
isLoaded={ !isLoading } loading={ isLoading }
color="text_secondary" color="text_secondary"
fontSize="xs" fontSize="xs"
lineHeight="16px" lineHeight="16px"
...@@ -157,10 +160,14 @@ const MarketplaceAppCard = ({ ...@@ -157,10 +160,14 @@ const MarketplaceAppCard = ({
</Flex> </Flex>
<Skeleton <Skeleton
isLoaded={ !isLoading } loading={ isLoading }
fontSize="sm" textStyle="sm"
lineHeight="20px" WebkitLineClamp={{ base: 2, md: 3 }}
noOfLines={{ base: 2, md: 3 }} style={{
WebkitBoxOrient: 'vertical',
}}
display="-webkit-box"
overflow="hidden"
> >
{ shortDescription } { shortDescription }
</Skeleton> </Skeleton>
...@@ -172,7 +179,7 @@ const MarketplaceAppCard = ({ ...@@ -172,7 +179,7 @@ const MarketplaceAppCard = ({
marginTop="auto" marginTop="auto"
> >
<Link <Link
fontSize="sm" textStyle="sm"
fontWeight="500" fontWeight="500"
paddingRight={{ md: 2 }} paddingRight={{ md: 2 }}
href="#" href="#"
...@@ -196,20 +203,18 @@ const MarketplaceAppCard = ({ ...@@ -196,20 +203,18 @@ const MarketplaceAppCard = ({
title="Mark as favorite" title="Mark as favorite"
variant="ghost" variant="ghost"
colorScheme="gray" colorScheme="gray"
w={{ base: 6, md: '30px' }} boxSize={{ base: 6, md: '30px' }}
h={{ base: 6, md: '30px' }}
onClick={ handleFavoriteClick } onClick={ handleFavoriteClick }
icon={ <FavoriteIcon isFavorite={ isFavorite }/> }
ml={ 2 } ml={ 2 }
/> >
<FavoriteIcon isFavorite={ isFavorite }/>
</IconButton>
<CopyToClipboard <CopyToClipboard
text={ isBrowser() ? window.location.origin + `/apps/${ id }` : '' } text={ isBrowser() ? window.location.origin + `/apps/${ id }` : '' }
icon="share" type="share"
size={ 4 } boxSize={{ base: 6, md: '30px' }}
variant="ghost" variant="ghost"
colorScheme="gray" colorScheme="gray"
w={{ base: 6, md: '30px' }}
h={{ base: 6, md: '30px' }}
color="gray.400" color="gray.400"
_hover={{ color: 'gray.400' }} _hover={{ color: 'gray.400' }}
ml={{ base: 1, md: 0 }} ml={{ base: 1, md: 0 }}
...@@ -228,11 +233,15 @@ const MarketplaceAppCard = ({ ...@@ -228,11 +233,15 @@ const MarketplaceAppCard = ({
isLoading={ isLoading } isLoading={ isLoading }
source="Discovery view" source="Discovery view"
popoverPlacement={ isMobile ? 'bottom-end' : 'left' } popoverPlacement={ isMobile ? 'bottom-end' : 'left' }
position="absolute" triggerWrapperProps={{
right={{ base: 3, md: 5 }} position: 'absolute',
top={{ base: '10px', md: 5 }} right: { base: 3, md: 5 },
border={ 0 } top: { base: '10px', md: 5 },
padding={ 0 } }}
buttonProps={{
border: 0,
padding: 0,
}}
/> />
) } ) }
</Flex> </Flex>
......
...@@ -19,7 +19,7 @@ const MarketplaceAppCardLink = ({ url, external, id, title, onClick, className } ...@@ -19,7 +19,7 @@ const MarketplaceAppCardLink = ({ url, external, id, title, onClick, className }
}, [ onClick, id ]); }, [ onClick, id ]);
return external ? ( return external ? (
<LinkOverlay href={ url } isExternal={ true } marginRight={ 2 } className={ className }> <LinkOverlay href={ url } marginRight={ 2 } className={ className }>
{ title } { title }
</LinkOverlay> </LinkOverlay>
) : ( ) : (
......
import { import {
Text, Text,
PopoverTrigger,
PopoverBody,
PopoverContent,
chakra, chakra,
Box, Box,
VStack, VStack,
...@@ -10,9 +7,9 @@ import { ...@@ -10,9 +7,9 @@ import {
import React from 'react'; import React from 'react';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import Popover from 'ui/shared/chakra/Popover'; import { Link } from 'toolkit/chakra/link';
import { Tooltip } from 'toolkit/chakra/tooltip';
import IconSvg from 'ui/shared/IconSvg'; import IconSvg from 'ui/shared/IconSvg';
import LinkExternal from 'ui/shared/links/LinkExternal';
interface Props { interface Props {
className?: string; className?: string;
...@@ -30,27 +27,25 @@ const MarketplaceAppGraphLinks = ({ className, links }: Props) => { ...@@ -30,27 +27,25 @@ const MarketplaceAppGraphLinks = ({ className, links }: Props) => {
return null; return null;
} }
const content = (
<VStack gap={ 4 } align="start" textStyle="sm" w="260px">
<Text>{ `This dapp uses ${ links.length > 1 ? 'several subgraphs' : 'a subgraph' } powered by The Graph` }</Text>
{ links.map(link => (
<Link external key={ link.url } href={ link.url }>{ link.title }</Link>
)) }
</VStack>
);
return ( return (
<Box position="relative" className={ className } display="inline-flex" alignItems="center" height={ 7 } onClick={ handleButtonClick }> <Box position="relative" className={ className } display="inline-flex" alignItems="center" onClick={ handleButtonClick }>
<Popover <Tooltip
placement={ isMobile ? 'bottom-end' : 'bottom' } variant="popover"
isLazy content={ content }
trigger="hover" positioning={{ placement: isMobile ? 'bottom-end' : 'bottom' }}
interactive
> >
<PopoverTrigger> <IconSvg name="brands/graph" boxSize={ 5 } onClick={ handleButtonClick }/>
<IconSvg name="brands/graph" boxSize={ 5 } onClick={ handleButtonClick }/> </Tooltip>
</PopoverTrigger>
<PopoverContent w="260px">
<PopoverBody fontSize="sm">
<VStack gap={ 4 } align="start">
<Text>{ `This dapp uses ${ links.length > 1 ? 'several subgraphs' : 'a subgraph' } powered by The Graph` }</Text>
{ links.map(link => (
<LinkExternal key={ link.url } href={ link.url }>{ link.title }</LinkExternal>
)) }
</VStack>
</PopoverBody>
</PopoverContent>
</Popover>
</Box> </Box>
); );
}; };
......
import { Tooltip } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import { Tooltip } from 'toolkit/chakra/tooltip';
import type { IconName } from 'ui/shared/IconSvg'; import type { IconName } from 'ui/shared/IconSvg';
import IconSvg from 'ui/shared/IconSvg'; import IconSvg from 'ui/shared/IconSvg';
...@@ -32,11 +32,9 @@ const MarketplaceAppIntegrationIcon = ({ external, internalWallet }: Props) => { ...@@ -32,11 +32,9 @@ const MarketplaceAppIntegrationIcon = ({ external, internalWallet }: Props) => {
return ( return (
<Tooltip <Tooltip
label={ text } content={ text }
textAlign="center"
padding={ 2 }
openDelay={ 300 } openDelay={ 300 }
maxW={{ base: 'calc(100vw - 8px)', lg: '400px' }} contentProps={{ maxW: { base: 'calc(100vw - 8px)', lg: '400px' } }}
> >
<IconSvg <IconSvg
name={ icon } name={ icon }
...@@ -45,7 +43,6 @@ const MarketplaceAppIntegrationIcon = ({ external, internalWallet }: Props) => { ...@@ -45,7 +43,6 @@ const MarketplaceAppIntegrationIcon = ({ external, internalWallet }: Props) => {
position="relative" position="relative"
cursor="pointer" cursor="pointer"
verticalAlign="middle" verticalAlign="middle"
mb={{ base: 0, md: 1 }}
/> />
</Tooltip> </Tooltip>
); );
......
import { import { Box, Flex, Text } from '@chakra-ui/react';
Box, Flex, Heading, IconButton, Image, Link, Modal, ModalBody,
ModalCloseButton, ModalContent, ModalFooter, ModalOverlay, Tag, Text, useColorModeValue,
} from '@chakra-ui/react';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import type { MarketplaceAppWithSecurityReport, AppRating } from 'types/client/marketplace'; import type { MarketplaceAppWithSecurityReport, AppRating } from 'types/client/marketplace';
import { ContractListTypes } from 'types/client/marketplace'; import { ContractListTypes } from 'types/client/marketplace';
import { route } from 'nextjs-routes';
import config from 'configs/app'; import config from 'configs/app';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import { nbsp } from 'lib/html-entities'; import { nbsp } from 'lib/html-entities';
import isBrowser from 'lib/isBrowser'; import isBrowser from 'lib/isBrowser';
import * as mixpanel from 'lib/mixpanel/index'; import * as mixpanel from 'lib/mixpanel/index';
import { Badge } from 'toolkit/chakra/badge';
import { Button } from 'toolkit/chakra/button';
import { useColorModeValue } from 'toolkit/chakra/color-mode';
import { DialogBody, DialogCloseTrigger, DialogContent, DialogFooter, DialogRoot } from 'toolkit/chakra/dialog';
import { Heading } from 'toolkit/chakra/heading';
import { IconButton } from 'toolkit/chakra/icon-button';
import { Image } from 'toolkit/chakra/image';
import { Link } from 'toolkit/chakra/link';
import CopyToClipboard from 'ui/shared/CopyToClipboard'; import CopyToClipboard from 'ui/shared/CopyToClipboard';
import type { IconName } from 'ui/shared/IconSvg'; import type { IconName } from 'ui/shared/IconSvg';
import IconSvg from 'ui/shared/IconSvg'; import IconSvg from 'ui/shared/IconSvg';
...@@ -20,7 +27,6 @@ import AppSecurityReport from './AppSecurityReport'; ...@@ -20,7 +27,6 @@ import AppSecurityReport from './AppSecurityReport';
import FavoriteIcon from './FavoriteIcon'; import FavoriteIcon from './FavoriteIcon';
import MarketplaceAppGraphLinks from './MarketplaceAppGraphLinks'; import MarketplaceAppGraphLinks from './MarketplaceAppGraphLinks';
import MarketplaceAppIntegrationIcon from './MarketplaceAppIntegrationIcon'; import MarketplaceAppIntegrationIcon from './MarketplaceAppIntegrationIcon';
import MarketplaceAppModalLink from './MarketplaceAppModalLink';
import Rating from './Rating/Rating'; import Rating from './Rating/Rating';
import type { RateFunction } from './Rating/useRatings'; import type { RateFunction } from './Rating/useRatings';
...@@ -38,7 +44,7 @@ type Props = { ...@@ -38,7 +44,7 @@ type Props = {
isRatingSending: boolean; isRatingSending: boolean;
isRatingLoading: boolean; isRatingLoading: boolean;
canRate: boolean | undefined; canRate: boolean | undefined;
graphLinks?: Array<{ text: string; url: string }>; graphLinks?: Array<{ title: string; url: string }>;
}; };
const MarketplaceAppModal = ({ const MarketplaceAppModal = ({
...@@ -97,14 +103,23 @@ const MarketplaceAppModal = ({ ...@@ -97,14 +103,23 @@ const MarketplaceAppModal = ({
} }
} }
const handleOpenChange = React.useCallback(({ open }: { open: boolean }) => {
if (!open) {
onClose();
}
}, [ onClose ]);
const handleFavoriteClick = useCallback(() => { const handleFavoriteClick = useCallback(() => {
onFavoriteClick(id, isFavorite, 'App modal'); onFavoriteClick(id, isFavorite, 'App modal');
}, [ onFavoriteClick, id, isFavorite ]); }, [ onFavoriteClick, id, isFavorite ]);
const showContractList = useCallback((id: string, type: ContractListTypes) => { const showContractList = useCallback((id: string, type: ContractListTypes) => {
onClose(); onClose();
showContractListProp(id, type, true); // FIXME: This is a workaround to avoid the dialog closing before the modal is opened
}, [ onClose, showContractListProp ]); window.setTimeout(() => {
showContractListProp(id, type, true);
}, 100);
}, [ showContractListProp, onClose ]);
const showAllContracts = React.useCallback(() => { const showAllContracts = React.useCallback(() => {
mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'Total contracts', Info: id, Source: 'App modal' }); mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'Total contracts', Info: id, Source: 'App modal' });
...@@ -120,22 +135,19 @@ const MarketplaceAppModal = ({ ...@@ -120,22 +135,19 @@ const MarketplaceAppModal = ({
} catch (err) {} } catch (err) {}
} }
const iconColor = useColorModeValue('blue.600', 'gray.400'); const iconColor = { _light: 'blue.600', _dark: 'gray.400' };
return ( return (
<Modal <DialogRoot
isOpen={ Boolean(data.id) } open={ Boolean(data.id) }
onClose={ onClose } onOpenChange={ handleOpenChange }
size={ isMobile ? 'full' : 'md' } size={{ lgDown: 'full', lg: 'md' }}
isCentered placement="center"
> >
<ModalOverlay/> <DialogContent>
<ModalContent>
<Box <Box
display="grid" display="grid"
gridTemplateColumns={{ base: 'auto 1fr' }} gridTemplateColumns={{ base: 'auto 1fr' }}
paddingRight={{ md: 12 }}
marginBottom={{ base: 6, md: 8 }} marginBottom={{ base: 6, md: 8 }}
> >
<Flex <Flex
...@@ -155,24 +167,22 @@ const MarketplaceAppModal = ({ ...@@ -155,24 +167,22 @@ const MarketplaceAppModal = ({
<Flex alignItems="center" mb={{ md: 2 }} gridColumn={ 2 }> <Flex alignItems="center" mb={{ md: 2 }} gridColumn={ 2 }>
<Heading <Heading
as="h2" level="2"
fontSize={{ base: '2xl', md: '32px' }}
fontWeight="medium" fontWeight="medium"
lineHeight={{ md: 10 }}
mr={ 2 } mr={ 2 }
> >
{ title } { title }
</Heading> </Heading>
<MarketplaceAppIntegrationIcon external={ external } internalWallet={ internalWallet }/> <MarketplaceAppIntegrationIcon external={ external } internalWallet={ internalWallet }/>
<MarketplaceAppGraphLinks links={ graphLinks } ml={ 2 }/> <MarketplaceAppGraphLinks links={ graphLinks } ml={ 2 }/>
<DialogCloseTrigger ml="auto"/>
</Flex> </Flex>
<Text <Text
variant="secondary" color="text.secondary"
gridColumn={ 2 } gridColumn={ 2 }
fontSize={{ base: 'sm', md: 'md' }} textStyle={{ base: 'sm', md: 'md' }}
fontWeight="normal" fontWeight="normal"
lineHeight={{ md: 6 }}
> >
By{ nbsp }{ author } By{ nbsp }{ author }
</Text> </Text>
...@@ -204,31 +214,28 @@ const MarketplaceAppModal = ({ ...@@ -204,31 +214,28 @@ const MarketplaceAppModal = ({
> >
<Flex flexWrap="wrap" gap={ 6 }> <Flex flexWrap="wrap" gap={ 6 }>
<Flex width={{ base: '100%', md: 'auto' }}> <Flex width={{ base: '100%', md: 'auto' }}>
<MarketplaceAppModalLink <Link href={ external ? url : route({ pathname: '/apps/[id]', query: { id: data.id } }) } external={ external } noIcon>
id={ data.id } <Button size="sm" mr={ 2 } w={{ base: '100%', sm: 'auto' }}>
url={ url } Launch app
external={ external } </Button>
title={ title } </Link>
/>
<IconButton <IconButton
aria-label="Mark as favorite" aria-label="Mark as favorite"
title="Mark as favorite" title="Mark as favorite"
variant="outline" variant="outline"
colorScheme="gray"
w={ 9 } w={ 9 }
h={ 8 } h={ 8 }
flexShrink={ 0 } flexShrink={ 0 }
onClick={ handleFavoriteClick } onClick={ handleFavoriteClick }
icon={ <FavoriteIcon isFavorite={ isFavorite } color={ iconColor }/> } >
/> <FavoriteIcon isFavorite={ isFavorite } color={ iconColor }/>
</IconButton>
<CopyToClipboard <CopyToClipboard
text={ isBrowser() ? window.location.origin + `/apps/${ id }` : '' } text={ isBrowser() ? window.location.origin + `/apps/${ id }` : '' }
icon="share" type="share"
size={ 4 }
variant="outline" variant="outline"
colorScheme="gray"
w={ 9 } w={ 9 }
h={ 8 } h={ 8 }
color={ iconColor } color={ iconColor }
...@@ -241,9 +248,7 @@ const MarketplaceAppModal = ({ ...@@ -241,9 +248,7 @@ const MarketplaceAppModal = ({
</Box> </Box>
</Box> </Box>
<ModalCloseButton/> <DialogBody mb={ 6 }>
<ModalBody mb={ 6 }>
{ securityReport && ( { securityReport && (
<Flex <Flex
direction={{ base: 'column', md: 'row' }} direction={{ base: 'column', md: 'row' }}
...@@ -275,34 +280,34 @@ const MarketplaceAppModal = ({ ...@@ -275,34 +280,34 @@ const MarketplaceAppModal = ({
</Flex> </Flex>
) } ) }
<Text>{ description }</Text> <Text>{ description }</Text>
</ModalBody> </DialogBody>
<ModalFooter <DialogFooter
display="flex" display="flex"
flexDirection={{ base: 'column', md: 'row' }} flexDirection={{ base: 'column', md: 'row' }}
justifyContent={{ base: 'flex-start', md: 'space-between' }} justifyContent={{ base: 'flex-start', md: 'space-between' }}
alignItems={{ base: 'flex-start', md: 'center' }} alignItems="flex-start"
gap={ 3 } gap={ 3 }
> >
<Flex gap={ 2 }> <Flex gap={ 2 } flexWrap="wrap">
{ categories.map((category) => ( { categories.map((category) => (
<Tag <Badge
colorScheme="blue" colorPalette="blue"
key={ category } key={ category }
> >
{ category } { category }
</Tag> </Badge>
)) } )) }
</Flex> </Flex>
<Flex alignItems="center" gap={ 3 }> <Flex alignItems="center" gap={ 3 } my="2px">
{ site && ( { site && (
<Link <Link
isExternal external
href={ site } href={ site }
display="flex" display="flex"
alignItems="center" alignItems="center"
fontSize="sm" textStyle="sm"
> >
<IconSvg <IconSvg
name="link" name="link"
...@@ -332,23 +337,20 @@ const MarketplaceAppModal = ({ ...@@ -332,23 +337,20 @@ const MarketplaceAppModal = ({
display="flex" display="flex"
alignItems="center" alignItems="center"
justifyContent="center" justifyContent="center"
isExternal external
w={ 5 } flexShrink={ 0 }
h={ 5 }
> >
<IconSvg <IconSvg
name={ icon } name={ icon }
w="20px" color="text.secondary"
h="20px" boxSize={ 5 }
display="block"
color="text_secondary"
/> />
</Link> </Link>
)) } )) }
</Flex> </Flex>
</ModalFooter> </DialogFooter>
</ModalContent> </DialogContent>
</Modal> </DialogRoot>
); );
}; };
......
import { Button } from '@chakra-ui/react';
import React from 'react';
import NextLink from 'ui/shared/NextLink';
type Props = {
id: string;
url: string;
external?: boolean;
title: string;
};
const MarketplaceAppModalLink = ({ url, external, id }: Props) => {
const buttonProps = {
size: 'sm',
marginRight: 2,
width: { base: '100%', sm: 'auto' },
...(external ? {
target: '_blank',
rel: 'noopener noreferrer',
} : {}),
};
return external ? (
<Button
as="a"
href={ url }
{ ...buttonProps }
>Launch app</Button>
) : (
<NextLink href={{ pathname: '/apps/[id]', query: { id } }} passHref legacyBehavior>
<Button
as="a"
{ ...buttonProps }
>Launch app</Button>
</NextLink>
);
};
export default MarketplaceAppModalLink;
import { Heading, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader, ModalOverlay, Text, Button, useColorModeValue } from '@chakra-ui/react'; import { Text } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import { route } from 'nextjs-routes';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import NextLink from 'ui/shared/NextLink'; import { Button } from 'toolkit/chakra/button';
import { DialogBody, DialogContent, DialogFooter, DialogHeader, DialogRoot } from 'toolkit/chakra/dialog';
import { Link } from 'toolkit/chakra/link';
type Props = { type Props = {
isOpen: boolean; isOpen: boolean;
...@@ -19,62 +23,45 @@ const MarketplaceDisclaimerModal = ({ isOpen, onClose, appId }: Props) => { ...@@ -19,62 +23,45 @@ const MarketplaceDisclaimerModal = ({ isOpen, onClose, appId }: Props) => {
}, [ ]); }, [ ]);
return ( return (
<Modal <DialogRoot
isOpen={ isOpen } open={ isOpen }
onClose={ onClose } onOpenChange={ onClose }
size={ isMobile ? 'full' : 'md' } size={ isMobile ? 'full' : 'md' }
isCentered placement="center"
> >
<ModalOverlay/> <DialogContent>
<DialogHeader>
<ModalContent> Disclaimer
<ModalHeader> </DialogHeader>
<Heading
as="h2"
fontSize="2xl"
fontWeight="medium"
lineHeight={ 1 }
color={ useColorModeValue('blackAlpha.800', 'whiteAlpha.800') }
>
Disclaimer
</Heading>
</ModalHeader>
<ModalBody> <DialogBody>
<Text color={ useColorModeValue('gray.800', 'whiteAlpha.800') }> <Text color={{ _light: 'gray.800', _dark: 'whiteAlpha.800' }}>
You are now accessing a third-party app. Blockscout does not own, control, maintain, or audit 3rd party apps,{ ' ' } You are now accessing a third-party app. Blockscout does not own, control, maintain, or audit 3rd party apps,{ ' ' }
and is not liable for any losses associated with these interactions. Please do so at your own risk. and is not liable for any losses associated with these interactions. Please do so at your own risk.
<br/><br/> <br/><br/>
By clicking continue, you agree that you understand the risks and have read the Disclaimer. By clicking continue, you agree that you understand the risks and have read the Disclaimer.
</Text> </Text>
</ModalBody> </DialogBody>
<ModalFooter <DialogFooter
display="flex" display="flex"
flexDirection="row" flexDirection="row"
alignItems="center" alignItems="center"
> >
<NextLink href={{ pathname: '/apps/[id]', query: { id: appId } }} passHref legacyBehavior> <Link href={ route({ pathname: '/apps/[id]', query: { id: appId } }) } asChild>
<Button <Button onClick={ handleContinueClick } >
variant="solid"
colorScheme="blue"
mr={ 6 }
py="10px"
onClick={ handleContinueClick }
>
Continue to app Continue to app
</Button> </Button>
</NextLink> </Link>
<Button <Button
variant="outline" variant="outline"
colorScheme="blue"
onClick={ onClose } onClick={ onClose }
> >
Cancel Cancel
</Button> </Button>
</ModalFooter> </DialogFooter>
</ModalContent> </DialogContent>
</Modal> </DialogRoot>
); );
}; };
......
...@@ -26,7 +26,7 @@ type Props = { ...@@ -26,7 +26,7 @@ type Props = {
isRatingSending: boolean; isRatingSending: boolean;
isRatingLoading: boolean; isRatingLoading: boolean;
canRate: boolean | undefined; canRate: boolean | undefined;
graphLinksQuery: UseQueryResult<Record<string, Array<{ text: string; url: string }>>, unknown>; graphLinksQuery: UseQueryResult<Record<string, Array<{ title: string; url: string }>>, unknown>;
}; };
const MarketplaceList = ({ const MarketplaceList = ({
...@@ -61,6 +61,8 @@ const MarketplaceList = ({ ...@@ -61,6 +61,8 @@ const MarketplaceList = ({
external={ app.external } external={ app.external }
url={ app.url } url={ app.url }
title={ app.title } title={ app.title }
description={ app.description }
author={ app.author }
logo={ app.logo } logo={ app.logo }
logoDarkMode={ app.logoDarkMode } logoDarkMode={ app.logoDarkMode }
shortDescription={ app.shortDescription } shortDescription={ app.shortDescription }
......
...@@ -57,7 +57,7 @@ const PopoverContent = ({ appId, rating, userRating, rate, isSending, source }: ...@@ -57,7 +57,7 @@ const PopoverContent = ({ appId, rating, userRating, rate, isSending, source }:
{ userRating && ( { userRating && (
<IconSvg name="verified" color="green.400" boxSize="30px" mr={ 1 } ml="-5px"/> <IconSvg name="verified" color="green.400" boxSize="30px" mr={ 1 } ml="-5px"/>
) } ) }
<Text fontWeight="500" fontSize="xs" lineHeight="30px" variant="secondary"> <Text fontWeight="500" textStyle="xs" color="text.secondary">
{ userRating ? 'App is already rated by you' : 'How was your experience?' } { userRating ? 'App is already rated by you' : 'How was your experience?' }
</Text> </Text>
</Flex> </Flex>
......
import { Text, PopoverTrigger, PopoverBody, PopoverContent, useDisclosure, useOutsideClick, Box } from '@chakra-ui/react'; import { Text } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { AppRating } from 'types/client/marketplace'; import type { AppRating } from 'types/client/marketplace';
import config from 'configs/app'; import config from 'configs/app';
import type { EventTypes, EventPayload } from 'lib/mixpanel/index'; import type { EventTypes, EventPayload } from 'lib/mixpanel/index';
import Popover from 'ui/shared/chakra/Popover'; import { PopoverBody, PopoverContent, PopoverRoot } from 'toolkit/chakra/popover';
import Skeleton from 'ui/shared/chakra/Skeleton'; import { Skeleton } from 'toolkit/chakra/skeleton';
import Content from './PopoverContent'; import Content from './PopoverContent';
import Stars from './Stars'; import Stars from './Stars';
...@@ -32,10 +32,6 @@ const Rating = ({ ...@@ -32,10 +32,6 @@ const Rating = ({
appId, rating, userRating, rate, appId, rating, userRating, rate,
isSending, isLoading, fullView, canRate, source, isSending, isLoading, fullView, canRate, source,
}: Props) => { }: Props) => {
const { isOpen, onToggle, onClose } = useDisclosure();
// have to implement this solution because popover loses focus on button click inside it (issue: https://github.com/chakra-ui/chakra-ui/issues/7359)
const popoverRef = React.useRef(null);
useOutsideClick({ ref: popoverRef, handler: onClose });
if (!isEnabled) { if (!isEnabled) {
return null; return null;
...@@ -45,30 +41,27 @@ const Rating = ({ ...@@ -45,30 +41,27 @@ const Rating = ({
<Skeleton <Skeleton
display="flex" display="flex"
alignItems="center" alignItems="center"
isLoaded={ !isLoading } loading={ isLoading }
w={ (isLoading && !fullView) ? '40px' : 'auto' } w={ (isLoading && !fullView) ? '40px' : 'auto' }
> >
{ fullView && ( { fullView && (
<> <>
<Stars filledIndex={ (rating?.value || 0) - 1 }/> <Stars filledIndex={ (rating?.value || 0) - 1 }/>
<Text fontSize="md" ml={ 2 }>{ rating?.value }</Text> <Text fontSize="md" ml={ 2 }>{ rating?.value }</Text>
{ rating?.count && <Text variant="secondary" fontSize="md" ml={ 1 }>({ rating?.count })</Text> } { rating?.count && <Text color="text.secondary" textStyle="md" ml={ 1 }>({ rating?.count })</Text> }
</> </>
) } ) }
<Box ref={ popoverRef }> <PopoverRoot positioning={{ placement: 'bottom' }}>
<Popover isOpen={ isOpen } placement="bottom" isLazy>
<PopoverTrigger> <TriggerButton
<TriggerButton rating={ rating?.value }
rating={ rating?.value } count={ rating?.count }
count={ rating?.count } fullView={ fullView }
fullView={ fullView } canRate={ canRate }
isActive={ isOpen } />
onClick={ onToggle } { canRate ? (
canRate={ canRate } <PopoverContent w="250px">
/> <PopoverBody>
</PopoverTrigger>
<PopoverContent w="250px" mx={ 3 }>
<PopoverBody p={ 4 }>
<Content <Content
appId={ appId } appId={ appId }
rating={ rating } rating={ rating }
...@@ -79,8 +72,8 @@ const Rating = ({ ...@@ -79,8 +72,8 @@ const Rating = ({
/> />
</PopoverBody> </PopoverBody>
</PopoverContent> </PopoverContent>
</Popover> ) : <PopoverContent/> }
</Box> </PopoverRoot>
</Skeleton> </Skeleton>
); );
}; };
......
import { Flex, useColorModeValue } from '@chakra-ui/react'; import { Flex } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { MouseEventHandler } from 'react'; import type { MouseEventHandler } from 'react';
...@@ -12,8 +12,7 @@ type Props = { ...@@ -12,8 +12,7 @@ type Props = {
}; };
const Stars = ({ filledIndex, onMouseOverFactory, onMouseOut, onClickFactory }: Props) => { const Stars = ({ filledIndex, onMouseOverFactory, onMouseOut, onClickFactory }: Props) => {
const disabledStarColor = useColorModeValue('gray.200', 'gray.700'); const outlineStartColor = onMouseOverFactory ? 'gray.400' : { _light: 'gray.200', _dark: 'gray.700' };
const outlineStartColor = onMouseOverFactory ? 'gray.400' : disabledStarColor;
return ( return (
<Flex> <Flex>
{ Array(5).fill(null).map((_, index) => ( { Array(5).fill(null).map((_, index) => (
......
import { Button, chakra, useColorModeValue, Tooltip, useDisclosure, Text } from '@chakra-ui/react'; import { chakra, Text } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import usePreventFocusAfterModalClosing from 'lib/hooks/usePreventFocusAfterModalClosing'; import usePreventFocusAfterModalClosing from 'lib/hooks/usePreventFocusAfterModalClosing';
import type { ButtonProps } from 'toolkit/chakra/button';
import { Button } from 'toolkit/chakra/button';
import { PopoverTrigger } from 'toolkit/chakra/popover';
import { Tooltip } from 'toolkit/chakra/tooltip';
import IconSvg from 'ui/shared/IconSvg'; import IconSvg from 'ui/shared/IconSvg';
type Props = { interface Props extends ButtonProps {
rating?: number; rating?: number;
count?: number; count?: number;
fullView?: boolean; fullView?: boolean;
isActive: boolean;
onClick: () => void;
canRate: boolean | undefined; canRate: boolean | undefined;
}; };
...@@ -25,66 +27,54 @@ const getTooltipText = (canRate: boolean | undefined) => { ...@@ -25,66 +27,54 @@ const getTooltipText = (canRate: boolean | undefined) => {
}; };
const TriggerButton = ( const TriggerButton = (
{ rating, count, fullView, isActive, onClick, canRate }: Props, { rating, count, fullView, canRate, onClick, ...rest }: Props,
ref: React.ForwardedRef<HTMLButtonElement>, ref: React.ForwardedRef<HTMLDivElement>,
) => { ) => {
const textColor = useColorModeValue('blackAlpha.800', 'whiteAlpha.800');
const onFocusCapture = usePreventFocusAfterModalClosing(); const onFocusCapture = usePreventFocusAfterModalClosing();
// TODO @tom2drum remove all such occurrences of useDisclosure
// have to implement controlled tooltip on mobile because of the issue - https://github.com/chakra-ui/chakra-ui/issues/7107
const { isOpen, onToggle, onClose } = useDisclosure();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const handleClick = React.useCallback(() => {
if (canRate) {
onClick();
} else if (isMobile) {
onToggle();
}
}, [ canRate, isMobile, onToggle, onClick ]);
return ( return (
<Tooltip <Tooltip
label={ getTooltipText(canRate) } content={ getTooltipText(canRate) }
openDelay={ 100 }
textAlign="center"
closeOnClick={ Boolean(canRate) || isMobile } closeOnClick={ Boolean(canRate) || isMobile }
isOpen={ isMobile ? isOpen : undefined } disableOnMobile={ canRate }
> >
<Button <div>
ref={ ref } <PopoverTrigger>
size="xs" <Button
variant="outline" ref={ ref }
border={ 0 } size="xs"
p={ 0 } variant="outline"
onClick={ handleClick } border={ 0 }
fontSize={ fullView ? 'md' : 'sm' } p={ 0 }
fontWeight={ fullView ? '400' : '500' } fontSize={ fullView ? 'md' : 'sm' }
lineHeight="21px" fontWeight={ fullView ? '400' : '500' }
ml={ fullView ? 3 : 0 } lineHeight="21px"
isActive={ isActive } ml={ fullView ? 3 : 0 }
onFocusCapture={ onFocusCapture } onFocusCapture={ onFocusCapture }
cursor={ canRate ? 'pointer' : 'default' } cursor={ canRate ? 'pointer' : 'default' }
onMouseLeave={ isMobile ? onClose : undefined } { ...rest }
> >
{ !fullView && ( { !fullView && (
<IconSvg <IconSvg
name={ rating ? 'star_filled' : 'star_outline' } name={ rating ? 'star_filled' : 'star_outline' }
color={ rating ? 'yellow.400' : 'gray.400' } color={ rating ? 'yellow.400' : 'gray.400' }
boxSize={ 5 } boxSize={ 5 }
mr={ 1 } mr={ 1 }
/> />
) } ) }
{ (rating && !fullView) ? ( { (rating && !fullView) ? (
<chakra.span color={ textColor } transition="inherit" display="inline-flex"> <chakra.span color={{ _light: 'blackAlpha.800', _dark: 'whiteAlpha.800' }} transition="inherit" display="inline-flex">
{ rating } { rating }
<Text variant="secondary" ml={ 1 }>({ count })</Text> <Text color="text.secondary" ml={ 1 }>({ count })</Text>
</chakra.span> </chakra.span>
) : ( ) : (
'Rate it!' 'Rate it!'
) } ) }
</Button> </Button>
</PopoverTrigger>
</div>
</Tooltip> </Tooltip>
); );
}; };
......
...@@ -6,10 +6,10 @@ import type { AppRating } from 'types/client/marketplace'; ...@@ -6,10 +6,10 @@ import type { AppRating } from 'types/client/marketplace';
import config from 'configs/app'; import config from 'configs/app';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import useToast from 'lib/hooks/useToast';
import type { EventTypes, EventPayload } from 'lib/mixpanel/index'; import type { EventTypes, EventPayload } from 'lib/mixpanel/index';
import * as mixpanel from 'lib/mixpanel/index'; import * as mixpanel from 'lib/mixpanel/index';
import { ADDRESS_COUNTERS } from 'stubs/address'; import { ADDRESS_COUNTERS } from 'stubs/address';
import { toaster } from 'toolkit/chakra/toaster';
const MIN_TRANSACTION_COUNT = 5; const MIN_TRANSACTION_COUNT = 5;
...@@ -41,7 +41,6 @@ function formatRatings(data: Airtable.Records<Airtable.FieldSet>) { ...@@ -41,7 +41,6 @@ function formatRatings(data: Airtable.Records<Airtable.FieldSet>) {
export default function useRatings() { export default function useRatings() {
const { address } = useAccount(); const { address } = useAccount();
const toast = useToast();
const addressCountersQuery = useApiQuery<'address_counters', { status: number }>('address_counters', { const addressCountersQuery = useApiQuery<'address_counters', { status: number }>('address_counters', {
pathParams: { hash: address }, pathParams: { hash: address },
...@@ -68,13 +67,12 @@ export default function useRatings() { ...@@ -68,13 +67,12 @@ export default function useRatings() {
const ratings = formatRatings(data); const ratings = formatRatings(data);
setRatings(ratings); setRatings(ratings);
} catch (error) { } catch (error) {
toast({ toaster.error({
status: 'error',
title: 'Error loading ratings', title: 'Error loading ratings',
description: 'Please try again later', description: 'Please try again later',
}); });
} }
}, [ toast ]); }, [ ]);
useEffect(() => { useEffect(() => {
async function fetch() { async function fetch() {
...@@ -97,8 +95,7 @@ export default function useRatings() { ...@@ -97,8 +95,7 @@ export default function useRatings() {
}).all(); }).all();
userRatings = formatRatings(data); userRatings = formatRatings(data);
} catch (error) { } catch (error) {
toast({ toaster.error({
status: 'error',
title: 'Error loading user ratings', title: 'Error loading user ratings',
description: 'Please try again later', description: 'Please try again later',
}); });
...@@ -108,7 +105,7 @@ export default function useRatings() { ...@@ -108,7 +105,7 @@ export default function useRatings() {
setIsUserRatingLoading(false); setIsUserRatingLoading(false);
} }
fetchUserRatings(); fetchUserRatings();
}, [ address, toast ]); }, [ address ]);
useEffect(() => { useEffect(() => {
const isPlaceholderData = addressCountersQuery?.isPlaceholderData; const isPlaceholderData = addressCountersQuery?.isPlaceholderData;
...@@ -163,8 +160,7 @@ export default function useRatings() { ...@@ -163,8 +160,7 @@ export default function useRatings() {
}); });
fetchRatings(); fetchRatings();
toast({ toaster.success({
status: 'success',
title: 'Awesome! Thank you 💜', title: 'Awesome! Thank you 💜',
description: 'Your rating improves the service', description: 'Your rating improves the service',
}); });
...@@ -173,15 +169,14 @@ export default function useRatings() { ...@@ -173,15 +169,14 @@ export default function useRatings() {
{ Action: 'Rating', Source: source, AppId: appId, Score: rating }, { Action: 'Rating', Source: source, AppId: appId, Score: rating },
); );
} catch (error) { } catch (error) {
toast({ toaster.error({
status: 'error',
title: 'Ooops! Something went wrong', title: 'Ooops! Something went wrong',
description: 'Please try again later', description: 'Please try again later',
}); });
} }
setIsSending(false); setIsSending(false);
}, [ address, userRatings, fetchRatings, toast ]); }, [ address, userRatings, fetchRatings ]);
return { return {
ratings, ratings,
......
...@@ -7,10 +7,10 @@ import type { SelectOption } from 'toolkit/chakra/select'; ...@@ -7,10 +7,10 @@ import type { SelectOption } from 'toolkit/chakra/select';
const feature = config.features.marketplace; const feature = config.features.marketplace;
export type SortValue = 'rating_score' | 'rating_count' | 'security_score'; export type SortValue = 'default' | 'rating_score' | 'rating_count' | 'security_score';
export const SORT_OPTIONS: Array<SelectOption<SortValue>> = [ export const SORT_OPTIONS: Array<SelectOption<SortValue>> = [
{ label: 'Default', value: undefined }, { label: 'Default', value: 'default' },
(feature.isEnabled && feature.rating) && { label: 'Top rated', value: 'rating_score' }, (feature.isEnabled && feature.rating) && { label: 'Top rated', value: 'rating_score' },
(feature.isEnabled && feature.rating) && { label: 'Most rated', value: 'rating_count' }, (feature.isEnabled && feature.rating) && { label: 'Most rated', value: 'rating_count' },
(feature.isEnabled && feature.securityReportsUrl) && { label: 'Security score', value: 'security_score' }, (feature.isEnabled && feature.securityReportsUrl) && { label: 'Security score', value: 'security_score' },
......
import { MenuButton, MenuItem, MenuList, Flex, IconButton } from '@chakra-ui/react'; import { createListCollection, Flex } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { MouseEvent } from 'react'; import type { MouseEvent } from 'react';
import type { TabItemRegular } from 'toolkit/components/AdaptiveTabs/types';
import { MarketplaceCategory } from 'types/client/marketplace'; import { MarketplaceCategory } from 'types/client/marketplace';
import type { TabItem } from 'ui/shared/Tabs/types';
import config from 'configs/app'; import config from 'configs/app';
import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError'; import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError';
import useGraphLinks from 'lib/hooks/useGraphLinks'; import useGraphLinks from 'lib/hooks/useGraphLinks';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import { IconButton } from 'toolkit/chakra/icon-button';
import { Link } from 'toolkit/chakra/link';
import { MenuContent, MenuItem, MenuRoot, MenuTrigger } from 'toolkit/chakra/menu';
import AdaptiveTabs from 'toolkit/components/AdaptiveTabs/AdaptiveTabs';
import Banner from 'ui/marketplace/Banner'; import Banner from 'ui/marketplace/Banner';
import ContractListModal from 'ui/marketplace/ContractListModal'; import ContractListModal from 'ui/marketplace/ContractListModal';
import MarketplaceAppModal from 'ui/marketplace/MarketplaceAppModal'; import MarketplaceAppModal from 'ui/marketplace/MarketplaceAppModal';
import MarketplaceDisclaimerModal from 'ui/marketplace/MarketplaceDisclaimerModal'; import MarketplaceDisclaimerModal from 'ui/marketplace/MarketplaceDisclaimerModal';
import MarketplaceList from 'ui/marketplace/MarketplaceList'; import MarketplaceList from 'ui/marketplace/MarketplaceList';
import type { SortValue } from 'ui/marketplace/utils';
import { SORT_OPTIONS } from 'ui/marketplace/utils'; import { SORT_OPTIONS } from 'ui/marketplace/utils';
import ActionBar from 'ui/shared/ActionBar'; import ActionBar from 'ui/shared/ActionBar';
import Menu from 'ui/shared/chakra/Menu';
import FilterInput from 'ui/shared/filters/FilterInput'; import FilterInput from 'ui/shared/filters/FilterInput';
import IconSvg from 'ui/shared/IconSvg'; import IconSvg from 'ui/shared/IconSvg';
import type { IconName } from 'ui/shared/IconSvg'; import type { IconName } from 'ui/shared/IconSvg';
import LinkExternal from 'ui/shared/links/LinkExternal';
import PageTitle from 'ui/shared/Page/PageTitle'; import PageTitle from 'ui/shared/Page/PageTitle';
import Sort from 'ui/shared/sort/Sort'; import Sort from 'ui/shared/sort/Sort';
import TabsWithScroll from 'ui/shared/Tabs/TabsWithScroll';
import useMarketplace from '../marketplace/useMarketplace'; import useMarketplace from '../marketplace/useMarketplace';
const sortCollection = createListCollection({ items: SORT_OPTIONS });
const feature = config.features.marketplace; const feature = config.features.marketplace;
const links: Array<{ label: string; href: string; icon: IconName }> = []; const links: Array<{ label: string; href: string; icon: IconName }> = [];
...@@ -84,7 +89,7 @@ const Marketplace = () => { ...@@ -84,7 +89,7 @@ const Marketplace = () => {
const graphLinksQuery = useGraphLinks(); const graphLinksQuery = useGraphLinks();
const categoryTabs = React.useMemo(() => { const categoryTabs = React.useMemo(() => {
const tabs: Array<TabItem> = categories.map(category => ({ const tabs: Array<TabItemRegular> = categories.map(category => ({
id: category.name, id: category.name,
title: category.name, title: category.name,
count: category.count, count: category.count,
...@@ -108,15 +113,15 @@ const Marketplace = () => { ...@@ -108,15 +113,15 @@ const Marketplace = () => {
return tabs; return tabs;
}, [ categories, appsTotal, favoriteApps.length ]); }, [ categories, appsTotal, favoriteApps.length ]);
const selectedCategoryIndex = React.useMemo(() => { const selectedTabId = React.useMemo(() => {
const index = categoryTabs.findIndex(c => c.id === selectedCategoryId); const tab = categoryTabs.find(c => c.id === selectedCategoryId);
return index === -1 ? 0 : index; return typeof tab?.id === 'string' ? tab.id : undefined;
}, [ categoryTabs, selectedCategoryId ]); }, [ categoryTabs, selectedCategoryId ]);
const selectedApp = displayedApps.find(app => app.id === selectedAppId); const selectedApp = displayedApps.find(app => app.id === selectedAppId);
const handleCategoryChange = React.useCallback((index: number) => { const handleCategoryChange = React.useCallback(({ value }: { value: string }) => {
const tabId = categoryTabs[index].id; const tabId = categoryTabs.find(c => c.id === value)?.id;
if (typeof tabId === 'string') { if (typeof tabId === 'string') {
onCategoryChange(tabId); onCategoryChange(tabId);
} }
...@@ -137,6 +142,10 @@ const Marketplace = () => { ...@@ -137,6 +142,10 @@ const Marketplace = () => {
} }
}, [ clearSelectedAppId, showAppInfo, selectedApp ]); }, [ clearSelectedAppId, showAppInfo, selectedApp ]);
const handleSortChange = React.useCallback(({ value }: { value: Array<string> }) => {
setSorting(value[0] as SortValue);
}, [ setSorting ]);
throwOnResourceLoadError(isError && error ? { isError, error } : { isError: false, error: null }); throwOnResourceLoadError(isError && error ? { isError, error } : { isError: false, error: null });
if (!feature.isEnabled) { if (!feature.isEnabled) {
...@@ -151,32 +160,34 @@ const Marketplace = () => { ...@@ -151,32 +160,34 @@ const Marketplace = () => {
title="DAppscout" title="DAppscout"
mb={ 2 } mb={ 2 }
contentAfter={ (isMobile && links.length > 1) ? ( contentAfter={ (isMobile && links.length > 1) ? (
<Menu> <MenuRoot>
<MenuButton <MenuTrigger asChild>
as={ IconButton } <IconButton
size="sm" variant="dropdown"
variant="outline" size="sm"
colorScheme="gray" px="9px"
px="9px" ml="auto"
ml="auto" >
icon={ <IconSvg name="dots" boxSize="18px"/> } <IconSvg name="dots" boxSize="18px"/>
/> </IconButton>
<MenuList minW="max-content"> </MenuTrigger>
<MenuContent>
{ links.map(({ label, href, icon }) => ( { links.map(({ label, href, icon }) => (
<MenuItem key={ label } as="a" href={ href } target="_blank" py={ 2 } px={ 4 }> <MenuItem key={ label } value={ label } asChild>
<IconSvg name={ icon } boxSize={ 4 } mr={ 2.5 }/> <Link external href={ href } variant="menu" gap={ 0 }>
{ label } <IconSvg name={ icon } boxSize={ 4 } mr={ 2 }/>
<IconSvg name="link_external" boxSize={ 3 } color="icon_link_external" ml={ 2 }/> { label }
</Link>
</MenuItem> </MenuItem>
)) } )) }
</MenuList> </MenuContent>
</Menu> </MenuRoot>
) : ( ) : (
<Flex ml="auto"> <Flex ml="auto">
{ links.map(({ label, href }) => ( { links.map(({ label, href }) => (
<LinkExternal key={ label } href={ href } variant="subtle" fontSize="sm" lineHeight={ 5 } ml={ 2 }> <Link external key={ label } href={ href } variant="underlaid" textStyle="sm" ml={ 2 }>
{ label } { label }
</LinkExternal> </Link>
)) } )) }
</Flex> </Flex>
) } ) }
...@@ -200,10 +211,10 @@ const Marketplace = () => { ...@@ -200,10 +211,10 @@ const Marketplace = () => {
pt={{ base: 4, lg: 6 }} pt={{ base: 4, lg: 6 }}
pb={{ base: 4, lg: 3 }} pb={{ base: 4, lg: 3 }}
> >
<TabsWithScroll <AdaptiveTabs
tabs={ categoryTabs } tabs={ categoryTabs }
onTabChange={ handleCategoryChange } onValueChange={ handleCategoryChange }
defaultTabIndex={ selectedCategoryIndex } defaultValue={ selectedTabId }
marginBottom={ -2 } marginBottom={ -2 }
isLoading={ isCategoriesPlaceholderData } isLoading={ isCategoriesPlaceholderData }
/> />
...@@ -212,8 +223,9 @@ const Marketplace = () => { ...@@ -212,8 +223,9 @@ const Marketplace = () => {
{ showSort && ( { showSort && (
<Sort <Sort
name="dapps_sorting" name="dapps_sorting"
options={ SORT_OPTIONS } collection={ sortCollection }
onChange={ setSorting } onValueChange={ handleSortChange }
defaultValue={ [ sortCollection.items[0].value ] }
isLoading={ isPlaceholderData } isLoading={ isPlaceholderData }
/> />
) } ) }
...@@ -221,8 +233,8 @@ const Marketplace = () => { ...@@ -221,8 +233,8 @@ const Marketplace = () => {
initialValue={ filterQuery } initialValue={ filterQuery }
onChange={ onSearchInputChange } onChange={ onSearchInputChange }
placeholder="Find app by name or keyword..." placeholder="Find app by name or keyword..."
isLoading={ isPlaceholderData } loading={ isPlaceholderData }
size={ showSort ? 'xs' : 'sm' } size="sm"
w={{ base: '100%', lg: '350px' }} w={{ base: '100%', lg: '350px' }}
/> />
</Flex> </Flex>
......
import type { PinInputProps, StyleProps } from '@chakra-ui/react';
// eslint-disable-next-line no-restricted-imports
import { PinInput as PinInputBase } from '@chakra-ui/react';
import React from 'react';
const PinInput = (props: PinInputProps & { bgColor?: StyleProps['bgColor'] }) => {
return <PinInputBase { ...props }/>;
};
export default React.memo(PinInput);
import type { PopoverProps } from '@chakra-ui/react';
// eslint-disable-next-line no-restricted-imports
import { Popover as PopoverBase } from '@chakra-ui/react';
import React from 'react';
const Popover = (props: PopoverProps) => {
return <PopoverBase gutter={ 4 } { ...props }/>;
};
export default React.memo(Popover);
import { Tag as ChakraTag } from '@chakra-ui/react';
import type { TagProps } from '@chakra-ui/react';
import React from 'react';
import Skeleton from 'ui/shared/chakra/Skeleton';
import TruncatedTextTooltip from 'ui/shared/TruncatedTextTooltip';
export interface Props extends TagProps {
isLoading?: boolean;
}
const Tag = ({ isLoading, ...props }: Props, ref: React.ForwardedRef<HTMLDivElement>) => {
if (props.isTruncated && typeof props.children === 'string') {
if (!props.children) {
return null;
}
return (
<Skeleton isLoaded={ !isLoading } display="inline-block" borderRadius="sm" maxW="100%">
<TruncatedTextTooltip label={ props.children }>
<ChakraTag { ...props } ref={ ref }/>
</TruncatedTextTooltip>
</Skeleton>
);
}
return (
<Skeleton isLoaded={ !isLoading } display="inline-block" borderRadius="sm" maxW="100%">
<ChakraTag { ...props } ref={ ref }/>
</Skeleton>
);
};
export default React.memo(React.forwardRef(Tag));
import { Spinner } from '@chakra-ui/react'; import type { BoxProps } from '@chakra-ui/react';
import { Box, Spinner } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import usePreventFocusAfterModalClosing from 'lib/hooks/usePreventFocusAfterModalClosing'; import usePreventFocusAfterModalClosing from 'lib/hooks/usePreventFocusAfterModalClosing';
...@@ -15,10 +16,11 @@ interface Props extends ButtonProps { ...@@ -15,10 +16,11 @@ interface Props extends ButtonProps {
isLoading?: boolean; isLoading?: boolean;
onlyIcon?: boolean; onlyIcon?: boolean;
label?: string | React.ReactElement; label?: string | React.ReactElement;
wrapperProps?: BoxProps;
} }
const SolidityscanReportButton = ( const SolidityscanReportButton = (
{ score, isLoading, onlyIcon, label = 'Security score', ...rest }: Props, { score, isLoading, onlyIcon, label = 'Security score', wrapperProps, ...rest }: Props,
) => { ) => {
const { scoreColor } = useScoreLevelAndColor(score); const { scoreColor } = useScoreLevelAndColor(score);
const colorLoading = { _light: 'gray.300', _dark: 'gray.600' }; const colorLoading = { _light: 'gray.300', _dark: 'gray.600' };
...@@ -26,7 +28,7 @@ const SolidityscanReportButton = ( ...@@ -26,7 +28,7 @@ const SolidityscanReportButton = (
return ( return (
<Tooltip content={ label } disableOnMobile> <Tooltip content={ label } disableOnMobile>
<div> <Box { ...wrapperProps }>
<PopoverTrigger> <PopoverTrigger>
<Button <Button
color={ isLoading ? colorLoading : scoreColor } color={ isLoading ? colorLoading : scoreColor }
...@@ -52,7 +54,7 @@ const SolidityscanReportButton = ( ...@@ -52,7 +54,7 @@ const SolidityscanReportButton = (
{ !isLoading && (onlyIcon ? null : score) } { !isLoading && (onlyIcon ? null : score) }
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
</div> </Box>
</Tooltip> </Tooltip>
); );
}; };
......
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