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 Paused

+

Campaign temporarily paused. Will resume shortly.

+
+ ); + } + + if (state === "settlement-normal") { + return ( + <> + +
+ +
+ + ); + } + + if (state === "settlement-final-burn") { + return ( + <> + +
+ +
+ + ); + } + + if (loading) { + return ( + <> + +
+

Loading...

+
+ + ); + } + + if (state === "pre-activation") { + return ( + <> + +
+ {!isConnected ? ( +
+

Connect your wallet to get started.

+
+ ) : ( + + )} +
+ + ); + } + + return ( + <> + +
+ +
+ + +
+
+ + ); +} diff --git a/src/app/airdrop/page.tsx b/src/app/airdrop/page.tsx index 2db516d6..769d75b9 100644 --- a/src/app/airdrop/page.tsx +++ b/src/app/airdrop/page.tsx @@ -1,37 +1,15 @@ import type { Metadata } from "next"; -import { CampaignHero } from "../../components/airdrop/CampaignHero"; -import { UserPoints } from "../../components/airdrop/UserPoints"; -import { ClaimPanel } from "../../components/airdrop/ClaimPanel"; -import { Leaderboard } from "../../components/airdrop/Leaderboard"; -import { WeeklySnapshots } from "../../components/airdrop/WeeklySnapshots"; -import { getAirdropConfig } from "../../../lib/airdrop/config"; +import { AirdropStateMachine } from "./AirdropStateMachine"; export const metadata: Metadata = { - title: "PLOT Big or Nothing Airdrop | PlotLink", - description: "Earn PL points through trading, writing, and referrals.", + title: "PlotLink Buy-Back Sprint | PlotLink", + description: "Activate, trade, and earn your share of the PLOT airdrop pool.", }; export default function AirdropPage() { - const campaignEnded = new Date() > getAirdropConfig().CAMPAIGN_END; - return (
- {/* Hero spans full width */} - - - {/* 2-column grid below hero */} -
- {/* Left column: user-specific */} -
- {campaignEnded ? : } -
- - {/* Right column: global sections */} -
- - -
-
+
); } diff --git a/src/components/airdrop/ActivationFlow.tsx b/src/components/airdrop/ActivationFlow.tsx new file mode 100644 index 00000000..c6a5105b --- /dev/null +++ b/src/components/airdrop/ActivationFlow.tsx @@ -0,0 +1,10 @@ +"use client"; + +export function ActivationFlow() { + return ( +
+

Activate Your Wallet

+

Connect your X account and follow PlotLink to activate.

+
+ ); +} diff --git a/src/components/airdrop/ClaimCard.tsx b/src/components/airdrop/ClaimCard.tsx new file mode 100644 index 00000000..29ace462 --- /dev/null +++ b/src/components/airdrop/ClaimCard.tsx @@ -0,0 +1,40 @@ +"use client"; + +interface ClaimCardProps { + mode: "normal" | "final-burn"; + finalState?: "sub_bronze" | "zero_recipient" | null; +} + +export function ClaimCard({ mode, finalState }: ClaimCardProps) { + if (mode === "final-burn") { + const message = finalState === "sub_bronze" + ? "Campaign ended below Bronze milestone. All tokens burned." + : finalState === "zero_recipient" + ? "No eligible recipients. All tokens burned." + : "Campaign ended. Watch for Season 2."; + + return ( +
+

Campaign Complete

+

{message}

+ {process.env.NEXT_PUBLIC_AIRDROP_FINAL_BURN_TX && ( + + View burn transaction + + )} +
+ ); + } + + return ( +
+

Claim Your PLOT

+

The campaign has ended. Claim your share below.

+
+ ); +} diff --git a/src/components/airdrop/ContributionPanel.tsx b/src/components/airdrop/ContributionPanel.tsx new file mode 100644 index 00000000..dc3ab6d6 --- /dev/null +++ b/src/components/airdrop/ContributionPanel.tsx @@ -0,0 +1,10 @@ +"use client"; + +export function ContributionPanel() { + return ( +
+

Your Contribution

+

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

+
+ ); +} diff --git a/src/components/airdrop/MilestoneClimb.tsx b/src/components/airdrop/MilestoneClimb.tsx new file mode 100644 index 00000000..f74e5c2e --- /dev/null +++ b/src/components/airdrop/MilestoneClimb.tsx @@ -0,0 +1,10 @@ +"use client"; + +export function MilestoneClimb() { + return ( +
+

Milestone Climb

+

Track community progress toward milestone tiers.

+
+ ); +} diff --git a/src/components/airdrop/ReferralCTA.tsx b/src/components/airdrop/ReferralCTA.tsx new file mode 100644 index 00000000..965e8a74 --- /dev/null +++ b/src/components/airdrop/ReferralCTA.tsx @@ -0,0 +1,10 @@ +"use client"; + +export function ReferralCTA() { + return ( +
+

Invite Friends

+

Share your referral link to boost your multiplier.

+
+ ); +}