DEVELOPER DAO
 
ACADEMY

Lesson 5: Connecting to a Frontend

If you get stuck or have questions please visit our forum.

If you've been through the previous lessons we learnt how to write smart contracts, use token standards, and test driven development, as well. But to create a full stack decentralised application, we also need a frontend for users to interact with. In this lesson that is exactly what we're going to create.

We shall use:

and create a basic NFT minting application that displays the three tiers of NFTs created in our smart contract and gives users a one-click option to mint them. Once minted, users will be redirected to a page that displays information of their mint.

Let's dive in!



Getting started

In the past, you've created a smart contract using Remix, you've shifted to a dedicated code editor environment and deployed to an actual test network. In lesson 3 you added tiers to your NFTs and finally learnt to make your contracts airtight using tests in lesson 4. Now we're going use all of this knowledge and connect it to a frontend for users to see and interact with.

We're going to start where we left off in lesson 4. Open your code editor and cd into the TierNFT project directory.

For all things related to our frontend we are going to create a new sub-directory within our project's root directory to maintain good practice, keep our code clean, and easy to locate and read.

The first step is to initiate a nextJS application inside the project's root directory by running the following command.

💡

Make sure you are in your root directory before running the command.

The command creates a sub-directory named frontend inside our root directory and sets up all the necessary files and folders within it. The create-next-app command uses the yarn package manager by default. But be careful...

There is a chance that a different package manager has been used for your previous project set up, so check that out and choose your command accordingly:

# default for yarn 
npx create-next-app frontend

# possible alternative for npm
npx create-next-app frontend --use-npm

Once the setup is complete, we open the frontend directory and get cracking.

cd frontend


The Frontend

For users to easily interact with our smart contract and mint the NFTs we must provide an interface. A simple process that is easily accessible.

Let's start by creating a way to handle wallet connections.

The Main Application

We will use RainbowKit for handling the wallet connections. It provides us the ability to handle wallet connections, support for numerous wallets and a customisable ConnectButton out-of-the-box among other features. It relies on the WAGMI and ethers libraries for interoperability between various chains and wallet providers and thus needs some configurations.

But first we need to install the necessary dependencies.

yarn add @rainbow-me/rainbowkit wagmi ethers

# or 

npm install @rainbow-me/rainbowkit wagmi ethers

To get this setup we need to start working on the _app.js file in the /frontend/pages directory. The file should look something like this from our initial setup.

import '../styles/globals.css'

function MyApp({ Component, pageProps }) {
  return <Component {...pageProps} />
}

export default MyApp

We need to add some additional imports to create our configurations.

// previous imports...
import '@rainbow-me/rainbowkit/styles.css';
import {
  getDefaultWallets,
  RainbowKitProvider,
} from '@rainbow-me/rainbowkit';
import {
  chain,
  configureChains,
  createClient,
  WagmiConfig,
} from 'wagmi';
import { alchemyProvider } from 'wagmi/providers/alchemy';
import { publicProvider } from 'wagmi/providers/public';
import { useEffect, useState } from 'react';

// function MyApp code...
💡

Hooks are JavaScript functions that manage the state's behaviour and side effects by isolating them from a component.

After importing, we create the configurations we need, and create a new instance of wagmiClient using them.

// import statements code...

const { chains, provider } = configureChains(
  [chain.polygonMumbai],
  [
    alchemyProvider({ apiKey: process.env.API_KEY }),
    publicProvider()
  ]
);

const { connectors } = getDefaultWallets({
  appName: 'My RainbowKit App',
  chains
});

const wagmiClient = createClient({
  autoConnect: true,
  connectors,
  provider
})

// function MyApp code...

For this application we will continue using the mumbai testnet. Other chains can be added simply using the following syntax: chain.chain_name. For custom providers, like the alchemyProvider we can pass in our private apiKey as well.

Set your API Key in a new file named .env inside our frontend directory as follows:

API_KEY='YOUR-API-KEY-FROM-PROVIDER'

Now we can wrap our application in the RainbowKitProvider and WagmiConfig so that have access to their features throughout our application. Our code should look like this:

// import statements and configs code...

function MyApp({ Component, pageProps }) {
  return (
    <WagmiConfig client={wagmiClient}>
      <RainbowKitProvider chains={chains}>
        <Component {...pageProps} />
      </RainbowKitProvider >
    </WagmiConfig >
  );
};

export default MyApp

Now that we have our application setup, we can edit the Index Page.



The Index Page

The index.js page in the /frontend/pages directory should look like this after deleting all the children within the <main> element and completely deleting the <footer>.

import Head from 'next/head'
import Image from 'next/image'
import styles from '../styles/Home.module.css'

export default function Home() {
  return (
    <div className={styles.container}>
      <Head>
        <title>Create Next App</title>
        <meta name="description" content="Generated by create next app" />
        <link rel="icon" href="/favicon.ico" />
      </Head>
      <main className={styles.main}>
      </main>
    </div>
  )
}

Before we dive into the code for our index page, let's copy and paste some styling from the side drawer below, into our file. This is some default styling created for the application, which you can play around with themes, responsiveness, etc., if needed.


Moving onto imports.

// prev imports...

import { ConnectButton } from '@rainbow-me/rainbowkit';
import { useAccount, useContractRead, useContractWrite } from 'wagmi';
import TierABI from '../../artifacts/contracts/TierNFT.sol/TierNFT.json';
import styles from '../styles/Home.module.css';
import { ethers } from 'ethers';
import { useEffect, useState } from 'react';

// function Home() code...
export default function Home() {
  const CONTRACT_ADDRESS = /*Enter Contract Address between ''*/;

  const { isConnected } = useAccount();

  const [isUserConnected, setIsUserConnected] = useState(false);
  const [latestNFTMinted, setLatestNFTMinted] = useState(0);
  const [modalShow, setModalShow] = useState(false);
  const [isMinting, setIsMinting] = useState(false);

  // More code to come...
}

We're creating a default function called Home that will render our home/ index page. As you can see , we are required to add the CONTRACT_ADDRESS which is displayed in your command line when we deploy the contract. The useAccount hook lets us access account data and check connection status. We will use this as a check to provide access to the minting features of our app. A few useStates let us store state information such as whether a user is connected. The general syntax for using the useState hook is:

const [variableName, functionToSetVariable] = useState(intialValue)

💡

The initial value also serves as the type for the value assigned. If the initial value is a string, then the values set later must be strings too. The hook can be created without an initial value as well but a good practice is to always choose and pass in an initial value based on the value type we expect to assign to that variable.

We will revisit these state variables later to see their implementation.

Header

We need a <header> that displays our application name and let's us connect our wallet.

export default function Home() {
  // variable definitions...

  return (
      <div className={styles.container}>
        
        {/* <Head> element... */}

        <header style={header}>
          <h1>TierNFTs</h1>
          <ConnectButton />
        </header>

        {/* <main> element */}

      </div>
  )
}

// styling code...

Since we care about accessibility, we ensure we are using only a single <h1> element on a page and the correct sub heading elements as well. The ConnectButton is a direct import from RainbowKit that is generated using the configurations setup previously in our Main Application.

With this setup we can run the npm run dev or yarn dev command in our console and click the returned link (http://localhost:portnumber) to preview our application. This is a good way to debug and see if our code is behaving as expected.

💡

Make sure you are in the frontend directory before running the command.

The preview should look like this:

App Header Preview

NFT Cards

Next we need to create some cards that display the 3 tiers of NFTs we created in our smart contract and provide a way to mint them.

export default function Home() {
  // variable definitions...

  const {
      data: mintData,
      writeAsync: mint,
      isLoading: isMintLoading,
    } = useContractWrite({
      addressOrName: CONTRACT_ADDRESS,
      contractInterface: TierABI.abi,
      functionName: "mint",
    });

 // return statement for rendering... 

}

The useContractWrite hook from WAGMI let's us pass in the contract address, contract ABI and the function name to return a JavaScript function (that we have called mint) that can interact with our smart contract and trigger a write function in it.

How cool is that?!

It also returns some additional features for error handling and checking whether the function is running or completed. We are assigning well defined names to the returned values for easily calling them later. For example, the data returned is called mintData.

Even though we have this function, we need to pass in custom message values i.e. an amount in ETH, as defined in our contract. For this we create our own mintToken function that calls the mint function from WAGMI within it.

// WAGMI useContractWrite hook...

const mintToken = async (e) => {
    try {
      let mintTxn = await mint({
        recklesslySetUnpreparedOverrides: {
          value: ethers.utils.parseEther(e.target.value),
        }
      });
      await mintTxn.wait();
      console.log("This is the mint data", mintData);
    } catch (error) {
      console.log("Error minting NFT", error.message);
    }
  };

// function Home() code...

Then we create a new folder named nfts in the /public directory for saving Tier SVGs. You can find links for downloading the SVGs here.

Save them with the same names as shown in the links above for smooth flow in the code.

💡

Make sure you have downloaded the SVG file and not just saved the URL of the file.

Now we can render our NFT Cards.

// return statement for rendering and <header> element...

        <main className={styles.main}>
          <div style={NFTFlex}>
            <div style={NFTCard}>
              Tier 0
              <Image
                src='/nfts/0_basic.svg'
                width='200px'
                height='200px' />
              <button
                value='0.01'
                onClick={(e) => mintToken(e)}
                style={NFTMint}
                disabled={isMintLoading}>
                Mint
              </button>
            </div>
            <div style={NFTCard} >
              Tier 1
              <Image
                src='/nfts/1_medium.svg'
                width='200px'
                height='200px' />
              <button
                value='0.02'
                onClick={(e) => mintToken(e)}
                style={NFTMint}
                disabled={isMintLoading}>
                Mint
              </button>
            </div>
            <div style={NFTCard} >
              Tier 2
              <Image
                src='/nfts/2_premium.svg'
                width='200px'
                height='200px' />
              <button
                value='0.05'
                onClick={(e) => mintToken(e)}
                style={NFTMint}
                disabled={isMintLoading}>
                Mint
              </button>
            </div>
          </div>
        </main>

// rest of the code...

Let's see what is happening in each NFT Card by taking Tier 0 as an example.

<div style={NFTCard}>
  Tier 0
  <Image
    src='/nfts/0_basic.svg'
    width='200px'
    height='200px' />
  <button
    value='0.01'
    onClick={(e) => mintToken(e)}
    style={NFTMint}
    disabled={isMintLoading}>
    Mint
  </button>
</div>

Great but anyone can view and call our mint functions in the frontend of our app even if their wallet is not connected to the application. This could result in some errors. We must check if a user is connected before we let them interact with our application simply by using the isConnected utility from Wagmi and setting setting its value in our isUserConnected state.

// consts...

  useEffect(() => {
    try {
      setIsUserConnected(isConnected);
    } catch (error) {
      console.log("Error connecting to user", error.message);
    }
  }, [isConnected]);

// function Home() code...

The useEffect hook runs the functions within it everytime the value of its dependencies (isConnected, in this case) changes.

Then we can wrap the <main> element as follows:

// return statement for rendering...

      {isUserConnected ?
        <main className={styles.main}>
          
          {/* NFT Flex div code here... */}

        </main>
        :
        <main className={styles.main}>
          <div>Please connect your wallet.</div>
        </main>
      }

// closing <div> and styling consts...

This type of syntax (condition ? ifTrueDoThis : ifFalseDoThis) is known as a ternary operator. It checks whether the condition is true or false and returns one of two values accordingly. In our case, we check if the the user is connected and show them the mint options if true, else we ask them to connect their wallet.

Now our preview should look like this once a wallet is connected:

NFT Cards Preview

Woohoo! You've done a lot so far. Try minting these NFTs and check your wallet's profile on https://testnets.opensea.io/YOUR_WALLET_ADDRESS. Refer to the following side drawer for the code written till now if stuck somewhere.


Take a breather and stretch yourself before we do the last bit of coding.

Successful Mint Modal

Doesn't it feel like we are still missing something?

Right! We want our users to see information when they minting. We want them to know when a mint is in progress and we want them to see the result once the mint is successful.

But what is this modal. It is a an element that is usually used as an overlay that provides its own interaction environment separate from the page below it. This interaction environment provides a focus element where only options from this can be selected while it is active.

Okay lets write some code for it.

To be able to display information about our latest mint, we must first get information about it from our smart contract.

// imports, consts and WAGMI useContractWrite hook...

  const {
    data: tokenData,
    refetch: refetchTokenData,
  } = useContractRead({
    addressOrName: CONTRACT_ADDRESS,
    contractInterface: TierABI.abi,
    functionName: "totalSupply",
    watch: true,
  })

  const {
    data: tokenURI,
  } = useContractRead({
    addressOrName: CONTRACT_ADDRESS,
    contractInterface: TierABI.abi,
    functionName: "tokenURI",
    args: tokenData,
    watch: true,
  })

  // useEffect to check user connection...

  useEffect(() => {
    try {
      if (tokenURI) {
        setLatestNFTMinted(JSON.parse(window.atob(tokenURI.substring(tokenURI.indexOf(',') + 1))));
      }
    } catch (error) {
      console.log("Error fetching token URI", error.message);
    }
  }, [tokenData, tokenURI]);

  const mintToken = async (e) => {
    try {
      let mintTxn = await mint({
        recklesslySetUnpreparedOverrides: {
          value: ethers.utils.parseEther(e.target.value),
        }
      });
      await mintTxn.wait();
      console.log("This is the mint data", mintData);
      refetchTokenData();
      
      // catch statement for mintToken function...
  };

  // return statement...

We're almost there. We just need to do two more checks before we actually look at our modal. The first is that we must display our modal only when we want it to and the other is to display a loading message until our NFT has successfully minted. This is how the mintToken function should look now:

// useEffect for tokenURI...

  const mintToken = async (e) => {
    try {
      setIsMinting(true);
      setModalShow(true);
      let mintTxn = await mint({
        recklesslySetUnpreparedOverrides: {
          value: ethers.utils.parseEther(e.target.value),
        }
      });
      await mintTxn.wait();
      console.log("This is the mint data", mintData);
      refetchTokenData();
      setIsMinting(false);
    } catch (error) {
      console.log("Error minting NFT", error.message);
    }
  };

// return statement...

When we trigger the mintToken function we want to set the state to isMinting. Based on this condition, we can inform the users that the mint is in progress. We also want our modal to pop up right after this so we set the value of modalShow to true.

We're there at last! Now we can render our modal.

// return statement for rendering...

      {isUserConnected ?
        <main className={styles.main}>
          <div style={NFTFlex}>

            {/* NFT Card div code... */}

          </div>
          {modalShow && (
            <div style={modal}>
              {isMinting ?
                <div style={modalContent}>
                  <h2>Minting...</h2>
                </div>
                :
                <div style={modalContent}>
                  <h2>Mint Successful</h2>
                  <div style={modalBody}>
                    <h3>{latestNFTMinted.name}</h3>
                    <Image src={latestNFTMinted.image} height='200px' width='200px' />
                  </div>
                  <div style={modalFooter}>
                    <button style={modalButton}>
                      <a href={`https://testnets.opensea.io/assets/mumbai/${CONTRACT_ADDRESS}/${tokenData}`} target="_blank">
                        View on OpenSea
                      </a>
                    </button>
                    <button style={modalButton}>
                      <a href={`https://mumbai.polygonscan.com/tx/${mintData.hash}`} target="_blank">
                        View on Polygonscan
                      </a>
                    </button>
                    <button onClick={() => setModalShow(false)} style={modalButton}>Close</button>
                  </div>
                </div>
              }
            </div>
          )}
        </main> 

// closing <div> and styling consts...

This may seem like a lot of code but we're going to look at it in bits:

The final modal should look like this:

Successful Mint Modal Preview

If you are stuck somewhere, please refer to the code side drawer below.




Fin.

Hoorah! We've done it. Pat yourselves on the back and get some ice cream!

Right from learning the basics of smart contract development using solidity to creating a full stack decentralised NFT Minting Application.

Stay tuned for more!