Commit 0d75bac8 authored by tom's avatar tom

network explorers popover

parent e0d2882e
# ui config # ui config
NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/featured-networks/eth-goerli.json NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/featured-networks/eth-goerli.json
NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Anyblock','baseUrl':'https://explorer.anyblock.tools','paths':{'tx':'/ethereum/ethereum/goerli/transaction','address':'/ethereum/ethereum/goerli/address'}},{'title':'Etherscan','baseUrl':'https://goerli.etherscan.io/','paths':{'tx':'/tx','address':'/address'}}] NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Bitquery','baseUrl':'https://explorer.bitquery.io/','paths':{'tx':'/goerli/tx','address':'/goerli/address','token':'/goerli/token'}},{'title':'Etherscan','baseUrl':'https://goerli.etherscan.io/','paths':{'tx':'/tx','address':'/address','token':'/token'}}]
NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0xf7d4972356e6ae44ae948d0cf19ef2beaf0e574c180997e969a2837da15e349d NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0xf7d4972356e6ae44ae948d0cf19ef2beaf0e574c180997e969a2837da15e349d
# network config # network config
......
...@@ -15,6 +15,7 @@ export interface NetworkExplorer { ...@@ -15,6 +15,7 @@ export interface NetworkExplorer {
paths: { paths: {
tx?: string; tx?: string;
address?: string; address?: string;
token?: string;
}; };
} }
......
...@@ -14,6 +14,7 @@ import { useAppContext } from 'lib/appContext'; ...@@ -14,6 +14,7 @@ import { useAppContext } from 'lib/appContext';
import useContractTabs from 'lib/hooks/useContractTabs'; import useContractTabs from 'lib/hooks/useContractTabs';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import useQueryWithPages from 'lib/hooks/useQueryWithPages'; import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import getQueryParamString from 'lib/router/getQueryParamString';
import useSocketChannel from 'lib/socket/useSocketChannel'; import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage'; import useSocketMessage from 'lib/socket/useSocketMessage';
import trimTokenSymbol from 'lib/token/trimTokenSymbol'; import trimTokenSymbol from 'lib/token/trimTokenSymbol';
...@@ -45,7 +46,7 @@ const TokenPageContent = () => { ...@@ -45,7 +46,7 @@ const TokenPageContent = () => {
const scrollRef = React.useRef<HTMLDivElement>(null); const scrollRef = React.useRef<HTMLDivElement>(null);
const hashString = router.query.hash?.toString(); const hashString = getQueryParamString(router.query.hash);
const queryClient = useQueryClient(); const queryClient = useQueryClient();
...@@ -252,7 +253,7 @@ const TokenPageContent = () => { ...@@ -252,7 +253,7 @@ const TokenPageContent = () => {
</> </>
) } ) }
<TokenContractInfo tokenQuery={ tokenQuery }/> <TokenContractInfo tokenQuery={ tokenQuery }/>
<TokenVerifiedInfo verifiedInfoQuery={ verifiedInfoQuery } isVerifiedInfoEnabled={ isVerifiedInfoEnabled }/> <TokenVerifiedInfo hash={ hashString } verifiedInfoQuery={ verifiedInfoQuery } isVerifiedInfoEnabled={ isVerifiedInfoEnabled }/>
<TokenDetails tokenQuery={ tokenQuery }/> <TokenDetails tokenQuery={ tokenQuery }/>
{ /* should stay before tabs to scroll up with pagination */ } { /* should stay before tabs to scroll up with pagination */ }
<Box ref={ scrollRef }></Box> <Box ref={ scrollRef }></Box>
......
...@@ -6,10 +6,9 @@ import type { RoutedTab } from 'ui/shared/RoutedTabs/types'; ...@@ -6,10 +6,9 @@ import type { RoutedTab } from 'ui/shared/RoutedTabs/types';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import { useAppContext } from 'lib/appContext'; import { useAppContext } from 'lib/appContext';
import networkExplorers from 'lib/networks/networkExplorers';
import getQueryParamString from 'lib/router/getQueryParamString'; import getQueryParamString from 'lib/router/getQueryParamString';
import TextAd from 'ui/shared/ad/TextAd'; import TextAd from 'ui/shared/ad/TextAd';
import LinkExternal from 'ui/shared/LinkExternal'; import NetworkExplorers from 'ui/shared/NetworkExplorers';
import Page from 'ui/shared/Page/Page'; import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle'; import PageTitle from 'ui/shared/Page/PageTitle';
import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs'; import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs';
...@@ -40,27 +39,10 @@ const TransactionPageContent = () => { ...@@ -40,27 +39,10 @@ const TransactionPageContent = () => {
queryOptions: { enabled: Boolean(hash) }, queryOptions: { enabled: Boolean(hash) },
}); });
const explorersLinks = networkExplorers
.filter((explorer) => explorer.paths.tx)
.map((explorer) => {
const url = new URL(explorer.paths.tx + '/' + hash, explorer.baseUrl);
return <LinkExternal key={ explorer.baseUrl } href={ url.toString() }>Open in { explorer.title }</LinkExternal>;
});
const additionals = ( const additionals = (
<Flex justifyContent="space-between" alignItems="center" flexGrow={ 1 } flexWrap="wrap"> <Flex justifyContent="space-between" alignItems="center" flexGrow={ 1 } flexWrap="wrap">
{ data?.tx_tag && <Tag my={ 2 }>{ data.tx_tag }</Tag> } { data?.tx_tag && <Tag my={ 2 }>{ data.tx_tag }</Tag> }
{ explorersLinks.length > 0 && ( <NetworkExplorers type="tx" pathParam={ hash } ml={{ base: 'initial', lg: 'auto' }}/>
<Flex
alignItems="center"
flexWrap="wrap"
columnGap={ 6 }
rowGap={ 3 }
ml={{ base: 'initial', lg: 'auto' }}
>
{ explorersLinks }
</Flex>
) }
</Flex> </Flex>
); );
......
import { Flex, Button, Icon, chakra, Popover, PopoverTrigger, PopoverBody, PopoverContent, useDisclosure } from '@chakra-ui/react';
import React from 'react';
import type { NetworkExplorer as TNetworkExplorer } from 'types/networks';
import arrowIcon from 'icons/arrows/east-mini.svg';
import searchIcon from 'icons/search.svg';
import networkExplorers from 'lib/networks/networkExplorers';
import LinkExternal from 'ui/shared/LinkExternal';
interface Props {
className?: string;
type: keyof TNetworkExplorer['paths'];
pathParam: string;
}
const NetworkExplorers = ({ className, type, pathParam }: Props) => {
const { isOpen, onToggle, onClose } = useDisclosure();
const explorersLinks = networkExplorers
.filter((explorer) => explorer.paths[type])
.map((explorer) => {
const url = new URL(explorer.paths[type] + '/' + pathParam, explorer.baseUrl);
return <LinkExternal key={ explorer.baseUrl } href={ url.toString() }>{ explorer.title }</LinkExternal>;
});
if (explorersLinks.length === 0) {
return null;
}
return (
<Popover isOpen={ isOpen } onClose={ onClose } placement="bottom-start" isLazy>
<PopoverTrigger>
<Button
className={ className }
size="sm"
variant="outline"
colorScheme="gray"
onClick={ onToggle }
aria-label="Verify in other explorers"
fontWeight={ 500 }
px={ 2 }
h="30px"
>
<Icon as={ searchIcon } boxSize={ 4 } mr={ 1 }/>
<span>Explorers</span>
<Icon as={ arrowIcon } transform={ isOpen ? 'rotate(90deg)' : 'rotate(-90deg)' } transitionDuration="faster" boxSize={ 5 } ml={ 1 }/>
</Button>
</PopoverTrigger>
<PopoverContent w="240px">
<PopoverBody >
<chakra.span color="text_secondary" fontSize="xs">Verify with other explorers</chakra.span>
<Flex
alignItems="center"
flexWrap="wrap"
columnGap={ 6 }
rowGap={ 3 }
mt={ 3 }
>
{ explorersLinks }
</Flex>
</PopoverBody>
</PopoverContent>
</Popover>
);
};
export default React.memo(chakra(NetworkExplorers));
import { Flex, Skeleton } from '@chakra-ui/react'; import { Flex, Skeleton, useColorModeValue } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query'; import type { UseQueryResult } from '@tanstack/react-query';
import React from 'react'; import React from 'react';
import type { TokenVerifiedInfo as TTokenVerifiedInfo } from 'types/api/token'; import type { TokenVerifiedInfo as TTokenVerifiedInfo } from 'types/api/token';
import LinkExternal from 'ui/shared/LinkExternal'; import LinkExternal from 'ui/shared/LinkExternal';
import NetworkExplorers from 'ui/shared/NetworkExplorers';
interface Props { interface Props {
verifiedInfoQuery: UseQueryResult<TTokenVerifiedInfo>; verifiedInfoQuery: UseQueryResult<TTokenVerifiedInfo>;
isVerifiedInfoEnabled: boolean; isVerifiedInfoEnabled: boolean;
hash: string;
} }
const TokenVerifiedInfo = ({ verifiedInfoQuery, isVerifiedInfoEnabled }: Props) => { const TokenVerifiedInfo = ({ verifiedInfoQuery, isVerifiedInfoEnabled, hash }: Props) => {
const { data, isLoading, isError } = verifiedInfoQuery; const { data, isLoading, isError } = verifiedInfoQuery;
const websiteLinkBg = useColorModeValue('gray.100', 'gray.700');
const content = (() => { const content = (() => {
const explorers = <NetworkExplorers type="token" pathParam={ hash }/>;
if (!isVerifiedInfoEnabled) { if (!isVerifiedInfoEnabled) {
return <span>explorers</span>; return explorers;
} }
if (isLoading) { if (isLoading) {
...@@ -38,7 +43,7 @@ const TokenVerifiedInfo = ({ verifiedInfoQuery, isVerifiedInfoEnabled }: Props) ...@@ -38,7 +43,7 @@ const TokenVerifiedInfo = ({ verifiedInfoQuery, isVerifiedInfoEnabled }: Props)
try { try {
const url = new URL(data.projectWebsite); const url = new URL(data.projectWebsite);
return ( return (
<LinkExternal href={ data.projectWebsite } px="10px" py="5px" bgColor="gray.100" borderRadius="base">{ url.host }</LinkExternal> <LinkExternal href={ data.projectWebsite } px="10px" py="5px" bgColor={ websiteLinkBg } borderRadius="base">{ url.host }</LinkExternal>
); );
} catch (error) { } catch (error) {
return null; return null;
...@@ -49,12 +54,12 @@ const TokenVerifiedInfo = ({ verifiedInfoQuery, isVerifiedInfoEnabled }: Props) ...@@ -49,12 +54,12 @@ const TokenVerifiedInfo = ({ verifiedInfoQuery, isVerifiedInfoEnabled }: Props)
<> <>
{ websiteLink } { websiteLink }
<Skeleton w="130px" h="30px" borderRadius="base"/> <Skeleton w="130px" h="30px" borderRadius="base"/>
<Skeleton w="120px" h="30px" borderRadius="base"/> { explorers }
</> </>
); );
})(); })();
return <Flex columnGap={ 3 } mt={ 5 }>{ content }</Flex>; return <Flex columnGap={ 3 } rowGap={ 3 } mt={ 5 } flexWrap="wrap" _empty={{ display: 'none' }}>{ content }</Flex>;
}; };
export default React.memo(TokenVerifiedInfo); export default React.memo(TokenVerifiedInfo);
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