├── .env.example ├── .eslintrc.json ├── .gitignore ├── .prettierignore ├── LICENSE.md ├── README.md ├── app ├── buy │ └── page.tsx ├── favicon.ico ├── layout.tsx ├── opengraph-image.png ├── page.tsx ├── sell │ └── page.tsx └── token │ └── [contractAddress] │ └── [tokenId] │ └── page.tsx ├── components ├── Container │ ├── Container.module.css │ └── Container.tsx ├── ListingGrid │ └── ListingGrid.tsx ├── NFT │ ├── NFT.tsx │ └── NFTGrid.tsx ├── Navbar │ └── index.tsx ├── SaleInfo │ ├── ApproveButton.tsx │ ├── AuctionListingButton.tsx │ ├── DirectListingButton.tsx │ └── index.tsx ├── Skeleton │ ├── Skeleton.module.css │ └── index.tsx └── token │ ├── BuyListingButton.tsx │ ├── Events.tsx │ └── MakeOfferButton.tsx ├── const └── contracts.ts ├── globals.css ├── lib └── client.ts ├── next-env.d.ts ├── next.config.js ├── package.json ├── postcss.config.js ├── public ├── favicon.ico ├── hero-asset.png ├── hero-gradient.png ├── logo.png ├── thirdweb.svg └── user-icon.png ├── tailwind.config.js ├── tsconfig.json └── util ├── randomColor.ts └── toastConfig.ts /.env.example: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_TEMPLATE_CLIENT_ID= 2 | TW_SECRET_KEY= -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals", 3 | "plugins": ["unused-imports"] 4 | } 5 | -------------------------------------------------------------------------------- /.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 | .pnpm-debug.log* 27 | 28 | # bun 29 | bun.lockb 30 | 31 | # local env files 32 | .env.local 33 | .env.development.local 34 | .env.test.local 35 | .env.production.local 36 | .env 37 | 38 | # vercel 39 | .vercel 40 | 41 | # typescript 42 | *.tsbuildinfo 43 | 44 | /scripts 45 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /application/const/contractAddresses.ts -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2021 Non-Fungible Labs, Inc 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > [!Important] 2 | > This repository is outdated and no longer maintained. But we have something better for you! 3 | > 4 | > Head over to this new repository: [marketplace-template](https://github.com/thirdweb-example/marketplace-template) 5 | > 6 | > The new marketplace template now comes with support for multiple chains and collections 7 | > 8 | 9 | # NFT Marketplace V3 10 | 11 | Create an NFT marketplace on top of your NFT collection on **any** EVM-compatible blockchain. 12 | 13 | ### Features 14 | 15 | - List multiple collections into an aggregated marketplace 16 | - Customize branding and theme to your application 17 | 18 | ## Installation 19 | 20 | Install via [thirdweb create](https://portal.thirdweb.com/cli/create) 21 | 22 | ```bash 23 | npx thirdweb create --template marketplace-v3 24 | ``` 25 | 26 | ## Environment Variables 27 | 28 | To run this project, you will need to add environment variables. Check the `.env.example` file for all the environment variables required and add it to `.env.local` file or set them up on your hosting provider. 29 | 30 | ## Run Locally 31 | 32 | Install dependencies 33 | 34 | ```bash 35 | # npm 36 | npm install 37 | 38 | # yarn 39 | yarn install 40 | ``` 41 | 42 | Start the server 43 | 44 | ```bash 45 | # npm 46 | npm run dev 47 | 48 | # yarn 49 | yarn dev 50 | ``` 51 | 52 | ## Additional Resources 53 | 54 | - [Documentation](https://portal.thirdweb.com) 55 | - [Templates](https://thirdweb.com/templates) 56 | - [Video Tutorials](https://youtube.com/thirdweb_) 57 | - [Blog](https://blog.thirdweb.com) 58 | 59 | ## Contributing 60 | 61 | Contributions and [feedback](https://feedback.thirdweb.com) are always welcome! Please check our [open source page](https://thirdweb.com/open-source) for more information. 62 | 63 | ## Need help? 64 | 65 | For help, please visit our [support site](https://support.thirdweb.com). 66 | -------------------------------------------------------------------------------- /app/buy/page.tsx: -------------------------------------------------------------------------------- 1 | export const dynamic = "force-dynamic"; 2 | export const revalidate = 0; 3 | import React, { Suspense } from "react"; 4 | import { NFTGridLoading } from "@/components/NFT/NFTGrid"; 5 | import ListingGrid from "@/components/ListingGrid/ListingGrid"; 6 | import { MARKETPLACE, NFT_COLLECTION } from "@/const/contracts"; 7 | 8 | export default function Buy() { 9 | return ( 10 |
11 |

Buy NFTs

12 | 13 |
14 | }> 15 | 22 | 23 |
24 |
25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thirdweb-example/marketplace-v3/6e8656bbe795e5799907c580ee70352830419fa1/app/favicon.ico -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { ThirdwebProvider } from "thirdweb/react"; 2 | import { Toaster } from "react-hot-toast"; 3 | import { Navbar } from "@/components/Navbar"; 4 | import Image from "next/image"; 5 | import "@/globals.css"; 6 | import { Metadata } from "next"; 7 | 8 | export const metadata: Metadata = { 9 | title: "thirdweb Marketplace Template", 10 | description: 11 | "Create an NFT marketplace on top of your NFT collection on any EVM-compatible blockchain.", 12 | }; 13 | 14 | export default function RootLayout({ 15 | children, 16 | }: { 17 | children: React.ReactNode; 18 | }) { 19 | return ( 20 | 21 | 22 |
23 | Background gradient from red to blue 31 |
32 | 33 | 34 | 35 | 36 |
37 |
38 | {children} 39 |
40 |
41 |
42 | 43 | 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /app/opengraph-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thirdweb-example/marketplace-v3/6e8656bbe795e5799907c580ee70352830419fa1/app/opengraph-image.png -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from "next"; 2 | import Link from "next/link"; 3 | import Image from "next/image"; 4 | 5 | /** 6 | * Landing page with a simple gradient background and a hero asset. 7 | * Free to customize as you see fit. 8 | */ 9 | const Home: NextPage = () => { 10 | return ( 11 |
12 |
13 | Hero asset, NFT marketplace 21 |
22 |
23 |

24 | 25 | Build NFT Marketplaces 26 | 27 |
28 | faster than ever. 29 |

30 |

31 | 36 | thirdweb 37 | {" "} 38 | gives you the tools you need to create audited, performant, 39 | and flexible NFT marketplaces in hours,{" "} 40 | not months. 41 |

42 | 43 |
44 | 48 | Get Started 49 | 50 | 55 | GitHub 56 | 57 |
58 |
59 |
60 | ); 61 | }; 62 | 63 | export default Home; 64 | -------------------------------------------------------------------------------- /app/sell/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | export const dynamic = "force-dynamic"; 3 | import React, { useEffect, useState } from "react"; 4 | import { useActiveAccount, MediaRenderer } from "thirdweb/react"; 5 | import NFTGrid, { NFTGridLoading } from "@/components/NFT/NFTGrid"; 6 | import { NFT as NFTType } from "thirdweb"; 7 | import { tokensOfOwner } from "thirdweb/extensions/erc721"; 8 | import SaleInfo from "@/components/SaleInfo"; 9 | import client from "@/lib/client"; 10 | import { NFT_COLLECTION } from "@/const/contracts"; 11 | import toast from "react-hot-toast"; 12 | import toastStyle from "@/util/toastConfig"; 13 | import { Cross1Icon } from "@radix-ui/react-icons"; 14 | 15 | export default function Sell() { 16 | const [loading, setLoading] = useState(false); 17 | const [ownedTokenIds, setOwnedTokenIds] = useState([]); 18 | const [selectedNft, setSelectedNft] = useState(); 19 | 20 | const account = useActiveAccount(); 21 | useEffect(() => { 22 | if (account) { 23 | setLoading(true); 24 | tokensOfOwner({ 25 | contract: NFT_COLLECTION, 26 | owner: account.address, 27 | }) 28 | .then(setOwnedTokenIds) 29 | .catch((err) => { 30 | toast.error( 31 | "Something went wrong while fetching your NFTs!", 32 | { 33 | position: "bottom-center", 34 | style: toastStyle, 35 | } 36 | ); 37 | console.log(err); 38 | }) 39 | .finally(() => { 40 | setLoading(false); 41 | }); 42 | } 43 | }, [account]); 44 | 45 | return ( 46 |
47 |

Sell NFTs

48 |
49 | {!selectedNft ? ( 50 | <> 51 | {loading ? ( 52 | 53 | ) : ( 54 | ({ 56 | tokenId, 57 | }))} 58 | overrideOnclickBehavior={(nft) => { 59 | setSelectedNft(nft); 60 | }} 61 | emptyText={ 62 | !account 63 | ? "Connect your wallet to list your NFTs!" 64 | : "Looks like you don't own any NFTs in this collection. Head to the buy page to buy some!" 65 | } 66 | /> 67 | )} 68 | 69 | ) : ( 70 |
71 |
72 |
73 | 78 | 86 |
87 |
88 | 89 |
90 |

91 | {selectedNft.metadata.name} 92 |

93 |

94 | #{selectedNft.id.toString()} 95 |

96 |

97 | You’re about to list the following item 98 | for sale. 99 |

100 | 101 |
102 | 103 |
104 |
105 |
106 | )} 107 |
108 |
109 | ); 110 | } 111 | -------------------------------------------------------------------------------- /app/token/[contractAddress]/[tokenId]/page.tsx: -------------------------------------------------------------------------------- 1 | export const dynamic = "force-dynamic"; 2 | export const revalidate = 0; 3 | import React from "react"; 4 | import { MediaRenderer } from "thirdweb/react"; 5 | import { 6 | getAllValidListings, 7 | getAllValidAuctions, 8 | } from "thirdweb/extensions/marketplace"; 9 | import { MARKETPLACE, NFT_COLLECTION } from "@/const/contracts"; 10 | import randomColor from "@/util/randomColor"; 11 | import { getNFT } from "thirdweb/extensions/erc721"; 12 | import client from "@/lib/client"; 13 | import BuyListingButton from "@/components/token/BuyListingButton"; 14 | import MakeOfferButton from "@/components/token/MakeOfferButton"; 15 | import Events from "@/components/token/Events"; 16 | 17 | const [randomColor1, randomColor2] = [randomColor(), randomColor()]; 18 | 19 | export default async function TokenPage({ 20 | params, 21 | }: { 22 | params: { contractAddress: string; tokenId: string }; 23 | }) { 24 | const listingsPromise = getAllValidListings({ 25 | contract: MARKETPLACE, 26 | }); 27 | const auctionsPromise = getAllValidAuctions({ 28 | contract: MARKETPLACE, 29 | }); 30 | const nftPromise = getNFT({ 31 | contract: NFT_COLLECTION, 32 | tokenId: BigInt(params.tokenId), 33 | includeOwner: true, 34 | }); 35 | 36 | const [listings, auctions, nft] = await Promise.all([ 37 | listingsPromise, 38 | auctionsPromise, 39 | nftPromise, 40 | ]); 41 | 42 | const directListing = listings?.find( 43 | (l) => 44 | l.assetContractAddress === params.contractAddress && 45 | l.tokenId === BigInt(params.tokenId) 46 | ); 47 | 48 | const auctionListing = auctions?.find( 49 | (a) => 50 | a.assetContractAddress === params.contractAddress && 51 | a.tokenId === BigInt(params.tokenId) 52 | ); 53 | 54 | return ( 55 |
56 |
57 | 62 |
63 |
64 |

65 | {nft.metadata.name} 66 |

67 |

68 | #{nft.id.toString()} 69 |

70 |
71 | 72 |
73 |
79 | {nft.owner && ( 80 |
81 |

Current Owner

82 |

83 | {nft.owner.slice(0, 8)}... 84 | {nft.owner.slice(-4)} 85 |

86 |
87 | )} 88 |
89 |
90 |
91 |

History

92 | 93 |
94 |
95 | 96 |
97 |
98 | {/* Pricing information */} 99 |
100 |

Price

101 |
102 | {directListing ? ( 103 | <> 104 | { 105 | directListing?.currencyValuePerToken 106 | .displayValue 107 | } 108 | {" " + 109 | directListing?.currencyValuePerToken 110 | .symbol} 111 | 112 | ) : auctionListing ? ( 113 | <> 114 | { 115 | auctionListing?.buyoutCurrencyValue 116 | .displayValue 117 | } 118 | {" " + 119 | auctionListing?.buyoutCurrencyValue 120 | .symbol} 121 | 122 | ) : ( 123 | "Not for sale" 124 | )} 125 |
126 |
127 | {auctionListing && ( 128 | <> 129 |

135 | Bids starting from 136 |

137 | 138 |
139 | { 140 | auctionListing 141 | ?.minimumBidCurrencyValue 142 | .displayValue 143 | } 144 | {" " + 145 | auctionListing 146 | ?.minimumBidCurrencyValue 147 | .symbol} 148 |
149 | 150 | )} 151 |
152 |
153 |
154 |
155 | 159 | 160 |
161 |

or

162 |
163 | 167 |
168 |
169 |
170 | ); 171 | } 172 | -------------------------------------------------------------------------------- /components/Container/Container.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | width: 100%; 3 | margin-left: auto; 4 | margin-right: auto; 5 | padding-left: 1rem; 6 | padding-right: 1rem; 7 | margin-top: 96px; 8 | } 9 | 10 | .xs { 11 | max-width: 600px; 12 | } 13 | 14 | .sm { 15 | max-width: 900px; 16 | } 17 | 18 | .md { 19 | max-width: 1200px; 20 | } 21 | 22 | .lg { 23 | max-width: 1200px; 24 | } 25 | 26 | .xl { 27 | max-width: 1536px; 28 | } 29 | -------------------------------------------------------------------------------- /components/Container/Container.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styles from "./Container.module.css"; 3 | 4 | type Props = { 5 | maxWidth: "xs" | "sm" | "md" | "lg" | "xl"; 6 | children: React.ReactNode; 7 | }; 8 | 9 | /** 10 | * A container component that sets the max-width of its children, and centers them on the page. 11 | * @param maxWidth: The max-width of the container. Can be "sm", "md", "lg", "xl", or "2xl". 12 | */ 13 | export default function Container({ maxWidth, children }: Props) { 14 | return ( 15 |
{children}
16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /components/ListingGrid/ListingGrid.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | getAllValidAuctions, 3 | getAllValidListings, 4 | } from "thirdweb/extensions/marketplace"; 5 | import { NFT as NFTType, ThirdwebContract } from "thirdweb"; 6 | import React, { Suspense } from "react"; 7 | import { MARKETPLACE, NFT_COLLECTION } from "../../const/contracts"; 8 | import NFTGrid, { NFTGridLoading } from "../NFT/NFTGrid"; 9 | 10 | type Props = { 11 | marketplace: ThirdwebContract; 12 | collection: ThirdwebContract; 13 | overrideOnclickBehavior?: (nft: NFTType) => void; 14 | emptyText: string; 15 | }; 16 | 17 | export default async function ListingGrid(props: Props) { 18 | const listingsPromise = getAllValidListings({ 19 | contract: MARKETPLACE, 20 | }); 21 | const auctionsPromise = getAllValidAuctions({ 22 | contract: MARKETPLACE, 23 | }); 24 | 25 | const [listings, auctions] = await Promise.all([ 26 | listingsPromise, 27 | auctionsPromise, 28 | ]); 29 | 30 | // Retrieve all NFTs from the listings 31 | const tokenIds = Array.from( 32 | new Set([ 33 | ...listings 34 | .filter( 35 | (l) => l.assetContractAddress === NFT_COLLECTION.address 36 | ) 37 | .map((l) => l.tokenId), 38 | ...auctions 39 | .filter( 40 | (a) => a.assetContractAddress === NFT_COLLECTION.address 41 | ) 42 | .map((a) => a.tokenId), 43 | ]) 44 | ); 45 | 46 | const nftData = tokenIds.map((tokenId) => { 47 | return { 48 | tokenId: tokenId, 49 | directListing: listings.find( 50 | (listing) => listing.tokenId === tokenId 51 | ), 52 | auctionListing: auctions.find( 53 | (listing) => listing.tokenId === tokenId 54 | ), 55 | }; 56 | }); 57 | 58 | return ( 59 | }> 60 | 65 | 66 | ); 67 | } 68 | -------------------------------------------------------------------------------- /components/NFT/NFT.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React, { useEffect, useState } from "react"; 3 | import { NFT } from "thirdweb"; 4 | import { NFT_COLLECTION } from "../../const/contracts"; 5 | import { DirectListing, EnglishAuction } from "thirdweb/extensions/marketplace"; 6 | import { MediaRenderer } from "thirdweb/react"; 7 | import { getNFT } from "thirdweb/extensions/erc721"; 8 | import client from "@/lib/client"; 9 | import Skeleton from "@/components/Skeleton"; 10 | import { useRouter } from "next/navigation"; 11 | 12 | type Props = { 13 | tokenId: bigint; 14 | nft?: NFT; 15 | directListing?: DirectListing; 16 | auctionListing?: EnglishAuction; 17 | overrideOnclickBehavior?: (nft: NFT) => void; 18 | }; 19 | 20 | export default function NFTComponent({ 21 | tokenId, 22 | directListing, 23 | auctionListing, 24 | overrideOnclickBehavior, 25 | ...props 26 | }: Props) { 27 | const router = useRouter(); 28 | const [nft, setNFT] = useState(props.nft); 29 | 30 | useEffect(() => { 31 | if (nft?.id !== tokenId) { 32 | getNFT({ 33 | contract: NFT_COLLECTION, 34 | tokenId: tokenId, 35 | includeOwner: true, 36 | }).then((nft) => { 37 | setNFT(nft); 38 | }); 39 | } 40 | }, [tokenId, nft?.id]); 41 | 42 | if (!nft) { 43 | return ; 44 | } 45 | 46 | return ( 47 |
overrideOnclickBehavior(nft!) 52 | : () => 53 | router.push( 54 | `/token/${ 55 | NFT_COLLECTION.address 56 | }/${tokenId.toString()}` 57 | ) 58 | } 59 | > 60 |
61 | {nft.metadata.image && ( 62 | 67 | )} 68 |
69 |
70 |
71 |

72 | {nft.metadata.name} 73 |

74 |

75 | #{nft.id.toString()} 76 |

77 |
78 | 79 | {(directListing || auctionListing) && ( 80 |
81 |

82 | Price 83 |

84 |

85 | {directListing 86 | ? `${directListing?.currencyValuePerToken.displayValue}${directListing?.currencyValuePerToken.symbol}` 87 | : `${auctionListing?.minimumBidCurrencyValue.displayValue}${auctionListing?.minimumBidCurrencyValue.symbol}`} 88 |

89 |
90 | )} 91 |
92 |
93 | ); 94 | } 95 | 96 | export function LoadingNFTComponent() { 97 | return ( 98 |
99 | 100 |
101 | ); 102 | } 103 | -------------------------------------------------------------------------------- /components/NFT/NFTGrid.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import type { NFT as NFTType } from "thirdweb"; 3 | import React from "react"; 4 | import NFT, { LoadingNFTComponent } from "./NFT"; 5 | import { DirectListing, EnglishAuction } from "thirdweb/extensions/marketplace"; 6 | 7 | type Props = { 8 | nftData: { 9 | tokenId: bigint; 10 | nft?: NFTType; 11 | directListing?: DirectListing; 12 | auctionListing?: EnglishAuction; 13 | }[]; 14 | overrideOnclickBehavior?: (nft: NFTType) => void; 15 | emptyText?: string; 16 | }; 17 | 18 | export default function NFTGrid({ 19 | nftData, 20 | overrideOnclickBehavior, 21 | emptyText = "No NFTs found for this collection.", 22 | }: Props) { 23 | if (nftData && nftData.length > 0) { 24 | return ( 25 |
26 | {nftData.map((nft) => ( 27 | 32 | ))} 33 |
34 | ); 35 | } 36 | 37 | return ( 38 |
39 |

40 | {emptyText} 41 |

42 |
43 | ); 44 | } 45 | 46 | export function NFTGridLoading() { 47 | return ( 48 |
49 | {[...Array(20)].map((_, index) => ( 50 | 51 | ))} 52 |
53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /components/Navbar/index.tsx: -------------------------------------------------------------------------------- 1 | import { ConnectButton } from "thirdweb/react"; 2 | import Image from "next/image"; 3 | import Link from "next/link"; 4 | import client from "@/lib/client"; 5 | import { NETWORK } from "@/const/contracts"; 6 | 7 | export function Navbar() { 8 | return ( 9 |
10 | 47 |
48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /components/SaleInfo/ApproveButton.tsx: -------------------------------------------------------------------------------- 1 | import { TransactionButton } from "thirdweb/react"; 2 | import { setApprovalForAll } from "thirdweb/extensions/erc721"; 3 | import toast from "react-hot-toast"; 4 | import { NFT_COLLECTION, MARKETPLACE } from "@/const/contracts"; 5 | import toastStyle from "@/util/toastConfig"; 6 | 7 | export default function ApprovalButton() { 8 | return ( 9 | { 11 | return setApprovalForAll({ 12 | contract: NFT_COLLECTION, 13 | operator: MARKETPLACE.address, 14 | approved: true, 15 | }); 16 | }} 17 | onTransactionSent={() => { 18 | toast.loading("Approving...", { 19 | id: "approve", 20 | style: toastStyle, 21 | position: "bottom-center", 22 | }); 23 | }} 24 | onError={(error) => { 25 | toast(`Approval Failed!`, { 26 | icon: "❌", 27 | id: "approve", 28 | style: toastStyle, 29 | position: "bottom-center", 30 | }); 31 | }} 32 | onTransactionConfirmed={(txResult) => { 33 | toast("Approval successful.", { 34 | icon: "👍", 35 | id: "approve", 36 | style: toastStyle, 37 | position: "bottom-center", 38 | }); 39 | }} 40 | > 41 | Approve 42 | 43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /components/SaleInfo/AuctionListingButton.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { NFT as NFTType } from "thirdweb"; 3 | import { TransactionButton } from "thirdweb/react"; 4 | import { useRouter } from "next/navigation"; 5 | import { createAuction } from "thirdweb/extensions/marketplace"; 6 | import { MARKETPLACE, NFT_COLLECTION } from "@/const/contracts"; 7 | import toastStyle from "@/util/toastConfig"; 8 | import toast from "react-hot-toast"; 9 | import { revalidatePath } from "next/cache"; 10 | 11 | export default function AuctionListingButton({ 12 | nft, 13 | minimumBidAmount, 14 | buyoutBidAmount, 15 | }: { 16 | nft: NFTType; 17 | minimumBidAmount: string; 18 | buyoutBidAmount: string; 19 | }) { 20 | const router = useRouter(); 21 | return ( 22 | { 24 | return createAuction({ 25 | contract: MARKETPLACE, 26 | assetContractAddress: NFT_COLLECTION.address, 27 | tokenId: nft.id, 28 | minimumBidAmount, 29 | buyoutBidAmount, 30 | }); 31 | }} 32 | onTransactionSent={() => { 33 | toast.loading("Listing...", { 34 | id: "auction", 35 | style: toastStyle, 36 | position: "bottom-center", 37 | }); 38 | }} 39 | onError={(error) => { 40 | toast(`Listing Failed!`, { 41 | icon: "❌", 42 | id: "auction", 43 | style: toastStyle, 44 | position: "bottom-center", 45 | }); 46 | }} 47 | onTransactionConfirmed={(txResult) => { 48 | toast("Listed Successfully!", { 49 | icon: "🥳", 50 | id: "auction", 51 | style: toastStyle, 52 | position: "bottom-center", 53 | }); 54 | router.push( 55 | `/token/${NFT_COLLECTION.address}/${nft.id.toString()}` 56 | ); 57 | }} 58 | > 59 | List for Auction 60 | 61 | ); 62 | } 63 | -------------------------------------------------------------------------------- /components/SaleInfo/DirectListingButton.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useRouter } from "next/navigation"; 3 | import { NFT as NFTType } from "thirdweb"; 4 | import { TransactionButton } from "thirdweb/react"; 5 | import { createListing } from "thirdweb/extensions/marketplace"; 6 | import toast from "react-hot-toast"; 7 | import { MARKETPLACE, NFT_COLLECTION } from "@/const/contracts"; 8 | import toastStyle from "@/util/toastConfig"; 9 | import { revalidatePath } from "next/cache"; 10 | 11 | export default function DirectListingButton({ 12 | nft, 13 | pricePerToken, 14 | }: { 15 | nft: NFTType; 16 | pricePerToken: string; 17 | }) { 18 | const router = useRouter(); 19 | return ( 20 | { 22 | return createListing({ 23 | contract: MARKETPLACE, 24 | assetContractAddress: NFT_COLLECTION.address, 25 | tokenId: nft.id, 26 | pricePerToken, 27 | }); 28 | }} 29 | onTransactionSent={() => { 30 | toast.loading("Listing...", { 31 | id: "direct", 32 | style: toastStyle, 33 | position: "bottom-center", 34 | }); 35 | }} 36 | onError={(error) => { 37 | toast(`Listing Failed!`, { 38 | icon: "❌", 39 | id: "direct", 40 | style: toastStyle, 41 | position: "bottom-center", 42 | }); 43 | }} 44 | onTransactionConfirmed={(txResult) => { 45 | toast("Listed Successfully!", { 46 | icon: "🥳", 47 | id: "direct", 48 | style: toastStyle, 49 | position: "bottom-center", 50 | }); 51 | router.push( 52 | `/token/${NFT_COLLECTION.address}/${nft.id.toString()}` 53 | ); 54 | }} 55 | > 56 | List for Sale 57 | 58 | ); 59 | } 60 | -------------------------------------------------------------------------------- /components/SaleInfo/index.tsx: -------------------------------------------------------------------------------- 1 | import { NFT as NFTType } from "thirdweb"; 2 | import React, { useState } from "react"; 3 | 4 | import { useActiveAccount, useReadContract } from "thirdweb/react"; 5 | import { ADDRESS_ZERO } from "thirdweb"; 6 | import { isApprovedForAll } from "thirdweb/extensions/erc721"; 7 | import { MARKETPLACE, NFT_COLLECTION } from "@/const/contracts"; 8 | import AuctionListingButton from "./AuctionListingButton"; 9 | import DirectListingButton from "./DirectListingButton"; 10 | import cn from "classnames"; 11 | import ApprovalButton from "./ApproveButton"; 12 | 13 | type Props = { 14 | nft: NFTType; 15 | }; 16 | 17 | const INPUT_STYLES = 18 | "block w-full py-3 px-4 mb-4 bg-transparent border border-white text-base box-shadow-md rounded-lg mb-4"; 19 | const LEGEND_STYLES = "mb-2 text-white/80"; 20 | export default function SaleInfo({ nft }: Props) { 21 | const account = useActiveAccount(); 22 | const [tab, setTab] = useState<"direct" | "auction">("direct"); 23 | 24 | const { data: hasApproval } = useReadContract(isApprovedForAll, { 25 | contract: NFT_COLLECTION, 26 | owner: account?.address || ADDRESS_ZERO, 27 | operator: MARKETPLACE.address, 28 | }); 29 | 30 | const [directListingState, setDirectListingState] = useState({ 31 | price: "0", 32 | }); 33 | const [auctionListingState, setAuctionListingState] = useState({ 34 | minimumBidAmount: "0", 35 | buyoutPrice: "0", 36 | }); 37 | 38 | return ( 39 | <> 40 |
41 |
42 |

setTab("direct")} 49 | > 50 | Direct 51 |

52 |

setTab("auction")} 59 | > 60 | Auction 61 |

62 |
63 | 64 | {/* Direct listing fields */} 65 |
71 | {/* Input field for buyout price */} 72 | 73 | {" "} 74 | Price per token 75 | 76 | 82 | setDirectListingState({ price: e.target.value }) 83 | } 84 | /> 85 | {!hasApproval ? ( 86 | 87 | ) : ( 88 | 92 | )} 93 |
94 | 95 | {/* Auction listing fields */} 96 |
102 | 103 | {" "} 104 | Allow bids starting from{" "} 105 | 106 | 112 | setAuctionListingState({ 113 | ...auctionListingState, 114 | minimumBidAmount: e.target.value, 115 | }) 116 | } 117 | /> 118 | 119 | 120 | {" "} 121 | Buyout price{" "} 122 | 123 | 129 | setAuctionListingState({ 130 | ...auctionListingState, 131 | buyoutPrice: e.target.value, 132 | }) 133 | } 134 | /> 135 | 136 | {!hasApproval ? ( 137 | 138 | ) : ( 139 | 146 | )} 147 |
148 |
149 | 150 | ); 151 | } 152 | -------------------------------------------------------------------------------- /components/Skeleton/Skeleton.module.css: -------------------------------------------------------------------------------- 1 | .skeleton { 2 | width: 100%; 3 | background: linear-gradient(to right, #333 0%, #555 50%, #333 100%); 4 | background-size: 400% 400%; 5 | animation: pulse 1s ease-in-out infinite; 6 | min-height: 12px; 7 | max-height: 100%; 8 | } 9 | 10 | @keyframes pulse { 11 | 0% { 12 | background-position: 0% 0%; 13 | } 14 | 100% { 15 | background-position: -135% 0%; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /components/Skeleton/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styles from "./Skeleton.module.css"; 3 | 4 | type Props = { 5 | width?: string; 6 | height?: string; 7 | }; 8 | 9 | export default function Skeleton({ height, width }: Props) { 10 | return ( 11 |
19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /components/token/BuyListingButton.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { TransactionButton, useActiveAccount } from "thirdweb/react"; 3 | import { 4 | DirectListing, 5 | EnglishAuction, 6 | buyFromListing, 7 | buyoutAuction, 8 | } from "thirdweb/extensions/marketplace"; 9 | import { MARKETPLACE, NFT_COLLECTION } from "@/const/contracts"; 10 | import toastStyle from "@/util/toastConfig"; 11 | import toast from "react-hot-toast"; 12 | import { revalidatePath } from "next/cache"; 13 | 14 | export default function BuyListingButton({ 15 | auctionListing, 16 | directListing, 17 | }: { 18 | auctionListing?: EnglishAuction; 19 | directListing?: DirectListing; 20 | }) { 21 | const account = useActiveAccount(); 22 | return ( 23 | { 30 | if (!account) throw new Error("No account"); 31 | if (auctionListing) { 32 | return buyoutAuction({ 33 | contract: MARKETPLACE, 34 | auctionId: auctionListing.id, 35 | }); 36 | } else if (directListing) { 37 | return buyFromListing({ 38 | contract: MARKETPLACE, 39 | listingId: directListing.id, 40 | recipient: account.address, 41 | quantity: BigInt(1), 42 | }); 43 | } else { 44 | throw new Error("No valid listing found for this NFT"); 45 | } 46 | }} 47 | onTransactionSent={() => { 48 | toast.loading("Purchasing...", { 49 | id: "buy", 50 | style: toastStyle, 51 | position: "bottom-center", 52 | }); 53 | }} 54 | onError={(error) => { 55 | toast(`Purchase Failed!`, { 56 | icon: "❌", 57 | id: "buy", 58 | style: toastStyle, 59 | position: "bottom-center", 60 | }); 61 | }} 62 | onTransactionConfirmed={(txResult) => { 63 | toast("Purchased Successfully!", { 64 | icon: "🥳", 65 | id: "buy", 66 | style: toastStyle, 67 | position: "bottom-center", 68 | }); 69 | }} 70 | > 71 | Buy Now 72 | 73 | ); 74 | } 75 | -------------------------------------------------------------------------------- /components/token/Events.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useContractEvents } from "thirdweb/react"; 3 | import { transferEvent } from "thirdweb/extensions/erc721"; 4 | import { ETHERSCAN_URL, NFT_COLLECTION } from "@/const/contracts"; 5 | import Link from "next/link"; 6 | 7 | export default function Events({ tokenId }: { tokenId: bigint }) { 8 | const { data: transferEvents } = useContractEvents({ 9 | contract: NFT_COLLECTION, 10 | events: [transferEvent({ tokenId })], 11 | }); 12 | 13 | return ( 14 |
15 | {transferEvents?.map((event, index) => ( 16 |
20 |
21 |

Event

22 |

Transfer

23 |
24 | 25 |
26 |

From

27 |

28 | {event.args.from.slice(0, 4)} 29 | ... 30 | {event.args.from.slice(-2)} 31 |

32 |
33 | 34 |
35 |

To

36 |

37 | {event.args.to.slice(0, 4)} 38 | ... 39 | {event.args.to.slice(-2)} 40 |

41 |
42 | 43 |
44 | 49 | ↗ 50 | 51 |
52 |
53 | ))} 54 |
55 | ); 56 | } 57 | -------------------------------------------------------------------------------- /components/token/MakeOfferButton.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { TransactionButton, useActiveAccount } from "thirdweb/react"; 3 | import { 4 | DirectListing, 5 | EnglishAuction, 6 | bidInAuction, 7 | makeOffer, 8 | } from "thirdweb/extensions/marketplace"; 9 | import { MARKETPLACE, NFT_COLLECTION } from "@/const/contracts"; 10 | import toastStyle from "@/util/toastConfig"; 11 | import toast from "react-hot-toast"; 12 | import { useState } from "react"; 13 | import { revalidatePath } from "next/cache"; 14 | 15 | export default function MakeOfferButton({ 16 | auctionListing, 17 | directListing, 18 | }: { 19 | auctionListing?: EnglishAuction; 20 | directListing?: DirectListing; 21 | }) { 22 | const account = useActiveAccount(); 23 | const [bid, setBid] = useState("0"); 24 | 25 | return ( 26 |
27 | setBid(e.target.value)} 33 | /> 34 | { 41 | if (!account) throw new Error("No account"); 42 | if (auctionListing) { 43 | return bidInAuction({ 44 | contract: MARKETPLACE, 45 | auctionId: auctionListing.id, 46 | bidAmount: bid, 47 | }); 48 | } else if (directListing) { 49 | return makeOffer({ 50 | contract: MARKETPLACE, 51 | assetContractAddress: 52 | directListing.assetContractAddress, 53 | tokenId: directListing.tokenId, 54 | currencyContractAddress: 55 | directListing.currencyContractAddress, 56 | totalOffer: bid, 57 | offerExpiresAt: new Date( 58 | Date.now() + 10 * 365 * 24 * 60 * 60 * 1000 59 | ), 60 | }); 61 | } else { 62 | throw new Error("No valid listing found for this NFT"); 63 | } 64 | }} 65 | onTransactionSent={() => { 66 | toast.loading("Making offer...", { 67 | id: "buy", 68 | style: toastStyle, 69 | position: "bottom-center", 70 | }); 71 | }} 72 | onError={(error) => { 73 | toast(`Offer Failed!`, { 74 | icon: "❌", 75 | id: "buy", 76 | style: toastStyle, 77 | position: "bottom-center", 78 | }); 79 | }} 80 | onTransactionConfirmed={(txResult) => { 81 | toast("Offer Placed Successfully!", { 82 | icon: "🥳", 83 | id: "buy", 84 | style: toastStyle, 85 | position: "bottom-center", 86 | }); 87 | }} 88 | > 89 | Make Offer 90 | 91 |
92 | ); 93 | } 94 | -------------------------------------------------------------------------------- /const/contracts.ts: -------------------------------------------------------------------------------- 1 | import client from "@/lib/client"; 2 | /** Replace the values below with the addresses of your smart contracts. */ 3 | 4 | // 1. Set up the network your smart contracts are deployed to. 5 | // First, import the chain from the package, then set the NETWORK variable to the chain. 6 | import { getContract } from "thirdweb"; 7 | import { sepolia } from "thirdweb/chains"; 8 | export const NETWORK = sepolia; 9 | 10 | // 2. The address of the marketplace V3 smart contract. 11 | // Deploy your own: https://thirdweb.com/thirdweb.eth/MarketplaceV3 12 | const MARKETPLACE_ADDRESS = "0x38ab4489E479c9266471bbe8C3794CB30EA11F20"; 13 | export const MARKETPLACE = getContract({ 14 | address: MARKETPLACE_ADDRESS, 15 | client, 16 | chain: NETWORK, 17 | }); 18 | 19 | // 3. The address of your NFT collection smart contract. 20 | const NFT_COLLECTION_ADDRESS = "0x72a6eb347D86Bb5DE9c3c6a3DFAb6f2eff80F3C9"; 21 | export const NFT_COLLECTION = getContract({ 22 | address: NFT_COLLECTION_ADDRESS, 23 | client, 24 | chain: NETWORK, 25 | }); 26 | 27 | // (Optional) Set up the URL of where users can view transactions on 28 | // For example, below, we use Mumbai.polygonscan to view transactions on the Mumbai testnet. 29 | export const ETHERSCAN_URL = "https://sepolia.etherscan.io"; 30 | -------------------------------------------------------------------------------- /globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | html, 6 | body { 7 | padding: 0; 8 | margin: 0; 9 | font-family: Inter, Helvetica, Arial, sans-serif; 10 | } 11 | 12 | * { 13 | box-sizing: border-box; 14 | } 15 | 16 | html { 17 | color-scheme: dark; 18 | } 19 | 20 | body { 21 | color: #ffffff; 22 | background: #191c1f; 23 | --color-primary: #fada3f; 24 | --color-secondary: #ff2136; 25 | --color-tertiary: #0294fe; 26 | --color-quaternary: #9b4dff; 27 | --card-background: #252133; 28 | --highlighted-background: #202326; 29 | font-weight: 400; 30 | } 31 | 32 | .gradient { 33 | @apply bg-gradient-to-tl from-[#0294fe] to-[#9b4dff]; 34 | } 35 | 36 | h1 { 37 | font-size: 2rem; 38 | font-weight: 700; 39 | line-height: 1.5; 40 | } 41 | 42 | h3 { 43 | font-size: 22px; 44 | line-height: 28px; 45 | font-weight: 700; 46 | margin-bottom: 0px; 47 | } 48 | 49 | a { 50 | color: inherit; 51 | text-decoration: none; 52 | } 53 | -------------------------------------------------------------------------------- /lib/client.ts: -------------------------------------------------------------------------------- 1 | import { createThirdwebClient } from "thirdweb"; 2 | 3 | const clientId = process.env.NEXT_PUBLIC_TEMPLATE_CLIENT_ID; 4 | const secretKey = process.env.TW_SECRET_KEY; 5 | 6 | if (!clientId) { 7 | throw new Error("Client ID not set"); 8 | } 9 | 10 | export default createThirdwebClient(secretKey ? { secretKey } : { clientId }); 11 | -------------------------------------------------------------------------------- /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 | basePath: "", 5 | images: { 6 | remotePatterns: [ 7 | { 8 | protocol: "https", 9 | hostname: "**", 10 | }, 11 | ], 12 | }, 13 | webpack: (config) => { 14 | config.externals.push("pino-pretty", "lokijs", "encoding"); 15 | return config; 16 | }, 17 | }; 18 | 19 | module.exports = nextConfig; 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "thirdweb-nft-marketplace", 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 | "deploy": "next build && next export && npx thirdweb@latest upload out" 12 | }, 13 | "dependencies": { 14 | "@radix-ui/react-icons": "^1.3.0", 15 | "classnames": "^2.5.1", 16 | "eslint-plugin-unused-imports": "^3.2.0", 17 | "next": "^14", 18 | "nextjs-progressbar": "^0.0.16", 19 | "pino-pretty": "^11.0.0", 20 | "react": "^18.3.1", 21 | "react-dom": "^18.3.1", 22 | "react-hook-form": "^7.43.0", 23 | "react-hot-toast": "^2.4.0", 24 | "thirdweb": "^5.16.1" 25 | }, 26 | "devDependencies": { 27 | "@eslint/js": "^8", 28 | "@types/node": "^18.11.11", 29 | "@types/react": "^18.3.1", 30 | "autoprefixer": "^10.4.19", 31 | "eslint": "^8", 32 | "eslint-config-next": "^14.2.3", 33 | "eslint-config-prettier": "^9.1.0", 34 | "eslint-plugin-react": "^7.34.1", 35 | "globals": "^15.1.0", 36 | "postcss": "^8.4.38", 37 | "tailwindcss": "^3.4.3", 38 | "typescript": "^5.4.5", 39 | "typescript-eslint": "^7.8.0" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thirdweb-example/marketplace-v3/6e8656bbe795e5799907c580ee70352830419fa1/public/favicon.ico -------------------------------------------------------------------------------- /public/hero-asset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thirdweb-example/marketplace-v3/6e8656bbe795e5799907c580ee70352830419fa1/public/hero-asset.png -------------------------------------------------------------------------------- /public/hero-gradient.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thirdweb-example/marketplace-v3/6e8656bbe795e5799907c580ee70352830419fa1/public/hero-gradient.png -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thirdweb-example/marketplace-v3/6e8656bbe795e5799907c580ee70352830419fa1/public/logo.png -------------------------------------------------------------------------------- /public/thirdweb.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /public/user-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thirdweb-example/marketplace-v3/6e8656bbe795e5799907c580ee70352830419fa1/public/user-icon.png -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | "./app/**/*.{js,ts,jsx,tsx,mdx}", 5 | "./components/**/*.{js,ts,jsx,tsx,mdx}", 6 | ], 7 | theme: { 8 | extend: {}, 9 | }, 10 | plugins: [], 11 | }; 12 | -------------------------------------------------------------------------------- /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 | "plugins": [ 18 | { 19 | "name": "next" 20 | } 21 | ], 22 | "paths": { 23 | "@/*": ["./*"] 24 | } 25 | }, 26 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 27 | "exclude": ["node_modules"] 28 | } 29 | -------------------------------------------------------------------------------- /util/randomColor.ts: -------------------------------------------------------------------------------- 1 | export default function randomColor() { 2 | let letters = "0123456789ABCDEF"; 3 | let color = "#"; 4 | for (let i = 0; i < 6; i++) { 5 | color += letters[Math.floor(Math.random() * 16)]; 6 | } 7 | return color; 8 | } 9 | -------------------------------------------------------------------------------- /util/toastConfig.ts: -------------------------------------------------------------------------------- 1 | const toastStyle = { 2 | borderRadius: "4px", 3 | background: "#222528", 4 | color: "#fff", 5 | }; 6 | 7 | export default toastStyle; 8 | --------------------------------------------------------------------------------