Commit 5e2bdc4e authored by Greg Bugyis's avatar Greg Bugyis Committed by GitHub

feat: Update Collection Stats and number formatting (#4937)

parent f2a33b6f
...@@ -7,8 +7,7 @@ import { headlineMedium } from 'nft/css/common.css' ...@@ -7,8 +7,7 @@ import { headlineMedium } from 'nft/css/common.css'
import { themeVars } from 'nft/css/sprinkles.css' import { themeVars } from 'nft/css/sprinkles.css'
import { useIsCollectionLoading } from 'nft/hooks/useIsCollectionLoading' import { useIsCollectionLoading } from 'nft/hooks/useIsCollectionLoading'
import { GenieCollection } from 'nft/types' import { GenieCollection } from 'nft/types'
import { ethNumberStandardFormatter } from 'nft/utils/currency' import { floorFormatter, quantityFormatter, roundWholePercentage, volumeFormatter } from 'nft/utils/numbers'
import { putCommas } from 'nft/utils/putCommas'
import { ReactNode, useEffect, useReducer, useRef, useState } from 'react' import { ReactNode, useEffect, useReducer, useRef, useState } from 'react'
import ReactMarkdown from 'react-markdown' import ReactMarkdown from 'react-markdown'
import styled from 'styled-components/macro' import styled from 'styled-components/macro'
...@@ -241,27 +240,29 @@ const CollectionDescription = ({ description }: { description: string }) => { ...@@ -241,27 +240,29 @@ const CollectionDescription = ({ description }: { description: string }) => {
const StatsItem = ({ children, label, isMobile }: { children: ReactNode; label: string; isMobile: boolean }) => { const StatsItem = ({ children, label, isMobile }: { children: ReactNode; label: string; isMobile: boolean }) => {
return ( return (
<Box display="flex" flexDirection={isMobile ? 'row' : 'column'} alignItems="baseline" gap="2" height="min"> <Box display="flex" flexDirection={'column'} alignItems="baseline" gap="2" height="min">
<span className={styles.statsValue}>{children}</span>
<Box as="span" className={styles.statsLabel}> <Box as="span" className={styles.statsLabel}>
{`${label}${isMobile ? ': ' : ''}`} {label}
</Box> </Box>
<span className={styles.statsValue}>{children}</span>
</Box> </Box>
) )
} }
const StatsRow = ({ stats, isMobile, ...props }: { stats: GenieCollection; isMobile?: boolean } & BoxProps) => { const StatsRow = ({ stats, isMobile, ...props }: { stats: GenieCollection; isMobile?: boolean } & BoxProps) => {
const numOwnersStr = stats.stats ? putCommas(stats.stats.num_owners) : 0 const uniqueOwnersPercentage = stats.stats
const totalSupplyStr = stats.stats ? putCommas(stats.stats.total_supply) : 0 ? roundWholePercentage((stats.stats.num_owners / stats.stats.total_supply) * 100)
: 0
const totalSupplyStr = stats.stats ? quantityFormatter(stats.stats.total_supply) : 0
const listedPercentageStr = const listedPercentageStr =
stats.stats && stats.stats.total_listings > 0 stats.stats && stats.stats.total_listings > 0
? ((stats.stats.total_listings / stats.stats.total_supply) * 100).toFixed(0) ? roundWholePercentage((stats.stats.total_listings / stats.stats.total_supply) * 100)
: 0 : 0
const isCollectionStatsLoading = useIsCollectionLoading((state) => state.isCollectionStatsLoading) const isCollectionStatsLoading = useIsCollectionLoading((state) => state.isCollectionStatsLoading)
// round daily volume & floorPrice to 3 decimals or less // round daily volume & floorPrice to 3 decimals or less
const totalVolumeStr = ethNumberStandardFormatter(stats.stats?.total_volume) const totalVolumeStr = volumeFormatter(stats.stats?.total_volume)
const floorPriceStr = ethNumberStandardFormatter(stats.floorPrice) const floorPriceStr = floorFormatter(stats.floorPrice)
const floorChangeStr = const floorChangeStr =
stats.stats && stats.stats.one_day_floor_change ? Math.round(Math.abs(stats.stats.one_day_floor_change) * 100) : 0 stats.stats && stats.stats.one_day_floor_change ? Math.round(Math.abs(stats.stats.one_day_floor_change) * 100) : 0
const arrow = stats.stats && stats.stats.one_day_change ? getDeltaArrow(stats.stats.one_day_floor_change) : null const arrow = stats.stats && stats.stats.one_day_change ? getDeltaArrow(stats.stats.one_day_floor_change) : null
...@@ -278,18 +279,8 @@ const StatsRow = ({ stats, isMobile, ...props }: { stats: GenieCollection; isMob ...@@ -278,18 +279,8 @@ const StatsRow = ({ stats, isMobile, ...props }: { stats: GenieCollection; isMob
return ( return (
<Row gap={{ sm: '20', md: '60' }} {...props}> <Row gap={{ sm: '20', md: '60' }} {...props}>
{isCollectionStatsLoading && statsLoadingSkeleton} {isCollectionStatsLoading && statsLoadingSkeleton}
{totalSupplyStr ? (
<StatsItem label="Items" isMobile={isMobile ?? false}>
{totalSupplyStr}
</StatsItem>
) : null}
{numOwnersStr ? (
<StatsItem label="Owners" isMobile={isMobile ?? false}>
{numOwnersStr}
</StatsItem>
) : null}
{stats.floorPrice ? ( {stats.floorPrice ? (
<StatsItem label="Floor Price" isMobile={isMobile ?? false}> <StatsItem label="Global floor" isMobile={isMobile ?? false}>
{floorPriceStr} ETH {floorPriceStr} ETH
</StatsItem> </StatsItem>
) : null} ) : null}
...@@ -300,6 +291,21 @@ const StatsRow = ({ stats, isMobile, ...props }: { stats: GenieCollection; isMob ...@@ -300,6 +291,21 @@ const StatsRow = ({ stats, isMobile, ...props }: { stats: GenieCollection; isMob
</PercentChange> </PercentChange>
</StatsItem> </StatsItem>
) : null} ) : null}
{stats.stats?.total_volume ? (
<StatsItem label="Total volume" isMobile={isMobile ?? false}>
{totalVolumeStr} ETH
</StatsItem>
) : null}
{totalSupplyStr ? (
<StatsItem label="Items" isMobile={isMobile ?? false}>
{totalSupplyStr}
</StatsItem>
) : null}
{uniqueOwnersPercentage ? (
<StatsItem label="Unique owners" isMobile={isMobile ?? false}>
{uniqueOwnersPercentage}%
</StatsItem>
) : null}
{stats.stats?.total_volume ? ( {stats.stats?.total_volume ? (
<StatsItem label="Total Volume" isMobile={isMobile ?? false}> <StatsItem label="Total Volume" isMobile={isMobile ?? false}>
{totalVolumeStr} ETH {totalVolumeStr} ETH
......
import { DEFAULT_LOCALE } from 'constants/locales'
import numbro from 'numbro'
export const isNumber = (s: string): boolean => { export const isNumber = (s: string): boolean => {
const reg = /^-?\d+\.?\d*$/ const reg = /^-?\d+\.?\d*$/
return reg.test(s) && !isNaN(parseFloat(s)) && isFinite(parseFloat(s)) return reg.test(s) && !isNaN(parseFloat(s)) && isFinite(parseFloat(s))
...@@ -9,3 +12,105 @@ export const formatPercentage = (percentage: string): string => { ...@@ -9,3 +12,105 @@ export const formatPercentage = (percentage: string): string => {
.toFixed(2) .toFixed(2)
.replace(/\B(?=(\d{3})+(?!\d))/g, ',')}%` .replace(/\B(?=(\d{3})+(?!\d))/g, ',')}%`
} }
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 ''
if (n < 1) {
return '<1'
}
return Math.round(n).toString()
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment