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 = {
groups: [
'module',
'/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' ],
],
alphabetize: { order: 'asc', ignoreCase: true },
......
<svg fill="currentColor" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 21 20">
<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="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"/>
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<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="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 fill="currentColor" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
<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"/>
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<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>
import type { NextPage } from 'next';
import dynamic from 'next/dynamic';
import Head from 'next/head';
import React from 'react';
import type { PageParams } from 'lib/next/token/types';
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 { title, description } = getSeo({ hash });
......@@ -16,7 +19,9 @@ const TokenPage: NextPage<PageParams> = ({ hash }: PageParams) => {
<title>{ title }</title>
<meta name="description" content={ description }/>
</Head>
<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) => {
return {
opacity: 1,
borderRadius: 'base',
borderRadius: 'md',
borderColor: start,
background: `linear-gradient(90deg, ${ start } 8%, ${ end } 18%, ${ start } 33%)`,
backgroundSize: '200% 100%',
......
......@@ -20,7 +20,7 @@ export interface TokenCounters {
export interface TokenHolders {
items: Array<TokenHolder>;
next_page_params: TokenHoldersPagination;
next_page_params: TokenHoldersPagination | null;
}
export type TokenHolder = {
......@@ -51,7 +51,7 @@ export interface TokenInstanceTransfersCount {
export interface TokenInventoryResponse {
items: Array<TokenInstance>;
next_page_params: TokenInventoryPagination;
next_page_params: TokenInventoryPagination | null;
}
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 React from 'react';
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 useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
import * as stubs from 'stubs/contract';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
......@@ -24,10 +27,10 @@ type Props = {
noSocket?: boolean;
}
const InfoItem = chakra(({ label, value, className }: { label: string; value: string; className?: string }) => (
<GridItem display="flex" columnGap={ 6 } wordBreak="break-all" className={ className }>
<Text w="170px" flexShrink={ 0 } fontWeight={ 500 }>{ label }</Text>
<Text>{ value }</Text>
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 } alignItems="baseline">
<Skeleton isLoaded={ !isLoading } w="170px" flexShrink={ 0 } fontWeight={ 500 }>{ label }</Skeleton>
<Skeleton isLoaded={ !isLoading }>{ value }</Skeleton>
</GridItem>
));
......@@ -35,11 +38,15 @@ const ContractCode = ({ addressHash, noSocket }: Props) => {
const [ isSocketOpen, setIsSocketOpen ] = React.useState(false);
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 },
queryOptions: {
enabled: Boolean(addressHash) && (noSocket || isSocketOpen),
refetchOnMount: false,
placeholderData: addressInfo?.is_verified ? stubs.CONTRACT_CODE_VERIFIED : stubs.CONTRACT_CODE_UNVERIFIED,
},
});
......@@ -62,24 +69,7 @@ const ContractCode = ({ addressHash, noSocket }: Props) => {
return <DataFetchAlert/>;
}
if (isLoading) {
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 = (
const verificationButton = isPlaceholderData ? <Skeleton w="130px" h={ 8 } mr={ 3 } ml="auto" borderRadius="base"/> : (
<Button
size="sm"
ml="auto"
......@@ -92,8 +82,8 @@ const ContractCode = ({ addressHash, noSocket }: Props) => {
);
const constructorArgs = (() => {
if (!data.decoded_constructor_args) {
return data.constructor_args;
if (!data?.decoded_constructor_args) {
return data?.constructor_args;
}
const decoded = data.decoded_constructor_args
......@@ -119,7 +109,7 @@ const ContractCode = ({ addressHash, noSocket }: Props) => {
})();
const externalLibraries = (() => {
if (!data.external_libraries || data.external_libraries.length === 0) {
if (!data?.external_libraries || data?.external_libraries.length === 0) {
return null;
}
......@@ -134,19 +124,23 @@ const ContractCode = ({ addressHash, noSocket }: Props) => {
return (
<>
<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_via_sourcify && (
{ data?.is_verified && (
<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">
<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> }
</Alert>
) }
{ (data.is_changed_bytecode || isChangedBytecodeSocket) && (
{ (data?.is_changed_bytecode || isChangedBytecodeSocket) && (
<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.
</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">
<span>Contract is not verified. However, we found a verified contract with the same bytecode in Blockscout DB </span>
<Address>
......@@ -160,7 +154,7 @@ const ContractCode = ({ addressHash, noSocket }: Props) => {
<span> page</span>
</Alert>
) }
{ data.minimal_proxy_address_hash && (
{ data?.minimal_proxy_address_hash && (
<Alert status="warning" flexWrap="wrap" whiteSpace="pre-wrap">
<span>Minimal Proxy Contract for </span>
<Address>
......@@ -175,14 +169,16 @@ const ContractCode = ({ addressHash, noSocket }: Props) => {
</Alert>
) }
</Flex>
{ data.is_verified && (
{ data?.is_verified && (
<Grid templateColumns={{ base: '1fr', lg: '1fr 1fr' }} rowGap={ 4 } columnGap={ 6 } mb={ 8 }>
{ data.name && <InfoItem label="Contract name" value={ data.name }/> }
{ data.compiler_version && <InfoItem label="Compiler version" value={ data.compiler_version }/> }
{ data.evm_version && <InfoItem label="EVM version" value={ data.evm_version } textTransform="capitalize"/> }
{ typeof data.optimization_enabled === 'boolean' && <InfoItem label="Optimization enabled" value={ data.optimization_enabled ? 'true' : 'false' }/> }
{ data.optimization_runs && <InfoItem label="Optimization runs" value={ String(data.optimization_runs) }/> }
{ data.verified_at && <InfoItem label="Verified at" value={ dayjs(data.verified_at).format('LLLL') } wordBreak="break-word"/> }
{ data.name && <InfoItem label="Contract name" value={ data.name } isLoading={ isPlaceholderData }/> }
{ 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" isLoading={ isPlaceholderData }/> }
{ typeof data.optimization_enabled === 'boolean' &&
<InfoItem label="Optimization enabled" value={ data.optimization_enabled ? 'true' : 'false' } isLoading={ isPlaceholderData }/> }
{ 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>
) }
<Flex flexDir="column" rowGap={ 6 }>
......@@ -191,9 +187,10 @@ const ContractCode = ({ addressHash, noSocket }: Props) => {
data={ constructorArgs }
title="Constructor Arguments"
textareaMaxHeight="200px"
isLoading={ isPlaceholderData }
/>
) }
{ data.source_code && (
{ data?.source_code && (
<ContractSourceCode
data={ data.source_code }
hasSol2Yml={ Boolean(data.can_be_visualized_via_sol2uml) }
......@@ -202,23 +199,26 @@ const ContractCode = ({ addressHash, noSocket }: Props) => {
filePath={ data.file_path }
additionalSource={ data.additional_sources }
remappings={ data.compiler_settings?.remappings }
isLoading={ isPlaceholderData }
/>
) }
{ Boolean(data.compiler_settings) && (
{ data?.compiler_settings ? (
<RawDataSnippet
data={ JSON.stringify(data.compiler_settings) }
title="Compiler Settings"
textareaMaxHeight="200px"
isLoading={ isPlaceholderData }
/>
) }
{ data.abi && (
) : null }
{ data?.abi && (
<RawDataSnippet
data={ JSON.stringify(data.abi) }
title="Contract ABI"
textareaMaxHeight="200px"
isLoading={ isPlaceholderData }
/>
) }
{ data.creation_bytecode && (
{ data?.creation_bytecode && (
<RawDataSnippet
data={ data.creation_bytecode }
title="Contract creation code"
......@@ -230,13 +230,15 @@ const ContractCode = ({ addressHash, noSocket }: Props) => {
</Alert>
) : null }
textareaMaxHeight="200px"
isLoading={ isPlaceholderData }
/>
) }
{ data.deployed_bytecode && (
{ data?.deployed_bytecode && (
<RawDataSnippet
data={ data.deployed_bytecode }
title="Deployed ByteCode"
textareaMaxHeight="200px"
isLoading={ isPlaceholderData }
/>
) }
{ externalLibraries && (
......@@ -244,6 +246,7 @@ const ContractCode = ({ addressHash, noSocket }: Props) => {
data={ externalLibraries }
title="External Libraries"
textareaMaxHeight="200px"
isLoading={ isPlaceholderData }
/>
) }
</Flex>
......
import { Flex, Text, Tooltip } from '@chakra-ui/react';
import { Flex, Skeleton, Text, Tooltip } from '@chakra-ui/react';
import { route } from 'nextjs-routes';
import React from 'react';
......@@ -17,14 +17,15 @@ interface Props {
filePath?: string;
additionalSource?: SmartContract['additional_sources'];
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 = (
<Text fontWeight={ 500 }>
<Skeleton isLoaded={ !isLoading } fontWeight={ 500 }>
<span>Contract source code</span>
<Text whiteSpace="pre" as="span" variant="secondary"> ({ isViper ? 'Vyper' : 'Solidity' })</Text>
</Text>
</Skeleton>
);
const diagramLink = hasSol2Yml && address ? (
......@@ -32,9 +33,10 @@ const ContractSourceCode = ({ data, hasSol2Yml, address, isViper, filePath, addi
<LinkInternal
href={ route({ pathname: '/visualize/sol2uml', query: { address } }) }
ml="auto"
mr={ 3 }
>
<Skeleton isLoaded={ !isLoading }>
View UML diagram
</Skeleton>
</LinkInternal>
</Tooltip>
) : null;
......@@ -47,7 +49,7 @@ const ContractSourceCode = ({ data, hasSol2Yml, address, isViper, filePath, addi
}, [ additionalSource, data, filePath, isViper ]);
const copyToClipboard = editorData.length === 1 ?
<CopyToClipboard text={ editorData[0].source_code }/> :
<CopyToClipboard text={ editorData[0].source_code } isLoading={ isLoading } ml={ 3 }/> :
null;
return (
......@@ -57,7 +59,7 @@ const ContractSourceCode = ({ data, hasSol2Yml, address, isViper, filePath, addi
{ diagramLink }
{ copyToClipboard }
</Flex>
<CodeEditor data={ editorData } remappings={ remappings }/>
{ isLoading ? <Skeleton h="557px" w="100%"/> : <CodeEditor data={ editorData } remappings={ remappings }/> }
</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 QRCode from 'qrcode';
import React from 'react';
......@@ -13,9 +27,10 @@ const SVG_OPTIONS = {
interface Props {
className?: string;
hash: string;
isLoading?: boolean;
}
const AddressQrCode = ({ hash, className }: Props) => {
const AddressQrCode = ({ hash, className, isLoading }: Props) => {
const { isOpen, onOpen, onClose } = useDisclosure();
const isMobile = useIsMobile();
const [ qr, setQr ] = React.useState('');
......@@ -36,6 +51,10 @@ const AddressQrCode = ({ hash, className }: Props) => {
}
}, [ hash, isOpen, onClose ]);
if (isLoading) {
return <Skeleton className={ className } w="36px" h="32px" borderRadius="base"/>;
}
return (
<>
<Tooltip label="Click to view QR code">
......
......@@ -49,7 +49,7 @@ const Home = () => {
</Box>
<Stats/>
<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 }>
<LatestBlocks/>
<Box flexGrow={ 1 }>
......
......@@ -67,7 +67,7 @@ test('base view', async({ mount, page, createSocket }) => {
await insertAdPlaceholder(page);
await expect(component.locator('main')).toHaveScreenshot();
await expect(component).toHaveScreenshot();
});
test.describe('mobile', () => {
......@@ -86,6 +86,6 @@ test.describe('mobile', () => {
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 { useRouter } from 'next/router';
import React, { useEffect } from 'react';
......@@ -16,9 +16,11 @@ import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
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 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 type { Props as PaginationProps } from 'ui/shared/Pagination';
import Pagination from 'ui/shared/Pagination';
......@@ -51,7 +53,18 @@ const TokenPageContent = () => {
const tokenQuery = useApiQuery('token', {
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(() => {
......@@ -88,7 +101,7 @@ const TokenPageContent = () => {
});
useEffect(() => {
if (tokenQuery.data) {
if (tokenQuery.data && !tokenQuery.isPlaceholderData) {
const tokenSymbol = tokenQuery.data.symbol ? ` (${ tokenQuery.data.symbol })` : '';
const tokenName = `${ tokenQuery.data.name || 'Unnamed' }${ tokenSymbol }`;
const title = document.getElementsByTagName('title')[0];
......@@ -100,14 +113,17 @@ const TokenPageContent = () => {
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({
resourceName: 'token_transfers',
pathParams: { hash: hashString },
scrollRef,
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 = () => {
pathParams: { hash: hashString },
scrollRef,
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 = () => {
pathParams: { hash: hashString },
scrollRef,
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 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 }/> },
(tokenQuery.data?.type === 'ERC-1155' || tokenQuery.data?.type === 'ERC-721') ?
{ id: 'inventory', title: 'Inventory', component: <TokenInventory inventoryQuery={ inventoryQuery }/> } :
......@@ -145,7 +158,7 @@ const TokenPageContent = () => {
contractQuery.data?.is_contract ? {
id: 'contract',
title: () => {
if (contractQuery.data.is_verified) {
if (contractQuery.data?.is_verified) {
return (
<>
<span>Contract</span>
......@@ -195,35 +208,26 @@ const TokenPageContent = () => {
}, [ isMobile ]);
return (
<Page>
{ tokenQuery.isLoading ? (
<>
<Skeleton h={{ base: 12, lg: 6 }} mb={ 6 } w="100%" maxW="680px"/>
<Flex alignItems="center" mb={ 6 }>
<SkeletonCircle w={ 6 } h={ 6 } mr={ 3 }/>
<Skeleton w="500px" h={ 10 }/>
</Flex>
</>
) : (
<>
<TextAd mb={ 6 }/>
<PageTitle
isLoading={ tokenQuery.isPlaceholderData }
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 }/>
<TokenLogo hash={ tokenQuery.data?.address } name={ tokenQuery.data?.name } boxSize={ 6 } isLoading={ tokenQuery.isPlaceholderData }/>
) }
additionalsRight={ <Tag>{ tokenQuery.data?.type }</Tag> }
additionalsRight={ <Tag isLoading={ tokenQuery.isPlaceholderData }>{ tokenQuery.data?.type }</Tag> }
/>
</>
) }
<TokenContractInfo tokenQuery={ tokenQuery }/>
<TokenContractInfo tokenQuery={ tokenQuery } contractQuery={ contractQuery }/>
<TokenDetails tokenQuery={ tokenQuery }/>
{ /* should stay before tabs to scroll up with pagination */ }
<Box ref={ scrollRef }></Box>
{ tokenQuery.isLoading || contractQuery.isLoading ? <SkeletonTabs/> : (
{ tokenQuery.isPlaceholderData || contractQuery.isPlaceholderData ?
<SkeletonTabs tabs={ tabs }/> :
(
<RoutedTabs
tabs={ tabs }
tabListProps={ tabListProps }
......@@ -233,7 +237,7 @@ const TokenPageContent = () => {
) }
{ !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 type { AddressTag } from 'types/api/account';
......@@ -11,9 +11,10 @@ interface Props {
item: AddressTag;
onEditClick: (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(() => {
return onEditClick(item);
}, [ item, onEditClick ]);
......@@ -25,15 +26,17 @@ const AddressTagListItem = ({ item, onEditClick, onDeleteClick }: Props) => {
return (
<ListItemMobile>
<Flex alignItems="flex-start" flexDirection="column" maxW="100%">
<AddressSnippet address={ item.address }/>
<AddressSnippet address={ item.address } isLoading={ isLoading }/>
<HStack spacing={ 3 } mt={ 4 }>
<Text fontSize="sm" fontWeight={ 500 }>Private tag</Text>
<Skeleton isLoaded={ !isLoading } display="inline-block" borderRadius="sm">
<Tag>
{ item.name }
</Tag>
</Skeleton>
</HStack>
</Flex>
<TableItemActionButtons onDeleteClick={ onItemDeleteClick } onEditClick={ onItemEditClick }/>
<TableItemActionButtons onDeleteClick={ onItemDeleteClick } onEditClick={ onItemEditClick } isLoading={ isLoading }/>
</ListItemMobile>
);
};
......
......@@ -12,28 +12,30 @@ import type { AddressTags, AddressTag } from 'types/api/account';
import AddressTagTableItem from './AddressTagTableItem';
interface Props {
data: AddressTags;
data?: AddressTags;
onEditClick: (data: AddressTag) => void;
onDeleteClick: (data: AddressTag) => void;
isLoading: boolean;
}
const AddressTagTable = ({ data, onDeleteClick, onEditClick }: Props) => {
const AddressTagTable = ({ data, onDeleteClick, onEditClick, isLoading }: Props) => {
return (
<Table variant="simple" minWidth="600px">
<Thead>
<Tr>
<Th width="60%">Address</Th>
<Th width="40%">Private tag</Th>
<Th width="108px"></Th>
<Th width="116px"></Th>
</Tr>
</Thead>
<Tbody>
{ data.map((item: AddressTag) => (
{ data?.map((item: AddressTag, index: number) => (
<AddressTagTableItem
item={ item }
key={ item.id }
key={ item.id + (isLoading ? index : '') }
onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick }
isLoading={ isLoading }
/>
)) }
</Tbody>
......
import {
Tag,
Tr,
Td,
} from '@chakra-ui/react';
......@@ -8,16 +7,17 @@ import React, { useCallback } from 'react';
import type { AddressTag } from 'types/api/account';
import AddressSnippet from 'ui/shared/AddressSnippet';
import Tag from 'ui/shared/chakra/Tag';
import TableItemActionButtons from 'ui/shared/TableItemActionButtons';
import TruncatedTextTooltip from 'ui/shared/TruncatedTextTooltip';
interface Props {
item: AddressTag;
onEditClick: (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(() => {
return onEditClick(item);
}, [ item, onEditClick ]);
......@@ -29,17 +29,13 @@ const AddressTagTableItem = ({ item, onEditClick, onDeleteClick }: Props) => {
return (
<Tr alignItems="top" key={ item.id }>
<Td>
<AddressSnippet address={ item.address }/>
<AddressSnippet address={ item.address } isLoading={ isLoading }/>
</Td>
<Td whiteSpace="nowrap">
<TruncatedTextTooltip label={ item.name }>
<Tag>
{ item.name }
</Tag>
</TruncatedTextTooltip>
<Tag isLoading={ isLoading } isTruncated>{ item.name }</Tag>
</Td>
<Td>
<TableItemActionButtons onDeleteClick={ onItemDeleteClick } onEditClick={ onItemEditClick }/>
<TableItemActionButtons onDeleteClick={ onItemDeleteClick } onEditClick={ onItemEditClick } isLoading={ isLoading }/>
</Td>
</Tr>
);
......
......@@ -5,10 +5,9 @@ import type { AddressTag } from 'types/api/account';
import useApiQuery from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile';
import { PRIVATE_TAG_ADDRESS } from 'stubs/account';
import AccountPageDescription from 'ui/shared/AccountPageDescription';
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 AddressTagListItem from './AddressTagTable/AddressTagListItem';
......@@ -16,7 +15,12 @@ import AddressTagTable from './AddressTagTable/AddressTagTable';
import DeletePrivateTagModal from './DeletePrivateTagModal';
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 deleteModalProps = useDisclosure();
......@@ -45,46 +49,25 @@ const PrivateAddressTags = () => {
deleteModalProps.onClose();
}, [ 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) {
return <DataFetchAlert/>;
}
const list = isMobile ? (
<Box>
{ addressTagsData.map((item: AddressTag) => (
{ addressTagsData?.map((item: AddressTag, index: number) => (
<AddressTagListItem
item={ item }
key={ item.id }
key={ item.id + (isPlaceholderData ? index : '') }
onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick }
isLoading={ isPlaceholderData }
/>
)) }
</Box>
) : (
<AddressTagTable
isLoading={ isPlaceholderData }
data={ addressTagsData }
onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick }
......@@ -93,15 +76,20 @@ const PrivateAddressTags = () => {
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 }
<Box marginTop={ 8 }>
<Skeleton isLoaded={ !isPlaceholderData } display="inline-block">
<Button
size="lg"
onClick={ addressModalProps.onOpen }
>
Add address tag
</Button>
</Skeleton>
</Box>
<AddressModal { ...addressModalProps } onClose={ onAddressModalClose } data={ addressModalData }/>
{ deleteModalData && (
......
......@@ -17,14 +17,15 @@ interface Props {
address: Pick<Address, 'hash' | 'is_contract' | 'implementation_name' | 'watchlist_names' | 'watchlist_address_id'>;
token?: TokenInfo | null;
isLinkDisabled?: boolean;
isLoading?: boolean;
}
const AddressHeadingInfo = ({ address, token, isLinkDisabled }: Props) => {
const AddressHeadingInfo = ({ address, token, isLinkDisabled, isLoading }: Props) => {
const isMobile = useIsMobile();
return (
<Flex alignItems="center">
<AddressIcon address={ address }/>
<AddressIcon address={ address } isLoading={ isLoading }/>
<AddressLink
type="address"
hash={ address.hash }
......@@ -33,13 +34,14 @@ const AddressHeadingInfo = ({ address, token, isLinkDisabled }: Props) => {
fontWeight={ 500 }
truncation={ isMobile ? 'constant' : 'none' }
isDisabled={ isLinkDisabled }
isLoading={ isLoading }
/>
<CopyToClipboard text={ address.hash }/>
{ address.is_contract && token && <AddressAddToWallet ml={ 2 } token={ token }/> }
{ !address.is_contract && config.isAccountSupported && (
<CopyToClipboard text={ address.hash } isLoading={ isLoading }/>
{ !isLoading && address.is_contract && token && <AddressAddToWallet ml={ 2 } token={ token }/> }
{ !isLoading && !address.is_contract && config.isAccountSupported && (
<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>
);
};
......
......@@ -11,15 +11,16 @@ import CopyToClipboard from 'ui/shared/CopyToClipboard';
interface Props {
address: AddressParam;
subtitle?: string;
isLoading?: boolean;
}
const AddressSnippet = ({ address, subtitle }: Props) => {
const AddressSnippet = ({ address, subtitle, isLoading }: Props) => {
return (
<Box maxW="100%">
<Address>
<AddressIcon address={ address }/>
<AddressLink type="address" hash={ address.hash } fontWeight="600" ml={ 2 }/>
<CopyToClipboard text={ address.hash } ml={ 1 }/>
<AddressIcon address={ address } isLoading={ isLoading }/>
<AddressLink type="address" hash={ address.hash } fontWeight="600" ml={ 2 } isLoading={ isLoading }/>
<CopyToClipboard text={ address.hash } ml={ 1 } isLoading={ isLoading }/>
</Address>
{ subtitle && <Text fontSize="sm" variant="secondary" mt={ 0.5 } ml={ 8 }>{ subtitle }</Text> }
</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 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 [ copied, setCopied ] = useState(false);
// 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}
}
}, [ hasCopied ]);
if (isLoading) {
return <Skeleton boxSize={ 5 } className={ className } borderRadius="sm" flexShrink={ 0 }/>;
}
return (
<Tooltip label={ copied ? 'Copied' : 'Copy to clipboard' } isOpen={ isOpen || copied }>
<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 React from 'react';
......@@ -9,18 +9,21 @@ interface Props extends Omit<HTMLChakraProps<'div'>, 'title'> {
hint: string;
children: React.ReactNode;
note?: string;
isLoading?: boolean;
}
const DetailsInfoItem = ({ title, hint, note, children, id, ...styles }: Props) => {
const DetailsInfoItem = ({ title, hint, note, children, id, isLoading, ...styles }: Props) => {
return (
<>
<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">
<Hint label={ hint }/>
<Hint label={ hint } isLoading={ isLoading }/>
<Skeleton isLoaded={ !isLoading }>
<Text fontWeight={{ base: 700, lg: 500 }}>
{ title }
{ note && <Text fontWeight={ 500 } variant="secondary" fontSize="xs" className="note" align="right">{ note }</Text> }
</Text>
</Skeleton>
</Flex>
</GridItem>
<GridItem
......
......@@ -7,18 +7,22 @@ import isSelfHosted from 'lib/isSelfHosted';
import AdBanner from 'ui/shared/ad/AdBanner';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
const DetailsSponsoredItem = () => {
interface Props {
isLoading?: boolean;
}
const DetailsSponsoredItem = ({ isLoading }: Props) => {
const isMobile = useIsMobile();
const hasAdblockCookie = cookies.get(cookies.NAMES.ADBLOCK_DETECTED);
if (hasAdblockCookie || !isSelfHosted()) {
if (!isSelfHosted() || hasAdblockCookie) {
return null;
}
if (isMobile) {
return (
<GridItem mt={ 5 }>
<AdBanner justifyContent="center"/>
<AdBanner mx="auto" isLoading={ isLoading } display="flex" justifyContent="center"/>
</GridItem>
);
}
......@@ -27,8 +31,9 @@ const DetailsSponsoredItem = () => {
<DetailsInfoItem
title="Sponsored"
hint="Sponsored banner advertisement"
isLoading={ isLoading }
>
<AdBanner/>
<AdBanner isLoading={ isLoading }/>
</DetailsInfoItem>
);
};
......
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 InfoIcon from 'icons/info.svg';
......@@ -8,9 +8,10 @@ interface Props {
label: string | React.ReactNode;
className?: string;
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
const { isOpen, onOpen, onToggle, onClose } = useDisclosure();
......@@ -19,6 +20,10 @@ const Hint = ({ label, className, tooltipProps }: Props) => {
onToggle();
}, [ onToggle ]);
if (isLoading) {
return <Skeleton boxSize={ 5 } borderRadius="sm"/>;
}
return (
<Tooltip
label={ label }
......
......@@ -26,6 +26,8 @@ const ListItemMobile = ({ children, className, isAnimated }: Props) => {
borderBottomWidth: '1px',
}}
className={ className }
fontSize="16px"
lineHeight="20px"
>
{ children }
</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 eastArrowIcon from 'icons/arrows/east.svg';
......@@ -13,10 +13,12 @@ type Props = {
className?: string;
backLinkLabel?: 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 = (
<Skeleton isLoaded={ !isLoading }>
<Heading
as="h1"
size="lg"
......@@ -25,6 +27,7 @@ const PageTitle = ({ text, additionalsLeft, additionalsRight, withTextAd, backLi
>
{ text }
</Heading>
</Skeleton>
);
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 CopyToClipboard from './CopyToClipboard';
......@@ -11,33 +11,36 @@ interface Props {
beforeSlot?: React.ReactNode;
textareaMaxHeight?: string;
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
const bgColor = useColorModeValue('#f5f5f6', '#1a1b1b');
return (
<Box className={ className } as="section" title={ title }>
{ (title || rightSlot || showCopy) && (
<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 }
{ typeof data === 'string' && showCopy && <CopyToClipboard text={ data }/> }
{ typeof data === 'string' && showCopy && <CopyToClipboard text={ data } isLoading={ isLoading }/> }
</Flex>
) }
{ beforeSlot }
<Box
<Skeleton
p={ 4 }
bgColor={ bgColor }
bgColor={ isLoading ? 'inherit' : bgColor }
maxH={ textareaMaxHeight || '400px' }
minH={ isLoading ? '200px' : undefined }
fontSize="sm"
borderRadius="md"
wordBreak="break-all"
whiteSpace="pre-wrap"
overflowY="auto"
isLoaded={ !isLoading }
>
{ data }
</Box>
</Skeleton>
</Box>
);
};
......
......@@ -175,6 +175,7 @@ const RoutedTabs = ({ tabs, tabListProps, rightSlot, stickyEnabled, className, .
}
onItemClick={ handleTabChange }
buttonRef={ tabsRefs[index] }
size={ themeProps.size || 'md' }
/>
);
}
......
import type {
ButtonProps } from '@chakra-ui/react';
import { Popover,
PopoverTrigger,
PopoverContent,
......@@ -20,9 +22,10 @@ interface Props {
styles?: StyleProps;
onItemClick: (index: number) => void;
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 handleItemClick = React.useCallback((event: React.MouseEvent<HTMLButtonElement>) => {
......@@ -40,6 +43,7 @@ const RoutedTabsMenu = ({ tabs, tabsCut, isActive, styles, onItemClick, buttonRe
variant="ghost"
isActive={ isOpen || isActive }
ref={ buttonRef }
size={ size }
{ ...styles }
>
{ 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 React from 'react';
......@@ -13,9 +13,10 @@ interface Props {
url: string;
alert?: string;
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 alertContent = (() => {
......@@ -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
className={ className }
status="warning"
......@@ -57,14 +61,39 @@ const SocketNewItemsNotice = ({ children, className, url, num, alert, type = 'tr
py="6px"
fontWeight={ 400 }
fontSize="sm"
bgColor={ useColorModeValue('orange.50', transparentize('orange.200', 0.16)(theme)) }
color={ useColorModeValue('blackAlpha.800', 'whiteAlpha.800') }
bgColor={ bgColor }
color={ color }
>
{ alertContent }
</Alert>
);
) : <Skeleton className={ className } h="33px"/>;
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 DeleteIcon from 'icons/delete.svg';
......@@ -8,33 +8,47 @@ import usePreventFocusAfterModalClosing from 'lib/hooks/usePreventFocusAfterModa
type Props = {
onEditClick: () => void;
onDeleteClick: () => void;
isLoading?: boolean;
}
const TableItemActionButtons = ({ onEditClick, onDeleteClick }: Props) => {
const TableItemActionButtons = ({ onEditClick, onDeleteClick, isLoading }: Props) => {
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 (
<HStack spacing={ 6 } alignSelf="flex-end">
<Tooltip label="Edit">
<IconButton
aria-label="edit"
variant="simple"
w="30px"
h="30px"
boxSize={ 5 }
onClick={ onEditClick }
icon={ <Icon as={ EditIcon } w="20px" h="20px"/> }
icon={ <EditIcon/> }
onFocusCapture={ onFocusCapture }
display="inline-block"
flexShrink={ 0 }
borderRadius="none"
/>
</Tooltip>
<Tooltip label="Delete">
<IconButton
aria-label="delete"
variant="simple"
w="30px"
h="30px"
boxSize={ 5 }
onClick={ onDeleteClick }
icon={ <Icon as={ DeleteIcon } w="20px" h="20px"/> }
icon={ <DeleteIcon/> }
onFocusCapture={ onFocusCapture }
display="inline-block"
flexShrink={ 0 }
borderRadius="none"
/>
</Tooltip>
</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 appConfig from 'configs/app/config';
......@@ -27,9 +27,15 @@ interface Props {
hash?: string;
name?: string | null;
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 ? [
'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/',
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 React from 'react';
......@@ -13,10 +13,11 @@ interface Props {
className?: string;
isDisabled?: boolean;
truncation?: 'dynamic' | 'constant';
isLoading?: boolean;
}
const TokenTransferNft = ({ hash, id, className, isDisabled, truncation = 'dynamic' }: Props) => {
const Component = isDisabled ? Box : LinkInternal;
const TokenTransferNft = ({ hash, id, className, isDisabled, isLoading, truncation = 'dynamic' }: Props) => {
const Component = isDisabled || isLoading ? Box : LinkInternal;
return (
<Component
......@@ -28,10 +29,12 @@ const TokenTransferNft = ({ hash, id, className, isDisabled, truncation = 'dynam
w="100%"
className={ className }
>
<Icon as={ nftPlaceholder } boxSize="30px" mr={ 1 } color="inherit"/>
<Box maxW="calc(100% - 34px)">
<Skeleton isLoaded={ !isLoading } boxSize="30px" mr={ 1 } borderRadius="base">
<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 }/> }
</Box>
</Skeleton>
</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 React from 'react';
......@@ -6,21 +6,28 @@ interface Props {
className?: string;
value: number;
colorScheme?: 'green' | 'gray';
isLoading?: boolean;
}
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 colorGrayScheme = useColorModeValue('gray.500', 'gray.400');
const color = colorScheme === 'gray' ? colorGrayScheme : 'green.500';
return (
<Flex className={ className } alignItems="center">
<Box bg={ useColorModeValue('blackAlpha.200', 'whiteAlpha.200') } w={ `${ WIDTH }px` } h="4px" borderRadius="full" overflow="hidden">
<Flex className={ className } alignItems="center" columnGap="10px">
<Skeleton isLoaded={ !isLoading } w={ `${ WIDTH }px` } h="4px" borderRadius="full" overflow="hidden">
<Box bg={ useColorModeValue('blackAlpha.200', 'whiteAlpha.200') } h="100%">
<Box bg={ color } w={ valueString } h="100%"/>
</Box>
<Text color={ color } ml="10px" fontWeight="bold">{ valueString }</Text>
</Skeleton>
<Skeleton isLoaded={ !isLoading } color={ color } fontWeight="bold">
<span>
{ valueString }
</span>
</Skeleton>
</Flex>
);
};
......
import { chakra } from '@chakra-ui/react';
import { chakra, Skeleton } from '@chakra-ui/react';
import React from 'react';
import appConfig from 'configs/app/config';
......@@ -9,18 +9,26 @@ import isSelfHosted from 'lib/isSelfHosted';
import AdbutlerBanner from './AdbutlerBanner';
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);
if (!isSelfHosted() || hasAdblockCookie) {
return null;
}
if (appConfig.ad.adButlerOn) {
return <AdbutlerBanner className={ className }/>;
}
const content = appConfig.ad.adButlerOn ? <AdbutlerBanner/> : <CoinzillaBanner/>;
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);
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 { useAppContext } from 'lib/appContext';
......@@ -55,7 +55,7 @@ const CoinzillaTextAd = ({ className }: {className?: string}) => {
}
if (isLoading) {
return <Box className={ className } h={{ base: 12, lg: 6 }}/>;
return <Skeleton className={ className } h={{ base: 12, lg: 6 }} maxW="1000px"/>;
}
if (!adData) {
......
import { Box, chakra, Tooltip } from '@chakra-ui/react';
import { Box, chakra, Skeleton, Tooltip } from '@chakra-ui/react';
import React from 'react';
import Jazzicon, { jsNumberForAddress } from 'react-jazzicon';
......@@ -9,9 +9,14 @@ import AddressContractIcon from 'ui/shared/address/AddressContractIcon';
type Props = {
address: Pick<AddressParam, 'hash' | 'is_contract' | 'implementation_name'>;
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) {
return (
<AddressContractIcon className={ className }/>
......@@ -20,7 +25,7 @@ const AddressIcon = ({ address, className }: Props) => {
return (
<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) }/>
</Box>
</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 type { HTMLAttributeAnchorTarget } from 'react';
import React from 'react';
......@@ -17,6 +17,7 @@ type CommonProps = {
isDisabled?: boolean;
fontWeight?: string;
alias?: string | null;
isLoading?: boolean;
}
type AddressTokenTxProps = {
......@@ -39,7 +40,7 @@ type AddressTokenProps = {
type Props = CommonProps & (AddressTokenTxProps | BlockProps | AddressTokenProps);
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();
let url;
......@@ -81,6 +82,10 @@ const AddressLink = (props: Props) => {
}
})();
if (isLoading) {
return <Skeleton className={ className } overflow="hidden" whiteSpace="nowrap">{ content }</Skeleton>;
}
if (isDisabled) {
return (
<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 {
imageUrl: string | null;
animationUrl: string | null;
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);
React.useEffect(() => {
if (!animationUrl) {
if (!animationUrl || isLoading) {
return;
}
......@@ -45,9 +46,9 @@ const NftMedia = ({ imageUrl, animationUrl, className }: Props) => {
setType('image');
});
}, [ animationUrl ]);
}, [ animationUrl, isLoading ]);
if (!type) {
if (!type || isLoading) {
return (
<AspectRatio
className={ className }
......
import { Flex, Skeleton, chakra } from '@chakra-ui/react';
import React from 'react';
import type { RoutedTab } from '../RoutedTabs/types';
interface Props {
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 (
<Flex my={ 8 } className={ className }>
<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 { useRouter } from 'next/router';
import React from 'react';
import type { Address } from 'types/api/address';
import type { TokenInfo } from 'types/api/token';
import useApiQuery from 'lib/api/useApiQuery';
import AddressHeadingInfo from 'ui/shared/AddressHeadingInfo';
interface Props {
tokenQuery: UseQueryResult<TokenInfo>;
contractQuery: UseQueryResult<Address>;
}
const TokenContractInfo = ({ tokenQuery }: 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>
);
}
const TokenContractInfo = ({ tokenQuery, contractQuery }: Props) => {
// we show error in parent component, this is only for TS
if (tokenQuery.isError) {
return null;
}
const address = {
hash: tokenQuery.data.address,
hash: tokenQuery.data?.address || '',
is_contract: true,
implementation_name: null,
watchlist_names: [],
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);
......@@ -8,11 +8,11 @@ import type { TokenInfo } from 'types/api/token';
import useApiQuery from 'lib/api/useApiQuery';
import getCurrencyValue from 'lib/getCurrencyValue';
import { TOKEN_COUNTERS } from 'stubs/token';
import type { TokenTabs } from 'ui/pages/Token';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import DetailsSponsoredItem from 'ui/shared/DetailsSponsoredItem';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import DetailsSkeletonRow from 'ui/shared/skeletons/DetailsSkeletonRow';
interface Props {
tokenQuery: UseQueryResult<TokenInfo>;
......@@ -23,7 +23,7 @@ const TokenDetails = ({ tokenQuery }: Props) => {
const tokenCountersQuery = useApiQuery('token_counters', {
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) => () => {
......@@ -56,32 +56,19 @@ const TokenDetails = ({ tokenQuery }: Props) => {
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 {
exchange_rate: exchangeRate,
total_supply: totalSupply,
decimals,
symbol,
type,
} = tokenQuery.data;
} = tokenQuery.data || {};
let marketcap;
let totalSupplyValue;
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;
totalSupplyValue = totalValue?.valueStr;
} else {
......@@ -119,40 +106,50 @@ const TokenDetails = ({ tokenQuery }: Props) => {
alignSelf="center"
wordBreak="break-word"
whiteSpace="pre-wrap"
isLoading={ tokenQuery.isPlaceholderData }
>
<Skeleton isLoaded={ !tokenQuery.isPlaceholderData }>
<Flex w="100%">
<Box whiteSpace="nowrap" overflow="hidden">
<HashStringShortenDynamic hash={ totalSupplyValue || '0' }/>
</Box>
<Box flexShrink={ 0 }> { symbol || '' }</Box>
</Flex>
</Skeleton>
</DetailsInfoItem>
<DetailsInfoItem
title="Holders"
hint="Number of accounts holding the token"
alignSelf="center"
isLoading={ tokenQuery.isPlaceholderData }
>
{ tokenCountersQuery.isLoading && <Skeleton w={ 20 } h={ 4 }/> }
{ !tokenCountersQuery.isLoading && countersItem('token_holders_count') }
<Skeleton isLoaded={ !tokenCountersQuery.isPlaceholderData }>
{ countersItem('token_holders_count') }
</Skeleton>
</DetailsInfoItem>
<DetailsInfoItem
title="Transfers"
hint="Number of transfer for the token"
alignSelf="center"
isLoading={ tokenQuery.isPlaceholderData }
>
{ tokenCountersQuery.isLoading && <Skeleton w={ 20 } h={ 4 }/> }
{ !tokenCountersQuery.isLoading && countersItem('transfers_count') }
<Skeleton isLoaded={ !tokenCountersQuery.isPlaceholderData }>
{ countersItem('transfers_count') }
</Skeleton>
</DetailsInfoItem>
{ decimals && (
<DetailsInfoItem
title="Decimals"
hint="Number of digits that come after the decimal place when displaying token value"
alignSelf="center"
isLoading={ tokenQuery.isPlaceholderData }
>
<Skeleton isLoaded={ !tokenQuery.isPlaceholderData } minW={ 6 }>
{ decimals }
</Skeleton>
</DetailsInfoItem>
) }
<DetailsSponsoredItem/>
<DetailsSponsoredItem isLoading={ tokenQuery.isPlaceholderData }/>
</Grid>
);
};
......
import { Box } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query';
import React from 'react';
......@@ -38,15 +39,28 @@ const TokenHoldersContent = ({ holdersQuery, token }: Props) => {
const content = items && token ? (
<>
{ !isMobile && <TokenHoldersTable data={ items } token={ token } top={ holdersQuery.isPaginationVisible ? 80 : 0 }/> }
{ isMobile && <TokenHoldersList data={ items } token={ token }/> }
<Box display={{ base: 'none', lg: 'block' }}>
<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;
return (
<DataListDisplay
isError={ holdersQuery.isError }
isLoading={ holdersQuery.isLoading }
isLoading={ false }
items={ holdersQuery.data?.items }
skeletonProps={{ skeletonDesktopColumns: [ '100%', '300px', '175px' ] }}
emptyText="There are no holders for this token."
......
......@@ -8,16 +8,18 @@ import TokenHoldersListItem from './TokenHoldersListItem';
interface Props {
data: Array<TokenHolder>;
token: TokenInfo;
isLoading?: boolean;
}
const TokenHoldersList = ({ data, token }: Props) => {
const TokenHoldersList = ({ data, token, isLoading }: Props) => {
return (
<Box>
{ data.map((item) => (
{ data.map((item, index) => (
<TokenHoldersListItem
key={ item.address.hash }
key={ item.address.hash + (isLoading ? index : '') }
token={ token }
holder={ item }
isLoading={ isLoading }
/>
)) }
</Box>
......
import { Flex } from '@chakra-ui/react';
import { Flex, Skeleton } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import React from 'react';
......@@ -14,25 +14,37 @@ import Utilization from 'ui/shared/Utilization/Utilization';
interface Props {
holder: TokenHolder;
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();
return (
<ListItemMobile rowGap={ 3 }>
<Address display="inline-flex" maxW="100%" lineHeight="30px">
<AddressIcon address={ holder.address }/>
<AddressLink type="address" ml={ 2 } fontWeight="700" hash={ holder.address.hash } alias={ holder.address.name } flexGrow={ 1 }/>
<CopyToClipboard text={ holder.address.hash }/>
<Address display="inline-flex" maxW="100%">
<AddressIcon address={ holder.address } isLoading={ isLoading }/>
<AddressLink
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>
<Flex justifyContent="space-between" alignItems="center" width="100%">
<Skeleton isLoaded={ !isLoading } display="inline-block">
{ quantity }
</Skeleton>
{ token.total_supply && (
<Utilization
value={ BigNumber(holder.value).div(BigNumber(token.total_supply)).dp(4).toNumber() }
colorScheme="green"
ml={ 6 }
isLoading={ isLoading }
/>
) }
</Flex>
......
......@@ -10,9 +10,10 @@ interface Props {
data: Array<TokenHolder>;
token: TokenInfo;
top: number;
isLoading?: boolean;
}
const TokenHoldersTable = ({ data, token, top }: Props) => {
const TokenHoldersTable = ({ data, token, top, isLoading }: Props) => {
return (
<Table variant="simple" size="sm">
<Thead top={ top }>
......@@ -23,8 +24,8 @@ const TokenHoldersTable = ({ data, token, top }: Props) => {
</Tr>
</Thead>
<Tbody>
{ data.map((item) => (
<TokenHoldersTableItem key={ item.address.hash } holder={ item } token={ token }/>
{ data.map((item, index) => (
<TokenHoldersTableItem key={ item.address.hash + (isLoading ? index : '') } holder={ item } token={ token } isLoading={ isLoading }/>
)) }
</Tbody>
</Table>
......
import { Tr, Td } from '@chakra-ui/react';
import { Tr, Td, Skeleton } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import React from 'react';
......@@ -13,29 +13,41 @@ import Utilization from 'ui/shared/Utilization/Utilization';
type Props = {
holder: TokenHolder;
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();
return (
<Tr>
<Td>
<Address display="inline-flex" maxW="100%" lineHeight="30px">
<AddressIcon address={ holder.address }/>
<AddressLink type="address" ml={ 2 } fontWeight="700" hash={ holder.address.hash } alias={ holder.address.name } flexGrow={ 1 }/>
<CopyToClipboard text={ holder.address.hash }/>
<Td verticalAlign="middle">
<Address display="inline-flex" maxW="100%">
<AddressIcon address={ holder.address } isLoading={ isLoading }/>
<AddressLink
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>
</Td>
<Td isNumeric>
<Td verticalAlign="middle" isNumeric>
<Skeleton isLoaded={ !isLoading } display="inline-block">
{ quantity }
</Skeleton>
</Td>
{ token.total_supply && (
<Td isNumeric>
<Td verticalAlign="middle" isNumeric>
<Utilization
value={ BigNumber(holder.value).div(BigNumber(token.total_supply)).dp(4).toNumber() }
colorScheme="green"
display="inline-flex"
isLoading={ isLoading }
/>
</Td>
) }
......
import { Grid, Skeleton } from '@chakra-ui/react';
import { Grid } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query';
import React from 'react';
......@@ -28,21 +28,6 @@ const TokenInventory = ({ inventoryQuery }: Props) => {
</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 content = items ? (
......@@ -52,19 +37,25 @@ const TokenInventory = ({ inventoryQuery }: Props) => {
rowGap={{ base: 3, lg: 6 }}
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>
) : null;
return (
<DataListDisplay
isError={ inventoryQuery.isError }
isLoading={ inventoryQuery.isLoading }
isLoading={ false }
items={ items }
emptyText="There are no tokens."
content={ content }
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 React from 'react';
......@@ -11,9 +11,9 @@ import LinkInternal from 'ui/shared/LinkInternal';
import NftMedia from 'ui/shared/nft/NftMedia';
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 (
<LinkBox
w={{ base: '100%', lg: '210px' }}
......@@ -32,6 +32,7 @@ const NFTItem = ({ item }: Props) => {
mb="18px"
imageUrl={ item.image_url }
animationUrl={ item.animation_url }
isLoading={ isLoading }
/>
</LinkOverlay>
</NextLink>
......@@ -39,13 +40,16 @@ const NFTItem = ({ item }: Props) => {
<Flex mb={ 2 } ml={ 1 }>
<Text whiteSpace="pre" variant="secondary">ID# </Text>
<TruncatedTextTooltip label={ item.id }>
<Skeleton isLoaded={ !isLoading } overflow="hidden">
<LinkInternal
overflow="hidden"
whiteSpace="nowrap"
textOverflow="ellipsis"
whiteSpace="nowrap"
display="block"
>
{ item.id }
</LinkInternal>
</Skeleton>
</TruncatedTextTooltip>
</Flex>
) }
......@@ -53,8 +57,8 @@ const NFTItem = ({ item }: Props) => {
<Flex mb={ 2 } ml={ 1 }>
<Text whiteSpace="pre" variant="secondary" mr={ 2 } lineHeight="24px">Owner</Text>
<Address>
<Hide below="lg" ssr={ false }><AddressIcon address={ item.owner } mr={ 1 }/></Hide>
<AddressLink hash={ item.owner.hash } alias={ item.owner.name } type="address" truncation="constant"/>
<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" isLoading={ isLoading }/>
</Address>
</Flex>
) }
......
import { Hide, Show } from '@chakra-ui/react';
import { Box } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import React from 'react';
import type { SocketMessage } from 'lib/socket/types';
import type { TokenInfo } from 'types/api/token';
import type { TokenTransferResponse } from 'types/api/tokenTransfer';
import useGradualIncrement from 'lib/hooks/useGradualIncrement';
......@@ -14,7 +15,7 @@ import ActionBar from 'ui/shared/ActionBar';
import DataListDisplay from 'ui/shared/DataListDisplay';
import Pagination 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 TokenTransferTable from 'ui/token/TokenTransfer/TokenTransferTable';
......@@ -24,12 +25,13 @@ type Props = {
isPaginationVisible: boolean;
};
tokenId?: string;
token?: TokenInfo;
}
const TokenTransfer = ({ transfersQuery, tokenId }: Props) => {
const TokenTransfer = ({ transfersQuery, tokenId, token }: Props) => {
const isMobile = useIsMobile();
const router = useRouter();
const { isError, isLoading, data, pagination, isPaginationVisible } = transfersQuery;
const { isError, isLoading, isPlaceholderData, data, pagination, isPaginationVisible } = transfersQuery;
const [ newItemsCount, setNewItemsCount ] = useGradualIncrement(0);
const [ socketAlert, setSocketAlert ] = React.useState('');
......@@ -61,7 +63,7 @@ const TokenTransfer = ({ transfersQuery, tokenId }: Props) => {
const content = data?.items ? (
<>
<Hide below="lg" ssr={ false }>
<Box display={{ base: 'none', lg: 'block' }}>
<TokenTransferTable
data={ data?.items }
top={ isPaginationVisible ? 80 : 0 }
......@@ -69,20 +71,22 @@ const TokenTransfer = ({ transfersQuery, tokenId }: Props) => {
socketInfoAlert={ socketAlert }
socketInfoNum={ newItemsCount }
tokenId={ tokenId }
token={ token }
isLoading={ isPlaceholderData }
/>
</Hide>
<Show below="lg" ssr={ false }>
</Box>
<Box display={{ base: 'block', lg: 'none' }}>
{ pagination.page === 1 && (
<SocketNewItemsNotice
<SocketNewItemsNotice.Mobile
url={ window.location.href }
num={ newItemsCount }
alert={ socketAlert }
type="token_transfer"
borderBottomRadius={ 0 }
isLoading={ isPlaceholderData }
/>
) }
<TokenTransferList data={ data?.items } tokenId={ tokenId }/>
</Show>
<TokenTransferList data={ data?.items } tokenId={ tokenId } isLoading={ isPlaceholderData }/>
</Box>
</>
) : null;
......@@ -95,7 +99,7 @@ const TokenTransfer = ({ transfersQuery, tokenId }: Props) => {
return (
<DataListDisplay
isError={ isError }
isLoading={ isLoading }
isLoading={ !isPlaceholderData && isLoading }
items={ data?.items }
skeletonProps={{
isLongSkeleton: true,
......
......@@ -8,9 +8,10 @@ import TokenTransferListItem from 'ui/token/TokenTransfer/TokenTransferListItem'
interface Props {
data: Array<TokenTransfer>;
tokenId?: string;
isLoading?: boolean;
}
const TokenTransferList = ({ data, tokenId }: Props) => {
const TokenTransferList = ({ data, tokenId, isLoading }: Props) => {
return (
<Box>
{ data.map((item, index) => (
......@@ -18,6 +19,7 @@ const TokenTransferList = ({ data, tokenId }: Props) => {
key={ index }
{ ...item }
tokenId={ tokenId }
isLoading={ isLoading }
/>
)) }
</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 React from 'react';
......@@ -11,11 +11,12 @@ import trimTokenSymbol from 'lib/token/trimTokenSymbol';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import Tag from 'ui/shared/chakra/Tag';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
import TokenTransferNft from 'ui/shared/TokenTransfer/TokenTransferNft';
type Props = TokenTransfer & {tokenId?: string};
type Props = TokenTransfer & { tokenId?: string; isLoading?: boolean };
const TokenTransferListItem = ({
token,
......@@ -26,6 +27,7 @@ const TokenTransferListItem = ({
method,
timestamp,
tokenId,
isLoading,
}: Props) => {
const value = (() => {
if (!('value' in total)) {
......@@ -43,46 +45,68 @@ const TokenTransferListItem = ({
<ListItemMobile rowGap={ 3 } isAnimated>
<Flex justifyContent="space-between" alignItems="center" lineHeight="24px" width="100%">
<Flex>
<Skeleton isLoaded={ !isLoading } boxSize="30px" mr={ 2 }>
<Icon
as={ transactionIcon }
boxSize="30px"
mr={ 2 }
color={ iconColor }
/>
</Skeleton>
<Address width="100%">
<AddressLink
hash={ txHash }
type="transaction"
fontWeight="700"
truncation="constant"
isLoading={ isLoading }
/>
</Address>
</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>
{ method && <Tag colorScheme="gray">{ method }</Tag> }
{ method && <Tag isLoading={ isLoading }>{ method }</Tag> }
<Flex w="100%" columnGap={ 3 }>
<Address width="50%">
<AddressIcon address={ from }/>
<AddressLink ml={ 2 } fontWeight="500" hash={ from.hash } type="address_token" tokenHash={ token.address }/>
<CopyToClipboard text={ from.hash }/>
<AddressIcon address={ from } isLoading={ isLoading }/>
<AddressLink ml={ 2 } fontWeight="500" hash={ from.hash } type="address_token" tokenHash={ token.address } isLoading={ isLoading }/>
<CopyToClipboard text={ from.hash } isLoading={ isLoading }/>
</Address>
<Skeleton isLoaded={ !isLoading } boxSize={ 6 }>
<Icon as={ eastArrowIcon } boxSize={ 6 } color="gray.500"/>
</Skeleton>
<Address width="50%">
<AddressIcon address={ to }/>
<AddressLink ml={ 2 } fontWeight="500" hash={ to.hash } type="address_token" tokenHash={ token.address }/>
<CopyToClipboard text={ to.hash }/>
<AddressIcon address={ to } isLoading={ isLoading }/>
<AddressLink ml={ 2 } fontWeight="500" hash={ to.hash } type="address_token" tokenHash={ token.address } isLoading={ isLoading }/>
<CopyToClipboard text={ to.hash } isLoading={ isLoading }/>
</Address>
</Flex>
{ value && (token.type === 'ERC-20' || token.type === 'ERC-1155') && (
<Flex columnGap={ 2 } w="100%">
<Text fontWeight={ 500 } flexShrink={ 0 }>Value</Text>
<Text variant="secondary">{ value }</Text>
<Text>{ trimTokenSymbol(token.symbol) }</Text>
<Skeleton isLoaded={ !isLoading } flexShrink={ 0 } fontWeight={ 500 }>
Value
</Skeleton>
<Skeleton isLoaded={ !isLoading } variant="secondary">
{ value }
</Skeleton>
<Skeleton isLoaded={ !isLoading }>{ trimTokenSymbol(token.symbol) }</Skeleton>
</Flex>
) }
{ '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) }/> }
{ '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) }
isLoading={ isLoading }
/>
) }
</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 type { TokenInfo } from 'types/api/token';
import type { TokenTransfer } from 'types/api/tokenTransfer';
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 TokenTransferTableItem from 'ui/token/TokenTransfer/TokenTransferTableItem';
......@@ -15,11 +16,12 @@ interface Props {
socketInfoAlert?: string;
socketInfoNum?: number;
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 tokenSymbol = data[0].token.symbol;
return (
<Table variant="simple" size="sm" minW="950px">
......@@ -27,30 +29,26 @@ const TokenTransferTable = ({ data, top, showSocketInfo, socketInfoAlert, socket
<Tr>
<Th width={ tokenType === 'ERC-1155' ? '60%' : '80%' }>Txn hash</Th>
<Th width="164px">Method</Th>
<Th width="148px">From</Th>
<Th width="160px">From</Th>
<Th width="36px" px={ 0 }/>
<Th width="218px" >To</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>
</Thead>
<Tbody>
{ showSocketInfo && (
<Tr>
<Td colSpan={ 10 } p={ 0 }>
<SocketNewItemsNotice
borderRadius={ 0 }
pl="10px"
<SocketNewItemsNotice.Desktop
url={ window.location.href }
alert={ socketInfoAlert }
num={ socketInfoNum }
type="token_transfer"
isLoading={ isLoading }
/>
</Td>
</Tr>
) }
{ data.map((item, index) => (
<TokenTransferTableItem key={ index } { ...item } tokenId={ tokenId }/>
<TokenTransferTableItem key={ index } { ...item } tokenId={ tokenId } isLoading={ isLoading }/>
)) }
</Tbody>
</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 React from 'react';
......@@ -9,10 +9,11 @@ import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import Tag from 'ui/shared/chakra/Tag';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import TokenTransferNft from 'ui/shared/TokenTransfer/TokenTransferNft';
type Props = TokenTransfer & { tokenId?: string }
type Props = TokenTransfer & { tokenId?: string; isLoading?: boolean }
const TokenTransferTableItem = ({
token,
......@@ -23,25 +24,36 @@ const TokenTransferTableItem = ({
method,
timestamp,
tokenId,
isLoading,
}: Props) => {
const timeAgo = useTimeAgoIncrement(timestamp, true);
return (
<Tr alignItems="top">
<Td>
<Grid alignItems="center" gridTemplateColumns="auto 130px" width="fit-content">
<Address display="inline-flex" fontWeight={ 600 } lineHeight="30px">
<AddressLink type="transaction" hash={ txHash }/>
<Grid alignItems="center" gridTemplateColumns="auto 130px" width="fit-content" py="7px">
<Address display="inline-flex" fontWeight={ 600 }>
<AddressLink type="transaction" hash={ txHash } isLoading={ isLoading }/>
</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>
</Td>
<Td>
{ method && <Tag colorScheme="gray">{ method }</Tag> }
{ method ? (
<Box my="3px">
<Tag isLoading={ isLoading } isTruncated>{ method }</Tag>
</Box>
) : null }
</Td>
<Td>
<Address display="inline-flex" maxW="100%" lineHeight="30px">
<AddressIcon address={ from }/>
<Address display="inline-flex" maxW="100%" py="3px">
<AddressIcon address={ from } isLoading={ isLoading }/>
<AddressLink
ml={ 2 }
flexGrow={ 1 }
......@@ -51,16 +63,19 @@ const TokenTransferTableItem = ({
alias={ from.name }
tokenHash={ token.address }
truncation="constant"
isLoading={ isLoading }
/>
<CopyToClipboard text={ from.hash }/>
<CopyToClipboard text={ from.hash } isLoading={ isLoading }/>
</Address>
</Td>
<Td px={ 0 }>
<Skeleton isLoaded={ !isLoading } boxSize={ 6 } my="3px">
<Icon as={ eastArrowIcon } boxSize={ 6 } color="gray.500"/>
</Skeleton>
</Td>
<Td>
<Address display="inline-flex" maxW="100%" lineHeight="30px">
<AddressIcon address={ to }/>
<Address display="inline-flex" maxW="100%" py="3px">
<AddressIcon address={ to } isLoading={ isLoading }/>
<AddressLink
ml={ 2 }
flexGrow={ 1 }
......@@ -70,26 +85,30 @@ const TokenTransferTableItem = ({
alias={ to.name }
tokenHash={ token.address }
truncation="constant"
isLoading={ isLoading }
/>
<CopyToClipboard text={ to.hash }/>
<CopyToClipboard text={ to.hash } isLoading={ isLoading }/>
</Address>
</Td>
{ (token.type === 'ERC-721' || token.type === 'ERC-1155') && (
<Td lineHeight="30px">
<Td>
{ 'token_id' in total ? (
<TokenTransferNft
hash={ token.address }
id={ total.token_id }
justifyContent={ token.type === 'ERC-721' ? 'end' : 'start' }
isDisabled={ Boolean(tokenId && tokenId === total.token_id) }
isLoading={ isLoading }
/>
) : ''
}
</Td>
) }
{ (token.type === 'ERC-20' || token.type === 'ERC-1155') && (
<Td isNumeric verticalAlign="top" lineHeight="30px">
<Td isNumeric verticalAlign="top">
<Skeleton isLoaded={ !isLoading } my="7px">
{ 'value' in total && BigNumber(total.value).div(BigNumber(10 ** Number(total.decimals))).dp(8).toFormat() }
</Skeleton>
</Td>
) }
</Tr>
......
......@@ -65,7 +65,11 @@ const TokenInstanceContent = () => {
});
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 ?
{ id: 'holders', title: 'Holders', component: <TokenHolders holdersQuery={ holdersQuery } token={ tokenInstanceQuery.data?.token }/> } :
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