Nft Mint Card

Mint NFTs using the mint card component with smart contract

NFTsMintSmart contract
Loading

Technologies

Installation

Depoloy smart contract

// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0 <0.9.0;

import {ERC721A} from "erc721a/contracts/ERC721A.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";

interface IErrors {
    // @dev Insufficient funds sent with mint request transaction
    error InsufficientFundsSent();

    // @dev Quantity to mint must be greater than 0
    error InvalidMintQuantity();

    // @dev Invalid timestamps provided for mint phases (must be linear)
    error InvalidTimestamps();

    // @dev Maximum amount of mints per wallet on public phase exceeded
    error MaxMintsPerWalletExceeded();

    // @dev Max supply cap has exceeded
    error MaxSupplyExceeded();

    // @dev All mint phases have ended
    error MintHasEnded();

    // @dev Minting has not started for the requested phase
    error MintHasNotStarted();

    // @dev Withdrawal of ethereum balance failed
    error WithdrawalFailed();
}

contract NftMint is ERC721A, Ownable, IErrors {
    // ================================================================
    // │                          STORAGE                             │
    // ================================================================
    // @dev Maximum supply cap for the collection
    // @notice This variable is preset to a placeholder and its value is user editable
    uint256 private s_maxSupply = 10000;

    // @dev Mint price per NFT
    // @notice This variable is preset to a placeholder and its value is user editable
    uint256 private s_mintPrice = 0.01 ether;

    // @dev Maximum allowed mints per user address
    // @notice This variable is preset to a placeholder and its value is user editable
    uint256 private s_maxMintsPerWallet = 5;

    // @dev Mint start time using UNIX timestamp
    uint256 private s_mintStartTimestamp;

    // @dev Mint end time using UNIX timestamp
    uint256 private s_mintEndTimestamp;

    // @dev Base URI for the collection metadata
    string private s_baseURI;

    // @dev Extension added to s_baseURI to form the final tokenURI
    string private baseExtension = ".json";

    // ================================================================
    // │                          EVENTS                              │
    // ================================================================
    // @dev Event emitted when the supply cap is edited
    event SupplyCapEdited(uint256 newSupplyCap, uint256 timestamp);

    // @dev Event emitted when a new mint is performed
    event Mint(address indexed minter, uint256 quantity, uint256 timestamp);

    // @dev Event emitted when the baseURI is edited
    event BaseURISet(string newBaseURI, uint256 timestamp);

    // @dev Event emitted when the mint price is edited
    event MintPriceEdited(uint256 newPrice, uint256 timestamp);

    // @dev Event emitted when the timestamps are edited
    event TimestampsEdited(
        uint256 mintStart,
        uint256 mintEnd,
        uint256 timestamp
    );

    // @dev Event emitted when the max mints per user address is edited
    event MaxMintsPerWalletEdited(
        uint256 newMaxMintsPerWallet,
        uint256 timestamp
    );

    // @dev Event emitted when the contract owner withdraws the balance
    event Withdrawal(
        address indexed caller,
        uint256 totalAmount,
        uint256 timestamp
    );

    // ================================================================
    // │                        CONSTRUCTOR                           │
    // ================================================================
    /**
     * @dev {constructor}
     *
     * @param initialOwner Project owner address to be forwarded to Ownable constructor.
     * @param _initBaseURI Initial baseUri to be set.
     * @param _initMintStart Initial mint start timestamp.
     * @param _initMintEnd Initial mint end timestamp.
     *
     * @notice This NFT collection is named "Gizmolab UI" with the symbol "GLUI" these are
     * placeholders that must be changed for your desired collection name and symbol    .
     */
    constructor(
        address initialOwner,
        string memory _initBaseURI,
        uint256 _initMintStart,
        uint256 _initMintEnd
    ) ERC721A("Gizmolab UI", "GLUI") Ownable(initialOwner) {
        if (
            _initMintStart >= _initMintEnd ||
            _initMintStart == 0 ||
            _initMintEnd == 0
        ) revert InvalidTimestamps();

        s_baseURI = _initBaseURI;
        s_mintStartTimestamp = _initMintStart;
        s_mintEndTimestamp = _initMintEnd;
    }

    // ================================================================
    // │                        MINT FUNCTION                         │
    // ================================================================
    /**
     * @dev function {mint}
     *
     * Function to mints NFTs.
     *
     * Emits a {Mint} event indicating the details of a successful mint.
     *
     * NOTE: Various mint criterias must be met in order for the transaction to succeed.
     * Criterias include the mint phase being active, the total supply cap not being exceeded,
     * the maximum mints per user address not being exceeded, and the correct amount of funds
     * being sent with the transaction.
     *
     * @param quantity Number of NFTs to mint.
     */
    function mint(uint256 quantity) external payable {
        if (block.timestamp < s_mintStartTimestamp) revert MintHasNotStarted();

        if (block.timestamp > s_mintEndTimestamp) revert MintHasEnded();

        if (quantity == 0) revert InvalidMintQuantity();

        uint256 previouslyMinted = _getAux(_msgSender());

        if (previouslyMinted + quantity > s_maxMintsPerWallet)
            revert MaxMintsPerWalletExceeded();

        if (totalSupply() + quantity > s_maxSupply) revert MaxSupplyExceeded();

        if (msg.value < s_mintPrice * quantity) revert InsufficientFundsSent();

        _setAux(_msgSender(), uint64(previouslyMinted + quantity));
        _safeMint(_msgSender(), quantity);

        emit Mint(msg.sender, quantity, block.timestamp);
    }

    // ================================================================
    // │                       VIEW FUNCTIONS                         │
    // ================================================================
    /**
     * @dev function {getMintPrice}
     *
     * Returns the mint price for a single token.
     *
     * @return s_mintPrice
     */
    function getMintPrice() external view returns (uint256) {
        return s_mintPrice;
    }

    /**
     * @dev function {getMaxSupply}
     *
     * Returns the maximum supply cap for the collection.
     *
     * @return s_maxSupply
     */
    function getMaxSupply() external view returns (uint256) {
        return s_maxSupply;
    }

    /**
     * @dev function {getMaxMintsPerWallet}
     *
     * Returns the maximum mints allowed for a single user address.
     *
     * @return s_maxMintsPerWallet
     */
    function getMaxMintsPerWallet() external view returns (uint256) {
        return s_maxMintsPerWallet;
    }

    /**
     * @dev function {getStartTimestamp}
     *
     * Returns the UNIX mint start timestamp.
     *
     * @return s_mintStartTimestamp
     */
    function getStartTimestamp() external view returns (uint256) {
        return s_mintStartTimestamp;
    }

    /**
     * @dev function {getEndTimestamp}
     *
     * Returns the UNIX end start timestamp.
     *
     * @return s_mintEndTimestamp
     */
    function getEndTimestamp() external view returns (uint256) {
        return s_mintEndTimestamp;
    }

    /**
     * @dev function {getUserMintedNftCount}
     *
     * Returns the amount of NFTs the function caller has minted.
     *
     * @param userAddress The user address to query the number of minted NFTs for.
     *
     * @return _getAux(userAddress)
     */
    function getUserMintedNftCount(
        address userAddress
    ) external view returns (uint256) {
        return _getAux(userAddress);
    }

    /**
     * @dev function {getBaseURI}
     *
     * Returns the current active collection base URI.
     *
     * @return s_baseURI
     */
    function getBaseURI() external view returns (string memory) {
        return s_baseURI;
    }

    // ================================================================
    // │                      COLLECTION METADATA                     │
    // ================================================================
    /**
     * @dev function {_baseURI}
     *
     * Override to return the storage variable of {_baseURI()} representing the base URI for the collection metadata.
     *
     * @return s_baseURI
     */
    function _baseURI() internal view virtual override returns (string memory) {
        return s_baseURI;
    }

    /**
     * @dev function {_startTokenId}
     *
     * Override to return 1 as a tokenId instead of 0 for the first token minted.
     *
     * @return number with a override value of 1
     */
    function _startTokenId() internal view virtual override returns (uint256) {
        return 1;
    }

    /**
     * @dev function {tokenURI}
     *
     * Override to return the appropriate metadata uri for a given tokenId.
     *
     * @param tokenId The tokenId to query the metadata URI for.
     *
     * @return string representing the metadata URI for the given tokenId.
     */
    function tokenURI(
        uint256 tokenId
    ) public view virtual override returns (string memory) {
        require(
            _exists(tokenId),
            "ERC721Metadata: URI query for nonexistent token"
        );

        string memory currentBaseURI = _baseURI();
        return
            bytes(currentBaseURI).length > 0
                ? string(
                    abi.encodePacked(
                        currentBaseURI,
                        _toString(tokenId),
                        baseExtension
                    )
                )
                : "";
    }

    // ================================================================
    // │                      OWNER FUNCTIONS                         │
    // ================================================================
    /**
     * @dev function {setBaseURI}
     *
     * Sets new URI to the storage variable {s_baseURI}.
     *
     * Emits a {BaseURISet} event indicating a new URI has been set as the base and timestamp of the edit.
     *
     * @param baseURI New base URI to set for the collection metadata.
     *
     * NOTE: Only the contract owner can call this function.
     */
    function setBaseURI(string memory baseURI) public onlyOwner {
        s_baseURI = baseURI;

        emit BaseURISet(baseURI, block.timestamp);
    }

    /**
     * @dev function {editMintPrice}
     *
     * Sets new price for minting a single token.
     *
     * Emits a {MintPriceEdited} event indicating the new mint price and timestamp of the edit.
     *
     * @param newPrice New mint price in WEI.
     *
     * NOTE: Only the contract owner can call this function.
     */
    function editMintPrice(uint256 newPrice) external onlyOwner {
        s_mintPrice = newPrice;

        emit MintPriceEdited(newPrice, block.timestamp);
    }

    /**
     * @dev function {editMaxSupply}
     *
     * Sets new limit for maximum supply cap for the collection.
     *
     * Emits a {SupplyCapEdited} event indicating the new supply cap and timestamp of the edit.
     *
     * @param newSupply New maximum supply cap for the collection.
     *
     * NOTE: Only the contract owner can call this function.
     */
    function editMaxSupply(uint256 newSupply) external onlyOwner {
        s_maxSupply = newSupply;

        emit SupplyCapEdited(newSupply, block.timestamp);
    }

    /**
     * @dev function {editMaxMintsPerWallet}
     *
     * Sets new maximum cap of number of NFTs mintable per user address.
     *
     * Emits a {MaxMintsPerWalletEdited} event indicating the maximum mintable NFTs cap and timestamp of the edit.
     *
     * @param newMaxMintsPerWallet New maximum cap of mintable NFTs per user address.
     *
     * NOTE:
     * Only the contract owner can call this function.
     */
    function editMaxMintsPerWallet(
        uint256 newMaxMintsPerWallet
    ) external onlyOwner {
        s_maxMintsPerWallet = newMaxMintsPerWallet;

        emit MaxMintsPerWalletEdited(newMaxMintsPerWallet, block.timestamp);
    }

    /**
     * @dev function {editTimestamps}
     *
     * Sets new timestamps for the start and end of the minting phase.
     *
     * Emits a {TimestampsEdited} event indicating the new timestamps and timestamp of the edit.
     *
     * @param startTime New timestamp for the start of the minting phase.
     * @param endTime New timestamp for the end of the minting phase.
     *
     * NOTE:
     * Minting phase timestamps must be linear ({mintStart} < {mintEnd}).
     * Only the contract owner can call this function.
     */
    function editTimestamps(
        uint256 startTime,
        uint256 endTime
    ) external onlyOwner {
        if (startTime >= endTime || startTime == 0 || endTime == 0)
            revert InvalidTimestamps();

        s_mintStartTimestamp = startTime;
        s_mintEndTimestamp = endTime;

        emit TimestampsEdited(startTime, endTime, block.timestamp);
    }

    /**
     * @dev function {withdraw}
     *
     * Withdraws the contract balance to the owner addresses.
     *
     * Emits a {Withdrawal} event indicating the caller, the amount of ether withdrawn and timestamp of the withdrawal.
     *
     * NOTE: Only the contract owner can call this function.
     */
    function withdraw() external onlyOwner {
        uint256 totalAmount = address(this).balance;
        (bool success, ) = payable(owner()).call{value: totalAmount}("");

        if (!success) revert WithdrawalFailed();

        emit Withdrawal(msg.sender, totalAmount, block.timestamp);
    }
}

Install Web3 dependencies

npm install @reown/appkit @reown/appkit-adapter-wagmi wagmi viem @tanstack/react-query

Add Shadcn components

npx shadcn@latest add card toast button skeleton

Add Sepolia testnet to config

Edit the file created during the Reown Appkit installation named config/index.tsx and add the sepolia network for this example:
import { sepolia } from "@reown/appkit/networks";
  
export const networks = [
  //...
  sepolia // add sepolia network here
];

Add Sepolia testnet to Appkit context

Edit the second file created during the Reown Appkit installation named context/index.tsx and add the sepolia network for this example:
import { sepolia } from "@reown/appkit/networks";
  
const modal = createAppKit({
  //...
  networks: [
    sepolia, // add sepolia network here
  ],
  defaultNetwork: sepolia, // set default network as sepolia or remove this line completely
  //...
});

Add Toaster component

Add the Shadcn Toaster component to the app/layout.tsx file to enable component toast notifications:
import type { Metadata } from "next";
import "./globals.css";
import { Toaster } from "@/components/ui/toaster"; // import Toaster component

import { cookies, headers } from "next/headers";
import ContextProvider from "@/context/AppKitContext";

export const metadata: Metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  const cookies = headers().get("cookie");

  return (
    <html lang="en">
      <body>
        <ContextProvider cookies={cookies}>{children}</ContextProvider>
        <Toaster /> {/* add Toaster component here */}
      </body>
    </html>
  );
}

Import smart contract ABI

Create a file named contracts/NftMint.json and add the smart contract ABI:
{
  "abi": [
    {
      "inputs": [
        {
          "internalType": "address",
          "name": "initialOwner",
          "type": "address"
        },
        { "internalType": "string", "name": "_initBaseURI", "type": "string" },
        {
          "internalType": "uint256",
          "name": "_initMintStart",
          "type": "uint256"
        },
        { "internalType": "uint256", "name": "_initMintEnd", "type": "uint256" }
      ],
      "stateMutability": "nonpayable",
      "type": "constructor"
    },
    {
      "inputs": [],
      "name": "ApprovalCallerNotOwnerNorApproved",
      "type": "error"
    },
    {
      "inputs": [],
      "name": "ApprovalQueryForNonexistentToken",
      "type": "error"
    },
    { "inputs": [], "name": "BalanceQueryForZeroAddress", "type": "error" },
    { "inputs": [], "name": "InsufficientFundsSent", "type": "error" },
    { "inputs": [], "name": "InvalidMintQuantity", "type": "error" },
    { "inputs": [], "name": "InvalidTimestamps", "type": "error" },
    { "inputs": [], "name": "MaxMintsPerWalletExceeded", "type": "error" },
    { "inputs": [], "name": "MaxSupplyExceeded", "type": "error" },
    {
      "inputs": [],
      "name": "MintERC2309QuantityExceedsLimit",
      "type": "error"
    },
    { "inputs": [], "name": "MintHasEnded", "type": "error" },
    { "inputs": [], "name": "MintHasNotStarted", "type": "error" },
    { "inputs": [], "name": "MintToZeroAddress", "type": "error" },
    { "inputs": [], "name": "MintZeroQuantity", "type": "error" },
    { "inputs": [], "name": "NotCompatibleWithSpotMints", "type": "error" },
    {
      "inputs": [
        { "internalType": "address", "name": "owner", "type": "address" }
      ],
      "name": "OwnableInvalidOwner",
      "type": "error"
    },
    {
      "inputs": [
        { "internalType": "address", "name": "account", "type": "address" }
      ],
      "name": "OwnableUnauthorizedAccount",
      "type": "error"
    },
    { "inputs": [], "name": "OwnerQueryForNonexistentToken", "type": "error" },
    {
      "inputs": [],
      "name": "OwnershipNotInitializedForExtraData",
      "type": "error"
    },
    { "inputs": [], "name": "SequentialMintExceedsLimit", "type": "error" },
    { "inputs": [], "name": "SequentialUpToTooSmall", "type": "error" },
    { "inputs": [], "name": "SpotMintTokenIdTooSmall", "type": "error" },
    { "inputs": [], "name": "TokenAlreadyExists", "type": "error" },
    {
      "inputs": [],
      "name": "TransferCallerNotOwnerNorApproved",
      "type": "error"
    },
    { "inputs": [], "name": "TransferFromIncorrectOwner", "type": "error" },
    {
      "inputs": [],
      "name": "TransferToNonERC721ReceiverImplementer",
      "type": "error"
    },
    { "inputs": [], "name": "TransferToZeroAddress", "type": "error" },
    { "inputs": [], "name": "URIQueryForNonexistentToken", "type": "error" },
    { "inputs": [], "name": "WithdrawalFailed", "type": "error" },
    {
      "anonymous": false,
      "inputs": [
        {
          "indexed": true,
          "internalType": "address",
          "name": "owner",
          "type": "address"
        },
        {
          "indexed": true,
          "internalType": "address",
          "name": "approved",
          "type": "address"
        },
        {
          "indexed": true,
          "internalType": "uint256",
          "name": "tokenId",
          "type": "uint256"
        }
      ],
      "name": "Approval",
      "type": "event"
    },
    {
      "anonymous": false,
      "inputs": [
        {
          "indexed": true,
          "internalType": "address",
          "name": "owner",
          "type": "address"
        },
        {
          "indexed": true,
          "internalType": "address",
          "name": "operator",
          "type": "address"
        },
        {
          "indexed": false,
          "internalType": "bool",
          "name": "approved",
          "type": "bool"
        }
      ],
      "name": "ApprovalForAll",
      "type": "event"
    },
    {
      "anonymous": false,
      "inputs": [
        {
          "indexed": false,
          "internalType": "string",
          "name": "newBaseURI",
          "type": "string"
        },
        {
          "indexed": false,
          "internalType": "uint256",
          "name": "timestamp",
          "type": "uint256"
        }
      ],
      "name": "BaseURISet",
      "type": "event"
    },
    {
      "anonymous": false,
      "inputs": [
        {
          "indexed": true,
          "internalType": "uint256",
          "name": "fromTokenId",
          "type": "uint256"
        },
        {
          "indexed": false,
          "internalType": "uint256",
          "name": "toTokenId",
          "type": "uint256"
        },
        {
          "indexed": true,
          "internalType": "address",
          "name": "from",
          "type": "address"
        },
        {
          "indexed": true,
          "internalType": "address",
          "name": "to",
          "type": "address"
        }
      ],
      "name": "ConsecutiveTransfer",
      "type": "event"
    },
    {
      "anonymous": false,
      "inputs": [
        {
          "indexed": false,
          "internalType": "uint256",
          "name": "newMaxMintsPerWallet",
          "type": "uint256"
        },
        {
          "indexed": false,
          "internalType": "uint256",
          "name": "timestamp",
          "type": "uint256"
        }
      ],
      "name": "MaxMintsPerWalletEdited",
      "type": "event"
    },
    {
      "anonymous": false,
      "inputs": [
        {
          "indexed": true,
          "internalType": "address",
          "name": "minter",
          "type": "address"
        },
        {
          "indexed": false,
          "internalType": "uint256",
          "name": "quantity",
          "type": "uint256"
        },
        {
          "indexed": false,
          "internalType": "uint256",
          "name": "timestamp",
          "type": "uint256"
        }
      ],
      "name": "Mint",
      "type": "event"
    },
    {
      "anonymous": false,
      "inputs": [
        {
          "indexed": false,
          "internalType": "uint256",
          "name": "newPrice",
          "type": "uint256"
        },
        {
          "indexed": false,
          "internalType": "uint256",
          "name": "timestamp",
          "type": "uint256"
        }
      ],
      "name": "MintPriceEdited",
      "type": "event"
    },
    {
      "anonymous": false,
      "inputs": [
        {
          "indexed": true,
          "internalType": "address",
          "name": "previousOwner",
          "type": "address"
        },
        {
          "indexed": true,
          "internalType": "address",
          "name": "newOwner",
          "type": "address"
        }
      ],
      "name": "OwnershipTransferred",
      "type": "event"
    },
    {
      "anonymous": false,
      "inputs": [
        {
          "indexed": false,
          "internalType": "uint256",
          "name": "newSupplyCap",
          "type": "uint256"
        },
        {
          "indexed": false,
          "internalType": "uint256",
          "name": "timestamp",
          "type": "uint256"
        }
      ],
      "name": "SupplyCapEdited",
      "type": "event"
    },
    {
      "anonymous": false,
      "inputs": [
        {
          "indexed": false,
          "internalType": "uint256",
          "name": "mintStart",
          "type": "uint256"
        },
        {
          "indexed": false,
          "internalType": "uint256",
          "name": "mintEnd",
          "type": "uint256"
        },
        {
          "indexed": false,
          "internalType": "uint256",
          "name": "timestamp",
          "type": "uint256"
        }
      ],
      "name": "TimestampsEdited",
      "type": "event"
    },
    {
      "anonymous": false,
      "inputs": [
        {
          "indexed": true,
          "internalType": "address",
          "name": "from",
          "type": "address"
        },
        {
          "indexed": true,
          "internalType": "address",
          "name": "to",
          "type": "address"
        },
        {
          "indexed": true,
          "internalType": "uint256",
          "name": "tokenId",
          "type": "uint256"
        }
      ],
      "name": "Transfer",
      "type": "event"
    },
    {
      "anonymous": false,
      "inputs": [
        {
          "indexed": true,
          "internalType": "address",
          "name": "caller",
          "type": "address"
        },
        {
          "indexed": false,
          "internalType": "uint256",
          "name": "totalAmount",
          "type": "uint256"
        },
        {
          "indexed": false,
          "internalType": "uint256",
          "name": "timestamp",
          "type": "uint256"
        }
      ],
      "name": "Withdrawal",
      "type": "event"
    },
    {
      "inputs": [
        { "internalType": "address", "name": "to", "type": "address" },
        { "internalType": "uint256", "name": "tokenId", "type": "uint256" }
      ],
      "name": "approve",
      "outputs": [],
      "stateMutability": "payable",
      "type": "function"
    },
    {
      "inputs": [
        { "internalType": "address", "name": "owner", "type": "address" }
      ],
      "name": "balanceOf",
      "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
      "stateMutability": "view",
      "type": "function"
    },
    {
      "inputs": [
        {
          "internalType": "uint256",
          "name": "newMaxMintsPerWallet",
          "type": "uint256"
        }
      ],
      "name": "editMaxMintsPerWallet",
      "outputs": [],
      "stateMutability": "nonpayable",
      "type": "function"
    },
    {
      "inputs": [
        { "internalType": "uint256", "name": "newSupply", "type": "uint256" }
      ],
      "name": "editMaxSupply",
      "outputs": [],
      "stateMutability": "nonpayable",
      "type": "function"
    },
    {
      "inputs": [
        { "internalType": "uint256", "name": "newPrice", "type": "uint256" }
      ],
      "name": "editMintPrice",
      "outputs": [],
      "stateMutability": "nonpayable",
      "type": "function"
    },
    {
      "inputs": [
        { "internalType": "uint256", "name": "startTime", "type": "uint256" },
        { "internalType": "uint256", "name": "endTime", "type": "uint256" }
      ],
      "name": "editTimestamps",
      "outputs": [],
      "stateMutability": "nonpayable",
      "type": "function"
    },
    {
      "inputs": [
        { "internalType": "uint256", "name": "tokenId", "type": "uint256" }
      ],
      "name": "getApproved",
      "outputs": [{ "internalType": "address", "name": "", "type": "address" }],
      "stateMutability": "view",
      "type": "function"
    },
    {
      "inputs": [],
      "name": "getBaseURI",
      "outputs": [{ "internalType": "string", "name": "", "type": "string" }],
      "stateMutability": "view",
      "type": "function"
    },
    {
      "inputs": [],
      "name": "getEndTimestamp",
      "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
      "stateMutability": "view",
      "type": "function"
    },
    {
      "inputs": [],
      "name": "getMaxMintsPerWallet",
      "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
      "stateMutability": "view",
      "type": "function"
    },
    {
      "inputs": [],
      "name": "getMaxSupply",
      "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
      "stateMutability": "view",
      "type": "function"
    },
    {
      "inputs": [],
      "name": "getMintPrice",
      "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
      "stateMutability": "view",
      "type": "function"
    },
    {
      "inputs": [],
      "name": "getStartTimestamp",
      "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
      "stateMutability": "view",
      "type": "function"
    },
    {
      "inputs": [
        { "internalType": "address", "name": "userAddress", "type": "address" }
      ],
      "name": "getUserMintedNftCount",
      "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
      "stateMutability": "view",
      "type": "function"
    },
    {
      "inputs": [
        { "internalType": "address", "name": "owner", "type": "address" },
        { "internalType": "address", "name": "operator", "type": "address" }
      ],
      "name": "isApprovedForAll",
      "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }],
      "stateMutability": "view",
      "type": "function"
    },
    {
      "inputs": [
        { "internalType": "uint256", "name": "quantity", "type": "uint256" }
      ],
      "name": "mint",
      "outputs": [],
      "stateMutability": "payable",
      "type": "function"
    },
    {
      "inputs": [],
      "name": "name",
      "outputs": [{ "internalType": "string", "name": "", "type": "string" }],
      "stateMutability": "view",
      "type": "function"
    },
    {
      "inputs": [],
      "name": "owner",
      "outputs": [{ "internalType": "address", "name": "", "type": "address" }],
      "stateMutability": "view",
      "type": "function"
    },
    {
      "inputs": [
        { "internalType": "uint256", "name": "tokenId", "type": "uint256" }
      ],
      "name": "ownerOf",
      "outputs": [{ "internalType": "address", "name": "", "type": "address" }],
      "stateMutability": "view",
      "type": "function"
    },
    {
      "inputs": [],
      "name": "renounceOwnership",
      "outputs": [],
      "stateMutability": "nonpayable",
      "type": "function"
    },
    {
      "inputs": [
        { "internalType": "address", "name": "from", "type": "address" },
        { "internalType": "address", "name": "to", "type": "address" },
        { "internalType": "uint256", "name": "tokenId", "type": "uint256" }
      ],
      "name": "safeTransferFrom",
      "outputs": [],
      "stateMutability": "payable",
      "type": "function"
    },
    {
      "inputs": [
        { "internalType": "address", "name": "from", "type": "address" },
        { "internalType": "address", "name": "to", "type": "address" },
        { "internalType": "uint256", "name": "tokenId", "type": "uint256" },
        { "internalType": "bytes", "name": "_data", "type": "bytes" }
      ],
      "name": "safeTransferFrom",
      "outputs": [],
      "stateMutability": "payable",
      "type": "function"
    },
    {
      "inputs": [
        { "internalType": "address", "name": "operator", "type": "address" },
        { "internalType": "bool", "name": "approved", "type": "bool" }
      ],
      "name": "setApprovalForAll",
      "outputs": [],
      "stateMutability": "nonpayable",
      "type": "function"
    },
    {
      "inputs": [
        { "internalType": "string", "name": "baseURI", "type": "string" }
      ],
      "name": "setBaseURI",
      "outputs": [],
      "stateMutability": "nonpayable",
      "type": "function"
    },
    {
      "inputs": [
        { "internalType": "bytes4", "name": "interfaceId", "type": "bytes4" }
      ],
      "name": "supportsInterface",
      "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }],
      "stateMutability": "view",
      "type": "function"
    },
    {
      "inputs": [],
      "name": "symbol",
      "outputs": [{ "internalType": "string", "name": "", "type": "string" }],
      "stateMutability": "view",
      "type": "function"
    },
    {
      "inputs": [
        { "internalType": "uint256", "name": "tokenId", "type": "uint256" }
      ],
      "name": "tokenURI",
      "outputs": [{ "internalType": "string", "name": "", "type": "string" }],
      "stateMutability": "view",
      "type": "function"
    },
    {
      "inputs": [],
      "name": "totalSupply",
      "outputs": [
        { "internalType": "uint256", "name": "result", "type": "uint256" }
      ],
      "stateMutability": "view",
      "type": "function"
    },
    {
      "inputs": [
        { "internalType": "address", "name": "from", "type": "address" },
        { "internalType": "address", "name": "to", "type": "address" },
        { "internalType": "uint256", "name": "tokenId", "type": "uint256" }
      ],
      "name": "transferFrom",
      "outputs": [],
      "stateMutability": "payable",
      "type": "function"
    },
    {
      "inputs": [
        { "internalType": "address", "name": "newOwner", "type": "address" }
      ],
      "name": "transferOwnership",
      "outputs": [],
      "stateMutability": "nonpayable",
      "type": "function"
    },
    {
      "inputs": [],
      "name": "withdraw",
      "outputs": [],
      "stateMutability": "nonpayable",
      "type": "function"
    }
  ]
}

Copy and paste source code

"use client";

import React, { useState, useEffect } from "react";
import Image from "next/image";
import {
  Card,
  CardContent,
  CardHeader,
  CardTitle,
  CardDescription,
} from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import { useToast } from "@/hooks/use-toast";
import { formatEther } from "viem";
import {
  useAccount,
  useChainId,
  useSwitchChain,
  useWriteContract,
  useWaitForTransactionReceipt,
  useReadContracts,
} from "wagmi";
import { useAppKit } from "@reown/appkit/react";
import Nft from "@/contracts/NftMint.json";

const NftMintCard = ({
  contractAddress,
  activeChainId,
}: {
  contractAddress: `0x${string}`;
  activeChainId: number;
}) => {
  const [mounted, setMounted] = useState<boolean>(false);
  const [mintQuantity, setMintQuantity] = useState<number>(1);
  const { address, isConnected, isConnecting, isReconnecting } = useAccount();
  const { open } = useAppKit();
  const { toast } = useToast();
  const { switchChain } = useSwitchChain();
  const currentChainId = useChainId();

  const currentUnixTimestamp = Math.floor(Date.now()) / 1000;

  // ================================================== CONTRACT READS ===================================================
  const { data, isLoading, refetch } = useReadContracts({
    contracts: [
      {
        address: contractAddress,
        abi: Nft.abi,
        functionName: "totalSupply",
      },
      {
        address: contractAddress,
        abi: Nft.abi,
        functionName: "getMaxSupply",
      },
      {
        address: contractAddress,
        abi: Nft.abi,
        functionName: "getUserMintedNftCount",
        args: [address],
      },
      {
        address: contractAddress,
        abi: Nft.abi,
        functionName: "getMaxMintsPerWallet",
      },
      {
        address: contractAddress,
        abi: Nft.abi,
        functionName: "getMintPrice",
      },
      {
        address: contractAddress,
        abi: Nft.abi,
        functionName: "getStartTimestamp",
      },
      {
        address: contractAddress,
        abi: Nft.abi,
        functionName: "getEndTimestamp",
      },
    ],
  });

  // ================================================== CONTRACT WRITES ==================================================
  const { data: mintData, writeContract } = useWriteContract({
    mutation: {
      onError: (error) =>
        toast({
          title: "Execution reverted",
          // @ts-expect-error - shortMessage is not in the type but it exists
          description: error.shortMessage,
          variant: "destructive",
        }),
    },
  });

  const {
    isSuccess: isTxSuccessful,
    isLoading: isTxProcessing,
    isError: isTxError,
  } = useWaitForTransactionReceipt({
    hash: mintData,
  });

  const handleMint = () => {
    try {
      writeContract({
        abi: Nft.abi,
        address: contractAddress,
        functionName: "mint",
        args: [mintQuantity],
        value: (data?.[4]?.result as bigint) * BigInt(mintQuantity),
      });
    } catch (error) {
      console.log(error);
    }
  };

  const handleIncrement = (remainingAllocation: number) => {
    if (mintQuantity < remainingAllocation) {
      setMintQuantity(mintQuantity + 1);
    }
  };

  const handleDecrement = () => {
    if (mintQuantity > 1) {
      setMintQuantity(mintQuantity - 1);
    }
  };

  const handleChainSwitch = () => {
    if (currentChainId === activeChainId) {
      return;
    }

    switchChain({ chainId: activeChainId });
  };

  useEffect(() => {
    if (isTxSuccessful) {
      toast({
        title: "NFT Minted",
        description: "Your NFT(s) has been minted successfully",
      });
      refetch();
      setMintQuantity(1);
    }

    if (isTxError) {
      toast({
        title: "Execution reverted",
        description: "Your transaction was reverted when processing.",
        variant: "destructive",
      });
    }
  }, [isTxSuccessful, isTxError]);

  useEffect(() => {
    setMounted(true);
  }, []);

  if (!mounted)
    return (
      <div className="w-full h-56 flex items-center">
        <div className="flex justify-center items-center gap-2 w-full h-44 text-[#A1A1AA]">
          <div className="w-3.5 h-3.5 border-2 border-gray-400 border-b-transparent rounded-full inline-block box-border animate-spin" />
          Loading
        </div>
      </div>
    );

  return (
    <Card className="max-w-[650px] md:w-[650px] md:h-[347px] shadow border-[0.5px] border-white/15">
      <CardHeader>
        <CardTitle>Mint your NFT</CardTitle>
        <CardDescription>
          Mint your very own unique NFT in a couple clicks.
        </CardDescription>
      </CardHeader>
      <CardContent className="flex flex-col md:flex-row items-center gap-10 w-full">
        <Image
          src="https://image.binance.vision/editor-uploads-original/9c15d9647b9643dfbc5e522299d13593.png"
          alt="NFT"
          height={230}
          width={230}
          className="rounded-xl"
        />

        {!isConnected || !address || isReconnecting || isConnecting ? (
          <div className="flex flex-col w-full">
            <p className="text-white text-sm py-20 text-center">
              Please connect wallet to mint your NFT
            </p>
            <Button
              onClick={() => open()}
              className="bg-indigo-500 w-full mt-2 text-white rounded-[4px] hover:text-black"
            >
              Connect wallet
            </Button>
          </div>
        ) : currentChainId !== activeChainId ? (
          <div className="flex flex-col w-full">
            <p className=" text-sm py-20 text-center ">
              Please connect to sepolia network
            </p>
            <Button
              onClick={handleChainSwitch}
              className="bg-red-500 w-full mt-2 rounded-[4px] hover:text-black text-white"
            >
              Switch to sepolia
            </Button>
          </div>
        ) : isLoading ? (
          <div className="w-full font-medium">
            <div className="flex justify-center">
              <Skeleton className="h-5 w-32 mb-6" />
            </div>
            <div className="flex justify-between gap-4 mb-2">
              <Skeleton className="h-5 w-14" />
              <Skeleton className="h-5 w-20" />
            </div>
            <div className="flex justify-between gap-4 mb-2">
              <Skeleton className="h-5 w-14" />
              <Skeleton className="h-5 w-20" />
            </div>
            <div className="flex justify-between items-center gap-4 mb-4">
              <Skeleton className="h-5 w-14" />
              <Skeleton className="h-5 w-20" />
            </div>

            <div className="flex justify-between items-center">
              <Button>-</Button>
              <p className="font-bold text-3xl">1</p>
              <Button>+</Button>
            </div>

            <Button
              disabled
              className="bg-indigo-500 mt-6 w-full flex items-center gap-2"
            >
              <div className="w-3.5 h-3.5 border-2 border-gray-400 border-b-transparent rounded-full inline-block box-border animate-spin" />{" "}
              loading
            </Button>
          </div>
        ) : (
          data &&
          Array.isArray(data) &&
          data[0]?.result != null &&
          data[1]?.result != null &&
          data[2]?.result != null &&
          data[3]?.result != null &&
          data[4]?.result != null &&
          data[5]?.result != null &&
          data[6]?.result != null && (
            <div className="w-full font-medium">
              <p className="font-bold text-center mb-4">
                {data[0].result.toString()} / {data[1].result.toString()} Minted
              </p>

              <div className="flex justify-between gap-4 mb-2">
                <p>Remaining allocation</p>
                <p>{Number(data[3].result) - Number(data[2].result)}</p>
              </div>
              <div className="flex justify-between gap-4 mb-2">
                <p>Price</p>
                <p>
                  {Number(formatEther(data[4].result as bigint)).toFixed(2)} Eth
                </p>
              </div>
              <div className="flex justify-between items-center gap-4 mb-4">
                <p>Mint status</p>

                {currentUnixTimestamp < Number(data[5].result) ? (
                  <p className="text-red-600">Not started</p>
                ) : currentUnixTimestamp > Number(data[6].result) ? (
                  <p className="text-red-600">Ended</p>
                ) : (
                  <p className="text-green-600">Live</p>
                )}
              </div>

              <div className="flex justify-between items-center">
                <Button
                  onClick={handleDecrement}
                  className="bg-indigo-500 text-white hover:text-black rounded-[4px]"
                >
                  -
                </Button>
                <p className="font-bold text-3xl">{mintQuantity}</p>
                <Button
                  onClick={() =>
                    handleIncrement(
                      Number(data[3].result) - Number(data[2].result)
                    )
                  }
                  className="bg-indigo-500 text-white hover:text-black rounded-[4px]"
                >
                  +
                </Button>
              </div>

              <Button
                onClick={handleMint}
                disabled={
                  isTxProcessing ||
                  Number(data[3].result) === Number(data[2].result)
                }
                className="bg-indigo-500 mt-4 w-full text-white hover:text-black rounded-[4px]"
              >
                {Number(data[3].result) === Number(data[2].result) ? (
                  "Max allocation minted"
                ) : isTxProcessing ? (
                  <div className="flex items-center gap-2">
                    <div className="w-3.5 h-3.5 border-2 border-gray-400 border-b-transparent rounded-full inline-block box-border animate-spin" />{" "}
                    Minting {mintQuantity} {mintQuantity === 1 ? "NFT" : "NFTs"}
                  </div>
                ) : (
                  `Mint ${mintQuantity} for
                  ${(
                    mintQuantity * Number(formatEther(data[4].result as bigint))
                  ).toFixed(2)} Eth`
                )}
              </Button>
            </div>
          )
        )}
      </CardContent>
    </Card>
  );
};

export default NftMintCard;

Props

PropTypeDescriptionDefault value
contractAddress`0x${string}`Deployed NFT smart contract address of your collectionundefined
activeChainIdnumberChain ID of the network to be used (must match the network you deployed your smart contract to)undefined

See something you like?

Take your project further with our advanced custom development.

"One of the only full-stack Web3 component libraries i've seen in the space so far. Ten out of ten recommended. Saved me a ton of time. Can't wait to see what templates they release next."

Samy

Side projects builder