diff --git a/app/nfts/[slug]/error.tsx b/app/nfts/[slug]/error.tsx new file mode 100644 index 0000000..fa7462e --- /dev/null +++ b/app/nfts/[slug]/error.tsx @@ -0,0 +1,17 @@ +"use client"; + +export default function Error({ + error, + reset, +}: { + error: Error; + reset: () => void; +}) { + return ( +
+

Something went wrong!

+

{error.message}

+ +
+ ); +} diff --git a/app/nfts/[slug]/layout.tsx b/app/nfts/[slug]/layout.tsx new file mode 100644 index 0000000..0610ee2 --- /dev/null +++ b/app/nfts/[slug]/layout.tsx @@ -0,0 +1,14 @@ +import Header from "@/components/Header"; + +export default function NFTsLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( +
+
+ {children} +
+ ); +} diff --git a/app/nfts/[slug]/loading.tsx b/app/nfts/[slug]/loading.tsx new file mode 100644 index 0000000..9384f45 --- /dev/null +++ b/app/nfts/[slug]/loading.tsx @@ -0,0 +1,11 @@ +import NFTCardSkeleton from "@/components/NFTCardSkeleton"; + +export default function Loading() { + return ( +
+ {[...Array(8)].map((_, i) => ( + + ))} +
+ ); +} diff --git a/app/nfts/[slug]/page.tsx b/app/nfts/[slug]/page.tsx new file mode 100644 index 0000000..b27d12d --- /dev/null +++ b/app/nfts/[slug]/page.tsx @@ -0,0 +1,66 @@ +import React from "react"; +import NFTCard from "@/components/NFTCard"; +import { NFT } from "@/typings"; +import { ethers } from "ethers"; +import abi from "@/artifacts/contracts/ByteBunch.sol/ByteBunch.json"; + +type Props = { + params: { + slug: string; + }; +}; + +const ipfsToHttps = (ipfs: string) => { + const gateway = "https://ipfs.io/ipfs/"; + const url = ipfs.replace("ipfs://", gateway); + return url; +}; + +const getData = async (address: string) => { + try { + const signer = new ethers.InfuraProvider( + "sepolia", + process.env.INFURA_API_KEY + ); + const contract = new ethers.Contract( + process.env.NEXT_PUBLIC_CONTRACT_ADDRESS!, + abi.abi, + signer + ); + + const tokens = await contract.walletOfOwner(address); + + const data = await Promise.all( + tokens.map(async (token: any) => { + const metadata = await contract.tokenURI(token); + const metadataJSON = await fetch(ipfsToHttps(metadata)); + const metadataJSONParsed = await metadataJSON.json(); + metadataJSONParsed.image = ipfsToHttps(metadataJSONParsed.image); + return metadataJSONParsed; + }) + ); + + return data; + } catch (error: any) { + throw new Error(error.message); + } +}; + +const NFTs = async ({ params }: Props) => { + const address = params.slug; + + const data: NFT[] = await getData(address); + return ( +
+ {data.length > 0 ? ( + data.map((item) => ) + ) : ( +
+ No NFTs found for address {address} +
+ )} +
+ ); +}; + +export default NFTs; diff --git a/app/nfts/page.tsx b/app/nfts/page.tsx deleted file mode 100644 index 35b5155..0000000 --- a/app/nfts/page.tsx +++ /dev/null @@ -1,165 +0,0 @@ -"use client"; -import React, { use, useEffect, useState } from "react"; -import { sepolia } from "@/utils/networks"; -import { toast } from "react-hot-toast"; -import { ethers } from "ethers"; -import abi from "@/artifacts/contracts/ByteBunch.sol/ByteBunch.json"; -import Image from "next/image"; - -type Props = {}; - -declare let window: any; - -export type NFT = { - dna: string; - name: string; - description: string; - image: string; - edition: number; - date: number; - attributes: Attribute[]; -}; - -export type Attribute = { - trait_type: string; - value: string; -}; - -const NFTs = (props: Props) => { - const [loading, setLoading] = useState(false); - const [error, setError] = useState(); - const [data, setData] = useState([]); - - const switchNetwork = async () => { - try { - await window.ethereum.request({ - method: "wallet_switchEthereumChain", - params: [{ chainId: sepolia.chainId }], - }); - } catch (error) { - toast.error("Please switch to Sepolia Network"); - } - }; - - const getSigner = async () => { - try { - const provider = new ethers.BrowserProvider(window.ethereum); - const network = await provider.getNetwork(); - const chainId = Number(ethers.formatUnits(network.chainId)) * 10e17; - - if (chainId !== parseInt(sepolia.chainId)) { - switchNetwork(); - throw new Error("Please switch to Sepolia Network and try again"); - } - - const signer = await provider.getSigner(); - return signer; - } catch (error: any) { - throw new Error(error.message.split("(")[0]); - } - }; - - const getContract = async () => { - try { - const signer = await getSigner(); - - const contract = new ethers.Contract( - process.env.NEXT_PUBLIC_CONTRACT_ADDRESS!, - abi.abi, - signer - ); - - return contract; - } catch (error: any) { - throw new Error(error.message.split("(")[0]); - } - }; - - const getTokenURI = async (tokenId: Number) => { - try { - const contract = await getContract(); - const tokenURI = await contract.tokenURI(tokenId); - - return tokenURI; - } catch (error: any) { - throw new Error(error.message.split("(")[0]); - } - }; - - const getMetadata = async (tokenId: Number) => { - try { - const tokenURI = await getTokenURI(tokenId); - const url = ipfsToHttps(tokenURI); - - const metadata = await fetch(url); - const metadataJSON: NFT = await metadata.json(); - metadataJSON.image = ipfsToHttps(metadataJSON.image); - - return metadataJSON; - } catch (error: any) { - throw new Error(error.message.split("(")[0]); - } - }; - - useEffect(() => { - const loadNFTs = async () => { - setLoading(true); - try { - const signer = await getSigner(); - const contract = await getContract(); - const tokens = await contract.walletOfOwner(signer.getAddress()); - - tokens.map(async (token: any) => { - const data = await getMetadata(token); - setData((prev: any) => [...prev, data]); - }); - } catch (error: any) { - setError(error.message.split("(")[0]); - } - setLoading(false); - }; - loadNFTs(); - }, []); - - const ipfsToHttps = (ipfs: string) => { - const gateway = "https://ipfs.io/ipfs/"; - const url = ipfs.replace("ipfs://", gateway); - return url; - }; - - if (loading) return
Loading...
; - if (error) return
{error}
; - - return ( -
-

NFTs

-
- {data.map((item) => ( -
- {item.name} -
-

{item.name}

-

{item.description}

-
-
- {item.attributes.map((attribute) => ( -
- {attribute.trait_type}: - {attribute.value} -
- ))} -
-
- ))} -
-
- ); -}; - -export default NFTs; diff --git a/components/Header.tsx b/components/Header.tsx new file mode 100644 index 0000000..8b9c953 --- /dev/null +++ b/components/Header.tsx @@ -0,0 +1,22 @@ +import Link from "next/link"; + +export default function Header() { + return ( +
+ +
+ ); +} diff --git a/components/NFTCard.tsx b/components/NFTCard.tsx new file mode 100644 index 0000000..844bc3a --- /dev/null +++ b/components/NFTCard.tsx @@ -0,0 +1,33 @@ +import { NFT } from "@/typings"; +import Image from "next/image"; +import Link from "next/link"; +import React from "react"; + +type Props = { + data: NFT; +}; + +const NFTCard = ({ data }: Props) => { + return ( + +
+
+ {data.name} +
+
+
+ {data.name} +
+
+
+ + ); +}; + +export default NFTCard; diff --git a/components/NFTCardSkeleton.tsx b/components/NFTCardSkeleton.tsx new file mode 100644 index 0000000..0b03ff3 --- /dev/null +++ b/components/NFTCardSkeleton.tsx @@ -0,0 +1,18 @@ +const NFTCardSkeleton = () => { + return ( +
+
+
+
+
+
+
+   +
+
+
+
+ ); +}; + +export default NFTCardSkeleton; diff --git a/typings.d.ts b/typings.d.ts new file mode 100644 index 0000000..857eea0 --- /dev/null +++ b/typings.d.ts @@ -0,0 +1,14 @@ +export type NFT = { + dna: string; + name: string; + description: string; + image: string; + edition: number; + date: number; + attributes: Attribute[]; +}; + +export type Attribute = { + trait_type: string; + value: string; +};