Commit 836ba37d authored by isstuev's avatar isstuev

tokens list + nft

parent 3c6224f8
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/tokenInfo';
import type { RoutedTab } from 'ui/shared/RoutedTabs/types';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import useQueryWithPages from 'lib/hooks/useQueryWithPages'; import useQueryWithPages from 'lib/hooks/useQueryWithPages';
...@@ -11,23 +11,30 @@ import Pagination from 'ui/shared/Pagination'; ...@@ -11,23 +11,30 @@ import Pagination from 'ui/shared/Pagination';
import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs'; import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs';
import TokenBalances from './tokens/TokenBalances'; import TokenBalances from './tokens/TokenBalances';
import TokensWithIds from './tokens/TokensWithIds';
import TokensWithoutIds from './tokens/TokensWithoutIds';
type Props = { const AddressTokens = () => {
tabs: Array<RoutedTab>;
}
const AddressTokens = ({ tabs }: Props) => {
const router = useRouter(); const router = useRouter();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const scrollRef = React.useRef<HTMLDivElement>(null);
const tokenType: TokenType = (Object.keys(tokenTabsByType) as Array<TokenType>).find(key => tokenTabsByType[key] === router.query.tab) || 'ERC-20'; const tokenType: TokenType = (Object.keys(tokenTabsByType) as Array<TokenType>).find(key => tokenTabsByType[key] === router.query.tab) || 'ERC-20';
const { pagination, isPaginationVisible } = useQueryWithPages({ const tokensQuery = useQueryWithPages({
resourceName: 'address_tokens', resourceName: 'address_tokens',
pathParams: { id: router.query.id?.toString() }, pathParams: { id: router.query.id?.toString() },
filters: { type: tokenType }, filters: { type: tokenType },
scrollRef,
}); });
const tabs = [
{ id: tokenTabsByType['ERC-20'], title: 'ERC-20', component: <TokensWithoutIds tokensQuery={ tokensQuery }/> },
{ id: tokenTabsByType['ERC-721'], title: 'ERC-721', component: <TokensWithoutIds tokensQuery={ tokensQuery }/> },
{ id: tokenTabsByType['ERC-1155'], title: 'ERC-1155', component: <TokensWithIds tokensQuery={ tokensQuery }/> },
];
const TAB_LIST_PROPS = { const TAB_LIST_PROPS = {
marginBottom: 0, marginBottom: 0,
py: 5, py: 5,
...@@ -38,13 +45,15 @@ const AddressTokens = ({ tabs }: Props) => { ...@@ -38,13 +45,15 @@ const AddressTokens = ({ tabs }: Props) => {
return ( return (
<> <>
<TokenBalances/> <TokenBalances/>
{ /* should stay before tabs to scroll up whith pagination */ }
<Box ref={ scrollRef }></Box>
<RoutedTabs <RoutedTabs
tabs={ tabs } tabs={ tabs }
variant="outline" variant="outline"
colorScheme="gray" colorScheme="gray"
size="sm" size="sm"
tabListProps={ isMobile ? { mt: 8, columnGap: 3 } : TAB_LIST_PROPS } tabListProps={ isMobile ? { mt: 8, columnGap: 3 } : TAB_LIST_PROPS }
rightSlot={ isPaginationVisible && !isMobile ? <Pagination { ...pagination }/> : null } rightSlot={ tokensQuery.isPaginationVisible && !isMobile ? <Pagination { ...tokensQuery.pagination }/> : null }
stickyEnabled={ !isMobile } stickyEnabled={ !isMobile }
/> />
</> </>
......
import { Box, Center, Flex, Icon, Link, Text } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';
import type { AddressTokenBalance } from 'types/api/address';
import NFTIcon from 'icons/nft_shield.svg';
import link from 'lib/link/link';
import TokenLogo from 'ui/shared/TokenLogo';
type Props = AddressTokenBalance;
const NFTItem = ({ token, token_id: tokenId }: Props) => {
const tokenLink = link('token_index', { hash: token.address });
const router = useRouter();
const onItemClick = React.useCallback(() => {
router.push(tokenLink);
}, [ router, tokenLink ]);
return (
<Box
w="210px"
h="272px"
border="1px solid"
borderColor="blackAlpha.100"
borderRadius="12px"
p="10px"
_hover={{ boxShadow: 'md' }}
fontSize="sm"
fontWeight={ 500 }
lineHeight="20px"
onClick={ onItemClick }
cursor="pointer"
>
<Center w="182px" h="182px" bg="blackAlpha.50" mb="18px" borderRadius="12px">
<Icon as={ NFTIcon } boxSize="112px" color="blackAlpha.500"/>
</Center>
{ tokenId && (
<Flex mb={ 2 } ml={ 1 }>
<Text whiteSpace="pre" variant="secondary">ID# </Text>
<Link>{ tokenId }</Link>
</Flex>
) }
{ token.name && (
<Flex>
<TokenLogo hash={ token.address } name={ token.name } boxSize={ 6 } ml={ 1 } mr={ 1 }/>
<Text variant="secondary">{ token.name || 'aaaaa' }</Text>
</Flex>
) }
</Box>
);
};
export default NFTItem;
import { Skeleton } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';
import type { TokenType } from 'types/api/tokenInfo';
import useIsMobile from 'lib/hooks/useIsMobile';
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import ActionBar from 'ui/shared/ActionBar';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import Pagination from 'ui/shared/Pagination';
import TokensTable from './TokensTable';
type Props = {
type: TokenType;
}
const TokensERC20 = ({ type }: Props) => {
const router = useRouter();
const isMobile = useIsMobile();
const { isError, isLoading, data, pagination, isPaginationVisible } = useQueryWithPages({
resourceName: 'address_tokens',
pathParams: { id: router.query.id?.toString() },
filters: { type },
});
if (isError) {
return <DataFetchAlert/>;
}
if (isLoading) {
return <Skeleton w="500px" h="50px"/>;
}
return (
<>
{ isMobile && isPaginationVisible && (
<ActionBar mt={ -6 }>
<Pagination ml="auto" { ...pagination }/>
</ActionBar>
) }
<TokensTable data={ data.items } top={ isPaginationVisible ? 72 : 0 }/>
</>
);
};
export default TokensERC20;
import { Flex, HStack, Text } from '@chakra-ui/react';
import React from 'react';
import type { AddressTokenBalance } from 'types/api/address';
import getCurrencyValue from 'lib/getCurrencyValue';
import AddressLink from 'ui/shared/address/AddressLink';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import ListItemMobile from 'ui/shared/ListItemMobile';
import TokenLogo from 'ui/shared/TokenLogo';
type Props = AddressTokenBalance;
const TokensListItem = ({ token, value }: Props) => {
const tokenString = [ token.name, token.symbol && `(${ token.symbol })` ].filter(Boolean).join(' ');
const {
valueStr: tokenQuantity,
usd: tokenValue,
} = getCurrencyValue({ value: value, exchangeRate: token.exchange_rate, decimals: token.decimals, accuracy: 8, accuracyUsd: 2 });
return (
<ListItemMobile rowGap={ 2 }>
<Flex alignItems="center">
<TokenLogo hash={ token.address } name={ token.name } boxSize={ 6 } mr={ 2 }/>
<AddressLink fontWeight="700" hash={ token.address } type="token" alias={ tokenString }/>
</Flex>
<Flex alignItems="center">
<AddressLink hash={ token.address } type="address" truncation="constant"/>
<CopyToClipboard text={ token.address } ml={ 1 }/>
</Flex>
{ token.exchange_rate !== undefined && token.exchange_rate !== null && (
<HStack spacing={ 3 }>
<Text fontSize="sm" fontWeight={ 500 }>Price</Text>
<Text fontSize="sm" variant="secondary">{ `$${ token.exchange_rate }` }</Text>
</HStack>
) }
<HStack spacing={ 3 }>
<Text fontSize="sm" fontWeight={ 500 }>Quantity</Text>
<Text fontSize="sm" variant="secondary">{ tokenQuantity }</Text>
</HStack>
{ tokenValue !== undefined && (
<HStack spacing={ 3 }>
<Text fontSize="sm" fontWeight={ 500 }>Value</Text>
<Text fontSize="sm" variant="secondary">{ tokenValue }</Text>
</HStack>
) }
</ListItemMobile>
);
};
export default TokensListItem;
...@@ -17,8 +17,8 @@ const TokensTable = ({ data, top }: Props) => { ...@@ -17,8 +17,8 @@ const TokensTable = ({ data, top }: Props) => {
<Table variant="simple" size="sm"> <Table variant="simple" size="sm">
<Thead top={ top }> <Thead top={ top }>
<Tr> <Tr>
<Th width="20%">Asset</Th> <Th width="30%">Asset</Th>
<Th width="40%">Contract address</Th> <Th width="30%">Contract address</Th>
<Th width="10%" isNumeric>Price</Th> <Th width="10%" isNumeric>Price</Th>
<Th width="20%" isNumeric>Quantity</Th> <Th width="20%" isNumeric>Quantity</Th>
<Th width="10%" isNumeric>Value</Th> <Th width="10%" isNumeric>Value</Th>
......
...@@ -6,6 +6,7 @@ import type { AddressTokenBalance } from 'types/api/address'; ...@@ -6,6 +6,7 @@ import type { AddressTokenBalance } from 'types/api/address';
import getCurrencyValue from 'lib/getCurrencyValue'; import getCurrencyValue from 'lib/getCurrencyValue';
import AddressLink from 'ui/shared/address/AddressLink'; import AddressLink from 'ui/shared/address/AddressLink';
import CopyToClipboard from 'ui/shared/CopyToClipboard'; import CopyToClipboard from 'ui/shared/CopyToClipboard';
import TokenLogo from 'ui/shared/TokenLogo';
import AddressAddToMetaMask from '../details/AddressAddToMetaMask'; import AddressAddToMetaMask from '../details/AddressAddToMetaMask';
...@@ -26,7 +27,10 @@ const TokensTableItem = ({ ...@@ -26,7 +27,10 @@ const TokensTableItem = ({
return ( return (
<Tr> <Tr>
<Td verticalAlign="middle"> <Td verticalAlign="middle">
<AddressLink fontWeight="700" hash={ token.address } type="token" alias={ tokenString }/> <Flex alignItems="center">
<TokenLogo hash={ token.address } name={ token.name } boxSize={ 6 } mr={ 2 }/>
<AddressLink fontWeight="700" hash={ token.address } type="token" alias={ tokenString }/>
</Flex>
</Td> </Td>
<Td verticalAlign="middle"> <Td verticalAlign="middle">
<Flex alignItems="center" width="150px" justifyContent="space-between"> <Flex alignItems="center" width="150px" justifyContent="space-between">
......
import { Flex, Skeleton, Text } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query';
import React from 'react';
import type { AddressTokensResponse } from 'types/api/address';
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 NFTItem from './NFTItem';
type Props = {
tokensQuery: UseQueryResult<AddressTokensResponse> & {
pagination: PaginationProps;
isPaginationVisible: boolean;
};
}
const TokensWithIds = ({ tokensQuery }: Props) => {
const isMobile = useIsMobile();
const { isError, isLoading, data, pagination, isPaginationVisible } = tokensQuery;
if (isError) {
return <DataFetchAlert/>;
}
const bar = isMobile && isPaginationVisible && (
<ActionBar mt={ -6 }>
<Pagination ml="auto" { ...pagination }/>
</ActionBar>
);
if (isLoading) {
return (
<>
{ bar }
<Flex columnGap={ 6 } rowGap={ 6 } flexWrap="wrap">
<Skeleton w="210px" h="272px"/>
<Skeleton w="210px" h="272px"/>
<Skeleton w="210px" h="272px"/>
</Flex>
</>
);
}
if (!data.items.length) {
return <Text as="span">There are no tokens of selected type.</Text>;
}
return (
<>
{ bar }
<Flex columnGap={ 6 } rowGap={ 6 } flexWrap="wrap">
{ data.items.map(item => <NFTItem key={ item.token.address } { ...item }/>) }
</Flex>
</>
);
};
export default TokensWithIds;
import { Text, Show, Hide } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query';
import React from 'react';
import type { AddressTokensResponse } from 'types/api/address';
import useIsMobile from 'lib/hooks/useIsMobile';
import ActionBar from 'ui/shared/ActionBar';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import Pagination from 'ui/shared/Pagination';
import type { Props as PaginationProps } from 'ui/shared/Pagination';
import SkeletonList from 'ui/shared/skeletons/SkeletonList';
import SkeletonTable from 'ui/shared/skeletons/SkeletonTable';
import TokensListItem from './TokensListItem';
import TokensTable from './TokensTable';
type Props = {
tokensQuery: UseQueryResult<AddressTokensResponse> & {
pagination: PaginationProps;
isPaginationVisible: boolean;
};
}
const TokensWithoutIds = ({ tokensQuery }: Props) => {
const isMobile = useIsMobile();
const { isError, isLoading, data, pagination, isPaginationVisible } = tokensQuery;
if (isError) {
return <DataFetchAlert/>;
}
const bar = isMobile && isPaginationVisible && (
<ActionBar mt={ -6 }>
<Pagination ml="auto" { ...pagination }/>
</ActionBar>
);
if (isLoading) {
return (
<>
{ bar }
<Hide below="lg" ssr={ false }><SkeletonTable columns={ [ '30%', '30%', '10%', '20%', '10%' ] }/></Hide>
<Show below="lg" ssr={ false }><SkeletonList/></Show>
</>
);
}
if (data.items.length === 0) {
return <Text as="span">There are no tokens of selected type.</Text>;
}
return (
<>
{ bar }
<Hide below="lg" ssr={ false }><TokensTable data={ data.items } top={ isPaginationVisible ? 72 : 0 }/></Hide>
<Show below="lg" ssr={ false }>{ data.items.map(item => <TokensListItem key={ item.token.address } { ...item }/>) }</Show>
</>
);
};
export default TokensWithoutIds;
...@@ -19,7 +19,6 @@ import AddressTxs from 'ui/address/AddressTxs'; ...@@ -19,7 +19,6 @@ import AddressTxs from 'ui/address/AddressTxs';
import ContractCode from 'ui/address/contract/ContractCode'; import ContractCode from 'ui/address/contract/ContractCode';
import ContractRead from 'ui/address/contract/ContractRead'; import ContractRead from 'ui/address/contract/ContractRead';
import ContractWrite from 'ui/address/contract/ContractWrite'; import ContractWrite from 'ui/address/contract/ContractWrite';
import Tokens from 'ui/address/tokens/Tokens';
import TextAd from 'ui/shared/ad/TextAd'; import TextAd from 'ui/shared/ad/TextAd';
import Page from 'ui/shared/Page/Page'; import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle'; import PageTitle from 'ui/shared/Page/PageTitle';
...@@ -32,11 +31,7 @@ export const tokenTabsByType: Record<TokenType, string> = { ...@@ -32,11 +31,7 @@ export const tokenTabsByType: Record<TokenType, string> = {
'ERC-1155': 'tokens_erc1155', 'ERC-1155': 'tokens_erc1155',
} as const; } as const;
const TOKENS_TABS = [ const TOKEN_TABS = Object.values(tokenTabsByType);
{ id: tokenTabsByType['ERC-20'], title: 'ERC-20', component: <Tokens type="ERC-20"/> },
{ id: tokenTabsByType['ERC-721'], title: 'ERC-721', component: null },
{ id: tokenTabsByType['ERC-1155'], title: 'ERC-1155', component: null },
];
const AddressPageContent = () => { const AddressPageContent = () => {
const router = useRouter(); const router = useRouter();
...@@ -87,7 +82,7 @@ const AddressPageContent = () => { ...@@ -87,7 +82,7 @@ const AddressPageContent = () => {
addressQuery.data?.has_token_transfers ? addressQuery.data?.has_token_transfers ?
{ id: 'token_transfers', title: 'Token transfers', component: <AddressTokenTransfers scrollRef={ tabsScrollRef }/> } : { id: 'token_transfers', title: 'Token transfers', component: <AddressTokenTransfers scrollRef={ tabsScrollRef }/> } :
undefined, undefined,
addressQuery.data?.has_tokens ? { id: 'tokens', title: 'Tokens', component: <AddressTokens tabs={ TOKENS_TABS }/>, subTabs: TOKENS_TABS } : undefined, addressQuery.data?.has_tokens ? { id: 'tokens', title: 'Tokens', component: <AddressTokens/>, subTabs: TOKEN_TABS } : undefined,
{ id: 'internal_txns', title: 'Internal txns', component: <AddressInternalTxs scrollRef={ tabsScrollRef }/> }, { id: 'internal_txns', title: 'Internal txns', component: <AddressInternalTxs scrollRef={ tabsScrollRef }/> },
{ id: 'coin_balance_history', title: 'Coin balance history', component: <AddressCoinBalance/> }, { id: 'coin_balance_history', title: 'Coin balance history', component: <AddressCoinBalance/> },
addressQuery.data?.has_validated_blocks ? addressQuery.data?.has_validated_blocks ?
...@@ -98,7 +93,7 @@ const AddressPageContent = () => { ...@@ -98,7 +93,7 @@ const AddressPageContent = () => {
id: 'contract', id: 'contract',
title: 'Contract', title: 'Contract',
component: <AddressContract tabs={ contractTabs }/>, component: <AddressContract tabs={ contractTabs }/>,
subTabs: contractTabs, subTabs: contractTabs.map(tab => tab.id),
} : undefined, } : undefined,
].filter(notEmpty); ].filter(notEmpty);
}, [ addressQuery.data, contractTabs ]); }, [ addressQuery.data, contractTabs ]);
......
...@@ -62,7 +62,7 @@ const RoutedTabs = ({ tabs, tabListProps, rightSlot, stickyEnabled, className, . ...@@ -62,7 +62,7 @@ const RoutedTabs = ({ tabs, tabListProps, rightSlot, stickyEnabled, className, .
let tabIndex = 0; let tabIndex = 0;
const tabFromRoute = router.query.tab; const tabFromRoute = router.query.tab;
if (tabFromRoute) { if (tabFromRoute) {
tabIndex = tabs.findIndex(({ id, subTabs }) => id === tabFromRoute || subTabs?.some(({ id }) => id === tabFromRoute)); tabIndex = tabs.findIndex(({ id, subTabs }) => id === tabFromRoute || subTabs?.some((id) => id === tabFromRoute));
if (tabIndex < 0) { if (tabIndex < 0) {
tabIndex = 0; tabIndex = 0;
} }
......
...@@ -2,7 +2,7 @@ export interface RoutedTab { ...@@ -2,7 +2,7 @@ export interface RoutedTab {
id: string; id: string;
title: string; title: string;
component: React.ReactNode; component: React.ReactNode;
subTabs?: Array<RoutedSubTab>; subTabs?: Array<string>;
} }
export type RoutedSubTab = Omit<RoutedTab, 'subTabs'>; export type RoutedSubTab = Omit<RoutedTab, 'subTabs'>;
......
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