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