Commit 695a0322 authored by tom's avatar tom

tokens page

parent b9417e2e
......@@ -4,12 +4,12 @@ import React from 'react';
import PageNextJs from 'nextjs/PageNextJs';
// const Tokens = dynamic(() => import('ui/pages/Tokens'), { ssr: false });
const Tokens = dynamic(() => import('ui/pages/Tokens'), { ssr: false });
const Page: NextPage = () => {
return (
<PageNextJs pathname="/tokens">
{ /* <Tokens/> */ }
<Tokens/>
</PageNextJs>
);
};
......
......@@ -34,4 +34,4 @@ export interface TokensSorting {
export type TokensSortingField = TokensSorting['sort'];
export type TokensSortingValue = `${ TokensSortingField }-${ TokensSorting['order'] }`;
export type TokensSortingValue = `${ TokensSortingField }-${ TokensSorting['order'] }` | 'default';
......@@ -2,9 +2,9 @@ import { Box } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';
import type { TabItemRegular } from 'toolkit/components/AdaptiveTabs/types';
import type { TokenType } from 'types/api/token';
import type { TokensSortingValue, TokensSortingField, TokensSorting } from 'types/api/tokens';
import type { RoutedTab } from 'ui/shared/Tabs/types';
import config from 'configs/app';
import useDebounce from 'lib/hooks/useDebounce';
......@@ -12,13 +12,13 @@ import useIsMobile from 'lib/hooks/useIsMobile';
import getQueryParamString from 'lib/router/getQueryParamString';
import { TOKEN_INFO_ERC_20 } from 'stubs/token';
import { generateListStub } from 'stubs/utils';
import RoutedTabs from 'toolkit/components/RoutedTabs/RoutedTabs';
import PopoverFilter from 'ui/shared/filters/PopoverFilter';
import TokenTypeFilter from 'ui/shared/filters/TokenTypeFilter';
import PageTitle from 'ui/shared/Page/PageTitle';
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
import getSortParamsFromValue from 'ui/shared/sort/getSortParamsFromValue';
import getSortValueFromQuery from 'ui/shared/sort/getSortValueFromQuery';
import RoutedTabs from 'ui/shared/Tabs/RoutedTabs';
import TokensList from 'ui/tokens/Tokens';
import TokensActionBar from 'ui/tokens/TokensActionBar';
import TokensBridgedChainsFilter from 'ui/tokens/TokensBridgedChainsFilter';
......@@ -48,7 +48,7 @@ const Tokens = () => {
const q = getQueryParamString(router.query.q);
const [ searchTerm, setSearchTerm ] = React.useState<string>(q ?? '');
const [ sort, setSort ] = React.useState<TokensSortingValue | undefined>(getSortValueFromQuery<TokensSortingValue>(router.query, SORT_OPTIONS));
const [ sort, setSort ] = React.useState<TokensSortingValue>(getSortValueFromQuery<TokensSortingValue>(router.query, SORT_OPTIONS) ?? 'default');
const [ tokenTypes, setTokenTypes ] = React.useState<Array<TokenType> | undefined>(getTokenFilterValue(router.query.type));
const [ bridgeChains, setBridgeChains ] = React.useState<Array<string> | undefined>(getBridgedChainsFilterValue(router.query.chain_ids));
......@@ -91,14 +91,14 @@ const Tokens = () => {
setBridgeChains(value);
}, [ debouncedSearchTerm, tokensQuery ]);
const handleSortChange = React.useCallback((value?: TokensSortingValue) => {
const handleSortChange = React.useCallback((value: TokensSortingValue) => {
setSort(value);
tokensQuery.onSortingChange(getSortParamsFromValue(value));
}, [ tokensQuery ]);
const handleTabChange = React.useCallback(() => {
setSearchTerm('');
setSort(undefined);
setSort('default');
setTokenTypes(undefined);
setBridgeChains(undefined);
}, []);
......@@ -144,7 +144,7 @@ const Tokens = () => {
);
})();
const tabs: Array<RoutedTab> = [
const tabs: Array<TabItemRegular> = [
{
id: 'all',
title: 'All',
......@@ -185,11 +185,11 @@ const Tokens = () => {
{ !hasMultipleTabs && !isMobile && actionBar }
<RoutedTabs
tabs={ tabs }
tabListProps={ isMobile ? undefined : TAB_LIST_PROPS }
listProps={ isMobile ? undefined : TAB_LIST_PROPS }
rightSlot={ hasMultipleTabs && !isMobile ? actionBar : null }
rightSlotProps={ !isMobile ? TABS_RIGHT_SLOT_PROPS : undefined }
stickyEnabled={ !isMobile }
onTabChange={ handleTabChange }
onValueChange={ handleTabChange }
/>
</>
);
......
export default function getSortParamsFromValue<SortValue extends string, SortField extends string, SortOrder extends string>(val?: SortValue) {
if (!val) {
if (!val || val === 'default') {
return undefined;
}
......
......@@ -22,6 +22,8 @@ const txSortingOptions = createListCollection({
items: SORT_OPTIONS,
});
// TODO @tom2drum + tanya: select with search
const SelectShowcase = () => {
const [ hasActiveFilter, setHasActiveFilter ] = React.useState(false);
......
import { Hide, Show } from '@chakra-ui/react';
import { Box } from '@chakra-ui/react';
import React from 'react';
import type { TokensSortingValue } from 'types/api/tokens';
......@@ -13,8 +13,8 @@ import TokensTable from './TokensTable';
interface Props {
query: QueryWithPagesResult<'tokens'> | QueryWithPagesResult<'tokens_bridged'>;
onSortChange: () => void;
sort: TokensSortingValue | undefined;
onSortChange: (value: TokensSortingValue) => void;
sort: TokensSortingValue;
actionBar?: React.ReactNode;
hasActiveFilters: boolean;
description?: React.ReactNode;
......@@ -31,7 +31,7 @@ const Tokens = ({ query, onSortChange, sort, actionBar, description, hasActiveFi
const content = data?.items ? (
<>
<Show below="lg" ssr={ false }>
<Box hideFrom="lg">
{ description }
{ data.items.map((item, index) => (
<TokensListItem
......@@ -42,8 +42,8 @@ const Tokens = ({ query, onSortChange, sort, actionBar, description, hasActiveFi
isLoading={ isPlaceholderData }
/>
)) }
</Show>
<Hide below="lg" ssr={ false }>
</Box>
<Box hideBelow="lg">
{ description }
<TokensTable
items={ data.items }
......@@ -53,22 +53,23 @@ const Tokens = ({ query, onSortChange, sort, actionBar, description, hasActiveFi
sorting={ sort }
top={ tableTop }
/>
</Hide>
</Box>
</>
) : null;
return (
<DataListDisplay
isError={ isError }
items={ data?.items }
itemsNum={ data?.items.length }
emptyText="There are no tokens."
filterProps={{
emptyFilteredText: `Couldn${ apos }t find token that matches your filter query.`,
hasActiveFilters,
}}
content={ content }
actionBar={ query.pagination.isVisible || hasActiveFilters ? actionBar : null }
/>
>
{ content }
</DataListDisplay>
);
};
......
import { HStack } from '@chakra-ui/react';
import { createListCollection, HStack } from '@chakra-ui/react';
import React from 'react';
import type { TokensSortingValue } from 'types/api/tokens';
......@@ -10,12 +10,16 @@ import Pagination from 'ui/shared/pagination/Pagination';
import Sort from 'ui/shared/sort/Sort';
import { SORT_OPTIONS } from 'ui/tokens/utils';
const sortCollection = createListCollection({
items: SORT_OPTIONS,
});
interface Props {
pagination: PaginationParams;
searchTerm: string | undefined;
onSearchChange: (value: string) => void;
sort: TokensSortingValue | undefined;
onSortChange: () => void;
sort: TokensSortingValue;
onSortChange: (value: TokensSortingValue) => void;
filter: React.ReactNode;
inTabsSlot?: boolean;
}
......@@ -30,10 +34,14 @@ const TokensActionBar = ({
inTabsSlot,
}: Props) => {
const handleSortChange = React.useCallback(({ value }: { value: Array<string> }) => {
onSortChange(value[0] as TokensSortingValue);
}, [ onSortChange ]);
const searchInput = (
<FilterInput
w={{ base: '100%', lg: '360px' }}
size="xs"
size="sm"
onChange={ onSearchChange }
placeholder="Token name or symbol"
initialValue={ searchTerm }
......@@ -42,13 +50,13 @@ const TokensActionBar = ({
return (
<>
<HStack spacing={ 3 } mb={ 6 } display={{ base: 'flex', lg: 'none' }}>
<HStack gap={ 3 } mb={ 6 } display={{ base: 'flex', lg: 'none' }}>
{ filter }
<Sort
name="tokens_sorting"
defaultValue={ sort }
options={ SORT_OPTIONS }
onChange={ onSortChange }
defaultValue={ [ sort ] }
collection={ sortCollection }
onValueChange={ handleSortChange }
/>
{ searchInput }
</HStack>
......@@ -58,7 +66,7 @@ const TokensActionBar = ({
justifyContent={ inTabsSlot ? 'space-between' : undefined }
display={{ base: pagination.isVisible ? 'flex' : 'none', lg: 'flex' }}
>
<HStack spacing={ 3 } display={{ base: 'none', lg: 'flex' }}>
<HStack gap={ 3 } display={{ base: 'none', lg: 'flex' }}>
{ filter }
{ searchInput }
</HStack>
......
import { CheckboxGroup, Checkbox, Text, Flex, Link, useCheckboxGroup, chakra } from '@chakra-ui/react';
import { CheckboxGroup, Text, Flex, useCheckboxGroup, chakra, Fieldset } from '@chakra-ui/react';
import React from 'react';
import config from 'configs/app';
import { Checkbox } from 'toolkit/chakra/checkbox';
import { Link } from 'toolkit/chakra/link';
const feature = config.features.bridgedTokens;
......@@ -33,7 +35,7 @@ const TokensBridgedChainsFilter = ({ onChange, defaultValue }: Props) => {
return (
<>
<Flex justifyContent="space-between" fontSize="sm">
<Text fontWeight={ 600 } variant="secondary">Show bridged tokens from</Text>
<Text fontWeight={ 600 } color="text.secondary">Show bridged tokens from</Text>
<Link
onClick={ handleReset }
color={ value.length > 0 ? 'link' : 'text_secondary' }
......@@ -44,14 +46,18 @@ const TokensBridgedChainsFilter = ({ onChange, defaultValue }: Props) => {
Reset
</Link>
</Flex>
<CheckboxGroup size="lg" onChange={ handleChange } value={ value }>
{ feature.chains.map(({ title, id, short_title: shortTitle }) => (
<Checkbox key={ id } value={ id } fontSize="md" whiteSpace="pre-wrap">
<span>{ title }</span>
<chakra.span color="text_secondary"> ({ shortTitle })</chakra.span>
</Checkbox>
)) }
</CheckboxGroup>
<Fieldset.Root>
<CheckboxGroup defaultValue={ defaultValue } onValueChange={ handleChange } value={ value } name="bridged_token_chain">
<Fieldset.Content>
{ feature.chains.map(({ title, id, short_title: shortTitle }) => (
<Checkbox key={ id } value={ id } textStyle="md" whiteSpace="pre-wrap">
<span>{ title }</span>
<chakra.span color="text_secondary"> ({ shortTitle })</chakra.span>
</Checkbox>
)) }
</Fieldset.Content>
</CheckboxGroup>
</Fieldset.Root>
</>
);
};
......
......@@ -7,9 +7,9 @@ import type { TokenInfo } from 'types/api/token';
import config from 'configs/app';
import getItemIndex from 'lib/getItemIndex';
import { getTokenTypeName } from 'lib/token/tokenTypes';
import { Skeleton } from 'toolkit/chakra/skeleton';
import { Tag } from 'toolkit/chakra/tag';
import AddressAddToWallet from 'ui/shared/address/AddressAddToWallet';
import Skeleton from 'ui/shared/chakra/Skeleton';
import Tag from 'ui/shared/chakra/Tag';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import TokenEntity from 'ui/shared/entities/token/TokenEntity';
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
......@@ -57,14 +57,14 @@ const TokensTableItem = ({
jointSymbol
noCopy
w="auto"
fontSize="sm"
textStyle="sm"
fontWeight="700"
/>
<Flex ml={ 3 } flexShrink={ 0 } columnGap={ 1 }>
<Tag isLoading={ isLoading }>{ getTokenTypeName(type) }</Tag>
{ bridgedChainTag && <Tag isLoading={ isLoading }>{ bridgedChainTag }</Tag> }
<Tag loading={ isLoading }>{ getTokenTypeName(type) }</Tag>
{ bridgedChainTag && <Tag loading={ isLoading }>{ bridgedChainTag }</Tag> }
</Flex>
<Skeleton isLoaded={ !isLoading } fontSize="sm" ml="auto" color="text_secondary" minW="24px" textAlign="right" lineHeight={ 6 }>
<Skeleton loading={ isLoading } textStyle="sm" ml="auto" color="text.secondary" minW="24px" textAlign="right">
<span>{ getItemIndex(index, page) }</span>
</Skeleton>
</GridItem>
......@@ -79,22 +79,22 @@ const TokensTableItem = ({
<AddressAddToWallet token={ token } isLoading={ isLoading }/>
</Flex>
{ exchangeRate && (
<HStack spacing={ 3 }>
<Skeleton isLoaded={ !isLoading } fontSize="sm" fontWeight={ 500 }>Price</Skeleton>
<Skeleton isLoaded={ !isLoading } fontSize="sm" color="text_secondary">
<HStack gap={ 3 }>
<Skeleton loading={ isLoading } textStyle="sm" fontWeight={ 500 }>Price</Skeleton>
<Skeleton loading={ isLoading } textStyle="sm" color="text.secondary">
<span>${ Number(exchangeRate).toLocaleString(undefined, { minimumSignificantDigits: 4 }) }</span>
</Skeleton>
</HStack>
) }
{ marketCap && (
<HStack spacing={ 3 }>
<Skeleton isLoaded={ !isLoading } fontSize="sm" fontWeight={ 500 }>On-chain market cap</Skeleton>
<Skeleton isLoaded={ !isLoading } fontSize="sm" color="text_secondary"><span>{ BigNumber(marketCap).toFormat() }</span></Skeleton>
<HStack gap={ 3 }>
<Skeleton loading={ isLoading } textStyle="sm" fontWeight={ 500 }>On-chain market cap</Skeleton>
<Skeleton loading={ isLoading } textStyle="sm" color="text.secondary"><span>{ BigNumber(marketCap).toFormat() }</span></Skeleton>
</HStack>
) }
<HStack spacing={ 3 }>
<Skeleton isLoaded={ !isLoading } fontSize="sm" fontWeight={ 500 }>Holders</Skeleton>
<Skeleton isLoaded={ !isLoading } fontSize="sm" color="text_secondary"><span>{ Number(holders).toLocaleString() }</span></Skeleton>
<HStack gap={ 3 }>
<Skeleton loading={ isLoading } textStyle="sm" fontWeight={ 500 }>Holders</Skeleton>
<Skeleton loading={ isLoading } textStyle="sm" color="text.secondary"><span>{ Number(holders).toLocaleString() }</span></Skeleton>
</HStack>
</ListItemMobile>
);
......
import { Link, Table, Tbody, Th, Tr } from '@chakra-ui/react';
import React from 'react';
import type { TokenInfo } from 'types/api/token';
import type { TokensSortingField, TokensSortingValue } from 'types/api/tokens';
import { Link } from 'toolkit/chakra/link';
import { TableBody, TableColumnHeader, TableHeaderSticky, TableRoot, TableRow } from 'toolkit/chakra/table';
import { ACTION_BAR_HEIGHT_DESKTOP } from 'ui/shared/ActionBar';
import IconSvg from 'ui/shared/IconSvg';
import { default as getNextSortValueShared } from 'ui/shared/sort/getNextSortValue';
import { default as Thead } from 'ui/shared/TheadSticky';
import TokensTableItem from './TokensTableItem';
const SORT_SEQUENCE: Record<TokensSortingField, Array<TokensSortingValue | undefined>> = {
fiat_value: [ 'fiat_value-desc', 'fiat_value-asc', undefined ],
holder_count: [ 'holder_count-desc', 'holder_count-asc', undefined ],
circulating_market_cap: [ 'circulating_market_cap-desc', 'circulating_market_cap-asc', undefined ],
const SORT_SEQUENCE: Record<TokensSortingField, Array<TokensSortingValue>> = {
fiat_value: [ 'fiat_value-desc', 'fiat_value-asc', 'default' ],
holder_count: [ 'holder_count-desc', 'holder_count-asc', 'default' ],
circulating_market_cap: [ 'circulating_market_cap-desc', 'circulating_market_cap-asc', 'default' ],
};
const getNextSortValue = (getNextSortValueShared<TokensSortingField, TokensSortingValue>).bind(undefined, SORT_SEQUENCE);
......@@ -22,8 +22,8 @@ const getNextSortValue = (getNextSortValueShared<TokensSortingField, TokensSorti
type Props = {
items: Array<TokenInfo>;
page: number;
sorting?: TokensSortingValue;
setSorting: (val?: TokensSortingValue) => void;
sorting: TokensSortingValue;
setSorting: (value: TokensSortingValue) => void;
isLoading?: boolean;
top?: number;
};
......@@ -37,36 +37,36 @@ const TokensTable = ({ items, page, isLoading, sorting, setSorting, top }: Props
}, [ sorting, setSorting ]);
return (
<Table>
<Thead top={ top ?? ACTION_BAR_HEIGHT_DESKTOP }>
<Tr>
<Th w="50%">Token</Th>
<Th isNumeric w="15%">
<TableRoot>
<TableHeaderSticky top={ top ?? ACTION_BAR_HEIGHT_DESKTOP }>
<TableRow>
<TableColumnHeader w="50%">Token</TableColumnHeader>
<TableColumnHeader isNumeric w="15%">
<Link onClick={ sort('fiat_value') } display="flex" justifyContent="end">
{ sorting?.includes('fiat_value') && <IconSvg name="arrows/east-mini" boxSize={ 4 } transform={ sortIconTransform }/> }
Price
</Link>
</Th>
<Th isNumeric w="20%">
</TableColumnHeader>
<TableColumnHeader isNumeric w="20%">
<Link onClick={ sort('circulating_market_cap') } display="flex" justifyContent="end">
{ sorting?.includes('circulating_market_cap') && <IconSvg name="arrows/east-mini" boxSize={ 4 } transform={ sortIconTransform }/> }
On-chain market cap
</Link>
</Th>
<Th isNumeric w="15%">
</TableColumnHeader>
<TableColumnHeader isNumeric w="15%">
<Link onClick={ sort('holder_count') } display="flex" justifyContent="end">
{ sorting?.includes('holder_count') && <IconSvg name="arrows/east-mini" boxSize={ 4 } transform={ sortIconTransform }/> }
Holders
</Link>
</Th>
</Tr>
</Thead>
<Tbody>
</TableColumnHeader>
</TableRow>
</TableHeaderSticky>
<TableBody>
{ items.map((item, index) => (
<TokensTableItem key={ item.address + (isLoading ? index : '') } token={ item } index={ index } page={ page } isLoading={ isLoading }/>
)) }
</Tbody>
</Table>
</TableBody>
</TableRoot>
);
};
......
import { Flex, Td, Tr } from '@chakra-ui/react';
import { Flex } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import React from 'react';
......@@ -7,9 +7,10 @@ import type { TokenInfo } from 'types/api/token';
import config from 'configs/app';
import getItemIndex from 'lib/getItemIndex';
import { getTokenTypeName } from 'lib/token/tokenTypes';
import { Skeleton } from 'toolkit/chakra/skeleton';
import { TableCell, TableRow } from 'toolkit/chakra/table';
import { Tag } from 'toolkit/chakra/tag';
import AddressAddToWallet from 'ui/shared/address/AddressAddToWallet';
import Skeleton from 'ui/shared/chakra/Skeleton';
import Tag from 'ui/shared/chakra/Tag';
import type { EntityProps as AddressEntityProps } from 'ui/shared/entities/address/AddressEntity';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import TokenEntity from 'ui/shared/entities/token/TokenEntity';
......@@ -57,15 +58,12 @@ const TokensTableItem = ({
};
return (
<Tr
role="group"
>
<Td>
<TableRow className="group">
<TableCell>
<Flex alignItems="flex-start">
<Skeleton
isLoaded={ !isLoading }
fontSize="sm"
lineHeight="20px"
loading={ isLoading }
textStyle="sm"
fontWeight={ 600 }
mr={ 3 }
minW="28px"
......@@ -78,7 +76,7 @@ const TokensTableItem = ({
isLoading={ isLoading }
jointSymbol
noCopy
fontSize="sm"
textStyle="sm"
fontWeight="700"
/>
<Flex columnGap={ 2 } py="5px" alignItems="center">
......@@ -86,7 +84,7 @@ const TokensTableItem = ({
address={ tokenAddress }
isLoading={ isLoading }
noIcon
fontSize="sm"
textStyle="sm"
fontWeight={ 500 }
/>
<AddressAddToWallet
......@@ -98,34 +96,33 @@ const TokensTableItem = ({
/>
</Flex>
<Flex columnGap={ 1 }>
<Tag isLoading={ isLoading }>{ getTokenTypeName(type) }</Tag>
{ bridgedChainTag && <Tag isLoading={ isLoading }>{ bridgedChainTag }</Tag> }
<Tag loading={ isLoading }>{ getTokenTypeName(type) }</Tag>
{ bridgedChainTag && <Tag loading={ isLoading }>{ bridgedChainTag }</Tag> }
</Flex>
</Flex>
</Flex>
</Td>
<Td isNumeric>
<Skeleton isLoaded={ !isLoading } fontSize="sm" lineHeight="24px" fontWeight={ 500 } display="inline-block">
</TableCell>
<TableCell isNumeric>
<Skeleton loading={ isLoading } textStyle="sm" fontWeight={ 500 } display="inline-block">
{ exchangeRate && `$${ Number(exchangeRate).toLocaleString(undefined, { minimumSignificantDigits: 4 }) }` }
</Skeleton>
</Td>
<Td isNumeric maxWidth="300px" width="300px">
<Skeleton isLoaded={ !isLoading } fontSize="sm" lineHeight="24px" fontWeight={ 500 } display="inline-block">
</TableCell>
<TableCell isNumeric maxWidth="300px" width="300px">
<Skeleton loading={ isLoading } textStyle="sm" fontWeight={ 500 } display="inline-block">
{ marketCap && `$${ BigNumber(marketCap).toFormat() }` }
</Skeleton>
</Td>
<Td isNumeric>
</TableCell>
<TableCell isNumeric>
<Skeleton
isLoaded={ !isLoading }
fontSize="sm"
lineHeight="24px"
loading={ isLoading }
textStyle="sm"
fontWeight={ 500 }
display="inline-block"
>
{ Number(holders).toLocaleString() }
</Skeleton>
</Td>
</Tr>
</TableCell>
</TableRow>
);
};
......
......@@ -7,7 +7,7 @@ import { TOKEN_TYPE_IDS } from 'lib/token/tokenTypes';
import type { SelectOption } from 'toolkit/chakra/select';
export const SORT_OPTIONS: Array<SelectOption<TokensSortingValue>> = [
{ label: 'Default', value: undefined },
{ label: 'Default', value: 'default' },
{ label: 'Price ascending', value: 'fiat_value-asc' },
{ label: 'Price descending', value: 'fiat_value-desc' },
{ label: 'Holders ascending', value: 'holder_count-asc' },
......
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