From 836401e9f9e93501d5d0a123c451cdc9891a5ebf Mon Sep 17 00:00:00 2001 From: dev-agent Date: Tue, 26 May 2026 09:55:21 +0000 Subject: [PATCH 1/2] [#1253] Build MilestoneClimb.tsx with SVG tier chart SVG milestone climb chart with 4 tier nodes (Bronze $100K, Silver $1M, Gold $5M, Diamond $10M) on log scale. Current FDV marker with arrow indicator. Per-tier projected share labels from /projection. Dimmed variant prop for pre-activation state. Fetches /status for FDV + milestones, /projection for shares. Closes #1253 Co-Authored-By: Claude Opus 4.7 (1M context) --- package-lock.json | 4 +- package.json | 2 +- src/components/airdrop/MilestoneClimb.tsx | 141 +++++++++++++++++++++- 3 files changed, 140 insertions(+), 7 deletions(-) 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/components/airdrop/MilestoneClimb.tsx b/src/components/airdrop/MilestoneClimb.tsx index f74e5c2..8e60002 100644 --- a/src/components/airdrop/MilestoneClimb.tsx +++ b/src/components/airdrop/MilestoneClimb.tsx @@ -1,10 +1,143 @@ "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 maxMcap = tierData[tierData.length - 1].mcap; + const clampedFdv = Math.min(currentFdv, maxMcap * 1.1); + + 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) { + return padX + (Math.log10(Math.max(mcap, 1)) / Math.log10(maxMcap * 1.1)) * chartW; + } + + const fdvX = xPos(clampedFdv); + const lineY = padY + chartH * 0.5; + return ( -
-

Milestone Climb

-

Track community progress toward milestone tiers.

+
+

Milestone Climb

+ + + {/* Baseline */} + + + {/* Tier nodes */} + {tierData.map(t => { + const cx = xPos(t.mcap); + return ( + + + + + {t.label} + + + {formatMcap(t.mcap)} + + {!dimmed && t.share > 0 && ( + + {Math.round(t.share).toLocaleString()} PLOT + + )} + + ); + })} + + {/* Current FDV marker */} + + + + {formatMcap(currentFdv)} + +
); } From 43210920480b85ae6e8c00101ba506d674552591 Mon Sep 17 00:00:00 2001 From: dev-agent Date: Tue, 26 May 2026 09:57:32 +0000 Subject: [PATCH 2/2] [#1253] Add dimmed preview to pre-activation + fix log scale normalization Render MilestoneClimb dimmed in pre-activation state for visual preview. Normalize log positioning between min milestone (50K) and max milestone (1.5x Diamond) so tiers spread evenly across chart instead of bunching near the right edge. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/app/airdrop/AirdropStateMachine.tsx | 3 ++- src/components/airdrop/MilestoneClimb.tsx | 11 ++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) 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 8e60002..82f2f05 100644 --- a/src/components/airdrop/MilestoneClimb.tsx +++ b/src/components/airdrop/MilestoneClimb.tsx @@ -74,8 +74,11 @@ export function MilestoneClimb({ dimmed }: MilestoneClimbProps) { share: projection?.projected_share[tier] ?? 0, })); + const minMcap = tierData[0].mcap; const maxMcap = tierData[tierData.length - 1].mcap; - const clampedFdv = Math.min(currentFdv, maxMcap * 1.1); + const logMin = Math.log10(minMcap * 0.5); + const logMax = Math.log10(maxMcap * 1.5); + const logRange = logMax - logMin; const svgW = 300; const svgH = 160; @@ -85,10 +88,12 @@ export function MilestoneClimb({ dimmed }: MilestoneClimbProps) { const chartH = svgH - padY * 2; function xPos(mcap: number) { - return padX + (Math.log10(Math.max(mcap, 1)) / Math.log10(maxMcap * 1.1)) * chartW; + 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(clampedFdv); + const fdvX = xPos(currentFdv); const lineY = padY + chartH * 0.5; return (