Commit 18845138 authored by tom's avatar tom

skeletons for homepage

parent 825aac59
......@@ -13,10 +13,11 @@ import useIsMobile from 'lib/hooks/useIsMobile';
import { nbsp } from 'lib/html-entities';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
import { BLOCK } from 'stubs/block';
import { HOMEPAGE_STATS } from 'stubs/stats';
import LinkInternal from 'ui/shared/LinkInternal';
import LatestBlocksItem from './LatestBlocksItem';
import LatestBlocksItemSkeleton from './LatestBlocksItemSkeleton';
const BLOCK_HEIGHT_L1 = 166;
const BLOCK_HEIGHT_L2 = 112;
......@@ -32,10 +33,18 @@ const LatestBlocks = () => {
} else {
blocksMaxCount = isMobile ? 2 : 3;
}
const { data, isLoading, isError } = useApiQuery('homepage_blocks');
const { data, isPlaceholderData, isError } = useApiQuery('homepage_blocks', {
queryOptions: {
placeholderData: Array(4).fill(BLOCK),
},
});
const queryClient = useQueryClient();
const statsQueryResult = useApiQuery('homepage_stats');
const statsQueryResult = useApiQuery('homepage_stats', {
queryOptions: {
placeholderData: HOMEPAGE_STATS,
},
});
const handleNewBlockMessage: SocketMessage.NewBlock['handler'] = React.useCallback((payload) => {
queryClient.setQueryData(getResourceKey('homepage_blocks'), (prevData: Array<Block> | undefined) => {
......@@ -52,7 +61,7 @@ const LatestBlocks = () => {
const channel = useSocketChannel({
topic: 'blocks:new_block',
isDisabled: isLoading || isError,
isDisabled: isPlaceholderData || isError,
});
useSocketMessage({
channel,
......@@ -62,22 +71,6 @@ const LatestBlocks = () => {
let content;
if (isLoading) {
content = (
<>
<Skeleton w="100%" h={ 6 } mb={ 9 }/>
<VStack
spacing={ `${ BLOCK_MARGIN }px` }
mb={ 6 }
height={ `${ blockHeight * blocksMaxCount + BLOCK_MARGIN * (blocksMaxCount - 1) }px` }
overflow="hidden"
>
{ Array.from(Array(blocksMaxCount)).map((item, index) => <LatestBlocksItemSkeleton key={ index }/>) }
</VStack>
</>
);
}
if (isError) {
content = <Text>No data. Please reload page.</Text>;
}
......@@ -88,22 +81,26 @@ const LatestBlocks = () => {
content = (
<>
{ statsQueryResult.isLoading && (
<Skeleton h="24px" w="170px" mb={{ base: 6, lg: 9 }}/>
) }
{ statsQueryResult.data?.network_utilization_percentage !== undefined && (
<Box mb={{ base: 6, lg: 3 }}>
<Skeleton isLoaded={ !statsQueryResult.isPlaceholderData } mb={{ base: 6, lg: 3 }} display="inline-block">
<Text as="span" fontSize="sm">
Network utilization:{ nbsp }
</Text>
<Text as="span" fontSize="sm" color="blue.400" fontWeight={ 700 }>
{ statsQueryResult.data?.network_utilization_percentage.toFixed(2) }%
</Text>
</Box>
</Skeleton>
) }
<VStack spacing={ `${ BLOCK_MARGIN }px` } mb={ 4 } height={ `${ blockHeight * blocksCount + BLOCK_MARGIN * (blocksCount - 1) }px` } overflow="hidden">
<AnimatePresence initial={ false } >
{ dataToShow.map((block => <LatestBlocksItem key={ block.height } block={ block } h={ blockHeight }/>)) }
{ dataToShow.map(((block, index) => (
<LatestBlocksItem
key={ block.height + (isPlaceholderData ? String(index) : '') }
block={ block }
h={ blockHeight }
isLoading={ isPlaceholderData }
/>
))) }
</AnimatePresence>
</VStack>
<Flex justifyContent="center">
......
......@@ -2,10 +2,8 @@ import {
Box,
Flex,
Grid,
GridItem,
HStack,
Icon,
Text,
Skeleton,
} from '@chakra-ui/react';
import { motion } from 'framer-motion';
import { route } from 'nextjs-routes';
......@@ -18,14 +16,16 @@ import blockIcon from 'icons/block.svg';
import getBlockTotalReward from 'lib/block/getBlockTotalReward';
import BlockTimestamp from 'ui/blocks/BlockTimestamp';
import AddressLink from 'ui/shared/address/AddressLink';
import Icon from 'ui/shared/chakra/Icon';
import LinkInternal from 'ui/shared/LinkInternal';
type Props = {
block: Block;
h: number;
isLoading?: boolean;
}
const LatestBlocksItem = ({ block, h }: Props) => {
const LatestBlocksItem = ({ block, h, isLoading }: Props) => {
const totalReward = getBlockTotalReward(block);
return (
<Box
......@@ -43,26 +43,29 @@ const LatestBlocksItem = ({ block, h }: Props) => {
>
<Flex justifyContent="space-between" alignItems="center" mb={ 3 }>
<HStack spacing={ 2 }>
<Icon as={ blockIcon } boxSize="30px" color="link"/>
<Icon as={ blockIcon } boxSize="30px" color="link" isLoading={ isLoading } borderRadius="base"/>
<LinkInternal
href={ route({ pathname: '/block/[height_or_hash]', query: { height_or_hash: String(block.height) } }) }
fontSize="xl"
fontWeight="500"
isLoading={ isLoading }
>
{ block.height }
<Skeleton isLoaded={ !isLoading }>
{ block.height }
</Skeleton>
</LinkInternal>
</HStack>
<BlockTimestamp ts={ block.timestamp } isEnabled fontSize="sm"/>
<BlockTimestamp ts={ block.timestamp } isEnabled={ !isLoading } isLoading={ isLoading } fontSize="sm"/>
</Flex>
<Grid gridGap={ 2 } templateColumns="auto minmax(0, 1fr)" fontSize="sm">
<GridItem>Txn</GridItem>
<GridItem><Text variant="secondary">{ block.tx_count }</Text></GridItem>
<Skeleton isLoaded={ !isLoading }>Txn</Skeleton>
<Skeleton isLoaded={ !isLoading } color="text_secondary"><span>{ block.tx_count }</span></Skeleton>
{ !appConfig.L2.isL2Network && (
<>
<GridItem>Reward</GridItem>
<GridItem><Text variant="secondary">{ totalReward.toFixed() }</Text></GridItem>
<GridItem>Miner</GridItem>
<GridItem><AddressLink type="address" alias={ block.miner.name } hash={ block.miner.hash } truncation="constant" maxW="100%"/></GridItem>
<Skeleton isLoaded={ !isLoading }>Reward</Skeleton>
<Skeleton isLoaded={ !isLoading } color="text_secondary"><span>{ totalReward.toFixed() }</span></Skeleton>
<Skeleton isLoaded={ !isLoading }>Miner</Skeleton>
<AddressLink type="address" alias={ block.miner.name } hash={ block.miner.hash } truncation="constant" maxW="100%" isLoading={ isLoading }/>
</>
) }
</Grid>
......
import {
Box,
Flex,
Grid,
GridItem,
HStack,
Skeleton,
} from '@chakra-ui/react';
import React from 'react';
import appConfig from 'configs/app/config';
const LatestBlocksItemSkeleton = () => {
return (
<Box
minWidth={{ base: '100%', lg: '280px' }}
borderRadius="12px"
border="1px solid"
borderColor="divider"
p={ 6 }
>
<Flex justifyContent="space-between" alignItems="center" mb={ 3 }>
<HStack spacing={ 2 }>
<Skeleton w="30px" h="30px"/>
<Skeleton w="93px" h="15px"/>
</HStack>
<Skeleton w="44px" h="15px"/>
</Flex>
<Grid gridGap={ 2 } templateColumns="auto minmax(0, 1fr)" fontSize="sm">
<GridItem><Skeleton w="30px" h="15px"/></GridItem>
<GridItem><Skeleton w="93px" h="15px"/></GridItem>
{ !appConfig.L2.isL2Network && (
<>
<GridItem><Skeleton w="30px" h="15px"/></GridItem>
<GridItem><Skeleton w="93px" h="15px"/></GridItem>
<GridItem><Skeleton w="30px" h="15px"/></GridItem>
<GridItem><Skeleton w="93px" h="15px"/></GridItem>
</>
) }
</Grid>
</Box>
);
};
export default LatestBlocksItemSkeleton;
import { Box, Flex, Text, Skeleton } from '@chakra-ui/react';
import { Box, Flex, Text } from '@chakra-ui/react';
import { route } from 'nextjs-routes';
import React from 'react';
......@@ -9,16 +9,20 @@ import useGradualIncrement from 'lib/hooks/useGradualIncrement';
import useIsMobile from 'lib/hooks/useIsMobile';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
import { L2_DEPOSIT_ITEM } from 'stubs/L2';
import LinkInternal from 'ui/shared/LinkInternal';
import SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice';
import LatestDepositsItem from './LatestDepositsItem';
import LatestDepositsItemSkeleton from './LatestDepositsItemSkeleton';
const LatestDeposits = () => {
const isMobile = useIsMobile();
const itemsCount = isMobile ? 2 : 6;
const { data, isLoading, isError } = useApiQuery('homepage_deposits');
const { data, isPlaceholderData, isError } = useApiQuery('homepage_deposits', {
queryOptions: {
placeholderData: Array(itemsCount).fill(L2_DEPOSIT_ITEM),
},
});
const [ num, setNum ] = useGradualIncrement(0);
const [ socketAlert, setSocketAlert ] = React.useState('');
......@@ -48,15 +52,6 @@ const LatestDeposits = () => {
handler: handleNewDepositMessage,
});
if (isLoading) {
return (
<>
<Skeleton h="32px" w="100%" borderBottomLeftRadius={ 0 } borderBottomRightRadius={ 0 }/>
{ Array.from(Array(itemsCount)).map((item, index) => <LatestDepositsItemSkeleton key={ index }/>) }
</>
);
}
if (isError) {
return <Text mt={ 4 }>No data. Please reload page.</Text>;
}
......@@ -65,9 +60,15 @@ const LatestDeposits = () => {
const depositsUrl = route({ pathname: '/l2-deposits' });
return (
<>
<SocketNewItemsNotice borderBottomRadius={ 0 } url={ depositsUrl } num={ num } alert={ socketAlert } type="deposit"/>
<SocketNewItemsNotice borderBottomRadius={ 0 } url={ depositsUrl } num={ num } alert={ socketAlert } type="deposit" isLoading={ isPlaceholderData }/>
<Box mb={{ base: 3, lg: 4 }}>
{ data.slice(0, itemsCount).map((item => <LatestDepositsItem key={ item.l2_tx_hash } item={ item }/>)) }
{ data.slice(0, itemsCount).map(((item, index) => (
<LatestDepositsItem
key={ item.l2_tx_hash + (isPlaceholderData ? index : '') }
item={ item }
isLoading={ isPlaceholderData }
/>
))) }
</Box>
<Flex justifyContent="center">
<LinkInternal fontSize="sm" href={ depositsUrl }>View all deposits</LinkInternal>
......
......@@ -2,8 +2,7 @@ import {
Box,
Flex,
Grid,
Icon,
Text,
Skeleton,
} from '@chakra-ui/react';
import { route } from 'nextjs-routes';
import React from 'react';
......@@ -15,15 +14,17 @@ import blockIcon from 'icons/block.svg';
import txIcon from 'icons/transactions.svg';
import dayjs from 'lib/date/dayjs';
import useIsMobile from 'lib/hooks/useIsMobile';
import Icon from 'ui/shared/chakra/Icon';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import LinkExternal from 'ui/shared/LinkExternal';
import LinkInternal from 'ui/shared/LinkInternal';
type Props = {
item: L2DepositsItem;
isLoading?: boolean;
}
const LatestTxsItem = ({ item }: Props) => {
const LatestTxsItem = ({ item, isLoading }: Props) => {
const timeAgo = dayjs(item.l1_block_timestamp).fromNow();
const isMobile = useIsMobile();
......@@ -35,9 +36,10 @@ const LatestTxsItem = ({ item }: Props) => {
fontWeight={ 700 }
display="inline-flex"
mr={ 2 }
isLoading={ isLoading }
>
<Icon as={ blockIcon } boxSize="30px" mr={ 1 }/>
{ item.l1_block_number }
<Icon as={ blockIcon } boxSize="30px" isLoading={ isLoading } borderRadius="base"/>
<Skeleton isLoaded={ !isLoading } ml={ 1 }>{ item.l1_block_number }</Skeleton>
</LinkExternal>
);
......@@ -48,9 +50,13 @@ const LatestTxsItem = ({ item }: Props) => {
display="inline-flex"
alignItems="center"
overflow="hidden"
isLoading={ isLoading }
my="3px"
>
<Icon as={ txIcon } boxSize={ 6 } mr={ 1 }/>
<Box overflow="hidden" whiteSpace="nowrap"><HashStringShortenDynamic hash={ item.l1_tx_hash }/></Box>
<Icon as={ txIcon } boxSize={ 6 } isLoading={ isLoading }/>
<Skeleton isLoaded={ !isLoading } overflow="hidden" whiteSpace="nowrap" ml={ 1 }>
<HashStringShortenDynamic hash={ item.l1_tx_hash }/>
</Skeleton>
</LinkExternal>
);
......@@ -61,9 +67,12 @@ const LatestTxsItem = ({ item }: Props) => {
alignItems="center"
overflow="hidden"
w="100%"
isLoading={ isLoading }
>
<Icon as={ txIcon } boxSize={ 6 } mr={ 1 }/>
<Box w="calc(100% - 36px)" overflow="hidden" whiteSpace="nowrap"><HashStringShortenDynamic hash={ item.l2_tx_hash }/></Box>
<Icon as={ txIcon } boxSize={ 6 } isLoading={ isLoading }/>
<Skeleton isLoaded={ !isLoading } w="calc(100% - 36px)" overflow="hidden" whiteSpace="nowrap" ml={ 1 }>
<HashStringShortenDynamic hash={ item.l2_tx_hash }/>
</Skeleton>
</LinkInternal>
);
......@@ -73,28 +82,42 @@ const LatestTxsItem = ({ item }: Props) => {
<>
<Flex justifyContent="space-between" alignItems="center" mb={ 1 }>
{ l1BlockLink }
<Text variant="secondary">{ timeAgo }</Text>
<Skeleton isLoaded={ !isLoading } color="text_secondary">
<span>{ timeAgo }</span>
</Skeleton>
</Flex>
<Grid gridTemplateColumns="56px auto">
<Text lineHeight="30px">L1 txn</Text>
<Skeleton isLoaded={ !isLoading } my="5px" w="fit-content">
L1 txn
</Skeleton>
{ l1TxLink }
<Text lineHeight="30px">L2 txn</Text>
<Skeleton isLoaded={ !isLoading } my="3px" w="fit-content">
L2 txn
</Skeleton>
{ l2TxLink }
</Grid>
</>
);
}
return (
<Grid width="100%" columnGap={ 4 } rowGap={ 2 } templateColumns="max-content max-content auto" w="100%">
{ l1BlockLink }
<Text lineHeight="30px">L1 txn</Text>
<Skeleton isLoaded={ !isLoading } w="fit-content" h="fit-content" my="5px">
L1 txn
</Skeleton>
{ l1TxLink }
<Text variant="secondary">{ timeAgo }</Text>
<Text lineHeight="30px">L2 txn</Text>
<Skeleton isLoaded={ !isLoading } color="text_secondary" w="fit-content" h="fit-content" my="2px">
<span>{ timeAgo }</span>
</Skeleton>
<Skeleton isLoaded={ !isLoading } w="fit-content" h="fit-content" my="2px">
L2 txn
</Skeleton>
{ l2TxLink }
</Grid>
);
})();
return (
<Box
width="100%"
......@@ -104,6 +127,7 @@ const LatestTxsItem = ({ item }: Props) => {
px={{ base: 0, lg: 4 }}
_last={{ borderBottom: '1px solid', borderColor: 'divider' }}
fontSize="sm"
lineHeight={ 5 }
>
{ content }
</Box>
......
import {
Box,
Flex,
Skeleton,
} from '@chakra-ui/react';
import React from 'react';
import useIsMobile from 'lib/hooks/useIsMobile';
const LatestTxsItemSkeleton = () => {
const isMobile = useIsMobile();
return (
<Box
width="100%"
borderTop="1px solid"
borderColor="divider"
py={ 4 }
px={{ base: 0, lg: 4 }}
_last={{ borderBottom: '1px solid', borderColor: 'divider' }}
>
{ isMobile && (
<>
<Flex justifyContent="space-between" alignItems="center" mt={ 1 } mb={ 4 }>
<Skeleton w="120px" h="20px"></Skeleton>
<Skeleton w="80px" h="20px"></Skeleton>
</Flex>
<Skeleton w="100%" h="20px" mb={ 2 }></Skeleton>
<Skeleton w="100%" h="20px" mb={ 2 }></Skeleton>
</>
) }
{ !isMobile && (
<>
<Flex w="100%" mb={ 2 } h="30px" alignItems="center" justifyContent="space-between">
<Skeleton w="120px" h="20px"></Skeleton>
<Skeleton w="calc(100% - 120px - 48px)" h="20px"></Skeleton>
</Flex><Flex w="100%" h="30px" alignItems="center" justifyContent="space-between">
<Skeleton w="120px" h="20px"></Skeleton>
<Skeleton w="calc(100% - 120px - 48px)" h="20px"></Skeleton>
</Flex>
</>
) }
</Box>
);
};
export default LatestTxsItemSkeleton;
import { Box, Flex, Text, Skeleton } from '@chakra-ui/react';
import { Box, Flex, Text } from '@chakra-ui/react';
import { route } from 'nextjs-routes';
import React from 'react';
import useApiQuery from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile';
import useNewTxsSocket from 'lib/hooks/useNewTxsSocket';
import { TX } from 'stubs/tx';
import LinkInternal from 'ui/shared/LinkInternal';
import SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice';
import LatestTxsItem from './LatestTxsItem';
import LatestTxsItemSkeleton from './LatestTxsItemSkeleton';
const LatestTransactions = () => {
const isMobile = useIsMobile();
const txsCount = isMobile ? 2 : 6;
const { data, isLoading, isError } = useApiQuery('homepage_txs');
const { data, isPlaceholderData, isError } = useApiQuery('homepage_txs', {
queryOptions: {
placeholderData: Array(txsCount).fill(TX),
},
});
const { num, socketAlert } = useNewTxsSocket();
if (isLoading) {
return (
<>
<Skeleton h="32px" w="100%" borderBottomLeftRadius={ 0 } borderBottomRightRadius={ 0 }/>
{ Array.from(Array(txsCount)).map((item, index) => <LatestTxsItemSkeleton key={ index }/>) }
</>
);
}
if (isError) {
return <Text mt={ 4 }>No data. Please reload page.</Text>;
}
......@@ -35,9 +30,15 @@ const LatestTransactions = () => {
const txsUrl = route({ pathname: '/txs' });
return (
<>
<SocketNewItemsNotice borderBottomRadius={ 0 } url={ txsUrl } num={ num } alert={ socketAlert }/>
<SocketNewItemsNotice borderBottomRadius={ 0 } url={ txsUrl } num={ num } alert={ socketAlert } isLoading={ isPlaceholderData }/>
<Box mb={{ base: 3, lg: 4 }}>
{ data.slice(0, txsCount).map((tx => <LatestTxsItem key={ tx.hash } tx={ tx }/>)) }
{ data.slice(0, txsCount).map(((tx, index) => (
<LatestTxsItem
key={ tx.hash + (isPlaceholderData ? index : '') }
tx={ tx }
isLoading={ isPlaceholderData }
/>
))) }
</Box>
<Flex justifyContent="center">
<LinkInternal fontSize="sm" href={ txsUrl }>View all transactions</LinkInternal>
......
......@@ -2,9 +2,9 @@ import {
Box,
Flex,
HStack,
Icon,
Text,
Grid,
Skeleton,
} from '@chakra-ui/react';
import React from 'react';
......@@ -19,15 +19,17 @@ import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import Icon from 'ui/shared/chakra/Icon';
import TxStatus from 'ui/shared/TxStatus';
import TxAdditionalInfo from 'ui/txs/TxAdditionalInfo';
import TxType from 'ui/txs/TxType';
type Props = {
tx: Transaction;
isLoading?: boolean;
}
const LatestTxsItem = ({ tx }: Props) => {
const LatestTxsItem = ({ tx, isLoading }: Props) => {
const dataTo = tx.to ? tx.to : tx.created_contract;
const timeAgo = useTimeAgoIncrement(tx.timestamp || '0', true);
......@@ -44,10 +46,10 @@ const LatestTxsItem = ({ tx }: Props) => {
>
<Flex justifyContent="space-between">
<HStack>
<TxType types={ tx.tx_types }/>
<TxStatus status={ tx.status } errorText={ tx.status === 'error' ? tx.result : undefined }/>
<TxType types={ tx.tx_types } isLoading={ isLoading }/>
<TxStatus status={ tx.status } errorText={ tx.status === 'error' ? tx.result : undefined } isLoading={ isLoading }/>
</HStack>
<TxAdditionalInfo tx={ tx } isMobile/>
<TxAdditionalInfo tx={ tx } isMobile isLoading={ isLoading }/>
</Flex>
<Flex
mt={ 2 }
......@@ -62,6 +64,7 @@ const LatestTxsItem = ({ tx }: Props) => {
boxSize="30px"
mr={ 2 }
color="link"
isLoading={ isLoading }
/>
<Address width="100%">
<AddressLink
......@@ -69,14 +72,19 @@ const LatestTxsItem = ({ tx }: Props) => {
type="transaction"
fontWeight="700"
truncation="constant"
isLoading={ isLoading }
/>
</Address>
</Flex>
{ tx.timestamp && <Text variant="secondary" fontWeight="400" fontSize="sm">{ timeAgo }</Text> }
{ tx.timestamp && (
<Skeleton isLoaded={ !isLoading } color="text_secondary" fontWeight="400" fontSize="sm">
<span>{ timeAgo }</span>
</Skeleton>
) }
</Flex>
<Flex alignItems="center" mb={ 3 }>
<Address>
<AddressIcon address={ tx.from }/>
<AddressIcon address={ tx.from } isLoading={ isLoading }/>
<AddressLink
type="address"
hash={ tx.from.hash }
......@@ -85,6 +93,7 @@ const LatestTxsItem = ({ tx }: Props) => {
ml={ 2 }
truncation="constant"
fontSize="sm"
isLoading={ isLoading }
/>
</Address>
<Icon
......@@ -92,10 +101,11 @@ const LatestTxsItem = ({ tx }: Props) => {
boxSize={ 6 }
mx={ 2 }
color="gray.500"
isLoading={ isLoading }
/>
{ dataTo && (
<Address>
<AddressIcon address={ dataTo }/>
<AddressIcon address={ dataTo } isLoading={ isLoading }/>
<AddressLink
type="address"
hash={ dataTo.hash }
......@@ -104,18 +114,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 +142,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 +157,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 +182,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 +194,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 +207,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);
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