diff --git a/package-lock.json b/package-lock.json
index c8b864e..eebbca7 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "plotlink",
- "version": "1.37.0",
+ "version": "1.38.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "plotlink",
- "version": "1.37.0",
+ "version": "1.38.0",
"workspaces": [
"packages/*"
],
diff --git a/package.json b/package.json
index b8b2820..af6351e 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "plotlink",
- "version": "1.37.0",
+ "version": "1.38.0",
"private": true,
"workspaces": [
"packages/*"
diff --git a/src/app/airdrop/AirdropStateMachine.tsx b/src/app/airdrop/AirdropStateMachine.tsx
index d93702b..bf5d8d5 100644
--- a/src/app/airdrop/AirdropStateMachine.tsx
+++ b/src/app/airdrop/AirdropStateMachine.tsx
@@ -105,7 +105,7 @@ export function AirdropStateMachine() {
return (
<>
-
+
{!isConnected ? (
Connect your wallet to get started.
@@ -113,6 +113,7 @@ export function AirdropStateMachine() {
) : (
)}
+
>
);
diff --git a/src/components/airdrop/MilestoneClimb.tsx b/src/components/airdrop/MilestoneClimb.tsx
index f74e5c2..82f2f05 100644
--- a/src/components/airdrop/MilestoneClimb.tsx
+++ b/src/components/airdrop/MilestoneClimb.tsx
@@ -1,10 +1,148 @@
"use client";
-export function MilestoneClimb() {
+import { useEffect, useState } from "react";
+import { useAccount } from "wagmi";
+
+interface StatusData {
+ currentFdv: number;
+ milestones: {
+ bronze: { mcap: number; pct: number; reached: boolean };
+ silver: { mcap: number; pct: number; reached: boolean };
+ gold: { mcap: number; pct: number; reached: boolean };
+ diamond: { mcap: number; pct: number; reached: boolean };
+ };
+}
+
+interface ProjectionData {
+ projected_share: { bronze: number; silver: number; gold: number; diamond: number };
+}
+
+interface MilestoneClimbProps {
+ dimmed?: boolean;
+}
+
+const TIERS = ["bronze", "silver", "gold", "diamond"] as const;
+const TIER_LABELS: Record
= {
+ bronze: "Bronze",
+ silver: "Silver",
+ gold: "Gold",
+ diamond: "Diamond",
+};
+
+function formatMcap(n: number): string {
+ if (n >= 1_000_000) return `$${(n / 1_000_000).toFixed(0)}M`;
+ if (n >= 1_000) return `$${(n / 1_000).toFixed(0)}K`;
+ return `$${n}`;
+}
+
+export function MilestoneClimb({ dimmed }: MilestoneClimbProps) {
+ const { address, isConnected } = useAccount();
+ const [status, setStatus] = useState(null);
+ const [projection, setProjection] = useState(null);
+
+ useEffect(() => {
+ fetch("/api/airdrop/status")
+ .then(r => r.ok ? r.json() : null)
+ .then(d => setStatus(d))
+ .catch(() => {});
+ }, []);
+
+ useEffect(() => {
+ if (!isConnected || !address || dimmed) return;
+ fetch(`/api/airdrop/projection?address=${address.toLowerCase()}`)
+ .then(r => r.ok ? r.json() : null)
+ .then(d => setProjection(d))
+ .catch(() => {});
+ }, [isConnected, address, dimmed]);
+
+ if (!status) {
+ return (
+
+
Milestone Climb
+
Loading...
+
+ );
+ }
+
+ const { milestones, currentFdv } = status;
+ const tierData = TIERS.map(tier => ({
+ key: tier,
+ label: TIER_LABELS[tier],
+ mcap: milestones[tier].mcap,
+ pct: milestones[tier].pct,
+ reached: milestones[tier].reached,
+ share: projection?.projected_share[tier] ?? 0,
+ }));
+
+ const minMcap = tierData[0].mcap;
+ const maxMcap = tierData[tierData.length - 1].mcap;
+ const logMin = Math.log10(minMcap * 0.5);
+ const logMax = Math.log10(maxMcap * 1.5);
+ const logRange = logMax - logMin;
+
+ const svgW = 300;
+ const svgH = 160;
+ const padX = 40;
+ const padY = 20;
+ const chartW = svgW - padX * 2;
+ const chartH = svgH - padY * 2;
+
+ function xPos(mcap: number) {
+ const logVal = Math.log10(Math.max(mcap, 1));
+ const normalized = Math.max(0, Math.min(1, (logVal - logMin) / logRange));
+ return padX + normalized * chartW;
+ }
+
+ const fdvX = xPos(currentFdv);
+ const lineY = padY + chartH * 0.5;
+
return (
-
-
Milestone Climb
-
Track community progress toward milestone tiers.
+
+
Milestone Climb
+
+
);
}