/** * Component: NFTDetail * Author: Nearblocks Pte Ltd * License: Business Source License 1.1 * Description: Non-Fungible Token Details. * @interface Props * @param {string} [network] - The network data to show, either mainnet or testnet * @param {Function} [t] - A function for internationalization (i18n) provided by the next-translate package. * @param {string} [id] - The token identifier passed as a string * @param {string} [tid] - The nf token identifier passed as a string */ /* INCLUDE COMPONENT: "includes/Common/Skeleton.jsx" */ /** * @interface Props * @param {string} [className] - The CSS class name(s) for styling purposes. */ const Skeleton = (props) => { return ( <div className={`bg-gray-200 rounded shadow-sm animate-pulse ${props.className}`} ></div> ); };/* END_INCLUDE COMPONENT: "includes/Common/Skeleton.jsx" */ /* INCLUDE COMPONENT: "includes/icons/ArrowDown.jsx" */ /** * @interface Props * @param {string} [className] - The CSS class name(s) for styling purposes. */ const ArrowDown = (props) => { return ( <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width={24} height={24} {...props} > <path fill="none" d="M0 0h24v24H0z" /> <path d="M12 13.172l4.95-4.95 1.414 1.414L12 16 5.636 9.636 7.05 8.222z" /> </svg> ); };/* END_INCLUDE COMPONENT: "includes/icons/ArrowDown.jsx" */ /* INCLUDE COMPONENT: "includes/icons/ArrowUp.jsx" */ const ArrowUp = (props) => { return ( <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width={24} height={24} {...props} > <path fill="none" d="M0 0h24v24H0z" /> <path d="M12 10.828l-4.95 4.95-1.414-1.414L12 8l6.364 6.364-1.414 1.414z" /> </svg> ); };/* END_INCLUDE COMPONENT: "includes/icons/ArrowUp.jsx" */ /* INCLUDE COMPONENT: "includes/icons/Question.jsx" */ /** * @interface Props * @param {string} [className] - The CSS class name(s) for styling purposes. */ const Question = (props) => { return ( <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width={16} height={16} {...props} > <path fill="none" d="M0 0h24v24H0z" /> <path d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm0-2a8 8 0 100-16 8 8 0 000 16zm-1-5h2v2h-2v-2zm2-1.645V14h-2v-1.5a1 1 0 011-1 1.5 1.5 0 10-1.471-1.794l-1.962-.393A3.501 3.501 0 1113 13.355z" /> </svg> ); };/* END_INCLUDE COMPONENT: "includes/icons/Question.jsx" */ /* INCLUDE COMPONENT: "includes/icons/TokenImage.jsx" */ /** * @interface Props * @param {string} [src] - The URL string pointing to the image source. * @param {string} [alt] - The alternate text description for the image. * @param {string} [className] - The CSS class name(s) for styling purposes. * @param {string} [appUrl] - The URL of the application. */ const TokenImage = ({ appUrl, src, alt, className, onLoad, onSetSrc, }) => { const placeholder = `${appUrl}images/tokenplaceholder.svg`; const handleLoad = () => { if (onLoad) { onLoad(); } }; const handleError = () => { if (onSetSrc) { onSetSrc(placeholder); } if (onLoad) { onLoad(); } }; return ( <img src={src || placeholder} alt={alt} className={className} onLoad={handleLoad} onError={handleError} /> ); };/* END_INCLUDE COMPONENT: "includes/icons/TokenImage.jsx" */ /* INCLUDE: "includes/libs.jsx" */ function getConfig(network) { switch (network) { case 'mainnet': return { ownerId: 'nearblocks.near', nodeUrl: 'https://rpc.mainnet.near.org', backendUrl: 'https://api3.nearblocks.io/v1/', rpcUrl: 'https://archival-rpc.testnet.near.org', appUrl: 'https://nearblocks.io/', }; case 'testnet': return { ownerId: 'nearblocks.testnet', nodeUrl: 'https://rpc.testnet.near.org', backendUrl: 'https://api3-testnet.nearblocks.io/v1/', rpcUrl: 'https://archival-rpc.testnet.near.org', appUrl: 'https://testnet.nearblocks.io/', }; default: return {}; } } function debounce( delay, func, ) { let timer; let active = true; const debounced = (arg) => { if (active) { clearTimeout(timer); timer = setTimeout(() => { active && func(arg); timer = undefined; }, delay); } else { func(arg); } }; debounced.isPending = () => { return timer !== undefined; }; debounced.cancel = () => { active = false; }; debounced.flush = (arg) => func(arg); return debounced; } function timeAgo(unixTimestamp) { const currentTimestamp = Math.floor(Date.now() / 1000); const secondsAgo = currentTimestamp - unixTimestamp; if (secondsAgo < 5) { return 'Just now'; } else if (secondsAgo < 60) { return `${secondsAgo} seconds ago`; } else if (secondsAgo < 3600) { const minutesAgo = Math.floor(secondsAgo / 60); return `${minutesAgo} minute${minutesAgo > 1 ? 's' : ''} ago`; } else if (secondsAgo < 86400) { const hoursAgo = Math.floor(secondsAgo / 3600); return `${hoursAgo} hour${hoursAgo > 1 ? 's' : ''} ago`; } else { const daysAgo = Math.floor(secondsAgo / 86400); return `${daysAgo} day${daysAgo > 1 ? 's' : ''} ago`; } } function shortenAddress(address) { const string = String(address); if (string.length <= 20) return string; return `${string.substr(0, 10)}...${string.substr(-7)}`; } function urlHostName(url) { try { const domain = new URL(url); return domain?.hostname ?? null; } catch (e) { return null; } } function holderPercentage(supply, quantity) { return Math.min(Big(quantity).div(Big(supply)).mul(Big(100)).toFixed(2), 100); } function isAction(type) { const actions = [ 'DEPLOY_CONTRACT', 'TRANSFER', 'STAKE', 'ADD_KEY', 'DELETE_KEY', 'DELETE_ACCOUNT', ]; return actions.includes(type.toUpperCase()); } function localFormat(number) { const formattedNumber = Number(number).toLocaleString('en', { minimumFractionDigits: 0, maximumFractionDigits: 5, }); return formattedNumber; } function formatWithCommas(number) { return number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); } function shortenAddress(address) { const string = String(address); if (string.length <= 20) return string; return `${string.substr(0, 10)}...${string.substr(-7)}`; } function urlHostName(url) { try { const domain = new URL(url); return domain?.hostname ?? null; } catch (e) { return null; } } function holderPercentage(supply, quantity) { return Math.min(Big(quantity).div(Big(supply)).mul(Big(100)).toFixed(2), 100); } function isAction(type) { const actions = [ 'DEPLOY_CONTRACT', 'TRANSFER', 'STAKE', 'ADD_KEY', 'DELETE_KEY', 'DELETE_ACCOUNT', ]; return actions.includes(type.toUpperCase()); } function localFormat(number) { const formattedNumber = Number(number).toLocaleString('en', { minimumFractionDigits: 0, maximumFractionDigits: 5, }); return formattedNumber; } function formatWithCommas(number) { return number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); } /* END_INCLUDE: "includes/libs.jsx" */ function MainComponent({ network, t, id, tid }) { const [indices, setIndices] = useState([1, 2]); const [token, setToken] = useState({} ); const [loading, setLoading] = useState(false); const config = getConfig(network); useEffect(() => { function fetchToken() { setLoading(true); asyncFetch(`${config?.backendUrl}nfts/${id}/tokens/${tid}`, { method: 'GET', headers: { 'Content-Type': 'application/json', }, }) .then( (res ) => { const resp = res?.body?.tokens?.[0]; if (res.status === 200) { setToken(resp); } }, ) .catch(() => {}) .finally(() => { setLoading(false); }); } fetchToken(); }, [config?.backendUrl, id, tid]); const toggleItem = (index) => { if (indices.includes(index)) { setIndices(indices.filter((currentIndex) => currentIndex !== index)); } else { setIndices([...indices, index].sort()); } }; return ( <> <div className="grid md:grid-cols-12 pt-4 mb-2"> <div className="md:col-span-5 lg:col-span-4 pt-4"> <div className="bg-white border rounded-xl soft-shadow p-3 aspect-square"> { <Widget src={`${config.ownerId}/widget/bos-components.components.Shared.NFTImage`} props={{ base: token?.nft?.base_uri, media: token?.media, reference: token?.reference, className: 'rounded max-h-full', network: network, }} /> } </div> </div> <div className="md:col-span-7 lg:col-span-8 md:px-4 lg:pl-8 pt-4"> <h1 className="break-all space-x-2 text-xl text-gray-700 leading-8 font-semibold"> {loading ? ( <div className="w-80 max-w-xs"> <Skeleton className="h-6" /> </div> ) : ( token?.title || token?.token )} </h1> <a href={`/nft-token/${id}`} className="hover:no-underline"> <a className="break-all text-green leading-6 text-sm hover:no-underline"> {loading ? ( <div className="w-60 max-w-xs py-2"> <Skeleton className="h-4" /> </div> ) : ( <> <span className="inline-flex align-middle h-5 w-5 mr-2"> <TokenImage src={token?.nft?.icon} alt={token?.nft?.name} className="w-5 h-5" appUrl={config.appUrl} /> </span> <span>{token?.nft?.name}</span> </> )} </a> </a> <Accordion.Root type="multiple" className="bg-white border rounded-xl soft-shadow mt-4" defaultValue={indices} collapsible > <Accordion.Item value={1}> <Accordion.Header> <Accordion.Trigger onClick={() => toggleItem(1)} className="w-full flex justify-between items-center text-sm font-semibold text-gray-600 border-b focus:outline-none p-3" > <h2>Details</h2> {indices.includes(1) ? ( <ArrowUp className="fill-current" /> ) : ( <ArrowDown className="fill-current" /> )} </Accordion.Trigger> </Accordion.Header> <Accordion.Content className="text-sm text-nearblue-600"> <div className="divide-solid divide-gray-200 divide-y"> {token?.asset && ( <div className="flex p-4"> <div className="flex items-center w-full xl:w-1/4 mb-2 xl:mb-0"> <Tooltip.Provider> <Tooltip.Root> <Tooltip.Trigger asChild> <div> <Question className="w-4 h-4 fill-current mr-1" /> </div> </Tooltip.Trigger> <Tooltip.Content className="h-auto max-w-xs bg-black bg-opacity-90 z-10 text-xs text-white px-3 py-2" align="start" side="bottom" > Current owner of this NFT </Tooltip.Content> </Tooltip.Root> </Tooltip.Provider> Owner: </div> <div className="w-full xl:w-3/4 word-break"> <a href={`/address/${token.asset.owner}`} className="hover:no-underline" > <a className="text-green hover:no-underline"> {shortenAddress(token.asset.owner)} </a> </a> </div> </div> )} <div className="flex p-4"> <div className="flex items-center w-full xl:w-1/4 mb-2 xl:mb-0"> <Tooltip.Provider> <Tooltip.Root> <Tooltip.Trigger asChild> <div> <Question className="w-4 h-4 fill-current mr-1" /> </div> </Tooltip.Trigger> <Tooltip.Content className="h-auto max-w-xs bg-black bg-opacity-90 z-10 text-xs text-white px-3 py-2" align="start" side="bottom" > Address of this NFT contract </Tooltip.Content> </Tooltip.Root> </Tooltip.Provider> Contract Address: </div> <div className="w-full xl:w-3/4 word-break"> <a href={`/address/${id}`} className="hover:no-underline"> <a className="text-green hover:no-underline"> {shortenAddress(id)} </a> </a> </div> </div> <div className="flex p-4"> <div className="flex items-center w-full xl:w-1/4 mb-2 xl:mb-0"> <Tooltip.Provider> <Tooltip.Root> <Tooltip.Trigger asChild> <div> <Question className="w-4 h-4 fill-current mr-1" /> </div> </Tooltip.Trigger> <Tooltip.Content className="h-auto max-w-xs bg-black bg-opacity-90 z-10 text-xs text-white px-3 py-2" align="start" side="bottom" > {"This NFT's unique token ID"} </Tooltip.Content> </Tooltip.Root> </Tooltip.Provider> Token ID: </div> <div className="w-full xl:w-3/4 word-break">{tid}</div> </div> <div className="flex p-4"> <div className="flex items-center w-full xl:w-1/4 mb-2 xl:mb-0"> <Tooltip.Provider> <Tooltip.Root> <Tooltip.Trigger asChild> <div> <Question className="w-4 h-4 fill-current mr-1" /> </div> </Tooltip.Trigger> <Tooltip.Content className="h-auto max-w-xs bg-black bg-opacity-90 z-10 text-xs text-white px-3 py-2" align="start" side="bottom" > The standard followed by this NFT </Tooltip.Content> </Tooltip.Root> </Tooltip.Provider> Token Standard: </div> <div className="w-full xl:w-3/4 word-break">NEP-171</div> </div> </div> </Accordion.Content> </Accordion.Item> {token?.description && ( <Accordion.Item value={2}> <Accordion.Trigger onClick={() => toggleItem(2)} className="w-full flex justify-between items-center text-sm font-semibold text-gray-600 border-b focus:outline-none p-3" > <h2>Description</h2> {indices.includes(2) ? ( <ArrowUp className="fill-current" /> ) : ( <ArrowDown className="fill-current" /> )} </Accordion.Trigger> <Accordion.Content className="text-sm text-nearblue-600 border-b p-3"> {token.description} </Accordion.Content> </Accordion.Item> )} </Accordion.Root> </div> </div> <div className="py-6"></div> <div className="block lg:flex lg:space-x-2 mb-10"> <div className="w-full "> <div className="bg-white soft-shadow rounded-xl pb-1"> { <Widget src={`${config.ownerId}/widget/bos-components.components.NFT.TokenTransfers`} props={{ network: network, t: t, id: id, tid: tid, }} /> } </div> </div> </div> </> ); } return MainComponent(props, context);