├── .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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------