@medialane/ui 0.13.1 → 0.15.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/README.md +31 -5
- package/dist/components/coin-card.cjs +131 -0
- package/dist/components/coin-card.cjs.map +1 -0
- package/dist/components/coin-card.d.cts +19 -0
- package/dist/components/coin-card.d.ts +19 -0
- package/dist/components/coin-card.js +99 -0
- package/dist/components/coin-card.js.map +1 -0
- package/dist/components/coins-explorer.cjs +117 -0
- package/dist/components/coins-explorer.cjs.map +1 -0
- package/dist/components/coins-explorer.d.cts +23 -0
- package/dist/components/coins-explorer.d.ts +23 -0
- package/dist/components/coins-explorer.js +93 -0
- package/dist/components/coins-explorer.js.map +1 -0
- package/dist/components/ip-type-display.cjs +26 -1
- package/dist/components/ip-type-display.cjs.map +1 -1
- package/dist/components/ip-type-display.js +27 -2
- package/dist/components/ip-type-display.js.map +1 -1
- package/dist/data/coins.cjs +48 -0
- package/dist/data/coins.cjs.map +1 -0
- package/dist/data/coins.d.cts +33 -0
- package/dist/data/coins.d.ts +33 -0
- package/dist/data/coins.js +22 -0
- package/dist/data/coins.js.map +1 -0
- package/dist/data/ip-templates.cjs +13 -0
- package/dist/data/ip-templates.cjs.map +1 -1
- package/dist/data/ip-templates.d.cts +16 -1
- package/dist/data/ip-templates.d.ts +16 -1
- package/dist/data/ip-templates.js +12 -0
- package/dist/data/ip-templates.js.map +1 -1
- package/dist/index.cjs +19 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.js +19 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useState, useMemo } from "react";
|
|
4
|
+
import { Coins, LayoutGrid, List, Search } from "lucide-react";
|
|
5
|
+
import { cn } from "../utils/cn.js";
|
|
6
|
+
import { CoinCard, CoinRow, CoinCardSkeleton } from "./coin-card.js";
|
|
7
|
+
const FILTER_TABS = [
|
|
8
|
+
{ label: "All", value: "all" },
|
|
9
|
+
{ label: "Creator Coins", value: "creator" },
|
|
10
|
+
{ label: "Memecoins", value: "memecoin" }
|
|
11
|
+
];
|
|
12
|
+
const SORT_OPTIONS = [
|
|
13
|
+
{ label: "Recently launched", value: "recent" },
|
|
14
|
+
{ label: "Name", value: "name" }
|
|
15
|
+
];
|
|
16
|
+
function CoinsExplorer({ useCoins, usePrice, coinHref, heading = true }) {
|
|
17
|
+
const [filter, setFilter] = useState("all");
|
|
18
|
+
const [sort, setSort] = useState("recent");
|
|
19
|
+
const [view, setView] = useState("grid");
|
|
20
|
+
const [query, setQuery] = useState("");
|
|
21
|
+
const { collections, isLoading } = useCoins({ filter, sort });
|
|
22
|
+
const items = useMemo(() => {
|
|
23
|
+
const q = query.trim().toLowerCase();
|
|
24
|
+
if (!q) return collections;
|
|
25
|
+
return collections.filter(
|
|
26
|
+
(c) => (c.name ?? "").toLowerCase().includes(q) || (c.symbol ?? "").toLowerCase().includes(q)
|
|
27
|
+
);
|
|
28
|
+
}, [collections, query]);
|
|
29
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-6", children: [
|
|
30
|
+
heading && /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
|
|
31
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 text-primary", children: [
|
|
32
|
+
/* @__PURE__ */ jsx(Coins, { className: "h-5 w-5" }),
|
|
33
|
+
/* @__PURE__ */ jsx("span", { className: "text-sm font-semibold uppercase tracking-wider", children: "Tokens" })
|
|
34
|
+
] }),
|
|
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." })
|
|
37
|
+
] }),
|
|
38
|
+
/* @__PURE__ */ jsxs("div", { className: "space-y-3 border-b border-border/60 pb-3", children: [
|
|
39
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center justify-between gap-3", children: [
|
|
40
|
+
/* @__PURE__ */ jsx("div", { className: "flex gap-1.5", children: FILTER_TABS.map(({ label, value }) => /* @__PURE__ */ jsx(
|
|
41
|
+
"button",
|
|
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: [
|
|
53
|
+
/* @__PURE__ */ jsx(
|
|
54
|
+
"select",
|
|
55
|
+
{
|
|
56
|
+
value: sort,
|
|
57
|
+
onChange: (e) => setSort(e.target.value),
|
|
58
|
+
className: "rounded-lg border border-border bg-background px-3 py-1.5 text-xs font-medium text-foreground",
|
|
59
|
+
children: SORT_OPTIONS.map((o) => /* @__PURE__ */ jsx("option", { value: o.value, children: o.label }, o.value))
|
|
60
|
+
}
|
|
61
|
+
),
|
|
62
|
+
/* @__PURE__ */ jsx("div", { className: "inline-flex rounded-lg border border-border p-0.5", children: [{ v: "grid", Icon: LayoutGrid }, { v: "table", Icon: List }].map(({ v, Icon }) => /* @__PURE__ */ jsx(
|
|
63
|
+
"button",
|
|
64
|
+
{
|
|
65
|
+
onClick: () => setView(v),
|
|
66
|
+
"aria-label": v === "grid" ? "Grid view" : "Table view",
|
|
67
|
+
className: cn("rounded-md p-1.5 transition-colors", view === v ? "bg-primary/10 text-primary" : "text-muted-foreground hover:text-foreground"),
|
|
68
|
+
children: /* @__PURE__ */ jsx(Icon, { className: "h-4 w-4" })
|
|
69
|
+
},
|
|
70
|
+
v
|
|
71
|
+
)) })
|
|
72
|
+
] })
|
|
73
|
+
] }),
|
|
74
|
+
/* @__PURE__ */ jsxs("div", { className: "relative", children: [
|
|
75
|
+
/* @__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" }),
|
|
76
|
+
/* @__PURE__ */ jsx(
|
|
77
|
+
"input",
|
|
78
|
+
{
|
|
79
|
+
value: query,
|
|
80
|
+
onChange: (e) => setQuery(e.target.value),
|
|
81
|
+
placeholder: "Search coins by name or symbol\u2026",
|
|
82
|
+
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
|
+
}
|
|
84
|
+
)
|
|
85
|
+
] })
|
|
86
|
+
] }),
|
|
87
|
+
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}`)) })
|
|
88
|
+
] });
|
|
89
|
+
}
|
|
90
|
+
export {
|
|
91
|
+
CoinsExplorer
|
|
92
|
+
};
|
|
93
|
+
//# sourceMappingURL=coins-explorer.js.map
|
|
@@ -0,0 +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":[]}
|
|
@@ -24,6 +24,7 @@ __export(ip_type_display_exports, {
|
|
|
24
24
|
module.exports = __toCommonJS(ip_type_display_exports);
|
|
25
25
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
26
26
|
var import_ip_templates = require("../data/ip-templates.js");
|
|
27
|
+
var import_ipfs = require("../utils/ipfs.js");
|
|
27
28
|
var import_lucide_react = require("lucide-react");
|
|
28
29
|
function parseYouTubeEmbed(url) {
|
|
29
30
|
try {
|
|
@@ -125,8 +126,32 @@ function IPTypeDisplay({ attributes }) {
|
|
|
125
126
|
const value = getAttr(meta.traitKey);
|
|
126
127
|
return value ? [{ platform, meta, value }] : [];
|
|
127
128
|
});
|
|
128
|
-
|
|
129
|
+
const docUri = template.docUpload ? getAttr(template.docUpload.traitType) : null;
|
|
130
|
+
if (embeds.length === 0 && socials.length === 0 && !docUri) return null;
|
|
129
131
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "space-y-5", children: [
|
|
132
|
+
docUri && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "space-y-1.5", children: [
|
|
133
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "text-xs font-semibold uppercase tracking-wider text-muted-foreground", children: "Document" }),
|
|
134
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
135
|
+
"a",
|
|
136
|
+
{
|
|
137
|
+
href: docUri.startsWith("ipfs://") ? (0, import_ipfs.ipfsToHttp)(docUri) : docUri,
|
|
138
|
+
target: "_blank",
|
|
139
|
+
rel: "noopener noreferrer",
|
|
140
|
+
className: "flex items-center gap-3 rounded-xl border border-border bg-muted/20 px-4 py-3 transition-colors hover:border-primary/40 group",
|
|
141
|
+
children: [
|
|
142
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.FileText, { className: "h-5 w-5 text-primary shrink-0" }),
|
|
143
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "min-w-0 flex-1", children: [
|
|
144
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "text-sm font-semibold group-hover:text-primary transition-colors", children: "View document" }),
|
|
145
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { className: "text-xs text-muted-foreground flex items-center gap-1 mt-0.5", children: [
|
|
146
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.ShieldCheck, { className: "h-3 w-3 shrink-0" }),
|
|
147
|
+
"Stored on IPFS \u2014 immutable, timestamped copy of the original"
|
|
148
|
+
] })
|
|
149
|
+
] }),
|
|
150
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.ExternalLink, { className: "h-3.5 w-3.5 text-muted-foreground shrink-0" })
|
|
151
|
+
]
|
|
152
|
+
}
|
|
153
|
+
)
|
|
154
|
+
] }),
|
|
130
155
|
embeds.map(({ platform, meta, value }) => {
|
|
131
156
|
const src = getEmbedSrc(platform, value);
|
|
132
157
|
if (src) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/ip-type-display.tsx"],"sourcesContent":["\"use client\";\n\nimport type { IPType } from \"../data/ip.js\";\nimport {\n IP_TEMPLATES,\n EMBED_PLATFORM_META,\n SOCIAL_PLATFORM_META,\n type EmbedPlatform,\n} from \"../data/ip-templates.js\";\nimport { ExternalLink } from \"lucide-react\";\n\ninterface Attr {\n trait_type?: string | null;\n value?: string | null;\n}\n\ninterface IPTypeDisplayProps {\n attributes: Attr[] | null | undefined;\n}\n\n// ── Embed URL parsers ────────────────────────────────────────────────────────\n\nfunction parseYouTubeEmbed(url: string): string | null {\n try {\n const u = new URL(url);\n let id: string | null = null;\n if (u.hostname.includes(\"youtu.be\")) {\n id = u.pathname.slice(1);\n } else if (u.hostname.includes(\"youtube.com\")) {\n id = u.searchParams.get(\"v\");\n }\n if (!id) return null;\n return `https://www.youtube.com/embed/${id}`;\n } catch {\n return null;\n }\n}\n\nfunction parseSpotifyEmbed(url: string): string | null {\n try {\n const u = new URL(url);\n if (!u.hostname.includes(\"spotify.com\")) return null;\n // Spotify's embed endpoint only accepts /embed/{type}/{id}. Isolate the\n // resource type + id so locale prefixes (e.g. /intl-pt) and trailing query\n // params don't leak through — `/embed/intl-pt/album/…` 404s on Spotify.\n const match = u.pathname.match(\n /(track|album|playlist|episode|show|artist)\\/([A-Za-z0-9]+)/\n );\n if (!match) return null;\n return `https://open.spotify.com/embed/${match[1]}/${match[2]}`;\n } catch {\n return null;\n }\n}\n\nfunction parseSoundCloudEmbed(url: string): string | null {\n try {\n new URL(url); // validate\n if (!url.includes(\"soundcloud.com\")) return null;\n const encoded = encodeURIComponent(url);\n return `https://w.soundcloud.com/player/?url=${encoded}&color=%23ff5500&auto_play=false&hide_related=true&show_comments=false&show_user=true&show_reposts=false`;\n } catch {\n return null;\n }\n}\n\nfunction parseTikTokEmbed(url: string): string | null {\n try {\n const u = new URL(url);\n if (!u.hostname.includes(\"tiktok.com\")) return null;\n const match = u.pathname.match(/\\/video\\/(\\d+)/);\n if (!match) return null;\n return `https://www.tiktok.com/embed/v2/${match[1]}`;\n } catch {\n return null;\n }\n}\n\nfunction parseVimeoEmbed(url: string): string | null {\n try {\n const u = new URL(url);\n if (!u.hostname.includes(\"vimeo.com\")) return null;\n const match = u.pathname.match(/(\\d+)/);\n if (!match) return null;\n return `https://player.vimeo.com/video/${match[1]}`;\n } catch {\n return null;\n }\n}\n\nfunction getEmbedSrc(platform: EmbedPlatform, value: string): string | null {\n switch (platform) {\n case \"youtube\": return parseYouTubeEmbed(value);\n case \"spotify\": return parseSpotifyEmbed(value);\n case \"soundcloud\": return parseSoundCloudEmbed(value);\n case \"tiktok\": return parseTikTokEmbed(value);\n case \"vimeo\": return parseVimeoEmbed(value);\n }\n}\n\n// Compact iframe (fixed height) vs 16:9 video frame.\nconst COMPACT: Record<EmbedPlatform, boolean> = {\n spotify: true,\n soundcloud: true,\n youtube: false,\n tiktok: false,\n vimeo: false,\n};\n\n// ── Component ─────────────────────────────────────────────────────────────────\n\nexport function IPTypeDisplay({ attributes }: IPTypeDisplayProps) {\n const attrs = attributes ?? [];\n\n const ipType = attrs.find(\n (a) => a.trait_type?.toLowerCase() === \"ip type\"\n )?.value as IPType | undefined;\n if (!ipType) return null;\n\n const template = IP_TEMPLATES[ipType];\n if (!template) return null;\n\n const getAttr = (key: string) =>\n attrs.find((a) => a.trait_type === key)?.value ?? null;\n\n const embeds = (template.embeds ?? []).flatMap((platform) => {\n const meta = EMBED_PLATFORM_META[platform];\n const value = getAttr(meta.traitKey);\n return value ? [{ platform, meta, value }] : [];\n });\n\n const socials = (template.socials ?? []).flatMap((platform) => {\n const meta = SOCIAL_PLATFORM_META[platform];\n const value = getAttr(meta.traitKey);\n return value ? [{ platform, meta, value }] : [];\n });\n\n if (embeds.length === 0 && socials.length === 0) return null;\n\n return (\n <div className=\"space-y-5\">\n {embeds.map(({ platform, meta, value }) => {\n const src = getEmbedSrc(platform, value);\n if (src) {\n return (\n <div key={platform} className=\"space-y-1.5\">\n <p className=\"text-xs font-semibold uppercase tracking-wider text-muted-foreground\">\n {meta.label}\n </p>\n {COMPACT[platform] ? (\n <iframe\n src={src}\n className=\"w-full rounded-xl border-0\"\n height={166}\n allow=\"autoplay\"\n loading=\"lazy\"\n title={meta.label}\n />\n ) : (\n <div className=\"relative w-full aspect-video rounded-xl overflow-hidden bg-muted/20\">\n <iframe\n src={src}\n className=\"absolute inset-0 w-full h-full\"\n allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture\"\n allowFullScreen\n loading=\"lazy\"\n title={meta.label}\n />\n </div>\n )}\n </div>\n );\n }\n // Fallback: plain external link if URL parsing failed\n return (\n <div key={platform}>\n <p className=\"text-xs font-semibold uppercase tracking-wider text-muted-foreground mb-1\">\n {meta.label}\n </p>\n <a\n href={value}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"inline-flex items-center gap-1.5 text-sm text-primary hover:underline\"\n >\n <ExternalLink className=\"h-3.5 w-3.5\" />\n Open link\n </a>\n </div>\n );\n })}\n\n {socials.length > 0 && (\n <div className=\"space-y-1.5\">\n <p className=\"text-xs font-semibold uppercase tracking-wider text-muted-foreground\">\n Links\n </p>\n <div className=\"flex flex-wrap gap-2\">\n {socials.map(({ platform, meta, value }) => {\n const SIcon = meta.icon;\n return (\n <a\n key={platform}\n href={value}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"inline-flex items-center gap-1.5 rounded-full border border-border bg-muted/30 px-3 py-1.5 text-xs font-medium text-muted-foreground transition-colors hover:border-primary/40 hover:text-foreground\"\n >\n <SIcon className=\"h-3.5 w-3.5\" />\n {meta.label}\n </a>\n );\n })}\n </div>\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/ip-type-display.tsx"],"sourcesContent":["\"use client\";\n\nimport type { IPType } from \"../data/ip.js\";\nimport {\n IP_TEMPLATES,\n EMBED_PLATFORM_META,\n SOCIAL_PLATFORM_META,\n type EmbedPlatform,\n} from \"../data/ip-templates.js\";\nimport { ipfsToHttp } from \"../utils/ipfs.js\";\nimport { ExternalLink, FileText, ShieldCheck } from \"lucide-react\";\n\ninterface Attr {\n trait_type?: string | null;\n value?: string | null;\n}\n\ninterface IPTypeDisplayProps {\n attributes: Attr[] | null | undefined;\n}\n\n// ── Embed URL parsers ────────────────────────────────────────────────────────\n\nfunction parseYouTubeEmbed(url: string): string | null {\n try {\n const u = new URL(url);\n let id: string | null = null;\n if (u.hostname.includes(\"youtu.be\")) {\n id = u.pathname.slice(1);\n } else if (u.hostname.includes(\"youtube.com\")) {\n id = u.searchParams.get(\"v\");\n }\n if (!id) return null;\n return `https://www.youtube.com/embed/${id}`;\n } catch {\n return null;\n }\n}\n\nfunction parseSpotifyEmbed(url: string): string | null {\n try {\n const u = new URL(url);\n if (!u.hostname.includes(\"spotify.com\")) return null;\n // Spotify's embed endpoint only accepts /embed/{type}/{id}. Isolate the\n // resource type + id so locale prefixes (e.g. /intl-pt) and trailing query\n // params don't leak through — `/embed/intl-pt/album/…` 404s on Spotify.\n const match = u.pathname.match(\n /(track|album|playlist|episode|show|artist)\\/([A-Za-z0-9]+)/\n );\n if (!match) return null;\n return `https://open.spotify.com/embed/${match[1]}/${match[2]}`;\n } catch {\n return null;\n }\n}\n\nfunction parseSoundCloudEmbed(url: string): string | null {\n try {\n new URL(url); // validate\n if (!url.includes(\"soundcloud.com\")) return null;\n const encoded = encodeURIComponent(url);\n return `https://w.soundcloud.com/player/?url=${encoded}&color=%23ff5500&auto_play=false&hide_related=true&show_comments=false&show_user=true&show_reposts=false`;\n } catch {\n return null;\n }\n}\n\nfunction parseTikTokEmbed(url: string): string | null {\n try {\n const u = new URL(url);\n if (!u.hostname.includes(\"tiktok.com\")) return null;\n const match = u.pathname.match(/\\/video\\/(\\d+)/);\n if (!match) return null;\n return `https://www.tiktok.com/embed/v2/${match[1]}`;\n } catch {\n return null;\n }\n}\n\nfunction parseVimeoEmbed(url: string): string | null {\n try {\n const u = new URL(url);\n if (!u.hostname.includes(\"vimeo.com\")) return null;\n const match = u.pathname.match(/(\\d+)/);\n if (!match) return null;\n return `https://player.vimeo.com/video/${match[1]}`;\n } catch {\n return null;\n }\n}\n\nfunction getEmbedSrc(platform: EmbedPlatform, value: string): string | null {\n switch (platform) {\n case \"youtube\": return parseYouTubeEmbed(value);\n case \"spotify\": return parseSpotifyEmbed(value);\n case \"soundcloud\": return parseSoundCloudEmbed(value);\n case \"tiktok\": return parseTikTokEmbed(value);\n case \"vimeo\": return parseVimeoEmbed(value);\n }\n}\n\n// Compact iframe (fixed height) vs 16:9 video frame.\nconst COMPACT: Record<EmbedPlatform, boolean> = {\n spotify: true,\n soundcloud: true,\n youtube: false,\n tiktok: false,\n vimeo: false,\n};\n\n// ── Component ─────────────────────────────────────────────────────────────────\n\nexport function IPTypeDisplay({ attributes }: IPTypeDisplayProps) {\n const attrs = attributes ?? [];\n\n const ipType = attrs.find(\n (a) => a.trait_type?.toLowerCase() === \"ip type\"\n )?.value as IPType | undefined;\n if (!ipType) return null;\n\n const template = IP_TEMPLATES[ipType];\n if (!template) return null;\n\n const getAttr = (key: string) =>\n attrs.find((a) => a.trait_type === key)?.value ?? null;\n\n const embeds = (template.embeds ?? []).flatMap((platform) => {\n const meta = EMBED_PLATFORM_META[platform];\n const value = getAttr(meta.traitKey);\n return value ? [{ platform, meta, value }] : [];\n });\n\n const socials = (template.socials ?? []).flatMap((platform) => {\n const meta = SOCIAL_PLATFORM_META[platform];\n const value = getAttr(meta.traitKey);\n return value ? [{ platform, meta, value }] : [];\n });\n\n const docUri = template.docUpload ? getAttr(template.docUpload.traitType) : null;\n\n if (embeds.length === 0 && socials.length === 0 && !docUri) return null;\n\n return (\n <div className=\"space-y-5\">\n {/* Document pinned to IPFS — immutable, timestamped copy of the work */}\n {docUri && (\n <div className=\"space-y-1.5\">\n <p className=\"text-xs font-semibold uppercase tracking-wider text-muted-foreground\">\n Document\n </p>\n <a\n href={docUri.startsWith(\"ipfs://\") ? ipfsToHttp(docUri) : docUri}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"flex items-center gap-3 rounded-xl border border-border bg-muted/20 px-4 py-3 transition-colors hover:border-primary/40 group\"\n >\n <FileText className=\"h-5 w-5 text-primary shrink-0\" />\n <div className=\"min-w-0 flex-1\">\n <p className=\"text-sm font-semibold group-hover:text-primary transition-colors\">\n View document\n </p>\n <p className=\"text-xs text-muted-foreground flex items-center gap-1 mt-0.5\">\n <ShieldCheck className=\"h-3 w-3 shrink-0\" />\n Stored on IPFS — immutable, timestamped copy of the original\n </p>\n </div>\n <ExternalLink className=\"h-3.5 w-3.5 text-muted-foreground shrink-0\" />\n </a>\n </div>\n )}\n {embeds.map(({ platform, meta, value }) => {\n const src = getEmbedSrc(platform, value);\n if (src) {\n return (\n <div key={platform} className=\"space-y-1.5\">\n <p className=\"text-xs font-semibold uppercase tracking-wider text-muted-foreground\">\n {meta.label}\n </p>\n {COMPACT[platform] ? (\n <iframe\n src={src}\n className=\"w-full rounded-xl border-0\"\n height={166}\n allow=\"autoplay\"\n loading=\"lazy\"\n title={meta.label}\n />\n ) : (\n <div className=\"relative w-full aspect-video rounded-xl overflow-hidden bg-muted/20\">\n <iframe\n src={src}\n className=\"absolute inset-0 w-full h-full\"\n allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture\"\n allowFullScreen\n loading=\"lazy\"\n title={meta.label}\n />\n </div>\n )}\n </div>\n );\n }\n // Fallback: plain external link if URL parsing failed\n return (\n <div key={platform}>\n <p className=\"text-xs font-semibold uppercase tracking-wider text-muted-foreground mb-1\">\n {meta.label}\n </p>\n <a\n href={value}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"inline-flex items-center gap-1.5 text-sm text-primary hover:underline\"\n >\n <ExternalLink className=\"h-3.5 w-3.5\" />\n Open link\n </a>\n </div>\n );\n })}\n\n {socials.length > 0 && (\n <div className=\"space-y-1.5\">\n <p className=\"text-xs font-semibold uppercase tracking-wider text-muted-foreground\">\n Links\n </p>\n <div className=\"flex flex-wrap gap-2\">\n {socials.map(({ platform, meta, value }) => {\n const SIcon = meta.icon;\n return (\n <a\n key={platform}\n href={value}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"inline-flex items-center gap-1.5 rounded-full border border-border bg-muted/30 px-3 py-1.5 text-xs font-medium text-muted-foreground transition-colors hover:border-primary/40 hover:text-foreground\"\n >\n <SIcon className=\"h-3.5 w-3.5\" />\n {meta.label}\n </a>\n );\n })}\n </div>\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -5,7 +5,8 @@ import {
|
|
|
5
5
|
EMBED_PLATFORM_META,
|
|
6
6
|
SOCIAL_PLATFORM_META
|
|
7
7
|
} from "../data/ip-templates.js";
|
|
8
|
-
import {
|
|
8
|
+
import { ipfsToHttp } from "../utils/ipfs.js";
|
|
9
|
+
import { ExternalLink, FileText, ShieldCheck } from "lucide-react";
|
|
9
10
|
function parseYouTubeEmbed(url) {
|
|
10
11
|
try {
|
|
11
12
|
const u = new URL(url);
|
|
@@ -106,8 +107,32 @@ function IPTypeDisplay({ attributes }) {
|
|
|
106
107
|
const value = getAttr(meta.traitKey);
|
|
107
108
|
return value ? [{ platform, meta, value }] : [];
|
|
108
109
|
});
|
|
109
|
-
|
|
110
|
+
const docUri = template.docUpload ? getAttr(template.docUpload.traitType) : null;
|
|
111
|
+
if (embeds.length === 0 && socials.length === 0 && !docUri) return null;
|
|
110
112
|
return /* @__PURE__ */ jsxs("div", { className: "space-y-5", children: [
|
|
113
|
+
docUri && /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
|
|
114
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs font-semibold uppercase tracking-wider text-muted-foreground", children: "Document" }),
|
|
115
|
+
/* @__PURE__ */ jsxs(
|
|
116
|
+
"a",
|
|
117
|
+
{
|
|
118
|
+
href: docUri.startsWith("ipfs://") ? ipfsToHttp(docUri) : docUri,
|
|
119
|
+
target: "_blank",
|
|
120
|
+
rel: "noopener noreferrer",
|
|
121
|
+
className: "flex items-center gap-3 rounded-xl border border-border bg-muted/20 px-4 py-3 transition-colors hover:border-primary/40 group",
|
|
122
|
+
children: [
|
|
123
|
+
/* @__PURE__ */ jsx(FileText, { className: "h-5 w-5 text-primary shrink-0" }),
|
|
124
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
|
|
125
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm font-semibold group-hover:text-primary transition-colors", children: "View document" }),
|
|
126
|
+
/* @__PURE__ */ jsxs("p", { className: "text-xs text-muted-foreground flex items-center gap-1 mt-0.5", children: [
|
|
127
|
+
/* @__PURE__ */ jsx(ShieldCheck, { className: "h-3 w-3 shrink-0" }),
|
|
128
|
+
"Stored on IPFS \u2014 immutable, timestamped copy of the original"
|
|
129
|
+
] })
|
|
130
|
+
] }),
|
|
131
|
+
/* @__PURE__ */ jsx(ExternalLink, { className: "h-3.5 w-3.5 text-muted-foreground shrink-0" })
|
|
132
|
+
]
|
|
133
|
+
}
|
|
134
|
+
)
|
|
135
|
+
] }),
|
|
111
136
|
embeds.map(({ platform, meta, value }) => {
|
|
112
137
|
const src = getEmbedSrc(platform, value);
|
|
113
138
|
if (src) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/ip-type-display.tsx"],"sourcesContent":["\"use client\";\n\nimport type { IPType } from \"../data/ip.js\";\nimport {\n IP_TEMPLATES,\n EMBED_PLATFORM_META,\n SOCIAL_PLATFORM_META,\n type EmbedPlatform,\n} from \"../data/ip-templates.js\";\nimport { ExternalLink } from \"lucide-react\";\n\ninterface Attr {\n trait_type?: string | null;\n value?: string | null;\n}\n\ninterface IPTypeDisplayProps {\n attributes: Attr[] | null | undefined;\n}\n\n// ── Embed URL parsers ────────────────────────────────────────────────────────\n\nfunction parseYouTubeEmbed(url: string): string | null {\n try {\n const u = new URL(url);\n let id: string | null = null;\n if (u.hostname.includes(\"youtu.be\")) {\n id = u.pathname.slice(1);\n } else if (u.hostname.includes(\"youtube.com\")) {\n id = u.searchParams.get(\"v\");\n }\n if (!id) return null;\n return `https://www.youtube.com/embed/${id}`;\n } catch {\n return null;\n }\n}\n\nfunction parseSpotifyEmbed(url: string): string | null {\n try {\n const u = new URL(url);\n if (!u.hostname.includes(\"spotify.com\")) return null;\n // Spotify's embed endpoint only accepts /embed/{type}/{id}. Isolate the\n // resource type + id so locale prefixes (e.g. /intl-pt) and trailing query\n // params don't leak through — `/embed/intl-pt/album/…` 404s on Spotify.\n const match = u.pathname.match(\n /(track|album|playlist|episode|show|artist)\\/([A-Za-z0-9]+)/\n );\n if (!match) return null;\n return `https://open.spotify.com/embed/${match[1]}/${match[2]}`;\n } catch {\n return null;\n }\n}\n\nfunction parseSoundCloudEmbed(url: string): string | null {\n try {\n new URL(url); // validate\n if (!url.includes(\"soundcloud.com\")) return null;\n const encoded = encodeURIComponent(url);\n return `https://w.soundcloud.com/player/?url=${encoded}&color=%23ff5500&auto_play=false&hide_related=true&show_comments=false&show_user=true&show_reposts=false`;\n } catch {\n return null;\n }\n}\n\nfunction parseTikTokEmbed(url: string): string | null {\n try {\n const u = new URL(url);\n if (!u.hostname.includes(\"tiktok.com\")) return null;\n const match = u.pathname.match(/\\/video\\/(\\d+)/);\n if (!match) return null;\n return `https://www.tiktok.com/embed/v2/${match[1]}`;\n } catch {\n return null;\n }\n}\n\nfunction parseVimeoEmbed(url: string): string | null {\n try {\n const u = new URL(url);\n if (!u.hostname.includes(\"vimeo.com\")) return null;\n const match = u.pathname.match(/(\\d+)/);\n if (!match) return null;\n return `https://player.vimeo.com/video/${match[1]}`;\n } catch {\n return null;\n }\n}\n\nfunction getEmbedSrc(platform: EmbedPlatform, value: string): string | null {\n switch (platform) {\n case \"youtube\": return parseYouTubeEmbed(value);\n case \"spotify\": return parseSpotifyEmbed(value);\n case \"soundcloud\": return parseSoundCloudEmbed(value);\n case \"tiktok\": return parseTikTokEmbed(value);\n case \"vimeo\": return parseVimeoEmbed(value);\n }\n}\n\n// Compact iframe (fixed height) vs 16:9 video frame.\nconst COMPACT: Record<EmbedPlatform, boolean> = {\n spotify: true,\n soundcloud: true,\n youtube: false,\n tiktok: false,\n vimeo: false,\n};\n\n// ── Component ─────────────────────────────────────────────────────────────────\n\nexport function IPTypeDisplay({ attributes }: IPTypeDisplayProps) {\n const attrs = attributes ?? [];\n\n const ipType = attrs.find(\n (a) => a.trait_type?.toLowerCase() === \"ip type\"\n )?.value as IPType | undefined;\n if (!ipType) return null;\n\n const template = IP_TEMPLATES[ipType];\n if (!template) return null;\n\n const getAttr = (key: string) =>\n attrs.find((a) => a.trait_type === key)?.value ?? null;\n\n const embeds = (template.embeds ?? []).flatMap((platform) => {\n const meta = EMBED_PLATFORM_META[platform];\n const value = getAttr(meta.traitKey);\n return value ? [{ platform, meta, value }] : [];\n });\n\n const socials = (template.socials ?? []).flatMap((platform) => {\n const meta = SOCIAL_PLATFORM_META[platform];\n const value = getAttr(meta.traitKey);\n return value ? [{ platform, meta, value }] : [];\n });\n\n if (embeds.length === 0 && socials.length === 0) return null;\n\n return (\n <div className=\"space-y-5\">\n {embeds.map(({ platform, meta, value }) => {\n const src = getEmbedSrc(platform, value);\n if (src) {\n return (\n <div key={platform} className=\"space-y-1.5\">\n <p className=\"text-xs font-semibold uppercase tracking-wider text-muted-foreground\">\n {meta.label}\n </p>\n {COMPACT[platform] ? (\n <iframe\n src={src}\n className=\"w-full rounded-xl border-0\"\n height={166}\n allow=\"autoplay\"\n loading=\"lazy\"\n title={meta.label}\n />\n ) : (\n <div className=\"relative w-full aspect-video rounded-xl overflow-hidden bg-muted/20\">\n <iframe\n src={src}\n className=\"absolute inset-0 w-full h-full\"\n allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture\"\n allowFullScreen\n loading=\"lazy\"\n title={meta.label}\n />\n </div>\n )}\n </div>\n );\n }\n // Fallback: plain external link if URL parsing failed\n return (\n <div key={platform}>\n <p className=\"text-xs font-semibold uppercase tracking-wider text-muted-foreground mb-1\">\n {meta.label}\n </p>\n <a\n href={value}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"inline-flex items-center gap-1.5 text-sm text-primary hover:underline\"\n >\n <ExternalLink className=\"h-3.5 w-3.5\" />\n Open link\n </a>\n </div>\n );\n })}\n\n {socials.length > 0 && (\n <div className=\"space-y-1.5\">\n <p className=\"text-xs font-semibold uppercase tracking-wider text-muted-foreground\">\n Links\n </p>\n <div className=\"flex flex-wrap gap-2\">\n {socials.map(({ platform, meta, value }) => {\n const SIcon = meta.icon;\n return (\n <a\n key={platform}\n href={value}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"inline-flex items-center gap-1.5 rounded-full border border-border bg-muted/30 px-3 py-1.5 text-xs font-medium text-muted-foreground transition-colors hover:border-primary/40 hover:text-foreground\"\n >\n <SIcon className=\"h-3.5 w-3.5\" />\n {meta.label}\n </a>\n );\n })}\n </div>\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/components/ip-type-display.tsx"],"sourcesContent":["\"use client\";\n\nimport type { IPType } from \"../data/ip.js\";\nimport {\n IP_TEMPLATES,\n EMBED_PLATFORM_META,\n SOCIAL_PLATFORM_META,\n type EmbedPlatform,\n} from \"../data/ip-templates.js\";\nimport { ipfsToHttp } from \"../utils/ipfs.js\";\nimport { ExternalLink, FileText, ShieldCheck } from \"lucide-react\";\n\ninterface Attr {\n trait_type?: string | null;\n value?: string | null;\n}\n\ninterface IPTypeDisplayProps {\n attributes: Attr[] | null | undefined;\n}\n\n// ── Embed URL parsers ────────────────────────────────────────────────────────\n\nfunction parseYouTubeEmbed(url: string): string | null {\n try {\n const u = new URL(url);\n let id: string | null = null;\n if (u.hostname.includes(\"youtu.be\")) {\n id = u.pathname.slice(1);\n } else if (u.hostname.includes(\"youtube.com\")) {\n id = u.searchParams.get(\"v\");\n }\n if (!id) return null;\n return `https://www.youtube.com/embed/${id}`;\n } catch {\n return null;\n }\n}\n\nfunction parseSpotifyEmbed(url: string): string | null {\n try {\n const u = new URL(url);\n if (!u.hostname.includes(\"spotify.com\")) return null;\n // Spotify's embed endpoint only accepts /embed/{type}/{id}. Isolate the\n // resource type + id so locale prefixes (e.g. /intl-pt) and trailing query\n // params don't leak through — `/embed/intl-pt/album/…` 404s on Spotify.\n const match = u.pathname.match(\n /(track|album|playlist|episode|show|artist)\\/([A-Za-z0-9]+)/\n );\n if (!match) return null;\n return `https://open.spotify.com/embed/${match[1]}/${match[2]}`;\n } catch {\n return null;\n }\n}\n\nfunction parseSoundCloudEmbed(url: string): string | null {\n try {\n new URL(url); // validate\n if (!url.includes(\"soundcloud.com\")) return null;\n const encoded = encodeURIComponent(url);\n return `https://w.soundcloud.com/player/?url=${encoded}&color=%23ff5500&auto_play=false&hide_related=true&show_comments=false&show_user=true&show_reposts=false`;\n } catch {\n return null;\n }\n}\n\nfunction parseTikTokEmbed(url: string): string | null {\n try {\n const u = new URL(url);\n if (!u.hostname.includes(\"tiktok.com\")) return null;\n const match = u.pathname.match(/\\/video\\/(\\d+)/);\n if (!match) return null;\n return `https://www.tiktok.com/embed/v2/${match[1]}`;\n } catch {\n return null;\n }\n}\n\nfunction parseVimeoEmbed(url: string): string | null {\n try {\n const u = new URL(url);\n if (!u.hostname.includes(\"vimeo.com\")) return null;\n const match = u.pathname.match(/(\\d+)/);\n if (!match) return null;\n return `https://player.vimeo.com/video/${match[1]}`;\n } catch {\n return null;\n }\n}\n\nfunction getEmbedSrc(platform: EmbedPlatform, value: string): string | null {\n switch (platform) {\n case \"youtube\": return parseYouTubeEmbed(value);\n case \"spotify\": return parseSpotifyEmbed(value);\n case \"soundcloud\": return parseSoundCloudEmbed(value);\n case \"tiktok\": return parseTikTokEmbed(value);\n case \"vimeo\": return parseVimeoEmbed(value);\n }\n}\n\n// Compact iframe (fixed height) vs 16:9 video frame.\nconst COMPACT: Record<EmbedPlatform, boolean> = {\n spotify: true,\n soundcloud: true,\n youtube: false,\n tiktok: false,\n vimeo: false,\n};\n\n// ── Component ─────────────────────────────────────────────────────────────────\n\nexport function IPTypeDisplay({ attributes }: IPTypeDisplayProps) {\n const attrs = attributes ?? [];\n\n const ipType = attrs.find(\n (a) => a.trait_type?.toLowerCase() === \"ip type\"\n )?.value as IPType | undefined;\n if (!ipType) return null;\n\n const template = IP_TEMPLATES[ipType];\n if (!template) return null;\n\n const getAttr = (key: string) =>\n attrs.find((a) => a.trait_type === key)?.value ?? null;\n\n const embeds = (template.embeds ?? []).flatMap((platform) => {\n const meta = EMBED_PLATFORM_META[platform];\n const value = getAttr(meta.traitKey);\n return value ? [{ platform, meta, value }] : [];\n });\n\n const socials = (template.socials ?? []).flatMap((platform) => {\n const meta = SOCIAL_PLATFORM_META[platform];\n const value = getAttr(meta.traitKey);\n return value ? [{ platform, meta, value }] : [];\n });\n\n const docUri = template.docUpload ? getAttr(template.docUpload.traitType) : null;\n\n if (embeds.length === 0 && socials.length === 0 && !docUri) return null;\n\n return (\n <div className=\"space-y-5\">\n {/* Document pinned to IPFS — immutable, timestamped copy of the work */}\n {docUri && (\n <div className=\"space-y-1.5\">\n <p className=\"text-xs font-semibold uppercase tracking-wider text-muted-foreground\">\n Document\n </p>\n <a\n href={docUri.startsWith(\"ipfs://\") ? ipfsToHttp(docUri) : docUri}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"flex items-center gap-3 rounded-xl border border-border bg-muted/20 px-4 py-3 transition-colors hover:border-primary/40 group\"\n >\n <FileText className=\"h-5 w-5 text-primary shrink-0\" />\n <div className=\"min-w-0 flex-1\">\n <p className=\"text-sm font-semibold group-hover:text-primary transition-colors\">\n View document\n </p>\n <p className=\"text-xs text-muted-foreground flex items-center gap-1 mt-0.5\">\n <ShieldCheck className=\"h-3 w-3 shrink-0\" />\n Stored on IPFS — immutable, timestamped copy of the original\n </p>\n </div>\n <ExternalLink className=\"h-3.5 w-3.5 text-muted-foreground shrink-0\" />\n </a>\n </div>\n )}\n {embeds.map(({ platform, meta, value }) => {\n const src = getEmbedSrc(platform, value);\n if (src) {\n return (\n <div key={platform} className=\"space-y-1.5\">\n <p className=\"text-xs font-semibold uppercase tracking-wider text-muted-foreground\">\n {meta.label}\n </p>\n {COMPACT[platform] ? (\n <iframe\n src={src}\n className=\"w-full rounded-xl border-0\"\n height={166}\n allow=\"autoplay\"\n loading=\"lazy\"\n title={meta.label}\n />\n ) : (\n <div className=\"relative w-full aspect-video rounded-xl overflow-hidden bg-muted/20\">\n <iframe\n src={src}\n className=\"absolute inset-0 w-full h-full\"\n allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture\"\n allowFullScreen\n loading=\"lazy\"\n title={meta.label}\n />\n </div>\n )}\n </div>\n );\n }\n // Fallback: plain external link if URL parsing failed\n return (\n <div key={platform}>\n <p className=\"text-xs font-semibold uppercase tracking-wider text-muted-foreground mb-1\">\n {meta.label}\n </p>\n <a\n href={value}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"inline-flex items-center gap-1.5 text-sm text-primary hover:underline\"\n >\n <ExternalLink className=\"h-3.5 w-3.5\" />\n Open link\n </a>\n </div>\n );\n })}\n\n {socials.length > 0 && (\n <div className=\"space-y-1.5\">\n <p className=\"text-xs font-semibold uppercase tracking-wider text-muted-foreground\">\n Links\n </p>\n <div className=\"flex flex-wrap gap-2\">\n {socials.map(({ platform, meta, value }) => {\n const SIcon = meta.icon;\n return (\n <a\n key={platform}\n href={value}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"inline-flex items-center gap-1.5 rounded-full border border-border bg-muted/30 px-3 py-1.5 text-xs font-medium text-muted-foreground transition-colors hover:border-primary/40 hover:text-foreground\"\n >\n <SIcon className=\"h-3.5 w-3.5\" />\n {meta.label}\n </a>\n );\n })}\n </div>\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var coins_exports = {};
|
|
20
|
+
__export(coins_exports, {
|
|
21
|
+
coinKind: () => coinKind,
|
|
22
|
+
formatCoinPrice: () => formatCoinPrice,
|
|
23
|
+
formatFdv: () => formatFdv
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(coins_exports);
|
|
26
|
+
function coinKind(service) {
|
|
27
|
+
return service === "external-erc20" ? "memecoin" : "creator";
|
|
28
|
+
}
|
|
29
|
+
function formatCoinPrice(n) {
|
|
30
|
+
if (n === 0) return "0";
|
|
31
|
+
if (n < 1e-6) return n.toExponential(2);
|
|
32
|
+
if (n < 1) return n.toPrecision(3);
|
|
33
|
+
return n.toLocaleString(void 0, { maximumFractionDigits: 4 });
|
|
34
|
+
}
|
|
35
|
+
function formatFdv(quotePerCoin, totalSupply, quoteSymbol) {
|
|
36
|
+
if (quotePerCoin == null || !totalSupply) return null;
|
|
37
|
+
const fdv = quotePerCoin * totalSupply;
|
|
38
|
+
const sym = quoteSymbol ?? "";
|
|
39
|
+
const abbr = fdv >= 1e9 ? `${(fdv / 1e9).toLocaleString(void 0, { maximumFractionDigits: 1 })}B` : fdv >= 1e6 ? `${(fdv / 1e6).toLocaleString(void 0, { maximumFractionDigits: 1 })}M` : fdv >= 1e3 ? `${(fdv / 1e3).toLocaleString(void 0, { maximumFractionDigits: 1 })}K` : fdv.toLocaleString(void 0, { maximumFractionDigits: 2 });
|
|
40
|
+
return sym ? `${abbr} ${sym}` : abbr;
|
|
41
|
+
}
|
|
42
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
43
|
+
0 && (module.exports = {
|
|
44
|
+
coinKind,
|
|
45
|
+
formatCoinPrice,
|
|
46
|
+
formatFdv
|
|
47
|
+
});
|
|
48
|
+
//# sourceMappingURL=coins.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/data/coins.ts"],"sourcesContent":["// Coin discovery — pure, dependency-free helpers + the structural shapes the\n// coin components read. No @medialane/sdk import: ui types shared code\n// structurally (same pattern as CollectionCard's `ApiCollection & {…}`), so a\n// coin on any chain works without bumping ui's SDK. Registry-dependent logic\n// (isCoinCollection / COIN_SERVICE_IDS, price reads) lives in the apps and is\n// injected — see the CoinsExplorer/CoinCard props.\n\nexport type CoinKind = \"creator\" | \"memecoin\";\n\n/** The fields a coin tile reads. An app's `ApiCollection` structurally satisfies\n * this. `chain` is first-class — rendered as a badge; nothing assumes Starknet. */\nexport interface CoinCollectionLike {\n contractAddress: string;\n chain?: string | null;\n name?: string | null;\n symbol?: string | null;\n image?: string | null;\n service?: string | null;\n claimedBy?: string | null;\n holderCount?: number | null;\n totalSupply?: number | null;\n profile?: { image?: string | null } | null;\n}\n\n/** Minimal spot-price shape — the concrete read (Ekubo on Starknet, a DEX on\n * another chain) is injected by the app and structurally satisfies this. */\nexport interface CoinPriceLike {\n quotePerCoin: number;\n quoteSymbol: string | null;\n}\n\n/** Native creator coin vs claimed external memecoin. */\nexport function coinKind(service: string | null | undefined): CoinKind {\n return service === \"external-erc20\" ? \"memecoin\" : \"creator\";\n}\n\n/** Format a quote-per-coin spot price. */\nexport function formatCoinPrice(n: number): string {\n if (n === 0) return \"0\";\n if (n < 0.000001) return n.toExponential(2);\n if (n < 1) return n.toPrecision(3);\n return n.toLocaleString(undefined, { maximumFractionDigits: 4 });\n}\n\n/** Fully-diluted value = price × supply, abbreviated, in the quote symbol.\n * Returns null when price/supply is unknown or zero (external coins aren't\n * supply-indexed, so price × 0 must read \"—\", never \"0\"). */\nexport function formatFdv(\n quotePerCoin: number | null | undefined,\n totalSupply: number | null | undefined,\n quoteSymbol: string | null | undefined\n): string | null {\n if (quotePerCoin == null || !totalSupply) return null;\n const fdv = quotePerCoin * totalSupply;\n const sym = quoteSymbol ?? \"\";\n const abbr =\n fdv >= 1_000_000_000 ? `${(fdv / 1_000_000_000).toLocaleString(undefined, { maximumFractionDigits: 1 })}B` :\n fdv >= 1_000_000 ? `${(fdv / 1_000_000).toLocaleString(undefined, { maximumFractionDigits: 1 })}M` :\n fdv >= 1_000 ? `${(fdv / 1_000).toLocaleString(undefined, { maximumFractionDigits: 1 })}K` :\n fdv.toLocaleString(undefined, { maximumFractionDigits: 2 });\n return sym ? `${abbr} ${sym}` : abbr;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAgCO,SAAS,SAAS,SAA8C;AACrE,SAAO,YAAY,mBAAmB,aAAa;AACrD;AAGO,SAAS,gBAAgB,GAAmB;AACjD,MAAI,MAAM,EAAG,QAAO;AACpB,MAAI,IAAI,KAAU,QAAO,EAAE,cAAc,CAAC;AAC1C,MAAI,IAAI,EAAG,QAAO,EAAE,YAAY,CAAC;AACjC,SAAO,EAAE,eAAe,QAAW,EAAE,uBAAuB,EAAE,CAAC;AACjE;AAKO,SAAS,UACd,cACA,aACA,aACe;AACf,MAAI,gBAAgB,QAAQ,CAAC,YAAa,QAAO;AACjD,QAAM,MAAM,eAAe;AAC3B,QAAM,MAAM,eAAe;AAC3B,QAAM,OACJ,OAAO,MAAgB,IAAI,MAAM,KAAe,eAAe,QAAW,EAAE,uBAAuB,EAAE,CAAC,CAAC,MACvG,OAAO,MAAgB,IAAI,MAAM,KAAW,eAAe,QAAW,EAAE,uBAAuB,EAAE,CAAC,CAAC,MACnG,OAAO,MAAgB,IAAI,MAAM,KAAO,eAAe,QAAW,EAAE,uBAAuB,EAAE,CAAC,CAAC,MACxE,IAAI,eAAe,QAAW,EAAE,uBAAuB,EAAE,CAAC;AACnF,SAAO,MAAM,GAAG,IAAI,IAAI,GAAG,KAAK;AAClC;","names":[]}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
type CoinKind = "creator" | "memecoin";
|
|
2
|
+
/** The fields a coin tile reads. An app's `ApiCollection` structurally satisfies
|
|
3
|
+
* this. `chain` is first-class — rendered as a badge; nothing assumes Starknet. */
|
|
4
|
+
interface CoinCollectionLike {
|
|
5
|
+
contractAddress: string;
|
|
6
|
+
chain?: string | null;
|
|
7
|
+
name?: string | null;
|
|
8
|
+
symbol?: string | null;
|
|
9
|
+
image?: string | null;
|
|
10
|
+
service?: string | null;
|
|
11
|
+
claimedBy?: string | null;
|
|
12
|
+
holderCount?: number | null;
|
|
13
|
+
totalSupply?: number | null;
|
|
14
|
+
profile?: {
|
|
15
|
+
image?: string | null;
|
|
16
|
+
} | null;
|
|
17
|
+
}
|
|
18
|
+
/** Minimal spot-price shape — the concrete read (Ekubo on Starknet, a DEX on
|
|
19
|
+
* another chain) is injected by the app and structurally satisfies this. */
|
|
20
|
+
interface CoinPriceLike {
|
|
21
|
+
quotePerCoin: number;
|
|
22
|
+
quoteSymbol: string | null;
|
|
23
|
+
}
|
|
24
|
+
/** Native creator coin vs claimed external memecoin. */
|
|
25
|
+
declare function coinKind(service: string | null | undefined): CoinKind;
|
|
26
|
+
/** Format a quote-per-coin spot price. */
|
|
27
|
+
declare function formatCoinPrice(n: number): string;
|
|
28
|
+
/** Fully-diluted value = price × supply, abbreviated, in the quote symbol.
|
|
29
|
+
* Returns null when price/supply is unknown or zero (external coins aren't
|
|
30
|
+
* supply-indexed, so price × 0 must read "—", never "0"). */
|
|
31
|
+
declare function formatFdv(quotePerCoin: number | null | undefined, totalSupply: number | null | undefined, quoteSymbol: string | null | undefined): string | null;
|
|
32
|
+
|
|
33
|
+
export { type CoinCollectionLike, type CoinKind, type CoinPriceLike, coinKind, formatCoinPrice, formatFdv };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
type CoinKind = "creator" | "memecoin";
|
|
2
|
+
/** The fields a coin tile reads. An app's `ApiCollection` structurally satisfies
|
|
3
|
+
* this. `chain` is first-class — rendered as a badge; nothing assumes Starknet. */
|
|
4
|
+
interface CoinCollectionLike {
|
|
5
|
+
contractAddress: string;
|
|
6
|
+
chain?: string | null;
|
|
7
|
+
name?: string | null;
|
|
8
|
+
symbol?: string | null;
|
|
9
|
+
image?: string | null;
|
|
10
|
+
service?: string | null;
|
|
11
|
+
claimedBy?: string | null;
|
|
12
|
+
holderCount?: number | null;
|
|
13
|
+
totalSupply?: number | null;
|
|
14
|
+
profile?: {
|
|
15
|
+
image?: string | null;
|
|
16
|
+
} | null;
|
|
17
|
+
}
|
|
18
|
+
/** Minimal spot-price shape — the concrete read (Ekubo on Starknet, a DEX on
|
|
19
|
+
* another chain) is injected by the app and structurally satisfies this. */
|
|
20
|
+
interface CoinPriceLike {
|
|
21
|
+
quotePerCoin: number;
|
|
22
|
+
quoteSymbol: string | null;
|
|
23
|
+
}
|
|
24
|
+
/** Native creator coin vs claimed external memecoin. */
|
|
25
|
+
declare function coinKind(service: string | null | undefined): CoinKind;
|
|
26
|
+
/** Format a quote-per-coin spot price. */
|
|
27
|
+
declare function formatCoinPrice(n: number): string;
|
|
28
|
+
/** Fully-diluted value = price × supply, abbreviated, in the quote symbol.
|
|
29
|
+
* Returns null when price/supply is unknown or zero (external coins aren't
|
|
30
|
+
* supply-indexed, so price × 0 must read "—", never "0"). */
|
|
31
|
+
declare function formatFdv(quotePerCoin: number | null | undefined, totalSupply: number | null | undefined, quoteSymbol: string | null | undefined): string | null;
|
|
32
|
+
|
|
33
|
+
export { type CoinCollectionLike, type CoinKind, type CoinPriceLike, coinKind, formatCoinPrice, formatFdv };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
function coinKind(service) {
|
|
2
|
+
return service === "external-erc20" ? "memecoin" : "creator";
|
|
3
|
+
}
|
|
4
|
+
function formatCoinPrice(n) {
|
|
5
|
+
if (n === 0) return "0";
|
|
6
|
+
if (n < 1e-6) return n.toExponential(2);
|
|
7
|
+
if (n < 1) return n.toPrecision(3);
|
|
8
|
+
return n.toLocaleString(void 0, { maximumFractionDigits: 4 });
|
|
9
|
+
}
|
|
10
|
+
function formatFdv(quotePerCoin, totalSupply, quoteSymbol) {
|
|
11
|
+
if (quotePerCoin == null || !totalSupply) return null;
|
|
12
|
+
const fdv = quotePerCoin * totalSupply;
|
|
13
|
+
const sym = quoteSymbol ?? "";
|
|
14
|
+
const abbr = fdv >= 1e9 ? `${(fdv / 1e9).toLocaleString(void 0, { maximumFractionDigits: 1 })}B` : fdv >= 1e6 ? `${(fdv / 1e6).toLocaleString(void 0, { maximumFractionDigits: 1 })}M` : fdv >= 1e3 ? `${(fdv / 1e3).toLocaleString(void 0, { maximumFractionDigits: 1 })}K` : fdv.toLocaleString(void 0, { maximumFractionDigits: 2 });
|
|
15
|
+
return sym ? `${abbr} ${sym}` : abbr;
|
|
16
|
+
}
|
|
17
|
+
export {
|
|
18
|
+
coinKind,
|
|
19
|
+
formatCoinPrice,
|
|
20
|
+
formatFdv
|
|
21
|
+
};
|
|
22
|
+
//# sourceMappingURL=coins.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/data/coins.ts"],"sourcesContent":["// Coin discovery — pure, dependency-free helpers + the structural shapes the\n// coin components read. No @medialane/sdk import: ui types shared code\n// structurally (same pattern as CollectionCard's `ApiCollection & {…}`), so a\n// coin on any chain works without bumping ui's SDK. Registry-dependent logic\n// (isCoinCollection / COIN_SERVICE_IDS, price reads) lives in the apps and is\n// injected — see the CoinsExplorer/CoinCard props.\n\nexport type CoinKind = \"creator\" | \"memecoin\";\n\n/** The fields a coin tile reads. An app's `ApiCollection` structurally satisfies\n * this. `chain` is first-class — rendered as a badge; nothing assumes Starknet. */\nexport interface CoinCollectionLike {\n contractAddress: string;\n chain?: string | null;\n name?: string | null;\n symbol?: string | null;\n image?: string | null;\n service?: string | null;\n claimedBy?: string | null;\n holderCount?: number | null;\n totalSupply?: number | null;\n profile?: { image?: string | null } | null;\n}\n\n/** Minimal spot-price shape — the concrete read (Ekubo on Starknet, a DEX on\n * another chain) is injected by the app and structurally satisfies this. */\nexport interface CoinPriceLike {\n quotePerCoin: number;\n quoteSymbol: string | null;\n}\n\n/** Native creator coin vs claimed external memecoin. */\nexport function coinKind(service: string | null | undefined): CoinKind {\n return service === \"external-erc20\" ? \"memecoin\" : \"creator\";\n}\n\n/** Format a quote-per-coin spot price. */\nexport function formatCoinPrice(n: number): string {\n if (n === 0) return \"0\";\n if (n < 0.000001) return n.toExponential(2);\n if (n < 1) return n.toPrecision(3);\n return n.toLocaleString(undefined, { maximumFractionDigits: 4 });\n}\n\n/** Fully-diluted value = price × supply, abbreviated, in the quote symbol.\n * Returns null when price/supply is unknown or zero (external coins aren't\n * supply-indexed, so price × 0 must read \"—\", never \"0\"). */\nexport function formatFdv(\n quotePerCoin: number | null | undefined,\n totalSupply: number | null | undefined,\n quoteSymbol: string | null | undefined\n): string | null {\n if (quotePerCoin == null || !totalSupply) return null;\n const fdv = quotePerCoin * totalSupply;\n const sym = quoteSymbol ?? \"\";\n const abbr =\n fdv >= 1_000_000_000 ? `${(fdv / 1_000_000_000).toLocaleString(undefined, { maximumFractionDigits: 1 })}B` :\n fdv >= 1_000_000 ? `${(fdv / 1_000_000).toLocaleString(undefined, { maximumFractionDigits: 1 })}M` :\n fdv >= 1_000 ? `${(fdv / 1_000).toLocaleString(undefined, { maximumFractionDigits: 1 })}K` :\n fdv.toLocaleString(undefined, { maximumFractionDigits: 2 });\n return sym ? `${abbr} ${sym}` : abbr;\n}\n"],"mappings":"AAgCO,SAAS,SAAS,SAA8C;AACrE,SAAO,YAAY,mBAAmB,aAAa;AACrD;AAGO,SAAS,gBAAgB,GAAmB;AACjD,MAAI,MAAM,EAAG,QAAO;AACpB,MAAI,IAAI,KAAU,QAAO,EAAE,cAAc,CAAC;AAC1C,MAAI,IAAI,EAAG,QAAO,EAAE,YAAY,CAAC;AACjC,SAAO,EAAE,eAAe,QAAW,EAAE,uBAAuB,EAAE,CAAC;AACjE;AAKO,SAAS,UACd,cACA,aACA,aACe;AACf,MAAI,gBAAgB,QAAQ,CAAC,YAAa,QAAO;AACjD,QAAM,MAAM,eAAe;AAC3B,QAAM,MAAM,eAAe;AAC3B,QAAM,OACJ,OAAO,MAAgB,IAAI,MAAM,KAAe,eAAe,QAAW,EAAE,uBAAuB,EAAE,CAAC,CAAC,MACvG,OAAO,MAAgB,IAAI,MAAM,KAAW,eAAe,QAAW,EAAE,uBAAuB,EAAE,CAAC,CAAC,MACnG,OAAO,MAAgB,IAAI,MAAM,KAAO,eAAe,QAAW,EAAE,uBAAuB,EAAE,CAAC,CAAC,MACxE,IAAI,eAAe,QAAW,EAAE,uBAAuB,EAAE,CAAC;AACnF,SAAO,MAAM,GAAG,IAAI,IAAI,GAAG,KAAK;AAClC;","names":[]}
|
|
@@ -18,6 +18,7 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
18
18
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
19
|
var ip_templates_exports = {};
|
|
20
20
|
__export(ip_templates_exports, {
|
|
21
|
+
DOC_UPLOAD: () => DOC_UPLOAD,
|
|
21
22
|
EMBED_PLATFORM_META: () => EMBED_PLATFORM_META,
|
|
22
23
|
IP_TEMPLATES: () => IP_TEMPLATES,
|
|
23
24
|
SOCIAL_PLATFORM_META: () => SOCIAL_PLATFORM_META,
|
|
@@ -39,6 +40,12 @@ const SOCIAL_PLATFORM_META = {
|
|
|
39
40
|
tiktok: { label: "TikTok", icon: import_lucide_react.Video, traitKey: "TikTok", placeholder: "https://tiktok.com/@\u2026" },
|
|
40
41
|
website: { label: "Website", icon: import_lucide_react.Globe, traitKey: "Website", placeholder: "https://\u2026" }
|
|
41
42
|
};
|
|
43
|
+
const DOC_UPLOAD = {
|
|
44
|
+
traitType: "Document File",
|
|
45
|
+
accept: ".pdf,.doc,.docx,.txt,.md,.rtf,.odt",
|
|
46
|
+
maxMb: 20,
|
|
47
|
+
hint: "Stored permanently on IPFS \u2014 an immutable, timestamped copy of your work (proof of authorship under the Berne Convention)."
|
|
48
|
+
};
|
|
42
49
|
const IP_TEMPLATES = {
|
|
43
50
|
Audio: {
|
|
44
51
|
type: "Audio",
|
|
@@ -113,6 +120,7 @@ const IP_TEMPLATES = {
|
|
|
113
120
|
description: "Books, journals, magazines, academic papers",
|
|
114
121
|
icon: import_lucide_react.BookOpen,
|
|
115
122
|
color: { bg: "bg-indigo-500/10", text: "text-indigo-400", border: "border-indigo-500/20" },
|
|
123
|
+
docUpload: DOC_UPLOAD,
|
|
116
124
|
socials: ["x", "instagram", "website"],
|
|
117
125
|
traitSuggestions: [
|
|
118
126
|
{ key: "Author" },
|
|
@@ -127,6 +135,7 @@ const IP_TEMPLATES = {
|
|
|
127
135
|
description: "Contracts, reports, whitepapers, legal documents",
|
|
128
136
|
icon: import_lucide_react.FileText,
|
|
129
137
|
color: { bg: "bg-zinc-500/10", text: "text-zinc-400", border: "border-zinc-500/20" },
|
|
138
|
+
docUpload: DOC_UPLOAD,
|
|
130
139
|
traitSuggestions: [
|
|
131
140
|
{ key: "Author" },
|
|
132
141
|
{ key: "Category" },
|
|
@@ -139,6 +148,7 @@ const IP_TEMPLATES = {
|
|
|
139
148
|
description: "Patents, inventions, technical innovations",
|
|
140
149
|
icon: import_lucide_react.Award,
|
|
141
150
|
color: { bg: "bg-amber-500/10", text: "text-amber-400", border: "border-amber-500/20" },
|
|
151
|
+
docUpload: DOC_UPLOAD,
|
|
142
152
|
traitSuggestions: [
|
|
143
153
|
{ key: "Inventor" },
|
|
144
154
|
{ key: "Field" },
|
|
@@ -151,6 +161,7 @@ const IP_TEMPLATES = {
|
|
|
151
161
|
description: "Applications, scripts, algorithms, code libraries",
|
|
152
162
|
icon: import_lucide_react.Code2,
|
|
153
163
|
color: { bg: "bg-violet-500/10", text: "text-violet-400", border: "border-violet-500/20" },
|
|
164
|
+
docUpload: DOC_UPLOAD,
|
|
154
165
|
socials: ["website"],
|
|
155
166
|
traitSuggestions: [
|
|
156
167
|
{ key: "Language", placeholder: "TypeScript" },
|
|
@@ -192,12 +203,14 @@ const IP_TEMPLATES = {
|
|
|
192
203
|
};
|
|
193
204
|
const TEMPLATE_TRAIT_TYPES = /* @__PURE__ */ new Set([
|
|
194
205
|
"IP Type",
|
|
206
|
+
DOC_UPLOAD.traitType,
|
|
195
207
|
...Object.values(EMBED_PLATFORM_META).map((m) => m.traitKey),
|
|
196
208
|
...Object.values(SOCIAL_PLATFORM_META).map((m) => m.traitKey),
|
|
197
209
|
...Object.values(IP_TEMPLATES).flatMap((t) => (t.traitSuggestions ?? []).map((s) => s.key))
|
|
198
210
|
]);
|
|
199
211
|
// Annotate the CommonJS export names for ESM import in node:
|
|
200
212
|
0 && (module.exports = {
|
|
213
|
+
DOC_UPLOAD,
|
|
201
214
|
EMBED_PLATFORM_META,
|
|
202
215
|
IP_TEMPLATES,
|
|
203
216
|
SOCIAL_PLATFORM_META,
|