Commit cc425ddb authored by tom goriunov's avatar tom goriunov Committed by GitHub

Merge pull request #767 from blockscout/auto-skeletons

smart skeletons
parents 6a60eac5 453144c5
...@@ -199,7 +199,7 @@ module.exports = { ...@@ -199,7 +199,7 @@ module.exports = {
groups: [ groups: [
'module', 'module',
'/types/', '/types/',
[ '/^configs/', '/^data/', '/^deploy/', '/^icons/', '/^lib/', '/^mocks/', '/^pages/', '/^playwright/', '/^theme/', '/^ui/' ], [ '/^configs/', '/^data/', '/^deploy/', '/^icons/', '/^lib/', '/^mocks/', '/^pages/', '/^playwright/', '/^stubs/', '/^theme/', '/^ui/' ],
[ 'parent', 'sibling', 'index' ], [ 'parent', 'sibling', 'index' ],
], ],
alphabetize: { order: 'asc', ignoreCase: true }, alphabetize: { order: 'asc', ignoreCase: true },
......
<svg fill="currentColor" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 21 20"> <svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15.645 19.375h-10A1.875 1.875 0 0 1 3.77 17.5V5.625a.625.625 0 0 1 1.25 0V17.5a.625.625 0 0 0 .625.625h10a.625.625 0 0 0 .625-.625V5.625a.625.625 0 0 1 1.25 0V17.5a1.875 1.875 0 0 1-1.875 1.875Zm2.5-15h-15a.625.625 0 0 1 0-1.25h15a.625.625 0 1 1 0 1.25Z"/> <path d="M15 19.375H5A1.875 1.875 0 0 1 3.125 17.5V5.625a.625.625 0 0 1 1.25 0V17.5a.625.625 0 0 0 .625.625h10a.624.624 0 0 0 .625-.625V5.625a.625.625 0 1 1 1.25 0V17.5A1.875 1.875 0 0 1 15 19.375Zm2.5-15h-15a.625.625 0 0 1 0-1.25h15a.625.625 0 1 1 0 1.25Z" fill="currentColor"/>
<path d="M13.145 4.375a.625.625 0 0 1-.625-.625V1.875H8.77V3.75a.625.625 0 0 1-1.25 0v-2.5a.625.625 0 0 1 .625-.625h5a.625.625 0 0 1 .625.625v2.5a.625.625 0 0 1-.625.625Zm-2.5 11.875a.625.625 0 0 1-.625-.625v-8.75a.625.625 0 0 1 1.25 0v8.75a.625.625 0 0 1-.625.625ZM13.77 15a.625.625 0 0 1-.625-.625v-6.25a.625.625 0 0 1 1.25 0v6.25a.625.625 0 0 1-.625.625Zm-6.25 0a.625.625 0 0 1-.625-.625v-6.25a.625.625 0 0 1 1.25 0v6.25A.625.625 0 0 1 7.52 15Z"/> <path d="M12.5 4.375a.625.625 0 0 1-.625-.625V1.875h-3.75V3.75a.625.625 0 0 1-1.25 0v-2.5A.625.625 0 0 1 7.5.625h5a.625.625 0 0 1 .625.625v2.5a.625.625 0 0 1-.625.625ZM10 16.25a.625.625 0 0 1-.625-.625v-8.75a.625.625 0 0 1 1.25 0v8.75a.624.624 0 0 1-.625.625ZM13.125 15a.624.624 0 0 1-.625-.625v-6.25a.625.625 0 1 1 1.25 0v6.25a.624.624 0 0 1-.625.625Zm-6.25 0a.625.625 0 0 1-.625-.625v-6.25a.625.625 0 0 1 1.25 0v6.25a.625.625 0 0 1-.625.625Z" fill="currentColor"/>
</svg> </svg>
<svg fill="currentColor" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"> <svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19.433 3.6 16.399.569A1.926 1.926 0 0 0 15.03 0c-.518 0-1.005.202-1.37.568L.961 13.264a.779.779 0 0 0-.221.446l-.734 5.406a.78.78 0 0 0 .877.877l5.406-.734a.78.78 0 0 0 .446-.221L19.433 6.342c.366-.366.567-.853.567-1.37 0-.518-.201-1.005-.567-1.371ZM5.82 17.75l-4.131.561.56-4.131 8.997-8.997 3.571 3.57L5.82 17.75ZM18.33 5.24l-2.41 2.412-3.57-3.57 2.411-2.413a.379.379 0 0 1 .538 0l3.033 3.033a.379.379 0 0 1 0 .538Z"/> <path d="M19.432 3.6 16.4.569A1.925 1.925 0 0 0 15.03 0c-.518 0-1.005.202-1.371.568L.962 13.264a.779.779 0 0 0-.221.446l-.734 5.406a.779.779 0 0 0 .877.877l5.406-.734a.778.778 0 0 0 .446-.221L19.432 6.342c.366-.366.568-.853.568-1.37 0-.518-.202-1.005-.568-1.371ZM5.82 17.75l-4.132.561.561-4.131 8.997-8.997 3.57 3.57L5.82 17.75ZM18.33 5.24l-2.41 2.412-3.571-3.57L14.76 1.67a.379.379 0 0 1 .537 0l3.034 3.032a.378.378 0 0 1 0 .538Z" fill="currentColor"/>
</svg> </svg>
import type { NextPage } from 'next'; import type { NextPage } from 'next';
import dynamic from 'next/dynamic';
import Head from 'next/head'; import Head from 'next/head';
import React from 'react'; import React from 'react';
import type { PageParams } from 'lib/next/token/types'; import type { PageParams } from 'lib/next/token/types';
import getSeo from 'lib/next/token/getSeo'; import getSeo from 'lib/next/token/getSeo';
import Token from 'ui/pages/Token'; import Page from 'ui/shared/Page/Page';
const Token = dynamic(() => import('ui/pages/Token'), { ssr: false });
const TokenPage: NextPage<PageParams> = ({ hash }: PageParams) => { const TokenPage: NextPage<PageParams> = ({ hash }: PageParams) => {
const { title, description } = getSeo({ hash }); const { title, description } = getSeo({ hash });
...@@ -16,7 +19,9 @@ const TokenPage: NextPage<PageParams> = ({ hash }: PageParams) => { ...@@ -16,7 +19,9 @@ const TokenPage: NextPage<PageParams> = ({ hash }: PageParams) => {
<title>{ title }</title> <title>{ title }</title>
<meta name="description" content={ description }/> <meta name="description" content={ description }/>
</Head> </Head>
<Token/> <Page>
<Token/>
</Page>
</> </>
); );
}; };
......
import { ADDRESS_PARAMS, ADDRESS_HASH } from './addressParams';
export const PRIVATE_TAG_ADDRESS = {
address: ADDRESS_PARAMS,
address_hash: ADDRESS_HASH,
id: '4',
name: 'placeholder',
};
import type { Address } from 'types/api/address';
import { ADDRESS_HASH } from './addressParams';
import { TOKEN_INFO_ERC_20 } from './token';
export const ADDRESS_INFO: Address = {
block_number_balance_updated_at: 8774377,
coin_balance: '0',
creation_tx_hash: null,
creator_address_hash: null,
exchange_rate: null,
has_custom_methods_read: false,
has_custom_methods_write: false,
has_decompiled_code: false,
has_logs: true,
has_methods_read: false,
has_methods_read_proxy: false,
has_methods_write: false,
has_methods_write_proxy: false,
has_token_transfers: false,
has_tokens: false,
has_validated_blocks: false,
hash: ADDRESS_HASH,
implementation_address: null,
implementation_name: null,
is_contract: false,
is_verified: false,
name: 'ChainLink Token (goerli)',
token: TOKEN_INFO_ERC_20,
private_tags: [],
public_tags: [],
watchlist_names: [],
watchlist_address_id: null,
};
import type { AddressParam } from 'types/api/addressParams';
export const ADDRESS_HASH = '0x2B51Ae4412F79c3c1cB12AA40Ea4ECEb4e80511a';
export const ADDRESS_PARAMS: AddressParam = {
hash: ADDRESS_HASH,
implementation_name: null,
is_contract: false,
is_verified: null,
name: null,
private_tags: [],
public_tags: [],
watchlist_names: [],
};
export const BLOCK_HASH = '0x8fa7b9e5e5e79deeb62d608db22ba9a5cb45388c7ebb9223ae77331c6080dc70';
import type { SmartContract } from 'types/api/contract';
export const CONTRACT_CODE_UNVERIFIED = {
creation_bytecode: '0x60806040526e',
deployed_bytecode: '0x608060405233',
is_self_destructed: false,
} as SmartContract;
export const CONTRACT_CODE_VERIFIED = {
abi: [],
additional_sources: [],
can_be_visualized_via_sol2uml: true,
compiler_settings: {
compilationTarget: {
'contracts/StubContract.sol': 'StubContract',
},
evmVersion: 'london',
libraries: {},
metadata: {
bytecodeHash: 'ipfs',
},
optimizer: {
enabled: false,
runs: 200,
},
remappings: [],
},
compiler_version: 'v0.8.7+commit.e28d00a7',
creation_bytecode: '0x6080604052348',
deployed_bytecode: '0x60806040',
evm_version: 'london',
external_libraries: [],
file_path: 'contracts/StubContract.sol',
is_verified: true,
name: 'StubContract',
optimization_enabled: false,
optimization_runs: 200,
source_code: 'source_code',
verified_at: '2023-02-21T14:39:16.906760Z',
} as unknown as SmartContract;
import type { TokenCounters, TokenHolder, TokenHolders, TokenInfo, TokenInstance, TokenInventoryResponse, TokenType } from 'types/api/token';
import type { TokenTransfer, TokenTransferResponse } from 'types/api/tokenTransfer';
import { ADDRESS_PARAMS, ADDRESS_HASH } from './addressParams';
import { BLOCK_HASH } from './block';
import { TX_HASH } from './tx';
export const TOKEN_INFO_ERC_20: TokenInfo<'ERC-20'> = {
address: ADDRESS_HASH,
decimals: '18',
exchange_rate: null,
holders: '16026',
name: 'Stub Token (goerli)',
symbol: 'STUB',
total_supply: '6000000000000000000',
type: 'ERC-20',
};
export const TOKEN_INFO_ERC_721: TokenInfo<'ERC-721'> = {
...TOKEN_INFO_ERC_20,
type: 'ERC-721',
};
export const TOKEN_INFO_ERC_1155: TokenInfo<'ERC-1155'> = {
...TOKEN_INFO_ERC_20,
type: 'ERC-1155',
};
export const TOKEN_COUNTERS: TokenCounters = {
token_holders_count: '123456',
transfers_count: '123456',
};
export const TOKEN_HOLDER: TokenHolder = {
address: ADDRESS_PARAMS,
value: '1021378038331138520',
};
export const TOKEN_HOLDERS: TokenHolders = { items: Array(50).fill(TOKEN_HOLDER), next_page_params: null };
export const TOKEN_TRANSFER_ERC_20: TokenTransfer = {
block_hash: BLOCK_HASH,
from: ADDRESS_PARAMS,
log_index: '4',
method: 'addLiquidity',
timestamp: '2022-06-24T10:22:11.000000Z',
to: ADDRESS_PARAMS,
token: TOKEN_INFO_ERC_20,
total: {
decimals: '18',
value: '9851351626684503',
},
tx_hash: TX_HASH,
type: 'token_minting',
};
export const TOKEN_TRANSFER_ERC_721: TokenTransfer = {
...TOKEN_TRANSFER_ERC_20,
total: {
token_id: '35870',
},
token: TOKEN_INFO_ERC_721,
};
export const TOKEN_TRANSFER_ERC_1155: TokenTransfer = {
...TOKEN_TRANSFER_ERC_20,
total: {
token_id: '35870',
value: '123',
decimals: '18',
},
token: TOKEN_INFO_ERC_1155,
};
export const getTokenTransfersStub = (type?: TokenType): TokenTransferResponse => {
switch (type) {
case 'ERC-721':
return { items: Array(50).fill(TOKEN_TRANSFER_ERC_721), next_page_params: null };
case 'ERC-1155':
return { items: Array(50).fill(TOKEN_TRANSFER_ERC_1155), next_page_params: null };
default:
return { items: Array(50).fill(TOKEN_TRANSFER_ERC_20), next_page_params: null };
}
};
export const TOKEN_INSTANCE: TokenInstance = {
animation_url: null,
external_app_url: 'https://vipsland.com/nft/collections/genesis/188882',
id: '188882',
image_url: 'https://ipfs.vipsland.com/nft/collections/genesis/188882.gif',
is_unique: true,
metadata: {
attributes: Array(3).fill({ trait_type: 'skin', value: '6' }),
description: '**GENESIS #188882**, **8a77ca1bcaa4036f** :: *844th* generation of *#57806 and #57809* :: **eGenetic Hash Code (eDNA)** = *2822355e953a462d*',
external_url: 'https://vipsland.com/nft/collections/genesis/188882',
image: 'https://ipfs.vipsland.com/nft/collections/genesis/188882.gif',
name: 'GENESIS #188882, 8a77ca1bcaa4036f. Blockchain pixel PFP NFT + "on music video" trait inspired by God',
},
owner: ADDRESS_PARAMS,
token: TOKEN_INFO_ERC_1155,
holder_address_hash: ADDRESS_HASH,
};
export const TOKEN_INSTANCES: TokenInventoryResponse = {
items: Array(50).fill(TOKEN_INSTANCE),
next_page_params: null,
};
export const TX_HASH = '0x3ed9d81e7c1001bdda1caa1dc62c0acbbe3d2c671cdc20dc1e65efdaa4186967';
...@@ -26,7 +26,7 @@ const baseStyle = defineStyle((props) => { ...@@ -26,7 +26,7 @@ const baseStyle = defineStyle((props) => {
return { return {
opacity: 1, opacity: 1,
borderRadius: 'base', borderRadius: 'md',
borderColor: start, borderColor: start,
background: `linear-gradient(90deg, ${ start } 8%, ${ end } 18%, ${ start } 33%)`, background: `linear-gradient(90deg, ${ start } 8%, ${ end } 18%, ${ start } 33%)`,
backgroundSize: '200% 100%', backgroundSize: '200% 100%',
......
...@@ -20,7 +20,7 @@ export interface TokenCounters { ...@@ -20,7 +20,7 @@ export interface TokenCounters {
export interface TokenHolders { export interface TokenHolders {
items: Array<TokenHolder>; items: Array<TokenHolder>;
next_page_params: TokenHoldersPagination; next_page_params: TokenHoldersPagination | null;
} }
export type TokenHolder = { export type TokenHolder = {
...@@ -51,7 +51,7 @@ export interface TokenInstanceTransfersCount { ...@@ -51,7 +51,7 @@ export interface TokenInstanceTransfersCount {
export interface TokenInventoryResponse { export interface TokenInventoryResponse {
items: Array<TokenInstance>; items: Array<TokenInstance>;
next_page_params: TokenInventoryPagination; next_page_params: TokenInventoryPagination | null;
} }
export type TokenInventoryPagination = { export type TokenInventoryPagination = {
......
import { Flex, Skeleton, Button, Grid, GridItem, Text, Alert, Link, chakra, Box } from '@chakra-ui/react'; import { Flex, Skeleton, Button, Grid, GridItem, Alert, Link, chakra, Box } from '@chakra-ui/react';
import { useQueryClient } from '@tanstack/react-query';
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
import type { SocketMessage } from 'lib/socket/types'; import type { SocketMessage } from 'lib/socket/types';
import type { Address as AddressInfo } from 'types/api/address';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery';
import dayjs from 'lib/date/dayjs'; import dayjs from 'lib/date/dayjs';
import useSocketChannel from 'lib/socket/useSocketChannel'; import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage'; import useSocketMessage from 'lib/socket/useSocketMessage';
import * as stubs from 'stubs/contract';
import Address from 'ui/shared/address/Address'; import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon'; import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink'; import AddressLink from 'ui/shared/address/AddressLink';
...@@ -24,10 +27,10 @@ type Props = { ...@@ -24,10 +27,10 @@ type Props = {
noSocket?: boolean; noSocket?: boolean;
} }
const InfoItem = chakra(({ label, value, className }: { label: string; value: string; className?: string }) => ( const InfoItem = chakra(({ label, value, className, isLoading }: { label: string; value: string; className?: string; isLoading: boolean }) => (
<GridItem display="flex" columnGap={ 6 } wordBreak="break-all" className={ className }> <GridItem display="flex" columnGap={ 6 } wordBreak="break-all" className={ className } alignItems="baseline">
<Text w="170px" flexShrink={ 0 } fontWeight={ 500 }>{ label }</Text> <Skeleton isLoaded={ !isLoading } w="170px" flexShrink={ 0 } fontWeight={ 500 }>{ label }</Skeleton>
<Text>{ value }</Text> <Skeleton isLoaded={ !isLoading }>{ value }</Skeleton>
</GridItem> </GridItem>
)); ));
...@@ -35,11 +38,15 @@ const ContractCode = ({ addressHash, noSocket }: Props) => { ...@@ -35,11 +38,15 @@ const ContractCode = ({ addressHash, noSocket }: Props) => {
const [ isSocketOpen, setIsSocketOpen ] = React.useState(false); const [ isSocketOpen, setIsSocketOpen ] = React.useState(false);
const [ isChangedBytecodeSocket, setIsChangedBytecodeSocket ] = React.useState<boolean>(); const [ isChangedBytecodeSocket, setIsChangedBytecodeSocket ] = React.useState<boolean>();
const { data, isLoading, isError } = useApiQuery('contract', { const queryClient = useQueryClient();
const addressInfo = queryClient.getQueryData<AddressInfo>(getResourceKey('address', { pathParams: { hash: addressHash } }));
const { data, isPlaceholderData, isError } = useApiQuery('contract', {
pathParams: { hash: addressHash }, pathParams: { hash: addressHash },
queryOptions: { queryOptions: {
enabled: Boolean(addressHash) && (noSocket || isSocketOpen), enabled: Boolean(addressHash) && (noSocket || isSocketOpen),
refetchOnMount: false, refetchOnMount: false,
placeholderData: addressInfo?.is_verified ? stubs.CONTRACT_CODE_VERIFIED : stubs.CONTRACT_CODE_UNVERIFIED,
}, },
}); });
...@@ -62,24 +69,7 @@ const ContractCode = ({ addressHash, noSocket }: Props) => { ...@@ -62,24 +69,7 @@ const ContractCode = ({ addressHash, noSocket }: Props) => {
return <DataFetchAlert/>; return <DataFetchAlert/>;
} }
if (isLoading) { const verificationButton = isPlaceholderData ? <Skeleton w="130px" h={ 8 } mr={ 3 } ml="auto" borderRadius="base"/> : (
return (
<>
<Flex justifyContent="space-between" mb={ 2 }>
<Skeleton w="180px" h={ 5 } borderRadius="full"/>
<Skeleton w={ 5 } h={ 5 }/>
</Flex>
<Skeleton w="100%" h="250px" borderRadius="md"/>
<Flex justifyContent="space-between" mb={ 2 } mt={ 6 }>
<Skeleton w="180px" h={ 5 } borderRadius="full"/>
<Skeleton w={ 5 } h={ 5 }/>
</Flex>
<Skeleton w="100%" h="400px" borderRadius="md"/>
</>
);
}
const verificationButton = (
<Button <Button
size="sm" size="sm"
ml="auto" ml="auto"
...@@ -92,8 +82,8 @@ const ContractCode = ({ addressHash, noSocket }: Props) => { ...@@ -92,8 +82,8 @@ const ContractCode = ({ addressHash, noSocket }: Props) => {
); );
const constructorArgs = (() => { const constructorArgs = (() => {
if (!data.decoded_constructor_args) { if (!data?.decoded_constructor_args) {
return data.constructor_args; return data?.constructor_args;
} }
const decoded = data.decoded_constructor_args const decoded = data.decoded_constructor_args
...@@ -119,7 +109,7 @@ const ContractCode = ({ addressHash, noSocket }: Props) => { ...@@ -119,7 +109,7 @@ const ContractCode = ({ addressHash, noSocket }: Props) => {
})(); })();
const externalLibraries = (() => { const externalLibraries = (() => {
if (!data.external_libraries || data.external_libraries.length === 0) { if (!data?.external_libraries || data?.external_libraries.length === 0) {
return null; return null;
} }
...@@ -134,19 +124,23 @@ const ContractCode = ({ addressHash, noSocket }: Props) => { ...@@ -134,19 +124,23 @@ const ContractCode = ({ addressHash, noSocket }: Props) => {
return ( return (
<> <>
<Flex flexDir="column" rowGap={ 2 } mb={ 6 } _empty={{ display: 'none' }}> <Flex flexDir="column" rowGap={ 2 } mb={ 6 } _empty={{ display: 'none' }}>
{ data.is_verified && <Alert status="success">Contract Source Code Verified (Exact Match)</Alert> } { data?.is_verified && (
{ data.is_verified_via_sourcify && ( <Skeleton isLoaded={ !isPlaceholderData }>
<Alert status="success">Contract Source Code Verified (Exact Match)</Alert>
</Skeleton>
) }
{ data?.is_verified_via_sourcify && (
<Alert status="warning" whiteSpace="pre-wrap" flexWrap="wrap"> <Alert status="warning" whiteSpace="pre-wrap" flexWrap="wrap">
<span>This contract has been { data.is_partially_verified ? 'partially ' : '' }verified via Sourcify. </span> <span>This contract has been { data.is_partially_verified ? 'partially ' : '' }verified via Sourcify. </span>
{ data.sourcify_repo_url && <LinkExternal href={ data.sourcify_repo_url } fontSize="md">View contract in Sourcify repository</LinkExternal> } { data.sourcify_repo_url && <LinkExternal href={ data.sourcify_repo_url } fontSize="md">View contract in Sourcify repository</LinkExternal> }
</Alert> </Alert>
) } ) }
{ (data.is_changed_bytecode || isChangedBytecodeSocket) && ( { (data?.is_changed_bytecode || isChangedBytecodeSocket) && (
<Alert status="warning"> <Alert status="warning">
Warning! Contract bytecode has been changed and does not match the verified one. Therefore, interaction with this smart contract may be risky. Warning! Contract bytecode has been changed and does not match the verified one. Therefore, interaction with this smart contract may be risky.
</Alert> </Alert>
) } ) }
{ !data.is_verified && data.verified_twin_address_hash && !data.minimal_proxy_address_hash && ( { !data?.is_verified && data?.verified_twin_address_hash && !data?.minimal_proxy_address_hash && (
<Alert status="warning" whiteSpace="pre-wrap" flexWrap="wrap"> <Alert status="warning" whiteSpace="pre-wrap" flexWrap="wrap">
<span>Contract is not verified. However, we found a verified contract with the same bytecode in Blockscout DB </span> <span>Contract is not verified. However, we found a verified contract with the same bytecode in Blockscout DB </span>
<Address> <Address>
...@@ -160,7 +154,7 @@ const ContractCode = ({ addressHash, noSocket }: Props) => { ...@@ -160,7 +154,7 @@ const ContractCode = ({ addressHash, noSocket }: Props) => {
<span> page</span> <span> page</span>
</Alert> </Alert>
) } ) }
{ data.minimal_proxy_address_hash && ( { data?.minimal_proxy_address_hash && (
<Alert status="warning" flexWrap="wrap" whiteSpace="pre-wrap"> <Alert status="warning" flexWrap="wrap" whiteSpace="pre-wrap">
<span>Minimal Proxy Contract for </span> <span>Minimal Proxy Contract for </span>
<Address> <Address>
...@@ -175,14 +169,16 @@ const ContractCode = ({ addressHash, noSocket }: Props) => { ...@@ -175,14 +169,16 @@ const ContractCode = ({ addressHash, noSocket }: Props) => {
</Alert> </Alert>
) } ) }
</Flex> </Flex>
{ data.is_verified && ( { data?.is_verified && (
<Grid templateColumns={{ base: '1fr', lg: '1fr 1fr' }} rowGap={ 4 } columnGap={ 6 } mb={ 8 }> <Grid templateColumns={{ base: '1fr', lg: '1fr 1fr' }} rowGap={ 4 } columnGap={ 6 } mb={ 8 }>
{ data.name && <InfoItem label="Contract name" value={ data.name }/> } { data.name && <InfoItem label="Contract name" value={ data.name } isLoading={ isPlaceholderData }/> }
{ data.compiler_version && <InfoItem label="Compiler version" value={ data.compiler_version }/> } { data.compiler_version && <InfoItem label="Compiler version" value={ data.compiler_version } isLoading={ isPlaceholderData }/> }
{ data.evm_version && <InfoItem label="EVM version" value={ data.evm_version } textTransform="capitalize"/> } { data.evm_version && <InfoItem label="EVM version" value={ data.evm_version } textTransform="capitalize" isLoading={ isPlaceholderData }/> }
{ typeof data.optimization_enabled === 'boolean' && <InfoItem label="Optimization enabled" value={ data.optimization_enabled ? 'true' : 'false' }/> } { typeof data.optimization_enabled === 'boolean' &&
{ data.optimization_runs && <InfoItem label="Optimization runs" value={ String(data.optimization_runs) }/> } <InfoItem label="Optimization enabled" value={ data.optimization_enabled ? 'true' : 'false' } isLoading={ isPlaceholderData }/> }
{ data.verified_at && <InfoItem label="Verified at" value={ dayjs(data.verified_at).format('LLLL') } wordBreak="break-word"/> } { data.optimization_runs && <InfoItem label="Optimization runs" value={ String(data.optimization_runs) } isLoading={ isPlaceholderData }/> }
{ data.verified_at &&
<InfoItem label="Verified at" value={ dayjs(data.verified_at).format('LLLL') } wordBreak="break-word" isLoading={ isPlaceholderData }/> }
</Grid> </Grid>
) } ) }
<Flex flexDir="column" rowGap={ 6 }> <Flex flexDir="column" rowGap={ 6 }>
...@@ -191,9 +187,10 @@ const ContractCode = ({ addressHash, noSocket }: Props) => { ...@@ -191,9 +187,10 @@ const ContractCode = ({ addressHash, noSocket }: Props) => {
data={ constructorArgs } data={ constructorArgs }
title="Constructor Arguments" title="Constructor Arguments"
textareaMaxHeight="200px" textareaMaxHeight="200px"
isLoading={ isPlaceholderData }
/> />
) } ) }
{ data.source_code && ( { data?.source_code && (
<ContractSourceCode <ContractSourceCode
data={ data.source_code } data={ data.source_code }
hasSol2Yml={ Boolean(data.can_be_visualized_via_sol2uml) } hasSol2Yml={ Boolean(data.can_be_visualized_via_sol2uml) }
...@@ -202,23 +199,26 @@ const ContractCode = ({ addressHash, noSocket }: Props) => { ...@@ -202,23 +199,26 @@ const ContractCode = ({ addressHash, noSocket }: Props) => {
filePath={ data.file_path } filePath={ data.file_path }
additionalSource={ data.additional_sources } additionalSource={ data.additional_sources }
remappings={ data.compiler_settings?.remappings } remappings={ data.compiler_settings?.remappings }
isLoading={ isPlaceholderData }
/> />
) } ) }
{ Boolean(data.compiler_settings) && ( { data?.compiler_settings ? (
<RawDataSnippet <RawDataSnippet
data={ JSON.stringify(data.compiler_settings) } data={ JSON.stringify(data.compiler_settings) }
title="Compiler Settings" title="Compiler Settings"
textareaMaxHeight="200px" textareaMaxHeight="200px"
isLoading={ isPlaceholderData }
/> />
) } ) : null }
{ data.abi && ( { data?.abi && (
<RawDataSnippet <RawDataSnippet
data={ JSON.stringify(data.abi) } data={ JSON.stringify(data.abi) }
title="Contract ABI" title="Contract ABI"
textareaMaxHeight="200px" textareaMaxHeight="200px"
isLoading={ isPlaceholderData }
/> />
) } ) }
{ data.creation_bytecode && ( { data?.creation_bytecode && (
<RawDataSnippet <RawDataSnippet
data={ data.creation_bytecode } data={ data.creation_bytecode }
title="Contract creation code" title="Contract creation code"
...@@ -230,13 +230,15 @@ const ContractCode = ({ addressHash, noSocket }: Props) => { ...@@ -230,13 +230,15 @@ const ContractCode = ({ addressHash, noSocket }: Props) => {
</Alert> </Alert>
) : null } ) : null }
textareaMaxHeight="200px" textareaMaxHeight="200px"
isLoading={ isPlaceholderData }
/> />
) } ) }
{ data.deployed_bytecode && ( { data?.deployed_bytecode && (
<RawDataSnippet <RawDataSnippet
data={ data.deployed_bytecode } data={ data.deployed_bytecode }
title="Deployed ByteCode" title="Deployed ByteCode"
textareaMaxHeight="200px" textareaMaxHeight="200px"
isLoading={ isPlaceholderData }
/> />
) } ) }
{ externalLibraries && ( { externalLibraries && (
...@@ -244,6 +246,7 @@ const ContractCode = ({ addressHash, noSocket }: Props) => { ...@@ -244,6 +246,7 @@ const ContractCode = ({ addressHash, noSocket }: Props) => {
data={ externalLibraries } data={ externalLibraries }
title="External Libraries" title="External Libraries"
textareaMaxHeight="200px" textareaMaxHeight="200px"
isLoading={ isPlaceholderData }
/> />
) } ) }
</Flex> </Flex>
......
import { Flex, Text, Tooltip } from '@chakra-ui/react'; import { Flex, Skeleton, Text, Tooltip } from '@chakra-ui/react';
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
...@@ -17,14 +17,15 @@ interface Props { ...@@ -17,14 +17,15 @@ interface Props {
filePath?: string; filePath?: string;
additionalSource?: SmartContract['additional_sources']; additionalSource?: SmartContract['additional_sources'];
remappings?: Array<string>; remappings?: Array<string>;
isLoading?: boolean;
} }
const ContractSourceCode = ({ data, hasSol2Yml, address, isViper, filePath, additionalSource, remappings }: Props) => { const ContractSourceCode = ({ data, hasSol2Yml, address, isViper, filePath, additionalSource, remappings, isLoading }: Props) => {
const heading = ( const heading = (
<Text fontWeight={ 500 }> <Skeleton isLoaded={ !isLoading } fontWeight={ 500 }>
<span>Contract source code</span> <span>Contract source code</span>
<Text whiteSpace="pre" as="span" variant="secondary"> ({ isViper ? 'Vyper' : 'Solidity' })</Text> <Text whiteSpace="pre" as="span" variant="secondary"> ({ isViper ? 'Vyper' : 'Solidity' })</Text>
</Text> </Skeleton>
); );
const diagramLink = hasSol2Yml && address ? ( const diagramLink = hasSol2Yml && address ? (
...@@ -32,9 +33,10 @@ const ContractSourceCode = ({ data, hasSol2Yml, address, isViper, filePath, addi ...@@ -32,9 +33,10 @@ const ContractSourceCode = ({ data, hasSol2Yml, address, isViper, filePath, addi
<LinkInternal <LinkInternal
href={ route({ pathname: '/visualize/sol2uml', query: { address } }) } href={ route({ pathname: '/visualize/sol2uml', query: { address } }) }
ml="auto" ml="auto"
mr={ 3 }
> >
View UML diagram <Skeleton isLoaded={ !isLoading }>
View UML diagram
</Skeleton>
</LinkInternal> </LinkInternal>
</Tooltip> </Tooltip>
) : null; ) : null;
...@@ -47,7 +49,7 @@ const ContractSourceCode = ({ data, hasSol2Yml, address, isViper, filePath, addi ...@@ -47,7 +49,7 @@ const ContractSourceCode = ({ data, hasSol2Yml, address, isViper, filePath, addi
}, [ additionalSource, data, filePath, isViper ]); }, [ additionalSource, data, filePath, isViper ]);
const copyToClipboard = editorData.length === 1 ? const copyToClipboard = editorData.length === 1 ?
<CopyToClipboard text={ editorData[0].source_code }/> : <CopyToClipboard text={ editorData[0].source_code } isLoading={ isLoading } ml={ 3 }/> :
null; null;
return ( return (
...@@ -57,7 +59,7 @@ const ContractSourceCode = ({ data, hasSol2Yml, address, isViper, filePath, addi ...@@ -57,7 +59,7 @@ const ContractSourceCode = ({ data, hasSol2Yml, address, isViper, filePath, addi
{ diagramLink } { diagramLink }
{ copyToClipboard } { copyToClipboard }
</Flex> </Flex>
<CodeEditor data={ editorData } remappings={ remappings }/> { isLoading ? <Skeleton h="557px" w="100%"/> : <CodeEditor data={ editorData } remappings={ remappings }/> }
</section> </section>
); );
}; };
......
import { chakra, Alert, Icon, Modal, ModalBody, ModalContent, ModalCloseButton, ModalOverlay, Box, useDisclosure, Tooltip, IconButton } from '@chakra-ui/react'; import {
chakra,
Alert,
Icon,
Modal,
ModalBody,
ModalContent,
ModalCloseButton,
ModalOverlay,
Box,
useDisclosure,
Tooltip,
IconButton,
Skeleton,
} from '@chakra-ui/react';
import * as Sentry from '@sentry/react'; import * as Sentry from '@sentry/react';
import QRCode from 'qrcode'; import QRCode from 'qrcode';
import React from 'react'; import React from 'react';
...@@ -13,9 +27,10 @@ const SVG_OPTIONS = { ...@@ -13,9 +27,10 @@ const SVG_OPTIONS = {
interface Props { interface Props {
className?: string; className?: string;
hash: string; hash: string;
isLoading?: boolean;
} }
const AddressQrCode = ({ hash, className }: Props) => { const AddressQrCode = ({ hash, className, isLoading }: Props) => {
const { isOpen, onOpen, onClose } = useDisclosure(); const { isOpen, onOpen, onClose } = useDisclosure();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const [ qr, setQr ] = React.useState(''); const [ qr, setQr ] = React.useState('');
...@@ -36,6 +51,10 @@ const AddressQrCode = ({ hash, className }: Props) => { ...@@ -36,6 +51,10 @@ const AddressQrCode = ({ hash, className }: Props) => {
} }
}, [ hash, isOpen, onClose ]); }, [ hash, isOpen, onClose ]);
if (isLoading) {
return <Skeleton className={ className } w="36px" h="32px" borderRadius="base"/>;
}
return ( return (
<> <>
<Tooltip label="Click to view QR code"> <Tooltip label="Click to view QR code">
......
...@@ -49,7 +49,7 @@ const Home = () => { ...@@ -49,7 +49,7 @@ const Home = () => {
</Box> </Box>
<Stats/> <Stats/>
<ChainIndicators/> <ChainIndicators/>
<AdBanner mt={{ base: 6, lg: 8 }} justifyContent="center"/> <AdBanner mt={{ base: 6, lg: 8 }} mx="auto" display="flex" justifyContent="center"/>
<Flex mt={ 8 } direction={{ base: 'column', lg: 'row' }} columnGap={ 12 } rowGap={ 8 }> <Flex mt={ 8 } direction={{ base: 'column', lg: 'row' }} columnGap={ 12 } rowGap={ 8 }>
<LatestBlocks/> <LatestBlocks/>
<Box flexGrow={ 1 }> <Box flexGrow={ 1 }>
......
...@@ -67,7 +67,7 @@ test('base view', async({ mount, page, createSocket }) => { ...@@ -67,7 +67,7 @@ test('base view', async({ mount, page, createSocket }) => {
await insertAdPlaceholder(page); await insertAdPlaceholder(page);
await expect(component.locator('main')).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
test.describe('mobile', () => { test.describe('mobile', () => {
...@@ -86,6 +86,6 @@ test.describe('mobile', () => { ...@@ -86,6 +86,6 @@ test.describe('mobile', () => {
await insertAdPlaceholder(page); await insertAdPlaceholder(page);
await expect(component.locator('main')).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
}); });
import { Skeleton, Box, Flex, SkeletonCircle, Icon, Tag } from '@chakra-ui/react'; import { Box, Icon } from '@chakra-ui/react';
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
...@@ -16,9 +16,11 @@ import useQueryWithPages from 'lib/hooks/useQueryWithPages'; ...@@ -16,9 +16,11 @@ import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import useSocketChannel from 'lib/socket/useSocketChannel'; import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage'; import useSocketMessage from 'lib/socket/useSocketMessage';
import trimTokenSymbol from 'lib/token/trimTokenSymbol'; import trimTokenSymbol from 'lib/token/trimTokenSymbol';
import * as addressStubs from 'stubs/address';
import * as tokenStubs from 'stubs/token';
import AddressContract from 'ui/address/AddressContract'; import AddressContract from 'ui/address/AddressContract';
import TextAd from 'ui/shared/ad/TextAd'; import TextAd from 'ui/shared/ad/TextAd';
import Page from 'ui/shared/Page/Page'; import Tag from 'ui/shared/chakra/Tag';
import PageTitle from 'ui/shared/Page/PageTitle'; import PageTitle from 'ui/shared/Page/PageTitle';
import type { Props as PaginationProps } from 'ui/shared/Pagination'; import type { Props as PaginationProps } from 'ui/shared/Pagination';
import Pagination from 'ui/shared/Pagination'; import Pagination from 'ui/shared/Pagination';
...@@ -51,7 +53,18 @@ const TokenPageContent = () => { ...@@ -51,7 +53,18 @@ const TokenPageContent = () => {
const tokenQuery = useApiQuery('token', { const tokenQuery = useApiQuery('token', {
pathParams: { hash: hashString }, pathParams: { hash: hashString },
queryOptions: { enabled: isSocketOpen && Boolean(router.query.hash) }, queryOptions: {
enabled: Boolean(router.query.hash),
placeholderData: tokenStubs.TOKEN_INFO_ERC_20,
},
});
const contractQuery = useApiQuery('address', {
pathParams: { hash: hashString },
queryOptions: {
enabled: isSocketOpen && Boolean(router.query.hash),
placeholderData: addressStubs.ADDRESS_INFO,
},
}); });
React.useEffect(() => { React.useEffect(() => {
...@@ -88,7 +101,7 @@ const TokenPageContent = () => { ...@@ -88,7 +101,7 @@ const TokenPageContent = () => {
}); });
useEffect(() => { useEffect(() => {
if (tokenQuery.data) { if (tokenQuery.data && !tokenQuery.isPlaceholderData) {
const tokenSymbol = tokenQuery.data.symbol ? ` (${ tokenQuery.data.symbol })` : ''; const tokenSymbol = tokenQuery.data.symbol ? ` (${ tokenQuery.data.symbol })` : '';
const tokenName = `${ tokenQuery.data.name || 'Unnamed' }${ tokenSymbol }`; const tokenName = `${ tokenQuery.data.name || 'Unnamed' }${ tokenSymbol }`;
const title = document.getElementsByTagName('title')[0]; const title = document.getElementsByTagName('title')[0];
...@@ -100,14 +113,17 @@ const TokenPageContent = () => { ...@@ -100,14 +113,17 @@ const TokenPageContent = () => {
description.content = description.content.replace(tokenQuery.data.address, tokenName) || description.content; description.content = description.content.replace(tokenQuery.data.address, tokenName) || description.content;
} }
} }
}, [ tokenQuery.data ]); }, [ tokenQuery.data, tokenQuery.isPlaceholderData ]);
const hasData = (tokenQuery.data && !tokenQuery.isPlaceholderData) && (contractQuery.data && !contractQuery.isPlaceholderData);
const transfersQuery = useQueryWithPages({ const transfersQuery = useQueryWithPages({
resourceName: 'token_transfers', resourceName: 'token_transfers',
pathParams: { hash: hashString }, pathParams: { hash: hashString },
scrollRef, scrollRef,
options: { options: {
enabled: Boolean(router.query.hash && (!router.query.tab || router.query.tab === 'token_transfers') && tokenQuery.data), enabled: Boolean(hashString && (!router.query.tab || router.query.tab === 'token_transfers') && hasData),
placeholderData: tokenStubs.getTokenTransfersStub(tokenQuery.data?.type),
}, },
}); });
...@@ -116,7 +132,8 @@ const TokenPageContent = () => { ...@@ -116,7 +132,8 @@ const TokenPageContent = () => {
pathParams: { hash: hashString }, pathParams: { hash: hashString },
scrollRef, scrollRef,
options: { options: {
enabled: Boolean(router.query.hash && router.query.tab === 'holders' && tokenQuery.data), enabled: Boolean(router.query.hash && router.query.tab === 'holders' && hasData),
placeholderData: tokenStubs.TOKEN_HOLDERS,
}, },
}); });
...@@ -125,19 +142,15 @@ const TokenPageContent = () => { ...@@ -125,19 +142,15 @@ const TokenPageContent = () => {
pathParams: { hash: hashString }, pathParams: { hash: hashString },
scrollRef, scrollRef,
options: { options: {
enabled: Boolean(router.query.hash && router.query.tab === 'inventory' && tokenQuery.data), enabled: Boolean(router.query.hash && router.query.tab === 'inventory' && hasData),
placeholderData: tokenStubs.TOKEN_INSTANCES,
}, },
}); });
const contractQuery = useApiQuery('address', {
pathParams: { hash: hashString },
queryOptions: { enabled: Boolean(router.query.hash) },
});
const contractTabs = useContractTabs(contractQuery.data); const contractTabs = useContractTabs(contractQuery.data);
const tabs: Array<RoutedTab> = [ const tabs: Array<RoutedTab> = [
{ id: 'token_transfers', title: 'Token transfers', component: <TokenTransfer transfersQuery={ transfersQuery }/> }, { id: 'token_transfers', title: 'Token transfers', component: <TokenTransfer transfersQuery={ transfersQuery } token={ tokenQuery.data }/> },
{ id: 'holders', title: 'Holders', component: <TokenHolders token={ tokenQuery.data } holdersQuery={ holdersQuery }/> }, { id: 'holders', title: 'Holders', component: <TokenHolders token={ tokenQuery.data } holdersQuery={ holdersQuery }/> },
(tokenQuery.data?.type === 'ERC-1155' || tokenQuery.data?.type === 'ERC-721') ? (tokenQuery.data?.type === 'ERC-1155' || tokenQuery.data?.type === 'ERC-721') ?
{ id: 'inventory', title: 'Inventory', component: <TokenInventory inventoryQuery={ inventoryQuery }/> } : { id: 'inventory', title: 'Inventory', component: <TokenInventory inventoryQuery={ inventoryQuery }/> } :
...@@ -145,7 +158,7 @@ const TokenPageContent = () => { ...@@ -145,7 +158,7 @@ const TokenPageContent = () => {
contractQuery.data?.is_contract ? { contractQuery.data?.is_contract ? {
id: 'contract', id: 'contract',
title: () => { title: () => {
if (contractQuery.data.is_verified) { if (contractQuery.data?.is_verified) {
return ( return (
<> <>
<span>Contract</span> <span>Contract</span>
...@@ -195,45 +208,36 @@ const TokenPageContent = () => { ...@@ -195,45 +208,36 @@ const TokenPageContent = () => {
}, [ isMobile ]); }, [ isMobile ]);
return ( return (
<Page> <>
{ tokenQuery.isLoading ? ( <TextAd mb={ 6 }/>
<> <PageTitle
<Skeleton h={{ base: 12, lg: 6 }} mb={ 6 } w="100%" maxW="680px"/> isLoading={ tokenQuery.isPlaceholderData }
<Flex alignItems="center" mb={ 6 }> text={ `${ tokenQuery.data?.name || 'Unnamed' }${ tokenSymbolText } token` }
<SkeletonCircle w={ 6 } h={ 6 } mr={ 3 }/> backLinkUrl={ hasGoBackLink ? appProps.referrer : undefined }
<Skeleton w="500px" h={ 10 }/> backLinkLabel="Back to tokens list"
</Flex> additionalsLeft={ (
</> <TokenLogo hash={ tokenQuery.data?.address } name={ tokenQuery.data?.name } boxSize={ 6 } isLoading={ tokenQuery.isPlaceholderData }/>
) : ( ) }
<> additionalsRight={ <Tag isLoading={ tokenQuery.isPlaceholderData }>{ tokenQuery.data?.type }</Tag> }
<TextAd mb={ 6 }/> />
<PageTitle <TokenContractInfo tokenQuery={ tokenQuery } contractQuery={ contractQuery }/>
text={ `${ tokenQuery.data?.name || 'Unnamed' }${ tokenSymbolText } token` }
backLinkUrl={ hasGoBackLink ? appProps.referrer : undefined }
backLinkLabel="Back to tokens list"
additionalsLeft={ (
<TokenLogo hash={ tokenQuery.data?.address } name={ tokenQuery.data?.name } boxSize={ 6 }/>
) }
additionalsRight={ <Tag>{ tokenQuery.data?.type }</Tag> }
/>
</>
) }
<TokenContractInfo tokenQuery={ tokenQuery }/>
<TokenDetails tokenQuery={ tokenQuery }/> <TokenDetails tokenQuery={ tokenQuery }/>
{ /* should stay before tabs to scroll up with pagination */ } { /* should stay before tabs to scroll up with pagination */ }
<Box ref={ scrollRef }></Box> <Box ref={ scrollRef }></Box>
{ tokenQuery.isLoading || contractQuery.isLoading ? <SkeletonTabs/> : ( { tokenQuery.isPlaceholderData || contractQuery.isPlaceholderData ?
<RoutedTabs <SkeletonTabs tabs={ tabs }/> :
tabs={ tabs } (
tabListProps={ tabListProps } <RoutedTabs
rightSlot={ !isMobile && hasPagination && pagination ? <Pagination { ...pagination }/> : null } tabs={ tabs }
stickyEnabled={ !isMobile } tabListProps={ tabListProps }
/> rightSlot={ !isMobile && hasPagination && pagination ? <Pagination { ...pagination }/> : null }
) } stickyEnabled={ !isMobile }
/>
) }
{ !tokenQuery.isLoading && !tokenQuery.isError && <Box h={{ base: 0, lg: '40vh' }}/> } { !tokenQuery.isLoading && !tokenQuery.isError && <Box h={{ base: 0, lg: '40vh' }}/> }
</Page> </>
); );
}; };
......
import { Tag, Flex, HStack, Text } from '@chakra-ui/react'; import { Tag, Flex, HStack, Text, Skeleton } from '@chakra-ui/react';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import type { AddressTag } from 'types/api/account'; import type { AddressTag } from 'types/api/account';
...@@ -11,9 +11,10 @@ interface Props { ...@@ -11,9 +11,10 @@ interface Props {
item: AddressTag; item: AddressTag;
onEditClick: (data: AddressTag) => void; onEditClick: (data: AddressTag) => void;
onDeleteClick: (data: AddressTag) => void; onDeleteClick: (data: AddressTag) => void;
isLoading?: boolean;
} }
const AddressTagListItem = ({ item, onEditClick, onDeleteClick }: Props) => { const AddressTagListItem = ({ item, onEditClick, onDeleteClick, isLoading }: Props) => {
const onItemEditClick = useCallback(() => { const onItemEditClick = useCallback(() => {
return onEditClick(item); return onEditClick(item);
}, [ item, onEditClick ]); }, [ item, onEditClick ]);
...@@ -25,15 +26,17 @@ const AddressTagListItem = ({ item, onEditClick, onDeleteClick }: Props) => { ...@@ -25,15 +26,17 @@ const AddressTagListItem = ({ item, onEditClick, onDeleteClick }: Props) => {
return ( return (
<ListItemMobile> <ListItemMobile>
<Flex alignItems="flex-start" flexDirection="column" maxW="100%"> <Flex alignItems="flex-start" flexDirection="column" maxW="100%">
<AddressSnippet address={ item.address }/> <AddressSnippet address={ item.address } isLoading={ isLoading }/>
<HStack spacing={ 3 } mt={ 4 }> <HStack spacing={ 3 } mt={ 4 }>
<Text fontSize="sm" fontWeight={ 500 }>Private tag</Text> <Text fontSize="sm" fontWeight={ 500 }>Private tag</Text>
<Tag> <Skeleton isLoaded={ !isLoading } display="inline-block" borderRadius="sm">
{ item.name } <Tag>
</Tag> { item.name }
</Tag>
</Skeleton>
</HStack> </HStack>
</Flex> </Flex>
<TableItemActionButtons onDeleteClick={ onItemDeleteClick } onEditClick={ onItemEditClick }/> <TableItemActionButtons onDeleteClick={ onItemDeleteClick } onEditClick={ onItemEditClick } isLoading={ isLoading }/>
</ListItemMobile> </ListItemMobile>
); );
}; };
......
...@@ -12,28 +12,30 @@ import type { AddressTags, AddressTag } from 'types/api/account'; ...@@ -12,28 +12,30 @@ import type { AddressTags, AddressTag } from 'types/api/account';
import AddressTagTableItem from './AddressTagTableItem'; import AddressTagTableItem from './AddressTagTableItem';
interface Props { interface Props {
data: AddressTags; data?: AddressTags;
onEditClick: (data: AddressTag) => void; onEditClick: (data: AddressTag) => void;
onDeleteClick: (data: AddressTag) => void; onDeleteClick: (data: AddressTag) => void;
isLoading: boolean;
} }
const AddressTagTable = ({ data, onDeleteClick, onEditClick }: Props) => { const AddressTagTable = ({ data, onDeleteClick, onEditClick, isLoading }: Props) => {
return ( return (
<Table variant="simple" minWidth="600px"> <Table variant="simple" minWidth="600px">
<Thead> <Thead>
<Tr> <Tr>
<Th width="60%">Address</Th> <Th width="60%">Address</Th>
<Th width="40%">Private tag</Th> <Th width="40%">Private tag</Th>
<Th width="108px"></Th> <Th width="116px"></Th>
</Tr> </Tr>
</Thead> </Thead>
<Tbody> <Tbody>
{ data.map((item: AddressTag) => ( { data?.map((item: AddressTag, index: number) => (
<AddressTagTableItem <AddressTagTableItem
item={ item } item={ item }
key={ item.id } key={ item.id + (isLoading ? index : '') }
onDeleteClick={ onDeleteClick } onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick } onEditClick={ onEditClick }
isLoading={ isLoading }
/> />
)) } )) }
</Tbody> </Tbody>
......
import { import {
Tag,
Tr, Tr,
Td, Td,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
...@@ -8,16 +7,17 @@ import React, { useCallback } from 'react'; ...@@ -8,16 +7,17 @@ import React, { useCallback } from 'react';
import type { AddressTag } from 'types/api/account'; import type { AddressTag } from 'types/api/account';
import AddressSnippet from 'ui/shared/AddressSnippet'; import AddressSnippet from 'ui/shared/AddressSnippet';
import Tag from 'ui/shared/chakra/Tag';
import TableItemActionButtons from 'ui/shared/TableItemActionButtons'; import TableItemActionButtons from 'ui/shared/TableItemActionButtons';
import TruncatedTextTooltip from 'ui/shared/TruncatedTextTooltip';
interface Props { interface Props {
item: AddressTag; item: AddressTag;
onEditClick: (data: AddressTag) => void; onEditClick: (data: AddressTag) => void;
onDeleteClick: (data: AddressTag) => void; onDeleteClick: (data: AddressTag) => void;
isLoading: boolean;
} }
const AddressTagTableItem = ({ item, onEditClick, onDeleteClick }: Props) => { const AddressTagTableItem = ({ item, onEditClick, onDeleteClick, isLoading }: Props) => {
const onItemEditClick = useCallback(() => { const onItemEditClick = useCallback(() => {
return onEditClick(item); return onEditClick(item);
}, [ item, onEditClick ]); }, [ item, onEditClick ]);
...@@ -29,17 +29,13 @@ const AddressTagTableItem = ({ item, onEditClick, onDeleteClick }: Props) => { ...@@ -29,17 +29,13 @@ const AddressTagTableItem = ({ item, onEditClick, onDeleteClick }: Props) => {
return ( return (
<Tr alignItems="top" key={ item.id }> <Tr alignItems="top" key={ item.id }>
<Td> <Td>
<AddressSnippet address={ item.address }/> <AddressSnippet address={ item.address } isLoading={ isLoading }/>
</Td> </Td>
<Td whiteSpace="nowrap"> <Td whiteSpace="nowrap">
<TruncatedTextTooltip label={ item.name }> <Tag isLoading={ isLoading } isTruncated>{ item.name }</Tag>
<Tag>
{ item.name }
</Tag>
</TruncatedTextTooltip>
</Td> </Td>
<Td> <Td>
<TableItemActionButtons onDeleteClick={ onItemDeleteClick } onEditClick={ onItemEditClick }/> <TableItemActionButtons onDeleteClick={ onItemDeleteClick } onEditClick={ onItemEditClick } isLoading={ isLoading }/>
</Td> </Td>
</Tr> </Tr>
); );
......
...@@ -5,10 +5,9 @@ import type { AddressTag } from 'types/api/account'; ...@@ -5,10 +5,9 @@ import type { AddressTag } from 'types/api/account';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import { PRIVATE_TAG_ADDRESS } from 'stubs/account';
import AccountPageDescription from 'ui/shared/AccountPageDescription'; import AccountPageDescription from 'ui/shared/AccountPageDescription';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
import SkeletonListAccount from 'ui/shared/skeletons/SkeletonListAccount';
import SkeletonTable from 'ui/shared/skeletons/SkeletonTable';
import AddressModal from './AddressModal/AddressModal'; import AddressModal from './AddressModal/AddressModal';
import AddressTagListItem from './AddressTagTable/AddressTagListItem'; import AddressTagListItem from './AddressTagTable/AddressTagListItem';
...@@ -16,7 +15,12 @@ import AddressTagTable from './AddressTagTable/AddressTagTable'; ...@@ -16,7 +15,12 @@ import AddressTagTable from './AddressTagTable/AddressTagTable';
import DeletePrivateTagModal from './DeletePrivateTagModal'; import DeletePrivateTagModal from './DeletePrivateTagModal';
const PrivateAddressTags = () => { const PrivateAddressTags = () => {
const { data: addressTagsData, isLoading, isError } = useApiQuery('private_tags_address', { queryOptions: { refetchOnMount: false } }); const { data: addressTagsData, isError, isPlaceholderData } = useApiQuery('private_tags_address', {
queryOptions: {
refetchOnMount: false,
placeholderData: Array(3).fill(PRIVATE_TAG_ADDRESS),
},
});
const addressModalProps = useDisclosure(); const addressModalProps = useDisclosure();
const deleteModalProps = useDisclosure(); const deleteModalProps = useDisclosure();
...@@ -45,46 +49,25 @@ const PrivateAddressTags = () => { ...@@ -45,46 +49,25 @@ const PrivateAddressTags = () => {
deleteModalProps.onClose(); deleteModalProps.onClose();
}, [ deleteModalProps ]); }, [ deleteModalProps ]);
const description = (
<AccountPageDescription>
Use private address tags to track any addresses of interest.
Private tags are saved in your account and are only visible when you are logged in.
</AccountPageDescription>
);
if (isLoading && !addressTagsData) {
const loader = isMobile ? <SkeletonListAccount/> : (
<>
<SkeletonTable columns={ [ '60%', '40%', '108px' ] }/>
<Skeleton height="44px" width="156px" marginTop={ 8 }/>
</>
);
return (
<>
{ description }
{ loader }
</>
);
}
if (isError) { if (isError) {
return <DataFetchAlert/>; return <DataFetchAlert/>;
} }
const list = isMobile ? ( const list = isMobile ? (
<Box> <Box>
{ addressTagsData.map((item: AddressTag) => ( { addressTagsData?.map((item: AddressTag, index: number) => (
<AddressTagListItem <AddressTagListItem
item={ item } item={ item }
key={ item.id } key={ item.id + (isPlaceholderData ? index : '') }
onDeleteClick={ onDeleteClick } onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick } onEditClick={ onEditClick }
isLoading={ isPlaceholderData }
/> />
)) } )) }
</Box> </Box>
) : ( ) : (
<AddressTagTable <AddressTagTable
isLoading={ isPlaceholderData }
data={ addressTagsData } data={ addressTagsData }
onDeleteClick={ onDeleteClick } onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick } onEditClick={ onEditClick }
...@@ -93,15 +76,20 @@ const PrivateAddressTags = () => { ...@@ -93,15 +76,20 @@ const PrivateAddressTags = () => {
return ( return (
<> <>
{ description } <AccountPageDescription>
Use private address tags to track any addresses of interest.
Private tags are saved in your account and are only visible when you are logged in.
</AccountPageDescription>
{ Boolean(addressTagsData?.length) && list } { Boolean(addressTagsData?.length) && list }
<Box marginTop={ 8 }> <Box marginTop={ 8 }>
<Button <Skeleton isLoaded={ !isPlaceholderData } display="inline-block">
size="lg" <Button
onClick={ addressModalProps.onOpen } size="lg"
> onClick={ addressModalProps.onOpen }
Add address tag >
</Button> Add address tag
</Button>
</Skeleton>
</Box> </Box>
<AddressModal { ...addressModalProps } onClose={ onAddressModalClose } data={ addressModalData }/> <AddressModal { ...addressModalProps } onClose={ onAddressModalClose } data={ addressModalData }/>
{ deleteModalData && ( { deleteModalData && (
......
...@@ -17,14 +17,15 @@ interface Props { ...@@ -17,14 +17,15 @@ interface Props {
address: Pick<Address, 'hash' | 'is_contract' | 'implementation_name' | 'watchlist_names' | 'watchlist_address_id'>; address: Pick<Address, 'hash' | 'is_contract' | 'implementation_name' | 'watchlist_names' | 'watchlist_address_id'>;
token?: TokenInfo | null; token?: TokenInfo | null;
isLinkDisabled?: boolean; isLinkDisabled?: boolean;
isLoading?: boolean;
} }
const AddressHeadingInfo = ({ address, token, isLinkDisabled }: Props) => { const AddressHeadingInfo = ({ address, token, isLinkDisabled, isLoading }: Props) => {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
return ( return (
<Flex alignItems="center"> <Flex alignItems="center">
<AddressIcon address={ address }/> <AddressIcon address={ address } isLoading={ isLoading }/>
<AddressLink <AddressLink
type="address" type="address"
hash={ address.hash } hash={ address.hash }
...@@ -33,13 +34,14 @@ const AddressHeadingInfo = ({ address, token, isLinkDisabled }: Props) => { ...@@ -33,13 +34,14 @@ const AddressHeadingInfo = ({ address, token, isLinkDisabled }: Props) => {
fontWeight={ 500 } fontWeight={ 500 }
truncation={ isMobile ? 'constant' : 'none' } truncation={ isMobile ? 'constant' : 'none' }
isDisabled={ isLinkDisabled } isDisabled={ isLinkDisabled }
isLoading={ isLoading }
/> />
<CopyToClipboard text={ address.hash }/> <CopyToClipboard text={ address.hash } isLoading={ isLoading }/>
{ address.is_contract && token && <AddressAddToWallet ml={ 2 } token={ token }/> } { !isLoading && address.is_contract && token && <AddressAddToWallet ml={ 2 } token={ token }/> }
{ !address.is_contract && config.isAccountSupported && ( { !isLoading && !address.is_contract && config.isAccountSupported && (
<AddressFavoriteButton hash={ address.hash } watchListId={ address.watchlist_address_id } ml={ 3 }/> <AddressFavoriteButton hash={ address.hash } watchListId={ address.watchlist_address_id } ml={ 3 }/>
) } ) }
<AddressQrCode hash={ address.hash } ml={ 2 }/> <AddressQrCode hash={ address.hash } ml={ 2 } isLoading={ isLoading }/>
</Flex> </Flex>
); );
}; };
......
...@@ -11,15 +11,16 @@ import CopyToClipboard from 'ui/shared/CopyToClipboard'; ...@@ -11,15 +11,16 @@ import CopyToClipboard from 'ui/shared/CopyToClipboard';
interface Props { interface Props {
address: AddressParam; address: AddressParam;
subtitle?: string; subtitle?: string;
isLoading?: boolean;
} }
const AddressSnippet = ({ address, subtitle }: Props) => { const AddressSnippet = ({ address, subtitle, isLoading }: Props) => {
return ( return (
<Box maxW="100%"> <Box maxW="100%">
<Address> <Address>
<AddressIcon address={ address }/> <AddressIcon address={ address } isLoading={ isLoading }/>
<AddressLink type="address" hash={ address.hash } fontWeight="600" ml={ 2 }/> <AddressLink type="address" hash={ address.hash } fontWeight="600" ml={ 2 } isLoading={ isLoading }/>
<CopyToClipboard text={ address.hash } ml={ 1 }/> <CopyToClipboard text={ address.hash } ml={ 1 } isLoading={ isLoading }/>
</Address> </Address>
{ subtitle && <Text fontSize="sm" variant="secondary" mt={ 0.5 } ml={ 8 }>{ subtitle }</Text> } { subtitle && <Text fontSize="sm" variant="secondary" mt={ 0.5 } ml={ 8 }>{ subtitle }</Text> }
</Box> </Box>
......
import { IconButton, Tooltip, useClipboard, chakra, useDisclosure } from '@chakra-ui/react'; import { IconButton, Tooltip, useClipboard, chakra, useDisclosure, Skeleton } from '@chakra-ui/react';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import CopyIcon from 'icons/copy.svg'; import CopyIcon from 'icons/copy.svg';
const CopyToClipboard = ({ text, className }: {text: string; className?: string}) => { interface Props {
text: string;
className?: string;
isLoading?: boolean;
}
const CopyToClipboard = ({ text, className, isLoading }: Props) => {
const { hasCopied, onCopy } = useClipboard(text, 1000); const { hasCopied, onCopy } = useClipboard(text, 1000);
const [ copied, setCopied ] = useState(false); const [ copied, setCopied ] = useState(false);
// have to implement controlled tooltip because of the issue - https://github.com/chakra-ui/chakra-ui/issues/7107 // have to implement controlled tooltip because of the issue - https://github.com/chakra-ui/chakra-ui/issues/7107
...@@ -17,6 +23,10 @@ const CopyToClipboard = ({ text, className }: {text: string; className?: string} ...@@ -17,6 +23,10 @@ const CopyToClipboard = ({ text, className }: {text: string; className?: string}
} }
}, [ hasCopied ]); }, [ hasCopied ]);
if (isLoading) {
return <Skeleton boxSize={ 5 } className={ className } borderRadius="sm" flexShrink={ 0 }/>;
}
return ( return (
<Tooltip label={ copied ? 'Copied' : 'Copy to clipboard' } isOpen={ isOpen || copied }> <Tooltip label={ copied ? 'Copied' : 'Copy to clipboard' } isOpen={ isOpen || copied }>
<IconButton <IconButton
......
import { GridItem, Flex, Text } from '@chakra-ui/react'; import { GridItem, Flex, Text, Skeleton } from '@chakra-ui/react';
import type { HTMLChakraProps } from '@chakra-ui/system'; import type { HTMLChakraProps } from '@chakra-ui/system';
import React from 'react'; import React from 'react';
...@@ -9,18 +9,21 @@ interface Props extends Omit<HTMLChakraProps<'div'>, 'title'> { ...@@ -9,18 +9,21 @@ interface Props extends Omit<HTMLChakraProps<'div'>, 'title'> {
hint: string; hint: string;
children: React.ReactNode; children: React.ReactNode;
note?: string; note?: string;
isLoading?: boolean;
} }
const DetailsInfoItem = ({ title, hint, note, children, id, ...styles }: Props) => { const DetailsInfoItem = ({ title, hint, note, children, id, isLoading, ...styles }: Props) => {
return ( return (
<> <>
<GridItem py={{ base: 1, lg: 2 }} id={ id } lineHeight={ 5 } { ...styles } whiteSpace="nowrap" _notFirst={{ mt: { base: 3, lg: 0 } }}> <GridItem py={{ base: 1, lg: 2 }} id={ id } lineHeight={ 5 } { ...styles } whiteSpace="nowrap" _notFirst={{ mt: { base: 3, lg: 0 } }}>
<Flex columnGap={ 2 } alignItems="flex-start"> <Flex columnGap={ 2 } alignItems="flex-start">
<Hint label={ hint }/> <Hint label={ hint } isLoading={ isLoading }/>
<Text fontWeight={{ base: 700, lg: 500 }}> <Skeleton isLoaded={ !isLoading }>
{ title } <Text fontWeight={{ base: 700, lg: 500 }}>
{ note && <Text fontWeight={ 500 } variant="secondary" fontSize="xs" className="note" align="right">{ note }</Text> } { title }
</Text> { note && <Text fontWeight={ 500 } variant="secondary" fontSize="xs" className="note" align="right">{ note }</Text> }
</Text>
</Skeleton>
</Flex> </Flex>
</GridItem> </GridItem>
<GridItem <GridItem
......
...@@ -7,18 +7,22 @@ import isSelfHosted from 'lib/isSelfHosted'; ...@@ -7,18 +7,22 @@ import isSelfHosted from 'lib/isSelfHosted';
import AdBanner from 'ui/shared/ad/AdBanner'; import AdBanner from 'ui/shared/ad/AdBanner';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem'; import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
const DetailsSponsoredItem = () => { interface Props {
isLoading?: boolean;
}
const DetailsSponsoredItem = ({ isLoading }: Props) => {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const hasAdblockCookie = cookies.get(cookies.NAMES.ADBLOCK_DETECTED); const hasAdblockCookie = cookies.get(cookies.NAMES.ADBLOCK_DETECTED);
if (hasAdblockCookie || !isSelfHosted()) { if (!isSelfHosted() || hasAdblockCookie) {
return null; return null;
} }
if (isMobile) { if (isMobile) {
return ( return (
<GridItem mt={ 5 }> <GridItem mt={ 5 }>
<AdBanner justifyContent="center"/> <AdBanner mx="auto" isLoading={ isLoading } display="flex" justifyContent="center"/>
</GridItem> </GridItem>
); );
} }
...@@ -27,8 +31,9 @@ const DetailsSponsoredItem = () => { ...@@ -27,8 +31,9 @@ const DetailsSponsoredItem = () => {
<DetailsInfoItem <DetailsInfoItem
title="Sponsored" title="Sponsored"
hint="Sponsored banner advertisement" hint="Sponsored banner advertisement"
isLoading={ isLoading }
> >
<AdBanner/> <AdBanner isLoading={ isLoading }/>
</DetailsInfoItem> </DetailsInfoItem>
); );
}; };
......
import type { TooltipProps } from '@chakra-ui/react'; import type { TooltipProps } from '@chakra-ui/react';
import { chakra, IconButton, Tooltip, useDisclosure } from '@chakra-ui/react'; import { chakra, IconButton, Tooltip, useDisclosure, Skeleton } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import InfoIcon from 'icons/info.svg'; import InfoIcon from 'icons/info.svg';
...@@ -8,9 +8,10 @@ interface Props { ...@@ -8,9 +8,10 @@ interface Props {
label: string | React.ReactNode; label: string | React.ReactNode;
className?: string; className?: string;
tooltipProps?: Partial<TooltipProps>; tooltipProps?: Partial<TooltipProps>;
isLoading?: boolean;
} }
const Hint = ({ label, className, tooltipProps }: Props) => { const Hint = ({ label, className, tooltipProps, isLoading }: Props) => {
// have to implement controlled tooltip because of the issue - https://github.com/chakra-ui/chakra-ui/issues/7107 // have to implement controlled tooltip because of the issue - https://github.com/chakra-ui/chakra-ui/issues/7107
const { isOpen, onOpen, onToggle, onClose } = useDisclosure(); const { isOpen, onOpen, onToggle, onClose } = useDisclosure();
...@@ -19,6 +20,10 @@ const Hint = ({ label, className, tooltipProps }: Props) => { ...@@ -19,6 +20,10 @@ const Hint = ({ label, className, tooltipProps }: Props) => {
onToggle(); onToggle();
}, [ onToggle ]); }, [ onToggle ]);
if (isLoading) {
return <Skeleton boxSize={ 5 } borderRadius="sm"/>;
}
return ( return (
<Tooltip <Tooltip
label={ label } label={ label }
......
...@@ -26,6 +26,8 @@ const ListItemMobile = ({ children, className, isAnimated }: Props) => { ...@@ -26,6 +26,8 @@ const ListItemMobile = ({ children, className, isAnimated }: Props) => {
borderBottomWidth: '1px', borderBottomWidth: '1px',
}} }}
className={ className } className={ className }
fontSize="16px"
lineHeight="20px"
> >
{ children } { children }
</Flex> </Flex>
......
import { Heading, Flex, Grid, Tooltip, Icon, chakra } from '@chakra-ui/react'; import { Heading, Flex, Grid, Tooltip, Icon, chakra, Skeleton } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import eastArrowIcon from 'icons/arrows/east.svg'; import eastArrowIcon from 'icons/arrows/east.svg';
...@@ -13,18 +13,21 @@ type Props = { ...@@ -13,18 +13,21 @@ type Props = {
className?: string; className?: string;
backLinkLabel?: string; backLinkLabel?: string;
backLinkUrl?: string; backLinkUrl?: string;
isLoading?: boolean;
} }
const PageTitle = ({ text, additionalsLeft, additionalsRight, withTextAd, backLinkUrl, backLinkLabel, className }: Props) => { const PageTitle = ({ text, additionalsLeft, additionalsRight, withTextAd, backLinkUrl, backLinkLabel, className, isLoading }: Props) => {
const title = ( const title = (
<Heading <Skeleton isLoaded={ !isLoading }>
as="h1" <Heading
size="lg" as="h1"
flex="none" size="lg"
wordBreak="break-word" flex="none"
> wordBreak="break-word"
{ text } >
</Heading> { text }
</Heading>
</Skeleton>
); );
return ( return (
......
import { Box, Flex, Text, chakra, useColorModeValue } from '@chakra-ui/react'; import { Box, Flex, chakra, useColorModeValue, Skeleton } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import CopyToClipboard from './CopyToClipboard'; import CopyToClipboard from './CopyToClipboard';
...@@ -11,33 +11,36 @@ interface Props { ...@@ -11,33 +11,36 @@ interface Props {
beforeSlot?: React.ReactNode; beforeSlot?: React.ReactNode;
textareaMaxHeight?: string; textareaMaxHeight?: string;
showCopy?: boolean; showCopy?: boolean;
isLoading?: boolean;
} }
const RawDataSnippet = ({ data, className, title, rightSlot, beforeSlot, textareaMaxHeight, showCopy = true }: Props) => { const RawDataSnippet = ({ data, className, title, rightSlot, beforeSlot, textareaMaxHeight, showCopy = true, isLoading }: Props) => {
// see issue in theme/components/Textarea.ts // see issue in theme/components/Textarea.ts
const bgColor = useColorModeValue('#f5f5f6', '#1a1b1b'); const bgColor = useColorModeValue('#f5f5f6', '#1a1b1b');
return ( return (
<Box className={ className } as="section" title={ title }> <Box className={ className } as="section" title={ title }>
{ (title || rightSlot || showCopy) && ( { (title || rightSlot || showCopy) && (
<Flex justifyContent={ title ? 'space-between' : 'flex-end' } alignItems="center" mb={ 3 }> <Flex justifyContent={ title ? 'space-between' : 'flex-end' } alignItems="center" mb={ 3 }>
{ title && <Text fontWeight={ 500 }>{ title }</Text> } { title && <Skeleton fontWeight={ 500 } isLoaded={ !isLoading }>{ title }</Skeleton> }
{ rightSlot } { rightSlot }
{ typeof data === 'string' && showCopy && <CopyToClipboard text={ data }/> } { typeof data === 'string' && showCopy && <CopyToClipboard text={ data } isLoading={ isLoading }/> }
</Flex> </Flex>
) } ) }
{ beforeSlot } { beforeSlot }
<Box <Skeleton
p={ 4 } p={ 4 }
bgColor={ bgColor } bgColor={ isLoading ? 'inherit' : bgColor }
maxH={ textareaMaxHeight || '400px' } maxH={ textareaMaxHeight || '400px' }
minH={ isLoading ? '200px' : undefined }
fontSize="sm" fontSize="sm"
borderRadius="md" borderRadius="md"
wordBreak="break-all" wordBreak="break-all"
whiteSpace="pre-wrap" whiteSpace="pre-wrap"
overflowY="auto" overflowY="auto"
isLoaded={ !isLoading }
> >
{ data } { data }
</Box> </Skeleton>
</Box> </Box>
); );
}; };
......
...@@ -175,6 +175,7 @@ const RoutedTabs = ({ tabs, tabListProps, rightSlot, stickyEnabled, className, . ...@@ -175,6 +175,7 @@ const RoutedTabs = ({ tabs, tabListProps, rightSlot, stickyEnabled, className, .
} }
onItemClick={ handleTabChange } onItemClick={ handleTabChange }
buttonRef={ tabsRefs[index] } buttonRef={ tabsRefs[index] }
size={ themeProps.size || 'md' }
/> />
); );
} }
......
import type {
ButtonProps } from '@chakra-ui/react';
import { Popover, import { Popover,
PopoverTrigger, PopoverTrigger,
PopoverContent, PopoverContent,
...@@ -20,9 +22,10 @@ interface Props { ...@@ -20,9 +22,10 @@ interface Props {
styles?: StyleProps; styles?: StyleProps;
onItemClick: (index: number) => void; onItemClick: (index: number) => void;
buttonRef: React.RefObject<HTMLButtonElement>; buttonRef: React.RefObject<HTMLButtonElement>;
size: ButtonProps['size'];
} }
const RoutedTabsMenu = ({ tabs, tabsCut, isActive, styles, onItemClick, buttonRef, activeTab }: Props) => { const RoutedTabsMenu = ({ tabs, tabsCut, isActive, styles, onItemClick, buttonRef, activeTab, size }: Props) => {
const { isOpen, onClose, onOpen } = useDisclosure(); const { isOpen, onClose, onOpen } = useDisclosure();
const handleItemClick = React.useCallback((event: React.MouseEvent<HTMLButtonElement>) => { const handleItemClick = React.useCallback((event: React.MouseEvent<HTMLButtonElement>) => {
...@@ -40,6 +43,7 @@ const RoutedTabsMenu = ({ tabs, tabsCut, isActive, styles, onItemClick, buttonRe ...@@ -40,6 +43,7 @@ const RoutedTabsMenu = ({ tabs, tabsCut, isActive, styles, onItemClick, buttonRe
variant="ghost" variant="ghost"
isActive={ isOpen || isActive } isActive={ isOpen || isActive }
ref={ buttonRef } ref={ buttonRef }
size={ size }
{ ...styles } { ...styles }
> >
{ menuButton.title } { menuButton.title }
......
import { Alert, Link, Text, chakra, useTheme, useColorModeValue } from '@chakra-ui/react'; import { Alert, Link, Text, chakra, useTheme, useColorModeValue, Skeleton, Tr, Td } from '@chakra-ui/react';
import { transparentize } from '@chakra-ui/theme-tools'; import { transparentize } from '@chakra-ui/theme-tools';
import React from 'react'; import React from 'react';
...@@ -13,9 +13,10 @@ interface Props { ...@@ -13,9 +13,10 @@ interface Props {
url: string; url: string;
alert?: string; alert?: string;
num?: number; num?: number;
isLoading?: boolean;
} }
const SocketNewItemsNotice = ({ children, className, url, num, alert, type = 'transaction' }: Props) => { const SocketNewItemsNotice = chakra(({ children, className, url, num, alert, type = 'transaction', isLoading }: Props) => {
const theme = useTheme(); const theme = useTheme();
const alertContent = (() => { const alertContent = (() => {
...@@ -49,7 +50,10 @@ const SocketNewItemsNotice = ({ children, className, url, num, alert, type = 'tr ...@@ -49,7 +50,10 @@ const SocketNewItemsNotice = ({ children, className, url, num, alert, type = 'tr
); );
})(); })();
const content = ( const color = useColorModeValue('blackAlpha.800', 'whiteAlpha.800');
const bgColor = useColorModeValue('orange.50', transparentize('orange.200', 0.16)(theme));
const content = !isLoading ? (
<Alert <Alert
className={ className } className={ className }
status="warning" status="warning"
...@@ -57,14 +61,39 @@ const SocketNewItemsNotice = ({ children, className, url, num, alert, type = 'tr ...@@ -57,14 +61,39 @@ const SocketNewItemsNotice = ({ children, className, url, num, alert, type = 'tr
py="6px" py="6px"
fontWeight={ 400 } fontWeight={ 400 }
fontSize="sm" fontSize="sm"
bgColor={ useColorModeValue('orange.50', transparentize('orange.200', 0.16)(theme)) } bgColor={ bgColor }
color={ useColorModeValue('blackAlpha.800', 'whiteAlpha.800') } color={ color }
> >
{ alertContent } { alertContent }
</Alert> </Alert>
); ) : <Skeleton className={ className } h="33px"/>;
return children ? children({ content }) : content; return children ? children({ content }) : content;
});
export default SocketNewItemsNotice;
export const Desktop = ({ ...props }: Props) => {
return (
<SocketNewItemsNotice
borderRadius={ props.isLoading ? 'sm' : 0 }
h={ props.isLoading ? 4 : 'auto' }
maxW={ props.isLoading ? '215px' : undefined }
w="100%"
mx={ props.isLoading ? 4 : 0 }
my={ props.isLoading ? '6px' : 0 }
{ ...props }
>
{ ({ content }) => <Tr><Td colSpan={ 100 } p={ 0 }>{ content }</Td></Tr> }
</SocketNewItemsNotice>
);
}; };
export default chakra(SocketNewItemsNotice); export const Mobile = ({ ...props }: Props) => {
return (
<SocketNewItemsNotice
borderBottomRadius={ 0 }
{ ...props }
/>
);
};
import { Tooltip, IconButton, Icon, HStack } from '@chakra-ui/react'; import { Tooltip, IconButton, HStack, Skeleton } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import DeleteIcon from 'icons/delete.svg'; import DeleteIcon from 'icons/delete.svg';
...@@ -8,33 +8,47 @@ import usePreventFocusAfterModalClosing from 'lib/hooks/usePreventFocusAfterModa ...@@ -8,33 +8,47 @@ import usePreventFocusAfterModalClosing from 'lib/hooks/usePreventFocusAfterModa
type Props = { type Props = {
onEditClick: () => void; onEditClick: () => void;
onDeleteClick: () => void; onDeleteClick: () => void;
isLoading?: boolean;
} }
const TableItemActionButtons = ({ onEditClick, onDeleteClick }: Props) => { const TableItemActionButtons = ({ onEditClick, onDeleteClick, isLoading }: Props) => {
const onFocusCapture = usePreventFocusAfterModalClosing(); const onFocusCapture = usePreventFocusAfterModalClosing();
if (isLoading) {
return (
<HStack spacing={ 6 } alignSelf="flex-end">
<Skeleton boxSize={ 5 } flexShrink={ 0 } borderRadius="base"/>
<Skeleton boxSize={ 5 } flexShrink={ 0 } borderRadius="base"/>
</HStack>
);
}
return ( return (
<HStack spacing={ 6 } alignSelf="flex-end"> <HStack spacing={ 6 } alignSelf="flex-end">
<Tooltip label="Edit"> <Tooltip label="Edit">
<IconButton <IconButton
aria-label="edit" aria-label="edit"
variant="simple" variant="simple"
w="30px" boxSize={ 5 }
h="30px"
onClick={ onEditClick } onClick={ onEditClick }
icon={ <Icon as={ EditIcon } w="20px" h="20px"/> } icon={ <EditIcon/> }
onFocusCapture={ onFocusCapture } onFocusCapture={ onFocusCapture }
display="inline-block"
flexShrink={ 0 }
borderRadius="none"
/> />
</Tooltip> </Tooltip>
<Tooltip label="Delete"> <Tooltip label="Delete">
<IconButton <IconButton
aria-label="delete" aria-label="delete"
variant="simple" variant="simple"
w="30px" boxSize={ 5 }
h="30px"
onClick={ onDeleteClick } onClick={ onDeleteClick }
icon={ <Icon as={ DeleteIcon } w="20px" h="20px"/> } icon={ <DeleteIcon/> }
onFocusCapture={ onFocusCapture } onFocusCapture={ onFocusCapture }
display="inline-block"
flexShrink={ 0 }
borderRadius="none"
/> />
</Tooltip> </Tooltip>
</HStack> </HStack>
......
import { Image, chakra, useColorModeValue, Icon } from '@chakra-ui/react'; import { Image, chakra, useColorModeValue, Icon, Skeleton } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
...@@ -27,9 +27,15 @@ interface Props { ...@@ -27,9 +27,15 @@ interface Props {
hash?: string; hash?: string;
name?: string | null; name?: string | null;
className?: string; className?: string;
isLoading?: boolean;
} }
const TokenLogo = ({ hash, name, className }: Props) => { const TokenLogo = ({ hash, name, className, isLoading }: Props) => {
if (isLoading) {
return <Skeleton className={ className } borderRadius="base"/>;
}
const logoSrc = appConfig.network.assetsPathname && hash ? [ const logoSrc = appConfig.network.assetsPathname && hash ? [
'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/', 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/',
appConfig.network.assetsPathname, appConfig.network.assetsPathname,
......
import { Box, Icon, chakra } from '@chakra-ui/react'; import { Box, Icon, chakra, Skeleton } from '@chakra-ui/react';
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
...@@ -13,10 +13,11 @@ interface Props { ...@@ -13,10 +13,11 @@ interface Props {
className?: string; className?: string;
isDisabled?: boolean; isDisabled?: boolean;
truncation?: 'dynamic' | 'constant'; truncation?: 'dynamic' | 'constant';
isLoading?: boolean;
} }
const TokenTransferNft = ({ hash, id, className, isDisabled, truncation = 'dynamic' }: Props) => { const TokenTransferNft = ({ hash, id, className, isDisabled, isLoading, truncation = 'dynamic' }: Props) => {
const Component = isDisabled ? Box : LinkInternal; const Component = isDisabled || isLoading ? Box : LinkInternal;
return ( return (
<Component <Component
...@@ -28,10 +29,12 @@ const TokenTransferNft = ({ hash, id, className, isDisabled, truncation = 'dynam ...@@ -28,10 +29,12 @@ const TokenTransferNft = ({ hash, id, className, isDisabled, truncation = 'dynam
w="100%" w="100%"
className={ className } className={ className }
> >
<Icon as={ nftPlaceholder } boxSize="30px" mr={ 1 } color="inherit"/> <Skeleton isLoaded={ !isLoading } boxSize="30px" mr={ 1 } borderRadius="base">
<Box maxW="calc(100% - 34px)"> <Icon as={ nftPlaceholder } boxSize="30px" color="inherit"/>
</Skeleton>
<Skeleton isLoaded={ !isLoading } maxW="calc(100% - 34px)">
{ truncation === 'constant' ? <HashStringShorten hash={ id }/> : <HashStringShortenDynamic hash={ id } fontWeight={ 500 }/> } { truncation === 'constant' ? <HashStringShorten hash={ id }/> : <HashStringShortenDynamic hash={ id } fontWeight={ 500 }/> }
</Box> </Skeleton>
</Component> </Component>
); );
}; };
......
import { Box, Flex, Text, chakra, useColorModeValue } from '@chakra-ui/react'; import { Box, Flex, chakra, useColorModeValue, Skeleton } from '@chakra-ui/react';
import clamp from 'lodash/clamp'; import clamp from 'lodash/clamp';
import React from 'react'; import React from 'react';
...@@ -6,21 +6,28 @@ interface Props { ...@@ -6,21 +6,28 @@ interface Props {
className?: string; className?: string;
value: number; value: number;
colorScheme?: 'green' | 'gray'; colorScheme?: 'green' | 'gray';
isLoading?: boolean;
} }
const WIDTH = 50; const WIDTH = 50;
const Utilization = ({ className, value, colorScheme = 'green' }: Props) => { const Utilization = ({ className, value, colorScheme = 'green', isLoading }: Props) => {
const valueString = (clamp(value * 100 || 0, 0, 100)).toLocaleString(undefined, { maximumFractionDigits: 2 }) + '%'; const valueString = (clamp(value * 100 || 0, 0, 100)).toLocaleString(undefined, { maximumFractionDigits: 2 }) + '%';
const colorGrayScheme = useColorModeValue('gray.500', 'gray.400'); const colorGrayScheme = useColorModeValue('gray.500', 'gray.400');
const color = colorScheme === 'gray' ? colorGrayScheme : 'green.500'; const color = colorScheme === 'gray' ? colorGrayScheme : 'green.500';
return ( return (
<Flex className={ className } alignItems="center"> <Flex className={ className } alignItems="center" columnGap="10px">
<Box bg={ useColorModeValue('blackAlpha.200', 'whiteAlpha.200') } w={ `${ WIDTH }px` } h="4px" borderRadius="full" overflow="hidden"> <Skeleton isLoaded={ !isLoading } w={ `${ WIDTH }px` } h="4px" borderRadius="full" overflow="hidden">
<Box bg={ color } w={ valueString } h="100%"/> <Box bg={ useColorModeValue('blackAlpha.200', 'whiteAlpha.200') } h="100%">
</Box> <Box bg={ color } w={ valueString } h="100%"/>
<Text color={ color } ml="10px" fontWeight="bold">{ valueString }</Text> </Box>
</Skeleton>
<Skeleton isLoaded={ !isLoading } color={ color } fontWeight="bold">
<span>
{ valueString }
</span>
</Skeleton>
</Flex> </Flex>
); );
}; };
......
import { chakra } from '@chakra-ui/react'; import { chakra, Skeleton } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
...@@ -9,18 +9,26 @@ import isSelfHosted from 'lib/isSelfHosted'; ...@@ -9,18 +9,26 @@ import isSelfHosted from 'lib/isSelfHosted';
import AdbutlerBanner from './AdbutlerBanner'; import AdbutlerBanner from './AdbutlerBanner';
import CoinzillaBanner from './CoinzillaBanner'; import CoinzillaBanner from './CoinzillaBanner';
const AdBanner = ({ className }: { className?: string }) => { const AdBanner = ({ className, isLoading }: { className?: string; isLoading?: boolean }) => {
const hasAdblockCookie = cookies.get(cookies.NAMES.ADBLOCK_DETECTED, useAppContext().cookies); const hasAdblockCookie = cookies.get(cookies.NAMES.ADBLOCK_DETECTED, useAppContext().cookies);
if (!isSelfHosted() || hasAdblockCookie) { if (!isSelfHosted() || hasAdblockCookie) {
return null; return null;
} }
if (appConfig.ad.adButlerOn) { const content = appConfig.ad.adButlerOn ? <AdbutlerBanner/> : <CoinzillaBanner/>;
return <AdbutlerBanner className={ className }/>;
}
return <CoinzillaBanner className={ className }/>; return (
<Skeleton
className={ className }
isLoaded={ !isLoading }
borderRadius="none"
maxW={ appConfig.ad.adButlerOn ? '760px' : '728px' }
w="100%"
>
{ content }
</Skeleton>
);
}; };
export default chakra(AdBanner); export default chakra(AdBanner);
import { Box, Image, Link, Text, chakra } from '@chakra-ui/react'; import { Box, Image, Link, Text, chakra, Skeleton } from '@chakra-ui/react';
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import { useAppContext } from 'lib/appContext'; import { useAppContext } from 'lib/appContext';
...@@ -55,7 +55,7 @@ const CoinzillaTextAd = ({ className }: {className?: string}) => { ...@@ -55,7 +55,7 @@ const CoinzillaTextAd = ({ className }: {className?: string}) => {
} }
if (isLoading) { if (isLoading) {
return <Box className={ className } h={{ base: 12, lg: 6 }}/>; return <Skeleton className={ className } h={{ base: 12, lg: 6 }} maxW="1000px"/>;
} }
if (!adData) { if (!adData) {
......
import { Box, chakra, Tooltip } from '@chakra-ui/react'; import { Box, chakra, Skeleton, Tooltip } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import Jazzicon, { jsNumberForAddress } from 'react-jazzicon'; import Jazzicon, { jsNumberForAddress } from 'react-jazzicon';
...@@ -9,9 +9,14 @@ import AddressContractIcon from 'ui/shared/address/AddressContractIcon'; ...@@ -9,9 +9,14 @@ import AddressContractIcon from 'ui/shared/address/AddressContractIcon';
type Props = { type Props = {
address: Pick<AddressParam, 'hash' | 'is_contract' | 'implementation_name'>; address: Pick<AddressParam, 'hash' | 'is_contract' | 'implementation_name'>;
className?: string; className?: string;
isLoading?: boolean;
} }
const AddressIcon = ({ address, className }: Props) => { const AddressIcon = ({ address, className, isLoading }: Props) => {
if (isLoading) {
return <Skeleton boxSize={ 6 } className={ className } borderRadius="full" flexShrink={ 0 }/>;
}
if (address.is_contract) { if (address.is_contract) {
return ( return (
<AddressContractIcon className={ className }/> <AddressContractIcon className={ className }/>
...@@ -20,7 +25,7 @@ const AddressIcon = ({ address, className }: Props) => { ...@@ -20,7 +25,7 @@ const AddressIcon = ({ address, className }: Props) => {
return ( return (
<Tooltip label={ address.implementation_name }> <Tooltip label={ address.implementation_name }>
<Box className={ className } width="24px" display="inline-flex"> <Box className={ className } boxSize={ 6 } display="inline-flex">
<Jazzicon diameter={ 24 } seed={ jsNumberForAddress(address.hash) }/> <Jazzicon diameter={ 24 } seed={ jsNumberForAddress(address.hash) }/>
</Box> </Box>
</Tooltip> </Tooltip>
......
import { chakra, shouldForwardProp, Tooltip, Box } from '@chakra-ui/react'; import { chakra, shouldForwardProp, Tooltip, Box, Skeleton } from '@chakra-ui/react';
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
import type { HTMLAttributeAnchorTarget } from 'react'; import type { HTMLAttributeAnchorTarget } from 'react';
import React from 'react'; import React from 'react';
...@@ -17,6 +17,7 @@ type CommonProps = { ...@@ -17,6 +17,7 @@ type CommonProps = {
isDisabled?: boolean; isDisabled?: boolean;
fontWeight?: string; fontWeight?: string;
alias?: string | null; alias?: string | null;
isLoading?: boolean;
} }
type AddressTokenTxProps = { type AddressTokenTxProps = {
...@@ -39,7 +40,7 @@ type AddressTokenProps = { ...@@ -39,7 +40,7 @@ type AddressTokenProps = {
type Props = CommonProps & (AddressTokenTxProps | BlockProps | AddressTokenProps); type Props = CommonProps & (AddressTokenTxProps | BlockProps | AddressTokenProps);
const AddressLink = (props: Props) => { const AddressLink = (props: Props) => {
const { alias, type, className, truncation = 'dynamic', hash, fontWeight, target = '_self', isDisabled } = props; const { alias, type, className, truncation = 'dynamic', hash, fontWeight, target = '_self', isDisabled, isLoading } = props;
const isMobile = useIsMobile(); const isMobile = useIsMobile();
let url; let url;
...@@ -81,6 +82,10 @@ const AddressLink = (props: Props) => { ...@@ -81,6 +82,10 @@ const AddressLink = (props: Props) => {
} }
})(); })();
if (isLoading) {
return <Skeleton className={ className } overflow="hidden" whiteSpace="nowrap">{ content }</Skeleton>;
}
if (isDisabled) { if (isDisabled) {
return ( return (
<chakra.span <chakra.span
......
import { Skeleton, Tag as ChakraTag } from '@chakra-ui/react';
import type { TagProps } from '@chakra-ui/react';
import React from 'react';
import TruncatedTextTooltip from 'ui/shared/TruncatedTextTooltip';
interface Props extends TagProps {
isLoading?: boolean;
}
const Tag = ({ isLoading, ...props }: Props) => {
if (props.isTruncated && typeof props.children === 'string') {
if (!props.children) {
return null;
}
return (
<Skeleton isLoaded={ !isLoading } display="inline-block" borderRadius="sm" maxW="100%">
<TruncatedTextTooltip label={ props.children }>
<ChakraTag { ...props }/>
</TruncatedTextTooltip>
</Skeleton>
);
}
return (
<Skeleton isLoaded={ !isLoading } display="inline-block" borderRadius="sm" maxW="100%">
<ChakraTag { ...props }/>
</Skeleton>
);
};
export default React.memo(Tag);
...@@ -11,13 +11,14 @@ interface Props { ...@@ -11,13 +11,14 @@ interface Props {
imageUrl: string | null; imageUrl: string | null;
animationUrl: string | null; animationUrl: string | null;
className?: string; className?: string;
isLoading?: boolean;
} }
const NftMedia = ({ imageUrl, animationUrl, className }: Props) => { const NftMedia = ({ imageUrl, animationUrl, className, isLoading }: Props) => {
const [ type, setType ] = React.useState<MediaType | undefined>(!animationUrl ? 'image' : undefined); const [ type, setType ] = React.useState<MediaType | undefined>(!animationUrl ? 'image' : undefined);
React.useEffect(() => { React.useEffect(() => {
if (!animationUrl) { if (!animationUrl || isLoading) {
return; return;
} }
...@@ -45,9 +46,9 @@ const NftMedia = ({ imageUrl, animationUrl, className }: Props) => { ...@@ -45,9 +46,9 @@ const NftMedia = ({ imageUrl, animationUrl, className }: Props) => {
setType('image'); setType('image');
}); });
}, [ animationUrl ]); }, [ animationUrl, isLoading ]);
if (!type) { if (!type || isLoading) {
return ( return (
<AspectRatio <AspectRatio
className={ className } className={ className }
......
import { Flex, Skeleton, chakra } from '@chakra-ui/react'; import { Flex, Skeleton, chakra } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { RoutedTab } from '../RoutedTabs/types';
interface Props { interface Props {
className?: string; className?: string;
tabs?: Array<RoutedTab>;
size?: 'sm' | 'md';
} }
const SkeletonTabs = ({ className }: Props) => { const SkeletonTabs = ({ className, tabs, size = 'md' }: Props) => {
if (tabs) {
if (tabs.length === 1) {
return null;
}
const paddingHor = size === 'sm' ? 3 : 4;
const paddingVert = size === 'sm' ? 1 : 2;
return (
<Flex className={ className } my={ 8 } alignItems="center">
{ tabs.map(({ title, id }, index) => (
<Skeleton
key={ id }
py={ index === 0 ? paddingVert : 0 }
px={ index === 0 ? paddingHor : 0 }
mx={ index === 0 ? 0 : paddingHor }
borderRadius="base"
fontWeight={ 600 }
borderWidth={ size === 'sm' ? '2px' : 0 }
>
{ typeof title === 'string' ? title : title() }
</Skeleton>
)) }
</Flex>
);
}
return ( return (
<Flex my={ 8 } className={ className }> <Flex my={ 8 } className={ className }>
<Skeleton h={ 6 } my={ 2 } mx={ 4 } w="100px"/> <Skeleton h={ 6 } my={ 2 } mx={ 4 } w="100px"/>
......
import { Flex, Skeleton, SkeletonCircle } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query'; import type { UseQueryResult } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import type { Address } from 'types/api/address';
import type { TokenInfo } from 'types/api/token'; import type { TokenInfo } from 'types/api/token';
import useApiQuery from 'lib/api/useApiQuery';
import AddressHeadingInfo from 'ui/shared/AddressHeadingInfo'; import AddressHeadingInfo from 'ui/shared/AddressHeadingInfo';
interface Props { interface Props {
tokenQuery: UseQueryResult<TokenInfo>; tokenQuery: UseQueryResult<TokenInfo>;
contractQuery: UseQueryResult<Address>;
} }
const TokenContractInfo = ({ tokenQuery }: Props) => { const TokenContractInfo = ({ tokenQuery, contractQuery }: Props) => {
const router = useRouter();
const contractQuery = useApiQuery('address', {
pathParams: { hash: router.query.hash?.toString() },
queryOptions: { enabled: Boolean(router.query.hash) },
});
if (tokenQuery.isLoading || contractQuery.isLoading) {
return (
<Flex alignItems="center">
<SkeletonCircle boxSize={ 6 }/>
<Skeleton w="400px" h={ 5 } ml={ 2 }/>
<Skeleton w={ 5 } h={ 5 } ml={ 1 }/>
<Skeleton w={ 9 } h={ 8 } ml={ 2 }/>
<Skeleton w={ 9 } h={ 8 } ml={ 2 }/>
</Flex>
);
}
// we show error in parent component, this is only for TS // we show error in parent component, this is only for TS
if (tokenQuery.isError) { if (tokenQuery.isError) {
return null; return null;
} }
const address = { const address = {
hash: tokenQuery.data.address, hash: tokenQuery.data?.address || '',
is_contract: true, is_contract: true,
implementation_name: null, implementation_name: null,
watchlist_names: [], watchlist_names: [],
watchlist_address_id: null, watchlist_address_id: null,
}; };
return <AddressHeadingInfo address={ address } token={ contractQuery.data?.token }/>; return (
<AddressHeadingInfo
address={ address }
token={ contractQuery.data?.token }
isLoading={ tokenQuery.isPlaceholderData || contractQuery.isPlaceholderData }
/>
);
}; };
export default React.memo(TokenContractInfo); export default React.memo(TokenContractInfo);
...@@ -8,11 +8,11 @@ import type { TokenInfo } from 'types/api/token'; ...@@ -8,11 +8,11 @@ import type { TokenInfo } from 'types/api/token';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import getCurrencyValue from 'lib/getCurrencyValue'; import getCurrencyValue from 'lib/getCurrencyValue';
import { TOKEN_COUNTERS } from 'stubs/token';
import type { TokenTabs } from 'ui/pages/Token'; import type { TokenTabs } from 'ui/pages/Token';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem'; import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import DetailsSponsoredItem from 'ui/shared/DetailsSponsoredItem'; import DetailsSponsoredItem from 'ui/shared/DetailsSponsoredItem';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic'; import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import DetailsSkeletonRow from 'ui/shared/skeletons/DetailsSkeletonRow';
interface Props { interface Props {
tokenQuery: UseQueryResult<TokenInfo>; tokenQuery: UseQueryResult<TokenInfo>;
...@@ -23,7 +23,7 @@ const TokenDetails = ({ tokenQuery }: Props) => { ...@@ -23,7 +23,7 @@ const TokenDetails = ({ tokenQuery }: Props) => {
const tokenCountersQuery = useApiQuery('token_counters', { const tokenCountersQuery = useApiQuery('token_counters', {
pathParams: { hash: router.query.hash?.toString() }, pathParams: { hash: router.query.hash?.toString() },
queryOptions: { enabled: Boolean(router.query.hash) }, queryOptions: { enabled: Boolean(router.query.hash), placeholderData: TOKEN_COUNTERS },
}); });
const changeUrlAndScroll = useCallback((tab: TokenTabs) => () => { const changeUrlAndScroll = useCallback((tab: TokenTabs) => () => {
...@@ -56,32 +56,19 @@ const TokenDetails = ({ tokenQuery }: Props) => { ...@@ -56,32 +56,19 @@ const TokenDetails = ({ tokenQuery }: Props) => {
throw Error('Token fetch error', { cause: tokenQuery.error as unknown as Error }); throw Error('Token fetch error', { cause: tokenQuery.error as unknown as Error });
} }
if (tokenQuery.isLoading) {
return (
<Grid mt={ 10 } columnGap={ 8 } rowGap={{ base: 5, lg: 7 }} templateColumns={{ base: '1fr', lg: '210px 1fr' }} maxW="1000px">
<DetailsSkeletonRow w="10%"/>
<DetailsSkeletonRow w="30%"/>
<DetailsSkeletonRow w="30%"/>
<DetailsSkeletonRow w="20%"/>
<DetailsSkeletonRow w="20%"/>
<DetailsSkeletonRow w="10%"/>
</Grid>
);
}
const { const {
exchange_rate: exchangeRate, exchange_rate: exchangeRate,
total_supply: totalSupply, total_supply: totalSupply,
decimals, decimals,
symbol, symbol,
type, type,
} = tokenQuery.data; } = tokenQuery.data || {};
let marketcap; let marketcap;
let totalSupplyValue; let totalSupplyValue;
if (type === 'ERC-20') { if (type === 'ERC-20') {
const totalValue = totalSupply !== null ? getCurrencyValue({ value: totalSupply, accuracy: 3, accuracyUsd: 2, exchangeRate, decimals }) : undefined; const totalValue = totalSupply ? getCurrencyValue({ value: totalSupply, accuracy: 3, accuracyUsd: 2, exchangeRate, decimals }) : undefined;
marketcap = totalValue?.usd; marketcap = totalValue?.usd;
totalSupplyValue = totalValue?.valueStr; totalSupplyValue = totalValue?.valueStr;
} else { } else {
...@@ -119,40 +106,50 @@ const TokenDetails = ({ tokenQuery }: Props) => { ...@@ -119,40 +106,50 @@ const TokenDetails = ({ tokenQuery }: Props) => {
alignSelf="center" alignSelf="center"
wordBreak="break-word" wordBreak="break-word"
whiteSpace="pre-wrap" whiteSpace="pre-wrap"
isLoading={ tokenQuery.isPlaceholderData }
> >
<Flex w="100%"> <Skeleton isLoaded={ !tokenQuery.isPlaceholderData }>
<Box whiteSpace="nowrap" overflow="hidden"> <Flex w="100%">
<HashStringShortenDynamic hash={ totalSupplyValue || '0' }/> <Box whiteSpace="nowrap" overflow="hidden">
</Box> <HashStringShortenDynamic hash={ totalSupplyValue || '0' }/>
<Box flexShrink={ 0 }> { symbol || '' }</Box> </Box>
</Flex> <Box flexShrink={ 0 }> { symbol || '' }</Box>
</Flex>
</Skeleton>
</DetailsInfoItem> </DetailsInfoItem>
<DetailsInfoItem <DetailsInfoItem
title="Holders" title="Holders"
hint="Number of accounts holding the token" hint="Number of accounts holding the token"
alignSelf="center" alignSelf="center"
isLoading={ tokenQuery.isPlaceholderData }
> >
{ tokenCountersQuery.isLoading && <Skeleton w={ 20 } h={ 4 }/> } <Skeleton isLoaded={ !tokenCountersQuery.isPlaceholderData }>
{ !tokenCountersQuery.isLoading && countersItem('token_holders_count') } { countersItem('token_holders_count') }
</Skeleton>
</DetailsInfoItem> </DetailsInfoItem>
<DetailsInfoItem <DetailsInfoItem
title="Transfers" title="Transfers"
hint="Number of transfer for the token" hint="Number of transfer for the token"
alignSelf="center" alignSelf="center"
isLoading={ tokenQuery.isPlaceholderData }
> >
{ tokenCountersQuery.isLoading && <Skeleton w={ 20 } h={ 4 }/> } <Skeleton isLoaded={ !tokenCountersQuery.isPlaceholderData }>
{ !tokenCountersQuery.isLoading && countersItem('transfers_count') } { countersItem('transfers_count') }
</Skeleton>
</DetailsInfoItem> </DetailsInfoItem>
{ decimals && ( { decimals && (
<DetailsInfoItem <DetailsInfoItem
title="Decimals" title="Decimals"
hint="Number of digits that come after the decimal place when displaying token value" hint="Number of digits that come after the decimal place when displaying token value"
alignSelf="center" alignSelf="center"
isLoading={ tokenQuery.isPlaceholderData }
> >
{ decimals } <Skeleton isLoaded={ !tokenQuery.isPlaceholderData } minW={ 6 }>
{ decimals }
</Skeleton>
</DetailsInfoItem> </DetailsInfoItem>
) } ) }
<DetailsSponsoredItem/> <DetailsSponsoredItem isLoading={ tokenQuery.isPlaceholderData }/>
</Grid> </Grid>
); );
}; };
......
import { Box } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query'; import type { UseQueryResult } from '@tanstack/react-query';
import React from 'react'; import React from 'react';
...@@ -38,15 +39,28 @@ const TokenHoldersContent = ({ holdersQuery, token }: Props) => { ...@@ -38,15 +39,28 @@ const TokenHoldersContent = ({ holdersQuery, token }: Props) => {
const content = items && token ? ( const content = items && token ? (
<> <>
{ !isMobile && <TokenHoldersTable data={ items } token={ token } top={ holdersQuery.isPaginationVisible ? 80 : 0 }/> } <Box display={{ base: 'none', lg: 'block' }}>
{ isMobile && <TokenHoldersList data={ items } token={ token }/> } <TokenHoldersTable
data={ items }
token={ token }
top={ holdersQuery.isPaginationVisible ? 80 : 0 }
isLoading={ holdersQuery.isPlaceholderData }
/>
</Box>
<Box display={{ base: 'block', lg: 'none' }}>
<TokenHoldersList
data={ items }
token={ token }
isLoading={ holdersQuery.isPlaceholderData }
/>
</Box>
</> </>
) : null; ) : null;
return ( return (
<DataListDisplay <DataListDisplay
isError={ holdersQuery.isError } isError={ holdersQuery.isError }
isLoading={ holdersQuery.isLoading } isLoading={ false }
items={ holdersQuery.data?.items } items={ holdersQuery.data?.items }
skeletonProps={{ skeletonDesktopColumns: [ '100%', '300px', '175px' ] }} skeletonProps={{ skeletonDesktopColumns: [ '100%', '300px', '175px' ] }}
emptyText="There are no holders for this token." emptyText="There are no holders for this token."
......
...@@ -8,16 +8,18 @@ import TokenHoldersListItem from './TokenHoldersListItem'; ...@@ -8,16 +8,18 @@ import TokenHoldersListItem from './TokenHoldersListItem';
interface Props { interface Props {
data: Array<TokenHolder>; data: Array<TokenHolder>;
token: TokenInfo; token: TokenInfo;
isLoading?: boolean;
} }
const TokenHoldersList = ({ data, token }: Props) => { const TokenHoldersList = ({ data, token, isLoading }: Props) => {
return ( return (
<Box> <Box>
{ data.map((item) => ( { data.map((item, index) => (
<TokenHoldersListItem <TokenHoldersListItem
key={ item.address.hash } key={ item.address.hash + (isLoading ? index : '') }
token={ token } token={ token }
holder={ item } holder={ item }
isLoading={ isLoading }
/> />
)) } )) }
</Box> </Box>
......
import { Flex } from '@chakra-ui/react'; import { Flex, Skeleton } from '@chakra-ui/react';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import React from 'react'; import React from 'react';
...@@ -14,25 +14,37 @@ import Utilization from 'ui/shared/Utilization/Utilization'; ...@@ -14,25 +14,37 @@ import Utilization from 'ui/shared/Utilization/Utilization';
interface Props { interface Props {
holder: TokenHolder; holder: TokenHolder;
token: TokenInfo; token: TokenInfo;
isLoading?: boolean;
} }
const TokenHoldersListItem = ({ holder, token }: Props) => { const TokenHoldersListItem = ({ holder, token, isLoading }: Props) => {
const quantity = BigNumber(holder.value).div(BigNumber(10 ** Number(token.decimals))).dp(6).toFormat(); const quantity = BigNumber(holder.value).div(BigNumber(10 ** Number(token.decimals))).dp(6).toFormat();
return ( return (
<ListItemMobile rowGap={ 3 }> <ListItemMobile rowGap={ 3 }>
<Address display="inline-flex" maxW="100%" lineHeight="30px"> <Address display="inline-flex" maxW="100%">
<AddressIcon address={ holder.address }/> <AddressIcon address={ holder.address } isLoading={ isLoading }/>
<AddressLink type="address" ml={ 2 } fontWeight="700" hash={ holder.address.hash } alias={ holder.address.name } flexGrow={ 1 }/> <AddressLink
<CopyToClipboard text={ holder.address.hash }/> type="address"
ml={ 2 }
fontWeight="700"
hash={ holder.address.hash }
alias={ holder.address.name }
flexGrow={ 1 }
isLoading={ isLoading }
/>
<CopyToClipboard text={ holder.address.hash } isLoading={ isLoading }/>
</Address> </Address>
<Flex justifyContent="space-between" alignItems="center" width="100%"> <Flex justifyContent="space-between" alignItems="center" width="100%">
{ quantity } <Skeleton isLoaded={ !isLoading } display="inline-block">
{ quantity }
</Skeleton>
{ token.total_supply && ( { token.total_supply && (
<Utilization <Utilization
value={ BigNumber(holder.value).div(BigNumber(token.total_supply)).dp(4).toNumber() } value={ BigNumber(holder.value).div(BigNumber(token.total_supply)).dp(4).toNumber() }
colorScheme="green" colorScheme="green"
ml={ 6 } ml={ 6 }
isLoading={ isLoading }
/> />
) } ) }
</Flex> </Flex>
......
...@@ -10,9 +10,10 @@ interface Props { ...@@ -10,9 +10,10 @@ interface Props {
data: Array<TokenHolder>; data: Array<TokenHolder>;
token: TokenInfo; token: TokenInfo;
top: number; top: number;
isLoading?: boolean;
} }
const TokenHoldersTable = ({ data, token, top }: Props) => { const TokenHoldersTable = ({ data, token, top, isLoading }: Props) => {
return ( return (
<Table variant="simple" size="sm"> <Table variant="simple" size="sm">
<Thead top={ top }> <Thead top={ top }>
...@@ -23,8 +24,8 @@ const TokenHoldersTable = ({ data, token, top }: Props) => { ...@@ -23,8 +24,8 @@ const TokenHoldersTable = ({ data, token, top }: Props) => {
</Tr> </Tr>
</Thead> </Thead>
<Tbody> <Tbody>
{ data.map((item) => ( { data.map((item, index) => (
<TokenHoldersTableItem key={ item.address.hash } holder={ item } token={ token }/> <TokenHoldersTableItem key={ item.address.hash + (isLoading ? index : '') } holder={ item } token={ token } isLoading={ isLoading }/>
)) } )) }
</Tbody> </Tbody>
</Table> </Table>
......
import { Tr, Td } from '@chakra-ui/react'; import { Tr, Td, Skeleton } from '@chakra-ui/react';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import React from 'react'; import React from 'react';
...@@ -13,29 +13,41 @@ import Utilization from 'ui/shared/Utilization/Utilization'; ...@@ -13,29 +13,41 @@ import Utilization from 'ui/shared/Utilization/Utilization';
type Props = { type Props = {
holder: TokenHolder; holder: TokenHolder;
token: TokenInfo; token: TokenInfo;
isLoading?: boolean;
} }
const TokenTransferTableItem = ({ holder, token }: Props) => { const TokenTransferTableItem = ({ holder, token, isLoading }: Props) => {
const quantity = BigNumber(holder.value).div(BigNumber(10 ** Number(token.decimals))).toFormat(); const quantity = BigNumber(holder.value).div(BigNumber(10 ** Number(token.decimals))).toFormat();
return ( return (
<Tr> <Tr>
<Td> <Td verticalAlign="middle">
<Address display="inline-flex" maxW="100%" lineHeight="30px"> <Address display="inline-flex" maxW="100%">
<AddressIcon address={ holder.address }/> <AddressIcon address={ holder.address } isLoading={ isLoading }/>
<AddressLink type="address" ml={ 2 } fontWeight="700" hash={ holder.address.hash } alias={ holder.address.name } flexGrow={ 1 }/> <AddressLink
<CopyToClipboard text={ holder.address.hash }/> type="address"
ml={ 2 }
fontWeight="700"
hash={ holder.address.hash }
alias={ holder.address.name }
flexGrow={ 1 }
isLoading={ isLoading }
/>
<CopyToClipboard text={ holder.address.hash } isLoading={ isLoading }/>
</Address> </Address>
</Td> </Td>
<Td isNumeric> <Td verticalAlign="middle" isNumeric>
{ quantity } <Skeleton isLoaded={ !isLoading } display="inline-block">
{ quantity }
</Skeleton>
</Td> </Td>
{ token.total_supply && ( { token.total_supply && (
<Td isNumeric> <Td verticalAlign="middle" isNumeric>
<Utilization <Utilization
value={ BigNumber(holder.value).div(BigNumber(token.total_supply)).dp(4).toNumber() } value={ BigNumber(holder.value).div(BigNumber(token.total_supply)).dp(4).toNumber() }
colorScheme="green" colorScheme="green"
display="inline-flex" display="inline-flex"
isLoading={ isLoading }
/> />
</Td> </Td>
) } ) }
......
import { Grid, Skeleton } from '@chakra-ui/react'; import { Grid } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query'; import type { UseQueryResult } from '@tanstack/react-query';
import React from 'react'; import React from 'react';
...@@ -28,21 +28,6 @@ const TokenInventory = ({ inventoryQuery }: Props) => { ...@@ -28,21 +28,6 @@ const TokenInventory = ({ inventoryQuery }: Props) => {
</ActionBar> </ActionBar>
); );
const skeleton = (
<Grid
w="100%"
columnGap={{ base: 3, lg: 6 }}
rowGap={{ base: 3, lg: 6 }}
gridTemplateColumns={{ base: 'repeat(2, calc((100% - 12px)/2))', lg: 'repeat(auto-fill, minmax(210px, 1fr))' }}
>
<Skeleton w={{ base: '100%', lg: '210px' }} h="272px"/>
<Skeleton w={{ base: '100%', lg: '210px' }} h="272px"/>
<Skeleton w={{ base: '100%', lg: '210px' }} h="272px"/>
<Skeleton w={{ base: '100%', lg: '210px' }} h="272px"/>
<Skeleton w={{ base: '100%', lg: '210px' }} h="272px"/>
</Grid>
);
const items = inventoryQuery.data?.items; const items = inventoryQuery.data?.items;
const content = items ? ( const content = items ? (
...@@ -52,19 +37,25 @@ const TokenInventory = ({ inventoryQuery }: Props) => { ...@@ -52,19 +37,25 @@ const TokenInventory = ({ inventoryQuery }: Props) => {
rowGap={{ base: 3, lg: 6 }} rowGap={{ base: 3, lg: 6 }}
gridTemplateColumns={{ base: 'repeat(2, calc((100% - 12px)/2))', lg: 'repeat(auto-fill, minmax(210px, 1fr))' }} gridTemplateColumns={{ base: 'repeat(2, calc((100% - 12px)/2))', lg: 'repeat(auto-fill, minmax(210px, 1fr))' }}
> >
{ items.map((item) => <TokenInventoryItem key={ item.token.address + '_' + item.id } item={ item }/>) } { items.map((item, index) => (
<TokenInventoryItem
key={ item.token.address + '_' + item.id + (inventoryQuery.isPlaceholderData ? '_' + index : '') }
item={ item }
isLoading={ inventoryQuery.isPlaceholderData }
/>
)) }
</Grid> </Grid>
) : null; ) : null;
return ( return (
<DataListDisplay <DataListDisplay
isError={ inventoryQuery.isError } isError={ inventoryQuery.isError }
isLoading={ inventoryQuery.isLoading } isLoading={ false }
items={ items } items={ items }
emptyText="There are no tokens." emptyText="There are no tokens."
content={ content } content={ content }
actionBar={ actionBar } actionBar={ actionBar }
skeletonProps={{ customSkeleton: skeleton }} skeletonProps={{ customSkeleton: null }}
/> />
); );
}; };
......
import { Flex, Text, LinkBox, LinkOverlay, useColorModeValue, Hide } from '@chakra-ui/react'; import { Flex, Text, LinkBox, LinkOverlay, useColorModeValue, Hide, Skeleton } from '@chakra-ui/react';
import NextLink from 'next/link'; import NextLink from 'next/link';
import React from 'react'; import React from 'react';
...@@ -11,9 +11,9 @@ import LinkInternal from 'ui/shared/LinkInternal'; ...@@ -11,9 +11,9 @@ import LinkInternal from 'ui/shared/LinkInternal';
import NftMedia from 'ui/shared/nft/NftMedia'; import NftMedia from 'ui/shared/nft/NftMedia';
import TruncatedTextTooltip from 'ui/shared/TruncatedTextTooltip'; import TruncatedTextTooltip from 'ui/shared/TruncatedTextTooltip';
type Props = { item: TokenInstance }; type Props = { item: TokenInstance; isLoading: boolean };
const NFTItem = ({ item }: Props) => { const NFTItem = ({ item, isLoading }: Props) => {
return ( return (
<LinkBox <LinkBox
w={{ base: '100%', lg: '210px' }} w={{ base: '100%', lg: '210px' }}
...@@ -32,6 +32,7 @@ const NFTItem = ({ item }: Props) => { ...@@ -32,6 +32,7 @@ const NFTItem = ({ item }: Props) => {
mb="18px" mb="18px"
imageUrl={ item.image_url } imageUrl={ item.image_url }
animationUrl={ item.animation_url } animationUrl={ item.animation_url }
isLoading={ isLoading }
/> />
</LinkOverlay> </LinkOverlay>
</NextLink> </NextLink>
...@@ -39,13 +40,16 @@ const NFTItem = ({ item }: Props) => { ...@@ -39,13 +40,16 @@ const NFTItem = ({ item }: Props) => {
<Flex mb={ 2 } ml={ 1 }> <Flex mb={ 2 } ml={ 1 }>
<Text whiteSpace="pre" variant="secondary">ID# </Text> <Text whiteSpace="pre" variant="secondary">ID# </Text>
<TruncatedTextTooltip label={ item.id }> <TruncatedTextTooltip label={ item.id }>
<LinkInternal <Skeleton isLoaded={ !isLoading } overflow="hidden">
overflow="hidden" <LinkInternal
whiteSpace="nowrap" overflow="hidden"
textOverflow="ellipsis" textOverflow="ellipsis"
> whiteSpace="nowrap"
{ item.id } display="block"
</LinkInternal> >
{ item.id }
</LinkInternal>
</Skeleton>
</TruncatedTextTooltip> </TruncatedTextTooltip>
</Flex> </Flex>
) } ) }
...@@ -53,8 +57,8 @@ const NFTItem = ({ item }: Props) => { ...@@ -53,8 +57,8 @@ const NFTItem = ({ item }: Props) => {
<Flex mb={ 2 } ml={ 1 }> <Flex mb={ 2 } ml={ 1 }>
<Text whiteSpace="pre" variant="secondary" mr={ 2 } lineHeight="24px">Owner</Text> <Text whiteSpace="pre" variant="secondary" mr={ 2 } lineHeight="24px">Owner</Text>
<Address> <Address>
<Hide below="lg" ssr={ false }><AddressIcon address={ item.owner } mr={ 1 }/></Hide> <Hide below="lg" ssr={ false }><AddressIcon address={ item.owner } mr={ 1 } isLoading={ isLoading }/></Hide>
<AddressLink hash={ item.owner.hash } alias={ item.owner.name } type="address" truncation="constant"/> <AddressLink hash={ item.owner.hash } alias={ item.owner.name } type="address" truncation="constant" isLoading={ isLoading }/>
</Address> </Address>
</Flex> </Flex>
) } ) }
......
import { Hide, Show } from '@chakra-ui/react'; import { Box } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query'; import type { UseQueryResult } from '@tanstack/react-query';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import type { SocketMessage } from 'lib/socket/types'; import type { SocketMessage } from 'lib/socket/types';
import type { TokenInfo } from 'types/api/token';
import type { TokenTransferResponse } from 'types/api/tokenTransfer'; import type { TokenTransferResponse } from 'types/api/tokenTransfer';
import useGradualIncrement from 'lib/hooks/useGradualIncrement'; import useGradualIncrement from 'lib/hooks/useGradualIncrement';
...@@ -14,7 +15,7 @@ import ActionBar from 'ui/shared/ActionBar'; ...@@ -14,7 +15,7 @@ import ActionBar from 'ui/shared/ActionBar';
import DataListDisplay from 'ui/shared/DataListDisplay'; import DataListDisplay from 'ui/shared/DataListDisplay';
import Pagination from 'ui/shared/Pagination'; import Pagination from 'ui/shared/Pagination';
import type { Props as PaginationProps } from 'ui/shared/Pagination'; import type { Props as PaginationProps } from 'ui/shared/Pagination';
import SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice'; import * as SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice';
import TokenTransferList from 'ui/token/TokenTransfer/TokenTransferList'; import TokenTransferList from 'ui/token/TokenTransfer/TokenTransferList';
import TokenTransferTable from 'ui/token/TokenTransfer/TokenTransferTable'; import TokenTransferTable from 'ui/token/TokenTransfer/TokenTransferTable';
...@@ -24,12 +25,13 @@ type Props = { ...@@ -24,12 +25,13 @@ type Props = {
isPaginationVisible: boolean; isPaginationVisible: boolean;
}; };
tokenId?: string; tokenId?: string;
token?: TokenInfo;
} }
const TokenTransfer = ({ transfersQuery, tokenId }: Props) => { const TokenTransfer = ({ transfersQuery, tokenId, token }: Props) => {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const router = useRouter(); const router = useRouter();
const { isError, isLoading, data, pagination, isPaginationVisible } = transfersQuery; const { isError, isLoading, isPlaceholderData, data, pagination, isPaginationVisible } = transfersQuery;
const [ newItemsCount, setNewItemsCount ] = useGradualIncrement(0); const [ newItemsCount, setNewItemsCount ] = useGradualIncrement(0);
const [ socketAlert, setSocketAlert ] = React.useState(''); const [ socketAlert, setSocketAlert ] = React.useState('');
...@@ -61,7 +63,7 @@ const TokenTransfer = ({ transfersQuery, tokenId }: Props) => { ...@@ -61,7 +63,7 @@ const TokenTransfer = ({ transfersQuery, tokenId }: Props) => {
const content = data?.items ? ( const content = data?.items ? (
<> <>
<Hide below="lg" ssr={ false }> <Box display={{ base: 'none', lg: 'block' }}>
<TokenTransferTable <TokenTransferTable
data={ data?.items } data={ data?.items }
top={ isPaginationVisible ? 80 : 0 } top={ isPaginationVisible ? 80 : 0 }
...@@ -69,20 +71,22 @@ const TokenTransfer = ({ transfersQuery, tokenId }: Props) => { ...@@ -69,20 +71,22 @@ const TokenTransfer = ({ transfersQuery, tokenId }: Props) => {
socketInfoAlert={ socketAlert } socketInfoAlert={ socketAlert }
socketInfoNum={ newItemsCount } socketInfoNum={ newItemsCount }
tokenId={ tokenId } tokenId={ tokenId }
token={ token }
isLoading={ isPlaceholderData }
/> />
</Hide> </Box>
<Show below="lg" ssr={ false }> <Box display={{ base: 'block', lg: 'none' }}>
{ pagination.page === 1 && ( { pagination.page === 1 && (
<SocketNewItemsNotice <SocketNewItemsNotice.Mobile
url={ window.location.href } url={ window.location.href }
num={ newItemsCount } num={ newItemsCount }
alert={ socketAlert } alert={ socketAlert }
type="token_transfer" type="token_transfer"
borderBottomRadius={ 0 } isLoading={ isPlaceholderData }
/> />
) } ) }
<TokenTransferList data={ data?.items } tokenId={ tokenId }/> <TokenTransferList data={ data?.items } tokenId={ tokenId } isLoading={ isPlaceholderData }/>
</Show> </Box>
</> </>
) : null; ) : null;
...@@ -95,7 +99,7 @@ const TokenTransfer = ({ transfersQuery, tokenId }: Props) => { ...@@ -95,7 +99,7 @@ const TokenTransfer = ({ transfersQuery, tokenId }: Props) => {
return ( return (
<DataListDisplay <DataListDisplay
isError={ isError } isError={ isError }
isLoading={ isLoading } isLoading={ !isPlaceholderData && isLoading }
items={ data?.items } items={ data?.items }
skeletonProps={{ skeletonProps={{
isLongSkeleton: true, isLongSkeleton: true,
......
...@@ -8,9 +8,10 @@ import TokenTransferListItem from 'ui/token/TokenTransfer/TokenTransferListItem' ...@@ -8,9 +8,10 @@ import TokenTransferListItem from 'ui/token/TokenTransfer/TokenTransferListItem'
interface Props { interface Props {
data: Array<TokenTransfer>; data: Array<TokenTransfer>;
tokenId?: string; tokenId?: string;
isLoading?: boolean;
} }
const TokenTransferList = ({ data, tokenId }: Props) => { const TokenTransferList = ({ data, tokenId, isLoading }: Props) => {
return ( return (
<Box> <Box>
{ data.map((item, index) => ( { data.map((item, index) => (
...@@ -18,6 +19,7 @@ const TokenTransferList = ({ data, tokenId }: Props) => { ...@@ -18,6 +19,7 @@ const TokenTransferList = ({ data, tokenId }: Props) => {
key={ index } key={ index }
{ ...item } { ...item }
tokenId={ tokenId } tokenId={ tokenId }
isLoading={ isLoading }
/> />
)) } )) }
</Box> </Box>
......
import { Text, Flex, Tag, Icon, useColorModeValue } from '@chakra-ui/react'; import { Text, Flex, Icon, useColorModeValue, Skeleton } from '@chakra-ui/react';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import React from 'react'; import React from 'react';
...@@ -11,11 +11,12 @@ import trimTokenSymbol from 'lib/token/trimTokenSymbol'; ...@@ -11,11 +11,12 @@ import trimTokenSymbol from 'lib/token/trimTokenSymbol';
import Address from 'ui/shared/address/Address'; import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon'; import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink'; import AddressLink from 'ui/shared/address/AddressLink';
import Tag from 'ui/shared/chakra/Tag';
import CopyToClipboard from 'ui/shared/CopyToClipboard'; import CopyToClipboard from 'ui/shared/CopyToClipboard';
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile'; import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
import TokenTransferNft from 'ui/shared/TokenTransfer/TokenTransferNft'; import TokenTransferNft from 'ui/shared/TokenTransfer/TokenTransferNft';
type Props = TokenTransfer & {tokenId?: string}; type Props = TokenTransfer & { tokenId?: string; isLoading?: boolean };
const TokenTransferListItem = ({ const TokenTransferListItem = ({
token, token,
...@@ -26,6 +27,7 @@ const TokenTransferListItem = ({ ...@@ -26,6 +27,7 @@ const TokenTransferListItem = ({
method, method,
timestamp, timestamp,
tokenId, tokenId,
isLoading,
}: Props) => { }: Props) => {
const value = (() => { const value = (() => {
if (!('value' in total)) { if (!('value' in total)) {
...@@ -43,46 +45,68 @@ const TokenTransferListItem = ({ ...@@ -43,46 +45,68 @@ const TokenTransferListItem = ({
<ListItemMobile rowGap={ 3 } isAnimated> <ListItemMobile rowGap={ 3 } isAnimated>
<Flex justifyContent="space-between" alignItems="center" lineHeight="24px" width="100%"> <Flex justifyContent="space-between" alignItems="center" lineHeight="24px" width="100%">
<Flex> <Flex>
<Icon <Skeleton isLoaded={ !isLoading } boxSize="30px" mr={ 2 }>
as={ transactionIcon } <Icon
boxSize="30px" as={ transactionIcon }
mr={ 2 } boxSize="30px"
color={ iconColor } color={ iconColor }
/> />
</Skeleton>
<Address width="100%"> <Address width="100%">
<AddressLink <AddressLink
hash={ txHash } hash={ txHash }
type="transaction" type="transaction"
fontWeight="700" fontWeight="700"
truncation="constant" truncation="constant"
isLoading={ isLoading }
/> />
</Address> </Address>
</Flex> </Flex>
{ timestamp && <Text variant="secondary" fontWeight="400" fontSize="sm">{ timeAgo }</Text> } { timestamp && (
<Text variant="secondary" fontWeight="400" fontSize="sm">
<Skeleton isLoaded={ !isLoading } display="inline-block">
<span>
{ timeAgo }
</span>
</Skeleton>
</Text>
) }
</Flex> </Flex>
{ method && <Tag colorScheme="gray">{ method }</Tag> } { method && <Tag isLoading={ isLoading }>{ method }</Tag> }
<Flex w="100%" columnGap={ 3 }> <Flex w="100%" columnGap={ 3 }>
<Address width="50%"> <Address width="50%">
<AddressIcon address={ from }/> <AddressIcon address={ from } isLoading={ isLoading }/>
<AddressLink ml={ 2 } fontWeight="500" hash={ from.hash } type="address_token" tokenHash={ token.address }/> <AddressLink ml={ 2 } fontWeight="500" hash={ from.hash } type="address_token" tokenHash={ token.address } isLoading={ isLoading }/>
<CopyToClipboard text={ from.hash }/> <CopyToClipboard text={ from.hash } isLoading={ isLoading }/>
</Address> </Address>
<Icon as={ eastArrowIcon } boxSize={ 6 } color="gray.500"/> <Skeleton isLoaded={ !isLoading } boxSize={ 6 }>
<Icon as={ eastArrowIcon } boxSize={ 6 } color="gray.500"/>
</Skeleton>
<Address width="50%"> <Address width="50%">
<AddressIcon address={ to }/> <AddressIcon address={ to } isLoading={ isLoading }/>
<AddressLink ml={ 2 } fontWeight="500" hash={ to.hash } type="address_token" tokenHash={ token.address }/> <AddressLink ml={ 2 } fontWeight="500" hash={ to.hash } type="address_token" tokenHash={ token.address } isLoading={ isLoading }/>
<CopyToClipboard text={ to.hash }/> <CopyToClipboard text={ to.hash } isLoading={ isLoading }/>
</Address> </Address>
</Flex> </Flex>
{ value && (token.type === 'ERC-20' || token.type === 'ERC-1155') && ( { value && (token.type === 'ERC-20' || token.type === 'ERC-1155') && (
<Flex columnGap={ 2 } w="100%"> <Flex columnGap={ 2 } w="100%">
<Text fontWeight={ 500 } flexShrink={ 0 }>Value</Text> <Skeleton isLoaded={ !isLoading } flexShrink={ 0 } fontWeight={ 500 }>
<Text variant="secondary">{ value }</Text> Value
<Text>{ trimTokenSymbol(token.symbol) }</Text> </Skeleton>
<Skeleton isLoaded={ !isLoading } variant="secondary">
{ value }
</Skeleton>
<Skeleton isLoaded={ !isLoading }>{ trimTokenSymbol(token.symbol) }</Skeleton>
</Flex> </Flex>
) } ) }
{ 'token_id' in total && (token.type === 'ERC-721' || token.type === 'ERC-1155') && { 'token_id' in total && (token.type === 'ERC-721' || token.type === 'ERC-1155') && (
<TokenTransferNft hash={ token.address } id={ total.token_id } isDisabled={ Boolean(tokenId && tokenId === total.token_id) }/> } <TokenTransferNft
hash={ token.address }
id={ total.token_id }
isDisabled={ Boolean(tokenId && tokenId === total.token_id) }
isLoading={ isLoading }
/>
) }
</ListItemMobile> </ListItemMobile>
); );
}; };
......
import { Table, Tbody, Tr, Th, Td } from '@chakra-ui/react'; import { Table, Tbody, Tr, Th } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { TokenInfo } from 'types/api/token';
import type { TokenTransfer } from 'types/api/tokenTransfer'; import type { TokenTransfer } from 'types/api/tokenTransfer';
import trimTokenSymbol from 'lib/token/trimTokenSymbol'; import trimTokenSymbol from 'lib/token/trimTokenSymbol';
import SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice'; import * as SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice';
import { default as Thead } from 'ui/shared/TheadSticky'; import { default as Thead } from 'ui/shared/TheadSticky';
import TokenTransferTableItem from 'ui/token/TokenTransfer/TokenTransferTableItem'; import TokenTransferTableItem from 'ui/token/TokenTransfer/TokenTransferTableItem';
...@@ -15,11 +16,12 @@ interface Props { ...@@ -15,11 +16,12 @@ interface Props {
socketInfoAlert?: string; socketInfoAlert?: string;
socketInfoNum?: number; socketInfoNum?: number;
tokenId?: string; tokenId?: string;
isLoading?: boolean;
token?: TokenInfo;
} }
const TokenTransferTable = ({ data, top, showSocketInfo, socketInfoAlert, socketInfoNum, tokenId }: Props) => { const TokenTransferTable = ({ data, top, showSocketInfo, socketInfoAlert, socketInfoNum, tokenId, isLoading, token }: Props) => {
const tokenType = data[0].token.type; const tokenType = data[0].token.type;
const tokenSymbol = data[0].token.symbol;
return ( return (
<Table variant="simple" size="sm" minW="950px"> <Table variant="simple" size="sm" minW="950px">
...@@ -27,30 +29,26 @@ const TokenTransferTable = ({ data, top, showSocketInfo, socketInfoAlert, socket ...@@ -27,30 +29,26 @@ const TokenTransferTable = ({ data, top, showSocketInfo, socketInfoAlert, socket
<Tr> <Tr>
<Th width={ tokenType === 'ERC-1155' ? '60%' : '80%' }>Txn hash</Th> <Th width={ tokenType === 'ERC-1155' ? '60%' : '80%' }>Txn hash</Th>
<Th width="164px">Method</Th> <Th width="164px">Method</Th>
<Th width="148px">From</Th> <Th width="160px">From</Th>
<Th width="36px" px={ 0 }/> <Th width="36px" px={ 0 }/>
<Th width="218px" >To</Th> <Th width="218px" >To</Th>
{ (tokenType === 'ERC-721' || tokenType === 'ERC-1155') && <Th width="20%" isNumeric={ tokenType === 'ERC-721' }>Token ID</Th> } { (tokenType === 'ERC-721' || tokenType === 'ERC-1155') && <Th width="20%" isNumeric={ tokenType === 'ERC-721' }>Token ID</Th> }
{ (tokenType === 'ERC-20' || tokenType === 'ERC-1155') && <Th width="20%" isNumeric whiteSpace="nowrap">Value { trimTokenSymbol(tokenSymbol) }</Th> } { (tokenType === 'ERC-20' || tokenType === 'ERC-1155') &&
<Th width="20%" isNumeric whiteSpace="nowrap">Value { trimTokenSymbol(token?.symbol || '') }</Th> }
</Tr> </Tr>
</Thead> </Thead>
<Tbody> <Tbody>
{ showSocketInfo && ( { showSocketInfo && (
<Tr> <SocketNewItemsNotice.Desktop
<Td colSpan={ 10 } p={ 0 }> url={ window.location.href }
<SocketNewItemsNotice alert={ socketInfoAlert }
borderRadius={ 0 } num={ socketInfoNum }
pl="10px" type="token_transfer"
url={ window.location.href } isLoading={ isLoading }
alert={ socketInfoAlert } />
num={ socketInfoNum }
type="token_transfer"
/>
</Td>
</Tr>
) } ) }
{ data.map((item, index) => ( { data.map((item, index) => (
<TokenTransferTableItem key={ index } { ...item } tokenId={ tokenId }/> <TokenTransferTableItem key={ index } { ...item } tokenId={ tokenId } isLoading={ isLoading }/>
)) } )) }
</Tbody> </Tbody>
</Table> </Table>
......
import { Tr, Td, Tag, Text, Icon, Grid } from '@chakra-ui/react'; import { Tr, Td, Icon, Grid, Skeleton, Box } from '@chakra-ui/react';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import React from 'react'; import React from 'react';
...@@ -9,10 +9,11 @@ import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement'; ...@@ -9,10 +9,11 @@ import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
import Address from 'ui/shared/address/Address'; import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon'; import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink'; import AddressLink from 'ui/shared/address/AddressLink';
import Tag from 'ui/shared/chakra/Tag';
import CopyToClipboard from 'ui/shared/CopyToClipboard'; import CopyToClipboard from 'ui/shared/CopyToClipboard';
import TokenTransferNft from 'ui/shared/TokenTransfer/TokenTransferNft'; import TokenTransferNft from 'ui/shared/TokenTransfer/TokenTransferNft';
type Props = TokenTransfer & { tokenId?: string } type Props = TokenTransfer & { tokenId?: string; isLoading?: boolean }
const TokenTransferTableItem = ({ const TokenTransferTableItem = ({
token, token,
...@@ -23,25 +24,36 @@ const TokenTransferTableItem = ({ ...@@ -23,25 +24,36 @@ const TokenTransferTableItem = ({
method, method,
timestamp, timestamp,
tokenId, tokenId,
isLoading,
}: Props) => { }: Props) => {
const timeAgo = useTimeAgoIncrement(timestamp, true); const timeAgo = useTimeAgoIncrement(timestamp, true);
return ( return (
<Tr alignItems="top"> <Tr alignItems="top">
<Td> <Td>
<Grid alignItems="center" gridTemplateColumns="auto 130px" width="fit-content"> <Grid alignItems="center" gridTemplateColumns="auto 130px" width="fit-content" py="7px">
<Address display="inline-flex" fontWeight={ 600 } lineHeight="30px"> <Address display="inline-flex" fontWeight={ 600 }>
<AddressLink type="transaction" hash={ txHash }/> <AddressLink type="transaction" hash={ txHash } isLoading={ isLoading }/>
</Address> </Address>
{ timestamp && <Text color="gray.500" fontWeight="400" ml="10px">{ timeAgo }</Text> } { timestamp && (
<Skeleton isLoaded={ !isLoading } display="inline-block" color="gray.500" fontWeight="400" ml="10px">
<span>
{ timeAgo }
</span>
</Skeleton>
) }
</Grid> </Grid>
</Td> </Td>
<Td> <Td>
{ method && <Tag colorScheme="gray">{ method }</Tag> } { method ? (
<Box my="3px">
<Tag isLoading={ isLoading } isTruncated>{ method }</Tag>
</Box>
) : null }
</Td> </Td>
<Td> <Td>
<Address display="inline-flex" maxW="100%" lineHeight="30px"> <Address display="inline-flex" maxW="100%" py="3px">
<AddressIcon address={ from }/> <AddressIcon address={ from } isLoading={ isLoading }/>
<AddressLink <AddressLink
ml={ 2 } ml={ 2 }
flexGrow={ 1 } flexGrow={ 1 }
...@@ -51,16 +63,19 @@ const TokenTransferTableItem = ({ ...@@ -51,16 +63,19 @@ const TokenTransferTableItem = ({
alias={ from.name } alias={ from.name }
tokenHash={ token.address } tokenHash={ token.address }
truncation="constant" truncation="constant"
isLoading={ isLoading }
/> />
<CopyToClipboard text={ from.hash }/> <CopyToClipboard text={ from.hash } isLoading={ isLoading }/>
</Address> </Address>
</Td> </Td>
<Td px={ 0 }> <Td px={ 0 }>
<Icon as={ eastArrowIcon } boxSize={ 6 } color="gray.500"/> <Skeleton isLoaded={ !isLoading } boxSize={ 6 } my="3px">
<Icon as={ eastArrowIcon } boxSize={ 6 } color="gray.500"/>
</Skeleton>
</Td> </Td>
<Td> <Td>
<Address display="inline-flex" maxW="100%" lineHeight="30px"> <Address display="inline-flex" maxW="100%" py="3px">
<AddressIcon address={ to }/> <AddressIcon address={ to } isLoading={ isLoading }/>
<AddressLink <AddressLink
ml={ 2 } ml={ 2 }
flexGrow={ 1 } flexGrow={ 1 }
...@@ -70,26 +85,30 @@ const TokenTransferTableItem = ({ ...@@ -70,26 +85,30 @@ const TokenTransferTableItem = ({
alias={ to.name } alias={ to.name }
tokenHash={ token.address } tokenHash={ token.address }
truncation="constant" truncation="constant"
isLoading={ isLoading }
/> />
<CopyToClipboard text={ to.hash }/> <CopyToClipboard text={ to.hash } isLoading={ isLoading }/>
</Address> </Address>
</Td> </Td>
{ (token.type === 'ERC-721' || token.type === 'ERC-1155') && ( { (token.type === 'ERC-721' || token.type === 'ERC-1155') && (
<Td lineHeight="30px"> <Td>
{ 'token_id' in total ? ( { 'token_id' in total ? (
<TokenTransferNft <TokenTransferNft
hash={ token.address } hash={ token.address }
id={ total.token_id } id={ total.token_id }
justifyContent={ token.type === 'ERC-721' ? 'end' : 'start' } justifyContent={ token.type === 'ERC-721' ? 'end' : 'start' }
isDisabled={ Boolean(tokenId && tokenId === total.token_id) } isDisabled={ Boolean(tokenId && tokenId === total.token_id) }
isLoading={ isLoading }
/> />
) : '' ) : ''
} }
</Td> </Td>
) } ) }
{ (token.type === 'ERC-20' || token.type === 'ERC-1155') && ( { (token.type === 'ERC-20' || token.type === 'ERC-1155') && (
<Td isNumeric verticalAlign="top" lineHeight="30px"> <Td isNumeric verticalAlign="top">
{ 'value' in total && BigNumber(total.value).div(BigNumber(10 ** Number(total.decimals))).dp(8).toFormat() } <Skeleton isLoaded={ !isLoading } my="7px">
{ 'value' in total && BigNumber(total.value).div(BigNumber(10 ** Number(total.decimals))).dp(8).toFormat() }
</Skeleton>
</Td> </Td>
) } ) }
</Tr> </Tr>
......
...@@ -65,7 +65,11 @@ const TokenInstanceContent = () => { ...@@ -65,7 +65,11 @@ const TokenInstanceContent = () => {
}); });
const tabs: Array<RoutedTab> = [ const tabs: Array<RoutedTab> = [
{ id: 'token_transfers', title: 'Token transfers', component: <TokenTransfer transfersQuery={ transfersQuery } tokenId={ id }/> }, {
id: 'token_transfers',
title: 'Token transfers',
component: <TokenTransfer transfersQuery={ transfersQuery } tokenId={ id } token={ tokenInstanceQuery.data?.token }/>,
},
shouldFetchHolders ? shouldFetchHolders ?
{ id: 'holders', title: 'Holders', component: <TokenHolders holdersQuery={ holdersQuery } token={ tokenInstanceQuery.data?.token }/> } : { id: 'holders', title: 'Holders', component: <TokenHolders holdersQuery={ holdersQuery } token={ tokenInstanceQuery.data?.token }/> } :
undefined, undefined,
......
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