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
9 changes: 4 additions & 5 deletions app/src/app/t/[chain]/[token]/loading.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import { Sk } from "@/components/Skeleton";
import PendingTokenHeader from "@/components/launchpad/PendingTokenHeader";

/* Same frame as page.tsx (main padding, breadcrumb row, header): the token header that the market-row
/* Same frame as page.tsx (main padding, the hero with its cover): the token header that the market-row
morph lands on must sit exactly where the real one renders, or the second hop would jump. */
export default function Loading() {
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">
<div className="mb-5 flex min-h-8 items-center"><Sk className="h-4 w-24" /></div>
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">
<PendingTokenHeader />
<div className="grid items-start gap-5 lg:grid-cols-[minmax(0,1fr)_21rem] lg:gap-6">
<div className="grid items-start gap-5 lg:grid-cols-[minmax(0,1fr)_23.75rem] lg:gap-6">
<div className="min-w-0 space-y-4">
<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>
<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>
<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>
</div>
<aside className="space-y-4">
Expand Down
159 changes: 122 additions & 37 deletions app/src/app/t/[chain]/[token]/page.tsx

Large diffs are not rendered by default.

16 changes: 9 additions & 7 deletions app/src/components/launchpad/CollectPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

import { useState } from "react";
import { useRouter } from "next/navigation";
import { useAccount, useConfig, useReadContracts } from "wagmi";
import { useConfig, useReadContracts } from "wagmi";
import { useHydratedAccount } from "@/lib/useHydratedAccount";
import { getPublicClient, getWalletClient } from "wagmi/actions";
import { type Address, type Hex } from "viem";
import { btn } from "@/components/ui";
Expand Down Expand Up @@ -54,7 +55,7 @@ export default function CollectPanel({
}) {
const router = useRouter();
const config = useConfig();
const { address, chainId } = useAccount();
const { address, chainId } = useHydratedAccount();
const CHAIN = CHAINS[chain];
const LOCKER_ADDRESS = launchpad(chain).locker;
const quoteUsd = quoteUsdOf(quote, ethUsd);
Expand Down Expand Up @@ -119,7 +120,7 @@ export default function CollectPanel({
].filter((c) => c.raw > 0n);

return (
<section className="rounded-2xl bg-paper border border-line p-5 space-y-3">
<section className="rounded-2xl border border-line bg-card p-5 space-y-3">
<div className="flex items-baseline justify-between gap-3">
<h2 className="text-sm font-semibold text-ink">Where the fees go</h2>
<span className="font-mono font-bold text-ink tnum">{pipsToPct(lpFee)}</span>
Expand All @@ -140,7 +141,7 @@ export default function CollectPanel({
{burn ? (
<span className="text-warm-ink font-medium">Burned</span>
) : (
<a href={explorerAddress(chain, r.payout)} target="_blank" rel="noreferrer" className="font-mono text-ink hover:underline underline-offset-2 truncate">
<a href={explorerAddress(chain, r.payout)} target="_blank" rel="noreferrer" className="font-code text-ink hover:underline underline-offset-2 truncate">
{address && r.payout.toLowerCase() === address.toLowerCase() ? "You" : shortAddr(r.payout)}
</a>
)}
Expand All @@ -163,8 +164,9 @@ export default function CollectPanel({
<dd className="text-[11px] text-body leading-relaxed">Both sides sent to the dead address. Nobody can claim them.</dd>
) : (
<>
<dd className="font-mono font-bold text-sm tnum text-warm-ink break-words">{fq(burned.quote)}</dd>
<dd className="font-mono font-bold text-sm tnum text-warm-ink break-words">{ft(burned.token)}</dd>
{/* amber only once something has actually burned; zero reads as the plain fact it is */}
<dd className={`font-mono font-bold text-sm tnum break-words ${burned.quote > 0n ? "text-warm-ink" : "text-muted"}`}>{fq(burned.quote)}</dd>
<dd className={`font-mono font-bold text-sm tnum break-words ${burned.token > 0n ? "text-warm-ink" : "text-muted"}`}>{ft(burned.token)}</dd>
</>
)}
</div>
Expand Down Expand Up @@ -199,7 +201,7 @@ export default function CollectPanel({
{phase.k === "sent" ? (
<p className="text-[11px] text-muted">
tx{" "}
<a href={explorerTx(chain, phase.hash)} target="_blank" rel="noreferrer" className="font-mono underline underline-offset-2">
<a href={explorerTx(chain, phase.hash)} target="_blank" rel="noreferrer" className="font-code underline underline-offset-2">
{phase.hash.slice(0, 10)}…
</a>
</p>
Expand Down
46 changes: 28 additions & 18 deletions app/src/components/launchpad/CopyChip.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,36 @@
"use client";

import { useState } from "react";
import { Check, Copy } from "lucide-react";

export default function CopyChip({ value, label, className = "" }: { value: string; label?: string; className?: string }) {
/** The chip's own look. A `className` replaces it entirely, so a toolbar can draw the button as one of its segments. */
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";

export default function CopyChip({ value, label, className = CHIP }: { value: string; label?: string; className?: string }) {
const [done, setDone] = useState(false);
const shown = label ?? `${value.slice(0, 6)}…${value.slice(-4)}`;
return (
<button
type="button"
onClick={async () => {
try {
await navigator.clipboard.writeText(value);
setDone(true);
setTimeout(() => setDone(false), 1200);
} catch {
/* clipboard blocked */
}
}}
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}`}
title={value}
>
{label ?? `${value.slice(0, 6)}…${value.slice(-4)}`}
<span className="text-faint">{done ? "✓" : "⧉"}</span>
</button>
<>
<button
type="button"
onClick={async () => {
try {
await navigator.clipboard.writeText(value);
setDone(true);
setTimeout(() => setDone(false), 1200);
} catch {
/* clipboard blocked */
}
}}
className={className}
title={value}
aria-label={`Copy ${shown}`}
>
{shown}
{done ? <Check size={13} aria-hidden="true" className="shrink-0 text-up" /> : <Copy size={13} aria-hidden="true" className="shrink-0 text-faint" />}
</button>
{/* outside the button: a button's children are presentational, so a live region inside it may never be read */}
<span role="status" className="sr-only">{done ? "Copied" : ""}</span>
</>
);
}
31 changes: 17 additions & 14 deletions app/src/components/launchpad/HoldersPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,24 +16,27 @@ const TAG_STYLE: Record<string, string> = {
sniper: "border-warm/40 bg-warm-soft text-warm-ink",
whale: "border-line-strong bg-card text-ink",
};
/** Tags read as words in sentence case, not shouted labels. */
const TAG_LABEL: Record<string, string> = { creator: "Creator", pool: "Pool", burn: "Burn", sniper: "Sniper", whale: "Whale" };
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;

export default function HoldersPanel({ chain, symbol, p, embedded = false }: { chain: ChainKey; symbol: string; p: HolderPanel | null; embedded?: boolean }) {
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>;
const supply = Number(BigInt(p.supply)) / 1e18;
return (
<section className={embedded ? "overflow-hidden bg-paper" : "overflow-hidden rounded-2xl border border-line bg-paper"} aria-label="holders">
<div className="px-4 pt-4 pb-3 flex items-baseline justify-between gap-3 flex-wrap">
<h2 className="font-semibold text-ink">Holders</h2>
<span className="text-xs text-muted">{p.synced ? "from every transfer of the token, live" : "indexing transfer history…"}</span>
<section className={embedded ? "overflow-hidden" : "overflow-hidden rounded-2xl border border-line bg-card"} aria-label="holders">
<div className="px-5 pt-4 pb-3 flex items-baseline justify-between gap-3 flex-wrap">
{/* inside the Holders tab the tab already names the panel; the heading stays for the document outline */}
<h2 className={embedded ? "sr-only" : "text-sm font-semibold text-ink"}>Holders</h2>
<span className="text-[11px] text-muted">{p.synced ? "From every transfer of the token, live" : "Indexing transfer history…"}</span>
</div>
<dl className="px-4 grid grid-cols-2 sm:grid-cols-4 gap-2.5">
<dl className="px-5 grid grid-cols-2 sm:grid-cols-4 gap-2.5">
<Stat k="Holders" v={`${p.holders}`} />
<Stat k="Top 10 hold" v={fmtShare(p.top10Bps)} warn={p.top10Bps >= 5000} />
<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} />
<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} />
</dl>
<ul className="px-4 pt-3 flex flex-wrap gap-1.5">
<ul className="px-5 pt-3 flex flex-wrap gap-1.5">
{p.notes.filter((n) => !n.text.includes("no red flags")).map((n) => (
<li key={n.text} className={`inline-flex items-center h-6 px-2 rounded-full border text-[11px] font-medium ${NOTE_STYLE[n.level]}`}>
{n.text}
Expand All @@ -43,21 +46,21 @@ export default function HoldersPanel({ chain, symbol, p, embedded = false }: { c
{p.top.length > 0 ? (
<ol className="mt-3 border-t border-line divide-y divide-line">
{p.top.map((h, i) => (
<li key={h.address} className="px-4 py-2 flex items-center gap-3 text-sm">
<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">
<span className="w-5 text-right font-mono text-xs text-muted tnum">{i + 1}</span>
<a href={explorerAddress(chain, h.address)} target="_blank" rel="noreferrer" className="font-mono text-xs text-body hover:text-ink" title={h.address}>
<a href={explorerAddress(chain, h.address)} target="_blank" rel="noreferrer" className="font-code text-xs text-body hover:text-ink" title={h.address}>
{shortAddr(h.address)}
</a>
<span className="flex gap-1">
{h.tags.map((t) => (
<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]}`}>
{t}
<span key={t} className={`inline-flex items-center h-5 px-1.5 rounded-md border text-[10px] font-medium ${TAG_STYLE[t]}`}>
{TAG_LABEL[t] ?? t}
</span>
))}
</span>
<span className="ml-auto flex items-center gap-2 min-w-0">
<span className="hidden sm:block h-1.5 w-24 rounded-full bg-paper overflow-hidden" aria-hidden>
<span className="block h-full bg-ink" style={{ width: `${Math.min(100, h.bps / 100)}%` }} />
<span className="hidden sm:block h-1.5 w-24 rounded-full bg-line overflow-hidden" aria-hidden>
<span className="block h-full rounded-full bg-[color:var(--tok,var(--color-ink))]" style={{ width: `${Math.min(100, h.bps / 100)}%` }} />
</span>
<span className="font-mono tnum text-xs text-muted hidden md:inline">{fmtCompact(Number(BigInt(h.balance)) / 1e18, 1)}</span>
<span className="font-mono tnum font-semibold text-ink w-14 text-right">{fmtShare(h.bps)}</span>
Expand All @@ -66,9 +69,9 @@ export default function HoldersPanel({ chain, symbol, p, embedded = false }: { c
))}
</ol>
) : (
<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>
<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>
)}
<p className="px-4 py-3 text-[11px] text-muted border-t border-line">
<p className="px-5 py-3 text-[11px] text-muted border-t border-line">
{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.
</p>
</section>
Expand Down
33 changes: 21 additions & 12 deletions app/src/components/launchpad/LaunchReceipt.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,29 @@
import { ArrowDown, ArrowUpRight, LockKeyhole } from "lucide-react";
import { ArrowUpRight } from "lucide-react";
import { launchpad } from "@/lib/launchpad/config";
import { explorerAddress, explorerTx, type ChainKey } from "@/lib/chainPublic";

/** A receipt of the launch mechanics, not a claim about current wallet balances. */
/** A receipt of the launch mechanics, not a claim about current wallet balances. Set like one: dotted leaders, mono figures. */
export default function LaunchReceipt({ chain, symbol, supply, txHash }: { chain: ChainKey; symbol: string; supply: string; txHash: string }) {
const locker = launchpad(chain).locker;
return <section className="overflow-hidden rounded-2xl border border-line bg-paper" aria-labelledby="receipt-title">
<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>
<div className="px-5 py-4">
<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>
<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>
<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">
<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>
</a>
<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>
<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>
return <section className="overflow-hidden rounded-2xl border border-line bg-card" aria-labelledby="receipt-title">
<div className="flex items-center justify-between gap-3 px-5 pt-4">
<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="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>
</div>
<dl className="px-5 pb-4 pt-2 text-xs">
<Line k="Fixed supply" v={<span title={`${supply} ${symbol}`}>{supply} {symbol}</span>} />
<Line k="Into the pool at launch" v="100%" />
<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>} />
<Line k="Platform fee" v={<span className="text-up">$0</span>} />
</dl>
<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>
</section>;
}

function Line({ k, v }: { k: string; v: React.ReactNode }) {
return <div className="flex items-baseline gap-2 py-1.5">
<dt className="shrink-0 text-muted">{k}</dt>
<span aria-hidden="true" className="min-w-4 flex-1 translate-y-[-3px] border-b border-dotted border-line-strong" />
<dd className="min-w-0 truncate font-mono font-semibold text-ink tnum">{v}</dd>
</div>;
}
Loading
Loading