diff --git a/src/components/airdrop/CampaignHero.tsx b/src/components/airdrop/CampaignHero.tsx index 7354c69..166782a 100644 --- a/src/components/airdrop/CampaignHero.tsx +++ b/src/components/airdrop/CampaignHero.tsx @@ -29,33 +29,20 @@ interface StatusData { const MAX_SUPPLY = 1_000_000; -const TIER_META = [ - { key: "bronze" as const, emoji: "🥉", label: "Bronze", cmcRank: 1900 }, - { key: "silver" as const, emoji: "🥈", label: "Silver", cmcRank: 950 }, - { key: "gold" as const, emoji: "🥇", label: "Gold", cmcRank: 400 }, - { key: "diamond" as const, emoji: "💎", label: "Diamond", cmcRank: 250 }, -]; - -type Tier = { - key: string; - emoji: string; - label: string; - cmcRank: number; +const TIER_KEYS = ["bronze", "silver", "gold", "diamond"] as const; + +interface MilestoneRow { fdv: number; pct: number; - reached: boolean; -}; - -const IS_PROD_MODE = process.env.NEXT_PUBLIC_AIRDROP_MODE !== "test"; - -/** Build tier array from API milestones so test/prod config is respected */ -function buildTiers(milestones: StatusData["milestones"]): Tier[] { - return TIER_META.map((m) => { - const ms = milestones[m.key]; - return { ...m, fdv: ms.mcap, pct: ms.pct, reached: ms.reached }; - }); + 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() { @@ -71,18 +58,12 @@ function useAirdropStatus() { }); } -/** Pool USD at a given milestone: poolAmount * (pct/100) * (fdv / maxSupply). */ -function poolUsdAtTier(tier: Tier, poolAmount: number): number { - return poolAmount * (tier.pct / 100) * (tier.fdv / MAX_SUPPLY); -} - -/** PLOT unlocked at a given milestone. */ -function plotAtTier(tier: Tier, poolAmount: number): number { - return poolAmount * (tier.pct / 100); +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)}`; } -/* ─── Countdown hook ─── */ - function useCountdown(endDateStr: string) { const [remaining, setRemaining] = useState({ d: 0, h: 0, m: 0, s: 0 }); @@ -106,147 +87,107 @@ function useCountdown(endDateStr: string) { return remaining; } -/* ─── Segmented progress bar ─── */ +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: 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 ─── */ -/** - * 4-segment progress bar. Each segment represents one milestone tier and - * fills based on log-scale progress between adjacent milestones, so reaching - * Bronze visibly fills the first segment instead of looking like 1% of the bar. - */ -function SegmentedProgressBar({ - tiers, +function BurnBar({ + burnPct, + distributePct, currentFdv, + poolUsd, }: { - tiers: Tier[]; + burnPct: number; + distributePct: number; currentFdv: number; + poolUsd: number; }) { - const segments = useMemo(() => { - return tiers.map((t, i) => { - const lowerFdv = i === 0 ? t.fdv / 10 : tiers[i - 1].fdv; - let fillPct = 0; - if (currentFdv >= t.fdv) { - fillPct = 100; - } else if (currentFdv > lowerFdv) { - const logCur = Math.log10(currentFdv); - const logLow = Math.log10(lowerFdv); - const logHi = Math.log10(t.fdv); - fillPct = ((logCur - logLow) / (logHi - logLow)) * 100; - } - return { ...t, fillPct }; - }); - }, [tiers, currentFdv]); - - const indicatorIdx = segments.findIndex((s) => s.fillPct < 100 && s.fillPct > 0); - const indicatorSegment = - indicatorIdx === -1 - ? segments.findIndex((s) => s.fillPct === 0) - : indicatorIdx; + const isFull = distributePct >= 100; + const isAllBurned = burnPct >= 100; return ( -
-
- {segments.map((s) => ( -
-
-
- ))} +
+
+ If the campaign ended right now...
-
- {segments.map((s, i) => ( +
+ {burnPct > 0 && (
-
- {s.emoji} {formatUsdValue(s.fdv)} -
- {indicatorSegment === i && currentFdv > 0 && ( -
- Current: {formatUsdValue(currentFdv)} -
- )} -
- ))} -
-
- ); -} - -/* ─── Milestone card ─── */ - -function MilestoneCard({ - tier, - poolAmount, - isCurrentTarget, -}: { - tier: Tier; - poolAmount: number; - isCurrentTarget: boolean; -}) { - const plot = plotAtTier(tier, poolAmount); - const poolUsd = poolUsdAtTier(tier, poolAmount); - const burnPct = 100 - tier.pct; - - const visualState = tier.reached - ? "border-accent text-foreground" - : isCurrentTarget - ? "border-border text-foreground" - : "border-border opacity-50"; - - return ( -
-
-
- {tier.emoji} {tier.label} -
- {tier.reached && ( - + className="h-full transition-all duration-700" + style={{ + width: `${burnPct}%`, + background: "linear-gradient(90deg, #CC3333, #E8650A)", + }} + /> )} -
- -
-
- FDV - {formatUsdValue(tier.fdv)} -
- - {IS_PROD_MODE && ( -
- CMC - ~#{tier.cmcRank.toLocaleString()} -
+ {distributePct > 0 && ( +
)} +
-
- Unlock - {tier.pct}% -
-
- PLOT - {plot.toLocaleString()} -
+
+ + ← {burnPct}% BURNED + + + {isFull ? "FULL DISTRIBUTION" : `${distributePct}% distributed →`} + +
-
- Pool - ~{formatUsdValue(poolUsd)} -
-
- Burn - {burnPct}% -
+
+ + Current FDV: {currentFdv > 0 ? formatUsdValue(currentFdv) : "—"} + + + Pool value right now: {poolUsd > 0 ? formatUsdValue(poolUsd) : "$0"} +
); @@ -258,8 +199,16 @@ export function CampaignHero() { const { data, isLoading } = useAirdropStatus(); const countdown = useCountdown(data?.campaignEnd ?? "2027-01-01"); - const tiers = useMemo( - () => (data ? buildTiers(data.milestones) : []), + 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], ); @@ -273,40 +222,19 @@ export function CampaignHero() { const pad2 = (n: number) => String(n).padStart(2, "0"); - // Current target = first unreached tier; null if all reached - const currentTargetIdx = tiers.findIndex((t) => !t.reached); - return ( -
- {/* Title + Explanation */} +
+ {/* ── Bold headline ── */}
-

- PLOT Big or Nothing Airdrop +

+ 5% OF ALL PLOT — LOCKED

-

- {data.poolAmount.toLocaleString()} PLOT locked in a time-locked contract. - Reach milestone FDV targets and the pool is distributed to point holders. - Miss them and the unreached portion is burned forever. +

+ Grow the market. Or watch it burn.

- - {/* Lock-up proof */} - {data.lockerTx ? ( - - 🔒 View lock-up proof on Basescan - - ) : ( - - 🔒 Lock-up proof: pending - - )}
- {/* Live Countdown */} + {/* ── Countdown ── */} {data.timeRemainingDays > 0 && (
{[ @@ -328,19 +256,85 @@ export function CampaignHero() {
)} - {/* Segmented progress bar */} - - - {/* Milestone cards: 2x2 mobile, 4-col desktop */} -
- {tiers.map((t, i) => ( - - ))} + {/* ── Lock-up proof ── */} +
+ {data.lockerTx ? ( + + 🔒 Verified on Basescan + + ) : ( + + 🔒 Lock-up proof: pending + + )} +
+ + {/* ── Burn bar ── */} + + + {/* ── What happens as PLOT grows ── */} +
+
+ What happens as PLOT grows +
+ +
+ {milestoneRows.map((row, i) => ( +
+ {i > 0 &&
} +
+
+ + FDV {formatCompact(row.fdv)} + + {row.cmcRank && ( + {row.cmcRank} + )} +
+
+ + {row.pct}% unlocked + + + {row.burnPct}% burned + +
+
+ {row.unlockPlot.toLocaleString()} PLOT +
+
+ Pool ~{formatCompact(row.poolUsd)} + {row.isFull && ( + + Full distribution + + )} +
+
+
+ ))} +
+
+ + {/* ── Participant count ── */} +
+ {data.totalParticipants > 0 + ? `${data.totalParticipants} participants earning` + : "Be the first to participate"}
);