Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "plotlink",
"version": "1.34.1",
"version": "1.35.0",
"private": true,
"workspaces": [
"packages/*"
Expand Down
129 changes: 129 additions & 0 deletions src/app/airdrop/AirdropStateMachine.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="border-border mt-8 rounded border p-8 text-center">
<h2 className="text-accent mb-2 text-sm font-bold uppercase tracking-wider">Campaign Paused</h2>
<p className="text-muted text-sm">Campaign temporarily paused. Will resume shortly.</p>
</div>
);
}

if (state === "settlement-normal") {
return (
<>
<CampaignHero />
<div className="mt-8">
<ClaimPanel />
</div>
</>
);
}

if (state === "settlement-final-burn") {
return (
<>
<CampaignHero />
<div className="mt-8">
<ClaimCard mode="final-burn" finalState={FINAL_STATE ?? null} />
</div>
</>
);
}

if (loading) {
return (
<>
<CampaignHero />
<div className="mt-8 text-center">
<p className="text-muted text-sm">Loading...</p>
</div>
</>
);
}

if (state === "pre-activation") {
return (
<>
<CampaignHero />
<div className="mt-8">
{!isConnected ? (
<div className="border-border rounded border p-8 text-center">
<p className="text-muted text-sm">Connect your wallet to get started.</p>
</div>
) : (
<ActivationFlow />
)}
</div>
</>
);
}

return (
<>
<CampaignHero />
<div className="mt-8 space-y-4">
<ContributionPanel />
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
<ReferralCTA />
<MilestoneClimb />
</div>
</div>
</>
);
}
30 changes: 4 additions & 26 deletions src/app/airdrop/page.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<main className="mx-auto max-w-[var(--page-max)] px-6 py-8 pb-24 lg:pb-8">
{/* Hero spans full width */}
<CampaignHero />

{/* 2-column grid below hero */}
<div className="mt-8 grid grid-cols-1 gap-4 lg:grid-cols-[1fr_340px]">
{/* Left column: user-specific */}
<div className="min-w-0 space-y-6">
{campaignEnded ? <ClaimPanel /> : <UserPoints />}
</div>

{/* Right column: global sections */}
<div className="space-y-6">
<Leaderboard />
<WeeklySnapshots />
</div>
</div>
<AirdropStateMachine />
</main>
);
}
10 changes: 10 additions & 0 deletions src/components/airdrop/ActivationFlow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"use client";

export function ActivationFlow() {
return (
<div className="border-border rounded border p-6">
<h2 className="text-accent mb-2 text-sm font-bold uppercase tracking-wider">Activate Your Wallet</h2>
<p className="text-muted text-xs">Connect your X account and follow PlotLink to activate.</p>
</div>
);
}
40 changes: 40 additions & 0 deletions src/components/airdrop/ClaimCard.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="border-border rounded border p-6">
<h2 className="text-accent mb-2 text-sm font-bold uppercase tracking-wider">Campaign Complete</h2>
<p className="text-muted text-xs">{message}</p>
{process.env.NEXT_PUBLIC_AIRDROP_FINAL_BURN_TX && (
<a
href={`https://basescan.org/tx/${process.env.NEXT_PUBLIC_AIRDROP_FINAL_BURN_TX}`}
target="_blank"
rel="noopener noreferrer"
className="text-accent mt-2 inline-block text-xs underline"
>
View burn transaction
</a>
)}
</div>
);
}

return (
<div className="border-border rounded border p-6">
<h2 className="text-accent mb-2 text-sm font-bold uppercase tracking-wider">Claim Your PLOT</h2>
<p className="text-muted text-xs">The campaign has ended. Claim your share below.</p>
</div>
);
}
10 changes: 10 additions & 0 deletions src/components/airdrop/ContributionPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"use client";

export function ContributionPanel() {
return (
<div className="border-border rounded border p-6">
<h2 className="text-accent mb-2 text-sm font-bold uppercase tracking-wider">Your Contribution</h2>
<p className="text-muted text-xs">Buy PLOT tokens to earn your share of the airdrop pool.</p>
</div>
);
}
10 changes: 10 additions & 0 deletions src/components/airdrop/MilestoneClimb.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"use client";

export function MilestoneClimb() {
return (
<div className="border-border rounded border p-4">
<h3 className="text-accent mb-1 text-xs font-bold uppercase tracking-wider">Milestone Climb</h3>
<p className="text-muted text-xs">Track community progress toward milestone tiers.</p>
</div>
);
}
10 changes: 10 additions & 0 deletions src/components/airdrop/ReferralCTA.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"use client";

export function ReferralCTA() {
return (
<div className="border-border rounded border p-4">
<h3 className="text-accent mb-1 text-xs font-bold uppercase tracking-wider">Invite Friends</h3>
<p className="text-muted text-xs">Share your referral link to boost your multiplier.</p>
</div>
);
}
Loading