diff --git a/lib/usd-price.ts b/lib/usd-price.ts index 1b493808..3784b058 100644 --- a/lib/usd-price.ts +++ b/lib/usd-price.ts @@ -55,17 +55,24 @@ export async function getPlotUsdPrice( async function fetchPlotUsdPrice(): Promise { const start = Date.now(); - // Try all external sources in parallel — use whichever responds first + // Prefer Mint Club SDK (on-chain read, matches /token page price source) + // Only fall back to API aggregators if on-chain fails + try { + const price = await fetchFromMintClub(); + console.info(`[USD Price] result=hit price=${price} elapsed=${Date.now() - start}ms`); + return price; + } catch { + // Mint Club SDK failed — try API aggregators + } + try { const price = await Promise.any([ - fetchFromMintClub(), fetchFromGeckoTerminal(), fetchFromCoinGecko(), ]); - console.info(`[USD Price] result=hit price=${price} elapsed=${Date.now() - start}ms`); + console.info(`[USD Price] result=hit price=${price} elapsed=${Date.now() - start}ms (api fallback)`); return price; } catch { - // All sources failed — AggregateError console.warn(`[USD Price] All external sources failed, elapsed=${Date.now() - start}ms`); } diff --git a/package.json b/package.json index c489ac45..f46ddb1c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "plotlink", - "version": "0.1.42", + "version": "0.1.43", "private": true, "workspaces": [ "packages/*" diff --git a/src/components/airdrop/CampaignHero.tsx b/src/components/airdrop/CampaignHero.tsx index 1d5de3f9..315427b5 100644 --- a/src/components/airdrop/CampaignHero.tsx +++ b/src/components/airdrop/CampaignHero.tsx @@ -148,7 +148,7 @@ function poolToY(usd: number, yLeftMax: number): number { function fdvToY(fdv: number, logMax: number): number { if (fdv <= 0) return PAD.top + CH; - const t = (Math.log10(Math.max(fdv, 100)) - FDV_LOG_MIN) / (logMax - FDV_LOG_MIN); + const t = Math.max(0, Math.min(1, (Math.log10(Math.max(fdv, 100)) - FDV_LOG_MIN) / (logMax - FDV_LOG_MIN))); return PAD.top + CH * (1 - t); } @@ -272,13 +272,26 @@ function TimelineChart({ return parts.join(" "); }, [hasHistory, dailyPrices, startMs, endMs, totalMs, currentFdv, nowX, fdvLogMax]); - // Linear projection: from current position (nowX) → Diamond at campaign end + // Linear projection: from campaign start → Diamond at campaign end + // Represents "constant growth needed from day 1 to hit Diamond" + const startFdv = useMemo(() => { + if (hasHistory && dailyPrices?.length) { + // Use first daily price within campaign period + for (const dp of dailyPrices) { + const dpMs = new Date(dp.date + "T00:00:00Z").getTime(); + if (dpMs >= startMs && dpMs <= endMs) return dp.fdv; + } + } + return currentFdv > 0 ? currentFdv : 100; + }, [hasHistory, dailyPrices, startMs, endMs, currentFdv]); + const projectionPath = useMemo(() => { + const fromX = PAD.left; const toX = PAD.left + CW; - const fromY = fdvToY(currentFdv > 0 ? currentFdv : 100, fdvLogMax); + const fromY = fdvToY(startFdv, fdvLogMax); const toY = fdvToY(diamondFdv, fdvLogMax); - return `M ${nowX} ${fromY} L ${toX} ${toY}`; - }, [currentFdv, nowX, diamondFdv, fdvLogMax]); + return `M ${fromX} ${fromY} L ${toX} ${toY}`; + }, [startFdv, diamondFdv, fdvLogMax]); const dotY = fdvToY(currentFdv > 0 ? currentFdv : 100, fdvLogMax); @@ -290,188 +303,246 @@ function TimelineChart({ const yLeftTicks = [0, diamondPoolUsd * 0.25, diamondPoolUsd * 0.5, diamondPoolUsd]; // Right-axis ticks omitted — milestone emoji labels already show FDV values + // Linear target today: where FDV should be if growing linearly from start to Diamond + const linearTargetToday = useMemo(() => { + const elapsed = Math.max(0, nowMs - startMs); + const progress = Math.min(1, elapsed / totalMs); + return startFdv + (diamondFdv - startFdv) * progress; + }, [nowMs, startMs, totalMs, startFdv, diamondFdv]); + return (
- - - - - - - - - {/* Grid lines (horizontal at each milestone FDV) */} - {milestoneLines.map((m) => ( - - - {/* Right-side label */} - - {m.emoji} {formatCompact(m.fdv)} - - - ))} - - {/* Y-left axis ticks (pool value) — hidden on mobile */} - - {yLeftTicks.map((val) => ( - - {formatCompact(val)} - + {/* Desktop: full SVG chart */} +
+ + + + + + + + + {/* Grid lines (horizontal at each milestone FDV) */} + {milestoneLines.map((m) => ( + + + {/* Right-side label */} + + {m.emoji} {formatCompact(m.fdv)} + + ))} - - - {/* X-axis month labels */} - {months.map((m) => ( - - - - {m.label} - + + {/* Y-left axis ticks (pool value) */} + + {yLeftTicks.map((val) => ( + + {formatCompact(val)} + + ))} - ))} - - {/* Axis labels — hidden on mobile via CSS media query */} - - Pool Value (USD) - - - FDV (USD) - - {/* 1. Pool value area fill */} - {poolAreaPath && ( - - )} + {/* X-axis month labels */} + {months.map((m) => ( + + + + {m.label} + + + ))} - {/* 2. Pool value step line */} - {poolStepPath && ( + {/* Axis labels */} + + Pool Value (USD) + + + FDV (USD) + + + {/* 1. Pool value area fill */} + {poolAreaPath && ( + + )} + + {/* 2. Pool value step line */} + {poolStepPath && ( + + )} + + {/* 3. Linear FDV projection (dashed) */} - )} - {/* 3. Linear FDV projection (dashed) */} - - - {/* 4. Actual FDV line (solid) */} - {actualFdvPath && ( - + )} + + {/* Heartbeat dot on current FDV position */} + {currentFdv > 0 && ( + + {/* Pulse ring */} + + + + + {/* Solid dot */} + + + + + + )} + + {/* Chart border */} + - )} + - {/* Heartbeat dot on current FDV position */} - {currentFdv > 0 && ( - - {/* Pulse ring */} - - - - - {/* Solid dot */} - - - - - - )} + {/* Legend */} +
+ + Actual FDV + + + Linear projection + + + Pool value + +
+
+ + {/* Mobile: simplified milestone progress view */} +
+
FDV Progress
+ + {/* Current FDV + overall progress bar */} +
+
+ + Current: {currentFdv > 0 ? formatCompact(currentFdv) : "\u2014"} + + + {diamondFdv > 0 ? Math.min(100, Math.round((currentFdv / diamondFdv) * 100)) : 0}% + +
+
+
0 ? Math.min(100, (currentFdv / diamondFdv) * 100) : 0}%` }} + /> +
+
- {/* Chart border */} - - - - {/* Legend */} -
- - Actual FDV - - - Linear projection - - - Pool value - + {/* Milestone list */} +
+ {tiers.map((t) => { + const reached = currentFdv >= t.fdv; + const tierPct = t.pct; + return ( +
+ + {t.emoji} {t.label} + + + {formatCompact(t.fdv)} + + {tierPct}% + + +
+ ); + })} +
+ + {/* Linear target comparison */} +
+
Linear target today
+
{formatCompact(linearTargetToday)}
+
);