From 630dda4674a4f609af362323b10fae79e30c128e Mon Sep 17 00:00:00 2001 From: Nguyen LNP Date: Thu, 21 May 2026 15:06:24 +0700 Subject: [PATCH] feat: add FNDRY price widget --- .../src/components/token/FndryPriceWidget.tsx | 163 ++++++++++++++++++ frontend/src/pages/HomePage.tsx | 4 + 2 files changed, 167 insertions(+) create mode 100644 frontend/src/components/token/FndryPriceWidget.tsx diff --git a/frontend/src/components/token/FndryPriceWidget.tsx b/frontend/src/components/token/FndryPriceWidget.tsx new file mode 100644 index 000000000..0884159df --- /dev/null +++ b/frontend/src/components/token/FndryPriceWidget.tsx @@ -0,0 +1,163 @@ +import React, { useEffect, useMemo, useState } from 'react'; +import { ArrowDownRight, ArrowUpRight, Loader2 } from 'lucide-react'; + +const FNDRY_TOKEN_ADDRESS = 'C2TvY8E8B75EF2UP8cTpTp3EDUjTgjWmpaGnT74VBAGS'; +const DEXSCREENER_URL = `https://api.dexscreener.com/latest/dex/tokens/${FNDRY_TOKEN_ADDRESS}`; + +interface DexScreenerPair { + chainId?: string; + dexId?: string; + pairAddress?: string; + priceUsd?: string; + volume?: { h24?: number }; + liquidity?: { usd?: number }; + priceChange?: { h24?: number }; +} + +interface DexScreenerResponse { + pairs?: DexScreenerPair[] | null; +} + +interface PriceSnapshot { + price: number; + timestamp: number; +} + +function pickPrimaryPair(pairs: DexScreenerPair[] = []): DexScreenerPair | null { + return pairs + .filter((pair) => pair.chainId === 'solana' && typeof pair.priceUsd === 'string') + .sort((a, b) => (b.liquidity?.usd ?? 0) - (a.liquidity?.usd ?? 0))[0] ?? null; +} + +function formatUsd(value: number): string { + if (value < 0.01) return `$${value.toFixed(6)}`; + return `$${value.toFixed(4)}`; +} + +function Sparkline({ points, positive }: { points: PriceSnapshot[]; positive: boolean }) { + const path = useMemo(() => { + if (points.length < 2) return ''; + + const width = 120; + const height = 36; + const prices = points.map((point) => point.price); + const min = Math.min(...prices); + const max = Math.max(...prices); + const range = max - min || 1; + + return points + .map((point, index) => { + const x = (index / (points.length - 1)) * width; + const y = height - ((point.price - min) / range) * height; + return `${index === 0 ? 'M' : 'L'} ${x.toFixed(2)} ${y.toFixed(2)}`; + }) + .join(' '); + }, [points]); + + if (!path) { + return