better number formatting (#9)

* tweak number display

* select whole amount upon input focus

* handle letter input

* number formatting
This commit is contained in:
Matthias Kretschmann 2024-04-16 12:20:29 +02:00 committed by GitHub
parent 34464d00aa
commit 0d08ba807b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 81 additions and 39 deletions

View File

@ -1,9 +1,11 @@
import { usePrices, type PriceCoingecko } from '@/features/prices' import { useLocale, usePrices, type PriceCoingecko } from '@/features/prices'
import { PriceChange } from './PriceChange' import { PriceChange } from './PriceChange'
import styles from './Price.module.css' import styles from './Price.module.css'
import { formatFiat } from '@/lib'
export function Price({ price }: { price: PriceCoingecko }) { export function Price({ price }: { price: PriceCoingecko }) {
const { isValidating, isLoading } = usePrices() const { isValidating, isLoading } = usePrices()
const locale = useLocale()
const feedbackClasses = isLoading const feedbackClasses = isLoading
? 'isLoading' ? 'isLoading'
@ -13,7 +15,9 @@ export function Price({ price }: { price: PriceCoingecko }) {
return ( return (
<p className={styles.price}> <p className={styles.price}>
<span className={`${styles.fiat} ${feedbackClasses}`}>${price.usd}</span> <span className={`${styles.fiat} ${feedbackClasses}`}>
{formatFiat(price.usd, 'USD', locale)}
</span>
{price?.usd_24h_change ? ( {price?.usd_24h_change ? (
<PriceChange priceChange={price.usd_24h_change} /> <PriceChange priceChange={price.usd_24h_change} />
) : null} ) : null}

View File

@ -2,19 +2,12 @@
import { TriangleUpIcon, TriangleDownIcon } from '@radix-ui/react-icons' import { TriangleUpIcon, TriangleDownIcon } from '@radix-ui/react-icons'
import styles from './PriceChange.module.css' import styles from './PriceChange.module.css'
import { useEffect, useState } from 'react' import { useLocale } from '@/features/prices/hooks/use-locale'
export function PriceChange({ priceChange }: { priceChange: number }) { export function PriceChange({ priceChange }: { priceChange: number }) {
const [locale, setLocale] = useState('en-US') const locale = useLocale()
const styleClasses = priceChange > 0 ? styles.positive : styles.negative const styleClasses = priceChange > 0 ? styles.positive : styles.negative
useEffect(() => {
const userLocale = navigator?.languages?.length
? navigator.languages[0]
: navigator.language
setLocale(userLocale)
}, [])
return ( return (
<span <span
className={`${styles.change} ${styleClasses}`} className={`${styles.change} ${styleClasses}`}

View File

@ -1 +1,2 @@
export * from './use-prices' export * from './use-prices'
export * from './use-locale'

View File

@ -0,0 +1,16 @@
'use client'
import { useState, useEffect } from 'react'
export function useLocale() {
const [locale, setLocale] = useState('en-US')
useEffect(() => {
const userLocale = navigator?.languages?.length
? navigator.languages[0]
: navigator.language
setLocale(userLocale)
}, [])
return locale
}

View File

@ -1,7 +1,7 @@
'use client' 'use client'
import { tokens } from '@/constants' import { tokens } from '@/constants'
import { fetcher, getTokenAddressBySymbol } from '@/lib/utils' import { fetcher, getTokenAddressBySymbol } from '@/lib'
import useSWR from 'swr' import useSWR from 'swr'
const tokenAddresses = tokens.map((token) => token.address).toString() const tokenAddresses = tokens.map((token) => token.address).toString()

View File

@ -4,8 +4,8 @@ import { useState } from 'react'
import { useDebounce } from 'use-debounce' import { useDebounce } from 'use-debounce'
import { ratioOceanToAsi, ratioAgixToAsi, ratioFetToAsi } from '@/constants' import { ratioOceanToAsi, ratioAgixToAsi, ratioFetToAsi } from '@/constants'
import { usePrices } from '@/features/prices' import { usePrices } from '@/features/prices'
import { getTokenBySymbol } from '@/lib/utils' import { getTokenBySymbol } from '@/lib'
import { FormAmount, Result } from '@/features/strategies/components' import { FormAmount, Result } from '@/features/strategies'
import stylesShared from '@/features/strategies/styles/shared.module.css' import stylesShared from '@/features/strategies/styles/shared.module.css'
export function Buy() { export function Buy() {

View File

@ -20,6 +20,8 @@ export function FormAmount({
if (value === '') { if (value === '') {
setAmount(0) setAmount(0)
} else if (isNaN(Number(value))) {
return
} else { } else {
setAmount(Number(value)) setAmount(Number(value))
} }
@ -30,6 +32,10 @@ export function FormAmount({
setToken(e.target.value as TokenSymbol) setToken(e.target.value as TokenSymbol)
} }
function handleFocus(e: React.FocusEvent<HTMLInputElement>) {
e.target.select()
}
const options = isFiat const options = isFiat
? [{ value: 'USD', label: 'USD' }] ? [{ value: 'USD', label: 'USD' }]
: [ : [
@ -46,6 +52,7 @@ export function FormAmount({
pattern="[0-9]*" pattern="[0-9]*"
value={amount} value={amount}
onChange={handleAmountChange} onChange={handleAmountChange}
onFocus={handleFocus}
style={{ width: amount.toString().length + 'ch' }} style={{ width: amount.toString().length + 'ch' }}
/> />

View File

@ -1,8 +1,9 @@
import styles from './Result.module.css' import styles from './Result.module.css'
import { formatNumber } from '@/lib/utils' import { formatCrypto, formatFiat } from '@/lib'
import { ArrowRightIcon } from '@radix-ui/react-icons' import { ArrowRightIcon } from '@radix-ui/react-icons'
import { TokenLogo } from '@/components' import { TokenLogo } from '@/components'
import { Token } from '@/types' import { Token } from '@/types'
import { useLocale } from '@/features/prices'
type Props = { type Props = {
token: Token | undefined token: Token | undefined
@ -23,6 +24,7 @@ export function Result({
isValidating, isValidating,
isLoading isLoading
}: Props) { }: Props) {
const locale = useLocale()
const feedbackClasses = isLoading const feedbackClasses = isLoading
? 'isLoading' ? 'isLoading'
: isValidating : isValidating
@ -35,15 +37,18 @@ export function Result({
<TokenLogo token={token} /> <TokenLogo token={token} />
<p> <p>
<span className={feedbackClasses}> <span
{formatNumber(amount || 0, token?.symbol || '')} className={feedbackClasses}
title={`${amount} ${token?.symbol}`}
>
{formatCrypto(amount || 0, token?.symbol || '', locale)}
</span> </span>
</p> </p>
{amountOriginalFiat ? ( {amountOriginalFiat ? (
<p> <p>
<span className={`${styles.fiat} ${feedbackClasses}`}> <span className={`${styles.fiat} ${feedbackClasses}`}>
{formatNumber(amountOriginalFiat || 0, 'USD')} {formatFiat(amountOriginalFiat || 0, 'USD', locale)}
</span> </span>
</p> </p>
) : null} ) : null}
@ -53,13 +58,13 @@ export function Result({
<ArrowRightIcon className={styles.iconArrow} /> <ArrowRightIcon className={styles.iconArrow} />
<p> <p>
<strong title={`${amountAsi}`} className={feedbackClasses}> <strong title={`${amountAsi} ASI`} className={feedbackClasses}>
{formatNumber(amountAsi || 0, 'ASI')} {formatCrypto(amountAsi || 0, 'ASI', locale)}
</strong> </strong>
</p> </p>
<p> <p>
<strong className={`${styles.fiat} ${feedbackClasses}`}> <strong className={`${styles.fiat} ${feedbackClasses}`}>
{formatNumber(amountFiat || 0, 'USD')} {formatFiat(amountFiat || 0, 'USD', locale)}
</strong> </strong>
</p> </p>
</div> </div>

View File

@ -1,5 +1,5 @@
import { ratioOceanToAsi, ratioAgixToAsi, ratioFetToAsi } from '@/constants' import { ratioOceanToAsi, ratioAgixToAsi, ratioFetToAsi } from '@/constants'
import { getTokenBySymbol } from '@/lib/utils' import { getTokenBySymbol } from '@/lib'
import { type TokenSymbol } from '@/types' import { type TokenSymbol } from '@/types'
import { usePrices, type Prices } from '@/features/prices' import { usePrices, type Prices } from '@/features/prices'
import { type Market, useQuote } from '@/features/strategies' import { type Market, useQuote } from '@/features/strategies'

View File

@ -1,7 +1,7 @@
'use client' 'use client'
import { TokenSymbol } from '@/types' import { TokenSymbol } from '@/types'
import { getTokenAddressBySymbol, fetcher } from '@/lib/utils' import { getTokenAddressBySymbol, fetcher } from '@/lib'
import useSWR from 'swr' import useSWR from 'swr'
const options = { const options = {

5
lib/fetch.ts Normal file
View File

@ -0,0 +1,5 @@
export async function fetcher(url: string) {
const res = await fetch(url)
if (!res.ok) throw new Error('Failed to fetch')
return await res.json()
}

3
lib/index.ts Normal file
View File

@ -0,0 +1,3 @@
export * from './numbers'
export * from './fetch'
export * from './tokens'

22
lib/numbers.ts Normal file
View File

@ -0,0 +1,22 @@
import { formatCurrency } from '@coingecko/cryptoformat'
export function formatCrypto(price: number, currency: string, locale: string) {
return formatCurrency(price, currency, locale, false, {
decimalPlaces: 3,
significantFigures: 1
})
}
export function formatFiat(price: number, currency: string, locale: string) {
let formattedPrice = formatCurrency(price, currency, locale, false, {
decimalPlaces: 2,
significantFigures: 8
})
// Add a trailing zero if only one digit after the decimal
if (formattedPrice.includes('.') && formattedPrice.split('.')[1].length < 2) {
formattedPrice += '0'
}
return formattedPrice
}

View File

@ -1,19 +1,5 @@
import { tokens } from '@/constants' import { tokens } from '@/constants'
import type { TokenAddress, Token } from '@/types' import type { TokenAddress, Token } from '@/types'
import { formatCurrency } from '@coingecko/cryptoformat'
export function formatNumber(price: number, currency: string) {
return formatCurrency(price, currency, 'en', false, {
decimalPlaces: 3,
significantFigures: 5
})
}
export async function fetcher(url: string) {
const res = await fetch(url)
if (!res.ok) throw new Error('Failed to fetch')
return await res.json()
}
export function getTokenBySymbol(symbol: string): Token | undefined { export function getTokenBySymbol(symbol: string): Token | undefined {
const token = tokens.find((t) => t.symbol === symbol) const token = tokens.find((t) => t.symbol === symbol)