diff --git a/landing/src/components/LiveStats.vue b/landing/src/components/LiveStats.vue index 93d0ceb..f72f949 100644 --- a/landing/src/components/LiveStats.vue +++ b/landing/src/components/LiveStats.vue @@ -3,7 +3,8 @@
ETH Reserve
-
{{ ethReserveAmount }}
+
{{ ethReserveDisplay }}
+
{{ ethReserveSecondary }}
{{ growthIndicator }}
@@ -11,11 +12,13 @@
ETH / Token
-
{{ ethPerToken }}
+
{{ ethPerTokenDisplay }}
+
{{ ethPerTokenSecondary }}
Floor Price
-
{{ floorPriceAmount }}
+
{{ floorPriceDisplay }}
+
{{ floorPriceSecondary }}
{{ floorDistanceText }}
@@ -78,7 +81,11 @@ interface Stats { const stats = ref(null); const error = ref(false); +const ethUsdPrice = ref(null); let refreshInterval: number | null = null; +let ethPriceInterval: number | null = null; +let ethPriceCacheTime = 0; +const ETH_PRICE_CACHE_MS = 5 * 60 * 1000; // 5 minutes // Helper: safely convert a Wei BigInt string to ETH float function weiToEth(wei: string | null | undefined): number { @@ -191,10 +198,58 @@ const holderGrowthClass = computed(() => { return 'growth-flat'; }); -const ethReserveAmount = computed(() => { - if (!stats.value) return '0.00 ETH'; +// Format tiny ETH values without truncating to zero +function formatSmallEth(eth: number): string { + if (eth === 0) return '0 ETH'; + if (eth >= 1) return `${eth.toFixed(2)} ETH`; + if (eth >= 0.01) return `${eth.toFixed(4)} ETH`; + if (eth >= 0.0001) return `${eth.toFixed(6)} ETH`; + const s = eth.toPrecision(4); + return `${s} ETH`; +} + +// Format USD with tiered precision +function formatUsd(usd: number): string { + if (usd >= 1000) return `$${(usd / 1000).toFixed(1)}k`; + if (usd >= 1) return `$${usd.toFixed(2)}`; + if (usd >= 0.01) return `$${usd.toFixed(3)}`; + return `$${usd.toFixed(4)}`; +} + +// Fetch ETH/USD price from CoinGecko with 5-min cache +async function fetchEthPrice() { + const now = Date.now(); + if (ethUsdPrice.value !== null && (now - ethPriceCacheTime) < ETH_PRICE_CACHE_MS) return; + try { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 5000); + const resp = await fetch('https://api.coingecko.com/api/v3/simple/price?ids=ethereum&vs_currencies=usd', { + signal: controller.signal, + }); + clearTimeout(timeout); + if (!resp.ok) throw new Error('ETH price fetch failed'); + const data = await resp.json(); + if (data.ethereum?.usd) { + ethUsdPrice.value = data.ethereum.usd; + ethPriceCacheTime = now; + } + } catch { + // Keep existing cached price or null; ETH fallback will be used + } +} + +// Primary/secondary display helpers +const ethReserveDisplay = computed(() => { + if (!stats.value) return '$0.00'; const eth = weiToEth(stats.value.lastEthReserve); - return `${eth.toFixed(2)} ETH`; + if (ethUsdPrice.value) return formatUsd(eth * ethUsdPrice.value); + return formatSmallEth(eth); +}); + +const ethReserveSecondary = computed((): string | null => { + if (!stats.value || !ethUsdPrice.value) return null; + const eth = weiToEth(stats.value.lastEthReserve); + return `(${eth.toFixed(2)} ETH)`; }); // Growth indicator: null = hide (< 7 days history or missing) @@ -222,9 +277,22 @@ const ethPerToken = computed(() => { const supply = Number(stats.value.kraikenTotalSupply) / 1e18; if (supply === 0) return '—'; const ratio = reserve / supply; - if (ratio >= 0.01) return `${ratio.toFixed(4)} ETH`; - if (ratio >= 0.0001) return `${ratio.toFixed(6)} ETH`; - return `${ratio.toExponential(2)} ETH`; + return ratio; +}); + +const ethPerTokenDisplay = computed(() => { + if (!stats.value) return '—'; + const ratio = ethPerToken.value; + if (typeof ratio === 'string') return ratio; + if (ethUsdPrice.value) return formatUsd(ratio * ethUsdPrice.value); + return formatSmallEth(ratio); +}); + +const ethPerTokenSecondary = computed((): string | null => { + if (!stats.value || !ethUsdPrice.value) return null; + const ratio = ethPerToken.value; + if (typeof ratio === 'string') return null; + return `(${formatSmallEth(ratio).replace(' ETH', '')} ETH)`; }); // Net supply change indicator (7d) @@ -292,8 +360,19 @@ const showFloorPrice = computed(() => { const floorPriceAmount = computed(() => { if (!showFloorPrice.value || !stats.value?.floorPriceWei) return null; - const eth = weiToEth(stats.value.floorPriceWei); - return `${eth.toFixed(4)} ETH`; + return weiToEth(stats.value.floorPriceWei); +}); + +const floorPriceDisplay = computed(() => { + if (floorPriceAmount.value == null) return '—'; + const eth = floorPriceAmount.value; + if (ethUsdPrice.value) return formatUsd(eth * ethUsdPrice.value); + return formatSmallEth(eth); +}); + +const floorPriceSecondary = computed((): string | null => { + if (floorPriceAmount.value == null || !ethUsdPrice.value) return null; + return `(${formatSmallEth(floorPriceAmount.value)})`; }); const floorDistanceText = computed((): string | null => { @@ -401,13 +480,14 @@ async function fetchStats() { onMounted(() => { fetchStats(); + fetchEthPrice(); refreshInterval = window.setInterval(fetchStats, 30000); // Refresh every 30 seconds + ethPriceInterval = window.setInterval(fetchEthPrice, ETH_PRICE_CACHE_MS); // Refresh ETH price every 5 min }); onUnmounted(() => { - if (refreshInterval) { - clearInterval(refreshInterval); - } + if (refreshInterval) clearInterval(refreshInterval); + if (ethPriceInterval) clearInterval(ethPriceInterval); }); @@ -489,6 +569,11 @@ onUnmounted(() => { &.growth-flat color: rgba(240, 240, 240, 0.45) +.stat-secondary + font-size: 12px + color: rgba(240, 240, 240, 0.45) + letter-spacing: 0.3px + .floor-distance font-size: 11px color: rgba(240, 240, 240, 0.5)