diff --git a/src/appConstants/apiFields.ts b/src/appConstants/apiFields.ts index 2f760344f..f67264a97 100644 --- a/src/appConstants/apiFields.ts +++ b/src/appConstants/apiFields.ts @@ -46,3 +46,19 @@ export const NODE_STATUS_PREVIEW_FIELDS = [ 'auctionQualified', 'isInDangerZone' ]; + +export const ACCOUNT_TOKENS_FIELDS = [ + 'type', + 'identifier', + 'collection', + 'name', + 'ticker', + 'decimals', + 'assets', + 'price', + 'totalLiquidity', + 'isLowLiquidity', + 'lowLiquidityThresholdPercent', + 'balance', + 'valueUsd' +]; diff --git a/src/assets/scss/components/_pages.scss b/src/assets/scss/components/_pages.scss index 25af80445..f71135710 100644 --- a/src/assets/scss/components/_pages.scss +++ b/src/assets/scss/components/_pages.scss @@ -1,4 +1,5 @@ @import '../../../pages/AccountDetails/AccountStaking/accountStaking.styles.scss'; +@import '../../../pages/AccountDetails/AccountTokensTable/accountTokensTable.styles.scss'; @import '../../../pages/Analytics/analytics.styles.scss'; @import '../../../pages/BlockDetails/blockDetails.styles.scss'; @import '../../../pages/Identities/identities.styles.scss'; diff --git a/src/assets/scss/elements/_badges.scss b/src/assets/scss/elements/_badges.scss index 86e556808..870696a31 100644 --- a/src/assets/scss/elements/_badges.scss +++ b/src/assets/scss/elements/_badges.scss @@ -144,6 +144,12 @@ button.badge { rgba(215, 221, 232, 0.3) ); } + &.active { + @extend .badge-grey; + &:before { + display: none; + } + } } &-primary { color: var(--primary); diff --git a/src/components/Filters/TableSearch.tsx b/src/components/Filters/TableSearch.tsx index 3440bbe38..e76da7556 100644 --- a/src/components/Filters/TableSearch.tsx +++ b/src/components/Filters/TableSearch.tsx @@ -61,7 +61,8 @@ export const TableSearch = ({
diff --git a/src/components/FormatValue/FormatDisplayValue/FormatDisplayValue.tsx b/src/components/FormatValue/FormatDisplayValue/FormatDisplayValue.tsx index 4fe5343dd..5f3315bfb 100644 --- a/src/components/FormatValue/FormatDisplayValue/FormatDisplayValue.tsx +++ b/src/components/FormatValue/FormatDisplayValue/FormatDisplayValue.tsx @@ -11,6 +11,7 @@ export interface FormatDisplayValueUIType symbol?: React.ReactNode; label?: React.ReactNode; details?: React.ReactNode; + hideLessThanOne?: boolean; showTooltipSymbol?: boolean; showTooltipLabel?: boolean; spacedLabel?: boolean; @@ -26,14 +27,15 @@ export const FormatDisplayValue = (props: FormatDisplayValueUIType) => { egldLabel, details, digits = DIGITS, - showLastNonZeroDecimal = false, + showLastNonZeroDecimal, + hideLessThanOne, showLabel = true, showTooltip = true, - showSymbol = false, - superSuffix = false, - showTooltipSymbol = false, - showTooltipLabel = false, - spacedLabel = false, + showSymbol, + superSuffix, + showTooltipSymbol, + showTooltipLabel, + spacedLabel, decimalOpacity = true, className } = props; @@ -43,6 +45,9 @@ export const FormatDisplayValue = (props: FormatDisplayValueUIType) => { const displayLabel = label ?? (token ? token : egldLabel); const DisplayValue = () => { + if (hideLessThanOne) { + return {'< 1'}; + } const completeValueParts = String(completeValue).split('.'); const decimalArray = completeValueParts?.[1]?.split('') ?? []; const areAllDigitsZeroes = decimalArray.every((digit) => digit === ZERO); diff --git a/src/components/FormatValue/FormatNumber/FormatNumber.tsx b/src/components/FormatValue/FormatNumber/FormatNumber.tsx index 50b4d935e..8f7de7c24 100644 --- a/src/components/FormatValue/FormatNumber/FormatNumber.tsx +++ b/src/components/FormatValue/FormatNumber/FormatNumber.tsx @@ -33,14 +33,10 @@ export const FormatNumber = (props: FormatNumberUIType) => { ); } - let formattedValue = bNamount.isInteger() + const formattedValue = bNamount.isInteger() ? completeValue : formatBigNumber({ value: bNamount, maxDigits }); - if (hideLessThanOne && bNamount.isLessThan(1)) { - formattedValue = '< 1'; - } - return ( { completeValue={completeValue} symbol={symbol} egldLabel={label} + hideLessThanOne={hideLessThanOne && bNamount.isLessThan(1)} showSymbol={Boolean(symbol)} showLabel={Boolean(label)} showTooltipSymbol={Boolean(symbol)} diff --git a/src/components/LowLiquidityTooltip/LowLiquidityTooltip.tsx b/src/components/LowLiquidityTooltip/LowLiquidityTooltip.tsx index cff25e9bf..4cd7c6ea0 100644 --- a/src/components/LowLiquidityTooltip/LowLiquidityTooltip.tsx +++ b/src/components/LowLiquidityTooltip/LowLiquidityTooltip.tsx @@ -1,4 +1,5 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import BigNumber from 'bignumber.js'; import classNames from 'classnames'; import { FormatUSD, Overlay } from 'components'; import { faSquareInfo } from 'icons/solid'; @@ -18,17 +19,23 @@ export const LowLiquidityTooltip = ({ return null; } - const { totalLiquidity, isLowLiquidity } = token; + const { totalLiquidity, isLowLiquidity, lowLiquidityThresholdPercent } = + token; + if (!isLowLiquidity) { return null; } + const displayTresholdPercent = new BigNumber( + lowLiquidityThresholdPercent ?? 0.5 + ).toFormat(); + return ( - Less than 0.5% of total Token Supply captured in xExchange Liquidity - Pools. + Less than {displayTresholdPercent}% of total Token Supply captured in + xExchange Liquidity Pools. {showTotalLiquidity && totalLiquidity && ( <> () diff --git a/src/components/ProvidersTable/ProvidersTable.tsx b/src/components/ProvidersTable/ProvidersTable.tsx index 57ca9ed3c..1e3823b3b 100644 --- a/src/components/ProvidersTable/ProvidersTable.tsx +++ b/src/components/ProvidersTable/ProvidersTable.tsx @@ -18,21 +18,21 @@ export const ProvidersTable = (props: ProvidersTableUIType) => { const { providers, showIndex = true, showIdentity = true } = props; const [displayProviders, setDisplayProviders] = useState(providers); - const sort = useGetSort(); + const { sort, order } = useGetSort(); useEffect(() => { - if (sort.sort && sort.order) { + if (sort && order) { setDisplayProviders((existing) => sortProviders({ - field: sort.sort as SortProviderFieldEnum, - order: sort.order, + field: sort as SortProviderFieldEnum, + order: order, sortArray: [...existing] }) ); } else { setDisplayProviders(providers); } - }, [sort.sort, sort.order]); + }, [sort, order]); return (
{ + const itemsPerPageBigNumber = new BigNumber(itemsPerPage); + const currentPageBigNumber = new BigNumber(currentPage); + const itemsLengthBigNumber = new BigNumber(items.length); + + const totalPages = Math.ceil( + itemsLengthBigNumber.dividedBy(itemsPerPage).toNumber() + ); + + const totalPagesArray = Array.from({ length: totalPages }); + const ranges = totalPagesArray.map((_, index) => [ + itemsPerPageBigNumber.times(index), + itemsPerPageBigNumber.times(new BigNumber(index).plus(1)) + ]); + + const rangesLengthBigNumber = new BigNumber(ranges.length); + const currentRange = ranges.find((_, index) => { + if (rangesLengthBigNumber.lte(currentPage)) { + return rangesLengthBigNumber.minus(1).isEqualTo(index); + } + + return currentPageBigNumber.minus(1).isEqualTo(index); + }); + + if (!currentRange) { + return items; + } + + const [currentRangeStart, currentRangeEnd] = currentRange; + const slicedTokensArray = items.slice( + currentRangeStart.toNumber(), + currentRangeEnd.toNumber() + ); + + return slicedTokensArray; +}; diff --git a/src/helpers/getValue/index.ts b/src/helpers/getValue/index.ts index f5ebe40aa..f2b5ed3ba 100644 --- a/src/helpers/getValue/index.ts +++ b/src/helpers/getValue/index.ts @@ -4,6 +4,7 @@ export * from './getAccountStakingDetails'; export * from './getAccountValidatorStakeDetails'; export * from './getColors'; export * from './getDisplayReceiver'; +export * from './getItemsPage'; export * from './getNftText'; export * from './getNodeIcon'; export * from './getNodeIssue'; diff --git a/src/helpers/index.ts b/src/helpers/index.ts index e51b46834..4cd02b7a2 100644 --- a/src/helpers/index.ts +++ b/src/helpers/index.ts @@ -16,6 +16,7 @@ export * from './isEllipsisActive'; export * from './isHash'; export * from './isMetachain'; export * from './isUtf8'; +export * from './isValidTokenValue'; export * from './parseAmount'; export * from './parseJwt'; export * from './partitionBy'; diff --git a/src/helpers/isValidTokenValue.ts b/src/helpers/isValidTokenValue.ts new file mode 100644 index 000000000..4252f7767 --- /dev/null +++ b/src/helpers/isValidTokenValue.ts @@ -0,0 +1,14 @@ +import BigNumber from 'bignumber.js'; + +import { LOW_LIQUIDITY_DISPLAY_TRESHOLD } from 'appConstants'; +import { TokenType } from 'types'; + +export const isValidTokenValue = (token: TokenType) => { + return Boolean( + token.valueUsd && + (!token.isLowLiquidity || + new BigNumber(token.valueUsd).isLessThan( + LOW_LIQUIDITY_DISPLAY_TRESHOLD + )) + ); +}; diff --git a/src/layouts/AccountLayout/AccountDetailsCard/AccountDetailsCard.tsx b/src/layouts/AccountLayout/AccountDetailsCard/AccountDetailsCard.tsx index d2af317b8..6960b3f28 100644 --- a/src/layouts/AccountLayout/AccountDetailsCard/AccountDetailsCard.tsx +++ b/src/layouts/AccountLayout/AccountDetailsCard/AccountDetailsCard.tsx @@ -1,13 +1,8 @@ import React, { useEffect } from 'react'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import BigNumber from 'bignumber.js'; import { useDispatch, useSelector } from 'react-redux'; -import { - ELLIPSIS, - MAX_ACOUNT_TOKENS_BALANCE, - LOW_LIQUIDITY_DISPLAY_TRESHOLD -} from 'appConstants'; +import { ELLIPSIS, MAX_ACOUNT_TOKENS_BALANCE } from 'appConstants'; import { NativeTokenSymbol } from 'components'; import { CardItem, @@ -22,7 +17,8 @@ import { urlBuilder, formatHerotag, formatBigNumber, - getTotalTokenUsdValue + getTotalTokenUsdValue, + isValidTokenValue } from 'helpers'; import { useAdapter, useIsSovereign } from 'hooks'; import { faClock, faExclamationTriangle } from 'icons/regular'; @@ -118,12 +114,7 @@ export const AccountDetailsCard = () => { } if (accountTokensValueData.success) { const validTokenValues = accountTokensValueData.data.filter( - (token: TokenType) => - token.valueUsd && - (!token.isLowLiquidity || - new BigNumber(token.valueUsd).isLessThan( - LOW_LIQUIDITY_DISPLAY_TRESHOLD - )) + (token: TokenType) => isValidTokenValue(token) ); const tokenBalance = getTotalTokenUsdValue(validTokenValues); accountExtraDetails.tokenBalance = tokenBalance; diff --git a/src/layouts/TokenLayout/TokenDetailsCard.tsx b/src/layouts/TokenLayout/TokenDetailsCard.tsx index b49b7adc3..ab9db6d0a 100644 --- a/src/layouts/TokenLayout/TokenDetailsCard.tsx +++ b/src/layouts/TokenLayout/TokenDetailsCard.tsx @@ -109,13 +109,14 @@ export const TokenDetailsCard = () => { ]; const smallStatsCards = [ - supply + supply && new BigNumber(supply).isGreaterThanOrEqualTo(0) ? { title: 'Supply', value: new BigNumber(supply).toFormat(0) } : {}, - circulatingSupply + circulatingSupply && + new BigNumber(circulatingSupply).isGreaterThanOrEqualTo(0) ? { title: 'Circulating', value: new BigNumber(circulatingSupply).toFormat(0) diff --git a/src/pages/AccountDetails/AccountTokens.tsx b/src/pages/AccountDetails/AccountTokens.tsx index c66a012f8..9171d3a27 100644 --- a/src/pages/AccountDetails/AccountTokens.tsx +++ b/src/pages/AccountDetails/AccountTokens.tsx @@ -1,9 +1,8 @@ import { useEffect, useRef, useState } from 'react'; -import BigNumber from 'bignumber.js'; import { useSelector } from 'react-redux'; import { useParams, useSearchParams } from 'react-router-dom'; -import { ZERO, LOW_LIQUIDITY_DISPLAY_TRESHOLD } from 'appConstants'; +import { ZERO } from 'appConstants'; import { DetailItem, Loader, @@ -15,6 +14,7 @@ import { FormatUSD, LowLiquidityTooltip } from 'components'; +import { isValidTokenValue } from 'helpers'; import { useAdapter, useGetPage } from 'hooks'; import { faCoins } from 'icons/solid'; import { AccountTabs } from 'layouts/AccountLayout/AccountTabs'; @@ -90,6 +90,7 @@ export const AccountTokens = () => { {dataReady === true && accountTokens.length > 0 && ( <> {accountTokens.map((token) => { + const isValidDisplayValue = isValidTokenValue(token); return ( { showLastNonZeroDecimal />
- {token.valueUsd && - (!token.isLowLiquidity || - new BigNumber(token.valueUsd).isLessThan( - LOW_LIQUIDITY_DISPLAY_TRESHOLD - )) && ( - - ( - - ) - - )} + {isValidDisplayValue && ( + + ( + + ) + + )}
diff --git a/src/pages/AccountDetails/AccountTokensTable/AccountTokensTable.tsx b/src/pages/AccountDetails/AccountTokensTable/AccountTokensTable.tsx new file mode 100644 index 000000000..d56fcb14f --- /dev/null +++ b/src/pages/AccountDetails/AccountTokensTable/AccountTokensTable.tsx @@ -0,0 +1,233 @@ +import { useEffect, useState } from 'react'; +import { useSelector } from 'react-redux'; +import { useParams } from 'react-router-dom'; + +import { ZERO, MAX_RESULTS, ACCOUNT_TOKENS_FIELDS } from 'appConstants'; +import { + Pager, + PageSize, + PageState, + FormatAmount, + TokenLink, + FormatUSD, + LowLiquidityTooltip, + FormatNumber, + Sort, + Loader, + Overlay +} from 'components'; +import { isValidTokenValue } from 'helpers'; +import { useAdapter } from 'hooks'; +import { faCoins } from 'icons/solid'; +import { AccountTabs } from 'layouts/AccountLayout/AccountTabs'; +import { activeNetworkSelector, accountSelector } from 'redux/selectors'; +import { TokenType, SortOrderEnum } from 'types'; + +import { AccountTokensTableHeader } from './components'; +import { SortTokenFieldEnum } from './helpers'; +import { usePageTokens, useProcessTokens } from './hooks'; + +const ColSpanWrapper = ({ children }: { children: React.ReactNode }) => ( + + {children} + +); + +export const AccountTokensTable = () => { + const { id: activeNetworkId } = useSelector(activeNetworkSelector); + const { account } = useSelector(accountSelector); + const { txCount } = account; + const { getAccountTokens } = useAdapter(); + const { hash: address } = useParams() as any; + + const [isDataReady, setIsDataReady] = useState(); + const [accountTokens, setAccountTokens] = useState([]); + + const fetchAccountTokens = async () => { + const { data, success } = await getAccountTokens({ + address, + includeMetaESDT: true, + size: MAX_RESULTS, + fields: ACCOUNT_TOKENS_FIELDS.join(',') + }); + if (success && data) { + setAccountTokens(data); + } + setIsDataReady(success); + }; + + const hasValidValues = accountTokens.some((token) => + isValidTokenValue(token) + ); + const processedAccountTokens = useProcessTokens(accountTokens); + const pagedTokens = usePageTokens(processedAccountTokens); + + useEffect(() => { + fetchAccountTokens(); + }, [txCount, activeNetworkId, address]); + + return ( +
+
+
+ +
+
+ +
+
+ +
+ + + + + + + + + + + + {isDataReady === undefined && ( + + + + )} + {isDataReady === false && ( + + + + )} + {isDataReady === true && ( + <> + {pagedTokens.length > 0 ? ( + <> + {pagedTokens.map((token) => { + const isValidDisplayValue = isValidTokenValue(token); + + return ( + + + + + + + + ); + })} + + ) : ( + <> + + + + + )} + + )} + +
+ + + + + + + + + Portofolio %} + /> +
+
+ + + ({token.assets?.name ?? token.name}) + +
+
+ + + {token.price ? ( +
+ + +
+ ) : ( + - + )} +
+ {isValidDisplayValue ? ( + + ) : ( + - + )} + + {token.portofolioPercentage && + token.portofolioPercentage.isGreaterThan(0) ? ( + + ) : ( + - + )} +
+
+ +
+ + 0} + /> +
+
+ ); +}; diff --git a/src/pages/AccountDetails/AccountTokensTable/accountTokensTable.styles.scss b/src/pages/AccountDetails/AccountTokensTable/accountTokensTable.styles.scss new file mode 100644 index 000000000..6e66ea822 --- /dev/null +++ b/src/pages/AccountDetails/AccountTokensTable/accountTokensTable.styles.scss @@ -0,0 +1,14 @@ +.account-tokens-table { + td { + width: 12.5%; + &:first-of-type { + width: 30%; + } + &:nth-child(2) { + width: 30%; + } + &:nth-child(4) { + width: 15%; + } + } +} diff --git a/src/pages/AccountDetails/AccountTokensTable/components/AccountTokensTableHeader.tsx b/src/pages/AccountDetails/AccountTokensTable/components/AccountTokensTableHeader.tsx new file mode 100644 index 000000000..d87167aa2 --- /dev/null +++ b/src/pages/AccountDetails/AccountTokensTable/components/AccountTokensTableHeader.tsx @@ -0,0 +1,89 @@ +import classNames from 'classnames'; +import { useSearchParams } from 'react-router-dom'; + +import { Pager, TableSearch } from 'components'; +import { TokenTypeEnum } from 'types'; + +export interface AccountTokensTableHeaderUIType { + tokenCount?: number; +} + +export const AccountTokensTableHeader = ({ + tokenCount = 0 +}: AccountTokensTableHeaderUIType) => { + const [searchParams, setSearchParams] = useSearchParams(); + const { type } = Object.fromEntries(searchParams); + + const updateTokenType = (typeValue?: TokenTypeEnum) => () => { + const { type, page, size, ...rest } = Object.fromEntries(searchParams); + const nextUrlParams = { + ...rest, + ...(typeValue ? { type: typeValue } : {}) + }; + + setSearchParams(nextUrlParams); + }; + + return ( + <> +
+ +
  • + +
  • +
  • + +
  • +
  • + +
  • +
    +
    + +
    +
    + + + ); +}; diff --git a/src/pages/AccountDetails/AccountTokensTable/components/index.ts b/src/pages/AccountDetails/AccountTokensTable/components/index.ts new file mode 100644 index 000000000..22188ef15 --- /dev/null +++ b/src/pages/AccountDetails/AccountTokensTable/components/index.ts @@ -0,0 +1 @@ +export * from './AccountTokensTableHeader'; diff --git a/src/pages/AccountDetails/AccountTokensTable/helpers/filterTokens.ts b/src/pages/AccountDetails/AccountTokensTable/helpers/filterTokens.ts new file mode 100644 index 000000000..661a2b097 --- /dev/null +++ b/src/pages/AccountDetails/AccountTokensTable/helpers/filterTokens.ts @@ -0,0 +1,38 @@ +import { TokenTypeEnum } from 'types'; + +import { ProcessedTokenType } from '../helpers'; + +export interface FilterTokensType { + tokens: ProcessedTokenType[]; + type?: TokenTypeEnum; + search?: string; +} + +export const filterTokens = ({ tokens, type, search }: FilterTokensType) => { + const searchTerm = (term?: string) => { + if (!search) { + return true; + } + + if (!term) { + return false; + } + + return term.toLowerCase().includes(search.toLowerCase()); + }; + + return tokens + .filter((token) => !type || token.type === type) + .filter(({ name, identifier, assets }) => { + if (!search) { + return true; + } + + return ( + searchTerm(name) || + searchTerm(identifier) || + searchTerm(assets?.description) || + searchTerm(assets?.name) + ); + }); +}; diff --git a/src/pages/AccountDetails/AccountTokensTable/helpers/index.ts b/src/pages/AccountDetails/AccountTokensTable/helpers/index.ts new file mode 100644 index 000000000..078568b77 --- /dev/null +++ b/src/pages/AccountDetails/AccountTokensTable/helpers/index.ts @@ -0,0 +1,2 @@ +export * from './filterTokens'; +export * from './sortTokens'; diff --git a/src/pages/AccountDetails/AccountTokensTable/helpers/sortTokens.ts b/src/pages/AccountDetails/AccountTokensTable/helpers/sortTokens.ts new file mode 100644 index 000000000..3902ce851 --- /dev/null +++ b/src/pages/AccountDetails/AccountTokensTable/helpers/sortTokens.ts @@ -0,0 +1,128 @@ +import BigNumber from 'bignumber.js'; + +import { LOW_LIQUIDITY_DISPLAY_TRESHOLD } from 'appConstants'; +import { formatAmount } from 'helpers'; +import { TokenType, SortOrderEnum } from 'types'; + +export enum SortTokenFieldEnum { + name = 'name', + balance = 'balance', + price = 'price', + value = 'value', + portofolioPercent = 'portofolioPercent' +} + +export interface ProcessedTokenType extends TokenType { + portofolioPercentage: BigNumber; +} + +export interface SortTokensType { + field?: SortTokenFieldEnum; + order?: SortOrderEnum; + tokens: ProcessedTokenType[]; + tokenBalance?: string; +} + +const getTokenDisplayValue = ({ + valueUsd, + isLowLiquidity +}: ProcessedTokenType) => { + if ( + valueUsd && + (!isLowLiquidity || + new BigNumber(valueUsd).isLessThan(LOW_LIQUIDITY_DISPLAY_TRESHOLD)) + ) { + return new BigNumber(valueUsd); + } + + return new BigNumber(0); +}; + +const getPortofolioPercent = ({ + token, + tokenBalance +}: { + token: ProcessedTokenType; + tokenBalance: string; +}) => { + return token.valueUsd && tokenBalance + ? new BigNumber(token.valueUsd).dividedBy(tokenBalance).times(100) + : new BigNumber(0); +}; + +export const sortTokens = ({ + field = SortTokenFieldEnum.value, + order = SortOrderEnum.desc, + tokens = [], + tokenBalance +}: SortTokensType) => { + if (field && order) { + const sortParams = order === SortOrderEnum.asc ? [1, -1] : [-1, 1]; + + switch (true) { + case field === SortTokenFieldEnum.name: + tokens.sort((a, b) => { + const aName = a.assets?.name ?? a.name; + const bName = b.assets?.name ?? b.name; + return aName.toLowerCase() > bName.toLowerCase() + ? sortParams[0] + : sortParams[1]; + }); + break; + + case field === SortTokenFieldEnum.value: + tokens.sort((a, b) => { + const aValue = getTokenDisplayValue(a); + const bValue = getTokenDisplayValue(b); + return aValue.isGreaterThan(bValue) ? sortParams[0] : sortParams[1]; + }); + break; + + case field === SortTokenFieldEnum.balance: + tokens.sort((a, b) => { + const aBalance = formatAmount({ + input: new BigNumber(a.balance ?? 0).toString(10), + decimals: a.decimals, + showLastNonZeroDecimal: true + }); + const bBalance = formatAmount({ + input: new BigNumber(b.balance ?? 0).toString(10), + decimals: b.decimals, + showLastNonZeroDecimal: true + }); + return new BigNumber(aBalance).isGreaterThan(bBalance) + ? sortParams[0] + : sortParams[1]; + }); + break; + + case field === SortTokenFieldEnum.portofolioPercent: + if (!tokenBalance) { + return tokens; + } + + tokens.sort((a, b) => { + const aPercent = getPortofolioPercent({ token: a, tokenBalance }); + const bPercent = getPortofolioPercent({ token: b, tokenBalance }); + return new BigNumber(aPercent).isGreaterThan(bPercent) + ? sortParams[0] + : sortParams[1]; + }); + break; + + case field === SortTokenFieldEnum.price: + tokens.sort((a, b) => { + return new BigNumber(a.price ?? 0).isGreaterThan(b.price ?? 0) + ? sortParams[0] + : sortParams[1]; + }); + break; + + default: + return tokens; + break; + } + } + + return tokens; +}; diff --git a/src/pages/AccountDetails/AccountTokensTable/hooks/index.ts b/src/pages/AccountDetails/AccountTokensTable/hooks/index.ts new file mode 100644 index 000000000..ac9358af0 --- /dev/null +++ b/src/pages/AccountDetails/AccountTokensTable/hooks/index.ts @@ -0,0 +1,2 @@ +export * from './useProcessTokens'; +export * from './usePageTokens'; diff --git a/src/pages/AccountDetails/AccountTokensTable/hooks/usePageTokens.ts b/src/pages/AccountDetails/AccountTokensTable/hooks/usePageTokens.ts new file mode 100644 index 000000000..8727c4311 --- /dev/null +++ b/src/pages/AccountDetails/AccountTokensTable/hooks/usePageTokens.ts @@ -0,0 +1,19 @@ +import { useMemo } from 'react'; + +import { getItemsPage } from 'helpers'; +import { useGetPage } from 'hooks'; +import { ProcessedTokenType } from '../helpers'; + +export const usePageTokens = (accountTokens: ProcessedTokenType[]) => { + const { page, size } = useGetPage(); + + return useMemo(() => { + const processedTokens = getItemsPage({ + items: accountTokens, + currentPage: page, + itemsPerPage: size + }); + + return processedTokens; + }, [accountTokens, page, size]); +}; diff --git a/src/pages/AccountDetails/AccountTokensTable/hooks/useProcessTokens.ts b/src/pages/AccountDetails/AccountTokensTable/hooks/useProcessTokens.ts new file mode 100644 index 000000000..5aa2de6fe --- /dev/null +++ b/src/pages/AccountDetails/AccountTokensTable/hooks/useProcessTokens.ts @@ -0,0 +1,90 @@ +import { useMemo } from 'react'; +import BigNumber from 'bignumber.js'; +import { useDispatch, useSelector } from 'react-redux'; +import { useParams, useSearchParams } from 'react-router-dom'; + +import { isValidTokenValue, getTotalTokenUsdValue } from 'helpers'; +import { useGetSearch, useGetSort } from 'hooks'; +import { accountExtraSelector } from 'redux/selectors'; +import { setAccountExtra, getInitialAccountExtraState } from 'redux/slices'; +import { TokenTypeEnum, TokenType, SortOrderEnum } from 'types'; + +import { + filterTokens, + sortTokens, + ProcessedTokenType, + SortTokenFieldEnum +} from '../helpers'; + +export const useProcessTokens = (accountTokens: TokenType[]) => { + const dispatch = useDispatch(); + + const [searchParams] = useSearchParams(); + const { hash: address } = useParams() as any; + const { accountExtra, isFetched: isAccountExtraFetched } = + useSelector(accountExtraSelector); + const { address: extraAddress } = accountExtra; + const { search } = useGetSearch(); + const { sort, order } = useGetSort(); + const { type } = Object.fromEntries(searchParams); + + const validTokenValues = accountTokens.filter((token: TokenType) => + isValidTokenValue(token) + ); + const tokenBalance = getTotalTokenUsdValue(validTokenValues); + + if (!isAccountExtraFetched && address === extraAddress) { + const accountExtraDetails = getInitialAccountExtraState().accountExtra; + accountExtraDetails.tokenBalance = tokenBalance; + dispatch( + setAccountExtra({ + accountExtra: { ...accountExtraDetails, address }, + isFetched: true + }) + ); + } + + const processTokens = ({ tokens }: { tokens: ProcessedTokenType[] }) => { + const filteredTokens = filterTokens({ + tokens, + type: type as TokenTypeEnum, + search + }); + + let currentSort = sort; + let currentOrder = order; + if (!(sort && order)) { + const hasValidValues = filteredTokens.some((token) => + isValidTokenValue(token) + ); + if (!hasValidValues) { + currentSort = SortTokenFieldEnum.name; + currentOrder = SortOrderEnum.asc; + } + } + + const sortedTokens = sortTokens({ + tokens: filteredTokens, + field: currentSort as SortTokenFieldEnum, + order: currentOrder, + tokenBalance + }); + + return sortedTokens; + }; + + const processedAccountTokens = useMemo(() => { + const processedSortArray = accountTokens.map((token) => { + const portofolioPercentage = + token.valueUsd && tokenBalance + ? new BigNumber(token.valueUsd).dividedBy(tokenBalance).times(100) + : new BigNumber(0); + return { ...token, portofolioPercentage }; + }); + + const processedTokens = processTokens({ tokens: processedSortArray }); + return [...processedTokens]; + }, [accountTokens, type, search, sort, order, tokenBalance]); + + return processedAccountTokens; +}; diff --git a/src/pages/AccountDetails/AccountTokensTable/index.ts b/src/pages/AccountDetails/AccountTokensTable/index.ts new file mode 100644 index 000000000..3a2626910 --- /dev/null +++ b/src/pages/AccountDetails/AccountTokensTable/index.ts @@ -0,0 +1 @@ +export * from './AccountTokensTable'; diff --git a/src/pages/Tokens/components/TokensTable/TokensTable.tsx b/src/pages/Tokens/components/TokensTable/TokensTable.tsx index 6976eec18..f03b4e552 100644 --- a/src/pages/Tokens/components/TokensTable/TokensTable.tsx +++ b/src/pages/Tokens/components/TokensTable/TokensTable.tsx @@ -2,10 +2,7 @@ import { Fragment } from 'react'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import BigNumber from 'bignumber.js'; -import { - ELLIPSIS, - LOW_LIQUIDITY_MARKET_CAP_DISPLAY_TRESHOLD -} from 'appConstants'; +import { ELLIPSIS } from 'appConstants'; import { NetworkLink, FormatAmount, @@ -13,7 +10,7 @@ import { LowLiquidityTooltip, FormatUSD } from 'components'; -import { urlBuilder } from 'helpers'; +import { isValidTokenValue, urlBuilder } from 'helpers'; import { useGetSort, useGetSearch, useIsNativeTokenSearched } from 'hooks'; import { faDiamond } from 'icons/regular'; import { TokenType, TokenSortEnum, SortOrderEnum } from 'types'; @@ -144,20 +141,14 @@ export const TokensTable = ({ )} - {token.marketCap && - (!token.isLowLiquidity || - new BigNumber(token.marketCap).isLessThan( - LOW_LIQUIDITY_MARKET_CAP_DISPLAY_TRESHOLD - )) && ( - <> - - - )} + {isValidTokenValue(token) && token.marketCap && ( + + )} {token.accounts diff --git a/src/routes/layouts/accountLayout.ts b/src/routes/layouts/accountLayout.ts index 621536d84..0e3a8e440 100644 --- a/src/routes/layouts/accountLayout.ts +++ b/src/routes/layouts/accountLayout.ts @@ -7,7 +7,7 @@ import { AccountNodes } from 'pages/AccountDetails/AccountNodes'; import { AccountCollectionRoles } from 'pages/AccountDetails/AccountRoles/AccountCollectionRoles'; import { AccountTokenRoles } from 'pages/AccountDetails/AccountRoles/AccountTokenRoles'; import { AccountStaking } from 'pages/AccountDetails/AccountStaking'; -import { AccountTokens } from 'pages/AccountDetails/AccountTokens'; +import { AccountTokensTable } from 'pages/AccountDetails/AccountTokensTable'; import { AccountTransactions } from 'pages/AccountDetails/AccountTransactions'; import { AccountUpgrades } from 'pages/AccountDetails/AccountUpgrades'; import { OldRouteRedirect } from 'pages/AccountDetails/OldRouteRedirect'; @@ -87,7 +87,7 @@ export const accountLayout: TitledRouteObject[] = [ path: accountsRoutes.accountTokens, title: 'Account Tokens', preventScroll: true, - Component: AccountTokens + Component: AccountTokensTable }, { path: accountsRoutes.accountNfts, diff --git a/src/types/token.types.ts b/src/types/token.types.ts index 3f6cc0405..6caf22c16 100644 --- a/src/types/token.types.ts +++ b/src/types/token.types.ts @@ -28,6 +28,7 @@ export interface TokenType { assets?: TokenAssetType; totalLiquidity?: number; isLowLiquidity?: boolean; + lowLiquidityThresholdPercent?: number; transfersCount?: number; roles?: TokenRolesType[]; } diff --git a/src/widgets/StatsCard/SmallStatsCard.tsx b/src/widgets/StatsCard/SmallStatsCard.tsx index 53d31267b..ab9445d8d 100644 --- a/src/widgets/StatsCard/SmallStatsCard.tsx +++ b/src/widgets/StatsCard/SmallStatsCard.tsx @@ -6,6 +6,10 @@ export const SmallStatsCard = ({ value, className }: StatsCardUIType) => { + if (!(title && value)) { + return null; + } + return (