├── .env.example ├── .eslintrc.json ├── .gitignore ├── README.md ├── addresses.ts ├── components ├── Header.tsx └── ThirdwebGuideFooter.tsx ├── next-env.d.ts ├── next.config.js ├── package.json ├── pages ├── _app.tsx ├── create.tsx ├── index.tsx └── listing │ └── [listingId].tsx ├── public ├── favicon.ico ├── github.png ├── logo.png └── thirdweb.svg ├── styles ├── Home.module.css ├── Thirdweb.module.css └── globals.css └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_MARKETPLACE_CONTRACT_ADDRESS=0x... -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | 36 | # typescript 37 | *.tsbuildinfo 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Marketplace With Next.JS 2 | 3 | ## Introduction 4 | 5 | In this guide, you will learn how to create a marketplace like [OpenSea](https://opensea.io/) on the Goerli Ethereum test network! 6 | 7 | By the end, we'll implement the following features: 8 | 9 | - A marketplace where we can list NFTs for **direct sale** or for **auction**. 10 | - Allow users to **make bids** and **buy** our NFTs. 11 | 12 | **Check out the Demo here**: https://marketplace.thirdweb-example.com 13 | 14 | ## Tools 15 | 16 | - [**thirdweb Marketplace**](https://portal.thirdweb.com/contracts/marketplace): to facilitate the listing of NFTs and enable users to make buy, sell, or make offers on the NFTs on the marketplace. 17 | - [**thirdweb NFT Collection**](https://portal.thirdweb.com/contracts/nft-collection): to create an ERC721 NFT Collection that we can list onto the marketplace. 18 | - [**thirdweb React SDK**](https://docs.thirdweb.com/react): to enable users to connect and disconnect their wallets with our website, and access hooks such as [useContract](https://portal.thirdweb.com/react/react.usecontract) and [useActiveListings](https://portal.thirdweb.com/react/react.useactivelistings) to interact with the marketplace. 19 | - [**thirdweb TypeScript SDK**](https://docs.thirdweb.com/typescript): to connect to our marketplace smart contract, create new listings, make offers and buy listings! 20 | - [**Next JS Dynamic Routes**](https://nextjs.org/docs/routing/dynamic-routes): so we can have a dynamic route for each listing. eg. `listing/1` will show listing 1. 21 | 22 | ## Using This Repo 23 | 24 | Create a project using this example by running: 25 | 26 | ```bash 27 | npx thirdweb create --template marketplace 28 | ``` 29 | 30 | - Create your own Marketplace contract via the thirdweb dashboard. (Follow the steps in the guide below if you need extra help)! 31 | 32 | - Replace the marketplace contract address with yours in [addresses.ts](/addresses.ts) file 33 | 34 | Need More help? Want to understand the code a bit more? Want to set the project up yourself? Follow the guide below! 👇 35 | 36 | --- 37 | 38 | ## Creating A Marketplace 39 | 40 | To create a marketplace contract: 41 | 42 | - Head to the [thirdweb dashboard](https://thirdweb.com/dashboard). 43 | - Click **Create a new contract**. 44 | - Click **Setup Marketplace**. 45 | - Configure & Deploy! 46 | 47 | ## The Thirdweb Provider 48 | 49 | The thirdweb React provider makes it straightforward to let your users connect their wallets to your website, and it abstracts away all the boilerplate you would usually have to write. 50 | 51 | Open `pages/_app.tsx` we wrap all of our pages in the `` component. 52 | 53 | ```tsx 54 | 55 | 56 | 57 | ``` 58 | 59 | ## Signing Users In With Their Wallets 60 | 61 | We connect user's wallets to our website by using the thirdweb React SDK's [useMetamask](https://docs.thirdweb.com/react/react.usemetamask) hook. 62 | 63 | ```ts 64 | const connectWithMetamask = useMetamask(); 65 | ``` 66 | 67 | ## Displaying Listings On The Marketplace 68 | 69 | On the [index.tsx file](./pages/index.tsx), we're displaying all of the current **active** listings on the marketplace. 70 | 71 | We're using React's `useState` to store the listings as well as a loading flag. 72 | 73 | ```ts 74 | // Loading Flag 75 | const [loadingListings, setLoadingListings] = useState(true); 76 | // Store Listings 77 | const [listings, setListings] = useState<(AuctionListing | DirectListing)[]>( 78 | [] 79 | ); 80 | ``` 81 | 82 | Then, we use the [useContract](https://docs.thirdweb.com/react/react.useContract) hook to connect to our smart contract via it's contract address. 83 | 84 | ```ts 85 | const { contract: marketplace } = useContract( 86 | "your-marketplace-address-here", 87 | "marketplace" 88 | ); 89 | ``` 90 | 91 | Once the marketplace is ready, we can use the `useActiveListings` hook to get all of the listings that are currently active (i.e. haven't expired or sold already). 92 | 93 | ```tsx 94 | const { data: listings, isLoading: loadingListings } = 95 | useActiveListings(marketplace); 96 | ``` 97 | 98 | Once we have the listings, we can display them to our users. 99 | 100 | We'll leave the details of how best to display the listings up to you, but if you're looking for an example, check out the code in our [index.tsx file](./pages/index.tsx) file. 101 | 102 | ## Listing Items on the marketplace 103 | 104 | We have a page called [create.tsx](./pages/create.tsx) that lets users upload existing NFTs onto the marketplace. 105 | 106 | If you don't have NFTs that you can list, [you can create an NFT Collection via our dashboard](https://thirdweb.com/dashboard). 107 | 108 | Once again, we are using the `useContract` hook to connect to our marketplace smart contract via it's contract address. 109 | 110 | ```ts 111 | const { contract: marketplace } = useContract( 112 | "your-marketplace-address-here", 113 | "marketplace" 114 | ); 115 | ``` 116 | 117 | **Create Auction Type Listing:** 118 | 119 | ```ts 120 | async function createAuctionListing( 121 | contractAddress: string, 122 | tokenId: string, 123 | price: string 124 | ) { 125 | try { 126 | const transaction = await marketplace?.auction.createListing({ 127 | assetContractAddress: contractAddress, // Contract Address of the NFT 128 | buyoutPricePerToken: price, // Maximum price, the auction will end immediately if a user pays this price. 129 | currencyContractAddress: NATIVE_TOKEN_ADDRESS, // NATIVE_TOKEN_ADDRESS is the cryptocurency that is native to the network. i.e. Goerli Ether 130 | listingDurationInSeconds: 60 * 60 * 24 * 7, // When the auction will be closed and no longer accept bids (1 Week) 131 | quantity: 1, // How many of the NFTs are being listed (useful for ERC 1155 tokens) 132 | reservePricePerToken: 0, // Minimum price, users cannot bid below this amount 133 | startTimestamp: new Date(), // When the listing will start (now) 134 | tokenId: tokenId, // Token ID of the NFT. 135 | }); 136 | 137 | return transaction; 138 | } catch (error) { 139 | console.error(error); 140 | } 141 | } 142 | ``` 143 | 144 | **Create Direct Type Listing** 145 | 146 | ```ts 147 | async function createDirectListing( 148 | contractAddress: string, 149 | tokenId: string, 150 | price: string 151 | ) { 152 | try { 153 | const transaction = await marketplace?.direct.createListing({ 154 | assetContractAddress: contractAddress, // Contract Address of the NFT 155 | buyoutPricePerToken: price, // Maximum price, the auction will end immediately if a user pays this price. 156 | currencyContractAddress: NATIVE_TOKEN_ADDRESS, // NATIVE_TOKEN_ADDRESS is the cryptocurency that is native to the network. i.e. Goerli Ether. 157 | listingDurationInSeconds: 60 * 60 * 24 * 7, // When the auction will be closed and no longer accept bids (1 Week) 158 | quantity: 1, // How many of the NFTs are being listed (useful for ERC 1155 tokens) 159 | startTimestamp: new Date(0), // When the listing will start (now) 160 | tokenId: tokenId, // Token ID of the NFT. 161 | }); 162 | 163 | return transaction; 164 | } catch (error) { 165 | console.error(error); 166 | } 167 | } 168 | ``` 169 | 170 | When you go to list your NFT, you'll be asked for two transactions: 171 | 172 | 1. Approve the marketplace to sell your NFTs while the NFT still lives in your wallet. (`setApprovalForAll`) 173 | 2. Create the listing on the marketplace (`createListing`) 174 | 175 | If everything worked as planned, after you approve these two transactions, you should now see the listing you just created on the home page! 176 | 177 | ## Viewing A Listing 178 | 179 | On the home page, we provide a `Link` on each listing's name to a URL that looks like: `/listing/${listing.id}`. This is using [Next JS's Dynamic Routes](https://nextjs.org/docs/routing/dynamic-routes). 180 | 181 | This way, each NFT navigates the user to a page that shows the details of the listing when they click on it, by taking them to the `/listing/[listingId]` page. 182 | 183 | When the user visits the `/listing/[listingId]` page, we can fetch the information about the listing the user is looking at! E.g if the user visits `/listing/1`, we call `marketplace.getListing(1)` and load that listings information! 184 | 185 | **Fetching The Listing** 186 | 187 | ```ts 188 | const { contract: marketplace } = useContract( 189 | "your-marketplace-address-here", 190 | "marketplace" 191 | ); 192 | 193 | useEffect(() => { 194 | if (!listingId || !marketplace) { 195 | return; 196 | } 197 | (async () => { 198 | // Use the listingId from the router.query to get the listing the user is looking at. 199 | const l = await marketplace.getListing(listingId); 200 | 201 | setLoadingListing(false); 202 | setListing(l); 203 | })(); 204 | }, [listingId, marketplace]); 205 | ``` 206 | 207 | On the `/listing/[listingId]` page, we'll want users to also be able to place bids/offers on the listing, and also buy the listing! 208 | 209 | **Creating A Bid / Offer** 210 | 211 | ```ts 212 | async function createBidOrOffer() { 213 | try { 214 | // If the listing type is a direct listing, then we can create an offer. 215 | if (listing?.type === ListingType.Direct) { 216 | await marketplace?.direct.makeOffer( 217 | listingId, // The listingId of the listing we want to make an offer for 218 | 1, // Quantity = 1 219 | NATIVE_TOKENS[ChainId.Goerli].wrapped.address, // Wrapped Ether address on Goerli 220 | bidAmount // The offer amount the user entered 221 | ); 222 | } 223 | 224 | // If the listing type is an auction listing, then we can create a bid. 225 | if (listing?.type === ListingType.Auction) { 226 | await marketplace?.auction.makeBid(listingId, bidAmount); 227 | } 228 | } catch (error) { 229 | console.error(error); 230 | } 231 | } 232 | ``` 233 | 234 | **Buying the NFT** 235 | 236 | ```ts 237 | async function buyNft() { 238 | try { 239 | // Simple one-liner for buying the NFT 240 | await marketplace?.buyoutListing(listingId, 1); 241 | } catch (error) { 242 | console.error(error); 243 | } 244 | } 245 | ``` 246 | 247 | We attach these functions to the `onClick` handlers of our `Buy` and `Make Offer` buttons. If you want to see how we do that, check out the code in our [[listingId].tsx file](./pages/listing/[listingId].tsx) page. 248 | 249 | **Note:** For making offers, you'll need to have an ERC20 token. For our Goerli marketplace, that means you'll need to have wrapped ETH (wETH). 250 | 251 | ## Join our Discord! 252 | 253 | For any questions, suggestions, join our discord at [https://discord.gg/thirdweb](https://discord.gg/thirdweb). 254 | -------------------------------------------------------------------------------- /addresses.ts: -------------------------------------------------------------------------------- 1 | // Use your marketplace contract address here 2 | export const marketplaceContractAddress = '0xf7ba7cd986d5bC960433697Ca8cF9e7101F3c017'; 3 | -------------------------------------------------------------------------------- /components/Header.tsx: -------------------------------------------------------------------------------- 1 | import { useAddress, useMetamask, useDisconnect } from "@thirdweb-dev/react"; 2 | import Link from "next/link"; 3 | import React from "react"; 4 | import styles from "../styles/Home.module.css"; 5 | 6 | export default function Header() { 7 | // Helpful thirdweb hooks to connect and manage the wallet from metamask. 8 | const address = useAddress(); 9 | const connectWithMetamask = useMetamask(); 10 | const disconnectWallet = useDisconnect(); 11 | 12 | return ( 13 |
14 |
15 |
16 | 17 | Thirdweb Logo 23 | 24 |
25 |
26 |
27 | {address ? ( 28 | <> 29 | disconnectWallet()} 32 | > 33 | Disconnect Wallet 34 | 35 |

|

36 |

{address.slice(0, 6).concat("...").concat(address.slice(-4))}

37 | 38 | ) : ( 39 | connectWithMetamask()} 42 | > 43 | Connect Wallet 44 | 45 | )} 46 |
47 |
48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /components/ThirdwebGuideFooter.tsx: -------------------------------------------------------------------------------- 1 | import styles from "../../styles/Thirdweb.module.css"; 2 | import React from "react"; 3 | 4 | export default function ThirdwebGuideFooter() { 5 | const url = "https://github.com/thirdweb-example/marketplace-next-ts"; 6 | return ( 7 | <> 8 |
window.open(url, "_blank")} 22 | /> 23 | 24 |
31 | github url window.open(url, "_blank")} 39 | /> 40 |
41 | 42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | } 5 | 6 | module.exports = nextConfig 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-marketplace", 3 | "private": true, 4 | "scripts": { 5 | "dev": "next dev", 6 | "build": "next build", 7 | "start": "next start", 8 | "lint": "next lint" 9 | }, 10 | "dependencies": { 11 | "@thirdweb-dev/react": "^3", 12 | "@thirdweb-dev/sdk": "^3", 13 | "ethers": "^5.6.4", 14 | "next": "^13", 15 | "react": "^18.2.0", 16 | "react-dom": "^18.2.0" 17 | }, 18 | "devDependencies": { 19 | "@types/node": "^18.11.12", 20 | "@types/react": "^18.0.26", 21 | "eslint": "^8.29.0", 22 | "eslint-config-next": "^13", 23 | "typescript": "^4.9.2" 24 | }, 25 | "engines": { 26 | "node": ">=16.0.0" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import { ThirdwebProvider } from "@thirdweb-dev/react"; 2 | import type { AppProps } from "next/app"; 3 | import Head from "next/head"; 4 | import Header from "../components/Header"; 5 | import ThirdwebGuideFooter from "../components/ThirdwebGuideFooter"; 6 | import "../styles/globals.css"; 7 | 8 | // This is the chain your dApp will work on. 9 | const activeChain = "goerli"; 10 | 11 | function MyApp({ Component, pageProps }: AppProps) { 12 | return ( 13 | 14 | 15 | thirdweb Marketplace with Next.JS 16 | 17 | 21 | 25 | 26 |
27 | 28 | 29 | 30 | ); 31 | } 32 | 33 | export default MyApp; 34 | -------------------------------------------------------------------------------- /pages/create.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | useContract, 3 | useNetwork, 4 | useNetworkMismatch, 5 | } from "@thirdweb-dev/react"; 6 | import { 7 | ChainId, 8 | NATIVE_TOKEN_ADDRESS, 9 | TransactionResult, 10 | } from "@thirdweb-dev/sdk"; 11 | import type { NextPage } from "next"; 12 | import { useRouter } from "next/router"; 13 | import { marketplaceContractAddress } from "../addresses"; 14 | import styles from "../styles/Home.module.css"; 15 | 16 | const Create: NextPage = () => { 17 | // Next JS Router hook to redirect to other pages 18 | const router = useRouter(); 19 | const networkMismatch = useNetworkMismatch(); 20 | const [, switchNetwork] = useNetwork(); 21 | 22 | // Connect to our marketplace contract via the useContract hook 23 | const { contract: marketplace } = useContract(marketplaceContractAddress, "marketplace"); 24 | 25 | // This function gets called when the form is submitted. 26 | async function handleCreateListing(e: any) { 27 | try { 28 | // Ensure user is on the correct network 29 | if (networkMismatch) { 30 | switchNetwork && switchNetwork(ChainId.Goerli); 31 | return; 32 | } 33 | 34 | // Prevent page from refreshing 35 | e.preventDefault(); 36 | 37 | // Store the result of either the direct listing creation or the auction listing creation 38 | let transactionResult: undefined | TransactionResult = undefined; 39 | 40 | // De-construct data from form submission 41 | const { listingType, contractAddress, tokenId, price } = 42 | e.target.elements; 43 | 44 | // Depending on the type of listing selected, call the appropriate function 45 | // For Direct Listings: 46 | if (listingType.value === "directListing") { 47 | transactionResult = await createDirectListing( 48 | contractAddress.value, 49 | tokenId.value, 50 | price.value 51 | ); 52 | } 53 | 54 | // For Auction Listings: 55 | if (listingType.value === "auctionListing") { 56 | transactionResult = await createAuctionListing( 57 | contractAddress.value, 58 | tokenId.value, 59 | price.value 60 | ); 61 | } 62 | 63 | // If the transaction succeeds, take the user back to the homepage to view their listing! 64 | if (transactionResult) { 65 | router.push(`/`); 66 | } 67 | } catch (error) { 68 | console.error(error); 69 | } 70 | } 71 | 72 | async function createAuctionListing( 73 | contractAddress: string, 74 | tokenId: string, 75 | price: string 76 | ) { 77 | try { 78 | const transaction = await marketplace?.auction.createListing({ 79 | assetContractAddress: contractAddress, // Contract Address of the NFT 80 | buyoutPricePerToken: price, // Maximum price, the auction will end immediately if a user pays this price. 81 | currencyContractAddress: NATIVE_TOKEN_ADDRESS, // NATIVE_TOKEN_ADDRESS is the crpyto curency that is native to the network. i.e. Goerli ETH. 82 | listingDurationInSeconds: 60 * 60 * 24 * 7, // When the auction will be closed and no longer accept bids (1 Week) 83 | quantity: 1, // How many of the NFTs are being listed (useful for ERC 1155 tokens) 84 | reservePricePerToken: 0, // Minimum price, users cannot bid below this amount 85 | startTimestamp: new Date(), // When the listing will start 86 | tokenId: tokenId, // Token ID of the NFT. 87 | }); 88 | 89 | return transaction; 90 | } catch (error) { 91 | console.error(error); 92 | } 93 | } 94 | 95 | async function createDirectListing( 96 | contractAddress: string, 97 | tokenId: string, 98 | price: string 99 | ) { 100 | try { 101 | const transaction = await marketplace?.direct.createListing({ 102 | assetContractAddress: contractAddress, // Contract Address of the NFT 103 | buyoutPricePerToken: price, // Maximum price, the auction will end immediately if a user pays this price. 104 | currencyContractAddress: NATIVE_TOKEN_ADDRESS, // NATIVE_TOKEN_ADDRESS is the crpyto curency that is native to the network. i.e. Goerli ETH. 105 | listingDurationInSeconds: 60 * 60 * 24 * 7, // When the auction will be closed and no longer accept bids (1 Week) 106 | quantity: 1, // How many of the NFTs are being listed (useful for ERC 1155 tokens) 107 | startTimestamp: new Date(0), // When the listing will start 108 | tokenId: tokenId, // Token ID of the NFT. 109 | }); 110 | 111 | return transaction; 112 | } catch (error) { 113 | console.error(error); 114 | } 115 | } 116 | 117 | return ( 118 |
handleCreateListing(e)}> 119 |
120 | {/* Form Section */} 121 |
122 |

123 | Upload your NFT to the marketplace: 124 |

125 | 126 | {/* Toggle between direct listing and auction listing */} 127 |
128 | 136 | 139 | 146 | 149 |
150 | 151 | {/* NFT Contract Address Field */} 152 | 158 | 159 | {/* NFT Token ID Field */} 160 | 166 | 167 | {/* Sale Price For Listing Field */} 168 | 174 | 175 | 182 |
183 |
184 |
185 | ); 186 | }; 187 | 188 | export default Create; 189 | -------------------------------------------------------------------------------- /pages/index.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from "next"; 2 | import styles from "../styles/Home.module.css"; 3 | import Link from "next/link"; 4 | import { 5 | MediaRenderer, 6 | useActiveListings, 7 | useContract, 8 | } from "@thirdweb-dev/react"; 9 | import { useRouter } from "next/router"; 10 | import { marketplaceContractAddress } from "../addresses"; 11 | 12 | const Home: NextPage = () => { 13 | const router = useRouter(); 14 | const { contract: marketplace } = useContract(marketplaceContractAddress, "marketplace"); 15 | const { data: listings, isLoading: loadingListings } = useActiveListings(marketplace); 16 | 17 | return ( 18 | <> 19 | {/* Content */} 20 |
21 | {/* Top Section */} 22 |

NFT Marketplace w/ thirdweb + Next.JS

23 |

24 | Build an NFT marketplace using{" "} 25 | 26 | {" "} 27 | 33 | thirdweb 34 | 35 | {" "} 36 | to list your ERC721 and ERC1155 tokens for auction or for direct sale. 37 |

38 | 39 |
40 | 41 |
42 | 43 | Create A Listing 44 | 45 |
46 | 47 |
48 | { 49 | // If the listings are loading, show a loading message 50 | loadingListings ? ( 51 |
Loading listings...
52 | ) : ( 53 | // Otherwise, show the listings 54 |
55 | {listings?.map((listing) => ( 56 |
router.push(`/listing/${listing.id}`)} 60 | > 61 | 70 |

71 | 72 | {listing.asset.name} 73 | 74 |

75 | 76 |

77 | {listing.buyoutCurrencyValuePerToken.displayValue}{" "} 78 | {listing.buyoutCurrencyValuePerToken.symbol} 79 |

80 |
81 | ))} 82 |
83 | ) 84 | } 85 |
86 |
87 | 88 | ); 89 | }; 90 | 91 | export default Home; 92 | -------------------------------------------------------------------------------- /pages/listing/[listingId].tsx: -------------------------------------------------------------------------------- 1 | import { 2 | MediaRenderer, 3 | useNetwork, 4 | useNetworkMismatch, 5 | useListing, 6 | useContract, 7 | } from "@thirdweb-dev/react"; 8 | import { 9 | ChainId, 10 | ListingType, 11 | Marketplace, 12 | NATIVE_TOKENS, 13 | } from "@thirdweb-dev/sdk"; 14 | import type { NextPage } from "next"; 15 | import { useRouter } from "next/router"; 16 | import { useState } from "react"; 17 | import { marketplaceContractAddress } from "../../addresses"; 18 | import styles from "../../styles/Home.module.css"; 19 | 20 | const ListingPage: NextPage = () => { 21 | // Next JS Router hook to redirect to other pages and to grab the query from the URL (listingId) 22 | const router = useRouter(); 23 | 24 | // De-construct listingId out of the router.query. 25 | // This means that if the user visits /listing/0 then the listingId will be 0. 26 | // If the user visits /listing/1 then the listingId will be 1. 27 | const { listingId } = router.query as { listingId: string }; 28 | 29 | // Hooks to detect user is on the right network and switch them if they are not 30 | const networkMismatch = useNetworkMismatch(); 31 | const [, switchNetwork] = useNetwork(); 32 | 33 | // Initialize the marketplace contract 34 | const { contract: marketplace } = useContract(marketplaceContractAddress, "marketplace"); 35 | 36 | // Fetch the listing from the marketplace contract 37 | const { data: listing, isLoading: loadingListing } = useListing( 38 | marketplace, 39 | listingId 40 | ); 41 | 42 | // Store the bid amount the user entered into the bidding textbox 43 | const [bidAmount, setBidAmount] = useState(""); 44 | 45 | if (loadingListing) { 46 | return
Loading...
; 47 | } 48 | 49 | if (!listing) { 50 | return
Listing not found
; 51 | } 52 | 53 | async function createBidOrOffer() { 54 | try { 55 | // Ensure user is on the correct network 56 | if (networkMismatch) { 57 | switchNetwork && switchNetwork(ChainId.Goerli); 58 | return; 59 | } 60 | 61 | // If the listing type is a direct listing, then we can create an offer. 62 | if (listing?.type === ListingType.Direct) { 63 | await marketplace?.direct.makeOffer( 64 | listingId, // The listingId of the listing we want to make an offer for 65 | 1, // Quantity = 1 66 | NATIVE_TOKENS[ChainId.Goerli].wrapped.address, // Wrapped Ether address on Goerli 67 | bidAmount // The offer amount the user entered 68 | ); 69 | } 70 | 71 | // If the listing type is an auction listing, then we can create a bid. 72 | if (listing?.type === ListingType.Auction) { 73 | await marketplace?.auction.makeBid(listingId, bidAmount); 74 | } 75 | 76 | alert( 77 | `${ 78 | listing?.type === ListingType.Auction ? "Bid" : "Offer" 79 | } created successfully!` 80 | ); 81 | } catch (error) { 82 | console.error(error); 83 | alert(error); 84 | } 85 | } 86 | 87 | async function buyNft() { 88 | try { 89 | // Ensure user is on the correct network 90 | if (networkMismatch) { 91 | switchNetwork && switchNetwork(ChainId.Goerli); 92 | return; 93 | } 94 | 95 | // Simple one-liner for buying the NFT 96 | await marketplace?.buyoutListing(listingId, 1); 97 | alert("NFT bought successfully!"); 98 | } catch (error) { 99 | console.error(error); 100 | alert(error); 101 | } 102 | } 103 | 104 | return ( 105 |
106 |
107 |
108 | 112 |
113 | 114 |
115 |

{listing.asset.name}

116 |

117 | Owned by{" "} 118 | 119 | {listing.sellerAddress?.slice(0, 6) + 120 | "..." + 121 | listing.sellerAddress?.slice(36, 40)} 122 | 123 |

124 | 125 |

126 | {listing.buyoutCurrencyValuePerToken.displayValue}{" "} 127 | {listing.buyoutCurrencyValuePerToken.symbol} 128 |

129 | 130 |
138 | 145 |

|

146 |
154 | setBidAmount(e.target.value)} 159 | placeholder="Amount" 160 | style={{ marginTop: 0, marginLeft: 0, width: 128 }} 161 | /> 162 | 173 |
174 |
175 |
176 |
177 |
178 | ); 179 | }; 180 | 181 | export default ListingPage; 182 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thirdweb-example/marketplace/54ce0594df2fcf7392934ace8aa6217a2a241eba/public/favicon.ico -------------------------------------------------------------------------------- /public/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thirdweb-example/marketplace/54ce0594df2fcf7392934ace8aa6217a2a241eba/public/github.png -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thirdweb-example/marketplace/54ce0594df2fcf7392934ace8aa6217a2a241eba/public/logo.png -------------------------------------------------------------------------------- /public/thirdweb.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /styles/Home.module.css: -------------------------------------------------------------------------------- 1 | .connect { 2 | position: fixed; 3 | top: 5%; 4 | left: 5%; 5 | } 6 | 7 | .btn { 8 | background-color: var(--tw-color1); 9 | outline: none; 10 | border: none; 11 | color: var(--white); 12 | height: 48px; 13 | padding: 0 24px; 14 | border-radius: 999px; 15 | font-size: 1.25rem; 16 | font-weight: 600; 17 | cursor: pointer; 18 | 19 | transition: background-color 300ms ease-in-out; 20 | transform-origin: center; 21 | } 22 | 23 | .btn:hover { 24 | background-color: var(--tw-color1-hover); 25 | } 26 | 27 | .btn:active { 28 | transform: scale(0.95); 29 | } 30 | 31 | .address { 32 | font-size: 1.5rem; 33 | margin-bottom: 16px; 34 | } 35 | 36 | .container { 37 | margin-top: 96px; 38 | display: flex; 39 | flex-direction: column; 40 | align-items: center; 41 | justify-content: center; 42 | width: 100vw; 43 | } 44 | 45 | .page { 46 | width: inherit; 47 | border-radius: 20px; 48 | border: 1px solid var(--tw-color1); 49 | padding: 16px; 50 | } 51 | 52 | .pageContainer { 53 | max-width: 600px; 54 | width: 90vw; 55 | display: flex; 56 | flex-direction: row; 57 | align-items: center; 58 | flex-direction: row; 59 | gap: 2%; 60 | padding: 1rem; 61 | } 62 | 63 | .arrowButton { 64 | border-radius: 50%; 65 | width: 30px; 66 | } 67 | 68 | .owner { 69 | font-size: 1rem; 70 | } 71 | 72 | .btnContainer { 73 | display: flex; 74 | flex-direction: column; 75 | align-items: center; 76 | justify-content: center; 77 | width: 100%; 78 | } 79 | 80 | .header { 81 | background-color: #262936; 82 | position: fixed; 83 | top: 0; 84 | left: 0; 85 | width: 100vw; 86 | height: 96px; 87 | margin-bottom: 96px; 88 | display: flex; 89 | flex-direction: row; 90 | align-items: center; 91 | justify-content: space-between; 92 | } 93 | 94 | .left { 95 | height: 100%; 96 | display: flex; 97 | align-items: center; 98 | justify-content: center; 99 | flex-direction: column; 100 | margin-left: 5%; 101 | } 102 | 103 | .right { 104 | height: 100%; 105 | display: flex; 106 | align-items: center; 107 | justify-content: center; 108 | flex-direction: row; 109 | margin-right: 5%; 110 | } 111 | 112 | .secondaryButton { 113 | cursor: pointer; 114 | text-align: center; 115 | padding: 5px; 116 | color: #fff; 117 | font-weight: 600; 118 | } 119 | 120 | .mainButton { 121 | cursor: pointer; 122 | display: inline-flex; 123 | appearance: none; 124 | align-items: center; 125 | -webkit-box-align: center; 126 | justify-content: center; 127 | white-space: nowrap; 128 | vertical-align: middle; 129 | outline: 2px solid transparent; 130 | outline-offset: 2px; 131 | line-height: 1.2; 132 | font-weight: 600; 133 | transition-property: background-color, border-color, color, fill, stroke, 134 | opacity, box-shadow, transform; 135 | transition-duration: 200ms; 136 | height: 3rem; 137 | min-width: 3rem; 138 | font-size: 1rem; 139 | background: #e5e5ea; 140 | background-image: linear-gradient(to left, #cc25b3 0%, #418dff 101.52%); 141 | color: #fff; 142 | width: 180px; 143 | text-align: center; 144 | border-radius: 9999px; 145 | } 146 | 147 | .mainButton:hover { 148 | opacity: 0.8; 149 | } 150 | 151 | .ourCollection { 152 | font-size: 1.5rem; 153 | margin-bottom: 16px; 154 | } 155 | 156 | .collectionContainer { 157 | width: 500px; 158 | max-width: 90vw; 159 | display: flex; 160 | flex-direction: column; 161 | align-items: center; 162 | margin-top: 2%; 163 | } 164 | 165 | .nftGrid { 166 | display: flex; 167 | flex-direction: column; 168 | align-items: center; 169 | justify-content: center; 170 | width: 100%; 171 | } 172 | 173 | .nftItem { 174 | display: flex; 175 | flex-direction: row; 176 | align-items: center; 177 | justify-content: space-between; 178 | width: 100%; 179 | border: 1px solid grey; 180 | border-radius: 16px; 181 | padding: 8px; 182 | margin-top: 12px; 183 | } 184 | 185 | .h1 { 186 | margin-bottom: 0px; 187 | } 188 | 189 | .explain { 190 | font-size: 1.125rem; 191 | } 192 | 193 | .nameContainer { 194 | margin-top: 0; 195 | margin-bottom: 0; 196 | } 197 | 198 | .name { 199 | font-size: 1.25rem; 200 | color: #fff; 201 | text-decoration: none; 202 | } 203 | 204 | .purple { 205 | color: #9f2c9d; 206 | } 207 | 208 | .divider { 209 | width: 50%; 210 | border-color: grey; 211 | opacity: 0.25; 212 | } 213 | 214 | .smallDivider { 215 | width: 25%; 216 | border-color: grey; 217 | margin-top: 64px; 218 | opacity: 0.25; 219 | } 220 | 221 | .textInput { 222 | width: 75%; 223 | background-color: transparent; 224 | border: 1px solid grey; 225 | border-radius: 8px; 226 | color: #fff; 227 | height: 48px; 228 | padding: 0 16px; 229 | font-size: 1rem; 230 | margin-top: 16px; 231 | } 232 | 233 | .imageInput { 234 | width: 100%; 235 | height: 100px; 236 | border: 1px dashed grey; 237 | border-radius: 16px; 238 | display: flex; 239 | align-items: center; 240 | justify-content: center; 241 | color: grey; 242 | cursor: pointer; 243 | } 244 | 245 | .listingGrid { 246 | display: flex; 247 | flex-direction: row; 248 | align-items: center; 249 | justify-content: flex-start; 250 | flex-wrap: wrap; 251 | width: 90vw; 252 | gap: 1%; 253 | } 254 | 255 | .listingShortView { 256 | border-radius: 20px; 257 | border: rgba(255, 255, 255, 0.25) 1px solid; 258 | width: 24%; 259 | margin-top: 8px; 260 | cursor: pointer; 261 | } 262 | 263 | /* On Mobile, make listingShortView 49% */ 264 | @media screen and (max-width: 800px) { 265 | .listingShortView { 266 | width: 49%; 267 | } 268 | } 269 | 270 | .listingTypeContainer { 271 | display: flex; 272 | flex-direction: row; 273 | align-items: center; 274 | justify-content: space-between; 275 | margin-bottom: 16px; 276 | gap: 12px; 277 | } 278 | 279 | .listingType { 280 | opacity: 0; 281 | width: 0; 282 | position: fixed; 283 | } 284 | 285 | .listingTypeLabel { 286 | display: inline-block; 287 | background-color: #262936; 288 | padding: 10px 20px; 289 | font-size: 16px; 290 | border: 2px solid #444; 291 | border-radius: 4px; 292 | border-radius: 16px; 293 | font-weight: 600; 294 | cursor: pointer; 295 | } 296 | 297 | .listingType:checked + label { 298 | border: 3px solid transparent; 299 | border-radius: 20px; 300 | background: linear-gradient(to right, #262936, #262936), 301 | linear-gradient(to right, #418dff, #cc25b3); 302 | background-clip: padding-box, border-box; 303 | background-origin: padding-box, border-box; 304 | } 305 | 306 | /* Listing Page */ 307 | 308 | .loadingOrError { 309 | height: 100vh; 310 | width: 100vw; 311 | display: flex; 312 | flex-direction: column; 313 | align-items: center; 314 | justify-content: center; 315 | } 316 | 317 | .listingContainer { 318 | display: flex; 319 | flex-direction: row; 320 | align-items: center; 321 | justify-content: space-between; 322 | width: 1200px; 323 | max-width: 90vw; 324 | gap: 20px; 325 | margin-top: 5%; 326 | height: 500px; 327 | } 328 | 329 | .leftListing { 330 | width: 40%; 331 | display: flex; 332 | flex-direction: column; 333 | align-items: center; 334 | justify-content: center; 335 | } 336 | 337 | .mainNftImage { 338 | width: 100%; 339 | height: 100%; 340 | max-height: 400px; 341 | max-width: 400px; 342 | border-radius: 16px; 343 | background-size: cover; 344 | background-position: center; 345 | background-repeat: no-repeat; 346 | } 347 | 348 | .rightListing { 349 | width: 55%; 350 | display: flex; 351 | flex-direction: column; 352 | align-items: flex-start; 353 | justify-content: center; 354 | } 355 | 356 | /* On Mobile, change .listingContainer to be stacked vertically */ 357 | @media screen and (max-width: 900px) { 358 | .listingContainer { 359 | flex-direction: column; 360 | align-items: center; 361 | } 362 | 363 | .leftListing { 364 | width: 100%; 365 | margin-bottom: 20px; 366 | } 367 | 368 | .rightListing { 369 | width: 100%; 370 | align-items: center; 371 | } 372 | } 373 | -------------------------------------------------------------------------------- /styles/Thirdweb.module.css: -------------------------------------------------------------------------------- 1 | .footerContainer { 2 | height: 120px; 3 | width: 100vw; 4 | background-color: #262936; 5 | position: fixed; 6 | bottom: 0; 7 | left: 0; 8 | z-index: 1; 9 | display: flex; 10 | justify-content: space-between; 11 | align-items: center; 12 | } 13 | 14 | .left { 15 | height: 100%; 16 | display: flex; 17 | align-items: center; 18 | justify-content: center; 19 | flex-direction: column; 20 | margin-left: 5%; 21 | } 22 | 23 | .right { 24 | height: 100%; 25 | display: flex; 26 | align-items: center; 27 | justify-content: center; 28 | flex-direction: row; 29 | margin-right: 5%; 30 | gap: 30px; 31 | } 32 | 33 | .secondaryButton { 34 | cursor: pointer; 35 | text-align: center; 36 | padding: 5px; 37 | color: #fff; 38 | font-weight: 600; 39 | } 40 | 41 | .mainButton { 42 | cursor: pointer; 43 | display: inline-flex; 44 | appearance: none; 45 | align-items: center; 46 | -webkit-box-align: center; 47 | justify-content: center; 48 | white-space: nowrap; 49 | vertical-align: middle; 50 | outline: 2px solid transparent; 51 | outline-offset: 2px; 52 | line-height: 1.2; 53 | font-weight: 600; 54 | transition-property: background-color, border-color, color, fill, stroke, 55 | opacity, box-shadow, transform; 56 | transition-duration: 200ms; 57 | height: 3rem; 58 | min-width: 3rem; 59 | font-size: 1rem; 60 | background: #e5e5ea; 61 | background-image: linear-gradient(to right, #cc25b3 0%, #418dff 101.52%); 62 | color: #fff; 63 | width: 180px; 64 | text-align: center; 65 | border-radius: 9999px; 66 | } 67 | 68 | .mainButton:hover { 69 | opacity: 0.8; 70 | } 71 | 72 | /* If mobile, stack right column */ 73 | @media screen and (max-width: 850px) { 74 | .right { 75 | flex-direction: column; 76 | gap: 10px; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap"); 2 | 3 | /* Box sizing rules */ 4 | *, 5 | *::before, 6 | *::after { 7 | box-sizing: border-box; 8 | } 9 | 10 | /* Set core body defaults */ 11 | body { 12 | min-height: 100vh; 13 | text-rendering: optimizeSpeed; 14 | line-height: 1.5; 15 | --hello: -100%; 16 | padding-bottom: 250px; 17 | } 18 | 19 | /* Inherit fonts for inputs and buttons */ 20 | input, 21 | button, 22 | textarea, 23 | select { 24 | font: inherit; 25 | } 26 | 27 | :root { 28 | --black: #1c1e21; 29 | --white: #ffffff; 30 | --tw-color1: #a855f7; 31 | --tw-color1-hover: #9333ea; 32 | } 33 | 34 | body { 35 | background: var(--black); 36 | font-family: "Inter", sans-serif; 37 | color: var(--white); 38 | display: flex; 39 | margin: 0; 40 | text-align: center; 41 | } 42 | 43 | h2 { 44 | font-size: 2rem; 45 | } 46 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true 17 | }, 18 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 19 | "exclude": ["node_modules"] 20 | } 21 | --------------------------------------------------------------------------------