|
@@ -1,11 +1,14 @@
|
|
|
+import { css } from '@emotion/react'
|
|
|
import styled from '@emotion/styled'
|
|
|
import BN from 'bn.js'
|
|
|
-import { forwardRef, useRef } from 'react'
|
|
|
+import { ReactNode, forwardRef, useRef } from 'react'
|
|
|
import { mergeRefs } from 'react-merge-refs'
|
|
|
|
|
|
import { Text, TextProps, TextVariant } from '@/components/Text'
|
|
|
import { atlasConfig } from '@/config'
|
|
|
import { hapiBnToTokenNumber } from '@/joystream-lib/utils'
|
|
|
+import { useTokenPrice } from '@/providers/joystream/joystream.hooks'
|
|
|
+import { sizes } from '@/styles'
|
|
|
import { formatNumber } from '@/utils/number'
|
|
|
|
|
|
import { Tooltip } from '../Tooltip'
|
|
@@ -19,8 +22,13 @@ export type NumberFormatProps = {
|
|
|
variant?: TextVariant
|
|
|
displayedValue?: string | number
|
|
|
isNegative?: boolean
|
|
|
+ icon?: ReactNode
|
|
|
+ withDenomination?: boolean | 'horizontal' | 'vertical' | 'before' | 'after'
|
|
|
+ denominationAlign?: 'left' | 'right'
|
|
|
} & Omit<TextProps, 'children' | 'variant'>
|
|
|
|
|
|
+const TEXT_DENOMINATION_ALIGNMENTS: NumberFormatProps['withDenomination'][] = ['before', 'after']
|
|
|
+
|
|
|
export const NumberFormat = forwardRef<HTMLHeadingElement, NumberFormatProps>(
|
|
|
(
|
|
|
{
|
|
@@ -32,25 +40,37 @@ export const NumberFormat = forwardRef<HTMLHeadingElement, NumberFormatProps>(
|
|
|
displayedValue,
|
|
|
isNegative,
|
|
|
color,
|
|
|
+ withDenomination: _withDenomination,
|
|
|
+ denominationAlign = 'left',
|
|
|
+ icon,
|
|
|
...textProps
|
|
|
},
|
|
|
ref
|
|
|
) => {
|
|
|
+ const withDenomination = atlasConfig.joystream.tokenPriceFeedUrl ? _withDenomination : undefined
|
|
|
+ const { convertTokensToUSD } = useTokenPrice()
|
|
|
const internalValue = BN.isBN(value) ? hapiBnToTokenNumber(value) : value
|
|
|
+ const fiatValue = convertTokensToUSD(internalValue)
|
|
|
const textRef = useRef<HTMLHeadingElement>(null)
|
|
|
+ const denominationRef = useRef<HTMLHeadingElement>(null)
|
|
|
const bnValue = new BN(value)
|
|
|
let formattedValue
|
|
|
+ let formattedDenominatedValue
|
|
|
let tooltipText
|
|
|
switch (isNegative || bnValue.isNeg() ? 'full' : format) {
|
|
|
case 'short':
|
|
|
- formattedValue = internalValue ? (internalValue > 0.01 ? formatNumberShort(internalValue) : `< 0.01`) : 0
|
|
|
+ formattedValue = internalValue ? (internalValue > 0.01 ? formatNumberShort(internalValue) : `<0.01`) : 0
|
|
|
+ formattedDenominatedValue = fiatValue ? (fiatValue > 0.01 ? formatNumberShort(fiatValue) : `<$0.01`) : 0
|
|
|
tooltipText = formatNumber(internalValue)
|
|
|
break
|
|
|
case 'full':
|
|
|
formattedValue = tooltipText = formatNumber(internalValue)
|
|
|
+ formattedDenominatedValue = fiatValue ? formatNumber(fiatValue) : 0
|
|
|
break
|
|
|
case 'dollar':
|
|
|
formattedValue = formatDollars(internalValue)
|
|
|
+ formattedDenominatedValue = fiatValue ? formatDollars(fiatValue) : 0
|
|
|
+
|
|
|
tooltipText = new Intl.NumberFormat('en-US', { maximumSignificantDigits, ...currencyFormatOptions })
|
|
|
.format(internalValue)
|
|
|
.replaceAll(',', ' ')
|
|
@@ -61,32 +81,130 @@ export const NumberFormat = forwardRef<HTMLHeadingElement, NumberFormatProps>(
|
|
|
const hasTooltip =
|
|
|
withTooltip &&
|
|
|
((format === 'short' && (internalValue > 999 || hasDecimals)) || (format === 'dollar' && hasDecimals))
|
|
|
+ const shouldShowDenominationTooltip = fiatValue && fiatValue <= 0.01
|
|
|
const content = (
|
|
|
- <StyledText
|
|
|
- {...textProps}
|
|
|
- color={bnValue.isNeg() || isNegative ? 'colorTextError' : color}
|
|
|
- variant={variant}
|
|
|
- ref={mergeRefs([ref, textRef])}
|
|
|
- >
|
|
|
- {displayedValue || formattedValue}
|
|
|
- {withToken && ` ${atlasConfig.joystream.tokenTicker}`}
|
|
|
- </StyledText>
|
|
|
+ <ContentContainer>
|
|
|
+ {withDenomination === 'before' && (
|
|
|
+ <Text
|
|
|
+ className="denomination"
|
|
|
+ as="span"
|
|
|
+ color={bnValue.isNeg() || isNegative ? 'colorTextError' : 'colorTextMuted'}
|
|
|
+ variant={variant}
|
|
|
+ ref={denominationRef}
|
|
|
+ >
|
|
|
+ ({formattedDenominatedValue !== '<$0.01' ? '$' : ''}
|
|
|
+ {formattedDenominatedValue}){' '}
|
|
|
+ </Text>
|
|
|
+ )}
|
|
|
+ <StyledText
|
|
|
+ {...textProps}
|
|
|
+ color={bnValue.isNeg() || isNegative ? 'colorTextError' : color}
|
|
|
+ variant={variant}
|
|
|
+ ref={mergeRefs([ref, textRef])}
|
|
|
+ >
|
|
|
+ {displayedValue || formattedValue}
|
|
|
+ {withToken && ` ${atlasConfig.joystream.tokenTicker}`}
|
|
|
+ </StyledText>
|
|
|
+ {withDenomination === 'after' && (
|
|
|
+ <Text
|
|
|
+ className="denomination"
|
|
|
+ as="span"
|
|
|
+ color={bnValue.isNeg() || isNegative ? 'colorTextError' : 'colorTextMuted'}
|
|
|
+ variant={variant}
|
|
|
+ ref={denominationRef}
|
|
|
+ >
|
|
|
+ {' '}
|
|
|
+ ({formattedDenominatedValue !== '<$0.01' ? '$' : ''}
|
|
|
+ {formattedDenominatedValue}){' '}
|
|
|
+ </Text>
|
|
|
+ )}
|
|
|
+ </ContentContainer>
|
|
|
)
|
|
|
|
|
|
return (
|
|
|
<>
|
|
|
- {content}
|
|
|
+ {withDenomination ? (
|
|
|
+ <Container orientation={withDenomination}>
|
|
|
+ {icon ? (
|
|
|
+ <IconContainer>
|
|
|
+ {icon}
|
|
|
+ {content}
|
|
|
+ </IconContainer>
|
|
|
+ ) : (
|
|
|
+ content
|
|
|
+ )}
|
|
|
+ {!TEXT_DENOMINATION_ALIGNMENTS.includes(withDenomination) && (
|
|
|
+ <Denomination
|
|
|
+ align={denominationAlign}
|
|
|
+ className="denomination"
|
|
|
+ as="span"
|
|
|
+ color={bnValue.isNeg() || isNegative ? 'colorTextError' : 'colorTextMuted'}
|
|
|
+ variant="t100"
|
|
|
+ ref={denominationRef}
|
|
|
+ >
|
|
|
+ {formattedDenominatedValue !== '<$0.01' ? '$' : ''}
|
|
|
+ {formattedDenominatedValue}
|
|
|
+ </Denomination>
|
|
|
+ )}
|
|
|
+ </Container>
|
|
|
+ ) : icon ? (
|
|
|
+ <IconContainer>
|
|
|
+ {icon}
|
|
|
+ {content}
|
|
|
+ </IconContainer>
|
|
|
+ ) : (
|
|
|
+ content
|
|
|
+ )}
|
|
|
+
|
|
|
<Tooltip reference={textRef} placement="top" delay={[500, null]} text={hasTooltip ? tooltipText : undefined} />
|
|
|
+ <Tooltip
|
|
|
+ reference={denominationRef}
|
|
|
+ placement="top"
|
|
|
+ delay={[500, null]}
|
|
|
+ text={shouldShowDenominationTooltip ? `$${fiatValue?.toPrecision(2)}` : undefined}
|
|
|
+ />
|
|
|
</>
|
|
|
)
|
|
|
}
|
|
|
)
|
|
|
NumberFormat.displayName = 'Number'
|
|
|
|
|
|
+export const ContentContainer = styled.div`
|
|
|
+ display: inline-block;
|
|
|
+`
|
|
|
+
|
|
|
const StyledText = styled(Text)`
|
|
|
display: inline-block;
|
|
|
`
|
|
|
|
|
|
+const Denomination = styled(Text)<{ align: 'right' | 'left' }>`
|
|
|
+ display: inline-block;
|
|
|
+ text-align: ${(props) => props.align};
|
|
|
+`
|
|
|
+
|
|
|
+const Container = styled.div<{ orientation: NumberFormatProps['withDenomination'] }>`
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: ${sizes(1)};
|
|
|
+ width: fit-content;
|
|
|
+ ${(props) =>
|
|
|
+ props.orientation === 'horizontal' &&
|
|
|
+ css`
|
|
|
+ width: 100%;
|
|
|
+ flex-direction: row;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ `}
|
|
|
+`
|
|
|
+
|
|
|
+const IconContainer = styled.div`
|
|
|
+ display: inline-grid;
|
|
|
+ grid-auto-flow: column;
|
|
|
+ grid-auto-columns: max-content;
|
|
|
+ align-items: center;
|
|
|
+ gap: ${sizes(1)};
|
|
|
+`
|
|
|
+
|
|
|
const maximumSignificantDigits = 21
|
|
|
|
|
|
const currencyFormatOptions = {
|