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 @@
},
{
"type": "shell",
"command": "NEXT_PUBLIC_API_HOST=${input:goerliApiHost} yarn dev:goerli",
"command": "NEXT_PUBLIC_API_HOST=${input:apiHost} yarn dev:goerli",
"problemMatcher": [],
"label": "dev server: goerli",
"detail": "start local dev server for Goerli network",
......@@ -68,7 +68,7 @@
},
{
"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": [],
"label": "dev server: goerli optimism",
"detail": "start local dev server for Goerli Optimism network",
......@@ -371,11 +371,12 @@
},
{
"type": "pickString",
"id": "goerliApiHost",
"id": "apiHost",
"description": "Choose API host:",
"options": [
"blockscout-main.test.aws-k8s.blockscout.com",
"eth-goerli.blockscout.com",
"eth.blockscout.com",
],
"default": ""
},
......
......@@ -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_HOMEPAGE_CHARTS=['daily_txs']
NEXT_PUBLIC_IS_TESTNET=true
NEXT_PUBLIC_HAS_BEACON_CHAIN=false
# api config
NEXT_PUBLIC_API_HOST=blockscout-main.test.aws-k8s.blockscout.com
......
......@@ -158,6 +158,7 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({
resetPage,
hasNextPage: nextPageParams ? Object.keys(nextPageParams).length > 0 : false,
canGoBackwards: canGoBackwards.current,
isLoading: queryResult.isPlaceholderData && !hasPagination,
};
const isPaginationVisible = hasPagination || (!queryResult.isLoading && !queryResult.isError && pagination.hasNextPage);
......
......@@ -47,6 +47,7 @@ export const base: Block = {
tx_fees: '26853607500000000',
type: 'block',
uncles_hashes: [],
has_beacon_chain_withdrawals: false,
};
export const genesis: Block = {
......@@ -83,6 +84,7 @@ export const genesis: Block = {
tx_fees: '0',
type: 'block',
uncles_hashes: [],
has_beacon_chain_withdrawals: false,
};
export const base2: Block = {
......
......@@ -65,6 +65,7 @@ export const base: Transaction = {
type: 2,
value: '42000000000000000000',
actions: [],
has_error_in_internal_txs: false,
};
export const withContractCreation: Transaction = {
......
import type { NextPage } from 'next';
import dynamic from 'next/dynamic';
import Head from 'next/head';
import type { RoutedQuery } from 'nextjs-routes';
import React from 'react';
import getSeo from 'lib/next/block/getSeo';
import Block from 'ui/pages/Block';
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 { title, description } = getSeo({ height });
return (
......
import type { NextPage } from 'next';
import dynamic from 'next/dynamic';
import Head from 'next/head';
import React from 'react';
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 { title } = getSeo();
......@@ -12,7 +15,9 @@ const BlockPage: NextPage = () => {
<Head>
<title>{ title }</title>
</Head>
<Blocks/>
<Page>
<Blocks/>
</Page>
</>
);
};
......
import type { NextPage } from 'next';
import dynamic from 'next/dynamic';
import Head from 'next/head';
import type { RoutedQuery } from 'nextjs-routes';
import React from 'react';
import getSeo from 'lib/next/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 { title, description } = getSeo({ hash });
......@@ -15,7 +18,9 @@ const TransactionPage: NextPage<RoutedQuery<'/tx/[hash]'>> = ({ hash }: RoutedQu
<title>{ title }</title>
<meta name="description" content={ description }/>
</Head>
<Transaction/>
<Page>
<Transaction/>
</Page>
</>
);
};
......
import type { NextPage } from 'next';
import dynamic from 'next/dynamic';
import Head from 'next/head';
import React from 'react';
import getNetworkTitle from 'lib/networks/getNetworkTitle';
import 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 title = getNetworkTitle();
return (
<>
<Head><title>{ title }</title></Head>
<Transactions/>
<Page>
<Transactions/>
</Page>
</>
);
};
......
import type { NextPage } from 'next';
import dynamic from 'next/dynamic';
import Head from 'next/head';
import React from 'react';
import getNetworkTitle from 'lib/networks/getNetworkTitle';
import 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 title = getNetworkTitle();
......@@ -12,7 +15,9 @@ const WithdrawalsPage: NextPage = () => {
<Head>
<title>{ title }</title>
</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: 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 { TokenTransfer, TokenTransferResponse } from 'types/api/tokenTransfer';
import type { TokenCounters, TokenHolder, TokenHolders, TokenInfo, TokenInstance, TokenType } from 'types/api/token';
import type { TokenTransfer, TokenTransferPagination, TokenTransferResponse } from 'types/api/tokenTransfer';
import { ADDRESS_PARAMS, ADDRESS_HASH } from './addressParams';
import { BLOCK_HASH } from './block';
import { TX_HASH } from './tx';
import { generateListStub } from './utils';
export const TOKEN_INFO_ERC_20: TokenInfo<'ERC-20'> = {
address: ADDRESS_HASH,
......@@ -73,14 +74,14 @@ export const TOKEN_TRANSFER_ERC_1155: TokenTransfer = {
token: TOKEN_INFO_ERC_1155,
};
export const getTokenTransfersStub = (type?: TokenType): TokenTransferResponse => {
export const getTokenTransfersStub = (type?: TokenType, pagination: TokenTransferPagination | null = null): TokenTransferResponse => {
switch (type) {
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':
return { items: Array(50).fill(TOKEN_TRANSFER_ERC_1155), next_page_params: null };
return generateListStub<'token_transfers'>(TOKEN_TRANSFER_ERC_1155, 50, pagination);
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 = {
token: TOKEN_INFO_ERC_1155,
holder_address_hash: ADDRESS_HASH,
};
export const TOKEN_INSTANCES: TokenInventoryResponse = {
items: Array(50).fill(TOKEN_INSTANCE),
next_page_params: null,
};
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: 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 {
tx_count: number;
miner: AddressParam;
size: number;
has_beacon_chain_withdrawals?: boolean;
has_beacon_chain_withdrawals: boolean | null;
hash: string;
parent_hash: string;
difficulty: string;
......
......@@ -16,7 +16,7 @@ export interface LogsResponseTx {
index: number;
items_count: number;
transaction_hash: string;
};
} | null;
}
export interface LogsResponseAddress {
......@@ -26,5 +26,5 @@ export interface LogsResponseAddress {
items_count: number;
transaction_index: number;
block_number: number;
};
} | null;
}
......@@ -8,7 +8,7 @@ export type HomeStats = {
transactions_today: string;
gas_used_today: string;
gas_prices: GasPrices | null;
static_gas_price: string;
static_gas_price: string | null;
market_cap: string;
network_utilization_percentage: number;
}
......
......@@ -47,6 +47,7 @@ export type Transaction = {
l1_fee_scalar?: string;
l1_gas_price?: string;
l1_gas_used?: string;
has_error_in_internal_txs: boolean | null;
}
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 React from 'react';
......@@ -13,15 +13,27 @@ interface Props {
address: string;
type: CsvExportType;
className?: string;
isLoading?: boolean;
}
const AddressCsvExportLink = ({ className, address, type }: Props) => {
const AddressCsvExportLink = ({ className, address, type, isLoading }: Props) => {
const isMobile = useIsMobile();
if (!appConfig.reCaptcha.siteKey) {
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 (
<Tooltip isDisabled={ !isMobile } label="Download CSV">
<LinkInternal
......
......@@ -3,20 +3,29 @@ import React from 'react';
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import getQueryParamString from 'lib/router/getQueryParamString';
import { LOG } from 'stubs/log';
import { generateListStub } from 'stubs/utils';
import ActionBar from 'ui/shared/ActionBar';
import DataListDisplay from 'ui/shared/DataListDisplay';
import LogItem from 'ui/shared/logs/LogItem';
import LogSkeleton from 'ui/shared/logs/LogSkeleton';
import Pagination from 'ui/shared/Pagination';
const AddressLogs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>}) => {
const router = useRouter();
const hash = getQueryParamString(router.query.hash);
const { data, isLoading, isError, pagination, isPaginationVisible } = useQueryWithPages({
const { data, isPlaceholderData, isError, pagination, isPaginationVisible } = useQueryWithPages({
resourceName: 'address_logs',
pathParams: { hash },
scrollRef,
options: {
placeholderData: generateListStub<'address_logs'>(LOG, 3, {
block_number: 9005750,
index: 42,
items_count: 50,
transaction_index: 23,
}),
},
});
const actionBar = isPaginationVisible ? (
......@@ -25,19 +34,17 @@ const AddressLogs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>
</ActionBar>
) : null;
const content = data?.items ? data.items.map((item, index) => <LogItem key={ index } { ...item } type="address"/>) : null;
const skeleton = <><LogSkeleton/><LogSkeleton/></>;
const content = data?.items ? data.items.map((item, index) => <LogItem key={ index } { ...item } type="address" isLoading={ isPlaceholderData }/>) : null;
return (
<DataListDisplay
isError={ isError }
isLoading={ isLoading }
isLoading={ false }
items={ data?.items }
emptyText="There are no logs for this address."
content={ content }
actionBar={ actionBar }
skeletonProps={{ customSkeleton: skeleton }}
skeletonProps={{ customSkeleton: null }}
/>
);
};
......
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 { erc1155A } from 'mocks/tokens/tokenTransfer';
......@@ -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({
status: 200,
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 }) => {
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({
status: 200,
body: JSON.stringify({ items: [ erc1155A ] }),
......@@ -50,3 +50,41 @@ test('with token filter and no pagination +@mobile', async({ mount, page }) => {
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';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
import TOKEN_TYPE from 'lib/token/tokenTypes';
import { getTokenTransfersStub } from 'stubs/token';
import ActionBar from 'ui/shared/ActionBar';
import DataListDisplay from 'ui/shared/DataListDisplay';
import HashStringShorten from 'ui/shared/HashStringShorten';
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 TokenTransferFilter from 'ui/shared/TokenTransfer/TokenTransferFilter';
import TokenTransferList from 'ui/shared/TokenTransfer/TokenTransferList';
......@@ -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',
pathParams: { hash: currentAddress },
filters: tokenFilter ? { token: tokenFilter } : filters,
scrollRef,
options: {
placeholderData: getTokenTransfersStub(undefined, {
block_number: 7793535,
index: 46,
items_count: 50,
}),
},
});
const handleTypeFilterChange = React.useCallback((nextValue: Array<TokenType>) => {
......@@ -172,16 +180,17 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD
showSocketInfo={ pagination.page === 1 && !tokenFilter }
socketInfoAlert={ socketAlert }
socketInfoNum={ newItemsCount }
isLoading={ isPlaceholderData }
/>
</Hide>
<Show below="lg" ssr={ false }>
{ pagination.page === 1 && !tokenFilter && (
<SocketNewItemsNotice
<SocketNewItemsNotice.Mobile
url={ window.location.href }
num={ newItemsCount }
alert={ socketAlert }
type="token_transfer"
borderBottomRadius={ 0 }
isLoading={ isPlaceholderData }
/>
) }
<TokenTransferList
......@@ -189,6 +198,7 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD
baseAddress={ currentAddress }
showTxInfo
enableTimeIncrement
isLoading={ isPlaceholderData }
/>
</Show>
</>
......@@ -227,7 +237,7 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD
<>
{ isMobile && tokenFilterComponent }
{ !isActionBarHidden && (
<ActionBar mt={ -6 } showShadow={ isLoading }>
<ActionBar mt={ -6 }>
{ !isMobile && tokenFilterComponent }
{ !tokenFilter && (
<TokenTransferFilter
......@@ -237,9 +247,17 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD
withAddressFilter
onAddressFilterChange={ handleAddressFilterChange }
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 }/> }
</ActionBar>
) }
......@@ -249,7 +267,7 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD
return (
<DataListDisplay
isError={ isError }
isLoading={ isLoading }
isLoading={ false }
items={ data?.items }
skeletonProps={{
isLongSkeleton: true,
......
......@@ -13,6 +13,8 @@ import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import getQueryParamString from 'lib/router/getQueryParamString';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
import { TX } from 'stubs/tx';
import { generateListStub } from 'stubs/utils';
import ActionBar from 'ui/shared/ActionBar';
import Pagination from 'ui/shared/Pagination';
import TxsContent from 'ui/txs/TxsContent';
......@@ -41,6 +43,13 @@ const AddressTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>}
pathParams: { hash: currentAddress },
filters: { filter: filterValue },
scrollRef,
options: {
placeholderData: generateListStub<'address_txs'>(TX, 50, {
block_number: 9005713,
index: 5,
items_count: 50,
}),
},
});
const handleFilterChange = React.useCallback((val: string | Array<string>) => {
......@@ -112,7 +121,7 @@ const AddressTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>}
topic: `addresses:${ currentAddress?.toLowerCase() }`,
onSocketClose: handleSocketClose,
onSocketError: handleSocketError,
isDisabled: addressTxsQuery.pagination.page !== 1,
isDisabled: addressTxsQuery.pagination.page !== 1 || addressTxsQuery.isPlaceholderData,
});
useSocketMessage({
......@@ -132,15 +141,23 @@ const AddressTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>}
defaultFilter={ filterValue }
onFilterChange={ handleFilterChange }
isActive={ Boolean(filterValue) }
isLoading={ addressTxsQuery.pagination.isLoading }
/>
);
return (
<>
{ !isMobile && (
<ActionBar mt={ -6 } showShadow={ addressTxsQuery.isLoading }>
<ActionBar mt={ -6 }>
{ 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 }/> }
</ActionBar>
) }
......
......@@ -16,9 +16,10 @@ interface Props {
isActive: boolean;
defaultFilter: AddressFromToFilter;
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();
return (
......@@ -26,6 +27,7 @@ const AddressTxsFilter = ({ onFilterChange, defaultFilter, isActive }: Props) =>
<MenuButton>
<FilterButton
isActive={ isOpen || isActive }
isLoading={ isLoading }
onClick={ onToggle }
as="div"
/>
......
......@@ -4,6 +4,8 @@ import React from 'react';
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import getQueryParamString from 'lib/router/getQueryParamString';
import { generateListStub } from 'stubs/utils';
import { WITHDRAWAL } from 'stubs/withdrawals';
import ActionBar from 'ui/shared/ActionBar';
import DataListDisplay from 'ui/shared/DataListDisplay';
import Pagination from 'ui/shared/Pagination';
......@@ -15,24 +17,37 @@ const AddressWithdrawals = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivE
const hash = getQueryParamString(router.query.hash);
const { data, isLoading, isError, pagination, isPaginationVisible } = useQueryWithPages({
const { data, isPlaceholderData, isError, pagination, isPaginationVisible } = useQueryWithPages({
resourceName: 'address_withdrawals',
pathParams: { hash },
scrollRef,
options: {
placeholderData: generateListStub<'address_withdrawals'>(WITHDRAWAL, 50, {
index: 5,
items_count: 50,
}),
},
});
const content = data?.items ? (
<>
<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>
<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>
</>
) : null ;
const actionBar = isPaginationVisible ? (
<ActionBar mt={ -6 } showShadow={ isLoading }>
<ActionBar mt={ -6 }>
<Pagination ml="auto" { ...pagination }/>
</ActionBar>
) : null;
......@@ -40,7 +55,7 @@ const AddressWithdrawals = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivE
return (
<DataListDisplay
isError={ isError }
isLoading={ isLoading }
isLoading={ false }
items={ data?.items }
skeletonProps={{ isLongSkeleton: true, skeletonDesktopColumns: Array(5).fill(`${ 100 / 5 }%`), skeletonDesktopMinW: '950px' }}
emptyText="There are no withdrawals for this address."
......
This diff is collapsed.
......@@ -22,10 +22,22 @@ const BlockWithdrawals = ({ blockWithdrawalsQuery }: Props) => {
const content = blockWithdrawalsQuery.data?.items ? (
<>
<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>
<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>
</>
) : null ;
......@@ -33,7 +45,7 @@ const BlockWithdrawals = ({ blockWithdrawalsQuery }: Props) => {
return (
<DataListDisplay
isError={ blockWithdrawalsQuery.isError }
isLoading={ blockWithdrawalsQuery.isLoading }
isLoading={ false }
items={ blockWithdrawalsQuery.data?.items }
skeletonProps={{ isLongSkeleton: true, skeletonDesktopColumns: Array(4).fill(`${ 100 / 4 }%`), skeletonDesktopMinW: '950px' }}
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 React from 'react';
......@@ -7,13 +7,18 @@ import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
interface Props {
ts: string;
isEnabled?: boolean;
isLoading?: boolean;
fontSize?: TypographyProps['fontSize'];
}
const BlockTimestamp = ({ ts, isEnabled, fontSize }: Props) => {
const BlockTimestamp = ({ ts, isEnabled, isLoading, fontSize }: Props) => {
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);
import { Show, Hide, Alert } from '@chakra-ui/react';
import { Alert, Box } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query';
import { useQueryClient } from '@tanstack/react-query';
import React from 'react';
......@@ -66,7 +66,7 @@ const BlocksContent = ({ type, query }: Props) => {
topic: 'blocks:new_block',
onSocketClose: handleSocketClose,
onSocketError: handleSocketError,
isDisabled: query.isLoading || query.isError || query.pagination.page !== 1,
isDisabled: query.isPlaceholderData || query.isError || query.pagination.page !== 1,
});
useSocketMessage({
channel,
......@@ -77,12 +77,12 @@ const BlocksContent = ({ type, query }: Props) => {
const content = query.data?.items ? (
<>
{ socketAlert && <Alert status="warning" mb={ 6 } as="a" href={ window.document.location.href }>{ socketAlert }</Alert> }
<Show below="lg" key="content-mobile" ssr={ false }>
<BlocksList data={ query.data.items }/>
</Show>
<Hide below="lg" key="content-desktop" ssr={ false }>
<BlocksTable data={ query.data.items } top={ query.isPaginationVisible ? 80 : 0 } page={ query.pagination.page }/>
</Hide>
<Box display={{ base: 'block', lg: 'none' }}>
<BlocksList data={ query.data.items } isLoading={ query.isPlaceholderData } page={ query.pagination.page }/>
</Box>
<Box display={{ base: 'none', lg: 'block' }}>
<BlocksTable data={ query.data.items } top={ query.isPaginationVisible ? 80 : 0 } page={ query.pagination.page } isLoading={ query.isPlaceholderData }/>
</Box>
</>
) : null;
......@@ -95,7 +95,7 @@ const BlocksContent = ({ type, query }: Props) => {
return (
<DataListDisplay
isError={ query.isError }
isLoading={ query.isLoading }
isLoading={ false }
items={ query.data?.items }
skeletonProps={{ skeletonDesktopColumns: [ '125px', '120px', '21%', '64px', '35%', '22%', '22%' ] }}
emptyText="There are no blocks."
......
......@@ -8,14 +8,22 @@ import BlocksListItem from 'ui/blocks/BlocksListItem';
interface Props {
data: Array<Block>;
isLoading: boolean;
page: number;
}
const BlocksList = ({ data }: Props) => {
const BlocksList = ({ data, isLoading, page }: Props) => {
return (
<Box>
<AnimatePresence initial={ false }>
{ /* TODO prop "enableTimeIncrement" should be set to false for second and later pages */ }
{ data.map((item) => <BlocksListItem key={ item.height } data={ item } enableTimeIncrement/>) }
{ data.map((item, index) => (
<BlocksListItem
key={ item.height + (isLoading ? String(index) : '') }
data={ item }
isLoading={ isLoading }
enableTimeIncrement={ page === 1 && !isLoading }
/>
)) }
</AnimatePresence>
</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 capitalize from 'lodash/capitalize';
import { route } from 'nextjs-routes';
......@@ -13,6 +13,7 @@ import { WEI } from 'lib/consts';
import getNetworkValidatorTitle from 'lib/networks/getNetworkValidatorTitle';
import BlockTimestamp from 'ui/blocks/BlockTimestamp';
import AddressLink from 'ui/shared/address/AddressLink';
import Icon from 'ui/shared/chakra/Icon';
import GasUsedToTargetRatio from 'ui/shared/GasUsedToTargetRatio';
import LinkInternal from 'ui/shared/LinkInternal';
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
......@@ -21,11 +22,11 @@ import Utilization from 'ui/shared/Utilization/Utilization';
interface Props {
data: Block;
isPending?: boolean;
isLoading?: boolean;
enableTimeIncrement?: boolean;
}
const BlocksListItem = ({ data, isPending, enableTimeIncrement }: Props) => {
const BlocksListItem = ({ data, isLoading, enableTimeIncrement }: Props) => {
const totalReward = getBlockTotalReward(data);
const burntFees = BigNumber(data.burnt_fees || 0);
const txFees = BigNumber(data.tx_fees || 0);
......@@ -36,30 +37,35 @@ const BlocksListItem = ({ data, isPending, enableTimeIncrement }: Props) => {
<ListItemMobile rowGap={ 3 } key={ String(data.height) } isAnimated>
<Flex justifyContent="space-between" w="100%">
<Flex columnGap={ 2 } alignItems="center">
{ isPending && <Spinner size="sm"/> }
<LinkInternal
fontWeight={ 600 }
href={ route({ pathname: '/block/[height]', query: { height: data.type === 'reorg' ? String(data.hash) : String(data.height) } }) }
>
{ data.height }
</LinkInternal>
<Skeleton isLoaded={ !isLoading } display="inline-block">
<LinkInternal
fontWeight={ 600 }
href={ route({ pathname: '/block/[height]', query: { height: data.type === 'reorg' ? String(data.hash) : String(data.height) } }) }
>
{ data.height }
</LinkInternal>
</Skeleton>
</Flex>
<BlockTimestamp ts={ data.timestamp } isEnabled={ enableTimeIncrement }/>
<BlockTimestamp ts={ data.timestamp } isEnabled={ enableTimeIncrement } isLoading={ isLoading }/>
</Flex>
<Flex columnGap={ 2 }>
<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 columnGap={ 2 }>
<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 columnGap={ 2 }>
<Text fontWeight={ 500 }>Txn</Text>
{ data.tx_count > 0 ? (
<LinkInternal href={ route({ pathname: '/block/[height]', query: { height: String(data.height), tab: 'txs' } }) }>
{ data.tx_count }
</LinkInternal>
<Skeleton isLoaded={ !isLoading } display="inline-block">
<LinkInternal href={ route({ pathname: '/block/[height]', query: { height: String(data.height), tab: 'txs' } }) }>
{ data.tx_count }
</LinkInternal>
</Skeleton>
) :
<Text variant="secondary">{ data.tx_count }</Text>
}
......@@ -67,12 +73,14 @@ const BlocksListItem = ({ data, isPending, enableTimeIncrement }: Props) => {
<Box>
<Text fontWeight={ 500 }>Gas used</Text>
<Flex mt={ 2 }>
<Text variant="secondary" mr={ 4 }>{ BigNumber(data.gas_used || 0).toFormat() }</Text>
<Utilization colorScheme="gray" value={ BigNumber(data.gas_used || 0).div(BigNumber(data.gas_limit)).toNumber() }/>
<Skeleton isLoaded={ !isLoading } display="inline-block" color="text_secondary" mr={ 4 }>
<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 && (
<>
<TextSeparator color={ separatorColor } mx={ 1 }/>
<GasUsedToTargetRatio value={ data.gas_target_percentage }/>
<GasUsedToTargetRatio value={ data.gas_target_percentage } isLoading={ isLoading }/>
</>
) }
</Flex>
......@@ -80,7 +88,9 @@ const BlocksListItem = ({ data, isPending, enableTimeIncrement }: Props) => {
{ !appConfig.L2.isL2Network && (
<Flex columnGap={ 2 }>
<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>
) }
{ !appConfig.L2.isL2Network && (
......@@ -88,10 +98,12 @@ const BlocksListItem = ({ data, isPending, enableTimeIncrement }: Props) => {
<Text fontWeight={ 500 }>Burnt fees</Text>
<Flex columnGap={ 4 } mt={ 2 }>
<Flex>
<Icon as={ flameIcon } boxSize={ 5 } color="gray.500"/>
<Text variant="secondary" ml={ 1 }>{ burntFees.div(WEI).toFixed() }</Text>
<Icon as={ flameIcon } boxSize={ 5 } color="gray.500" isLoading={ isLoading }/>
<Skeleton isLoaded={ !isLoading } display="inline-block" color="text_secondary" ml={ 1 }>
<span>{ burntFees.div(WEI).toFixed() }</span>
</Skeleton>
</Flex>
<Utilization ml={ 4 } value={ burntFees.div(txFees).toNumber() }/>
<Utilization ml={ 4 } value={ burntFees.div(txFees).toNumber() } isLoading={ isLoading }/>
</Flex>
</Box>
) }
......
......@@ -2,8 +2,8 @@ import { Flex, Box, Text, Skeleton } from '@chakra-ui/react';
import React from 'react';
import useApiQuery from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile';
import { nbsp } from 'lib/html-entities';
import { HOMEPAGE_STATS } from 'stubs/stats';
import type { Props as PaginationProps } from 'ui/shared/Pagination';
import Pagination from 'ui/shared/Pagination';
......@@ -13,24 +13,22 @@ interface Props {
}
const BlocksTabSlot = ({ pagination, isPaginationVisible }: Props) => {
const isMobile = useIsMobile();
const statsQuery = useApiQuery('homepage_stats');
if (isMobile) {
return null;
}
const statsQuery = useApiQuery('homepage_stats', {
queryOptions: {
placeholderData: HOMEPAGE_STATS,
},
});
return (
<Flex alignItems="center" columnGap={ 8 }>
{ statsQuery.isLoading && <Skeleton w="175px" h="24px"/> }
<Flex alignItems="center" columnGap={ 8 } display={{ base: 'none', lg: 'flex' }}>
{ statsQuery.data?.network_utilization_percentage !== undefined && (
<Box>
<Text as="span" fontSize="sm">
Network utilization (last 50 blocks):{ nbsp }
</Text>
<Text as="span" fontSize="sm" color="blue.400" fontWeight={ 600 }>
{ statsQuery.data.network_utilization_percentage.toFixed(2) }%
</Text>
<Skeleton display="inline-block" fontSize="sm" color="blue.400" fontWeight={ 600 } isLoaded={ !statsQuery.isPlaceholderData }>
<span>{ statsQuery.data.network_utilization_percentage.toFixed(2) }%</span>
</Skeleton>
</Box>
) }
{ isPaginationVisible && <Pagination my={ 1 } { ...pagination }/> }
......
......@@ -12,11 +12,12 @@ import { default as Thead } from 'ui/shared/TheadSticky';
interface Props {
data: Array<Block>;
isLoading?: boolean;
top: number;
page: number;
}
const BlocksTable = ({ data, top, page }: Props) => {
const BlocksTable = ({ data, isLoading, top, page }: Props) => {
return (
<Table variant="simple" minWidth="1040px" size="md" fontWeight={ 500 }>
......@@ -33,7 +34,14 @@ const BlocksTable = ({ data, top, page }: Props) => {
</Thead>
<Tbody>
<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>
</Tbody>
</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 { motion } from 'framer-motion';
import { route } from 'nextjs-routes';
......@@ -12,6 +12,7 @@ import getBlockTotalReward from 'lib/block/getBlockTotalReward';
import { WEI } from 'lib/consts';
import BlockTimestamp from 'ui/blocks/BlockTimestamp';
import AddressLink from 'ui/shared/address/AddressLink';
import Icon from 'ui/shared/chakra/Icon';
import GasUsedToTargetRatio from 'ui/shared/GasUsedToTargetRatio';
import LinkInternal from 'ui/shared/LinkInternal';
import TextSeparator from 'ui/shared/TextSeparator';
......@@ -19,17 +20,18 @@ import Utilization from 'ui/shared/Utilization/Utilization';
interface Props {
data: Block;
isPending?: boolean;
isLoading?: boolean;
enableTimeIncrement?: boolean;
}
const BlocksTableItem = ({ data, isPending, enableTimeIncrement }: Props) => {
const BlocksTableItem = ({ data, isLoading, enableTimeIncrement }: Props) => {
const totalReward = getBlockTotalReward(data);
const burntFees = BigNumber(data.burnt_fees || 0);
const txFees = BigNumber(data.tx_fees || 0);
const separatorColor = useColorModeValue('gray.200', 'gray.700');
const burntFeesIconColor = useColorModeValue('gray.500', 'inherit');
return (
<Tr
as={ motion.tr }
......@@ -41,57 +43,82 @@ const BlocksTableItem = ({ data, isPending, enableTimeIncrement }: Props) => {
>
<Td fontSize="sm">
<Flex columnGap={ 2 } alignItems="center" mb={ 2 }>
{ isPending && <Spinner size="sm" flexShrink={ 0 }/> }
<Tooltip isDisabled={ data.type !== 'reorg' } label="Chain reorganizations">
<LinkInternal
fontWeight={ 600 }
href={ route({ pathname: '/block/[height]', query: { height: data.type === 'reorg' ? String(data.hash) : String(data.height) } }) }
>
{ data.height }
</LinkInternal>
<Skeleton isLoaded={ !isLoading } display="inline-block">
<LinkInternal
fontWeight={ 600 }
href={ route({ pathname: '/block/[height]', query: { height: data.type === 'reorg' ? String(data.hash) : String(data.height) } }) }
>
{ data.height }
</LinkInternal>
</Skeleton>
</Tooltip>
</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 fontSize="sm">{ data.size.toLocaleString() }</Td>
<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 isNumeric fontSize="sm">
{ data.tx_count > 0 ? (
<LinkInternal href={ route({ pathname: '/block/[height]', query: { height: String(data.height), tab: 'txs' } }) }>
{ data.tx_count }
</LinkInternal>
<Skeleton isLoaded={ !isLoading } display="inline-block">
<LinkInternal href={ route({ pathname: '/block/[height]', query: { height: String(data.height), tab: 'txs' } }) }>
{ data.tx_count }
</LinkInternal>
</Skeleton>
) : data.tx_count }
</Td>
{ !appConfig.L2.isL2Network && (
<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 }>
<Tooltip label="Gas Used %">
<Tooltip label={ isLoading ? undefined : 'Gas Used %' }>
<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>
</Tooltip>
{ data.gas_target_percentage && (
<>
<TextSeparator color={ separatorColor } mx={ 1 }/>
<GasUsedToTargetRatio value={ data.gas_target_percentage }/>
<GasUsedToTargetRatio value={ data.gas_target_percentage } isLoading={ isLoading }/>
</>
) }
</Flex>
</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 && (
<Td fontSize="sm">
<Flex alignItems="center" columnGap={ 1 }>
<Icon as={ flameIcon } boxSize={ 5 } color={ burntFeesIconColor }/>
{ burntFees.dividedBy(WEI).toFixed(8) }
<Icon as={ flameIcon } boxSize={ 5 } color={ burntFeesIconColor } isLoading={ isLoading }/>
<Skeleton isLoaded={ !isLoading } display="inline-block">
{ burntFees.dividedBy(WEI).toFixed(8) }
</Skeleton>
</Flex>
<Tooltip label="Burnt fees / Txn fees * 100%">
<Tooltip label={ isLoading ? undefined : 'Burnt fees / Txn fees * 100%' }>
<Box w="min-content">
<Utilization mt={ 2 } value={ burntFees.div(txFees).toNumber() }/>
<Utilization mt={ 2 } value={ burntFees.div(txFees).toNumber() } isLoading={ isLoading }/>
</Box>
</Tooltip>
</Td>
......
import { Skeleton } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';
......@@ -10,6 +9,10 @@ import { useAppContext } from 'lib/appContext';
import useIsMobile from 'lib/hooks/useIsMobile';
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
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 BlockWithdrawals from 'ui/block/BlockWithdrawals';
import TextAd from 'ui/shared/ad/TextAd';
......@@ -36,14 +39,22 @@ const BlockPageContent = () => {
const blockQuery = useApiQuery('block', {
pathParams: { height },
queryOptions: { enabled: Boolean(height) },
queryOptions: {
enabled: Boolean(height),
placeholderData: BLOCK,
},
});
const blockTxsQuery = useQueryWithPages({
resourceName: 'block_txs',
pathParams: { height },
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 = () => {
resourceName: 'block_withdrawals',
pathParams: { height },
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 = () => {
return (
<>
{ blockQuery.isLoading ? <Skeleton h={{ base: 12, lg: 6 }} mb={ 6 } w="100%" maxW="680px"/> : <TextAd mb={ 6 }/> }
{ blockQuery.isLoading ? (
<Skeleton h={ 10 } w="300px" mb={ 6 }/>
) : (
<PageTitle
title={ `Block #${ blockQuery.data?.height }` }
backLink={ backLink }
contentAfter={ <NetworkExplorers type="block" pathParam={ height } ml={{ base: 'initial', lg: 'auto' }}/> }
/>
) }
{ blockQuery.isLoading ? <SkeletonTabs/> : (
<TextAd mb={ 6 }/>
<PageTitle
title={ `Block #${ blockQuery.data?.height }` }
backLink={ backLink }
contentAfter={ <NetworkExplorers type="block" pathParam={ height } ml={{ base: 'initial', lg: 'auto' }}/> }
isLoading={ blockQuery.isPlaceholderData }
/>
{ blockQuery.isPlaceholderData ? <SkeletonTabs tabs={ tabs }/> : (
<RoutedTabs
tabs={ tabs }
tabListProps={ isMobile ? undefined : TAB_LIST_PROPS }
......
......@@ -58,7 +58,7 @@ test('base view +@mobile +@dark-mode', async({ mount, page }) => {
);
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 }) => {
......@@ -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 }) => {
......@@ -105,5 +105,5 @@ test('socket error', async({ mount, page, createSocket }) => {
await socketServer.joinChannel(socket, 'blocks:new_block');
socket.close();
await expect(component.locator('main')).toHaveScreenshot();
await expect(component).toHaveScreenshot();
});
......@@ -6,9 +6,10 @@ import type { RoutedTab } from 'ui/shared/Tabs/types';
import useIsMobile from 'lib/hooks/useIsMobile';
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import { BLOCK } from 'stubs/block';
import { generateListStub } from 'stubs/utils';
import BlocksContent from 'ui/blocks/BlocksContent';
import BlocksTabSlot from 'ui/blocks/BlocksTabSlot';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle';
import RoutedTabs from 'ui/shared/Tabs/RoutedTabs';
......@@ -32,6 +33,12 @@ const BlocksPageContent = () => {
const blocksQuery = useQueryWithPages({
resourceName: 'blocks',
filters: { type },
options: {
placeholderData: generateListStub<'blocks'>(BLOCK, 50, {
block_number: 8988686,
items_count: 50,
}),
},
});
const tabs: Array<RoutedTab> = [
......@@ -41,7 +48,7 @@ const BlocksPageContent = () => {
];
return (
<Page>
<>
<PageTitle title="Blocks" withTextAd/>
<RoutedTabs
tabs={ tabs }
......@@ -49,7 +56,7 @@ const BlocksPageContent = () => {
rightSlot={ <BlocksTabSlot pagination={ blocksQuery.pagination } isPaginationVisible={ blocksQuery.isPaginationVisible }/> }
stickyEnabled={ !isMobile }
/>
</Page>
</>
);
};
......
......@@ -20,6 +20,7 @@ import useSocketMessage from 'lib/socket/useSocketMessage';
import trimTokenSymbol from 'lib/token/trimTokenSymbol';
import * as addressStubs from 'stubs/address';
import * as tokenStubs from 'stubs/token';
import { generateListStub } from 'stubs/utils';
import AddressContract from 'ui/address/AddressContract';
import TextAd from 'ui/shared/ad/TextAd';
import EntityTags from 'ui/shared/EntityTags';
......@@ -145,7 +146,7 @@ const TokenPageContent = () => {
scrollRef,
options: {
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';
import { useAppContext } from 'lib/appContext';
import useIsMobile from 'lib/hooks/useIsMobile';
import getQueryParamString from 'lib/router/getQueryParamString';
import { TX } from 'stubs/tx';
import TextAd from 'ui/shared/ad/TextAd';
import EntityTags from 'ui/shared/EntityTags';
import NetworkExplorers from 'ui/shared/NetworkExplorers';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle';
import RoutedTabs from 'ui/shared/Tabs/RoutedTabs';
import TxDetails from 'ui/tx/TxDetails';
......@@ -38,7 +38,10 @@ const TransactionPageContent = () => {
const { data, isPlaceholderData } = useApiQuery('tx', {
pathParams: { hash },
queryOptions: { enabled: Boolean(hash) },
queryOptions: {
enabled: Boolean(hash),
placeholderData: TX,
},
});
const tags = (
......@@ -65,7 +68,7 @@ const TransactionPageContent = () => {
}, [ appProps.referrer ]);
return (
<Page>
<>
<TextAd mb={ 6 }/>
<PageTitle
title="Transaction details"
......@@ -73,7 +76,7 @@ const TransactionPageContent = () => {
contentAfter={ tags }
/>
<RoutedTabs tabs={ TABS }/>
</Page>
</>
);
};
......
import { Box } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';
......@@ -9,7 +8,8 @@ import useHasAccount from 'lib/hooks/useHasAccount';
import useIsMobile from 'lib/hooks/useIsMobile';
import useNewTxsSocket from 'lib/hooks/useNewTxsSocket';
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 RoutedTabs from 'ui/shared/Tabs/RoutedTabs';
import TxsContent from 'ui/txs/TxsContent';
......@@ -31,6 +31,12 @@ const Transactions = () => {
filters: { filter: router.query.tab === 'pending' ? 'pending' : 'validated' },
options: {
enabled: !router.query.tab || router.query.tab === 'validated' || router.query.tab === 'pending',
placeholderData: generateListStub<'txs_validated'>(TX, 50, {
block_number: 9005713,
index: 5,
items_count: 50,
filter: 'validated',
}),
},
});
......@@ -71,17 +77,15 @@ const Transactions = () => {
].filter(Boolean);
return (
<Page>
<Box h="100%">
<PageTitle title="Transactions" withTextAd/>
<RoutedTabs
tabs={ tabs }
tabListProps={ isMobile ? undefined : TAB_LIST_PROPS }
rightSlot={ <TxsTabSlot pagination={ txsQuery.pagination } isPaginationVisible={ txsQuery.isPaginationVisible && !isMobile }/> }
stickyEnabled={ !isMobile }
/>
</Box>
</Page>
<>
<PageTitle title="Transactions" withTextAd/>
<RoutedTabs
tabs={ tabs }
tabListProps={ isMobile ? undefined : TAB_LIST_PROPS }
rightSlot={ <TxsTabSlot pagination={ txsQuery.pagination } isPaginationVisible={ txsQuery.isPaginationVisible && !isMobile }/> }
stickyEnabled={ !isMobile }
/>
</>
);
};
......
......@@ -32,5 +32,5 @@ test('base view +@mobile', async({ mount, page }) => {
</TestApp>,
);
await expect(component.locator('main')).toHaveScreenshot();
await expect(component).toHaveScreenshot();
});
......@@ -6,9 +6,10 @@ import useApiQuery from 'lib/api/useApiQuery';
import getCurrencyValue from 'lib/getCurrencyValue';
import useIsMobile from 'lib/hooks/useIsMobile';
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import { generateListStub } from 'stubs/utils';
import { WITHDRAWAL } from 'stubs/withdrawals';
import ActionBar from 'ui/shared/ActionBar';
import DataListDisplay from 'ui/shared/DataListDisplay';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle';
import Pagination from 'ui/shared/Pagination';
import WithdrawalsListItem from 'ui/withdrawals/WithdrawalsListItem';
......@@ -17,16 +18,33 @@ import WithdrawalsTable from 'ui/withdrawals/WithdrawalsTable';
const Withdrawals = () => {
const isMobile = useIsMobile();
const { data, isError, isLoading, isPaginationVisible, pagination } = useQueryWithPages({
const { data, isError, isPlaceholderData, isPaginationVisible, pagination } = useQueryWithPages({
resourceName: 'withdrawals',
options: {
placeholderData: generateListStub<'withdrawals'>(WITHDRAWAL, 50, {
index: 5,
items_count: 50,
}),
},
});
const countersQuery = useApiQuery('withdrawals_counters');
const content = data?.items ? (
<>
<Show below="lg" ssr={ false }>{ data.items.map((item => <WithdrawalsListItem key={ item.index } item={ item } view="list"/>)) }</Show>
<Hide below="lg" ssr={ false }><WithdrawalsTable items={ data.items } view="list" top={ isPaginationVisible ? 80 : 0 }/></Hide>
<Show below="lg" ssr={ false }>
{ 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;
......@@ -69,18 +87,18 @@ const Withdrawals = () => {
);
return (
<Page>
<>
<PageTitle title="Withdrawals" withTextAd/>
<DataListDisplay
isError={ isError }
isLoading={ isLoading }
isLoading={ false }
items={ data?.items }
skeletonProps={{ skeletonDesktopColumns: Array(6).fill(`${ 100 / 6 }%`), skeletonDesktopMinW: '950px' }}
emptyText="There are no withdrawals."
content={ content }
actionBar={ actionBar }
/>
</Page>
</>
);
};
......
......@@ -3,6 +3,7 @@ import {
useColorModeValue,
chakra,
Button,
Skeleton,
} from '@chakra-ui/react';
import React from 'react';
......@@ -10,14 +11,19 @@ import infoIcon from 'icons/info.svg';
interface Props {
isOpen?: boolean;
isLoading?: boolean;
className?: string;
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');
if (isLoading) {
return <Skeleton boxSize={ 6 } borderRadius="sm" flexShrink={ 0 }/>;
}
return (
<Button
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 keyIcon from 'icons/key.svg';
import Icon from 'ui/shared/chakra/Icon';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
interface Props {
......@@ -13,9 +14,7 @@ interface Props {
const ApiKeySnippet = ({ apiKey, name, isLoading }: Props) => {
return (
<HStack spacing={ 2 } alignItems="start">
<Skeleton isLoaded={ !isLoading } boxSize={ 6 } display="inline-block">
<Icon as={ keyIcon } boxSize={ 6 } color={ useColorModeValue('gray.500', 'gray.400') }/>
</Skeleton>
<Icon as={ keyIcon } boxSize={ 6 } color={ useColorModeValue('gray.500', 'gray.400') } isLoading={ isLoading }/>
<Box>
<Flex alignItems={{ base: 'flex-start', lg: 'center' }}>
<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';
type Props = {
value: number;
isLoading?: boolean;
}
const GasUsedToTargetRatio = ({ value }: Props) => {
const GasUsedToTargetRatio = ({ value, isLoading }: Props) => {
return (
<Tooltip label="% of Gas Target">
<Text variant="secondary">
{ (value > 0 ? '+' : '') + value.toLocaleString(undefined, { maximumFractionDigits: 2 }) }%
</Text>
<Skeleton color="text_secondary" isLoaded={ !isLoading }>
<span>{ (value > 0 ? '+' : '') + value.toLocaleString(undefined, { maximumFractionDigits: 2 }) }%</span>
</Skeleton>
</Tooltip>
);
};
......
import { Tag, chakra } from '@chakra-ui/react';
import { chakra } from '@chakra-ui/react';
import React from 'react';
import Tag from 'ui/shared/chakra/Tag';
interface Props {
isIn: boolean;
isOut: boolean;
className?: string;
isLoading?: boolean;
}
const InOutTag = ({ isIn, isOut, className }: Props) => {
const InOutTag = ({ isIn, isOut, className, isLoading }: Props) => {
if (!isIn && !isOut) {
return null;
}
......@@ -20,6 +23,7 @@ const InOutTag = ({ isIn, isOut, className }: Props) => {
colorScheme={ colorScheme }
display="flex"
justifyContent="center"
isLoading={ isLoading }
>
{ isOut ? 'OUT' : 'IN' }
</Tag>
......
import { Grid, chakra, GridItem } from '@chakra-ui/react';
import { Grid, chakra, GridItem, Skeleton } from '@chakra-ui/react';
import { motion } from 'framer-motion';
import React from 'react';
......@@ -38,13 +38,21 @@ const Container = chakra(({ isAnimated, children, className }: ContainerProps) =
interface LabelProps {
className?: string;
children: React.ReactNode;
isLoading?: boolean;
}
const Label = chakra(({ children, className }: LabelProps) => {
const Label = chakra(({ children, className, isLoading }: LabelProps) => {
return (
<GridItem className={ className } fontWeight={ 500 } lineHeight="20px" py="5px">
<Skeleton
className={ className }
isLoaded={ !isLoading }
fontWeight={ 500 }
lineHeight="20px"
my="5px"
justifySelf="start"
>
{ 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 arrowIcon from 'icons/arrows/east-mini.svg';
......@@ -11,9 +11,10 @@ export type Props = {
hasNextPage: boolean;
className?: string;
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 (
<Flex
......@@ -21,46 +22,51 @@ const Pagination = ({ page, onNextPageClick, onPrevPageClick, resetPage, hasNext
fontSize="sm"
alignItems="center"
>
<Button
variant="outline"
size="sm"
onClick={ resetPage }
isDisabled={ page === 1 }
mr={ 4 }
>
<Skeleton isLoaded={ !isLoading } display="inline-block" mr={ 4 } borderRadius="base">
<Button
variant="outline"
size="sm"
onClick={ resetPage }
isDisabled={ page === 1 }
>
First
</Button>
<IconButton
variant="outline"
onClick={ onPrevPageClick }
size="sm"
aria-label="Next page"
w="36px"
icon={ <Icon as={ arrowIcon } w={ 5 } h={ 5 }/> }
mr={ 3 }
isDisabled={ !canGoBackwards || page === 1 }
/>
<Button
variant="outline"
size="sm"
isActive
borderWidth="1px"
fontWeight={ 400 }
h={ 8 }
cursor="unset"
>
{ page }
</Button>
<IconButton
variant="outline"
onClick={ onNextPageClick }
size="sm"
aria-label="Next page"
w="36px"
icon={ <Icon as={ arrowIcon } w={ 5 } h={ 5 } transform="rotate(180deg)"/> }
ml={ 3 }
isDisabled={ !hasNextPage }
/>
</Button>
</Skeleton>
<Skeleton isLoaded={ !isLoading } display="inline-block" mr={ 3 } borderRadius="base">
<IconButton
variant="outline"
onClick={ onPrevPageClick }
size="sm"
aria-label="Next page"
w="36px"
icon={ <Icon as={ arrowIcon } w={ 5 } h={ 5 }/> }
isDisabled={ !canGoBackwards || page === 1 }
/>
</Skeleton>
<Skeleton isLoaded={ !isLoading } display="inline-block" borderRadius="base">
<Button
variant="outline"
size="sm"
isActive
borderWidth="1px"
fontWeight={ 400 }
h={ 8 }
cursor="unset"
>
{ page }
</Button>
</Skeleton>
<Skeleton isLoaded={ !isLoading } display="inline-block" ml={ 3 } borderRadius="base">
<IconButton
variant="outline"
onClick={ onNextPageClick }
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 */ }
{ /* <Flex alignItems="center" width="132px" ml={ 16 } display={{ base: 'none', lg: 'flex' }}>
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 eastArrow from 'icons/arrows/east-mini.svg';
......@@ -10,9 +10,10 @@ interface Props {
nextLabel?: string;
isPrevDisabled?: 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(() => {
onClick('prev');
}, [ onClick ]);
......@@ -21,6 +22,15 @@ const PrevNext = ({ className, onClick, prevLabel, nextLabel, isPrevDisabled, is
onClick('next');
}, [ onClick ]);
if (isLoading) {
return (
<Flex columnGap="10px" className={ className }>
<Skeleton boxSize={ 6 } borderRadius="sm"/>
<Skeleton boxSize={ 6 } borderRadius="sm"/>
</Flex>
);
}
return (
<Box className={ className }>
<Tooltip label={ prevLabel }>
......
......@@ -61,6 +61,7 @@ const SocketNewItemsNotice = chakra(({ children, className, url, num, alert, typ
py="6px"
fontWeight={ 400 }
fontSize="sm"
lineHeight={ 5 }
bgColor={ bgColor }
color={ color }
>
......@@ -77,7 +78,7 @@ export const Desktop = ({ ...props }: Props) => {
return (
<SocketNewItemsNotice
borderRadius={ props.isLoading ? 'sm' : 0 }
h={ props.isLoading ? 4 : 'auto' }
h={ props.isLoading ? 5 : 'auto' }
maxW={ props.isLoading ? '215px' : undefined }
w="100%"
mx={ props.isLoading ? 4 : 0 }
......
......@@ -15,7 +15,7 @@ export interface Props {
const TokenLogo = ({ className, isLoading, data }: Props) => {
if (isLoading) {
return <Skeleton className={ className } borderRadius="base"/>;
return <Skeleton className={ className } borderRadius="base" flexShrink={ 0 }/>;
}
const logoSrc = (() => {
......
......@@ -12,14 +12,15 @@ interface Props {
className?: string;
logoSize?: number;
isDisabled?: boolean;
isLoading?: boolean;
hideSymbol?: boolean;
}
const TokenSnippet = ({ data, className, logoSize = 6, isDisabled, hideSymbol }: Props) => {
const TokenSnippet = ({ data, className, logoSize = 6, isDisabled, hideSymbol, isLoading }: Props) => {
return (
<Flex className={ className } alignItems="center" columnGap={ 2 } w="100%">
<TokenLogo boxSize={ logoSize } data={ data }/>
<AddressLink hash={ data?.address || '' } alias={ data?.name || 'Unnamed token' } type="token" isDisabled={ isDisabled }/>
<TokenLogo boxSize={ logoSize } data={ data } isLoading={ isLoading }/>
<AddressLink hash={ data?.address || '' } alias={ data?.name || 'Unnamed token' } type="token" isDisabled={ isDisabled } isLoading={ isLoading }/>
{ data?.symbol && !hideSymbol && <Text variant="secondary">({ trimTokenSymbol(data.symbol) })</Text> }
</Flex>
);
......
......@@ -19,6 +19,7 @@ interface Props {
withAddressFilter?: boolean;
onAddressFilterChange?: (nextValue: string) => void;
defaultAddressFilter?: AddressFromToFilter;
isLoading?: boolean;
}
const TokenTransferFilter = ({
......@@ -28,10 +29,11 @@ const TokenTransferFilter = ({
withAddressFilter,
onAddressFilterChange,
defaultAddressFilter,
isLoading,
}: Props) => {
return (
<PopoverFilter appliedFiltersNum={ appliedFiltersNum } contentProps={{ w: '200px' }}>
<PopoverFilter appliedFiltersNum={ appliedFiltersNum } contentProps={{ w: '200px' }} isLoading={ isLoading }>
{ withAddressFilter && (
<>
<Text variant="secondary" fontWeight={ 600 }>Address</Text>
......
......@@ -10,18 +10,20 @@ interface Props {
baseAddress?: string;
showTxInfo?: boolean;
enableTimeIncrement?: boolean;
isLoading?: boolean;
}
const TokenTransferList = ({ data, baseAddress, showTxInfo, enableTimeIncrement }: Props) => {
const TokenTransferList = ({ data, baseAddress, showTxInfo, enableTimeIncrement, isLoading }: Props) => {
return (
<Box>
{ data.map((item) => (
{ data.map((item, index) => (
<TokenTransferListItem
key={ item.tx_hash + item.block_hash + item.log_index }
key={ item.tx_hash + item.block_hash + item.log_index + (isLoading ? index : '') }
{ ...item }
baseAddress={ baseAddress }
showTxInfo={ showTxInfo }
enableTimeIncrement={ enableTimeIncrement }
isLoading={ isLoading }
/>
)) }
</Box>
......
import { Text, Flex, Tag, Icon } from '@chakra-ui/react';
import { Flex, Skeleton } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import React from 'react';
......@@ -10,6 +10,9 @@ import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import Icon from 'ui/shared/chakra/Icon';
import Tag from 'ui/shared/chakra/Tag';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import InOutTag from 'ui/shared/InOutTag';
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
import TokenSnippet from 'ui/shared/TokenSnippet/TokenSnippet';
......@@ -17,12 +20,11 @@ import { getTokenTransferTypeText } from 'ui/shared/TokenTransfer/helpers';
import TokenTransferNft from 'ui/shared/TokenTransfer/TokenTransferNft';
import TxAdditionalInfo from 'ui/txs/TxAdditionalInfo';
import CopyToClipboard from '../CopyToClipboard';
type Props = TokenTransfer & {
baseAddress?: string;
showTxInfo?: boolean;
enableTimeIncrement?: boolean;
isLoading?: boolean;
}
const TokenTransferListItem = ({
......@@ -36,6 +38,7 @@ const TokenTransferListItem = ({
type,
timestamp,
enableTimeIncrement,
isLoading,
}: Props) => {
const value = (() => {
if (!('value' in total)) {
......@@ -51,57 +54,62 @@ const TokenTransferListItem = ({
return (
<ListItemMobile rowGap={ 3 } isAnimated>
<Flex w="100%" justifyContent="space-between">
<Flex flexWrap="wrap" rowGap={ 1 } mr={ showTxInfo && txHash ? 2 : 0 }>
<TokenSnippet data={ token } w="auto" maxW="calc(100% - 140px)" hideSymbol/>
<Tag flexShrink={ 0 } ml={ 2 } mr={ 2 }>{ token.type }</Tag>
<Tag colorScheme="orange">{ getTokenTransferTypeText(type) }</Tag>
<Flex flexWrap="wrap" rowGap={ 1 } mr={ showTxInfo && txHash ? 2 : 0 } columnGap={ 2 }>
<TokenSnippet data={ token } w="auto" maxW="calc(100% - 140px)" hideSymbol isLoading={ isLoading }/>
<Tag flexShrink={ 0 } isLoading={ isLoading }>{ token.type }</Tag>
<Tag colorScheme="orange" isLoading={ isLoading }>{ getTokenTransferTypeText(type) }</Tag>
</Flex>
{ showTxInfo && txHash && (
<TxAdditionalInfo hash={ txHash } isMobile/>
<TxAdditionalInfo hash={ txHash } isMobile isLoading={ isLoading }/>
) }
</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 && (
<Flex justifyContent="space-between" alignItems="center" lineHeight="24px" width="100%">
<Flex>
<Icon
as={ transactionIcon }
boxSize="30px"
mr={ 2 }
color="link"
isLoading={ isLoading }
/>
<Address width="100%">
<Address width="100%" ml={ 2 }>
<AddressLink
hash={ txHash }
type="transaction"
fontWeight="700"
truncation="constant"
isLoading={ isLoading }
/>
</Address>
</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 w="100%" columnGap={ 3 }>
<Address width={ addressWidth }>
<AddressIcon address={ from }/>
<AddressLink type="address" ml={ 2 } fontWeight="500" hash={ from.hash } isDisabled={ baseAddress === from.hash }/>
{ baseAddress !== from.hash && <CopyToClipboard text={ from.hash }/> }
<AddressIcon address={ from } isLoading={ isLoading }/>
<AddressLink type="address" ml={ 2 } fontWeight="500" hash={ from.hash } isDisabled={ baseAddress === from.hash } isLoading={ isLoading }/>
{ baseAddress !== from.hash && <CopyToClipboard text={ from.hash } isLoading={ isLoading }/> }
</Address>
{ baseAddress ?
<InOutTag isIn={ baseAddress === to.hash } isOut={ baseAddress === from.hash } w="50px" textAlign="center"/> :
<Icon as={ eastArrowIcon } boxSize={ 6 } color="gray.500"/>
<InOutTag isIn={ baseAddress === to.hash } isOut={ baseAddress === from.hash } w="50px" textAlign="center" isLoading={ isLoading }/> :
<Icon as={ eastArrowIcon } boxSize={ 6 } color="gray.500" isLoading={ isLoading }/>
}
<Address width={ addressWidth }>
<AddressIcon address={ to }/>
<AddressLink type="address" ml={ 2 } fontWeight="500" hash={ to.hash } isDisabled={ baseAddress === to.hash }/>
{ baseAddress !== to.hash && <CopyToClipboard text={ to.hash }/> }
<AddressIcon address={ to } isLoading={ isLoading }/>
<AddressLink type="address" ml={ 2 } fontWeight="500" hash={ to.hash } isDisabled={ baseAddress === to.hash } isLoading={ isLoading }/>
{ baseAddress !== to.hash && <CopyToClipboard text={ to.hash } isLoading={ isLoading }/> }
</Address>
</Flex>
{ value && (
<Flex columnGap={ 2 } w="100%">
<Text fontWeight={ 500 } flexShrink={ 0 }>Value</Text>
<Text variant="secondary">{ value }</Text>
<Skeleton isLoaded={ !isLoading } fontWeight={ 500 } flexShrink={ 0 }>Value</Skeleton>
<Skeleton isLoaded={ !isLoading } color="text_secondary"><span>{ value }</span></Skeleton>
</Flex>
) }
</ListItemMobile>
......
import { Box, Icon, chakra, Skeleton } from '@chakra-ui/react';
import { Box, chakra, Skeleton } from '@chakra-ui/react';
import { route } from 'nextjs-routes';
import React from 'react';
import nftPlaceholder from 'icons/nft_shield.svg';
import Icon from 'ui/shared/chakra/Icon';
import HashStringShorten from 'ui/shared/HashStringShorten';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import LinkInternal from 'ui/shared/LinkInternal';
......@@ -29,10 +30,8 @@ const TokenTransferNft = ({ hash, id, className, isDisabled, isLoading, truncati
w="100%"
className={ className }
>
<Skeleton isLoaded={ !isLoading } boxSize="30px" mr={ 1 } borderRadius="base">
<Icon as={ nftPlaceholder } boxSize="30px" color="inherit"/>
</Skeleton>
<Skeleton isLoaded={ !isLoading } maxW="calc(100% - 34px)">
<Icon as={ nftPlaceholder } boxSize="30px" color="inherit" isLoading={ isLoading } borderRadius="base"/>
<Skeleton isLoaded={ !isLoading } maxW="calc(100% - 34px)" ml={ 1 }>
{ truncation === 'constant' ? <HashStringShorten hash={ id }/> : <HashStringShortenDynamic hash={ id } fontWeight={ 500 }/> }
</Skeleton>
</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 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 TokenTransferTableItem from 'ui/shared/TokenTransfer/TokenTransferTableItem';
......@@ -16,6 +16,7 @@ interface Props {
showSocketInfo?: boolean;
socketInfoAlert?: string;
socketInfoNum?: number;
isLoading?: boolean;
}
const TokenTransferTable = ({
......@@ -27,6 +28,7 @@ const TokenTransferTable = ({
showSocketInfo,
socketInfoAlert,
socketInfoNum,
isLoading,
}: Props) => {
return (
......@@ -45,26 +47,22 @@ const TokenTransferTable = ({
</Thead>
<Tbody>
{ showSocketInfo && (
<Tr>
<Td colSpan={ 10 } p={ 0 }>
<SocketNewItemsNotice
borderRadius={ 0 }
pl="10px"
url={ window.location.href }
alert={ socketInfoAlert }
num={ socketInfoNum }
type="token_transfer"
/>
</Td>
</Tr>
<SocketNewItemsNotice.Desktop
url={ window.location.href }
alert={ socketInfoAlert }
num={ socketInfoNum }
type="token_transfer"
isLoading={ isLoading }
/>
) }
{ data.map((item) => (
{ data.map((item, index) => (
<TokenTransferTableItem
key={ item.tx_hash + item.block_hash + item.log_index }
key={ item.tx_hash + item.block_hash + item.log_index + (isLoading ? index : '') }
{ ...item }
baseAddress={ baseAddress }
showTxInfo={ showTxInfo }
enableTimeIncrement={ enableTimeIncrement }
isLoading={ isLoading }
/>
)) }
</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 React from 'react';
......@@ -8,18 +8,19 @@ import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import Tag from 'ui/shared/chakra/Tag';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import InOutTag from 'ui/shared/InOutTag';
import TokenSnippet from 'ui/shared/TokenSnippet/TokenSnippet';
import { getTokenTransferTypeText } from 'ui/shared/TokenTransfer/helpers';
import TokenTransferNft from 'ui/shared/TokenTransfer/TokenTransferNft';
import TxAdditionalInfo from 'ui/txs/TxAdditionalInfo';
import CopyToClipboard from '../CopyToClipboard';
type Props = TokenTransfer & {
baseAddress?: string;
showTxInfo?: boolean;
enableTimeIncrement?: boolean;
isLoading?: boolean;
}
const TokenTransferTableItem = ({
......@@ -33,6 +34,7 @@ const TokenTransferTableItem = ({
type,
timestamp,
enableTimeIncrement,
isLoading,
}: Props) => {
const timeAgo = useTimeAgoIncrement(timestamp, enableTimeIncrement);
......@@ -40,48 +42,81 @@ const TokenTransferTableItem = ({
<Tr alignItems="top">
{ showTxInfo && txHash && (
<Td>
<TxAdditionalInfo hash={ txHash }/>
<Box my="3px">
<TxAdditionalInfo hash={ txHash } isLoading={ isLoading }/>
</Box>
</Td>
) }
<Td>
<Flex flexDir="column" alignItems="flex-start">
<TokenSnippet data={ token } lineHeight="30px" hideSymbol/>
<Tag mt={ 1 }>{ token.type }</Tag>
<Tag colorScheme="orange" mt={ 2 }>{ getTokenTransferTypeText(type) }</Tag>
<Flex flexDir="column" alignItems="flex-start" my="3px" rowGap={ 2 }>
<TokenSnippet data={ token } isLoading={ isLoading } hideSymbol/>
<Tag isLoading={ isLoading }>{ token.type }</Tag>
<Tag colorScheme="orange" isLoading={ isLoading }>{ getTokenTransferTypeText(type) }</Tag>
</Flex>
</Td>
<Td lineHeight="30px">
{ 'token_id' in total && <TokenTransferNft hash={ token.address } id={ total.token_id }/> }
<Td>
{ 'token_id' in total && <TokenTransferNft hash={ token.address } id={ total.token_id } isLoading={ isLoading }/> }
</Td>
{ showTxInfo && txHash && (
<Td>
<Address display="inline-flex" maxW="100%" fontWeight={ 600 } lineHeight="30px">
<AddressLink type="transaction" hash={ txHash }/>
<Address display="inline-flex" maxW="100%" fontWeight={ 600 } mt="7px">
<AddressLink type="transaction" hash={ txHash } isLoading={ isLoading }/>
</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>
<Address display="inline-flex" maxW="100%" lineHeight="30px">
<AddressIcon address={ from }/>
<AddressLink type="address" ml={ 2 } fontWeight="500" hash={ from.hash } alias={ from.name } flexGrow={ 1 } isDisabled={ baseAddress === from.hash }/>
{ baseAddress !== from.hash && <CopyToClipboard text={ from.hash }/> }
<Address display="inline-flex" maxW="100%" my="3px">
<AddressIcon address={ from } isLoading={ isLoading }/>
<AddressLink
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>
</Td>
{ baseAddress && (
<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>
<Address display="inline-flex" maxW="100%" lineHeight="30px">
<AddressIcon address={ to }/>
<AddressLink type="address" ml={ 2 } fontWeight="500" hash={ to.hash } alias={ to.name } flexGrow={ 1 } isDisabled={ baseAddress === to.hash }/>
{ baseAddress !== to.hash && <CopyToClipboard text={ to.hash }/> }
<Address display="inline-flex" maxW="100%" my="3px">
<AddressIcon address={ to } isLoading={ isLoading }/>
<AddressLink
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>
</Td>
<Td isNumeric verticalAlign="top" lineHeight="30px">
{ 'value' in total && BigNumber(total.value).div(BigNumber(10 ** Number(total.decimals))).dp(8).toFormat() }
<Td isNumeric verticalAlign="top">
<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>
</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