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
13 changes: 6 additions & 7 deletions src/app/airdrop/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import type { Metadata } from "next";
import { CampaignHero } from "../../components/airdrop/CampaignHero";
import { MilestoneTrack } from "../../components/airdrop/MilestoneTrack";
import { ReferralInput } from "../../components/ReferralInput";

export const metadata: Metadata = {
Expand All @@ -8,14 +10,11 @@ export const metadata: Metadata = {

export default function AirdropPage() {
return (
<main className="mx-auto max-w-xl px-4 py-8">
<h1 className="text-foreground text-xl font-bold mb-2">PLOT 10x Airdrop</h1>
<p className="text-muted text-sm mb-6">
Earn PL points through trading, writing, rating, and referrals.
Campaign details coming soon.
</p>
<main className="mx-auto max-w-xl px-4 py-8 space-y-6">
<CampaignHero />
<MilestoneTrack />

<section className="mb-6">
<section>
<h2 className="text-foreground text-sm font-bold mb-3">Referral</h2>
<ReferralInput />
</section>
Expand Down
166 changes: 166 additions & 0 deletions src/components/airdrop/CampaignHero.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
"use client";

import { useQuery } from "@tanstack/react-query";
import { formatUsdValue } from "../../../lib/usd-price";

interface StatusData {
campaignStart: string;
campaignEnd: string;
timeRemainingDays: number;
timeElapsedPercent: number;
poolAmount: number;
currentMcap: number;
latestPriceUsd: number | null;
milestones: {
bronze: { mcap: number; pct: number; reached: boolean };
silver: { mcap: number; pct: number; reached: boolean };
gold: { mcap: number; pct: number; reached: boolean };
};
totalPointsEarned: number;
totalParticipants: number;
lockerId: string | null;
}

function useAirdropStatus() {
return useQuery<StatusData>({
queryKey: ["airdrop-status"],
queryFn: async () => {
const res = await fetch("/api/airdrop/status");
if (!res.ok) throw new Error("Failed to fetch status");
return res.json();
},
staleTime: 60_000,
refetchInterval: 60_000,
});
}

export function CampaignHero() {
const { data, isLoading } = useAirdropStatus();

if (isLoading || !data) {
return (
<div className="border-border rounded border p-4">
<div className="text-muted text-sm">Loading campaign status...</div>
</div>
);
}

// Find the next milestone target
const nextMilestone = !data.milestones.bronze.reached
? { name: "Bronze", mcap: data.milestones.bronze.mcap }
: !data.milestones.silver.reached
? { name: "Silver", mcap: data.milestones.silver.mcap }
: !data.milestones.gold.reached
? { name: "Gold", mcap: data.milestones.gold.mcap }
: null;

const mcapProgress = nextMilestone
? Math.min(100, (data.currentMcap / nextMilestone.mcap) * 100)
: 100;

return (
<div className="border-border rounded border p-4 space-y-4">
<div>
<h2 className="text-foreground text-lg font-bold">PLOT 10x Airdrop</h2>
<p className="text-muted text-xs mt-1">
{data.poolAmount.toLocaleString()} PLOT locked. Big or nothing.
</p>
</div>

{/* Time progress */}
<div>
<div className="flex justify-between text-xs mb-1">
<span className="text-muted">Time remaining</span>
<span className="text-foreground font-medium">{data.timeRemainingDays} days</span>
</div>
<div className="bg-surface border-border h-2 rounded border overflow-hidden">
<div
className="bg-accent h-full transition-all"
style={{ width: `${data.timeElapsedPercent}%` }}
/>
</div>
<div className="text-muted text-[10px] mt-0.5 text-right">{data.timeElapsedPercent}% elapsed</div>
</div>

{/* Market Cap progress */}
<div>
<div className="flex justify-between text-xs mb-1">
<span className="text-muted">Market Cap</span>
<span className="text-foreground font-medium">{formatUsdValue(data.currentMcap)}</span>
</div>
<div className="bg-surface border-border h-2 rounded border overflow-hidden">
<div
className="bg-accent h-full transition-all"
style={{ width: `${mcapProgress}%` }}
/>
</div>
<div className="text-muted text-[10px] mt-0.5 text-right">
{nextMilestone ? `< ${nextMilestone.name} (${formatUsdValue(nextMilestone.mcap)})` : "Gold reached"}
</div>
</div>

{/* Pool value at next milestone */}
{data.latestPriceUsd != null && data.latestPriceUsd > 0 && (
<div className="text-center text-xs">
{nextMilestone ? (
<span className="text-muted">
Pool value if {nextMilestone.name}:{" "}
<span className="text-foreground font-medium">
{formatUsdValue(
data.poolAmount *
(data.milestones[
nextMilestone.name.toLowerCase() as keyof typeof data.milestones
].pct / 100) *
data.latestPriceUsd
)}
</span>
</span>
) : (
<span className="text-muted">
Pool value at Gold:{" "}
<span className="text-foreground font-medium">
{formatUsdValue(
data.poolAmount *
(data.milestones.gold.pct / 100) *
data.latestPriceUsd
)}
</span>
</span>
)}
</div>
)}

{/* Stats row */}
<div className="grid grid-cols-3 gap-2 text-center">
<div className="border-border rounded border px-2 py-1.5">
<div className="text-foreground text-sm font-bold">{data.totalParticipants}</div>
<div className="text-muted text-[9px]">Participants</div>
</div>
<div className="border-border rounded border px-2 py-1.5">
<div className="text-foreground text-sm font-bold">{Math.round(data.totalPointsEarned).toLocaleString()}</div>
<div className="text-muted text-[9px]">PL Earned</div>
</div>
<div className="border-border rounded border px-2 py-1.5">
<div className="text-foreground text-sm font-bold">
{data.latestPriceUsd ? formatUsdValue(data.latestPriceUsd) : "—"}
</div>
<div className="text-muted text-[9px]">PLOT Price</div>
</div>
</div>

{/* Lockup proof */}
{data.lockerId && (
<div className="text-center">
<a
href={`https://mint.club/locker/${data.lockerId}`}
target="_blank"
rel="noopener noreferrer"
className="text-accent text-xs hover:underline"
>
View lockup proof on-chain
</a>
</div>
)}
</div>
);
}
133 changes: 133 additions & 0 deletions src/components/airdrop/MilestoneTrack.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
"use client";

import { useQuery } from "@tanstack/react-query";
import { formatUsdValue } from "../../../lib/usd-price";

interface StatusData {
poolAmount: number;
currentMcap: number;
latestPriceUsd: number | null;
milestones: {
bronze: { mcap: number; pct: number; reached: boolean };
silver: { mcap: number; pct: number; reached: boolean };
gold: { mcap: number; pct: number; reached: boolean };
};
}

const TIERS = [
{ key: "bronze" as const, label: "Bronze", color: "text-[#cd7f32]" },
{ key: "silver" as const, label: "Silver", color: "text-[#c0c0c0]" },
{ key: "gold" as const, label: "Gold", color: "text-[#ffd700]" },
];

export function MilestoneTrack() {
const { data, isLoading } = useQuery<StatusData>({
queryKey: ["airdrop-status"],
queryFn: async () => {
const res = await fetch("/api/airdrop/status");
if (!res.ok) throw new Error("Failed to fetch status");
return res.json();
},
staleTime: 60_000,
});

if (isLoading || !data) {
return (
<div className="border-border rounded border p-4">
<div className="text-muted text-sm">Loading milestones...</div>
</div>
);
}

// Progress across all three tiers (0–100 mapped to full track)
const goldMcap = data.milestones.gold.mcap;
const overallProgress = Math.min(100, (data.currentMcap / goldMcap) * 100);

return (
<div className="border-border rounded border p-4">
<h3 className="text-foreground text-sm font-bold mb-4">Milestone Progress</h3>

{/* Track bar */}
<div className="relative mb-6">
<div className="bg-surface border-border h-3 rounded-full border overflow-hidden">
<div
className="bg-accent h-full rounded-full transition-all"
style={{ width: `${overallProgress}%` }}
/>
</div>

{/* Current mcap marker */}
<div
className="absolute top-0 -translate-x-1/2"
style={{ left: `${overallProgress}%` }}
>
<div className="bg-foreground w-1.5 h-3 rounded-sm" />
<div className="text-foreground text-[8px] font-bold mt-0.5 whitespace-nowrap -translate-x-1/4">
{formatUsdValue(data.currentMcap)}
</div>
</div>

{/* Milestone markers */}
{TIERS.map((tier) => {
const milestone = data.milestones[tier.key];
const position = (milestone.mcap / goldMcap) * 100;
return (
<div
key={tier.key}
className="absolute top-0 -translate-x-1/2"
style={{ left: `${position}%` }}
>
<div
className={`w-3 h-3 rounded-full border-2 ${
milestone.reached
? "bg-accent border-accent"
: "bg-surface border-border"
}`}
/>
</div>
);
})}
</div>

{/* Tier details */}
<div className="grid grid-cols-3 gap-2">
{TIERS.map((tier) => {
const milestone = data.milestones[tier.key];
const poolValue = data.poolAmount * (milestone.pct / 100);
return (
<div
key={tier.key}
className={`border-border rounded border px-2 py-2 text-center ${
milestone.reached ? "border-accent" : ""
}`}
>
<div className={`text-xs font-bold ${tier.color}`}>
{tier.label}
{milestone.reached && " \u2713"}
</div>
<div className="text-foreground text-sm font-bold mt-1">
{formatUsdValue(milestone.mcap)}
</div>
<div className="text-muted text-[9px]">MCap target</div>
<div className="text-foreground text-xs font-medium mt-1">
{milestone.pct}% &middot; {poolValue.toLocaleString()} PLOT
</div>
{data.latestPriceUsd != null && data.latestPriceUsd > 0 && (
<div className="text-accent text-[10px] font-medium mt-0.5">
Pool: {formatUsdValue(poolValue * data.latestPriceUsd)}
</div>
)}
</div>
);
})}
</div>

{/* Current position label */}
<div className="text-center mt-3">
<span className="text-muted text-[10px]">
Current: {formatUsdValue(data.currentMcap)} / {formatUsdValue(goldMcap)}
</span>
</div>
</div>
);
}
Loading