Features
- Let's the user create an NFT, with an image and metadata (properties)
- Stores and displays the NFTs the user owns
- Collect fees from every mint
- Dark/Light mode
Summary
In this tutorial, we will create an NFT minting web application, that allows users to upload an Image to IPFS, add properties and attributes, and mint a custom-made NFT.
Setup
For this you will need to have installed python, node.js and brownie-eth.
We also need a local blockchain to test and develop our application, I'll be using Ganache, which is a beginner-friendly app to run a local blockchain, and there is a lot of tutorial and documentation on how to use it.
After that, open your terminal and run brownie bake react simple-mint, this will generate Brownie project using ReactJS, but wait I said that we will use NextJS for this project, go to the folder simple-mint (the project that you just created) and delete the folder called client. After that is done go back to your terminal and inside the simple-mint folder run npx create-next-app client, this command will create a brand new NextJS project. For now, that's all we need now onto the smart contracts!
Smart contracts
Before we start programming the smart contract, we need to modify the brownie-config.yaml
, here we will include the dependencies we need, re-mappings for the compiler, also now that we are here, create a .env
file to avoid annoying errors.
The brownie-config.yaml
file should now look like this:
# change the build directory to be within react's scope
project_structure:
build: client/artifacts
dependencies:
- OpenZeppelin/openzeppelin-contracts@4.5.0
# automatically fetch contract sources from Etherscan
autofetch_sources: True
dotenv: .env
compiler:
solc:
version: '0.8.4'
optimizer:
enabled: true
runs: 200
remappings:
- "@openzeppelin=OpenZeppelin/openzeppelin-contracts@4.5.0"
networks:
default: development
development:
update_interval: 60
verify: False
kovan:
verify: False
update_interval: 60
wallets:
from_key: ${PRIVATE_KEY}
# enable output of development artifacts to load with react
dev_deployment_artifacts: true
For this dapp we only need one smart contract, the contract is really simple is an extended version of an ERC-721 contract. Inside the contracts folder create a SimpleMint.sol
file.
// SPDX-License-Identifier: MIT
// contracts/SimpleMint.sol
pragma solidity ^0.8.4;
// Te will extend/use this open zeppelin smart contract to save time
// if you nee more information about ERC721 checkout the OpenZeppelin docs
// https://docs.openzeppelin.com/contracts/4.x/erc721
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
// This smart contract enabled us to give access control to some functions
// https://docs.openzeppelin.com/contracts/4.x/api/access#Ownable
import "@openzeppelin/contracts/access/Ownable.sol";
contract SimpleMint is ERC721, ERC721URIStorage, Ownable {
using Counters for Counters.Counter;
Counters.Counter private _tokenIdCounter;
// This is the minting fee users have to pay to mint an NFT
// on the platform
uint256 private _fee = 0.0025 ether;
constructor() ERC721("SimpleMint", "SIMPLE") {}
function safeMint(string memory uri) public payable {
// This 'require' ensures the user is paying
// the minting fee
require (
msg.value == _fee,
"You nee to pay a small fee to mint the NFT."
);
uint256 tokenId = _tokenIdCounter.current();
_tokenIdCounter.increment();
_safeMint(msg.sender, tokenId);
_setTokenURI(tokenId, uri);
}
// The following functions are overrides required by Solidity.
function _burn(uint256 tokenId) internal override(ERC721, ERC721URIStorage) {
super._burn(tokenId);
}
function tokenURI(uint256 tokenId)
public
view
override(ERC721, ERC721URIStorage)
returns (string memory)
{
return super.tokenURI(tokenId);
}
// This function will return a list of Token URIs
// given an Ethreum address
function tokensOf(address minter)
public
view
returns (string[] memory)
{
// Here we count how many tokens does the user have
uint256 count = 0;
for(uint256 i = 0; i < _tokenIdCounter.current(); i++) {
if(ownerOf(i) == minter) {
count ++;
}
}
// Here we create and populate the tokens with their
// correspoding Token URI
string[] memory tokens = new string[](count);
uint256 index = 0;
for(uint256 i = 0; i < _tokenIdCounter.current(); i++) {
if(ownerOf(i) == minter) {
tokens[index] = tokenURI(i);
index ++;
}
}
return tokens;
}
// This function returns the minting fee to users
function fee()
public
view
returns (uint256)
{
return _fee;
}
// This function allows you, **and only you**, to change
// the minting fee
function setFee(uint256 newFee)
public
onlyOwner
{
_fee = newFee;
}
// This function will transfer all the fees collected
// to the owner
function withdraw()
public
onlyOwner
{
(bool success, ) = payable(owner()).call{ value: address(this).balance }("");
require (success);
}
}
Well, that's it, this is the smart contract that will be the backbone of our application.
Testing
Before we deploy this dapp to production, we need to make sure everything is working properly, for this we will write some automated tests that will check if the functionality of the smart contract is what we expect.
Go to the tests folder, and open the conftest.py
file, this is code that will execute before each test. The file should be looking like this:
# tests/conftest.py
import pytest
@pytest.fixture(autouse=True)
def setup(fn_isolation):
"""
Isolation setup fixture.
This ensures that each test runs against the same base environment.
"""
pass
@pytest.fixture(scope="module")
def simple_mint(accounts, SimpleMint):
"""
Yield a `Contract` object for the SimpleMint contract.
"""
yield accounts[0].deploy(SimpleMint)
Now create a file called test_simple_mint.py
inside the tests folder. The first test that we will write is to check if the contract is deployed correctly.
# tests/test_simple_mint.py
from brownie import Wei
def test_simple_mint_deploy(simple_mint):
"""
Test if the contract is correctly deployed.
"""
assert simple_mint.fee() == Wei('0.0025 ether')
Now we create a test to check if the user can and cannot mint NFTs while paying the fee.
# tests/test_simple_mint.py
# ...
def test_simple_mint_minting(accounts, simple_mint):
"""
Test if the contract can mint an NFT, and charge the
corresponding fee.
"""
token_uri = 'https://example.mock/uri.json'
# can't mint, not paying fee
with reverts():
simple_mint.safeMint(token_uri, {'from': accounts[1]})
# can mint, paying fee
fee = simple_mint.fee()
simple_mint.safeMint(token_uri, {'from': accounts[1], 'value': fee})
The next test will check the display of user tokens after minting, and if the owner of the token is correct, this means that the users can only see NFTs that they own.
# tests/test_simple_mint.py
# ...
def test_simple_mint_tokens(accounts, simple_mint):
"""
Test if the contract can mint an NFT, and charge the
corresponding fee.
"""
token_uri = 'https://example.mock/uri.json'
user_one, user_two = accounts[1], accounts[2]
fee = simple_mint.fee()
# minting 3 tokens as user one
simple_mint.safeMint(token_uri, {'from': user_one, 'value': fee})
simple_mint.safeMint(token_uri, {'from': user_one, 'value': fee})
simple_mint.safeMint(token_uri, {'from': user_one, 'value': fee})
# minting 2 tokens as user two
simple_mint.safeMint(token_uri, {'from': user_two, 'value': fee})
simple_mint.safeMint(token_uri, {'from': user_two, 'value': fee})
user_one_tokens = simple_mint.tokensOf(user_one.address)
assert len(user_one_tokens) == 3
# here we assert that the owner of the token is the correct one
print("--- user one's tokens")
for token_uri, token_id in user_one_tokens:
assert simple_mint.ownerOf(token_id) == user_one.address
print(token_uri, token_id)
user_two_tokens = simple_mint.tokensOf(user_two.address)
assert len(user_two_tokens) == 2
# here we assert that the owner of the token is the correct one
print("--- user two's tokens")
for token_uri, token_id in user_two_tokens:
assert simple_mint.ownerOf(token_id) == user_two.address
print(token_uri, token_id)
We will test the fee-related functions, first, we will test if we can change the minting fee.
# tests/test_simple_mint.py
# ...
def test_simple_mint_fees(accounts, simple_mint):
"""
Test if the owner, and the owner only, can change the minting fee.
"""
fee = simple_mint.fee()
assert simple_mint.fee() == Wei('0.0025 ether')
# another user cannot change the minting fee
with reverts():
simple_mint.setFee(Wei('0.5 ether'), {'from': accounts[1]})
# the owner can change the minting fee
new_fee = Wei('0.0025 ether')
simple_mint.setFee(new_fee, {'from': accounts[0]})
assert simple_mint.fee() == new_fee
Finally, we will test the withdrawal function, and check if we got the correct amount of collected fees.
# tests/test_simple_mint.py
# ...
def test_simple_withdraw(accounts, simple_mint):
"""
Test if the owner, and the owner only, can withdraw all the
collected fees.
"""
fee = Wei('0.5 ether')
simple_mint.setFee(fee, {'from': accounts[0]})
initial_balance = accounts[0].balance()
print(f'Intial balance: {initial_balance}')
# here we will mint 10 tokens, at a 0.5 ETH fee, this will cost 5 ETH
# to account one, so the collected fees should amount to 5 ETH
token_uri = 'https://example.mock/uri.json'
for i in range(10):
print(f'mint {i}/10')
simple_mint.safeMint(
token_uri, {'from': accounts[1], 'value': fee})
simple_mint.withdraw({'from': accounts[0]})
# the owner new balance should be:
# initial_balance + 5 ETH
new_balance = accounts[0].balance()
print(f'New balance: {new_balance}')
assert initial_balance + Wei('5 ether') == new_balance
And that's it for testing, we checked the basic functionality that the smart contract needs to have.
To run the tests open ganache, once ganache is running a local blockchain, you can go back to your terminal/console and run the following command brownie test
, this will run all the tests that we have written (you can also run brownie test -s
to view prints and more details on each test).
If you want to see how the code is structured, check out the code repository.
Front-end
Now go back to your terminal, and go to the client folder, we need to install some dependencies, go check the setup guide for TailwindCSS with NextJS, after that is complete we need to install some package to interact with a blockchain from NextJS, run npm install web3modal ethers Axios ipfs-http-client
, once that is installed we can start building our front-end.
Components
First, we will build all the components that we need to have to use the application, take in mind these are dumb components, which means that they will not interact with the state directly.
Loading component
A simple component to display an overlay, with a spinning wheel and some text. The text is variable, to allow you to display any message that you want.
// components/Loading/index.js
export default function Loading({ text }) {
return (
<div className="overflow-none fixed top-0 left-0 flex h-screen w-screen items-center justify-center bg-black bg-opacity-50">
<div className="flex items-center text-white">
<svg
className="-ml-1 mr-3 h-5 w-5 animate-spin text-white"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
{text}...
</div>
</div>
)
}
Navbar
The first component we will create is a navbar, to navigate between views and connect our web3 wallet to the application. This component displays a 'connect wallet button' and the logo, once the user connects the 'connect wallet button' will disappear and the wallet address and navigation links will show up. This component also has the theme toggler button, although the button is fixed since this component will be used in every view is better to have it here.
// components/Navbar/index.js
import Image from 'next/image'
import Link from 'next/link'
// This returns a **readable** wallet address
const formatAddress = (address) =>
address.slice(0, 5) + '...' + address.slice(38)
export default function Navbar({ address, connectWallet, theme, setTheme }) {
return (
<div className="py-6 md:px-6">
<div className="flex items-center justify-center border-b border-zinc-100 px-3 pb-6 dark:border-zinc-600 sm:justify-between">
{/* logo */}
<div className="hidden cursor-pointer sm:inline-flex">
<Link href="/">
<a>
<Image src="/logo.png" width={90} height={78} />
</a>
</Link>
</div>
{/* connect button */}
{!address && (
<div className="flex items-center">
<button
onClick={connectWallet}
className="cursor-pointer rounded-md bg-green-400 py-2 px-3 text-white hover:bg-green-500"
>
Connect
</button>
</div>
)}
{/* navigation & user's address */}
{address && (
<div className="flex items-center space-x-3">
<Link href="/">
<a className="hover:underline">My NFTs</a>
</Link>
<Link href="/create">
<a className="hover:underline">Create</a>
</Link>
<p className="rounded-md bg-green-400 py-2 px-3 text-white">
{formatAddress(address)}
</p>
</div>
)}
</div>
{/* theme toggler */}
<div className="fixed -bottom-1 -right-1 rounded-md border border-zinc-100 bg-white bg-zinc-50 p-3 dark:border-zinc-600 dark:bg-zinc-600">
{theme === 'light' && (
<svg
cursor="pointer"
xmlns="http://www.w3.org/2000/svg"
className="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
onClick={() => setTheme('dark')}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"
/>
</svg>
)}
{theme === 'dark' && (
<svg
cursor="pointer"
xmlns="http://www.w3.org/2000/svg"
className="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
onClick={() => setTheme('light')}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"
/>
</svg>
)}
</div>
</div>
)
}
No wallet
This is a simple component to prompt the user to connect their wallet to the user of the application.
// components/NoWallet/index.js
export default function NoWallet() {
return (
<div className="flex h-screen w-screen items-center justify-center md:h-[65vh]">
<p className="font-3xl font-bold text-zinc-400 dark:text-zinc-200">
Connect your wallet to access the application
</p>
</div>
)
}
No Mints
We also need a no mints component, to show the user that he hasn't created an NFT with that address.
// components/NoMints/index.js
import Link from 'next/link'
export default function NoMints() {
return (
<div className="flex h-screen w-full items-center justify-center md:h-[65vh]">
<p className="font-3xl font-bold text-zinc-400 dark:text-zinc-200">
Looks like you haven't created any NFT's yet,{' '}
<Link href="/create">
<span className="cursor-pointer text-green-500 hover:underline">
creaate one now
</span>
</Link>
.
</p>
</div>
)
}
NFT Card
This component will display the image and the name of the NFT that the user owns/mints, and will also include a link to a details page of that NFT.
// components/NFTCard/index.js
import Link from 'next/link'
import Image from 'next/image'
export default function NFTCard({ data }) {
return (
<div className="max-w-96 group relative h-72 cursor-pointer rounded-md duration-100 ease-in-out hover:scale-105 sm:w-72">
<div className="max-w-96 relative h-72 rounded-md sm:w-72">
<Image
className="rounded-md"
layout="fill"
objectFit="cover"
quality={100}
src={data.metadata.image}
alt="text"
/>
<div className="none absolute bottom-0 flex hidden h-12 w-full items-center rounded-b-md bg-zinc-800 px-3 font-bold text-white ease-in-out group-hover:flex dark:bg-white dark:text-zinc-800 ">
<Link href={`/details/${data.tokenId}`}>
<a className="hover:text-green-300 hover:underline">
{data.metadata.name}
</a>
</Link>
</div>
</div>
</div>
)
}
Details
This is a wrapper component, a dropdown to display the details from an NFT. The details themselfs can be anything that you want, and the component can either display them on a grid or not.
// components/Details/index.js
export default function Details({ summary, isGrid, children }) {
return (
<details className="group border border-zinc-100 p-3 hover:cursor-pointer dark:border-zinc-600">
<summary className="font-xl flex w-full list-none items-center justify-between font-bold">
{/* The title of the drop down */}
<span className="group-hover:underline">{summary}</span>
<div className="icon">
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth="2"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M19 9l-7 7-7-7"
/>
</svg>
</div>
</summary>
{/* if its children should be in a grid */}
{isGrid && (
<div className="grid grid-flow-row grid-cols-2 gap-4 pt-3 md:grid-cols-3 xl:grid-cols-4">
{children}
</div>
)}
{/* else */}
{!isGrid && (
<div className="pt-3 text-zinc-800 dark:text-zinc-50">{children}</div>
)}
</details>
)
}
Details tile
This component will be used inside the details component, and it will display properties and attributes from the NFT, is really simple.
// components/DetailTile/index.js
export default function DetailTile({ title, value }) {
return (
<div className="flex h-32 w-full flex-col items-center justify-center rounded-md border border-zinc-100 p-3 dark:border-zinc-600">
<p className="text-3xl text-zinc-800 dark:text-zinc-50">{value}</p>
<p className="text-xl font-bold text-green-500">{title}</p>
</div>
)
}
Add attributes form
This component will be used in the create NFT view, it will be responsible of handle the logic to add an attribute, an attribute is stored inside the metadata of an NFT, in the attributes array, which gives it unique properties and values.
// components/AddAttributes/index.js
import { useState } from 'react'
// A function to capitalize text
// Ex. capitalize("soME TeXT") => "Some Text"
const capitalize = (text) =>
text
.trim()
.toLowerCase()
.split(' ')
.map((word) => word[0].toUpperCase() + word.slice(1))
.join(' ')
export default function AddAttributes({ addAttribute }) {
// ERC-721 metadata attributes
// {
// "display_type": "boost_number",
// "trait_type": "Aqua Power",
// "value": 40
// }
const [displayType, setDisplayType] = useState('')
const [traitType, setTraitType] = useState('text')
const [value, setValue] = useState('')
function handleAddAttribute(e) {
e.preventDefault()
// if one field is empty return
if (!displayType || !traitType || !value) {
return
}
let data = { displayType: capitalize(displayType), traitType, value }
switch (data.traitType) {
case 'text': {
data.value = capitalize(data.value)
break
}
case 'boost_percentage': {
data.value = Number(data.value) + '%'
break
}
case 'boost_number':
case 'number': {
data.value = Number(data.value)
break
}
}
addAttribute(data)
// reset all fields
setDisplayType('')
setTraitType('text')
setValue('')
}
return (
<form className="flex h-16 w-full items-center space-x-3">
<div className="flex flex-grow flex-col">
<label
htmlFor="name"
className="text-sm uppercase text-zinc-500 dark:text-zinc-300"
>
Name
</label>
<input
className="placeholder-text-xl h-12 rounded-md border-2 border-zinc-100 bg-zinc-100 px-2 py-1 outline-none focus:border-green-400 dark:border-zinc-500 dark:bg-zinc-500"
id="name"
type="text"
value={displayType}
placeholder="Ex. Power"
onChange={(e) => setDisplayType(e.target.value)}
/>
</div>
<div className="flex flex-grow flex-col">
<label
htmlFor="traitType"
className="text-sm uppercase text-zinc-500 dark:text-zinc-300"
>
Trait Type
</label>
<select
className="placeholder-text-xl h-12 rounded-md border-2 border-zinc-100 bg-zinc-100 px-2 py-1 outline-none focus:border-green-400 dark:border-zinc-500 dark:bg-zinc-500"
name="traitType"
value={traitType}
onChange={(e) => setTraitType(e.target.value)}
>
<option value="text">Text</option>
<option value="boost_percentage">Boost Percentage</option>
<option value="boost_number">Boost Number</option>
<option value="number">Number</option>
</select>
</div>
<div className="flex flex-grow flex-col">
<label
htmlFor="value"
className="text-sm uppercase text-zinc-500 dark:text-zinc-300"
>
Value
</label>
<input
id="value"
className="placeholder-text-xl h-12 rounded-md border-2 border-zinc-100 bg-zinc-100 px-2 py-1 outline-none focus:border-green-400 dark:border-zinc-500 dark:bg-zinc-500"
type="text"
value={value}
placeholder="Ex. 25"
onChange={(e) => setValue(e.target.value)}
/>
</div>
<div className="flex w-12 flex-col">
<button
className="mt-5 flex h-12 w-12 items-center justify-center rounded-md bg-green-400 text-white hover:bg-green-500"
type="submit"
onClick={handleAddAttribute}
>
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={4}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12 6v6m0 0v6m0-6h6m-6 0H6"
/>
</svg>
</button>
</div>
</form>
)
}
Attributes table
This component will be use to display the attributes as we add them.
// components/AttributesTable/index.js
export default function AttributesTable({ attributes, removeAttribute }) {
// If there are no attributes, don't show anything
if (attributes.length === 0) {
return null
}
return (
<table className="w-full border border-zinc-100 dark:border-zinc-600">
<thead>
<tr>
<th className="border border-zinc-100 p-3 text-sm uppercase text-zinc-500 dark:border-zinc-600 dark:text-zinc-300">
Name
</th>
<th className="border border-zinc-100 p-3 text-sm uppercase text-zinc-500 dark:border-zinc-600 dark:text-zinc-300">
Display Type
</th>
<th className="border border-zinc-100 p-3 text-sm uppercase text-zinc-500 dark:border-zinc-600 dark:text-zinc-300">
Value
</th>
<th className="border border-zinc-100 p-3 text-sm uppercase text-zinc-500 dark:border-zinc-600 dark:text-zinc-300">
Remove
</th>
</tr>
</thead>
<tbody>
{attributes.map((attribute, i) => (
<tr key={i}>
<td className="border border-zinc-100 text-center dark:border-zinc-600">
{attribute.displayType}
</td>
<td className="border border-zinc-100 text-center lowercase text-zinc-400 dark:border-zinc-600 dark:text-zinc-300">
{attribute.traitType}
</td>
<td className="border border-zinc-100 text-center dark:border-zinc-600">
{attribute.value}
</td>
<td className="border border-zinc-100 text-center dark:border-zinc-600">
<button
className="my-1 rounded-md px-2 py-1 font-semibold text-red-500 hover:bg-red-500 hover:text-white"
onClick={() => removeAttribute(i)}
>
REMOVE
</button>
</td>
</tr>
))}
</tbody>
</table>
)
}
Well, that's it for components, we separated the logic between components and pages, because components should be small pieces of code, not aware of the global state, this makes them easy to debug and easy to test.
If you want to see how the code is structured, check out the code repository.
Pages
Here we will manage our context, our smart contracts, and all the logic that connects to something else. I don't expect you to be an expert with the Context API, but I would recommend you to take a look at how it works if you haven't already.
First, we will create a Web3 Context, which will contain all the information about the smart contracts and the connected wallet. Go ahead, and inside the client
folder create a file store/web3Context.js
.
import { createContext } from 'react'
export default createContext({
simpleMint: null,
signer: null,
address: null,
})
Now we will create the Theme Context, it may be a little more complex, but it only takes care of the app theme (light or dark).
export const getInitialTheme = () => {
if (typeof window !== 'undefined' && window.localStorage) {
const storedPrefs = window.localStorage.getItem('color-theme')
if (typeof storedPrefs === 'string') {
return storedPrefs
}
const userMedia = window.matchMedia('(prefers-color-scheme: dark)')
if (userMedia.matches) {
return 'dark'
}
}
return 'light' // light theme as the default;
}
export const rawSetTheme = (rawTheme) => {
const root = window.document.documentElement
const isDark = rawTheme === 'dark'
root.classList.remove(isDark ? 'light' : 'dark')
root.classList.add(rawTheme)
localStorage.setItem('color-theme', rawTheme)
}
_app page
This is a wrapper component, here we will put in place our contexts, and connect them to our web3 application. This page displays the NoWallet component if there is no wallet connected to the application.
It does a lot of things, so take your time to read the comments.
import { useState, useEffect } from 'react'
import Web3Modal from 'web3modal'
import { ethers } from 'ethers'
// components
import Navbar from '../components/Navbar'
import NoWallet from '../components/NoWallet'
// store and context
import { getInitialTheme, rawSetTheme } from '../store/themeContext'
import Web3Context, { getNetworkName } from '../store/web3Context'
// styles
import '../styles/globals.css'
// smart-contracts
import SimpleMint from '../artifacts/contracts/SimpleMint.json'
function App({ Component, pageProps }) {
// app theme
const [theme, setTheme] = useState(getInitialTheme)
// web3 dapp state
const [signer, setSigner] = useState(null)
const [address, setAddress] = useState(null)
const [simpleMint, setSimpleMint] = useState(null)
// sets the theme on change
useEffect(() => {
rawSetTheme(theme)
}, [theme])
async function connectWallet() {
const web3Modal = new Web3Modal()
const connection = await web3Modal.connect()
const provider = new ethers.providers.Web3Provider(connection)
const signer = provider.getSigner()
const address = await signer.getAddress()
const { chainId } = await provider.getNetwork()
const chainName = getNetworkName(chainId)
// this deployed simple mint smartcontract address
/// *** REPLACE THIS ***
const simpleMintAddress = '0x854b699d119c5f89681c96d282098e4420eDa135'
const simpleMintContract = new ethers.Contract(
simpleMintAddress,
SimpleMint.abi,
signer
)
setSigner(signer)
setAddress(address)
setSimpleMint(simpleMintContract)
}
return (
<div>
<Navbar
address={address}
connectWallet={connectWallet}
theme={theme}
setTheme={setTheme}
/>
{address && (
<Web3Context.Provider value={{ signer, address, simpleMint }}>
<Component {...pageProps} />
</Web3Context.Provider>
)}
{!address && <NoWallet />}
</div>
)
}
export default App
Index Page
This is the main page, here we display the NFTs that the user owns, and if the user hasn't minted any NFTs yet we will display the NoMints component.
// pages/index.js
import { useContext, useState, useEffect } from 'react'
import axios from 'axios'
import Head from 'next/head'
// store
import Web3Context from '../store/web3Context'
// components
import NFTCard from '../components/NFTCard'
import NoMints from '../components/NoMints'
export default function Home() {
const { simpleMint, address } = useContext(Web3Context)
const [nfts, setNfts] = useState([])
// once connected a wallet load the nfts
useEffect(() => {
if (simpleMint && address) {
loadNfts()
}
}, [simpleMint, address])
async function loadNfts() {
let nfts = await simpleMint.tokensOf(address)
// tokensOf returns a Token ID and a Token URI
// we need to retrive and parse that data
nfts = await Promise.all(
nfts.map(async (nft) => {
// token as returned from the smart-contract
let [metadata, tokenId] = nft
// parsing the token id
tokenId = tokenId.toString()
// fetching the metadata
metadata = await axios.get(metadata).then((res) => res.data)
return { metadata, tokenId }
})
)
setNfts(nfts)
}
return (
<div>
<Head>
<title>Simple Mint</title>
<meta name="description" content="NFT minting Dapp" />
<link rel="icon" href="/favicon.ico" />
</Head>
<div className="px-3 md:px-6">
<h1 className="text-3xl font-bold">NFTs</h1>
{nfts.length == 0 && <NoMints />}
{nfts.length != 0 && (
<div className="sm: grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-4 p-3">
{nfts.map((nft, i) => (
<NFTCard key={i} data={nft} />
))}
</div>
)}
</div>
</div>
)
}
Create page
This page gives the user a great UI to create an NFT, without code.
// pages/create.js
import { useContext, useState } from 'react'
import { create as ipfsHttpClient } from 'ipfs-http-client'
import { useRouter } from 'next/router'
import Head from 'next/head'
import Image from 'next/image'
// components
import AddAttributes from '../components/AddAttributes'
import AttributesTable from '../components/AttributesTable'
import Loading from '../components/Loading'
// store
import Web3Context from '../store/web3Context'
// IPFS access point
const client = ipfsHttpClient('https://ipfs.infura.io:5001/api/v0')
export default function Create() {
const { simpleMint, address } = useContext(Web3Context)
// we will use the router to change the view after creating the NFT
const router = useRouter()
const [name, setName] = useState('')
const [description, setDescription] = useState('')
const [attributes, setAttributes] = useState([])
const [imageUrl, setImageUrl] = useState(null)
const [uploading, setUploading] = useState(false)
const [loading, setLoading] = useState(false)
// simple function to remove an attribute
function removeAttribute(index) {
let newAttributes = []
for (let i = 0; i < attributes.length; i++) {
if (i == index) {
continue
}
newAttributes.push(attributes[i])
}
setAttributes(newAttributes)
}
async function uploadImage(event) {
try {
setUploading(true)
if (!event.target.files || event.target.files.length === 0) {
throw new Error('You must select an image to upload.')
}
const file = event.target.files[0]
const added = await client.add(file)
const url = `https://ipfs.infura.io/ipfs/${added.path}`
setImageUrl(url)
} catch (error) {
alert(error.message)
} finally {
setUploading(false)
}
}
async function createNft() {
// all data is required to create an NFT
if (!name && !description && attributes.length === 0 && !imageUrl) {
return
}
// collect all data into an object
const data = {
name,
image: imageUrl,
description,
attributes,
}
try {
setLoading(true)
// we parse the data as JSON before uploading it to IPFS
const added = await client.add(JSON.stringify(data))
const url = `https://ipfs.infura.io/ipfs/${added.path}`
// get the minting fee to mint the NFT
let fee = await simpleMint.fee()
fee = fee.toString()
// wait till the transaction is confirmed
const tx = await simpleMint.safeMint(url, { value: fee })
await tx.wait()
router.push('/')
} catch (error) {
alert(error.message)
} finally {
setLoading(false)
}
}
return (
<div>
<Head>
<title>Create | Simple Mint</title>
<meta name="description" content="NFT minting Dapp" />
<link rel="icon" href="/favicon.ico" />
</Head>
{loading && <Loading text="Processing" />}
<div className="px-3 md:px-6">
<h1 className="text-3xl font-bold">Create NFT</h1>
<div className="flex flex-col space-y-6 py-12">
<div className="flex flex-grow flex-col">
<label
htmlFor="name"
className="mb-1 text-sm uppercase text-zinc-500 dark:text-zinc-300"
>
Image
</label>
<input
className="
block w-full cursor-pointer text-sm
text-slate-500 file:mr-4 file:rounded-full
file:border-0 file:bg-green-400
file:py-2 file:px-4
file:text-sm file:font-semibold
file:text-white
hover:file:bg-green-500
"
type="file"
id="single"
accept="image/*"
onChange={uploadImage}
disabled={uploading}
/>
</div>
<div className="flex flex-grow flex-col">
<label
htmlFor="name"
className="mb-1 text-sm uppercase text-zinc-500 dark:text-zinc-300"
>
Loaded Image
</label>
{uploading && (
<div className="max-w-96 flex h-72 w-full animate-pulse items-center justify-center rounded-md bg-zinc-400">
<p>Loading...</p>
</div>
)}
{!uploading && imageUrl && (
<div className="max-w-96 relative h-72 rounded-md sm:w-72">
<Image
className="rounded-md"
layout="fill"
objectFit="cover"
quality={100}
src={imageUrl}
alt="text"
/>
</div>
)}
</div>
<div className="flex flex-grow flex-col">
<label
htmlFor="name"
className="text-sm uppercase text-zinc-500 dark:text-zinc-300"
>
Name
</label>
<input
className="placeholder-text-xl h-12 rounded-md border-2 border-zinc-100 bg-zinc-100 px-2 py-1 outline-none focus:border-green-400 dark:border-zinc-500 dark:bg-zinc-500 dark:focus:border-green-400"
id="name"
type="text"
value={name}
placeholder="Ex. Power"
onChange={(e) => setName(e.target.value)}
/>
</div>
<div className="flex flex-grow flex-col">
<label
htmlFor="description"
className="text-sm uppercase text-zinc-500 dark:text-zinc-300"
>
Description
</label>
<textarea
className="placeholder-text-xl rounded-md border-2 border-zinc-100 bg-zinc-100 px-2 py-1 outline-none focus:border-green-400 dark:border-zinc-500 dark:bg-zinc-500 dark:focus:border-green-400"
id="description"
type="text"
value={description}
placeholder="Ex. Lorem ipsum dolor sit amet."
onChange={(e) => setDescription(e.target.value)}
/>
</div>
<div className="flex flex-grow flex-col">
<AddAttributes
addAttribute={(data) => setAttributes((prev) => [...prev, data])}
/>
</div>
<div className="flex-grow">
<p className="pb-1 text-sm uppercase text-zinc-500 dark:text-zinc-300">
Attributes
</p>
<AttributesTable
attributes={attributes}
removeAttribute={removeAttribute}
/>
</div>
<div>
<button
onClick={createNft}
className="w-full cursor-pointer rounded-md bg-green-400 py-2 px-3 text-white hover:bg-green-500"
>
Create
</button>
</div>
</div>
</div>
</div>
)
}
Details page
This page is really self explenatory, it fetches and display data from an specific token. This page has a parameter, the token ID, check the next docs for more information.
// pages/details/[tokenId].js
import { useContext, useEffect, useState } from 'react'
import { useRouter } from 'next/router'
import Head from 'next/head'
import Image from 'next/image'
import axios from 'axios'
// components
import Details from '../../components/Details'
import DetailTile from '../../components/DetailTile'
import Loading from '../../components/Loading'
// store
import Web3Context from '../../store/web3Context'
export default function TokenDetails() {
const { simpleMint, address } = useContext(Web3Context)
const router = useRouter()
const { tokenId } = router.query
const [nft, setNft] = useState(null)
const [loading, setLoading] = useState(false)
useEffect(() => {
if (simpleMint && address) {
loadNft()
}
}, [simpleMint, address])
async function loadNft() {
try {
setLoading(true)
const tokenURI = await simpleMint.tokenURI(tokenId)
const metadata = await axios.get(tokenURI).then((res) => res.data)
setNft({ metadata, tokenId })
setLoading(false)
} catch (err) {
window.alert(err)
}
}
return (
<div>
<Head>
<title>Token: #{tokenId} | Simple Mint</title>
<meta name="description" content="NFT minting Dapp" />
<link rel="icon" href="/favicon.ico" />
</Head>
{loading && <Loading text="Loading" />}
{nft && (
<div className="px-3 md:px-6">
<h1 className="text-3xl font-bold">Token: #{nft.tokenId}</h1>
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
<div className="group relative h-96 rounded-md p-3 md:max-w-[100%]">
<div className="relative h-96 rounded-md">
<Image
className="rounded-md"
layout="fill"
objectFit="cover"
quality={100}
src={nft.metadata.image}
alt="text"
/>
</div>
</div>
<div className="p-3">
{/* NFT Name */}
<Details summary="Name">
<p>{nft.metadata.name}</p>
</Details>
{/* NFT Description */}
<Details summary="Description">
<p>{nft.metadata.description}</p>
</Details>
{/* NFT Properties, if there are no properties don't display */}
{nft.metadata.attributes.filter(
(attr) => attr.traitType == 'text' || attr.traitType == 'number'
).length !== 0 && (
<Details summary="Properties" isGrid>
{nft.metadata.attributes
.filter(
(attr) =>
attr.traitType == 'text' || attr.traitType == 'number'
)
.map((attr) => (
<DetailTile title={attr.displayType} value={attr.value} />
))}
</Details>
)}
{/* NFT Boosts, if there are no boosts don't display */}
{nft.metadata.attributes.filter(
(attr) =>
attr.traitType == 'boost_percentage' ||
attr.traitType == 'boost_number'
).length !== 0 && (
<Details summary="Boosts" isGrid>
{nft.metadata.attributes
.filter(
(attr) =>
attr.traitType == 'boost_percentage' ||
attr.traitType == 'boost_number'
)
.map((attr) => (
<DetailTile title={attr.displayType} value={attr.value} />
))}
</Details>
)}
</div>
</div>
</div>
)}
</div>
)
}
And that's all we needed to do with the front end.
Deploy
To deploy from the terminal/console, open the scripts/deploy.js
file, this file will be run by Brownie ETH to deploy the contract.
from brownie import SimpleMint, accounts, network
def main():
# requires brownie account to have been created
if network.show_active()=='development':
# add these accounts to metamask by importing private key
owner = accounts[0]
SimpleMint.deploy({'from':accounts[0]})
After this, you can run brownie run deploy
to deploy your Simple Mint smart contract, be sure to run Ganache before running this command and to change the smart contract address in the _app.js file in the front-end.
That's all you needed to deploy the simple mint program to a local blockchain, there are many tutorials on how to deploy to an actual blockchain, so if you want to do that feel free to try it out.
Thanks for reading, if you have any questions let me know in the comments.
Top comments (0)