From 934dd57a7b7f36c1f63a5efd51a7b0b6e71cf46d Mon Sep 17 00:00:00 2001 From: liqiuniu <1165448306@qq.com> Date: Sun, 5 Apr 2026 15:27:06 +0800 Subject: [PATCH] feat: add FNDRY token price widget (Bounty #846) --- src/components/FNDRYPriceWidget.tsx | 208 ++++++++++++++++++++++++++++ 1 file changed, 208 insertions(+) create mode 100644 src/components/FNDRYPriceWidget.tsx diff --git a/src/components/FNDRYPriceWidget.tsx b/src/components/FNDRYPriceWidget.tsx new file mode 100644 index 000000000..b2c64e786 --- /dev/null +++ b/src/components/FNDRYPriceWidget.tsx @@ -0,0 +1,208 @@ +import { useState, useEffect } from 'react'; +import { motion } from 'framer-motion'; + +interface TokenPrice { + priceUsd: number; + priceChange24h: number; + volume24h: number; + marketCap?: number; +} + +interface SparklinePoint { + time: number; + price: number; +} + +export function FNDRYPriceWidget({ compact = false }: { compact?: boolean }) { + const [price, setPrice] = useState(null); + const [sparkline, setSparkline] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(false); + + useEffect(() => { + const fetchPrice = async () => { + try { + // DexScreener API for FNDRY token + const res = await fetch( + 'https://api.dexscreener.com/latest/dex/tokens/FNDRY_TOKEN_ADDRESS' + ); + const data = await res.json(); + + if (data.pairs?.[0]) { + const pair = data.pairs[0]; + setPrice({ + priceUsd: parseFloat(pair.priceUsd), + priceChange24h: parseFloat(pair.priceChange.h24), + volume24h: parseFloat(pair.volume.h24), + marketCap: pair.fdv ? parseFloat(pair.fdv) : undefined, + }); + + // Generate sparkline from price history + if (pair.priceHistory) { + setSparkline( + pair.priceHistory.map((p: { time: number; price: number }) => ({ + time: p.time, + price: p.price, + })) + ); + } + } + setError(false); + } catch { + setError(true); + } finally { + setLoading(false); + } + }; + + fetchPrice(); + const interval = setInterval(fetchPrice, 30000); // Update every 30s + return () => clearInterval(interval); + }, []); + + if (loading) { + return ( +
+
+
+
+
+
+
+
+
+ ); + } + + if (error || !price) { + return ( +
+
Price unavailable
+
+ ); + } + + const isPositive = price.priceChange24h >= 0; + const changeColor = isPositive ? 'text-emerald' : 'text-error'; + const changeIcon = isPositive ? '鈫? : '鈫?; + + return ( + +
+ {/* Token Icon */} +
+
+ F +
+
+ + {/* Price Info */} +
+
+ FNDRY + SOL +
+ +
+ + ${price.priceUsd.toLocaleString(undefined, { + minimumFractionDigits: 4, + maximumFractionDigits: 6, + })} + + + + {changeIcon} {Math.abs(price.priceChange24h).toFixed(2)}% + +
+
+
+ + {/* Sparkline Chart */} + {!compact && sparkline.length > 0 && ( +
+ +
+ )} + + {/* Additional Stats */} + {!compact && ( +
+
+
24h Volume
+
+ ${(price.volume24h / 1000).toFixed(1)}K +
+
+ {price.marketCap && ( +
+
Market Cap
+
+ ${(price.marketCap / 1000000).toFixed(2)}M +
+
+ )} +
+ )} +
+ ); +} + +function Sparkline({ data, positive }: { data: SparklinePoint[]; positive: boolean }) { + if (data.length < 2) return null; + + const prices = data.map((d) => d.price); + const min = Math.min(...prices); + const max = Math.max(...prices); + const range = max - min || 1; + + const width = 200; + const height = 48; + const padding = 2; + + const points = data + .map((d, i) => { + const x = padding + (i / (data.length - 1)) * (width - 2 * padding); + const y = height - padding - ((d.price - min) / range) * (height - 2 * padding); + return `${x},${y}`; + }) + .join(' '); + + const gradientId = `sparkline-gradient-${positive ? 'up' : 'down'}`; + const strokeColor = positive ? '#00E676' : '#FF5252'; + + return ( + + + + + + + + + {/* Fill area */} + + + {/* Line */} + + + ); +} + +export default FNDRYPriceWidget; \ No newline at end of file