Build an AI NFT Minting dApp - Part 2

ยท

12 min read

So in the first part of the tutorial. We completed building the backend for our dApp.

Now, it's time to build a front end to interact with our smart contract.

If you completed the first tutorial, then you have already setup the folder structure for our frontend.

Starting off, let's build our home page. So inside client/pages directory remove all that is there in the index.js file and paste this.

import Head from "next/head";
import Image from "next/image";
import styles from "../styles/Home.module.css";
import { lazy, useState } from "react";
import axios from "axios";
import ai from "../public/ai.png"
import {useRouter} from "next/router"
export default function Home() {

// keep track of user's prompts
  const [prompt, setPrompt] = useState("");
// keep track of loading
  const [loading, setLoading] = useState(false)
// keep track of the generated images
  const [images, setImages] = useState([]);
// keep track of whether there is an error or not
  const [isError, setIsError] = useState(false)
  const router = useRouter();

// function to generate image after user has entered their prompt
  const submitHandler = async () => {
    try {
      setImages([])
      setLoading(true)
      const res = await axios.get(`/api/${prompt}`);
      setPrompt("");
      const imageData = res.data.imageData;

      const imageURLs = imageData.map((el) => b64ToFile(el.b64_json));

      setLoading(false)

      storeToLocalStorage(imageData);
      setImages(imageURLs);
    } catch (error) {
      setLoading(false)
      setIsError("Error Fetching Images");
      console.log("Error", error);
    }
  };


// function to convert the image blob to a file.
  const b64ToFile = (b64String) => {
    const binaryData = atob(b64String);

    // Create an array containing the binary data
    const byteArray = new Uint8Array(binaryData.length);
    for (let i = 0; i < binaryData.length; i++) {
      byteArray[i] = binaryData.charCodeAt(i);
    }

    // Create a Blob containing the binary data
    const blob = new Blob([byteArray], { type: "image/png" });

    const file = new File([blob], "newImage.png", {type: "image/png"})

    // Create a URL for the Blob
    const url = URL.createObjectURL(file);

    return url;
  }

// function to store the generated images in the localStorage of the user to fetch them faster
  const storeToLocalStorage = (arr) => {
    arr.map((el, i) => {
      localStorage.setItem(`url${i+1}`, el.b64_json);
    })
  }

  const redirectToMint = (e) => {
    const id = e.target.getAttribute('id');
    const imageId = e.target.getAttribute('id');
    router.push({pathname: "/mint", query: { imageId: id }})
    console.log("Image Id -->", imageId);

  }

  return (
    <div>
      <main className={styles.main}>

      <div className={styles.promptBar_container}>
        <input
          className={styles.promptBar}
          onChange={(e) => setPrompt(e.target.value)}
          placeholder="Enter Your Prompt"
          value={prompt}
        />
        <button onClick={submitHandler} className={styles.generate_btn}>
          Generate Image
        </button>
      </div>
      <p className={styles.user_guide_txt}>{images.length > 0 ? "Select the Image for which you want to mint an NFT" :"Generate Images by Entering Prompts in the above bar"}</p>

        <div className={styles.bottom_section}>
          {images.length > 0 ? (
            <div className={styles.images_container}>
              {
                images.map((el, i) => <Image key={i+1}  id={i+1} onClick={redirectToMint} alt="prompt images" src={el} width={400} height={400} className={styles.prompt_img} />)
              }
            </div>
          ): (
            <div className={styles.text_container}>
              {loading || isError ? ( loading ? <p className={styles.cta_txt}>Loading...Fetching Images</p> : <p className={`${styles.cta_txt} ${styles.red_text}`}>Error Fetching Images</p>   ) : (
                <div className={styles.cta_container}>
              <h1 className={styles.cta_txt}>AI NFT Minting App</h1>
              <Image alt="ai robot" src={ai} width={400} height={400} className={styles.cta_img} />
              </div>
              ) }

            </div>
          )}
        </div>
      </main>
    </div>
  );
}

Now that is for the home page. Now after user has generated the image, we want them to redirect to the mint page. So, inside the pages directory, create a new file called mint.js and paste the following:

import Image from "next/image";
import { useEffect, useState } from "react";
import { useRouter } from "next/router";


import { uploadFileToIPFS } from "../utils/pinata";


import styles from "../styles/Mint.module.css";
import { useAiMintsContext } from "../utils/AiMintsContext";

const Mint = () => {
  const [image, setImage] = useState(null);
  const [imageFile, setImageFile] = useState(null);
  const [btnText, setBtnText] = useState("Mint NFT");
  const [isWhitelisted, setIsWhitelisted] = useState(false)


  const router = useRouter();
  const { mintNFT, checkIfWhitelisted, nftsMinted, connectWallet } = useAiMintsContext();

  const b64ToFile = (b64String) => {
    const binaryData = atob(b64String);

    // Create an array containing the binary data
    const byteArray = new Uint8Array(binaryData.length);
    for (let i = 0; i < binaryData.length; i++) {
      byteArray[i] = binaryData.charCodeAt(i);
    }

    // Create a Blob containing the binary data
    const blob = new Blob([byteArray], { type: "image/png" });

    const file = new File([blob], "newImage.png", { type: "image/png" });

    setImageFile(file);

    console.log("image file --> ", imageFile);

    // Create a URL for the Blob
    const url = URL.createObjectURL(file);

    return url;
  };


  const whitelistCheck = async () => {
    const txn = await checkIfWhitelisted();
    console.log("result --> ", txn);
    setIsWhitelisted(txn);

  }

  const id = router.query.imageId;

  useEffect(() => {
    connectWallet()
    whitelistCheck();
    createFile();
  }, []);

  const createFile = () => {
    const b64String = localStorage.getItem(`url${id}`);
    const imageURL = b64ToFile(b64String);
    setImage(imageURL);
  };

  const mintHandler = async (e) => {
    try {
      e.target.disabled = true;
      setBtnText("Uploading File to IPFS...");
      console.log("Uploading metadata to ipfs");
      // console.log("Image file before uploading to IPFS", imageFile)
      let response = await uploadFileToIPFS(imageFile);


      if (response.success == true) {
        setBtnText("Successfully Uploaded File to IPFS โœ…");
        const tokenURI = response.pinataURL;
        console.log("TOKen URI -->", tokenURI)
        // setBtnText("Minting NFT...");
        if(isWhitelisted){
          await mintNFT(tokenURI, true, setBtnText);
        } else {
          await mintNFT(tokenURI, false, setBtnText);
        }
        console.log("Minted NFT")
        setBtnText("Mint NFT");
        e.target.disabled = false;

      } else {
        alert("Failed to Upload File to IPFS");
        e.target.disabled = false;
        setBtnText("Mint NFT");
      }
    } catch (error) {
      alert("Error Minting NFT")
      e.target.disabled = false;
      console.log("Error Minting NFT", error);
    }
  };

  return (
    <div className={styles.mint_container}>
      {!(nftsMinted < 3) ? (
        <p className={styles.no_mints}>Max No of Mints Reached !</p>
      ) : (
        <>
          <div className={styles.mint_left_section}>
            {image ? (
              <Image
                src={image}
                alt="nft image"
                width={400}
                height={400}
                className={styles.mint_nft_img}
              />
            ) : null}
          </div>
          <div className={styles.mint_right_section}>
            <p className={styles.prompt}>{isWhitelisted ? "You are Whitelisted!" : "Get Whitelisted to mint at half the price"}</p>
            <p>
              <span>Price : {isWhitelisted ? "0.01": "0.02" } ETH</span>
            </p>
            <button
              className={styles.mint_btn}
              onClick={mintHandler}
            >
              {btnText}
            </button>
          </div>
        </>
      )}
    </div>
  );
};

export default Mint;

That's it for the mint.js page.

For a successful mint, we want to congratulate the user by redirecting them to the success.js page. Create a success.js file and paste the following:

import styles from "../styles/Success.module.css";
import Link from "next/link";
import { useAiMintsContext } from "../utils/AiMintsContext";
import { useEffect, useState } from "react";
import { useRouter } from "next/router";
const Success = () => {
  const [latestId, setLatestId] = useState("");
  const { fetchTokenURI, getLatestTokenId } = useAiMintsContext();

  const router = useRouter();

  const isError = router.query.isError;

  const fetchLatestId = async () => {
    await getLatestTokenId(setLatestId);

  };

  useEffect(() => {
    fetchLatestId();
  }, [])
  useEffect(() => {
    fetchTokenURI(latestId);
  }, [latestId]);

  return (
    <div className={styles.success_container}>
      {isError == "true" ? (
        <>
          <p className={`${styles.success_txt} ${styles.red_txt}`}>
          Error Minting NFT
        </p>
        <p className={styles.home_link}>
            Go Back to <span onClick={()=> router.push("/")}> Home Page</span>
          </p>
        </>

      ) : latestId.length > 0 ? (
        <>
          <p className={styles.success_txt}>๐ŸŽ‰ Successfully Minted the NFT!</p>
          <p className={styles.opensea_link}>
            You can view your NFT on <Link target="" href={`https://testnets.opensea.io/assets/0x744656fbCa6EfEBC042dD080a7AC3660c0fDCEBb/${latestId}/?force_update=true`}>OpenSea</Link>
          </p>
          <p className={styles.home_link}>
            Go Back to <span onClick={()=> router.push("/")}> Home Page</span>
          </p>
        </>
      ) : (
        <p className={styles.success_txt}>
          Fetching Latest Token Id! Please Wait...
        </p>
      )}
    </div>
  );
};

export default Success;

Now we need to provide a way for the user to get whitelisted. For that, you need to create a file called whitelist.js in the pages dir and paste the following:

import styles from "../styles/Whitelist.module.css"
import { useEffect, useState } from "react";
import { useAiMintsContext } from "../utils/AiMintsContext"
const Whitelist = () => {

  const { totalWhitelisted, getWhitelisted, getTotalWhitelisted, checkIfWhitelisted } = useAiMintsContext();
  const [isWhitelisted, setisWhitelisted] = useState(false);
  const [whitelistTxt, setWhitelistTxt] = useState("WHITELIST")
  const checkWhitelist = async () => {
    let result = await checkIfWhitelisted();
    console.log("Result --> ", result);
    setisWhitelisted(result)
  }

  useEffect(() =>{
    checkWhitelist();
    getTotalWhitelisted();
  }, [])

  return (
    <div className={styles.whitelist_container}> {
      isWhitelisted ? <h2 className={styles.whitelist_heading}>You are Already Whitelisted !!</h2> : (
        <>
        <h2 className={styles.whitelist_heading}>{ whitelistTxt }</h2>
        <p className={styles.whitelist_info}>{totalWhitelisted}/100 Adresses already Whitelisted!</p>
        <button className={styles.whitelist_btn} onClick={ () => { getWhitelisted(setWhitelistTxt)  }}>Get Whitelisted</button>
        </>
      )
    }

    </div>
  )
}

export default Whitelist

Ok, all this is good, but what if there is an error, we will create a custom 404 error page to handle this. Create 404.js file and paste the following.


const ErrorPage = () => {
  return (
    <div>        <style jsx>
    {`
    p {
      display: block;
      width: fit-content;
      text-align: center;
      font-family: monospace;
      font-size: 1.5rem;
      margin: 0 auto;
      padding: 0 20px;
      border-radius: 10px;

      filter: var(--text-shadow);
      color: red;
    }

    a {
        display: block;
      width: fit-content;
      text-align: center;
      font-family: monospace;
      font-size: 1rem;
      margin: 0 auto;
      padding: 0 20px;
      border-radius: 10px;
      filter: var(--text-shadow);
      color: white;
    }


    @media screen and (max-width: 768px) {
        p {
            font-size: 1rem;
        }
    }
  `}
  </style>
        <p>Error 404</p>
        <a href='/'>Go Back to Home Page</a>

    </div>
  )
}

export default ErrorPage

Overwhelming right? You've really come to a long way dev!

Take some rest, grab a cup of coffee, and get back in the game.

I know, while coding all this stuff you must be wondering 'how are we going to generate images?'. I got you!

For that you need to create an api endpoint for your app. To do that,

create a file called [prompt].js inside the pages directory and paste the following:

require("dotenv").config({path: "/.env.local"});
const { Configuration, OpenAIApi } = require("openai");

export default async function imageGenerator (req, res) {

    const { OPEN_AI_API } = process.env;

  const prompt = req.query.prompt;

  console.log("Prompt --> ", prompt);

    const configuration = new Configuration({
        apiKey: OPEN_AI_API,
      });
      const openai = new OpenAIApi(configuration);


    try {
        const response = await openai.createImage({
            prompt: prompt,
            n: 4,
            size: "256x256",
            response_format: "b64_json"
          });
        const arr = response.data.data;




    res.status(200).json({
        success: true,
        imageData: arr
    })
    return ;
      } catch (error) {
        if (error.response) {
          console.log(error.response.status);
          console.log(error.response.data);
        } else {
          console.log(error.message);
        }
      }

      res.status(404).json( {
        success: false
      })

}

To get your API keys, head on to OpenAi 's website.

To provide an endpoint for OpenSea to fetch NFT data, you need to create folder tokenURI inside api folder and then inside that you need to create a file [tokenURI].js, and paste the following:

export default async function tokenURIFetcher (req, res) {

    const ipfsHash = req.query.tokenId.replace(".json", "");

    console.log("Ipfs hash ==> ", ipfsHash);

    const imageURL = "ipfs://" + ipfsHash;

    res.status(200).json({
        "name": `Ai Mints ${ipfsHash}`,
        "image": imageURL,
        "description": "An Ai NFT Minting application"
    })

}

Finally, to make all this work smoothly, you need to create a .env.local file in the client directory and paste the following

NEXT_PUBLIC_PINATA_SECRET_KEY="Your Pinata API Secret Key"

NEXT_PUBLIC_PINATA_KEY="Your Pinata Public Key"

OPEN_AI_API="You openAi api Key"

You can get your PINATA keys by creating an endpoint here.

We have successfully created the front end for our application but it can't really interact with our smart contract. To make sure it can do that we need to do the following.

Create a utils folder in the client directory and inside that create a file named AiMintsContext.js and paste the following:

import { useEffect, useContext, useState } from "react";
import { AiMintsContractAbi, AiMintsContractAddress, WhitelistContractAbi, WhitelistContractAddress } from "./constants";
import { ethers } from "ethers";
import React from 'react'
import {useRouter} from "next/router";

const AiMintsContext = React.createContext();

const AiMintsProvider = ({children}) => {
const [currentAddress, setCurrentAddress] = useState("")
  const [walletConnected, setWalletConnected] = useState(false);
  const [totalWhitelisted, setTotalWhitelisted] = useState(0);
  const [nftsMinted, setNftsMinted] = useState(0);

  const router = useRouter();

  useEffect(() => {
    if(!(window.ethereum)) {
      alert("Please Install Metamask");
    }
  }, [])


  const getProviderOrSigner = async (needSigner = false) => {
    try {
         const provider = new ethers.providers.Web3Provider(window.ethereum);

    const chainId = await provider.send("eth_chainId", []);
    console.log(chainId);
    if (chainId != "0x5") {
      window.alert("Please connect to the Goerli Testnet");
      throw new Error("Please Switch to the localhost");
    }

    if (needSigner) {
      const signer = provider.getSigner();
      return signer;
    }
    return provider;
    } catch (error) {
      console.log("Error getting provider/signer", error )
    }

  };


  const connectWallet = async () => {
    try {
      const provider = await getProviderOrSigner(false);

      const accounts = await provider.send("eth_requestAccounts", []);
      setWalletConnected(true);
      setCurrentAddress(accounts[0])
    } catch (error) {
      console.log("Error COnnecting to the wallet", error)
    }
  }

  const getContractInstance = async (contractAddress, contractAbi, providerOrSigner) => {
    const instance =  new ethers.Contract(
      contractAddress,
      contractAbi,
      providerOrSigner
    );
    return instance
  }

  const getWhitelisted = async (setWhitelistTxt) => {
    try {

      setWhitelistTxt("Whitelisting...");
      const signer = await getProviderOrSigner(true);


      console.log("Whitelist contract address --> ", WhitelistContractAddress);

      const whitelistContract = await getContractInstance(WhitelistContractAddress, WhitelistContractAbi, signer);

      console.log("WHite list Contract ",whitelistContract )



      const txn = await whitelistContract.addAddressToWhitelist();
      await txn.wait();

      setWhitelistTxt("๐ŸŽ‰ Whitelisted");
      await getTotalWhitelisted();


      console.log("Number of Addresses whitelisted -->", totalWhitelisted);
    } catch (error) {
      // console.log("error msg", error.data.message)
      if(error.data){
        if(error.data.message == "Error: VM Exception while processing transaction: reverted with reason string 'Sender has already been whitelisted'")
        alert("You are already Whitelisted");
      }
      console.log("Error Whitelisting the Address", error);
    }
  }

  const getTotalWhitelisted = async () => {
    try {
      const provider = await getProviderOrSigner(false);
      const whitelistContract = await getContractInstance(WhitelistContractAddress, WhitelistContractAbi, provider);
      const noWhitelisted = await whitelistContract.numAddressesWhitelisted();

      setTotalWhitelisted(noWhitelisted);

    } catch (error) {
      console.log("Error fetching the number of whitelisted addresses", error)
    }
  }

  const checkIfWhitelisted = async () => {
    try {
      const signer = await getProviderOrSigner(true);
      const whitelistContract = await getContractInstance(WhitelistContractAddress, WhitelistContractAbi, signer)

      const address = await signer.getAddress();
      console.log("WHitelist Contract", whitelistContract);
      const txn = await whitelistContract.isWhitelisted(address);

      console.log("txn -->", txn)

      return txn;
    } catch (error) {
      console.log("Error checking for Whitelist", error )
    }
  }

  const mintNFT = async (tokenURI, isWhitelisted, setBtnText) => {
    try {
      const signer = await getProviderOrSigner(true);
      const aiMintsContract = await getContractInstance(AiMintsContractAddress, AiMintsContractAbi, signer);
      console.log("aiMintsContract", aiMintsContract);

      setBtnText("Minting NFT");

      if(isWhitelisted) {
        let txn = await aiMintsContract.whitelistMint(tokenURI, {value: ethers.utils.parseUnits("0.01", "ether")});
        await txn.wait();
        alert("Successfully Minted the NFT")
      } else {
          let txn = await aiMintsContract.publicMint(tokenURI, {value: ethers.utils.parseUnits("0.02", "ether")});
          await txn.wait();
        alert("Successfully Minted the NFT")
        }

        router.push({ pathname: "success", query:{isError : "false"} });
    } catch (error) {
      alert("Failed to Mint NFT");
      router.push({ pathname: "success", query:{isError : "true"} });

      console.log("Error Minting NFT", error);
    }
  }

  const fetchTokenURI = async (tokenId) => {
    try {
      const provider = await getProviderOrSigner(false);
      const aiMintsContract = await getContractInstance(AiMintsContractAddress, AiMintsContractAbi, provider);

      console.log("tokenId before fetching tokenURI ", tokenId)

      const tokenURI = await aiMintsContract.tokenURI(tokenId);
      console.log("TOKEN URI --> ", tokenURI)

      return tokenURI;
    } catch (error) {
      console.log("error fetching token URI", error);
    }
  }

  const numberOfNFTsMinted = async() => {
    try {
      const signer = await getProviderOrSigner(true);
      const aiMintsContract = await getContractInstance(AiMintsContractAddress, AiMintsContractAbi, signer);

      const address = await signer.getAddress();
      const no = await aiMintsContract.getNumberOfNFTsMinted(address);

      console.log("address --> ", address);

      console.log("number of NFTs minted --> ", no);
      setNftsMinted(no);
    } catch (error) {
      console.log("Error fetching the number of NFTs Minted", error);
    }
  }

  const getLatestTokenId = async (setLatestId) => {
    try {
      const provider = await getProviderOrSigner(false);

      const aiMintsContract = await getContractInstance(AiMintsContractAddress, AiMintsContractAbi, provider);

      let latestId = await aiMintsContract.latestTokenId();
      latestId = latestId.toString();

      setLatestId(latestId);
      console.log("latest token Id -->", latestId);

      return latestId;
    } catch (error) {
      console.log("Error fetching latest token Id ", error);
    }
  }

  return (
    <AiMintsContext.Provider value={
       {
        totalWhitelisted,
        connectWallet,
        currentAddress,
        walletConnected,
        getWhitelisted,
        getTotalWhitelisted,
        checkIfWhitelisted,
        mintNFT,
        numberOfNFTsMinted,
        nftsMinted,
        fetchTokenURI,
        getLatestTokenId
       }
    }>{children}</AiMintsContext.Provider>
  )
}

export const useAiMintsContext = () => {
    return useContext(AiMintsContext)
}

export default AiMintsProvider

This file provides us with all the functions to interact with our smart contract

To upload files to IPFS, create file called pinata.js and paste the following:

const key = process.env.PINATA_KEY;
const secret = process.env.PINATA_SECRET_KEY;


const axios = require('axios');
const FormData = require('form-data');

export const uploadFileToIPFS = async(file) => {
    const url = `https://api.pinata.cloud/pinning/pinFileToIPFS`;
    //making axios POST request to Pinata โฌ‡๏ธ


    let data = new FormData();
    data.append('file', file);

    const metadata = JSON.stringify({
        name: 'testname',
        keyvalues: {
            exampleKey: 'exampleValue'
        }
    });
    data.append('pinataMetadata', metadata);

    //pinataOptions are optional
    const pinataOptions = JSON.stringify({
        cidVersion: 0
    });
    data.append('pinataOptions', pinataOptions);

    return axios 
        .post(url, data, {
            maxBodyLength: 'Infinity',
            headers: {
                'Content-Type': `multipart/form-data; boundary=${data._boundary}`,
                pinata_api_key: process.env.NEXT_PUBLIC_PINATA_KEY,
                pinata_secret_api_key: process.env.NEXT_PUBLIC_PINATA_SECRET_KEY,
            }
        })
        .then(function (response) {
            console.log("image uploaded", response.data.IpfsHash)
            return {
               success: true,
               pinataURL: response.data.IpfsHash
           };
        })
        .catch(function (error) {
            console.log(error)
            return {
                success: false,
                message: error.message,
            }

    });
};

Also you need to create another file called constants.js to store the contract addresses and ABIs for our smart contract ๐Ÿ‘‡


const AiMintsContractAddress = "<Paste the AIMints Smart Contract Address>";
const WhitelistContractAddress = "<Paste the Whitelist Contract Addr>";
const AiMintsContractAbi = ["<AiMints Contract ABI>"];
const WhitelistContractAbi = ["Whitelist Contract ABI"];

export { AiMintsContractAbi, AiMintsContractAddress, WhitelistContractAbi, WhitelistContractAddress  };

AND THAT IS IT!!

CONGRATULATIONS! ๐Ÿฅณ . Give yourself a pat on the back Dev!

We have successfully created our AI NFT Minting Dapp.

You can test it by running npm run dev inside a terminal in the client dir.

Thank you so much for reading this far.

This is my first blog post.

I hope I added a little value to your time.

Bye.

I am avalaible for work. You can contact me @moyezrabbani

My Portfolio Website.

ย