paint-brush
How to Build a Profitable NFT Marketplace with React, Solidity, and CometChatby@daltonic
13,845 reads
13,845 reads

How to Build a Profitable NFT Marketplace with React, Solidity, and CometChat

by Darlington Gospel July 23rd, 2022
Read on Terminal Reader
Read this story w/o Javascript

Too Long; Didn't Read

This tutorial will teach you how to create a profitable and well-designed NFT marketplace with chat functionality. It will use web3 technology to create non-fungible tokens (NFTs) and digitalize assets while retaining ownership rights. You will need the following tools to successfully crush this build: Node, Ganache-Cli, and Truffle.

Companies Mentioned

Mention Thumbnail
Mention Thumbnail

Coin Mentioned

Mention Thumbnail
featured image - How to Build a Profitable NFT Marketplace with React, Solidity, and CometChat
Darlington Gospel  HackerNoon profile picture


What you will be building, see a demo on the Rinkeby test network and git repo here…


Introduction

Web3 is the internet's future, and we must all learn and embrace this technology. Its use-cases continue to emerge and have a positive impact on the world.


One magnificent application of web3 technology is the creation of non-fungible tokens (NFTs), which are a viable solution for digitalizing assets while retaining ownership rights.



This tutorial will teach you how to create a profitable and well-designed NFT marketplace with chat functionality.


Book your private classes with me if you need someone to help you learn web3 development faster.


With that said, let's get started...


Check out my Youtube channel for FREE web3 tutorials now.

Prerequisite

You will need the following tools installed to successfully crush this build:

  • Node
  • Ganache-Cli
  • Truffle
  • React
  • Infura
  • Tailwind CSS
  • CometChat SDK
  • Metamask
  • Yarn

Installing Dependencies

NodeJs Installation Check that NodeJs is already installed on your machine, and if not, install it from HERE. Run the code again on the terminal to ensure it is installed.


Yarn, Ganache-cli and Truffle Installation Run the commands below in your terminal to install these critical packages globally.


npm i -g yarn
npm i -g truffle
npm i -g ganache-cli


To confirm installation, enter the following code into the terminal.


yarn --version && ganache-cli --version && truffle version

Cloning Web3 Starter Project Clone the web 3.0 starter project using the commands below. This ensures that we're all on the same page and using the same software.


git clone https://github.com/Daltonic/timelessNFT


Excellent, please replace the **package.json** file with the following:


{
  "name": "TimelessNFT",
  "private": true,
  "version": "0.0.0",
  "scripts": {
    "start": "react-app-rewired start",
    "build": "react-app-rewired build",
    "test": "react-app-rewired test",
    "eject": "react-scripts eject"
  },
  "dependencies": {
    "@cometchat-pro/chat": "^3.0.9",
    "ipfs-http-client": "^56.0.0",
    "moment": "^2.29.4",
    "moment-timezone": "^0.5.34",
    "react": "^17.0.2",
    "react-dom": "^17.0.2",
    "react-hooks-global-state": "^1.0.2",
    "react-icons": "^4.3.1",
    "react-identicons": "^1.2.5",
    "react-moment": "^1.1.2",
    "react-router-dom": "6",
    "react-scripts": "5.0.0",
    "web-vitals": "^2.1.4",
    "web3": "^1.7.1"
  },
  "devDependencies": {
    "@faker-js/faker": "^6.0.0-alpha.5",
    "@openzeppelin/contracts": "^4.5.0",
    "@tailwindcss/forms": "0.4.0",
    "@truffle/hdwallet-provider": "^2.0.4",
    "assert": "^2.0.0",
    "autoprefixer": "10.4.2",
    "babel-polyfill": "^6.26.0",
    "babel-preset-env": "^1.7.0",
    "babel-preset-es2015": "^6.24.1",
    "babel-preset-stage-2": "^6.24.1",
    "babel-preset-stage-3": "^6.24.1",
    "babel-register": "^6.26.0",
    "buffer": "^6.0.3",
    "chai": "^4.3.6",
    "chai-as-promised": "^7.1.1",
    "crypto-browserify": "^3.12.0",
    "dotenv": "^16.0.0",
    "https-browserify": "^1.0.0",
    "mnemonics": "^1.1.3",
    "os-browserify": "^0.3.0",
    "postcss": "8.4.5",
    "process": "^0.11.10",
    "react-app-rewired": "^2.1.11",
    "sharp": "^0.30.1",
    "stream-browserify": "^3.0.0",
    "stream-http": "^3.2.0",
    "tailwindcss": "3.0.18",
    "url": "^0.11.0"
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  }
}


After replacing the packages as directed, run **yarn install** on your terminal to load all of the packages with the specified versions.

Configuring CometChat SDK

Follow the steps below to configure the CometChat SDK; at the end, you must save these keys as an environment variable.


STEP 1: Head to CometChat Dashboard and create an account.


STEP 2: Log in to the CometChat dashboard, only after registering.


STEP 3: From the dashboard, add a new app called timelessNFT.


STEP 4: Select the app you just created from the list.


STEP 5: From the Quick Start copy the APP_ID, REGION, and AUTH_KEY, to your .env file. See the image and code snippet.


Replace the REACT_COMET_CHAT placeholders keys with their appropriate values.

REACT_APP_COMET_CHAT_REGION=**
REACT_APP_COMET_CHAT_APP_ID=**************
REACT_APP_COMET_CHAT_AUTH_KEY=******************************

Configuring Infura App

STEP 1: Head to Infura, and create an account.



STEP 2: From the dashboard create a new project.



STEP 3: Copy the Rinkeby test network WebSocket endpoint URL to your .env file.



After that, enter your Metamask secret phrase and preferred account's private key. If you followed the instructions correctly, your environment variables should now look like this.


ENDPOINT_URL=***************************
DEPLOYER_KEY=**********************

REACT_APP_COMET_CHAT_REGION=**
REACT_APP_COMET_CHAT_APP_ID=**************
REACT_APP_COMET_CHAT_AUTH_KEY=******************************


See the section below if you don't know how to access your private key.

Accessing Your Metamask Private Key

STEP 1: Make sure Rinkeby is selected as the test network in your Metamask browser extension. Then, on the preferred account, click the vertical dotted line and choose account details. Please see the image below.


STEP 2: Enter your password on the field provided and click the confirm button, this will enable you to access your account private key.


STEP 3: Click on "export private key" to see your private key. Make sure you never expose your keys on a public page such as Github. That is why we are appending it as an environment variable.


STEP 4: Copy your private key to your .env file. See the image and code snippet below.



ENDPOINT_URL=***************************
SECRET_KEY=******************
DEPLOYER_KEY=**********************

REACT_APP_COMET_CHAT_REGION=**
REACT_APP_COMET_CHAT_APP_ID=**************
REACT_APP_COMET_CHAT_AUTH_KEY=******************************


As for your SECRET_KEY, you are required to paste your Metamask secret phrase in the space provided in the environment file.

The Timeless NFT Smart Contract

Here is the complete smart contract code; Let’s go over all of the functions and variables one by one.


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

import "./ERC721.sol";
import "./ERC721Enumerable.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract TimelessNFT is ERC721Enumerable, Ownable {
    using Strings for uint256;

    mapping(string => uint8) existingURIs;
    mapping(uint256 => address) public holderOf;

    address public artist;
    uint256 public royalityFee;
    uint256 public supply = 0;
    uint256 public totalTx = 0;
    uint256 public cost = 0.01 ether;

    event Sale(
        uint256 id,
        address indexed owner,
        uint256 cost,
        string metadataURI,
        uint256 timestamp
    );

    struct TransactionStruct {
        uint256 id;
        address owner;
        uint256 cost;
        string title;
        string description;
        string metadataURI;
        uint256 timestamp;
    }

    TransactionStruct[] transactions;
    TransactionStruct[] minted;

    constructor(
        string memory _name,
        string memory _symbol,
        uint256 _royalityFee,
        address _artist
    ) ERC721(_name, _symbol) {
        royalityFee = _royalityFee;
        artist = _artist;
    }

    function payToMint(
        string memory title,
        string memory description,
        string memory metadataURI,
        uint256 salesPrice
    ) external payable {
        require(msg.value >= cost, "Ether too low for minting!");
        require(existingURIs[metadataURI] == 0, "This NFT is already minted!");
        require(msg.sender != owner(), "Sales not allowed!");
        

        uint256 royality = (msg.value * royalityFee) / 100;
        payTo(artist, royality);
        payTo(owner(), (msg.value - royality));

        supply++;

        minted.push(
            TransactionStruct(
                supply,
                msg.sender,
                salesPrice,
                title,
                description,
                metadataURI,
                block.timestamp
            )
        );

        emit Sale(
            supply,
            msg.sender,
            msg.value,
            metadataURI,
            block.timestamp
        );

        _safeMint(msg.sender, supply);
        existingURIs[metadataURI] = 1;
        holderOf[supply] = msg.sender;
    }

    function payToBuy(uint256 id) external payable {
        require(msg.value >= minted[id - 1].cost, "Ether too low for purchase!");
        require(msg.sender != minted[id - 1].owner, "Operation Not Allowed!");

        uint256 royality = (msg.value * royalityFee) / 100;
        payTo(artist, royality);
        payTo(minted[id - 1].owner, (msg.value - royality));

        totalTx++;

        transactions.push(
            TransactionStruct(
                totalTx,
                msg.sender,
                msg.value,
                minted[id - 1].title,
                minted[id - 1].description,
                minted[id - 1].metadataURI,
                block.timestamp
            )
        );

        emit Sale(
            totalTx,
            msg.sender,
            msg.value,
            minted[id - 1].metadataURI,
            block.timestamp
        );

        minted[id - 1].owner = msg.sender;
    }

    function changePrice(uint256 id, uint256 newPrice) external returns (bool) {
        require(newPrice > 0 ether, "Ether too low!");
        require(msg.sender == minted[id - 1].owner, "Operation Not Allowed!");

        minted[id - 1].cost = newPrice;
        return true;
    }

    function payTo(address to, uint256 amount) internal {
        (bool success, ) = payable(to).call{value: amount}("");
        require(success);
    }

    function getAllNFTs() external view returns (TransactionStruct[] memory) {
        return minted;
    }

    function getNFT(uint256 id) external view returns (TransactionStruct memory) {
        return minted[id - 1];
    }

    function getAllTransactions() external view returns (TransactionStruct[] memory) {
        return transactions;
    }
}


Code imports and contract information In the code below, we informed the solidity compiler of the license identifier and compiler versions qualified to compile this code.


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

import "./ERC721.sol";
import "./ERC721Enumerable.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract TimelessNFT is ERC721Enumerable, Ownable {
  // codes goes in here...
}


Also, this smart contract makes use of some openzepplin's ERC721 smart contracts. You have to make sure that you put them in the same directory as seen in the image below.


Visit this link and download these smart contracts as shown in the image above.


State variables declarations

using Strings for uint256;

mapping(string => uint8) existingURIs;
mapping(uint256 => address) public holderOf;

address public artist;
uint256 public royalityFee;
uint256 public supply = 0;
uint256 public totalTx = 0;
uint256 public cost = 0.01 ether;


We specified that we’re using the string library for performing uint to string operations. Next, we also declared mappings for recording minted NFT artworks and also for knowing the current owner of a token.


Then we specified the other essential variables for capturing the artist account, the royalty fees, the current supply, total transactions that have been made on the platform, and the mint cost of an NFT.


Setting up events and structures

event Sale(
    uint256 id,
    address indexed owner,
    uint256 cost,
    string metadataURI,
    uint256 timestamp
);

struct TransactionStruct {
    uint256 id;
    address owner;
    uint256 cost;
    string title;
    string description;
    string metadataURI;
    uint256 timestamp;
}

TransactionStruct[] transactions;
TransactionStruct[] minted;


In the preceding code, we have a sales event that emits data from any transaction that occurs on the smart contract, whether it is on minting or NFT transfer.


We designed a transaction structure to collect data about minted or transferred NFTs. Using the transaction structure we defined, we created two variables called transactions and minted.


Initializing the constructor

constructor(
    string memory _name,
    string memory _symbol,
    uint256 _royalityFee,
    address _artist
) ERC721(_name, _symbol) {
    royalityFee = _royalityFee;
    artist = _artist;
}


The constructor takes in four parameters for initializing the smart contract. A token name, symbol, an artist account, and a royalty fee per transaction. The token name and symbol are then passed into the ERC721 smart contract during deployment.


The mint function algorithm

function payToMint(
    string memory title,
    string memory description,
    string memory metadataURI,
    uint256 salesPrice
) external payable {
    require(msg.value >= cost, "Ether too low for minting!");
    require(existingURIs[metadataURI] == 0, "This NFT is already minted!");
    require(msg.sender != owner(), "Sales not allowed!");
    

    uint256 royality = (msg.value * royalityFee) / 100;
    payTo(artist, royality);
    payTo(owner(), (msg.value - royality));

    supply++;

    minted.push(
        TransactionStruct(
            supply,
            msg.sender,
            salesPrice,
            title,
            description,
            metadataURI,
            block.timestamp
        )
    );

    emit Sale(
        supply,
        msg.sender,
        msg.value,
        metadataURI,
        block.timestamp
    );

    _safeMint(msg.sender, supply);
    existingURIs[metadataURI] = 1;
    holderOf[supply] = msg.sender;
}


The above function is responsible for minting new tokens on the smart contract. The caller of this method must provide four parameters which include; an NFT title, description, metadata URI, and the selling price of the NFT after minting.


Validations are carried out to ensure that the NFT is minting and are done accordingly with payments made for each minting. Also, the validation ensures that each artwork is uniquely linked with a token, and no other token bears the same artwork. Lastly for the validation, we made sure that the caller of this method is not the deployer of the smart contract, this is to ensure we don’t mix up things too bad.


Next in the function is the payment sharing rule. The royalty percentage goes to the artist and the rest of the ethers goes to the owner.


Afterward, we recorded that NFT inside the minted array and emitted a sales event. Lastly, we minted the NFT while recording the caller’s address as the owner of the token.


The NFT transfer function algorithm

function payToBuy(uint256 id) external payable {
    require(msg.value >= minted[id - 1].cost, "Ether too low for purchase!");
    require(msg.sender != minted[id - 1].owner, "Operation Not Allowed!");

    uint256 royality = (msg.value * royalityFee) / 100;
    payTo(artist, royality);
    payTo(minted[id - 1].owner, (msg.value - royality));

    totalTx++;

    transactions.push(
        TransactionStruct(
            totalTx,
            msg.sender,
            msg.value,
            minted[id - 1].title,
            minted[id - 1].description,
            minted[id - 1].metadataURI,
            block.timestamp
        )
    );

    emit Sale(
        totalTx,
        msg.sender,
        msg.value,
        minted[id - 1].metadataURI,
        block.timestamp
    );

    minted[id - 1].owner = msg.sender;
}


The above function takes an NFT id and makes a purchase of the NFT according to the set price by the minter (owner).


Necessary validations are done to obstruct owners from buying their NFTs and others from buying with zero ethers.


Next, a royalty fee is sent to the artist account and the current owner of the NFT gets to keep the rest.


Each token transfer is recorded in a transactions array to keep track of all transactions done on the platform.


Afterward, a sales event is again emitted for this purchase to enrich the logged data on the EVM.


Other Essential functions

// changes the price of an NFT
function changePrice(uint256 id, uint256 newPrice) external returns (bool) {
    require(newPrice > 0 ether, "Ether too low!");
    require(msg.sender == minted[id - 1].owner, "Operation Not Allowed!");

    minted[id - 1].cost = newPrice;
    return true;
}

// sends ethers to a specific account
function payTo(address to, uint256 amount) internal {
    (bool success, ) = payable(to).call{value: amount}("");
    require(success);
}

// returns all minted NFTs
function getAllNFTs() external view returns (TransactionStruct[] memory) {
    return minted;
}

// returns a specific NFT by token id
function getNFT(uint256 id) external view returns (TransactionStruct memory) {
    return minted[id - 1];
}

// returns all transactions
function getAllTransactions() external view returns (TransactionStruct[] memory) {
    return transactions;
}


And there you have it for developing the smart contract, we will next dive into building the UI components with ReactJs.

Configuring the Deployment Script

One more thing to do with the smart contract is to configure the deployment script.

On the project head to the migrations folder >> 2_deploy_contracts.js and update it with the code snippet below.


const TimelessNFT = artifacts.require('TimelessNFT')

module.exports = async (deployer) => {
  const accounts = await web3.eth.getAccounts()
  await deployer.deploy(
    TimelessNFT, 
    'Timeless NFTs', 
    'TNT', 10, accounts[1]
  )
}


Superb, we just finished the smart contract for our application; now it's time to get started on the DApp interface. If you need a private tutor to help you learn smart contract development, book your classes with me.

Developing the Frontend

The front end is made up of numerous components and parts. All of the components, views, and peripherals will be created by us.


Header Component


This component was created with tailwind CSS and uses the pink Connect Wallet button to access the Metamask wallet. The codes below demonstrate the programming.


import { useGlobalState } from '../store'
import timelessLogo from '../assets/timeless.png'
import { connectWallet } from '../TimelessNFT'

const Header = () => {
  const [connectedAccount] = useGlobalState('connectedAccount')

  return (
    <nav className="w-4/5 flex md:justify-center justify-between items-center py-4 mx-auto">
      <div className="md:flex-[0.5] flex-initial justify-center items-center">
        <img
          className="w-32 cursor-pointer"
          src={timelessLogo}
          alt="Timeless Logo"
        />
      </div>

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

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

export default Header


Hero Component


This component is responsible for displaying the connected wallet and also for launching the modal used for creating a new NFT. Additionally, it is responsible for signing in or up users for one-on-one chats with a seller of an NFT. Here is the code responsible for these actions.


import Identicon from 'react-identicons'
import { setGlobalState, useGlobalState, truncate } from '../store'
import { getConversations, loginWithCometChat, signUpWithCometChat } from '../CometChat'
import ChatList from './ChatList'
import { useState } from 'react'

const Hero = () => {
  const [connectedAccount] = useGlobalState('connectedAccount')
  const [currentUser] = useGlobalState('currentUser')
  const [recentOpened] = useGlobalState('recentOpened')
  const [conversations, setConversations] = useState([])

  const onCreatedNFT = () => {
    if (currentUser?.uid.toLowerCase() != connectedAccount.toLowerCase())
      return alert('Please login to receive chats from buyers!')

    setGlobalState('modal', 'scale-100')
  }

  const onLunchRecent = () => {
    getConversations().then((convs) => {
      setConversations(convs)
      setGlobalState('recentOpened', true)
    })
  }

  return (
    <div
      className="flex flex-col md:flex-row w-4/5 justify-between 
      items-center mx-auto py-10"
    >
      <div className="md:w-3/6 w-full">
        <div>
          <h1 className="text-white text-5xl font-bold">
            Buy and Sell <br /> Digital Arts, <br />
            <span className="text-gradient">NFTs</span> Collections
          </h1>
          <p className="text-gray-500 font-semibold text-sm mt-3">
            Mint and collect the hottest NFTs around.
          </p>
        </div>

        <div className="flex flex-row mt-5">
          {connectedAccount ? (
            <>
              <button
                className="shadow-xl shadow-black text-white
                bg-[#e32970] hover:bg-[#bd255f]
                rounded-full cursor-pointer p-2"
                onClick={onCreatedNFT}
              >
                Create NFT
              </button>
              <>
                {currentUser?.uid.toLowerCase() ==
                connectedAccount.toLowerCase() ? (
                  <button
                    className="text-white border border-gray-500 
                    hover:border-[#e32970] hover:bg-[#bd255f] cursor-pointer 
                    rounded-full p-2 mx-3"
                    onClick={onLunchRecent}
                  >
                    Recent Chats
                  </button>
                ) : (
                  <>
                    <button
                      className="text-white border border-gray-500 
                    hover:border-[#e32970] hover:bg-[#bd255f] cursor-pointer 
                    rounded-full p-2 mx-3"
                      onClick={() => loginWithCometChat(connectedAccount)}
                    >
                      Login for Chat
                    </button>
                    <button
                      className="text-white border border-gray-500 
                    hover:border-[#e32970] hover:bg-[#bd255f] cursor-pointer 
                    rounded-full p-2 mx-3"
                      onClick={() => signUpWithCometChat(connectedAccount, connectedAccount)}
                    >
                      Signup for Chat
                    </button>
                  </>
                )}
              </>
            </>
          ) : null}
        </div>

        <div className="w-3/4 flex justify-between items-center mt-5">
          <div>
            <p className="text-white font-bold">1231k</p>
            <small className="text-gray-300">User</small>
          </div>
          <div>
            <p className="text-white font-bold">152k</p>
            <small className="text-gray-300">Artwork</small>
          </div>
          <div>
            <p className="text-white font-bold">200k</p>
            <small className="text-gray-300">Artist</small>
          </div>
        </div>
      </div>

      <div
        className="shadow-xl shadow-black md:w-2/5 w-full 
      mt-10 md:mt-0 rounded-md overflow-hidden bg-gray-800"
      >
        <img
          src="https://images.cointelegraph.com/images/1434_aHR0cHM6Ly9zMy5jb2ludGVsZWdyYXBoLmNvbS91cGxvYWRzLzIwMjEtMDYvNGE4NmNmOWQtODM2Mi00YmVhLThiMzctZDEyODAxNjUxZTE1LmpwZWc=.jpg"
          alt="NFT Art"
          className="h-60 w-full object-cover"
        />
        <div className="flex justify-start items-center p-3">
          <Identicon
            string={
              connectedAccount
                ? connectedAccount.toLowerCase()
                : 'Connect Your Wallet'
            }
            size={50}
            className="h-10 w-10 object-contain rounded-full mr-3"
          />
          <div>
            <p className="text-white font-semibold">
              {connectedAccount
                ? truncate(connectedAccount, 4, 4, 11)
                : 'Connect Your Wallet'}
            </p>
            <small className="text-pink-800 font-bold">@you</small>
          </div>
        </div>
      </div>

      {recentOpened ? <ChatList users={conversations} /> : null}
    </div>
  )
}

export default Hero


Artworks component

This component is responsible for rendering the list of NFTs minted on the platform using the beautifully crafted tailwind CSS cards. Each card has an NFT image, title, description, price, and owner. See the codes below for its implementation.


import { useEffect, useState } from 'react'
import { setGlobalState, useGlobalState, truncate } from '../store'

const Artworks = () => {
  const [nfts] = useGlobalState('nfts')
  const [end, setEnd] = useState(4)
  const [count] = useState(4)
  const [collection, setCollection] = useState([])

  const setNFT = (nft) => {
    setGlobalState('nft', nft)
    setGlobalState('showModal', 'scale-100')
  }

  const getCollection = () => {
    return nfts.slice(0, end)
  }

  useEffect(() => {
    setCollection(getCollection())
  }, [nfts, end])
  

  return (
    <div className="bg-[#151c25] gradient-bg-artworks">
      <div className="w-4/5 py-10 mx-auto">
        <h4 className="text-white text-3xl font-bold uppercase text-gradient">
          {collection.length > 0 ? 'Latest Artworks' : 'No Artworks Yet'}
        </h4>

        <div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-6 md:gap-4 lg:gap-3 py-2.5">
          {collection.map((nft) => (
            <div
              key={nft.id}
              className="w-full shadow-xl shadow-black rounded-md overflow-hidden bg-gray-800 my-2 p-3"
            >
              <img
                src={nft.metadataURI}
                alt={truncate(nft.title, 6)}
                className="h-60 w-full object-cover shadow-lg shadow-black rounded-lg mb-3"
              />
              <h4 className="text-white font-semibold">{nft.title}</h4>
              <p className="text-gray-400 text-xs my-1">
                {truncate(nft.description)}
              </p>
              <div className="flex justify-between items-center mt-3 text-white">
                <div className="flex flex-col">
                  <small className="text-xs">Current Price</small>
                  <p className="text-sm font-semibold">{nft.cost} ETH</p>
                </div>

                <button
                  onClick={() => setNFT(nft)}
                  className="shadow-lg shadow-black text-white text-sm bg-[#e32970] hover:bg-[#bd255f] cursor-pointer rounded-full px-1.5 py-1"
                >
                  View Details
                </button>
              </div>
            </div>
          ))}
        </div>

        {collection.length > 0 && nfts.length > collection.length ? (
          <div className="text-center my-5">
            <button
              className="shadow-xl shadow-black text-white
            bg-[#e32970] hover:bg-[#bd255f]
            rounded-full cursor-pointer p-2"
            onClick={() => setEnd(end + count)}
            >
              Load More
            </button>
          </div>
        ) : null}
      </div>
    </div>
  )
}

export default Artworks


Transactions Component

This component is responsible for rendering all the transactions that took place in our smart contract. A transaction for example would be Alison purchasing an NFT from Duke. This purchase will be captured in this component as a transaction. See the snippet below.


import { useEffect, useState } from 'react'
import { BiTransfer } from 'react-icons/bi'
import { MdOpenInNew } from 'react-icons/md'
import { useGlobalState, truncate } from '../store'

const Transactions = () => {
  const [transactions] = useGlobalState('transactions')
  const [end, setEnd] = useState(3)
  const [count] = useState(3)
  const [collection, setCollection] = useState([])

  const getCollection = () => {
    return transactions.slice(0, end)
  }

  useEffect(() => {
    setCollection(getCollection())
  }, [transactions, end])

  return (
    <div className="bg-[#151c25]">
      <div className="w-4/5 py-10 mx-auto">
        <h4 className="text-white text-3xl font-bold uppercase text-gradient">
          {collection.length > 0 ? 'Latest Transactions' : 'No Transaction Yet'}
        </h4>

        <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 md:gap-4 lg:gap-2 py-2.5">
          {collection
            .map((tx) => (
              <div
                key={tx.id}
                className="flex justify-between items-center border border-pink-500 text-gray-400 w-full shadow-xl shadow-black rounded-md overflow-hidden bg-gray-800 my-2 p-3"
              >
                <div className="rounded-md shadow-sm shadow-pink-500 p-2">
                  <BiTransfer />
                </div>

                <div>
                  <h4 className="text-sm">{tx.title} Transfered</h4>
                  <small className="flex flex-row justify-start items-center">
                    <span className="mr-1">Received by</span>
                    <a href="#" className="text-pink-500 mr-2">
                      {truncate(tx.owner, 4, 4, 11)}
                    </a>
                    <a href="#">
                      <MdOpenInNew />
                    </a>
                  </small>
                </div>

                <p className="text-sm font-medium">{tx.cost}ETH</p>
              </div>
            ))}
        </div>

        {collection.length > 0 && transactions.length > collection.length ? (
          <div className="text-center my-5">
            <button
              className="shadow-xl shadow-black text-white
            bg-[#e32970] hover:bg-[#bd255f]
            rounded-full cursor-pointer p-2"
            onClick={() => setEnd(end + count)}
            >
              Load More
            </button>
          </div>
        ) : null}
      </div>
    </div>
  )
}

export default Transactions


Footer Component


This component simply displays some beautiful links at the bottom of the page, it doesn’t do much when it comes to functionalities but complements the user interface. Its codes are written below.


import timelessLogo from '../assets/timeless.png'

const Footer = () => (
  <div className="w-full flex md:justify-center justify-between items-center flex-col p-4 gradient-bg-footer">
    <div className="w-full flex sm:flex-row flex-col justify-between items-center my-4">
      <div className="flex flex-[0.25] justify-center items-center">
        <img src={timelessLogo} alt="logo" className="w-32" />
      </div>

      <div className="flex flex-1 justify-evenly items-center flex-wrap sm:mt-0 mt-5 w-full">
        <p className="text-white text-base text-center mx-2 cursor-pointer">
          Market
        </p>
        <p className="text-white text-base text-center mx-2 cursor-pointer">
          Artist
        </p>
        <p className="text-white text-base text-center mx-2 cursor-pointer">
          Features
        </p>
        <p className="text-white text-base text-center mx-2 cursor-pointer">
          Community
        </p>
      </div>

      <div className="flex flex-[0.25] justify-center items-center">
      <p className="text-white text-right text-xs">&copy;2022 All rights reserved</p>
      </div>
    </div>
  </div>
)

export default Footer


Fantastic, that is it for the obvious components, let’s include the hidden components that are only invoked via a modal interface.


CreateNFT Component


This component is saddled with the duty of minting new NFTs by supplying an image, title, price, and description. Once the Mint Now button is clicked, the image is uploaded to IPFS (Inter Planetary File System) and an image URL is returned.


The returned image URL along with the NFT data supplied in the form is sent to our smart contract for minting, immediately after the user authorizes the transaction with their Metamask wallet.


On completion of the transaction, the NFT is then listed among the artworks, and interested buyers can then purchase them and even change their prices. See the code below for details.


import {
  useGlobalState,
  setGlobalState,
  setLoadingMsg,
  setAlert,
} from '../store'
import { mintNFT } from '../TimelessNFT'
import { useState } from 'react'
import { FaTimes } from 'react-icons/fa'
import { create } from 'ipfs-http-client'

const client = create('https://ipfs.infura.io:5001/api/v0')

const CreateNFT = () => {
  const [modal] = useGlobalState('modal')
  const [title, setTitle] = useState('')
  const [price, setPrice] = useState('')
  const [description, setDescription] = useState('')
  const [fileUrl, setFileUrl] = useState('')
  const [imgBase64, setImgBase64] = useState(null)

  const onChange = async (e) => {
    const reader = new FileReader()
    if (e.target.files[0]) reader.readAsDataURL(e.target.files[0])

    reader.onload = (readerEvent) => {
      const file = readerEvent.target.result
      setImgBase64(file)
      setFileUrl(e.target.files[0])
    }
  }

  const handleSubmit = async (e) => {
    e.preventDefault()

    if (!title || !price || !description) return

    setGlobalState('modal', 'scale-0')
    setGlobalState('loading', { show: true, msg: 'Uploading IPFS data...' })

    try {
      const created = await client.add(fileUrl)
      const metadataURI = `https://ipfs.infura.io/ipfs/${created.path}`
      const nft = { title, price, description, metadataURI }
      setLoadingMsg('Intializing transaction...')

      mintNFT(nft).then((res) => {
        if (res) {
          setFileUrl(metadataURI)
          resetForm()
          setAlert('Minting completed...', 'green')
          window.location.reload()
        }
      })
    } catch (error) {
      console.log('Error uploading file: ', error)
      setAlert('Minting failed...', 'red')
    }
  }

  const closeModal = () => {
    setGlobalState('modal', 'scale-0')
    resetForm()
  }

  const resetForm = () => {
    setFileUrl('')
    setImgBase64(null)
    setTitle('')
    setPrice('')
    setDescription('')
  }

  return (
    <div
      className={`fixed top-0 left-0 w-screen h-screen flex items-center
      justify-center bg-black bg-opacity-50 transform
      transition-transform duration-300 ${modal}`}
    >
      <div className="bg-[#151c25] shadow-xl shadow-[#e32970] rounded-xl w-11/12 md:w-2/5 h-7/12 p-6">
        <form className="flex flex-col">
          <div className="flex flex-row justify-between items-center">
            <p className="font-semibold text-gray-400">Add NFT</p>
            <button
              type="button"
              onClick={closeModal}
              className="border-0 bg-transparent focus:outline-none"
            >
              <FaTimes className="text-gray-400" />
            </button>
          </div>

          <div className="flex flex-row justify-center items-center rounded-xl mt-5">
            <div className="shrink-0 rounded-xl overflow-hidden h-20 w-20">
              <img
                alt="NFT"
                className="h-full w-full object-cover cursor-pointer"
                src={
                  imgBase64 ||
                  'https://images.unsplash.com/photo-1580489944761-15a19d654956?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1361&q=80'
                }
              />
            </div>
          </div>

          <div className="flex flex-row justify-between items-center bg-gray-800 rounded-xl mt-5">
            <label className="block">
              <span className="sr-only">Choose profile photo</span>
              <input
                type="file"
                accept="image/png, image/gif, image/jpeg, image/webp"
                className="block w-full text-sm text-slate-500
                file:mr-4 file:py-2 file:px-4
                file:rounded-full file:border-0
                file:text-sm file:font-semibold
                file:bg-[#19212c] file:text-gray-400
                hover:file:bg-[#1d2631]
                cursor-pointer focus:ring-0 focus:outline-none"
                onChange={onChange}
                required
              />
            </label>
          </div>

          <div className="flex flex-row justify-between items-center bg-gray-800 rounded-xl mt-5">
            <input
              className="block w-full text-sm
              text-slate-500 bg-transparent border-0
              focus:outline-none focus:ring-0"
              type="text"
              name="title"
              placeholder="Title"
              onChange={(e) => setTitle(e.target.value)}
              value={title}
              required
            />
          </div>

          <div className="flex flex-row justify-between items-center bg-gray-800 rounded-xl mt-5">
            <input
              className="block w-full text-sm
              text-slate-500 bg-transparent border-0
              focus:outline-none focus:ring-0"
              type="number"
              step={0.01}
              min={0.01}
              name="price"
              placeholder="Price (Eth)"
              onChange={(e) => setPrice(e.target.value)}
              value={price}
              required
            />
          </div>

          <div className="flex flex-row justify-between items-center bg-gray-800 rounded-xl mt-5">
            <textarea
              className="block w-full text-sm resize-none
              text-slate-500 bg-transparent border-0
              focus:outline-none focus:ring-0 h-20"
              type="text"
              name="description"
              placeholder="Description"
              onChange={(e) => setDescription(e.target.value)}
              value={description}
              required
            ></textarea>
          </div>

          <button
            type="submit"
            onClick={handleSubmit}
            className="flex flex-row justify-center items-center
            w-full text-white text-md bg-[#e32970]
            hover:bg-[#bd255f] py-2 px-5 rounded-full
            drop-shadow-xl border border-transparent
            hover:bg-transparent hover:text-[#e32970]
            hover:border hover:border-[#bd255f]
            focus:outline-none focus:ring mt-5"
          >
            Mint Now
          </button>
        </form>
      </div>
    </div>
  )
}

export default CreateNFT


ShowNFT Component

This component displays more information about a specific NFT, offering the owner a button to change the price, and the buyer a button to either purchase the NFT or chat with the seller. See the code below for more details.


import Chat from './Chat'
import Identicon from 'react-identicons'
import { FaTimes } from 'react-icons/fa'
import { buyNFT } from '../TimelessNFT'
import { useGlobalState, setGlobalState, truncate, setAlert } from '../store'
import { useState } from 'react'
import { getMessages } from '../CometChat'

const ShowNFT = () => {
  const [showModal] = useGlobalState('showModal')
  const [chatOpened] = useGlobalState('chatOpened')
  const [currentUser] = useGlobalState('currentUser')
  const [connectedAccount] = useGlobalState('connectedAccount')
  const [nft] = useGlobalState('nft')
  const [messages, setMessages] = useState([])

  const onChangePrice = () => {
    setGlobalState('nft', nft)
    setGlobalState('showModal', 'scale-0')
    setGlobalState('updateModal', 'scale-100')
  }

  const onChatSeller = () => {
    if (currentUser?.uid.toLowerCase() != connectedAccount.toLowerCase())
      return alert('Please login to receive chats from buyers!')

    getMessages(nft.owner).then((msgs) => {
      setMessages(
        msgs.filter((msg) => {
          return (
            !!!msg?.deletedAt &&
            !!!msg?.action &&
            (msg?.conversationId ==
              `${msg?.rawMessage.receiver}_user_${msg?.rawMessage.sender}` ||
              msg?.conversationId ==
                `${msg?.rawMessage.sender}_user_${msg?.rawMessage.receiver}`)
          )
        })
      )
      setGlobalState('nft', nft)
      setGlobalState('chatOpened', true)
    })
  }

  const handleNFTPurchase = () => {
    setGlobalState('showModal', 'scale-0')
    setGlobalState('loading', {
      show: true,
      msg: 'Initializing NFT transfer...',
    })

    try {
      buyNFT(nft).then((res) => {
        if (res) {
          setAlert('Transfer completed...', 'green')
          window.location.reload()
        }
      })
    } catch (error) {
      console.log('Error transfering NFT: ', error)
      setAlert('Purchase failed...', 'red')
    }
  }

  return (
    <div>
      {chatOpened ? (
        <Chat receiver={nft.owner} chats={messages} />
      ) : (
        <div
          className={`fixed top-0 left-0 w-screen h-screen flex items-center
          justify-center bg-black bg-opacity-50 transform
          transition-transform duration-300 ${showModal}`}
        >
          <div className="bg-[#151c25] shadow-xl shadow-[#e32970] rounded-xl w-11/12 md:w-2/5 h-7/12 p-6">
            <div className="flex flex-col">
              <div className="flex flex-row justify-between items-center">
                <p className="font-semibold text-gray-400">Buy NFT</p>
                <button
                  type="button"
                  onClick={() => setGlobalState('showModal', 'scale-0')}
                  className="border-0 bg-transparent focus:outline-none"
                >
                  <FaTimes className="text-gray-400" />
                </button>
              </div>

              <div className="flex flex-row justify-center items-center rounded-xl mt-5">
                <div className="shrink-0 rounded-xl overflow-hidden h-40 w-40">
                  <img
                    className="h-full w-full object-cover cursor-pointer"
                    src={nft?.metadataURI}
                    alt={nft?.title}
                  />
                </div>
              </div>

              <div className="flex flex-col justify-start rounded-xl mt-5">
                <h4 className="text-white font-semibold">{nft?.title}</h4>
                <p className="text-gray-400 text-xs my-1">{nft?.description}</p>

                <div className="flex justify-between items-center mt-3 text-white">
                  <div className="flex justify-start items-center">
                    <Identicon
                      string={nft?.owner.toLowerCase()}
                      size={50}
                      className="h-10 w-10 object-contain rounded-full mr-3"
                    />
                    <div className="flex flex-col justify-center items-start">
                      <small className="text-white font-bold">@owner</small>
                      <small className="text-pink-800 font-semibold">
                        {nft?.owner ? truncate(nft.owner, 4, 4, 11) : '...'}
                      </small>
                    </div>
                  </div>

                  <div className="flex flex-col">
                    <small className="text-xs">Current Price</small>
                    <p className="text-sm font-semibold">{nft?.cost} ETH</p>
                  </div>
                </div>
              </div>
              {connectedAccount != nft?.owner ? (
                <div className="flex flex-row justify-between items-center">
                  <button
                    className="flex flex-row justify-center items-center
                w-full text-white text-md bg-[#e32970]
                hover:bg-[#bd255f] py-2 px-5 rounded-full
                drop-shadow-xl border border-transparent
                hover:bg-transparent hover:text-[#e32970]
                hover:border hover:border-[#bd255f]
                focus:outline-none focus:ring mt-5"
                    onClick={handleNFTPurchase}
                  >
                    Purchase Now
                  </button>
                  <button
                    className="flex flex-row justify-center items-center
                w-full text-white text-md bg-transparent 
                py-2 px-5 rounded-full drop-shadow-xl border
                border-transparent hover:bg-transparent
                hover:text-[#e32970] focus:outline-none
                focus:ring mt-5"
                    onClick={onChatSeller}
                  >
                    Chat with Seller
                  </button>
                </div>
              ) : (
                <button
                  className="flex flex-row justify-center items-center
              w-full text-white text-md bg-[#e32970]
              hover:bg-[#bd255f] py-2 px-5 rounded-full
              drop-shadow-xl border border-transparent
              hover:bg-transparent hover:text-[#e32970]
              hover:border hover:border-[#bd255f]
              focus:outline-none focus:ring mt-5"
                  onClick={onChangePrice}
                >
                  Change Price
                </button>
              )}
            </div>
          </div>
        </div>
      )}
    </div>
  )
}

export default ShowNFT


UpdateNFT Component

This component is tasked with changing the price of the NFT. This action can only be performed by the owner of the NFT. While this option is available, it will take some gas fee to effect these changes. Once an NFT exchanges a hand with another buyer, the new owner might decide to increase the price, and that’s why this option was made available. See the code snippet below.


import {
  useGlobalState,
  setGlobalState,
  setLoadingMsg,
  setAlert,
} from '../store'
import { updateNFT } from '../TimelessNFT'
import { useState } from 'react'
import { FaTimes } from 'react-icons/fa'

const UpdateNFT = () => {
  const [modal] = useGlobalState('updateModal')
  const [nft] = useGlobalState('nft')
  const [price, setPrice] = useState('')
  

  const handleSubmit = async (e) => {
    e.preventDefault()
    if (!price || price <= 0) return

    setGlobalState('modal', 'scale-0')
    setGlobalState('loading', { show: true, msg: 'Initiating price update...' })

    try {
      setLoadingMsg('Price updating...')
      setGlobalState('updateModal', 'scale-0')

      updateNFT({...nft, cost: price}).then((res) => {
        if (res) {
          setAlert('Price updated...', 'green')
          window.location.reload()
        }
      })
    } catch (error) {
      console.log('Error updating file: ', error)
      setAlert('Update failed...', 'red')
    }
    
  }

  return (
    <div
      className={`fixed top-0 left-0 w-screen h-screen flex items-center
      justify-center bg-black bg-opacity-50 transform
      transition-transform duration-300 ${modal}`}
    >
      <div className="bg-[#151c25] shadow-xl shadow-[#e32970] rounded-xl w-11/12 md:w-2/5 h-7/12 p-6">
        <form className="flex flex-col">
          <div className="flex flex-row justify-between items-center">
            <p className="font-semibold text-gray-400">{nft?.title}</p>
            <button
              type="button"
              onClick={() => setGlobalState('updateModal', 'scale-0')}
              className="border-0 bg-transparent focus:outline-none"
            >
              <FaTimes className="text-gray-400" />
            </button>
          </div>

          <div className="flex flex-row justify-center items-center rounded-xl mt-5">
            <div className="shrink-0 rounded-xl overflow-hidden h-20 w-20">
              <img
                alt="NFT"
                className="h-full w-full object-cover cursor-pointer"
                src={nft?.metadataURI}
              />
            </div>
          </div>

          <div className="flex flex-row justify-between items-center bg-gray-800 rounded-xl mt-5">
            <input
              className="block w-full text-sm
              text-slate-500 bg-transparent border-0
              focus:outline-none focus:ring-0"
              type="number"
              step={0.01}
              min={0.01}
              name="price"
              placeholder="Price (Eth)"
              onChange={(e) => setPrice(e.target.value)}
              required
            />
          </div>

          <button
            type="submit"
            onClick={handleSubmit}
            className="flex flex-row justify-center items-center
            w-full text-white text-md bg-[#e32970]
            hover:bg-[#bd255f] py-2 px-5 rounded-full
            drop-shadow-xl border border-transparent
            hover:bg-transparent hover:text-[#e32970]
            hover:border hover:border-[#bd255f]
            focus:outline-none focus:ring mt-5"
          >
            Update Now
          </button>
        </form>
      </div>
    </div>
  )
}

export default UpdateNFT


ChatList Component

This component reveals the recent chats a user has made with a seller or buyer on the platform. The component also captures the last message that was sent in their conversation. A click on each conversation will lead to the chat interface. See the code below.


import Chat from './Chat'
import Moment from 'react-moment'
import { useState } from 'react'
import { FaTimes } from 'react-icons/fa'
import { setGlobalState, truncate, useGlobalState } from '../store'
import { getMessages } from '../CometChat'

const ChatList = ({ users }) => {
  const [messages, setMessages] = useState([])
  const [receiver, setReceiver] = useState('')
  const [recentChatOpened] = useGlobalState('recentChatOpened')
  const [connectedAccount] = useGlobalState('connectedAccount')

  const onEnterChat = (receiver) => {
    setReceiver(receiver)
    getMessages(receiver).then((msgs) => {
      setMessages(
        msgs.filter((msg) => {
          return (
            !!!msg?.deletedAt &&
            !!!msg?.action &&
            msg?.conversationId ==
              `${msg?.rawMessage.receiver}_user_${msg?.rawMessage.sender}`
          )
        })
      )
      setGlobalState('recentChatOpened', true)
    })
  }

  return (
    <div>
      {recentChatOpened ? (
        <Chat receiver={receiver} chats={messages} />
      ) : (
        <div
          className={`fixed top-0 left-0 w-screen h-screen flex items-center
        justify-center bg-black bg-opacity-50 transform
        transition-transform duration-300 scale-100`}
        >
          <div className="bg-[#151c25] shadow-xl shadow-[#e32970] rounded-xl w-11/12 md:w-2/5 h-7/12 p-6">
            <div className="flex flex-col text-gray-400">
              <div className="flex flex-row justify-between items-center">
                <p className="font-semibold text-gray-400">Conversations</p>
                <button
                  type="button"
                  onClick={() => setGlobalState('recentOpened', false)}
                  className="border-0 bg-transparent focus:outline-none"
                >
                  <FaTimes />
                </button>
              </div>

              <div className="h-[calc(100vh_-_20rem)] overflow-y-auto sm:pr-4 my-3">
                {users.map((user, i) => (
                  <button
                    key={i}
                    className="flex flex-row justify-between w-full
                    items-center bg-gray-800 hover:bg-gray-900 rounded-md 
                    px-4 py-3 my-1 cursor-pointer transform
                    transition-transform duration-300"
                    onClick={() => onEnterChat(user?.lastMessage.sender.uid)}
                  >
                    <div className="flex flex-col text-left">
                      <h4 className="text-sm text-[#e32970] font-semiBold">
                        @{truncate(user?.lastMessage.sender.uid, 4, 4, 11)}
                      </h4>
                      <p className="text-xs">
                        {user?.lastMessage.text}
                      </p>
                    </div>

                    <Moment
                      className="text-xs font-bold"
                      unix
                      date={user?.lastMessage.sentAt}
                      format="YYYY/MM/D hh:mm A"
                    />
                  </button>
                ))}
              </div>
            </div>
          </div>
        </div>
      )}
    </div>
  )
}

export default ChatList


Chat Component

This component is responsible for engaging two users in a one-on-one chat. The image above shows a conversation between a buyer and a seller on the platform, from two different browsers. See the code below for its implementation.


import Identicon from 'react-identicons'
import { useGlobalState, setGlobalState, truncate } from '../store'
import { sendMessage, CometChat } from '../CometChat'
import { useEffect, useState } from 'react'
import { FaTimes } from 'react-icons/fa'

const Chat = ({ receiver, chats }) => {
  const [connectedAccount] = useGlobalState('connectedAccount')
  const [message, setMessage] = useState('')
  const [messages, setMessages] = useState(chats)

  const handleSubmit = async (e) => {
    e.preventDefault()
    sendMessage(receiver, message).then((msg) => {
      setMessages((prevState) => [...prevState, msg])
      setMessage('')
      scrollToEnd()
    })
  }

  const listenForMessage = (listenerID) => {
    CometChat.addMessageListener(
      listenerID,
      new CometChat.MessageListener({
        onTextMessageReceived: (message) => {
          setMessages((prevState) => [...prevState, message])
          scrollToEnd()
        },
      })
    )
  }

  const onClose = () => {
    setGlobalState('chatOpened', false)
    setGlobalState('recentChatOpened', false)
  }

  const scrollToEnd = () => {
    const element = document.getElementById('messages-container')
    element.scrollTop = element.scrollHeight
  }

  useEffect(() => {
    listenForMessage(receiver)
  }, [receiver])

  return (
    <div
      className={`fixed top-0 left-0 w-screen h-screen flex items-center
      justify-center bg-black bg-opacity-50 transform
      transition-transform duration-300 scale-100`}
    >
      <div className="bg-[#151c25] shadow-xl shadow-[#e32970] rounded-xl w-5/6 h-5/6 p-6">
        <div className="flex flex-col text-gray-400">
          <div className="flex flex-row justify-between items-center">
            <div className="flex flex-row justify-center items-center">
              <div className="shrink-0 rounded-full overflow-hidden h-10 w-10 mr-3">
                <Identicon
                  string={receiver.toLowerCase()}
                  size={50}
                  className="h-full w-full object-cover cursor-pointer rounded-full"
                />
              </div>
              <p className="font-bold">@{receiver ? truncate(receiver, 4, 4, 11) : '...'}</p>
            </div>
            <button
              type="button"
              onClick={onClose}
              className="border-0 bg-transparent focus:outline-none"
            >
              <FaTimes />
            </button>
          </div>
          <div
            id="messages-container"
            className="h-[calc(100vh_-_20rem)] overflow-y-auto sm:pr-4 my-3"
          >
            {messages.map((msg, i) =>
              msg?.receiverId?.toLowerCase() ==
              connectedAccount.toLowerCase() ? (
                <div
                  key={i}
                  className="flex flex-row justify-start items-center mt-5"
                >
                  <div className="flex flex-col justify-start items-start">
                    <h4 className="text-[#e32970]">
                      @{receiver ? truncate(receiver, 4, 4, 11) : '...'}
                    </h4>
                    <p className="text-xs">{msg.text}</p>
                  </div>
                </div>
              ) : (
                <div
                  key={i}
                  className="flex flex-row justify-end items-center mt-5"
                >
                  <div className="flex flex-col justify-start items-end">
                    <h4 className="text-[#e32970]">@you</h4>
                    <p className="text-xs">{msg.text}</p>
                  </div>
                </div>
              )
            )}
          </div>
          <form
            onSubmit={handleSubmit}
            className="flex flex-row justify-between items-center bg-gray-800 rounded-xl mt-5"
          >
            <input
              className="block w-full text-sm resize-none
              text-slate-500 bg-transparent border-0
              focus:outline-none focus:ring-0 h-20"
              type="text"
              name="message"
              placeholder="Write message..."
              onChange={(e) => setMessage(e.target.value)}
              value={message}
              required
            />
          </form>
        </div>
      </div>
    </div>
  )
}

export default Chat


Nice, now that we’ve included those fantastic components, let's finish it up with these last two.


Loading Component

This component simply displays the current activity and status when a transaction is in process. See the code below.


import { useGlobalState } from '../store'

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

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

export default Loading


The App Component This file bundles up the above component discussed in this work. This is just how a ReactJs architecture works. See the codes below.


import Alert from './components/Alert'
import Artworks from './components/Artworks'
import CreateNFT from './components/CreateNFT'
import Footer from './components/Footer'
import Header from './components/Header'
import Hero from './components/Hero'
import Loading from './components/Loading'
import ShowNFT from './components/ShowNFT'
import Transactions from './components/Transactions'
import UpdateNFT from './components/UpdateNFT'
import { isUserLoggedIn } from './CometChat'
import { loadWeb3 } from './TimelessNFT'
import { useEffect } from 'react'

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

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

export default App


Fantastic, we’ve just completed the integration of the various components, let’s seal it up with the other parts of this project.

Other Essential Files

This application utilizes a state management store, a CometChat SDK, and a contract service file. Let’s take a look at them one after the other.


The Store This state management file uses the react-hooks-global-state npm package. It is simple, fast, and easier than Redux. All the global variables and functions used in this app were created in this store.


At the root of the project, go to the src directory and create a folder named store. Now, within this store folder, create a file called index.js and paste the codes below inside of it.


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

const { setGlobalState, useGlobalState, getGlobalState } = createGlobalState({
  modal: 'scale-0',
  updateModal: 'scale-0',
  mintModal: '',
  alert: { show: false, msg: '', color: '' },
  loading: { show: false, msg: '' },
  showModal: 'scale-0',
  chatOpened: false,
  recentChatOpened: false,
  recentOpened: false,
  chatList: 'scale-0',
  connectedAccount: '',
  currentUser: null,
  nft: null,
  nfts: [],
  transactions: [],
  contract: null,
})

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

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

const truncate = (text, startChars, endChars, maxLength) => {
  if (text.length > maxLength) {
    var start = text.substring(0, startChars)
    var end = text.substring(text.length - endChars, text.length)
    while (start.length + end.length < maxLength) {
      start = start + '.'
    }
    return start + end
  }
  return text
}

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


The CometChat Service This file contains all the essential functions to communicate with the CometChat SDK. See the codes below.


import { CometChat } from '@cometchat-pro/chat'
import { setGlobalState } from './store'

const CONSTANTS = {
  APP_ID: process.env.REACT_APP_COMET_CHAT_APP_ID,
  REGION: process.env.REACT_APP_COMET_CHAT_REGION,
  Auth_Key: process.env.REACT_APP_COMET_CHAT_AUTH_KEY,
}

const initCometChat = async () => {
  const appID = CONSTANTS.APP_ID
  const region = CONSTANTS.REGION

  const appSetting = new CometChat.AppSettingsBuilder()
    .subscribePresenceForAllUsers()
    .setRegion(region)
    .build()

  await CometChat.init(appID, appSetting)
    .then(() => console.log('Initialization completed successfully'))
    .catch((error) => error)
}

const loginWithCometChat = async (UID) => {
  const authKey = CONSTANTS.Auth_Key
  await CometChat.login(UID, authKey)
    .then((user) => setGlobalState('currentUser', user))
    .catch((error) => {
      alert(error.message)
      console.log(error)
    })
  return true
}

const signUpWithCometChat = async (UID, name) => {
  let authKey = CONSTANTS.Auth_Key
  const user = new CometChat.User(UID)
  user.setName(name)

  await CometChat.createUser(user, authKey)
    .then((user) => {
      alert('Signed up successfully, click login now!')
      console.log('Logged In: ', user)
    })
    .catch((error) => {
      alert(error.message)
      console.log(error)
    })
  return true
}

const logOutWithCometChat = async () => {
  return await CometChat.logout()
    .then(() => console.log('Logged Out Successfully'))
    .catch((error) => error)
}

const isUserLoggedIn = async () => {
  await CometChat.getLoggedinUser()
    .then((user) => setGlobalState('currentUser', user))
    .catch((error) => console.log('error:', error))
}

const getMessages = async (UID) => {
  const limit = 30
  const messagesRequest = new CometChat.MessagesRequestBuilder()
    .setUID(UID)
    .setLimit(limit)
    .build()

  return await messagesRequest
    .fetchPrevious()
    .then((messages) => messages)
    .catch((error) => error)
}

const sendMessage = async (receiverID, messageText) => {
  const receiverType = CometChat.RECEIVER_TYPE.USER
  const textMessage = new CometChat.TextMessage(
    receiverID,
    messageText,
    receiverType
  )

  return await CometChat.sendMessage(textMessage)
    .then((message) => message)
    .catch((error) => error)
}

const getConversations = async () => {
  const limit = 30
  const conversationsRequest = new CometChat.ConversationsRequestBuilder()
    .setLimit(limit)
    .build()

  return await conversationsRequest
    .fetchNext()
    .then((conversationList) => conversationList)
}

export {
  initCometChat,
  loginWithCometChat,
  signUpWithCometChat,
  logOutWithCometChat,
  getMessages,
  sendMessage,
  getConversations,
  isUserLoggedIn,
  CometChat,
}


The Contract Service File This file contains all the functions and procedures responsible for interacting with the smart contract on the blockchain using the Web3 library. See the codes below.


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

const { ethereum } = window

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

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

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

    window.web3 = new Web3(ethereum)

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

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

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

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

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

const mintNFT = async ({ title, description, metadataURI, price }) => {
  try {
    price = window.web3.utils.toWei(price.toString(), 'ether')
    const contract = getGlobalState('contract')
    const account = getGlobalState('connectedAccount')
    const mintPrice = window.web3.utils.toWei('0.01', 'ether')

    await contract.methods
      .payToMint(title, description, metadataURI, price)
      .send({ from: account, value: mintPrice })

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

const buyNFT = async ({ id, cost }) => {
  try {
    cost = window.web3.utils.toWei(cost.toString(), 'ether')
    const contract = getGlobalState('contract')
    const buyer = getGlobalState('connectedAccount')

    await contract.methods.payToBuy(Number(id)).send({ from: buyer, value: cost })

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

const updateNFT = async ({ id, cost }) => {
  try {
    cost = window.web3.utils.toWei(cost.toString(), 'ether')
    const contract = getGlobalState('contract')
    const buyer = getGlobalState('connectedAccount')

    await contract.methods.changePrice(Number(id), cost).send({ from: buyer })

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

export { loadWeb3, connectWallet, mintNFT, buyNFT, updateNFT }


Project Assets Download this logo and include it inside the assets folder in your root directory. And with that, you’ve successfully included all that is needed to run this application.

Starting up the server

To proceed with this step, migrate the smart contract to the Web so you can interact with it. Run the following code on your terminal.


truffle migrate --network rinkeby


The above code will ship your smart contract to the server using the Infura RPC.


You can also set up a local blockchain using the ganache-cli server we set up at the beginning of this tutorial. Simply run the code below to ship it to your local blockchain server if you prefer that way.


Open one terminal run **ganache-cli -d** and on a different terminal run **truffle migrate** or **truffle deploy**.


Note, if you are using ganache-cli as your EVM, you must also add the localhost server to Metamask, and import the private keys generated by ganache. See Starting Up the Development Environment for guidance.


If you need my help resolving issues on your project, consult me on this page.

Now, run yarn start to boot up your react application. And there you have it with this build on the NFT marketplace.


Watch my FREE web3 tutorials on Youtube now.

Conclusion

We’ve come to the finish line of this NFT build, I know you’ve gotten a ton of value building along with me.


Whatever level you are, if you want to grow faster in your web3 development skills, get into my private class.


Till next time, keep crushing it!


About the Author

Gospel Darlington is a full-stack blockchain developer with 6+ years of experience in the software development industry.


By combining Software Development, writing, and teaching, he demonstrates how to build decentralized applications on EVM-compatible blockchain networks.


His stacks include JavaScript, React, Vue, Angular, Node, React Native, NextJs, Solidity, and more.


For more information about him, kindly visit and follow his page on Twitter, GitHub, LinkedIn, or on his website.