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