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 }
......
import { Text, Flex, Icon, Box, chakra } from '@chakra-ui/react'; import { Flex, Icon, Box, chakra, Skeleton } from '@chakra-ui/react';
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
...@@ -19,9 +19,10 @@ import TokenLogo from 'ui/shared/TokenLogo'; ...@@ -19,9 +19,10 @@ import TokenLogo from 'ui/shared/TokenLogo';
interface Props { interface Props {
data: SearchResultItem; data: SearchResultItem;
searchTerm: string; searchTerm: string;
isLoading?: boolean;
} }
const SearchResultListItem = ({ data, searchTerm }: Props) => { const SearchResultListItem = ({ data, searchTerm, isLoading }: Props) => {
const firstRow = (() => { const firstRow = (() => {
switch (data.type) { switch (data.type) {
...@@ -30,9 +31,15 @@ const SearchResultListItem = ({ data, searchTerm }: Props) => { ...@@ -30,9 +31,15 @@ const SearchResultListItem = ({ data, searchTerm }: Props) => {
return ( return (
<Flex alignItems="flex-start"> <Flex alignItems="flex-start">
<TokenLogo boxSize={ 6 } hash={ data.address } name={ data.name } flexShrink={ 0 }/> <TokenLogo boxSize={ 6 } hash={ data.address } name={ data.name } flexShrink={ 0 } isLoading={ isLoading }/>
<LinkInternal ml={ 2 } href={ route({ pathname: '/token/[hash]', query: { hash: data.address } }) } fontWeight={ 700 } wordBreak="break-all"> <LinkInternal
<chakra.span dangerouslySetInnerHTML={{ __html: highlightText(name, searchTerm) }}/> ml={ 2 }
href={ route({ pathname: '/token/[hash]', query: { hash: data.address } }) }
fontWeight={ 700 }
wordBreak="break-all"
isLoading={ isLoading }
>
<Skeleton isLoaded={ !isLoading } dangerouslySetInnerHTML={{ __html: highlightText(name, searchTerm) }}/>
</LinkInternal> </LinkInternal>
</Flex> </Flex>
); );
...@@ -80,7 +87,9 @@ const SearchResultListItem = ({ data, searchTerm }: Props) => { ...@@ -80,7 +87,9 @@ const SearchResultListItem = ({ data, searchTerm }: Props) => {
switch (data.type) { switch (data.type) {
case 'token': { case 'token': {
return ( return (
<HashStringShortenDynamic hash={ data.address }/> <Skeleton isLoaded={ !isLoading }>
<HashStringShortenDynamic hash={ data.address }/>
</Skeleton>
); );
} }
case 'block': { case 'block': {
...@@ -106,7 +115,9 @@ const SearchResultListItem = ({ data, searchTerm }: Props) => { ...@@ -106,7 +115,9 @@ const SearchResultListItem = ({ data, searchTerm }: Props) => {
<ListItemMobile py={ 3 } fontSize="sm" rowGap={ 2 }> <ListItemMobile py={ 3 } fontSize="sm" rowGap={ 2 }>
<Flex justifyContent="space-between" w="100%" overflow="hidden" lineHeight={ 6 }> <Flex justifyContent="space-between" w="100%" overflow="hidden" lineHeight={ 6 }>
{ firstRow } { firstRow }
<Text variant="secondary" ml={ 8 } textTransform="capitalize">{ data.type }</Text> <Skeleton isLoaded={ !isLoading } color="text_secondary" ml={ 8 } textTransform="capitalize">
<span>{ data.type }</span>
</Skeleton>
</Flex> </Flex>
{ secondRow } { secondRow }
</ListItemMobile> </ListItemMobile>
......
import { Tr, Td, Text, Flex, Icon, Box } from '@chakra-ui/react'; import { Tr, Td, Flex, Icon, Box, Skeleton } from '@chakra-ui/react';
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
...@@ -18,9 +18,10 @@ import TokenLogo from 'ui/shared/TokenLogo'; ...@@ -18,9 +18,10 @@ import TokenLogo from 'ui/shared/TokenLogo';
interface Props { interface Props {
data: SearchResultItem; data: SearchResultItem;
searchTerm: string; searchTerm: string;
isLoading?: boolean;
} }
const SearchResultTableItem = ({ data, searchTerm }: Props) => { const SearchResultTableItem = ({ data, searchTerm, isLoading }: Props) => {
const content = (() => { const content = (() => {
switch (data.type) { switch (data.type) {
...@@ -30,16 +31,22 @@ const SearchResultTableItem = ({ data, searchTerm }: Props) => { ...@@ -30,16 +31,22 @@ const SearchResultTableItem = ({ data, searchTerm }: Props) => {
<> <>
<Td fontSize="sm"> <Td fontSize="sm">
<Flex alignItems="center"> <Flex alignItems="center">
<TokenLogo boxSize={ 6 } hash={ data.address } name={ data.name } flexShrink={ 0 }/> <TokenLogo boxSize={ 6 } hash={ data.address } name={ data.name } flexShrink={ 0 } isLoading={ isLoading }/>
<LinkInternal ml={ 2 } href={ route({ pathname: '/token/[hash]', query: { hash: data.address } }) } fontWeight={ 700 } wordBreak="break-all"> <LinkInternal
<span dangerouslySetInnerHTML={{ __html: highlightText(name, searchTerm) }}/> ml={ 2 }
href={ route({ pathname: '/token/[hash]', query: { hash: data.address } }) }
fontWeight={ 700 }
wordBreak="break-all"
isLoading={ isLoading }
>
<Skeleton isLoaded={ !isLoading } dangerouslySetInnerHTML={{ __html: highlightText(name, searchTerm) }}/>
</LinkInternal> </LinkInternal>
</Flex> </Flex>
</Td> </Td>
<Td fontSize="sm" verticalAlign="middle"> <Td fontSize="sm" verticalAlign="middle">
<Box whiteSpace="nowrap" overflow="hidden"> <Skeleton isLoaded={ !isLoading } whiteSpace="nowrap" overflow="hidden">
<HashStringShortenDynamic hash={ data.address }/> <HashStringShortenDynamic hash={ data.address }/>
</Box> </Skeleton>
</Td> </Td>
</> </>
); );
...@@ -126,9 +133,9 @@ const SearchResultTableItem = ({ data, searchTerm }: Props) => { ...@@ -126,9 +133,9 @@ const SearchResultTableItem = ({ data, searchTerm }: Props) => {
<Tr> <Tr>
{ content } { content }
<Td fontSize="sm" textTransform="capitalize" verticalAlign="middle"> <Td fontSize="sm" textTransform="capitalize" verticalAlign="middle">
<Text variant="secondary"> <Skeleton isLoaded={ !isLoading } color="text_secondary" display="inline-block">
{ data.type } <span>{ data.type }</span>
</Text> </Skeleton>
</Td> </Td>
</Tr> </Tr>
); );
......
...@@ -39,7 +39,7 @@ const AddressHeadingInfo = ({ address, token, isLinkDisabled, isLoading }: Props ...@@ -39,7 +39,7 @@ const AddressHeadingInfo = ({ address, token, isLinkDisabled, isLoading }: Props
{ !isLoading && !address.is_contract && appConfig.isAccountSupported && ( { !isLoading && !address.is_contract && appConfig.isAccountSupported && (
<AddressFavoriteButton hash={ address.hash } watchListId={ address.watchlist_address_id } ml={ 3 }/> <AddressFavoriteButton hash={ address.hash } watchListId={ address.watchlist_address_id } ml={ 3 }/>
) } ) }
<AddressQrCode hash={ address.hash } ml={ 2 } isLoading={ isLoading }/> <AddressQrCode hash={ address.hash } ml={ 2 } isLoading={ isLoading } flexShrink={ 0 }/>
{ appConfig.isAccountSupported && <AddressActionsMenu isLoading={ isLoading }/> } { appConfig.isAccountSupported && <AddressActionsMenu isLoading={ isLoading }/> }
</Flex> </Flex>
); );
......
...@@ -4,16 +4,6 @@ import React from 'react'; ...@@ -4,16 +4,6 @@ import React from 'react';
import EmptySearchResult from 'ui/shared/EmptySearchResult'; import EmptySearchResult from 'ui/shared/EmptySearchResult';
import DataFetchAlert from './DataFetchAlert'; import DataFetchAlert from './DataFetchAlert';
import SkeletonList from './skeletons/SkeletonList';
import SkeletonTable from './skeletons/SkeletonTable';
type SkeletonProps =
{ customSkeleton: React.ReactNode } |
{
skeletonDesktopColumns: Array<string>;
isLongSkeleton?: boolean;
skeletonDesktopMinW?: string;
}
type FilterProps = { type FilterProps = {
hasActiveFilters: boolean; hasActiveFilters: boolean;
...@@ -22,13 +12,11 @@ type FilterProps = { ...@@ -22,13 +12,11 @@ type FilterProps = {
type Props = { type Props = {
isError: boolean; isError: boolean;
isLoading: boolean;
items?: Array<unknown>; items?: Array<unknown>;
emptyText: string; emptyText: string;
actionBar?: React.ReactNode; actionBar?: React.ReactNode;
content: React.ReactNode; content: React.ReactNode;
className?: string; className?: string;
skeletonProps: SkeletonProps;
filterProps?: FilterProps; filterProps?: FilterProps;
} }
...@@ -37,27 +25,6 @@ const DataListDisplay = (props: Props) => { ...@@ -37,27 +25,6 @@ const DataListDisplay = (props: Props) => {
return <DataFetchAlert className={ props.className }/>; return <DataFetchAlert className={ props.className }/>;
} }
if (props.isLoading) {
return (
<Box className={ props.className }>
{ props.actionBar }
{ 'customSkeleton' in props.skeletonProps && props.skeletonProps.customSkeleton }
{ 'skeletonDesktopColumns' in props.skeletonProps && (
<>
<SkeletonList display={{ base: 'block', lg: 'none' }}/>
<SkeletonTable
display={{ base: 'none', lg: 'block' }}
columns={ props.skeletonProps.skeletonDesktopColumns || [] }
isLong={ props.skeletonProps.isLongSkeleton }
minW={ props.skeletonProps.skeletonDesktopMinW }
/>
</>
) }
</Box>
);
}
if (props.filterProps?.hasActiveFilters && !props.items?.length) { if (props.filterProps?.hasActiveFilters && !props.items?.length) {
return ( return (
<Box className={ props.className }> <Box className={ props.className }>
......
import { Link, Icon, chakra } from '@chakra-ui/react'; import { Link, Icon, chakra, Box, Skeleton } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import arrowIcon from 'icons/arrows/north-east.svg'; import arrowIcon from 'icons/arrows/north-east.svg';
...@@ -7,9 +7,19 @@ interface Props { ...@@ -7,9 +7,19 @@ interface Props {
href: string; href: string;
className?: string; className?: string;
children: React.ReactNode; children: React.ReactNode;
isLoading?: boolean;
} }
const LinkExternal = ({ href, children, className }: Props) => { const LinkExternal = ({ href, children, className, isLoading }: Props) => {
if (isLoading) {
return (
<Box className={ className } fontSize="sm" lineHeight={ 5 } display="inline-block" alignItems="center">
{ children }
<Skeleton boxSize={ 4 } verticalAlign="middle" display="inline-block"/>
</Box>
);
}
return ( return (
<Link className={ className } fontSize="sm" lineHeight={ 5 } display="inline-block" alignItems="center" target="_blank" href={ href }> <Link className={ className } fontSize="sm" lineHeight={ 5 } display="inline-block" alignItems="center" target="_blank" href={ href }>
{ children } { children }
......
import type { LinkProps } from '@chakra-ui/react'; import type { LinkProps, FlexProps } from '@chakra-ui/react';
import { Link } from '@chakra-ui/react'; import { Flex, Link } from '@chakra-ui/react';
import type { LinkProps as NextLinkProps } from 'next/link'; import type { LinkProps as NextLinkProps } from 'next/link';
import NextLink from 'next/link'; import NextLink from 'next/link';
import type { LegacyRef } from 'react'; import type { LegacyRef } from 'react';
import React from 'react'; import React from 'react';
// NOTE! use this component only for links to pages that are completely implemented in new UI // NOTE! use this component only for links to pages that are completely implemented in new UI
const LinkInternal = (props: LinkProps, ref: LegacyRef<HTMLAnchorElement>) => { const LinkInternal = ({ isLoading, ...props }: LinkProps & { isLoading?: boolean }, ref: LegacyRef<HTMLAnchorElement>) => {
if (isLoading) {
return <Flex alignItems="center" { ...props as FlexProps }>{ props.children }</Flex>;
}
if (!props.href) { if (!props.href) {
return <Link { ...props } ref={ ref }/>; return <Link { ...props } ref={ ref }/>;
} }
......
...@@ -20,7 +20,7 @@ const Container = chakra(({ isAnimated, children, className }: ContainerProps) = ...@@ -20,7 +20,7 @@ const Container = chakra(({ isAnimated, children, className }: ContainerProps) =
rowGap={ 2 } rowGap={ 2 }
columnGap={ 2 } columnGap={ 2 }
gridTemplateColumns="86px auto" gridTemplateColumns="86px auto"
gridTemplateRows="minmax(30px, max-content)" alignItems="start"
paddingY={ 4 } paddingY={ 4 }
borderColor="divider" borderColor="divider"
borderTopWidth="1px" borderTopWidth="1px"
...@@ -29,6 +29,7 @@ const Container = chakra(({ isAnimated, children, className }: ContainerProps) = ...@@ -29,6 +29,7 @@ const Container = chakra(({ isAnimated, children, className }: ContainerProps) =
}} }}
className={ className } className={ className }
fontSize="sm" fontSize="sm"
lineHeight="20px"
> >
{ children } { children }
</Grid> </Grid>
...@@ -47,7 +48,6 @@ const Label = chakra(({ children, className, isLoading }: LabelProps) => { ...@@ -47,7 +48,6 @@ const Label = chakra(({ children, className, isLoading }: LabelProps) => {
className={ className } className={ className }
isLoaded={ !isLoading } isLoaded={ !isLoading }
fontWeight={ 500 } fontWeight={ 500 }
lineHeight="20px"
my="5px" my="5px"
justifySelf="start" justifySelf="start"
> >
......
...@@ -59,12 +59,12 @@ const PageTitle = ({ title, contentAfter, withTextAd, backLink, className, isLoa ...@@ -59,12 +59,12 @@ const PageTitle = ({ title, contentAfter, withTextAd, backLink, className, isLoa
columnGap={ 3 } columnGap={ 3 }
alignItems="center" alignItems="center"
> >
<Box> <Box h={{ base: 'auto', lg: isLoading ? 10 : 'auto' }}>
{ backLink && <BackLink { ...backLink } isLoading={ isLoading }/> } { backLink && <BackLink { ...backLink } isLoading={ isLoading }/> }
{ beforeTitle } { beforeTitle }
<Skeleton <Skeleton
isLoaded={ !isLoading } isLoaded={ !isLoading }
display={ isLoading ? 'inline-block' : 'inline' } display={{ base: 'inline', lg: isLoading ? 'inline-block' : 'inline' }}
verticalAlign={ isLoading ? 'super' : undefined } verticalAlign={ isLoading ? 'super' : undefined }
> >
<Heading <Heading
......
import { Flex, Skeleton, chakra, Box, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
import type { RoutedTab } from '../Tabs/types';
import useTabIndexFromQuery from 'ui/shared/Tabs/useTabIndexFromQuery';
type TabSize = 'sm' | 'md';
const SkeletonTabText = ({ size, title }: { size: TabSize; title: RoutedTab['title'] }) => (
<Skeleton
borderRadius="base"
borderWidth={ size === 'sm' ? '2px' : 0 }
fontWeight={ 600 }
mx={ size === 'sm' ? 3 : 4 }
flexShrink={ 0 }
>
{ typeof title === 'string' ? title : title() }
</Skeleton>
);
interface Props {
className?: string;
tabs: Array<RoutedTab>;
size?: 'sm' | 'md';
}
const TabsSkeleton = ({ className, tabs, size = 'md' }: Props) => {
const bgColor = useColorModeValue('blackAlpha.50', 'whiteAlpha.50');
const tabIndex = useTabIndexFromQuery(tabs || []);
if (tabs.length === 1) {
return null;
}
return (
<Flex className={ className } my={ 8 } alignItems="center" overflow="hidden">
{ tabs.slice(0, tabIndex).map(({ title, id }) => (
<SkeletonTabText
key={ id }
title={ title }
size={ size }
/>
)) }
{ tabs.slice(tabIndex, tabIndex + 1).map(({ title, id }) => (
<Box key={ id } bgColor={ bgColor } py={ size === 'sm' ? 1 : 2 } borderRadius="base" flexShrink={ 0 }>
<SkeletonTabText
key={ id }
title={ title }
size={ size }
/>
</Box>
)) }
{ tabs.slice(tabIndex + 1).map(({ title, id }) => (
<SkeletonTabText
key={ id }
title={ title }
size={ size }
/>
)) }
</Flex>
);
};
export default chakra(TabsSkeleton);
...@@ -9,12 +9,27 @@ import TokenTransferList from './TokenTransferList'; ...@@ -9,12 +9,27 @@ import TokenTransferList from './TokenTransferList';
test.use({ viewport: devices['iPhone 13 Pro'].viewport }); test.use({ viewport: devices['iPhone 13 Pro'].viewport });
const data = [
{
...tokenTransferMock.erc20,
to: {
...tokenTransferMock.erc20.to,
hash: tokenTransferMock.erc721.to.hash,
},
},
tokenTransferMock.erc721,
tokenTransferMock.erc1155A,
tokenTransferMock.erc1155B,
tokenTransferMock.erc1155C,
tokenTransferMock.erc1155D,
];
test('without tx info', async({ mount }) => { test('without tx info', async({ mount }) => {
const component = await mount( const component = await mount(
<TestApp> <TestApp>
<Box h={{ base: '134px', lg: 6 }}/> <Box h={{ base: '134px', lg: 6 }}/>
<TokenTransferList <TokenTransferList
data={ tokenTransferMock.mixTokens.items } data={ data }
showTxInfo={ false } showTxInfo={ false }
/> />
</TestApp>, </TestApp>,
...@@ -28,7 +43,7 @@ test('with tx info', async({ mount }) => { ...@@ -28,7 +43,7 @@ test('with tx info', async({ mount }) => {
<TestApp> <TestApp>
<Box h={{ base: '134px', lg: 6 }}/> <Box h={{ base: '134px', lg: 6 }}/>
<TokenTransferList <TokenTransferList
data={ tokenTransferMock.mixTokens.items } data={ data }
showTxInfo={ true } showTxInfo={ true }
/> />
</TestApp>, </TestApp>,
......
...@@ -50,7 +50,7 @@ const TokenTransferListItem = ({ ...@@ -50,7 +50,7 @@ const TokenTransferListItem = ({
const timeAgo = useTimeAgoIncrement(timestamp, enableTimeIncrement); const timeAgo = useTimeAgoIncrement(timestamp, enableTimeIncrement);
const addressWidth = `calc((100% - ${ baseAddress ? '50px' : '0px' }) / 2)`; const addressWidth = `calc((100% - ${ baseAddress ? '50px - 24px' : '24px - 24px' }) / 2)`;
return ( return (
<ListItemMobile rowGap={ 3 } isAnimated> <ListItemMobile rowGap={ 3 } isAnimated>
<Flex w="100%" justifyContent="space-between"> <Flex w="100%" justifyContent="space-between">
...@@ -91,16 +91,24 @@ const TokenTransferListItem = ({ ...@@ -91,16 +91,24 @@ const TokenTransferListItem = ({
</Flex> </Flex>
) } ) }
<Flex w="100%" columnGap={ 3 }> <Flex w="100%" columnGap={ 3 }>
<Address width={ addressWidth }> <Address width={ addressWidth } flexShrink={ 0 }>
<AddressIcon address={ from } isLoading={ isLoading }/> <AddressIcon address={ from } isLoading={ isLoading }/>
<AddressLink type="address" ml={ 2 } fontWeight="500" hash={ from.hash } isDisabled={ baseAddress === from.hash } isLoading={ isLoading }/> <AddressLink type="address" ml={ 2 } fontWeight="500" hash={ from.hash } isDisabled={ baseAddress === from.hash } isLoading={ isLoading }/>
{ baseAddress !== from.hash && <CopyToClipboard text={ from.hash } isLoading={ isLoading }/> } { baseAddress !== from.hash && <CopyToClipboard text={ from.hash } isLoading={ isLoading }/> }
</Address> </Address>
{ baseAddress ? { baseAddress ? (
<InOutTag isIn={ baseAddress === to.hash } isOut={ baseAddress === from.hash } w="50px" textAlign="center" isLoading={ isLoading }/> : <InOutTag
<Icon as={ eastArrowIcon } boxSize={ 6 } color="gray.500" isLoading={ isLoading }/> isIn={ baseAddress === to.hash }
isOut={ baseAddress === from.hash }
w="50px"
textAlign="center"
isLoading={ isLoading }
flexShrink={ 0 }
/>
) :
<Icon as={ eastArrowIcon } boxSize={ 6 } color="gray.500" isLoading={ isLoading } flexShrink={ 0 }/>
} }
<Address width={ addressWidth }> <Address width={ addressWidth } flexShrink={ 0 }>
<AddressIcon address={ to } isLoading={ isLoading }/> <AddressIcon address={ to } isLoading={ isLoading }/>
<AddressLink type="address" ml={ 2 } fontWeight="500" hash={ to.hash } isDisabled={ baseAddress === to.hash } isLoading={ isLoading }/> <AddressLink type="address" ml={ 2 } fontWeight="500" hash={ to.hash } isDisabled={ baseAddress === to.hash } isLoading={ isLoading }/>
{ baseAddress !== to.hash && <CopyToClipboard text={ to.hash } isLoading={ isLoading }/> } { baseAddress !== to.hash && <CopyToClipboard text={ to.hash } isLoading={ isLoading }/> }
......
import { chakra, Icon, Input, InputGroup, InputLeftElement, InputRightElement, useColorModeValue } from '@chakra-ui/react'; import { chakra, Icon, Input, InputGroup, InputLeftElement, InputRightElement, Skeleton, useColorModeValue } from '@chakra-ui/react';
import type { ChangeEvent } from 'react'; import type { ChangeEvent } from 'react';
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
...@@ -11,9 +11,10 @@ type Props = { ...@@ -11,9 +11,10 @@ type Props = {
size?: 'xs' | 'sm' | 'md' | 'lg'; size?: 'xs' | 'sm' | 'md' | 'lg';
placeholder: string; placeholder: string;
initialValue?: string; initialValue?: string;
isLoading?: boolean;
} }
const FilterInput = ({ onChange, className, size = 'sm', placeholder, initialValue }: Props) => { const FilterInput = ({ onChange, className, size = 'sm', placeholder, initialValue, isLoading }: Props) => {
const [ filterQuery, setFilterQuery ] = useState(initialValue || ''); const [ filterQuery, setFilterQuery ] = useState(initialValue || '');
const inputRef = React.useRef<HTMLInputElement>(null); const inputRef = React.useRef<HTMLInputElement>(null);
const iconColor = useColorModeValue('blackAlpha.600', 'whiteAlpha.600'); const iconColor = useColorModeValue('blackAlpha.600', 'whiteAlpha.600');
...@@ -32,34 +33,38 @@ const FilterInput = ({ onChange, className, size = 'sm', placeholder, initialVal ...@@ -32,34 +33,38 @@ const FilterInput = ({ onChange, className, size = 'sm', placeholder, initialVal
}, [ onChange ]); }, [ onChange ]);
return ( return (
<InputGroup <Skeleton
size={ size } isLoaded={ !isLoading }
className={ className } className={ className }
minW="250px" minW="250px"
> >
<InputLeftElement <InputGroup
pointerEvents="none" size={ size }
> >
<Icon as={ searchIcon } color={ iconColor }/> <InputLeftElement
</InputLeftElement> pointerEvents="none"
>
<Icon as={ searchIcon } color={ iconColor }/>
</InputLeftElement>
<Input <Input
ref={ inputRef } ref={ inputRef }
size={ size } size={ size }
value={ filterQuery } value={ filterQuery }
onChange={ handleFilterQueryChange } onChange={ handleFilterQueryChange }
placeholder={ placeholder } placeholder={ placeholder }
borderWidth="2px" borderWidth="2px"
textOverflow="ellipsis" textOverflow="ellipsis"
whiteSpace="nowrap" whiteSpace="nowrap"
/> />
{ filterQuery ? ( { filterQuery ? (
<InputRightElement> <InputRightElement>
<InputClearButton onClick={ handleFilterQueryClear }/> <InputClearButton onClick={ handleFilterQueryClear }/>
</InputRightElement> </InputRightElement>
) : null } ) : null }
</InputGroup> </InputGroup>
</Skeleton>
); );
}; };
......
import { GridItem, Skeleton, SkeletonCircle } from '@chakra-ui/react';
import React from 'react';
const DetailsSkeletonRow = ({ w = '100%', maxW }: { w?: string; maxW?: string }) => {
return (
<>
<GridItem display="flex" columnGap={ 2 } w={{ base: '50%', lg: 'auto' }} _notFirst={{ mt: { base: 3, lg: 0 } }}>
<SkeletonCircle h={ 5 } w={ 5 }/>
<Skeleton flexGrow={ 1 } h={ 5 } borderRadius="full"/>
</GridItem>
<GridItem pl={{ base: 7, lg: 0 }}>
<Skeleton h={ 5 } borderRadius="full" w={{ base: '100%', lg: w }} maxW={ maxW }/>
</GridItem>
</>
);
};
export default DetailsSkeletonRow;
import { Box, Flex, Skeleton, SkeletonCircle, chakra } from '@chakra-ui/react';
import React from 'react';
const SkeletonList = ({ className }: {className?: string}) => {
return (
<Box className={ className }>
{ Array.from(Array(2)).map((item, index) => (
<Flex
key={ index }
rowGap={ 4 }
flexDirection="column"
paddingY={ 6 }
borderTopWidth="1px"
borderColor="divider"
_last={{
borderBottomWidth: '0px',
}}
>
<Flex h={ 4 }>
<Skeleton w="30%" mr={ 2 } borderRadius="full"/>
<Skeleton w="15%" borderRadius="full"/>
</Flex>
<Flex h={ 4 }>
<SkeletonCircle boxSize={ 4 } mr={ 2 } flexShrink={ 0 }/>
<Skeleton flexGrow={ 1 } mr={ 3 } borderRadius="full"/>
<Skeleton w={ 6 } mr={ 3 } borderRadius="full"/>
<SkeletonCircle boxSize={ 4 } mr={ 2 } flexShrink={ 0 }/>
<Skeleton flexGrow={ 1 } mr={ 3 } borderRadius="full"/>
</Flex>
<Skeleton w="75%" h={ 4 } borderRadius="full"/>
<Skeleton w="60%" h={ 4 } borderRadius="full"/>
</Flex>
)) }
</Box>
);
};
export default chakra(SkeletonList);
import { Box, Flex, Skeleton, SkeletonCircle } from '@chakra-ui/react';
import React from 'react';
interface Props {
showFooterSlot?: boolean;
}
const SkeletonListAccount = ({ showFooterSlot }: Props) => {
return (
<Box>
{ Array.from(Array(2)).map((item, index) => (
<Flex
key={ index }
rowGap={ 3 }
flexDirection="column"
paddingY={ 6 }
borderTopWidth="1px"
borderColor="divider"
_last={{
borderBottomWidth: '0px',
}}
>
<Flex columnGap={ 2 } w="100%" alignItems="center">
<SkeletonCircle size="6" flexShrink="0"/>
<Skeleton h={ 4 } w="100%"/>
</Flex>
<Skeleton h={ 4 } w="164px"/>
<Skeleton h={ 4 } w="164px"/>
<Flex columnGap={ 3 } mt={ 7 }>
{ showFooterSlot && (
<Flex alignItems="center" columnGap={ 2 }>
<Skeleton h={ 4 } w="164px"/>
<SkeletonCircle size="6" flexShrink="0"/>
</Flex>
) }
<SkeletonCircle size="6" flexShrink="0" ml="auto"/>
<SkeletonCircle size="6" flexShrink="0"/>
</Flex>
</Flex>
)) }
</Box>
);
};
export default SkeletonListAccount;
import { Box, HStack, Skeleton, chakra } from '@chakra-ui/react';
import React from 'react';
interface Props {
columns: Array<string>;
className?: string;
isLong?: boolean;
}
const SkeletonTable = ({ columns, className, isLong }: Props) => {
const rowsNum = isLong ? 50 : 3;
return (
<Box className={ className }>
<Skeleton height={ 10 } width="100%" borderBottomLeftRadius="none" borderBottomRightRadius="none"/>
{ Array.from(Array(rowsNum)).map((item, index) => (
<HStack key={ index } spacing={ 6 } marginTop={ 8 }>
{ columns.map((width, index) => (
<Skeleton
key={ index }
height={ 5 }
width={ width }
flexShrink={ width.includes('%') ? 'initial' : 0 }
borderRadius="full"
/>
)) }
</HStack>
)) }
</Box>
);
};
export default React.memo(chakra(SkeletonTable));
import { Flex, Skeleton, chakra, Box, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
import type { RoutedTab } from '../Tabs/types';
import useTabIndexFromQuery from 'ui/shared/Tabs/useTabIndexFromQuery';
interface Props {
className?: string;
tabs?: Array<RoutedTab>;
size?: 'sm' | 'md';
}
const SkeletonTabs = ({ className, tabs, size = 'md' }: Props) => {
const bgColor = useColorModeValue('blackAlpha.50', 'whiteAlpha.50');
const tabIndex = useTabIndexFromQuery(tabs || []);
if (tabs) {
if (tabs.length === 1) {
return null;
}
const paddingHor = size === 'sm' ? 3 : 4;
const paddingVert = size === 'sm' ? 1 : 2;
return (
<Flex className={ className } my={ 8 } alignItems="center" overflow="hidden">
{ tabs.slice(0, tabIndex).map(({ title, id }) => (
<Skeleton
key={ id }
mx={ paddingHor }
borderRadius="base"
fontWeight={ 600 }
borderWidth={ size === 'sm' ? '2px' : 0 }
flexShrink={ 0 }
>
{ typeof title === 'string' ? title : title() }
</Skeleton>
)) }
{ tabs.slice(tabIndex, tabIndex + 1).map(({ title, id }) => (
<Box key={ id } bgColor={ bgColor } px={ paddingHor } py={ paddingVert } borderRadius="base" flexShrink={ 0 }>
<Skeleton borderRadius="base" borderWidth={ size === 'sm' ? '2px' : 0 }>
{ typeof title === 'string' ? title : title() }
</Skeleton>
</Box>
)) }
{ tabs.slice(tabIndex + 1).map(({ title, id }) => (
<Skeleton
key={ id }
mx={ paddingHor }
borderRadius="base"
fontWeight={ 600 }
borderWidth={ size === 'sm' ? '2px' : 0 }
flexShrink={ 0 }
>
{ typeof title === 'string' ? title : title() }
</Skeleton>
)) }
</Flex>
);
}
return (
<Flex my={ 8 } className={ className }>
<Skeleton h={ 6 } my={ 2 } mx={ 4 } w="100px"/>
<Skeleton h={ 6 } my={ 2 } mx={ 4 } w="120px"/>
<Skeleton h={ 6 } my={ 2 } mx={ 4 } w="80px" display={{ base: 'none', lg: 'block' }}/>
<Skeleton h={ 6 } my={ 2 } mx={ 4 } w="100px" display={{ base: 'none', lg: 'block' }}/>
<Skeleton h={ 6 } my={ 2 } mx={ 4 } w="140px" display={{ base: 'none', lg: 'block' }}/>
</Flex>
);
};
export default chakra(SkeletonTabs);
...@@ -6,6 +6,8 @@ import useDebounce from 'lib/hooks/useDebounce'; ...@@ -6,6 +6,8 @@ import useDebounce from 'lib/hooks/useDebounce';
import useQueryWithPages from 'lib/hooks/useQueryWithPages'; import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import useUpdateValueEffect from 'lib/hooks/useUpdateValueEffect'; import useUpdateValueEffect from 'lib/hooks/useUpdateValueEffect';
import getQueryParamString from 'lib/router/getQueryParamString'; import getQueryParamString from 'lib/router/getQueryParamString';
import { SEARCH_RESULT_ITEM, SEARCH_RESULT_NEXT_PAGE_PARAMS } from 'stubs/search';
import { generateListStub } from 'stubs/utils';
export default function useSearchQuery(isSearchPage = false) { export default function useSearchQuery(isSearchPage = false) {
const router = useRouter(); const router = useRouter();
...@@ -20,7 +22,12 @@ export default function useSearchQuery(isSearchPage = false) { ...@@ -20,7 +22,12 @@ export default function useSearchQuery(isSearchPage = false) {
const query = useQueryWithPages({ const query = useQueryWithPages({
resourceName: 'search', resourceName: 'search',
filters: { q: debouncedSearchTerm }, filters: { q: debouncedSearchTerm },
options: { enabled: debouncedSearchTerm.trim().length > 0 }, options: {
enabled: debouncedSearchTerm.trim().length > 0,
placeholderData: isSearchPage ?
generateListStub<'search'>(SEARCH_RESULT_ITEM, 50, { next_page_params: SEARCH_RESULT_NEXT_PAGE_PARAMS }) :
undefined,
},
}); });
const redirectCheckQuery = useApiQuery('search_check_redirect', { const redirectCheckQuery = useApiQuery('search_check_redirect', {
......
import { Box, Text, useColorModeValue } from '@chakra-ui/react'; import { Box, Skeleton, useColorModeValue } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
type Props = { type Props = {
label: string; label: string;
value: string; value: string;
isLoading?: boolean;
} }
const NumberWidget = ({ label, value }: Props) => { const NumberWidget = ({ label, value, isLoading }: Props) => {
const bgColor = useColorModeValue('blue.50', 'blue.800');
const skeletonBgColor = useColorModeValue('blackAlpha.50', 'whiteAlpha.50');
return ( return (
<Box <Box
bg={ useColorModeValue('blue.50', 'blue.800') } bg={ isLoading ? skeletonBgColor : bgColor }
px={ 3 } px={ 3 }
py={{ base: 2, lg: 3 }} py={{ base: 2, lg: 3 }}
borderRadius={ 12 } borderRadius={ 12 }
> >
<Text <Skeleton
variant="secondary" isLoaded={ !isLoading }
color="text_secondary"
fontSize="xs" fontSize="xs"
w="fit-content"
> >
{ label } <span>{ label }</span>
</Text> </Skeleton>
<Text <Skeleton
isLoaded={ !isLoading }
fontWeight={ 500 } fontWeight={ 500 }
fontSize="lg" fontSize="lg"
w="fit-content"
> >
{ value } { value }
</Text> </Skeleton>
</Box> </Box>
); );
}; };
......
import { Box, Skeleton, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
const NumberWidgetSkeleton = () => {
const bgColor = useColorModeValue('blackAlpha.50', 'whiteAlpha.50');
return (
<Box
backgroundColor={ bgColor }
p={ 3 }
borderRadius={ 12 }
>
<Skeleton w="70px" h="10px" mb={ 2 }/>
<Skeleton w="100px" h="27px"/>
</Box>
);
};
export default NumberWidgetSkeleton;
...@@ -2,18 +2,17 @@ import { Grid } from '@chakra-ui/react'; ...@@ -2,18 +2,17 @@ import { Grid } 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 { STATS_COUNTER } from 'stubs/stats';
import DataFetchAlert from '../shared/DataFetchAlert'; import DataFetchAlert from '../shared/DataFetchAlert';
import NumberWidget from './NumberWidget'; import NumberWidget from './NumberWidget';
import NumberWidgetSkeleton from './NumberWidgetSkeleton';
const skeletonsCount = 8;
const NumberWidgetsList = () => { const NumberWidgetsList = () => {
const { data, isLoading, isError } = useApiQuery('stats_counters'); const { data, isPlaceholderData, isError } = useApiQuery('stats_counters', {
queryOptions: {
const skeletonElement = [ ...Array(skeletonsCount) ] placeholderData: { counters: Array(10).fill(STATS_COUNTER) },
.map((e, i) => <NumberWidgetSkeleton key={ i }/>); },
});
if (isError) { if (isError) {
return <DataFetchAlert/>; return <DataFetchAlert/>;
...@@ -24,17 +23,19 @@ const NumberWidgetsList = () => { ...@@ -24,17 +23,19 @@ const NumberWidgetsList = () => {
gridTemplateColumns={{ base: 'repeat(2, 1fr)', lg: 'repeat(4, 1fr)' }} gridTemplateColumns={{ base: 'repeat(2, 1fr)', lg: 'repeat(4, 1fr)' }}
gridGap={ 4 } gridGap={ 4 }
> >
{ isLoading ? skeletonElement : {
data?.counters?.map(({ id, title, value, units }) => { data?.counters?.map(({ id, title, value, units }, index) => {
return ( return (
<NumberWidget <NumberWidget
key={ id } key={ id + (isPlaceholderData ? index : '') }
label={ title } label={ title }
value={ `${ Number(value).toLocaleString(undefined, { maximumFractionDigits: 3, notation: 'compact' }) } ${ units ? units : '' }` } value={ `${ Number(value).toLocaleString(undefined, { maximumFractionDigits: 3, notation: 'compact' }) } ${ units ? units : '' }` }
isLoading={ isPlaceholderData }
/> />
); );
}) } })
}
</Grid> </Grid>
); );
}; };
......
...@@ -60,9 +60,7 @@ const TokenHoldersContent = ({ holdersQuery, token }: Props) => { ...@@ -60,9 +60,7 @@ const TokenHoldersContent = ({ holdersQuery, token }: Props) => {
return ( return (
<DataListDisplay <DataListDisplay
isError={ holdersQuery.isError } isError={ holdersQuery.isError }
isLoading={ false }
items={ holdersQuery.data?.items } items={ holdersQuery.data?.items }
skeletonProps={{ skeletonDesktopColumns: [ '100%', '300px', '175px' ] }}
emptyText="There are no holders for this token." emptyText="There are no holders for this token."
content={ content } content={ content }
actionBar={ actionBar } actionBar={ actionBar }
......
...@@ -50,12 +50,10 @@ const TokenInventory = ({ inventoryQuery }: Props) => { ...@@ -50,12 +50,10 @@ const TokenInventory = ({ inventoryQuery }: Props) => {
return ( return (
<DataListDisplay <DataListDisplay
isError={ inventoryQuery.isError } isError={ inventoryQuery.isError }
isLoading={ false }
items={ items } items={ items }
emptyText="There are no tokens." emptyText="There are no tokens."
content={ content } content={ content }
actionBar={ actionBar } actionBar={ actionBar }
skeletonProps={{ customSkeleton: null }}
/> />
); );
}; };
......
...@@ -99,12 +99,7 @@ const TokenTransfer = ({ transfersQuery, tokenId, token }: Props) => { ...@@ -99,12 +99,7 @@ const TokenTransfer = ({ transfersQuery, tokenId, token }: Props) => {
return ( return (
<DataListDisplay <DataListDisplay
isError={ isError } isError={ isError }
isLoading={ false }
items={ data?.items } items={ data?.items }
skeletonProps={{
isLongSkeleton: true,
skeletonDesktopColumns: [ '45%', '15%', '36px', '15%', '25%' ],
}}
emptyText="There are no token transfers." emptyText="There are no token transfers."
content={ content } content={ content }
actionBar={ actionBar } actionBar={ actionBar }
......
...@@ -116,9 +116,7 @@ const Tokens = () => { ...@@ -116,9 +116,7 @@ const Tokens = () => {
return ( return (
<DataListDisplay <DataListDisplay
isError={ isError } isError={ isError }
isLoading={ false }
items={ data?.items } items={ data?.items }
skeletonProps={{ skeletonDesktopColumns: [ '25px', '33%', '33%', '33%', '110px' ] }}
emptyText="There are no tokens." emptyText="There are no tokens."
filterProps={{ filterProps={{
emptyFilteredText: `Couldn${ apos }t find token that matches your filter query.`, emptyFilteredText: `Couldn${ apos }t find token that matches your filter query.`,
......
...@@ -130,9 +130,7 @@ const TxInternals = () => { ...@@ -130,9 +130,7 @@ const TxInternals = () => {
return ( return (
<DataListDisplay <DataListDisplay
isError={ isError || txInfo.isError } isError={ isError || txInfo.isError }
isLoading={ false }
items={ data?.items } items={ data?.items }
skeletonProps={{ skeletonDesktopColumns: [ '28%', '20%', '24px', '20%', '16%', '16%' ] }}
emptyText="There are no internal transactions for this transaction." emptyText="There are no internal transactions for this transaction."
// filterProps={{ // filterProps={{
// emptyFilteredText: `Couldn${ apos }t find any transaction that matches your query.`. // emptyFilteredText: `Couldn${ apos }t find any transaction that matches your query.`.
......
...@@ -45,11 +45,9 @@ const TxState = () => { ...@@ -45,11 +45,9 @@ const TxState = () => {
</Text> </Text>
<DataListDisplay <DataListDisplay
isError={ isError } isError={ isError }
isLoading={ false }
items={ data } items={ data }
emptyText="There are no state changes for this transaction." emptyText="There are no state changes for this transaction."
content={ content } content={ content }
skeletonProps={{ customSkeleton: null }}
/> />
</> </>
); );
......
...@@ -84,12 +84,7 @@ const TxTokenTransfer = () => { ...@@ -84,12 +84,7 @@ const TxTokenTransfer = () => {
return ( return (
<DataListDisplay <DataListDisplay
isError={ txsInfo.isError || tokenTransferQuery.isError } isError={ txsInfo.isError || tokenTransferQuery.isError }
isLoading={ false }
items={ tokenTransferQuery.data?.items } items={ tokenTransferQuery.data?.items }
skeletonProps={{
isLongSkeleton: true,
skeletonDesktopColumns: [ '185px', '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.`,
......
...@@ -30,7 +30,6 @@ type Props = { ...@@ -30,7 +30,6 @@ type Props = {
filter?: React.ReactNode; filter?: React.ReactNode;
enableTimeIncrement?: boolean; enableTimeIncrement?: boolean;
top?: number; top?: number;
hasLongSkeleton?: boolean;
} }
const TxsContent = ({ const TxsContent = ({
...@@ -42,7 +41,6 @@ const TxsContent = ({ ...@@ -42,7 +41,6 @@ const TxsContent = ({
socketInfoNum, socketInfoNum,
currentAddress, currentAddress,
enableTimeIncrement, enableTimeIncrement,
hasLongSkeleton,
top, top,
}: Props) => { }: Props) => {
const { data, isPlaceholderData, isError, setSortByField, setSortByValue, sorting } = useTxsSort(query); const { data, isPlaceholderData, isError, setSortByField, setSortByValue, sorting } = useTxsSort(query);
...@@ -107,14 +105,7 @@ const TxsContent = ({ ...@@ -107,14 +105,7 @@ const TxsContent = ({
return ( return (
<DataListDisplay <DataListDisplay
isError={ isError } isError={ isError }
isLoading={ false }
items={ data?.items } items={ data?.items }
skeletonProps={{
isLongSkeleton: hasLongSkeleton,
skeletonDesktopColumns: showBlockInfo ?
[ '32px', '22%', '160px', '20%', '18%', '292px', '20%', '20%' ] :
[ '32px', '22%', '160px', '20%', '292px', '20%', '20%' ],
}}
emptyText="There are no transactions." emptyText="There are no transactions."
content={ content } content={ content }
actionBar={ actionBar } actionBar={ actionBar }
......
import { Icon, IconButton, Link, Tooltip } from '@chakra-ui/react'; import { IconButton, Link, Skeleton, Tooltip } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { TokenInfoApplication, VerifiedAddress } from 'types/api/account'; import type { TokenInfoApplication, VerifiedAddress } from 'types/api/account';
...@@ -6,6 +6,7 @@ import type { TokenInfoApplication, VerifiedAddress } from 'types/api/account'; ...@@ -6,6 +6,7 @@ import type { TokenInfoApplication, VerifiedAddress } from 'types/api/account';
import editIcon from 'icons/edit.svg'; import editIcon from 'icons/edit.svg';
import dayjs from 'lib/date/dayjs'; import dayjs from 'lib/date/dayjs';
import AddressSnippet from 'ui/shared/AddressSnippet'; import AddressSnippet from 'ui/shared/AddressSnippet';
import Icon from 'ui/shared/chakra/Icon';
import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid'; import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid';
import VerifiedAddressesStatus from './VerifiedAddressesStatus'; import VerifiedAddressesStatus from './VerifiedAddressesStatus';
...@@ -16,64 +17,89 @@ interface Props { ...@@ -16,64 +17,89 @@ interface Props {
application: TokenInfoApplication | undefined; application: TokenInfoApplication | undefined;
onAdd: (address: string) => void; onAdd: (address: string) => void;
onEdit: (address: string) => void; onEdit: (address: string) => void;
isLoading: boolean;
} }
const VerifiedAddressesListItem = ({ item, application, onAdd, onEdit }: Props) => { const VerifiedAddressesListItem = ({ item, application, onAdd, onEdit, isLoading }: Props) => {
const handleAddClick = React.useCallback(() => { const handleAddClick = React.useCallback(() => {
if (isLoading) {
return;
}
onAdd(item.contractAddress); onAdd(item.contractAddress);
}, [ item, onAdd ]); }, [ isLoading, item.contractAddress, onAdd ]);
const handleEditClick = React.useCallback(() => { const handleEditClick = React.useCallback(() => {
if (isLoading) {
return;
}
onEdit(item.contractAddress); onEdit(item.contractAddress);
}, [ item, onEdit ]); }, [ isLoading, item.contractAddress, onEdit ]);
const tokenInfo = (() => {
if (isLoading) {
return <Skeleton height={ 6 } width="140px"/>;
}
if (!item.metadata.tokenName) {
return <span>Not a token</span>;
}
if (!application) {
return <Link onClick={ handleAddClick }>Add details</Link>;
}
return (
<>
<VerifiedAddressesTokenSnippet application={ application } name={ item.metadata.tokenName }/>
<Tooltip label="Edit">
<IconButton
aria-label="edit"
variant="simple"
boxSize={ 5 }
borderRadius="none"
flexShrink={ 0 }
onClick={ handleEditClick }
icon={ <Icon as={ editIcon }/> }
/>
</Tooltip>
</>
);
})();
return ( return (
<ListItemMobileGrid.Container> <ListItemMobileGrid.Container>
<ListItemMobileGrid.Label>Address</ListItemMobileGrid.Label> <ListItemMobileGrid.Label isLoading={ isLoading }>Address</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value py="3px"> <ListItemMobileGrid.Value py="3px">
<AddressSnippet address={{ hash: item.contractAddress, is_contract: true, implementation_name: null }}/> <AddressSnippet address={{ hash: item.contractAddress, is_contract: true, implementation_name: null }} isLoading={ isLoading }/>
</ListItemMobileGrid.Value> </ListItemMobileGrid.Value>
{ item.metadata.tokenName && ( { item.metadata.tokenName && (
<> <>
<ListItemMobileGrid.Label>Token Info</ListItemMobileGrid.Label> <ListItemMobileGrid.Label isLoading={ isLoading }>Token Info</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value py={ application ? '3px' : '5px' } display="flex" alignItems="center"> <ListItemMobileGrid.Value py={ application ? '3px' : '5px' } display="flex" alignItems="center">
{ application ? ( { tokenInfo }
<>
<VerifiedAddressesTokenSnippet application={ application } name={ item.metadata.tokenName }/>
<Tooltip label="Edit">
<IconButton
aria-label="edit"
variant="simple"
boxSize={ 5 }
borderRadius="none"
flexShrink={ 0 }
onClick={ handleEditClick }
icon={ <Icon as={ editIcon }/> }
/>
</Tooltip>
</>
) : (
<Link onClick={ handleAddClick }>Add details</Link>
) }
</ListItemMobileGrid.Value> </ListItemMobileGrid.Value>
</> </>
) } ) }
{ item.metadata.tokenName && application && ( { item.metadata.tokenName && application && (
<> <>
<ListItemMobileGrid.Label>Status</ListItemMobileGrid.Label> <ListItemMobileGrid.Label isLoading={ isLoading }>Status</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value> <ListItemMobileGrid.Value>
<VerifiedAddressesStatus status={ application.status }/> <Skeleton isLoaded={ !isLoading } display="inline-block">
<VerifiedAddressesStatus status={ application.status }/>
</Skeleton>
</ListItemMobileGrid.Value> </ListItemMobileGrid.Value>
</> </>
) } ) }
{ item.metadata.tokenName && application && ( { item.metadata.tokenName && application && (
<> <>
<ListItemMobileGrid.Label>Date</ListItemMobileGrid.Label> <ListItemMobileGrid.Label isLoading={ isLoading }>Date</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value> <ListItemMobileGrid.Value>
{ dayjs(application.updatedAt).format('MMM DD, YYYY') } <Skeleton isLoaded={ !isLoading } display="inline-block">
{ dayjs(application.updatedAt).format('MMM DD, YYYY') }
</Skeleton>
</ListItemMobileGrid.Value> </ListItemMobileGrid.Value>
</> </>
) } ) }
......
...@@ -10,9 +10,10 @@ interface Props { ...@@ -10,9 +10,10 @@ interface Props {
applications: Array<TokenInfoApplication> | undefined; applications: Array<TokenInfoApplication> | undefined;
onItemAdd: (address: string) => void; onItemAdd: (address: string) => void;
onItemEdit: (address: string) => void; onItemEdit: (address: string) => void;
isLoading: boolean;
} }
const VerifiedAddressesTable = ({ data, applications, onItemEdit, onItemAdd }: Props) => { const VerifiedAddressesTable = ({ data, applications, onItemEdit, onItemAdd, isLoading }: Props) => {
return ( return (
<Table variant="simple"> <Table variant="simple">
<Thead> <Thead>
...@@ -25,13 +26,14 @@ const VerifiedAddressesTable = ({ data, applications, onItemEdit, onItemAdd }: P ...@@ -25,13 +26,14 @@ const VerifiedAddressesTable = ({ data, applications, onItemEdit, onItemAdd }: P
</Tr> </Tr>
</Thead> </Thead>
<Tbody> <Tbody>
{ data.map((item) => ( { data.map((item, index) => (
<VerifiedAddressesTableItem <VerifiedAddressesTableItem
key={ item.contractAddress } key={ item.contractAddress + (isLoading ? index : '') }
item={ item } item={ item }
application={ applications?.find(({ tokenAddress }) => tokenAddress.toLowerCase() === item.contractAddress.toLowerCase()) } application={ applications?.find(({ tokenAddress }) => tokenAddress.toLowerCase() === item.contractAddress.toLowerCase()) }
onAdd={ onItemAdd } onAdd={ onItemAdd }
onEdit={ onItemEdit } onEdit={ onItemEdit }
isLoading={ isLoading }
/> />
)) } )) }
</Tbody> </Tbody>
......
import { Td, Tr, Link, Tooltip, IconButton, Icon } from '@chakra-ui/react'; import { Td, Tr, Link, Tooltip, IconButton, Skeleton } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { TokenInfoApplication, VerifiedAddress } from 'types/api/account'; import type { TokenInfoApplication, VerifiedAddress } from 'types/api/account';
...@@ -6,6 +6,7 @@ import type { TokenInfoApplication, VerifiedAddress } from 'types/api/account'; ...@@ -6,6 +6,7 @@ import type { TokenInfoApplication, VerifiedAddress } from 'types/api/account';
import editIcon from 'icons/edit.svg'; import editIcon from 'icons/edit.svg';
import dayjs from 'lib/date/dayjs'; import dayjs from 'lib/date/dayjs';
import AddressSnippet from 'ui/shared/AddressSnippet'; import AddressSnippet from 'ui/shared/AddressSnippet';
import Icon from 'ui/shared/chakra/Icon';
import VerifiedAddressesStatus from './VerifiedAddressesStatus'; import VerifiedAddressesStatus from './VerifiedAddressesStatus';
import VerifiedAddressesTokenSnippet from './VerifiedAddressesTokenSnippet'; import VerifiedAddressesTokenSnippet from './VerifiedAddressesTokenSnippet';
...@@ -15,19 +16,30 @@ interface Props { ...@@ -15,19 +16,30 @@ interface Props {
application: TokenInfoApplication | undefined; application: TokenInfoApplication | undefined;
onAdd: (address: string) => void; onAdd: (address: string) => void;
onEdit: (address: string) => void; onEdit: (address: string) => void;
isLoading: boolean;
} }
const VerifiedAddressesTableItem = ({ item, application, onAdd, onEdit }: Props) => { const VerifiedAddressesTableItem = ({ item, application, onAdd, onEdit, isLoading }: Props) => {
const handleAddClick = React.useCallback(() => { const handleAddClick = React.useCallback(() => {
if (isLoading) {
return;
}
onAdd(item.contractAddress); onAdd(item.contractAddress);
}, [ item, onAdd ]); }, [ isLoading, item.contractAddress, onAdd ]);
const handleEditClick = React.useCallback(() => { const handleEditClick = React.useCallback(() => {
if (isLoading) {
return;
}
onEdit(item.contractAddress); onEdit(item.contractAddress);
}, [ item, onEdit ]); }, [ isLoading, item.contractAddress, onEdit ]);
const tokenInfo = (() => { const tokenInfo = (() => {
if (isLoading) {
return <Skeleton height={ 6 } width="140px"/>;
}
if (!item.metadata.tokenName) { if (!item.metadata.tokenName) {
return <span>Not a token</span>; return <span>Not a token</span>;
} }
...@@ -42,14 +54,14 @@ const VerifiedAddressesTableItem = ({ item, application, onAdd, onEdit }: Props) ...@@ -42,14 +54,14 @@ const VerifiedAddressesTableItem = ({ item, application, onAdd, onEdit }: Props)
return ( return (
<Tr> <Tr>
<Td> <Td>
<AddressSnippet address={{ hash: item.contractAddress, is_contract: true, implementation_name: null }}/> <AddressSnippet address={{ hash: item.contractAddress, is_contract: true, implementation_name: null }} isLoading={ isLoading }/>
</Td> </Td>
<Td fontSize="sm" verticalAlign="middle" pr={ 1 }> <Td fontSize="sm" verticalAlign="middle" pr={ 1 }>
{ tokenInfo } { tokenInfo }
</Td> </Td>
<Td pl="0"> <Td pl="0">
{ item.metadata.tokenName && application ? ( { item.metadata.tokenName && application && !isLoading ? (
<Tooltip label="Edit"> <Tooltip label={ isLoading ? undefined : 'Edit' }>
<IconButton <IconButton
aria-label="edit" aria-label="edit"
variant="simple" variant="simple"
...@@ -62,8 +74,16 @@ const VerifiedAddressesTableItem = ({ item, application, onAdd, onEdit }: Props) ...@@ -62,8 +74,16 @@ const VerifiedAddressesTableItem = ({ item, application, onAdd, onEdit }: Props)
</Tooltip> </Tooltip>
) : null } ) : null }
</Td> </Td>
<Td fontSize="sm"><VerifiedAddressesStatus status={ item.metadata.tokenName ? application?.status : undefined }/></Td> <Td fontSize="sm">
<Td fontSize="sm" color="text_secondary">{ item.metadata.tokenName && application ? dayjs(application.updatedAt).format('MMM DD, YYYY') : null }</Td> <Skeleton isLoaded={ !isLoading } display="inline-block">
<VerifiedAddressesStatus status={ item.metadata.tokenName ? application?.status : undefined }/>
</Skeleton>
</Td>
<Td fontSize="sm" color="text_secondary">
<Skeleton isLoaded={ !isLoading } display="inline-block">
{ item.metadata.tokenName && application ? dayjs(application.updatedAt).format('MMM DD, YYYY') : null }
</Skeleton>
</Td>
</Tr> </Tr>
); );
}; };
......
...@@ -5,10 +5,16 @@ import type { VerifiedContract } from 'types/api/contracts'; ...@@ -5,10 +5,16 @@ import type { VerifiedContract } from 'types/api/contracts';
import VerifiedContractsListItem from './VerifiedContractsListItem'; import VerifiedContractsListItem from './VerifiedContractsListItem';
const VerifiedContractsList = ({ data }: { data: Array<VerifiedContract>}) => { const VerifiedContractsList = ({ data, isLoading }: { data: Array<VerifiedContract>; isLoading: boolean }) => {
return ( return (
<Box> <Box>
{ data.map((item) => <VerifiedContractsListItem key={ item.address.hash } data={ item }/>) } { data.map((item, index) => (
<VerifiedContractsListItem
key={ item.address.hash + (isLoading ? index : '') }
data={ item }
isLoading={ isLoading }
/>
)) }
</Box> </Box>
); );
}; };
......
import { Box, Flex, Icon } from '@chakra-ui/react'; import { Box, Flex, Skeleton } from '@chakra-ui/react';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import React from 'react'; import React from 'react';
...@@ -12,14 +12,16 @@ import dayjs from 'lib/date/dayjs'; ...@@ -12,14 +12,16 @@ 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 ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile'; import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
interface Props { interface Props {
data: VerifiedContract; data: VerifiedContract;
isLoading?: boolean;
} }
const VerifiedContractsListItem = ({ data }: Props) => { const VerifiedContractsListItem = ({ data, isLoading }: Props) => {
const balance = data.coin_balance && data.coin_balance !== '0' ? const balance = data.coin_balance && data.coin_balance !== '0' ?
BigNumber(data.coin_balance).div(10 ** appConfig.network.currency.decimals).dp(6).toFormat() : BigNumber(data.coin_balance).div(10 ** appConfig.network.currency.decimals).dp(6).toFormat() :
'0'; '0';
...@@ -27,50 +29,50 @@ const VerifiedContractsListItem = ({ data }: Props) => { ...@@ -27,50 +29,50 @@ const VerifiedContractsListItem = ({ data }: Props) => {
return ( return (
<ListItemMobile rowGap={ 3 }> <ListItemMobile rowGap={ 3 }>
<Address columnGap={ 2 } overflow="hidden" w="100%"> <Address columnGap={ 2 } overflow="hidden" w="100%">
<AddressIcon address={ data.address }/> <AddressIcon address={ data.address } isLoading={ isLoading }/>
<AddressLink hash={ data.address.hash } type="address" alias={ data.address.name }/> <AddressLink hash={ data.address.hash } type="address" alias={ data.address.name } isLoading={ isLoading }/>
<Box color="text_secondary" ml="auto"> <Skeleton isLoaded={ !isLoading } color="text_secondary" ml="auto">
<HashStringShorten hash={ data.address.hash } isTooltipDisabled/> <HashStringShorten hash={ data.address.hash } isTooltipDisabled/>
</Box> </Skeleton>
</Address> </Address>
<Flex columnGap={ 3 }> <Flex columnGap={ 3 }>
<Box fontWeight={ 500 }>Balance { appConfig.network.currency.symbol }</Box> <Skeleton isLoaded={ !isLoading } fontWeight={ 500 }>Balance { appConfig.network.currency.symbol }</Skeleton>
<Box color="text_secondary"> <Skeleton isLoaded={ !isLoading } color="text_secondary">
{ balance } <span>{ balance }</span>
</Box> </Skeleton>
</Flex> </Flex>
<Flex columnGap={ 3 }> <Flex columnGap={ 3 }>
<Box fontWeight={ 500 }>Txs count</Box> <Skeleton isLoaded={ !isLoading } fontWeight={ 500 }>Txs count</Skeleton>
<Box color="text_secondary"> <Skeleton isLoaded={ !isLoading } color="text_secondary">
{ data.tx_count ? data.tx_count.toLocaleString() : '0' } <span>{ data.tx_count ? data.tx_count.toLocaleString() : '0' }</span>
</Box> </Skeleton>
</Flex> </Flex>
<Flex columnGap={ 3 }> <Flex columnGap={ 3 }>
<Box fontWeight={ 500 } flexShrink="0">Compiler</Box> <Skeleton isLoaded={ !isLoading } fontWeight={ 500 } flexShrink="0">Compiler</Skeleton>
<Flex flexWrap="wrap"> <Skeleton isLoaded={ !isLoading } display="flex" flexWrap="wrap">
<Box textTransform="capitalize">{ data.language }</Box> <Box textTransform="capitalize">{ data.language }</Box>
<Box color="text_secondary" wordBreak="break-all" whiteSpace="pre-wrap"> ({ data.compiler_version })</Box> <Box color="text_secondary" wordBreak="break-all" whiteSpace="pre-wrap"> ({ data.compiler_version })</Box>
</Flex> </Skeleton>
</Flex> </Flex>
<Flex columnGap={ 3 }> <Flex columnGap={ 3 }>
<Box fontWeight={ 500 }>Optimization</Box> <Skeleton isLoaded={ !isLoading } fontWeight={ 500 }>Optimization</Skeleton>
{ data.optimization_enabled ? { data.optimization_enabled ?
<Icon as={ iconCheck } boxSize={ 6 } color="green.500" cursor="pointer"/> : <Icon as={ iconCheck } boxSize={ 6 } color="green.500" cursor="pointer" isLoading={ isLoading }/> :
<Icon as={ iconCross } boxSize={ 6 } color="red.600" cursor="pointer"/> } <Icon as={ iconCross } boxSize={ 6 } color="red.600" cursor="pointer" isLoading={ isLoading }/> }
</Flex> </Flex>
<Flex columnGap={ 3 }> <Flex columnGap={ 3 }>
<Box fontWeight={ 500 }>Constructor args</Box> <Skeleton isLoaded={ !isLoading } fontWeight={ 500 }>Constructor args</Skeleton>
{ data.has_constructor_args ? { data.has_constructor_args ?
<Icon as={ iconCheck } boxSize={ 6 } color="green.500" cursor="pointer"/> : <Icon as={ iconCheck } boxSize={ 6 } color="green.500" cursor="pointer" isLoading={ isLoading }/> :
<Icon as={ iconCross } boxSize={ 6 } color="red.600" cursor="pointer"/> } <Icon as={ iconCross } boxSize={ 6 } color="red.600" cursor="pointer" isLoading={ isLoading }/> }
</Flex> </Flex>
<Flex columnGap={ 3 }> <Flex columnGap={ 3 }>
<Box fontWeight={ 500 }>Verified</Box> <Skeleton isLoaded={ !isLoading } fontWeight={ 500 }>Verified</Skeleton>
<Flex alignItems="center" columnGap={ 2 }> <Flex alignItems="center" columnGap={ 2 }>
<Icon as={ iconSuccess } boxSize={ 4 } color="green.500"/> <Icon as={ iconSuccess } boxSize={ 4 } color="green.500" isLoading={ isLoading }/>
<Box color="text_secondary"> <Skeleton isLoaded={ !isLoading } color="text_secondary">
{ dayjs(data.verified_at).fromNow() } <span>{ dayjs(data.verified_at).fromNow() }</span>
</Box> </Skeleton>
</Flex> </Flex>
</Flex> </Flex>
{ /* <Flex columnGap={ 3 }> { /* <Flex columnGap={ 3 }>
......
...@@ -14,9 +14,10 @@ interface Props { ...@@ -14,9 +14,10 @@ interface Props {
data: Array<VerifiedContract>; data: Array<VerifiedContract>;
sort: Sort | undefined; sort: Sort | undefined;
onSortToggle: (field: SortField) => () => void; onSortToggle: (field: SortField) => () => void;
isLoading?: boolean;
} }
const VerifiedContractsTable = ({ data, sort, onSortToggle }: Props) => { const VerifiedContractsTable = ({ data, sort, onSortToggle, isLoading }: Props) => {
const sortIconTransform = sort?.includes('asc') ? 'rotate(-90deg)' : 'rotate(90deg)'; const sortIconTransform = sort?.includes('asc') ? 'rotate(-90deg)' : 'rotate(90deg)';
return ( return (
...@@ -25,13 +26,13 @@ const VerifiedContractsTable = ({ data, sort, onSortToggle }: Props) => { ...@@ -25,13 +26,13 @@ const VerifiedContractsTable = ({ data, sort, onSortToggle }: Props) => {
<Tr> <Tr>
<Th width="50%">Contract</Th> <Th width="50%">Contract</Th>
<Th width="130px" isNumeric> <Th width="130px" isNumeric>
<Link display="flex" alignItems="center" justifyContent="flex-end" onClick={ onSortToggle('balance') } columnGap={ 1 }> <Link display="flex" alignItems="center" justifyContent="flex-end" onClick={ isLoading ? undefined : onSortToggle('balance') } columnGap={ 1 }>
{ sort?.includes('balance') && <Icon as={ arrowIcon } boxSize={ 4 } transform={ sortIconTransform }/> } { sort?.includes('balance') && <Icon as={ arrowIcon } boxSize={ 4 } transform={ sortIconTransform }/> }
Balance { appConfig.network.currency.symbol } Balance { appConfig.network.currency.symbol }
</Link> </Link>
</Th> </Th>
<Th width="130px" isNumeric> <Th width="130px" isNumeric>
<Link display="flex" alignItems="center" justifyContent="flex-end" onClick={ onSortToggle('txs') } columnGap={ 1 }> <Link display="flex" alignItems="center" justifyContent="flex-end" onClick={ isLoading ? undefined : onSortToggle('txs') } columnGap={ 1 }>
{ sort?.includes('txs') && <Icon as={ arrowIcon } boxSize={ 4 } transform={ sortIconTransform }/> } { sort?.includes('txs') && <Icon as={ arrowIcon } boxSize={ 4 } transform={ sortIconTransform }/> }
Txs Txs
</Link> </Link>
...@@ -43,7 +44,12 @@ const VerifiedContractsTable = ({ data, sort, onSortToggle }: Props) => { ...@@ -43,7 +44,12 @@ const VerifiedContractsTable = ({ data, sort, onSortToggle }: Props) => {
</Tr> </Tr>
</Thead> </Thead>
<Tbody> <Tbody>
{ data.map((item) => <VerifiedContractsTableItem key={ item.address.hash } data={ item }/>) } { data.map((item, index) => (
<VerifiedContractsTableItem
key={ item.address.hash + (isLoading ? index : '') }
data={ item }
isLoading={ isLoading }/>
)) }
</Tbody> </Tbody>
</Table> </Table>
); );
......
import { Tr, Td, Icon, Box, Flex, chakra, Tooltip } from '@chakra-ui/react'; import { Tr, Td, Flex, chakra, Tooltip, Skeleton } from '@chakra-ui/react';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import React from 'react'; import React from 'react';
...@@ -11,13 +11,15 @@ import iconSuccess from 'icons/status/success.svg'; ...@@ -11,13 +11,15 @@ import iconSuccess from 'icons/status/success.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 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';
interface Props { interface Props {
data: VerifiedContract; data: VerifiedContract;
isLoading?: boolean;
} }
const VerifiedContractsTableItem = ({ data }: Props) => { const VerifiedContractsTableItem = ({ data, isLoading }: Props) => {
const balance = data.coin_balance && data.coin_balance !== '0' ? const balance = data.coin_balance && data.coin_balance !== '0' ?
BigNumber(data.coin_balance).div(10 ** appConfig.network.currency.decimals).dp(6).toFormat() : BigNumber(data.coin_balance).div(10 ** appConfig.network.currency.decimals).dp(6).toFormat() :
'0'; '0';
...@@ -26,52 +28,58 @@ const VerifiedContractsTableItem = ({ data }: Props) => { ...@@ -26,52 +28,58 @@ const VerifiedContractsTableItem = ({ data }: Props) => {
<Tr> <Tr>
<Td> <Td>
<Flex columnGap={ 2 }> <Flex columnGap={ 2 }>
<AddressIcon address={ data.address }/> <AddressIcon address={ data.address } isLoading={ isLoading }/>
<Flex columnGap={ 2 } flexWrap="wrap" lineHeight={ 6 } w="calc(100% - 32px)"> <Flex columnGap={ 2 } flexWrap="wrap" w="calc(100% - 32px)">
<AddressLink hash={ data.address.hash } type="address" alias={ data.address.name }/> <AddressLink hash={ data.address.hash } type="address" alias={ data.address.name } isLoading={ isLoading } my={ 1 }/>
<Box color="text_secondary"> <Skeleton isLoaded={ !isLoading } color="text_secondary" my={ 1 }>
<HashStringShorten hash={ data.address.hash } isTooltipDisabled/> <HashStringShorten hash={ data.address.hash } isTooltipDisabled/>
</Box> </Skeleton>
</Flex> </Flex>
</Flex> </Flex>
</Td> </Td>
<Td isNumeric lineHeight={ 6 }> <Td isNumeric>
{ balance } <Skeleton isLoaded={ !isLoading } display="inline-block" my={ 1 }>
{ balance }
</Skeleton>
</Td> </Td>
<Td isNumeric lineHeight={ 6 }> <Td isNumeric>
{ data.tx_count ? data.tx_count.toLocaleString() : '0' } <Skeleton isLoaded={ !isLoading } display="inline-block" my={ 1 }>
{ data.tx_count ? data.tx_count.toLocaleString() : '0' }
</Skeleton>
</Td> </Td>
<Td lineHeight={ 6 }> <Td>
<Flex flexWrap="wrap" columnGap={ 2 }> <Flex flexWrap="wrap" columnGap={ 2 }>
<chakra.span textTransform="capitalize">{ data.language }</chakra.span> <Skeleton isLoaded={ !isLoading } textTransform="capitalize" my={ 1 }>{ data.language }</Skeleton>
<chakra.span color="text_secondary" wordBreak="break-all">{ data.compiler_version }</chakra.span> <Skeleton isLoaded={ !isLoading } color="text_secondary" wordBreak="break-all" my={ 1 }>
<span>{ data.compiler_version }</span>
</Skeleton>
</Flex> </Flex>
</Td> </Td>
<Td> <Td>
<Tooltip label="Optimization"> <Tooltip label={ isLoading ? undefined : 'Optimization' }>
<span> <chakra.span display="inline-block">
{ data.optimization_enabled ? { data.optimization_enabled ?
<Icon as={ iconCheck } boxSize={ 6 } color="green.500" cursor="pointer"/> : <Icon as={ iconCheck } boxSize={ 6 } color="green.500" cursor="pointer" isLoading={ isLoading }/> :
<Icon as={ iconCross } boxSize={ 6 } color="red.600" cursor="pointer"/> } <Icon as={ iconCross } boxSize={ 6 } color="red.600" cursor="pointer" isLoading={ isLoading }/> }
</span> </chakra.span>
</Tooltip> </Tooltip>
<Tooltip label="Constructor args"> <Tooltip label={ isLoading ? undefined : 'Constructor args' }>
<chakra.span ml={ 3 }> <chakra.span display="inline-block" ml={ 3 }>
{ data.has_constructor_args ? { data.has_constructor_args ?
<Icon as={ iconCheck } boxSize={ 6 } color="green.500" cursor="pointer"/> : <Icon as={ iconCheck } boxSize={ 6 } color="green.500" cursor="pointer" isLoading={ isLoading }/> :
<Icon as={ iconCross } boxSize={ 6 } color="red.600" cursor="pointer"/> } <Icon as={ iconCross } boxSize={ 6 } color="red.600" cursor="pointer" isLoading={ isLoading }/> }
</chakra.span> </chakra.span>
</Tooltip> </Tooltip>
</Td> </Td>
<Td lineHeight={ 6 }> <Td>
<Flex alignItems="center" columnGap={ 2 }> <Flex alignItems="center" columnGap={ 2 } my={ 1 }>
<Icon as={ iconSuccess } boxSize={ 4 } color="green.500"/> <Icon as={ iconSuccess } boxSize={ 4 } color="green.500" isLoading={ isLoading }/>
<chakra.span color="text_secondary"> <Skeleton isLoaded={ !isLoading } color="text_secondary">
{ dayjs(data.verified_at).fromNow() } <span>{ dayjs(data.verified_at).fromNow() }</span>
</chakra.span> </Skeleton>
</Flex> </Flex>
</Td> </Td>
{ /* <Td lineHeight={ 6 }> { /* <Td>
N/A N/A
</Td> */ } </Td> */ }
</Tr> </Tr>
......
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