- {data.poolAmount.toLocaleString()} PLOT locked. Big or nothing.
+
+ {/* Title + Tagline */}
+
+
+ PLOT Big or Nothing Airdrop
+
+
+ {data.poolAmount.toLocaleString()} PLOT locked. Earn or burn.
- {/* Time progress */}
-
-
- Time remaining
- {data.timeRemainingDays} days
-
-
-
{data.timeElapsedPercent}% elapsed
-
-
- {/* Market Cap progress */}
-
-
- Market Cap
- {formatUsdValue(data.currentMcap)}
-
-
-
- {nextMilestone ? `< ${nextMilestone.name} (${formatUsdValue(nextMilestone.mcap)})` : "Gold reached"}
-
-
-
- {/* Pool value at next milestone */}
- {data.latestPriceUsd != null && data.latestPriceUsd > 0 && (
-
- {nextMilestone ? (
-
- Pool value if {nextMilestone.name}:{" "}
-
- {formatUsdValue(
- data.poolAmount *
- (data.milestones[
- nextMilestone.name.toLowerCase() as keyof typeof data.milestones
- ].pct / 100) *
- data.latestPriceUsd
- )}
-
-
- ) : (
-
- Pool value at Gold:{" "}
-
- {formatUsdValue(
- data.poolAmount *
- (data.milestones.gold.pct / 100) *
- data.latestPriceUsd
- )}
-
-
- )}
+ {/* Countdown */}
+ {data.timeRemainingDays > 0 && (
+
+
+
+
+
+ {data.timeElapsedPercent}% elapsed
+
+
)}
@@ -142,7 +110,7 @@ export function CampaignHero() {
- {data.latestPriceUsd ? formatUsdValue(data.latestPriceUsd) : "—"}
+ {data.latestPriceUsd ? formatUsdValue(data.latestPriceUsd) : "\u2014"}
PLOT Price
diff --git a/src/components/airdrop/MilestoneTrack.tsx b/src/components/airdrop/MilestoneTrack.tsx
index 912e27e9..eeee42dc 100644
--- a/src/components/airdrop/MilestoneTrack.tsx
+++ b/src/components/airdrop/MilestoneTrack.tsx
@@ -1,5 +1,6 @@
"use client";
+import { useMemo } from "react";
import { useQuery } from "@tanstack/react-query";
import { formatUsdValue } from "../../../lib/usd-price";
@@ -14,11 +15,89 @@ interface StatusData {
};
}
-const TIERS = [
- { key: "bronze" as const, label: "Bronze", color: "text-[#cd7f32]" },
- { key: "silver" as const, label: "Silver", color: "text-[#c0c0c0]" },
- { key: "gold" as const, label: "Gold", color: "text-[#ffd700]" },
-];
+/* ─── Chart tier definitions (presentation layer) ─── */
+
+const MAX_SUPPLY = 1_000_000;
+const CHART_TIERS = [
+ { label: "Bronze", emoji: "\uD83E\uDD49", fdv: 1_000_000, poolPct: 10 },
+ { label: "Silver", emoji: "\uD83E\uDD48", fdv: 10_000_000, poolPct: 30 },
+ { label: "Gold", emoji: "\uD83E\uDD47", fdv: 50_000_000, poolPct: 50 },
+ { label: "Diamond", emoji: "\uD83D\uDC8E", fdv: 100_000_000, poolPct: 100 },
+] as const;
+
+/** Pool USD value at a given FDV milestone: poolAmount * (pct/100) * (fdv / maxSupply) */
+function poolUsdAt(fdv: number, poolPct: number, poolAmount: number): number {
+ return poolAmount * (poolPct / 100) * (fdv / MAX_SUPPLY);
+}
+
+type ChartMilestone = {
+ fdv: number;
+ poolUsd: number;
+ label: string;
+ emoji: string;
+ poolPct: number;
+};
+
+/* ─── SVG layout constants ─── */
+
+const SVG_W = 600;
+const SVG_H = 300;
+const PAD = { top: 40, right: 20, bottom: 50, left: 65 };
+const CHART_W = SVG_W - PAD.left - PAD.right;
+const CHART_H = SVG_H - PAD.top - PAD.bottom;
+
+// Log scale helpers — FDV axis from 10k to 200M
+const LOG_MIN = Math.log10(10_000);
+const LOG_MAX = Math.log10(200_000_000);
+
+function fdvToX(fdv: number): number {
+ if (fdv <= 0) return PAD.left;
+ const logVal = Math.log10(Math.max(fdv, 10_000));
+ const t = (logVal - LOG_MIN) / (LOG_MAX - LOG_MIN);
+ return PAD.left + t * CHART_W;
+}
+
+function usdToY(usd: number, yMax: number): number {
+ const t = usd / yMax;
+ return PAD.top + CHART_H * (1 - t);
+}
+
+/* ─── Path builders ─── */
+
+function buildAreaPath(milestones: ChartMilestone[], yMax: number): string {
+ const baseline = usdToY(0, yMax);
+ let path = `M ${fdvToX(10_000)} ${baseline}`;
+ let prevY = baseline;
+ for (const m of milestones) {
+ const x = fdvToX(m.fdv);
+ const y = usdToY(m.poolUsd, yMax);
+ path += ` L ${x} ${prevY} L ${x} ${y}`;
+ prevY = y;
+ }
+ path += ` L ${PAD.left + CHART_W} ${prevY}`;
+ path += ` L ${PAD.left + CHART_W} ${baseline} Z`;
+ return path;
+}
+
+function buildLinePath(milestones: ChartMilestone[], yMax: number): string {
+ let path = "";
+ let prevY = usdToY(0, yMax);
+ for (let i = 0; i < milestones.length; i++) {
+ const m = milestones[i];
+ const x = fdvToX(m.fdv);
+ const y = usdToY(m.poolUsd, yMax);
+ if (i === 0) {
+ path = `M ${fdvToX(10_000)} ${prevY} L ${x} ${prevY} L ${x} ${y}`;
+ } else {
+ path += ` L ${x} ${prevY} L ${x} ${y}`;
+ }
+ prevY = y;
+ }
+ path += ` L ${PAD.left + CHART_W} ${prevY}`;
+ return path;
+}
+
+/* ─── Component ─── */
export function MilestoneTrack() {
const { data, isLoading } = useQuery
({
@@ -31,6 +110,15 @@ export function MilestoneTrack() {
staleTime: 60_000,
});
+ const milestones: ChartMilestone[] = useMemo(
+ () =>
+ CHART_TIERS.map((t) => ({
+ ...t,
+ poolUsd: poolUsdAt(t.fdv, t.poolPct, data?.poolAmount ?? 50_000),
+ })),
+ [data?.poolAmount],
+ );
+
if (isLoading || !data) {
return (
@@ -39,94 +127,286 @@ export function MilestoneTrack() {
);
}
- // Progress across all three tiers (0–100 mapped to full track)
- const goldMcap = data.milestones.gold.mcap;
- const overallProgress = Math.min(100, (data.currentMcap / goldMcap) * 100);
+ // Y-axis max with 10% headroom above Diamond
+ const yMax = milestones[milestones.length - 1].poolUsd * 1.1;
+
+ // Y-axis ticks: evenly space 4 ticks from 0 to Diamond poolUsd
+ const diamondUsd = milestones[milestones.length - 1].poolUsd;
+ const yTicks = [0, diamondUsd * 0.2, diamondUsd * 0.5, diamondUsd];
+
+ // FDV = price * max supply
+ const currentFdv =
+ data.latestPriceUsd != null && data.latestPriceUsd > 0
+ ? data.latestPriceUsd * MAX_SUPPLY
+ : 0;
+
+ // Determine current zone
+ const currentZone = milestones.reduce(
+ (zone, m, i) => (currentFdv >= m.fdv ? i + 1 : zone),
+ 0,
+ );
+ const currentZoneLabel =
+ currentZone === 0 ? "Pre-Bronze" : milestones[currentZone - 1].label;
+
+ const currentPoolUsd =
+ currentZone > 0 ? milestones[currentZone - 1].poolUsd : 0;
+
+ const dotX = fdvToX(Math.max(currentFdv, 10_000));
+ const dotY = usdToY(currentPoolUsd, yMax);
+
+ const areaPath = buildAreaPath(milestones, yMax);
+ const linePath = buildLinePath(milestones, yMax);
return (
-
Milestone Progress
-
- {/* Track bar */}
-
-
+
+ FDV Milestone Chart
+
+
+ Pool unlock curve across FDV milestones · FDV = PLOT price
+ × {MAX_SUPPLY.toLocaleString()} max supply
+
- {/* Current mcap marker */}
-
+
+
+
+
+
+
+
- {/* Milestone markers */}
- {TIERS.map((tier) => {
- const milestone = data.milestones[tier.key];
- const position = (milestone.mcap / goldMcap) * 100;
- return (
-
+
+ {val === 0
+ ? "$0"
+ : val >= 1_000_000
+ ? `$${(val / 1_000_000).toFixed(1)}M`
+ : `$${(val / 1_000).toFixed(0)}K`}
+
+
+ ))}
+
+ {/* Filled area */}
+
+
+ {/* Step line */}
+
- {/* Tier details */}
-
- {TIERS.map((tier) => {
- const milestone = data.milestones[tier.key];
- const poolValue = data.poolAmount * (milestone.pct / 100);
- return (
-
{
+ const x = fdvToX(m.fdv);
+ const y = usdToY(m.poolUsd, yMax);
+ return (
+
+
+
+ {m.emoji} {m.label}
+
+
+ {m.poolPct}% ($
+ {m.poolUsd >= 1_000_000
+ ? `${(m.poolUsd / 1_000_000).toFixed(1)}M`
+ : `${(m.poolUsd / 1_000).toFixed(0)}K`}
+ )
+
+
+
+ );
+ })}
+
+ {/* X-axis labels */}
+ {milestones.map((m) => (
+
-
- {tier.label}
- {milestone.reached && " \u2713"}
-
-
- {formatUsdValue(milestone.mcap)}
-
- MCap target
-
- {milestone.pct}% · {poolValue.toLocaleString()} PLOT
-
- {data.latestPriceUsd != null && data.latestPriceUsd > 0 && (
-
- Pool: {formatUsdValue(poolValue * data.latestPriceUsd)}
-
- )}
-
- );
- })}
+ ${m.fdv >= 1_000_000 ? `${m.fdv / 1_000_000}M` : `${m.fdv / 1_000}K`}
+
+ ))}
+
+ {/* Axis labels */}
+
+ FDV →
+
+
+ Pool Value
+
+
+ {/* Current FDV indicator — dot on x-axis, dashed line up to area */}
+ {currentFdv > 0 && (
+
+ {/* Vertical dashed line from x-axis up to the step level */}
+
+ {/* Small marker at the step intersection */}
+
+ {/* Pulse ring on x-axis */}
+
+
+
+
+ {/* Heartbeat dot on x-axis */}
+
+
+
+
+ {/* FDV label below axis */}
+
+ {formatUsdValue(currentFdv)}
+
+
+ Current
+
+
+ )}
+
- {/* Current position label */}
-
-
- Current: {formatUsdValue(data.currentMcap)} / {formatUsdValue(goldMcap)}
-
+ {/* Current position summary */}
+
+
+ Current FDV: {currentFdv > 0 ? formatUsdValue(currentFdv) : "\u2014"}
+ · Zone: {currentZoneLabel}
+
+ {currentFdv > 0 && currentZone < milestones.length && (
+
+ Next: {milestones[currentZone].emoji}{" "}
+ {milestones[currentZone].label} at{" "}
+ {formatUsdValue(milestones[currentZone].fdv)} FDV
+
+ )}
);