├── .env.example ├── .gitignore ├── .prettierignore ├── .prettierrc ├── README.md ├── addEvents.js ├── components ├── Header.js ├── NFTBox.js └── UpdateListingModal.js ├── constants ├── BasicNft.json ├── NftMarketplace.json ├── networkMapping.json └── subgraphQueries.js ├── next.config.js ├── package.json ├── pages ├── _app.js ├── api │ └── hello.js ├── graphExample.js ├── index.js └── sell-nft.js ├── postcss.config.js ├── public ├── favicon.ico └── vercel.svg ├── styles ├── Home.module.css └── globals.css ├── tailwind.config.js └── yarn.lock /.env.example: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_SUBGRAPH_URL=https://subgraph.thegraph.... -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | .env 4 | .encryptedKey.json 5 | 6 | # dependencies 7 | /node_modules 8 | /.pnp 9 | .pnp.js 10 | 11 | # testing 12 | /coverage 13 | 14 | # next.js 15 | /.next/ 16 | /out/ 17 | 18 | # production 19 | /build 20 | 21 | # misc 22 | .DS_Store 23 | *.pem 24 | 25 | # debug 26 | npm-debug.log* 27 | yarn-debug.log* 28 | yarn-error.log* 29 | .pnpm-debug.log* 30 | 31 | # local env files 32 | .env*.local 33 | 34 | # vercel 35 | .vercel 36 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | artifacts 3 | cache 4 | coverage* 5 | gasReporterOutput.json 6 | package.json 7 | img 8 | .env 9 | .* 10 | README.md 11 | coverage.json 12 | deployments 13 | .next -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 4, 3 | "useTabs": false, 4 | "semi": false, 5 | "singleQuote": false, 6 | "printWidth": 99 7 | } 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Important update! 2 | > !Update! 3 | > As of May 31, 2024. We are no longer supporting this repo/tutorial. You can still follow the course, but it's a little stale. 4 | > Please visit [Cyfrin Updraft](https://updraft.cyfrin.io/) and stay tuned for a new full-stack course slated for 2024! 5 | 6 | 7 | # NextJS NFT Marketplace with TheGraph 8 | 9 | *This repo has been updated for Sepolia over Goerli.* 10 | 11 | ## 1. Git clone the contracts repo 12 | 13 | In it's own terminal / command line, run: 14 | 15 | ``` 16 | git clone https://github.com/PatrickAlphaC/hardhat-nft-marketplace-fcc 17 | cd hardhat-nextjs-nft-marketplace-fcc 18 | yarn 19 | ``` 20 | 21 | ## 2. Deploy to sepolia 22 | 23 | After installing dependencies, deploy your contracts to sepolia: 24 | 25 | ``` 26 | yarn hardhat deploy --network sepolia 27 | ``` 28 | 29 | ## 3. Deploy your subgraph 30 | 31 | ``` 32 | cd .. 33 | git clone https://github.com/PatrickAlphaC/graph-nft-marketplace-fcc 34 | cd graph-nft-marketplace-fcc 35 | yarn 36 | ``` 37 | 38 | Follow the instructions of the [README](https://github.com/PatrickAlphaC/graph-nft-marketplace-fcc/blob/main/README.md) of that repo. 39 | 40 | Then, make a `.env` file and place your temporary query URL into it as `NEXT_PUBLIC_SUBGRAPH_URL`. 41 | 42 | 43 | ## 4. Start your UI 44 | 45 | Make sure that: 46 | - In your `networkMapping.json` you have an entry for `NftMarketplace` on the sepolia network. 47 | - You have a `NEXT_PUBLIC_SUBGRAPH_URL` in your `.env` file. 48 | 49 | ``` 50 | yarn dev 51 | ``` 52 | 53 | -------------------------------------------------------------------------------- /addEvents.js: -------------------------------------------------------------------------------- 1 | const Moralis = require("moralis/node") 2 | require("dotenv").config() 3 | const contractAddresses = require("./constants/networkMapping.json") 4 | let chainId = process.env.chainId || 31337 5 | let moralisChainId = chainId == "31337" ? "1337" : chainId 6 | const contractAddress = contractAddresses[chainId]["NftMarketplace"][0] 7 | 8 | const serverUrl = process.env.NEXT_PUBLIC_SERVER_URL 9 | const appId = process.env.NEXT_PUBLIC_APP_ID 10 | const masterKey = process.env.masterKey 11 | 12 | async function main() { 13 | await Moralis.start({ serverUrl, appId, masterKey }) 14 | console.log(`Working with contrat address ${contractAddress}`) 15 | 16 | let itemListedOptions = { 17 | // Moralis understands a local chain is 1337 18 | chainId: moralisChainId, 19 | sync_historical: true, 20 | topic: "ItemListed(address,address,uint256,uint256)", 21 | address: contractAddress, 22 | abi: { 23 | anonymous: false, 24 | inputs: [ 25 | { 26 | indexed: true, 27 | internalType: "address", 28 | name: "seller", 29 | type: "address", 30 | }, 31 | { 32 | indexed: true, 33 | internalType: "address", 34 | name: "nftAddress", 35 | type: "address", 36 | }, 37 | { 38 | indexed: true, 39 | internalType: "uint256", 40 | name: "tokenId", 41 | type: "uint256", 42 | }, 43 | { 44 | indexed: false, 45 | internalType: "uint256", 46 | name: "price", 47 | type: "uint256", 48 | }, 49 | ], 50 | name: "ItemListed", 51 | type: "event", 52 | }, 53 | tableName: "ItemListed", 54 | } 55 | 56 | let itemBoughtOptions = { 57 | chainId: moralisChainId, 58 | address: contractAddress, 59 | sync_historical: true, 60 | topic: "ItemBought(address,address,uint256,uint256)", 61 | abi: { 62 | anonymous: false, 63 | inputs: [ 64 | { 65 | indexed: true, 66 | internalType: "address", 67 | name: "buyer", 68 | type: "address", 69 | }, 70 | { 71 | indexed: true, 72 | internalType: "address", 73 | name: "nftAddress", 74 | type: "address", 75 | }, 76 | { 77 | indexed: true, 78 | internalType: "uint256", 79 | name: "tokenId", 80 | type: "uint256", 81 | }, 82 | { 83 | indexed: false, 84 | internalType: "uint256", 85 | name: "price", 86 | type: "uint256", 87 | }, 88 | ], 89 | name: "ItemBought", 90 | type: "event", 91 | }, 92 | tableName: "ItemBought", 93 | } 94 | 95 | let itemCanceledOptions = { 96 | chainId: moralisChainId, 97 | address: contractAddress, 98 | topic: "ItemCanceled(address,address,uint256)", 99 | sync_historical: true, 100 | abi: { 101 | anonymous: false, 102 | inputs: [ 103 | { 104 | indexed: true, 105 | internalType: "address", 106 | name: "seller", 107 | type: "address", 108 | }, 109 | { 110 | indexed: true, 111 | internalType: "address", 112 | name: "nftAddress", 113 | type: "address", 114 | }, 115 | { 116 | indexed: true, 117 | internalType: "uint256", 118 | name: "tokenId", 119 | type: "uint256", 120 | }, 121 | ], 122 | name: "ItemCanceled", 123 | type: "event", 124 | }, 125 | tableName: "ItemCanceled", 126 | } 127 | 128 | const listedResponse = await Moralis.Cloud.run("watchContractEvent", itemListedOptions, { 129 | useMasterKey: true, 130 | }) 131 | const boughtResponse = await Moralis.Cloud.run("watchContractEvent", itemBoughtOptions, { 132 | useMasterKey: true, 133 | }) 134 | const canceledResponse = await Moralis.Cloud.run("watchContractEvent", itemCanceledOptions, { 135 | useMasterKey: true, 136 | }) 137 | if (listedResponse.success && canceledResponse.success && boughtResponse.success) { 138 | console.log("Success! Database Updated with watching events") 139 | } else { 140 | console.log("Something went wrong...") 141 | } 142 | } 143 | 144 | main() 145 | .then(() => process.exit(0)) 146 | .catch((error) => { 147 | console.error(error) 148 | process.exit(1) 149 | }) 150 | -------------------------------------------------------------------------------- /components/Header.js: -------------------------------------------------------------------------------- 1 | import { ConnectButton } from "web3uikit" 2 | import Link from "next/link" 3 | 4 | export default function Header() { 5 | return ( 6 | 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /components/NFTBox.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react" 2 | import { useWeb3Contract, useMoralis } from "react-moralis" 3 | import nftMarketplaceAbi from "../constants/NftMarketplace.json" 4 | import nftAbi from "../constants/BasicNft.json" 5 | import Image from "next/image" 6 | import { Card, useNotification } from "web3uikit" 7 | import { ethers } from "ethers" 8 | import UpdateListingModal from "./UpdateListingModal" 9 | 10 | const truncateStr = (fullStr, strLen) => { 11 | if (fullStr.length <= strLen) return fullStr 12 | 13 | const separator = "..." 14 | const seperatorLength = separator.length 15 | const charsToShow = strLen - seperatorLength 16 | const frontChars = Math.ceil(charsToShow / 2) 17 | const backChars = Math.floor(charsToShow / 2) 18 | return ( 19 | fullStr.substring(0, frontChars) + 20 | separator + 21 | fullStr.substring(fullStr.length - backChars) 22 | ) 23 | } 24 | 25 | export default function NFTBox({ price, nftAddress, tokenId, marketplaceAddress, seller }) { 26 | const { isWeb3Enabled, account } = useMoralis() 27 | const [imageURI, setImageURI] = useState("") 28 | const [tokenName, setTokenName] = useState("") 29 | const [tokenDescription, setTokenDescription] = useState("") 30 | const [showModal, setShowModal] = useState(false) 31 | const hideModal = () => setShowModal(false) 32 | const dispatch = useNotification() 33 | 34 | const { runContractFunction: getTokenURI } = useWeb3Contract({ 35 | abi: nftAbi, 36 | contractAddress: nftAddress, 37 | functionName: "tokenURI", 38 | params: { 39 | tokenId: tokenId, 40 | }, 41 | }) 42 | 43 | const { runContractFunction: buyItem } = useWeb3Contract({ 44 | abi: nftMarketplaceAbi, 45 | contractAddress: marketplaceAddress, 46 | functionName: "buyItem", 47 | msgValue: price, 48 | params: { 49 | nftAddress: nftAddress, 50 | tokenId: tokenId, 51 | }, 52 | }) 53 | 54 | async function updateUI() { 55 | const tokenURI = await getTokenURI() 56 | console.log(`The TokenURI is ${tokenURI}`) 57 | // We are going to cheat a little here... 58 | if (tokenURI) { 59 | // IPFS Gateway: A server that will return IPFS files from a "normal" URL. 60 | const requestURL = tokenURI.replace("ipfs://", "https://ipfs.io/ipfs/") 61 | const tokenURIResponse = await (await fetch(requestURL)).json() 62 | const imageURI = tokenURIResponse.image 63 | const imageURIURL = imageURI.replace("ipfs://", "https://ipfs.io/ipfs/") 64 | setImageURI(imageURIURL) 65 | setTokenName(tokenURIResponse.name) 66 | setTokenDescription(tokenURIResponse.description) 67 | // We could render the Image on our sever, and just call our sever. 68 | // For testnets & mainnet -> use moralis server hooks 69 | // Have the world adopt IPFS 70 | // Build our own IPFS gateway 71 | } 72 | // get the tokenURI 73 | // using the image tag from the tokenURI, get the image 74 | } 75 | 76 | useEffect(() => { 77 | if (isWeb3Enabled) { 78 | updateUI() 79 | } 80 | }, [isWeb3Enabled]) 81 | 82 | const isOwnedByUser = seller === account || seller === undefined 83 | const formattedSellerAddress = isOwnedByUser ? "you" : truncateStr(seller || "", 15) 84 | 85 | const handleCardClick = () => { 86 | isOwnedByUser 87 | ? setShowModal(true) 88 | : buyItem({ 89 | onError: (error) => console.log(error), 90 | onSuccess: () => handleBuyItemSuccess(), 91 | }) 92 | } 93 | 94 | const handleBuyItemSuccess = () => { 95 | dispatch({ 96 | type: "success", 97 | message: "Item bought!", 98 | title: "Item Bought", 99 | position: "topR", 100 | }) 101 | } 102 | 103 | return ( 104 |
105 |
106 | {imageURI ? ( 107 |
108 | 115 | 120 |
121 |
122 |
#{tokenId}
123 |
124 | Owned by {formattedSellerAddress} 125 |
126 | imageURI} 128 | src={imageURI} 129 | height="200" 130 | width="200" 131 | /> 132 |
133 | {ethers.utils.formatUnits(price, "ether")} ETH 134 |
135 |
136 |
137 |
138 |
139 | ) : ( 140 |
Loading...
141 | )} 142 |
143 |
144 | ) 145 | } 146 | -------------------------------------------------------------------------------- /components/UpdateListingModal.js: -------------------------------------------------------------------------------- 1 | import { Modal, Input, useNotification } from "web3uikit" 2 | import { useState } from "react" 3 | import { useWeb3Contract } from "react-moralis" 4 | import nftMarketplaceAbi from "../constants/NftMarketplace.json" 5 | import { ethers } from "ethers" 6 | 7 | export default function UpdateListingModal({ 8 | nftAddress, 9 | tokenId, 10 | isVisible, 11 | marketplaceAddress, 12 | onClose, 13 | }) { 14 | const dispatch = useNotification() 15 | 16 | const [priceToUpdateListingWith, setPriceToUpdateListingWith] = useState(0) 17 | 18 | const handleUpdateListingSuccess = () => { 19 | dispatch({ 20 | type: "success", 21 | message: "listing updated", 22 | title: "Listing updated - please refresh (and move blocks)", 23 | position: "topR", 24 | }) 25 | onClose && onClose() 26 | setPriceToUpdateListingWith("0") 27 | } 28 | 29 | const { runContractFunction: updateListing } = useWeb3Contract({ 30 | abi: nftMarketplaceAbi, 31 | contractAddress: marketplaceAddress, 32 | functionName: "updateListing", 33 | params: { 34 | nftAddress: nftAddress, 35 | tokenId: tokenId, 36 | newPrice: ethers.utils.parseEther(priceToUpdateListingWith || "0"), 37 | }, 38 | }) 39 | 40 | return ( 41 | { 46 | updateListing({ 47 | onError: (error) => { 48 | console.log(error) 49 | }, 50 | onSuccess: () => handleUpdateListingSuccess(), 51 | }) 52 | }} 53 | > 54 | { 59 | setPriceToUpdateListingWith(event.target.value) 60 | }} 61 | /> 62 | 63 | ) 64 | } 65 | -------------------------------------------------------------------------------- /constants/BasicNft.json: -------------------------------------------------------------------------------- 1 | [{"type":"constructor","payable":false,"inputs":[]},{"type":"event","anonymous":false,"name":"Approval","inputs":[{"type":"address","name":"owner","indexed":true},{"type":"address","name":"approved","indexed":true},{"type":"uint256","name":"tokenId","indexed":true}]},{"type":"event","anonymous":false,"name":"ApprovalForAll","inputs":[{"type":"address","name":"owner","indexed":true},{"type":"address","name":"operator","indexed":true},{"type":"bool","name":"approved","indexed":false}]},{"type":"event","anonymous":false,"name":"DogMinted","inputs":[{"type":"uint256","name":"tokenId","indexed":true}]},{"type":"event","anonymous":false,"name":"Transfer","inputs":[{"type":"address","name":"from","indexed":true},{"type":"address","name":"to","indexed":true},{"type":"uint256","name":"tokenId","indexed":true}]},{"type":"function","name":"TOKEN_URI","constant":true,"stateMutability":"view","payable":false,"inputs":[],"outputs":[{"type":"string"}]},{"type":"function","name":"approve","constant":false,"payable":false,"inputs":[{"type":"address","name":"to"},{"type":"uint256","name":"tokenId"}],"outputs":[]},{"type":"function","name":"balanceOf","constant":true,"stateMutability":"view","payable":false,"inputs":[{"type":"address","name":"owner"}],"outputs":[{"type":"uint256"}]},{"type":"function","name":"getApproved","constant":true,"stateMutability":"view","payable":false,"inputs":[{"type":"uint256","name":"tokenId"}],"outputs":[{"type":"address"}]},{"type":"function","name":"getTokenCounter","constant":true,"stateMutability":"view","payable":false,"inputs":[],"outputs":[{"type":"uint256"}]},{"type":"function","name":"isApprovedForAll","constant":true,"stateMutability":"view","payable":false,"inputs":[{"type":"address","name":"owner"},{"type":"address","name":"operator"}],"outputs":[{"type":"bool"}]},{"type":"function","name":"mintNft","constant":false,"payable":false,"inputs":[],"outputs":[]},{"type":"function","name":"name","constant":true,"stateMutability":"view","payable":false,"inputs":[],"outputs":[{"type":"string"}]},{"type":"function","name":"ownerOf","constant":true,"stateMutability":"view","payable":false,"inputs":[{"type":"uint256","name":"tokenId"}],"outputs":[{"type":"address"}]},{"type":"function","name":"safeTransferFrom","constant":false,"payable":false,"inputs":[{"type":"address","name":"from"},{"type":"address","name":"to"},{"type":"uint256","name":"tokenId"}],"outputs":[]},{"type":"function","name":"safeTransferFrom","constant":false,"payable":false,"inputs":[{"type":"address","name":"from"},{"type":"address","name":"to"},{"type":"uint256","name":"tokenId"},{"type":"bytes","name":"_data"}],"outputs":[]},{"type":"function","name":"setApprovalForAll","constant":false,"payable":false,"inputs":[{"type":"address","name":"operator"},{"type":"bool","name":"approved"}],"outputs":[]},{"type":"function","name":"supportsInterface","constant":true,"stateMutability":"view","payable":false,"inputs":[{"type":"bytes4","name":"interfaceId"}],"outputs":[{"type":"bool"}]},{"type":"function","name":"symbol","constant":true,"stateMutability":"view","payable":false,"inputs":[],"outputs":[{"type":"string"}]},{"type":"function","name":"tokenURI","constant":true,"stateMutability":"view","payable":false,"inputs":[{"type":"uint256","name":"tokenId"}],"outputs":[{"type":"string"}]},{"type":"function","name":"transferFrom","constant":false,"payable":false,"inputs":[{"type":"address","name":"from"},{"type":"address","name":"to"},{"type":"uint256","name":"tokenId"}],"outputs":[]}] -------------------------------------------------------------------------------- /constants/NftMarketplace.json: -------------------------------------------------------------------------------- 1 | [{"type":"error","name":"NftMarketplace__AlreadyListed","inputs":[{"type":"address","name":"nftAddress"},{"type":"uint256","name":"tokenId"}]},{"type":"error","name":"NftMarketplace__NoProceeds","inputs":[]},{"type":"error","name":"NftMarketplace__NotApprovedForMarketplace","inputs":[]},{"type":"error","name":"NftMarketplace__NotListed","inputs":[{"type":"address","name":"nftAddress"},{"type":"uint256","name":"tokenId"}]},{"type":"error","name":"NftMarketplace__NotOwner","inputs":[]},{"type":"error","name":"NftMarketplace__PriceMustBeAboveZero","inputs":[]},{"type":"error","name":"NftMarketplace__PriceNotMet","inputs":[{"type":"address","name":"nftAddress"},{"type":"uint256","name":"tokenId"},{"type":"uint256","name":"price"}]},{"type":"error","name":"NftMarketplace__TransferFailed","inputs":[]},{"type":"event","anonymous":false,"name":"ItemBought","inputs":[{"type":"address","name":"buyer","indexed":true},{"type":"address","name":"nftAddress","indexed":true},{"type":"uint256","name":"tokenId","indexed":true},{"type":"uint256","name":"price","indexed":false}]},{"type":"event","anonymous":false,"name":"ItemCanceled","inputs":[{"type":"address","name":"seller","indexed":true},{"type":"address","name":"nftAddress","indexed":true},{"type":"uint256","name":"tokenId","indexed":true}]},{"type":"event","anonymous":false,"name":"ItemListed","inputs":[{"type":"address","name":"seller","indexed":true},{"type":"address","name":"nftAddress","indexed":true},{"type":"uint256","name":"tokenId","indexed":true},{"type":"uint256","name":"price","indexed":false}]},{"type":"function","name":"buyItem","constant":false,"stateMutability":"payable","payable":true,"inputs":[{"type":"address","name":"nftAddress"},{"type":"uint256","name":"tokenId"}],"outputs":[]},{"type":"function","name":"cancelListing","constant":false,"payable":false,"inputs":[{"type":"address","name":"nftAddress"},{"type":"uint256","name":"tokenId"}],"outputs":[]},{"type":"function","name":"getListing","constant":true,"stateMutability":"view","payable":false,"inputs":[{"type":"address","name":"nftAddress"},{"type":"uint256","name":"tokenId"}],"outputs":[{"type":"tuple","components":[{"type":"uint256","name":"price"},{"type":"address","name":"seller"}]}]},{"type":"function","name":"getProceeds","constant":true,"stateMutability":"view","payable":false,"inputs":[{"type":"address","name":"seller"}],"outputs":[{"type":"uint256"}]},{"type":"function","name":"listItem","constant":false,"payable":false,"inputs":[{"type":"address","name":"nftAddress"},{"type":"uint256","name":"tokenId"},{"type":"uint256","name":"price"}],"outputs":[]},{"type":"function","name":"updateListing","constant":false,"payable":false,"inputs":[{"type":"address","name":"nftAddress"},{"type":"uint256","name":"tokenId"},{"type":"uint256","name":"newPrice"}],"outputs":[]},{"type":"function","name":"withdrawProceeds","constant":false,"payable":false,"inputs":[],"outputs":[]}] -------------------------------------------------------------------------------- /constants/networkMapping.json: -------------------------------------------------------------------------------- 1 | {"11155111":{"NftMarketplace":["0x813c6080F2BCdB5855c4c6A8B5E3f3c6DF91008E"]},"31337":{"NftMarketplace":["0x5FbDB2315678afecb367f032d93F642f64180aa3"]}} 2 | -------------------------------------------------------------------------------- /constants/subgraphQueries.js: -------------------------------------------------------------------------------- 1 | import { gql } from "@apollo/client" 2 | 3 | const GET_ACTIVE_ITEMS = gql` 4 | { 5 | activeItems(first: 5, where: { buyer: "0x0000000000000000000000000000000000000000" }) { 6 | id 7 | buyer 8 | seller 9 | nftAddress 10 | tokenId 11 | price 12 | } 13 | } 14 | ` 15 | export default GET_ACTIVE_ITEMS 16 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | images: { 5 | loader: "akamai", 6 | path: "", 7 | }, 8 | } 9 | 10 | module.exports = nextConfig 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextjs-nft-marketplace-fcc", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "export": "next export", 11 | "moralis:sync": "moralis-admin-cli connect-local-devchain --chain hardhat --moralisSubdomain wciosc5v5doe.usemoralis.com --frpcPath ./frp/frpc", 12 | "moralis:cloud": "moralis-admin-cli watch-cloud-folder --moralisSubdomain wciosc5v5doe.usemoralis.com --autoSave 1 --moralisCloudfolder ./cloudFunctions" 13 | }, 14 | "dependencies": { 15 | "@apollo/client": "3.5.10", 16 | "graphql": "^16.4.0", 17 | "moralis": "^1.5.11", 18 | "next": "12.1.5", 19 | "react": "18.1.0", 20 | "react-dom": "18.1.0", 21 | "react-moralis": "^1.3.5", 22 | "web3uikit": "^0.0.133" 23 | }, 24 | "devDependencies": { 25 | "autoprefixer": "^10.4.5", 26 | "dotenv": "^16.0.0", 27 | "eslint": "8.14.0", 28 | "eslint-config-next": "12.1.5", 29 | "postcss": "^8.4.12", 30 | "tailwindcss": "^3.0.24" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /pages/_app.js: -------------------------------------------------------------------------------- 1 | import "../styles/globals.css" 2 | import { MoralisProvider } from "react-moralis" 3 | import Header from "../components/Header" 4 | import Head from "next/head" 5 | import { NotificationProvider } from "web3uikit" 6 | import { ApolloProvider, ApolloClient, InMemoryCache } from "@apollo/client" 7 | 8 | const client = new ApolloClient({ 9 | cache: new InMemoryCache(), 10 | uri: process.env.NEXT_PUBLIC_SUBGRAPH_URL, 11 | }) 12 | 13 | function MyApp({ Component, pageProps }) { 14 | return ( 15 |
16 | 17 | NFT Marketplace 18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 |
30 | ) 31 | } 32 | 33 | export default MyApp 34 | -------------------------------------------------------------------------------- /pages/api/hello.js: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | 3 | export default function handler(req, res) { 4 | res.status(200).json({ name: 'John Doe' }) 5 | } 6 | -------------------------------------------------------------------------------- /pages/graphExample.js: -------------------------------------------------------------------------------- 1 | import { useQuery, gql } from "@apollo/client" 2 | 3 | const GET_ACTIVE_ITEMS = gql` 4 | { 5 | activeItems(first: 5, where: { buyer: "0x0000000000000000000000000000000000000000" }) { 6 | id 7 | buyer 8 | seller 9 | nftAddress 10 | tokenId 11 | price 12 | } 13 | } 14 | ` 15 | 16 | export default function GraphExample() { 17 | const { loading, error, data } = useQuery(GET_ACTIVE_ITEMS) 18 | console.log(data) 19 | return
hi
20 | } 21 | -------------------------------------------------------------------------------- /pages/index.js: -------------------------------------------------------------------------------- 1 | import styles from "../styles/Home.module.css" 2 | import { useMoralis } from "react-moralis" 3 | import NFTBox from "../components/NFTBox" 4 | import networkMapping from "../constants/networkMapping.json" 5 | import GET_ACTIVE_ITEMS from "../constants/subgraphQueries" 6 | import { useQuery } from "@apollo/client" 7 | 8 | export default function Home() { 9 | const { chainId, isWeb3Enabled } = useMoralis() 10 | const chainString = chainId ? parseInt(chainId).toString() : null 11 | const marketplaceAddress = chainId ? networkMapping[chainString].NftMarketplace[0] : null 12 | 13 | const { loading, error, data: listedNfts } = useQuery(GET_ACTIVE_ITEMS) 14 | 15 | return ( 16 |
17 |

Recently Listed

18 |
19 | {isWeb3Enabled && chainId ? ( 20 | loading || !listedNfts ? ( 21 |
Loading...
22 | ) : ( 23 | listedNfts.activeItems.map((nft) => { 24 | const { price, nftAddress, tokenId, seller } = nft 25 | return marketplaceAddress ? ( 26 | 34 | ) : ( 35 |
Network error, please switch to a supported network.
36 | ) 37 | }) 38 | ) 39 | ) : ( 40 |
Web3 Currently Not Enabled
41 | )} 42 |
43 |
44 | ) 45 | } 46 | -------------------------------------------------------------------------------- /pages/sell-nft.js: -------------------------------------------------------------------------------- 1 | import Head from "next/head" 2 | import Image from "next/image" 3 | import styles from "../styles/Home.module.css" 4 | import { Form, useNotification, Button } from "web3uikit" 5 | import { useMoralis, useWeb3Contract } from "react-moralis" 6 | import { ethers } from "ethers" 7 | import nftAbi from "../constants/BasicNft.json" 8 | import nftMarketplaceAbi from "../constants/NftMarketplace.json" 9 | import networkMapping from "../constants/networkMapping.json" 10 | import { useEffect, useState } from "react" 11 | 12 | export default function Home() { 13 | const { chainId, account, isWeb3Enabled } = useMoralis() 14 | const chainString = chainId ? parseInt(chainId).toString() : "31337" 15 | const marketplaceAddress = networkMapping[chainString].NftMarketplace[0] 16 | const dispatch = useNotification() 17 | const [proceeds, setProceeds] = useState("0") 18 | 19 | const { runContractFunction } = useWeb3Contract() 20 | 21 | async function approveAndList(data) { 22 | console.log("Approving...") 23 | const nftAddress = data.data[0].inputResult 24 | const tokenId = data.data[1].inputResult 25 | const price = ethers.utils.parseUnits(data.data[2].inputResult, "ether").toString() 26 | 27 | const approveOptions = { 28 | abi: nftAbi, 29 | contractAddress: nftAddress, 30 | functionName: "approve", 31 | params: { 32 | to: marketplaceAddress, 33 | tokenId: tokenId, 34 | }, 35 | } 36 | 37 | await runContractFunction({ 38 | params: approveOptions, 39 | onSuccess: (tx) => handleApproveSuccess(tx, nftAddress, tokenId, price), 40 | onError: (error) => { 41 | console.log(error) 42 | }, 43 | }) 44 | } 45 | 46 | async function handleApproveSuccess(tx, nftAddress, tokenId, price) { 47 | console.log("Ok! Now time to list") 48 | await tx.wait() 49 | const listOptions = { 50 | abi: nftMarketplaceAbi, 51 | contractAddress: marketplaceAddress, 52 | functionName: "listItem", 53 | params: { 54 | nftAddress: nftAddress, 55 | tokenId: tokenId, 56 | price: price, 57 | }, 58 | } 59 | 60 | await runContractFunction({ 61 | params: listOptions, 62 | onSuccess: () => handleListSuccess(), 63 | onError: (error) => console.log(error), 64 | }) 65 | } 66 | 67 | async function handleListSuccess() { 68 | dispatch({ 69 | type: "success", 70 | message: "NFT listing", 71 | title: "NFT listed", 72 | position: "topR", 73 | }) 74 | } 75 | 76 | const handleWithdrawSuccess = () => { 77 | dispatch({ 78 | type: "success", 79 | message: "Withdrawing proceeds", 80 | position: "topR", 81 | }) 82 | } 83 | 84 | async function setupUI() { 85 | const returnedProceeds = await runContractFunction({ 86 | params: { 87 | abi: nftMarketplaceAbi, 88 | contractAddress: marketplaceAddress, 89 | functionName: "getProceeds", 90 | params: { 91 | seller: account, 92 | }, 93 | }, 94 | onError: (error) => console.log(error), 95 | }) 96 | if (returnedProceeds) { 97 | setProceeds(returnedProceeds.toString()) 98 | } 99 | } 100 | 101 | useEffect(() => { 102 | setupUI() 103 | }, [proceeds, account, isWeb3Enabled, chainId]) 104 | 105 | return ( 106 |
107 |
133 |
Withdraw {proceeds} proceeds
134 | {proceeds != "0" ? ( 135 |
155 | ) 156 | } 157 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PatrickAlphaC/nextjs-nft-marketplace-thegraph-fcc/027a6fbe95f26718ccc7cd409afc41b9c1b7c9c0/public/favicon.ico -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /styles/Home.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | padding: 0 2rem; 3 | } 4 | 5 | .main { 6 | min-height: 100vh; 7 | padding: 4rem 0; 8 | flex: 1; 9 | display: flex; 10 | flex-direction: column; 11 | justify-content: center; 12 | align-items: center; 13 | } 14 | 15 | .footer { 16 | display: flex; 17 | flex: 1; 18 | padding: 2rem 0; 19 | border-top: 1px solid #eaeaea; 20 | justify-content: center; 21 | align-items: center; 22 | } 23 | 24 | .footer a { 25 | display: flex; 26 | justify-content: center; 27 | align-items: center; 28 | flex-grow: 1; 29 | } 30 | 31 | .title a { 32 | color: #0070f3; 33 | text-decoration: none; 34 | } 35 | 36 | .title a:hover, 37 | .title a:focus, 38 | .title a:active { 39 | text-decoration: underline; 40 | } 41 | 42 | .title { 43 | margin: 0; 44 | line-height: 1.15; 45 | font-size: 4rem; 46 | } 47 | 48 | .title, 49 | .description { 50 | text-align: center; 51 | } 52 | 53 | .description { 54 | margin: 4rem 0; 55 | line-height: 1.5; 56 | font-size: 1.5rem; 57 | } 58 | 59 | .code { 60 | background: #fafafa; 61 | border-radius: 5px; 62 | padding: 0.75rem; 63 | font-size: 1.1rem; 64 | font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, 65 | Bitstream Vera Sans Mono, Courier New, monospace; 66 | } 67 | 68 | .grid { 69 | display: flex; 70 | align-items: center; 71 | justify-content: center; 72 | flex-wrap: wrap; 73 | max-width: 800px; 74 | } 75 | 76 | .card { 77 | margin: 1rem; 78 | padding: 1.5rem; 79 | text-align: left; 80 | color: inherit; 81 | text-decoration: none; 82 | border: 1px solid #eaeaea; 83 | border-radius: 10px; 84 | transition: color 0.15s ease, border-color 0.15s ease; 85 | max-width: 300px; 86 | } 87 | 88 | .card:hover, 89 | .card:focus, 90 | .card:active { 91 | color: #0070f3; 92 | border-color: #0070f3; 93 | } 94 | 95 | .card h2 { 96 | margin: 0 0 1rem 0; 97 | font-size: 1.5rem; 98 | } 99 | 100 | .card p { 101 | margin: 0; 102 | font-size: 1.25rem; 103 | line-height: 1.5; 104 | } 105 | 106 | .logo { 107 | height: 1em; 108 | margin-left: 0.5rem; 109 | } 110 | 111 | @media (max-width: 600px) { 112 | .grid { 113 | width: 100%; 114 | flex-direction: column; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: ["./pages/**/*.{js,ts,jsx,tsx}", "./components/**/*.{js,ts,jsx,tsx}"], 3 | theme: { 4 | extend: {}, 5 | }, 6 | plugins: [], 7 | } 8 | --------------------------------------------------------------------------------