Commit 0bc65c00 authored by tom goriunov's avatar tom goriunov Committed by GitHub

Merge pull request #821 from blockscout/skeletons/blocks-and-txs

skeletons: blocks and txs
parents ee3337ae 10176478
...@@ -47,7 +47,7 @@ ...@@ -47,7 +47,7 @@
}, },
{ {
"type": "shell", "type": "shell",
"command": "NEXT_PUBLIC_API_HOST=${input:goerliApiHost} yarn dev:goerli", "command": "NEXT_PUBLIC_API_HOST=${input:apiHost} yarn dev:goerli",
"problemMatcher": [], "problemMatcher": [],
"label": "dev server: goerli", "label": "dev server: goerli",
"detail": "start local dev server for Goerli network", "detail": "start local dev server for Goerli network",
...@@ -68,7 +68,7 @@ ...@@ -68,7 +68,7 @@
}, },
{ {
"type": "shell", "type": "shell",
"command": "NEXT_PUBLIC_API_HOST=${input:L2ApiHost} NEXT_PUBLIC_L1_BASE_URL=https://${input:goerliApiHost} yarn dev:goerli:optimism", "command": "NEXT_PUBLIC_API_HOST=${input:L2ApiHost} NEXT_PUBLIC_L1_BASE_URL=https://${input:apiHost} yarn dev:goerli:optimism",
"problemMatcher": [], "problemMatcher": [],
"label": "dev server: goerli optimism", "label": "dev server: goerli optimism",
"detail": "start local dev server for Goerli Optimism network", "detail": "start local dev server for Goerli Optimism network",
...@@ -371,11 +371,12 @@ ...@@ -371,11 +371,12 @@
}, },
{ {
"type": "pickString", "type": "pickString",
"id": "goerliApiHost", "id": "apiHost",
"description": "Choose API host:", "description": "Choose API host:",
"options": [ "options": [
"blockscout-main.test.aws-k8s.blockscout.com", "blockscout-main.test.aws-k8s.blockscout.com",
"eth-goerli.blockscout.com", "eth-goerli.blockscout.com",
"eth.blockscout.com",
], ],
"default": "" "default": ""
}, },
......
...@@ -21,6 +21,7 @@ NEXT_PUBLIC_MARKETPLACE_CONFIG_URL=https://raw.githubusercontent.com/blockscout/ ...@@ -21,6 +21,7 @@ NEXT_PUBLIC_MARKETPLACE_CONFIG_URL=https://raw.githubusercontent.com/blockscout/
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/shrqUAcjgGJ4jU88C NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/shrqUAcjgGJ4jU88C
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs'] NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs']
NEXT_PUBLIC_IS_TESTNET=true NEXT_PUBLIC_IS_TESTNET=true
NEXT_PUBLIC_HAS_BEACON_CHAIN=false
# api config # api config
NEXT_PUBLIC_API_HOST=blockscout-main.test.aws-k8s.blockscout.com NEXT_PUBLIC_API_HOST=blockscout-main.test.aws-k8s.blockscout.com
......
...@@ -158,6 +158,7 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({ ...@@ -158,6 +158,7 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({
resetPage, resetPage,
hasNextPage: nextPageParams ? Object.keys(nextPageParams).length > 0 : false, hasNextPage: nextPageParams ? Object.keys(nextPageParams).length > 0 : false,
canGoBackwards: canGoBackwards.current, canGoBackwards: canGoBackwards.current,
isLoading: queryResult.isPlaceholderData && !hasPagination,
}; };
const isPaginationVisible = hasPagination || (!queryResult.isLoading && !queryResult.isError && pagination.hasNextPage); const isPaginationVisible = hasPagination || (!queryResult.isLoading && !queryResult.isError && pagination.hasNextPage);
......
...@@ -47,6 +47,7 @@ export const base: Block = { ...@@ -47,6 +47,7 @@ export const base: Block = {
tx_fees: '26853607500000000', tx_fees: '26853607500000000',
type: 'block', type: 'block',
uncles_hashes: [], uncles_hashes: [],
has_beacon_chain_withdrawals: false,
}; };
export const genesis: Block = { export const genesis: Block = {
...@@ -83,6 +84,7 @@ export const genesis: Block = { ...@@ -83,6 +84,7 @@ export const genesis: Block = {
tx_fees: '0', tx_fees: '0',
type: 'block', type: 'block',
uncles_hashes: [], uncles_hashes: [],
has_beacon_chain_withdrawals: false,
}; };
export const base2: Block = { export const base2: Block = {
......
...@@ -65,6 +65,7 @@ export const base: Transaction = { ...@@ -65,6 +65,7 @@ export const base: Transaction = {
type: 2, type: 2,
value: '42000000000000000000', value: '42000000000000000000',
actions: [], actions: [],
has_error_in_internal_txs: false,
}; };
export const withContractCreation: Transaction = { export const withContractCreation: Transaction = {
......
import type { NextPage } from 'next'; import type { NextPage } from 'next';
import dynamic from 'next/dynamic';
import Head from 'next/head'; import Head from 'next/head';
import type { RoutedQuery } from 'nextjs-routes'; import type { RoutedQuery } from 'nextjs-routes';
import React from 'react'; import React from 'react';
import getSeo from 'lib/next/block/getSeo'; import getSeo from 'lib/next/block/getSeo';
import Block from 'ui/pages/Block';
import Page from 'ui/shared/Page/Page'; import Page from 'ui/shared/Page/Page';
const Block = dynamic(() => import('ui/pages/Block'), { ssr: false });
const BlockPage: NextPage<RoutedQuery<'/block/[height]'>> = ({ height }: RoutedQuery<'/block/[height]'>) => { const BlockPage: NextPage<RoutedQuery<'/block/[height]'>> = ({ height }: RoutedQuery<'/block/[height]'>) => {
const { title, description } = getSeo({ height }); const { title, description } = getSeo({ height });
return ( return (
......
import type { NextPage } from 'next'; import type { NextPage } from 'next';
import dynamic from 'next/dynamic';
import Head from 'next/head'; import Head from 'next/head';
import React from 'react'; import React from 'react';
import getSeo from 'lib/next/blocks/getSeo'; import getSeo from 'lib/next/blocks/getSeo';
import Blocks from 'ui/pages/Blocks'; import Page from 'ui/shared/Page/Page';
const Blocks = dynamic(() => import('ui/pages/Blocks'), { ssr: false });
const BlockPage: NextPage = () => { const BlockPage: NextPage = () => {
const { title } = getSeo(); const { title } = getSeo();
...@@ -12,7 +15,9 @@ const BlockPage: NextPage = () => { ...@@ -12,7 +15,9 @@ const BlockPage: NextPage = () => {
<Head> <Head>
<title>{ title }</title> <title>{ title }</title>
</Head> </Head>
<Blocks/> <Page>
<Blocks/>
</Page>
</> </>
); );
}; };
......
import type { NextPage } from 'next'; import type { NextPage } from 'next';
import dynamic from 'next/dynamic';
import Head from 'next/head'; import Head from 'next/head';
import type { RoutedQuery } from 'nextjs-routes'; import type { RoutedQuery } from 'nextjs-routes';
import React from 'react'; import React from 'react';
import getSeo from 'lib/next/tx/getSeo'; import getSeo from 'lib/next/tx/getSeo';
import Transaction from 'ui/pages/Transaction'; import Page from 'ui/shared/Page/Page';
const Transaction = dynamic(() => import('ui/pages/Transaction'), { ssr: false });
const TransactionPage: NextPage<RoutedQuery<'/tx/[hash]'>> = ({ hash }: RoutedQuery<'/tx/[hash]'>) => { const TransactionPage: NextPage<RoutedQuery<'/tx/[hash]'>> = ({ hash }: RoutedQuery<'/tx/[hash]'>) => {
const { title, description } = getSeo({ hash }); const { title, description } = getSeo({ hash });
...@@ -15,7 +18,9 @@ const TransactionPage: NextPage<RoutedQuery<'/tx/[hash]'>> = ({ hash }: RoutedQu ...@@ -15,7 +18,9 @@ const TransactionPage: NextPage<RoutedQuery<'/tx/[hash]'>> = ({ hash }: RoutedQu
<title>{ title }</title> <title>{ title }</title>
<meta name="description" content={ description }/> <meta name="description" content={ description }/>
</Head> </Head>
<Transaction/> <Page>
<Transaction/>
</Page>
</> </>
); );
}; };
......
import type { NextPage } from 'next'; import type { NextPage } from 'next';
import dynamic from 'next/dynamic';
import Head from 'next/head'; import Head from 'next/head';
import React from 'react'; import React from 'react';
import getNetworkTitle from 'lib/networks/getNetworkTitle'; import getNetworkTitle from 'lib/networks/getNetworkTitle';
import Transactions from 'ui/pages/Transactions'; import Page from 'ui/shared/Page/Page';
const Transactions = dynamic(() => import('ui/pages/Transactions'), { ssr: false });
const TxsPage: NextPage = () => { const TxsPage: NextPage = () => {
const title = getNetworkTitle(); const title = getNetworkTitle();
return ( return (
<> <>
<Head><title>{ title }</title></Head> <Head><title>{ title }</title></Head>
<Transactions/> <Page>
<Transactions/>
</Page>
</> </>
); );
}; };
......
import type { NextPage } from 'next'; import type { NextPage } from 'next';
import dynamic from 'next/dynamic';
import Head from 'next/head'; import Head from 'next/head';
import React from 'react'; import React from 'react';
import getNetworkTitle from 'lib/networks/getNetworkTitle'; import getNetworkTitle from 'lib/networks/getNetworkTitle';
import Withdrawals from 'ui/pages/Withdrawals'; import Page from 'ui/shared/Page/Page';
const Withdrawals = dynamic(() => import('ui/pages/Withdrawals'), { ssr: false });
const WithdrawalsPage: NextPage = () => { const WithdrawalsPage: NextPage = () => {
const title = getNetworkTitle(); const title = getNetworkTitle();
...@@ -12,7 +15,9 @@ const WithdrawalsPage: NextPage = () => { ...@@ -12,7 +15,9 @@ const WithdrawalsPage: NextPage = () => {
<Head> <Head>
<title>{ title }</title> <title>{ title }</title>
</Head> </Head>
<Withdrawals/> <Page>
<Withdrawals/>
</Page>
</> </>
); );
}; };
......
import type { Block } from 'types/api/block';
import { ADDRESS_PARAMS } from './addressParams';
export const BLOCK_HASH = '0x8fa7b9e5e5e79deeb62d608db22ba9a5cb45388c7ebb9223ae77331c6080dc70'; export const BLOCK_HASH = '0x8fa7b9e5e5e79deeb62d608db22ba9a5cb45388c7ebb9223ae77331c6080dc70';
export const BLOCK: Block = {
base_fee_per_gas: '14',
burnt_fees: '92834504000000000',
burnt_fees_percentage: 42.2,
difficulty: '340282366920938463463374607431768211451',
extra_data: 'TODO',
gas_limit: '30000000',
gas_target_percentage: 55.79,
gas_used: '6631036',
gas_used_percentage: 22.10,
has_beacon_chain_withdrawals: null,
hash: BLOCK_HASH,
height: 8988736,
miner: ADDRESS_PARAMS,
nonce: '0x0000000000000000',
parent_hash: BLOCK_HASH,
priority_fee: '19241635454943109',
rewards: [
{
reward: '19241635454943109',
type: 'Validator Reward',
},
],
size: 46406,
state_root: 'TODO',
timestamp: '2023-05-12T19:29:12.000000Z',
total_difficulty: '10837812015930321201107455268036056402048391639',
tx_count: 142,
tx_fees: '19241635547777613',
type: 'block',
uncles_hashes: [],
};
import type { InternalTransaction } from 'types/api/internalTransaction';
import { ADDRESS_PARAMS } from './addressParams';
import { TX_HASH } from './tx';
export const INTERNAL_TX: InternalTransaction = {
block: 9006105,
created_contract: null,
error: null,
from: ADDRESS_PARAMS,
gas_limit: '754278',
index: 1,
success: true,
timestamp: '2023-05-15T20:14:00.000000Z',
to: ADDRESS_PARAMS,
transaction_hash: TX_HASH,
type: 'staticcall',
value: '22324344900000000',
};
import type { Log } from 'types/api/log';
import { ADDRESS_PARAMS } from './addressParams';
import { TX_HASH } from './tx';
export const LOG: Log = {
address: ADDRESS_PARAMS,
data: '0x000000000000000000000000000000000000000000000000000000d75e4be200',
decoded: {
method_call: 'CreditSpended(uint256 indexed _type, uint256 _quantity)',
method_id: '58cdf94a',
parameters: [
{
indexed: true,
name: '_type',
type: 'uint256',
value: 'placeholder',
},
{
indexed: false,
name: '_quantity',
type: 'uint256',
value: 'placeholder',
},
],
},
index: 42,
topics: [
'0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925',
'0x000000000000000000000000c52ea157a7fb3e25a069d47df0428ac70cd656b1',
'0x000000000000000000000000302fd86163cb9ad5533b3952dafa3b633a82bc51',
null,
],
tx_hash: TX_HASH,
};
import type { HomeStats } from 'types/api/stats';
export const HOMEPAGE_STATS: HomeStats = {
average_block_time: 14346,
coin_price: '1807.68',
gas_prices: {
average: 0.1,
fast: 0.11,
slow: 0.1,
},
gas_used_today: '0',
market_cap: '0',
network_utilization_percentage: 22.56,
static_gas_price: null,
total_addresses: '28634064',
total_blocks: '8940150',
total_gas_used: '0',
total_transactions: '193823272',
transactions_today: '0',
};
import type { TokenCounters, TokenHolder, TokenHolders, TokenInfo, TokenInstance, TokenInventoryResponse, TokenType } from 'types/api/token'; import type { TokenCounters, TokenHolder, TokenHolders, TokenInfo, TokenInstance, TokenType } from 'types/api/token';
import type { TokenTransfer, TokenTransferResponse } from 'types/api/tokenTransfer'; import type { TokenTransfer, TokenTransferPagination, TokenTransferResponse } from 'types/api/tokenTransfer';
import { ADDRESS_PARAMS, ADDRESS_HASH } from './addressParams'; import { ADDRESS_PARAMS, ADDRESS_HASH } from './addressParams';
import { BLOCK_HASH } from './block'; import { BLOCK_HASH } from './block';
import { TX_HASH } from './tx'; import { TX_HASH } from './tx';
import { generateListStub } from './utils';
export const TOKEN_INFO_ERC_20: TokenInfo<'ERC-20'> = { export const TOKEN_INFO_ERC_20: TokenInfo<'ERC-20'> = {
address: ADDRESS_HASH, address: ADDRESS_HASH,
...@@ -73,14 +74,14 @@ export const TOKEN_TRANSFER_ERC_1155: TokenTransfer = { ...@@ -73,14 +74,14 @@ export const TOKEN_TRANSFER_ERC_1155: TokenTransfer = {
token: TOKEN_INFO_ERC_1155, token: TOKEN_INFO_ERC_1155,
}; };
export const getTokenTransfersStub = (type?: TokenType): TokenTransferResponse => { export const getTokenTransfersStub = (type?: TokenType, pagination: TokenTransferPagination | null = null): TokenTransferResponse => {
switch (type) { switch (type) {
case 'ERC-721': case 'ERC-721':
return { items: Array(50).fill(TOKEN_TRANSFER_ERC_721), next_page_params: null }; return generateListStub<'token_transfers'>(TOKEN_TRANSFER_ERC_721, 50, pagination);
case 'ERC-1155': case 'ERC-1155':
return { items: Array(50).fill(TOKEN_TRANSFER_ERC_1155), next_page_params: null }; return generateListStub<'token_transfers'>(TOKEN_TRANSFER_ERC_1155, 50, pagination);
default: default:
return { items: Array(50).fill(TOKEN_TRANSFER_ERC_20), next_page_params: null }; return generateListStub<'token_transfers'>(TOKEN_TRANSFER_ERC_20, 50, pagination);
} }
}; };
...@@ -101,8 +102,3 @@ export const TOKEN_INSTANCE: TokenInstance = { ...@@ -101,8 +102,3 @@ export const TOKEN_INSTANCE: TokenInstance = {
token: TOKEN_INFO_ERC_1155, token: TOKEN_INFO_ERC_1155,
holder_address_hash: ADDRESS_HASH, holder_address_hash: ADDRESS_HASH,
}; };
export const TOKEN_INSTANCES: TokenInventoryResponse = {
items: Array(50).fill(TOKEN_INSTANCE),
next_page_params: null,
};
import type { RawTracesResponse } from 'types/api/rawTrace';
import type { Transaction } from 'types/api/transaction';
import { ADDRESS_PARAMS } from './addressParams';
export const TX_HASH = '0x3ed9d81e7c1001bdda1caa1dc62c0acbbe3d2c671cdc20dc1e65efdaa4186967'; export const TX_HASH = '0x3ed9d81e7c1001bdda1caa1dc62c0acbbe3d2c671cdc20dc1e65efdaa4186967';
export const TX: Transaction = {
timestamp: '2022-11-11T11:11:11.000000Z',
fee: {
type: 'actual',
value: '2100000000000000',
},
gas_limit: '21000',
block: 9004925,
status: 'ok',
method: 'placeholder',
confirmations: 71,
type: 0,
exchange_rate: '1828.71',
to: ADDRESS_PARAMS,
tx_burnt_fee: null,
max_fee_per_gas: null,
result: 'success',
hash: '0x2b824349b320cfa72f292ab26bf525adb00083ba9fa097141896c3c8c74567cc',
gas_price: '100000000000',
priority_fee: null,
base_fee_per_gas: '24',
from: ADDRESS_PARAMS,
token_transfers: null,
tx_types: [
'coin_transfer',
],
gas_used: '21000',
created_contract: null,
position: 0,
nonce: 295929,
has_error_in_internal_txs: false,
actions: [],
decoded_input: null,
token_transfers_overflow: false,
raw_input: '0x',
value: '42000420000000000000',
max_priority_fee_per_gas: null,
revert_reason: null,
confirmation_duration: [
0,
14545,
],
tx_tag: null,
};
export const TX_RAW_TRACE: RawTracesResponse = [];
import type { TxStateChange, TxStateChanges } from 'types/api/txStateChanges';
import { ADDRESS_PARAMS } from './addressParams';
import { TOKEN_INFO_ERC_721 } from './token';
export const STATE_CHANGE_MINER: TxStateChange = {
address: ADDRESS_PARAMS,
balance_after: '124280364215547113',
balance_before: '123405277440098758',
change: '875086775448355',
is_miner: true,
token: null,
type: 'coin',
};
export const STATE_CHANGE_COIN: TxStateChange = {
address: ADDRESS_PARAMS,
balance_after: '61659392141463351540',
balance_before: '61660292436225994690',
change: '-900294762600000',
is_miner: false,
token: null,
type: 'coin',
};
export const STATE_CHANGE_TOKEN: TxStateChange = {
address: ADDRESS_PARAMS,
balance_after: '43',
balance_before: '42',
change: [
{
direction: 'to',
total: {
token_id: '1621395',
},
},
],
is_miner: false,
token: TOKEN_INFO_ERC_721,
type: 'token',
};
export const TX_STATE_CHANGES: TxStateChanges = [
STATE_CHANGE_MINER,
STATE_CHANGE_COIN,
STATE_CHANGE_TOKEN,
];
import type { ArrayElement } from 'types/utils';
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,
) {
return {
items: Array(num).fill(stub),
next_page_params: pagination,
};
}
import type { WithdrawalsItem } from 'types/api/withdrawals';
import { ADDRESS_PARAMS } from './addressParams';
export const WITHDRAWAL: WithdrawalsItem = {
amount: '12565723',
index: 3810697,
receiver: ADDRESS_PARAMS,
validator_index: 25987,
block_number: 9005713,
timestamp: '2023-05-12T19:29:12.000000Z',
};
...@@ -10,7 +10,7 @@ export interface Block { ...@@ -10,7 +10,7 @@ export interface Block {
tx_count: number; tx_count: number;
miner: AddressParam; miner: AddressParam;
size: number; size: number;
has_beacon_chain_withdrawals?: boolean; has_beacon_chain_withdrawals: boolean | null;
hash: string; hash: string;
parent_hash: string; parent_hash: string;
difficulty: string; difficulty: string;
......
...@@ -16,7 +16,7 @@ export interface LogsResponseTx { ...@@ -16,7 +16,7 @@ export interface LogsResponseTx {
index: number; index: number;
items_count: number; items_count: number;
transaction_hash: string; transaction_hash: string;
}; } | null;
} }
export interface LogsResponseAddress { export interface LogsResponseAddress {
...@@ -26,5 +26,5 @@ export interface LogsResponseAddress { ...@@ -26,5 +26,5 @@ export interface LogsResponseAddress {
items_count: number; items_count: number;
transaction_index: number; transaction_index: number;
block_number: number; block_number: number;
}; } | null;
} }
...@@ -8,7 +8,7 @@ export type HomeStats = { ...@@ -8,7 +8,7 @@ export type HomeStats = {
transactions_today: string; transactions_today: string;
gas_used_today: string; gas_used_today: string;
gas_prices: GasPrices | null; gas_prices: GasPrices | null;
static_gas_price: string; static_gas_price: string | null;
market_cap: string; market_cap: string;
network_utilization_percentage: number; network_utilization_percentage: number;
} }
......
...@@ -47,6 +47,7 @@ export type Transaction = { ...@@ -47,6 +47,7 @@ export type Transaction = {
l1_fee_scalar?: string; l1_fee_scalar?: string;
l1_gas_price?: string; l1_gas_price?: string;
l1_gas_used?: string; l1_gas_used?: string;
has_error_in_internal_txs: boolean | null;
} }
export type TransactionsResponse = TransactionsResponseValidated | TransactionsResponsePending; export type TransactionsResponse = TransactionsResponseValidated | TransactionsResponsePending;
......
import { chakra, Icon, Tooltip, Hide } from '@chakra-ui/react'; import { chakra, Icon, Tooltip, Hide, Skeleton, Flex } from '@chakra-ui/react';
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
...@@ -13,15 +13,27 @@ interface Props { ...@@ -13,15 +13,27 @@ interface Props {
address: string; address: string;
type: CsvExportType; type: CsvExportType;
className?: string; className?: string;
isLoading?: boolean;
} }
const AddressCsvExportLink = ({ className, address, type }: Props) => { const AddressCsvExportLink = ({ className, address, type, isLoading }: Props) => {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
if (!appConfig.reCaptcha.siteKey) { if (!appConfig.reCaptcha.siteKey) {
return null; return null;
} }
if (isLoading) {
return (
<Flex className={ className } flexShrink={ 0 } alignItems="center">
<Skeleton boxSize={{ base: '32px', lg: 6 }} borderRadius="base"/>
<Hide ssr={ false } below="lg">
<Skeleton w="112px" h={ 6 } ml={ 1 }/>
</Hide>
</Flex>
);
}
return ( return (
<Tooltip isDisabled={ !isMobile } label="Download CSV"> <Tooltip isDisabled={ !isMobile } label="Download CSV">
<LinkInternal <LinkInternal
......
...@@ -3,20 +3,29 @@ import React from 'react'; ...@@ -3,20 +3,29 @@ import React from 'react';
import useQueryWithPages from 'lib/hooks/useQueryWithPages'; import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import getQueryParamString from 'lib/router/getQueryParamString'; import getQueryParamString from 'lib/router/getQueryParamString';
import { LOG } from 'stubs/log';
import { generateListStub } from 'stubs/utils';
import ActionBar from 'ui/shared/ActionBar'; import ActionBar from 'ui/shared/ActionBar';
import DataListDisplay from 'ui/shared/DataListDisplay'; import DataListDisplay from 'ui/shared/DataListDisplay';
import LogItem from 'ui/shared/logs/LogItem'; import LogItem from 'ui/shared/logs/LogItem';
import LogSkeleton from 'ui/shared/logs/LogSkeleton';
import Pagination from 'ui/shared/Pagination'; import Pagination from 'ui/shared/Pagination';
const AddressLogs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>}) => { const AddressLogs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>}) => {
const router = useRouter(); const router = useRouter();
const hash = getQueryParamString(router.query.hash); const hash = getQueryParamString(router.query.hash);
const { data, isLoading, isError, pagination, isPaginationVisible } = useQueryWithPages({ const { data, isPlaceholderData, isError, pagination, isPaginationVisible } = useQueryWithPages({
resourceName: 'address_logs', resourceName: 'address_logs',
pathParams: { hash }, pathParams: { hash },
scrollRef, scrollRef,
options: {
placeholderData: generateListStub<'address_logs'>(LOG, 3, {
block_number: 9005750,
index: 42,
items_count: 50,
transaction_index: 23,
}),
},
}); });
const actionBar = isPaginationVisible ? ( const actionBar = isPaginationVisible ? (
...@@ -25,19 +34,17 @@ const AddressLogs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement> ...@@ -25,19 +34,17 @@ const AddressLogs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>
</ActionBar> </ActionBar>
) : null; ) : null;
const content = data?.items ? data.items.map((item, index) => <LogItem key={ index } { ...item } type="address"/>) : null; const content = data?.items ? data.items.map((item, index) => <LogItem key={ index } { ...item } type="address" isLoading={ isPlaceholderData }/>) : null;
const skeleton = <><LogSkeleton/><LogSkeleton/></>;
return ( return (
<DataListDisplay <DataListDisplay
isError={ isError } isError={ isError }
isLoading={ isLoading } isLoading={ false }
items={ data?.items } items={ data?.items }
emptyText="There are no logs for this address." emptyText="There are no logs for this address."
content={ content } content={ content }
actionBar={ actionBar } actionBar={ actionBar }
skeletonProps={{ customSkeleton: skeleton }} skeletonProps={{ customSkeleton: null }}
/> />
); );
}; };
......
import { Box } from '@chakra-ui/react'; import { Box } from '@chakra-ui/react';
import { test, expect } from '@playwright/experimental-ct-react'; import { test, expect, devices } from '@playwright/experimental-ct-react';
import React from 'react'; import React from 'react';
import { erc1155A } from 'mocks/tokens/tokenTransfer'; import { erc1155A } from 'mocks/tokens/tokenTransfer';
...@@ -17,7 +17,7 @@ const hooksConfig = { ...@@ -17,7 +17,7 @@ const hooksConfig = {
}, },
}; };
test('with token filter and pagination +@mobile', async({ mount, page }) => { test('with token filter and pagination', async({ mount, page }) => {
await page.route(API_URL, (route) => route.fulfill({ await page.route(API_URL, (route) => route.fulfill({
status: 200, status: 200,
body: JSON.stringify({ items: [ erc1155A ], next_page_params: { block_number: 1 } }), body: JSON.stringify({ items: [ erc1155A ], next_page_params: { block_number: 1 } }),
...@@ -34,7 +34,7 @@ test('with token filter and pagination +@mobile', async({ mount, page }) => { ...@@ -34,7 +34,7 @@ test('with token filter and pagination +@mobile', async({ mount, page }) => {
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
test('with token filter and no pagination +@mobile', async({ mount, page }) => { test('with token filter and no pagination', async({ mount, page }) => {
await page.route(API_URL, (route) => route.fulfill({ await page.route(API_URL, (route) => route.fulfill({
status: 200, status: 200,
body: JSON.stringify({ items: [ erc1155A ] }), body: JSON.stringify({ items: [ erc1155A ] }),
...@@ -50,3 +50,41 @@ test('with token filter and no pagination +@mobile', async({ mount, page }) => { ...@@ -50,3 +50,41 @@ test('with token filter and no pagination +@mobile', async({ mount, page }) => {
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
test.describe('mobile', () => {
test.use({ viewport: devices['iPhone 13 Pro'].viewport });
test('with token filter and pagination', async({ mount, page }) => {
await page.route(API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify({ items: [ erc1155A ], next_page_params: { block_number: 1 } }),
}));
const component = await mount(
<TestApp>
<Box h={{ base: '134px', lg: 6 }}/>
<AddressTokenTransfers/>
</TestApp>,
{ hooksConfig },
);
await expect(component).toHaveScreenshot();
});
test('with token filter and no pagination', async({ mount, page }) => {
await page.route(API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify({ items: [ erc1155A ] }),
}));
const component = await mount(
<TestApp>
<Box h={{ base: '134px', lg: 6 }}/>
<AddressTokenTransfers/>
</TestApp>,
{ hooksConfig },
);
await expect(component).toHaveScreenshot();
});
});
...@@ -20,11 +20,12 @@ import getQueryParamString from 'lib/router/getQueryParamString'; ...@@ -20,11 +20,12 @@ import getQueryParamString from 'lib/router/getQueryParamString';
import useSocketChannel from 'lib/socket/useSocketChannel'; import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage'; import useSocketMessage from 'lib/socket/useSocketMessage';
import TOKEN_TYPE from 'lib/token/tokenTypes'; import TOKEN_TYPE from 'lib/token/tokenTypes';
import { getTokenTransfersStub } from 'stubs/token';
import ActionBar from 'ui/shared/ActionBar'; import ActionBar from 'ui/shared/ActionBar';
import DataListDisplay from 'ui/shared/DataListDisplay'; import DataListDisplay from 'ui/shared/DataListDisplay';
import HashStringShorten from 'ui/shared/HashStringShorten'; import HashStringShorten from 'ui/shared/HashStringShorten';
import Pagination from 'ui/shared/Pagination'; import Pagination from 'ui/shared/Pagination';
import SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice'; import * as SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice';
import TokenLogo from 'ui/shared/TokenLogo'; import TokenLogo from 'ui/shared/TokenLogo';
import TokenTransferFilter from 'ui/shared/TokenTransfer/TokenTransferFilter'; import TokenTransferFilter from 'ui/shared/TokenTransfer/TokenTransferFilter';
import TokenTransferList from 'ui/shared/TokenTransfer/TokenTransferList'; import TokenTransferList from 'ui/shared/TokenTransfer/TokenTransferList';
...@@ -81,11 +82,18 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD ...@@ -81,11 +82,18 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD
}, },
); );
const { isError, isLoading, data, pagination, onFilterChange, isPaginationVisible } = useQueryWithPages({ const { isError, isPlaceholderData, data, pagination, onFilterChange, isPaginationVisible } = useQueryWithPages({
resourceName: 'address_token_transfers', resourceName: 'address_token_transfers',
pathParams: { hash: currentAddress }, pathParams: { hash: currentAddress },
filters: tokenFilter ? { token: tokenFilter } : filters, filters: tokenFilter ? { token: tokenFilter } : filters,
scrollRef, scrollRef,
options: {
placeholderData: getTokenTransfersStub(undefined, {
block_number: 7793535,
index: 46,
items_count: 50,
}),
},
}); });
const handleTypeFilterChange = React.useCallback((nextValue: Array<TokenType>) => { const handleTypeFilterChange = React.useCallback((nextValue: Array<TokenType>) => {
...@@ -172,16 +180,17 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD ...@@ -172,16 +180,17 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD
showSocketInfo={ pagination.page === 1 && !tokenFilter } showSocketInfo={ pagination.page === 1 && !tokenFilter }
socketInfoAlert={ socketAlert } socketInfoAlert={ socketAlert }
socketInfoNum={ newItemsCount } socketInfoNum={ newItemsCount }
isLoading={ isPlaceholderData }
/> />
</Hide> </Hide>
<Show below="lg" ssr={ false }> <Show below="lg" ssr={ false }>
{ pagination.page === 1 && !tokenFilter && ( { pagination.page === 1 && !tokenFilter && (
<SocketNewItemsNotice <SocketNewItemsNotice.Mobile
url={ window.location.href } url={ window.location.href }
num={ newItemsCount } num={ newItemsCount }
alert={ socketAlert } alert={ socketAlert }
type="token_transfer" type="token_transfer"
borderBottomRadius={ 0 } isLoading={ isPlaceholderData }
/> />
) } ) }
<TokenTransferList <TokenTransferList
...@@ -189,6 +198,7 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD ...@@ -189,6 +198,7 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD
baseAddress={ currentAddress } baseAddress={ currentAddress }
showTxInfo showTxInfo
enableTimeIncrement enableTimeIncrement
isLoading={ isPlaceholderData }
/> />
</Show> </Show>
</> </>
...@@ -227,7 +237,7 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD ...@@ -227,7 +237,7 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD
<> <>
{ isMobile && tokenFilterComponent } { isMobile && tokenFilterComponent }
{ !isActionBarHidden && ( { !isActionBarHidden && (
<ActionBar mt={ -6 } showShadow={ isLoading }> <ActionBar mt={ -6 }>
{ !isMobile && tokenFilterComponent } { !isMobile && tokenFilterComponent }
{ !tokenFilter && ( { !tokenFilter && (
<TokenTransferFilter <TokenTransferFilter
...@@ -237,9 +247,17 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD ...@@ -237,9 +247,17 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD
withAddressFilter withAddressFilter
onAddressFilterChange={ handleAddressFilterChange } onAddressFilterChange={ handleAddressFilterChange }
defaultAddressFilter={ filters.filter } defaultAddressFilter={ filters.filter }
isLoading={ isPlaceholderData }
/>
) }
{ currentAddress && (
<AddressCsvExportLink
address={ currentAddress }
type="token-transfers"
ml={{ base: 2, lg: 'auto' }}
isLoading={ isPlaceholderData }
/> />
) } ) }
{ currentAddress && <AddressCsvExportLink address={ currentAddress } type="token-transfers" ml={{ base: 2, lg: 'auto' }}/> }
{ isPaginationVisible && <Pagination ml={{ base: 'auto', lg: 8 }} { ...pagination }/> } { isPaginationVisible && <Pagination ml={{ base: 'auto', lg: 8 }} { ...pagination }/> }
</ActionBar> </ActionBar>
) } ) }
...@@ -249,7 +267,7 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD ...@@ -249,7 +267,7 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD
return ( return (
<DataListDisplay <DataListDisplay
isError={ isError } isError={ isError }
isLoading={ isLoading } isLoading={ false }
items={ data?.items } items={ data?.items }
skeletonProps={{ skeletonProps={{
isLongSkeleton: true, isLongSkeleton: true,
......
...@@ -13,6 +13,8 @@ import useQueryWithPages from 'lib/hooks/useQueryWithPages'; ...@@ -13,6 +13,8 @@ import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import getQueryParamString from 'lib/router/getQueryParamString'; import getQueryParamString from 'lib/router/getQueryParamString';
import useSocketChannel from 'lib/socket/useSocketChannel'; import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage'; import useSocketMessage from 'lib/socket/useSocketMessage';
import { TX } from 'stubs/tx';
import { generateListStub } from 'stubs/utils';
import ActionBar from 'ui/shared/ActionBar'; import ActionBar from 'ui/shared/ActionBar';
import Pagination from 'ui/shared/Pagination'; import Pagination from 'ui/shared/Pagination';
import TxsContent from 'ui/txs/TxsContent'; import TxsContent from 'ui/txs/TxsContent';
...@@ -41,6 +43,13 @@ const AddressTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>} ...@@ -41,6 +43,13 @@ const AddressTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>}
pathParams: { hash: currentAddress }, pathParams: { hash: currentAddress },
filters: { filter: filterValue }, filters: { filter: filterValue },
scrollRef, scrollRef,
options: {
placeholderData: generateListStub<'address_txs'>(TX, 50, {
block_number: 9005713,
index: 5,
items_count: 50,
}),
},
}); });
const handleFilterChange = React.useCallback((val: string | Array<string>) => { const handleFilterChange = React.useCallback((val: string | Array<string>) => {
...@@ -112,7 +121,7 @@ const AddressTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>} ...@@ -112,7 +121,7 @@ const AddressTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>}
topic: `addresses:${ currentAddress?.toLowerCase() }`, topic: `addresses:${ currentAddress?.toLowerCase() }`,
onSocketClose: handleSocketClose, onSocketClose: handleSocketClose,
onSocketError: handleSocketError, onSocketError: handleSocketError,
isDisabled: addressTxsQuery.pagination.page !== 1, isDisabled: addressTxsQuery.pagination.page !== 1 || addressTxsQuery.isPlaceholderData,
}); });
useSocketMessage({ useSocketMessage({
...@@ -132,15 +141,23 @@ const AddressTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>} ...@@ -132,15 +141,23 @@ const AddressTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>}
defaultFilter={ filterValue } defaultFilter={ filterValue }
onFilterChange={ handleFilterChange } onFilterChange={ handleFilterChange }
isActive={ Boolean(filterValue) } isActive={ Boolean(filterValue) }
isLoading={ addressTxsQuery.pagination.isLoading }
/> />
); );
return ( return (
<> <>
{ !isMobile && ( { !isMobile && (
<ActionBar mt={ -6 } showShadow={ addressTxsQuery.isLoading }> <ActionBar mt={ -6 }>
{ filter } { filter }
{ currentAddress && <AddressCsvExportLink address={ currentAddress } type="transactions" ml="auto"/> } { currentAddress && (
<AddressCsvExportLink
address={ currentAddress }
type="transactions"
ml="auto"
isLoading={ addressTxsQuery.pagination.isLoading }
/>
) }
{ addressTxsQuery.isPaginationVisible && <Pagination { ...addressTxsQuery.pagination } ml={ 8 }/> } { addressTxsQuery.isPaginationVisible && <Pagination { ...addressTxsQuery.pagination } ml={ 8 }/> }
</ActionBar> </ActionBar>
) } ) }
......
...@@ -16,9 +16,10 @@ interface Props { ...@@ -16,9 +16,10 @@ interface Props {
isActive: boolean; isActive: boolean;
defaultFilter: AddressFromToFilter; defaultFilter: AddressFromToFilter;
onFilterChange: (nextValue: string | Array<string>) => void; onFilterChange: (nextValue: string | Array<string>) => void;
isLoading?: boolean;
} }
const AddressTxsFilter = ({ onFilterChange, defaultFilter, isActive }: Props) => { const AddressTxsFilter = ({ onFilterChange, defaultFilter, isActive, isLoading }: Props) => {
const { isOpen, onToggle } = useDisclosure(); const { isOpen, onToggle } = useDisclosure();
return ( return (
...@@ -26,6 +27,7 @@ const AddressTxsFilter = ({ onFilterChange, defaultFilter, isActive }: Props) => ...@@ -26,6 +27,7 @@ const AddressTxsFilter = ({ onFilterChange, defaultFilter, isActive }: Props) =>
<MenuButton> <MenuButton>
<FilterButton <FilterButton
isActive={ isOpen || isActive } isActive={ isOpen || isActive }
isLoading={ isLoading }
onClick={ onToggle } onClick={ onToggle }
as="div" as="div"
/> />
......
...@@ -4,6 +4,8 @@ import React from 'react'; ...@@ -4,6 +4,8 @@ import React from 'react';
import useQueryWithPages from 'lib/hooks/useQueryWithPages'; import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import getQueryParamString from 'lib/router/getQueryParamString'; import getQueryParamString from 'lib/router/getQueryParamString';
import { generateListStub } from 'stubs/utils';
import { WITHDRAWAL } from 'stubs/withdrawals';
import ActionBar from 'ui/shared/ActionBar'; import ActionBar from 'ui/shared/ActionBar';
import DataListDisplay from 'ui/shared/DataListDisplay'; import DataListDisplay from 'ui/shared/DataListDisplay';
import Pagination from 'ui/shared/Pagination'; import Pagination from 'ui/shared/Pagination';
...@@ -15,24 +17,37 @@ const AddressWithdrawals = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivE ...@@ -15,24 +17,37 @@ const AddressWithdrawals = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivE
const hash = getQueryParamString(router.query.hash); const hash = getQueryParamString(router.query.hash);
const { data, isLoading, isError, pagination, isPaginationVisible } = useQueryWithPages({ const { data, isPlaceholderData, isError, pagination, isPaginationVisible } = useQueryWithPages({
resourceName: 'address_withdrawals', resourceName: 'address_withdrawals',
pathParams: { hash }, pathParams: { hash },
scrollRef, scrollRef,
options: {
placeholderData: generateListStub<'address_withdrawals'>(WITHDRAWAL, 50, {
index: 5,
items_count: 50,
}),
},
}); });
const content = data?.items ? ( const content = data?.items ? (
<> <>
<Show below="lg" ssr={ false }> <Show below="lg" ssr={ false }>
{ data.items.map((item) => <WithdrawalsListItem item={ item } key={ item.index } view="address"/>) } { data.items.map((item, index) => (
<WithdrawalsListItem
key={ item.index + Number(isPlaceholderData ? index : '') }
item={ item }
view="address"
isLoading={ isPlaceholderData }
/>
)) }
</Show> </Show>
<Hide below="lg" ssr={ false }> <Hide below="lg" ssr={ false }>
<WithdrawalsTable items={ data.items } view="address" top={ isPaginationVisible ? 80 : 0 }/> <WithdrawalsTable items={ data.items } view="address" top={ isPaginationVisible ? 80 : 0 } isLoading={ isPlaceholderData }/>
</Hide> </Hide>
</> </>
) : null ; ) : null ;
const actionBar = isPaginationVisible ? ( const actionBar = isPaginationVisible ? (
<ActionBar mt={ -6 } showShadow={ isLoading }> <ActionBar mt={ -6 }>
<Pagination ml="auto" { ...pagination }/> <Pagination ml="auto" { ...pagination }/>
</ActionBar> </ActionBar>
) : null; ) : null;
...@@ -40,7 +55,7 @@ const AddressWithdrawals = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivE ...@@ -40,7 +55,7 @@ const AddressWithdrawals = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivE
return ( return (
<DataListDisplay <DataListDisplay
isError={ isError } isError={ isError }
isLoading={ isLoading } isLoading={ false }
items={ data?.items } items={ data?.items }
skeletonProps={{ isLongSkeleton: true, skeletonDesktopColumns: Array(5).fill(`${ 100 / 5 }%`), skeletonDesktopMinW: '950px' }} skeletonProps={{ isLongSkeleton: true, skeletonDesktopColumns: Array(5).fill(`${ 100 / 5 }%`), skeletonDesktopMinW: '950px' }}
emptyText="There are no withdrawals for this address." emptyText="There are no withdrawals for this address."
......
This diff is collapsed.
...@@ -22,10 +22,22 @@ const BlockWithdrawals = ({ blockWithdrawalsQuery }: Props) => { ...@@ -22,10 +22,22 @@ const BlockWithdrawals = ({ blockWithdrawalsQuery }: Props) => {
const content = blockWithdrawalsQuery.data?.items ? ( const content = blockWithdrawalsQuery.data?.items ? (
<> <>
<Show below="lg" ssr={ false }> <Show below="lg" ssr={ false }>
{ blockWithdrawalsQuery.data.items.map((item) => <WithdrawalsListItem item={ item } key={ item.index } view="block"/>) } { blockWithdrawalsQuery.data.items.map((item, index) => (
<WithdrawalsListItem
key={ item.index + (blockWithdrawalsQuery.isPlaceholderData ? String(index) : '') }
item={ item }
view="block"
isLoading={ blockWithdrawalsQuery.isPlaceholderData }
/>
)) }
</Show> </Show>
<Hide below="lg" ssr={ false }> <Hide below="lg" ssr={ false }>
<WithdrawalsTable items={ blockWithdrawalsQuery.data.items } view="block" top={ blockWithdrawalsQuery.isPaginationVisible ? 80 : 0 }/> <WithdrawalsTable
items={ blockWithdrawalsQuery.data.items }
isLoading={ blockWithdrawalsQuery.isPlaceholderData }
top={ blockWithdrawalsQuery.isPaginationVisible ? 80 : 0 }
view="block"
/>
</Hide> </Hide>
</> </>
) : null ; ) : null ;
...@@ -33,7 +45,7 @@ const BlockWithdrawals = ({ blockWithdrawalsQuery }: Props) => { ...@@ -33,7 +45,7 @@ const BlockWithdrawals = ({ blockWithdrawalsQuery }: Props) => {
return ( return (
<DataListDisplay <DataListDisplay
isError={ blockWithdrawalsQuery.isError } isError={ blockWithdrawalsQuery.isError }
isLoading={ blockWithdrawalsQuery.isLoading } isLoading={ false }
items={ blockWithdrawalsQuery.data?.items } items={ blockWithdrawalsQuery.data?.items }
skeletonProps={{ isLongSkeleton: true, skeletonDesktopColumns: Array(4).fill(`${ 100 / 4 }%`), skeletonDesktopMinW: '950px' }} skeletonProps={{ isLongSkeleton: true, skeletonDesktopColumns: Array(4).fill(`${ 100 / 4 }%`), skeletonDesktopMinW: '950px' }}
emptyText="There are no withdrawals for this block." emptyText="There are no withdrawals for this block."
......
import { Grid, GridItem, Skeleton } from '@chakra-ui/react';
import React from 'react';
import DetailsSkeletonRow from 'ui/shared/skeletons/DetailsSkeletonRow';
const BlockDetailsSkeleton = () => {
const sectionGap = (
<GridItem
colSpan={{ base: undefined, lg: 2 }}
mt={{ base: 2, lg: 3 }}
mb={{ base: 0, lg: 3 }}
borderBottom="1px solid"
borderColor="divider"
/>
);
return (
<Grid columnGap={ 8 } rowGap={{ base: 5, lg: 7 }} templateColumns={{ base: '1fr', lg: '210px 1fr' }} maxW="1000px">
<DetailsSkeletonRow w="25%"/>
<DetailsSkeletonRow w="25%"/>
<DetailsSkeletonRow w="65%"/>
<DetailsSkeletonRow w="25%"/>
<DetailsSkeletonRow/>
<DetailsSkeletonRow/>
{ sectionGap }
<DetailsSkeletonRow w="50%"/>
<DetailsSkeletonRow w="25%"/>
<DetailsSkeletonRow w="50%"/>
<DetailsSkeletonRow w="50%"/>
<DetailsSkeletonRow w="50%"/>
{ sectionGap }
<GridItem colSpan={{ base: undefined, lg: 2 }}>
<Skeleton h={ 5 } borderRadius="full" w="100px"/>
</GridItem>
</Grid>
);
};
export default BlockDetailsSkeleton;
import { Text } from '@chakra-ui/react'; import { Skeleton } from '@chakra-ui/react';
import type { TypographyProps } from '@chakra-ui/react'; import type { TypographyProps } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
...@@ -7,13 +7,18 @@ import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement'; ...@@ -7,13 +7,18 @@ import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
interface Props { interface Props {
ts: string; ts: string;
isEnabled?: boolean; isEnabled?: boolean;
isLoading?: boolean;
fontSize?: TypographyProps['fontSize']; fontSize?: TypographyProps['fontSize'];
} }
const BlockTimestamp = ({ ts, isEnabled, fontSize }: Props) => { const BlockTimestamp = ({ ts, isEnabled, isLoading, fontSize }: Props) => {
const timeAgo = useTimeAgoIncrement(ts, isEnabled); const timeAgo = useTimeAgoIncrement(ts, isEnabled);
return <Text variant="secondary" fontWeight={ 400 } fontSize={ fontSize }>{ timeAgo }</Text>; return (
<Skeleton isLoaded={ !isLoading } color="text_secondary" fontWeight={ 400 } fontSize={ fontSize } display="inline-block">
<span>{ timeAgo }</span>
</Skeleton>
);
}; };
export default React.memo(BlockTimestamp); export default React.memo(BlockTimestamp);
import { Show, Hide, Alert } from '@chakra-ui/react'; import { Alert, Box } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query'; import type { UseQueryResult } from '@tanstack/react-query';
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import React from 'react'; import React from 'react';
...@@ -66,7 +66,7 @@ const BlocksContent = ({ type, query }: Props) => { ...@@ -66,7 +66,7 @@ const BlocksContent = ({ type, query }: Props) => {
topic: 'blocks:new_block', topic: 'blocks:new_block',
onSocketClose: handleSocketClose, onSocketClose: handleSocketClose,
onSocketError: handleSocketError, onSocketError: handleSocketError,
isDisabled: query.isLoading || query.isError || query.pagination.page !== 1, isDisabled: query.isPlaceholderData || query.isError || query.pagination.page !== 1,
}); });
useSocketMessage({ useSocketMessage({
channel, channel,
...@@ -77,12 +77,12 @@ const BlocksContent = ({ type, query }: Props) => { ...@@ -77,12 +77,12 @@ const BlocksContent = ({ type, query }: Props) => {
const content = query.data?.items ? ( const content = query.data?.items ? (
<> <>
{ socketAlert && <Alert status="warning" mb={ 6 } as="a" href={ window.document.location.href }>{ socketAlert }</Alert> } { socketAlert && <Alert status="warning" mb={ 6 } as="a" href={ window.document.location.href }>{ socketAlert }</Alert> }
<Show below="lg" key="content-mobile" ssr={ false }> <Box display={{ base: 'block', lg: 'none' }}>
<BlocksList data={ query.data.items }/> <BlocksList data={ query.data.items } isLoading={ query.isPlaceholderData } page={ query.pagination.page }/>
</Show> </Box>
<Hide below="lg" key="content-desktop" ssr={ false }> <Box display={{ base: 'none', lg: 'block' }}>
<BlocksTable data={ query.data.items } top={ query.isPaginationVisible ? 80 : 0 } page={ query.pagination.page }/> <BlocksTable data={ query.data.items } top={ query.isPaginationVisible ? 80 : 0 } page={ query.pagination.page } isLoading={ query.isPlaceholderData }/>
</Hide> </Box>
</> </>
) : null; ) : null;
...@@ -95,7 +95,7 @@ const BlocksContent = ({ type, query }: Props) => { ...@@ -95,7 +95,7 @@ const BlocksContent = ({ type, query }: Props) => {
return ( return (
<DataListDisplay <DataListDisplay
isError={ query.isError } isError={ query.isError }
isLoading={ query.isLoading } isLoading={ false }
items={ query.data?.items } items={ query.data?.items }
skeletonProps={{ skeletonDesktopColumns: [ '125px', '120px', '21%', '64px', '35%', '22%', '22%' ] }} skeletonProps={{ skeletonDesktopColumns: [ '125px', '120px', '21%', '64px', '35%', '22%', '22%' ] }}
emptyText="There are no blocks." emptyText="There are no blocks."
......
...@@ -8,14 +8,22 @@ import BlocksListItem from 'ui/blocks/BlocksListItem'; ...@@ -8,14 +8,22 @@ import BlocksListItem from 'ui/blocks/BlocksListItem';
interface Props { interface Props {
data: Array<Block>; data: Array<Block>;
isLoading: boolean;
page: number;
} }
const BlocksList = ({ data }: Props) => { const BlocksList = ({ data, isLoading, page }: Props) => {
return ( return (
<Box> <Box>
<AnimatePresence initial={ false }> <AnimatePresence initial={ false }>
{ /* TODO prop "enableTimeIncrement" should be set to false for second and later pages */ } { data.map((item, index) => (
{ data.map((item) => <BlocksListItem key={ item.height } data={ item } enableTimeIncrement/>) } <BlocksListItem
key={ item.height + (isLoading ? String(index) : '') }
data={ item }
isLoading={ isLoading }
enableTimeIncrement={ page === 1 && !isLoading }
/>
)) }
</AnimatePresence> </AnimatePresence>
</Box> </Box>
); );
......
import { Flex, Spinner, Text, Box, Icon, useColorModeValue } from '@chakra-ui/react'; import { Flex, Skeleton, Text, Box, useColorModeValue } from '@chakra-ui/react';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import capitalize from 'lodash/capitalize'; import capitalize from 'lodash/capitalize';
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
...@@ -13,6 +13,7 @@ import { WEI } from 'lib/consts'; ...@@ -13,6 +13,7 @@ import { WEI } from 'lib/consts';
import getNetworkValidatorTitle from 'lib/networks/getNetworkValidatorTitle'; import getNetworkValidatorTitle from 'lib/networks/getNetworkValidatorTitle';
import BlockTimestamp from 'ui/blocks/BlockTimestamp'; import BlockTimestamp from 'ui/blocks/BlockTimestamp';
import AddressLink from 'ui/shared/address/AddressLink'; import AddressLink from 'ui/shared/address/AddressLink';
import Icon from 'ui/shared/chakra/Icon';
import GasUsedToTargetRatio from 'ui/shared/GasUsedToTargetRatio'; import GasUsedToTargetRatio from 'ui/shared/GasUsedToTargetRatio';
import LinkInternal from 'ui/shared/LinkInternal'; import LinkInternal from 'ui/shared/LinkInternal';
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile'; import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
...@@ -21,11 +22,11 @@ import Utilization from 'ui/shared/Utilization/Utilization'; ...@@ -21,11 +22,11 @@ import Utilization from 'ui/shared/Utilization/Utilization';
interface Props { interface Props {
data: Block; data: Block;
isPending?: boolean; isLoading?: boolean;
enableTimeIncrement?: boolean; enableTimeIncrement?: boolean;
} }
const BlocksListItem = ({ data, isPending, enableTimeIncrement }: Props) => { const BlocksListItem = ({ data, isLoading, enableTimeIncrement }: Props) => {
const totalReward = getBlockTotalReward(data); const totalReward = getBlockTotalReward(data);
const burntFees = BigNumber(data.burnt_fees || 0); const burntFees = BigNumber(data.burnt_fees || 0);
const txFees = BigNumber(data.tx_fees || 0); const txFees = BigNumber(data.tx_fees || 0);
...@@ -36,30 +37,35 @@ const BlocksListItem = ({ data, isPending, enableTimeIncrement }: Props) => { ...@@ -36,30 +37,35 @@ const BlocksListItem = ({ data, isPending, enableTimeIncrement }: Props) => {
<ListItemMobile rowGap={ 3 } key={ String(data.height) } isAnimated> <ListItemMobile rowGap={ 3 } key={ String(data.height) } isAnimated>
<Flex justifyContent="space-between" w="100%"> <Flex justifyContent="space-between" w="100%">
<Flex columnGap={ 2 } alignItems="center"> <Flex columnGap={ 2 } alignItems="center">
{ isPending && <Spinner size="sm"/> } <Skeleton isLoaded={ !isLoading } display="inline-block">
<LinkInternal <LinkInternal
fontWeight={ 600 } fontWeight={ 600 }
href={ route({ pathname: '/block/[height]', query: { height: data.type === 'reorg' ? String(data.hash) : String(data.height) } }) } href={ route({ pathname: '/block/[height]', query: { height: data.type === 'reorg' ? String(data.hash) : String(data.height) } }) }
> >
{ data.height } { data.height }
</LinkInternal> </LinkInternal>
</Skeleton>
</Flex> </Flex>
<BlockTimestamp ts={ data.timestamp } isEnabled={ enableTimeIncrement }/> <BlockTimestamp ts={ data.timestamp } isEnabled={ enableTimeIncrement } isLoading={ isLoading }/>
</Flex> </Flex>
<Flex columnGap={ 2 }> <Flex columnGap={ 2 }>
<Text fontWeight={ 500 }>Size</Text> <Text fontWeight={ 500 }>Size</Text>
<Text variant="secondary">{ data.size.toLocaleString() } bytes</Text> <Skeleton isLoaded={ !isLoading } display="inline-block" color="text_secondary">
<span>{ data.size.toLocaleString() } bytes</span>
</Skeleton>
</Flex> </Flex>
<Flex columnGap={ 2 }> <Flex columnGap={ 2 }>
<Text fontWeight={ 500 }>{ capitalize(getNetworkValidatorTitle()) }</Text> <Text fontWeight={ 500 }>{ capitalize(getNetworkValidatorTitle()) }</Text>
<AddressLink type="address" alias={ data.miner.name } hash={ data.miner.hash } truncation="constant"/> <AddressLink type="address" alias={ data.miner.name } hash={ data.miner.hash } truncation="constant" isLoading={ isLoading }/>
</Flex> </Flex>
<Flex columnGap={ 2 }> <Flex columnGap={ 2 }>
<Text fontWeight={ 500 }>Txn</Text> <Text fontWeight={ 500 }>Txn</Text>
{ data.tx_count > 0 ? ( { data.tx_count > 0 ? (
<LinkInternal href={ route({ pathname: '/block/[height]', query: { height: String(data.height), tab: 'txs' } }) }> <Skeleton isLoaded={ !isLoading } display="inline-block">
{ data.tx_count } <LinkInternal href={ route({ pathname: '/block/[height]', query: { height: String(data.height), tab: 'txs' } }) }>
</LinkInternal> { data.tx_count }
</LinkInternal>
</Skeleton>
) : ) :
<Text variant="secondary">{ data.tx_count }</Text> <Text variant="secondary">{ data.tx_count }</Text>
} }
...@@ -67,12 +73,14 @@ const BlocksListItem = ({ data, isPending, enableTimeIncrement }: Props) => { ...@@ -67,12 +73,14 @@ const BlocksListItem = ({ data, isPending, enableTimeIncrement }: Props) => {
<Box> <Box>
<Text fontWeight={ 500 }>Gas used</Text> <Text fontWeight={ 500 }>Gas used</Text>
<Flex mt={ 2 }> <Flex mt={ 2 }>
<Text variant="secondary" mr={ 4 }>{ BigNumber(data.gas_used || 0).toFormat() }</Text> <Skeleton isLoaded={ !isLoading } display="inline-block" color="text_secondary" mr={ 4 }>
<Utilization colorScheme="gray" value={ BigNumber(data.gas_used || 0).div(BigNumber(data.gas_limit)).toNumber() }/> <span>{ BigNumber(data.gas_used || 0).toFormat() }</span>
</Skeleton>
<Utilization colorScheme="gray" value={ BigNumber(data.gas_used || 0).div(BigNumber(data.gas_limit)).toNumber() } isLoading={ isLoading }/>
{ data.gas_target_percentage && ( { data.gas_target_percentage && (
<> <>
<TextSeparator color={ separatorColor } mx={ 1 }/> <TextSeparator color={ separatorColor } mx={ 1 }/>
<GasUsedToTargetRatio value={ data.gas_target_percentage }/> <GasUsedToTargetRatio value={ data.gas_target_percentage } isLoading={ isLoading }/>
</> </>
) } ) }
</Flex> </Flex>
...@@ -80,7 +88,9 @@ const BlocksListItem = ({ data, isPending, enableTimeIncrement }: Props) => { ...@@ -80,7 +88,9 @@ const BlocksListItem = ({ data, isPending, enableTimeIncrement }: Props) => {
{ !appConfig.L2.isL2Network && ( { !appConfig.L2.isL2Network && (
<Flex columnGap={ 2 }> <Flex columnGap={ 2 }>
<Text fontWeight={ 500 }>Reward { appConfig.network.currency.symbol }</Text> <Text fontWeight={ 500 }>Reward { appConfig.network.currency.symbol }</Text>
<Text variant="secondary">{ totalReward.toFixed() }</Text> <Skeleton isLoaded={ !isLoading } display="inline-block" color="text_secondary">
<span>{ totalReward.toFixed() }</span>
</Skeleton>
</Flex> </Flex>
) } ) }
{ !appConfig.L2.isL2Network && ( { !appConfig.L2.isL2Network && (
...@@ -88,10 +98,12 @@ const BlocksListItem = ({ data, isPending, enableTimeIncrement }: Props) => { ...@@ -88,10 +98,12 @@ const BlocksListItem = ({ data, isPending, enableTimeIncrement }: Props) => {
<Text fontWeight={ 500 }>Burnt fees</Text> <Text fontWeight={ 500 }>Burnt fees</Text>
<Flex columnGap={ 4 } mt={ 2 }> <Flex columnGap={ 4 } mt={ 2 }>
<Flex> <Flex>
<Icon as={ flameIcon } boxSize={ 5 } color="gray.500"/> <Icon as={ flameIcon } boxSize={ 5 } color="gray.500" isLoading={ isLoading }/>
<Text variant="secondary" ml={ 1 }>{ burntFees.div(WEI).toFixed() }</Text> <Skeleton isLoaded={ !isLoading } display="inline-block" color="text_secondary" ml={ 1 }>
<span>{ burntFees.div(WEI).toFixed() }</span>
</Skeleton>
</Flex> </Flex>
<Utilization ml={ 4 } value={ burntFees.div(txFees).toNumber() }/> <Utilization ml={ 4 } value={ burntFees.div(txFees).toNumber() } isLoading={ isLoading }/>
</Flex> </Flex>
</Box> </Box>
) } ) }
......
...@@ -2,8 +2,8 @@ import { Flex, Box, Text, Skeleton } from '@chakra-ui/react'; ...@@ -2,8 +2,8 @@ import { Flex, Box, Text, Skeleton } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile';
import { nbsp } from 'lib/html-entities'; import { nbsp } from 'lib/html-entities';
import { HOMEPAGE_STATS } from 'stubs/stats';
import type { Props as PaginationProps } from 'ui/shared/Pagination'; import type { Props as PaginationProps } from 'ui/shared/Pagination';
import Pagination from 'ui/shared/Pagination'; import Pagination from 'ui/shared/Pagination';
...@@ -13,24 +13,22 @@ interface Props { ...@@ -13,24 +13,22 @@ interface Props {
} }
const BlocksTabSlot = ({ pagination, isPaginationVisible }: Props) => { const BlocksTabSlot = ({ pagination, isPaginationVisible }: Props) => {
const isMobile = useIsMobile(); const statsQuery = useApiQuery('homepage_stats', {
const statsQuery = useApiQuery('homepage_stats'); queryOptions: {
placeholderData: HOMEPAGE_STATS,
if (isMobile) { },
return null; });
}
return ( return (
<Flex alignItems="center" columnGap={ 8 }> <Flex alignItems="center" columnGap={ 8 } display={{ base: 'none', lg: 'flex' }}>
{ statsQuery.isLoading && <Skeleton w="175px" h="24px"/> }
{ statsQuery.data?.network_utilization_percentage !== undefined && ( { statsQuery.data?.network_utilization_percentage !== undefined && (
<Box> <Box>
<Text as="span" fontSize="sm"> <Text as="span" fontSize="sm">
Network utilization (last 50 blocks):{ nbsp } Network utilization (last 50 blocks):{ nbsp }
</Text> </Text>
<Text as="span" fontSize="sm" color="blue.400" fontWeight={ 600 }> <Skeleton display="inline-block" fontSize="sm" color="blue.400" fontWeight={ 600 } isLoaded={ !statsQuery.isPlaceholderData }>
{ statsQuery.data.network_utilization_percentage.toFixed(2) }% <span>{ statsQuery.data.network_utilization_percentage.toFixed(2) }%</span>
</Text> </Skeleton>
</Box> </Box>
) } ) }
{ isPaginationVisible && <Pagination my={ 1 } { ...pagination }/> } { isPaginationVisible && <Pagination my={ 1 } { ...pagination }/> }
......
...@@ -12,11 +12,12 @@ import { default as Thead } from 'ui/shared/TheadSticky'; ...@@ -12,11 +12,12 @@ import { default as Thead } from 'ui/shared/TheadSticky';
interface Props { interface Props {
data: Array<Block>; data: Array<Block>;
isLoading?: boolean;
top: number; top: number;
page: number; page: number;
} }
const BlocksTable = ({ data, top, page }: Props) => { const BlocksTable = ({ data, isLoading, top, page }: Props) => {
return ( return (
<Table variant="simple" minWidth="1040px" size="md" fontWeight={ 500 }> <Table variant="simple" minWidth="1040px" size="md" fontWeight={ 500 }>
...@@ -33,7 +34,14 @@ const BlocksTable = ({ data, top, page }: Props) => { ...@@ -33,7 +34,14 @@ const BlocksTable = ({ data, top, page }: Props) => {
</Thead> </Thead>
<Tbody> <Tbody>
<AnimatePresence initial={ false }> <AnimatePresence initial={ false }>
{ data.map((item) => <BlocksTableItem key={ item.height } data={ item } enableTimeIncrement={ page === 1 }/>) } { data.map((item, index) => (
<BlocksTableItem
key={ item.height + (isLoading ? `${ index }_${ page }` : '') }
data={ item }
enableTimeIncrement={ page === 1 && !isLoading }
isLoading={ isLoading }
/>
)) }
</AnimatePresence> </AnimatePresence>
</Tbody> </Tbody>
</Table> </Table>
......
import { Tr, Td, Flex, Box, Icon, Tooltip, Spinner, useColorModeValue } from '@chakra-ui/react'; import { Tr, Td, Flex, Box, Tooltip, Skeleton, useColorModeValue } from '@chakra-ui/react';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
...@@ -12,6 +12,7 @@ import getBlockTotalReward from 'lib/block/getBlockTotalReward'; ...@@ -12,6 +12,7 @@ import getBlockTotalReward from 'lib/block/getBlockTotalReward';
import { WEI } from 'lib/consts'; import { WEI } from 'lib/consts';
import BlockTimestamp from 'ui/blocks/BlockTimestamp'; import BlockTimestamp from 'ui/blocks/BlockTimestamp';
import AddressLink from 'ui/shared/address/AddressLink'; import AddressLink from 'ui/shared/address/AddressLink';
import Icon from 'ui/shared/chakra/Icon';
import GasUsedToTargetRatio from 'ui/shared/GasUsedToTargetRatio'; import GasUsedToTargetRatio from 'ui/shared/GasUsedToTargetRatio';
import LinkInternal from 'ui/shared/LinkInternal'; import LinkInternal from 'ui/shared/LinkInternal';
import TextSeparator from 'ui/shared/TextSeparator'; import TextSeparator from 'ui/shared/TextSeparator';
...@@ -19,17 +20,18 @@ import Utilization from 'ui/shared/Utilization/Utilization'; ...@@ -19,17 +20,18 @@ import Utilization from 'ui/shared/Utilization/Utilization';
interface Props { interface Props {
data: Block; data: Block;
isPending?: boolean; isLoading?: boolean;
enableTimeIncrement?: boolean; enableTimeIncrement?: boolean;
} }
const BlocksTableItem = ({ data, isPending, enableTimeIncrement }: Props) => { const BlocksTableItem = ({ data, isLoading, enableTimeIncrement }: Props) => {
const totalReward = getBlockTotalReward(data); const totalReward = getBlockTotalReward(data);
const burntFees = BigNumber(data.burnt_fees || 0); const burntFees = BigNumber(data.burnt_fees || 0);
const txFees = BigNumber(data.tx_fees || 0); const txFees = BigNumber(data.tx_fees || 0);
const separatorColor = useColorModeValue('gray.200', 'gray.700'); const separatorColor = useColorModeValue('gray.200', 'gray.700');
const burntFeesIconColor = useColorModeValue('gray.500', 'inherit'); const burntFeesIconColor = useColorModeValue('gray.500', 'inherit');
return ( return (
<Tr <Tr
as={ motion.tr } as={ motion.tr }
...@@ -41,57 +43,82 @@ const BlocksTableItem = ({ data, isPending, enableTimeIncrement }: Props) => { ...@@ -41,57 +43,82 @@ const BlocksTableItem = ({ data, isPending, enableTimeIncrement }: Props) => {
> >
<Td fontSize="sm"> <Td fontSize="sm">
<Flex columnGap={ 2 } alignItems="center" mb={ 2 }> <Flex columnGap={ 2 } alignItems="center" mb={ 2 }>
{ isPending && <Spinner size="sm" flexShrink={ 0 }/> }
<Tooltip isDisabled={ data.type !== 'reorg' } label="Chain reorganizations"> <Tooltip isDisabled={ data.type !== 'reorg' } label="Chain reorganizations">
<LinkInternal <Skeleton isLoaded={ !isLoading } display="inline-block">
fontWeight={ 600 } <LinkInternal
href={ route({ pathname: '/block/[height]', query: { height: data.type === 'reorg' ? String(data.hash) : String(data.height) } }) } fontWeight={ 600 }
> href={ route({ pathname: '/block/[height]', query: { height: data.type === 'reorg' ? String(data.hash) : String(data.height) } }) }
{ data.height } >
</LinkInternal> { data.height }
</LinkInternal>
</Skeleton>
</Tooltip> </Tooltip>
</Flex> </Flex>
<BlockTimestamp ts={ data.timestamp } isEnabled={ enableTimeIncrement }/> <BlockTimestamp ts={ data.timestamp } isEnabled={ enableTimeIncrement } isLoading={ isLoading }/>
</Td>
<Td fontSize="sm">
<Skeleton isLoaded={ !isLoading } display="inline-block">
{ data.size.toLocaleString() }
</Skeleton>
</Td> </Td>
<Td fontSize="sm">{ data.size.toLocaleString() }</Td>
<Td fontSize="sm"> <Td fontSize="sm">
<AddressLink type="address" alias={ data.miner.name } hash={ data.miner.hash } truncation="constant" display="inline-flex" maxW="100%"/> <AddressLink
type="address"
alias={ data.miner.name }
hash={ data.miner.hash }
truncation="constant"
display="inline-flex"
maxW="100%"
isLoading={ isLoading }
/>
</Td> </Td>
<Td isNumeric fontSize="sm"> <Td isNumeric fontSize="sm">
{ data.tx_count > 0 ? ( { data.tx_count > 0 ? (
<LinkInternal href={ route({ pathname: '/block/[height]', query: { height: String(data.height), tab: 'txs' } }) }> <Skeleton isLoaded={ !isLoading } display="inline-block">
{ data.tx_count } <LinkInternal href={ route({ pathname: '/block/[height]', query: { height: String(data.height), tab: 'txs' } }) }>
</LinkInternal> { data.tx_count }
</LinkInternal>
</Skeleton>
) : data.tx_count } ) : data.tx_count }
</Td> </Td>
{ !appConfig.L2.isL2Network && ( { !appConfig.L2.isL2Network && (
<Td fontSize="sm"> <Td fontSize="sm">
<Box>{ BigNumber(data.gas_used || 0).toFormat() }</Box> <Skeleton isLoaded={ !isLoading } display="inline-block">{ BigNumber(data.gas_used || 0).toFormat() }</Skeleton>
<Flex mt={ 2 }> <Flex mt={ 2 }>
<Tooltip label="Gas Used %"> <Tooltip label={ isLoading ? undefined : 'Gas Used %' }>
<Box> <Box>
<Utilization colorScheme="gray" value={ BigNumber(data.gas_used || 0).dividedBy(BigNumber(data.gas_limit)).toNumber() }/> <Utilization
colorScheme="gray"
value={ BigNumber(data.gas_used || 0).dividedBy(BigNumber(data.gas_limit)).toNumber() }
isLoading={ isLoading }
/>
</Box> </Box>
</Tooltip> </Tooltip>
{ data.gas_target_percentage && ( { data.gas_target_percentage && (
<> <>
<TextSeparator color={ separatorColor } mx={ 1 }/> <TextSeparator color={ separatorColor } mx={ 1 }/>
<GasUsedToTargetRatio value={ data.gas_target_percentage }/> <GasUsedToTargetRatio value={ data.gas_target_percentage } isLoading={ isLoading }/>
</> </>
) } ) }
</Flex> </Flex>
</Td> </Td>
) } ) }
<Td fontSize="sm">{ totalReward.toFixed(8) }</Td> <Td fontSize="sm">
<Skeleton isLoaded={ !isLoading } display="inline-block">
{ totalReward.toFixed(8) }
</Skeleton>
</Td>
{ !appConfig.L2.isL2Network && ( { !appConfig.L2.isL2Network && (
<Td fontSize="sm"> <Td fontSize="sm">
<Flex alignItems="center" columnGap={ 1 }> <Flex alignItems="center" columnGap={ 1 }>
<Icon as={ flameIcon } boxSize={ 5 } color={ burntFeesIconColor }/> <Icon as={ flameIcon } boxSize={ 5 } color={ burntFeesIconColor } isLoading={ isLoading }/>
{ burntFees.dividedBy(WEI).toFixed(8) } <Skeleton isLoaded={ !isLoading } display="inline-block">
{ burntFees.dividedBy(WEI).toFixed(8) }
</Skeleton>
</Flex> </Flex>
<Tooltip label="Burnt fees / Txn fees * 100%"> <Tooltip label={ isLoading ? undefined : 'Burnt fees / Txn fees * 100%' }>
<Box w="min-content"> <Box w="min-content">
<Utilization mt={ 2 } value={ burntFees.div(txFees).toNumber() }/> <Utilization mt={ 2 } value={ burntFees.div(txFees).toNumber() } isLoading={ isLoading }/>
</Box> </Box>
</Tooltip> </Tooltip>
</Td> </Td>
......
import { Skeleton } from '@chakra-ui/react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
...@@ -10,6 +9,10 @@ import { useAppContext } from 'lib/appContext'; ...@@ -10,6 +9,10 @@ import { useAppContext } from 'lib/appContext';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import useQueryWithPages from 'lib/hooks/useQueryWithPages'; import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import getQueryParamString from 'lib/router/getQueryParamString'; import getQueryParamString from 'lib/router/getQueryParamString';
import { BLOCK } from 'stubs/block';
import { TX } from 'stubs/tx';
import { generateListStub } from 'stubs/utils';
import { WITHDRAWAL } from 'stubs/withdrawals';
import BlockDetails from 'ui/block/BlockDetails'; import BlockDetails from 'ui/block/BlockDetails';
import BlockWithdrawals from 'ui/block/BlockWithdrawals'; import BlockWithdrawals from 'ui/block/BlockWithdrawals';
import TextAd from 'ui/shared/ad/TextAd'; import TextAd from 'ui/shared/ad/TextAd';
...@@ -36,14 +39,22 @@ const BlockPageContent = () => { ...@@ -36,14 +39,22 @@ const BlockPageContent = () => {
const blockQuery = useApiQuery('block', { const blockQuery = useApiQuery('block', {
pathParams: { height }, pathParams: { height },
queryOptions: { enabled: Boolean(height) }, queryOptions: {
enabled: Boolean(height),
placeholderData: BLOCK,
},
}); });
const blockTxsQuery = useQueryWithPages({ const blockTxsQuery = useQueryWithPages({
resourceName: 'block_txs', resourceName: 'block_txs',
pathParams: { height }, pathParams: { height },
options: { options: {
enabled: Boolean(blockQuery.data?.height && tab === 'txs'), enabled: Boolean(!blockQuery.isPlaceholderData && blockQuery.data?.height && tab === 'txs'),
placeholderData: generateListStub<'block_txs'>(TX, 50, {
block_number: 9004925,
index: 49,
items_count: 50,
}),
}, },
}); });
...@@ -51,7 +62,11 @@ const BlockPageContent = () => { ...@@ -51,7 +62,11 @@ const BlockPageContent = () => {
resourceName: 'block_withdrawals', resourceName: 'block_withdrawals',
pathParams: { height }, pathParams: { height },
options: { options: {
enabled: Boolean(blockQuery.data?.height && appConfig.beaconChain.hasBeaconChain && tab === 'withdrawals'), enabled: Boolean(!blockQuery.isPlaceholderData && blockQuery.data?.height && appConfig.beaconChain.hasBeaconChain && tab === 'withdrawals'),
placeholderData: generateListStub<'block_withdrawals'>(WITHDRAWAL, 50, {
index: 5,
items_count: 50,
}),
}, },
}); });
...@@ -98,17 +113,14 @@ const BlockPageContent = () => { ...@@ -98,17 +113,14 @@ const BlockPageContent = () => {
return ( return (
<> <>
{ blockQuery.isLoading ? <Skeleton h={{ base: 12, lg: 6 }} mb={ 6 } w="100%" maxW="680px"/> : <TextAd mb={ 6 }/> } <TextAd mb={ 6 }/>
{ blockQuery.isLoading ? ( <PageTitle
<Skeleton h={ 10 } w="300px" mb={ 6 }/> title={ `Block #${ blockQuery.data?.height }` }
) : ( backLink={ backLink }
<PageTitle contentAfter={ <NetworkExplorers type="block" pathParam={ height } ml={{ base: 'initial', lg: 'auto' }}/> }
title={ `Block #${ blockQuery.data?.height }` } isLoading={ blockQuery.isPlaceholderData }
backLink={ backLink } />
contentAfter={ <NetworkExplorers type="block" pathParam={ height } ml={{ base: 'initial', lg: 'auto' }}/> } { blockQuery.isPlaceholderData ? <SkeletonTabs tabs={ tabs }/> : (
/>
) }
{ blockQuery.isLoading ? <SkeletonTabs/> : (
<RoutedTabs <RoutedTabs
tabs={ tabs } tabs={ tabs }
tabListProps={ isMobile ? undefined : TAB_LIST_PROPS } tabListProps={ isMobile ? undefined : TAB_LIST_PROPS }
......
...@@ -58,7 +58,7 @@ test('base view +@mobile +@dark-mode', async({ mount, page }) => { ...@@ -58,7 +58,7 @@ test('base view +@mobile +@dark-mode', async({ mount, page }) => {
); );
await page.waitForResponse(BLOCKS_API_URL); await page.waitForResponse(BLOCKS_API_URL);
await expect(component.locator('main')).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
test('new item from socket', async({ mount, page, createSocket }) => { test('new item from socket', async({ mount, page, createSocket }) => {
...@@ -85,7 +85,7 @@ test('new item from socket', async({ mount, page, createSocket }) => { ...@@ -85,7 +85,7 @@ test('new item from socket', async({ mount, page, createSocket }) => {
}, },
}); });
await expect(component.locator('main')).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
test('socket error', async({ mount, page, createSocket }) => { test('socket error', async({ mount, page, createSocket }) => {
...@@ -105,5 +105,5 @@ test('socket error', async({ mount, page, createSocket }) => { ...@@ -105,5 +105,5 @@ test('socket error', async({ mount, page, createSocket }) => {
await socketServer.joinChannel(socket, 'blocks:new_block'); await socketServer.joinChannel(socket, 'blocks:new_block');
socket.close(); socket.close();
await expect(component.locator('main')).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
...@@ -6,9 +6,10 @@ import type { RoutedTab } from 'ui/shared/Tabs/types'; ...@@ -6,9 +6,10 @@ import type { RoutedTab } from 'ui/shared/Tabs/types';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import useQueryWithPages from 'lib/hooks/useQueryWithPages'; import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import { BLOCK } from 'stubs/block';
import { generateListStub } from 'stubs/utils';
import BlocksContent from 'ui/blocks/BlocksContent'; import BlocksContent from 'ui/blocks/BlocksContent';
import BlocksTabSlot from 'ui/blocks/BlocksTabSlot'; import BlocksTabSlot from 'ui/blocks/BlocksTabSlot';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle'; import PageTitle from 'ui/shared/Page/PageTitle';
import RoutedTabs from 'ui/shared/Tabs/RoutedTabs'; import RoutedTabs from 'ui/shared/Tabs/RoutedTabs';
...@@ -32,6 +33,12 @@ const BlocksPageContent = () => { ...@@ -32,6 +33,12 @@ const BlocksPageContent = () => {
const blocksQuery = useQueryWithPages({ const blocksQuery = useQueryWithPages({
resourceName: 'blocks', resourceName: 'blocks',
filters: { type }, filters: { type },
options: {
placeholderData: generateListStub<'blocks'>(BLOCK, 50, {
block_number: 8988686,
items_count: 50,
}),
},
}); });
const tabs: Array<RoutedTab> = [ const tabs: Array<RoutedTab> = [
...@@ -41,7 +48,7 @@ const BlocksPageContent = () => { ...@@ -41,7 +48,7 @@ const BlocksPageContent = () => {
]; ];
return ( return (
<Page> <>
<PageTitle title="Blocks" withTextAd/> <PageTitle title="Blocks" withTextAd/>
<RoutedTabs <RoutedTabs
tabs={ tabs } tabs={ tabs }
...@@ -49,7 +56,7 @@ const BlocksPageContent = () => { ...@@ -49,7 +56,7 @@ const BlocksPageContent = () => {
rightSlot={ <BlocksTabSlot pagination={ blocksQuery.pagination } isPaginationVisible={ blocksQuery.isPaginationVisible }/> } rightSlot={ <BlocksTabSlot pagination={ blocksQuery.pagination } isPaginationVisible={ blocksQuery.isPaginationVisible }/> }
stickyEnabled={ !isMobile } stickyEnabled={ !isMobile }
/> />
</Page> </>
); );
}; };
......
...@@ -20,6 +20,7 @@ import useSocketMessage from 'lib/socket/useSocketMessage'; ...@@ -20,6 +20,7 @@ import useSocketMessage from 'lib/socket/useSocketMessage';
import trimTokenSymbol from 'lib/token/trimTokenSymbol'; import trimTokenSymbol from 'lib/token/trimTokenSymbol';
import * as addressStubs from 'stubs/address'; import * as addressStubs from 'stubs/address';
import * as tokenStubs from 'stubs/token'; import * as tokenStubs from 'stubs/token';
import { generateListStub } from 'stubs/utils';
import AddressContract from 'ui/address/AddressContract'; import AddressContract from 'ui/address/AddressContract';
import TextAd from 'ui/shared/ad/TextAd'; import TextAd from 'ui/shared/ad/TextAd';
import EntityTags from 'ui/shared/EntityTags'; import EntityTags from 'ui/shared/EntityTags';
...@@ -145,7 +146,7 @@ const TokenPageContent = () => { ...@@ -145,7 +146,7 @@ const TokenPageContent = () => {
scrollRef, scrollRef,
options: { options: {
enabled: Boolean(router.query.hash && router.query.tab === 'inventory' && hasData), enabled: Boolean(router.query.hash && router.query.tab === 'inventory' && hasData),
placeholderData: tokenStubs.TOKEN_INSTANCES, placeholderData: generateListStub<'token_inventory'>(tokenStubs.TOKEN_INSTANCE),
}, },
}); });
......
...@@ -7,10 +7,10 @@ import useApiQuery from 'lib/api/useApiQuery'; ...@@ -7,10 +7,10 @@ import useApiQuery from 'lib/api/useApiQuery';
import { useAppContext } from 'lib/appContext'; import { useAppContext } from 'lib/appContext';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import getQueryParamString from 'lib/router/getQueryParamString'; import getQueryParamString from 'lib/router/getQueryParamString';
import { TX } from 'stubs/tx';
import TextAd from 'ui/shared/ad/TextAd'; import TextAd from 'ui/shared/ad/TextAd';
import EntityTags from 'ui/shared/EntityTags'; import EntityTags from 'ui/shared/EntityTags';
import NetworkExplorers from 'ui/shared/NetworkExplorers'; import NetworkExplorers from 'ui/shared/NetworkExplorers';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle'; import PageTitle from 'ui/shared/Page/PageTitle';
import RoutedTabs from 'ui/shared/Tabs/RoutedTabs'; import RoutedTabs from 'ui/shared/Tabs/RoutedTabs';
import TxDetails from 'ui/tx/TxDetails'; import TxDetails from 'ui/tx/TxDetails';
...@@ -38,7 +38,10 @@ const TransactionPageContent = () => { ...@@ -38,7 +38,10 @@ const TransactionPageContent = () => {
const { data, isPlaceholderData } = useApiQuery('tx', { const { data, isPlaceholderData } = useApiQuery('tx', {
pathParams: { hash }, pathParams: { hash },
queryOptions: { enabled: Boolean(hash) }, queryOptions: {
enabled: Boolean(hash),
placeholderData: TX,
},
}); });
const tags = ( const tags = (
...@@ -65,7 +68,7 @@ const TransactionPageContent = () => { ...@@ -65,7 +68,7 @@ const TransactionPageContent = () => {
}, [ appProps.referrer ]); }, [ appProps.referrer ]);
return ( return (
<Page> <>
<TextAd mb={ 6 }/> <TextAd mb={ 6 }/>
<PageTitle <PageTitle
title="Transaction details" title="Transaction details"
...@@ -73,7 +76,7 @@ const TransactionPageContent = () => { ...@@ -73,7 +76,7 @@ const TransactionPageContent = () => {
contentAfter={ tags } contentAfter={ tags }
/> />
<RoutedTabs tabs={ TABS }/> <RoutedTabs tabs={ TABS }/>
</Page> </>
); );
}; };
......
import { Box } from '@chakra-ui/react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
...@@ -9,7 +8,8 @@ import useHasAccount from 'lib/hooks/useHasAccount'; ...@@ -9,7 +8,8 @@ import useHasAccount from 'lib/hooks/useHasAccount';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import useNewTxsSocket from 'lib/hooks/useNewTxsSocket'; import useNewTxsSocket from 'lib/hooks/useNewTxsSocket';
import useQueryWithPages from 'lib/hooks/useQueryWithPages'; import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import Page from 'ui/shared/Page/Page'; import { TX } from 'stubs/tx';
import { generateListStub } from 'stubs/utils';
import PageTitle from 'ui/shared/Page/PageTitle'; import PageTitle from 'ui/shared/Page/PageTitle';
import RoutedTabs from 'ui/shared/Tabs/RoutedTabs'; import RoutedTabs from 'ui/shared/Tabs/RoutedTabs';
import TxsContent from 'ui/txs/TxsContent'; import TxsContent from 'ui/txs/TxsContent';
...@@ -31,6 +31,12 @@ const Transactions = () => { ...@@ -31,6 +31,12 @@ const Transactions = () => {
filters: { filter: router.query.tab === 'pending' ? 'pending' : 'validated' }, filters: { filter: router.query.tab === 'pending' ? 'pending' : 'validated' },
options: { options: {
enabled: !router.query.tab || router.query.tab === 'validated' || router.query.tab === 'pending', enabled: !router.query.tab || router.query.tab === 'validated' || router.query.tab === 'pending',
placeholderData: generateListStub<'txs_validated'>(TX, 50, {
block_number: 9005713,
index: 5,
items_count: 50,
filter: 'validated',
}),
}, },
}); });
...@@ -71,17 +77,15 @@ const Transactions = () => { ...@@ -71,17 +77,15 @@ const Transactions = () => {
].filter(Boolean); ].filter(Boolean);
return ( return (
<Page> <>
<Box h="100%"> <PageTitle title="Transactions" withTextAd/>
<PageTitle title="Transactions" withTextAd/> <RoutedTabs
<RoutedTabs tabs={ tabs }
tabs={ tabs } tabListProps={ isMobile ? undefined : TAB_LIST_PROPS }
tabListProps={ isMobile ? undefined : TAB_LIST_PROPS } rightSlot={ <TxsTabSlot pagination={ txsQuery.pagination } isPaginationVisible={ txsQuery.isPaginationVisible && !isMobile }/> }
rightSlot={ <TxsTabSlot pagination={ txsQuery.pagination } isPaginationVisible={ txsQuery.isPaginationVisible && !isMobile }/> } stickyEnabled={ !isMobile }
stickyEnabled={ !isMobile } />
/> </>
</Box>
</Page>
); );
}; };
......
...@@ -32,5 +32,5 @@ test('base view +@mobile', async({ mount, page }) => { ...@@ -32,5 +32,5 @@ test('base view +@mobile', async({ mount, page }) => {
</TestApp>, </TestApp>,
); );
await expect(component.locator('main')).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
...@@ -6,9 +6,10 @@ import useApiQuery from 'lib/api/useApiQuery'; ...@@ -6,9 +6,10 @@ import useApiQuery from 'lib/api/useApiQuery';
import getCurrencyValue from 'lib/getCurrencyValue'; import getCurrencyValue from 'lib/getCurrencyValue';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import useQueryWithPages from 'lib/hooks/useQueryWithPages'; import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import { generateListStub } from 'stubs/utils';
import { WITHDRAWAL } from 'stubs/withdrawals';
import ActionBar from 'ui/shared/ActionBar'; import ActionBar from 'ui/shared/ActionBar';
import DataListDisplay from 'ui/shared/DataListDisplay'; import DataListDisplay from 'ui/shared/DataListDisplay';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle'; import PageTitle from 'ui/shared/Page/PageTitle';
import Pagination from 'ui/shared/Pagination'; import Pagination from 'ui/shared/Pagination';
import WithdrawalsListItem from 'ui/withdrawals/WithdrawalsListItem'; import WithdrawalsListItem from 'ui/withdrawals/WithdrawalsListItem';
...@@ -17,16 +18,33 @@ import WithdrawalsTable from 'ui/withdrawals/WithdrawalsTable'; ...@@ -17,16 +18,33 @@ import WithdrawalsTable from 'ui/withdrawals/WithdrawalsTable';
const Withdrawals = () => { const Withdrawals = () => {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const { data, isError, isLoading, isPaginationVisible, pagination } = useQueryWithPages({ const { data, isError, isPlaceholderData, isPaginationVisible, pagination } = useQueryWithPages({
resourceName: 'withdrawals', resourceName: 'withdrawals',
options: {
placeholderData: generateListStub<'withdrawals'>(WITHDRAWAL, 50, {
index: 5,
items_count: 50,
}),
},
}); });
const countersQuery = useApiQuery('withdrawals_counters'); const countersQuery = useApiQuery('withdrawals_counters');
const content = data?.items ? ( const content = data?.items ? (
<> <>
<Show below="lg" ssr={ false }>{ data.items.map((item => <WithdrawalsListItem key={ item.index } item={ item } view="list"/>)) }</Show> <Show below="lg" ssr={ false }>
<Hide below="lg" ssr={ false }><WithdrawalsTable items={ data.items } view="list" top={ isPaginationVisible ? 80 : 0 }/></Hide> { data.items.map(((item, index) => (
<WithdrawalsListItem
key={ item.index + (isPlaceholderData ? String(index) : '') }
item={ item }
view="list"
isLoading={ isPlaceholderData }
/>
))) }
</Show>
<Hide below="lg" ssr={ false }>
<WithdrawalsTable items={ data.items } view="list" top={ isPaginationVisible ? 80 : 0 } isLoading={ isPlaceholderData }/>
</Hide>
</> </>
) : null; ) : null;
...@@ -69,18 +87,18 @@ const Withdrawals = () => { ...@@ -69,18 +87,18 @@ const Withdrawals = () => {
); );
return ( return (
<Page> <>
<PageTitle title="Withdrawals" withTextAd/> <PageTitle title="Withdrawals" withTextAd/>
<DataListDisplay <DataListDisplay
isError={ isError } isError={ isError }
isLoading={ isLoading } isLoading={ false }
items={ data?.items } items={ data?.items }
skeletonProps={{ skeletonDesktopColumns: Array(6).fill(`${ 100 / 6 }%`), skeletonDesktopMinW: '950px' }} skeletonProps={{ skeletonDesktopColumns: Array(6).fill(`${ 100 / 6 }%`), skeletonDesktopMinW: '950px' }}
emptyText="There are no withdrawals." emptyText="There are no withdrawals."
content={ content } content={ content }
actionBar={ actionBar } actionBar={ actionBar }
/> />
</Page> </>
); );
}; };
......
...@@ -3,6 +3,7 @@ import { ...@@ -3,6 +3,7 @@ import {
useColorModeValue, useColorModeValue,
chakra, chakra,
Button, Button,
Skeleton,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
...@@ -10,14 +11,19 @@ import infoIcon from 'icons/info.svg'; ...@@ -10,14 +11,19 @@ import infoIcon from 'icons/info.svg';
interface Props { interface Props {
isOpen?: boolean; isOpen?: boolean;
isLoading?: boolean;
className?: string; className?: string;
onClick?: () => void; onClick?: () => void;
} }
const AdditionalInfoButton = ({ isOpen, onClick, className }: Props, ref: React.ForwardedRef<HTMLButtonElement>) => { const AdditionalInfoButton = ({ isOpen, onClick, className, isLoading }: Props, ref: React.ForwardedRef<HTMLButtonElement>) => {
const infoBgColor = useColorModeValue('blue.50', 'gray.600'); const infoBgColor = useColorModeValue('blue.50', 'gray.600');
if (isLoading) {
return <Skeleton boxSize={ 6 } borderRadius="sm" flexShrink={ 0 }/>;
}
return ( return (
<Button <Button
variant="unstyled" variant="unstyled"
......
import { Box, HStack, Icon, Flex, Skeleton, useColorModeValue } from '@chakra-ui/react'; import { Box, HStack, Flex, Skeleton, useColorModeValue } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import keyIcon from 'icons/key.svg'; import keyIcon from 'icons/key.svg';
import Icon from 'ui/shared/chakra/Icon';
import CopyToClipboard from 'ui/shared/CopyToClipboard'; import CopyToClipboard from 'ui/shared/CopyToClipboard';
interface Props { interface Props {
...@@ -13,9 +14,7 @@ interface Props { ...@@ -13,9 +14,7 @@ interface Props {
const ApiKeySnippet = ({ apiKey, name, isLoading }: Props) => { const ApiKeySnippet = ({ apiKey, name, isLoading }: Props) => {
return ( return (
<HStack spacing={ 2 } alignItems="start"> <HStack spacing={ 2 } alignItems="start">
<Skeleton isLoaded={ !isLoading } boxSize={ 6 } display="inline-block"> <Icon as={ keyIcon } boxSize={ 6 } color={ useColorModeValue('gray.500', 'gray.400') } isLoading={ isLoading }/>
<Icon as={ keyIcon } boxSize={ 6 } color={ useColorModeValue('gray.500', 'gray.400') }/>
</Skeleton>
<Box> <Box>
<Flex alignItems={{ base: 'flex-start', lg: 'center' }}> <Flex alignItems={{ base: 'flex-start', lg: 'center' }}>
<Skeleton isLoaded={ !isLoading } display="inline-block" fontWeight={ 600 } mr={ 1 }> <Skeleton isLoaded={ !isLoading } display="inline-block" fontWeight={ 600 } mr={ 1 }>
......
import { Text, Tooltip } from '@chakra-ui/react'; import { Skeleton, Tooltip } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
type Props = { type Props = {
value: number; value: number;
isLoading?: boolean;
} }
const GasUsedToTargetRatio = ({ value }: Props) => { const GasUsedToTargetRatio = ({ value, isLoading }: Props) => {
return ( return (
<Tooltip label="% of Gas Target"> <Tooltip label="% of Gas Target">
<Text variant="secondary"> <Skeleton color="text_secondary" isLoaded={ !isLoading }>
{ (value > 0 ? '+' : '') + value.toLocaleString(undefined, { maximumFractionDigits: 2 }) }% <span>{ (value > 0 ? '+' : '') + value.toLocaleString(undefined, { maximumFractionDigits: 2 }) }%</span>
</Text> </Skeleton>
</Tooltip> </Tooltip>
); );
}; };
......
import { Tag, chakra } from '@chakra-ui/react'; import { chakra } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import Tag from 'ui/shared/chakra/Tag';
interface Props { interface Props {
isIn: boolean; isIn: boolean;
isOut: boolean; isOut: boolean;
className?: string; className?: string;
isLoading?: boolean;
} }
const InOutTag = ({ isIn, isOut, className }: Props) => { const InOutTag = ({ isIn, isOut, className, isLoading }: Props) => {
if (!isIn && !isOut) { if (!isIn && !isOut) {
return null; return null;
} }
...@@ -20,6 +23,7 @@ const InOutTag = ({ isIn, isOut, className }: Props) => { ...@@ -20,6 +23,7 @@ const InOutTag = ({ isIn, isOut, className }: Props) => {
colorScheme={ colorScheme } colorScheme={ colorScheme }
display="flex" display="flex"
justifyContent="center" justifyContent="center"
isLoading={ isLoading }
> >
{ isOut ? 'OUT' : 'IN' } { isOut ? 'OUT' : 'IN' }
</Tag> </Tag>
......
import { Grid, chakra, GridItem } from '@chakra-ui/react'; import { Grid, chakra, GridItem, Skeleton } from '@chakra-ui/react';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import React from 'react'; import React from 'react';
...@@ -38,13 +38,21 @@ const Container = chakra(({ isAnimated, children, className }: ContainerProps) = ...@@ -38,13 +38,21 @@ const Container = chakra(({ isAnimated, children, className }: ContainerProps) =
interface LabelProps { interface LabelProps {
className?: string; className?: string;
children: React.ReactNode; children: React.ReactNode;
isLoading?: boolean;
} }
const Label = chakra(({ children, className }: LabelProps) => { const Label = chakra(({ children, className, isLoading }: LabelProps) => {
return ( return (
<GridItem className={ className } fontWeight={ 500 } lineHeight="20px" py="5px"> <Skeleton
className={ className }
isLoaded={ !isLoading }
fontWeight={ 500 }
lineHeight="20px"
my="5px"
justifySelf="start"
>
{ children } { children }
</GridItem> </Skeleton>
); );
}); });
......
import { Button, Flex, Icon, IconButton, chakra } from '@chakra-ui/react'; import { Button, Skeleton, Flex, Icon, IconButton, chakra } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import arrowIcon from 'icons/arrows/east-mini.svg'; import arrowIcon from 'icons/arrows/east-mini.svg';
...@@ -11,9 +11,10 @@ export type Props = { ...@@ -11,9 +11,10 @@ export type Props = {
hasNextPage: boolean; hasNextPage: boolean;
className?: string; className?: string;
canGoBackwards: boolean; canGoBackwards: boolean;
isLoading?: boolean;
} }
const Pagination = ({ page, onNextPageClick, onPrevPageClick, resetPage, hasNextPage, className, canGoBackwards }: Props) => { const Pagination = ({ page, onNextPageClick, onPrevPageClick, resetPage, hasNextPage, className, canGoBackwards, isLoading }: Props) => {
return ( return (
<Flex <Flex
...@@ -21,46 +22,51 @@ const Pagination = ({ page, onNextPageClick, onPrevPageClick, resetPage, hasNext ...@@ -21,46 +22,51 @@ const Pagination = ({ page, onNextPageClick, onPrevPageClick, resetPage, hasNext
fontSize="sm" fontSize="sm"
alignItems="center" alignItems="center"
> >
<Button <Skeleton isLoaded={ !isLoading } display="inline-block" mr={ 4 } borderRadius="base">
variant="outline" <Button
size="sm" variant="outline"
onClick={ resetPage } size="sm"
isDisabled={ page === 1 } onClick={ resetPage }
mr={ 4 } isDisabled={ page === 1 }
> >
First First
</Button> </Button>
<IconButton </Skeleton>
variant="outline" <Skeleton isLoaded={ !isLoading } display="inline-block" mr={ 3 } borderRadius="base">
onClick={ onPrevPageClick } <IconButton
size="sm" variant="outline"
aria-label="Next page" onClick={ onPrevPageClick }
w="36px" size="sm"
icon={ <Icon as={ arrowIcon } w={ 5 } h={ 5 }/> } aria-label="Next page"
mr={ 3 } w="36px"
isDisabled={ !canGoBackwards || page === 1 } icon={ <Icon as={ arrowIcon } w={ 5 } h={ 5 }/> }
/> isDisabled={ !canGoBackwards || page === 1 }
<Button />
variant="outline" </Skeleton>
size="sm" <Skeleton isLoaded={ !isLoading } display="inline-block" borderRadius="base">
isActive <Button
borderWidth="1px" variant="outline"
fontWeight={ 400 } size="sm"
h={ 8 } isActive
cursor="unset" borderWidth="1px"
> fontWeight={ 400 }
{ page } h={ 8 }
</Button> cursor="unset"
<IconButton >
variant="outline" { page }
onClick={ onNextPageClick } </Button>
size="sm" </Skeleton>
aria-label="Next page" <Skeleton isLoaded={ !isLoading } display="inline-block" ml={ 3 } borderRadius="base">
w="36px" <IconButton
icon={ <Icon as={ arrowIcon } w={ 5 } h={ 5 } transform="rotate(180deg)"/> } variant="outline"
ml={ 3 } onClick={ onNextPageClick }
isDisabled={ !hasNextPage } size="sm"
/> aria-label="Next page"
w="36px"
icon={ <Icon as={ arrowIcon } w={ 5 } h={ 5 } transform="rotate(180deg)"/> }
isDisabled={ !hasNextPage }
/>
</Skeleton>
{ /* not implemented yet */ } { /* not implemented yet */ }
{ /* <Flex alignItems="center" width="132px" ml={ 16 } display={{ base: 'none', lg: 'flex' }}> { /* <Flex alignItems="center" width="132px" ml={ 16 } display={{ base: 'none', lg: 'flex' }}>
Go to <Input w="84px" size="xs" ml={ 2 }/> Go to <Input w="84px" size="xs" ml={ 2 }/>
......
import { Box, Icon, IconButton, chakra, Tooltip } from '@chakra-ui/react'; import { Box, Icon, IconButton, chakra, Tooltip, Flex, Skeleton } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import eastArrow from 'icons/arrows/east-mini.svg'; import eastArrow from 'icons/arrows/east-mini.svg';
...@@ -10,9 +10,10 @@ interface Props { ...@@ -10,9 +10,10 @@ interface Props {
nextLabel?: string; nextLabel?: string;
isPrevDisabled?: boolean; isPrevDisabled?: boolean;
isNextDisabled?: boolean; isNextDisabled?: boolean;
isLoading?: boolean;
} }
const PrevNext = ({ className, onClick, prevLabel, nextLabel, isPrevDisabled, isNextDisabled }: Props) => { const PrevNext = ({ className, onClick, prevLabel, nextLabel, isPrevDisabled, isNextDisabled, isLoading }: Props) => {
const handelPrevClick = React.useCallback(() => { const handelPrevClick = React.useCallback(() => {
onClick('prev'); onClick('prev');
}, [ onClick ]); }, [ onClick ]);
...@@ -21,6 +22,15 @@ const PrevNext = ({ className, onClick, prevLabel, nextLabel, isPrevDisabled, is ...@@ -21,6 +22,15 @@ const PrevNext = ({ className, onClick, prevLabel, nextLabel, isPrevDisabled, is
onClick('next'); onClick('next');
}, [ onClick ]); }, [ onClick ]);
if (isLoading) {
return (
<Flex columnGap="10px" className={ className }>
<Skeleton boxSize={ 6 } borderRadius="sm"/>
<Skeleton boxSize={ 6 } borderRadius="sm"/>
</Flex>
);
}
return ( return (
<Box className={ className }> <Box className={ className }>
<Tooltip label={ prevLabel }> <Tooltip label={ prevLabel }>
......
...@@ -61,6 +61,7 @@ const SocketNewItemsNotice = chakra(({ children, className, url, num, alert, typ ...@@ -61,6 +61,7 @@ const SocketNewItemsNotice = chakra(({ children, className, url, num, alert, typ
py="6px" py="6px"
fontWeight={ 400 } fontWeight={ 400 }
fontSize="sm" fontSize="sm"
lineHeight={ 5 }
bgColor={ bgColor } bgColor={ bgColor }
color={ color } color={ color }
> >
...@@ -77,7 +78,7 @@ export const Desktop = ({ ...props }: Props) => { ...@@ -77,7 +78,7 @@ export const Desktop = ({ ...props }: Props) => {
return ( return (
<SocketNewItemsNotice <SocketNewItemsNotice
borderRadius={ props.isLoading ? 'sm' : 0 } borderRadius={ props.isLoading ? 'sm' : 0 }
h={ props.isLoading ? 4 : 'auto' } h={ props.isLoading ? 5 : 'auto' }
maxW={ props.isLoading ? '215px' : undefined } maxW={ props.isLoading ? '215px' : undefined }
w="100%" w="100%"
mx={ props.isLoading ? 4 : 0 } mx={ props.isLoading ? 4 : 0 }
......
...@@ -15,7 +15,7 @@ export interface Props { ...@@ -15,7 +15,7 @@ export interface Props {
const TokenLogo = ({ className, isLoading, data }: Props) => { const TokenLogo = ({ className, isLoading, data }: Props) => {
if (isLoading) { if (isLoading) {
return <Skeleton className={ className } borderRadius="base"/>; return <Skeleton className={ className } borderRadius="base" flexShrink={ 0 }/>;
} }
const logoSrc = (() => { const logoSrc = (() => {
......
...@@ -12,14 +12,15 @@ interface Props { ...@@ -12,14 +12,15 @@ interface Props {
className?: string; className?: string;
logoSize?: number; logoSize?: number;
isDisabled?: boolean; isDisabled?: boolean;
isLoading?: boolean;
hideSymbol?: boolean; hideSymbol?: boolean;
} }
const TokenSnippet = ({ data, className, logoSize = 6, isDisabled, hideSymbol }: Props) => { const TokenSnippet = ({ data, className, logoSize = 6, isDisabled, hideSymbol, isLoading }: Props) => {
return ( return (
<Flex className={ className } alignItems="center" columnGap={ 2 } w="100%"> <Flex className={ className } alignItems="center" columnGap={ 2 } w="100%">
<TokenLogo boxSize={ logoSize } data={ data }/> <TokenLogo boxSize={ logoSize } data={ data } isLoading={ isLoading }/>
<AddressLink hash={ data?.address || '' } alias={ data?.name || 'Unnamed token' } type="token" isDisabled={ isDisabled }/> <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 && <Text variant="secondary">({ trimTokenSymbol(data.symbol) })</Text> }
</Flex> </Flex>
); );
......
...@@ -19,6 +19,7 @@ interface Props { ...@@ -19,6 +19,7 @@ interface Props {
withAddressFilter?: boolean; withAddressFilter?: boolean;
onAddressFilterChange?: (nextValue: string) => void; onAddressFilterChange?: (nextValue: string) => void;
defaultAddressFilter?: AddressFromToFilter; defaultAddressFilter?: AddressFromToFilter;
isLoading?: boolean;
} }
const TokenTransferFilter = ({ const TokenTransferFilter = ({
...@@ -28,10 +29,11 @@ const TokenTransferFilter = ({ ...@@ -28,10 +29,11 @@ const TokenTransferFilter = ({
withAddressFilter, withAddressFilter,
onAddressFilterChange, onAddressFilterChange,
defaultAddressFilter, defaultAddressFilter,
isLoading,
}: Props) => { }: Props) => {
return ( return (
<PopoverFilter appliedFiltersNum={ appliedFiltersNum } contentProps={{ w: '200px' }}> <PopoverFilter appliedFiltersNum={ appliedFiltersNum } contentProps={{ w: '200px' }} isLoading={ isLoading }>
{ withAddressFilter && ( { withAddressFilter && (
<> <>
<Text variant="secondary" fontWeight={ 600 }>Address</Text> <Text variant="secondary" fontWeight={ 600 }>Address</Text>
......
...@@ -10,18 +10,20 @@ interface Props { ...@@ -10,18 +10,20 @@ interface Props {
baseAddress?: string; baseAddress?: string;
showTxInfo?: boolean; showTxInfo?: boolean;
enableTimeIncrement?: boolean; enableTimeIncrement?: boolean;
isLoading?: boolean;
} }
const TokenTransferList = ({ data, baseAddress, showTxInfo, enableTimeIncrement }: Props) => { const TokenTransferList = ({ data, baseAddress, showTxInfo, enableTimeIncrement, isLoading }: Props) => {
return ( return (
<Box> <Box>
{ data.map((item) => ( { data.map((item, index) => (
<TokenTransferListItem <TokenTransferListItem
key={ item.tx_hash + item.block_hash + item.log_index } key={ item.tx_hash + item.block_hash + item.log_index + (isLoading ? index : '') }
{ ...item } { ...item }
baseAddress={ baseAddress } baseAddress={ baseAddress }
showTxInfo={ showTxInfo } showTxInfo={ showTxInfo }
enableTimeIncrement={ enableTimeIncrement } enableTimeIncrement={ enableTimeIncrement }
isLoading={ isLoading }
/> />
)) } )) }
</Box> </Box>
......
import { Text, Flex, Tag, Icon } from '@chakra-ui/react'; import { Flex, Skeleton } from '@chakra-ui/react';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import React from 'react'; import React from 'react';
...@@ -10,6 +10,9 @@ import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement'; ...@@ -10,6 +10,9 @@ import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
import Address from 'ui/shared/address/Address'; import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon'; import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink'; import AddressLink from 'ui/shared/address/AddressLink';
import Icon from 'ui/shared/chakra/Icon';
import Tag from 'ui/shared/chakra/Tag';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import InOutTag from 'ui/shared/InOutTag'; import InOutTag from 'ui/shared/InOutTag';
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile'; import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
import TokenSnippet from 'ui/shared/TokenSnippet/TokenSnippet'; import TokenSnippet from 'ui/shared/TokenSnippet/TokenSnippet';
...@@ -17,12 +20,11 @@ import { getTokenTransferTypeText } from 'ui/shared/TokenTransfer/helpers'; ...@@ -17,12 +20,11 @@ import { getTokenTransferTypeText } from 'ui/shared/TokenTransfer/helpers';
import TokenTransferNft from 'ui/shared/TokenTransfer/TokenTransferNft'; import TokenTransferNft from 'ui/shared/TokenTransfer/TokenTransferNft';
import TxAdditionalInfo from 'ui/txs/TxAdditionalInfo'; import TxAdditionalInfo from 'ui/txs/TxAdditionalInfo';
import CopyToClipboard from '../CopyToClipboard';
type Props = TokenTransfer & { type Props = TokenTransfer & {
baseAddress?: string; baseAddress?: string;
showTxInfo?: boolean; showTxInfo?: boolean;
enableTimeIncrement?: boolean; enableTimeIncrement?: boolean;
isLoading?: boolean;
} }
const TokenTransferListItem = ({ const TokenTransferListItem = ({
...@@ -36,6 +38,7 @@ const TokenTransferListItem = ({ ...@@ -36,6 +38,7 @@ const TokenTransferListItem = ({
type, type,
timestamp, timestamp,
enableTimeIncrement, enableTimeIncrement,
isLoading,
}: Props) => { }: Props) => {
const value = (() => { const value = (() => {
if (!('value' in total)) { if (!('value' in total)) {
...@@ -51,57 +54,62 @@ const TokenTransferListItem = ({ ...@@ -51,57 +54,62 @@ const TokenTransferListItem = ({
return ( return (
<ListItemMobile rowGap={ 3 } isAnimated> <ListItemMobile rowGap={ 3 } isAnimated>
<Flex w="100%" justifyContent="space-between"> <Flex w="100%" justifyContent="space-between">
<Flex flexWrap="wrap" rowGap={ 1 } mr={ showTxInfo && txHash ? 2 : 0 }> <Flex flexWrap="wrap" rowGap={ 1 } mr={ showTxInfo && txHash ? 2 : 0 } columnGap={ 2 }>
<TokenSnippet data={ token } w="auto" maxW="calc(100% - 140px)" hideSymbol/> <TokenSnippet data={ token } w="auto" maxW="calc(100% - 140px)" hideSymbol isLoading={ isLoading }/>
<Tag flexShrink={ 0 } ml={ 2 } mr={ 2 }>{ token.type }</Tag> <Tag flexShrink={ 0 } isLoading={ isLoading }>{ token.type }</Tag>
<Tag colorScheme="orange">{ getTokenTransferTypeText(type) }</Tag> <Tag colorScheme="orange" isLoading={ isLoading }>{ getTokenTransferTypeText(type) }</Tag>
</Flex> </Flex>
{ showTxInfo && txHash && ( { showTxInfo && txHash && (
<TxAdditionalInfo hash={ txHash } isMobile/> <TxAdditionalInfo hash={ txHash } isMobile isLoading={ isLoading }/>
) } ) }
</Flex> </Flex>
{ 'token_id' in total && <TokenTransferNft hash={ token.address } id={ total.token_id }/> } { 'token_id' in total && <TokenTransferNft hash={ token.address } id={ total.token_id } isLoading={ isLoading }/> }
{ showTxInfo && txHash && ( { showTxInfo && txHash && (
<Flex justifyContent="space-between" alignItems="center" lineHeight="24px" width="100%"> <Flex justifyContent="space-between" alignItems="center" lineHeight="24px" width="100%">
<Flex> <Flex>
<Icon <Icon
as={ transactionIcon } as={ transactionIcon }
boxSize="30px" boxSize="30px"
mr={ 2 }
color="link" color="link"
isLoading={ isLoading }
/> />
<Address width="100%"> <Address width="100%" ml={ 2 }>
<AddressLink <AddressLink
hash={ txHash } hash={ txHash }
type="transaction" type="transaction"
fontWeight="700" fontWeight="700"
truncation="constant" truncation="constant"
isLoading={ isLoading }
/> />
</Address> </Address>
</Flex> </Flex>
{ timestamp && <Text variant="secondary" fontWeight="400" fontSize="sm">{ timeAgo }</Text> } { timestamp && (
<Skeleton isLoaded={ !isLoading } color="text_secondary" fontWeight="400" fontSize="sm">
<span>{ timeAgo }</span>
</Skeleton>
) }
</Flex> </Flex>
) } ) }
<Flex w="100%" columnGap={ 3 }> <Flex w="100%" columnGap={ 3 }>
<Address width={ addressWidth }> <Address width={ addressWidth }>
<AddressIcon address={ from }/> <AddressIcon address={ from } isLoading={ isLoading }/>
<AddressLink type="address" ml={ 2 } fontWeight="500" hash={ from.hash } isDisabled={ baseAddress === from.hash }/> <AddressLink type="address" ml={ 2 } fontWeight="500" hash={ from.hash } isDisabled={ baseAddress === from.hash } isLoading={ isLoading }/>
{ baseAddress !== from.hash && <CopyToClipboard text={ from.hash }/> } { baseAddress !== from.hash && <CopyToClipboard text={ from.hash } isLoading={ isLoading }/> }
</Address> </Address>
{ baseAddress ? { baseAddress ?
<InOutTag isIn={ baseAddress === to.hash } isOut={ baseAddress === from.hash } w="50px" textAlign="center"/> : <InOutTag isIn={ baseAddress === to.hash } isOut={ baseAddress === from.hash } w="50px" textAlign="center" isLoading={ isLoading }/> :
<Icon as={ eastArrowIcon } boxSize={ 6 } color="gray.500"/> <Icon as={ eastArrowIcon } boxSize={ 6 } color="gray.500" isLoading={ isLoading }/>
} }
<Address width={ addressWidth }> <Address width={ addressWidth }>
<AddressIcon address={ to }/> <AddressIcon address={ to } isLoading={ isLoading }/>
<AddressLink type="address" ml={ 2 } fontWeight="500" hash={ to.hash } isDisabled={ baseAddress === to.hash }/> <AddressLink type="address" ml={ 2 } fontWeight="500" hash={ to.hash } isDisabled={ baseAddress === to.hash } isLoading={ isLoading }/>
{ baseAddress !== to.hash && <CopyToClipboard text={ to.hash }/> } { baseAddress !== to.hash && <CopyToClipboard text={ to.hash } isLoading={ isLoading }/> }
</Address> </Address>
</Flex> </Flex>
{ value && ( { value && (
<Flex columnGap={ 2 } w="100%"> <Flex columnGap={ 2 } w="100%">
<Text fontWeight={ 500 } flexShrink={ 0 }>Value</Text> <Skeleton isLoaded={ !isLoading } fontWeight={ 500 } flexShrink={ 0 }>Value</Skeleton>
<Text variant="secondary">{ value }</Text> <Skeleton isLoaded={ !isLoading } color="text_secondary"><span>{ value }</span></Skeleton>
</Flex> </Flex>
) } ) }
</ListItemMobile> </ListItemMobile>
......
import { Box, Icon, chakra, Skeleton } from '@chakra-ui/react'; import { Box, chakra, Skeleton } from '@chakra-ui/react';
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
import nftPlaceholder from 'icons/nft_shield.svg'; import nftPlaceholder from 'icons/nft_shield.svg';
import Icon from 'ui/shared/chakra/Icon';
import HashStringShorten from 'ui/shared/HashStringShorten'; import HashStringShorten from 'ui/shared/HashStringShorten';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic'; import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import LinkInternal from 'ui/shared/LinkInternal'; import LinkInternal from 'ui/shared/LinkInternal';
...@@ -29,10 +30,8 @@ const TokenTransferNft = ({ hash, id, className, isDisabled, isLoading, truncati ...@@ -29,10 +30,8 @@ const TokenTransferNft = ({ hash, id, className, isDisabled, isLoading, truncati
w="100%" w="100%"
className={ className } className={ className }
> >
<Skeleton isLoaded={ !isLoading } boxSize="30px" mr={ 1 } borderRadius="base"> <Icon as={ nftPlaceholder } boxSize="30px" color="inherit" isLoading={ isLoading } borderRadius="base"/>
<Icon as={ nftPlaceholder } boxSize="30px" color="inherit"/> <Skeleton isLoaded={ !isLoading } maxW="calc(100% - 34px)" ml={ 1 }>
</Skeleton>
<Skeleton isLoaded={ !isLoading } maxW="calc(100% - 34px)">
{ truncation === 'constant' ? <HashStringShorten hash={ id }/> : <HashStringShortenDynamic hash={ id } fontWeight={ 500 }/> } { truncation === 'constant' ? <HashStringShorten hash={ id }/> : <HashStringShortenDynamic hash={ id } fontWeight={ 500 }/> }
</Skeleton> </Skeleton>
</Component> </Component>
......
import { Table, Tbody, Tr, Th, Td } from '@chakra-ui/react'; import { Table, Tbody, Tr, Th } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { TokenTransfer } from 'types/api/tokenTransfer'; import type { TokenTransfer } from 'types/api/tokenTransfer';
import SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice'; import * as SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice';
import { default as Thead } from 'ui/shared/TheadSticky'; import { default as Thead } from 'ui/shared/TheadSticky';
import TokenTransferTableItem from 'ui/shared/TokenTransfer/TokenTransferTableItem'; import TokenTransferTableItem from 'ui/shared/TokenTransfer/TokenTransferTableItem';
...@@ -16,6 +16,7 @@ interface Props { ...@@ -16,6 +16,7 @@ interface Props {
showSocketInfo?: boolean; showSocketInfo?: boolean;
socketInfoAlert?: string; socketInfoAlert?: string;
socketInfoNum?: number; socketInfoNum?: number;
isLoading?: boolean;
} }
const TokenTransferTable = ({ const TokenTransferTable = ({
...@@ -27,6 +28,7 @@ const TokenTransferTable = ({ ...@@ -27,6 +28,7 @@ const TokenTransferTable = ({
showSocketInfo, showSocketInfo,
socketInfoAlert, socketInfoAlert,
socketInfoNum, socketInfoNum,
isLoading,
}: Props) => { }: Props) => {
return ( return (
...@@ -45,26 +47,22 @@ const TokenTransferTable = ({ ...@@ -45,26 +47,22 @@ const TokenTransferTable = ({
</Thead> </Thead>
<Tbody> <Tbody>
{ showSocketInfo && ( { showSocketInfo && (
<Tr> <SocketNewItemsNotice.Desktop
<Td colSpan={ 10 } p={ 0 }> url={ window.location.href }
<SocketNewItemsNotice alert={ socketInfoAlert }
borderRadius={ 0 } num={ socketInfoNum }
pl="10px" type="token_transfer"
url={ window.location.href } isLoading={ isLoading }
alert={ socketInfoAlert } />
num={ socketInfoNum }
type="token_transfer"
/>
</Td>
</Tr>
) } ) }
{ data.map((item) => ( { data.map((item, index) => (
<TokenTransferTableItem <TokenTransferTableItem
key={ item.tx_hash + item.block_hash + item.log_index } key={ item.tx_hash + item.block_hash + item.log_index + (isLoading ? index : '') }
{ ...item } { ...item }
baseAddress={ baseAddress } baseAddress={ baseAddress }
showTxInfo={ showTxInfo } showTxInfo={ showTxInfo }
enableTimeIncrement={ enableTimeIncrement } enableTimeIncrement={ enableTimeIncrement }
isLoading={ isLoading }
/> />
)) } )) }
</Tbody> </Tbody>
......
import { Tr, Td, Tag, Flex, Text } from '@chakra-ui/react'; import { Tr, Td, Flex, Skeleton, Box } from '@chakra-ui/react';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import React from 'react'; import React from 'react';
...@@ -8,18 +8,19 @@ import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement'; ...@@ -8,18 +8,19 @@ import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
import Address from 'ui/shared/address/Address'; import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon'; import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink'; import AddressLink from 'ui/shared/address/AddressLink';
import Tag from 'ui/shared/chakra/Tag';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import InOutTag from 'ui/shared/InOutTag'; import InOutTag from 'ui/shared/InOutTag';
import TokenSnippet from 'ui/shared/TokenSnippet/TokenSnippet'; import TokenSnippet from 'ui/shared/TokenSnippet/TokenSnippet';
import { getTokenTransferTypeText } from 'ui/shared/TokenTransfer/helpers'; import { getTokenTransferTypeText } from 'ui/shared/TokenTransfer/helpers';
import TokenTransferNft from 'ui/shared/TokenTransfer/TokenTransferNft'; import TokenTransferNft from 'ui/shared/TokenTransfer/TokenTransferNft';
import TxAdditionalInfo from 'ui/txs/TxAdditionalInfo'; import TxAdditionalInfo from 'ui/txs/TxAdditionalInfo';
import CopyToClipboard from '../CopyToClipboard';
type Props = TokenTransfer & { type Props = TokenTransfer & {
baseAddress?: string; baseAddress?: string;
showTxInfo?: boolean; showTxInfo?: boolean;
enableTimeIncrement?: boolean; enableTimeIncrement?: boolean;
isLoading?: boolean;
} }
const TokenTransferTableItem = ({ const TokenTransferTableItem = ({
...@@ -33,6 +34,7 @@ const TokenTransferTableItem = ({ ...@@ -33,6 +34,7 @@ const TokenTransferTableItem = ({
type, type,
timestamp, timestamp,
enableTimeIncrement, enableTimeIncrement,
isLoading,
}: Props) => { }: Props) => {
const timeAgo = useTimeAgoIncrement(timestamp, enableTimeIncrement); const timeAgo = useTimeAgoIncrement(timestamp, enableTimeIncrement);
...@@ -40,48 +42,81 @@ const TokenTransferTableItem = ({ ...@@ -40,48 +42,81 @@ const TokenTransferTableItem = ({
<Tr alignItems="top"> <Tr alignItems="top">
{ showTxInfo && txHash && ( { showTxInfo && txHash && (
<Td> <Td>
<TxAdditionalInfo hash={ txHash }/> <Box my="3px">
<TxAdditionalInfo hash={ txHash } isLoading={ isLoading }/>
</Box>
</Td> </Td>
) } ) }
<Td> <Td>
<Flex flexDir="column" alignItems="flex-start"> <Flex flexDir="column" alignItems="flex-start" my="3px" rowGap={ 2 }>
<TokenSnippet data={ token } lineHeight="30px" hideSymbol/> <TokenSnippet data={ token } isLoading={ isLoading } hideSymbol/>
<Tag mt={ 1 }>{ token.type }</Tag> <Tag isLoading={ isLoading }>{ token.type }</Tag>
<Tag colorScheme="orange" mt={ 2 }>{ getTokenTransferTypeText(type) }</Tag> <Tag colorScheme="orange" isLoading={ isLoading }>{ getTokenTransferTypeText(type) }</Tag>
</Flex> </Flex>
</Td> </Td>
<Td lineHeight="30px"> <Td>
{ 'token_id' in total && <TokenTransferNft hash={ token.address } id={ total.token_id }/> } { 'token_id' in total && <TokenTransferNft hash={ token.address } id={ total.token_id } isLoading={ isLoading }/> }
</Td> </Td>
{ showTxInfo && txHash && ( { showTxInfo && txHash && (
<Td> <Td>
<Address display="inline-flex" maxW="100%" fontWeight={ 600 } lineHeight="30px"> <Address display="inline-flex" maxW="100%" fontWeight={ 600 } mt="7px">
<AddressLink type="transaction" hash={ txHash }/> <AddressLink type="transaction" hash={ txHash } isLoading={ isLoading }/>
</Address> </Address>
{ timestamp && <Text color="gray.500" fontWeight="400" mt="10px">{ timeAgo }</Text> } { timestamp && (
<Skeleton isLoaded={ !isLoading } color="text_secondary" fontWeight="400" mt="10px" display="inline-block">
<span>{ timeAgo }</span>
</Skeleton>
) }
</Td> </Td>
) } ) }
<Td> <Td>
<Address display="inline-flex" maxW="100%" lineHeight="30px"> <Address display="inline-flex" maxW="100%" my="3px">
<AddressIcon address={ from }/> <AddressIcon address={ from } isLoading={ isLoading }/>
<AddressLink type="address" ml={ 2 } fontWeight="500" hash={ from.hash } alias={ from.name } flexGrow={ 1 } isDisabled={ baseAddress === from.hash }/> <AddressLink
{ baseAddress !== from.hash && <CopyToClipboard text={ from.hash }/> } type="address" ml={ 2 }
fontWeight="500"
hash={ from.hash }
alias={ from.name }
flexGrow={ 1 }
isDisabled={ baseAddress === from.hash }
isLoading={ isLoading }
/>
{ baseAddress !== from.hash && <CopyToClipboard text={ from.hash } isLoading={ isLoading }/> }
</Address> </Address>
</Td> </Td>
{ baseAddress && ( { baseAddress && (
<Td px={ 0 }> <Td px={ 0 }>
<InOutTag isIn={ baseAddress === to.hash } isOut={ baseAddress === from.hash } w="50px" textAlign="center" mt="3px"/> <Box mt="3px">
<InOutTag
isIn={ baseAddress === to.hash }
isOut={ baseAddress === from.hash }
w="50px"
textAlign="center"
isLoading={ isLoading }
/>
</Box>
</Td> </Td>
) } ) }
<Td> <Td>
<Address display="inline-flex" maxW="100%" lineHeight="30px"> <Address display="inline-flex" maxW="100%" my="3px">
<AddressIcon address={ to }/> <AddressIcon address={ to } isLoading={ isLoading }/>
<AddressLink type="address" ml={ 2 } fontWeight="500" hash={ to.hash } alias={ to.name } flexGrow={ 1 } isDisabled={ baseAddress === to.hash }/> <AddressLink
{ baseAddress !== to.hash && <CopyToClipboard text={ to.hash }/> } type="address"
ml={ 2 }
fontWeight="500"
hash={ to.hash }
alias={ to.name }
flexGrow={ 1 }
isDisabled={ baseAddress === to.hash }
isLoading={ isLoading }
/>
{ baseAddress !== to.hash && <CopyToClipboard text={ to.hash } isLoading={ isLoading }/> }
</Address> </Address>
</Td> </Td>
<Td isNumeric verticalAlign="top" lineHeight="30px"> <Td isNumeric verticalAlign="top">
{ 'value' in total && BigNumber(total.value).div(BigNumber(10 ** Number(total.decimals))).dp(8).toFormat() } <Skeleton isLoaded={ !isLoading } display="inline-block" my="7px">
{ 'value' in total && BigNumber(total.value).div(BigNumber(10 ** Number(total.decimals))).dp(8).toFormat() }
</Skeleton>
</Td> </Td>
</Tr> </Tr>
); );
......
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.
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.
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