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

+ + + {/* 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)} + +
); }