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:
cleanup_docker_image:
uses: blockscout/blockscout-ci-cd/.github/workflows/cleanup_docker.yaml@master
with:
dockerImage: prerelease-$GITHUB_REF_NAME_SLUG
dockerImage: review-$GITHUB_REF_NAME_SLUG
secrets: inherit
......@@ -318,6 +318,7 @@
"main.L2",
"poa_core",
"eth_goerli",
"sepolia",
"eth",
"rootstock",
"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';
import PageNextJs from 'nextjs/PageNextJs';
import config from 'configs/app';
import LinkExternal from 'ui/shared/LinkExternal';
import PageTitle from 'ui/shared/Page/PageTitle';
const feature = config.features.marketplace;
const Marketplace = dynamic(() => import('ui/pages/Marketplace'), { ssr: false });
const Page: NextPage = () => {
return (
<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/>
</>
</PageNextJs>
......
......@@ -78,6 +78,31 @@ test('verified with changed byte code socket', async({ mount, page, createSocket
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 }) => {
await page.route(CONTRACT_API_URL, (route) => route.fulfill({
status: 200,
......
......@@ -38,7 +38,6 @@ const ContractCode = ({ addressHash, noSocket }: Props) => {
const [ isChangedBytecodeSocket, setIsChangedBytecodeSocket ] = React.useState<boolean>();
const queryClient = useQueryClient();
const refetchQueries = queryClient.refetchQueries;
const addressInfo = queryClient.getQueryData<AddressInfo>(getResourceKey('address', { pathParams: { hash: addressHash } }));
const { data, isPlaceholderData, isError } = useApiQuery('contract', {
......@@ -55,13 +54,13 @@ const ContractCode = ({ addressHash, noSocket }: Props) => {
}, [ ]);
const handleContractWasVerifiedMessage: SocketMessage.SmartContractWasVerified['handler'] = React.useCallback(() => {
refetchQueries({
queryClient.refetchQueries({
queryKey: getResourceKey('address', { pathParams: { hash: addressHash } }),
});
refetchQueries({
queryClient.refetchQueries({
queryKey: getResourceKey('contract', { pathParams: { hash: addressHash } }),
});
}, [ addressHash, refetchQueries ]);
}, [ addressHash, queryClient ]);
const enableQuery = React.useCallback(() => setIsQueryEnabled(true), []);
......
......@@ -22,7 +22,13 @@ const ERC20TokensTableItem = ({
} = getCurrencyValue({ value: value, exchangeRate: token.exchange_rate, decimals: token.decimals, accuracy: 8, accuracyUsd: 2 });
return (
<Tr>
<Tr
sx={{
'&:hover [aria-label="Add token to wallet"]': {
opacity: 1,
},
}}
>
<Td verticalAlign="middle">
<TokenEntity
token={ token }
......@@ -39,7 +45,7 @@ const ERC20TokensTableItem = ({
isLoading={ isLoading }
noIcon
/>
<AddressAddToWallet token={ token } ml={ 4 } isLoading={ isLoading }/>
<AddressAddToWallet token={ token } ml={ 4 } isLoading={ isLoading } opacity="0"/>
</Flex>
</Td>
<Td isNumeric verticalAlign="middle">
......
import { Box, Link, Skeleton } from '@chakra-ui/react';
import { Box } from '@chakra-ui/react';
import React from 'react';
import config from 'configs/app';
......@@ -7,7 +7,6 @@ import MarketplaceCategoriesMenu from 'ui/marketplace/MarketplaceCategoriesMenu'
import MarketplaceDisclaimerModal from 'ui/marketplace/MarketplaceDisclaimerModal';
import MarketplaceList from 'ui/marketplace/MarketplaceList';
import FilterInput from 'ui/shared/filters/FilterInput';
import IconSvg from 'ui/shared/IconSvg';
import useMarketplace from '../marketplace/useMarketplace';
const feature = config.features.marketplace;
......@@ -91,29 +90,6 @@ const Marketplace = () => {
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
return (
<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 }/>
</Box>
</Tooltip>
......
......@@ -12,10 +12,11 @@ import * as Layout from './components';
const LayoutDefault = ({ children }: Props) => {
return (
<Layout.Container>
<Layout.TopRow/>
<HeaderMobile/>
<Layout.MainArea>
<Layout.MainColumn
paddingTop={{ base: '138px', lg: 6 }}
paddingTop={{ base: 16, lg: 6 }}
paddingX={{ base: 4, lg: 6 }}
>
<HeaderAlert/>
......
......@@ -76,7 +76,7 @@ const ProfileMenuDesktop = ({ isHomePage }: Props) => {
textAlign="center"
padding={ 2 }
isDisabled={ hasMenu }
openDelay={ 300 }
openDelay={ 500 }
>
<Box>
<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 { useRouter } from 'next/router';
import type { FormEvent, FocusEvent } from 'react';
import type { FormEvent } from 'react';
import React from 'react';
import { Element } from 'react-scroll';
......@@ -59,13 +59,15 @@ const SearchBar = ({ isHomepage }: Props) => {
inputRef.current?.querySelector('input')?.blur();
}, [ onClose ]);
const handleBlur = React.useCallback((event: FocusEvent<HTMLFormElement>) => {
const isFocusInMenu = menuRef.current?.contains(event.relatedTarget);
const isFocusInInput = inputRef.current?.contains(event.relatedTarget);
if (!isFocusInMenu && !isFocusInInput) {
onClose();
const handleOutsideClick = React.useCallback((event: Event) => {
const isFocusInInput = inputRef.current?.contains(event.target as Node);
if (!isFocusInInput) {
handelHide();
}
}, [ onClose ]);
}, [ handelHide ]);
useOutsideClick({ ref: menuRef, handler: handleOutsideClick });
const handleClear = React.useCallback(() => {
handleSearchTermChange('');
......@@ -118,53 +120,54 @@ const SearchBar = ({ isHomepage }: Props) => {
onChange={ handleSearchTermChange }
onSubmit={ handleSubmit }
onFocus={ handleFocus }
onBlur={ handleBlur }
onHide={ handelHide }
onClear={ handleClear }
isHomepage={ isHomepage }
value={ searchTerm }
/>
</PopoverTrigger>
<PopoverContent
w={ `${ menuWidth.current }px` }
ref={ menuRef }
>
<PopoverBody
p={ 0 }
color="chakra-body-text"
<Portal>
<PopoverContent
w={ `${ menuWidth.current }px` }
ref={ menuRef }
>
<Box
maxH="50vh"
overflowY="auto"
id={ SCROLL_CONTAINER_ID }
ref={ scrollRef }
as={ Element }
px={ 4 }
<PopoverBody
p={ 0 }
color="chakra-body-text"
>
{ 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"
<Box
maxH="50vh"
overflowY="auto"
id={ SCROLL_CONTAINER_ID }
ref={ scrollRef }
as={ Element }
px={ 4 }
>
{ 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
</LinkInternal>
</PopoverFooter>
) }
</PopoverContent>
</LinkInternal>
</PopoverFooter>
) }
</PopoverContent>
</Portal>
</Popover>
);
};
......
......@@ -20,25 +20,33 @@ interface Props {
}
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 scrollDirection = useScrollDirection();
const isMobile = useIsMobile();
const handleScroll = React.useCallback(() => {
const TOP_BAR_HEIGHT = 36;
if (window.pageYOffset >= TOP_BAR_HEIGHT) {
setIsSticky(true);
} else {
setIsSticky(false);
if (!isHomepage) {
if (window.scrollY >= TOP_BAR_HEIGHT) {
setIsSticky(true);
} 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>) => {
onChange(event.target.value);
}, [ onChange ]);
React.useEffect(() => {
if (!isMobile || isHomepage) {
if (!isMobile) {
return;
}
const throttledHandleScroll = throttle(handleScroll, 300);
......@@ -48,22 +56,14 @@ const SearchBarInput = ({ onChange, onSubmit, isHomepage, onFocus, onBlur, onHid
return () => {
window.removeEventListener('scroll', throttledHandleScroll);
};
// replicate componentDidMount
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ isMobile ]);
}, [ isMobile, handleScroll ]);
const bgColor = useColorModeValue('white', 'black');
const transformMobile = scrollDirection !== 'down' ? 'translateY(0)' : 'translateY(-100%)';
React.useEffect(() => {
if (isMobile && scrollDirection === 'down') {
onHide?.();
}
}, [ scrollDirection, onHide, isMobile ]);
return (
<chakra.form
ref={ ref }
ref={ innerRef }
noValidate
onSubmit={ onSubmit }
onBlur={ onBlur }
......
......@@ -2,6 +2,7 @@ import type { ButtonProps } from '@chakra-ui/react';
import { Popover, PopoverContent, PopoverBody, PopoverTrigger, Button, Box, useBoolean } from '@chakra-ui/react';
import React from 'react';
import useIsMobile from 'lib/hooks/useIsMobile';
import AddressIdenticon from 'ui/shared/entities/address/AddressIdenticon';
import HashStringShorten from 'ui/shared/HashStringShorten';
import useWallet from 'ui/snippets/walletMenu/useWallet';
......@@ -18,6 +19,7 @@ const WalletMenuDesktop = ({ isHomePage }: Props) => {
const { isWalletConnected, address, connect, disconnect, isModalOpening, isModalOpen } = useWallet();
const { themedBackground, themedBorderColor, themedColor } = useMenuButtonColors();
const [ isPopoverOpen, setIsPopoverOpen ] = useBoolean(false);
const isMobile = useIsMobile();
const variant = React.useMemo(() => {
if (isWalletConnected) {
......@@ -55,7 +57,7 @@ const WalletMenuDesktop = ({ isHomePage }: Props) => {
isOpen={ isPopoverOpen }
onClose={ setIsPopoverOpen.off }
>
<WalletTooltip isDisabled={ isWalletConnected }>
<WalletTooltip isDisabled={ isWalletConnected || isMobile === undefined || isMobile }>
<Box ml={ 2 }>
<PopoverTrigger>
<Button
......
import { Drawer, DrawerOverlay, DrawerContent, DrawerBody, useDisclosure, IconButton } from '@chakra-ui/react';
import React from 'react';
import useIsMobile from 'lib/hooks/useIsMobile';
import AddressIdenticon from 'ui/shared/entities/address/AddressIdenticon';
import IconSvg from 'ui/shared/IconSvg';
import useWallet from 'ui/snippets/walletMenu/useWallet';
......@@ -13,10 +14,11 @@ const WalletMenuMobile = () => {
const { isOpen, onOpen, onClose } = useDisclosure();
const { isWalletConnected, address, connect, disconnect, isModalOpening, isModalOpen } = useWallet();
const { themedBackground, themedBorderColor, themedColor } = useMenuButtonColors();
const isMobile = useIsMobile();
return (
<>
<WalletTooltip isDisabled={ isWalletConnected } isMobile>
<WalletTooltip isDisabled={ isWalletConnected || isMobile === undefined || !isMobile } isMobile>
<IconButton
aria-label="wallet menu"
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';
type Props = {
......@@ -8,27 +9,41 @@ type Props = {
};
const WalletTooltip = ({ children, isDisabled, isMobile }: Props) => {
const router = useRouter();
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(() => {
const key = `wallet-connect-tooltip-shown-${ isMobile ? 'mobile' : 'desktop' }`;
const wasShown = window.localStorage.getItem(key);
if (!wasShown) {
const wasShown = window.localStorage.getItem(localStorageKey);
if (!isDisabled && !wasShown) {
setIsTooltipShown.on();
window.localStorage.setItem(key, 'true');
window.localStorage.setItem(localStorageKey, 'true');
setTimeout(() => setIsTooltipShown.off(), 3000);
}
}, [ setIsTooltipShown, isMobile ]);
}, [ setIsTooltipShown, localStorageKey, isDisabled ]);
return (
<Tooltip
label={ <span>Your wallet is used to interact with<br/>apps and contracts in the explorer</span> }
label={ isTooltipShown ? label : defaultLabel }
textAlign="center"
padding={ 2 }
isDisabled={ isDisabled }
openDelay={ 300 }
openDelay={ 500 }
isOpen={ isTooltipShown || (isMobile ? false : undefined) }
onClose={ setIsTooltipShown.off }
display={ isMobile ? { base: 'flex', lg: 'none' } : { base: 'none', lg: 'flex' } }
ref={ ref }
>
{ children }
</Tooltip>
......
......@@ -51,7 +51,13 @@ const TokensTableItem = ({
};
return (
<Tr>
<Tr
sx={{
'&:hover [aria-label="Add token to wallet"]': {
opacity: 1,
},
}}
>
<Td>
<Flex alignItems="flex-start">
<Skeleton
......@@ -81,7 +87,12 @@ const TokensTableItem = ({
fontSize="sm"
fontWeight={ 500 }
/>
<AddressAddToWallet token={ token } isLoading={ isLoading } iconSize={ 5 }/>
<AddressAddToWallet
token={ token }
isLoading={ isLoading }
iconSize={ 5 }
opacity={ 0 }
/>
</Flex>
<Flex columnGap={ 1 }>
<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