Skip to content

Commit

Permalink
Ray/v3 update (tokenbound#27)
Browse files Browse the repository at this point in the history
* add hook to fetch v2 and v3

* fix bug

* add notes & clean up code

* update UI to change accounts

* fix bug with nft render for acc

* add v3 v2 badge & disclaimer on v2
  • Loading branch information
huynhr authored Nov 17, 2023
1 parent 3f64521 commit 3d1b253
Show file tree
Hide file tree
Showing 12 changed files with 326 additions and 105 deletions.
109 changes: 68 additions & 41 deletions app/[contractAddress]/[tokenId]/[chainId]/Panel.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
/* eslint-disable @next/next/no-img-element */
import clsx from "clsx";
import { useState } from "react";
import { Check, Exclamation } from "@/components/icon";
import { Tabs, TabPanel, MediaViewer, ExternalLink } from "@/components/ui";
import { Check } from "@/components/icon";
import {
Tabs,
TabPanel,
MediaViewer,
ExternalLink,
DropdownMenu,
Disclaimer,
} from "@/components/ui";
import { TbaOwnedNft } from "@/lib/types";
import useSWR from "swr";
import { getAlchemy } from "@/lib/clients";
Expand All @@ -16,10 +23,55 @@ export const TABS = {
ASSETS: "Assets",
};

interface CopyAddressProps {
account: string;
displayedAddress: string;
}

const CopyAddress = ({ account, displayedAddress }: CopyAddressProps) => {
const [copied, setCopied] = useState(false);

return (
<div
className="inline-block rounded-2xl bg-[#F6F8FA] px-4 py-2 text-xs font-bold text-[#666D74] hover:cursor-pointer"
onClick={() => {
const textarea = document.createElement("textarea");
textarea.textContent = account;
textarea.style.position = "fixed"; // Prevent scrolling to bottom of page in MS Edge.
document.body.appendChild(textarea);
textarea.select();

try {
document.execCommand("copy"); // Security exception may be thrown by some browsers.
setCopied(true);
setTimeout(() => setCopied(false), 1000);

return;
} catch (ex) {
console.warn("Copy to clipboard failed.", ex);
return false;
} finally {
document.body.removeChild(textarea);
}
}}
>
{copied ? (
<span>
<Check />
</span>
) : (
shortenAddress(displayedAddress)
)}
</div>
);
};

interface Props {
className?: string;
approvalTokensCount?: number;
account?: string;
accounts?: string[];
handleAccountChange?: (account: string) => void;
tokens: TbaOwnedNft[];
title: string;
chainId: number;
Expand All @@ -29,6 +81,8 @@ export const Panel = ({
className,
approvalTokensCount,
account,
accounts,
handleAccountChange,
tokens,
title,
chainId,
Expand Down Expand Up @@ -58,53 +112,26 @@ export const Panel = ({
<div className="h-[2.5px] w-[34px] bg-[#E4E4E4]"></div>
</div>
<h1 className="text-base font-bold uppercase text-black">{title}</h1>

{account && displayedAddress && (
<div className="flex items-center justify-start space-x-2">
<span
className="inline-block rounded-2xl bg-[#F6F8FA] px-4 py-2 text-xs font-bold text-[#666D74] hover:cursor-pointer"
onClick={() => {
const textarea = document.createElement("textarea");
textarea.textContent = account;
textarea.style.position = "fixed"; // Prevent scrolling to bottom of page in MS Edge.
document.body.appendChild(textarea);
textarea.select();

try {
document.execCommand("copy"); // Security exception may be thrown by some browsers.
setCopied(true);
setTimeout(() => setCopied(false), 1000);

return;
} catch (ex) {
console.warn("Copy to clipboard failed.", ex);
return false;
} finally {
document.body.removeChild(textarea);
}
}}
<DropdownMenu
options={accounts}
currentOption={account}
setCurrentOption={handleAccountChange}
>
{copied ? (
<span>
<Check />
</span>
) : (
shortenAddress(displayedAddress)
)}
</span>
<CopyAddress account={account} displayedAddress={displayedAddress} />
</DropdownMenu>
<ExternalLink className="h-[20px] w-[20px]" link={etherscanLink} />
</div>
)}
{approvalTokensCount ? (
<div className="flex items-start space-x-2 rounded-lg border-0 bg-tb-warning-secondary p-2">
<div className="h-5 min-h-[20px] w-5 min-w-[20px]">
<Exclamation />
</div>
<p className="text-xs text-tb-warning-primary">
{`There are existing approvals on (${approvalTokensCount}) tokens owned by this account. Check approval status on tokenbound.org before purchasing.`}
</p>
</div>
<Disclaimer
message={`There are existing approvals on (${approvalTokensCount}) tokens owned by this account. Check approval status on tokenbound.org before purchasing.`}
/>
) : null}
{typeof account === "string" && accounts && account === accounts[1] && (
<Disclaimer message="Migrate your assets to V3 account for latest features." />
)}
<Tabs
tabs={Object.values(TABS)}
currentTab={currentTab}
Expand Down
6 changes: 6 additions & 0 deletions app/[contractAddress]/[tokenId]/[chainId]/TokenDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ interface Props {
handleOpenClose: (arg0: boolean) => void;
approvalTokensCount?: number;
account?: string;
accounts?: string[];
handleAccountChange: (arg0: string) => void;
tokens: TbaOwnedNft[];
title: string;
chainId: number;
Expand Down Expand Up @@ -48,6 +50,8 @@ export const TokenDetail = ({
handleOpenClose,
approvalTokensCount,
account,
accounts,
handleAccountChange,
tokens,
title,
chainId,
Expand Down Expand Up @@ -80,6 +84,8 @@ export const TokenDetail = ({
tokens={tokens}
title={title}
chainId={chainId}
accounts={accounts}
handleAccountChange={handleAccountChange}
/>
</motion.div>
)}
Expand Down
49 changes: 14 additions & 35 deletions app/[contractAddress]/[tokenId]/[chainId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ import { useEffect, useState } from "react";
import useSWR from "swr";
import { isNil } from "lodash";
import { TokenboundClient } from "@tokenbound/sdk";
import { getAccount, getAccountStatus, getLensNfts, getNfts } from "@/lib/utils";
import { getAccountStatus } from "@/lib/utils";
import { TbLogo } from "@/components/icon";
import { useGetApprovals, useNft } from "@/lib/hooks";
import { useGetApprovals, useNft, useTBADetails } from "@/lib/hooks";
import { TbaOwnedNft } from "@/lib/types";
import { getAddress } from "viem";
import { TokenDetail } from "./TokenDetail";
import { HAS_CUSTOM_IMPLEMENTATION, alchemyApiKey } from "@/lib/constants";
import { HAS_CUSTOM_IMPLEMENTATION } from "@/lib/constants";

interface TokenParams {
params: {
Expand All @@ -26,12 +26,11 @@ interface TokenParams {

export default function Token({ params, searchParams }: TokenParams) {
const [imagesLoaded, setImagesLoaded] = useState(false);
const [nfts, setNfts] = useState<TbaOwnedNft[]>([]);
const [lensNfts, setLensNfts] = useState<TbaOwnedNft[]>([]);
const { tokenId, contractAddress, chainId } = params;
const { disableloading, logo } = searchParams;
const [showTokenDetail, setShowTokenDetail] = useState(false);
const chainIdNumber = parseInt(chainId);

const tokenboundClient = new TokenboundClient({ chainId: chainIdNumber });

const {
Expand Down Expand Up @@ -67,9 +66,11 @@ export default function Token({ params, searchParams }: TokenParams) {
}, [nftImages, nftMetadataLoading]);

// Fetch nft's TBA
const { data: account } = useSWR(tokenId ? `/account/${tokenId}` : null, async () => {
const result = await getAccount(Number(tokenId), contractAddress, chainIdNumber);
return result.data;
const { account, nfts, handleAccountChange, tba, tbaV2 } = useTBADetails({
tokenboundClient,
tokenId,
tokenContract: contractAddress as `0x${string}`,
chainId: chainIdNumber,
});

// Get nft's TBA account bytecode to check if account is deployed or not
Expand All @@ -88,33 +89,12 @@ export default function Token({ params, searchParams }: TokenParams) {
return data ?? false;
});

// fetch nfts inside TBA
useEffect(() => {
async function fetchNfts(account: string) {
const [data, lensData] = await Promise.all([
getNfts(chainIdNumber, account),
getLensNfts(account),
]);
if (data) {
setNfts(data);
}
if (lensData) {
setLensNfts(lensData);
}
}

if (account) {
fetchNfts(account);
}
}, [account, accountIsDeployed, chainIdNumber]);

const [tokens, setTokens] = useState<TbaOwnedNft[]>([]);
const allNfts = [...nfts, ...lensNfts];

const { data: approvalData } = useGetApprovals(allNfts, account, chainIdNumber);
const { data: approvalData } = useGetApprovals(nfts, account, chainIdNumber);

useEffect(() => {
if (nfts !== undefined && nfts.length) {
if (nfts !== undefined) {
nfts.map((token) => {
const foundApproval = approvalData?.find((item) => {
const contract = item.contract.address;
Expand All @@ -129,11 +109,8 @@ export default function Token({ params, searchParams }: TokenParams) {
token.hasApprovals = foundApproval?.hasApprovals || false;
});
setTokens(nfts);
if (lensNfts) {
setTokens([...nfts, ...lensNfts]);
}
}
}, [nfts, approvalData, lensNfts]);
}, [nfts, approvalData, account]);

const showLoading = disableloading !== "true" && nftMetadataLoading;

Expand All @@ -151,6 +128,8 @@ export default function Token({ params, searchParams }: TokenParams) {
title={nftMetadata.title}
chainId={chainIdNumber}
logo={logo}
accounts={[tba, tbaV2 as string]}
handleAccountChange={handleAccountChange}
/>
)}
<div className="max-h-1080[px] relative h-full w-full max-w-[1080px]">
Expand Down
24 changes: 24 additions & 0 deletions components/ui/Disclaimer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import clsx from "clsx";
import { Exclamation } from "../icon";

interface Props
extends React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement> {
message: string;
}

export const Disclaimer = ({ message, className, ...rest }: Props) => {
return (
<div
className={clsx(
className,
"flex items-start space-x-2 rounded-lg border-0 bg-tb-warning-secondary p-2"
)}
{...rest}
>
<div className="h-5 min-h-[20px] w-5 min-w-[20px]">
<Exclamation />
</div>
<p className="text-xs text-tb-warning-primary">{message}</p>
</div>
);
};
Loading

0 comments on commit 3d1b253

Please sign in to comment.