Commit 89b91fc2 authored by Max Alekseenko's avatar Max Alekseenko

create contract list modal

parent 37f66880
...@@ -28,3 +28,9 @@ export enum MarketplaceCategory { ...@@ -28,3 +28,9 @@ export enum MarketplaceCategory {
ALL = 'All', ALL = 'All',
FAVORITES = 'Favorites', FAVORITES = 'Favorites',
} }
export enum ContractListTypes {
ANALYZED = 'Analyzed',
ALL = 'All',
VERIFIED = 'Verified',
}
import {
Grid, Modal, ModalBody,
ModalCloseButton, ModalContent, ModalHeader, ModalOverlay,
} from '@chakra-ui/react';
import React from 'react';
import { ContractListTypes } from 'types/client/marketplace';
import useIsMobile from 'lib/hooks/useIsMobile';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import ContractSecurityReport from './ContractSecurityReport';
type Props = {
onClose: () => void;
type: ContractListTypes;
contracts: Array<any>; // eslint-disable-line @typescript-eslint/no-explicit-any
}
const ContractListModal = ({ onClose, type, contracts }: Props) => {
const isMobile = useIsMobile();
const displayedContracts = React.useMemo(() => {
switch (type) {
default:
case ContractListTypes.ALL:
return contracts;
case ContractListTypes.ANALYZED:
return contracts
.filter((contract) => Boolean(contract.solidityScanReport))
.sort((a, b) => b.solidityScanReport.scan_summary.score_v2 - a.solidityScanReport.scan_summary.score_v2);
case ContractListTypes.VERIFIED:
return contracts.filter((contract) => contract.isVerified);
}
}, [ contracts, type ]);
return (
<Modal
isOpen={ Boolean(type) }
onClose={ onClose }
size={ isMobile ? 'full' : 'md' }
isCentered
>
<ModalOverlay/>
<ModalContent>
<ModalHeader fontWeight="500" textStyle="h3" mb={ 4 }>Contracts</ModalHeader>
<ModalCloseButton/>
<ModalBody maxH="352px" overflow="scroll" mb={ 0 } display="flex" flexDirection="column" gap={ 2 }>
{ displayedContracts.map((contract) => (
<Grid key={ contract.address } height={ 8 } alignItems="center" gap={ 6 } templateColumns="max-content 1fr">
{ type === ContractListTypes.ANALYZED && (
<ContractSecurityReport securityReport={ contract.solidityScanReport }/>
) }
<AddressEntity
address={{
hash: contract.address,
name: contract.solidityScanReport?.contractname,
is_contract: true,
is_verified: contract.isVerified,
}}
noCopy
/>
</Grid>
)) }
</ModalBody>
</ModalContent>
</Modal>
);
};
export default ContractListModal;
import { Box, Text } from '@chakra-ui/react';
import React from 'react';
import config from 'configs/app';
import LinkExternal from 'ui/shared/LinkExternal';
import SolidityscanReportButton from 'ui/shared/solidityscanReport/SolidityscanReportButton';
import SolidityscanReportDetails from 'ui/shared/solidityscanReport/SolidityscanReportDetails';
import SolidityscanReportScore from 'ui/shared/solidityscanReport/SolidityscanReportScore';
type Props = {
securityReport?: any; // eslint-disable-line @typescript-eslint/no-explicit-any
}
const ContractSecurityReport = ({ securityReport }: Props) => {
const {
scanner_reference_url: url,
scan_summary: {
score_v2: securityScore,
issue_severity_distribution: issueSeverityDistribution,
},
} = securityReport;
const totalIssues = Object.values(issueSeverityDistribution as Record<string, number>).reduce((acc, val) => acc + val, 0);
return (
<SolidityscanReportButton
score={ securityScore }
popoverContent={ (
<>
<Box mb={ 5 }>
The security score was derived from evaluating the smart contracts of a protocol on the { config.chain.name } network.
</Box>
<SolidityscanReportScore score={ securityScore }/>
{ issueSeverityDistribution && totalIssues > 0 && (
<Box mb={ 5 }>
<Text py="7px" variant="secondary" fontSize="xs" fontWeight={ 500 }>Threat score & vulnerabilities</Text>
<SolidityscanReportDetails vulnerabilities={ issueSeverityDistribution } vulnerabilitiesCount={ totalIssues }/>
</Box>
) }
<LinkExternal href={ url }>View full report</LinkExternal>
</>
) }
/>
);
};
export default ContractSecurityReport;
...@@ -2,7 +2,7 @@ import { Hide, Show } from '@chakra-ui/react'; ...@@ -2,7 +2,7 @@ import { Hide, Show } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { MouseEvent } from 'react'; import type { MouseEvent } from 'react';
import type { MarketplaceAppPreview } from 'types/client/marketplace'; import type { MarketplaceAppPreview, ContractListTypes } from 'types/client/marketplace';
import { MarketplaceCategory } from 'types/client/marketplace'; import { MarketplaceCategory } from 'types/client/marketplace';
import config from 'configs/app'; import config from 'configs/app';
...@@ -23,6 +23,7 @@ interface Props { ...@@ -23,6 +23,7 @@ interface Props {
selectedCategoryId?: string; selectedCategoryId?: string;
onAppClick: (event: MouseEvent, id: string) => void; onAppClick: (event: MouseEvent, id: string) => void;
securityReports: Array<any> | undefined; // eslint-disable-line @typescript-eslint/no-explicit-any securityReports: Array<any> | undefined; // eslint-disable-line @typescript-eslint/no-explicit-any
showContractList: (id: string, type: ContractListTypes) => void;
} }
const MarketplaceListWithScores = ({ const MarketplaceListWithScores = ({
...@@ -34,6 +35,7 @@ const MarketplaceListWithScores = ({ ...@@ -34,6 +35,7 @@ const MarketplaceListWithScores = ({
selectedCategoryId, selectedCategoryId,
onAppClick, onAppClick,
securityReports = [], securityReports = [],
showContractList,
}: Props) => { }: Props) => {
const displayedApps = React.useMemo(() => const displayedApps = React.useMemo(() =>
...@@ -63,6 +65,7 @@ const MarketplaceListWithScores = ({ ...@@ -63,6 +65,7 @@ const MarketplaceListWithScores = ({
onFavoriteClick={ onFavoriteClick } onFavoriteClick={ onFavoriteClick }
isLoading={ isLoading } isLoading={ isLoading }
onAppClick={ onAppClick } onAppClick={ onAppClick }
showContractList={ showContractList }
/> />
)) } )) }
</Show> </Show>
...@@ -74,6 +77,7 @@ const MarketplaceListWithScores = ({ ...@@ -74,6 +77,7 @@ const MarketplaceListWithScores = ({
favoriteApps={ favoriteApps } favoriteApps={ favoriteApps }
onFavoriteClick={ onFavoriteClick } onFavoriteClick={ onFavoriteClick }
onInfoClick={ showAppInfo } onInfoClick={ showAppInfo }
showContractList={ showContractList }
/> />
</Hide> </Hide>
</> </>
......
import { Box, Text } from '@chakra-ui/react'; import { Box, Text, Link } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import { ContractListTypes } from 'types/client/marketplace';
import config from 'configs/app'; import config from 'configs/app';
import { apos } from 'lib/html-entities'; import { apos } from 'lib/html-entities';
import LinkExternal from 'ui/shared/LinkExternal';
import SolidityscanReportButton from 'ui/shared/solidityscanReport/SolidityscanReportButton'; import SolidityscanReportButton from 'ui/shared/solidityscanReport/SolidityscanReportButton';
import SolidityscanReportDetails from 'ui/shared/solidityscanReport/SolidityscanReportDetails'; import SolidityscanReportDetails from 'ui/shared/solidityscanReport/SolidityscanReportDetails';
import SolidityscanReportScore from 'ui/shared/solidityscanReport/SolidityscanReportScore'; import SolidityscanReportScore from 'ui/shared/solidityscanReport/SolidityscanReportScore';
type Props = { type Props = {
id: string;
securityReport?: any; // eslint-disable-line @typescript-eslint/no-explicit-any securityReport?: any; // eslint-disable-line @typescript-eslint/no-explicit-any
isLarge?: boolean; isLarge?: boolean;
showContractList: (id: string, type: ContractListTypes) => void;
} }
const AppSecurityReport = ({ securityReport, isLarge }: Props) => { const AppSecurityReport = ({ id, securityReport, isLarge, showContractList }: Props) => {
const { const {
overallInfo: { overallInfo: {
securityScore, securityScore,
...@@ -23,6 +26,11 @@ const AppSecurityReport = ({ securityReport, isLarge }: Props) => { ...@@ -23,6 +26,11 @@ const AppSecurityReport = ({ securityReport, isLarge }: Props) => {
}, },
} = securityReport; } = securityReport;
const showAnalyzedContracts = React.useCallback(() => {
showContractList(id, ContractListTypes.ANALYZED);
}, [ showContractList, id ]);
return ( return (
<SolidityscanReportButton <SolidityscanReportButton
height={ isLarge ? undefined : '30px' } height={ isLarge ? undefined : '30px' }
...@@ -40,7 +48,7 @@ const AppSecurityReport = ({ securityReport, isLarge }: Props) => { ...@@ -40,7 +48,7 @@ const AppSecurityReport = ({ securityReport, isLarge }: Props) => {
<SolidityscanReportDetails vulnerabilities={ issueSeverityDistribution } vulnerabilitiesCount={ totalIssues }/> <SolidityscanReportDetails vulnerabilities={ issueSeverityDistribution } vulnerabilitiesCount={ totalIssues }/>
</Box> </Box>
) } ) }
<LinkExternal href="#">Analyzed contracts</LinkExternal> <Link onClick={ showAnalyzedContracts }>Analyzed contracts</Link>
</> </>
) } ) }
/> />
......
...@@ -2,7 +2,7 @@ import { Flex, IconButton } from '@chakra-ui/react'; ...@@ -2,7 +2,7 @@ import { Flex, IconButton } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { MouseEvent } from 'react'; import type { MouseEvent } from 'react';
import type { MarketplaceAppPreview } from 'types/client/marketplace'; import type { MarketplaceAppPreview, ContractListTypes } from 'types/client/marketplace';
import * as mixpanel from 'lib/mixpanel/index'; import * as mixpanel from 'lib/mixpanel/index';
import IconSvg from 'ui/shared/IconSvg'; import IconSvg from 'ui/shared/IconSvg';
...@@ -19,9 +19,10 @@ type Props = { ...@@ -19,9 +19,10 @@ type Props = {
onFavoriteClick: (id: string, isFavorite: boolean) => void; onFavoriteClick: (id: string, isFavorite: boolean) => void;
isLoading: boolean; isLoading: boolean;
onAppClick: (event: MouseEvent, id: string) => void; onAppClick: (event: MouseEvent, id: string) => void;
showContractList: (id: string, type: ContractListTypes) => void;
} }
const ListItem = ({ app, onInfoClick, isFavorite, onFavoriteClick, isLoading, onAppClick }: Props) => { const ListItem = ({ app, onInfoClick, isFavorite, onFavoriteClick, isLoading, onAppClick, showContractList }: Props) => {
const { const {
id, id,
securityReport, securityReport,
...@@ -78,7 +79,7 @@ const ListItem = ({ app, onInfoClick, isFavorite, onFavoriteClick, isLoading, on ...@@ -78,7 +79,7 @@ const ListItem = ({ app, onInfoClick, isFavorite, onFavoriteClick, isLoading, on
</Flex> </Flex>
<Flex alignItems="center"> <Flex alignItems="center">
<Flex flex={ 1 } gap={ 3 } alignItems="center"> <Flex flex={ 1 } gap={ 3 } alignItems="center">
<AppSecurityReport securityReport={ securityReport }/> <AppSecurityReport id={ id } securityReport={ securityReport } showContractList={ showContractList }/>
<LinkButton onClick={ handleInfoClick } icon="contracts">{ totalContractsNumber }</LinkButton> <LinkButton onClick={ handleInfoClick } icon="contracts">{ totalContractsNumber }</LinkButton>
<LinkButton onClick={ handleInfoClick } icon="contracts_verified" iconColor="green.500">{ verifiedNumber }</LinkButton> <LinkButton onClick={ handleInfoClick } icon="contracts_verified" iconColor="green.500">{ verifiedNumber }</LinkButton>
</Flex> </Flex>
......
...@@ -2,7 +2,7 @@ import { Table as ChakraTable, Tbody, Th, Tr } from '@chakra-ui/react'; ...@@ -2,7 +2,7 @@ import { Table as ChakraTable, Tbody, Th, Tr } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { MouseEvent } from 'react'; import type { MouseEvent } from 'react';
import type { MarketplaceAppPreview } from 'types/client/marketplace'; import type { MarketplaceAppPreview, ContractListTypes } from 'types/client/marketplace';
import { default as Thead } from 'ui/shared/TheadSticky'; import { default as Thead } from 'ui/shared/TheadSticky';
...@@ -15,9 +15,10 @@ type Props = { ...@@ -15,9 +15,10 @@ type Props = {
onFavoriteClick: (id: string, isFavorite: boolean) => void; onFavoriteClick: (id: string, isFavorite: boolean) => void;
onAppClick: (event: MouseEvent, id: string) => void; onAppClick: (event: MouseEvent, id: string) => void;
onInfoClick: (id: string) => void; onInfoClick: (id: string) => void;
showContractList: (id: string, type: ContractListTypes) => void;
} }
const Table = ({ apps, isLoading, favoriteApps, onFavoriteClick, onAppClick, onInfoClick }: Props) => { const Table = ({ apps, isLoading, favoriteApps, onFavoriteClick, onAppClick, onInfoClick, showContractList }: Props) => {
return ( return (
<ChakraTable> <ChakraTable>
<Thead top={ 0 }> <Thead top={ 0 }>
...@@ -40,6 +41,7 @@ const Table = ({ apps, isLoading, favoriteApps, onFavoriteClick, onAppClick, onI ...@@ -40,6 +41,7 @@ const Table = ({ apps, isLoading, favoriteApps, onFavoriteClick, onAppClick, onI
onFavoriteClick={ onFavoriteClick } onFavoriteClick={ onFavoriteClick }
onAppClick={ onAppClick } onAppClick={ onAppClick }
onInfoClick={ onInfoClick } onInfoClick={ onInfoClick }
showContractList={ showContractList }
/> />
)) } )) }
</Tbody> </Tbody>
......
...@@ -3,6 +3,7 @@ import React from 'react'; ...@@ -3,6 +3,7 @@ import React from 'react';
import type { MouseEvent } from 'react'; import type { MouseEvent } from 'react';
import type { MarketplaceAppPreview } from 'types/client/marketplace'; import type { MarketplaceAppPreview } from 'types/client/marketplace';
import { ContractListTypes } from 'types/client/marketplace';
import * as mixpanel from 'lib/mixpanel/index'; import * as mixpanel from 'lib/mixpanel/index';
import IconSvg from 'ui/shared/IconSvg'; import IconSvg from 'ui/shared/IconSvg';
...@@ -18,6 +19,7 @@ type Props = { ...@@ -18,6 +19,7 @@ type Props = {
onFavoriteClick: (id: string, isFavorite: boolean) => void; onFavoriteClick: (id: string, isFavorite: boolean) => void;
onAppClick: (event: MouseEvent, id: string) => void; onAppClick: (event: MouseEvent, id: string) => void;
onInfoClick: (id: string) => void; onInfoClick: (id: string) => void;
showContractList: (id: string, type: ContractListTypes) => void;
} }
const TableItem = ({ const TableItem = ({
...@@ -27,6 +29,7 @@ const TableItem = ({ ...@@ -27,6 +29,7 @@ const TableItem = ({
onFavoriteClick, onFavoriteClick,
onAppClick, onAppClick,
onInfoClick, onInfoClick,
showContractList,
}: Props) => { }: Props) => {
const { const {
...@@ -50,6 +53,14 @@ const TableItem = ({ ...@@ -50,6 +53,14 @@ const TableItem = ({
onFavoriteClick(id, isFavorite); onFavoriteClick(id, isFavorite);
}, [ onFavoriteClick, id, isFavorite ]); }, [ onFavoriteClick, id, isFavorite ]);
const showAllContracts = React.useCallback(() => {
showContractList(id, ContractListTypes.ALL);
}, [ showContractList, id ]);
const showVerifiedContracts = React.useCallback(() => {
showContractList(id, ContractListTypes.VERIFIED);
}, [ showContractList, id ]);
return ( return (
<Tr> <Tr>
<Td verticalAlign="middle" px={ 2 }> <Td verticalAlign="middle" px={ 2 }>
...@@ -71,13 +82,13 @@ const TableItem = ({ ...@@ -71,13 +82,13 @@ const TableItem = ({
<AppLink app={ app } isLoading={ isLoading } onAppClick={ onAppClick } isLarge/> <AppLink app={ app } isLoading={ isLoading } onAppClick={ onAppClick } isLarge/>
</Td> </Td>
<Td verticalAlign="middle"> <Td verticalAlign="middle">
<AppSecurityReport securityReport={ securityReport } isLarge/> <AppSecurityReport id={ id } securityReport={ securityReport } showContractList={ showContractList } isLarge/>
</Td> </Td>
<Td verticalAlign="middle"> <Td verticalAlign="middle">
<LinkButton onClick={ handleInfoClick } icon="contracts">{ totalContractsNumber }</LinkButton> <LinkButton onClick={ showAllContracts } icon="contracts">{ totalContractsNumber }</LinkButton>
</Td> </Td>
<Td verticalAlign="middle"> <Td verticalAlign="middle">
<LinkButton onClick={ handleInfoClick } icon="contracts_verified" iconColor="green.500">{ verifiedNumber }</LinkButton> <LinkButton onClick={ showVerifiedContracts } icon="contracts_verified" iconColor="green.500">{ verifiedNumber }</LinkButton>
</Td> </Td>
<Td verticalAlign="middle" isNumeric> <Td verticalAlign="middle" isNumeric>
<LinkButton onClick={ handleInfoClick }>More info</LinkButton> <LinkButton onClick={ handleInfoClick }>More info</LinkButton>
......
...@@ -2,6 +2,7 @@ import _pickBy from 'lodash/pickBy'; ...@@ -2,6 +2,7 @@ import _pickBy from 'lodash/pickBy';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import type { ContractListTypes } from 'types/client/marketplace';
import { MarketplaceCategory } from 'types/client/marketplace'; import { MarketplaceCategory } from 'types/client/marketplace';
import useDebounce from 'lib/hooks/useDebounce'; import useDebounce from 'lib/hooks/useDebounce';
...@@ -33,6 +34,7 @@ export default function useMarketplace() { ...@@ -33,6 +34,7 @@ export default function useMarketplace() {
const [ isFavoriteAppsLoaded, setIsFavoriteAppsLoaded ] = React.useState<boolean>(false); const [ isFavoriteAppsLoaded, setIsFavoriteAppsLoaded ] = React.useState<boolean>(false);
const [ isAppInfoModalOpen, setIsAppInfoModalOpen ] = React.useState<boolean>(false); const [ isAppInfoModalOpen, setIsAppInfoModalOpen ] = React.useState<boolean>(false);
const [ isDisclaimerModalOpen, setIsDisclaimerModalOpen ] = React.useState<boolean>(false); const [ isDisclaimerModalOpen, setIsDisclaimerModalOpen ] = React.useState<boolean>(false);
const [ contractListModalType, setContractListModalType ] = React.useState<ContractListTypes | null>(null);
const handleFavoriteClick = React.useCallback((id: string, isFavorite: boolean) => { const handleFavoriteClick = React.useCallback((id: string, isFavorite: boolean) => {
mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'Favorite app', Info: id }); mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'Favorite app', Info: id });
...@@ -60,11 +62,17 @@ export default function useMarketplace() { ...@@ -60,11 +62,17 @@ export default function useMarketplace() {
setIsDisclaimerModalOpen(true); setIsDisclaimerModalOpen(true);
}, []); }, []);
const showContractList = React.useCallback((id: string, type: ContractListTypes) => {
setSelectedAppId(id);
setContractListModalType(type);
}, []);
const debouncedFilterQuery = useDebounce(filterQuery, 500); const debouncedFilterQuery = useDebounce(filterQuery, 500);
const clearSelectedAppId = React.useCallback(() => { const clearSelectedAppId = React.useCallback(() => {
setSelectedAppId(null); setSelectedAppId(null);
setIsAppInfoModalOpen(false); setIsAppInfoModalOpen(false);
setIsDisclaimerModalOpen(false); setIsDisclaimerModalOpen(false);
setContractListModalType(null);
}, []); }, []);
const handleCategoryChange = React.useCallback((newCategory: string) => { const handleCategoryChange = React.useCallback((newCategory: string) => {
...@@ -133,6 +141,8 @@ export default function useMarketplace() { ...@@ -133,6 +141,8 @@ export default function useMarketplace() {
showDisclaimer, showDisclaimer,
appsTotal: data?.length || 0, appsTotal: data?.length || 0,
isCategoriesPlaceholderData, isCategoriesPlaceholderData,
showContractList,
contractListModalType,
}), [ }), [
selectedCategoryId, selectedCategoryId,
categories, categories,
...@@ -152,5 +162,7 @@ export default function useMarketplace() { ...@@ -152,5 +162,7 @@ export default function useMarketplace() {
showDisclaimer, showDisclaimer,
data?.length, data?.length,
isCategoriesPlaceholderData, isCategoriesPlaceholderData,
showContractList,
contractListModalType,
]); ]);
} }
...@@ -10,6 +10,7 @@ import { useAppContext } from 'lib/contexts/app'; ...@@ -10,6 +10,7 @@ import { useAppContext } from 'lib/contexts/app';
import * as cookies from 'lib/cookies'; import * as cookies from 'lib/cookies';
import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError'; import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import ContractListModal from 'ui/marketplace/ContractListModal';
import MarketplaceAppModal from 'ui/marketplace/MarketplaceAppModal'; import MarketplaceAppModal from 'ui/marketplace/MarketplaceAppModal';
import MarketplaceDisclaimerModal from 'ui/marketplace/MarketplaceDisclaimerModal'; import MarketplaceDisclaimerModal from 'ui/marketplace/MarketplaceDisclaimerModal';
import MarketplaceList from 'ui/marketplace/MarketplaceList'; import MarketplaceList from 'ui/marketplace/MarketplaceList';
...@@ -68,6 +69,8 @@ const Marketplace = () => { ...@@ -68,6 +69,8 @@ const Marketplace = () => {
showDisclaimer, showDisclaimer,
appsTotal, appsTotal,
isCategoriesPlaceholderData, isCategoriesPlaceholderData,
showContractList,
contractListModalType,
} = useMarketplace(); } = useMarketplace();
const { const {
...@@ -134,6 +137,10 @@ const Marketplace = () => { ...@@ -134,6 +137,10 @@ const Marketplace = () => {
} }
const selectedApp = displayedApps.find(app => app.id === selectedAppId); const selectedApp = displayedApps.find(app => app.id === selectedAppId);
const selectedAppContractList = securityReports
?.find(item => item.appName === selectedAppId)
?.chainsData[config.chain.name?.toLowerCase() || '']
?.contractsData;
return ( return (
<> <>
...@@ -214,6 +221,7 @@ const Marketplace = () => { ...@@ -214,6 +221,7 @@ const Marketplace = () => {
selectedCategoryId={ selectedCategoryId } selectedCategoryId={ selectedCategoryId }
onAppClick={ handleAppClick } onAppClick={ handleAppClick }
securityReports={ securityReports } securityReports={ securityReports }
showContractList={ showContractList }
/> />
) : ( ) : (
<MarketplaceList <MarketplaceList
...@@ -243,6 +251,14 @@ const Marketplace = () => { ...@@ -243,6 +251,14 @@ const Marketplace = () => {
appId={ selectedApp.id } appId={ selectedApp.id }
/> />
) } ) }
{ (selectedApp && contractListModalType) && (
<ContractListModal
type={ contractListModalType }
contracts={ selectedAppContractList }
onClose={ clearSelectedAppId }
/>
) }
</> </>
); );
}; };
......
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