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
# network config
NEXT_PUBLIC_NETWORK_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_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/network-icons/goerli.svg
NEXT_PUBLIC_NETWORK_ID=5
......@@ -26,4 +26,4 @@ NEXT_PUBLIC_HAS_BEACON_CHAIN=false
# api config
NEXT_PUBLIC_API_HOST=blockscout-main.k8s-dev.blockscout.com
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
# network config
NEXT_PUBLIC_NETWORK_NAME=Base Göerli
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_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/network-icons/base.svg
NEXT_PUBLIC_NETWORK_ID=84531
......
......@@ -19,7 +19,7 @@ NEXT_PUBLIC_API_SPEC_URL=https://raw.githubusercontent.com/blockscout/blockscout
# network config
NEXT_PUBLIC_NETWORK_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_CURRENCY_NAME=POA
NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=POA
......
......@@ -29,7 +29,7 @@ NEXT_PUBLIC_IS_L2_NETWORK=false
# network config
NEXT_PUBLIC_NETWORK_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_CURRENCY_NAME=Ether
NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH
......
......@@ -362,8 +362,6 @@ frontend:
_default: "Base Göerli"
NEXT_PUBLIC_NETWORK_SHORT_NAME:
_default: Base
NEXT_PUBLIC_NETWORK_ASSETS_PATHNAME:
_default: optimism
NEXT_PUBLIC_NETWORK_ID:
_default: 420
NEXT_PUBLIC_NETWORK_CURRENCY_NAME:
......
......@@ -250,8 +250,6 @@ frontend:
_default: Göerli
NEXT_PUBLIC_NETWORK_SHORT_NAME:
_default: Göerli
NEXT_PUBLIC_NETWORK_ASSETS_PATHNAME:
_default: ethereum
NEXT_PUBLIC_NETWORK_LOGO:
_default: https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/goerli.svg
NEXT_PUBLIC_NETWORK_ICON:
......
......@@ -73,8 +73,6 @@ frontend:
_default: "Base Göerli"
NEXT_PUBLIC_NETWORK_SHORT_NAME:
_default: Base
NEXT_PUBLIC_NETWORK_ASSETS_PATHNAME:
_default: optimism
NEXT_PUBLIC_NETWORK_ID:
_default: 420
NEXT_PUBLIC_NETWORK_CURRENCY_NAME:
......
......@@ -73,8 +73,6 @@ frontend:
_default: Göerli
NEXT_PUBLIC_NETWORK_SHORT_NAME:
_default: Göerli
NEXT_PUBLIC_NETWORK_ASSETS_PATHNAME:
_default: ethereum
NEXT_PUBLIC_NETWORK_LOGO:
_default: https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/goerli.svg
NEXT_PUBLIC_NETWORK_ICON:
......
......@@ -191,7 +191,7 @@ export const RESOURCES = {
},
txs_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: [ ],
},
tx: {
......@@ -296,7 +296,7 @@ export const RESOURCES = {
address_tokens: {
path: '/api/v2/addresses/:hash/tokens',
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 ],
},
address_withdrawals: {
......@@ -383,7 +383,7 @@ export const RESOURCES = {
},
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 ],
},
......
......@@ -7,54 +7,63 @@ export const erc20a: AddressTokenBalance = {
token: tokens.tokenInfoERC20a,
token_id: null,
value: '1169320000000000000000000',
token_instance: null,
};
export const erc20b: AddressTokenBalance = {
token: tokens.tokenInfoERC20b,
token_id: null,
value: '872500000000',
token_instance: null,
};
export const erc20c: AddressTokenBalance = {
token: tokens.tokenInfoERC20c,
token_id: null,
value: '9852000000000000000000',
token_instance: null,
};
export const erc20d: AddressTokenBalance = {
token: tokens.tokenInfoERC20d,
token_id: null,
value: '39000000000000000000',
token_instance: null,
};
export const erc20LongSymbol: AddressTokenBalance = {
token: tokens.tokenInfoERC20LongSymbol,
token_id: null,
value: '39000000000000000000',
token_instance: null,
};
export const erc721a: AddressTokenBalance = {
token: tokens.tokenInfoERC721a,
token_id: null,
value: '51',
token_instance: null,
};
export const erc721b: AddressTokenBalance = {
token: tokens.tokenInfoERC721b,
token_id: null,
value: '1',
token_instance: null,
};
export const erc721c: AddressTokenBalance = {
token: tokens.tokenInfoERC721c,
token_id: null,
value: '5',
token_instance: null,
};
export const erc721LongSymbol: AddressTokenBalance = {
token: tokens.tokenInfoERC721LongSymbol,
token_id: null,
value: '5',
token_instance: null,
};
export const erc1155a: AddressTokenBalance = {
......
......@@ -26,7 +26,7 @@ export const tokenInfoERC20a: TokenInfo = {
symbol: 'HyFi',
total_supply: '369000000000000000000000000',
type: 'ERC-20',
icon_url: null,
icon_url: 'https://example.com/token-icon.png',
};
export const tokenInfoERC20b: TokenInfo = {
......
import type { NextPage } from 'next';
import dynamic from 'next/dynamic';
import Head from 'next/head';
import React from 'react';
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 title = `Top Accounts - ${ getNetworkTitle() }`;
return (
<>
<Head><title>{ title }</title></Head>
<Accounts/>
<Page>
<Accounts/>
</Page>
</>
);
};
......
import type { NextPage } from 'next';
import dynamic from 'next/dynamic';
import Head from 'next/head';
import type { RoutedQuery } from 'nextjs-routes';
import React from 'react';
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 { title, description } = getSeo({ hash });
......@@ -15,7 +18,9 @@ const AddressPage: NextPage<RoutedQuery<'/address/[hash]'>> = ({ hash }: RoutedQ
<title>{ title }</title>
<meta name="description" content={ description }/>
</Head>
<Address/>
<Page>
<Address/>
</Page>
</>
);
};
......
import type { NextPage } from 'next';
import dynamic from 'next/dynamic';
import Head from 'next/head';
import React from 'react';
import type { PageParams } from 'lib/next/token/types';
import 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 title = getNetworkTitle();
......@@ -15,7 +17,9 @@ const TokenInstancePage: NextPage<PageParams> = () => {
<Head>
<title>{ title }</title>
</Head>
<TokenInstance/>
<Page>
<TokenInstance/>
</Page>
</>
);
};
......
import type { NextPage } from 'next';
import dynamic from 'next/dynamic';
import Head from 'next/head';
import React from 'react';
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 title = getNetworkTitle();
......@@ -12,7 +15,9 @@ const TokensPage: NextPage = () => {
<Head>
<title>{ title }</title>
</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 { 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 = {
block_number_balance_updated_at: 8774377,
coin_balance: '0',
coin_balance: '810941268802273085757',
creation_tx_hash: null,
creator_address_hash: null,
creator_address_hash: ADDRESS_HASH,
exchange_rate: null,
has_custom_methods_read: false,
has_custom_methods_write: false,
......@@ -32,3 +34,52 @@ export const ADDRESS_INFO: Address = {
watchlist_names: [],
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 = {
average_block_time: 14346,
......@@ -18,3 +18,38 @@ export const HOMEPAGE_STATS: HomeStats = {
total_transactions: '193823272',
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 { ADDRESS_PARAMS, ADDRESS_HASH } from './addressParams';
......@@ -9,11 +9,11 @@ import { generateListStub } from './utils';
export const TOKEN_INFO_ERC_20: TokenInfo<'ERC-20'> = {
address: ADDRESS_HASH,
decimals: '18',
exchange_rate: null,
exchange_rate: '0.999997',
holders: '16026',
name: 'Stub Token (goerli)',
symbol: 'STUB',
total_supply: '6000000000000000000',
total_supply: '60000000000000000000000',
type: 'ERC-20',
icon_url: null,
};
......@@ -38,8 +38,6 @@ export const TOKEN_HOLDER: TokenHolder = {
value: '1021378038331138520',
};
export const TOKEN_HOLDERS: TokenHolders = { items: Array(50).fill(TOKEN_HOLDER), next_page_params: null };
export const TOKEN_TRANSFER_ERC_20: TokenTransfer = {
block_hash: BLOCK_HASH,
from: ADDRESS_PARAMS,
......@@ -77,11 +75,11 @@ export const TOKEN_TRANSFER_ERC_1155: TokenTransfer = {
export const getTokenTransfersStub = (type?: TokenType, pagination: TokenTransferPagination | null = null): TokenTransferResponse => {
switch (type) {
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':
return generateListStub<'token_transfers'>(TOKEN_TRANSFER_ERC_1155, 50, pagination);
return generateListStub<'token_transfers'>(TOKEN_TRANSFER_ERC_1155, 50, { next_page_params: pagination });
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 = {
image_url: 'https://ipfs.vipsland.com/nft/collections/genesis/188882.gif',
is_unique: true,
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*',
external_url: 'https://vipsland.com/nft/collections/genesis/188882',
image: 'https://ipfs.vipsland.com/nft/collections/genesis/188882.gif',
......
......@@ -5,10 +5,10 @@ import type { PaginatedResources, PaginatedResponse } from 'lib/api/resources';
export function generateListStub<Resource extends PaginatedResources>(
stub: ArrayElement<PaginatedResponse<Resource>['items']>,
num = 50,
pagination: PaginatedResponse<Resource>['next_page_params'] = null,
rest: Omit<PaginatedResponse<Resource>, 'items'>,
) {
return {
items: Array(num).fill(stub),
next_page_params: pagination,
...rest,
};
}
......@@ -45,7 +45,7 @@ export interface AddressTokenBalance {
token: TokenInfo;
token_id: string | null;
value: string;
token_instance?: TokenInstance;
token_instance: TokenInstance | null;
}
export interface AddressTokensResponse {
......@@ -55,6 +55,7 @@ export interface AddressTokensResponse {
token_name: 'string' | null;
token_type: TokenType;
value: number;
fiat_value: string | null;
} | null;
}
......
......@@ -8,6 +8,6 @@ export type AddressesResponse = {
fetched_coin_balance: string;
hash: string;
items_count: number;
};
} | null;
total_supply: string;
}
......@@ -7,6 +7,7 @@ export type TokensResponse = {
holder_count: number;
items_count: number;
name: string;
market_cap: string | null;
};
}
......
......@@ -74,9 +74,9 @@ export interface TransactionsResponsePending {
export interface TransactionsResponseWatchlist {
items: Array<Transaction>;
next_page_params: {
inserted_at: string;
hash: string;
filter: 'pending';
block_number: number;
index: number;
items_count: 50;
} | null;
}
......
......@@ -11,6 +11,8 @@ import { getResourceKey } from 'lib/api/useApiQuery';
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
import { BLOCK } from 'stubs/block';
import { generateListStub } from 'stubs/utils';
import ActionBar from 'ui/shared/ActionBar';
import DataListDisplay from 'ui/shared/DataListDisplay';
import Pagination from 'ui/shared/Pagination';
......@@ -34,6 +36,18 @@ const AddressBlocksValidated = ({ scrollRef }: Props) => {
resourceName: 'address_blocks_validated',
pathParams: { hash: addressHash },
scrollRef,
options: {
placeholderData: generateListStub<'address_blocks_validated'>(
BLOCK,
50,
{
next_page_params: {
block_number: 9060562,
items_count: 50,
},
},
),
},
});
const handleSocketError = React.useCallback(() => {
......@@ -61,7 +75,7 @@ const AddressBlocksValidated = ({ scrollRef }: Props) => {
topic: `blocks:${ addressHash.toLowerCase() }`,
onSocketClose: handleSocketError,
onSocketError: handleSocketError,
isDisabled: !addressHash || query.pagination.page !== 1,
isDisabled: !addressHash || query.isPlaceholderData || query.pagination.page !== 1,
});
useSocketMessage({
channel,
......@@ -84,22 +98,32 @@ const AddressBlocksValidated = ({ scrollRef }: Props) => {
</Tr>
</Thead>
<Tbody>
{ query.data.items.map((item) => (
<AddressBlocksValidatedTableItem key={ item.height } { ...item } page={ query.pagination.page }/>
{ query.data.items.map((item, index) => (
<AddressBlocksValidatedTableItem
key={ item.height + (query.isPlaceholderData ? String(index) : '') }
{ ...item }
page={ query.pagination.page }
isLoading={ query.isPlaceholderData }
/>
)) }
</Tbody>
</Table>
</Hide>
<Show below="lg" ssr={ false }>
{ query.data.items.map((item) => (
<AddressBlocksValidatedListItem key={ item.height } { ...item } page={ query.pagination.page }/>
{ query.data.items.map((item, index) => (
<AddressBlocksValidatedListItem
key={ item.height + (query.isPlaceholderData ? String(index) : '') }
{ ...item }
page={ query.pagination.page }
isLoading={ query.isPlaceholderData }
/>
)) }
</Show>
</>
) : null;
const actionBar = query.isPaginationVisible ? (
<ActionBar mt={ -6 } showShadow={ query.isLoading }>
<ActionBar mt={ -6 }>
<Pagination ml="auto" { ...query.pagination }/>
</ActionBar>
) : null;
......@@ -107,7 +131,7 @@ const AddressBlocksValidated = ({ scrollRef }: Props) => {
return (
<DataListDisplay
isError={ query.isError }
isLoading={ query.isLoading }
isLoading={ false }
items={ query.data?.items }
skeletonProps={{ isLongSkeleton: true, skeletonDesktopColumns: [ '17%', '17%', '16%', '25%', '25%' ] }}
emptyText="There are no validated blocks for this address."
......
......@@ -10,6 +10,8 @@ import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import getQueryParamString from 'lib/router/getQueryParamString';
import useSocketChannel from 'lib/socket/useSocketChannel';
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 AddressCoinBalanceChart from './coinBalance/AddressCoinBalanceChart';
......@@ -26,6 +28,18 @@ const AddressCoinBalance = () => {
resourceName: 'address_coin_balance',
pathParams: { hash: addressHash },
scrollRef,
options: {
placeholderData: generateListStub<'address_coin_balance'>(
ADDRESS_COIN_BALANCE,
50,
{
next_page_params: {
block_number: 8009880,
items_count: 50,
},
},
),
},
});
const handleSocketError = React.useCallback(() => {
......@@ -56,7 +70,7 @@ const AddressCoinBalance = () => {
topic: `addresses:${ addressHash.toLowerCase() }`,
onSocketClose: handleSocketError,
onSocketError: handleSocketError,
isDisabled: !addressHash || coinBalanceQuery.pagination.page !== 1,
isDisabled: !addressHash || coinBalanceQuery.isPlaceholderData || coinBalanceQuery.pagination.page !== 1,
});
useSocketMessage({
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 { useRouter } from 'next/router';
import { route } from 'nextjs-routes';
......@@ -10,9 +10,11 @@ import blockIcon from 'icons/block.svg';
import type { ResourceError } from 'lib/api/resources';
import useApiQuery from 'lib/api/useApiQuery';
import getQueryParamString from 'lib/router/getQueryParamString';
import { ADDRESS_COUNTERS } from 'stubs/address';
import AddressCounterItem from 'ui/address/details/AddressCounterItem';
import AddressLink from 'ui/shared/address/AddressLink';
import AddressHeadingInfo from 'ui/shared/AddressHeadingInfo';
import Icon from 'ui/shared/chakra/Icon';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import DetailsSponsoredItem from 'ui/shared/DetailsSponsoredItem';
......@@ -20,7 +22,6 @@ import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import LinkInternal from 'ui/shared/LinkInternal';
import AddressBalance from './details/AddressBalance';
import AddressDetailsSkeleton from './details/AddressDetailsSkeleton';
import AddressNameInfo from './details/AddressNameInfo';
import TokenSelect from './tokenSelect/TokenSelect';
......@@ -38,6 +39,7 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
pathParams: { hash: addressHash },
queryOptions: {
enabled: Boolean(addressHash) && Boolean(addressQuery.data),
placeholderData: ADDRESS_COUNTERS,
},
});
......@@ -77,26 +79,27 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
return <DataFetchAlert/>;
}
if (addressQuery.isLoading) {
return <AddressDetailsSkeleton/>;
}
const data = addressQuery.isError ? errorData : addressQuery.data;
if (!data) {
return null;
}
return (
<Box>
<AddressHeadingInfo address={ data } token={ data.token } isLinkDisabled/>
<AddressHeadingInfo address={ data } token={ data.token } isLoading={ addressQuery.isPlaceholderData } isLinkDisabled/>
<Grid
mt={ 8 }
columnGap={ 8 }
rowGap={{ base: 1, lg: 3 }}
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 && (
<DetailsInfoItem
title="Creator"
hint="Transaction and address of creation"
isLoading={ addressQuery.isPlaceholderData }
>
<AddressLink type="address" hash={ data.creator_address_hash } truncation="constant"/>
<Text whiteSpace="pre"> at txn </Text>
......@@ -119,7 +122,7 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
) }
</DetailsInfoItem>
) }
<AddressBalance data={ data }/>
<AddressBalance data={ data } isLoading={ addressQuery.isPlaceholderData }/>
{ data.has_tokens && (
<DetailsInfoItem
title="Tokens"
......@@ -133,36 +136,68 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
<DetailsInfoItem
title="Transactions"
hint="Number of transactions related to this address"
isLoading={ addressQuery.isPlaceholderData || countersQuery.isPlaceholderData }
>
{ addressQuery.data ?
<AddressCounterItem prop="transactions_count" query={ countersQuery } address={ data.hash } onClick={ handleCounterItemClick }/> :
{ addressQuery.data ? (
<AddressCounterItem
prop="transactions_count"
query={ countersQuery }
address={ data.hash }
onClick={ handleCounterItemClick }
isAddressQueryLoading={ addressQuery.isPlaceholderData }
/>
) :
0 }
</DetailsInfoItem>
{ data.has_token_transfers && (
<DetailsInfoItem
title="Transfers"
hint="Number of transfers to/from this address"
isLoading={ addressQuery.isPlaceholderData || countersQuery.isPlaceholderData }
>
{ addressQuery.data ?
<AddressCounterItem prop="token_transfers_count" query={ countersQuery } address={ data.hash } onClick={ handleCounterItemClick }/> :
{ addressQuery.data ? (
<AddressCounterItem
prop="token_transfers_count"
query={ countersQuery }
address={ data.hash }
onClick={ handleCounterItemClick }
isAddressQueryLoading={ addressQuery.isPlaceholderData }
/>
) :
0 }
</DetailsInfoItem>
) }
<DetailsInfoItem
title="Gas used"
hint="Gas used by the address"
isLoading={ addressQuery.isPlaceholderData || countersQuery.isPlaceholderData }
>
{ addressQuery.data ?
<AddressCounterItem prop="gas_usage_count" query={ countersQuery } address={ data.hash } onClick={ handleCounterItemClick }/> :
{ addressQuery.data ? (
<AddressCounterItem
prop="gas_usage_count"
query={ countersQuery }
address={ data.hash }
onClick={ handleCounterItemClick }
isAddressQueryLoading={ addressQuery.isPlaceholderData }
/>
) :
0 }
</DetailsInfoItem>
{ data.has_validated_blocks && (
<DetailsInfoItem
title="Blocks validated"
hint="Number of blocks validated by this validator"
isLoading={ addressQuery.isPlaceholderData || countersQuery.isPlaceholderData }
>
{ addressQuery.data ?
<AddressCounterItem prop="validations_count" query={ countersQuery } address={ data.hash } onClick={ handleCounterItemClick }/> :
{ addressQuery.data ? (
<AddressCounterItem
prop="validations_count"
query={ countersQuery }
address={ data.hash }
onClick={ handleCounterItemClick }
isAddressQueryLoading={ addressQuery.isPlaceholderData }
/>
) :
0 }
</DetailsInfoItem>
) }
......@@ -172,18 +207,21 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
hint="Block number in which the address was updated"
alignSelf="center"
py={{ base: '2px', lg: 1 }}
isLoading={ addressQuery.isPlaceholderData }
>
<LinkInternal
href={ route({ pathname: '/block/[height]', query: { height: String(data.block_number_balance_updated_at) } }) }
display="flex"
alignItems="center"
>
<Icon as={ blockIcon } boxSize={ 6 } mr={ 2 }/>
{ data.block_number_balance_updated_at }
<Box mr={ 2 }>
<Icon as={ blockIcon } boxSize={ 6 } isLoading={ addressQuery.isPlaceholderData }/>
</Box>
<Skeleton isLoaded={ !addressQuery.isPlaceholderData }>{ data.block_number_balance_updated_at }</Skeleton>
</LinkInternal>
</DetailsInfoItem>
) }
<DetailsSponsoredItem/>
<DetailsSponsoredItem isLoading={ addressQuery.isPlaceholderData }/>
</Grid>
</Box>
);
......
......@@ -9,6 +9,8 @@ import getFilterValueFromQuery from 'lib/getFilterValueFromQuery';
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import { apos } from 'lib/html-entities';
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 ActionBar from 'ui/shared/ActionBar';
import DataListDisplay from 'ui/shared/DataListDisplay';
......@@ -26,15 +28,28 @@ const AddressInternalTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivE
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',
pathParams: { hash },
filters: { filter: filterValue },
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 newVal = getFilterValue(val);
setFilterValue(newVal);
onFilterChange({ filter: newVal });
......@@ -43,22 +58,23 @@ const AddressInternalTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivE
const content = data?.items ? (
<>
<Show below="lg" ssr={ false }>
<AddressIntTxsList data={ data.items } currentAddress={ hash }/>
<AddressIntTxsList data={ data.items } currentAddress={ hash } isLoading={ isPlaceholderData }/>
</Show>
<Hide below="lg" ssr={ false }>
<AddressIntTxsTable data={ data.items } currentAddress={ hash }/>
<AddressIntTxsTable data={ data.items } currentAddress={ hash } isLoading={ isPlaceholderData }/>
</Hide>
</>
) : null ;
const actionBar = (
<ActionBar mt={ -6 } justifyContent="left" showShadow={ isLoading }>
<ActionBar mt={ -6 } justifyContent="left">
<AddressTxsFilter
defaultFilter={ filterValue }
onFilterChange={ handleFilterChange }
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 }/> }
</ActionBar>
);
......@@ -66,7 +82,7 @@ const AddressInternalTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivE
return (
<DataListDisplay
isError={ isError }
isLoading={ isLoading }
isLoading={ false }
items={ data?.items }
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) }}
......
......@@ -19,12 +19,12 @@ const AddressLogs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>
pathParams: { hash },
scrollRef,
options: {
placeholderData: generateListStub<'address_logs'>(LOG, 3, {
placeholderData: generateListStub<'address_logs'>(LOG, 3, { next_page_params: {
block_number: 9005750,
index: 42,
items_count: 50,
transaction_index: 23,
}),
} }),
},
});
......
......@@ -6,6 +6,8 @@ import type { TokenType } from 'types/api/token';
import useIsMobile from 'lib/hooks/useIsMobile';
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 Pagination from 'ui/shared/Pagination';
import type { Props as PaginationProps } from 'ui/shared/Pagination';
......@@ -43,7 +45,9 @@ const AddressTokens = () => {
filters: { type: 'ERC-20' },
scrollRef,
options: {
refetchOnMount: false,
enabled: tokenType === 'ERC-20',
placeholderData: generateListStub<'address_tokens'>(ADDRESS_TOKEN_BALANCE_ERC_20, 10, { next_page_params: null }),
},
});
......@@ -53,7 +57,9 @@ const AddressTokens = () => {
filters: { type: 'ERC-721' },
scrollRef,
options: {
refetchOnMount: false,
enabled: tokenType === 'ERC-721',
placeholderData: generateListStub<'address_tokens'>(ADDRESS_TOKEN_BALANCE_ERC_721, 10, { next_page_params: null }),
},
});
......@@ -63,7 +69,9 @@ const AddressTokens = () => {
filters: { type: 'ERC-1155' },
scrollRef,
options: {
refetchOnMount: false,
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>}
filters: { filter: filterValue },
scrollRef,
options: {
placeholderData: generateListStub<'address_txs'>(TX, 50, {
placeholderData: generateListStub<'address_txs'>(TX, 50, { next_page_params: {
block_number: 9005713,
index: 5,
items_count: 50,
}),
} }),
},
});
......
......@@ -22,10 +22,10 @@ const AddressWithdrawals = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivE
pathParams: { hash },
scrollRef,
options: {
placeholderData: generateListStub<'address_withdrawals'>(WITHDRAWAL, 50, {
placeholderData: generateListStub<'address_withdrawals'>(WITHDRAWAL, 50, { next_page_params: {
index: 5,
items_count: 50,
}),
} }),
},
});
const content = data?.items ? (
......
import { Text, Flex } from '@chakra-ui/react';
import { Flex, Skeleton } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import { route } from 'nextjs-routes';
import React from 'react';
......@@ -14,6 +14,7 @@ import Utilization from 'ui/shared/Utilization/Utilization';
type Props = Block & {
page: number;
isLoading: boolean;
};
const AddressBlocksValidatedListItem = (props: Props) => {
......@@ -24,21 +25,31 @@ const AddressBlocksValidatedListItem = (props: Props) => {
return (
<ListItemMobile rowGap={ 2 } isAnimated>
<Flex justifyContent="space-between" w="100%">
<LinkInternal href={ blockUrl } fontWeight="700">{ props.height }</LinkInternal>
<Text variant="secondary">{ timeAgo }</Text>
<Skeleton isLoaded={ !props.isLoading } display="inline-block">
<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 columnGap={ 2 } w="100%">
<Text fontWeight={ 500 } flexShrink={ 0 }>Txn</Text>
<Text variant="secondary">{ props.tx_count }</Text>
<Skeleton isLoaded={ !props.isLoading } fontWeight={ 500 } flexShrink={ 0 }>Txn</Skeleton>
<Skeleton isLoaded={ !props.isLoading } display="inline-block" color="Skeleton_secondary">
<span>{ props.tx_count }</span>
</Skeleton>
</Flex>
<Flex columnGap={ 2 } w="100%">
<Text fontWeight={ 500 } flexShrink={ 0 }>Gas used</Text>
<Text variant="secondary">{ BigNumber(props.gas_used || 0).toFormat() }</Text>
<Utilization colorScheme="gray" value={ BigNumber(props.gas_used || 0).dividedBy(BigNumber(props.gas_limit)).toNumber() }/>
<Skeleton isLoaded={ !props.isLoading } fontWeight={ 500 } flexShrink={ 0 }>Gas used</Skeleton>
<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() }
isLoading={ props.isLoading }
/>
</Flex>
<Flex columnGap={ 2 } w="100%">
<Text fontWeight={ 500 } flexShrink={ 0 }>Reward { appConfig.network.currency.symbol }</Text>
<Text variant="secondary">{ totalReward.toFixed() }</Text>
<Skeleton isLoaded={ !props.isLoading } fontWeight={ 500 } flexShrink={ 0 }>Reward { appConfig.network.currency.symbol }</Skeleton>
<Skeleton isLoaded={ !props.isLoading } color="text_secondary">{ totalReward.toFixed() }</Skeleton>
</Flex>
</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 { route } from 'nextjs-routes';
import React from 'react';
......@@ -12,6 +12,7 @@ import Utilization from 'ui/shared/Utilization/Utilization';
type Props = Block & {
page: number;
isLoading: boolean;
};
const AddressBlocksValidatedTableItem = (props: Props) => {
......@@ -22,22 +23,36 @@ const AddressBlocksValidatedTableItem = (props: Props) => {
return (
<Tr>
<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>
<Text variant="secondary">{ timeAgo }</Text>
<Skeleton isLoaded={ !props.isLoading } color="text_secondary" display="inline-block">
<span>{ timeAgo }</span>
</Skeleton>
</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>
<Flex alignItems="center" columnGap={ 2 }>
<Box flexBasis="80px">{ BigNumber(props.gas_used || 0).toFormat() }</Box>
<Utilization colorScheme="gray" value={ BigNumber(props.gas_used || 0).dividedBy(BigNumber(props.gas_limit)).toNumber() }/>
<Skeleton isLoaded={ !props.isLoading } flexBasis="80px">
{ 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>
</Td>
<Td isNumeric display="flex" justifyContent="end">
{ totalReward.toFixed() }
<Skeleton isLoaded={ !props.isLoading } display="inline-block">
<span>{ totalReward.toFixed() }</span>
</Skeleton>
</Td>
</Tr>
);
......
......@@ -25,7 +25,7 @@ const AddressCoinBalanceChart = ({ addressHash }: Props) => {
title="Balances"
items={ items }
isLoading={ isLoading }
h="250px"
h="300px"
/>
);
};
......
......@@ -37,15 +37,25 @@ const AddressCoinBalanceHistory = ({ query }: Props) => {
</Tr>
</Thead>
<Tbody>
{ query.data.items.map((item) => (
<AddressCoinBalanceTableItem key={ item.block_number } { ...item } page={ query.pagination.page }/>
{ query.data.items.map((item, index) => (
<AddressCoinBalanceTableItem
key={ item.block_number + (query.isPlaceholderData ? String(index) : '') }
{ ...item }
page={ query.pagination.page }
isLoading={ query.isPlaceholderData }
/>
)) }
</Tbody>
</Table>
</Hide>
<Show below="lg" ssr={ false }>
{ query.data.items.map((item) => (
<AddressCoinBalanceListItem key={ item.block_number } { ...item } page={ query.pagination.page }/>
{ query.data.items.map((item, index) => (
<AddressCoinBalanceListItem
key={ item.block_number + (query.isPlaceholderData ? String(index) : '') }
{ ...item }
page={ query.pagination.page }
isLoading={ query.isPlaceholderData }
/>
)) }
</Show>
</>
......@@ -61,7 +71,7 @@ const AddressCoinBalanceHistory = ({ query }: Props) => {
<DataListDisplay
mt={ 8 }
isError={ query.isError }
isLoading={ query.isLoading }
isLoading={ false }
items={ query.data?.items }
skeletonProps={{ skeletonDesktopColumns: [ '25%', '25%', '25%', '25%', '120px' ] }}
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 { route } from 'nextjs-routes';
import React from 'react';
......@@ -15,6 +15,7 @@ import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
type Props = AddressCoinBalanceHistoryItem & {
page: number;
isLoading: boolean;
};
const AddressCoinBalanceListItem = (props: Props) => {
......@@ -26,31 +27,37 @@ const AddressCoinBalanceListItem = (props: Props) => {
return (
<ListItemMobile rowGap={ 2 } isAnimated>
<Flex justifyContent="space-between" w="100%">
<Text fontWeight={ 600 }>{ BigNumber(props.value).div(WEI).dp(8).toFormat() } { appConfig.network.currency.symbol }</Text>
<Stat flexGrow="0">
<StatHelpText display="flex" mb={ 0 } alignItems="center">
<StatArrow type={ isPositiveDelta ? 'increase' : 'decrease' }/>
<Text as="span" color={ isPositiveDelta ? 'green.500' : 'red.500' } fontWeight={ 600 }>
{ deltaBn.dp(8).toFormat() }
</Text>
</StatHelpText>
</Stat>
<Skeleton isLoaded={ !props.isLoading } fontWeight={ 600 }>
{ BigNumber(props.value).div(WEI).dp(8).toFormat() } { appConfig.network.currency.symbol }
</Skeleton>
<Skeleton isLoaded={ !props.isLoading }>
<Stat flexGrow="0">
<StatHelpText display="flex" mb={ 0 } alignItems="center">
<StatArrow type={ isPositiveDelta ? 'increase' : 'decrease' }/>
<Text as="span" color={ isPositiveDelta ? 'green.500' : 'red.500' } fontWeight={ 600 }>
{ deltaBn.dp(8).toFormat() }
</Text>
</StatHelpText>
</Stat>
</Skeleton>
</Flex>
<Flex columnGap={ 2 } w="100%">
<Text fontWeight={ 500 } flexShrink={ 0 }>Block</Text>
<LinkInternal href={ blockUrl } fontWeight="700">{ props.block_number }</LinkInternal>
<Skeleton isLoaded={ !props.isLoading } fontWeight={ 500 } flexShrink={ 0 }>Block</Skeleton>
<Skeleton isLoaded={ !props.isLoading }>
<LinkInternal href={ blockUrl } fontWeight="700">{ props.block_number }</LinkInternal>
</Skeleton>
</Flex>
{ props.transaction_hash && (
<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">
<AddressLink hash={ props.transaction_hash } type="transaction"/>
<AddressLink hash={ props.transaction_hash } type="transaction" isLoading={ props.isLoading }/>
</Address>
</Flex>
) }
<Flex columnGap={ 2 } w="100%">
<Text fontWeight={ 500 } flexShrink={ 0 }>Age</Text>
<Text variant="secondary">{ timeAgo }</Text>
<Skeleton isLoaded={ !props.isLoading } fontWeight={ 500 } flexShrink={ 0 }>Age</Skeleton>
<Skeleton isLoaded={ !props.isLoading } color="text_secondary">{ timeAgo }</Skeleton>
</Flex>
</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 { route } from 'nextjs-routes';
import React from 'react';
......@@ -13,6 +13,7 @@ import LinkInternal from 'ui/shared/LinkInternal';
type Props = AddressCoinBalanceHistoryItem & {
page: number;
isLoading: boolean;
};
const AddressCoinBalanceTableItem = (props: Props) => {
......@@ -24,32 +25,40 @@ const AddressCoinBalanceTableItem = (props: Props) => {
return (
<Tr>
<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>
{ props.transaction_hash &&
(
<Address w="150px" fontWeight="700">
<AddressLink hash={ props.transaction_hash } type="transaction"/>
<AddressLink hash={ props.transaction_hash } type="transaction" isLoading={ props.isLoading }/>
</Address>
)
}
</Td>
<Td>
<Text variant="secondary">{ timeAgo }</Text>
<Skeleton isLoaded={ !props.isLoading } color="text_secondary" display="inline-block">
<span>{ timeAgo }</span>
</Skeleton>
</Td>
<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 isNumeric display="flex" justifyContent="end">
<Stat flexGrow="0">
<StatHelpText display="flex" mb={ 0 } alignItems="center">
<StatArrow type={ isPositiveDelta ? 'increase' : 'decrease' }/>
<Text as="span" color={ isPositiveDelta ? 'green.500' : 'red.500' } fontWeight={ 600 }>
{ deltaBn.dp(8).toFormat() }
</Text>
</StatHelpText>
</Stat>
<Skeleton isLoaded={ !props.isLoading }>
<Stat flexGrow="0">
<StatHelpText display="flex" mb={ 0 } alignItems="center">
<StatArrow type={ isPositiveDelta ? 'increase' : 'decrease' }/>
<Text as="span" color={ isPositiveDelta ? 'green.500' : 'red.500' } fontWeight={ 600 }>
{ deltaBn.dp(8).toFormat() }
</Text>
</StatHelpText>
</Stat>
</Skeleton>
</Td>
</Tr>
);
......
......@@ -14,9 +14,10 @@ import TokenLogo from 'ui/shared/TokenLogo';
interface Props {
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 queryClient = useQueryClient();
......@@ -75,12 +76,14 @@ const AddressBalance = ({ data }: Props) => {
hint={ `Address balance in ${ appConfig.network.currency.symbol }. Doesn't include ERC20, ERC721 and ERC1155 tokens` }
flexWrap="nowrap"
alignItems="flex-start"
isLoading={ isLoading }
>
<TokenLogo
data={ tokenData }
boxSize={ 5 }
mr={ 2 }
fontSize="sm"
isLoading={ isLoading }
/>
<CurrencyValue
value={ data.coin_balance || '0' }
......@@ -90,6 +93,7 @@ const AddressBalance = ({ data }: Props) => {
accuracyUsd={ 2 }
accuracy={ 8 }
flexWrap="wrap"
isLoading={ isLoading }
/>
</DetailsInfoItem>
);
......
......@@ -13,6 +13,7 @@ interface Props {
query: UseQueryResult<AddressCounters>;
address: string;
onClick: () => void;
isAddressQueryLoading: boolean;
}
const PROP_TO_TAB = {
......@@ -21,8 +22,8 @@ const PROP_TO_TAB = {
validations_count: 'blocks_validated',
};
const AddressCounterItem = ({ prop, query, address, onClick }: Props) => {
if (query.isLoading) {
const AddressCounterItem = ({ prop, query, address, onClick, isAddressQueryLoading }: Props) => {
if (query.isPlaceholderData || isAddressQueryLoading) {
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 React from 'react';
......@@ -9,19 +10,23 @@ import LinkInternal from 'ui/shared/LinkInternal';
interface Props {
data: Pick<Address, 'name' | 'token' | 'is_contract'>;
isLoading: boolean;
}
const AddressNameInfo = ({ data }: Props) => {
const AddressNameInfo = ({ data, isLoading }: Props) => {
if (data.token) {
const symbol = data.token.symbol ? ` (${ trimTokenSymbol(data.token.symbol) })` : '';
return (
<DetailsInfoItem
title="Token name"
hint="Token name and symbol"
isLoading={ isLoading }
>
<LinkInternal href={ route({ pathname: '/token/[hash]', query: { hash: data.token.address } }) }>
{ data.token.name || 'Unnamed token' }{ symbol }
</LinkInternal>
<Skeleton isLoaded={ !isLoading }>
<LinkInternal href={ route({ pathname: '/token/[hash]', query: { hash: data.token.address } }) }>
{ data.token.name || 'Unnamed token' }{ symbol }
</LinkInternal>
</Skeleton>
</DetailsInfoItem>
);
}
......@@ -31,8 +36,11 @@ const AddressNameInfo = ({ data }: Props) => {
<DetailsInfoItem
title="Contract name"
hint="The name found in the source code of the Contract"
isLoading={ isLoading }
>
{ data.name }
<Skeleton isLoaded={ !isLoading }>
{ data.name }
</Skeleton>
</DetailsInfoItem>
);
}
......@@ -42,8 +50,11 @@ const AddressNameInfo = ({ data }: Props) => {
<DetailsInfoItem
title="Validator name"
hint="The name of the validator"
isLoading={ isLoading }
>
{ data.name }
<Skeleton isLoaded={ !isLoading }>
{ data.name }
</Skeleton>
</DetailsInfoItem>
);
}
......
......@@ -8,12 +8,20 @@ import AddressIntTxsListItem from 'ui/address/internals/AddressIntTxsListItem';
type Props = {
data: Array<InternalTransaction>;
currentAddress: string;
isLoading?: boolean;
}
const AddressIntTxsList = ({ data, currentAddress }: Props) => {
const AddressIntTxsList = ({ data, currentAddress, isLoading }: Props) => {
return (
<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>
);
};
......
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 { route } from 'nextjs-routes';
import React from 'react';
......@@ -11,6 +11,8 @@ import dayjs from 'lib/date/dayjs';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
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 InOutTag from 'ui/shared/InOutTag';
import LinkInternal from 'ui/shared/LinkInternal';
......@@ -18,7 +20,7 @@ import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
import TxStatus from 'ui/shared/TxStatus';
import { TX_INTERNALS_ITEMS } from 'ui/tx/internals/utils';
type Props = InternalTransaction & { currentAddress: string };
type Props = InternalTransaction & { currentAddress: string; isLoading?: boolean };
const TxInternalsListItem = ({
type,
......@@ -32,6 +34,7 @@ const TxInternalsListItem = ({
block,
timestamp,
currentAddress,
isLoading,
}: Props) => {
const typeTitle = TX_INTERNALS_ITEMS.find(({ id }) => id === type)?.title;
const toData = to ? to : createdContract;
......@@ -41,41 +44,45 @@ const TxInternalsListItem = ({
return (
<ListItemMobile rowGap={ 3 }>
<Flex>
{ typeTitle && <Tag colorScheme="cyan" mr={ 2 }>{ typeTitle }</Tag> }
<TxStatus status={ success ? 'ok' : 'error' } errorText={ error }/>
<Flex columnGap={ 2 }>
{ typeTitle && <Tag colorScheme="cyan" isLoading={ isLoading }>{ typeTitle }</Tag> }
<TxStatus status={ success ? 'ok' : 'error' } errorText={ error } isLoading={ isLoading }/>
</Flex>
<Flex justifyContent="space-between" width="100%">
<AddressLink fontWeight="700" hash={ txnHash } truncation="constant" type="transaction"/>
<Text variant="secondary" fontWeight="400" fontSize="sm">{ dayjs(timestamp).fromNow() }</Text>
<AddressLink fontWeight="700" hash={ txnHash } truncation="constant" type="transaction" isLoading={ isLoading }/>
<Skeleton isLoaded={ !isLoading } color="text_secondary" fontWeight="400" fontSize="sm">
<span>{ dayjs(timestamp).fromNow() }</span>
</Skeleton>
</Flex>
<HStack spacing={ 1 }>
<Text fontSize="sm" fontWeight={ 500 }>Block</Text>
<LinkInternal href={ route({ pathname: '/block/[height]', query: { height: block.toString() } }) }>{ block }</LinkInternal>
<Skeleton isLoaded={ !isLoading } fontSize="sm" fontWeight={ 500 }>Block</Skeleton>
<Skeleton isLoaded={ !isLoading }>
<LinkInternal href={ route({ pathname: '/block/[height]', query: { height: block.toString() } }) }>{ block }</LinkInternal>
</Skeleton>
</HStack>
<Box w="100%" display="flex" columnGap={ 3 }>
<Address width="calc((100% - 48px) / 2)">
<AddressIcon address={ from }/>
<AddressLink type="address" ml={ 2 } fontWeight="500" hash={ from.hash } isDisabled={ isOut }/>
{ isIn && <CopyToClipboard text={ from.hash }/> }
<AddressIcon address={ from } isLoading={ isLoading }/>
<AddressLink type="address" ml={ 2 } fontWeight="500" hash={ from.hash } isDisabled={ isOut } isLoading={ isLoading }/>
{ isIn && <CopyToClipboard text={ from.hash } isLoading={ isLoading }/> }
</Address>
{ (isIn || isOut) ?
<InOutTag isIn={ isIn } isOut={ isOut }/> :
<Icon as={ eastArrowIcon } boxSize={ 6 } color="gray.500"/>
<InOutTag isIn={ isIn } isOut={ isOut } isLoading={ isLoading }/> :
<Icon as={ eastArrowIcon } boxSize={ 6 } color="gray.500" isLoading={ isLoading }/>
}
{ toData && (
<Address width="calc((100% - 48px) / 2)">
<AddressIcon address={ toData }/>
<AddressLink type="address" ml={ 2 } fontWeight="500" hash={ toData.hash } isDisabled={ isIn }/>
{ isOut && <CopyToClipboard text={ toData.hash }/> }
<AddressIcon address={ toData } isLoading={ isLoading }/>
<AddressLink type="address" ml={ 2 } fontWeight="500" hash={ toData.hash } isDisabled={ isIn } isLoading={ isLoading }/>
{ isOut && <CopyToClipboard text={ toData.hash } isLoading={ isLoading }/> }
</Address>
) }
</Box>
<HStack spacing={ 3 }>
<Text fontSize="sm" fontWeight={ 500 }>Value { appConfig.network.currency.symbol }</Text>
<Text fontSize="sm" variant="secondary">
{ BigNumber(value).div(BigNumber(10 ** appConfig.network.currency.decimals)).toFormat() }
</Text>
<Skeleton isLoaded={ !isLoading } fontSize="sm" fontWeight={ 500 }>Value { appConfig.network.currency.symbol }</Skeleton>
<Skeleton isLoaded={ !isLoading } fontSize="sm" color="text_secondary" minW={ 6 }>
<span>{ BigNumber(value).div(BigNumber(10 ** appConfig.network.currency.decimals)).toFormat() }</span>
</Skeleton>
</HStack>
</ListItemMobile>
);
......
......@@ -11,9 +11,10 @@ import AddressIntTxsTableItem from './AddressIntTxsTableItem';
interface Props {
data: Array<InternalTransaction>;
currentAddress: string;
isLoading?: boolean;
}
const AddressIntTxsTable = ({ data, currentAddress }: Props) => {
const AddressIntTxsTable = ({ data, currentAddress, isLoading }: Props) => {
return (
<Table variant="simple" size="sm">
<Thead top={ 80 }>
......@@ -30,8 +31,13 @@ const AddressIntTxsTable = ({ data, currentAddress }: Props) => {
</Tr>
</Thead>
<Tbody>
{ data.map((item) => (
<AddressIntTxsTableItem key={ item.transaction_hash } { ...item } currentAddress={ currentAddress }/>
{ data.map((item, index) => (
<AddressIntTxsTableItem
key={ item.transaction_hash + '_' + index }
{ ...item }
currentAddress={ currentAddress }
isLoading={ isLoading }
/>
)) }
</Tbody>
</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 { route } from 'nextjs-routes';
import React from 'react';
......@@ -11,13 +11,15 @@ import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import Icon from 'ui/shared/chakra/Icon';
import Tag from 'ui/shared/chakra/Tag';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import InOutTag from 'ui/shared/InOutTag';
import LinkInternal from 'ui/shared/LinkInternal';
import TxStatus from 'ui/shared/TxStatus';
import { TX_INTERNALS_ITEMS } from 'ui/tx/internals/utils';
type Props = InternalTransaction & { currentAddress: string }
type Props = InternalTransaction & { currentAddress: string; isLoading?: boolean }
const AddressIntTxsTableItem = ({
type,
......@@ -31,6 +33,7 @@ const AddressIntTxsTableItem = ({
block,
timestamp,
currentAddress,
isLoading,
}: Props) => {
const typeTitle = TX_INTERNALS_ITEMS.find(({ id }) => id === type)?.title;
const toData = to ? to : createdContract;
......@@ -44,47 +47,66 @@ const AddressIntTxsTableItem = ({
<Tr alignItems="top">
<Td verticalAlign="middle">
<Flex rowGap={ 3 } flexWrap="wrap">
<AddressLink fontWeight="700" hash={ txnHash } type="transaction"/>
{ timestamp && <Text variant="secondary" fontWeight="400" fontSize="sm">{ timeAgo }</Text> }
<AddressLink fontWeight="700" hash={ txnHash } type="transaction" isLoading={ isLoading }/>
{ timestamp && (
<Skeleton isLoaded={ !isLoading } color="text_secondary" fontWeight="400" fontSize="sm">
<span>{ timeAgo }</span>
</Skeleton>
) }
</Flex>
</Td>
<Td verticalAlign="middle">
<Flex rowGap={ 2 } flexWrap="wrap">
{ typeTitle && (
<Box w="126px" display="inline-block">
<Tag colorScheme="cyan" mr={ 5 }>{ typeTitle }</Tag>
<Tag colorScheme="cyan" mr={ 5 } isLoading={ isLoading }>{ typeTitle }</Tag>
</Box>
) }
<TxStatus status={ success ? 'ok' : 'error' } errorText={ error }/>
<TxStatus status={ success ? 'ok' : 'error' } errorText={ error } isLoading={ isLoading }/>
</Flex>
</Td>
<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 verticalAlign="middle">
<Address display="inline-flex" maxW="100%">
<AddressIcon address={ from }/>
<AddressLink type="address" ml={ 2 } fontWeight="500" hash={ from.hash } alias={ from.name } flexGrow={ 1 } isDisabled={ isOut }/>
{ isIn && <CopyToClipboard text={ from.hash }/> }
<AddressIcon address={ from } isLoading={ isLoading }/>
<AddressLink
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>
</Td>
<Td px={ 0 } verticalAlign="middle">
{ (isIn || isOut) ?
<InOutTag isIn={ isIn } isOut={ isOut }/> :
<Icon as={ rightArrowIcon } boxSize={ 6 } color="gray.500"/>
<InOutTag isIn={ isIn } isOut={ isOut } isLoading={ isLoading }/> :
<Icon as={ rightArrowIcon } boxSize={ 6 } color="gray.500" isLoading={ isLoading }/>
}
</Td>
<Td verticalAlign="middle">
{ toData && (
<Address display="inline-flex" maxW="100%">
<AddressIcon address={ toData }/>
<AddressLink type="address" hash={ toData.hash } alias={ toData.name } fontWeight="500" ml={ 2 } isDisabled={ isIn }/>
{ isOut && <CopyToClipboard text={ toData.hash }/> }
<AddressIcon address={ toData } isLoading={ isLoading }/>
<AddressLink type="address" hash={ toData.hash } alias={ toData.name } fontWeight="500" ml={ 2 } isDisabled={ isIn } isLoading={ isLoading }/>
{ isOut && <CopyToClipboard text={ toData.hash } isLoading={ isLoading }/> }
</Address>
) }
</Td>
<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>
</Tr>
);
......
......@@ -4,6 +4,7 @@ import React from 'react';
import * as coinBalanceMock from 'mocks/address/coinBalanceHistory';
import * as tokensMock from 'mocks/address/tokens';
import { tokenInfoERC20a } from 'mocks/tokens/tokenInfo';
import * as socketServer from 'playwright/fixtures/socketServer';
import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
......@@ -11,7 +12,7 @@ import MockAddressPage from 'ui/address/testUtils/MockAddressPage';
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_ERC721_API_URL = buildApiUrl('address_tokens', { hash: '1' }) + '?type=ERC-721';
const TOKENS_ER1155_API_URL = buildApiUrl('address_tokens', { hash: '1' }) + '?type=ERC-1155';
......
......@@ -67,7 +67,12 @@ const TokenSelect = ({ onClick }: Props) => {
});
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;
......
import { Grid, Skeleton } from '@chakra-ui/react';
import { Grid } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query';
import React from 'react';
......@@ -22,7 +22,7 @@ type Props = {
const ERC1155Tokens = ({ tokensQuery }: Props) => {
const isMobile = useIsMobile();
const { isError, isLoading, data, pagination, isPaginationVisible } = tokensQuery;
const { isError, isPlaceholderData, data, pagination, isPaginationVisible } = tokensQuery;
const actionBar = isMobile && isPaginationVisible && (
<ActionBar mt={ -6 }>
......@@ -30,21 +30,6 @@ const ERC1155Tokens = ({ tokensQuery }: Props) => {
</ActionBar>
);
const skeleton = (
<Grid
w="100%"
columnGap={{ base: 3, lg: 6 }}
rowGap={{ base: 3, lg: 6 }}
gridTemplateColumns={{ base: 'repeat(2, calc((100% - 12px)/2))', lg: 'repeat(auto-fill, minmax(210px, 1fr))' }}
>
<Skeleton w={{ base: '100%', lg: '210px' }} h="272px"/>
<Skeleton w={{ base: '100%', lg: '210px' }} h="272px"/>
<Skeleton w={{ base: '100%', lg: '210px' }} h="272px"/>
<Skeleton w={{ base: '100%', lg: '210px' }} h="272px"/>
<Skeleton w={{ base: '100%', lg: '210px' }} h="272px"/>
</Grid>
);
const content = data?.items ? (
<Grid
w="100%"
......@@ -52,19 +37,29 @@ const ERC1155Tokens = ({ tokensQuery }: Props) => {
rowGap={{ base: 3, lg: 6 }}
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>
) : null;
return (
<DataListDisplay
isError={ isError }
isLoading={ isLoading }
isLoading={ false }
items={ data?.items }
emptyText="There are no tokens of selected type."
content={ content }
actionBar={ actionBar }
skeletonProps={{ customSkeleton: skeleton }}
skeletonProps={{ customSkeleton: null }}
/>
);
};
......
......@@ -23,7 +23,7 @@ type Props = {
const ERC20Tokens = ({ tokensQuery }: Props) => {
const isMobile = useIsMobile();
const { isError, isLoading, data, pagination, isPaginationVisible } = tokensQuery;
const { isError, isPlaceholderData, data, pagination, isPaginationVisible } = tokensQuery;
const actionBar = isMobile && isPaginationVisible && (
<ActionBar mt={ -6 }>
......@@ -33,14 +33,20 @@ const ERC20Tokens = ({ tokensQuery }: Props) => {
const content = data?.items ? (
<>
<Hide below="lg" ssr={ false }><ERC20TokensTable data={ data.items } top={ isPaginationVisible ? 72 : 0 }/></Hide>
<Show below="lg" ssr={ false }>{ data.items.map(item => <ERC20TokensListItem key={ item.token.address } { ...item }/>) }</Show></>
<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, index) => (
<ERC20TokensListItem
key={ item.token.address + (isPlaceholderData ? index : '') }
{ ...item }
isLoading={ isPlaceholderData }
/>
)) }</Show></>
) : null;
return (
<DataListDisplay
isError={ isError }
isLoading={ isLoading }
isLoading={ false }
items={ data?.items }
skeletonProps={{
isLongSkeleton: true,
......
import { Flex, HStack, Text } from '@chakra-ui/react';
import { Flex, HStack, Skeleton } from '@chakra-ui/react';
import React from 'react';
import type { AddressTokenBalance } from 'types/api/address';
......@@ -10,9 +10,9 @@ import CopyToClipboard from 'ui/shared/CopyToClipboard';
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
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(' ');
......@@ -24,28 +24,34 @@ const ERC20TokensListItem = ({ token, value }: Props) => {
return (
<ListItemMobile rowGap={ 2 }>
<Flex alignItems="center" width="100%">
<TokenLogo data={ token } boxSize={ 6 } mr={ 2 }/>
<AddressLink fontWeight="700" hash={ token.address } type="token" alias={ tokenString }/>
<TokenLogo data={ token } boxSize={ 6 } mr={ 2 } isLoading={ isLoading }/>
<AddressLink fontWeight="700" hash={ token.address } type="token" alias={ tokenString } isLoading={ isLoading }/>
</Flex>
<Flex alignItems="center" pl={ 8 }>
<AddressLink hash={ token.address } type="address" truncation="constant"/>
<CopyToClipboard text={ token.address } ml={ 1 }/>
<AddressAddToWallet token={ token } ml={ 2 }/>
<AddressLink hash={ token.address } type="address" truncation="constant" isLoading={ isLoading }/>
<CopyToClipboard text={ token.address } isLoading={ isLoading }/>
<AddressAddToWallet token={ token } ml={ 2 } isLoading={ isLoading }/>
</Flex>
{ token.exchange_rate !== undefined && token.exchange_rate !== null && (
<HStack spacing={ 3 }>
<Text fontSize="sm" fontWeight={ 500 }>Price</Text>
<Text fontSize="sm" variant="secondary">{ `$${ token.exchange_rate }` }</Text>
<Skeleton isLoaded={ !isLoading } fontSize="sm" fontWeight={ 500 }>Price</Skeleton>
<Skeleton isLoaded={ !isLoading } fontSize="sm" color="text_secondary">
<span>{ `$${ token.exchange_rate }` }</span>
</Skeleton>
</HStack>
) }
<HStack spacing={ 3 }>
<Text fontSize="sm" fontWeight={ 500 }>Quantity</Text>
<Text fontSize="sm" variant="secondary">{ tokenQuantity }</Text>
<Skeleton isLoaded={ !isLoading } fontSize="sm" fontWeight={ 500 }>Quantity</Skeleton>
<Skeleton isLoaded={ !isLoading } fontSize="sm" color="text_secondary">
<span>{ tokenQuantity }</span>
</Skeleton>
</HStack>
{ tokenValue !== undefined && (
<HStack spacing={ 3 }>
<Text fontSize="sm" fontWeight={ 500 }>Value</Text>
<Text fontSize="sm" variant="secondary">{ tokenValue }</Text>
<Skeleton isLoaded={ !isLoading } fontSize="sm" fontWeight={ 500 }>Value</Skeleton>
<Skeleton isLoaded={ !isLoading } fontSize="sm" color="text_secondary">
<span>{ tokenValue }</span>
</Skeleton>
</HStack>
) }
</ListItemMobile>
......
......@@ -10,9 +10,10 @@ import ERC20TokensTableItem from './ERC20TokensTableItem';
interface Props {
data: Array<AddressTokenBalance>;
top: number;
isLoading: boolean;
}
const ERC20TokensTable = ({ data, top }: Props) => {
const ERC20TokensTable = ({ data, top, isLoading }: Props) => {
return (
<Table variant="simple" size="sm">
<Thead top={ top }>
......@@ -25,8 +26,8 @@ const ERC20TokensTable = ({ data, top }: Props) => {
</Tr>
</Thead>
<Tbody>
{ data.map((item) => (
<ERC20TokensTableItem key={ item.token.address } { ...item }/>
{ data.map((item, index) => (
<ERC20TokensTableItem key={ item.token.address + (isLoading ? index : '') } { ...item } isLoading={ isLoading }/>
)) }
</Tbody>
</Table>
......
import { Tr, Td, Flex } from '@chakra-ui/react';
import { Tr, Td, Flex, Skeleton } from '@chakra-ui/react';
import React from 'react';
import type { AddressTokenBalance } from 'types/api/address';
......@@ -9,11 +9,12 @@ import AddressLink from 'ui/shared/address/AddressLink';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import TokenLogo from 'ui/shared/TokenLogo';
type Props = AddressTokenBalance;
type Props = AddressTokenBalance & { isLoading: boolean };
const ERC20TokensTableItem = ({
token,
value,
isLoading,
}: Props) => {
const tokenString = [ token.name, token.symbol && `(${ token.symbol })` ].filter(Boolean).join(' ');
......@@ -27,27 +28,33 @@ const ERC20TokensTableItem = ({
<Tr>
<Td verticalAlign="middle">
<Flex alignItems="center">
<TokenLogo data={ token } boxSize={ 6 } mr={ 2 }/>
<AddressLink fontWeight="700" hash={ token.address } type="token" alias={ tokenString }/>
<TokenLogo data={ token } boxSize={ 6 } mr={ 2 } isLoading={ isLoading }/>
<AddressLink fontWeight="700" hash={ token.address } type="token" alias={ tokenString } isLoading={ isLoading }/>
</Flex>
</Td>
<Td verticalAlign="middle">
<Flex alignItems="center" width="150px" justifyContent="space-between">
<Flex alignItems="center">
<AddressLink hash={ token.address } type="address" truncation="constant"/>
<CopyToClipboard text={ token.address } ml={ 1 }/>
<AddressLink hash={ token.address } type="address" truncation="constant" isLoading={ isLoading }/>
<CopyToClipboard text={ token.address } isLoading={ isLoading }/>
</Flex>
<AddressAddToWallet token={ token } ml={ 4 }/>
<AddressAddToWallet token={ token } ml={ 4 } isLoading={ isLoading }/>
</Flex>
</Td>
<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 isNumeric verticalAlign="middle">
{ tokenQuantity }
<Skeleton isLoaded={ !isLoading } display="inline">
{ tokenQuantity }
</Skeleton>
</Td>
<Td isNumeric verticalAlign="middle">
{ tokenValue && `$${ tokenValue }` }
<Skeleton isLoaded={ !isLoading } display="inline">
{ tokenValue && `$${ tokenValue }` }
</Skeleton>
</Td>
</Tr>
);
......
......@@ -23,7 +23,7 @@ type Props = {
const ERC721Tokens = ({ tokensQuery }: Props) => {
const isMobile = useIsMobile();
const { isError, isLoading, data, pagination, isPaginationVisible } = tokensQuery;
const { isError, isPlaceholderData, data, pagination, isPaginationVisible } = tokensQuery;
const actionBar = isMobile && isPaginationVisible && (
<ActionBar mt={ -6 }>
......@@ -33,14 +33,20 @@ const ERC721Tokens = ({ tokensQuery }: Props) => {
const content = data?.items ? (
<>
<Hide below="lg" ssr={ false }><ERC721TokensTable data={ data.items } top={ isPaginationVisible ? 72 : 0 }/></Hide>
<Show below="lg" ssr={ false }>{ data.items.map(item => <ERC721TokensListItem key={ item.token.address } { ...item }/>) }</Show></>
<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, index) => (
<ERC721TokensListItem
key={ item.token.address + (isPlaceholderData ? index : '') }
{ ...item }
isLoading={ isPlaceholderData }
/>
)) }</Show></>
) : null;
return (
<DataListDisplay
isError={ isError }
isLoading={ isLoading }
isLoading={ false }
items={ data?.items }
skeletonProps={{
isLongSkeleton: true,
......
import { Flex, HStack, Text } from '@chakra-ui/react';
import { Flex, HStack, Skeleton } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';
......@@ -10,9 +10,9 @@ import CopyToClipboard from 'ui/shared/CopyToClipboard';
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
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 hash = router.query.hash?.toString() || '';
......@@ -22,17 +22,19 @@ const ERC721TokensListItem = ({ token, value }: Props) => {
return (
<ListItemMobile rowGap={ 2 }>
<Flex alignItems="center" width="100%">
<TokenLogo data={ token } boxSize={ 6 } mr={ 2 }/>
<AddressLink fontWeight="700" hash={ hash } tokenHash={ token.address } type="address_token" alias={ tokenString }/>
<TokenLogo data={ token } boxSize={ 6 } mr={ 2 } isLoading={ isLoading }/>
<AddressLink fontWeight="700" hash={ hash } tokenHash={ token.address } type="address_token" alias={ tokenString } isLoading={ isLoading }/>
</Flex>
<Flex alignItems="center" pl={ 8 }>
<AddressLink hash={ token.address } type="address" truncation="constant"/>
<CopyToClipboard text={ token.address } ml={ 1 }/>
<AddressAddToWallet token={ token } ml={ 2 }/>
<AddressLink hash={ token.address } type="address" truncation="constant" isLoading={ isLoading }/>
<CopyToClipboard text={ token.address } isLoading={ isLoading }/>
<AddressAddToWallet token={ token } ml={ 2 } isLoading={ isLoading }/>
</Flex>
<HStack spacing={ 3 }>
<Text fontSize="sm" fontWeight={ 500 }>Quantity</Text>
<Text fontSize="sm" variant="secondary">{ value }</Text>
<Skeleton isLoaded={ !isLoading } fontSize="sm" fontWeight={ 500 }>Quantity</Skeleton>
<Skeleton isLoaded={ !isLoading } fontSize="sm" color="text_secondary">
<span>{ value }</span>
</Skeleton>
</HStack>
</ListItemMobile>
);
......
......@@ -10,9 +10,10 @@ import ERC721TokensTableItem from './ERC721TokensTableItem';
interface Props {
data: Array<AddressTokenBalance>;
top: number;
isLoading: boolean;
}
const ERC721TokensTable = ({ data, top }: Props) => {
const ERC721TokensTable = ({ data, top, isLoading }: Props) => {
return (
<Table variant="simple" size="sm">
<Thead top={ top }>
......@@ -23,8 +24,8 @@ const ERC721TokensTable = ({ data, top }: Props) => {
</Tr>
</Thead>
<Tbody>
{ data.map((item) => (
<ERC721TokensTableItem key={ item.token.address } { ...item }/>
{ data.map((item, index) => (
<ERC721TokensTableItem key={ item.token.address + (isLoading ? index : '') } { ...item } isLoading={ isLoading }/>
)) }
</Tbody>
</Table>
......
import { Tr, Td, Flex } from '@chakra-ui/react';
import { Tr, Td, Flex, Skeleton } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';
......@@ -9,11 +9,12 @@ import AddressLink from 'ui/shared/address/AddressLink';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import TokenLogo from 'ui/shared/TokenLogo';
type Props = AddressTokenBalance;
type Props = AddressTokenBalance & { isLoading: boolean};
const ERC721TokensTableItem = ({
token,
value,
isLoading,
}: Props) => {
const router = useRouter();
......@@ -24,21 +25,23 @@ const ERC721TokensTableItem = ({
<Tr>
<Td verticalAlign="middle">
<Flex alignItems="center">
<TokenLogo data={ token } boxSize={ 6 } mr={ 2 }/>
<AddressLink fontWeight="700" hash={ hash } tokenHash={ token.address } type="address_token" alias={ tokenString }/>
<TokenLogo data={ token } boxSize={ 6 } mr={ 2 } isLoading={ isLoading }/>
<AddressLink fontWeight="700" hash={ hash } tokenHash={ token.address } type="address_token" alias={ tokenString } isLoading={ isLoading }/>
</Flex>
</Td>
<Td verticalAlign="middle">
<Flex alignItems="center" width="150px" justifyContent="space-between">
<Flex alignItems="center">
<AddressLink hash={ token.address } type="address" truncation="dynamic"/>
<CopyToClipboard text={ token.address } ml={ 1 }/>
<AddressLink hash={ token.address } type="address" truncation="dynamic" isLoading={ isLoading }/>
<CopyToClipboard text={ token.address } isLoading={ isLoading }/>
</Flex>
<AddressAddToWallet token={ token } ml={ 4 }/>
<AddressAddToWallet token={ token } ml={ 4 } isLoading={ isLoading }/>
</Flex>
</Td>
<Td isNumeric verticalAlign="middle">
{ value }
<Skeleton isLoaded={ !isLoading } display="inline-block">
{ value }
</Skeleton>
</Td>
</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 React from 'react';
......@@ -8,9 +8,9 @@ import NftMedia from 'ui/shared/nft/NftMedia';
import TokenLogo from 'ui/shared/TokenLogo';
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 } });
return (
......@@ -25,33 +25,40 @@ const NFTItem = ({ token, token_id: tokenId, token_instance: tokenInstance }: Pr
fontWeight={ 500 }
lineHeight="20px"
>
<LinkOverlay href={ tokenLink }>
<LinkOverlay href={ isLoading ? undefined : tokenLink }>
<NftMedia
mb="18px"
imageUrl={ tokenInstance?.image_url || null }
animationUrl={ tokenInstance?.animation_url || null }
isLoading={ isLoading }
/>
</LinkOverlay>
{ tokenId && (
<Flex mb={ 2 } ml={ 1 }>
<Text whiteSpace="pre" variant="secondary">ID# </Text>
<TruncatedTextTooltip label={ tokenId }>
<Link
overflow="hidden"
whiteSpace="nowrap"
textOverflow="ellipsis"
href={ route({ pathname: '/token/[hash]/instance/[id]', query: { hash: token.address, id: tokenId } }) }
>
{ tokenId }
</Link>
<Skeleton isLoaded={ !isLoading } overflow="hidden" h="20px">
<Link
w="100%"
display="inline-block"
whiteSpace="nowrap"
textOverflow="ellipsis"
overflow="hidden"
href={ route({ pathname: '/token/[hash]/instance/[id]', query: { hash: token.address, id: tokenId } }) }
>
{ tokenId }
</Link>
</Skeleton>
</TruncatedTextTooltip>
</Flex>
) }
{ token.name && (
<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 }>
<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>
</Flex>
) }
......
import { Flex, Skeleton } from '@chakra-ui/react';
import { Flex } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';
......@@ -19,7 +19,7 @@ const TokenBalances = () => {
const addressQuery = useApiQuery('address', {
pathParams: { hash },
queryOptions: { enabled: Boolean(hash) },
queryOptions: { enabled: Boolean(hash), refetchOnMount: false },
});
const tokenQuery = useFetchTokens({ hash });
......@@ -28,23 +28,12 @@ const TokenBalances = () => {
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 { valueStr: nativeValue, usdBn: nativeUsd } = getCurrencyValue({
value: addressData.coin_balance || '0',
value: addressData?.coin_balance || '0',
accuracy: 8,
accuracyUsd: 2,
exchangeRate: addressData.exchange_rate,
exchangeRate: addressData?.exchange_rate,
decimals: String(appConfig.network.currency.decimals),
});
......@@ -57,10 +46,15 @@ const TokenBalances = () => {
return (
<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
name={ `${ appConfig.network.currency.symbol } Balance` }
value={ (!nativeUsd.eq(ZERO) ? `$${ nativeUsd.toFormat(2) } USD | ` : '') + `${ nativeValue } ${ appConfig.network.currency.symbol }` }
isLoading={ addressQuery.isLoading || tokenQuery.isLoading }
/>
<TokenBalancesItem
name="Tokens"
......@@ -68,9 +62,10 @@ const TokenBalances = () => {
`${ prefix }$${ tokensInfo.usd.toFormat(2) } USD ` +
tokensNumText
}
isLoading={ addressQuery.isLoading || tokenQuery.isLoading }
/>
</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 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');
......@@ -12,7 +12,7 @@ const TokenBalancesItem = ({ name, value }: {name: string; value: string }) => {
<Icon as={ walletIcon } boxSize="30px" mr={ 3 }/>
<Box>
<Text variant="secondary" fontSize="xs">{ name }</Text>
<Text fontWeight="500">{ value }</Text>
<Skeleton isLoaded={ !isLoading } fontWeight="500">{ value }</Skeleton>
</Box>
</Flex>
);
......
import { Flex, Tag, Text, HStack } from '@chakra-ui/react';
import { Flex, HStack, Skeleton } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import React from 'react';
......@@ -8,18 +8,22 @@ import appConfig from 'configs/app/config';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import Tag from 'ui/shared/chakra/Tag';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
type Props = {
item: AddressesItem;
index: number;
totalSupply: string;
isLoading?: boolean;
}
const AddressesListItem = ({
item,
index,
totalSupply,
isLoading,
}: Props) => {
const addressBalance = BigNumber(item.coin_balance).div(BigNumber(10 ** appConfig.network.currency.decimals));
......@@ -28,7 +32,7 @@ const AddressesListItem = ({
<ListItemMobile rowGap={ 3 }>
<Flex alignItems="center" justifyContent="space-between" w="100%">
<Address maxW="100%" mr={ 8 }>
<AddressIcon address={ item } mr={ 2 }/>
<AddressIcon address={ item } mr={ 2 } isLoading={ isLoading }/>
<AddressLink
fontWeight={ 700 }
flexGrow={ 1 }
......@@ -36,26 +40,36 @@ const AddressesListItem = ({
hash={ item.hash }
alias={ item.name }
type="address"
isLoading={ isLoading }
/>
<CopyToClipboard text={ item.hash } isLoading={ isLoading }/>
</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>
{ 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 }>
<Text fontSize="sm" fontWeight={ 500 }>{ `Balance ${ appConfig.network.currency.symbol }` }</Text>
<Text fontSize="sm" variant="secondary">{ addressBalance.dp(8).toFormat() }</Text>
<Skeleton isLoaded={ !isLoading } fontSize="sm" fontWeight={ 500 }>{ `Balance ${ appConfig.network.currency.symbol }` }</Skeleton>
<Skeleton isLoaded={ !isLoading } fontSize="sm" color="text_secondary">
<span>{ addressBalance.dp(8).toFormat() }</span>
</Skeleton>
</HStack>
{ totalSupply && totalSupply !== '0' && (
<HStack spacing={ 3 }>
<Text fontSize="sm" fontWeight={ 500 }>Percentage</Text>
<Text fontSize="sm" variant="secondary">{ addressBalance.div(BigNumber(totalSupply)).multipliedBy(100).dp(8).toFormat() + '%' }</Text>
<Skeleton isLoaded={ !isLoading } fontSize="sm" fontWeight={ 500 }>Percentage</Skeleton>
<Skeleton isLoaded={ !isLoading } fontSize="sm" color="text_secondary">
<span>{ addressBalance.div(BigNumber(totalSupply)).multipliedBy(100).dp(8).toFormat() + '%' }</span>
</Skeleton>
</HStack>
) }
<HStack spacing={ 3 }>
<Text fontSize="sm" fontWeight={ 500 }>Txn count</Text>
<Text fontSize="sm" variant="secondary">{ Number(item.tx_count).toLocaleString() }</Text>
<Skeleton isLoaded={ !isLoading } fontSize="sm" fontWeight={ 500 }>Txn count</Skeleton>
<Skeleton isLoaded={ !isLoading } fontSize="sm" color="text_secondary">
<span>{ Number(item.tx_count).toLocaleString() }</span>
</Skeleton>
</HStack>
</ListItemMobile>
);
......
......@@ -13,9 +13,10 @@ interface Props {
totalSupply: string;
pageStartIndex: 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');
return (
<Table variant="simple" size="sm">
......@@ -32,11 +33,12 @@ const AddressesTable = ({ items, totalSupply, pageStartIndex, top }: Props) => {
<Tbody>
{ items.map((item, index) => (
<AddressesTableItem
key={ item.hash }
key={ item.hash + (isLoading ? index : '') }
item={ item }
totalSupply={ totalSupply }
index={ pageStartIndex + index }
hasPercentage={ hasPercentage }
isLoading={ isLoading }
/>
)) }
</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 React from 'react';
......@@ -8,12 +8,15 @@ import appConfig from 'configs/app/config';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import Tag from 'ui/shared/chakra/Tag';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
type Props = {
item: AddressesItem;
index: number;
totalSupply: string;
hasPercentage: boolean;
isLoading?: boolean;
}
const AddressesTableItem = ({
......@@ -21,6 +24,7 @@ const AddressesTableItem = ({
index,
totalSupply,
hasPercentage,
isLoading,
}: Props) => {
const addressBalance = BigNumber(item.coin_balance).div(BigNumber(10 ** appConfig.network.currency.decimals));
......@@ -29,11 +33,13 @@ const AddressesTableItem = ({
return (
<Tr>
<Td>
<Text lineHeight="24px">{ index }</Text>
<Skeleton isLoaded={ !isLoading } display="inline-block" minW={ 6 } lineHeight="24px">
{ index }
</Skeleton>
</Td>
<Td>
<Address display="inline-flex" maxW="100%">
<AddressIcon address={ item } mr={ 2 }/>
<AddressIcon address={ item } mr={ 2 } isLoading={ isLoading }/>
<AddressLink
fontWeight={ 700 }
flexGrow={ 1 }
......@@ -41,18 +47,22 @@ const AddressesTableItem = ({
hash={ item.hash }
alias={ item.name }
type="address"
isLoading={ isLoading }
/>
<CopyToClipboard text={ item.hash } isLoading={ isLoading }/>
</Address>
</Td>
<Td pl={ 10 }>
{ item.public_tags && item.public_tags.length ? item.public_tags.map(tag => (
<Tag key={ tag.label }>{ tag.display_name }</Tag>
)) : <Text lineHeight="24px">-</Text> }
<Tag key={ tag.label } isLoading={ isLoading } isTruncated>{ tag.display_name }</Tag>
)) : null }
</Td>
<Td isNumeric>
<Text lineHeight="24px" as="span">{ addressBalanceChunks[0] }</Text>
{ addressBalanceChunks[1] && <Text lineHeight="24px" as="span">.</Text> }
<Text lineHeight="24px" variant="secondary" as="span">{ addressBalanceChunks[1] }</Text>
<Skeleton isLoaded={ !isLoading } display="inline-block">
<Text lineHeight="24px" as="span">{ addressBalanceChunks[0] }</Text>
{ addressBalanceChunks[1] && <Text lineHeight="24px" as="span">.</Text> }
<Text lineHeight="24px" variant="secondary" as="span">{ addressBalanceChunks[1] }</Text>
</Skeleton>
</Td>
{ hasPercentage && (
<Td isNumeric>
......@@ -60,7 +70,9 @@ const AddressesTableItem = ({
</Td>
) }
<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>
</Tr>
);
......
......@@ -28,11 +28,7 @@ const addresses: AddressesResponse = {
},
],
total_supply: '25222000',
next_page_params: {
items_count: 50,
fetched_coin_balance: '123',
hash: 'aa',
},
next_page_params: null,
};
test('base view +@mobile +@dark-mode', async({ mount, page }) => {
......@@ -51,5 +47,5 @@ test('base view +@mobile +@dark-mode', async({ mount, page }) => {
</TestApp>,
);
await expect(component.locator('main')).toHaveScreenshot();
await expect(component).toHaveScreenshot();
});
......@@ -2,19 +2,34 @@ import { Hide, Show } from '@chakra-ui/react';
import React from 'react';
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import { TOP_ADDRESS } from 'stubs/address';
import { generateListStub } from 'stubs/utils';
import AddressesListItem from 'ui/addresses/AddressesListItem';
import AddressesTable from 'ui/addresses/AddressesTable';
import ActionBar from 'ui/shared/ActionBar';
import DataListDisplay from 'ui/shared/DataListDisplay';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle';
import Pagination from 'ui/shared/Pagination';
const PAGE_SIZE = 50;
const Accounts = () => {
const { isError, isLoading, data, isPaginationVisible, pagination } = useQueryWithPages({
const { isError, isPlaceholderData, data, isPaginationVisible, pagination } = useQueryWithPages({
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 && (
......@@ -32,16 +47,18 @@ const Accounts = () => {
items={ data.items }
totalSupply={ data.total_supply }
pageStartIndex={ pageStartIndex }
isLoading={ isPlaceholderData }
/>
</Hide>
<Show below="lg" ssr={ false }>
{ data.items.map((item, index) => {
return (
<AddressesListItem
key={ item.hash }
key={ item.hash + (isPlaceholderData ? index : '') }
item={ item }
index={ pageStartIndex + index }
totalSupply={ data.total_supply }
isLoading={ isPlaceholderData }
/>
);
}) }
......@@ -50,18 +67,18 @@ const Accounts = () => {
) : null;
return (
<Page>
<>
<PageTitle title="Top accounts" withTextAd/>
<DataListDisplay
isError={ isError }
isLoading={ isLoading }
isLoading={ false }
items={ data?.items }
skeletonProps={{ skeletonDesktopColumns: [ '64px', '30%', '20%', '20%', '15%', '15%' ] }}
emptyText="There are no accounts."
content={ content }
actionBar={ actionBar }
/>
</Page>
</>
);
};
......
......@@ -12,6 +12,7 @@ import { useAppContext } from 'lib/appContext';
import useContractTabs from 'lib/hooks/useContractTabs';
import useIsMobile from 'lib/hooks/useIsMobile';
import getQueryParamString from 'lib/router/getQueryParamString';
import { ADDRESS_INFO } from 'stubs/address';
import AddressBlocksValidated from 'ui/address/AddressBlocksValidated';
import AddressCoinBalance from 'ui/address/AddressCoinBalance';
import AddressContract from 'ui/address/AddressContract';
......@@ -25,7 +26,6 @@ import AddressWithdrawals from 'ui/address/AddressWithdrawals';
import TextAd from 'ui/shared/ad/TextAd';
import EntityTags from 'ui/shared/EntityTags';
import NetworkExplorers from 'ui/shared/NetworkExplorers';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle';
import SkeletonTabs from 'ui/shared/skeletons/SkeletonTabs';
import RoutedTabs from 'ui/shared/Tabs/RoutedTabs';
......@@ -48,7 +48,10 @@ const AddressPageContent = () => {
const addressQuery = useApiQuery('address', {
pathParams: { hash },
queryOptions: { enabled: Boolean(hash) },
queryOptions: {
enabled: Boolean(hash),
placeholderData: ADDRESS_INFO,
},
});
const contractTabs = useContractTabs(addressQuery.data);
......@@ -120,23 +123,20 @@ const AddressPageContent = () => {
}, [ appProps.referrer ]);
return (
<Page>
{ addressQuery.isLoading ? <Skeleton h={{ base: 12, lg: 6 }} mb={ 6 } w="100%" maxW="680px"/> : <TextAd mb={ 6 }/> }
{ addressQuery.isLoading ? (
<Skeleton h={ 10 } w="260px" mb={ 6 }/>
) : (
<PageTitle
title={ `${ addressQuery.data?.is_contract ? 'Contract' : 'Address' } details` }
backLink={ backLink }
contentAfter={ tags }
/>
) }
<>
{ addressQuery.isPlaceholderData ? <Skeleton h={{ base: 12, lg: 6 }} mb={ 6 } w="100%" maxW="680px"/> : <TextAd mb={ 6 }/> }
<PageTitle
title={ `${ addressQuery.data?.is_contract ? 'Contract' : 'Address' } details` }
backLink={ backLink }
contentAfter={ tags }
isLoading={ addressQuery.isPlaceholderData }
/>
<AddressDetails addressQuery={ addressQuery } scrollRef={ tabsScrollRef }/>
{ /* should stay before tabs to scroll up with pagination */ }
<Box ref={ tabsScrollRef }></Box>
{ addressQuery.isLoading ? <SkeletonTabs/> : content }
{ !addressQuery.isLoading && !addressQuery.isError && <Box h={{ base: 0, lg: '40vh' }}/> }
</Page>
{ addressQuery.isPlaceholderData ? <SkeletonTabs tabs={ tabs }/> : content }
{ !addressQuery.isPlaceholderData && !addressQuery.isError && <Box h={{ base: 0, lg: '40vh' }}/> }
</>
);
};
......
......@@ -50,11 +50,11 @@ const BlockPageContent = () => {
pathParams: { height },
options: {
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,
index: 49,
items_count: 50,
}),
} }),
},
});
......@@ -63,10 +63,10 @@ const BlockPageContent = () => {
pathParams: { height },
options: {
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,
items_count: 50,
}),
} }),
},
});
......
......@@ -34,10 +34,10 @@ const BlocksPageContent = () => {
resourceName: 'blocks',
filters: { type },
options: {
placeholderData: generateListStub<'blocks'>(BLOCK, 50, {
placeholderData: generateListStub<'blocks'>(BLOCK, 50, { next_page_params: {
block_number: 8988686,
items_count: 50,
}),
} }),
},
});
......
......@@ -12,14 +12,14 @@ import useStats from '../stats/useStats';
const Stats = () => {
const {
isLoading,
isPlaceholderData,
isError,
sections,
currentSection,
handleSectionChange,
interval,
handleIntervalChange,
debounceFilterCharts,
handleFilterChange,
displayedCharts,
filterQuery,
} = useStats();
......@@ -39,14 +39,14 @@ const Stats = () => {
onSectionChange={ handleSectionChange }
interval={ interval }
onIntervalChange={ handleIntervalChange }
onFilterInputChange={ debounceFilterCharts }
onFilterInputChange={ handleFilterChange }
/>
</Box>
<ChartsWidgetsList
filterQuery={ filterQuery }
isError={ isError }
isLoading={ isLoading }
isPlaceholderData={ isPlaceholderData }
charts={ displayedCharts }
interval={ interval }
/>
......
......@@ -136,7 +136,7 @@ const TokenPageContent = () => {
scrollRef,
options: {
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 = () => {
scrollRef,
options: {
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 Page from 'ui/shared/Page/Page';
import TokenInstanceContent from 'ui/tokenInstance/TokenInstanceContent';
import type { RoutedTab } from 'ui/shared/Tabs/types';
import nftIcon from 'icons/nft_shield.svg';
import useApiQuery from 'lib/api/useApiQuery';
import { useAppContext } from 'lib/appContext';
import useIsMobile from 'lib/hooks/useIsMobile';
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import { 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'
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 (
<Page>
<TokenInstanceContent/>
</Page>
<>
<TextAd mb={ 6 }/>
<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 Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle';
import TokensList from 'ui/tokens/Tokens';
const Tokens = () => {
return (
<Page>
<>
<PageTitle title="Tokens" withTextAd/>
<TokensList/>
</Page>
</>
);
};
......
......@@ -31,12 +31,12 @@ const Transactions = () => {
filters: { filter: router.query.tab === 'pending' ? 'pending' : 'validated' },
options: {
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,
index: 5,
items_count: 50,
filter: 'validated',
}),
} }),
},
});
......@@ -44,6 +44,11 @@ const Transactions = () => {
resourceName: 'txs_watchlist',
options: {
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 = () => {
<RoutedTabs
tabs={ tabs }
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 }
/>
</>
......
......@@ -21,10 +21,10 @@ const Withdrawals = () => {
const { data, isError, isPlaceholderData, isPaginationVisible, pagination } = useQueryWithPages({
resourceName: 'withdrawals',
options: {
placeholderData: generateListStub<'withdrawals'>(WITHDRAWAL, 50, {
placeholderData: generateListStub<'withdrawals'>(WITHDRAWAL, 50, { next_page_params: {
index: 5,
items_count: 50,
}),
} }),
},
});
......
......@@ -24,7 +24,7 @@ const BackLink = (props: BackLinkProp & { isLoading?: boolean }) => {
}
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"/>;
......@@ -64,7 +64,7 @@ const PageTitle = ({ title, contentAfter, withTextAd, backLink, className, isLoa
{ beforeTitle }
<Skeleton
isLoaded={ !isLoading }
display="inline"
display={ isLoading ? 'inline-block' : 'inline' }
verticalAlign={ isLoading ? 'super' : undefined }
>
<Heading
......
......@@ -7,6 +7,7 @@ import React, { useEffect, useRef } from 'react';
import type { RoutedTab } from './types';
import TabsWithScroll from './TabsWithScroll';
import useTabIndexFromQuery from './useTabIndexFromQuery';
interface Props extends ThemingProps<'Tabs'> {
tabs: Array<RoutedTab>;
......@@ -18,16 +19,7 @@ interface Props extends ThemingProps<'Tabs'> {
const RoutedTabs = ({ tabs, tabListProps, rightSlot, stickyEnabled, className, ...themeProps }: Props) => {
const router = useRouter();
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 tabIndex = useTabIndexFromQuery(tabs);
const tabsRef = useRef<HTMLDivElement>(null);
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 type { TokenInfo } from 'types/api/token';
......@@ -21,7 +21,11 @@ const TokenSnippet = ({ data, className, logoSize = 6, isDisabled, hideSymbol, i
<Flex className={ className } alignItems="center" columnGap={ 2 } w="100%">
<TokenLogo boxSize={ logoSize } data={ data } 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>
);
};
......
......@@ -55,7 +55,7 @@ const CoinzillaTextAd = ({ className }: {className?: string}) => {
}
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) {
......
import { Box, chakra, Icon, Tooltip } from '@chakra-ui/react';
import { Box, chakra, Icon, Skeleton, Tooltip } from '@chakra-ui/react';
import React from 'react';
import type { TokenInfo } from 'types/api/token';
......@@ -11,9 +11,10 @@ import { WALLETS_INFO } from 'lib/web3/wallets';
interface Props {
className?: string;
token: TokenInfo;
isLoading?: boolean;
}
const AddressAddToWallet = ({ className, token }: Props) => {
const AddressAddToWallet = ({ className, token, isLoading }: Props) => {
const toast = useToast();
const provider = useProvider();
......@@ -59,6 +60,10 @@ const AddressAddToWallet = ({ className, token }: Props) => {
return null;
}
if (isLoading) {
return <Skeleton className={ className } boxSize={ 6 } borderRadius="base"/>;
}
const defaultWallet = appConfig.web3.defaultWallet;
return (
......
......@@ -57,7 +57,7 @@ test('base view +@dark-mode', async({ mount, page }) => {
test('loading', async({ mount }) => {
const component = await mount(
<TestApp>
<ChartWidget { ...props } isLoading/>
<ChartWidget { ...props } isLoading minH="250px"/>
</TestApp>,
);
......
......@@ -3,13 +3,13 @@ import {
Center,
chakra,
Flex,
Grid,
Icon,
IconButton, Link,
Menu,
MenuButton,
MenuItem,
MenuList,
Skeleton,
Text,
Tooltip,
useColorModeValue,
......@@ -30,7 +30,6 @@ import { apos } from 'lib/html-entities';
import saveAsCSV from 'lib/saveAsCSV';
import ChartWidgetGraph from './ChartWidgetGraph';
import ChartWidgetSkeleton from './ChartWidgetSkeleton';
import FullscreenChartModal from './FullscreenChartModal';
export type Props = {
......@@ -110,10 +109,6 @@ const ChartWidget = ({ items, title, description, isLoading, className, isError,
}
}, [ items, title ]);
if (isLoading) {
return <ChartWidgetSkeleton hasDescription={ Boolean(description) }/>;
}
const hasItems = items && items.length > 2;
const content = (() => {
......@@ -137,6 +132,10 @@ const ChartWidget = ({ items, title, description, isLoading, className, isError,
);
}
if (isLoading) {
return <Skeleton flexGrow={ 1 } w="100%"/>;
}
if (!hasItems) {
return (
<Center flexGrow={ 1 }>
......@@ -146,7 +145,7 @@ const ChartWidget = ({ items, title, description, isLoading, className, isError,
}
return (
<Box h="100%" maxW="100%">
<Box flexGrow={ 1 } maxW="100%">
<ChartWidgetGraph
items={ items }
onZoom={ handleZoom }
......@@ -160,112 +159,104 @@ const ChartWidget = ({ items, title, description, isLoading, className, isError,
return (
<>
<Box
<Flex
height="100%"
display="flex"
flexDirection="column"
ref={ ref }
flexDir="column"
padding={{ base: 3, lg: 4 }}
borderRadius="md"
border="1px"
borderColor={ borderColor }
className={ className }
>
<Grid
gridTemplateColumns="auto auto 36px"
gridColumnGap={ 2 }
>
<Text
fontWeight={ 600 }
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"
<Flex columnGap={ 6 } mb={ 1 } alignItems="flex-start">
<Flex flexGrow={ 1 } flexDir="column" alignItems="flex-start">
<Skeleton
isLoaded={ !isLoading }
fontWeight={ 600 }
size={{ base: 'xs', lg: 'sm' }}
>
{ description }
</Text>
) }
<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>
{ title }
</Skeleton>
{ hasItems && (
<Menu>
<MenuButton
gridColumn={ 3 }
gridRow="1/3"
justifySelf="end"
w="36px"
h="32px"
icon={ <Icon as={ dotsIcon } w={ 4 } h={ 4 }/> }
colorScheme="gray"
variant="ghost"
as={ IconButton }
{ description && (
<Skeleton
isLoaded={ !isLoading }
color="text_secondary"
fontSize="xs"
mt={ 1 }
>
<VisuallyHidden>
Open chart options menu
</VisuallyHidden>
</MenuButton>
<MenuList>
<MenuItem
display="flex"
alignItems="center"
onClick={ showChartFullscreen }
>
<Icon as={ scopeIcon } boxSize={ 5 } mr={ 3 }/>
<span>{ description }</span>
</Skeleton>
) }
</Flex>
<Flex ml="auto" columnGap={ 2 }>
<Tooltip label="Reset zoom">
<IconButton
hidden={ isZoomResetInitial }
aria-label="Reset zoom"
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
</MenuItem>
</MenuItem>
<MenuItem
display="flex"
alignItems="center"
onClick={ handleFileSaveClick }
>
<Icon as={ imageIcon } boxSize={ 5 } mr={ 3 }/>
<MenuItem
display="flex"
alignItems="center"
onClick={ handleFileSaveClick }
>
<Icon as={ imageIcon } boxSize={ 5 } mr={ 3 }/>
Save as PNG
</MenuItem>
</MenuItem>
<MenuItem
display="flex"
alignItems="center"
onClick={ handleSVGSavingClick }
>
<Icon as={ svgFileIcon } boxSize={ 5 } mr={ 3 }/>
<MenuItem
display="flex"
alignItems="center"
onClick={ handleSVGSavingClick }
>
<Icon as={ svgFileIcon } boxSize={ 5 } mr={ 3 }/>
Save as CSV
</MenuItem>
</MenuList>
</Menu>
) }
</Grid>
</MenuItem>
</MenuList>
</Menu>
) }
</Flex>
</Flex>
{ content }
</Box>
</Flex>
{ hasItems && (
<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 type { RoutedTab } from '../Tabs/types';
import useTabIndexFromQuery from 'ui/shared/Tabs/useTabIndexFromQuery';
interface Props {
className?: string;
tabs?: Array<RoutedTab>;
......@@ -10,6 +12,9 @@ interface Props {
}
const SkeletonTabs = ({ className, tabs, size = 'md' }: Props) => {
const bgColor = useColorModeValue('blackAlpha.50', 'whiteAlpha.50');
const tabIndex = useTabIndexFromQuery(tabs || []);
if (tabs) {
if (tabs.length === 1) {
return null;
......@@ -19,16 +24,34 @@ const SkeletonTabs = ({ className, tabs, size = 'md' }: Props) => {
const paddingVert = size === 'sm' ? 1 : 2;
return (
<Flex className={ className } my={ 8 } alignItems="center">
{ tabs.map(({ title, id }, index) => (
<Flex className={ className } my={ 8 } alignItems="center" overflow="hidden">
{ 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
key={ id }
py={ index === 0 ? paddingVert : 0 }
px={ index === 0 ? paddingHor : 0 }
mx={ index === 0 ? 0 : paddingHor }
mx={ paddingHor }
borderRadius="base"
fontWeight={ 600 }
borderWidth={ size === 'sm' ? '2px' : 0 }
flexShrink={ 0 }
>
{ typeof title === 'string' ? title : title() }
</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