@medialane/ui 0.112.0 → 0.114.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/asset-card.cjs +15 -4
- package/dist/components/asset-card.cjs.map +1 -1
- package/dist/components/asset-card.d.cts +6 -0
- package/dist/components/asset-card.d.ts +6 -0
- package/dist/components/asset-card.js +17 -6
- package/dist/components/asset-card.js.map +1 -1
- package/dist/components/listing-card.cjs +43 -17
- package/dist/components/listing-card.cjs.map +1 -1
- package/dist/components/listing-card.js +43 -17
- package/dist/components/listing-card.js.map +1 -1
- package/package.json +1 -1
|
@@ -83,10 +83,21 @@ function AssetCard({
|
|
|
83
83
|
] }) })
|
|
84
84
|
}
|
|
85
85
|
),
|
|
86
|
-
hasPrice && !indexing &&
|
|
87
|
-
|
|
88
|
-
(0, import_format.
|
|
89
|
-
|
|
86
|
+
hasPrice && !indexing && (() => {
|
|
87
|
+
const cryptoDisplay = (0, import_format.formatDisplayPrice)(price.formatted);
|
|
88
|
+
const stable = (0, import_format.isStableCurrency)(price.currency);
|
|
89
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "absolute bottom-2 right-2 inline-flex items-center gap-1.5 text-sm font-bold text-white/95 backdrop-blur-md bg-black/40 rounded-full pl-3 pr-3 py-1.5 tabular-nums", children: price.usdValue ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
90
|
+
price.usdValue,
|
|
91
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "text-white/30", children: "\xB7" }),
|
|
92
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "inline-flex items-center gap-1 font-semibold text-white/65", children: [
|
|
93
|
+
price.currency && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_currency_icon.CurrencyIcon, { symbol: price.currency, size: 12 }),
|
|
94
|
+
stable ? price.currency : cryptoDisplay
|
|
95
|
+
] })
|
|
96
|
+
] }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
97
|
+
price.currency && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_currency_icon.CurrencyIcon, { symbol: price.currency, size: 13 }),
|
|
98
|
+
cryptoDisplay
|
|
99
|
+
] }) });
|
|
100
|
+
})(),
|
|
90
101
|
indexing && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "absolute bottom-0 inset-x-0 flex items-center justify-center gap-1.5 bg-black/50 backdrop-blur-sm py-1.5", children: [
|
|
91
102
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.Loader2, { className: "h-3 w-3 animate-spin text-white/70" }),
|
|
92
103
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "text-2xs text-white/70", children: "Indexing\u2026" })
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/asset-card.tsx"],"sourcesContent":["\"use client\";\n\nimport Link from \"next/link\";\nimport { Loader2 } from \"lucide-react\";\nimport { cn } from \"../utils/cn.js\";\nimport { ipfsToHttp } from \"../utils/ipfs.js\";\nimport { formatDisplayPrice } from \"../utils/format.js\";\nimport { CurrencyIcon } from \"./currency-icon.js\";\nimport { IpTypeBadge } from \"./ip-type-badge.js\";\nimport { AnimatedTokenMedia } from \"./animated-token-media.js\";\n\nexport interface AssetCardPrice {\n formatted?: string | null;\n currency?: string | null;\n}\n\nexport interface AssetCardProps {\n /** Asset detail link. */\n href: string;\n /** Asset title. */\n name: string;\n /** Raw image (ipfs:// or http) — resolved internally. */\n image?: string | null;\n /** Resolved animation_url — the live on-chain renderer, if any. */\n animationUrl?: string | null;\n /** Caller-computed eligibility for the living-render treatment. */\n live?: boolean;\n /** Secondary line under the title (collection or description). */\n subtitle?: string | null;\n /** IP type (apiValue or label) — renders the icon badge on the footer left. */\n ipType?: string | null;\n /** Listing price; omit/null shows no price. */\n price?: AssetCardPrice | null;\n /** Shown when the image is missing/errors (e.g. token id). */\n fallbackId?: string | null;\n /** Show the \"Indexing…\" overlay. */\n indexing?: boolean;\n /** Base URL for the ipType badge link. Default \"\" (relative). */\n ipTypeBaseUrl?: string;\n className?: string;\n}\n\n/**\n * The platform's shared asset card — lean and presentational.\n *\n * Intentionally has no action buttons, dropdowns or dialogs: the image and\n * text are the only interactive surface (a single asset link), which keeps\n * browse grids and carousels fast even when many cards render. Surfaces that\n * need owner/marketplace actions use the richer `TokenCard` / `ListingCard`.\n *\n * Layout: inset 4:5 artwork (gallery ratio, echoing the Medialane Collection\n * Card) with the price as a glass pill on the artwork (the collection cards'\n * Floor-pill vocabulary) → one body row: display-face title + optional\n * subtitle, with the optional ipType badge inline on the right.\n */\nexport function AssetCard({\n href,\n name,\n image,\n animationUrl,\n live = false,\n subtitle,\n ipType,\n price,\n fallbackId,\n indexing = false,\n ipTypeBaseUrl = \"\",\n className,\n}: AssetCardProps) {\n const resolved = image ? ipfsToHttp(image) : null;\n const hasPrice = !!price?.formatted;\n\n return (\n <div\n className={cn(\n \"card-base group relative flex flex-col w-full transition-colors hover:border-foreground/20\",\n className\n )}\n >\n {/* Artwork — inset with a gallery 4:5 ratio, like the Collection Card */}\n <Link href={href} className=\"block p-1.5 pb-0\">\n <div className=\"relative aspect-[4/5] rounded-[12px] bg-muted overflow-hidden ring-1 ring-border/50\">\n <AnimatedTokenMedia\n image={resolved}\n animationUrl={animationUrl}\n live={live}\n alt={name}\n mode=\"fill\"\n sizes=\"(max-width: 640px) 50vw, (max-width: 1024px) 33vw, 22vw\"\n className=\"object-cover transition-transform duration-500 group-hover:scale-105\"\n fallback={\n <div className=\"absolute inset-0 flex items-center justify-center bg-gradient-to-br from-brand-blue/25 via-brand-purple/25 to-brand-rose/25\">\n <span className=\"text-sm font-semibold text-muted-foreground tabular-nums\">\n #{fallbackId ?? \"?\"}\n </span>\n </div>\n }\n />\n\n {/* Price pill — anchored on the artwork, same glass chip as the\n collection cards' Floor pill */}\n {hasPrice && !indexing && (\n <span className=\"absolute bottom-2 right-2 inline-flex items-center gap-1 text-
|
|
1
|
+
{"version":3,"sources":["../../src/components/asset-card.tsx"],"sourcesContent":["\"use client\";\n\nimport Link from \"next/link\";\nimport { Loader2 } from \"lucide-react\";\nimport { cn } from \"../utils/cn.js\";\nimport { ipfsToHttp } from \"../utils/ipfs.js\";\nimport { formatDisplayPrice, isStableCurrency } from \"../utils/format.js\";\nimport { CurrencyIcon } from \"./currency-icon.js\";\nimport { IpTypeBadge } from \"./ip-type-badge.js\";\nimport { AnimatedTokenMedia } from \"./animated-token-media.js\";\n\nexport interface AssetCardPrice {\n formatted?: string | null;\n currency?: string | null;\n /**\n * Pre-formatted USD equivalent (e.g. \"$13.15\"), computed by the host from\n * its own live rate feed — this package has no price-feed access by\n * design. Omit/null renders the crypto amount alone (today's behavior).\n */\n usdValue?: string | null;\n}\n\nexport interface AssetCardProps {\n /** Asset detail link. */\n href: string;\n /** Asset title. */\n name: string;\n /** Raw image (ipfs:// or http) — resolved internally. */\n image?: string | null;\n /** Resolved animation_url — the live on-chain renderer, if any. */\n animationUrl?: string | null;\n /** Caller-computed eligibility for the living-render treatment. */\n live?: boolean;\n /** Secondary line under the title (collection or description). */\n subtitle?: string | null;\n /** IP type (apiValue or label) — renders the icon badge on the footer left. */\n ipType?: string | null;\n /** Listing price; omit/null shows no price. */\n price?: AssetCardPrice | null;\n /** Shown when the image is missing/errors (e.g. token id). */\n fallbackId?: string | null;\n /** Show the \"Indexing…\" overlay. */\n indexing?: boolean;\n /** Base URL for the ipType badge link. Default \"\" (relative). */\n ipTypeBaseUrl?: string;\n className?: string;\n}\n\n/**\n * The platform's shared asset card — lean and presentational.\n *\n * Intentionally has no action buttons, dropdowns or dialogs: the image and\n * text are the only interactive surface (a single asset link), which keeps\n * browse grids and carousels fast even when many cards render. Surfaces that\n * need owner/marketplace actions use the richer `TokenCard` / `ListingCard`.\n *\n * Layout: inset 4:5 artwork (gallery ratio, echoing the Medialane Collection\n * Card) with the price as a glass pill on the artwork (the collection cards'\n * Floor-pill vocabulary) → one body row: display-face title + optional\n * subtitle, with the optional ipType badge inline on the right.\n */\nexport function AssetCard({\n href,\n name,\n image,\n animationUrl,\n live = false,\n subtitle,\n ipType,\n price,\n fallbackId,\n indexing = false,\n ipTypeBaseUrl = \"\",\n className,\n}: AssetCardProps) {\n const resolved = image ? ipfsToHttp(image) : null;\n const hasPrice = !!price?.formatted;\n\n return (\n <div\n className={cn(\n \"card-base group relative flex flex-col w-full transition-colors hover:border-foreground/20\",\n className\n )}\n >\n {/* Artwork — inset with a gallery 4:5 ratio, like the Collection Card */}\n <Link href={href} className=\"block p-1.5 pb-0\">\n <div className=\"relative aspect-[4/5] rounded-[12px] bg-muted overflow-hidden ring-1 ring-border/50\">\n <AnimatedTokenMedia\n image={resolved}\n animationUrl={animationUrl}\n live={live}\n alt={name}\n mode=\"fill\"\n sizes=\"(max-width: 640px) 50vw, (max-width: 1024px) 33vw, 22vw\"\n className=\"object-cover transition-transform duration-500 group-hover:scale-105\"\n fallback={\n <div className=\"absolute inset-0 flex items-center justify-center bg-gradient-to-br from-brand-blue/25 via-brand-purple/25 to-brand-rose/25\">\n <span className=\"text-sm font-semibold text-muted-foreground tabular-nums\">\n #{fallbackId ?? \"?\"}\n </span>\n </div>\n }\n />\n\n {/* Price pill — anchored on the artwork, same glass chip as the\n collection cards' Floor pill. Fiat leads when a live rate is\n available; the on-chain amount trails, dimmer, as a secondary\n fact — never a second full-weight number competing for\n attention. Stablecoins collapse to fiat + symbol alone, since\n the crypto amount would just repeat the same figure. */}\n {hasPrice && !indexing && (() => {\n const cryptoDisplay = formatDisplayPrice(price!.formatted!);\n const stable = isStableCurrency(price!.currency);\n return (\n <span className=\"absolute bottom-2 right-2 inline-flex items-center gap-1.5 text-sm font-bold text-white/95 backdrop-blur-md bg-black/40 rounded-full pl-3 pr-3 py-1.5 tabular-nums\">\n {price!.usdValue ? (\n <>\n {price!.usdValue}\n <span className=\"text-white/30\">·</span>\n <span className=\"inline-flex items-center gap-1 font-semibold text-white/65\">\n {price!.currency && <CurrencyIcon symbol={price!.currency} size={12} />}\n {stable ? price!.currency : cryptoDisplay}\n </span>\n </>\n ) : (\n <>\n {price!.currency && <CurrencyIcon symbol={price!.currency} size={13} />}\n {cryptoDisplay}\n </>\n )}\n </span>\n );\n })()}\n\n {indexing && (\n <div className=\"absolute bottom-0 inset-x-0 flex items-center justify-center gap-1.5 bg-black/50 backdrop-blur-sm py-1.5\">\n <Loader2 className=\"h-3 w-3 animate-spin text-white/70\" />\n <span className=\"text-2xs text-white/70\">Indexing…</span>\n </div>\n )}\n </div>\n </Link>\n\n {/* Body — one tight block: title (+ optional ipType badge inline) and subtitle */}\n <div className=\"flex items-start justify-between gap-2 px-3 py-3\">\n <Link href={href} className=\"block min-w-0 flex-1\">\n <p className=\"text-base font-bold line-clamp-1 leading-snug\">\n {name}\n </p>\n {subtitle && (\n <p className=\"text-xs text-muted-foreground line-clamp-1 leading-snug\">\n {subtitle}\n </p>\n )}\n </Link>\n\n {ipType && (\n <div className=\"shrink-0 mt-0.5\">\n <IpTypeBadge ipType={ipType} size=\"sm\" baseUrl={ipTypeBaseUrl} />\n </div>\n )}\n </div>\n </div>\n );\n}\n\nexport function AssetCardSkeleton() {\n return (\n <div className=\"card-base flex flex-col w-full\">\n <div className=\"p-1.5 pb-0\">\n <div className=\"aspect-[4/5] w-full animate-pulse bg-muted rounded-[12px]\" />\n </div>\n <div className=\"px-3 py-3 space-y-1.5\">\n <div className=\"h-4 w-3/4 rounded-md animate-pulse bg-muted\" />\n <div className=\"h-3.5 w-2/5 rounded-md animate-pulse bg-muted\" />\n </div>\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAiGc;AA/Fd,kBAAiB;AACjB,0BAAwB;AACxB,gBAAmB;AACnB,kBAA2B;AAC3B,oBAAqD;AACrD,2BAA6B;AAC7B,2BAA4B;AAC5B,kCAAmC;AAoD5B,SAAS,UAAU;AAAA,EACxB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,OAAO;AAAA,EACP;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,WAAW;AAAA,EACX,gBAAgB;AAAA,EAChB;AACF,GAAmB;AACjB,QAAM,WAAW,YAAQ,wBAAW,KAAK,IAAI;AAC7C,QAAM,WAAW,CAAC,CAAC,OAAO;AAE1B,SACE;AAAA,IAAC;AAAA;AAAA,MACC,eAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MAGA;AAAA,oDAAC,YAAAA,SAAA,EAAK,MAAY,WAAU,oBAC1B,uDAAC,SAAI,WAAU,uFACb;AAAA;AAAA,YAAC;AAAA;AAAA,cACC,OAAO;AAAA,cACP;AAAA,cACA;AAAA,cACA,KAAK;AAAA,cACL,MAAK;AAAA,cACL,OAAM;AAAA,cACN,WAAU;AAAA,cACV,UACE,4CAAC,SAAI,WAAU,+HACb,uDAAC,UAAK,WAAU,4DAA2D;AAAA;AAAA,gBACvE,cAAc;AAAA,iBAClB,GACF;AAAA;AAAA,UAEJ;AAAA,UAQC,YAAY,CAAC,aAAa,MAAM;AAC/B,kBAAM,oBAAgB,kCAAmB,MAAO,SAAU;AAC1D,kBAAM,aAAS,gCAAiB,MAAO,QAAQ;AAC/C,mBACE,4CAAC,UAAK,WAAU,sKACb,gBAAO,WACN,4EACG;AAAA,oBAAO;AAAA,cACR,4CAAC,UAAK,WAAU,iBAAgB,kBAAC;AAAA,cACjC,6CAAC,UAAK,WAAU,8DACb;AAAA,sBAAO,YAAY,4CAAC,qCAAa,QAAQ,MAAO,UAAU,MAAM,IAAI;AAAA,gBACpE,SAAS,MAAO,WAAW;AAAA,iBAC9B;AAAA,eACF,IAEA,4EACG;AAAA,oBAAO,YAAY,4CAAC,qCAAa,QAAQ,MAAO,UAAU,MAAM,IAAI;AAAA,cACpE;AAAA,eACH,GAEJ;AAAA,UAEJ,GAAG;AAAA,UAEF,YACC,6CAAC,SAAI,WAAU,4GACb;AAAA,wDAAC,+BAAQ,WAAU,sCAAqC;AAAA,YACxD,4CAAC,UAAK,WAAU,0BAAyB,4BAAS;AAAA,aACpD;AAAA,WAEJ,GACF;AAAA,QAGA,6CAAC,SAAI,WAAU,oDACb;AAAA,uDAAC,YAAAA,SAAA,EAAK,MAAY,WAAU,wBAC1B;AAAA,wDAAC,OAAE,WAAU,iDACV,gBACH;AAAA,YACC,YACC,4CAAC,OAAE,WAAU,2DACV,oBACH;AAAA,aAEJ;AAAA,UAEC,UACC,4CAAC,SAAI,WAAU,mBACb,sDAAC,oCAAY,QAAgB,MAAK,MAAK,SAAS,eAAe,GACjE;AAAA,WAEJ;AAAA;AAAA;AAAA,EACF;AAEJ;AAEO,SAAS,oBAAoB;AAClC,SACE,6CAAC,SAAI,WAAU,kCACb;AAAA,gDAAC,SAAI,WAAU,cACb,sDAAC,SAAI,WAAU,6DAA4D,GAC7E;AAAA,IACA,6CAAC,SAAI,WAAU,yBACb;AAAA,kDAAC,SAAI,WAAU,+CAA8C;AAAA,MAC7D,4CAAC,SAAI,WAAU,iDAAgD;AAAA,OACjE;AAAA,KACF;AAEJ;","names":["Link"]}
|
|
@@ -3,6 +3,12 @@ import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
|
3
3
|
interface AssetCardPrice {
|
|
4
4
|
formatted?: string | null;
|
|
5
5
|
currency?: string | null;
|
|
6
|
+
/**
|
|
7
|
+
* Pre-formatted USD equivalent (e.g. "$13.15"), computed by the host from
|
|
8
|
+
* its own live rate feed — this package has no price-feed access by
|
|
9
|
+
* design. Omit/null renders the crypto amount alone (today's behavior).
|
|
10
|
+
*/
|
|
11
|
+
usdValue?: string | null;
|
|
6
12
|
}
|
|
7
13
|
interface AssetCardProps {
|
|
8
14
|
/** Asset detail link. */
|
|
@@ -3,6 +3,12 @@ import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
|
3
3
|
interface AssetCardPrice {
|
|
4
4
|
formatted?: string | null;
|
|
5
5
|
currency?: string | null;
|
|
6
|
+
/**
|
|
7
|
+
* Pre-formatted USD equivalent (e.g. "$13.15"), computed by the host from
|
|
8
|
+
* its own live rate feed — this package has no price-feed access by
|
|
9
|
+
* design. Omit/null renders the crypto amount alone (today's behavior).
|
|
10
|
+
*/
|
|
11
|
+
usdValue?: string | null;
|
|
6
12
|
}
|
|
7
13
|
interface AssetCardProps {
|
|
8
14
|
/** Asset detail link. */
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import Link from "next/link";
|
|
4
4
|
import { Loader2 } from "lucide-react";
|
|
5
5
|
import { cn } from "../utils/cn.js";
|
|
6
6
|
import { ipfsToHttp } from "../utils/ipfs.js";
|
|
7
|
-
import { formatDisplayPrice } from "../utils/format.js";
|
|
7
|
+
import { formatDisplayPrice, isStableCurrency } from "../utils/format.js";
|
|
8
8
|
import { CurrencyIcon } from "./currency-icon.js";
|
|
9
9
|
import { IpTypeBadge } from "./ip-type-badge.js";
|
|
10
10
|
import { AnimatedTokenMedia } from "./animated-token-media.js";
|
|
@@ -49,10 +49,21 @@ function AssetCard({
|
|
|
49
49
|
] }) })
|
|
50
50
|
}
|
|
51
51
|
),
|
|
52
|
-
hasPrice && !indexing &&
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
52
|
+
hasPrice && !indexing && (() => {
|
|
53
|
+
const cryptoDisplay = formatDisplayPrice(price.formatted);
|
|
54
|
+
const stable = isStableCurrency(price.currency);
|
|
55
|
+
return /* @__PURE__ */ jsx("span", { className: "absolute bottom-2 right-2 inline-flex items-center gap-1.5 text-sm font-bold text-white/95 backdrop-blur-md bg-black/40 rounded-full pl-3 pr-3 py-1.5 tabular-nums", children: price.usdValue ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
56
|
+
price.usdValue,
|
|
57
|
+
/* @__PURE__ */ jsx("span", { className: "text-white/30", children: "\xB7" }),
|
|
58
|
+
/* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1 font-semibold text-white/65", children: [
|
|
59
|
+
price.currency && /* @__PURE__ */ jsx(CurrencyIcon, { symbol: price.currency, size: 12 }),
|
|
60
|
+
stable ? price.currency : cryptoDisplay
|
|
61
|
+
] })
|
|
62
|
+
] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
63
|
+
price.currency && /* @__PURE__ */ jsx(CurrencyIcon, { symbol: price.currency, size: 13 }),
|
|
64
|
+
cryptoDisplay
|
|
65
|
+
] }) });
|
|
66
|
+
})(),
|
|
56
67
|
indexing && /* @__PURE__ */ jsxs("div", { className: "absolute bottom-0 inset-x-0 flex items-center justify-center gap-1.5 bg-black/50 backdrop-blur-sm py-1.5", children: [
|
|
57
68
|
/* @__PURE__ */ jsx(Loader2, { className: "h-3 w-3 animate-spin text-white/70" }),
|
|
58
69
|
/* @__PURE__ */ jsx("span", { className: "text-2xs text-white/70", children: "Indexing\u2026" })
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/asset-card.tsx"],"sourcesContent":["\"use client\";\n\nimport Link from \"next/link\";\nimport { Loader2 } from \"lucide-react\";\nimport { cn } from \"../utils/cn.js\";\nimport { ipfsToHttp } from \"../utils/ipfs.js\";\nimport { formatDisplayPrice } from \"../utils/format.js\";\nimport { CurrencyIcon } from \"./currency-icon.js\";\nimport { IpTypeBadge } from \"./ip-type-badge.js\";\nimport { AnimatedTokenMedia } from \"./animated-token-media.js\";\n\nexport interface AssetCardPrice {\n formatted?: string | null;\n currency?: string | null;\n}\n\nexport interface AssetCardProps {\n /** Asset detail link. */\n href: string;\n /** Asset title. */\n name: string;\n /** Raw image (ipfs:// or http) — resolved internally. */\n image?: string | null;\n /** Resolved animation_url — the live on-chain renderer, if any. */\n animationUrl?: string | null;\n /** Caller-computed eligibility for the living-render treatment. */\n live?: boolean;\n /** Secondary line under the title (collection or description). */\n subtitle?: string | null;\n /** IP type (apiValue or label) — renders the icon badge on the footer left. */\n ipType?: string | null;\n /** Listing price; omit/null shows no price. */\n price?: AssetCardPrice | null;\n /** Shown when the image is missing/errors (e.g. token id). */\n fallbackId?: string | null;\n /** Show the \"Indexing…\" overlay. */\n indexing?: boolean;\n /** Base URL for the ipType badge link. Default \"\" (relative). */\n ipTypeBaseUrl?: string;\n className?: string;\n}\n\n/**\n * The platform's shared asset card — lean and presentational.\n *\n * Intentionally has no action buttons, dropdowns or dialogs: the image and\n * text are the only interactive surface (a single asset link), which keeps\n * browse grids and carousels fast even when many cards render. Surfaces that\n * need owner/marketplace actions use the richer `TokenCard` / `ListingCard`.\n *\n * Layout: inset 4:5 artwork (gallery ratio, echoing the Medialane Collection\n * Card) with the price as a glass pill on the artwork (the collection cards'\n * Floor-pill vocabulary) → one body row: display-face title + optional\n * subtitle, with the optional ipType badge inline on the right.\n */\nexport function AssetCard({\n href,\n name,\n image,\n animationUrl,\n live = false,\n subtitle,\n ipType,\n price,\n fallbackId,\n indexing = false,\n ipTypeBaseUrl = \"\",\n className,\n}: AssetCardProps) {\n const resolved = image ? ipfsToHttp(image) : null;\n const hasPrice = !!price?.formatted;\n\n return (\n <div\n className={cn(\n \"card-base group relative flex flex-col w-full transition-colors hover:border-foreground/20\",\n className\n )}\n >\n {/* Artwork — inset with a gallery 4:5 ratio, like the Collection Card */}\n <Link href={href} className=\"block p-1.5 pb-0\">\n <div className=\"relative aspect-[4/5] rounded-[12px] bg-muted overflow-hidden ring-1 ring-border/50\">\n <AnimatedTokenMedia\n image={resolved}\n animationUrl={animationUrl}\n live={live}\n alt={name}\n mode=\"fill\"\n sizes=\"(max-width: 640px) 50vw, (max-width: 1024px) 33vw, 22vw\"\n className=\"object-cover transition-transform duration-500 group-hover:scale-105\"\n fallback={\n <div className=\"absolute inset-0 flex items-center justify-center bg-gradient-to-br from-brand-blue/25 via-brand-purple/25 to-brand-rose/25\">\n <span className=\"text-sm font-semibold text-muted-foreground tabular-nums\">\n #{fallbackId ?? \"?\"}\n </span>\n </div>\n }\n />\n\n {/* Price pill — anchored on the artwork, same glass chip as the\n collection cards' Floor pill */}\n {hasPrice && !indexing && (\n <span className=\"absolute bottom-2 right-2 inline-flex items-center gap-1 text-
|
|
1
|
+
{"version":3,"sources":["../../src/components/asset-card.tsx"],"sourcesContent":["\"use client\";\n\nimport Link from \"next/link\";\nimport { Loader2 } from \"lucide-react\";\nimport { cn } from \"../utils/cn.js\";\nimport { ipfsToHttp } from \"../utils/ipfs.js\";\nimport { formatDisplayPrice, isStableCurrency } from \"../utils/format.js\";\nimport { CurrencyIcon } from \"./currency-icon.js\";\nimport { IpTypeBadge } from \"./ip-type-badge.js\";\nimport { AnimatedTokenMedia } from \"./animated-token-media.js\";\n\nexport interface AssetCardPrice {\n formatted?: string | null;\n currency?: string | null;\n /**\n * Pre-formatted USD equivalent (e.g. \"$13.15\"), computed by the host from\n * its own live rate feed — this package has no price-feed access by\n * design. Omit/null renders the crypto amount alone (today's behavior).\n */\n usdValue?: string | null;\n}\n\nexport interface AssetCardProps {\n /** Asset detail link. */\n href: string;\n /** Asset title. */\n name: string;\n /** Raw image (ipfs:// or http) — resolved internally. */\n image?: string | null;\n /** Resolved animation_url — the live on-chain renderer, if any. */\n animationUrl?: string | null;\n /** Caller-computed eligibility for the living-render treatment. */\n live?: boolean;\n /** Secondary line under the title (collection or description). */\n subtitle?: string | null;\n /** IP type (apiValue or label) — renders the icon badge on the footer left. */\n ipType?: string | null;\n /** Listing price; omit/null shows no price. */\n price?: AssetCardPrice | null;\n /** Shown when the image is missing/errors (e.g. token id). */\n fallbackId?: string | null;\n /** Show the \"Indexing…\" overlay. */\n indexing?: boolean;\n /** Base URL for the ipType badge link. Default \"\" (relative). */\n ipTypeBaseUrl?: string;\n className?: string;\n}\n\n/**\n * The platform's shared asset card — lean and presentational.\n *\n * Intentionally has no action buttons, dropdowns or dialogs: the image and\n * text are the only interactive surface (a single asset link), which keeps\n * browse grids and carousels fast even when many cards render. Surfaces that\n * need owner/marketplace actions use the richer `TokenCard` / `ListingCard`.\n *\n * Layout: inset 4:5 artwork (gallery ratio, echoing the Medialane Collection\n * Card) with the price as a glass pill on the artwork (the collection cards'\n * Floor-pill vocabulary) → one body row: display-face title + optional\n * subtitle, with the optional ipType badge inline on the right.\n */\nexport function AssetCard({\n href,\n name,\n image,\n animationUrl,\n live = false,\n subtitle,\n ipType,\n price,\n fallbackId,\n indexing = false,\n ipTypeBaseUrl = \"\",\n className,\n}: AssetCardProps) {\n const resolved = image ? ipfsToHttp(image) : null;\n const hasPrice = !!price?.formatted;\n\n return (\n <div\n className={cn(\n \"card-base group relative flex flex-col w-full transition-colors hover:border-foreground/20\",\n className\n )}\n >\n {/* Artwork — inset with a gallery 4:5 ratio, like the Collection Card */}\n <Link href={href} className=\"block p-1.5 pb-0\">\n <div className=\"relative aspect-[4/5] rounded-[12px] bg-muted overflow-hidden ring-1 ring-border/50\">\n <AnimatedTokenMedia\n image={resolved}\n animationUrl={animationUrl}\n live={live}\n alt={name}\n mode=\"fill\"\n sizes=\"(max-width: 640px) 50vw, (max-width: 1024px) 33vw, 22vw\"\n className=\"object-cover transition-transform duration-500 group-hover:scale-105\"\n fallback={\n <div className=\"absolute inset-0 flex items-center justify-center bg-gradient-to-br from-brand-blue/25 via-brand-purple/25 to-brand-rose/25\">\n <span className=\"text-sm font-semibold text-muted-foreground tabular-nums\">\n #{fallbackId ?? \"?\"}\n </span>\n </div>\n }\n />\n\n {/* Price pill — anchored on the artwork, same glass chip as the\n collection cards' Floor pill. Fiat leads when a live rate is\n available; the on-chain amount trails, dimmer, as a secondary\n fact — never a second full-weight number competing for\n attention. Stablecoins collapse to fiat + symbol alone, since\n the crypto amount would just repeat the same figure. */}\n {hasPrice && !indexing && (() => {\n const cryptoDisplay = formatDisplayPrice(price!.formatted!);\n const stable = isStableCurrency(price!.currency);\n return (\n <span className=\"absolute bottom-2 right-2 inline-flex items-center gap-1.5 text-sm font-bold text-white/95 backdrop-blur-md bg-black/40 rounded-full pl-3 pr-3 py-1.5 tabular-nums\">\n {price!.usdValue ? (\n <>\n {price!.usdValue}\n <span className=\"text-white/30\">·</span>\n <span className=\"inline-flex items-center gap-1 font-semibold text-white/65\">\n {price!.currency && <CurrencyIcon symbol={price!.currency} size={12} />}\n {stable ? price!.currency : cryptoDisplay}\n </span>\n </>\n ) : (\n <>\n {price!.currency && <CurrencyIcon symbol={price!.currency} size={13} />}\n {cryptoDisplay}\n </>\n )}\n </span>\n );\n })()}\n\n {indexing && (\n <div className=\"absolute bottom-0 inset-x-0 flex items-center justify-center gap-1.5 bg-black/50 backdrop-blur-sm py-1.5\">\n <Loader2 className=\"h-3 w-3 animate-spin text-white/70\" />\n <span className=\"text-2xs text-white/70\">Indexing…</span>\n </div>\n )}\n </div>\n </Link>\n\n {/* Body — one tight block: title (+ optional ipType badge inline) and subtitle */}\n <div className=\"flex items-start justify-between gap-2 px-3 py-3\">\n <Link href={href} className=\"block min-w-0 flex-1\">\n <p className=\"text-base font-bold line-clamp-1 leading-snug\">\n {name}\n </p>\n {subtitle && (\n <p className=\"text-xs text-muted-foreground line-clamp-1 leading-snug\">\n {subtitle}\n </p>\n )}\n </Link>\n\n {ipType && (\n <div className=\"shrink-0 mt-0.5\">\n <IpTypeBadge ipType={ipType} size=\"sm\" baseUrl={ipTypeBaseUrl} />\n </div>\n )}\n </div>\n </div>\n );\n}\n\nexport function AssetCardSkeleton() {\n return (\n <div className=\"card-base flex flex-col w-full\">\n <div className=\"p-1.5 pb-0\">\n <div className=\"aspect-[4/5] w-full animate-pulse bg-muted rounded-[12px]\" />\n </div>\n <div className=\"px-3 py-3 space-y-1.5\">\n <div className=\"h-4 w-3/4 rounded-md animate-pulse bg-muted\" />\n <div className=\"h-3.5 w-2/5 rounded-md animate-pulse bg-muted\" />\n </div>\n </div>\n );\n}\n"],"mappings":";AAiGc,SAoBI,UApBJ,KACE,YADF;AA/Fd,OAAO,UAAU;AACjB,SAAS,eAAe;AACxB,SAAS,UAAU;AACnB,SAAS,kBAAkB;AAC3B,SAAS,oBAAoB,wBAAwB;AACrD,SAAS,oBAAoB;AAC7B,SAAS,mBAAmB;AAC5B,SAAS,0BAA0B;AAoD5B,SAAS,UAAU;AAAA,EACxB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,OAAO;AAAA,EACP;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,WAAW;AAAA,EACX,gBAAgB;AAAA,EAChB;AACF,GAAmB;AACjB,QAAM,WAAW,QAAQ,WAAW,KAAK,IAAI;AAC7C,QAAM,WAAW,CAAC,CAAC,OAAO;AAE1B,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MAGA;AAAA,4BAAC,QAAK,MAAY,WAAU,oBAC1B,+BAAC,SAAI,WAAU,uFACb;AAAA;AAAA,YAAC;AAAA;AAAA,cACC,OAAO;AAAA,cACP;AAAA,cACA;AAAA,cACA,KAAK;AAAA,cACL,MAAK;AAAA,cACL,OAAM;AAAA,cACN,WAAU;AAAA,cACV,UACE,oBAAC,SAAI,WAAU,+HACb,+BAAC,UAAK,WAAU,4DAA2D;AAAA;AAAA,gBACvE,cAAc;AAAA,iBAClB,GACF;AAAA;AAAA,UAEJ;AAAA,UAQC,YAAY,CAAC,aAAa,MAAM;AAC/B,kBAAM,gBAAgB,mBAAmB,MAAO,SAAU;AAC1D,kBAAM,SAAS,iBAAiB,MAAO,QAAQ;AAC/C,mBACE,oBAAC,UAAK,WAAU,sKACb,gBAAO,WACN,iCACG;AAAA,oBAAO;AAAA,cACR,oBAAC,UAAK,WAAU,iBAAgB,kBAAC;AAAA,cACjC,qBAAC,UAAK,WAAU,8DACb;AAAA,sBAAO,YAAY,oBAAC,gBAAa,QAAQ,MAAO,UAAU,MAAM,IAAI;AAAA,gBACpE,SAAS,MAAO,WAAW;AAAA,iBAC9B;AAAA,eACF,IAEA,iCACG;AAAA,oBAAO,YAAY,oBAAC,gBAAa,QAAQ,MAAO,UAAU,MAAM,IAAI;AAAA,cACpE;AAAA,eACH,GAEJ;AAAA,UAEJ,GAAG;AAAA,UAEF,YACC,qBAAC,SAAI,WAAU,4GACb;AAAA,gCAAC,WAAQ,WAAU,sCAAqC;AAAA,YACxD,oBAAC,UAAK,WAAU,0BAAyB,4BAAS;AAAA,aACpD;AAAA,WAEJ,GACF;AAAA,QAGA,qBAAC,SAAI,WAAU,oDACb;AAAA,+BAAC,QAAK,MAAY,WAAU,wBAC1B;AAAA,gCAAC,OAAE,WAAU,iDACV,gBACH;AAAA,YACC,YACC,oBAAC,OAAE,WAAU,2DACV,oBACH;AAAA,aAEJ;AAAA,UAEC,UACC,oBAAC,SAAI,WAAU,mBACb,8BAAC,eAAY,QAAgB,MAAK,MAAK,SAAS,eAAe,GACjE;AAAA,WAEJ;AAAA;AAAA;AAAA,EACF;AAEJ;AAEO,SAAS,oBAAoB;AAClC,SACE,qBAAC,SAAI,WAAU,kCACb;AAAA,wBAAC,SAAI,WAAU,cACb,8BAAC,SAAI,WAAU,6DAA4D,GAC7E;AAAA,IACA,qBAAC,SAAI,WAAU,yBACb;AAAA,0BAAC,SAAI,WAAU,+CAA8C;AAAA,MAC7D,oBAAC,SAAI,WAAU,iDAAgD;AAAA,OACjE;AAAA,KACF;AAEJ;","names":[]}
|
|
@@ -48,26 +48,58 @@ var import_living_render_collections = require("../data/living-render-collection
|
|
|
48
48
|
function PriceLine({
|
|
49
49
|
amountFormatted,
|
|
50
50
|
currency,
|
|
51
|
-
usdValue
|
|
52
|
-
compact = false
|
|
51
|
+
usdValue
|
|
53
52
|
}) {
|
|
54
53
|
if (!amountFormatted) return null;
|
|
55
54
|
if (!usdValue) {
|
|
56
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { className:
|
|
57
|
-
currency && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_currency_icon.CurrencyIcon, { symbol: currency, size:
|
|
58
|
-
(0, import_format.formatDisplayPrice)(amountFormatted)
|
|
59
|
-
!compact && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "sr-only", children: currency })
|
|
55
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { className: "text-2xs font-bold price-value inline-flex items-center gap-1", children: [
|
|
56
|
+
currency && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_currency_icon.CurrencyIcon, { symbol: currency, size: 11 }),
|
|
57
|
+
(0, import_format.formatDisplayPrice)(amountFormatted)
|
|
60
58
|
] });
|
|
61
59
|
}
|
|
62
60
|
const stable = (0, import_format.isStableCurrency)(currency);
|
|
63
61
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
64
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className:
|
|
65
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { className:
|
|
66
|
-
currency && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_currency_icon.CurrencyIcon, { symbol: currency, size:
|
|
62
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "text-2xs font-bold price-value", children: usdValue }),
|
|
63
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { className: "text-2xs text-muted-foreground/70 inline-flex items-center gap-1", children: [
|
|
64
|
+
currency && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_currency_icon.CurrencyIcon, { symbol: currency, size: 10 }),
|
|
67
65
|
stable ? currency : `${(0, import_format.formatDisplayPrice)(amountFormatted)} ${currency}`
|
|
68
66
|
] })
|
|
69
67
|
] });
|
|
70
68
|
}
|
|
69
|
+
function CoinChip({ symbol, size }) {
|
|
70
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
71
|
+
"span",
|
|
72
|
+
{
|
|
73
|
+
className: "grid shrink-0 place-items-center rounded-full bg-foreground/[0.06]",
|
|
74
|
+
style: { width: size + 10, height: size + 10 },
|
|
75
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_currency_icon.CurrencyIcon, { symbol: symbol ?? "", size })
|
|
76
|
+
}
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
function CardDualPrice({
|
|
80
|
+
amountFormatted,
|
|
81
|
+
currency,
|
|
82
|
+
usdValue
|
|
83
|
+
}) {
|
|
84
|
+
if (!amountFormatted) return null;
|
|
85
|
+
const cryptoDisplay = (0, import_format.formatDisplayPrice)(amountFormatted);
|
|
86
|
+
const displayFace = "font-[family-name:var(--font-display)] font-extrabold tracking-tight tabular-nums";
|
|
87
|
+
if (!usdValue) {
|
|
88
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { className: `flex items-center gap-1.5 ${displayFace} text-lg`, children: [
|
|
89
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(CoinChip, { symbol: currency, size: 11 }),
|
|
90
|
+
cryptoDisplay
|
|
91
|
+
] });
|
|
92
|
+
}
|
|
93
|
+
const stable = (0, import_format.isStableCurrency)(currency);
|
|
94
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex items-center gap-2", children: [
|
|
95
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: `${displayFace} text-lg`, children: usdValue }),
|
|
96
|
+
!stable && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "h-4 w-px bg-border/60 shrink-0" }),
|
|
97
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "inline-flex items-center gap-1.5", children: [
|
|
98
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(CoinChip, { symbol: currency, size: stable ? 9 : 11 }),
|
|
99
|
+
stable ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "text-2xs font-semibold text-muted-foreground", children: currency }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: `${displayFace} text-sm text-foreground/80`, children: cryptoDisplay })
|
|
100
|
+
] })
|
|
101
|
+
] });
|
|
102
|
+
}
|
|
71
103
|
function ListingCard({ order: rawOrder, inCart = false, onBuy, onCart, overflowMenu, primaryAction, compact = false, imageUrl, usdValue }) {
|
|
72
104
|
const order = rawOrder;
|
|
73
105
|
const [imgError, setImgError] = (0, import_react.useState)(false);
|
|
@@ -100,7 +132,7 @@ function ListingCard({ order: rawOrder, inCart = false, onBuy, onCart, overflowM
|
|
|
100
132
|
) }),
|
|
101
133
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "p-2.5 space-y-0.5", children: [
|
|
102
134
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "text-xs font-semibold truncate", children: name }),
|
|
103
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(PriceLine, { amountFormatted: order.price?.formatted, currency: order.price?.currency, usdValue
|
|
135
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(PriceLine, { amountFormatted: order.price?.formatted, currency: order.price?.currency, usdValue }),
|
|
104
136
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "text-2xs text-muted-foreground", children: (0, import_time.timeAgo)(order.createdAt) })
|
|
105
137
|
] })
|
|
106
138
|
] }) });
|
|
@@ -129,13 +161,7 @@ function ListingCard({ order: rawOrder, inCart = false, onBuy, onCart, overflowM
|
|
|
129
161
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "font-semibold text-base truncate leading-snug", children: name }),
|
|
130
162
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "text-2xs text-muted-foreground truncate leading-snug mt-0.5", children: order.token?.description ? order.token.description : `#${order.nftTokenId}` })
|
|
131
163
|
] }),
|
|
132
|
-
order.price?.formatted && /* @__PURE__ */ (0, import_jsx_runtime.
|
|
133
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "min-w-0", children: [
|
|
134
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "text-2xs uppercase tracking-widest text-muted-foreground/55 leading-none", children: isListing ? "Price" : "Offer" }),
|
|
135
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(PriceLine, { amountFormatted: order.price.formatted, currency: order.price.currency, usdValue })
|
|
136
|
-
] }),
|
|
137
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "text-2xs text-muted-foreground/60 whitespace-nowrap shrink-0", children: (0, import_time.timeAgo)(order.createdAt) })
|
|
138
|
-
] }),
|
|
164
|
+
order.price?.formatted && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CardDualPrice, { amountFormatted: order.price.formatted, currency: order.price.currency, usdValue }),
|
|
139
165
|
showActionBar && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex items-center gap-1.5", children: [
|
|
140
166
|
isListing ? primaryAction ? primaryAction : onBuy ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "btn-border-animated p-[1.5px] rounded-[10px] flex-1 h-9", children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
141
167
|
"button",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/listing-card.tsx"],"sourcesContent":["\"use client\";\n\nimport { useState } from \"react\";\nimport Link from \"next/link\";\nimport { ShoppingCart, Check, Zap, ArrowUpRight } from \"lucide-react\";\nimport { MotionCard } from \"./motion-primitives.js\";\nimport { CurrencyIcon } from \"./currency-icon.js\";\nimport { AnimatedTokenMedia } from \"./animated-token-media.js\";\nimport { cn } from \"../utils/cn.js\";\nimport { ipfsToHttp } from \"../utils/ipfs.js\";\nimport { formatDisplayPrice, isStableCurrency } from \"../utils/format.js\";\nimport { timeAgo } from \"../utils/time.js\";\nimport { isLivingRenderCollection } from \"../data/living-render-collections.js\";\nimport type { ApiOrder, Chain } from \"@medialane/sdk\";\n\n/** ui's pinned SDK lags the apps — extend structurally for fields newer SDKs carry. */\ntype OrderWithAnimation = ApiOrder & { token: (ApiOrder[\"token\"] & { animationUrl?: string | null }) | null };\n\n/**\n * Fiat-first price line: USD leads (io's non-crypto audience reads fiat, not\n * token amounts), the on-chain currency trails as a small icon+label — never\n * a second near-duplicate number for USD-pegged stablecoins. Falls back to\n * a crypto-only line when no USD rate is available.\n */\nfunction PriceLine({\n amountFormatted, currency, usdValue, compact = false,\n}: {\n amountFormatted: string | null | undefined;\n currency: string | null | undefined;\n usdValue?: string | null;\n compact?: boolean;\n}) {\n if (!amountFormatted) return null;\n if (!usdValue) {\n return (\n <p className={compact ? \"text-2xs font-bold price-value inline-flex items-center gap-1\" : \"text-lg font-bold price-value leading-none inline-flex items-center gap-1.5 mt-1.5\"}>\n {currency && <CurrencyIcon symbol={currency} size={compact ? 11 : 18} />}\n {formatDisplayPrice(amountFormatted)}\n {!compact && <span className=\"sr-only\">{currency}</span>}\n </p>\n );\n }\n const stable = isStableCurrency(currency);\n return (\n <>\n <p className={compact ? \"text-2xs font-bold price-value\" : \"text-lg font-bold price-value leading-none mt-1.5\"}>\n {usdValue}\n </p>\n <p className={compact ? \"text-2xs text-muted-foreground/70 inline-flex items-center gap-1\" : \"text-2xs text-muted-foreground/60 mt-0.5 inline-flex items-center gap-1\"}>\n {currency && <CurrencyIcon symbol={currency} size={compact ? 10 : 11} />}\n {stable ? currency : `${formatDisplayPrice(amountFormatted)} ${currency}`}\n </p>\n </>\n );\n}\n\nexport interface ListingCardProps {\n order: ApiOrder;\n inCart?: boolean;\n onBuy?: (order: ApiOrder) => void;\n onCart?: (order: ApiOrder) => void;\n /** App passes a fully constructed <DropdownMenu> here */\n overflowMenu?: React.ReactNode;\n /**\n * Optional flex-1 primary control for listings, replacing the default\n * Buy/View button — lets a host inject its own action (e.g. an owner\n * \"Cancel\" with an app-specific, auth-coupled handler) while keeping this\n * card's layout. Ignored for offers (which always render \"View asset\").\n */\n primaryAction?: React.ReactNode;\n compact?: boolean;\n /**\n * Pre-resolved, browser-loadable image URL. When provided (including null),\n * overrides the card's internal ipfsToHttp resolution — apps that proxy or\n * resize images (e.g. a same-origin /api/ipfs?w= proxy) pass it here.\n * Omit for the default gateway resolution; null renders the no-image fallback.\n */\n imageUrl?: string | null;\n /**\n * Pre-formatted USD equivalent of order.price (e.g. \"$12.34\"), computed by\n * the host from its own live rate feed — this package has no price-feed\n * access by design. Omit (or pass null) to render no USD line, e.g. when\n * the host has no live rate for this order's currency.\n */\n usdValue?: string | null;\n}\n\nexport function ListingCard({ order: rawOrder, inCart = false, onBuy, onCart, overflowMenu, primaryAction, compact = false, imageUrl, usdValue }: ListingCardProps) {\n const order = rawOrder as OrderWithAnimation;\n const [imgError, setImgError] = useState(false);\n const isListing = order.offer.itemType === \"ERC721\" || order.offer.itemType === \"ERC1155\";\n const name = order.token?.name ?? `Token #${order.nftTokenId}`;\n const image = imageUrl !== undefined ? imageUrl : (order.token?.image ? ipfsToHttp(order.token.image) : null);\n // Not resolved through ipfsToHttp — animation_url is legitimately a data: URI for\n // fully on-chain-rendered collections (ipfsToHttp rejects data: by design).\n const animationUrl = order.token?.animationUrl ?? null;\n const live = !!(order.chain && order.nftContract) && isLivingRenderCollection(order.chain.toUpperCase() as Chain, order.nftContract!);\n const assetHref = `/asset/${order.nftContract}/${order.nftTokenId}`;\n\n // Show the action bar for listings (Buy/View) and for offers (View asset),\n // whenever the host wired any action or an overflow menu.\n const showActionBar = !!(onBuy || onCart || overflowMenu || primaryAction);\n\n // ─── Compact variant ──────────────────────────────────────────────────────\n if (compact) {\n return (\n <MotionCard className=\"card-base\">\n <Link href={assetHref} className=\"block\">\n <div className=\"relative aspect-square bg-muted overflow-hidden\">\n <AnimatedTokenMedia\n image={image}\n animationUrl={animationUrl}\n live={live}\n alt={name}\n mode=\"fill\"\n sizes=\"(max-width: 640px) 33vw, 20vw\"\n className=\"object-cover group-hover:scale-105 transition-transform duration-500\"\n imgError={imgError}\n onImageError={() => setImgError(true)}\n fallback={\n <div className=\"absolute inset-0 flex items-center justify-center bg-gradient-to-br from-brand-purple/15 to-brand-blue/15\">\n <span className=\"text-xl font-mono text-muted-foreground\">#{order.nftTokenId}</span>\n </div>\n }\n />\n </div>\n <div className=\"p-2.5 space-y-0.5\">\n <p className=\"text-xs font-semibold truncate\">{name}</p>\n <PriceLine amountFormatted={order.price?.formatted} currency={order.price?.currency} usdValue={usdValue} compact />\n <p className=\"text-2xs text-muted-foreground\">{timeAgo(order.createdAt)}</p>\n </div>\n </Link>\n </MotionCard>\n );\n }\n\n // ─── Full variant ─────────────────────────────────────────────────────────\n return (\n <MotionCard className=\"card-base\">\n <Link href={assetHref} className=\"block\">\n <div className=\"relative aspect-square bg-muted overflow-hidden\">\n <AnimatedTokenMedia\n image={image}\n animationUrl={animationUrl}\n live={live}\n alt={name}\n mode=\"fill\"\n sizes=\"(max-width: 640px) 50vw, (max-width: 1024px) 33vw, 25vw\"\n className=\"object-cover group-hover:scale-105 transition-transform duration-500\"\n imgError={imgError}\n onImageError={() => setImgError(true)}\n fallback={\n <div className=\"absolute inset-0 flex items-center justify-center bg-gradient-to-br from-brand-purple/15 to-brand-blue/15\">\n <span className=\"text-2xl font-mono text-muted-foreground\">#{order.nftTokenId}</span>\n </div>\n }\n />\n </div>\n\n <div className=\"p-3.5 space-y-3\">\n <div className=\"min-w-0\">\n <p className=\"font-semibold text-base truncate leading-snug\">{name}</p>\n <p className=\"text-2xs text-muted-foreground truncate leading-snug mt-0.5\">\n {order.token?.description ? order.token.description : `#${order.nftTokenId}`}\n </p>\n </div>\n\n {/* Price / Offer + age */}\n {order.price?.formatted && (\n <div className=\"flex items-end justify-between gap-2\">\n <div className=\"min-w-0\">\n <p className=\"text-2xs uppercase tracking-widest text-muted-foreground/55 leading-none\">\n {isListing ? \"Price\" : \"Offer\"}\n </p>\n <PriceLine amountFormatted={order.price.formatted} currency={order.price.currency} usdValue={usdValue} />\n </div>\n <p className=\"text-2xs text-muted-foreground/60 whitespace-nowrap shrink-0\">\n {timeAgo(order.createdAt)}\n </p>\n </div>\n )}\n\n {showActionBar && (\n <div className=\"flex items-center gap-1.5\">\n {isListing ? (\n primaryAction ? (\n primaryAction\n ) : onBuy ? (\n <div className=\"btn-border-animated p-[1.5px] rounded-[10px] flex-1 h-9\">\n <button\n className=\"w-full h-full rounded-[9px] bg-background flex items-center justify-center gap-1.5 text-xs font-semibold text-foreground hover:bg-muted/60 transition-all active:scale-[0.98]\"\n onClick={(e) => { e.preventDefault(); onBuy(order); }}\n >\n <Zap className=\"h-3.5 w-3.5 shrink-0\" /> Buy\n </button>\n </div>\n ) : (\n <Link\n href={assetHref}\n className=\"flex-1 h-9 inline-flex items-center justify-center gap-1.5 rounded-[9px] border border-border bg-background text-xs font-semibold text-foreground hover:bg-muted/60 transition-all active:scale-[0.98]\"\n >\n <ArrowUpRight className=\"h-3.5 w-3.5 shrink-0\" /> View\n </Link>\n )\n ) : (\n <Link\n href={assetHref}\n className=\"flex-1 h-9 inline-flex items-center justify-center gap-1.5 rounded-[9px] border border-border bg-background text-xs font-semibold text-foreground hover:bg-muted/60 transition-all active:scale-[0.98]\"\n >\n <ArrowUpRight className=\"h-3.5 w-3.5 shrink-0\" /> View asset\n </Link>\n )}\n {onCart && (\n <button\n className={cn(\n \"min-h-11 min-w-11 shrink-0 rounded-[9px] border flex items-center justify-center transition-colors\",\n inCart\n ? \"border-brand-orange/50 bg-brand-orange/10 text-brand-orange\"\n : \"border-border bg-background hover:bg-muted text-foreground\"\n )}\n onClick={(e) => { e.preventDefault(); if (!inCart) onCart(order); }}\n disabled={inCart}\n aria-label={inCart ? \"Added to cart\" : \"Add to cart\"}\n >\n {inCart ? <Check className=\"h-3.5 w-3.5\" /> : <ShoppingCart className=\"h-3.5 w-3.5\" />}\n </button>\n )}\n {overflowMenu}\n </div>\n )}\n </div>\n </Link>\n </MotionCard>\n );\n}\n\nexport function ListingCardSkeleton() {\n return (\n <div className=\"card-base\">\n <div className=\"aspect-square w-full bg-muted animate-pulse\" />\n <div className=\"p-3 space-y-2.5\">\n <div className=\"space-y-1\">\n <div className=\"h-4 w-3/4 bg-muted animate-pulse rounded\" />\n <div className=\"h-3 w-1/2 bg-muted animate-pulse rounded\" />\n </div>\n <div className=\"h-6 w-1/2 bg-muted animate-pulse rounded\" />\n <div className=\"h-3 w-2/3 bg-muted animate-pulse rounded\" />\n </div>\n </div>\n );\n}\n"],"mappings":"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","names":["Link"]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/listing-card.tsx"],"sourcesContent":["\"use client\";\n\nimport { useState } from \"react\";\nimport Link from \"next/link\";\nimport { ShoppingCart, Check, Zap, ArrowUpRight } from \"lucide-react\";\nimport { MotionCard } from \"./motion-primitives.js\";\nimport { CurrencyIcon } from \"./currency-icon.js\";\nimport { AnimatedTokenMedia } from \"./animated-token-media.js\";\nimport { cn } from \"../utils/cn.js\";\nimport { ipfsToHttp } from \"../utils/ipfs.js\";\nimport { formatDisplayPrice, isStableCurrency } from \"../utils/format.js\";\nimport { timeAgo } from \"../utils/time.js\";\nimport { isLivingRenderCollection } from \"../data/living-render-collections.js\";\nimport type { ApiOrder, Chain } from \"@medialane/sdk\";\n\n/** ui's pinned SDK lags the apps — extend structurally for fields newer SDKs carry. */\ntype OrderWithAnimation = ApiOrder & { token: (ApiOrder[\"token\"] & { animationUrl?: string | null }) | null };\n\n/** Compact grid thumbnails: too little room for the full dual-price\n * treatment, so this stays fiat-primary + a small crypto caption. */\nfunction PriceLine({\n amountFormatted, currency, usdValue,\n}: {\n amountFormatted: string | null | undefined;\n currency: string | null | undefined;\n usdValue?: string | null;\n}) {\n if (!amountFormatted) return null;\n if (!usdValue) {\n return (\n <p className=\"text-2xs font-bold price-value inline-flex items-center gap-1\">\n {currency && <CurrencyIcon symbol={currency} size={11} />}\n {formatDisplayPrice(amountFormatted)}\n </p>\n );\n }\n const stable = isStableCurrency(currency);\n return (\n <>\n <p className=\"text-2xs font-bold price-value\">\n {usdValue}\n </p>\n <p className=\"text-2xs text-muted-foreground/70 inline-flex items-center gap-1\">\n {currency && <CurrencyIcon symbol={currency} size={10} />}\n {stable ? currency : `${formatDisplayPrice(amountFormatted)} ${currency}`}\n </p>\n </>\n );\n}\n\n/** Coin icon in a soft circular chip — same treatment as AssetMarketplacePanel,\n * scaled down for card density. */\nfunction CoinChip({ symbol, size }: { symbol: string | null | undefined; size: number }) {\n return (\n <span\n className=\"grid shrink-0 place-items-center rounded-full bg-foreground/[0.06]\"\n style={{ width: size + 10, height: size + 10 }}\n >\n <CurrencyIcon symbol={symbol ?? \"\"} size={size} />\n </span>\n );\n}\n\n/**\n * Full-card dual price: fiat and the on-chain currency as equal-weight\n * peers (same language as AssetMarketplacePanel's PrimaryPrice), scaled\n * down for card density — no \"PRICE\" eyebrow label, no divider text\n * duplicating what the coin chip already conveys.\n */\nfunction CardDualPrice({\n amountFormatted, currency, usdValue,\n}: {\n amountFormatted: string | null | undefined;\n currency: string | null | undefined;\n usdValue?: string | null;\n}) {\n if (!amountFormatted) return null;\n const cryptoDisplay = formatDisplayPrice(amountFormatted);\n const displayFace = \"font-[family-name:var(--font-display)] font-extrabold tracking-tight tabular-nums\";\n\n if (!usdValue) {\n return (\n <p className={`flex items-center gap-1.5 ${displayFace} text-lg`}>\n <CoinChip symbol={currency} size={11} />\n {cryptoDisplay}\n </p>\n );\n }\n\n const stable = isStableCurrency(currency);\n return (\n <div className=\"flex items-center gap-2\">\n <span className={`${displayFace} text-lg`}>{usdValue}</span>\n {!stable && <span className=\"h-4 w-px bg-border/60 shrink-0\" />}\n <span className=\"inline-flex items-center gap-1.5\">\n <CoinChip symbol={currency} size={stable ? 9 : 11} />\n {stable ? (\n <span className=\"text-2xs font-semibold text-muted-foreground\">{currency}</span>\n ) : (\n <span className={`${displayFace} text-sm text-foreground/80`}>{cryptoDisplay}</span>\n )}\n </span>\n </div>\n );\n}\n\nexport interface ListingCardProps {\n order: ApiOrder;\n inCart?: boolean;\n onBuy?: (order: ApiOrder) => void;\n onCart?: (order: ApiOrder) => void;\n /** App passes a fully constructed <DropdownMenu> here */\n overflowMenu?: React.ReactNode;\n /**\n * Optional flex-1 primary control for listings, replacing the default\n * Buy/View button — lets a host inject its own action (e.g. an owner\n * \"Cancel\" with an app-specific, auth-coupled handler) while keeping this\n * card's layout. Ignored for offers (which always render \"View asset\").\n */\n primaryAction?: React.ReactNode;\n compact?: boolean;\n /**\n * Pre-resolved, browser-loadable image URL. When provided (including null),\n * overrides the card's internal ipfsToHttp resolution — apps that proxy or\n * resize images (e.g. a same-origin /api/ipfs?w= proxy) pass it here.\n * Omit for the default gateway resolution; null renders the no-image fallback.\n */\n imageUrl?: string | null;\n /**\n * Pre-formatted USD equivalent of order.price (e.g. \"$12.34\"), computed by\n * the host from its own live rate feed — this package has no price-feed\n * access by design. Omit (or pass null) to render no USD line, e.g. when\n * the host has no live rate for this order's currency.\n */\n usdValue?: string | null;\n}\n\nexport function ListingCard({ order: rawOrder, inCart = false, onBuy, onCart, overflowMenu, primaryAction, compact = false, imageUrl, usdValue }: ListingCardProps) {\n const order = rawOrder as OrderWithAnimation;\n const [imgError, setImgError] = useState(false);\n const isListing = order.offer.itemType === \"ERC721\" || order.offer.itemType === \"ERC1155\";\n const name = order.token?.name ?? `Token #${order.nftTokenId}`;\n const image = imageUrl !== undefined ? imageUrl : (order.token?.image ? ipfsToHttp(order.token.image) : null);\n // Not resolved through ipfsToHttp — animation_url is legitimately a data: URI for\n // fully on-chain-rendered collections (ipfsToHttp rejects data: by design).\n const animationUrl = order.token?.animationUrl ?? null;\n const live = !!(order.chain && order.nftContract) && isLivingRenderCollection(order.chain.toUpperCase() as Chain, order.nftContract!);\n const assetHref = `/asset/${order.nftContract}/${order.nftTokenId}`;\n\n // Show the action bar for listings (Buy/View) and for offers (View asset),\n // whenever the host wired any action or an overflow menu.\n const showActionBar = !!(onBuy || onCart || overflowMenu || primaryAction);\n\n // ─── Compact variant ──────────────────────────────────────────────────────\n if (compact) {\n return (\n <MotionCard className=\"card-base\">\n <Link href={assetHref} className=\"block\">\n <div className=\"relative aspect-square bg-muted overflow-hidden\">\n <AnimatedTokenMedia\n image={image}\n animationUrl={animationUrl}\n live={live}\n alt={name}\n mode=\"fill\"\n sizes=\"(max-width: 640px) 33vw, 20vw\"\n className=\"object-cover group-hover:scale-105 transition-transform duration-500\"\n imgError={imgError}\n onImageError={() => setImgError(true)}\n fallback={\n <div className=\"absolute inset-0 flex items-center justify-center bg-gradient-to-br from-brand-purple/15 to-brand-blue/15\">\n <span className=\"text-xl font-mono text-muted-foreground\">#{order.nftTokenId}</span>\n </div>\n }\n />\n </div>\n <div className=\"p-2.5 space-y-0.5\">\n <p className=\"text-xs font-semibold truncate\">{name}</p>\n <PriceLine amountFormatted={order.price?.formatted} currency={order.price?.currency} usdValue={usdValue} />\n <p className=\"text-2xs text-muted-foreground\">{timeAgo(order.createdAt)}</p>\n </div>\n </Link>\n </MotionCard>\n );\n }\n\n // ─── Full variant ─────────────────────────────────────────────────────────\n return (\n <MotionCard className=\"card-base\">\n <Link href={assetHref} className=\"block\">\n <div className=\"relative aspect-square bg-muted overflow-hidden\">\n <AnimatedTokenMedia\n image={image}\n animationUrl={animationUrl}\n live={live}\n alt={name}\n mode=\"fill\"\n sizes=\"(max-width: 640px) 50vw, (max-width: 1024px) 33vw, 25vw\"\n className=\"object-cover group-hover:scale-105 transition-transform duration-500\"\n imgError={imgError}\n onImageError={() => setImgError(true)}\n fallback={\n <div className=\"absolute inset-0 flex items-center justify-center bg-gradient-to-br from-brand-purple/15 to-brand-blue/15\">\n <span className=\"text-2xl font-mono text-muted-foreground\">#{order.nftTokenId}</span>\n </div>\n }\n />\n </div>\n\n <div className=\"p-3.5 space-y-3\">\n <div className=\"min-w-0\">\n <p className=\"font-semibold text-base truncate leading-snug\">{name}</p>\n <p className=\"text-2xs text-muted-foreground truncate leading-snug mt-0.5\">\n {order.token?.description ? order.token.description : `#${order.nftTokenId}`}\n </p>\n </div>\n\n {order.price?.formatted && (\n <CardDualPrice amountFormatted={order.price.formatted} currency={order.price.currency} usdValue={usdValue} />\n )}\n\n {showActionBar && (\n <div className=\"flex items-center gap-1.5\">\n {isListing ? (\n primaryAction ? (\n primaryAction\n ) : onBuy ? (\n <div className=\"btn-border-animated p-[1.5px] rounded-[10px] flex-1 h-9\">\n <button\n className=\"w-full h-full rounded-[9px] bg-background flex items-center justify-center gap-1.5 text-xs font-semibold text-foreground hover:bg-muted/60 transition-all active:scale-[0.98]\"\n onClick={(e) => { e.preventDefault(); onBuy(order); }}\n >\n <Zap className=\"h-3.5 w-3.5 shrink-0\" /> Buy\n </button>\n </div>\n ) : (\n <Link\n href={assetHref}\n className=\"flex-1 h-9 inline-flex items-center justify-center gap-1.5 rounded-[9px] border border-border bg-background text-xs font-semibold text-foreground hover:bg-muted/60 transition-all active:scale-[0.98]\"\n >\n <ArrowUpRight className=\"h-3.5 w-3.5 shrink-0\" /> View\n </Link>\n )\n ) : (\n <Link\n href={assetHref}\n className=\"flex-1 h-9 inline-flex items-center justify-center gap-1.5 rounded-[9px] border border-border bg-background text-xs font-semibold text-foreground hover:bg-muted/60 transition-all active:scale-[0.98]\"\n >\n <ArrowUpRight className=\"h-3.5 w-3.5 shrink-0\" /> View asset\n </Link>\n )}\n {onCart && (\n <button\n className={cn(\n \"min-h-11 min-w-11 shrink-0 rounded-[9px] border flex items-center justify-center transition-colors\",\n inCart\n ? \"border-brand-orange/50 bg-brand-orange/10 text-brand-orange\"\n : \"border-border bg-background hover:bg-muted text-foreground\"\n )}\n onClick={(e) => { e.preventDefault(); if (!inCart) onCart(order); }}\n disabled={inCart}\n aria-label={inCart ? \"Added to cart\" : \"Add to cart\"}\n >\n {inCart ? <Check className=\"h-3.5 w-3.5\" /> : <ShoppingCart className=\"h-3.5 w-3.5\" />}\n </button>\n )}\n {overflowMenu}\n </div>\n )}\n </div>\n </Link>\n </MotionCard>\n );\n}\n\nexport function ListingCardSkeleton() {\n return (\n <div className=\"card-base\">\n <div className=\"aspect-square w-full bg-muted animate-pulse\" />\n <div className=\"p-3 space-y-2.5\">\n <div className=\"space-y-1\">\n <div className=\"h-4 w-3/4 bg-muted animate-pulse rounded\" />\n <div className=\"h-3 w-1/2 bg-muted animate-pulse rounded\" />\n </div>\n <div className=\"h-6 w-1/2 bg-muted animate-pulse rounded\" />\n <div className=\"h-3 w-2/3 bg-muted animate-pulse rounded\" />\n </div>\n </div>\n );\n}\n"],"mappings":"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","names":["Link"]}
|
|
@@ -14,26 +14,58 @@ import { isLivingRenderCollection } from "../data/living-render-collections.js";
|
|
|
14
14
|
function PriceLine({
|
|
15
15
|
amountFormatted,
|
|
16
16
|
currency,
|
|
17
|
-
usdValue
|
|
18
|
-
compact = false
|
|
17
|
+
usdValue
|
|
19
18
|
}) {
|
|
20
19
|
if (!amountFormatted) return null;
|
|
21
20
|
if (!usdValue) {
|
|
22
|
-
return /* @__PURE__ */ jsxs("p", { className:
|
|
23
|
-
currency && /* @__PURE__ */ jsx(CurrencyIcon, { symbol: currency, size:
|
|
24
|
-
formatDisplayPrice(amountFormatted)
|
|
25
|
-
!compact && /* @__PURE__ */ jsx("span", { className: "sr-only", children: currency })
|
|
21
|
+
return /* @__PURE__ */ jsxs("p", { className: "text-2xs font-bold price-value inline-flex items-center gap-1", children: [
|
|
22
|
+
currency && /* @__PURE__ */ jsx(CurrencyIcon, { symbol: currency, size: 11 }),
|
|
23
|
+
formatDisplayPrice(amountFormatted)
|
|
26
24
|
] });
|
|
27
25
|
}
|
|
28
26
|
const stable = isStableCurrency(currency);
|
|
29
27
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
30
|
-
/* @__PURE__ */ jsx("p", { className:
|
|
31
|
-
/* @__PURE__ */ jsxs("p", { className:
|
|
32
|
-
currency && /* @__PURE__ */ jsx(CurrencyIcon, { symbol: currency, size:
|
|
28
|
+
/* @__PURE__ */ jsx("p", { className: "text-2xs font-bold price-value", children: usdValue }),
|
|
29
|
+
/* @__PURE__ */ jsxs("p", { className: "text-2xs text-muted-foreground/70 inline-flex items-center gap-1", children: [
|
|
30
|
+
currency && /* @__PURE__ */ jsx(CurrencyIcon, { symbol: currency, size: 10 }),
|
|
33
31
|
stable ? currency : `${formatDisplayPrice(amountFormatted)} ${currency}`
|
|
34
32
|
] })
|
|
35
33
|
] });
|
|
36
34
|
}
|
|
35
|
+
function CoinChip({ symbol, size }) {
|
|
36
|
+
return /* @__PURE__ */ jsx(
|
|
37
|
+
"span",
|
|
38
|
+
{
|
|
39
|
+
className: "grid shrink-0 place-items-center rounded-full bg-foreground/[0.06]",
|
|
40
|
+
style: { width: size + 10, height: size + 10 },
|
|
41
|
+
children: /* @__PURE__ */ jsx(CurrencyIcon, { symbol: symbol ?? "", size })
|
|
42
|
+
}
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
function CardDualPrice({
|
|
46
|
+
amountFormatted,
|
|
47
|
+
currency,
|
|
48
|
+
usdValue
|
|
49
|
+
}) {
|
|
50
|
+
if (!amountFormatted) return null;
|
|
51
|
+
const cryptoDisplay = formatDisplayPrice(amountFormatted);
|
|
52
|
+
const displayFace = "font-[family-name:var(--font-display)] font-extrabold tracking-tight tabular-nums";
|
|
53
|
+
if (!usdValue) {
|
|
54
|
+
return /* @__PURE__ */ jsxs("p", { className: `flex items-center gap-1.5 ${displayFace} text-lg`, children: [
|
|
55
|
+
/* @__PURE__ */ jsx(CoinChip, { symbol: currency, size: 11 }),
|
|
56
|
+
cryptoDisplay
|
|
57
|
+
] });
|
|
58
|
+
}
|
|
59
|
+
const stable = isStableCurrency(currency);
|
|
60
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
61
|
+
/* @__PURE__ */ jsx("span", { className: `${displayFace} text-lg`, children: usdValue }),
|
|
62
|
+
!stable && /* @__PURE__ */ jsx("span", { className: "h-4 w-px bg-border/60 shrink-0" }),
|
|
63
|
+
/* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1.5", children: [
|
|
64
|
+
/* @__PURE__ */ jsx(CoinChip, { symbol: currency, size: stable ? 9 : 11 }),
|
|
65
|
+
stable ? /* @__PURE__ */ jsx("span", { className: "text-2xs font-semibold text-muted-foreground", children: currency }) : /* @__PURE__ */ jsx("span", { className: `${displayFace} text-sm text-foreground/80`, children: cryptoDisplay })
|
|
66
|
+
] })
|
|
67
|
+
] });
|
|
68
|
+
}
|
|
37
69
|
function ListingCard({ order: rawOrder, inCart = false, onBuy, onCart, overflowMenu, primaryAction, compact = false, imageUrl, usdValue }) {
|
|
38
70
|
const order = rawOrder;
|
|
39
71
|
const [imgError, setImgError] = useState(false);
|
|
@@ -66,7 +98,7 @@ function ListingCard({ order: rawOrder, inCart = false, onBuy, onCart, overflowM
|
|
|
66
98
|
) }),
|
|
67
99
|
/* @__PURE__ */ jsxs("div", { className: "p-2.5 space-y-0.5", children: [
|
|
68
100
|
/* @__PURE__ */ jsx("p", { className: "text-xs font-semibold truncate", children: name }),
|
|
69
|
-
/* @__PURE__ */ jsx(PriceLine, { amountFormatted: order.price?.formatted, currency: order.price?.currency, usdValue
|
|
101
|
+
/* @__PURE__ */ jsx(PriceLine, { amountFormatted: order.price?.formatted, currency: order.price?.currency, usdValue }),
|
|
70
102
|
/* @__PURE__ */ jsx("p", { className: "text-2xs text-muted-foreground", children: timeAgo(order.createdAt) })
|
|
71
103
|
] })
|
|
72
104
|
] }) });
|
|
@@ -95,13 +127,7 @@ function ListingCard({ order: rawOrder, inCart = false, onBuy, onCart, overflowM
|
|
|
95
127
|
/* @__PURE__ */ jsx("p", { className: "font-semibold text-base truncate leading-snug", children: name }),
|
|
96
128
|
/* @__PURE__ */ jsx("p", { className: "text-2xs text-muted-foreground truncate leading-snug mt-0.5", children: order.token?.description ? order.token.description : `#${order.nftTokenId}` })
|
|
97
129
|
] }),
|
|
98
|
-
order.price?.formatted && /* @__PURE__ */
|
|
99
|
-
/* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
|
|
100
|
-
/* @__PURE__ */ jsx("p", { className: "text-2xs uppercase tracking-widest text-muted-foreground/55 leading-none", children: isListing ? "Price" : "Offer" }),
|
|
101
|
-
/* @__PURE__ */ jsx(PriceLine, { amountFormatted: order.price.formatted, currency: order.price.currency, usdValue })
|
|
102
|
-
] }),
|
|
103
|
-
/* @__PURE__ */ jsx("p", { className: "text-2xs text-muted-foreground/60 whitespace-nowrap shrink-0", children: timeAgo(order.createdAt) })
|
|
104
|
-
] }),
|
|
130
|
+
order.price?.formatted && /* @__PURE__ */ jsx(CardDualPrice, { amountFormatted: order.price.formatted, currency: order.price.currency, usdValue }),
|
|
105
131
|
showActionBar && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5", children: [
|
|
106
132
|
isListing ? primaryAction ? primaryAction : onBuy ? /* @__PURE__ */ jsx("div", { className: "btn-border-animated p-[1.5px] rounded-[10px] flex-1 h-9", children: /* @__PURE__ */ jsxs(
|
|
107
133
|
"button",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/listing-card.tsx"],"sourcesContent":["\"use client\";\n\nimport { useState } from \"react\";\nimport Link from \"next/link\";\nimport { ShoppingCart, Check, Zap, ArrowUpRight } from \"lucide-react\";\nimport { MotionCard } from \"./motion-primitives.js\";\nimport { CurrencyIcon } from \"./currency-icon.js\";\nimport { AnimatedTokenMedia } from \"./animated-token-media.js\";\nimport { cn } from \"../utils/cn.js\";\nimport { ipfsToHttp } from \"../utils/ipfs.js\";\nimport { formatDisplayPrice, isStableCurrency } from \"../utils/format.js\";\nimport { timeAgo } from \"../utils/time.js\";\nimport { isLivingRenderCollection } from \"../data/living-render-collections.js\";\nimport type { ApiOrder, Chain } from \"@medialane/sdk\";\n\n/** ui's pinned SDK lags the apps — extend structurally for fields newer SDKs carry. */\ntype OrderWithAnimation = ApiOrder & { token: (ApiOrder[\"token\"] & { animationUrl?: string | null }) | null };\n\n/**\n * Fiat-first price line: USD leads (io's non-crypto audience reads fiat, not\n * token amounts), the on-chain currency trails as a small icon+label — never\n * a second near-duplicate number for USD-pegged stablecoins. Falls back to\n * a crypto-only line when no USD rate is available.\n */\nfunction PriceLine({\n amountFormatted, currency, usdValue, compact = false,\n}: {\n amountFormatted: string | null | undefined;\n currency: string | null | undefined;\n usdValue?: string | null;\n compact?: boolean;\n}) {\n if (!amountFormatted) return null;\n if (!usdValue) {\n return (\n <p className={compact ? \"text-2xs font-bold price-value inline-flex items-center gap-1\" : \"text-lg font-bold price-value leading-none inline-flex items-center gap-1.5 mt-1.5\"}>\n {currency && <CurrencyIcon symbol={currency} size={compact ? 11 : 18} />}\n {formatDisplayPrice(amountFormatted)}\n {!compact && <span className=\"sr-only\">{currency}</span>}\n </p>\n );\n }\n const stable = isStableCurrency(currency);\n return (\n <>\n <p className={compact ? \"text-2xs font-bold price-value\" : \"text-lg font-bold price-value leading-none mt-1.5\"}>\n {usdValue}\n </p>\n <p className={compact ? \"text-2xs text-muted-foreground/70 inline-flex items-center gap-1\" : \"text-2xs text-muted-foreground/60 mt-0.5 inline-flex items-center gap-1\"}>\n {currency && <CurrencyIcon symbol={currency} size={compact ? 10 : 11} />}\n {stable ? currency : `${formatDisplayPrice(amountFormatted)} ${currency}`}\n </p>\n </>\n );\n}\n\nexport interface ListingCardProps {\n order: ApiOrder;\n inCart?: boolean;\n onBuy?: (order: ApiOrder) => void;\n onCart?: (order: ApiOrder) => void;\n /** App passes a fully constructed <DropdownMenu> here */\n overflowMenu?: React.ReactNode;\n /**\n * Optional flex-1 primary control for listings, replacing the default\n * Buy/View button — lets a host inject its own action (e.g. an owner\n * \"Cancel\" with an app-specific, auth-coupled handler) while keeping this\n * card's layout. Ignored for offers (which always render \"View asset\").\n */\n primaryAction?: React.ReactNode;\n compact?: boolean;\n /**\n * Pre-resolved, browser-loadable image URL. When provided (including null),\n * overrides the card's internal ipfsToHttp resolution — apps that proxy or\n * resize images (e.g. a same-origin /api/ipfs?w= proxy) pass it here.\n * Omit for the default gateway resolution; null renders the no-image fallback.\n */\n imageUrl?: string | null;\n /**\n * Pre-formatted USD equivalent of order.price (e.g. \"$12.34\"), computed by\n * the host from its own live rate feed — this package has no price-feed\n * access by design. Omit (or pass null) to render no USD line, e.g. when\n * the host has no live rate for this order's currency.\n */\n usdValue?: string | null;\n}\n\nexport function ListingCard({ order: rawOrder, inCart = false, onBuy, onCart, overflowMenu, primaryAction, compact = false, imageUrl, usdValue }: ListingCardProps) {\n const order = rawOrder as OrderWithAnimation;\n const [imgError, setImgError] = useState(false);\n const isListing = order.offer.itemType === \"ERC721\" || order.offer.itemType === \"ERC1155\";\n const name = order.token?.name ?? `Token #${order.nftTokenId}`;\n const image = imageUrl !== undefined ? imageUrl : (order.token?.image ? ipfsToHttp(order.token.image) : null);\n // Not resolved through ipfsToHttp — animation_url is legitimately a data: URI for\n // fully on-chain-rendered collections (ipfsToHttp rejects data: by design).\n const animationUrl = order.token?.animationUrl ?? null;\n const live = !!(order.chain && order.nftContract) && isLivingRenderCollection(order.chain.toUpperCase() as Chain, order.nftContract!);\n const assetHref = `/asset/${order.nftContract}/${order.nftTokenId}`;\n\n // Show the action bar for listings (Buy/View) and for offers (View asset),\n // whenever the host wired any action or an overflow menu.\n const showActionBar = !!(onBuy || onCart || overflowMenu || primaryAction);\n\n // ─── Compact variant ──────────────────────────────────────────────────────\n if (compact) {\n return (\n <MotionCard className=\"card-base\">\n <Link href={assetHref} className=\"block\">\n <div className=\"relative aspect-square bg-muted overflow-hidden\">\n <AnimatedTokenMedia\n image={image}\n animationUrl={animationUrl}\n live={live}\n alt={name}\n mode=\"fill\"\n sizes=\"(max-width: 640px) 33vw, 20vw\"\n className=\"object-cover group-hover:scale-105 transition-transform duration-500\"\n imgError={imgError}\n onImageError={() => setImgError(true)}\n fallback={\n <div className=\"absolute inset-0 flex items-center justify-center bg-gradient-to-br from-brand-purple/15 to-brand-blue/15\">\n <span className=\"text-xl font-mono text-muted-foreground\">#{order.nftTokenId}</span>\n </div>\n }\n />\n </div>\n <div className=\"p-2.5 space-y-0.5\">\n <p className=\"text-xs font-semibold truncate\">{name}</p>\n <PriceLine amountFormatted={order.price?.formatted} currency={order.price?.currency} usdValue={usdValue} compact />\n <p className=\"text-2xs text-muted-foreground\">{timeAgo(order.createdAt)}</p>\n </div>\n </Link>\n </MotionCard>\n );\n }\n\n // ─── Full variant ─────────────────────────────────────────────────────────\n return (\n <MotionCard className=\"card-base\">\n <Link href={assetHref} className=\"block\">\n <div className=\"relative aspect-square bg-muted overflow-hidden\">\n <AnimatedTokenMedia\n image={image}\n animationUrl={animationUrl}\n live={live}\n alt={name}\n mode=\"fill\"\n sizes=\"(max-width: 640px) 50vw, (max-width: 1024px) 33vw, 25vw\"\n className=\"object-cover group-hover:scale-105 transition-transform duration-500\"\n imgError={imgError}\n onImageError={() => setImgError(true)}\n fallback={\n <div className=\"absolute inset-0 flex items-center justify-center bg-gradient-to-br from-brand-purple/15 to-brand-blue/15\">\n <span className=\"text-2xl font-mono text-muted-foreground\">#{order.nftTokenId}</span>\n </div>\n }\n />\n </div>\n\n <div className=\"p-3.5 space-y-3\">\n <div className=\"min-w-0\">\n <p className=\"font-semibold text-base truncate leading-snug\">{name}</p>\n <p className=\"text-2xs text-muted-foreground truncate leading-snug mt-0.5\">\n {order.token?.description ? order.token.description : `#${order.nftTokenId}`}\n </p>\n </div>\n\n {/* Price / Offer + age */}\n {order.price?.formatted && (\n <div className=\"flex items-end justify-between gap-2\">\n <div className=\"min-w-0\">\n <p className=\"text-2xs uppercase tracking-widest text-muted-foreground/55 leading-none\">\n {isListing ? \"Price\" : \"Offer\"}\n </p>\n <PriceLine amountFormatted={order.price.formatted} currency={order.price.currency} usdValue={usdValue} />\n </div>\n <p className=\"text-2xs text-muted-foreground/60 whitespace-nowrap shrink-0\">\n {timeAgo(order.createdAt)}\n </p>\n </div>\n )}\n\n {showActionBar && (\n <div className=\"flex items-center gap-1.5\">\n {isListing ? (\n primaryAction ? (\n primaryAction\n ) : onBuy ? (\n <div className=\"btn-border-animated p-[1.5px] rounded-[10px] flex-1 h-9\">\n <button\n className=\"w-full h-full rounded-[9px] bg-background flex items-center justify-center gap-1.5 text-xs font-semibold text-foreground hover:bg-muted/60 transition-all active:scale-[0.98]\"\n onClick={(e) => { e.preventDefault(); onBuy(order); }}\n >\n <Zap className=\"h-3.5 w-3.5 shrink-0\" /> Buy\n </button>\n </div>\n ) : (\n <Link\n href={assetHref}\n className=\"flex-1 h-9 inline-flex items-center justify-center gap-1.5 rounded-[9px] border border-border bg-background text-xs font-semibold text-foreground hover:bg-muted/60 transition-all active:scale-[0.98]\"\n >\n <ArrowUpRight className=\"h-3.5 w-3.5 shrink-0\" /> View\n </Link>\n )\n ) : (\n <Link\n href={assetHref}\n className=\"flex-1 h-9 inline-flex items-center justify-center gap-1.5 rounded-[9px] border border-border bg-background text-xs font-semibold text-foreground hover:bg-muted/60 transition-all active:scale-[0.98]\"\n >\n <ArrowUpRight className=\"h-3.5 w-3.5 shrink-0\" /> View asset\n </Link>\n )}\n {onCart && (\n <button\n className={cn(\n \"min-h-11 min-w-11 shrink-0 rounded-[9px] border flex items-center justify-center transition-colors\",\n inCart\n ? \"border-brand-orange/50 bg-brand-orange/10 text-brand-orange\"\n : \"border-border bg-background hover:bg-muted text-foreground\"\n )}\n onClick={(e) => { e.preventDefault(); if (!inCart) onCart(order); }}\n disabled={inCart}\n aria-label={inCart ? \"Added to cart\" : \"Add to cart\"}\n >\n {inCart ? <Check className=\"h-3.5 w-3.5\" /> : <ShoppingCart className=\"h-3.5 w-3.5\" />}\n </button>\n )}\n {overflowMenu}\n </div>\n )}\n </div>\n </Link>\n </MotionCard>\n );\n}\n\nexport function ListingCardSkeleton() {\n return (\n <div className=\"card-base\">\n <div className=\"aspect-square w-full bg-muted animate-pulse\" />\n <div className=\"p-3 space-y-2.5\">\n <div className=\"space-y-1\">\n <div className=\"h-4 w-3/4 bg-muted animate-pulse rounded\" />\n <div className=\"h-3 w-1/2 bg-muted animate-pulse rounded\" />\n </div>\n <div className=\"h-6 w-1/2 bg-muted animate-pulse rounded\" />\n <div className=\"h-3 w-2/3 bg-muted animate-pulse rounded\" />\n </div>\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/listing-card.tsx"],"sourcesContent":["\"use client\";\n\nimport { useState } from \"react\";\nimport Link from \"next/link\";\nimport { ShoppingCart, Check, Zap, ArrowUpRight } from \"lucide-react\";\nimport { MotionCard } from \"./motion-primitives.js\";\nimport { CurrencyIcon } from \"./currency-icon.js\";\nimport { AnimatedTokenMedia } from \"./animated-token-media.js\";\nimport { cn } from \"../utils/cn.js\";\nimport { ipfsToHttp } from \"../utils/ipfs.js\";\nimport { formatDisplayPrice, isStableCurrency } from \"../utils/format.js\";\nimport { timeAgo } from \"../utils/time.js\";\nimport { isLivingRenderCollection } from \"../data/living-render-collections.js\";\nimport type { ApiOrder, Chain } from \"@medialane/sdk\";\n\n/** ui's pinned SDK lags the apps — extend structurally for fields newer SDKs carry. */\ntype OrderWithAnimation = ApiOrder & { token: (ApiOrder[\"token\"] & { animationUrl?: string | null }) | null };\n\n/** Compact grid thumbnails: too little room for the full dual-price\n * treatment, so this stays fiat-primary + a small crypto caption. */\nfunction PriceLine({\n amountFormatted, currency, usdValue,\n}: {\n amountFormatted: string | null | undefined;\n currency: string | null | undefined;\n usdValue?: string | null;\n}) {\n if (!amountFormatted) return null;\n if (!usdValue) {\n return (\n <p className=\"text-2xs font-bold price-value inline-flex items-center gap-1\">\n {currency && <CurrencyIcon symbol={currency} size={11} />}\n {formatDisplayPrice(amountFormatted)}\n </p>\n );\n }\n const stable = isStableCurrency(currency);\n return (\n <>\n <p className=\"text-2xs font-bold price-value\">\n {usdValue}\n </p>\n <p className=\"text-2xs text-muted-foreground/70 inline-flex items-center gap-1\">\n {currency && <CurrencyIcon symbol={currency} size={10} />}\n {stable ? currency : `${formatDisplayPrice(amountFormatted)} ${currency}`}\n </p>\n </>\n );\n}\n\n/** Coin icon in a soft circular chip — same treatment as AssetMarketplacePanel,\n * scaled down for card density. */\nfunction CoinChip({ symbol, size }: { symbol: string | null | undefined; size: number }) {\n return (\n <span\n className=\"grid shrink-0 place-items-center rounded-full bg-foreground/[0.06]\"\n style={{ width: size + 10, height: size + 10 }}\n >\n <CurrencyIcon symbol={symbol ?? \"\"} size={size} />\n </span>\n );\n}\n\n/**\n * Full-card dual price: fiat and the on-chain currency as equal-weight\n * peers (same language as AssetMarketplacePanel's PrimaryPrice), scaled\n * down for card density — no \"PRICE\" eyebrow label, no divider text\n * duplicating what the coin chip already conveys.\n */\nfunction CardDualPrice({\n amountFormatted, currency, usdValue,\n}: {\n amountFormatted: string | null | undefined;\n currency: string | null | undefined;\n usdValue?: string | null;\n}) {\n if (!amountFormatted) return null;\n const cryptoDisplay = formatDisplayPrice(amountFormatted);\n const displayFace = \"font-[family-name:var(--font-display)] font-extrabold tracking-tight tabular-nums\";\n\n if (!usdValue) {\n return (\n <p className={`flex items-center gap-1.5 ${displayFace} text-lg`}>\n <CoinChip symbol={currency} size={11} />\n {cryptoDisplay}\n </p>\n );\n }\n\n const stable = isStableCurrency(currency);\n return (\n <div className=\"flex items-center gap-2\">\n <span className={`${displayFace} text-lg`}>{usdValue}</span>\n {!stable && <span className=\"h-4 w-px bg-border/60 shrink-0\" />}\n <span className=\"inline-flex items-center gap-1.5\">\n <CoinChip symbol={currency} size={stable ? 9 : 11} />\n {stable ? (\n <span className=\"text-2xs font-semibold text-muted-foreground\">{currency}</span>\n ) : (\n <span className={`${displayFace} text-sm text-foreground/80`}>{cryptoDisplay}</span>\n )}\n </span>\n </div>\n );\n}\n\nexport interface ListingCardProps {\n order: ApiOrder;\n inCart?: boolean;\n onBuy?: (order: ApiOrder) => void;\n onCart?: (order: ApiOrder) => void;\n /** App passes a fully constructed <DropdownMenu> here */\n overflowMenu?: React.ReactNode;\n /**\n * Optional flex-1 primary control for listings, replacing the default\n * Buy/View button — lets a host inject its own action (e.g. an owner\n * \"Cancel\" with an app-specific, auth-coupled handler) while keeping this\n * card's layout. Ignored for offers (which always render \"View asset\").\n */\n primaryAction?: React.ReactNode;\n compact?: boolean;\n /**\n * Pre-resolved, browser-loadable image URL. When provided (including null),\n * overrides the card's internal ipfsToHttp resolution — apps that proxy or\n * resize images (e.g. a same-origin /api/ipfs?w= proxy) pass it here.\n * Omit for the default gateway resolution; null renders the no-image fallback.\n */\n imageUrl?: string | null;\n /**\n * Pre-formatted USD equivalent of order.price (e.g. \"$12.34\"), computed by\n * the host from its own live rate feed — this package has no price-feed\n * access by design. Omit (or pass null) to render no USD line, e.g. when\n * the host has no live rate for this order's currency.\n */\n usdValue?: string | null;\n}\n\nexport function ListingCard({ order: rawOrder, inCart = false, onBuy, onCart, overflowMenu, primaryAction, compact = false, imageUrl, usdValue }: ListingCardProps) {\n const order = rawOrder as OrderWithAnimation;\n const [imgError, setImgError] = useState(false);\n const isListing = order.offer.itemType === \"ERC721\" || order.offer.itemType === \"ERC1155\";\n const name = order.token?.name ?? `Token #${order.nftTokenId}`;\n const image = imageUrl !== undefined ? imageUrl : (order.token?.image ? ipfsToHttp(order.token.image) : null);\n // Not resolved through ipfsToHttp — animation_url is legitimately a data: URI for\n // fully on-chain-rendered collections (ipfsToHttp rejects data: by design).\n const animationUrl = order.token?.animationUrl ?? null;\n const live = !!(order.chain && order.nftContract) && isLivingRenderCollection(order.chain.toUpperCase() as Chain, order.nftContract!);\n const assetHref = `/asset/${order.nftContract}/${order.nftTokenId}`;\n\n // Show the action bar for listings (Buy/View) and for offers (View asset),\n // whenever the host wired any action or an overflow menu.\n const showActionBar = !!(onBuy || onCart || overflowMenu || primaryAction);\n\n // ─── Compact variant ──────────────────────────────────────────────────────\n if (compact) {\n return (\n <MotionCard className=\"card-base\">\n <Link href={assetHref} className=\"block\">\n <div className=\"relative aspect-square bg-muted overflow-hidden\">\n <AnimatedTokenMedia\n image={image}\n animationUrl={animationUrl}\n live={live}\n alt={name}\n mode=\"fill\"\n sizes=\"(max-width: 640px) 33vw, 20vw\"\n className=\"object-cover group-hover:scale-105 transition-transform duration-500\"\n imgError={imgError}\n onImageError={() => setImgError(true)}\n fallback={\n <div className=\"absolute inset-0 flex items-center justify-center bg-gradient-to-br from-brand-purple/15 to-brand-blue/15\">\n <span className=\"text-xl font-mono text-muted-foreground\">#{order.nftTokenId}</span>\n </div>\n }\n />\n </div>\n <div className=\"p-2.5 space-y-0.5\">\n <p className=\"text-xs font-semibold truncate\">{name}</p>\n <PriceLine amountFormatted={order.price?.formatted} currency={order.price?.currency} usdValue={usdValue} />\n <p className=\"text-2xs text-muted-foreground\">{timeAgo(order.createdAt)}</p>\n </div>\n </Link>\n </MotionCard>\n );\n }\n\n // ─── Full variant ─────────────────────────────────────────────────────────\n return (\n <MotionCard className=\"card-base\">\n <Link href={assetHref} className=\"block\">\n <div className=\"relative aspect-square bg-muted overflow-hidden\">\n <AnimatedTokenMedia\n image={image}\n animationUrl={animationUrl}\n live={live}\n alt={name}\n mode=\"fill\"\n sizes=\"(max-width: 640px) 50vw, (max-width: 1024px) 33vw, 25vw\"\n className=\"object-cover group-hover:scale-105 transition-transform duration-500\"\n imgError={imgError}\n onImageError={() => setImgError(true)}\n fallback={\n <div className=\"absolute inset-0 flex items-center justify-center bg-gradient-to-br from-brand-purple/15 to-brand-blue/15\">\n <span className=\"text-2xl font-mono text-muted-foreground\">#{order.nftTokenId}</span>\n </div>\n }\n />\n </div>\n\n <div className=\"p-3.5 space-y-3\">\n <div className=\"min-w-0\">\n <p className=\"font-semibold text-base truncate leading-snug\">{name}</p>\n <p className=\"text-2xs text-muted-foreground truncate leading-snug mt-0.5\">\n {order.token?.description ? order.token.description : `#${order.nftTokenId}`}\n </p>\n </div>\n\n {order.price?.formatted && (\n <CardDualPrice amountFormatted={order.price.formatted} currency={order.price.currency} usdValue={usdValue} />\n )}\n\n {showActionBar && (\n <div className=\"flex items-center gap-1.5\">\n {isListing ? (\n primaryAction ? (\n primaryAction\n ) : onBuy ? (\n <div className=\"btn-border-animated p-[1.5px] rounded-[10px] flex-1 h-9\">\n <button\n className=\"w-full h-full rounded-[9px] bg-background flex items-center justify-center gap-1.5 text-xs font-semibold text-foreground hover:bg-muted/60 transition-all active:scale-[0.98]\"\n onClick={(e) => { e.preventDefault(); onBuy(order); }}\n >\n <Zap className=\"h-3.5 w-3.5 shrink-0\" /> Buy\n </button>\n </div>\n ) : (\n <Link\n href={assetHref}\n className=\"flex-1 h-9 inline-flex items-center justify-center gap-1.5 rounded-[9px] border border-border bg-background text-xs font-semibold text-foreground hover:bg-muted/60 transition-all active:scale-[0.98]\"\n >\n <ArrowUpRight className=\"h-3.5 w-3.5 shrink-0\" /> View\n </Link>\n )\n ) : (\n <Link\n href={assetHref}\n className=\"flex-1 h-9 inline-flex items-center justify-center gap-1.5 rounded-[9px] border border-border bg-background text-xs font-semibold text-foreground hover:bg-muted/60 transition-all active:scale-[0.98]\"\n >\n <ArrowUpRight className=\"h-3.5 w-3.5 shrink-0\" /> View asset\n </Link>\n )}\n {onCart && (\n <button\n className={cn(\n \"min-h-11 min-w-11 shrink-0 rounded-[9px] border flex items-center justify-center transition-colors\",\n inCart\n ? \"border-brand-orange/50 bg-brand-orange/10 text-brand-orange\"\n : \"border-border bg-background hover:bg-muted text-foreground\"\n )}\n onClick={(e) => { e.preventDefault(); if (!inCart) onCart(order); }}\n disabled={inCart}\n aria-label={inCart ? \"Added to cart\" : \"Add to cart\"}\n >\n {inCart ? <Check className=\"h-3.5 w-3.5\" /> : <ShoppingCart className=\"h-3.5 w-3.5\" />}\n </button>\n )}\n {overflowMenu}\n </div>\n )}\n </div>\n </Link>\n </MotionCard>\n );\n}\n\nexport function ListingCardSkeleton() {\n return (\n <div className=\"card-base\">\n <div className=\"aspect-square w-full bg-muted animate-pulse\" />\n <div className=\"p-3 space-y-2.5\">\n <div className=\"space-y-1\">\n <div className=\"h-4 w-3/4 bg-muted animate-pulse rounded\" />\n <div className=\"h-3 w-1/2 bg-muted animate-pulse rounded\" />\n </div>\n <div className=\"h-6 w-1/2 bg-muted animate-pulse rounded\" />\n <div className=\"h-3 w-2/3 bg-muted animate-pulse rounded\" />\n </div>\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|