Commit c0834be6 authored by tom's avatar tom

skeleton for address details

parent 16467af8
import type { NextPage } from 'next';
import dynamic from 'next/dynamic';
import Head from 'next/head';
import type { RoutedQuery } from 'nextjs-routes';
import React from 'react';
import getSeo from 'lib/next/address/getSeo';
import Address from 'ui/pages/Address';
import Page from 'ui/shared/Page/Page';
const Address = dynamic(() => import('ui/pages/Address'), { ssr: false });
const AddressPage: NextPage<RoutedQuery<'/address/[hash]'>> = ({ hash }: RoutedQuery<'/address/[hash]'>) => {
const { title, description } = getSeo({ hash });
......@@ -15,7 +18,9 @@ const AddressPage: NextPage<RoutedQuery<'/address/[hash]'>> = ({ hash }: RoutedQ
<title>{ title }</title>
<meta name="description" content={ description }/>
</Head>
<Address/>
<Page>
<Address/>
</Page>
</>
);
};
......
import type { Address } from 'types/api/address';
import type { Address, AddressCounters } from 'types/api/address';
import type { AddressesItem } from 'types/api/addresses';
import { ADDRESS_HASH } from './addressParams';
......@@ -6,7 +6,7 @@ import { TOKEN_INFO_ERC_20 } from './token';
export const ADDRESS_INFO: Address = {
block_number_balance_updated_at: 8774377,
coin_balance: '0',
coin_balance: '810941268802273085757',
creation_tx_hash: null,
creator_address_hash: null,
exchange_rate: null,
......@@ -34,6 +34,13 @@ export const ADDRESS_INFO: Address = {
watchlist_address_id: null,
};
export const ADDRESS_COUNTERS: AddressCounters = {
gas_usage_count: '8028907522',
token_transfers_count: '420',
transactions_count: '119020',
validations_count: '0',
};
export const TOP_ADDRESS: AddressesItem = {
coin_balance: '11886682377162664596540805',
tx_count: '1835',
......
import { Box, Text, Icon, Grid } from '@chakra-ui/react';
import { Box, Text, Grid, Skeleton } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import { route } from 'nextjs-routes';
......@@ -10,9 +10,11 @@ import blockIcon from 'icons/block.svg';
import type { ResourceError } from 'lib/api/resources';
import useApiQuery from 'lib/api/useApiQuery';
import getQueryParamString from 'lib/router/getQueryParamString';
import { ADDRESS_COUNTERS } from 'stubs/address';
import AddressCounterItem from 'ui/address/details/AddressCounterItem';
import AddressLink from 'ui/shared/address/AddressLink';
import AddressHeadingInfo from 'ui/shared/AddressHeadingInfo';
import Icon from 'ui/shared/chakra/Icon';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import DetailsSponsoredItem from 'ui/shared/DetailsSponsoredItem';
......@@ -20,7 +22,6 @@ import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import LinkInternal from 'ui/shared/LinkInternal';
import AddressBalance from './details/AddressBalance';
import AddressDetailsSkeleton from './details/AddressDetailsSkeleton';
import AddressNameInfo from './details/AddressNameInfo';
import TokenSelect from './tokenSelect/TokenSelect';
......@@ -38,6 +39,7 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
pathParams: { hash: addressHash },
queryOptions: {
enabled: Boolean(addressHash) && Boolean(addressQuery.data),
placeholderData: ADDRESS_COUNTERS,
},
});
......@@ -77,26 +79,27 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
return <DataFetchAlert/>;
}
if (addressQuery.isLoading) {
return <AddressDetailsSkeleton/>;
}
const data = addressQuery.isError ? errorData : addressQuery.data;
if (!data) {
return null;
}
return (
<Box>
<AddressHeadingInfo address={ data } token={ data.token } isLinkDisabled/>
<AddressHeadingInfo address={ data } token={ data.token } isLoading={ addressQuery.isPlaceholderData } isLinkDisabled/>
<Grid
mt={ 8 }
columnGap={ 8 }
rowGap={{ base: 1, lg: 3 }}
templateColumns={{ base: 'minmax(0, 1fr)', lg: 'auto minmax(0, 1fr)' }} overflow="hidden"
>
<AddressNameInfo data={ data }/>
<AddressNameInfo data={ data } isLoading={ addressQuery.isPlaceholderData }/>
{ data.is_contract && data.creation_tx_hash && data.creator_address_hash && (
<DetailsInfoItem
title="Creator"
hint="Transaction and address of creation"
isLoading={ addressQuery.isPlaceholderData }
>
<AddressLink type="address" hash={ data.creator_address_hash } truncation="constant"/>
<Text whiteSpace="pre"> at txn </Text>
......@@ -119,7 +122,7 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
) }
</DetailsInfoItem>
) }
<AddressBalance data={ data }/>
<AddressBalance data={ data } isLoading={ addressQuery.isPlaceholderData }/>
{ data.has_tokens && (
<DetailsInfoItem
title="Tokens"
......@@ -133,36 +136,68 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
<DetailsInfoItem
title="Transactions"
hint="Number of transactions related to this address"
isLoading={ addressQuery.isPlaceholderData || countersQuery.isPlaceholderData }
>
{ addressQuery.data ?
<AddressCounterItem prop="transactions_count" query={ countersQuery } address={ data.hash } onClick={ handleCounterItemClick }/> :
{ addressQuery.data ? (
<AddressCounterItem
prop="transactions_count"
query={ countersQuery }
address={ data.hash }
onClick={ handleCounterItemClick }
isAddressQueryLoading={ addressQuery.isPlaceholderData }
/>
) :
0 }
</DetailsInfoItem>
{ data.has_token_transfers && (
<DetailsInfoItem
title="Transfers"
hint="Number of transfers to/from this address"
isLoading={ addressQuery.isPlaceholderData || countersQuery.isPlaceholderData }
>
{ addressQuery.data ?
<AddressCounterItem prop="token_transfers_count" query={ countersQuery } address={ data.hash } onClick={ handleCounterItemClick }/> :
{ addressQuery.data ? (
<AddressCounterItem
prop="token_transfers_count"
query={ countersQuery }
address={ data.hash }
onClick={ handleCounterItemClick }
isAddressQueryLoading={ addressQuery.isPlaceholderData }
/>
) :
0 }
</DetailsInfoItem>
) }
<DetailsInfoItem
title="Gas used"
hint="Gas used by the address"
isLoading={ addressQuery.isPlaceholderData || countersQuery.isPlaceholderData }
>
{ addressQuery.data ?
<AddressCounterItem prop="gas_usage_count" query={ countersQuery } address={ data.hash } onClick={ handleCounterItemClick }/> :
{ addressQuery.data ? (
<AddressCounterItem
prop="gas_usage_count"
query={ countersQuery }
address={ data.hash }
onClick={ handleCounterItemClick }
isAddressQueryLoading={ addressQuery.isPlaceholderData }
/>
) :
0 }
</DetailsInfoItem>
{ data.has_validated_blocks && (
<DetailsInfoItem
title="Blocks validated"
hint="Number of blocks validated by this validator"
isLoading={ addressQuery.isPlaceholderData || countersQuery.isPlaceholderData }
>
{ addressQuery.data ?
<AddressCounterItem prop="validations_count" query={ countersQuery } address={ data.hash } onClick={ handleCounterItemClick }/> :
{ addressQuery.data ? (
<AddressCounterItem
prop="validations_count"
query={ countersQuery }
address={ data.hash }
onClick={ handleCounterItemClick }
isAddressQueryLoading={ addressQuery.isPlaceholderData }
/>
) :
0 }
</DetailsInfoItem>
) }
......@@ -172,18 +207,21 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
hint="Block number in which the address was updated"
alignSelf="center"
py={{ base: '2px', lg: 1 }}
isLoading={ addressQuery.isPlaceholderData }
>
<LinkInternal
href={ route({ pathname: '/block/[height]', query: { height: String(data.block_number_balance_updated_at) } }) }
display="flex"
alignItems="center"
>
<Icon as={ blockIcon } boxSize={ 6 } mr={ 2 }/>
{ data.block_number_balance_updated_at }
<Box mr={ 2 }>
<Icon as={ blockIcon } boxSize={ 6 } isLoading={ addressQuery.isPlaceholderData }/>
</Box>
<Skeleton isLoaded={ !addressQuery.isPlaceholderData }>{ data.block_number_balance_updated_at }</Skeleton>
</LinkInternal>
</DetailsInfoItem>
) }
<DetailsSponsoredItem/>
<DetailsSponsoredItem isLoading={ addressQuery.isPlaceholderData }/>
</Grid>
</Box>
);
......
......@@ -14,9 +14,10 @@ import TokenLogo from 'ui/shared/TokenLogo';
interface Props {
data: Pick<Address, 'block_number_balance_updated_at' | 'coin_balance' | 'hash' | 'exchange_rate'>;
isLoading: boolean;
}
const AddressBalance = ({ data }: Props) => {
const AddressBalance = ({ data, isLoading }: Props) => {
const [ lastBlockNumber, setLastBlockNumber ] = React.useState<number>(data.block_number_balance_updated_at || 0);
const queryClient = useQueryClient();
......@@ -75,12 +76,14 @@ const AddressBalance = ({ data }: Props) => {
hint={ `Address balance in ${ appConfig.network.currency.symbol }. Doesn't include ERC20, ERC721 and ERC1155 tokens` }
flexWrap="nowrap"
alignItems="flex-start"
isLoading={ isLoading }
>
<TokenLogo
data={ tokenData }
boxSize={ 5 }
mr={ 2 }
fontSize="sm"
isLoading={ isLoading }
/>
<CurrencyValue
value={ data.coin_balance || '0' }
......@@ -90,6 +93,7 @@ const AddressBalance = ({ data }: Props) => {
accuracyUsd={ 2 }
accuracy={ 8 }
flexWrap="wrap"
isLoading={ isLoading }
/>
</DetailsInfoItem>
);
......
......@@ -13,6 +13,7 @@ interface Props {
query: UseQueryResult<AddressCounters>;
address: string;
onClick: () => void;
isAddressQueryLoading: boolean;
}
const PROP_TO_TAB = {
......@@ -21,8 +22,8 @@ const PROP_TO_TAB = {
validations_count: 'blocks_validated',
};
const AddressCounterItem = ({ prop, query, address, onClick }: Props) => {
if (query.isLoading) {
const AddressCounterItem = ({ prop, query, address, onClick, isAddressQueryLoading }: Props) => {
if (query.isPlaceholderData || isAddressQueryLoading) {
return <Skeleton h={ 5 } w="80px" borderRadius="full"/>;
}
......
import { Box, Flex, Grid, Skeleton, SkeletonCircle } from '@chakra-ui/react';
import React from 'react';
import DetailsSkeletonRow from 'ui/shared/skeletons/DetailsSkeletonRow';
const AddressDetailsSkeleton = () => {
return (
<Box>
<Flex align="center">
<SkeletonCircle boxSize={ 6 } flexShrink={ 0 }/>
<Skeleton h={ 6 } w={{ base: '100px', lg: '420px' }} ml={ 2 } borderRadius="full"/>
<Skeleton h={ 8 } w="36px" ml={ 3 } flexShrink={ 0 }/>
<Skeleton h={ 8 } w="36px" ml={ 3 } flexShrink={ 0 }/>
</Flex>
<Flex align="center" columnGap={ 4 } mt={ 8 }>
<Skeleton h={ 6 } w="200px" borderRadius="full"/>
<Skeleton h={ 6 } w="80px" borderRadius="full"/>
</Flex>
<Grid columnGap={ 8 } rowGap={{ base: 5, lg: 7 }} templateColumns={{ base: '1fr', lg: '150px 1fr' }} maxW="1000px" mt={ 8 }>
<DetailsSkeletonRow w="30%"/>
<DetailsSkeletonRow w="30%"/>
<DetailsSkeletonRow w="10%"/>
<DetailsSkeletonRow w="10%"/>
<DetailsSkeletonRow w="20%"/>
<DetailsSkeletonRow w="20%"/>
</Grid>
</Box>
);
};
export default AddressDetailsSkeleton;
import { Skeleton } from '@chakra-ui/react';
import { route } from 'nextjs-routes';
import React from 'react';
......@@ -9,19 +10,23 @@ import LinkInternal from 'ui/shared/LinkInternal';
interface Props {
data: Pick<Address, 'name' | 'token' | 'is_contract'>;
isLoading: boolean;
}
const AddressNameInfo = ({ data }: Props) => {
const AddressNameInfo = ({ data, isLoading }: Props) => {
if (data.token) {
const symbol = data.token.symbol ? ` (${ trimTokenSymbol(data.token.symbol) })` : '';
return (
<DetailsInfoItem
title="Token name"
hint="Token name and symbol"
isLoading={ isLoading }
>
<LinkInternal href={ route({ pathname: '/token/[hash]', query: { hash: data.token.address } }) }>
{ data.token.name || 'Unnamed token' }{ symbol }
</LinkInternal>
<Skeleton isLoaded={ !isLoading }>
<LinkInternal href={ route({ pathname: '/token/[hash]', query: { hash: data.token.address } }) }>
{ data.token.name || 'Unnamed token' }{ symbol }
</LinkInternal>
</Skeleton>
</DetailsInfoItem>
);
}
......@@ -31,8 +36,11 @@ const AddressNameInfo = ({ data }: Props) => {
<DetailsInfoItem
title="Contract name"
hint="The name found in the source code of the Contract"
isLoading={ isLoading }
>
{ data.name }
<Skeleton isLoaded={ !isLoading }>
{ data.name }
</Skeleton>
</DetailsInfoItem>
);
}
......@@ -42,8 +50,11 @@ const AddressNameInfo = ({ data }: Props) => {
<DetailsInfoItem
title="Validator name"
hint="The name of the validator"
isLoading={ isLoading }
>
{ data.name }
<Skeleton isLoaded={ !isLoading }>
{ data.name }
</Skeleton>
</DetailsInfoItem>
);
}
......
......@@ -67,7 +67,12 @@ const TokenSelect = ({ onClick }: Props) => {
});
if (isLoading) {
return <Skeleton h={ 8 } w="160px"/>;
return (
<Flex columnGap={ 3 }>
<Skeleton h={ 8 } w="150px" borderRadius="base"/>
<Skeleton h={ 8 } w={ 9 } borderRadius="base"/>
</Flex>
);
}
const hasTokens = _sumBy(Object.values(data), ({ items }) => items.length) > 0;
......
......@@ -12,6 +12,7 @@ import { useAppContext } from 'lib/appContext';
import useContractTabs from 'lib/hooks/useContractTabs';
import useIsMobile from 'lib/hooks/useIsMobile';
import getQueryParamString from 'lib/router/getQueryParamString';
import { ADDRESS_INFO } from 'stubs/address';
import AddressBlocksValidated from 'ui/address/AddressBlocksValidated';
import AddressCoinBalance from 'ui/address/AddressCoinBalance';
import AddressContract from 'ui/address/AddressContract';
......@@ -25,7 +26,6 @@ import AddressWithdrawals from 'ui/address/AddressWithdrawals';
import TextAd from 'ui/shared/ad/TextAd';
import EntityTags from 'ui/shared/EntityTags';
import NetworkExplorers from 'ui/shared/NetworkExplorers';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle';
import SkeletonTabs from 'ui/shared/skeletons/SkeletonTabs';
import RoutedTabs from 'ui/shared/Tabs/RoutedTabs';
......@@ -48,7 +48,10 @@ const AddressPageContent = () => {
const addressQuery = useApiQuery('address', {
pathParams: { hash },
queryOptions: { enabled: Boolean(hash) },
queryOptions: {
enabled: Boolean(hash),
placeholderData: ADDRESS_INFO,
},
});
const contractTabs = useContractTabs(addressQuery.data);
......@@ -120,23 +123,20 @@ const AddressPageContent = () => {
}, [ appProps.referrer ]);
return (
<Page>
{ addressQuery.isLoading ? <Skeleton h={{ base: 12, lg: 6 }} mb={ 6 } w="100%" maxW="680px"/> : <TextAd mb={ 6 }/> }
{ addressQuery.isLoading ? (
<Skeleton h={ 10 } w="260px" mb={ 6 }/>
) : (
<PageTitle
title={ `${ addressQuery.data?.is_contract ? 'Contract' : 'Address' } details` }
backLink={ backLink }
contentAfter={ tags }
/>
) }
<>
{ addressQuery.isPlaceholderData ? <Skeleton h={{ base: 12, lg: 6 }} mb={ 6 } w="100%" maxW="680px"/> : <TextAd mb={ 6 }/> }
<PageTitle
title={ `${ addressQuery.data?.is_contract ? 'Contract' : 'Address' } details` }
backLink={ backLink }
contentAfter={ tags }
isLoading={ addressQuery.isPlaceholderData }
/>
<AddressDetails addressQuery={ addressQuery } scrollRef={ tabsScrollRef }/>
{ /* should stay before tabs to scroll up with pagination */ }
<Box ref={ tabsScrollRef }></Box>
{ addressQuery.isLoading ? <SkeletonTabs/> : content }
{ !addressQuery.isLoading && !addressQuery.isError && <Box h={{ base: 0, lg: '40vh' }}/> }
</Page>
{ addressQuery.isPlaceholderData ? <SkeletonTabs tabs={ tabs }/> : content }
{ !addressQuery.isPlaceholderData && !addressQuery.isError && <Box h={{ base: 0, lg: '40vh' }}/> }
</>
);
};
......
......@@ -64,7 +64,7 @@ const PageTitle = ({ title, contentAfter, withTextAd, backLink, className, isLoa
{ beforeTitle }
<Skeleton
isLoaded={ !isLoading }
display="inline"
display={ isLoading ? 'inline-block' : 'inline' }
verticalAlign={ isLoading ? 'super' : undefined }
>
<Heading
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment