Commit 1484bb36 authored by tom's avatar tom

Merge branch 'main' of github.com:blockscout/frontend into tom2drum/issue-1414

parents f8d8b7f8 95243dc7
...@@ -29,5 +29,5 @@ jobs: ...@@ -29,5 +29,5 @@ jobs:
cleanup_docker_image: cleanup_docker_image:
uses: blockscout/blockscout-ci-cd/.github/workflows/cleanup_docker.yaml@master uses: blockscout/blockscout-ci-cd/.github/workflows/cleanup_docker.yaml@master
with: with:
dockerImage: prerelease-$GITHUB_REF_NAME_SLUG dockerImage: review-$GITHUB_REF_NAME_SLUG
secrets: inherit secrets: inherit
...@@ -318,6 +318,7 @@ ...@@ -318,6 +318,7 @@
"main.L2", "main.L2",
"poa_core", "poa_core",
"eth_goerli", "eth_goerli",
"sepolia",
"eth", "eth",
"rootstock", "rootstock",
"polygon", "polygon",
......
# Set of ENVs for Sepolia testnet network explorer
# https://eth-sepolia.blockscout.com/
# app configuration
NEXT_PUBLIC_APP_PROTOCOL=http
NEXT_PUBLIC_APP_HOST=localhost
NEXT_PUBLIC_APP_PORT=3000
# blockchain parameters
NEXT_PUBLIC_NETWORK_NAME=Sepolia
NEXT_PUBLIC_NETWORK_SHORT_NAME=Sepolia
NEXT_PUBLIC_NETWORK_ID=11155111
NEXT_PUBLIC_NETWORK_CURRENCY_NAME=Ether
NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation
NEXT_PUBLIC_NETWORK_RPC_URL=https://eth-sepolia.public.blastapi.io
NEXT_PUBLIC_IS_TESTNET=true
# api configuration
NEXT_PUBLIC_API_HOST=eth-sepolia.blockscout.com
NEXT_PUBLIC_API_BASE_PATH=/
# ui config
## homepage
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs']
NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND='rgba(51, 53, 67, 1)'
NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR='rgba(165, 252, 122, 1)'
## sidebar
NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/eth-sepolia.json
NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/sepolia.svg
NEXT_PUBLIC_NETWORK_LOGO_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/sepolia.svg
NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/sepolia.png
NEXT_PUBLIC_NETWORK_ICON_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/sepolia.png
NEXT_PUBLIC_OTHER_LINKS=[{'url':'https://sepolia.drpc.org?ref=559183','text':'Public RPC'}]
## footer
NEXT_PUBLIC_FOOTER_LINKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/footer-links/sepolia.json
##views
NEXT_PUBLIC_VIEWS_NFT_MARKETPLACES=[{'name':'LooksRare','collection_url':'https://sepolia.looksrare.org/collections/{hash}','instance_url':'https://sepolia.looksrare.org/collections/{hash}/{id}','logo_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/nft-marketplace-logos/looks-rare.png'}]
## misc
NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Etherscan','baseUrl':'https://sepolia.etherscan.io/','paths':{'tx':'/tx','address':'/address','token':'/token','block':'/block'}},{'title':'Tenderly','baseUrl':'https://dashboard.tenderly.co','paths':{'tx':'/tx/sepolia'}}]
# app features
NEXT_PUBLIC_APP_ENV=development
NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0xbf69c7abc4fee283b59a9633dadfdaedde5c5ee0fba3e80a08b5b8a3acbd4363
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
NEXT_PUBLIC_AUTH_URL=http://localhost:3000
NEXT_PUBLIC_LOGOUT_URL=https://blockscout-goerli.us.auth0.com/v2/logout
NEXT_PUBLIC_MARKETPLACE_CONFIG_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/marketplace/eth-goerli.json
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/shrqUAcjgGJ4jU88C
NEXT_PUBLIC_STATS_API_HOST=https://stats-sepolia.k8s-dev.blockscout.com
NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info.services.blockscout.com
NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com
NEXT_PUBLIC_WEB3_WALLETS=['token_pocket','metamask']
NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true
NEXT_PUBLIC_HAS_BEACON_CHAIN=true
#meta
NEXT_PUBLIC_OG_IMAGE_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/og-images/sepolia-testnet.png
...@@ -4,15 +4,26 @@ import React from 'react'; ...@@ -4,15 +4,26 @@ import React from 'react';
import PageNextJs from 'nextjs/PageNextJs'; import PageNextJs from 'nextjs/PageNextJs';
import config from 'configs/app';
import LinkExternal from 'ui/shared/LinkExternal';
import PageTitle from 'ui/shared/Page/PageTitle'; import PageTitle from 'ui/shared/Page/PageTitle';
const feature = config.features.marketplace;
const Marketplace = dynamic(() => import('ui/pages/Marketplace'), { ssr: false }); const Marketplace = dynamic(() => import('ui/pages/Marketplace'), { ssr: false });
const Page: NextPage = () => { const Page: NextPage = () => {
return ( return (
<PageNextJs pathname="/apps"> <PageNextJs pathname="/apps">
<> <>
<PageTitle title="DAppscout"/> <PageTitle
title="DAppscout"
contentAfter={ feature.isEnabled && (
<LinkExternal href={ feature.submitFormUrl } variant="subtle" fontSize="sm" lineHeight={ 5 } ml="auto">
Submit app
</LinkExternal>
) }
/>
<Marketplace/> <Marketplace/>
</> </>
</PageNextJs> </PageNextJs>
......
...@@ -78,6 +78,31 @@ test('verified with changed byte code socket', async({ mount, page, createSocket ...@@ -78,6 +78,31 @@ test('verified with changed byte code socket', async({ mount, page, createSocket
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
test('verified via lookup in eth_bytecode_db', async({ mount, page, createSocket }) => {
await page.route(CONTRACT_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(contractMock.nonVerified),
}));
await page.route('https://cdn.jsdelivr.net/npm/monaco-editor@0.33.0/**', (route) => route.abort());
await mount(
<TestApp withSocket>
<ContractCode addressHash={ addressHash }/>
</TestApp>,
{ hooksConfig },
);
const socket = await createSocket();
const channel = await socketServer.joinChannel(socket, 'addresses:' + addressHash.toLowerCase());
await page.waitForResponse(CONTRACT_API_URL);
socketServer.sendMessage(socket, channel, 'smart_contract_was_verified', {});
const request = await page.waitForRequest(CONTRACT_API_URL);
expect(request).toBeTruthy();
});
test('verified with multiple sources', async({ mount, page }) => { test('verified with multiple sources', async({ mount, page }) => {
await page.route(CONTRACT_API_URL, (route) => route.fulfill({ await page.route(CONTRACT_API_URL, (route) => route.fulfill({
status: 200, status: 200,
......
...@@ -38,7 +38,6 @@ const ContractCode = ({ addressHash, noSocket }: Props) => { ...@@ -38,7 +38,6 @@ const ContractCode = ({ addressHash, noSocket }: Props) => {
const [ isChangedBytecodeSocket, setIsChangedBytecodeSocket ] = React.useState<boolean>(); const [ isChangedBytecodeSocket, setIsChangedBytecodeSocket ] = React.useState<boolean>();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const refetchQueries = queryClient.refetchQueries;
const addressInfo = queryClient.getQueryData<AddressInfo>(getResourceKey('address', { pathParams: { hash: addressHash } })); const addressInfo = queryClient.getQueryData<AddressInfo>(getResourceKey('address', { pathParams: { hash: addressHash } }));
const { data, isPlaceholderData, isError } = useApiQuery('contract', { const { data, isPlaceholderData, isError } = useApiQuery('contract', {
...@@ -55,13 +54,13 @@ const ContractCode = ({ addressHash, noSocket }: Props) => { ...@@ -55,13 +54,13 @@ const ContractCode = ({ addressHash, noSocket }: Props) => {
}, [ ]); }, [ ]);
const handleContractWasVerifiedMessage: SocketMessage.SmartContractWasVerified['handler'] = React.useCallback(() => { const handleContractWasVerifiedMessage: SocketMessage.SmartContractWasVerified['handler'] = React.useCallback(() => {
refetchQueries({ queryClient.refetchQueries({
queryKey: getResourceKey('address', { pathParams: { hash: addressHash } }), queryKey: getResourceKey('address', { pathParams: { hash: addressHash } }),
}); });
refetchQueries({ queryClient.refetchQueries({
queryKey: getResourceKey('contract', { pathParams: { hash: addressHash } }), queryKey: getResourceKey('contract', { pathParams: { hash: addressHash } }),
}); });
}, [ addressHash, refetchQueries ]); }, [ addressHash, queryClient ]);
const enableQuery = React.useCallback(() => setIsQueryEnabled(true), []); const enableQuery = React.useCallback(() => setIsQueryEnabled(true), []);
......
...@@ -22,7 +22,13 @@ const ERC20TokensTableItem = ({ ...@@ -22,7 +22,13 @@ const ERC20TokensTableItem = ({
} = getCurrencyValue({ value: value, exchangeRate: token.exchange_rate, decimals: token.decimals, accuracy: 8, accuracyUsd: 2 }); } = getCurrencyValue({ value: value, exchangeRate: token.exchange_rate, decimals: token.decimals, accuracy: 8, accuracyUsd: 2 });
return ( return (
<Tr> <Tr
sx={{
'&:hover [aria-label="Add token to wallet"]': {
opacity: 1,
},
}}
>
<Td verticalAlign="middle"> <Td verticalAlign="middle">
<TokenEntity <TokenEntity
token={ token } token={ token }
...@@ -39,7 +45,7 @@ const ERC20TokensTableItem = ({ ...@@ -39,7 +45,7 @@ const ERC20TokensTableItem = ({
isLoading={ isLoading } isLoading={ isLoading }
noIcon noIcon
/> />
<AddressAddToWallet token={ token } ml={ 4 } isLoading={ isLoading }/> <AddressAddToWallet token={ token } ml={ 4 } isLoading={ isLoading } opacity="0"/>
</Flex> </Flex>
</Td> </Td>
<Td isNumeric verticalAlign="middle"> <Td isNumeric verticalAlign="middle">
......
import { Box, Link, Skeleton } from '@chakra-ui/react'; import { Box } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import config from 'configs/app'; import config from 'configs/app';
...@@ -7,7 +7,6 @@ import MarketplaceCategoriesMenu from 'ui/marketplace/MarketplaceCategoriesMenu' ...@@ -7,7 +7,6 @@ import MarketplaceCategoriesMenu from 'ui/marketplace/MarketplaceCategoriesMenu'
import MarketplaceDisclaimerModal from 'ui/marketplace/MarketplaceDisclaimerModal'; import MarketplaceDisclaimerModal from 'ui/marketplace/MarketplaceDisclaimerModal';
import MarketplaceList from 'ui/marketplace/MarketplaceList'; import MarketplaceList from 'ui/marketplace/MarketplaceList';
import FilterInput from 'ui/shared/filters/FilterInput'; import FilterInput from 'ui/shared/filters/FilterInput';
import IconSvg from 'ui/shared/IconSvg';
import useMarketplace from '../marketplace/useMarketplace'; import useMarketplace from '../marketplace/useMarketplace';
const feature = config.features.marketplace; const feature = config.features.marketplace;
...@@ -91,29 +90,6 @@ const Marketplace = () => { ...@@ -91,29 +90,6 @@ const Marketplace = () => {
appId={ selectedApp.id } appId={ selectedApp.id }
/> />
) } ) }
<Skeleton
isLoaded={ !isPlaceholderData }
marginTop={{ base: 8, sm: 16 }}
display="inline-block"
>
<Link
fontWeight="bold"
display="inline-flex"
alignItems="baseline"
href={ feature.submitFormUrl }
isExternal
>
<IconSvg
name="plus"
w={ 3 }
h={ 3 }
mr={ 2 }
/>
Submit an app
</Link>
</Skeleton>
</> </>
); );
}; };
......
...@@ -107,7 +107,7 @@ const AddressAddToWallet = ({ className, token, isLoading, variant = 'icon', ico ...@@ -107,7 +107,7 @@ const AddressAddToWallet = ({ className, token, isLoading, variant = 'icon', ico
return ( return (
<Tooltip label={ `Add token to ${ WALLETS_INFO[wallet].name }` }> <Tooltip label={ `Add token to ${ WALLETS_INFO[wallet].name }` }>
<Box className={ className } display="inline-flex" cursor="pointer" onClick={ handleClick } flexShrink={ 0 }> <Box className={ className } display="inline-flex" cursor="pointer" onClick={ handleClick } flexShrink={ 0 } aria-label="Add token to wallet">
<IconSvg name={ WALLETS_INFO[wallet].icon } boxSize={ iconSize }/> <IconSvg name={ WALLETS_INFO[wallet].icon } boxSize={ iconSize }/>
</Box> </Box>
</Tooltip> </Tooltip>
......
...@@ -12,10 +12,11 @@ import * as Layout from './components'; ...@@ -12,10 +12,11 @@ import * as Layout from './components';
const LayoutDefault = ({ children }: Props) => { const LayoutDefault = ({ children }: Props) => {
return ( return (
<Layout.Container> <Layout.Container>
<Layout.TopRow/>
<HeaderMobile/> <HeaderMobile/>
<Layout.MainArea> <Layout.MainArea>
<Layout.MainColumn <Layout.MainColumn
paddingTop={{ base: '138px', lg: 6 }} paddingTop={{ base: 16, lg: 6 }}
paddingX={{ base: 4, lg: 6 }} paddingX={{ base: 4, lg: 6 }}
> >
<HeaderAlert/> <HeaderAlert/>
......
...@@ -76,7 +76,7 @@ const ProfileMenuDesktop = ({ isHomePage }: Props) => { ...@@ -76,7 +76,7 @@ const ProfileMenuDesktop = ({ isHomePage }: Props) => {
textAlign="center" textAlign="center"
padding={ 2 } padding={ 2 }
isDisabled={ hasMenu } isDisabled={ hasMenu }
openDelay={ 300 } openDelay={ 500 }
> >
<Box> <Box>
<PopoverTrigger> <PopoverTrigger>
......
import { Box, Popover, PopoverTrigger, PopoverContent, PopoverBody, useDisclosure, PopoverFooter } from '@chakra-ui/react'; import { Box, Portal, Popover, PopoverTrigger, PopoverContent, PopoverBody, useDisclosure, PopoverFooter, useOutsideClick } from '@chakra-ui/react';
import _debounce from 'lodash/debounce'; import _debounce from 'lodash/debounce';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import type { FormEvent, FocusEvent } from 'react'; import type { FormEvent } from 'react';
import React from 'react'; import React from 'react';
import { Element } from 'react-scroll'; import { Element } from 'react-scroll';
...@@ -59,13 +59,15 @@ const SearchBar = ({ isHomepage }: Props) => { ...@@ -59,13 +59,15 @@ const SearchBar = ({ isHomepage }: Props) => {
inputRef.current?.querySelector('input')?.blur(); inputRef.current?.querySelector('input')?.blur();
}, [ onClose ]); }, [ onClose ]);
const handleBlur = React.useCallback((event: FocusEvent<HTMLFormElement>) => { const handleOutsideClick = React.useCallback((event: Event) => {
const isFocusInMenu = menuRef.current?.contains(event.relatedTarget); const isFocusInInput = inputRef.current?.contains(event.target as Node);
const isFocusInInput = inputRef.current?.contains(event.relatedTarget);
if (!isFocusInMenu && !isFocusInInput) { if (!isFocusInInput) {
onClose(); handelHide();
} }
}, [ onClose ]); }, [ handelHide ]);
useOutsideClick({ ref: menuRef, handler: handleOutsideClick });
const handleClear = React.useCallback(() => { const handleClear = React.useCallback(() => {
handleSearchTermChange(''); handleSearchTermChange('');
...@@ -118,53 +120,54 @@ const SearchBar = ({ isHomepage }: Props) => { ...@@ -118,53 +120,54 @@ const SearchBar = ({ isHomepage }: Props) => {
onChange={ handleSearchTermChange } onChange={ handleSearchTermChange }
onSubmit={ handleSubmit } onSubmit={ handleSubmit }
onFocus={ handleFocus } onFocus={ handleFocus }
onBlur={ handleBlur }
onHide={ handelHide } onHide={ handelHide }
onClear={ handleClear } onClear={ handleClear }
isHomepage={ isHomepage } isHomepage={ isHomepage }
value={ searchTerm } value={ searchTerm }
/> />
</PopoverTrigger> </PopoverTrigger>
<PopoverContent <Portal>
w={ `${ menuWidth.current }px` } <PopoverContent
ref={ menuRef } w={ `${ menuWidth.current }px` }
> ref={ menuRef }
<PopoverBody
p={ 0 }
color="chakra-body-text"
> >
<Box <PopoverBody
maxH="50vh" p={ 0 }
overflowY="auto" color="chakra-body-text"
id={ SCROLL_CONTAINER_ID }
ref={ scrollRef }
as={ Element }
px={ 4 }
> >
{ searchTerm.trim().length === 0 && recentSearchKeywords.length > 0 && ( <Box
<SearchBarRecentKeywords onClick={ handleSearchTermChange } onClear={ onClose }/> maxH="50vh"
) } overflowY="auto"
{ searchTerm.trim().length > 0 && ( id={ SCROLL_CONTAINER_ID }
<SearchBarSuggest ref={ scrollRef }
query={ query } as={ Element }
searchTerm={ debouncedSearchTerm } px={ 4 }
onItemClick={ handleItemClick }
containerId={ SCROLL_CONTAINER_ID }
/>
) }
</Box>
</PopoverBody>
{ searchTerm.trim().length > 0 && query.data && query.data.length >= 50 && (
<PopoverFooter>
<LinkInternal
href={ route({ pathname: '/search-results', query: { q: searchTerm } }) }
fontSize="sm"
> >
{ searchTerm.trim().length === 0 && recentSearchKeywords.length > 0 && (
<SearchBarRecentKeywords onClick={ handleSearchTermChange } onClear={ onClose }/>
) }
{ searchTerm.trim().length > 0 && (
<SearchBarSuggest
query={ query }
searchTerm={ debouncedSearchTerm }
onItemClick={ handleItemClick }
containerId={ SCROLL_CONTAINER_ID }
/>
) }
</Box>
</PopoverBody>
{ searchTerm.trim().length > 0 && query.data && query.data.length >= 50 && (
<PopoverFooter>
<LinkInternal
href={ route({ pathname: '/search-results', query: { q: searchTerm } }) }
fontSize="sm"
>
View all results View all results
</LinkInternal> </LinkInternal>
</PopoverFooter> </PopoverFooter>
) } ) }
</PopoverContent> </PopoverContent>
</Portal>
</Popover> </Popover>
); );
}; };
......
...@@ -20,25 +20,33 @@ interface Props { ...@@ -20,25 +20,33 @@ interface Props {
} }
const SearchBarInput = ({ onChange, onSubmit, isHomepage, onFocus, onBlur, onHide, onClear, value }: Props, ref: React.ForwardedRef<HTMLFormElement>) => { const SearchBarInput = ({ onChange, onSubmit, isHomepage, onFocus, onBlur, onHide, onClear, value }: Props, ref: React.ForwardedRef<HTMLFormElement>) => {
const innerRef = React.useRef<HTMLFormElement>(null);
React.useImperativeHandle(ref, () => innerRef.current as HTMLFormElement, []);
const [ isSticky, setIsSticky ] = React.useState(false); const [ isSticky, setIsSticky ] = React.useState(false);
const scrollDirection = useScrollDirection(); const scrollDirection = useScrollDirection();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const handleScroll = React.useCallback(() => { const handleScroll = React.useCallback(() => {
const TOP_BAR_HEIGHT = 36; const TOP_BAR_HEIGHT = 36;
if (window.pageYOffset >= TOP_BAR_HEIGHT) { if (!isHomepage) {
setIsSticky(true); if (window.scrollY >= TOP_BAR_HEIGHT) {
} else { setIsSticky(true);
setIsSticky(false); } else {
setIsSticky(false);
}
} }
}, [ ]); const clientRect = isMobile && innerRef?.current?.getBoundingClientRect();
if (clientRect && clientRect.y < TOP_BAR_HEIGHT) {
onHide?.();
}
}, [ isMobile, onHide, isHomepage ]);
const handleChange = React.useCallback((event: ChangeEvent<HTMLInputElement>) => { const handleChange = React.useCallback((event: ChangeEvent<HTMLInputElement>) => {
onChange(event.target.value); onChange(event.target.value);
}, [ onChange ]); }, [ onChange ]);
React.useEffect(() => { React.useEffect(() => {
if (!isMobile || isHomepage) { if (!isMobile) {
return; return;
} }
const throttledHandleScroll = throttle(handleScroll, 300); const throttledHandleScroll = throttle(handleScroll, 300);
...@@ -48,22 +56,14 @@ const SearchBarInput = ({ onChange, onSubmit, isHomepage, onFocus, onBlur, onHid ...@@ -48,22 +56,14 @@ const SearchBarInput = ({ onChange, onSubmit, isHomepage, onFocus, onBlur, onHid
return () => { return () => {
window.removeEventListener('scroll', throttledHandleScroll); window.removeEventListener('scroll', throttledHandleScroll);
}; };
// replicate componentDidMount }, [ isMobile, handleScroll ]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ isMobile ]);
const bgColor = useColorModeValue('white', 'black'); const bgColor = useColorModeValue('white', 'black');
const transformMobile = scrollDirection !== 'down' ? 'translateY(0)' : 'translateY(-100%)'; const transformMobile = scrollDirection !== 'down' ? 'translateY(0)' : 'translateY(-100%)';
React.useEffect(() => {
if (isMobile && scrollDirection === 'down') {
onHide?.();
}
}, [ scrollDirection, onHide, isMobile ]);
return ( return (
<chakra.form <chakra.form
ref={ ref } ref={ innerRef }
noValidate noValidate
onSubmit={ onSubmit } onSubmit={ onSubmit }
onBlur={ onBlur } onBlur={ onBlur }
......
...@@ -2,6 +2,7 @@ import type { ButtonProps } from '@chakra-ui/react'; ...@@ -2,6 +2,7 @@ import type { ButtonProps } from '@chakra-ui/react';
import { Popover, PopoverContent, PopoverBody, PopoverTrigger, Button, Box, useBoolean } from '@chakra-ui/react'; import { Popover, PopoverContent, PopoverBody, PopoverTrigger, Button, Box, useBoolean } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import useIsMobile from 'lib/hooks/useIsMobile';
import AddressIdenticon from 'ui/shared/entities/address/AddressIdenticon'; import AddressIdenticon from 'ui/shared/entities/address/AddressIdenticon';
import HashStringShorten from 'ui/shared/HashStringShorten'; import HashStringShorten from 'ui/shared/HashStringShorten';
import useWallet from 'ui/snippets/walletMenu/useWallet'; import useWallet from 'ui/snippets/walletMenu/useWallet';
...@@ -18,6 +19,7 @@ const WalletMenuDesktop = ({ isHomePage }: Props) => { ...@@ -18,6 +19,7 @@ const WalletMenuDesktop = ({ isHomePage }: Props) => {
const { isWalletConnected, address, connect, disconnect, isModalOpening, isModalOpen } = useWallet(); const { isWalletConnected, address, connect, disconnect, isModalOpening, isModalOpen } = useWallet();
const { themedBackground, themedBorderColor, themedColor } = useMenuButtonColors(); const { themedBackground, themedBorderColor, themedColor } = useMenuButtonColors();
const [ isPopoverOpen, setIsPopoverOpen ] = useBoolean(false); const [ isPopoverOpen, setIsPopoverOpen ] = useBoolean(false);
const isMobile = useIsMobile();
const variant = React.useMemo(() => { const variant = React.useMemo(() => {
if (isWalletConnected) { if (isWalletConnected) {
...@@ -55,7 +57,7 @@ const WalletMenuDesktop = ({ isHomePage }: Props) => { ...@@ -55,7 +57,7 @@ const WalletMenuDesktop = ({ isHomePage }: Props) => {
isOpen={ isPopoverOpen } isOpen={ isPopoverOpen }
onClose={ setIsPopoverOpen.off } onClose={ setIsPopoverOpen.off }
> >
<WalletTooltip isDisabled={ isWalletConnected }> <WalletTooltip isDisabled={ isWalletConnected || isMobile === undefined || isMobile }>
<Box ml={ 2 }> <Box ml={ 2 }>
<PopoverTrigger> <PopoverTrigger>
<Button <Button
......
import { Drawer, DrawerOverlay, DrawerContent, DrawerBody, useDisclosure, IconButton } from '@chakra-ui/react'; import { Drawer, DrawerOverlay, DrawerContent, DrawerBody, useDisclosure, IconButton } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import useIsMobile from 'lib/hooks/useIsMobile';
import AddressIdenticon from 'ui/shared/entities/address/AddressIdenticon'; import AddressIdenticon from 'ui/shared/entities/address/AddressIdenticon';
import IconSvg from 'ui/shared/IconSvg'; import IconSvg from 'ui/shared/IconSvg';
import useWallet from 'ui/snippets/walletMenu/useWallet'; import useWallet from 'ui/snippets/walletMenu/useWallet';
...@@ -13,10 +14,11 @@ const WalletMenuMobile = () => { ...@@ -13,10 +14,11 @@ const WalletMenuMobile = () => {
const { isOpen, onOpen, onClose } = useDisclosure(); const { isOpen, onOpen, onClose } = useDisclosure();
const { isWalletConnected, address, connect, disconnect, isModalOpening, isModalOpen } = useWallet(); const { isWalletConnected, address, connect, disconnect, isModalOpening, isModalOpen } = useWallet();
const { themedBackground, themedBorderColor, themedColor } = useMenuButtonColors(); const { themedBackground, themedBorderColor, themedColor } = useMenuButtonColors();
const isMobile = useIsMobile();
return ( return (
<> <>
<WalletTooltip isDisabled={ isWalletConnected } isMobile> <WalletTooltip isDisabled={ isWalletConnected || isMobile === undefined || !isMobile } isMobile>
<IconButton <IconButton
aria-label="wallet menu" aria-label="wallet menu"
icon={ isWalletConnected ? icon={ isWalletConnected ?
......
import { Tooltip, useBoolean } from '@chakra-ui/react'; import { Tooltip, useBoolean, useOutsideClick } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
type Props = { type Props = {
...@@ -8,27 +9,41 @@ type Props = { ...@@ -8,27 +9,41 @@ type Props = {
}; };
const WalletTooltip = ({ children, isDisabled, isMobile }: Props) => { const WalletTooltip = ({ children, isDisabled, isMobile }: Props) => {
const router = useRouter();
const [ isTooltipShown, setIsTooltipShown ] = useBoolean(false); const [ isTooltipShown, setIsTooltipShown ] = useBoolean(false);
const ref = React.useRef(null);
useOutsideClick({ ref, handler: setIsTooltipShown.off });
const { defaultLabel, label, localStorageKey } = React.useMemo(() => {
const isAppPage = router.pathname === '/apps/[id]';
const defaultLabel = <span>Your wallet is used to interact with<br/>apps and contracts in the explorer</span>;
const label = isAppPage ?
<span>Connect once to use your wallet with<br/>all apps in the DAppscout marketplace!</span> :
defaultLabel;
const localStorageKey = `${ isAppPage ? 'dapp-' : '' }wallet-connect-tooltip-shown`;
return { defaultLabel, label, localStorageKey };
}, [ router.pathname ]);
React.useEffect(() => { React.useEffect(() => {
const key = `wallet-connect-tooltip-shown-${ isMobile ? 'mobile' : 'desktop' }`; const wasShown = window.localStorage.getItem(localStorageKey);
const wasShown = window.localStorage.getItem(key); if (!isDisabled && !wasShown) {
if (!wasShown) {
setIsTooltipShown.on(); setIsTooltipShown.on();
window.localStorage.setItem(key, 'true'); window.localStorage.setItem(localStorageKey, 'true');
setTimeout(() => setIsTooltipShown.off(), 3000);
} }
}, [ setIsTooltipShown, isMobile ]); }, [ setIsTooltipShown, localStorageKey, isDisabled ]);
return ( return (
<Tooltip <Tooltip
label={ <span>Your wallet is used to interact with<br/>apps and contracts in the explorer</span> } label={ isTooltipShown ? label : defaultLabel }
textAlign="center" textAlign="center"
padding={ 2 } padding={ 2 }
isDisabled={ isDisabled } isDisabled={ isDisabled }
openDelay={ 300 } openDelay={ 500 }
isOpen={ isTooltipShown || (isMobile ? false : undefined) } isOpen={ isTooltipShown || (isMobile ? false : undefined) }
onClose={ setIsTooltipShown.off } onClose={ setIsTooltipShown.off }
display={ isMobile ? { base: 'flex', lg: 'none' } : { base: 'none', lg: 'flex' } } display={ isMobile ? { base: 'flex', lg: 'none' } : { base: 'none', lg: 'flex' } }
ref={ ref }
> >
{ children } { children }
</Tooltip> </Tooltip>
......
...@@ -51,7 +51,13 @@ const TokensTableItem = ({ ...@@ -51,7 +51,13 @@ const TokensTableItem = ({
}; };
return ( return (
<Tr> <Tr
sx={{
'&:hover [aria-label="Add token to wallet"]': {
opacity: 1,
},
}}
>
<Td> <Td>
<Flex alignItems="flex-start"> <Flex alignItems="flex-start">
<Skeleton <Skeleton
...@@ -81,7 +87,12 @@ const TokensTableItem = ({ ...@@ -81,7 +87,12 @@ const TokensTableItem = ({
fontSize="sm" fontSize="sm"
fontWeight={ 500 } fontWeight={ 500 }
/> />
<AddressAddToWallet token={ token } isLoading={ isLoading } iconSize={ 5 }/> <AddressAddToWallet
token={ token }
isLoading={ isLoading }
iconSize={ 5 }
opacity={ 0 }
/>
</Flex> </Flex>
<Flex columnGap={ 1 }> <Flex columnGap={ 1 }>
<Tag isLoading={ isLoading }>{ type }</Tag> <Tag isLoading={ isLoading }>{ type }</Tag>
......
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