Skip to content

Commit b4f7fab

Browse files
authored
feat: show full NFT metadata fields with raw JSON toggle on token page (#77)
1 parent 3b11f28 commit b4f7fab

1 file changed

Lines changed: 79 additions & 0 deletions

File tree

frontend/src/pages/NFTTokenPage.tsx

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { useEffect, useState } from 'react';
2+
import type { ReactNode } from 'react';
23
import { useParams, Link } from 'react-router-dom';
34
import { useNftToken, useNftContract, useNftTokenTransfers } from '../hooks';
45
import { AddressLink, CopyButton, Pagination, EmptyState } from '../components';
@@ -14,12 +15,35 @@ import {
1415
truncateHash,
1516
} from '../utils';
1617

18+
const SKIP_METADATA_KEYS = new Set(['image', 'image_url', 'imageUrl', 'image_data', 'description', 'attributes', 'name']);
19+
20+
function renderMetadataValue(value: unknown): ReactNode {
21+
if (typeof value === 'string') {
22+
if (/^https?:\/\//.test(value)) {
23+
return <a href={value} target="_blank" rel="noopener noreferrer" className="text-accent-primary hover:underline break-all text-sm">{value}</a>;
24+
}
25+
return <span className="text-fg text-sm">{value}</span>;
26+
}
27+
if (typeof value === 'number') {
28+
return <span className="font-mono text-fg text-sm">{value}</span>;
29+
}
30+
if (typeof value === 'boolean') {
31+
return <span className="font-mono text-fg text-sm">{value ? 'true' : 'false'}</span>;
32+
}
33+
return (
34+
<pre className="text-xs font-mono text-fg bg-dark-800 rounded p-2 overflow-x-auto whitespace-pre-wrap break-all">
35+
{JSON.stringify(value, null, 2)}
36+
</pre>
37+
);
38+
}
39+
1740
export default function NFTTokenPage() {
1841
const { contract: contractAddress, tokenId } = useParams<{ contract: string; tokenId: string }>();
1942

2043
const { contract } = useNftContract(contractAddress);
2144
const { token, loading: tokenLoading, error: tokenError, refetch } = useNftToken(contractAddress, tokenId);
2245
const [txPage, setTxPage] = useState(1);
46+
const [metadataView, setMetadataView] = useState<'formatted' | 'raw'>('formatted');
2347
const { transfers, pagination, loading } = useNftTokenTransfers(contractAddress, tokenId, { page: txPage, limit: 20 });
2448

2549
const metadataPending = isNftMetadataPending(token);
@@ -49,6 +73,11 @@ export default function NFTTokenPage() {
4973
const attributes = getNftAttributes(token);
5074
const displayName = token?.name || `${contract?.name || contract?.symbol || 'NFT'} #${token?.token_id || tokenId || ''}`;
5175

76+
const extraMetadataEntries = token?.metadata_status === 'fetched' && token.metadata
77+
? Object.entries(token.metadata).filter(([k]) => !SKIP_METADATA_KEYS.has(k))
78+
: [];
79+
const hasExtraMetadata = token?.metadata_status === 'fetched' && token.metadata !== null;
80+
5281
return (
5382
<div>
5483
{/* Breadcrumb */}
@@ -207,6 +236,56 @@ export default function NFTTokenPage() {
207236
</div>
208237
</div>
209238

239+
{/* Metadata */}
240+
{hasExtraMetadata && (
241+
<div className="card mt-6">
242+
<div className="flex items-center justify-between mb-4">
243+
<h3 className="text-lg font-semibold text-fg">Metadata</h3>
244+
<div className="flex">
245+
<button
246+
type="button"
247+
onClick={() => setMetadataView('formatted')}
248+
className={`px-3 py-1 text-sm border rounded-l-lg ${
249+
metadataView === 'formatted'
250+
? 'border-accent-primary text-accent-primary bg-accent-primary/10'
251+
: 'border-dark-500 text-gray-400 hover:border-gray-400'
252+
}`}
253+
>
254+
Formatted
255+
</button>
256+
<button
257+
type="button"
258+
onClick={() => setMetadataView('raw')}
259+
className={`px-3 py-1 text-sm border rounded-r-lg ${
260+
metadataView === 'raw'
261+
? 'border-accent-primary text-accent-primary bg-accent-primary/10'
262+
: 'border-dark-500 text-gray-400 hover:border-gray-400'
263+
}`}
264+
>
265+
Raw
266+
</button>
267+
</div>
268+
</div>
269+
270+
{metadataView === 'raw' ? (
271+
<pre className="text-xs font-mono text-fg bg-dark-800 rounded p-3 overflow-auto max-h-96 whitespace-pre-wrap break-all">
272+
{JSON.stringify(token!.metadata, null, 2)}
273+
</pre>
274+
) : extraMetadataEntries.length > 0 ? (
275+
<dl className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-x-8 gap-y-3">
276+
{extraMetadataEntries.map(([key, value]) => (
277+
<div key={key} className="flex flex-col gap-0.5">
278+
<dt className="text-fg-subtle text-xs uppercase tracking-wider">{key.replace(/_/g, ' ')}</dt>
279+
<dd>{renderMetadataValue(value)}</dd>
280+
</div>
281+
))}
282+
</dl>
283+
) : (
284+
<p className="text-fg-subtle text-sm">No additional fields.</p>
285+
)}
286+
</div>
287+
)}
288+
210289
{/* Transfers */}
211290
<div className="card mt-6 overflow-hidden">
212291
<div className="flex items-center justify-between mb-4">

0 commit comments

Comments
 (0)