Skip to content

Commit

Permalink
add header and account support
Browse files Browse the repository at this point in the history
  • Loading branch information
ngundotra committed May 14, 2024
1 parent 48007f2 commit 14cd693
Show file tree
Hide file tree
Showing 5 changed files with 430 additions and 14 deletions.
58 changes: 48 additions & 10 deletions app/address/[address]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,11 @@ import { FEATURE_PROGRAM_ID } from '@utils/parseFeatureAccount';
import { useClusterPath } from '@utils/url';
import Link from 'next/link';
import { redirect, useSelectedLayoutSegment } from 'next/navigation';
import React, { PropsWithChildren } from 'react';
import React, { PropsWithChildren, Suspense } from 'react';
import useSWRImmutable from 'swr/immutable';
import { Base58EncodedAddress } from 'web3js-experimental';

import { CompressedNftAccountHeader, CompressedNftCard } from '@/app/components/account/CompressedNftCard';
import { FullTokenInfo, getFullTokenInfo } from '@/app/utils/token-info';

const IDENTICON_WIDTH = 64;
Expand Down Expand Up @@ -192,7 +193,9 @@ function AddressLayoutInner({ children, params: { address } }: Props) {
const infoParsed = info?.data?.data.parsed;

const { data: fullTokenInfo, isLoading: isFullTokenInfoLoading } = useSWRImmutable(
infoStatus === FetchStatus.Fetched && infoParsed && isTokenProgramData(infoParsed) && pubkey ? ['get-full-token-info', address, cluster, url] : null,
infoStatus === FetchStatus.Fetched && infoParsed && isTokenProgramData(infoParsed) && pubkey
? ['get-full-token-info', address, cluster, url]
: null,
fetchFullTokenInfo
);

Expand All @@ -207,13 +210,23 @@ function AddressLayoutInner({ children, params: { address } }: Props) {
<div className="container mt-n3">
<div className="header">
<div className="header-body">
<AccountHeader address={address} account={info?.data} tokenInfo={fullTokenInfo} isTokenInfoLoading={isFullTokenInfoLoading} />
<AccountHeader
address={address}
account={info?.data}
tokenInfo={fullTokenInfo}
isTokenInfoLoading={isFullTokenInfoLoading}
/>
</div>
</div>
{!pubkey ? (
<ErrorCard text={`Address "${address}" is not valid`} />
) : (
<DetailsSections info={info} pubkey={pubkey} tokenInfo={fullTokenInfo} isTokenInfoLoading={isFullTokenInfoLoading}>
<DetailsSections
info={info}
pubkey={pubkey}
tokenInfo={fullTokenInfo}
isTokenInfoLoading={isFullTokenInfoLoading}
>
{children}
</DetailsSections>
)}
Expand All @@ -229,7 +242,17 @@ export default function AddressLayout({ children, params }: Props) {
);
}

function AccountHeader({ address, account, tokenInfo, isTokenInfoLoading }: { address: string; account?: Account, tokenInfo?: FullTokenInfo, isTokenInfoLoading: boolean }) {
function AccountHeader({
address,
account,
tokenInfo,
isTokenInfoLoading,
}: {
address: string;
account?: Account;
tokenInfo?: FullTokenInfo;
isTokenInfoLoading: boolean;
}) {
const mintInfo = useMintAccountInfo(address);

const parsedData = account?.data.parsed;
Expand Down Expand Up @@ -300,6 +323,10 @@ function AccountHeader({ address, account, tokenInfo, isTokenInfoLoading }: { ad
);
}

if (account) {
return <CompressedNftAccountHeader account={account} />;
}

return (
<>
<h6 className="header-pretitle">Details</h6>
Expand All @@ -314,7 +341,7 @@ function DetailsSections({
tab,
info,
tokenInfo,
isTokenInfoLoading
isTokenInfoLoading,
}: {
children: React.ReactNode;
pubkey: PublicKey;
Expand Down Expand Up @@ -348,7 +375,7 @@ function DetailsSections({
);
}

function InfoSection({ account, tokenInfo }: { account: Account, tokenInfo?: FullTokenInfo }) {
function InfoSection({ account, tokenInfo }: { account: Account; tokenInfo?: FullTokenInfo }) {
const parsedData = account.data.parsed;
const rawData = account.data.raw;

Expand Down Expand Up @@ -392,7 +419,11 @@ function InfoSection({ account, tokenInfo }: { account: Account, tokenInfo?: Ful
} else if (account.owner.toBase58() === FEATURE_PROGRAM_ID) {
return <FeatureAccountSection account={account} />;
} else {
return <UnknownAccountCard account={account} />;
return (
<Suspense fallback={<UnknownAccountCard account={account} />}>
<CompressedNftCard account={account} />
</Suspense>
);
}
}

Expand Down Expand Up @@ -467,12 +498,19 @@ function getTabs(pubkey: PublicKey, account: Account): TabComponent[] {
}

// Add the key for address lookup tables
if (account.data.raw && isAddressLookupTableAccount(account.owner.toBase58() as Base58EncodedAddress, account.data.raw)) {
if (
account.data.raw &&
isAddressLookupTableAccount(account.owner.toBase58() as Base58EncodedAddress, account.data.raw)
) {
tabs.push(...TABS_LOOKUP['address-lookup-table']);
}

// Add the key for Metaplex NFTs
if (parsedData && (programTypeKey === 'spl-token:mint' || programTypeKey == 'spl-token-2022:mint') && (parsedData as TokenProgramData).nftData) {
if (
parsedData &&
(programTypeKey === 'spl-token:mint' || programTypeKey == 'spl-token-2022:mint') &&
(parsedData as TokenProgramData).nftData
) {
tabs.push(...TABS_LOOKUP[`${programTypeKey}:metaplexNFT`]);
}

Expand Down
169 changes: 169 additions & 0 deletions app/components/account/CompressedNftCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import { Account } from '@providers/accounts';
import { PublicKey } from '@solana/web3.js';
import { createRef, Suspense } from 'react';
import { ChevronDown, ExternalLink } from 'react-feather';
import useAsyncEffect from 'use-async-effect';

import { useCluster } from '@/app/providers/cluster';
import { CompressedNft, useCompressedNft, useMetadataJsonLink } from '@/app/providers/compressed-nft';

import { Address } from '../common/Address';
import { InfoTooltip } from '../common/InfoTooltip';
import { LoadingArtPlaceholder } from '../common/LoadingArtPlaceholder';
import { ArtContent } from '../common/NFTArt';
import { TableCardBody } from '../common/TableCardBody';
import { getCreatorDropdownItems, getIsMutablePill, getVerifiedCollectionPill } from './MetaplexNFTHeader';
import { UnknownAccountCard } from './UnknownAccountCard';

export function CompressedNftCard({ account }: { account: Account }) {
const { url } = useCluster();
const compressedNft = useCompressedNft({ address: account.pubkey.toString(), url });
if (!compressedNft) return <UnknownAccountCard account={account} />;

const collectionGroup = compressedNft.grouping.find(group => group.group_key === 'collection');
const updateAuthority = compressedNft.authorities.find(authority => authority.scopes.includes('full'))?.address;

return (
<div className="card">
<div className="card-header">
<h3 className="card-header-title mb-0 d-flex align-items-center">Overview</h3>
</div>
<TableCardBody>
<tr>
<td>Address</td>
<td className="text-lg-end">
<Address pubkey={account.pubkey} alignRight raw />
</td>
</tr>
<tr>
<td>Verified Collection Address</td>
<td className="text-lg-end">
{collectionGroup ? (
<Address pubkey={new PublicKey(collectionGroup.group_value)} alignRight link />
) : (
'None'
)}
</td>
</tr>
<tr>
<td>Update Authority</td>
<td className="text-lg-end">
{updateAuthority ? <Address pubkey={new PublicKey(updateAuthority)} alignRight link /> : 'None'}
</td>
</tr>
<tr>
<td>Website</td>
<td className="text-lg-end">
<a rel="noopener noreferrer" target="_blank" href={compressedNft.content.links.external_url}>
{compressedNft.content.links.external_url}
<ExternalLink className="align-text-top ms-2" size={13} />
</a>
</td>
</tr>
<tr>
<td>Seller Fee</td>
<td className="text-lg-end">{`${compressedNft.royalty.basis_points / 100}%`}</td>
</tr>
</TableCardBody>
</div>
);
}

function NoHeader() {
return (
<>
<h6 className="header-pretitle">Details</h6>
<h2 className="header-title">Account</h2>
</>
);
}

export function CompressedNftAccountHeader({ account }: { account: Account }) {
const { url } = useCluster();
const compressedNft = useCompressedNft({ address: account.pubkey.toString(), url });
if (!compressedNft) return NoHeader();

// LoadingCard
if (compressedNft) {
return (
<Suspense fallback={<LoadingArtPlaceholder />}>
<CompressedNFTHeader compressedNft={compressedNft} />
</Suspense>
);
}

return NoHeader();
}
export function CompressedNFTHeader({ compressedNft }: { compressedNft: CompressedNft }) {
const metadataJson = useMetadataJsonLink(compressedNft.content.json_uri);
const dropdownRef = createRef<HTMLButtonElement>();

useAsyncEffect(
async isMounted => {
if (!dropdownRef.current) {
return;
}
const Dropdown = (await import('bootstrap/js/dist/dropdown')).default;
if (!isMounted || !dropdownRef.current) {
return;
}
return new Dropdown(dropdownRef.current);
},
dropdown => {
if (dropdown) {
dropdown.dispose();
}
},
[dropdownRef]
);

return (
<div className="row">
<div className="col-auto ms-2 d-flex align-items-center">
<ArtContent pubkey={compressedNft.id} data={metadataJson} />
</div>
<div className="col mb-3 ms-0.5 mt-3">
{<h6 className="header-pretitle ms-1">Metaplex Compressed NFT</h6>}
<div className="d-flex align-items-center">
<h2 className="header-title ms-1 align-items-center no-overflow-with-ellipsis">
{compressedNft.content.metadata.name !== ''
? compressedNft.content.metadata.name
: 'No NFT name was found'}
</h2>
{getVerifiedCollectionPill()}
</div>
<h4 className="header-pretitle ms-1 mt-1 no-overflow-with-ellipsis">
{compressedNft.content.metadata.symbol !== ''
? compressedNft.content.metadata.symbol
: 'No Symbol was found'}
</h4>
<div className="mb-2 mt-2">{getCompressedNftPill()}</div>
<div className="mb-3 mt-2">{getIsMutablePill(compressedNft.mutable)}</div>
<div className="btn-group">
<button
className="btn btn-dark btn-sm creators-dropdown-button-width"
type="button"
aria-haspopup="true"
aria-expanded="false"
data-bs-toggle="dropdown"
ref={dropdownRef}
>
Creators <ChevronDown size={15} className="align-text-top" />
</button>
<div className="dropdown-menu mt-2">{getCreatorDropdownItems(compressedNft.creators)}</div>
</div>
</div>
</div>
);
}

function getCompressedNftPill() {
const onchainVerifiedToolTip =
'This NFT does not have on-chain data, but can be transferred and traded on-chain normally. This tag guarantees that this compressed NFT has the correct format.';
return (
<div className={'d-inline-flex align-items-center ms-2'}>
<span className="badge badge-pill bg-dark">{'Compressed'}</span>
<InfoTooltip bottom text={onchainVerifiedToolTip} />
</div>
);
}
6 changes: 3 additions & 3 deletions app/components/account/MetaplexNFTHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ export function MetaplexNFTHeader({ nftData, address }: { nftData: NFTData; addr
}

type Creator = programs.metadata.Creator;
function getCreatorDropdownItems(creators: Creator[] | null) {
export function getCreatorDropdownItems(creators: Creator[] | null) {
const CreatorHeader = () => {
const creatorTooltip = 'Verified creators signed the metadata associated with this NFT when it was created.';

Expand Down Expand Up @@ -170,11 +170,11 @@ function getSaleTypePill(hasPrimarySaleHappened: boolean) {
);
}

function getIsMutablePill(isMutable: boolean) {
export function getIsMutablePill(isMutable: boolean) {
return <span className="badge badge-pill bg-dark">{`${isMutable ? 'Mutable' : 'Immutable'}`}</span>;
}

function getVerifiedCollectionPill() {
export function getVerifiedCollectionPill() {
const onchainVerifiedToolTip =
'This NFT has been verified as a member of an on-chain collection. This tag guarantees authenticity.';
return (
Expand Down
2 changes: 1 addition & 1 deletion app/providers/accounts/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ export function isTokenProgramData(data: { program: string }): data is TokenProg
try {
assertIsTokenProgram(data.program);
return true;
} catch(e) {
} catch (e) {
return false;
}
}
Expand Down
Loading

0 comments on commit 14cd693

Please sign in to comment.