diff --git a/package-lock.json b/package-lock.json
index f4eb8b5..ed88443 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "plotlink",
- "version": "1.40.5",
+ "version": "1.41.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "plotlink",
- "version": "1.40.5",
+ "version": "1.41.0",
"workspaces": [
"packages/*"
],
diff --git a/package.json b/package.json
index 0bc61fb..eebae34 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "plotlink",
- "version": "1.40.5",
+ "version": "1.41.0",
"private": true,
"workspaces": [
"packages/*"
diff --git a/src/app/airdrop/AirdropStateMachine.tsx b/src/app/airdrop/AirdropStateMachine.tsx
index aeac4cd..5607f80 100644
--- a/src/app/airdrop/AirdropStateMachine.tsx
+++ b/src/app/airdrop/AirdropStateMachine.tsx
@@ -1,8 +1,8 @@
"use client";
-import { useEffect, useState, useCallback } from "react";
+import { useEffect, useState, useCallback, useRef } from "react";
import { useAccount } from "wagmi";
-import Link from "next/link";
+import { useConnectModal } from "@rainbow-me/rainbowkit";
import { CampaignHero } from "../../components/airdrop/CampaignHero";
import { ActivationFlow } from "../../components/airdrop/ActivationFlow";
import { ContributionPanel } from "../../components/airdrop/ContributionPanel";
@@ -23,10 +23,7 @@ const MERKLE_CLAIM_ADDRESS = process.env.NEXT_PUBLIC_MERKLE_CLAIM_ADDRESS;
const FINAL_BURN_TX = process.env.NEXT_PUBLIC_AIRDROP_FINAL_BURN_TX;
const FINAL_STATE = process.env.NEXT_PUBLIC_AIRDROP_FINAL_STATE as "sub_bronze" | "zero_recipient" | undefined;
-function deriveState(
- isConnected: boolean,
- activatedAt: string | null,
-): AirdropState {
+function deriveState(isConnected: boolean, activatedAt: string | null): AirdropState {
if (IS_PAUSED) return "paused";
if (MERKLE_CLAIM_ADDRESS) return "settlement-normal";
if (FINAL_BURN_TX) return "settlement-final-burn";
@@ -36,14 +33,50 @@ function deriveState(
const needsFetch = !IS_PAUSED && !MERKLE_CLAIM_ADDRESS && !FINAL_BURN_TX;
+function HeroBlock({ isConnected, onActivate }: { isConnected: boolean; onActivate: () => void }) {
+ const { openConnectModal } = useConnectModal();
+
+ return (
+
+
+ The airdrop pool
+ grows with us.
+ Or it burns.
+
+
Buy storyline tokens · Bring friends · Push PLOT together
+ {!isConnected ? (
+
+ ) : (
+
+ )}
+
One signature + a few clicks. ~2 min.
+
+ );
+}
+
export function AirdropStateMachine() {
const { address, isConnected } = useAccount();
const [fetchResult, setFetchResult] = useState<{ activatedAt: string | null; done: boolean }>({ activatedAt: null, done: !needsFetch });
+ const activationRef = useRef(null);
const onActivated = useCallback(() => {
setFetchResult({ activatedAt: new Date().toISOString(), done: true });
}, []);
+ const scrollToActivation = useCallback(() => {
+ activationRef.current?.scrollIntoView({ behavior: "smooth", block: "start" });
+ }, []);
+
useEffect(() => {
if (!needsFetch || !isConnected || !address) return;
@@ -57,14 +90,13 @@ export function AirdropStateMachine() {
const activatedAt = (needsFetch && isConnected) ? fetchResult.activatedAt : null;
const loading = needsFetch && isConnected && !fetchResult.done;
-
const state = deriveState(isConnected, activatedAt);
if (state === "paused") {
return (
-
-
Campaign Paused
-
Campaign temporarily paused. Will resume shortly.
+
+
Campaign Paused
+
Campaign temporarily paused. Will resume shortly.
);
}
@@ -73,9 +105,7 @@ export function AirdropStateMachine() {
return (
<>
-
-
-
+
>
);
}
@@ -84,9 +114,7 @@ export function AirdropStateMachine() {
return (
<>
-
-
-
+
>
);
}
@@ -95,9 +123,7 @@ export function AirdropStateMachine() {
return (
<>
-
+
>
);
}
@@ -106,32 +132,27 @@ export function AirdropStateMachine() {
return (
<>
-
- {!isConnected ? (
-
-
Connect your wallet to get started.
-
- Browse storylines →
-
-
- ) : (
+
+ {isConnected && (
+
+ )}
+
>
);
}
+ // mining
return (
<>
>
);
diff --git a/src/app/globals.css b/src/app/globals.css
index 791516e..d97f20a 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -14,6 +14,8 @@
--success: oklch(45% 0.14 145);
--burn: oklch(50% 0.20 25);
--dist: oklch(45% 0.16 145);
+ --gold-1: #FFE07A;
+ --gold-2: #D9A847;
--font-display: 'Newsreader', 'Iowan Old Style', Georgia, serif;
--font-body: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
diff --git a/src/components/airdrop/MilestoneClimb.tsx b/src/components/airdrop/MilestoneClimb.tsx
index 82f2f05..2657826 100644
--- a/src/components/airdrop/MilestoneClimb.tsx
+++ b/src/components/airdrop/MilestoneClimb.tsx
@@ -1,10 +1,11 @@
"use client";
import { useEffect, useState } from "react";
-import { useAccount } from "wagmi";
interface StatusData {
currentFdv: number;
+ latestPriceUsd: number | null;
+ poolAmount: number;
milestones: {
bronze: { mcap: number; pct: number; reached: boolean };
silver: { mcap: number; pct: number; reached: boolean };
@@ -13,136 +14,155 @@ interface StatusData {
};
}
-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",
-};
+const TIERS = [
+ { key: "bronze", emoji: "\u{1F949}", label: "Bronze", cx: 140, cy: 164 },
+ { key: "silver", emoji: "\u{1F948}", label: "Silver", cx: 260, cy: 125 },
+ { key: "gold", emoji: "\u{1F947}", label: "Gold", cx: 400, cy: 55 },
+ { key: "diamond", emoji: "\u{1F48E}", label: "Diamond", cx: 530, cy: 18 },
+] as const;
function formatMcap(n: number): string {
- if (n >= 1_000_000) return `$${(n / 1_000_000).toFixed(0)}M`;
+ if (n >= 1_000_000) return `$${(n / 1_000_000).toFixed(1)}M`;
if (n >= 1_000) return `$${(n / 1_000).toFixed(0)}K`;
return `$${n}`;
}
+function formatPool(n: number): string {
+ if (n >= 1_000_000) return `$${(n / 1_000_000).toFixed(1)}M`;
+ if (n >= 1_000) return `$${(n / 1_000).toFixed(0)}K`;
+ if (n >= 1) return `$${Math.round(n)}`;
+ return "$0";
+}
+
+function interpolateFdvX(fdv: number, milestones: StatusData["milestones"]): number {
+ const mcaps = [milestones.bronze.mcap, milestones.silver.mcap, milestones.gold.mcap, milestones.diamond.mcap];
+ const xs = [140, 260, 400, 530];
+
+ if (fdv <= 0) return 30;
+ if (fdv >= mcaps[3]) return 530;
+
+ const logFdv = Math.log10(fdv);
+ const logMin = Math.log10(mcaps[0] * 0.1);
+
+ for (let i = 0; i < mcaps.length; i++) {
+ if (fdv <= mcaps[i]) {
+ const prevX = i === 0 ? 30 : xs[i - 1];
+ const prevLog = i === 0 ? logMin : Math.log10(mcaps[i - 1]);
+ const curLog = Math.log10(mcaps[i]);
+ const t = (logFdv - prevLog) / (curLog - prevLog);
+ return prevX + t * (xs[i] - prevX);
+ }
+ }
+ return 530;
+}
+
+function interpolateFdvY(x: number): number {
+ const points = [[30, 175], [140, 164], [260, 125], [400, 55], [530, 18]];
+ for (let i = 1; i < points.length; i++) {
+ if (x <= points[i][0]) {
+ const t = (x - points[i - 1][0]) / (points[i][0] - points[i - 1][0]);
+ return points[i - 1][1] + t * (points[i][1] - points[i - 1][1]);
+ }
+ }
+ return 18;
+}
+
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))
+ .then(d => { if (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...
+
+
Loading milestone chart...
);
}
- 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;
+ const { milestones, currentFdv, latestPriceUsd, poolAmount } = status;
+ const plotPrice = latestPriceUsd ?? 0;
- return (
-
-
Milestone Climb
+ const tierData = TIERS.map(t => {
+ const m = milestones[t.key as keyof typeof milestones];
+ return {
+ ...t,
+ mcap: m.mcap,
+ pct: m.pct,
+ reached: m.reached,
+ poolUsd: (poolAmount * m.pct / 100) * plotPrice,
+ };
+ });
-