token details page setup and chart

This commit is contained in:
saml33 2023-11-30 23:19:24 +11:00
parent 0122122cec
commit 0be22628ca
61 changed files with 1210 additions and 124 deletions

View File

@ -0,0 +1,94 @@
import { Metadata } from 'next'
import { draftMode } from 'next/headers'
import { notFound } from 'next/navigation'
import {
fetchTokenPage,
fetchTokenPages,
} from '../../../../contentful/tokenPage'
import { makeApiRequest } from '../../../utils/birdeye'
import { DAILY_SECONDS } from '../../../utils/constants'
import TokenPriceChart from '../../../components/explore/TokenPriceChart'
import TokenMarketStats from '../../../components/explore/TokenMarketStats'
interface TokenPageParams {
slug: string
}
interface TokenPageProps {
params: TokenPageParams
}
// Tell Next.js about all our token pages so
// they can be statically generated at build time.
export async function generateStaticParams(): Promise<TokenPageParams[]> {
const tokenPages = await fetchTokenPages({ preview: false })
return tokenPages.map((page) => ({ slug: page.slug }))
}
// For each token page, tell Next.js which metadata
// (e.g. page title) to display.
export async function generateMetadata(
{ params }: TokenPageProps,
// parent: ResolvingMetadata,
): Promise<Metadata> {
const tokenPage = await fetchTokenPage({
slug: params.slug,
preview: draftMode().isEnabled,
})
if (!tokenPage) {
return notFound()
}
return {
title: tokenPage.seoTitle,
description: tokenPage.seoDescription,
}
}
async function TokenPage({ params }: TokenPageProps) {
const tokenPageData = await fetchTokenPage({
slug: params.slug,
preview: draftMode().isEnabled,
})
if (!tokenPageData) {
return notFound()
}
// get price history for token price chart
const queryEnd = Math.floor(Date.now() / 1000)
const queryStart = queryEnd - 30 * DAILY_SECONDS
const birdeyeQuery = `defi/history_price?address=${tokenPageData.mint}&address_type=token&type=30m&time_from=${queryStart}&time_to=${queryEnd}`
const birdeyePricesResponse = await makeApiRequest(birdeyeQuery)
const birdeyePrices = birdeyePricesResponse?.data?.items?.length
? birdeyePricesResponse.data.items
: []
for (const data of birdeyePrices) {
data.unixTime = data.unixTime * 1000
}
// const hasCustomIcon = CUSTOM_TOKEN_ICONS[tokenPageData.symbol.toLowerCase()]
// const logoPath = hasCustomIcon
// ? `/icons/tokens/${mangoSymbol.toLowerCase()}.svg`
// : birdeyeData?.logoURI
return (
<div>
<div className="mb-4 pb-4 border-b border-th-bkg-3 flex items-center">
<h1 className="text-4xl">
{tokenPageData.tokenName}{' '}
<span className="text-xl font-body font-normal text-th-fgd-4">
{tokenPageData.symbol}
</span>
</h1>
</div>
<TokenPriceChart chartData={birdeyePrices} />
<h2 className="mb-4 text-2xl">Mango stats</h2>
<TokenMarketStats tokenData={tokenPageData} />
<h2 className="mb-4 text-2xl">{`About ${tokenPageData.tokenName}`}</h2>
</div>
)
}
export default TokenPage

View File

@ -0,0 +1,7 @@
export default function PageLayout({
children,
}: {
children: React.ReactNode
}) {
return <div className="px-20 py-12">{children}</div>
}

View File

@ -1,28 +1,26 @@
import { fetchTokenPages } from '../../../contentful/tokenPage'
import Explore from '../../components/explore/Explore'
import SectionWrapper from '../../components/shared/SectionWrapper'
// import { fetchMarketData } from '../../utils/mango'
import { draftMode } from 'next/headers'
import { fetchMangoTokenData } from '../../utils/mango'
async function ExplorePage() {
// const marketData = await fetchMarketData()
const tokens = await fetchTokenPages({
preview: draftMode().isEnabled,
})
const mangoTokensData = await fetchMangoTokenData()
return (
<SectionWrapper>
<>
<h1 className="text-5xl mb-10">Explore</h1>
<ul>
{tokens && tokens?.length ? (
<Explore tokens={tokens} />
<Explore tokens={tokens} mangoTokensData={mangoTokensData} />
) : (
<div className="p-6 rounded-xl border border-th-bkg-3">
<p className="text-center">Nothing to see here...</p>
</div>
)}
</ul>
</SectionWrapper>
</>
)
}

View File

@ -1,9 +1,25 @@
'use client'
import Image from 'next/image'
import { TokenPageWithData } from '../../../contentful/tokenPage'
import { MangoTokenData } from '../../types/mango'
import { Table, Td, Th, TrBody, TrHead } from '../shared/TableElements'
import { CUSTOM_TOKEN_ICONS } from '../../utils/constants'
import { useRouter } from 'next/navigation'
import { AppRouterInstance } from 'next/dist/shared/lib/app-router-context.shared-runtime'
const Explore = ({ tokens }: { tokens: TokenPageWithData[] }) => {
export const goToTokenPage = (slug: string, router: AppRouterInstance) => {
router.push(`/explore/${slug}`, { shallow: true })
}
const Explore = ({
tokens,
mangoTokensData,
}: {
tokens: TokenPageWithData[]
mangoTokensData: MangoTokenData[]
}) => {
const router = useRouter()
return (
<Table>
<thead>
@ -15,16 +31,43 @@ const Explore = ({ tokens }: { tokens: TokenPageWithData[] }) => {
</thead>
<tbody>
{tokens.map((token) => {
const { tokenName, slug, symbol, birdeyeData } = token
const price = birdeyeData?.price || ''
const change24Hour = birdeyeData?.priceChange24hPercent
? `${birdeyeData.priceChange24hPercent.toFixed(2)}%`
: ''
const { tokenName, slug, symbol, birdeyeData, mint } = token
const mangoTokenData: MangoTokenData | undefined =
mangoTokensData.find((mangoToken) => mangoToken?.mint === mint)
let price
let change24Hour
let mangoSymbol
if (mangoTokenData) {
price = mangoTokenData?.price || ''
const change =
mangoTokenData?.price && mangoTokenData?.['24_hr_price']
? ((mangoTokenData['24_hr_price'] - mangoTokenData.price) /
mangoTokenData['24_hr_price']) *
100
: ''
change24Hour = change
mangoSymbol = mangoTokenData.symbol
}
const hasCustomIcon = CUSTOM_TOKEN_ICONS[mangoSymbol.toLowerCase()]
const logoPath = hasCustomIcon
? `/icons/tokens/${mangoSymbol.toLowerCase()}.svg`
: birdeyeData?.logoURI
return (
<TrBody key={slug}>
<TrBody
key={slug}
className="default-transition md:hover:cursor-pointer md:hover:bg-th-bkg-2"
onClick={() => goToTokenPage(slug, router)}
>
<Td>
<p>{tokenName}</p>
<p>{symbol}</p>
<div className="flex items-center space-x-3">
<Image src={logoPath} alt="Logo" height={32} width={32} />
<div>
<p>{tokenName}</p>
<p className="text-sm text-th-fgd-4">
{mangoSymbol || symbol}
</p>
</div>
</div>
</Td>
<Td>
<p>{price}</p>

View File

@ -0,0 +1,34 @@
'use client'
import { TokenPageWithData } from '../../../contentful/tokenPage'
import { numberCompacter } from '../../utils/numbers'
const TokenMarketStats = ({ tokenData }: { tokenData: TokenPageWithData }) => {
if (!tokenData?.birdeyeData) return null
const { birdeyeData } = tokenData
return (
<div className="border-t border-th-bkg-3 pt-4 mt-12">
<h2 className="mb-4 text-2xl">Market stats</h2>
<div className="grid grid-cols-3 grid-flow-row gap-6">
<div>
<p>FDMC</p>
<span className="text-4xl font-display text-th-fgd-1">
{birdeyeData?.mc
? `$${numberCompacter.format(birdeyeData.mc)}`
: ''}
</span>
</div>
<div>
<p>Supply</p>
<span className="text-4xl font-display text-th-fgd-1">
{birdeyeData?.supply
? `$${numberCompacter.format(birdeyeData.supply)}`
: ''}
</span>
</div>
</div>
</div>
)
}
export default TokenMarketStats

View File

@ -0,0 +1,31 @@
'use client'
import { useState } from 'react'
import { BirdeyePriceHistoryData } from '../../types/birdeye'
import { formatYAxis } from '../../utils/numbers'
import DetailedAreaOrBarChart from '../shared/DetailedAreaOrBarChart'
const TokenPriceChart = ({
chartData,
}: {
chartData: BirdeyePriceHistoryData[]
}) => {
const [daysToShow, setDaysToShow] = useState('30')
return (
<DetailedAreaOrBarChart
changeAsPercent
data={chartData}
daysToShow={daysToShow}
setDaysToShow={setDaysToShow}
heightClass="h-[180px] lg:h-[205px]"
hideAxis
prefix="$"
tickFormat={(x) => `$${formatYAxis(x)}`}
title=""
xKey="unixTime"
yKey="value"
/>
)
}
export default TokenPriceChart

View File

@ -18,7 +18,7 @@ import ColorBlur from '../shared/ColorBlur'
import Ottersec from '../icons/Ottersec'
// import TabsText from '../shared/TabsText'
// import MarketCard from './MarketCard'
import { formatNumericValue, numberCompacter } from '../../utils'
import { formatNumericValue, numberCompacter } from '../../utils/numbers'
import HeroStat from './HeroStat'
import useMarketsData from '../../hooks/useMarketData'
import { useQuery } from '@tanstack/react-query'

View File

@ -1,6 +1,6 @@
import Image from 'next/image'
import { MarketsDataItem } from '../../types'
import { formatNumericValue, numberCompacter } from '../../utils'
import { MarketsDataItem } from '../../types/mango'
import { formatNumericValue, numberCompacter } from '../../utils/numbers'
import Change from '../shared/Change'
import SimpleAreaChart from '../shared/SimpleAreaChart'
import { ArrowTopRightOnSquareIcon } from '@heroicons/react/20/solid'

View File

@ -184,7 +184,7 @@ const NavigationLink = ({ path, text }: { path: string; text: string }) => {
<Link href={path} shallow>
<span
className={`font-display ${
pathname === path
pathname.includes(path)
? 'text-th-active md:hover:text-th-active'
: 'text-th-fgd-2 md:hover:text-th-fgd-1'
} default-transition text-base`}

View File

@ -0,0 +1,53 @@
import { FunctionComponent } from 'react'
interface ChartRangeButtonsProps {
activeValue: string
className?: string
onChange: (x: string) => void
values: Array<string>
names?: Array<string>
}
const ChartRangeButtons: FunctionComponent<ChartRangeButtonsProps> = ({
activeValue,
className,
values,
onChange,
names,
}) => {
return (
<div className="relative flex">
{activeValue && values.includes(activeValue) ? (
<div
className="absolute left-0 top-0 h-full transform rounded-md bg-th-bkg-3"
style={{
transform: `translateX(${
values.findIndex((v) => v === activeValue) * 100
}%)`,
width: `${100 / values.length}%`,
}}
/>
) : null}
{values.map((v, i) => (
<button
className={`${className} relative h-6 cursor-pointer rounded-md px-3 text-center text-xs focus-visible:bg-th-bkg-3 focus-visible:text-th-fgd-1
${
v === activeValue
? `text-th-active`
: `text-th-fgd-3 md:hover:text-th-active`
}
`}
key={`${v}${i}`}
onClick={() => onChange(v)}
style={{
width: `${100 / values.length}%`,
}}
>
{names ? names[i] : v}
</button>
))}
</div>
)
}
export default ChartRangeButtons

View File

@ -0,0 +1,593 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { FunctionComponent, useMemo, useState } from 'react'
import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime'
import {
AreaChart,
Area,
XAxis,
YAxis,
Tooltip as RechartsTooltip,
ResponsiveContainer,
ReferenceLine,
BarChart,
Bar,
Cell,
Label,
} from 'recharts'
import { ContentType } from 'recharts/types/src/component/Tooltip'
import { AxisDomain } from 'recharts/types/src/util/types'
import SheenLoader from './SheenLoader'
import { IconButton } from './Button'
import {
ArrowLeftIcon,
ArrowsRightLeftIcon,
NoSymbolIcon,
} from '@heroicons/react/20/solid'
import ChartRangeButtons from './ChartRangeButtons'
import Change from './Change'
import FormatNumericValue from './FormatNumericValue'
import Tooltip from './Tooltip'
import { FadeInFadeOut } from './Transitions'
import { DAILY_MILLISECONDS } from '../../utils/constants'
dayjs.extend(relativeTime)
const titleClasses = 'mb-0.5 text-base'
interface DetailedAreaOrBarChartProps {
changeAsPercent?: boolean
chartType?: 'area' | 'bar'
customTooltip?: ContentType<number, string>
data: any[] | undefined
daysToShow?: string
domain?: AxisDomain
heightClass?: string
hideChange?: boolean
hideChart?: () => void
hideAxis?: boolean
loaderHeightClass?: string
loading?: boolean
prefix?: string
setDaysToShow?: (x: string) => void
small?: boolean
suffix?: string
tickFormat?: (x: number) => string
title?: string
tooltipContent?: string
xKey: string
formatXKeyHeading?: (k: string | number) => string
xAxisLabel?: string
xAxisType?: 'number' | 'category' | undefined
yDecimals?: number
yKey: string
showZeroLine?: boolean
tooltipDateFormat?: string
}
export const formatDateAxis = (date: string, days: number) => {
if (days === 1) {
return dayjs(date).format('h:mma')
} else {
return dayjs(date).format('D MMM')
}
}
const DetailedAreaOrBarChart: FunctionComponent<
DetailedAreaOrBarChartProps
> = ({
changeAsPercent,
chartType = 'area',
customTooltip,
data,
daysToShow = '1',
// domain,
heightClass,
hideChange,
hideChart,
hideAxis,
loaderHeightClass,
loading,
prefix = '',
setDaysToShow,
showZeroLine,
small,
suffix = '',
tickFormat,
title,
tooltipContent,
tooltipDateFormat,
xKey,
formatXKeyHeading,
xAxisLabel,
xAxisType,
yDecimals,
yKey,
}) => {
const [mouseData, setMouseData] = useState<any>(null)
const [showChangePercentage, setShowChangePercentage] = useState(
changeAsPercent || false,
)
const handleMouseMove = (coords: any) => {
if (coords.activePayload) {
setMouseData(coords.activePayload[0].payload)
}
}
const handleMouseLeave = () => {
setMouseData(null)
}
const flipGradientCoords = useMemo(() => {
if (!data || !data.length) return
return data[0][yKey] <= 0 && data[data.length - 1][yKey] < 0
}, [data])
const filteredData = useMemo(() => {
if (!data || !data.length) return []
if (xAxisType === 'number') return data
const start = Number(daysToShow) * DAILY_MILLISECONDS
const filtered = data.filter((d: any) => {
const dataTime = new Date(d[xKey]).getTime()
const now = new Date().getTime()
const limit = now - start
return dataTime >= limit
})
return filtered
}, [data, daysToShow, xAxisType])
const calculateChartChange = () => {
if (filteredData.length) {
let firstValue = filteredData[0][yKey]
if (xAxisType === 'number') {
const minValue = filteredData.reduce(
(min, current) => (current[xKey] < min[xKey] ? current : min),
filteredData[0],
)
firstValue = minValue[yKey]
}
if (mouseData) {
const index = filteredData.findIndex(
(d: any) => d[xKey] === mouseData[xKey],
)
const currentValue = filteredData[index]?.[yKey]
const change =
index >= 0
? showChangePercentage
? ((currentValue - firstValue) / Math.abs(firstValue)) * 100
: currentValue - firstValue
: 0
return isNaN(change) ? 0 : change
} else {
const currentValue = filteredData[filteredData.length - 1][yKey]
return showChangePercentage
? ((currentValue - firstValue) / Math.abs(firstValue)) * 100
: currentValue - firstValue
}
}
return 0
}
return (
<FadeInFadeOut show={true}>
<div>
{loading ? (
<SheenLoader className="flex flex-1">
<div
className={`${
loaderHeightClass ? loaderHeightClass : 'h-96'
} w-full rounded-lg bg-th-bkg-2`}
/>
</SheenLoader>
) : filteredData.length ? (
<div className="relative">
{setDaysToShow ? (
<div className="mb-4 sm:absolute sm:-top-1 sm:right-0 sm:mb-0 sm:flex sm:justify-end">
<ChartRangeButtons
activeValue={daysToShow}
names={['24H', '7D', '30D']}
values={['1', '7', '30']}
onChange={(v) => setDaysToShow(v)}
/>
</div>
) : null}
<div className="flex items-start justify-between">
<div className="flex flex-col md:flex-row md:items-start md:space-x-6">
{hideChart ? (
<IconButton
className="mb-6"
onClick={hideChart}
size="medium"
>
<ArrowLeftIcon className="h-5 w-5" />
</IconButton>
) : null}
<div>
{title ? (
tooltipContent ? (
<Tooltip content={tooltipContent}>
<p
className={`${titleClasses}
tooltip-underline`}
>
{title}
</p>
</Tooltip>
) : (
<p className={titleClasses}>{title}</p>
)
) : null}
{mouseData ? (
<div>
<div
className={`flex items-end ${
small ? 'h-8 text-2xl' : 'mb-1 text-4xl'
} font-display text-th-fgd-1`}
>
<span className="tabular-nums">
{mouseData[yKey] < 0 ? '-' : ''}
{prefix}
<FormatNumericValue
value={Math.abs(mouseData[yKey])}
decimals={yDecimals}
/>
{suffix}
</span>
{!hideChange ? (
<div
className={`ml-3 flex items-center ${
small ? 'mb-[3px]' : 'mb-0.5'
}`}
>
<Change
change={calculateChartChange()}
decimals={!showChangePercentage ? yDecimals : 2}
prefix={!showChangePercentage ? prefix : ''}
suffix={!showChangePercentage ? suffix : '%'}
/>
{changeAsPercent ? (
<ToggleChangeTypeButton
changeType={showChangePercentage}
setChangeType={setShowChangePercentage}
/>
) : null}
</div>
) : null}
</div>
<p
className={`${
small ? 'text-xs' : 'text-sm'
} text-th-fgd-4`}
>
{formatXKeyHeading
? formatXKeyHeading(mouseData[xKey])
: dayjs(mouseData[xKey]).format(
tooltipDateFormat
? tooltipDateFormat
: 'DD MMM YY, h:mma',
)}
</p>
</div>
) : (
<div>
<div
className={`flex items-end ${
small ? 'h-8 text-2xl' : 'mb-1 text-4xl'
} font-display text-th-fgd-1`}
>
<span>
{filteredData[filteredData.length - 1][yKey] < 0
? '-'
: ''}
{prefix}
<span className="tabular-nums">
<FormatNumericValue
value={
data ? Math.abs(data[data.length - 1][yKey]) : 0
}
decimals={yDecimals}
/>
</span>
{suffix}
</span>
{!hideChange ? (
<div
className={`ml-3 flex items-center ${
small ? 'mb-[3px]' : 'mb-0.5'
}`}
>
<Change
change={calculateChartChange()}
decimals={!showChangePercentage ? yDecimals : 2}
prefix={!showChangePercentage ? prefix : ''}
suffix={!showChangePercentage ? suffix : '%'}
/>
{changeAsPercent ? (
<ToggleChangeTypeButton
changeType={showChangePercentage}
setChangeType={setShowChangePercentage}
/>
) : null}
</div>
) : null}
</div>
<p
className={`${
small ? 'text-xs' : 'text-sm'
} text-th-fgd-4`}
>
{formatXKeyHeading
? formatXKeyHeading(
filteredData[filteredData.length - 1][xKey],
)
: dayjs(
filteredData[filteredData.length - 1][xKey],
).format(
tooltipDateFormat
? tooltipDateFormat
: 'DD MMM YY, h:mma',
)}
</p>
</div>
)}
</div>
</div>
</div>
<div
className={`-mt-1 ${heightClass ? heightClass : 'h-96'} w-auto`}
>
<div className="-mx-6 mt-6 h-full">
<ResponsiveContainer width="100%" height="100%">
{chartType === 'area' ? (
<AreaChart
data={filteredData}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
>
<RechartsTooltip
cursor={{
strokeOpacity: 0.09,
}}
content={customTooltip ? customTooltip : <></>}
/>
<defs>
<linearGradient
id={`gradientArea-${title?.replace(
/[^a-zA-Z]/g,
'',
)}`}
x1="0"
y1={flipGradientCoords ? '1' : '0'}
x2="0"
y2={flipGradientCoords ? '0' : '1'}
>
<stop
offset="0%"
stopColor={
calculateChartChange() >= 0
? 'var(--up)'
: 'var(--down)'
}
stopOpacity={0.15}
/>
<stop
offset="99%"
stopColor={
calculateChartChange() >= 0
? 'var(--up)'
: 'var(--down)'
}
stopOpacity={0}
/>
</linearGradient>
</defs>
<Area
isAnimationActive={false}
type="monotone"
dataKey={yKey}
stroke={
isNaN(calculateChartChange())
? 'var(--fgd-4)'
: calculateChartChange() >= 0
? 'var(--up)'
: 'var(--down)'
}
strokeWidth={1.5}
fill={`url(#gradientArea-${title?.replace(
/[^a-zA-Z]/g,
'',
)})`}
/>
<XAxis
axisLine={false}
dataKey={xKey}
hide={hideAxis}
minTickGap={20}
padding={{ left: 20, right: 20 }}
tick={{
fill: 'var(--fgd-4)',
fontSize: 10,
}}
tickLine={false}
tickFormatter={
xAxisType !== 'number'
? (d) => formatDateAxis(d, parseInt(daysToShow))
: undefined
}
type={xAxisType}
>
{xAxisLabel ? (
<Label
value={xAxisLabel}
offset={-2}
position="insideBottom"
fontSize={10}
fill="var(--fgd-3)"
/>
) : null}
</XAxis>
<YAxis
axisLine={false}
dataKey={yKey}
hide={hideAxis}
minTickGap={20}
type="number"
domain={['dataMin', 'dataMax']}
padding={{ top: 20, bottom: 20 }}
tick={{
fill: 'var(--fgd-4)',
fontSize: 10,
}}
tickFormatter={
tickFormat ? (v) => tickFormat(v) : undefined
}
tickLine={false}
/>
{showZeroLine ? (
<ReferenceLine
y={0}
stroke="var(--fgd-4)"
strokeDasharray="2 2"
/>
) : null}
</AreaChart>
) : (
<BarChart
data={filteredData}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
>
<RechartsTooltip
cursor={{
fill: 'var(--bkg-2)',
opacity: 0.5,
}}
content={customTooltip ? customTooltip : <></>}
/>
<defs>
<linearGradient
id="greenGradientBar"
x1="0"
y1="0"
x2="0"
y2="1"
>
<stop
offset="0%"
stopColor="var(--up)"
stopOpacity={1}
/>
<stop
offset="100%"
stopColor="var(--up)"
stopOpacity={0.7}
/>
</linearGradient>
<linearGradient
id="redGradientBar"
x1="0"
y1="1"
x2="0"
y2="0"
>
<stop
offset="0%"
stopColor="var(--down)"
stopOpacity={1}
/>
<stop
offset="100%"
stopColor="var(--down)"
stopOpacity={0.7}
/>
</linearGradient>
</defs>
<Bar dataKey={yKey}>
{filteredData.map((entry, index) => {
return (
<Cell
key={`cell-${index}`}
fill={
entry[yKey] > 0
? 'url(#greenGradientBar)'
: 'url(#redGradientBar)'
}
/>
)
})}
</Bar>
<XAxis
dataKey={xKey}
axisLine={false}
dy={10}
minTickGap={20}
padding={{ left: 20, right: 20 }}
tick={{
fill: 'var(--fgd-4)',
fontSize: 10,
}}
tickLine={false}
tickFormatter={(v) =>
formatDateAxis(v, parseInt(daysToShow))
}
/>
<YAxis
dataKey={yKey}
interval="preserveStartEnd"
axisLine={false}
dx={-10}
padding={{ top: 20, bottom: 20 }}
tick={{
fill: 'var(--fgd-4)',
fontSize: 10,
}}
tickLine={false}
tickFormatter={
tickFormat ? (v) => tickFormat(v) : undefined
}
type="number"
/>
<ReferenceLine y={0} stroke="var(--bkg-4)" />
</BarChart>
)}
</ResponsiveContainer>
</div>
</div>
</div>
) : (
<div
className={`flex ${
heightClass ? heightClass : 'h-96'
} items-center justify-center p-4 text-th-fgd-3`}
>
<div>
<NoSymbolIcon className="mx-auto mb-1 h-6 w-6 text-th-fgd-4" />
<p className="text-th-fgd-4">Chart Unavailable</p>
</div>
</div>
)}
</div>
</FadeInFadeOut>
)
}
export default DetailedAreaOrBarChart
const ToggleChangeTypeButton = ({
changeType,
setChangeType,
}: {
changeType: boolean
setChangeType: (isPercent: boolean) => void
}) => {
return (
<button
className="ml-2 flex h-4 w-4 items-center justify-center text-th-fgd-3 focus:outline-none md:hover:text-th-active"
onClick={() => setChangeType(!changeType)}
>
<ArrowsRightLeftIcon className="h-3.5 w-3.5" />
</button>
)
}

View File

@ -1,5 +1,5 @@
import Decimal from 'decimal.js'
import { formatCurrencyValue, formatNumericValue } from '../../utils'
import { formatCurrencyValue, formatNumericValue } from '../../utils/numbers'
const FormatNumericValue = ({
classNames,

View File

@ -42,14 +42,7 @@ const SimpleAreaChart = ({
fill={`url(#gradientArea-${name.replace(/[^a-zA-Z]/g, '')}`}
/>
<XAxis dataKey={xKey} hide />
<YAxis
domain={([dataMin, dataMax]) => {
const adjustment = (dataMax - dataMin) / 5
return [dataMin - adjustment, dataMax + adjustment]
}}
dataKey={yKey}
hide
/>
<YAxis domain={['dataMin', 'dataMax']} dataKey={yKey} hide />
</AreaChart>
</ResponsiveContainer>
)

View File

@ -0,0 +1,29 @@
import { Transition } from '@headlessui/react'
import { ReactNode } from 'react'
const transitionEnterStyle = 'transition-all ease-out duration-300'
const transitionExitStyle = 'transition-all ease-in duration-300'
export const FadeInFadeOut = ({
children,
className,
show,
}: {
children: ReactNode
className?: string
show: boolean
}) => (
<Transition
appear={true}
className={className}
show={show}
enter={transitionEnterStyle}
enterFrom="opacity-0"
enterTo="opacity-100"
leave={transitionExitStyle}
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
{children}
</Transition>
)

View File

@ -1,9 +1,9 @@
import { useQuery } from '@tanstack/react-query'
import { fetchMarketData } from '../utils/mango'
import { fetchMangoMarketData } from '../utils/mango'
export default function useMarketsData() {
return useQuery({
queryKey: ['market-data'],
queryFn: fetchMarketData,
queryFn: fetchMangoMarketData,
})
}

View File

@ -206,6 +206,10 @@ li {
@apply text-lg md:text-xl text-th-fgd-4;
}
table p {
@apply text-th-fgd-1;
}
/* Scrollbars */
.hide-scroll::-webkit-scrollbar {

13
app/types/birdeye.ts Normal file
View File

@ -0,0 +1,13 @@
export interface BirdeyeOverviewData {
logoURI: string
mc: number
price: number
priceChange24hPercent: number
supply: number
}
export interface BirdeyePriceHistoryData {
address: string
unixTime: number
value: number
}

View File

@ -30,3 +30,16 @@ export type AppStatsData = {
num_openbook_fills_24h: number
open_interest: number
}
type PriceHistoryData = {
price: number
time: string
}
export type MangoTokenData = {
'24_hr_price': number
mint: string
price: number
price_history: PriceHistoryData[]
symbol: string
}

View File

@ -6,6 +6,7 @@ export async function makeApiRequest(path: string) {
headers: {
'X-API-KEY': process.env.NEXT_PUBLIC_BIRDEYE_API_KEY!,
},
cache: 'no-store',
})
return response.json()
}

View File

@ -1 +1,40 @@
export const MANGO_DATA_API_URL = 'https://api.mngo.cloud/data/v4'
export const CUSTOM_TOKEN_ICONS: { [key: string]: boolean } = {
all: true,
bonk: true,
btc: true,
chai: true,
crown: true,
dai: true,
dual: true,
eth: true,
ethpo: true,
'eth (portal)': true,
guac: true,
hnt: true,
jitosol: true,
jlp: true,
kin: true,
ldo: true,
mngo: true,
msol: true,
neon: true,
orca: true,
pyth: true,
ray: true,
render: true,
rlb: true,
slcl: true,
sol: true,
stsol: true,
tbtc: true,
usdc: true,
usdh: true,
usdt: true,
wbtcpo: true,
'wbtc (portal)': true,
}
export const DAILY_SECONDS = 86400
export const DAILY_MILLISECONDS = 86400000

View File

@ -1,7 +1,25 @@
import { FormattedMarketData, MarketData } from '../types'
import { FormattedMarketData, MarketData } from '../types/mango'
import { MANGO_DATA_API_URL } from './constants'
export const fetchMarketData = async () => {
export const fetchMangoTokenData = async () => {
try {
const response = await fetch(
`${MANGO_DATA_API_URL}/stats/token-price-history`,
{
cache: 'no-store',
},
)
const data = await response.json()
if (data && data?.length) {
return data
} else return []
} catch (e) {
console.error('Failed to fetch mango token data', e)
return []
}
}
export const fetchMangoMarketData = async () => {
const promises = [
fetch(`${MANGO_DATA_API_URL}/stats/perp-market-summary`, {
cache: 'no-store',
@ -44,7 +62,7 @@ export const fetchMarketData = async () => {
}
return { perp: formattedPerpData, spot: formattedSpotData }
} catch (e) {
console.error('Failed to fetch market data', e)
console.error('Failed to fetch mango market data', e)
return { perp: [], spot: [] }
}
}

View File

@ -1,5 +1,66 @@
import Decimal from 'decimal.js'
export const numberFormat = new Intl.NumberFormat('en', {
maximumSignificantDigits: 7,
})
export const floorToDecimal = (
value: number | string | Decimal,
decimals: number,
): Decimal => {
const decimal = value instanceof Decimal ? value : new Decimal(value)
return decimal.toDecimalPlaces(decimals, Decimal.ROUND_FLOOR)
}
export const numberCompacter = Intl.NumberFormat('en', {
maximumFractionDigits: 2,
notation: 'compact',
})
const digits2 = new Intl.NumberFormat('en', {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})
const digits3 = new Intl.NumberFormat('en', { maximumFractionDigits: 3 })
const digits4 = new Intl.NumberFormat('en', { maximumFractionDigits: 4 })
const digits5 = new Intl.NumberFormat('en', { maximumFractionDigits: 5 })
const digits6 = new Intl.NumberFormat('en', { maximumFractionDigits: 6 })
const digits7 = new Intl.NumberFormat('en', { maximumFractionDigits: 7 })
const digits8 = new Intl.NumberFormat('en', { maximumFractionDigits: 8 })
const digits9 = new Intl.NumberFormat('en', { maximumFractionDigits: 9 })
const roundValue = (
value: number | string | Decimal,
decimals: number,
roundUp?: boolean,
): string => {
const decimalValue = value instanceof Decimal ? value : new Decimal(value)
const roundMode = roundUp ? Decimal.ROUND_UP : Decimal.ROUND_FLOOR
const roundedValue = decimalValue
.toDecimalPlaces(decimals, roundMode)
.toNumber()
if (decimals === 2) return digits2.format(roundedValue)
if (decimals === 3) return digits3.format(roundedValue)
if (decimals === 4) return digits4.format(roundedValue)
if (decimals === 5) return digits5.format(roundedValue)
if (decimals === 6) return digits6.format(roundedValue)
if (decimals === 7) return digits7.format(roundedValue)
if (decimals === 8) return digits8.format(roundedValue)
if (decimals === 9) return digits9.format(roundedValue)
return roundedValue.toLocaleString(undefined, {
maximumFractionDigits: decimals,
})
}
export const countLeadingZeros = (x: number) => {
const absoluteX = Math.abs(x)
if (absoluteX % 1 == 0) {
return 0
} else {
return -1 - Math.floor(Math.log10(absoluteX % 1))
}
}
export const formatNumericValue = (
value: number | string | Decimal,
decimals?: number,
@ -31,6 +92,27 @@ export const formatNumericValue = (
return formattedValue
}
const usdFormatter0 = Intl.NumberFormat('en', {
minimumFractionDigits: 0,
maximumFractionDigits: 0,
style: 'currency',
currency: 'USD',
})
const usdFormatter2 = Intl.NumberFormat('en', {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
style: 'currency',
currency: 'USD',
})
const usdFormatter3Sig = Intl.NumberFormat('en', {
minimumSignificantDigits: 3,
maximumSignificantDigits: 3,
style: 'currency',
currency: 'USD',
})
export const formatCurrencyValue = (
value: number | string | Decimal,
decimals?: number,
@ -58,84 +140,10 @@ export const formatCurrencyValue = (
return formattedValue
}
const roundValue = (
value: number | string | Decimal,
decimals: number,
roundUp?: boolean,
): string => {
const decimalValue = value instanceof Decimal ? value : new Decimal(value)
const roundMode = roundUp ? Decimal.ROUND_UP : Decimal.ROUND_FLOOR
const roundedValue = decimalValue
.toDecimalPlaces(decimals, roundMode)
.toNumber()
if (decimals === 2) return digits2.format(roundedValue)
if (decimals === 3) return digits3.format(roundedValue)
if (decimals === 4) return digits4.format(roundedValue)
if (decimals === 5) return digits5.format(roundedValue)
if (decimals === 6) return digits6.format(roundedValue)
if (decimals === 7) return digits7.format(roundedValue)
if (decimals === 8) return digits8.format(roundedValue)
if (decimals === 9) return digits9.format(roundedValue)
return roundedValue.toLocaleString(undefined, {
maximumFractionDigits: decimals,
})
export const formatYAxis = (value: number) => {
return value === 0
? '0'
: Math.abs(value) > 1
? numberCompacter.format(value)
: formatNumericValue(value, 4)
}
const digits2 = new Intl.NumberFormat('en', {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})
const digits3 = new Intl.NumberFormat('en', { maximumFractionDigits: 3 })
const digits4 = new Intl.NumberFormat('en', { maximumFractionDigits: 4 })
const digits5 = new Intl.NumberFormat('en', { maximumFractionDigits: 5 })
const digits6 = new Intl.NumberFormat('en', { maximumFractionDigits: 6 })
const digits7 = new Intl.NumberFormat('en', { maximumFractionDigits: 7 })
const digits8 = new Intl.NumberFormat('en', { maximumFractionDigits: 8 })
const digits9 = new Intl.NumberFormat('en', { maximumFractionDigits: 9 })
export const numberFormat = new Intl.NumberFormat('en', {
maximumSignificantDigits: 7,
})
export const floorToDecimal = (
value: number | string | Decimal,
decimals: number,
): Decimal => {
const decimal = value instanceof Decimal ? value : new Decimal(value)
return decimal.toDecimalPlaces(decimals, Decimal.ROUND_FLOOR)
}
const usdFormatter0 = Intl.NumberFormat('en', {
minimumFractionDigits: 0,
maximumFractionDigits: 0,
style: 'currency',
currency: 'USD',
})
const usdFormatter2 = Intl.NumberFormat('en', {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
style: 'currency',
currency: 'USD',
})
const usdFormatter3Sig = Intl.NumberFormat('en', {
minimumSignificantDigits: 3,
maximumSignificantDigits: 3,
style: 'currency',
currency: 'USD',
})
export const countLeadingZeros = (x: number) => {
const absoluteX = Math.abs(x)
if (absoluteX % 1 == 0) {
return 0
} else {
return -1 - Math.floor(Math.log10(absoluteX % 1))
}
}
export const numberCompacter = Intl.NumberFormat('en', {
maximumFractionDigits: 2,
notation: 'compact',
})

View File

@ -3,6 +3,7 @@ import { Entry } from 'contentful'
import { Document as RichTextDocument } from '@contentful/rich-text-types'
import contentfulClient from './contentfulClient'
import { makeApiRequest } from '../app/utils/birdeye'
import { BirdeyeOverviewData } from '../app/types/birdeye'
type TokenPageEntry = Entry<TypeTokenSkeleton, undefined, string>
@ -16,15 +17,12 @@ export interface TokenPage {
twitterUrl?: string
mint: string
coingeckoId: string
}
interface BirdeyeData {
price: number
priceChange24hPercent: number
seoTitle: string
seoDescription: string
}
export interface TokenPageWithData extends TokenPage {
birdeyeData: BirdeyeData
birdeyeData: BirdeyeOverviewData
}
export function parseContentfulTokenPage(
@ -44,6 +42,8 @@ export function parseContentfulTokenPage(
twitterUrl: tokenPageEntry.fields.twitterUrl || undefined,
mint: tokenPageEntry.fields.mint,
coingeckoId: tokenPageEntry.fields.coingeckoId,
seoTitle: tokenPageEntry.fields.seoTitle,
seoDescription: tokenPageEntry.fields.seoDescription,
}
}
@ -85,14 +85,25 @@ interface FetchTokenPageOptions {
export async function fetchTokenPage({
slug,
preview,
}: FetchTokenPageOptions): Promise<TokenPage | null> {
}: FetchTokenPageOptions): Promise<TokenPageWithData | null> {
const contentful = contentfulClient({ preview })
const blogPostsResult = await contentful.getEntries<TypeTokenSkeleton>({
const tokenPageResult = await contentful.getEntries<TypeTokenSkeleton>({
content_type: 'token',
'fields.slug': slug,
include: 2,
})
return parseContentfulTokenPage(blogPostsResult.items[0])
const parsedTokenPage = parseContentfulTokenPage(tokenPageResult.items[0])
if (parsedTokenPage) {
const birdeyeDataResponse = await makeApiRequest(
`defi/token_overview?address=${parsedTokenPage.mint}`,
)
const birdeyeData = birdeyeDataResponse?.success
? birdeyeDataResponse?.data
: undefined
return { ...parsedTokenPage, birdeyeData: birdeyeData }
} else return null
}

View File

@ -9,6 +9,8 @@ import type {
export interface TypeTokenFields {
tokenName: EntryFieldTypes.Symbol
slug: EntryFieldTypes.Symbol
seoTitle: EntryFieldTypes.Symbol
seoDescription: EntryFieldTypes.Text
description: EntryFieldTypes.RichText
tags: EntryFieldTypes.Array<
EntryFieldTypes.Symbol<'Governanace' | 'Liquid Staking' | 'Meme'>

View File

@ -31,6 +31,7 @@
"@tanstack/react-query": "^5.8.4",
"@tippyjs/react": "^4.2.6",
"contentful": "^10.6.12",
"dayjs": "^1.11.10",
"decimal.js": "^10.4.3",
"gsap": "^3.12.2",
"i18next": "^23.7.6",
@ -42,6 +43,7 @@
"next-themes": "^0.2.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-flip-numbers": "^3.0.8",
"react-i18next": "^13.5.0",
"recharts": "^2.10.0"
},
@ -50,6 +52,7 @@
"@types/jest": "^29.5.8",
"@types/node": "20.9.2",
"@types/react": "18.2.37",
"@types/recharts": "^1.8.28",
"@typescript-eslint/eslint-plugin": "^6.11.0",
"@typescript-eslint/parser": "^6.11.0",
"babel-jest": "^29.7.0",

View File

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 112 KiB

View File

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 8.0 KiB

After

Width:  |  Height:  |  Size: 8.0 KiB

View File

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 6.5 KiB

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

Before

Width:  |  Height:  |  Size: 955 B

After

Width:  |  Height:  |  Size: 955 B

View File

@ -0,0 +1,36 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_3_228)">
<g clip-path="url(#clip1_3_228)">
<path d="M16 32C24.8366 32 32 24.8366 32 16C32 7.16344 24.8366 0 16 0C7.16344 0 0 7.16344 0 16C0 24.8366 7.16344 32 16 32Z" fill="#F3F3F3"/>
<path d="M15.9979 3V12.4613L23.9947 16.0346L15.9979 3Z" fill="#343434"/>
<path d="M15.9978 3L8 16.0346L15.9978 12.4613V3Z" fill="#8C8C8C"/>
<path d="M15.9979 22.166V28.5948L24 17.5239L15.9979 22.166Z" fill="#3C3C3B"/>
<path d="M15.9978 28.5948V22.165L8 17.5239L15.9978 28.5948Z" fill="#8C8C8C"/>
<path d="M15.9979 20.6779L23.9947 16.0347L15.9979 12.4635V20.6779Z" fill="#141414"/>
<path d="M8 16.0347L15.9978 20.6779V12.4635L8 16.0347Z" fill="#393939"/>
</g>
<circle cx="27" cy="5" r="5" fill="url(#paint0_radial_3_228)"/>
<circle cx="27" cy="5" r="3.75" fill="url(#paint1_radial_3_228)"/>
<circle cx="27" cy="5" r="2.5" fill="url(#paint2_radial_3_228)"/>
</g>
<defs>
<radialGradient id="paint0_radial_3_228" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(27 5) rotate(90) scale(5)">
<stop offset="0.739583" stop-color="#49266B"/>
<stop offset="1" stop-color="#976EC0"/>
</radialGradient>
<radialGradient id="paint1_radial_3_228" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(27 5) rotate(90) scale(3.75)">
<stop offset="0.713542" stop-color="#3E2755"/>
<stop offset="1" stop-color="#845EAA"/>
</radialGradient>
<radialGradient id="paint2_radial_3_228" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(27 5) rotate(90) scale(2.5)">
<stop offset="0.494792"/>
<stop offset="1" stop-color="#845EAA"/>
</radialGradient>
<clipPath id="clip0_3_228">
<rect width="32" height="32" fill="white"/>
</clipPath>
<clipPath id="clip1_3_228">
<rect width="32" height="32" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

View File

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

Before

Width:  |  Height:  |  Size: 963 B

After

Width:  |  Height:  |  Size: 963 B

View File

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

View File

Before

Width:  |  Height:  |  Size: 8.6 KiB

After

Width:  |  Height:  |  Size: 8.6 KiB

View File

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

Before

Width:  |  Height:  |  Size: 73 KiB

After

Width:  |  Height:  |  Size: 73 KiB

View File

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -0,0 +1,26 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_3_219)">
<path d="M16 32C24.8366 32 32 24.8366 32 16C32 7.16344 24.8366 0 16 0C7.16344 0 0 7.16344 0 16C0 24.8366 7.16344 32 16 32Z" fill="#F7931A"/>
<path d="M23.189 14.02C23.503 11.924 21.906 10.797 19.724 10.045L20.432 7.205L18.704 6.775L18.014 9.54C17.56 9.426 17.094 9.32 16.629 9.214L17.324 6.431L15.596 6L14.888 8.839C14.512 8.753 14.142 8.669 13.784 8.579L13.786 8.57L11.402 7.975L10.942 9.821C10.942 9.821 12.225 10.115 12.198 10.133C12.898 10.308 13.024 10.771 13.003 11.139L12.197 14.374C12.245 14.386 12.307 14.404 12.377 14.431L12.194 14.386L11.064 18.918C10.978 19.13 10.761 19.449 10.271 19.328C10.289 19.353 9.015 19.015 9.015 19.015L8.157 20.993L10.407 21.554C10.825 21.659 11.235 21.769 11.638 21.872L10.923 24.744L12.65 25.174L13.358 22.334C13.83 22.461 14.288 22.579 14.736 22.691L14.03 25.519L15.758 25.949L16.473 23.083C19.421 23.641 21.637 23.416 22.57 20.75C23.322 18.604 22.533 17.365 20.982 16.558C22.112 16.298 22.962 15.555 23.189 14.02V14.02ZM19.239 19.558C18.706 21.705 15.091 20.544 13.919 20.253L14.869 16.448C16.041 16.741 19.798 17.32 19.239 19.558ZM19.774 13.989C19.287 15.942 16.279 14.949 15.304 14.706L16.164 11.256C17.139 11.499 20.282 11.952 19.774 13.989Z" fill="white"/>
<circle cx="27" cy="5" r="5" fill="url(#paint0_radial_3_219)"/>
<circle cx="27" cy="5" r="3.75" fill="url(#paint1_radial_3_219)"/>
<circle cx="27" cy="5" r="2.5" fill="url(#paint2_radial_3_219)"/>
</g>
<defs>
<radialGradient id="paint0_radial_3_219" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(27 5) rotate(90) scale(5)">
<stop offset="0.739583" stop-color="#49266B"/>
<stop offset="1" stop-color="#976EC0"/>
</radialGradient>
<radialGradient id="paint1_radial_3_219" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(27 5) rotate(90) scale(3.75)">
<stop offset="0.713542" stop-color="#3E2755"/>
<stop offset="1" stop-color="#845EAA"/>
</radialGradient>
<radialGradient id="paint2_radial_3_219" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(27 5) rotate(90) scale(2.5)">
<stop offset="0.494792"/>
<stop offset="1" stop-color="#845EAA"/>
</radialGradient>
<clipPath id="clip0_3_219">
<rect width="32" height="32" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

File diff suppressed because one or more lines are too long

View File

@ -1213,6 +1213,11 @@
resolved "https://registry.yarnpkg.com/@types/d3-path/-/d3-path-3.0.2.tgz#4327f4a05d475cf9be46a93fc2e0f8d23380805a"
integrity sha512-WAIEVlOCdd/NKRYTsqCpOMHQHemKBEINf8YXMYOtXH0GA7SY0dqMB78P3Uhgfy+4X+/Mlw2wDtlETkN6kQUCMA==
"@types/d3-path@^1":
version "1.0.11"
resolved "https://registry.yarnpkg.com/@types/d3-path/-/d3-path-1.0.11.tgz#45420fee2d93387083b34eae4fe6d996edf482bc"
integrity sha512-4pQMp8ldf7UaB/gR8Fvvy69psNHkTpD/pVw3vmEi8iZAB9EPMBruB1JvHO4BIq9QkUUd2lV1F5YXpMNj7JPBpw==
"@types/d3-scale@^4.0.2":
version "4.0.8"
resolved "https://registry.yarnpkg.com/@types/d3-scale/-/d3-scale-4.0.8.tgz#d409b5f9dcf63074464bf8ddfb8ee5a1f95945bb"
@ -1220,6 +1225,13 @@
dependencies:
"@types/d3-time" "*"
"@types/d3-shape@^1":
version "1.3.12"
resolved "https://registry.yarnpkg.com/@types/d3-shape/-/d3-shape-1.3.12.tgz#8f2f9f7a12e631ce6700d6d55b84795ce2c8b259"
integrity sha512-8oMzcd4+poSLGgV0R1Q1rOlx/xdmozS4Xab7np0eamFFUYq71AU9pOCJEFnkXW2aI/oXdVYJzw6pssbSut7Z9Q==
dependencies:
"@types/d3-path" "^1"
"@types/d3-shape@^3.1.0":
version "3.1.5"
resolved "https://registry.yarnpkg.com/@types/d3-shape/-/d3-shape-3.1.5.tgz#ab2f9c1485667be729b68bf2d9219858bc6d4009"
@ -1317,6 +1329,14 @@
"@types/scheduler" "*"
csstype "^3.0.2"
"@types/recharts@^1.8.28":
version "1.8.28"
resolved "https://registry.yarnpkg.com/@types/recharts/-/recharts-1.8.28.tgz#2e4a4468e4390c5db8d822783f925d6e80ae8297"
integrity sha512-31D+dVBdVMtBnRMOjfM9210oRsclujQetwDNnCfapy/gF1BruvQkiK9WZ2ZMqDZY2xnDpIV8sWjISBcY+wgkLw==
dependencies:
"@types/d3-shape" "^1"
"@types/react" "*"
"@types/scheduler@*":
version "0.16.6"
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.6.tgz#eb26db6780c513de59bee0b869ef289ad3068711"
@ -2397,6 +2417,11 @@ date-fns@^2.0.1, date-fns@^2.28.0:
dependencies:
"@babel/runtime" "^7.21.0"
dayjs@^1.11.10:
version "1.11.10"
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.10.tgz#68acea85317a6e164457d6d6947564029a6a16a0"
integrity sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==
debug@4, debug@4.3.4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.4:
version "4.3.4"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
@ -5387,6 +5412,13 @@ react-dom@^18.2.0:
loose-envify "^1.1.0"
scheduler "^0.23.0"
react-flip-numbers@^3.0.8:
version "3.0.8"
resolved "https://registry.yarnpkg.com/react-flip-numbers/-/react-flip-numbers-3.0.8.tgz#45f9240c3afd51e55f40d3da2380ac9d68d83456"
integrity sha512-iEh4WScZFiGYkIWw3ATA352+XVWBsiKLg97CQHGXvpBetJFYazaYWMqv/mR2fGHdoDLA/2uES74e2wdEgy69BQ==
dependencies:
react-simple-animate "^3.0.1"
react-i18next@^13.5.0:
version "13.5.0"
resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-13.5.0.tgz#44198f747628267a115c565f0c736a50a76b1ab0"
@ -5415,6 +5447,11 @@ react-lifecycles-compat@^3.0.4:
resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
react-simple-animate@^3.0.1:
version "3.5.2"
resolved "https://registry.yarnpkg.com/react-simple-animate/-/react-simple-animate-3.5.2.tgz#ab08865c8bd47872b92bd1e25902326bf7c695b3"
integrity sha512-xLE65euP920QMTOmv5haPlml+hmOPDkbIr5WeF7ADIXWBYt5kW/vwpNfWg8EKMab8aeDxIZ6QjffVh8v2dUyhg==
react-smooth@^2.0.5:
version "2.0.5"
resolved "https://registry.yarnpkg.com/react-smooth/-/react-smooth-2.0.5.tgz#d153b7dffc7143d0c99e82db1532f8cf93f20ecd"