Commit de9afb2b authored by isstuev's avatar isstuev

nft address start

parent 208183b5
...@@ -26,6 +26,8 @@ import type { ...@@ -26,6 +26,8 @@ import type {
AddressTokensFilter, AddressTokensFilter,
AddressTokensResponse, AddressTokensResponse,
AddressWithdrawalsResponse, AddressWithdrawalsResponse,
AddressNFTsResponse,
AddressCollectionsResponse,
} from 'types/api/address'; } from 'types/api/address';
import type { AddressesResponse } from 'types/api/addresses'; import type { AddressesResponse } from 'types/api/addresses';
import type { BlocksResponse, BlockTransactionsResponse, Block, BlockFilters, BlockWithdrawalsResponse } from 'types/api/block'; import type { BlocksResponse, BlockTransactionsResponse, Block, BlockFilters, BlockWithdrawalsResponse } from 'types/api/block';
...@@ -51,6 +53,7 @@ import type { ...@@ -51,6 +53,7 @@ import type {
TokenInstance, TokenInstance,
TokenInstanceTransfersCount, TokenInstanceTransfersCount,
TokenVerifiedInfo, TokenVerifiedInfo,
TokenInventoryFilters,
} from 'types/api/token'; } from 'types/api/token';
import type { TokensResponse, TokensFilters, TokensSorting, TokenInstanceTransferResponse, TokensBridgedFilters } from 'types/api/tokens'; import type { TokensResponse, TokensFilters, TokensSorting, TokenInstanceTransferResponse, TokensBridgedFilters } from 'types/api/tokens';
import type { TokenTransferResponse, TokenTransferFilters } from 'types/api/tokenTransfer'; import type { TokenTransferResponse, TokenTransferFilters } from 'types/api/tokenTransfer';
...@@ -305,6 +308,16 @@ export const RESOURCES = { ...@@ -305,6 +308,16 @@ export const RESOURCES = {
pathParams: [ 'hash' as const ], pathParams: [ 'hash' as const ],
filterFields: [ 'type' 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: { address_withdrawals: {
path: '/api/v2/addresses/:hash/withdrawals', path: '/api/v2/addresses/:hash/withdrawals',
pathParams: [ 'hash' as const ], pathParams: [ 'hash' as const ],
...@@ -384,7 +397,7 @@ export const RESOURCES = { ...@@ -384,7 +397,7 @@ export const RESOURCES = {
token_inventory: { token_inventory: {
path: '/api/v2/tokens/:hash/instances', path: '/api/v2/tokens/:hash/instances',
pathParams: [ 'hash' as const ], pathParams: [ 'hash' as const ],
filterFields: [], filterFields: [ 'holder_address_hash' as const ],
}, },
tokens: { tokens: {
path: '/api/v2/tokens', path: '/api/v2/tokens',
...@@ -580,7 +593,7 @@ export type PaginatedResources = 'blocks' | 'block_txs' | ...@@ -580,7 +593,7 @@ export type PaginatedResources = 'blocks' | 'block_txs' |
'addresses' | 'addresses' |
'address_txs' | 'address_internal_txs' | 'address_token_transfers' | 'address_blocks_validated' | 'address_coin_balance' | 'address_txs' | 'address_internal_txs' | 'address_token_transfers' | 'address_blocks_validated' | 'address_coin_balance' |
'search' | 'search' |
'address_logs' | 'address_tokens' | 'address_logs' | 'address_tokens' | 'address_nfts' | 'address_collections' |
'token_transfers' | 'token_holders' | 'token_inventory' | 'tokens' | 'tokens_bridged' | 'token_transfers' | 'token_holders' | 'token_inventory' | 'tokens' | 'tokens_bridged' |
'token_instance_transfers' | 'token_instance_holders' | 'token_instance_transfers' | 'token_instance_holders' |
'verified_contracts' | 'verified_contracts' |
...@@ -642,6 +655,8 @@ Q extends 'address_coin_balance' ? AddressCoinBalanceHistoryResponse : ...@@ -642,6 +655,8 @@ Q extends 'address_coin_balance' ? AddressCoinBalanceHistoryResponse :
Q extends 'address_coin_balance_chart' ? AddressCoinBalanceHistoryChart : Q extends 'address_coin_balance_chart' ? AddressCoinBalanceHistoryChart :
Q extends 'address_logs' ? LogsResponseAddress : Q extends 'address_logs' ? LogsResponseAddress :
Q extends 'address_tokens' ? AddressTokensResponse : Q extends 'address_tokens' ? AddressTokensResponse :
Q extends 'address_nfts' ? AddressNFTsResponse :
Q extends 'address_collections' ? AddressCollectionsResponse :
Q extends 'address_withdrawals' ? AddressWithdrawalsResponse : Q extends 'address_withdrawals' ? AddressWithdrawalsResponse :
Q extends 'token' ? TokenInfo : Q extends 'token' ? TokenInfo :
Q extends 'token_verified_info' ? TokenVerifiedInfo : Q extends 'token_verified_info' ? TokenVerifiedInfo :
...@@ -696,6 +711,7 @@ Q extends 'address_txs' | 'address_internal_txs' ? AddressTxsFilters : ...@@ -696,6 +711,7 @@ Q extends 'address_txs' | 'address_internal_txs' ? AddressTxsFilters :
Q extends 'address_token_transfers' ? AddressTokenTransferFilters : Q extends 'address_token_transfers' ? AddressTokenTransferFilters :
Q extends 'address_tokens' ? AddressTokensFilter : Q extends 'address_tokens' ? AddressTokensFilter :
Q extends 'search' ? SearchResultFilters : Q extends 'search' ? SearchResultFilters :
Q extends 'token_inventory' ? TokenInventoryFilters :
Q extends 'tokens' ? TokensFilters : Q extends 'tokens' ? TokensFilters :
Q extends 'tokens_bridged' ? TokensBridgedFilters : Q extends 'tokens_bridged' ? TokensBridgedFilters :
Q extends 'verified_contracts' ? VerifiedContractsFilters : Q extends 'verified_contracts' ? VerifiedContractsFilters :
......
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 type { AddressesItem } from 'types/api/addresses';
import { ADDRESS_HASH } from './addressParams'; 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'; import { TX_HASH } from './tx';
export const ADDRESS_INFO: Address = { export const ADDRESS_INFO: Address = {
...@@ -80,16 +88,20 @@ export const ADDRESS_TOKEN_BALANCE_ERC_20: AddressTokenBalance = { ...@@ -80,16 +88,20 @@ export const ADDRESS_TOKEN_BALANCE_ERC_20: AddressTokenBalance = {
value: '1000000000000000000000000', value: '1000000000000000000000000',
}; };
export const ADDRESS_TOKEN_BALANCE_ERC_721: AddressTokenBalance = { export const ADDRESS_NFT_721: AddressNFT = {
token: TOKEN_INFO_ERC_721, token_type: 'ERC-721',
token_id: null, value: '1',
token_instance: null, ...TOKEN_INSTANCE,
value: '176', };
export const ADDRESS_NFT_1155: AddressNFT = {
token_type: 'ERC-1155',
value: '10',
...TOKEN_INSTANCE,
}; };
export const ADDRESS_TOKEN_BALANCE_ERC_1155: AddressTokenBalance = { export const ADDRESS_COLLECTION: AddressCollection = {
token: TOKEN_INFO_ERC_1155, token: TOKEN_INFO_ERC_1155,
token_id: '188882', amount: '4',
token_instance: TOKEN_INSTANCE, token_instances: Array(4).fill(TOKEN_INSTANCE),
value: '176',
}; };
...@@ -49,11 +49,43 @@ export interface AddressTokenBalance { ...@@ -49,11 +49,43 @@ export interface AddressTokenBalance {
token_instance: TokenInstance | null; 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 { export interface AddressTokensResponse {
items: Array<AddressTokenBalance>; items: Array<AddressTokenBalance>;
next_page_params: { next_page_params: {
items_count: number; 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; token_type: TokenType;
value: number; value: number;
fiat_value: string | null; fiat_value: string | null;
......
...@@ -77,3 +77,7 @@ export type TokenInventoryPagination = { ...@@ -77,3 +77,7 @@ export type TokenInventoryPagination = {
} }
export type TokenVerifiedInfo = Omit<TokenInfoApplication, 'id' | 'status'>; export type TokenVerifiedInfo = Omit<TokenInfoApplication, 'id' | 'status'>;
export type TokenInventoryFilters = {
holder_address_hash?: string;
}
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 { useQueryClient } from '@tanstack/react-query';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
...@@ -9,7 +9,6 @@ import type { AddressFromToFilter, AddressTokenTransferResponse } from 'types/ap ...@@ -9,7 +9,6 @@ import type { AddressFromToFilter, AddressTokenTransferResponse } from 'types/ap
import type { TokenType } from 'types/api/token'; import type { TokenType } from 'types/api/token';
import type { TokenTransfer } from 'types/api/tokenTransfer'; import type { TokenTransfer } from 'types/api/tokenTransfer';
import crossIcon from 'icons/cross.svg';
import { getResourceKey } from 'lib/api/useApiQuery'; import { getResourceKey } from 'lib/api/useApiQuery';
import getFilterValueFromQuery from 'lib/getFilterValueFromQuery'; import getFilterValueFromQuery from 'lib/getFilterValueFromQuery';
import getFilterValuesFromQuery from 'lib/getFilterValuesFromQuery'; import getFilterValuesFromQuery from 'lib/getFilterValuesFromQuery';
...@@ -26,6 +25,7 @@ import * as TokenEntity from 'ui/shared/entities/token/TokenEntity'; ...@@ -26,6 +25,7 @@ import * as TokenEntity from 'ui/shared/entities/token/TokenEntity';
import HashStringShorten from 'ui/shared/HashStringShorten'; import HashStringShorten from 'ui/shared/HashStringShorten';
import Pagination from 'ui/shared/pagination/Pagination'; import Pagination from 'ui/shared/pagination/Pagination';
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages'; import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
import ResetIconButton from 'ui/shared/ResetIconButton';
import * as SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice'; import * as SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice';
import TokenTransferFilter from 'ui/shared/TokenTransfer/TokenTransferFilter'; import TokenTransferFilter from 'ui/shared/TokenTransfer/TokenTransferFilter';
import TokenTransferList from 'ui/shared/TokenTransfer/TokenTransferList'; import TokenTransferList from 'ui/shared/TokenTransfer/TokenTransferList';
...@@ -115,9 +115,6 @@ const AddressTokenTransfers = ({ scrollRef, overloadCount = OVERLOAD_COUNT }: Pr ...@@ -115,9 +115,6 @@ const AddressTokenTransfers = ({ scrollRef, overloadCount = OVERLOAD_COUNT }: Pr
onFilterChange({}); onFilterChange({});
}, [ onFilterChange ]); }, [ onFilterChange ]);
const resetTokenIconColor = useColorModeValue('blue.600', 'blue.300');
const resetTokenIconHoverColor = useColorModeValue('blue.400', 'blue.200');
const handleNewSocketMessage: SocketMessage.AddressTokenTransfer['handler'] = (payload) => { const handleNewSocketMessage: SocketMessage.AddressTokenTransfer['handler'] = (payload) => {
setSocketAlert(''); setSocketAlert('');
...@@ -235,19 +232,7 @@ const AddressTokenTransfers = ({ scrollRef, overloadCount = OVERLOAD_COUNT }: Pr ...@@ -235,19 +232,7 @@ const AddressTokenTransfers = ({ scrollRef, overloadCount = OVERLOAD_COUNT }: Pr
<Flex alignItems="center" py={ 1 }> <Flex alignItems="center" py={ 1 }>
<TokenEntity.Icon token={ tokenData } isLoading={ isPlaceholderData }/> <TokenEntity.Icon token={ tokenData } isLoading={ isPlaceholderData }/>
{ isMobile ? <HashStringShorten hash={ tokenFilter }/> : tokenFilter } { isMobile ? <HashStringShorten hash={ tokenFilter }/> : tokenFilter }
<Tooltip label="Reset filter"> <ResetIconButton onClick={ resetTokenFilter }/>
<Flex>
<Icon
as={ crossIcon }
boxSize={ 5 }
ml={ 1 }
color={ resetTokenIconColor }
cursor="pointer"
_hover={{ color: resetTokenIconHoverColor }}
onClick={ resetTokenFilter }
/>
</Flex>
</Tooltip>
</Flex> </Flex>
</Flex> </Flex>
); );
......
import { Box } from '@chakra-ui/react'; import { Box, Radio, RadioGroup } from '@chakra-ui/react';
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
...@@ -13,18 +13,19 @@ import useIsMobile from 'lib/hooks/useIsMobile'; ...@@ -13,18 +13,19 @@ import useIsMobile from 'lib/hooks/useIsMobile';
import getQueryParamString from 'lib/router/getQueryParamString'; import getQueryParamString from 'lib/router/getQueryParamString';
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 { 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 { generateListStub } from 'stubs/utils';
import { tokenTabsByType } from 'ui/pages/Address';
import Pagination from 'ui/shared/pagination/Pagination'; import Pagination from 'ui/shared/pagination/Pagination';
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages'; import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
import RoutedTabs from 'ui/shared/Tabs/RoutedTabs'; 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 ERC20Tokens from './tokens/ERC20Tokens';
import ERC721Tokens from './tokens/ERC721Tokens';
import TokenBalances from './tokens/TokenBalances'; import TokenBalances from './tokens/TokenBalances';
type TNftDisplayType = 'collection' | 'list';
const TAB_LIST_PROPS = { const TAB_LIST_PROPS = {
marginBottom: 0, marginBottom: 0,
py: 5, py: 5,
...@@ -49,6 +50,8 @@ const AddressTokens = () => { ...@@ -49,6 +50,8 @@ const AddressTokens = () => {
const scrollRef = React.useRef<HTMLDivElement>(null); const scrollRef = React.useRef<HTMLDivElement>(null);
const [ nftDisplayType, setNftDisplayType ] = React.useState<TNftDisplayType>('collection');
const tab = getQueryParamString(router.query.tab); const tab = getQueryParamString(router.query.tab);
const hash = getQueryParamString(router.query.hash); const hash = getQueryParamString(router.query.hash);
...@@ -58,30 +61,31 @@ const AddressTokens = () => { ...@@ -58,30 +61,31 @@ const AddressTokens = () => {
filters: { type: 'ERC-20' }, filters: { type: 'ERC-20' },
scrollRef, scrollRef,
options: { options: {
enabled: !tab || tab === 'tokens_erc20',
refetchOnMount: false, refetchOnMount: false,
placeholderData: generateListStub<'address_tokens'>(ADDRESS_TOKEN_BALANCE_ERC_20, 10, { next_page_params: null }), placeholderData: generateListStub<'address_tokens'>(ADDRESS_TOKEN_BALANCE_ERC_20, 10, { next_page_params: null }),
}, },
}); });
const erc721Query = useQueryWithPages({ const collectionsQuery = useQueryWithPages({
resourceName: 'address_tokens', resourceName: 'address_collections',
pathParams: { hash }, pathParams: { hash },
filters: { type: 'ERC-721' },
scrollRef, scrollRef,
options: { options: {
enabled: tab === 'tokens_nfts' && nftDisplayType === 'collection',
refetchOnMount: false, 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({ const nftsQuery = useQueryWithPages({
resourceName: 'address_tokens', resourceName: 'address_nfts',
pathParams: { hash }, pathParams: { hash },
filters: { type: 'ERC-1155' },
scrollRef, scrollRef,
options: { options: {
enabled: tab === 'tokens_nfts' && nftDisplayType === 'list',
refetchOnMount: false, 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 = () => { ...@@ -128,7 +132,8 @@ const AddressTokens = () => {
const channel = useSocketChannel({ const channel = useSocketChannel({
topic: `addresses:${ hash.toLowerCase() }`, topic: `addresses:${ hash.toLowerCase() }`,
isDisabled: erc20Query.isPlaceholderData || erc721Query.isPlaceholderData || erc1155Query.isPlaceholderData, // !!!
isDisabled: erc20Query.isPlaceholderData || nftsQuery.isPlaceholderData || collectionsQuery.isPlaceholderData,
}); });
useSocketMessage({ useSocketMessage({
...@@ -147,22 +152,43 @@ const AddressTokens = () => { ...@@ -147,22 +152,43 @@ const AddressTokens = () => {
handler: handleTokenBalancesErc1155Message, handler: handleTokenBalancesErc1155Message,
}); });
const handleNFTsDisplayTypeChange = React.useCallback((val: TNftDisplayType) => {
setNftDisplayType(val);
}, []);
const tabs = [ const tabs = [
{ id: tokenTabsByType['ERC-20'], title: 'ERC-20', component: <ERC20Tokens tokensQuery={ erc20Query }/> }, { id: 'tokens_erc20', 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_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; let pagination: PaginationParams | undefined;
if (tab === tokenTabsByType['ERC-1155']) { if (tab === 'tokens_nfts') {
pagination = erc1155Query.pagination; pagination = nftDisplayType === 'list' ? nftsQuery.pagination : collectionsQuery.pagination;
} else if (tab === tokenTabsByType['ERC-721']) {
pagination = erc721Query.pagination;
} else { } else {
pagination = erc20Query.pagination; pagination = erc20Query.pagination;
} }
const rightSlot = (
<>
{ tab !== 'tokens_erc20' && nftDisplayTypeRadio }
{ pagination.isVisible && !isMobile && <Pagination { ...pagination }/> }
</>
);
return ( return (
<> <>
<TokenBalances/> <TokenBalances/>
...@@ -174,7 +200,7 @@ const AddressTokens = () => { ...@@ -174,7 +200,7 @@ const AddressTokens = () => {
colorScheme="gray" colorScheme="gray"
size="sm" size="sm"
tabListProps={ isMobile ? TAB_LIST_PROPS_MOBILE : TAB_LIST_PROPS } tabListProps={ isMobile ? TAB_LIST_PROPS_MOBILE : TAB_LIST_PROPS }
rightSlot={ pagination.isVisible && !isMobile ? <Pagination { ...pagination }/> : null } rightSlot={ rightSlot }
stickyEnabled={ !isMobile } stickyEnabled={ !isMobile }
/> />
</> </>
......
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 ...@@ -10,10 +10,10 @@ import type { QueryWithPagesResult } from 'ui/shared/pagination/useQueryWithPage
import NFTItem from './NFTItem'; import NFTItem from './NFTItem';
type Props = { type Props = {
tokensQuery: QueryWithPagesResult<'address_tokens'>; tokensQuery: QueryWithPagesResult<'address_nfts'>;
} }
const ERC1155Tokens = ({ tokensQuery }: Props) => { const AddressNFTs = ({ tokensQuery }: Props) => {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const { isError, isPlaceholderData, data, pagination } = tokensQuery; const { isError, isPlaceholderData, data, pagination } = tokensQuery;
...@@ -32,13 +32,14 @@ const ERC1155Tokens = ({ tokensQuery }: Props) => { ...@@ -32,13 +32,14 @@ const ERC1155Tokens = ({ tokensQuery }: Props) => {
gridTemplateColumns={{ base: 'repeat(2, calc((100% - 12px)/2))', lg: 'repeat(auto-fill, minmax(210px, 1fr))' }} gridTemplateColumns={{ base: 'repeat(2, calc((100% - 12px)/2))', lg: 'repeat(auto-fill, minmax(210px, 1fr))' }}
> >
{ data.items.map((item, index) => { { 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 ( return (
<NFTItem <NFTItem
key={ key } key={ key }
{ ...item } { ...item }
isLoading={ isPlaceholderData } isLoading={ isPlaceholderData }
withTokenLink
/> />
); );
}) } }) }
...@@ -56,4 +57,4 @@ const ERC1155Tokens = ({ tokensQuery }: Props) => { ...@@ -56,4 +57,4 @@ const ERC1155Tokens = ({ tokensQuery }: Props) => {
); );
}; };
export default ERC1155Tokens; export default AddressNFTs;
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;
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;
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;
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);
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 React from 'react';
import type { AddressTokenBalance } from 'types/api/address'; import type { AddressNFT } from 'types/api/address';
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
...@@ -9,22 +9,20 @@ import NftEntity from 'ui/shared/entities/nft/NftEntity'; ...@@ -9,22 +9,20 @@ import NftEntity from 'ui/shared/entities/nft/NftEntity';
import TokenEntity from 'ui/shared/entities/token/TokenEntity'; import TokenEntity from 'ui/shared/entities/token/TokenEntity';
import NftMedia from 'ui/shared/nft/NftMedia'; 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) => { type Props = AddressNFT & { isLoading: boolean; withTokenLink?: boolean };
const tokenInstanceLink = tokenId ? route({ pathname: '/token/[hash]/instance/[id]', query: { hash: token.address, id: tokenId } }) : undefined;
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 ( return (
<Box <NFTItemContainer position="relative">
w={{ base: '100%', lg: '210px' }} <Skeleton isLoaded={ !isLoading }>
border="1px solid" <LightMode><Tag background="gray.50" zIndex={ 1 } position="absolute" top="18px" right="18px">{ token.type }</Tag></LightMode>
borderColor={ useColorModeValue('blackAlpha.100', 'whiteAlpha.200') } </Skeleton>
borderRadius="12px"
p="10px"
fontSize="sm"
fontWeight={ 500 }
lineHeight="20px"
>
<Link href={ isLoading ? undefined : tokenInstanceLink }> <Link href={ isLoading ? undefined : tokenInstanceLink }>
<NftMedia <NftMedia
mb="18px" mb="18px"
...@@ -32,19 +30,25 @@ const NFTItem = ({ token, token_id: tokenId, token_instance: tokenInstance, isLo ...@@ -32,19 +30,25 @@ const NFTItem = ({ token, token_id: tokenId, token_instance: tokenInstance, isLo
isLoading={ isLoading } isLoading={ isLoading }
/> />
</Link> </Link>
{ tokenId && ( <Flex justifyContent="space-between" w="100%">
<Flex mb={ 2 } ml={ 1 }> <Flex ml={ 1 }>
<Text whiteSpace="pre" variant="secondary">ID# </Text> <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> </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 </NFTItemContainer>
token={ token }
isLoading={ isLoading }
noCopy
noSymbol
/>
</Box>
); );
}; };
......
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'; ...@@ -2,7 +2,6 @@ import { Box, Flex, HStack, Icon } from '@chakra-ui/react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import type { TokenType } from 'types/api/token';
import type { RoutedTab } from 'ui/shared/Tabs/types'; import type { RoutedTab } from 'ui/shared/Tabs/types';
import config from 'configs/app'; import config from 'configs/app';
...@@ -36,13 +35,7 @@ import PageTitle from 'ui/shared/Page/PageTitle'; ...@@ -36,13 +35,7 @@ import PageTitle from 'ui/shared/Page/PageTitle';
import RoutedTabs from 'ui/shared/Tabs/RoutedTabs'; import RoutedTabs from 'ui/shared/Tabs/RoutedTabs';
import TabsSkeleton from 'ui/shared/Tabs/TabsSkeleton'; import TabsSkeleton from 'ui/shared/Tabs/TabsSkeleton';
export const tokenTabsByType: Record<TokenType, string> = { const TOKEN_TABS = [ 'tokens_erc20', 'tokens_nfts', 'tokens_nfts_collection', 'tokens_nfts_list' ];
'ERC-20': 'tokens_erc20',
'ERC-721': 'tokens_erc721',
'ERC-1155': 'tokens_erc1155',
} as const;
const TOKEN_TABS = Object.values(tokenTabsByType);
const AddressPageContent = () => { const AddressPageContent = () => {
const router = useRouter(); const router = useRouter();
......
...@@ -56,6 +56,7 @@ const TokenPageContent = () => { ...@@ -56,6 +56,7 @@ const TokenPageContent = () => {
const hashString = getQueryParamString(router.query.hash); const hashString = getQueryParamString(router.query.hash);
const tab = getQueryParamString(router.query.tab); const tab = getQueryParamString(router.query.tab);
const ownerFilter = getQueryParamString(router.query.holder_address_hash) || undefined;
const queryClient = useQueryClient(); const queryClient = useQueryClient();
...@@ -140,6 +141,7 @@ const TokenPageContent = () => { ...@@ -140,6 +141,7 @@ const TokenPageContent = () => {
const inventoryQuery = useQueryWithPages({ const inventoryQuery = useQueryWithPages({
resourceName: 'token_inventory', resourceName: 'token_inventory',
pathParams: { hash: hashString }, pathParams: { hash: hashString },
filters: ownerFilter ? { holder_address_hash: ownerFilter } : {},
scrollRef, scrollRef,
options: { options: {
enabled: Boolean( enabled: Boolean(
...@@ -150,7 +152,7 @@ const TokenPageContent = () => { ...@@ -150,7 +152,7 @@ const TokenPageContent = () => {
tab === 'inventory' 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 = () => { ...@@ -173,9 +175,11 @@ const TokenPageContent = () => {
const contractTabs = useContractTabs(contractQuery.data); const contractTabs = useContractTabs(contractQuery.data);
const tabs: Array<RoutedTab> = [ const tabs: Array<RoutedTab> = [
(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 } tokenQuery={ tokenQuery }/> } : id: 'inventory',
undefined, 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: 'token_transfers', title: 'Token transfers', component: <TokenTransfer transfersQuery={ transfersQuery } token={ tokenQuery.data }/> },
{ id: 'holders', title: 'Holders', component: <TokenHolders token={ tokenQuery.data } holdersQuery={ holdersQuery }/> }, { id: 'holders', title: 'Holders', component: <TokenHolders token={ tokenQuery.data } holdersQuery={ holdersQuery }/> },
contractQuery.data?.is_contract ? { contractQuery.data?.is_contract ? {
......
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;
import { Grid } from '@chakra-ui/react'; import { Flex, Grid, Text } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query'; import type { UseQueryResult } from '@tanstack/react-query';
import React from 'react'; import React from 'react';
...@@ -8,23 +8,50 @@ import type { ResourceError } from 'lib/api/resources'; ...@@ -8,23 +8,50 @@ import type { ResourceError } from 'lib/api/resources';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import ActionBar from 'ui/shared/ActionBar'; import ActionBar from 'ui/shared/ActionBar';
import DataListDisplay from 'ui/shared/DataListDisplay'; import DataListDisplay from 'ui/shared/DataListDisplay';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import Pagination from 'ui/shared/pagination/Pagination'; import Pagination from 'ui/shared/pagination/Pagination';
import type { QueryWithPagesResult } from 'ui/shared/pagination/useQueryWithPages'; import type { QueryWithPagesResult } from 'ui/shared/pagination/useQueryWithPages';
import ResetIconButton from 'ui/shared/ResetIconButton';
import TokenInventoryItem from './TokenInventoryItem'; import TokenInventoryItem from './TokenInventoryItem';
type Props = { type Props = {
inventoryQuery: QueryWithPagesResult<'token_inventory'>; inventoryQuery: QueryWithPagesResult<'token_inventory'>;
tokenQuery: UseQueryResult<TokenInfo, ResourceError<unknown>>; tokenQuery: UseQueryResult<TokenInfo, ResourceError<unknown>>;
ownerFilter?: string;
} }
const TokenInventory = ({ inventoryQuery, tokenQuery }: Props) => { const TokenInventory = ({ inventoryQuery, tokenQuery, ownerFilter }: Props) => {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const actionBar = isMobile && inventoryQuery.pagination.isVisible && ( const resetOwnerFilter = React.useCallback(() => {
<ActionBar mt={ -6 }> inventoryQuery.onFilterChange({});
<Pagination ml="auto" { ...inventoryQuery.pagination }/> }, [ inventoryQuery ]);
</ActionBar>
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; const items = inventoryQuery.data?.items;
......
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