From dc33591a1f86c3bfb3049b37558b82b622b542f4 Mon Sep 17 00:00:00 2001 From: dev-agent Date: Tue, 26 May 2026 09:45:22 +0000 Subject: [PATCH 1/2] [#1252] Build ContributionPanel.tsx reading from /projection Displays spend, qualified refs, FC bonus status, multiplier, weighted spend, share %, and projected share at all 4 milestone tiers. Single fetch to /api/airdrop/projection. Handles loading, error, and no-data states. Does not fetch /points (deprecated) or /referral-code (ReferralCTA's responsibility). Closes #1252 Co-Authored-By: Claude Opus 4.7 (1M context) --- package-lock.json | 4 +- package.json | 2 +- src/components/airdrop/ContributionPanel.tsx | 129 ++++++++++++++++++- 3 files changed, 129 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0411ca6..c8b864e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "plotlink", - "version": "1.36.0", + "version": "1.37.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "plotlink", - "version": "1.36.0", + "version": "1.37.0", "workspaces": [ "packages/*" ], diff --git a/package.json b/package.json index fe9fa37..b8b2820 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "plotlink", - "version": "1.36.0", + "version": "1.37.0", "private": true, "workspaces": [ "packages/*" diff --git a/src/components/airdrop/ContributionPanel.tsx b/src/components/airdrop/ContributionPanel.tsx index dc3ab6d..0a9d011 100644 --- a/src/components/airdrop/ContributionPanel.tsx +++ b/src/components/airdrop/ContributionPanel.tsx @@ -1,10 +1,133 @@ "use client"; +import { useEffect, useState } from "react"; +import { useAccount } from "wagmi"; + +interface ProjectionData { + address: string; + buy_volume: number; + qualified_refs: number; + has_fc_bonus: boolean; + multiplier: number; + weighted_spend: number; + community_total: number; + projected_share: { + bronze: number; + silver: number; + gold: number; + diamond: number; + }; +} + +function Stat({ label, value, accent }: { label: string; value: string; accent?: boolean }) { + return ( +
+
{label}
+
{value}
+
+ ); +} + export function ContributionPanel() { + const { address, isConnected } = useAccount(); + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + if (!isConnected || !address) { + setLoading(false); + return; + } + + let cancelled = false; + setLoading(true); + setError(null); + + fetch(`/api/airdrop/projection?address=${address.toLowerCase()}`) + .then(res => { + if (res.status === 404) return null; + if (!res.ok) throw new Error("Failed to load"); + return res.json(); + }) + .then(d => { if (!cancelled) setData(d); }) + .catch(() => { if (!cancelled) setError("Failed to load contribution data."); }) + .finally(() => { if (!cancelled) setLoading(false); }); + + return () => { cancelled = true; }; + }, [isConnected, address]); + + if (loading) { + return ( +
+

Loading contribution data...

+
+ ); + } + + if (error) { + return ( +
+

{error}

+
+ ); + } + + if (!data) { + return ( +
+

Your Contribution

+

Buy PLOT tokens to start earning your share of the airdrop pool.

+
+ ); + } + + const currentShare = data.community_total > 0 + ? (data.weighted_spend / data.community_total * 100).toFixed(2) + : "0.00"; + return ( -
-

Your Contribution

-

Buy PLOT tokens to earn your share of the airdrop pool.

+
+

Your Contribution

+ +
+ + + + + + +
+ +
+
Projected Share by Milestone
+
+
+
Bronze
+
{Math.round(data.projected_share.bronze).toLocaleString()}
+
PLOT
+
+
+
Silver
+
{Math.round(data.projected_share.silver).toLocaleString()}
+
PLOT
+
+
+
Gold
+
{Math.round(data.projected_share.gold).toLocaleString()}
+
PLOT
+
+
+
Diamond
+
{Math.round(data.projected_share.diamond).toLocaleString()}
+
PLOT
+
+
+
); } From 334f69183ed2aa7cc14da491fe089b499edb21ae Mon Sep 17 00:00:00 2001 From: dev-agent Date: Tue, 26 May 2026 09:49:17 +0000 Subject: [PATCH 2/2] =?UTF-8?q?[#1252]=20Fix=20sync=20setState=20in=20effe?= =?UTF-8?q?ct=20=E2=80=94=20use=20single=20fetchState?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace separate data/loading/error states with single fetchState object. Only setState call in effect is in async fetch callbacks. Derive loading from fetchState.done + isConnected. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/components/airdrop/ContributionPanel.tsx | 26 +++++++++----------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/src/components/airdrop/ContributionPanel.tsx b/src/components/airdrop/ContributionPanel.tsx index 0a9d011..7a0402c 100644 --- a/src/components/airdrop/ContributionPanel.tsx +++ b/src/components/airdrop/ContributionPanel.tsx @@ -30,33 +30,31 @@ function Stat({ label, value, accent }: { label: string; value: string; accent?: export function ContributionPanel() { const { address, isConnected } = useAccount(); - const [data, setData] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); + const [fetchState, setFetchState] = useState<{ + data: ProjectionData | null; + error: string | null; + done: boolean; + }>({ data: null, error: null, done: !isConnected }); useEffect(() => { - if (!isConnected || !address) { - setLoading(false); - return; - } + if (!isConnected || !address) return; let cancelled = false; - setLoading(true); - setError(null); - fetch(`/api/airdrop/projection?address=${address.toLowerCase()}`) .then(res => { if (res.status === 404) return null; if (!res.ok) throw new Error("Failed to load"); return res.json(); }) - .then(d => { if (!cancelled) setData(d); }) - .catch(() => { if (!cancelled) setError("Failed to load contribution data."); }) - .finally(() => { if (!cancelled) setLoading(false); }); + .then(d => { if (!cancelled) setFetchState({ data: d, error: null, done: true }); }) + .catch(() => { if (!cancelled) setFetchState({ data: null, error: "Failed to load contribution data.", done: true }); }); - return () => { cancelled = true; }; + return () => { cancelled = true; setFetchState({ data: null, error: null, done: false }); }; }, [isConnected, address]); + const { data, error } = fetchState; + const loading = isConnected && !fetchState.done; + if (loading) { return (