mirror of
https://github.com/luxfi/explore.git
synced 2026-09-24 00:28:18 +00:00
nft address start
This commit is contained in:
+18
-2
@@ -26,6 +26,8 @@ import type {
|
||||
AddressTokensFilter,
|
||||
AddressTokensResponse,
|
||||
AddressWithdrawalsResponse,
|
||||
AddressNFTsResponse,
|
||||
AddressCollectionsResponse,
|
||||
} from 'types/api/address';
|
||||
import type { AddressesResponse } from 'types/api/addresses';
|
||||
import type { BlocksResponse, BlockTransactionsResponse, Block, BlockFilters, BlockWithdrawalsResponse } from 'types/api/block';
|
||||
@@ -51,6 +53,7 @@ import type {
|
||||
TokenInstance,
|
||||
TokenInstanceTransfersCount,
|
||||
TokenVerifiedInfo,
|
||||
TokenInventoryFilters,
|
||||
} from 'types/api/token';
|
||||
import type { TokensResponse, TokensFilters, TokensSorting, TokenInstanceTransferResponse, TokensBridgedFilters } from 'types/api/tokens';
|
||||
import type { TokenTransferResponse, TokenTransferFilters } from 'types/api/tokenTransfer';
|
||||
@@ -305,6 +308,16 @@ export const RESOURCES = {
|
||||
pathParams: [ 'hash' as const ],
|
||||
filterFields: [ 'type' as const ],
|
||||
},
|
||||
address_nfts: {
|
||||
path: '/api/v2/addresses/:hash/nft',
|
||||
pathParams: [ 'hash' as const ],
|
||||
filterFields: [ ],
|
||||
},
|
||||
address_collections: {
|
||||
path: '/api/v2/addresses/:hash/nft/collections',
|
||||
pathParams: [ 'hash' as const ],
|
||||
filterFields: [ ],
|
||||
},
|
||||
address_withdrawals: {
|
||||
path: '/api/v2/addresses/:hash/withdrawals',
|
||||
pathParams: [ 'hash' as const ],
|
||||
@@ -384,7 +397,7 @@ export const RESOURCES = {
|
||||
token_inventory: {
|
||||
path: '/api/v2/tokens/:hash/instances',
|
||||
pathParams: [ 'hash' as const ],
|
||||
filterFields: [],
|
||||
filterFields: [ 'holder_address_hash' as const ],
|
||||
},
|
||||
tokens: {
|
||||
path: '/api/v2/tokens',
|
||||
@@ -580,7 +593,7 @@ export type PaginatedResources = 'blocks' | 'block_txs' |
|
||||
'addresses' |
|
||||
'address_txs' | 'address_internal_txs' | 'address_token_transfers' | 'address_blocks_validated' | 'address_coin_balance' |
|
||||
'search' |
|
||||
'address_logs' | 'address_tokens' |
|
||||
'address_logs' | 'address_tokens' | 'address_nfts' | 'address_collections' |
|
||||
'token_transfers' | 'token_holders' | 'token_inventory' | 'tokens' | 'tokens_bridged' |
|
||||
'token_instance_transfers' | 'token_instance_holders' |
|
||||
'verified_contracts' |
|
||||
@@ -642,6 +655,8 @@ Q extends 'address_coin_balance' ? AddressCoinBalanceHistoryResponse :
|
||||
Q extends 'address_coin_balance_chart' ? AddressCoinBalanceHistoryChart :
|
||||
Q extends 'address_logs' ? LogsResponseAddress :
|
||||
Q extends 'address_tokens' ? AddressTokensResponse :
|
||||
Q extends 'address_nfts' ? AddressNFTsResponse :
|
||||
Q extends 'address_collections' ? AddressCollectionsResponse :
|
||||
Q extends 'address_withdrawals' ? AddressWithdrawalsResponse :
|
||||
Q extends 'token' ? TokenInfo :
|
||||
Q extends 'token_verified_info' ? TokenVerifiedInfo :
|
||||
@@ -696,6 +711,7 @@ Q extends 'address_txs' | 'address_internal_txs' ? AddressTxsFilters :
|
||||
Q extends 'address_token_transfers' ? AddressTokenTransferFilters :
|
||||
Q extends 'address_tokens' ? AddressTokensFilter :
|
||||
Q extends 'search' ? SearchResultFilters :
|
||||
Q extends 'token_inventory' ? TokenInventoryFilters :
|
||||
Q extends 'tokens' ? TokensFilters :
|
||||
Q extends 'tokens_bridged' ? TokensBridgedFilters :
|
||||
Q extends 'verified_contracts' ? VerifiedContractsFilters :
|
||||
|
||||
+23
-11
@@ -1,8 +1,16 @@
|
||||
import type { Address, AddressCoinBalanceHistoryItem, AddressCounters, AddressTabsCounters, AddressTokenBalance } from 'types/api/address';
|
||||
import type {
|
||||
Address,
|
||||
AddressCoinBalanceHistoryItem,
|
||||
AddressCollection,
|
||||
AddressCounters,
|
||||
AddressNFT,
|
||||
AddressTabsCounters,
|
||||
AddressTokenBalance,
|
||||
} from 'types/api/address';
|
||||
import type { AddressesItem } from 'types/api/addresses';
|
||||
|
||||
import { ADDRESS_HASH } from './addressParams';
|
||||
import { TOKEN_INFO_ERC_1155, TOKEN_INFO_ERC_20, TOKEN_INFO_ERC_721, TOKEN_INSTANCE } from './token';
|
||||
import { TOKEN_INFO_ERC_1155, TOKEN_INFO_ERC_20, TOKEN_INSTANCE } from './token';
|
||||
import { TX_HASH } from './tx';
|
||||
|
||||
export const ADDRESS_INFO: Address = {
|
||||
@@ -80,16 +88,20 @@ export const ADDRESS_TOKEN_BALANCE_ERC_20: AddressTokenBalance = {
|
||||
value: '1000000000000000000000000',
|
||||
};
|
||||
|
||||
export const ADDRESS_TOKEN_BALANCE_ERC_721: AddressTokenBalance = {
|
||||
token: TOKEN_INFO_ERC_721,
|
||||
token_id: null,
|
||||
token_instance: null,
|
||||
value: '176',
|
||||
export const ADDRESS_NFT_721: AddressNFT = {
|
||||
token_type: 'ERC-721',
|
||||
value: '1',
|
||||
...TOKEN_INSTANCE,
|
||||
};
|
||||
|
||||
export const ADDRESS_TOKEN_BALANCE_ERC_1155: AddressTokenBalance = {
|
||||
export const ADDRESS_NFT_1155: AddressNFT = {
|
||||
token_type: 'ERC-1155',
|
||||
value: '10',
|
||||
...TOKEN_INSTANCE,
|
||||
};
|
||||
|
||||
export const ADDRESS_COLLECTION: AddressCollection = {
|
||||
token: TOKEN_INFO_ERC_1155,
|
||||
token_id: '188882',
|
||||
token_instance: TOKEN_INSTANCE,
|
||||
value: '176',
|
||||
amount: '4',
|
||||
token_instances: Array(4).fill(TOKEN_INSTANCE),
|
||||
};
|
||||
|
||||
+33
-1
@@ -49,11 +49,43 @@ export interface AddressTokenBalance {
|
||||
token_instance: TokenInstance | null;
|
||||
}
|
||||
|
||||
export type AddressNFT = TokenInstance & {
|
||||
token_type: Omit<TokenType, 'ERC-20'>;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export type AddressCollection = {
|
||||
token: TokenInfo;
|
||||
amount: string;
|
||||
token_instances: Array<Omit<AddressNFT, 'token'>>;
|
||||
}
|
||||
|
||||
export interface AddressTokensResponse {
|
||||
items: Array<AddressTokenBalance>;
|
||||
next_page_params: {
|
||||
items_count: number;
|
||||
token_name: 'string' | null;
|
||||
token_name: string | null;
|
||||
token_type: TokenType;
|
||||
value: number;
|
||||
fiat_value: string | null;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export interface AddressNFTsResponse {
|
||||
items: Array<AddressNFT>;
|
||||
next_page_params: {
|
||||
items_count: number;
|
||||
token_id: string;
|
||||
token_type: TokenType;
|
||||
token_contract_address_hash: string;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export interface AddressCollectionsResponse {
|
||||
items: Array<AddressCollection>;
|
||||
next_page_params: {
|
||||
items_count: number;
|
||||
token_name: string | null;
|
||||
token_type: TokenType;
|
||||
value: number;
|
||||
fiat_value: string | null;
|
||||
|
||||
@@ -77,3 +77,7 @@ export type TokenInventoryPagination = {
|
||||
}
|
||||
|
||||
export type TokenVerifiedInfo = Omit<TokenInfoApplication, 'id' | 'status'>;
|
||||
|
||||
export type TokenInventoryFilters = {
|
||||
holder_address_hash?: string;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Flex, Hide, Icon, Show, Text, Tooltip, useColorModeValue } from '@chakra-ui/react';
|
||||
import { Flex, Hide, Show, Text } from '@chakra-ui/react';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { useRouter } from 'next/router';
|
||||
import React from 'react';
|
||||
@@ -9,7 +9,6 @@ import type { AddressFromToFilter, AddressTokenTransferResponse } from 'types/ap
|
||||
import type { TokenType } from 'types/api/token';
|
||||
import type { TokenTransfer } from 'types/api/tokenTransfer';
|
||||
|
||||
import crossIcon from 'icons/cross.svg';
|
||||
import { getResourceKey } from 'lib/api/useApiQuery';
|
||||
import getFilterValueFromQuery from 'lib/getFilterValueFromQuery';
|
||||
import getFilterValuesFromQuery from 'lib/getFilterValuesFromQuery';
|
||||
@@ -26,6 +25,7 @@ import * as TokenEntity from 'ui/shared/entities/token/TokenEntity';
|
||||
import HashStringShorten from 'ui/shared/HashStringShorten';
|
||||
import Pagination from 'ui/shared/pagination/Pagination';
|
||||
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
|
||||
import ResetIconButton from 'ui/shared/ResetIconButton';
|
||||
import * as SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice';
|
||||
import TokenTransferFilter from 'ui/shared/TokenTransfer/TokenTransferFilter';
|
||||
import TokenTransferList from 'ui/shared/TokenTransfer/TokenTransferList';
|
||||
@@ -115,9 +115,6 @@ const AddressTokenTransfers = ({ scrollRef, overloadCount = OVERLOAD_COUNT }: Pr
|
||||
onFilterChange({});
|
||||
}, [ onFilterChange ]);
|
||||
|
||||
const resetTokenIconColor = useColorModeValue('blue.600', 'blue.300');
|
||||
const resetTokenIconHoverColor = useColorModeValue('blue.400', 'blue.200');
|
||||
|
||||
const handleNewSocketMessage: SocketMessage.AddressTokenTransfer['handler'] = (payload) => {
|
||||
setSocketAlert('');
|
||||
|
||||
@@ -235,19 +232,7 @@ const AddressTokenTransfers = ({ scrollRef, overloadCount = OVERLOAD_COUNT }: Pr
|
||||
<Flex alignItems="center" py={ 1 }>
|
||||
<TokenEntity.Icon token={ tokenData } isLoading={ isPlaceholderData }/>
|
||||
{ isMobile ? <HashStringShorten hash={ tokenFilter }/> : tokenFilter }
|
||||
<Tooltip label="Reset filter">
|
||||
<Flex>
|
||||
<Icon
|
||||
as={ crossIcon }
|
||||
boxSize={ 5 }
|
||||
ml={ 1 }
|
||||
color={ resetTokenIconColor }
|
||||
cursor="pointer"
|
||||
_hover={{ color: resetTokenIconHoverColor }}
|
||||
onClick={ resetTokenFilter }
|
||||
/>
|
||||
</Flex>
|
||||
</Tooltip>
|
||||
<ResetIconButton onClick={ resetTokenFilter }/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import { Box, Radio, RadioGroup } from '@chakra-ui/react';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { useRouter } from 'next/router';
|
||||
import React from 'react';
|
||||
@@ -13,18 +13,19 @@ import useIsMobile from 'lib/hooks/useIsMobile';
|
||||
import getQueryParamString from 'lib/router/getQueryParamString';
|
||||
import useSocketChannel from 'lib/socket/useSocketChannel';
|
||||
import useSocketMessage from 'lib/socket/useSocketMessage';
|
||||
import { ADDRESS_TOKEN_BALANCE_ERC_1155, ADDRESS_TOKEN_BALANCE_ERC_20, ADDRESS_TOKEN_BALANCE_ERC_721 } from 'stubs/address';
|
||||
import { ADDRESS_TOKEN_BALANCE_ERC_20, ADDRESS_NFT_1155, ADDRESS_COLLECTION } from 'stubs/address';
|
||||
import { generateListStub } from 'stubs/utils';
|
||||
import { tokenTabsByType } from 'ui/pages/Address';
|
||||
import Pagination from 'ui/shared/pagination/Pagination';
|
||||
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
|
||||
import RoutedTabs from 'ui/shared/Tabs/RoutedTabs';
|
||||
|
||||
import ERC1155Tokens from './tokens/ERC1155Tokens';
|
||||
import AddressCollections from './tokens/AddressCollections';
|
||||
import AddressNFTs from './tokens/AddressNFTs';
|
||||
import ERC20Tokens from './tokens/ERC20Tokens';
|
||||
import ERC721Tokens from './tokens/ERC721Tokens';
|
||||
import TokenBalances from './tokens/TokenBalances';
|
||||
|
||||
type TNftDisplayType = 'collection' | 'list';
|
||||
|
||||
const TAB_LIST_PROPS = {
|
||||
marginBottom: 0,
|
||||
py: 5,
|
||||
@@ -49,6 +50,8 @@ const AddressTokens = () => {
|
||||
|
||||
const scrollRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
const [ nftDisplayType, setNftDisplayType ] = React.useState<TNftDisplayType>('collection');
|
||||
|
||||
const tab = getQueryParamString(router.query.tab);
|
||||
const hash = getQueryParamString(router.query.hash);
|
||||
|
||||
@@ -58,30 +61,31 @@ const AddressTokens = () => {
|
||||
filters: { type: 'ERC-20' },
|
||||
scrollRef,
|
||||
options: {
|
||||
enabled: !tab || tab === 'tokens_erc20',
|
||||
refetchOnMount: false,
|
||||
placeholderData: generateListStub<'address_tokens'>(ADDRESS_TOKEN_BALANCE_ERC_20, 10, { next_page_params: null }),
|
||||
},
|
||||
});
|
||||
|
||||
const erc721Query = useQueryWithPages({
|
||||
resourceName: 'address_tokens',
|
||||
const collectionsQuery = useQueryWithPages({
|
||||
resourceName: 'address_collections',
|
||||
pathParams: { hash },
|
||||
filters: { type: 'ERC-721' },
|
||||
scrollRef,
|
||||
options: {
|
||||
enabled: tab === 'tokens_nfts' && nftDisplayType === 'collection',
|
||||
refetchOnMount: false,
|
||||
placeholderData: generateListStub<'address_tokens'>(ADDRESS_TOKEN_BALANCE_ERC_721, 10, { next_page_params: null }),
|
||||
placeholderData: generateListStub<'address_collections'>(ADDRESS_COLLECTION, 10, { next_page_params: null }),
|
||||
},
|
||||
});
|
||||
|
||||
const erc1155Query = useQueryWithPages({
|
||||
resourceName: 'address_tokens',
|
||||
const nftsQuery = useQueryWithPages({
|
||||
resourceName: 'address_nfts',
|
||||
pathParams: { hash },
|
||||
filters: { type: 'ERC-1155' },
|
||||
scrollRef,
|
||||
options: {
|
||||
enabled: tab === 'tokens_nfts' && nftDisplayType === 'list',
|
||||
refetchOnMount: false,
|
||||
placeholderData: generateListStub<'address_tokens'>(ADDRESS_TOKEN_BALANCE_ERC_1155, 10, { next_page_params: null }),
|
||||
placeholderData: generateListStub<'address_nfts'>(ADDRESS_NFT_1155, 10, { next_page_params: null }),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -128,7 +132,8 @@ const AddressTokens = () => {
|
||||
|
||||
const channel = useSocketChannel({
|
||||
topic: `addresses:${ hash.toLowerCase() }`,
|
||||
isDisabled: erc20Query.isPlaceholderData || erc721Query.isPlaceholderData || erc1155Query.isPlaceholderData,
|
||||
// !!!
|
||||
isDisabled: erc20Query.isPlaceholderData || nftsQuery.isPlaceholderData || collectionsQuery.isPlaceholderData,
|
||||
});
|
||||
|
||||
useSocketMessage({
|
||||
@@ -147,22 +152,43 @@ const AddressTokens = () => {
|
||||
handler: handleTokenBalancesErc1155Message,
|
||||
});
|
||||
|
||||
const handleNFTsDisplayTypeChange = React.useCallback((val: TNftDisplayType) => {
|
||||
setNftDisplayType(val);
|
||||
}, []);
|
||||
|
||||
const tabs = [
|
||||
{ id: tokenTabsByType['ERC-20'], title: 'ERC-20', component: <ERC20Tokens tokensQuery={ erc20Query }/> },
|
||||
{ id: tokenTabsByType['ERC-721'], title: 'ERC-721', component: <ERC721Tokens tokensQuery={ erc721Query }/> },
|
||||
{ id: tokenTabsByType['ERC-1155'], title: 'ERC-1155', component: <ERC1155Tokens tokensQuery={ erc1155Query }/> },
|
||||
{ id: 'tokens_erc20', title: 'ERC-20', component: <ERC20Tokens tokensQuery={ erc20Query }/> },
|
||||
{
|
||||
id: 'tokens_nfts',
|
||||
title: 'NFTs',
|
||||
component: nftDisplayType === 'list' ?
|
||||
<AddressNFTs tokensQuery={ nftsQuery }/> :
|
||||
<AddressCollections collectionsQuery={ collectionsQuery } address={ hash }/>,
|
||||
},
|
||||
];
|
||||
|
||||
const nftDisplayTypeRadio = (
|
||||
<RadioGroup onChange={ handleNFTsDisplayTypeChange } value={ nftDisplayType }>
|
||||
<Radio value="collection">Collection</Radio>
|
||||
<Radio value="list">List</Radio>
|
||||
</RadioGroup>
|
||||
);
|
||||
|
||||
let pagination: PaginationParams | undefined;
|
||||
|
||||
if (tab === tokenTabsByType['ERC-1155']) {
|
||||
pagination = erc1155Query.pagination;
|
||||
} else if (tab === tokenTabsByType['ERC-721']) {
|
||||
pagination = erc721Query.pagination;
|
||||
if (tab === 'tokens_nfts') {
|
||||
pagination = nftDisplayType === 'list' ? nftsQuery.pagination : collectionsQuery.pagination;
|
||||
} else {
|
||||
pagination = erc20Query.pagination;
|
||||
}
|
||||
|
||||
const rightSlot = (
|
||||
<>
|
||||
{ tab !== 'tokens_erc20' && nftDisplayTypeRadio }
|
||||
{ pagination.isVisible && !isMobile && <Pagination { ...pagination }/> }
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<TokenBalances/>
|
||||
@@ -174,7 +200,7 @@ const AddressTokens = () => {
|
||||
colorScheme="gray"
|
||||
size="sm"
|
||||
tabListProps={ isMobile ? TAB_LIST_PROPS_MOBILE : TAB_LIST_PROPS }
|
||||
rightSlot={ pagination.isVisible && !isMobile ? <Pagination { ...pagination }/> : null }
|
||||
rightSlot={ rightSlot }
|
||||
stickyEnabled={ !isMobile }
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
import { Box, Flex, Text, Grid, HStack, Skeleton } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import { route } from 'nextjs-routes';
|
||||
|
||||
import useIsMobile from 'lib/hooks/useIsMobile';
|
||||
import ActionBar from 'ui/shared/ActionBar';
|
||||
import DataListDisplay from 'ui/shared/DataListDisplay';
|
||||
import TokenEntity from 'ui/shared/entities/token/TokenEntity';
|
||||
import LinkInternal from 'ui/shared/LinkInternal';
|
||||
import NftFallback from 'ui/shared/nft/NftFallback';
|
||||
import Pagination from 'ui/shared/pagination/Pagination';
|
||||
import type { QueryWithPagesResult } from 'ui/shared/pagination/useQueryWithPages';
|
||||
|
||||
import NFTItem from './NFTItem';
|
||||
import NFTItemContainer from './NFTItemContainer';
|
||||
|
||||
type Props = {
|
||||
collectionsQuery: QueryWithPagesResult<'address_collections'>;
|
||||
address: string;
|
||||
}
|
||||
|
||||
const AddressCollections = ({ collectionsQuery, address }: Props) => {
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const { isError, isPlaceholderData, data, pagination } = collectionsQuery;
|
||||
|
||||
const actionBar = isMobile && pagination.isVisible && (
|
||||
<ActionBar mt={ -6 }>
|
||||
<Pagination ml="auto" { ...pagination }/>
|
||||
</ActionBar>
|
||||
);
|
||||
|
||||
const content = data?.items ? data?.items.map((item, index) => {
|
||||
const collectionUrl = route({
|
||||
pathname: '/token/[hash]',
|
||||
query: {
|
||||
hash: item.token.address,
|
||||
tab: 'inventory',
|
||||
holder_address_hash: address,
|
||||
scroll_to_tabs: 'true',
|
||||
},
|
||||
});
|
||||
const hasOverload = Number(item.amount) > item.token_instances.length;
|
||||
return (
|
||||
<Box key={ item.token.address + index } mb={ 6 }>
|
||||
<Flex mb={ 3 }>
|
||||
<TokenEntity
|
||||
width="auto"
|
||||
noSymbol
|
||||
token={ item.token }
|
||||
isLoading={ isPlaceholderData }
|
||||
noCopy
|
||||
fontWeight="600"
|
||||
/>
|
||||
<Skeleton isLoaded={ !isPlaceholderData } >
|
||||
<Text variant="secondary" whiteSpace="pre">{ ` - ${ Number(item.amount).toLocaleString() } item${ Number(item.amount) > 1 ? 's' : '' }` }</Text>
|
||||
</Skeleton>
|
||||
{ hasOverload && (
|
||||
<LinkInternal href={ collectionUrl } ml={ 3 } isLoading={ isPlaceholderData }>
|
||||
<Skeleton isLoaded={ !isPlaceholderData }>View in collection</Skeleton>
|
||||
</LinkInternal>
|
||||
) }
|
||||
</Flex>
|
||||
<Grid
|
||||
w="100%"
|
||||
mb={ 7 }
|
||||
columnGap={{ base: 3, lg: 6 }}
|
||||
rowGap={{ base: 3, lg: 6 }}
|
||||
gridTemplateColumns={{ base: 'repeat(2, calc((100% - 12px)/2))', lg: 'repeat(auto-fill, minmax(210px, 1fr))' }}
|
||||
>
|
||||
{ item.token_instances.map((instance, index) => {
|
||||
const key = item.token.address + '_' + (instance.id && !isPlaceholderData ? `id_${ instance.id }` : `index_${ index }`);
|
||||
|
||||
return (
|
||||
<NFTItem
|
||||
key={ key }
|
||||
{ ...instance }
|
||||
token={ item.token }
|
||||
isLoading={ isPlaceholderData }
|
||||
/>
|
||||
);
|
||||
}) }
|
||||
{ hasOverload && (
|
||||
<LinkInternal display="flex" href={ collectionUrl }>
|
||||
<NFTItemContainer display="flex" alignItems="center" justifyContent="center" flexDirection="column" minH="248px">
|
||||
<HStack gap={ 2 } mb={ 3 }>
|
||||
<NftFallback bgColor="unset" w="30px" h="30px" boxSize="30px" p={ 0 }/>
|
||||
<NftFallback bgColor="unset" w="30px" h="30px" boxSize="30px" p={ 0 }/>
|
||||
<NftFallback bgColor="unset" w="30px" h="30px" boxSize="30px" p={ 0 }/>
|
||||
</HStack>
|
||||
View all NFTs
|
||||
</NFTItemContainer>
|
||||
</LinkInternal>
|
||||
) }
|
||||
</Grid>
|
||||
</Box>
|
||||
);
|
||||
}) : null;
|
||||
|
||||
return (
|
||||
<DataListDisplay
|
||||
isError={ isError }
|
||||
items={ data?.items }
|
||||
emptyText="There are no tokens of selected type."
|
||||
content={ content }
|
||||
actionBar={ actionBar }
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddressCollections;
|
||||
@@ -10,10 +10,10 @@ import type { QueryWithPagesResult } from 'ui/shared/pagination/useQueryWithPage
|
||||
import NFTItem from './NFTItem';
|
||||
|
||||
type Props = {
|
||||
tokensQuery: QueryWithPagesResult<'address_tokens'>;
|
||||
tokensQuery: QueryWithPagesResult<'address_nfts'>;
|
||||
}
|
||||
|
||||
const ERC1155Tokens = ({ tokensQuery }: Props) => {
|
||||
const AddressNFTs = ({ tokensQuery }: Props) => {
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const { isError, isPlaceholderData, data, pagination } = tokensQuery;
|
||||
@@ -32,13 +32,14 @@ const ERC1155Tokens = ({ tokensQuery }: Props) => {
|
||||
gridTemplateColumns={{ base: 'repeat(2, calc((100% - 12px)/2))', lg: 'repeat(auto-fill, minmax(210px, 1fr))' }}
|
||||
>
|
||||
{ data.items.map((item, index) => {
|
||||
const key = item.token.address + '_' + (item.token_instance?.id && !isPlaceholderData ? `id_${ item.token_instance?.id }` : `index_${ index }`);
|
||||
const key = item.token.address + '_' + (item.id && !isPlaceholderData ? `id_${ item.id }` : `index_${ index }`);
|
||||
|
||||
return (
|
||||
<NFTItem
|
||||
key={ key }
|
||||
{ ...item }
|
||||
isLoading={ isPlaceholderData }
|
||||
withTokenLink
|
||||
/>
|
||||
);
|
||||
}) }
|
||||
@@ -56,4 +57,4 @@ const ERC1155Tokens = ({ tokensQuery }: Props) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default ERC1155Tokens;
|
||||
export default AddressNFTs;
|
||||
@@ -1,52 +0,0 @@
|
||||
import { Show, Hide } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import useIsMobile from 'lib/hooks/useIsMobile';
|
||||
import ActionBar from 'ui/shared/ActionBar';
|
||||
import DataListDisplay from 'ui/shared/DataListDisplay';
|
||||
import Pagination from 'ui/shared/pagination/Pagination';
|
||||
import type { QueryWithPagesResult } from 'ui/shared/pagination/useQueryWithPages';
|
||||
|
||||
import ERC721TokensListItem from './ERC721TokensListItem';
|
||||
import ERC721TokensTable from './ERC721TokensTable';
|
||||
|
||||
type Props = {
|
||||
tokensQuery: QueryWithPagesResult<'address_tokens'>;
|
||||
}
|
||||
|
||||
const ERC721Tokens = ({ tokensQuery }: Props) => {
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const { isError, isPlaceholderData, data, pagination } = tokensQuery;
|
||||
|
||||
const actionBar = isMobile && pagination.isVisible && (
|
||||
<ActionBar mt={ -6 }>
|
||||
<Pagination ml="auto" { ...pagination }/>
|
||||
</ActionBar>
|
||||
);
|
||||
|
||||
const content = data?.items ? (
|
||||
<>
|
||||
<Hide below="lg" ssr={ false }><ERC721TokensTable data={ data.items } isLoading={ isPlaceholderData } top={ pagination.isVisible ? 72 : 0 }/></Hide>
|
||||
<Show below="lg" ssr={ false }>{ data.items.map((item, index) => (
|
||||
<ERC721TokensListItem
|
||||
key={ item.token.address + (isPlaceholderData ? index : '') }
|
||||
{ ...item }
|
||||
isLoading={ isPlaceholderData }
|
||||
/>
|
||||
)) }</Show></>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<DataListDisplay
|
||||
isError={ isError }
|
||||
items={ data?.items }
|
||||
emptyText="There are no tokens of selected type."
|
||||
content={ content }
|
||||
actionBar={ actionBar }
|
||||
/>
|
||||
);
|
||||
|
||||
};
|
||||
|
||||
export default ERC721Tokens;
|
||||
@@ -1,48 +0,0 @@
|
||||
import { Flex, HStack, Skeleton } from '@chakra-ui/react';
|
||||
import { useRouter } from 'next/router';
|
||||
import React from 'react';
|
||||
|
||||
import type { AddressTokenBalance } from 'types/api/address';
|
||||
|
||||
import AddressAddToWallet from 'ui/shared/address/AddressAddToWallet';
|
||||
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
|
||||
import TokenEntityWithAddressFilter from 'ui/shared/entities/token/TokenEntityWithAddressFilter';
|
||||
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
|
||||
|
||||
type Props = AddressTokenBalance & { isLoading: boolean};
|
||||
|
||||
const ERC721TokensListItem = ({ token, value, isLoading }: Props) => {
|
||||
const router = useRouter();
|
||||
|
||||
const hash = router.query.hash?.toString() || '';
|
||||
|
||||
return (
|
||||
<ListItemMobile rowGap={ 2 }>
|
||||
<TokenEntityWithAddressFilter
|
||||
token={ token }
|
||||
isLoading={ isLoading }
|
||||
addressHash={ hash }
|
||||
noCopy
|
||||
jointSymbol
|
||||
fontWeight={ 700 }
|
||||
/>
|
||||
<Flex alignItems="center" pl={ 8 }>
|
||||
<AddressEntity
|
||||
address={{ hash: token.address }}
|
||||
isLoading={ isLoading }
|
||||
truncation="constant"
|
||||
noIcon
|
||||
/>
|
||||
<AddressAddToWallet token={ token } ml={ 2 } isLoading={ isLoading }/>
|
||||
</Flex>
|
||||
<HStack spacing={ 3 }>
|
||||
<Skeleton isLoaded={ !isLoading } fontSize="sm" fontWeight={ 500 }>Quantity</Skeleton>
|
||||
<Skeleton isLoaded={ !isLoading } fontSize="sm" color="text_secondary">
|
||||
<span>{ value }</span>
|
||||
</Skeleton>
|
||||
</HStack>
|
||||
</ListItemMobile>
|
||||
);
|
||||
};
|
||||
|
||||
export default ERC721TokensListItem;
|
||||
@@ -1,35 +0,0 @@
|
||||
import { Table, Tbody, Tr, Th } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import type { AddressTokenBalance } from 'types/api/address';
|
||||
|
||||
import { default as Thead } from 'ui/shared/TheadSticky';
|
||||
|
||||
import ERC721TokensTableItem from './ERC721TokensTableItem';
|
||||
|
||||
interface Props {
|
||||
data: Array<AddressTokenBalance>;
|
||||
top: number;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
const ERC721TokensTable = ({ data, top, isLoading }: Props) => {
|
||||
return (
|
||||
<Table variant="simple" size="sm">
|
||||
<Thead top={ top }>
|
||||
<Tr>
|
||||
<Th width="40%">Asset</Th>
|
||||
<Th width="40%">Contract address</Th>
|
||||
<Th width="20%" isNumeric>Quantity</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{ data.map((item, index) => (
|
||||
<ERC721TokensTableItem key={ item.token.address + (isLoading ? index : '') } { ...item } isLoading={ isLoading }/>
|
||||
)) }
|
||||
</Tbody>
|
||||
</Table>
|
||||
);
|
||||
};
|
||||
|
||||
export default ERC721TokensTable;
|
||||
@@ -1,53 +0,0 @@
|
||||
import { Tr, Td, Flex, Skeleton } from '@chakra-ui/react';
|
||||
import { useRouter } from 'next/router';
|
||||
import React from 'react';
|
||||
|
||||
import type { AddressTokenBalance } from 'types/api/address';
|
||||
|
||||
import AddressAddToWallet from 'ui/shared/address/AddressAddToWallet';
|
||||
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
|
||||
import TokenEntityWithAddressFilter from 'ui/shared/entities/token/TokenEntityWithAddressFilter';
|
||||
|
||||
type Props = AddressTokenBalance & { isLoading: boolean};
|
||||
|
||||
const ERC721TokensTableItem = ({
|
||||
token,
|
||||
value,
|
||||
isLoading,
|
||||
}: Props) => {
|
||||
const router = useRouter();
|
||||
|
||||
const hash = router.query.hash?.toString() || '';
|
||||
|
||||
return (
|
||||
<Tr>
|
||||
<Td verticalAlign="middle">
|
||||
<TokenEntityWithAddressFilter
|
||||
token={ token }
|
||||
addressHash={ hash }
|
||||
isLoading={ isLoading }
|
||||
noCopy
|
||||
jointSymbol
|
||||
fontWeight="700"
|
||||
/>
|
||||
</Td>
|
||||
<Td verticalAlign="middle">
|
||||
<Flex alignItems="center" width="150px" justifyContent="space-between">
|
||||
<AddressEntity
|
||||
address={{ hash: token.address }}
|
||||
isLoading={ isLoading }
|
||||
noIcon
|
||||
/>
|
||||
<AddressAddToWallet token={ token } ml={ 4 } isLoading={ isLoading }/>
|
||||
</Flex>
|
||||
</Td>
|
||||
<Td isNumeric verticalAlign="middle">
|
||||
<Skeleton isLoaded={ !isLoading } display="inline-block">
|
||||
{ value }
|
||||
</Skeleton>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(ERC721TokensTableItem);
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Box, Flex, Text, Link, useColorModeValue } from '@chakra-ui/react';
|
||||
import { Tag, Flex, Text, Link, Skeleton, LightMode } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import type { AddressTokenBalance } from 'types/api/address';
|
||||
import type { AddressNFT } from 'types/api/address';
|
||||
|
||||
import { route } from 'nextjs-routes';
|
||||
|
||||
@@ -9,22 +9,20 @@ import NftEntity from 'ui/shared/entities/nft/NftEntity';
|
||||
import TokenEntity from 'ui/shared/entities/token/TokenEntity';
|
||||
import NftMedia from 'ui/shared/nft/NftMedia';
|
||||
|
||||
type Props = AddressTokenBalance & { isLoading: boolean };
|
||||
import NFTItemContainer from './NFTItemContainer';
|
||||
|
||||
const NFTItem = ({ token, token_id: tokenId, token_instance: tokenInstance, isLoading }: Props) => {
|
||||
const tokenInstanceLink = tokenId ? route({ pathname: '/token/[hash]/instance/[id]', query: { hash: token.address, id: tokenId } }) : undefined;
|
||||
type Props = AddressNFT & { isLoading: boolean; withTokenLink?: boolean };
|
||||
|
||||
const NFTItem = ({ token, value, isLoading, withTokenLink, ...tokenInstance }: Props) => {
|
||||
const tokenInstanceLink = tokenInstance.id ?
|
||||
route({ pathname: '/token/[hash]/instance/[id]', query: { hash: token.address, id: tokenInstance.id } }) :
|
||||
undefined;
|
||||
|
||||
return (
|
||||
<Box
|
||||
w={{ base: '100%', lg: '210px' }}
|
||||
border="1px solid"
|
||||
borderColor={ useColorModeValue('blackAlpha.100', 'whiteAlpha.200') }
|
||||
borderRadius="12px"
|
||||
p="10px"
|
||||
fontSize="sm"
|
||||
fontWeight={ 500 }
|
||||
lineHeight="20px"
|
||||
>
|
||||
<NFTItemContainer position="relative">
|
||||
<Skeleton isLoaded={ !isLoading }>
|
||||
<LightMode><Tag background="gray.50" zIndex={ 1 } position="absolute" top="18px" right="18px">{ token.type }</Tag></LightMode>
|
||||
</Skeleton>
|
||||
<Link href={ isLoading ? undefined : tokenInstanceLink }>
|
||||
<NftMedia
|
||||
mb="18px"
|
||||
@@ -32,19 +30,25 @@ const NFTItem = ({ token, token_id: tokenId, token_instance: tokenInstance, isLo
|
||||
isLoading={ isLoading }
|
||||
/>
|
||||
</Link>
|
||||
{ tokenId && (
|
||||
<Flex mb={ 2 } ml={ 1 }>
|
||||
<Flex justifyContent="space-between" w="100%">
|
||||
<Flex ml={ 1 }>
|
||||
<Text whiteSpace="pre" variant="secondary">ID# </Text>
|
||||
<NftEntity hash={ token.address } id={ tokenId } isLoading={ isLoading } noIcon/>
|
||||
<NftEntity hash={ token.address } id={ tokenInstance.id } isLoading={ isLoading } noIcon/>
|
||||
</Flex>
|
||||
<Skeleton isLoaded={ !isLoading }>
|
||||
{ Number(value) > 1 && <Flex><Text variant="secondary" whiteSpace="pre">Qty </Text>{ value }</Flex> }
|
||||
</Skeleton>
|
||||
</Flex>
|
||||
{ withTokenLink && (
|
||||
<TokenEntity
|
||||
mt={ 2 }
|
||||
token={ token }
|
||||
isLoading={ isLoading }
|
||||
noCopy
|
||||
noSymbol
|
||||
/>
|
||||
) }
|
||||
<TokenEntity
|
||||
token={ token }
|
||||
isLoading={ isLoading }
|
||||
noCopy
|
||||
noSymbol
|
||||
/>
|
||||
</Box>
|
||||
</NFTItemContainer>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { Box, useColorModeValue, chakra } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const NFTItem = ({ children, className }: Props) => {
|
||||
return (
|
||||
<Box
|
||||
w={{ base: '100%', lg: '210px' }}
|
||||
border="1px solid"
|
||||
borderColor={ useColorModeValue('blackAlpha.100', 'whiteAlpha.200') }
|
||||
borderRadius="12px"
|
||||
p="10px"
|
||||
fontSize="sm"
|
||||
fontWeight={ 500 }
|
||||
lineHeight="20px"
|
||||
className={ className }
|
||||
>
|
||||
{ children }
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default chakra(NFTItem);
|
||||
@@ -2,7 +2,6 @@ import { Box, Flex, HStack, Icon } from '@chakra-ui/react';
|
||||
import { useRouter } from 'next/router';
|
||||
import React from 'react';
|
||||
|
||||
import type { TokenType } from 'types/api/token';
|
||||
import type { RoutedTab } from 'ui/shared/Tabs/types';
|
||||
|
||||
import config from 'configs/app';
|
||||
@@ -36,13 +35,7 @@ import PageTitle from 'ui/shared/Page/PageTitle';
|
||||
import RoutedTabs from 'ui/shared/Tabs/RoutedTabs';
|
||||
import TabsSkeleton from 'ui/shared/Tabs/TabsSkeleton';
|
||||
|
||||
export const tokenTabsByType: Record<TokenType, string> = {
|
||||
'ERC-20': 'tokens_erc20',
|
||||
'ERC-721': 'tokens_erc721',
|
||||
'ERC-1155': 'tokens_erc1155',
|
||||
} as const;
|
||||
|
||||
const TOKEN_TABS = Object.values(tokenTabsByType);
|
||||
const TOKEN_TABS = [ 'tokens_erc20', 'tokens_nfts', 'tokens_nfts_collection', 'tokens_nfts_list' ];
|
||||
|
||||
const AddressPageContent = () => {
|
||||
const router = useRouter();
|
||||
|
||||
+8
-4
@@ -56,6 +56,7 @@ const TokenPageContent = () => {
|
||||
|
||||
const hashString = getQueryParamString(router.query.hash);
|
||||
const tab = getQueryParamString(router.query.tab);
|
||||
const ownerFilter = getQueryParamString(router.query.holder_address_hash) || undefined;
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
@@ -140,6 +141,7 @@ const TokenPageContent = () => {
|
||||
const inventoryQuery = useQueryWithPages({
|
||||
resourceName: 'token_inventory',
|
||||
pathParams: { hash: hashString },
|
||||
filters: ownerFilter ? { holder_address_hash: ownerFilter } : {},
|
||||
scrollRef,
|
||||
options: {
|
||||
enabled: Boolean(
|
||||
@@ -150,7 +152,7 @@ const TokenPageContent = () => {
|
||||
tab === 'inventory'
|
||||
),
|
||||
),
|
||||
placeholderData: generateListStub<'token_inventory'>(tokenStubs.TOKEN_INSTANCE, 50, { next_page_params: null }),
|
||||
placeholderData: generateListStub<'token_inventory'>(tokenStubs.TOKEN_INSTANCE, 50, { next_page_params: { unique_token: 1 } }),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -173,9 +175,11 @@ const TokenPageContent = () => {
|
||||
const contractTabs = useContractTabs(contractQuery.data);
|
||||
|
||||
const tabs: Array<RoutedTab> = [
|
||||
(tokenQuery.data?.type === 'ERC-1155' || tokenQuery.data?.type === 'ERC-721') ?
|
||||
{ id: 'inventory', title: 'Inventory', component: <TokenInventory inventoryQuery={ inventoryQuery } tokenQuery={ tokenQuery }/> } :
|
||||
undefined,
|
||||
(tokenQuery.data?.type === 'ERC-1155' || tokenQuery.data?.type === 'ERC-721') ? {
|
||||
id: 'inventory',
|
||||
title: 'Inventory',
|
||||
component: <TokenInventory inventoryQuery={ inventoryQuery } tokenQuery={ tokenQuery } ownerFilter={ ownerFilter }/>
|
||||
} : undefined,
|
||||
{ id: 'token_transfers', title: 'Token transfers', component: <TokenTransfer transfersQuery={ transfersQuery } token={ tokenQuery.data }/> },
|
||||
{ id: 'holders', title: 'Holders', component: <TokenHolders token={ tokenQuery.data } holdersQuery={ holdersQuery }/> },
|
||||
contractQuery.data?.is_contract ? {
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Tooltip, Flex, Icon, useColorModeValue } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import crossIcon from 'icons/cross.svg';
|
||||
|
||||
type Props = {
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
const ResetIconButton = ({ onClick }: Props) => {
|
||||
const resetTokenIconColor = useColorModeValue('blue.600', 'blue.300');
|
||||
const resetTokenIconHoverColor = useColorModeValue('blue.400', 'blue.200');
|
||||
|
||||
return (
|
||||
<Tooltip label="Reset filter">
|
||||
<Flex>
|
||||
<Icon
|
||||
as={ crossIcon }
|
||||
boxSize={ 5 }
|
||||
ml={ 1 }
|
||||
color={ resetTokenIconColor }
|
||||
cursor="pointer"
|
||||
_hover={{ color: resetTokenIconHoverColor }}
|
||||
onClick={ onClick }
|
||||
/>
|
||||
</Flex>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export default ResetIconButton;
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Grid } from '@chakra-ui/react';
|
||||
import { Flex, Grid, Text } from '@chakra-ui/react';
|
||||
import type { UseQueryResult } from '@tanstack/react-query';
|
||||
import React from 'react';
|
||||
|
||||
@@ -8,23 +8,50 @@ import type { ResourceError } from 'lib/api/resources';
|
||||
import useIsMobile from 'lib/hooks/useIsMobile';
|
||||
import ActionBar from 'ui/shared/ActionBar';
|
||||
import DataListDisplay from 'ui/shared/DataListDisplay';
|
||||
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
|
||||
import Pagination from 'ui/shared/pagination/Pagination';
|
||||
import type { QueryWithPagesResult } from 'ui/shared/pagination/useQueryWithPages';
|
||||
import ResetIconButton from 'ui/shared/ResetIconButton';
|
||||
|
||||
import TokenInventoryItem from './TokenInventoryItem';
|
||||
|
||||
type Props = {
|
||||
inventoryQuery: QueryWithPagesResult<'token_inventory'>;
|
||||
tokenQuery: UseQueryResult<TokenInfo, ResourceError<unknown>>;
|
||||
ownerFilter?: string;
|
||||
}
|
||||
|
||||
const TokenInventory = ({ inventoryQuery, tokenQuery }: Props) => {
|
||||
const TokenInventory = ({ inventoryQuery, tokenQuery, ownerFilter }: Props) => {
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const actionBar = isMobile && inventoryQuery.pagination.isVisible && (
|
||||
<ActionBar mt={ -6 }>
|
||||
<Pagination ml="auto" { ...inventoryQuery.pagination }/>
|
||||
</ActionBar>
|
||||
const resetOwnerFilter = React.useCallback(() => {
|
||||
inventoryQuery.onFilterChange({});
|
||||
}, [ inventoryQuery ]);
|
||||
|
||||
const isActionBarHidden = !ownerFilter && !inventoryQuery.data?.items.length;
|
||||
|
||||
const ownerFilterComponent = ownerFilter && (
|
||||
<Flex
|
||||
alignItems="center"
|
||||
flexWrap="wrap"
|
||||
mb={{ base: isActionBarHidden ? 3 : 6, lg: 3 }}
|
||||
mr={ 4 }
|
||||
>
|
||||
<Text whiteSpace="nowrap" mr={ 2 } py={ 1 }>Filtered by owner</Text>
|
||||
<Flex alignItems="center" py={ 1 }>
|
||||
<AddressEntity address={{ hash: ownerFilter }} truncation={ isMobile ? 'constant' : 'none' }/>
|
||||
<ResetIconButton onClick={ resetOwnerFilter }/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
|
||||
const actionBar = !isActionBarHidden && (
|
||||
<>
|
||||
{ ownerFilterComponent }
|
||||
<ActionBar mt={ -6 }>
|
||||
{ isMobile && <Pagination ml="auto" { ...inventoryQuery.pagination }/> }
|
||||
</ActionBar>
|
||||
</>
|
||||
);
|
||||
|
||||
const items = inventoryQuery.data?.items;
|
||||
|
||||
Reference in New Issue
Block a user