diff --git a/src/app/airdrop/page.tsx b/src/app/airdrop/page.tsx index d7879477..f145e653 100644 --- a/src/app/airdrop/page.tsx +++ b/src/app/airdrop/page.tsx @@ -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 = { @@ -8,14 +10,11 @@ export const metadata: Metadata = { export default function AirdropPage() { return ( -
-

PLOT 10x Airdrop

-

- Earn PL points through trading, writing, rating, and referrals. - Campaign details coming soon. -

+
+ + -
+

Referral

diff --git a/src/components/airdrop/CampaignHero.tsx b/src/components/airdrop/CampaignHero.tsx new file mode 100644 index 00000000..545eceb4 --- /dev/null +++ b/src/components/airdrop/CampaignHero.tsx @@ -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({ + 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 ( +
+
Loading campaign status...
+
+ ); + } + + // 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 ( +
+
+

PLOT 10x Airdrop

+

+ {data.poolAmount.toLocaleString()} PLOT locked. Big or nothing. +

+
+ + {/* Time progress */} +
+
+ Time remaining + {data.timeRemainingDays} days +
+
+
+
+
{data.timeElapsedPercent}% elapsed
+
+ + {/* Market Cap progress */} +
+
+ Market Cap + {formatUsdValue(data.currentMcap)} +
+
+
+
+
+ {nextMilestone ? `< ${nextMilestone.name} (${formatUsdValue(nextMilestone.mcap)})` : "Gold reached"} +
+
+ + {/* Pool value at next milestone */} + {data.latestPriceUsd != null && data.latestPriceUsd > 0 && ( +
+ {nextMilestone ? ( + + Pool value if {nextMilestone.name}:{" "} + + {formatUsdValue( + data.poolAmount * + (data.milestones[ + nextMilestone.name.toLowerCase() as keyof typeof data.milestones + ].pct / 100) * + data.latestPriceUsd + )} + + + ) : ( + + Pool value at Gold:{" "} + + {formatUsdValue( + data.poolAmount * + (data.milestones.gold.pct / 100) * + data.latestPriceUsd + )} + + + )} +
+ )} + + {/* Stats row */} +
+
+
{data.totalParticipants}
+
Participants
+
+
+
{Math.round(data.totalPointsEarned).toLocaleString()}
+
PL Earned
+
+
+
+ {data.latestPriceUsd ? formatUsdValue(data.latestPriceUsd) : "—"} +
+
PLOT Price
+
+
+ + {/* Lockup proof */} + {data.lockerId && ( + + )} +
+ ); +} diff --git a/src/components/airdrop/MilestoneTrack.tsx b/src/components/airdrop/MilestoneTrack.tsx new file mode 100644 index 00000000..912e27e9 --- /dev/null +++ b/src/components/airdrop/MilestoneTrack.tsx @@ -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({ + 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 ( +
+
Loading milestones...
+
+ ); + } + + // 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 ( +
+

Milestone Progress

+ + {/* Track bar */} +
+
+
+
+ + {/* Current mcap marker */} +
+
+
+ {formatUsdValue(data.currentMcap)} +
+
+ + {/* Milestone markers */} + {TIERS.map((tier) => { + const milestone = data.milestones[tier.key]; + const position = (milestone.mcap / goldMcap) * 100; + return ( +
+
+
+ ); + })} +
+ + {/* Tier details */} +
+ {TIERS.map((tier) => { + const milestone = data.milestones[tier.key]; + const poolValue = data.poolAmount * (milestone.pct / 100); + return ( +
+
+ {tier.label} + {milestone.reached && " \u2713"} +
+
+ {formatUsdValue(milestone.mcap)} +
+
MCap target
+
+ {milestone.pct}% · {poolValue.toLocaleString()} PLOT +
+ {data.latestPriceUsd != null && data.latestPriceUsd > 0 && ( +
+ Pool: {formatUsdValue(poolValue * data.latestPriceUsd)} +
+ )} +
+ ); + })} +
+ + {/* Current position label */} +
+ + Current: {formatUsdValue(data.currentMcap)} / {formatUsdValue(goldMcap)} + +
+
+ ); +}