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
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.3.0",
"version": "1.3.1",
"private": true,
"workspaces": [
"packages/*"
Expand Down
261 changes: 1 addition & 260 deletions src/components/airdrop/CampaignHero.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
"use client";

import { useEffect, useMemo, useState } from "react";
import { useEffect, useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { formatUsdValue } from "../../../lib/usd-price";
import { AIRDROP_TEST_MODE } from "../../../lib/airdrop/config";

/* ─── Types ─── */

Expand All @@ -26,24 +24,6 @@ interface StatusData {
lockerTx: string | null;
}

/* ─── Constants ─── */

const MAX_SUPPLY = 1_000_000;

const TIER_KEYS = ["bronze", "silver", "gold", "diamond"] as const;

interface MilestoneRow {
fdv: number;
pct: number;
unlockPlot: number;
poolUsd: number;
burnPct: number;
cmcRank: string | null;
isFull: boolean;
}

const CMC_RANKS = ["≈ CMC #1900", "≈ CMC #950", "≈ CMC #400", "≈ CMC #250"];

/* ─── Helpers ─── */

function useAirdropStatus() {
Expand All @@ -59,12 +39,6 @@ function useAirdropStatus() {
});
}

function formatCompact(val: number): string {
if (val >= 1_000_000) return `$${(val / 1_000_000).toFixed(1)}M`;
if (val >= 1_000) return `$${(val / 1_000).toFixed(0)}K`;
return `$${val.toFixed(0)}`;
}

function useCountdown(endDateStr: string) {
const [remaining, setRemaining] = useState({ d: 0, h: 0, m: 0, s: 0 });

Expand All @@ -88,131 +62,12 @@ function useCountdown(endDateStr: string) {
return remaining;
}

function buildMilestoneRows(
milestones: StatusData["milestones"],
poolAmount: number,
): MilestoneRow[] {
return TIER_KEYS.map((key, i) => {
const ms = milestones[key];
const price = ms.mcap / MAX_SUPPLY;
const unlockPlot = poolAmount * (ms.pct / 100);
return {
fdv: ms.mcap,
pct: ms.pct,
unlockPlot,
poolUsd: unlockPlot * price,
burnPct: 100 - ms.pct,
cmcRank: AIRDROP_TEST_MODE ? null : (CMC_RANKS[i] ?? null),
isFull: ms.pct === 100,
};
});
}

function getCurrentBurnState(
currentFdv: number,
milestones: StatusData["milestones"],
poolAmount: number,
): { burnPct: number; distributePct: number; poolUsd: number } {
const entries = TIER_KEYS.map((k) => milestones[k]);
let highestPct = 0;
for (let i = entries.length - 1; i >= 0; i--) {
if (currentFdv >= entries[i].mcap) {
highestPct = entries[i].pct;
break;
}
}
const price = currentFdv / MAX_SUPPLY;
const unlockPlot = poolAmount * (highestPct / 100);
return {
burnPct: 100 - highestPct,
distributePct: highestPct,
poolUsd: unlockPlot * price,
};
}

/* ─── Burn Bar ─── */

function BurnBar({
burnPct,
distributePct,
currentFdv,
poolUsd,
}: {
burnPct: number;
distributePct: number;
currentFdv: number;
poolUsd: number;
}) {
const isFull = distributePct >= 100;
const isAllBurned = burnPct >= 100;

return (
<div className="border-border bg-surface rounded border p-4 space-y-3">
<div className="text-foreground text-xs font-bold uppercase tracking-wider">
If the campaign ended right now...
</div>

<div className="h-5 w-full rounded overflow-hidden flex">
{burnPct > 0 && (
<div
className="h-full transition-all duration-700"
style={{
width: `${burnPct}%`,
background: "linear-gradient(90deg, #CC3333, #E8650A)",
}}
/>
)}
{distributePct > 0 && (
<div
className="h-full transition-all duration-700"
style={{
width: `${distributePct}%`,
background: "linear-gradient(90deg, #2D8B4E, #00CC66)",
}}
/>
)}
</div>

<div className="flex items-center justify-between text-[11px]">
<span className={isAllBurned ? "text-[#CC3333] font-bold" : "text-muted"}>
← {burnPct}% BURNED
</span>
<span className={isFull ? "text-[#00CC66] font-bold" : "text-muted"}>
{isFull ? "FULL DISTRIBUTION" : `${distributePct}% distributed →`}
</span>
</div>

<div className="flex items-center justify-between text-xs">
<span className="text-muted">
Current MCap: {currentFdv > 0 ? formatUsdValue(currentFdv) : "—"}
</span>
<span className="text-foreground font-bold">
Pool value right now: {poolUsd > 0 ? formatUsdValue(poolUsd) : "$0"}
</span>
</div>
</div>
);
}

/* ─── Main component ─── */

export function CampaignHero() {
const { data, isLoading } = useAirdropStatus();
const countdown = useCountdown(data?.campaignEnd ?? "2027-01-01");

const milestoneRows = useMemo(
() => (data ? buildMilestoneRows(data.milestones, data.poolAmount) : []),
[data],
);

const burnState = useMemo(
() =>
data
? getCurrentBurnState(data.currentFdv, data.milestones, data.poolAmount)
: { burnPct: 100, distributePct: 0, poolUsd: 0 },
[data],
);

if (isLoading || !data) {
return (
<div className="border-border rounded border p-4">
Expand Down Expand Up @@ -275,120 +130,6 @@ export function CampaignHero() {
)}
</div>

{/* ── Burn bar ── */}
<BurnBar
burnPct={burnState.burnPct}
distributePct={burnState.distributePct}
currentFdv={data.currentFdv}
poolUsd={burnState.poolUsd}
/>

{/* ── Section 1: Your airdrop grows with $PLOT ── */}
<div className="space-y-3">
<div className="text-foreground text-xs font-bold uppercase tracking-wider text-center">
Your airdrop grows with $PLOT
</div>
<p className="text-muted text-xs text-center max-w-md mx-auto">
The pool is {data.poolAmount.toLocaleString()} PLOT. Its value depends on the market.
</p>

<div className="border-border rounded border overflow-hidden">
{/* "Now" row */}
<div className="grid grid-cols-3 gap-x-4 py-2.5 px-3 text-xs bg-surface">
<span className="text-muted font-bold">Now</span>
<span className="text-muted">MCap {data.currentFdv > 0 ? formatCompact(data.currentFdv) : "—"}</span>
<span className="text-muted">Pool value: {data.currentFdv > 0 ? formatUsdValue(burnState.poolUsd) : "$0"}</span>
</div>
{/* Milestone rows */}
{milestoneRows.map((row, i) => (
<div key={row.fdv} className="grid grid-cols-3 gap-x-4 py-2.5 px-3 text-xs border-border border-t">
<span className={row.isFull ? "text-accent font-bold" : "text-foreground font-medium"}>
Step {i + 1}
</span>
<span className="text-foreground">MCap {formatCompact(row.fdv)}</span>
<span className={row.isFull ? "text-accent font-bold" : "text-foreground"}>
Pool ~{formatCompact(row.poolUsd)}
</span>
</div>
))}
</div>

<p className="text-muted text-[11px] text-center max-w-md mx-auto leading-relaxed">
The same {data.poolAmount.toLocaleString()} PLOT — worth{" "}
{data.currentFdv > 0 ? formatUsdValue(burnState.poolUsd) : "$0"} today, or{" "}
~{formatCompact(milestoneRows[milestoneRows.length - 1]?.poolUsd ?? 0)} at full distribution.
Your airdrop size = market growth.
</p>
</div>

{/* ── Section 2: Four steps — not unrealistic ── */}
<div className="space-y-3">
<div className="text-foreground text-xs font-bold uppercase tracking-wider text-center">
Four steps — not unrealistic
</div>
<p className="text-muted text-xs text-center max-w-md mx-auto">
Each step unlocks a bigger share of the pool.
</p>

<div className="grid grid-cols-2 sm:grid-cols-4 gap-2">
{milestoneRows.map((row) => {
const reached = data.currentFdv >= row.fdv;
return (
<div
key={row.fdv}
className={`rounded border px-3 py-3 text-center space-y-1.5 ${
reached ? "border-accent" : "border-border opacity-60"
}`}
>
<div className={`text-sm font-bold ${reached ? "text-accent" : "text-foreground"}`}>
{formatCompact(row.fdv)}
</div>
{row.cmcRank && (
<div className="text-muted text-[10px]">{row.cmcRank}</div>
)}
<div className="text-muted text-[10px]">unlocks</div>
<div className={`text-xs font-bold ${reached ? "text-accent" : "text-foreground"}`}>
{row.pct}%
</div>
</div>
);
})}
</div>

{!AIRDROP_TEST_MODE && (
<p className="text-muted text-[11px] text-center max-w-md mx-auto leading-relaxed">
These aren&apos;t moonshot numbers — #250 on CMC is a mid-tier project.
Thousands of tokens have done it.
</p>
)}
</div>

{/* ── Section 3: Not reached? Burned forever. ── */}
<div className="space-y-3">
<div className="text-foreground text-xs font-bold uppercase tracking-wider text-center">
Not reached? Burned forever.
</div>

<div className="border-border bg-surface rounded border px-4 py-4 text-center space-y-2">
<div className="text-foreground text-sm font-bold">
If MCap stays below {formatCompact(milestoneRows[0]?.fdv ?? 0)}:
</div>
<div className="text-foreground text-base">
{data.poolAmount.toLocaleString()} PLOT &rarr; burned permanently &#x1F525;
</div>
<p className="text-muted text-xs leading-relaxed max-w-sm mx-auto">
No team keeps it. No treasury recycles it.<br />
Burned = reduced supply = value for holders.
</p>
</div>

<p className="text-muted text-[11px] text-center max-w-md mx-auto leading-relaxed">
Either way, PLOT holders benefit:<br />
reach milestones &rarr; earn airdrop.
Miss milestones &rarr; supply shrinks.
</p>
</div>

{/* ── Participant count ── */}
<div className="text-center text-muted text-xs">
{data.totalParticipants > 0
Expand Down
Loading