Building a Classic Web3 NFT Minting Dapp With React and Solidity: Part 2

Written by daltonic | Published 2022/04/05
Tech Story Tags: web3 | web3-writing-contest | solidity | oct_network | react | web3.0 | nft-minting | nft

TLDRPart 1 of this tutorial explains how to use ReactJs to build an application for Bitcoin. Part 2 of the tutorial is based on the smart contract part of the application. Part 1 is written in Solidity, now it's time we merge it up with ReactJs. Part 3 of the project is built using ReactJs and Adulam. Part 4 of the code is the header component, the minted NFTs. Part 5 is the [live demo and [GitHub repo] for more info.via the TL;DR App

What you will be building, see the live demo and GitHub repo for more info, don’t forget to star the project.

Introduction

In PART ONE of this tutorial, we coded the smart contract part of this application with Solidity, now it's time we merge it up with ReactJs.

If you haven’t checked PART ONE of this tutorial, I recommend that you do that first before continuing with this second part.

If you’re getting value out of this tutorial and you want to go all-in with blockchain development then You can also contact me for lessons…

Let’s jump in and start coding…

Prerequisites

You must have completed PART ONE of this article in other to fully benefit from this part. If you haven’t, please quickly check PART ONE, blockchain development is no child’s play.

Building the Components

Let’s start with building out the components one step at a time, make sure you follow the steps accurately…

The Header Component Like always, we’ll start with the header component, this is the normal flow of any website or application.

This was beautifully crafted with tailwind CSS using the gradients styling. It simply enables a user to connect a wallet address for minting. In the project, go to your components folder and create a new file called Header.jsx. Afterward, paste the codes below inside of it.

import ethlogo from '../assets/ethlogo.png'
import { connectWallet } from '../Adulam'
import { useGlobalState } from '../store'

const Header = () => {
  const [connectedAccount] = useGlobalState('connectedAccount')
  return (
    <nav className="w-4/5 flex md:justify-center justify-between items-center py-4 mx-auto">
      <div className="flex flex-row justify-start items-center md:flex-[0.5] flex-initial">
        <img className="w-8 cursor-pointer" src={ethlogo} alt="Adulam Logo" />
        <span className="text-white text-2xl ml-2">Adulam</span>
      </div>

      <ul
        className="md:flex-[0.5] text-white 
        md:flex hidden list-none flex-row 
        justify-between items-center flex-initial"
      >
        <li className="mx-4 cursor-pointer">Explore</li>
        <li className="mx-4 cursor-pointer">Features</li>
        <li className="mx-4 cursor-pointer">Community</li>
      </ul>

      {connectedAccount ? null : (
        <button
          className="shadow-xl shadow-black text-white 
        bg-[#e32970] hover:bg-[#bd255f] md:text-xs p-2
        rounded-full cursor-pointer"
          onClick={connectWallet}
        >
          Connect Wallet
        </button>
      )}
    </nav>
  )
}

export default Header

That will be it for the header, let’s work on the Hero component.

The Hero Component

This component is responsible for initiating the minting process as you can see with the mint button. Also, it takes a record of the total number of NFTs minted against the ones remaining.

Here is the code snippet responsible for this operation…

import avatar from '../assets/owner.jpg'
import github from '../assets/github_icon.png'
import facebook from '../assets/facebook_icon.png'
import twitter from '../assets/twitter_icon.png'
import linkedIn from '../assets/linkedIn_icon.png'
import medium from '../assets/medium_icon.png'
import {
  setAlert,
  useGlobalState,
  setGlobalState,
  setLoadingMsg,
} from '../store'
import { BASE_URI, payForArt } from '../Adulam'

const Hero = () => {
  const [connectedAccount] = useGlobalState('connectedAccount')
  const [maxSupply] = useGlobalState('maxSupply')
  const [nfts] = useGlobalState('nfts')

  const mint = async () => {
    setGlobalState('loading', { show: true, msg: 'Retrieving IPFS data...' })
    const nextTokenIndex = Number(nfts.length + 1)

    fetch(`${BASE_URI + nextTokenIndex}.json`)
      .then((data) => data.json())
      .then((res) => {
        setLoadingMsg('Intializing transaction...')
        payForArt({ ...res, buyer: connectedAccount }).then((result) => {
          if (result) {
            setGlobalState('loading', { show: false, msg: '' })
            setAlert('Minting Successful...', 'green')
            window.location.reload()
          }
        })
      })
      .catch((error) => {
        setGlobalState('loading', { show: false, msg: '' })
        console.log(error)
      })
  }

  return (
    <div
      className="bg-[url('https://cdn.pixabay.com/photo/2022/03/01/02/51/galaxy-7040416_960_720.png')]
        bg-no-repeat bg-cover"
    >
      <div className="flex flex-col justify-center items-center mx-auto py-10">
        <div className="flex flex-col justify-center items-center">
          <h1 className="text-white text-5xl font-bold text-center">
            A.I Arts <br />
            <span className="text-gradient">NFTs</span> Collection
          </h1>

          <p className="text-white font-semibold text-sm mt-3">
            Mint and collect the hottest NFTs around.
          </p>

          <button
            className="shadow-xl shadow-black text-white
            bg-[#e32970] hover:bg-[#bd255f] p-2
            rounded-full cursor-pointer my-4"
            onClick={mint}
          >
            Mint Now
          </button>

          <a
            href="https://daltonic.github.io/"
            className="flex flex-row justify-center space-x-2 items-center
            bg-[#000000ad] rounded-full my-4 pr-3 cursor-pointer"
          >
            <img
              className="w-11 h-11 object-contain rounded-full"
              src={avatar}
              alt="Adulam Logo"
            />
            <div className="flex flex-col font-semibold">
              <span className="text-white text-sm">0xf55...146a</span>
              <span className="text-[#e32970] text-xs">Daltonic</span>
            </div>
          </a>

          <p className="text-white text-sm font-medium text-center">
            Gospel Darlington kick-started his journey as a software engineer in
            2016. <br /> Over the years, he has grown full-blown skills in
            JavaScript stacks such as <br /> React, ReactNative, VueJs, and now
            blockchain.
          </p>

          <ul className="flex flex-row justify-center space-x-2 items-center my-4">
            <a
              className="bg-white hover:scale-50 transition-all duration-75 delay-75 rounded-full mx-2"
              href="https://github.com/Daltonic"
            >
              <img className="w-7 h-7" src={github} alt="Github" />
            </a>
            <a
              className="bg-white hover:scale-50 transition-all duration-75 delay-75 rounded-full mx-2"
              href="https://www.linkedin.com/in/darlington-gospel-aa626b125"
            >
              <img className="w-7 h-7" src={linkedIn} alt="linkedIn" />
            </a>
            <a
              className="bg-white hover:scale-50 transition-all duration-75 delay-75 rounded-full mx-2"
              href="https://fb.com/darlington.gospel01"
            >
              <img className="w-7 h-7" src={facebook} alt="facebook" />
            </a>
            <a
              className="bg-white hover:scale-50 transition-all duration-75 delay-75 rounded-full mx-2"
              href="https://twitter.com/idaltonic"
            >
              <img className="w-7 h-7" src={twitter} alt="twitter" />
            </a>
            <a
              className="bg-white hover:scale-50 transition-all duration-75 delay-75 rounded-full mx-2"
              href="https://darlingtongospel.medium.com/"
            >
              <img className="w-7 h-7" src={medium} alt="medium" />
            </a>
          </ul>

          <div
            className="shadow-xl shadow-black flex flex-row
            justify-center items-center w-10 h-10 rounded-full
          bg-white cursor-pointer p-3 ml-4 text-black 
            hover:bg-[#bd255f] hover:text-white transition-all
            duration-75 delay-100"
          >
            <span className="text-xs font-bold">
              {nfts.length}/{maxSupply}
            </span>
          </div>
        </div>
      </div>
    </div>
  )
}

export default Hero

Next on our list is the artworks component…

The Artworks Component

This component is saddled with the responsibility of rendering the artworks one after the other. The tailwind CSS came through here for helping us design a stunning interface.

Let’s take a look at the codes responsible for these components’ behavior…

import ethlogo from '../assets/ethlogo.png'
import { useGlobalState } from '../store'
import { BASE_URI } from '../Adulam'

const Artworks = () => {
  const [nfts] = useGlobalState('nfts')

  const trucncate = (str, num = 20) => {
    if (str.length > num) {
      return str.slice(0, num) + "..."
    } else {
      return str
    }
  }

  return (
    <div className="bg-[#131835] py-10">
      <div className="w-4/5 mx-auto">
        <h4 className="text-gradient uppercase text-2xl">Artworks</h4>

        <div className="flex flex-wrap justify-start items-center mt-4">
          {nfts.map((nft) => (
            <div
              key={nft.id}
              className={`relative shadow-xl shadow-black p-3
                bg-white rounded-lg item w-64 h-64 object-contain
                bg-[url(${BASE_URI + nft.id}.webp)]
                bg-no-repeat bg-cover overflow-hidden mr-2 mb-2 cursor-pointer
                transition-all duration-75 delay-100 hover:shadow-[#bd255f]`}
              style={{backgroundImage: `url(${BASE_URI + nft.id}.webp)`}}
            >
              <div
                className="absolute bottom-0 left-0 right-0
                  flex flex-row justify-between items-center
                  label-gradient p-2 w-full text-white text-sm"
              >
                <p>
                  {nft.id}# {trucncate(nft.title)}
                </p>
                <div className="flex justify-center items-center space-x-2">
                  <img
                    className="w-5 cursor-pointer"
                    src={ethlogo}
                    alt="Adulam Logo"
                  />
                  {nft.cost}
                </div>
              </div>
            </div>
          ))}
        </div>

        <div className="flex flex-row justify-center items-center mx-auto mt-4">
          <button
            className="shadow-xl shadow-black text-white
            bg-[#e32970] hover:bg-[#bd255f] p-2
            rounded-full cursor-pointer my-4"
          >
            Load more
          </button>
        </div>
      </div>
    </div>
  )
}

export default Artworks

Let’s move on to adding the Footer component…

The Footer Component

If you appreciate good work, you will love this design. Tailwind CSS has enabled me to build beautiful components such as this. Hey, if you are interested, I could take you on a private teaching session on blockchain development, kindly see my offers here.

Coming back to this build, this current component lightly features a signature display of site brand and logo, nothing much to this component, however, I needed to include it in this tutorial.

Below is the code for it…

import ethlogo from '../assets/ethlogo.png'

const Footer = () => (
  <div className="w-full flex md:justify-center justify-between items-center flex-col p-4 gradient-bg-footer">
    <div className="w-full flex flex-col justify-between items-center my-4">
      <div className="flex flex-1 justify-evenly items-center flex-wrap sm:mt-0 mt-5 w-full">
        <p className="text-white text-base text-center mx-2 cursor-pointer">
          Explore
        </p>
        <p className="text-white text-base text-center mx-2 cursor-pointer">
          Features
        </p>
        <p className="text-white text-base text-center mx-2 cursor-pointer">
          Community
        </p>
      </div>

      <div className="flex flex-row justify-center items-center mt-2">
        <img src={ethlogo} alt="logo" className="w-8" />
        <span className="text-white text-xs">
          Adulam © 2016 - 2022 With Love ❤️ Daltonic
        </span>
      </div>
    </div>
  </div>
)

export default Footer

Fantastic, we are almost done with these components, let’s add up the last two…

The Alert Component

This component, as intuitive as it sounds is responsible for notifying us when our minting process is done. Again, it was handcrafted by the use of Tailwind CSS and some react Icons.

Let’s take a look at the codes exhibiting its behavior…

import { useGlobalState } from '../store'
import { FaRegTimesCircle } from 'react-icons/fa'
import { BsCheck2Circle } from 'react-icons/bs'

const Alert = () => {
  const [alert] = useGlobalState('alert')

  return (
    <div
      className={`fixed top-0 left-0 w-screen h-screen
      flex items-center justify-center bg-black 
      bg-opacity-50 transform transition-transform
      duration-300 ${alert.show ? 'scale-100' : 'scale-0'}`}
    >
      <div
        className="flex flex-col justify-center items-center
        bg-[#151c25] shadow-xl shadow-[#e32970] rounded-xl
        min-w-min py-3 px-10"
      >
        {alert.color == 'red' ? (
          <FaRegTimesCircle className="text-red-600 text-4xl" />
        ) : (
          <BsCheck2Circle className="text-green-600 text-4xl" />
        )}
        <p className="text-white my-3">{alert.msg}</p>
      </div>
    </div>
  )
}

export default Alert

Nice, let's complete these components by adding the Loader component into the mix.

The Loader Component

This component simply displays a spinner that also shows the current progress of the NFT as it is being minted.

The state management library react-global-hooks manages the activities that occur under the hood here; more on this later.

Here is the code for this component...

import { useGlobalState } from '../store'

const Loading = () => {
  const [loading] = useGlobalState('loading')

  return (
    <div
      className={`fixed top-0 left-0 w-screen h-screen
      flex items-center justify-center bg-black 
      bg-opacity-50 transform transition-transform
      duration-300 ${loading.show ? 'scale-100' : 'scale-0'}`}
    >
      <div
        className="flex flex-col justify-center
        items-center bg-[#151c25] shadow-xl 
        shadow-[#e32970] rounded-xl 
        min-w-min px-10 pb-2"
      >
        <div className="flex flex-row justify-center items-center">
          <div className="lds-dual-ring scale-50"></div>
          <p className="text-lg text-white">Minting...</p>
        </div>
        <small className="text-white">{loading.msg}</small>
      </div>
    </div>
  )
}

export default Loading

Awesome, now that we’re done with coding the components, let’s dive into App.jsx and couple them together.

The App Component

This component is responsible for connecting all other components to be used in this project, let’s look at how it is coded.

import Alert from './components/Alert'
import Artworks from './components/Artworks'
import Footer from './components/Footer'
import Header from './components/Header'
import Hero from './components/Hero'
import Loading from './components/Loading'
import { loadWeb3 } from './Adulam'
import { useEffect } from 'react'

const App = () => {
  useEffect(() => loadWeb3(), [])

  return (
    <div className="min-h-screen">
      <div className="gradient-bg-hero">
        <Header />
        <Hero />
      </div>
      <Artworks />
      <Footer />
      <Loading />
      <Alert />
    </div>
  )
}

export default App

We are not quite done yet, let’s include other essential configurations.

The Index Files

Please make sure that your index.jsx and index.css are having the configurations as seen in the code snippet below.

@import url('https://fonts.googleapis.com/css2?family=Open+Sans:wght@300;400;500;600;700&display=swap');

* html {
  padding: 0;
  margin: 0;
  box-sizing: border-box;
}

body {
  margin: 0;
  font-family: 'Open Sans', sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

.gradient-bg-hero {
  background-color: #131835;
  background-image: radial-gradient(
      at 0% 0%,
      hsl(231deg 47% 14%) 0%,
      transparent 50%
    ),
    radial-gradient(at 50% 0%, hsl(333deg 85% 53%) 0, transparent 50%),
    radial-gradient(at 100% 0%, hsla(339, 49%, 30%, 1) 0, transparent 50%);
}

.gradient-bg-artworks {
  background-color: #0f0e13;
  background-image: radial-gradient(
      at 50% 50%,
      hsl(302deg 25% 18%) 0,
      transparent 50%
    ),
    radial-gradient(at 0% 0%, hsla(253, 16%, 7%, 1) 0, transparent 50%),
    radial-gradient(at 50% 50%, hsla(339, 39%, 25%, 1) 0, transparent 50%);
}

.gradient-bg-footer {
  background-color: #131835;
  background-image: radial-gradient(
      at 20% 100%,
      hsl(333deg 85% 53%) 0,
      transparent 40%
    ),
    radial-gradient(at 50% 120%, hsla(339, 49%, 30%, 1) 0, transparent 40%);
}

.text-gradient {
  background: -webkit-linear-gradient(#eee, #333);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
}

.label-gradient {
  background: rgb(19, 24, 53);
  background: linear-gradient(
    31deg,
    rgba(19, 24, 53, 1) 0%,
    rgba(237, 33, 124, 0) 100%
  );
}

.no-scrollbar::-webkit-scrollbar {
  display: none;
}

.lds-dual-ring {
  display: inline-block;
}
.lds-dual-ring:after {
  content: ' ';
  display: block;
  width: 64px;
  height: 64px;
  margin: 8px;
  border-radius: 50%;
  border: 6px solid #fff;
  border-color: #fff transparent #fff transparent;
  animation: lds-dual-ring 1.2s linear infinite;
}
@keyframes lds-dual-ring {
  0% {
    transform: rotate(0deg);
  }
  100% {
    transform: rotate(360deg);
  }
}

@tailwind base;
@tailwind components;
@tailwind utilities;

import React from 'react'
import ReactDOM from 'react-dom'
import { BrowserRouter } from 'react-router-dom'
import './index.css'
import App from './App'

ReactDOM.render(
  <BrowserRouter>
    <App />
  </BrowserRouter>,
  document.getElementById('root')
)

Fantastic, are there two more important files you must be aware of, let’s look at them…

The Adulam Blockchain Interface

For you to interact with our deployed smart contract, you need to access it via functions. The codes below enable us to interact with our smart contract which is now running on a live blockchain network. Create a file named Adulam.jsx in the src folder of this project and paste the following codes into it.

import Web3 from 'web3'
import {
  setAlert,
  setGlobalState,
  getGlobalState,
  setLoadingMsg,
} from './store'
import Adulam from './abis/Adulam.json'

const { ethereum } = window
const BASE_URI =
  'https://bafybeidfpvjszubegtoomoknmc7zcqnay7noteadbwxktw46guhdeqohrm.ipfs.infura-ipfs.io/'

const payForArt = async (art) => {
  try {
    const web3 = window.web3
    const buyer = art.buyer
    const title = art.title
    const description = art.description
    const cost = web3.utils.toWei('0.01', 'ether')

    const contract = await getGlobalState('contract')
    setLoadingMsg('NFT minting in progress...')

    await contract.methods
      .payToMint(title, description)
      .send({ from: buyer, value: cost })

    setLoadingMsg('Minting successful...')

    return true
  } catch (error) {
    setAlert(error.message, 'red')
  }
}

const connectWallet = async () => {
  try {
    if (!ethereum) return alert('Please install Metamask')
    const accounts = await ethereum.request({ method: 'eth_requestAccounts' })
    setGlobalState('connectedAccount', accounts[0])
  } catch (error) {
    setAlert(JSON.stringify(error), 'red')
  }
}

const loadWeb3 = async () => {
  try {
    if (!ethereum) return alert('Please install Metamask')

    window.web3 = new Web3(ethereum)
    await ethereum.enable()

    window.web3 = new Web3(window.web3.currentProvider)

    const web3 = window.web3
    const accounts = await web3.eth.getAccounts()
    setGlobalState('connectedAccount', accounts[0])

    const networkId = await web3.eth.net.getId()
    const networkData = Adulam.networks[networkId]

    if (networkData) {
      const contract = new web3.eth.Contract(Adulam.abi, networkData.address)
      const nfts = await contract.methods.getAllNFTs().call()

      setGlobalState('nfts', structuredNfts(nfts))
      setGlobalState('contract', contract)
    } else {
      window.alert('Adulam contract not deployed to detected network.')
    }
  } catch (error) {
    alert('Please connect your metamask wallet!')
  }
}

const structuredNfts = (nfts) => {
  const web3 = window.web3
  return nfts
    .map((nft) => ({
      id: nft.id,
      to: nft.to,
      from: nft.from,
      cost: web3.utils.fromWei(nft.cost),
      title: nft.title,
      description: nft.description,
      timestamp: nft.timestamp,
    }))
    .reverse()
}

export { loadWeb3, connectWallet, payForArt, BASE_URI }

This is such a handy function structure that you should consider using in your subsequent blockchain project. It keeps all the blockchain-related functions together and helps us keep our sanity.

Next, let’s discuss how our little but not so little state management library is coordinating these entire activities behind the scene.

The Statement Management Library

We’re using the react-global-hook package for our state management. Setting up redux for a small project like this can be cumbersome, and why should you when you have an implementation so simple as the one below?

Create a folder inside the src directory called the store and also create a file named index.jsx within it, now paste the codes below in the file and save.

import { createGlobalState } from 'react-hooks-global-state'

const { setGlobalState, useGlobalState, getGlobalState } = createGlobalState({
  alert: { show: false, msg: '', color: '' },
  loading: { show: false, msg: '' },
  contract: null,
  maxSupply: 100,
  connectedAccount: '',
  nfts: [],
})

const setAlert = (msg, color = 'green') => {
  setGlobalState('alert', { show: true, msg, color })
  setTimeout(() => {
    setGlobalState('alert', { show: false, msg: '', color })
    setGlobalState('loading', false)
  }, 8000)
}

const setLoadingMsg = (msg) => {
  const loading = getGlobalState('loading')
  setGlobalState('loading', { ...loading, msg })
}

export {
  useGlobalState,
  setGlobalState,
  getGlobalState,
  setAlert,
  setLoadingMsg,
}

We are almost done here…

The ABIs folder and files

Let me direct your attention to this folder which should not be empty by now… During PART ONE of this article, we specified in truffle-config.js to create these files in this folder whenever we compile a smart contract, that’s why we’re having that folder available to us.

The Assets Files

I must say that we’re just about done, except that we haven’t included the assets folder and files. Let’s quickly do that…

Create a folder in the src directory called assets, next, download and move the file below inside of it.

Use this link to the git repo to download the images.

Now that we are done with all the builds, let’s start up the server to go live by running the command below on the terminal to do this!

yarn start #starts the server on localhost:3000

Congratulations, you are officially done with this build…

Conclusion

You have seen another classic example of how to build a web3 application. I firmly believe that if you've been coding along with me, you are one of the blockchain armies the decentralized internet is looking for.

I’m currently teaching blockchain development online, if you want to go deeper with this skill, You can reach me on my website.

Till next time, all the best!

About the Author

Gospel Darlington kick-started his journey as a software engineer in 2016. Over the years, he has grown full-blown skills in JavaScript stacks such as React, ReactNative, VueJs, and now blockchain.

He is currently freelancing, building apps for clients, and writing technical tutorials teaching others how to do what he does.

Gospel Darlington is open and available to hear from you. You can reach him on LinkedIn, Facebook, Github, or on his website.


Written by daltonic | Youtuber | Blockchain Developer | Writer | Instructor
Published by HackerNoon on 2022/04/05