Commit 70d95e92 authored by tom's avatar tom

skeletons for token instance

parent dd1daf82
......@@ -7,54 +7,63 @@ export const erc20a: AddressTokenBalance = {
token: tokens.tokenInfoERC20a,
token_id: null,
value: '1169320000000000000000000',
token_instance: null,
};
export const erc20b: AddressTokenBalance = {
token: tokens.tokenInfoERC20b,
token_id: null,
value: '872500000000',
token_instance: null,
};
export const erc20c: AddressTokenBalance = {
token: tokens.tokenInfoERC20c,
token_id: null,
value: '9852000000000000000000',
token_instance: null,
};
export const erc20d: AddressTokenBalance = {
token: tokens.tokenInfoERC20d,
token_id: null,
value: '39000000000000000000',
token_instance: null,
};
export const erc20LongSymbol: AddressTokenBalance = {
token: tokens.tokenInfoERC20LongSymbol,
token_id: null,
value: '39000000000000000000',
token_instance: null,
};
export const erc721a: AddressTokenBalance = {
token: tokens.tokenInfoERC721a,
token_id: null,
value: '51',
token_instance: null,
};
export const erc721b: AddressTokenBalance = {
token: tokens.tokenInfoERC721b,
token_id: null,
value: '1',
token_instance: null,
};
export const erc721c: AddressTokenBalance = {
token: tokens.tokenInfoERC721c,
token_id: null,
value: '5',
token_instance: null,
};
export const erc721LongSymbol: AddressTokenBalance = {
token: tokens.tokenInfoERC721LongSymbol,
token_id: null,
value: '5',
token_instance: null,
};
export const erc1155a: AddressTokenBalance = {
......
import type { NextPage } from 'next';
import dynamic from 'next/dynamic';
import Head from 'next/head';
import React from 'react';
import type { PageParams } from 'lib/next/token/types';
import getNetworkTitle from 'lib/networks/getNetworkTitle';
import TokenInstance from 'ui/pages/TokenInstance';
import Page from 'ui/shared/Page/Page';
const TokenInstance = dynamic(() => import('ui/pages/TokenInstance'), { ssr: false });
const TokenInstancePage: NextPage<PageParams> = () => {
const title = getNetworkTitle();
......@@ -15,7 +17,9 @@ const TokenInstancePage: NextPage<PageParams> = () => {
<Head>
<title>{ title }</title>
</Head>
<Page>
<TokenInstance/>
</Page>
</>
);
};
......
......@@ -9,7 +9,7 @@ export const ADDRESS_INFO: Address = {
block_number_balance_updated_at: 8774377,
coin_balance: '810941268802273085757',
creation_tx_hash: null,
creator_address_hash: null,
creator_address_hash: ADDRESS_HASH,
exchange_rate: null,
has_custom_methods_read: false,
has_custom_methods_write: false,
......
import type { TokenCounters, TokenHolder, TokenHolders, TokenInfo, TokenInstance, TokenType } from 'types/api/token';
import type { TokenCounters, TokenHolder, TokenInfo, TokenInstance, TokenType } from 'types/api/token';
import type { TokenTransfer, TokenTransferPagination, TokenTransferResponse } from 'types/api/tokenTransfer';
import { ADDRESS_PARAMS, ADDRESS_HASH } from './addressParams';
......@@ -38,8 +38,6 @@ export const TOKEN_HOLDER: TokenHolder = {
value: '1021378038331138520',
};
export const TOKEN_HOLDERS: TokenHolders = { items: Array(50).fill(TOKEN_HOLDER), next_page_params: null };
export const TOKEN_TRANSFER_ERC_20: TokenTransfer = {
block_hash: BLOCK_HASH,
from: ADDRESS_PARAMS,
......@@ -92,7 +90,7 @@ export const TOKEN_INSTANCE: TokenInstance = {
image_url: 'https://ipfs.vipsland.com/nft/collections/genesis/188882.gif',
is_unique: true,
metadata: {
attributes: Array(3).fill({ trait_type: 'skin', value: '6' }),
attributes: Array(3).fill({ trait_type: 'skin tone', value: 'very light skin tone' }),
description: '**GENESIS #188882**, **8a77ca1bcaa4036f** :: *844th* generation of *#57806 and #57809* :: **eGenetic Hash Code (eDNA)** = *2822355e953a462d*',
external_url: 'https://vipsland.com/nft/collections/genesis/188882',
image: 'https://ipfs.vipsland.com/nft/collections/genesis/188882.gif',
......
......@@ -136,7 +136,7 @@ const TokenPageContent = () => {
scrollRef,
options: {
enabled: Boolean(router.query.hash && router.query.tab === 'holders' && hasData),
placeholderData: tokenStubs.TOKEN_HOLDERS,
placeholderData: generateListStub<'token_holders'>(tokenStubs.TOKEN_HOLDER, 50, { next_page_params: null }),
},
});
......
import { Box, Icon, Skeleton } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';
import Page from 'ui/shared/Page/Page';
import TokenInstanceContent from 'ui/tokenInstance/TokenInstanceContent';
import type { RoutedTab } from 'ui/shared/Tabs/types';
import nftIcon from 'icons/nft_shield.svg';
import useApiQuery from 'lib/api/useApiQuery';
import { useAppContext } from 'lib/appContext';
import useIsMobile from 'lib/hooks/useIsMobile';
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import { TOKEN_INSTANCE } from 'stubs/token';
import * as tokenStubs from 'stubs/token';
import { generateListStub } from 'stubs/utils';
import TextAd from 'ui/shared/ad/TextAd';
import AddressHeadingInfo from 'ui/shared/AddressHeadingInfo';
import Tag from 'ui/shared/chakra/Tag';
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 TokenHolders from 'ui/token/TokenHolders/TokenHolders';
import TokenTransfer from 'ui/token/TokenTransfer/TokenTransfer';
import TokenInstanceDetails from 'ui/tokenInstance/TokenInstanceDetails';
import TokenInstanceMetadata from 'ui/tokenInstance/TokenInstanceMetadata';
export type TokenTabs = 'token_transfers' | 'holders'
const TokenInstance = () => {
const TokenInstanceContent = () => {
const router = useRouter();
const isMobile = useIsMobile();
const appProps = useAppContext();
const hash = router.query.hash?.toString();
const id = router.query.id?.toString();
const tab = router.query.tab?.toString();
const scrollRef = React.useRef<HTMLDivElement>(null);
const tokenInstanceQuery = useApiQuery('token_instance', {
pathParams: { hash, id },
queryOptions: {
enabled: Boolean(hash && id),
placeholderData: TOKEN_INSTANCE,
},
});
const transfersQuery = useQueryWithPages({
resourceName: 'token_instance_transfers',
pathParams: { hash, id },
scrollRef,
options: {
enabled: Boolean(hash && id && (!tab || tab === 'token_transfers') && !tokenInstanceQuery.isPlaceholderData && tokenInstanceQuery.data),
placeholderData: generateListStub<'token_instance_transfers'>(
tokenInstanceQuery.data?.token.type === 'ERC-1155' ? tokenStubs.TOKEN_TRANSFER_ERC_1155 : tokenStubs.TOKEN_TRANSFER_ERC_721,
10,
{ next_page_params: null },
),
},
});
const shouldFetchHolders = !tokenInstanceQuery.isPlaceholderData && tokenInstanceQuery.data && !tokenInstanceQuery.data.is_unique;
const holdersQuery = useQueryWithPages({
resourceName: 'token_instance_holders',
pathParams: { hash, id },
scrollRef,
options: {
enabled: Boolean(hash && tab === 'holders' && shouldFetchHolders),
placeholderData: generateListStub<'token_instance_holders'>(tokenStubs.TOKEN_HOLDER, 10, { next_page_params: null }),
},
});
const backLink = React.useMemo(() => {
const hasGoBackLink = appProps.referrer && appProps.referrer.includes(`/token/${ hash }`) && !appProps.referrer.includes('instance');
if (!hasGoBackLink) {
return;
}
return {
label: 'Back to token page',
url: appProps.referrer,
};
}, [ appProps.referrer, hash ]);
const tabs: Array<RoutedTab> = [
{
id: 'token_transfers',
title: 'Token transfers',
component: <TokenTransfer transfersQuery={ transfersQuery } tokenId={ id } token={ tokenInstanceQuery.data?.token }/>,
},
shouldFetchHolders ?
{ id: 'holders', title: 'Holders', component: <TokenHolders holdersQuery={ holdersQuery } token={ tokenInstanceQuery.data?.token }/> } :
undefined,
{ id: 'metadata', title: 'Metadata', component: (
<TokenInstanceMetadata
data={ tokenInstanceQuery.data?.metadata }
isPlaceholderData={ tokenInstanceQuery.isPlaceholderData }
/>
) },
].filter(Boolean);
if (tokenInstanceQuery.isError) {
throw Error('Token instance fetch failed', { cause: tokenInstanceQuery.error });
}
const nftShieldIcon = tokenInstanceQuery.isPlaceholderData ?
<Skeleton boxSize={ 6 } display="inline-block" borderRadius="base" mr={ 2 } my={ 2 } verticalAlign="text-bottom"/> :
<Icon as={ nftIcon } boxSize={ 6 } mr={ 2 }/>;
const tokenTag = <Tag isLoading={ tokenInstanceQuery.isPlaceholderData }>{ tokenInstanceQuery.data?.token.type }</Tag>;
const address = {
hash: hash || '',
is_contract: true,
implementation_name: null,
watchlist_names: [],
watchlist_address_id: null,
};
const appLink = (() => {
if (!tokenInstanceQuery.data?.external_app_url) {
return null;
}
try {
const url = new URL(tokenInstanceQuery.data.external_app_url);
return (
<Page>
<TokenInstanceContent/>
</Page>
<Skeleton isLoaded={ !tokenInstanceQuery.isPlaceholderData } display="inline-block" fontSize="sm" mt={ 6 }>
<span>View in app </span>
<LinkExternal href={ tokenInstanceQuery.data.external_app_url }>
{ url.hostname }
</LinkExternal>
</Skeleton>
);
} catch (error) {
return (
<Box fontSize="sm" mt={ 6 }>
<LinkExternal href={ tokenInstanceQuery.data.external_app_url }>
View in app
</LinkExternal>
</Box>
);
}
})();
let pagination: PaginationProps | undefined;
let isPaginationVisible;
if (tab === 'token_transfers') {
pagination = transfersQuery.pagination;
isPaginationVisible = transfersQuery.isPaginationVisible;
} else if (tab === 'holders') {
pagination = holdersQuery.pagination;
isPaginationVisible = holdersQuery.isPaginationVisible;
}
return (
<>
<TextAd mb={ 6 }/>
<PageTitle
title={ `${ tokenInstanceQuery.data?.token.name || 'Unnamed token' } #${ tokenInstanceQuery.data?.id }` }
backLink={ backLink }
beforeTitle={ nftShieldIcon }
contentAfter={ tokenTag }
isLoading={ tokenInstanceQuery.isPlaceholderData }
/>
<AddressHeadingInfo address={ address } token={ tokenInstanceQuery.data?.token } isLoading={ tokenInstanceQuery.isPlaceholderData }/>
{ appLink }
<TokenInstanceDetails data={ tokenInstanceQuery?.data } isLoading={ tokenInstanceQuery.isPlaceholderData } scrollRef={ scrollRef }/>
{ /* should stay before tabs to scroll up with pagination */ }
<Box ref={ scrollRef }></Box>
{ tokenInstanceQuery.isPlaceholderData ? <SkeletonTabs tabs={ tabs }/> : (
<RoutedTabs
tabs={ tabs }
tabListProps={ isMobile ? { mt: 8 } : { mt: 3, py: 5, marginBottom: 0 } }
rightSlot={ !isMobile && isPaginationVisible && pagination ? <Pagination { ...pagination }/> : null }
stickyEnabled={ !isMobile }
/>
) }
{ !tokenInstanceQuery.isLoading && !tokenInstanceQuery.isError && <Box h={{ base: 0, lg: '40vh' }}/> }
</>
);
};
export default TokenInstance;
export default React.memo(TokenInstanceContent);
......@@ -24,7 +24,7 @@ const BackLink = (props: BackLinkProp & { isLoading?: boolean }) => {
}
if (props.isLoading) {
return <Skeleton boxSize={ 6 } display="inline-block" borderRadius="base" mr={ 3 } isLoaded={ !props.isLoading }/>;
return <Skeleton boxSize={ 6 } display="inline-block" borderRadius="base" mr={ 3 } my={ 2 } verticalAlign="text-bottom" isLoaded={ !props.isLoading }/>;
}
const icon = <Icon as={ eastArrowIcon } boxSize={ 6 } transform="rotate(180deg)" margin="auto"/>;
......
import { Flex, Text, chakra } from '@chakra-ui/react';
import { Flex, chakra, Skeleton } from '@chakra-ui/react';
import React from 'react';
import type { TokenInfo } from 'types/api/token';
......@@ -21,7 +21,11 @@ const TokenSnippet = ({ data, className, logoSize = 6, isDisabled, hideSymbol, i
<Flex className={ className } alignItems="center" columnGap={ 2 } w="100%">
<TokenLogo boxSize={ logoSize } data={ data } isLoading={ isLoading }/>
<AddressLink hash={ data?.address || '' } alias={ data?.name || 'Unnamed token' } type="token" isDisabled={ isDisabled } isLoading={ isLoading }/>
{ data?.symbol && !hideSymbol && <Text variant="secondary">({ trimTokenSymbol(data.symbol) })</Text> }
{ data?.symbol && !hideSymbol && (
<Skeleton isLoaded={ !isLoading } color="text_secondary">
<span>({ trimTokenSymbol(data.symbol) })</span>
</Skeleton>
) }
</Flex>
);
};
......
......@@ -87,8 +87,11 @@ const TokenDetails = ({ tokenQuery }: Props) => {
title="Price"
hint="Price per token on the exchanges"
alignSelf="center"
isLoading={ tokenQuery.isPlaceholderData }
>
{ `$${ exchangeRate }` }
<Skeleton isLoaded={ !tokenQuery.isPlaceholderData } display="inline-block">
<span>{ `$${ exchangeRate }` }</span>
</Skeleton>
</DetailsInfoItem>
) }
{ marketcap && (
......@@ -96,8 +99,11 @@ const TokenDetails = ({ tokenQuery }: Props) => {
title="Fully diluted market cap"
hint="Total supply * Price"
alignSelf="center"
isLoading={ tokenQuery.isPlaceholderData }
>
{ `$${ marketcap }` }
<Skeleton isLoaded={ !tokenQuery.isPlaceholderData } display="inline-block">
<span>{ `$${ marketcap }` }</span>
</Skeleton>
</DetailsInfoItem>
) }
<DetailsInfoItem
......
......@@ -31,7 +31,7 @@ type Props = {
const TokenTransfer = ({ transfersQuery, tokenId, token }: Props) => {
const isMobile = useIsMobile();
const router = useRouter();
const { isError, isLoading, isPlaceholderData, data, pagination, isPaginationVisible } = transfersQuery;
const { isError, isPlaceholderData, data, pagination, isPaginationVisible } = transfersQuery;
const [ newItemsCount, setNewItemsCount ] = useGradualIncrement(0);
const [ socketAlert, setSocketAlert ] = React.useState('');
......@@ -52,7 +52,7 @@ const TokenTransfer = ({ transfersQuery, tokenId, token }: Props) => {
topic: `tokens:${ router.query.hash?.toString().toLowerCase() }`,
onSocketClose: handleSocketClose,
onSocketError: handleSocketError,
isDisabled: isLoading || isError || pagination.page !== 1,
isDisabled: isPlaceholderData || isError || pagination.page !== 1,
});
useSocketMessage({
channel,
......@@ -99,7 +99,7 @@ const TokenTransfer = ({ transfersQuery, tokenId, token }: Props) => {
return (
<DataListDisplay
isError={ isError }
isLoading={ !isPlaceholderData && isLoading }
isLoading={ false }
items={ data?.items }
skeletonProps={{
isLongSkeleton: true,
......
import { Box, Tag, Icon } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';
import type { RoutedTab } from 'ui/shared/Tabs/types';
import nftIcon from 'icons/nft_shield.svg';
import useApiQuery from 'lib/api/useApiQuery';
import { useAppContext } from 'lib/appContext';
import useIsMobile from 'lib/hooks/useIsMobile';
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import TextAd from 'ui/shared/ad/TextAd';
import AddressHeadingInfo from 'ui/shared/AddressHeadingInfo';
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 TokenHolders from 'ui/token/TokenHolders/TokenHolders';
import TokenTransfer from 'ui/token/TokenTransfer/TokenTransfer';
import TokenInstanceDetails from './TokenInstanceDetails';
import TokenInstanceMetadata from './TokenInstanceMetadata';
import TokenInstanceSkeleton from './TokenInstanceSkeleton';
export type TokenTabs = 'token_transfers' | 'holders'
const TokenInstanceContent = () => {
const router = useRouter();
const isMobile = useIsMobile();
const appProps = useAppContext();
const hash = router.query.hash?.toString();
const id = router.query.id?.toString();
const tab = router.query.tab?.toString();
const scrollRef = React.useRef<HTMLDivElement>(null);
const tokenInstanceQuery = useApiQuery('token_instance', {
pathParams: { hash, id },
queryOptions: { enabled: Boolean(hash && id) },
});
const transfersQuery = useQueryWithPages({
resourceName: 'token_instance_transfers',
pathParams: { hash, id },
scrollRef,
options: {
enabled: Boolean(hash && (!tab || tab === 'token_transfers') && tokenInstanceQuery.data),
},
});
const shouldFetchHolders = tokenInstanceQuery.data && !tokenInstanceQuery.data.is_unique;
const holdersQuery = useQueryWithPages({
resourceName: 'token_instance_holders',
pathParams: { hash, id },
scrollRef,
options: {
enabled: Boolean(hash && (!tab || tab === 'holders') && shouldFetchHolders),
},
});
const backLink = React.useMemo(() => {
const hasGoBackLink = appProps.referrer && appProps.referrer.includes(`/token/${ hash }`) && !appProps.referrer.includes('instance');
if (!hasGoBackLink) {
return;
}
return {
label: 'Back to token page',
url: appProps.referrer,
};
}, [ appProps.referrer, hash ]);
const tabs: Array<RoutedTab> = [
{
id: 'token_transfers',
title: 'Token transfers',
component: <TokenTransfer transfersQuery={ transfersQuery } tokenId={ id } token={ tokenInstanceQuery.data?.token }/>,
},
shouldFetchHolders ?
{ id: 'holders', title: 'Holders', component: <TokenHolders holdersQuery={ holdersQuery } token={ tokenInstanceQuery.data?.token }/> } :
undefined,
{ id: 'metadata', title: 'Metadata', component: <TokenInstanceMetadata data={ tokenInstanceQuery.data?.metadata }/> },
].filter(Boolean);
if (tokenInstanceQuery.isError) {
throw Error('Token instance fetch failed', { cause: tokenInstanceQuery.error });
}
if (tokenInstanceQuery.isLoading) {
return <TokenInstanceSkeleton/>;
}
const nftShieldIcon = <Icon as={ nftIcon } boxSize={ 6 } mr={ 2 }/>;
const tokenTag = <Tag>{ tokenInstanceQuery.data.token.type }</Tag>;
const address = {
hash: hash || '',
is_contract: true,
implementation_name: null,
watchlist_names: [],
watchlist_address_id: null,
};
const appLink = (() => {
if (!tokenInstanceQuery.data.external_app_url) {
return null;
}
try {
const url = new URL(tokenInstanceQuery.data.external_app_url);
return (
<Box fontSize="sm" mt={ 6 }>
<span>View in app </span>
<LinkExternal href={ tokenInstanceQuery.data.external_app_url }>
{ url.hostname }
</LinkExternal>
</Box>
);
} catch (error) {
return (
<Box fontSize="sm" mt={ 6 }>
<LinkExternal href={ tokenInstanceQuery.data.external_app_url }>
View in app
</LinkExternal>
</Box>
);
}
})();
let pagination: PaginationProps | undefined;
let isPaginationVisible;
if (tab === 'token_transfers') {
pagination = transfersQuery.pagination;
isPaginationVisible = transfersQuery.isPaginationVisible;
} else if (tab === 'holders') {
pagination = holdersQuery.pagination;
isPaginationVisible = holdersQuery.isPaginationVisible;
}
return (
<>
<TextAd mb={ 6 }/>
<PageTitle
title={ `${ tokenInstanceQuery.data.token.name || 'Unnamed token' } #${ tokenInstanceQuery.data.id }` }
backLink={ backLink }
beforeTitle={ nftShieldIcon }
contentAfter={ tokenTag }
/>
<AddressHeadingInfo address={ address } token={ tokenInstanceQuery.data.token }/>
{ appLink }
<TokenInstanceDetails data={ tokenInstanceQuery.data } scrollRef={ scrollRef }/>
{ /* should stay before tabs to scroll up with pagination */ }
<Box ref={ scrollRef }></Box>
{ tokenInstanceQuery.isLoading ? <SkeletonTabs/> : (
<RoutedTabs
tabs={ tabs }
tabListProps={ isMobile ? { mt: 8 } : { mt: 3, py: 5, marginBottom: 0 } }
rightSlot={ !isMobile && isPaginationVisible && pagination ? <Pagination { ...pagination }/> : null }
stickyEnabled={ !isMobile }
/>
) }
{ !tokenInstanceQuery.isLoading && !tokenInstanceQuery.isError && <Box h={{ base: 0, lg: '40vh' }}/> }
</>
);
};
export default React.memo(TokenInstanceContent);
import { Box, Flex, Grid, GridItem, useColorModeValue } from '@chakra-ui/react';
import { Flex, Grid, GridItem, Skeleton, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
import type { TokenInstance } from 'types/api/token';
......@@ -18,11 +18,12 @@ import TokenInstanceCreatorAddress from './details/TokenInstanceCreatorAddress';
import TokenInstanceTransfersCount from './details/TokenInstanceTransfersCount';
interface Props {
data: TokenInstance;
data?: TokenInstance;
isLoading?: boolean;
scrollRef?: React.RefObject<HTMLDivElement>;
}
const TokenInstanceDetails = ({ data, scrollRef }: Props) => {
const TokenInstanceDetails = ({ data, scrollRef, isLoading }: Props) => {
const handleCounterItemClick = React.useCallback(() => {
window.setTimeout(() => {
// cannot do scroll instantly, have to wait a little
......@@ -30,7 +31,7 @@ const TokenInstanceDetails = ({ data, scrollRef }: Props) => {
}, 500);
}, [ scrollRef ]);
const metadata = parseMetadata(data.metadata);
const metadata = parseMetadata(data?.metadata);
const hasMetadata = metadata && Boolean((metadata.name || metadata.description || metadata.attributes));
const attributeBgColor = useColorModeValue('blackAlpha.50', 'whiteAlpha.50');
......@@ -44,6 +45,10 @@ const TokenInstanceDetails = ({ data, scrollRef }: Props) => {
/>
);
if (!data) {
return null;
}
return (
<>
<Flex alignItems="flex-start" mt={ 8 } flexDir={{ base: 'column-reverse', lg: 'row' }} columnGap={ 6 } rowGap={ 6 }>
......@@ -57,34 +62,37 @@ const TokenInstanceDetails = ({ data, scrollRef }: Props) => {
<DetailsInfoItem
title="Token"
hint="Token name"
isLoading={ isLoading }
>
<TokenSnippet data={ data.token }/>
<TokenSnippet data={ data.token } isLoading={ isLoading }/>
</DetailsInfoItem>
{ data.is_unique && data.owner && (
<DetailsInfoItem
title="Owner"
hint="Current owner of this token instance"
isLoading={ isLoading }
>
<Address>
<AddressIcon address={ data.owner }/>
<AddressLink type="address" hash={ data.owner.hash } ml={ 2 }/>
<CopyToClipboard text={ data.owner.hash }/>
<AddressIcon address={ data.owner } isLoading={ isLoading }/>
<AddressLink type="address" hash={ data.owner.hash } ml={ 2 } isLoading={ isLoading }/>
<CopyToClipboard text={ data.owner.hash } isLoading={ isLoading }/>
</Address>
</DetailsInfoItem>
) }
<TokenInstanceCreatorAddress hash={ data.token.address }/>
<TokenInstanceCreatorAddress hash={ isLoading ? '' : data.token.address }/>
<DetailsInfoItem
title="Token ID"
hint="This token instance unique token ID"
isLoading={ isLoading }
>
<Flex alignItems="center" overflow="hidden">
<Box overflow="hidden" display="inline-block" w="100%">
<Skeleton isLoaded={ !isLoading } overflow="hidden" display="inline-block" w="100%">
<HashStringShortenDynamic hash={ data.id }/>
</Box>
<CopyToClipboard text={ data.id } ml={ 1 }/>
</Skeleton>
<CopyToClipboard text={ data.id } isLoading={ isLoading }/>
</Flex>
</DetailsInfoItem>
<TokenInstanceTransfersCount hash={ data.token.address } id={ data.id } onClick={ handleCounterItemClick }/>
<TokenInstanceTransfersCount hash={ isLoading ? '' : data.token.address } id={ isLoading ? '' : data.id } onClick={ handleCounterItemClick }/>
</Grid>
<NftMedia
imageUrl={ data.image_url }
......@@ -92,6 +100,7 @@ const TokenInstanceDetails = ({ data, scrollRef }: Props) => {
w="250px"
flexShrink={ 0 }
alignSelf={{ base: 'center', lg: 'flex-start' }}
isLoading={ isLoading }
/>
</Flex>
<Grid
......@@ -110,8 +119,11 @@ const TokenInstanceDetails = ({ data, scrollRef }: Props) => {
hint="NFT name"
whiteSpace="normal"
wordBreak="break-word"
isLoading={ isLoading }
>
<Skeleton isLoaded={ !isLoading }>
{ metadata.name }
</Skeleton>
</DetailsInfoItem>
) }
{ metadata?.description && (
......@@ -120,8 +132,11 @@ const TokenInstanceDetails = ({ data, scrollRef }: Props) => {
hint="NFT description"
whiteSpace="normal"
wordBreak="break-word"
isLoading={ isLoading }
>
<Skeleton isLoaded={ !isLoading }>
{ metadata.description }
</Skeleton>
</DetailsInfoItem>
) }
{ metadata?.attributes && (
......@@ -129,6 +144,7 @@ const TokenInstanceDetails = ({ data, scrollRef }: Props) => {
title="Attributes"
hint="NFT attributes"
whiteSpace="normal"
isLoading={ isLoading }
>
<Grid gap={ 2 } templateColumns="repeat(auto-fill,minmax(160px, 1fr))" w="100%">
{ metadata.attributes.map((attribute, index) => (
......@@ -138,9 +154,16 @@ const TokenInstanceDetails = ({ data, scrollRef }: Props) => {
borderRadius="md"
px={ 4 }
py={ 2 }
display="flex"
flexDir="column"
alignItems="flex-start"
>
<Box fontSize="xs" color="text_secondary" fontWeight={ 500 }>{ attribute.trait_type }</Box>
<Box fontSize="sm">{ attribute.value }</Box>
<Skeleton isLoaded={ !isLoading } fontSize="xs" lineHeight={ 4 } color="text_secondary" fontWeight={ 500 } mb={ 1 }>
<span>{ attribute.trait_type }</span>
</Skeleton>
<Skeleton isLoaded={ !isLoading } fontSize="sm">
<span>{ attribute.value }</span>
</Skeleton>
</GridItem>
)) }
</Grid>
......@@ -149,7 +172,7 @@ const TokenInstanceDetails = ({ data, scrollRef }: Props) => {
</>
) }
{ divider }
<DetailsSponsoredItem/>
<DetailsSponsoredItem isLoading={ isLoading }/>
</Grid>
</>
);
......
......@@ -3,6 +3,7 @@ import React from 'react';
import type { TokenInstance } from 'types/api/token';
import ContentLoader from 'ui/shared/ContentLoader';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import RawDataSnippet from 'ui/shared/RawDataSnippet';
......@@ -12,15 +13,20 @@ type Format = 'JSON' | 'Table'
interface Props {
data: TokenInstance['metadata'] | undefined;
isPlaceholderData?: boolean;
}
const TokenInstanceMetadata = ({ data }: Props) => {
const TokenInstanceMetadata = ({ data, isPlaceholderData }: Props) => {
const [ format, setFormat ] = React.useState<Format>('Table');
const handleSelectChange = React.useCallback((event: React.ChangeEvent<HTMLSelectElement>) => {
setFormat(event.target.value as Format);
}, []);
if (isPlaceholderData) {
return <ContentLoader/>;
}
if (!data) {
return <Box>There is no metadata for this NFT</Box>;
}
......
import { Box, Flex, Grid, Skeleton, SkeletonCircle } from '@chakra-ui/react';
import React from 'react';
import DetailsSkeletonRow from 'ui/shared/skeletons/DetailsSkeletonRow';
import SkeletonTabs from 'ui/shared/skeletons/SkeletonTabs';
const TokenInstanceSkeleton = () => {
return (
<Box>
<Skeleton h={{ base: 12, lg: 6 }} mb={ 6 } w="100%" maxW="680px"/>
<Skeleton h={ 10 } maxW="400px" w="100%" mb={ 6 }/>
<Flex align="center">
<SkeletonCircle boxSize={ 6 } flexShrink={ 0 }/>
<Skeleton h={ 6 } w={{ base: '100px', lg: '420px' }} ml={ 2 } borderRadius="full"/>
<Skeleton h={ 8 } w="36px" ml={ 3 } flexShrink={ 0 }/>
<Skeleton h={ 8 } w="36px" ml={ 3 } flexShrink={ 0 }/>
</Flex>
<Flex columnGap={ 6 } rowGap={ 6 } alignItems="flex-start" flexDir={{ base: 'column-reverse', lg: 'row' }} mt={ 8 }>
<Grid
columnGap={ 8 }
rowGap={{ base: 5, lg: 7 }}
templateColumns={{ base: '1fr', lg: '200px 1fr' }}
flexGrow={ 1 }
w="100%"
>
<DetailsSkeletonRow w="30%"/>
<DetailsSkeletonRow w="100%" maxW="450px"/>
<DetailsSkeletonRow w="100%" maxW="450px"/>
<DetailsSkeletonRow w="10%"/>
<DetailsSkeletonRow w="10%"/>
<DetailsSkeletonRow w="10%"/>
</Grid>
<Skeleton h="250px" w="250px" flexShrink={ 0 } alignSelf="center"/>
</Flex>
<SkeletonTabs/>
</Box>
);
};
export default TokenInstanceSkeleton;
import React from 'react';
import useApiQuery from 'lib/api/useApiQuery';
import { ADDRESS_INFO } from 'stubs/address';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import DetailsSkeletonRow from 'ui/shared/skeletons/DetailsSkeletonRow';
interface Props {
hash: string;
......@@ -15,17 +15,17 @@ interface Props {
const TokenInstanceCreatorAddress = ({ hash }: Props) => {
const addressQuery = useApiQuery('address', {
pathParams: { hash },
queryOptions: {
enabled: Boolean(hash),
placeholderData: ADDRESS_INFO,
},
});
if (addressQuery.isError) {
return null;
}
if (addressQuery.isLoading) {
return <DetailsSkeletonRow w="30%"/>;
}
if (!addressQuery.data.creator_address_hash) {
if (!addressQuery.data?.creator_address_hash) {
return null;
}
......@@ -39,11 +39,12 @@ const TokenInstanceCreatorAddress = ({ hash }: Props) => {
<DetailsInfoItem
title="Creator"
hint="Address that deployed this token contract"
isLoading={ addressQuery.isPlaceholderData }
>
<Address>
<AddressIcon address={ creatorAddress }/>
<AddressLink type="address" hash={ creatorAddress.hash } ml={ 2 }/>
<CopyToClipboard text={ creatorAddress.hash }/>
<AddressIcon address={ creatorAddress } isLoading={ addressQuery.isPlaceholderData }/>
<AddressLink type="address" hash={ creatorAddress.hash } ml={ 2 } isLoading={ addressQuery.isPlaceholderData }/>
<CopyToClipboard text={ creatorAddress.hash } isLoading={ addressQuery.isPlaceholderData }/>
</Address>
</DetailsInfoItem>
);
......
import { Skeleton } from '@chakra-ui/react';
import { route } from 'nextjs-routes';
import React from 'react';
import useApiQuery from 'lib/api/useApiQuery';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import LinkInternal from 'ui/shared/LinkInternal';
import DetailsSkeletonRow from 'ui/shared/skeletons/DetailsSkeletonRow';
interface Props {
hash: string;
......@@ -15,17 +15,19 @@ interface Props {
const TokenInstanceTransfersCount = ({ hash, id, onClick }: Props) => {
const transfersCountQuery = useApiQuery('token_instance_transfers_count', {
pathParams: { hash, id },
queryOptions: {
enabled: Boolean(hash && id),
placeholderData: {
transfers_count: 420,
},
},
});
if (transfersCountQuery.isError) {
return null;
}
if (transfersCountQuery.isLoading) {
return <DetailsSkeletonRow w="30%"/>;
}
if (!transfersCountQuery.data.transfers_count) {
if (!transfersCountQuery.data?.transfers_count) {
return null;
}
......@@ -37,13 +39,16 @@ const TokenInstanceTransfersCount = ({ hash, id, onClick }: Props) => {
<DetailsInfoItem
title="Transfers"
hint="Number of transfer for the token instance"
isLoading={ transfersCountQuery.isPlaceholderData }
>
<Skeleton isLoaded={ !transfersCountQuery.isPlaceholderData } display="inline-block">
<LinkInternal
href={ url }
onClick={ transfersCountQuery.data.transfers_count > 0 ? onClick : undefined }
>
{ transfersCountQuery.data.transfers_count.toLocaleString() }
</LinkInternal>
</Skeleton>
</DetailsInfoItem>
);
};
......
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