Commit 3dc54294 authored by isstuev's avatar isstuev

add holders on token instance page

parent 4d40c441
...@@ -344,6 +344,12 @@ export const RESOURCES = { ...@@ -344,6 +344,12 @@ export const RESOURCES = {
paginationFields: [ 'block_number' as const, 'items_count' as const, 'index' as const, 'token_id' as const ], paginationFields: [ 'block_number' as const, 'items_count' as const, 'index' as const, 'token_id' as const ],
filterFields: [], filterFields: [],
}, },
token_instance_holders: {
path: '/api/v2/tokens/:hash/instances/:id/holders',
pathParams: [ 'hash' as const, 'id' as const ],
paginationFields: [ 'items_count' as const, 'token_id' as const, 'value' as const ],
filterFields: [],
},
// HOMEPAGE // HOMEPAGE
homepage_stats: { homepage_stats: {
...@@ -491,7 +497,7 @@ export type PaginatedResources = 'blocks' | 'block_txs' | ...@@ -491,7 +497,7 @@ export type PaginatedResources = 'blocks' | 'block_txs' |
'search' | 'search' |
'address_logs' | 'address_tokens' | 'address_logs' | 'address_tokens' |
'token_transfers' | 'token_holders' | 'token_inventory' | 'tokens' | 'token_transfers' | 'token_holders' | 'token_inventory' | 'tokens' |
'token_instance_transfers' | 'token_instance_transfers' | 'token_instance_holders' |
'verified_contracts' | 'verified_contracts' |
'l2_output_roots' | 'l2_withdrawals' | 'l2_txn_batches' | 'l2_deposits' | 'l2_output_roots' | 'l2_withdrawals' | 'l2_txn_batches' | 'l2_deposits' |
'withdrawals' | 'address_withdrawals' | 'block_withdrawals'; 'withdrawals' | 'address_withdrawals' | 'block_withdrawals';
...@@ -548,6 +554,7 @@ Q extends 'token_holders' ? TokenHolders : ...@@ -548,6 +554,7 @@ Q extends 'token_holders' ? TokenHolders :
Q extends 'token_instance' ? TokenInstance : Q extends 'token_instance' ? TokenInstance :
Q extends 'token_instance_transfers_count' ? TokenInstanceTransfersCount : Q extends 'token_instance_transfers_count' ? TokenInstanceTransfersCount :
Q extends 'token_instance_transfers' ? TokenInstanceTransferResponse : Q extends 'token_instance_transfers' ? TokenInstanceTransferResponse :
Q extends 'token_instance_holders' ? TokenHolders :
Q extends 'token_inventory' ? TokenInventoryResponse : Q extends 'token_inventory' ? TokenInventoryResponse :
Q extends 'tokens' ? TokensResponse : Q extends 'tokens' ? TokensResponse :
Q extends 'search' ? SearchResult : Q extends 'search' ? SearchResult :
......
...@@ -179,3 +179,8 @@ export const withRichMetadata: TokenInstance = { ...@@ -179,3 +179,8 @@ export const withRichMetadata: TokenInstance = {
status: null, status: null,
}, },
}; };
export const unique: TokenInstance = {
...base,
is_unique: true,
};
...@@ -138,7 +138,7 @@ const TokenPageContent = () => { ...@@ -138,7 +138,7 @@ const TokenPageContent = () => {
const tabs: Array<RoutedTab> = [ const tabs: Array<RoutedTab> = [
{ id: 'token_transfers', title: 'Token transfers', component: <TokenTransfer transfersQuery={ transfersQuery }/> }, { id: 'token_transfers', title: 'Token transfers', component: <TokenTransfer transfersQuery={ transfersQuery }/> },
{ id: 'holders', title: 'Holders', component: <TokenHolders tokenQuery={ tokenQuery } holdersQuery={ holdersQuery }/> }, { id: 'holders', title: 'Holders', component: <TokenHolders token={ tokenQuery.data } holdersQuery={ holdersQuery }/> },
(tokenQuery.data?.type === 'ERC-1155' || tokenQuery.data?.type === 'ERC-721') ? (tokenQuery.data?.type === 'ERC-1155' || tokenQuery.data?.type === 'ERC-721') ?
{ id: 'inventory', title: 'Inventory', component: <TokenInventory inventoryQuery={ inventoryQuery }/> } : { id: 'inventory', title: 'Inventory', component: <TokenInventory inventoryQuery={ inventoryQuery }/> } :
undefined, undefined,
......
...@@ -14,17 +14,17 @@ import TokenHoldersList from './TokenHoldersList'; ...@@ -14,17 +14,17 @@ import TokenHoldersList from './TokenHoldersList';
import TokenHoldersTable from './TokenHoldersTable'; import TokenHoldersTable from './TokenHoldersTable';
type Props = { type Props = {
tokenQuery: UseQueryResult<TokenInfo>; token?: TokenInfo;
holdersQuery: UseQueryResult<TokenHolders> & { holdersQuery: UseQueryResult<TokenHolders> & {
pagination: PaginationProps; pagination: PaginationProps;
isPaginationVisible: boolean; isPaginationVisible: boolean;
}; };
} }
const TokenHoldersContent = ({ holdersQuery, tokenQuery }: Props) => { const TokenHoldersContent = ({ holdersQuery, token }: Props) => {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
if (holdersQuery.isError || tokenQuery.isError) { if (holdersQuery.isError) {
return <DataFetchAlert/>; return <DataFetchAlert/>;
} }
...@@ -36,17 +36,17 @@ const TokenHoldersContent = ({ holdersQuery, tokenQuery }: Props) => { ...@@ -36,17 +36,17 @@ const TokenHoldersContent = ({ holdersQuery, tokenQuery }: Props) => {
const items = holdersQuery.data?.items; const items = holdersQuery.data?.items;
const content = items && tokenQuery.data ? ( const content = items && token ? (
<> <>
{ !isMobile && <TokenHoldersTable data={ items } token={ tokenQuery.data } top={ holdersQuery.isPaginationVisible ? 80 : 0 }/> } { !isMobile && <TokenHoldersTable data={ items } token={ token } top={ holdersQuery.isPaginationVisible ? 80 : 0 }/> }
{ isMobile && <TokenHoldersList data={ items } token={ tokenQuery.data }/> } { isMobile && <TokenHoldersList data={ items } token={ token }/> }
</> </>
) : null; ) : null;
return ( return (
<DataListDisplay <DataListDisplay
isError={ holdersQuery.isError || tokenQuery.isError } isError={ holdersQuery.isError }
isLoading={ holdersQuery.isLoading || tokenQuery.isLoading } isLoading={ holdersQuery.isLoading }
items={ holdersQuery.data?.items } items={ holdersQuery.data?.items }
skeletonProps={{ skeletonDesktopColumns: [ '100%', '300px', '175px' ] }} skeletonProps={{ skeletonDesktopColumns: [ '100%', '300px', '175px' ] }}
emptyText="There are no holders for this token." emptyText="There are no holders for this token."
......
...@@ -14,7 +14,10 @@ import AddressHeadingInfo from 'ui/shared/AddressHeadingInfo'; ...@@ -14,7 +14,10 @@ import AddressHeadingInfo from 'ui/shared/AddressHeadingInfo';
import LinkExternal from 'ui/shared/LinkExternal'; import LinkExternal from 'ui/shared/LinkExternal';
import PageTitle from 'ui/shared/Page/PageTitle'; import PageTitle from 'ui/shared/Page/PageTitle';
import Pagination from 'ui/shared/Pagination'; import Pagination from 'ui/shared/Pagination';
import type { Props as PaginationProps } from 'ui/shared/Pagination';
import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs'; import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs';
import SkeletonTabs from 'ui/shared/skeletons/SkeletonTabs';
import TokenHolders from 'ui/token/TokenHolders/TokenHolders';
import TokenTransfer from 'ui/token/TokenTransfer/TokenTransfer'; import TokenTransfer from 'ui/token/TokenTransfer/TokenTransfer';
import TokenInstanceDetails from './TokenInstanceDetails'; import TokenInstanceDetails from './TokenInstanceDetails';
...@@ -50,12 +53,24 @@ const TokenInstanceContent = () => { ...@@ -50,12 +53,24 @@ const TokenInstanceContent = () => {
}, },
}); });
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 tabs: Array<RoutedTab> = [ const tabs: Array<RoutedTab> = [
{ id: 'token_transfers', title: 'Token transfers', component: <TokenTransfer transfersQuery={ transfersQuery } tokenId={ id }/> }, { id: 'token_transfers', title: 'Token transfers', component: <TokenTransfer transfersQuery={ transfersQuery } tokenId={ id }/> },
// there is no api for this tab yet shouldFetchHolders ?
// { id: 'holders', title: 'Holders', component: <span>Holders</span> }, { id: 'holders', title: 'Holders', component: <TokenHolders holdersQuery={ holdersQuery } token={ tokenInstanceQuery.data?.token }/> } :
undefined,
{ id: 'metadata', title: 'Metadata', component: <TokenInstanceMetadata data={ tokenInstanceQuery.data?.metadata }/> }, { id: 'metadata', title: 'Metadata', component: <TokenInstanceMetadata data={ tokenInstanceQuery.data?.metadata }/> },
]; ].filter(Boolean);
if (tokenInstanceQuery.isError) { if (tokenInstanceQuery.isError) {
throw Error('Token instance fetch failed', { cause: tokenInstanceQuery.error }); throw Error('Token instance fetch failed', { cause: tokenInstanceQuery.error });
...@@ -100,6 +115,17 @@ const TokenInstanceContent = () => { ...@@ -100,6 +115,17 @@ const TokenInstanceContent = () => {
} }
})(); })();
let pagination;
let isPaginationVisible;
if (tab === 'token_transfers') {
pagination = transfersQuery.pagination;
isPaginationVisible = transfersQuery.isPaginationVisible;
} else if (tab === 'holders') {
pagination = holdersQuery.pagination;
isPaginationVisible = holdersQuery.isPaginationVisible;
}
return ( return (
<> <>
<TextAd mb={ 6 }/> <TextAd mb={ 6 }/>
...@@ -120,12 +146,14 @@ const TokenInstanceContent = () => { ...@@ -120,12 +146,14 @@ const TokenInstanceContent = () => {
{ /* should stay before tabs to scroll up with pagination */ } { /* should stay before tabs to scroll up with pagination */ }
<Box ref={ scrollRef }></Box> <Box ref={ scrollRef }></Box>
<RoutedTabs { tokenInstanceQuery.isLoading ? <SkeletonTabs/> : (
tabs={ tabs } <RoutedTabs
tabListProps={ isMobile ? { mt: 8 } : { mt: 3, py: 5, marginBottom: 0 } } tabs={ tabs }
rightSlot={ !isMobile && transfersQuery.isPaginationVisible && tab !== 'metadata' ? <Pagination { ...transfersQuery.pagination }/> : null } tabListProps={ isMobile ? { mt: 8 } : { mt: 3, py: 5, marginBottom: 0 } }
stickyEnabled={ !isMobile } rightSlot={ !isMobile && isPaginationVisible ? <Pagination { ...(pagination as PaginationProps) }/> : null }
/> stickyEnabled={ !isMobile }
/>
) }
{ !tokenInstanceQuery.isLoading && !tokenInstanceQuery.isError && <Box h={{ base: 0, lg: '40vh' }}/> } { !tokenInstanceQuery.isLoading && !tokenInstanceQuery.isError && <Box h={{ base: 0, lg: '40vh' }}/> }
</> </>
......
...@@ -11,8 +11,8 @@ import TokenInstanceDetails from './TokenInstanceDetails'; ...@@ -11,8 +11,8 @@ import TokenInstanceDetails from './TokenInstanceDetails';
const API_URL_ADDRESS = buildApiUrl('address', { hash: tokenInstanceMock.base.token.address }); const API_URL_ADDRESS = buildApiUrl('address', { hash: tokenInstanceMock.base.token.address });
const API_URL_TOKEN_TRANSFERS_COUNT = buildApiUrl('token_instance_transfers_count', { const API_URL_TOKEN_TRANSFERS_COUNT = buildApiUrl('token_instance_transfers_count', {
id: tokenInstanceMock.base.id, id: tokenInstanceMock.unique.id,
hash: tokenInstanceMock.base.token.address, hash: tokenInstanceMock.unique.token.address,
}); });
test('base view +@dark-mode +@mobile', async({ mount, page }) => { test('base view +@dark-mode +@mobile', async({ mount, page }) => {
...@@ -27,7 +27,7 @@ test('base view +@dark-mode +@mobile', async({ mount, page }) => { ...@@ -27,7 +27,7 @@ test('base view +@dark-mode +@mobile', async({ mount, page }) => {
const component = await mount( const component = await mount(
<TestApp> <TestApp>
<TokenInstanceDetails data={ tokenInstanceMock.base }/> <TokenInstanceDetails data={ tokenInstanceMock.unique }/>
</TestApp>, </TestApp>,
); );
......
...@@ -60,7 +60,7 @@ const TokenInstanceDetails = ({ data, scrollRef }: Props) => { ...@@ -60,7 +60,7 @@ const TokenInstanceDetails = ({ data, scrollRef }: Props) => {
> >
<TokenSnippet hash={ data.token.address } name={ data.token.name }/> <TokenSnippet hash={ data.token.address } name={ data.token.name }/>
</DetailsInfoItem> </DetailsInfoItem>
{ data.owner && ( { data.is_unique && data.owner && (
<DetailsInfoItem <DetailsInfoItem
title="Owner" title="Owner"
hint="Current owner of this token instance" hint="Current owner of this token instance"
......
...@@ -42,7 +42,7 @@ const TokenInstanceTransfersCount = ({ hash, id, onClick }: Props) => { ...@@ -42,7 +42,7 @@ const TokenInstanceTransfersCount = ({ hash, id, onClick }: Props) => {
href={ url } href={ url }
onClick={ transfersCountQuery.data.transfers_count > 0 ? onClick : undefined } onClick={ transfersCountQuery.data.transfers_count > 0 ? onClick : undefined }
> >
{ transfersCountQuery.data.transfers_count } { transfersCountQuery.data.transfers_count.toLocaleString() }
</LinkInternal> </LinkInternal>
</DetailsInfoItem> </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