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 = {
......
This diff is collapsed.
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