diff --git a/package.json b/package.json index 00464f7f..6fd7875f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "plotlink", - "version": "0.1.30", + "version": "0.1.31", "private": true, "workspaces": [ "packages/*" diff --git a/src/components/airdrop/CampaignHero.tsx b/src/components/airdrop/CampaignHero.tsx index 545eceb4..bd45889f 100644 --- a/src/components/airdrop/CampaignHero.tsx +++ b/src/components/airdrop/CampaignHero.tsx @@ -34,6 +34,29 @@ function useAirdropStatus() { }); } +function CountdownDisplay({ days }: { days: number }) { + const weeks = Math.floor(days / 7); + const remainingDays = days % 7; + + return ( +
+ {weeks > 0 && ( +
+
{weeks}
+
weeks
+
+ )} + {weeks > 0 && ( +
:
+ )} +
+
{weeks > 0 ? remainingDays : days}
+
days
+
+
+ ); +} + export function CampaignHero() { const { data, isLoading } = useAirdropStatus(); @@ -45,88 +68,33 @@ export function CampaignHero() { ); } - // 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. +

+ {/* Title + Tagline */} +
+

+ PLOT Big or Nothing Airdrop +

+

+ {data.poolAmount.toLocaleString()} PLOT locked. Earn or burn.

- {/* 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 - )} - - - )} + {/* Countdown */} + {data.timeRemainingDays > 0 && ( +
+ +
+
+
+
+
+ {data.timeElapsedPercent}% elapsed +
+
)} @@ -142,7 +110,7 @@ export function CampaignHero() {
- {data.latestPriceUsd ? formatUsdValue(data.latestPriceUsd) : "—"} + {data.latestPriceUsd ? formatUsdValue(data.latestPriceUsd) : "\u2014"}
PLOT Price
diff --git a/src/components/airdrop/MilestoneTrack.tsx b/src/components/airdrop/MilestoneTrack.tsx index 912e27e9..eeee42dc 100644 --- a/src/components/airdrop/MilestoneTrack.tsx +++ b/src/components/airdrop/MilestoneTrack.tsx @@ -1,5 +1,6 @@ "use client"; +import { useMemo } from "react"; import { useQuery } from "@tanstack/react-query"; import { formatUsdValue } from "../../../lib/usd-price"; @@ -14,11 +15,89 @@ interface StatusData { }; } -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]" }, -]; +/* ─── Chart tier definitions (presentation layer) ─── */ + +const MAX_SUPPLY = 1_000_000; +const CHART_TIERS = [ + { label: "Bronze", emoji: "\uD83E\uDD49", fdv: 1_000_000, poolPct: 10 }, + { label: "Silver", emoji: "\uD83E\uDD48", fdv: 10_000_000, poolPct: 30 }, + { label: "Gold", emoji: "\uD83E\uDD47", fdv: 50_000_000, poolPct: 50 }, + { label: "Diamond", emoji: "\uD83D\uDC8E", fdv: 100_000_000, poolPct: 100 }, +] as const; + +/** Pool USD value at a given FDV milestone: poolAmount * (pct/100) * (fdv / maxSupply) */ +function poolUsdAt(fdv: number, poolPct: number, poolAmount: number): number { + return poolAmount * (poolPct / 100) * (fdv / MAX_SUPPLY); +} + +type ChartMilestone = { + fdv: number; + poolUsd: number; + label: string; + emoji: string; + poolPct: number; +}; + +/* ─── SVG layout constants ─── */ + +const SVG_W = 600; +const SVG_H = 300; +const PAD = { top: 40, right: 20, bottom: 50, left: 65 }; +const CHART_W = SVG_W - PAD.left - PAD.right; +const CHART_H = SVG_H - PAD.top - PAD.bottom; + +// Log scale helpers — FDV axis from 10k to 200M +const LOG_MIN = Math.log10(10_000); +const LOG_MAX = Math.log10(200_000_000); + +function fdvToX(fdv: number): number { + if (fdv <= 0) return PAD.left; + const logVal = Math.log10(Math.max(fdv, 10_000)); + const t = (logVal - LOG_MIN) / (LOG_MAX - LOG_MIN); + return PAD.left + t * CHART_W; +} + +function usdToY(usd: number, yMax: number): number { + const t = usd / yMax; + return PAD.top + CHART_H * (1 - t); +} + +/* ─── Path builders ─── */ + +function buildAreaPath(milestones: ChartMilestone[], yMax: number): string { + const baseline = usdToY(0, yMax); + let path = `M ${fdvToX(10_000)} ${baseline}`; + let prevY = baseline; + for (const m of milestones) { + const x = fdvToX(m.fdv); + const y = usdToY(m.poolUsd, yMax); + path += ` L ${x} ${prevY} L ${x} ${y}`; + prevY = y; + } + path += ` L ${PAD.left + CHART_W} ${prevY}`; + path += ` L ${PAD.left + CHART_W} ${baseline} Z`; + return path; +} + +function buildLinePath(milestones: ChartMilestone[], yMax: number): string { + let path = ""; + let prevY = usdToY(0, yMax); + for (let i = 0; i < milestones.length; i++) { + const m = milestones[i]; + const x = fdvToX(m.fdv); + const y = usdToY(m.poolUsd, yMax); + if (i === 0) { + path = `M ${fdvToX(10_000)} ${prevY} L ${x} ${prevY} L ${x} ${y}`; + } else { + path += ` L ${x} ${prevY} L ${x} ${y}`; + } + prevY = y; + } + path += ` L ${PAD.left + CHART_W} ${prevY}`; + return path; +} + +/* ─── Component ─── */ export function MilestoneTrack() { const { data, isLoading } = useQuery({ @@ -31,6 +110,15 @@ export function MilestoneTrack() { staleTime: 60_000, }); + const milestones: ChartMilestone[] = useMemo( + () => + CHART_TIERS.map((t) => ({ + ...t, + poolUsd: poolUsdAt(t.fdv, t.poolPct, data?.poolAmount ?? 50_000), + })), + [data?.poolAmount], + ); + if (isLoading || !data) { return (
@@ -39,94 +127,286 @@ export function MilestoneTrack() { ); } - // 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); + // Y-axis max with 10% headroom above Diamond + const yMax = milestones[milestones.length - 1].poolUsd * 1.1; + + // Y-axis ticks: evenly space 4 ticks from 0 to Diamond poolUsd + const diamondUsd = milestones[milestones.length - 1].poolUsd; + const yTicks = [0, diamondUsd * 0.2, diamondUsd * 0.5, diamondUsd]; + + // FDV = price * max supply + const currentFdv = + data.latestPriceUsd != null && data.latestPriceUsd > 0 + ? data.latestPriceUsd * MAX_SUPPLY + : 0; + + // Determine current zone + const currentZone = milestones.reduce( + (zone, m, i) => (currentFdv >= m.fdv ? i + 1 : zone), + 0, + ); + const currentZoneLabel = + currentZone === 0 ? "Pre-Bronze" : milestones[currentZone - 1].label; + + const currentPoolUsd = + currentZone > 0 ? milestones[currentZone - 1].poolUsd : 0; + + const dotX = fdvToX(Math.max(currentFdv, 10_000)); + const dotY = usdToY(currentPoolUsd, yMax); + + const areaPath = buildAreaPath(milestones, yMax); + const linePath = buildLinePath(milestones, yMax); return (
-

Milestone Progress

- - {/* Track bar */} -
-
-
-
+

+ FDV Milestone Chart +

+

+ Pool unlock curve across FDV milestones · FDV = PLOT price + × {MAX_SUPPLY.toLocaleString()} max supply +

- {/* Current mcap marker */} -
+ -
-
- {formatUsdValue(data.currentMcap)} -
-
+ + + + + + - {/* Milestone markers */} - {TIERS.map((tier) => { - const milestone = data.milestones[tier.key]; - const position = (milestone.mcap / goldMcap) * 100; - return ( -
-
( + + -
- ); - })} -
+ + {val === 0 + ? "$0" + : val >= 1_000_000 + ? `$${(val / 1_000_000).toFixed(1)}M` + : `$${(val / 1_000).toFixed(0)}K`} + + + ))} + + {/* Filled area */} + + + {/* Step line */} + - {/* Tier details */} -
- {TIERS.map((tier) => { - const milestone = data.milestones[tier.key]; - const poolValue = data.poolAmount * (milestone.pct / 100); - return ( -
{ + const x = fdvToX(m.fdv); + const y = usdToY(m.poolUsd, yMax); + return ( + + + + {m.emoji} {m.label} + + + {m.poolPct}% ($ + {m.poolUsd >= 1_000_000 + ? `${(m.poolUsd / 1_000_000).toFixed(1)}M` + : `${(m.poolUsd / 1_000).toFixed(0)}K`} + ) + + + + ); + })} + + {/* X-axis labels */} + {milestones.map((m) => ( + -
- {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)} -
- )} -
- ); - })} + ${m.fdv >= 1_000_000 ? `${m.fdv / 1_000_000}M` : `${m.fdv / 1_000}K`} + + ))} + + {/* Axis labels */} + + FDV → + + + Pool Value + + + {/* Current FDV indicator — dot on x-axis, dashed line up to area */} + {currentFdv > 0 && ( + + {/* Vertical dashed line from x-axis up to the step level */} + + {/* Small marker at the step intersection */} + + {/* Pulse ring on x-axis */} + + + + + {/* Heartbeat dot on x-axis */} + + + + + {/* FDV label below axis */} + + {formatUsdValue(currentFdv)} + + + Current + + + )} +
- {/* Current position label */} -
- - Current: {formatUsdValue(data.currentMcap)} / {formatUsdValue(goldMcap)} - + {/* Current position summary */} +
+
+ Current FDV: {currentFdv > 0 ? formatUsdValue(currentFdv) : "\u2014"} + · Zone: {currentZoneLabel} +
+ {currentFdv > 0 && currentZone < milestones.length && ( +
+ Next: {milestones[currentZone].emoji}{" "} + {milestones[currentZone].label} at{" "} + {formatUsdValue(milestones[currentZone].fdv)} FDV +
+ )}
);