Skip to content

Commit 42b9cae

Browse files
committed
Add transparency stats to landing page
1 parent b9bb2f0 commit 42b9cae

4 files changed

Lines changed: 841 additions & 0 deletions

File tree

landing/src/app/token/page.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import type {Metadata} from "next";
1212
import {Footer} from "@/components/Footer";
1313
import {Navbar} from "@/components/Navbar";
1414
import {TokenSection} from "@/components/TokenSection";
15+
import {TokenStatsSection} from "@/components/TokenStats";
1516
import {
1617
TOKEN_PROOFS,
1718
TOKEN_SOLSCAN_URL,
@@ -30,6 +31,10 @@ export const metadata: Metadata = {
3031
},
3132
};
3233

34+
// Re-fetch live on-chain stats at most every 10 minutes (matches the server
35+
// cache in token-stats.ts). Keeps the page static-fast while staying fresh.
36+
export const revalidate = 600;
37+
3338
const USE_OF_FUNDS = [
3439
{
3540
icon: Rocket,
@@ -77,6 +82,9 @@ export default function TokenPage() {
7782
<main className="pt-16">
7883
<TokenSection />
7984

85+
{/* ── Live on-chain stats ──────────────────────────────────── */}
86+
<TokenStatsSection />
87+
8088
{/* ── Why a token ──────────────────────────────────────────── */}
8189
<section className="border-t border-border py-20">
8290
<div className="mx-auto max-w-3xl px-6">
Lines changed: 310 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,310 @@
1+
import {ArrowUpRight, Coins, Flame, Lock, Users, Wallet} from "lucide-react";
2+
import {
3+
TOKEN_CONTRACT_ADDRESS,
4+
TOKEN_CREATOR_ADDRESS,
5+
TOKEN_SOLSCAN_URL,
6+
TOKEN_TICKER,
7+
} from "@/lib/constants";
8+
import {getTokenStats, type TokenStats} from "@/lib/token-stats";
9+
10+
// ── formatters ───────────────────────────────────────────────────────────────
11+
function compact(n: number | null): string {
12+
if (n == null) return "—";
13+
const abs = Math.abs(n);
14+
if (abs >= 1_000_000_000) return `${(n / 1_000_000_000).toFixed(2)}B`;
15+
if (abs >= 1_000_000) return `${(n / 1_000_000).toFixed(2)}M`;
16+
if (abs >= 1_000) return `${(n / 1_000).toFixed(1)}K`;
17+
return n.toLocaleString("en-US", {maximumFractionDigits: 0});
18+
}
19+
20+
function pct(n: number | null): string {
21+
if (n == null) return "—";
22+
if (n > 0 && n < 0.01) return "<0.01%";
23+
return `${n.toFixed(2)}%`;
24+
}
25+
26+
function usdPrice(n: number | null): string {
27+
if (n == null) return "—";
28+
if (n < 0.000001) return `$${n.toExponential(2)}`;
29+
if (n < 1) return `$${n.toPrecision(3)}`;
30+
return `$${n.toLocaleString("en-US", {maximumFractionDigits: 2})}`;
31+
}
32+
33+
function usdBig(n: number | null): string {
34+
if (n == null) return "—";
35+
if (n >= 1_000_000) return `$${(n / 1_000_000).toFixed(2)}M`;
36+
if (n >= 1_000) return `$${(n / 1_000).toFixed(1)}K`;
37+
return `$${n.toLocaleString("en-US", {maximumFractionDigits: 0})}`;
38+
}
39+
40+
function sol(n: number | null): string {
41+
if (n == null) return "—";
42+
return `${n.toLocaleString("en-US", {maximumFractionDigits: 2})} SOL`;
43+
}
44+
45+
function shortAddr(a: string): string {
46+
return a.length > 12 ? `${a.slice(0, 4)}${a.slice(-4)}` : a;
47+
}
48+
49+
function solscanAccount(a: string): string {
50+
return `https://solscan.io/account/${a}`;
51+
}
52+
53+
function timeAgo(ts: number): string {
54+
const secs = Math.max(0, Math.round((Date.now() - ts) / 1000));
55+
if (secs < 60) return "just now";
56+
const mins = Math.round(secs / 60);
57+
if (mins < 60) return `${mins}m ago`;
58+
const hrs = Math.round(mins / 60);
59+
return `${hrs}h ago`;
60+
}
61+
62+
export async function TokenStatsSection() {
63+
const stats = await getTokenStats();
64+
return <TokenStatsView stats={stats} />;
65+
}
66+
67+
// Hidden for now — flip to true to bring the "fees earned for development"
68+
// card back. The data is still fetched; it's just not rendered.
69+
const SHOW_CREATOR_REWARDS = false;
70+
71+
function TokenStatsView({stats}: {stats: TokenStats}) {
72+
const cards = [
73+
{
74+
icon: Flame,
75+
label: "Burned",
76+
value: compact(stats.burned),
77+
sub: stats.burnedPct != null ? `${pct(stats.burnedPct)} of initial supply` : "Removed from supply forever",
78+
},
79+
{
80+
icon: Lock,
81+
label: "Locked",
82+
value: stats.locked != null ? compact(stats.locked) : "Not configured",
83+
sub: stats.lockedPct != null ? `${pct(stats.lockedPct)} of supply` : "Liquidity & vesting locks",
84+
},
85+
{
86+
icon: Wallet,
87+
label: "Dev / treasury",
88+
value: stats.devBalance != null ? compact(stats.devBalance) : "Not configured",
89+
sub: stats.devPct != null ? `${pct(stats.devPct)} of supply` : "Team-held tokens",
90+
},
91+
{
92+
icon: Users,
93+
label: "Holders",
94+
value: stats.holders != null ? `${stats.holdersCapped ? "" : ""}${stats.holders.toLocaleString("en-US")}` : "—",
95+
sub: stats.holdersCapped ? "counted (capped)" : "unique wallets",
96+
},
97+
];
98+
99+
return (
100+
<section className="border-t border-border py-20">
101+
<div className="mx-auto max-w-5xl px-6">
102+
{/* Header */}
103+
<div className="text-center mb-12">
104+
<div className="text-[11px] font-semibold uppercase tracking-[0.22em] text-accent mb-4">
105+
Live on-chain stats
106+
</div>
107+
<h2 className="text-3xl md:text-4xl font-semibold tracking-tight text-foreground">
108+
Every number, straight from the chain.
109+
</h2>
110+
<p className="text-muted-foreground max-w-2xl mx-auto mt-4">
111+
Supply, holders, burns, locks and team holdings for {TOKEN_TICKER},
112+
read live from Solana. Nothing here is hand-entered — cross-check any
113+
figure on Solscan.
114+
</p>
115+
</div>
116+
117+
{/* Supply + market headline */}
118+
<div className="grid gap-4 sm:grid-cols-3 mb-4">
119+
<HeadlineStat
120+
label="Circulating supply"
121+
value={compact(stats.circulating)}
122+
sub={
123+
stats.totalSupply != null
124+
? `of ${compact(stats.totalSupply)} total`
125+
: undefined
126+
}
127+
/>
128+
<HeadlineStat label="Price" value={usdPrice(stats.priceUsd)} sub="via Jupiter" />
129+
<HeadlineStat label="Market cap" value={usdBig(stats.marketCapUsd)} sub="price × supply" />
130+
</div>
131+
132+
{/* Stat cards */}
133+
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
134+
{cards.map((c) => {
135+
const Icon = c.icon;
136+
return (
137+
<div
138+
key={c.label}
139+
className="rounded-xl border border-border bg-card/40 backdrop-blur-sm p-6"
140+
>
141+
<Icon className="h-5 w-5 text-accent mb-3" />
142+
<div className="text-[11px] font-medium uppercase tracking-wider text-muted-foreground mb-1">
143+
{c.label}
144+
</div>
145+
<div className="text-2xl font-semibold tracking-tight text-foreground tabular-nums">
146+
{c.value}
147+
</div>
148+
<div className="text-xs text-muted-foreground mt-1">{c.sub}</div>
149+
</div>
150+
);
151+
})}
152+
</div>
153+
154+
{/* Creator rewards — pump.fun creator fees, the funding story */}
155+
{SHOW_CREATOR_REWARDS && stats.creatorRewardsSol != null && (
156+
<div className="mt-4 rounded-2xl border border-accent/30 bg-gradient-to-b from-accent/[0.08] to-transparent p-8 text-center">
157+
<div className="mx-auto mb-3 flex h-10 w-10 items-center justify-center rounded-full border border-accent/30 bg-accent/10">
158+
<Coins className="h-5 w-5 text-accent" />
159+
</div>
160+
<div className="text-[11px] font-semibold uppercase tracking-[0.2em] text-muted-foreground">
161+
Fees earned for development
162+
</div>
163+
<div className="mt-2 text-4xl font-semibold tracking-tight text-foreground tabular-nums">
164+
{sol(stats.creatorRewardsSol)}
165+
</div>
166+
{stats.creatorRewardsUsd != null && (
167+
<div className="mt-1 text-sm text-muted-foreground tabular-nums">
168+
{usdBig(stats.creatorRewardsUsd)}
169+
</div>
170+
)}
171+
<p className="mx-auto mt-4 max-w-md text-sm leading-relaxed text-muted-foreground">
172+
Lifetime {TOKEN_TICKER} trading fees — the funding that pays for
173+
full-time work on Voicebox.{" "}
174+
<a
175+
href={`https://solscan.io/account/${TOKEN_CREATOR_ADDRESS}`}
176+
target="_blank"
177+
rel="noopener noreferrer"
178+
className="inline-flex items-center gap-0.5 text-foreground/80 hover:text-foreground"
179+
>
180+
Verify <ArrowUpRight className="h-3 w-3" />
181+
</a>
182+
</p>
183+
</div>
184+
)}
185+
186+
{/* Locked breakdown (only if any configured) */}
187+
{stats.lockedBreakdown.length > 0 && (
188+
<div className="mt-4 rounded-xl border border-border bg-card/40 backdrop-blur-sm p-6">
189+
<div className="text-[11px] font-medium uppercase tracking-wider text-muted-foreground mb-4">
190+
Locked &amp; vesting
191+
</div>
192+
<ul className="space-y-3">
193+
{stats.lockedBreakdown.map((l) => (
194+
<li
195+
key={l.account}
196+
className="flex items-center gap-3 text-sm"
197+
>
198+
<Lock className="h-4 w-4 shrink-0 text-accent" />
199+
<span className="text-foreground/90">{l.label}</span>
200+
{l.unlocksAt && (
201+
<span className="text-xs text-muted-foreground">· {l.unlocksAt}</span>
202+
)}
203+
<span className="ml-auto font-medium tabular-nums text-foreground">
204+
{compact(l.amount)}
205+
</span>
206+
<a
207+
href={l.url ?? solscanAccount(l.account)}
208+
target="_blank"
209+
rel="noopener noreferrer"
210+
className="text-muted-foreground hover:text-foreground"
211+
aria-label="View on Solscan"
212+
>
213+
<ArrowUpRight className="h-4 w-4" />
214+
</a>
215+
</li>
216+
))}
217+
</ul>
218+
</div>
219+
)}
220+
221+
{/* Top holders */}
222+
{stats.topHolders.length > 0 && (
223+
<div className="mt-4 rounded-xl border border-border bg-card/40 backdrop-blur-sm p-6">
224+
<div className="flex items-center justify-between mb-4">
225+
<div className="text-[11px] font-medium uppercase tracking-wider text-muted-foreground">
226+
Top holders
227+
</div>
228+
<a
229+
href={`${TOKEN_SOLSCAN_URL}#holders`}
230+
target="_blank"
231+
rel="noopener noreferrer"
232+
className="inline-flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground"
233+
>
234+
All holders <ArrowUpRight className="h-3 w-3" />
235+
</a>
236+
</div>
237+
<ul className="divide-y divide-border/60">
238+
{stats.topHolders.map((h, i) => (
239+
<li
240+
key={h.owner}
241+
className="flex items-center gap-3 py-2.5 text-sm"
242+
>
243+
<span className="w-5 text-xs text-muted-foreground tabular-nums">
244+
{i + 1}
245+
</span>
246+
<a
247+
href={solscanAccount(h.owner)}
248+
target="_blank"
249+
rel="noopener noreferrer"
250+
className="font-mono text-foreground/90 hover:text-foreground hover:underline"
251+
>
252+
{shortAddr(h.owner)}
253+
</a>
254+
<span className="ml-auto tabular-nums text-foreground">
255+
{compact(h.amount)}
256+
</span>
257+
<span className="w-16 text-right tabular-nums text-muted-foreground">
258+
{pct(h.pct)}
259+
</span>
260+
</li>
261+
))}
262+
</ul>
263+
</div>
264+
)}
265+
266+
{/* Footer: provenance + freshness */}
267+
<div className="mt-6 flex flex-col sm:flex-row items-center justify-between gap-3 text-xs text-muted-foreground">
268+
<span>
269+
{stats.live ? (
270+
<>Updated {timeAgo(stats.updatedAt)} · data via Helius &amp; Jupiter</>
271+
) : (
272+
<>Live stats unavailable right now — verify on Solscan.</>
273+
)}
274+
</span>
275+
<a
276+
href={TOKEN_SOLSCAN_URL}
277+
target="_blank"
278+
rel="noopener noreferrer"
279+
className="inline-flex items-center gap-1.5 hover:text-foreground transition-colors"
280+
>
281+
<span className="font-mono">{shortAddr(TOKEN_CONTRACT_ADDRESS)}</span>
282+
Inspect on Solscan <ArrowUpRight className="h-3.5 w-3.5" />
283+
</a>
284+
</div>
285+
</div>
286+
</section>
287+
);
288+
}
289+
290+
function HeadlineStat({
291+
label,
292+
value,
293+
sub,
294+
}: {
295+
label: string;
296+
value: string;
297+
sub?: string;
298+
}) {
299+
return (
300+
<div className="rounded-2xl border border-border bg-card/60 backdrop-blur-sm p-6 text-center">
301+
<div className="text-[11px] font-medium uppercase tracking-wider text-muted-foreground mb-2">
302+
{label}
303+
</div>
304+
<div className="text-3xl font-semibold tracking-tight text-foreground tabular-nums">
305+
{value}
306+
</div>
307+
{sub && <div className="text-xs text-muted-foreground mt-1">{sub}</div>}
308+
</div>
309+
);
310+
}

0 commit comments

Comments
 (0)