Merge pull request 'fix: show USD values in LiveStats, fix floor price truncation (#171)' (#178) from fix/issue-171 into master
This commit is contained in:
commit
3ea3053134
1 changed files with 99 additions and 14 deletions
|
|
@ -3,7 +3,8 @@
|
|||
<div class="stats-grid" :class="{ 'has-floor': showFloorPrice }">
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">ETH Reserve</div>
|
||||
<div class="stat-value">{{ ethReserveAmount }}</div>
|
||||
<div class="stat-value">{{ ethReserveDisplay }}</div>
|
||||
<div v-if="ethReserveSecondary" class="stat-secondary">{{ ethReserveSecondary }}</div>
|
||||
<div v-if="growthIndicator !== null" class="growth-badge" :class="growthClass">{{ growthIndicator }}</div>
|
||||
<svg v-if="ethReserveSpark.length > 1" class="sparkline" viewBox="0 0 80 24" preserveAspectRatio="none">
|
||||
<polyline :points="toSvgPoints(ethReserveSpark)" fill="none" stroke="rgba(96,165,250,0.5)" stroke-width="1.5" stroke-linejoin="round" stroke-linecap="round" />
|
||||
|
|
@ -11,11 +12,13 @@
|
|||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">ETH / Token</div>
|
||||
<div class="stat-value">{{ ethPerToken }}</div>
|
||||
<div class="stat-value">{{ ethPerTokenDisplay }}</div>
|
||||
<div v-if="ethPerTokenSecondary" class="stat-secondary">{{ ethPerTokenSecondary }}</div>
|
||||
</div>
|
||||
<div v-if="showFloorPrice" class="stat-item">
|
||||
<div class="stat-label">Floor Price</div>
|
||||
<div class="stat-value">{{ floorPriceAmount }}</div>
|
||||
<div class="stat-value">{{ floorPriceDisplay }}</div>
|
||||
<div v-if="floorPriceSecondary" class="stat-secondary">{{ floorPriceSecondary }}</div>
|
||||
<div v-if="floorDistanceText" class="floor-distance">{{ floorDistanceText }}</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
|
|
@ -78,7 +81,11 @@ interface Stats {
|
|||
|
||||
const stats = ref<Stats | null>(null);
|
||||
const error = ref(false);
|
||||
const ethUsdPrice = ref<number | null>(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);
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue