Skip to content

Commit 70805cb

Browse files
Vasanthdev2004kevincodex1
authored andcommitted
ui: token page hero, trade panel and details
A hero that wears the token's card cover, the trade panel with price impact, proof facts, the about card, and the loading frame the market row morphs into.
1 parent 8ad79ed commit 70805cb

19 files changed

Lines changed: 841 additions & 170 deletions

‎app/src/app/t/[chain]/[token]/loading.tsx‎

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
import { Sk } from "@/components/Skeleton";
22
import PendingTokenHeader from "@/components/launchpad/PendingTokenHeader";
33

4-
/* Same frame as page.tsx (main padding, breadcrumb row, header): the token header that the market-row
4+
/* Same frame as page.tsx (main padding, the hero with its cover): the token header that the market-row
55
morph lands on must sit exactly where the real one renders, or the second hop would jump. */
66
export default function Loading() {
7-
return <main className="mx-auto max-w-6xl px-4 pt-5 pb-28 sm:pt-7 lg:pb-16" aria-busy="true" aria-label="Loading token market">
8-
<div className="mb-5 flex min-h-8 items-center"><Sk className="h-4 w-24" /></div>
7+
return <main className="bb-mid bb-page pt-5 pb-28 sm:pt-7 lg:pb-16" aria-busy="true" aria-label="Loading token market">
98
<PendingTokenHeader />
10-
<div className="grid items-start gap-5 lg:grid-cols-[minmax(0,1fr)_21rem] lg:gap-6">
9+
<div className="grid items-start gap-5 lg:grid-cols-[minmax(0,1fr)_23.75rem] lg:gap-6">
1110
<div className="min-w-0 space-y-4">
12-
<div className="overflow-hidden rounded-2xl border border-line p-5"><Sk className="h-3 w-24" /><Sk className="mt-3 h-10 w-44" /><Sk className="mt-6 h-10 w-full" /><Sk className="mt-3 h-72 w-full sm:h-[360px] xl:h-[390px]" /></div>
11+
<div className="overflow-hidden rounded-2xl border border-line p-5"><Sk className="h-3 w-24" /><Sk className="mt-3 h-10 w-44" /><Sk className="mt-6 h-10 w-full" /><Sk className="mt-3 h-72 w-full sm:h-[360px] xl:h-[380px]" /></div>
1312
<div className="grid grid-cols-2 gap-4 rounded-xl border border-line bg-card p-4 sm:grid-cols-4">{Array.from({ length: 4 }, (_, i) => <div key={i} className="space-y-2"><Sk className="h-3 w-16" /><Sk className="h-5 w-full" /></div>)}</div>
1413
</div>
1514
<aside className="space-y-4">

‎app/src/app/t/[chain]/[token]/page.tsx‎

Lines changed: 122 additions & 37 deletions
Large diffs are not rendered by default.

‎app/src/components/launchpad/CollectPanel.tsx‎

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33
import { useState } from "react";
44
import { useRouter } from "next/navigation";
5-
import { useAccount, useConfig, useReadContracts } from "wagmi";
5+
import { useConfig, useReadContracts } from "wagmi";
6+
import { useHydratedAccount } from "@/lib/useHydratedAccount";
67
import { getPublicClient, getWalletClient } from "wagmi/actions";
78
import { type Address, type Hex } from "viem";
89
import { btn } from "@/components/ui";
@@ -54,7 +55,7 @@ export default function CollectPanel({
5455
}) {
5556
const router = useRouter();
5657
const config = useConfig();
57-
const { address, chainId } = useAccount();
58+
const { address, chainId } = useHydratedAccount();
5859
const CHAIN = CHAINS[chain];
5960
const LOCKER_ADDRESS = launchpad(chain).locker;
6061
const quoteUsd = quoteUsdOf(quote, ethUsd);
@@ -119,7 +120,7 @@ export default function CollectPanel({
119120
].filter((c) => c.raw > 0n);
120121

121122
return (
122-
<section className="rounded-2xl bg-paper border border-line p-5 space-y-3">
123+
<section className="rounded-2xl border border-line bg-card p-5 space-y-3">
123124
<div className="flex items-baseline justify-between gap-3">
124125
<h2 className="text-sm font-semibold text-ink">Where the fees go</h2>
125126
<span className="font-mono font-bold text-ink tnum">{pipsToPct(lpFee)}</span>
@@ -140,7 +141,7 @@ export default function CollectPanel({
140141
{burn ? (
141142
<span className="text-warm-ink font-medium">Burned</span>
142143
) : (
143-
<a href={explorerAddress(chain, r.payout)} target="_blank" rel="noreferrer" className="font-mono text-ink hover:underline underline-offset-2 truncate">
144+
<a href={explorerAddress(chain, r.payout)} target="_blank" rel="noreferrer" className="font-code text-ink hover:underline underline-offset-2 truncate">
144145
{address && r.payout.toLowerCase() === address.toLowerCase() ? "You" : shortAddr(r.payout)}
145146
</a>
146147
)}
@@ -163,8 +164,9 @@ export default function CollectPanel({
163164
<dd className="text-[11px] text-body leading-relaxed">Both sides sent to the dead address. Nobody can claim them.</dd>
164165
) : (
165166
<>
166-
<dd className="font-mono font-bold text-sm tnum text-warm-ink break-words">{fq(burned.quote)}</dd>
167-
<dd className="font-mono font-bold text-sm tnum text-warm-ink break-words">{ft(burned.token)}</dd>
167+
{/* amber only once something has actually burned; zero reads as the plain fact it is */}
168+
<dd className={`font-mono font-bold text-sm tnum break-words ${burned.quote > 0n ? "text-warm-ink" : "text-muted"}`}>{fq(burned.quote)}</dd>
169+
<dd className={`font-mono font-bold text-sm tnum break-words ${burned.token > 0n ? "text-warm-ink" : "text-muted"}`}>{ft(burned.token)}</dd>
168170
</>
169171
)}
170172
</div>
@@ -199,7 +201,7 @@ export default function CollectPanel({
199201
{phase.k === "sent" ? (
200202
<p className="text-[11px] text-muted">
201203
tx{" "}
202-
<a href={explorerTx(chain, phase.hash)} target="_blank" rel="noreferrer" className="font-mono underline underline-offset-2">
204+
<a href={explorerTx(chain, phase.hash)} target="_blank" rel="noreferrer" className="font-code underline underline-offset-2">
203205
{phase.hash.slice(0, 10)}…
204206
</a>
205207
</p>
Lines changed: 28 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,36 @@
11
"use client";
22

33
import { useState } from "react";
4+
import { Check, Copy } from "lucide-react";
45

5-
export default function CopyChip({ value, label, className = "" }: { value: string; label?: string; className?: string }) {
6+
/** The chip's own look. A `className` replaces it entirely, so a toolbar can draw the button as one of its segments. */
7+
const CHIP = "inline-flex h-7 items-center gap-1.5 rounded-full border border-line bg-card px-2.5 font-code text-[11px] text-body hover:border-line-strong hover:text-ink";
8+
9+
export default function CopyChip({ value, label, className = CHIP }: { value: string; label?: string; className?: string }) {
610
const [done, setDone] = useState(false);
11+
const shown = label ?? `${value.slice(0, 6)}…${value.slice(-4)}`;
712
return (
8-
<button
9-
type="button"
10-
onClick={async () => {
11-
try {
12-
await navigator.clipboard.writeText(value);
13-
setDone(true);
14-
setTimeout(() => setDone(false), 1200);
15-
} catch {
16-
/* clipboard blocked */
17-
}
18-
}}
19-
className={`inline-flex items-center gap-1.5 h-7 px-2.5 rounded-full border border-line bg-card text-[11px] font-mono text-body hover:text-ink hover:border-line-strong ${className}`}
20-
title={value}
21-
>
22-
{label ?? `${value.slice(0, 6)}…${value.slice(-4)}`}
23-
<span className="text-faint">{done ? "✓" : "⧉"}</span>
24-
</button>
13+
<>
14+
<button
15+
type="button"
16+
onClick={async () => {
17+
try {
18+
await navigator.clipboard.writeText(value);
19+
setDone(true);
20+
setTimeout(() => setDone(false), 1200);
21+
} catch {
22+
/* clipboard blocked */
23+
}
24+
}}
25+
className={className}
26+
title={value}
27+
aria-label={`Copy ${shown}`}
28+
>
29+
{shown}
30+
{done ? <Check size={13} aria-hidden="true" className="shrink-0 text-up" /> : <Copy size={13} aria-hidden="true" className="shrink-0 text-faint" />}
31+
</button>
32+
{/* outside the button: a button's children are presentational, so a live region inside it may never be read */}
33+
<span role="status" className="sr-only">{done ? "Copied" : ""}</span>
34+
</>
2535
);
2636
}

‎app/src/components/launchpad/HoldersPanel.tsx‎

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,24 +16,27 @@ const TAG_STYLE: Record<string, string> = {
1616
sniper: "border-warm/40 bg-warm-soft text-warm-ink",
1717
whale: "border-line-strong bg-card text-ink",
1818
};
19+
/** Tags read as words in sentence case, not shouted labels. */
20+
const TAG_LABEL: Record<string, string> = { creator: "Creator", pool: "Pool", burn: "Burn", sniper: "Sniper", whale: "Whale" };
1921
const NOTE_STYLE = { warn: "border-warm/40 bg-warm-soft text-warm-ink", info: "border-line bg-card text-body", good: "border-holder-good-line bg-holder-good-bg text-up" } as const;
2022

2123
export default function HoldersPanel({ chain, symbol, p, embedded = false }: { chain: ChainKey; symbol: string; p: HolderPanel | null; embedded?: boolean }) {
2224
if (!p || !holderFactsAvailable(p)) return <section aria-label="holders" className={embedded ? "p-5" : "rounded-2xl border border-line bg-paper p-5"}><div className="flex items-center justify-between gap-3"><h2 className="text-sm font-semibold text-ink">Supply distribution</h2><span className="text-[11px] text-muted">Awaiting transfer history</span></div><div className="flex min-h-44 flex-col items-center justify-center gap-2 text-center"><UsersRound size={24} strokeWidth={1.4} className="mb-1 text-muted" /><h3 className="text-sm font-medium text-ink">The holder picture is not ready yet.</h3><p className="max-w-sm text-xs leading-relaxed text-muted text-pretty">Concentration, creator holdings and early buyers appear after transfer history is indexed. Missing data is not a clean bill of health.</p></div></section>;
2325
const supply = Number(BigInt(p.supply)) / 1e18;
2426
return (
25-
<section className={embedded ? "overflow-hidden bg-paper" : "overflow-hidden rounded-2xl border border-line bg-paper"} aria-label="holders">
26-
<div className="px-4 pt-4 pb-3 flex items-baseline justify-between gap-3 flex-wrap">
27-
<h2 className="font-semibold text-ink">Holders</h2>
28-
<span className="text-xs text-muted">{p.synced ? "from every transfer of the token, live" : "indexing transfer history…"}</span>
27+
<section className={embedded ? "overflow-hidden" : "overflow-hidden rounded-2xl border border-line bg-card"} aria-label="holders">
28+
<div className="px-5 pt-4 pb-3 flex items-baseline justify-between gap-3 flex-wrap">
29+
{/* inside the Holders tab the tab already names the panel; the heading stays for the document outline */}
30+
<h2 className={embedded ? "sr-only" : "text-sm font-semibold text-ink"}>Holders</h2>
31+
<span className="text-[11px] text-muted">{p.synced ? "From every transfer of the token, live" : "Indexing transfer history…"}</span>
2932
</div>
30-
<dl className="px-4 grid grid-cols-2 sm:grid-cols-4 gap-2.5">
33+
<dl className="px-5 grid grid-cols-2 sm:grid-cols-4 gap-2.5">
3134
<Stat k="Holders" v={`${p.holders}`} />
3235
<Stat k="Top 10 hold" v={fmtShare(p.top10Bps)} warn={p.top10Bps >= 5000} />
3336
<Stat k="Creator holds" v={fmtShare(p.creator.bps)} sub={p.creator.sells > 0 ? `sold ${p.creator.sells}×` : "never sold"} warn={p.creator.bps >= 2000 || p.creator.sells > 0} />
3437
<Stat k="Sniped at launch" v={fmtShare(p.sniper.bps)} sub={`${p.sniper.wallets} wallet${p.sniper.wallets === 1 ? "" : "s"}`} warn={p.sniper.bps >= 1000} />
3538
</dl>
36-
<ul className="px-4 pt-3 flex flex-wrap gap-1.5">
39+
<ul className="px-5 pt-3 flex flex-wrap gap-1.5">
3740
{p.notes.filter((n) => !n.text.includes("no red flags")).map((n) => (
3841
<li key={n.text} className={`inline-flex items-center h-6 px-2 rounded-full border text-[11px] font-medium ${NOTE_STYLE[n.level]}`}>
3942
{n.text}
@@ -43,21 +46,21 @@ export default function HoldersPanel({ chain, symbol, p, embedded = false }: { c
4346
{p.top.length > 0 ? (
4447
<ol className="mt-3 border-t border-line divide-y divide-line">
4548
{p.top.map((h, i) => (
46-
<li key={h.address} className="px-4 py-2 flex items-center gap-3 text-sm">
49+
<li key={h.address} className="px-5 py-2.5 flex items-center gap-3 text-sm transition-colors hover:bg-ink/[0.03] motion-reduce:transition-none">
4750
<span className="w-5 text-right font-mono text-xs text-muted tnum">{i + 1}</span>
48-
<a href={explorerAddress(chain, h.address)} target="_blank" rel="noreferrer" className="font-mono text-xs text-body hover:text-ink" title={h.address}>
51+
<a href={explorerAddress(chain, h.address)} target="_blank" rel="noreferrer" className="font-code text-xs text-body hover:text-ink" title={h.address}>
4952
{shortAddr(h.address)}
5053
</a>
5154
<span className="flex gap-1">
5255
{h.tags.map((t) => (
53-
<span key={t} className={`inline-flex items-center h-5 px-1.5 rounded-full border text-[10px] font-semibold uppercase tracking-wide ${TAG_STYLE[t]}`}>
54-
{t}
56+
<span key={t} className={`inline-flex items-center h-5 px-1.5 rounded-md border text-[10px] font-medium ${TAG_STYLE[t]}`}>
57+
{TAG_LABEL[t] ?? t}
5558
</span>
5659
))}
5760
</span>
5861
<span className="ml-auto flex items-center gap-2 min-w-0">
59-
<span className="hidden sm:block h-1.5 w-24 rounded-full bg-paper overflow-hidden" aria-hidden>
60-
<span className="block h-full bg-ink" style={{ width: `${Math.min(100, h.bps / 100)}%` }} />
62+
<span className="hidden sm:block h-1.5 w-24 rounded-full bg-line overflow-hidden" aria-hidden>
63+
<span className="block h-full rounded-full bg-[color:var(--tok,var(--color-ink))]" style={{ width: `${Math.min(100, h.bps / 100)}%` }} />
6164
</span>
6265
<span className="font-mono tnum text-xs text-muted hidden md:inline">{fmtCompact(Number(BigInt(h.balance)) / 1e18, 1)}</span>
6366
<span className="font-mono tnum font-semibold text-ink w-14 text-right">{fmtShare(h.bps)}</span>
@@ -66,9 +69,9 @@ export default function HoldersPanel({ chain, symbol, p, embedded = false }: { c
6669
))}
6770
</ol>
6871
) : (
69-
<p className="px-4 pb-4 pt-3 text-sm text-muted">{p.synced ? `Nobody holds ${symbol} outside the pool yet.` : "Holder list appears once the transfer history is indexed."}</p>
72+
<p className="px-5 pb-4 pt-3 text-sm text-muted">{p.synced ? `Nobody holds ${symbol} outside the pool yet.` : "Holder list appears once the transfer history is indexed."}</p>
7073
)}
71-
<p className="px-4 py-3 text-[11px] text-muted border-t border-line">
74+
<p className="px-5 py-3 text-[11px] text-muted border-t border-line">
7275
{fmtShare(p.poolBps)} of the {fmtCompact(supply, 0)} supply is in the locked Uniswap v4 pool. Pool, locker and burn addresses are never counted as holders.
7376
</p>
7477
</section>
Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,29 @@
1-
import { ArrowDown, ArrowUpRight, LockKeyhole } from "lucide-react";
1+
import { ArrowUpRight } from "lucide-react";
22
import { launchpad } from "@/lib/launchpad/config";
33
import { explorerAddress, explorerTx, type ChainKey } from "@/lib/chainPublic";
44

5-
/** A receipt of the launch mechanics, not a claim about current wallet balances. */
5+
/** A receipt of the launch mechanics, not a claim about current wallet balances. Set like one: dotted leaders, mono figures. */
66
export default function LaunchReceipt({ chain, symbol, supply, txHash }: { chain: ChainKey; symbol: string; supply: string; txHash: string }) {
77
const locker = launchpad(chain).locker;
8-
return <section className="overflow-hidden rounded-2xl border border-line bg-paper" aria-labelledby="receipt-title">
9-
<div className="flex min-h-12 items-center justify-between border-b border-line px-5"><h2 id="receipt-title" className="text-sm font-semibold text-ink">The launch receipt</h2><a href={explorerTx(chain, txHash)} target="_blank" rel="noreferrer" className="flex size-9 items-center justify-center text-muted hover:text-ink" aria-label="Verify the launch transaction"><ArrowUpRight size={16} /></a></div>
10-
<div className="px-5 py-4">
11-
<div className="flex items-baseline justify-between gap-3"><span className="text-xs text-muted">Fixed supply</span><span className="truncate font-mono text-sm text-ink tnum" title={`${supply} ${symbol}`}>{supply} {symbol}</span></div>
12-
<div className="flex items-center gap-3 py-2 text-[10px] text-muted"><ArrowDown size={13} className="ml-1" /><span>100% deposited at launch</span></div>
13-
<a href={locker ? explorerAddress(chain, locker) : "/rules#contracts"} target={locker ? "_blank" : undefined} rel={locker ? "noreferrer" : undefined} className="flex items-center justify-between gap-3 rounded-xl border border-line-strong px-3 py-3 hover:bg-card">
14-
<div><span className="flex items-center gap-2 text-xs font-medium text-ink"><LockKeyhole size={13} /> Liquidity position</span><span className="mt-1 block text-[11px] text-muted">Ownerless locker · read the code ↗</span></div><span className="font-mono text-xs font-bold text-up">Forever</span>
15-
</a>
16-
<div className="mt-4 flex items-center justify-between border-t border-dashed border-line-strong pt-4"><span className="text-xs text-muted line-through decoration-faint">Platform fee</span><span className="font-mono text-xl font-bold text-up tnum">$0</span></div>
17-
<p className="mt-1 text-[11px] leading-relaxed text-muted text-pretty">No withdrawal key. No platform cut. This does not prevent token prices from falling.</p>
8+
return <section className="overflow-hidden rounded-2xl border border-line bg-card" aria-labelledby="receipt-title">
9+
<div className="flex items-center justify-between gap-3 px-5 pt-4">
10+
<h2 id="receipt-title" className="text-sm font-semibold text-ink">The launch receipt</h2>
11+
<a href={explorerTx(chain, txHash)} target="_blank" rel="noreferrer" className="inline-flex min-h-8 items-center gap-1 text-xs text-muted transition-colors hover:text-ink motion-reduce:transition-none" aria-label="Verify the launch transaction">Launch tx<ArrowUpRight size={12} aria-hidden="true" /></a>
1812
</div>
13+
<dl className="px-5 pb-4 pt-2 text-xs">
14+
<Line k="Fixed supply" v={<span title={`${supply} ${symbol}`}>{supply} {symbol}</span>} />
15+
<Line k="Into the pool at launch" v="100%" />
16+
<Line k="Liquidity" v={<a href={locker ? explorerAddress(chain, locker) : "/rules#contracts"} target={locker ? "_blank" : undefined} rel={locker ? "noreferrer" : undefined} title="An ownerless locker holds the position. Read the code." className="inline-flex items-center gap-1 text-up underline decoration-up/30 underline-offset-4 transition-colors hover:decoration-up motion-reduce:transition-none">Locked forever<ArrowUpRight size={12} aria-hidden="true" /></a>} />
17+
<Line k="Platform fee" v={<span className="text-up">$0</span>} />
18+
</dl>
19+
<p className="border-t border-dashed border-line-strong px-5 py-3 text-[11px] leading-relaxed text-muted text-pretty">No withdrawal key. No platform cut. This does not prevent token prices from falling.</p>
1920
</section>;
2021
}
22+
23+
function Line({ k, v }: { k: string; v: React.ReactNode }) {
24+
return <div className="flex items-baseline gap-2 py-1.5">
25+
<dt className="shrink-0 text-muted">{k}</dt>
26+
<span aria-hidden="true" className="min-w-4 flex-1 translate-y-[-3px] border-b border-dotted border-line-strong" />
27+
<dd className="min-w-0 truncate font-mono font-semibold text-ink tnum">{v}</dd>
28+
</div>;
29+
}

0 commit comments

Comments
 (0)