DEV Community

Cover image for How to Build an NFT Auction Site with React, Solidity, and CometChat
Gospel Darlington
Gospel Darlington

Posted on • Edited on

4

How to Build an NFT Auction Site with React, Solidity, and CometChat

What you will be building, see the git repo here.

Auctioning NFT Item on Marketplace

Introduction

Welcome to this tutorial on building an NFT auction site using React, Solidity, and CometChat. In this guide, we will walk you through the steps of creating a decentralized marketplace for buying and selling non-fungible tokens. We will use React for the front end, Solidity for the smart contract development, and CometChat for real-time messaging and notifications. By the end of this tutorial, you will have a fully functional NFT auction platform ready to go live on the Ethereum blockchain.

Prerequisites

To follow this tutorial, you will need to have the items below installed on your local machine. NodeJs is non-negotiable, the rest can be installed following this guide, so make sure you have it installed and running.

  • NodeJs
  • React
  • Solidity
  • Tailwind
  • CometChat SDK
  • Hardhat
  • EthersJs
  • Metamask
  • Yarn

Installing Dependencies

Clone the starter kit using the command below to your projects folder:

git clone https://github.com/Daltonic/tailwind_ethers_starter_kit <PROJECT_NAME>
cd <PROJECT_NAME>
Enter fullscreen mode Exit fullscreen mode

Next, open the project in VS Code or on your preferred code editor. Locate the package.json file and update it with the codes below.

{
"name": "Auction",
"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.10",
"@nomiclabs/hardhat-ethers": "^2.1.0",
"@nomiclabs/hardhat-waffle": "^2.0.3",
"axios": "^1.2.1",
"ethereum-waffle": "^3.4.4",
"ethers": "^5.6.9",
"hardhat": "^2.10.1",
"ipfs-http-client": "55.0.1-rc.2",
"moment": "^2.29.4",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-hooks-global-state": "^1.0.2",
"react-icons": "^4.7.1",
"react-identicons": "^1.2.5",
"react-moment": "^1.1.2",
"react-router-dom": "6",
"react-scripts": "5.0.0",
"react-toastify": "^9.1.1",
"web-vitals": "^2.1.4"
},
"devDependencies": {
"@faker-js/faker": "^7.6.0",
"@openzeppelin/contracts": "^4.5.0",
"@tailwindcss/forms": "0.4.0",
"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",
"express": "^4.18.2",
"express-fileupload": "^1.4.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.31.2",
"stream-browserify": "^3.0.0",
"stream-http": "^3.2.0",
"tailwindcss": "3.0.18",
"url": "^0.11.0",
"uuid": "^9.0.0"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}
view raw package.json hosted with ❤ by GitHub

To install all the required dependencies as indicated in the package.json file, run the command **yarn install** in the terminal.

Configuring CometChat SDK

CometChat SDK

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

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

Register a new CometChat account if you do not have one

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

Log in to the CometChat Dashboard with your created account

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

Create a new CometChat app - Step 1

Create a new CometChat app - Step 2

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

Select your created app

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

Copy the the APP_ID, REGION, and AUTH_KEY

Replace the REACT_COMET_CHAT placeholder keys with their appropriate values.

REACT_APP_COMET_CHAT_REGION=**
REACT_APP_COMET_CHAT_APP_ID=**************
REACT_APP_COMET_CHAT_AUTH_KEY=******************************
Enter fullscreen mode Exit fullscreen mode

The .env file should be created at the root of your project.

Configuring the Hardhat script

Open the hardhat.config.js file at the root of this project and replace the contents with the following settings.

require("@nomiclabs/hardhat-waffle");
require('dotenv').config()
module.exports = {
defaultNetwork: "localhost",
networks: {
hardhat: {
},
localhost: {
url: "http://127.0.0.1:8545"
},
},
solidity: {
version: '0.8.11',
settings: {
optimizer: {
enabled: true,
runs: 200
}
}
},
paths: {
sources: "./src/contracts",
artifacts: "./src/abis"
},
mocha: {
timeout: 40000
}
}

The above script instructs hardhat on these three important rules.

  • Networks: This block contains the configurations for your choice of networks. On deployment, hardhat will require you to specify a network for shipping your smart contracts.

  • Solidity: This describes the version of the compiler to be used by hardhat for compiling your smart contract codes into bytecodes and abi.

  • Paths: This simply informs hardhat of the location of your smart contracts and also a location to dump the output of the compiler which is the ABI.

Check out this video on how to build a decentralized autonomous organization.

https://www.youtube.com/watch?v=Gm442Ihv1GU&

You can also subscribe to the channel for more videos like these.

Developing The Smart Contract

Let's create a smart contract for this project by creating a new folder called contracts in the src directory of the project.

Inside the contracts folder, create a file called 'DappAuction.sol' which will contain the code that defines the behavior of the smart contract. Copy and paste the following code into the 'DappAuction.sol' file. The complete code is shown below."

//SPDX-License-Identifier: MIT
pragma solidity >=0.7.0 <0.9.0;
import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract Auction is ERC721URIStorage, ReentrancyGuard {
using Counters for Counters.Counter;
Counters.Counter private totalItems;
address companyAcc;
uint listingPrice = 0.02 ether;
uint royalityFee;
mapping(uint => AuctionStruct) auctionedItem;
mapping(uint => bool) auctionedItemExist;
mapping(string => uint) existingURIs;
mapping(uint => BidderStruct[]) biddersOf;
constructor(uint _royaltyFee) ERC721("Daltonic Tokens", "DAT") {
companyAcc = msg.sender;
royalityFee = _royaltyFee;
}
struct BidderStruct {
address bidder;
uint price;
uint timestamp;
bool refunded;
bool won;
}
struct AuctionStruct {
string name;
string description;
string image;
uint tokenId;
address seller;
address owner;
address winner;
uint price;
bool sold;
bool live;
bool biddable;
uint bids;
uint duration;
}
event AuctionItemCreated(
uint indexed tokenId,
address seller,
address owner,
uint price,
bool sold
);
function getListingPrice() public view returns (uint) {
return listingPrice;
}
function setListingPrice(uint _price) public {
require(msg.sender == companyAcc, "Unauthorized entity");
listingPrice = _price;
}
function changePrice(uint tokenId, uint price) public {
require(
auctionedItem[tokenId].owner == msg.sender,
"Unauthorized entity"
);
require(
getTimestamp(0, 0, 0, 0) > auctionedItem[tokenId].duration,
"Auction still Live"
);
require(price > 0 ether, "Price must be greater than zero");
auctionedItem[tokenId].price = price;
}
function mintToken(string memory tokenURI) internal returns (bool) {
totalItems.increment();
uint tokenId = totalItems.current();
_mint(msg.sender, tokenId);
_setTokenURI(tokenId, tokenURI);
return true;
}
function createAuction(
string memory name,
string memory description,
string memory image,
string memory tokenURI,
uint price
) public payable nonReentrant {
require(price > 0 ether, "Sales price must be greater than 0 ethers.");
require(
msg.value >= listingPrice,
"Price must be up to the listing price."
);
require(mintToken(tokenURI), "Could not mint token");
uint tokenId = totalItems.current();
AuctionStruct memory item;
item.tokenId = tokenId;
item.name = name;
item.description = description;
item.image = image;
item.price = price;
item.duration = getTimestamp(0, 0, 0, 0);
item.seller = msg.sender;
item.owner = msg.sender;
auctionedItem[tokenId] = item;
auctionedItemExist[tokenId] = true;
payTo(companyAcc, listingPrice);
emit AuctionItemCreated(tokenId, msg.sender, address(0), price, false);
}
function offerAuction(
uint tokenId,
bool biddable,
uint sec,
uint min,
uint hour,
uint day
) public {
require(
auctionedItem[tokenId].owner == msg.sender,
"Unauthorized entity"
);
require(
auctionedItem[tokenId].bids == 0,
"Winner should claim prize first"
);
if (!auctionedItem[tokenId].live) {
setApprovalForAll(address(this), true);
IERC721(address(this)).transferFrom(
msg.sender,
address(this),
tokenId
);
}
auctionedItem[tokenId].bids = 0;
auctionedItem[tokenId].live = true;
auctionedItem[tokenId].sold = false;
auctionedItem[tokenId].biddable = biddable;
auctionedItem[tokenId].duration = getTimestamp(sec, min, hour, day);
}
function placeBid(uint tokenId) public payable {
require(
msg.value >= auctionedItem[tokenId].price,
"Insufficient Amount"
);
require(
auctionedItem[tokenId].duration > getTimestamp(0, 0, 0, 0),
"Auction not available"
);
require(auctionedItem[tokenId].biddable, "Auction only for bidding");
BidderStruct memory bidder;
bidder.bidder = msg.sender;
bidder.price = msg.value;
bidder.timestamp = getTimestamp(0, 0, 0, 0);
biddersOf[tokenId].push(bidder);
auctionedItem[tokenId].bids++;
auctionedItem[tokenId].price = msg.value;
auctionedItem[tokenId].winner = msg.sender;
}
function claimPrize(uint tokenId, uint bid) public {
require(
getTimestamp(0, 0, 0, 0) > auctionedItem[tokenId].duration,
"Auction still Live"
);
require(
auctionedItem[tokenId].winner == msg.sender,
"You are not the winner"
);
biddersOf[tokenId][bid].won = true;
uint price = auctionedItem[tokenId].price;
address seller = auctionedItem[tokenId].seller;
auctionedItem[tokenId].winner = address(0);
auctionedItem[tokenId].live = false;
auctionedItem[tokenId].sold = true;
auctionedItem[tokenId].bids = 0;
auctionedItem[tokenId].duration = getTimestamp(0, 0, 0, 0);
uint royality = (price * royalityFee) / 100;
payTo(auctionedItem[tokenId].owner, (price - royality));
payTo(seller, royality);
IERC721(address(this)).transferFrom(address(this), msg.sender, tokenId);
auctionedItem[tokenId].owner = msg.sender;
performRefund(tokenId);
}
function performRefund(uint tokenId) internal {
for (uint i = 0; i < biddersOf[tokenId].length; i++) {
if (biddersOf[tokenId][i].bidder != msg.sender) {
biddersOf[tokenId][i].refunded = true;
payTo(
biddersOf[tokenId][i].bidder,
biddersOf[tokenId][i].price
);
} else {
biddersOf[tokenId][i].won = true;
}
biddersOf[tokenId][i].timestamp = getTimestamp(0, 0, 0, 0);
}
delete biddersOf[tokenId];
}
function buyAuctionedItem(uint tokenId) public payable nonReentrant {
require(
msg.value >= auctionedItem[tokenId].price,
"Insufficient Amount"
);
require(
auctionedItem[tokenId].duration > getTimestamp(0, 0, 0, 0),
"Auction not available"
);
require(!auctionedItem[tokenId].biddable, "Auction only for purchase");
address seller = auctionedItem[tokenId].seller;
auctionedItem[tokenId].live = false;
auctionedItem[tokenId].sold = true;
auctionedItem[tokenId].bids = 0;
auctionedItem[tokenId].duration = getTimestamp(0, 0, 0, 0);
uint royality = (msg.value * royalityFee) / 100;
payTo(auctionedItem[tokenId].owner, (msg.value - royality));
payTo(seller, royality);
IERC721(address(this)).transferFrom(
address(this),
msg.sender,
auctionedItem[tokenId].tokenId
);
auctionedItem[tokenId].owner = msg.sender;
}
function getAuction(uint id) public view returns (AuctionStruct memory) {
require(auctionedItemExist[id], "Auctioned Item not found");
return auctionedItem[id];
}
function getAllAuctions()
public
view
returns (AuctionStruct[] memory Auctions)
{
uint totalItemsCount = totalItems.current();
Auctions = new AuctionStruct[](totalItemsCount);
for (uint i = 0; i < totalItemsCount; i++) {
Auctions[i] = auctionedItem[i + 1];
}
}
function getUnsoldAuction()
public
view
returns (AuctionStruct[] memory Auctions)
{
uint totalItemsCount = totalItems.current();
uint totalSpace;
for (uint i = 0; i < totalItemsCount; i++) {
if (!auctionedItem[i + 1].sold) {
totalSpace++;
}
}
Auctions = new AuctionStruct[](totalSpace);
uint index;
for (uint i = 0; i < totalItemsCount; i++) {
if (!auctionedItem[i + 1].sold) {
Auctions[index] = auctionedItem[i + 1];
index++;
}
}
}
function getMyAuctions()
public
view
returns (AuctionStruct[] memory Auctions)
{
uint totalItemsCount = totalItems.current();
uint totalSpace;
for (uint i = 0; i < totalItemsCount; i++) {
if (auctionedItem[i + 1].owner == msg.sender) {
totalSpace++;
}
}
Auctions = new AuctionStruct[](totalSpace);
uint index;
for (uint i = 0; i < totalItemsCount; i++) {
if (auctionedItem[i + 1].owner == msg.sender) {
Auctions[index] = auctionedItem[i + 1];
index++;
}
}
}
function getSoldAuction()
public
view
returns (AuctionStruct[] memory Auctions)
{
uint totalItemsCount = totalItems.current();
uint totalSpace;
for (uint i = 0; i < totalItemsCount; i++) {
if (auctionedItem[i + 1].sold) {
totalSpace++;
}
}
Auctions = new AuctionStruct[](totalSpace);
uint index;
for (uint i = 0; i < totalItemsCount; i++) {
if (auctionedItem[i + 1].sold) {
Auctions[index] = auctionedItem[i + 1];
index++;
}
}
}
function getLiveAuctions()
public
view
returns (AuctionStruct[] memory Auctions)
{
uint totalItemsCount = totalItems.current();
uint totalSpace;
for (uint i = 0; i < totalItemsCount; i++) {
if (auctionedItem[i + 1].duration > getTimestamp(0, 0, 0, 0)) {
totalSpace++;
}
}
Auctions = new AuctionStruct[](totalSpace);
uint index;
for (uint i = 0; i < totalItemsCount; i++) {
if (auctionedItem[i + 1].duration > getTimestamp(0, 0, 0, 0)) {
Auctions[index] = auctionedItem[i + 1];
index++;
}
}
}
function getBidders(uint tokenId)
public
view
returns (BidderStruct[] memory)
{
return biddersOf[tokenId];
}
function getTimestamp(
uint sec,
uint min,
uint hour,
uint day
) internal view returns (uint) {
return
block.timestamp +
(1 seconds * sec) +
(1 minutes * min) +
(1 hours * hour) +
(1 days * day);
}
function payTo(address to, uint amount) internal {
(bool success, ) = payable(to).call{value: amount}("");
require(success);
}
}
view raw DappAuction.sol hosted with ❤ by GitHub

Let's go over some of the specifics of what's happening in the smart contract above. The following items are available:

Contract Imports
The following are the smart contracts imported from the openzeppelin library:

  • Counters: For keeping track of all NFTs on the platform.
  • ERC721: This is a standard for non-fungible tokens on the Ethereum blockchain. It defines a set of functions and events that a smart contract implementing the ERC721 standard should have.
  • ERC721URIStorage: This is a smart contract that stores the URI (Uniform Resource Identifier) of an ERC721 token.
  • ReentrancyGuard: This import keeps our smart contract safe against reentrancy attacks.

State variables

  • Totalitems: This variable bears records of the number of NFTs available in our smart contract.
  • CompanyAcc: This contains a record of the deployer wallet address.
  • ListingPrice: This contains the price for creating and listing an NFT on the platform.
  • RoyalityFee: This is the percentage of royalty that the seller of an NFT gets on every sale.

Mappings

  • AuctionedItem: This holds all the NFT data minted on our platform.
  • AuctionedItemExist: Used to validate the existence of an NFT.
  • ExistingURIs: Holds minted metadata URIs.
  • BiddersOf: Bears record of bidders for a particular auction.

Structs and Events

  • BidderStruct: Describes the information about a particular bidder.
  • AuctionStruct: Describes the information about a particular NFT item.
  • AuctionItemCreated: An event that logs information about the just-created NFT.

Functions

  • Constructor(): This initializes the smart contract with the company’s account, stipulated royalty fee, and token name and symbol.
  • GetListingPrice(): Returns the price set for creating an NFT on the platform.
  • SetListingPrice(): Used for updating the minting price for creating an NFT.
  • ChangePrice(): Used for modifying the cost for a specific NFT.
  • MintToken(): Used for Creating a new token.
  • CreateAuction(): Used for Creating a new auction using a minted token Id.
  • OfferAuction(): Used for placing an NFT item on the market.
  • PlaceBid(): Used for bidding in an auction.
  • ClaimPrize(): Used to transfer an NFT to the highest bidders.
  • PerformRefund(): Used to refund bidders who didn’t emerge as winners for each auction.
  • BuyAuctionedItem(): Used to purchase NFTs sold outrightly.
  • GetAuction(): Returns an auction by token Id.
  • GetAllAuctions(): Returns all available auctions from the contract.
  • GetUnsoldAuction() Returns all unsold auctions.
  • GetSoldAuction(): Returns all sold auctions.
  • GetMyAuctions(): Returns all auctions belonging to the function caller.
  • GetLiveAuctions(): Returns all auctions listed on the market.
  • GetBidders(): Returns bidders of a specific auction by specifying the token Id.
  • GetTimestamp(): Returns a timestamp for a specific date.
  • PayTo(): Sends ethers to a specific account.

Configuring the Deployment Script

Navigate to the scripts folder and then to your deploy.js file and paste the code below into it. If you can't find a script folder, make one, create a deploy.js file, and paste the following code into it.

const { ethers } = require('hardhat')
const fs = require('fs')
async function main() {
const royaltyFee = 5
const Contract = await ethers.getContractFactory('Auction')
const contract = await Contract.deploy(royaltyFee)
await contract.deployed()
const address = JSON.stringify({ address: contract.address }, null, 4)
fs.writeFile('./src/abis/contractAddress.json', address, 'utf8', (err) => {
if (err) {
console.error(err)
return
}
console.log('Deployed contract address', contract.address)
})
}
main().catch((error) => {
console.error(error)
process.exitCode = 1
})
view raw deploy.js hosted with ❤ by GitHub

When run as a Hardhat command, the above script will deploy the Auction.sol smart contract to your local blockchain network.

Following the above instructions, open a terminal pointing to this project and run the commands listed below separately on two terminals. You can do this directly from your editor in VS Code.
Look at the command below.

yarn hardhat node # Terminal 1
yarn hardhat run scripts/deploy.js # Terminal 2
Enter fullscreen mode Exit fullscreen mode

If the preceding commands were successfully executed, you should see the following activity on your terminal. Please see the image below.

Step One

Step Two

Configuring Infuria App

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

Login to your infuria account

STEP 2: From the dashboard create a new project.

Create a new project step 1

Create a new project step 2

STEP 3: Copy the project Id and your API secret to your .env file in the format below and save.

Copy the project Id and your API secret

Env File

INFURIA_PID=***************************
INFURIA_API=**********************

REACT_APP_COMET_CHAT_REGION=**
REACT_APP_COMET_CHAT_APP_ID=**************
REACT_APP_COMET_CHAT_AUTH_KEY=******************************
Enter fullscreen mode Exit fullscreen mode

Developing an Image Processing API

We need a way to generate metadata out of an Image we intend to make an NFT. The problem is that JavaScript on the browser cannot get us the result we intend. It will take a NodeJs script to help us process the images, generate metadata, deploy to IPFS, and return the token URI as an API response. No need for much talking, let me show you how to implement this API.

First, you will need the following libraries, which are already installed on this project courtesy of the yarn install command you executed previously.

  • Express(): Enables server creation and resource sharing.
  • Express-Fileupload(): Enables file upload such as uploading an image.
  • Cors(): Enables cross-origin request sharing.
  • Fs(): Enables access to the file system of our local machine.
  • Dotenv(): Enable the management of environment variables.
  • Sharp(): Enables the processing of images to different dimensions and extensions.
  • Faker(): Enables the generation of random and fake data.
  • IpfsClient(): Enables the upload of files to the IPFS.

Let us now write some essential script functions that will assist us in converting images, as well as other information such as their names, descriptions, prices, Ids, and so on, to their metadata equivalent.

Create a folder called "api" at the root of your project, then create a new file called metadata.js within it and paste the code below inside.

Metadata.js File

const sharp = require('sharp')
const { faker } = require('@faker-js/faker')
const ipfsClient = require('ipfs-http-client')
const auth =
'Basic ' +
Buffer.from(process.env.INFURIA_PID + ':' + process.env.INFURIA_API).toString(
'base64',
)
const client = ipfsClient.create({
host: 'ipfs.infura.io',
port: 5001,
protocol: 'https',
headers: {
authorization: auth,
},
})
const attributes = {
weapon: [
'Stick',
'Knife',
'Blade',
'Club',
'Ax',
'Sword',
'Spear',
'Halberd',
],
environment: [
'Space',
'Sky',
'Deserts',
'Forests',
'Grasslands',
'Mountains',
'Oceans',
'Rainforests',
],
rarity: Array.from(Array(6).keys()),
}
const toMetadata = ({ id, name, description, price, image }) => ({
id,
name,
description,
price,
image,
demand: faker.random.numeric({ min: 10, max: 100 }),
attributes: [
{
trait_type: 'Environment',
value: attributes.environment.sort(() => 0.5 - Math.random())[0],
},
{
trait_type: 'Weapon',
value: attributes.weapon.sort(() => 0.5 - Math.random())[0],
},
{
trait_type: 'Rarity',
value: attributes.rarity.sort(() => 0.5 - Math.random())[0],
},
{
display_type: 'date',
trait_type: 'Created',
value: Date.now(),
},
{
display_type: 'number',
trait_type: 'generation',
value: 1,
},
],
})
const toWebp = async (image) => await sharp(image).resize(500).webp().toBuffer()
const uploadToIPFS = async (data) => {
const created = await client.add(data)
return `https://ipfs.io/ipfs/${created.path}`
}
exports.toWebp = toWebp
exports.toMetadata = toMetadata
exports.uploadToIPFS = uploadToIPFS
view raw metadata.js hosted with ❤ by GitHub

Now let’s utilize these functions in the main NodeJs file below.

App.js File
Create another script called app.js within this API folder and paste the codes below; this is where the API's controlling logic will reside.

require('dotenv').config()
const cors = require('cors')
const fs = require('fs').promises
const express = require('express')
const fileupload = require('express-fileupload')
const { toWebp, toMetadata, uploadToIPFS } = require('./metadata')
const app = express()
app.use(cors())
app.use(fileupload())
app.use(express.json())
app.use(express.static('public'))
app.use(express.urlencoded({ extended: true }))
app.post('/process', async (req, res) => {
try {
const name = req.body.name
const description = req.body.description
const price = req.body.price
const image = req.files.image
if (!name || !description || !price || !image) {
return res
.status(400)
.send('name, description, and price must not be empty')
}
let params
await toWebp(image.data).then(async (data) => {
const imageURL = await uploadToIPFS(data)
params = {
id: Date.now(),
name,
description,
price,
image: imageURL,
}
})
fs.writeFile('token.json', JSON.stringify(toMetadata(params)))
.then(() => {
fs.readFile('token.json')
.then(async (data) => {
const metadataURI = await uploadToIPFS(data)
console.log({ ...toMetadata(params), metadataURI })
return res.status(201).json({ ...toMetadata(params), metadataURI })
})
.catch((error) => console.log(error))
})
.catch((error) => console.log(error))
} catch (error) {
console.log(error)
return res.status(400).json({ error })
}
})
app.listen(9000, () => {
console.log('Listen on the port 9000...')
})
view raw app.js hosted with ❤ by GitHub

The IPFS library uses the Infuria Gateway for uploading files to the IPFS which we have already set up in the .env file.

Now, run node api/app.js on the terminal to start up the API service as can be seen in the image below.

API receiving Image and Metadata Info from the Frontend

Importing Private Keys to Metamask

To use Metamask with your Hardhat local network which is represented as Localhost:8545, use the following steps to set it up.

Run yarn hardhat node on your terminal to spin up your local blockchain server. You should see a similar image to the one below on the terminal.

Hardhat Node Started

Copy the private key of the account at zero(0) and import it on your Metamask, see the image below.

Step 1: Click on the Import account Option

Step 2: Enter the private key from the Hardhat Server

Step 3: Result

Now, you can repeat the above steps and import up to three or four accounts depending on your need.

All of the processes needed to develop a production-ready smart contract are already packaged inside this book, in an easy-to-understand manner.

Capturing Smart Contract Development

Grab a copy of my book titled, “capturing smart contract development” to become an in-demand smart contract developer.

Developing the Frontend

We will now use React to build the front end of our project, using the smart contract and related information that has been placed on the network and generated as artifacts (including the bytecodes and ABI). We will do this by following a step-by-step process.

Components

In the src directory, create a new folder called **components** to house all of the React components below.

Header Component

Header Component

Now, create a component in the components folder called Header.jsx and paste the following codes below. The designs of all these components were achieved using the Tailwind CSS framework.

import { Link } from 'react-router-dom'
import { connectWallet } from '../services/blockchain'
import { truncate, useGlobalState } from '../store'
const Header = () => {
const [connectedAccount] = useGlobalState('connectedAccount')
return (
<nav className="w-4/5 flex flex-row md:justify-center justify-between items-center py-4 mx-auto">
<div className="md:flex-[0.5] flex-initial justify-center items-center">
<Link to="/" className="text-white">
<span className="px-2 py-1 font-bold text-3xl italic">Dapp</span>
<span className="py-1 font-semibold italic">Auction-NFT</span>
</Link>
</div>
<ul
className="md:flex-[0.5] text-white md:flex
hidden list-none flex-row justify-between
items-center flex-initial"
>
<Link to="/" className="mx-4 cursor-pointer">Market</Link>
<Link to="/collections" className="mx-4 cursor-pointer">Collection</Link>
<Link className="mx-4 cursor-pointer">Artists</Link>
<Link className="mx-4 cursor-pointer">Community</Link>
</ul>
{connectedAccount ? (
<button
className="shadow-xl shadow-black text-white
bg-green-500 hover:bg-green-700 md:text-xs p-2
rounded-full cursor-pointer text-xs sm:text-base"
>
{truncate(connectedAccount, 4, 4, 11)}
</button>
) : (
<button
className="shadow-xl shadow-black text-white
bg-green-500 hover:bg-green-700 md:text-xs p-2
rounded-full cursor-pointer text-xs sm:text-base"
onClick={connectWallet}
>
Connect Wallet
</button>
)}
</nav>
)
}
export default Header
view raw Header.jsx hosted with ❤ by GitHub

Hero Component

Hero Component

Next, create another component in the components folder called Hero.jsx and paste the following codes below.

import { toast } from 'react-toastify'
import { BsArrowRightShort } from 'react-icons/bs'
import picture0 from '../assets/images/picture0.png'
import { setGlobalState, useGlobalState } from '../store'
import { loginWithCometChat, signUpWithCometChat } from '../services/chat'
const Hero = () => {
return (
<div className="flex flex-col items-start md:flex-row w-4/5 mx-auto mt-11">
<Banner />
<Bidder />
</div>
)
}
const Bidder = () => (
<div
className="w-full text-white overflow-hidden bg-gray-800 rounded-md shadow-xl
shadow-black md:w-3/5 lg:w-2/5 md:mt-0 font-sans"
>
<img src={picture0} alt="nft" className="object-cover w-full h-60" />
<div
className="shadow-lg shadow-gray-400 border-4 border-[#ffffff36]
flex flex-row justify-between items-center px-3"
>
<div className="p-2">
Current Bid
<div className="font-bold text-center">2.231 ETH</div>
</div>
<div className="p-2">
Auction End
<div className="font-bold text-center">20:10</div>
</div>
</div>
<div
className="bg-green-500 w-full h-[40px] p-2 text-center
font-bold font-mono "
>
Place a Bid
</div>
</div>
)
const Banner = () => {
const [currentUser] = useGlobalState('currentUser')
const handleLogin = async () => {
await toast.promise(
new Promise(async (resolve, reject) => {
await loginWithCometChat()
.then((user) => {
setGlobalState('currentUser', user)
console.log(user)
resolve()
})
.catch((err) => {
console.log(err)
reject()
})
}),
{
pending: 'Signing in...',
success: 'Logged in successful 👌',
error: 'Error, are you signed up? 🤯',
},
)
}
const handleSignup = async () => {
await toast.promise(
new Promise(async (resolve, reject) => {
await signUpWithCometChat()
.then((user) => {
console.log(user)
resolve(user)
})
.catch((err) => {
console.log(err)
reject(err)
})
}),
{
pending: 'Signing up...',
success: 'Signned up successful 👌',
error: 'Error, maybe you should login instead? 🤯',
},
)
}
return (
<div
className="flex flex-col md:flex-row w-full justify-between
items-center mx-auto"
>
<div className="">
<h1 className="text-white font-semibold text-5xl py-1">
Discover, Collect
</h1>
<h1 className="font-semibold text-4xl mb-5 text-white py-1">
and Sell
<span className="text-green-500 px-1">NFTs</span>.
</h1>
<p className="text-white font-light">
More than 100+ NFT available for collect
</p>
<p className="text-white mb-11 font-light">& sell, get your NFT now.</p>
<div className="flex flew-row text-5xl mb-4">
{!currentUser ? (
<div className="flex justify-start items-center space-x-2">
<button
className="text-white text-sm p-2 bg-green-500 rounded-sm w-auto
flex flex-row justify-center items-center shadow-md shadow-gray-700"
onClick={handleLogin}
>
Login Now
</button>
<button
className="text-white text-sm p-2 flex flex-row shadow-md shadow-gray-700
justify-center items-center bg-[#ffffff36] rounded-sm w-auto"
onClick={handleSignup}
>
Signup Now
</button>
</div>
) : (
<button
className="text-white text-sm p-2 bg-green-500 rounded-sm w-auto
flex flex-row justify-center items-center shadow-md shadow-gray-700"
onClick={() => setGlobalState('boxModal', 'scale-100')}
>
Create NFT
<BsArrowRightShort className="font-bold animate-pulse" />
</button>
)}
</div>
<div className="flex items-center justify-between w-3/4 mt-5">
<div>
<p className="text-white font-bold">100k+</p>
<small className="text-gray-300">Auction</small>
</div>
<div>
<p className="text-white font-bold">210k+</p>
<small className="text-gray-300">Rare</small>
</div>
<div>
<p className="text-white font-bold">120k+</p>
<small className="text-gray-300">Artist</small>
</div>
</div>
</div>
</div>
)
}
export default Hero
view raw Hero.jsx hosted with ❤ by GitHub

Artworks Component

Artworks Component

Again, create a component in the components folder called Artworks.jsx and paste the following codes below.

import { Link } from 'react-router-dom'
import { toast } from 'react-toastify'
import { buyNFTItem } from '../services/blockchain'
import { setGlobalState } from '../store'
import Countdown from './Countdown'
const Artworks = ({ auctions, title, showOffer }) => {
return (
<div className="w-4/5 py-10 mx-auto justify-center">
<p className="text-xl uppercase text-white mb-4">
{title ? title : 'Current Bids'}
</p>
<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 text-white font-mono px-1"
>
{auctions.map((auction, i) => (
<Auction key={i} auction={auction} showOffer={showOffer} />
))}
</div>
</div>
)
}
const Auction = ({ auction, showOffer }) => {
const onOffer = () => {
setGlobalState('auction', auction)
setGlobalState('offerModal', 'scale-100')
}
const onPlaceBid = () => {
setGlobalState('auction', auction)
setGlobalState('bidBox', 'scale-100')
}
const onEdit = () => {
setGlobalState('auction', auction)
setGlobalState('priceModal', 'scale-100')
}
const handleNFTpurchase = async () => {
await toast.promise(
new Promise(async (resolve, reject) => {
await buyNFTItem(auction)
.then(() => resolve())
.catch(() => reject())
}),
{
pending: 'Processing...',
success: 'Purchase successful, will reflect within 30sec 👌',
error: 'Encountered error 🤯',
},
)
}
return (
<div
className="full overflow-hidden bg-gray-800 rounded-md shadow-xl
shadow-black md:w-6/4 md:mt-0 font-sans my-4"
>
<Link to={'/nft/' + auction.tokenId}>
<img
src={auction.image}
alt={auction.name}
className="object-cover w-full h-60"
/>
</Link>
<div
className="shadow-lg shadow-gray-400 border-4 border-[#ffffff36]
flex flex-row justify-between items-center text-gray-300 px-2"
>
<div className="flex flex-col items-start py-2 px-1">
<span>Current Bid</span>
<div className="font-bold text-center">{auction.price} ETH</div>
</div>
<div className="flex flex-col items-start py-2 px-1">
<span>Auction End</span>
<div className="font-bold text-center">
{auction.live && auction.duration > Date.now() ? (
<Countdown timestamp={auction.duration} />
) : (
'00:00:00'
)}
</div>
</div>
</div>
{showOffer ? (
auction.live && Date.now() < auction.duration ? (
<button
className="bg-yellow-500 w-full h-[40px] p-2 text-center
font-bold font-mono"
onClick={onOffer}
>
Auction Live
</button>
) : (
<div className="flex justify-start">
<button
className="bg-red-500 w-full h-[40px] p-2 text-center
font-bold font-mono"
onClick={onOffer}
>
Offer
</button>
<button
className="bg-orange-500 w-full h-[40px] p-2 text-center
font-bold font-mono"
onClick={onEdit}
>
Change
</button>
</div>
)
) : auction.biddable ? (
<button
className="bg-green-500 w-full h-[40px] p-2 text-center
font-bold font-mono"
onClick={onPlaceBid}
disabled={Date.now() > auction.duration}
>
Place a Bid
</button>
) : (
<button
className="bg-red-500 w-full h-[40px] p-2 text-center
font-bold font-mono"
onClick={handleNFTpurchase}
disabled={Date.now() > auction.duration}
>
Buy NFT
</button>
)}
</div>
)
}
export default Artworks
view raw Artworks.jsx hosted with ❤ by GitHub

Footer Component

Footer Component

Next, create a component in the components folder called Footer.jsx and paste the following codes below.

view raw Footer.jsx hosted with ❤ by GitHub

Other Component

The following are components supporting the full functionality of the rest of this application.

Countdown Component
This component is responsible for rendering a countdown timer on all the NFTs, see the codes below.

import { useState, useEffect } from 'react'
const Countdown = ({ timestamp }) => {
const [timeLeft, setTimeLeft] = useState(timestamp - Date.now())
useEffect(() => {
const interval = setInterval(() => {
setTimeLeft(timestamp - Date.now())
}, 1000)
return () => clearInterval(interval)
}, [timestamp])
const days = Math.floor(timeLeft / (1000 * 60 * 60 * 24))
const hours = Math.floor(
(timeLeft % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60),
)
const minutes = Math.floor((timeLeft % (1000 * 60 * 60)) / (1000 * 60))
const seconds = Math.floor((timeLeft % (1000 * 60)) / 1000)
return Date.now() > timestamp ? (
'00:00:00'
) : (
<div>
{days}d : {hours}h : {minutes}m : {seconds}s
</div>
)
}
export default Countdown
view raw Countdown.jsx hosted with ❤ by GitHub

The Empty Component

The Empty Component

This component is responsible for displaying a small text informing the users of no NFTs in the platform. See the example code below for implementation.

const Empty = () => {
return (
<div className="w-4/5 h-48 py-10 mx-auto justify-center">
<h4 className="text-xl capitalize text-white mb-4">Nothing here bring some artworks</h4>
</div>
)
}
export default Empty
view raw Empty.jsx hosted with ❤ by GitHub

Creating an NFT

Creating an NFT

To write a Create NFT component, use the following codes. This will be a modal that accepts an image, a title, a description, and a price before submitting it to the blockchain.

Before being stored on the blockchain, data collected from a form is sent to a NodeJs API, which converts it into metadata and deploys it to IPFS.

Next, in the components folder, create a new file called "CreateNFT.jsx" and paste the following code into it.

import axios from 'axios'
import { useState } from 'react'
import { toast } from 'react-toastify'
import { FaTimes } from 'react-icons/fa'
import picture6 from '../assets/images/picture6.png'
import { setGlobalState, useGlobalState } from '../store'
import { createNftItem } from '../services/blockchain'
const CreateNFT = () => {
const [boxModal] = useGlobalState('boxModal')
const [name, setName] = useState('')
const [description, setDescription] = useState('')
const [price, setPrice] = useState('')
const [fileUrl, setFileUrl] = useState('')
const [imgBase64, setImgBase64] = useState(null)
const handleSubmit = async (e) => {
e.preventDefault()
if (!name || !price || !description || !fileUrl) return
const formData = new FormData()
formData.append('name', name)
formData.append('price', price)
formData.append('description', description)
formData.append('image', fileUrl)
await toast.promise(
new Promise(async (resolve, reject) => {
await axios
.post('http://localhost:9000/process', formData)
.then(async (res) => {
await createNftItem(res.data)
.then(async () => {
closeModal()
resolve()
})
.catch(() => reject())
reject()
})
.catch(() => reject())
}),
{
pending: 'Minting & saving data to chain...',
success: 'Minting completed, will reflect within 30sec 👌',
error: 'Encountered error 🤯',
},
)
}
const changeImage = 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 closeModal = () => {
setGlobalState('boxModal', 'scale-0')
resetForm()
}
const resetForm = () => {
setFileUrl('')
setImgBase64(null)
setName('')
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 ${boxModal}`}
>
<div className="bg-[#151c25] shadow-xl shadow-[#25bd9c] rounded-xl w-11/12 md:w-2/5 h-7/12 p-6">
<form onSubmit={handleSubmit} className="flex flex-col">
<div className="flex flex-row justify-between items-center">
<p className="font-semibold text-gray-400 italic">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 || picture6}
/>
</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-300
hover:file:bg-[#1d2631]
cursor-pointer focus:ring-0 focus:outline-none"
onChange={changeImage}
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 px-4 py-2"
type="text"
name="name"
placeholder="Title"
onChange={(e) => setName(e.target.value)}
value={name}
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 px-4 py-2"
type="number"
name="price"
step={0.01}
min={0.01}
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-18 py-2 px-4"
type="text"
name="description"
placeholder="Description"
onChange={(e) => setDescription(e.target.value)}
value={description}
required
></textarea>
</div>
<button
type="submit"
className="flex flex-row justify-center items-center
w-full text-white text-md bg-[#25bd9c]
py-2 px-5 rounded-full
drop-shadow-xl border border-transparent
hover:bg-transparent hover:text-[#ffffff]
hover:border hover:border-[#25bd9c]
focus:outline-none focus:ring mt-5"
>
Mint Now
</button>
</form>
</div>
</div>
)
}
export default CreateNFT
view raw CreateNFT.jsx hosted with ❤ by GitHub

Offering NFTs on the Market

Offering an NFT Item

This component is responsible for offering new items live on the market. Using a form, it accepts a duration for which you intend to have your NFT live on the market. Once this timeline expires, the live NFT will disappear from the market. See the codes below.

import { useState } from 'react'
import { FaTimes } from 'react-icons/fa'
import { toast } from 'react-toastify'
import { offerItemOnMarket } from '../services/blockchain'
import { setGlobalState, useGlobalState } from '../store'
const OfferItem = () => {
const [auction] = useGlobalState('auction')
const [offerModal] = useGlobalState('offerModal')
const [period, setPeriod] = useState('')
const [biddable, setBiddable] = useState('')
const [timeline, setTimeline] = useState('')
const handleSubmit = async (e) => {
e.preventDefault()
if (!period || !biddable || !timeline) return
const params = {
biddable: biddable == 'true',
}
if (timeline == 'sec') {
params.sec = Number(period)
params.min = 0
params.hour = 0
params.day = 0
} else if (timeline == 'min') {
params.sec = 0
params.min = Number(period)
params.hour = 0
params.day = 0
} else if (timeline == 'hour') {
params.sec = 0
params.min = 0
params.hour = Number(period)
params.day = 0
} else {
params.sec = 0
params.min = 0
params.hour = 0
params.day = Number(period)
}
await toast.promise(
new Promise(async (resolve, reject) => {
await offerItemOnMarket({ ...auction, ...params })
.then(async () => {
closeModal()
resolve()
})
.catch(() => reject())
}),
{
pending: 'Processing...',
success: 'Offered on Market, will reflect within 30sec 👌',
error: 'Encountered error 🤯',
},
)
}
const closeModal = () => {
setGlobalState('offerModal', 'scale-0')
setPeriod('')
setBiddable('')
}
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 timeline-300 ${offerModal}`}
>
<div className="bg-[#151c25] shadow-xl shadow-[#25bd9c] rounded-xl w-11/12 md:w-2/5 h-7/12 p-6">
<form onSubmit={handleSubmit} className="flex flex-col">
<div className="flex flex-row justify-between items-center">
<p className="font-semibold text-gray-400 italic">
{auction?.name}
</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={auction?.image}
/>
</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 px-4 py-2"
type="number"
name="period"
min={1}
placeholder="Days E.g 7"
onChange={(e) => setPeriod(e.target.value)}
value={period}
required
/>
</div>
<div className="flex flex-row justify-between items-center bg-gray-800 rounded-xl mt-5">
<select
className="block w-full text-sm
text-slate-500 bg-transparent border-0
focus:outline-none focus:ring-0 px-4 py-2"
name="biddable"
onChange={(e) => setTimeline(e.target.value)}
value={timeline}
required
>
<option value="" hidden>
Select Duration
</option>
<option value="sec">Seconds</option>
<option value="min">Minutes</option>
<option value="hour">Hours</option>
<option value="day">Days</option>
</select>
</div>
<div className="flex flex-row justify-between items-center bg-gray-800 rounded-xl mt-5">
<select
className="block w-full text-sm
text-slate-500 bg-transparent border-0
focus:outline-none focus:ring-0 px-4 py-2"
name="biddable"
onChange={(e) => setBiddable(e.target.value)}
value={biddable}
required
>
<option value="" hidden>
Select Biddability
</option>
<option value={true}>Yes</option>
<option value={false}>No</option>
</select>
</div>
<button
type="submit"
className="flex flex-row justify-center items-center
w-full text-white text-md bg-[#25bd9c]
py-2 px-5 rounded-full
drop-shadow-xl border border-transparent
hover:bg-transparent hover:text-[#ffffff]
hover:border hover:border-[#25bd9c]
focus:outline-none focus:ring mt-5"
>
Offer Item
</button>
</form>
</div>
</div>
)
}
export default OfferItem
view raw OfferItem.jsx hosted with ❤ by GitHub

Placing Bids

Placing Bids

This component allows a user to place a bid and participate in an NFT auction. This is accomplished through the use of a modal that receives the price a user intends to bid if it is within the time limit for bidding. See the codes listed below.

import { useState } from 'react'
import { FaTimes } from 'react-icons/fa'
import { toast } from 'react-toastify'
import { bidOnNFT } from '../services/blockchain'
import { setGlobalState, useGlobalState } from '../store'
const PlaceBid = () => {
const [auction] = useGlobalState('auction')
const [bidBox] = useGlobalState('bidBox')
const [price, setPrice] = useState('')
const closeModal = () => {
setGlobalState('bidBox', 'scale-0')
setPrice('')
}
const handleBidPlacement = async (e) => {
e.preventDefault()
if (!price) return
await toast.promise(
new Promise(async (resolve, reject) => {
await bidOnNFT({ ...auction, price })
.then(() => {
resolve()
closeModal()
})
.catch(() => reject())
}),
{
pending: 'Processing...',
success: 'Bid placed successful, will reflect within 30sec 👌',
error: 'Encountered error 🤯',
},
)
}
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 ${bidBox}`}
>
<div className="bg-[#151c25] shadow-xl shadow-[#25bd9c] rounded-xl w-11/12 md:w-2/5 h-7/12 p-6">
<form onSubmit={handleBidPlacement} className="flex flex-col">
<div className="flex flex-row justify-between items-center">
<p className="font-semibold text-gray-400 italic">
{auction?.name}
</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={auction?.image}
/>
</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 px-4 py-2"
type="number"
name="price"
step={0.01}
min={0.01}
placeholder="Price (Eth)"
onChange={(e) => setPrice(e.target.value)}
value={price}
required
/>
</div>
<button
type="submit"
className="flex flex-row justify-center items-center
w-full text-white text-md bg-[#25bd9c]
py-2 px-5 rounded-full
drop-shadow-xl border border-transparent
hover:bg-transparent hover:text-[#ffffff]
hover:border hover:border-[#25bd9c]
focus:outline-none focus:ring mt-5"
>
Place Bid
</button>
</form>
</div>
</div>
)
}
export default PlaceBid
view raw PlaceBid.jsx hosted with ❤ by GitHub

Changing an NFT price

Changing NFT Price

This component allows an NFT owner to change the price of an NFT that is not currently trading on the market. Accepting a new price from a form and sending it to the blockchain accomplishes this. Look at the codes below.

import { useState } from 'react'
import { toast } from 'react-toastify'
import { FaTimes } from 'react-icons/fa'
import { updatePrice } from '../services/blockchain'
import { setGlobalState, useGlobalState } from '../store'
const ChangePrice = () => {
const [auction] = useGlobalState('auction')
const [priceModal] = useGlobalState('priceModal')
const [price, setPrice] = useState('')
const handleSubmit = async (e) => {
e.preventDefault()
if (!price) return
await toast.promise(
new Promise(async (resolve, reject) => {
await updatePrice({ ...auction, price })
.then(async () => {
closeModal()
resolve()
})
.catch(() => reject())
}),
{
pending: 'Processing...',
success: 'Price updated, will reflect within 30sec 👌',
error: 'Encountered error 🤯',
},
)
}
const closeModal = () => {
setGlobalState('priceModal', 'scale-0')
setPrice('')
}
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 timeline-300 ${priceModal}`}
>
<div className="bg-[#151c25] shadow-xl shadow-[#25bd9c] rounded-xl w-11/12 md:w-2/5 h-7/12 p-6">
<form onSubmit={handleSubmit} className="flex flex-col">
<div className="flex flex-row justify-between items-center">
<p className="font-semibold text-gray-400 italic">
{auction?.name}
</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={auction?.image}
/>
</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 px-4 py-2"
type="number"
name="price"
step={0.01}
min={0.01}
placeholder="Days E.g 2.3 ETH"
onChange={(e) => setPrice(e.target.value)}
value={price}
required
/>
</div>
<button
type="submit"
className="flex flex-row justify-center items-center
w-full text-white text-md bg-[#25bd9c]
py-2 px-5 rounded-full
drop-shadow-xl border border-transparent
hover:bg-transparent hover:text-[#ffffff]
hover:border hover:border-[#25bd9c]
focus:outline-none focus:ring mt-5"
>
Change Price
</button>
</form>
</div>
</div>
)
}
export default ChangePrice
view raw ChangePrice.jsx hosted with ❤ by GitHub

The Chat Component

Live Chat with CometChat SDK

Finally, for the components, there is a chat component that is controlled by the CometChat SDK. See the codes below.

import { useEffect, useState } from 'react'
import Identicon from 'react-identicons'
import { toast } from 'react-toastify'
import { getMessages, listenForMessage, sendMessage } from '../services/chat'
import { truncate, useGlobalState } from '../store'
const Chat = ({ id, group }) => {
const [message, setMessage] = useState('')
const [messages, setMessages] = useState([])
const [connectedAccount] = useGlobalState('connectedAccount')
const handleSubmit = async (e) => {
e.preventDefault()
if (!message) return
await sendMessage(`pid_${id}`, message)
.then(async (msg) => {
setMessages((prevState) => [...prevState, msg])
setMessage('')
scrollToEnd()
})
.catch((error) => {
toast.error('Encountered Error, check the console')
console.log(error)
})
}
useEffect(async () => {
await getMessages(`pid_${id}`)
.then((msgs) => {
setMessages(msgs)
scrollToEnd()
})
.catch((error) => console.log(error))
await listenForMessage(`pid_${id}`)
.then((msg) => {
setMessages((prevState) => [...prevState, msg])
scrollToEnd()
})
.catch((error) => console.log(error))
}, [])
const scrollToEnd = () => {
const elmnt = document.getElementById('messages-container')
elmnt.scrollTop = elmnt.scrollHeight
}
return (
<div>
<h2 className="mt-12 px-2 py-1 font-bold text-2xl italic">NFT-World</h2>
<h4 className="px-2 font-semibold text-xs">Join the Live Chat</h4>
<div
className="bg-gray-800 bg-opacity-50 w-full
rounded-md p-2 sm:p-8 mt-5 shadow-md shadow-[#25bd9c]"
>
<div
id="messages-container"
className="h-[calc(100vh_-_30rem)] overflow-y-auto"
>
{messages.map((msg, i) => (
<Message
isOwner={msg.sender.uid == connectedAccount}
owner={msg.sender.uid}
msg={msg.text}
key={i}
/>
))}
</div>
<form
onSubmit={handleSubmit}
className="flex flex-row justify-between items-center bg-gray-800 rounded-md"
>
<input
className="block w-full text-sm resize-none
text-slate-100 bg-transparent border-0
focus:outline-none focus:ring-0 h-15 px-4 py-4"
type="text"
name="Leave a Message"
placeholder={
!group?.hasJoined
? 'Join group first to chat...'
: 'Leave a Message...'
}
disabled={!group?.hasJoined}
value={message}
onChange={(e) => setMessage(e.target.value)}
required
/>
<button type="submit" hidden>
Send
</button>
</form>
</div>
</div>
)
}
const Message = ({ msg, owner, isOwner }) => (
<div>
<div className="flex justify-start items-center space-x-1 mb-2">
<Identicon
string={owner}
className="h-5 w-5 object-contain bg-gray-800 rounded-full"
size={18}
/>
<div className="space-x-1">
<span className="text-[#25bd9c] font-bold">
{isOwner ? '@You' : truncate(owner, 4, 4, 11)}
</span>
<span className="text-gray-200 text-xs">{msg}</span>
</div>
</div>
</div>
)
export default Chat
view raw Chat.jsx hosted with ❤ by GitHub

The Pages

This application has about three views or pages; let's organize all of the above components into their respective views using the steps below. First, create a folder called views in the src directory and create the soon-to-be-discussed pages.

Home View

The Home View

The Home view combines two major components: the Hero and Artworks components. See the codes below.

import Artworks from '../components/Artworks'
import Empty from '../components/Empty'
import Hero from '../components/Hero'
import { useGlobalState } from '../store'
const Home = () => {
const [auctions] = useGlobalState('auctions')
return (
<div>
<Hero />
{auctions.length > 0 ? <Artworks auctions={auctions} /> : <Empty />}
</div>
)
}
export default Home
view raw Home.jsx hosted with ❤ by GitHub

Collections View

The Collection View

This view displays all of the NFTs owned by a specific user. It empowers a user to manage an NFT, such as whether or not to offer it on the market or change its price. See the codes shown below.

import { useEffect } from 'react'
import Empty from '../components/Empty'
import { useGlobalState } from '../store'
import Artworks from '../components/Artworks'
import { loadCollections } from '../services/blockchain'
const Collections = () => {
const [collections] = useGlobalState('collections')
useEffect(async () => {
await loadCollections()
})
return (
<div>
{collections.length > 0 ? (
<Artworks title="Your Collections" auctions={collections} showOffer />
) : (
<Empty />
)}
</div>
)
}
export default Collections
view raw Collections.jsx hosted with ❤ by GitHub

The NFT View

NFT View

Lastly, this view contains the chat component as well as other important components, as shown in the code below.

import { useEffect } from 'react'
import Chat from '../components/Chat'
import { toast } from 'react-toastify'
import Identicons from 'react-identicons'
import { useNavigate, useParams } from 'react-router-dom'
import Countdown from '../components/Countdown'
import { setGlobalState, truncate, useGlobalState } from '../store'
import {
buyNFTItem,
claimPrize,
getBidders,
loadAuction,
} from '../services/blockchain'
import { createNewGroup, getGroup, joinGroup } from '../services/chat'
const Nft = () => {
const { id } = useParams()
const [group] = useGlobalState('group')
const [bidders] = useGlobalState('bidders')
const [auction] = useGlobalState('auction')
const [currentUser] = useGlobalState('currentUser')
const [connectedAccount] = useGlobalState('connectedAccount')
useEffect(async () => {
await loadAuction(id)
await getBidders(id)
await getGroup(`pid_${id}`)
.then((group) => setGlobalState('group', group))
.catch((error) => console.log(error))
}, [])
return (
<>
<div
className="grid sm:flex-row md:flex-row lg:grid-cols-2 gap-6
md:gap-4 lg:gap-3 py-2.5 text-white font-sans capitalize
w-4/5 mx-auto mt-5 justify-between items-center"
>
<div
className=" text-white h-[400px] bg-gray-800 rounded-md shadow-xl
shadow-black md:w-4/5 md:items-center lg:w-4/5 md:mt-0"
>
<img
src={auction?.image}
alt={auction?.name}
className="object-contain w-full h-80 mt-10"
/>
</div>
<div className="">
<Details auction={auction} account={connectedAccount} />
{bidders.length > 0 ? (
<Bidders bidders={bidders} auction={auction} />
) : null}
<CountdownNPrice auction={auction} />
<ActionButton auction={auction} account={connectedAccount} />
</div>
</div>
<div className="w-4/5 mx-auto">
{currentUser ? <Chat id={id} group={group} /> : null}
</div>
</>
)
}
const Details = ({ auction, account }) => (
<div className="py-2">
<h1 className="font-bold text-lg mb-1">{auction?.name}</h1>
<p className="font-semibold text-sm">
<span className="text-green-500">
@
{auction?.owner == account
? 'you'
: auction?.owner
? truncate(auction?.owner, 4, 4, 11)
: ''}
</span>
</p>
<p className="text-sm py-2">{auction?.description}</p>
</div>
)
const Bidders = ({ bidders, auction }) => {
const handlePrizeClaim = async (id) => {
await toast.promise(
new Promise(async (resolve, reject) => {
await claimPrize({ tokenId: auction?.tokenId, id })
.then(() => resolve())
.catch(() => reject())
}),
{
pending: 'Processing...',
success: 'Price claim successful, will reflect within 30sec 👌',
error: 'Encountered error 🤯',
},
)
}
return (
<div className="flex flex-col">
<span>Top Bidders</span>
<div className="h-[calc(100vh_-_40.5rem)] overflow-y-auto">
{bidders.map((bid, i) => (
<div key={i} className="flex justify-between items-center">
<div className="flex justify-start items-center my-1 space-x-1">
<Identicons
className="h-5 w-5 object-contain bg-gray-800 rounded-full"
size={18}
string={bid.bidder}
/>
<span className="font-medium text-sm mr-3">
{truncate(bid.bidder, 4, 4, 11)}
</span>
<span className="text-green-400 font-medium text-sm">
{bid.price} ETH
</span>
</div>
{bid.bidder == auction?.winner &&
!bid.won &&
Date.now() > auction?.duration ? (
<button
type="button"
className="shadow-sm shadow-black text-white
bg-green-500 hover:bg-green-700 md:text-xs p-1
rounded-sm text-sm cursor-pointer font-light"
onClick={() => handlePrizeClaim(i)}
>
Claim Prize
</button>
) : null}
</div>
))}
</div>
</div>
)
}
const CountdownNPrice = ({ auction }) => {
return (
<div className="flex justify-between items-center py-5 ">
<div>
<span className="font-bold">Current Price</span>
<p className="text-sm font-light">{auction?.price}ETH</p>
</div>
<div className="lowercase">
<span className="font-bold">
{auction?.duration > Date.now() ? (
<Countdown timestamp={auction?.duration} />
) : (
'00:00:00'
)}
</span>
</div>
</div>
)
}
const ActionButton = ({ auction, account }) => {
const [group] = useGlobalState('group')
const [currentUser] = useGlobalState('currentUser')
const navigate = useNavigate()
const onPlaceBid = () => {
setGlobalState('auction', auction)
setGlobalState('bidBox', 'scale-100')
}
const handleNFTpurchase = async () => {
await toast.promise(
new Promise(async (resolve, reject) => {
await buyNFTItem(auction)
.then(() => resolve())
.catch(() => reject())
}),
{
pending: 'Processing...',
success: 'Purchase successful, will reflect within 30sec 👌',
error: 'Encountered error 🤯',
},
)
}
const handleCreateGroup = async () => {
if (!currentUser) {
navigate('/')
toast.warning('You need to login or sign up first.')
return
}
await toast.promise(
new Promise(async (resolve, reject) => {
await createNewGroup(`pid_${auction?.tokenId}`, auction?.name)
.then((gp) => {
setGlobalState('group', gp)
resolve(gp)
})
.catch((error) => reject(new Error(error)))
}),
{
pending: 'Creating...',
success: 'Group created 👌',
error: 'Encountered error 🤯',
},
)
}
const handleJoineGroup = async () => {
if (!currentUser) {
navigate('/')
toast.warning('You need to login or sign up first.')
return
}
await toast.promise(
new Promise(async (resolve, reject) => {
await joinGroup(`pid_${auction?.tokenId}`)
.then((gp) => {
setGlobalState('group', gp)
resolve(gp)
})
.catch((error) => reject(new Error(error)))
}),
{
pending: 'Joining...',
success: 'Group Joined 👌',
error: 'Encountered error 🤯',
},
)
}
return auction?.owner == account ? (
<div className="flex justify-start items-center space-x-2 mt-2">
{!group ? (
<button
type="button"
className="shadow-sm shadow-black text-white
bg-red-500 hover:bg-red-700 md:text-xs p-2.5
rounded-sm cursor-pointer font-light"
onClick={handleCreateGroup}
>
Create Group
</button>
) : null}
</div>
) : (
<div className="flex justify-start items-center space-x-2 mt-2">
{!group?.hasJoined ? (
<button
type="button"
className="shadow-sm shadow-black text-white
bg-gray-500 hover:bg-gray-700 md:text-xs p-2.5
rounded-sm cursor-pointer font-light"
onClick={handleJoineGroup}
>
Join Group
</button>
) : null}
{auction?.biddable && auction?.duration > Date.now() ? (
<button
type="button"
className="shadow-sm shadow-black text-white
bg-gray-500 hover:bg-gray-700 md:text-xs p-2.5
rounded-sm cursor-pointer font-light"
onClick={onPlaceBid}
>
Place a Bid
</button>
) : null}
{!auction?.biddable && auction?.duration > Date.now() ? (
<button
type="button"
className="shadow-sm shadow-black text-white
bg-red-500 hover:bg-red-700 md:text-xs p-2.5
rounded-sm cursor-pointer font-light"
onClick={handleNFTpurchase}
>
Buy NFT
</button>
) : null}
</div>
)
}
export default Nft
view raw Nft.jsx hosted with ❤ by GitHub

Updating The App.jsx File

Update the App file with the codes below in other to bundle up all the components and pages together.

import Nft from './views/Nft'
import Home from './views/Home'
import Header from './components/Header'
import Footer from './components/Footer'
import { useEffect, useState } from 'react'
import PlaceBid from './components/PlaceBid'
import Collections from './views/Collections'
import CreateNFT from './components/CreateNFT'
import { ToastContainer } from 'react-toastify'
import { Route, Routes } from 'react-router-dom'
import { isWallectConnected, loadAuctions } from './services/blockchain'
import { setGlobalState, useGlobalState } from './store'
import OfferItem from './components/OfferItem'
import ChangePrice from './components/ChangePrice'
import { checkAuthState } from './services/chat'
function App() {
const [loaded, setLoaded] = useState(false)
const [auction] = useGlobalState('auction')
useEffect(async () => {
await isWallectConnected()
await loadAuctions().finally(() => setLoaded(true))
await checkAuthState()
.then((user) => setGlobalState('currentUser', user))
.catch((error) => setGlobalState('currentUser', null))
console.log('Blockchain Loaded')
}, [])
return (
<div
className="min-h-screen bg-gradient-to-t from-gray-800 bg-repeat
via-[#25bd9c] to-gray-900 bg-center subpixel-antialiased"
>
<Header />
{loaded ? (
<Routes>
<Route path="/" element={<Home />} />
<Route path="/collections" element={<Collections />} />
<Route path="/nft/:id" element={<Nft />} />
</Routes>
) : null}
<CreateNFT />
{auction ? (
<>
<PlaceBid />
<OfferItem />
<ChangePrice />
</>
) : null}
<Footer />
<ToastContainer
position="bottom-center"
autoClose={5000}
hideProgressBar={false}
newestOnTop={false}
closeOnClick
rtl={false}
pauseOnFocusLoss
draggable
pauseOnHover
theme="dark"
/>
</div>
)
}
export default App
view raw App.jsx hosted with ❤ by GitHub

Updating the Index.jsx and CSS files

Use the codes below to update the index.jsx and index.css files respectively.

@import url('https://fonts.googleapis.com/css2?family=Open+Sans:wght@300;400;500;600;700&display=swap');
* html {
padding: 0;
margin: 0;
box-sizing: border-box;
}
body {
margin: 0;
font-family: 'Open Sans', sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.gradient-bg-hero {
background-color: #151c25;
background-image: radial-gradient(
at 0% 0%,
hsl(302deg 25% 18%) 0,
transparent 50%
),
radial-gradient(at 50% 0%, hsl(0deg 39% 30%) 0, transparent 50%),
radial-gradient(at 100% 0%, hsla(339, 49%, 30%, 1) 0, transparent 50%);
}
.gradient-bg-artworks {
background-color: #0f0e13;
background-image: radial-gradient(
at 50% 50%,
hsl(302deg 25% 18%) 0,
transparent 50%
),
radial-gradient(at 0% 0%, hsla(253, 16%, 7%, 1) 0, transparent 50%),
radial-gradient(at 50% 50%, hsla(339, 39%, 25%, 1) 0, transparent 50%);
}
.gradient-bg-footer {
background-color: #151c25;
background-image: radial-gradient(
at 0% 100%,
hsl(0deg 39% 30%) 0,
transparent 53%
),
radial-gradient(at 50% 150%, hsla(339, 49%, 30%, 1) 0, transparent 50%);
}
.text-gradient {
background: -webkit-linear-gradient(#eee, #333);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.lds-dual-ring {
display: inline-block;
}
.lds-dual-ring:after {
content: ' ';
display: block;
width: 64px;
height: 64px;
margin: 8px;
border-radius: 50%;
border: 6px solid #fff;
border-color: #fff transparent #fff transparent;
animation: lds-dual-ring 1.2s linear infinite;
}
@keyframes lds-dual-ring {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
@tailwind base;
@tailwind components;
@tailwind utilities;
view raw Index.css hosted with ❤ by GitHub
import './index.css'
import App from './App'
import React from 'react'
import ReactDOM from 'react-dom'
import 'react-toastify/dist/ReactToastify.css'
import { initCometChat } from './services/chat'
import { BrowserRouter } from 'react-router-dom'
initCometChat().then(() => {
ReactDOM.render(
<BrowserRouter>
<App />
</BrowserRouter>,
document.getElementById('root'),
)
})
view raw Index.jsx hosted with ❤ by GitHub

Adding the App Services

In this application, we have two services: chat and blockchain, as shown in the codes below. Simply create a new folder called services in the src directory and place the following files in it using the codes below.

Chat Services

import { CometChat } from '@cometchat-pro/chat'
import { getGlobalState } 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) => console.log(error))
}
const loginWithCometChat = async () => {
const authKey = CONSTANTS.Auth_Key
const UID = getGlobalState('connectedAccount')
return new Promise(async (resolve, reject) => {
await CometChat.login(UID, authKey)
.then((user) => resolve(user))
.catch((error) => reject(error))
})
}
const signUpWithCometChat = async () => {
const authKey = CONSTANTS.Auth_Key
const UID = getGlobalState('connectedAccount')
const user = new CometChat.User(UID)
user.setName(UID)
return new Promise(async (resolve, reject) => {
await CometChat.createUser(user, authKey)
.then((user) => resolve(user))
.catch((error) => reject(error))
})
}
const logOutWithCometChat = async () => {
return new Promise(async (resolve, reject) => {
await CometChat.logout()
.then(() => resolve())
.catch(() => reject())
})
}
const checkAuthState = async () => {
return new Promise(async (resolve, reject) => {
await CometChat.getLoggedinUser()
.then((user) => resolve(user))
.catch((error) => reject(error))
})
}
const createNewGroup = async (GUID, groupName) => {
const groupType = CometChat.GROUP_TYPE.PUBLIC
const password = ''
const group = new CometChat.Group(GUID, groupName, groupType, password)
return new Promise(async (resolve, reject) => {
await CometChat.createGroup(group)
.then((group) => resolve(group))
.catch((error) => reject(error))
})
}
const getGroup = async (GUID) => {
return new Promise(async (resolve, reject) => {
await CometChat.getGroup(GUID)
.then((group) => resolve(group))
.catch((error) => reject(error))
})
}
const joinGroup = async (GUID) => {
const groupType = CometChat.GROUP_TYPE.PUBLIC
const password = ''
return new Promise(async (resolve, reject) => {
await CometChat.joinGroup(GUID, groupType, password)
.then((group) => resolve(group))
.catch((error) => reject(error))
})
}
const getMessages = async (UID) => {
const limit = 30
const messagesRequest = new CometChat.MessagesRequestBuilder()
.setGUID(UID)
.setLimit(limit)
.build()
return new Promise(async (resolve, reject) => {
await messagesRequest
.fetchPrevious()
.then((messages) => resolve(messages.filter((msg) => msg.type == 'text')))
.catch((error) => reject(error))
})
}
const sendMessage = async (receiverID, messageText) => {
const receiverType = CometChat.RECEIVER_TYPE.GROUP
const textMessage = new CometChat.TextMessage(
receiverID,
messageText,
receiverType,
)
return new Promise(async (resolve, reject) => {
await CometChat.sendMessage(textMessage)
.then((message) => resolve(message))
.catch((error) => reject(error))
})
}
const listenForMessage = async (listenerID) => {
return new Promise(async (resolve, reject) => {
CometChat.addMessageListener(
listenerID,
new CometChat.MessageListener({
onTextMessageReceived: (message) => resolve(message),
}),
)
})
}
export {
initCometChat,
loginWithCometChat,
signUpWithCometChat,
logOutWithCometChat,
getMessages,
sendMessage,
checkAuthState,
createNewGroup,
getGroup,
joinGroup,
listenForMessage,
}
view raw chat.jsx hosted with ❤ by GitHub

Blockchain Service

import abi from '../abis/src/contracts/Auction.sol/Auction.json'
import address from '../abis/contractAddress.json'
import { getGlobalState, setGlobalState } from '../store'
import { ethers } from 'ethers'
import { checkAuthState, logOutWithCometChat } from './chat'
const { ethereum } = window
const ContractAddress = address.address
const ContractAbi = abi.abi
let tx
const toWei = (num) => ethers.utils.parseEther(num.toString())
const fromWei = (num) => ethers.utils.formatEther(num)
const getEthereumContract = async () => {
const connectedAccount = getGlobalState('connectedAccount')
if (connectedAccount) {
const provider = new ethers.providers.Web3Provider(ethereum)
const signer = provider.getSigner()
const contract = new ethers.Contract(ContractAddress, ContractAbi, signer)
return contract
} else {
return getGlobalState('contract')
}
}
const isWallectConnected = async () => {
try {
if (!ethereum) return alert('Please install Metamask')
const accounts = await ethereum.request({ method: 'eth_accounts' })
setGlobalState('connectedAccount', accounts[0]?.toLowerCase())
window.ethereum.on('chainChanged', (chainId) => {
window.location.reload()
})
window.ethereum.on('accountsChanged', async () => {
setGlobalState('connectedAccount', accounts[0]?.toLowerCase())
await isWallectConnected()
await loadCollections()
await logOutWithCometChat()
await checkAuthState()
.then((user) => setGlobalState('currentUser', user))
.catch((error) => setGlobalState('currentUser', null))
})
if (accounts.length) {
setGlobalState('connectedAccount', accounts[0]?.toLowerCase())
} else {
alert('Please connect wallet.')
console.log('No accounts found.')
}
} catch (error) {
reportError(error)
}
}
const connectWallet = async () => {
try {
if (!ethereum) return alert('Please install Metamask')
const accounts = await ethereum.request({ method: 'eth_requestAccounts' })
setGlobalState('connectedAccount', accounts[0]?.toLowerCase())
} catch (error) {
reportError(error)
}
}
const createNftItem = async ({
name,
description,
image,
metadataURI,
price,
}) => {
try {
if (!ethereum) return alert('Please install Metamask')
const connectedAccount = getGlobalState('connectedAccount')
const contract = await getEthereumContract()
tx = await contract.createAuction(
name,
description,
image,
metadataURI,
toWei(price),
{
from: connectedAccount,
value: toWei(0.02),
},
)
await tx.wait()
await loadAuctions()
} catch (error) {
reportError(error)
}
}
const updatePrice = async ({ tokenId, price }) => {
try {
if (!ethereum) return alert('Please install Metamask')
const connectedAccount = getGlobalState('connectedAccount')
const contract = await getEthereumContract()
tx = await contract.changePrice(tokenId, toWei(price), {
from: connectedAccount,
})
await tx.wait()
await loadAuctions()
} catch (error) {
reportError(error)
}
}
const offerItemOnMarket = async ({
tokenId,
biddable,
sec,
min,
hour,
day,
}) => {
try {
if (!ethereum) return alert('Please install Metamask')
const connectedAccount = getGlobalState('connectedAccount')
const contract = await getEthereumContract()
tx = await contract.offerAuction(tokenId, biddable, sec, min, hour, day, {
from: connectedAccount,
})
await tx.wait()
await loadAuctions()
} catch (error) {
reportError(error)
}
}
const buyNFTItem = async ({ tokenId, price }) => {
try {
if (!ethereum) return alert('Please install Metamask')
const connectedAccount = getGlobalState('connectedAccount')
const contract = await getEthereumContract()
tx = await contract.buyAuctionedItem(tokenId, {
from: connectedAccount,
value: toWei(price),
})
await tx.wait()
await loadAuctions()
await loadAuction(tokenId)
} catch (error) {
reportError(error)
}
}
const bidOnNFT = async ({ tokenId, price }) => {
try {
if (!ethereum) return alert('Please install Metamask')
const connectedAccount = getGlobalState('connectedAccount')
const contract = await getEthereumContract()
tx = await contract.placeBid(tokenId, {
from: connectedAccount,
value: toWei(price),
})
await tx.wait()
await getBidders(tokenId)
await loadAuction(tokenId)
} catch (error) {
reportError(error)
}
}
const claimPrize = async ({ tokenId, id }) => {
try {
if (!ethereum) return alert('Please install Metamask')
const connectedAccount = getGlobalState('connectedAccount')
const contract = await getEthereumContract()
tx = await contract.claimPrize(tokenId, id, {
from: connectedAccount,
})
await tx.wait()
await getBidders(tokenId)
} catch (error) {
reportError(error)
}
}
const loadAuctions = async () => {
try {
if (!ethereum) return alert('Please install Metamask')
const contract = await getEthereumContract()
const auctions = await contract.getLiveAuctions()
setGlobalState('auctions', structuredAuctions(auctions))
setGlobalState(
'auction',
structuredAuctions(auctions).sort(() => 0.5 - Math.random())[0],
)
} catch (error) {
reportError(error)
}
}
const loadAuction = async (id) => {
try {
if (!ethereum) return alert('Please install Metamask')
const contract = await getEthereumContract()
const auction = await contract.getAuction(id)
setGlobalState('auction', structuredAuctions([auction])[0])
} catch (error) {
reportError(error)
}
}
const getBidders = async (id) => {
try {
if (!ethereum) return alert('Please install Metamask')
const contract = await getEthereumContract()
const bidders = await contract.getBidders(id)
setGlobalState('bidders', structuredBidders(bidders))
} catch (error) {
reportError(error)
}
}
const loadCollections = async () => {
try {
if (!ethereum) return alert('Please install Metamask')
const connectedAccount = getGlobalState('connectedAccount')
const contract = await getEthereumContract()
const collections = await contract.getMyAuctions({ from: connectedAccount })
setGlobalState('collections', structuredAuctions(collections))
} catch (error) {
reportError(error)
}
}
const structuredAuctions = (auctions) =>
auctions
.map((auction) => ({
tokenId: auction.tokenId.toNumber(),
owner: auction.owner.toLowerCase(),
seller: auction.seller.toLowerCase(),
winner: auction.winner.toLowerCase(),
name: auction.name,
description: auction.description,
duration: Number(auction.duration + '000'),
image: auction.image,
price: fromWei(auction.price),
biddable: auction.biddable,
sold: auction.sold,
live: auction.live,
}))
.reverse()
const structuredBidders = (bidders) =>
bidders
.map((bidder) => ({
timestamp: Number(bidder.timestamp + '000'),
bidder: bidder.bidder.toLowerCase(),
price: fromWei(bidder.price),
refunded: bidder.refunded,
won: bidder.won,
}))
.sort((a, b) => b.price - a.price)
const reportError = (error) => {
console.log(error.message)
throw new Error('No ethereum object.')
}
export {
isWallectConnected,
connectWallet,
createNftItem,
loadAuctions,
loadAuction,
loadCollections,
offerItemOnMarket,
buyNFTItem,
bidOnNFT,
getBidders,
claimPrize,
updatePrice,
}
view raw blockchain.jsx hosted with ❤ by GitHub

The Store

The store is a state management service included in this application. This is where all of the data extracted from the blockchain is kept. To replicate, create a store folder within the src directory. Next, within this folder, create a file called index.jsx and paste the codes below into it.

import { createGlobalState } from 'react-hooks-global-state'
const { getGlobalState, useGlobalState, setGlobalState } = createGlobalState({
boxModal: 'scale-0',
bidBox: 'scale-0',
offerModal: 'scale-0',
priceModal: 'scale-0',
connectedAccount: '',
collections: [],
bidders: [],
auctions: [],
auction: null,
currentUser: null,
group: null,
})
const truncate = (text, startChars, endChars, maxLength) => {
if (text.length > maxLength) {
let start = text.substring(0, startChars)
let end = text.substring(text.length - endChars, text.length)
while (start.length + end.length < maxLength) {
start = start + '.'
}
return start + end
}
return text
}
const convertToSeconds = (minutes, hours, days) => {
const seconds = minutes * 60 + hours * 3600 + days * 86400
const timestamp = new Date().getTime()
return timestamp + seconds
}
export {
getGlobalState,
useGlobalState,
setGlobalState,
truncate,
convertToSeconds,
}
view raw index.jsx hosted with ❤ by GitHub

Now start up the application by running **yarn start** on another terminal to see the result on the terminal. If you experience any trouble replicating this project you can drop a question on our discord channel.

You can also see this video to learn more about how to build an NFT marketplace from design to deployment.

https://www.youtube.com/watch?v=fJIiqeevqoU&

Congratulations, that is how you build an NFT Marketplace using React, Solidity, and CometChat.

Conclusion

In conclusion, building an NFT auction site with React, Solidity, and CometChat requires a combination of front-end and back-end development skills.

By using these tools together, it is possible to create a fully functional NFT auction site that is secure, scalable, and user-friendly.

If you're ready to dive deeper into web3 development, schedule your private web3 classes with me to accelerate your web3 learning.

That said, I'll see you next time, and have a great day!

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 his website.

Sentry image

See why 4M developers consider Sentry, “not bad.”

Fixing code doesn’t have to be the worst part of your day. Learn how Sentry can help.

Learn more

Top comments (3)

Collapse
 
juyuancai profile image
juyuancai

Hi, when you use IPFS to publish file, do you need to purchase a VPS also with pinata pin service? Now we have a better solution called Foggie. With Foggie , you can get a 2C4G VPS with pin service together with only the same price as pinata. You will also get much larger bandwith to 4T. Also , when you use it, you can have token rewards. Here is the link: https://foggie.fogworks.io/?pcode=uZVcLL&cc=1008#/fogworks

Collapse
 
christopherxp profile image
Christopherxp

I really liked these auctions recently. Thanks to the campaign, I bought a very interesting nft. If anyone wants to start bidding nft, I recommend this post: nftmonk.com/what-is-an-nft-auction...

Collapse
 
9opsec profile image
9opsec

I'm using chrome and the github embedded code is not rendering correctly.