Commit 988d3f6f authored by tom's avatar tom

Merge branch 'contract-verification-form' into fancy-select

parents 954d757f 9f8cc98c
......@@ -50,12 +50,12 @@ blockscout:
resources:
limits:
memory:
_default: "4Gi"
_default: "5Gi"
cpu:
_default: "4"
requests:
memory:
_default: "4Gi"
_default: "5Gi"
cpu:
_default: "4"
# enable service to connect to RDS
......@@ -143,7 +143,7 @@ blockscout:
RUST_VERIFICATION_SERVICE_URL:
_default: http://sc-verifier-svc:80
INDEXER_MEMORY_LIMIT:
_default: 3
_default: 5
ACCOUNT_ENABLED:
_default: 'true'
API_V2_ENABLED:
......@@ -171,14 +171,14 @@ postgres:
resources:
limits:
memory:
_default: "5Gi"
_default: "6Gi"
cpu:
_default: "1.5"
_default: "2"
requests:
memory:
_default: "5Gi"
_default: "6Gi"
cpu:
_default: "1.5"
_default: "2"
# node label
nodeSelector:
enabled: true
......
import _debounce from 'lodash/debounce';
import type { LegacyRef } from 'react';
import React from 'react';
export default function useClientRect<E extends Element>(): [ DOMRect | null, LegacyRef<E> | undefined ] {
const [ rect, setRect ] = React.useState<DOMRect | null>(null);
const nodeRef = React.useRef<E>();
const ref = React.useCallback((node: E) => {
if (node !== null) {
setRect(node.getBoundingClientRect());
}
nodeRef.current = node;
}, []);
React.useEffect(() => {
const content = window.document.querySelector('main');
if (!content) {
return;
}
const resizeHandler = _debounce(() => {
setRect(nodeRef.current?.getBoundingClientRect() ?? null);
}, 100);
const resizeObserver = new ResizeObserver(resizeHandler);
resizeObserver.observe(content);
resizeObserver.observe(window.document.body);
return function cleanup() {
resizeObserver.unobserve(content);
resizeObserver.unobserve(window.document.body);
};
}, [ ]);
return [ rect, ref ];
}
import React from 'react';
// Returns true if component is just mounted (on first render) and false otherwise.
export function useFirstMountState(): boolean {
const isFirst = React.useRef(true);
if (isFirst.current) {
isFirst.current = false;
return true;
}
return isFirst.current;
}
......@@ -25,13 +25,13 @@ export default function useNavItems() {
return React.useMemo(() => {
const mainNavItems = [
{ text: 'Blocks', url: link('blocks'), icon: blocksIcon, isActive: currentRoute.startsWith('block'), isNewUi: false },
{ text: 'Transactions', url: link('txs'), icon: transactionsIcon, isActive: currentRoute.startsWith('tx'), isNewUi: false },
{ text: 'Tokens', url: link('tokens'), icon: tokensIcon, isActive: currentRoute === 'tokens', isNewUi: false },
{ text: 'Accounts', url: link('accounts'), icon: walletIcon, isActive: currentRoute === 'accounts', isNewUi: false },
{ text: 'Blocks', url: link('blocks'), icon: blocksIcon, isActive: currentRoute.startsWith('block'), isNewUi: true },
{ text: 'Transactions', url: link('txs'), icon: transactionsIcon, isActive: currentRoute.startsWith('tx'), isNewUi: true },
{ text: 'Tokens', url: link('tokens'), icon: tokensIcon, isActive: currentRoute.startsWith('token'), isNewUi: true },
{ text: 'Accounts', url: link('accounts'), icon: walletIcon, isActive: currentRoute === 'accounts', isNewUi: true },
isMarketplaceFilled ?
{ text: 'Apps', url: link('apps'), icon: appsIcon, isActive: currentRoute === 'apps', isNewUi: true } : null,
{ text: 'Charts & stats', url: link('stats'), icon: statsIcon, isActive: currentRoute === 'stats', isNewUi: false },
{ text: 'Apps', url: link('apps'), icon: appsIcon, isActive: currentRoute.startsWith('app'), isNewUi: true } : null,
{ text: 'Charts & stats', url: link('stats'), icon: statsIcon, isActive: currentRoute === 'stats', isNewUi: true },
// there should be custom site sections like Stats, Faucet, More, etc but never an 'other'
// examples https://explorer-edgenet.polygon.technology/ and https://explorer.celo.org/
// at this stage custom menu items is under development, we will implement it later
......
import React from 'react';
import { useFirstMountState } from './useFirstMountState';
// React effect hook that ignores the first invocation (e.g. on mount). The signature is exactly the same as the useEffect hook.
const useUpdateEffect: typeof React.useEffect = (effect, deps) => {
const isFirstMount = useFirstMountState();
React.useEffect(() => {
if (!isFirstMount) {
return effect();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, deps);
};
export default useUpdateEffect;
import type { ServerResponse } from 'http';
export function appendValue(res: ServerResponse | undefined, name: string, value: number) {
const currentValue = res?.getHeader('Server-Timing') || '';
const nextValue = [
currentValue,
`${ name };dur=${ value }`,
].filter(Boolean).join(',');
res?.setHeader('Server-Timing', nextValue);
}
......@@ -8,7 +8,7 @@ export default function getSeo(params: PageParams) {
return {
title: params ? `${ params.hash } - ${ networkTitle }` : '',
description: params ?
`${ params.hash }, balances, and analytics on the on the ${ networkTitle }` :
`${ params.hash }, balances and analytics on the ${ networkTitle }` :
'',
};
}
......@@ -10,6 +10,7 @@ const cspPolicy = getCspPolicy();
export function middleware(req: NextRequest) {
const isPageRequest = req.headers.get('accept')?.includes('text/html');
const start = Date.now();
if (!isPageRequest) {
return;
......@@ -25,8 +26,11 @@ export function middleware(req: NextRequest) {
return NextResponse.redirect(authUrl);
}
const end = Date.now();
const res = NextResponse.next();
res.headers.append('Content-Security-Policy-Report-Only', cspPolicy);
res.headers.append('Server-Timing', `middleware;dur=${ end - start }`);
return res;
}
......
......@@ -21,7 +21,7 @@ export const baseResponse: AddressCoinBalanceHistoryResponse = {
block_number: 30367234,
block_timestamp: '2022-10-01T17:55:20Z',
delta: '1933020674364000',
transaction_hash: '0x62d597ebcf3e8d60096dd0363bc2f0f5e2df27ba1dacd696c51aa7c9409f3193',
transaction_hash: null,
value: '10143933697708939226',
},
{
......
import { ColorModeScript } from '@chakra-ui/react';
import type { DocumentContext } from 'next/document';
import Document, { Html, Head, Main, NextScript } from 'next/document';
import React from 'react';
import * as serverTiming from 'lib/next/serverTiming';
import theme from 'theme';
class MyDocument extends Document {
static async getInitialProps(ctx: DocumentContext) {
const originalRenderPage = ctx.renderPage;
ctx.renderPage = async() => {
const start = Date.now();
const result = await originalRenderPage();
const end = Date.now();
serverTiming.appendValue(ctx.res, 'renderPage', end - start);
return result;
};
const initialProps = await Document.getInitialProps(ctx);
return initialProps;
}
render() {
return (
<Html lang="en">
......
......@@ -10,6 +10,7 @@ import link from 'lib/link/link';
import getNetworkTitle from 'lib/networks/getNetworkTitle';
import type { Props } from 'lib/next/getServerSideProps';
import { getServerSideProps as getServerSidePropsBase } from 'lib/next/getServerSideProps';
import * as serverTiming from 'lib/next/serverTiming';
import SearchResults from 'ui/pages/SearchResults';
const SearchResultsPage: NextPage = () => {
......@@ -27,6 +28,8 @@ const SearchResultsPage: NextPage = () => {
export default SearchResultsPage;
export const getServerSideProps: GetServerSideProps<Props> = async({ req, res, resolvedUrl, query }) => {
const start = Date.now();
try {
const q = String(query.q);
const url = buildUrlNode('search_check_redirect', undefined, { q });
......@@ -63,5 +66,9 @@ export const getServerSideProps: GetServerSideProps<Props> = async({ req, res, r
};
} catch (error) {}
const end = Date.now();
serverTiming.appendValue(res, 'query.search.check-redirect', end - start);
return getServerSidePropsBase({ req, res, resolvedUrl, query });
};
......@@ -6,7 +6,7 @@ export default async function insertAdPlaceholder(page: Page) {
const adContainer = document.getElementById('adBanner');
const adReplacer = document.createElement('div');
adReplacer.style.width = '200px';
adReplacer.style.height = '100px';
adReplacer.style.height = '100%';
adReplacer.style.background = '#f00';
adContainer?.replaceChildren(adReplacer);
});
......
......@@ -15,6 +15,10 @@ const semanticTokens = {
link_hovered: {
'default': 'blue.400',
},
error: {
'default': 'red.400',
_dark: 'red.300',
},
},
};
......
......@@ -75,10 +75,10 @@ const AddressBlocksValidated = ({ scrollRef }: Props) => {
if (query.isLoading) {
return (
<>
<Hide below="lg">
<Hide below="lg" ssr={ false }>
<SkeletonTable columns={ [ '17%', '17%', '16%', '25%', '25%' ] }/>
</Hide>
<Show below="lg">
<Show below="lg" ssr={ false }>
<SkeletonList/>
</Show>
</>
......@@ -95,7 +95,7 @@ const AddressBlocksValidated = ({ scrollRef }: Props) => {
return (
<>
<Hide below="lg">
<Hide below="lg" ssr={ false }>
<Table variant="simple" size="sm">
<Thead top={ 80 }>
<Tr>
......@@ -113,7 +113,7 @@ const AddressBlocksValidated = ({ scrollRef }: Props) => {
</Tbody>
</Table>
</Hide>
<Show below="lg">
<Show below="lg" ssr={ false }>
{ query.data.items.map((item) => (
<AddressBlocksValidatedListItem key={ item.height } { ...item } page={ query.pagination.page }/>
)) }
......
......@@ -18,11 +18,13 @@ const AddressCoinBalance = () => {
const [ socketAlert, setSocketAlert ] = React.useState(false);
const queryClient = useQueryClient();
const router = useRouter();
const scrollRef = React.useRef<HTMLDivElement>(null);
const addressHash = String(router.query?.id);
const coinBalanceQuery = useQueryWithPages({
resourceName: 'address_coin_balance',
pathParams: { id: addressHash },
scrollRef,
});
const handleSocketError = React.useCallback(() => {
......@@ -65,6 +67,7 @@ const AddressCoinBalance = () => {
<>
{ socketAlert && <SocketAlert mb={ 6 }/> }
<AddressCoinBalanceChart addressHash={ addressHash }/>
<div ref={ scrollRef }></div>
<AddressCoinBalanceHistory query={ coinBalanceQuery }/>
</>
);
......
......@@ -5,6 +5,7 @@ import React from 'react';
import type { Address } from 'types/api/address';
import type { ResourceError } from 'lib/api/resources';
import * as addressMock from 'mocks/address/address';
import * as countersMock from 'mocks/address/counters';
import * as tokenBalanceMock from 'mocks/address/tokenBalance';
......@@ -36,7 +37,7 @@ test('contract +@mobile', async({ mount, page }) => {
const component = await mount(
<TestApp>
<AddressDetails addressQuery={{ data: addressMock.contract } as UseQueryResult<Address, unknown>}/>
<AddressDetails addressQuery={{ data: addressMock.contract } as UseQueryResult<Address, ResourceError>}/>
</TestApp>,
{ hooksConfig },
);
......@@ -65,7 +66,7 @@ test('token', async({ mount, page }) => {
const component = await mount(
<TestApp>
<MockAddressPage>
<AddressDetails addressQuery={{ data: addressMock.token } as UseQueryResult<Address, unknown>}/>
<AddressDetails addressQuery={{ data: addressMock.token } as UseQueryResult<Address, ResourceError>}/>
</MockAddressPage>
</TestApp>,
{ hooksConfig },
......@@ -86,7 +87,7 @@ test('validator +@mobile', async({ mount, page }) => {
const component = await mount(
<TestApp>
<AddressDetails addressQuery={{ data: addressMock.validator } as UseQueryResult<Address, unknown>}/>
<AddressDetails addressQuery={{ data: addressMock.validator } as UseQueryResult<Address, ResourceError>}/>
</TestApp>,
{ hooksConfig },
);
......
import { Box, Flex, Text, Icon, Grid, Link } from '@chakra-ui/react';
import { Box, Flex, Text, Icon, Grid } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import React from 'react';
......@@ -7,6 +7,7 @@ import type { Address as TAddress } from 'types/api/address';
import appConfig from 'configs/app/config';
import blockIcon from 'icons/block.svg';
import type { ResourceError } from 'lib/api/resources';
import useApiQuery from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile';
import link from 'lib/link/link';
......@@ -16,9 +17,10 @@ import AddressLink from 'ui/shared/address/AddressLink';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import ExternalLink from 'ui/shared/ExternalLink';
import HashStringShorten from 'ui/shared/HashStringShorten';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import LinkExternal from 'ui/shared/LinkExternal';
import LinkInternal from 'ui/shared/LinkInternal';
import AddressAddToMetaMask from './details/AddressAddToMetaMask';
import AddressBalance from './details/AddressBalance';
......@@ -29,7 +31,7 @@ import AddressQrCode from './details/AddressQrCode';
import TokenSelect from './tokenSelect/TokenSelect';
interface Props {
addressQuery: UseQueryResult<TAddress>;
addressQuery: UseQueryResult<TAddress, ResourceError>;
scrollRef?: React.RefObject<HTMLDivElement>;
}
......@@ -37,8 +39,10 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
const router = useRouter();
const isMobile = useIsMobile();
const addressHash = router.query.id?.toString();
const countersQuery = useApiQuery('address_counters', {
pathParams: { id: router.query.id?.toString() },
pathParams: { id: addressHash },
queryOptions: {
enabled: Boolean(router.query.id) && Boolean(addressQuery.data),
},
......@@ -51,40 +55,61 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
}, 500);
}, [ scrollRef ]);
if (addressQuery.isError) {
const errorData = React.useMemo(() => ({
hash: addressHash || '',
is_contract: false,
implementation_name: null,
implementation_address: null,
token: null,
watchlist_names: null,
creation_tx_hash: null,
block_number_balance_updated_at: null,
name: null,
exchange_rate: null,
coin_balance: null,
has_tokens: true,
has_token_transfers: true,
has_validated_blocks: false,
}), [ addressHash ]);
const is404Error = addressQuery.isError && 'status' in addressQuery.error && addressQuery.error.status === 404;
const is422Error = addressQuery.isError && 'status' in addressQuery.error && addressQuery.error.status === 422;
if (addressQuery.isError && is422Error) {
throw Error('Address fetch error', { cause: addressQuery.error as unknown as Error });
}
if (addressQuery.isLoading) {
return <AddressDetailsSkeleton/>;
if (addressQuery.isError && !is404Error) {
return <DataFetchAlert/>;
}
if (addressQuery.isError) {
return <DataFetchAlert/>;
if (addressQuery.isLoading) {
return <AddressDetailsSkeleton/>;
}
const explorers = appConfig.network.explorers.filter(({ paths }) => paths.address);
const data = addressQuery.isError ? errorData : addressQuery.data;
return (
<Box>
<Flex alignItems="center">
<AddressIcon address={ addressQuery.data }/>
<AddressIcon address={ data }/>
<Text ml={ 2 } fontFamily="heading" fontWeight={ 500 }>
{ isMobile ? <HashStringShorten hash={ addressQuery.data.hash }/> : addressQuery.data.hash }
{ isMobile ? <HashStringShorten hash={ data.hash }/> : data.hash }
</Text>
<CopyToClipboard text={ addressQuery.data.hash }/>
{ addressQuery.data.is_contract && addressQuery.data.token && <AddressAddToMetaMask ml={ 2 } token={ addressQuery.data.token }/> }
{ !addressQuery.data.is_contract && (
<AddressFavoriteButton hash={ addressQuery.data.hash } isAdded={ Boolean(addressQuery.data.watchlist_names?.length) } ml={ 3 }/>
<CopyToClipboard text={ data.hash }/>
{ data.is_contract && data.token && <AddressAddToMetaMask ml={ 2 } token={ data.token }/> }
{ !data.is_contract && (
<AddressFavoriteButton hash={ data.hash } isAdded={ Boolean(data.watchlist_names?.length) } ml={ 3 }/>
) }
<AddressQrCode hash={ addressQuery.data.hash } ml={ 2 }/>
<AddressQrCode hash={ data.hash } ml={ 2 }/>
</Flex>
{ explorers.length > 0 && (
<Flex mt={ 8 } columnGap={ 4 } flexWrap="wrap">
<Text fontSize="sm">Verify with other explorers</Text>
{ explorers.map((explorer) => {
const url = new URL(explorer.paths.tx + '/' + router.query.id, explorer.baseUrl);
return <ExternalLink key={ explorer.baseUrl } title={ explorer.title } href={ url.toString() }/>;
const url = new URL(explorer.paths.address + '/' + router.query.id, explorer.baseUrl);
return <LinkExternal key={ explorer.baseUrl } title={ explorer.title } href={ url.toString() }/>;
}) }
</Flex>
) }
......@@ -94,83 +119,93 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
rowGap={{ base: 1, lg: 3 }}
templateColumns={{ base: 'minmax(0, 1fr)', lg: 'auto minmax(0, 1fr)' }} overflow="hidden"
>
<AddressNameInfo data={ addressQuery.data }/>
{ addressQuery.data.is_contract && addressQuery.data.creation_tx_hash && addressQuery.data.creator_address_hash && (
<AddressNameInfo data={ data }/>
{ data.is_contract && data.creation_tx_hash && data.creator_address_hash && (
<DetailsInfoItem
title="Creator"
hint="Transaction and address of creation."
>
<AddressLink type="address" hash={ addressQuery.data.creator_address_hash } truncation="constant"/>
<AddressLink type="address" hash={ data.creator_address_hash } truncation="constant"/>
<Text whiteSpace="pre"> at txn </Text>
<AddressLink hash={ addressQuery.data.creation_tx_hash } type="transaction" truncation="constant"/>
<AddressLink hash={ data.creation_tx_hash } type="transaction" truncation="constant"/>
</DetailsInfoItem>
) }
{ addressQuery.data.is_contract && addressQuery.data.implementation_address && (
{ data.is_contract && data.implementation_address && (
<DetailsInfoItem
title="Implementation"
hint="Implementation address of the proxy contract."
columnGap={ 1 }
>
<Link href={ link('address_index', { id: addressQuery.data.implementation_address }) }>{ addressQuery.data.implementation_name }</Link>
<LinkInternal href={ link('address_index', { id: data.implementation_address }) }>
{ data.implementation_name }
</LinkInternal>
<Text variant="secondary" overflow="hidden">
<HashStringShortenDynamic hash={ `(${ addressQuery.data.implementation_address })` }/>
<HashStringShortenDynamic hash={ `(${ data.implementation_address })` }/>
</Text>
</DetailsInfoItem>
) }
<AddressBalance data={ addressQuery.data }/>
{ addressQuery.data.has_tokens && (
<AddressBalance data={ data }/>
{ data.has_tokens && (
<DetailsInfoItem
title="Tokens"
hint="All tokens in the account and total value."
alignSelf="center"
py={ 0 }
>
<TokenSelect/>
{ addressQuery.data ? <TokenSelect onClick={ handleCounterItemClick }/> : <Box py="6px">0</Box> }
</DetailsInfoItem>
) }
<DetailsInfoItem
title="Transactions"
hint="Number of transactions related to this address."
>
<AddressCounterItem prop="transactions_count" query={ countersQuery } address={ addressQuery.data.hash } onClick={ handleCounterItemClick }/>
{ addressQuery.data ?
<AddressCounterItem prop="transactions_count" query={ countersQuery } address={ data.hash } onClick={ handleCounterItemClick }/> :
0 }
</DetailsInfoItem>
{ addressQuery.data.has_token_transfers && (
{ data.has_token_transfers && (
<DetailsInfoItem
title="Transfers"
hint="Number of transfers to/from this address."
>
<AddressCounterItem prop="token_transfers_count" query={ countersQuery } address={ addressQuery.data.hash } onClick={ handleCounterItemClick }/>
{ addressQuery.data ?
<AddressCounterItem prop="token_transfers_count" query={ countersQuery } address={ data.hash } onClick={ handleCounterItemClick }/> :
0 }
</DetailsInfoItem>
) }
<DetailsInfoItem
title="Gas used"
hint="Gas used by the address."
>
<AddressCounterItem prop="gas_usage_count" query={ countersQuery } address={ addressQuery.data.hash } onClick={ handleCounterItemClick }/>
{ addressQuery.data ?
<AddressCounterItem prop="gas_usage_count" query={ countersQuery } address={ data.hash } onClick={ handleCounterItemClick }/> :
0 }
</DetailsInfoItem>
{ addressQuery.data.has_validated_blocks && (
{ data.has_validated_blocks && (
<DetailsInfoItem
title="Blocks validated"
hint="Number of blocks validated by this validator."
>
<AddressCounterItem prop="validations_count" query={ countersQuery } address={ addressQuery.data.hash } onClick={ handleCounterItemClick }/>
{ addressQuery.data ?
<AddressCounterItem prop="validations_count" query={ countersQuery } address={ data.hash } onClick={ handleCounterItemClick }/> :
0 }
</DetailsInfoItem>
) }
{ addressQuery.data.block_number_balance_updated_at && (
{ data.block_number_balance_updated_at && (
<DetailsInfoItem
title="Last balance update"
hint="Block number in which the address was updated."
alignSelf="center"
py={{ base: '2px', lg: 1 }}
>
<Link
href={ link('block', { id: String(addressQuery.data.block_number_balance_updated_at) }) }
<LinkInternal
href={ link('block', { id: String(data.block_number_balance_updated_at) }) }
display="flex"
alignItems="center"
>
<Icon as={ blockIcon } boxSize={ 6 } mr={ 2 }/>
{ addressQuery.data.block_number_balance_updated_at }
</Link>
{ data.block_number_balance_updated_at }
</LinkInternal>
</DetailsInfoItem>
) }
</Grid>
......
......@@ -48,8 +48,8 @@ const AddressInternalTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivE
if (isLoading) {
return (
<>
<Show below="lg"><AddressIntTxsSkeletonMobile/></Show>
<Hide below="lg"><AddressIntTxsSkeletonDesktop/></Hide>
<Show below="lg" ssr={ false }><AddressIntTxsSkeletonMobile/></Show>
<Hide below="lg" ssr={ false }><AddressIntTxsSkeletonDesktop/></Hide>
</>
);
}
......
......@@ -166,10 +166,10 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD
if (isLoading) {
return (
<>
<Hide below="lg">
<Hide below="lg" ssr={ false }>
<SkeletonTable columns={ [ '44px', '185px', '160px', '25%', '25%', '25%', '25%' ] }/>
</Hide>
<Show below="lg">
<Show below="lg" ssr={ false }>
<SkeletonList/>
</Show>
</>
......@@ -191,7 +191,7 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD
const items = data.items.reduce(flattenTotal, []);
return (
<>
<Hide below="lg">
<Hide below="lg" ssr={ false }>
<TokenTransferTable
data={ items }
baseAddress={ currentAddress }
......@@ -203,7 +203,7 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD
socketInfoNum={ newItemsCount }
/>
</Hide>
<Show below="lg">
<Show below="lg" ssr={ false }>
{ pagination.page === 1 && !tokenFilter && (
<SocketNewItemsNotice
url={ window.location.href }
......
......@@ -27,6 +27,7 @@ const AddressTxsFilter = ({ onFilterChange, defaultFilter, isActive }: Props) =>
<FilterButton
isActive={ isOpen || isActive }
onClick={ onToggle }
as="div"
/>
</MenuButton>
<MenuList zIndex={ 2 }>
......
import { Link, Text, Flex } from '@chakra-ui/react';
import { Text, Flex } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import React from 'react';
......@@ -8,6 +8,7 @@ import appConfig from 'configs/app/config';
import getBlockTotalReward from 'lib/block/getBlockTotalReward';
import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
import link from 'lib/link/link';
import LinkInternal from 'ui/shared/LinkInternal';
import ListItemMobile from 'ui/shared/ListItemMobile';
import Utilization from 'ui/shared/Utilization/Utilization';
......@@ -23,7 +24,7 @@ const AddressBlocksValidatedListItem = (props: Props) => {
return (
<ListItemMobile rowGap={ 2 } isAnimated>
<Flex justifyContent="space-between" w="100%">
<Link href={ blockUrl } fontWeight="700">{ props.height }</Link>
<LinkInternal href={ blockUrl } fontWeight="700">{ props.height }</LinkInternal>
<Text variant="secondary">{ timeAgo }</Text>
</Flex>
<Flex columnGap={ 2 } w="100%">
......
import { Link, Td, Tr, Text, Box, Flex } from '@chakra-ui/react';
import { Td, Tr, Text, Box, Flex } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import React from 'react';
......@@ -7,6 +7,7 @@ import type { Block } from 'types/api/block';
import getBlockTotalReward from 'lib/block/getBlockTotalReward';
import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
import link from 'lib/link/link';
import LinkInternal from 'ui/shared/LinkInternal';
import Utilization from 'ui/shared/Utilization/Utilization';
type Props = Block & {
......@@ -21,7 +22,7 @@ const AddressBlocksValidatedTableItem = (props: Props) => {
return (
<Tr>
<Td>
<Link href={ blockUrl } fontWeight="700">{ props.height }</Link>
<LinkInternal href={ blockUrl } fontWeight="700">{ props.height }</LinkInternal>
</Td>
<Td>
<Text variant="secondary">{ timeAgo }</Text>
......
......@@ -25,6 +25,7 @@ const AddressCoinBalanceChart = ({ addressHash }: Props) => {
title="Balances"
items={ items }
isLoading={ isLoading }
h="250px"
/>
);
};
......
......@@ -29,10 +29,10 @@ const AddressCoinBalanceHistory = ({ query }: Props) => {
if (query.isLoading) {
return (
<>
<Hide below="lg">
<Hide below="lg" ssr={ false }>
<SkeletonTable columns={ [ '25%', '25%', '25%', '25%', '120px' ] }/>
</Hide>
<Show below="lg">
<Show below="lg" ssr={ false }>
<SkeletonList/>
</Show>
</>
......@@ -45,7 +45,7 @@ const AddressCoinBalanceHistory = ({ query }: Props) => {
return (
<>
<Hide below="lg">
<Hide below="lg" ssr={ false }>
<Table variant="simple" size="sm">
<Thead top={ 80 }>
<Tr>
......@@ -63,7 +63,7 @@ const AddressCoinBalanceHistory = ({ query }: Props) => {
</Tbody>
</Table>
</Hide>
<Show below="lg">
<Show below="lg" ssr={ false }>
{ query.data.items.map((item) => (
<AddressCoinBalanceListItem key={ item.block_number } { ...item } page={ query.pagination.page }/>
)) }
......
import { Link, Text, Stat, StatHelpText, StatArrow, Flex } from '@chakra-ui/react';
import { Text, Stat, StatHelpText, StatArrow, Flex } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import React from 'react';
......@@ -10,6 +10,7 @@ import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
import link from 'lib/link/link';
import Address from 'ui/shared/address/Address';
import AddressLink from 'ui/shared/address/AddressLink';
import LinkInternal from 'ui/shared/LinkInternal';
import ListItemMobile from 'ui/shared/ListItemMobile';
type Props = AddressCoinBalanceHistoryItem & {
......@@ -25,19 +26,19 @@ const AddressCoinBalanceListItem = (props: Props) => {
return (
<ListItemMobile rowGap={ 2 } isAnimated>
<Flex justifyContent="space-between" w="100%">
<Text fontWeight={ 600 }>{ BigNumber(props.value).div(WEI).toFixed(8) } { appConfig.network.currency.symbol }</Text>
<Text fontWeight={ 600 }>{ BigNumber(props.value).div(WEI).dp(8).toFormat() } { appConfig.network.currency.symbol }</Text>
<Stat flexGrow="0">
<StatHelpText display="flex" mb={ 0 } alignItems="center">
<StatArrow type={ isPositiveDelta ? 'increase' : 'decrease' }/>
<Text as="span" color={ isPositiveDelta ? 'green.500' : 'red.500' } fontWeight={ 600 }>
{ deltaBn.toFormat() }
{ deltaBn.dp(8).toFormat() }
</Text>
</StatHelpText>
</Stat>
</Flex>
<Flex columnGap={ 2 } w="100%">
<Text fontWeight={ 500 } flexShrink={ 0 }>Block</Text>
<Link href={ blockUrl } fontWeight="700">{ props.block_number }</Link>
<LinkInternal href={ blockUrl } fontWeight="700">{ props.block_number }</LinkInternal>
</Flex>
{ props.transaction_hash && (
<Flex columnGap={ 2 } w="100%">
......
import { Link, Td, Tr, Text, Stat, StatHelpText, StatArrow } from '@chakra-ui/react';
import { Td, Tr, Text, Stat, StatHelpText, StatArrow } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import React from 'react';
......@@ -9,6 +9,7 @@ import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
import link from 'lib/link/link';
import Address from 'ui/shared/address/Address';
import AddressLink from 'ui/shared/address/AddressLink';
import LinkInternal from 'ui/shared/LinkInternal';
type Props = AddressCoinBalanceHistoryItem & {
page: number;
......@@ -23,12 +24,12 @@ const AddressCoinBalanceTableItem = (props: Props) => {
return (
<Tr>
<Td>
<Link href={ blockUrl } fontWeight="700">{ props.block_number }</Link>
<LinkInternal href={ blockUrl } fontWeight="700">{ props.block_number }</LinkInternal>
</Td>
<Td>
{ props.transaction_hash ?
(
<Address maxW="150px" fontWeight="700">
<Address w="150px" fontWeight="700">
<AddressLink hash={ props.transaction_hash } type="transaction"/>
</Address>
) :
......@@ -39,14 +40,14 @@ const AddressCoinBalanceTableItem = (props: Props) => {
<Text variant="secondary">{ timeAgo }</Text>
</Td>
<Td isNumeric pr={ 1 }>
<Text>{ BigNumber(props.value).div(WEI).toFixed(8) }</Text>
<Text>{ BigNumber(props.value).div(WEI).dp(8).toFormat() }</Text>
</Td>
<Td isNumeric display="flex" justifyContent="end">
<Stat flexGrow="0">
<StatHelpText display="flex" mb={ 0 } alignItems="center">
<StatArrow type={ isPositiveDelta ? 'increase' : 'decrease' }/>
<Text as="span" color={ isPositiveDelta ? 'green.500' : 'red.500' } fontWeight={ 600 }>
{ deltaBn.toFormat() }
{ deltaBn.dp(8).toFormat() }
</Text>
</StatHelpText>
</Stat>
......
......@@ -8,7 +8,8 @@ import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import ExternalLink from 'ui/shared/ExternalLink';
import LinkExternal from 'ui/shared/LinkExternal';
import LinkInternal from 'ui/shared/LinkInternal';
import RawDataSnippet from 'ui/shared/RawDataSnippet';
import ContractSourceCode from './ContractSourceCode';
......@@ -72,7 +73,9 @@ const ContractCode = () => {
const decoded = data.decoded_constructor_args
.map(([ value, { name, type } ], index) => {
const valueEl = type === 'address' ? <Link href={ link('address_index', { id: value }) }>{ value }</Link> : <span>{ value }</span>;
const valueEl = type === 'address' ?
<LinkInternal href={ link('address_index', { id: value }) }>{ value }</LinkInternal> :
<span>{ value }</span>;
return (
<Box key={ index }>
<span>Arg [{ index }] { name || '' } ({ type }): </span>
......@@ -98,7 +101,7 @@ const ContractCode = () => {
return data.external_libraries.map((item) => (
<Box key={ item.address_hash }>
<chakra.span fontWeight={ 500 }>{ item.name }: </chakra.span>
<Link href={ link('address_index', { id: item.address_hash }, { tab: 'contract' }) }>{ item.address_hash }</Link>
<LinkInternal href={ link('address_index', { id: item.address_hash }, { tab: 'contract' }) }>{ item.address_hash }</LinkInternal>
</Box>
));
})();
......@@ -110,7 +113,7 @@ const ContractCode = () => {
{ data.is_verified_via_sourcify && (
<Alert status="warning" whiteSpace="pre-wrap" flexWrap="wrap">
<span>This contract has been { data.is_partially_verified ? 'partially ' : '' }verified via Sourcify. </span>
{ data.sourcify_repo_url && <ExternalLink href={ data.sourcify_repo_url } title="View contract in Sourcify repository" fontSize="md"/> }
{ data.sourcify_repo_url && <LinkExternal href={ data.sourcify_repo_url } title="View contract in Sourcify repository" fontSize="md"/> }
</Alert>
) }
{ data.is_changed_bytecode && (
......@@ -126,7 +129,7 @@ const ContractCode = () => {
<AddressLink type="address" hash={ data.verified_twin_address_hash } truncation="constant" ml={ 2 }/>
</Address>
<chakra.span mt={ 1 }>All functions displayed below are from ABI of that contract. In order to verify current contract, proceed with </chakra.span>
<Link href={ link('address_contract_verification', { id: data.verified_twin_address_hash }) }>Verify & Publish</Link>
<LinkInternal href={ link('address_contract_verification', { id: data.verified_twin_address_hash }) }>Verify & Publish</LinkInternal>
<span> page</span>
</Alert>
) }
......
import { Box, chakra, Link, Spinner } from '@chakra-ui/react';
import { Box, chakra, Spinner } from '@chakra-ui/react';
import React from 'react';
import type { ContractMethodWriteResult } from './types';
import link from 'lib/link/link';
import LinkInternal from 'ui/shared/LinkInternal';
interface Props {
result: ContractMethodWriteResult;
......@@ -30,7 +31,7 @@ const ContractWriteResultDumb = ({ result, onSettle, txInfo }: Props) => {
const isErrorResult = 'message' in result;
const txLink = (
<Link href={ link('tx', { id: txHash }) }>View transaction details</Link>
<LinkInternal href={ link('tx', { id: txHash }) }>View transaction details</LinkInternal>
);
const content = (() => {
......@@ -85,7 +86,7 @@ const ContractWriteResultDumb = ({ result, onSettle, txInfo }: Props) => {
alignItems="center"
whiteSpace="pre-wrap"
wordBreak="break-all"
color={ txInfo.status === 'error' || isErrorResult ? 'red.600' : undefined }
color={ txInfo.status === 'error' || isErrorResult ? 'error' : undefined }
>
{ content }
</Box>
......
......@@ -13,7 +13,7 @@ import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import TokenLogo from 'ui/shared/TokenLogo';
interface Props {
data: Address;
data: Pick<Address, 'block_number_balance_updated_at' | 'coin_balance' | 'hash' | 'exchange_rate'>;
}
const AddressBalance = ({ data }: Props) => {
......@@ -63,10 +63,6 @@ const AddressBalance = ({ data }: Props) => {
handler: handleNewCoinBalanceMessage,
});
if (!data.coin_balance) {
return null;
}
return (
<DetailsInfoItem
title="Balance"
......@@ -82,7 +78,7 @@ const AddressBalance = ({ data }: Props) => {
fontSize="sm"
/>
<CurrencyValue
value={ data.coin_balance }
value={ data.coin_balance || '0' }
exchangeRate={ data.exchange_rate }
decimals={ String(appConfig.network.currency.decimals) }
currency={ appConfig.network.currency.symbol }
......
import { Link, Skeleton } from '@chakra-ui/react';
import { Skeleton } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query';
import BigNumber from 'bignumber.js';
import NextLink from 'next/link';
import React from 'react';
import type { AddressCounters } from 'types/api/address';
import link from 'lib/link/link';
import LinkInternal from 'ui/shared/LinkInternal';
interface Props {
prop: keyof AddressCounters;
......@@ -29,7 +29,7 @@ const AddressCounterItem = ({ prop, query, address, onClick }: Props) => {
const data = query.data?.[prop];
if (query.isError || data === null || data === undefined) {
return <span>no data</span>;
return <span>0</span>;
}
switch (prop) {
......@@ -42,11 +42,9 @@ const AddressCounterItem = ({ prop, query, address, onClick }: Props) => {
return <span>0</span>;
}
return (
<NextLink href={ link('address_index', { id: address }, { tab: PROP_TO_TAB[prop] }) } passHref>
<Link onClick={ onClick }>
{ Number(data).toLocaleString() }
</Link>
</NextLink>
<LinkInternal href={ link('address_index', { id: address }, { tab: PROP_TO_TAB[prop] }) } onClick={ onClick }>
{ Number(data).toLocaleString() }
</LinkInternal>
);
}
}
......
import { Link } from '@chakra-ui/react';
import React from 'react';
import type { Address } from 'types/api/address';
import link from 'lib/link/link';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import LinkInternal from 'ui/shared/LinkInternal';
interface Props {
data: Address;
data: Pick<Address, 'name' | 'token' | 'is_contract'>;
}
const AddressNameInfo = ({ data }: Props) => {
......@@ -17,9 +17,9 @@ const AddressNameInfo = ({ data }: Props) => {
title="Token name"
hint="Token name and symbol"
>
<Link href={ link('token_index', { hash: data.token.address }) }>
<LinkInternal href={ link('token_index', { hash: data.token.address }) }>
{ data.token.name } ({ data.token.symbol })
</Link>
</LinkInternal>
</DetailsInfoItem>
);
}
......
import { Flex, Tag, Icon, Box, HStack, Text, Link } from '@chakra-ui/react';
import { Flex, Tag, Icon, Box, HStack, Text } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import React from 'react';
......@@ -12,6 +12,7 @@ import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import InOutTag from 'ui/shared/InOutTag';
import LinkInternal from 'ui/shared/LinkInternal';
import ListItemMobile from 'ui/shared/ListItemMobile';
import TxStatus from 'ui/shared/TxStatus';
import { TX_INTERNALS_ITEMS } from 'ui/tx/internals/utils';
......@@ -49,7 +50,7 @@ const TxInternalsListItem = ({
</Flex>
<HStack spacing={ 1 }>
<Text fontSize="sm" fontWeight={ 500 }>Block</Text>
<Link href={ link('block', { id: block.toString() }) }>{ block }</Link>
<LinkInternal href={ link('block', { id: block.toString() }) }>{ block }</LinkInternal>
</HStack>
<Box w="100%" display="flex" columnGap={ 3 }>
<Address width="calc((100% - 48px) / 2)">
......
import { Tr, Td, Tag, Icon, Box, Flex, Text, Link } from '@chakra-ui/react';
import { Tr, Td, Tag, Icon, Box, Flex, Text } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import React from 'react';
......@@ -12,6 +12,7 @@ import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import InOutTag from 'ui/shared/InOutTag';
import LinkInternal from 'ui/shared/LinkInternal';
import TxStatus from 'ui/shared/TxStatus';
import { TX_INTERNALS_ITEMS } from 'ui/tx/internals/utils';
......@@ -57,7 +58,7 @@ const AddressIntTxsTableItem = ({
</Flex>
</Td>
<Td verticalAlign="middle">
<Link href={ link('block', { id: block.toString() }) }>{ block }</Link>
<LinkInternal href={ link('block', { id: block.toString() }) }>{ block }</LinkInternal>
</Td>
<Td verticalAlign="middle">
<Address display="inline-flex" maxW="100%">
......
......@@ -17,7 +17,11 @@ import useSocketMessage from 'lib/socket/useSocketMessage';
import TokenSelectDesktop from './TokenSelectDesktop';
import TokenSelectMobile from './TokenSelectMobile';
const TokenSelect = () => {
interface Props {
onClick?: () => void;
}
const TokenSelect = ({ onClick }: Props) => {
const router = useRouter();
const isMobile = useIsMobile();
const queryClient = useQueryClient();
......@@ -88,6 +92,7 @@ const TokenSelect = () => {
pr="6px"
icon={ <Icon as={ walletIcon } boxSize={ 5 }/> }
as="a"
onClick={ onClick }
/>
</NextLink>
</Box>
......
......@@ -33,7 +33,7 @@ type Inputs = {
const NAME_MAX_LENGTH = 255;
const ApiKeyForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
const { control, handleSubmit, formState: { errors, isValid, isDirty }, setError } = useForm<Inputs>({
const { control, handleSubmit, formState: { errors, isDirty }, setError } = useForm<Inputs>({
mode: 'onTouched',
defaultValues: {
token: data?.api_key || '',
......@@ -147,7 +147,7 @@ const ApiKeyForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
<Button
size="lg"
type="submit"
disabled={ !isValid || !isDirty }
disabled={ !isDirty }
isLoading={ mutation.isLoading }
>
{ data ? 'Save' : 'Generate API key' }
......
import { Grid, GridItem, Text, Icon, Link, Box, Tooltip } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import capitalize from 'lodash/capitalize';
import NextLink from 'next/link';
import { useRouter } from 'next/router';
import React from 'react';
import { scroller, Element } from 'react-scroll';
......@@ -23,6 +22,7 @@ import DataFetchAlert from 'ui/shared/DataFetchAlert';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import GasUsedToTargetRatio from 'ui/shared/GasUsedToTargetRatio';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import LinkInternal from 'ui/shared/LinkInternal';
import PrevNext from 'ui/shared/PrevNext';
import TextSeparator from 'ui/shared/TextSeparator';
import Utilization from 'ui/shared/Utilization/Utilization';
......@@ -116,11 +116,9 @@ const BlockDetails = () => {
title="Transactions"
hint="The number of transactions in the block."
>
<NextLink href={ link('block', { id: router.query.id }, { tab: 'txs' }) } passHref>
<Link>
{ data.tx_count } transactions
</Link>
</NextLink>
<LinkInternal href={ link('block', { id: router.query.id }, { tab: 'txs' }) }>
{ data.tx_count } transactions
</LinkInternal>
</DetailsInfoItem>
<DetailsInfoItem
title={ appConfig.network.verificationType === 'validation' ? 'Validated by' : 'Mined by' }
......
......@@ -46,7 +46,13 @@ const BlocksContent = ({ type, query }: Props) => {
next_page_params: null,
};
}
return shouldAddToList ? { ...prevData, items: [ payload.block, ...prevData.items ] } : prevData;
if (!shouldAddToList || prevData.items.some((block => block.height === payload.block.height))) {
return prevData;
}
const newItems = [ payload.block, ...prevData.items ].sort((b1, b2) => b2.height - b1.height);
return { ...prevData, items: newItems };
});
}, [ queryClient, type ]);
......
import { Flex, Link, Spinner, Text, Box, Icon } from '@chakra-ui/react';
import { Flex, Spinner, Text, Box, Icon } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import capitalize from 'lodash/capitalize';
import React from 'react';
......@@ -14,6 +14,7 @@ import getNetworkValidatorTitle from 'lib/networks/getNetworkValidatorTitle';
import BlockTimestamp from 'ui/blocks/BlockTimestamp';
import AddressLink from 'ui/shared/address/AddressLink';
import GasUsedToTargetRatio from 'ui/shared/GasUsedToTargetRatio';
import LinkInternal from 'ui/shared/LinkInternal';
import ListItemMobile from 'ui/shared/ListItemMobile';
import Utilization from 'ui/shared/Utilization/Utilization';
......@@ -33,12 +34,12 @@ const BlocksListItem = ({ data, isPending, enableTimeIncrement }: Props) => {
<Flex justifyContent="space-between" w="100%">
<Flex columnGap={ 2 } alignItems="center">
{ isPending && <Spinner size="sm"/> }
<Link
<LinkInternal
fontWeight={ 600 }
href={ link('block', { id: String(data.height) }) }
>
{ data.height }
</Link>
</LinkInternal>
</Flex>
<BlockTimestamp ts={ data.timestamp } isEnabled={ enableTimeIncrement }/>
</Flex>
......
import { Tr, Td, Link, Flex, Box, Icon, Tooltip, Spinner, useColorModeValue } from '@chakra-ui/react';
import { Tr, Td, Flex, Box, Icon, Tooltip, Spinner, useColorModeValue } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import { motion } from 'framer-motion';
import React from 'react';
......@@ -12,6 +12,7 @@ import link from 'lib/link/link';
import BlockTimestamp from 'ui/blocks/BlockTimestamp';
import AddressLink from 'ui/shared/address/AddressLink';
import GasUsedToTargetRatio from 'ui/shared/GasUsedToTargetRatio';
import LinkInternal from 'ui/shared/LinkInternal';
import Utilization from 'ui/shared/Utilization/Utilization';
interface Props {
......@@ -38,12 +39,12 @@ const BlocksTableItem = ({ data, isPending, enableTimeIncrement }: Props) => {
<Flex columnGap={ 2 } alignItems="center" mb={ 2 }>
{ isPending && <Spinner size="sm" flexShrink={ 0 }/> }
<Tooltip isDisabled={ data.type !== 'reorg' } label="Chain reorganizations">
<Link
<LinkInternal
fontWeight={ 600 }
href={ link('block', { id: String(data.height) }) }
>
{ data.height }
</Link>
</LinkInternal>
</Tooltip>
</Flex>
<BlockTimestamp ts={ data.timestamp } isEnabled={ enableTimeIncrement }/>
......
......@@ -6,6 +6,7 @@ import type { TimeChartData } from 'ui/shared/chart/types';
import ethTokenTransferData from 'data/charts_eth_token_transfer.json';
import ethTxsData from 'data/charts_eth_txs.json';
import dayjs from 'lib/date/dayjs';
import useClientRect from 'lib/hooks/useClientRect';
import ChartArea from 'ui/shared/chart/ChartArea';
import ChartAxis from 'ui/shared/chart/ChartAxis';
import ChartGridLine from 'ui/shared/chart/ChartGridLine';
......@@ -16,8 +17,8 @@ import ChartSelectionX from 'ui/shared/chart/ChartSelectionX';
import ChartTooltip from 'ui/shared/chart/ChartTooltip';
// import useBrushX from 'ui/shared/chart/useBrushX';
import useChartLegend from 'ui/shared/chart/useChartLegend';
import useChartSize from 'ui/shared/chart/useChartSize';
import useTimeChartController from 'ui/shared/chart/useTimeChartController';
import calculateInnerSize from 'ui/shared/chart/utils/calculateInnerSize';
const CHART_MARGIN = { bottom: 20, left: 65, right: 30, top: 10 };
const CHART_OFFSET = {
......@@ -27,10 +28,10 @@ const RANGE_DEFAULT_START_DATE = dayjs.min(dayjs(ethTokenTransferData[0].date),
const RANGE_DEFAULT_LAST_DATE = dayjs.max(dayjs(ethTokenTransferData.at(-1)?.date), dayjs(ethTxsData.at(-1)?.date)).toDate();
const EthereumChart = () => {
const ref = React.useRef<SVGSVGElement>(null);
const overlayRef = React.useRef<SVGRectElement>(null);
const { width, height, innerWidth, innerHeight } = useChartSize(ref.current, CHART_MARGIN, CHART_OFFSET);
const [ rect, ref ] = useClientRect<SVGSVGElement>();
const { innerWidth, innerHeight } = calculateInnerSize(rect, CHART_MARGIN, CHART_OFFSET);
const [ range, setRange ] = React.useState<[ Date, Date ]>([ RANGE_DEFAULT_START_DATE, RANGE_DEFAULT_LAST_DATE ]);
const data: TimeChartData = [
......@@ -75,8 +76,8 @@ const EthereumChart = () => {
return (
<Box display="inline-block" position="relative" width="100%" height="100%">
<svg width={ width || '100%' } height={ height || '100%' } ref={ ref }>
<g transform={ `translate(${ CHART_MARGIN?.left || 0 },${ CHART_MARGIN?.top || 0 })` } opacity={ width ? 1 : 0 }>
<svg width="100%" height="calc(100% - 26px)" ref={ ref }>
<g transform={ `translate(${ CHART_MARGIN?.left || 0 },${ CHART_MARGIN?.top || 0 })` } opacity={ rect ? 1 : 0 }>
{ /* BASE GRID LINE */ }
<ChartGridLine
type="horizontal"
......
......@@ -2,17 +2,19 @@ import { useToken } from '@chakra-ui/react';
import React from 'react';
import ethTxsData from 'data/charts_eth_txs.json';
import useClientRect from 'lib/hooks/useClientRect';
import ChartLine from 'ui/shared/chart/ChartLine';
import useChartSize from 'ui/shared/chart/useChartSize';
import useTimeChartController from 'ui/shared/chart/useTimeChartController';
import calculateInnerSize from 'ui/shared/chart/utils/calculateInnerSize';
import { BlueLineGradient } from 'ui/shared/chart/utils/gradients';
const CHART_MARGIN = { bottom: 0, left: 0, right: 0, top: 0 };
const DATA = ethTxsData.slice(-30).map((d) => ({ ...d, date: new Date(d.date) }));
const SplineChartExample = () => {
const ref = React.useRef<SVGSVGElement>(null);
const { width, height, innerWidth, innerHeight } = useChartSize(ref.current, CHART_MARGIN);
const [ rect, ref ] = useClientRect<SVGSVGElement>();
const { innerWidth, innerHeight } = calculateInnerSize(rect, CHART_MARGIN);
const color = useToken('colors', 'blue.500');
const { xScale, yScale } = useTimeChartController({
data: [ { items: DATA, name: 'spline', color } ],
......@@ -21,7 +23,7 @@ const SplineChartExample = () => {
});
return (
<svg width={ width || '100%' } height={ height || '100%' } ref={ ref }>
<svg width="100%" height="100%" ref={ ref }>
<defs>
<BlueLineGradient.defs/>
</defs>
......
......@@ -59,7 +59,6 @@ const ContractVerificationForm = () => {
size="lg"
type="submit"
mt={ 12 }
isDisabled={ !formState.isValid || !formState.isDirty }
isLoading={ formState.isSubmitting }
loadingText="Verify & publish"
>
......
......@@ -39,6 +39,7 @@ const ContractVerificationFieldLibraryItem = ({ control, index, fieldsLength, on
isInvalid={ Boolean(error?.name) }
isDisabled={ isDisabled }
maxLength={ 255 }
autoComplete="off"
/>
<InputPlaceholder text="Library name (.sol file)" error={ error?.name }/>
</FormControl>
......@@ -53,6 +54,7 @@ const ContractVerificationFieldLibraryItem = ({ control, index, fieldsLength, on
isInvalid={ Boolean(error?.address) }
isDisabled={ isDisabled }
required
autoComplete="off"
/>
<InputPlaceholder text="Library address (0x...)" error={ error?.address }/>
</FormControl>
......
......@@ -27,6 +27,7 @@ const ContractVerificationFieldName = ({ hint }: Props) => {
isInvalid={ Boolean(error) }
maxLength={ 255 }
isDisabled={ formState.isSubmitting }
autoComplete="off"
/>
<InputPlaceholder text="Contract name" error={ error }/>
</FormControl>
......
......@@ -34,6 +34,7 @@ const ContractVerificationFieldOptimization = () => {
{ ...field }
required
isDisabled={ formState.isSubmitting }
autoComplete="off"
type="number"
/>
<InputPlaceholder text="Optimization runs"/>
......
......@@ -21,6 +21,8 @@ interface Props {
const ContractVerificationFieldSources = ({ accept, multiple, title, className, hint }: Props) => {
const { setValue, getValues, control, formState } = useFormContext<FormFields>();
const error = 'sources' in formState.errors ? formState.errors.sources : undefined;
const handleFileRemove = React.useCallback((index?: number) => {
if (index === undefined) {
return;
......@@ -57,8 +59,13 @@ const ContractVerificationFieldSources = ({ accept, multiple, title, className,
</Button>
</FileInput>
{ field.value && field.value.length > 0 && renderFiles(field.value) }
{ error && (
<Box fontSize="sm" mt={ 2 } color="error">
{ error.type === 'required' ? 'Field is required' : error.message }
</Box>
) }
</>
), [ accept, multiple, renderFiles ]);
), [ accept, error, multiple, renderFiles ]);
return (
<>
......
......@@ -36,7 +36,7 @@ type Inputs = {
const NAME_MAX_LENGTH = 255;
const CustomAbiForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
const { control, formState: { errors, isValid, isDirty }, handleSubmit, setError } = useForm<Inputs>({
const { control, formState: { errors, isDirty }, handleSubmit, setError } = useForm<Inputs>({
defaultValues: {
contract_address_hash: data?.contract_address_hash || '',
name: data?.name || '',
......@@ -174,7 +174,7 @@ const CustomAbiForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
<Button
size="lg"
type="submit"
disabled={ !isValid || !isDirty }
disabled={ !isDirty }
isLoading={ mutation.isLoading }
>
{ data ? 'Save' : 'Create custom ABI' }
......
import { Box, Heading, Flex, Link, Text, VStack, Skeleton } from '@chakra-ui/react';
import { Box, Heading, Flex, Text, VStack, Skeleton } from '@chakra-ui/react';
import { useQueryClient } from '@tanstack/react-query';
import { AnimatePresence } from 'framer-motion';
import React from 'react';
......@@ -12,6 +12,7 @@ import { nbsp } from 'lib/html-entities';
import link from 'lib/link/link';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
import LinkInternal from 'ui/shared/LinkInternal';
import LatestBlocksItem from './LatestBlocksItem';
import LatestBlocksItemSkeleton from './LatestBlocksItemSkeleton';
......@@ -32,7 +33,11 @@ const LatestBlocks = () => {
const newData = prevData ? [ ...prevData ] : [];
return [ payload.block, ...newData ].slice(0, blocksMaxCount);
if (newData.some((block => block.height === payload.block.height))) {
return newData;
}
return [ payload.block, ...newData ].sort((b1, b2) => b2.height - b1.height).slice(0, blocksMaxCount);
});
}, [ queryClient, blocksMaxCount ]);
......@@ -93,7 +98,7 @@ const LatestBlocks = () => {
</AnimatePresence>
</VStack>
<Flex justifyContent="center">
<Link fontSize="sm" href={ link('blocks') }>View all blocks</Link>
<LinkInternal fontSize="sm" href={ link('blocks') }>View all blocks</LinkInternal>
</Flex>
</>
);
......
......@@ -5,7 +5,6 @@ import {
GridItem,
HStack,
Icon,
Link,
Text,
} from '@chakra-ui/react';
import { motion } from 'framer-motion';
......@@ -18,6 +17,7 @@ import getBlockTotalReward from 'lib/block/getBlockTotalReward';
import link from 'lib/link/link';
import BlockTimestamp from 'ui/blocks/BlockTimestamp';
import AddressLink from 'ui/shared/address/AddressLink';
import LinkInternal from 'ui/shared/LinkInternal';
type Props = {
block: Block;
......@@ -43,13 +43,13 @@ const LatestBlocksItem = ({ block, h }: Props) => {
<Flex justifyContent="space-between" alignItems="center" mb={ 3 }>
<HStack spacing={ 2 }>
<Icon as={ blockIcon } boxSize="30px" color="link"/>
<Link
<LinkInternal
href={ link('block', { id: String(block.height) }) }
fontSize="xl"
fontWeight="500"
>
{ block.height }
</Link>
</LinkInternal>
</HStack>
<BlockTimestamp ts={ block.timestamp } isEnabled fontSize="sm"/>
</Flex>
......
import { Box, Heading, Flex, Link, Text, Skeleton } from '@chakra-ui/react';
import { Box, Heading, Flex, Text, Skeleton } from '@chakra-ui/react';
import React from 'react';
import useApiQuery from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile';
import useNewTxsSocket from 'lib/hooks/useNewTxsSocket';
import link from 'lib/link/link';
import LinkInternal from 'ui/shared/LinkInternal';
import SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice';
import LatestTxsItem from './LatestTxsItem';
......@@ -36,12 +37,12 @@ const LatestTransactions = () => {
const txsUrl = link('txs');
content = (
<>
<SocketNewItemsNotice borderBottomRadius={ 0 } url={ link('txs') } num={ num } alert={ socketAlert }/>
<SocketNewItemsNotice borderBottomRadius={ 0 } url={ txsUrl } num={ num } alert={ socketAlert }/>
<Box mb={{ base: 3, lg: 4 }}>
{ data.slice(0, txsCount).map((tx => <LatestTxsItem key={ tx.hash } tx={ tx }/>)) }
</Box>
<Flex justifyContent="center">
<Link fontSize="sm" href={ txsUrl }>View all transactions</Link>
<LinkInternal fontSize="sm" href={ txsUrl }>View all transactions</LinkInternal>
</Flex>
</>
);
......
......@@ -3,12 +3,13 @@ import React from 'react';
import type { TimeChartData } from 'ui/shared/chart/types';
import useClientRect from 'lib/hooks/useClientRect';
import ChartArea from 'ui/shared/chart/ChartArea';
import ChartLine from 'ui/shared/chart/ChartLine';
import ChartOverlay from 'ui/shared/chart/ChartOverlay';
import ChartTooltip from 'ui/shared/chart/ChartTooltip';
import useChartSize from 'ui/shared/chart/useChartSize';
import useTimeChartController from 'ui/shared/chart/useTimeChartController';
import calculateInnerSize from 'ui/shared/chart/utils/calculateInnerSize';
interface Props {
data: TimeChartData;
......@@ -18,11 +19,11 @@ interface Props {
const CHART_MARGIN = { bottom: 5, left: 10, right: 10, top: 0 };
const ChainIndicatorChart = ({ data }: Props) => {
const ref = React.useRef<SVGSVGElement>(null);
const overlayRef = React.useRef<SVGRectElement>(null);
const lineColor = useToken('colors', 'blue.500');
const { width, height, innerWidth, innerHeight } = useChartSize(ref.current, CHART_MARGIN);
const [ rect, ref ] = useClientRect<SVGSVGElement>();
const { innerWidth, innerHeight } = calculateInnerSize(rect, CHART_MARGIN);
const { xScale, yScale } = useTimeChartController({
data,
width: innerWidth,
......@@ -30,8 +31,8 @@ const ChainIndicatorChart = ({ data }: Props) => {
});
return (
<svg width={ width || '100%' } height={ height || '100%' } ref={ ref } cursor="pointer">
<g transform={ `translate(${ CHART_MARGIN?.left || 0 },${ CHART_MARGIN?.top || 0 })` } opacity={ width ? 1 : 0 }>
<svg width="100%" height="100%" ref={ ref } cursor="pointer">
<g transform={ `translate(${ CHART_MARGIN?.left || 0 },${ CHART_MARGIN?.top || 0 })` } opacity={ rect ? 1 : 0 }>
<ChartArea
data={ data[0].items }
xScale={ xScale }
......
......@@ -34,12 +34,8 @@ const Accounts = () => {
return (
<>
{ bar }
<Show below="lg">
<SkeletonList/>
</Show>
<Hide below="lg">
<SkeletonTable columns={ [ '64px', '30%', '20%', '20%', '15%', '15%' ] }/>
</Hide>
<SkeletonList display={{ base: 'block', lg: 'none' }}/>
<SkeletonTable display={{ base: 'none', lg: 'block' }} columns={ [ '64px', '30%', '20%', '20%', '15%', '15%' ] }/>
</>
);
}
......
......@@ -7,6 +7,7 @@ import type { RoutedTab } from 'ui/shared/RoutedTabs/types';
import iconSuccess from 'icons/status/success.svg';
import useApiQuery from 'lib/api/useApiQuery';
import { useAppContext } from 'lib/appContext';
import notEmpty from 'lib/notEmpty';
import AddressBlocksValidated from 'ui/address/AddressBlocksValidated';
import AddressCoinBalance from 'ui/address/AddressCoinBalance';
......@@ -20,6 +21,7 @@ import AddressTxs from 'ui/address/AddressTxs';
import ContractCode from 'ui/address/contract/ContractCode';
import ContractRead from 'ui/address/contract/ContractRead';
import ContractWrite from 'ui/address/contract/ContractWrite';
import AdBanner from 'ui/shared/ad/AdBanner';
import TextAd from 'ui/shared/ad/TextAd';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle';
......@@ -37,6 +39,10 @@ const TOKEN_TABS = Object.values(tokenTabsByType);
const AddressPageContent = () => {
const router = useRouter();
const appProps = useAppContext();
const hasGoBackLink = appProps.referrer && appProps.referrer.includes('/accounts');
const tabsScrollRef = React.useRef<HTMLDivElement>(null);
const addressQuery = useApiQuery('address', {
......@@ -113,6 +119,8 @@ const AddressPageContent = () => {
const tagsNode = tags.length > 0 ? <Flex columnGap={ 2 }>{ tags }</Flex> : null;
const content = addressQuery.isError ? null : <RoutedTabs tabs={ tabs } tabListProps={{ mt: 8 }}/>;
return (
<Page>
<TextAd mb={ 6 }/>
......@@ -121,13 +129,16 @@ const AddressPageContent = () => {
) : (
<PageTitle
text={ `${ addressQuery.data?.is_contract ? 'Contract' : 'Address' } details` }
additionals={ tagsNode }
additionalsRight={ tagsNode }
backLinkUrl={ hasGoBackLink ? appProps.referrer : undefined }
backLinkLabel="Back to top accounts list"
/>
) }
<AddressDetails addressQuery={ addressQuery } scrollRef={ tabsScrollRef }/>
<AdBanner mt={{ base: 6, lg: 8 }} justifyContent="center"/>
{ /* should stay before tabs to scroll up whith pagination */ }
<Box ref={ tabsScrollRef }></Box>
{ addressQuery.isLoading ? <SkeletonTabs/> : <RoutedTabs tabs={ tabs } tabListProps={{ mt: 8 }}/> }
{ addressQuery.isLoading ? <SkeletonTabs/> : content }
</Page>
);
};
......
......@@ -6,7 +6,6 @@ import type { RoutedTab } from 'ui/shared/RoutedTabs/types';
import { useAppContext } from 'lib/appContext';
import useIsMobile from 'lib/hooks/useIsMobile';
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import isBrowser from 'lib/isBrowser';
import BlockDetails from 'ui/block/BlockDetails';
import TextAd from 'ui/shared/ad/TextAd';
import Page from 'ui/shared/Page/Page';
......@@ -24,7 +23,6 @@ const TAB_LIST_PROPS = {
const BlockPageContent = () => {
const router = useRouter();
const isMobile = useIsMobile();
const isInBrowser = isBrowser();
const appProps = useAppContext();
const blockTxsQuery = useQueryWithPages({
......@@ -46,15 +44,14 @@ const BlockPageContent = () => {
const hasPagination = !isMobile && router.query.tab === 'txs' && blockTxsQuery.isPaginationVisible;
const referrer = isInBrowser ? window.document.referrer : appProps.referrer;
const hasGoBackLink = referrer && referrer.includes('/blocks');
const hasGoBackLink = appProps.referrer && appProps.referrer.includes('/blocks');
return (
<Page>
<TextAd mb={ 6 }/>
<PageTitle
text={ `Block #${ router.query.id }` }
backLinkUrl={ hasGoBackLink ? referrer : undefined }
backLinkUrl={ hasGoBackLink ? appProps.referrer : undefined }
backLinkLabel="Back to blocks list"
/>
<RoutedTabs
......
......@@ -4,6 +4,7 @@ import React from 'react';
import { useAppContext } from 'lib/appContext';
import isBrowser from 'lib/isBrowser';
import link from 'lib/link/link';
import ContractVerificationForm from 'ui/contractVerification/ContractVerificationForm';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
......@@ -20,13 +21,22 @@ const ContractVerification = () => {
const router = useRouter();
const hash = router.query.id?.toString();
const method = router.query.id?.toString();
React.useEffect(() => {
if (method && hash) {
router.replace(link('address_contract_verification', { id: hash }), undefined, { scroll: false, shallow: true });
}
// onMount only
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ ]);
return (
<Page>
<PageTitle
text="New smart contract verification"
backLinkUrl={ hasGoBackLink ? referrer : undefined }
backLinkLabel="Back to address"
backLinkLabel="Back to contract"
/>
{ hash && (
<Address>
......
......@@ -32,10 +32,10 @@ const SearchResultsPageContent = () => {
if (isLoading) {
return (
<Box>
<Show below="lg">
<Show below="lg" ssr={ false }>
<SkeletonList/>
</Show>
<Hide below="lg">
<Hide below="lg" ssr={ false }>
<SkeletonTable columns={ [ '50%', '50%', '150px' ] }/>
</Hide>
</Box>
......
......@@ -5,6 +5,7 @@ import { token as contract } from 'mocks/address/address';
import { tokenInfo, tokenCounters } from 'mocks/tokens/tokenInfo';
import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import insertAdPlaceholder from 'playwright/utils/insertAdPlaceholder';
import Token from './Token';
......@@ -22,6 +23,11 @@ const hooksConfig = {
// FIXME: idk why mobile test doesn't work (it's ok locally)
// test('base view +@mobile +@dark-mode', async({ mount, page }) => {
test('base view +@dark-mode', async({ mount, page }) => {
await page.route('https://request-global.czilladx.com/serve/native.php?z=19260bf627546ab7242', (route) => route.fulfill({
status: 200,
body: '',
}));
await page.route(TOKEN_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(tokenInfo),
......@@ -46,5 +52,7 @@ test('base view +@dark-mode', async({ mount, page }) => {
{ hooksConfig },
);
await insertAdPlaceholder(page);
await expect(component.locator('main')).toHaveScreenshot();
});
import { Skeleton, Box } from '@chakra-ui/react';
import { Skeleton, Box, Flex, SkeletonCircle } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';
import React, { useEffect } from 'react';
import type { RoutedTab } from 'ui/shared/RoutedTabs/types';
import useApiQuery from 'lib/api/useApiQuery';
import { useAppContext } from 'lib/appContext';
import useIsMobile from 'lib/hooks/useIsMobile';
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import AdBanner from 'ui/shared/ad/AdBanner';
import TextAd from 'ui/shared/ad/TextAd';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle';
import type { Props as PaginationProps } from 'ui/shared/Pagination';
import Pagination from 'ui/shared/Pagination';
import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs';
import TokenLogo from 'ui/shared/TokenLogo';
import TokenContractInfo from 'ui/token/TokenContractInfo';
import TokenDetails from 'ui/token/TokenDetails';
import TokenHolders from 'ui/token/TokenHolders/TokenHolders';
......@@ -23,6 +27,10 @@ const TokenPageContent = () => {
const router = useRouter();
const isMobile = useIsMobile();
const appProps = useAppContext();
const hasGoBackLink = appProps.referrer && appProps.referrer.includes('/tokens');
const scrollRef = React.useRef<HTMLDivElement>(null);
const tokenQuery = useApiQuery('token', {
......@@ -30,6 +38,20 @@ const TokenPageContent = () => {
queryOptions: { enabled: Boolean(router.query.hash) },
});
useEffect(() => {
if (tokenQuery.data) {
const tokenName = `${ tokenQuery.data.name } (${ tokenQuery.data.symbol })`;
const title = document.getElementsByTagName('title')[0];
if (title) {
title.textContent = title.textContent?.replace(tokenQuery.data.address, tokenName) || title.textContent;
}
const description = document.getElementsByName('description')[0] as HTMLMetaElement;
if (description) {
description.content = description.content.replace(tokenQuery.data.address, tokenName) || description.content;
}
}
}, [ tokenQuery.data ]);
const transfersQuery = useQueryWithPages({
resourceName: 'token_transfers',
pathParams: { hash: router.query.hash?.toString() },
......@@ -68,12 +90,27 @@ const TokenPageContent = () => {
return (
<Page>
{ tokenQuery.isLoading ?
<Skeleton w="500px" h={ 10 } mb={ 6 }/> :
<PageTitle text={ `${ tokenQuery.data?.name } (${ tokenQuery.data?.symbol }) token` }/> }
{ tokenQuery.isLoading ? (
<Flex alignItems="center" mb={ 6 }>
<SkeletonCircle w={ 6 } h={ 6 } mr={ 3 }/>
<Skeleton w="500px" h={ 10 }/>
</Flex>
) : (
<>
<TextAd mb={ 6 }/>
<PageTitle
text={ `${ tokenQuery.data?.name } (${ tokenQuery.data?.symbol }) token` }
backLinkUrl={ hasGoBackLink ? appProps.referrer : undefined }
backLinkLabel="Back to tokens list"
additionalsLeft={ (
<TokenLogo hash={ tokenQuery.data?.address } name={ tokenQuery.data?.name } boxSize={ 6 }/>
) }
/>
</>
) }
<TokenContractInfo tokenQuery={ tokenQuery }/>
<TokenDetails tokenQuery={ tokenQuery }/>
<AdBanner mt={{ base: 6, lg: 8 }} justifyContent="center"/>
{ /* should stay before tabs to scroll up whith pagination */ }
<Box ref={ scrollRef }></Box>
......
......@@ -6,10 +6,9 @@ import type { RoutedTab } from 'ui/shared/RoutedTabs/types';
import useApiQuery from 'lib/api/useApiQuery';
import { useAppContext } from 'lib/appContext';
import isBrowser from 'lib/isBrowser';
import networkExplorers from 'lib/networks/networkExplorers';
import TextAd from 'ui/shared/ad/TextAd';
import ExternalLink from 'ui/shared/ExternalLink';
import LinkExternal from 'ui/shared/LinkExternal';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle';
import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs';
......@@ -33,11 +32,8 @@ const TABS: Array<RoutedTab> = [
const TransactionPageContent = () => {
const router = useRouter();
const appProps = useAppContext();
const isInBrowser = isBrowser();
const referrer = isInBrowser ? window.document.referrer : appProps.referrer;
const hasGoBackLink = referrer && referrer.includes('/txs');
const hasGoBackLink = appProps.referrer && appProps.referrer.includes('/txs');
const { data } = useApiQuery('tx', {
pathParams: { id: router.query.id?.toString() },
......@@ -48,7 +44,7 @@ const TransactionPageContent = () => {
.filter((explorer) => explorer.paths.tx)
.map((explorer) => {
const url = new URL(explorer.paths.tx + '/' + router.query.id, explorer.baseUrl);
return <ExternalLink key={ explorer.baseUrl } title={ `Open in ${ explorer.title }` } href={ url.toString() }/>;
return <LinkExternal key={ explorer.baseUrl } title={ `Open in ${ explorer.title }` } href={ url.toString() }/>;
});
const additionals = (
......@@ -73,8 +69,8 @@ const TransactionPageContent = () => {
<TextAd mb={ 6 }/>
<PageTitle
text="Transaction details"
additionals={ additionals }
backLinkUrl={ hasGoBackLink ? referrer : undefined }
additionalsRight={ additionals }
backLinkUrl={ hasGoBackLink ? appProps.referrer : undefined }
backLinkLabel="Back to transactions list"
/>
<RoutedTabs tabs={ TABS }/>
......
......@@ -34,7 +34,7 @@ type Inputs = {
const AddressForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
const apiFetch = useApiFetch();
const [ pending, setPending ] = useState(false);
const { control, handleSubmit, formState: { errors, isValid, isDirty }, setError } = useForm<Inputs>({
const { control, handleSubmit, formState: { errors, isDirty }, setError } = useForm<Inputs>({
mode: 'onTouched',
defaultValues: {
address: data?.address_hash || '',
......@@ -124,7 +124,7 @@ const AddressForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
<Button
size="lg"
type="submit"
disabled={ !isValid || !isDirty }
disabled={ !isDirty }
isLoading={ pending }
>
{ data ? 'Save changes' : 'Add tag' }
......
......@@ -35,7 +35,7 @@ const TransactionForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) =>
const [ pending, setPending ] = useState(false);
const formBackgroundColor = useColorModeValue('white', 'gray.900');
const { control, handleSubmit, formState: { errors, isValid, isDirty }, setError } = useForm<Inputs>({
const { control, handleSubmit, formState: { errors, isDirty }, setError } = useForm<Inputs>({
mode: 'onTouched',
defaultValues: {
transaction: data?.transaction_hash || '',
......@@ -123,7 +123,7 @@ const TransactionForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) =>
<Button
size="lg"
type="submit"
disabled={ !isValid || !isDirty }
disabled={ !isDirty }
isLoading={ pending }
>
{ data ? 'Save changes' : 'Add tag' }
......
......@@ -61,7 +61,7 @@ const PublicTagsForm = ({ changeToDataScreen, data }: Props) => {
const apiFetch = useApiFetch();
const inputSize = { base: 'md', lg: 'lg' };
const { control, handleSubmit, formState: { errors, isValid, isDirty }, setError } = useForm<Inputs>({
const { control, handleSubmit, formState: { errors, isDirty }, setError } = useForm<Inputs>({
defaultValues: {
fullName: data?.full_name || '',
email: data?.email || '',
......@@ -236,7 +236,7 @@ const PublicTagsForm = ({ changeToDataScreen, data }: Props) => {
<Button
size="lg"
type="submit"
disabled={ !isValid || !isDirty }
disabled={ !isDirty }
isLoading={ mutation.isLoading }
>
Send request
......
import { Text, Link, Flex, Icon, Box, chakra } from '@chakra-ui/react';
import { Text, Flex, Icon, Box, chakra } from '@chakra-ui/react';
import React from 'react';
import type { SearchResultItem } from 'types/api/search';
......@@ -11,6 +11,7 @@ import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import LinkInternal from 'ui/shared/LinkInternal';
import ListItemMobile from 'ui/shared/ListItemMobile';
import TokenLogo from 'ui/shared/TokenLogo';
......@@ -29,9 +30,9 @@ const SearchResultListItem = ({ data, searchTerm }: Props) => {
return (
<Flex alignItems="flex-start">
<TokenLogo boxSize={ 6 } hash={ data.address } name={ data.name } flexShrink={ 0 }/>
<Link ml={ 2 } href={ link('token_index', { hash: data.address }) } fontWeight={ 700 } wordBreak="break-all">
<LinkInternal ml={ 2 } href={ link('token_index', { hash: data.address }) } fontWeight={ 700 } wordBreak="break-all">
<chakra.span dangerouslySetInnerHTML={{ __html: highlightText(name, searchTerm) }}/>
</Link>
</LinkInternal>
</Flex>
);
}
......@@ -54,9 +55,9 @@ const SearchResultListItem = ({ data, searchTerm }: Props) => {
return (
<Flex alignItems="center">
<Icon as={ blockIcon } boxSize={ 6 } mr={ 2 } color="gray.500"/>
<Link fontWeight={ 700 } href={ link('block', { id: String(data.block_number) }) }>
<LinkInternal fontWeight={ 700 } href={ link('block', { id: String(data.block_number) }) }>
<Box as={ shouldHighlightHash ? 'span' : 'mark' }>{ data.block_number }</Box>
</Link>
</LinkInternal>
</Flex>
);
}
......
import { Tr, Td, Text, Link, Flex, Icon, Box } from '@chakra-ui/react';
import { Tr, Td, Text, Flex, Icon, Box } from '@chakra-ui/react';
import React from 'react';
import type { SearchResultItem } from 'types/api/search';
......@@ -11,6 +11,7 @@ import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import LinkInternal from 'ui/shared/LinkInternal';
import TokenLogo from 'ui/shared/TokenLogo';
interface Props {
......@@ -29,9 +30,9 @@ const SearchResultTableItem = ({ data, searchTerm }: Props) => {
<Td fontSize="sm">
<Flex alignItems="center">
<TokenLogo boxSize={ 6 } hash={ data.address } name={ data.name } flexShrink={ 0 }/>
<Link ml={ 2 } href={ link('token_index', { hash: data.address }) } fontWeight={ 700 } wordBreak="break-all">
<LinkInternal ml={ 2 } href={ link('token_index', { hash: data.address }) } fontWeight={ 700 } wordBreak="break-all">
<span dangerouslySetInnerHTML={{ __html: highlightText(name, searchTerm) }}/>
</Link>
</LinkInternal>
</Flex>
</Td>
<Td fontSize="sm" verticalAlign="middle">
......@@ -52,11 +53,11 @@ const SearchResultTableItem = ({ data, searchTerm }: Props) => {
<Td fontSize="sm">
<Flex alignItems="center" overflow="hidden">
<AddressIcon address={{ hash: data.address, is_contract: data.type === 'contract', implementation_name: null }} mr={ 2 } flexShrink={ 0 }/>
<Link href={ link('address_index', { id: data.address }) } fontWeight={ 700 } overflow="hidden" whiteSpace="nowrap">
<LinkInternal href={ link('address_index', { id: data.address }) } fontWeight={ 700 } overflow="hidden" whiteSpace="nowrap">
<Box as={ shouldHighlightHash ? 'mark' : 'span' } display="block">
<HashStringShortenDynamic hash={ data.address }/>
</Box>
</Link>
</LinkInternal>
</Flex>
</Td>
<Td fontSize="sm" verticalAlign="middle">
......@@ -86,9 +87,9 @@ const SearchResultTableItem = ({ data, searchTerm }: Props) => {
<Td fontSize="sm">
<Flex alignItems="center">
<Icon as={ blockIcon } boxSize={ 6 } mr={ 2 } color="gray.500"/>
<Link fontWeight={ 700 } href={ link('block', { id: String(data.block_number) }) }>
<LinkInternal fontWeight={ 700 } href={ link('block', { id: String(data.block_number) }) }>
<Box as={ shouldHighlightHash ? 'span' : 'mark' }>{ data.block_number }</Box>
</Link>
</LinkInternal>
</Flex>
</Td>
<Td fontSize="sm" verticalAlign="middle">
......
......@@ -9,7 +9,7 @@ interface Props {
className?: string;
}
const ExternalLink = ({ href, title, className }: Props) => {
const LinkExternal = ({ href, title, className }: Props) => {
return (
<Link className={ className } fontSize="sm" display="inline-flex" alignItems="center" target="_blank" href={ href }>
{ title }
......@@ -18,4 +18,4 @@ const ExternalLink = ({ href, title, className }: Props) => {
);
};
export default React.memo(chakra(ExternalLink));
export default React.memo(chakra(LinkExternal));
import type { LinkProps } from '@chakra-ui/react';
import { Link } from '@chakra-ui/react';
import NextLink from 'next/link';
import type { LegacyRef } from 'react';
import React from 'react';
// NOTE! use this component only for links to pages that are completely implemented in new UI
const LinkInternal = (props: LinkProps, ref: LegacyRef<HTMLAnchorElement>) => {
if (!props.href) {
return <Link { ...props } ref={ ref }/>;
}
return (
<NextLink href={ props.href } passHref target={ props.target }>
<Link { ...props } ref={ ref }/>
</NextLink>
);
};
export default React.memo(React.forwardRef(LinkInternal));
// import { Icon } from '@chakra-ui/react';
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
// import plusIcon from 'icons/plus.svg';
import * as textAdMock from 'mocks/ad/textAd';
import TestApp from 'playwright/TestApp';
......@@ -32,6 +34,10 @@ test('default view +@mobile', async({ mount }) => {
});
test('with text ad, back link and addons +@mobile +@dark-mode', async({ mount }) => {
// https://github.com/microsoft/playwright/issues/15620
// not possible to pass component as a prop in tests
// const left = <Icon as={ plusIcon }/>;
const component = await mount(
<TestApp>
<PageTitle
......@@ -39,7 +45,8 @@ test('with text ad, back link and addons +@mobile +@dark-mode', async({ mount })
withTextAd
backLinkLabel="Back"
backLinkUrl="back"
additionals="Privet"
// additionalsLeft={ left }
additionalsRight="Privet"
/>
</TestApp>,
);
......@@ -55,7 +62,7 @@ test('long title with text ad, back link and addons +@mobile', async({ mount })
withTextAd
backLinkLabel="Back"
backLinkUrl="back"
additionals="Privet, kak dela?"
additionalsRight="Privet, kak dela?"
/>
</TestApp>,
);
......
import { Heading, Flex, Tooltip, Link, Icon, chakra } from '@chakra-ui/react';
import { Heading, Flex, Grid, Tooltip, Icon, chakra } from '@chakra-ui/react';
import React from 'react';
import eastArrowIcon from 'icons/arrows/east.svg';
import TextAd from 'ui/shared/ad/TextAd';
import LinkInternal from 'ui/shared/LinkInternal';
type Props = {
text: string;
additionals?: React.ReactNode;
additionalsLeft?: React.ReactNode;
additionalsRight?: React.ReactNode;
withTextAd?: boolean;
className?: string;
backLinkLabel?: string;
backLinkUrl?: string;
}
const PageTitle = ({ text, additionals, withTextAd, backLinkUrl, backLinkLabel, className }: Props) => {
const PageTitle = ({ text, additionalsLeft, additionalsRight, withTextAd, backLinkUrl, backLinkLabel, className }: Props) => {
const title = (
<Heading
as="h1"
size="lg"
flex="none"
width={ backLinkUrl ? 'calc(100% - 36px)' : '100%' }
>
{ text }
</Heading>
......@@ -36,22 +37,25 @@ const PageTitle = ({ text, additionals, withTextAd, backLinkUrl, backLinkLabel,
className={ className }
>
<Flex flexWrap="wrap" columnGap={ 3 } alignItems="center" width={ withTextAd ? 'unset' : '100%' }>
<Flex
flexWrap="nowrap"
alignItems="start"
<Grid
templateColumns={ [ backLinkUrl && 'auto', additionalsLeft && 'auto', '1fr' ].filter(Boolean).join(' ') }
columnGap={ 3 }
overflow="hidden"
>
{ backLinkUrl && (
<Tooltip label={ backLinkLabel }>
<Link display="inline-flex" href={ backLinkUrl } py={ 2 }>
<Icon as={ eastArrowIcon } boxSize={ 6 } transform="rotate(180deg)"/>
</Link>
<LinkInternal display="inline-flex" href={ backLinkUrl } h="40px">
<Icon as={ eastArrowIcon } boxSize={ 6 } transform="rotate(180deg)" margin="auto"/>
</LinkInternal>
</Tooltip>
) }
{ additionalsLeft !== undefined && (
<Flex h="40px" alignItems="center">
{ additionalsLeft }
</Flex>
) }
{ title }
</Flex>
{ additionals }
</Grid>
{ additionalsRight }
</Flex>
{ withTextAd && <TextAd flexShrink={ 100 }/> }
</Flex>
......
......@@ -48,14 +48,14 @@ const TokenTransferListItem = ({
const addressWidth = `calc((100% - ${ baseAddress ? '50px' : '0px' }) / 2)`;
return (
<ListItemMobile rowGap={ 3 } isAnimated>
<Flex w="100%" flexWrap="wrap" rowGap={ 1 } position="relative">
<TokenSnippet hash={ token.address } w="auto" maxW="calc(100% - 140px)" name={ token.name || 'Unnamed token' }/>
<Tag flexShrink={ 0 } ml={ 2 } mr={ 2 }>{ token.type }</Tag>
<Tag colorScheme="orange">{ getTokenTransferTypeText(type) }</Tag>
<Flex w="100%" justifyContent="space-between">
<Flex flexWrap="wrap" rowGap={ 1 } mr={ showTxInfo && txHash ? 2 : 0 }>
<TokenSnippet hash={ token.address } w="auto" maxW="calc(100% - 140px)" name={ token.name || 'Unnamed token' }/>
<Tag flexShrink={ 0 } ml={ 2 } mr={ 2 }>{ token.type }</Tag>
<Tag colorScheme="orange">{ getTokenTransferTypeText(type) }</Tag>
</Flex>
{ showTxInfo && txHash && (
<Flex position="absolute" top={ 0 } right={ 0 }>
<TxAdditionalInfo hash={ txHash } isMobile/>
</Flex>
<TxAdditionalInfo hash={ txHash } isMobile/>
) }
</Flex>
{ 'token_id' in total && <TokenTransferNft hash={ token.address } id={ total.token_id }/> }
......
......@@ -22,7 +22,7 @@ var AdButler = AdButler || {}; AdButler.ads = AdButler.ads || [];
const AdbutlerBanner = ({ className }: { className?: string }) => {
return (
<Flex className={ className } id="adBanner">
<Flex className={ className } id="adBanner" h={{ base: '100px', lg: '90px' }}>
<div id="ad-banner"></div>
<Script id="ad-butler-1">{ scriptText1 }</Script>
<Script id="ad-butler-2">{ scriptText2 }</Script>
......
......@@ -32,7 +32,7 @@ const CoinzillaBanner = ({ className }: { className?: string }) => {
}, [ isInBrowser ]);
return (
<Flex className={ className } id="adBanner">
<Flex className={ className } id="adBanner" h={{ base: '100px', lg: '90px' }}>
<Script src="https://coinzillatag.com/lib/display.js"/>
<div className="coinzilla" data-zone="C-26660bf627543e46851"></div>
</Flex>
......
import { Link, chakra, shouldForwardProp, Tooltip, Box } from '@chakra-ui/react';
import { chakra, shouldForwardProp, Tooltip, Box } from '@chakra-ui/react';
import type { HTMLAttributeAnchorTarget } from 'react';
import React from 'react';
......@@ -6,6 +6,7 @@ import useIsMobile from 'lib/hooks/useIsMobile';
import link from 'lib/link/link';
import HashStringShorten from 'ui/shared/HashStringShorten';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import LinkInternal from 'ui/shared/LinkInternal';
import TruncatedTextTooltip from '../TruncatedTextTooltip';
......@@ -93,7 +94,7 @@ const AddressLink = (props: Props) => {
}
return (
<Link
<LinkInternal
className={ className }
href={ url }
target={ target }
......@@ -101,7 +102,7 @@ const AddressLink = (props: Props) => {
whiteSpace="nowrap"
>
{ content }
</Link>
</LinkInternal>
);
};
......
import {
Box,
Center,
chakra,
Flex,
Grid,
Icon,
......@@ -36,13 +38,13 @@ type Props = {
title: string;
description?: string;
isLoading: boolean;
chartHeight?: string;
className?: string;
isError: boolean;
}
const DOWNLOAD_IMAGE_SCALE = 5;
const ChartWidget = ({ items, title, description, isLoading, chartHeight, isError }: Props) => {
const ChartWidget = ({ items, title, description, isLoading, className, isError }: Props) => {
const ref = useRef<HTMLDivElement>(null);
const [ isFullscreen, setIsFullscreen ] = useState(false);
const [ isZoomResetInitial, setIsZoomResetInitial ] = React.useState(true);
......@@ -111,10 +113,53 @@ const ChartWidget = ({ items, title, description, isLoading, chartHeight, isErro
return <ChartWidgetSkeleton hasDescription={ Boolean(description) }/>;
}
const hasItems = items && items.length > 2;
const content = (() => {
if (isError) {
return (
<Flex
alignItems="center"
justifyContent="center"
flexGrow={ 1 }
py={ 4 }
>
<Text
variant="secondary"
fontSize="sm"
textAlign="center"
>
{ `The data didn${ apos }t load. Please, ` }
<Link href={ window.document.location.href }>try to reload the page.</Link>
</Text>
</Flex>
);
}
if (!hasItems) {
return (
<Center flexGrow={ 1 }>
<Text variant="secondary" fontSize="sm">No data</Text>
</Center>
);
}
return (
<Box h="100%" maxW="100%">
<ChartWidgetGraph
items={ items }
onZoom={ handleZoom }
isZoomResetInitial={ isZoomResetInitial }
title={ title }
/>
</Box>
);
})();
return (
<>
<Box
height={ chartHeight }
height="100%"
display="flex"
flexDirection="column"
ref={ ref }
......@@ -122,6 +167,7 @@ const ChartWidget = ({ items, title, description, isLoading, chartHeight, isErro
borderRadius="md"
border="1px"
borderColor={ borderColor }
className={ className }
>
<Grid
gridTemplateColumns="auto auto 36px"
......@@ -167,7 +213,7 @@ const ChartWidget = ({ items, title, description, isLoading, chartHeight, isErro
/>
</Tooltip>
{ !isError && (
{ hasItems && (
<Menu>
<MenuButton
gridColumn={ 3 }
......@@ -216,36 +262,10 @@ const ChartWidget = ({ items, title, description, isLoading, chartHeight, isErro
) }
</Grid>
{ items ? (
<Box h={ chartHeight || 'auto' } maxW="100%">
<ChartWidgetGraph
margin={{ bottom: 20 }}
items={ items }
onZoom={ handleZoom }
isZoomResetInitial={ isZoomResetInitial }
title={ title }
/>
</Box>
) : (
<Flex
alignItems="center"
justifyContent="center"
flexGrow={ 1 }
py={ 4 }
>
<Text
variant="secondary"
fontSize="sm"
textAlign="center"
>
{ `The data didn${ apos }t load. Please, ` }
<Link href={ window.document.location.href }>try to reload the page.</Link>
</Text>
</Flex>
) }
{ content }
</Box>
{ items && (
{ hasItems && (
<FullscreenChartModal
isOpen={ isFullscreen }
items={ items }
......@@ -258,4 +278,4 @@ const ChartWidget = ({ items, title, description, isLoading, chartHeight, isErro
);
};
export default React.memo(ChartWidget);
export default React.memo(chakra(ChartWidget));
......@@ -5,6 +5,7 @@ import React, { useEffect, useMemo } from 'react';
import type { ChartMargin, TimeChartItem } from 'ui/shared/chart/types';
import dayjs from 'lib/date/dayjs';
import useClientRect from 'lib/hooks/useClientRect';
import useIsMobile from 'lib/hooks/useIsMobile';
import ChartArea from 'ui/shared/chart/ChartArea';
import ChartAxis from 'ui/shared/chart/ChartAxis';
......@@ -13,8 +14,8 @@ import ChartLine from 'ui/shared/chart/ChartLine';
import ChartOverlay from 'ui/shared/chart/ChartOverlay';
import ChartSelectionX from 'ui/shared/chart/ChartSelectionX';
import ChartTooltip from 'ui/shared/chart/ChartTooltip';
import useChartSize from 'ui/shared/chart/useChartSize';
import useTimeChartController from 'ui/shared/chart/useTimeChartController';
import calculateInnerSize from 'ui/shared/chart/utils/calculateInnerSize';
interface Props {
isEnlarged?: boolean;
......@@ -22,7 +23,7 @@ interface Props {
items: Array<TimeChartItem>;
onZoom: () => void;
isZoomResetInitial: boolean;
margin: ChartMargin;
margin?: ChartMargin;
}
const MAX_SHOW_ITEMS = 100;
......@@ -31,10 +32,12 @@ const DEFAULT_CHART_MARGIN = { bottom: 20, left: 40, right: 20, top: 10 };
const ChartWidgetGraph = ({ isEnlarged, items, onZoom, isZoomResetInitial, title, margin }: Props) => {
const isMobile = useIsMobile();
const color = useToken('colors', 'blue.200');
const ref = React.useRef<SVGSVGElement>(null);
const overlayRef = React.useRef<SVGRectElement>(null);
const [ rect, ref ] = useClientRect<SVGSVGElement>();
const chartMargin = { ...DEFAULT_CHART_MARGIN, ...margin };
const { width, height, innerWidth, innerHeight } = useChartSize(ref.current, chartMargin);
const { innerWidth, innerHeight } = calculateInnerSize(rect, chartMargin);
const chartId = `chart-${ title.split(' ').join('') }-${ isEnlarged ? 'fullscreen' : 'small' }`;
const [ range, setRange ] = React.useState<[ Date, Date ]>([ items[0].date, items[items.length - 1].date ]);
......@@ -70,7 +73,7 @@ const ChartWidgetGraph = ({ isEnlarged, items, onZoom, isZoomResetInitial, title
}, [ isZoomResetInitial, items ]);
return (
<svg width="100%" height={ height || '100%' } ref={ ref } cursor="pointer" id={ chartId } opacity={ width ? 1 : 0 }>
<svg width="100%" height="100%" ref={ ref } cursor="pointer" id={ chartId } opacity={ rect ? 1 : 0 }>
<g transform={ `translate(${ chartMargin?.left || 0 },${ chartMargin?.top || 0 })` }>
<ChartGridLine
......@@ -149,6 +152,6 @@ function groupChartItemsByWeekNumber(items: Array<TimeChartItem>): Array<TimeCha
value: d3.sum(group, (d) => d.value),
dateLabel: `${ d3.timeFormat('%e %b %Y')(group[0].date) }${ d3.timeFormat('%e %b %Y')(group[group.length - 1].date) }`,
}),
(t) => dayjs(t.date).week(),
(t) => `${ dayjs(t.date).week() } / ${ dayjs(t.date).year() }`,
).map(([ , v ]) => v);
}
......@@ -3,10 +3,9 @@ import React from 'react';
interface Props {
hasDescription: boolean;
chartHeight?: string;
}
const ChartWidgetSkeleton = ({ hasDescription, chartHeight }: Props) => {
const ChartWidgetSkeleton = ({ hasDescription }: Props) => {
return (
<Box
height="235px"
......@@ -15,7 +14,7 @@ const ChartWidgetSkeleton = ({ hasDescription, chartHeight }: Props) => {
<Skeleton w="75%" h="24px"/>
{ hasDescription && <Skeleton w="50%" h="18px" mt={ 1 }/> }
<Skeleton w="100%" h={ chartHeight || '150px' } mt={ 5 }/>
<Skeleton w="100%" h="150px" mt={ 5 }/>
</Box>
);
};
......
import _debounce from 'lodash/debounce';
import React from 'react';
import type { ChartMargin, ChartOffset } from 'ui/shared/chart/types';
export default function useChartSize(svgEl: SVGSVGElement | null, margin?: ChartMargin, offsets?: ChartOffset) {
const [ rect, setRect ] = React.useState<{ width: number; height: number}>({ width: 0, height: 0 });
const calculateRect = React.useCallback(() => {
const rect = svgEl?.getBoundingClientRect();
return { width: rect?.width || 0, height: rect?.height || 0 };
}, [ svgEl ]);
React.useEffect(() => {
setRect(calculateRect());
}, [ calculateRect ]);
React.useEffect(() => {
const content = window.document.querySelector('main');
if (!content) {
return;
}
let timeoutId: number;
const resizeHandler = _debounce(() => {
setRect({ width: 0, height: 0 });
timeoutId = window.setTimeout(() => {
setRect(calculateRect());
}, 100);
}, 100);
const resizeObserver = new ResizeObserver(resizeHandler);
resizeObserver.observe(content);
resizeObserver.observe(window.document.body);
return function cleanup() {
resizeObserver.unobserve(content);
resizeObserver.unobserve(window.document.body);
window.clearTimeout(timeoutId);
};
}, [ calculateRect ]);
return React.useMemo(() => {
return {
width: Math.max(rect.width - (offsets?.x || 0), 0),
height: Math.max(rect.height - (offsets?.y || 0), 0),
innerWidth: Math.max(rect.width - (offsets?.x || 0) - (margin?.left || 0) - (margin?.right || 0), 0),
innerHeight: Math.max(rect.height - (offsets?.y || 0) - (margin?.bottom || 0) - (margin?.top || 0), 0),
};
}, [ margin?.bottom, margin?.left, margin?.right, margin?.top, offsets?.x, offsets?.y, rect.height, rect.width ]);
}
import type { ChartMargin, ChartOffset } from 'ui/shared/chart/types';
export default function calculateInnerSize(rect: DOMRect | null, margin?: ChartMargin, offsets?: ChartOffset) {
if (!rect) {
return { innerWidth: 0, innerHeight: 0 };
}
return {
innerWidth: Math.max(rect.width - (offsets?.x || 0) - (margin?.left || 0) - (margin?.right || 0), 0),
innerHeight: Math.max(rect.height - (offsets?.y || 0) - (margin?.bottom || 0) - (margin?.top || 0), 0),
};
}
import type { As } from '@chakra-ui/react';
import { Box, Button, Circle, Icon, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
......@@ -9,9 +10,10 @@ interface Props {
isActive?: boolean;
appliedFiltersNum?: number;
onClick: () => void;
as?: As;
}
const FilterButton = ({ isActive, appliedFiltersNum, onClick }: Props, ref: React.ForwardedRef<HTMLButtonElement>) => {
const FilterButton = ({ isActive, appliedFiltersNum, onClick, as }: Props, ref: React.ForwardedRef<HTMLButtonElement>) => {
const badgeColor = useColorModeValue('white', 'black');
const badgeBgColor = useColorModeValue('blue.700', 'gray.50');
......@@ -27,6 +29,7 @@ const FilterButton = ({ isActive, appliedFiltersNum, onClick }: Props, ref: Reac
isActive={ isActive }
px={ 1.5 }
flexShrink={ 0 }
as={ as }
>
{ FilterIcon }
<Box display={{ base: 'none', lg: 'block' }}>Filter</Box>
......
......@@ -35,6 +35,7 @@ const FilterInput = ({ onChange, className, size = 'sm', placeholder, initialVal
<InputGroup
size={ size }
className={ className }
minW="250px"
>
<InputLeftElement
pointerEvents="none"
......
......@@ -58,7 +58,7 @@ const LogItem = ({ address, index, topics, data, decoded, type, tx_hash: txHash
<AddressLink
hash={ hasTxInfo ? txHash : address.hash }
alias={ hasTxInfo ? undefined : address.name }
type={ type }
type={ type === 'address' ? 'transaction' : 'address' }
/>
</Address>
{ /* api doesn't have find topic feature yet */ }
......
import { Box, Flex, Skeleton, SkeletonCircle } from '@chakra-ui/react';
import { Box, Flex, Skeleton, SkeletonCircle, chakra } from '@chakra-ui/react';
import React from 'react';
const SkeletonList = () => {
const SkeletonList = ({ className }: {className?: string}) => {
return (
<Box>
<Box className={ className }>
{ Array.from(Array(2)).map((item, index) => (
<Flex
key={ index }
......@@ -35,4 +35,4 @@ const SkeletonList = () => {
);
};
export default SkeletonList;
export default chakra(SkeletonList);
import { HStack, Skeleton } from '@chakra-ui/react';
import { Box, HStack, Skeleton, chakra } from '@chakra-ui/react';
import React from 'react';
interface Props {
columns: Array<string>;
className?: string;
}
const SkeletonTable = ({ columns }: Props) => {
const SkeletonTable = ({ columns, className }: Props) => {
return (
<div>
<Box className={ className }>
<Skeleton height={ 10 } width="100%" borderBottomLeftRadius="none" borderBottomRightRadius="none"/>
{ Array.from(Array(3)).map((item, index) => (
<HStack key={ index } spacing={ 6 } marginTop={ 8 }>
......@@ -22,8 +23,8 @@ const SkeletonTable = ({ columns }: Props) => {
)) }
</HStack>
)) }
</div>
</Box>
);
};
export default React.memo(SkeletonTable);
export default React.memo(chakra(SkeletonTable));
import React, { useEffect } from 'react';
import React, { useEffect, useMemo } from 'react';
import type { StatsIntervalIds } from 'types/client/stats';
......@@ -33,9 +33,9 @@ const ChartWidgetContainer = ({ id, title, description, interval, onLoadingError
},
});
const items = data?.chart?.map((item) => {
const items = useMemo(() => data?.chart?.map((item) => {
return { date: new Date(item.date), value: Number(item.value) };
});
}), [ data ]);
useEffect(() => {
if (isError) {
......@@ -45,7 +45,6 @@ const ChartWidgetContainer = ({ id, title, description, interval, onLoadingError
return (
<ChartWidget
chartHeight="100%"
isError={ isError }
items={ items }
title={ title }
......
......@@ -41,8 +41,8 @@ const StatsFilters = ({
<Grid
gap={ 2 }
templateAreas={{
base: `"input input"
"section interval"`,
base: `"section interval"
"input input"`,
lg: `"section interval input"`,
}}
gridTemplateColumns={{ base: 'repeat(2, minmax(0, 1fr))', lg: 'auto auto 1fr' }}
......
......@@ -69,11 +69,6 @@ const TokenDetails = ({ tokenQuery }: Props) => {
);
}
// we show error in parent component, this is only for TS
if (tokenQuery.isError) {
return null;
}
const {
exchange_rate: exchangeRate,
total_supply: totalSupply,
......
......@@ -66,11 +66,11 @@ const TokenTransfer = ({ transfersQuery, token }: Props) => {
if (isLoading) {
return (
<>
<Hide below="lg">
<Hide below="lg" ssr={ false }>
<SkeletonTable columns={ [ '45%', '15%', '36px', '15%', '25%' ] }
/>
</Hide>
<Show below="lg">
<Show below="lg" ssr={ false }>
<SkeletonList/>
</Show>
</>
......@@ -88,7 +88,7 @@ const TokenTransfer = ({ transfersQuery, token }: Props) => {
const items = data.items.reduce(flattenTotal, []);
return (
<>
<Hide below="lg">
<Hide below="lg" ssr={ false }>
<TokenTransferTable
data={ items }
top={ 80 }
......@@ -100,7 +100,7 @@ const TokenTransfer = ({ transfersQuery, token }: Props) => {
socketInfoNum={ newItemsCount }
/>
</Hide>
<Show below="lg">
<Show below="lg" ssr={ false }>
{ pagination.page === 1 && (
<SocketNewItemsNotice
url={ window.location.href }
......
......@@ -69,19 +69,15 @@ const Tokens = () => {
const bar = (
<>
<Show below="lg">
<HStack spacing={ 3 } mb={ 6 }>
<HStack spacing={ 3 } mb={ 6 } display={{ base: 'flex', lg: 'none' }}>
{ typeFilter }
{ filterInput }
</HStack>
<ActionBar mt={ -6 }>
<HStack spacing={ 3 } display={{ base: 'none', lg: 'flex' }}>
{ typeFilter }
{ filterInput }
</HStack>
</Show>
<ActionBar mt={ -6 }>
<Hide below="lg">
<HStack spacing={ 3 }>
{ typeFilter }
{ filterInput }
</HStack>
</Hide>
{ isPaginationVisible && <Pagination ml="auto" { ...pagination }/> }
</ActionBar>
</>
......@@ -91,10 +87,8 @@ const Tokens = () => {
return (
<>
{ bar }
<Show below="lg"><SkeletonList/></Show>
<Hide below="lg">
<SkeletonTable columns={ [ '25px', '33%', '33%', '33%', '110px' ] }/>
</Hide>
<SkeletonList display={{ base: 'block', lg: 'none' }}/>
<SkeletonTable display={{ base: 'none', lg: 'block' }} columns={ [ '25px', '33%', '33%', '33%', '110px' ] }/>
</>
);
}
......
......@@ -99,7 +99,7 @@ const TxDetails = () => {
const executionFailedBadge = toAddress.is_contract && Boolean(data.status) && data.result !== 'success' ? (
<Tooltip label="Error occurred during contract execution">
<chakra.span display="inline-flex" ml={ 2 } mr={ 1 }>
<Icon as={ errorIcon } boxSize={ 4 } color="red.500" cursor="pointer"/>
<Icon as={ errorIcon } boxSize={ 4 } color="error" cursor="pointer"/>
</chakra.span>
</Tooltip>
) : null;
......
......@@ -101,8 +101,8 @@ const TxInternals = () => {
if (isLoading || txInfo.isLoading) {
return (
<>
<Show below="lg"><SkeletonList/></Show>
<Hide below="lg"><SkeletonTable columns={ [ '28%', '20%', '24px', '20%', '16%', '16%' ] }/></Hide>
<Show below="lg" ssr={ false }><SkeletonList/></Show>
<Hide below="lg" ssr={ false }><SkeletonTable columns={ [ '28%', '20%', '24px', '20%', '16%', '16%' ] }/></Hide>
</>
);
}
......
......@@ -83,10 +83,10 @@ const TxTokenTransfer = () => {
const items = tokenTransferQuery.data.items.reduce(flattenTotal, []);
return (
<>
<Hide below="lg">
<Hide below="lg" ssr={ false }>
<TokenTransferTable data={ items } top={ 80 }/>
</Hide>
<Show below="lg">
<Show below="lg" ssr={ false }>
<TokenTransferList data={ items }/>
</Show>
</>
......
......@@ -15,13 +15,14 @@ const TxDetailsSkeleton = () => {
);
return (
<Grid columnGap={ 8 } rowGap={{ base: 5, lg: 7 }} templateColumns={{ base: '1fr', lg: '210px 1fr' }} maxW="1000px">
<Grid columnGap={ 8 } rowGap={{ base: 5, lg: 7 }} templateColumns={{ base: '1fr', lg: '210px 1fr' }} maxW="1000px" pt={{ base: 1, lg: 2 }}>
<DetailsSkeletonRow/>
<DetailsSkeletonRow w="20%"/>
<DetailsSkeletonRow w="50%"/>
<DetailsSkeletonRow/>
<DetailsSkeletonRow w="70%"/>
<DetailsSkeletonRow w="70%"/>
<GridItem h={{ base: '82px', lg: '38px' }}/>
{ sectionGap }
<DetailsSkeletonRow w="40%"/>
<DetailsSkeletonRow w="40%"/>
......
import { Icon, Link, GridItem, Show, Flex } from '@chakra-ui/react';
import NextLink from 'next/link';
import { Icon, GridItem, Show, Flex } from '@chakra-ui/react';
import React from 'react';
import type { TokenTransfer } from 'types/api/tokenTransfer';
......@@ -7,6 +6,7 @@ import type { TokenTransfer } from 'types/api/tokenTransfer';
import tokenIcon from 'icons/token.svg';
import link from 'lib/link/link';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import LinkInternal from 'ui/shared/LinkInternal';
import { flattenTotal } from 'ui/shared/TokenTransfer/helpers';
import TxDetailsTokenTransfer from './TxDetailsTokenTransfer';
......@@ -61,12 +61,12 @@ const TxDetailsTokenTransfers = ({ data, txHash }: Props) => {
}) }
{ showViewAllLink && (
<>
<Show above="lg"><GridItem></GridItem></Show>
<Show above="lg" ssr={ false }><GridItem></GridItem></Show>
<GridItem fontSize="sm" alignItems="center" display="inline-flex" pl={{ base: '28px', lg: 0 }}>
<Icon as={ tokenIcon } boxSize={ 6 }/>
<NextLink href={ viewAllUrl } passHref>
<Link>View all</Link>
</NextLink>
<LinkInternal href={ viewAllUrl }>
View all
</LinkInternal>
</GridItem>
</>
) }
......
import { Box, Heading, Text, Flex, Link } from '@chakra-ui/react';
import { Box, Heading, Text, Flex } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import React from 'react';
......@@ -8,6 +8,7 @@ import appConfig from 'configs/app/config';
import getValueWithUnit from 'lib/getValueWithUnit';
import link from 'lib/link/link';
import CurrencyValue from 'ui/shared/CurrencyValue';
import LinkInternal from 'ui/shared/LinkInternal';
import TextSeparator from 'ui/shared/TextSeparator';
import Utilization from 'ui/shared/Utilization/Utilization';
......@@ -89,7 +90,7 @@ const TxAdditionalInfoContent = ({ tx }: { tx: Transaction }) => {
<Text fontWeight="600" as="span">{ tx.position }</Text>
</Box>
</Box>
<Link fontSize="sm" href={ link('tx', { id: tx.hash }) }>More details</Link>
<LinkInternal fontSize="sm" href={ link('tx', { id: tx.hash }) }>More details</LinkInternal>
</>
);
};
......
......@@ -3,7 +3,6 @@ import {
Box,
Flex,
Icon,
Link,
Text,
} from '@chakra-ui/react';
import React from 'react';
......@@ -20,6 +19,7 @@ import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import InOutTag from 'ui/shared/InOutTag';
import LinkInternal from 'ui/shared/LinkInternal';
import ListItemMobile from 'ui/shared/ListItemMobile';
import TxStatus from 'ui/shared/TxStatus';
import TxAdditionalInfo from 'ui/txs/TxAdditionalInfo';
......@@ -88,7 +88,7 @@ const TxsListItem = ({ tx, showBlockInfo, currentAddress, enableTimeIncrement }:
{ showBlockInfo && tx.block !== null && (
<Box mt={ 2 }>
<Text as="span">Block </Text>
<Link href={ link('block', { id: tx.block.toString() }) }>{ tx.block }</Link>
<LinkInternal href={ link('block', { id: tx.block.toString() }) }>{ tx.block }</LinkInternal>
</Box>
) }
<Flex alignItems="center" height={ 6 } mt={ 6 }>
......
......@@ -3,7 +3,6 @@ import {
Tr,
Td,
Tag,
Link,
Icon,
VStack,
Text,
......@@ -23,6 +22,7 @@ import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import CurrencyValue from 'ui/shared/CurrencyValue';
import InOutTag from 'ui/shared/InOutTag';
import LinkInternal from 'ui/shared/LinkInternal';
import TruncatedTextTooltip from 'ui/shared/TruncatedTextTooltip';
import TxStatus from 'ui/shared/TxStatus';
import TxAdditionalInfo from 'ui/txs/TxAdditionalInfo';
......@@ -99,7 +99,7 @@ const TxsTableItem = ({ tx, showBlockInfo, currentAddress, enableTimeIncrement }
</Td>
{ showBlockInfo && (
<Td>
{ tx.block && <Link href={ link('block', { id: tx.block.toString() }) }>{ tx.block }</Link> }
{ tx.block && <LinkInternal href={ link('block', { id: tx.block.toString() }) }>{ tx.block }</LinkInternal> }
</Td>
) }
<Show above="xl" ssr={ false }>
......
......@@ -73,7 +73,7 @@ const AddressForm: React.FC<Props> = ({ data, onSuccess, setAlertVisible, isAdd
notificationsDefault = data.notification_settings;
}
const { control, handleSubmit, formState: { errors, isValid, isDirty }, setError } = useForm<Inputs>({
const { control, handleSubmit, formState: { errors, isDirty }, setError } = useForm<Inputs>({
defaultValues: {
address: data?.address_hash || '',
tag: data?.name || '',
......@@ -192,7 +192,7 @@ const AddressForm: React.FC<Props> = ({ data, onSuccess, setAlertVisible, isAdd
size="lg"
type="submit"
isLoading={ pending }
disabled={ !isValid || !isDirty }
disabled={ !isDirty }
>
{ !isAdd ? 'Save changes' : 'Add address' }
</Button>
......
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