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