diff --git a/lib/api/resources.ts b/lib/api/resources.ts index 69f1ec6169..668cd6c1a4 100644 --- a/lib/api/resources.ts +++ b/lib/api/resources.ts @@ -28,6 +28,7 @@ import type { AddressWithdrawalsResponse, AddressNFTsResponse, AddressCollectionsResponse, + AddressNFTTokensFilter, } from 'types/api/address'; import type { AddressesResponse } from 'types/api/addresses'; import type { BlocksResponse, BlockTransactionsResponse, Block, BlockFilters, BlockWithdrawalsResponse } from 'types/api/block'; @@ -307,12 +308,12 @@ export const RESOURCES = { address_nfts: { path: '/api/v2/addresses/:hash/nft', pathParams: [ 'hash' as const ], - filterFields: [ ], + filterFields: [ 'type' as const ], }, address_collections: { path: '/api/v2/addresses/:hash/nft/collections', pathParams: [ 'hash' as const ], - filterFields: [ ], + filterFields: [ 'type' as const ], }, address_withdrawals: { path: '/api/v2/addresses/:hash/withdrawals', @@ -668,6 +669,8 @@ Q extends 'token_transfers' ? TokenTransferFilters : Q extends 'address_txs' | 'address_internal_txs' ? AddressTxsFilters : Q extends 'address_token_transfers' ? AddressTokenTransferFilters : Q extends 'address_tokens' ? AddressTokensFilter : +Q extends 'address_nfts' ? AddressNFTTokensFilter : +Q extends 'address_collections' ? AddressNFTTokensFilter : Q extends 'search' ? SearchResultFilters : Q extends 'token_inventory' ? TokenInventoryFilters : Q extends 'tokens' ? TokensFilters : diff --git a/lib/token/tokenTypes.ts b/lib/token/tokenTypes.ts index c8f258b4fb..5246fc2418 100644 --- a/lib/token/tokenTypes.ts +++ b/lib/token/tokenTypes.ts @@ -1,9 +1,14 @@ -import type { TokenType } from 'types/api/token'; +import type { NFTTokenType, TokenType } from 'types/api/token'; -export const TOKEN_TYPES: Array<{ title: string; id: TokenType }> = [ - { title: 'ERC-20', id: 'ERC-20' }, +export const NFT_TOKEN_TYPES: Array<{ title: string; id: NFTTokenType }> = [ { title: 'ERC-721', id: 'ERC-721' }, { title: 'ERC-1155', id: 'ERC-1155' }, ]; +export const TOKEN_TYPES: Array<{ title: string; id: TokenType }> = [ + { title: 'ERC-20', id: 'ERC-20' }, + ...NFT_TOKEN_TYPES, +]; + +export const NFT_TOKEN_TYPE_IDS = NFT_TOKEN_TYPES.map(i => i.id); export const TOKEN_TYPE_IDS = TOKEN_TYPES.map(i => i.id); diff --git a/types/api/address.ts b/types/api/address.ts index aa1b5f1334..bd70c3166c 100644 --- a/types/api/address.ts +++ b/types/api/address.ts @@ -3,7 +3,7 @@ import type { Transaction } from 'types/api/transaction'; import type { UserTags } from './addressParams'; import type { Block } from './block'; import type { InternalTransaction } from './internalTransaction'; -import type { TokenInfo, TokenInstance, TokenType } from './token'; +import type { NFTTokenType, TokenInfo, TokenInstance, TokenType } from './token'; import type { TokenTransfer, TokenTransferPagination } from './tokenTransfer'; export interface Address extends UserTags { @@ -126,6 +126,10 @@ export type AddressTokensFilter = { type: TokenType; } +export type AddressNFTTokensFilter = { + type: Array | undefined; +} + export interface AddressCoinBalanceHistoryItem { block_number: number; block_timestamp: string; diff --git a/types/api/token.ts b/types/api/token.ts index 417a6022e7..aa935f6d3a 100644 --- a/types/api/token.ts +++ b/types/api/token.ts @@ -1,7 +1,8 @@ import type { TokenInfoApplication } from './account'; import type { AddressParam } from './addressParams'; -export type TokenType = 'ERC-20' | 'ERC-721' | 'ERC-1155'; +export type NFTTokenType = 'ERC-721' | 'ERC-1155'; +export type TokenType = 'ERC-20' | NFTTokenType; export interface TokenInfo { address: string; diff --git a/ui/address/AddressTokens.tsx b/ui/address/AddressTokens.tsx index bcf8bdeee4..1d477af987 100644 --- a/ui/address/AddressTokens.tsx +++ b/ui/address/AddressTokens.tsx @@ -1,17 +1,22 @@ -import { Box } from '@chakra-ui/react'; +import { Box, HStack } from '@chakra-ui/react'; import { useRouter } from 'next/router'; import React from 'react'; +import type { NFTTokenType } from 'types/api/token'; import type { PaginationParams } from 'ui/shared/pagination/types'; import listIcon from 'icons/apps.svg'; import collectionIcon from 'icons/collection.svg'; import { useAppContext } from 'lib/contexts/app'; import * as cookies from 'lib/cookies'; +import getFilterValuesFromQuery from 'lib/getFilterValuesFromQuery'; import useIsMobile from 'lib/hooks/useIsMobile'; import getQueryParamString from 'lib/router/getQueryParamString'; +import { NFT_TOKEN_TYPE_IDS } from 'lib/token/tokenTypes'; import { ADDRESS_TOKEN_BALANCE_ERC_20, ADDRESS_NFT_1155, ADDRESS_COLLECTION } from 'stubs/address'; import { generateListStub } from 'stubs/utils'; +import PopoverFilter from 'ui/shared/filters/PopoverFilter'; +import TokenTypeFilter from 'ui/shared/filters/TokenTypeFilter'; import Pagination from 'ui/shared/pagination/Pagination'; import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages'; import RadioButtonGroup from 'ui/shared/RadioButtonGroup'; @@ -25,17 +30,18 @@ import TokenBalances from './tokens/TokenBalances'; type TNftDisplayType = 'collection' | 'list'; const TAB_LIST_PROPS = { - marginBottom: 0, + my: 3, py: 5, - marginTop: 3, columnGap: 3, }; const TAB_LIST_PROPS_MOBILE = { - mt: 8, + my: 8, columnGap: 3, }; +const getTokenFilterValue = (getFilterValuesFromQuery).bind(null, NFT_TOKEN_TYPE_IDS); + const AddressTokens = () => { const router = useRouter(); const isMobile = useIsMobile(); @@ -44,6 +50,7 @@ const AddressTokens = () => { const displayTypeCookie = cookies.get(cookies.NAMES.ADDRESS_NFT_DISPLAY_TYPE, useAppContext().cookies); const [ nftDisplayType, setNftDisplayType ] = React.useState(displayTypeCookie === 'list' ? 'list' : 'collection'); + const [ tokenTypes, setTokenTypes ] = React.useState | undefined>(getTokenFilterValue(router.query.type) || []); const tab = getQueryParamString(router.query.tab); const hash = getQueryParamString(router.query.hash); @@ -69,6 +76,7 @@ const AddressTokens = () => { refetchOnMount: false, placeholderData: generateListStub<'address_collections'>(ADDRESS_COLLECTION, 10, { next_page_params: null }), }, + filters: { type: tokenTypes }, }); const nftsQuery = useQueryWithPages({ @@ -80,6 +88,7 @@ const AddressTokens = () => { refetchOnMount: false, placeholderData: generateListStub<'address_nfts'>(ADDRESS_NFT_1155, 10, { next_page_params: null }), }, + filters: { type: tokenTypes }, }); const handleNFTsDisplayTypeChange = React.useCallback((val: TNftDisplayType) => { @@ -87,6 +96,18 @@ const AddressTokens = () => { setNftDisplayType(val); }, []); + const handleTokenTypesChange = React.useCallback((value: Array) => { + nftsQuery.onFilterChange({ type: value }); + collectionsQuery.onFilterChange({ type: value }); + setTokenTypes(value); + }, [ nftsQuery, collectionsQuery ]); + + const nftTypeFilter = ( + 0 } contentProps={{ w: '200px' }} appliedFiltersNum={ tokenTypes?.length }> + nftOnly onChange={ handleTokenTypesChange } defaultValue={ tokenTypes }/> + + ); + const tabs = [ { id: 'tokens_erc20', title: 'ERC-20', component: }, { @@ -120,7 +141,10 @@ const AddressTokens = () => { const rightSlot = ( <> - { tab !== 'tokens' && tab !== 'tokens_erc20' && nftDisplayTypeRadio } + + { tab !== 'tokens' && tab !== 'tokens_erc20' && nftDisplayTypeRadio } + { tab !== 'tokens' && tab !== 'tokens_erc20' && nftTypeFilter } + { pagination.isVisible && !isMobile && } ); diff --git a/ui/address/tokens/AddressCollections.tsx b/ui/address/tokens/AddressCollections.tsx index 6ab437779b..21955a944a 100644 --- a/ui/address/tokens/AddressCollections.tsx +++ b/ui/address/tokens/AddressCollections.tsx @@ -44,7 +44,7 @@ const AddressCollections = ({ collectionsQuery, address }: Props) => { const hasOverload = Number(item.amount) > item.token_instances.length; return ( - + { ) : ( 0 } contentProps={{ w: '200px' }} appliedFiltersNum={ tokenTypes?.length }> - + ); diff --git a/ui/shared/RadioButtonGroup.tsx b/ui/shared/RadioButtonGroup.tsx index 518e58e579..246844fa04 100644 --- a/ui/shared/RadioButtonGroup.tsx +++ b/ui/shared/RadioButtonGroup.tsx @@ -17,6 +17,7 @@ type RadioButtonProps = UseRadioProps & RadioItemProps; const RadioButton = (props: RadioButtonProps) => { const { getInputProps, getRadioProps } = useRadio(props); const buttonColor = useColorModeValue('blue.50', 'gray.800'); + const checkedTextColor = useColorModeValue('blue.700', 'gray.50'); const input = getInputProps(); const checkbox = getRadioProps(); @@ -35,7 +36,7 @@ const RadioButton = (props: RadioButtonProps) => { _active: { backgroundColor: 'none', }, - ...(props.isChecked ? { color: 'text' } : {}), + ...(props.isChecked ? { color: checkedTextColor } : {}), }; if (props.onlyIcon) { @@ -58,7 +59,7 @@ const RadioButton = (props: RadioButtonProps) => { return (