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

Merge pull request #858 from blockscout/skeletons/rest-pages

smart skeletons: rest  of the pages
parents d0fff2d1 c7e0f297
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 VerifiedAddresses from 'ui/pages/VerifiedAddresses';
import Page from 'ui/shared/Page/Page'; import Page from 'ui/shared/Page/Page';
const VerifiedAddresses = dynamic(() => import('ui/pages/VerifiedAddresses'), { ssr: false });
const VerifiedAddressesPage: NextPage = () => { const VerifiedAddressesPage: NextPage = () => {
const title = getNetworkTitle(); const title = getNetworkTitle();
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 Marketplace from 'ui/pages/Marketplace';
import Page from 'ui/shared/Page/Page'; import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle'; import PageTitle from 'ui/shared/Page/PageTitle';
const Marketplace = dynamic(() => import('ui/pages/Marketplace'), { ssr: false });
const MarketplacePage: NextPage = () => { const MarketplacePage: NextPage = () => {
return ( return (
<Page> <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 L2Deposits from 'ui/pages/L2Deposits'; import Page from 'ui/shared/Page/Page';
const L2Deposits = dynamic(() => import('ui/pages/L2Deposits'), { ssr: false });
const DepositsPage: NextPage = () => { const DepositsPage: NextPage = () => {
const title = getNetworkTitle(); const title = getNetworkTitle();
...@@ -12,7 +15,9 @@ const DepositsPage: NextPage = () => { ...@@ -12,7 +15,9 @@ const DepositsPage: NextPage = () => {
<Head> <Head>
<title>{ title }</title> <title>{ title }</title>
</Head> </Head>
<L2Deposits/> <Page>
<L2Deposits/>
</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 L2OutputRoots from 'ui/pages/L2OutputRoots'; import Page from 'ui/shared/Page/Page';
const L2OutputRoots = dynamic(() => import('ui/pages/L2OutputRoots'), { ssr: false });
const OutputRootsPage: NextPage = () => { const OutputRootsPage: NextPage = () => {
const title = getNetworkTitle(); const title = getNetworkTitle();
...@@ -12,7 +15,9 @@ const OutputRootsPage: NextPage = () => { ...@@ -12,7 +15,9 @@ const OutputRootsPage: NextPage = () => {
<Head> <Head>
<title>{ title }</title> <title>{ title }</title>
</Head> </Head>
<L2OutputRoots/> <Page>
<L2OutputRoots/>
</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 L2TxnBatches from 'ui/pages/L2TxnBatches'; import Page from 'ui/shared/Page/Page';
const L2TxnBatches = dynamic(() => import('ui/pages/L2TxnBatches'), { ssr: false });
const TxnBatchesPage: NextPage = () => { const TxnBatchesPage: NextPage = () => {
const title = getNetworkTitle(); const title = getNetworkTitle();
...@@ -12,7 +15,9 @@ const TxnBatchesPage: NextPage = () => { ...@@ -12,7 +15,9 @@ const TxnBatchesPage: NextPage = () => {
<Head> <Head>
<title>{ title }</title> <title>{ title }</title>
</Head> </Head>
<L2TxnBatches/> <Page>
<L2TxnBatches/>
</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 L2Withdrawals from 'ui/pages/L2Withdrawals'; import Page from 'ui/shared/Page/Page';
const L2Withdrawals = dynamic(() => import('ui/pages/L2Withdrawals'), { 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>
<L2Withdrawals/> <Page>
<L2Withdrawals/>
</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 SearchResults from 'ui/pages/SearchResults';
const SearchResults = dynamic(() => import('ui/pages/SearchResults'), { ssr: false });
const SearchResultsPage: NextPage = () => { const SearchResultsPage: NextPage = () => {
const title = getNetworkTitle(); const title = getNetworkTitle();
......
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 VerifiedContracts from 'ui/pages/VerifiedContracts';
import Page from 'ui/shared/Page/Page'; import Page from 'ui/shared/Page/Page';
const VerifiedContracts = dynamic(() => import('ui/pages/VerifiedContracts'), { ssr: false });
const VerifiedContractsPage: NextPage = () => { const VerifiedContractsPage: NextPage = () => {
const title = getNetworkTitle(); const title = getNetworkTitle();
......
import type { L2DepositsItem } from 'types/api/l2Deposits';
import type { L2OutputRootsItem } from 'types/api/l2OutputRoots';
import type { L2TxnBatchesItem } from 'types/api/l2TxnBatches';
import type { L2WithdrawalsItem } from 'types/api/l2Withdrawals';
import { ADDRESS_HASH, ADDRESS_PARAMS } from './addressParams';
import { TX_HASH } from './tx';
export const L2_DEPOSIT_ITEM: L2DepositsItem = {
l1_block_number: 9045233,
l1_block_timestamp: '2023-05-22T18:00:36.000000Z',
l1_tx_hash: TX_HASH,
l1_tx_origin: ADDRESS_HASH,
l2_tx_gas_limit: '100000',
l2_tx_hash: TX_HASH,
};
export const L2_WITHDRAWAL_ITEM: L2WithdrawalsItem = {
challenge_period_end: null,
from: ADDRESS_PARAMS,
l1_tx_hash: TX_HASH,
l2_timestamp: '2023-06-01T13:44:56.000000Z',
l2_tx_hash: TX_HASH,
msg_nonce: 2393,
msg_nonce_version: 1,
status: 'Ready to prove',
};
export const L2_TXN_BATCHES_ITEM: L2TxnBatchesItem = {
epoch_number: 9103513,
l1_timestamp: '2023-06-01T14:46:48.000000Z',
l1_tx_hashes: [
TX_HASH,
],
l2_block_number: 5218590,
tx_count: 9,
};
export const L2_OUTPUT_ROOTS_ITEM: L2OutputRootsItem = {
l1_block_number: 9103684,
l1_timestamp: '2023-06-01T15:26:12.000000Z',
l1_tx_hash: TX_HASH,
l2_block_number: 10102468,
l2_output_index: 50655,
output_root: TX_HASH,
};
import type { PublicTag, AddressTag, TransactionTag, ApiKey, CustomAbi } from 'types/api/account'; import type { PublicTag, AddressTag, TransactionTag, ApiKey, CustomAbi, VerifiedAddress, TokenInfoApplication } from 'types/api/account';
import type { TWatchlistItem } from 'types/client/account'; import type { TWatchlistItem } from 'types/client/account';
import { ADDRESS_PARAMS, ADDRESS_HASH } from './addressParams'; import { ADDRESS_PARAMS, ADDRESS_HASH } from './addressParams';
...@@ -79,3 +79,28 @@ export const CUSTOM_ABI: CustomAbi = { ...@@ -79,3 +79,28 @@ export const CUSTOM_ABI: CustomAbi = {
id: '1', id: '1',
name: 'placeholder', name: 'placeholder',
}; };
export const VERIFIED_ADDRESS: VerifiedAddress = {
userId: 'john.doe@gmail.com',
chainId: '5',
contractAddress: ADDRESS_HASH,
verifiedDate: '2022-11-11',
metadata: {
tokenName: 'Placeholder Token',
tokenSymbol: 'PLC',
},
};
export const TOKEN_INFO_APPLICATION: TokenInfoApplication = {
id: '1',
tokenAddress: ADDRESS_HASH,
status: 'IN_PROCESS',
updatedAt: '2022-11-11 13:49:48.031453Z',
requesterName: 'John Doe',
requesterEmail: 'john.doe@gmail.com',
projectWebsite: 'http://example.com',
projectEmail: 'info@example.com',
iconUrl: 'https://example.com/100/100',
projectDescription: 'Hello!',
projectSector: 'DeFi',
};
import type { SmartContract } from 'types/api/contract'; import type { SmartContract } from 'types/api/contract';
import type { VerifiedContract } from 'types/api/contracts';
import { ADDRESS_PARAMS } from './addressParams';
export const CONTRACT_CODE_UNVERIFIED = { export const CONTRACT_CODE_UNVERIFIED = {
creation_bytecode: '0x60806040526e', creation_bytecode: '0x60806040526e',
...@@ -38,3 +41,15 @@ export const CONTRACT_CODE_VERIFIED = { ...@@ -38,3 +41,15 @@ export const CONTRACT_CODE_VERIFIED = {
source_code: 'source_code', source_code: 'source_code',
verified_at: '2023-02-21T14:39:16.906760Z', verified_at: '2023-02-21T14:39:16.906760Z',
} as unknown as SmartContract; } as unknown as SmartContract;
export const VERIFIED_CONTRACT_INFO: VerifiedContract = {
address: { ...ADDRESS_PARAMS, name: 'StubContract' },
coin_balance: '30319033612988277',
compiler_version: 'v0.8.17+commit.8df45f5f',
has_constructor_args: true,
language: 'solidity',
market_cap: null,
optimization_enabled: false,
tx_count: 565058,
verified_at: '2023-04-10T13:16:33.884921Z',
};
/* eslint-disable max-len */
import type { MarketplaceAppOverview } from 'types/client/marketplace';
export const MARKETPLACE_APP: MarketplaceAppOverview = {
author: 'StubApp Inc.',
id: 'stub-app',
title: 'My cool app name',
logo: '',
categories: [
'Bridge',
],
shortDescription: 'Hop is a scalable rollup-to-rollup general token bridge. It allows users to send tokens from one rollup or sidechain to another almost immediately without having to wait for the networks challenge period.',
site: 'https://example.com',
description: 'Hop is a scalable rollup-to-rollup general token bridge. It allows users to send tokens from one rollup or sidechain to another almost immediately without having to wait for the networks challenge period.',
external: true,
url: 'https://example.com',
};
import type { SearchResult, SearchResultItem } from 'types/api/search';
import { ADDRESS_HASH } from './addressParams';
export const SEARCH_RESULT_ITEM: SearchResultItem = {
address: ADDRESS_HASH,
address_url: '/address/0x3714A8C7824B22271550894f7555f0a672f97809',
name: 'USDC',
symbol: 'USDC',
token_url: '/token/0x3714A8C7824B22271550894f7555f0a672f97809',
type: 'token',
};
export const SEARCH_RESULT_NEXT_PAGE_PARAMS: SearchResult['next_page_params'] = {
address_hash: ADDRESS_HASH,
block_hash: null,
holder_count: 11,
inserted_at: '2023-05-19T17:21:19.203681Z',
item_type: 'token',
items_count: 50,
name: 'USDCTest',
q: 'usd',
tx_hash: null,
};
import type { HomeStats, StatsChartsSection } from 'types/api/stats'; import type { Counter, HomeStats, StatsChartsSection } from 'types/api/stats';
export const HOMEPAGE_STATS: HomeStats = { export const HOMEPAGE_STATS: HomeStats = {
average_block_time: 14346, average_block_time: 14346,
...@@ -53,3 +53,10 @@ export const STATS_CHARTS_SECTION: StatsChartsSection = { ...@@ -53,3 +53,10 @@ export const STATS_CHARTS_SECTION: StatsChartsSection = {
export const STATS_CHARTS = { export const STATS_CHARTS = {
sections: [ STATS_CHARTS_SECTION ], sections: [ STATS_CHARTS_SECTION ],
}; };
export const STATS_COUNTER: Counter = {
id: 'stub',
value: '9074405',
title: 'Placeholder Counter',
units: '',
};
...@@ -14,5 +14,4 @@ export type L2DepositsResponse = { ...@@ -14,5 +14,4 @@ export type L2DepositsResponse = {
l1_block_number: number; l1_block_number: number;
tx_hash: string; tx_hash: string;
}; };
total: number;
} }
...@@ -9,7 +9,6 @@ export type L2OutputRootsItem = { ...@@ -9,7 +9,6 @@ export type L2OutputRootsItem = {
export type L2OutputRootsResponse = { export type L2OutputRootsResponse = {
items: Array<L2OutputRootsItem>; items: Array<L2OutputRootsItem>;
total: number;
next_page_params: { next_page_params: {
index: number; index: number;
items_count: number; items_count: number;
......
...@@ -8,9 +8,8 @@ export type L2TxnBatchesItem = { ...@@ -8,9 +8,8 @@ export type L2TxnBatchesItem = {
export type L2TxnBatchesResponse = { export type L2TxnBatchesResponse = {
items: Array<L2TxnBatchesItem>; items: Array<L2TxnBatchesItem>;
total: number;
next_page_params: { next_page_params: {
index: number; block_number: number;
items_count: number; items_count: number;
}; };
} }
...@@ -24,5 +24,4 @@ export type L2WithdrawalsResponse = { ...@@ -24,5 +24,4 @@ export type L2WithdrawalsResponse = {
'items_count': number; 'items_count': number;
'nonce': string; 'nonce': string;
}; };
total: number;
} }
...@@ -23,7 +23,7 @@ export type Counters = { ...@@ -23,7 +23,7 @@ export type Counters = {
counters: Array<Counter>; counters: Array<Counter>;
} }
type Counter = { export type Counter = {
id: string; id: string;
value: string; value: string;
title: string; title: string;
......
...@@ -131,9 +131,7 @@ const AddressBlocksValidated = ({ scrollRef }: Props) => { ...@@ -131,9 +131,7 @@ const AddressBlocksValidated = ({ scrollRef }: Props) => {
return ( return (
<DataListDisplay <DataListDisplay
isError={ query.isError } isError={ query.isError }
isLoading={ false }
items={ query.data?.items } items={ query.data?.items }
skeletonProps={{ isLongSkeleton: true, skeletonDesktopColumns: [ '17%', '17%', '16%', '25%', '25%' ] }}
emptyText="There are no validated blocks for this address." emptyText="There are no validated blocks for this address."
content={ content } content={ content }
actionBar={ actionBar } actionBar={ actionBar }
......
...@@ -82,9 +82,7 @@ const AddressInternalTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivE ...@@ -82,9 +82,7 @@ const AddressInternalTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivE
return ( return (
<DataListDisplay <DataListDisplay
isError={ isError } isError={ isError }
isLoading={ false }
items={ data?.items } items={ data?.items }
skeletonProps={{ isLongSkeleton: true, skeletonDesktopColumns: [ '15%', '15%', '10%', '20%', '20%', '20%' ] }}
filterProps={{ emptyFilteredText: `Couldn${ apos }t find any transaction that matches your query.`, hasActiveFilters: Boolean(filterValue) }} filterProps={{ emptyFilteredText: `Couldn${ apos }t find any transaction that matches your query.`, hasActiveFilters: Boolean(filterValue) }}
emptyText="There are no internal transactions for this address." emptyText="There are no internal transactions for this address."
content={ content } content={ content }
......
...@@ -39,12 +39,10 @@ const AddressLogs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement> ...@@ -39,12 +39,10 @@ const AddressLogs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>
return ( return (
<DataListDisplay <DataListDisplay
isError={ isError } isError={ isError }
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: null }}
/> />
); );
}; };
......
...@@ -286,12 +286,7 @@ const AddressTokenTransfers = ({ scrollRef, overloadCount = OVERLOAD_COUNT }: Pr ...@@ -286,12 +286,7 @@ const AddressTokenTransfers = ({ scrollRef, overloadCount = OVERLOAD_COUNT }: Pr
return ( return (
<DataListDisplay <DataListDisplay
isError={ isError } isError={ isError }
isLoading={ false }
items={ data?.items } items={ data?.items }
skeletonProps={{
isLongSkeleton: true,
skeletonDesktopColumns: [ '44px', '185px', '160px', '25%', '25%', '25%', '25%' ],
}}
emptyText="There are no token transfers." emptyText="There are no token transfers."
filterProps={{ filterProps={{
emptyFilteredText: `Couldn${ apos }t find any token transfer that matches your query.`, emptyFilteredText: `Couldn${ apos }t find any token transfer that matches your query.`,
......
...@@ -184,7 +184,6 @@ const AddressTxs = ({ scrollRef, overloadCount = OVERLOAD_COUNT }: Props) => { ...@@ -184,7 +184,6 @@ const AddressTxs = ({ scrollRef, overloadCount = OVERLOAD_COUNT }: Props) => {
socketInfoAlert={ socketAlert } socketInfoAlert={ socketAlert }
socketInfoNum={ newItemsCount } socketInfoNum={ newItemsCount }
top={ 80 } top={ 80 }
hasLongSkeleton
/> />
</> </>
); );
......
...@@ -55,9 +55,7 @@ const AddressWithdrawals = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivE ...@@ -55,9 +55,7 @@ const AddressWithdrawals = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivE
return ( return (
<DataListDisplay <DataListDisplay
isError={ isError } isError={ isError }
isLoading={ false }
items={ data?.items } items={ data?.items }
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."
content={ content } content={ content }
actionBar={ actionBar } actionBar={ actionBar }
......
...@@ -71,9 +71,7 @@ const AddressCoinBalanceHistory = ({ query }: Props) => { ...@@ -71,9 +71,7 @@ const AddressCoinBalanceHistory = ({ query }: Props) => {
<DataListDisplay <DataListDisplay
mt={ 8 } mt={ 8 }
isError={ query.isError } isError={ query.isError }
isLoading={ false }
items={ query.data?.items } items={ query.data?.items }
skeletonProps={{ skeletonDesktopColumns: [ '25%', '25%', '25%', '25%', '120px' ] }}
emptyText="There is no coin balance history for this address." emptyText="There is no coin balance history for this address."
content={ content } content={ content }
actionBar={ actionBar } actionBar={ actionBar }
......
...@@ -54,12 +54,10 @@ const ERC1155Tokens = ({ tokensQuery }: Props) => { ...@@ -54,12 +54,10 @@ const ERC1155Tokens = ({ tokensQuery }: Props) => {
return ( return (
<DataListDisplay <DataListDisplay
isError={ isError } isError={ isError }
isLoading={ false }
items={ data?.items } items={ data?.items }
emptyText="There are no tokens of selected type." emptyText="There are no tokens of selected type."
content={ content } content={ content }
actionBar={ actionBar } actionBar={ actionBar }
skeletonProps={{ customSkeleton: null }}
/> />
); );
}; };
......
...@@ -46,12 +46,7 @@ const ERC20Tokens = ({ tokensQuery }: Props) => { ...@@ -46,12 +46,7 @@ const ERC20Tokens = ({ tokensQuery }: Props) => {
return ( return (
<DataListDisplay <DataListDisplay
isError={ isError } isError={ isError }
isLoading={ false }
items={ data?.items } items={ data?.items }
skeletonProps={{
isLongSkeleton: true,
skeletonDesktopColumns: [ '30%', '30%', '10%', '20%', '10%' ],
}}
emptyText="There are no tokens of selected type." emptyText="There are no tokens of selected type."
content={ content } content={ content }
actionBar={ actionBar } actionBar={ actionBar }
......
...@@ -46,12 +46,7 @@ const ERC721Tokens = ({ tokensQuery }: Props) => { ...@@ -46,12 +46,7 @@ const ERC721Tokens = ({ tokensQuery }: Props) => {
return ( return (
<DataListDisplay <DataListDisplay
isError={ isError } isError={ isError }
isLoading={ false }
items={ data?.items } items={ data?.items }
skeletonProps={{
isLongSkeleton: true,
skeletonDesktopColumns: [ '40%', '40%', '20%' ],
}}
emptyText="There are no tokens of selected type." emptyText="There are no tokens of selected type."
content={ content } content={ content }
actionBar={ actionBar } actionBar={ actionBar }
......
...@@ -45,9 +45,7 @@ const BlockWithdrawals = ({ blockWithdrawalsQuery }: Props) => { ...@@ -45,9 +45,7 @@ const BlockWithdrawals = ({ blockWithdrawalsQuery }: Props) => {
return ( return (
<DataListDisplay <DataListDisplay
isError={ blockWithdrawalsQuery.isError } isError={ blockWithdrawalsQuery.isError }
isLoading={ false }
items={ blockWithdrawalsQuery.data?.items } items={ blockWithdrawalsQuery.data?.items }
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."
content={ content } content={ content }
/> />
......
...@@ -95,9 +95,7 @@ const BlocksContent = ({ type, query }: Props) => { ...@@ -95,9 +95,7 @@ const BlocksContent = ({ type, query }: Props) => {
return ( return (
<DataListDisplay <DataListDisplay
isError={ query.isError } isError={ query.isError }
isLoading={ false }
items={ query.data?.items } items={ query.data?.items }
skeletonProps={{ skeletonDesktopColumns: [ '125px', '120px', '21%', '64px', '35%', '22%', '22%' ] }}
emptyText="There are no blocks." emptyText="There are no blocks."
content={ content } content={ content }
actionBar={ actionBar } actionBar={ actionBar }
......
...@@ -13,10 +13,11 @@ import useIsMobile from 'lib/hooks/useIsMobile'; ...@@ -13,10 +13,11 @@ import useIsMobile from 'lib/hooks/useIsMobile';
import { nbsp } from 'lib/html-entities'; import { nbsp } from 'lib/html-entities';
import useSocketChannel from 'lib/socket/useSocketChannel'; import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage'; import useSocketMessage from 'lib/socket/useSocketMessage';
import { BLOCK } from 'stubs/block';
import { HOMEPAGE_STATS } from 'stubs/stats';
import LinkInternal from 'ui/shared/LinkInternal'; import LinkInternal from 'ui/shared/LinkInternal';
import LatestBlocksItem from './LatestBlocksItem'; import LatestBlocksItem from './LatestBlocksItem';
import LatestBlocksItemSkeleton from './LatestBlocksItemSkeleton';
const BLOCK_HEIGHT_L1 = 166; const BLOCK_HEIGHT_L1 = 166;
const BLOCK_HEIGHT_L2 = 112; const BLOCK_HEIGHT_L2 = 112;
...@@ -32,10 +33,18 @@ const LatestBlocks = () => { ...@@ -32,10 +33,18 @@ const LatestBlocks = () => {
} else { } else {
blocksMaxCount = isMobile ? 2 : 3; blocksMaxCount = isMobile ? 2 : 3;
} }
const { data, isLoading, isError } = useApiQuery('homepage_blocks'); const { data, isPlaceholderData, isError } = useApiQuery('homepage_blocks', {
queryOptions: {
placeholderData: Array(4).fill(BLOCK),
},
});
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const statsQueryResult = useApiQuery('homepage_stats'); const statsQueryResult = useApiQuery('homepage_stats', {
queryOptions: {
placeholderData: HOMEPAGE_STATS,
},
});
const handleNewBlockMessage: SocketMessage.NewBlock['handler'] = React.useCallback((payload) => { const handleNewBlockMessage: SocketMessage.NewBlock['handler'] = React.useCallback((payload) => {
queryClient.setQueryData(getResourceKey('homepage_blocks'), (prevData: Array<Block> | undefined) => { queryClient.setQueryData(getResourceKey('homepage_blocks'), (prevData: Array<Block> | undefined) => {
...@@ -52,7 +61,7 @@ const LatestBlocks = () => { ...@@ -52,7 +61,7 @@ const LatestBlocks = () => {
const channel = useSocketChannel({ const channel = useSocketChannel({
topic: 'blocks:new_block', topic: 'blocks:new_block',
isDisabled: isLoading || isError, isDisabled: isPlaceholderData || isError,
}); });
useSocketMessage({ useSocketMessage({
channel, channel,
...@@ -62,22 +71,6 @@ const LatestBlocks = () => { ...@@ -62,22 +71,6 @@ const LatestBlocks = () => {
let content; let content;
if (isLoading) {
content = (
<>
<Skeleton w="100%" h={ 6 } mb={ 9 }/>
<VStack
spacing={ `${ BLOCK_MARGIN }px` }
mb={ 6 }
height={ `${ blockHeight * blocksMaxCount + BLOCK_MARGIN * (blocksMaxCount - 1) }px` }
overflow="hidden"
>
{ Array.from(Array(blocksMaxCount)).map((item, index) => <LatestBlocksItemSkeleton key={ index }/>) }
</VStack>
</>
);
}
if (isError) { if (isError) {
content = <Text>No data. Please reload page.</Text>; content = <Text>No data. Please reload page.</Text>;
} }
...@@ -88,22 +81,26 @@ const LatestBlocks = () => { ...@@ -88,22 +81,26 @@ const LatestBlocks = () => {
content = ( content = (
<> <>
{ statsQueryResult.isLoading && (
<Skeleton h="24px" w="170px" mb={{ base: 6, lg: 9 }}/>
) }
{ statsQueryResult.data?.network_utilization_percentage !== undefined && ( { statsQueryResult.data?.network_utilization_percentage !== undefined && (
<Box mb={{ base: 6, lg: 3 }}> <Skeleton isLoaded={ !statsQueryResult.isPlaceholderData } mb={{ base: 6, lg: 3 }} display="inline-block">
<Text as="span" fontSize="sm"> <Text as="span" fontSize="sm">
Network utilization:{ nbsp } Network utilization:{ nbsp }
</Text> </Text>
<Text as="span" fontSize="sm" color="blue.400" fontWeight={ 700 }> <Text as="span" fontSize="sm" color="blue.400" fontWeight={ 700 }>
{ statsQueryResult.data?.network_utilization_percentage.toFixed(2) }% { statsQueryResult.data?.network_utilization_percentage.toFixed(2) }%
</Text> </Text>
</Box> </Skeleton>
) } ) }
<VStack spacing={ `${ BLOCK_MARGIN }px` } mb={ 4 } height={ `${ blockHeight * blocksCount + BLOCK_MARGIN * (blocksCount - 1) }px` } overflow="hidden"> <VStack spacing={ `${ BLOCK_MARGIN }px` } mb={ 4 } height={ `${ blockHeight * blocksCount + BLOCK_MARGIN * (blocksCount - 1) }px` } overflow="hidden">
<AnimatePresence initial={ false } > <AnimatePresence initial={ false } >
{ dataToShow.map((block => <LatestBlocksItem key={ block.height } block={ block } h={ blockHeight }/>)) } { dataToShow.map(((block, index) => (
<LatestBlocksItem
key={ block.height + (isPlaceholderData ? String(index) : '') }
block={ block }
h={ blockHeight }
isLoading={ isPlaceholderData }
/>
))) }
</AnimatePresence> </AnimatePresence>
</VStack> </VStack>
<Flex justifyContent="center"> <Flex justifyContent="center">
......
...@@ -2,10 +2,8 @@ import { ...@@ -2,10 +2,8 @@ import {
Box, Box,
Flex, Flex,
Grid, Grid,
GridItem,
HStack, HStack,
Icon, Skeleton,
Text,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
...@@ -18,14 +16,16 @@ import blockIcon from 'icons/block.svg'; ...@@ -18,14 +16,16 @@ import blockIcon from 'icons/block.svg';
import getBlockTotalReward from 'lib/block/getBlockTotalReward'; import getBlockTotalReward from 'lib/block/getBlockTotalReward';
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 LinkInternal from 'ui/shared/LinkInternal'; import LinkInternal from 'ui/shared/LinkInternal';
type Props = { type Props = {
block: Block; block: Block;
h: number; h: number;
isLoading?: boolean;
} }
const LatestBlocksItem = ({ block, h }: Props) => { const LatestBlocksItem = ({ block, h, isLoading }: Props) => {
const totalReward = getBlockTotalReward(block); const totalReward = getBlockTotalReward(block);
return ( return (
<Box <Box
...@@ -43,26 +43,29 @@ const LatestBlocksItem = ({ block, h }: Props) => { ...@@ -43,26 +43,29 @@ const LatestBlocksItem = ({ block, h }: Props) => {
> >
<Flex justifyContent="space-between" alignItems="center" mb={ 3 }> <Flex justifyContent="space-between" alignItems="center" mb={ 3 }>
<HStack spacing={ 2 }> <HStack spacing={ 2 }>
<Icon as={ blockIcon } boxSize="30px" color="link"/> <Icon as={ blockIcon } boxSize="30px" color="link" isLoading={ isLoading } borderRadius="base"/>
<LinkInternal <LinkInternal
href={ route({ pathname: '/block/[height_or_hash]', query: { height_or_hash: String(block.height) } }) } href={ route({ pathname: '/block/[height_or_hash]', query: { height_or_hash: String(block.height) } }) }
fontSize="xl" fontSize="xl"
fontWeight="500" fontWeight="500"
isLoading={ isLoading }
> >
{ block.height } <Skeleton isLoaded={ !isLoading }>
{ block.height }
</Skeleton>
</LinkInternal> </LinkInternal>
</HStack> </HStack>
<BlockTimestamp ts={ block.timestamp } isEnabled fontSize="sm"/> <BlockTimestamp ts={ block.timestamp } isEnabled={ !isLoading } isLoading={ isLoading } fontSize="sm"/>
</Flex> </Flex>
<Grid gridGap={ 2 } templateColumns="auto minmax(0, 1fr)" fontSize="sm"> <Grid gridGap={ 2 } templateColumns="auto minmax(0, 1fr)" fontSize="sm">
<GridItem>Txn</GridItem> <Skeleton isLoaded={ !isLoading }>Txn</Skeleton>
<GridItem><Text variant="secondary">{ block.tx_count }</Text></GridItem> <Skeleton isLoaded={ !isLoading } color="text_secondary"><span>{ block.tx_count }</span></Skeleton>
{ !appConfig.L2.isL2Network && ( { !appConfig.L2.isL2Network && (
<> <>
<GridItem>Reward</GridItem> <Skeleton isLoaded={ !isLoading }>Reward</Skeleton>
<GridItem><Text variant="secondary">{ totalReward.toFixed() }</Text></GridItem> <Skeleton isLoaded={ !isLoading } color="text_secondary"><span>{ totalReward.toFixed() }</span></Skeleton>
<GridItem>Miner</GridItem> <Skeleton isLoaded={ !isLoading }>Miner</Skeleton>
<GridItem><AddressLink type="address" alias={ block.miner.name } hash={ block.miner.hash } truncation="constant" maxW="100%"/></GridItem> <AddressLink type="address" alias={ block.miner.name } hash={ block.miner.hash } truncation="constant" maxW="100%" isLoading={ isLoading }/>
</> </>
) } ) }
</Grid> </Grid>
......
import {
Box,
Flex,
Grid,
GridItem,
HStack,
Skeleton,
} from '@chakra-ui/react';
import React from 'react';
import appConfig from 'configs/app/config';
const LatestBlocksItemSkeleton = () => {
return (
<Box
minWidth={{ base: '100%', lg: '280px' }}
borderRadius="12px"
border="1px solid"
borderColor="divider"
p={ 6 }
>
<Flex justifyContent="space-between" alignItems="center" mb={ 3 }>
<HStack spacing={ 2 }>
<Skeleton w="30px" h="30px"/>
<Skeleton w="93px" h="15px"/>
</HStack>
<Skeleton w="44px" h="15px"/>
</Flex>
<Grid gridGap={ 2 } templateColumns="auto minmax(0, 1fr)" fontSize="sm">
<GridItem><Skeleton w="30px" h="15px"/></GridItem>
<GridItem><Skeleton w="93px" h="15px"/></GridItem>
{ !appConfig.L2.isL2Network && (
<>
<GridItem><Skeleton w="30px" h="15px"/></GridItem>
<GridItem><Skeleton w="93px" h="15px"/></GridItem>
<GridItem><Skeleton w="30px" h="15px"/></GridItem>
<GridItem><Skeleton w="93px" h="15px"/></GridItem>
</>
) }
</Grid>
</Box>
);
};
export default LatestBlocksItemSkeleton;
import { Box, Flex, Text, Skeleton } from '@chakra-ui/react'; import { Box, Flex, Text } from '@chakra-ui/react';
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
...@@ -9,16 +9,20 @@ import useGradualIncrement from 'lib/hooks/useGradualIncrement'; ...@@ -9,16 +9,20 @@ import useGradualIncrement from 'lib/hooks/useGradualIncrement';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
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 { L2_DEPOSIT_ITEM } from 'stubs/L2';
import LinkInternal from 'ui/shared/LinkInternal'; import LinkInternal from 'ui/shared/LinkInternal';
import SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice'; import SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice';
import LatestDepositsItem from './LatestDepositsItem'; import LatestDepositsItem from './LatestDepositsItem';
import LatestDepositsItemSkeleton from './LatestDepositsItemSkeleton';
const LatestDeposits = () => { const LatestDeposits = () => {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const itemsCount = isMobile ? 2 : 6; const itemsCount = isMobile ? 2 : 6;
const { data, isLoading, isError } = useApiQuery('homepage_deposits'); const { data, isPlaceholderData, isError } = useApiQuery('homepage_deposits', {
queryOptions: {
placeholderData: Array(itemsCount).fill(L2_DEPOSIT_ITEM),
},
});
const [ num, setNum ] = useGradualIncrement(0); const [ num, setNum ] = useGradualIncrement(0);
const [ socketAlert, setSocketAlert ] = React.useState(''); const [ socketAlert, setSocketAlert ] = React.useState('');
...@@ -48,15 +52,6 @@ const LatestDeposits = () => { ...@@ -48,15 +52,6 @@ const LatestDeposits = () => {
handler: handleNewDepositMessage, handler: handleNewDepositMessage,
}); });
if (isLoading) {
return (
<>
<Skeleton h="32px" w="100%" borderBottomLeftRadius={ 0 } borderBottomRightRadius={ 0 }/>
{ Array.from(Array(itemsCount)).map((item, index) => <LatestDepositsItemSkeleton key={ index }/>) }
</>
);
}
if (isError) { if (isError) {
return <Text mt={ 4 }>No data. Please reload page.</Text>; return <Text mt={ 4 }>No data. Please reload page.</Text>;
} }
...@@ -65,9 +60,15 @@ const LatestDeposits = () => { ...@@ -65,9 +60,15 @@ const LatestDeposits = () => {
const depositsUrl = route({ pathname: '/l2-deposits' }); const depositsUrl = route({ pathname: '/l2-deposits' });
return ( return (
<> <>
<SocketNewItemsNotice borderBottomRadius={ 0 } url={ depositsUrl } num={ num } alert={ socketAlert } type="deposit"/> <SocketNewItemsNotice borderBottomRadius={ 0 } url={ depositsUrl } num={ num } alert={ socketAlert } type="deposit" isLoading={ isPlaceholderData }/>
<Box mb={{ base: 3, lg: 4 }}> <Box mb={{ base: 3, lg: 4 }}>
{ data.slice(0, itemsCount).map((item => <LatestDepositsItem key={ item.l2_tx_hash } item={ item }/>)) } { data.slice(0, itemsCount).map(((item, index) => (
<LatestDepositsItem
key={ item.l2_tx_hash + (isPlaceholderData ? index : '') }
item={ item }
isLoading={ isPlaceholderData }
/>
))) }
</Box> </Box>
<Flex justifyContent="center"> <Flex justifyContent="center">
<LinkInternal fontSize="sm" href={ depositsUrl }>View all deposits</LinkInternal> <LinkInternal fontSize="sm" href={ depositsUrl }>View all deposits</LinkInternal>
......
...@@ -2,8 +2,7 @@ import { ...@@ -2,8 +2,7 @@ import {
Box, Box,
Flex, Flex,
Grid, Grid,
Icon, Skeleton,
Text,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
...@@ -15,15 +14,17 @@ import blockIcon from 'icons/block.svg'; ...@@ -15,15 +14,17 @@ import blockIcon from 'icons/block.svg';
import txIcon from 'icons/transactions.svg'; import txIcon from 'icons/transactions.svg';
import dayjs from 'lib/date/dayjs'; import dayjs from 'lib/date/dayjs';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import Icon from 'ui/shared/chakra/Icon';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic'; import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import LinkExternal from 'ui/shared/LinkExternal'; import LinkExternal from 'ui/shared/LinkExternal';
import LinkInternal from 'ui/shared/LinkInternal'; import LinkInternal from 'ui/shared/LinkInternal';
type Props = { type Props = {
item: L2DepositsItem; item: L2DepositsItem;
isLoading?: boolean;
} }
const LatestTxsItem = ({ item }: Props) => { const LatestTxsItem = ({ item, isLoading }: Props) => {
const timeAgo = dayjs(item.l1_block_timestamp).fromNow(); const timeAgo = dayjs(item.l1_block_timestamp).fromNow();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
...@@ -35,9 +36,10 @@ const LatestTxsItem = ({ item }: Props) => { ...@@ -35,9 +36,10 @@ const LatestTxsItem = ({ item }: Props) => {
fontWeight={ 700 } fontWeight={ 700 }
display="inline-flex" display="inline-flex"
mr={ 2 } mr={ 2 }
isLoading={ isLoading }
> >
<Icon as={ blockIcon } boxSize="30px" mr={ 1 }/> <Icon as={ blockIcon } boxSize="30px" isLoading={ isLoading } borderRadius="base"/>
{ item.l1_block_number } <Skeleton isLoaded={ !isLoading } ml={ 1 }>{ item.l1_block_number }</Skeleton>
</LinkExternal> </LinkExternal>
); );
...@@ -48,9 +50,13 @@ const LatestTxsItem = ({ item }: Props) => { ...@@ -48,9 +50,13 @@ const LatestTxsItem = ({ item }: Props) => {
display="inline-flex" display="inline-flex"
alignItems="center" alignItems="center"
overflow="hidden" overflow="hidden"
isLoading={ isLoading }
my="3px"
> >
<Icon as={ txIcon } boxSize={ 6 } mr={ 1 }/> <Icon as={ txIcon } boxSize={ 6 } isLoading={ isLoading }/>
<Box overflow="hidden" whiteSpace="nowrap"><HashStringShortenDynamic hash={ item.l1_tx_hash }/></Box> <Skeleton isLoaded={ !isLoading } overflow="hidden" whiteSpace="nowrap" ml={ 1 }>
<HashStringShortenDynamic hash={ item.l1_tx_hash }/>
</Skeleton>
</LinkExternal> </LinkExternal>
); );
...@@ -61,9 +67,12 @@ const LatestTxsItem = ({ item }: Props) => { ...@@ -61,9 +67,12 @@ const LatestTxsItem = ({ item }: Props) => {
alignItems="center" alignItems="center"
overflow="hidden" overflow="hidden"
w="100%" w="100%"
isLoading={ isLoading }
> >
<Icon as={ txIcon } boxSize={ 6 } mr={ 1 }/> <Icon as={ txIcon } boxSize={ 6 } isLoading={ isLoading }/>
<Box w="calc(100% - 36px)" overflow="hidden" whiteSpace="nowrap"><HashStringShortenDynamic hash={ item.l2_tx_hash }/></Box> <Skeleton isLoaded={ !isLoading } w="calc(100% - 36px)" overflow="hidden" whiteSpace="nowrap" ml={ 1 }>
<HashStringShortenDynamic hash={ item.l2_tx_hash }/>
</Skeleton>
</LinkInternal> </LinkInternal>
); );
...@@ -73,28 +82,42 @@ const LatestTxsItem = ({ item }: Props) => { ...@@ -73,28 +82,42 @@ const LatestTxsItem = ({ item }: Props) => {
<> <>
<Flex justifyContent="space-between" alignItems="center" mb={ 1 }> <Flex justifyContent="space-between" alignItems="center" mb={ 1 }>
{ l1BlockLink } { l1BlockLink }
<Text variant="secondary">{ timeAgo }</Text> <Skeleton isLoaded={ !isLoading } color="text_secondary">
<span>{ timeAgo }</span>
</Skeleton>
</Flex> </Flex>
<Grid gridTemplateColumns="56px auto"> <Grid gridTemplateColumns="56px auto">
<Text lineHeight="30px">L1 txn</Text> <Skeleton isLoaded={ !isLoading } my="5px" w="fit-content">
L1 txn
</Skeleton>
{ l1TxLink } { l1TxLink }
<Text lineHeight="30px">L2 txn</Text> <Skeleton isLoaded={ !isLoading } my="3px" w="fit-content">
L2 txn
</Skeleton>
{ l2TxLink } { l2TxLink }
</Grid> </Grid>
</> </>
); );
} }
return ( return (
<Grid width="100%" columnGap={ 4 } rowGap={ 2 } templateColumns="max-content max-content auto" w="100%"> <Grid width="100%" columnGap={ 4 } rowGap={ 2 } templateColumns="max-content max-content auto" w="100%">
{ l1BlockLink } { l1BlockLink }
<Text lineHeight="30px">L1 txn</Text> <Skeleton isLoaded={ !isLoading } w="fit-content" h="fit-content" my="5px">
L1 txn
</Skeleton>
{ l1TxLink } { l1TxLink }
<Text variant="secondary">{ timeAgo }</Text> <Skeleton isLoaded={ !isLoading } color="text_secondary" w="fit-content" h="fit-content" my="2px">
<Text lineHeight="30px">L2 txn</Text> <span>{ timeAgo }</span>
</Skeleton>
<Skeleton isLoaded={ !isLoading } w="fit-content" h="fit-content" my="2px">
L2 txn
</Skeleton>
{ l2TxLink } { l2TxLink }
</Grid> </Grid>
); );
})(); })();
return ( return (
<Box <Box
width="100%" width="100%"
...@@ -104,6 +127,7 @@ const LatestTxsItem = ({ item }: Props) => { ...@@ -104,6 +127,7 @@ const LatestTxsItem = ({ item }: Props) => {
px={{ base: 0, lg: 4 }} px={{ base: 0, lg: 4 }}
_last={{ borderBottom: '1px solid', borderColor: 'divider' }} _last={{ borderBottom: '1px solid', borderColor: 'divider' }}
fontSize="sm" fontSize="sm"
lineHeight={ 5 }
> >
{ content } { content }
</Box> </Box>
......
import {
Box,
Flex,
Skeleton,
} from '@chakra-ui/react';
import React from 'react';
import useIsMobile from 'lib/hooks/useIsMobile';
const LatestTxsItemSkeleton = () => {
const isMobile = useIsMobile();
return (
<Box
width="100%"
borderTop="1px solid"
borderColor="divider"
py={ 4 }
px={{ base: 0, lg: 4 }}
_last={{ borderBottom: '1px solid', borderColor: 'divider' }}
>
{ isMobile && (
<>
<Flex justifyContent="space-between" alignItems="center" mt={ 1 } mb={ 4 }>
<Skeleton w="120px" h="20px"></Skeleton>
<Skeleton w="80px" h="20px"></Skeleton>
</Flex>
<Skeleton w="100%" h="20px" mb={ 2 }></Skeleton>
<Skeleton w="100%" h="20px" mb={ 2 }></Skeleton>
</>
) }
{ !isMobile && (
<>
<Flex w="100%" mb={ 2 } h="30px" alignItems="center" justifyContent="space-between">
<Skeleton w="120px" h="20px"></Skeleton>
<Skeleton w="calc(100% - 120px - 48px)" h="20px"></Skeleton>
</Flex><Flex w="100%" h="30px" alignItems="center" justifyContent="space-between">
<Skeleton w="120px" h="20px"></Skeleton>
<Skeleton w="calc(100% - 120px - 48px)" h="20px"></Skeleton>
</Flex>
</>
) }
</Box>
);
};
export default LatestTxsItemSkeleton;
import { Box, Flex, Text, Skeleton } from '@chakra-ui/react'; import { Box, Flex, Text } from '@chakra-ui/react';
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import useNewTxsSocket from 'lib/hooks/useNewTxsSocket'; import useNewTxsSocket from 'lib/hooks/useNewTxsSocket';
import { TX } from 'stubs/tx';
import LinkInternal from 'ui/shared/LinkInternal'; import LinkInternal from 'ui/shared/LinkInternal';
import SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice'; import SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice';
import LatestTxsItem from './LatestTxsItem'; import LatestTxsItem from './LatestTxsItem';
import LatestTxsItemSkeleton from './LatestTxsItemSkeleton';
const LatestTransactions = () => { const LatestTransactions = () => {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const txsCount = isMobile ? 2 : 6; const txsCount = isMobile ? 2 : 6;
const { data, isLoading, isError } = useApiQuery('homepage_txs'); const { data, isPlaceholderData, isError } = useApiQuery('homepage_txs', {
queryOptions: {
placeholderData: Array(txsCount).fill(TX),
},
});
const { num, socketAlert } = useNewTxsSocket(); const { num, socketAlert } = useNewTxsSocket();
if (isLoading) {
return (
<>
<Skeleton h="32px" w="100%" borderBottomLeftRadius={ 0 } borderBottomRightRadius={ 0 }/>
{ Array.from(Array(txsCount)).map((item, index) => <LatestTxsItemSkeleton key={ index }/>) }
</>
);
}
if (isError) { if (isError) {
return <Text mt={ 4 }>No data. Please reload page.</Text>; return <Text mt={ 4 }>No data. Please reload page.</Text>;
} }
...@@ -35,9 +30,15 @@ const LatestTransactions = () => { ...@@ -35,9 +30,15 @@ const LatestTransactions = () => {
const txsUrl = route({ pathname: '/txs' }); const txsUrl = route({ pathname: '/txs' });
return ( return (
<> <>
<SocketNewItemsNotice borderBottomRadius={ 0 } url={ txsUrl } num={ num } alert={ socketAlert }/> <SocketNewItemsNotice borderBottomRadius={ 0 } url={ txsUrl } num={ num } alert={ socketAlert } isLoading={ isPlaceholderData }/>
<Box mb={{ base: 3, lg: 4 }}> <Box mb={{ base: 3, lg: 4 }}>
{ data.slice(0, txsCount).map((tx => <LatestTxsItem key={ tx.hash } tx={ tx }/>)) } { data.slice(0, txsCount).map(((tx, index) => (
<LatestTxsItem
key={ tx.hash + (isPlaceholderData ? index : '') }
tx={ tx }
isLoading={ isPlaceholderData }
/>
))) }
</Box> </Box>
<Flex justifyContent="center"> <Flex justifyContent="center">
<LinkInternal fontSize="sm" href={ txsUrl }>View all transactions</LinkInternal> <LinkInternal fontSize="sm" href={ txsUrl }>View all transactions</LinkInternal>
......
...@@ -2,9 +2,9 @@ import { ...@@ -2,9 +2,9 @@ import {
Box, Box,
Flex, Flex,
HStack, HStack,
Icon,
Text, Text,
Grid, Grid,
Skeleton,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
...@@ -19,15 +19,17 @@ import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement'; ...@@ -19,15 +19,17 @@ 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 TxStatus from 'ui/shared/TxStatus'; import TxStatus from 'ui/shared/TxStatus';
import TxAdditionalInfo from 'ui/txs/TxAdditionalInfo'; import TxAdditionalInfo from 'ui/txs/TxAdditionalInfo';
import TxType from 'ui/txs/TxType'; import TxType from 'ui/txs/TxType';
type Props = { type Props = {
tx: Transaction; tx: Transaction;
isLoading?: boolean;
} }
const LatestTxsItem = ({ tx }: Props) => { const LatestTxsItem = ({ tx, isLoading }: Props) => {
const dataTo = tx.to ? tx.to : tx.created_contract; const dataTo = tx.to ? tx.to : tx.created_contract;
const timeAgo = useTimeAgoIncrement(tx.timestamp || '0', true); const timeAgo = useTimeAgoIncrement(tx.timestamp || '0', true);
...@@ -44,10 +46,10 @@ const LatestTxsItem = ({ tx }: Props) => { ...@@ -44,10 +46,10 @@ const LatestTxsItem = ({ tx }: Props) => {
> >
<Flex justifyContent="space-between"> <Flex justifyContent="space-between">
<HStack> <HStack>
<TxType types={ tx.tx_types }/> <TxType types={ tx.tx_types } isLoading={ isLoading }/>
<TxStatus status={ tx.status } errorText={ tx.status === 'error' ? tx.result : undefined }/> <TxStatus status={ tx.status } errorText={ tx.status === 'error' ? tx.result : undefined } isLoading={ isLoading }/>
</HStack> </HStack>
<TxAdditionalInfo tx={ tx } isMobile/> <TxAdditionalInfo tx={ tx } isMobile isLoading={ isLoading }/>
</Flex> </Flex>
<Flex <Flex
mt={ 2 } mt={ 2 }
...@@ -62,6 +64,7 @@ const LatestTxsItem = ({ tx }: Props) => { ...@@ -62,6 +64,7 @@ const LatestTxsItem = ({ tx }: Props) => {
boxSize="30px" boxSize="30px"
mr={ 2 } mr={ 2 }
color="link" color="link"
isLoading={ isLoading }
/> />
<Address width="100%"> <Address width="100%">
<AddressLink <AddressLink
...@@ -69,14 +72,19 @@ const LatestTxsItem = ({ tx }: Props) => { ...@@ -69,14 +72,19 @@ const LatestTxsItem = ({ tx }: Props) => {
type="transaction" type="transaction"
fontWeight="700" fontWeight="700"
truncation="constant" truncation="constant"
isLoading={ isLoading }
/> />
</Address> </Address>
</Flex> </Flex>
{ tx.timestamp && <Text variant="secondary" fontWeight="400" fontSize="sm">{ timeAgo }</Text> } { tx.timestamp && (
<Skeleton isLoaded={ !isLoading } color="text_secondary" fontWeight="400" fontSize="sm">
<span>{ timeAgo }</span>
</Skeleton>
) }
</Flex> </Flex>
<Flex alignItems="center" mb={ 3 }> <Flex alignItems="center" mb={ 3 }>
<Address> <Address mr={ 2 }>
<AddressIcon address={ tx.from }/> <AddressIcon address={ tx.from } isLoading={ isLoading }/>
<AddressLink <AddressLink
type="address" type="address"
hash={ tx.from.hash } hash={ tx.from.hash }
...@@ -85,17 +93,18 @@ const LatestTxsItem = ({ tx }: Props) => { ...@@ -85,17 +93,18 @@ const LatestTxsItem = ({ tx }: Props) => {
ml={ 2 } ml={ 2 }
truncation="constant" truncation="constant"
fontSize="sm" fontSize="sm"
isLoading={ isLoading }
/> />
</Address> </Address>
<Icon <Icon
as={ rightArrowIcon } as={ rightArrowIcon }
boxSize={ 6 } boxSize={ 6 }
mx={ 2 }
color="gray.500" color="gray.500"
isLoading={ isLoading }
/> />
{ dataTo && ( { dataTo && (
<Address> <Address ml={ 2 }>
<AddressIcon address={ dataTo }/> <AddressIcon address={ dataTo } isLoading={ isLoading }/>
<AddressLink <AddressLink
type="address" type="address"
hash={ dataTo.hash } hash={ dataTo.hash }
...@@ -104,18 +113,19 @@ const LatestTxsItem = ({ tx }: Props) => { ...@@ -104,18 +113,19 @@ const LatestTxsItem = ({ tx }: Props) => {
ml={ 2 } ml={ 2 }
truncation="constant" truncation="constant"
fontSize="sm" fontSize="sm"
isLoading={ isLoading }
/> />
</Address> </Address>
) } ) }
</Flex> </Flex>
<Box mb={ 2 } fontSize="sm"> <Skeleton isLoaded={ !isLoading } mb={ 2 } fontSize="sm" w="fit-content">
<Text as="span">Value { appConfig.network.currency.symbol } </Text> <Text as="span">Value { appConfig.network.currency.symbol } </Text>
<Text as="span" variant="secondary">{ getValueWithUnit(tx.value).dp(5).toFormat() }</Text> <Text as="span" variant="secondary">{ getValueWithUnit(tx.value).dp(5).toFormat() }</Text>
</Box> </Skeleton>
<Box fontSize="sm"> <Skeleton isLoaded={ !isLoading } fontSize="sm" w="fit-content">
<Text as="span">Fee { appConfig.network.currency.symbol } </Text> <Text as="span">Fee { appConfig.network.currency.symbol } </Text>
<Text as="span" variant="secondary">{ getValueWithUnit(tx.fee.value).dp(5).toFormat() }</Text> <Text as="span" variant="secondary">{ getValueWithUnit(tx.fee.value).dp(5).toFormat() }</Text>
</Box> </Skeleton>
</Box> </Box>
); );
} }
...@@ -131,11 +141,11 @@ const LatestTxsItem = ({ tx }: Props) => { ...@@ -131,11 +141,11 @@ const LatestTxsItem = ({ tx }: Props) => {
> >
<Grid width="100%" gridTemplateColumns="3fr 2fr 150px" gridGap={ 8 }> <Grid width="100%" gridTemplateColumns="3fr 2fr 150px" gridGap={ 8 }>
<Flex overflow="hidden" w="100%"> <Flex overflow="hidden" w="100%">
<TxAdditionalInfo tx={ tx }/> <TxAdditionalInfo tx={ tx } isLoading={ isLoading }/>
<Box ml={ 3 } w="calc(100% - 40px)"> <Box ml={ 3 } w="calc(100% - 40px)">
<HStack> <HStack>
<TxType types={ tx.tx_types }/> <TxType types={ tx.tx_types } isLoading={ isLoading }/>
<TxStatus status={ tx.status } errorText={ tx.status === 'error' ? tx.result : undefined }/> <TxStatus status={ tx.status } errorText={ tx.status === 'error' ? tx.result : undefined } isLoading={ isLoading }/>
</HStack> </HStack>
<Flex <Flex
mt={ 2 } mt={ 2 }
...@@ -146,16 +156,22 @@ const LatestTxsItem = ({ tx }: Props) => { ...@@ -146,16 +156,22 @@ const LatestTxsItem = ({ tx }: Props) => {
boxSize="30px" boxSize="30px"
color="link" color="link"
display="inline" display="inline"
mr={ 2 } isLoading={ isLoading }
borderRadius="base"
/> />
<Address overflow="hidden" w="calc(100% - 130px)" maxW="calc(100% - 130px)" mr={ 2 }> <Address overflow="hidden" w="calc(100% - 130px)" maxW="calc(100% - 130px)" ml={ 2 } mr={ 2 }>
<AddressLink <AddressLink
hash={ tx.hash } hash={ tx.hash }
type="transaction" type="transaction"
fontWeight="700" fontWeight="700"
isLoading={ isLoading }
/> />
</Address> </Address>
{ tx.timestamp && <Text variant="secondary" fontWeight="400" fontSize="sm">{ timeAgo }</Text> } { tx.timestamp && (
<Skeleton isLoaded={ !isLoading } color="text_secondary" fontWeight="400" fontSize="sm">
<span>{ timeAgo }</span>
</Skeleton>
) }
</Flex> </Flex>
</Box> </Box>
</Flex> </Flex>
...@@ -165,10 +181,11 @@ const LatestTxsItem = ({ tx }: Props) => { ...@@ -165,10 +181,11 @@ const LatestTxsItem = ({ tx }: Props) => {
boxSize={ 6 } boxSize={ 6 }
color="gray.500" color="gray.500"
transform="rotate(90deg)" transform="rotate(90deg)"
isLoading={ isLoading }
/> />
<Box overflow="hidden" ml={ 1 }> <Box overflow="hidden" ml={ 1 }>
<Address mb={ 2 }> <Address mb={ 2 }>
<AddressIcon address={ tx.from }/> <AddressIcon address={ tx.from } isLoading={ isLoading }/>
<AddressLink <AddressLink
type="address" type="address"
hash={ tx.from.hash } hash={ tx.from.hash }
...@@ -176,11 +193,12 @@ const LatestTxsItem = ({ tx }: Props) => { ...@@ -176,11 +193,12 @@ const LatestTxsItem = ({ tx }: Props) => {
fontWeight="500" fontWeight="500"
ml={ 2 } ml={ 2 }
fontSize="sm" fontSize="sm"
isLoading={ isLoading }
/> />
</Address> </Address>
{ dataTo && ( { dataTo && (
<Address> <Address>
<AddressIcon address={ dataTo }/> <AddressIcon address={ dataTo } isLoading={ isLoading }/>
<AddressLink <AddressLink
type="address" type="address"
hash={ dataTo.hash } hash={ dataTo.hash }
...@@ -188,20 +206,21 @@ const LatestTxsItem = ({ tx }: Props) => { ...@@ -188,20 +206,21 @@ const LatestTxsItem = ({ tx }: Props) => {
fontWeight="500" fontWeight="500"
ml={ 2 } ml={ 2 }
fontSize="sm" fontSize="sm"
isLoading={ isLoading }
/> />
</Address> </Address>
) } ) }
</Box> </Box>
</Grid> </Grid>
<Box> <Box>
<Box mb={ 2 }> <Skeleton isLoaded={ !isLoading } mb={ 2 }>
<Text as="span" whiteSpace="pre">{ appConfig.network.currency.symbol } </Text> <Text as="span" whiteSpace="pre">{ appConfig.network.currency.symbol } </Text>
<Text as="span" variant="secondary">{ getValueWithUnit(tx.value).dp(5).toFormat() }</Text> <Text as="span" variant="secondary">{ getValueWithUnit(tx.value).dp(5).toFormat() }</Text>
</Box> </Skeleton>
<Box> <Skeleton isLoaded={ !isLoading }>
<Text as="span">Fee </Text> <Text as="span">Fee </Text>
<Text as="span" variant="secondary">{ getValueWithUnit(tx.fee.value).dp(5).toFormat() }</Text> <Text as="span" variant="secondary">{ getValueWithUnit(tx.fee.value).dp(5).toFormat() }</Text>
</Box> </Skeleton>
</Box> </Box>
</Grid> </Grid>
</Box> </Box>
......
import {
Box,
Flex,
HStack,
Skeleton,
SkeletonCircle,
} from '@chakra-ui/react';
import React from 'react';
const LatestTxsItemSkeleton = () => {
return (
<Box
width="100%"
minW={{ base: 'unset', lg: '700px' }}
borderTop="1px solid"
borderColor="divider"
py={ 4 }
px={{ base: 0, lg: 4 }}
_last={{ borderBottom: '1px solid', borderColor: 'divider' }}
>
<Box width="100%" display={{ base: 'block', lg: 'none' }}>
<HStack spacing={ 2 }>
<Skeleton w="101px" h="24px"/>
<Skeleton w="101px" h="24px"/>
</HStack>
<Flex
mt={ 2 }
alignItems="center"
width="100%"
justifyContent="space-between"
mb={ 6 }
>
<Flex mr={ 3 } alignItems="center">
<Skeleton w="30px" h="30px" mr={ 2 }/>
<Skeleton w="101px" h="12px"/>
</Flex>
<Skeleton w="40px" h="12px"/>
</Flex>
<Flex alignItems="center" mb={ 3 }>
<SkeletonCircle w="30px" h="30px" mr={ 2 }/>
<Skeleton w="101px" h="12px" mr={ 5 }/>
<SkeletonCircle w="30px" h="30px" mr={ 2 }/>
<Skeleton w="101px" h="12px"/>
</Flex>
<Skeleton w="123px" h="12px" mb={ 2 } mt={ 3 }/>
<Skeleton w="123px" h="12px"/>
</Box>
<Box display={{ base: 'none', lg: 'grid' }} width="100%" gridTemplateColumns="3fr 2fr 150px" gridGap={ 8 }>
<Flex w="100%">
<Skeleton w={ 5 } h={ 5 } mr={ 3 }/>
<Box w="100%">
<HStack>
<Skeleton w="101px" h="24px"/>
<Skeleton w="101px" h="24px"/>
</HStack>
<Flex alignItems="center" mt={ 2 }>
<Skeleton w="30px" h="30px" mr={ 2 }/>
<Skeleton w="calc(100% - 100px)" h="20px" mr={ 5 }/>
<Skeleton w="40px" h="16px"/>
</Flex>
</Box>
</Flex>
<Box>
<Flex alignItems="center" mb={ 2 } mt={ 1 }>
<SkeletonCircle w="24px" h="24px" mr={ 2 }/>
<Skeleton w="100%" h="16px"/>
</Flex>
<Flex alignItems="center">
<SkeletonCircle w="24px" h="24px" mr={ 2 }/>
<Skeleton w="100%" h="16px"/>
</Flex>
</Box>
<Box>
<Skeleton w="123px" h="16px" mb={ 4 } mt={ 2 }/>
<Skeleton w="123px" h="16px"/>
</Box>
</Box>
</Box>
);
};
export default LatestTxsItemSkeleton;
...@@ -5,26 +5,26 @@ import React from 'react'; ...@@ -5,26 +5,26 @@ import React from 'react';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import useRedirectForInvalidAuthToken from 'lib/hooks/useRedirectForInvalidAuthToken'; import useRedirectForInvalidAuthToken from 'lib/hooks/useRedirectForInvalidAuthToken';
import { TX } from 'stubs/tx';
import LinkInternal from 'ui/shared/LinkInternal'; import LinkInternal from 'ui/shared/LinkInternal';
import LatestTxsItem from './LatestTxsItem'; import LatestTxsItem from './LatestTxsItem';
import LatestTxsItemSkeleton from './LatestTxsItemSkeleton';
const LatestWatchlistTxs = () => { const LatestWatchlistTxs = () => {
useRedirectForInvalidAuthToken(); useRedirectForInvalidAuthToken();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const txsCount = isMobile ? 2 : 6; const txsCount = isMobile ? 2 : 6;
const { data, isLoading, isError } = useApiQuery('homepage_txs_watchlist'); const { data, isPlaceholderData, isError } = useApiQuery('homepage_txs_watchlist', {
queryOptions: {
if (isLoading) { placeholderData: Array(txsCount).fill(TX),
return <>{ Array.from(Array(txsCount)).map((item, index) => <LatestTxsItemSkeleton key={ index }/>) }</>; },
} });
if (isError) { if (isError) {
return <Text mt={ 4 }>No data. Please reload page.</Text>; return <Text mt={ 4 }>No data. Please reload page.</Text>;
} }
if (data.length === 0) { if (!data?.length) {
return <Text mt={ 4 }>There are no transactions.</Text>; return <Text mt={ 4 }>There are no transactions.</Text>;
} }
...@@ -33,7 +33,13 @@ const LatestWatchlistTxs = () => { ...@@ -33,7 +33,13 @@ const LatestWatchlistTxs = () => {
return ( return (
<> <>
<Box mb={{ base: 3, lg: 4 }}> <Box mb={{ base: 3, lg: 4 }}>
{ data.slice(0, txsCount).map((tx => <LatestTxsItem key={ tx.hash } tx={ tx }/>)) } { data.slice(0, txsCount).map(((tx, index) => (
<LatestTxsItem
key={ tx.hash + (isPlaceholderData ? index : '') }
tx={ tx }
isLoading={ isPlaceholderData }
/>
))) }
</Box> </Box>
<Flex justifyContent="center"> <Flex justifyContent="center">
<LinkInternal fontSize="sm" href={ txsUrl }>View all watch list transactions</LinkInternal> <LinkInternal fontSize="sm" href={ txsUrl }>View all watch list transactions</LinkInternal>
......
...@@ -9,10 +9,10 @@ import gasIcon from 'icons/gas.svg'; ...@@ -9,10 +9,10 @@ import gasIcon from 'icons/gas.svg';
import txIcon from 'icons/transactions.svg'; import txIcon from 'icons/transactions.svg';
import walletIcon from 'icons/wallet.svg'; import walletIcon from 'icons/wallet.svg';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import { HOMEPAGE_STATS } from 'stubs/stats';
import StatsGasPrices from './StatsGasPrices'; import StatsGasPrices from './StatsGasPrices';
import StatsItem from './StatsItem'; import StatsItem from './StatsItem';
import StatsItemSkeleton from './StatsItemSkeleton';
const hasGasTracker = appConfig.homepage.showGasTracker; const hasGasTracker = appConfig.homepage.showGasTracker;
const hasAvgBlockTime = appConfig.homepage.showAvgBlockTime; const hasAvgBlockTime = appConfig.homepage.showAvgBlockTime;
...@@ -22,7 +22,11 @@ let itemsCount = 5; ...@@ -22,7 +22,11 @@ let itemsCount = 5;
!hasAvgBlockTime && itemsCount--; !hasAvgBlockTime && itemsCount--;
const Stats = () => { const Stats = () => {
const { data, isLoading, isError } = useApiQuery('homepage_stats'); const { data, isPlaceholderData, isError } = useApiQuery('homepage_stats', {
queryOptions: {
placeholderData: HOMEPAGE_STATS,
},
});
if (isError) { if (isError) {
return null; return null;
...@@ -32,13 +36,10 @@ const Stats = () => { ...@@ -32,13 +36,10 @@ const Stats = () => {
const lastItemTouchStyle = { gridColumn: { base: 'span 2', lg: 'unset' } }; const lastItemTouchStyle = { gridColumn: { base: 'span 2', lg: 'unset' } };
if (isLoading) {
content = Array.from(Array(itemsCount)).map((item, index) => <StatsItemSkeleton key={ index } _last={ itemsCount % 2 ? lastItemTouchStyle : undefined }/>);
}
if (data) { if (data) {
const isOdd = Boolean(hasGasTracker && !data.gas_prices ? (itemsCount - 1) % 2 : itemsCount % 2); const isOdd = Boolean(hasGasTracker && !data.gas_prices ? (itemsCount - 1) % 2 : itemsCount % 2);
const gasLabel = hasGasTracker && data.gas_prices ? <StatsGasPrices gasPrices={ data.gas_prices }/> : null; const gasLabel = hasGasTracker && data.gas_prices ? <StatsGasPrices gasPrices={ data.gas_prices }/> : null;
content = ( content = (
<> <>
<StatsItem <StatsItem
...@@ -46,12 +47,14 @@ const Stats = () => { ...@@ -46,12 +47,14 @@ const Stats = () => {
title="Total blocks" title="Total blocks"
value={ Number(data.total_blocks).toLocaleString() } value={ Number(data.total_blocks).toLocaleString() }
url={ route({ pathname: '/blocks' }) } url={ route({ pathname: '/blocks' }) }
isLoading={ isPlaceholderData }
/> />
{ hasAvgBlockTime && ( { hasAvgBlockTime && (
<StatsItem <StatsItem
icon={ clockIcon } icon={ clockIcon }
title="Average block time" title="Average block time"
value={ `${ (data.average_block_time / 1000).toFixed(1) } s` } value={ `${ (data.average_block_time / 1000).toFixed(1) } s` }
isLoading={ isPlaceholderData }
/> />
) } ) }
<StatsItem <StatsItem
...@@ -59,12 +62,14 @@ const Stats = () => { ...@@ -59,12 +62,14 @@ const Stats = () => {
title="Total transactions" title="Total transactions"
value={ Number(data.total_transactions).toLocaleString() } value={ Number(data.total_transactions).toLocaleString() }
url={ route({ pathname: '/txs' }) } url={ route({ pathname: '/txs' }) }
isLoading={ isPlaceholderData }
/> />
<StatsItem <StatsItem
icon={ walletIcon } icon={ walletIcon }
title="Wallet addresses" title="Wallet addresses"
value={ Number(data.total_addresses).toLocaleString() } value={ Number(data.total_addresses).toLocaleString() }
_last={ isOdd ? lastItemTouchStyle : undefined } _last={ isOdd ? lastItemTouchStyle : undefined }
isLoading={ isPlaceholderData }
/> />
{ hasGasTracker && data.gas_prices && ( { hasGasTracker && data.gas_prices && (
<StatsItem <StatsItem
...@@ -73,6 +78,7 @@ const Stats = () => { ...@@ -73,6 +78,7 @@ const Stats = () => {
value={ `${ Number(data.gas_prices.average).toLocaleString() } Gwei` } value={ `${ Number(data.gas_prices.average).toLocaleString() } Gwei` }
_last={ isOdd ? lastItemTouchStyle : undefined } _last={ isOdd ? lastItemTouchStyle : undefined }
tooltipLabel={ gasLabel } tooltipLabel={ gasLabel }
isLoading={ isPlaceholderData }
/> />
) } ) }
</> </>
......
import type { SystemStyleObject, TooltipProps } from '@chakra-ui/react'; import type { SystemStyleObject, TooltipProps } from '@chakra-ui/react';
import { Flex, Icon, Text, useColorModeValue, chakra, LightMode } from '@chakra-ui/react'; import { Skeleton, Flex, useColorModeValue, chakra, LightMode } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import breakpoints from 'theme/foundations/breakpoints'; import breakpoints from 'theme/foundations/breakpoints';
import Icon from 'ui/shared/chakra/Icon';
import Hint from 'ui/shared/Hint'; import Hint from 'ui/shared/Hint';
type Props = { type Props = {
...@@ -12,6 +13,7 @@ type Props = { ...@@ -12,6 +13,7 @@ type Props = {
className?: string; className?: string;
tooltipLabel?: React.ReactNode; tooltipLabel?: React.ReactNode;
url?: string; url?: string;
isLoading?: boolean;
} }
const LARGEST_BREAKPOINT = '1240px'; const LARGEST_BREAKPOINT = '1240px';
...@@ -24,7 +26,7 @@ const TOOLTIP_PROPS: Partial<TooltipProps> = { ...@@ -24,7 +26,7 @@ const TOOLTIP_PROPS: Partial<TooltipProps> = {
bgColor: 'blackAlpha.900', bgColor: 'blackAlpha.900',
}; };
const StatsItem = ({ icon, title, value, className, tooltipLabel, url }: Props) => { const StatsItem = ({ icon, title, value, className, tooltipLabel, url, isLoading }: Props) => {
const sxContainer: SystemStyleObject = { const sxContainer: SystemStyleObject = {
[`@media screen and (min-width: ${ breakpoints.lg }) and (max-width: ${ LARGEST_BREAKPOINT })`]: { flexDirection: 'column' }, [`@media screen and (min-width: ${ breakpoints.lg }) and (max-width: ${ LARGEST_BREAKPOINT })`]: { flexDirection: 'column' },
}; };
...@@ -33,11 +35,13 @@ const StatsItem = ({ icon, title, value, className, tooltipLabel, url }: Props) ...@@ -33,11 +35,13 @@ const StatsItem = ({ icon, title, value, className, tooltipLabel, url }: Props)
[`@media screen and (min-width: ${ breakpoints.lg }) and (max-width: ${ LARGEST_BREAKPOINT })`]: { alignItems: 'center' }, [`@media screen and (min-width: ${ breakpoints.lg }) and (max-width: ${ LARGEST_BREAKPOINT })`]: { alignItems: 'center' },
}; };
const bgColor = useColorModeValue('blue.50', 'blue.800');
const loadingBgColor = useColorModeValue('blackAlpha.50', 'whiteAlpha.50');
const infoColor = useColorModeValue('gray.600', 'gray.400'); const infoColor = useColorModeValue('gray.600', 'gray.400');
return ( return (
<Flex <Flex
backgroundColor={ useColorModeValue('blue.50', 'blue.800') } backgroundColor={ isLoading ? loadingBgColor : bgColor }
padding={ 3 } padding={ 3 }
borderRadius="md" borderRadius="md"
flexDirection="row" flexDirection="row"
...@@ -48,21 +52,25 @@ const StatsItem = ({ icon, title, value, className, tooltipLabel, url }: Props) ...@@ -48,21 +52,25 @@ const StatsItem = ({ icon, title, value, className, tooltipLabel, url }: Props)
className={ className } className={ className }
color={ useColorModeValue('black', 'white') } color={ useColorModeValue('black', 'white') }
position="relative" position="relative"
{ ...(url ? { { ...(url && !isLoading ? {
as: 'a', as: 'a',
href: url, href: url,
} : {}) } } : {}) }
> >
<Icon as={ icon } boxSize={ 7 }/> <Icon as={ icon } boxSize={ 7 } isLoading={ isLoading } borderRadius="base"/>
<Flex <Flex
flexDirection="column" flexDirection="column"
alignItems="start" alignItems="start"
sx={ sxText } sx={ sxText }
> >
<Text variant="secondary" fontSize="xs" lineHeight="16px">{ title }</Text> <Skeleton isLoaded={ !isLoading } color="text_secondary" fontSize="xs" lineHeight="16px" borderRadius="base">
<Text fontWeight={ 500 } fontSize="md" color={ useColorModeValue('black', 'white') }>{ value }</Text> <span>{ title }</span>
</Skeleton>
<Skeleton isLoaded={ !isLoading } fontWeight={ 500 } fontSize="md" color={ useColorModeValue('black', 'white') } borderRadius="base">
<span>{ value }</span>
</Skeleton>
</Flex> </Flex>
{ tooltipLabel && ( { tooltipLabel && !isLoading && (
<LightMode> <LightMode>
<Hint <Hint
label={ tooltipLabel } label={ tooltipLabel }
......
import { Flex, Skeleton, useColorModeValue, chakra } from '@chakra-ui/react';
import React from 'react';
const StatsItemSkeleton = ({ className }: {className?: string}) => {
const bgColor = useColorModeValue('blackAlpha.50', 'whiteAlpha.50');
return (
<Flex
backgroundColor={ bgColor }
padding={ 3 }
borderRadius="md"
flexDirection={{ base: 'row', lg: 'column', xl: 'row' }}
alignItems="center"
columnGap={ 3 }
rowGap={ 2 }
className={ className }
>
<Skeleton
w="40px"
h="40px"
/>
<Flex flexDirection="column" alignItems={{ base: 'start', lg: 'center', xl: 'start' }}>
<Skeleton w="69px" h="10px" mt="4px" mb="8px"/>
<Skeleton w="93px" h="14px" mb="4px"/>
</Flex>
</Flex>
);
};
export default chakra(StatsItemSkeleton);
import { Box, Icon } from '@chakra-ui/react'; import { Skeleton } from '@chakra-ui/react';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
...@@ -10,33 +10,35 @@ import blockIcon from 'icons/block.svg'; ...@@ -10,33 +10,35 @@ import blockIcon from 'icons/block.svg';
import txIcon from 'icons/transactions.svg'; import txIcon from 'icons/transactions.svg';
import dayjs from 'lib/date/dayjs'; import dayjs from 'lib/date/dayjs';
import AddressIcon from 'ui/shared/address/AddressIcon'; import AddressIcon from 'ui/shared/address/AddressIcon';
import Icon from 'ui/shared/chakra/Icon';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic'; import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import LinkExternal from 'ui/shared/LinkExternal'; import LinkExternal from 'ui/shared/LinkExternal';
import LinkInternal from 'ui/shared/LinkInternal'; import LinkInternal from 'ui/shared/LinkInternal';
import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid'; import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid';
type Props = { item: L2DepositsItem }; type Props = { item: L2DepositsItem; isLoading?: boolean };
const DepositsListItem = ({ item }: Props) => { const DepositsListItem = ({ item, isLoading }: Props) => {
const timeAgo = dayjs(item.l1_block_timestamp).fromNow(); const timeAgo = dayjs(item.l1_block_timestamp).fromNow();
return ( return (
<ListItemMobileGrid.Container> <ListItemMobileGrid.Container>
<ListItemMobileGrid.Label>L1 block No</ListItemMobileGrid.Label> <ListItemMobileGrid.Label isLoading={ isLoading }>L1 block No</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value> <ListItemMobileGrid.Value py="3px">
<LinkExternal <LinkExternal
href={ appConfig.L2.L1BaseUrl + route({ pathname: '/block/[height_or_hash]', query: { height_or_hash: item.l1_block_number.toString() } }) } href={ appConfig.L2.L1BaseUrl + route({ pathname: '/block/[height_or_hash]', query: { height_or_hash: item.l1_block_number.toString() } }) }
fontWeight={ 600 } fontWeight={ 600 }
display="inline-flex" display="flex"
isLoading={ isLoading }
> >
<Icon as={ blockIcon } boxSize={ 6 } mr={ 1 }/> <Icon as={ blockIcon } boxSize={ 6 } isLoading={ isLoading }/>
{ item.l1_block_number } <Skeleton isLoaded={ !isLoading } ml={ 1 }>{ item.l1_block_number }</Skeleton>
</LinkExternal> </LinkExternal>
</ListItemMobileGrid.Value> </ListItemMobileGrid.Value>
<ListItemMobileGrid.Label>L2 txn hash</ListItemMobileGrid.Label> <ListItemMobileGrid.Label isLoading={ isLoading }>L2 txn hash</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value> <ListItemMobileGrid.Value py="3px">
<LinkInternal <LinkInternal
href={ route({ pathname: '/tx/[hash]', query: { hash: item.l2_tx_hash } }) } href={ route({ pathname: '/tx/[hash]', query: { hash: item.l2_tx_hash } }) }
display="flex" display="flex"
...@@ -44,43 +46,56 @@ const DepositsListItem = ({ item }: Props) => { ...@@ -44,43 +46,56 @@ const DepositsListItem = ({ item }: Props) => {
alignItems="center" alignItems="center"
overflow="hidden" overflow="hidden"
w="100%" w="100%"
isLoading={ isLoading }
> >
<Icon as={ txIcon } boxSize={ 6 } mr={ 1 }/> <Icon as={ txIcon } boxSize={ 6 } isLoading={ isLoading }/>
<Box w="calc(100% - 36px)" overflow="hidden" whiteSpace="nowrap"><HashStringShortenDynamic hash={ item.l2_tx_hash }/></Box> <Skeleton isLoaded={ !isLoading } w="calc(100% - 36px)" overflow="hidden" whiteSpace="nowrap" ml={ 1 }>
<HashStringShortenDynamic hash={ item.l2_tx_hash }/>
</Skeleton>
</LinkInternal> </LinkInternal>
</ListItemMobileGrid.Value> </ListItemMobileGrid.Value>
<ListItemMobileGrid.Label>Age</ListItemMobileGrid.Label> <ListItemMobileGrid.Label isLoading={ isLoading }>Age</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>{ timeAgo }</ListItemMobileGrid.Value>
<ListItemMobileGrid.Label>L1 txn hash</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value> <ListItemMobileGrid.Value>
<Skeleton isLoaded={ !isLoading } display="inline-block">{ timeAgo }</Skeleton>
</ListItemMobileGrid.Value>
<ListItemMobileGrid.Label isLoading={ isLoading }>L1 txn hash</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value py="3px">
<LinkExternal <LinkExternal
href={ appConfig.L2.L1BaseUrl + route({ pathname: '/tx/[hash]', query: { hash: item.l1_tx_hash } }) } href={ appConfig.L2.L1BaseUrl + route({ pathname: '/tx/[hash]', query: { hash: item.l1_tx_hash } }) }
maxW="100%" maxW="100%"
display="inline-flex" display="flex"
overflow="hidden" overflow="hidden"
isLoading={ isLoading }
> >
<Icon as={ txIcon } boxSize={ 6 } mr={ 1 }/> <Icon as={ txIcon } boxSize={ 6 } isLoading={ isLoading }/>
<Box w="calc(100% - 44px)" overflow="hidden" whiteSpace="nowrap"><HashStringShortenDynamic hash={ item.l1_tx_hash }/></Box> <Skeleton isLoaded={ !isLoading } w="calc(100% - 44px)" overflow="hidden" whiteSpace="nowrap" ml={ 1 }>
<HashStringShortenDynamic hash={ item.l1_tx_hash }/>
</Skeleton>
</LinkExternal> </LinkExternal>
</ListItemMobileGrid.Value> </ListItemMobileGrid.Value>
<ListItemMobileGrid.Label>L1 txn origin</ListItemMobileGrid.Label> <ListItemMobileGrid.Label isLoading={ isLoading }>L1 txn origin</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value> <ListItemMobileGrid.Value py="3px">
<LinkExternal <LinkExternal
href={ appConfig.L2.L1BaseUrl + route({ pathname: '/address/[hash]', query: { hash: item.l1_tx_origin } }) } href={ appConfig.L2.L1BaseUrl + route({ pathname: '/address/[hash]', query: { hash: item.l1_tx_origin } }) }
maxW="100%" maxW="100%"
display="inline-flex" display="flex"
overflow="hidden" overflow="hidden"
isLoading={ isLoading }
> >
<AddressIcon address={{ hash: item.l1_tx_origin, is_contract: false, implementation_name: '' }} mr={ 2 }/> <AddressIcon address={{ hash: item.l1_tx_origin, is_contract: false, implementation_name: '' }} isLoading={ isLoading }/>
<Box w="calc(100% - 44px)" overflow="hidden" whiteSpace="nowrap"><HashStringShortenDynamic hash={ item.l1_tx_origin }/></Box> <Skeleton isLoaded={ !isLoading } w="calc(100% - 44px)" overflow="hidden" whiteSpace="nowrap" ml={ 2 }>
<HashStringShortenDynamic hash={ item.l1_tx_origin }/>
</Skeleton>
</LinkExternal> </LinkExternal>
</ListItemMobileGrid.Value> </ListItemMobileGrid.Value>
<ListItemMobileGrid.Label>Gas limit</ListItemMobileGrid.Label> <ListItemMobileGrid.Label isLoading={ isLoading }>Gas limit</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>{ BigNumber(item.l2_tx_gas_limit).toFormat() }</ListItemMobileGrid.Value> <ListItemMobileGrid.Value>
<Skeleton isLoaded={ !isLoading } display="inline-block">{ BigNumber(item.l2_tx_gas_limit).toFormat() }</Skeleton>
</ListItemMobileGrid.Value>
</ListItemMobileGrid.Container> </ListItemMobileGrid.Container>
); );
......
...@@ -10,9 +10,10 @@ import DepositsTableItem from './DepositsTableItem'; ...@@ -10,9 +10,10 @@ import DepositsTableItem from './DepositsTableItem';
type Props = { type Props = {
items: Array<L2DepositsItem>; items: Array<L2DepositsItem>;
top: number; top: number;
isLoading?: boolean;
} }
const DepositsTable = ({ items, top }: Props) => { const DepositsTable = ({ items, top, isLoading }: Props) => {
return ( return (
<Table variant="simple" size="sm" style={{ tableLayout: 'auto' }} minW="950px"> <Table variant="simple" size="sm" style={{ tableLayout: 'auto' }} minW="950px">
<Thead top={ top }> <Thead top={ top }>
...@@ -26,8 +27,8 @@ const DepositsTable = ({ items, top }: Props) => { ...@@ -26,8 +27,8 @@ const DepositsTable = ({ items, top }: Props) => {
</Tr> </Tr>
</Thead> </Thead>
<Tbody> <Tbody>
{ items.map((item) => ( { items.map((item, index) => (
<DepositsTableItem key={ item.l2_tx_hash } item={ item }/> <DepositsTableItem key={ item.l2_tx_hash + (isLoading ? index : '') } item={ item } isLoading={ isLoading }/>
)) } )) }
</Tbody> </Tbody>
</Table> </Table>
......
import { Box, Td, Tr, Text, Icon } from '@chakra-ui/react'; import { Td, Tr, Skeleton } from '@chakra-ui/react';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
...@@ -10,13 +10,14 @@ import blockIcon from 'icons/block.svg'; ...@@ -10,13 +10,14 @@ import blockIcon from 'icons/block.svg';
import txIcon from 'icons/transactions.svg'; import txIcon from 'icons/transactions.svg';
import dayjs from 'lib/date/dayjs'; import dayjs from 'lib/date/dayjs';
import AddressIcon from 'ui/shared/address/AddressIcon'; import AddressIcon from 'ui/shared/address/AddressIcon';
import Icon from 'ui/shared/chakra/Icon';
import HashStringShorten from 'ui/shared/HashStringShorten'; import HashStringShorten from 'ui/shared/HashStringShorten';
import LinkExternal from 'ui/shared/LinkExternal'; import LinkExternal from 'ui/shared/LinkExternal';
import LinkInternal from 'ui/shared/LinkInternal'; import LinkInternal from 'ui/shared/LinkInternal';
type Props = { item: L2DepositsItem }; type Props = { item: L2DepositsItem; isLoading?: boolean };
const WithdrawalsTableItem = ({ item }: Props) => { const WithdrawalsTableItem = ({ item, isLoading }: Props) => {
const timeAgo = dayjs(item.l1_block_timestamp).fromNow(); const timeAgo = dayjs(item.l1_block_timestamp).fromNow();
return ( return (
...@@ -26,9 +27,12 @@ const WithdrawalsTableItem = ({ item }: Props) => { ...@@ -26,9 +27,12 @@ const WithdrawalsTableItem = ({ item }: Props) => {
href={ appConfig.L2.L1BaseUrl + route({ pathname: '/block/[height_or_hash]', query: { height_or_hash: item.l1_block_number.toString() } }) } href={ appConfig.L2.L1BaseUrl + route({ pathname: '/block/[height_or_hash]', query: { height_or_hash: item.l1_block_number.toString() } }) }
fontWeight={ 600 } fontWeight={ 600 }
display="inline-flex" display="inline-flex"
isLoading={ isLoading }
> >
<Icon as={ blockIcon } boxSize={ 6 } mr={ 1 }/> <Icon as={ blockIcon } boxSize={ 6 } isLoading={ isLoading }/>
{ item.l1_block_number } <Skeleton isLoaded={ !isLoading } ml={ 1 }>
{ item.l1_block_number }
</Skeleton>
</LinkExternal> </LinkExternal>
</Td> </Td>
<Td verticalAlign="middle"> <Td verticalAlign="middle">
...@@ -39,13 +43,16 @@ const WithdrawalsTableItem = ({ item }: Props) => { ...@@ -39,13 +43,16 @@ const WithdrawalsTableItem = ({ item }: Props) => {
alignItems="center" alignItems="center"
overflow="hidden" overflow="hidden"
w="100%" w="100%"
isLoading={ isLoading }
> >
<Icon as={ txIcon } boxSize={ 6 } mr={ 1 }/> <Icon as={ txIcon } boxSize={ 6 } isLoading={ isLoading }/>
<Box w="calc(100% - 36px)" overflow="hidden" whiteSpace="nowrap"><HashStringShorten hash={ item.l2_tx_hash }/></Box> <Skeleton isLoaded={ !isLoading } w="calc(100% - 36px)" ml={ 1 } overflow="hidden" whiteSpace="nowrap">
<HashStringShorten hash={ item.l2_tx_hash }/>
</Skeleton>
</LinkInternal> </LinkInternal>
</Td> </Td>
<Td verticalAlign="middle" pr={ 12 }> <Td verticalAlign="middle" pr={ 12 }>
<Text variant="secondary">{ timeAgo }</Text> <Skeleton isLoaded={ !isLoading } color="text_secondary" display="inline-block"><span>{ timeAgo }</span></Skeleton>
</Td> </Td>
<Td verticalAlign="middle"> <Td verticalAlign="middle">
<LinkExternal <LinkExternal
...@@ -53,9 +60,12 @@ const WithdrawalsTableItem = ({ item }: Props) => { ...@@ -53,9 +60,12 @@ const WithdrawalsTableItem = ({ item }: Props) => {
maxW="100%" maxW="100%"
display="inline-flex" display="inline-flex"
overflow="hidden" overflow="hidden"
isLoading={ isLoading }
> >
<Icon as={ txIcon } boxSize={ 6 } mr={ 1 }/> <Icon as={ txIcon } boxSize={ 6 } isLoading={ isLoading }/>
<Box w="calc(100% - 44px)" overflow="hidden" whiteSpace="nowrap"><HashStringShorten hash={ item.l1_tx_hash }/></Box> <Skeleton isLoaded={ !isLoading } w="calc(100% - 44px)" overflow="hidden" whiteSpace="nowrap" ml={ 1 }>
<HashStringShorten hash={ item.l1_tx_hash }/>
</Skeleton>
</LinkExternal> </LinkExternal>
</Td> </Td>
<Td verticalAlign="middle"> <Td verticalAlign="middle">
...@@ -64,13 +74,18 @@ const WithdrawalsTableItem = ({ item }: Props) => { ...@@ -64,13 +74,18 @@ const WithdrawalsTableItem = ({ item }: Props) => {
maxW="100%" maxW="100%"
display="inline-flex" display="inline-flex"
overflow="hidden" overflow="hidden"
isLoading={ isLoading }
> >
<AddressIcon address={{ hash: item.l1_tx_origin, is_contract: false, implementation_name: '' }} mr={ 2 }/> <AddressIcon address={{ hash: item.l1_tx_origin, is_contract: false, implementation_name: '' }} isLoading={ isLoading }/>
<Box w="calc(100% - 44px)" overflow="hidden" whiteSpace="nowrap"><HashStringShorten hash={ item.l1_tx_origin }/></Box> <Skeleton isLoaded={ !isLoading } w="calc(100% - 44px)" overflow="hidden" whiteSpace="nowrap" ml={ 2 }>
<HashStringShorten hash={ item.l1_tx_origin }/>
</Skeleton>
</LinkExternal> </LinkExternal>
</Td> </Td>
<Td verticalAlign="middle" isNumeric> <Td verticalAlign="middle" isNumeric>
<Text variant="secondary">{ BigNumber(item.l2_tx_gas_limit).toFormat() }</Text> <Skeleton isLoaded={ !isLoading } color="text_secondary" display="inline-block">
<span>{ BigNumber(item.l2_tx_gas_limit).toFormat() }</span>
</Skeleton>
</Td> </Td>
</Tr> </Tr>
); );
......
import { Box, Flex, Text, Icon } from '@chakra-ui/react'; import { Flex, Skeleton } from '@chakra-ui/react';
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
...@@ -7,58 +7,69 @@ import type { L2OutputRootsItem } from 'types/api/l2OutputRoots'; ...@@ -7,58 +7,69 @@ import type { L2OutputRootsItem } from 'types/api/l2OutputRoots';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
import txIcon from 'icons/transactions.svg'; import txIcon from 'icons/transactions.svg';
import dayjs from 'lib/date/dayjs'; import dayjs from 'lib/date/dayjs';
import Icon from 'ui/shared/chakra/Icon';
import CopyToClipboard from 'ui/shared/CopyToClipboard'; import CopyToClipboard from 'ui/shared/CopyToClipboard';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic'; import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import LinkExternal from 'ui/shared/LinkExternal'; import LinkExternal from 'ui/shared/LinkExternal';
import LinkInternal from 'ui/shared/LinkInternal'; import LinkInternal from 'ui/shared/LinkInternal';
import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid'; import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid';
type Props = { item: L2OutputRootsItem }; type Props = { item: L2OutputRootsItem; isLoading?: boolean };
const OutputRootsListItem = ({ item }: Props) => { const OutputRootsListItem = ({ item, isLoading }: Props) => {
const timeAgo = dayjs(item.l1_timestamp).fromNow(); const timeAgo = dayjs(item.l1_timestamp).fromNow();
return ( return (
<ListItemMobileGrid.Container> <ListItemMobileGrid.Container>
<ListItemMobileGrid.Label>L2 output index</ListItemMobileGrid.Label> <ListItemMobileGrid.Label isLoading={ isLoading }>L2 output index</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value fontWeight={ 600 } color="text"> <ListItemMobileGrid.Value fontWeight={ 600 } color="text">
{ item.l2_output_index } <Skeleton isLoaded={ !isLoading } display="inline-block">{ item.l2_output_index }</Skeleton>
</ListItemMobileGrid.Value> </ListItemMobileGrid.Value>
<ListItemMobileGrid.Label>Age</ListItemMobileGrid.Label> <ListItemMobileGrid.Label isLoading={ isLoading }>Age</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>{ timeAgo }</ListItemMobileGrid.Value> <ListItemMobileGrid.Value>
<Skeleton isLoaded={ !isLoading } display="inline-block">
<span>{ timeAgo }</span>
</Skeleton>
</ListItemMobileGrid.Value>
<ListItemMobileGrid.Label>L2 block #</ListItemMobileGrid.Label> <ListItemMobileGrid.Label isLoading={ isLoading }>L2 block #</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value> <ListItemMobileGrid.Value>
<LinkInternal <LinkInternal
display="flex" display="flex"
width="fit-content" width="fit-content"
alignItems="center" alignItems="center"
href={ route({ pathname: '/block/[height_or_hash]', query: { height_or_hash: item.l2_block_number.toString() } }) } href={ route({ pathname: '/block/[height_or_hash]', query: { height_or_hash: item.l2_block_number.toString() } }) }
isLoading={ isLoading }
> >
{ item.l2_block_number } <Skeleton isLoaded={ !isLoading } display="inline-block">{ item.l2_block_number }</Skeleton>
</LinkInternal> </LinkInternal>
</ListItemMobileGrid.Value> </ListItemMobileGrid.Value>
<ListItemMobileGrid.Label>L1 txn hash</ListItemMobileGrid.Label> <ListItemMobileGrid.Label isLoading={ isLoading }>L1 txn hash</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value> <ListItemMobileGrid.Value py="3px">
<LinkExternal <LinkExternal
maxW="100%" maxW="100%"
display="inline-flex" display="flex"
overflow="hidden" overflow="hidden"
href={ appConfig.L2.L1BaseUrl + route({ pathname: '/tx/[hash]', query: { hash: item.l1_tx_hash } }) } href={ appConfig.L2.L1BaseUrl + route({ pathname: '/tx/[hash]', query: { hash: item.l1_tx_hash } }) }
isLoading={ isLoading }
> >
<Icon as={ txIcon } boxSize={ 6 } mr={ 1 }/> <Icon as={ txIcon } boxSize={ 6 } isLoading={ isLoading }/>
<Box w="calc(100% - 36px)" overflow="hidden" whiteSpace="nowrap"><HashStringShortenDynamic hash={ item.l1_tx_hash }/></Box> <Skeleton isLoaded={ !isLoading } w="calc(100% - 36px)" overflow="hidden" whiteSpace="nowrap" ml={ 1 }>
<HashStringShortenDynamic hash={ item.l1_tx_hash }/>
</Skeleton>
</LinkExternal> </LinkExternal>
</ListItemMobileGrid.Value> </ListItemMobileGrid.Value>
<ListItemMobileGrid.Label>Output root</ListItemMobileGrid.Label> <ListItemMobileGrid.Label isLoading={ isLoading }>Output root</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value> <ListItemMobileGrid.Value>
<Flex overflow="hidden" whiteSpace="nowrap" alignItems="center" w="100%" justifyContent="space-between"> <Flex overflow="hidden" whiteSpace="nowrap" alignItems="center" w="100%" justifyContent="space-between">
<Text variant="secondary" w="calc(100% - 24px)"><HashStringShortenDynamic hash={ item.output_root }/></Text> <Skeleton isLoaded={ !isLoading } color="text_secondary" w="calc(100% - 24px)">
<CopyToClipboard text={ item.output_root }/> <HashStringShortenDynamic hash={ item.output_root }/>
</Skeleton>
<CopyToClipboard text={ item.output_root } isLoading={ isLoading }/>
</Flex> </Flex>
</ListItemMobileGrid.Value> </ListItemMobileGrid.Value>
......
...@@ -10,9 +10,10 @@ import OutputRootsTableItem from './OutputRootsTableItem'; ...@@ -10,9 +10,10 @@ import OutputRootsTableItem from './OutputRootsTableItem';
type Props = { type Props = {
items: Array<L2OutputRootsItem>; items: Array<L2OutputRootsItem>;
top: number; top: number;
isLoading?: boolean;
} }
const OutputRootsTable = ({ items, top }: Props) => { const OutputRootsTable = ({ items, top, isLoading }: Props) => {
return ( return (
<Table variant="simple" size="sm" minW="900px"> <Table variant="simple" size="sm" minW="900px">
<Thead top={ top }> <Thead top={ top }>
...@@ -25,8 +26,8 @@ const OutputRootsTable = ({ items, top }: Props) => { ...@@ -25,8 +26,8 @@ const OutputRootsTable = ({ items, top }: Props) => {
</Tr> </Tr>
</Thead> </Thead>
<Tbody> <Tbody>
{ items.map((item) => ( { items.map((item, index) => (
<OutputRootsTableItem key={ item.l2_output_index } item={ item }/> <OutputRootsTableItem key={ item.l2_output_index + (Number(isLoading ? index : '') ? String(index) : '') } item={ item } isLoading={ isLoading }/>
)) } )) }
</Tbody> </Tbody>
</Table> </Table>
......
import { Box, Flex, Td, Tr, Text, Icon } from '@chakra-ui/react'; import { Flex, Td, Tr, Skeleton } from '@chakra-ui/react';
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
...@@ -8,23 +8,24 @@ import appConfig from 'configs/app/config'; ...@@ -8,23 +8,24 @@ import appConfig from 'configs/app/config';
import txIcon from 'icons/transactions.svg'; import txIcon from 'icons/transactions.svg';
import txBatchIcon from 'icons/txBatch.svg'; import txBatchIcon from 'icons/txBatch.svg';
import dayjs from 'lib/date/dayjs'; import dayjs from 'lib/date/dayjs';
import Icon from 'ui/shared/chakra/Icon';
import CopyToClipboard from 'ui/shared/CopyToClipboard'; import CopyToClipboard from 'ui/shared/CopyToClipboard';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic'; import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import LinkExternal from 'ui/shared/LinkExternal'; import LinkExternal from 'ui/shared/LinkExternal';
import LinkInternal from 'ui/shared/LinkInternal'; import LinkInternal from 'ui/shared/LinkInternal';
type Props = { item: L2OutputRootsItem }; type Props = { item: L2OutputRootsItem; isLoading?: boolean };
const OutputRootsTableItem = ({ item }: Props) => { const OutputRootsTableItem = ({ item, isLoading }: Props) => {
const timeAgo = dayjs(item.l1_timestamp).fromNow(); const timeAgo = dayjs(item.l1_timestamp).fromNow();
return ( return (
<Tr> <Tr>
<Td verticalAlign="middle"> <Td verticalAlign="middle">
<Text>{ item.l2_output_index }</Text> <Skeleton isLoaded={ !isLoading } display="inline-block">{ item.l2_output_index }</Skeleton>
</Td> </Td>
<Td verticalAlign="middle"> <Td verticalAlign="middle">
<Text variant="secondary">{ timeAgo }</Text> <Skeleton isLoaded={ !isLoading } color="text_secondary" display="inline-block"><span>{ timeAgo }</span></Skeleton>
</Td> </Td>
<Td verticalAlign="middle"> <Td verticalAlign="middle">
<LinkInternal <LinkInternal
...@@ -33,9 +34,12 @@ const OutputRootsTableItem = ({ item }: Props) => { ...@@ -33,9 +34,12 @@ const OutputRootsTableItem = ({ item }: Props) => {
width="fit-content" width="fit-content"
alignItems="center" alignItems="center"
href={ route({ pathname: '/block/[height_or_hash]', query: { height_or_hash: item.l2_block_number.toString() } }) } href={ route({ pathname: '/block/[height_or_hash]', query: { height_or_hash: item.l2_block_number.toString() } }) }
isLoading={ isLoading }
> >
<Icon as={ txBatchIcon } boxSize={ 6 } mr={ 1 }/> <Icon as={ txBatchIcon } boxSize={ 6 } isLoading={ isLoading }/>
{ item.l2_block_number } <Skeleton isLoaded={ !isLoading } display="inline-block" ml={ 1 }>
{ item.l2_block_number }
</Skeleton>
</LinkInternal> </LinkInternal>
</Td> </Td>
<Td verticalAlign="middle" pr={ 12 }> <Td verticalAlign="middle" pr={ 12 }>
...@@ -44,16 +48,21 @@ const OutputRootsTableItem = ({ item }: Props) => { ...@@ -44,16 +48,21 @@ const OutputRootsTableItem = ({ item }: Props) => {
maxW="100%" maxW="100%"
display="inline-flex" display="inline-flex"
href={ appConfig.L2.L1BaseUrl + route({ pathname: '/tx/[hash]', query: { hash: item.l1_tx_hash } }) } href={ appConfig.L2.L1BaseUrl + route({ pathname: '/tx/[hash]', query: { hash: item.l1_tx_hash } }) }
isLoading={ isLoading }
> >
<Icon as={ txIcon } boxSize={ 6 } mr={ 1 }/> <Icon as={ txIcon } boxSize={ 6 } isLoading={ isLoading }/>
<Box w="calc(100% - 36px)" overflow="hidden" whiteSpace="nowrap"><HashStringShortenDynamic hash={ item.l1_tx_hash }/></Box> <Skeleton isLoaded={ !isLoading } w="calc(100% - 36px)" overflow="hidden" whiteSpace="nowrap" ml={ 1 } >
<HashStringShortenDynamic hash={ item.l1_tx_hash }/>
</Skeleton>
</LinkExternal> </LinkExternal>
</Flex> </Flex>
</Td> </Td>
<Td verticalAlign="middle"> <Td verticalAlign="middle">
<Flex overflow="hidden" whiteSpace="nowrap" w="100%" alignItems="center"> <Flex overflow="hidden" whiteSpace="nowrap" w="100%" alignItems="center">
<Box w="calc(100% - 36px)"><HashStringShortenDynamic hash={ item.output_root }/></Box> <Skeleton isLoaded={ !isLoading } w="calc(100% - 36px)">
<CopyToClipboard text={ item.output_root } ml={ 2 }/> <HashStringShortenDynamic hash={ item.output_root }/>
</Skeleton>
<CopyToClipboard text={ item.output_root } ml={ 2 } isLoading={ isLoading }/>
</Flex> </Flex>
</Td> </Td>
</Tr> </Tr>
......
import { Box, Icon, VStack } from '@chakra-ui/react'; import { Skeleton, VStack } from '@chakra-ui/react';
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
...@@ -8,70 +8,87 @@ import appConfig from 'configs/app/config'; ...@@ -8,70 +8,87 @@ import appConfig from 'configs/app/config';
import txIcon from 'icons/transactions.svg'; import txIcon from 'icons/transactions.svg';
import txBatchIcon from 'icons/txBatch.svg'; import txBatchIcon from 'icons/txBatch.svg';
import dayjs from 'lib/date/dayjs'; import dayjs from 'lib/date/dayjs';
import Icon from 'ui/shared/chakra/Icon';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic'; import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import LinkExternal from 'ui/shared/LinkExternal'; import LinkExternal from 'ui/shared/LinkExternal';
import LinkInternal from 'ui/shared/LinkInternal'; import LinkInternal from 'ui/shared/LinkInternal';
import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid'; import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid';
type Props = { item: L2TxnBatchesItem }; type Props = { item: L2TxnBatchesItem; isLoading?: boolean };
const TxnBatchesListItem = ({ item }: Props) => { const TxnBatchesListItem = ({ item, isLoading }: Props) => {
const timeAgo = dayjs(item.l1_timestamp).fromNow(); const timeAgo = dayjs(item.l1_timestamp).fromNow();
return ( return (
<ListItemMobileGrid.Container gridTemplateColumns="100px auto"> <ListItemMobileGrid.Container gridTemplateColumns="100px auto">
<ListItemMobileGrid.Label>L2 block #</ListItemMobileGrid.Label> <ListItemMobileGrid.Label isLoading={ isLoading }>L2 block #</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value> <ListItemMobileGrid.Value py="3px">
<LinkInternal <LinkInternal
fontWeight={ 600 } fontWeight={ 600 }
display="flex" display="flex"
width="fit-content" width="fit-content"
alignItems="center" alignItems="center"
href={ route({ pathname: '/block/[height_or_hash]', query: { height_or_hash: item.l2_block_number.toString() } }) } href={ route({ pathname: '/block/[height_or_hash]', query: { height_or_hash: item.l2_block_number.toString() } }) }
isLoading={ isLoading }
> >
<Icon as={ txBatchIcon } boxSize={ 6 } mr={ 1 }/> <Icon as={ txBatchIcon } boxSize={ 6 } isLoading={ isLoading }/>
{ item.l2_block_number } <Skeleton isLoaded={ !isLoading } ml={ 1 }>
{ item.l2_block_number }
</Skeleton>
</LinkInternal> </LinkInternal>
</ListItemMobileGrid.Value> </ListItemMobileGrid.Value>
<ListItemMobileGrid.Label>L2 block txn count</ListItemMobileGrid.Label> <ListItemMobileGrid.Label isLoading={ isLoading }>L2 block txn count</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value> <ListItemMobileGrid.Value>
<LinkInternal href={ route({ pathname: '/block/[height_or_hash]', query: { height_or_hash: item.l2_block_number.toString(), tab: 'txs' } }) }> <LinkInternal
{ item.tx_count } href={ route({ pathname: '/block/[height_or_hash]', query: { height_or_hash: item.l2_block_number.toString(), tab: 'txs' } }) }
isLoading={ isLoading }
>
<Skeleton isLoaded={ !isLoading } minW="40px">
{ item.tx_count }
</Skeleton>
</LinkInternal> </LinkInternal>
</ListItemMobileGrid.Value> </ListItemMobileGrid.Value>
<ListItemMobileGrid.Label>Epoch number</ListItemMobileGrid.Label> <ListItemMobileGrid.Label isLoading={ isLoading }>Epoch number</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value> <ListItemMobileGrid.Value>
<LinkExternal <LinkExternal
fontWeight={ 600 } fontWeight={ 600 }
display="inline-flex" display="inline-flex"
href={ appConfig.L2.L1BaseUrl + route({ pathname: '/block/[height_or_hash]', query: { height_or_hash: item.epoch_number.toString() } }) } href={ appConfig.L2.L1BaseUrl + route({ pathname: '/block/[height_or_hash]', query: { height_or_hash: item.epoch_number.toString() } }) }
isLoading={ isLoading }
> >
{ item.epoch_number } <Skeleton isLoaded={ !isLoading }>
{ item.epoch_number }
</Skeleton>
</LinkExternal> </LinkExternal>
</ListItemMobileGrid.Value> </ListItemMobileGrid.Value>
<ListItemMobileGrid.Label>L1 txn hash</ListItemMobileGrid.Label> <ListItemMobileGrid.Label isLoading={ isLoading }>L1 txn hash</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value> <ListItemMobileGrid.Value py="3px">
<VStack spacing={ 3 } w="100%" overflow="hidden"> <VStack spacing={ 3 } w="100%" overflow="hidden" alignItems="flex-start">
{ item.l1_tx_hashes.map(hash => ( { item.l1_tx_hashes.map(hash => (
<LinkExternal <LinkExternal
maxW="100%" maxW="100%"
display="inline-flex" display="inline-flex"
href={ appConfig.L2.L1BaseUrl + route({ pathname: '/tx/[hash]', query: { hash: hash } }) } href={ appConfig.L2.L1BaseUrl + route({ pathname: '/tx/[hash]', query: { hash: hash } }) }
key={ hash } key={ hash }
isLoading={ isLoading }
> >
<Icon as={ txIcon } boxSize={ 6 } mr={ 1 }/> <Icon as={ txIcon } boxSize={ 6 } isLoading={ isLoading }/>
<Box w="calc(100% - 36px)" overflow="hidden" whiteSpace="nowrap"><HashStringShortenDynamic hash={ hash }/></Box> <Skeleton isLoaded={ !isLoading } w="calc(100% - 36px)" overflow="hidden" whiteSpace="nowrap" ml={ 1 }>
<HashStringShortenDynamic hash={ hash }/>
</Skeleton>
</LinkExternal> </LinkExternal>
)) } )) }
</VStack> </VStack>
</ListItemMobileGrid.Value> </ListItemMobileGrid.Value>
<ListItemMobileGrid.Label>Age</ListItemMobileGrid.Label> <ListItemMobileGrid.Label isLoading={ isLoading }>Age</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>{ timeAgo }</ListItemMobileGrid.Value> <ListItemMobileGrid.Value>
<Skeleton isLoaded={ !isLoading } display="inline-block">{ timeAgo }</Skeleton>
</ListItemMobileGrid.Value>
</ListItemMobileGrid.Container> </ListItemMobileGrid.Container>
); );
......
...@@ -10,9 +10,10 @@ import TxnBatchesTableItem from './TxnBatchesTableItem'; ...@@ -10,9 +10,10 @@ import TxnBatchesTableItem from './TxnBatchesTableItem';
type Props = { type Props = {
items: Array<L2TxnBatchesItem>; items: Array<L2TxnBatchesItem>;
top: number; top: number;
isLoading?: boolean;
} }
const TxnBatchesTable = ({ items, top }: Props) => { const TxnBatchesTable = ({ items, top, isLoading }: Props) => {
return ( return (
<Table variant="simple" size="sm" minW="850px"> <Table variant="simple" size="sm" minW="850px">
<Thead top={ top }> <Thead top={ top }>
...@@ -25,8 +26,12 @@ const TxnBatchesTable = ({ items, top }: Props) => { ...@@ -25,8 +26,12 @@ const TxnBatchesTable = ({ items, top }: Props) => {
</Tr> </Tr>
</Thead> </Thead>
<Tbody> <Tbody>
{ items.map((item) => ( { items.map((item, index) => (
<TxnBatchesTableItem key={ item.l2_block_number } item={ item }/> <TxnBatchesTableItem
key={ item.l2_block_number + (isLoading ? String(index) : '') }
item={ item }
isLoading={ isLoading }
/>
)) } )) }
</Tbody> </Tbody>
</Table> </Table>
......
import { Box, Td, Tr, Text, Icon, VStack } from '@chakra-ui/react'; import { Td, Tr, VStack, Skeleton } from '@chakra-ui/react';
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
...@@ -8,13 +8,14 @@ import appConfig from 'configs/app/config'; ...@@ -8,13 +8,14 @@ import appConfig from 'configs/app/config';
import txIcon from 'icons/transactions.svg'; import txIcon from 'icons/transactions.svg';
import txBatchIcon from 'icons/txBatch.svg'; import txBatchIcon from 'icons/txBatch.svg';
import dayjs from 'lib/date/dayjs'; import dayjs from 'lib/date/dayjs';
import Icon from 'ui/shared/chakra/Icon';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic'; import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import LinkExternal from 'ui/shared/LinkExternal'; import LinkExternal from 'ui/shared/LinkExternal';
import LinkInternal from 'ui/shared/LinkInternal'; import LinkInternal from 'ui/shared/LinkInternal';
type Props = { item: L2TxnBatchesItem }; type Props = { item: L2TxnBatchesItem; isLoading?: boolean };
const TxnBatchesTableItem = ({ item }: Props) => { const TxnBatchesTableItem = ({ item, isLoading }: Props) => {
const timeAgo = dayjs(item.l1_timestamp).fromNow(); const timeAgo = dayjs(item.l1_timestamp).fromNow();
return ( return (
...@@ -26,46 +27,59 @@ const TxnBatchesTableItem = ({ item }: Props) => { ...@@ -26,46 +27,59 @@ const TxnBatchesTableItem = ({ item }: Props) => {
width="fit-content" width="fit-content"
alignItems="center" alignItems="center"
href={ route({ pathname: '/block/[height_or_hash]', query: { height_or_hash: item.l2_block_number.toString() } }) } href={ route({ pathname: '/block/[height_or_hash]', query: { height_or_hash: item.l2_block_number.toString() } }) }
isLoading={ isLoading }
> >
<Icon as={ txBatchIcon } boxSize={ 6 } mr={ 1 }/> <Icon as={ txBatchIcon } boxSize={ 6 } isLoading={ isLoading }/>
{ item.l2_block_number } <Skeleton isLoaded={ !isLoading } ml={ 1 }>
{ item.l2_block_number }
</Skeleton>
</LinkInternal> </LinkInternal>
</Td> </Td>
<Td> <Td>
<LinkInternal <LinkInternal
href={ route({ pathname: '/block/[height_or_hash]', query: { height_or_hash: item.l2_block_number.toString(), tab: 'txs' } }) } href={ route({ pathname: '/block/[height_or_hash]', query: { height_or_hash: item.l2_block_number.toString(), tab: 'txs' } }) }
lineHeight="24px" isLoading={ isLoading }
> >
{ item.tx_count } <Skeleton isLoaded={ !isLoading } minW="40px" my={ 1 }>
{ item.tx_count }
</Skeleton>
</LinkInternal> </LinkInternal>
</Td> </Td>
<Td> <Td>
<LinkExternal <LinkExternal
href={ appConfig.L2.L1BaseUrl + route({ pathname: '/block/[height_or_hash]', query: { height_or_hash: item.epoch_number.toString() } }) } href={ appConfig.L2.L1BaseUrl + route({ pathname: '/block/[height_or_hash]', query: { height_or_hash: item.epoch_number.toString() } }) }
fontWeight={ 600 } fontWeight={ 600 }
lineHeight="24px"
display="inline-flex" display="inline-flex"
isLoading={ isLoading }
py="2px"
> >
{ item.epoch_number } <Skeleton isLoaded={ !isLoading } display="inline-block">
{ item.epoch_number }
</Skeleton>
</LinkExternal> </LinkExternal>
</Td> </Td>
<Td pr={ 12 }> <Td pr={ 12 }>
<VStack spacing={ 3 }> <VStack spacing={ 3 } alignItems="flex-start">
{ item.l1_tx_hashes.map(hash => ( { item.l1_tx_hashes.map(hash => (
<LinkExternal <LinkExternal
maxW="100%" maxW="100%"
display="inline-flex" display="inline-flex"
key={ hash } key={ hash }
href={ appConfig.L2.L1BaseUrl + route({ pathname: '/tx/[hash]', query: { hash: hash } }) } href={ appConfig.L2.L1BaseUrl + route({ pathname: '/tx/[hash]', query: { hash: hash } }) }
isLoading={ isLoading }
> >
<Icon as={ txIcon } boxSize={ 6 } mr={ 1 }/> <Icon as={ txIcon } boxSize={ 6 } isLoading={ isLoading }/>
<Box w="calc(100% - 36px)" overflow="hidden" whiteSpace="nowrap"><HashStringShortenDynamic hash={ hash }/></Box> <Skeleton isLoaded={ !isLoading } w="calc(100% - 36px)" overflow="hidden" whiteSpace="nowrap" ml={ 1 }>
<HashStringShortenDynamic hash={ hash }/>
</Skeleton>
</LinkExternal> </LinkExternal>
)) } )) }
</VStack> </VStack>
</Td> </Td>
<Td> <Td>
<Text variant="secondary" lineHeight="24px">{ timeAgo }</Text> <Skeleton isLoaded={ !isLoading } color="text_secondary" my={ 1 } display="inline-block">
<span>{ timeAgo }</span>
</Skeleton>
</Td> </Td>
</Tr> </Tr>
); );
......
import { Box, Icon } from '@chakra-ui/react'; import { Skeleton } from '@chakra-ui/react';
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
...@@ -10,39 +10,42 @@ import dayjs from 'lib/date/dayjs'; ...@@ -10,39 +10,42 @@ import dayjs from 'lib/date/dayjs';
import Address from 'ui/shared/address/Address'; import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon'; import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink'; import AddressLink from 'ui/shared/address/AddressLink';
import Icon from 'ui/shared/chakra/Icon';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic'; import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import LinkExternal from 'ui/shared/LinkExternal'; import LinkExternal from 'ui/shared/LinkExternal';
import LinkInternal from 'ui/shared/LinkInternal'; import LinkInternal from 'ui/shared/LinkInternal';
import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid'; import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid';
type Props = { item: L2WithdrawalsItem }; type Props = { item: L2WithdrawalsItem; isLoading?: boolean };
const WithdrawalsListItem = ({ item }: Props) => { const WithdrawalsListItem = ({ item, isLoading }: Props) => {
const timeAgo = item.l2_timestamp ? dayjs(item.l2_timestamp).fromNow() : null; const timeAgo = item.l2_timestamp ? dayjs(item.l2_timestamp).fromNow() : null;
const timeToEnd = item.challenge_period_end ? dayjs(item.challenge_period_end).fromNow(true) + ' left' : null; const timeToEnd = item.challenge_period_end ? dayjs(item.challenge_period_end).fromNow(true) + ' left' : null;
return ( return (
<ListItemMobileGrid.Container> <ListItemMobileGrid.Container>
<ListItemMobileGrid.Label>Msg nonce</ListItemMobileGrid.Label> <ListItemMobileGrid.Label isLoading={ isLoading }>Msg nonce</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value> <ListItemMobileGrid.Value>
{ item.msg_nonce_version + '-' + item.msg_nonce } <Skeleton isLoaded={ !isLoading } display="inline-block">
{ item.msg_nonce_version + '-' + item.msg_nonce }
</Skeleton>
</ListItemMobileGrid.Value> </ListItemMobileGrid.Value>
{ item.from && ( { item.from && (
<> <>
<ListItemMobileGrid.Label>From</ListItemMobileGrid.Label> <ListItemMobileGrid.Label isLoading={ isLoading }>From</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value> <ListItemMobileGrid.Value py="3px">
<Address> <Address>
<AddressIcon address={ item.from }/> <AddressIcon address={ item.from } isLoading={ isLoading }/>
<AddressLink hash={ item.from.hash } type="address" truncation="dynamic" ml={ 2 }/> <AddressLink hash={ item.from.hash } type="address" truncation="dynamic" ml={ 2 } isLoading={ isLoading }/>
</Address> </Address>
</ListItemMobileGrid.Value> </ListItemMobileGrid.Value>
</> </>
) } ) }
<ListItemMobileGrid.Label>L2 txn hash</ListItemMobileGrid.Label> <ListItemMobileGrid.Label isLoading={ isLoading }>L2 txn hash</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value> <ListItemMobileGrid.Value py="3px">
<LinkInternal <LinkInternal
href={ route({ pathname: '/tx/[hash]', query: { hash: item.l2_tx_hash } }) } href={ route({ pathname: '/tx/[hash]', query: { hash: item.l2_tx_hash } }) }
display="flex" display="flex"
...@@ -51,37 +54,46 @@ const WithdrawalsListItem = ({ item }: Props) => { ...@@ -51,37 +54,46 @@ const WithdrawalsListItem = ({ item }: Props) => {
overflow="hidden" overflow="hidden"
w="100%" w="100%"
> >
<Icon as={ txIcon } boxSize={ 6 } mr={ 1 }/> <Icon as={ txIcon } boxSize={ 6 } isLoading={ isLoading }/>
<Box w="calc(100% - 36px)" overflow="hidden" whiteSpace="nowrap"><HashStringShortenDynamic hash={ item.l2_tx_hash }/></Box> <Skeleton isLoaded={ !isLoading } w="calc(100% - 36px)" overflow="hidden" whiteSpace="nowrap" ml={ 1 }>
<HashStringShortenDynamic hash={ item.l2_tx_hash }/>
</Skeleton>
</LinkInternal> </LinkInternal>
</ListItemMobileGrid.Value> </ListItemMobileGrid.Value>
{ timeAgo && ( { timeAgo && (
<> <>
<ListItemMobileGrid.Label>Age</ListItemMobileGrid.Label> <ListItemMobileGrid.Label isLoading={ isLoading }>Age</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>{ timeAgo }</ListItemMobileGrid.Value> <ListItemMobileGrid.Value>
<Skeleton isLoaded={ !isLoading } display="inline-block">
{ timeAgo }
</Skeleton>
</ListItemMobileGrid.Value>
</> </>
) } ) }
<ListItemMobileGrid.Label>Status</ListItemMobileGrid.Label> <ListItemMobileGrid.Label isLoading={ isLoading }>Status</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value> <ListItemMobileGrid.Value>
{ item.status === 'Ready for relay' ? { item.status === 'Ready for relay' ?
<LinkExternal href={ appConfig.L2.withdrawalUrl }>{ item.status }</LinkExternal> : <LinkExternal href={ appConfig.L2.withdrawalUrl }>{ item.status }</LinkExternal> :
item.status } <Skeleton isLoaded={ !isLoading } display="inline-block">{ item.status }</Skeleton> }
</ListItemMobileGrid.Value> </ListItemMobileGrid.Value>
{ item.l1_tx_hash && ( { item.l1_tx_hash && (
<> <>
<ListItemMobileGrid.Label>L1 txn hash</ListItemMobileGrid.Label> <ListItemMobileGrid.Label isLoading={ isLoading }>L1 txn hash</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value> <ListItemMobileGrid.Value py="3px">
<LinkExternal <LinkExternal
href={ appConfig.L2.L1BaseUrl + route({ pathname: '/tx/[hash]', query: { hash: item.l1_tx_hash } }) } href={ appConfig.L2.L1BaseUrl + route({ pathname: '/tx/[hash]', query: { hash: item.l1_tx_hash } }) }
maxW="100%" maxW="100%"
display="inline-flex" display="inline-flex"
overflow="hidden" overflow="hidden"
isLoading={ isLoading }
> >
<Icon as={ txIcon } boxSize={ 6 } mr={ 1 }/> <Icon as={ txIcon } boxSize={ 6 } isLoading={ isLoading }/>
<Box w="calc(100% - 44px)" overflow="hidden" whiteSpace="nowrap"><HashStringShortenDynamic hash={ item.l1_tx_hash }/></Box> <Skeleton isLoaded={ !isLoading } w="calc(100% - 44px)" overflow="hidden" whiteSpace="nowrap" ml={ 1 }>
<HashStringShortenDynamic hash={ item.l1_tx_hash }/>
</Skeleton>
</LinkExternal> </LinkExternal>
</ListItemMobileGrid.Value> </ListItemMobileGrid.Value>
</> </>
...@@ -89,7 +101,7 @@ const WithdrawalsListItem = ({ item }: Props) => { ...@@ -89,7 +101,7 @@ const WithdrawalsListItem = ({ item }: Props) => {
{ timeToEnd && ( { timeToEnd && (
<> <>
<ListItemMobileGrid.Label>Time left</ListItemMobileGrid.Label> <ListItemMobileGrid.Label isLoading={ isLoading }>Time left</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>{ timeToEnd }</ListItemMobileGrid.Value> <ListItemMobileGrid.Value>{ timeToEnd }</ListItemMobileGrid.Value>
</> </>
) } ) }
......
...@@ -10,9 +10,10 @@ import WithdrawalsTableItem from './WithdrawalsTableItem'; ...@@ -10,9 +10,10 @@ import WithdrawalsTableItem from './WithdrawalsTableItem';
type Props = { type Props = {
items: Array<L2WithdrawalsItem>; items: Array<L2WithdrawalsItem>;
top: number; top: number;
isLoading?: boolean;
} }
const WithdrawalsTable = ({ items, top }: Props) => { const WithdrawalsTable = ({ items, top, isLoading }: Props) => {
return ( return (
<Table variant="simple" size="sm" style={{ tableLayout: 'auto' }} minW="950px"> <Table variant="simple" size="sm" style={{ tableLayout: 'auto' }} minW="950px">
<Thead top={ top }> <Thead top={ top }>
...@@ -27,8 +28,8 @@ const WithdrawalsTable = ({ items, top }: Props) => { ...@@ -27,8 +28,8 @@ const WithdrawalsTable = ({ items, top }: Props) => {
</Tr> </Tr>
</Thead> </Thead>
<Tbody> <Tbody>
{ items.map((item) => ( { items.map((item, index) => (
<WithdrawalsTableItem key={ item.l2_tx_hash } item={ item }/> <WithdrawalsTableItem key={ item.l2_tx_hash + (isLoading ? index : '') } item={ item } isLoading={ isLoading }/>
)) } )) }
</Tbody> </Tbody>
</Table> </Table>
......
import { Box, Td, Tr, Text, Icon } from '@chakra-ui/react'; import { Td, Tr, Skeleton } from '@chakra-ui/react';
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
...@@ -10,26 +10,27 @@ import dayjs from 'lib/date/dayjs'; ...@@ -10,26 +10,27 @@ import dayjs from 'lib/date/dayjs';
import Address from 'ui/shared/address/Address'; import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon'; import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink'; import AddressLink from 'ui/shared/address/AddressLink';
import Icon from 'ui/shared/chakra/Icon';
import HashStringShorten from 'ui/shared/HashStringShorten'; import HashStringShorten from 'ui/shared/HashStringShorten';
import LinkExternal from 'ui/shared/LinkExternal'; import LinkExternal from 'ui/shared/LinkExternal';
import LinkInternal from 'ui/shared/LinkInternal'; import LinkInternal from 'ui/shared/LinkInternal';
type Props = { item: L2WithdrawalsItem }; type Props = { item: L2WithdrawalsItem; isLoading?: boolean };
const WithdrawalsTableItem = ({ item }: Props) => { const WithdrawalsTableItem = ({ item, isLoading }: Props) => {
const timeAgo = item.l2_timestamp ? dayjs(item.l2_timestamp).fromNow() : 'N/A'; const timeAgo = item.l2_timestamp ? dayjs(item.l2_timestamp).fromNow() : 'N/A';
const timeToEnd = item.challenge_period_end ? dayjs(item.challenge_period_end).fromNow(true) + ' left' : ''; const timeToEnd = item.challenge_period_end ? dayjs(item.challenge_period_end).fromNow(true) + ' left' : '';
return ( return (
<Tr> <Tr>
<Td verticalAlign="middle" fontWeight={ 600 }> <Td verticalAlign="middle" fontWeight={ 600 }>
<Text>{ item.msg_nonce_version + '-' + item.msg_nonce }</Text> <Skeleton isLoaded={ !isLoading } display="inline-block">{ item.msg_nonce_version + '-' + item.msg_nonce }</Skeleton>
</Td> </Td>
<Td verticalAlign="middle"> <Td verticalAlign="middle">
{ item.from ? ( { item.from ? (
<Address> <Address>
<AddressIcon address={ item.from }/> <AddressIcon address={ item.from } isLoading={ isLoading }/>
<AddressLink hash={ item.from.hash } type="address" truncation="constant" ml={ 2 }/> <AddressLink hash={ item.from.hash } type="address" truncation="constant" ml={ 2 } isLoading={ isLoading }/>
</Address> </Address>
) : 'N/A' } ) : 'N/A' }
</Td> </Td>
...@@ -39,33 +40,42 @@ const WithdrawalsTableItem = ({ item }: Props) => { ...@@ -39,33 +40,42 @@ const WithdrawalsTableItem = ({ item }: Props) => {
display="flex" display="flex"
width="fit-content" width="fit-content"
alignItems="center" alignItems="center"
isLoading={ isLoading }
> >
<Icon as={ txIcon } boxSize={ 6 } mr={ 1 }/> <Icon as={ txIcon } boxSize={ 6 } isLoading={ isLoading }/>
<Box w="calc(100% - 36px)" overflow="hidden" whiteSpace="nowrap"><HashStringShorten hash={ item.l2_tx_hash }/></Box> <Skeleton isLoaded={ !isLoading } w="calc(100% - 36px)" overflow="hidden" whiteSpace="nowrap" ml={ 1 }>
<HashStringShorten hash={ item.l2_tx_hash }/>
</Skeleton>
</LinkInternal> </LinkInternal>
</Td> </Td>
<Td verticalAlign="middle" pr={ 12 }> <Td verticalAlign="middle" pr={ 12 }>
<Text variant="secondary">{ timeAgo }</Text> <Skeleton isLoaded={ !isLoading } color="text_secondary" display="inline-block">
<span> { timeAgo }</span>
</Skeleton>
</Td> </Td>
<Td verticalAlign="middle"> <Td verticalAlign="middle">
{ item.status === 'Ready for relay' ? { item.status === 'Ready for relay' ?
<LinkExternal href={ appConfig.L2.withdrawalUrl }>{ item.status }</LinkExternal> : <LinkExternal href={ appConfig.L2.withdrawalUrl }>{ item.status }</LinkExternal> :
<Text>{ item.status }</Text> <Skeleton isLoaded={ !isLoading } display="inline-block">{ item.status }</Skeleton>
} }
</Td> </Td>
<Td verticalAlign="middle"> <Td verticalAlign="middle">
{ item.l1_tx_hash ? ( { item.l1_tx_hash ? (
<LinkExternal <LinkExternal
href={ appConfig.L2.L1BaseUrl + route({ pathname: '/tx/[hash]', query: { hash: item.l1_tx_hash } }) } href={ appConfig.L2.L1BaseUrl + route({ pathname: '/tx/[hash]', query: { hash: item.l1_tx_hash } }) }
isLoading={ isLoading }
display="inline-flex"
> >
<HashStringShorten hash={ item.l1_tx_hash }/> <Skeleton isLoaded={ !isLoading }>
<HashStringShorten hash={ item.l1_tx_hash }/>
</Skeleton>
</LinkExternal> </LinkExternal>
) : ) :
'N/A' 'N/A'
} }
</Td> </Td>
<Td verticalAlign="middle"> <Td verticalAlign="middle">
<Text variant="secondary">{ timeToEnd }</Text> <Skeleton isLoaded={ !isLoading } color="text_secondary" minW="50px" minH="20px" display="inline-block">{ timeToEnd }</Skeleton>
</Td> </Td>
</Tr> </Tr>
); );
......
import { Box, Heading, Icon, IconButton, Image, Link, LinkBox, Text, useColorModeValue } from '@chakra-ui/react'; import { Box, Icon, IconButton, Image, Link, LinkBox, Skeleton, useColorModeValue } from '@chakra-ui/react';
import type { MouseEvent } from 'react'; import type { MouseEvent } from 'react';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
...@@ -14,6 +14,7 @@ interface Props extends MarketplaceAppPreview { ...@@ -14,6 +14,7 @@ interface Props extends MarketplaceAppPreview {
onInfoClick: (id: string) => void; onInfoClick: (id: string) => void;
isFavorite: boolean; isFavorite: boolean;
onFavoriteClick: (id: string, isFavorite: boolean) => void; onFavoriteClick: (id: string, isFavorite: boolean) => void;
isLoading: boolean;
} }
const MarketplaceAppCard = ({ const MarketplaceAppCard = ({
...@@ -28,6 +29,7 @@ const MarketplaceAppCard = ({ ...@@ -28,6 +29,7 @@ const MarketplaceAppCard = ({
onInfoClick, onInfoClick,
isFavorite, isFavorite,
onFavoriteClick, onFavoriteClick,
isLoading,
}: Props) => { }: Props) => {
const categoriesLabel = categories.join(', '); const categoriesLabel = categories.join(', ');
...@@ -41,14 +43,15 @@ const MarketplaceAppCard = ({ ...@@ -41,14 +43,15 @@ const MarketplaceAppCard = ({
}, [ onFavoriteClick, id, isFavorite ]); }, [ onFavoriteClick, id, isFavorite ]);
const logoUrl = useColorModeValue(logo, logoDarkMode || logo); const logoUrl = useColorModeValue(logo, logoDarkMode || logo);
const moreButtonBgGradient = `linear(to-r, ${ useColorModeValue('whiteAlpha.50', 'blackAlpha.50') }, ${ useColorModeValue('white', 'black') } 20%)`;
return ( return (
<LinkBox <LinkBox
_hover={{ _hover={{
boxShadow: 'md', boxShadow: isLoading ? 'none' : 'md',
}} }}
_focusWithin={{ _focusWithin={{
boxShadow: 'md', boxShadow: isLoading ? 'none' : 'md',
}} }}
borderRadius="md" borderRadius="md"
height="100%" height="100%"
...@@ -60,12 +63,13 @@ const MarketplaceAppCard = ({ ...@@ -60,12 +63,13 @@ const MarketplaceAppCard = ({
<Box <Box
display={{ base: 'grid', sm: 'block' }} display={{ base: 'grid', sm: 'block' }}
gridTemplateColumns={{ base: '64px 1fr', sm: '1fr' }} gridTemplateColumns={{ base: '64px 1fr', sm: '1fr' }}
gridTemplateRows={{ base: '20px 20px auto', sm: 'none' }} gridTemplateRows={{ base: 'none', sm: 'none' }}
gridRowGap={{ base: 2, sm: 'none' }} gridRowGap={{ base: 2, sm: 'none' }}
gridColumnGap={{ base: 4, sm: 'none' }} gridColumnGap={{ base: 4, sm: 'none' }}
height="100%" height="100%"
> >
<Box <Skeleton
isLoaded={ !isLoading }
gridRow={{ base: '1 / 4', sm: 'auto' }} gridRow={{ base: '1 / 4', sm: 'auto' }}
marginBottom={ 4 } marginBottom={ 4 }
w={{ base: '64px', sm: '96px' }} w={{ base: '64px', sm: '96px' }}
...@@ -76,17 +80,20 @@ const MarketplaceAppCard = ({ ...@@ -76,17 +80,20 @@ const MarketplaceAppCard = ({
justifyContent="center" justifyContent="center"
> >
<Image <Image
src={ logoUrl } src={ isLoading ? undefined : logoUrl }
alt={ `${ title } app icon` } alt={ `${ title } app icon` }
/> />
</Box> </Skeleton>
<Heading <Skeleton
isLoaded={ !isLoading }
gridColumn={{ base: 2, sm: 'auto' }} gridColumn={{ base: 2, sm: 'auto' }}
as="h3" as="h3"
marginBottom={ 2 } marginBottom={{ base: 0, sm: 2 }}
size={{ base: 'xs', sm: 'sm' }} fontSize={{ base: 'sm', sm: 'lg' }}
fontWeight="semibold" fontWeight="semibold"
fontFamily="heading"
display="inline-block"
> >
<MarketplaceAppCardLink <MarketplaceAppCardLink
id={ id } id={ id }
...@@ -94,68 +101,74 @@ const MarketplaceAppCard = ({ ...@@ -94,68 +101,74 @@ const MarketplaceAppCard = ({
external={ external } external={ external }
title={ title } title={ title }
/> />
</Heading> </Skeleton>
<Text <Skeleton
marginBottom={ 2 } isLoaded={ !isLoading }
variant="secondary" marginBottom={{ base: 0, sm: 2 }}
color="text_secondary"
fontSize="xs" fontSize="xs"
> >
{ categoriesLabel } <span>{ categoriesLabel }</span>
</Text> </Skeleton>
<Text <Skeleton
isLoaded={ !isLoading }
fontSize={{ base: 'xs', sm: 'sm' }} fontSize={{ base: 'xs', sm: 'sm' }}
lineHeight="20px" lineHeight="20px"
noOfLines={ 4 } noOfLines={ 4 }
> >
{ shortDescription } { shortDescription }
</Text> </Skeleton>
<Box { !isLoading && (
position="absolute" <Box
right={{ base: 3, sm: '20px' }} position="absolute"
bottom={{ base: 3, sm: '20px' }} right={{ base: 3, sm: '20px' }}
paddingLeft={ 8 } bottom={{ base: 3, sm: '20px' }}
bgGradient={ `linear(to-r, ${ useColorModeValue('whiteAlpha.50', 'blackAlpha.50') }, ${ useColorModeValue('white', 'black') } 20%)` } paddingLeft={ 8 }
> bgGradient={ moreButtonBgGradient }
<Link
fontSize={{ base: 'xs', sm: 'sm' }}
display="flex"
alignItems="center"
paddingRight={{ sm: 2 }}
maxW="100%"
overflow="hidden"
href="#"
onClick={ handleInfoClick }
> >
<Link
fontSize={{ base: 'xs', sm: 'sm' }}
display="flex"
alignItems="center"
paddingRight={{ sm: 2 }}
maxW="100%"
overflow="hidden"
href="#"
onClick={ handleInfoClick }
>
More More
<Icon <Icon
as={ northEastIcon } as={ northEastIcon }
marginLeft={ 1 } marginLeft={ 1 }
/> />
</Link> </Link>
</Box> </Box>
) }
<IconButton
display={{ base: 'block', sm: isFavorite ? 'block' : 'none' }} { !isLoading && (
_groupHover={{ display: 'block' }} <IconButton
position="absolute" display={{ base: 'block', sm: isFavorite ? 'block' : 'none' }}
right={{ base: 3, sm: '10px' }} _groupHover={{ display: 'block' }}
top={{ base: 3, sm: '14px' }} position="absolute"
aria-label="Mark as favorite" right={{ base: 3, sm: '10px' }}
title="Mark as favorite" top={{ base: 3, sm: '14px' }}
variant="ghost" aria-label="Mark as favorite"
colorScheme="gray" title="Mark as favorite"
w={ 9 } variant="ghost"
h={ 8 } colorScheme="gray"
onClick={ handleFavoriteClick } w={ 9 }
icon={ isFavorite ? h={ 8 }
<Icon as={ starFilledIcon } w={ 4 } h={ 4 } color="yellow.400"/> : onClick={ handleFavoriteClick }
<Icon as={ starOutlineIcon } w={ 4 } h={ 4 } color="gray.300"/> icon={ isFavorite ?
} <Icon as={ starFilledIcon } w={ 4 } h={ 4 } color="yellow.400"/> :
/> <Icon as={ starOutlineIcon } w={ 4 } h={ 4 } color="gray.300"/>
}
/>
) }
</Box> </Box>
</LinkBox> </LinkBox>
); );
......
import { Box, Heading, Skeleton, SkeletonCircle, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
const MarketplaceAppCardSkeleton = () => {
return (
<Box
borderRadius="md"
height="100%"
padding={{ base: 3, sm: '20px' }}
border="1px"
borderColor={ useColorModeValue('gray.200', 'gray.600') }
>
<Box
display={{ base: 'grid', sm: 'block' }}
gridTemplateColumns={{ base: '64px 1fr', sm: '1fr' }}
gridTemplateRows={{ base: '20px 20px auto', sm: 'none' }}
gridRowGap={{ base: 2, sm: 'none' }}
gridColumnGap={{ base: 4, sm: 'none' }}
height="100%"
>
<Box
gridRow={{ base: '1 / 4', sm: 'auto' }}
marginBottom={ 4 }
w={{ base: '64px', sm: '96px' }}
h={{ base: '64px', sm: '96px' }}
>
<SkeletonCircle w="100%" h="100%"/>
</Box>
<Heading
gridColumn={{ base: 2, sm: 'auto' }}
marginBottom={ 2 }
>
<Skeleton h={ 4 } w="50%"/>
</Heading>
<Box>
<Skeleton h={ 4 } mb={ 1 }/>
<Skeleton h={ 4 } mb={ 1 }/>
<Skeleton h={ 4 } w="50%"/>
</Box>
</Box>
</Box>
);
};
export default MarketplaceAppCardSkeleton;
import { Box, Button, Icon, Menu, MenuButton, MenuList } from '@chakra-ui/react'; import { Box, Button, Icon, Menu, MenuButton, MenuList, Skeleton } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import { MarketplaceCategory } from 'types/client/marketplace'; import { MarketplaceCategory } from 'types/client/marketplace';
...@@ -11,15 +11,28 @@ type Props = { ...@@ -11,15 +11,28 @@ type Props = {
categories: Array<string>; categories: Array<string>;
selectedCategoryId: string; selectedCategoryId: string;
onSelect: (category: string) => void; onSelect: (category: string) => void;
isLoading: boolean;
} }
const MarketplaceCategoriesMenu = ({ selectedCategoryId, onSelect, categories }: Props) => { const MarketplaceCategoriesMenu = ({ selectedCategoryId, onSelect, categories, isLoading }: Props) => {
const options = React.useMemo(() => ([ const options = React.useMemo(() => ([
MarketplaceCategory.FAVORITES, MarketplaceCategory.FAVORITES,
MarketplaceCategory.ALL, MarketplaceCategory.ALL,
...categories, ...categories,
]), [ categories ]); ]), [ categories ]);
if (isLoading) {
return (
<Skeleton
h="40px"
w={{ base: '100%', sm: '120px' }}
borderRadius="base"
mb={{ base: 2, sm: 0 }}
mr={{ base: 0, sm: 2 }}
/>
);
}
return ( return (
<Menu> <Menu>
<MenuButton <MenuButton
......
import { Grid, GridItem } from '@chakra-ui/react'; import { Grid } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { MarketplaceAppPreview } from 'types/client/marketplace'; import type { MarketplaceAppPreview } from 'types/client/marketplace';
...@@ -13,9 +13,10 @@ type Props = { ...@@ -13,9 +13,10 @@ type Props = {
onAppClick: (id: string) => void; onAppClick: (id: string) => void;
favoriteApps: Array<string>; favoriteApps: Array<string>;
onFavoriteClick: (id: string, isFavorite: boolean) => void; onFavoriteClick: (id: string, isFavorite: boolean) => void;
isLoading: boolean;
} }
const MarketplaceList = ({ apps, onAppClick, favoriteApps, onFavoriteClick }: Props) => { const MarketplaceList = ({ apps, onAppClick, favoriteApps, onFavoriteClick, isLoading }: Props) => {
return apps.length > 0 ? ( return apps.length > 0 ? (
<Grid <Grid
templateColumns={{ templateColumns={{
...@@ -25,24 +26,22 @@ const MarketplaceList = ({ apps, onAppClick, favoriteApps, onFavoriteClick }: Pr ...@@ -25,24 +26,22 @@ const MarketplaceList = ({ apps, onAppClick, favoriteApps, onFavoriteClick }: Pr
autoRows="1fr" autoRows="1fr"
gap={{ base: '16px', sm: '24px' }} gap={{ base: '16px', sm: '24px' }}
> >
{ apps.map((app) => ( { apps.map((app, index) => (
<GridItem <MarketplaceAppCard
key={ app.id } key={ app.id + (isLoading ? index : '') }
> onInfoClick={ onAppClick }
<MarketplaceAppCard id={ app.id }
onInfoClick={ onAppClick } external={ app.external }
id={ app.id } url={ app.url }
external={ app.external } title={ app.title }
url={ app.url } logo={ app.logo }
title={ app.title } logoDarkMode={ app.logoDarkMode }
logo={ app.logo } shortDescription={ app.shortDescription }
logoDarkMode={ app.logoDarkMode } categories={ app.categories }
shortDescription={ app.shortDescription } isFavorite={ favoriteApps.includes(app.id) }
categories={ app.categories } onFavoriteClick={ onFavoriteClick }
isFavorite={ favoriteApps.includes(app.id) } isLoading={ isLoading }
onFavoriteClick={ onFavoriteClick } />
/>
</GridItem>
)) } )) }
</Grid> </Grid>
) : ( ) : (
......
import { Grid, GridItem } from '@chakra-ui/react';
import React from 'react';
import MarketplaceAppCardSkeleton from './MarketplaceAppCardSkeleton';
const applicationStubs = [ ...Array(12) ];
const MarketplaceListSkeleton = () => {
return (
<Grid
templateColumns={{
sm: 'repeat(auto-fill, minmax(170px, 1fr))',
lg: 'repeat(auto-fill, minmax(260px, 1fr))',
}}
autoRows="1fr"
gap={{ base: '16px', sm: '24px' }}
>
{ applicationStubs.map((app, index) => (
<GridItem
key={ index }
>
<MarketplaceAppCardSkeleton/>
</GridItem>
)) }
</Grid>
);
};
export default MarketplaceListSkeleton;
...@@ -12,6 +12,7 @@ import type { ResourceError } from 'lib/api/resources'; ...@@ -12,6 +12,7 @@ import type { ResourceError } from 'lib/api/resources';
import useDebounce from 'lib/hooks/useDebounce'; import useDebounce from 'lib/hooks/useDebounce';
import useApiFetch from 'lib/hooks/useFetch'; import useApiFetch from 'lib/hooks/useFetch';
import getQueryParamString from 'lib/router/getQueryParamString'; import getQueryParamString from 'lib/router/getQueryParamString';
import { MARKETPLACE_APP } from 'stubs/marketplace';
const favoriteAppsLocalStorageKey = 'favoriteApps'; const favoriteAppsLocalStorageKey = 'favoriteApps';
...@@ -44,11 +45,12 @@ export default function useMarketplace() { ...@@ -44,11 +45,12 @@ export default function useMarketplace() {
const [ favoriteApps, setFavoriteApps ] = React.useState<Array<string>>([]); const [ favoriteApps, setFavoriteApps ] = React.useState<Array<string>>([]);
const apiFetch = useApiFetch(); const apiFetch = useApiFetch();
const { isLoading, isError, error, data } = useQuery<unknown, ResourceError<unknown>, Array<MarketplaceAppOverview>>( const { isPlaceholderData, isError, error, data } = useQuery<unknown, ResourceError<unknown>, Array<MarketplaceAppOverview>>(
[ 'marketplace-apps' ], [ 'marketplace-apps' ],
async() => apiFetch(appConfig.marketplaceConfigUrl || ''), async() => apiFetch(appConfig.marketplaceConfigUrl || ''),
{ {
select: (data) => (data as Array<MarketplaceAppOverview>).sort((a, b) => a.title.localeCompare(b.title)), select: (data) => (data as Array<MarketplaceAppOverview>).sort((a, b) => a.title.localeCompare(b.title)),
placeholderData: Array(9).fill(MARKETPLACE_APP),
staleTime: Infinity, staleTime: Infinity,
}); });
...@@ -90,13 +92,13 @@ export default function useMarketplace() { ...@@ -90,13 +92,13 @@ export default function useMarketplace() {
}, [ ]); }, [ ]);
React.useEffect(() => { React.useEffect(() => {
if (!isLoading && !isError) { if (!isPlaceholderData && !isError) {
const isValidDefaultCategory = categories.includes(defaultCategoryId); const isValidDefaultCategory = categories.includes(defaultCategoryId);
isValidDefaultCategory && setSelectedCategoryId(defaultCategoryId); isValidDefaultCategory && setSelectedCategoryId(defaultCategoryId);
} }
// run only when data is loaded // run only when data is loaded
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [ isLoading ]); }, [ isPlaceholderData ]);
React.useEffect(() => { React.useEffect(() => {
const query = _pickBy({ const query = _pickBy({
...@@ -118,7 +120,7 @@ export default function useMarketplace() { ...@@ -118,7 +120,7 @@ export default function useMarketplace() {
onCategoryChange: handleCategoryChange, onCategoryChange: handleCategoryChange,
filterQuery: debouncedFilterQuery, filterQuery: debouncedFilterQuery,
onSearchInputChange: setFilterQuery, onSearchInputChange: setFilterQuery,
isLoading, isPlaceholderData,
isError, isError,
error, error,
categories, categories,
...@@ -139,7 +141,7 @@ export default function useMarketplace() { ...@@ -139,7 +141,7 @@ export default function useMarketplace() {
handleCategoryChange, handleCategoryChange,
handleFavoriteClick, handleFavoriteClick,
isError, isError,
isLoading, isPlaceholderData,
showAppInfo, showAppInfo,
debouncedFilterQuery, debouncedFilterQuery,
]); ]);
......
...@@ -71,9 +71,7 @@ const Accounts = () => { ...@@ -71,9 +71,7 @@ const Accounts = () => {
<PageTitle title="Top accounts" withTextAd/> <PageTitle title="Top accounts" withTextAd/>
<DataListDisplay <DataListDisplay
isError={ isError } isError={ isError }
isLoading={ false }
items={ data?.items } items={ data?.items }
skeletonProps={{ skeletonDesktopColumns: [ '64px', '30%', '20%', '20%', '15%', '15%' ] }}
emptyText="There are no accounts." emptyText="There are no accounts."
content={ content } content={ content }
actionBar={ actionBar } actionBar={ actionBar }
......
...@@ -27,8 +27,8 @@ import TextAd from 'ui/shared/ad/TextAd'; ...@@ -27,8 +27,8 @@ 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 PageTitle from 'ui/shared/Page/PageTitle'; import PageTitle from 'ui/shared/Page/PageTitle';
import SkeletonTabs from 'ui/shared/skeletons/SkeletonTabs';
import RoutedTabs from 'ui/shared/Tabs/RoutedTabs'; import RoutedTabs from 'ui/shared/Tabs/RoutedTabs';
import TabsSkeleton from 'ui/shared/Tabs/TabsSkeleton';
export const tokenTabsByType: Record<TokenType, string> = { export const tokenTabsByType: Record<TokenType, string> = {
'ERC-20': 'tokens_erc20', 'ERC-20': 'tokens_erc20',
...@@ -134,7 +134,7 @@ const AddressPageContent = () => { ...@@ -134,7 +134,7 @@ const AddressPageContent = () => {
<AddressDetails addressQuery={ addressQuery } scrollRef={ tabsScrollRef }/> <AddressDetails addressQuery={ addressQuery } scrollRef={ tabsScrollRef }/>
{ /* should stay before tabs to scroll up with pagination */ } { /* should stay before tabs to scroll up with pagination */ }
<Box ref={ tabsScrollRef }></Box> <Box ref={ tabsScrollRef }></Box>
{ addressQuery.isPlaceholderData ? <SkeletonTabs tabs={ tabs }/> : content } { addressQuery.isPlaceholderData ? <TabsSkeleton tabs={ tabs }/> : content }
</> </>
); );
}; };
......
...@@ -20,8 +20,8 @@ import NetworkExplorers from 'ui/shared/NetworkExplorers'; ...@@ -20,8 +20,8 @@ import NetworkExplorers from 'ui/shared/NetworkExplorers';
import PageTitle from 'ui/shared/Page/PageTitle'; import PageTitle from 'ui/shared/Page/PageTitle';
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';
import SkeletonTabs from 'ui/shared/skeletons/SkeletonTabs';
import RoutedTabs from 'ui/shared/Tabs/RoutedTabs'; import RoutedTabs from 'ui/shared/Tabs/RoutedTabs';
import TabsSkeleton from 'ui/shared/Tabs/TabsSkeleton';
import TxsContent from 'ui/txs/TxsContent'; import TxsContent from 'ui/txs/TxsContent';
const TAB_LIST_PROPS = { const TAB_LIST_PROPS = {
...@@ -120,7 +120,7 @@ const BlockPageContent = () => { ...@@ -120,7 +120,7 @@ const BlockPageContent = () => {
contentAfter={ <NetworkExplorers type="block" pathParam={ heightOrHash } ml={{ base: 'initial', lg: 'auto' }}/> } contentAfter={ <NetworkExplorers type="block" pathParam={ heightOrHash } ml={{ base: 'initial', lg: 'auto' }}/> }
isLoading={ blockQuery.isPlaceholderData } isLoading={ blockQuery.isPlaceholderData }
/> />
{ blockQuery.isPlaceholderData ? <SkeletonTabs tabs={ tabs }/> : ( { blockQuery.isPlaceholderData ? <TabsSkeleton tabs={ tabs }/> : (
<RoutedTabs <RoutedTabs
tabs={ tabs } tabs={ tabs }
tabListProps={ isMobile ? undefined : TAB_LIST_PROPS } tabListProps={ isMobile ? undefined : TAB_LIST_PROPS }
......
...@@ -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();
}); });
import { Flex, Hide, Show, Skeleton, Text } from '@chakra-ui/react'; import { Box, Hide, Show, 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 useQueryWithPages from 'lib/hooks/useQueryWithPages'; import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import { rightLineArrow, nbsp } from 'lib/html-entities'; import { rightLineArrow, nbsp } from 'lib/html-entities';
import { L2_DEPOSIT_ITEM } from 'stubs/L2';
import { generateListStub } from 'stubs/utils';
import DepositsListItem from 'ui/l2Deposits/DepositsListItem'; import DepositsListItem from 'ui/l2Deposits/DepositsListItem';
import DepositsTable from 'ui/l2Deposits/DepositsTable'; import DepositsTable from 'ui/l2Deposits/DepositsTable';
import ActionBar from 'ui/shared/ActionBar'; import ActionBar from 'ui/shared/ActionBar';
import DataListDisplay from 'ui/shared/DataListDisplay'; import DataListDisplay from 'ui/shared/DataListDisplay';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle'; import PageTitle from 'ui/shared/Page/PageTitle';
import Pagination from 'ui/shared/Pagination'; import Pagination from 'ui/shared/Pagination';
const L2Deposits = () => { const L2Deposits = () => {
const isMobile = useIsMobile(); const { data, isError, isPlaceholderData, isPaginationVisible, pagination } = useQueryWithPages({
const { data, isError, isLoading, isPaginationVisible, pagination } = useQueryWithPages({
resourceName: 'l2_deposits', resourceName: 'l2_deposits',
options: {
placeholderData: generateListStub<'l2_deposits'>(
L2_DEPOSIT_ITEM,
50,
{
next_page_params: {
items_count: 50,
l1_block_number: 9045200,
tx_hash: '',
},
},
),
},
}); });
const countersQuery = useApiQuery('l2_deposits_count'); const countersQuery = useApiQuery('l2_deposits_count', {
queryOptions: {
placeholderData: 1927029,
},
});
const content = data?.items ? ( const content = data?.items ? (
<> <>
<Show below="lg" ssr={ false }>{ data.items.map((item => <DepositsListItem key={ item.l2_tx_hash } item={ item }/>)) }</Show> <Show below="lg" ssr={ false }>
<Hide below="lg" ssr={ false }><DepositsTable items={ data.items } top={ isPaginationVisible ? 80 : 0 }/></Hide> { data.items.map(((item, index) => (
<DepositsListItem
key={ item.l2_tx_hash + (isPlaceholderData ? index : '') }
isLoading={ isPlaceholderData }
item={ item }
/>
))) }
</Show>
<Hide below="lg" ssr={ false }>
<DepositsTable items={ data.items } top={ isPaginationVisible ? 80 : 0 } isLoading={ isPlaceholderData }/>
</Hide>
</> </>
) : null; ) : null;
const text = (() => { const text = (() => {
if (countersQuery.isLoading) {
return (
<Skeleton
w={{ base: '100%', lg: '320px' }}
h="24px"
mb={{ base: 7, lg: isPaginationVisible ? 0 : 7 }}
mt={{ base: 1, lg: isPaginationVisible ? 0 : 7 }}
/>
);
}
if (countersQuery.isError) { if (countersQuery.isError) {
return null; return null;
} }
return ( return (
<Text mb={{ base: 6, lg: isPaginationVisible ? 0 : 6 }} lineHeight={{ base: '24px', lg: '32px' }}> <Skeleton
A total of { countersQuery.data.toLocaleString() } deposits found isLoaded={ !countersQuery.isPlaceholderData }
</Text> display="inline-block"
>
A total of { countersQuery.data?.toLocaleString() } deposits found
</Skeleton>
); );
})(); })();
const actionBar = ( const actionBar = (
<> <>
{ (isMobile || !isPaginationVisible) && text } <Box mb={ 6 } display={{ base: 'block', lg: 'none' }}>
{ isPaginationVisible && ( { text }
<ActionBar mt={ -6 }> </Box>
<Flex alignItems="center" justifyContent="space-between" w="100%"> <ActionBar mt={ -6 }>
{ !isMobile && text } <Box display={{ base: 'none', lg: 'block' }}>
<Pagination ml="auto" { ...pagination }/> { text }
</Flex> </Box>
</ActionBar> { isPaginationVisible && <Pagination ml="auto" { ...pagination }/> }
) } </ActionBar>
</> </>
); );
return ( return (
<Page> <>
<PageTitle title={ `Deposits (L1${ nbsp }${ rightLineArrow }${ nbsp }L2)` } withTextAd/> <PageTitle title={ `Deposits (L1${ nbsp }${ rightLineArrow }${ nbsp }L2)` } withTextAd/>
<DataListDisplay <DataListDisplay
isError={ isError } isError={ isError }
isLoading={ isLoading }
items={ data?.items } items={ data?.items }
skeletonProps={{ skeletonDesktopColumns: Array(7).fill(`${ 100 / 7 }%`), skeletonDesktopMinW: '950px' }}
emptyText="There are no withdrawals." emptyText="There are no withdrawals."
content={ content } content={ content }
actionBar={ actionBar } actionBar={ actionBar }
/> />
</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();
}); });
import { Flex, Hide, Show, Skeleton, Text } from '@chakra-ui/react'; import { Box, Hide, Show, Skeleton, Text } 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 useQueryWithPages from 'lib/hooks/useQueryWithPages'; import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import { L2_OUTPUT_ROOTS_ITEM } from 'stubs/L2';
import { generateListStub } from 'stubs/utils';
import OutputRootsListItem from 'ui/l2OutputRoots/OutputRootsListItem'; import OutputRootsListItem from 'ui/l2OutputRoots/OutputRootsListItem';
import OutputRootsTable from 'ui/l2OutputRoots/OutputRootsTable'; import OutputRootsTable from 'ui/l2OutputRoots/OutputRootsTable';
import ActionBar from 'ui/shared/ActionBar'; import ActionBar from 'ui/shared/ActionBar';
import DataListDisplay from 'ui/shared/DataListDisplay'; import DataListDisplay from 'ui/shared/DataListDisplay';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle'; import PageTitle from 'ui/shared/Page/PageTitle';
import Pagination from 'ui/shared/Pagination'; import Pagination from 'ui/shared/Pagination';
const L2OutputRoots = () => { const L2OutputRoots = () => {
const isMobile = useIsMobile(); const { data, isError, isPlaceholderData, isPaginationVisible, pagination } = useQueryWithPages({
const { data, isError, isLoading, isPaginationVisible, pagination } = useQueryWithPages({
resourceName: 'l2_output_roots', resourceName: 'l2_output_roots',
options: {
placeholderData: generateListStub<'l2_output_roots'>(
L2_OUTPUT_ROOTS_ITEM,
50,
{
next_page_params: {
items_count: 50,
index: 9045200,
},
},
),
},
}); });
const countersQuery = useApiQuery('l2_output_roots_count'); const countersQuery = useApiQuery('l2_output_roots_count', {
queryOptions: {
placeholderData: 50617,
},
});
const content = data?.items ? ( const content = data?.items ? (
<> <>
<Show below="lg" ssr={ false }>{ data.items.map((item => <OutputRootsListItem key={ item.l2_output_index } item={ item }/>)) }</Show> <Show below="lg" ssr={ false }>
<Hide below="lg" ssr={ false }><OutputRootsTable items={ data.items } top={ isPaginationVisible ? 80 : 0 }/></Hide> { data.items.map(((item, index) => (
<OutputRootsListItem
key={ item.l2_output_index + (isPlaceholderData ? String(index) : '') }
item={ item }
isLoading={ isPlaceholderData }
/>
))) }
</Show>
<Hide below="lg" ssr={ false }>
<OutputRootsTable items={ data.items } top={ isPaginationVisible ? 80 : 0 } isLoading={ isPlaceholderData }/>
</Hide>
</> </>
) : null; ) : null;
const text = (() => { const text = (() => {
if (countersQuery.isLoading || isLoading) { if (countersQuery.isError || isError || !data?.items.length) {
return (
<Skeleton
w={{ base: '100%', lg: '400px' }}
h={{ base: '48px', lg: '24px' }}
mb={{ base: 6, lg: isPaginationVisible ? 0 : 7 }}
mt={{ base: 0, lg: isPaginationVisible ? 0 : 7 }}
/>
);
}
if (countersQuery.isError || isError || data?.items.length === 0) {
return null; return null;
} }
return ( return (
<Flex mb={{ base: 6, lg: isPaginationVisible ? 0 : 6 }} flexWrap="wrap"> <Skeleton isLoaded={ !countersQuery.isPlaceholderData && !isPlaceholderData } display="flex" flexWrap="wrap">
L2 output index L2 output index
<Text fontWeight={ 600 } whiteSpace="pre"> #{ data.items[0].l2_output_index } </Text>to <Text fontWeight={ 600 } whiteSpace="pre"> #{ data.items[0].l2_output_index } </Text>to
<Text fontWeight={ 600 } whiteSpace="pre"> #{ data.items[data.items.length - 1].l2_output_index } </Text> <Text fontWeight={ 600 } whiteSpace="pre"> #{ data.items[data.items.length - 1].l2_output_index } </Text>
(total of { countersQuery.data.toLocaleString() } roots) (total of { countersQuery.data?.toLocaleString() } roots)
</Flex> </Skeleton>
); );
})(); })();
const actionBar = ( const actionBar = (
<> <>
{ (isMobile || !isPaginationVisible) && text } <Box mb={ 6 } display={{ base: 'block', lg: 'none' }}>
{ isPaginationVisible && ( { text }
<ActionBar mt={ -6 }> </Box>
<Flex alignItems="center" justifyContent="space-between" w="100%"> <ActionBar mt={ -6 } alignItems="center">
{ !isMobile && text } <Box display={{ base: 'none', lg: 'block' }}>
<Pagination ml="auto" { ...pagination }/> { text }
</Flex> </Box>
</ActionBar> { isPaginationVisible && <Pagination ml="auto" { ...pagination }/> }
) } </ActionBar>
</> </>
); );
return ( return (
<Page> <>
<PageTitle title="Output roots" withTextAd/> <PageTitle title="Output roots" withTextAd/>
<DataListDisplay <DataListDisplay
isError={ isError } isError={ isError }
isLoading={ isLoading }
items={ data?.items } items={ data?.items }
skeletonProps={{ skeletonDesktopColumns: [ '140px', '20%', '20%', '30%', '30%' ] }}
emptyText="There are no output roots." emptyText="There are no output roots."
content={ content } content={ content }
actionBar={ actionBar } actionBar={ actionBar }
/> />
</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();
}); });
import { Flex, Hide, Show, Skeleton, Text } from '@chakra-ui/react'; import { Box, Hide, Show, Skeleton, Text } 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 useQueryWithPages from 'lib/hooks/useQueryWithPages'; import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import { nbsp } from 'lib/html-entities'; import { nbsp } from 'lib/html-entities';
import { L2_TXN_BATCHES_ITEM } from 'stubs/L2';
import { generateListStub } from 'stubs/utils';
import TxnBatchesListItem from 'ui/l2TxnBatches/TxnBatchesListItem'; import TxnBatchesListItem from 'ui/l2TxnBatches/TxnBatchesListItem';
import TxnBatchesTable from 'ui/l2TxnBatches/TxnBatchesTable'; import TxnBatchesTable from 'ui/l2TxnBatches/TxnBatchesTable';
import ActionBar from 'ui/shared/ActionBar'; import ActionBar from 'ui/shared/ActionBar';
import DataListDisplay from 'ui/shared/DataListDisplay'; import DataListDisplay from 'ui/shared/DataListDisplay';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle'; import PageTitle from 'ui/shared/Page/PageTitle';
import Pagination from 'ui/shared/Pagination'; import Pagination from 'ui/shared/Pagination';
const L2TxnBatches = () => { const L2TxnBatches = () => {
const isMobile = useIsMobile(); const { data, isError, isPlaceholderData, isPaginationVisible, pagination } = useQueryWithPages({
const { data, isError, isLoading, isPaginationVisible, pagination } = useQueryWithPages({
resourceName: 'l2_txn_batches', resourceName: 'l2_txn_batches',
options: {
placeholderData: generateListStub<'l2_txn_batches'>(
L2_TXN_BATCHES_ITEM,
50,
{
next_page_params: {
items_count: 50,
block_number: 9045200,
},
},
),
},
}); });
const countersQuery = useApiQuery('l2_txn_batches_count'); const countersQuery = useApiQuery('l2_txn_batches_count', {
queryOptions: {
placeholderData: 5231746,
},
});
const content = data?.items ? ( const content = data?.items ? (
<> <>
<Show below="lg" ssr={ false }>{ data.items.map((item => <TxnBatchesListItem key={ item.l2_block_number } item={ item }/>)) }</Show> <Show below="lg" ssr={ false }>
<Hide below="lg" ssr={ false }><TxnBatchesTable items={ data.items } top={ isPaginationVisible ? 80 : 0 }/></Hide> { data.items.map(((item, index) => (
<TxnBatchesListItem
key={ item.l2_block_number + (isPlaceholderData ? String(index) : '') }
item={ item }
isLoading={ isPlaceholderData }
/>
))) }
</Show>
<Hide below="lg" ssr={ false }><TxnBatchesTable items={ data.items } top={ isPaginationVisible ? 80 : 0 } isLoading={ isPlaceholderData }/></Hide>
</> </>
) : null; ) : null;
const text = (() => { const text = (() => {
if (countersQuery.isLoading || isLoading) { if (countersQuery.isError || isError || !data?.items.length) {
return (
<Skeleton
w={{ base: '100%', lg: '400px' }}
h={{ base: '48px', lg: '24px' }}
mb={{ base: 6, lg: isPaginationVisible ? 0 : 7 }}
mt={{ base: 0, lg: isPaginationVisible ? 0 : 7 }}
/>
);
}
if (countersQuery.isError || isError || data.items.length === 0) {
return null; return null;
} }
return ( return (
<Flex mb={{ base: 6, lg: isPaginationVisible ? 0 : 6 }} flexWrap="wrap"> <Skeleton isLoaded={ !countersQuery.isPlaceholderData && !isPlaceholderData } display="flex" flexWrap="wrap">
Tx batch (L2 block) Tx batch (L2 block)
<Text fontWeight={ 600 } whiteSpace="pre"> #{ data.items[0].l2_block_number } </Text>to <Text fontWeight={ 600 } whiteSpace="pre"> #{ data.items[0].l2_block_number } </Text>to
<Text fontWeight={ 600 } whiteSpace="pre"> #{ data.items[data.items.length - 1].l2_block_number } </Text> <Text fontWeight={ 600 } whiteSpace="pre"> #{ data.items[data.items.length - 1].l2_block_number } </Text>
(total of { countersQuery.data.toLocaleString() } batches) (total of { countersQuery.data?.toLocaleString() } batches)
</Flex> </Skeleton>
); );
})(); })();
const actionBar = ( const actionBar = (
<> <>
{ (isMobile || !isPaginationVisible) && text } <Box mb={ 6 } display={{ base: 'block', lg: 'none' }}>
{ isPaginationVisible && ( { text }
<ActionBar mt={ -6 }> </Box>
<Flex alignItems="center" justifyContent="space-between" w="100%"> <ActionBar mt={ -6 } alignItems="center">
{ !isMobile && text } <Box display={{ base: 'none', lg: 'block' }}>
<Pagination ml="auto" { ...pagination }/> { text }
</Flex> </Box>
</ActionBar> { isPaginationVisible && <Pagination ml="auto" { ...pagination }/> }
) } </ActionBar>
</> </>
); );
return ( return (
<Page> <>
<PageTitle title={ `Tx batches (L2${ nbsp }blocks)` } withTextAd/> <PageTitle title={ `Tx batches (L2${ nbsp }blocks)` } withTextAd/>
<DataListDisplay <DataListDisplay
isError={ isError } isError={ isError }
isLoading={ isLoading }
items={ data?.items } items={ data?.items }
skeletonProps={{ skeletonDesktopColumns: [ '170px', '170px', '160px', '100%', '150px' ] }}
emptyText="There are no tx batches." emptyText="There are no tx batches."
content={ content } content={ content }
actionBar={ actionBar } actionBar={ actionBar }
/> />
</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();
}); });
import { Flex, Hide, Show, Skeleton, Text } from '@chakra-ui/react'; import { Box, Hide, Show, 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 useQueryWithPages from 'lib/hooks/useQueryWithPages'; import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import { rightLineArrow, nbsp } from 'lib/html-entities'; import { rightLineArrow, nbsp } from 'lib/html-entities';
import { L2_WITHDRAWAL_ITEM } from 'stubs/L2';
import { generateListStub } from 'stubs/utils';
import WithdrawalsListItem from 'ui/l2Withdrawals/WithdrawalsListItem'; import WithdrawalsListItem from 'ui/l2Withdrawals/WithdrawalsListItem';
import WithdrawalsTable from 'ui/l2Withdrawals/WithdrawalsTable'; import WithdrawalsTable from 'ui/l2Withdrawals/WithdrawalsTable';
import ActionBar from 'ui/shared/ActionBar'; import ActionBar from 'ui/shared/ActionBar';
import DataListDisplay from 'ui/shared/DataListDisplay'; import DataListDisplay from 'ui/shared/DataListDisplay';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle'; import PageTitle from 'ui/shared/Page/PageTitle';
import Pagination from 'ui/shared/Pagination'; import Pagination from 'ui/shared/Pagination';
const L2Withdrawals = () => { const L2Withdrawals = () => {
const isMobile = useIsMobile(); const { data, isError, isPlaceholderData, isPaginationVisible, pagination } = useQueryWithPages({
const { data, isError, isLoading, isPaginationVisible, pagination } = useQueryWithPages({
resourceName: 'l2_withdrawals', resourceName: 'l2_withdrawals',
options: {
placeholderData: generateListStub<'l2_withdrawals'>(
L2_WITHDRAWAL_ITEM,
50,
{
next_page_params: {
items_count: 50,
nonce: '',
},
},
),
},
}); });
const countersQuery = useApiQuery('l2_withdrawals_count'); const countersQuery = useApiQuery('l2_withdrawals_count', {
queryOptions: {
placeholderData: 23700,
},
});
const content = data?.items ? ( const content = data?.items ? (
<> <>
<Show below="lg" ssr={ false }>{ data.items.map((item => <WithdrawalsListItem key={ item.l2_tx_hash } item={ item }/>)) }</Show> <Show below="lg" ssr={ false }>{ data.items.map(((item, index) => (
<Hide below="lg" ssr={ false }><WithdrawalsTable items={ data.items } top={ isPaginationVisible ? 80 : 0 }/></Hide> <WithdrawalsListItem
key={ item.l2_tx_hash + (isPlaceholderData ? index : '') }
item={ item }
isLoading={ isPlaceholderData }
/>
))) }</Show>
<Hide below="lg" ssr={ false }>
<WithdrawalsTable items={ data.items } top={ isPaginationVisible ? 80 : 0 } isLoading={ isPlaceholderData }/>
</Hide>
</> </>
) : null; ) : null;
const text = (() => { const text = (() => {
if (countersQuery.isLoading) {
return (
<Skeleton
w={{ base: '100%', lg: '320px' }}
h="24px"
mb={{ base: 6, lg: isPaginationVisible ? 0 : 7 }}
mt={{ base: 0, lg: isPaginationVisible ? 0 : 7 }}
/>
);
}
if (countersQuery.isError) { if (countersQuery.isError) {
return null; return null;
} }
return ( return (
<Text mb={{ base: 6, lg: isPaginationVisible ? 0 : 6 }} lineHeight={{ base: '24px', lg: '32px' }}> <Skeleton
A total of { countersQuery.data.toLocaleString() } withdrawals found isLoaded={ !countersQuery.isPlaceholderData }
</Text> display="inline-block"
>
A total of { countersQuery.data?.toLocaleString() } withdrawals found
</Skeleton>
); );
})(); })();
const actionBar = ( const actionBar = (
<> <>
{ (isMobile || !isPaginationVisible) && text } <Box mb={ 6 } display={{ base: 'block', lg: 'none' }}>
{ isPaginationVisible && ( { text }
<ActionBar mt={ -6 }> </Box>
<Flex alignItems="center" justifyContent="space-between" w="100%"> <ActionBar mt={ -6 }>
{ !isMobile && text } <Box display={{ base: 'none', lg: 'block' }}>
<Pagination ml="auto" { ...pagination }/> { text }
</Flex> </Box>
</ActionBar> { isPaginationVisible && <Pagination ml="auto" { ...pagination }/> }
) } </ActionBar>
</> </>
); );
return ( return (
<Page> <>
<PageTitle title={ `Withdrawals (L2${ nbsp }${ rightLineArrow }${ nbsp }L1)` } withTextAd/> <PageTitle title={ `Withdrawals (L2${ nbsp }${ rightLineArrow }${ nbsp }L1)` } withTextAd/>
<DataListDisplay <DataListDisplay
isError={ isError } isError={ isError }
isLoading={ isLoading }
items={ data?.items } items={ data?.items }
skeletonProps={{ skeletonDesktopColumns: Array(7).fill(`${ 100 / 7 }%`), skeletonDesktopMinW: '950px' }}
emptyText="There are no withdrawals." emptyText="There are no withdrawals."
content={ content } content={ content }
actionBar={ actionBar } actionBar={ actionBar }
/> />
</Page> </>
); );
}; };
......
import { Box, Icon, Link } from '@chakra-ui/react'; import { Box, Icon, Link, Skeleton } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import config from 'configs/app/config'; import config from 'configs/app/config';
...@@ -6,14 +6,13 @@ import PlusIcon from 'icons/plus.svg'; ...@@ -6,14 +6,13 @@ import PlusIcon from 'icons/plus.svg';
import MarketplaceAppModal from 'ui/marketplace/MarketplaceAppModal'; import MarketplaceAppModal from 'ui/marketplace/MarketplaceAppModal';
import MarketplaceCategoriesMenu from 'ui/marketplace/MarketplaceCategoriesMenu'; import MarketplaceCategoriesMenu from 'ui/marketplace/MarketplaceCategoriesMenu';
import MarketplaceList from 'ui/marketplace/MarketplaceList'; import MarketplaceList from 'ui/marketplace/MarketplaceList';
import MarketplaceListSkeleton from 'ui/marketplace/MarketplaceListSkeleton';
import FilterInput from 'ui/shared/filters/FilterInput'; import FilterInput from 'ui/shared/filters/FilterInput';
import useMarketplace from '../marketplace/useMarketplace'; import useMarketplace from '../marketplace/useMarketplace';
const Marketplace = () => { const Marketplace = () => {
const { const {
isLoading, isPlaceholderData,
isError, isError,
error, error,
selectedCategoryId, selectedCategoryId,
...@@ -45,24 +44,26 @@ const Marketplace = () => { ...@@ -45,24 +44,26 @@ const Marketplace = () => {
categories={ categories } categories={ categories }
selectedCategoryId={ selectedCategoryId } selectedCategoryId={ selectedCategoryId }
onSelect={ onCategoryChange } onSelect={ onCategoryChange }
isLoading={ isPlaceholderData }
/> />
<FilterInput <FilterInput
initialValue={ filterQuery } initialValue={ filterQuery }
onChange={ onSearchInputChange } onChange={ onSearchInputChange }
marginBottom={{ base: '4', lg: '6' }} marginBottom={{ base: '4', lg: '6' }}
w="100%"
placeholder="Find app" placeholder="Find app"
isLoading={ isPlaceholderData }
/> />
</Box> </Box>
{ isLoading ? <MarketplaceListSkeleton/> : ( <MarketplaceList
<MarketplaceList apps={ displayedApps }
apps={ displayedApps } onAppClick={ showAppInfo }
onAppClick={ showAppInfo } favoriteApps={ favoriteApps }
favoriteApps={ favoriteApps } onFavoriteClick={ onFavoriteClick }
onFavoriteClick={ onFavoriteClick } isLoading={ isPlaceholderData }
/> />
) }
{ selectedApp && ( { selectedApp && (
<MarketplaceAppModal <MarketplaceAppModal
...@@ -74,23 +75,28 @@ const Marketplace = () => { ...@@ -74,23 +75,28 @@ const Marketplace = () => {
) } ) }
{ config.marketplaceSubmitForm && ( { config.marketplaceSubmitForm && (
<Link <Skeleton
fontWeight="bold" isLoaded={ !isPlaceholderData }
display="inline-flex"
alignItems="baseline"
marginTop={{ base: 8, sm: 16 }} marginTop={{ base: 8, sm: 16 }}
href={ config.marketplaceSubmitForm } display="inline-block"
isExternal
> >
<Icon <Link
as={ PlusIcon } fontWeight="bold"
w={ 3 } display="inline-flex"
h={ 3 } alignItems="baseline"
mr={ 2 } href={ config.marketplaceSubmitForm }
/> isExternal
>
<Icon
as={ PlusIcon }
w={ 3 }
h={ 3 }
mr={ 2 }
/>
Submit an App Submit an App
</Link> </Link>
</Skeleton>
) } ) }
</> </>
); );
......
...@@ -6,13 +6,12 @@ import React from 'react'; ...@@ -6,13 +6,12 @@ import React from 'react';
import SearchResultListItem from 'ui/searchResults/SearchResultListItem'; import SearchResultListItem from 'ui/searchResults/SearchResultListItem';
import SearchResultTableItem from 'ui/searchResults/SearchResultTableItem'; import SearchResultTableItem from 'ui/searchResults/SearchResultTableItem';
import ActionBar from 'ui/shared/ActionBar'; import ActionBar from 'ui/shared/ActionBar';
import ContentLoader from 'ui/shared/ContentLoader';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
import Page from 'ui/shared/Page/Page'; 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 SkeletonList from 'ui/shared/skeletons/SkeletonList'; import Thead from 'ui/shared/TheadSticky';
import SkeletonTable from 'ui/shared/skeletons/SkeletonTable';
import { default as Thead } from 'ui/shared/TheadSticky';
import Header from 'ui/snippets/header/Header'; import Header from 'ui/snippets/header/Header';
import SearchBarInput from 'ui/snippets/searchBar/SearchBarInput'; import SearchBarInput from 'ui/snippets/searchBar/SearchBarInput';
import useSearchQuery from 'ui/snippets/searchBar/useSearchQuery'; import useSearchQuery from 'ui/snippets/searchBar/useSearchQuery';
...@@ -20,7 +19,8 @@ import useSearchQuery from 'ui/snippets/searchBar/useSearchQuery'; ...@@ -20,7 +19,8 @@ import useSearchQuery from 'ui/snippets/searchBar/useSearchQuery';
const SearchResultsPageContent = () => { const SearchResultsPageContent = () => {
const router = useRouter(); const router = useRouter();
const { query, redirectCheckQuery, searchTerm, debouncedSearchTerm, handleSearchTermChange } = useSearchQuery(true); const { query, redirectCheckQuery, searchTerm, debouncedSearchTerm, handleSearchTermChange } = useSearchQuery(true);
const { data, isError, isLoading, pagination, isPaginationVisible } = query; const { data, isError, isPlaceholderData, pagination, isPaginationVisible } = query;
const [ showContent, setShowContent ] = React.useState(false);
React.useEffect(() => { React.useEffect(() => {
if (redirectCheckQuery.data?.redirect && redirectCheckQuery.data.parameter) { if (redirectCheckQuery.data?.redirect && redirectCheckQuery.data.parameter) {
...@@ -39,7 +39,9 @@ const SearchResultsPageContent = () => { ...@@ -39,7 +39,9 @@ const SearchResultsPageContent = () => {
} }
} }
} }
}, [ redirectCheckQuery.data, router ]);
!redirectCheckQuery.isLoading && setShowContent(true);
}, [ redirectCheckQuery, router ]);
const handleSubmit = React.useCallback((event: FormEvent<HTMLFormElement>) => { const handleSubmit = React.useCallback((event: FormEvent<HTMLFormElement>) => {
event.preventDefault(); event.preventDefault();
...@@ -50,23 +52,21 @@ const SearchResultsPageContent = () => { ...@@ -50,23 +52,21 @@ const SearchResultsPageContent = () => {
return <DataFetchAlert/>; return <DataFetchAlert/>;
} }
if (isLoading || redirectCheckQuery.isLoading) { if (!data?.items.length) {
return (
<Box>
<SkeletonList display={{ base: 'block', lg: 'none' }}/>
<SkeletonTable display={{ base: 'none', lg: 'block' }} columns={ [ '50%', '50%', '150px' ] }/>
</Box>
);
}
if (data.items.length === 0) {
return null; return null;
} }
return ( return (
<> <>
<Show below="lg" ssr={ false }> <Show below="lg" ssr={ false }>
{ data.items.map((item, index) => <SearchResultListItem key={ index } data={ item } searchTerm={ debouncedSearchTerm }/>) } { data.items.map((item, index) => (
<SearchResultListItem
key={ (isPlaceholderData ? 'placeholder_' : 'actual_') + index }
data={ item }
searchTerm={ debouncedSearchTerm }
isLoading={ isPlaceholderData }
/>
)) }
</Show> </Show>
<Hide below="lg" ssr={ false }> <Hide below="lg" ssr={ false }>
<Table variant="simple" size="md" fontWeight={ 500 }> <Table variant="simple" size="md" fontWeight={ 500 }>
...@@ -78,7 +78,14 @@ const SearchResultsPageContent = () => { ...@@ -78,7 +78,14 @@ const SearchResultsPageContent = () => {
</Tr> </Tr>
</Thead> </Thead>
<Tbody> <Tbody>
{ data.items.map((item, index) => <SearchResultTableItem key={ index } data={ item } searchTerm={ debouncedSearchTerm }/>) } { data.items.map((item, index) => (
<SearchResultTableItem
key={ (isPlaceholderData ? 'placeholder_' : 'actual_') + index }
data={ item }
searchTerm={ debouncedSearchTerm }
isLoading={ isPlaceholderData }
/>
)) }
</Tbody> </Tbody>
</Table> </Table>
</Hide> </Hide>
...@@ -91,16 +98,16 @@ const SearchResultsPageContent = () => { ...@@ -91,16 +98,16 @@ const SearchResultsPageContent = () => {
return null; return null;
} }
const text = isLoading || redirectCheckQuery.isLoading ? ( const text = isPlaceholderData && pagination.page === 1 ? (
<Skeleton h={ 6 } w="280px" borderRadius="full" mb={ isPaginationVisible ? 0 : 6 }/> <Skeleton h={ 6 } w="280px" borderRadius="full" mb={ isPaginationVisible ? 0 : 6 }/>
) : ( ) : (
( (
<Box mb={ isPaginationVisible ? 0 : 6 } lineHeight="32px"> <Box mb={ isPaginationVisible ? 0 : 6 } lineHeight="32px">
<span>Found </span> <span>Found </span>
<chakra.span fontWeight={ 700 }> <chakra.span fontWeight={ 700 }>
{ pagination.page > 1 ? 50 : data.items.length }{ data.next_page_params || pagination.page > 1 ? '+' : '' } { pagination.page > 1 ? 50 : data?.items.length }{ data?.next_page_params || pagination.page > 1 ? '+' : '' }
</chakra.span> </chakra.span>
<span> matching result{ data.items.length > 1 || pagination.page > 1 ? 's' : '' } for </span> <span> matching result{ (data?.items && data.items.length > 1) || pagination.page > 1 ? 's' : '' } for </span>
<chakra.span fontWeight={ 700 }>{ debouncedSearchTerm }</chakra.span> <chakra.span fontWeight={ 700 }>{ debouncedSearchTerm }</chakra.span>
</Box> </Box>
) )
...@@ -148,14 +155,17 @@ const SearchResultsPageContent = () => { ...@@ -148,14 +155,17 @@ const SearchResultsPageContent = () => {
return <Header renderSearchBar={ renderSearchBar }/>; return <Header renderSearchBar={ renderSearchBar }/>;
}, [ renderSearchBar ]); }, [ renderSearchBar ]);
return ( const pageContent = !showContent ? <ContentLoader/> : (
<Page renderHeader={ renderHeader }> <>
{ isLoading || redirectCheckQuery.isLoading ? <PageTitle title="Search results"/>
<Skeleton h={ 10 } mb={ 6 } w="100%" maxW="222px"/> :
<PageTitle title="Search results"/>
}
{ bar } { bar }
{ content } { content }
</>
);
return (
<Page renderHeader={ renderHeader }>
{ pageContent }
</Page> </Page>
); );
}; };
......
...@@ -28,8 +28,8 @@ import NetworkExplorers from 'ui/shared/NetworkExplorers'; ...@@ -28,8 +28,8 @@ import NetworkExplorers from 'ui/shared/NetworkExplorers';
import PageTitle from 'ui/shared/Page/PageTitle'; import PageTitle from 'ui/shared/Page/PageTitle';
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';
import SkeletonTabs from 'ui/shared/skeletons/SkeletonTabs';
import RoutedTabs from 'ui/shared/Tabs/RoutedTabs'; import RoutedTabs from 'ui/shared/Tabs/RoutedTabs';
import TabsSkeleton from 'ui/shared/Tabs/TabsSkeleton';
import TokenLogo from 'ui/shared/TokenLogo'; import TokenLogo from 'ui/shared/TokenLogo';
import TokenContractInfo from 'ui/token/TokenContractInfo'; import TokenContractInfo from 'ui/token/TokenContractInfo';
import TokenDetails from 'ui/token/TokenDetails'; import TokenDetails from 'ui/token/TokenDetails';
...@@ -251,7 +251,15 @@ const TokenPageContent = () => { ...@@ -251,7 +251,15 @@ const TokenPageContent = () => {
isLoading={ tokenQuery.isPlaceholderData } isLoading={ tokenQuery.isPlaceholderData }
backLink={ backLink } backLink={ backLink }
beforeTitle={ ( beforeTitle={ (
<TokenLogo data={ tokenQuery.data } boxSize={ 6 } isLoading={ tokenQuery.isPlaceholderData } display="inline-block" mr={ 2 }/> <TokenLogo
data={ tokenQuery.data }
boxSize={ 6 }
isLoading={ tokenQuery.isPlaceholderData }
display="inline-block"
mr={ 2 }
my={{ base: 'auto', lg: tokenQuery.isPlaceholderData ? 2 : 'auto' }}
verticalAlign={{ base: undefined, lg: tokenQuery.isPlaceholderData ? 'text-bottom' : undefined }}
/>
) } ) }
afterTitle={ afterTitle={
verifiedInfoQuery.data?.tokenAddress ? verifiedInfoQuery.data?.tokenAddress ?
...@@ -267,7 +275,7 @@ const TokenPageContent = () => { ...@@ -267,7 +275,7 @@ const TokenPageContent = () => {
<Box ref={ scrollRef }></Box> <Box ref={ scrollRef }></Box>
{ tokenQuery.isPlaceholderData || contractQuery.isPlaceholderData ? { tokenQuery.isPlaceholderData || contractQuery.isPlaceholderData ?
<SkeletonTabs tabs={ tabs }/> : <TabsSkeleton tabs={ tabs }/> :
( (
<RoutedTabs <RoutedTabs
tabs={ tabs } tabs={ tabs }
......
...@@ -19,8 +19,8 @@ import LinkExternal from 'ui/shared/LinkExternal'; ...@@ -19,8 +19,8 @@ import LinkExternal from 'ui/shared/LinkExternal';
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 type { Props as PaginationProps } from 'ui/shared/Pagination'; import type { Props as PaginationProps } from 'ui/shared/Pagination';
import SkeletonTabs from 'ui/shared/skeletons/SkeletonTabs';
import RoutedTabs from 'ui/shared/Tabs/RoutedTabs'; import RoutedTabs from 'ui/shared/Tabs/RoutedTabs';
import TabsSkeleton from 'ui/shared/Tabs/TabsSkeleton';
import TokenHolders from 'ui/token/TokenHolders/TokenHolders'; import TokenHolders from 'ui/token/TokenHolders/TokenHolders';
import TokenTransfer from 'ui/token/TokenTransfer/TokenTransfer'; import TokenTransfer from 'ui/token/TokenTransfer/TokenTransfer';
import TokenInstanceDetails from 'ui/tokenInstance/TokenInstanceDetails'; import TokenInstanceDetails from 'ui/tokenInstance/TokenInstanceDetails';
...@@ -175,7 +175,7 @@ const TokenInstanceContent = () => { ...@@ -175,7 +175,7 @@ const TokenInstanceContent = () => {
{ /* should stay before tabs to scroll up with pagination */ } { /* should stay before tabs to scroll up with pagination */ }
<Box ref={ scrollRef }></Box> <Box ref={ scrollRef }></Box>
{ tokenInstanceQuery.isPlaceholderData ? <SkeletonTabs tabs={ tabs }/> : ( { tokenInstanceQuery.isPlaceholderData ? <TabsSkeleton tabs={ tabs }/> : (
<RoutedTabs <RoutedTabs
tabs={ tabs } tabs={ tabs }
tabListProps={ isMobile ? { mt: 8 } : { mt: 3, py: 5, marginBottom: 0 } } tabListProps={ isMobile ? { mt: 8 } : { mt: 3, py: 5, marginBottom: 0 } }
......
...@@ -74,8 +74,6 @@ test('address verification flow', async({ mount, page }) => { ...@@ -74,8 +74,6 @@ test('address verification flow', async({ mount, page }) => {
await page.getByRole('button', { name: /continue/i }).click(); await page.getByRole('button', { name: /continue/i }).click();
// fill second step // fill second step
const option = page.getByText(/sign manually/i);
option.click();
const signatureInput = page.getByLabel(/signature hash/i); const signatureInput = page.getByLabel(/signature hash/i);
await signatureInput.fill(mocks.SIGNATURE); await signatureInput.fill(mocks.SIGNATURE);
await page.getByRole('button', { name: /verify/i }).click(); await page.getByRole('button', { name: /verify/i }).click();
......
import { OrderedList, ListItem, chakra, Button, useDisclosure, Show, Hide, Skeleton, Box, Link } from '@chakra-ui/react'; import { OrderedList, ListItem, chakra, Button, useDisclosure, Show, Hide, Skeleton, Link } from '@chakra-ui/react';
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
...@@ -9,12 +9,11 @@ import appConfig from 'configs/app/config'; ...@@ -9,12 +9,11 @@ import appConfig from 'configs/app/config';
import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery'; import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery';
import useRedirectForInvalidAuthToken from 'lib/hooks/useRedirectForInvalidAuthToken'; import useRedirectForInvalidAuthToken from 'lib/hooks/useRedirectForInvalidAuthToken';
import getQueryParamString from 'lib/router/getQueryParamString'; import getQueryParamString from 'lib/router/getQueryParamString';
import { TOKEN_INFO_APPLICATION, VERIFIED_ADDRESS } from 'stubs/account';
import AddressVerificationModal from 'ui/addressVerification/AddressVerificationModal'; import AddressVerificationModal from 'ui/addressVerification/AddressVerificationModal';
import AccountPageDescription from 'ui/shared/AccountPageDescription'; import AccountPageDescription from 'ui/shared/AccountPageDescription';
import DataListDisplay from 'ui/shared/DataListDisplay'; import DataListDisplay from 'ui/shared/DataListDisplay';
import PageTitle from 'ui/shared/Page/PageTitle'; import PageTitle from 'ui/shared/Page/PageTitle';
import SkeletonListAccount from 'ui/shared/skeletons/SkeletonListAccount';
import SkeletonTable from 'ui/shared/skeletons/SkeletonTable';
import AdminSupportText from 'ui/shared/texts/AdminSupportText'; import AdminSupportText from 'ui/shared/texts/AdminSupportText';
import TokenInfoForm from 'ui/tokenInfo/TokenInfoForm'; import TokenInfoForm from 'ui/tokenInfo/TokenInfoForm';
import VerifiedAddressesListItem from 'ui/verifiedAddresses/VerifiedAddressesListItem'; import VerifiedAddressesListItem from 'ui/verifiedAddresses/VerifiedAddressesListItem';
...@@ -35,12 +34,18 @@ const VerifiedAddresses = () => { ...@@ -35,12 +34,18 @@ const VerifiedAddresses = () => {
}, [ ]); }, [ ]);
const modalProps = useDisclosure(); const modalProps = useDisclosure();
const queryClient = useQueryClient();
const addressesQuery = useApiQuery('verified_addresses', { const addressesQuery = useApiQuery('verified_addresses', {
pathParams: { chainId: appConfig.network.id }, pathParams: { chainId: appConfig.network.id },
queryOptions: {
placeholderData: { verifiedAddresses: Array(3).fill(VERIFIED_ADDRESS) },
},
}); });
const applicationsQuery = useApiQuery('token_info_applications', { const applicationsQuery = useApiQuery('token_info_applications', {
pathParams: { chainId: appConfig.network.id, id: undefined }, pathParams: { chainId: appConfig.network.id, id: undefined },
queryOptions: { queryOptions: {
placeholderData: { submissions: Array(3).fill(TOKEN_INFO_APPLICATION) },
select: (data) => { select: (data) => {
return { return {
...data, ...data,
...@@ -49,7 +54,8 @@ const VerifiedAddresses = () => { ...@@ -49,7 +54,8 @@ const VerifiedAddresses = () => {
}, },
}, },
}); });
const queryClient = useQueryClient();
const isLoading = addressesQuery.isPlaceholderData || applicationsQuery.isPlaceholderData;
const handleGoBack = React.useCallback(() => { const handleGoBack = React.useCallback(() => {
setSelectedAddress(undefined); setSelectedAddress(undefined);
...@@ -94,24 +100,11 @@ const VerifiedAddresses = () => { ...@@ -94,24 +100,11 @@ const VerifiedAddresses = () => {
}, [ queryClient ]); }, [ queryClient ]);
const addButton = ( const addButton = (
<Box marginTop={ 8 }> <Skeleton mt={ 8 } isLoaded={ !isLoading } display="inline-block">
<Button size="lg" onClick={ modalProps.onOpen }> <Button size="lg" onClick={ modalProps.onOpen }>
Add address Add address
</Button> </Button>
</Box> </Skeleton>
);
const skeleton = (
<>
<Box display={{ base: 'block', lg: 'none' }}>
<SkeletonListAccount/>
<Skeleton height="44px" width="156px" marginTop={ 8 }/>
</Box>
<Box display={{ base: 'none', lg: 'block' }}>
<SkeletonTable columns={ [ '100%', '180px', '260px', '160px' ] }/>
<Skeleton height="44px" width="156px" marginTop={ 8 }/>
</Box>
</>
); );
const backLink = React.useMemo(() => { const backLink = React.useMemo(() => {
...@@ -144,13 +137,14 @@ const VerifiedAddresses = () => { ...@@ -144,13 +137,14 @@ const VerifiedAddresses = () => {
const content = addressesQuery.data?.verifiedAddresses ? ( const content = addressesQuery.data?.verifiedAddresses ? (
<> <>
<Show below="lg" key="content-mobile" ssr={ false }> <Show below="lg" key="content-mobile" ssr={ false }>
{ addressesQuery.data.verifiedAddresses.map((item) => ( { addressesQuery.data.verifiedAddresses.map((item, index) => (
<VerifiedAddressesListItem <VerifiedAddressesListItem
key={ item.contractAddress } key={ item.contractAddress + (isLoading ? index : '') }
item={ item } item={ item }
application={ applicationsQuery.data?.submissions?.find(({ tokenAddress }) => tokenAddress.toLowerCase() === item.contractAddress.toLowerCase()) } application={ applicationsQuery.data?.submissions?.find(({ tokenAddress }) => tokenAddress.toLowerCase() === item.contractAddress.toLowerCase()) }
onAdd={ handleItemAdd } onAdd={ handleItemAdd }
onEdit={ handleItemEdit } onEdit={ handleItemEdit }
isLoading={ isLoading }
/> />
)) } )) }
</Show> </Show>
...@@ -160,6 +154,7 @@ const VerifiedAddresses = () => { ...@@ -160,6 +154,7 @@ const VerifiedAddresses = () => {
applications={ applicationsQuery.data?.submissions } applications={ applicationsQuery.data?.submissions }
onItemEdit={ handleItemEdit } onItemEdit={ handleItemEdit }
onItemAdd={ handleItemAdd } onItemAdd={ handleItemAdd }
isLoading={ isLoading }
/> />
</Hide> </Hide>
</> </>
...@@ -192,12 +187,10 @@ const VerifiedAddresses = () => { ...@@ -192,12 +187,10 @@ const VerifiedAddresses = () => {
<AdminSupportText mt={ 5 }/> <AdminSupportText mt={ 5 }/>
</AccountPageDescription> </AccountPageDescription>
<DataListDisplay <DataListDisplay
isLoading={ addressesQuery.isLoading || applicationsQuery.isLoading }
isError={ addressesQuery.isError || applicationsQuery.isError } isError={ addressesQuery.isError || applicationsQuery.isError }
items={ addressesQuery.data?.verifiedAddresses } items={ addressesQuery.data?.verifiedAddresses }
content={ content } content={ content }
emptyText="" emptyText=""
skeletonProps={{ customSkeleton: skeleton }}
/> />
{ addButton } { addButton }
<AddressVerificationModal <AddressVerificationModal
......
...@@ -9,6 +9,8 @@ import useIsMobile from 'lib/hooks/useIsMobile'; ...@@ -9,6 +9,8 @@ import useIsMobile from 'lib/hooks/useIsMobile';
import useQueryWithPages from 'lib/hooks/useQueryWithPages'; import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import { apos } from 'lib/html-entities'; import { apos } from 'lib/html-entities';
import getQueryParamString from 'lib/router/getQueryParamString'; import getQueryParamString from 'lib/router/getQueryParamString';
import { VERIFIED_CONTRACT_INFO } from 'stubs/contract';
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 FilterInput from 'ui/shared/filters/FilterInput'; import FilterInput from 'ui/shared/filters/FilterInput';
...@@ -32,9 +34,21 @@ const VerifiedContracts = () => { ...@@ -32,9 +34,21 @@ const VerifiedContracts = () => {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const { isError, isLoading, data, isPaginationVisible, pagination, onFilterChange } = useQueryWithPages({ const { isError, isPlaceholderData, data, isPaginationVisible, pagination, onFilterChange } = useQueryWithPages({
resourceName: 'verified_contracts', resourceName: 'verified_contracts',
filters: { q: debouncedSearchTerm, filter: type }, filters: { q: debouncedSearchTerm, filter: type },
options: {
placeholderData: generateListStub<'verified_contracts'>(
VERIFIED_CONTRACT_INFO,
50,
{
next_page_params: {
items_count: '50',
smart_contract_id: '50',
},
},
),
},
}); });
const handleSearchTermChange = React.useCallback((value: string) => { const handleSearchTermChange = React.useCallback((value: string) => {
...@@ -107,10 +121,10 @@ const VerifiedContracts = () => { ...@@ -107,10 +121,10 @@ const VerifiedContracts = () => {
const content = sortedData ? ( const content = sortedData ? (
<> <>
<Show below="lg" ssr={ false }> <Show below="lg" ssr={ false }>
<VerifiedContractsList data={ sortedData }/> <VerifiedContractsList data={ sortedData } isLoading={ isPlaceholderData }/>
</Show> </Show>
<Hide below="lg" ssr={ false }> <Hide below="lg" ssr={ false }>
<VerifiedContractsTable data={ sortedData } sort={ sort } onSortToggle={ handleSortToggle }/> <VerifiedContractsTable data={ sortedData } sort={ sort } onSortToggle={ handleSortToggle } isLoading={ isPlaceholderData }/>
</Hide> </Hide>
</> </>
) : null; ) : null;
...@@ -121,9 +135,7 @@ const VerifiedContracts = () => { ...@@ -121,9 +135,7 @@ const VerifiedContracts = () => {
<VerifiedContractsCounters/> <VerifiedContractsCounters/>
<DataListDisplay <DataListDisplay
isError={ isError } isError={ isError }
isLoading={ isLoading }
items={ data?.items } items={ data?.items }
skeletonProps={{ skeletonDesktopColumns: [ '50%', '130px', '130px', '50%', '80px', '110px' ] }}
emptyText="There are no verified contracts." emptyText="There are no verified contracts."
filterProps={{ filterProps={{
emptyFilteredText: `Couldn${ apos }t find any contract that matches your query.`, emptyFilteredText: `Couldn${ apos }t find any contract that matches your query.`,
......
...@@ -91,9 +91,7 @@ const Withdrawals = () => { ...@@ -91,9 +91,7 @@ const Withdrawals = () => {
<PageTitle title="Withdrawals" withTextAd/> <PageTitle title="Withdrawals" withTextAd/>
<DataListDisplay <DataListDisplay
isError={ isError } isError={ isError }
isLoading={ false }
items={ data?.items } items={ data?.items }
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 }
......
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