@medialane/ui 0.23.1 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/coin-card.cjs +58 -56
- package/dist/components/coin-card.cjs.map +1 -1
- package/dist/components/coin-card.js +59 -58
- package/dist/components/coin-card.js.map +1 -1
- package/dist/components/coins-explorer.cjs +106 -42
- package/dist/components/coins-explorer.cjs.map +1 -1
- package/dist/components/coins-explorer.js +108 -44
- package/dist/components/coins-explorer.js.map +1 -1
- package/package.json +1 -1
|
@@ -37,89 +37,91 @@ module.exports = __toCommonJS(coin_card_exports);
|
|
|
37
37
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
38
38
|
var import_link = __toESM(require("next/link"), 1);
|
|
39
39
|
var import_image = __toESM(require("next/image"), 1);
|
|
40
|
-
var import_lucide_react = require("lucide-react");
|
|
41
40
|
var import_cn = require("../utils/cn.js");
|
|
42
41
|
var import_ipfs = require("../utils/ipfs.js");
|
|
42
|
+
var import_motion_primitives = require("./motion-primitives.js");
|
|
43
|
+
var import_currency_icon = require("./currency-icon.js");
|
|
43
44
|
var import_coins = require("../data/coins.js");
|
|
44
45
|
const KIND_TAG = {
|
|
45
|
-
creator: "border-brand-
|
|
46
|
-
memecoin: "border-brand-
|
|
46
|
+
creator: "border-brand-blue/30 bg-brand-blue/10 text-brand-blue",
|
|
47
|
+
memecoin: "border-brand-purple/30 bg-brand-purple/10 text-brand-purple"
|
|
47
48
|
};
|
|
49
|
+
const KIND_OVER_IMAGE = {
|
|
50
|
+
creator: "bg-brand-blue/85 text-white",
|
|
51
|
+
memecoin: "bg-brand-purple/85 text-white"
|
|
52
|
+
};
|
|
53
|
+
const KIND_FALLBACK = {
|
|
54
|
+
creator: "from-brand-blue to-brand-purple",
|
|
55
|
+
memecoin: "from-brand-purple to-brand-rose"
|
|
56
|
+
};
|
|
57
|
+
const kindLabel = (kind) => kind === "creator" ? "Creator Coin" : "Memecoin";
|
|
48
58
|
function useTileModel({ collection, usePrice }) {
|
|
49
59
|
const { price, isLoading } = usePrice(collection);
|
|
50
60
|
const kind = (0, import_coins.coinKind)(collection.service);
|
|
51
|
-
const verified = collection.claimedBy != null || kind === "creator";
|
|
52
61
|
const logoUri = collection.profile?.image ?? collection.image;
|
|
53
62
|
const logo = logoUri ? (0, import_ipfs.ipfsToHttp)(logoUri) : null;
|
|
54
63
|
const initials = (collection.symbol ?? collection.name ?? "?").trim().slice(0, 2).toUpperCase();
|
|
55
|
-
|
|
56
|
-
const chain = (collection.chain ?? "").toString().toLowerCase();
|
|
57
|
-
return { price, isLoading, kind, verified, logo, initials, fdv, chain };
|
|
64
|
+
return { price, isLoading, kind, logo, initials };
|
|
58
65
|
}
|
|
59
|
-
function
|
|
60
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className:
|
|
66
|
+
function PriceValue({ price, isLoading }) {
|
|
67
|
+
if (isLoading) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "inline-block h-4 w-14 rounded bg-muted-foreground/20 animate-pulse" });
|
|
68
|
+
if (!price) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "text-muted-foreground", children: "\u2014" });
|
|
69
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "inline-flex items-center gap-1 font-semibold tabular-nums", children: [
|
|
70
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_currency_icon.CurrencyIcon, { symbol: price.quoteSymbol, size: 14 }),
|
|
71
|
+
(0, import_coins.formatCoinPrice)(price.quotePerCoin)
|
|
72
|
+
] });
|
|
61
73
|
}
|
|
62
74
|
function CoinCard({ collection, usePrice, href }) {
|
|
63
75
|
const m = useTileModel({ collection, usePrice });
|
|
64
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.
|
|
65
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
76
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_motion_primitives.MotionCard, { className: "card-base group relative flex h-full flex-col", children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_link.default, { href, className: "flex h-full flex-col", children: [
|
|
77
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "relative aspect-square w-full overflow-hidden bg-muted", children: [
|
|
78
|
+
m.logo ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
79
|
+
import_image.default,
|
|
80
|
+
{
|
|
81
|
+
src: m.logo,
|
|
82
|
+
alt: collection.name ?? "Coin",
|
|
83
|
+
fill: true,
|
|
84
|
+
sizes: "(min-width:1280px) 25vw, (min-width:1024px) 33vw, (min-width:640px) 50vw, 100vw",
|
|
85
|
+
className: "object-cover transition-transform duration-500 group-hover:scale-105",
|
|
86
|
+
unoptimized: true
|
|
87
|
+
}
|
|
88
|
+
) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: (0, import_cn.cn)("absolute inset-0 flex items-center justify-center bg-gradient-to-br", KIND_FALLBACK[m.kind]), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "select-none text-5xl font-black tracking-tighter text-white/90", children: m.initials }) }),
|
|
89
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: (0, import_cn.cn)("absolute left-2 top-2 rounded-full px-2 py-0.5 text-[10px] font-semibold backdrop-blur-md", KIND_OVER_IMAGE[m.kind]), children: kindLabel(m.kind) })
|
|
75
90
|
] }),
|
|
76
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "
|
|
77
|
-
/* @__PURE__ */ (0, import_jsx_runtime.
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
m.chain ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "text-[10px] uppercase tracking-wide text-muted-foreground", children: m.chain }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {}),
|
|
86
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "font-medium text-primary", children: "Trade" })
|
|
91
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex flex-col gap-2 p-3", children: [
|
|
92
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "min-w-0", children: [
|
|
93
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "truncate font-semibold leading-tight", children: collection.name ?? "Untitled coin" }),
|
|
94
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "truncate text-sm text-muted-foreground", children: collection.symbol ?? "\u2014" })
|
|
95
|
+
] }),
|
|
96
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex items-baseline justify-between gap-2", children: [
|
|
97
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "text-[10px] uppercase tracking-wide text-muted-foreground", children: "Price" }),
|
|
98
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(PriceValue, { price: m.price, isLoading: m.isLoading })
|
|
99
|
+
] })
|
|
87
100
|
] })
|
|
88
|
-
] });
|
|
101
|
+
] }) });
|
|
89
102
|
}
|
|
90
103
|
function CoinRow({ collection, usePrice, href }) {
|
|
91
104
|
const m = useTileModel({ collection, usePrice });
|
|
92
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_link.default, { href, className: "flex items-center gap-3 rounded-lg border border-border/60 bg-card px-3 py-2.5 transition-colors hover:border-primary/40", children: [
|
|
93
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "relative h-9 w-9 shrink-0 overflow-hidden rounded-full bg-muted", children: m.logo ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_image.default, { src: m.logo, alt: "", fill: true, sizes: "36px", className: "object-cover", unoptimized: true }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "flex h-full w-full items-center justify-center bg-gradient-to-br
|
|
105
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_link.default, { href, className: "flex items-center gap-3 rounded-lg border border-border/60 bg-card px-3 py-2.5 transition-colors hover:border-primary/40 active:scale-[0.99]", children: [
|
|
106
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "relative h-9 w-9 shrink-0 overflow-hidden rounded-full bg-muted", children: m.logo ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_image.default, { src: m.logo, alt: "", fill: true, sizes: "36px", className: "object-cover", unoptimized: true }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: (0, import_cn.cn)("flex h-full w-full items-center justify-center bg-gradient-to-br text-[11px] font-bold text-white", KIND_FALLBACK[m.kind]), children: m.initials }) }),
|
|
94
107
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "min-w-0 flex-1", children: [
|
|
95
|
-
/* @__PURE__ */ (0, import_jsx_runtime.
|
|
96
|
-
|
|
97
|
-
m.verified && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.BadgeCheck, { className: "h-3.5 w-3.5 shrink-0 text-primary", "aria-label": "Verified" })
|
|
98
|
-
] }),
|
|
99
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "text-xs text-muted-foreground", children: collection.symbol ?? "\u2014" })
|
|
108
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "truncate text-sm font-semibold", children: collection.name ?? "Untitled coin" }),
|
|
109
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "block truncate text-xs text-muted-foreground", children: collection.symbol ?? "\u2014" })
|
|
100
110
|
] }),
|
|
101
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: (0, import_cn.cn)("hidden shrink-0 rounded-full border px-2 py-0.5 text-[10px] font-medium sm:inline-block", KIND_TAG[m.kind]), children: m.kind
|
|
102
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "w-
|
|
103
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "hidden shrink-0 text-sm font-medium text-primary sm:inline-block", children: "Trade" })
|
|
104
|
-
] });
|
|
105
|
-
}
|
|
106
|
-
function Stat({ label, children }) {
|
|
107
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex flex-col gap-0.5", children: [
|
|
108
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "text-[10px] uppercase tracking-wide text-muted-foreground", children: label }),
|
|
109
|
-
children
|
|
111
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: (0, import_cn.cn)("hidden shrink-0 rounded-full border px-2 py-0.5 text-[10px] font-medium sm:inline-block", KIND_TAG[m.kind]), children: kindLabel(m.kind) }),
|
|
112
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "w-28 shrink-0 text-right text-sm", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PriceValue, { price: m.price, isLoading: m.isLoading }) })
|
|
110
113
|
] });
|
|
111
114
|
}
|
|
112
115
|
function CoinCardSkeleton() {
|
|
113
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex flex-col
|
|
114
|
-
/* @__PURE__ */ (0, import_jsx_runtime.
|
|
115
|
-
|
|
116
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "
|
|
116
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "card-base flex flex-col", children: [
|
|
117
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "aspect-square w-full animate-pulse bg-muted" }),
|
|
118
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex flex-col gap-2 p-3", children: [
|
|
119
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "space-y-1.5", children: [
|
|
117
120
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "h-4 w-24 rounded bg-muted-foreground/20 animate-pulse" }),
|
|
118
121
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "h-3 w-12 rounded bg-muted-foreground/20 animate-pulse" })
|
|
119
|
-
] })
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "h-9 w-full bg-muted-foreground/10 animate-pulse" })
|
|
122
|
+
] }),
|
|
123
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "h-4 w-full rounded bg-muted-foreground/10 animate-pulse" })
|
|
124
|
+
] })
|
|
123
125
|
] });
|
|
124
126
|
}
|
|
125
127
|
// Annotate the CommonJS export names for ESM import in node:
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/coin-card.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * CoinCard / CoinRow — chain-agnostic coin discovery tiles.\n *\n * Pure presentation: the price read (`usePrice`) and the link target (`href`)\n * are injected by the consuming app, and the collection is typed structurally\n * (CoinCollectionLike) — so a coin on Starknet, Ethereum, or Solana renders the\n * same with zero changes here. `chain` is shown as a badge.\n */\n\nimport Link from \"next/link\";\nimport Image from \"next/image\";\nimport { BadgeCheck, Users } from \"lucide-react\";\nimport { cn } from \"../utils/cn.js\";\nimport { ipfsToHttp } from \"../utils/ipfs.js\";\nimport {\n coinKind, formatCoinPrice, formatFdv,\n type CoinCollectionLike, type CoinPriceLike,\n} from \"../data/coins.js\";\n\n/** Injected per-chain spot-price read. `price` is null while loading/unknown. */\nexport type UseCoinPrice = (collection: CoinCollectionLike) => {\n price: CoinPriceLike | null;\n isLoading: boolean;\n};\n\nexport interface CoinTileProps {\n collection: CoinCollectionLike;\n usePrice: UseCoinPrice;\n /** Link target — internal coin page or the per-chain trading app. */\n href: string;\n}\n\nconst KIND_TAG: Record<string, string> = {\n creator: \"border-brand-purple/30 bg-brand-purple/10 text-brand-purple\",\n memecoin: \"border-brand-rose/30 bg-brand-rose/10 text-brand-rose\",\n};\n\nfunction useTileModel({ collection, usePrice }: Omit<CoinTileProps, \"href\">) {\n const { price, isLoading } = usePrice(collection);\n const kind = coinKind(collection.service);\n const verified = collection.claimedBy != null || kind === \"creator\";\n const logoUri = collection.profile?.image ?? collection.image;\n const logo = logoUri ? ipfsToHttp(logoUri) : null;\n const initials = (collection.symbol ?? collection.name ?? \"?\").trim().slice(0, 2).toUpperCase();\n const fdv = formatFdv(price?.quotePerCoin, collection.totalSupply, price?.quoteSymbol ?? null);\n const chain = (collection.chain ?? \"\").toString().toLowerCase();\n return { price, isLoading, kind, verified, logo, initials, fdv, chain };\n}\n\nfunction PriceSkel({ wide }: { wide?: boolean }) {\n return <span className={cn(\"inline-block h-4 rounded bg-muted-foreground/20 animate-pulse\", wide ? \"w-16\" : \"w-12\")} />;\n}\n\nexport function CoinCard({ collection, usePrice, href }: CoinTileProps) {\n const m = useTileModel({ collection, usePrice });\n return (\n <Link href={href} className=\"flex flex-col rounded-xl border border-border/60 bg-card overflow-hidden transition-transform active:scale-[0.99]\">\n <div className=\"flex items-center gap-3 p-4\">\n <div className=\"relative h-12 w-12 shrink-0 rounded-full overflow-hidden bg-muted\">\n {m.logo ? (\n <Image src={m.logo} alt=\"\" fill sizes=\"48px\" className=\"object-cover\" unoptimized />\n ) : (\n <div className=\"flex h-full w-full items-center justify-center bg-gradient-to-br from-brand-blue to-brand-purple text-sm font-bold text-white\">{m.initials}</div>\n )}\n </div>\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex items-center gap-1\">\n <span className=\"truncate font-semibold\">{collection.name ?? \"Untitled coin\"}</span>\n {m.verified && <BadgeCheck className=\"h-4 w-4 shrink-0 text-primary\" aria-label=\"Verified\" />}\n </div>\n <span className=\"text-sm text-muted-foreground\">{collection.symbol ?? \"—\"}</span>\n </div>\n <span className={cn(\"shrink-0 rounded-full border px-2 py-0.5 text-[10px] font-medium\", KIND_TAG[m.kind])}>\n {m.kind === \"creator\" ? \"Creator Coin\" : \"Memecoin\"}\n </span>\n </div>\n <div className=\"grid grid-cols-3 gap-2 border-t border-border/60 px-4 py-3 text-sm\">\n <Stat label=\"Price\">\n {m.isLoading ? <PriceSkel /> : m.price ? <span className=\"font-semibold\">{formatCoinPrice(m.price.quotePerCoin)}</span> : <span className=\"text-muted-foreground\">—</span>}\n </Stat>\n <Stat label=\"FDV\">\n {m.isLoading ? <PriceSkel /> : <span className=\"font-semibold\">{m.fdv ?? \"—\"}</span>}\n </Stat>\n <Stat label=\"Holders\">\n <span className=\"inline-flex items-center gap-1 font-semibold\">\n <Users className=\"h-3 w-3 text-muted-foreground\" />{collection.holderCount || \"—\"}\n </span>\n </Stat>\n </div>\n <div className=\"flex items-center justify-between border-t border-border/60 px-4 py-2.5 text-sm\">\n {m.chain ? <span className=\"text-[10px] uppercase tracking-wide text-muted-foreground\">{m.chain}</span> : <span />}\n <span className=\"font-medium text-primary\">Trade</span>\n </div>\n </Link>\n );\n}\n\nexport function CoinRow({ collection, usePrice, href }: CoinTileProps) {\n const m = useTileModel({ collection, usePrice });\n return (\n <Link href={href} className=\"flex items-center gap-3 rounded-lg border border-border/60 bg-card px-3 py-2.5 transition-colors hover:border-primary/40\">\n <div className=\"relative h-9 w-9 shrink-0 overflow-hidden rounded-full bg-muted\">\n {m.logo ? <Image src={m.logo} alt=\"\" fill sizes=\"36px\" className=\"object-cover\" unoptimized /> :\n <div className=\"flex h-full w-full items-center justify-center bg-gradient-to-br from-brand-blue to-brand-purple text-[11px] font-bold text-white\">{m.initials}</div>}\n </div>\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex items-center gap-1\">\n <span className=\"truncate text-sm font-semibold\">{collection.name ?? \"Untitled coin\"}</span>\n {m.verified && <BadgeCheck className=\"h-3.5 w-3.5 shrink-0 text-primary\" aria-label=\"Verified\" />}\n </div>\n <span className=\"text-xs text-muted-foreground\">{collection.symbol ?? \"—\"}</span>\n </div>\n <span className={cn(\"hidden shrink-0 rounded-full border px-2 py-0.5 text-[10px] font-medium sm:inline-block\", KIND_TAG[m.kind])}>\n {m.kind === \"creator\" ? \"Creator Coin\" : \"Memecoin\"}\n </span>\n <span className=\"w-24 shrink-0 text-right text-sm font-semibold tabular-nums\">\n {m.isLoading ? <span className=\"ml-auto inline-block h-4 w-12 rounded bg-muted-foreground/20 animate-pulse\" /> : m.price ? formatCoinPrice(m.price.quotePerCoin) : <span className=\"text-muted-foreground\">—</span>}\n </span>\n <span className=\"hidden shrink-0 text-sm font-medium text-primary sm:inline-block\">Trade</span>\n </Link>\n );\n}\n\nfunction Stat({ label, children }: { label: string; children: React.ReactNode }) {\n return (\n <div className=\"flex flex-col gap-0.5\">\n <span className=\"text-[10px] uppercase tracking-wide text-muted-foreground\">{label}</span>\n {children}\n </div>\n );\n}\n\nexport function CoinCardSkeleton() {\n return (\n <div className=\"flex flex-col rounded-xl border border-border/60 bg-card overflow-hidden\">\n <div className=\"flex items-center gap-3 p-4\">\n <div className=\"h-12 w-12 rounded-full bg-muted animate-pulse\" />\n <div className=\"flex-1 space-y-1.5\">\n <div className=\"h-4 w-24 rounded bg-muted-foreground/20 animate-pulse\" />\n <div className=\"h-3 w-12 rounded bg-muted-foreground/20 animate-pulse\" />\n </div>\n </div>\n <div className=\"grid grid-cols-3 gap-2 border-t border-border/60 px-4 py-3\">\n {[0, 1, 2].map((i) => <div key={i} className=\"h-8 w-full rounded bg-muted-foreground/10 animate-pulse\" />)}\n </div>\n <div className=\"h-9 w-full bg-muted-foreground/10 animate-pulse\" />\n </div>\n );\n}\n"],"mappings":"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","names":["Link","Image"]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/coin-card.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * CoinCard / CoinRow — chain-agnostic, art-forward coin discovery tiles.\n *\n * Pure presentation: the price read (`usePrice`) and the link target (`href`)\n * are injected by the consuming app, and the collection is typed structurally\n * (CoinCollectionLike) — so a coin on Starknet, Ethereum, or Solana renders the\n * same with zero changes here. The coin's artwork is the hero; the whole tile\n * links to the coin page (where trading happens), and the only stat is the live\n * spot price with its quote-currency icon. FDV / holders / a \"verified\" mark are\n * intentionally not shown — Medialane indexes none of them.\n */\n\nimport Link from \"next/link\";\nimport Image from \"next/image\";\nimport { cn } from \"../utils/cn.js\";\nimport { ipfsToHttp } from \"../utils/ipfs.js\";\nimport { MotionCard } from \"./motion-primitives.js\";\nimport { CurrencyIcon } from \"./currency-icon.js\";\nimport {\n coinKind, formatCoinPrice,\n type CoinCollectionLike, type CoinPriceLike,\n} from \"../data/coins.js\";\n\n/** Injected per-chain spot-price read. `price` is null while loading/unknown. */\nexport type UseCoinPrice = (collection: CoinCollectionLike) => {\n price: CoinPriceLike | null;\n isLoading: boolean;\n};\n\nexport interface CoinTileProps {\n collection: CoinCollectionLike;\n usePrice: UseCoinPrice;\n /** Link target — internal coin page or the per-chain trading app. */\n href: string;\n}\n\n// Kind tag on a card surface (table row) — brand hues, never red.\nconst KIND_TAG: Record<string, string> = {\n creator: \"border-brand-blue/30 bg-brand-blue/10 text-brand-blue\",\n memecoin: \"border-brand-purple/30 bg-brand-purple/10 text-brand-purple\",\n};\n// Kind tag layered over artwork — solid brand pill, white text for legibility.\nconst KIND_OVER_IMAGE: Record<string, string> = {\n creator: \"bg-brand-blue/85 text-white\",\n memecoin: \"bg-brand-purple/85 text-white\",\n};\n// Art-fallback gradient (no logo) — vivid brand wash.\nconst KIND_FALLBACK: Record<string, string> = {\n creator: \"from-brand-blue to-brand-purple\",\n memecoin: \"from-brand-purple to-brand-rose\",\n};\n\nconst kindLabel = (kind: string) => (kind === \"creator\" ? \"Creator Coin\" : \"Memecoin\");\n\nfunction useTileModel({ collection, usePrice }: Omit<CoinTileProps, \"href\">) {\n const { price, isLoading } = usePrice(collection);\n const kind = coinKind(collection.service);\n const logoUri = collection.profile?.image ?? collection.image;\n const logo = logoUri ? ipfsToHttp(logoUri) : null;\n const initials = (collection.symbol ?? collection.name ?? \"?\").trim().slice(0, 2).toUpperCase();\n return { price, isLoading, kind, logo, initials };\n}\n\nfunction PriceValue({ price, isLoading }: { price: CoinPriceLike | null; isLoading: boolean }) {\n if (isLoading) return <span className=\"inline-block h-4 w-14 rounded bg-muted-foreground/20 animate-pulse\" />;\n if (!price) return <span className=\"text-muted-foreground\">—</span>;\n return (\n <span className=\"inline-flex items-center gap-1 font-semibold tabular-nums\">\n <CurrencyIcon symbol={price.quoteSymbol} size={14} />\n {formatCoinPrice(price.quotePerCoin)}\n </span>\n );\n}\n\nexport function CoinCard({ collection, usePrice, href }: CoinTileProps) {\n const m = useTileModel({ collection, usePrice });\n return (\n <MotionCard className=\"card-base group relative flex h-full flex-col\">\n <Link href={href} className=\"flex h-full flex-col\">\n {/* Hero artwork */}\n <div className=\"relative aspect-square w-full overflow-hidden bg-muted\">\n {m.logo ? (\n <Image\n src={m.logo}\n alt={collection.name ?? \"Coin\"}\n fill\n sizes=\"(min-width:1280px) 25vw, (min-width:1024px) 33vw, (min-width:640px) 50vw, 100vw\"\n className=\"object-cover transition-transform duration-500 group-hover:scale-105\"\n unoptimized\n />\n ) : (\n <div className={cn(\"absolute inset-0 flex items-center justify-center bg-gradient-to-br\", KIND_FALLBACK[m.kind])}>\n <span className=\"select-none text-5xl font-black tracking-tighter text-white/90\">{m.initials}</span>\n </div>\n )}\n <span className={cn(\"absolute left-2 top-2 rounded-full px-2 py-0.5 text-[10px] font-semibold backdrop-blur-md\", KIND_OVER_IMAGE[m.kind])}>\n {kindLabel(m.kind)}\n </span>\n </div>\n\n {/* Body */}\n <div className=\"flex flex-col gap-2 p-3\">\n <div className=\"min-w-0\">\n <div className=\"truncate font-semibold leading-tight\">{collection.name ?? \"Untitled coin\"}</div>\n <div className=\"truncate text-sm text-muted-foreground\">{collection.symbol ?? \"—\"}</div>\n </div>\n <div className=\"flex items-baseline justify-between gap-2\">\n <span className=\"text-[10px] uppercase tracking-wide text-muted-foreground\">Price</span>\n <PriceValue price={m.price} isLoading={m.isLoading} />\n </div>\n </div>\n </Link>\n </MotionCard>\n );\n}\n\nexport function CoinRow({ collection, usePrice, href }: CoinTileProps) {\n const m = useTileModel({ collection, usePrice });\n return (\n <Link href={href} className=\"flex items-center gap-3 rounded-lg border border-border/60 bg-card px-3 py-2.5 transition-colors hover:border-primary/40 active:scale-[0.99]\">\n <div className=\"relative h-9 w-9 shrink-0 overflow-hidden rounded-full bg-muted\">\n {m.logo ? <Image src={m.logo} alt=\"\" fill sizes=\"36px\" className=\"object-cover\" unoptimized /> :\n <div className={cn(\"flex h-full w-full items-center justify-center bg-gradient-to-br text-[11px] font-bold text-white\", KIND_FALLBACK[m.kind])}>{m.initials}</div>}\n </div>\n <div className=\"min-w-0 flex-1\">\n <span className=\"truncate text-sm font-semibold\">{collection.name ?? \"Untitled coin\"}</span>\n <span className=\"block truncate text-xs text-muted-foreground\">{collection.symbol ?? \"—\"}</span>\n </div>\n <span className={cn(\"hidden shrink-0 rounded-full border px-2 py-0.5 text-[10px] font-medium sm:inline-block\", KIND_TAG[m.kind])}>\n {kindLabel(m.kind)}\n </span>\n <span className=\"w-28 shrink-0 text-right text-sm\">\n <PriceValue price={m.price} isLoading={m.isLoading} />\n </span>\n </Link>\n );\n}\n\nexport function CoinCardSkeleton() {\n return (\n <div className=\"card-base flex flex-col\">\n <div className=\"aspect-square w-full animate-pulse bg-muted\" />\n <div className=\"flex flex-col gap-2 p-3\">\n <div className=\"space-y-1.5\">\n <div className=\"h-4 w-24 rounded bg-muted-foreground/20 animate-pulse\" />\n <div className=\"h-3 w-12 rounded bg-muted-foreground/20 animate-pulse\" />\n </div>\n <div className=\"h-4 w-full rounded bg-muted-foreground/10 animate-pulse\" />\n </div>\n </div>\n );\n}\n"],"mappings":"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","names":["Link","Image"]}
|
|
@@ -2,93 +2,94 @@
|
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import Link from "next/link";
|
|
4
4
|
import Image from "next/image";
|
|
5
|
-
import { BadgeCheck, Users } from "lucide-react";
|
|
6
5
|
import { cn } from "../utils/cn.js";
|
|
7
6
|
import { ipfsToHttp } from "../utils/ipfs.js";
|
|
7
|
+
import { MotionCard } from "./motion-primitives.js";
|
|
8
|
+
import { CurrencyIcon } from "./currency-icon.js";
|
|
8
9
|
import {
|
|
9
10
|
coinKind,
|
|
10
|
-
formatCoinPrice
|
|
11
|
-
formatFdv
|
|
11
|
+
formatCoinPrice
|
|
12
12
|
} from "../data/coins.js";
|
|
13
13
|
const KIND_TAG = {
|
|
14
|
-
creator: "border-brand-
|
|
15
|
-
memecoin: "border-brand-
|
|
14
|
+
creator: "border-brand-blue/30 bg-brand-blue/10 text-brand-blue",
|
|
15
|
+
memecoin: "border-brand-purple/30 bg-brand-purple/10 text-brand-purple"
|
|
16
16
|
};
|
|
17
|
+
const KIND_OVER_IMAGE = {
|
|
18
|
+
creator: "bg-brand-blue/85 text-white",
|
|
19
|
+
memecoin: "bg-brand-purple/85 text-white"
|
|
20
|
+
};
|
|
21
|
+
const KIND_FALLBACK = {
|
|
22
|
+
creator: "from-brand-blue to-brand-purple",
|
|
23
|
+
memecoin: "from-brand-purple to-brand-rose"
|
|
24
|
+
};
|
|
25
|
+
const kindLabel = (kind) => kind === "creator" ? "Creator Coin" : "Memecoin";
|
|
17
26
|
function useTileModel({ collection, usePrice }) {
|
|
18
27
|
const { price, isLoading } = usePrice(collection);
|
|
19
28
|
const kind = coinKind(collection.service);
|
|
20
|
-
const verified = collection.claimedBy != null || kind === "creator";
|
|
21
29
|
const logoUri = collection.profile?.image ?? collection.image;
|
|
22
30
|
const logo = logoUri ? ipfsToHttp(logoUri) : null;
|
|
23
31
|
const initials = (collection.symbol ?? collection.name ?? "?").trim().slice(0, 2).toUpperCase();
|
|
24
|
-
|
|
25
|
-
const chain = (collection.chain ?? "").toString().toLowerCase();
|
|
26
|
-
return { price, isLoading, kind, verified, logo, initials, fdv, chain };
|
|
32
|
+
return { price, isLoading, kind, logo, initials };
|
|
27
33
|
}
|
|
28
|
-
function
|
|
29
|
-
return /* @__PURE__ */ jsx("span", { className:
|
|
34
|
+
function PriceValue({ price, isLoading }) {
|
|
35
|
+
if (isLoading) return /* @__PURE__ */ jsx("span", { className: "inline-block h-4 w-14 rounded bg-muted-foreground/20 animate-pulse" });
|
|
36
|
+
if (!price) return /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: "\u2014" });
|
|
37
|
+
return /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1 font-semibold tabular-nums", children: [
|
|
38
|
+
/* @__PURE__ */ jsx(CurrencyIcon, { symbol: price.quoteSymbol, size: 14 }),
|
|
39
|
+
formatCoinPrice(price.quotePerCoin)
|
|
40
|
+
] });
|
|
30
41
|
}
|
|
31
42
|
function CoinCard({ collection, usePrice, href }) {
|
|
32
43
|
const m = useTileModel({ collection, usePrice });
|
|
33
|
-
return /* @__PURE__ */
|
|
34
|
-
/* @__PURE__ */ jsxs("div", { className: "
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
+
return /* @__PURE__ */ jsx(MotionCard, { className: "card-base group relative flex h-full flex-col", children: /* @__PURE__ */ jsxs(Link, { href, className: "flex h-full flex-col", children: [
|
|
45
|
+
/* @__PURE__ */ jsxs("div", { className: "relative aspect-square w-full overflow-hidden bg-muted", children: [
|
|
46
|
+
m.logo ? /* @__PURE__ */ jsx(
|
|
47
|
+
Image,
|
|
48
|
+
{
|
|
49
|
+
src: m.logo,
|
|
50
|
+
alt: collection.name ?? "Coin",
|
|
51
|
+
fill: true,
|
|
52
|
+
sizes: "(min-width:1280px) 25vw, (min-width:1024px) 33vw, (min-width:640px) 50vw, 100vw",
|
|
53
|
+
className: "object-cover transition-transform duration-500 group-hover:scale-105",
|
|
54
|
+
unoptimized: true
|
|
55
|
+
}
|
|
56
|
+
) : /* @__PURE__ */ jsx("div", { className: cn("absolute inset-0 flex items-center justify-center bg-gradient-to-br", KIND_FALLBACK[m.kind]), children: /* @__PURE__ */ jsx("span", { className: "select-none text-5xl font-black tracking-tighter text-white/90", children: m.initials }) }),
|
|
57
|
+
/* @__PURE__ */ jsx("span", { className: cn("absolute left-2 top-2 rounded-full px-2 py-0.5 text-[10px] font-semibold backdrop-blur-md", KIND_OVER_IMAGE[m.kind]), children: kindLabel(m.kind) })
|
|
44
58
|
] }),
|
|
45
|
-
/* @__PURE__ */ jsxs("div", { className: "
|
|
46
|
-
/* @__PURE__ */
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
m.chain ? /* @__PURE__ */ jsx("span", { className: "text-[10px] uppercase tracking-wide text-muted-foreground", children: m.chain }) : /* @__PURE__ */ jsx("span", {}),
|
|
55
|
-
/* @__PURE__ */ jsx("span", { className: "font-medium text-primary", children: "Trade" })
|
|
59
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2 p-3", children: [
|
|
60
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
|
|
61
|
+
/* @__PURE__ */ jsx("div", { className: "truncate font-semibold leading-tight", children: collection.name ?? "Untitled coin" }),
|
|
62
|
+
/* @__PURE__ */ jsx("div", { className: "truncate text-sm text-muted-foreground", children: collection.symbol ?? "\u2014" })
|
|
63
|
+
] }),
|
|
64
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-baseline justify-between gap-2", children: [
|
|
65
|
+
/* @__PURE__ */ jsx("span", { className: "text-[10px] uppercase tracking-wide text-muted-foreground", children: "Price" }),
|
|
66
|
+
/* @__PURE__ */ jsx(PriceValue, { price: m.price, isLoading: m.isLoading })
|
|
67
|
+
] })
|
|
56
68
|
] })
|
|
57
|
-
] });
|
|
69
|
+
] }) });
|
|
58
70
|
}
|
|
59
71
|
function CoinRow({ collection, usePrice, href }) {
|
|
60
72
|
const m = useTileModel({ collection, usePrice });
|
|
61
|
-
return /* @__PURE__ */ jsxs(Link, { href, className: "flex items-center gap-3 rounded-lg border border-border/60 bg-card px-3 py-2.5 transition-colors hover:border-primary/40", children: [
|
|
62
|
-
/* @__PURE__ */ jsx("div", { className: "relative h-9 w-9 shrink-0 overflow-hidden rounded-full bg-muted", children: m.logo ? /* @__PURE__ */ jsx(Image, { src: m.logo, alt: "", fill: true, sizes: "36px", className: "object-cover", unoptimized: true }) : /* @__PURE__ */ jsx("div", { className: "flex h-full w-full items-center justify-center bg-gradient-to-br
|
|
73
|
+
return /* @__PURE__ */ jsxs(Link, { href, className: "flex items-center gap-3 rounded-lg border border-border/60 bg-card px-3 py-2.5 transition-colors hover:border-primary/40 active:scale-[0.99]", children: [
|
|
74
|
+
/* @__PURE__ */ jsx("div", { className: "relative h-9 w-9 shrink-0 overflow-hidden rounded-full bg-muted", children: m.logo ? /* @__PURE__ */ jsx(Image, { src: m.logo, alt: "", fill: true, sizes: "36px", className: "object-cover", unoptimized: true }) : /* @__PURE__ */ jsx("div", { className: cn("flex h-full w-full items-center justify-center bg-gradient-to-br text-[11px] font-bold text-white", KIND_FALLBACK[m.kind]), children: m.initials }) }),
|
|
63
75
|
/* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
|
|
64
|
-
/* @__PURE__ */
|
|
65
|
-
|
|
66
|
-
m.verified && /* @__PURE__ */ jsx(BadgeCheck, { className: "h-3.5 w-3.5 shrink-0 text-primary", "aria-label": "Verified" })
|
|
67
|
-
] }),
|
|
68
|
-
/* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: collection.symbol ?? "\u2014" })
|
|
76
|
+
/* @__PURE__ */ jsx("span", { className: "truncate text-sm font-semibold", children: collection.name ?? "Untitled coin" }),
|
|
77
|
+
/* @__PURE__ */ jsx("span", { className: "block truncate text-xs text-muted-foreground", children: collection.symbol ?? "\u2014" })
|
|
69
78
|
] }),
|
|
70
|
-
/* @__PURE__ */ jsx("span", { className: cn("hidden shrink-0 rounded-full border px-2 py-0.5 text-[10px] font-medium sm:inline-block", KIND_TAG[m.kind]), children: m.kind
|
|
71
|
-
/* @__PURE__ */ jsx("span", { className: "w-
|
|
72
|
-
/* @__PURE__ */ jsx("span", { className: "hidden shrink-0 text-sm font-medium text-primary sm:inline-block", children: "Trade" })
|
|
73
|
-
] });
|
|
74
|
-
}
|
|
75
|
-
function Stat({ label, children }) {
|
|
76
|
-
return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-0.5", children: [
|
|
77
|
-
/* @__PURE__ */ jsx("span", { className: "text-[10px] uppercase tracking-wide text-muted-foreground", children: label }),
|
|
78
|
-
children
|
|
79
|
+
/* @__PURE__ */ jsx("span", { className: cn("hidden shrink-0 rounded-full border px-2 py-0.5 text-[10px] font-medium sm:inline-block", KIND_TAG[m.kind]), children: kindLabel(m.kind) }),
|
|
80
|
+
/* @__PURE__ */ jsx("span", { className: "w-28 shrink-0 text-right text-sm", children: /* @__PURE__ */ jsx(PriceValue, { price: m.price, isLoading: m.isLoading }) })
|
|
79
81
|
] });
|
|
80
82
|
}
|
|
81
83
|
function CoinCardSkeleton() {
|
|
82
|
-
return /* @__PURE__ */ jsxs("div", { className: "flex flex-col
|
|
83
|
-
/* @__PURE__ */
|
|
84
|
-
|
|
85
|
-
/* @__PURE__ */ jsxs("div", { className: "
|
|
84
|
+
return /* @__PURE__ */ jsxs("div", { className: "card-base flex flex-col", children: [
|
|
85
|
+
/* @__PURE__ */ jsx("div", { className: "aspect-square w-full animate-pulse bg-muted" }),
|
|
86
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2 p-3", children: [
|
|
87
|
+
/* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
|
|
86
88
|
/* @__PURE__ */ jsx("div", { className: "h-4 w-24 rounded bg-muted-foreground/20 animate-pulse" }),
|
|
87
89
|
/* @__PURE__ */ jsx("div", { className: "h-3 w-12 rounded bg-muted-foreground/20 animate-pulse" })
|
|
88
|
-
] })
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
/* @__PURE__ */ jsx("div", { className: "h-9 w-full bg-muted-foreground/10 animate-pulse" })
|
|
90
|
+
] }),
|
|
91
|
+
/* @__PURE__ */ jsx("div", { className: "h-4 w-full rounded bg-muted-foreground/10 animate-pulse" })
|
|
92
|
+
] })
|
|
92
93
|
] });
|
|
93
94
|
}
|
|
94
95
|
export {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/coin-card.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * CoinCard / CoinRow — chain-agnostic coin discovery tiles.\n *\n * Pure presentation: the price read (`usePrice`) and the link target (`href`)\n * are injected by the consuming app, and the collection is typed structurally\n * (CoinCollectionLike) — so a coin on Starknet, Ethereum, or Solana renders the\n * same with zero changes here. `chain` is shown as a badge.\n */\n\nimport Link from \"next/link\";\nimport Image from \"next/image\";\nimport { BadgeCheck, Users } from \"lucide-react\";\nimport { cn } from \"../utils/cn.js\";\nimport { ipfsToHttp } from \"../utils/ipfs.js\";\nimport {\n coinKind, formatCoinPrice, formatFdv,\n type CoinCollectionLike, type CoinPriceLike,\n} from \"../data/coins.js\";\n\n/** Injected per-chain spot-price read. `price` is null while loading/unknown. */\nexport type UseCoinPrice = (collection: CoinCollectionLike) => {\n price: CoinPriceLike | null;\n isLoading: boolean;\n};\n\nexport interface CoinTileProps {\n collection: CoinCollectionLike;\n usePrice: UseCoinPrice;\n /** Link target — internal coin page or the per-chain trading app. */\n href: string;\n}\n\nconst KIND_TAG: Record<string, string> = {\n creator: \"border-brand-purple/30 bg-brand-purple/10 text-brand-purple\",\n memecoin: \"border-brand-rose/30 bg-brand-rose/10 text-brand-rose\",\n};\n\nfunction useTileModel({ collection, usePrice }: Omit<CoinTileProps, \"href\">) {\n const { price, isLoading } = usePrice(collection);\n const kind = coinKind(collection.service);\n const verified = collection.claimedBy != null || kind === \"creator\";\n const logoUri = collection.profile?.image ?? collection.image;\n const logo = logoUri ? ipfsToHttp(logoUri) : null;\n const initials = (collection.symbol ?? collection.name ?? \"?\").trim().slice(0, 2).toUpperCase();\n const fdv = formatFdv(price?.quotePerCoin, collection.totalSupply, price?.quoteSymbol ?? null);\n const chain = (collection.chain ?? \"\").toString().toLowerCase();\n return { price, isLoading, kind, verified, logo, initials, fdv, chain };\n}\n\nfunction PriceSkel({ wide }: { wide?: boolean }) {\n return <span className={cn(\"inline-block h-4 rounded bg-muted-foreground/20 animate-pulse\", wide ? \"w-16\" : \"w-12\")} />;\n}\n\nexport function CoinCard({ collection, usePrice, href }: CoinTileProps) {\n const m = useTileModel({ collection, usePrice });\n return (\n <Link href={href} className=\"flex flex-col rounded-xl border border-border/60 bg-card overflow-hidden transition-transform active:scale-[0.99]\">\n <div className=\"flex items-center gap-3 p-4\">\n <div className=\"relative h-12 w-12 shrink-0 rounded-full overflow-hidden bg-muted\">\n {m.logo ? (\n <Image src={m.logo} alt=\"\" fill sizes=\"48px\" className=\"object-cover\" unoptimized />\n ) : (\n <div className=\"flex h-full w-full items-center justify-center bg-gradient-to-br from-brand-blue to-brand-purple text-sm font-bold text-white\">{m.initials}</div>\n )}\n </div>\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex items-center gap-1\">\n <span className=\"truncate font-semibold\">{collection.name ?? \"Untitled coin\"}</span>\n {m.verified && <BadgeCheck className=\"h-4 w-4 shrink-0 text-primary\" aria-label=\"Verified\" />}\n </div>\n <span className=\"text-sm text-muted-foreground\">{collection.symbol ?? \"—\"}</span>\n </div>\n <span className={cn(\"shrink-0 rounded-full border px-2 py-0.5 text-[10px] font-medium\", KIND_TAG[m.kind])}>\n {m.kind === \"creator\" ? \"Creator Coin\" : \"Memecoin\"}\n </span>\n </div>\n <div className=\"grid grid-cols-3 gap-2 border-t border-border/60 px-4 py-3 text-sm\">\n <Stat label=\"Price\">\n {m.isLoading ? <PriceSkel /> : m.price ? <span className=\"font-semibold\">{formatCoinPrice(m.price.quotePerCoin)}</span> : <span className=\"text-muted-foreground\">—</span>}\n </Stat>\n <Stat label=\"FDV\">\n {m.isLoading ? <PriceSkel /> : <span className=\"font-semibold\">{m.fdv ?? \"—\"}</span>}\n </Stat>\n <Stat label=\"Holders\">\n <span className=\"inline-flex items-center gap-1 font-semibold\">\n <Users className=\"h-3 w-3 text-muted-foreground\" />{collection.holderCount || \"—\"}\n </span>\n </Stat>\n </div>\n <div className=\"flex items-center justify-between border-t border-border/60 px-4 py-2.5 text-sm\">\n {m.chain ? <span className=\"text-[10px] uppercase tracking-wide text-muted-foreground\">{m.chain}</span> : <span />}\n <span className=\"font-medium text-primary\">Trade</span>\n </div>\n </Link>\n );\n}\n\nexport function CoinRow({ collection, usePrice, href }: CoinTileProps) {\n const m = useTileModel({ collection, usePrice });\n return (\n <Link href={href} className=\"flex items-center gap-3 rounded-lg border border-border/60 bg-card px-3 py-2.5 transition-colors hover:border-primary/40\">\n <div className=\"relative h-9 w-9 shrink-0 overflow-hidden rounded-full bg-muted\">\n {m.logo ? <Image src={m.logo} alt=\"\" fill sizes=\"36px\" className=\"object-cover\" unoptimized /> :\n <div className=\"flex h-full w-full items-center justify-center bg-gradient-to-br from-brand-blue to-brand-purple text-[11px] font-bold text-white\">{m.initials}</div>}\n </div>\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex items-center gap-1\">\n <span className=\"truncate text-sm font-semibold\">{collection.name ?? \"Untitled coin\"}</span>\n {m.verified && <BadgeCheck className=\"h-3.5 w-3.5 shrink-0 text-primary\" aria-label=\"Verified\" />}\n </div>\n <span className=\"text-xs text-muted-foreground\">{collection.symbol ?? \"—\"}</span>\n </div>\n <span className={cn(\"hidden shrink-0 rounded-full border px-2 py-0.5 text-[10px] font-medium sm:inline-block\", KIND_TAG[m.kind])}>\n {m.kind === \"creator\" ? \"Creator Coin\" : \"Memecoin\"}\n </span>\n <span className=\"w-24 shrink-0 text-right text-sm font-semibold tabular-nums\">\n {m.isLoading ? <span className=\"ml-auto inline-block h-4 w-12 rounded bg-muted-foreground/20 animate-pulse\" /> : m.price ? formatCoinPrice(m.price.quotePerCoin) : <span className=\"text-muted-foreground\">—</span>}\n </span>\n <span className=\"hidden shrink-0 text-sm font-medium text-primary sm:inline-block\">Trade</span>\n </Link>\n );\n}\n\nfunction Stat({ label, children }: { label: string; children: React.ReactNode }) {\n return (\n <div className=\"flex flex-col gap-0.5\">\n <span className=\"text-[10px] uppercase tracking-wide text-muted-foreground\">{label}</span>\n {children}\n </div>\n );\n}\n\nexport function CoinCardSkeleton() {\n return (\n <div className=\"flex flex-col rounded-xl border border-border/60 bg-card overflow-hidden\">\n <div className=\"flex items-center gap-3 p-4\">\n <div className=\"h-12 w-12 rounded-full bg-muted animate-pulse\" />\n <div className=\"flex-1 space-y-1.5\">\n <div className=\"h-4 w-24 rounded bg-muted-foreground/20 animate-pulse\" />\n <div className=\"h-3 w-12 rounded bg-muted-foreground/20 animate-pulse\" />\n </div>\n </div>\n <div className=\"grid grid-cols-3 gap-2 border-t border-border/60 px-4 py-3\">\n {[0, 1, 2].map((i) => <div key={i} className=\"h-8 w-full rounded bg-muted-foreground/10 animate-pulse\" />)}\n </div>\n <div className=\"h-9 w-full bg-muted-foreground/10 animate-pulse\" />\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/coin-card.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * CoinCard / CoinRow — chain-agnostic, art-forward coin discovery tiles.\n *\n * Pure presentation: the price read (`usePrice`) and the link target (`href`)\n * are injected by the consuming app, and the collection is typed structurally\n * (CoinCollectionLike) — so a coin on Starknet, Ethereum, or Solana renders the\n * same with zero changes here. The coin's artwork is the hero; the whole tile\n * links to the coin page (where trading happens), and the only stat is the live\n * spot price with its quote-currency icon. FDV / holders / a \"verified\" mark are\n * intentionally not shown — Medialane indexes none of them.\n */\n\nimport Link from \"next/link\";\nimport Image from \"next/image\";\nimport { cn } from \"../utils/cn.js\";\nimport { ipfsToHttp } from \"../utils/ipfs.js\";\nimport { MotionCard } from \"./motion-primitives.js\";\nimport { CurrencyIcon } from \"./currency-icon.js\";\nimport {\n coinKind, formatCoinPrice,\n type CoinCollectionLike, type CoinPriceLike,\n} from \"../data/coins.js\";\n\n/** Injected per-chain spot-price read. `price` is null while loading/unknown. */\nexport type UseCoinPrice = (collection: CoinCollectionLike) => {\n price: CoinPriceLike | null;\n isLoading: boolean;\n};\n\nexport interface CoinTileProps {\n collection: CoinCollectionLike;\n usePrice: UseCoinPrice;\n /** Link target — internal coin page or the per-chain trading app. */\n href: string;\n}\n\n// Kind tag on a card surface (table row) — brand hues, never red.\nconst KIND_TAG: Record<string, string> = {\n creator: \"border-brand-blue/30 bg-brand-blue/10 text-brand-blue\",\n memecoin: \"border-brand-purple/30 bg-brand-purple/10 text-brand-purple\",\n};\n// Kind tag layered over artwork — solid brand pill, white text for legibility.\nconst KIND_OVER_IMAGE: Record<string, string> = {\n creator: \"bg-brand-blue/85 text-white\",\n memecoin: \"bg-brand-purple/85 text-white\",\n};\n// Art-fallback gradient (no logo) — vivid brand wash.\nconst KIND_FALLBACK: Record<string, string> = {\n creator: \"from-brand-blue to-brand-purple\",\n memecoin: \"from-brand-purple to-brand-rose\",\n};\n\nconst kindLabel = (kind: string) => (kind === \"creator\" ? \"Creator Coin\" : \"Memecoin\");\n\nfunction useTileModel({ collection, usePrice }: Omit<CoinTileProps, \"href\">) {\n const { price, isLoading } = usePrice(collection);\n const kind = coinKind(collection.service);\n const logoUri = collection.profile?.image ?? collection.image;\n const logo = logoUri ? ipfsToHttp(logoUri) : null;\n const initials = (collection.symbol ?? collection.name ?? \"?\").trim().slice(0, 2).toUpperCase();\n return { price, isLoading, kind, logo, initials };\n}\n\nfunction PriceValue({ price, isLoading }: { price: CoinPriceLike | null; isLoading: boolean }) {\n if (isLoading) return <span className=\"inline-block h-4 w-14 rounded bg-muted-foreground/20 animate-pulse\" />;\n if (!price) return <span className=\"text-muted-foreground\">—</span>;\n return (\n <span className=\"inline-flex items-center gap-1 font-semibold tabular-nums\">\n <CurrencyIcon symbol={price.quoteSymbol} size={14} />\n {formatCoinPrice(price.quotePerCoin)}\n </span>\n );\n}\n\nexport function CoinCard({ collection, usePrice, href }: CoinTileProps) {\n const m = useTileModel({ collection, usePrice });\n return (\n <MotionCard className=\"card-base group relative flex h-full flex-col\">\n <Link href={href} className=\"flex h-full flex-col\">\n {/* Hero artwork */}\n <div className=\"relative aspect-square w-full overflow-hidden bg-muted\">\n {m.logo ? (\n <Image\n src={m.logo}\n alt={collection.name ?? \"Coin\"}\n fill\n sizes=\"(min-width:1280px) 25vw, (min-width:1024px) 33vw, (min-width:640px) 50vw, 100vw\"\n className=\"object-cover transition-transform duration-500 group-hover:scale-105\"\n unoptimized\n />\n ) : (\n <div className={cn(\"absolute inset-0 flex items-center justify-center bg-gradient-to-br\", KIND_FALLBACK[m.kind])}>\n <span className=\"select-none text-5xl font-black tracking-tighter text-white/90\">{m.initials}</span>\n </div>\n )}\n <span className={cn(\"absolute left-2 top-2 rounded-full px-2 py-0.5 text-[10px] font-semibold backdrop-blur-md\", KIND_OVER_IMAGE[m.kind])}>\n {kindLabel(m.kind)}\n </span>\n </div>\n\n {/* Body */}\n <div className=\"flex flex-col gap-2 p-3\">\n <div className=\"min-w-0\">\n <div className=\"truncate font-semibold leading-tight\">{collection.name ?? \"Untitled coin\"}</div>\n <div className=\"truncate text-sm text-muted-foreground\">{collection.symbol ?? \"—\"}</div>\n </div>\n <div className=\"flex items-baseline justify-between gap-2\">\n <span className=\"text-[10px] uppercase tracking-wide text-muted-foreground\">Price</span>\n <PriceValue price={m.price} isLoading={m.isLoading} />\n </div>\n </div>\n </Link>\n </MotionCard>\n );\n}\n\nexport function CoinRow({ collection, usePrice, href }: CoinTileProps) {\n const m = useTileModel({ collection, usePrice });\n return (\n <Link href={href} className=\"flex items-center gap-3 rounded-lg border border-border/60 bg-card px-3 py-2.5 transition-colors hover:border-primary/40 active:scale-[0.99]\">\n <div className=\"relative h-9 w-9 shrink-0 overflow-hidden rounded-full bg-muted\">\n {m.logo ? <Image src={m.logo} alt=\"\" fill sizes=\"36px\" className=\"object-cover\" unoptimized /> :\n <div className={cn(\"flex h-full w-full items-center justify-center bg-gradient-to-br text-[11px] font-bold text-white\", KIND_FALLBACK[m.kind])}>{m.initials}</div>}\n </div>\n <div className=\"min-w-0 flex-1\">\n <span className=\"truncate text-sm font-semibold\">{collection.name ?? \"Untitled coin\"}</span>\n <span className=\"block truncate text-xs text-muted-foreground\">{collection.symbol ?? \"—\"}</span>\n </div>\n <span className={cn(\"hidden shrink-0 rounded-full border px-2 py-0.5 text-[10px] font-medium sm:inline-block\", KIND_TAG[m.kind])}>\n {kindLabel(m.kind)}\n </span>\n <span className=\"w-28 shrink-0 text-right text-sm\">\n <PriceValue price={m.price} isLoading={m.isLoading} />\n </span>\n </Link>\n );\n}\n\nexport function CoinCardSkeleton() {\n return (\n <div className=\"card-base flex flex-col\">\n <div className=\"aspect-square w-full animate-pulse bg-muted\" />\n <div className=\"flex flex-col gap-2 p-3\">\n <div className=\"space-y-1.5\">\n <div className=\"h-4 w-24 rounded bg-muted-foreground/20 animate-pulse\" />\n <div className=\"h-3 w-12 rounded bg-muted-foreground/20 animate-pulse\" />\n </div>\n <div className=\"h-4 w-full rounded bg-muted-foreground/10 animate-pulse\" />\n </div>\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -36,11 +36,21 @@ const SORT_OPTIONS = [
|
|
|
36
36
|
{ label: "Recently launched", value: "recent" },
|
|
37
37
|
{ label: "Name", value: "name" }
|
|
38
38
|
];
|
|
39
|
+
const filterLabel = (v) => FILTER_TABS.find((t) => t.value === v)?.label ?? "";
|
|
40
|
+
const sortLabel = (v) => SORT_OPTIONS.find((o) => o.value === v)?.label ?? "";
|
|
39
41
|
function CoinsExplorer({ useCoins, usePrice, coinHref, heading = true }) {
|
|
40
42
|
const [filter, setFilter] = (0, import_react.useState)("all");
|
|
41
43
|
const [sort, setSort] = (0, import_react.useState)("recent");
|
|
42
44
|
const [view, setView] = (0, import_react.useState)("grid");
|
|
43
45
|
const [query, setQuery] = (0, import_react.useState)("");
|
|
46
|
+
const [filtersOpen, setFiltersOpen] = (0, import_react.useState)(false);
|
|
47
|
+
const filterCount = (filter !== "all" ? 1 : 0) + (sort !== "recent" ? 1 : 0);
|
|
48
|
+
(0, import_react.useEffect)(() => {
|
|
49
|
+
if (!filtersOpen) return;
|
|
50
|
+
const onKey = (e) => e.key === "Escape" && setFiltersOpen(false);
|
|
51
|
+
window.addEventListener("keydown", onKey);
|
|
52
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
53
|
+
}, [filtersOpen]);
|
|
44
54
|
const { collections, isLoading } = useCoins({ filter, sort });
|
|
45
55
|
const items = (0, import_react.useMemo)(() => {
|
|
46
56
|
const q = query.trim().toLowerCase();
|
|
@@ -55,59 +65,113 @@ function CoinsExplorer({ useCoins, usePrice, coinHref, heading = true }) {
|
|
|
55
65
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.Coins, { className: "h-5 w-5" }),
|
|
56
66
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "text-sm font-semibold uppercase tracking-wider", children: "Tokens" })
|
|
57
67
|
] }),
|
|
58
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h1", { className: "text-3xl font-bold", children: "Creator coins & memecoins" })
|
|
59
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "text-muted-foreground", children: "Discover creator-issued social tokens and claimed memecoins." })
|
|
68
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h1", { className: "text-3xl font-bold", children: "Creator coins & memecoins" })
|
|
60
69
|
] }),
|
|
61
70
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "space-y-3 border-b border-border/60 pb-3", children: [
|
|
62
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex
|
|
63
|
-
/* @__PURE__ */ (0, import_jsx_runtime.
|
|
64
|
-
"
|
|
65
|
-
{
|
|
66
|
-
onClick: () => setFilter(value),
|
|
67
|
-
className: (0, import_cn.cn)(
|
|
68
|
-
"rounded-lg border px-3 py-1.5 text-xs font-medium transition-colors",
|
|
69
|
-
filter === value ? "border-primary bg-primary/10 text-primary" : "border-border text-muted-foreground hover:border-primary/50 hover:text-foreground"
|
|
70
|
-
),
|
|
71
|
-
children: label
|
|
72
|
-
},
|
|
73
|
-
value
|
|
74
|
-
)) }),
|
|
75
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex items-center gap-2", children: [
|
|
71
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex items-center gap-2", children: [
|
|
72
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "relative flex-1", children: [
|
|
73
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.Search, { className: "pointer-events-none absolute left-3 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground" }),
|
|
76
74
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
77
|
-
"
|
|
75
|
+
"input",
|
|
78
76
|
{
|
|
79
|
-
value:
|
|
80
|
-
onChange: (e) =>
|
|
81
|
-
|
|
82
|
-
|
|
77
|
+
value: query,
|
|
78
|
+
onChange: (e) => setQuery(e.target.value),
|
|
79
|
+
placeholder: "Search coins by name or symbol\u2026",
|
|
80
|
+
className: "w-full rounded-lg border border-border bg-background py-2 pl-9 pr-3 text-sm outline-none focus:border-primary/50"
|
|
83
81
|
}
|
|
84
|
-
)
|
|
85
|
-
|
|
82
|
+
)
|
|
83
|
+
] }),
|
|
84
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
85
|
+
"button",
|
|
86
|
+
{
|
|
87
|
+
onClick: () => setFiltersOpen(true),
|
|
88
|
+
className: "inline-flex shrink-0 items-center gap-1.5 rounded-lg border border-border px-3 py-2 text-xs font-medium text-foreground hover:border-primary/50",
|
|
89
|
+
children: [
|
|
90
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.SlidersHorizontal, { className: "h-3.5 w-3.5" }),
|
|
91
|
+
"Filters",
|
|
92
|
+
filterCount > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "ml-0.5 inline-flex h-4 min-w-4 items-center justify-center rounded-full bg-primary px-1 text-[10px] font-semibold text-primary-foreground", children: filterCount })
|
|
93
|
+
]
|
|
94
|
+
}
|
|
95
|
+
),
|
|
96
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "inline-flex shrink-0 rounded-lg border border-border p-0.5", children: [{ v: "grid", Icon: import_lucide_react.LayoutGrid }, { v: "table", Icon: import_lucide_react.List }].map(({ v, Icon }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
97
|
+
"button",
|
|
98
|
+
{
|
|
99
|
+
onClick: () => setView(v),
|
|
100
|
+
"aria-label": v === "grid" ? "Grid view" : "Table view",
|
|
101
|
+
className: (0, import_cn.cn)("rounded-md p-1.5 transition-colors", view === v ? "bg-primary/10 text-primary" : "text-muted-foreground hover:text-foreground"),
|
|
102
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Icon, { className: "h-4 w-4" })
|
|
103
|
+
},
|
|
104
|
+
v
|
|
105
|
+
)) })
|
|
106
|
+
] }),
|
|
107
|
+
filterCount > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex flex-wrap items-center gap-1.5", children: [
|
|
108
|
+
filter !== "all" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Chip, { onClear: () => setFilter("all"), children: filterLabel(filter) }),
|
|
109
|
+
sort !== "recent" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Chip, { onClear: () => setSort("recent"), children: sortLabel(sort) })
|
|
110
|
+
] })
|
|
111
|
+
] }),
|
|
112
|
+
isLoading && items.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4", children: Array.from({ length: 8 }).map((_, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_coin_card.CoinCardSkeleton, {}, i)) }) : items.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "rounded-xl border border-border/60 py-16 text-center text-muted-foreground", children: query.trim() ? `No coins match "${query.trim()}".` : "No coins yet." }) : view === "table" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "space-y-2", children: items.map((c) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_coin_card.CoinRow, { collection: c, usePrice, href: coinHref(c) }, `${c.chain}-${c.contractAddress}`)) }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4", children: items.map((c) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_coin_card.CoinCard, { collection: c, usePrice, href: coinHref(c) }, `${c.chain}-${c.contractAddress}`)) }),
|
|
113
|
+
filtersOpen && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "fixed inset-0 z-50 flex items-center justify-center p-4", role: "dialog", "aria-modal": "true", "aria-label": "Filters", children: [
|
|
114
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "absolute inset-0 bg-background/70 backdrop-blur-sm", onClick: () => setFiltersOpen(false) }),
|
|
115
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "card-base relative z-10 w-full max-w-sm space-y-5 p-5", children: [
|
|
116
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex items-center justify-between", children: [
|
|
117
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("h2", { className: "flex items-center gap-2 text-base font-bold", children: [
|
|
118
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.SlidersHorizontal, { className: "h-4 w-4 text-primary" }),
|
|
119
|
+
"Filters"
|
|
120
|
+
] }),
|
|
121
|
+
filterCount > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
86
122
|
"button",
|
|
87
123
|
{
|
|
88
|
-
onClick: () =>
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
99
|
-
/* @__PURE__ */ (0, import_jsx_runtime.
|
|
100
|
-
"
|
|
124
|
+
onClick: () => {
|
|
125
|
+
setFilter("all");
|
|
126
|
+
setSort("recent");
|
|
127
|
+
},
|
|
128
|
+
className: "text-xs font-medium text-muted-foreground hover:text-foreground",
|
|
129
|
+
children: "Clear all"
|
|
130
|
+
}
|
|
131
|
+
)
|
|
132
|
+
] }),
|
|
133
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(FilterGroup, { label: "Type", children: FILTER_TABS.map(({ label, value }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PillButton, { active: filter === value, onClick: () => setFilter(value), children: label }, value)) }),
|
|
134
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(FilterGroup, { label: "Sort", children: SORT_OPTIONS.map(({ label, value }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PillButton, { active: sort === value, onClick: () => setSort(value), children: label }, value)) }),
|
|
135
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
136
|
+
"button",
|
|
101
137
|
{
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
138
|
+
onClick: () => setFiltersOpen(false),
|
|
139
|
+
className: "w-full rounded-lg bg-gradient-to-r from-brand-blue to-brand-purple py-2.5 text-sm font-semibold text-white",
|
|
140
|
+
children: [
|
|
141
|
+
"Show ",
|
|
142
|
+
items.length,
|
|
143
|
+
" ",
|
|
144
|
+
items.length === 1 ? "coin" : "coins"
|
|
145
|
+
]
|
|
106
146
|
}
|
|
107
147
|
)
|
|
108
148
|
] })
|
|
109
|
-
] })
|
|
110
|
-
|
|
149
|
+
] })
|
|
150
|
+
] });
|
|
151
|
+
}
|
|
152
|
+
function FilterGroup({ label, children }) {
|
|
153
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "space-y-2", children: [
|
|
154
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "text-[10px] font-semibold uppercase tracking-wider text-muted-foreground", children: label }),
|
|
155
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "flex flex-wrap gap-1.5", children })
|
|
156
|
+
] });
|
|
157
|
+
}
|
|
158
|
+
function PillButton({ active, onClick, children }) {
|
|
159
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
160
|
+
"button",
|
|
161
|
+
{
|
|
162
|
+
onClick,
|
|
163
|
+
className: (0, import_cn.cn)(
|
|
164
|
+
"rounded-lg border px-3 py-1.5 text-xs font-medium transition-colors",
|
|
165
|
+
active ? "border-primary bg-primary/10 text-primary" : "border-border text-muted-foreground hover:border-primary/50 hover:text-foreground"
|
|
166
|
+
),
|
|
167
|
+
children
|
|
168
|
+
}
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
function Chip({ children, onClear }) {
|
|
172
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "inline-flex items-center gap-1 rounded-full border border-primary/30 bg-primary/10 px-2.5 py-1 text-xs font-medium text-primary", children: [
|
|
173
|
+
children,
|
|
174
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { onClick: onClear, "aria-label": "Clear filter", className: "hover:text-primary/60", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.X, { className: "h-3 w-3" }) })
|
|
111
175
|
] });
|
|
112
176
|
}
|
|
113
177
|
// Annotate the CommonJS export names for ESM import in node:
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/coins-explorer.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * CoinsExplorer — the shared coin-discovery surface (chain-agnostic).\n *\n * Owns view/search/filter/sort UI only. The data source (`useCoins`), the price\n * read (`usePrice`), and the link target (`coinHref`) are injected, so each app\n * (and each chain) wires its own without forking this component.\n */\n\nimport { useState, useMemo } from \"react\";\nimport { Coins, LayoutGrid, List, Search } from \"lucide-react\";\nimport { cn } from \"../utils/cn.js\";\nimport { CoinCard, CoinRow, CoinCardSkeleton, type UseCoinPrice } from \"./coin-card.js\";\nimport type { CoinCollectionLike } from \"../data/coins.js\";\n\nexport type CoinFilter = \"all\" | \"creator\" | \"memecoin\";\nexport type CoinSort = \"recent\" | \"name\";\nexport type UseCoins = (opts: { filter: CoinFilter; sort: CoinSort }) => {\n collections: CoinCollectionLike[];\n isLoading: boolean;\n};\n\nexport interface CoinsExplorerProps {\n useCoins: UseCoins;\n usePrice: UseCoinPrice;\n /** Build the link target for a coin (internal or per-chain trading app). */\n coinHref: (collection: CoinCollectionLike) => string;\n heading?: boolean;\n}\n\nconst FILTER_TABS: { label: string; value: CoinFilter }[] = [\n { label: \"All\", value: \"all\" },\n { label: \"Creator Coins\", value: \"creator\" },\n { label: \"Memecoins\", value: \"memecoin\" },\n];\n\n// Recency default — never raw swap volume (05 §11 anti-wash hygiene).\nconst SORT_OPTIONS: { label: string; value: CoinSort }[] = [\n { label: \"Recently launched\", value: \"recent\" },\n { label: \"Name\", value: \"name\" },\n];\n\nexport function CoinsExplorer({ useCoins, usePrice, coinHref, heading = true }: CoinsExplorerProps) {\n const [filter, setFilter] = useState<CoinFilter>(\"all\");\n const [sort, setSort] = useState<CoinSort>(\"recent\");\n const [view, setView] = useState<\"grid\" | \"table\">(\"grid\");\n const [query, setQuery] = useState(\"\");\n\n const { collections, isLoading } = useCoins({ filter, sort });\n const items = useMemo(() => {\n const q = query.trim().toLowerCase();\n if (!q) return collections;\n return collections.filter(\n (c) => (c.name ?? \"\").toLowerCase().includes(q) || (c.symbol ?? \"\").toLowerCase().includes(q)\n );\n }, [collections, query]);\n\n return (\n <div className=\"space-y-6\">\n {heading && (\n <div className=\"space-y-2\">\n <div className=\"flex items-center gap-2 text-primary\">\n <Coins className=\"h-5 w-5\" />\n <span className=\"text-sm font-semibold uppercase tracking-wider\">Tokens</span>\n </div>\n <h1 className=\"text-3xl font-bold\">Creator coins & memecoins</h1>\n <p className=\"text-muted-foreground\">Discover creator-issued social tokens and claimed memecoins.</p>\n </div>\n )}\n\n <div className=\"space-y-3 border-b border-border/60 pb-3\">\n <div className=\"flex flex-wrap items-center justify-between gap-3\">\n <div className=\"flex gap-1.5\">\n {FILTER_TABS.map(({ label, value }) => (\n <button\n key={value}\n onClick={() => setFilter(value)}\n className={cn(\n \"rounded-lg border px-3 py-1.5 text-xs font-medium transition-colors\",\n filter === value\n ? \"border-primary bg-primary/10 text-primary\"\n : \"border-border text-muted-foreground hover:border-primary/50 hover:text-foreground\"\n )}\n >\n {label}\n </button>\n ))}\n </div>\n <div className=\"flex items-center gap-2\">\n <select\n value={sort}\n onChange={(e) => setSort(e.target.value as CoinSort)}\n className=\"rounded-lg border border-border bg-background px-3 py-1.5 text-xs font-medium text-foreground\"\n >\n {SORT_OPTIONS.map((o) => <option key={o.value} value={o.value}>{o.label}</option>)}\n </select>\n <div className=\"inline-flex rounded-lg border border-border p-0.5\">\n {([{ v: \"grid\", Icon: LayoutGrid }, { v: \"table\", Icon: List }] as const).map(({ v, Icon }) => (\n <button\n key={v}\n onClick={() => setView(v)}\n aria-label={v === \"grid\" ? \"Grid view\" : \"Table view\"}\n className={cn(\"rounded-md p-1.5 transition-colors\", view === v ? \"bg-primary/10 text-primary\" : \"text-muted-foreground hover:text-foreground\")}\n >\n <Icon className=\"h-4 w-4\" />\n </button>\n ))}\n </div>\n </div>\n </div>\n <div className=\"relative\">\n <Search className=\"pointer-events-none absolute left-3 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground\" />\n <input\n value={query}\n onChange={(e) => setQuery(e.target.value)}\n placeholder=\"Search coins by name or symbol…\"\n className=\"w-full rounded-lg border border-border bg-background py-2 pl-9 pr-3 text-sm outline-none focus:border-primary/50\"\n />\n </div>\n </div>\n\n {isLoading && items.length === 0 ? (\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4\">\n {Array.from({ length: 8 }).map((_, i) => <CoinCardSkeleton key={i} />)}\n </div>\n ) : items.length === 0 ? (\n <div className=\"rounded-xl border border-border/60 py-16 text-center text-muted-foreground\">\n {query.trim() ? `No coins match \"${query.trim()}\".` : \"No coins yet.\"}\n </div>\n ) : view === \"table\" ? (\n <div className=\"space-y-2\">\n {items.map((c) => <CoinRow key={`${c.chain}-${c.contractAddress}`} collection={c} usePrice={usePrice} href={coinHref(c)} />)}\n </div>\n ) : (\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4\">\n {items.map((c) => <CoinCard key={`${c.chain}-${c.contractAddress}`} collection={c} usePrice={usePrice} href={coinHref(c)} />)}\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/coins-explorer.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * CoinsExplorer — the shared coin-discovery surface (chain-agnostic).\n *\n * Owns view/search/filter/sort UI only. The data source (`useCoins`), the price\n * read (`usePrice`), and the link target (`coinHref`) are injected, so each app\n * (and each chain) wires its own without forking this component.\n *\n * Type + sort live in a centered Filters dialog (the \"action focus\" pattern, and\n * matching the marketplace page); the header stays to two lines.\n */\n\nimport { useState, useMemo, useEffect } from \"react\";\nimport { Coins, LayoutGrid, List, Search, SlidersHorizontal, X } from \"lucide-react\";\nimport { cn } from \"../utils/cn.js\";\nimport { CoinCard, CoinRow, CoinCardSkeleton, type UseCoinPrice } from \"./coin-card.js\";\nimport type { CoinCollectionLike } from \"../data/coins.js\";\n\nexport type CoinFilter = \"all\" | \"creator\" | \"memecoin\";\nexport type CoinSort = \"recent\" | \"name\";\nexport type UseCoins = (opts: { filter: CoinFilter; sort: CoinSort }) => {\n collections: CoinCollectionLike[];\n isLoading: boolean;\n};\n\nexport interface CoinsExplorerProps {\n useCoins: UseCoins;\n usePrice: UseCoinPrice;\n /** Build the link target for a coin (internal or per-chain trading app). */\n coinHref: (collection: CoinCollectionLike) => string;\n heading?: boolean;\n}\n\nconst FILTER_TABS: { label: string; value: CoinFilter }[] = [\n { label: \"All\", value: \"all\" },\n { label: \"Creator Coins\", value: \"creator\" },\n { label: \"Memecoins\", value: \"memecoin\" },\n];\n\n// Recency default — never raw swap volume (05 §11 anti-wash hygiene).\nconst SORT_OPTIONS: { label: string; value: CoinSort }[] = [\n { label: \"Recently launched\", value: \"recent\" },\n { label: \"Name\", value: \"name\" },\n];\n\nconst filterLabel = (v: CoinFilter) => FILTER_TABS.find((t) => t.value === v)?.label ?? \"\";\nconst sortLabel = (v: CoinSort) => SORT_OPTIONS.find((o) => o.value === v)?.label ?? \"\";\n\nexport function CoinsExplorer({ useCoins, usePrice, coinHref, heading = true }: CoinsExplorerProps) {\n const [filter, setFilter] = useState<CoinFilter>(\"all\");\n const [sort, setSort] = useState<CoinSort>(\"recent\");\n const [view, setView] = useState<\"grid\" | \"table\">(\"grid\");\n const [query, setQuery] = useState(\"\");\n const [filtersOpen, setFiltersOpen] = useState(false);\n\n const filterCount = (filter !== \"all\" ? 1 : 0) + (sort !== \"recent\" ? 1 : 0);\n\n useEffect(() => {\n if (!filtersOpen) return;\n const onKey = (e: KeyboardEvent) => e.key === \"Escape\" && setFiltersOpen(false);\n window.addEventListener(\"keydown\", onKey);\n return () => window.removeEventListener(\"keydown\", onKey);\n }, [filtersOpen]);\n\n const { collections, isLoading } = useCoins({ filter, sort });\n const items = useMemo(() => {\n const q = query.trim().toLowerCase();\n if (!q) return collections;\n return collections.filter(\n (c) => (c.name ?? \"\").toLowerCase().includes(q) || (c.symbol ?? \"\").toLowerCase().includes(q)\n );\n }, [collections, query]);\n\n return (\n <div className=\"space-y-6\">\n {heading && (\n <div className=\"space-y-2\">\n <div className=\"flex items-center gap-2 text-primary\">\n <Coins className=\"h-5 w-5\" />\n <span className=\"text-sm font-semibold uppercase tracking-wider\">Tokens</span>\n </div>\n <h1 className=\"text-3xl font-bold\">Creator coins & memecoins</h1>\n </div>\n )}\n\n {/* Controls — search + filters + view, kept lean. */}\n <div className=\"space-y-3 border-b border-border/60 pb-3\">\n <div className=\"flex items-center gap-2\">\n <div className=\"relative flex-1\">\n <Search className=\"pointer-events-none absolute left-3 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground\" />\n <input\n value={query}\n onChange={(e) => setQuery(e.target.value)}\n placeholder=\"Search coins by name or symbol…\"\n className=\"w-full rounded-lg border border-border bg-background py-2 pl-9 pr-3 text-sm outline-none focus:border-primary/50\"\n />\n </div>\n <button\n onClick={() => setFiltersOpen(true)}\n className=\"inline-flex shrink-0 items-center gap-1.5 rounded-lg border border-border px-3 py-2 text-xs font-medium text-foreground hover:border-primary/50\"\n >\n <SlidersHorizontal className=\"h-3.5 w-3.5\" />\n Filters\n {filterCount > 0 && (\n <span className=\"ml-0.5 inline-flex h-4 min-w-4 items-center justify-center rounded-full bg-primary px-1 text-[10px] font-semibold text-primary-foreground\">\n {filterCount}\n </span>\n )}\n </button>\n <div className=\"inline-flex shrink-0 rounded-lg border border-border p-0.5\">\n {([{ v: \"grid\", Icon: LayoutGrid }, { v: \"table\", Icon: List }] as const).map(({ v, Icon }) => (\n <button\n key={v}\n onClick={() => setView(v)}\n aria-label={v === \"grid\" ? \"Grid view\" : \"Table view\"}\n className={cn(\"rounded-md p-1.5 transition-colors\", view === v ? \"bg-primary/10 text-primary\" : \"text-muted-foreground hover:text-foreground\")}\n >\n <Icon className=\"h-4 w-4\" />\n </button>\n ))}\n </div>\n </div>\n\n {/* Active-filter chips */}\n {filterCount > 0 && (\n <div className=\"flex flex-wrap items-center gap-1.5\">\n {filter !== \"all\" && (\n <Chip onClear={() => setFilter(\"all\")}>{filterLabel(filter)}</Chip>\n )}\n {sort !== \"recent\" && (\n <Chip onClear={() => setSort(\"recent\")}>{sortLabel(sort)}</Chip>\n )}\n </div>\n )}\n </div>\n\n {isLoading && items.length === 0 ? (\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4\">\n {Array.from({ length: 8 }).map((_, i) => <CoinCardSkeleton key={i} />)}\n </div>\n ) : items.length === 0 ? (\n <div className=\"rounded-xl border border-border/60 py-16 text-center text-muted-foreground\">\n {query.trim() ? `No coins match \"${query.trim()}\".` : \"No coins yet.\"}\n </div>\n ) : view === \"table\" ? (\n <div className=\"space-y-2\">\n {items.map((c) => <CoinRow key={`${c.chain}-${c.contractAddress}`} collection={c} usePrice={usePrice} href={coinHref(c)} />)}\n </div>\n ) : (\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4\">\n {items.map((c) => <CoinCard key={`${c.chain}-${c.contractAddress}`} collection={c} usePrice={usePrice} href={coinHref(c)} />)}\n </div>\n )}\n\n {filtersOpen && (\n <div className=\"fixed inset-0 z-50 flex items-center justify-center p-4\" role=\"dialog\" aria-modal=\"true\" aria-label=\"Filters\">\n <div className=\"absolute inset-0 bg-background/70 backdrop-blur-sm\" onClick={() => setFiltersOpen(false)} />\n <div className=\"card-base relative z-10 w-full max-w-sm space-y-5 p-5\">\n <div className=\"flex items-center justify-between\">\n <h2 className=\"flex items-center gap-2 text-base font-bold\">\n <SlidersHorizontal className=\"h-4 w-4 text-primary\" />\n Filters\n </h2>\n {filterCount > 0 && (\n <button\n onClick={() => { setFilter(\"all\"); setSort(\"recent\"); }}\n className=\"text-xs font-medium text-muted-foreground hover:text-foreground\"\n >\n Clear all\n </button>\n )}\n </div>\n\n <FilterGroup label=\"Type\">\n {FILTER_TABS.map(({ label, value }) => (\n <PillButton key={value} active={filter === value} onClick={() => setFilter(value)}>{label}</PillButton>\n ))}\n </FilterGroup>\n\n <FilterGroup label=\"Sort\">\n {SORT_OPTIONS.map(({ label, value }) => (\n <PillButton key={value} active={sort === value} onClick={() => setSort(value)}>{label}</PillButton>\n ))}\n </FilterGroup>\n\n <button\n onClick={() => setFiltersOpen(false)}\n className=\"w-full rounded-lg bg-gradient-to-r from-brand-blue to-brand-purple py-2.5 text-sm font-semibold text-white\"\n >\n Show {items.length} {items.length === 1 ? \"coin\" : \"coins\"}\n </button>\n </div>\n </div>\n )}\n </div>\n );\n}\n\nfunction FilterGroup({ label, children }: { label: string; children: React.ReactNode }) {\n return (\n <div className=\"space-y-2\">\n <p className=\"text-[10px] font-semibold uppercase tracking-wider text-muted-foreground\">{label}</p>\n <div className=\"flex flex-wrap gap-1.5\">{children}</div>\n </div>\n );\n}\n\nfunction PillButton({ active, onClick, children }: { active: boolean; onClick: () => void; children: React.ReactNode }) {\n return (\n <button\n onClick={onClick}\n className={cn(\n \"rounded-lg border px-3 py-1.5 text-xs font-medium transition-colors\",\n active ? \"border-primary bg-primary/10 text-primary\" : \"border-border text-muted-foreground hover:border-primary/50 hover:text-foreground\"\n )}\n >\n {children}\n </button>\n );\n}\n\nfunction Chip({ children, onClear }: { children: React.ReactNode; onClear: () => void }) {\n return (\n <span className=\"inline-flex items-center gap-1 rounded-full border border-primary/30 bg-primary/10 px-2.5 py-1 text-xs font-medium text-primary\">\n {children}\n <button onClick={onClear} aria-label=\"Clear filter\" className=\"hover:text-primary/60\">\n <X className=\"h-3 w-3\" />\n </button>\n </span>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useState, useMemo } from "react";
|
|
4
|
-
import { Coins, LayoutGrid, List, Search } from "lucide-react";
|
|
3
|
+
import { useState, useMemo, useEffect } from "react";
|
|
4
|
+
import { Coins, LayoutGrid, List, Search, SlidersHorizontal, X } from "lucide-react";
|
|
5
5
|
import { cn } from "../utils/cn.js";
|
|
6
6
|
import { CoinCard, CoinRow, CoinCardSkeleton } from "./coin-card.js";
|
|
7
7
|
const FILTER_TABS = [
|
|
@@ -13,11 +13,21 @@ const SORT_OPTIONS = [
|
|
|
13
13
|
{ label: "Recently launched", value: "recent" },
|
|
14
14
|
{ label: "Name", value: "name" }
|
|
15
15
|
];
|
|
16
|
+
const filterLabel = (v) => FILTER_TABS.find((t) => t.value === v)?.label ?? "";
|
|
17
|
+
const sortLabel = (v) => SORT_OPTIONS.find((o) => o.value === v)?.label ?? "";
|
|
16
18
|
function CoinsExplorer({ useCoins, usePrice, coinHref, heading = true }) {
|
|
17
19
|
const [filter, setFilter] = useState("all");
|
|
18
20
|
const [sort, setSort] = useState("recent");
|
|
19
21
|
const [view, setView] = useState("grid");
|
|
20
22
|
const [query, setQuery] = useState("");
|
|
23
|
+
const [filtersOpen, setFiltersOpen] = useState(false);
|
|
24
|
+
const filterCount = (filter !== "all" ? 1 : 0) + (sort !== "recent" ? 1 : 0);
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
if (!filtersOpen) return;
|
|
27
|
+
const onKey = (e) => e.key === "Escape" && setFiltersOpen(false);
|
|
28
|
+
window.addEventListener("keydown", onKey);
|
|
29
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
30
|
+
}, [filtersOpen]);
|
|
21
31
|
const { collections, isLoading } = useCoins({ filter, sort });
|
|
22
32
|
const items = useMemo(() => {
|
|
23
33
|
const q = query.trim().toLowerCase();
|
|
@@ -32,59 +42,113 @@ function CoinsExplorer({ useCoins, usePrice, coinHref, heading = true }) {
|
|
|
32
42
|
/* @__PURE__ */ jsx(Coins, { className: "h-5 w-5" }),
|
|
33
43
|
/* @__PURE__ */ jsx("span", { className: "text-sm font-semibold uppercase tracking-wider", children: "Tokens" })
|
|
34
44
|
] }),
|
|
35
|
-
/* @__PURE__ */ jsx("h1", { className: "text-3xl font-bold", children: "Creator coins & memecoins" })
|
|
36
|
-
/* @__PURE__ */ jsx("p", { className: "text-muted-foreground", children: "Discover creator-issued social tokens and claimed memecoins." })
|
|
45
|
+
/* @__PURE__ */ jsx("h1", { className: "text-3xl font-bold", children: "Creator coins & memecoins" })
|
|
37
46
|
] }),
|
|
38
47
|
/* @__PURE__ */ jsxs("div", { className: "space-y-3 border-b border-border/60 pb-3", children: [
|
|
39
|
-
/* @__PURE__ */ jsxs("div", { className: "flex
|
|
40
|
-
/* @__PURE__ */
|
|
41
|
-
"
|
|
42
|
-
{
|
|
43
|
-
onClick: () => setFilter(value),
|
|
44
|
-
className: cn(
|
|
45
|
-
"rounded-lg border px-3 py-1.5 text-xs font-medium transition-colors",
|
|
46
|
-
filter === value ? "border-primary bg-primary/10 text-primary" : "border-border text-muted-foreground hover:border-primary/50 hover:text-foreground"
|
|
47
|
-
),
|
|
48
|
-
children: label
|
|
49
|
-
},
|
|
50
|
-
value
|
|
51
|
-
)) }),
|
|
52
|
-
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
48
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
49
|
+
/* @__PURE__ */ jsxs("div", { className: "relative flex-1", children: [
|
|
50
|
+
/* @__PURE__ */ jsx(Search, { className: "pointer-events-none absolute left-3 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground" }),
|
|
53
51
|
/* @__PURE__ */ jsx(
|
|
54
|
-
"
|
|
52
|
+
"input",
|
|
55
53
|
{
|
|
56
|
-
value:
|
|
57
|
-
onChange: (e) =>
|
|
58
|
-
|
|
59
|
-
|
|
54
|
+
value: query,
|
|
55
|
+
onChange: (e) => setQuery(e.target.value),
|
|
56
|
+
placeholder: "Search coins by name or symbol\u2026",
|
|
57
|
+
className: "w-full rounded-lg border border-border bg-background py-2 pl-9 pr-3 text-sm outline-none focus:border-primary/50"
|
|
60
58
|
}
|
|
61
|
-
)
|
|
62
|
-
|
|
59
|
+
)
|
|
60
|
+
] }),
|
|
61
|
+
/* @__PURE__ */ jsxs(
|
|
62
|
+
"button",
|
|
63
|
+
{
|
|
64
|
+
onClick: () => setFiltersOpen(true),
|
|
65
|
+
className: "inline-flex shrink-0 items-center gap-1.5 rounded-lg border border-border px-3 py-2 text-xs font-medium text-foreground hover:border-primary/50",
|
|
66
|
+
children: [
|
|
67
|
+
/* @__PURE__ */ jsx(SlidersHorizontal, { className: "h-3.5 w-3.5" }),
|
|
68
|
+
"Filters",
|
|
69
|
+
filterCount > 0 && /* @__PURE__ */ jsx("span", { className: "ml-0.5 inline-flex h-4 min-w-4 items-center justify-center rounded-full bg-primary px-1 text-[10px] font-semibold text-primary-foreground", children: filterCount })
|
|
70
|
+
]
|
|
71
|
+
}
|
|
72
|
+
),
|
|
73
|
+
/* @__PURE__ */ jsx("div", { className: "inline-flex shrink-0 rounded-lg border border-border p-0.5", children: [{ v: "grid", Icon: LayoutGrid }, { v: "table", Icon: List }].map(({ v, Icon }) => /* @__PURE__ */ jsx(
|
|
74
|
+
"button",
|
|
75
|
+
{
|
|
76
|
+
onClick: () => setView(v),
|
|
77
|
+
"aria-label": v === "grid" ? "Grid view" : "Table view",
|
|
78
|
+
className: cn("rounded-md p-1.5 transition-colors", view === v ? "bg-primary/10 text-primary" : "text-muted-foreground hover:text-foreground"),
|
|
79
|
+
children: /* @__PURE__ */ jsx(Icon, { className: "h-4 w-4" })
|
|
80
|
+
},
|
|
81
|
+
v
|
|
82
|
+
)) })
|
|
83
|
+
] }),
|
|
84
|
+
filterCount > 0 && /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-1.5", children: [
|
|
85
|
+
filter !== "all" && /* @__PURE__ */ jsx(Chip, { onClear: () => setFilter("all"), children: filterLabel(filter) }),
|
|
86
|
+
sort !== "recent" && /* @__PURE__ */ jsx(Chip, { onClear: () => setSort("recent"), children: sortLabel(sort) })
|
|
87
|
+
] })
|
|
88
|
+
] }),
|
|
89
|
+
isLoading && items.length === 0 ? /* @__PURE__ */ jsx("div", { className: "grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4", children: Array.from({ length: 8 }).map((_, i) => /* @__PURE__ */ jsx(CoinCardSkeleton, {}, i)) }) : items.length === 0 ? /* @__PURE__ */ jsx("div", { className: "rounded-xl border border-border/60 py-16 text-center text-muted-foreground", children: query.trim() ? `No coins match "${query.trim()}".` : "No coins yet." }) : view === "table" ? /* @__PURE__ */ jsx("div", { className: "space-y-2", children: items.map((c) => /* @__PURE__ */ jsx(CoinRow, { collection: c, usePrice, href: coinHref(c) }, `${c.chain}-${c.contractAddress}`)) }) : /* @__PURE__ */ jsx("div", { className: "grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4", children: items.map((c) => /* @__PURE__ */ jsx(CoinCard, { collection: c, usePrice, href: coinHref(c) }, `${c.chain}-${c.contractAddress}`)) }),
|
|
90
|
+
filtersOpen && /* @__PURE__ */ jsxs("div", { className: "fixed inset-0 z-50 flex items-center justify-center p-4", role: "dialog", "aria-modal": "true", "aria-label": "Filters", children: [
|
|
91
|
+
/* @__PURE__ */ jsx("div", { className: "absolute inset-0 bg-background/70 backdrop-blur-sm", onClick: () => setFiltersOpen(false) }),
|
|
92
|
+
/* @__PURE__ */ jsxs("div", { className: "card-base relative z-10 w-full max-w-sm space-y-5 p-5", children: [
|
|
93
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
|
|
94
|
+
/* @__PURE__ */ jsxs("h2", { className: "flex items-center gap-2 text-base font-bold", children: [
|
|
95
|
+
/* @__PURE__ */ jsx(SlidersHorizontal, { className: "h-4 w-4 text-primary" }),
|
|
96
|
+
"Filters"
|
|
97
|
+
] }),
|
|
98
|
+
filterCount > 0 && /* @__PURE__ */ jsx(
|
|
63
99
|
"button",
|
|
64
100
|
{
|
|
65
|
-
onClick: () =>
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
/* @__PURE__ */ jsx(
|
|
76
|
-
/* @__PURE__ */
|
|
77
|
-
"
|
|
101
|
+
onClick: () => {
|
|
102
|
+
setFilter("all");
|
|
103
|
+
setSort("recent");
|
|
104
|
+
},
|
|
105
|
+
className: "text-xs font-medium text-muted-foreground hover:text-foreground",
|
|
106
|
+
children: "Clear all"
|
|
107
|
+
}
|
|
108
|
+
)
|
|
109
|
+
] }),
|
|
110
|
+
/* @__PURE__ */ jsx(FilterGroup, { label: "Type", children: FILTER_TABS.map(({ label, value }) => /* @__PURE__ */ jsx(PillButton, { active: filter === value, onClick: () => setFilter(value), children: label }, value)) }),
|
|
111
|
+
/* @__PURE__ */ jsx(FilterGroup, { label: "Sort", children: SORT_OPTIONS.map(({ label, value }) => /* @__PURE__ */ jsx(PillButton, { active: sort === value, onClick: () => setSort(value), children: label }, value)) }),
|
|
112
|
+
/* @__PURE__ */ jsxs(
|
|
113
|
+
"button",
|
|
78
114
|
{
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
115
|
+
onClick: () => setFiltersOpen(false),
|
|
116
|
+
className: "w-full rounded-lg bg-gradient-to-r from-brand-blue to-brand-purple py-2.5 text-sm font-semibold text-white",
|
|
117
|
+
children: [
|
|
118
|
+
"Show ",
|
|
119
|
+
items.length,
|
|
120
|
+
" ",
|
|
121
|
+
items.length === 1 ? "coin" : "coins"
|
|
122
|
+
]
|
|
83
123
|
}
|
|
84
124
|
)
|
|
85
125
|
] })
|
|
86
|
-
] })
|
|
87
|
-
|
|
126
|
+
] })
|
|
127
|
+
] });
|
|
128
|
+
}
|
|
129
|
+
function FilterGroup({ label, children }) {
|
|
130
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
|
|
131
|
+
/* @__PURE__ */ jsx("p", { className: "text-[10px] font-semibold uppercase tracking-wider text-muted-foreground", children: label }),
|
|
132
|
+
/* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-1.5", children })
|
|
133
|
+
] });
|
|
134
|
+
}
|
|
135
|
+
function PillButton({ active, onClick, children }) {
|
|
136
|
+
return /* @__PURE__ */ jsx(
|
|
137
|
+
"button",
|
|
138
|
+
{
|
|
139
|
+
onClick,
|
|
140
|
+
className: cn(
|
|
141
|
+
"rounded-lg border px-3 py-1.5 text-xs font-medium transition-colors",
|
|
142
|
+
active ? "border-primary bg-primary/10 text-primary" : "border-border text-muted-foreground hover:border-primary/50 hover:text-foreground"
|
|
143
|
+
),
|
|
144
|
+
children
|
|
145
|
+
}
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
function Chip({ children, onClear }) {
|
|
149
|
+
return /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1 rounded-full border border-primary/30 bg-primary/10 px-2.5 py-1 text-xs font-medium text-primary", children: [
|
|
150
|
+
children,
|
|
151
|
+
/* @__PURE__ */ jsx("button", { onClick: onClear, "aria-label": "Clear filter", className: "hover:text-primary/60", children: /* @__PURE__ */ jsx(X, { className: "h-3 w-3" }) })
|
|
88
152
|
] });
|
|
89
153
|
}
|
|
90
154
|
export {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/coins-explorer.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * CoinsExplorer — the shared coin-discovery surface (chain-agnostic).\n *\n * Owns view/search/filter/sort UI only. The data source (`useCoins`), the price\n * read (`usePrice`), and the link target (`coinHref`) are injected, so each app\n * (and each chain) wires its own without forking this component.\n */\n\nimport { useState, useMemo } from \"react\";\nimport { Coins, LayoutGrid, List, Search } from \"lucide-react\";\nimport { cn } from \"../utils/cn.js\";\nimport { CoinCard, CoinRow, CoinCardSkeleton, type UseCoinPrice } from \"./coin-card.js\";\nimport type { CoinCollectionLike } from \"../data/coins.js\";\n\nexport type CoinFilter = \"all\" | \"creator\" | \"memecoin\";\nexport type CoinSort = \"recent\" | \"name\";\nexport type UseCoins = (opts: { filter: CoinFilter; sort: CoinSort }) => {\n collections: CoinCollectionLike[];\n isLoading: boolean;\n};\n\nexport interface CoinsExplorerProps {\n useCoins: UseCoins;\n usePrice: UseCoinPrice;\n /** Build the link target for a coin (internal or per-chain trading app). */\n coinHref: (collection: CoinCollectionLike) => string;\n heading?: boolean;\n}\n\nconst FILTER_TABS: { label: string; value: CoinFilter }[] = [\n { label: \"All\", value: \"all\" },\n { label: \"Creator Coins\", value: \"creator\" },\n { label: \"Memecoins\", value: \"memecoin\" },\n];\n\n// Recency default — never raw swap volume (05 §11 anti-wash hygiene).\nconst SORT_OPTIONS: { label: string; value: CoinSort }[] = [\n { label: \"Recently launched\", value: \"recent\" },\n { label: \"Name\", value: \"name\" },\n];\n\nexport function CoinsExplorer({ useCoins, usePrice, coinHref, heading = true }: CoinsExplorerProps) {\n const [filter, setFilter] = useState<CoinFilter>(\"all\");\n const [sort, setSort] = useState<CoinSort>(\"recent\");\n const [view, setView] = useState<\"grid\" | \"table\">(\"grid\");\n const [query, setQuery] = useState(\"\");\n\n const { collections, isLoading } = useCoins({ filter, sort });\n const items = useMemo(() => {\n const q = query.trim().toLowerCase();\n if (!q) return collections;\n return collections.filter(\n (c) => (c.name ?? \"\").toLowerCase().includes(q) || (c.symbol ?? \"\").toLowerCase().includes(q)\n );\n }, [collections, query]);\n\n return (\n <div className=\"space-y-6\">\n {heading && (\n <div className=\"space-y-2\">\n <div className=\"flex items-center gap-2 text-primary\">\n <Coins className=\"h-5 w-5\" />\n <span className=\"text-sm font-semibold uppercase tracking-wider\">Tokens</span>\n </div>\n <h1 className=\"text-3xl font-bold\">Creator coins & memecoins</h1>\n <p className=\"text-muted-foreground\">Discover creator-issued social tokens and claimed memecoins.</p>\n </div>\n )}\n\n <div className=\"space-y-3 border-b border-border/60 pb-3\">\n <div className=\"flex flex-wrap items-center justify-between gap-3\">\n <div className=\"flex gap-1.5\">\n {FILTER_TABS.map(({ label, value }) => (\n <button\n key={value}\n onClick={() => setFilter(value)}\n className={cn(\n \"rounded-lg border px-3 py-1.5 text-xs font-medium transition-colors\",\n filter === value\n ? \"border-primary bg-primary/10 text-primary\"\n : \"border-border text-muted-foreground hover:border-primary/50 hover:text-foreground\"\n )}\n >\n {label}\n </button>\n ))}\n </div>\n <div className=\"flex items-center gap-2\">\n <select\n value={sort}\n onChange={(e) => setSort(e.target.value as CoinSort)}\n className=\"rounded-lg border border-border bg-background px-3 py-1.5 text-xs font-medium text-foreground\"\n >\n {SORT_OPTIONS.map((o) => <option key={o.value} value={o.value}>{o.label}</option>)}\n </select>\n <div className=\"inline-flex rounded-lg border border-border p-0.5\">\n {([{ v: \"grid\", Icon: LayoutGrid }, { v: \"table\", Icon: List }] as const).map(({ v, Icon }) => (\n <button\n key={v}\n onClick={() => setView(v)}\n aria-label={v === \"grid\" ? \"Grid view\" : \"Table view\"}\n className={cn(\"rounded-md p-1.5 transition-colors\", view === v ? \"bg-primary/10 text-primary\" : \"text-muted-foreground hover:text-foreground\")}\n >\n <Icon className=\"h-4 w-4\" />\n </button>\n ))}\n </div>\n </div>\n </div>\n <div className=\"relative\">\n <Search className=\"pointer-events-none absolute left-3 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground\" />\n <input\n value={query}\n onChange={(e) => setQuery(e.target.value)}\n placeholder=\"Search coins by name or symbol…\"\n className=\"w-full rounded-lg border border-border bg-background py-2 pl-9 pr-3 text-sm outline-none focus:border-primary/50\"\n />\n </div>\n </div>\n\n {isLoading && items.length === 0 ? (\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4\">\n {Array.from({ length: 8 }).map((_, i) => <CoinCardSkeleton key={i} />)}\n </div>\n ) : items.length === 0 ? (\n <div className=\"rounded-xl border border-border/60 py-16 text-center text-muted-foreground\">\n {query.trim() ? `No coins match \"${query.trim()}\".` : \"No coins yet.\"}\n </div>\n ) : view === \"table\" ? (\n <div className=\"space-y-2\">\n {items.map((c) => <CoinRow key={`${c.chain}-${c.contractAddress}`} collection={c} usePrice={usePrice} href={coinHref(c)} />)}\n </div>\n ) : (\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4\">\n {items.map((c) => <CoinCard key={`${c.chain}-${c.contractAddress}`} collection={c} usePrice={usePrice} href={coinHref(c)} />)}\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/coins-explorer.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * CoinsExplorer — the shared coin-discovery surface (chain-agnostic).\n *\n * Owns view/search/filter/sort UI only. The data source (`useCoins`), the price\n * read (`usePrice`), and the link target (`coinHref`) are injected, so each app\n * (and each chain) wires its own without forking this component.\n *\n * Type + sort live in a centered Filters dialog (the \"action focus\" pattern, and\n * matching the marketplace page); the header stays to two lines.\n */\n\nimport { useState, useMemo, useEffect } from \"react\";\nimport { Coins, LayoutGrid, List, Search, SlidersHorizontal, X } from \"lucide-react\";\nimport { cn } from \"../utils/cn.js\";\nimport { CoinCard, CoinRow, CoinCardSkeleton, type UseCoinPrice } from \"./coin-card.js\";\nimport type { CoinCollectionLike } from \"../data/coins.js\";\n\nexport type CoinFilter = \"all\" | \"creator\" | \"memecoin\";\nexport type CoinSort = \"recent\" | \"name\";\nexport type UseCoins = (opts: { filter: CoinFilter; sort: CoinSort }) => {\n collections: CoinCollectionLike[];\n isLoading: boolean;\n};\n\nexport interface CoinsExplorerProps {\n useCoins: UseCoins;\n usePrice: UseCoinPrice;\n /** Build the link target for a coin (internal or per-chain trading app). */\n coinHref: (collection: CoinCollectionLike) => string;\n heading?: boolean;\n}\n\nconst FILTER_TABS: { label: string; value: CoinFilter }[] = [\n { label: \"All\", value: \"all\" },\n { label: \"Creator Coins\", value: \"creator\" },\n { label: \"Memecoins\", value: \"memecoin\" },\n];\n\n// Recency default — never raw swap volume (05 §11 anti-wash hygiene).\nconst SORT_OPTIONS: { label: string; value: CoinSort }[] = [\n { label: \"Recently launched\", value: \"recent\" },\n { label: \"Name\", value: \"name\" },\n];\n\nconst filterLabel = (v: CoinFilter) => FILTER_TABS.find((t) => t.value === v)?.label ?? \"\";\nconst sortLabel = (v: CoinSort) => SORT_OPTIONS.find((o) => o.value === v)?.label ?? \"\";\n\nexport function CoinsExplorer({ useCoins, usePrice, coinHref, heading = true }: CoinsExplorerProps) {\n const [filter, setFilter] = useState<CoinFilter>(\"all\");\n const [sort, setSort] = useState<CoinSort>(\"recent\");\n const [view, setView] = useState<\"grid\" | \"table\">(\"grid\");\n const [query, setQuery] = useState(\"\");\n const [filtersOpen, setFiltersOpen] = useState(false);\n\n const filterCount = (filter !== \"all\" ? 1 : 0) + (sort !== \"recent\" ? 1 : 0);\n\n useEffect(() => {\n if (!filtersOpen) return;\n const onKey = (e: KeyboardEvent) => e.key === \"Escape\" && setFiltersOpen(false);\n window.addEventListener(\"keydown\", onKey);\n return () => window.removeEventListener(\"keydown\", onKey);\n }, [filtersOpen]);\n\n const { collections, isLoading } = useCoins({ filter, sort });\n const items = useMemo(() => {\n const q = query.trim().toLowerCase();\n if (!q) return collections;\n return collections.filter(\n (c) => (c.name ?? \"\").toLowerCase().includes(q) || (c.symbol ?? \"\").toLowerCase().includes(q)\n );\n }, [collections, query]);\n\n return (\n <div className=\"space-y-6\">\n {heading && (\n <div className=\"space-y-2\">\n <div className=\"flex items-center gap-2 text-primary\">\n <Coins className=\"h-5 w-5\" />\n <span className=\"text-sm font-semibold uppercase tracking-wider\">Tokens</span>\n </div>\n <h1 className=\"text-3xl font-bold\">Creator coins & memecoins</h1>\n </div>\n )}\n\n {/* Controls — search + filters + view, kept lean. */}\n <div className=\"space-y-3 border-b border-border/60 pb-3\">\n <div className=\"flex items-center gap-2\">\n <div className=\"relative flex-1\">\n <Search className=\"pointer-events-none absolute left-3 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground\" />\n <input\n value={query}\n onChange={(e) => setQuery(e.target.value)}\n placeholder=\"Search coins by name or symbol…\"\n className=\"w-full rounded-lg border border-border bg-background py-2 pl-9 pr-3 text-sm outline-none focus:border-primary/50\"\n />\n </div>\n <button\n onClick={() => setFiltersOpen(true)}\n className=\"inline-flex shrink-0 items-center gap-1.5 rounded-lg border border-border px-3 py-2 text-xs font-medium text-foreground hover:border-primary/50\"\n >\n <SlidersHorizontal className=\"h-3.5 w-3.5\" />\n Filters\n {filterCount > 0 && (\n <span className=\"ml-0.5 inline-flex h-4 min-w-4 items-center justify-center rounded-full bg-primary px-1 text-[10px] font-semibold text-primary-foreground\">\n {filterCount}\n </span>\n )}\n </button>\n <div className=\"inline-flex shrink-0 rounded-lg border border-border p-0.5\">\n {([{ v: \"grid\", Icon: LayoutGrid }, { v: \"table\", Icon: List }] as const).map(({ v, Icon }) => (\n <button\n key={v}\n onClick={() => setView(v)}\n aria-label={v === \"grid\" ? \"Grid view\" : \"Table view\"}\n className={cn(\"rounded-md p-1.5 transition-colors\", view === v ? \"bg-primary/10 text-primary\" : \"text-muted-foreground hover:text-foreground\")}\n >\n <Icon className=\"h-4 w-4\" />\n </button>\n ))}\n </div>\n </div>\n\n {/* Active-filter chips */}\n {filterCount > 0 && (\n <div className=\"flex flex-wrap items-center gap-1.5\">\n {filter !== \"all\" && (\n <Chip onClear={() => setFilter(\"all\")}>{filterLabel(filter)}</Chip>\n )}\n {sort !== \"recent\" && (\n <Chip onClear={() => setSort(\"recent\")}>{sortLabel(sort)}</Chip>\n )}\n </div>\n )}\n </div>\n\n {isLoading && items.length === 0 ? (\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4\">\n {Array.from({ length: 8 }).map((_, i) => <CoinCardSkeleton key={i} />)}\n </div>\n ) : items.length === 0 ? (\n <div className=\"rounded-xl border border-border/60 py-16 text-center text-muted-foreground\">\n {query.trim() ? `No coins match \"${query.trim()}\".` : \"No coins yet.\"}\n </div>\n ) : view === \"table\" ? (\n <div className=\"space-y-2\">\n {items.map((c) => <CoinRow key={`${c.chain}-${c.contractAddress}`} collection={c} usePrice={usePrice} href={coinHref(c)} />)}\n </div>\n ) : (\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4\">\n {items.map((c) => <CoinCard key={`${c.chain}-${c.contractAddress}`} collection={c} usePrice={usePrice} href={coinHref(c)} />)}\n </div>\n )}\n\n {filtersOpen && (\n <div className=\"fixed inset-0 z-50 flex items-center justify-center p-4\" role=\"dialog\" aria-modal=\"true\" aria-label=\"Filters\">\n <div className=\"absolute inset-0 bg-background/70 backdrop-blur-sm\" onClick={() => setFiltersOpen(false)} />\n <div className=\"card-base relative z-10 w-full max-w-sm space-y-5 p-5\">\n <div className=\"flex items-center justify-between\">\n <h2 className=\"flex items-center gap-2 text-base font-bold\">\n <SlidersHorizontal className=\"h-4 w-4 text-primary\" />\n Filters\n </h2>\n {filterCount > 0 && (\n <button\n onClick={() => { setFilter(\"all\"); setSort(\"recent\"); }}\n className=\"text-xs font-medium text-muted-foreground hover:text-foreground\"\n >\n Clear all\n </button>\n )}\n </div>\n\n <FilterGroup label=\"Type\">\n {FILTER_TABS.map(({ label, value }) => (\n <PillButton key={value} active={filter === value} onClick={() => setFilter(value)}>{label}</PillButton>\n ))}\n </FilterGroup>\n\n <FilterGroup label=\"Sort\">\n {SORT_OPTIONS.map(({ label, value }) => (\n <PillButton key={value} active={sort === value} onClick={() => setSort(value)}>{label}</PillButton>\n ))}\n </FilterGroup>\n\n <button\n onClick={() => setFiltersOpen(false)}\n className=\"w-full rounded-lg bg-gradient-to-r from-brand-blue to-brand-purple py-2.5 text-sm font-semibold text-white\"\n >\n Show {items.length} {items.length === 1 ? \"coin\" : \"coins\"}\n </button>\n </div>\n </div>\n )}\n </div>\n );\n}\n\nfunction FilterGroup({ label, children }: { label: string; children: React.ReactNode }) {\n return (\n <div className=\"space-y-2\">\n <p className=\"text-[10px] font-semibold uppercase tracking-wider text-muted-foreground\">{label}</p>\n <div className=\"flex flex-wrap gap-1.5\">{children}</div>\n </div>\n );\n}\n\nfunction PillButton({ active, onClick, children }: { active: boolean; onClick: () => void; children: React.ReactNode }) {\n return (\n <button\n onClick={onClick}\n className={cn(\n \"rounded-lg border px-3 py-1.5 text-xs font-medium transition-colors\",\n active ? \"border-primary bg-primary/10 text-primary\" : \"border-border text-muted-foreground hover:border-primary/50 hover:text-foreground\"\n )}\n >\n {children}\n </button>\n );\n}\n\nfunction Chip({ children, onClear }: { children: React.ReactNode; onClear: () => void }) {\n return (\n <span className=\"inline-flex items-center gap-1 rounded-full border border-primary/30 bg-primary/10 px-2.5 py-1 text-xs font-medium text-primary\">\n {children}\n <button onClick={onClear} aria-label=\"Clear filter\" className=\"hover:text-primary/60\">\n <X className=\"h-3 w-3\" />\n </button>\n </span>\n );\n}\n"],"mappings":";AA8EU,SACE,KADF;AAjEV,SAAS,UAAU,SAAS,iBAAiB;AAC7C,SAAS,OAAO,YAAY,MAAM,QAAQ,mBAAmB,SAAS;AACtE,SAAS,UAAU;AACnB,SAAS,UAAU,SAAS,wBAA2C;AAkBvE,MAAM,cAAsD;AAAA,EAC1D,EAAE,OAAO,OAAO,OAAO,MAAM;AAAA,EAC7B,EAAE,OAAO,iBAAiB,OAAO,UAAU;AAAA,EAC3C,EAAE,OAAO,aAAa,OAAO,WAAW;AAC1C;AAGA,MAAM,eAAqD;AAAA,EACzD,EAAE,OAAO,qBAAqB,OAAO,SAAS;AAAA,EAC9C,EAAE,OAAO,QAAQ,OAAO,OAAO;AACjC;AAEA,MAAM,cAAc,CAAC,MAAkB,YAAY,KAAK,CAAC,MAAM,EAAE,UAAU,CAAC,GAAG,SAAS;AACxF,MAAM,YAAY,CAAC,MAAgB,aAAa,KAAK,CAAC,MAAM,EAAE,UAAU,CAAC,GAAG,SAAS;AAE9E,SAAS,cAAc,EAAE,UAAU,UAAU,UAAU,UAAU,KAAK,GAAuB;AAClG,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAqB,KAAK;AACtD,QAAM,CAAC,MAAM,OAAO,IAAI,SAAmB,QAAQ;AACnD,QAAM,CAAC,MAAM,OAAO,IAAI,SAA2B,MAAM;AACzD,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAS,EAAE;AACrC,QAAM,CAAC,aAAa,cAAc,IAAI,SAAS,KAAK;AAEpD,QAAM,eAAe,WAAW,QAAQ,IAAI,MAAM,SAAS,WAAW,IAAI;AAE1E,YAAU,MAAM;AACd,QAAI,CAAC,YAAa;AAClB,UAAM,QAAQ,CAAC,MAAqB,EAAE,QAAQ,YAAY,eAAe,KAAK;AAC9E,WAAO,iBAAiB,WAAW,KAAK;AACxC,WAAO,MAAM,OAAO,oBAAoB,WAAW,KAAK;AAAA,EAC1D,GAAG,CAAC,WAAW,CAAC;AAEhB,QAAM,EAAE,aAAa,UAAU,IAAI,SAAS,EAAE,QAAQ,KAAK,CAAC;AAC5D,QAAM,QAAQ,QAAQ,MAAM;AAC1B,UAAM,IAAI,MAAM,KAAK,EAAE,YAAY;AACnC,QAAI,CAAC,EAAG,QAAO;AACf,WAAO,YAAY;AAAA,MACjB,CAAC,OAAO,EAAE,QAAQ,IAAI,YAAY,EAAE,SAAS,CAAC,MAAM,EAAE,UAAU,IAAI,YAAY,EAAE,SAAS,CAAC;AAAA,IAC9F;AAAA,EACF,GAAG,CAAC,aAAa,KAAK,CAAC;AAEvB,SACE,qBAAC,SAAI,WAAU,aACZ;AAAA,eACC,qBAAC,SAAI,WAAU,aACb;AAAA,2BAAC,SAAI,WAAU,wCACb;AAAA,4BAAC,SAAM,WAAU,WAAU;AAAA,QAC3B,oBAAC,UAAK,WAAU,kDAAiD,oBAAM;AAAA,SACzE;AAAA,MACA,oBAAC,QAAG,WAAU,sBAAqB,uCAA6B;AAAA,OAClE;AAAA,IAIF,qBAAC,SAAI,WAAU,4CACb;AAAA,2BAAC,SAAI,WAAU,2BACb;AAAA,6BAAC,SAAI,WAAU,mBACb;AAAA,8BAAC,UAAO,WAAU,kGAAiG;AAAA,UACnH;AAAA,YAAC;AAAA;AAAA,cACC,OAAO;AAAA,cACP,UAAU,CAAC,MAAM,SAAS,EAAE,OAAO,KAAK;AAAA,cACxC,aAAY;AAAA,cACZ,WAAU;AAAA;AAAA,UACZ;AAAA,WACF;AAAA,QACA;AAAA,UAAC;AAAA;AAAA,YACC,SAAS,MAAM,eAAe,IAAI;AAAA,YAClC,WAAU;AAAA,YAEV;AAAA,kCAAC,qBAAkB,WAAU,eAAc;AAAA,cAAE;AAAA,cAE5C,cAAc,KACb,oBAAC,UAAK,WAAU,6IACb,uBACH;AAAA;AAAA;AAAA,QAEJ;AAAA,QACA,oBAAC,SAAI,WAAU,8DACX,WAAC,EAAE,GAAG,QAAQ,MAAM,WAAW,GAAG,EAAE,GAAG,SAAS,MAAM,KAAK,CAAC,EAAY,IAAI,CAAC,EAAE,GAAG,KAAK,MACvF;AAAA,UAAC;AAAA;AAAA,YAEC,SAAS,MAAM,QAAQ,CAAC;AAAA,YACxB,cAAY,MAAM,SAAS,cAAc;AAAA,YACzC,WAAW,GAAG,sCAAsC,SAAS,IAAI,+BAA+B,6CAA6C;AAAA,YAE7I,8BAAC,QAAK,WAAU,WAAU;AAAA;AAAA,UALrB;AAAA,QAMP,CACD,GACH;AAAA,SACF;AAAA,MAGC,cAAc,KACb,qBAAC,SAAI,WAAU,uCACZ;AAAA,mBAAW,SACV,oBAAC,QAAK,SAAS,MAAM,UAAU,KAAK,GAAI,sBAAY,MAAM,GAAE;AAAA,QAE7D,SAAS,YACR,oBAAC,QAAK,SAAS,MAAM,QAAQ,QAAQ,GAAI,oBAAU,IAAI,GAAE;AAAA,SAE7D;AAAA,OAEJ;AAAA,IAEC,aAAa,MAAM,WAAW,IAC7B,oBAAC,SAAI,WAAU,uEACZ,gBAAM,KAAK,EAAE,QAAQ,EAAE,CAAC,EAAE,IAAI,CAAC,GAAG,MAAM,oBAAC,sBAAsB,CAAG,CAAE,GACvE,IACE,MAAM,WAAW,IACnB,oBAAC,SAAI,WAAU,8EACZ,gBAAM,KAAK,IAAI,mBAAmB,MAAM,KAAK,CAAC,OAAO,iBACxD,IACE,SAAS,UACX,oBAAC,SAAI,WAAU,aACZ,gBAAM,IAAI,CAAC,MAAM,oBAAC,WAAgD,YAAY,GAAG,UAAoB,MAAM,SAAS,CAAC,KAAtF,GAAG,EAAE,KAAK,IAAI,EAAE,eAAe,EAA0D,CAAE,GAC7H,IAEA,oBAAC,SAAI,WAAU,uEACZ,gBAAM,IAAI,CAAC,MAAM,oBAAC,YAAiD,YAAY,GAAG,UAAoB,MAAM,SAAS,CAAC,KAAtF,GAAG,EAAE,KAAK,IAAI,EAAE,eAAe,EAA0D,CAAE,GAC9H;AAAA,IAGD,eACC,qBAAC,SAAI,WAAU,2DAA0D,MAAK,UAAS,cAAW,QAAO,cAAW,WAClH;AAAA,0BAAC,SAAI,WAAU,sDAAqD,SAAS,MAAM,eAAe,KAAK,GAAG;AAAA,MAC1G,qBAAC,SAAI,WAAU,yDACb;AAAA,6BAAC,SAAI,WAAU,qCACb;AAAA,+BAAC,QAAG,WAAU,+CACZ;AAAA,gCAAC,qBAAkB,WAAU,wBAAuB;AAAA,YAAE;AAAA,aAExD;AAAA,UACC,cAAc,KACb;AAAA,YAAC;AAAA;AAAA,cACC,SAAS,MAAM;AAAE,0BAAU,KAAK;AAAG,wBAAQ,QAAQ;AAAA,cAAG;AAAA,cACtD,WAAU;AAAA,cACX;AAAA;AAAA,UAED;AAAA,WAEJ;AAAA,QAEA,oBAAC,eAAY,OAAM,QAChB,sBAAY,IAAI,CAAC,EAAE,OAAO,MAAM,MAC/B,oBAAC,cAAuB,QAAQ,WAAW,OAAO,SAAS,MAAM,UAAU,KAAK,GAAI,mBAAnE,KAAyE,CAC3F,GACH;AAAA,QAEA,oBAAC,eAAY,OAAM,QAChB,uBAAa,IAAI,CAAC,EAAE,OAAO,MAAM,MAChC,oBAAC,cAAuB,QAAQ,SAAS,OAAO,SAAS,MAAM,QAAQ,KAAK,GAAI,mBAA/D,KAAqE,CACvF,GACH;AAAA,QAEA;AAAA,UAAC;AAAA;AAAA,YACC,SAAS,MAAM,eAAe,KAAK;AAAA,YACnC,WAAU;AAAA,YACX;AAAA;AAAA,cACO,MAAM;AAAA,cAAO;AAAA,cAAE,MAAM,WAAW,IAAI,SAAS;AAAA;AAAA;AAAA,QACrD;AAAA,SACF;AAAA,OACF;AAAA,KAEJ;AAEJ;AAEA,SAAS,YAAY,EAAE,OAAO,SAAS,GAAiD;AACtF,SACE,qBAAC,SAAI,WAAU,aACb;AAAA,wBAAC,OAAE,WAAU,4EAA4E,iBAAM;AAAA,IAC/F,oBAAC,SAAI,WAAU,0BAA0B,UAAS;AAAA,KACpD;AAEJ;AAEA,SAAS,WAAW,EAAE,QAAQ,SAAS,SAAS,GAAwE;AACtH,SACE;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA,WAAW;AAAA,QACT;AAAA,QACA,SAAS,8CAA8C;AAAA,MACzD;AAAA,MAEC;AAAA;AAAA,EACH;AAEJ;AAEA,SAAS,KAAK,EAAE,UAAU,QAAQ,GAAuD;AACvF,SACE,qBAAC,UAAK,WAAU,mIACb;AAAA;AAAA,IACD,oBAAC,YAAO,SAAS,SAAS,cAAW,gBAAe,WAAU,yBAC5D,8BAAC,KAAE,WAAU,WAAU,GACzB;AAAA,KACF;AAEJ;","names":[]}
|