diff --git a/src/components/AccountDrawer/MiniPortfolio/NFTs/NFTItem.tsx b/src/components/AccountDrawer/MiniPortfolio/NFTs/NFTItem.tsx index c9a5658fe3..0223bc7374 100644 --- a/src/components/AccountDrawer/MiniPortfolio/NFTs/NFTItem.tsx +++ b/src/components/AccountDrawer/MiniPortfolio/NFTs/NFTItem.tsx @@ -8,10 +8,10 @@ import { NftCard } from 'nft/components/card' import { detailsHref } from 'nft/components/card/utils' import { VerifiedIcon } from 'nft/components/icons' import { WalletAsset } from 'nft/types' -import { floorFormatter } from 'nft/utils' import { useNavigate } from 'react-router-dom' import styled from 'styled-components' import { ThemedText } from 'theme/components' +import { NumberType, useFormatter } from 'utils/formatNumbers' const FloorPrice = styled(Row)` opacity: 0; @@ -83,6 +83,8 @@ export function NFT({ } function NFTDetails({ asset }: { asset: WalletAsset }) { + const { formatNumberOrString } = useFormatter() + return ( @@ -91,7 +93,9 @@ function NFTDetails({ asset }: { asset: WalletAsset }) { - {asset.floorPrice ? `${floorFormatter(asset.floorPrice)} ETH` : ' '} + {asset.floorPrice + ? `${formatNumberOrString({ input: asset.floorPrice, type: NumberType.NFTTokenFloorPrice })} ETH` + : ' '} diff --git a/src/components/NavBar/SuggestionRow.tsx b/src/components/NavBar/SuggestionRow.tsx index ed581a6f12..8814d5b56f 100644 --- a/src/components/NavBar/SuggestionRow.tsx +++ b/src/components/NavBar/SuggestionRow.tsx @@ -13,13 +13,11 @@ import { Column, Row } from 'nft/components/Flex' import { VerifiedIcon } from 'nft/components/icons' import { vars } from 'nft/css/sprinkles.css' import { GenieCollection } from 'nft/types' -import { ethNumberStandardFormatter } from 'nft/utils/currency' -import { putCommas } from 'nft/utils/putCommas' import { useCallback, useEffect, useState } from 'react' import { Link, useNavigate } from 'react-router-dom' import styled from 'styled-components' import { ThemedText } from 'theme/components' -import { useFormatter } from 'utils/formatNumbers' +import { NumberType, useFormatter } from 'utils/formatNumbers' import { DeltaArrow, DeltaText } from '../Tokens/TokenDetails/Delta' import { useAddRecentlySearchedAsset } from './RecentlySearchedAssets' @@ -49,6 +47,8 @@ export const CollectionRow = ({ index, eventProperties, }: CollectionRowProps) => { + const { formatNumberOrString } = useFormatter() + const [brokenImage, setBrokenImage] = useState(false) const [loaded, setLoaded] = useState(false) @@ -102,13 +102,17 @@ export const CollectionRow = ({ {collection.name} {collection.isVerified && } - {putCommas(collection?.stats?.total_supply ?? 0)} items + + {formatNumberOrString({ input: collection?.stats?.total_supply, type: NumberType.WholeNumber })} items + {collection.stats?.floor_price ? ( - {ethNumberStandardFormatter(collection.stats?.floor_price)} ETH + + {formatNumberOrString({ input: collection.stats?.floor_price, type: NumberType.NFTToken })} ETH + Floor diff --git a/src/nft/components/bag/BagFooter.tsx b/src/nft/components/bag/BagFooter.tsx index d29502ede5..22f6cac3c3 100644 --- a/src/nft/components/bag/BagFooter.tsx +++ b/src/nft/components/bag/BagFooter.tsx @@ -31,12 +31,12 @@ import { useSubscribeTransactionState } from 'nft/hooks/useSubscribeTransactionS import { useTokenInput } from 'nft/hooks/useTokenInput' import { useWalletBalance } from 'nft/hooks/useWalletBalance' import { BagStatus } from 'nft/types' -import { ethNumberStandardFormatter, formatWeiToDecimal } from 'nft/utils' import { PropsWithChildren, useEffect, useMemo, useState } from 'react' import { AlertTriangle, ChevronDown } from 'react-feather' import { InterfaceTrade, TradeFillType, TradeState } from 'state/routing/types' import styled, { useTheme } from 'styled-components' import { ThemedText } from 'theme/components' +import { NumberType, useFormatter } from 'utils/formatNumbers' import { BuyButtonStateData, BuyButtonStates, getBuyButtonStateData } from './ButtonStates' @@ -189,10 +189,12 @@ const InputCurrencyValue = ({ tradeState: TradeState trade?: InterfaceTrade }) => { + const { formatEther, formatNumberOrString } = useFormatter() + if (!usingPayWithAnyToken) { return ( - {formatWeiToDecimal(totalEthPrice.toString())} + {formatEther({ input: totalEthPrice.toString(), type: NumberType.NFTToken })}  {activeCurrency?.symbol ?? 'ETH'} ) @@ -208,7 +210,7 @@ const InputCurrencyValue = ({ return ( - {ethNumberStandardFormatter(trade?.inputAmount.toExact())} + {formatNumberOrString({ input: trade?.inputAmount.toExact(), type: NumberType.NFTToken })} ) } @@ -224,6 +226,8 @@ const FiatValue = ({ tradeState: TradeState usingPayWithAnyToken: boolean }) => { + const { formatNumberOrString } = useFormatter() + if (!usdcValue) { if (usingPayWithAnyToken && (tradeState === TradeState.INVALID || tradeState === TradeState.NO_ROUTE_FOUND)) { return null @@ -247,7 +251,7 @@ const FiatValue = ({ )} - {`${ethNumberStandardFormatter(usdcValue?.toExact(), true)}`} + {`${formatNumberOrString({ input: usdcValue?.toExact(), type: NumberType.FiatNFTToken })}`} ) diff --git a/src/nft/components/bag/BagRow.tsx b/src/nft/components/bag/BagRow.tsx index 843e22f9d9..eb8a330733 100644 --- a/src/nft/components/bag/BagRow.tsx +++ b/src/nft/components/bag/BagRow.tsx @@ -1,5 +1,5 @@ import { BigNumber } from '@ethersproject/bignumber' -import { formatEther } from '@ethersproject/units' +import { formatEther as ethersFormatEther } from '@ethersproject/units' import clsx from 'clsx' import { ButtonEmphasis, ButtonSize, ThemeButton } from 'components/Button' import { TimedLoader } from 'nft/components/bag/TimedLoader' @@ -18,10 +18,11 @@ import { import { bodySmall } from 'nft/css/common.css' import { loadingBlock } from 'nft/css/loading.css' import { GenieAsset, UpdatedGenieAsset } from 'nft/types' -import { ethNumberStandardFormatter, formatWeiToDecimal, getAssetHref } from 'nft/utils' +import { getAssetHref } from 'nft/utils' import { MouseEvent, useCallback, useEffect, useReducer, useState } from 'react' import { Link } from 'react-router-dom' import styled from 'styled-components' +import { NumberType, useFormatter } from 'utils/formatNumbers' import * as styles from './BagRow.css' @@ -90,6 +91,7 @@ interface BagRowProps { } export const BagRow = ({ asset, usdPrice, removeAsset, showRemove, grayscale, isMobile }: BagRowProps) => { + const { formatEther, formatNumberOrString } = useFormatter() const [loadedImage, setImageLoaded] = useState(false) const [noImageAvailable, setNoImageAvailable] = useState(!asset.smallImageUrl) @@ -99,11 +101,11 @@ export const BagRow = ({ asset, usdPrice, removeAsset, showRemove, grayscale, is const showRemoveButton = Boolean(showRemove && cardHovered && !isMobile) const assetEthPrice = asset.updatedPriceInfo ? asset.updatedPriceInfo.ETHPrice : asset.priceInfo.ETHPrice - const assetEthPriceFormatted = formatWeiToDecimal(assetEthPrice) - const assetUSDPriceFormatted = ethNumberStandardFormatter( - usdPrice ? parseFloat(formatEther(assetEthPrice)) * usdPrice : usdPrice, - true - ) + const assetEthPriceFormatted = formatEther({ input: assetEthPrice, type: NumberType.NFTToken }) + const assetUSDPriceFormatted = formatNumberOrString({ + input: usdPrice ? parseFloat(ethersFormatEther(assetEthPrice)) * usdPrice : usdPrice, + type: NumberType.FiatNFTToken, + }) const handleRemoveClick = useCallback( (e: MouseEvent) => { @@ -175,6 +177,7 @@ interface PriceChangeBagRowProps { } export const PriceChangeBagRow = ({ asset, usdPrice, markAssetAsReviewed, top, isMobile }: PriceChangeBagRowProps) => { + const { formatEther } = useFormatter() const isPriceIncrease = BigNumber.from(asset.updatedPriceInfo?.ETHPrice).gt(BigNumber.from(asset.priceInfo.ETHPrice)) const handleRemove = useCallback( (e: MouseEvent) => { @@ -198,9 +201,10 @@ export const PriceChangeBagRow = ({ asset, usdPrice, markAssetAsReviewed, top, i {isPriceIncrease ? : } - {`Price ${isPriceIncrease ? 'increased' : 'decreased'} from ${formatWeiToDecimal( - asset.priceInfo.ETHPrice - )} ETH`} + {`Price ${isPriceIncrease ? 'increased' : 'decreased'} from ${formatEther({ + input: asset.priceInfo.ETHPrice, + type: NumberType.NFTToken, + })} ETH`} undefined} isMobile={isMobile} /> diff --git a/src/nft/components/bag/MobileHoverBag.tsx b/src/nft/components/bag/MobileHoverBag.tsx index dca1d72555..caf4f7c409 100644 --- a/src/nft/components/bag/MobileHoverBag.tsx +++ b/src/nft/components/bag/MobileHoverBag.tsx @@ -3,7 +3,8 @@ import { Column, Row } from 'nft/components/Flex' import { body, bodySmall } from 'nft/css/common.css' import { useBag } from 'nft/hooks' import { useBagTotalEthPrice, useBagTotalUsdPrice } from 'nft/hooks/useBagTotalEthPrice' -import { ethNumberStandardFormatter, formatWeiToDecimal, roundAndPluralize } from 'nft/utils' +import { roundAndPluralize } from 'nft/utils' +import { NumberType, useFormatter } from 'utils/formatNumbers' import * as styles from './MobileHoverBag.css' export const MobileHoverBag = () => { @@ -11,6 +12,7 @@ export const MobileHoverBag = () => { const toggleBag = useBag((state) => state.toggleBag) const totalEthPrice = useBagTotalEthPrice() const totalUsdPrice = useBagTotalUsdPrice() + const { formatEther, formatNumberOrString } = useFormatter() const shouldShowBag = itemsInBag.length > 0 @@ -48,9 +50,11 @@ export const MobileHoverBag = () => { {roundAndPluralize(itemsInBag.length, 'NFT')} - {`${formatWeiToDecimal(totalEthPrice.toString())}`} ETH + + {`${formatEther({ input: totalEthPrice.toString(), type: NumberType.NFTToken })}`} ETH + - {ethNumberStandardFormatter(totalUsdPrice, true)} + {formatNumberOrString({ input: totalUsdPrice, type: NumberType.FiatNFTToken })} diff --git a/src/nft/components/card/icons.tsx b/src/nft/components/card/icons.tsx index d1cb38dc36..db12cd109c 100644 --- a/src/nft/components/card/icons.tsx +++ b/src/nft/components/card/icons.tsx @@ -5,10 +5,10 @@ import { NftStandard } from 'graphql/data/__generated__/types-and-hooks' import { getMarketplaceIcon } from 'nft/components/card/utils' import { CollectionSelectedAssetIcon } from 'nft/components/icons' import { Markets } from 'nft/types' -import { putCommas } from 'nft/utils' import { AlertTriangle, Check, Tag } from 'react-feather' import styled from 'styled-components' import { ThemedText } from 'theme/components' +import { NumberType, useFormatter } from 'utils/formatNumbers' const StyledMarketplaceContainer = styled.div<{ isText?: boolean }>` position: absolute; @@ -114,6 +114,8 @@ const RarityInfo = styled(ThemedText.BodySmall)` ` export const Ranking = ({ provider }: RankingProps) => { + const { formatNumber } = useFormatter() + if (!provider.rank) { return null } @@ -131,7 +133,7 @@ export const Ranking = ({ provider }: RankingProps) => { } placement="top" > - # {putCommas(provider.rank)} + # {formatNumber({ input: provider.rank, type: NumberType.WholeNumber })} ) diff --git a/src/nft/components/card/index.tsx b/src/nft/components/card/index.tsx index 8b8bb25fa2..9f412e8d33 100644 --- a/src/nft/components/card/index.tsx +++ b/src/nft/components/card/index.tsx @@ -4,8 +4,8 @@ import { MediaContainer } from 'nft/components/card/media' import { detailsHref, getNftDisplayComponent, useSelectAsset } from 'nft/components/card/utils' import { useBag } from 'nft/hooks' import { GenieAsset, UniformAspectRatio, UniformAspectRatios, WalletAsset } from 'nft/types' -import { floorFormatter } from 'nft/utils' import { ReactNode } from 'react' +import { NumberType, useFormatter } from 'utils/formatNumbers' interface NftCardProps { asset: GenieAsset | WalletAsset @@ -74,12 +74,15 @@ export const NftCard = ({ setBagExpanded: state.setBagExpanded, })) + const { formatNumberOrString } = useFormatter() const collectionNft = 'marketplace' in asset const profileNft = 'asset_contract' in asset const tokenType = collectionNft ? asset.tokenType : profileNft ? asset.asset_contract.tokenType : undefined const marketplace = collectionNft ? asset.marketplace : undefined const listedPrice = - profileNft && !isDisabled && asset.floor_sell_order_price ? floorFormatter(asset.floor_sell_order_price) : undefined + profileNft && !isDisabled && asset.floor_sell_order_price + ? formatNumberOrString({ input: asset.floor_sell_order_price, type: NumberType.NFTTokenFloorPrice }) + : undefined return ( ( ) export const PriceCell = ({ marketplace, price }: { marketplace?: Markets | string; price?: string | number }) => { - const formattedPrice = useMemo(() => (price ? formatEth(parseFloat(price?.toString())) : null), [price]) + const { formatNumberOrString } = useFormatter() + const formattedPrice = useMemo( + () => (price ? formatNumberOrString({ input: parseFloat(price?.toString()), type: NumberType.NFTToken }) : null), + [formatNumberOrString, price] + ) return ( @@ -257,7 +260,11 @@ export const EventCell = ({ price, isMobile, }: EventCellProps) => { - const formattedPrice = useMemo(() => (price ? formatEth(parseFloat(price?.toString())) : null), [price]) + const { formatNumberOrString } = useFormatter() + const formattedPrice = useMemo( + () => (price ? formatNumberOrString({ input: parseFloat(price?.toString()), type: NumberType.NFTToken }) : null), + [formatNumberOrString, price] + ) return ( @@ -318,6 +325,7 @@ interface RankingProps { } const Ranking = ({ rarity, collectionName, rarityVerified }: RankingProps) => { + const { formatNumber } = useFormatter() const rank = (rarity as TokenRarity).rank || (rarity as Rarity).providers?.[0].rank if (!rank) return null @@ -339,7 +347,7 @@ const Ranking = ({ rarity, collectionName, rarityVerified }: RankingProps) => { > - {putCommas(rank)} + {formatNumber({ input: rank, type: NumberType.WholeNumber })} diff --git a/src/nft/components/collection/CollectionAsset.tsx b/src/nft/components/collection/CollectionAsset.tsx index f911909e35..ea5d06015c 100644 --- a/src/nft/components/collection/CollectionAsset.tsx +++ b/src/nft/components/collection/CollectionAsset.tsx @@ -6,8 +6,8 @@ import { NftCard, NftCardDisplayProps } from 'nft/components/card' import { Ranking as RankingContainer, Suspicious as SuspiciousContainer } from 'nft/components/card/icons' import { useBag } from 'nft/hooks' import { GenieAsset, UniformAspectRatio } from 'nft/types' -import { formatWeiToDecimal } from 'nft/utils' import { useCallback, useMemo } from 'react' +import { NumberType, useFormatter } from 'utils/formatNumbers' interface CollectionAssetProps { asset: GenieAsset @@ -31,6 +31,7 @@ export const CollectionAsset = ({ renderedHeight, setRenderedHeight, }: CollectionAssetProps) => { + const { formatEther } = useFormatter() const bagManuallyClosed = useBag((state) => state.bagManuallyClosed) const addAssetsToBag = useBag((state) => state.addAssetsToBag) const removeAssetsFromBag = useBag((state) => state.removeAssetsFromBag) @@ -76,12 +77,23 @@ export const CollectionAsset = ({ primaryInfo: asset.name ? asset.name : `#${asset.tokenId}`, primaryInfoIcon: asset.susFlag ? : null, primaryInfoRight: asset.rarity && provider ? : null, - secondaryInfo: notForSale ? '' : `${formatWeiToDecimal(asset.priceInfo.ETHPrice, true)} ETH`, + secondaryInfo: notForSale + ? '' + : `${formatEther({ input: asset.priceInfo.ETHPrice, type: NumberType.NFTToken })} ETH`, selectedInfo: Remove from bag, notSelectedInfo: Add to bag, disabledInfo: Not listed, } - }, [asset.name, asset.priceInfo.ETHPrice, asset.rarity, asset.susFlag, asset.tokenId, notForSale, provider]) + }, [ + asset.name, + asset.priceInfo.ETHPrice, + asset.rarity, + asset.susFlag, + asset.tokenId, + formatEther, + notForSale, + provider, + ]) return ( )) const StatsRow = ({ stats, isMobile, ...props }: { stats: GenieCollection; isMobile?: boolean } & BoxProps) => { + const { formatNumberOrString, formatDelta } = useFormatter() + const uniqueOwnersPercentage = stats?.stats?.total_supply ? roundWholePercentage(((stats.stats.num_owners ?? 0) / stats.stats.total_supply) * 100) : 0 - const totalSupplyStr = stats.stats ? quantityFormatter(stats.stats.total_supply ?? 0) : 0 + const totalSupplyStr = stats.stats + ? formatNumberOrString({ input: stats.stats.total_supply ?? 0, type: NumberType.NFTCollectionStats }) + : 0 const listedPercentageStr = stats?.stats?.total_supply ? roundWholePercentage(((stats.stats.total_listings ?? 0) / stats.stats.total_supply) * 100) : 0 const isCollectionStatsLoading = useIsCollectionLoading((state) => state.isCollectionStatsLoading) // round daily volume & floorPrice to 3 decimals or less - const totalVolumeStr = volumeFormatter(Number(stats.stats?.total_volume) ?? 0) - const floorPriceStr = floorFormatter(stats.stats?.floor_price ?? 0) + const totalVolumeStr = formatNumberOrString({ + input: Number(stats.stats?.total_volume) ?? 0, + type: NumberType.NFTCollectionStats, + }) + const floorPriceStr = formatNumberOrString({ + input: stats.stats?.floor_price ?? 0, + type: NumberType.NFTTokenFloorPrice, + }) // graphQL formatted %age values out of 100, whereas v3 endpoint did a decimal between 0 & 1 - const floorChangeStr = Math.round(Math.abs(stats?.stats?.one_day_floor_change ?? 0)) + const floorChangeStr = formatDelta(Math.round(Math.abs(stats?.stats?.one_day_floor_change ?? 0))) const isBagExpanded = useBag((state) => state.bagExpanded) const isScreenSize = useScreenSize() @@ -372,7 +383,7 @@ const StatsRow = ({ stats, isMobile, ...props }: { stats: GenieCollection; isMob - {floorChangeStr}% + {floorChangeStr} ) : null} diff --git a/src/nft/components/collection/FilterButton.tsx b/src/nft/components/collection/FilterButton.tsx index 1877504da7..24b972151b 100644 --- a/src/nft/components/collection/FilterButton.tsx +++ b/src/nft/components/collection/FilterButton.tsx @@ -4,7 +4,8 @@ import * as styles from 'nft/components/collection/FilterButton.css' import { FilterIcon } from 'nft/components/icons' import { buttonTextMedium } from 'nft/css/common.css' import { breakpoints } from 'nft/css/sprinkles.css' -import { pluralize, putCommas } from 'nft/utils' +import { pluralize } from 'nft/utils' +import { NumberType, useFormatter } from 'utils/formatNumbers' export const FilterButton = ({ onClick, @@ -17,6 +18,7 @@ export const FilterButton = ({ onClick: () => void collectionCount?: number }) => { + const { formatNumberOrString } = useFormatter() const hideResultsCount = window.innerWidth >= breakpoints.sm && window.innerWidth < breakpoints.md return ( @@ -41,7 +43,10 @@ export const FilterButton = ({ {' '} {!collectionCount || hideResultsCount ? 'Filter' - : `Filter • ${putCommas(collectionCount)} result${pluralize(collectionCount)}`} + : `Filter • ${formatNumberOrString({ + input: collectionCount, + type: NumberType.WholeNumber, + })} result${pluralize(collectionCount)}`} ) : null} diff --git a/src/nft/components/collection/Sweep.tsx b/src/nft/components/collection/Sweep.tsx index c88c10efde..3cb7f76d04 100644 --- a/src/nft/components/collection/Sweep.tsx +++ b/src/nft/components/collection/Sweep.tsx @@ -1,16 +1,17 @@ import 'rc-slider/assets/index.css' import { BigNumber } from '@ethersproject/bignumber' -import { formatEther, parseEther } from '@ethersproject/units' +import { formatEther as ethersFormatEther, parseEther } from '@ethersproject/units' import { Trans } from '@lingui/macro' import { SweepFetcherParams, useSweepNftAssets } from 'graphql/data/nft/Asset' import { useBag, useCollectionFilters } from 'nft/hooks' import { GenieAsset, isPooledMarket, Markets } from 'nft/types' -import { calcPoolPrice, formatWeiToDecimal, isInSameSudoSwapPool } from 'nft/utils' +import { calcPoolPrice, isInSameSudoSwapPool } from 'nft/utils' import { default as Slider } from 'rc-slider' import { useEffect, useMemo, useReducer, useState } from 'react' import styled, { useTheme } from 'styled-components' import { ThemedText } from 'theme/components' +import { NumberType, useFormatter } from 'utils/formatNumbers' const SweepContainer = styled.div` display: flex; @@ -160,6 +161,7 @@ interface SweepProps { export const Sweep = ({ contractAddress, minPrice, maxPrice }: SweepProps) => { const theme = useTheme() + const { formatEther } = useFormatter() const [isItemsToggled, toggleSweep] = useReducer((state) => !state, true) const [sweepAmount, setSweepAmount] = useState('') @@ -374,8 +376,12 @@ export const Sweep = ({ contractAddress, minPrice, maxPrice }: SweepProps) => { { - {`${formatWeiToDecimal( - sweepEthPrice.toString() - )} ETH`} + {`${formatEther({ + input: sweepEthPrice.toString(), + type: NumberType.NFTToken, + })} ETH`} diff --git a/src/nft/components/collection/TransactionCompleteModal.tsx b/src/nft/components/collection/TransactionCompleteModal.tsx index cc8b8fb56d..72d033cf6e 100644 --- a/src/nft/components/collection/TransactionCompleteModal.tsx +++ b/src/nft/components/collection/TransactionCompleteModal.tsx @@ -1,4 +1,4 @@ -import { formatEther } from '@ethersproject/units' +import { formatEther as ethersFormatEther } from '@ethersproject/units' import { Trans } from '@lingui/macro' import { InterfaceModalName, NFTEventName } from '@uniswap/analytics-events' import { Trace, useTrace } from 'analytics' @@ -12,17 +12,11 @@ import { Overlay, stopPropagation } from 'nft/components/modals/Overlay' import { themeVars, vars } from 'nft/css/sprinkles.css' import { useIsMobile, useNativeUsdPrice, useSendTransaction, useTransactionResponse } from 'nft/hooks' import { TxResponse, TxStateType } from 'nft/types' -import { - formatEthPrice, - formatUsdPrice, - formatUSDPriceWithCommas, - generateTweetForPurchase, - getSuccessfulImageSize, - parseTransactionResponse, -} from 'nft/utils' +import { generateTweetForPurchase, getSuccessfulImageSize, parseTransactionResponse } from 'nft/utils' import { formatAssetEventProperties } from 'nft/utils/formatEventProperties' import { useEffect, useMemo, useRef, useState } from 'react' import styled from 'styled-components' +import { NumberType, useFormatter } from 'utils/formatNumbers' import { ExplorerDataType, getExplorerLink } from 'utils/getExplorerLink' import * as styles from './TransactionCompleteModal.css' @@ -47,6 +41,7 @@ const UploadLink = styled.a` const TxCompleteModal = () => { const ethUsdPrice = useNativeUsdPrice() + const { formatEther, formatNumberOrString } = useFormatter() const [showUnavailable, setShowUnavailable] = useState(false) const txHash = useSendTransaction((state) => state.txHash) const purchasedWithErc20 = useSendTransaction((state) => state.purchasedWithErc20) @@ -107,7 +102,7 @@ const TxCompleteModal = () => { name={NFTEventName.NFT_BUY_BAG_SUCCEEDED} properties={{ buy_quantity: nftsPurchased.length, - usd_value: parseFloat(formatEther(totalPurchaseValue)) * ethUsdPrice, + usd_value: parseFloat(ethersFormatEther(totalPurchaseValue)) * ethUsdPrice, transaction_hash: txHash, using_erc20: purchasedWithErc20, ...formatAssetEventProperties(nftsPurchased), @@ -168,7 +163,13 @@ const TxCompleteModal = () => { {nftsPurchased.length} NFT{nftsPurchased.length === 1 ? '' : 's'} - {formatEthPrice(totalPurchaseValue.toString())} ETH + + {formatEther({ + input: totalPurchaseValue.toString(), + type: NumberType.NFTToken, + })}{' '} + ETH + @@ -205,7 +206,13 @@ const TxCompleteModal = () => {

Instant Refund

Uniswap returned{' '} - {formatEthPrice(totalRefundValue.toString())} ETH{' '} + + {formatEther({ + input: totalRefundValue.toString(), + type: NumberType.NFTToken, + })}{' '} + ETH + {' '} back to your wallet for unavailable items.

{ position={{ sm: 'absolute', md: 'static' }} >

- {formatEthPrice(totalRefundValue.toString())} ETH + {formatEther({ + input: totalRefundValue.toString(), + type: NumberType.NFTToken, + })}{' '} + ETH +

+

+ {formatNumberOrString({ input: totalUSDRefund, type: NumberType.FiatNFTToken })}

-

{formatUSDPriceWithCommas(totalUSDRefund)}

for {nftsNotPurchased.length} unavailable item {nftsNotPurchased.length === 1 ? '' : 's'}. @@ -280,8 +293,9 @@ const TxCompleteModal = () => { `Selected item${ nftsPurchased.length === 1 ? ' is' : 's are' } no longer available. Uniswap instantly refunded you for this incomplete transaction. `} - {formatUsdPrice(txFeeFiat)} was used for gas in attempt to complete this transaction. For support, - please visit our Discord + {formatNumberOrString({ input: txFeeFiat, type: NumberType.FiatNFTToken })} was used for gas in + attempt to complete this transaction. For support, please visit our{' '} + Discord

{nftsNotPurchased.length >= 3 && ( @@ -324,9 +338,12 @@ const TxCompleteModal = () => {

- {formatEthPrice( - asset.updatedPriceInfo ? asset.updatedPriceInfo.ETHPrice : asset.priceInfo.ETHPrice - )}{' '} + {formatEther({ + input: asset.updatedPriceInfo + ? asset.updatedPriceInfo.ETHPrice + : asset.priceInfo.ETHPrice, + type: NumberType.NFTToken, + })}{' '} ETH

@@ -339,9 +356,15 @@ const TxCompleteModal = () => {
{showUnavailable && }

- {formatEthPrice(totalRefundValue.toString())} ETH + {formatEther({ + input: totalRefundValue.toString(), + type: NumberType.NFTToken, + })}{' '} + ETH +

+

+ {formatNumberOrString({ input: totalUSDRefund, type: NumberType.FiatNFTToken })}

-

{formatUSDPriceWithCommas(totalUSDRefund)}

View on Etherscan diff --git a/src/nft/components/details/AssetActivity.tsx b/src/nft/components/details/AssetActivity.tsx index 84015a3d09..8ac016985f 100644 --- a/src/nft/components/details/AssetActivity.tsx +++ b/src/nft/components/details/AssetActivity.tsx @@ -4,11 +4,11 @@ import { LoadingBubble } from 'components/Tokens/loading' import { EventCell } from 'nft/components/collection/ActivityCells' import { ActivityEvent } from 'nft/types' import { getMarketplaceIcon } from 'nft/utils' -import { formatEth } from 'nft/utils/currency' import { getTimeDifference } from 'nft/utils/date' import { ReactNode } from 'react' import styled from 'styled-components' import { shortenAddress } from 'utils' +import { NumberType, useFormatter } from 'utils/formatNumbers' const TR = styled.tr` border-bottom: ${({ theme }) => `1px solid ${theme.surface3}`}; @@ -148,12 +148,15 @@ export const LoadingAssetActivity = ({ rowCount }: { rowCount: number }) => { } const AssetActivity = ({ events }: { events?: ActivityEvent[] }) => { + const { formatNumberOrString } = useFormatter() return ( {events && events.map((event, index) => { const { eventTimestamp, eventType, fromAddress, marketplace, price, toAddress, transactionHash } = event - const formattedPrice = price ? formatEth(parseFloat(price ?? '')) : null + const formattedPrice = price + ? formatNumberOrString({ input: parseFloat(price), type: NumberType.NFTToken }) + : null if (!eventType) return null return ( diff --git a/src/nft/components/details/AssetDetails.tsx b/src/nft/components/details/AssetDetails.tsx index dc74184e5b..92e3b55dc1 100644 --- a/src/nft/components/details/AssetDetails.tsx +++ b/src/nft/components/details/AssetDetails.tsx @@ -10,15 +10,14 @@ import { AssetPriceDetails } from 'nft/components/details/AssetPriceDetails' import { Center } from 'nft/components/Flex' import { themeVars, vars } from 'nft/css/sprinkles.css' import { ActivityEventType, CollectionInfoForAsset, GenieAsset } from 'nft/types' -import { formatEth } from 'nft/utils/currency' import { isAudio } from 'nft/utils/isAudio' import { isVideo } from 'nft/utils/isVideo' -import { putCommas } from 'nft/utils/putCommas' import { useCallback, useMemo, useReducer, useState } from 'react' import InfiniteScroll from 'react-infinite-scroll-component' import { Link as RouterLink } from 'react-router-dom' import styled from 'styled-components' import { shortenAddress } from 'utils/addresses' +import { NumberType, useFormatter } from 'utils/formatNumbers' import AssetActivity, { LoadingAssetActivity } from './AssetActivity' import * as styles from './AssetDetails.css' @@ -243,6 +242,7 @@ interface AssetDetailsProps { } export const AssetDetails = ({ asset, collection }: AssetDetailsProps) => { + const { formatNumberOrString } = useFormatter() const [dominantColor] = useState<[number, number, number]>([0, 0, 0]) const { rarityProvider } = useMemo( @@ -281,7 +281,9 @@ export const AssetDetails = ({ asset, collection }: AssetDetailsProps) => { ) const weiPrice = gqlPriceData?.[0]?.price - const formattedPrice = weiPrice ? formatEth(parseFloat(weiPrice)) : undefined + const formattedPrice = weiPrice + ? formatNumberOrString({ input: parseFloat(weiPrice), type: NumberType.NFTToken }) + : undefined const [activeFilters, filtersDispatch] = useReducer(reduceFilters, initialFilterState) const Filter = useCallback( @@ -359,7 +361,9 @@ export const AssetDetails = ({ asset, collection }: AssetDetailsProps) => { } placement="top" > - Rarity: {putCommas(rarity.score)} + + Rarity: {formatNumberOrString({ input: rarity.score, type: NumberType.WholeNumber })} + ) : null } diff --git a/src/nft/components/details/AssetPriceDetails.tsx b/src/nft/components/details/AssetPriceDetails.tsx index 6dc5ed21c4..d5749b1683 100644 --- a/src/nft/components/details/AssetPriceDetails.tsx +++ b/src/nft/components/details/AssetPriceDetails.tsx @@ -7,18 +7,13 @@ import { useNftBalance } from 'graphql/data/nft/NftBalance' import { CancelListingIcon, VerifiedIcon } from 'nft/components/icons' import { useBag, useNativeUsdPrice, useProfilePageState, useSellAsset, useUsdPriceofNftAsset } from 'nft/hooks' import { CollectionInfoForAsset, GenieAsset, ProfilePageStateType, WalletAsset } from 'nft/types' -import { - ethNumberStandardFormatter, - formatEthPrice, - generateTweetForAsset, - getMarketplaceIcon, - timeLeft, -} from 'nft/utils' +import { generateTweetForAsset, getMarketplaceIcon, timeLeft } from 'nft/utils' import { useMemo } from 'react' import { Link, useNavigate } from 'react-router-dom' import styled, { css, useTheme } from 'styled-components' import { ExternalLink, ThemedText } from 'theme/components' import { shortenAddress } from 'utils/addresses' +import { NumberType, useFormatter } from 'utils/formatNumbers' const TWITTER_WIDTH = 560 const TWITTER_HEIGHT = 480 @@ -213,6 +208,7 @@ const OwnerContainer = ({ asset }: { asset: WalletAsset }) => { const setSellPageState = useProfilePageState((state) => state.setProfilePageState) const selectSellAsset = useSellAsset((state) => state.selectSellAsset) const resetSellAssets = useSellAsset((state) => state.reset) + const { formatEther, formatNumberOrString } = useFormatter() const listing = asset.sellOrders && asset.sellOrders.length > 0 ? asset.sellOrders[0] : undefined const expirationDate = listing?.endAt ? new Date(listing.endAt) : undefined @@ -249,11 +245,15 @@ const OwnerContainer = ({ asset }: { asset: WalletAsset }) => { {listing ? ( <> - {formatEthPrice(asset.priceInfo?.ETHPrice)} ETH + {formatEther({ + input: asset.priceInfo?.ETHPrice, + type: NumberType.NFTToken, + })}{' '} + ETH {USDPrice && ( - {ethNumberStandardFormatter(USDPrice, true, true)} + {formatNumberOrString({ input: USDPrice, type: NumberType.FiatNFTToken })} )} @@ -311,6 +311,7 @@ const NotForSale = ({ collectionName, collectionUrl }: { collectionName: string; export const AssetPriceDetails = ({ asset, collection }: AssetPriceDetailsProps) => { const { account } = useWeb3React() + const { formatEther, formatNumberOrString } = useFormatter() const cheapestOrder = asset.sellorders && asset.sellorders.length > 0 ? asset.sellorders[0] : undefined const expirationDate = cheapestOrder?.endAt ? new Date(cheapestOrder.endAt) : undefined @@ -376,11 +377,11 @@ export const AssetPriceDetails = ({ asset, collection }: AssetPriceDetailsProps) - {formatEthPrice(asset.priceInfo.ETHPrice)} ETH + {formatEther({ input: asset.priceInfo.ETHPrice, type: NumberType.NFTToken })} ETH {USDPrice && ( - {ethNumberStandardFormatter(USDPrice, true, true)} + {formatNumberOrString({ input: USDPrice, type: NumberType.FiatNFTToken })} )} diff --git a/src/nft/components/details/DetailsContainer.tsx b/src/nft/components/details/DetailsContainer.tsx index e3ce7a564d..3d192255cf 100644 --- a/src/nft/components/details/DetailsContainer.tsx +++ b/src/nft/components/details/DetailsContainer.tsx @@ -1,11 +1,11 @@ import { OpacityHoverState } from 'components/Common' import useCopyClipboard from 'hooks/useCopyClipboard' import { CollectionInfoForAsset, GenieAsset } from 'nft/types' -import { putCommas } from 'nft/utils' import { useCallback } from 'react' import { Copy } from 'react-feather' import styled from 'styled-components' import { shortenAddress } from 'utils' +import { NumberType, useFormatter } from 'utils/formatNumbers' const Details = styled.div` display: grid; @@ -66,6 +66,7 @@ const GridItem = ({ header, body }: { header: string; body: React.ReactNode }) = const stringShortener = (text: string) => `${text.substring(0, 4)}...${text.substring(text.length - 4, text.length)}` const DetailsContainer = ({ asset, collection }: { asset: GenieAsset; collection: CollectionInfoForAsset }) => { + const { formatNumber } = useFormatter() const { address, tokenId, tokenType, creator } = asset const { totalSupply } = collection @@ -87,7 +88,10 @@ const DetailsContainer = ({ asset, collection }: { asset: GenieAsset; collection 9 ? stringShortener(tokenId) : tokenId} /> - + { const { data: gqlCollection, loading } = useCollection(collection.address ?? '') + const { formatNumber } = useFormatter() if (loading) return @@ -257,7 +257,7 @@ export const CarouselCard = ({ collection, onClick }: CarouselCardProps) => { {collection.floor && ( - {ethNumberStandardFormatter(collection.floor)} ETH Floor + {formatNumber({ input: collection.floor, type: NumberType.NFTToken })} ETH Floor )} diff --git a/src/nft/components/explore/Cells/Cells.tsx b/src/nft/components/explore/Cells/Cells.tsx index 91c61ee814..a58ba536cf 100644 --- a/src/nft/components/explore/Cells/Cells.tsx +++ b/src/nft/components/explore/Cells/Cells.tsx @@ -3,10 +3,10 @@ import { DeltaArrow } from 'components/Tokens/TokenDetails/Delta' import { VerifiedIcon } from 'nft/components/icons' import { useIsMobile } from 'nft/hooks' import { Denomination } from 'nft/types' -import { ethNumberStandardFormatter, volumeFormatter } from 'nft/utils' import { ReactNode } from 'react' import styled from 'styled-components' import { ThemedText } from 'theme/components' +import { NumberType, useFormatter } from 'utils/formatNumbers' import * as styles from './Cells.css' @@ -93,9 +93,12 @@ export const CollectionTitleCell = ({ value }: CellProps) => { ) } -export const DiscreteNumberCell = ({ value }: CellProps) => ( - {value.value ? volumeFormatter(value.value) : '-'} -) +export const DiscreteNumberCell = ({ value }: CellProps) => { + const { formatNumberOrString } = useFormatter() + return ( + {value.value ? formatNumberOrString({ input: value.value, type: NumberType.NFTCollectionStats }) : '-'} + ) +} const getDenominatedValue = (denomination: Denomination, inWei: boolean, value?: number, usdPrice?: number) => { if (denomination === Denomination.ETH) return value @@ -113,12 +116,14 @@ export const EthCell = ({ denomination: Denomination usdPrice?: number }) => { + const { formatNumberOrString } = useFormatter() const denominatedValue = getDenominatedValue(denomination, false, value, usdPrice) - const formattedValue = denominatedValue - ? denomination === Denomination.ETH - ? ethNumberStandardFormatter(denominatedValue.toString(), false, true, false) + ' ETH' - : ethNumberStandardFormatter(denominatedValue, true, false, true) - : '-' + const ethDenomination = denomination === Denomination.ETH + const formattedValue = + formatNumberOrString({ + input: denominatedValue, + type: ethDenomination ? NumberType.NFTToken : NumberType.FiatTokenStats, + }) + (ethDenomination ? ' ETH' : '') const isMobile = useIsMobile() const TextComponent = isMobile ? ThemedText.BodySmall : ThemedText.BodyPrimary @@ -141,17 +146,19 @@ export const VolumeCell = ({ denomination: Denomination usdPrice?: number }) => { + const { formatNumberOrString } = useFormatter() const denominatedValue = getDenominatedValue(denomination, false, value, usdPrice) + const ethDenomination = denomination === Denomination.ETH - const formattedValue = denominatedValue - ? denomination === Denomination.ETH - ? ethNumberStandardFormatter(denominatedValue.toString(), false, false, true) + ' ETH' - : ethNumberStandardFormatter(denominatedValue, true, false, true) - : '-' + const formattedValue = + formatNumberOrString({ + input: denominatedValue, + type: ethDenomination ? NumberType.WholeNumber : NumberType.FiatTokenStats, + }) + (ethDenomination ? ' ETH' : '') return ( - {value ? formattedValue : '-'} + {formattedValue} ) } diff --git a/src/nft/components/explore/TrendingCollections.tsx b/src/nft/components/explore/TrendingCollections.tsx index 79becbef25..55df3eecc4 100644 --- a/src/nft/components/explore/TrendingCollections.tsx +++ b/src/nft/components/explore/TrendingCollections.tsx @@ -6,6 +6,7 @@ import { CollectionTableColumn, Denomination, TimePeriod, VolumeType } from 'nft import { useMemo, useState } from 'react' import styled from 'styled-components' import { ThemedText } from 'theme/components' +import { useFormatterLocales } from 'utils/formatNumbers' import CollectionTable from './CollectionTable' @@ -84,6 +85,7 @@ function convertTimePeriodToHistoryDuration(timePeriod: TimePeriod): HistoryDura } const TrendingCollections = () => { + const { formatterLocalCurrency } = useFormatterLocales() const [timePeriod, setTimePeriod] = useState(TimePeriod.OneDay) const [isEthToggled, setEthToggled] = useState(true) @@ -151,7 +153,7 @@ const TrendingCollections = () => { - USD + {formatterLocalCurrency} diff --git a/src/nft/components/layout/Input.tsx b/src/nft/components/layout/Input.tsx index 91f7b4bd97..572462cc4e 100644 --- a/src/nft/components/layout/Input.tsx +++ b/src/nft/components/layout/Input.tsx @@ -1,8 +1,12 @@ -import { isNumber } from 'nft/utils/numbers' import { FormEvent, forwardRef } from 'react' import { Box, BoxProps } from '../Box' +const isNumber = (s: string): boolean => { + const reg = /^-?\d+\.?\d*$/ + return reg.test(s) && !isNaN(parseFloat(s)) && isFinite(parseFloat(s)) +} + export const Input = forwardRef((props, ref) => ( ` ` export const ListPage = () => { + const { formatNumberOrString } = useFormatter() const { setProfilePageState: setSellPageState } = useProfilePageState() const { provider, chainId } = useWeb3React() const isMobile = useIsMobile() @@ -295,7 +295,10 @@ export const ListPage = () => { - {totalEthListingValue > 0 ? formatEth(totalEthListingValue) : '-'} ETH + {totalEthListingValue > 0 + ? formatNumberOrString({ input: totalEthListingValue, type: NumberType.NFTToken }) + : '-'}{' '} + ETH {!!usdcValue && {usdcAmount}} diff --git a/src/nft/components/profile/list/MarketplaceRow.tsx b/src/nft/components/profile/list/MarketplaceRow.tsx index ab1b3e4598..5c10d305a6 100644 --- a/src/nft/components/profile/list/MarketplaceRow.tsx +++ b/src/nft/components/profile/list/MarketplaceRow.tsx @@ -8,11 +8,11 @@ import { useSellAsset } from 'nft/hooks' import { useNativeUsdPrice } from 'nft/hooks/useUsdPrice' import { ListingMarket, WalletAsset } from 'nft/types' import { getMarketplaceIcon } from 'nft/utils' -import { formatEth, formatUsdPrice } from 'nft/utils/currency' import { Dispatch, DispatchWithoutAction, useCallback, useEffect, useMemo, useReducer, useState } from 'react' import styled from 'styled-components' import { BREAKPOINTS } from 'theme' import { ThemedText } from 'theme/components' +import { NumberType, useFormatter } from 'utils/formatNumbers' import { PriceTextInput } from './PriceTextInput' import { RoyaltyTooltip } from './RoyaltyTooltip' @@ -126,6 +126,7 @@ export const MarketplaceRow = ({ toggleExpandMarketplaceRows, rowHovered, }: MarketplaceRowProps) => { + const { formatNumberOrString, formatDelta } = useFormatter() const setAssetListPrice = useSellAsset((state) => state.setAssetListPrice) const removeAssetMarketplace = useSellAsset((state) => state.removeAssetMarketplace) const [marketIconHovered, toggleMarketIconHovered] = useReducer((s) => !s, false) @@ -184,12 +185,17 @@ export const MarketplaceRow = ({ - {asset.floorPrice ? `${asset.floorPrice.toFixed(3)} ETH` : '-'} + {formatNumberOrString({ + input: asset.floorPrice, + type: NumberType.NFTToken, + }) + asset.floorPrice + ? ' ETH' + : ''} - {asset.lastPrice ? `${asset.lastPrice.toFixed(3)} ETH` : '-'} + {asset.lastPrice ? `${formatNumberOrString({ input: asset.lastPrice, type: NumberType.NFTToken })} ETH` : '-'} @@ -235,7 +241,7 @@ export const MarketplaceRow = ({ > - {fees > 0 ? `${fees.toFixed(2)}${selectedMarkets.length > 1 ? t`% max` : '%'}` : '--%'} + {fees > 0 ? `${formatDelta(fees)}${selectedMarkets.length > 1 ? t`max` : ''}` : '--%'} @@ -249,6 +255,7 @@ export const MarketplaceRow = ({ } const EthPriceDisplay = ({ ethPrice = 0 }: { ethPrice?: number }) => { + const { formatNumberOrString } = useFormatter() const ethUsdPrice = useNativeUsdPrice() return ( @@ -256,8 +263,10 @@ const EthPriceDisplay = ({ ethPrice = 0 }: { ethPrice?: number }) => { {ethPrice !== 0 ? ( - {formatEth(ethPrice)} ETH - {formatUsdPrice(ethPrice * ethUsdPrice)} + {formatNumberOrString({ input: ethPrice, type: NumberType.NFTToken })} ETH + + {formatNumberOrString({ input: ethPrice * ethUsdPrice, type: NumberType.FiatNFTToken })} + ) : ( '- ETH' diff --git a/src/nft/components/profile/list/Modal/BelowFloorWarningModal.tsx b/src/nft/components/profile/list/Modal/BelowFloorWarningModal.tsx index e72377ad7d..fc3f2e39ec 100644 --- a/src/nft/components/profile/list/Modal/BelowFloorWarningModal.tsx +++ b/src/nft/components/profile/list/Modal/BelowFloorWarningModal.tsx @@ -10,6 +10,7 @@ import styled, { useTheme } from 'styled-components' import { BREAKPOINTS } from 'theme' import { ThemedText } from 'theme/components' import { Z_INDEX } from 'theme/zIndex' +import { useFormatter } from 'utils/formatNumbers' const ModalWrapper = styled(Column)` position: fixed; @@ -82,6 +83,7 @@ export const BelowFloorWarningModal = ({ startListing: () => void }) => { const theme = useTheme() + const { formatDelta } = useFormatter() const clickContinue = (e: React.MouseEvent) => { e.preventDefault() e.stopPropagation() @@ -103,10 +105,9 @@ export const BelowFloorWarningModal = ({   diff --git a/src/nft/components/profile/list/Modal/SuccessScreen.tsx b/src/nft/components/profile/list/Modal/SuccessScreen.tsx index 0c0fc75cd4..75df33a93e 100644 --- a/src/nft/components/profile/list/Modal/SuccessScreen.tsx +++ b/src/nft/components/profile/list/Modal/SuccessScreen.tsx @@ -8,7 +8,7 @@ import useNativeCurrency from 'lib/hooks/useNativeCurrency' import tryParseCurrencyAmount from 'lib/utils/tryParseCurrencyAmount' import { getTotalEthValue } from 'nft/components/profile/list/utils' import { useSellAsset } from 'nft/hooks' -import { formatEth, generateTweetForList, pluralize } from 'nft/utils' +import { generateTweetForList, pluralize } from 'nft/utils' import { useMemo } from 'react' import { Twitter, X } from 'react-feather' import styled, { css, useTheme } from 'styled-components' @@ -77,6 +77,7 @@ const TweetRow = styled(Row)` export const SuccessScreen = ({ overlayClick }: { overlayClick: () => void }) => { const theme = useTheme() + const { formatNumberOrString } = useFormatter() const sellAssets = useSellAsset((state) => state.sellAssets) const { chainId } = useWeb3React() const nativeCurrency = useNativeCurrency(chainId) @@ -109,7 +110,9 @@ export const SuccessScreen = ({ overlayClick }: { overlayClick: () => void }) => Proceeds if sold - {formatEth(totalEthListingValue)} ETH + + {formatNumberOrString({ input: totalEthListingValue, type: NumberType.NFTToken })} ETH + {usdcValue && ( {formatCurrencyAmount({ diff --git a/src/nft/components/profile/list/PriceTextInput.tsx b/src/nft/components/profile/list/PriceTextInput.tsx index e1f436ecc4..db8051de0e 100644 --- a/src/nft/components/profile/list/PriceTextInput.tsx +++ b/src/nft/components/profile/list/PriceTextInput.tsx @@ -7,12 +7,12 @@ import { useUpdateInputAndWarnings } from 'nft/components/profile/list/utils' import { body } from 'nft/css/common.css' import { useSellAsset } from 'nft/hooks' import { WalletAsset } from 'nft/types' -import { formatEth } from 'nft/utils/currency' import { Dispatch, useRef, useState } from 'react' import { AlertTriangle, Link } from 'react-feather' import styled, { useTheme } from 'styled-components' import { BREAKPOINTS } from 'theme' import { colors } from 'theme/colors' +import { NumberType, useFormatter } from 'utils/formatNumbers' import { WarningType } from './shared' @@ -104,6 +104,7 @@ export const PriceTextInput = ({ globalOverride, asset, }: PriceTextInputProps) => { + const { formatNumberOrString, formatDelta } = useFormatter() const [warningType, setWarningType] = useState(WarningType.NONE) const removeSellAsset = useSellAsset((state) => state.removeSellAsset) const showResolveIssues = useSellAsset((state) => state.showResolveIssues) @@ -159,10 +160,14 @@ export const PriceTextInput = ({ - {warningType === WarningType.BELOW_FLOOR && `${percentBelowFloor.toFixed(0)}% `} + {warningType === WarningType.BELOW_FLOOR && `${formatDelta(percentBelowFloor)} `} {getWarningMessage(warningType)}   - {warningType === WarningType.ALREADY_LISTED && `${formatEth(asset?.floor_sell_order_price ?? 0)} ETH`} + {warningType === WarningType.ALREADY_LISTED && + `${formatNumberOrString({ + input: asset?.floor_sell_order_price ?? 0, + type: NumberType.NFTToken, + })} ETH`} { diff --git a/src/nft/components/profile/list/RoyaltyTooltip.tsx b/src/nft/components/profile/list/RoyaltyTooltip.tsx index d842e455af..f6f1cd7585 100644 --- a/src/nft/components/profile/list/RoyaltyTooltip.tsx +++ b/src/nft/components/profile/list/RoyaltyTooltip.tsx @@ -3,9 +3,10 @@ import Column from 'components/Column' import Row from 'components/Row' import { getRoyalty } from 'nft/components/profile/list/utils' import { ListingMarket, WalletAsset } from 'nft/types' -import { formatEth, getMarketplaceIcon } from 'nft/utils' +import { getMarketplaceIcon } from 'nft/utils' import styled, { css } from 'styled-components' import { ThemedText } from 'theme/components' +import { NumberType, useFormatter } from 'utils/formatNumbers' const FeeWrap = styled(Row)` margin-bottom: 4px; @@ -56,7 +57,8 @@ export const RoyaltyTooltip = ({ asset: WalletAsset fees?: number }) => { - const maxRoyalty = Math.max(...selectedMarkets.map((market) => getRoyalty(market, asset) ?? 0)).toFixed(2) + const { formatNumberOrString, formatDelta } = useFormatter() + const maxRoyalty = Math.max(...selectedMarkets.map((market) => getRoyalty(market, asset) ?? 0)) return ( {selectedMarkets.map((market) => ( @@ -68,7 +70,7 @@ export const RoyaltyTooltip = ({ fee - {market.fee}% + {formatDelta(market.fee)} ))} @@ -85,7 +87,7 @@ export const RoyaltyTooltip = ({ Max fees - {fees ? formatEth(fees) : '-'} ETH + {fees ? formatNumberOrString({ input: fees, type: NumberType.NFTToken }) : '-'} ETH diff --git a/src/nft/hooks/usePriceImpact.ts b/src/nft/hooks/usePriceImpact.ts index 5ffe4f476e..adac5587ca 100644 --- a/src/nft/hooks/usePriceImpact.ts +++ b/src/nft/hooks/usePriceImpact.ts @@ -1,7 +1,7 @@ -import { Percent } from '@uniswap/sdk-core' import { useMemo } from 'react' import { ClassicTrade } from 'state/routing/types' import { useTheme } from 'styled-components' +import { useFormatter } from 'utils/formatNumbers' import { computeRealizedPriceImpact, getPriceImpactWarning } from 'utils/prices' export interface PriceImpact { @@ -16,6 +16,7 @@ interface PriceImpactSeverity { export function usePriceImpact(trade?: ClassicTrade): PriceImpact | undefined { const theme = useTheme() + const { formatPercent } = useFormatter() return useMemo(() => { const marketPriceImpact = trade ? computeRealizedPriceImpact(trade) : undefined @@ -33,18 +34,8 @@ export function usePriceImpact(trade?: ClassicTrade): PriceImpact | undefined { type: priceImpactWarning, color: warningColor, }, - displayPercentage: () => toHumanReadablePercent(marketPriceImpact), + displayPercentage: () => formatPercent(marketPriceImpact), } : undefined - }, [theme.critical, theme.deprecated_accentWarning, trade]) -} - -function toHumanReadablePercent(priceImpact: Percent): string { - const sign = priceImpact.lessThan(0) ? '+' : '' - const exactFloat = (Number(priceImpact.numerator) / Number(priceImpact.denominator)) * 100 - if (exactFloat < 0.005) { - return '0.00%' - } - const number = parseFloat(priceImpact.multiply(-1)?.toFixed(2)) - return `${sign}${number}%` + }, [formatPercent, theme.critical, theme.deprecated_accentWarning, trade]) } diff --git a/src/nft/utils/buildActivityAsset.ts b/src/nft/utils/buildActivityAsset.ts index cef25531ae..99edde5fb7 100644 --- a/src/nft/utils/buildActivityAsset.ts +++ b/src/nft/utils/buildActivityAsset.ts @@ -1,10 +1,8 @@ import { parseEther } from 'ethers/lib/utils' import { ActivityEvent, GenieAsset } from 'nft/types' -import { formatEth } from './currency' - export const buildActivityAsset = (event: ActivityEvent, collectionName: string, ethPriceInUSD: number): GenieAsset => { - const assetUsdPrice = event.price ? formatEth(parseFloat(event.price) * ethPriceInUSD) : '0' + const assetUsdPrice = event.price ? parseFloat(event.price) * ethPriceInUSD : '0' const weiPrice = event.price ? parseEther(event.price) : '' diff --git a/src/nft/utils/currency.ts b/src/nft/utils/currency.ts index 7f3cc4ff1f..d131b37ccd 100644 --- a/src/nft/utils/currency.ts +++ b/src/nft/utils/currency.ts @@ -1,74 +1,3 @@ -import { formatEther } from '@ethersproject/units' - -export const formatUsdPrice = (price: number) => { - if (price > 1000000) { - return `$${(price / 1000000).toFixed(1)}M` - } else if (price > 1000) { - return `$${(price / 1000).toFixed(1)}K` - } else { - return `$${price.toFixed(2)}` - } -} - -export const formatEth = (price: number) => { - if (price > 1000000) { - return `${Math.round(price / 1000000)}M` - } else if (price > 1000) { - return `${Math.round(price / 1000)}K` - } else if (price < 0.001) { - return '<0.001' - } else { - return `${Math.round(price * 1000 + Number.EPSILON) / 1000}` - } -} - -export const formatUSDPriceWithCommas = (price: number) => { - return `$${Math.round(price) - .toString() - .replace(/\B(?=(\d{3})+(?!\d))/g, ',')}` -} - -export const formatEthPrice = (price: string | undefined) => { - if (!price) return 0 - - const formattedPrice = parseFloat(formatEther(String(price))) - return ( - Math.round(formattedPrice * (formattedPrice >= 1 ? 100 : 1000) + Number.EPSILON) / - (formattedPrice >= 1 ? 100 : 1000) - ) -} - -export const ethNumberStandardFormatter = ( - amount: string | number | undefined, - includeDollarSign = false, - removeZeroes = false, - roundToNearestWholeNumber = false -): string => { - if (!amount) return '-' - - const amountInDecimals = parseFloat(amount.toString()) - const conditionalDollarSign = includeDollarSign ? '$' : '' - - if (amountInDecimals <= 0) return '-' - if (amountInDecimals < 0.0001) return `< ${conditionalDollarSign}0.00001` - if (amountInDecimals < 1) return `${conditionalDollarSign}${parseFloat(amountInDecimals.toFixed(3))}` - const formattedPrice = ( - removeZeroes - ? parseFloat(amountInDecimals.toFixed(2)) - : roundToNearestWholeNumber - ? Math.round(amountInDecimals) - : amountInDecimals.toFixed(2) - ) - .toString() - .replace(/\B(?=(\d{3})+(?!\d))/g, ',') - return conditionalDollarSign + formattedPrice -} - -export const formatWeiToDecimal = (amount: string, removeZeroes = false) => { - if (!amount) return '-' - return ethNumberStandardFormatter(formatEther(amount), false, removeZeroes, false) -} - // prevent BigNumber overflow by properly handling scientific notation and comma delimited values export function wrapScientificNotation(value: string | number): string { return parseFloat(value.toString()) diff --git a/src/nft/utils/index.ts b/src/nft/utils/index.ts index eefa461951..c5e7287550 100644 --- a/src/nft/utils/index.ts +++ b/src/nft/utils/index.ts @@ -10,21 +10,11 @@ export { blocklistedCollections } from './blocklist' export { buildNftTradeInputFromBagItems } from './buildSellObject' export { calculateCardIndex, calculateFirstCardIndex, calculateRank } from './carousel' export { isInSameMarketplaceCollection, isInSameSudoSwapPool } from './collection' -export { - ethNumberStandardFormatter, - formatEth, - formatEthPrice, - formatUsdPrice, - formatUSDPriceWithCommas, - formatWeiToDecimal, - wrapScientificNotation, -} from './currency' +export { wrapScientificNotation } from './currency' export { formatAssetEventProperties } from './formatEventProperties' export { isAudio } from './isAudio' export { isVideo } from './isVideo' -export { floorFormatter, volumeFormatter } from './numbers' export { calcAvgGroupPoolPrice, calcPoolPrice, recalculateBagUsingPooledAssets } from './pooledAssets' -export { putCommas } from './putCommas' export { pluralize, roundAndPluralize } from './roundAndPluralize' export { timeLeft } from './time' export { getSuccessfulImageSize, parseTransactionResponse } from './transactionResponse' diff --git a/src/nft/utils/numbers.ts b/src/nft/utils/numbers.ts index def40ed87d..0d2d15f259 100644 --- a/src/nft/utils/numbers.ts +++ b/src/nft/utils/numbers.ts @@ -1,104 +1,3 @@ -import { DEFAULT_LOCALE } from 'constants/locales' -import numbro from 'numbro' - -export const isNumber = (s: string): boolean => { - const reg = /^-?\d+\.?\d*$/ - return reg.test(s) && !isNaN(parseFloat(s)) && isFinite(parseFloat(s)) -} - -export const floorFormatter = (n: number): string => { - if (n === 0) return '0.00' - if (!n) return '' - if (n < 0.001) { - return '<0.001' - } - if (n >= 0.001 && n < 1) { - return `${parseFloat(n.toFixed(3)).toLocaleString(DEFAULT_LOCALE, { - minimumFractionDigits: 1, - maximumFractionDigits: 3, - })}` - } - if (n >= 1 && n < 1e6) { - return `${parseFloat(n.toPrecision(6)).toLocaleString(DEFAULT_LOCALE, { - minimumFractionDigits: 0, - maximumFractionDigits: 2, - })}` - } - if (n >= 1e6 && n < 1e15) { - return numbro(n) - .format({ - average: true, - mantissa: 2, - optionalMantissa: true, - abbreviations: { - million: 'M', - billion: 'B', - trillion: 'T', - }, - }) - .toUpperCase() - } - if (n >= 1e15) { - return `${n.toExponential(3).replace(/(\.[0-9]*[1-9])0*|(\.0*)/, '$1')}` - } - return `${Number(n.toFixed(2)).toLocaleString(DEFAULT_LOCALE, { minimumFractionDigits: 2 })}` -} - -export const volumeFormatter = (n: number): string => { - if (n === 0) return '0.00' - if (!n) return '' - if (n < 0.01) { - return '<0.01' - } - if (n >= 0.01 && n < 1) { - return `${parseFloat(n.toFixed(2)).toLocaleString(DEFAULT_LOCALE)}` - } - if (n >= 1 && n < 1000) { - return `${Number(Math.round(n).toLocaleString(DEFAULT_LOCALE))}` - } - if (n >= 1000) { - return numbro(n) - .format({ - average: true, - mantissa: 1, - optionalMantissa: true, - abbreviations: { - thousand: 'K', - million: 'M', - billion: 'B', - trillion: 'T', - }, - }) - .toUpperCase() - } - return `${Number(n.toFixed(1)).toLocaleString(DEFAULT_LOCALE, { minimumFractionDigits: 1 })}` -} - -export const quantityFormatter = (n: number): string => { - if (n === 0) return '0.00' - if (!n) return '' - if (n >= 1 && n < 1000) { - return `${Number(Math.round(n).toLocaleString(DEFAULT_LOCALE))}` - } - if (n >= 1000) { - return numbro(n) - .format({ - average: true, - mantissa: 1, - thousandSeparated: true, - optionalMantissa: true, - abbreviations: { - thousand: 'K', - million: 'M', - billion: 'B', - trillion: 'T', - }, - }) - .toUpperCase() - } - return `${Number(n.toFixed(2)).toLocaleString(DEFAULT_LOCALE, { minimumFractionDigits: 2 })}` -} - export const roundWholePercentage = (n: number): string => { if (n === 0) return '0' if (!n) return '' diff --git a/src/nft/utils/putCommas.ts b/src/nft/utils/putCommas.ts deleted file mode 100644 index bd0b114aa0..0000000000 --- a/src/nft/utils/putCommas.ts +++ /dev/null @@ -1,8 +0,0 @@ -export const putCommas = (value: number) => { - try { - if (!value) return value - return value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',') - } catch (err) { - return value - } -} diff --git a/src/nft/utils/txRoute/combineItemsWithTxRoute.ts b/src/nft/utils/txRoute/combineItemsWithTxRoute.ts index bfbc7b1747..a2bac03f30 100644 --- a/src/nft/utils/txRoute/combineItemsWithTxRoute.ts +++ b/src/nft/utils/txRoute/combineItemsWithTxRoute.ts @@ -1,10 +1,6 @@ +import { formatEther } from '@ethersproject/units' import { BuyItem, GenieAsset, isPooledMarket, Markets, PriceInfo, RoutingItem, UpdatedGenieAsset } from 'nft/types' -import { - calcAvgGroupPoolPrice, - formatWeiToDecimal, - isInSameMarketplaceCollection, - isInSameSudoSwapPool, -} from 'nft/utils' +import { calcAvgGroupPoolPrice, isInSameMarketplaceCollection, isInSameSudoSwapPool } from 'nft/utils' const isTheSame = (item: GenieAsset, routeAsset: BuyItem | PriceInfo) => { // if route asset has id, match by id @@ -21,7 +17,7 @@ const isTheSame = (item: GenieAsset, routeAsset: BuyItem | PriceInfo) => { const getPriceDiff = (oldPrice: string, newPrice: string): { hasPriceDiff: boolean; hasVisiblePriceDiff: boolean } => { const hasPriceDiff = oldPrice !== newPrice - const hasVisiblePriceDiff = formatWeiToDecimal(oldPrice) !== formatWeiToDecimal(newPrice) + const hasVisiblePriceDiff = formatEther(oldPrice) !== formatEther(newPrice) return { hasPriceDiff, hasVisiblePriceDiff } } diff --git a/src/utils/formatNumbers.ts b/src/utils/formatNumbers.ts index f3bc8656dc..e1531c036a 100644 --- a/src/utils/formatNumbers.ts +++ b/src/utils/formatNumbers.ts @@ -1,3 +1,4 @@ +import { formatEther as ethersFormatEther } from '@ethersproject/units' import { Currency, CurrencyAmount, Percent, Price, Token } from '@uniswap/sdk-core' import { DEFAULT_LOCAL_CURRENCY, @@ -67,11 +68,23 @@ const THREE_DECIMALS_CURRENCY: NumberFormatOptions = { style: 'currency', } +const THREE_DECIMALS_NO_TRAILING_ZEROS_CURRENCY: NumberFormatOptions = { + ...THREE_DECIMALS_NO_TRAILING_ZEROS, + currency: 'USD', + style: 'currency', +} + const TWO_DECIMALS_NO_TRAILING_ZEROS: NumberFormatOptions = { notation: 'standard', maximumFractionDigits: 2, } +const TWO_DECIMALS_NO_TRAILING_ZEROS_CURRENCY: NumberFormatOptions = { + ...TWO_DECIMALS_NO_TRAILING_ZEROS, + currency: 'USD', + style: 'currency', +} + const TWO_DECIMALS: NumberFormatOptions = { notation: 'standard', maximumFractionDigits: 2, @@ -97,6 +110,12 @@ const SHORTHAND_TWO_DECIMALS_NO_TRAILING_ZEROS: NumberFormatOptions = { maximumFractionDigits: 2, } +const SHORTHAND_TWO_DECIMALS_NO_TRAILING_ZEROS_CURRENCY: NumberFormatOptions = { + ...SHORTHAND_TWO_DECIMALS_NO_TRAILING_ZEROS, + currency: 'USD', + style: 'currency', +} + const SHORTHAND_ONE_DECIMAL: NumberFormatOptions = { notation: 'compact', minimumFractionDigits: 1, @@ -317,6 +336,42 @@ const ntfCollectionStatsFormatter: FormatterRule[] = [ { upperBound: Infinity, formatterOptions: SHORTHAND_ONE_DECIMAL }, ] +const nftTokenFormatter: FormatterRule[] = [ + { exact: 0, hardCodedInput: { hardcodedOutput: '-' }, formatterOptions: FIVE_DECIMALS_MAX_TWO_DECIMALS_MIN }, + { + upperBound: 0.0001, + hardCodedInput: { input: 0.0001, prefix: '<' }, + formatterOptions: FIVE_DECIMALS_MAX_TWO_DECIMALS_MIN, + }, + { upperBound: 1.0, formatterOptions: THREE_DECIMALS }, + { upperBound: 1000, formatterOptions: TWO_DECIMALS_NO_TRAILING_ZEROS }, + { upperBound: 1e15, formatterOptions: SHORTHAND_TWO_DECIMALS_NO_TRAILING_ZEROS }, + { + upperBound: Infinity, + hardCodedInput: { input: 999_000_000_000_000, prefix: '>' }, + formatterOptions: SHORTHAND_TWO_DECIMALS_NO_TRAILING_ZEROS, + }, +] + +const fiatNftTokenFormatter: FormatterRule[] = [ + { exact: 0, hardCodedInput: { hardcodedOutput: '-' }, formatterOptions: NO_DECIMALS }, + { + upperBound: 0.0001, + hardCodedInput: { input: 0.0001, prefix: '<' }, + formatterOptions: ONE_SIG_FIG_CURRENCY, + }, + { upperBound: 1.0, formatterOptions: THREE_DECIMALS_NO_TRAILING_ZEROS_CURRENCY }, + { upperBound: 1000, formatterOptions: TWO_DECIMALS_NO_TRAILING_ZEROS_CURRENCY }, + { upperBound: 1e15, formatterOptions: SHORTHAND_TWO_DECIMALS_NO_TRAILING_ZEROS_CURRENCY }, + { + upperBound: Infinity, + hardCodedInput: { input: 999_000_000_000_000, prefix: '>' }, + formatterOptions: SHORTHAND_TWO_DECIMALS_NO_TRAILING_ZEROS_CURRENCY, + }, +] + +const wholeNumberFormatter: FormatterRule[] = [{ upperBound: Infinity, formatterOptions: NO_DECIMALS }] + export enum NumberType { // used for token quantities in non-transaction contexts (e.g. portfolio balances) TokenNonTx = 'token-non-tx', @@ -360,6 +415,15 @@ export enum NumberType { // nft floor price with trailing zeros NFTTokenFloorPriceTrailingZeros = 'nft-token-floor-price-trailing-zeros', + + // nft token price in currency + NFTToken = 'nft-token', + + // nft token price in local fiat currency + FiatNFTToken = 'fiat-nft-token', + + // whole number formatting + WholeNumber = 'whole-number', } type FormatterType = NumberType | FormatterRule[] @@ -378,6 +442,9 @@ const TYPE_TO_FORMATTER_RULES = { [NumberType.NFTTokenFloorPrice]: ntfTokenFloorPriceFormatter, [NumberType.NFTTokenFloorPriceTrailingZeros]: ntfTokenFloorPriceFormatterTrailingZeros, [NumberType.NFTCollectionStats]: ntfCollectionStatsFormatter, + [NumberType.NFTToken]: nftTokenFormatter, + [NumberType.FiatNFTToken]: fiatNftTokenFormatter, + [NumberType.WholeNumber]: wholeNumberFormatter, } function getFormatterRule(input: number, type: FormatterType, conversionRate?: number): FormatterRule { @@ -563,6 +630,18 @@ function formatNumberOrString({ return formatNumber({ input, type, locale, localCurrency, conversionRate }) } +interface FormatEtherOptions { + input: Nullish + type: FormatterType + locale?: SupportedLocale + localCurrency?: SupportedLocalCurrency +} + +function formatEther({ input, type, locale, localCurrency }: FormatEtherOptions) { + if (input === null || input === undefined) return '-' + return formatNumber({ input: parseFloat(ethersFormatEther(input.toString())), type, locale, localCurrency }) +} + interface FormatFiatPriceOptions { price: Nullish type?: FormatterType @@ -733,9 +812,20 @@ export function useFormatter() { [formatterLocale] ) + const formatEtherwithLocales = useCallback( + (options: Omit) => + formatEther({ + ...options, + locale: formatterLocale, + localCurrency: currencyToFormatWith, + }), + [currencyToFormatWith, formatterLocale] + ) + return useMemo( () => ({ formatCurrencyAmount: formatCurrencyAmountWithLocales, + formatEther: formatEtherwithLocales, formatFiatPrice: formatFiatPriceWithLocales, formatNumber: formatNumberWithLocales, formatNumberOrString: formatNumberOrStringWithLocales, @@ -747,6 +837,7 @@ export function useFormatter() { }), [ formatCurrencyAmountWithLocales, + formatEtherwithLocales, formatFiatPriceWithLocales, formatNumberOrStringWithLocales, formatNumberWithLocales,