Merge branch 'main' of github.com:blockscout/frontend into contract-verification-form
@@ -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
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
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 ];
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
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
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
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;
|
||||
@@ -0,0 +1,10 @@
|
||||
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',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,10 +1,29 @@
|
||||
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);
|
||||
});
|
||||
|
||||
@@ -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 },
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
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.isError && !is404Error) {
|
||||
return <DataFetchAlert/>;
|
||||
}
|
||||
|
||||
if (addressQuery.isLoading) {
|
||||
return <AddressDetailsSkeleton/>;
|
||||
}
|
||||
|
||||
if (addressQuery.isError) {
|
||||
return <DataFetchAlert/>;
|
||||
}
|
||||
|
||||
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 }>
|
||||
|
||||
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 38 KiB |
@@ -1,4 +1,4 @@
|
||||
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%">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
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 }/>
|
||||
)) }
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
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%">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
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>
|
||||
) }
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
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 = (() => {
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
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)">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
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' }
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
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 ]);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
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>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
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>
|
||||
|
||||
@@ -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' }
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
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>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
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
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
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 }/>
|
||||
|
||||
|
Before Width: | Height: | Size: 289 KiB After Width: | Height: | Size: 289 KiB |
|
Before Width: | Height: | Size: 322 KiB After Width: | Height: | Size: 322 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 39 KiB |
@@ -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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
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));
|
||||
@@ -0,0 +1,20 @@
|
||||
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));
|
||||
@@ -1,6 +1,8 @@
|
||||
// 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>,
|
||||
);
|
||||
|
||||
@@ -1,25 +1,26 @@
|
||||
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 }/> }
|
||||
|
||||
|
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 98 KiB |
@@ -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>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
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 ]);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
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),
|
||||
};
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
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 */ }
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
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);
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
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));
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
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' ] }/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
|
||||
|
Before Width: | Height: | Size: 134 KiB After Width: | Height: | Size: 134 KiB |
|
Before Width: | Height: | Size: 132 KiB After Width: | Height: | Size: 132 KiB |
|
Before Width: | Height: | Size: 95 KiB After Width: | Height: | Size: 95 KiB |
|
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 114 KiB After Width: | Height: | Size: 114 KiB |
|
Before Width: | Height: | Size: 146 KiB After Width: | Height: | Size: 146 KiB |
@@ -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%"/>
|
||||
|
||||