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

Merge pull request #840 from blockscout/skeletons/addresses

skeletons: address page
parents 22a89acf ee61d721
...@@ -6,7 +6,7 @@ NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0xf7d4972356e6ae44ae948d0cf19ef2beaf0e574c18099 ...@@ -6,7 +6,7 @@ NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0xf7d4972356e6ae44ae948d0cf19ef2beaf0e574c18099
# network config # network config
NEXT_PUBLIC_NETWORK_NAME=Goerli NEXT_PUBLIC_NETWORK_NAME=Goerli
NEXT_PUBLIC_NETWORK_SHORT_NAME=Goerli NEXT_PUBLIC_NETWORK_SHORT_NAME=Goerli
NEXT_PUBLIC_NETWORK_ASSETS_PATHNAME=ethereum NEXT_PUBLIC_NETWORK_ASSETS_PATHNAME=
NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/network-logos/goerli.svg NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/network-logos/goerli.svg
NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/network-icons/goerli.svg NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/network-icons/goerli.svg
NEXT_PUBLIC_NETWORK_ID=5 NEXT_PUBLIC_NETWORK_ID=5
...@@ -26,4 +26,4 @@ NEXT_PUBLIC_HAS_BEACON_CHAIN=false ...@@ -26,4 +26,4 @@ NEXT_PUBLIC_HAS_BEACON_CHAIN=false
# api config # api config
NEXT_PUBLIC_API_HOST=blockscout-main.k8s-dev.blockscout.com NEXT_PUBLIC_API_HOST=blockscout-main.k8s-dev.blockscout.com
NEXT_PUBLIC_API_BASE_PATH=/ NEXT_PUBLIC_API_BASE_PATH=/
NEXT_PUBLIC_STATS_API_HOST=https://stats-test.aws-k8s.blockscout.com NEXT_PUBLIC_STATS_API_HOST=https://stats-goerli.k8s-dev.blockscout.com
...@@ -9,7 +9,7 @@ NEXT_PUBLIC_WEB3_DISABLE_ADD_TOKEN_TO_WALLET=true ...@@ -9,7 +9,7 @@ NEXT_PUBLIC_WEB3_DISABLE_ADD_TOKEN_TO_WALLET=true
# network config # network config
NEXT_PUBLIC_NETWORK_NAME=Base Göerli NEXT_PUBLIC_NETWORK_NAME=Base Göerli
NEXT_PUBLIC_NETWORK_SHORT_NAME=Base NEXT_PUBLIC_NETWORK_SHORT_NAME=Base
NEXT_PUBLIC_NETWORK_ASSETS_PATHNAME=optimism NEXT_PUBLIC_NETWORK_ASSETS_PATHNAME=
NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/network-logos/base.svg NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/network-logos/base.svg
NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/network-icons/base.svg NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/network-icons/base.svg
NEXT_PUBLIC_NETWORK_ID=84531 NEXT_PUBLIC_NETWORK_ID=84531
......
...@@ -19,7 +19,7 @@ NEXT_PUBLIC_API_SPEC_URL=https://raw.githubusercontent.com/blockscout/blockscout ...@@ -19,7 +19,7 @@ NEXT_PUBLIC_API_SPEC_URL=https://raw.githubusercontent.com/blockscout/blockscout
# network config # network config
NEXT_PUBLIC_NETWORK_NAME=POA NEXT_PUBLIC_NETWORK_NAME=POA
NEXT_PUBLIC_NETWORK_SHORT_NAME=POA NEXT_PUBLIC_NETWORK_SHORT_NAME=POA
NEXT_PUBLIC_NETWORK_ASSETS_PATHNAME=poa NEXT_PUBLIC_NETWORK_ASSETS_PATHNAME=
NEXT_PUBLIC_NETWORK_ID=99 NEXT_PUBLIC_NETWORK_ID=99
NEXT_PUBLIC_NETWORK_CURRENCY_NAME=POA NEXT_PUBLIC_NETWORK_CURRENCY_NAME=POA
NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=POA NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=POA
......
...@@ -29,7 +29,7 @@ NEXT_PUBLIC_IS_L2_NETWORK=false ...@@ -29,7 +29,7 @@ NEXT_PUBLIC_IS_L2_NETWORK=false
# network config # network config
NEXT_PUBLIC_NETWORK_NAME=Blockscout NEXT_PUBLIC_NETWORK_NAME=Blockscout
NEXT_PUBLIC_NETWORK_SHORT_NAME=Blockscout NEXT_PUBLIC_NETWORK_SHORT_NAME=Blockscout
NEXT_PUBLIC_NETWORK_ASSETS_PATHNAME=ethereum NEXT_PUBLIC_NETWORK_ASSETS_PATHNAME=
NEXT_PUBLIC_NETWORK_ID=1 NEXT_PUBLIC_NETWORK_ID=1
NEXT_PUBLIC_NETWORK_CURRENCY_NAME=Ether NEXT_PUBLIC_NETWORK_CURRENCY_NAME=Ether
NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH
......
...@@ -362,8 +362,6 @@ frontend: ...@@ -362,8 +362,6 @@ frontend:
_default: "Base Göerli" _default: "Base Göerli"
NEXT_PUBLIC_NETWORK_SHORT_NAME: NEXT_PUBLIC_NETWORK_SHORT_NAME:
_default: Base _default: Base
NEXT_PUBLIC_NETWORK_ASSETS_PATHNAME:
_default: optimism
NEXT_PUBLIC_NETWORK_ID: NEXT_PUBLIC_NETWORK_ID:
_default: 420 _default: 420
NEXT_PUBLIC_NETWORK_CURRENCY_NAME: NEXT_PUBLIC_NETWORK_CURRENCY_NAME:
......
...@@ -250,8 +250,6 @@ frontend: ...@@ -250,8 +250,6 @@ frontend:
_default: Göerli _default: Göerli
NEXT_PUBLIC_NETWORK_SHORT_NAME: NEXT_PUBLIC_NETWORK_SHORT_NAME:
_default: Göerli _default: Göerli
NEXT_PUBLIC_NETWORK_ASSETS_PATHNAME:
_default: ethereum
NEXT_PUBLIC_NETWORK_LOGO: NEXT_PUBLIC_NETWORK_LOGO:
_default: https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/goerli.svg _default: https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/goerli.svg
NEXT_PUBLIC_NETWORK_ICON: NEXT_PUBLIC_NETWORK_ICON:
......
...@@ -73,8 +73,6 @@ frontend: ...@@ -73,8 +73,6 @@ frontend:
_default: "Base Göerli" _default: "Base Göerli"
NEXT_PUBLIC_NETWORK_SHORT_NAME: NEXT_PUBLIC_NETWORK_SHORT_NAME:
_default: Base _default: Base
NEXT_PUBLIC_NETWORK_ASSETS_PATHNAME:
_default: optimism
NEXT_PUBLIC_NETWORK_ID: NEXT_PUBLIC_NETWORK_ID:
_default: 420 _default: 420
NEXT_PUBLIC_NETWORK_CURRENCY_NAME: NEXT_PUBLIC_NETWORK_CURRENCY_NAME:
......
...@@ -73,8 +73,6 @@ frontend: ...@@ -73,8 +73,6 @@ frontend:
_default: Göerli _default: Göerli
NEXT_PUBLIC_NETWORK_SHORT_NAME: NEXT_PUBLIC_NETWORK_SHORT_NAME:
_default: Göerli _default: Göerli
NEXT_PUBLIC_NETWORK_ASSETS_PATHNAME:
_default: ethereum
NEXT_PUBLIC_NETWORK_LOGO: NEXT_PUBLIC_NETWORK_LOGO:
_default: https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/goerli.svg _default: https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/goerli.svg
NEXT_PUBLIC_NETWORK_ICON: NEXT_PUBLIC_NETWORK_ICON:
......
...@@ -191,7 +191,7 @@ export const RESOURCES = { ...@@ -191,7 +191,7 @@ export const RESOURCES = {
}, },
txs_watchlist: { txs_watchlist: {
path: '/api/v2/transactions/watchlist', path: '/api/v2/transactions/watchlist',
paginationFields: [ 'filter' as const, 'hash' as const, 'inserted_at' as const ], paginationFields: [ 'block_number' as const, 'index' as const, 'items_count' as const ],
filterFields: [ ], filterFields: [ ],
}, },
tx: { tx: {
...@@ -296,7 +296,7 @@ export const RESOURCES = { ...@@ -296,7 +296,7 @@ export const RESOURCES = {
address_tokens: { address_tokens: {
path: '/api/v2/addresses/:hash/tokens', path: '/api/v2/addresses/:hash/tokens',
pathParams: [ 'hash' as const ], pathParams: [ 'hash' as const ],
paginationFields: [ 'items_count' as const, 'token_name' as const, 'token_type' as const, 'value' as const ], paginationFields: [ 'items_count' as const, 'token_name' as const, 'token_type' as const, 'value' as const, 'fiat_value' as const, 'id' as const ],
filterFields: [ 'type' as const ], filterFields: [ 'type' as const ],
}, },
address_withdrawals: { address_withdrawals: {
...@@ -383,7 +383,7 @@ export const RESOURCES = { ...@@ -383,7 +383,7 @@ export const RESOURCES = {
}, },
tokens: { tokens: {
path: '/api/v2/tokens', path: '/api/v2/tokens',
paginationFields: [ 'holder_count' as const, 'items_count' as const, 'name' as const ], paginationFields: [ 'holder_count' as const, 'items_count' as const, 'name' as const, 'market_cap' as const ],
filterFields: [ 'q' as const, 'type' as const ], filterFields: [ 'q' as const, 'type' as const ],
}, },
......
...@@ -7,54 +7,63 @@ export const erc20a: AddressTokenBalance = { ...@@ -7,54 +7,63 @@ export const erc20a: AddressTokenBalance = {
token: tokens.tokenInfoERC20a, token: tokens.tokenInfoERC20a,
token_id: null, token_id: null,
value: '1169320000000000000000000', value: '1169320000000000000000000',
token_instance: null,
}; };
export const erc20b: AddressTokenBalance = { export const erc20b: AddressTokenBalance = {
token: tokens.tokenInfoERC20b, token: tokens.tokenInfoERC20b,
token_id: null, token_id: null,
value: '872500000000', value: '872500000000',
token_instance: null,
}; };
export const erc20c: AddressTokenBalance = { export const erc20c: AddressTokenBalance = {
token: tokens.tokenInfoERC20c, token: tokens.tokenInfoERC20c,
token_id: null, token_id: null,
value: '9852000000000000000000', value: '9852000000000000000000',
token_instance: null,
}; };
export const erc20d: AddressTokenBalance = { export const erc20d: AddressTokenBalance = {
token: tokens.tokenInfoERC20d, token: tokens.tokenInfoERC20d,
token_id: null, token_id: null,
value: '39000000000000000000', value: '39000000000000000000',
token_instance: null,
}; };
export const erc20LongSymbol: AddressTokenBalance = { export const erc20LongSymbol: AddressTokenBalance = {
token: tokens.tokenInfoERC20LongSymbol, token: tokens.tokenInfoERC20LongSymbol,
token_id: null, token_id: null,
value: '39000000000000000000', value: '39000000000000000000',
token_instance: null,
}; };
export const erc721a: AddressTokenBalance = { export const erc721a: AddressTokenBalance = {
token: tokens.tokenInfoERC721a, token: tokens.tokenInfoERC721a,
token_id: null, token_id: null,
value: '51', value: '51',
token_instance: null,
}; };
export const erc721b: AddressTokenBalance = { export const erc721b: AddressTokenBalance = {
token: tokens.tokenInfoERC721b, token: tokens.tokenInfoERC721b,
token_id: null, token_id: null,
value: '1', value: '1',
token_instance: null,
}; };
export const erc721c: AddressTokenBalance = { export const erc721c: AddressTokenBalance = {
token: tokens.tokenInfoERC721c, token: tokens.tokenInfoERC721c,
token_id: null, token_id: null,
value: '5', value: '5',
token_instance: null,
}; };
export const erc721LongSymbol: AddressTokenBalance = { export const erc721LongSymbol: AddressTokenBalance = {
token: tokens.tokenInfoERC721LongSymbol, token: tokens.tokenInfoERC721LongSymbol,
token_id: null, token_id: null,
value: '5', value: '5',
token_instance: null,
}; };
export const erc1155a: AddressTokenBalance = { export const erc1155a: AddressTokenBalance = {
......
...@@ -26,7 +26,7 @@ export const tokenInfoERC20a: TokenInfo = { ...@@ -26,7 +26,7 @@ export const tokenInfoERC20a: TokenInfo = {
symbol: 'HyFi', symbol: 'HyFi',
total_supply: '369000000000000000000000000', total_supply: '369000000000000000000000000',
type: 'ERC-20', type: 'ERC-20',
icon_url: null, icon_url: 'https://example.com/token-icon.png',
}; };
export const tokenInfoERC20b: TokenInfo = { export const tokenInfoERC20b: TokenInfo = {
......
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 getNetworkTitle from 'lib/networks/getNetworkTitle'; import getNetworkTitle from 'lib/networks/getNetworkTitle';
import Accounts from 'ui/pages/Accounts'; import Page from 'ui/shared/Page/Page';
const Accounts = dynamic(() => import('ui/pages/Accounts'), { ssr: false });
const AccountsPage: NextPage = () => { const AccountsPage: NextPage = () => {
const title = `Top Accounts - ${ getNetworkTitle() }`; const title = `Top Accounts - ${ getNetworkTitle() }`;
return ( return (
<> <>
<Head><title>{ title }</title></Head> <Head><title>{ title }</title></Head>
<Accounts/> <Page>
<Accounts/>
</Page>
</> </>
); );
}; };
......
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 type { RoutedQuery } from 'nextjs-routes'; import type { RoutedQuery } from 'nextjs-routes';
import React from 'react'; import React from 'react';
import getSeo from 'lib/next/address/getSeo'; import getSeo from 'lib/next/address/getSeo';
import Address from 'ui/pages/Address'; import Page from 'ui/shared/Page/Page';
const Address = dynamic(() => import('ui/pages/Address'), { ssr: false });
const AddressPage: NextPage<RoutedQuery<'/address/[hash]'>> = ({ hash }: RoutedQuery<'/address/[hash]'>) => { const AddressPage: NextPage<RoutedQuery<'/address/[hash]'>> = ({ hash }: RoutedQuery<'/address/[hash]'>) => {
const { title, description } = getSeo({ hash }); const { title, description } = getSeo({ hash });
...@@ -15,7 +18,9 @@ const AddressPage: NextPage<RoutedQuery<'/address/[hash]'>> = ({ hash }: RoutedQ ...@@ -15,7 +18,9 @@ const AddressPage: NextPage<RoutedQuery<'/address/[hash]'>> = ({ hash }: RoutedQ
<title>{ title }</title> <title>{ title }</title>
<meta name="description" content={ description }/> <meta name="description" content={ description }/>
</Head> </Head>
<Address/> <Page>
<Address/>
</Page>
</> </>
); );
}; };
......
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 getNetworkTitle from 'lib/networks/getNetworkTitle'; import getNetworkTitle from 'lib/networks/getNetworkTitle';
import TokenInstance from 'ui/pages/TokenInstance'; import Page from 'ui/shared/Page/Page';
const TokenInstance = dynamic(() => import('ui/pages/TokenInstance'), { ssr: false });
const TokenInstancePage: NextPage<PageParams> = () => { const TokenInstancePage: NextPage<PageParams> = () => {
const title = getNetworkTitle(); const title = getNetworkTitle();
...@@ -15,7 +17,9 @@ const TokenInstancePage: NextPage<PageParams> = () => { ...@@ -15,7 +17,9 @@ const TokenInstancePage: NextPage<PageParams> = () => {
<Head> <Head>
<title>{ title }</title> <title>{ title }</title>
</Head> </Head>
<TokenInstance/> <Page>
<TokenInstance/>
</Page>
</> </>
); );
}; };
......
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 getNetworkTitle from 'lib/networks/getNetworkTitle'; import getNetworkTitle from 'lib/networks/getNetworkTitle';
import Tokens from 'ui/pages/Tokens'; import Page from 'ui/shared/Page/Page';
const Tokens = dynamic(() => import('ui/pages/Tokens'), { ssr: false });
const TokensPage: NextPage = () => { const TokensPage: NextPage = () => {
const title = getNetworkTitle(); const title = getNetworkTitle();
...@@ -12,7 +15,9 @@ const TokensPage: NextPage = () => { ...@@ -12,7 +15,9 @@ const TokensPage: NextPage = () => {
<Head> <Head>
<title>{ title }</title> <title>{ title }</title>
</Head> </Head>
<Tokens/> <Page>
<Tokens/>
</Page>
</> </>
); );
}; };
......
import type { Address } from 'types/api/address'; import type { Address, AddressCoinBalanceHistoryItem, AddressCounters, AddressTokenBalance } from 'types/api/address';
import type { AddressesItem } from 'types/api/addresses';
import { ADDRESS_HASH } from './addressParams'; import { ADDRESS_HASH } from './addressParams';
import { TOKEN_INFO_ERC_20 } from './token'; import { TOKEN_INFO_ERC_1155, TOKEN_INFO_ERC_20, TOKEN_INFO_ERC_721, TOKEN_INSTANCE } from './token';
import { TX_HASH } from './tx';
export const ADDRESS_INFO: Address = { export const ADDRESS_INFO: Address = {
block_number_balance_updated_at: 8774377, block_number_balance_updated_at: 8774377,
coin_balance: '0', coin_balance: '810941268802273085757',
creation_tx_hash: null, creation_tx_hash: null,
creator_address_hash: null, creator_address_hash: ADDRESS_HASH,
exchange_rate: null, exchange_rate: null,
has_custom_methods_read: false, has_custom_methods_read: false,
has_custom_methods_write: false, has_custom_methods_write: false,
...@@ -32,3 +34,52 @@ export const ADDRESS_INFO: Address = { ...@@ -32,3 +34,52 @@ export const ADDRESS_INFO: Address = {
watchlist_names: [], watchlist_names: [],
watchlist_address_id: null, watchlist_address_id: null,
}; };
export const ADDRESS_COUNTERS: AddressCounters = {
gas_usage_count: '8028907522',
token_transfers_count: '420',
transactions_count: '119020',
validations_count: '0',
};
export const TOP_ADDRESS: AddressesItem = {
coin_balance: '11886682377162664596540805',
tx_count: '1835',
hash: '0x4f7A67464B5976d7547c860109e4432d50AfB38e',
implementation_name: null,
is_contract: false,
is_verified: null,
name: null,
private_tags: [],
public_tags: [ ],
watchlist_names: [],
};
export const ADDRESS_COIN_BALANCE: AddressCoinBalanceHistoryItem = {
block_number: 9004413,
block_timestamp: '2023-05-15T13:16:24Z',
delta: '1000000000000000000',
transaction_hash: TX_HASH,
value: '953427250000000000000000',
};
export const ADDRESS_TOKEN_BALANCE_ERC_20: AddressTokenBalance = {
token: TOKEN_INFO_ERC_20,
token_id: null,
token_instance: null,
value: '1000000000000000000000000',
};
export const ADDRESS_TOKEN_BALANCE_ERC_721: AddressTokenBalance = {
token: TOKEN_INFO_ERC_721,
token_id: null,
token_instance: null,
value: '176',
};
export const ADDRESS_TOKEN_BALANCE_ERC_1155: AddressTokenBalance = {
token: TOKEN_INFO_ERC_1155,
token_id: '188882',
token_instance: TOKEN_INSTANCE,
value: '176',
};
import type { HomeStats } from 'types/api/stats'; import type { HomeStats, StatsChartsSection } from 'types/api/stats';
export const HOMEPAGE_STATS: HomeStats = { export const HOMEPAGE_STATS: HomeStats = {
average_block_time: 14346, average_block_time: 14346,
...@@ -18,3 +18,38 @@ export const HOMEPAGE_STATS: HomeStats = { ...@@ -18,3 +18,38 @@ export const HOMEPAGE_STATS: HomeStats = {
total_transactions: '193823272', total_transactions: '193823272',
transactions_today: '0', transactions_today: '0',
}; };
export const STATS_CHARTS_SECTION: StatsChartsSection = {
id: 'placeholder',
title: 'Placeholder',
charts: [
{
id: 'chart_0',
title: 'Average transaction fee',
description: 'The average amount in ETH spent per transaction',
units: 'ETH',
},
{
id: 'chart_1',
title: 'Transactions fees',
description: 'Amount of tokens paid as fees',
units: 'ETH',
},
{
id: 'chart_2',
title: 'New transactions',
description: 'New transactions number',
units: null,
},
{
id: 'chart_3',
title: 'Transactions growth',
description: 'Cumulative transactions number',
units: null,
},
],
};
export const STATS_CHARTS = {
sections: [ STATS_CHARTS_SECTION ],
};
import type { TokenCounters, TokenHolder, TokenHolders, TokenInfo, TokenInstance, TokenType } from 'types/api/token'; import type { TokenCounters, TokenHolder, TokenInfo, TokenInstance, TokenType } from 'types/api/token';
import type { TokenTransfer, TokenTransferPagination, TokenTransferResponse } from 'types/api/tokenTransfer'; import type { TokenTransfer, TokenTransferPagination, TokenTransferResponse } from 'types/api/tokenTransfer';
import { ADDRESS_PARAMS, ADDRESS_HASH } from './addressParams'; import { ADDRESS_PARAMS, ADDRESS_HASH } from './addressParams';
...@@ -9,11 +9,11 @@ import { generateListStub } from './utils'; ...@@ -9,11 +9,11 @@ import { generateListStub } from './utils';
export const TOKEN_INFO_ERC_20: TokenInfo<'ERC-20'> = { export const TOKEN_INFO_ERC_20: TokenInfo<'ERC-20'> = {
address: ADDRESS_HASH, address: ADDRESS_HASH,
decimals: '18', decimals: '18',
exchange_rate: null, exchange_rate: '0.999997',
holders: '16026', holders: '16026',
name: 'Stub Token (goerli)', name: 'Stub Token (goerli)',
symbol: 'STUB', symbol: 'STUB',
total_supply: '6000000000000000000', total_supply: '60000000000000000000000',
type: 'ERC-20', type: 'ERC-20',
icon_url: null, icon_url: null,
}; };
...@@ -38,8 +38,6 @@ export const TOKEN_HOLDER: TokenHolder = { ...@@ -38,8 +38,6 @@ export const TOKEN_HOLDER: TokenHolder = {
value: '1021378038331138520', value: '1021378038331138520',
}; };
export const TOKEN_HOLDERS: TokenHolders = { items: Array(50).fill(TOKEN_HOLDER), next_page_params: null };
export const TOKEN_TRANSFER_ERC_20: TokenTransfer = { export const TOKEN_TRANSFER_ERC_20: TokenTransfer = {
block_hash: BLOCK_HASH, block_hash: BLOCK_HASH,
from: ADDRESS_PARAMS, from: ADDRESS_PARAMS,
...@@ -77,11 +75,11 @@ export const TOKEN_TRANSFER_ERC_1155: TokenTransfer = { ...@@ -77,11 +75,11 @@ export const TOKEN_TRANSFER_ERC_1155: TokenTransfer = {
export const getTokenTransfersStub = (type?: TokenType, pagination: TokenTransferPagination | null = null): TokenTransferResponse => { export const getTokenTransfersStub = (type?: TokenType, pagination: TokenTransferPagination | null = null): TokenTransferResponse => {
switch (type) { switch (type) {
case 'ERC-721': case 'ERC-721':
return generateListStub<'token_transfers'>(TOKEN_TRANSFER_ERC_721, 50, pagination); return generateListStub<'token_transfers'>(TOKEN_TRANSFER_ERC_721, 50, { next_page_params: pagination });
case 'ERC-1155': case 'ERC-1155':
return generateListStub<'token_transfers'>(TOKEN_TRANSFER_ERC_1155, 50, pagination); return generateListStub<'token_transfers'>(TOKEN_TRANSFER_ERC_1155, 50, { next_page_params: pagination });
default: default:
return generateListStub<'token_transfers'>(TOKEN_TRANSFER_ERC_20, 50, pagination); return generateListStub<'token_transfers'>(TOKEN_TRANSFER_ERC_20, 50, { next_page_params: pagination });
} }
}; };
...@@ -92,7 +90,7 @@ export const TOKEN_INSTANCE: TokenInstance = { ...@@ -92,7 +90,7 @@ export const TOKEN_INSTANCE: TokenInstance = {
image_url: 'https://ipfs.vipsland.com/nft/collections/genesis/188882.gif', image_url: 'https://ipfs.vipsland.com/nft/collections/genesis/188882.gif',
is_unique: true, is_unique: true,
metadata: { metadata: {
attributes: Array(3).fill({ trait_type: 'skin', value: '6' }), attributes: Array(3).fill({ trait_type: 'skin tone', value: 'very light skin tone' }),
description: '**GENESIS #188882**, **8a77ca1bcaa4036f** :: *844th* generation of *#57806 and #57809* :: **eGenetic Hash Code (eDNA)** = *2822355e953a462d*', description: '**GENESIS #188882**, **8a77ca1bcaa4036f** :: *844th* generation of *#57806 and #57809* :: **eGenetic Hash Code (eDNA)** = *2822355e953a462d*',
external_url: 'https://vipsland.com/nft/collections/genesis/188882', external_url: 'https://vipsland.com/nft/collections/genesis/188882',
image: 'https://ipfs.vipsland.com/nft/collections/genesis/188882.gif', image: 'https://ipfs.vipsland.com/nft/collections/genesis/188882.gif',
......
...@@ -5,10 +5,10 @@ import type { PaginatedResources, PaginatedResponse } from 'lib/api/resources'; ...@@ -5,10 +5,10 @@ import type { PaginatedResources, PaginatedResponse } from 'lib/api/resources';
export function generateListStub<Resource extends PaginatedResources>( export function generateListStub<Resource extends PaginatedResources>(
stub: ArrayElement<PaginatedResponse<Resource>['items']>, stub: ArrayElement<PaginatedResponse<Resource>['items']>,
num = 50, num = 50,
pagination: PaginatedResponse<Resource>['next_page_params'] = null, rest: Omit<PaginatedResponse<Resource>, 'items'>,
) { ) {
return { return {
items: Array(num).fill(stub), items: Array(num).fill(stub),
next_page_params: pagination, ...rest,
}; };
} }
...@@ -45,7 +45,7 @@ export interface AddressTokenBalance { ...@@ -45,7 +45,7 @@ export interface AddressTokenBalance {
token: TokenInfo; token: TokenInfo;
token_id: string | null; token_id: string | null;
value: string; value: string;
token_instance?: TokenInstance; token_instance: TokenInstance | null;
} }
export interface AddressTokensResponse { export interface AddressTokensResponse {
...@@ -55,6 +55,7 @@ export interface AddressTokensResponse { ...@@ -55,6 +55,7 @@ export interface AddressTokensResponse {
token_name: 'string' | null; token_name: 'string' | null;
token_type: TokenType; token_type: TokenType;
value: number; value: number;
fiat_value: string | null;
} | null; } | null;
} }
......
...@@ -8,6 +8,6 @@ export type AddressesResponse = { ...@@ -8,6 +8,6 @@ export type AddressesResponse = {
fetched_coin_balance: string; fetched_coin_balance: string;
hash: string; hash: string;
items_count: number; items_count: number;
}; } | null;
total_supply: string; total_supply: string;
} }
...@@ -7,6 +7,7 @@ export type TokensResponse = { ...@@ -7,6 +7,7 @@ export type TokensResponse = {
holder_count: number; holder_count: number;
items_count: number; items_count: number;
name: string; name: string;
market_cap: string | null;
}; };
} }
......
...@@ -74,9 +74,9 @@ export interface TransactionsResponsePending { ...@@ -74,9 +74,9 @@ export interface TransactionsResponsePending {
export interface TransactionsResponseWatchlist { export interface TransactionsResponseWatchlist {
items: Array<Transaction>; items: Array<Transaction>;
next_page_params: { next_page_params: {
inserted_at: string; block_number: number;
hash: string; index: number;
filter: 'pending'; items_count: 50;
} | null; } | null;
} }
......
...@@ -11,6 +11,8 @@ import { getResourceKey } from 'lib/api/useApiQuery'; ...@@ -11,6 +11,8 @@ import { getResourceKey } from 'lib/api/useApiQuery';
import useQueryWithPages from 'lib/hooks/useQueryWithPages'; 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 { BLOCK } from 'stubs/block';
import { generateListStub } from 'stubs/utils';
import ActionBar from 'ui/shared/ActionBar'; 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';
...@@ -34,6 +36,18 @@ const AddressBlocksValidated = ({ scrollRef }: Props) => { ...@@ -34,6 +36,18 @@ const AddressBlocksValidated = ({ scrollRef }: Props) => {
resourceName: 'address_blocks_validated', resourceName: 'address_blocks_validated',
pathParams: { hash: addressHash }, pathParams: { hash: addressHash },
scrollRef, scrollRef,
options: {
placeholderData: generateListStub<'address_blocks_validated'>(
BLOCK,
50,
{
next_page_params: {
block_number: 9060562,
items_count: 50,
},
},
),
},
}); });
const handleSocketError = React.useCallback(() => { const handleSocketError = React.useCallback(() => {
...@@ -61,7 +75,7 @@ const AddressBlocksValidated = ({ scrollRef }: Props) => { ...@@ -61,7 +75,7 @@ const AddressBlocksValidated = ({ scrollRef }: Props) => {
topic: `blocks:${ addressHash.toLowerCase() }`, topic: `blocks:${ addressHash.toLowerCase() }`,
onSocketClose: handleSocketError, onSocketClose: handleSocketError,
onSocketError: handleSocketError, onSocketError: handleSocketError,
isDisabled: !addressHash || query.pagination.page !== 1, isDisabled: !addressHash || query.isPlaceholderData || query.pagination.page !== 1,
}); });
useSocketMessage({ useSocketMessage({
channel, channel,
...@@ -84,22 +98,32 @@ const AddressBlocksValidated = ({ scrollRef }: Props) => { ...@@ -84,22 +98,32 @@ const AddressBlocksValidated = ({ scrollRef }: Props) => {
</Tr> </Tr>
</Thead> </Thead>
<Tbody> <Tbody>
{ query.data.items.map((item) => ( { query.data.items.map((item, index) => (
<AddressBlocksValidatedTableItem key={ item.height } { ...item } page={ query.pagination.page }/> <AddressBlocksValidatedTableItem
key={ item.height + (query.isPlaceholderData ? String(index) : '') }
{ ...item }
page={ query.pagination.page }
isLoading={ query.isPlaceholderData }
/>
)) } )) }
</Tbody> </Tbody>
</Table> </Table>
</Hide> </Hide>
<Show below="lg" ssr={ false }> <Show below="lg" ssr={ false }>
{ query.data.items.map((item) => ( { query.data.items.map((item, index) => (
<AddressBlocksValidatedListItem key={ item.height } { ...item } page={ query.pagination.page }/> <AddressBlocksValidatedListItem
key={ item.height + (query.isPlaceholderData ? String(index) : '') }
{ ...item }
page={ query.pagination.page }
isLoading={ query.isPlaceholderData }
/>
)) } )) }
</Show> </Show>
</> </>
) : null; ) : null;
const actionBar = query.isPaginationVisible ? ( const actionBar = query.isPaginationVisible ? (
<ActionBar mt={ -6 } showShadow={ query.isLoading }> <ActionBar mt={ -6 }>
<Pagination ml="auto" { ...query.pagination }/> <Pagination ml="auto" { ...query.pagination }/>
</ActionBar> </ActionBar>
) : null; ) : null;
...@@ -107,7 +131,7 @@ const AddressBlocksValidated = ({ scrollRef }: Props) => { ...@@ -107,7 +131,7 @@ const AddressBlocksValidated = ({ scrollRef }: Props) => {
return ( return (
<DataListDisplay <DataListDisplay
isError={ query.isError } isError={ query.isError }
isLoading={ query.isLoading } isLoading={ false }
items={ query.data?.items } items={ query.data?.items }
skeletonProps={{ isLongSkeleton: true, skeletonDesktopColumns: [ '17%', '17%', '16%', '25%', '25%' ] }} skeletonProps={{ isLongSkeleton: true, skeletonDesktopColumns: [ '17%', '17%', '16%', '25%', '25%' ] }}
emptyText="There are no validated blocks for this address." emptyText="There are no validated blocks for this address."
......
...@@ -10,6 +10,8 @@ import useQueryWithPages from 'lib/hooks/useQueryWithPages'; ...@@ -10,6 +10,8 @@ import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import getQueryParamString from 'lib/router/getQueryParamString'; import getQueryParamString from 'lib/router/getQueryParamString';
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 { ADDRESS_COIN_BALANCE } from 'stubs/address';
import { generateListStub } from 'stubs/utils';
import SocketAlert from 'ui/shared/SocketAlert'; import SocketAlert from 'ui/shared/SocketAlert';
import AddressCoinBalanceChart from './coinBalance/AddressCoinBalanceChart'; import AddressCoinBalanceChart from './coinBalance/AddressCoinBalanceChart';
...@@ -26,6 +28,18 @@ const AddressCoinBalance = () => { ...@@ -26,6 +28,18 @@ const AddressCoinBalance = () => {
resourceName: 'address_coin_balance', resourceName: 'address_coin_balance',
pathParams: { hash: addressHash }, pathParams: { hash: addressHash },
scrollRef, scrollRef,
options: {
placeholderData: generateListStub<'address_coin_balance'>(
ADDRESS_COIN_BALANCE,
50,
{
next_page_params: {
block_number: 8009880,
items_count: 50,
},
},
),
},
}); });
const handleSocketError = React.useCallback(() => { const handleSocketError = React.useCallback(() => {
...@@ -56,7 +70,7 @@ const AddressCoinBalance = () => { ...@@ -56,7 +70,7 @@ const AddressCoinBalance = () => {
topic: `addresses:${ addressHash.toLowerCase() }`, topic: `addresses:${ addressHash.toLowerCase() }`,
onSocketClose: handleSocketError, onSocketClose: handleSocketError,
onSocketError: handleSocketError, onSocketError: handleSocketError,
isDisabled: !addressHash || coinBalanceQuery.pagination.page !== 1, isDisabled: !addressHash || coinBalanceQuery.isPlaceholderData || coinBalanceQuery.pagination.page !== 1,
}); });
useSocketMessage({ useSocketMessage({
channel, channel,
......
import { Box, Text, Icon, Grid } from '@chakra-ui/react'; import { Box, Text, Grid, Skeleton } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query'; import type { UseQueryResult } from '@tanstack/react-query';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
...@@ -10,9 +10,11 @@ import blockIcon from 'icons/block.svg'; ...@@ -10,9 +10,11 @@ import blockIcon from 'icons/block.svg';
import type { ResourceError } from 'lib/api/resources'; import type { ResourceError } from 'lib/api/resources';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import getQueryParamString from 'lib/router/getQueryParamString'; import getQueryParamString from 'lib/router/getQueryParamString';
import { ADDRESS_COUNTERS } from 'stubs/address';
import AddressCounterItem from 'ui/address/details/AddressCounterItem'; import AddressCounterItem from 'ui/address/details/AddressCounterItem';
import AddressLink from 'ui/shared/address/AddressLink'; import AddressLink from 'ui/shared/address/AddressLink';
import AddressHeadingInfo from 'ui/shared/AddressHeadingInfo'; import AddressHeadingInfo from 'ui/shared/AddressHeadingInfo';
import Icon from 'ui/shared/chakra/Icon';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem'; import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import DetailsSponsoredItem from 'ui/shared/DetailsSponsoredItem'; import DetailsSponsoredItem from 'ui/shared/DetailsSponsoredItem';
...@@ -20,7 +22,6 @@ import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic'; ...@@ -20,7 +22,6 @@ import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import LinkInternal from 'ui/shared/LinkInternal'; import LinkInternal from 'ui/shared/LinkInternal';
import AddressBalance from './details/AddressBalance'; import AddressBalance from './details/AddressBalance';
import AddressDetailsSkeleton from './details/AddressDetailsSkeleton';
import AddressNameInfo from './details/AddressNameInfo'; import AddressNameInfo from './details/AddressNameInfo';
import TokenSelect from './tokenSelect/TokenSelect'; import TokenSelect from './tokenSelect/TokenSelect';
...@@ -38,6 +39,7 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => { ...@@ -38,6 +39,7 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
pathParams: { hash: addressHash }, pathParams: { hash: addressHash },
queryOptions: { queryOptions: {
enabled: Boolean(addressHash) && Boolean(addressQuery.data), enabled: Boolean(addressHash) && Boolean(addressQuery.data),
placeholderData: ADDRESS_COUNTERS,
}, },
}); });
...@@ -77,26 +79,27 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => { ...@@ -77,26 +79,27 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
return <DataFetchAlert/>; return <DataFetchAlert/>;
} }
if (addressQuery.isLoading) {
return <AddressDetailsSkeleton/>;
}
const data = addressQuery.isError ? errorData : addressQuery.data; const data = addressQuery.isError ? errorData : addressQuery.data;
if (!data) {
return null;
}
return ( return (
<Box> <Box>
<AddressHeadingInfo address={ data } token={ data.token } isLinkDisabled/> <AddressHeadingInfo address={ data } token={ data.token } isLoading={ addressQuery.isPlaceholderData } isLinkDisabled/>
<Grid <Grid
mt={ 8 } mt={ 8 }
columnGap={ 8 } columnGap={ 8 }
rowGap={{ base: 1, lg: 3 }} rowGap={{ base: 1, lg: 3 }}
templateColumns={{ base: 'minmax(0, 1fr)', lg: 'auto minmax(0, 1fr)' }} overflow="hidden" templateColumns={{ base: 'minmax(0, 1fr)', lg: 'auto minmax(0, 1fr)' }} overflow="hidden"
> >
<AddressNameInfo data={ data }/> <AddressNameInfo data={ data } isLoading={ addressQuery.isPlaceholderData }/>
{ data.is_contract && data.creation_tx_hash && data.creator_address_hash && ( { data.is_contract && data.creation_tx_hash && data.creator_address_hash && (
<DetailsInfoItem <DetailsInfoItem
title="Creator" title="Creator"
hint="Transaction and address of creation" hint="Transaction and address of creation"
isLoading={ addressQuery.isPlaceholderData }
> >
<AddressLink type="address" hash={ data.creator_address_hash } truncation="constant"/> <AddressLink type="address" hash={ data.creator_address_hash } truncation="constant"/>
<Text whiteSpace="pre"> at txn </Text> <Text whiteSpace="pre"> at txn </Text>
...@@ -119,7 +122,7 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => { ...@@ -119,7 +122,7 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
) } ) }
</DetailsInfoItem> </DetailsInfoItem>
) } ) }
<AddressBalance data={ data }/> <AddressBalance data={ data } isLoading={ addressQuery.isPlaceholderData }/>
{ data.has_tokens && ( { data.has_tokens && (
<DetailsInfoItem <DetailsInfoItem
title="Tokens" title="Tokens"
...@@ -133,36 +136,68 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => { ...@@ -133,36 +136,68 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
<DetailsInfoItem <DetailsInfoItem
title="Transactions" title="Transactions"
hint="Number of transactions related to this address" hint="Number of transactions related to this address"
isLoading={ addressQuery.isPlaceholderData || countersQuery.isPlaceholderData }
> >
{ addressQuery.data ? { addressQuery.data ? (
<AddressCounterItem prop="transactions_count" query={ countersQuery } address={ data.hash } onClick={ handleCounterItemClick }/> : <AddressCounterItem
prop="transactions_count"
query={ countersQuery }
address={ data.hash }
onClick={ handleCounterItemClick }
isAddressQueryLoading={ addressQuery.isPlaceholderData }
/>
) :
0 } 0 }
</DetailsInfoItem> </DetailsInfoItem>
{ data.has_token_transfers && ( { data.has_token_transfers && (
<DetailsInfoItem <DetailsInfoItem
title="Transfers" title="Transfers"
hint="Number of transfers to/from this address" hint="Number of transfers to/from this address"
isLoading={ addressQuery.isPlaceholderData || countersQuery.isPlaceholderData }
> >
{ addressQuery.data ? { addressQuery.data ? (
<AddressCounterItem prop="token_transfers_count" query={ countersQuery } address={ data.hash } onClick={ handleCounterItemClick }/> : <AddressCounterItem
prop="token_transfers_count"
query={ countersQuery }
address={ data.hash }
onClick={ handleCounterItemClick }
isAddressQueryLoading={ addressQuery.isPlaceholderData }
/>
) :
0 } 0 }
</DetailsInfoItem> </DetailsInfoItem>
) } ) }
<DetailsInfoItem <DetailsInfoItem
title="Gas used" title="Gas used"
hint="Gas used by the address" hint="Gas used by the address"
isLoading={ addressQuery.isPlaceholderData || countersQuery.isPlaceholderData }
> >
{ addressQuery.data ? { addressQuery.data ? (
<AddressCounterItem prop="gas_usage_count" query={ countersQuery } address={ data.hash } onClick={ handleCounterItemClick }/> : <AddressCounterItem
prop="gas_usage_count"
query={ countersQuery }
address={ data.hash }
onClick={ handleCounterItemClick }
isAddressQueryLoading={ addressQuery.isPlaceholderData }
/>
) :
0 } 0 }
</DetailsInfoItem> </DetailsInfoItem>
{ data.has_validated_blocks && ( { data.has_validated_blocks && (
<DetailsInfoItem <DetailsInfoItem
title="Blocks validated" title="Blocks validated"
hint="Number of blocks validated by this validator" hint="Number of blocks validated by this validator"
isLoading={ addressQuery.isPlaceholderData || countersQuery.isPlaceholderData }
> >
{ addressQuery.data ? { addressQuery.data ? (
<AddressCounterItem prop="validations_count" query={ countersQuery } address={ data.hash } onClick={ handleCounterItemClick }/> : <AddressCounterItem
prop="validations_count"
query={ countersQuery }
address={ data.hash }
onClick={ handleCounterItemClick }
isAddressQueryLoading={ addressQuery.isPlaceholderData }
/>
) :
0 } 0 }
</DetailsInfoItem> </DetailsInfoItem>
) } ) }
...@@ -172,18 +207,21 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => { ...@@ -172,18 +207,21 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
hint="Block number in which the address was updated" hint="Block number in which the address was updated"
alignSelf="center" alignSelf="center"
py={{ base: '2px', lg: 1 }} py={{ base: '2px', lg: 1 }}
isLoading={ addressQuery.isPlaceholderData }
> >
<LinkInternal <LinkInternal
href={ route({ pathname: '/block/[height]', query: { height: String(data.block_number_balance_updated_at) } }) } href={ route({ pathname: '/block/[height]', query: { height: String(data.block_number_balance_updated_at) } }) }
display="flex" display="flex"
alignItems="center" alignItems="center"
> >
<Icon as={ blockIcon } boxSize={ 6 } mr={ 2 }/> <Box mr={ 2 }>
{ data.block_number_balance_updated_at } <Icon as={ blockIcon } boxSize={ 6 } isLoading={ addressQuery.isPlaceholderData }/>
</Box>
<Skeleton isLoaded={ !addressQuery.isPlaceholderData }>{ data.block_number_balance_updated_at }</Skeleton>
</LinkInternal> </LinkInternal>
</DetailsInfoItem> </DetailsInfoItem>
) } ) }
<DetailsSponsoredItem/> <DetailsSponsoredItem isLoading={ addressQuery.isPlaceholderData }/>
</Grid> </Grid>
</Box> </Box>
); );
......
...@@ -9,6 +9,8 @@ import getFilterValueFromQuery from 'lib/getFilterValueFromQuery'; ...@@ -9,6 +9,8 @@ import getFilterValueFromQuery from 'lib/getFilterValueFromQuery';
import useQueryWithPages from 'lib/hooks/useQueryWithPages'; import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import { apos } from 'lib/html-entities'; import { apos } from 'lib/html-entities';
import getQueryParamString from 'lib/router/getQueryParamString'; import getQueryParamString from 'lib/router/getQueryParamString';
import { INTERNAL_TX } from 'stubs/internalTx';
import { generateListStub } from 'stubs/utils';
import AddressIntTxsTable from 'ui/address/internals/AddressIntTxsTable'; import AddressIntTxsTable from 'ui/address/internals/AddressIntTxsTable';
import ActionBar from 'ui/shared/ActionBar'; import ActionBar from 'ui/shared/ActionBar';
import DataListDisplay from 'ui/shared/DataListDisplay'; import DataListDisplay from 'ui/shared/DataListDisplay';
...@@ -26,15 +28,28 @@ const AddressInternalTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivE ...@@ -26,15 +28,28 @@ const AddressInternalTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivE
const hash = getQueryParamString(router.query.hash); const hash = getQueryParamString(router.query.hash);
const { data, isLoading, isError, pagination, onFilterChange, isPaginationVisible } = useQueryWithPages({ const { data, isPlaceholderData, isError, pagination, onFilterChange, isPaginationVisible } = useQueryWithPages({
resourceName: 'address_internal_txs', resourceName: 'address_internal_txs',
pathParams: { hash }, pathParams: { hash },
filters: { filter: filterValue }, filters: { filter: filterValue },
scrollRef, scrollRef,
options: {
placeholderData: generateListStub<'address_internal_txs'>(
INTERNAL_TX,
50,
{
next_page_params: {
block_number: 8987561,
index: 2,
items_count: 50,
transaction_index: 67,
},
},
),
},
}); });
const handleFilterChange = React.useCallback((val: string | Array<string>) => { const handleFilterChange = React.useCallback((val: string | Array<string>) => {
const newVal = getFilterValue(val); const newVal = getFilterValue(val);
setFilterValue(newVal); setFilterValue(newVal);
onFilterChange({ filter: newVal }); onFilterChange({ filter: newVal });
...@@ -43,22 +58,23 @@ const AddressInternalTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivE ...@@ -43,22 +58,23 @@ const AddressInternalTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivE
const content = data?.items ? ( const content = data?.items ? (
<> <>
<Show below="lg" ssr={ false }> <Show below="lg" ssr={ false }>
<AddressIntTxsList data={ data.items } currentAddress={ hash }/> <AddressIntTxsList data={ data.items } currentAddress={ hash } isLoading={ isPlaceholderData }/>
</Show> </Show>
<Hide below="lg" ssr={ false }> <Hide below="lg" ssr={ false }>
<AddressIntTxsTable data={ data.items } currentAddress={ hash }/> <AddressIntTxsTable data={ data.items } currentAddress={ hash } isLoading={ isPlaceholderData }/>
</Hide> </Hide>
</> </>
) : null ; ) : null ;
const actionBar = ( const actionBar = (
<ActionBar mt={ -6 } justifyContent="left" showShadow={ isLoading }> <ActionBar mt={ -6 } justifyContent="left">
<AddressTxsFilter <AddressTxsFilter
defaultFilter={ filterValue } defaultFilter={ filterValue }
onFilterChange={ handleFilterChange } onFilterChange={ handleFilterChange }
isActive={ Boolean(filterValue) } isActive={ Boolean(filterValue) }
isLoading={ pagination.isLoading }
/> />
<AddressCsvExportLink address={ hash } type="internal-transactions" ml={{ base: 2, lg: 'auto' }}/> <AddressCsvExportLink address={ hash } isLoading={ pagination.isLoading } type="internal-transactions" ml={{ base: 2, lg: 'auto' }}/>
{ isPaginationVisible && <Pagination ml={{ base: 'auto', lg: 8 }} { ...pagination }/> } { isPaginationVisible && <Pagination ml={{ base: 'auto', lg: 8 }} { ...pagination }/> }
</ActionBar> </ActionBar>
); );
...@@ -66,7 +82,7 @@ const AddressInternalTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivE ...@@ -66,7 +82,7 @@ const AddressInternalTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivE
return ( return (
<DataListDisplay <DataListDisplay
isError={ isError } isError={ isError }
isLoading={ isLoading } isLoading={ false }
items={ data?.items } items={ data?.items }
skeletonProps={{ isLongSkeleton: true, skeletonDesktopColumns: [ '15%', '15%', '10%', '20%', '20%', '20%' ] }} skeletonProps={{ isLongSkeleton: true, skeletonDesktopColumns: [ '15%', '15%', '10%', '20%', '20%', '20%' ] }}
filterProps={{ emptyFilteredText: `Couldn${ apos }t find any transaction that matches your query.`, hasActiveFilters: Boolean(filterValue) }} filterProps={{ emptyFilteredText: `Couldn${ apos }t find any transaction that matches your query.`, hasActiveFilters: Boolean(filterValue) }}
......
...@@ -19,12 +19,12 @@ const AddressLogs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement> ...@@ -19,12 +19,12 @@ const AddressLogs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>
pathParams: { hash }, pathParams: { hash },
scrollRef, scrollRef,
options: { options: {
placeholderData: generateListStub<'address_logs'>(LOG, 3, { placeholderData: generateListStub<'address_logs'>(LOG, 3, { next_page_params: {
block_number: 9005750, block_number: 9005750,
index: 42, index: 42,
items_count: 50, items_count: 50,
transaction_index: 23, transaction_index: 23,
}), } }),
}, },
}); });
......
...@@ -6,6 +6,8 @@ import type { TokenType } from 'types/api/token'; ...@@ -6,6 +6,8 @@ import type { TokenType } from 'types/api/token';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import useQueryWithPages from 'lib/hooks/useQueryWithPages'; import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import { ADDRESS_TOKEN_BALANCE_ERC_1155, ADDRESS_TOKEN_BALANCE_ERC_20, ADDRESS_TOKEN_BALANCE_ERC_721 } from 'stubs/address';
import { generateListStub } from 'stubs/utils';
import { tokenTabsByType } from 'ui/pages/Address'; import { tokenTabsByType } from 'ui/pages/Address';
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';
...@@ -43,7 +45,9 @@ const AddressTokens = () => { ...@@ -43,7 +45,9 @@ const AddressTokens = () => {
filters: { type: 'ERC-20' }, filters: { type: 'ERC-20' },
scrollRef, scrollRef,
options: { options: {
refetchOnMount: false,
enabled: tokenType === 'ERC-20', enabled: tokenType === 'ERC-20',
placeholderData: generateListStub<'address_tokens'>(ADDRESS_TOKEN_BALANCE_ERC_20, 10, { next_page_params: null }),
}, },
}); });
...@@ -53,7 +57,9 @@ const AddressTokens = () => { ...@@ -53,7 +57,9 @@ const AddressTokens = () => {
filters: { type: 'ERC-721' }, filters: { type: 'ERC-721' },
scrollRef, scrollRef,
options: { options: {
refetchOnMount: false,
enabled: tokenType === 'ERC-721', enabled: tokenType === 'ERC-721',
placeholderData: generateListStub<'address_tokens'>(ADDRESS_TOKEN_BALANCE_ERC_721, 10, { next_page_params: null }),
}, },
}); });
...@@ -63,7 +69,9 @@ const AddressTokens = () => { ...@@ -63,7 +69,9 @@ const AddressTokens = () => {
filters: { type: 'ERC-1155' }, filters: { type: 'ERC-1155' },
scrollRef, scrollRef,
options: { options: {
refetchOnMount: false,
enabled: tokenType === 'ERC-1155', enabled: tokenType === 'ERC-1155',
placeholderData: generateListStub<'address_tokens'>(ADDRESS_TOKEN_BALANCE_ERC_1155, 10, { next_page_params: null }),
}, },
}); });
......
...@@ -44,11 +44,11 @@ const AddressTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>} ...@@ -44,11 +44,11 @@ const AddressTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>}
filters: { filter: filterValue }, filters: { filter: filterValue },
scrollRef, scrollRef,
options: { options: {
placeholderData: generateListStub<'address_txs'>(TX, 50, { placeholderData: generateListStub<'address_txs'>(TX, 50, { next_page_params: {
block_number: 9005713, block_number: 9005713,
index: 5, index: 5,
items_count: 50, items_count: 50,
}), } }),
}, },
}); });
......
...@@ -22,10 +22,10 @@ const AddressWithdrawals = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivE ...@@ -22,10 +22,10 @@ const AddressWithdrawals = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivE
pathParams: { hash }, pathParams: { hash },
scrollRef, scrollRef,
options: { options: {
placeholderData: generateListStub<'address_withdrawals'>(WITHDRAWAL, 50, { placeholderData: generateListStub<'address_withdrawals'>(WITHDRAWAL, 50, { next_page_params: {
index: 5, index: 5,
items_count: 50, items_count: 50,
}), } }),
}, },
}); });
const content = data?.items ? ( const content = data?.items ? (
......
import { Text, Flex } from '@chakra-ui/react'; import { Flex, Skeleton } from '@chakra-ui/react';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
...@@ -14,6 +14,7 @@ import Utilization from 'ui/shared/Utilization/Utilization'; ...@@ -14,6 +14,7 @@ import Utilization from 'ui/shared/Utilization/Utilization';
type Props = Block & { type Props = Block & {
page: number; page: number;
isLoading: boolean;
}; };
const AddressBlocksValidatedListItem = (props: Props) => { const AddressBlocksValidatedListItem = (props: Props) => {
...@@ -24,21 +25,31 @@ const AddressBlocksValidatedListItem = (props: Props) => { ...@@ -24,21 +25,31 @@ const AddressBlocksValidatedListItem = (props: Props) => {
return ( return (
<ListItemMobile rowGap={ 2 } isAnimated> <ListItemMobile rowGap={ 2 } isAnimated>
<Flex justifyContent="space-between" w="100%"> <Flex justifyContent="space-between" w="100%">
<LinkInternal href={ blockUrl } fontWeight="700">{ props.height }</LinkInternal> <Skeleton isLoaded={ !props.isLoading } display="inline-block">
<Text variant="secondary">{ timeAgo }</Text> <LinkInternal href={ blockUrl } fontWeight="700">{ props.height }</LinkInternal>
</Skeleton>
<Skeleton isLoaded={ !props.isLoading } color="text_secondary" display="inline-block">
<span>{ timeAgo }</span>
</Skeleton>
</Flex> </Flex>
<Flex columnGap={ 2 } w="100%"> <Flex columnGap={ 2 } w="100%">
<Text fontWeight={ 500 } flexShrink={ 0 }>Txn</Text> <Skeleton isLoaded={ !props.isLoading } fontWeight={ 500 } flexShrink={ 0 }>Txn</Skeleton>
<Text variant="secondary">{ props.tx_count }</Text> <Skeleton isLoaded={ !props.isLoading } display="inline-block" color="Skeleton_secondary">
<span>{ props.tx_count }</span>
</Skeleton>
</Flex> </Flex>
<Flex columnGap={ 2 } w="100%"> <Flex columnGap={ 2 } w="100%">
<Text fontWeight={ 500 } flexShrink={ 0 }>Gas used</Text> <Skeleton isLoaded={ !props.isLoading } fontWeight={ 500 } flexShrink={ 0 }>Gas used</Skeleton>
<Text variant="secondary">{ BigNumber(props.gas_used || 0).toFormat() }</Text> <Skeleton isLoaded={ !props.isLoading } color="text_secondary">{ BigNumber(props.gas_used || 0).toFormat() }</Skeleton>
<Utilization colorScheme="gray" value={ BigNumber(props.gas_used || 0).dividedBy(BigNumber(props.gas_limit)).toNumber() }/> <Utilization
colorScheme="gray"
value={ BigNumber(props.gas_used || 0).dividedBy(BigNumber(props.gas_limit)).toNumber() }
isLoading={ props.isLoading }
/>
</Flex> </Flex>
<Flex columnGap={ 2 } w="100%"> <Flex columnGap={ 2 } w="100%">
<Text fontWeight={ 500 } flexShrink={ 0 }>Reward { appConfig.network.currency.symbol }</Text> <Skeleton isLoaded={ !props.isLoading } fontWeight={ 500 } flexShrink={ 0 }>Reward { appConfig.network.currency.symbol }</Skeleton>
<Text variant="secondary">{ totalReward.toFixed() }</Text> <Skeleton isLoaded={ !props.isLoading } color="text_secondary">{ totalReward.toFixed() }</Skeleton>
</Flex> </Flex>
</ListItemMobile> </ListItemMobile>
); );
......
import { Td, Tr, Text, Box, Flex } from '@chakra-ui/react'; import { Td, Tr, Flex, Skeleton } from '@chakra-ui/react';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
...@@ -12,6 +12,7 @@ import Utilization from 'ui/shared/Utilization/Utilization'; ...@@ -12,6 +12,7 @@ import Utilization from 'ui/shared/Utilization/Utilization';
type Props = Block & { type Props = Block & {
page: number; page: number;
isLoading: boolean;
}; };
const AddressBlocksValidatedTableItem = (props: Props) => { const AddressBlocksValidatedTableItem = (props: Props) => {
...@@ -22,22 +23,36 @@ const AddressBlocksValidatedTableItem = (props: Props) => { ...@@ -22,22 +23,36 @@ const AddressBlocksValidatedTableItem = (props: Props) => {
return ( return (
<Tr> <Tr>
<Td> <Td>
<LinkInternal href={ blockUrl } fontWeight="700">{ props.height }</LinkInternal> <Skeleton isLoaded={ !props.isLoading } display="inline-block">
<LinkInternal href={ blockUrl } fontWeight="700">{ props.height }</LinkInternal>
</Skeleton>
</Td> </Td>
<Td> <Td>
<Text variant="secondary">{ timeAgo }</Text> <Skeleton isLoaded={ !props.isLoading } color="text_secondary" display="inline-block">
<span>{ timeAgo }</span>
</Skeleton>
</Td> </Td>
<Td> <Td>
<Text fontWeight="500">{ props.tx_count }</Text> <Skeleton isLoaded={ !props.isLoading } display="inline-block" fontWeight="500">
<span>{ props.tx_count }</span>
</Skeleton>
</Td> </Td>
<Td> <Td>
<Flex alignItems="center" columnGap={ 2 }> <Flex alignItems="center" columnGap={ 2 }>
<Box flexBasis="80px">{ BigNumber(props.gas_used || 0).toFormat() }</Box> <Skeleton isLoaded={ !props.isLoading } flexBasis="80px">
<Utilization colorScheme="gray" value={ BigNumber(props.gas_used || 0).dividedBy(BigNumber(props.gas_limit)).toNumber() }/> { BigNumber(props.gas_used || 0).toFormat() }
</Skeleton>
<Utilization
colorScheme="gray"
value={ BigNumber(props.gas_used || 0).dividedBy(BigNumber(props.gas_limit)).toNumber() }
isLoading={ props.isLoading }
/>
</Flex> </Flex>
</Td> </Td>
<Td isNumeric display="flex" justifyContent="end"> <Td isNumeric display="flex" justifyContent="end">
{ totalReward.toFixed() } <Skeleton isLoaded={ !props.isLoading } display="inline-block">
<span>{ totalReward.toFixed() }</span>
</Skeleton>
</Td> </Td>
</Tr> </Tr>
); );
......
...@@ -25,7 +25,7 @@ const AddressCoinBalanceChart = ({ addressHash }: Props) => { ...@@ -25,7 +25,7 @@ const AddressCoinBalanceChart = ({ addressHash }: Props) => {
title="Balances" title="Balances"
items={ items } items={ items }
isLoading={ isLoading } isLoading={ isLoading }
h="250px" h="300px"
/> />
); );
}; };
......
...@@ -37,15 +37,25 @@ const AddressCoinBalanceHistory = ({ query }: Props) => { ...@@ -37,15 +37,25 @@ const AddressCoinBalanceHistory = ({ query }: Props) => {
</Tr> </Tr>
</Thead> </Thead>
<Tbody> <Tbody>
{ query.data.items.map((item) => ( { query.data.items.map((item, index) => (
<AddressCoinBalanceTableItem key={ item.block_number } { ...item } page={ query.pagination.page }/> <AddressCoinBalanceTableItem
key={ item.block_number + (query.isPlaceholderData ? String(index) : '') }
{ ...item }
page={ query.pagination.page }
isLoading={ query.isPlaceholderData }
/>
)) } )) }
</Tbody> </Tbody>
</Table> </Table>
</Hide> </Hide>
<Show below="lg" ssr={ false }> <Show below="lg" ssr={ false }>
{ query.data.items.map((item) => ( { query.data.items.map((item, index) => (
<AddressCoinBalanceListItem key={ item.block_number } { ...item } page={ query.pagination.page }/> <AddressCoinBalanceListItem
key={ item.block_number + (query.isPlaceholderData ? String(index) : '') }
{ ...item }
page={ query.pagination.page }
isLoading={ query.isPlaceholderData }
/>
)) } )) }
</Show> </Show>
</> </>
...@@ -61,7 +71,7 @@ const AddressCoinBalanceHistory = ({ query }: Props) => { ...@@ -61,7 +71,7 @@ const AddressCoinBalanceHistory = ({ query }: Props) => {
<DataListDisplay <DataListDisplay
mt={ 8 } mt={ 8 }
isError={ query.isError } isError={ query.isError }
isLoading={ query.isLoading } isLoading={ false }
items={ query.data?.items } items={ query.data?.items }
skeletonProps={{ skeletonDesktopColumns: [ '25%', '25%', '25%', '25%', '120px' ] }} skeletonProps={{ skeletonDesktopColumns: [ '25%', '25%', '25%', '25%', '120px' ] }}
emptyText="There is no coin balance history for this address." emptyText="There is no coin balance history for this address."
......
import { Text, Stat, StatHelpText, StatArrow, Flex } from '@chakra-ui/react'; import { Text, Stat, StatHelpText, StatArrow, Flex, Skeleton } from '@chakra-ui/react';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
...@@ -15,6 +15,7 @@ import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile'; ...@@ -15,6 +15,7 @@ import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
type Props = AddressCoinBalanceHistoryItem & { type Props = AddressCoinBalanceHistoryItem & {
page: number; page: number;
isLoading: boolean;
}; };
const AddressCoinBalanceListItem = (props: Props) => { const AddressCoinBalanceListItem = (props: Props) => {
...@@ -26,31 +27,37 @@ const AddressCoinBalanceListItem = (props: Props) => { ...@@ -26,31 +27,37 @@ const AddressCoinBalanceListItem = (props: Props) => {
return ( return (
<ListItemMobile rowGap={ 2 } isAnimated> <ListItemMobile rowGap={ 2 } isAnimated>
<Flex justifyContent="space-between" w="100%"> <Flex justifyContent="space-between" w="100%">
<Text fontWeight={ 600 }>{ BigNumber(props.value).div(WEI).dp(8).toFormat() } { appConfig.network.currency.symbol }</Text> <Skeleton isLoaded={ !props.isLoading } fontWeight={ 600 }>
<Stat flexGrow="0"> { BigNumber(props.value).div(WEI).dp(8).toFormat() } { appConfig.network.currency.symbol }
<StatHelpText display="flex" mb={ 0 } alignItems="center"> </Skeleton>
<StatArrow type={ isPositiveDelta ? 'increase' : 'decrease' }/> <Skeleton isLoaded={ !props.isLoading }>
<Text as="span" color={ isPositiveDelta ? 'green.500' : 'red.500' } fontWeight={ 600 }> <Stat flexGrow="0">
{ deltaBn.dp(8).toFormat() } <StatHelpText display="flex" mb={ 0 } alignItems="center">
</Text> <StatArrow type={ isPositiveDelta ? 'increase' : 'decrease' }/>
</StatHelpText> <Text as="span" color={ isPositiveDelta ? 'green.500' : 'red.500' } fontWeight={ 600 }>
</Stat> { deltaBn.dp(8).toFormat() }
</Text>
</StatHelpText>
</Stat>
</Skeleton>
</Flex> </Flex>
<Flex columnGap={ 2 } w="100%"> <Flex columnGap={ 2 } w="100%">
<Text fontWeight={ 500 } flexShrink={ 0 }>Block</Text> <Skeleton isLoaded={ !props.isLoading } fontWeight={ 500 } flexShrink={ 0 }>Block</Skeleton>
<LinkInternal href={ blockUrl } fontWeight="700">{ props.block_number }</LinkInternal> <Skeleton isLoaded={ !props.isLoading }>
<LinkInternal href={ blockUrl } fontWeight="700">{ props.block_number }</LinkInternal>
</Skeleton>
</Flex> </Flex>
{ props.transaction_hash && ( { props.transaction_hash && (
<Flex columnGap={ 2 } w="100%"> <Flex columnGap={ 2 } w="100%">
<Text fontWeight={ 500 } flexShrink={ 0 }>Txs</Text> <Skeleton isLoaded={ !props.isLoading } fontWeight={ 500 } flexShrink={ 0 }>Txs</Skeleton>
<Address maxW="150px" fontWeight="700"> <Address maxW="150px" fontWeight="700">
<AddressLink hash={ props.transaction_hash } type="transaction"/> <AddressLink hash={ props.transaction_hash } type="transaction" isLoading={ props.isLoading }/>
</Address> </Address>
</Flex> </Flex>
) } ) }
<Flex columnGap={ 2 } w="100%"> <Flex columnGap={ 2 } w="100%">
<Text fontWeight={ 500 } flexShrink={ 0 }>Age</Text> <Skeleton isLoaded={ !props.isLoading } fontWeight={ 500 } flexShrink={ 0 }>Age</Skeleton>
<Text variant="secondary">{ timeAgo }</Text> <Skeleton isLoaded={ !props.isLoading } color="text_secondary">{ timeAgo }</Skeleton>
</Flex> </Flex>
</ListItemMobile> </ListItemMobile>
); );
......
import { Td, Tr, Text, Stat, StatHelpText, StatArrow } from '@chakra-ui/react'; import { Td, Tr, Text, Stat, StatHelpText, StatArrow, Skeleton } from '@chakra-ui/react';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
...@@ -13,6 +13,7 @@ import LinkInternal from 'ui/shared/LinkInternal'; ...@@ -13,6 +13,7 @@ import LinkInternal from 'ui/shared/LinkInternal';
type Props = AddressCoinBalanceHistoryItem & { type Props = AddressCoinBalanceHistoryItem & {
page: number; page: number;
isLoading: boolean;
}; };
const AddressCoinBalanceTableItem = (props: Props) => { const AddressCoinBalanceTableItem = (props: Props) => {
...@@ -24,32 +25,40 @@ const AddressCoinBalanceTableItem = (props: Props) => { ...@@ -24,32 +25,40 @@ const AddressCoinBalanceTableItem = (props: Props) => {
return ( return (
<Tr> <Tr>
<Td> <Td>
<LinkInternal href={ blockUrl } fontWeight="700">{ props.block_number }</LinkInternal> <Skeleton isLoaded={ !props.isLoading } display="inline-block">
<LinkInternal href={ blockUrl } fontWeight="700">{ props.block_number }</LinkInternal>
</Skeleton>
</Td> </Td>
<Td> <Td>
{ props.transaction_hash && { props.transaction_hash &&
( (
<Address w="150px" fontWeight="700"> <Address w="150px" fontWeight="700">
<AddressLink hash={ props.transaction_hash } type="transaction"/> <AddressLink hash={ props.transaction_hash } type="transaction" isLoading={ props.isLoading }/>
</Address> </Address>
) )
} }
</Td> </Td>
<Td> <Td>
<Text variant="secondary">{ timeAgo }</Text> <Skeleton isLoaded={ !props.isLoading } color="text_secondary" display="inline-block">
<span>{ timeAgo }</span>
</Skeleton>
</Td> </Td>
<Td isNumeric pr={ 1 }> <Td isNumeric pr={ 1 }>
<Text>{ BigNumber(props.value).div(WEI).dp(8).toFormat() }</Text> <Skeleton isLoaded={ !props.isLoading } color="text_secondary" display="inline-block">
<span>{ BigNumber(props.value).div(WEI).dp(8).toFormat() }</span>
</Skeleton>
</Td> </Td>
<Td isNumeric display="flex" justifyContent="end"> <Td isNumeric display="flex" justifyContent="end">
<Stat flexGrow="0"> <Skeleton isLoaded={ !props.isLoading }>
<StatHelpText display="flex" mb={ 0 } alignItems="center"> <Stat flexGrow="0">
<StatArrow type={ isPositiveDelta ? 'increase' : 'decrease' }/> <StatHelpText display="flex" mb={ 0 } alignItems="center">
<Text as="span" color={ isPositiveDelta ? 'green.500' : 'red.500' } fontWeight={ 600 }> <StatArrow type={ isPositiveDelta ? 'increase' : 'decrease' }/>
{ deltaBn.dp(8).toFormat() } <Text as="span" color={ isPositiveDelta ? 'green.500' : 'red.500' } fontWeight={ 600 }>
</Text> { deltaBn.dp(8).toFormat() }
</StatHelpText> </Text>
</Stat> </StatHelpText>
</Stat>
</Skeleton>
</Td> </Td>
</Tr> </Tr>
); );
......
...@@ -14,9 +14,10 @@ import TokenLogo from 'ui/shared/TokenLogo'; ...@@ -14,9 +14,10 @@ import TokenLogo from 'ui/shared/TokenLogo';
interface Props { interface Props {
data: Pick<Address, 'block_number_balance_updated_at' | 'coin_balance' | 'hash' | 'exchange_rate'>; data: Pick<Address, 'block_number_balance_updated_at' | 'coin_balance' | 'hash' | 'exchange_rate'>;
isLoading: boolean;
} }
const AddressBalance = ({ data }: Props) => { const AddressBalance = ({ data, isLoading }: Props) => {
const [ lastBlockNumber, setLastBlockNumber ] = React.useState<number>(data.block_number_balance_updated_at || 0); const [ lastBlockNumber, setLastBlockNumber ] = React.useState<number>(data.block_number_balance_updated_at || 0);
const queryClient = useQueryClient(); const queryClient = useQueryClient();
...@@ -75,12 +76,14 @@ const AddressBalance = ({ data }: Props) => { ...@@ -75,12 +76,14 @@ const AddressBalance = ({ data }: Props) => {
hint={ `Address balance in ${ appConfig.network.currency.symbol }. Doesn't include ERC20, ERC721 and ERC1155 tokens` } hint={ `Address balance in ${ appConfig.network.currency.symbol }. Doesn't include ERC20, ERC721 and ERC1155 tokens` }
flexWrap="nowrap" flexWrap="nowrap"
alignItems="flex-start" alignItems="flex-start"
isLoading={ isLoading }
> >
<TokenLogo <TokenLogo
data={ tokenData } data={ tokenData }
boxSize={ 5 } boxSize={ 5 }
mr={ 2 } mr={ 2 }
fontSize="sm" fontSize="sm"
isLoading={ isLoading }
/> />
<CurrencyValue <CurrencyValue
value={ data.coin_balance || '0' } value={ data.coin_balance || '0' }
...@@ -90,6 +93,7 @@ const AddressBalance = ({ data }: Props) => { ...@@ -90,6 +93,7 @@ const AddressBalance = ({ data }: Props) => {
accuracyUsd={ 2 } accuracyUsd={ 2 }
accuracy={ 8 } accuracy={ 8 }
flexWrap="wrap" flexWrap="wrap"
isLoading={ isLoading }
/> />
</DetailsInfoItem> </DetailsInfoItem>
); );
......
...@@ -13,6 +13,7 @@ interface Props { ...@@ -13,6 +13,7 @@ interface Props {
query: UseQueryResult<AddressCounters>; query: UseQueryResult<AddressCounters>;
address: string; address: string;
onClick: () => void; onClick: () => void;
isAddressQueryLoading: boolean;
} }
const PROP_TO_TAB = { const PROP_TO_TAB = {
...@@ -21,8 +22,8 @@ const PROP_TO_TAB = { ...@@ -21,8 +22,8 @@ const PROP_TO_TAB = {
validations_count: 'blocks_validated', validations_count: 'blocks_validated',
}; };
const AddressCounterItem = ({ prop, query, address, onClick }: Props) => { const AddressCounterItem = ({ prop, query, address, onClick, isAddressQueryLoading }: Props) => {
if (query.isLoading) { if (query.isPlaceholderData || isAddressQueryLoading) {
return <Skeleton h={ 5 } w="80px" borderRadius="full"/>; return <Skeleton h={ 5 } w="80px" borderRadius="full"/>;
} }
......
import { Box, Flex, Grid, Skeleton, SkeletonCircle } from '@chakra-ui/react';
import React from 'react';
import DetailsSkeletonRow from 'ui/shared/skeletons/DetailsSkeletonRow';
const AddressDetailsSkeleton = () => {
return (
<Box>
<Flex align="center">
<SkeletonCircle boxSize={ 6 } flexShrink={ 0 }/>
<Skeleton h={ 6 } w={{ base: '100px', lg: '420px' }} ml={ 2 } borderRadius="full"/>
<Skeleton h={ 8 } w="36px" ml={ 3 } flexShrink={ 0 }/>
<Skeleton h={ 8 } w="36px" ml={ 3 } flexShrink={ 0 }/>
</Flex>
<Flex align="center" columnGap={ 4 } mt={ 8 }>
<Skeleton h={ 6 } w="200px" borderRadius="full"/>
<Skeleton h={ 6 } w="80px" borderRadius="full"/>
</Flex>
<Grid columnGap={ 8 } rowGap={{ base: 5, lg: 7 }} templateColumns={{ base: '1fr', lg: '150px 1fr' }} maxW="1000px" mt={ 8 }>
<DetailsSkeletonRow w="30%"/>
<DetailsSkeletonRow w="30%"/>
<DetailsSkeletonRow w="10%"/>
<DetailsSkeletonRow w="10%"/>
<DetailsSkeletonRow w="20%"/>
<DetailsSkeletonRow w="20%"/>
</Grid>
</Box>
);
};
export default AddressDetailsSkeleton;
import { Skeleton } from '@chakra-ui/react';
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
...@@ -9,19 +10,23 @@ import LinkInternal from 'ui/shared/LinkInternal'; ...@@ -9,19 +10,23 @@ import LinkInternal from 'ui/shared/LinkInternal';
interface Props { interface Props {
data: Pick<Address, 'name' | 'token' | 'is_contract'>; data: Pick<Address, 'name' | 'token' | 'is_contract'>;
isLoading: boolean;
} }
const AddressNameInfo = ({ data }: Props) => { const AddressNameInfo = ({ data, isLoading }: Props) => {
if (data.token) { if (data.token) {
const symbol = data.token.symbol ? ` (${ trimTokenSymbol(data.token.symbol) })` : ''; const symbol = data.token.symbol ? ` (${ trimTokenSymbol(data.token.symbol) })` : '';
return ( return (
<DetailsInfoItem <DetailsInfoItem
title="Token name" title="Token name"
hint="Token name and symbol" hint="Token name and symbol"
isLoading={ isLoading }
> >
<LinkInternal href={ route({ pathname: '/token/[hash]', query: { hash: data.token.address } }) }> <Skeleton isLoaded={ !isLoading }>
{ data.token.name || 'Unnamed token' }{ symbol } <LinkInternal href={ route({ pathname: '/token/[hash]', query: { hash: data.token.address } }) }>
</LinkInternal> { data.token.name || 'Unnamed token' }{ symbol }
</LinkInternal>
</Skeleton>
</DetailsInfoItem> </DetailsInfoItem>
); );
} }
...@@ -31,8 +36,11 @@ const AddressNameInfo = ({ data }: Props) => { ...@@ -31,8 +36,11 @@ const AddressNameInfo = ({ data }: Props) => {
<DetailsInfoItem <DetailsInfoItem
title="Contract name" title="Contract name"
hint="The name found in the source code of the Contract" hint="The name found in the source code of the Contract"
isLoading={ isLoading }
> >
{ data.name } <Skeleton isLoaded={ !isLoading }>
{ data.name }
</Skeleton>
</DetailsInfoItem> </DetailsInfoItem>
); );
} }
...@@ -42,8 +50,11 @@ const AddressNameInfo = ({ data }: Props) => { ...@@ -42,8 +50,11 @@ const AddressNameInfo = ({ data }: Props) => {
<DetailsInfoItem <DetailsInfoItem
title="Validator name" title="Validator name"
hint="The name of the validator" hint="The name of the validator"
isLoading={ isLoading }
> >
{ data.name } <Skeleton isLoaded={ !isLoading }>
{ data.name }
</Skeleton>
</DetailsInfoItem> </DetailsInfoItem>
); );
} }
......
...@@ -8,12 +8,20 @@ import AddressIntTxsListItem from 'ui/address/internals/AddressIntTxsListItem'; ...@@ -8,12 +8,20 @@ import AddressIntTxsListItem from 'ui/address/internals/AddressIntTxsListItem';
type Props = { type Props = {
data: Array<InternalTransaction>; data: Array<InternalTransaction>;
currentAddress: string; currentAddress: string;
isLoading?: boolean;
} }
const AddressIntTxsList = ({ data, currentAddress }: Props) => { const AddressIntTxsList = ({ data, currentAddress, isLoading }: Props) => {
return ( return (
<Box> <Box>
{ data.map((item) => <AddressIntTxsListItem key={ item.transaction_hash } { ...item } currentAddress={ currentAddress }/>) } { data.map((item, index) => (
<AddressIntTxsListItem
key={ item.transaction_hash + '_' + index }
{ ...item }
currentAddress={ currentAddress }
isLoading={ isLoading }
/>
)) }
</Box> </Box>
); );
}; };
......
import { Flex, Tag, Icon, Box, HStack, Text } from '@chakra-ui/react'; import { Flex, Box, HStack, Skeleton } from '@chakra-ui/react';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
...@@ -11,6 +11,8 @@ import dayjs from 'lib/date/dayjs'; ...@@ -11,6 +11,8 @@ import dayjs from 'lib/date/dayjs';
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 Icon from 'ui/shared/chakra/Icon';
import Tag from 'ui/shared/chakra/Tag';
import CopyToClipboard from 'ui/shared/CopyToClipboard'; import CopyToClipboard from 'ui/shared/CopyToClipboard';
import InOutTag from 'ui/shared/InOutTag'; import InOutTag from 'ui/shared/InOutTag';
import LinkInternal from 'ui/shared/LinkInternal'; import LinkInternal from 'ui/shared/LinkInternal';
...@@ -18,7 +20,7 @@ import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile'; ...@@ -18,7 +20,7 @@ import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
import TxStatus from 'ui/shared/TxStatus'; import TxStatus from 'ui/shared/TxStatus';
import { TX_INTERNALS_ITEMS } from 'ui/tx/internals/utils'; import { TX_INTERNALS_ITEMS } from 'ui/tx/internals/utils';
type Props = InternalTransaction & { currentAddress: string }; type Props = InternalTransaction & { currentAddress: string; isLoading?: boolean };
const TxInternalsListItem = ({ const TxInternalsListItem = ({
type, type,
...@@ -32,6 +34,7 @@ const TxInternalsListItem = ({ ...@@ -32,6 +34,7 @@ const TxInternalsListItem = ({
block, block,
timestamp, timestamp,
currentAddress, currentAddress,
isLoading,
}: Props) => { }: Props) => {
const typeTitle = TX_INTERNALS_ITEMS.find(({ id }) => id === type)?.title; const typeTitle = TX_INTERNALS_ITEMS.find(({ id }) => id === type)?.title;
const toData = to ? to : createdContract; const toData = to ? to : createdContract;
...@@ -41,41 +44,45 @@ const TxInternalsListItem = ({ ...@@ -41,41 +44,45 @@ const TxInternalsListItem = ({
return ( return (
<ListItemMobile rowGap={ 3 }> <ListItemMobile rowGap={ 3 }>
<Flex> <Flex columnGap={ 2 }>
{ typeTitle && <Tag colorScheme="cyan" mr={ 2 }>{ typeTitle }</Tag> } { typeTitle && <Tag colorScheme="cyan" isLoading={ isLoading }>{ typeTitle }</Tag> }
<TxStatus status={ success ? 'ok' : 'error' } errorText={ error }/> <TxStatus status={ success ? 'ok' : 'error' } errorText={ error } isLoading={ isLoading }/>
</Flex> </Flex>
<Flex justifyContent="space-between" width="100%"> <Flex justifyContent="space-between" width="100%">
<AddressLink fontWeight="700" hash={ txnHash } truncation="constant" type="transaction"/> <AddressLink fontWeight="700" hash={ txnHash } truncation="constant" type="transaction" isLoading={ isLoading }/>
<Text variant="secondary" fontWeight="400" fontSize="sm">{ dayjs(timestamp).fromNow() }</Text> <Skeleton isLoaded={ !isLoading } color="text_secondary" fontWeight="400" fontSize="sm">
<span>{ dayjs(timestamp).fromNow() }</span>
</Skeleton>
</Flex> </Flex>
<HStack spacing={ 1 }> <HStack spacing={ 1 }>
<Text fontSize="sm" fontWeight={ 500 }>Block</Text> <Skeleton isLoaded={ !isLoading } fontSize="sm" fontWeight={ 500 }>Block</Skeleton>
<LinkInternal href={ route({ pathname: '/block/[height]', query: { height: block.toString() } }) }>{ block }</LinkInternal> <Skeleton isLoaded={ !isLoading }>
<LinkInternal href={ route({ pathname: '/block/[height]', query: { height: block.toString() } }) }>{ block }</LinkInternal>
</Skeleton>
</HStack> </HStack>
<Box w="100%" display="flex" columnGap={ 3 }> <Box w="100%" display="flex" columnGap={ 3 }>
<Address width="calc((100% - 48px) / 2)"> <Address width="calc((100% - 48px) / 2)">
<AddressIcon address={ from }/> <AddressIcon address={ from } isLoading={ isLoading }/>
<AddressLink type="address" ml={ 2 } fontWeight="500" hash={ from.hash } isDisabled={ isOut }/> <AddressLink type="address" ml={ 2 } fontWeight="500" hash={ from.hash } isDisabled={ isOut } isLoading={ isLoading }/>
{ isIn && <CopyToClipboard text={ from.hash }/> } { isIn && <CopyToClipboard text={ from.hash } isLoading={ isLoading }/> }
</Address> </Address>
{ (isIn || isOut) ? { (isIn || isOut) ?
<InOutTag isIn={ isIn } isOut={ isOut }/> : <InOutTag isIn={ isIn } isOut={ isOut } isLoading={ isLoading }/> :
<Icon as={ eastArrowIcon } boxSize={ 6 } color="gray.500"/> <Icon as={ eastArrowIcon } boxSize={ 6 } color="gray.500" isLoading={ isLoading }/>
} }
{ toData && ( { toData && (
<Address width="calc((100% - 48px) / 2)"> <Address width="calc((100% - 48px) / 2)">
<AddressIcon address={ toData }/> <AddressIcon address={ toData } isLoading={ isLoading }/>
<AddressLink type="address" ml={ 2 } fontWeight="500" hash={ toData.hash } isDisabled={ isIn }/> <AddressLink type="address" ml={ 2 } fontWeight="500" hash={ toData.hash } isDisabled={ isIn } isLoading={ isLoading }/>
{ isOut && <CopyToClipboard text={ toData.hash }/> } { isOut && <CopyToClipboard text={ toData.hash } isLoading={ isLoading }/> }
</Address> </Address>
) } ) }
</Box> </Box>
<HStack spacing={ 3 }> <HStack spacing={ 3 }>
<Text fontSize="sm" fontWeight={ 500 }>Value { appConfig.network.currency.symbol }</Text> <Skeleton isLoaded={ !isLoading } fontSize="sm" fontWeight={ 500 }>Value { appConfig.network.currency.symbol }</Skeleton>
<Text fontSize="sm" variant="secondary"> <Skeleton isLoaded={ !isLoading } fontSize="sm" color="text_secondary" minW={ 6 }>
{ BigNumber(value).div(BigNumber(10 ** appConfig.network.currency.decimals)).toFormat() } <span>{ BigNumber(value).div(BigNumber(10 ** appConfig.network.currency.decimals)).toFormat() }</span>
</Text> </Skeleton>
</HStack> </HStack>
</ListItemMobile> </ListItemMobile>
); );
......
...@@ -11,9 +11,10 @@ import AddressIntTxsTableItem from './AddressIntTxsTableItem'; ...@@ -11,9 +11,10 @@ import AddressIntTxsTableItem from './AddressIntTxsTableItem';
interface Props { interface Props {
data: Array<InternalTransaction>; data: Array<InternalTransaction>;
currentAddress: string; currentAddress: string;
isLoading?: boolean;
} }
const AddressIntTxsTable = ({ data, currentAddress }: Props) => { const AddressIntTxsTable = ({ data, currentAddress, isLoading }: Props) => {
return ( return (
<Table variant="simple" size="sm"> <Table variant="simple" size="sm">
<Thead top={ 80 }> <Thead top={ 80 }>
...@@ -30,8 +31,13 @@ const AddressIntTxsTable = ({ data, currentAddress }: Props) => { ...@@ -30,8 +31,13 @@ const AddressIntTxsTable = ({ data, currentAddress }: Props) => {
</Tr> </Tr>
</Thead> </Thead>
<Tbody> <Tbody>
{ data.map((item) => ( { data.map((item, index) => (
<AddressIntTxsTableItem key={ item.transaction_hash } { ...item } currentAddress={ currentAddress }/> <AddressIntTxsTableItem
key={ item.transaction_hash + '_' + index }
{ ...item }
currentAddress={ currentAddress }
isLoading={ isLoading }
/>
)) } )) }
</Tbody> </Tbody>
</Table> </Table>
......
import { Tr, Td, Tag, Icon, Box, Flex, Text } from '@chakra-ui/react'; import { Tr, Td, Box, Flex, Skeleton } from '@chakra-ui/react';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
...@@ -11,13 +11,15 @@ import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement'; ...@@ -11,13 +11,15 @@ 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 Icon from 'ui/shared/chakra/Icon';
import Tag from 'ui/shared/chakra/Tag';
import CopyToClipboard from 'ui/shared/CopyToClipboard'; import CopyToClipboard from 'ui/shared/CopyToClipboard';
import InOutTag from 'ui/shared/InOutTag'; import InOutTag from 'ui/shared/InOutTag';
import LinkInternal from 'ui/shared/LinkInternal'; import LinkInternal from 'ui/shared/LinkInternal';
import TxStatus from 'ui/shared/TxStatus'; import TxStatus from 'ui/shared/TxStatus';
import { TX_INTERNALS_ITEMS } from 'ui/tx/internals/utils'; import { TX_INTERNALS_ITEMS } from 'ui/tx/internals/utils';
type Props = InternalTransaction & { currentAddress: string } type Props = InternalTransaction & { currentAddress: string; isLoading?: boolean }
const AddressIntTxsTableItem = ({ const AddressIntTxsTableItem = ({
type, type,
...@@ -31,6 +33,7 @@ const AddressIntTxsTableItem = ({ ...@@ -31,6 +33,7 @@ const AddressIntTxsTableItem = ({
block, block,
timestamp, timestamp,
currentAddress, currentAddress,
isLoading,
}: Props) => { }: Props) => {
const typeTitle = TX_INTERNALS_ITEMS.find(({ id }) => id === type)?.title; const typeTitle = TX_INTERNALS_ITEMS.find(({ id }) => id === type)?.title;
const toData = to ? to : createdContract; const toData = to ? to : createdContract;
...@@ -44,47 +47,66 @@ const AddressIntTxsTableItem = ({ ...@@ -44,47 +47,66 @@ const AddressIntTxsTableItem = ({
<Tr alignItems="top"> <Tr alignItems="top">
<Td verticalAlign="middle"> <Td verticalAlign="middle">
<Flex rowGap={ 3 } flexWrap="wrap"> <Flex rowGap={ 3 } flexWrap="wrap">
<AddressLink fontWeight="700" hash={ txnHash } type="transaction"/> <AddressLink fontWeight="700" hash={ txnHash } type="transaction" isLoading={ isLoading }/>
{ timestamp && <Text variant="secondary" fontWeight="400" fontSize="sm">{ timeAgo }</Text> } { timestamp && (
<Skeleton isLoaded={ !isLoading } color="text_secondary" fontWeight="400" fontSize="sm">
<span>{ timeAgo }</span>
</Skeleton>
) }
</Flex> </Flex>
</Td> </Td>
<Td verticalAlign="middle"> <Td verticalAlign="middle">
<Flex rowGap={ 2 } flexWrap="wrap"> <Flex rowGap={ 2 } flexWrap="wrap">
{ typeTitle && ( { typeTitle && (
<Box w="126px" display="inline-block"> <Box w="126px" display="inline-block">
<Tag colorScheme="cyan" mr={ 5 }>{ typeTitle }</Tag> <Tag colorScheme="cyan" mr={ 5 } isLoading={ isLoading }>{ typeTitle }</Tag>
</Box> </Box>
) } ) }
<TxStatus status={ success ? 'ok' : 'error' } errorText={ error }/> <TxStatus status={ success ? 'ok' : 'error' } errorText={ error } isLoading={ isLoading }/>
</Flex> </Flex>
</Td> </Td>
<Td verticalAlign="middle"> <Td verticalAlign="middle">
<LinkInternal href={ route({ pathname: '/block/[height]', query: { height: block.toString() } }) }>{ block }</LinkInternal> <Skeleton isLoaded={ !isLoading } display="inline-block">
<LinkInternal href={ route({ pathname: '/block/[height]', query: { height: block.toString() } }) }>
{ block }
</LinkInternal>
</Skeleton>
</Td> </Td>
<Td verticalAlign="middle"> <Td verticalAlign="middle">
<Address display="inline-flex" maxW="100%"> <Address display="inline-flex" maxW="100%">
<AddressIcon address={ from }/> <AddressIcon address={ from } isLoading={ isLoading }/>
<AddressLink type="address" ml={ 2 } fontWeight="500" hash={ from.hash } alias={ from.name } flexGrow={ 1 } isDisabled={ isOut }/> <AddressLink
{ isIn && <CopyToClipboard text={ from.hash }/> } type="address"
ml={ 2 }
fontWeight="500"
hash={ from.hash }
alias={ from.name }
flexGrow={ 1 }
isDisabled={ isOut }
isLoading={ isLoading }
/>
{ isIn && <CopyToClipboard text={ from.hash } isLoading={ isLoading }/> }
</Address> </Address>
</Td> </Td>
<Td px={ 0 } verticalAlign="middle"> <Td px={ 0 } verticalAlign="middle">
{ (isIn || isOut) ? { (isIn || isOut) ?
<InOutTag isIn={ isIn } isOut={ isOut }/> : <InOutTag isIn={ isIn } isOut={ isOut } isLoading={ isLoading }/> :
<Icon as={ rightArrowIcon } boxSize={ 6 } color="gray.500"/> <Icon as={ rightArrowIcon } boxSize={ 6 } color="gray.500" isLoading={ isLoading }/>
} }
</Td> </Td>
<Td verticalAlign="middle"> <Td verticalAlign="middle">
{ toData && ( { toData && (
<Address display="inline-flex" maxW="100%"> <Address display="inline-flex" maxW="100%">
<AddressIcon address={ toData }/> <AddressIcon address={ toData } isLoading={ isLoading }/>
<AddressLink type="address" hash={ toData.hash } alias={ toData.name } fontWeight="500" ml={ 2 } isDisabled={ isIn }/> <AddressLink type="address" hash={ toData.hash } alias={ toData.name } fontWeight="500" ml={ 2 } isDisabled={ isIn } isLoading={ isLoading }/>
{ isOut && <CopyToClipboard text={ toData.hash }/> } { isOut && <CopyToClipboard text={ toData.hash } isLoading={ isLoading }/> }
</Address> </Address>
) } ) }
</Td> </Td>
<Td isNumeric verticalAlign="middle"> <Td isNumeric verticalAlign="middle">
{ BigNumber(value).div(BigNumber(10 ** appConfig.network.currency.decimals)).toFormat() } <Skeleton isLoaded={ !isLoading } display="inline-block" minW={ 6 }>
{ BigNumber(value).div(BigNumber(10 ** appConfig.network.currency.decimals)).toFormat() }
</Skeleton>
</Td> </Td>
</Tr> </Tr>
); );
......
...@@ -4,6 +4,7 @@ import React from 'react'; ...@@ -4,6 +4,7 @@ import React from 'react';
import * as coinBalanceMock from 'mocks/address/coinBalanceHistory'; import * as coinBalanceMock from 'mocks/address/coinBalanceHistory';
import * as tokensMock from 'mocks/address/tokens'; import * as tokensMock from 'mocks/address/tokens';
import { tokenInfoERC20a } from 'mocks/tokens/tokenInfo';
import * as socketServer from 'playwright/fixtures/socketServer'; import * as socketServer from 'playwright/fixtures/socketServer';
import TestApp from 'playwright/TestApp'; import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl'; import buildApiUrl from 'playwright/utils/buildApiUrl';
...@@ -11,7 +12,7 @@ import MockAddressPage from 'ui/address/testUtils/MockAddressPage'; ...@@ -11,7 +12,7 @@ import MockAddressPage from 'ui/address/testUtils/MockAddressPage';
import TokenSelect from './TokenSelect'; import TokenSelect from './TokenSelect';
const ASSET_URL = 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xb2a90505dc6680a7a695f7975d0d32EeF610f456/logo.png'; const ASSET_URL = tokenInfoERC20a.icon_url as string;
const TOKENS_ERC20_API_URL = buildApiUrl('address_tokens', { hash: '1' }) + '?type=ERC-20'; const TOKENS_ERC20_API_URL = buildApiUrl('address_tokens', { hash: '1' }) + '?type=ERC-20';
const TOKENS_ERC721_API_URL = buildApiUrl('address_tokens', { hash: '1' }) + '?type=ERC-721'; const TOKENS_ERC721_API_URL = buildApiUrl('address_tokens', { hash: '1' }) + '?type=ERC-721';
const TOKENS_ER1155_API_URL = buildApiUrl('address_tokens', { hash: '1' }) + '?type=ERC-1155'; const TOKENS_ER1155_API_URL = buildApiUrl('address_tokens', { hash: '1' }) + '?type=ERC-1155';
......
...@@ -67,7 +67,12 @@ const TokenSelect = ({ onClick }: Props) => { ...@@ -67,7 +67,12 @@ const TokenSelect = ({ onClick }: Props) => {
}); });
if (isLoading) { if (isLoading) {
return <Skeleton h={ 8 } w="160px"/>; return (
<Flex columnGap={ 3 }>
<Skeleton h={ 8 } w="150px" borderRadius="base"/>
<Skeleton h={ 8 } w={ 9 } borderRadius="base"/>
</Flex>
);
} }
const hasTokens = _sumBy(Object.values(data), ({ items }) => items.length) > 0; const hasTokens = _sumBy(Object.values(data), ({ items }) => items.length) > 0;
......
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';
...@@ -22,7 +22,7 @@ type Props = { ...@@ -22,7 +22,7 @@ type Props = {
const ERC1155Tokens = ({ tokensQuery }: Props) => { const ERC1155Tokens = ({ tokensQuery }: Props) => {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const { isError, isLoading, data, pagination, isPaginationVisible } = tokensQuery; const { isError, isPlaceholderData, data, pagination, isPaginationVisible } = tokensQuery;
const actionBar = isMobile && isPaginationVisible && ( const actionBar = isMobile && isPaginationVisible && (
<ActionBar mt={ -6 }> <ActionBar mt={ -6 }>
...@@ -30,21 +30,6 @@ const ERC1155Tokens = ({ tokensQuery }: Props) => { ...@@ -30,21 +30,6 @@ const ERC1155Tokens = ({ tokensQuery }: 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 content = data?.items ? ( const content = data?.items ? (
<Grid <Grid
w="100%" w="100%"
...@@ -52,19 +37,29 @@ const ERC1155Tokens = ({ tokensQuery }: Props) => { ...@@ -52,19 +37,29 @@ const ERC1155Tokens = ({ tokensQuery }: 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))' }}
> >
{ data.items.map(item => <NFTItem key={ item.token.address } { ...item }/>) } { data.items.map((item, index) => {
const key = item.token.address + '_' + (item.token_instance?.id && !isPlaceholderData ? `id_${ item.token_instance?.id }` : `index_${ index }`);
return (
<NFTItem
key={ key }
{ ...item }
isLoading={ isPlaceholderData }
/>
);
}) }
</Grid> </Grid>
) : null; ) : null;
return ( return (
<DataListDisplay <DataListDisplay
isError={ isError } isError={ isError }
isLoading={ isLoading } isLoading={ false }
items={ data?.items } items={ data?.items }
emptyText="There are no tokens of selected type." emptyText="There are no tokens of selected type."
content={ content } content={ content }
actionBar={ actionBar } actionBar={ actionBar }
skeletonProps={{ customSkeleton: skeleton }} skeletonProps={{ customSkeleton: null }}
/> />
); );
}; };
......
...@@ -23,7 +23,7 @@ type Props = { ...@@ -23,7 +23,7 @@ type Props = {
const ERC20Tokens = ({ tokensQuery }: Props) => { const ERC20Tokens = ({ tokensQuery }: Props) => {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const { isError, isLoading, data, pagination, isPaginationVisible } = tokensQuery; const { isError, isPlaceholderData, data, pagination, isPaginationVisible } = tokensQuery;
const actionBar = isMobile && isPaginationVisible && ( const actionBar = isMobile && isPaginationVisible && (
<ActionBar mt={ -6 }> <ActionBar mt={ -6 }>
...@@ -33,14 +33,20 @@ const ERC20Tokens = ({ tokensQuery }: Props) => { ...@@ -33,14 +33,20 @@ const ERC20Tokens = ({ tokensQuery }: Props) => {
const content = data?.items ? ( const content = data?.items ? (
<> <>
<Hide below="lg" ssr={ false }><ERC20TokensTable data={ data.items } top={ isPaginationVisible ? 72 : 0 }/></Hide> <Hide below="lg" ssr={ false }><ERC20TokensTable data={ data.items } top={ isPaginationVisible ? 72 : 0 } isLoading={ isPlaceholderData }/></Hide>
<Show below="lg" ssr={ false }>{ data.items.map(item => <ERC20TokensListItem key={ item.token.address } { ...item }/>) }</Show></> <Show below="lg" ssr={ false }>{ data.items.map((item, index) => (
<ERC20TokensListItem
key={ item.token.address + (isPlaceholderData ? index : '') }
{ ...item }
isLoading={ isPlaceholderData }
/>
)) }</Show></>
) : null; ) : null;
return ( return (
<DataListDisplay <DataListDisplay
isError={ isError } isError={ isError }
isLoading={ isLoading } isLoading={ false }
items={ data?.items } items={ data?.items }
skeletonProps={{ skeletonProps={{
isLongSkeleton: true, isLongSkeleton: true,
......
import { Flex, HStack, Text } from '@chakra-ui/react'; import { Flex, HStack, Skeleton } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { AddressTokenBalance } from 'types/api/address'; import type { AddressTokenBalance } from 'types/api/address';
...@@ -10,9 +10,9 @@ import CopyToClipboard from 'ui/shared/CopyToClipboard'; ...@@ -10,9 +10,9 @@ import CopyToClipboard from 'ui/shared/CopyToClipboard';
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile'; import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
import TokenLogo from 'ui/shared/TokenLogo'; import TokenLogo from 'ui/shared/TokenLogo';
type Props = AddressTokenBalance; type Props = AddressTokenBalance & { isLoading: boolean};
const ERC20TokensListItem = ({ token, value }: Props) => { const ERC20TokensListItem = ({ token, value, isLoading }: Props) => {
const tokenString = [ token.name, token.symbol && `(${ token.symbol })` ].filter(Boolean).join(' '); const tokenString = [ token.name, token.symbol && `(${ token.symbol })` ].filter(Boolean).join(' ');
...@@ -24,28 +24,34 @@ const ERC20TokensListItem = ({ token, value }: Props) => { ...@@ -24,28 +24,34 @@ const ERC20TokensListItem = ({ token, value }: Props) => {
return ( return (
<ListItemMobile rowGap={ 2 }> <ListItemMobile rowGap={ 2 }>
<Flex alignItems="center" width="100%"> <Flex alignItems="center" width="100%">
<TokenLogo data={ token } boxSize={ 6 } mr={ 2 }/> <TokenLogo data={ token } boxSize={ 6 } mr={ 2 } isLoading={ isLoading }/>
<AddressLink fontWeight="700" hash={ token.address } type="token" alias={ tokenString }/> <AddressLink fontWeight="700" hash={ token.address } type="token" alias={ tokenString } isLoading={ isLoading }/>
</Flex> </Flex>
<Flex alignItems="center" pl={ 8 }> <Flex alignItems="center" pl={ 8 }>
<AddressLink hash={ token.address } type="address" truncation="constant"/> <AddressLink hash={ token.address } type="address" truncation="constant" isLoading={ isLoading }/>
<CopyToClipboard text={ token.address } ml={ 1 }/> <CopyToClipboard text={ token.address } isLoading={ isLoading }/>
<AddressAddToWallet token={ token } ml={ 2 }/> <AddressAddToWallet token={ token } ml={ 2 } isLoading={ isLoading }/>
</Flex> </Flex>
{ token.exchange_rate !== undefined && token.exchange_rate !== null && ( { token.exchange_rate !== undefined && token.exchange_rate !== null && (
<HStack spacing={ 3 }> <HStack spacing={ 3 }>
<Text fontSize="sm" fontWeight={ 500 }>Price</Text> <Skeleton isLoaded={ !isLoading } fontSize="sm" fontWeight={ 500 }>Price</Skeleton>
<Text fontSize="sm" variant="secondary">{ `$${ token.exchange_rate }` }</Text> <Skeleton isLoaded={ !isLoading } fontSize="sm" color="text_secondary">
<span>{ `$${ token.exchange_rate }` }</span>
</Skeleton>
</HStack> </HStack>
) } ) }
<HStack spacing={ 3 }> <HStack spacing={ 3 }>
<Text fontSize="sm" fontWeight={ 500 }>Quantity</Text> <Skeleton isLoaded={ !isLoading } fontSize="sm" fontWeight={ 500 }>Quantity</Skeleton>
<Text fontSize="sm" variant="secondary">{ tokenQuantity }</Text> <Skeleton isLoaded={ !isLoading } fontSize="sm" color="text_secondary">
<span>{ tokenQuantity }</span>
</Skeleton>
</HStack> </HStack>
{ tokenValue !== undefined && ( { tokenValue !== undefined && (
<HStack spacing={ 3 }> <HStack spacing={ 3 }>
<Text fontSize="sm" fontWeight={ 500 }>Value</Text> <Skeleton isLoaded={ !isLoading } fontSize="sm" fontWeight={ 500 }>Value</Skeleton>
<Text fontSize="sm" variant="secondary">{ tokenValue }</Text> <Skeleton isLoaded={ !isLoading } fontSize="sm" color="text_secondary">
<span>{ tokenValue }</span>
</Skeleton>
</HStack> </HStack>
) } ) }
</ListItemMobile> </ListItemMobile>
......
...@@ -10,9 +10,10 @@ import ERC20TokensTableItem from './ERC20TokensTableItem'; ...@@ -10,9 +10,10 @@ import ERC20TokensTableItem from './ERC20TokensTableItem';
interface Props { interface Props {
data: Array<AddressTokenBalance>; data: Array<AddressTokenBalance>;
top: number; top: number;
isLoading: boolean;
} }
const ERC20TokensTable = ({ data, top }: Props) => { const ERC20TokensTable = ({ data, top, isLoading }: Props) => {
return ( return (
<Table variant="simple" size="sm"> <Table variant="simple" size="sm">
<Thead top={ top }> <Thead top={ top }>
...@@ -25,8 +26,8 @@ const ERC20TokensTable = ({ data, top }: Props) => { ...@@ -25,8 +26,8 @@ const ERC20TokensTable = ({ data, top }: Props) => {
</Tr> </Tr>
</Thead> </Thead>
<Tbody> <Tbody>
{ data.map((item) => ( { data.map((item, index) => (
<ERC20TokensTableItem key={ item.token.address } { ...item }/> <ERC20TokensTableItem key={ item.token.address + (isLoading ? index : '') } { ...item } isLoading={ isLoading }/>
)) } )) }
</Tbody> </Tbody>
</Table> </Table>
......
import { Tr, Td, Flex } from '@chakra-ui/react'; import { Tr, Td, Flex, Skeleton } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { AddressTokenBalance } from 'types/api/address'; import type { AddressTokenBalance } from 'types/api/address';
...@@ -9,11 +9,12 @@ import AddressLink from 'ui/shared/address/AddressLink'; ...@@ -9,11 +9,12 @@ import AddressLink from 'ui/shared/address/AddressLink';
import CopyToClipboard from 'ui/shared/CopyToClipboard'; import CopyToClipboard from 'ui/shared/CopyToClipboard';
import TokenLogo from 'ui/shared/TokenLogo'; import TokenLogo from 'ui/shared/TokenLogo';
type Props = AddressTokenBalance; type Props = AddressTokenBalance & { isLoading: boolean };
const ERC20TokensTableItem = ({ const ERC20TokensTableItem = ({
token, token,
value, value,
isLoading,
}: Props) => { }: Props) => {
const tokenString = [ token.name, token.symbol && `(${ token.symbol })` ].filter(Boolean).join(' '); const tokenString = [ token.name, token.symbol && `(${ token.symbol })` ].filter(Boolean).join(' ');
...@@ -27,27 +28,33 @@ const ERC20TokensTableItem = ({ ...@@ -27,27 +28,33 @@ const ERC20TokensTableItem = ({
<Tr> <Tr>
<Td verticalAlign="middle"> <Td verticalAlign="middle">
<Flex alignItems="center"> <Flex alignItems="center">
<TokenLogo data={ token } boxSize={ 6 } mr={ 2 }/> <TokenLogo data={ token } boxSize={ 6 } mr={ 2 } isLoading={ isLoading }/>
<AddressLink fontWeight="700" hash={ token.address } type="token" alias={ tokenString }/> <AddressLink fontWeight="700" hash={ token.address } type="token" alias={ tokenString } isLoading={ isLoading }/>
</Flex> </Flex>
</Td> </Td>
<Td verticalAlign="middle"> <Td verticalAlign="middle">
<Flex alignItems="center" width="150px" justifyContent="space-between"> <Flex alignItems="center" width="150px" justifyContent="space-between">
<Flex alignItems="center"> <Flex alignItems="center">
<AddressLink hash={ token.address } type="address" truncation="constant"/> <AddressLink hash={ token.address } type="address" truncation="constant" isLoading={ isLoading }/>
<CopyToClipboard text={ token.address } ml={ 1 }/> <CopyToClipboard text={ token.address } isLoading={ isLoading }/>
</Flex> </Flex>
<AddressAddToWallet token={ token } ml={ 4 }/> <AddressAddToWallet token={ token } ml={ 4 } isLoading={ isLoading }/>
</Flex> </Flex>
</Td> </Td>
<Td isNumeric verticalAlign="middle"> <Td isNumeric verticalAlign="middle">
{ token.exchange_rate && `$${ token.exchange_rate }` } <Skeleton isLoaded={ !isLoading } display="inline-block">
{ token.exchange_rate && `$${ token.exchange_rate }` }
</Skeleton>
</Td> </Td>
<Td isNumeric verticalAlign="middle"> <Td isNumeric verticalAlign="middle">
{ tokenQuantity } <Skeleton isLoaded={ !isLoading } display="inline">
{ tokenQuantity }
</Skeleton>
</Td> </Td>
<Td isNumeric verticalAlign="middle"> <Td isNumeric verticalAlign="middle">
{ tokenValue && `$${ tokenValue }` } <Skeleton isLoaded={ !isLoading } display="inline">
{ tokenValue && `$${ tokenValue }` }
</Skeleton>
</Td> </Td>
</Tr> </Tr>
); );
......
...@@ -23,7 +23,7 @@ type Props = { ...@@ -23,7 +23,7 @@ type Props = {
const ERC721Tokens = ({ tokensQuery }: Props) => { const ERC721Tokens = ({ tokensQuery }: Props) => {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const { isError, isLoading, data, pagination, isPaginationVisible } = tokensQuery; const { isError, isPlaceholderData, data, pagination, isPaginationVisible } = tokensQuery;
const actionBar = isMobile && isPaginationVisible && ( const actionBar = isMobile && isPaginationVisible && (
<ActionBar mt={ -6 }> <ActionBar mt={ -6 }>
...@@ -33,14 +33,20 @@ const ERC721Tokens = ({ tokensQuery }: Props) => { ...@@ -33,14 +33,20 @@ const ERC721Tokens = ({ tokensQuery }: Props) => {
const content = data?.items ? ( const content = data?.items ? (
<> <>
<Hide below="lg" ssr={ false }><ERC721TokensTable data={ data.items } top={ isPaginationVisible ? 72 : 0 }/></Hide> <Hide below="lg" ssr={ false }><ERC721TokensTable data={ data.items } isLoading={ isPlaceholderData } top={ isPaginationVisible ? 72 : 0 }/></Hide>
<Show below="lg" ssr={ false }>{ data.items.map(item => <ERC721TokensListItem key={ item.token.address } { ...item }/>) }</Show></> <Show below="lg" ssr={ false }>{ data.items.map((item, index) => (
<ERC721TokensListItem
key={ item.token.address + (isPlaceholderData ? index : '') }
{ ...item }
isLoading={ isPlaceholderData }
/>
)) }</Show></>
) : null; ) : null;
return ( return (
<DataListDisplay <DataListDisplay
isError={ isError } isError={ isError }
isLoading={ isLoading } isLoading={ false }
items={ data?.items } items={ data?.items }
skeletonProps={{ skeletonProps={{
isLongSkeleton: true, isLongSkeleton: true,
......
import { Flex, HStack, Text } from '@chakra-ui/react'; import { Flex, HStack, Skeleton } from '@chakra-ui/react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
...@@ -10,9 +10,9 @@ import CopyToClipboard from 'ui/shared/CopyToClipboard'; ...@@ -10,9 +10,9 @@ import CopyToClipboard from 'ui/shared/CopyToClipboard';
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile'; import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
import TokenLogo from 'ui/shared/TokenLogo'; import TokenLogo from 'ui/shared/TokenLogo';
type Props = AddressTokenBalance; type Props = AddressTokenBalance & { isLoading: boolean};
const ERC721TokensListItem = ({ token, value }: Props) => { const ERC721TokensListItem = ({ token, value, isLoading }: Props) => {
const router = useRouter(); const router = useRouter();
const hash = router.query.hash?.toString() || ''; const hash = router.query.hash?.toString() || '';
...@@ -22,17 +22,19 @@ const ERC721TokensListItem = ({ token, value }: Props) => { ...@@ -22,17 +22,19 @@ const ERC721TokensListItem = ({ token, value }: Props) => {
return ( return (
<ListItemMobile rowGap={ 2 }> <ListItemMobile rowGap={ 2 }>
<Flex alignItems="center" width="100%"> <Flex alignItems="center" width="100%">
<TokenLogo data={ token } boxSize={ 6 } mr={ 2 }/> <TokenLogo data={ token } boxSize={ 6 } mr={ 2 } isLoading={ isLoading }/>
<AddressLink fontWeight="700" hash={ hash } tokenHash={ token.address } type="address_token" alias={ tokenString }/> <AddressLink fontWeight="700" hash={ hash } tokenHash={ token.address } type="address_token" alias={ tokenString } isLoading={ isLoading }/>
</Flex> </Flex>
<Flex alignItems="center" pl={ 8 }> <Flex alignItems="center" pl={ 8 }>
<AddressLink hash={ token.address } type="address" truncation="constant"/> <AddressLink hash={ token.address } type="address" truncation="constant" isLoading={ isLoading }/>
<CopyToClipboard text={ token.address } ml={ 1 }/> <CopyToClipboard text={ token.address } isLoading={ isLoading }/>
<AddressAddToWallet token={ token } ml={ 2 }/> <AddressAddToWallet token={ token } ml={ 2 } isLoading={ isLoading }/>
</Flex> </Flex>
<HStack spacing={ 3 }> <HStack spacing={ 3 }>
<Text fontSize="sm" fontWeight={ 500 }>Quantity</Text> <Skeleton isLoaded={ !isLoading } fontSize="sm" fontWeight={ 500 }>Quantity</Skeleton>
<Text fontSize="sm" variant="secondary">{ value }</Text> <Skeleton isLoaded={ !isLoading } fontSize="sm" color="text_secondary">
<span>{ value }</span>
</Skeleton>
</HStack> </HStack>
</ListItemMobile> </ListItemMobile>
); );
......
...@@ -10,9 +10,10 @@ import ERC721TokensTableItem from './ERC721TokensTableItem'; ...@@ -10,9 +10,10 @@ import ERC721TokensTableItem from './ERC721TokensTableItem';
interface Props { interface Props {
data: Array<AddressTokenBalance>; data: Array<AddressTokenBalance>;
top: number; top: number;
isLoading: boolean;
} }
const ERC721TokensTable = ({ data, top }: Props) => { const ERC721TokensTable = ({ data, 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 ERC721TokensTable = ({ data, top }: Props) => { ...@@ -23,8 +24,8 @@ const ERC721TokensTable = ({ data, top }: Props) => {
</Tr> </Tr>
</Thead> </Thead>
<Tbody> <Tbody>
{ data.map((item) => ( { data.map((item, index) => (
<ERC721TokensTableItem key={ item.token.address } { ...item }/> <ERC721TokensTableItem key={ item.token.address + (isLoading ? index : '') } { ...item } isLoading={ isLoading }/>
)) } )) }
</Tbody> </Tbody>
</Table> </Table>
......
import { Tr, Td, Flex } from '@chakra-ui/react'; import { Tr, Td, Flex, Skeleton } from '@chakra-ui/react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
...@@ -9,11 +9,12 @@ import AddressLink from 'ui/shared/address/AddressLink'; ...@@ -9,11 +9,12 @@ import AddressLink from 'ui/shared/address/AddressLink';
import CopyToClipboard from 'ui/shared/CopyToClipboard'; import CopyToClipboard from 'ui/shared/CopyToClipboard';
import TokenLogo from 'ui/shared/TokenLogo'; import TokenLogo from 'ui/shared/TokenLogo';
type Props = AddressTokenBalance; type Props = AddressTokenBalance & { isLoading: boolean};
const ERC721TokensTableItem = ({ const ERC721TokensTableItem = ({
token, token,
value, value,
isLoading,
}: Props) => { }: Props) => {
const router = useRouter(); const router = useRouter();
...@@ -24,21 +25,23 @@ const ERC721TokensTableItem = ({ ...@@ -24,21 +25,23 @@ const ERC721TokensTableItem = ({
<Tr> <Tr>
<Td verticalAlign="middle"> <Td verticalAlign="middle">
<Flex alignItems="center"> <Flex alignItems="center">
<TokenLogo data={ token } boxSize={ 6 } mr={ 2 }/> <TokenLogo data={ token } boxSize={ 6 } mr={ 2 } isLoading={ isLoading }/>
<AddressLink fontWeight="700" hash={ hash } tokenHash={ token.address } type="address_token" alias={ tokenString }/> <AddressLink fontWeight="700" hash={ hash } tokenHash={ token.address } type="address_token" alias={ tokenString } isLoading={ isLoading }/>
</Flex> </Flex>
</Td> </Td>
<Td verticalAlign="middle"> <Td verticalAlign="middle">
<Flex alignItems="center" width="150px" justifyContent="space-between"> <Flex alignItems="center" width="150px" justifyContent="space-between">
<Flex alignItems="center"> <Flex alignItems="center">
<AddressLink hash={ token.address } type="address" truncation="dynamic"/> <AddressLink hash={ token.address } type="address" truncation="dynamic" isLoading={ isLoading }/>
<CopyToClipboard text={ token.address } ml={ 1 }/> <CopyToClipboard text={ token.address } isLoading={ isLoading }/>
</Flex> </Flex>
<AddressAddToWallet token={ token } ml={ 4 }/> <AddressAddToWallet token={ token } ml={ 4 } isLoading={ isLoading }/>
</Flex> </Flex>
</Td> </Td>
<Td isNumeric verticalAlign="middle"> <Td isNumeric verticalAlign="middle">
{ value } <Skeleton isLoaded={ !isLoading } display="inline-block">
{ value }
</Skeleton>
</Td> </Td>
</Tr> </Tr>
); );
......
import { Flex, Link, Text, LinkBox, LinkOverlay, useColorModeValue } from '@chakra-ui/react'; import { Flex, Link, Text, LinkBox, LinkOverlay, useColorModeValue, Skeleton } from '@chakra-ui/react';
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
...@@ -8,9 +8,9 @@ import NftMedia from 'ui/shared/nft/NftMedia'; ...@@ -8,9 +8,9 @@ import NftMedia from 'ui/shared/nft/NftMedia';
import TokenLogo from 'ui/shared/TokenLogo'; import TokenLogo from 'ui/shared/TokenLogo';
import TruncatedTextTooltip from 'ui/shared/TruncatedTextTooltip'; import TruncatedTextTooltip from 'ui/shared/TruncatedTextTooltip';
type Props = AddressTokenBalance; type Props = AddressTokenBalance & { isLoading: boolean };
const NFTItem = ({ token, token_id: tokenId, token_instance: tokenInstance }: Props) => { const NFTItem = ({ token, token_id: tokenId, token_instance: tokenInstance, isLoading }: Props) => {
const tokenLink = route({ pathname: '/token/[hash]', query: { hash: token.address } }); const tokenLink = route({ pathname: '/token/[hash]', query: { hash: token.address } });
return ( return (
...@@ -25,33 +25,40 @@ const NFTItem = ({ token, token_id: tokenId, token_instance: tokenInstance }: Pr ...@@ -25,33 +25,40 @@ const NFTItem = ({ token, token_id: tokenId, token_instance: tokenInstance }: Pr
fontWeight={ 500 } fontWeight={ 500 }
lineHeight="20px" lineHeight="20px"
> >
<LinkOverlay href={ tokenLink }> <LinkOverlay href={ isLoading ? undefined : tokenLink }>
<NftMedia <NftMedia
mb="18px" mb="18px"
imageUrl={ tokenInstance?.image_url || null } imageUrl={ tokenInstance?.image_url || null }
animationUrl={ tokenInstance?.animation_url || null } animationUrl={ tokenInstance?.animation_url || null }
isLoading={ isLoading }
/> />
</LinkOverlay> </LinkOverlay>
{ tokenId && ( { tokenId && (
<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={ tokenId }> <TruncatedTextTooltip label={ tokenId }>
<Link <Skeleton isLoaded={ !isLoading } overflow="hidden" h="20px">
overflow="hidden" <Link
whiteSpace="nowrap" w="100%"
textOverflow="ellipsis" display="inline-block"
href={ route({ pathname: '/token/[hash]/instance/[id]', query: { hash: token.address, id: tokenId } }) } whiteSpace="nowrap"
> textOverflow="ellipsis"
{ tokenId } overflow="hidden"
</Link> href={ route({ pathname: '/token/[hash]/instance/[id]', query: { hash: token.address, id: tokenId } }) }
>
{ tokenId }
</Link>
</Skeleton>
</TruncatedTextTooltip> </TruncatedTextTooltip>
</Flex> </Flex>
) } ) }
{ token.name && ( { token.name && (
<Flex alignItems="center"> <Flex alignItems="center">
<TokenLogo data={ token } boxSize={ 6 } ml={ 1 } mr={ 1 }/> <TokenLogo data={ token } boxSize={ 6 } ml={ 1 } mr={ 1 } isLoading={ isLoading }/>
<TruncatedTextTooltip label={ token.name }> <TruncatedTextTooltip label={ token.name }>
<Text variant="secondary" overflow="hidden" whiteSpace="nowrap" textOverflow="ellipsis">{ token.name }</Text> <Skeleton isLoaded={ !isLoading } color="text_secondary" overflow="hidden" whiteSpace="nowrap" textOverflow="ellipsis">
<span>{ token.name }</span>
</Skeleton>
</TruncatedTextTooltip> </TruncatedTextTooltip>
</Flex> </Flex>
) } ) }
......
import { Flex, Skeleton } from '@chakra-ui/react'; import { Flex } from '@chakra-ui/react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
...@@ -19,7 +19,7 @@ const TokenBalances = () => { ...@@ -19,7 +19,7 @@ const TokenBalances = () => {
const addressQuery = useApiQuery('address', { const addressQuery = useApiQuery('address', {
pathParams: { hash }, pathParams: { hash },
queryOptions: { enabled: Boolean(hash) }, queryOptions: { enabled: Boolean(hash), refetchOnMount: false },
}); });
const tokenQuery = useFetchTokens({ hash }); const tokenQuery = useFetchTokens({ hash });
...@@ -28,23 +28,12 @@ const TokenBalances = () => { ...@@ -28,23 +28,12 @@ const TokenBalances = () => {
return <DataFetchAlert/>; return <DataFetchAlert/>;
} }
if (addressQuery.isLoading || tokenQuery.isLoading) {
const item = <Skeleton w={{ base: '100%', lg: '240px' }} h="82px" borderRadius="16px"/>;
return (
<Flex columnGap={ 3 } rowGap={ 3 } mt={{ base: '6px', lg: 0 }} flexDirection={{ base: 'column', lg: 'row' }}>
{ item }
{ item }
{ item }
</Flex>
);
}
const addressData = addressQuery.data; const addressData = addressQuery.data;
const { valueStr: nativeValue, usdBn: nativeUsd } = getCurrencyValue({ const { valueStr: nativeValue, usdBn: nativeUsd } = getCurrencyValue({
value: addressData.coin_balance || '0', value: addressData?.coin_balance || '0',
accuracy: 8, accuracy: 8,
accuracyUsd: 2, accuracyUsd: 2,
exchangeRate: addressData.exchange_rate, exchangeRate: addressData?.exchange_rate,
decimals: String(appConfig.network.currency.decimals), decimals: String(appConfig.network.currency.decimals),
}); });
...@@ -57,10 +46,15 @@ const TokenBalances = () => { ...@@ -57,10 +46,15 @@ const TokenBalances = () => {
return ( return (
<Flex columnGap={ 3 } rowGap={ 3 } mt={{ base: '6px', lg: 0 }} flexDirection={{ base: 'column', lg: 'row' }}> <Flex columnGap={ 3 } rowGap={ 3 } mt={{ base: '6px', lg: 0 }} flexDirection={{ base: 'column', lg: 'row' }}>
<TokenBalancesItem name="Net Worth" value={ addressData.exchange_rate ? `${ prefix }$${ totalUsd.toFormat(2) } USD` : 'N/A' }/> <TokenBalancesItem
name="Net Worth"
value={ addressData?.exchange_rate ? `${ prefix }$${ totalUsd.toFormat(2) } USD` : 'N/A' }
isLoading={ addressQuery.isLoading || tokenQuery.isLoading }
/>
<TokenBalancesItem <TokenBalancesItem
name={ `${ appConfig.network.currency.symbol } Balance` } name={ `${ appConfig.network.currency.symbol } Balance` }
value={ (!nativeUsd.eq(ZERO) ? `$${ nativeUsd.toFormat(2) } USD | ` : '') + `${ nativeValue } ${ appConfig.network.currency.symbol }` } value={ (!nativeUsd.eq(ZERO) ? `$${ nativeUsd.toFormat(2) } USD | ` : '') + `${ nativeValue } ${ appConfig.network.currency.symbol }` }
isLoading={ addressQuery.isLoading || tokenQuery.isLoading }
/> />
<TokenBalancesItem <TokenBalancesItem
name="Tokens" name="Tokens"
...@@ -68,9 +62,10 @@ const TokenBalances = () => { ...@@ -68,9 +62,10 @@ const TokenBalances = () => {
`${ prefix }$${ tokensInfo.usd.toFormat(2) } USD ` + `${ prefix }$${ tokensInfo.usd.toFormat(2) } USD ` +
tokensNumText tokensNumText
} }
isLoading={ addressQuery.isLoading || tokenQuery.isLoading }
/> />
</Flex> </Flex>
); );
}; };
export default React.memo(TokenBalances); export default TokenBalances;
import { Box, Flex, Icon, Text, useColorModeValue } from '@chakra-ui/react'; import { Box, Flex, Icon, Skeleton, Text, useColorModeValue } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import walletIcon from 'icons/wallet.svg'; import walletIcon from 'icons/wallet.svg';
const TokenBalancesItem = ({ name, value }: {name: string; value: string }) => { const TokenBalancesItem = ({ name, value, isLoading }: {name: string; value: string; isLoading: boolean }) => {
const bgColor = useColorModeValue('blackAlpha.50', 'whiteAlpha.50'); const bgColor = useColorModeValue('blackAlpha.50', 'whiteAlpha.50');
...@@ -12,7 +12,7 @@ const TokenBalancesItem = ({ name, value }: {name: string; value: string }) => { ...@@ -12,7 +12,7 @@ const TokenBalancesItem = ({ name, value }: {name: string; value: string }) => {
<Icon as={ walletIcon } boxSize="30px" mr={ 3 }/> <Icon as={ walletIcon } boxSize="30px" mr={ 3 }/>
<Box> <Box>
<Text variant="secondary" fontSize="xs">{ name }</Text> <Text variant="secondary" fontSize="xs">{ name }</Text>
<Text fontWeight="500">{ value }</Text> <Skeleton isLoaded={ !isLoading } fontWeight="500">{ value }</Skeleton>
</Box> </Box>
</Flex> </Flex>
); );
......
import { Flex, Tag, Text, HStack } from '@chakra-ui/react'; import { Flex, HStack, Skeleton } from '@chakra-ui/react';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import React from 'react'; import React from 'react';
...@@ -8,18 +8,22 @@ import appConfig from 'configs/app/config'; ...@@ -8,18 +8,22 @@ import appConfig from 'configs/app/config';
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 ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile'; import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
type Props = { type Props = {
item: AddressesItem; item: AddressesItem;
index: number; index: number;
totalSupply: string; totalSupply: string;
isLoading?: boolean;
} }
const AddressesListItem = ({ const AddressesListItem = ({
item, item,
index, index,
totalSupply, totalSupply,
isLoading,
}: Props) => { }: Props) => {
const addressBalance = BigNumber(item.coin_balance).div(BigNumber(10 ** appConfig.network.currency.decimals)); const addressBalance = BigNumber(item.coin_balance).div(BigNumber(10 ** appConfig.network.currency.decimals));
...@@ -28,7 +32,7 @@ const AddressesListItem = ({ ...@@ -28,7 +32,7 @@ const AddressesListItem = ({
<ListItemMobile rowGap={ 3 }> <ListItemMobile rowGap={ 3 }>
<Flex alignItems="center" justifyContent="space-between" w="100%"> <Flex alignItems="center" justifyContent="space-between" w="100%">
<Address maxW="100%" mr={ 8 }> <Address maxW="100%" mr={ 8 }>
<AddressIcon address={ item } mr={ 2 }/> <AddressIcon address={ item } mr={ 2 } isLoading={ isLoading }/>
<AddressLink <AddressLink
fontWeight={ 700 } fontWeight={ 700 }
flexGrow={ 1 } flexGrow={ 1 }
...@@ -36,26 +40,36 @@ const AddressesListItem = ({ ...@@ -36,26 +40,36 @@ const AddressesListItem = ({
hash={ item.hash } hash={ item.hash }
alias={ item.name } alias={ item.name }
type="address" type="address"
isLoading={ isLoading }
/> />
<CopyToClipboard text={ item.hash } isLoading={ isLoading }/>
</Address> </Address>
<Text fontSize="sm" ml="auto" variant="secondary">{ index }</Text> <Skeleton isLoaded={ !isLoading } fontSize="sm" ml="auto" minW={ 6 } color="text_secondary">
<span>{ index }</span>
</Skeleton>
</Flex> </Flex>
{ item.public_tags !== null && item.public_tags.length > 0 && item.public_tags.map(tag => ( { item.public_tags !== null && item.public_tags.length > 0 && item.public_tags.map(tag => (
<Tag key={ tag.label }>{ tag.display_name }</Tag> <Tag key={ tag.label } isLoading={ isLoading }>{ tag.display_name }</Tag>
)) } )) }
<HStack spacing={ 3 }> <HStack spacing={ 3 }>
<Text fontSize="sm" fontWeight={ 500 }>{ `Balance ${ appConfig.network.currency.symbol }` }</Text> <Skeleton isLoaded={ !isLoading } fontSize="sm" fontWeight={ 500 }>{ `Balance ${ appConfig.network.currency.symbol }` }</Skeleton>
<Text fontSize="sm" variant="secondary">{ addressBalance.dp(8).toFormat() }</Text> <Skeleton isLoaded={ !isLoading } fontSize="sm" color="text_secondary">
<span>{ addressBalance.dp(8).toFormat() }</span>
</Skeleton>
</HStack> </HStack>
{ totalSupply && totalSupply !== '0' && ( { totalSupply && totalSupply !== '0' && (
<HStack spacing={ 3 }> <HStack spacing={ 3 }>
<Text fontSize="sm" fontWeight={ 500 }>Percentage</Text> <Skeleton isLoaded={ !isLoading } fontSize="sm" fontWeight={ 500 }>Percentage</Skeleton>
<Text fontSize="sm" variant="secondary">{ addressBalance.div(BigNumber(totalSupply)).multipliedBy(100).dp(8).toFormat() + '%' }</Text> <Skeleton isLoaded={ !isLoading } fontSize="sm" color="text_secondary">
<span>{ addressBalance.div(BigNumber(totalSupply)).multipliedBy(100).dp(8).toFormat() + '%' }</span>
</Skeleton>
</HStack> </HStack>
) } ) }
<HStack spacing={ 3 }> <HStack spacing={ 3 }>
<Text fontSize="sm" fontWeight={ 500 }>Txn count</Text> <Skeleton isLoaded={ !isLoading } fontSize="sm" fontWeight={ 500 }>Txn count</Skeleton>
<Text fontSize="sm" variant="secondary">{ Number(item.tx_count).toLocaleString() }</Text> <Skeleton isLoaded={ !isLoading } fontSize="sm" color="text_secondary">
<span>{ Number(item.tx_count).toLocaleString() }</span>
</Skeleton>
</HStack> </HStack>
</ListItemMobile> </ListItemMobile>
); );
......
...@@ -13,9 +13,10 @@ interface Props { ...@@ -13,9 +13,10 @@ interface Props {
totalSupply: string; totalSupply: string;
pageStartIndex: number; pageStartIndex: number;
top: number; top: number;
isLoading?: boolean;
} }
const AddressesTable = ({ items, totalSupply, pageStartIndex, top }: Props) => { const AddressesTable = ({ items, totalSupply, pageStartIndex, top, isLoading }: Props) => {
const hasPercentage = Boolean(totalSupply && totalSupply !== '0'); const hasPercentage = Boolean(totalSupply && totalSupply !== '0');
return ( return (
<Table variant="simple" size="sm"> <Table variant="simple" size="sm">
...@@ -32,11 +33,12 @@ const AddressesTable = ({ items, totalSupply, pageStartIndex, top }: Props) => { ...@@ -32,11 +33,12 @@ const AddressesTable = ({ items, totalSupply, pageStartIndex, top }: Props) => {
<Tbody> <Tbody>
{ items.map((item, index) => ( { items.map((item, index) => (
<AddressesTableItem <AddressesTableItem
key={ item.hash } key={ item.hash + (isLoading ? index : '') }
item={ item } item={ item }
totalSupply={ totalSupply } totalSupply={ totalSupply }
index={ pageStartIndex + index } index={ pageStartIndex + index }
hasPercentage={ hasPercentage } hasPercentage={ hasPercentage }
isLoading={ isLoading }
/> />
)) } )) }
</Tbody> </Tbody>
......
import { Tr, Td, Tag, Text } from '@chakra-ui/react'; import { Tr, Td, Text, Skeleton } from '@chakra-ui/react';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import React from 'react'; import React from 'react';
...@@ -8,12 +8,15 @@ import appConfig from 'configs/app/config'; ...@@ -8,12 +8,15 @@ import appConfig from 'configs/app/config';
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';
type Props = { type Props = {
item: AddressesItem; item: AddressesItem;
index: number; index: number;
totalSupply: string; totalSupply: string;
hasPercentage: boolean; hasPercentage: boolean;
isLoading?: boolean;
} }
const AddressesTableItem = ({ const AddressesTableItem = ({
...@@ -21,6 +24,7 @@ const AddressesTableItem = ({ ...@@ -21,6 +24,7 @@ const AddressesTableItem = ({
index, index,
totalSupply, totalSupply,
hasPercentage, hasPercentage,
isLoading,
}: Props) => { }: Props) => {
const addressBalance = BigNumber(item.coin_balance).div(BigNumber(10 ** appConfig.network.currency.decimals)); const addressBalance = BigNumber(item.coin_balance).div(BigNumber(10 ** appConfig.network.currency.decimals));
...@@ -29,11 +33,13 @@ const AddressesTableItem = ({ ...@@ -29,11 +33,13 @@ const AddressesTableItem = ({
return ( return (
<Tr> <Tr>
<Td> <Td>
<Text lineHeight="24px">{ index }</Text> <Skeleton isLoaded={ !isLoading } display="inline-block" minW={ 6 } lineHeight="24px">
{ index }
</Skeleton>
</Td> </Td>
<Td> <Td>
<Address display="inline-flex" maxW="100%"> <Address display="inline-flex" maxW="100%">
<AddressIcon address={ item } mr={ 2 }/> <AddressIcon address={ item } mr={ 2 } isLoading={ isLoading }/>
<AddressLink <AddressLink
fontWeight={ 700 } fontWeight={ 700 }
flexGrow={ 1 } flexGrow={ 1 }
...@@ -41,18 +47,22 @@ const AddressesTableItem = ({ ...@@ -41,18 +47,22 @@ const AddressesTableItem = ({
hash={ item.hash } hash={ item.hash }
alias={ item.name } alias={ item.name }
type="address" type="address"
isLoading={ isLoading }
/> />
<CopyToClipboard text={ item.hash } isLoading={ isLoading }/>
</Address> </Address>
</Td> </Td>
<Td pl={ 10 }> <Td pl={ 10 }>
{ item.public_tags && item.public_tags.length ? item.public_tags.map(tag => ( { item.public_tags && item.public_tags.length ? item.public_tags.map(tag => (
<Tag key={ tag.label }>{ tag.display_name }</Tag> <Tag key={ tag.label } isLoading={ isLoading } isTruncated>{ tag.display_name }</Tag>
)) : <Text lineHeight="24px">-</Text> } )) : null }
</Td> </Td>
<Td isNumeric> <Td isNumeric>
<Text lineHeight="24px" as="span">{ addressBalanceChunks[0] }</Text> <Skeleton isLoaded={ !isLoading } display="inline-block">
{ addressBalanceChunks[1] && <Text lineHeight="24px" as="span">.</Text> } <Text lineHeight="24px" as="span">{ addressBalanceChunks[0] }</Text>
<Text lineHeight="24px" variant="secondary" as="span">{ addressBalanceChunks[1] }</Text> { addressBalanceChunks[1] && <Text lineHeight="24px" as="span">.</Text> }
<Text lineHeight="24px" variant="secondary" as="span">{ addressBalanceChunks[1] }</Text>
</Skeleton>
</Td> </Td>
{ hasPercentage && ( { hasPercentage && (
<Td isNumeric> <Td isNumeric>
...@@ -60,7 +70,9 @@ const AddressesTableItem = ({ ...@@ -60,7 +70,9 @@ const AddressesTableItem = ({
</Td> </Td>
) } ) }
<Td isNumeric> <Td isNumeric>
<Text lineHeight="24px">{ Number(item.tx_count).toLocaleString() }</Text> <Skeleton isLoaded={ !isLoading } display="inline-block" lineHeight="24px">
{ Number(item.tx_count).toLocaleString() }
</Skeleton>
</Td> </Td>
</Tr> </Tr>
); );
......
...@@ -28,11 +28,7 @@ const addresses: AddressesResponse = { ...@@ -28,11 +28,7 @@ const addresses: AddressesResponse = {
}, },
], ],
total_supply: '25222000', total_supply: '25222000',
next_page_params: { next_page_params: null,
items_count: 50,
fetched_coin_balance: '123',
hash: 'aa',
},
}; };
test('base view +@mobile +@dark-mode', async({ mount, page }) => { test('base view +@mobile +@dark-mode', async({ mount, page }) => {
...@@ -51,5 +47,5 @@ test('base view +@mobile +@dark-mode', async({ mount, page }) => { ...@@ -51,5 +47,5 @@ test('base view +@mobile +@dark-mode', async({ mount, page }) => {
</TestApp>, </TestApp>,
); );
await expect(component.locator('main')).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
...@@ -2,19 +2,34 @@ import { Hide, Show } from '@chakra-ui/react'; ...@@ -2,19 +2,34 @@ import { Hide, Show } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import useQueryWithPages from 'lib/hooks/useQueryWithPages'; import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import { TOP_ADDRESS } from 'stubs/address';
import { generateListStub } from 'stubs/utils';
import AddressesListItem from 'ui/addresses/AddressesListItem'; import AddressesListItem from 'ui/addresses/AddressesListItem';
import AddressesTable from 'ui/addresses/AddressesTable'; import AddressesTable from 'ui/addresses/AddressesTable';
import ActionBar from 'ui/shared/ActionBar'; import ActionBar from 'ui/shared/ActionBar';
import DataListDisplay from 'ui/shared/DataListDisplay'; import DataListDisplay from 'ui/shared/DataListDisplay';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle'; import PageTitle from 'ui/shared/Page/PageTitle';
import Pagination from 'ui/shared/Pagination'; import Pagination from 'ui/shared/Pagination';
const PAGE_SIZE = 50; const PAGE_SIZE = 50;
const Accounts = () => { const Accounts = () => {
const { isError, isLoading, data, isPaginationVisible, pagination } = useQueryWithPages({ const { isError, isPlaceholderData, data, isPaginationVisible, pagination } = useQueryWithPages({
resourceName: 'addresses', resourceName: 'addresses',
options: {
placeholderData: generateListStub<'addresses'>(
TOP_ADDRESS,
50,
{
next_page_params: {
fetched_coin_balance: '42',
hash: '0x99f0ec06548b086e46cb0019c78d0b9b9f36cd53',
items_count: 50,
},
total_supply: '0',
},
),
},
}); });
const actionBar = isPaginationVisible && ( const actionBar = isPaginationVisible && (
...@@ -32,16 +47,18 @@ const Accounts = () => { ...@@ -32,16 +47,18 @@ const Accounts = () => {
items={ data.items } items={ data.items }
totalSupply={ data.total_supply } totalSupply={ data.total_supply }
pageStartIndex={ pageStartIndex } pageStartIndex={ pageStartIndex }
isLoading={ isPlaceholderData }
/> />
</Hide> </Hide>
<Show below="lg" ssr={ false }> <Show below="lg" ssr={ false }>
{ data.items.map((item, index) => { { data.items.map((item, index) => {
return ( return (
<AddressesListItem <AddressesListItem
key={ item.hash } key={ item.hash + (isPlaceholderData ? index : '') }
item={ item } item={ item }
index={ pageStartIndex + index } index={ pageStartIndex + index }
totalSupply={ data.total_supply } totalSupply={ data.total_supply }
isLoading={ isPlaceholderData }
/> />
); );
}) } }) }
...@@ -50,18 +67,18 @@ const Accounts = () => { ...@@ -50,18 +67,18 @@ const Accounts = () => {
) : null; ) : null;
return ( return (
<Page> <>
<PageTitle title="Top accounts" withTextAd/> <PageTitle title="Top accounts" withTextAd/>
<DataListDisplay <DataListDisplay
isError={ isError } isError={ isError }
isLoading={ isLoading } isLoading={ false }
items={ data?.items } items={ data?.items }
skeletonProps={{ skeletonDesktopColumns: [ '64px', '30%', '20%', '20%', '15%', '15%' ] }} skeletonProps={{ skeletonDesktopColumns: [ '64px', '30%', '20%', '20%', '15%', '15%' ] }}
emptyText="There are no accounts." emptyText="There are no accounts."
content={ content } content={ content }
actionBar={ actionBar } actionBar={ actionBar }
/> />
</Page> </>
); );
}; };
......
...@@ -12,6 +12,7 @@ import { useAppContext } from 'lib/appContext'; ...@@ -12,6 +12,7 @@ import { useAppContext } from 'lib/appContext';
import useContractTabs from 'lib/hooks/useContractTabs'; import useContractTabs from 'lib/hooks/useContractTabs';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import getQueryParamString from 'lib/router/getQueryParamString'; import getQueryParamString from 'lib/router/getQueryParamString';
import { ADDRESS_INFO } from 'stubs/address';
import AddressBlocksValidated from 'ui/address/AddressBlocksValidated'; import AddressBlocksValidated from 'ui/address/AddressBlocksValidated';
import AddressCoinBalance from 'ui/address/AddressCoinBalance'; import AddressCoinBalance from 'ui/address/AddressCoinBalance';
import AddressContract from 'ui/address/AddressContract'; import AddressContract from 'ui/address/AddressContract';
...@@ -25,7 +26,6 @@ import AddressWithdrawals from 'ui/address/AddressWithdrawals'; ...@@ -25,7 +26,6 @@ import AddressWithdrawals from 'ui/address/AddressWithdrawals';
import TextAd from 'ui/shared/ad/TextAd'; import TextAd from 'ui/shared/ad/TextAd';
import EntityTags from 'ui/shared/EntityTags'; import EntityTags from 'ui/shared/EntityTags';
import NetworkExplorers from 'ui/shared/NetworkExplorers'; import NetworkExplorers from 'ui/shared/NetworkExplorers';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle'; import PageTitle from 'ui/shared/Page/PageTitle';
import SkeletonTabs from 'ui/shared/skeletons/SkeletonTabs'; import SkeletonTabs from 'ui/shared/skeletons/SkeletonTabs';
import RoutedTabs from 'ui/shared/Tabs/RoutedTabs'; import RoutedTabs from 'ui/shared/Tabs/RoutedTabs';
...@@ -48,7 +48,10 @@ const AddressPageContent = () => { ...@@ -48,7 +48,10 @@ const AddressPageContent = () => {
const addressQuery = useApiQuery('address', { const addressQuery = useApiQuery('address', {
pathParams: { hash }, pathParams: { hash },
queryOptions: { enabled: Boolean(hash) }, queryOptions: {
enabled: Boolean(hash),
placeholderData: ADDRESS_INFO,
},
}); });
const contractTabs = useContractTabs(addressQuery.data); const contractTabs = useContractTabs(addressQuery.data);
...@@ -120,23 +123,20 @@ const AddressPageContent = () => { ...@@ -120,23 +123,20 @@ const AddressPageContent = () => {
}, [ appProps.referrer ]); }, [ appProps.referrer ]);
return ( return (
<Page> <>
{ addressQuery.isLoading ? <Skeleton h={{ base: 12, lg: 6 }} mb={ 6 } w="100%" maxW="680px"/> : <TextAd mb={ 6 }/> } { addressQuery.isPlaceholderData ? <Skeleton h={{ base: 12, lg: 6 }} mb={ 6 } w="100%" maxW="680px"/> : <TextAd mb={ 6 }/> }
{ addressQuery.isLoading ? ( <PageTitle
<Skeleton h={ 10 } w="260px" mb={ 6 }/> title={ `${ addressQuery.data?.is_contract ? 'Contract' : 'Address' } details` }
) : ( backLink={ backLink }
<PageTitle contentAfter={ tags }
title={ `${ addressQuery.data?.is_contract ? 'Contract' : 'Address' } details` } isLoading={ addressQuery.isPlaceholderData }
backLink={ backLink } />
contentAfter={ tags }
/>
) }
<AddressDetails addressQuery={ addressQuery } scrollRef={ tabsScrollRef }/> <AddressDetails addressQuery={ addressQuery } scrollRef={ tabsScrollRef }/>
{ /* should stay before tabs to scroll up with pagination */ } { /* should stay before tabs to scroll up with pagination */ }
<Box ref={ tabsScrollRef }></Box> <Box ref={ tabsScrollRef }></Box>
{ addressQuery.isLoading ? <SkeletonTabs/> : content } { addressQuery.isPlaceholderData ? <SkeletonTabs tabs={ tabs }/> : content }
{ !addressQuery.isLoading && !addressQuery.isError && <Box h={{ base: 0, lg: '40vh' }}/> } { !addressQuery.isPlaceholderData && !addressQuery.isError && <Box h={{ base: 0, lg: '40vh' }}/> }
</Page> </>
); );
}; };
......
...@@ -50,11 +50,11 @@ const BlockPageContent = () => { ...@@ -50,11 +50,11 @@ const BlockPageContent = () => {
pathParams: { height }, pathParams: { height },
options: { options: {
enabled: Boolean(!blockQuery.isPlaceholderData && blockQuery.data?.height && tab === 'txs'), enabled: Boolean(!blockQuery.isPlaceholderData && blockQuery.data?.height && tab === 'txs'),
placeholderData: generateListStub<'block_txs'>(TX, 50, { placeholderData: generateListStub<'block_txs'>(TX, 50, { next_page_params: {
block_number: 9004925, block_number: 9004925,
index: 49, index: 49,
items_count: 50, items_count: 50,
}), } }),
}, },
}); });
...@@ -63,10 +63,10 @@ const BlockPageContent = () => { ...@@ -63,10 +63,10 @@ const BlockPageContent = () => {
pathParams: { height }, pathParams: { height },
options: { options: {
enabled: Boolean(!blockQuery.isPlaceholderData && blockQuery.data?.height && appConfig.beaconChain.hasBeaconChain && tab === 'withdrawals'), enabled: Boolean(!blockQuery.isPlaceholderData && blockQuery.data?.height && appConfig.beaconChain.hasBeaconChain && tab === 'withdrawals'),
placeholderData: generateListStub<'block_withdrawals'>(WITHDRAWAL, 50, { placeholderData: generateListStub<'block_withdrawals'>(WITHDRAWAL, 50, { next_page_params: {
index: 5, index: 5,
items_count: 50, items_count: 50,
}), } }),
}, },
}); });
......
...@@ -34,10 +34,10 @@ const BlocksPageContent = () => { ...@@ -34,10 +34,10 @@ const BlocksPageContent = () => {
resourceName: 'blocks', resourceName: 'blocks',
filters: { type }, filters: { type },
options: { options: {
placeholderData: generateListStub<'blocks'>(BLOCK, 50, { placeholderData: generateListStub<'blocks'>(BLOCK, 50, { next_page_params: {
block_number: 8988686, block_number: 8988686,
items_count: 50, items_count: 50,
}), } }),
}, },
}); });
......
...@@ -12,14 +12,14 @@ import useStats from '../stats/useStats'; ...@@ -12,14 +12,14 @@ import useStats from '../stats/useStats';
const Stats = () => { const Stats = () => {
const { const {
isLoading, isPlaceholderData,
isError, isError,
sections, sections,
currentSection, currentSection,
handleSectionChange, handleSectionChange,
interval, interval,
handleIntervalChange, handleIntervalChange,
debounceFilterCharts, handleFilterChange,
displayedCharts, displayedCharts,
filterQuery, filterQuery,
} = useStats(); } = useStats();
...@@ -39,14 +39,14 @@ const Stats = () => { ...@@ -39,14 +39,14 @@ const Stats = () => {
onSectionChange={ handleSectionChange } onSectionChange={ handleSectionChange }
interval={ interval } interval={ interval }
onIntervalChange={ handleIntervalChange } onIntervalChange={ handleIntervalChange }
onFilterInputChange={ debounceFilterCharts } onFilterInputChange={ handleFilterChange }
/> />
</Box> </Box>
<ChartsWidgetsList <ChartsWidgetsList
filterQuery={ filterQuery } filterQuery={ filterQuery }
isError={ isError } isError={ isError }
isLoading={ isLoading } isPlaceholderData={ isPlaceholderData }
charts={ displayedCharts } charts={ displayedCharts }
interval={ interval } interval={ interval }
/> />
......
...@@ -136,7 +136,7 @@ const TokenPageContent = () => { ...@@ -136,7 +136,7 @@ const TokenPageContent = () => {
scrollRef, scrollRef,
options: { options: {
enabled: Boolean(router.query.hash && router.query.tab === 'holders' && hasData), enabled: Boolean(router.query.hash && router.query.tab === 'holders' && hasData),
placeholderData: tokenStubs.TOKEN_HOLDERS, placeholderData: generateListStub<'token_holders'>(tokenStubs.TOKEN_HOLDER, 50, { next_page_params: null }),
}, },
}); });
...@@ -146,7 +146,7 @@ const TokenPageContent = () => { ...@@ -146,7 +146,7 @@ const TokenPageContent = () => {
scrollRef, scrollRef,
options: { options: {
enabled: Boolean(router.query.hash && router.query.tab === 'inventory' && hasData), enabled: Boolean(router.query.hash && router.query.tab === 'inventory' && hasData),
placeholderData: generateListStub<'token_inventory'>(tokenStubs.TOKEN_INSTANCE), placeholderData: generateListStub<'token_inventory'>(tokenStubs.TOKEN_INSTANCE, 50, { next_page_params: null }),
}, },
}); });
......
import { Box, Icon, Skeleton } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import Page from 'ui/shared/Page/Page'; import type { RoutedTab } from 'ui/shared/Tabs/types';
import TokenInstanceContent from 'ui/tokenInstance/TokenInstanceContent';
import nftIcon from 'icons/nft_shield.svg';
import useApiQuery from 'lib/api/useApiQuery';
import { useAppContext } from 'lib/appContext';
import useIsMobile from 'lib/hooks/useIsMobile';
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import { TOKEN_INSTANCE } from 'stubs/token';
import * as tokenStubs from 'stubs/token';
import { generateListStub } from 'stubs/utils';
import TextAd from 'ui/shared/ad/TextAd';
import AddressHeadingInfo from 'ui/shared/AddressHeadingInfo';
import Tag from 'ui/shared/chakra/Tag';
import LinkExternal from 'ui/shared/LinkExternal';
import PageTitle from 'ui/shared/Page/PageTitle';
import Pagination from 'ui/shared/Pagination';
import type { Props as PaginationProps } from 'ui/shared/Pagination';
import SkeletonTabs from 'ui/shared/skeletons/SkeletonTabs';
import RoutedTabs from 'ui/shared/Tabs/RoutedTabs';
import TokenHolders from 'ui/token/TokenHolders/TokenHolders';
import TokenTransfer from 'ui/token/TokenTransfer/TokenTransfer';
import TokenInstanceDetails from 'ui/tokenInstance/TokenInstanceDetails';
import TokenInstanceMetadata from 'ui/tokenInstance/TokenInstanceMetadata';
export type TokenTabs = 'token_transfers' | 'holders' export type TokenTabs = 'token_transfers' | 'holders'
const TokenInstance = () => { const TokenInstanceContent = () => {
const router = useRouter();
const isMobile = useIsMobile();
const appProps = useAppContext();
const hash = router.query.hash?.toString();
const id = router.query.id?.toString();
const tab = router.query.tab?.toString();
const scrollRef = React.useRef<HTMLDivElement>(null);
const tokenInstanceQuery = useApiQuery('token_instance', {
pathParams: { hash, id },
queryOptions: {
enabled: Boolean(hash && id),
placeholderData: TOKEN_INSTANCE,
},
});
const transfersQuery = useQueryWithPages({
resourceName: 'token_instance_transfers',
pathParams: { hash, id },
scrollRef,
options: {
enabled: Boolean(hash && id && (!tab || tab === 'token_transfers') && !tokenInstanceQuery.isPlaceholderData && tokenInstanceQuery.data),
placeholderData: generateListStub<'token_instance_transfers'>(
tokenInstanceQuery.data?.token.type === 'ERC-1155' ? tokenStubs.TOKEN_TRANSFER_ERC_1155 : tokenStubs.TOKEN_TRANSFER_ERC_721,
10,
{ next_page_params: null },
),
},
});
const shouldFetchHolders = !tokenInstanceQuery.isPlaceholderData && tokenInstanceQuery.data && !tokenInstanceQuery.data.is_unique;
const holdersQuery = useQueryWithPages({
resourceName: 'token_instance_holders',
pathParams: { hash, id },
scrollRef,
options: {
enabled: Boolean(hash && tab === 'holders' && shouldFetchHolders),
placeholderData: generateListStub<'token_instance_holders'>(tokenStubs.TOKEN_HOLDER, 10, { next_page_params: null }),
},
});
const backLink = React.useMemo(() => {
const hasGoBackLink = appProps.referrer && appProps.referrer.includes(`/token/${ hash }`) && !appProps.referrer.includes('instance');
if (!hasGoBackLink) {
return;
}
return {
label: 'Back to token page',
url: appProps.referrer,
};
}, [ appProps.referrer, hash ]);
const tabs: Array<RoutedTab> = [
{
id: 'token_transfers',
title: 'Token transfers',
component: <TokenTransfer transfersQuery={ transfersQuery } tokenId={ id } token={ tokenInstanceQuery.data?.token }/>,
},
shouldFetchHolders ?
{ id: 'holders', title: 'Holders', component: <TokenHolders holdersQuery={ holdersQuery } token={ tokenInstanceQuery.data?.token }/> } :
undefined,
{ id: 'metadata', title: 'Metadata', component: (
<TokenInstanceMetadata
data={ tokenInstanceQuery.data?.metadata }
isPlaceholderData={ tokenInstanceQuery.isPlaceholderData }
/>
) },
].filter(Boolean);
if (tokenInstanceQuery.isError) {
throw Error('Token instance fetch failed', { cause: tokenInstanceQuery.error });
}
const nftShieldIcon = tokenInstanceQuery.isPlaceholderData ?
<Skeleton boxSize={ 6 } display="inline-block" borderRadius="base" mr={ 2 } my={ 2 } verticalAlign="text-bottom"/> :
<Icon as={ nftIcon } boxSize={ 6 } mr={ 2 }/>;
const tokenTag = <Tag isLoading={ tokenInstanceQuery.isPlaceholderData }>{ tokenInstanceQuery.data?.token.type }</Tag>;
const address = {
hash: hash || '',
is_contract: true,
implementation_name: null,
watchlist_names: [],
watchlist_address_id: null,
};
const appLink = (() => {
if (!tokenInstanceQuery.data?.external_app_url) {
return null;
}
try {
const url = new URL(tokenInstanceQuery.data.external_app_url);
return (
<Skeleton isLoaded={ !tokenInstanceQuery.isPlaceholderData } display="inline-block" fontSize="sm" mt={ 6 }>
<span>View in app </span>
<LinkExternal href={ tokenInstanceQuery.data.external_app_url }>
{ url.hostname }
</LinkExternal>
</Skeleton>
);
} catch (error) {
return (
<Box fontSize="sm" mt={ 6 }>
<LinkExternal href={ tokenInstanceQuery.data.external_app_url }>
View in app
</LinkExternal>
</Box>
);
}
})();
let pagination: PaginationProps | undefined;
let isPaginationVisible;
if (tab === 'token_transfers') {
pagination = transfersQuery.pagination;
isPaginationVisible = transfersQuery.isPaginationVisible;
} else if (tab === 'holders') {
pagination = holdersQuery.pagination;
isPaginationVisible = holdersQuery.isPaginationVisible;
}
return ( return (
<Page> <>
<TokenInstanceContent/> <TextAd mb={ 6 }/>
</Page> <PageTitle
title={ `${ tokenInstanceQuery.data?.token.name || 'Unnamed token' } #${ tokenInstanceQuery.data?.id }` }
backLink={ backLink }
beforeTitle={ nftShieldIcon }
contentAfter={ tokenTag }
isLoading={ tokenInstanceQuery.isPlaceholderData }
/>
<AddressHeadingInfo address={ address } token={ tokenInstanceQuery.data?.token } isLoading={ tokenInstanceQuery.isPlaceholderData }/>
{ appLink }
<TokenInstanceDetails data={ tokenInstanceQuery?.data } isLoading={ tokenInstanceQuery.isPlaceholderData } scrollRef={ scrollRef }/>
{ /* should stay before tabs to scroll up with pagination */ }
<Box ref={ scrollRef }></Box>
{ tokenInstanceQuery.isPlaceholderData ? <SkeletonTabs tabs={ tabs }/> : (
<RoutedTabs
tabs={ tabs }
tabListProps={ isMobile ? { mt: 8 } : { mt: 3, py: 5, marginBottom: 0 } }
rightSlot={ !isMobile && isPaginationVisible && pagination ? <Pagination { ...pagination }/> : null }
stickyEnabled={ !isMobile }
/>
) }
{ !tokenInstanceQuery.isLoading && !tokenInstanceQuery.isError && <Box h={{ base: 0, lg: '40vh' }}/> }
</>
); );
}; };
export default TokenInstance; export default React.memo(TokenInstanceContent);
import React from 'react'; import React from 'react';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle'; import PageTitle from 'ui/shared/Page/PageTitle';
import TokensList from 'ui/tokens/Tokens'; import TokensList from 'ui/tokens/Tokens';
const Tokens = () => { const Tokens = () => {
return ( return (
<Page> <>
<PageTitle title="Tokens" withTextAd/> <PageTitle title="Tokens" withTextAd/>
<TokensList/> <TokensList/>
</Page> </>
); );
}; };
......
...@@ -31,12 +31,12 @@ const Transactions = () => { ...@@ -31,12 +31,12 @@ const Transactions = () => {
filters: { filter: router.query.tab === 'pending' ? 'pending' : 'validated' }, filters: { filter: router.query.tab === 'pending' ? 'pending' : 'validated' },
options: { options: {
enabled: !router.query.tab || router.query.tab === 'validated' || router.query.tab === 'pending', enabled: !router.query.tab || router.query.tab === 'validated' || router.query.tab === 'pending',
placeholderData: generateListStub<'txs_validated'>(TX, 50, { placeholderData: generateListStub<'txs_validated'>(TX, 50, { next_page_params: {
block_number: 9005713, block_number: 9005713,
index: 5, index: 5,
items_count: 50, items_count: 50,
filter: 'validated', filter: 'validated',
}), } }),
}, },
}); });
...@@ -44,6 +44,11 @@ const Transactions = () => { ...@@ -44,6 +44,11 @@ const Transactions = () => {
resourceName: 'txs_watchlist', resourceName: 'txs_watchlist',
options: { options: {
enabled: router.query.tab === 'watchlist', enabled: router.query.tab === 'watchlist',
placeholderData: generateListStub<'txs_watchlist'>(TX, 50, { next_page_params: {
block_number: 9005713,
index: 5,
items_count: 50,
} }),
}, },
}); });
...@@ -82,7 +87,12 @@ const Transactions = () => { ...@@ -82,7 +87,12 @@ const Transactions = () => {
<RoutedTabs <RoutedTabs
tabs={ tabs } tabs={ tabs }
tabListProps={ isMobile ? undefined : TAB_LIST_PROPS } tabListProps={ isMobile ? undefined : TAB_LIST_PROPS }
rightSlot={ <TxsTabSlot pagination={ txsQuery.pagination } isPaginationVisible={ txsQuery.isPaginationVisible && !isMobile }/> } rightSlot={ (
<TxsTabSlot
pagination={ router.query.tab === 'watchlist' ? txsWatchlistQuery.pagination : txsQuery.pagination }
isPaginationVisible={ txsQuery.isPaginationVisible && !isMobile }
/>
) }
stickyEnabled={ !isMobile } stickyEnabled={ !isMobile }
/> />
</> </>
......
...@@ -21,10 +21,10 @@ const Withdrawals = () => { ...@@ -21,10 +21,10 @@ const Withdrawals = () => {
const { data, isError, isPlaceholderData, isPaginationVisible, pagination } = useQueryWithPages({ const { data, isError, isPlaceholderData, isPaginationVisible, pagination } = useQueryWithPages({
resourceName: 'withdrawals', resourceName: 'withdrawals',
options: { options: {
placeholderData: generateListStub<'withdrawals'>(WITHDRAWAL, 50, { placeholderData: generateListStub<'withdrawals'>(WITHDRAWAL, 50, { next_page_params: {
index: 5, index: 5,
items_count: 50, items_count: 50,
}), } }),
}, },
}); });
......
...@@ -24,7 +24,7 @@ const BackLink = (props: BackLinkProp & { isLoading?: boolean }) => { ...@@ -24,7 +24,7 @@ const BackLink = (props: BackLinkProp & { isLoading?: boolean }) => {
} }
if (props.isLoading) { if (props.isLoading) {
return <Skeleton boxSize={ 6 } display="inline-block" borderRadius="base" mr={ 3 } isLoaded={ !props.isLoading }/>; return <Skeleton boxSize={ 6 } display="inline-block" borderRadius="base" mr={ 3 } my={ 2 } verticalAlign="text-bottom" isLoaded={ !props.isLoading }/>;
} }
const icon = <Icon as={ eastArrowIcon } boxSize={ 6 } transform="rotate(180deg)" margin="auto"/>; const icon = <Icon as={ eastArrowIcon } boxSize={ 6 } transform="rotate(180deg)" margin="auto"/>;
...@@ -64,7 +64,7 @@ const PageTitle = ({ title, contentAfter, withTextAd, backLink, className, isLoa ...@@ -64,7 +64,7 @@ const PageTitle = ({ title, contentAfter, withTextAd, backLink, className, isLoa
{ beforeTitle } { beforeTitle }
<Skeleton <Skeleton
isLoaded={ !isLoading } isLoaded={ !isLoading }
display="inline" display={ isLoading ? 'inline-block' : 'inline' }
verticalAlign={ isLoading ? 'super' : undefined } verticalAlign={ isLoading ? 'super' : undefined }
> >
<Heading <Heading
......
...@@ -7,6 +7,7 @@ import React, { useEffect, useRef } from 'react'; ...@@ -7,6 +7,7 @@ import React, { useEffect, useRef } from 'react';
import type { RoutedTab } from './types'; import type { RoutedTab } from './types';
import TabsWithScroll from './TabsWithScroll'; import TabsWithScroll from './TabsWithScroll';
import useTabIndexFromQuery from './useTabIndexFromQuery';
interface Props extends ThemingProps<'Tabs'> { interface Props extends ThemingProps<'Tabs'> {
tabs: Array<RoutedTab>; tabs: Array<RoutedTab>;
...@@ -18,16 +19,7 @@ interface Props extends ThemingProps<'Tabs'> { ...@@ -18,16 +19,7 @@ interface Props extends ThemingProps<'Tabs'> {
const RoutedTabs = ({ tabs, tabListProps, rightSlot, stickyEnabled, className, ...themeProps }: Props) => { const RoutedTabs = ({ tabs, tabListProps, rightSlot, stickyEnabled, className, ...themeProps }: Props) => {
const router = useRouter(); const router = useRouter();
const tabIndex = useTabIndexFromQuery(tabs);
let tabIndex = 0;
const tabFromRoute = router.query.tab;
if (tabFromRoute) {
tabIndex = tabs.findIndex(({ id, subTabs }) => id === tabFromRoute || subTabs?.some((id) => id === tabFromRoute));
if (tabIndex < 0) {
tabIndex = 0;
}
}
const tabsRef = useRef<HTMLDivElement>(null); const tabsRef = useRef<HTMLDivElement>(null);
const handleTabChange = React.useCallback((index: number) => { const handleTabChange = React.useCallback((index: number) => {
......
import { useRouter } from 'next/router';
import type { RoutedTab } from './types';
import getQueryParamString from 'lib/router/getQueryParamString';
export default function useTabIndexFromQuery(tabs: Array<RoutedTab>) {
const router = useRouter();
const tabFromQuery = getQueryParamString(router.query.tab);
if (!tabFromQuery) {
return 0;
}
const tabIndex = tabs.findIndex(({ id, subTabs }) => id === tabFromQuery || subTabs?.some((id) => id === tabFromQuery));
if (tabIndex < 0) {
return 0;
}
return tabIndex;
}
import { Flex, Text, chakra } from '@chakra-ui/react'; import { Flex, chakra, Skeleton } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { TokenInfo } from 'types/api/token'; import type { TokenInfo } from 'types/api/token';
...@@ -21,7 +21,11 @@ const TokenSnippet = ({ data, className, logoSize = 6, isDisabled, hideSymbol, i ...@@ -21,7 +21,11 @@ const TokenSnippet = ({ data, className, logoSize = 6, isDisabled, hideSymbol, i
<Flex className={ className } alignItems="center" columnGap={ 2 } w="100%"> <Flex className={ className } alignItems="center" columnGap={ 2 } w="100%">
<TokenLogo boxSize={ logoSize } data={ data } isLoading={ isLoading }/> <TokenLogo boxSize={ logoSize } data={ data } isLoading={ isLoading }/>
<AddressLink hash={ data?.address || '' } alias={ data?.name || 'Unnamed token' } type="token" isDisabled={ isDisabled } isLoading={ isLoading }/> <AddressLink hash={ data?.address || '' } alias={ data?.name || 'Unnamed token' } type="token" isDisabled={ isDisabled } isLoading={ isLoading }/>
{ data?.symbol && !hideSymbol && <Text variant="secondary">({ trimTokenSymbol(data.symbol) })</Text> } { data?.symbol && !hideSymbol && (
<Skeleton isLoaded={ !isLoading } color="text_secondary">
<span>({ trimTokenSymbol(data.symbol) })</span>
</Skeleton>
) }
</Flex> </Flex>
); );
}; };
......
...@@ -55,7 +55,7 @@ const CoinzillaTextAd = ({ className }: {className?: string}) => { ...@@ -55,7 +55,7 @@ const CoinzillaTextAd = ({ className }: {className?: string}) => {
} }
if (isLoading) { if (isLoading) {
return <Skeleton className={ className } h={{ base: 12, lg: 6 }} w="100%" maxW="1000px"/>; return <Skeleton className={ className } h={{ base: 12, lg: 6 }} w={{ base: '100%', lg: 'auto' }} flexGrow={ 1 } maxW="1000px" display="inline-block"/>;
} }
if (!adData) { if (!adData) {
......
import { Box, chakra, Icon, Tooltip } from '@chakra-ui/react'; import { Box, chakra, Icon, Skeleton, Tooltip } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { TokenInfo } from 'types/api/token'; import type { TokenInfo } from 'types/api/token';
...@@ -11,9 +11,10 @@ import { WALLETS_INFO } from 'lib/web3/wallets'; ...@@ -11,9 +11,10 @@ import { WALLETS_INFO } from 'lib/web3/wallets';
interface Props { interface Props {
className?: string; className?: string;
token: TokenInfo; token: TokenInfo;
isLoading?: boolean;
} }
const AddressAddToWallet = ({ className, token }: Props) => { const AddressAddToWallet = ({ className, token, isLoading }: Props) => {
const toast = useToast(); const toast = useToast();
const provider = useProvider(); const provider = useProvider();
...@@ -59,6 +60,10 @@ const AddressAddToWallet = ({ className, token }: Props) => { ...@@ -59,6 +60,10 @@ const AddressAddToWallet = ({ className, token }: Props) => {
return null; return null;
} }
if (isLoading) {
return <Skeleton className={ className } boxSize={ 6 } borderRadius="base"/>;
}
const defaultWallet = appConfig.web3.defaultWallet; const defaultWallet = appConfig.web3.defaultWallet;
return ( return (
......
...@@ -57,7 +57,7 @@ test('base view +@dark-mode', async({ mount, page }) => { ...@@ -57,7 +57,7 @@ test('base view +@dark-mode', async({ mount, page }) => {
test('loading', async({ mount }) => { test('loading', async({ mount }) => {
const component = await mount( const component = await mount(
<TestApp> <TestApp>
<ChartWidget { ...props } isLoading/> <ChartWidget { ...props } isLoading minH="250px"/>
</TestApp>, </TestApp>,
); );
......
...@@ -3,13 +3,13 @@ import { ...@@ -3,13 +3,13 @@ import {
Center, Center,
chakra, chakra,
Flex, Flex,
Grid,
Icon, Icon,
IconButton, Link, IconButton, Link,
Menu, Menu,
MenuButton, MenuButton,
MenuItem, MenuItem,
MenuList, MenuList,
Skeleton,
Text, Text,
Tooltip, Tooltip,
useColorModeValue, useColorModeValue,
...@@ -30,7 +30,6 @@ import { apos } from 'lib/html-entities'; ...@@ -30,7 +30,6 @@ import { apos } from 'lib/html-entities';
import saveAsCSV from 'lib/saveAsCSV'; import saveAsCSV from 'lib/saveAsCSV';
import ChartWidgetGraph from './ChartWidgetGraph'; import ChartWidgetGraph from './ChartWidgetGraph';
import ChartWidgetSkeleton from './ChartWidgetSkeleton';
import FullscreenChartModal from './FullscreenChartModal'; import FullscreenChartModal from './FullscreenChartModal';
export type Props = { export type Props = {
...@@ -110,10 +109,6 @@ const ChartWidget = ({ items, title, description, isLoading, className, isError, ...@@ -110,10 +109,6 @@ const ChartWidget = ({ items, title, description, isLoading, className, isError,
} }
}, [ items, title ]); }, [ items, title ]);
if (isLoading) {
return <ChartWidgetSkeleton hasDescription={ Boolean(description) }/>;
}
const hasItems = items && items.length > 2; const hasItems = items && items.length > 2;
const content = (() => { const content = (() => {
...@@ -137,6 +132,10 @@ const ChartWidget = ({ items, title, description, isLoading, className, isError, ...@@ -137,6 +132,10 @@ const ChartWidget = ({ items, title, description, isLoading, className, isError,
); );
} }
if (isLoading) {
return <Skeleton flexGrow={ 1 } w="100%"/>;
}
if (!hasItems) { if (!hasItems) {
return ( return (
<Center flexGrow={ 1 }> <Center flexGrow={ 1 }>
...@@ -146,7 +145,7 @@ const ChartWidget = ({ items, title, description, isLoading, className, isError, ...@@ -146,7 +145,7 @@ const ChartWidget = ({ items, title, description, isLoading, className, isError,
} }
return ( return (
<Box h="100%" maxW="100%"> <Box flexGrow={ 1 } maxW="100%">
<ChartWidgetGraph <ChartWidgetGraph
items={ items } items={ items }
onZoom={ handleZoom } onZoom={ handleZoom }
...@@ -160,112 +159,104 @@ const ChartWidget = ({ items, title, description, isLoading, className, isError, ...@@ -160,112 +159,104 @@ const ChartWidget = ({ items, title, description, isLoading, className, isError,
return ( return (
<> <>
<Box <Flex
height="100%" height="100%"
display="flex"
flexDirection="column"
ref={ ref } ref={ ref }
flexDir="column"
padding={{ base: 3, lg: 4 }} padding={{ base: 3, lg: 4 }}
borderRadius="md" borderRadius="md"
border="1px" border="1px"
borderColor={ borderColor } borderColor={ borderColor }
className={ className } className={ className }
> >
<Grid <Flex columnGap={ 6 } mb={ 1 } alignItems="flex-start">
gridTemplateColumns="auto auto 36px" <Flex flexGrow={ 1 } flexDir="column" alignItems="flex-start">
gridColumnGap={ 2 } <Skeleton
> isLoaded={ !isLoading }
<Text fontWeight={ 600 }
fontWeight={ 600 } size={{ base: 'xs', lg: 'sm' }}
fontSize="md"
lineHeight={ 6 }
as="p"
size={{ base: 'xs', lg: 'sm' }}
>
{ title }
</Text>
{ description && (
<Text
mb={ 1 }
gridColumn={ 1 }
as="p"
variant="secondary"
fontSize="xs"
> >
{ description } { title }
</Text> </Skeleton>
) }
<Tooltip label="Reset zoom">
<IconButton
hidden={ isZoomResetInitial }
aria-label="Reset zoom"
colorScheme="blue"
w={ 9 }
h={ 8 }
gridColumn={ 2 }
justifySelf="end"
alignSelf="top"
gridRow="1/3"
size="sm"
variant="outline"
onClick={ handleZoomResetClick }
icon={ <Icon as={ repeatArrowIcon } w={ 4 } h={ 4 }/> }
/>
</Tooltip>
{ hasItems && ( { description && (
<Menu> <Skeleton
<MenuButton isLoaded={ !isLoading }
gridColumn={ 3 } color="text_secondary"
gridRow="1/3" fontSize="xs"
justifySelf="end" mt={ 1 }
w="36px"
h="32px"
icon={ <Icon as={ dotsIcon } w={ 4 } h={ 4 }/> }
colorScheme="gray"
variant="ghost"
as={ IconButton }
> >
<VisuallyHidden> <span>{ description }</span>
Open chart options menu </Skeleton>
</VisuallyHidden> ) }
</MenuButton> </Flex>
<MenuList>
<MenuItem <Flex ml="auto" columnGap={ 2 }>
display="flex" <Tooltip label="Reset zoom">
alignItems="center" <IconButton
onClick={ showChartFullscreen } hidden={ isZoomResetInitial }
> aria-label="Reset zoom"
<Icon as={ scopeIcon } boxSize={ 5 } mr={ 3 }/> colorScheme="blue"
w={ 9 }
h={ 8 }
size="sm"
variant="outline"
onClick={ handleZoomResetClick }
icon={ <Icon as={ repeatArrowIcon } w={ 4 } h={ 4 }/> }
/>
</Tooltip>
{ hasItems && (
<Menu>
<Skeleton isLoaded={ !isLoading } borderRadius="base">
<MenuButton
w="36px"
h="32px"
icon={ <Icon as={ dotsIcon } w={ 4 } h={ 4 }/> }
colorScheme="gray"
variant="ghost"
as={ IconButton }
>
<VisuallyHidden>
Open chart options menu
</VisuallyHidden>
</MenuButton>
</Skeleton>
<MenuList>
<MenuItem
display="flex"
alignItems="center"
onClick={ showChartFullscreen }
>
<Icon as={ scopeIcon } boxSize={ 5 } mr={ 3 }/>
View fullscreen View fullscreen
</MenuItem> </MenuItem>
<MenuItem <MenuItem
display="flex" display="flex"
alignItems="center" alignItems="center"
onClick={ handleFileSaveClick } onClick={ handleFileSaveClick }
> >
<Icon as={ imageIcon } boxSize={ 5 } mr={ 3 }/> <Icon as={ imageIcon } boxSize={ 5 } mr={ 3 }/>
Save as PNG Save as PNG
</MenuItem> </MenuItem>
<MenuItem <MenuItem
display="flex" display="flex"
alignItems="center" alignItems="center"
onClick={ handleSVGSavingClick } onClick={ handleSVGSavingClick }
> >
<Icon as={ svgFileIcon } boxSize={ 5 } mr={ 3 }/> <Icon as={ svgFileIcon } boxSize={ 5 } mr={ 3 }/>
Save as CSV Save as CSV
</MenuItem> </MenuItem>
</MenuList> </MenuList>
</Menu> </Menu>
) } ) }
</Grid> </Flex>
</Flex>
{ content } { content }
</Box> </Flex>
{ hasItems && ( { hasItems && (
<FullscreenChartModal <FullscreenChartModal
......
import { Box, Skeleton } from '@chakra-ui/react';
import React from 'react';
interface Props {
hasDescription: boolean;
}
const ChartWidgetSkeleton = ({ hasDescription }: Props) => {
return (
<Box
height="235px"
paddingY={{ base: 3, lg: 4 }}
>
<Skeleton w="75%" h="24px"/>
{ hasDescription && <Skeleton w="50%" h="18px" mt={ 1 }/> }
<Skeleton w="100%" h="150px" mt={ 5 }/>
</Box>
);
};
export default ChartWidgetSkeleton;
import { Flex, Skeleton, chakra } from '@chakra-ui/react'; import { Flex, Skeleton, chakra, Box, useColorModeValue } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { RoutedTab } from '../Tabs/types'; import type { RoutedTab } from '../Tabs/types';
import useTabIndexFromQuery from 'ui/shared/Tabs/useTabIndexFromQuery';
interface Props { interface Props {
className?: string; className?: string;
tabs?: Array<RoutedTab>; tabs?: Array<RoutedTab>;
...@@ -10,6 +12,9 @@ interface Props { ...@@ -10,6 +12,9 @@ interface Props {
} }
const SkeletonTabs = ({ className, tabs, size = 'md' }: Props) => { const SkeletonTabs = ({ className, tabs, size = 'md' }: Props) => {
const bgColor = useColorModeValue('blackAlpha.50', 'whiteAlpha.50');
const tabIndex = useTabIndexFromQuery(tabs || []);
if (tabs) { if (tabs) {
if (tabs.length === 1) { if (tabs.length === 1) {
return null; return null;
...@@ -19,16 +24,34 @@ const SkeletonTabs = ({ className, tabs, size = 'md' }: Props) => { ...@@ -19,16 +24,34 @@ const SkeletonTabs = ({ className, tabs, size = 'md' }: Props) => {
const paddingVert = size === 'sm' ? 1 : 2; const paddingVert = size === 'sm' ? 1 : 2;
return ( return (
<Flex className={ className } my={ 8 } alignItems="center"> <Flex className={ className } my={ 8 } alignItems="center" overflow="hidden">
{ tabs.map(({ title, id }, index) => ( { tabs.slice(0, tabIndex).map(({ title, id }) => (
<Skeleton
key={ id }
mx={ paddingHor }
borderRadius="base"
fontWeight={ 600 }
borderWidth={ size === 'sm' ? '2px' : 0 }
flexShrink={ 0 }
>
{ typeof title === 'string' ? title : title() }
</Skeleton>
)) }
{ tabs.slice(tabIndex, tabIndex + 1).map(({ title, id }) => (
<Box key={ id } bgColor={ bgColor } px={ paddingHor } py={ paddingVert } borderRadius="base" flexShrink={ 0 }>
<Skeleton borderRadius="base" borderWidth={ size === 'sm' ? '2px' : 0 }>
{ typeof title === 'string' ? title : title() }
</Skeleton>
</Box>
)) }
{ tabs.slice(tabIndex + 1).map(({ title, id }) => (
<Skeleton <Skeleton
key={ id } key={ id }
py={ index === 0 ? paddingVert : 0 } mx={ paddingHor }
px={ index === 0 ? paddingHor : 0 }
mx={ index === 0 ? 0 : paddingHor }
borderRadius="base" borderRadius="base"
fontWeight={ 600 } fontWeight={ 600 }
borderWidth={ size === 'sm' ? '2px' : 0 } borderWidth={ size === 'sm' ? '2px' : 0 }
flexShrink={ 0 }
> >
{ typeof title === 'string' ? title : title() } { typeof title === 'string' ? title : title() }
</Skeleton> </Skeleton>
......
...@@ -14,13 +14,14 @@ type Props = { ...@@ -14,13 +14,14 @@ type Props = {
units?: string; units?: string;
interval: StatsIntervalIds; interval: StatsIntervalIds;
onLoadingError: () => void; onLoadingError: () => void;
isPlaceholderData: boolean;
} }
function formatDate(date: Date) { function formatDate(date: Date) {
return date.toISOString().substring(0, 10); return date.toISOString().substring(0, 10);
} }
const ChartWidgetContainer = ({ id, title, description, interval, onLoadingError, units }: Props) => { const ChartWidgetContainer = ({ id, title, description, interval, onLoadingError, units, isPlaceholderData }: Props) => {
const selectedInterval = STATS_INTERVALS[interval]; const selectedInterval = STATS_INTERVALS[interval];
const endDate = selectedInterval.start ? formatDate(new Date()) : undefined; const endDate = selectedInterval.start ? formatDate(new Date()) : undefined;
...@@ -32,6 +33,10 @@ const ChartWidgetContainer = ({ id, title, description, interval, onLoadingError ...@@ -32,6 +33,10 @@ const ChartWidgetContainer = ({ id, title, description, interval, onLoadingError
from: startDate, from: startDate,
to: endDate, to: endDate,
}, },
queryOptions: {
enabled: !isPlaceholderData,
refetchOnMount: false,
},
}); });
const items = useMemo(() => data?.chart?.map((item) => { const items = useMemo(() => data?.chart?.map((item) => {
...@@ -52,6 +57,7 @@ const ChartWidgetContainer = ({ id, title, description, interval, onLoadingError ...@@ -52,6 +57,7 @@ const ChartWidgetContainer = ({ id, title, description, interval, onLoadingError
units={ units } units={ units }
description={ description } description={ description }
isLoading={ isLoading } isLoading={ isLoading }
minH="230px"
/> />
); );
}; };
......
import { Box, Grid, GridItem, Heading, List, ListItem, Skeleton } from '@chakra-ui/react'; import { Box, Grid, Heading, List, ListItem, Skeleton } from '@chakra-ui/react';
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
import type { StatsChartsSection } from 'types/api/stats'; import type { StatsChartsSection } from 'types/api/stats';
import type { StatsIntervalIds } from 'types/client/stats'; import type { StatsIntervalIds } from 'types/client/stats';
import { apos } from 'lib/html-entities'; import { apos } from 'lib/html-entities';
import ChartWidgetSkeleton from 'ui/shared/chart/ChartWidgetSkeleton';
import EmptySearchResult from 'ui/shared/EmptySearchResult'; import EmptySearchResult from 'ui/shared/EmptySearchResult';
import ChartsLoadingErrorAlert from './ChartsLoadingErrorAlert'; import ChartsLoadingErrorAlert from './ChartsLoadingErrorAlert';
...@@ -14,14 +13,12 @@ import ChartWidgetContainer from './ChartWidgetContainer'; ...@@ -14,14 +13,12 @@ import ChartWidgetContainer from './ChartWidgetContainer';
type Props = { type Props = {
filterQuery: string; filterQuery: string;
isError: boolean; isError: boolean;
isLoading: boolean; isPlaceholderData: boolean;
charts?: Array<StatsChartsSection>; charts?: Array<StatsChartsSection>;
interval: StatsIntervalIds; interval: StatsIntervalIds;
} }
const skeletonsCount = 4; const ChartsWidgetsList = ({ filterQuery, isError, isPlaceholderData, charts, interval }: Props) => {
const ChartsWidgetsList = ({ filterQuery, isError, isLoading, charts, interval }: Props) => {
const [ isSomeChartLoadingError, setIsSomeChartLoadingError ] = useState(false); const [ isSomeChartLoadingError, setIsSomeChartLoadingError ] = useState(false);
const isAnyChartDisplayed = charts?.some((section) => section.charts.length > 0); const isAnyChartDisplayed = charts?.some((section) => section.charts.length > 0);
const isEmptyChartList = Boolean(filterQuery) && !isAnyChartDisplayed; const isEmptyChartList = Boolean(filterQuery) && !isAnyChartDisplayed;
...@@ -30,29 +27,6 @@ const ChartsWidgetsList = ({ filterQuery, isError, isLoading, charts, interval } ...@@ -30,29 +27,6 @@ const ChartsWidgetsList = ({ filterQuery, isError, isLoading, charts, interval }
() => setIsSomeChartLoadingError(true), () => setIsSomeChartLoadingError(true),
[ setIsSomeChartLoadingError ]); [ setIsSomeChartLoadingError ]);
const skeletonElement = [ ...Array(skeletonsCount) ]
.map((e, i) => (
<GridItem key={ i }>
<ChartWidgetSkeleton hasDescription={ true }/>
</GridItem>
));
if (isLoading) {
return (
<>
<Skeleton w="30%" h="32px" mb={ 4 }/>
<Grid
templateColumns={{
lg: 'repeat(2, minmax(0, 1fr))',
}}
gap={ 4 }
>
{ skeletonElement }
</Grid>
</>
);
}
if (isError) { if (isError) {
return <ChartsLoadingErrorAlert/>; return <ChartsLoadingErrorAlert/>;
} }
...@@ -77,32 +51,27 @@ const ChartsWidgetsList = ({ filterQuery, isError, isLoading, charts, interval } ...@@ -77,32 +51,27 @@ const ChartsWidgetsList = ({ filterQuery, isError, isLoading, charts, interval }
marginBottom: 0, marginBottom: 0,
}} }}
> >
<Heading <Skeleton isLoaded={ !isPlaceholderData } mb={ 4 } display="inline-block">
size="md" <Heading size="md" >
mb={ 4 } { section.title }
> </Heading>
{ section.title } </Skeleton>
</Heading>
<Grid <Grid
templateColumns={{ templateColumns={{ lg: 'repeat(2, minmax(0, 1fr))' }}
lg: 'repeat(2, minmax(0, 1fr))',
}}
gap={ 4 } gap={ 4 }
> >
{ section.charts.map((chart) => ( { section.charts.map((chart) => (
<GridItem <ChartWidgetContainer
key={ chart.id } key={ chart.id }
> id={ chart.id }
<ChartWidgetContainer title={ chart.title }
id={ chart.id } description={ chart.description }
title={ chart.title } interval={ interval }
description={ chart.description } units={ chart.units || undefined }
interval={ interval } isPlaceholderData={ isPlaceholderData }
units={ chart.units || undefined } onLoadingError={ handleChartLoadingError }
onLoadingError={ handleChartLoadingError } />
/>
</GridItem>
)) } )) }
</Grid> </Grid>
</ListItem> </ListItem>
......
import debounce from 'lodash/debounce'; import React, { useCallback, useMemo, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import type { StatsChartInfo, StatsChartsSection } from 'types/api/stats'; import type { StatsChartInfo, StatsChartsSection } from 'types/api/stats';
import type { StatsIntervalIds } from 'types/client/stats'; import type { StatsIntervalIds } from 'types/client/stats';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import useDebounce from 'lib/hooks/useDebounce';
import { STATS_CHARTS } from 'stubs/stats';
function isSectionMatches(section: StatsChartsSection, currentSection: string): boolean { function isSectionMatches(section: StatsChartsSection, currentSection: string): boolean {
return currentSection === 'all' || section.id === currentSection; return currentSection === 'all' || section.id === currentSection;
...@@ -15,30 +16,30 @@ function isChartNameMatches(q: string, chart: StatsChartInfo) { ...@@ -15,30 +16,30 @@ function isChartNameMatches(q: string, chart: StatsChartInfo) {
} }
export default function useStats() { export default function useStats() {
const { data, isLoading, isError } = useApiQuery('stats_lines'); const { data, isPlaceholderData, isError } = useApiQuery('stats_lines', {
queryOptions: {
placeholderData: STATS_CHARTS,
},
});
const [ currentSection, setCurrentSection ] = useState('all'); const [ currentSection, setCurrentSection ] = useState('all');
const [ filterQuery, setFilterQuery ] = useState(''); const [ filterQuery, setFilterQuery ] = useState('');
const [ displayedCharts, setDisplayedCharts ] = useState(data?.sections);
const [ interval, setInterval ] = useState<StatsIntervalIds>('oneMonth'); const [ interval, setInterval ] = useState<StatsIntervalIds>('oneMonth');
const sectionIds = useMemo(() => data?.sections?.map(({ id }) => id), [ data ]); const sectionIds = useMemo(() => data?.sections?.map(({ id }) => id), [ data ]);
// eslint-disable-next-line react-hooks/exhaustive-deps const debouncedFilterQuery = useDebounce(filterQuery, 500);
const debounceFilterCharts = useCallback(debounce(q => setFilterQuery(q), 500), []);
const filterCharts = useCallback((q: string, currentSection: string) => { const displayedCharts = React.useMemo(() => {
const charts = data?.sections return data?.sections
?.map((section) => { ?.map((section) => {
const charts = section.charts.filter((chart) => isSectionMatches(section, currentSection) && isChartNameMatches(q, chart)); const charts = section.charts.filter((chart) => isSectionMatches(section, currentSection) && isChartNameMatches(debouncedFilterQuery, chart));
return { return {
...section, ...section,
charts, charts,
}; };
}).filter((section) => section.charts.length > 0); }).filter((section) => section.charts.length > 0);
}, [ currentSection, data?.sections, debouncedFilterQuery ]);
setDisplayedCharts(charts || []);
}, [ data ]);
const handleSectionChange = useCallback((newSection: string) => { const handleSectionChange = useCallback((newSection: string) => {
setCurrentSection(newSection); setCurrentSection(newSection);
...@@ -48,33 +49,33 @@ export default function useStats() { ...@@ -48,33 +49,33 @@ export default function useStats() {
setInterval(newInterval); setInterval(newInterval);
}, []); }, []);
useEffect(() => { const handleFilterChange = useCallback((q: string) => {
filterCharts(filterQuery, currentSection); setFilterQuery(q);
}, [ filterQuery, currentSection, filterCharts ]); }, []);
return React.useMemo(() => ({ return React.useMemo(() => ({
sections: data?.sections, sections: data?.sections,
sectionIds, sectionIds,
isLoading, isPlaceholderData,
isError, isError,
filterQuery, filterQuery,
currentSection, currentSection,
handleSectionChange, handleSectionChange,
interval, interval,
handleIntervalChange, handleIntervalChange,
debounceFilterCharts, handleFilterChange,
displayedCharts, displayedCharts,
}), [ }), [
data, data,
sectionIds, sectionIds,
isLoading, isPlaceholderData,
isError, isError,
filterQuery, filterQuery,
currentSection, currentSection,
handleSectionChange, handleSectionChange,
interval, interval,
handleIntervalChange, handleIntervalChange,
debounceFilterCharts, handleFilterChange,
displayedCharts, displayedCharts,
]); ]);
} }
...@@ -87,8 +87,11 @@ const TokenDetails = ({ tokenQuery }: Props) => { ...@@ -87,8 +87,11 @@ const TokenDetails = ({ tokenQuery }: Props) => {
title="Price" title="Price"
hint="Price per token on the exchanges" hint="Price per token on the exchanges"
alignSelf="center" alignSelf="center"
isLoading={ tokenQuery.isPlaceholderData }
> >
{ `$${ exchangeRate }` } <Skeleton isLoaded={ !tokenQuery.isPlaceholderData } display="inline-block">
<span>{ `$${ exchangeRate }` }</span>
</Skeleton>
</DetailsInfoItem> </DetailsInfoItem>
) } ) }
{ marketcap && ( { marketcap && (
...@@ -96,8 +99,11 @@ const TokenDetails = ({ tokenQuery }: Props) => { ...@@ -96,8 +99,11 @@ const TokenDetails = ({ tokenQuery }: Props) => {
title="Fully diluted market cap" title="Fully diluted market cap"
hint="Total supply * Price" hint="Total supply * Price"
alignSelf="center" alignSelf="center"
isLoading={ tokenQuery.isPlaceholderData }
> >
{ `$${ marketcap }` } <Skeleton isLoaded={ !tokenQuery.isPlaceholderData } display="inline-block">
<span>{ `$${ marketcap }` }</span>
</Skeleton>
</DetailsInfoItem> </DetailsInfoItem>
) } ) }
<DetailsInfoItem <DetailsInfoItem
......
import { Flex, Link, Icon } from '@chakra-ui/react';
import React from 'react';
import type { TokenVerifiedInfo } from 'types/api/token';
import iconDocs from 'icons/docs.svg';
import iconEmail from 'icons/email.svg';
import iconLink from 'icons/link.svg';
import iconCoinGecko from 'icons/social/coingecko.svg';
import iconCoinMarketCap from 'icons/social/coinmarketcap.svg';
import iconDefiLlama from 'icons/social/defi_llama.svg';
import iconDiscord from 'icons/social/discord_filled.svg';
import iconFacebook from 'icons/social/facebook_filled.svg';
import iconGithub from 'icons/social/github_filled.svg';
import iconLinkedIn from 'icons/social/linkedin_filled.svg';
import iconMedium from 'icons/social/medium_filled.svg';
import iconOpenSea from 'icons/social/opensea_filled.svg';
import iconReddit from 'icons/social/reddit_filled.svg';
import iconSlack from 'icons/social/slack_filled.svg';
import iconTelegram from 'icons/social/telegram_filled.svg';
import iconTwitter from 'icons/social/twitter_filled.svg';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import LinkExternal from 'ui/shared/LinkExternal';
import TextSeparator from 'ui/shared/TextSeparator';
interface Props {
data: TokenVerifiedInfo;
}
interface TServiceLink {
field: keyof TokenVerifiedInfo;
icon: React.FunctionComponent<React.SVGAttributes<SVGElement>>;
hint: string;
}
const SOCIAL_LINKS: Array<TServiceLink> = [
{ field: 'github', icon: iconGithub, hint: 'Github account' },
{ field: 'twitter', icon: iconTwitter, hint: 'Twitter account' },
{ field: 'telegram', icon: iconTelegram, hint: 'Telegram account' },
{ field: 'openSea', icon: iconOpenSea, hint: 'OpenSea page' },
{ field: 'linkedin', icon: iconLinkedIn, hint: 'LinkedIn page' },
{ field: 'facebook', icon: iconFacebook, hint: 'Facebook account' },
{ field: 'discord', icon: iconDiscord, hint: 'Discord account' },
{ field: 'medium', icon: iconMedium, hint: 'Medium account' },
{ field: 'slack', icon: iconSlack, hint: 'Slack account' },
{ field: 'reddit', icon: iconReddit, hint: 'Reddit account' },
];
const PRICE_TICKERS: Array<TServiceLink> = [
{ field: 'coinGeckoTicker', icon: iconCoinGecko, hint: 'Coin Gecko' },
{ field: 'coinMarketCapTicker', icon: iconCoinMarketCap, hint: 'Coin Market Cap' },
{ field: 'defiLlamaTicker', icon: iconDefiLlama, hint: 'Defi Llama' },
];
const ServiceLink = ({ href, hint, icon }: TServiceLink & { href: string | undefined }) => (
<Link
href={ href }
variant="secondary"
boxSize={ 5 }
aria-label={ hint }
title={ hint }
target="_blank"
>
<Icon as={ icon } boxSize={ 5 }/>
</Link>
);
// todo_tom DELETE ME
const TokenDetailsVerifiedInfo = ({ data }: Props) => {
const websiteLink = (() => {
try {
const url = new URL(data.projectWebsite);
return (
<Flex alignItems="center" columnGap={ 1 } color="text_secondary" _hover={{ color: 'link_hovered' }}>
<Icon as={ iconLink } boxSize={ 6 }/>
<LinkExternal href={ data.projectWebsite } fontSize="md">{ url.host }</LinkExternal>
</Flex>
);
} catch (error) {
return null;
}
})();
const docsLink = (() => {
if (!data.docs) {
return null;
}
return (
<Flex alignItems="center" columnGap={ 1 } color="text_secondary" _hover={{ color: 'link_hovered' }}>
<Icon as={ iconDocs } boxSize={ 6 }/>
<LinkExternal href={ data.docs } fontSize="md">Documentation</LinkExternal>
</Flex>
);
})();
const supportLink = (() => {
if (!data.support) {
return null;
}
const isEmail = data.support.includes('@');
const href = isEmail ? `mailto:${ data.support }` : data.support;
return (
<Flex alignItems="center" columnGap={ 1 } color="text_secondary" _hover={{ color: 'link_hovered' }}>
<Icon as={ iconEmail } boxSize={ 6 }/>
<Link href={ href } target="_blank">
{ data.support }
</Link>
</Flex>
);
})();
const socialLinks = SOCIAL_LINKS
.map((link) => ({ ...link, href: data[link.field] }))
.filter(({ href }) => href);
const priceTickersLinks = PRICE_TICKERS
.map((link) => ({ ...link, href: data[link.field] }))
.filter(({ href }) => href);
return (
<DetailsInfoItem
title="Links"
hint="Links to the project's official website and social media channels."
>
<Flex flexDir="column" rowGap={ 5 }>
<Flex
flexDir={{ base: 'column', lg: 'row' }}
alignItems={{ base: 'flex-start', lg: 'center' }}
columnGap={ 6 }
rowGap={ 2 }
>
{ websiteLink }
{ docsLink }
{ supportLink }
</Flex>
{ (socialLinks.length > 0 || priceTickersLinks.length > 0) && (
<Flex
columnGap={ 2 }
rowGap={ 2 }
flexWrap="wrap"
>
{ socialLinks.map((link) => <ServiceLink key={ link.field } { ...link }/>) }
{ priceTickersLinks.length > 0 && (
<>
<TextSeparator color="divider"/>
{ priceTickersLinks.map((link) => <ServiceLink key={ link.field } { ...link }/>) }
</>
) }
</Flex>
) }
</Flex>
</DetailsInfoItem>
);
};
export default React.memo(TokenDetailsVerifiedInfo);
...@@ -31,7 +31,7 @@ type Props = { ...@@ -31,7 +31,7 @@ type Props = {
const TokenTransfer = ({ transfersQuery, tokenId, token }: Props) => { const TokenTransfer = ({ transfersQuery, tokenId, token }: Props) => {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const router = useRouter(); const router = useRouter();
const { isError, isLoading, isPlaceholderData, data, pagination, isPaginationVisible } = transfersQuery; const { isError, 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('');
...@@ -52,7 +52,7 @@ const TokenTransfer = ({ transfersQuery, tokenId, token }: Props) => { ...@@ -52,7 +52,7 @@ const TokenTransfer = ({ transfersQuery, tokenId, token }: Props) => {
topic: `tokens:${ router.query.hash?.toString().toLowerCase() }`, topic: `tokens:${ router.query.hash?.toString().toLowerCase() }`,
onSocketClose: handleSocketClose, onSocketClose: handleSocketClose,
onSocketError: handleSocketError, onSocketError: handleSocketError,
isDisabled: isLoading || isError || pagination.page !== 1, isDisabled: isPlaceholderData || isError || pagination.page !== 1,
}); });
useSocketMessage({ useSocketMessage({
channel, channel,
...@@ -99,7 +99,7 @@ const TokenTransfer = ({ transfersQuery, tokenId, token }: Props) => { ...@@ -99,7 +99,7 @@ const TokenTransfer = ({ transfersQuery, tokenId, token }: Props) => {
return ( return (
<DataListDisplay <DataListDisplay
isError={ isError } isError={ isError }
isLoading={ !isPlaceholderData && isLoading } isLoading={ false }
items={ data?.items } items={ data?.items }
skeletonProps={{ skeletonProps={{
isLongSkeleton: true, isLongSkeleton: true,
......
...@@ -89,7 +89,7 @@ const TokenTransferListItem = ({ ...@@ -89,7 +89,7 @@ const TokenTransferListItem = ({
<Skeleton isLoaded={ !isLoading } flexShrink={ 0 } fontWeight={ 500 }> <Skeleton isLoaded={ !isLoading } flexShrink={ 0 } fontWeight={ 500 }>
Value Value
</Skeleton> </Skeleton>
<Skeleton isLoaded={ !isLoading } variant="secondary"> <Skeleton isLoaded={ !isLoading } color="text_secondary">
{ value } { value }
</Skeleton> </Skeleton>
<Skeleton isLoaded={ !isLoading }>{ trimTokenSymbol(token.symbol) }</Skeleton> <Skeleton isLoaded={ !isLoading }>{ trimTokenSymbol(token.symbol) }</Skeleton>
......
import { Box, Tag, Icon } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';
import type { RoutedTab } from 'ui/shared/Tabs/types';
import nftIcon from 'icons/nft_shield.svg';
import useApiQuery from 'lib/api/useApiQuery';
import { useAppContext } from 'lib/appContext';
import useIsMobile from 'lib/hooks/useIsMobile';
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import TextAd from 'ui/shared/ad/TextAd';
import AddressHeadingInfo from 'ui/shared/AddressHeadingInfo';
import LinkExternal from 'ui/shared/LinkExternal';
import PageTitle from 'ui/shared/Page/PageTitle';
import Pagination from 'ui/shared/Pagination';
import type { Props as PaginationProps } from 'ui/shared/Pagination';
import SkeletonTabs from 'ui/shared/skeletons/SkeletonTabs';
import RoutedTabs from 'ui/shared/Tabs/RoutedTabs';
import TokenHolders from 'ui/token/TokenHolders/TokenHolders';
import TokenTransfer from 'ui/token/TokenTransfer/TokenTransfer';
import TokenInstanceDetails from './TokenInstanceDetails';
import TokenInstanceMetadata from './TokenInstanceMetadata';
import TokenInstanceSkeleton from './TokenInstanceSkeleton';
export type TokenTabs = 'token_transfers' | 'holders'
const TokenInstanceContent = () => {
const router = useRouter();
const isMobile = useIsMobile();
const appProps = useAppContext();
const hash = router.query.hash?.toString();
const id = router.query.id?.toString();
const tab = router.query.tab?.toString();
const scrollRef = React.useRef<HTMLDivElement>(null);
const tokenInstanceQuery = useApiQuery('token_instance', {
pathParams: { hash, id },
queryOptions: { enabled: Boolean(hash && id) },
});
const transfersQuery = useQueryWithPages({
resourceName: 'token_instance_transfers',
pathParams: { hash, id },
scrollRef,
options: {
enabled: Boolean(hash && (!tab || tab === 'token_transfers') && tokenInstanceQuery.data),
},
});
const shouldFetchHolders = tokenInstanceQuery.data && !tokenInstanceQuery.data.is_unique;
const holdersQuery = useQueryWithPages({
resourceName: 'token_instance_holders',
pathParams: { hash, id },
scrollRef,
options: {
enabled: Boolean(hash && (!tab || tab === 'holders') && shouldFetchHolders),
},
});
const backLink = React.useMemo(() => {
const hasGoBackLink = appProps.referrer && appProps.referrer.includes(`/token/${ hash }`) && !appProps.referrer.includes('instance');
if (!hasGoBackLink) {
return;
}
return {
label: 'Back to token page',
url: appProps.referrer,
};
}, [ appProps.referrer, hash ]);
const tabs: Array<RoutedTab> = [
{
id: 'token_transfers',
title: 'Token transfers',
component: <TokenTransfer transfersQuery={ transfersQuery } tokenId={ id } token={ tokenInstanceQuery.data?.token }/>,
},
shouldFetchHolders ?
{ id: 'holders', title: 'Holders', component: <TokenHolders holdersQuery={ holdersQuery } token={ tokenInstanceQuery.data?.token }/> } :
undefined,
{ id: 'metadata', title: 'Metadata', component: <TokenInstanceMetadata data={ tokenInstanceQuery.data?.metadata }/> },
].filter(Boolean);
if (tokenInstanceQuery.isError) {
throw Error('Token instance fetch failed', { cause: tokenInstanceQuery.error });
}
if (tokenInstanceQuery.isLoading) {
return <TokenInstanceSkeleton/>;
}
const nftShieldIcon = <Icon as={ nftIcon } boxSize={ 6 } mr={ 2 }/>;
const tokenTag = <Tag>{ tokenInstanceQuery.data.token.type }</Tag>;
const address = {
hash: hash || '',
is_contract: true,
implementation_name: null,
watchlist_names: [],
watchlist_address_id: null,
};
const appLink = (() => {
if (!tokenInstanceQuery.data.external_app_url) {
return null;
}
try {
const url = new URL(tokenInstanceQuery.data.external_app_url);
return (
<Box fontSize="sm" mt={ 6 }>
<span>View in app </span>
<LinkExternal href={ tokenInstanceQuery.data.external_app_url }>
{ url.hostname }
</LinkExternal>
</Box>
);
} catch (error) {
return (
<Box fontSize="sm" mt={ 6 }>
<LinkExternal href={ tokenInstanceQuery.data.external_app_url }>
View in app
</LinkExternal>
</Box>
);
}
})();
let pagination: PaginationProps | undefined;
let isPaginationVisible;
if (tab === 'token_transfers') {
pagination = transfersQuery.pagination;
isPaginationVisible = transfersQuery.isPaginationVisible;
} else if (tab === 'holders') {
pagination = holdersQuery.pagination;
isPaginationVisible = holdersQuery.isPaginationVisible;
}
return (
<>
<TextAd mb={ 6 }/>
<PageTitle
title={ `${ tokenInstanceQuery.data.token.name || 'Unnamed token' } #${ tokenInstanceQuery.data.id }` }
backLink={ backLink }
beforeTitle={ nftShieldIcon }
contentAfter={ tokenTag }
/>
<AddressHeadingInfo address={ address } token={ tokenInstanceQuery.data.token }/>
{ appLink }
<TokenInstanceDetails data={ tokenInstanceQuery.data } scrollRef={ scrollRef }/>
{ /* should stay before tabs to scroll up with pagination */ }
<Box ref={ scrollRef }></Box>
{ tokenInstanceQuery.isLoading ? <SkeletonTabs/> : (
<RoutedTabs
tabs={ tabs }
tabListProps={ isMobile ? { mt: 8 } : { mt: 3, py: 5, marginBottom: 0 } }
rightSlot={ !isMobile && isPaginationVisible && pagination ? <Pagination { ...pagination }/> : null }
stickyEnabled={ !isMobile }
/>
) }
{ !tokenInstanceQuery.isLoading && !tokenInstanceQuery.isError && <Box h={{ base: 0, lg: '40vh' }}/> }
</>
);
};
export default React.memo(TokenInstanceContent);
import { Box, Flex, Grid, GridItem, useColorModeValue } from '@chakra-ui/react'; import { Flex, Grid, GridItem, Skeleton, useColorModeValue } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { TokenInstance } from 'types/api/token'; import type { TokenInstance } from 'types/api/token';
...@@ -18,11 +18,12 @@ import TokenInstanceCreatorAddress from './details/TokenInstanceCreatorAddress'; ...@@ -18,11 +18,12 @@ import TokenInstanceCreatorAddress from './details/TokenInstanceCreatorAddress';
import TokenInstanceTransfersCount from './details/TokenInstanceTransfersCount'; import TokenInstanceTransfersCount from './details/TokenInstanceTransfersCount';
interface Props { interface Props {
data: TokenInstance; data?: TokenInstance;
isLoading?: boolean;
scrollRef?: React.RefObject<HTMLDivElement>; scrollRef?: React.RefObject<HTMLDivElement>;
} }
const TokenInstanceDetails = ({ data, scrollRef }: Props) => { const TokenInstanceDetails = ({ data, scrollRef, isLoading }: Props) => {
const handleCounterItemClick = React.useCallback(() => { const handleCounterItemClick = React.useCallback(() => {
window.setTimeout(() => { window.setTimeout(() => {
// cannot do scroll instantly, have to wait a little // cannot do scroll instantly, have to wait a little
...@@ -30,7 +31,7 @@ const TokenInstanceDetails = ({ data, scrollRef }: Props) => { ...@@ -30,7 +31,7 @@ const TokenInstanceDetails = ({ data, scrollRef }: Props) => {
}, 500); }, 500);
}, [ scrollRef ]); }, [ scrollRef ]);
const metadata = parseMetadata(data.metadata); const metadata = parseMetadata(data?.metadata);
const hasMetadata = metadata && Boolean((metadata.name || metadata.description || metadata.attributes)); const hasMetadata = metadata && Boolean((metadata.name || metadata.description || metadata.attributes));
const attributeBgColor = useColorModeValue('blackAlpha.50', 'whiteAlpha.50'); const attributeBgColor = useColorModeValue('blackAlpha.50', 'whiteAlpha.50');
...@@ -44,6 +45,10 @@ const TokenInstanceDetails = ({ data, scrollRef }: Props) => { ...@@ -44,6 +45,10 @@ const TokenInstanceDetails = ({ data, scrollRef }: Props) => {
/> />
); );
if (!data) {
return null;
}
return ( return (
<> <>
<Flex alignItems="flex-start" mt={ 8 } flexDir={{ base: 'column-reverse', lg: 'row' }} columnGap={ 6 } rowGap={ 6 }> <Flex alignItems="flex-start" mt={ 8 } flexDir={{ base: 'column-reverse', lg: 'row' }} columnGap={ 6 } rowGap={ 6 }>
...@@ -57,34 +62,37 @@ const TokenInstanceDetails = ({ data, scrollRef }: Props) => { ...@@ -57,34 +62,37 @@ const TokenInstanceDetails = ({ data, scrollRef }: Props) => {
<DetailsInfoItem <DetailsInfoItem
title="Token" title="Token"
hint="Token name" hint="Token name"
isLoading={ isLoading }
> >
<TokenSnippet data={ data.token }/> <TokenSnippet data={ data.token } isLoading={ isLoading }/>
</DetailsInfoItem> </DetailsInfoItem>
{ data.is_unique && data.owner && ( { data.is_unique && data.owner && (
<DetailsInfoItem <DetailsInfoItem
title="Owner" title="Owner"
hint="Current owner of this token instance" hint="Current owner of this token instance"
isLoading={ isLoading }
> >
<Address> <Address>
<AddressIcon address={ data.owner }/> <AddressIcon address={ data.owner } isLoading={ isLoading }/>
<AddressLink type="address" hash={ data.owner.hash } ml={ 2 }/> <AddressLink type="address" hash={ data.owner.hash } ml={ 2 } isLoading={ isLoading }/>
<CopyToClipboard text={ data.owner.hash }/> <CopyToClipboard text={ data.owner.hash } isLoading={ isLoading }/>
</Address> </Address>
</DetailsInfoItem> </DetailsInfoItem>
) } ) }
<TokenInstanceCreatorAddress hash={ data.token.address }/> <TokenInstanceCreatorAddress hash={ isLoading ? '' : data.token.address }/>
<DetailsInfoItem <DetailsInfoItem
title="Token ID" title="Token ID"
hint="This token instance unique token ID" hint="This token instance unique token ID"
isLoading={ isLoading }
> >
<Flex alignItems="center" overflow="hidden"> <Flex alignItems="center" overflow="hidden">
<Box overflow="hidden" display="inline-block" w="100%"> <Skeleton isLoaded={ !isLoading } overflow="hidden" display="inline-block" w="100%">
<HashStringShortenDynamic hash={ data.id }/> <HashStringShortenDynamic hash={ data.id }/>
</Box> </Skeleton>
<CopyToClipboard text={ data.id } ml={ 1 }/> <CopyToClipboard text={ data.id } isLoading={ isLoading }/>
</Flex> </Flex>
</DetailsInfoItem> </DetailsInfoItem>
<TokenInstanceTransfersCount hash={ data.token.address } id={ data.id } onClick={ handleCounterItemClick }/> <TokenInstanceTransfersCount hash={ isLoading ? '' : data.token.address } id={ isLoading ? '' : data.id } onClick={ handleCounterItemClick }/>
</Grid> </Grid>
<NftMedia <NftMedia
imageUrl={ data.image_url } imageUrl={ data.image_url }
...@@ -92,6 +100,7 @@ const TokenInstanceDetails = ({ data, scrollRef }: Props) => { ...@@ -92,6 +100,7 @@ const TokenInstanceDetails = ({ data, scrollRef }: Props) => {
w="250px" w="250px"
flexShrink={ 0 } flexShrink={ 0 }
alignSelf={{ base: 'center', lg: 'flex-start' }} alignSelf={{ base: 'center', lg: 'flex-start' }}
isLoading={ isLoading }
/> />
</Flex> </Flex>
<Grid <Grid
...@@ -110,8 +119,11 @@ const TokenInstanceDetails = ({ data, scrollRef }: Props) => { ...@@ -110,8 +119,11 @@ const TokenInstanceDetails = ({ data, scrollRef }: Props) => {
hint="NFT name" hint="NFT name"
whiteSpace="normal" whiteSpace="normal"
wordBreak="break-word" wordBreak="break-word"
isLoading={ isLoading }
> >
{ metadata.name } <Skeleton isLoaded={ !isLoading }>
{ metadata.name }
</Skeleton>
</DetailsInfoItem> </DetailsInfoItem>
) } ) }
{ metadata?.description && ( { metadata?.description && (
...@@ -120,8 +132,11 @@ const TokenInstanceDetails = ({ data, scrollRef }: Props) => { ...@@ -120,8 +132,11 @@ const TokenInstanceDetails = ({ data, scrollRef }: Props) => {
hint="NFT description" hint="NFT description"
whiteSpace="normal" whiteSpace="normal"
wordBreak="break-word" wordBreak="break-word"
isLoading={ isLoading }
> >
{ metadata.description } <Skeleton isLoaded={ !isLoading }>
{ metadata.description }
</Skeleton>
</DetailsInfoItem> </DetailsInfoItem>
) } ) }
{ metadata?.attributes && ( { metadata?.attributes && (
...@@ -129,6 +144,7 @@ const TokenInstanceDetails = ({ data, scrollRef }: Props) => { ...@@ -129,6 +144,7 @@ const TokenInstanceDetails = ({ data, scrollRef }: Props) => {
title="Attributes" title="Attributes"
hint="NFT attributes" hint="NFT attributes"
whiteSpace="normal" whiteSpace="normal"
isLoading={ isLoading }
> >
<Grid gap={ 2 } templateColumns="repeat(auto-fill,minmax(160px, 1fr))" w="100%"> <Grid gap={ 2 } templateColumns="repeat(auto-fill,minmax(160px, 1fr))" w="100%">
{ metadata.attributes.map((attribute, index) => ( { metadata.attributes.map((attribute, index) => (
...@@ -138,9 +154,16 @@ const TokenInstanceDetails = ({ data, scrollRef }: Props) => { ...@@ -138,9 +154,16 @@ const TokenInstanceDetails = ({ data, scrollRef }: Props) => {
borderRadius="md" borderRadius="md"
px={ 4 } px={ 4 }
py={ 2 } py={ 2 }
display="flex"
flexDir="column"
alignItems="flex-start"
> >
<Box fontSize="xs" color="text_secondary" fontWeight={ 500 }>{ attribute.trait_type }</Box> <Skeleton isLoaded={ !isLoading } fontSize="xs" lineHeight={ 4 } color="text_secondary" fontWeight={ 500 } mb={ 1 }>
<Box fontSize="sm">{ attribute.value }</Box> <span>{ attribute.trait_type }</span>
</Skeleton>
<Skeleton isLoaded={ !isLoading } fontSize="sm">
<span>{ attribute.value }</span>
</Skeleton>
</GridItem> </GridItem>
)) } )) }
</Grid> </Grid>
...@@ -149,7 +172,7 @@ const TokenInstanceDetails = ({ data, scrollRef }: Props) => { ...@@ -149,7 +172,7 @@ const TokenInstanceDetails = ({ data, scrollRef }: Props) => {
</> </>
) } ) }
{ divider } { divider }
<DetailsSponsoredItem/> <DetailsSponsoredItem isLoading={ isLoading }/>
</Grid> </Grid>
</> </>
); );
......
...@@ -3,6 +3,7 @@ import React from 'react'; ...@@ -3,6 +3,7 @@ import React from 'react';
import type { TokenInstance } from 'types/api/token'; import type { TokenInstance } from 'types/api/token';
import ContentLoader from 'ui/shared/ContentLoader';
import CopyToClipboard from 'ui/shared/CopyToClipboard'; import CopyToClipboard from 'ui/shared/CopyToClipboard';
import RawDataSnippet from 'ui/shared/RawDataSnippet'; import RawDataSnippet from 'ui/shared/RawDataSnippet';
...@@ -12,15 +13,20 @@ type Format = 'JSON' | 'Table' ...@@ -12,15 +13,20 @@ type Format = 'JSON' | 'Table'
interface Props { interface Props {
data: TokenInstance['metadata'] | undefined; data: TokenInstance['metadata'] | undefined;
isPlaceholderData?: boolean;
} }
const TokenInstanceMetadata = ({ data }: Props) => { const TokenInstanceMetadata = ({ data, isPlaceholderData }: Props) => {
const [ format, setFormat ] = React.useState<Format>('Table'); const [ format, setFormat ] = React.useState<Format>('Table');
const handleSelectChange = React.useCallback((event: React.ChangeEvent<HTMLSelectElement>) => { const handleSelectChange = React.useCallback((event: React.ChangeEvent<HTMLSelectElement>) => {
setFormat(event.target.value as Format); setFormat(event.target.value as Format);
}, []); }, []);
if (isPlaceholderData) {
return <ContentLoader/>;
}
if (!data) { if (!data) {
return <Box>There is no metadata for this NFT</Box>; return <Box>There is no metadata for this NFT</Box>;
} }
......
import { Box, Flex, Grid, Skeleton, SkeletonCircle } from '@chakra-ui/react';
import React from 'react';
import DetailsSkeletonRow from 'ui/shared/skeletons/DetailsSkeletonRow';
import SkeletonTabs from 'ui/shared/skeletons/SkeletonTabs';
const TokenInstanceSkeleton = () => {
return (
<Box>
<Skeleton h={{ base: 12, lg: 6 }} mb={ 6 } w="100%" maxW="680px"/>
<Skeleton h={ 10 } maxW="400px" w="100%" mb={ 6 }/>
<Flex align="center">
<SkeletonCircle boxSize={ 6 } flexShrink={ 0 }/>
<Skeleton h={ 6 } w={{ base: '100px', lg: '420px' }} ml={ 2 } borderRadius="full"/>
<Skeleton h={ 8 } w="36px" ml={ 3 } flexShrink={ 0 }/>
<Skeleton h={ 8 } w="36px" ml={ 3 } flexShrink={ 0 }/>
</Flex>
<Flex columnGap={ 6 } rowGap={ 6 } alignItems="flex-start" flexDir={{ base: 'column-reverse', lg: 'row' }} mt={ 8 }>
<Grid
columnGap={ 8 }
rowGap={{ base: 5, lg: 7 }}
templateColumns={{ base: '1fr', lg: '200px 1fr' }}
flexGrow={ 1 }
w="100%"
>
<DetailsSkeletonRow w="30%"/>
<DetailsSkeletonRow w="100%" maxW="450px"/>
<DetailsSkeletonRow w="100%" maxW="450px"/>
<DetailsSkeletonRow w="10%"/>
<DetailsSkeletonRow w="10%"/>
<DetailsSkeletonRow w="10%"/>
</Grid>
<Skeleton h="250px" w="250px" flexShrink={ 0 } alignSelf="center"/>
</Flex>
<SkeletonTabs/>
</Box>
);
};
export default TokenInstanceSkeleton;
import React from 'react'; import React from 'react';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import { ADDRESS_INFO } from 'stubs/address';
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 CopyToClipboard from 'ui/shared/CopyToClipboard'; import CopyToClipboard from 'ui/shared/CopyToClipboard';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem'; import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import DetailsSkeletonRow from 'ui/shared/skeletons/DetailsSkeletonRow';
interface Props { interface Props {
hash: string; hash: string;
...@@ -15,17 +15,17 @@ interface Props { ...@@ -15,17 +15,17 @@ interface Props {
const TokenInstanceCreatorAddress = ({ hash }: Props) => { const TokenInstanceCreatorAddress = ({ hash }: Props) => {
const addressQuery = useApiQuery('address', { const addressQuery = useApiQuery('address', {
pathParams: { hash }, pathParams: { hash },
queryOptions: {
enabled: Boolean(hash),
placeholderData: ADDRESS_INFO,
},
}); });
if (addressQuery.isError) { if (addressQuery.isError) {
return null; return null;
} }
if (addressQuery.isLoading) { if (!addressQuery.data?.creator_address_hash) {
return <DetailsSkeletonRow w="30%"/>;
}
if (!addressQuery.data.creator_address_hash) {
return null; return null;
} }
...@@ -39,11 +39,12 @@ const TokenInstanceCreatorAddress = ({ hash }: Props) => { ...@@ -39,11 +39,12 @@ const TokenInstanceCreatorAddress = ({ hash }: Props) => {
<DetailsInfoItem <DetailsInfoItem
title="Creator" title="Creator"
hint="Address that deployed this token contract" hint="Address that deployed this token contract"
isLoading={ addressQuery.isPlaceholderData }
> >
<Address> <Address>
<AddressIcon address={ creatorAddress }/> <AddressIcon address={ creatorAddress } isLoading={ addressQuery.isPlaceholderData }/>
<AddressLink type="address" hash={ creatorAddress.hash } ml={ 2 }/> <AddressLink type="address" hash={ creatorAddress.hash } ml={ 2 } isLoading={ addressQuery.isPlaceholderData }/>
<CopyToClipboard text={ creatorAddress.hash }/> <CopyToClipboard text={ creatorAddress.hash } isLoading={ addressQuery.isPlaceholderData }/>
</Address> </Address>
</DetailsInfoItem> </DetailsInfoItem>
); );
......
import { Skeleton } from '@chakra-ui/react';
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem'; import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import LinkInternal from 'ui/shared/LinkInternal'; import LinkInternal from 'ui/shared/LinkInternal';
import DetailsSkeletonRow from 'ui/shared/skeletons/DetailsSkeletonRow';
interface Props { interface Props {
hash: string; hash: string;
...@@ -15,17 +15,19 @@ interface Props { ...@@ -15,17 +15,19 @@ interface Props {
const TokenInstanceTransfersCount = ({ hash, id, onClick }: Props) => { const TokenInstanceTransfersCount = ({ hash, id, onClick }: Props) => {
const transfersCountQuery = useApiQuery('token_instance_transfers_count', { const transfersCountQuery = useApiQuery('token_instance_transfers_count', {
pathParams: { hash, id }, pathParams: { hash, id },
queryOptions: {
enabled: Boolean(hash && id),
placeholderData: {
transfers_count: 420,
},
},
}); });
if (transfersCountQuery.isError) { if (transfersCountQuery.isError) {
return null; return null;
} }
if (transfersCountQuery.isLoading) { if (!transfersCountQuery.data?.transfers_count) {
return <DetailsSkeletonRow w="30%"/>;
}
if (!transfersCountQuery.data.transfers_count) {
return null; return null;
} }
...@@ -37,13 +39,16 @@ const TokenInstanceTransfersCount = ({ hash, id, onClick }: Props) => { ...@@ -37,13 +39,16 @@ const TokenInstanceTransfersCount = ({ hash, id, onClick }: Props) => {
<DetailsInfoItem <DetailsInfoItem
title="Transfers" title="Transfers"
hint="Number of transfer for the token instance" hint="Number of transfer for the token instance"
isLoading={ transfersCountQuery.isPlaceholderData }
> >
<LinkInternal <Skeleton isLoaded={ !transfersCountQuery.isPlaceholderData } display="inline-block">
href={ url } <LinkInternal
onClick={ transfersCountQuery.data.transfers_count > 0 ? onClick : undefined } href={ url }
> onClick={ transfersCountQuery.data.transfers_count > 0 ? onClick : undefined }
{ transfersCountQuery.data.transfers_count.toLocaleString() } >
</LinkInternal> { transfersCountQuery.data.transfers_count.toLocaleString() }
</LinkInternal>
</Skeleton>
</DetailsInfoItem> </DetailsInfoItem>
); );
}; };
......
...@@ -9,6 +9,8 @@ import useDebounce from 'lib/hooks/useDebounce'; ...@@ -9,6 +9,8 @@ import useDebounce from 'lib/hooks/useDebounce';
import useQueryWithPages from 'lib/hooks/useQueryWithPages'; import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import { apos } from 'lib/html-entities'; import { apos } from 'lib/html-entities';
import TOKEN_TYPE from 'lib/token/tokenTypes'; import TOKEN_TYPE from 'lib/token/tokenTypes';
import { TOKEN_INFO_ERC_20 } from 'stubs/token';
import { generateListStub } from 'stubs/utils';
import ActionBar from 'ui/shared/ActionBar'; import ActionBar from 'ui/shared/ActionBar';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
import DataListDisplay from 'ui/shared/DataListDisplay'; import DataListDisplay from 'ui/shared/DataListDisplay';
...@@ -25,14 +27,28 @@ const getTokenFilterValue = (getFilterValuesFromQuery<TokenType>).bind(null, TOK ...@@ -25,14 +27,28 @@ const getTokenFilterValue = (getFilterValuesFromQuery<TokenType>).bind(null, TOK
const Tokens = () => { const Tokens = () => {
const router = useRouter(); const router = useRouter();
const [ filter, setFilter ] = React.useState<string>(router.query.filter?.toString() || ''); const [ filter, setFilter ] = React.useState<string>(router.query.q?.toString() || '');
const [ type, setType ] = React.useState<Array<TokenType> | undefined>(getTokenFilterValue(router.query.type)); const [ type, setType ] = React.useState<Array<TokenType> | undefined>(getTokenFilterValue(router.query.type));
const debouncedFilter = useDebounce(filter, 300); const debouncedFilter = useDebounce(filter, 300);
const { isError, isLoading, data, isPaginationVisible, pagination, onFilterChange } = useQueryWithPages({ const { isError, isPlaceholderData, data, isPaginationVisible, pagination, onFilterChange } = useQueryWithPages({
resourceName: 'tokens', resourceName: 'tokens',
filters: { q: debouncedFilter, type }, filters: { q: debouncedFilter, type },
options: {
placeholderData: generateListStub<'tokens'>(
TOKEN_INFO_ERC_20,
50,
{
next_page_params: {
holder_count: 81528,
items_count: 50,
name: '',
market_cap: null,
},
},
),
},
}); });
const onSearchChange = useCallback((value: string) => { const onSearchChange = useCallback((value: string) => {
...@@ -84,15 +100,23 @@ const Tokens = () => { ...@@ -84,15 +100,23 @@ const Tokens = () => {
const content = data?.items ? ( const content = data?.items ? (
<> <>
<Show below="lg" ssr={ false }> <Show below="lg" ssr={ false }>
{ data.items.map((item, index) => <TokensListItem key={ item.address } token={ item } index={ index } page={ pagination.page }/>) } { data.items.map((item, index) => (
<TokensListItem
key={ item.address + (isPlaceholderData ? index : '') }
token={ item }
index={ index }
page={ pagination.page }
isLoading={ isPlaceholderData }
/>
)) }
</Show> </Show>
<Hide below="lg" ssr={ false }><TokensTable items={ data.items } page={ pagination.page }/></Hide></> <Hide below="lg" ssr={ false }><TokensTable items={ data.items } page={ pagination.page } isLoading={ isPlaceholderData }/></Hide></>
) : null; ) : null;
return ( return (
<DataListDisplay <DataListDisplay
isError={ isError } isError={ isError }
isLoading={ isLoading } isLoading={ false }
items={ data?.items } items={ data?.items }
skeletonProps={{ skeletonDesktopColumns: [ '25px', '33%', '33%', '33%', '110px' ] }} skeletonProps={{ skeletonDesktopColumns: [ '25px', '33%', '33%', '33%', '110px' ] }}
emptyText="There are no tokens." emptyText="There are no tokens."
......
import { Flex, Text, Tag, HStack, Grid, GridItem } from '@chakra-ui/react'; import { Flex, HStack, Grid, GridItem, Skeleton } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { TokenInfo } from 'types/api/token'; import type { TokenInfo } from 'types/api/token';
...@@ -6,6 +6,7 @@ import type { TokenInfo } from 'types/api/token'; ...@@ -6,6 +6,7 @@ import type { TokenInfo } from 'types/api/token';
import getCurrencyValue from 'lib/getCurrencyValue'; import getCurrencyValue from 'lib/getCurrencyValue';
import AddressAddToWallet from 'ui/shared/address/AddressAddToWallet'; import AddressAddToWallet from 'ui/shared/address/AddressAddToWallet';
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 TokenLogo from 'ui/shared/TokenLogo'; import TokenLogo from 'ui/shared/TokenLogo';
...@@ -14,6 +15,7 @@ type Props = { ...@@ -14,6 +15,7 @@ type Props = {
token: TokenInfo; token: TokenInfo;
index: number; index: number;
page: number; page: number;
isLoading?: boolean;
} }
const PAGE_SIZE = 50; const PAGE_SIZE = 50;
...@@ -22,6 +24,7 @@ const TokensTableItem = ({ ...@@ -22,6 +24,7 @@ const TokensTableItem = ({
token, token,
page, page,
index, index,
isLoading,
}: Props) => { }: Props) => {
const { const {
...@@ -47,37 +50,39 @@ const TokensTableItem = ({ ...@@ -47,37 +50,39 @@ const TokensTableItem = ({
> >
<GridItem display="flex"> <GridItem display="flex">
<Flex overflow="hidden" mr={ 3 } alignItems="center"> <Flex overflow="hidden" mr={ 3 } alignItems="center">
<TokenLogo data={ token } boxSize={ 6 } mr={ 2 }/> <TokenLogo data={ token } boxSize={ 6 } mr={ 2 } isLoading={ isLoading }/>
<AddressLink fontSize="sm" fontWeight="700" hash={ address } type="token" alias={ tokenString }/> <AddressLink fontSize="sm" fontWeight="700" hash={ address } type="token" alias={ tokenString } isLoading={ isLoading } mr={ 3 }/>
<Tag flexShrink={ 0 } ml={ 3 }>{ type }</Tag> <Tag flexShrink={ 0 } isLoading={ isLoading }>{ type }</Tag>
</Flex> </Flex>
<Text fontSize="sm" ml="auto" variant="secondary">{ (page - 1) * PAGE_SIZE + index + 1 }</Text> <Skeleton isLoaded={ !isLoading } fontSize="sm" ml="auto" color="text_secondary" minW="24px" textAlign="right" lineHeight={ 6 }>
<span>{ (page - 1) * PAGE_SIZE + index + 1 }</span>
</Skeleton>
</GridItem> </GridItem>
</Grid> </Grid>
<Flex justifyContent="space-between" alignItems="center" width="100%"> <Flex justifyContent="space-between" alignItems="center" width="100%">
<Flex alignItems="center" width="136px" justifyContent="space-between" ml={ 8 } mt="-8px"> <Flex alignItems="center" width="136px" justifyContent="space-between" ml={ 8 } mt="-8px">
<Flex alignItems="center"> <Flex alignItems="center">
<AddressLink fontSize="sm" hash={ address } type="address" truncation="constant"/> <AddressLink fontSize="sm" hash={ address } type="address" truncation="constant" isLoading={ isLoading }/>
<CopyToClipboard text={ address } ml={ 1 }/> <CopyToClipboard text={ address } isLoading={ isLoading }/>
</Flex> </Flex>
<AddressAddToWallet token={ token }/> <AddressAddToWallet token={ token } isLoading={ isLoading }/>
</Flex> </Flex>
</Flex> </Flex>
{ exchangeRate && ( { exchangeRate && (
<HStack spacing={ 3 }> <HStack spacing={ 3 }>
<Text fontSize="sm" fontWeight={ 500 }>Price</Text> <Skeleton isLoaded={ !isLoading } fontSize="sm" fontWeight={ 500 }>Price</Skeleton>
<Text fontSize="sm" variant="secondary">{ exchangeRate }</Text> <Skeleton isLoaded={ !isLoading } fontSize="sm" color="text_secondary"><span>{ exchangeRate }</span></Skeleton>
</HStack> </HStack>
) } ) }
{ totalValue?.usd && ( { totalValue?.usd && (
<HStack spacing={ 3 }> <HStack spacing={ 3 }>
<Text fontSize="sm" fontWeight={ 500 }>On-chain market cap</Text> <Skeleton isLoaded={ !isLoading } fontSize="sm" fontWeight={ 500 }>On-chain market cap</Skeleton>
<Text fontSize="sm" variant="secondary">{ totalValue.usd }</Text> <Skeleton isLoaded={ !isLoading } fontSize="sm" color="text_secondary"><span>{ totalValue.usd }</span></Skeleton>
</HStack> </HStack>
) } ) }
<HStack spacing={ 3 }> <HStack spacing={ 3 }>
<Text fontSize="sm" fontWeight={ 500 }>Holders</Text> <Skeleton isLoaded={ !isLoading } fontSize="sm" fontWeight={ 500 }>Holders</Skeleton>
<Text fontSize="sm" variant="secondary">{ Number(holders).toLocaleString() }</Text> <Skeleton isLoaded={ !isLoading } fontSize="sm" color="text_secondary"><span>{ Number(holders).toLocaleString() }</span></Skeleton>
</HStack> </HStack>
</ListItemMobile> </ListItemMobile>
); );
......
...@@ -10,9 +10,10 @@ import TokensTableItem from './TokensTableItem'; ...@@ -10,9 +10,10 @@ import TokensTableItem from './TokensTableItem';
type Props = { type Props = {
items: Array<TokenInfo>; items: Array<TokenInfo>;
page: number; page: number;
isLoading?: boolean;
} }
const TokensTable = ({ items, page }: Props) => { const TokensTable = ({ items, page, isLoading }: Props) => {
return ( return (
<Table style={{ tableLayout: 'auto' }}> <Table style={{ tableLayout: 'auto' }}>
<Thead top={ 80 }> <Thead top={ 80 }>
...@@ -25,7 +26,7 @@ const TokensTable = ({ items, page }: Props) => { ...@@ -25,7 +26,7 @@ const TokensTable = ({ items, page }: Props) => {
</Thead> </Thead>
<Tbody> <Tbody>
{ items.map((item, index) => ( { items.map((item, index) => (
<TokensTableItem key={ item.address } token={ item } index={ index } page={ page }/> <TokensTableItem key={ item.address + (isLoading ? index : '') } token={ item } index={ index } page={ page } isLoading={ isLoading }/>
)) } )) }
</Tbody> </Tbody>
</Table> </Table>
......
import { Box, Flex, Td, Tr, Text, Tag } from '@chakra-ui/react'; import { Box, Flex, Td, Tr, Skeleton } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { TokenInfo } from 'types/api/token'; import type { TokenInfo } from 'types/api/token';
import getCurrencyValue from 'lib/getCurrencyValue'; import getCurrencyValue from 'lib/getCurrencyValue';
import Address from 'ui/shared/address/Address';
import AddressAddToWallet from 'ui/shared/address/AddressAddToWallet'; import AddressAddToWallet from 'ui/shared/address/AddressAddToWallet';
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 TokenLogo from 'ui/shared/TokenLogo'; import TokenLogo from 'ui/shared/TokenLogo';
...@@ -13,6 +15,7 @@ type Props = { ...@@ -13,6 +15,7 @@ type Props = {
token: TokenInfo; token: TokenInfo;
index: number; index: number;
page: number; page: number;
isLoading?: boolean;
} }
const PAGE_SIZE = 50; const PAGE_SIZE = 50;
...@@ -21,6 +24,7 @@ const TokensTableItem = ({ ...@@ -21,6 +24,7 @@ const TokensTableItem = ({
token, token,
page, page,
index, index,
isLoading,
}: Props) => { }: Props) => {
const { const {
...@@ -41,8 +45,9 @@ const TokensTableItem = ({ ...@@ -41,8 +45,9 @@ const TokensTableItem = ({
return ( return (
<Tr> <Tr>
<Td> <Td>
<Flex> <Flex alignItems="flex-start">
<Text <Skeleton
isLoaded={ !isLoading }
fontSize="sm" fontSize="sm"
lineHeight="24px" lineHeight="24px"
fontWeight={ 600 } fontWeight={ 600 }
...@@ -50,28 +55,46 @@ const TokensTableItem = ({ ...@@ -50,28 +55,46 @@ const TokensTableItem = ({
minW="28px" minW="28px"
> >
{ (page - 1) * PAGE_SIZE + index + 1 } { (page - 1) * PAGE_SIZE + index + 1 }
</Text> </Skeleton>
<Box> <Box>
<Flex alignItems="center"> <Flex alignItems="center">
<TokenLogo data={ token } boxSize={ 6 } mr={ 2 }/> <TokenLogo data={ token } boxSize={ 6 } mr={ 2 } isLoading={ isLoading }/>
<AddressLink fontSize="sm" fontWeight="700" hash={ address } type="token" alias={ tokenString }/> <AddressLink fontSize="sm" fontWeight="700" hash={ address } type="token" alias={ tokenString } isLoading={ isLoading }/>
</Flex> </Flex>
<Flex alignItems="center" width="136px" justifyContent="space-between" ml={ 8 } mt={ 2 }> <Box ml={ 8 } mt={ 2 }>
<Flex alignItems="center"> <Address>
<AddressLink fontSize="sm" hash={ address } type="address" truncation="constant" fontWeight={ 500 }/> <AddressLink fontSize="sm" hash={ address } type="address" truncation="constant" fontWeight={ 500 } isLoading={ isLoading }/>
<CopyToClipboard text={ address } ml={ 1 }/> <CopyToClipboard text={ address } isLoading={ isLoading }/>
</Flex> <AddressAddToWallet token={ token } ml={ 2 } isLoading={ isLoading }/>
<AddressAddToWallet token={ token }/> </Address>
</Flex> <Box mt={ 3 } >
<Tag flexShrink={ 0 } ml={ 8 } mt={ 3 }>{ type }</Tag> <Tag isLoading={ isLoading }>{ type }</Tag>
</Box>
</Box>
</Box> </Box>
</Flex> </Flex>
</Td> </Td>
<Td isNumeric><Text fontSize="sm" lineHeight="24px" fontWeight={ 500 }>{ exchangeRate && `$${ exchangeRate }` }</Text></Td> <Td isNumeric>
<Skeleton isLoaded={ !isLoading } fontSize="sm" lineHeight="24px" fontWeight={ 500 } display="inline-block">
{ exchangeRate && `$${ exchangeRate }` }
</Skeleton>
</Td>
<Td isNumeric maxWidth="300px" width="300px"> <Td isNumeric maxWidth="300px" width="300px">
<Text fontSize="sm" lineHeight="24px" fontWeight={ 500 }>{ totalValue?.usd && `$${ totalValue.usd }` }</Text> <Skeleton isLoaded={ !isLoading } fontSize="sm" lineHeight="24px" fontWeight={ 500 } display="inline-block">
{ totalValue?.usd && `$${ totalValue.usd }` }
</Skeleton>
</Td>
<Td isNumeric>
<Skeleton
isLoaded={ !isLoading }
fontSize="sm"
lineHeight="24px"
fontWeight={ 500 }
display="inline-block"
>
{ Number(holders).toLocaleString() }
</Skeleton>
</Td> </Td>
<Td isNumeric><Text fontSize="sm" lineHeight="24px" fontWeight={ 500 }>{ Number(holders).toLocaleString() }</Text></Td>
</Tr> </Tr>
); );
}; };
......
...@@ -193,7 +193,7 @@ const TxDetails = () => { ...@@ -193,7 +193,7 @@ const TxDetails = () => {
</Skeleton> </Skeleton>
</DetailsInfoItem> </DetailsInfoItem>
) } ) }
<DetailsSponsoredItem/> <DetailsSponsoredItem isLoading={ isPlaceholderData }/>
{ divider } { divider }
......
...@@ -77,7 +77,7 @@ const TxInternals = () => { ...@@ -77,7 +77,7 @@ const TxInternals = () => {
pathParams: { hash: txInfo.data?.hash }, pathParams: { hash: txInfo.data?.hash },
options: { options: {
enabled: !txInfo.isPlaceholderData && Boolean(txInfo.data?.hash) && Boolean(txInfo.data?.status), enabled: !txInfo.isPlaceholderData && Boolean(txInfo.data?.hash) && Boolean(txInfo.data?.status),
placeholderData: generateListStub<'tx_internal_txs'>(INTERNAL_TX, 3), placeholderData: generateListStub<'tx_internal_txs'>(INTERNAL_TX, 3, { next_page_params: null }),
}, },
}); });
......
...@@ -20,7 +20,7 @@ const TxLogs = () => { ...@@ -20,7 +20,7 @@ const TxLogs = () => {
pathParams: { hash: txInfo.data?.hash }, pathParams: { hash: txInfo.data?.hash },
options: { options: {
enabled: !txInfo.isPlaceholderData && Boolean(txInfo.data?.hash) && Boolean(txInfo.data?.status), enabled: !txInfo.isPlaceholderData && Boolean(txInfo.data?.hash) && Boolean(txInfo.data?.status),
placeholderData: generateListStub<'tx_logs'>(LOG, 3), placeholderData: generateListStub<'tx_logs'>(LOG, 3, { next_page_params: null }),
}, },
}); });
......
...@@ -45,13 +45,13 @@ const TxInternalsListItem = ({ type, from, to, value, success, error, gas_limit: ...@@ -45,13 +45,13 @@ const TxInternalsListItem = ({ type, from, to, value, success, error, gas_limit:
</Box> </Box>
<HStack spacing={ 3 }> <HStack spacing={ 3 }>
<Skeleton isLoaded={ !isLoading } fontSize="sm" fontWeight={ 500 }>Value { appConfig.network.currency.symbol }</Skeleton> <Skeleton isLoaded={ !isLoading } fontSize="sm" fontWeight={ 500 }>Value { appConfig.network.currency.symbol }</Skeleton>
<Skeleton isLoaded={ !isLoading } fontSize="sm" variant="secondary"> <Skeleton isLoaded={ !isLoading } fontSize="sm" color="text_secondary">
{ BigNumber(value).div(BigNumber(10 ** appConfig.network.currency.decimals)).toFormat() } { BigNumber(value).div(BigNumber(10 ** appConfig.network.currency.decimals)).toFormat() }
</Skeleton> </Skeleton>
</HStack> </HStack>
<HStack spacing={ 3 }> <HStack spacing={ 3 }>
<Skeleton isLoaded={ !isLoading } fontSize="sm" fontWeight={ 500 }>Gas limit</Skeleton> <Skeleton isLoaded={ !isLoading } fontSize="sm" fontWeight={ 500 }>Gas limit</Skeleton>
<Skeleton isLoaded={ !isLoading } fontSize="sm" variant="secondary">{ BigNumber(gasLimit).toFormat() }</Skeleton> <Skeleton isLoaded={ !isLoading } fontSize="sm" color="text_secondary">{ BigNumber(gasLimit).toFormat() }</Skeleton>
</HStack> </HStack>
</ListItemMobile> </ListItemMobile>
); );
......
...@@ -97,7 +97,6 @@ const TxsContent = ({ ...@@ -97,7 +97,6 @@ const TxsContent = ({
setSorting={ setSortByValue } setSorting={ setSortByValue }
paginationProps={ query.pagination } paginationProps={ query.pagination }
showPagination={ query.isPaginationVisible } showPagination={ query.isPaginationVisible }
isLoading={ query.pagination.isLoading }
filterComponent={ filter } filterComponent={ filter }
linkSlot={ currentAddress ? linkSlot={ currentAddress ?
<AddressCsvExportLink address={ currentAddress } type="transactions" ml={ 2 } isLoading={ query.pagination.isLoading }/> : null <AddressCsvExportLink address={ currentAddress } type="transactions" ml={ 2 } isLoading={ query.pagination.isLoading }/> : null
......
...@@ -28,10 +28,9 @@ type Props = { ...@@ -28,10 +28,9 @@ type Props = {
showPagination?: boolean; showPagination?: boolean;
filterComponent?: React.ReactNode; filterComponent?: React.ReactNode;
linkSlot?: React.ReactNode; linkSlot?: React.ReactNode;
isLoading?: boolean;
} }
const TxsHeaderMobile = ({ filterComponent, sorting, setSorting, paginationProps, className, showPagination = true, linkSlot, isLoading }: Props) => { const TxsHeaderMobile = ({ filterComponent, sorting, setSorting, paginationProps, className, showPagination = true, linkSlot }: Props) => {
return ( return (
<ActionBar className={ className }> <ActionBar className={ className }>
<HStack> <HStack>
...@@ -40,7 +39,7 @@ const TxsHeaderMobile = ({ filterComponent, sorting, setSorting, paginationProps ...@@ -40,7 +39,7 @@ const TxsHeaderMobile = ({ filterComponent, sorting, setSorting, paginationProps
options={ SORT_OPTIONS } options={ SORT_OPTIONS }
setSort={ setSorting } setSort={ setSorting }
sort={ sorting } sort={ sorting }
isLoading={ isLoading } isLoading={ paginationProps.isLoading }
/> />
{ /* api is not implemented */ } { /* api is not implemented */ }
{ /* <FilterInput { /* <FilterInput
......
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