mirror of
https://github.com/luxfi/explore.git
synced 2026-09-24 00:28:18 +00:00
Merge pull request #757 from blockscout/inctance-holders
add holders on token instance page
This commit is contained in:
@@ -344,6 +344,12 @@ export const RESOURCES = {
|
||||
paginationFields: [ 'block_number' as const, 'items_count' as const, 'index' as const, 'token_id' as const ],
|
||||
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_stats: {
|
||||
@@ -491,7 +497,7 @@ export type PaginatedResources = 'blocks' | 'block_txs' |
|
||||
'search' |
|
||||
'address_logs' | 'address_tokens' |
|
||||
'token_transfers' | 'token_holders' | 'token_inventory' | 'tokens' |
|
||||
'token_instance_transfers' |
|
||||
'token_instance_transfers' | 'token_instance_holders' |
|
||||
'verified_contracts' |
|
||||
'l2_output_roots' | 'l2_withdrawals' | 'l2_txn_batches' | 'l2_deposits' |
|
||||
'withdrawals' | 'address_withdrawals' | 'block_withdrawals';
|
||||
@@ -548,6 +554,7 @@ Q extends 'token_holders' ? TokenHolders :
|
||||
Q extends 'token_instance' ? TokenInstance :
|
||||
Q extends 'token_instance_transfers_count' ? TokenInstanceTransfersCount :
|
||||
Q extends 'token_instance_transfers' ? TokenInstanceTransferResponse :
|
||||
Q extends 'token_instance_holders' ? TokenHolders :
|
||||
Q extends 'token_inventory' ? TokenInventoryResponse :
|
||||
Q extends 'tokens' ? TokensResponse :
|
||||
Q extends 'search' ? SearchResult :
|
||||
|
||||
@@ -179,3 +179,8 @@ export const withRichMetadata: TokenInstance = {
|
||||
status: null,
|
||||
},
|
||||
};
|
||||
|
||||
export const unique: TokenInstance = {
|
||||
...base,
|
||||
is_unique: true,
|
||||
};
|
||||
|
||||
+3
-3
@@ -138,7 +138,7 @@ const TokenPageContent = () => {
|
||||
|
||||
const tabs: Array<RoutedTab> = [
|
||||
{ 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') ?
|
||||
{ id: 'inventory', title: 'Inventory', component: <TokenInventory inventoryQuery={ inventoryQuery }/> } :
|
||||
undefined,
|
||||
@@ -162,7 +162,7 @@ const TokenPageContent = () => {
|
||||
].filter(Boolean);
|
||||
|
||||
let hasPagination;
|
||||
let pagination;
|
||||
let pagination: PaginationProps | undefined;
|
||||
|
||||
if (!router.query.tab || router.query.tab === 'token_transfers') {
|
||||
hasPagination = transfersQuery.isPaginationVisible;
|
||||
@@ -227,7 +227,7 @@ const TokenPageContent = () => {
|
||||
<RoutedTabs
|
||||
tabs={ tabs }
|
||||
tabListProps={ tabListProps }
|
||||
rightSlot={ !isMobile && hasPagination ? <Pagination { ...(pagination as PaginationProps) }/> : null }
|
||||
rightSlot={ !isMobile && hasPagination && pagination ? <Pagination { ...pagination }/> : null }
|
||||
stickyEnabled={ !isMobile }
|
||||
/>
|
||||
) }
|
||||
|
||||
@@ -14,17 +14,17 @@ import TokenHoldersList from './TokenHoldersList';
|
||||
import TokenHoldersTable from './TokenHoldersTable';
|
||||
|
||||
type Props = {
|
||||
tokenQuery: UseQueryResult<TokenInfo>;
|
||||
token?: TokenInfo;
|
||||
holdersQuery: UseQueryResult<TokenHolders> & {
|
||||
pagination: PaginationProps;
|
||||
isPaginationVisible: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
const TokenHoldersContent = ({ holdersQuery, tokenQuery }: Props) => {
|
||||
const TokenHoldersContent = ({ holdersQuery, token }: Props) => {
|
||||
|
||||
const isMobile = useIsMobile();
|
||||
if (holdersQuery.isError || tokenQuery.isError) {
|
||||
if (holdersQuery.isError) {
|
||||
return <DataFetchAlert/>;
|
||||
}
|
||||
|
||||
@@ -36,17 +36,17 @@ const TokenHoldersContent = ({ holdersQuery, tokenQuery }: Props) => {
|
||||
|
||||
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 && <TokenHoldersList data={ items } token={ tokenQuery.data }/> }
|
||||
{ !isMobile && <TokenHoldersTable data={ items } token={ token } top={ holdersQuery.isPaginationVisible ? 80 : 0 }/> }
|
||||
{ isMobile && <TokenHoldersList data={ items } token={ token }/> }
|
||||
</>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<DataListDisplay
|
||||
isError={ holdersQuery.isError || tokenQuery.isError }
|
||||
isLoading={ holdersQuery.isLoading || tokenQuery.isLoading }
|
||||
isError={ holdersQuery.isError }
|
||||
isLoading={ holdersQuery.isLoading }
|
||||
items={ holdersQuery.data?.items }
|
||||
skeletonProps={{ skeletonDesktopColumns: [ '100%', '300px', '175px' ] }}
|
||||
emptyText="There are no holders for this token."
|
||||
|
||||
@@ -14,7 +14,10 @@ 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 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 TokenInstanceDetails from './TokenInstanceDetails';
|
||||
@@ -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> = [
|
||||
{ id: 'token_transfers', title: 'Token transfers', component: <TokenTransfer transfersQuery={ transfersQuery } tokenId={ id }/> },
|
||||
// there is no api for this tab yet
|
||||
// { id: 'holders', title: 'Holders', component: <span>Holders</span> },
|
||||
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 });
|
||||
@@ -100,6 +115,17 @@ const TokenInstanceContent = () => {
|
||||
}
|
||||
})();
|
||||
|
||||
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 }/>
|
||||
@@ -120,12 +146,14 @@ const TokenInstanceContent = () => {
|
||||
{ /* should stay before tabs to scroll up with pagination */ }
|
||||
<Box ref={ scrollRef }></Box>
|
||||
|
||||
<RoutedTabs
|
||||
tabs={ tabs }
|
||||
tabListProps={ isMobile ? { mt: 8 } : { mt: 3, py: 5, marginBottom: 0 } }
|
||||
rightSlot={ !isMobile && transfersQuery.isPaginationVisible && tab !== 'metadata' ? <Pagination { ...transfersQuery.pagination }/> : null }
|
||||
stickyEnabled={ !isMobile }
|
||||
/>
|
||||
{ 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' }}/> }
|
||||
</>
|
||||
|
||||
@@ -11,8 +11,8 @@ import TokenInstanceDetails from './TokenInstanceDetails';
|
||||
|
||||
const API_URL_ADDRESS = buildApiUrl('address', { hash: tokenInstanceMock.base.token.address });
|
||||
const API_URL_TOKEN_TRANSFERS_COUNT = buildApiUrl('token_instance_transfers_count', {
|
||||
id: tokenInstanceMock.base.id,
|
||||
hash: tokenInstanceMock.base.token.address,
|
||||
id: tokenInstanceMock.unique.id,
|
||||
hash: tokenInstanceMock.unique.token.address,
|
||||
});
|
||||
|
||||
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(
|
||||
<TestApp>
|
||||
<TokenInstanceDetails data={ tokenInstanceMock.base }/>
|
||||
<TokenInstanceDetails data={ tokenInstanceMock.unique }/>
|
||||
</TestApp>,
|
||||
);
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ const TokenInstanceDetails = ({ data, scrollRef }: Props) => {
|
||||
>
|
||||
<TokenSnippet hash={ data.token.address } name={ data.token.name }/>
|
||||
</DetailsInfoItem>
|
||||
{ data.owner && (
|
||||
{ data.is_unique && data.owner && (
|
||||
<DetailsInfoItem
|
||||
title="Owner"
|
||||
hint="Current owner of this token instance"
|
||||
|
||||
@@ -42,7 +42,7 @@ const TokenInstanceTransfersCount = ({ hash, id, onClick }: Props) => {
|
||||
href={ url }
|
||||
onClick={ transfersCountQuery.data.transfers_count > 0 ? onClick : undefined }
|
||||
>
|
||||
{ transfersCountQuery.data.transfers_count }
|
||||
{ transfersCountQuery.data.transfers_count.toLocaleString() }
|
||||
</LinkInternal>
|
||||
</DetailsInfoItem>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user