Commit fe554059 authored by Igor Stuev's avatar Igor Stuev Committed by GitHub

Merge pull request #512 from blockscout/tokens-list

tokens list
parents 2d8dbbec c47464cc
...@@ -473,6 +473,7 @@ frontend: ...@@ -473,6 +473,7 @@ frontend:
# - "/address" # - "/address"
- "/stats" - "/stats"
- "/search-results" - "/search-results"
- "/tokens"
resources: resources:
limits: limits:
memory: memory:
......
...@@ -322,6 +322,7 @@ frontend: ...@@ -322,6 +322,7 @@ frontend:
- "/stats" - "/stats"
- "/search-results" - "/search-results"
- "/token" - "/token"
- "/tokens"
resources: resources:
limits: limits:
......
...@@ -24,6 +24,7 @@ import type { RawTracesResponse } from 'types/api/rawTrace'; ...@@ -24,6 +24,7 @@ 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 } from 'types/api/tokenInfo';
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';
...@@ -213,6 +214,11 @@ export const RESOURCES = { ...@@ -213,6 +214,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: [],
}, },
tokens: {
path: '/api/v2/tokens',
paginationFields: [ 'holder_count' as const, 'items_count' as const, 'name' as const ],
filterFields: [ 'filter' as const, 'type' as const ],
},
// HOMEPAGE // HOMEPAGE
homepage_stats: { homepage_stats: {
...@@ -283,7 +289,7 @@ export type PaginatedResources = 'blocks' | 'block_txs' | ...@@ -283,7 +289,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'; 'token_transfers' | 'token_holders' | 'tokens';
export type PaginatedResponse<Q extends PaginatedResources> = ResourcePayload<Q>; export type PaginatedResponse<Q extends PaginatedResources> = ResourcePayload<Q>;
...@@ -330,6 +336,7 @@ Q extends 'token' ? TokenInfo : ...@@ -330,6 +336,7 @@ Q extends 'token' ? TokenInfo :
Q extends 'token_counters' ? TokenCounters : Q extends 'token_counters' ? TokenCounters :
Q extends 'token_transfers' ? TokenTransferResponse : Q extends 'token_transfers' ? TokenTransferResponse :
Q extends 'token_holders' ? TokenHolders : Q extends 'token_holders' ? TokenHolders :
Q extends 'tokens' ? TokensResponse :
Q extends 'search' ? SearchResult : Q extends 'search' ? SearchResult :
Q extends 'contract' ? SmartContract : Q extends 'contract' ? SmartContract :
Q extends 'contract_methods_read' ? Array<SmartContractReadMethod> : Q extends 'contract_methods_read' ? Array<SmartContractReadMethod> :
...@@ -349,5 +356,6 @@ Q extends 'address_txs' | 'address_internal_txs' ? AddressTxsFilters : ...@@ -349,5 +356,6 @@ 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 'tokens' ? TokensFilters :
never; never;
/* eslint-enable @typescript-eslint/indent */ /* eslint-enable @typescript-eslint/indent */
import type { TokenType } from 'types/api/tokenInfo';
const TOKEN_TYPE: Array<{ title: string; id: TokenType }> = [
{ title: 'ERC-20', id: 'ERC-20' },
{ title: 'ERC-721', id: 'ERC-721' },
{ title: 'ERC-1155', id: 'ERC-1155' },
];
export default TOKEN_TYPE;
import type { AddressTokenBalance } from 'types/api/address'; import type { AddressTokenBalance } from 'types/api/address';
import * as tokens from 'mocks/tokens/tokenInfo';
export const erc20a: AddressTokenBalance = { export const erc20a: AddressTokenBalance = {
token: { token: tokens.tokenInfoERC20a,
address: '0xb2a90505dc6680a7a695f7975d0d32EeF610f456',
decimals: '18',
exchange_rate: null,
holders: '23',
name: 'hyfi.token',
symbol: 'HyFi',
total_supply: '369000000000000000000000000',
type: 'ERC-20',
},
token_id: null, token_id: null,
value: '1169320000000000000000000', value: '1169320000000000000000000',
}; };
export const erc20b: AddressTokenBalance = { export const erc20b: AddressTokenBalance = {
token: { token: tokens.tokenInfoERC20b,
address: '0xc1116c98ba622a6218433fF90a2E40DEa482d7A7',
decimals: '6',
exchange_rate: '0.982',
holders: '17',
name: 'USD Coin',
symbol: 'USDC',
total_supply: '900000000000000000000000000',
type: 'ERC-20',
},
token_id: null, token_id: null,
value: '872500000000', value: '872500000000',
}; };
export const erc20c: AddressTokenBalance = { export const erc20c: AddressTokenBalance = {
token: { token: tokens.tokenInfoERC20c,
address: '0xc1116c98ba622a6218433fF90a2E40DEa482d7A7',
decimals: '18',
exchange_rate: '1328.89',
holders: '17',
name: 'Ethereum',
symbol: 'ETH',
total_supply: '1000000000000000000000000',
type: 'ERC-20',
},
token_id: null, token_id: null,
value: '9852000000000000000000', value: '9852000000000000000000',
}; };
export const erc20d: AddressTokenBalance = { export const erc20d: AddressTokenBalance = {
token: { token: tokens.tokenInfoERC20d,
address: '0xCc7bb2D219A0FC08033E130629C2B854b7bA9195',
decimals: '18',
exchange_rate: null,
holders: '102625',
name: 'Zeta',
symbol: 'ZETA',
total_supply: '2100000000000000000000000000',
type: 'ERC-20',
},
token_id: null, token_id: null,
value: '39000000000000000000', value: '39000000000000000000',
}; };
export const erc721a: AddressTokenBalance = { export const erc721a: AddressTokenBalance = {
token: { token: tokens.tokenInfoERC721a,
address: '0xDe7cAc71E072FCBd4453E5FB3558C2684d1F88A0',
decimals: null,
exchange_rate: null,
holders: '7',
name: 'HyFi Athena',
symbol: 'HYFI_ATHENA',
total_supply: '105',
type: 'ERC-721',
},
token_id: null, token_id: null,
value: '51', value: '51',
}; };
export const erc721b: AddressTokenBalance = { export const erc721b: AddressTokenBalance = {
token: { token: tokens.tokenInfoERC721b,
address: '0xA8d5C7beEA8C9bB57f5fBa35fB638BF45550b11F',
decimals: null,
exchange_rate: null,
holders: '2',
name: 'World Of Women Galaxy',
symbol: 'WOWG',
total_supply: null,
type: 'ERC-721',
},
token_id: null, token_id: null,
value: '1', value: '1',
}; };
export const erc721c: AddressTokenBalance = { export const erc721c: AddressTokenBalance = {
token: { token: tokens.tokenInfoERC721c,
address: '0x47646F1d7dc4Dd2Db5a41D092e2Cf966e27A4992',
decimals: null,
exchange_rate: null,
holders: '12',
name: 'Puma',
symbol: 'PUMA',
total_supply: null,
type: 'ERC-721',
},
token_id: null, token_id: null,
value: '5', value: '5',
}; };
export const erc1155a: AddressTokenBalance = { export const erc1155a: AddressTokenBalance = {
token: { token: tokens.tokenInfoERC1155a,
address: '0x4b333DEd10c7ca855EA2C8D4D90A0a8b73788c8e',
decimals: null,
exchange_rate: null,
holders: '22',
name: 'HyFi Membership',
symbol: 'HYFI_MEMBERSHIP',
total_supply: '482',
type: 'ERC-1155',
},
token_id: '42', token_id: '42',
value: '24', value: '24',
}; };
export const erc1155b: AddressTokenBalance = { export const erc1155b: AddressTokenBalance = {
token: { token: tokens.tokenInfoERC1155b,
address: '0xf4b71b179132ad457f6bcae2a55efa9e4b26eefc',
decimals: null,
exchange_rate: null,
holders: '100',
name: 'WinkyVerse Collections',
symbol: 'WVC',
total_supply: '4943',
type: 'ERC-1155',
},
token_id: '100010000000001', token_id: '100010000000001',
value: '11', value: '11',
}; };
export const erc1155withoutName: AddressTokenBalance = { export const erc1155withoutName: AddressTokenBalance = {
token: { token: tokens.tokenInfoERC1155WithoutName,
address: '0x4b333DEd10c7ca855EA2C8D4D90A0a8b73788c8e',
decimals: null,
exchange_rate: null,
holders: '22',
name: null,
symbol: null,
total_supply: '482',
type: 'ERC-1155',
},
token_id: '64532245', token_id: '64532245',
value: '42', value: '42',
}; };
......
...@@ -15,3 +15,113 @@ export const tokenCounters: TokenCounters = { ...@@ -15,3 +15,113 @@ export const tokenCounters: TokenCounters = {
token_holders_count: '8838883', token_holders_count: '8838883',
transfers_count: '88282281', transfers_count: '88282281',
}; };
export const tokenInfoERC20a: TokenInfo = {
address: '0xb2a90505dc6680a7a695f7975d0d32EeF610f456',
decimals: '18',
exchange_rate: null,
holders: '23',
name: 'hyfi.token',
symbol: 'HyFi',
total_supply: '369000000000000000000000000',
type: 'ERC-20',
};
export const tokenInfoERC20b: TokenInfo = {
address: '0xc1116c98ba622a6218433fF90a2E40DEa482d7A7',
decimals: '6',
exchange_rate: '0.982',
holders: '17',
name: 'USD Coin',
symbol: 'USDC',
total_supply: '900000000000000000000000000',
type: 'ERC-20',
};
export const tokenInfoERC20c: TokenInfo = {
address: '0xc1116c98ba622a6218433fF90a2E40DEa482d7A7',
decimals: '18',
exchange_rate: '1328.89',
holders: '17',
name: 'Ethereum',
symbol: 'ETH',
total_supply: '1000000000000000000000000',
type: 'ERC-20',
};
export const tokenInfoERC20d: TokenInfo = {
address: '0xCc7bb2D219A0FC08033E130629C2B854b7bA9195',
decimals: '18',
exchange_rate: null,
holders: '102625',
name: 'Zeta',
symbol: 'ZETA',
total_supply: '2100000000000000000000000000',
type: 'ERC-20',
};
export const tokenInfoERC721a: TokenInfo = {
address: '0xDe7cAc71E072FCBd4453E5FB3558C2684d1F88A0',
decimals: null,
exchange_rate: null,
holders: '7',
name: 'HyFi Athena',
symbol: 'HYFI_ATHENA',
total_supply: '105',
type: 'ERC-721',
};
export const tokenInfoERC721b: TokenInfo = {
address: '0xA8d5C7beEA8C9bB57f5fBa35fB638BF45550b11F',
decimals: null,
exchange_rate: null,
holders: '2',
name: 'World Of Women Galaxy',
symbol: 'WOWG',
total_supply: null,
type: 'ERC-721',
};
export const tokenInfoERC721c: TokenInfo = {
address: '0x47646F1d7dc4Dd2Db5a41D092e2Cf966e27A4992',
decimals: null,
exchange_rate: null,
holders: '12',
name: 'Puma',
symbol: 'PUMA',
total_supply: null,
type: 'ERC-721',
};
export const tokenInfoERC1155a: TokenInfo = {
address: '0x4b333DEd10c7ca855EA2C8D4D90A0a8b73788c8e',
decimals: null,
exchange_rate: null,
holders: '22',
name: 'HyFi Membership',
symbol: 'HYFI_MEMBERSHIP',
total_supply: '482',
type: 'ERC-1155',
};
export const tokenInfoERC1155b: TokenInfo = {
address: '0xf4b71b179132ad457f6bcae2a55efa9e4b26eefc',
decimals: null,
exchange_rate: null,
holders: '100',
name: 'WinkyVerse Collections',
symbol: 'WVC',
total_supply: '4943',
type: 'ERC-1155',
};
export const tokenInfoERC1155WithoutName: TokenInfo = {
address: '0x4b333DEd10c7ca855EA2C8D4D90A0a8b73788c8e',
decimals: null,
exchange_rate: null,
holders: '22',
name: null,
symbol: null,
total_supply: '482',
type: 'ERC-1155',
};
import type { NextPage } from 'next';
import Head from 'next/head';
import React from 'react';
import getNetworkTitle from 'lib/networks/getNetworkTitle';
import Tokens from 'ui/pages/Tokens';
const TokensPage: NextPage = () => {
const title = getNetworkTitle();
return (
<>
<Head>
<title>{ title }</title>
</Head>
<Tokens/>
</>
);
};
export default TokensPage;
export { getServerSideProps } from 'lib/next/getServerSideProps';
import type { TokenInfo, TokenType } from './tokenInfo';
export type TokensResponse = {
items: Array<TokenInfo>;
next_page_params: {
holder_count: number;
items_count: number;
name: string;
};
}
export type TokensFilters = { filter: string; type: Array<TokenType> | undefined };
...@@ -16,6 +16,7 @@ import useQueryWithPages from 'lib/hooks/useQueryWithPages'; ...@@ -16,6 +16,7 @@ import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import { apos } from 'lib/html-entities'; import { apos } from 'lib/html-entities';
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 TOKEN_TYPE from 'lib/token/tokenTypes';
import EmptySearchResult from 'ui/apps/EmptySearchResult'; import EmptySearchResult from 'ui/apps/EmptySearchResult';
import ActionBar from 'ui/shared/ActionBar'; import ActionBar from 'ui/shared/ActionBar';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
...@@ -23,7 +24,7 @@ import Pagination from 'ui/shared/Pagination'; ...@@ -23,7 +24,7 @@ import Pagination from 'ui/shared/Pagination';
import SkeletonList from 'ui/shared/skeletons/SkeletonList'; import SkeletonList from 'ui/shared/skeletons/SkeletonList';
import SkeletonTable from 'ui/shared/skeletons/SkeletonTable'; import SkeletonTable from 'ui/shared/skeletons/SkeletonTable';
import SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice'; import SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice';
import { TOKEN_TYPE, flattenTotal } from 'ui/shared/TokenTransfer/helpers'; import { flattenTotal } from 'ui/shared/TokenTransfer/helpers';
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';
import TokenTransferTable from 'ui/shared/TokenTransfer/TokenTransferTable'; import TokenTransferTable from 'ui/shared/TokenTransfer/TokenTransferTable';
......
...@@ -10,7 +10,7 @@ import React from 'react'; ...@@ -10,7 +10,7 @@ import React from 'react';
import type { AddressFromToFilter } from 'types/api/address'; import type { AddressFromToFilter } from 'types/api/address';
import FilterButton from 'ui/shared/FilterButton'; import FilterButton from 'ui/shared/filters/FilterButton';
interface Props { interface Props {
isActive: boolean; isActive: boolean;
......
...@@ -50,7 +50,7 @@ const AddressAddToMetaMask = ({ className, token }: Props) => { ...@@ -50,7 +50,7 @@ const AddressAddToMetaMask = ({ className, token }: Props) => {
} }
}, [ toast, token ]); }, [ toast, token ]);
if (token.type !== 'ERC-20' || !('ethereum' in window)) { if (!('ethereum' in window)) {
return null; return null;
} }
......
...@@ -6,7 +6,7 @@ import PlusIcon from 'icons/plus.svg'; ...@@ -6,7 +6,7 @@ import PlusIcon from 'icons/plus.svg';
import AppList from 'ui/apps/AppList'; import AppList from 'ui/apps/AppList';
import AppListSkeleton from 'ui/apps/AppListSkeleton'; import AppListSkeleton from 'ui/apps/AppListSkeleton';
import CategoriesMenu from 'ui/apps/CategoriesMenu'; import CategoriesMenu from 'ui/apps/CategoriesMenu';
import FilterInput from 'ui/shared/FilterInput'; import FilterInput from 'ui/shared/filters/FilterInput';
import useMarketplaceApps from '../apps/useMarketplaceApps'; import useMarketplaceApps from '../apps/useMarketplaceApps';
......
import React from 'react';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle';
import TokensList from 'ui/tokens/Tokens';
const Tokens = () => {
return (
<Page>
<PageTitle text="Tokens" withTextAd/>
<TokensList/>
</Page>
);
};
export default Tokens;
import { import {
Popover,
PopoverTrigger,
PopoverContent,
PopoverBody,
CheckboxGroup,
Checkbox,
Text, Text,
useDisclosure,
Radio, Radio,
RadioGroup, RadioGroup,
Stack, Stack,
...@@ -17,9 +10,8 @@ import React from 'react'; ...@@ -17,9 +10,8 @@ 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/tokenInfo';
import FilterButton from 'ui/shared/FilterButton'; import PopoverFilter from 'ui/shared/filters/PopoverFilter';
import TokenTypeFilter from 'ui/shared/filters/TokenTypeFilter';
import { TOKEN_TYPE } from './helpers';
interface Props { interface Props {
appliedFiltersNum?: number; appliedFiltersNum?: number;
...@@ -38,47 +30,32 @@ const TokenTransferFilter = ({ ...@@ -38,47 +30,32 @@ const TokenTransferFilter = ({
onAddressFilterChange, onAddressFilterChange,
defaultAddressFilter, defaultAddressFilter,
}: Props) => { }: Props) => {
const { isOpen, onToggle, onClose } = useDisclosure();
const borderColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200'); const borderColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200');
return ( return (
<Popover isOpen={ isOpen } onClose={ onClose } placement="bottom-start" isLazy> <PopoverFilter appliedFiltersNum={ appliedFiltersNum } contentProps={{ w: '200px' }}>
<PopoverTrigger> { withAddressFilter && (
<FilterButton <>
isActive={ isOpen || Number(appliedFiltersNum) > 0 } <Text variant="secondary" fontWeight={ 600 }>Address</Text>
onClick={ onToggle } <RadioGroup
appliedFiltersNum={ appliedFiltersNum } size="lg"
/> onChange={ onAddressFilterChange }
</PopoverTrigger> defaultValue={ defaultAddressFilter || 'all' }
<PopoverContent w="200px"> paddingBottom={ 4 }
<PopoverBody px={ 4 } py={ 6 } display="flex" flexDir="column" rowGap={ 5 }> borderBottom="1px solid"
{ withAddressFilter && ( borderColor={ borderColor }
<> >
<Text variant="secondary" fontWeight={ 600 }>Address</Text> <Stack spacing={ 4 }>
<RadioGroup <Radio value="all"><Text fontSize="md">All</Text></Radio>
size="lg" <Radio value="from"><Text fontSize="md">From</Text></Radio>
onChange={ onAddressFilterChange } <Radio value="to"><Text fontSize="md">To</Text></Radio>
defaultValue={ defaultAddressFilter || 'all' } </Stack>
paddingBottom={ 4 } </RadioGroup>
borderBottom="1px solid" </>
borderColor={ borderColor } ) }
> <Text variant="secondary" fontWeight={ 600 }>Type</Text>
<Stack spacing={ 4 }> <TokenTypeFilter onChange={ onTypeFilterChange } defaultValue={ defaultTypeFilters }/>
<Radio value="all"><Text fontSize="md">All</Text></Radio> </PopoverFilter>
<Radio value="from"><Text fontSize="md">From</Text></Radio>
<Radio value="to"><Text fontSize="md">To</Text></Radio>
</Stack>
</RadioGroup>
</>
) }
<Text variant="secondary" fontWeight={ 600 }>Type</Text>
<CheckboxGroup size="lg" onChange={ onTypeFilterChange } defaultValue={ defaultTypeFilters }>
{ TOKEN_TYPE.map(({ title, id }) => <Checkbox key={ id } value={ id }><Text fontSize="md">{ title }</Text></Checkbox>) }
</CheckboxGroup>
</PopoverBody>
</PopoverContent>
</Popover>
); );
}; };
......
import type { TokenType } from 'types/api/tokenInfo';
import type { TokenTransfer } from 'types/api/tokenTransfer'; import type { TokenTransfer } from 'types/api/tokenTransfer';
export const flattenTotal = (result: Array<TokenTransfer>, item: TokenTransfer): Array<TokenTransfer> => { export const flattenTotal = (result: Array<TokenTransfer>, item: TokenTransfer): Array<TokenTransfer> => {
...@@ -25,9 +24,3 @@ export const getTokenTransferTypeText = (type: TokenTransfer['type']) => { ...@@ -25,9 +24,3 @@ export const getTokenTransferTypeText = (type: TokenTransfer['type']) => {
return 'Token transfer'; return 'Token transfer';
} }
}; };
export const TOKEN_TYPE: Array<{ title: string; id: TokenType }> = [
{ title: 'ERC-20', id: 'ERC-20' },
{ title: 'ERC-721', id: 'ERC-721' },
{ title: 'ERC-1155', id: 'ERC-1155' },
];
...@@ -7,6 +7,8 @@ import link from 'lib/link/link'; ...@@ -7,6 +7,8 @@ import link from 'lib/link/link';
import HashStringShorten from 'ui/shared/HashStringShorten'; import HashStringShorten from 'ui/shared/HashStringShorten';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic'; import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import TruncatedTextTooltip from '../TruncatedTextTooltip';
interface Props { interface Props {
type?: 'address' | 'transaction' | 'token' | 'block' | 'token_instance_item'; type?: 'address' | 'transaction' | 'token' | 'block' | 'token_instance_item';
alias?: string | null; alias?: string | null;
...@@ -37,9 +39,17 @@ const AddressLink = ({ alias, type, className, truncation = 'dynamic', hash, id, ...@@ -37,9 +39,17 @@ const AddressLink = ({ alias, type, className, truncation = 'dynamic', hash, id,
const content = (() => { const content = (() => {
if (alias) { if (alias) {
const text = <Box overflow="hidden" textOverflow="ellipsis" whiteSpace="nowrap">{ alias }</Box>;
if (type === 'token') {
return (
<TruncatedTextTooltip label={ alias }>
{ text }
</TruncatedTextTooltip>
);
}
return ( return (
<Tooltip label={ hash } isDisabled={ isMobile }> <Tooltip label={ hash } isDisabled={ isMobile }>
<Box overflow="hidden" textOverflow="ellipsis" whiteSpace="nowrap">{ alias }</Box> { text }
</Tooltip> </Tooltip>
); );
} }
......
...@@ -10,10 +10,11 @@ type Props = { ...@@ -10,10 +10,11 @@ type Props = {
className?: string; className?: string;
size?: 'xs' | 'sm' | 'md' | 'lg'; size?: 'xs' | 'sm' | 'md' | 'lg';
placeholder: string; placeholder: string;
initialValue?: string;
} }
const FilterInput = ({ onChange, className, size = 'sm', placeholder }: Props) => { const FilterInput = ({ onChange, className, size = 'sm', placeholder, initialValue }: Props) => {
const [ filterQuery, setFilterQuery ] = useState(''); const [ filterQuery, setFilterQuery ] = useState(initialValue || '');
const inputRef = React.useRef<HTMLInputElement>(null); const inputRef = React.useRef<HTMLInputElement>(null);
const iconColor = useColorModeValue('blackAlpha.600', 'whiteAlpha.600'); const iconColor = useColorModeValue('blackAlpha.600', 'whiteAlpha.600');
......
import type { PopoverContentProps } from '@chakra-ui/react';
import {
Popover,
PopoverTrigger,
PopoverContent,
PopoverBody,
useDisclosure,
} from '@chakra-ui/react';
import React from 'react';
import FilterButton from 'ui/shared/filters/FilterButton';
interface Props {
appliedFiltersNum?: number;
isActive?: boolean;
children: React.ReactNode;
contentProps?: PopoverContentProps;
}
const PopoverFilter = ({ appliedFiltersNum, children, contentProps, isActive }: Props) => {
const { isOpen, onToggle, onClose } = useDisclosure();
return (
<Popover isOpen={ isOpen } onClose={ onClose } placement="bottom-start" isLazy>
<PopoverTrigger>
<FilterButton
isActive={ isOpen || isActive || Number(appliedFiltersNum) > 0 }
onClick={ onToggle }
appliedFiltersNum={ appliedFiltersNum }
/>
</PopoverTrigger>
<PopoverContent { ...contentProps }>
<PopoverBody px={ 4 } py={ 6 } display="flex" flexDir="column" rowGap={ 5 }>
{ children }
</PopoverBody>
</PopoverContent>
</Popover>
);
};
export default React.memo(PopoverFilter);
import { CheckboxGroup, Checkbox, Text } from '@chakra-ui/react';
import React from 'react';
import type { TokenType } from 'types/api/tokenInfo';
import TOKEN_TYPE from 'lib/token/tokenTypes';
type Props = {
onChange: (nextValue: Array<TokenType>) => void;
defaultValue?: Array<TokenType>;
}
const TokenTypeFilter = ({ onChange, defaultValue }: Props) => {
return (
<CheckboxGroup size="lg" onChange={ onChange } defaultValue={ defaultValue }>
{ TOKEN_TYPE.map(({ title, id }) => (
<Checkbox key={ id } value={ id }>
<Text fontSize="md">{ title }</Text>
</Checkbox>
)) }
</CheckboxGroup>
);
};
export default TokenTypeFilter;
...@@ -4,7 +4,7 @@ import React from 'react'; ...@@ -4,7 +4,7 @@ import React from 'react';
import type { StatsChartsSection } from 'types/api/stats'; import type { StatsChartsSection } from 'types/api/stats';
import type { StatsInterval, StatsIntervalIds } from 'types/client/stats'; import type { StatsInterval, StatsIntervalIds } from 'types/client/stats';
import FilterInput from 'ui/shared/FilterInput'; import FilterInput from 'ui/shared/filters/FilterInput';
import { STATS_INTERVALS } from './constants'; import { STATS_INTERVALS } from './constants';
import StatsDropdownMenu from './StatsDropdownMenu'; import StatsDropdownMenu from './StatsDropdownMenu';
......
import { Box } from '@chakra-ui/react';
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import * as tokens from 'mocks/tokens/tokenInfo';
import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import Tokens from './Tokens';
const API_URL_TOKENS = buildApiUrl('tokens');
const tokensResponse = {
items: [
tokens.tokenInfoERC20a, tokens.tokenInfoERC20b, tokens.tokenInfoERC20c, tokens.tokenInfoERC20d,
tokens.tokenInfoERC721a, tokens.tokenInfoERC721b, tokens.tokenInfoERC721c,
tokens.tokenInfoERC1155a, tokens.tokenInfoERC1155b, tokens.tokenInfoERC1155WithoutName,
],
next_page_params: {
holder_count: 1,
items_count: 1,
name: 'a',
},
};
test('base view +@mobile +@dark-mode', async({ mount, page }) => {
await page.route(API_URL_TOKENS, (route) => route.fulfill({
status: 200,
body: JSON.stringify(tokensResponse),
}));
const component = await mount(
<TestApp>
<Box h={{ base: '134px', lg: 6 }}/>
<Tokens/>
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
import { Hide, HStack, Show, Text } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React, { useCallback } from 'react';
import type { TokenType } from 'types/api/tokenInfo';
import getFilterValuesFromQuery from 'lib/getFilterValuesFromQuery';
import useDebounce from 'lib/hooks/useDebounce';
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import { apos } from 'lib/html-entities';
import TOKEN_TYPE from 'lib/token/tokenTypes';
import EmptySearchResult from 'ui/apps/EmptySearchResult';
import ActionBar from 'ui/shared/ActionBar';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import FilterInput from 'ui/shared/filters/FilterInput';
import PopoverFilter from 'ui/shared/filters/PopoverFilter';
import TokenTypeFilter from 'ui/shared/filters/TokenTypeFilter';
import Pagination 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';
const TOKEN_TYPES = TOKEN_TYPE.map(i => i.id);
const getTokenFilterValue = (getFilterValuesFromQuery<TokenType>).bind(null, TOKEN_TYPES);
const Tokens = () => {
const router = useRouter();
const [ filter, setFilter ] = React.useState<string>(router.query.filter?.toString() || '');
const [ type, setType ] = React.useState<Array<TokenType> | undefined>(getTokenFilterValue(router.query.type));
const debouncedFilter = useDebounce(filter, 300);
const { isError, isLoading, data, isPaginationVisible, pagination, onFilterChange } = useQueryWithPages({
resourceName: 'tokens',
filters: { filter: debouncedFilter, type },
});
const onSearchChange = useCallback((value: string) => {
onFilterChange({ filter: value, type });
setFilter(value);
}, [ type, onFilterChange ]);
const onTypeChange = useCallback((value: Array<TokenType>) => {
onFilterChange({ filter: debouncedFilter, type: value });
setType(value);
}, [ debouncedFilter, onFilterChange ]);
if (isError) {
return <DataFetchAlert/>;
}
const typeFilter = (
<PopoverFilter isActive={ type && type.length > 0 } contentProps={{ w: '200px' }}>
<TokenTypeFilter onChange={ onTypeChange } defaultValue={ type }/>
</PopoverFilter>
);
const filterInput = (
<FilterInput
w="100%"
size="xs"
onChange={ onSearchChange }
placeholder="Token name or symbol"
initialValue={ filter }
/>
);
const bar = (
<>
<Show below="lg">
<HStack spacing={ 3 } mb={ 6 }>
{ typeFilter }
{ filterInput }
</HStack>
</Show>
<ActionBar mt={ -6 }>
<Hide below="lg">
<HStack spacing={ 3 }>
{ typeFilter }
{ filterInput }
</HStack>
</Hide>
{ isPaginationVisible && <Pagination ml="auto" { ...pagination }/> }
</ActionBar>
</>
);
if (isLoading) {
return (
<>
{ bar }
<Show below="lg"><SkeletonList/></Show>
<Hide below="lg">
<SkeletonTable columns={ [ '25px', '33%', '33%', '33%', '110px' ] }/>
</Hide>
</>
);
}
if (!data.items.length) {
if (debouncedFilter) {
return (
<>
{ bar }
<EmptySearchResult text={ `Couldn${ apos }t find token that matches your filter query.` }/>;
</>
);
}
return <Text as="span">There are no tokens</Text>;
}
return (
<>
{ bar }
<Show below="lg" ssr={ false }>
{ data.items.map((item, index) => <TokensListItem key={ item.address } token={ item } index={ index } page={ pagination.page }/>) }
</Show>
<Hide below="lg" ssr={ false }><TokensTable items={ data.items } page={ pagination.page }/></Hide>
</>
);
};
export default Tokens;
import { Flex, Text, Tag, HStack, Grid, GridItem } from '@chakra-ui/react';
import React from 'react';
import type { TokenInfo } from 'types/api/tokenInfo';
import getCurrencyValue from 'lib/getCurrencyValue';
import AddressAddToMetaMask from 'ui/address/details/AddressAddToMetaMask';
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 = {
token: TokenInfo;
index: number;
page: number;
}
const PAGE_SIZE = 50;
const TokensTableItem = ({
token,
page,
index,
}: Props) => {
const {
address,
total_supply: totalSupply,
exchange_rate: exchangeRate,
type,
name,
symbol,
decimals,
holders,
} = token;
const totalValue = totalSupply !== null ? getCurrencyValue({ value: totalSupply, accuracy: 3, accuracyUsd: 2, exchangeRate, decimals }) : undefined;
const tokenString = [ name, symbol && `(${ symbol })` ].filter(Boolean).join(' ');
return (
<ListItemMobile rowGap={ 3 }>
<Grid
width="100%"
gridTemplateColumns="minmax(0, 1fr)"
>
<GridItem display="flex">
<Flex overflow="hidden" mr={ 3 } alignItems="center">
<TokenLogo hash={ address } name={ name } boxSize={ 6 } mr={ 2 }/>
<AddressLink fontSize="sm" fontWeight="700" hash={ address } type="token" alias={ tokenString }/>
<Tag flexShrink={ 0 } ml={ 3 }>{ type }</Tag>
</Flex>
<Text fontSize="sm" ml="auto" variant="secondary">{ (page - 1) * PAGE_SIZE + index + 1 }</Text>
</GridItem>
</Grid>
<Flex justifyContent="space-between" alignItems="center" width="100%">
<Flex alignItems="center" width="136px" justifyContent="space-between" ml={ 8 } mt="-8px">
<Flex alignItems="center">
<AddressLink fontSize="sm" hash={ address } type="address" truncation="constant"/>
<CopyToClipboard text={ address } ml={ 1 }/>
</Flex>
<AddressAddToMetaMask token={ token }/>
</Flex>
</Flex>
{ exchangeRate && (
<HStack spacing={ 3 }>
<Text fontSize="sm" fontWeight={ 500 }>Price</Text>
<Text fontSize="sm" variant="secondary">{ exchangeRate || '-' }</Text>
</HStack>
) }
{ totalValue?.usd && (
<HStack spacing={ 3 }>
<Text fontSize="sm" fontWeight={ 500 }>On-chain market cap</Text>
<Text fontSize="sm" variant="secondary">{ totalValue.usd }</Text>
</HStack>
) }
<HStack spacing={ 3 }>
<Text fontSize="sm" fontWeight={ 500 }>Holders</Text>
<Text fontSize="sm" variant="secondary">{ holders }</Text>
</HStack>
</ListItemMobile>
);
};
export default TokensTableItem;
import { Table, Tbody, Th, Tr } from '@chakra-ui/react';
import React from 'react';
import type { TokenInfo } from 'types/api/tokenInfo';
import { default as Thead } from 'ui/shared/TheadSticky';
import TokensTableItem from './TokensTableItem';
type Props = {
items: Array<TokenInfo>;
page: number;
}
const TokensTable = ({ items, page }: Props) => {
return (
<Table style={{ tableLayout: 'auto' }}>
<Thead top={ 80 }>
<Tr>
<Th>Token</Th>
<Th isNumeric>Price</Th>
<Th isNumeric>On-chain market cap</Th>
<Th isNumeric>Holders</Th>
</Tr>
</Thead>
<Tbody>
{ items.map((item, index) => (
<TokensTableItem key={ item.address } token={ item } index={ index } page={ page }/>
)) }
</Tbody>
</Table>
);
};
export default TokensTable;
import { Box, Flex, Td, Tr, Text, Tag } from '@chakra-ui/react';
import React from 'react';
import type { TokenInfo } from 'types/api/tokenInfo';
import getCurrencyValue from 'lib/getCurrencyValue';
import AddressAddToMetaMask from 'ui/address/details/AddressAddToMetaMask';
import AddressLink from 'ui/shared/address/AddressLink';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import TokenLogo from 'ui/shared/TokenLogo';
type Props = {
token: TokenInfo;
index: number;
page: number;
}
const PAGE_SIZE = 50;
const TokensTableItem = ({
token,
page,
index,
}: Props) => {
const {
address,
total_supply: totalSupply,
exchange_rate: exchangeRate,
type,
name,
symbol,
decimals,
holders,
} = token;
const totalValue = totalSupply !== null ? getCurrencyValue({ value: totalSupply, accuracy: 3, accuracyUsd: 2, exchangeRate, decimals }) : undefined;
const tokenString = [ name, symbol && `(${ symbol })` ].filter(Boolean).join(' ');
return (
<Tr>
<Td>
<Flex>
<Text
fontSize="sm"
lineHeight="24px"
fontWeight={ 600 }
mr={ 3 }
minW="28px"
>
{ (page - 1) * PAGE_SIZE + index + 1 }
</Text>
<Box>
<Flex alignItems="center">
<TokenLogo hash={ address } name={ name } boxSize={ 6 } mr={ 2 }/>
<AddressLink fontSize="sm" fontWeight="700" hash={ address } type="token" alias={ tokenString }/>
</Flex>
<Flex alignItems="center" width="136px" justifyContent="space-between" ml={ 8 } mt={ 2 }>
<Flex alignItems="center">
<AddressLink fontSize="sm" hash={ address } type="address" truncation="constant" fontWeight={ 500 }/>
<CopyToClipboard text={ address } ml={ 1 }/>
</Flex>
<AddressAddToMetaMask token={ token }/>
</Flex>
<Tag flexShrink={ 0 } ml={ 8 } mt={ 3 }>{ type }</Tag>
</Box>
</Flex>
</Td>
<Td isNumeric><Text fontSize="sm" lineHeight="24px" fontWeight={ 500 }>{ exchangeRate ? `$${ exchangeRate }` : '-' }</Text></Td>
<Td isNumeric maxWidth="300px" width="300px">
<Text fontSize="sm" lineHeight="24px" fontWeight={ 500 }>{ totalValue?.usd ? `$${ totalValue.usd }` : '-' }</Text>
</Td>
<Td isNumeric><Text fontSize="sm" lineHeight="24px" fontWeight={ 500 }>{ holders }</Text></Td>
</Tr>
);
};
export default TokensTableItem;
...@@ -10,7 +10,7 @@ import { apos } from 'lib/html-entities'; ...@@ -10,7 +10,7 @@ import { apos } from 'lib/html-entities';
import EmptySearchResult from 'ui/apps/EmptySearchResult'; import EmptySearchResult from 'ui/apps/EmptySearchResult';
import ActionBar from 'ui/shared/ActionBar'; import ActionBar from 'ui/shared/ActionBar';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
// import FilterInput from 'ui/shared/FilterInput'; // import FilterInput from 'ui/shared/filters/FilterInput';
// import TxInternalsFilter from 'ui/tx/internals/TxInternalsFilter'; // import TxInternalsFilter from 'ui/tx/internals/TxInternalsFilter';
import Pagination from 'ui/shared/Pagination'; import Pagination from 'ui/shared/Pagination';
import SkeletonList from 'ui/shared/skeletons/SkeletonList'; import SkeletonList from 'ui/shared/skeletons/SkeletonList';
......
...@@ -8,13 +8,14 @@ import { SECOND } from 'lib/consts'; ...@@ -8,13 +8,14 @@ import { SECOND } from 'lib/consts';
import getFilterValuesFromQuery from 'lib/getFilterValuesFromQuery'; import getFilterValuesFromQuery from 'lib/getFilterValuesFromQuery';
import useQueryWithPages from 'lib/hooks/useQueryWithPages'; import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import { apos } from 'lib/html-entities'; import { apos } from 'lib/html-entities';
import TOKEN_TYPE from 'lib/token/tokenTypes';
import EmptySearchResult from 'ui/apps/EmptySearchResult'; import EmptySearchResult from 'ui/apps/EmptySearchResult';
import ActionBar from 'ui/shared/ActionBar'; import ActionBar from 'ui/shared/ActionBar';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
import Pagination from 'ui/shared/Pagination'; import Pagination from 'ui/shared/Pagination';
import SkeletonList from 'ui/shared/skeletons/SkeletonList'; import SkeletonList from 'ui/shared/skeletons/SkeletonList';
import SkeletonTable from 'ui/shared/skeletons/SkeletonTable'; import SkeletonTable from 'ui/shared/skeletons/SkeletonTable';
import { TOKEN_TYPE, flattenTotal } from 'ui/shared/TokenTransfer/helpers'; import { flattenTotal } from 'ui/shared/TokenTransfer/helpers';
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';
import TokenTransferTable from 'ui/shared/TokenTransfer/TokenTransferTable'; import TokenTransferTable from 'ui/shared/TokenTransfer/TokenTransferTable';
......
import { Popover, PopoverTrigger, PopoverContent, PopoverBody, CheckboxGroup, Checkbox, Text, useDisclosure } from '@chakra-ui/react'; import { CheckboxGroup, Checkbox, Text } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { TxInternalsType } from 'types/api/internalTransaction'; import type { TxInternalsType } from 'types/api/internalTransaction';
import FilterButton from 'ui/shared/FilterButton'; import PopoverFilter from 'ui/shared/filters/PopoverFilter';
import { TX_INTERNALS_ITEMS } from 'ui/tx/internals/utils'; import { TX_INTERNALS_ITEMS } from 'ui/tx/internals/utils';
interface Props { interface Props {
...@@ -13,25 +13,12 @@ interface Props { ...@@ -13,25 +13,12 @@ interface Props {
} }
const TxInternalsFilter = ({ onFilterChange, defaultFilters, appliedFiltersNum }: Props) => { const TxInternalsFilter = ({ onFilterChange, defaultFilters, appliedFiltersNum }: Props) => {
const { isOpen, onToggle, onClose } = useDisclosure();
return ( return (
<Popover isOpen={ isOpen } onClose={ onClose } placement="bottom-start" isLazy> <PopoverFilter appliedFiltersNum={ appliedFiltersNum } contentProps={{ w: { md: '100%', lg: '438px' } }}>
<PopoverTrigger> <CheckboxGroup size="lg" onChange={ onFilterChange } defaultValue={ defaultFilters }>
<FilterButton { TX_INTERNALS_ITEMS.map(({ title, id }) => <Checkbox key={ id } value={ id }><Text fontSize="md">{ title }</Text></Checkbox>) }
isActive={ isOpen || Number(appliedFiltersNum) > 0 } </CheckboxGroup>
onClick={ onToggle } </PopoverFilter>
appliedFiltersNum={ appliedFiltersNum }
/>
</PopoverTrigger>
<PopoverContent w={{ md: '100%', lg: '438px' }}>
<PopoverBody px={ 4 } py={ 6 } display="grid" gridTemplateColumns="1fr 1fr" rowGap={ 5 }>
<CheckboxGroup size="lg" onChange={ onFilterChange } defaultValue={ defaultFilters }>
{ TX_INTERNALS_ITEMS.map(({ title, id }) => <Checkbox key={ id } value={ id }><Text fontSize="md">{ title }</Text></Checkbox>) }
</CheckboxGroup>
</PopoverBody>
</PopoverContent>
</Popover>
); );
}; };
......
import { import {
Button,
Checkbox, Checkbox,
CheckboxGroup, CheckboxGroup,
Grid, Grid,
Link,
Popover,
PopoverTrigger,
PopoverContent,
PopoverBody,
Text, Text,
useColorModeValue, useColorModeValue,
useDisclosure,
Flex,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
import type { TTxsFilters, TypeFilter, MethodFilter } from 'types/api/txsFilters'; import type { TTxsFilters, TypeFilter, MethodFilter } from 'types/api/txsFilters';
import FilterButton from 'ui/shared/FilterButton'; import PopoverFilter from 'ui/shared/filters/PopoverFilter';
interface Props { interface Props {
appliedFiltersNum?: number; appliedFiltersNum?: number;
...@@ -41,13 +33,7 @@ const METHOD_OPTIONS = [ ...@@ -41,13 +33,7 @@ const METHOD_OPTIONS = [
{ title: 'Commit', id: 'commit' }, { title: 'Commit', id: 'commit' },
]; ];
// TODO: i think we need to reload page after applying filters, const TxsFilters = ({ filters, appliedFiltersNum }: Props) => {
// because we need to reset pagination, clear query caches, reconnect websocket...
// also mobile version of filters is not implemented
const TxsFilters = ({ onFiltersChange, filters, appliedFiltersNum }: Props) => {
const { isOpen, onToggle, onClose } = useDisclosure();
const [ typeFilter, setTypeFilter ] = useState<Array<TypeFilter>>(filters.type || []); const [ typeFilter, setTypeFilter ] = useState<Array<TypeFilter>>(filters.type || []);
const [ methodFilter, setMethodFilter ] = useState<Array<MethodFilter>>(filters.method || []); const [ methodFilter, setMethodFilter ] = useState<Array<MethodFilter>>(filters.method || []);
...@@ -60,50 +46,23 @@ const TxsFilters = ({ onFiltersChange, filters, appliedFiltersNum }: Props) => { ...@@ -60,50 +46,23 @@ const TxsFilters = ({ onFiltersChange, filters, appliedFiltersNum }: Props) => {
setMethodFilter(val); setMethodFilter(val);
}, []); }, []);
const onFilterReset = useCallback(() => {
setTypeFilter([]);
setMethodFilter([]);
onFiltersChange({ type: [], method: [] });
onClose();
}, [ onClose, onFiltersChange ]);
const onFilterApply = useCallback(() => {
onFiltersChange({ type: typeFilter, method: methodFilter } as Partial<TTxsFilters>);
onClose();
}, [ onClose, onFiltersChange, typeFilter, methodFilter ]);
const borderColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200'); const borderColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200');
return ( return (
<Popover isOpen={ isOpen } onClose={ onClose } placement="bottom-start" isLazy> <PopoverFilter contentProps={{ w: { md: '100%', lg: '438px' } }} appliedFiltersNum={ appliedFiltersNum }>
<PopoverTrigger> <Text variant="secondary" fontWeight="600" fontSize="sm">Type</Text>
<FilterButton <Grid gridTemplateColumns="1fr 1fr" rowGap={ 5 } mt={ 4 } mb={ 4 } pb={ 6 } borderBottom="1px solid" borderColor={ borderColor }>
isActive={ isOpen || Number(appliedFiltersNum) > 0 } <CheckboxGroup size="lg" onChange={ onTypeFilterChange } defaultValue={ typeFilter }>
onClick={ onToggle } { TYPE_OPTIONS.map(({ title, id }) => <Checkbox key={ id } value={ id }><Text fontSize="md">{ title }</Text></Checkbox>) }
appliedFiltersNum={ appliedFiltersNum } </CheckboxGroup>
/> </Grid>
</PopoverTrigger> <Text variant="secondary" fontWeight="600" fontSize="sm">Method</Text>
<PopoverContent w={{ md: '100%', lg: '438px' }}> <Grid gridTemplateColumns="1fr 1fr" rowGap={ 5 } mt={ 4 } mb={ 4 } pb={ 6 } borderBottom="1px solid" borderColor={ borderColor }>
<PopoverBody px={ 4 } py={ 6 }> <CheckboxGroup size="lg" onChange={ onMethodFilterChange } defaultValue={ methodFilter }>
<Text variant="secondary" fontWeight="600" fontSize="sm">Type</Text> { METHOD_OPTIONS.map(({ title, id }) => <Checkbox key={ id } value={ id }><Text fontSize="md">{ title }</Text></Checkbox>) }
<Grid gridTemplateColumns="1fr 1fr" rowGap={ 5 } mt={ 4 } mb={ 4 } pb={ 6 } borderBottom="1px solid" borderColor={ borderColor }> </CheckboxGroup>
<CheckboxGroup size="lg" onChange={ onTypeFilterChange } defaultValue={ typeFilter }> </Grid>
{ TYPE_OPTIONS.map(({ title, id }) => <Checkbox key={ id } value={ id }><Text fontSize="md">{ title }</Text></Checkbox>) } </PopoverFilter>
</CheckboxGroup>
</Grid>
<Text variant="secondary" fontWeight="600" fontSize="sm">Method</Text>
<Grid gridTemplateColumns="1fr 1fr" rowGap={ 5 } mt={ 4 } mb={ 4 } pb={ 6 } borderBottom="1px solid" borderColor={ borderColor }>
<CheckboxGroup size="lg" onChange={ onMethodFilterChange } defaultValue={ methodFilter }>
{ METHOD_OPTIONS.map(({ title, id }) => <Checkbox key={ id } value={ id }><Text fontSize="md">{ title }</Text></Checkbox>) }
</CheckboxGroup>
</Grid>
<Flex alignItems="center" justifyContent="space-between">
<Link fontSize="sm" onClick={ onFilterReset }>Reset filters</Link>
<Button variant="outline" size="sm" onClick={ onFilterApply }>Apply</Button>
</Flex>
</PopoverBody>
</PopoverContent>
</Popover>
); );
}; };
......
...@@ -3,7 +3,7 @@ import React from 'react'; ...@@ -3,7 +3,7 @@ import React from 'react';
import type { Sort } from 'types/client/txs-sort'; import type { Sort } from 'types/client/txs-sort';
// import FilterInput from 'ui/shared/FilterInput'; // import FilterInput from 'ui/shared/filters/FilterInput';
import ActionBar from 'ui/shared/ActionBar'; import ActionBar from 'ui/shared/ActionBar';
import Pagination from 'ui/shared/Pagination'; import Pagination from 'ui/shared/Pagination';
import type { Props as PaginationProps } from 'ui/shared/Pagination'; import type { Props as PaginationProps } from 'ui/shared/Pagination';
......
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