Commit 6aae1e3d authored by Igor Stuev's avatar Igor Stuev Committed by GitHub

Merge pull request #588 from blockscout/inventory

inventory
parents 68193e1b 04bef2e8
...@@ -24,8 +24,8 @@ import type { LogsResponseTx, LogsResponseAddress } from 'types/api/log'; ...@@ -24,8 +24,8 @@ import type { LogsResponseTx, LogsResponseAddress } from 'types/api/log';
import type { RawTracesResponse } from 'types/api/rawTrace'; import type { RawTracesResponse } from 'types/api/rawTrace';
import type { SearchResult, SearchResultFilters } from 'types/api/search'; import type { SearchResult, SearchResultFilters } from 'types/api/search';
import type { Counters, StatsCharts, StatsChart, HomeStats } from 'types/api/stats'; import type { Counters, StatsCharts, StatsChart, HomeStats } from 'types/api/stats';
import type { TokenCounters, TokenInfo, TokenHolders } from 'types/api/tokenInfo'; import type { TokenCounters, TokenInfo, TokenHolders, TokenInventoryResponse, TokenInstance, TokenInstanceTransfersCount } from 'types/api/token';
import type { TokensResponse, TokensFilters, TokenInstance, TokenInstanceTransfersCount } from 'types/api/tokens'; import type { TokensResponse, TokensFilters } from 'types/api/tokens';
import type { TokenTransferResponse, TokenTransferFilters } from 'types/api/tokenTransfer'; import type { TokenTransferResponse, TokenTransferFilters } from 'types/api/tokenTransfer';
import type { TransactionsResponseValidated, TransactionsResponsePending, Transaction } from 'types/api/transaction'; import type { TransactionsResponseValidated, TransactionsResponsePending, Transaction } from 'types/api/transaction';
import type { TTxsFilters } from 'types/api/txsFilters'; import type { TTxsFilters } from 'types/api/txsFilters';
...@@ -230,6 +230,11 @@ export const RESOURCES = { ...@@ -230,6 +230,11 @@ export const RESOURCES = {
paginationFields: [ 'block_number' as const, 'items_count' as const, 'index' as const ], paginationFields: [ 'block_number' as const, 'items_count' as const, 'index' as const ],
filterFields: [], filterFields: [],
}, },
token_inventory: {
path: '/api/v2/tokens/:hash/instances',
paginationFields: [ 'unique_token' as const ],
filterFields: [],
},
tokens: { tokens: {
path: '/api/v2/tokens', path: '/api/v2/tokens',
paginationFields: [ 'holder_count' as const, 'items_count' as const, 'name' as const ], paginationFields: [ 'holder_count' as const, 'items_count' as const, 'name' as const ],
...@@ -317,7 +322,7 @@ export type PaginatedResources = 'blocks' | 'block_txs' | ...@@ -317,7 +322,7 @@ export type PaginatedResources = 'blocks' | 'block_txs' |
'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' |
'token_transfers' | 'token_holders' | 'tokens'; 'token_transfers' | 'token_holders' | 'token_inventory' | 'tokens';
export type PaginatedResponse<Q extends PaginatedResources> = ResourcePayload<Q>; export type PaginatedResponse<Q extends PaginatedResources> = ResourcePayload<Q>;
...@@ -367,6 +372,7 @@ Q extends 'token_transfers' ? TokenTransferResponse : ...@@ -367,6 +372,7 @@ Q extends 'token_transfers' ? TokenTransferResponse :
Q extends 'token_holders' ? TokenHolders : 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_inventory' ? TokenInventoryResponse :
Q extends 'tokens' ? TokensResponse : Q extends 'tokens' ? TokensResponse :
Q extends 'search' ? SearchResult : Q extends 'search' ? SearchResult :
Q extends 'contract' ? SmartContract : Q extends 'contract' ? SmartContract :
......
import type { TokenType } from 'types/api/tokenInfo'; import type { TokenType } from 'types/api/token';
const TOKEN_TYPE: Array<{ title: string; id: TokenType }> = [ const TOKEN_TYPE: Array<{ title: string; id: TokenType }> = [
{ title: 'ERC-20', id: 'ERC-20' }, { title: 'ERC-20', id: 'ERC-20' },
......
import type { TokenHolders } from 'types/api/tokenInfo'; import type { TokenHolders } from 'types/api/token';
import { withName, withoutName } from 'mocks/address/address'; import { withName, withoutName } from 'mocks/address/address';
......
import type { TokenCounters, TokenInfo } from 'types/api/tokenInfo'; import type { TokenCounters, TokenInfo } from 'types/api/token';
export const tokenInfo: TokenInfo = { export const tokenInfo: TokenInfo = {
address: '0x55d536e4d6c1993d8ef2e2a4ef77f02088419420', address: '0x55d536e4d6c1993d8ef2e2a4ef77f02088419420',
......
import type { TokenInstance } from 'types/api/tokens'; import type { TokenInstance } from 'types/api/token';
import * as addressMock from '../address/address'; import * as addressMock from '../address/address';
import { tokenInfoERC721a } from './tokenInfo'; import { tokenInfoERC721a } from './tokenInfo';
......
...@@ -3,7 +3,7 @@ import type { Transaction } from 'types/api/transaction'; ...@@ -3,7 +3,7 @@ import type { Transaction } from 'types/api/transaction';
import type { AddressTag, WatchlistName } from './addressParams'; import type { AddressTag, WatchlistName } from './addressParams';
import type { Block } from './block'; import type { Block } from './block';
import type { InternalTransaction } from './internalTransaction'; import type { InternalTransaction } from './internalTransaction';
import type { TokenInfo, TokenType } from './tokenInfo'; import type { TokenInfo, TokenType } from './token';
import type { TokenTransfer, TokenTransferPagination } from './tokenTransfer'; import type { TokenTransfer, TokenTransferPagination } from './tokenTransfer';
export interface Address { export interface Address {
......
...@@ -34,3 +34,28 @@ export type TokenHoldersPagination = { ...@@ -34,3 +34,28 @@ export type TokenHoldersPagination = {
items_count: number; items_count: number;
value: string; value: string;
} }
export interface TokenInstance {
is_unique: boolean;
id: string;
holder_address_hash: string | null;
image_url: string | null;
animation_url: string | null;
external_app_url: string | null;
metadata: unknown;
owner: AddressParam;
token: TokenInfo;
}
export interface TokenInstanceTransfersCount {
transfers_count: number;
}
export interface TokenInventoryResponse {
items: Array<TokenInstance>;
next_page_params: TokenInventoryPagination;
}
export type TokenInventoryPagination = {
unique_token: number;
}
import type { AddressParam } from './addressParams'; import type { AddressParam } from './addressParams';
import type { TokenInfoGeneric, TokenType } from './tokenInfo'; import type { TokenInfoGeneric, TokenType } from './token';
export type Erc20TotalPayload = { export type Erc20TotalPayload = {
decimals: string | null; decimals: string | null;
......
import type { AddressParam } from './addressParams'; import type { TokenInfo, TokenType } from './token';
import type { TokenInfo, TokenType } from './tokenInfo';
export type TokensResponse = { export type TokensResponse = {
items: Array<TokenInfo>; items: Array<TokenInfo>;
...@@ -11,19 +10,3 @@ export type TokensResponse = { ...@@ -11,19 +10,3 @@ export type TokensResponse = {
} }
export type TokensFilters = { filter: string; type: Array<TokenType> | undefined }; export type TokensFilters = { filter: string; type: Array<TokenType> | undefined };
export interface TokenInstance {
is_unique: boolean;
id: string;
holder_address_hash: string | null;
image_url: string | null;
animation_url: string | null;
external_app_url: string | null;
metadata: unknown;
owner: AddressParam;
token: TokenInfo;
}
export interface TokenInstanceTransfersCount {
transfers_count: number;
}
...@@ -6,7 +6,7 @@ import React from 'react'; ...@@ -6,7 +6,7 @@ import React from 'react';
import type { SocketMessage } from 'lib/socket/types'; import type { SocketMessage } from 'lib/socket/types';
import { AddressFromToFilterValues } from 'types/api/address'; import { AddressFromToFilterValues } from 'types/api/address';
import type { AddressFromToFilter, AddressTokenTransferResponse } from 'types/api/address'; import type { AddressFromToFilter, AddressTokenTransferResponse } from 'types/api/address';
import type { TokenType } from 'types/api/tokenInfo'; 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 crossIcon from 'icons/cross.svg';
......
...@@ -2,7 +2,7 @@ import { Box } from '@chakra-ui/react'; ...@@ -2,7 +2,7 @@ import { Box } 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/tokenInfo'; import type { TokenType } from 'types/api/token';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import useQueryWithPages from 'lib/hooks/useQueryWithPages'; import useQueryWithPages from 'lib/hooks/useQueryWithPages';
......
import { Box, chakra, Icon, Tooltip } from '@chakra-ui/react'; import { Box, chakra, Icon, Tooltip } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { TokenInfo } from 'types/api/tokenInfo'; import type { TokenInfo } from 'types/api/token';
import metamaskIcon from 'icons/metamask.svg'; import metamaskIcon from 'icons/metamask.svg';
import useToast from 'lib/hooks/useToast'; import useToast from 'lib/hooks/useToast';
......
...@@ -3,7 +3,7 @@ import type { Dictionary } from 'lodash'; ...@@ -3,7 +3,7 @@ import type { Dictionary } from 'lodash';
import type { ChangeEvent } from 'react'; import type { ChangeEvent } from 'react';
import React from 'react'; import React from 'react';
import type { TokenType } from 'types/api/tokenInfo'; import type { TokenType } from 'types/api/token';
import arrowIcon from 'icons/arrows/east.svg'; import arrowIcon from 'icons/arrows/east.svg';
import searchIcon from 'icons/search.svg'; import searchIcon from 'icons/search.svg';
......
...@@ -15,7 +15,7 @@ const NFTItem = ({ token, token_id: tokenId }: Props) => { ...@@ -15,7 +15,7 @@ const NFTItem = ({ token, token_id: tokenId }: Props) => {
return ( return (
<LinkBox <LinkBox
w={{ base: 'calc((100% - 12px)/2)', lg: '210px' }} w={{ base: '100%', lg: '210px' }}
border="1px solid" border="1px solid"
borderColor={ useColorModeValue('blackAlpha.100', 'whiteAlpha.200') } borderColor={ useColorModeValue('blackAlpha.100', 'whiteAlpha.200') }
borderRadius="12px" borderRadius="12px"
...@@ -25,13 +25,14 @@ const NFTItem = ({ token, token_id: tokenId }: Props) => { ...@@ -25,13 +25,14 @@ const NFTItem = ({ token, token_id: tokenId }: Props) => {
fontWeight={ 500 } fontWeight={ 500 }
lineHeight="20px" lineHeight="20px"
> >
<LinkOverlay href={ tokenLink }/> <LinkOverlay href={ tokenLink }>
<NftImage <NftImage
mb="18px" mb="18px"
url={ null } url={ null }
fallbackPadding="30px" fallbackPadding="30px"
cursor="pointer" cursor="pointer"
/> />
</LinkOverlay>
{ tokenId && ( { tokenId && (
<Flex mb={ 2 } ml={ 1 }> <Flex mb={ 2 } ml={ 1 }>
<Text whiteSpace="pre" variant="secondary">ID# </Text> <Text whiteSpace="pre" variant="secondary">ID# </Text>
......
import { Flex, Skeleton, Text } from '@chakra-ui/react'; import { Grid, Skeleton, 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';
...@@ -38,11 +38,18 @@ const TokensWithIds = ({ tokensQuery }: Props) => { ...@@ -38,11 +38,18 @@ const TokensWithIds = ({ tokensQuery }: Props) => {
return ( return (
<> <>
{ bar } { bar }
<Flex columnGap={ 6 } rowGap={ 6 } flexWrap="wrap"> <Grid
<Skeleton w="210px" h="272px"/> w="100%"
<Skeleton w="210px" h="272px"/> columnGap={{ base: 3, lg: 6 }}
<Skeleton w="210px" h="272px"/> rowGap={{ base: 3, lg: 6 }}
</Flex> gridTemplateColumns={{ base: 'repeat(2, calc((100% - 12px)/2))', lg: 'repeat(auto-fill, minmax(210px, 1fr))' }}
>
<Skeleton w={{ base: '100%', lg: '210px' }} h="272px"/>
<Skeleton w={{ base: '100%', lg: '210px' }} h="272px"/>
<Skeleton w={{ base: '100%', lg: '210px' }} h="272px"/>
<Skeleton w={{ base: '100%', lg: '210px' }} h="272px"/>
<Skeleton w={{ base: '100%', lg: '210px' }} h="272px"/>
</Grid>
</> </>
); );
} }
...@@ -54,9 +61,14 @@ const TokensWithIds = ({ tokensQuery }: Props) => { ...@@ -54,9 +61,14 @@ const TokensWithIds = ({ tokensQuery }: Props) => {
return ( return (
<> <>
{ bar } { bar }
<Flex columnGap={{ base: 3, lg: 6 }} rowGap={{ base: 3, lg: 6 }} flexWrap="wrap"> <Grid
w="100%"
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))' }}
>
{ data.items.map(item => <NFTItem key={ item.token.address } { ...item }/>) } { data.items.map(item => <NFTItem key={ item.token.address } { ...item }/>) }
</Flex> </Grid>
</> </>
); );
}; };
......
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import type { AddressTokenBalance } from 'types/api/address'; import type { AddressTokenBalance } from 'types/api/address';
import type { TokenType } from 'types/api/tokenInfo'; import type { TokenType } from 'types/api/token';
import { ZERO } from 'lib/consts'; import { ZERO } from 'lib/consts';
......
...@@ -2,7 +2,7 @@ import { Flex, Skeleton, Tag, Box, Icon } from '@chakra-ui/react'; ...@@ -2,7 +2,7 @@ import { Flex, Skeleton, Tag, Box, 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/tokenInfo'; import type { TokenType } from 'types/api/token';
import type { RoutedTab } from 'ui/shared/RoutedTabs/types'; import type { RoutedTab } from 'ui/shared/RoutedTabs/types';
import iconSuccess from 'icons/status/success.svg'; import iconSuccess from 'icons/status/success.svg';
......
...@@ -16,13 +16,15 @@ import PageTitle from 'ui/shared/Page/PageTitle'; ...@@ -16,13 +16,15 @@ import PageTitle from 'ui/shared/Page/PageTitle';
import type { Props as PaginationProps } from 'ui/shared/Pagination'; import type { Props as PaginationProps } from 'ui/shared/Pagination';
import Pagination from 'ui/shared/Pagination'; import Pagination 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 TokenLogo from 'ui/shared/TokenLogo'; import TokenLogo from 'ui/shared/TokenLogo';
import TokenContractInfo from 'ui/token/TokenContractInfo'; import TokenContractInfo from 'ui/token/TokenContractInfo';
import TokenDetails from 'ui/token/TokenDetails'; import TokenDetails from 'ui/token/TokenDetails';
import TokenHolders from 'ui/token/TokenHolders/TokenHolders'; import TokenHolders from 'ui/token/TokenHolders/TokenHolders';
import TokenInventory from 'ui/token/TokenInventory';
import TokenTransfer from 'ui/token/TokenTransfer/TokenTransfer'; import TokenTransfer from 'ui/token/TokenTransfer/TokenTransfer';
export type TokenTabs = 'token_transfers' | 'holders' export type TokenTabs = 'token_transfers' | 'holders' | 'inventory';
const TokenPageContent = () => { const TokenPageContent = () => {
const router = useRouter(); const router = useRouter();
...@@ -72,11 +74,24 @@ const TokenPageContent = () => { ...@@ -72,11 +74,24 @@ const TokenPageContent = () => {
}, },
}); });
const inventoryQuery = useQueryWithPages({
resourceName: 'token_inventory',
pathParams: { hash: router.query.hash?.toString() },
scrollRef,
options: {
enabled: Boolean(router.query.hash && router.query.tab === 'inventory' && tokenQuery.data),
},
});
const tabs: Array<RoutedTab> = [ const tabs: Array<RoutedTab> = [
{ 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 tokenQuery={ tokenQuery } holdersQuery={ holdersQuery }/> }, { id: 'holders', title: 'Holders', component: <TokenHolders tokenQuery={ tokenQuery } holdersQuery={ holdersQuery }/> },
]; ];
if (tokenQuery.data?.type === 'ERC-1155' || tokenQuery.data?.type === 'ERC-721') {
tabs.push({ id: 'inventory', title: 'Inventory', component: <TokenInventory inventoryQuery={ inventoryQuery }/> });
}
let hasPagination; let hasPagination;
let pagination; let pagination;
...@@ -90,6 +105,11 @@ const TokenPageContent = () => { ...@@ -90,6 +105,11 @@ const TokenPageContent = () => {
pagination = holdersQuery.pagination; pagination = holdersQuery.pagination;
} }
if (router.query.tab === 'inventory') {
hasPagination = inventoryQuery.isPaginationVisible;
pagination = inventoryQuery.pagination;
}
const tokenSymbolText = tokenQuery.data?.symbol ? ` (${ trimTokenSymbol(tokenQuery.data.symbol) })` : ''; const tokenSymbolText = tokenQuery.data?.symbol ? ` (${ trimTokenSymbol(tokenQuery.data.symbol) })` : '';
return ( return (
...@@ -118,12 +138,14 @@ const TokenPageContent = () => { ...@@ -118,12 +138,14 @@ const TokenPageContent = () => {
{ /* should stay before tabs to scroll up whith pagination */ } { /* should stay before tabs to scroll up whith pagination */ }
<Box ref={ scrollRef }></Box> <Box ref={ scrollRef }></Box>
<RoutedTabs { tokenQuery.isLoading ? <SkeletonTabs/> : (
tabs={ tabs } <RoutedTabs
tabListProps={ isMobile ? { mt: 8 } : { mt: 3, py: 5, marginBottom: 0 } } tabs={ tabs }
rightSlot={ !isMobile && hasPagination ? <Pagination { ...(pagination as PaginationProps) }/> : null } tabListProps={ isMobile ? { mt: 8 } : { mt: 3, py: 5, marginBottom: 0 } }
stickyEnabled={ !isMobile } rightSlot={ !isMobile && hasPagination ? <Pagination { ...(pagination as PaginationProps) }/> : null }
/> stickyEnabled={ !isMobile }
/>
) }
</Page> </Page>
); );
}; };
......
...@@ -2,7 +2,7 @@ import { Flex } from '@chakra-ui/react'; ...@@ -2,7 +2,7 @@ import { Flex } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { AddressParam } from 'types/api/addressParams'; import type { AddressParam } from 'types/api/addressParams';
import type { TokenInfo } from 'types/api/tokenInfo'; import type { TokenInfo } from 'types/api/token';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import AddressAddToMetaMask from 'ui/address/details/AddressAddToMetaMask'; import AddressAddToMetaMask from 'ui/address/details/AddressAddToMetaMask';
......
...@@ -7,7 +7,7 @@ import { ...@@ -7,7 +7,7 @@ import {
import React from 'react'; import React from 'react';
import type { AddressFromToFilter } from 'types/api/address'; import type { AddressFromToFilter } from 'types/api/address';
import type { TokenType } from 'types/api/tokenInfo'; import type { TokenType } from 'types/api/token';
import PopoverFilter from 'ui/shared/filters/PopoverFilter'; import PopoverFilter from 'ui/shared/filters/PopoverFilter';
import TokenTypeFilter from 'ui/shared/filters/TokenTypeFilter'; import TokenTypeFilter from 'ui/shared/filters/TokenTypeFilter';
......
import { CheckboxGroup, Checkbox, Text } from '@chakra-ui/react'; import { CheckboxGroup, Checkbox, Text } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { TokenType } from 'types/api/tokenInfo'; import type { TokenType } from 'types/api/token';
import TOKEN_TYPE from 'lib/token/tokenTypes'; import TOKEN_TYPE from 'lib/token/tokenTypes';
......
...@@ -3,7 +3,7 @@ import type { UseQueryResult } from '@tanstack/react-query'; ...@@ -3,7 +3,7 @@ import type { UseQueryResult } from '@tanstack/react-query';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import type { TokenInfo } from 'types/api/tokenInfo'; import type { TokenInfo } from 'types/api/token';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import AddressHeadingInfo from 'ui/shared/AddressHeadingInfo'; import AddressHeadingInfo from 'ui/shared/AddressHeadingInfo';
......
...@@ -4,7 +4,7 @@ import { useRouter } from 'next/router'; ...@@ -4,7 +4,7 @@ import { useRouter } from 'next/router';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import { scroller } from 'react-scroll'; import { scroller } from 'react-scroll';
import type { TokenInfo } from 'types/api/tokenInfo'; import type { TokenInfo } from 'types/api/token';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import getCurrencyValue from 'lib/getCurrencyValue'; import getCurrencyValue from 'lib/getCurrencyValue';
......
...@@ -2,7 +2,7 @@ import { Text } from '@chakra-ui/react'; ...@@ -2,7 +2,7 @@ import { 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';
import type { TokenHolders, TokenInfo } from 'types/api/tokenInfo'; import type { TokenHolders, TokenInfo } from 'types/api/token';
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 { Box } from '@chakra-ui/react'; import { Box } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { TokenHolder, TokenInfo } from 'types/api/tokenInfo'; import type { TokenHolder, TokenInfo } from 'types/api/token';
import TokenHoldersListItem from './TokenHoldersListItem'; import TokenHoldersListItem from './TokenHoldersListItem';
......
...@@ -2,7 +2,7 @@ import { Flex } from '@chakra-ui/react'; ...@@ -2,7 +2,7 @@ import { Flex } from '@chakra-ui/react';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import React from 'react'; import React from 'react';
import type { TokenHolder, TokenInfo } from 'types/api/tokenInfo'; import type { TokenHolder, TokenInfo } from 'types/api/token';
import Address from 'ui/shared/address/Address'; import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon'; import AddressIcon from 'ui/shared/address/AddressIcon';
......
import { Table, Tbody, Tr, Th } from '@chakra-ui/react'; import { Table, Tbody, Tr, Th } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { TokenHolder, TokenInfo } from 'types/api/tokenInfo'; import type { TokenHolder, TokenInfo } from 'types/api/token';
import { default as Thead } from 'ui/shared/TheadSticky'; import { default as Thead } from 'ui/shared/TheadSticky';
import TokenHoldersTableItem from 'ui/token/TokenHolders/TokenHoldersTableItem'; import TokenHoldersTableItem from 'ui/token/TokenHolders/TokenHoldersTableItem';
......
...@@ -2,7 +2,7 @@ import { Tr, Td } from '@chakra-ui/react'; ...@@ -2,7 +2,7 @@ import { Tr, Td } from '@chakra-ui/react';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import React from 'react'; import React from 'react';
import type { TokenHolder, TokenInfo } from 'types/api/tokenInfo'; import type { TokenHolder, TokenInfo } from 'types/api/token';
import Address from 'ui/shared/address/Address'; import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon'; import AddressIcon from 'ui/shared/address/AddressIcon';
......
import { Box } from '@chakra-ui/react';
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import { base as tokenInstanse } from 'mocks/tokens/tokenInstance';
import TestApp from 'playwright/TestApp';
import TokenInventory from './TokenInventory';
test('base view +@mobile', async({ mount }) => {
const component = await mount(
<TestApp>
<Box h={{ base: '134px', lg: 0 }}/>
<TokenInventory
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore:
inventoryQuery={{
data: {
items: [ tokenInstanse, tokenInstanse, tokenInstanse ],
next_page_params: { unique_token: 1 },
},
isPaginationVisible: true,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore:
pagination: { page: 1 },
}}
/>
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
import { Grid, Text, Skeleton } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query';
import React from 'react';
import type { TokenInventoryResponse } from 'types/api/token';
import useIsMobile from 'lib/hooks/useIsMobile';
import ActionBar from 'ui/shared/ActionBar';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import type { Props as PaginationProps } from 'ui/shared/Pagination';
import Pagination from 'ui/shared/Pagination';
import TokenInventoryItem from './TokenInventoryItem';
type Props = {
inventoryQuery: UseQueryResult<TokenInventoryResponse> & {
pagination: PaginationProps;
isPaginationVisible: boolean;
};
}
const TokenInventory = ({ inventoryQuery }: Props) => {
const isMobile = useIsMobile();
if (inventoryQuery.isError) {
return <DataFetchAlert/>;
}
const bar = isMobile && inventoryQuery.isPaginationVisible && (
<ActionBar mt={ -6 }>
<Pagination ml="auto" { ...inventoryQuery.pagination }/>
</ActionBar>
);
if (inventoryQuery.isLoading) {
return (
<>
{ bar }
<Grid
w="100%"
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))' }}
>
<Skeleton w={{ base: '100%', lg: '210px' }} h="272px"/>
<Skeleton w={{ base: '100%', lg: '210px' }} h="272px"/>
<Skeleton w={{ base: '100%', lg: '210px' }} h="272px"/>
<Skeleton w={{ base: '100%', lg: '210px' }} h="272px"/>
<Skeleton w={{ base: '100%', lg: '210px' }} h="272px"/>
</Grid>
</>
);
}
const items = inventoryQuery.data.items;
if (!items?.length) {
return <Text as="span">There are no tokens.</Text>;
}
return (
<>
{ bar }
<Grid
w="100%"
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))' }}
>
{ items.map((item) => <TokenInventoryItem key={ item.token.address } item={ item }/>) }
</Grid></>
);
};
export default TokenInventory;
import { Flex, Link, Text, LinkBox, LinkOverlay, useColorModeValue, Hide } from '@chakra-ui/react';
import React from 'react';
import type { TokenInstance } from 'types/api/token';
import link from 'lib/link/link';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import NftMedia from 'ui/shared/nft/NftMedia';
import TruncatedTextTooltip from 'ui/shared/TruncatedTextTooltip';
type Props = { item: TokenInstance };
const NFTItem = ({ item }: Props) => {
const tokenLink = link('token_instance_item', { hash: item.token.address, id: item.id });
return (
<LinkBox
w={{ base: '100%', lg: '210px' }}
border="1px solid"
borderColor={ useColorModeValue('blackAlpha.100', 'whiteAlpha.200') }
borderRadius="12px"
p="10px"
_hover={{ boxShadow: 'md' }}
fontSize="sm"
fontWeight={ 500 }
lineHeight="20px"
>
<LinkOverlay href={ tokenLink }>
<NftMedia
mb="18px"
imageUrl={ item.image_url }
animationUrl={ item.animation_url }
/>
</LinkOverlay>
{ item.id && (
<Flex mb={ 2 } ml={ 1 }>
<Text whiteSpace="pre" variant="secondary">ID# </Text>
<TruncatedTextTooltip label={ item.id }>
<Link
overflow="hidden"
whiteSpace="nowrap"
textOverflow="ellipsis"
>
{ item.id }
</Link>
</TruncatedTextTooltip>
</Flex>
) }
{ item.owner && (
<Flex mb={ 2 } ml={ 1 }>
<Text whiteSpace="pre" variant="secondary" mr={ 2 } lineHeight="24px">Owner</Text>
<Address>
<Hide below="lg" ssr={ false }><AddressIcon address={ item.owner } mr={ 1 }/></Hide>
<AddressLink hash={ item.owner.hash } alias={ item.owner.name } type="address" truncation="constant"/>
</Address>
</Flex>
) }
</LinkBox>
);
};
export default NFTItem;
...@@ -4,7 +4,7 @@ import { useRouter } from 'next/router'; ...@@ -4,7 +4,7 @@ import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import type { SocketMessage } from 'lib/socket/types'; import type { SocketMessage } from 'lib/socket/types';
import type { TokenInfo } from 'types/api/tokenInfo'; import type { TokenInfo } from 'types/api/token';
import type { TokenTransferResponse } from 'types/api/tokenTransfer'; import type { TokenTransferResponse } from 'types/api/tokenTransfer';
import useGradualIncrement from 'lib/hooks/useGradualIncrement'; import useGradualIncrement from 'lib/hooks/useGradualIncrement';
......
import { Table, Tbody, Tr, Th, Td } from '@chakra-ui/react'; import { Table, Tbody, Tr, Th, Td } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { TokenInfo } from 'types/api/tokenInfo'; import type { TokenInfo } from 'types/api/token';
import type { TokenTransfer } from 'types/api/tokenTransfer'; import type { TokenTransfer } from 'types/api/tokenTransfer';
import trimTokenSymbol from 'lib/token/trimTokenSymbol'; import trimTokenSymbol from 'lib/token/trimTokenSymbol';
......
import { Box, Flex, Grid } from '@chakra-ui/react'; import { Box, Flex, Grid } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { TokenInstance } from 'types/api/tokens'; import type { TokenInstance } from 'types/api/token';
import Address from 'ui/shared/address/Address'; import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon'; import AddressIcon from 'ui/shared/address/AddressIcon';
......
...@@ -2,7 +2,7 @@ import { Hide, HStack, Show, Text } from '@chakra-ui/react'; ...@@ -2,7 +2,7 @@ import { Hide, HStack, Show, Text } from '@chakra-ui/react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import type { TokenType } from 'types/api/tokenInfo'; import type { TokenType } from 'types/api/token';
import getFilterValuesFromQuery from 'lib/getFilterValuesFromQuery'; import getFilterValuesFromQuery from 'lib/getFilterValuesFromQuery';
import useDebounce from 'lib/hooks/useDebounce'; import useDebounce from 'lib/hooks/useDebounce';
......
import { Flex, Text, Tag, HStack, Grid, GridItem } from '@chakra-ui/react'; import { Flex, Text, Tag, HStack, Grid, GridItem } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { TokenInfo } from 'types/api/tokenInfo'; import type { TokenInfo } from 'types/api/token';
import getCurrencyValue from 'lib/getCurrencyValue'; import getCurrencyValue from 'lib/getCurrencyValue';
import AddressAddToMetaMask from 'ui/address/details/AddressAddToMetaMask'; import AddressAddToMetaMask from 'ui/address/details/AddressAddToMetaMask';
......
import { Table, Tbody, Th, Tr } from '@chakra-ui/react'; import { Table, Tbody, Th, Tr } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { TokenInfo } from 'types/api/tokenInfo'; import type { TokenInfo } from 'types/api/token';
import { default as Thead } from 'ui/shared/TheadSticky'; import { default as Thead } from 'ui/shared/TheadSticky';
......
import { Box, Flex, Td, Tr, Text, Tag } from '@chakra-ui/react'; import { Box, Flex, Td, Tr, Text, Tag } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { TokenInfo } from 'types/api/tokenInfo'; import type { TokenInfo } from 'types/api/token';
import getCurrencyValue from 'lib/getCurrencyValue'; import getCurrencyValue from 'lib/getCurrencyValue';
import AddressAddToMetaMask from 'ui/address/details/AddressAddToMetaMask'; import AddressAddToMetaMask from 'ui/address/details/AddressAddToMetaMask';
......
...@@ -2,7 +2,7 @@ import { Hide, Show, Text } from '@chakra-ui/react'; ...@@ -2,7 +2,7 @@ import { Hide, Show, Text } 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/tokenInfo'; import type { TokenType } from 'types/api/token';
import { SECOND } from 'lib/consts'; import { SECOND } from 'lib/consts';
import getFilterValuesFromQuery from 'lib/getFilterValuesFromQuery'; import getFilterValuesFromQuery from 'lib/getFilterValuesFromQuery';
......
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