Commit 18845138 authored by tom's avatar tom

skeletons for homepage

parent 825aac59
...@@ -13,10 +13,11 @@ import useIsMobile from 'lib/hooks/useIsMobile'; ...@@ -13,10 +13,11 @@ import useIsMobile from 'lib/hooks/useIsMobile';
import { nbsp } from 'lib/html-entities'; import { nbsp } from 'lib/html-entities';
import useSocketChannel from 'lib/socket/useSocketChannel'; import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage'; import useSocketMessage from 'lib/socket/useSocketMessage';
import { BLOCK } from 'stubs/block';
import { HOMEPAGE_STATS } from 'stubs/stats';
import LinkInternal from 'ui/shared/LinkInternal'; import LinkInternal from 'ui/shared/LinkInternal';
import LatestBlocksItem from './LatestBlocksItem'; import LatestBlocksItem from './LatestBlocksItem';
import LatestBlocksItemSkeleton from './LatestBlocksItemSkeleton';
const BLOCK_HEIGHT_L1 = 166; const BLOCK_HEIGHT_L1 = 166;
const BLOCK_HEIGHT_L2 = 112; const BLOCK_HEIGHT_L2 = 112;
...@@ -32,10 +33,18 @@ const LatestBlocks = () => { ...@@ -32,10 +33,18 @@ const LatestBlocks = () => {
} else { } else {
blocksMaxCount = isMobile ? 2 : 3; blocksMaxCount = isMobile ? 2 : 3;
} }
const { data, isLoading, isError } = useApiQuery('homepage_blocks'); const { data, isPlaceholderData, isError } = useApiQuery('homepage_blocks', {
queryOptions: {
placeholderData: Array(4).fill(BLOCK),
},
});
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const statsQueryResult = useApiQuery('homepage_stats'); const statsQueryResult = useApiQuery('homepage_stats', {
queryOptions: {
placeholderData: HOMEPAGE_STATS,
},
});
const handleNewBlockMessage: SocketMessage.NewBlock['handler'] = React.useCallback((payload) => { const handleNewBlockMessage: SocketMessage.NewBlock['handler'] = React.useCallback((payload) => {
queryClient.setQueryData(getResourceKey('homepage_blocks'), (prevData: Array<Block> | undefined) => { queryClient.setQueryData(getResourceKey('homepage_blocks'), (prevData: Array<Block> | undefined) => {
...@@ -52,7 +61,7 @@ const LatestBlocks = () => { ...@@ -52,7 +61,7 @@ const LatestBlocks = () => {
const channel = useSocketChannel({ const channel = useSocketChannel({
topic: 'blocks:new_block', topic: 'blocks:new_block',
isDisabled: isLoading || isError, isDisabled: isPlaceholderData || isError,
}); });
useSocketMessage({ useSocketMessage({
channel, channel,
...@@ -62,22 +71,6 @@ const LatestBlocks = () => { ...@@ -62,22 +71,6 @@ const LatestBlocks = () => {
let content; let content;
if (isLoading) {
content = (
<>
<Skeleton w="100%" h={ 6 } mb={ 9 }/>
<VStack
spacing={ `${ BLOCK_MARGIN }px` }
mb={ 6 }
height={ `${ blockHeight * blocksMaxCount + BLOCK_MARGIN * (blocksMaxCount - 1) }px` }
overflow="hidden"
>
{ Array.from(Array(blocksMaxCount)).map((item, index) => <LatestBlocksItemSkeleton key={ index }/>) }
</VStack>
</>
);
}
if (isError) { if (isError) {
content = <Text>No data. Please reload page.</Text>; content = <Text>No data. Please reload page.</Text>;
} }
...@@ -88,22 +81,26 @@ const LatestBlocks = () => { ...@@ -88,22 +81,26 @@ const LatestBlocks = () => {
content = ( content = (
<> <>
{ statsQueryResult.isLoading && (
<Skeleton h="24px" w="170px" mb={{ base: 6, lg: 9 }}/>
) }
{ statsQueryResult.data?.network_utilization_percentage !== undefined && ( { statsQueryResult.data?.network_utilization_percentage !== undefined && (
<Box mb={{ base: 6, lg: 3 }}> <Skeleton isLoaded={ !statsQueryResult.isPlaceholderData } mb={{ base: 6, lg: 3 }} display="inline-block">
<Text as="span" fontSize="sm"> <Text as="span" fontSize="sm">
Network utilization:{ nbsp } Network utilization:{ nbsp }
</Text> </Text>
<Text as="span" fontSize="sm" color="blue.400" fontWeight={ 700 }> <Text as="span" fontSize="sm" color="blue.400" fontWeight={ 700 }>
{ statsQueryResult.data?.network_utilization_percentage.toFixed(2) }% { statsQueryResult.data?.network_utilization_percentage.toFixed(2) }%
</Text> </Text>
</Box> </Skeleton>
) } ) }
<VStack spacing={ `${ BLOCK_MARGIN }px` } mb={ 4 } height={ `${ blockHeight * blocksCount + BLOCK_MARGIN * (blocksCount - 1) }px` } overflow="hidden"> <VStack spacing={ `${ BLOCK_MARGIN }px` } mb={ 4 } height={ `${ blockHeight * blocksCount + BLOCK_MARGIN * (blocksCount - 1) }px` } overflow="hidden">
<AnimatePresence initial={ false } > <AnimatePresence initial={ false } >
{ dataToShow.map((block => <LatestBlocksItem key={ block.height } block={ block } h={ blockHeight }/>)) } { dataToShow.map(((block, index) => (
<LatestBlocksItem
key={ block.height + (isPlaceholderData ? String(index) : '') }
block={ block }
h={ blockHeight }
isLoading={ isPlaceholderData }
/>
))) }
</AnimatePresence> </AnimatePresence>
</VStack> </VStack>
<Flex justifyContent="center"> <Flex justifyContent="center">
......
...@@ -2,10 +2,8 @@ import { ...@@ -2,10 +2,8 @@ import {
Box, Box,
Flex, Flex,
Grid, Grid,
GridItem,
HStack, HStack,
Icon, Skeleton,
Text,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
...@@ -18,14 +16,16 @@ import blockIcon from 'icons/block.svg'; ...@@ -18,14 +16,16 @@ import blockIcon from 'icons/block.svg';
import getBlockTotalReward from 'lib/block/getBlockTotalReward'; import getBlockTotalReward from 'lib/block/getBlockTotalReward';
import BlockTimestamp from 'ui/blocks/BlockTimestamp'; import BlockTimestamp from 'ui/blocks/BlockTimestamp';
import AddressLink from 'ui/shared/address/AddressLink'; import AddressLink from 'ui/shared/address/AddressLink';
import Icon from 'ui/shared/chakra/Icon';
import LinkInternal from 'ui/shared/LinkInternal'; import LinkInternal from 'ui/shared/LinkInternal';
type Props = { type Props = {
block: Block; block: Block;
h: number; h: number;
isLoading?: boolean;
} }
const LatestBlocksItem = ({ block, h }: Props) => { const LatestBlocksItem = ({ block, h, isLoading }: Props) => {
const totalReward = getBlockTotalReward(block); const totalReward = getBlockTotalReward(block);
return ( return (
<Box <Box
...@@ -43,26 +43,29 @@ const LatestBlocksItem = ({ block, h }: Props) => { ...@@ -43,26 +43,29 @@ const LatestBlocksItem = ({ block, h }: Props) => {
> >
<Flex justifyContent="space-between" alignItems="center" mb={ 3 }> <Flex justifyContent="space-between" alignItems="center" mb={ 3 }>
<HStack spacing={ 2 }> <HStack spacing={ 2 }>
<Icon as={ blockIcon } boxSize="30px" color="link"/> <Icon as={ blockIcon } boxSize="30px" color="link" isLoading={ isLoading } borderRadius="base"/>
<LinkInternal <LinkInternal
href={ route({ pathname: '/block/[height_or_hash]', query: { height_or_hash: String(block.height) } }) } href={ route({ pathname: '/block/[height_or_hash]', query: { height_or_hash: String(block.height) } }) }
fontSize="xl" fontSize="xl"
fontWeight="500" fontWeight="500"
isLoading={ isLoading }
> >
<Skeleton isLoaded={ !isLoading }>
{ block.height } { block.height }
</Skeleton>
</LinkInternal> </LinkInternal>
</HStack> </HStack>
<BlockTimestamp ts={ block.timestamp } isEnabled fontSize="sm"/> <BlockTimestamp ts={ block.timestamp } isEnabled={ !isLoading } isLoading={ isLoading } fontSize="sm"/>
</Flex> </Flex>
<Grid gridGap={ 2 } templateColumns="auto minmax(0, 1fr)" fontSize="sm"> <Grid gridGap={ 2 } templateColumns="auto minmax(0, 1fr)" fontSize="sm">
<GridItem>Txn</GridItem> <Skeleton isLoaded={ !isLoading }>Txn</Skeleton>
<GridItem><Text variant="secondary">{ block.tx_count }</Text></GridItem> <Skeleton isLoaded={ !isLoading } color="text_secondary"><span>{ block.tx_count }</span></Skeleton>
{ !appConfig.L2.isL2Network && ( { !appConfig.L2.isL2Network && (
<> <>
<GridItem>Reward</GridItem> <Skeleton isLoaded={ !isLoading }>Reward</Skeleton>
<GridItem><Text variant="secondary">{ totalReward.toFixed() }</Text></GridItem> <Skeleton isLoaded={ !isLoading } color="text_secondary"><span>{ totalReward.toFixed() }</span></Skeleton>
<GridItem>Miner</GridItem> <Skeleton isLoaded={ !isLoading }>Miner</Skeleton>
<GridItem><AddressLink type="address" alias={ block.miner.name } hash={ block.miner.hash } truncation="constant" maxW="100%"/></GridItem> <AddressLink type="address" alias={ block.miner.name } hash={ block.miner.hash } truncation="constant" maxW="100%" isLoading={ isLoading }/>
</> </>
) } ) }
</Grid> </Grid>
......
import {
Box,
Flex,
Grid,
GridItem,
HStack,
Skeleton,
} from '@chakra-ui/react';
import React from 'react';
import appConfig from 'configs/app/config';
const LatestBlocksItemSkeleton = () => {
return (
<Box
minWidth={{ base: '100%', lg: '280px' }}
borderRadius="12px"
border="1px solid"
borderColor="divider"
p={ 6 }
>
<Flex justifyContent="space-between" alignItems="center" mb={ 3 }>
<HStack spacing={ 2 }>
<Skeleton w="30px" h="30px"/>
<Skeleton w="93px" h="15px"/>
</HStack>
<Skeleton w="44px" h="15px"/>
</Flex>
<Grid gridGap={ 2 } templateColumns="auto minmax(0, 1fr)" fontSize="sm">
<GridItem><Skeleton w="30px" h="15px"/></GridItem>
<GridItem><Skeleton w="93px" h="15px"/></GridItem>
{ !appConfig.L2.isL2Network && (
<>
<GridItem><Skeleton w="30px" h="15px"/></GridItem>
<GridItem><Skeleton w="93px" h="15px"/></GridItem>
<GridItem><Skeleton w="30px" h="15px"/></GridItem>
<GridItem><Skeleton w="93px" h="15px"/></GridItem>
</>
) }
</Grid>
</Box>
);
};
export default LatestBlocksItemSkeleton;
import { Box, Flex, Text, Skeleton } from '@chakra-ui/react'; import { Box, Flex, Text } from '@chakra-ui/react';
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
...@@ -9,16 +9,20 @@ import useGradualIncrement from 'lib/hooks/useGradualIncrement'; ...@@ -9,16 +9,20 @@ import useGradualIncrement from 'lib/hooks/useGradualIncrement';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import useSocketChannel from 'lib/socket/useSocketChannel'; import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage'; import useSocketMessage from 'lib/socket/useSocketMessage';
import { L2_DEPOSIT_ITEM } from 'stubs/L2';
import LinkInternal from 'ui/shared/LinkInternal'; import LinkInternal from 'ui/shared/LinkInternal';
import SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice'; import SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice';
import LatestDepositsItem from './LatestDepositsItem'; import LatestDepositsItem from './LatestDepositsItem';
import LatestDepositsItemSkeleton from './LatestDepositsItemSkeleton';
const LatestDeposits = () => { const LatestDeposits = () => {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const itemsCount = isMobile ? 2 : 6; const itemsCount = isMobile ? 2 : 6;
const { data, isLoading, isError } = useApiQuery('homepage_deposits'); const { data, isPlaceholderData, isError } = useApiQuery('homepage_deposits', {
queryOptions: {
placeholderData: Array(itemsCount).fill(L2_DEPOSIT_ITEM),
},
});
const [ num, setNum ] = useGradualIncrement(0); const [ num, setNum ] = useGradualIncrement(0);
const [ socketAlert, setSocketAlert ] = React.useState(''); const [ socketAlert, setSocketAlert ] = React.useState('');
...@@ -48,15 +52,6 @@ const LatestDeposits = () => { ...@@ -48,15 +52,6 @@ const LatestDeposits = () => {
handler: handleNewDepositMessage, handler: handleNewDepositMessage,
}); });
if (isLoading) {
return (
<>
<Skeleton h="32px" w="100%" borderBottomLeftRadius={ 0 } borderBottomRightRadius={ 0 }/>
{ Array.from(Array(itemsCount)).map((item, index) => <LatestDepositsItemSkeleton key={ index }/>) }
</>
);
}
if (isError) { if (isError) {
return <Text mt={ 4 }>No data. Please reload page.</Text>; return <Text mt={ 4 }>No data. Please reload page.</Text>;
} }
...@@ -65,9 +60,15 @@ const LatestDeposits = () => { ...@@ -65,9 +60,15 @@ const LatestDeposits = () => {
const depositsUrl = route({ pathname: '/l2-deposits' }); const depositsUrl = route({ pathname: '/l2-deposits' });
return ( return (
<> <>
<SocketNewItemsNotice borderBottomRadius={ 0 } url={ depositsUrl } num={ num } alert={ socketAlert } type="deposit"/> <SocketNewItemsNotice borderBottomRadius={ 0 } url={ depositsUrl } num={ num } alert={ socketAlert } type="deposit" isLoading={ isPlaceholderData }/>
<Box mb={{ base: 3, lg: 4 }}> <Box mb={{ base: 3, lg: 4 }}>
{ data.slice(0, itemsCount).map((item => <LatestDepositsItem key={ item.l2_tx_hash } item={ item }/>)) } { data.slice(0, itemsCount).map(((item, index) => (
<LatestDepositsItem
key={ item.l2_tx_hash + (isPlaceholderData ? index : '') }
item={ item }
isLoading={ isPlaceholderData }
/>
))) }
</Box> </Box>
<Flex justifyContent="center"> <Flex justifyContent="center">
<LinkInternal fontSize="sm" href={ depositsUrl }>View all deposits</LinkInternal> <LinkInternal fontSize="sm" href={ depositsUrl }>View all deposits</LinkInternal>
......
...@@ -2,8 +2,7 @@ import { ...@@ -2,8 +2,7 @@ import {
Box, Box,
Flex, Flex,
Grid, Grid,
Icon, Skeleton,
Text,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
...@@ -15,15 +14,17 @@ import blockIcon from 'icons/block.svg'; ...@@ -15,15 +14,17 @@ import blockIcon from 'icons/block.svg';
import txIcon from 'icons/transactions.svg'; import txIcon from 'icons/transactions.svg';
import dayjs from 'lib/date/dayjs'; import dayjs from 'lib/date/dayjs';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import Icon from 'ui/shared/chakra/Icon';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic'; import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import LinkExternal from 'ui/shared/LinkExternal'; import LinkExternal from 'ui/shared/LinkExternal';
import LinkInternal from 'ui/shared/LinkInternal'; import LinkInternal from 'ui/shared/LinkInternal';
type Props = { type Props = {
item: L2DepositsItem; item: L2DepositsItem;
isLoading?: boolean;
} }
const LatestTxsItem = ({ item }: Props) => { const LatestTxsItem = ({ item, isLoading }: Props) => {
const timeAgo = dayjs(item.l1_block_timestamp).fromNow(); const timeAgo = dayjs(item.l1_block_timestamp).fromNow();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
...@@ -35,9 +36,10 @@ const LatestTxsItem = ({ item }: Props) => { ...@@ -35,9 +36,10 @@ const LatestTxsItem = ({ item }: Props) => {
fontWeight={ 700 } fontWeight={ 700 }
display="inline-flex" display="inline-flex"
mr={ 2 } mr={ 2 }
isLoading={ isLoading }
> >
<Icon as={ blockIcon } boxSize="30px" mr={ 1 }/> <Icon as={ blockIcon } boxSize="30px" isLoading={ isLoading } borderRadius="base"/>
{ item.l1_block_number } <Skeleton isLoaded={ !isLoading } ml={ 1 }>{ item.l1_block_number }</Skeleton>
</LinkExternal> </LinkExternal>
); );
...@@ -48,9 +50,13 @@ const LatestTxsItem = ({ item }: Props) => { ...@@ -48,9 +50,13 @@ const LatestTxsItem = ({ item }: Props) => {
display="inline-flex" display="inline-flex"
alignItems="center" alignItems="center"
overflow="hidden" overflow="hidden"
isLoading={ isLoading }
my="3px"
> >
<Icon as={ txIcon } boxSize={ 6 } mr={ 1 }/> <Icon as={ txIcon } boxSize={ 6 } isLoading={ isLoading }/>
<Box overflow="hidden" whiteSpace="nowrap"><HashStringShortenDynamic hash={ item.l1_tx_hash }/></Box> <Skeleton isLoaded={ !isLoading } overflow="hidden" whiteSpace="nowrap" ml={ 1 }>
<HashStringShortenDynamic hash={ item.l1_tx_hash }/>
</Skeleton>
</LinkExternal> </LinkExternal>
); );
...@@ -61,9 +67,12 @@ const LatestTxsItem = ({ item }: Props) => { ...@@ -61,9 +67,12 @@ const LatestTxsItem = ({ item }: Props) => {
alignItems="center" alignItems="center"
overflow="hidden" overflow="hidden"
w="100%" w="100%"
isLoading={ isLoading }
> >
<Icon as={ txIcon } boxSize={ 6 } mr={ 1 }/> <Icon as={ txIcon } boxSize={ 6 } isLoading={ isLoading }/>
<Box w="calc(100% - 36px)" overflow="hidden" whiteSpace="nowrap"><HashStringShortenDynamic hash={ item.l2_tx_hash }/></Box> <Skeleton isLoaded={ !isLoading } w="calc(100% - 36px)" overflow="hidden" whiteSpace="nowrap" ml={ 1 }>
<HashStringShortenDynamic hash={ item.l2_tx_hash }/>
</Skeleton>
</LinkInternal> </LinkInternal>
); );
...@@ -73,28 +82,42 @@ const LatestTxsItem = ({ item }: Props) => { ...@@ -73,28 +82,42 @@ const LatestTxsItem = ({ item }: Props) => {
<> <>
<Flex justifyContent="space-between" alignItems="center" mb={ 1 }> <Flex justifyContent="space-between" alignItems="center" mb={ 1 }>
{ l1BlockLink } { l1BlockLink }
<Text variant="secondary">{ timeAgo }</Text> <Skeleton isLoaded={ !isLoading } color="text_secondary">
<span>{ timeAgo }</span>
</Skeleton>
</Flex> </Flex>
<Grid gridTemplateColumns="56px auto"> <Grid gridTemplateColumns="56px auto">
<Text lineHeight="30px">L1 txn</Text> <Skeleton isLoaded={ !isLoading } my="5px" w="fit-content">
L1 txn
</Skeleton>
{ l1TxLink } { l1TxLink }
<Text lineHeight="30px">L2 txn</Text> <Skeleton isLoaded={ !isLoading } my="3px" w="fit-content">
L2 txn
</Skeleton>
{ l2TxLink } { l2TxLink }
</Grid> </Grid>
</> </>
); );
} }
return ( return (
<Grid width="100%" columnGap={ 4 } rowGap={ 2 } templateColumns="max-content max-content auto" w="100%"> <Grid width="100%" columnGap={ 4 } rowGap={ 2 } templateColumns="max-content max-content auto" w="100%">
{ l1BlockLink } { l1BlockLink }
<Text lineHeight="30px">L1 txn</Text> <Skeleton isLoaded={ !isLoading } w="fit-content" h="fit-content" my="5px">
L1 txn
</Skeleton>
{ l1TxLink } { l1TxLink }
<Text variant="secondary">{ timeAgo }</Text> <Skeleton isLoaded={ !isLoading } color="text_secondary" w="fit-content" h="fit-content" my="2px">
<Text lineHeight="30px">L2 txn</Text> <span>{ timeAgo }</span>
</Skeleton>
<Skeleton isLoaded={ !isLoading } w="fit-content" h="fit-content" my="2px">
L2 txn
</Skeleton>
{ l2TxLink } { l2TxLink }
</Grid> </Grid>
); );
})(); })();
return ( return (
<Box <Box
width="100%" width="100%"
...@@ -104,6 +127,7 @@ const LatestTxsItem = ({ item }: Props) => { ...@@ -104,6 +127,7 @@ const LatestTxsItem = ({ item }: Props) => {
px={{ base: 0, lg: 4 }} px={{ base: 0, lg: 4 }}
_last={{ borderBottom: '1px solid', borderColor: 'divider' }} _last={{ borderBottom: '1px solid', borderColor: 'divider' }}
fontSize="sm" fontSize="sm"
lineHeight={ 5 }
> >
{ content } { content }
</Box> </Box>
......
import {
Box,
Flex,
Skeleton,
} from '@chakra-ui/react';
import React from 'react';
import useIsMobile from 'lib/hooks/useIsMobile';
const LatestTxsItemSkeleton = () => {
const isMobile = useIsMobile();
return (
<Box
width="100%"
borderTop="1px solid"
borderColor="divider"
py={ 4 }
px={{ base: 0, lg: 4 }}
_last={{ borderBottom: '1px solid', borderColor: 'divider' }}
>
{ isMobile && (
<>
<Flex justifyContent="space-between" alignItems="center" mt={ 1 } mb={ 4 }>
<Skeleton w="120px" h="20px"></Skeleton>
<Skeleton w="80px" h="20px"></Skeleton>
</Flex>
<Skeleton w="100%" h="20px" mb={ 2 }></Skeleton>
<Skeleton w="100%" h="20px" mb={ 2 }></Skeleton>
</>
) }
{ !isMobile && (
<>
<Flex w="100%" mb={ 2 } h="30px" alignItems="center" justifyContent="space-between">
<Skeleton w="120px" h="20px"></Skeleton>
<Skeleton w="calc(100% - 120px - 48px)" h="20px"></Skeleton>
</Flex><Flex w="100%" h="30px" alignItems="center" justifyContent="space-between">
<Skeleton w="120px" h="20px"></Skeleton>
<Skeleton w="calc(100% - 120px - 48px)" h="20px"></Skeleton>
</Flex>
</>
) }
</Box>
);
};
export default LatestTxsItemSkeleton;
import { Box, Flex, Text, Skeleton } from '@chakra-ui/react'; import { Box, Flex, Text } from '@chakra-ui/react';
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import useNewTxsSocket from 'lib/hooks/useNewTxsSocket'; import useNewTxsSocket from 'lib/hooks/useNewTxsSocket';
import { TX } from 'stubs/tx';
import LinkInternal from 'ui/shared/LinkInternal'; import LinkInternal from 'ui/shared/LinkInternal';
import SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice'; import SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice';
import LatestTxsItem from './LatestTxsItem'; import LatestTxsItem from './LatestTxsItem';
import LatestTxsItemSkeleton from './LatestTxsItemSkeleton';
const LatestTransactions = () => { const LatestTransactions = () => {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const txsCount = isMobile ? 2 : 6; const txsCount = isMobile ? 2 : 6;
const { data, isLoading, isError } = useApiQuery('homepage_txs'); const { data, isPlaceholderData, isError } = useApiQuery('homepage_txs', {
queryOptions: {
placeholderData: Array(txsCount).fill(TX),
},
});
const { num, socketAlert } = useNewTxsSocket(); const { num, socketAlert } = useNewTxsSocket();
if (isLoading) {
return (
<>
<Skeleton h="32px" w="100%" borderBottomLeftRadius={ 0 } borderBottomRightRadius={ 0 }/>
{ Array.from(Array(txsCount)).map((item, index) => <LatestTxsItemSkeleton key={ index }/>) }
</>
);
}
if (isError) { if (isError) {
return <Text mt={ 4 }>No data. Please reload page.</Text>; return <Text mt={ 4 }>No data. Please reload page.</Text>;
} }
...@@ -35,9 +30,15 @@ const LatestTransactions = () => { ...@@ -35,9 +30,15 @@ const LatestTransactions = () => {
const txsUrl = route({ pathname: '/txs' }); const txsUrl = route({ pathname: '/txs' });
return ( return (
<> <>
<SocketNewItemsNotice borderBottomRadius={ 0 } url={ txsUrl } num={ num } alert={ socketAlert }/> <SocketNewItemsNotice borderBottomRadius={ 0 } url={ txsUrl } num={ num } alert={ socketAlert } isLoading={ isPlaceholderData }/>
<Box mb={{ base: 3, lg: 4 }}> <Box mb={{ base: 3, lg: 4 }}>
{ data.slice(0, txsCount).map((tx => <LatestTxsItem key={ tx.hash } tx={ tx }/>)) } { data.slice(0, txsCount).map(((tx, index) => (
<LatestTxsItem
key={ tx.hash + (isPlaceholderData ? index : '') }
tx={ tx }
isLoading={ isPlaceholderData }
/>
))) }
</Box> </Box>
<Flex justifyContent="center"> <Flex justifyContent="center">
<LinkInternal fontSize="sm" href={ txsUrl }>View all transactions</LinkInternal> <LinkInternal fontSize="sm" href={ txsUrl }>View all transactions</LinkInternal>
......
...@@ -2,9 +2,9 @@ import { ...@@ -2,9 +2,9 @@ import {
Box, Box,
Flex, Flex,
HStack, HStack,
Icon,
Text, Text,
Grid, Grid,
Skeleton,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
...@@ -19,15 +19,17 @@ import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement'; ...@@ -19,15 +19,17 @@ import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
import Address from 'ui/shared/address/Address'; import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon'; import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink'; import AddressLink from 'ui/shared/address/AddressLink';
import Icon from 'ui/shared/chakra/Icon';
import TxStatus from 'ui/shared/TxStatus'; import TxStatus from 'ui/shared/TxStatus';
import TxAdditionalInfo from 'ui/txs/TxAdditionalInfo'; import TxAdditionalInfo from 'ui/txs/TxAdditionalInfo';
import TxType from 'ui/txs/TxType'; import TxType from 'ui/txs/TxType';
type Props = { type Props = {
tx: Transaction; tx: Transaction;
isLoading?: boolean;
} }
const LatestTxsItem = ({ tx }: Props) => { const LatestTxsItem = ({ tx, isLoading }: Props) => {
const dataTo = tx.to ? tx.to : tx.created_contract; const dataTo = tx.to ? tx.to : tx.created_contract;
const timeAgo = useTimeAgoIncrement(tx.timestamp || '0', true); const timeAgo = useTimeAgoIncrement(tx.timestamp || '0', true);
...@@ -44,10 +46,10 @@ const LatestTxsItem = ({ tx }: Props) => { ...@@ -44,10 +46,10 @@ const LatestTxsItem = ({ tx }: Props) => {
> >
<Flex justifyContent="space-between"> <Flex justifyContent="space-between">
<HStack> <HStack>
<TxType types={ tx.tx_types }/> <TxType types={ tx.tx_types } isLoading={ isLoading }/>
<TxStatus status={ tx.status } errorText={ tx.status === 'error' ? tx.result : undefined }/> <TxStatus status={ tx.status } errorText={ tx.status === 'error' ? tx.result : undefined } isLoading={ isLoading }/>
</HStack> </HStack>
<TxAdditionalInfo tx={ tx } isMobile/> <TxAdditionalInfo tx={ tx } isMobile isLoading={ isLoading }/>
</Flex> </Flex>
<Flex <Flex
mt={ 2 } mt={ 2 }
...@@ -62,6 +64,7 @@ const LatestTxsItem = ({ tx }: Props) => { ...@@ -62,6 +64,7 @@ const LatestTxsItem = ({ tx }: Props) => {
boxSize="30px" boxSize="30px"
mr={ 2 } mr={ 2 }
color="link" color="link"
isLoading={ isLoading }
/> />
<Address width="100%"> <Address width="100%">
<AddressLink <AddressLink
...@@ -69,14 +72,19 @@ const LatestTxsItem = ({ tx }: Props) => { ...@@ -69,14 +72,19 @@ const LatestTxsItem = ({ tx }: Props) => {
type="transaction" type="transaction"
fontWeight="700" fontWeight="700"
truncation="constant" truncation="constant"
isLoading={ isLoading }
/> />
</Address> </Address>
</Flex> </Flex>
{ tx.timestamp && <Text variant="secondary" fontWeight="400" fontSize="sm">{ timeAgo }</Text> } { tx.timestamp && (
<Skeleton isLoaded={ !isLoading } color="text_secondary" fontWeight="400" fontSize="sm">
<span>{ timeAgo }</span>
</Skeleton>
) }
</Flex> </Flex>
<Flex alignItems="center" mb={ 3 }> <Flex alignItems="center" mb={ 3 }>
<Address> <Address>
<AddressIcon address={ tx.from }/> <AddressIcon address={ tx.from } isLoading={ isLoading }/>
<AddressLink <AddressLink
type="address" type="address"
hash={ tx.from.hash } hash={ tx.from.hash }
...@@ -85,6 +93,7 @@ const LatestTxsItem = ({ tx }: Props) => { ...@@ -85,6 +93,7 @@ const LatestTxsItem = ({ tx }: Props) => {
ml={ 2 } ml={ 2 }
truncation="constant" truncation="constant"
fontSize="sm" fontSize="sm"
isLoading={ isLoading }
/> />
</Address> </Address>
<Icon <Icon
...@@ -92,10 +101,11 @@ const LatestTxsItem = ({ tx }: Props) => { ...@@ -92,10 +101,11 @@ const LatestTxsItem = ({ tx }: Props) => {
boxSize={ 6 } boxSize={ 6 }
mx={ 2 } mx={ 2 }
color="gray.500" color="gray.500"
isLoading={ isLoading }
/> />
{ dataTo && ( { dataTo && (
<Address> <Address>
<AddressIcon address={ dataTo }/> <AddressIcon address={ dataTo } isLoading={ isLoading }/>
<AddressLink <AddressLink
type="address" type="address"
hash={ dataTo.hash } hash={ dataTo.hash }
...@@ -104,18 +114,19 @@ const LatestTxsItem = ({ tx }: Props) => { ...@@ -104,18 +114,19 @@ const LatestTxsItem = ({ tx }: Props) => {
ml={ 2 } ml={ 2 }
truncation="constant" truncation="constant"
fontSize="sm" fontSize="sm"
isLoading={ isLoading }
/> />
</Address> </Address>
) } ) }
</Flex> </Flex>
<Box mb={ 2 } fontSize="sm"> <Skeleton isLoaded={ !isLoading } mb={ 2 } fontSize="sm" w="fit-content">
<Text as="span">Value { appConfig.network.currency.symbol } </Text> <Text as="span">Value { appConfig.network.currency.symbol } </Text>
<Text as="span" variant="secondary">{ getValueWithUnit(tx.value).dp(5).toFormat() }</Text> <Text as="span" variant="secondary">{ getValueWithUnit(tx.value).dp(5).toFormat() }</Text>
</Box> </Skeleton>
<Box fontSize="sm"> <Skeleton isLoaded={ !isLoading } fontSize="sm" w="fit-content">
<Text as="span">Fee { appConfig.network.currency.symbol } </Text> <Text as="span">Fee { appConfig.network.currency.symbol } </Text>
<Text as="span" variant="secondary">{ getValueWithUnit(tx.fee.value).dp(5).toFormat() }</Text> <Text as="span" variant="secondary">{ getValueWithUnit(tx.fee.value).dp(5).toFormat() }</Text>
</Box> </Skeleton>
</Box> </Box>
); );
} }
...@@ -131,11 +142,11 @@ const LatestTxsItem = ({ tx }: Props) => { ...@@ -131,11 +142,11 @@ const LatestTxsItem = ({ tx }: Props) => {
> >
<Grid width="100%" gridTemplateColumns="3fr 2fr 150px" gridGap={ 8 }> <Grid width="100%" gridTemplateColumns="3fr 2fr 150px" gridGap={ 8 }>
<Flex overflow="hidden" w="100%"> <Flex overflow="hidden" w="100%">
<TxAdditionalInfo tx={ tx }/> <TxAdditionalInfo tx={ tx } isLoading={ isLoading }/>
<Box ml={ 3 } w="calc(100% - 40px)"> <Box ml={ 3 } w="calc(100% - 40px)">
<HStack> <HStack>
<TxType types={ tx.tx_types }/> <TxType types={ tx.tx_types } isLoading={ isLoading }/>
<TxStatus status={ tx.status } errorText={ tx.status === 'error' ? tx.result : undefined }/> <TxStatus status={ tx.status } errorText={ tx.status === 'error' ? tx.result : undefined } isLoading={ isLoading }/>
</HStack> </HStack>
<Flex <Flex
mt={ 2 } mt={ 2 }
...@@ -146,16 +157,22 @@ const LatestTxsItem = ({ tx }: Props) => { ...@@ -146,16 +157,22 @@ const LatestTxsItem = ({ tx }: Props) => {
boxSize="30px" boxSize="30px"
color="link" color="link"
display="inline" display="inline"
mr={ 2 } isLoading={ isLoading }
borderRadius="base"
/> />
<Address overflow="hidden" w="calc(100% - 130px)" maxW="calc(100% - 130px)" mr={ 2 }> <Address overflow="hidden" w="calc(100% - 130px)" maxW="calc(100% - 130px)" ml={ 2 } mr={ 2 }>
<AddressLink <AddressLink
hash={ tx.hash } hash={ tx.hash }
type="transaction" type="transaction"
fontWeight="700" fontWeight="700"
isLoading={ isLoading }
/> />
</Address> </Address>
{ tx.timestamp && <Text variant="secondary" fontWeight="400" fontSize="sm">{ timeAgo }</Text> } { tx.timestamp && (
<Skeleton isLoaded={ !isLoading } color="text_secondary" fontWeight="400" fontSize="sm">
<span>{ timeAgo }</span>
</Skeleton>
) }
</Flex> </Flex>
</Box> </Box>
</Flex> </Flex>
...@@ -165,10 +182,11 @@ const LatestTxsItem = ({ tx }: Props) => { ...@@ -165,10 +182,11 @@ const LatestTxsItem = ({ tx }: Props) => {
boxSize={ 6 } boxSize={ 6 }
color="gray.500" color="gray.500"
transform="rotate(90deg)" transform="rotate(90deg)"
isLoading={ isLoading }
/> />
<Box overflow="hidden" ml={ 1 }> <Box overflow="hidden" ml={ 1 }>
<Address mb={ 2 }> <Address mb={ 2 }>
<AddressIcon address={ tx.from }/> <AddressIcon address={ tx.from } isLoading={ isLoading }/>
<AddressLink <AddressLink
type="address" type="address"
hash={ tx.from.hash } hash={ tx.from.hash }
...@@ -176,11 +194,12 @@ const LatestTxsItem = ({ tx }: Props) => { ...@@ -176,11 +194,12 @@ const LatestTxsItem = ({ tx }: Props) => {
fontWeight="500" fontWeight="500"
ml={ 2 } ml={ 2 }
fontSize="sm" fontSize="sm"
isLoading={ isLoading }
/> />
</Address> </Address>
{ dataTo && ( { dataTo && (
<Address> <Address>
<AddressIcon address={ dataTo }/> <AddressIcon address={ dataTo } isLoading={ isLoading }/>
<AddressLink <AddressLink
type="address" type="address"
hash={ dataTo.hash } hash={ dataTo.hash }
...@@ -188,20 +207,21 @@ const LatestTxsItem = ({ tx }: Props) => { ...@@ -188,20 +207,21 @@ const LatestTxsItem = ({ tx }: Props) => {
fontWeight="500" fontWeight="500"
ml={ 2 } ml={ 2 }
fontSize="sm" fontSize="sm"
isLoading={ isLoading }
/> />
</Address> </Address>
) } ) }
</Box> </Box>
</Grid> </Grid>
<Box> <Box>
<Box mb={ 2 }> <Skeleton isLoaded={ !isLoading } mb={ 2 }>
<Text as="span" whiteSpace="pre">{ appConfig.network.currency.symbol } </Text> <Text as="span" whiteSpace="pre">{ appConfig.network.currency.symbol } </Text>
<Text as="span" variant="secondary">{ getValueWithUnit(tx.value).dp(5).toFormat() }</Text> <Text as="span" variant="secondary">{ getValueWithUnit(tx.value).dp(5).toFormat() }</Text>
</Box> </Skeleton>
<Box> <Skeleton isLoaded={ !isLoading }>
<Text as="span">Fee </Text> <Text as="span">Fee </Text>
<Text as="span" variant="secondary">{ getValueWithUnit(tx.fee.value).dp(5).toFormat() }</Text> <Text as="span" variant="secondary">{ getValueWithUnit(tx.fee.value).dp(5).toFormat() }</Text>
</Box> </Skeleton>
</Box> </Box>
</Grid> </Grid>
</Box> </Box>
......
import {
Box,
Flex,
HStack,
Skeleton,
SkeletonCircle,
} from '@chakra-ui/react';
import React from 'react';
const LatestTxsItemSkeleton = () => {
return (
<Box
width="100%"
minW={{ base: 'unset', lg: '700px' }}
borderTop="1px solid"
borderColor="divider"
py={ 4 }
px={{ base: 0, lg: 4 }}
_last={{ borderBottom: '1px solid', borderColor: 'divider' }}
>
<Box width="100%" display={{ base: 'block', lg: 'none' }}>
<HStack spacing={ 2 }>
<Skeleton w="101px" h="24px"/>
<Skeleton w="101px" h="24px"/>
</HStack>
<Flex
mt={ 2 }
alignItems="center"
width="100%"
justifyContent="space-between"
mb={ 6 }
>
<Flex mr={ 3 } alignItems="center">
<Skeleton w="30px" h="30px" mr={ 2 }/>
<Skeleton w="101px" h="12px"/>
</Flex>
<Skeleton w="40px" h="12px"/>
</Flex>
<Flex alignItems="center" mb={ 3 }>
<SkeletonCircle w="30px" h="30px" mr={ 2 }/>
<Skeleton w="101px" h="12px" mr={ 5 }/>
<SkeletonCircle w="30px" h="30px" mr={ 2 }/>
<Skeleton w="101px" h="12px"/>
</Flex>
<Skeleton w="123px" h="12px" mb={ 2 } mt={ 3 }/>
<Skeleton w="123px" h="12px"/>
</Box>
<Box display={{ base: 'none', lg: 'grid' }} width="100%" gridTemplateColumns="3fr 2fr 150px" gridGap={ 8 }>
<Flex w="100%">
<Skeleton w={ 5 } h={ 5 } mr={ 3 }/>
<Box w="100%">
<HStack>
<Skeleton w="101px" h="24px"/>
<Skeleton w="101px" h="24px"/>
</HStack>
<Flex alignItems="center" mt={ 2 }>
<Skeleton w="30px" h="30px" mr={ 2 }/>
<Skeleton w="calc(100% - 100px)" h="20px" mr={ 5 }/>
<Skeleton w="40px" h="16px"/>
</Flex>
</Box>
</Flex>
<Box>
<Flex alignItems="center" mb={ 2 } mt={ 1 }>
<SkeletonCircle w="24px" h="24px" mr={ 2 }/>
<Skeleton w="100%" h="16px"/>
</Flex>
<Flex alignItems="center">
<SkeletonCircle w="24px" h="24px" mr={ 2 }/>
<Skeleton w="100%" h="16px"/>
</Flex>
</Box>
<Box>
<Skeleton w="123px" h="16px" mb={ 4 } mt={ 2 }/>
<Skeleton w="123px" h="16px"/>
</Box>
</Box>
</Box>
);
};
export default LatestTxsItemSkeleton;
...@@ -5,26 +5,26 @@ import React from 'react'; ...@@ -5,26 +5,26 @@ import React from 'react';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import useRedirectForInvalidAuthToken from 'lib/hooks/useRedirectForInvalidAuthToken'; import useRedirectForInvalidAuthToken from 'lib/hooks/useRedirectForInvalidAuthToken';
import { TX } from 'stubs/tx';
import LinkInternal from 'ui/shared/LinkInternal'; import LinkInternal from 'ui/shared/LinkInternal';
import LatestTxsItem from './LatestTxsItem'; import LatestTxsItem from './LatestTxsItem';
import LatestTxsItemSkeleton from './LatestTxsItemSkeleton';
const LatestWatchlistTxs = () => { const LatestWatchlistTxs = () => {
useRedirectForInvalidAuthToken(); useRedirectForInvalidAuthToken();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const txsCount = isMobile ? 2 : 6; const txsCount = isMobile ? 2 : 6;
const { data, isLoading, isError } = useApiQuery('homepage_txs_watchlist'); const { data, isPlaceholderData, isError } = useApiQuery('homepage_txs_watchlist', {
queryOptions: {
if (isLoading) { placeholderData: Array(txsCount).fill(TX),
return <>{ Array.from(Array(txsCount)).map((item, index) => <LatestTxsItemSkeleton key={ index }/>) }</>; },
} });
if (isError) { if (isError) {
return <Text mt={ 4 }>No data. Please reload page.</Text>; return <Text mt={ 4 }>No data. Please reload page.</Text>;
} }
if (data.length === 0) { if (!data?.length) {
return <Text mt={ 4 }>There are no transactions.</Text>; return <Text mt={ 4 }>There are no transactions.</Text>;
} }
...@@ -33,7 +33,13 @@ const LatestWatchlistTxs = () => { ...@@ -33,7 +33,13 @@ const LatestWatchlistTxs = () => {
return ( return (
<> <>
<Box mb={{ base: 3, lg: 4 }}> <Box mb={{ base: 3, lg: 4 }}>
{ data.slice(0, txsCount).map((tx => <LatestTxsItem key={ tx.hash } tx={ tx }/>)) } { data.slice(0, txsCount).map(((tx, index) => (
<LatestTxsItem
key={ tx.hash + (isPlaceholderData ? index : '') }
tx={ tx }
isLoading={ isPlaceholderData }
/>
))) }
</Box> </Box>
<Flex justifyContent="center"> <Flex justifyContent="center">
<LinkInternal fontSize="sm" href={ txsUrl }>View all watch list transactions</LinkInternal> <LinkInternal fontSize="sm" href={ txsUrl }>View all watch list transactions</LinkInternal>
......
...@@ -9,10 +9,10 @@ import gasIcon from 'icons/gas.svg'; ...@@ -9,10 +9,10 @@ import gasIcon from 'icons/gas.svg';
import txIcon from 'icons/transactions.svg'; import txIcon from 'icons/transactions.svg';
import walletIcon from 'icons/wallet.svg'; import walletIcon from 'icons/wallet.svg';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import { HOMEPAGE_STATS } from 'stubs/stats';
import StatsGasPrices from './StatsGasPrices'; import StatsGasPrices from './StatsGasPrices';
import StatsItem from './StatsItem'; import StatsItem from './StatsItem';
import StatsItemSkeleton from './StatsItemSkeleton';
const hasGasTracker = appConfig.homepage.showGasTracker; const hasGasTracker = appConfig.homepage.showGasTracker;
const hasAvgBlockTime = appConfig.homepage.showAvgBlockTime; const hasAvgBlockTime = appConfig.homepage.showAvgBlockTime;
...@@ -22,7 +22,11 @@ let itemsCount = 5; ...@@ -22,7 +22,11 @@ let itemsCount = 5;
!hasAvgBlockTime && itemsCount--; !hasAvgBlockTime && itemsCount--;
const Stats = () => { const Stats = () => {
const { data, isLoading, isError } = useApiQuery('homepage_stats'); const { data, isPlaceholderData, isError } = useApiQuery('homepage_stats', {
queryOptions: {
placeholderData: HOMEPAGE_STATS,
},
});
if (isError) { if (isError) {
return null; return null;
...@@ -32,13 +36,10 @@ const Stats = () => { ...@@ -32,13 +36,10 @@ const Stats = () => {
const lastItemTouchStyle = { gridColumn: { base: 'span 2', lg: 'unset' } }; const lastItemTouchStyle = { gridColumn: { base: 'span 2', lg: 'unset' } };
if (isLoading) {
content = Array.from(Array(itemsCount)).map((item, index) => <StatsItemSkeleton key={ index } _last={ itemsCount % 2 ? lastItemTouchStyle : undefined }/>);
}
if (data) { if (data) {
const isOdd = Boolean(hasGasTracker && !data.gas_prices ? (itemsCount - 1) % 2 : itemsCount % 2); const isOdd = Boolean(hasGasTracker && !data.gas_prices ? (itemsCount - 1) % 2 : itemsCount % 2);
const gasLabel = hasGasTracker && data.gas_prices ? <StatsGasPrices gasPrices={ data.gas_prices }/> : null; const gasLabel = hasGasTracker && data.gas_prices ? <StatsGasPrices gasPrices={ data.gas_prices }/> : null;
content = ( content = (
<> <>
<StatsItem <StatsItem
...@@ -46,12 +47,14 @@ const Stats = () => { ...@@ -46,12 +47,14 @@ const Stats = () => {
title="Total blocks" title="Total blocks"
value={ Number(data.total_blocks).toLocaleString() } value={ Number(data.total_blocks).toLocaleString() }
url={ route({ pathname: '/blocks' }) } url={ route({ pathname: '/blocks' }) }
isLoading={ isPlaceholderData }
/> />
{ hasAvgBlockTime && ( { hasAvgBlockTime && (
<StatsItem <StatsItem
icon={ clockIcon } icon={ clockIcon }
title="Average block time" title="Average block time"
value={ `${ (data.average_block_time / 1000).toFixed(1) } s` } value={ `${ (data.average_block_time / 1000).toFixed(1) } s` }
isLoading={ isPlaceholderData }
/> />
) } ) }
<StatsItem <StatsItem
...@@ -59,12 +62,14 @@ const Stats = () => { ...@@ -59,12 +62,14 @@ const Stats = () => {
title="Total transactions" title="Total transactions"
value={ Number(data.total_transactions).toLocaleString() } value={ Number(data.total_transactions).toLocaleString() }
url={ route({ pathname: '/txs' }) } url={ route({ pathname: '/txs' }) }
isLoading={ isPlaceholderData }
/> />
<StatsItem <StatsItem
icon={ walletIcon } icon={ walletIcon }
title="Wallet addresses" title="Wallet addresses"
value={ Number(data.total_addresses).toLocaleString() } value={ Number(data.total_addresses).toLocaleString() }
_last={ isOdd ? lastItemTouchStyle : undefined } _last={ isOdd ? lastItemTouchStyle : undefined }
isLoading={ isPlaceholderData }
/> />
{ hasGasTracker && data.gas_prices && ( { hasGasTracker && data.gas_prices && (
<StatsItem <StatsItem
...@@ -73,6 +78,7 @@ const Stats = () => { ...@@ -73,6 +78,7 @@ const Stats = () => {
value={ `${ Number(data.gas_prices.average).toLocaleString() } Gwei` } value={ `${ Number(data.gas_prices.average).toLocaleString() } Gwei` }
_last={ isOdd ? lastItemTouchStyle : undefined } _last={ isOdd ? lastItemTouchStyle : undefined }
tooltipLabel={ gasLabel } tooltipLabel={ gasLabel }
isLoading={ isPlaceholderData }
/> />
) } ) }
</> </>
......
import type { SystemStyleObject, TooltipProps } from '@chakra-ui/react'; import type { SystemStyleObject, TooltipProps } from '@chakra-ui/react';
import { Flex, Icon, Text, useColorModeValue, chakra, LightMode } from '@chakra-ui/react'; import { Skeleton, Flex, useColorModeValue, chakra, LightMode } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import breakpoints from 'theme/foundations/breakpoints'; import breakpoints from 'theme/foundations/breakpoints';
import Icon from 'ui/shared/chakra/Icon';
import Hint from 'ui/shared/Hint'; import Hint from 'ui/shared/Hint';
type Props = { type Props = {
...@@ -12,6 +13,7 @@ type Props = { ...@@ -12,6 +13,7 @@ type Props = {
className?: string; className?: string;
tooltipLabel?: React.ReactNode; tooltipLabel?: React.ReactNode;
url?: string; url?: string;
isLoading?: boolean;
} }
const LARGEST_BREAKPOINT = '1240px'; const LARGEST_BREAKPOINT = '1240px';
...@@ -24,7 +26,7 @@ const TOOLTIP_PROPS: Partial<TooltipProps> = { ...@@ -24,7 +26,7 @@ const TOOLTIP_PROPS: Partial<TooltipProps> = {
bgColor: 'blackAlpha.900', bgColor: 'blackAlpha.900',
}; };
const StatsItem = ({ icon, title, value, className, tooltipLabel, url }: Props) => { const StatsItem = ({ icon, title, value, className, tooltipLabel, url, isLoading }: Props) => {
const sxContainer: SystemStyleObject = { const sxContainer: SystemStyleObject = {
[`@media screen and (min-width: ${ breakpoints.lg }) and (max-width: ${ LARGEST_BREAKPOINT })`]: { flexDirection: 'column' }, [`@media screen and (min-width: ${ breakpoints.lg }) and (max-width: ${ LARGEST_BREAKPOINT })`]: { flexDirection: 'column' },
}; };
...@@ -33,11 +35,13 @@ const StatsItem = ({ icon, title, value, className, tooltipLabel, url }: Props) ...@@ -33,11 +35,13 @@ const StatsItem = ({ icon, title, value, className, tooltipLabel, url }: Props)
[`@media screen and (min-width: ${ breakpoints.lg }) and (max-width: ${ LARGEST_BREAKPOINT })`]: { alignItems: 'center' }, [`@media screen and (min-width: ${ breakpoints.lg }) and (max-width: ${ LARGEST_BREAKPOINT })`]: { alignItems: 'center' },
}; };
const bgColor = useColorModeValue('blue.50', 'blue.800');
const loadingBgColor = useColorModeValue('blackAlpha.50', 'whiteAlpha.50');
const infoColor = useColorModeValue('gray.600', 'gray.400'); const infoColor = useColorModeValue('gray.600', 'gray.400');
return ( return (
<Flex <Flex
backgroundColor={ useColorModeValue('blue.50', 'blue.800') } backgroundColor={ isLoading ? loadingBgColor : bgColor }
padding={ 3 } padding={ 3 }
borderRadius="md" borderRadius="md"
flexDirection="row" flexDirection="row"
...@@ -48,21 +52,25 @@ const StatsItem = ({ icon, title, value, className, tooltipLabel, url }: Props) ...@@ -48,21 +52,25 @@ const StatsItem = ({ icon, title, value, className, tooltipLabel, url }: Props)
className={ className } className={ className }
color={ useColorModeValue('black', 'white') } color={ useColorModeValue('black', 'white') }
position="relative" position="relative"
{ ...(url ? { { ...(url && !isLoading ? {
as: 'a', as: 'a',
href: url, href: url,
} : {}) } } : {}) }
> >
<Icon as={ icon } boxSize={ 7 }/> <Icon as={ icon } boxSize={ 7 } isLoading={ isLoading } borderRadius="base"/>
<Flex <Flex
flexDirection="column" flexDirection="column"
alignItems="start" alignItems="start"
sx={ sxText } sx={ sxText }
> >
<Text variant="secondary" fontSize="xs" lineHeight="16px">{ title }</Text> <Skeleton isLoaded={ !isLoading } color="text_secondary" fontSize="xs" lineHeight="16px" borderRadius="base">
<Text fontWeight={ 500 } fontSize="md" color={ useColorModeValue('black', 'white') }>{ value }</Text> <span>{ title }</span>
</Skeleton>
<Skeleton isLoaded={ !isLoading } fontWeight={ 500 } fontSize="md" color={ useColorModeValue('black', 'white') } borderRadius="base">
<span>{ value }</span>
</Skeleton>
</Flex> </Flex>
{ tooltipLabel && ( { tooltipLabel && !isLoading && (
<LightMode> <LightMode>
<Hint <Hint
label={ tooltipLabel } label={ tooltipLabel }
......
import { Flex, Skeleton, useColorModeValue, chakra } from '@chakra-ui/react';
import React from 'react';
const StatsItemSkeleton = ({ className }: {className?: string}) => {
const bgColor = useColorModeValue('blackAlpha.50', 'whiteAlpha.50');
return (
<Flex
backgroundColor={ bgColor }
padding={ 3 }
borderRadius="md"
flexDirection={{ base: 'row', lg: 'column', xl: 'row' }}
alignItems="center"
columnGap={ 3 }
rowGap={ 2 }
className={ className }
>
<Skeleton
w="40px"
h="40px"
/>
<Flex flexDirection="column" alignItems={{ base: 'start', lg: 'center', xl: 'start' }}>
<Skeleton w="69px" h="10px" mt="4px" mb="8px"/>
<Skeleton w="93px" h="14px" mb="4px"/>
</Flex>
</Flex>
);
};
export default chakra(StatsItemSkeleton);
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