|
| 1 | +import { ErrorCard } from '@components/common/ErrorCard'; |
| 2 | +import { LoadingCard } from '@components/common/LoadingCard'; |
| 3 | +import { Account, isTokenProgramData } from '@providers/accounts'; |
| 4 | +import React from 'react'; |
| 5 | + |
| 6 | +import { getProxiedUri } from '@/app/features/metadata/utils'; |
| 7 | +import { useCluster } from '@/app/providers/cluster'; |
| 8 | +import { useCompressedNft } from '@/app/providers/compressed-nft'; |
| 9 | + |
| 10 | +interface File { |
| 11 | + uri: string; |
| 12 | + type: string; |
| 13 | + cdn?: boolean; |
| 14 | +} |
| 15 | + |
| 16 | +export function MetaplexFilesCard({ account, onNotFound }: { account?: Account; onNotFound: () => never }) { |
| 17 | + const { url } = useCluster(); |
| 18 | + const compressedNft = useCompressedNft({ address: account?.pubkey.toString() ?? '', url }); |
| 19 | + |
| 20 | + const parsedData = account?.data?.parsed; |
| 21 | + if (!parsedData || !isTokenProgramData(parsedData) || parsedData.parsed.type !== 'mint' || !parsedData.nftData) { |
| 22 | + if (compressedNft && compressedNft.compression.compressed) { |
| 23 | + return <NormalMetaplexFilesCard metadataUri={compressedNft.content.json_uri} />; |
| 24 | + } |
| 25 | + return onNotFound(); |
| 26 | + } |
| 27 | + return <NormalMetaplexFilesCard metadataUri={parsedData.nftData.metadata.data.uri} />; |
| 28 | +} |
| 29 | + |
| 30 | +function NormalMetaplexFilesCard({ metadataUri }: { metadataUri: string }) { |
| 31 | + const [files, setFiles] = React.useState<File[]>([]); |
| 32 | + const [status, setStatus] = React.useState<'loading' | 'success' | 'error'>('loading'); |
| 33 | + |
| 34 | + async function fetchMetadataFiles() { |
| 35 | + try { |
| 36 | + const response = await fetch(getProxiedUri(metadataUri)); |
| 37 | + const metadata = await response.json(); |
| 38 | + // Verify if the attributes value is an array |
| 39 | + if (Array.isArray(metadata.properties.files)) { |
| 40 | + // Filter files to keep objects matching schema |
| 41 | + const filteredFiles = metadata.properties.files.filter((file: any) => { |
| 42 | + return typeof file === 'object' && typeof file.uri === 'string' && typeof file.type === 'string'; |
| 43 | + }); |
| 44 | + |
| 45 | + setFiles(filteredFiles); |
| 46 | + setStatus('success'); |
| 47 | + } else { |
| 48 | + throw new Error('Files is not an array'); |
| 49 | + } |
| 50 | + } catch (error) { |
| 51 | + setStatus('error'); |
| 52 | + } |
| 53 | + } |
| 54 | + |
| 55 | + React.useEffect(() => { |
| 56 | + fetchMetadataFiles(); |
| 57 | + }, []); // eslint-disable-line react-hooks/exhaustive-deps |
| 58 | + |
| 59 | + if (status === 'loading') { |
| 60 | + return <LoadingCard />; |
| 61 | + } |
| 62 | + |
| 63 | + if (status === 'error') { |
| 64 | + return <ErrorCard text="Failed to fetch files" />; |
| 65 | + } |
| 66 | + |
| 67 | + const filesList: React.ReactNode[] = files.map(({ uri, type }) => { |
| 68 | + return ( |
| 69 | + <tr key={`${uri}:${type}`}> |
| 70 | + <td> |
| 71 | + <a href={uri} target="_blank" rel="noopener noreferrer"> |
| 72 | + {uri} |
| 73 | + </a> |
| 74 | + </td> |
| 75 | + <td>{type}</td> |
| 76 | + </tr> |
| 77 | + ); |
| 78 | + }); |
| 79 | + |
| 80 | + return ( |
| 81 | + <div className="card"> |
| 82 | + <div className="card-header align-items-center"> |
| 83 | + <h3 className="card-header-title">Files</h3> |
| 84 | + </div> |
| 85 | + <div className="table-responsive mb-0"> |
| 86 | + <table className="table table-sm table-nowrap card-table"> |
| 87 | + <thead> |
| 88 | + <tr> |
| 89 | + <th className="text-muted w-1">File URI</th> |
| 90 | + <th className="text-muted w-1">File Type</th> |
| 91 | + </tr> |
| 92 | + </thead> |
| 93 | + <tbody className="list">{filesList}</tbody> |
| 94 | + </table> |
| 95 | + </div> |
| 96 | + </div> |
| 97 | + ); |
| 98 | +} |
0 commit comments