How to create a minting page for your NFTs

In this post we will see how to create a simple NFT minting page. We will use the NW3 boilerplate available here

Project setup

Let's start by cloning the boilerplate and installing the dependencies

git clone https://github.com/0xtiby/nw3
cd nw3
yarn

What we need

To add a minting page we will need to :

  • create a new mint page in the pages folder
  • create two components Mint and ConnectWallet in the components folder
  • create a custom hook in the hooks folder to handle the minting logic
  • add a new config value for the mint contract address in the config.js file
  • add some new locales

Create the useMint hook

Let's start by creating the useMint hook. This hook will handle the minting logic and will return all the necessary data to the minting page. Create a new file hooks/useMint.ts add the following code :

import {
  useAccount,
  useContractWrite,
  useEnsName,
  useNetwork,
  usePrepareContractWrite,
  useWaitForTransaction,
} from 'wagmi'
import { useAccountModal, useConnectModal } from '@rainbow-me/rainbowkit'

import { config } from '@/config'

export const useMint = () => {
  const { address, isConnected } = useAccount()
  const { data: ensName } = useEnsName({ address })
  const { openConnectModal } = useConnectModal()
  const { openAccountModal } = useAccountModal()
  const { chain } = useNetwork()

  const {
    config: mintConfig,
    error: prepareError,
    isError: isPrepareError,
  } = usePrepareContractWrite({
    address: config.MINT_ADDRESS as `0x${string}`,
    abi: [
      {
        name: 'mint',
        type: 'function',
        stateMutability: 'nonpayable',
        inputs: [],
        outputs: [],
      },
    ],
    functionName: 'mint',
  })
  const {
    data,
    write,
    error: mintError,
    isError: isMintError,
  } = useContractWrite(mintConfig)

  const { isLoading, isSuccess } = useWaitForTransaction({
    hash: data?.hash,
  })

  const formattedAdr = address
    ? `${address.substring(0, 4)}${address.substring(
        address.length - 4,
        address.length
      )}`
    : ''

  const displayName = ensName
    ? `${ensName.substring(0, 16)}`
    : `${formattedAdr}`

  return {
    displayName,
    openAccountModal,
    openConnectModal,
    isConnected,
    isSupportedNetwork: chain?.unsupported === false,
    mint: write,
    isMinting: isLoading,
    isMinted: isSuccess,
    mintData: data,
    prepareError,
    isPrepareError,
    mintError,
    isMintError,
  }
}

First we import all the necessary hooks from wagmi and rainbowkit. Next we use the hook usePrepareContractWrite to prepare the contract write. It eagerly fetches the parameters required for sending a contract write transaction such as the gas estimate. Next we use the useContractWrite hook to actually perform the write transaction to the contract. We use the useWaitForTransaction hook to show feedback on the status of the transaction to the user. Finally we return all the necessary data to the minting page.

Creating components

First we will create a ConnectWallet component. This component will be used to connect the user's wallet. For the minting page we don't need the user to signed in with a session to our app. Create a new file components/ConnectWallet.tsx and add the following code

import React from 'react'
import { locales } from '@/locales'
import { useMint } from '@/hooks/useMint'

export const ConnectWallet: React.FC = () => {
  const {
    displayName,
    openAccountModal,
    openConnectModal,
    isConnected,
    isSupportedNetwork,
  } = useMint()

  return (
    <button
      onClick={() =>
        isConnected
          ? openAccountModal && openAccountModal()
          : openConnectModal && openConnectModal()
      }
      className="bg-theme-600 hover:bg-theme-500 focus-visible:outline-theme-600 rounded-md px-3.5 py-1.5 text-base font-semibold leading-7 text-white shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2"
    >
      {isConnected ? displayName : locales.connectWallet}
    </button>
  )
}

Here nothing crazy we just create a custom connect button that will open the connect modal if the user is not connected and the account modal if the user is connected and if the user is not on the right network we display an error message.

Next we will create the Mint component. This component will be used to display the mint button and the status of the minting transaction.

Create the following file components?Mint.tsx with this code :

import React from 'react'
import { config } from '@/config'
import { locales } from '@/locales'
import { useMint } from '@/hooks/useMint'

export const Mint: React.FC = () => {
  const {
    mint,
    isMinting,
    isMinted,
    mintData,
    isConnected,
    isPrepareError,
    prepareError,
    isMintError,
    mintError,
    isSupportedNetwork,
  } = useMint()

  if (!isConnected) return null
  if (!isSupportedNetwork)
    return <p className="text-red-500">{locales.wrongNetworkMessage}</p>

  return (
    <div>
      <button
        disabled={!mint || isMinting}
        onClick={() => mint?.()}
        className="bg-theme-600 hover:bg-theme-500 focus-visible:outline-theme-600 rounded-md px-3.5 py-1.5 text-base font-semibold leading-7 text-white shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2"
      >
        {isMinting ? 'Minting...' : 'Mint'}
      </button>
      {isMinted && (
        <div>
          <div className="mt-2 text-green-500">{locales.mintSuccess}</div>
          <div>
            <a href={`${config.BLOCKCHAIN_EXPLORER_URL}/tx/${mintData?.hash}`}>
              Etherscan
            </a>
          </div>
        </div>
      )}
      {(isPrepareError || isMintError) && (
        <div className="mt-2 text-red-500">
          Error: {(prepareError || mintError)?.message}
        </div>
      )}
    </div>
  )
}

Here first if the user is not connected or not on the right network we don't display anything. Next we display the mint button and the status of the minting transaction. If the transaction is successfull we display a success message and a link to the transaction on the blockchain explorer. Otherwise we display an error message.

Updates config and locales

Before wrapping everything up in the minting page, we need to update the config and the locales files.

In config/config.ts add the following

  BLOCKCHAIN_EXPLORER_URL: process.env.NEXT_PUBLIC_BLOCKCHAIN_EXPLORER_URL ?? "https://goerli.etherscan.io" ,
  MINT_ADDRESS: process.env.NEXT_PUBLIC_MINT_ADDRESS ?? "",

And in locales/index.ts add this :

  mint: "Mint",
  mintInstuctions: "Before minting you need to connect your wallet.",
  connectWallet: "Connect wallet",
  mintSuccess: "Successfully minted your NFT!",

Wrapping up

Now we can wrap everything up in the minting page. Create the pages/mint.tsx file and add the following code

import { ConnectWallet } from '@/components/ConnectWallet'
import Head from 'next/head'
import Image from 'next/image'
import Link from 'next/link'
import Logo from 'public/logo.png'
import { Mint } from '@/components/Mint'
import React from 'react'
import { config } from '@/config'
import { locales } from '@/locales'
import { routes } from '@/routes'
import { withHydratationFix } from '@/hoc/withHydratationFix'

const MintPage: React.FC = () => {
  return (
    <>
      <Head>
        <title>{locales.mint}</title>
        <meta name="description" content={locales.heroDescription} />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <link rel="icon" href="/favicon.ico" />
      </Head>
      <div className="isolate bg-white">
        <div className="absolute inset-x-0 top-[-10rem] -z-10 transform-gpu overflow-hidden blur-3xl sm:top-[-20rem]">
          <svg
            className="relative left-[calc(50%-11rem)] -z-10 h-[21.1875rem] max-w-none -translate-x-1/2 rotate-[30deg] sm:left-[calc(50%-30rem)] sm:h-[42.375rem]"
            viewBox="0 0 1155 678"
            xmlns="http://www.w3.org/2000/svg"
          >
            <path
              fill="url(#45de2b6b-92d5-4d68-a6a0-9b9b2abad533)"
              fillOpacity=".3"
              d="M317.219 518.975L203.852 678 0 438.341l317.219 80.634 204.172-286.402c1.307 132.337 45.083 346.658 209.733 145.248C936.936 126.058 882.053-94.234 1031.02 41.331c119.18 108.451 130.68 295.337 121.53 375.223L855 299l21.173 362.054-558.954-142.079z"
            />
            <defs>
              <linearGradient
                id="45de2b6b-92d5-4d68-a6a0-9b9b2abad533"
                x1="1155.49"
                x2="-78.208"
                y1=".177"
                y2="474.645"
                gradientUnits="userSpaceOnUse"
              >
                <stop stopColor="#fc8d89"></stop>
                <stop offset="1" stopColor="#ff8080"></stop>
              </linearGradient>
            </defs>
          </svg>
        </div>
        <div className="px-6 pt-6 lg:px-8">
          <nav
            className="flex items-center justify-between"
            aria-label="Global"
          >
            <div className="flex lg:flex-1">
              <Link href={routes.home} className="-m-1.5 p-1.5">
                <span className="sr-only">{config.APP_NAME}</span>
                <Image
                  className="h-10 w-10 rounded-full"
                  src={Logo}
                  alt="Logo"
                />
              </Link>
            </div>
          </nav>
        </div>
        <main>
          <div className="relative px-6 lg:px-8">
            <div className="mx-auto max-w-2xl py-32 sm:py-48 lg:py-56">
              <div className="text-center">
                <h1 className="text-4xl font-bold tracking-tight text-gray-900 sm:text-6xl">
                  {locales.mint}
                </h1>
                <p className="mt-6 text-lg leading-8 text-gray-600">
                  {locales.mintInstuctions}
                </p>
                <div className="mt-10 flex flex-col items-center justify-center gap-x-6 space-y-6">
                  <ConnectWallet />
                  <Mint />
                </div>
              </div>
            </div>
            <div className="absolute inset-x-0 top-[calc(100%-13rem)] -z-10 transform-gpu overflow-hidden blur-3xl sm:top-[calc(100%-30rem)]">
              <svg
                className="relative left-[calc(50%+3rem)] h-[21.1875rem] max-w-none -translate-x-1/2 sm:left-[calc(50%+36rem)] sm:h-[42.375rem]"
                viewBox="0 0 1155 678"
                xmlns="http://www.w3.org/2000/svg"
              >
                <path
                  fill="url(#ecb5b0c9-546c-4772-8c71-4d3f06d544bc)"
                  fillOpacity=".3"
                  d="M317.219 518.975L203.852 678 0 438.341l317.219 80.634 204.172-286.402c1.307 132.337 45.083 346.658 209.733 145.248C936.936 126.058 882.053-94.234 1031.02 41.331c119.18 108.451 130.68 295.337 121.53 375.223L855 299l21.173 362.054-558.954-142.079z"
                />
                <defs>
                  <linearGradient
                    id="ecb5b0c9-546c-4772-8c71-4d3f06d544bc"
                    x1="1155.49"
                    x2="-78.208"
                    y1=".177"
                    y2="474.645"
                    gradientUnits="userSpaceOnUse"
                  >
                    <stop stopColor="#fc8d89"></stop>
                    <stop offset="1" stopColor="#ff8080"></stop>
                  </linearGradient>
                </defs>
              </svg>
            </div>
          </div>
        </main>
      </div>
    </>
  )
}

export default withHydratationFix(MintPage)

This page take the same layout and style as the home page, but it has a different content. The content is the ConnectWallet and Mint component. Now you have a working minting page, to test it you can use this contract 0xFBA3912Ca04dd458c843e2EE08967fC04f3579c2 wich is the Wagmi contract deployed on the Goerli testnet. You can find the code directly in the NW3 repo as it's included in the project right now.