diff --git a/package-lock.json b/package-lock.json index f38f9c1d..50ef49b5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "plotlink", - "version": "1.34.1", + "version": "1.35.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "plotlink", - "version": "1.34.1", + "version": "1.35.0", "workspaces": [ "packages/*" ], diff --git a/package.json b/package.json index 2c86e178..05b44a3e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "plotlink", - "version": "1.34.1", + "version": "1.35.0", "private": true, "workspaces": [ "packages/*" diff --git a/src/app/airdrop/AirdropStateMachine.tsx b/src/app/airdrop/AirdropStateMachine.tsx new file mode 100644 index 00000000..b75b3f3a --- /dev/null +++ b/src/app/airdrop/AirdropStateMachine.tsx @@ -0,0 +1,129 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { useAccount } from "wagmi"; +import { CampaignHero } from "../../components/airdrop/CampaignHero"; +import { ActivationFlow } from "../../components/airdrop/ActivationFlow"; +import { ContributionPanel } from "../../components/airdrop/ContributionPanel"; +import { ReferralCTA } from "../../components/airdrop/ReferralCTA"; +import { MilestoneClimb } from "../../components/airdrop/MilestoneClimb"; +import { ClaimCard } from "../../components/airdrop/ClaimCard"; +import { ClaimPanel } from "../../components/airdrop/ClaimPanel"; + +type AirdropState = + | "paused" + | "pre-activation" + | "mining" + | "settlement-normal" + | "settlement-final-burn"; + +const IS_PAUSED = process.env.NEXT_PUBLIC_AIRDROP_PAUSED === "1"; +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 { + if (IS_PAUSED) return "paused"; + if (MERKLE_CLAIM_ADDRESS) return "settlement-normal"; + if (FINAL_BURN_TX) return "settlement-final-burn"; + if (!isConnected || !activatedAt) return "pre-activation"; + return "mining"; +} + +const needsFetch = !IS_PAUSED && !MERKLE_CLAIM_ADDRESS && !FINAL_BURN_TX; + +export function AirdropStateMachine() { + const { address, isConnected } = useAccount(); + const [fetchResult, setFetchResult] = useState<{ activatedAt: string | null; done: boolean }>({ activatedAt: null, done: !needsFetch }); + + useEffect(() => { + if (!needsFetch || !isConnected || !address) return; + + let cancelled = false; + fetch(`/api/airdrop/activation-status?address=${address.toLowerCase()}`) + .then(res => res.json()) + .then(data => { if (!cancelled) setFetchResult({ activatedAt: data.activated_at ?? null, done: true }); }) + .catch(() => { if (!cancelled) setFetchResult({ activatedAt: null, done: true }); }); + return () => { cancelled = true; setFetchResult({ activatedAt: null, done: false }); }; + }, [isConnected, address]); + + const activatedAt = (needsFetch && isConnected) ? fetchResult.activatedAt : null; + const loading = needsFetch && isConnected && !fetchResult.done; + + const state = deriveState(isConnected, activatedAt); + + if (state === "paused") { + return ( +
Campaign temporarily paused. Will resume shortly.
+Loading...
+Connect your wallet to get started.
+Connect your X account and follow PlotLink to activate.
+{message}
+ {process.env.NEXT_PUBLIC_AIRDROP_FINAL_BURN_TX && ( + + View burn transaction + + )} +The campaign has ended. Claim your share below.
+Buy PLOT tokens to earn your share of the airdrop pool.
+Track community progress toward milestone tiers.
+Share your referral link to boost your multiplier.
+