diff --git a/types/client/marketplace.ts b/types/client/marketplace.ts index 9063f538af..5f9255abec 100644 --- a/types/client/marketplace.ts +++ b/types/client/marketplace.ts @@ -41,11 +41,6 @@ export enum ContractListTypes { VERIFIED = 'Verified', } -export enum MarketplaceDisplayType { - DEFAULT = 'default', - SCORES = 'scores', -} - export type MarketplaceAppSecurityReport = { overallInfo: { verifiedNumber: number; diff --git a/ui/marketplace/AppSecurityReport.tsx b/ui/marketplace/AppSecurityReport.tsx index 94970b363d..8d9edb895c 100644 --- a/ui/marketplace/AppSecurityReport.tsx +++ b/ui/marketplace/AppSecurityReport.tsx @@ -1,9 +1,14 @@ -import { Box, Text, Link, Popover, PopoverTrigger, PopoverBody, PopoverContent, useDisclosure } from '@chakra-ui/react'; +import { Box, Text, Link, Popover, PopoverTrigger, PopoverBody, PopoverContent, useDisclosure, chakra, Flex, Divider, Icon } from '@chakra-ui/react'; import React from 'react'; import type { MarketplaceAppSecurityReport } from 'types/client/marketplace'; +import { ContractListTypes } from 'types/client/marketplace'; import config from 'configs/app'; +// This icon doesn't work properly when it is in the sprite +// Probably because of the gradient +// eslint-disable-next-line no-restricted-imports +import solidityScanIcon from 'icons/brands/solidity_scan.svg'; import { apos } from 'lib/html-entities'; import * as mixpanel from 'lib/mixpanel/index'; import IconSvg from 'ui/shared/IconSvg'; @@ -14,13 +19,17 @@ import SolidityscanReportScore from 'ui/shared/solidityscanReport/SolidityscanRe type Props = { id: string; securityReport?: MarketplaceAppSecurityReport; - showContractList: () => void; + showContractList: (id: string, type: ContractListTypes) => void; isLoading?: boolean; onlyIcon?: boolean; - source: 'Security view' | 'App modal' | 'App page'; + source: 'Discovery view' | 'App modal' | 'App page'; + className?: string; + popoverPlacement?: 'bottom-start' | 'bottom-end' | 'left'; } -const AppSecurityReport = ({ id, securityReport, showContractList, isLoading, onlyIcon, source }: Props) => { +const AppSecurityReport = ({ + id, securityReport, showContractList, isLoading, onlyIcon, source, className, popoverPlacement = 'bottom-start', +}: Props) => { const { isOpen, onToggle, onClose } = useDisclosure(); const handleButtonClick = React.useCallback(() => { @@ -28,14 +37,15 @@ const AppSecurityReport = ({ id, securityReport, showContractList, isLoading, on onToggle(); }, [ id, source, onToggle ]); - const handleLinkClick = React.useCallback(() => { + const showAnalyzedContracts = React.useCallback(() => { mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'Analyzed contracts', Info: id, Source: 'Security score popup' }); - showContractList(); - }, [ id, showContractList ]); + showContractList(id, ContractListTypes.ANALYZED); + }, [ showContractList, id ]); - if (!securityReport && !isLoading) { - return null; - } + const showAllContracts = React.useCallback(() => { + mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'Total contracts', Info: id, Source: 'Security score popup' }); + showContractList(id, ContractListTypes.ALL); + }, [ showContractList, id ]); const { securityScore = 0, @@ -44,8 +54,12 @@ const AppSecurityReport = ({ id, securityReport, showContractList, isLoading, on totalIssues = 0, } = securityReport?.overallInfo || {}; + if ((!securityReport || !securityScore) && !isLoading) { + return null; + } + return ( - + The security score is based on analysis
of a DApp{ apos }s smart contracts. } + className={ className } />
+ Smart contracts info + + + + Verified contracts + + + { securityReport?.overallInfo.verifiedNumber ?? 0 } of { securityReport?.overallInfo.totalContractsNumber ?? 0 } + + + { solidityScanContractsNumber } smart contract{ solidityScanContractsNumber === 1 ? ' was' : 's were' } evaluated to determine - this protocol{ apos }s overall security score on the { config.chain.name } network. + this protocol{ apos }s overall security score on the { config.chain.name } network by { ' ' } + + + SolidityScan + { issueSeverityDistribution && totalIssues > 0 && ( @@ -69,9 +99,8 @@ const AppSecurityReport = ({ id, securityReport, showContractList, isLoading, on ) } - + Analyzed contracts - @@ -79,4 +108,4 @@ const AppSecurityReport = ({ id, securityReport, showContractList, isLoading, on ); }; -export default AppSecurityReport; +export default chakra(AppSecurityReport); diff --git a/ui/marketplace/ContractListButton.tsx b/ui/marketplace/ContractListButton.tsx deleted file mode 100644 index 6a8bfce05e..0000000000 --- a/ui/marketplace/ContractListButton.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import { Link, Tooltip, Skeleton } from '@chakra-ui/react'; -import React from 'react'; -import type { MouseEvent } from 'react'; - -import config from 'configs/app'; -import IconSvg from 'ui/shared/IconSvg'; - -export enum ContractListButtonVariants { - ALL_CONTRACTS = 'all contracts', - VERIFIED_CONTRACTS = 'verified contracts', -} - -const values = { - [ContractListButtonVariants.ALL_CONTRACTS]: { - icon: 'contracts' as const, - iconColor: 'gray.500', - tooltip: `Total number of contracts deployed by the protocol on ${ config.chain.name }`, - }, - [ContractListButtonVariants.VERIFIED_CONTRACTS]: { - icon: 'contracts_verified' as const, - iconColor: 'green.500', - tooltip: `Number of verified contracts on ${ config.chain.name }`, - }, -}; - -interface Props { - children: string | number; - onClick: (event: MouseEvent) => void; - variant: ContractListButtonVariants; - isLoading?: boolean; -} - -const ContractListButton = ({ children, onClick, variant, isLoading }: Props) => { - const { icon, iconColor, tooltip } = values[variant]; - return ( - - - - { icon && } - { children } - - - - ); -}; - -export default ContractListButton; diff --git a/ui/marketplace/ContractListModal.tsx b/ui/marketplace/ContractListModal.tsx index 090c73891e..ec13977ba3 100644 --- a/ui/marketplace/ContractListModal.tsx +++ b/ui/marketplace/ContractListModal.tsx @@ -37,7 +37,7 @@ const ContractListModal = ({ onClose, onBack, type, contracts }: Props) => { switch (type) { default: case ContractListTypes.ALL: - return contracts; + return contracts.sort((a) => a.isVerified ? -1 : 1); case ContractListTypes.ANALYZED: return contracts .filter((contract) => Boolean(contract.solidityScanReport)) diff --git a/ui/marketplace/ContractSecurityReport.tsx b/ui/marketplace/ContractSecurityReport.tsx index da14a07c8c..3ee1bc110b 100644 --- a/ui/marketplace/ContractSecurityReport.tsx +++ b/ui/marketplace/ContractSecurityReport.tsx @@ -1,9 +1,13 @@ -import { Box, Text, Popover, PopoverTrigger, PopoverBody, PopoverContent, useDisclosure } from '@chakra-ui/react'; +import { Box, Text, Popover, PopoverTrigger, PopoverBody, PopoverContent, useDisclosure, Icon } from '@chakra-ui/react'; import React from 'react'; import type { SolidityscanReport } from 'types/api/contract'; import config from 'configs/app'; +// This icon doesn't work properly when it is in the sprite +// Probably because of the gradient +// eslint-disable-next-line no-restricted-imports +import solidityScanIcon from 'icons/brands/solidity_scan.svg'; import * as mixpanel from 'lib/mixpanel/index'; import LinkExternal from 'ui/shared/links/LinkExternal'; import SolidityscanReportButton from 'ui/shared/solidityscanReport/SolidityscanReportButton'; @@ -46,7 +50,11 @@ const ContractSecurityReport = ({ securityReport }: Props) => { - The security score was derived from evaluating the smart contracts of a protocol on the { config.chain.name } network. + The security score was derived from evaluating the smart contracts of a protocol on the { config.chain.name } network by { ' ' } + + + SolidityScan + { issueSeverityDistribution && totalIssues > 0 && ( diff --git a/ui/marketplace/MarketplaceAppCard.tsx b/ui/marketplace/MarketplaceAppCard.tsx index be9abd3fe1..d35533d2d3 100644 --- a/ui/marketplace/MarketplaceAppCard.tsx +++ b/ui/marketplace/MarketplaceAppCard.tsx @@ -2,20 +2,23 @@ import { Box, IconButton, Image, Link, LinkBox, Skeleton, useColorModeValue, cha import type { MouseEvent } from 'react'; import React, { useCallback } from 'react'; -import type { MarketplaceAppPreview } from 'types/client/marketplace'; +import type { MarketplaceAppWithSecurityReport, ContractListTypes } from 'types/client/marketplace'; +import useIsMobile from 'lib/hooks/useIsMobile'; import IconSvg from 'ui/shared/IconSvg'; +import AppSecurityReport from './AppSecurityReport'; import MarketplaceAppCardLink from './MarketplaceAppCardLink'; import MarketplaceAppIntegrationIcon from './MarketplaceAppIntegrationIcon'; -interface Props extends MarketplaceAppPreview { +interface Props extends MarketplaceAppWithSecurityReport { onInfoClick: (id: string) => void; isFavorite: boolean; onFavoriteClick: (id: string, isFavorite: boolean) => void; isLoading: boolean; onAppClick: (event: MouseEvent, id: string) => void; className?: string; + showContractList: (id: string, type: ContractListTypes) => void; } const MarketplaceAppCard = ({ @@ -33,8 +36,11 @@ const MarketplaceAppCard = ({ isLoading, internalWallet, onAppClick, + securityReport, className, + showContractList, }: Props) => { + const isMobile = useIsMobile(); const categoriesLabel = categories.join(', '); const handleInfoClick = useCallback((event: MouseEvent) => { @@ -58,32 +64,29 @@ const MarketplaceAppCard = ({ boxShadow: isLoading ? 'none' : 'md', }} borderRadius="md" - padding={{ base: 3, sm: '20px' }} + padding={{ base: 3, md: '20px' }} border="1px" borderColor={ useColorModeValue('gray.200', 'gray.600') } role="group" > - { !isLoading && ( - + + + + + + - - More info - - - ) } + { categoriesLabel } + + - - - - - - - - { categoriesLabel } - - - - { shortDescription } - - + { shortDescription } + { !isLoading && ( - + + More info + + : + + } + /> + + ) } + + { securityReport && ( + : - - } + right={{ base: 3, md: 5 }} + top={{ base: '10px', md: 5 }} + border={ 0 } + padding={ 0 } /> ) } diff --git a/ui/marketplace/MarketplaceAppIntegrationIcon.tsx b/ui/marketplace/MarketplaceAppIntegrationIcon.tsx index f884c11ca2..6b6b8788e9 100644 --- a/ui/marketplace/MarketplaceAppIntegrationIcon.tsx +++ b/ui/marketplace/MarketplaceAppIntegrationIcon.tsx @@ -42,7 +42,7 @@ const MarketplaceAppIntegrationIcon = ({ external, internalWallet }: Props) => { position="relative" cursor="pointer" verticalAlign="middle" - mb={ 1 } + mb={{ base: 0, md: 1 }} /> ); diff --git a/ui/marketplace/MarketplaceAppModal.pw.tsx b/ui/marketplace/MarketplaceAppModal.pw.tsx index dd3ac167d2..33166bef17 100644 --- a/ui/marketplace/MarketplaceAppModal.pw.tsx +++ b/ui/marketplace/MarketplaceAppModal.pw.tsx @@ -3,6 +3,7 @@ import React from 'react'; import type { MarketplaceAppWithSecurityReport } from 'types/client/marketplace'; import { apps as appsMock } from 'mocks/apps/apps'; +import { securityReports as securityReportsMock } from 'mocks/apps/securityReports'; import { test, expect, devices } from 'playwright/lib'; import MarketplaceAppModal from './MarketplaceAppModal'; @@ -11,7 +12,10 @@ const props = { onClose: () => {}, onFavoriteClick: () => {}, showContractList: () => {}, - data: appsMock[0] as MarketplaceAppWithSecurityReport, + data: { + ...appsMock[0], + securityReport: securityReportsMock[0].chainsData['1'], + } as MarketplaceAppWithSecurityReport, isFavorite: false, }; diff --git a/ui/marketplace/MarketplaceAppModal.tsx b/ui/marketplace/MarketplaceAppModal.tsx index ff318722b8..0bb31cfe97 100644 --- a/ui/marketplace/MarketplaceAppModal.tsx +++ b/ui/marketplace/MarketplaceAppModal.tsx @@ -1,5 +1,5 @@ import { - Box, Flex, Heading, IconButton, Image, Link, List, Modal, ModalBody, + Box, Flex, Heading, IconButton, Image, Link, Modal, ModalBody, ModalCloseButton, ModalContent, ModalFooter, ModalOverlay, Tag, Text, useColorModeValue, } from '@chakra-ui/react'; import React, { useCallback } from 'react'; @@ -14,7 +14,6 @@ import type { IconName } from 'ui/shared/IconSvg'; import IconSvg from 'ui/shared/IconSvg'; import AppSecurityReport from './AppSecurityReport'; -import ContractListButton, { ContractListButtonVariants } from './ContractListButton'; import MarketplaceAppModalLink from './MarketplaceAppModalLink'; type Props = { @@ -79,25 +78,16 @@ const MarketplaceAppModal = ({ onFavoriteClick(id, isFavorite, 'App modal'); }, [ onFavoriteClick, id, isFavorite ]); - const showContractList = useCallback((type: ContractListTypes) => { + const showContractList = useCallback((id: string, type: ContractListTypes) => { onClose(); showContractListProp(id, type, true); - }, [ onClose, showContractListProp, id ]); + }, [ onClose, showContractListProp ]); const showAllContracts = React.useCallback(() => { mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'Total contracts', Info: id, Source: 'App modal' }); - showContractList(ContractListTypes.ALL); + showContractList(id, ContractListTypes.ALL); }, [ showContractList, id ]); - const showVerifiedContracts = React.useCallback(() => { - mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'Verified contracts', Info: id, Source: 'App modal' }); - showContractList(ContractListTypes.VERIFIED); - }, [ showContractList, id ]); - - const showAnalyzedContracts = React.useCallback(() => { - showContractList(ContractListTypes.ANALYZED); - }, [ showContractList ]); - const isMobile = useIsMobile(); const logoUrl = useColorModeValue(logo, logoDarkMode || logo); @@ -120,16 +110,16 @@ const MarketplaceAppModal = ({ - + } /> - - { securityReport && ( - - - - { securityReport.overallInfo.totalContractsNumber } - - - { securityReport.overallInfo.verifiedNumber } - - - ) } - - - Overview - + + { securityReport && ( + + + + Verified contracts + + { securityReport?.overallInfo.verifiedNumber ?? 0 } of { securityReport?.overallInfo.totalContractsNumber ?? 0 } + + + View all contracts + + + + Security level + + + + ) } + { description } + - + + { categories.map((category) => ( { category } )) } - - - { description } - - - - { site && ( - - + - + { site && ( + - { getHostname(site) } - - - ) } + - { socialLinks.length > 0 && ( - - { socialLinks.map(({ icon, url }) => ( - - - - )) } - - ) } + { getHostname(site) } + + + ) } + + { socialLinks.map(({ icon, url }) => ( + + + + )) } + diff --git a/ui/marketplace/MarketplaceAppTopBar.tsx b/ui/marketplace/MarketplaceAppTopBar.tsx index 892e48554f..f7e5235f2d 100644 --- a/ui/marketplace/MarketplaceAppTopBar.tsx +++ b/ui/marketplace/MarketplaceAppTopBar.tsx @@ -1,8 +1,7 @@ -import { chakra, Flex, Tooltip, Skeleton, useBoolean } from '@chakra-ui/react'; +import { chakra, Flex, Tooltip, Skeleton } from '@chakra-ui/react'; import React from 'react'; -import type { MarketplaceAppOverview, MarketplaceAppSecurityReport } from 'types/client/marketplace'; -import { ContractListTypes } from 'types/client/marketplace'; +import type { MarketplaceAppOverview, MarketplaceAppSecurityReport, ContractListTypes } from 'types/client/marketplace'; import { route } from 'nextjs-routes'; @@ -27,7 +26,7 @@ type Props = { } const MarketplaceAppTopBar = ({ data, isLoading, securityReport }: Props) => { - const [ showContractList, setShowContractList ] = useBoolean(false); + const [ contractListType, setContractListType ] = React.useState(); const appProps = useAppContext(); const isMobile = useIsMobile(); @@ -44,6 +43,9 @@ const MarketplaceAppTopBar = ({ data, isLoading, securityReport }: Props) => { } catch (err) {} } + const showContractList = React.useCallback((id: string, type: ContractListTypes) => setContractListType(type), []); + const hideContractList = React.useCallback(() => setContractListType(undefined), []); + return ( <> @@ -74,7 +76,7 @@ const MarketplaceAppTopBar = ({ data, isLoading, securityReport }: Props) => { { ) } - { showContractList && ( + { contractListType && ( ) } diff --git a/ui/marketplace/MarketplaceList.tsx b/ui/marketplace/MarketplaceList.tsx index ec4eea487a..896ca99df5 100644 --- a/ui/marketplace/MarketplaceList.tsx +++ b/ui/marketplace/MarketplaceList.tsx @@ -2,7 +2,7 @@ import { Grid } from '@chakra-ui/react'; import React, { useCallback } from 'react'; import type { MouseEvent } from 'react'; -import type { MarketplaceAppPreview } from 'types/client/marketplace'; +import type { MarketplaceAppWithSecurityReport, ContractListTypes } from 'types/client/marketplace'; import * as mixpanel from 'lib/mixpanel/index'; @@ -10,16 +10,17 @@ import EmptySearchResult from './EmptySearchResult'; import MarketplaceAppCard from './MarketplaceAppCard'; type Props = { - apps: Array; + apps: Array; showAppInfo: (id: string) => void; favoriteApps: Array; onFavoriteClick: (id: string, isFavorite: boolean, source: 'Discovery view') => void; isLoading: boolean; selectedCategoryId?: string; onAppClick: (event: MouseEvent, id: string) => void; + showContractList: (id: string, type: ContractListTypes) => void; } -const MarketplaceList = ({ apps, showAppInfo, favoriteApps, onFavoriteClick, isLoading, selectedCategoryId, onAppClick }: Props) => { +const MarketplaceList = ({ apps, showAppInfo, favoriteApps, onFavoriteClick, isLoading, selectedCategoryId, onAppClick, showContractList }: Props) => { const handleInfoClick = useCallback((id: string) => { mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'More button', Info: id, Source: 'Discovery view' }); showAppInfo(id); @@ -32,11 +33,11 @@ const MarketplaceList = ({ apps, showAppInfo, favoriteApps, onFavoriteClick, isL return apps.length > 0 ? ( { apps.map((app, index) => ( )) } diff --git a/ui/marketplace/MarketplaceListWithScores.tsx b/ui/marketplace/MarketplaceListWithScores.tsx deleted file mode 100644 index 2ed58dafec..0000000000 --- a/ui/marketplace/MarketplaceListWithScores.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import { Hide, Show } from '@chakra-ui/react'; -import React from 'react'; -import type { MouseEvent } from 'react'; - -import type { MarketplaceAppWithSecurityReport, ContractListTypes } from 'types/client/marketplace'; - -import DataListDisplay from 'ui/shared/DataListDisplay'; - -import EmptySearchResult from './EmptySearchResult'; -import ListItem from './MarketplaceListWithScores/ListItem'; -import Table from './MarketplaceListWithScores/Table'; - -interface Props { - apps: Array; - showAppInfo: (id: string) => void; - favoriteApps: Array; - onFavoriteClick: (id: string, isFavorite: boolean, source: 'Security view') => void; - isLoading: boolean; - selectedCategoryId?: string; - onAppClick: (event: MouseEvent, id: string) => void; - showContractList: (id: string, type: ContractListTypes) => void; -} - -const MarketplaceListWithScores = ({ - apps, - showAppInfo, - favoriteApps, - onFavoriteClick, - isLoading, - selectedCategoryId, - onAppClick, - showContractList, -}: Props) => { - - const displayedApps = React.useMemo(() => [ ...apps ].sort((a, b) => { - if (!a.securityReport) { - return 1; - } else if (!b.securityReport) { - return -1; - } - return b.securityReport.overallInfo.securityScore - a.securityReport.overallInfo.securityScore; - }), [ apps ]); - - const content = apps.length > 0 ? ( - <> - - { displayedApps.map((app, index) => ( - - )) } - - - - - - ) : null; - - return apps.length > 0 ? ( - - ) : ( - - ); -}; - -export default MarketplaceListWithScores; diff --git a/ui/marketplace/MarketplaceListWithScores/AppLink.tsx b/ui/marketplace/MarketplaceListWithScores/AppLink.tsx deleted file mode 100644 index a40ece8d6a..0000000000 --- a/ui/marketplace/MarketplaceListWithScores/AppLink.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { Flex, Skeleton, LinkBox, Image, useColorModeValue } from '@chakra-ui/react'; -import React from 'react'; -import type { MouseEvent } from 'react'; - -import type { MarketplaceAppPreview } from 'types/client/marketplace'; - -import MarketplaceAppCardLink from '../MarketplaceAppCardLink'; -import MarketplaceAppIntegrationIcon from '../MarketplaceAppIntegrationIcon'; - -interface Props { - app: MarketplaceAppPreview; - isLoading: boolean | undefined; - onAppClick: (event: MouseEvent, id: string) => void; - isLarge?: boolean; -} - -const AppLink = ({ app, isLoading, onAppClick, isLarge = false }: Props) => { - const { id, url, external, title, logo, logoDarkMode, internalWallet, categories } = app; - - const logoUrl = useColorModeValue(logo, logoDarkMode || logo); - - const categoriesLabel = categories.join(', '); - - return ( - - - - - - - - - - - - { categoriesLabel } - - - - ); -}; - -export default AppLink; diff --git a/ui/marketplace/MarketplaceListWithScores/ListItem.tsx b/ui/marketplace/MarketplaceListWithScores/ListItem.tsx deleted file mode 100644 index 2a4f8b6102..0000000000 --- a/ui/marketplace/MarketplaceListWithScores/ListItem.tsx +++ /dev/null @@ -1,125 +0,0 @@ -import { Flex, IconButton, Text } from '@chakra-ui/react'; -import React from 'react'; -import type { MouseEvent } from 'react'; - -import type { MarketplaceAppWithSecurityReport } from 'types/client/marketplace'; -import { ContractListTypes } from 'types/client/marketplace'; - -import * as mixpanel from 'lib/mixpanel/index'; -import IconSvg from 'ui/shared/IconSvg'; -import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile'; - -import AppSecurityReport from '../AppSecurityReport'; -import ContractListButton, { ContractListButtonVariants } from '../ContractListButton'; -import AppLink from './AppLink'; -import MoreInfoButton from './MoreInfoButton'; - -type Props = { - app: MarketplaceAppWithSecurityReport; - onInfoClick: (id: string) => void; - isFavorite: boolean; - onFavoriteClick: (id: string, isFavorite: boolean, source: 'Security view') => void; - isLoading: boolean; - onAppClick: (event: MouseEvent, id: string) => void; - showContractList: (id: string, type: ContractListTypes) => void; -} - -const ListItem = ({ app, onInfoClick, isFavorite, onFavoriteClick, isLoading, onAppClick, showContractList }: Props) => { - const { id, securityReport } = app; - - const handleInfoClick = React.useCallback((event: MouseEvent) => { - event.preventDefault(); - mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'More button', Info: id, Source: 'Security view' }); - onInfoClick(id); - }, [ onInfoClick, id ]); - - const handleFavoriteClick = React.useCallback(() => { - onFavoriteClick(id, isFavorite, 'Security view'); - }, [ onFavoriteClick, id, isFavorite ]); - - const showAllContracts = React.useCallback(() => { - mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'Total contracts', Info: id, Source: 'Security view' }); - showContractList(id, ContractListTypes.ALL); - }, [ showContractList, id ]); - - const showVerifiedContracts = React.useCallback(() => { - mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'Verified contracts', Info: id, Source: 'Security view' }); - showContractList(id, ContractListTypes.VERIFIED); - }, [ showContractList, id ]); - - const showAnalyzedContracts = React.useCallback(() => { - showContractList(id, ContractListTypes.ANALYZED); - }, [ showContractList, id ]); - - return ( - - - - - { !isLoading && ( - : - - } - /> - ) } - - - - { (securityReport || isLoading) ? ( - <> - - - { securityReport?.overallInfo.totalContractsNumber ?? 0 } - - - { securityReport?.overallInfo.verifiedNumber ?? 0 } - - - ) : ( - Data will be available soon - ) } - - - - - - ); -}; - -export default ListItem; diff --git a/ui/marketplace/MarketplaceListWithScores/MoreInfoButton.tsx b/ui/marketplace/MarketplaceListWithScores/MoreInfoButton.tsx deleted file mode 100644 index d96bddb66c..0000000000 --- a/ui/marketplace/MarketplaceListWithScores/MoreInfoButton.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { Link, Skeleton } from '@chakra-ui/react'; -import React from 'react'; -import type { MouseEvent } from 'react'; - -interface Props { - onClick: (event: MouseEvent) => void; - isLoading?: boolean; -} - -const MoreInfoButton = ({ onClick, isLoading }: Props) => ( - - - More info - - -); - -export default MoreInfoButton; diff --git a/ui/marketplace/MarketplaceListWithScores/Table.tsx b/ui/marketplace/MarketplaceListWithScores/Table.tsx deleted file mode 100644 index 1e8cbddf56..0000000000 --- a/ui/marketplace/MarketplaceListWithScores/Table.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { Table as ChakraTable, Tbody, Th, Tr } from '@chakra-ui/react'; -import React from 'react'; -import type { MouseEvent } from 'react'; - -import type { MarketplaceAppWithSecurityReport, ContractListTypes } from 'types/client/marketplace'; - -import { default as Thead } from 'ui/shared/TheadSticky'; - -import TableItem from './TableItem'; - -type Props = { - apps: Array; - isLoading?: boolean; - favoriteApps: Array; - onFavoriteClick: (id: string, isFavorite: boolean, source: 'Security view') => void; - onAppClick: (event: MouseEvent, id: string) => void; - onInfoClick: (id: string) => void; - showContractList: (id: string, type: ContractListTypes) => void; -} - -const Table = ({ apps, isLoading, favoriteApps, onFavoriteClick, onAppClick, onInfoClick, showContractList }: Props) => { - return ( - - - - - - - - - - - - - { apps.map((app, index) => ( - - )) } - - - ); -}; - -export default Table; diff --git a/ui/marketplace/MarketplaceListWithScores/TableItem.tsx b/ui/marketplace/MarketplaceListWithScores/TableItem.tsx deleted file mode 100644 index c77b5c5c78..0000000000 --- a/ui/marketplace/MarketplaceListWithScores/TableItem.tsx +++ /dev/null @@ -1,126 +0,0 @@ -import { Td, Tr, IconButton, Skeleton, Text } from '@chakra-ui/react'; -import React from 'react'; -import type { MouseEvent } from 'react'; - -import type { MarketplaceAppWithSecurityReport } from 'types/client/marketplace'; -import { ContractListTypes } from 'types/client/marketplace'; - -import * as mixpanel from 'lib/mixpanel/index'; -import IconSvg from 'ui/shared/IconSvg'; - -import AppSecurityReport from '../AppSecurityReport'; -import ContractListButton, { ContractListButtonVariants } from '../ContractListButton'; -import AppLink from './AppLink'; -import MoreInfoButton from './MoreInfoButton'; - -type Props = { - app: MarketplaceAppWithSecurityReport; - isLoading?: boolean; - isFavorite: boolean; - onFavoriteClick: (id: string, isFavorite: boolean, source: 'Security view') => void; - onAppClick: (event: MouseEvent, id: string) => void; - onInfoClick: (id: string) => void; - showContractList: (id: string, type: ContractListTypes) => void; -} - -const TableItem = ({ - app, - isLoading, - isFavorite, - onFavoriteClick, - onAppClick, - onInfoClick, - showContractList, -}: Props) => { - - const { id, securityReport } = app; - - const handleInfoClick = React.useCallback((event: MouseEvent) => { - event.preventDefault(); - mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'More button', Info: id, Source: 'Security view' }); - onInfoClick(id); - }, [ onInfoClick, id ]); - - const handleFavoriteClick = React.useCallback(() => { - onFavoriteClick(id, isFavorite, 'Security view'); - }, [ onFavoriteClick, id, isFavorite ]); - - const showAllContracts = React.useCallback(() => { - mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'Total contracts', Info: id, Source: 'Security view' }); - showContractList(id, ContractListTypes.ALL); - }, [ showContractList, id ]); - - const showVerifiedContracts = React.useCallback(() => { - mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'Verified contracts', Info: id, Source: 'Security view' }); - showContractList(id, ContractListTypes.VERIFIED); - }, [ showContractList, id ]); - - const showAnalyzedContracts = React.useCallback(() => { - showContractList(id, ContractListTypes.ANALYZED); - }, [ showContractList, id ]); - - return ( - - - - { (securityReport || isLoading) ? ( - <> - - - - - ) : ( - - ) } - - - ); -}; - -export default TableItem; diff --git a/ui/marketplace/__screenshots__/MarketplaceAppModal.pw.tsx_dark-color-mode_base-view-dark-mode-1.png b/ui/marketplace/__screenshots__/MarketplaceAppModal.pw.tsx_dark-color-mode_base-view-dark-mode-1.png index d5fba4f1f9..350dafd2f9 100644 Binary files a/ui/marketplace/__screenshots__/MarketplaceAppModal.pw.tsx_dark-color-mode_base-view-dark-mode-1.png and b/ui/marketplace/__screenshots__/MarketplaceAppModal.pw.tsx_dark-color-mode_base-view-dark-mode-1.png differ diff --git a/ui/marketplace/__screenshots__/MarketplaceAppModal.pw.tsx_default_base-view-dark-mode-1.png b/ui/marketplace/__screenshots__/MarketplaceAppModal.pw.tsx_default_base-view-dark-mode-1.png index 25dffc6800..992ae80fd2 100644 Binary files a/ui/marketplace/__screenshots__/MarketplaceAppModal.pw.tsx_default_base-view-dark-mode-1.png and b/ui/marketplace/__screenshots__/MarketplaceAppModal.pw.tsx_default_base-view-dark-mode-1.png differ diff --git a/ui/marketplace/__screenshots__/MarketplaceAppModal.pw.tsx_default_mobile-base-view-1.png b/ui/marketplace/__screenshots__/MarketplaceAppModal.pw.tsx_default_mobile-base-view-1.png index 63ab8f1099..5f01caf93e 100644 Binary files a/ui/marketplace/__screenshots__/MarketplaceAppModal.pw.tsx_default_mobile-base-view-1.png and b/ui/marketplace/__screenshots__/MarketplaceAppModal.pw.tsx_default_mobile-base-view-1.png differ diff --git a/ui/marketplace/useMarketplace.tsx b/ui/marketplace/useMarketplace.tsx index 99ba708e3a..c7db6567ab 100644 --- a/ui/marketplace/useMarketplace.tsx +++ b/ui/marketplace/useMarketplace.tsx @@ -3,7 +3,7 @@ import { useRouter } from 'next/router'; import React from 'react'; import type { ContractListTypes } from 'types/client/marketplace'; -import { MarketplaceCategory, MarketplaceDisplayType } from 'types/client/marketplace'; +import { MarketplaceCategory } from 'types/client/marketplace'; import useDebounce from 'lib/hooks/useDebounce'; import * as mixpanel from 'lib/mixpanel/index'; @@ -26,15 +26,9 @@ export default function useMarketplace() { const router = useRouter(); const defaultCategoryId = getQueryParamString(router.query.category); const defaultFilterQuery = getQueryParamString(router.query.filter); - const defaultDisplayType = getQueryParamString(router.query.tab); const [ selectedAppId, setSelectedAppId ] = React.useState(null); const [ selectedCategoryId, setSelectedCategoryId ] = React.useState(MarketplaceCategory.ALL); - const [ selectedDisplayType, setSelectedDisplayType ] = React.useState( - Object.values(MarketplaceDisplayType).includes(defaultDisplayType as MarketplaceDisplayType) ? - defaultDisplayType : - MarketplaceDisplayType.DEFAULT, - ); const [ filterQuery, setFilterQuery ] = React.useState(defaultFilterQuery); const [ favoriteApps, setFavoriteApps ] = React.useState>([]); const [ isFavoriteAppsLoaded, setIsFavoriteAppsLoaded ] = React.useState(false); @@ -91,12 +85,8 @@ export default function useMarketplace() { setSelectedCategoryId(newCategory); }, []); - const handleDisplayTypeChange = React.useCallback((newDisplayType: MarketplaceDisplayType) => { - setSelectedDisplayType(newDisplayType); - }, []); - const { - isPlaceholderData, isError, error, data, displayedApps, + isPlaceholderData, isError, error, data, displayedApps, setSorting, } = useMarketplaceApps(debouncedFilterQuery, selectedCategoryId, favoriteApps, isFavoriteAppsLoaded); const { isPlaceholderData: isCategoriesPlaceholderData, data: categories, @@ -120,7 +110,6 @@ export default function useMarketplace() { const query = _pickBy({ category: selectedCategoryId === MarketplaceCategory.ALL ? undefined : selectedCategoryId, filter: debouncedFilterQuery, - tab: selectedDisplayType === MarketplaceDisplayType.DEFAULT ? undefined : selectedDisplayType, }, Boolean); if (debouncedFilterQuery.length > 0) { @@ -135,7 +124,7 @@ export default function useMarketplace() { // omit router in the deps because router.push() somehow modifies it // and we get infinite re-renders then // eslint-disable-next-line react-hooks/exhaustive-deps - }, [ debouncedFilterQuery, selectedCategoryId, selectedDisplayType ]); + }, [ debouncedFilterQuery, selectedCategoryId ]); return React.useMemo(() => ({ selectedCategoryId, @@ -160,9 +149,8 @@ export default function useMarketplace() { isCategoriesPlaceholderData, showContractList, contractListModalType, - selectedDisplayType, - onDisplayTypeChange: handleDisplayTypeChange, hasPreviousStep, + setSorting, }), [ selectedCategoryId, categories, @@ -184,8 +172,7 @@ export default function useMarketplace() { isCategoriesPlaceholderData, showContractList, contractListModalType, - selectedDisplayType, - handleDisplayTypeChange, hasPreviousStep, + setSorting, ]); } diff --git a/ui/marketplace/useMarketplaceApps.tsx b/ui/marketplace/useMarketplaceApps.tsx index 96f09f379e..f3c952b539 100644 --- a/ui/marketplace/useMarketplaceApps.tsx +++ b/ui/marketplace/useMarketplaceApps.tsx @@ -11,6 +11,7 @@ import useFetch from 'lib/hooks/useFetch'; import { MARKETPLACE_APP } from 'stubs/marketplace'; import useSecurityReports from './useSecurityReports'; +import type { SortValue } from './utils'; const feature = config.features.marketplace; @@ -88,13 +89,22 @@ export default function useMarketplaceApps( enabled: feature.isEnabled && Boolean(snapshotFavoriteApps), }); + const [ sorting, setSorting ] = React.useState(); + const appsWithSecurityReports = React.useMemo(() => data?.map((app) => ({ ...app, securityReport: securityReports?.[app.id] })), [ data, securityReports ]); const displayedApps = React.useMemo(() => { - return appsWithSecurityReports?.filter(app => isAppNameMatches(filter, app) && isAppCategoryMatches(selectedCategoryId, app, favoriteApps)) || []; - }, [ selectedCategoryId, appsWithSecurityReports, filter, favoriteApps ]); + return appsWithSecurityReports + ?.filter(app => isAppNameMatches(filter, app) && isAppCategoryMatches(selectedCategoryId, app, favoriteApps)) + .sort((a, b) => { + if (sorting === 'security_score') { + return (b.securityReport?.overallInfo.securityScore || 0) - (a.securityReport?.overallInfo.securityScore || 0); + } + return 0; + }) || []; + }, [ selectedCategoryId, appsWithSecurityReports, filter, favoriteApps, sorting ]); return React.useMemo(() => ({ data, @@ -102,6 +112,7 @@ export default function useMarketplaceApps( error, isError, isPlaceholderData: isPlaceholderData || isSecurityReportsPlaceholderData, + setSorting, }), [ data, displayedApps, @@ -109,5 +120,6 @@ export default function useMarketplaceApps( isError, isPlaceholderData, isSecurityReportsPlaceholderData, + setSorting, ]); } diff --git a/ui/marketplace/utils.ts b/ui/marketplace/utils.ts index 23e79e7491..09cb0ec28f 100644 --- a/ui/marketplace/utils.ts +++ b/ui/marketplace/utils.ts @@ -2,6 +2,14 @@ import type { NextRouter } from 'next/router'; import getQueryParamString from 'lib/router/getQueryParamString'; import removeQueryParam from 'lib/router/removeQueryParam'; +import type { TOption } from 'ui/shared/sort/Option'; + +export type SortValue = 'security_score'; + +export const SORT_OPTIONS: Array> = [ + { title: 'Default', id: undefined }, + { title: 'Security score', id: 'security_score' }, +]; export function getAppUrl(url: string | undefined, router: NextRouter) { if (!url) { diff --git a/ui/nameDomains/NameDomainsActionBar.tsx b/ui/nameDomains/NameDomainsActionBar.tsx index cba88c79bd..ccc0a6e631 100644 --- a/ui/nameDomains/NameDomainsActionBar.tsx +++ b/ui/nameDomains/NameDomainsActionBar.tsx @@ -135,9 +135,10 @@ const NameDomainsActionBar = ({ const sortButton = ( ); diff --git a/ui/nameDomains/utils.ts b/ui/nameDomains/utils.ts index c240e5c93e..431b8437db 100644 --- a/ui/nameDomains/utils.ts +++ b/ui/nameDomains/utils.ts @@ -1,12 +1,12 @@ import type { EnsLookupSorting } from 'types/api/ens'; import getNextSortValueShared from 'ui/shared/sort/getNextSortValue'; -import type { Option } from 'ui/shared/sort/Sort'; +import type { TOption } from 'ui/shared/sort/Option'; export type SortField = EnsLookupSorting['sort']; export type Sort = `${ EnsLookupSorting['sort'] }-${ EnsLookupSorting['order'] }`; -export const SORT_OPTIONS: Array> = [ +export const SORT_OPTIONS: Array> = [ { title: 'Default', id: undefined }, { title: 'Registered on descending', id: 'registration_date-DESC' }, { title: 'Registered on ascending', id: 'registration_date-ASC' }, diff --git a/ui/pages/Marketplace.pw.tsx b/ui/pages/Marketplace.pw.tsx index 004b1c6f95..b25d67f47c 100644 --- a/ui/pages/Marketplace.pw.tsx +++ b/ui/pages/Marketplace.pw.tsx @@ -7,13 +7,16 @@ import { test, expect, devices } from 'playwright/lib'; import Marketplace from './Marketplace'; const MARKETPLACE_CONFIG_URL = 'http://localhost/marketplace-config.json'; +const MARKETPLACE_SECURITY_REPORTS_URL = 'https://marketplace-security-reports.json'; test.beforeEach(async({ mockConfigResponse, mockEnvs, mockAssetResponse }) => { await mockEnvs([ [ 'NEXT_PUBLIC_MARKETPLACE_ENABLED', 'true' ], [ 'NEXT_PUBLIC_MARKETPLACE_CONFIG_URL', MARKETPLACE_CONFIG_URL ], + [ 'NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL', MARKETPLACE_SECURITY_REPORTS_URL ], ]); await mockConfigResponse('NEXT_PUBLIC_MARKETPLACE_CONFIG_URL', MARKETPLACE_CONFIG_URL, JSON.stringify(appsMock)); + await mockConfigResponse('NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL', MARKETPLACE_SECURITY_REPORTS_URL, JSON.stringify(securityReportsMock)); await Promise.all(appsMock.map(app => mockAssetResponse(app.logo, './playwright/mocks/image_s.jpg'))); }); @@ -46,18 +49,6 @@ test('with banner +@dark-mode', async({ render, mockEnvs, mockConfigResponse }) await expect(component).toHaveScreenshot(); }); -test('with scores +@dark-mode', async({ render, mockConfigResponse, mockEnvs }) => { - const MARKETPLACE_SECURITY_REPORTS_URL = 'https://marketplace-security-reports.json'; - await mockEnvs([ - [ 'NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL', MARKETPLACE_SECURITY_REPORTS_URL ], - ]); - await mockConfigResponse('NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL', MARKETPLACE_SECURITY_REPORTS_URL, JSON.stringify(securityReportsMock)); - const component = await render(); - await component.getByText('Apps scores').click(); - - await expect(component).toHaveScreenshot(); -}); - // I had a memory error while running tests in GH actions // separate run for mobile tests fixes it test.describe('mobile', () => { @@ -91,16 +82,4 @@ test.describe('mobile', () => { await expect(component).toHaveScreenshot(); }); - - test('with scores', async({ render, mockConfigResponse, mockEnvs }) => { - const MARKETPLACE_SECURITY_REPORTS_URL = 'https://marketplace-security-reports.json'; - await mockEnvs([ - [ 'NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL', MARKETPLACE_SECURITY_REPORTS_URL ], - ]); - await mockConfigResponse('NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL', MARKETPLACE_SECURITY_REPORTS_URL, JSON.stringify(securityReportsMock)); - const component = await render(); - await component.getByText('Apps scores').click(); - - await expect(component).toHaveScreenshot(); - }); }); diff --git a/ui/pages/Marketplace.tsx b/ui/pages/Marketplace.tsx index 6cd802acad..fdcd814197 100644 --- a/ui/pages/Marketplace.tsx +++ b/ui/pages/Marketplace.tsx @@ -1,8 +1,8 @@ -import { Box, Menu, MenuButton, MenuItem, MenuList, Flex, IconButton, Skeleton } from '@chakra-ui/react'; +import { Box, Menu, MenuButton, MenuItem, MenuList, Flex, IconButton } from '@chakra-ui/react'; import React from 'react'; import type { MouseEvent } from 'react'; -import { MarketplaceCategory, MarketplaceDisplayType } from 'types/client/marketplace'; +import { MarketplaceCategory } from 'types/client/marketplace'; import type { TabItem } from 'ui/shared/Tabs/types'; import config from 'configs/app'; @@ -13,13 +13,13 @@ import ContractListModal from 'ui/marketplace/ContractListModal'; import MarketplaceAppModal from 'ui/marketplace/MarketplaceAppModal'; import MarketplaceDisclaimerModal from 'ui/marketplace/MarketplaceDisclaimerModal'; import MarketplaceList from 'ui/marketplace/MarketplaceList'; -import MarketplaceListWithScores from 'ui/marketplace/MarketplaceListWithScores'; +import { SORT_OPTIONS } from 'ui/marketplace/utils'; import FilterInput from 'ui/shared/filters/FilterInput'; import IconSvg from 'ui/shared/IconSvg'; import type { IconName } from 'ui/shared/IconSvg'; import LinkExternal from 'ui/shared/links/LinkExternal'; import PageTitle from 'ui/shared/Page/PageTitle'; -import RadioButtonGroup from 'ui/shared/radioButtonGroup/RadioButtonGroup'; +import Sort from 'ui/shared/sort/Sort'; import TabsWithScroll from 'ui/shared/Tabs/TabsWithScroll'; import useMarketplace from '../marketplace/useMarketplace'; @@ -67,9 +67,8 @@ const Marketplace = () => { isCategoriesPlaceholderData, showContractList, contractListModalType, - selectedDisplayType, - onDisplayTypeChange, hasPreviousStep, + setSorting, } = useMarketplace(); const isMobile = useIsMobile(); @@ -186,38 +185,14 @@ const Marketplace = () => { /> - + { feature.securityReportsUrl && ( - - - onChange={ onDisplayTypeChange } - defaultValue={ selectedDisplayType } - name="type" - options={ [ - { - title: 'Discovery', - value: MarketplaceDisplayType.DEFAULT, - icon: 'apps_xs', - onlyIcon: false, - }, - { - title: 'Apps scores', - value: MarketplaceDisplayType.SCORES, - icon: 'apps_list', - onlyIcon: false, - contentAfter: ( - - ), - }, - ] } - autoWidth - /> - + ) } { /> - { (selectedDisplayType === MarketplaceDisplayType.SCORES && feature.securityReportsUrl) ? ( - - ) : ( - - ) } + { (selectedApp && isAppInfoModalOpen) && ( { const sortButton = ( ); diff --git a/ui/pages/VerifiedContracts.tsx b/ui/pages/VerifiedContracts.tsx index 3c17beda81..31b16d244b 100644 --- a/ui/pages/VerifiedContracts.tsx +++ b/ui/pages/VerifiedContracts.tsx @@ -97,9 +97,11 @@ const VerifiedContracts = () => { const sortButton = ( ); diff --git a/ui/pages/__screenshots__/Marketplace.pw.tsx_dark-color-mode_base-view-dark-mode-1.png b/ui/pages/__screenshots__/Marketplace.pw.tsx_dark-color-mode_base-view-dark-mode-1.png index 4af09ccd95..3b66a74c5e 100644 Binary files a/ui/pages/__screenshots__/Marketplace.pw.tsx_dark-color-mode_base-view-dark-mode-1.png and b/ui/pages/__screenshots__/Marketplace.pw.tsx_dark-color-mode_base-view-dark-mode-1.png differ diff --git a/ui/pages/__screenshots__/Marketplace.pw.tsx_dark-color-mode_with-banner-dark-mode-1.png b/ui/pages/__screenshots__/Marketplace.pw.tsx_dark-color-mode_with-banner-dark-mode-1.png index 4f12a387cc..823a1ec505 100644 Binary files a/ui/pages/__screenshots__/Marketplace.pw.tsx_dark-color-mode_with-banner-dark-mode-1.png and b/ui/pages/__screenshots__/Marketplace.pw.tsx_dark-color-mode_with-banner-dark-mode-1.png differ diff --git a/ui/pages/__screenshots__/Marketplace.pw.tsx_dark-color-mode_with-featured-app-dark-mode-1.png b/ui/pages/__screenshots__/Marketplace.pw.tsx_dark-color-mode_with-featured-app-dark-mode-1.png index febfbbdaf6..d347f3bb31 100644 Binary files a/ui/pages/__screenshots__/Marketplace.pw.tsx_dark-color-mode_with-featured-app-dark-mode-1.png and b/ui/pages/__screenshots__/Marketplace.pw.tsx_dark-color-mode_with-featured-app-dark-mode-1.png differ diff --git a/ui/pages/__screenshots__/Marketplace.pw.tsx_dark-color-mode_with-scores-dark-mode-1.png b/ui/pages/__screenshots__/Marketplace.pw.tsx_dark-color-mode_with-scores-dark-mode-1.png deleted file mode 100644 index 281051a789..0000000000 Binary files a/ui/pages/__screenshots__/Marketplace.pw.tsx_dark-color-mode_with-scores-dark-mode-1.png and /dev/null differ diff --git a/ui/pages/__screenshots__/Marketplace.pw.tsx_default_base-view-dark-mode-1.png b/ui/pages/__screenshots__/Marketplace.pw.tsx_default_base-view-dark-mode-1.png index cb3ceec416..09674c51d3 100644 Binary files a/ui/pages/__screenshots__/Marketplace.pw.tsx_default_base-view-dark-mode-1.png and b/ui/pages/__screenshots__/Marketplace.pw.tsx_default_base-view-dark-mode-1.png differ diff --git a/ui/pages/__screenshots__/Marketplace.pw.tsx_default_mobile-base-view-1.png b/ui/pages/__screenshots__/Marketplace.pw.tsx_default_mobile-base-view-1.png index 7bf36b50ef..551b1f747a 100644 Binary files a/ui/pages/__screenshots__/Marketplace.pw.tsx_default_mobile-base-view-1.png and b/ui/pages/__screenshots__/Marketplace.pw.tsx_default_mobile-base-view-1.png differ diff --git a/ui/pages/__screenshots__/Marketplace.pw.tsx_default_mobile-with-banner-1.png b/ui/pages/__screenshots__/Marketplace.pw.tsx_default_mobile-with-banner-1.png index 4a898c8e79..82f6953169 100644 Binary files a/ui/pages/__screenshots__/Marketplace.pw.tsx_default_mobile-with-banner-1.png and b/ui/pages/__screenshots__/Marketplace.pw.tsx_default_mobile-with-banner-1.png differ diff --git a/ui/pages/__screenshots__/Marketplace.pw.tsx_default_mobile-with-featured-app-1.png b/ui/pages/__screenshots__/Marketplace.pw.tsx_default_mobile-with-featured-app-1.png index 9b3b3d0548..fd4f1e934f 100644 Binary files a/ui/pages/__screenshots__/Marketplace.pw.tsx_default_mobile-with-featured-app-1.png and b/ui/pages/__screenshots__/Marketplace.pw.tsx_default_mobile-with-featured-app-1.png differ diff --git a/ui/pages/__screenshots__/Marketplace.pw.tsx_default_mobile-with-scores-1.png b/ui/pages/__screenshots__/Marketplace.pw.tsx_default_mobile-with-scores-1.png deleted file mode 100644 index 7074547aed..0000000000 Binary files a/ui/pages/__screenshots__/Marketplace.pw.tsx_default_mobile-with-scores-1.png and /dev/null differ diff --git a/ui/pages/__screenshots__/Marketplace.pw.tsx_default_with-banner-dark-mode-1.png b/ui/pages/__screenshots__/Marketplace.pw.tsx_default_with-banner-dark-mode-1.png index 70648d636c..cae5e115fc 100644 Binary files a/ui/pages/__screenshots__/Marketplace.pw.tsx_default_with-banner-dark-mode-1.png and b/ui/pages/__screenshots__/Marketplace.pw.tsx_default_with-banner-dark-mode-1.png differ diff --git a/ui/pages/__screenshots__/Marketplace.pw.tsx_default_with-featured-app-dark-mode-1.png b/ui/pages/__screenshots__/Marketplace.pw.tsx_default_with-featured-app-dark-mode-1.png index 5f86e00765..92991e8748 100644 Binary files a/ui/pages/__screenshots__/Marketplace.pw.tsx_default_with-featured-app-dark-mode-1.png and b/ui/pages/__screenshots__/Marketplace.pw.tsx_default_with-featured-app-dark-mode-1.png differ diff --git a/ui/pages/__screenshots__/Marketplace.pw.tsx_default_with-scores-dark-mode-1.png b/ui/pages/__screenshots__/Marketplace.pw.tsx_default_with-scores-dark-mode-1.png deleted file mode 100644 index 5b1d949cb8..0000000000 Binary files a/ui/pages/__screenshots__/Marketplace.pw.tsx_default_with-scores-dark-mode-1.png and /dev/null differ diff --git a/ui/shared/solidityscanReport/SolidityscanReportButton.tsx b/ui/shared/solidityscanReport/SolidityscanReportButton.tsx index c76ba58b03..c099ef70b6 100644 --- a/ui/shared/solidityscanReport/SolidityscanReportButton.tsx +++ b/ui/shared/solidityscanReport/SolidityscanReportButton.tsx @@ -1,7 +1,8 @@ -import { Button, Spinner, Tooltip, useColorModeValue } from '@chakra-ui/react'; +import { Button, Spinner, Tooltip, useColorModeValue, chakra } from '@chakra-ui/react'; import React from 'react'; import useIsMobile from 'lib/hooks/useIsMobile'; +import usePreventFocusAfterModalClosing from 'lib/hooks/usePreventFocusAfterModalClosing'; import IconSvg from 'ui/shared/IconSvg'; import useScoreLevelAndColor from './useScoreLevelAndColor'; @@ -11,21 +12,24 @@ interface Props { isLoading?: boolean; onlyIcon?: boolean; onClick?: () => void; - label?: string; + label?: string | React.ReactElement; isActive: boolean; + className?: string; } const SolidityscanReportButton = ( - { score, isLoading, onlyIcon, onClick, label = 'Security score', isActive }: Props, + { score, isLoading, onlyIcon, onClick, label = 'Security score', isActive, className }: Props, ref: React.ForwardedRef, ) => { const { scoreColor } = useScoreLevelAndColor(score); const colorLoading = useColorModeValue('gray.300', 'gray.600'); const isMobile = useIsMobile(); + const onFocusCapture = usePreventFocusAfterModalClosing(); return ( - + + + ); +}; + +export default chakra(React.forwardRef(ButtonDesktop)); diff --git a/ui/shared/sort/SortButton.tsx b/ui/shared/sort/ButtonMobile.tsx similarity index 77% rename from ui/shared/sort/SortButton.tsx rename to ui/shared/sort/ButtonMobile.tsx index dc28d17c55..b05f5bf1ce 100644 --- a/ui/shared/sort/SortButton.tsx +++ b/ui/shared/sort/ButtonMobile.tsx @@ -10,13 +10,14 @@ type Props = { isLoading?: boolean; } -const SortButton = ({ onClick, isActive, className, isLoading }: Props) => { +const ButtonMobile = ({ onClick, isActive, className, isLoading }: Props, ref: React.ForwardedRef) => { if (isLoading) { return ; } return ( } aria-label="sort" size="sm" @@ -31,4 +32,4 @@ const SortButton = ({ onClick, isActive, className, isLoading }: Props) => { ); }; -export default chakra(SortButton); +export default chakra(React.forwardRef(ButtonMobile)); diff --git a/ui/shared/sort/Option.tsx b/ui/shared/sort/Option.tsx new file mode 100644 index 0000000000..c24eaada0b --- /dev/null +++ b/ui/shared/sort/Option.tsx @@ -0,0 +1,47 @@ +import { + useRadio, + Box, + useColorModeValue, +} from '@chakra-ui/react'; +import type { useRadioGroup } from '@chakra-ui/react'; +import React from 'react'; + +import IconSvg from 'ui/shared/IconSvg'; + +export interface TOption { + id: Sort | undefined; + title: string; +} + +type OptionProps = ReturnType['getRadioProps']>; + +const Option = (props: OptionProps) => { + const { getInputProps, getRadioProps } = useRadio(props); + + const input = getInputProps(); + const checkbox = getRadioProps(); + const bgColorHover = useColorModeValue('blue.50', 'whiteAlpha.100'); + + return ( + + + + { props.children } + + { props.isChecked && } + + ); +}; + +export default Option; diff --git a/ui/shared/sort/Sort.tsx b/ui/shared/sort/Sort.tsx index 37b378abf4..abb2fe6f6a 100644 --- a/ui/shared/sort/Sort.tsx +++ b/ui/shared/sort/Sort.tsx @@ -1,58 +1,70 @@ import { - chakra, - Menu, - MenuButton, - MenuList, - MenuOptionGroup, - MenuItemOption, + Popover, + PopoverTrigger, + PopoverContent, + PopoverBody, useDisclosure, + useRadioGroup, + chakra, } from '@chakra-ui/react'; import React from 'react'; -import SortButton from './SortButton'; +import useIsMobile from 'lib/hooks/useIsMobile'; -export interface Option { - title: string; - id: Sort | undefined; -} +import SortButtonDesktop from './ButtonDesktop'; +import SortButtonMobile from './ButtonMobile'; +import Option from './Option'; +import type { TOption } from './Option'; interface Props { - options: Array>; - sort: Sort | undefined; - setSort: (value: Sort | undefined) => void; + name: string; + options: Array>; + defaultValue?: Sort; isLoading?: boolean; + onChange: (value: Sort | undefined) => void; } -const Sort = ({ sort, setSort, options, isLoading }: Props) => { - const { isOpen, onToggle } = useDisclosure(); +const Sort = ({ name, options, isLoading, onChange, defaultValue }: Props) => { + const isMobile = useIsMobile(false); + const { isOpen, onToggle, onClose } = useDisclosure(); + + const handleChange = (value: Sort) => { + onChange(value); + onClose(); + }; + + const { value, getRootProps, getRadioProps } = useRadioGroup({ + name, + defaultValue, + onChange: handleChange, + }); - const setSortingFromMenu = React.useCallback((val: string | Array) => { - const value = val as Sort | Array; - setSort(Array.isArray(value) ? value[0] : value); - }, [ setSort ]); + const root = getRootProps(); return ( - - - - - - - { options.map((option) => ( - - { option.title } - - )) } - - - + + + { isMobile ? ( + + ) : ( + + { options.find((option: TOption) => option.id === value || (!option.id && !value))?.title } + + ) } + + + + { options.map((option, index) => { + const radio = getRadioProps({ value: option.id }); + return ( + + ); + }) } + + + ); }; diff --git a/ui/shared/sort/getSortValueFromQuery.ts b/ui/shared/sort/getSortValueFromQuery.ts index 9e8f823c60..efb07981ce 100644 --- a/ui/shared/sort/getSortValueFromQuery.ts +++ b/ui/shared/sort/getSortValueFromQuery.ts @@ -1,8 +1,8 @@ import type { Query } from 'nextjs-routes'; -import type { Option } from 'ui/shared/sort/Sort'; +import type { TOption } from 'ui/shared/sort/Option'; -export default function getSortValueFromQuery(query: Query, sortOptions: Array>) { +export default function getSortValueFromQuery(query: Query, sortOptions: Array>) { if (!query.sort || !query.order) { return undefined; } diff --git a/ui/tokens/TokensActionBar.tsx b/ui/tokens/TokensActionBar.tsx index 01077c9836..bca2d05112 100644 --- a/ui/tokens/TokensActionBar.tsx +++ b/ui/tokens/TokensActionBar.tsx @@ -45,9 +45,10 @@ const TokensActionBar = ({ { filter } { searchInput } diff --git a/ui/tokens/utils.ts b/ui/tokens/utils.ts index 804271ccbc..1069626b63 100644 --- a/ui/tokens/utils.ts +++ b/ui/tokens/utils.ts @@ -4,9 +4,9 @@ import type { TokensSortingValue } from 'types/api/tokens'; import config from 'configs/app'; import getFilterValuesFromQuery from 'lib/getFilterValuesFromQuery'; import { TOKEN_TYPE_IDS } from 'lib/token/tokenTypes'; -import type { Option } from 'ui/shared/sort/Sort'; +import type { TOption } from 'ui/shared/sort/Option'; -export const SORT_OPTIONS: Array> = [ +export const SORT_OPTIONS: Array> = [ { title: 'Default', id: undefined }, { title: 'Price ascending', id: 'fiat_value-asc' }, { title: 'Price descending', id: 'fiat_value-desc' }, diff --git a/ui/txs/TxsHeaderMobile.tsx b/ui/txs/TxsHeaderMobile.tsx index 9efde1de84..cad7d45840 100644 --- a/ui/txs/TxsHeaderMobile.tsx +++ b/ui/txs/TxsHeaderMobile.tsx @@ -30,9 +30,10 @@ const TxsHeaderMobile = ({ filterComponent, sorting, setSorting, paginationProps { filterComponent } { /* api is not implemented */ } diff --git a/ui/txs/useTxsSort.tsx b/ui/txs/useTxsSort.tsx index c2ec9d8377..37e49cdf03 100644 --- a/ui/txs/useTxsSort.tsx +++ b/ui/txs/useTxsSort.tsx @@ -5,11 +5,11 @@ import type { TransactionsSortingValue, TxsResponse } from 'types/api/transactio import type { ResourceError } from 'lib/api/resources'; import * as cookies from 'lib/cookies'; -import type { Option } from 'ui/shared/sort/Sort'; +import type { TOption } from 'ui/shared/sort/Option'; import sortTxs from './sortTxs'; -export const SORT_OPTIONS: Array> = [ +export const SORT_OPTIONS: Array> = [ { title: 'Default', id: undefined }, { title: 'Value ascending', id: 'value-asc' }, { title: 'Value descending', id: 'value-desc' }, diff --git a/ui/validators/utils.ts b/ui/validators/utils.ts index d35b6dce11..71558189c3 100644 --- a/ui/validators/utils.ts +++ b/ui/validators/utils.ts @@ -1,8 +1,8 @@ import type { ValidatorsSortingValue, ValidatorsSortingField } from 'types/api/validators'; -import type { Option } from 'ui/shared/sort/Sort'; +import type { TOption } from 'ui/shared/sort/Option'; -export const SORT_OPTIONS: Array> = [ +export const SORT_OPTIONS: Array> = [ { title: 'Default', id: undefined }, { title: 'Status descending', id: 'state-desc' }, { title: 'Status ascending', id: 'state-asc' }, diff --git a/ui/verifiedContracts/utils.ts b/ui/verifiedContracts/utils.ts index caf609899f..3f6a13bf65 100644 --- a/ui/verifiedContracts/utils.ts +++ b/ui/verifiedContracts/utils.ts @@ -1,8 +1,8 @@ import type { VerifiedContractsSortingValue, VerifiedContractsSortingField } from 'types/api/verifiedContracts'; -import type { Option } from 'ui/shared/sort/Sort'; +import type { TOption } from 'ui/shared/sort/Option'; -export const SORT_OPTIONS: Array> = [ +export const SORT_OPTIONS: Array> = [ { title: 'Default', id: undefined }, { title: 'Balance descending', id: 'balance-desc' }, { title: 'Balance ascending', id: 'balance-asc' },
AppContracts scoreTotalVerified
- - : - - } - /> - - - - - - - - { securityReport?.overallInfo.totalContractsNumber ?? 0 } - - - - { securityReport?.overallInfo.verifiedNumber ?? 0 } - - - Data will be available soon - - -