Commit 6aba61a4 authored by tom's avatar tom

marketplace apps page

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