Techno Blender
Digitally Yours.

How to Build a Web3 Game eShop with React, Solidity, and CometChat

0 84


Web3 development is officially the new way to build web applications, and if you are not there yet, you need to catch up. The way to master building web3 apps is by understanding smart contracts, a frontend framework such as React, and how to link the smart contract with the front end.

In this tutorial, you will learn how to build a decentralized web3 eShop for selling game items using the native ETH currency.

This app comprises the smart contract layer, a front end where all the interactions with the smart contract take place, and an anonymous chat feature using the CometChat SDK.

If you are ready to crush this build, then let’s get started.

What you will be building: see the demo on the Goerli test network and git repo here.

Prerequisite

You will need the following tools installed to build along with me:

  • NodeJs (Super important)
  • EthersJs
  • Hardhat
  • React
  • Tailwind CSS
  • CometChat SDK
  • Metamask
  • Yarn

Installing Dependencies

Clone the starter project from this Git repo to your computer. Also, make sure to replace it with the name of your preferred project. See the command below.

git clone https://github.com/Daltonic/tailwind_ethers_starter_kit <PROJECT_NAME>
cd <PROJECT_NAME>

Now, 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": "GameShop",
  "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",
    "deploy": "yarn hardhat run scripts/deploy.js --network localhost"
  },
  "dependencies": {
    "@cometchat-pro/chat": "^3.0.10",
    "@nomiclabs/hardhat-ethers": "^2.1.0",
    "@nomiclabs/hardhat-waffle": "^2.0.3",
    "ethereum-waffle": "^3.4.4",
    "ethers": "^5.6.9",
    "hardhat": "^2.10.1",
    "ipfs-http-client": "^57.0.3",
    "moment": "^2.29.4",
    "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",
    "react-toastify": "^9.0.8",
    "web-vitals": "^2.1.4"
  },
  "devDependencies": {
    "@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",
    "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",
    "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"
    ]
  }
}

With the above codes replaced and saved in your package.json, run the command below to install the entire packages listed above.

yarn install

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.

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 GameShop.

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.

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=******************************

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

Configuring Alchemy App

STEP 1:

Head to Alchemy, and create an account.

STEP 2:
From the dashboard create a new project.

STEP 3:
Copy the Goerli test network WebSocket or HTTPS endpoint URL to your .env file.

After that, enter the private key of your preferred Metamask account to the DEPLOYER_KEY in your environment variables and save. 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.

Extracting Your Metamask Private Key

STEP 1:
Make sure Goerli is selected as the test network in your Metamask browser extension, Rinkeby and the older test nets have now been depreciated.

Next, 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=***************************
DEPLOYER_KEY=**********************

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

Configuring the Hardhat script

At the root of this project, open the hardhat.config.js file and replace its content 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"
    },
    goerli: {
      url: process.env.ENDPOINT_URL,
      accounts: [process.env.DEPLOYER_KEY]
    }
  },
  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 code 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.

The Blockchain Service File

Now that we have the above configurations set up, let’s create the smart contract for this build. On your project, head to the **src** directory and create a new folder called **contracts**.

Inside this contracts folder, create a new file called **Shop.sol**, this file will contain all the logics that regulates the activities of the smart contract. Copy, paste, and save the codes below inside the **Shop.sol** file. See the full code below.

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

contract Shop {
    enum OrderEnum {
        PLACED,
        DELEVIRED,
        CANCELED,
        REFUNDED
    }

    struct ProductStruct {
        uint id;
        string sku;
        address seller;
        string name;
        string imageURL;
        string description;
        uint price;
        uint timestamp;
        bool deleted;
        uint stock;
    }

    struct OrderStruct {
        uint pid;
        uint id;
        string sku;
        string name;
        string imageURL;
        address buyer;
        address seller;
        uint qty;
        uint total;
        uint timestamp;
        string destination;
        string phone;
        OrderEnum status;
    }

    struct CartStruct {
        uint id;
        uint qty;
    }

    struct BuyerStruct {
        address buyer;
        uint price;
        uint qty;
        uint timestamp;
    }

    struct ShopStats {
        uint products;
        uint orders;
        uint sellers;
        uint sales;
        uint paid;
        uint balance;
    }

    address public owner;
    ShopStats public stats;
    uint public fee;
    ProductStruct[] products;
    mapping(address => ProductStruct[]) productsOf;
    mapping(uint => OrderStruct[]) ordersOf;
    mapping(address => ShopStats) public statsOf;
    mapping(uint => BuyerStruct[]) buyersOf;
    mapping(uint => bool) public productExist;
    mapping(uint => mapping(uint => bool)) public orderExist;

    event Sale(
        uint256 id,
        address indexed buyer,
        address indexed seller,
        uint256 price,
        uint256 timestamp
    );

    constructor(uint _fee) {
        owner = msg.sender;
        fee = _fee;
    }

    function createProduct(
        string memory sku,
        string memory name,
        string memory description,
        string memory imageURL,
        uint price,
        uint stock
    ) public payable returns (bool) {
        require(msg.value >= fee, "Insufficient fund");
        require(bytes(sku).length > 0, "sku cannot be empty");
        require(bytes(name).length > 0, "name cannot be empty");
        require(bytes(description).length > 0, "description cannot be empty");
        require(bytes(imageURL).length > 0, "image URL cannot be empty");
        require(price > 0, "price cannot be zero");
        require(stock > 0, "stock cannot be zero");

        productExist[stats.products] = true;
        statsOf[msg.sender].products++;
        stats.sellers++;
        ProductStruct memory product;

        product.id = stats.products++;
        product.sku = sku;
        product.seller = msg.sender;
        product.name = name;
        product.imageURL = imageURL;
        product.description = description;
        product.price = price;
        product.stock = stock;
        product.timestamp = block.timestamp;

        products.push(product);
        return true;
    }

    function updateProduct(
        uint id,
        string memory name,
        string memory description,
        string memory imageURL,
        uint price,
        uint stock
    ) public returns (bool) {
        require(products[id].seller == msg.sender, "Unauthorize Personel");
        require(bytes(name).length > 0, "name cannot be empty");
        require(bytes(description).length > 0, "description cannot be empty");
        require(price > 0, "price cannot be zero");
        require(stock > 0, "stock cannot be zero");

        ProductStruct memory product;
        product.id = id;
        product.seller = msg.sender;
        product.name = name;
        product.imageURL = imageURL;
        product.description = description;
        product.price = price;
        product.stock = stock;

        products[id] = product;
        updateOrderDetails(product);

        return true;
    }

    function updateOrderDetails(ProductStruct memory product) internal {
        for(uint i=0; i < ordersOf[product.id].length; i++) {
            OrderStruct memory order = ordersOf[product.id][i];
            order.name = product.name;
            order.imageURL = product.imageURL;
            ordersOf[product.id][i] = order;
        }
    }

    function deleteProduct(uint id) public returns (bool) {
        require(products[id].seller == msg.sender, "Unauthorize Personel");
        products[id].deleted = true;
        return true;
    }

    function getProduct(uint id) public view returns (ProductStruct memory) {
        require(productExist[id], "Product not found");
        return products[id];
    }
    
    function getProducts() public view returns (ProductStruct[] memory) {
        return products;
    }

    function createOrder(
        uint[] memory ids,
        uint[] memory qtys,
        string memory destination,
        string memory phone
    ) public payable returns (bool) {
        require(msg.value >= totalCost(ids, qtys), "Insufficient amount");
        require(bytes(destination).length > 0, "destination cannot be empty");
        require(bytes(phone).length > 0, "phone cannot be empty");
    
        stats.balance += totalCost(ids, qtys);

        for(uint i = 0; i < ids.length; i++) {
            
            if(productExist[ids[i]] && products[ids[i]].stock >= qtys[i]) {
                products[ids[i]].stock -= qtys[i];
                statsOf[msg.sender].orders++;
                stats.orders++;

                OrderStruct memory order;

                order.pid = products[ids[i]].id;
                order.id = ordersOf[order.pid].length; // order Id resolved
                order.sku = products[ids[i]].sku;
                order.buyer = msg.sender;
                order.seller = products[ids[i]].seller;
                order.name = products[ids[i]].name;
                order.imageURL = products[ids[i]].imageURL;
                order.qty = qtys[i];
                order.total = qtys[i] * products[ids[i]].price;
                order.timestamp = block.timestamp;
                order.destination = destination;
                order.phone = phone;

                ordersOf[order.pid].push(order);
                orderExist[order.pid][order.id] = true;

                emit Sale(
                    order.id,
                    order.buyer,
                    order.seller,
                    order.total,
                    block.timestamp
                );
            }
        }

        return true;
    }

    function totalCost(uint[] memory ids, uint[] memory qtys) internal view returns (uint) {
        uint total;
        for(uint i = 0; i < ids.length; i++) {
            total += products[i].price * qtys[i];
        }
        return total;
    }

    function deliverOrder(uint pid, uint id) public returns (bool) {
        require(orderExist[pid][id], "Order not found");
        OrderStruct memory order = ordersOf[pid][id];
        require(order.seller == msg.sender, "Unauthorized Entity");
        require(order.status != OrderEnum.DELEVIRED, "Order already delievered");
        
        order.status = OrderEnum.DELEVIRED;
        ordersOf[pid][id] = order;

        stats.balance -= order.total;
        statsOf[order.seller].paid += order.total;
        statsOf[order.seller].sales++;
        stats.sales++;

        payTo(order.seller, order.total);

        buyersOf[id].push(
            BuyerStruct(
                order.buyer,
                order.total,
                order.qty,
                block.timestamp
            )
        );
        return true;
    }

    function cancelOrder(uint pid, uint id) public returns (bool) {
        require(orderExist[pid][id], "Order not found");
        OrderStruct memory order = ordersOf[pid][id];
        require(order.buyer == msg.sender, "Unauthorized Entity");
        require(order.status != OrderEnum.CANCELED, "Order already canceled");

        order.status = OrderEnum.CANCELED;
        products[order.pid].stock += order.qty;
        ordersOf[pid][id] = order;

        payTo(order.buyer, order.total);
        return true;
    }

    function getOrders() public view returns (OrderStruct[] memory props) {
        props = new OrderStruct[](stats.orders);

        for(uint i=0; i < stats.orders; i++) {
            for(uint j=0; j < ordersOf[i].length; j++) {
                props[i] = ordersOf[i][j];
            }
        }
    }

    function getOrder(uint pid, uint id) public view returns (OrderStruct memory) {
        require(orderExist[pid][id], "Order not found");
        return ordersOf[pid][id];
    }

    function getBuyers(uint pid) public view returns (BuyerStruct[] memory buyers) {
        require(productExist[pid], "Product does not exist");
        return buyersOf[pid];
    }

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

Now, let’s explain what is going on in the smart contract above. We have the following:

  • OrderEnum: This enumerable describes the various status an order goes through in its lifecycle. For example, an order could be placed, delivered, canceled, etc.

  • ProductStruct: This structure model the details of each product to be stored in this smart contract. For example, the SKU, stock, price, and so on.

  • OrderStruct: This structure embodies the details of each order placed in the shop such as the order id, the buyer, the quantity of items, and more.

  • CartStruct: This structure contains the data a cart collects for each item to be submitted as an order in this shop.

  • BuyerStruct: This structure speaks of the kind of data to be collected whenever a buyer purchases a product from our shop.

  • ShopStats: This is a structure that details the statistics of our shop. Information such as the number of sellers, products, orders, and sales are contained by this struct.

For the state variables, we have the following.

  • Owner: This state variable contains the account of the deployer of this smart contract.

  • Stats: This holds information about the current statistics of our shop.

  • Fee: This contains how much to be charged per creation of a product on this platform.

  • Products: This holds a collection of products added to this platform.

  • ProductsOf: This captures the products added by a specific seller to our shop.

  • OrdersOf: This contains a list of orders purchased by a specific buyer in the shop.

  • StatsOf: This holds the statistics of each buyer or seller on the platform.

  • BuyersOf: This accommodates information of the buyers of a specific product.

  • ProductExist: This checks if a product is found in our shop.

  • OrderExist: This checks if an order is found in our shop.

For the functions, we have the following.

  • CreateProduct: This adds a new function to the shop using supplied product information such as the name, description, and price.
  • UpdateProduct: This modifies existing product information with new data supplied via the function’s parameters.
  • UpdateOrderDetails: This function sends a product’s update across every order it has received already.
  • DeleteProduct: This toggles an existing product to a deleted state and becomes unavailable for purchase.
  • GetProduct: This returns the entire list of products in our shop.
  • GetProducts: This returns a specific product from our shop by targeting its Id.
  • CreateOrder: This function cancels an order, it is accessible only by the buyer of such product.
  • TotalCost: This calculates the overall cost for each product ordered.
  • DeliverOrder: This function delivers an order, it is accessible only by the seller of such product.
  • CancelOrder: This function marks an order as canceled and is accessible only to be the buyer of such product.
  • GetOrders: This returns the entire collection of orders placed on this shop.
  • GetOrder: This returns a specific order by its Id.
  • GetBuyers: Returns a collection of buyers of a particular product.
  • PayTo: Sends a specific amount to a specific address when invoked.

If you are new to Solidity, I have a full FREE course on YouTube called, Mastering Solidity Basics. So do check it out, like it, and subscribe!

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 fee = ethers.utils.parseEther('0.002')
  const Contract = await ethers.getContractFactory('Shop')
  const contract = await Contract.deploy(fee)

  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
})

The above script when executed as a Hardhat command will ship the Shop.sol smart contract into any chosen network.

With the above instructions diligently followed, open up a terminal pointing to this project and run the below commands separately on two terminals. VS Code enables you to do this straight from your editor. See the command below.

yarn hardhat node # Terminal #1
yarn hardhat run scripts/deploy.js --network localhost # Terminal #2

If the above commands were successfully executed, you will see these sorts of activities on your terminal. See the image below.

Activities of Deployment on the Terminal

Developing the Frontend

Now that we have our smart contract on a network and all our artifacts (bytecodes and abi) generated, let’s start creating the front end with React step-by-step.

Components
Create a new folder called components in the src directory, which will house all of the React components.

Header component

This component is in charge of displaying information about the currently connected user, the number of items in his cart, and a clickable Identicon that shows more seller options. See the codes responsible for its behavior below.

import Identicon from 'react-identicons'
import { FaEthereum } from 'react-icons/fa'
import { Link, useNavigate } from 'react-router-dom'
import { AiOutlineShoppingCart } from 'react-icons/ai'
import { setGlobalState, truncate, useGlobalState } from '../store'
import { connectWallet } from '../Blockchain.Service'

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

  return (
    <div className="flex justify-between items-center shadow-sm shadow-gray-200 p-5">
      <Link
        to="/"
        className="flex justify-start items-center space-x-1 text-md font-bold"
      >
        <FaEthereum className="cursor-pointer" size={25} />
        <span>GameShop</span>
      </Link>

      <div className="flex justify-end items-center space-x-6">
        <div className="flex justify-center items-center space-x-4">
          <button
            onClick={() => navigate('/cart')}
            className="rounded-full text-gray-500 bg-gray-200 font-semibold text-sm flex 
            align-center cursor-pointer active:bg-gray-300 transition duration-300 
            ease w-max py-1 px-2"
          >
            <AiOutlineShoppingCart className="cursor-pointer" size={25} />
            <span
              className="rounded-full py-[2px] px-[10px] text-center font-bold
            bg-red-600 text-white ml-2"
            >
              {cart.length}
            </span>
          </button>

          <button
            onClick={() => setGlobalState('menu', 'scale-100')}
            className="bg-transparent shadow-sm shadow-gray-400 rounded-full"
          >
            <Identicon
              string={connectedAccount}
              size={25}
              className="h-10 w-10 object-contain rounded-full cursor-pointer"
            />
          </button>
        </div>
        {connectedAccount ? (
          <button
            className="px-6 py-2.5 bg-blue-800 text-white font-medium text-xs 
            leading-tight uppercase rounded shadow-md hover:bg-blue-900 hover:shadow-lg
            focus:bg-blue-900 focus:shadow-lg focus:outline-none focus:ring-0 active:bg-blue-900 
            active:shadow-lg transition duration-150 ease-in-out"
          >
            {truncate(connectedAccount, 4, 4, 11)}
          </button>
        ) : (
          <button
            className="px-6 py-2.5 bg-blue-800 text-white font-medium text-xs 
            leading-tight uppercase rounded shadow-md hover:bg-blue-900 hover:shadow-lg
            focus:bg-blue-900 focus:shadow-lg focus:outline-none focus:ring-0 active:bg-blue-900 
            active:shadow-lg transition duration-150 ease-in-out"
            onClick={connectWallet}
          >
            Connect
          </button>
        )}
      </div>
    </div>
  )
}

export default Header

Banner Component

This component captures a beautiful display of game items. This was designed to give our app a good feel of being a GameShop.

import bannerImg from '../assets/banner.png'

const Banner = () => {
  return (
    <div
      className="flex flex-col lg:flex-row justify-center lg:justify-between 
      items-center lg:space-x-10 md:w-2/3 w-full p-5 mx-auto"
    >
      <img className="mb-5 lg:mb-0" src={bannerImg} alt="banner" />
      <div className="flex flex-col justify-between  items-start lg:items-center text-center lg:text-left">
        <div className="flex flex-col space-y-4 mb-5">
          <h4 className="text-3xl font-bold">Win a Game</h4>
          <p className="text-gray-500">
            Win some money worth upto a game console while surfing our game
            collection, click on the spin button.
          </p>
        </div>
        <div className="flex justify-start text-center items-center space-x-2 mx-auto lg:ml-0">
          <button
            className="px-6 py-2.5 bg-blue-800 text-white font-medium text-xs 
                leading-tight uppercase rounded shadow-md hover:bg-blue-900 hover:shadow-lg
                focus:bg-blue-900 focus:shadow-lg focus:outline-none focus:ring-0 active:bg-blue-900 
                active:shadow-lg transition duration-150 ease-in-out"
          >
            Spin Now
          </button>
        </div>
      </div>
    </div>
  )
}

export default Banner

ShopStats Component

This component records a statistic about the shop’s current state. This section displays the number of products, sellers, orders, and so on. Look at the code that is responsible for this.

import React from 'react'

const ShopStats = ({ stats }) => {
  return (
    <div className="flex flex-col sm:flex-row justify-center items-center p-5">
      <div className="flex flex-col justify-center items-center h-20 border border-gray-200 shadow-md w-full">
        <span className="text-lg font-bold text-black leading-5">
          {stats.products}
        </span>
        <span>Products</span>
      </div>
      <div className="flex flex-col justify-center items-center h-20 border border-gray-200 shadow-md w-full">
        <span className="text-lg font-bold text-black leading-5">
          {stats.sellers}
        </span>
        <span>Sellers</span>
      </div>
      <div className="flex flex-col justify-center items-center h-20 border border-gray-200 shadow-md w-full">
        <span className="text-lg font-bold text-black leading-5">
          {stats.sales}
        </span>
        <span>Sales</span>
      </div>
    </div>
  )
}

export default ShopStats

The Cards Component

This component renders a collection of game products on cards. Each card contains game information such as the name, price, stock, and image URL. See the code snippet below.

import Identicon from 'react-identicons'
import { FaEthereum } from 'react-icons/fa'
import { Link } from 'react-router-dom'
import { truncate } from '../store'

const Cards = ({ products, title, seller }) => {
  return (
    <>
      <div className="flex flex-col items-center space-y-4">
        {seller ? (
          <Identicon
            string={'0adsclsidnt'}
            size={70}
            className="h-10 w-10 object-contain rounded-full cursor-pointer shadow-sm shadow-gray-400"
          />
        ) : null}
        <h4 className="text-center uppercase">{title}</h4>
      </div>

      <div className="flex flex-wrap justify-center items-center space-x-6 md:w-2/3 w-full p-5 mx-auto">
        {products.map((product, i) =>
          product.deleted ? null : <Card product={product} key={i} />,
        )}
      </div>

      <div className="flex justify-center items-center my-5">
        <button
          className="px-6 py-2.5 bg-blue-800 text-white font-medium text-xs 
          leading-tight uppercase rounded shadow-md hover:bg-blue-900 hover:shadow-lg
        focus:bg-blue-900 focus:shadow-lg focus:outline-none focus:ring-0 active:bg-blue-900 
          active:shadow-lg transition duration-150 ease-in-out"
        >
          Load More
        </button>
      </div>
    </>
  )
}

const Card = ({ product }) => (
  <div className="flex flex-col justify-center items-center sm:items-start my-5 w-full sm:w-1/4">
    <Link to={'/product/' + product.id}>
      <img
        className="h-56 w-56 object-cover"
        src={product.imageURL}
        alt={product.name}
      />
      <h4 className="text-lg font-bold">{truncate(product.name, 20, 0, 23)}</h4>
    </Link>

    <div className="flex flex-row sm:flex-col justify-between items-start w-56">
      <div className="flex justify-start items-center">
        <FaEthereum size={15} />
        <span className="font-semibold">{product.price}</span>
      </div>

      <span className="text-sm text-gray-500">{product.stock} in stock</span>
    </div>
  </div>
)

export default Cards

Details Component

This component displays the details of a specific game item such as the full name, image, description, seller details, and so on. Also, this component contains essential buttons for editing, adding items to the cart, deleting, and chatting with the seller button. See the codes below.

import Identicon from 'react-identicons'
import { FaEthereum } from 'react-icons/fa'
import { useNavigate, Link } from 'react-router-dom'
import { setGlobalState, truncate, useGlobalState } from '../store'
import { addToCart } from '../Cart.Service'
import { useEffect, useState } from 'react'
import { getUser } from '../Chat.Service'
import { toast } from 'react-toastify'

const Details = ({ product }) => {
  const navigate = useNavigate()
  const [connectedAccount] = useGlobalState('connectedAccount')
  const [currentUser] = useGlobalState('currentUser')
  const [seller, setSeller] = useState(false)

  const handleChat = () => {
    if (currentUser) {
      if (seller) {
        navigate('/chat/' + product.seller)
      } else {
        toast('Seller not registered for chat yet!')
      }
    } else {
      setGlobalState('chatModal', 'scale-100')
    }
  }

  const handleEdit = () => {
    setGlobalState('product', product)
    setGlobalState('updateModal', 'scale-100')
  }

  const handleDelete = () => {
    setGlobalState('product', product)
    setGlobalState('deleteModal', 'scale-100')
  }

  useEffect(async () => {
    await getUser(product.seller).then((user) => {
      if (user.name) setSeller(user.uid == product.seller)
    })
  }, [])

  return (
    <div
      className="flex flex-col lg:flex-row justify-center lg:justify-between 
      items-center lg:space-x-10 md:w-2/3 w-full p-5 mx-auto"
    >
      <img
        className="h-56 w-56 object-cover mb-5 lg:mb-0"
        src={product.imageURL}
        alt={product.name}
      />
      <div className="flex flex-col justify-between  items-start lg:items-center text-center lg:text-left">
        <div className="flex flex-col space-y-4 mb-5">
          <h4 className="text-3xl font-bold">{product.name}</h4>
          <p className="text-gray-500">{product.description}</p>

          <div className="flex justify-center lg:justify-between space-x-2 items-center">
            <Link
              to={'/seller/0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266'}
              className="flex justify-start items-center space-x-2"
            >
              <Identicon
                string={product.seller}
                size={25}
                className="h-10 w-10 object-contain rounded-full cursor-pointer"
              />
              <small className="font-bold">
                {truncate(product.seller, 4, 4, 11)}
              </small>
            </Link>

            <span className="text-sm text-gray-500">
              {product.stock} in stock
            </span>
          </div>
        </div>

        <div className="flex justify-start text-center items-center flex-wrap space-x-1 mx-auto lg:ml-0">
          {product.deleted ? null : connectedAccount == product.seller ? (
            <div className="flex justify-start text-center items-center space-x-1">
              <button
                className="px-6 py-2.5 bg-blue-800 text-white font-medium text-xs 
                  leading-tight uppercase rounded shadow-md hover:bg-blue-900 hover:shadow-lg
                  focus:bg-blue-900 focus:shadow-lg focus:outline-none focus:ring-0 active:bg-blue-900 
                  active:shadow-lg transition duration-150 ease-in-out flex justify-start items-center space-x-2"
                onClick={handleEdit}
              >
                <span>Edit Product</span>
              </button>

              <button
                className="px-6 py-2.5 bg-red-800 text-white font-medium text-xs 
                  leading-tight uppercase rounded shadow-md hover:bg-red-900 hover:shadow-lg
                  focus:bg-red-900 focus:shadow-lg focus:outline-none focus:ring-0 active:bg-red-900 
                  active:shadow-lg transition duration-150 ease-in-out flex justify-start items-center space-x-2"
                onClick={handleDelete}
              >
                <span>Delete Product</span>
              </button>
            </div>
          ) : (
            <button
              className="px-6 py-2.5 bg-blue-800 text-white font-medium text-xs 
              leading-tight uppercase rounded shadow-md hover:bg-blue-900 hover:shadow-lg
              focus:bg-blue-900 focus:shadow-lg focus:outline-none focus:ring-0 active:bg-blue-900 
              active:shadow-lg transition duration-150 ease-in-out flex justify-start items-center space-x-2"
              onClick={() => addToCart(product)}
            >
              <span>Add to Cart</span>

              <div className="flex justify-start items-center">
                <FaEthereum size={15} />
                <span className="font-semibold">{product.price}</span>
              </div>
            </button>
          )}
          <button
            className="px-6 py-2.5 bg-transparent border-blue-800 text-blue-800 font-medium text-xs 
            leading-tight uppercase rounded shadow-md hover:bg-blue-900 hover:shadow-lg border
            focus:border-blue-900 focus:shadow-lg focus:outline-none focus:ring-0 active:border-blue-900 
            active:shadow-lg transition duration-150 ease-in-out hover:text-white"
            onClick={handleChat}
          >
            Chat with Seller
          </button>
        </div>
      </div>
    </div>
  )
}

export default Details

Buyers Component

Buyers of a specific game product

This component shows a list of buyers who bought a specific game item. See the codes listed below.

import { FaEthereum } from 'react-icons/fa'
import Identicon from 'react-identicons'
import { truncate } from '../store'

const Buyers = ({ buyers }) => {
  return (
    <div className="flex justify-center flex-col items-start w-full md:w-2/3 p-5 mx-auto">
      <div className="max-h-[calc(100vh_-_20rem)] overflow-y-auto shadow-md rounded-md w-full">
        {buyers.length < 1 ? null : (
          <table className="min-w-full">
            <thead className="border-b">
              <tr>
                <th
                  scope="col"
                  className="text-sm font-medium px-6 py-4 text-left"
                >
                  Buyer
                </th>
                <th
                  scope="col"
                  className="text-sm font-medium px-6 py-4 text-left"
                >
                  Cost
                </th>
                <th
                  scope="col"
                  className="text-sm font-medium px-6 py-4 text-left"
                >
                  Qty
                </th>
                <th
                  scope="col"
                  className="text-sm font-medium px-6 py-4 text-left"
                >
                  Date
                </th>
              </tr>
            </thead>
            <tbody>
              {buyers.map((buyer, i) => (
                <tr
                  key={i}
                  className="border-b border-gray-200 transition duration-300 ease-in-out"
                >
                  <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
                    <div className="flex flex-row justify-start items-center space-x-3">
                      <Identicon
                        string={buyer.buyer}
                        size={25}
                        className="h-10 w-10 object-contain rounded-full mr-3"
                      />
                      <small className="font-bold">
                        {truncate(buyer.buyer, 4, 4, 11)}
                      </small>
                    </div>
                  </td>
                  <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
                    <small className="flex justify-start items-center space-x-1">
                      <FaEthereum />
                      <span className="text-gray-700 font-bold">
                        {buyer.price} EHT
                      </span>
                    </small>
                  </td>

                  <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
                    <span className="text-gray-700 font-bold">{buyer.qty}</span>
                  </td>
                  <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
                    {buyer.timestamp}
                  </td>
                </tr>
              ))}
            </tbody>
          </table>
        )}
      </div>
    </div>
  )
}

export default Buyers

Orders Component

This component renders a collection of orders for both the buyer and the seller, giving the buyer the ability to cancel an order so long as it isn’t delivered, and the seller, the ability to deliver a game product. See the codes below.

import { Link } from 'react-router-dom'
import { FaEthereum } from 'react-icons/fa'
import { cancelOrder, delieverOrder } from '../Blockchain.Service'
import { useGlobalState } from '../store'
import { toast } from 'react-toastify'

const DELEVIRED = 1
const CANCELED = 2

const onDeliver = async (pid, id) => {
  await toast.promise(
    new Promise(async (resolve, reject) => {
      await delieverOrder(pid, id)
        .then(() => resolve())
        .catch(() => reject())
    }),
    {
      pending: 'Approve transaction...',
      success:
        'Order delivered, will reflect in your Order history within 30sec 🙌',
      error: 'Encountered error placing order 🤯',
    },
  )
}

const onCancel = async (pid, id) => {
  await toast.promise(
    new Promise(async (resolve, reject) => {
      await cancelOrder(pid, id)
        .then(() => resolve())
        .catch(() => reject())
    }),
    {
      pending: 'Approve transaction...',
      success:
        'Order delivered, will reflect in your Order history within 30sec 🙌',
      error: 'Encountered error placing order 🤯',
    },
  )
}

const Order = ({ orders, title, seller }) => {
  const [connectedAccount] = useGlobalState('connectedAccount')

  return (
    <div className="flex flex-col justify-between items-center space-x-2 md:w-2/3 w-full p-5 mx-auto">
      <h4 className="text-center uppercase mb-8">{title}</h4>

      <table className="min-w-full hidden md:table">
        <thead className="border-b">
          <tr>
            <th scope="col" className="text-sm font-medium px-6 py-4 text-left">
              S/N
            </th>
            <th scope="col" className="text-sm font-medium px-6 py-4 text-left">
              Product
            </th>
            <th scope="col" className="text-sm font-medium px-6 py-4 text-left">
              Qty
            </th>
            <th scope="col" className="text-sm font-medium px-6 py-4 text-left">
              Price
            </th>
            <th scope="col" className="text-sm font-medium px-6 py-4 text-left">
              Status
            </th>
            <th scope="col" className="text-sm font-medium px-6 py-4 text-left">
              Total
            </th>
          </tr>
        </thead>
        <tbody>
          {seller
            ? orders.map((order, i) =>
                order.seller == connectedAccount ? (
                  <SellerOrder key={i} order={order} i={i} />
                ) : null,
              )
            : orders.map((order, i) =>
                order.buyer == connectedAccount ? (
                  <BuyerOrder key={i} order={order} i={i} />
                ) : null,
              )}
        </tbody>
      </table>

      <div className="flex flex-col justify-center items-center w-full md:hidden">
        {seller
          ? orders.map((order, i) =>
              order.seller == connectedAccount ? (
                <MobileSellerOrder key={i} order={order} i={i} />
              ) : null,
            )
          : orders.map((order, i) =>
              order.buyer == connectedAccount ? (
                <MobileBuyerOrder key={i} order={order} i={i} />
              ) : null,
            )}
      </div>
    </div>
  )
}

const SellerOrder = ({ order, i }) => (
  <tr className="border-b border-gray-200 transition duration-300 ease-in-out">
    <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
      <span className="text-gray-700 font-bold">{i + 1}</span>
    </td>

    <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
      <Link to={'/product/' + order.pid}>
        <img className="w-20" src={order.imageURL} alt="game" />
        <small className="font-bold">{order.name}</small>
      </Link>
    </td>

    <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
      <span className="text-gray-700 font-bold">{order.qty}</span>
    </td>

    <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
      <small className="flex justify-start items-center space-x-1">
        <FaEthereum />
        <span className="text-gray-700 font-bold">
          {(order.total / order.qty).toFixed(3)} EHT
        </span>
      </small>
    </td>

    {order.status == DELEVIRED ? (
      <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
        <span className="text-green-500">Delievered</span>
      </td>
    ) : order.status == CANCELED ? (
      <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
        <span className="text-red-500">Canceled</span>
      </td>
    ) : (
      <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
        <button
          type="button"
          className="rounded inline-block px-4 py-1.5 bg-green-600 text-white
                font-medium text-xs leading-tight uppercase hover:bg-green-700 
                focus:bg-green-700 focus:outline-none focus:ring-0 active:bg-green-800
                transition duration-150 ease-in-out"
          onClick={() => onDeliver(order.pid, order.id)}
        >
          Deliever
        </button>
      </td>
    )}

    <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
      <small className="flex justify-start items-center space-x-1">
        <FaEthereum />
        <span className="text-gray-700 font-bold">{order.total} EHT</span>
      </small>
    </td>
  </tr>
)

const BuyerOrder = ({ order, i }) => (
  <tr className="border-b border-gray-200 transition duration-300 ease-in-out">
    <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
      <span className="text-gray-700 font-bold">{i + 1}</span>
    </td>

    <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
      <Link to={'/product/' + order.pid}>
        <img className="w-20" src={order.imageURL} alt="game" />
        <small className="font-bold">{order.name}</small>
      </Link>
    </td>

    <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
      <span className="text-gray-700 font-bold">{order.qty}</span>
    </td>

    <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
      <small className="flex justify-start items-center space-x-1">
        <FaEthereum />
        <span className="text-gray-700 font-bold">
          {(order.total / order.qty).toFixed(3)} EHT
        </span>
      </small>
    </td>

    {order.status == DELEVIRED ? (
      <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
        <span className="text-green-500">Delievered</span>
      </td>
    ) : order.status == CANCELED ? (
      <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
        <span className="text-red-500">Canceled</span>
      </td>
    ) : (
      <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
        <button
          type="button"
          className="rounded inline-block px-4 py-1.5 bg-blue-600 text-white
              font-medium text-xs leading-tight uppercase hover:bg-blue-700 
              focus:bg-blue-700 focus:outline-none focus:ring-0 active:bg-blue-800
              transition duration-150 ease-in-out"
          onClick={() => onCancel(order.pid, order.id)}
        >
          Cancel
        </button>
      </td>
    )}
    <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
      <small className="flex justify-start items-center space-x-1">
        <FaEthereum />
        <span className="text-gray-700 font-bold">{order.total} EHT</span>
      </small>
    </td>
  </tr>
)

const MobileSellerOrder = ({ order, i }) => (
  <div
    className="flex flex-col justify-center items-center  my-4
    transition duration-300 ease-in-out border-b border-gray-200"
  >
    <div className="flex justify-center">
      <span className="text-gray-700 font-bold text-sm">#{i + 1}</span>
    </div>

    <Link
      to={'/product/' + order.pid}
      className="flex flex-col justify-center items-center space-y-2 text-sm font-light"
    >
      <img className="w-1/3 md:w-2/3" src={order.imageURL} alt="game" />
      <small className="font-bold">{order.name}</small>
    </Link>

    <div className="text-sm font-light">
      <small className="flex justify-start items-center space-x-1">
        <FaEthereum />
        <span className="text-gray-700 font-bold">
          {order.qty} x {order.total / order.qty} EHT = {order.total} EHT
        </span>
      </small>
    </div>

    {order.status == DELEVIRED ? (
      <div className="text-sm font-light mt-2 mb-4">
        <span
          className="px-4 py-2 rounded-full text-green-500 bg-green-200 font-semibold
          text-sm flex align-center w-max cursor-pointer active:bg-gray-300
          transition duration-300 ease"
        >
          Delievered
        </span>
      </div>
    ) : order.status == CANCELED ? (
      <div className="text-sm font-light mt-2 mb-4">
        <span
          className="px-4 py-2 rounded-full text-red-500 bg-red-200 font-semibold
          text-sm flex align-center w-max cursor-pointer active:bg-gray-300
          transition duration-300 ease"
        >
          Canceled
        </span>
      </div>
    ) : (
      <div className="text-sm font-light mt-2 mb-4">
        <button
          type="button"
          className="rounded inline-block px-4 py-1.5 bg-green-600 text-white
                font-medium text-xs leading-tight uppercase hover:bg-green-700 
                focus:bg-green-700 focus:outline-none focus:ring-0 active:bg-green-800
                transition duration-150 ease-in-out"
          onClick={() => onDeliver(order.pid, order.id)}
        >
          Deliever
        </button>
      </div>
    )}
  </div>
)

const MobileBuyerOrder = ({ order, i }) => (
  <div
    className="flex flex-col justify-center items-center  my-4
    transition duration-300 ease-in-out border-b border-gray-200"
  >
    <div className="flex justify-center">
      <span className="text-gray-700 font-bold text-sm">#{i + 1}</span>
    </div>

    <Link
      to={'/product/' + order.pid}
      className="flex flex-col justify-center items-center space-y-2 text-sm font-light"
    >
      <img className="w-3/5" src={order.imageURL} alt="game" />
      <small className="font-bold">{order.name}</small>
    </Link>

    <div className="text-sm font-light">
      <small className="flex justify-start items-center space-x-1">
        <FaEthereum />
        <span className="text-gray-700 font-bold">
          {order.qty} x {order.total / order.qty} EHT = {order.total} EHT
        </span>
      </small>
    </div>

    {order.status == DELEVIRED ? (
      <div className="text-sm font-light mt-2 mb-4">
        <span
          className="px-4 py-2 rounded-full text-green-500 bg-green-200 font-semibold
          text-sm flex align-center w-max cursor-pointer active:bg-gray-300
          transition duration-300 ease"
        >
          Delievered
        </span>
      </div>
    ) : order.status == CANCELED ? (
      <div className="text-sm font-light mt-2 mb-4">
        <span
          className="px-4 py-2 rounded-full text-red-500 bg-red-200 font-semibold
          text-sm flex align-center w-max cursor-pointer active:bg-gray-300
          transition duration-300 ease"
        >
          Canceled
        </span>
      </div>
    ) : (
      <div className="text-sm font-light mt-2 mb-4">
        <button
          type="button"
          className="rounded inline-block px-4 py-1.5 bg-green-600 text-white
                font-medium text-xs leading-tight uppercase hover:bg-green-700 
                focus:bg-green-700 focus:outline-none focus:ring-0 active:bg-green-800
                transition duration-150 ease-in-out"
          onClick={() => onCancel(order.pid, order.id)}
        >
          Cancel
        </button>
      </div>
    )}
  </div>
)

export default Order

Adding a Game to The Shop

To add a new game to our shop we use two components, the “AddButton” and the “CreateProduct” component. The “AddButton” is responsible for launching the create product modal. Create each one of these components in the components folder and paste the following codes inside them. see the codes below.

import { BsPlusLg } from 'react-icons/bs'
import { setGlobalState } from '../store'

const AddButton = () => {
  return (
    <div className="fixed right-10 bottom-10 flex space-x-2 justify-center">
      <div>
        <button
          type="button"
          className="flex justify-center items-center rounded-full bg-blue-600
          text-white leading-normal uppercase shadow-md hover:bg-blue-700
          hover:shadow-lg focus:bg-blue-700 focus:shadow-lg focus:outline-none
          focus:ring-0 active:bg-blue-800 active:shadow-lg transition
          duration-150 ease-in-out w-9 h-9"
          onClick={() => setGlobalState('modal', 'scale-100')}
        >
          <BsPlusLg className="font-bold" size={20} />
        </button>
      </div>
    </div>
  )
}

export default AddButton
import { useEffect, useState } from 'react'
import { FaTimes } from 'react-icons/fa'
import { createProduct } from '../Blockchain.Service'
import { setGlobalState, useGlobalState } from '../store'
import { toast } from 'react-toastify'
import { getUser } from '../Chat.Service'

const CreateProduct = () => {
  const [modal] = useGlobalState('modal')
  const [connectedAccount] = useGlobalState('connectedAccount')
  const [name, setName] = useState('')
  const [price, setPrice] = useState('')
  const [stock, setStock] = useState('')
  const [description, setDescription] = useState('')
  const [imageURL, setImageURL] = useState('')
  const [seller, setSeller] = useState(false)

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

    if (!name || !price || !imageURL || !description || !stock) return
    const params = {
      sku: (Math.random() + 1).toString(36).substring(7).toUpperCase(),
      name,
      description,
      stock,
      price,
      imageURL,
    }

    await toast.promise(
      new Promise(async (resolve, reject) => {
        await createProduct(params)
          .then(() => resolve())
          .catch(() => reject())
      }),
      {
        pending: 'Approve transaction to product...',
        success: 'Product successfully created, will reflect within 30sec 👌',
        error: 'Encountered error updating your product 🤯',
      },
    )

    closeModal()

    if(!seller) toast("Please sign in to have your customers chat with you.")
  }

  useEffect(async () => {
    await getUser(connectedAccount).then((user) => {
      if (user.name) setSeller(user.uid == connectedAccount)
    })
  }, [])

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

  const resetForm = () => {
    setImageURL('')
    setName('')
    setPrice('')
    setStock('')
    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-white shadow-xl shadow-black 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-black">Add Product</p>
            <button
              type="button"
              onClick={closeModal}
              className="border-0 bg-transparent focus:outline-none"
            >
              <FaTimes className="text-black" />
            </button>
          </div>

          {imageURL ? (
            <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="Project"
                  className="h-full w-full object-cover cursor-pointer"
                  src={imageURL}
                />
              </div>
            </div>
          ) : null}

          <div className="flex flex-row justify-between items-center bg-gray-300 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="name"
              placeholder="Title"
              onChange={(e) => setName(e.target.value)}
              value={name}
              required
            />
          </div>

          <div className="flex flex-row justify-between items-center bg-gray-300 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.001}
              min={0.001}
              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-300 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"
              min={1}
              name="stock"
              placeholder="E.g. 2"
              onChange={(e) => setStock(e.target.value)}
              value={stock}
              required
            />
          </div>

          <div className="flex flex-row justify-between items-center bg-gray-300 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="url"
              name="imageURL"
              placeholder="ImageURL"
              onChange={(e) => setImageURL(e.target.value)}
              pattern="^(http(s)?:\/\/)+[\w\-\._~:\/?#[\]@!\$&'\(\)\*\+,;=.]+$"
              value={imageURL}
              required
            />
          </div>

          <div className="flex flex-row justify-between items-center bg-gray-300 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"
            className="flex flex-row justify-center items-center
              w-full text-white text-md bg-blue-500
              py-2 px-5 rounded-full drop-shadow-xl
              border-transparent border
              hover:bg-transparent hover:text-blue-500
              hover:border hover:border-blue-500
              focus:outline-none focus:ring mt-5"
          >
            Create Product
          </button>
        </form>
      </div>
    </div>
  )
}

export default CreateProduct

The Administrative Components

Launched by the Edit Product Button

This component includes the edit, delete, and chat with seller components. The ability to edit or delete a product is solely the responsibility of the owner of such a product.

Launched by the Delete Button

For the chat with seller button, both the seller and the buyer must willfully sign up for this service to be able to receive anonymous chats from the buyers. Then he will be able to see them in his chat history.

Launched by the Chat with Seller Button

The logic for each of these components is contained in the codes below; create and paste the codes into their respective components.

import { FaTimes } from 'react-icons/fa'
import { setGlobalState, useGlobalState } from '../store'
import { loginWithCometChat, signUpWithCometChat } from '../Chat.Service'
import { toast } from 'react-toastify'

const ChatModal = () => {
  const [chatModal] = useGlobalState('chatModal')
  const [connectedAccount] = useGlobalState('connectedAccount')

  const handleLogin = async () => {
    await toast.promise(
      new Promise(async (resolve, reject) => {
        await loginWithCometChat(connectedAccount)
          .then((res) => res == true ? resolve() : reject())
          .catch(() => reject())
      }),
      {
        pending: 'Signing in...',
        success: 'Successfully signed in 👌',
        error: 'Encountered error while signing in 🤯',
      },
    )

    closeModal()
  }

  const handleSignup = async () => {
    await toast.promise(
      new Promise(async (resolve, reject) => {
        await signUpWithCometChat(connectedAccount, connectedAccount)
          .then((res) => res == true ? resolve() : reject())
          .catch(() => reject())
      }),
      {
        pending: 'Signing up...',
        success: 'Successfully signed up, proceed to login... 👌',
        error: 'Encountered error while signing up 🤯',
      },
    )

    closeModal()
  }

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

  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 ${chatModal}`}
    >
      <div className="bg-white shadow-xl shadow-black 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-end items-center">
            <button
              type="button"
              onClick={closeModal}
              className="border-0 bg-transparent focus:outline-none"
            >
              <FaTimes className="text-black" />
            </button>
          </div>
          <ChatAuth login={handleLogin} sign={handleSignup} />
        </div>
      </div>
    </div>
  )
}

const ChatAuth = ({ login, sign }) => (
  <>
    <div className="flex flex-col justify-center items-center text-center">
      <h4 className="text-xl text-bold mb-3">Authentication</h4>
      <p>
        You will have to sign up or login to access the chat features of this
        app.
      </p>
    </div>

    <div className="flex justify-center items-center space-x-3 text-center mt-5">
      <button
        type="submit"
        onClick={login}
        className="flex flex-row justify-center items-center w-full 
              text-white text-md bg-blue-900
              py-2 px-5 rounded-full drop-shadow-xl
              border-transparent border
              hover:bg-transparent hover:text-blue-900
              hover:border hover:border-blue-900
              focus:outline-none focus:ring mt-5"
      >
        Login
      </button>

      <button
        type="submit"
        onClick={sign}
        className="flex flex-row justify-center items-center w-full 
              text-blue-900 text-md border-blue-900
              py-2 px-5 rounded-full drop-shadow-xl
              border-transparent border
              hover:text-white
              hover:border hover:bg-blue-900
              focus:outline-none focus:ring mt-5"
      >
        Sign Up
      </button>
    </div>
  </>
)

export default ChatModal
import { FaTimes } from 'react-icons/fa'
import { setGlobalState, useGlobalState } from '../store'
import { deleteProduct } from '../Blockchain.Service'
import { toast } from 'react-toastify'

const DeleteProduct = () => {
  const [deleteModal] = useGlobalState('deleteModal')
  const [product] = useGlobalState('product')

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

    await toast.promise(
      new Promise(async (resolve, reject) => {
        await deleteProduct(product?.id)
          .then(() => resolve())
          .catch(() => reject())
      }),
      {
        pending: 'Approving transaction...',
        success: 'Product deleted, will reflect within 30sec 👌',
        error: 'Encountered error deleting your product 🤯',
      },
    )

    closeModal()
  }

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

  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 ${deleteModal}`}
    >
      <div className="bg-white shadow-xl shadow-black 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-end items-center">
            <button
              type="button"
              onClick={closeModal}
              className="border-0 bg-transparent focus:outline-none"
            >
              <FaTimes className="text-black" />
            </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="Product"
                className="h-full w-full object-cover cursor-pointer"
                src={product?.imageURL}
              />
            </div>
          </div>

          <div className="flex flex-col justify-center items-center  text-center mt-5">
            <p>
              You are about to delete <strong>"{product?.name}"</strong>{' '}
              permanently!
            </p>
            <small className="text-red-400">Are you sure?</small>
          </div>

          <button
            type="submit"
            onClick={handleDelete}
            className="flex flex-row justify-center items-center w-full 
              text-white text-md bg-red-500
              py-2 px-5 rounded-full drop-shadow-xl
              border-transparent border
              hover:bg-transparent hover:text-red-500
              hover:border hover:border-red-500
              focus:outline-none focus:ring mt-5"
          >
            Delete Product
          </button>
        </div>
      </div>
    </div>
  )
}

export default DeleteProduct
import { useEffect, useState } from 'react'
import { FaTimes } from 'react-icons/fa'
import { updateProduct } from '../Blockchain.Service'
import { setGlobalState, useGlobalState } from '../store'
import { toast } from 'react-toastify'

const UpateProduct = () => {
  const [modal] = useGlobalState('updateModal')
  const [product] = useGlobalState('product')
  const [name, setName] = useState(product?.name)
  const [price, setPrice] = useState(product?.price)
  const [stock, setStock] = useState(product?.stock)
  const [oldStock, setOldStock] = useState(product?.stock)
  const [description, setDescription] = useState(product?.description)
  const [imageURL, setImageURL] = useState(product?.imageURL)

  useEffect(() => {
    setName(product?.name)
    setDescription(product?.description)
    setPrice(product?.price)
    setStock(product?.stock)
    setImageURL(product?.imageURL)
  }, [product])

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

    if (
      !name ||
      !price ||
      !imageURL ||
      !description ||
      !stock ||
      stock < oldStock
    )
      return
    const params = {
      id: product.id,
      name,
      description,
      stock,
      price,
      imageURL,
    }

    await toast.promise(
      new Promise(async (resolve, reject) => {
        await updateProduct(params)
          .then(() => resolve())
          .catch(() => reject())
      }),
      {
        pending: 'Approve transaction to product...',
        success: 'Product successfully updated, will reflect within 30sec 🦄',
        error: 'Encountered error updating your product 🤯',
      },
    )

    closeModal()
    console.log('Product updated')
  }

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

  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 z-50 ${modal}`}
    >
      <div className="bg-white shadow-xl shadow-black 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-black">Edit Product</p>
            <button
              type="button"
              onClick={closeModal}
              className="border-0 bg-transparent focus:outline-none"
            >
              <FaTimes className="text-black" />
            </button>
          </div>

          {imageURL ? (
            <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="Project"
                  className="h-full w-full object-cover cursor-pointer"
                  src={imageURL}
                />
              </div>
            </div>
          ) : null}

          <div className="flex flex-row justify-between items-center bg-gray-300 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="name"
              placeholder="Title"
              onChange={(e) => setName(e.target.value)}
              value={name || ''}
              required
            />
          </div>

          <div className="flex flex-row justify-between items-center bg-gray-300 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.001}
              min={0.001}
              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-300 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"
              min={1}
              name="stock"
              placeholder="E.g. 2"
              onChange={(e) => setStock(e.target.value)}
              value={stock || ''}
              required
            />
          </div>

          <div className="flex flex-row justify-between items-center bg-gray-300 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="url"
              name="imageURL"
              placeholder="ImageURL"
              onChange={(e) => setImageURL(e.target.value)}
              pattern="^(http(s)?:\/\/)+[\w\-\._~:\/?#[\]@!\$&'\(\)\*\+,;=.]+$"
              value={imageURL || ''}
              required
            />
          </div>

          <div className="flex flex-row justify-between items-center bg-gray-300 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"
            className="flex flex-row justify-center items-center
              w-full text-white text-md bg-blue-500
              py-2 px-5 rounded-full drop-shadow-xl
              border-transparent border
              hover:bg-transparent hover:text-blue-500
              hover:border hover:border-blue-500
              focus:outline-none focus:ring mt-5"
          >
            Update Product
          </button>
        </form>
      </div>
    </div>
  )
}

export default UpateProduct

The Menu Component

This component is in charge of directing users to other areas of the application, such as your order and sales history, recent customer chats, and statistics. See the code for the component below.

import { FaTimes } from 'react-icons/fa'
import { useNavigate } from 'react-router-dom'
import { setGlobalState, useGlobalState } from '../store'

const Menu = () => {
  const [menu] = useGlobalState('menu')
  const navigate = useNavigate()

  const navTo = (route) => {
    setGlobalState('menu', 'scale-0')
    navigate(route)
  }

  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 ${menu}`}
    >
      <div className="bg-white shadow-xl shadow-black 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-black">Account</p>
            <button
              type="button"
              className="border-0 bg-transparent focus:outline-none"
              onClick={() => setGlobalState('menu', 'scale-0')}
            >
              <FaTimes className="text-black" />
            </button>
          </div>

          <div className="flex justify-start mt-4">
            <button
              type="button"
              data-mdb-ripple="true"
              data-mdb-ripple-color="light"
              className="px-6 py-2.5 bg-white text-black font-medium text-xs leading-tight
                uppercase rounded shadow-md hover:bg-blue-700 hover:shadow-lg focus:bg-blue-700
                focus:shadow-lg focus:outline-none focus:ring-0 active:bg-blue-800 hover:text-white
                active:shadow-lg transition duration-150 ease-in-out w-full text-left"
              onClick={() => navTo('/orders')}
            >
              Order History
            </button>
          </div>
          
          <div className="flex justify-start mt-4">
            <button
              type="button"
              data-mdb-ripple="true"
              data-mdb-ripple-color="light"
              className="px-6 py-2.5 bg-white text-black font-medium text-xs leading-tight
                uppercase rounded shadow-md hover:bg-blue-700 hover:shadow-lg focus:bg-blue-700
                focus:shadow-lg focus:outline-none focus:ring-0 active:bg-blue-800 hover:text-white
                active:shadow-lg transition duration-150 ease-in-out w-full text-left"
              onClick={() => navTo('/sales')}
            >
              Sales History
            </button>
          </div>

          <div className="flex justify-start mt-4">
            <button
              type="button"
              data-mdb-ripple="true"
              data-mdb-ripple-color="light"
              className="px-6 py-2.5 bg-white text-black font-medium text-xs leading-tight
                uppercase rounded shadow-md hover:bg-blue-700 hover:shadow-lg focus:bg-blue-700
                focus:shadow-lg focus:outline-none focus:ring-0 active:bg-blue-800 hover:text-white
                active:shadow-lg transition duration-150 ease-in-out w-full text-left"
              onClick={() => navTo('/recents')}
            >
              Recent Chats
            </button>
          </div>

          <div className="flex justify-start mt-4">
            <button
              type="button"
              data-mdb-ripple="true"
              data-mdb-ripple-color="light"
              className="px-6 py-2.5 bg-white text-black font-medium text-xs leading-tight
                uppercase rounded shadow-md hover:bg-blue-700 hover:shadow-lg focus:bg-blue-700
                focus:shadow-lg focus:outline-none focus:ring-0 active:bg-blue-800 hover:text-white
                active:shadow-lg transition duration-150 ease-in-out w-full text-left"
            onClick={() => navTo('/seller/0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266')}
            >
              My Products
            </button>
          </div>

          <div className="flex justify-start mt-4">
            <button
              type="button"
              data-mdb-ripple="true"
              data-mdb-ripple-color="light"
              className="px-6 py-2.5 bg-white text-black font-medium text-xs leading-tight
                uppercase rounded shadow-md hover:bg-blue-700 hover:shadow-lg focus:bg-blue-700
                focus:shadow-lg focus:outline-none focus:ring-0 active:bg-blue-800 hover:text-white
                active:shadow-lg transition duration-150 ease-in-out w-full text-left"
              onClick={() => navTo('/stats/0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266')}
            >
              My Stats
            </button>
          </div>
        </div>
      </div>
    </div>
  )
}

export default Menu

The Shopping Cart Components

The cart component has a highly responsive design as well as an instant price calibrator. See the codes listed below.

import { useEffect, useState } from 'react'
import { FaEthereum } from 'react-icons/fa'
import { Link } from 'react-router-dom'
import { remFromCart, updateCart } from '../Cart.Service'
import Summary from './Summary'

const Cart = ({ cart, summary }) => {
  const [cartItems, setCartItems] = useState([])
  const [process, setProcess] = useState(false)

  const increase = (product) => {
    product.qty++
    updateCart(product)
    setCartItems(cart)
    setProcess(!process)
  }

  const decrease = (product) => {
    if (product.qty == 1) {
      remFromCart(product)
    } else {
      product.qty--
      updateCart(product)
    }
    setCartItems(cart)
    setProcess(!process)
  }

  useEffect(() => {
    setCartItems(cart)
  }, [process])

  return (
    <>
      <div className="flex flex-col justify-between items-center space-x-2 md:w-2/3 w-full p-5 mx-auto">
        <h4 className="text-center uppercase mb-8">Shopping Cart</h4>

        <table className="min-w-full hidden md:table">
          <thead className="border-b">
            <tr>
              <th
                scope="col"
                className="text-sm font-medium px-6 py-4 text-left"
              >
                S/N
              </th>
              <th
                scope="col"
                className="text-sm font-medium px-6 py-4 text-left"
              >
                Product
              </th>
              <th
                scope="col"
                className="text-sm font-medium px-6 py-4 text-left"
              >
                Qty
              </th>
              <th
                scope="col"
                className="text-sm font-medium px-6 py-4 text-left"
              >
                Price
              </th>
              <th
                scope="col"
                className="text-sm font-medium px-6 py-4 text-left"
              >
                Action
              </th>
              <th
                scope="col"
                className="text-sm font-medium px-6 py-4 text-left"
              >
                Total
              </th>
            </tr>
          </thead>

          <tbody>
            {cartItems.map((product, i) => (
              <tr
                key={i}
                className="border-b border-gray-200 transition duration-300 ease-in-out"
              >
                <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
                  <span className="text-gray-700 font-bold">{i + 1}</span>
                </td>

                <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
                  <Link to={'/product/' + product.id}>
                    <img className="w-20" src={product.imageURL} alt="game" />
                    <small className="font-bold">{product.name}</small>
                  </Link>
                </td>

                <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
                  <div
                    className="inline-flex shadow-md hover:shadow-lg focus:shadow-lg"
                    role="group"
                  >
                    <button
                      type="button"
                      className="rounded-l inline-block px-4 py-1.5 bg-blue-600 text-white font-medium
                      text-xs leading-tight uppercase hover:bg-blue-700 focus:bg-blue-700 focus:outline-none
                      focus:ring-0 active:bg-blue-800 transition duration-150 ease-in-out"
                      onClick={() => decrease(product)}
                    >
                      -
                    </button>
                    <button
                      type="button"
                      className=" inline-block px-4 py-1.5 bg-transparent text-black font-medium
                      text-xs leading-tight uppercase focus:outline-none
                      focus:ring-0 transition duration-150 ease-in-out"
                    >
                      {product.qty}
                    </button>
                    <button
                      type="button"
                      className=" rounded-r inline-block px-4 py-1.5 bg-blue-600 text-white font-medium
                      text-xs leading-tight uppercase hover:bg-blue-700 focus:bg-blue-700 focus:outline-none
                      focus:ring-0 active:bg-blue-800 transition duration-150 ease-in-out"
                      onClick={() => increase(product)}
                    >
                      +
                    </button>
                  </div>
                </td>

                <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
                  <small className="flex justify-start items-center space-x-1">
                    <FaEthereum />
                    <span className="text-gray-700 font-bold">
                      {product.price} EHT
                    </span>
                  </small>
                </td>

                <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
                  <button
                    type="button"
                    className="inline-block px-6 py-2.5 bg-transparent text-red-600 font-medium
                text-xs leading-tight uppercase rounded hover:text-red-700
                hover:bg-gray-100 focus:bg-gray-100 focus:outline-none focus:ring-0
                active:bg-gray-200 transition duration-150 ease-in-out"
                    onClick={() => remFromCart(product)}
                  >
                    Remove
                  </button>
                </td>
                <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
                  <small className="flex justify-start items-center space-x-1">
                    <FaEthereum />
                    <span className="text-gray-700 font-bold">
                      {(product.qty * product.price).toFixed(3)} EHT
                    </span>
                  </small>
                </td>
              </tr>
            ))}
          </tbody>
        </table>

        <div className="flex flex-col justify-center items-center space-y-2 w-full md:hidden">
          {cartItems.map((product, i) => (
            <div
              key={i}
              className="flex flex-col justify-center items-center my-4 space-y-2
              border-b border-gray-200 transition duration-300 ease-in-out"
            >
              <Link
                to={'/product/' + product.id}
                className="flex flex-col justify-center items-center space-y-2 text-sm font-light"
              >
                <img
                  className="w-1/3 md:w-2/3"
                  src={product.imageURL}
                  alt="game"
                />
                <small className="font-bold">{product.name}</small>
              </Link>

              <div className="flex justify-center">
                <div
                  className="inline-flex shadow-md hover:shadow-lg focus:shadow-lg"
                  role="group"
                >
                  <button
                    type="button"
                    className="rounded-l inline-block px-4 py-1.5 bg-blue-600 text-white font-medium
                      text-xs leading-tight uppercase hover:bg-blue-700 focus:bg-blue-700 focus:outline-none
                      focus:ring-0 active:bg-blue-800 transition duration-150 ease-in-out"
                    onClick={() => decrease(product)}
                  >
                    -
                  </button>
                  <button
                    type="button"
                    className=" inline-block px-4 py-1.5 bg-transparent text-black font-medium
                      text-xs leading-tight uppercase focus:outline-none
                      focus:ring-0 transition duration-150 ease-in-out"
                  >
                    {product.qty}
                  </button>
                  <button
                    type="button"
                    className=" rounded-r inline-block px-4 py-1.5 bg-blue-600 text-white font-medium
                      text-xs leading-tight uppercase hover:bg-blue-700 focus:bg-blue-700 focus:outline-none
                      focus:ring-0 active:bg-blue-800 transition duration-150 ease-in-out"
                    onClick={() => increase(product)}
                  >
                    +
                  </button>
                </div>
              </div>

              <div className="text-sm font-light">
                <small className="flex justify-start items-center space-x-1">
                  <FaEthereum />
                  <span className="text-gray-700 font-bold">
                    {(product.qty * product.price).toFixed(3)} EHT
                  </span>
                </small>
              </div>

              <div className="text-sm font-light mb-4">
                <button
                  type="button"
                  className="inline-block px-6 py-2.5 bg-transparent text-red-600 font-medium
                text-xs leading-tight uppercase rounded hover:text-red-700
                hover:bg-gray-100 focus:bg-gray-100 focus:outline-none focus:ring-0
                active:bg-gray-200 transition duration-150 ease-in-out"
                  onClick={() => remFromCart(product)}
                >
                  Remove
                </button>
              </div>
            </div>
          ))}
        </div>
      </div>
      <Summary summary={summary} />
    </>
  )
}

export default Cart

The Summary Component

This component enables you to supply the address and phone number of where you want the item shipped. See the codes below.

import { FaEthereum } from 'react-icons/fa'
import { useState } from 'react'
import { createOrder } from '../Blockchain.Service'
import { clearCart } from '../Cart.Service'
import { toast } from 'react-toastify'

const Summary = ({ summary }) => {
  const [destination, setDestination] = useState('')
  const [phone, setPhone] = useState('')

  const handleCheckout = async (e) => {
    e.preventDefault()
    if (!phone || !destination) return

    const params = { phone, destination, ...summary }

    await toast.promise(
      new Promise(async (resolve, reject) => {
        await createOrder(params)
          .then(() => {
            onReset()
            clearCart()
            resolve()
          })
          .catch(() => reject())
      }),
      {
        pending: 'Approve transaction...',
        success:
          'Order placed, will reflect in your Order history within 30sec 🙌',
        error: 'Encountered error placing order 🤯',
      },
    )
  }

  const onReset = () => {
    setDestination('')
    setPhone('')
  }

  return (
    <div
      className="flex flex-col md:flex-row justify-center md:justify-between
      items-center flex-wrap space-x-2 md:w-2/3 w-full p-5 mx-auto"
    >
      <form className="w-4/5 md:w-2/5 my-2">
        <div className="mb-3">
          <label className="form-label inline-block mb-2 font-bold text-sm text-gray-700">
            Destination
          </label>

          <input
            type="text"
            className="form-control block w-full px-3 py-1.5 text-base font-normal
            text-gray-700 bg-white bg-clip-padding border border-solid border-gray-300
            rounded transition ease-in-out m-0 focus:text-gray-700 focus:bg-white
            focus:border-blue-600 focus:outline-none"
            placeholder="Your full address"
            name="destination"
            onChange={(e) => setDestination(e.target.value)}
            value={destination}
          />
        </div>

        <div className="mb-3">
          <label className="form-label inline-block mb-2 font-bold text-sm text-gray-700">
            Phone
          </label>

          <input
            type="text"
            className="form-control block w-full px-3 py-1.5 text-base font-normal
            text-gray-700 bg-white bg-clip-padding border border-solid border-gray-300
            rounded transition ease-in-out m-0 focus:text-gray-700 focus:bg-white
            focus:border-blue-600 focus:outline-none"
            placeholder="Phone"
            name="phone"
            onChange={(e) => setPhone(e.target.value)}
            value={phone}
          />
        </div>

        <div className="flex justify-between items-center mb-3">
          <button
            className="px-6 py-2.5 bg-transparent border-blue-800 text-blue-800 font-medium text-xs 
            leading-tight uppercase rounded shadow-md hover:bg-blue-900 hover:shadow-lg border
            focus:border-blue-900 focus:shadow-lg focus:outline-none focus:ring-0 active:border-blue-900 
            active:shadow-lg transition duration-150 ease-in-out hover:text-white w-full"
          >
            Back to Shopping
          </button>
        </div>
      </form>

      <div className="w-4/5 md:w-2/5 my-2">
        <div className="mb-3">
          <h4 className="mb-2 font-bold text-sm text-gray-700">
            Order Summary
          </h4>
        </div>

        <div className="flex justify-between items-center mb-3">
          <h4 className="mb-2 text-sm text-gray-700">Subtotal</h4>

          <small className="flex justify-start items-center space-x-1">
            <FaEthereum />
            <span className="text-gray-700">
              {(summary.grand - summary.tax).toFixed(3)} EHT
            </span>
          </small>
        </div>

        <div className="flex justify-between items-center mb-3">
          <h4 className="mb-2 text-sm text-gray-700">Tax</h4>

          <small className="flex justify-start items-center space-x-1">
            <FaEthereum />
            <span className="text-gray-700">{summary.tax.toFixed(3)} EHT</span>
          </small>
        </div>

        <div className="flex justify-between items-center mb-3">
          <h4 className="mb-2 text-sm text-gray-700 font-bold">Grand Total</h4>

          <small className="flex justify-start items-center space-x-1">
            <FaEthereum />
            <span className="text-gray-700 font-bold">
              {summary.grand.toFixed(3)} EHT
            </span>
          </small>
        </div>

        <div className="flex justify-between items-center mb-3">
          <button
            className="px-6 py-2.5 bg-blue-800 text-white font-medium text-xs 
          leading-tight uppercase rounded shadow-md hover:bg-blue-900 hover:shadow-lg
          focus:bg-blue-900 focus:shadow-lg focus:outline-none focus:ring-0 active:bg-blue-900 
          active:shadow-lg transition duration-150 ease-in-out w-full"
            onClick={handleCheckout}
          >
            Place Order Now
          </button>
        </div>
      </div>
    </div>
  )
}

export default Summary

The Stats Components

This section handles funding and withdrawals from your store. For a complete understanding, refer to the codes below.

const Treasury = ({ stats }) => {
  return (
    <div className="flex flex-col sm:flex-row justify-center items-center p-5">
      <div className="flex flex-col justify-center items-center h-20 border border-gray-200 shadow-md w-full">
        <span className="text-lg font-bold text-black leading-5">{0} ETH</span>
        <span>Paid</span>
      </div>
      <div className="flex flex-col justify-center items-center h-20 border border-gray-200 shadow-md w-full">
        <span className="text-lg font-bold text-black leading-5">{stats.balance} ETH</span>
        <span>Balance</span>
      </div>
      <div className="flex justify-center items-center h-20 space-x-2 border border-gray-200 shadow-md w-full">
        <button
          type="button"
          className="inline-block px-6 py-2.5 bg-blue-600
            text-white font-medium text-xs leading-tight
            uppercase rounded-full shadow-md hover:bg-blue-700
            hover:shadow-lg focus:bg-blue-700 focus:shadow-lg
            focus:outline-none focus:ring-0 active:bg-blue-800
            active:shadow-lg transition duration-150 ease-in-out"
        >
          Withdraw Fund
        </button>
        <button
          type="button"
          className="inline-block px-6 py-2.5 bg-blue-600
            text-white font-medium text-xs leading-tight
            uppercase rounded-full shadow-md hover:bg-blue-700
            hover:shadow-lg focus:bg-blue-700 focus:shadow-lg
            focus:outline-none focus:ring-0 active:bg-blue-800
            active:shadow-lg transition duration-150 ease-in-out"
        >
          Fund Account
        </button>
      </div>
    </div>
  )
}

export default Treasury

And there you have it for all the chunks of components.

Pages

It is time to put together all the components on their respective pages. On the root of your project, head to **src** folder and create a new folder called **views**. Now all the created components in this section must all be included in this views folder.

The Home Page

This page bundles up the banner, shop stats, and the cards components together, see the codes below.

import Banner from '../components/Banner'
import ShopStats from '../components/ShopStats'
import Cards from '../components/Cards'
import { useGlobalState } from '../store'
import { loadProducts } from '../Blockchain.Service'
import { useEffect, useState } from 'react'


const Home = () => {
  const [products] = useGlobalState('products')
  const [stats] = useGlobalState('stats')
  const [loaded, setLoaded] = useState(false)

  useEffect(async () => {
    await loadProducts().then(() => setLoaded(true))
  }, [])

  return loaded ? (
    <>
      <Banner />
      <ShopStats stats={stats} />
      <div className="h-20"></div>
      <Cards products={products} title="Global Shop" />
    </>
  ) : null
}

export default Home

The Shopping Cart Page

This page features two components, the cart, and the summary components, they both help a customer place orders. The customer pays in ethers, see the codes below.

import Cart from '../components/Cart'
import { useGlobalState } from '../store'

const ShoppingCart = () => {
  const [cart] = useGlobalState('cart')
  const [summary] = useGlobalState('summary')

  return (
    <>
      <div className="h-10"></div>
      {cart.length > 0 ? (
        <Cart cart={cart} summary={summary} />
      ) : (
        <div className="flex flex-col justify-between items-center space-x-2 md:w-2/3 w-full p-5 mx-auto">
          <h4 className="text-center uppercase mb-8">Cart Empty</h4>
          <p>Add some products to your cart...</p>
        </div>
      )}
    </>
  )
}

export default ShoppingCart

The Product Page

The product page contains two essential components for displaying details pertaining to a specific game product. See the codes below.

import { useEffect, useState } from 'react'
import { useParams } from 'react-router-dom'
import { loadProduct } from '../Blockchain.Service'
import { useGlobalState } from '../store'
import Buyers from '../components/Buyers'
import Details from '../components/Details'

const Product = () => {
  const { id } = useParams()
  const [product] = useGlobalState('product')
  const [buyers] = useGlobalState('buyers')
  const [loaded, setLoaded] = useState(false)

  useEffect(async () => {
    await loadProduct(id).then(() => setLoaded(true))
  }, [])

  return loaded ? (
    <>
      <Details product={product} />
      <Buyers buyers={buyers} />
    </>
  ) : null
}

export default Product

The Orders and Sales Page

The orders page uses the order component to render a list of orders for a buyer of a product that he can see from his order history.

Replicate the page by creating the component below inside the views folder. See the codes below.

import { useEffect } from "react"
import { loadOrders } from "../Blockchain.Service"
import { useGlobalState } from "../store"
import Order from "../components/Order"

const Orders = () => {
  const [orders] = useGlobalState('orders')
  useEffect(async () => {
    await loadOrders()
  }, [])

  return (
    <>
      <Order orders={orders} title="Orders" />
    </>
  )
}

export default Orders
import { useEffect } from "react"
import { loadOrders } from "../Blockchain.Service"
import { useGlobalState } from "../store"
import Order from "../components/Order"

const Sales = () => {
  const [orders] = useGlobalState('orders')

  useEffect(async () => {
    await loadOrders()
  }, [])

  return (
    <>
      <Order orders={orders} title={'Sales'} seller />
    </>
  )
}

export default Sales

Chat Page

Buyer and seller using the CometChat SDK

This page enables a buyer to chat with a seller of a product, this was made possible with the CometChat SDK.

Chat with Seller CometChat SDK

Each seller must be authenticated anonymously with this chat service before receiving chats from their buyers. To enjoy this service, you must have configured the CometChat SDK which has been discussed above. See the codes below.

import Identicon from 'react-identicons'
import React, { useEffect, useState } from 'react'
import { FaTimes } from 'react-icons/fa'
import { useNavigate, useParams } from 'react-router-dom'
import { truncate, useGlobalState } from '../store'
import { sendMessage, CometChat, getMessages } from '../Chat.Service'
import { toast } from 'react-toastify'

const Chat = () => {
  const { id } = useParams()
  const [currentUser] = useGlobalState('currentUser')
  const navigate = useNavigate()

  useEffect(async () => {
    if (currentUser) {
      await getConversations().then((list) => setUsers(list))
    } else {
      toast('Please authenticate with the chat feature first!')
      navigate('/')
    }
  }, [])

  return currentUser ? (
    <>
      <ChatHeader id={id} />
      <Messages id={id} />
    </>
  ) : null
}

const ChatHeader = ({ id }) => {
  const navigate = useNavigate()

  return (
    <div className="flex justify-between items-start w-full md:w-2/3 p-5 mx-auto">
      <span
        className="rounded-full text-gray-500 bg-gray-200 font-semibold text-sm
        flex align-center cursor-pointer active:bg-gray-300
        transition duration-300 ease w-max"
      >
        <Identicon
          string={id}
          size={35}
          className="w-11 h-11 max-w-none object-contain rounded-full"
        />
        <span className="flex items-center px-3 py-2">
          {truncate(id, 4, 4, 11)}
        </span>
      </span>

      <span
        onClick={() => navigate('/product/' + 1)}
        className="rounded-full text-gray-500 bg-gray-200 font-semibold text-sm
        flex align-center cursor-pointer active:bg-gray-300
        transition duration-300 ease w-max"
      >
        <span className="flex items-center px-3 py-2">Exit</span>
        <button className="bg-transparent hover focus:outline-none pr-2">
          <FaTimes size={15} />
        </button>
      </span>
    </div>
  )
}

const Messages = ({ id }) => {
  const [connectedAccount] = useGlobalState('connectedAccount')
  const [message, setMessage] = useState('')
  const [messages, setMessages] = useState([])

  const handleSubmit = async (e) => {
    e.preventDefault()
    sendMessage(id, 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 scrollToEnd = () => {
    const element = document.getElementById('messages-container')
    element.scrollTop = element.scrollHeight
  }

  useEffect(async () => {
    listenForMessage(id)
    await getMessages(id).then((messages) =>
      setMessages(messages.filter((msg) => msg.category == 'message')),
    )
  }, [id])

  return (
    <div className="w-full lg:w-2/3 p-5 mx-auto">
      <div
        id="messages-container"
        className="h-[calc(100vh_-_18rem)] overflow-y-auto mb-8"
      >
        {messages.map((message, i) =>
          message.sender.uid != connectedAccount ? (
            <LeftMessage msg={message} key={i} />
          ) : (
            <RightMessage msg={message} key={i} />
          ),
        )}
      </div>
      <form onSubmit={handleSubmit} className="flex w-full">
        <input
          className="w-full bg-gray-200 rounded-lg p-4 
          focus:ring-0 focus:outline-none border-gray-500"
          type="text"
          placeholder="Write a message..."
          onChange={(e) => setMessage(e.target.value)}
          value={message}
          required
        />
        <button type="submit" hidden>
          Send
        </button>
      </form>
    </div>
  )
}

const RightMessage = ({ msg }) => (
  <div className="flex flex-row justify-end my-2">
    <div className="flex justify-center items-end space-x-2">
      <div
        className="flex flex-col bg-blue-600 w-80 p-3 px-5 rounded-t-3xl
        rounded-bl-3xl shadow shadow-black text-white font-semibold"
      >
        <div className="flex flex-row justify-start items-center space-x-2">
          <span>@You</span>
          <small>
            {new Date(msg.sentAt * 1000).toLocaleDateString()}{' '}
            {new Date(msg.sentAt * 1000).toLocaleTimeString()}
          </small>
        </div>
        <small className="leading-tight my-2">{msg.text}</small>
      </div>
    </div>
  </div>
)

const LeftMessage = ({ msg }) => (
  <div className="flex flex-row justify-start my-2">
    <div className="flex justify-center items-end space-x-2">
      <div
        className="flex flex-col bg-transparent w-80 p-3 px-5 rounded-t-3xl
            rounded-br-3xl shadow shadow-gray-500"
      >
        <div className="flex flex-row justify-start items-center space-x-2">
          <span>@{truncate(msg.sender.uid, 4, 4, 11)}</span>
          <small>
            {new Date(msg.sentAt * 1000).toLocaleDateString()}{' '}
            {new Date(msg.sentAt * 1000).toLocaleTimeString()}
          </small>
        </div>
        <small className="leading-tight my-2">{msg.text}</small>
      </div>
    </div>
  </div>
)

export default Chat

The Recent Chat Page

This page shows you a list of buyers who wants to contact you for extra information about your listed products. The CometChat SDK enables all these chatting functionalities to take place, you will have to sign in or up specifically for the chat feature before you utilize it on your account.

Sellers who don’t opt-in for this service cannot receive chats from their customers. See the code below.

import { useEffect, useState } from 'react'
import Identicon from 'react-identicons'
import { useNavigate } from 'react-router-dom'
import { getConversations } from '../Chat.Service'
import { truncate, useGlobalState } from '../store'
import { toast } from 'react-toastify'

const Recent = () => {
  const [users, setUsers] = useState([])
  const [currentUser] = useGlobalState('currentUser')
  const navigate = useNavigate()

  useEffect(async () => {
    if (currentUser) {
      await getConversations().then((list) => setUsers(list))
    } else {
      toast('Please authenticate with the chat feature first!')
      navigate('/')
    }
  }, [])

  return currentUser ? (
    <>
      <div className="h-20"></div>
      <div className="flex flex-col justify-between items-center space-x-2 md:w-2/3 w-full p-5 mx-auto">
        <h4 className="text-center uppercase mb-8">Recent Chats</h4>
        <div className="max-h-[calc(100vh_-_20rem)] overflow-y-auto shadow-md rounded-md w-full">
          {users.map((user, i) => (
            <Conversation conversation={user.lastMessage} key={i} />
          ))}
        </div>

        <div className="flex justify-between items-center my-4">
          <button
            className="px-6 py-2.5 bg-transparent border-blue-800 text-blue-800 font-medium text-xs 
            leading-tight uppercase rounded shadow-md hover:bg-blue-900 hover:shadow-lg border
            focus:border-blue-900 focus:shadow-lg focus:outline-none focus:ring-0 active:border-blue-900 
            active:shadow-lg transition duration-150 ease-in-out hover:text-white w-full"
          >
            Back to Home
          </button>
        </div>
      </div>
    </>
  ) : null
}

const Conversation = ({ conversation }) => {
  const navigate = useNavigate()
  const [connectedAccount] = useGlobalState('connectedAccount')

  const uid = (conversation) => {
    return conversation.sender.uid == connectedAccount
      ? conversation.receiver.uid
      : conversation.sender.uid
  }

  return (
    <button
      type="button"
      data-mdb-ripple="true"
      data-mdb-ripple-color="light"
      className="px-6 py-2.5 bg-white text-black font-medium text-xs leading-tight
      rounded shadow-md hover:bg-blue-700 hover:shadow-lg focus:bg-blue-700
      focus:shadow-lg focus:outline-none focus:ring-0 active:bg-blue-800 hover:text-white
      active:shadow-lg transition duration-150 ease-in-out w-full text-left my-2"
      onClick={() => navigate('/chat/' + uid(conversation))}
    >
      <div className="flex justify-start items-center space-x-4">
        <Identicon
          string={uid(conversation)}
          size={30}
          className="h-10 w-10 object-contain rounded-fullbg-white cursor-pointer"
        />

        <div className="flex flex-col justify-start space-y-2">
          <h4 className="font-bold text-md">
            {truncate(uid(conversation), 4, 4, 11)}
          </h4>
          <span className="text-sm">{conversation.text}</span>
          <small className="font-bold">
            {new Date(conversation.sentAt * 1000).toLocaleDateString()}{' '}
            {new Date(conversation.sentAt * 1000).toLocaleTimeString()}
          </small>
        </div>
      </div>
    </button>
  )
}

export default Recent

The Seller and Stats page

My Products and Stats Page

The last two pages are dedicated to listing specific sellers’ products as well as some shop statistics. The statistics show how well the seller is doing in this market. Still, in the views component, create these two pages. See the codes listed below.

import { useParams } from 'react-router-dom'
import Cards from '../components/Cards'

const Seller = () => {
  const { id } = useParams()

  return (
    <>
      <div className="h-20"></div>
      <Cards products={[]} title="Seller Shop" seller={id} />
    </>
  )
}

export default Seller
import { useEffect, useState } from 'react'
import { loadStats } from '../Blockchain.Service'
import ShopStats from '../components/ShopStats'
import Treasury from '../components/Treasury'
import { useGlobalState } from '../store'

const Stats = () => {
  const [stats] = useGlobalState('myStats')
  const [loaded, setLoaded] = useState(false)
  useEffect(async () => {
    await loadStats().then(() => setLoaded(true))
  }, [])

  return loaded ? (
    <>
      <div className="h-20"></div>
      <h4 className="text-center uppercase mb-8">Your Stats</h4>
      <ShopStats stats={stats} />
      <Treasury stats={stats} />
      <div className="flex justify-center items-center my-4">
        <button
          className="px-6 py-2.5 bg-transparent border-blue-800 text-blue-800 font-medium text-xs 
            leading-tight uppercase rounded shadow-md hover:bg-blue-900 hover:shadow-lg border
            focus:border-blue-900 focus:shadow-lg focus:outline-none focus:ring-0 active:border-blue-900 
            active:shadow-lg transition duration-150 ease-in-out hover:text-white"
        >
          Back to Home
        </button>
      </div>
    </>
  ) : null
}

export default Stats

Fantastic, that will be all for the pages, let’s proceed to other essential components of this application.

Configuring Other Components

There are other components that complete this application and in this part, we will be working on them one step at a time.

The App.jsx File
Head to the src folder and open the **App.jsx** file and replace its content with the codes below.

import { Route, Routes } from 'react-router-dom'
import { useEffect, useState } from 'react'
import { isWallectConnected } from './Blockchain.Service'
import { ToastContainer } from 'react-toastify'
import { checkStorage } from './Cart.Service'
import Header from './components/Header'
import AddButton from './components/AddButton'
import CreateProduct from './components/CreateProduct'
import UpateProduct from './components/UpateProduct'
import Menu from './components/Menu'
import Home from './views/Home'
import Product from './views/Product'
import Orders from './views/Orders'
import Chat from './views/Chat'
import Seller from './views/Seller'
import Recent from './views/Recent'
import Stats from './views/Stats'
import Sales from './views/Sales'
import ShoppingCart from './views/ShoppingCart'
import DeleteProduct from './components/DeleteProduct'
import ChatModal from './components/ChatModal'
import { isUserLoggedIn } from './Chat.Service'

const App = () => {
  const [loaded, setLoaded] = useState(false)

  useEffect(async () => {
    await isWallectConnected().then(async () => {
      checkStorage()
      await isUserLoggedIn()
      setLoaded(true)
      console.log('Blockchain Loaded')
    })
  }, [])

  return loaded ? (
    <div className="min-h-screen">
      <Header />

      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/cart" element={<ShoppingCart />} />
        <Route path="/product/:id" element={<Product />} />
        <Route path="/orders/" element={<Orders />} />
        <Route path="/sales/" element={<Sales />} />
        <Route path="/chat/:id" element={<Chat />} />
        <Route path="/recents" element={<Recent />} />
        <Route path="/seller/:id" element={<Seller />} />
        <Route path="/stats/:id" element={<Stats />} />
      </Routes>

      <AddButton />
      <CreateProduct />
      <UpateProduct />
      <DeleteProduct />
      <Menu />
      <ChatModal />
      <ToastContainer
        position="bottom-center"
        autoClose={5000}
        hideProgressBar={false}
        newestOnTop={false}
        closeOnClick
        rtl={false}
        pauseOnFocusLoss
        draggable
        pauseOnHover
        theme="dark"
      />
    </div>
  ) : null
}

export default App

The above codes will ensure that all the components and pages are represented properly.

State Management Service
You will need a state management library to work with the blockchain and link all the various components together. For the sake of simplicity, we are using a react-hooks-global-state.

Navigate to the **project** >> **src** and create a new folder called the store. Inside this store folder, create a new file called **index.jsx** and paste the codes below inside and save.

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

const { setGlobalState, useGlobalState, getGlobalState } = createGlobalState({
  chatModal: 'scale-0',
  deleteModal: 'scale-0',
  updateModal: 'scale-0',
  modal: 'scale-0',
  menu: 'scale-0',
  connectedAccount: '',
  currentUser: null,
  contract: null,
  stats: null,
  myStats: null,
  buyers: [],
  orders: [],
  sales: [],
  products: [],
  product: null,
  cart: [],
  summary: { total: 0, grand: 0, tax: 0, qtys: [], ids: [] },
})

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
}

export { useGlobalState, setGlobalState, getGlobalState, truncate }

All the data coming from the blockchain will be stored in the above file and used across the entire application.

The Blockchain Service
This file contains all the EthersJs procedures for communicating with your smart contract that lives on the blockchain. In the src folder, create a file called **Blockchain.services.jsx** and paste the codes below and save.

import abi from './abis/src/contracts/Shop.sol/Shop.json'
import address from './abis/contractAddress.json'
import { getGlobalState, setGlobalState } from './store'
import { ethers } from 'ethers'
import { logOutWithCometChat } from './Chat.Service'

const toWei = (num) => ethers.utils.parseEther(num.toString())

const { ethereum } = window
const contractAddress = address.address
const contractAbi = abi.abi
const fee = toWei('0.002')

const getEtheriumContract = () => {
  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' })

    window.ethereum.on('chainChanged', (chainId) => {
      window.location.reload()
    })

    window.ethereum.on('accountsChanged', async () => {
      setGlobalState('connectedAccount', accounts[0].toLowerCase())
      await logOutWithCometChat()
      await isWallectConnected()
    })

    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 createProduct = async ({
  sku,
  name,
  description,
  imageURL,
  price,
  stock,
}) => {
  try {
    if (!ethereum) return alert('Please install Metamask')
    const connectedAccount = getGlobalState('connectedAccount')
    const contract = getEtheriumContract()
    price = toWei(price)

    await contract.createProduct(
      sku,
      name,
      description,
      imageURL,
      price,
      stock,
      {
        from: connectedAccount,
        value: fee._hex,
      },
    )
  } catch (error) {
    reportError(error)
  }
}

const updateProduct = async ({
  id,
  name,
  description,
  imageURL,
  price,
  stock,
}) => {
  try {
    if (!ethereum) return alert('Please install Metamask')
    const connectedAccount = getGlobalState('connectedAccount')
    const contract = getEtheriumContract()
    price = toWei(price)

    await contract.updateProduct(
      id,
      name,
      description,
      imageURL,
      price,
      stock,
      {
        from: connectedAccount,
      },
    )
  } catch (error) {
    reportError(error)
  }
}

const deleteProduct = async (id) => {
  try {
    if (!ethereum) return alert('Please install Metamask')
    const connectedAccount = getGlobalState('connectedAccount')
    const contract = getEtheriumContract()
    await contract.deleteProduct(id, { from: connectedAccount })
  } catch (error) {
    reportError(error)
  }
}

const createOrder = async ({ ids, qtys, phone, destination, grand }) => {
  try {
    if (!ethereum) return alert('Please install Metamask')
    const connectedAccount = getGlobalState('connectedAccount')
    const contract = getEtheriumContract()
    grand = toWei(grand)

    await contract.createOrder(ids, qtys, destination, phone, {
      from: connectedAccount,
      value: grand._hex,
    })
  } catch (error) {
    reportError(error)
  }
}

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

    const connectedAccount = getGlobalState('connectedAccount')
    const contract = getEtheriumContract()
    const products = await contract.getProducts()
    const stats = await contract.stats()
    const myStats = await contract.statsOf(connectedAccount)

    setGlobalState('products', structuredProducts(products))
    setGlobalState('stats', structureStats(stats))
    setGlobalState('myStats', structureStats(myStats))
  } catch (error) {
    reportError(error)
  }
}

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

    const contract = getEtheriumContract()
    const product = await contract.getProduct(id)
    const buyers = await contract.getBuyers(id)

    setGlobalState('product', structuredProducts([product])[0])
    setGlobalState('buyers', structuredBuyers(buyers))
  } catch (error) {
    reportError(error)
  }
}

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

    const orders = await contract.getOrders()
    setGlobalState('orders', structuredOrders(orders))
  } catch (error) {
    reportError(error)
  }
}

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

    const connectedAccount = getGlobalState('connectedAccount')
    const contract = getEtheriumContract()
    const myStats = await contract.statsOf(connectedAccount)

    setGlobalState('myStats', structureStats(myStats))
  } catch (error) {
    reportError(error)
  }
}

const delieverOrder = async (pid, id) => {
  try {
    if (!ethereum) return alert('Please install Metamask')
    const connectedAccount = getGlobalState('connectedAccount')
    const contract = getEtheriumContract()
    await contract.deliverOrder(pid, id, { from: connectedAccount })
  } catch (error) {
    reportError(error)
  }
}

const cancelOrder = async (pid, id) => {
  try {
    if (!ethereum) return alert('Please install Metamask')
    const connectedAccount = getGlobalState('connectedAccount')
    const contract = getEtheriumContract()
    await contract.cancelOrder(pid, id, { from: connectedAccount })
  } catch (error) {
    reportError(error)
  }
}

const reportError = (error) => {
  console.log(error.message)
  throw new Error('No ethereum object.')
}

const structuredProducts = (products) =>
  products
    .map((product) => ({
      id: Number(product.id),
      sku: product.sku,
      seller: product.seller.toLowerCase(),
      name: product.name,
      description: product.description,
      imageURL: product.imageURL,
      stock: Number(product.stock),
      price: parseInt(product.price._hex) / 10 ** 18,
      deleted: product.deleted,
      timestamp: new Date(product.timestamp).getTime(),
    }))
    .reverse()

const structuredOrders = (orders) =>
  orders
    .map((order) => ({
      pid: Number(order.pid),
      id: Number(order.id),
      name: order.name,
      sku: order.sku,
      seller: order.seller.toLowerCase(),
      buyer: order.buyer.toLowerCase(),
      destination: order.destination,
      phone: order.phone,
      imageURL: order.imageURL,
      qty: Number(order.qty),
      status: Number(order.status),
      total: parseInt(order.total._hex) / 10 ** 18,
      timestamp: new Date(order.timestamp.toNumber()).getTime(),
    }))
    .reverse()

const structuredBuyers = (buyers) =>
  buyers
    .map((buyer) => ({
      buyer: buyer.buyer.toLowerCase(),
      qty: Number(buyer.qty),
      price: parseInt(buyer.price._hex) / 10 ** 18,
      timestamp: new Date(buyer.timestamp.toNumber() * 1000).toDateString(),
    }))
    .reverse()

const structureStats = (stats) => ({
  balance: Number(stats.balance),
  orders: Number(stats.orders),
  products: Number(stats.products),
  sales: Number(stats.sales),
  paid: Number(stats.paid._hex),
  sellers: Number(stats.sellers),
})

export {
  isWallectConnected,
  connectWallet,
  createProduct,
  updateProduct,
  deleteProduct,
  loadProducts,
  loadProduct,
  createOrder,
  loadOrders,
  loadStats,
  delieverOrder,
  cancelOrder,
}

The Cart Service
This file contains the codes that calibrate our cart system, it ensures that every change in price and quantity of items is reflected in the sub and grand total of our cart.

On the **src** directory, create a new file named **Cart.Services.jsx**, copy the codes below and paste them into it and save.

import { getGlobalState, setGlobalState } from './store'

const addToCart = (product) => {
  const products = getGlobalState('cart')
  if (!products.find((p) => product.id == p.id)) {
    setGlobalState('cart', [...products, { ...product, qty: 1 }])
    localStorage.setItem(
      'cart',
      JSON.stringify([...products, { ...product, qty: 1 }]),
    )
    summarizeCart()
  }
}

const remFromCart = (product) => {
  let products = getGlobalState('cart')
  products = products.filter((p) => p.id != product.id)
  setGlobalState('cart', products)
  localStorage.setItem('cart', JSON.stringify(products))
  summarizeCart()
}

const updateCart = (product) => {
  const products = getGlobalState('cart')
  products.forEach((p) => {
    if (p.id == product.id) p = product
  })
  setGlobalState('cart', products)
  localStorage.setItem('cart', JSON.stringify(products))
  summarizeCart()
}

const clearCart = () => {
  setGlobalState('cart', [])
  localStorage.removeItem('cart')
  summarizeCart()
}

const summarizeCart = () => {
  const products = getGlobalState('cart')
  const summary = getGlobalState('summary')
  products.forEach((p, i) => {
    summary.total += p.qty * p.price
    if (summary.ids.includes(p.id)) {
      summary.qtys[i] = p.qty
    } else {
      summary.ids[i] = p.id
      summary.qtys[i] = p.qty
    }
  })
  summary.tax = 0.002
  summary.grand = summary.total + summary.tax
  setGlobalState('summary', summary)
  summary.total = 0
  // summary.grand = 0
}

const checkStorage = () => {
  let products = JSON.parse(localStorage.getItem('cart'))
  if (products?.length) {
    setGlobalState('cart', JSON.parse(localStorage.getItem('cart')))
    summarizeCart()
  }
}

export { addToCart, remFromCart, updateCart, checkStorage, clearCart }

The Chat Service
This file contains the codes for interacting with the CometChat SDK. In the **src** folder, create a new file named **Chat.Services.jsx**. Now, copy the codes below, paste them into the file, and save.

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
  return await CometChat.login(UID, authKey)
    .then((user) => {
      setGlobalState('currentUser', user)
      return true
    })
    .catch((error) => error)
}

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

  return await CometChat.createUser(user, authKey)
    .then((user) => {
      console.log('Signed In: ', user)
      return true
    })
    .catch((error) => error)
}

const logOutWithCometChat = async () => {
  return await CometChat.logout()
    .then(() => setGlobalState('currentUser', null))
    .catch((error) => error)
}

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

const getUser = async (UID) => {
  return await CometChat.getUser(UID)
    .then((user) => user)
    .catch((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)
    .catch((error) => error)
}

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

Finally, click the link below to download the image. If the asset folder does not already exist in your src directory, create one.

https://github.com/Daltonic/gameshop/blob/master/src/assets/banner.png?raw=true

With all that setup, run the command below to have the project running on your local machine.

yarn start

This will open the project on the browser at **localhost:3000**.

Conclusion

That concludes the tutorial for this build, you have learned how to create a decentralized eCommerce platform that allows you to list game products on a marketplace.

Buyers can purchase your game product and on delivery, the money is released to the seller of the product.

This is one powerful use case for developing a real-life decentralized web3 application. More of these kinds of builds can be found here on my account.

You can also watch my free videos on my YouTube channel. Or book your private web3 classes with me to speed up your web3 learning process.

With that said, I will catch you next time, 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.

L O A D I N G
. . . comments & more!




Web3 development is officially the new way to build web applications, and if you are not there yet, you need to catch up. The way to master building web3 apps is by understanding smart contracts, a frontend framework such as React, and how to link the smart contract with the front end.

In this tutorial, you will learn how to build a decentralized web3 eShop for selling game items using the native ETH currency.

This app comprises the smart contract layer, a front end where all the interactions with the smart contract take place, and an anonymous chat feature using the CometChat SDK.

If you are ready to crush this build, then let’s get started.

What you will be building: see the demo on the Goerli test network and git repo here.

Prerequisite

You will need the following tools installed to build along with me:

  • NodeJs (Super important)
  • EthersJs
  • Hardhat
  • React
  • Tailwind CSS
  • CometChat SDK
  • Metamask
  • Yarn

Installing Dependencies

Clone the starter project from this Git repo to your computer. Also, make sure to replace it with the name of your preferred project. See the command below.

git clone https://github.com/Daltonic/tailwind_ethers_starter_kit <PROJECT_NAME>
cd <PROJECT_NAME>

Now, 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": "GameShop",
  "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",
    "deploy": "yarn hardhat run scripts/deploy.js --network localhost"
  },
  "dependencies": {
    "@cometchat-pro/chat": "^3.0.10",
    "@nomiclabs/hardhat-ethers": "^2.1.0",
    "@nomiclabs/hardhat-waffle": "^2.0.3",
    "ethereum-waffle": "^3.4.4",
    "ethers": "^5.6.9",
    "hardhat": "^2.10.1",
    "ipfs-http-client": "^57.0.3",
    "moment": "^2.29.4",
    "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",
    "react-toastify": "^9.0.8",
    "web-vitals": "^2.1.4"
  },
  "devDependencies": {
    "@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",
    "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",
    "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"
    ]
  }
}

With the above codes replaced and saved in your package.json, run the command below to install the entire packages listed above.

yarn install

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.

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 GameShop.

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.

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=******************************

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

Configuring Alchemy App

STEP 1:

Head to Alchemy, and create an account.

STEP 2:
From the dashboard create a new project.

STEP 3:
Copy the Goerli test network WebSocket or HTTPS endpoint URL to your .env file.

After that, enter the private key of your preferred Metamask account to the DEPLOYER_KEY in your environment variables and save. 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.

Extracting Your Metamask Private Key

STEP 1:
Make sure Goerli is selected as the test network in your Metamask browser extension, Rinkeby and the older test nets have now been depreciated.

Next, 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=***************************
DEPLOYER_KEY=**********************

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

Configuring the Hardhat script

At the root of this project, open the hardhat.config.js file and replace its content 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"
    },
    goerli: {
      url: process.env.ENDPOINT_URL,
      accounts: [process.env.DEPLOYER_KEY]
    }
  },
  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 code 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.

The Blockchain Service File

Now that we have the above configurations set up, let’s create the smart contract for this build. On your project, head to the **src** directory and create a new folder called **contracts**.

Inside this contracts folder, create a new file called **Shop.sol**, this file will contain all the logics that regulates the activities of the smart contract. Copy, paste, and save the codes below inside the **Shop.sol** file. See the full code below.

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

contract Shop {
    enum OrderEnum {
        PLACED,
        DELEVIRED,
        CANCELED,
        REFUNDED
    }

    struct ProductStruct {
        uint id;
        string sku;
        address seller;
        string name;
        string imageURL;
        string description;
        uint price;
        uint timestamp;
        bool deleted;
        uint stock;
    }

    struct OrderStruct {
        uint pid;
        uint id;
        string sku;
        string name;
        string imageURL;
        address buyer;
        address seller;
        uint qty;
        uint total;
        uint timestamp;
        string destination;
        string phone;
        OrderEnum status;
    }

    struct CartStruct {
        uint id;
        uint qty;
    }

    struct BuyerStruct {
        address buyer;
        uint price;
        uint qty;
        uint timestamp;
    }

    struct ShopStats {
        uint products;
        uint orders;
        uint sellers;
        uint sales;
        uint paid;
        uint balance;
    }

    address public owner;
    ShopStats public stats;
    uint public fee;
    ProductStruct[] products;
    mapping(address => ProductStruct[]) productsOf;
    mapping(uint => OrderStruct[]) ordersOf;
    mapping(address => ShopStats) public statsOf;
    mapping(uint => BuyerStruct[]) buyersOf;
    mapping(uint => bool) public productExist;
    mapping(uint => mapping(uint => bool)) public orderExist;

    event Sale(
        uint256 id,
        address indexed buyer,
        address indexed seller,
        uint256 price,
        uint256 timestamp
    );

    constructor(uint _fee) {
        owner = msg.sender;
        fee = _fee;
    }

    function createProduct(
        string memory sku,
        string memory name,
        string memory description,
        string memory imageURL,
        uint price,
        uint stock
    ) public payable returns (bool) {
        require(msg.value >= fee, "Insufficient fund");
        require(bytes(sku).length > 0, "sku cannot be empty");
        require(bytes(name).length > 0, "name cannot be empty");
        require(bytes(description).length > 0, "description cannot be empty");
        require(bytes(imageURL).length > 0, "image URL cannot be empty");
        require(price > 0, "price cannot be zero");
        require(stock > 0, "stock cannot be zero");

        productExist[stats.products] = true;
        statsOf[msg.sender].products++;
        stats.sellers++;
        ProductStruct memory product;

        product.id = stats.products++;
        product.sku = sku;
        product.seller = msg.sender;
        product.name = name;
        product.imageURL = imageURL;
        product.description = description;
        product.price = price;
        product.stock = stock;
        product.timestamp = block.timestamp;

        products.push(product);
        return true;
    }

    function updateProduct(
        uint id,
        string memory name,
        string memory description,
        string memory imageURL,
        uint price,
        uint stock
    ) public returns (bool) {
        require(products[id].seller == msg.sender, "Unauthorize Personel");
        require(bytes(name).length > 0, "name cannot be empty");
        require(bytes(description).length > 0, "description cannot be empty");
        require(price > 0, "price cannot be zero");
        require(stock > 0, "stock cannot be zero");

        ProductStruct memory product;
        product.id = id;
        product.seller = msg.sender;
        product.name = name;
        product.imageURL = imageURL;
        product.description = description;
        product.price = price;
        product.stock = stock;

        products[id] = product;
        updateOrderDetails(product);

        return true;
    }

    function updateOrderDetails(ProductStruct memory product) internal {
        for(uint i=0; i < ordersOf[product.id].length; i++) {
            OrderStruct memory order = ordersOf[product.id][i];
            order.name = product.name;
            order.imageURL = product.imageURL;
            ordersOf[product.id][i] = order;
        }
    }

    function deleteProduct(uint id) public returns (bool) {
        require(products[id].seller == msg.sender, "Unauthorize Personel");
        products[id].deleted = true;
        return true;
    }

    function getProduct(uint id) public view returns (ProductStruct memory) {
        require(productExist[id], "Product not found");
        return products[id];
    }
    
    function getProducts() public view returns (ProductStruct[] memory) {
        return products;
    }

    function createOrder(
        uint[] memory ids,
        uint[] memory qtys,
        string memory destination,
        string memory phone
    ) public payable returns (bool) {
        require(msg.value >= totalCost(ids, qtys), "Insufficient amount");
        require(bytes(destination).length > 0, "destination cannot be empty");
        require(bytes(phone).length > 0, "phone cannot be empty");
    
        stats.balance += totalCost(ids, qtys);

        for(uint i = 0; i < ids.length; i++) {
            
            if(productExist[ids[i]] && products[ids[i]].stock >= qtys[i]) {
                products[ids[i]].stock -= qtys[i];
                statsOf[msg.sender].orders++;
                stats.orders++;

                OrderStruct memory order;

                order.pid = products[ids[i]].id;
                order.id = ordersOf[order.pid].length; // order Id resolved
                order.sku = products[ids[i]].sku;
                order.buyer = msg.sender;
                order.seller = products[ids[i]].seller;
                order.name = products[ids[i]].name;
                order.imageURL = products[ids[i]].imageURL;
                order.qty = qtys[i];
                order.total = qtys[i] * products[ids[i]].price;
                order.timestamp = block.timestamp;
                order.destination = destination;
                order.phone = phone;

                ordersOf[order.pid].push(order);
                orderExist[order.pid][order.id] = true;

                emit Sale(
                    order.id,
                    order.buyer,
                    order.seller,
                    order.total,
                    block.timestamp
                );
            }
        }

        return true;
    }

    function totalCost(uint[] memory ids, uint[] memory qtys) internal view returns (uint) {
        uint total;
        for(uint i = 0; i < ids.length; i++) {
            total += products[i].price * qtys[i];
        }
        return total;
    }

    function deliverOrder(uint pid, uint id) public returns (bool) {
        require(orderExist[pid][id], "Order not found");
        OrderStruct memory order = ordersOf[pid][id];
        require(order.seller == msg.sender, "Unauthorized Entity");
        require(order.status != OrderEnum.DELEVIRED, "Order already delievered");
        
        order.status = OrderEnum.DELEVIRED;
        ordersOf[pid][id] = order;

        stats.balance -= order.total;
        statsOf[order.seller].paid += order.total;
        statsOf[order.seller].sales++;
        stats.sales++;

        payTo(order.seller, order.total);

        buyersOf[id].push(
            BuyerStruct(
                order.buyer,
                order.total,
                order.qty,
                block.timestamp
            )
        );
        return true;
    }

    function cancelOrder(uint pid, uint id) public returns (bool) {
        require(orderExist[pid][id], "Order not found");
        OrderStruct memory order = ordersOf[pid][id];
        require(order.buyer == msg.sender, "Unauthorized Entity");
        require(order.status != OrderEnum.CANCELED, "Order already canceled");

        order.status = OrderEnum.CANCELED;
        products[order.pid].stock += order.qty;
        ordersOf[pid][id] = order;

        payTo(order.buyer, order.total);
        return true;
    }

    function getOrders() public view returns (OrderStruct[] memory props) {
        props = new OrderStruct[](stats.orders);

        for(uint i=0; i < stats.orders; i++) {
            for(uint j=0; j < ordersOf[i].length; j++) {
                props[i] = ordersOf[i][j];
            }
        }
    }

    function getOrder(uint pid, uint id) public view returns (OrderStruct memory) {
        require(orderExist[pid][id], "Order not found");
        return ordersOf[pid][id];
    }

    function getBuyers(uint pid) public view returns (BuyerStruct[] memory buyers) {
        require(productExist[pid], "Product does not exist");
        return buyersOf[pid];
    }

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

Now, let’s explain what is going on in the smart contract above. We have the following:

  • OrderEnum: This enumerable describes the various status an order goes through in its lifecycle. For example, an order could be placed, delivered, canceled, etc.

  • ProductStruct: This structure model the details of each product to be stored in this smart contract. For example, the SKU, stock, price, and so on.

  • OrderStruct: This structure embodies the details of each order placed in the shop such as the order id, the buyer, the quantity of items, and more.

  • CartStruct: This structure contains the data a cart collects for each item to be submitted as an order in this shop.

  • BuyerStruct: This structure speaks of the kind of data to be collected whenever a buyer purchases a product from our shop.

  • ShopStats: This is a structure that details the statistics of our shop. Information such as the number of sellers, products, orders, and sales are contained by this struct.

For the state variables, we have the following.

  • Owner: This state variable contains the account of the deployer of this smart contract.

  • Stats: This holds information about the current statistics of our shop.

  • Fee: This contains how much to be charged per creation of a product on this platform.

  • Products: This holds a collection of products added to this platform.

  • ProductsOf: This captures the products added by a specific seller to our shop.

  • OrdersOf: This contains a list of orders purchased by a specific buyer in the shop.

  • StatsOf: This holds the statistics of each buyer or seller on the platform.

  • BuyersOf: This accommodates information of the buyers of a specific product.

  • ProductExist: This checks if a product is found in our shop.

  • OrderExist: This checks if an order is found in our shop.

For the functions, we have the following.

  • CreateProduct: This adds a new function to the shop using supplied product information such as the name, description, and price.
  • UpdateProduct: This modifies existing product information with new data supplied via the function’s parameters.
  • UpdateOrderDetails: This function sends a product’s update across every order it has received already.
  • DeleteProduct: This toggles an existing product to a deleted state and becomes unavailable for purchase.
  • GetProduct: This returns the entire list of products in our shop.
  • GetProducts: This returns a specific product from our shop by targeting its Id.
  • CreateOrder: This function cancels an order, it is accessible only by the buyer of such product.
  • TotalCost: This calculates the overall cost for each product ordered.
  • DeliverOrder: This function delivers an order, it is accessible only by the seller of such product.
  • CancelOrder: This function marks an order as canceled and is accessible only to be the buyer of such product.
  • GetOrders: This returns the entire collection of orders placed on this shop.
  • GetOrder: This returns a specific order by its Id.
  • GetBuyers: Returns a collection of buyers of a particular product.
  • PayTo: Sends a specific amount to a specific address when invoked.

If you are new to Solidity, I have a full FREE course on YouTube called, Mastering Solidity Basics. So do check it out, like it, and subscribe!

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 fee = ethers.utils.parseEther('0.002')
  const Contract = await ethers.getContractFactory('Shop')
  const contract = await Contract.deploy(fee)

  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
})

The above script when executed as a Hardhat command will ship the Shop.sol smart contract into any chosen network.

With the above instructions diligently followed, open up a terminal pointing to this project and run the below commands separately on two terminals. VS Code enables you to do this straight from your editor. See the command below.

yarn hardhat node # Terminal #1
yarn hardhat run scripts/deploy.js --network localhost # Terminal #2

If the above commands were successfully executed, you will see these sorts of activities on your terminal. See the image below.

Activities of Deployment on the Terminal

Developing the Frontend

Now that we have our smart contract on a network and all our artifacts (bytecodes and abi) generated, let’s start creating the front end with React step-by-step.

Components
Create a new folder called components in the src directory, which will house all of the React components.

Header component

This component is in charge of displaying information about the currently connected user, the number of items in his cart, and a clickable Identicon that shows more seller options. See the codes responsible for its behavior below.

import Identicon from 'react-identicons'
import { FaEthereum } from 'react-icons/fa'
import { Link, useNavigate } from 'react-router-dom'
import { AiOutlineShoppingCart } from 'react-icons/ai'
import { setGlobalState, truncate, useGlobalState } from '../store'
import { connectWallet } from '../Blockchain.Service'

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

  return (
    <div className="flex justify-between items-center shadow-sm shadow-gray-200 p-5">
      <Link
        to="/"
        className="flex justify-start items-center space-x-1 text-md font-bold"
      >
        <FaEthereum className="cursor-pointer" size={25} />
        <span>GameShop</span>
      </Link>

      <div className="flex justify-end items-center space-x-6">
        <div className="flex justify-center items-center space-x-4">
          <button
            onClick={() => navigate('/cart')}
            className="rounded-full text-gray-500 bg-gray-200 font-semibold text-sm flex 
            align-center cursor-pointer active:bg-gray-300 transition duration-300 
            ease w-max py-1 px-2"
          >
            <AiOutlineShoppingCart className="cursor-pointer" size={25} />
            <span
              className="rounded-full py-[2px] px-[10px] text-center font-bold
            bg-red-600 text-white ml-2"
            >
              {cart.length}
            </span>
          </button>

          <button
            onClick={() => setGlobalState('menu', 'scale-100')}
            className="bg-transparent shadow-sm shadow-gray-400 rounded-full"
          >
            <Identicon
              string={connectedAccount}
              size={25}
              className="h-10 w-10 object-contain rounded-full cursor-pointer"
            />
          </button>
        </div>
        {connectedAccount ? (
          <button
            className="px-6 py-2.5 bg-blue-800 text-white font-medium text-xs 
            leading-tight uppercase rounded shadow-md hover:bg-blue-900 hover:shadow-lg
            focus:bg-blue-900 focus:shadow-lg focus:outline-none focus:ring-0 active:bg-blue-900 
            active:shadow-lg transition duration-150 ease-in-out"
          >
            {truncate(connectedAccount, 4, 4, 11)}
          </button>
        ) : (
          <button
            className="px-6 py-2.5 bg-blue-800 text-white font-medium text-xs 
            leading-tight uppercase rounded shadow-md hover:bg-blue-900 hover:shadow-lg
            focus:bg-blue-900 focus:shadow-lg focus:outline-none focus:ring-0 active:bg-blue-900 
            active:shadow-lg transition duration-150 ease-in-out"
            onClick={connectWallet}
          >
            Connect
          </button>
        )}
      </div>
    </div>
  )
}

export default Header

Banner Component

This component captures a beautiful display of game items. This was designed to give our app a good feel of being a GameShop.

import bannerImg from '../assets/banner.png'

const Banner = () => {
  return (
    <div
      className="flex flex-col lg:flex-row justify-center lg:justify-between 
      items-center lg:space-x-10 md:w-2/3 w-full p-5 mx-auto"
    >
      <img className="mb-5 lg:mb-0" src={bannerImg} alt="banner" />
      <div className="flex flex-col justify-between  items-start lg:items-center text-center lg:text-left">
        <div className="flex flex-col space-y-4 mb-5">
          <h4 className="text-3xl font-bold">Win a Game</h4>
          <p className="text-gray-500">
            Win some money worth upto a game console while surfing our game
            collection, click on the spin button.
          </p>
        </div>
        <div className="flex justify-start text-center items-center space-x-2 mx-auto lg:ml-0">
          <button
            className="px-6 py-2.5 bg-blue-800 text-white font-medium text-xs 
                leading-tight uppercase rounded shadow-md hover:bg-blue-900 hover:shadow-lg
                focus:bg-blue-900 focus:shadow-lg focus:outline-none focus:ring-0 active:bg-blue-900 
                active:shadow-lg transition duration-150 ease-in-out"
          >
            Spin Now
          </button>
        </div>
      </div>
    </div>
  )
}

export default Banner

ShopStats Component

This component records a statistic about the shop’s current state. This section displays the number of products, sellers, orders, and so on. Look at the code that is responsible for this.

import React from 'react'

const ShopStats = ({ stats }) => {
  return (
    <div className="flex flex-col sm:flex-row justify-center items-center p-5">
      <div className="flex flex-col justify-center items-center h-20 border border-gray-200 shadow-md w-full">
        <span className="text-lg font-bold text-black leading-5">
          {stats.products}
        </span>
        <span>Products</span>
      </div>
      <div className="flex flex-col justify-center items-center h-20 border border-gray-200 shadow-md w-full">
        <span className="text-lg font-bold text-black leading-5">
          {stats.sellers}
        </span>
        <span>Sellers</span>
      </div>
      <div className="flex flex-col justify-center items-center h-20 border border-gray-200 shadow-md w-full">
        <span className="text-lg font-bold text-black leading-5">
          {stats.sales}
        </span>
        <span>Sales</span>
      </div>
    </div>
  )
}

export default ShopStats

The Cards Component

This component renders a collection of game products on cards. Each card contains game information such as the name, price, stock, and image URL. See the code snippet below.

import Identicon from 'react-identicons'
import { FaEthereum } from 'react-icons/fa'
import { Link } from 'react-router-dom'
import { truncate } from '../store'

const Cards = ({ products, title, seller }) => {
  return (
    <>
      <div className="flex flex-col items-center space-y-4">
        {seller ? (
          <Identicon
            string={'0adsclsidnt'}
            size={70}
            className="h-10 w-10 object-contain rounded-full cursor-pointer shadow-sm shadow-gray-400"
          />
        ) : null}
        <h4 className="text-center uppercase">{title}</h4>
      </div>

      <div className="flex flex-wrap justify-center items-center space-x-6 md:w-2/3 w-full p-5 mx-auto">
        {products.map((product, i) =>
          product.deleted ? null : <Card product={product} key={i} />,
        )}
      </div>

      <div className="flex justify-center items-center my-5">
        <button
          className="px-6 py-2.5 bg-blue-800 text-white font-medium text-xs 
          leading-tight uppercase rounded shadow-md hover:bg-blue-900 hover:shadow-lg
        focus:bg-blue-900 focus:shadow-lg focus:outline-none focus:ring-0 active:bg-blue-900 
          active:shadow-lg transition duration-150 ease-in-out"
        >
          Load More
        </button>
      </div>
    </>
  )
}

const Card = ({ product }) => (
  <div className="flex flex-col justify-center items-center sm:items-start my-5 w-full sm:w-1/4">
    <Link to={'/product/' + product.id}>
      <img
        className="h-56 w-56 object-cover"
        src={product.imageURL}
        alt={product.name}
      />
      <h4 className="text-lg font-bold">{truncate(product.name, 20, 0, 23)}</h4>
    </Link>

    <div className="flex flex-row sm:flex-col justify-between items-start w-56">
      <div className="flex justify-start items-center">
        <FaEthereum size={15} />
        <span className="font-semibold">{product.price}</span>
      </div>

      <span className="text-sm text-gray-500">{product.stock} in stock</span>
    </div>
  </div>
)

export default Cards

Details Component

This component displays the details of a specific game item such as the full name, image, description, seller details, and so on. Also, this component contains essential buttons for editing, adding items to the cart, deleting, and chatting with the seller button. See the codes below.

import Identicon from 'react-identicons'
import { FaEthereum } from 'react-icons/fa'
import { useNavigate, Link } from 'react-router-dom'
import { setGlobalState, truncate, useGlobalState } from '../store'
import { addToCart } from '../Cart.Service'
import { useEffect, useState } from 'react'
import { getUser } from '../Chat.Service'
import { toast } from 'react-toastify'

const Details = ({ product }) => {
  const navigate = useNavigate()
  const [connectedAccount] = useGlobalState('connectedAccount')
  const [currentUser] = useGlobalState('currentUser')
  const [seller, setSeller] = useState(false)

  const handleChat = () => {
    if (currentUser) {
      if (seller) {
        navigate('/chat/' + product.seller)
      } else {
        toast('Seller not registered for chat yet!')
      }
    } else {
      setGlobalState('chatModal', 'scale-100')
    }
  }

  const handleEdit = () => {
    setGlobalState('product', product)
    setGlobalState('updateModal', 'scale-100')
  }

  const handleDelete = () => {
    setGlobalState('product', product)
    setGlobalState('deleteModal', 'scale-100')
  }

  useEffect(async () => {
    await getUser(product.seller).then((user) => {
      if (user.name) setSeller(user.uid == product.seller)
    })
  }, [])

  return (
    <div
      className="flex flex-col lg:flex-row justify-center lg:justify-between 
      items-center lg:space-x-10 md:w-2/3 w-full p-5 mx-auto"
    >
      <img
        className="h-56 w-56 object-cover mb-5 lg:mb-0"
        src={product.imageURL}
        alt={product.name}
      />
      <div className="flex flex-col justify-between  items-start lg:items-center text-center lg:text-left">
        <div className="flex flex-col space-y-4 mb-5">
          <h4 className="text-3xl font-bold">{product.name}</h4>
          <p className="text-gray-500">{product.description}</p>

          <div className="flex justify-center lg:justify-between space-x-2 items-center">
            <Link
              to={'/seller/0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266'}
              className="flex justify-start items-center space-x-2"
            >
              <Identicon
                string={product.seller}
                size={25}
                className="h-10 w-10 object-contain rounded-full cursor-pointer"
              />
              <small className="font-bold">
                {truncate(product.seller, 4, 4, 11)}
              </small>
            </Link>

            <span className="text-sm text-gray-500">
              {product.stock} in stock
            </span>
          </div>
        </div>

        <div className="flex justify-start text-center items-center flex-wrap space-x-1 mx-auto lg:ml-0">
          {product.deleted ? null : connectedAccount == product.seller ? (
            <div className="flex justify-start text-center items-center space-x-1">
              <button
                className="px-6 py-2.5 bg-blue-800 text-white font-medium text-xs 
                  leading-tight uppercase rounded shadow-md hover:bg-blue-900 hover:shadow-lg
                  focus:bg-blue-900 focus:shadow-lg focus:outline-none focus:ring-0 active:bg-blue-900 
                  active:shadow-lg transition duration-150 ease-in-out flex justify-start items-center space-x-2"
                onClick={handleEdit}
              >
                <span>Edit Product</span>
              </button>

              <button
                className="px-6 py-2.5 bg-red-800 text-white font-medium text-xs 
                  leading-tight uppercase rounded shadow-md hover:bg-red-900 hover:shadow-lg
                  focus:bg-red-900 focus:shadow-lg focus:outline-none focus:ring-0 active:bg-red-900 
                  active:shadow-lg transition duration-150 ease-in-out flex justify-start items-center space-x-2"
                onClick={handleDelete}
              >
                <span>Delete Product</span>
              </button>
            </div>
          ) : (
            <button
              className="px-6 py-2.5 bg-blue-800 text-white font-medium text-xs 
              leading-tight uppercase rounded shadow-md hover:bg-blue-900 hover:shadow-lg
              focus:bg-blue-900 focus:shadow-lg focus:outline-none focus:ring-0 active:bg-blue-900 
              active:shadow-lg transition duration-150 ease-in-out flex justify-start items-center space-x-2"
              onClick={() => addToCart(product)}
            >
              <span>Add to Cart</span>

              <div className="flex justify-start items-center">
                <FaEthereum size={15} />
                <span className="font-semibold">{product.price}</span>
              </div>
            </button>
          )}
          <button
            className="px-6 py-2.5 bg-transparent border-blue-800 text-blue-800 font-medium text-xs 
            leading-tight uppercase rounded shadow-md hover:bg-blue-900 hover:shadow-lg border
            focus:border-blue-900 focus:shadow-lg focus:outline-none focus:ring-0 active:border-blue-900 
            active:shadow-lg transition duration-150 ease-in-out hover:text-white"
            onClick={handleChat}
          >
            Chat with Seller
          </button>
        </div>
      </div>
    </div>
  )
}

export default Details

Buyers Component

Buyers of a specific game product

This component shows a list of buyers who bought a specific game item. See the codes listed below.

import { FaEthereum } from 'react-icons/fa'
import Identicon from 'react-identicons'
import { truncate } from '../store'

const Buyers = ({ buyers }) => {
  return (
    <div className="flex justify-center flex-col items-start w-full md:w-2/3 p-5 mx-auto">
      <div className="max-h-[calc(100vh_-_20rem)] overflow-y-auto shadow-md rounded-md w-full">
        {buyers.length < 1 ? null : (
          <table className="min-w-full">
            <thead className="border-b">
              <tr>
                <th
                  scope="col"
                  className="text-sm font-medium px-6 py-4 text-left"
                >
                  Buyer
                </th>
                <th
                  scope="col"
                  className="text-sm font-medium px-6 py-4 text-left"
                >
                  Cost
                </th>
                <th
                  scope="col"
                  className="text-sm font-medium px-6 py-4 text-left"
                >
                  Qty
                </th>
                <th
                  scope="col"
                  className="text-sm font-medium px-6 py-4 text-left"
                >
                  Date
                </th>
              </tr>
            </thead>
            <tbody>
              {buyers.map((buyer, i) => (
                <tr
                  key={i}
                  className="border-b border-gray-200 transition duration-300 ease-in-out"
                >
                  <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
                    <div className="flex flex-row justify-start items-center space-x-3">
                      <Identicon
                        string={buyer.buyer}
                        size={25}
                        className="h-10 w-10 object-contain rounded-full mr-3"
                      />
                      <small className="font-bold">
                        {truncate(buyer.buyer, 4, 4, 11)}
                      </small>
                    </div>
                  </td>
                  <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
                    <small className="flex justify-start items-center space-x-1">
                      <FaEthereum />
                      <span className="text-gray-700 font-bold">
                        {buyer.price} EHT
                      </span>
                    </small>
                  </td>

                  <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
                    <span className="text-gray-700 font-bold">{buyer.qty}</span>
                  </td>
                  <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
                    {buyer.timestamp}
                  </td>
                </tr>
              ))}
            </tbody>
          </table>
        )}
      </div>
    </div>
  )
}

export default Buyers

Orders Component

This component renders a collection of orders for both the buyer and the seller, giving the buyer the ability to cancel an order so long as it isn’t delivered, and the seller, the ability to deliver a game product. See the codes below.

import { Link } from 'react-router-dom'
import { FaEthereum } from 'react-icons/fa'
import { cancelOrder, delieverOrder } from '../Blockchain.Service'
import { useGlobalState } from '../store'
import { toast } from 'react-toastify'

const DELEVIRED = 1
const CANCELED = 2

const onDeliver = async (pid, id) => {
  await toast.promise(
    new Promise(async (resolve, reject) => {
      await delieverOrder(pid, id)
        .then(() => resolve())
        .catch(() => reject())
    }),
    {
      pending: 'Approve transaction...',
      success:
        'Order delivered, will reflect in your Order history within 30sec 🙌',
      error: 'Encountered error placing order 🤯',
    },
  )
}

const onCancel = async (pid, id) => {
  await toast.promise(
    new Promise(async (resolve, reject) => {
      await cancelOrder(pid, id)
        .then(() => resolve())
        .catch(() => reject())
    }),
    {
      pending: 'Approve transaction...',
      success:
        'Order delivered, will reflect in your Order history within 30sec 🙌',
      error: 'Encountered error placing order 🤯',
    },
  )
}

const Order = ({ orders, title, seller }) => {
  const [connectedAccount] = useGlobalState('connectedAccount')

  return (
    <div className="flex flex-col justify-between items-center space-x-2 md:w-2/3 w-full p-5 mx-auto">
      <h4 className="text-center uppercase mb-8">{title}</h4>

      <table className="min-w-full hidden md:table">
        <thead className="border-b">
          <tr>
            <th scope="col" className="text-sm font-medium px-6 py-4 text-left">
              S/N
            </th>
            <th scope="col" className="text-sm font-medium px-6 py-4 text-left">
              Product
            </th>
            <th scope="col" className="text-sm font-medium px-6 py-4 text-left">
              Qty
            </th>
            <th scope="col" className="text-sm font-medium px-6 py-4 text-left">
              Price
            </th>
            <th scope="col" className="text-sm font-medium px-6 py-4 text-left">
              Status
            </th>
            <th scope="col" className="text-sm font-medium px-6 py-4 text-left">
              Total
            </th>
          </tr>
        </thead>
        <tbody>
          {seller
            ? orders.map((order, i) =>
                order.seller == connectedAccount ? (
                  <SellerOrder key={i} order={order} i={i} />
                ) : null,
              )
            : orders.map((order, i) =>
                order.buyer == connectedAccount ? (
                  <BuyerOrder key={i} order={order} i={i} />
                ) : null,
              )}
        </tbody>
      </table>

      <div className="flex flex-col justify-center items-center w-full md:hidden">
        {seller
          ? orders.map((order, i) =>
              order.seller == connectedAccount ? (
                <MobileSellerOrder key={i} order={order} i={i} />
              ) : null,
            )
          : orders.map((order, i) =>
              order.buyer == connectedAccount ? (
                <MobileBuyerOrder key={i} order={order} i={i} />
              ) : null,
            )}
      </div>
    </div>
  )
}

const SellerOrder = ({ order, i }) => (
  <tr className="border-b border-gray-200 transition duration-300 ease-in-out">
    <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
      <span className="text-gray-700 font-bold">{i + 1}</span>
    </td>

    <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
      <Link to={'/product/' + order.pid}>
        <img className="w-20" src={order.imageURL} alt="game" />
        <small className="font-bold">{order.name}</small>
      </Link>
    </td>

    <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
      <span className="text-gray-700 font-bold">{order.qty}</span>
    </td>

    <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
      <small className="flex justify-start items-center space-x-1">
        <FaEthereum />
        <span className="text-gray-700 font-bold">
          {(order.total / order.qty).toFixed(3)} EHT
        </span>
      </small>
    </td>

    {order.status == DELEVIRED ? (
      <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
        <span className="text-green-500">Delievered</span>
      </td>
    ) : order.status == CANCELED ? (
      <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
        <span className="text-red-500">Canceled</span>
      </td>
    ) : (
      <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
        <button
          type="button"
          className="rounded inline-block px-4 py-1.5 bg-green-600 text-white
                font-medium text-xs leading-tight uppercase hover:bg-green-700 
                focus:bg-green-700 focus:outline-none focus:ring-0 active:bg-green-800
                transition duration-150 ease-in-out"
          onClick={() => onDeliver(order.pid, order.id)}
        >
          Deliever
        </button>
      </td>
    )}

    <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
      <small className="flex justify-start items-center space-x-1">
        <FaEthereum />
        <span className="text-gray-700 font-bold">{order.total} EHT</span>
      </small>
    </td>
  </tr>
)

const BuyerOrder = ({ order, i }) => (
  <tr className="border-b border-gray-200 transition duration-300 ease-in-out">
    <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
      <span className="text-gray-700 font-bold">{i + 1}</span>
    </td>

    <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
      <Link to={'/product/' + order.pid}>
        <img className="w-20" src={order.imageURL} alt="game" />
        <small className="font-bold">{order.name}</small>
      </Link>
    </td>

    <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
      <span className="text-gray-700 font-bold">{order.qty}</span>
    </td>

    <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
      <small className="flex justify-start items-center space-x-1">
        <FaEthereum />
        <span className="text-gray-700 font-bold">
          {(order.total / order.qty).toFixed(3)} EHT
        </span>
      </small>
    </td>

    {order.status == DELEVIRED ? (
      <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
        <span className="text-green-500">Delievered</span>
      </td>
    ) : order.status == CANCELED ? (
      <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
        <span className="text-red-500">Canceled</span>
      </td>
    ) : (
      <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
        <button
          type="button"
          className="rounded inline-block px-4 py-1.5 bg-blue-600 text-white
              font-medium text-xs leading-tight uppercase hover:bg-blue-700 
              focus:bg-blue-700 focus:outline-none focus:ring-0 active:bg-blue-800
              transition duration-150 ease-in-out"
          onClick={() => onCancel(order.pid, order.id)}
        >
          Cancel
        </button>
      </td>
    )}
    <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
      <small className="flex justify-start items-center space-x-1">
        <FaEthereum />
        <span className="text-gray-700 font-bold">{order.total} EHT</span>
      </small>
    </td>
  </tr>
)

const MobileSellerOrder = ({ order, i }) => (
  <div
    className="flex flex-col justify-center items-center  my-4
    transition duration-300 ease-in-out border-b border-gray-200"
  >
    <div className="flex justify-center">
      <span className="text-gray-700 font-bold text-sm">#{i + 1}</span>
    </div>

    <Link
      to={'/product/' + order.pid}
      className="flex flex-col justify-center items-center space-y-2 text-sm font-light"
    >
      <img className="w-1/3 md:w-2/3" src={order.imageURL} alt="game" />
      <small className="font-bold">{order.name}</small>
    </Link>

    <div className="text-sm font-light">
      <small className="flex justify-start items-center space-x-1">
        <FaEthereum />
        <span className="text-gray-700 font-bold">
          {order.qty} x {order.total / order.qty} EHT = {order.total} EHT
        </span>
      </small>
    </div>

    {order.status == DELEVIRED ? (
      <div className="text-sm font-light mt-2 mb-4">
        <span
          className="px-4 py-2 rounded-full text-green-500 bg-green-200 font-semibold
          text-sm flex align-center w-max cursor-pointer active:bg-gray-300
          transition duration-300 ease"
        >
          Delievered
        </span>
      </div>
    ) : order.status == CANCELED ? (
      <div className="text-sm font-light mt-2 mb-4">
        <span
          className="px-4 py-2 rounded-full text-red-500 bg-red-200 font-semibold
          text-sm flex align-center w-max cursor-pointer active:bg-gray-300
          transition duration-300 ease"
        >
          Canceled
        </span>
      </div>
    ) : (
      <div className="text-sm font-light mt-2 mb-4">
        <button
          type="button"
          className="rounded inline-block px-4 py-1.5 bg-green-600 text-white
                font-medium text-xs leading-tight uppercase hover:bg-green-700 
                focus:bg-green-700 focus:outline-none focus:ring-0 active:bg-green-800
                transition duration-150 ease-in-out"
          onClick={() => onDeliver(order.pid, order.id)}
        >
          Deliever
        </button>
      </div>
    )}
  </div>
)

const MobileBuyerOrder = ({ order, i }) => (
  <div
    className="flex flex-col justify-center items-center  my-4
    transition duration-300 ease-in-out border-b border-gray-200"
  >
    <div className="flex justify-center">
      <span className="text-gray-700 font-bold text-sm">#{i + 1}</span>
    </div>

    <Link
      to={'/product/' + order.pid}
      className="flex flex-col justify-center items-center space-y-2 text-sm font-light"
    >
      <img className="w-3/5" src={order.imageURL} alt="game" />
      <small className="font-bold">{order.name}</small>
    </Link>

    <div className="text-sm font-light">
      <small className="flex justify-start items-center space-x-1">
        <FaEthereum />
        <span className="text-gray-700 font-bold">
          {order.qty} x {order.total / order.qty} EHT = {order.total} EHT
        </span>
      </small>
    </div>

    {order.status == DELEVIRED ? (
      <div className="text-sm font-light mt-2 mb-4">
        <span
          className="px-4 py-2 rounded-full text-green-500 bg-green-200 font-semibold
          text-sm flex align-center w-max cursor-pointer active:bg-gray-300
          transition duration-300 ease"
        >
          Delievered
        </span>
      </div>
    ) : order.status == CANCELED ? (
      <div className="text-sm font-light mt-2 mb-4">
        <span
          className="px-4 py-2 rounded-full text-red-500 bg-red-200 font-semibold
          text-sm flex align-center w-max cursor-pointer active:bg-gray-300
          transition duration-300 ease"
        >
          Canceled
        </span>
      </div>
    ) : (
      <div className="text-sm font-light mt-2 mb-4">
        <button
          type="button"
          className="rounded inline-block px-4 py-1.5 bg-green-600 text-white
                font-medium text-xs leading-tight uppercase hover:bg-green-700 
                focus:bg-green-700 focus:outline-none focus:ring-0 active:bg-green-800
                transition duration-150 ease-in-out"
          onClick={() => onCancel(order.pid, order.id)}
        >
          Cancel
        </button>
      </div>
    )}
  </div>
)

export default Order

Adding a Game to The Shop

To add a new game to our shop we use two components, the “AddButton” and the “CreateProduct” component. The “AddButton” is responsible for launching the create product modal. Create each one of these components in the components folder and paste the following codes inside them. see the codes below.

import { BsPlusLg } from 'react-icons/bs'
import { setGlobalState } from '../store'

const AddButton = () => {
  return (
    <div className="fixed right-10 bottom-10 flex space-x-2 justify-center">
      <div>
        <button
          type="button"
          className="flex justify-center items-center rounded-full bg-blue-600
          text-white leading-normal uppercase shadow-md hover:bg-blue-700
          hover:shadow-lg focus:bg-blue-700 focus:shadow-lg focus:outline-none
          focus:ring-0 active:bg-blue-800 active:shadow-lg transition
          duration-150 ease-in-out w-9 h-9"
          onClick={() => setGlobalState('modal', 'scale-100')}
        >
          <BsPlusLg className="font-bold" size={20} />
        </button>
      </div>
    </div>
  )
}

export default AddButton
import { useEffect, useState } from 'react'
import { FaTimes } from 'react-icons/fa'
import { createProduct } from '../Blockchain.Service'
import { setGlobalState, useGlobalState } from '../store'
import { toast } from 'react-toastify'
import { getUser } from '../Chat.Service'

const CreateProduct = () => {
  const [modal] = useGlobalState('modal')
  const [connectedAccount] = useGlobalState('connectedAccount')
  const [name, setName] = useState('')
  const [price, setPrice] = useState('')
  const [stock, setStock] = useState('')
  const [description, setDescription] = useState('')
  const [imageURL, setImageURL] = useState('')
  const [seller, setSeller] = useState(false)

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

    if (!name || !price || !imageURL || !description || !stock) return
    const params = {
      sku: (Math.random() + 1).toString(36).substring(7).toUpperCase(),
      name,
      description,
      stock,
      price,
      imageURL,
    }

    await toast.promise(
      new Promise(async (resolve, reject) => {
        await createProduct(params)
          .then(() => resolve())
          .catch(() => reject())
      }),
      {
        pending: 'Approve transaction to product...',
        success: 'Product successfully created, will reflect within 30sec 👌',
        error: 'Encountered error updating your product 🤯',
      },
    )

    closeModal()

    if(!seller) toast("Please sign in to have your customers chat with you.")
  }

  useEffect(async () => {
    await getUser(connectedAccount).then((user) => {
      if (user.name) setSeller(user.uid == connectedAccount)
    })
  }, [])

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

  const resetForm = () => {
    setImageURL('')
    setName('')
    setPrice('')
    setStock('')
    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-white shadow-xl shadow-black 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-black">Add Product</p>
            <button
              type="button"
              onClick={closeModal}
              className="border-0 bg-transparent focus:outline-none"
            >
              <FaTimes className="text-black" />
            </button>
          </div>

          {imageURL ? (
            <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="Project"
                  className="h-full w-full object-cover cursor-pointer"
                  src={imageURL}
                />
              </div>
            </div>
          ) : null}

          <div className="flex flex-row justify-between items-center bg-gray-300 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="name"
              placeholder="Title"
              onChange={(e) => setName(e.target.value)}
              value={name}
              required
            />
          </div>

          <div className="flex flex-row justify-between items-center bg-gray-300 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.001}
              min={0.001}
              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-300 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"
              min={1}
              name="stock"
              placeholder="E.g. 2"
              onChange={(e) => setStock(e.target.value)}
              value={stock}
              required
            />
          </div>

          <div className="flex flex-row justify-between items-center bg-gray-300 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="url"
              name="imageURL"
              placeholder="ImageURL"
              onChange={(e) => setImageURL(e.target.value)}
              pattern="^(http(s)?:\/\/)+[\w\-\._~:\/?#[\]@!\$&'\(\)\*\+,;=.]+$"
              value={imageURL}
              required
            />
          </div>

          <div className="flex flex-row justify-between items-center bg-gray-300 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"
            className="flex flex-row justify-center items-center
              w-full text-white text-md bg-blue-500
              py-2 px-5 rounded-full drop-shadow-xl
              border-transparent border
              hover:bg-transparent hover:text-blue-500
              hover:border hover:border-blue-500
              focus:outline-none focus:ring mt-5"
          >
            Create Product
          </button>
        </form>
      </div>
    </div>
  )
}

export default CreateProduct

The Administrative Components

Launched by the Edit Product Button

This component includes the edit, delete, and chat with seller components. The ability to edit or delete a product is solely the responsibility of the owner of such a product.

Launched by the Delete Button

For the chat with seller button, both the seller and the buyer must willfully sign up for this service to be able to receive anonymous chats from the buyers. Then he will be able to see them in his chat history.

Launched by the Chat with Seller Button

The logic for each of these components is contained in the codes below; create and paste the codes into their respective components.

import { FaTimes } from 'react-icons/fa'
import { setGlobalState, useGlobalState } from '../store'
import { loginWithCometChat, signUpWithCometChat } from '../Chat.Service'
import { toast } from 'react-toastify'

const ChatModal = () => {
  const [chatModal] = useGlobalState('chatModal')
  const [connectedAccount] = useGlobalState('connectedAccount')

  const handleLogin = async () => {
    await toast.promise(
      new Promise(async (resolve, reject) => {
        await loginWithCometChat(connectedAccount)
          .then((res) => res == true ? resolve() : reject())
          .catch(() => reject())
      }),
      {
        pending: 'Signing in...',
        success: 'Successfully signed in 👌',
        error: 'Encountered error while signing in 🤯',
      },
    )

    closeModal()
  }

  const handleSignup = async () => {
    await toast.promise(
      new Promise(async (resolve, reject) => {
        await signUpWithCometChat(connectedAccount, connectedAccount)
          .then((res) => res == true ? resolve() : reject())
          .catch(() => reject())
      }),
      {
        pending: 'Signing up...',
        success: 'Successfully signed up, proceed to login... 👌',
        error: 'Encountered error while signing up 🤯',
      },
    )

    closeModal()
  }

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

  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 ${chatModal}`}
    >
      <div className="bg-white shadow-xl shadow-black 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-end items-center">
            <button
              type="button"
              onClick={closeModal}
              className="border-0 bg-transparent focus:outline-none"
            >
              <FaTimes className="text-black" />
            </button>
          </div>
          <ChatAuth login={handleLogin} sign={handleSignup} />
        </div>
      </div>
    </div>
  )
}

const ChatAuth = ({ login, sign }) => (
  <>
    <div className="flex flex-col justify-center items-center text-center">
      <h4 className="text-xl text-bold mb-3">Authentication</h4>
      <p>
        You will have to sign up or login to access the chat features of this
        app.
      </p>
    </div>

    <div className="flex justify-center items-center space-x-3 text-center mt-5">
      <button
        type="submit"
        onClick={login}
        className="flex flex-row justify-center items-center w-full 
              text-white text-md bg-blue-900
              py-2 px-5 rounded-full drop-shadow-xl
              border-transparent border
              hover:bg-transparent hover:text-blue-900
              hover:border hover:border-blue-900
              focus:outline-none focus:ring mt-5"
      >
        Login
      </button>

      <button
        type="submit"
        onClick={sign}
        className="flex flex-row justify-center items-center w-full 
              text-blue-900 text-md border-blue-900
              py-2 px-5 rounded-full drop-shadow-xl
              border-transparent border
              hover:text-white
              hover:border hover:bg-blue-900
              focus:outline-none focus:ring mt-5"
      >
        Sign Up
      </button>
    </div>
  </>
)

export default ChatModal
import { FaTimes } from 'react-icons/fa'
import { setGlobalState, useGlobalState } from '../store'
import { deleteProduct } from '../Blockchain.Service'
import { toast } from 'react-toastify'

const DeleteProduct = () => {
  const [deleteModal] = useGlobalState('deleteModal')
  const [product] = useGlobalState('product')

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

    await toast.promise(
      new Promise(async (resolve, reject) => {
        await deleteProduct(product?.id)
          .then(() => resolve())
          .catch(() => reject())
      }),
      {
        pending: 'Approving transaction...',
        success: 'Product deleted, will reflect within 30sec 👌',
        error: 'Encountered error deleting your product 🤯',
      },
    )

    closeModal()
  }

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

  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 ${deleteModal}`}
    >
      <div className="bg-white shadow-xl shadow-black 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-end items-center">
            <button
              type="button"
              onClick={closeModal}
              className="border-0 bg-transparent focus:outline-none"
            >
              <FaTimes className="text-black" />
            </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="Product"
                className="h-full w-full object-cover cursor-pointer"
                src={product?.imageURL}
              />
            </div>
          </div>

          <div className="flex flex-col justify-center items-center  text-center mt-5">
            <p>
              You are about to delete <strong>"{product?.name}"</strong>{' '}
              permanently!
            </p>
            <small className="text-red-400">Are you sure?</small>
          </div>

          <button
            type="submit"
            onClick={handleDelete}
            className="flex flex-row justify-center items-center w-full 
              text-white text-md bg-red-500
              py-2 px-5 rounded-full drop-shadow-xl
              border-transparent border
              hover:bg-transparent hover:text-red-500
              hover:border hover:border-red-500
              focus:outline-none focus:ring mt-5"
          >
            Delete Product
          </button>
        </div>
      </div>
    </div>
  )
}

export default DeleteProduct
import { useEffect, useState } from 'react'
import { FaTimes } from 'react-icons/fa'
import { updateProduct } from '../Blockchain.Service'
import { setGlobalState, useGlobalState } from '../store'
import { toast } from 'react-toastify'

const UpateProduct = () => {
  const [modal] = useGlobalState('updateModal')
  const [product] = useGlobalState('product')
  const [name, setName] = useState(product?.name)
  const [price, setPrice] = useState(product?.price)
  const [stock, setStock] = useState(product?.stock)
  const [oldStock, setOldStock] = useState(product?.stock)
  const [description, setDescription] = useState(product?.description)
  const [imageURL, setImageURL] = useState(product?.imageURL)

  useEffect(() => {
    setName(product?.name)
    setDescription(product?.description)
    setPrice(product?.price)
    setStock(product?.stock)
    setImageURL(product?.imageURL)
  }, [product])

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

    if (
      !name ||
      !price ||
      !imageURL ||
      !description ||
      !stock ||
      stock < oldStock
    )
      return
    const params = {
      id: product.id,
      name,
      description,
      stock,
      price,
      imageURL,
    }

    await toast.promise(
      new Promise(async (resolve, reject) => {
        await updateProduct(params)
          .then(() => resolve())
          .catch(() => reject())
      }),
      {
        pending: 'Approve transaction to product...',
        success: 'Product successfully updated, will reflect within 30sec 🦄',
        error: 'Encountered error updating your product 🤯',
      },
    )

    closeModal()
    console.log('Product updated')
  }

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

  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 z-50 ${modal}`}
    >
      <div className="bg-white shadow-xl shadow-black 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-black">Edit Product</p>
            <button
              type="button"
              onClick={closeModal}
              className="border-0 bg-transparent focus:outline-none"
            >
              <FaTimes className="text-black" />
            </button>
          </div>

          {imageURL ? (
            <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="Project"
                  className="h-full w-full object-cover cursor-pointer"
                  src={imageURL}
                />
              </div>
            </div>
          ) : null}

          <div className="flex flex-row justify-between items-center bg-gray-300 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="name"
              placeholder="Title"
              onChange={(e) => setName(e.target.value)}
              value={name || ''}
              required
            />
          </div>

          <div className="flex flex-row justify-between items-center bg-gray-300 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.001}
              min={0.001}
              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-300 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"
              min={1}
              name="stock"
              placeholder="E.g. 2"
              onChange={(e) => setStock(e.target.value)}
              value={stock || ''}
              required
            />
          </div>

          <div className="flex flex-row justify-between items-center bg-gray-300 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="url"
              name="imageURL"
              placeholder="ImageURL"
              onChange={(e) => setImageURL(e.target.value)}
              pattern="^(http(s)?:\/\/)+[\w\-\._~:\/?#[\]@!\$&'\(\)\*\+,;=.]+$"
              value={imageURL || ''}
              required
            />
          </div>

          <div className="flex flex-row justify-between items-center bg-gray-300 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"
            className="flex flex-row justify-center items-center
              w-full text-white text-md bg-blue-500
              py-2 px-5 rounded-full drop-shadow-xl
              border-transparent border
              hover:bg-transparent hover:text-blue-500
              hover:border hover:border-blue-500
              focus:outline-none focus:ring mt-5"
          >
            Update Product
          </button>
        </form>
      </div>
    </div>
  )
}

export default UpateProduct

The Menu Component

This component is in charge of directing users to other areas of the application, such as your order and sales history, recent customer chats, and statistics. See the code for the component below.

import { FaTimes } from 'react-icons/fa'
import { useNavigate } from 'react-router-dom'
import { setGlobalState, useGlobalState } from '../store'

const Menu = () => {
  const [menu] = useGlobalState('menu')
  const navigate = useNavigate()

  const navTo = (route) => {
    setGlobalState('menu', 'scale-0')
    navigate(route)
  }

  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 ${menu}`}
    >
      <div className="bg-white shadow-xl shadow-black 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-black">Account</p>
            <button
              type="button"
              className="border-0 bg-transparent focus:outline-none"
              onClick={() => setGlobalState('menu', 'scale-0')}
            >
              <FaTimes className="text-black" />
            </button>
          </div>

          <div className="flex justify-start mt-4">
            <button
              type="button"
              data-mdb-ripple="true"
              data-mdb-ripple-color="light"
              className="px-6 py-2.5 bg-white text-black font-medium text-xs leading-tight
                uppercase rounded shadow-md hover:bg-blue-700 hover:shadow-lg focus:bg-blue-700
                focus:shadow-lg focus:outline-none focus:ring-0 active:bg-blue-800 hover:text-white
                active:shadow-lg transition duration-150 ease-in-out w-full text-left"
              onClick={() => navTo('/orders')}
            >
              Order History
            </button>
          </div>
          
          <div className="flex justify-start mt-4">
            <button
              type="button"
              data-mdb-ripple="true"
              data-mdb-ripple-color="light"
              className="px-6 py-2.5 bg-white text-black font-medium text-xs leading-tight
                uppercase rounded shadow-md hover:bg-blue-700 hover:shadow-lg focus:bg-blue-700
                focus:shadow-lg focus:outline-none focus:ring-0 active:bg-blue-800 hover:text-white
                active:shadow-lg transition duration-150 ease-in-out w-full text-left"
              onClick={() => navTo('/sales')}
            >
              Sales History
            </button>
          </div>

          <div className="flex justify-start mt-4">
            <button
              type="button"
              data-mdb-ripple="true"
              data-mdb-ripple-color="light"
              className="px-6 py-2.5 bg-white text-black font-medium text-xs leading-tight
                uppercase rounded shadow-md hover:bg-blue-700 hover:shadow-lg focus:bg-blue-700
                focus:shadow-lg focus:outline-none focus:ring-0 active:bg-blue-800 hover:text-white
                active:shadow-lg transition duration-150 ease-in-out w-full text-left"
              onClick={() => navTo('/recents')}
            >
              Recent Chats
            </button>
          </div>

          <div className="flex justify-start mt-4">
            <button
              type="button"
              data-mdb-ripple="true"
              data-mdb-ripple-color="light"
              className="px-6 py-2.5 bg-white text-black font-medium text-xs leading-tight
                uppercase rounded shadow-md hover:bg-blue-700 hover:shadow-lg focus:bg-blue-700
                focus:shadow-lg focus:outline-none focus:ring-0 active:bg-blue-800 hover:text-white
                active:shadow-lg transition duration-150 ease-in-out w-full text-left"
            onClick={() => navTo('/seller/0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266')}
            >
              My Products
            </button>
          </div>

          <div className="flex justify-start mt-4">
            <button
              type="button"
              data-mdb-ripple="true"
              data-mdb-ripple-color="light"
              className="px-6 py-2.5 bg-white text-black font-medium text-xs leading-tight
                uppercase rounded shadow-md hover:bg-blue-700 hover:shadow-lg focus:bg-blue-700
                focus:shadow-lg focus:outline-none focus:ring-0 active:bg-blue-800 hover:text-white
                active:shadow-lg transition duration-150 ease-in-out w-full text-left"
              onClick={() => navTo('/stats/0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266')}
            >
              My Stats
            </button>
          </div>
        </div>
      </div>
    </div>
  )
}

export default Menu

The Shopping Cart Components

The cart component has a highly responsive design as well as an instant price calibrator. See the codes listed below.

import { useEffect, useState } from 'react'
import { FaEthereum } from 'react-icons/fa'
import { Link } from 'react-router-dom'
import { remFromCart, updateCart } from '../Cart.Service'
import Summary from './Summary'

const Cart = ({ cart, summary }) => {
  const [cartItems, setCartItems] = useState([])
  const [process, setProcess] = useState(false)

  const increase = (product) => {
    product.qty++
    updateCart(product)
    setCartItems(cart)
    setProcess(!process)
  }

  const decrease = (product) => {
    if (product.qty == 1) {
      remFromCart(product)
    } else {
      product.qty--
      updateCart(product)
    }
    setCartItems(cart)
    setProcess(!process)
  }

  useEffect(() => {
    setCartItems(cart)
  }, [process])

  return (
    <>
      <div className="flex flex-col justify-between items-center space-x-2 md:w-2/3 w-full p-5 mx-auto">
        <h4 className="text-center uppercase mb-8">Shopping Cart</h4>

        <table className="min-w-full hidden md:table">
          <thead className="border-b">
            <tr>
              <th
                scope="col"
                className="text-sm font-medium px-6 py-4 text-left"
              >
                S/N
              </th>
              <th
                scope="col"
                className="text-sm font-medium px-6 py-4 text-left"
              >
                Product
              </th>
              <th
                scope="col"
                className="text-sm font-medium px-6 py-4 text-left"
              >
                Qty
              </th>
              <th
                scope="col"
                className="text-sm font-medium px-6 py-4 text-left"
              >
                Price
              </th>
              <th
                scope="col"
                className="text-sm font-medium px-6 py-4 text-left"
              >
                Action
              </th>
              <th
                scope="col"
                className="text-sm font-medium px-6 py-4 text-left"
              >
                Total
              </th>
            </tr>
          </thead>

          <tbody>
            {cartItems.map((product, i) => (
              <tr
                key={i}
                className="border-b border-gray-200 transition duration-300 ease-in-out"
              >
                <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
                  <span className="text-gray-700 font-bold">{i + 1}</span>
                </td>

                <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
                  <Link to={'/product/' + product.id}>
                    <img className="w-20" src={product.imageURL} alt="game" />
                    <small className="font-bold">{product.name}</small>
                  </Link>
                </td>

                <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
                  <div
                    className="inline-flex shadow-md hover:shadow-lg focus:shadow-lg"
                    role="group"
                  >
                    <button
                      type="button"
                      className="rounded-l inline-block px-4 py-1.5 bg-blue-600 text-white font-medium
                      text-xs leading-tight uppercase hover:bg-blue-700 focus:bg-blue-700 focus:outline-none
                      focus:ring-0 active:bg-blue-800 transition duration-150 ease-in-out"
                      onClick={() => decrease(product)}
                    >
                      -
                    </button>
                    <button
                      type="button"
                      className=" inline-block px-4 py-1.5 bg-transparent text-black font-medium
                      text-xs leading-tight uppercase focus:outline-none
                      focus:ring-0 transition duration-150 ease-in-out"
                    >
                      {product.qty}
                    </button>
                    <button
                      type="button"
                      className=" rounded-r inline-block px-4 py-1.5 bg-blue-600 text-white font-medium
                      text-xs leading-tight uppercase hover:bg-blue-700 focus:bg-blue-700 focus:outline-none
                      focus:ring-0 active:bg-blue-800 transition duration-150 ease-in-out"
                      onClick={() => increase(product)}
                    >
                      +
                    </button>
                  </div>
                </td>

                <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
                  <small className="flex justify-start items-center space-x-1">
                    <FaEthereum />
                    <span className="text-gray-700 font-bold">
                      {product.price} EHT
                    </span>
                  </small>
                </td>

                <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
                  <button
                    type="button"
                    className="inline-block px-6 py-2.5 bg-transparent text-red-600 font-medium
                text-xs leading-tight uppercase rounded hover:text-red-700
                hover:bg-gray-100 focus:bg-gray-100 focus:outline-none focus:ring-0
                active:bg-gray-200 transition duration-150 ease-in-out"
                    onClick={() => remFromCart(product)}
                  >
                    Remove
                  </button>
                </td>
                <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
                  <small className="flex justify-start items-center space-x-1">
                    <FaEthereum />
                    <span className="text-gray-700 font-bold">
                      {(product.qty * product.price).toFixed(3)} EHT
                    </span>
                  </small>
                </td>
              </tr>
            ))}
          </tbody>
        </table>

        <div className="flex flex-col justify-center items-center space-y-2 w-full md:hidden">
          {cartItems.map((product, i) => (
            <div
              key={i}
              className="flex flex-col justify-center items-center my-4 space-y-2
              border-b border-gray-200 transition duration-300 ease-in-out"
            >
              <Link
                to={'/product/' + product.id}
                className="flex flex-col justify-center items-center space-y-2 text-sm font-light"
              >
                <img
                  className="w-1/3 md:w-2/3"
                  src={product.imageURL}
                  alt="game"
                />
                <small className="font-bold">{product.name}</small>
              </Link>

              <div className="flex justify-center">
                <div
                  className="inline-flex shadow-md hover:shadow-lg focus:shadow-lg"
                  role="group"
                >
                  <button
                    type="button"
                    className="rounded-l inline-block px-4 py-1.5 bg-blue-600 text-white font-medium
                      text-xs leading-tight uppercase hover:bg-blue-700 focus:bg-blue-700 focus:outline-none
                      focus:ring-0 active:bg-blue-800 transition duration-150 ease-in-out"
                    onClick={() => decrease(product)}
                  >
                    -
                  </button>
                  <button
                    type="button"
                    className=" inline-block px-4 py-1.5 bg-transparent text-black font-medium
                      text-xs leading-tight uppercase focus:outline-none
                      focus:ring-0 transition duration-150 ease-in-out"
                  >
                    {product.qty}
                  </button>
                  <button
                    type="button"
                    className=" rounded-r inline-block px-4 py-1.5 bg-blue-600 text-white font-medium
                      text-xs leading-tight uppercase hover:bg-blue-700 focus:bg-blue-700 focus:outline-none
                      focus:ring-0 active:bg-blue-800 transition duration-150 ease-in-out"
                    onClick={() => increase(product)}
                  >
                    +
                  </button>
                </div>
              </div>

              <div className="text-sm font-light">
                <small className="flex justify-start items-center space-x-1">
                  <FaEthereum />
                  <span className="text-gray-700 font-bold">
                    {(product.qty * product.price).toFixed(3)} EHT
                  </span>
                </small>
              </div>

              <div className="text-sm font-light mb-4">
                <button
                  type="button"
                  className="inline-block px-6 py-2.5 bg-transparent text-red-600 font-medium
                text-xs leading-tight uppercase rounded hover:text-red-700
                hover:bg-gray-100 focus:bg-gray-100 focus:outline-none focus:ring-0
                active:bg-gray-200 transition duration-150 ease-in-out"
                  onClick={() => remFromCart(product)}
                >
                  Remove
                </button>
              </div>
            </div>
          ))}
        </div>
      </div>
      <Summary summary={summary} />
    </>
  )
}

export default Cart

The Summary Component

This component enables you to supply the address and phone number of where you want the item shipped. See the codes below.

import { FaEthereum } from 'react-icons/fa'
import { useState } from 'react'
import { createOrder } from '../Blockchain.Service'
import { clearCart } from '../Cart.Service'
import { toast } from 'react-toastify'

const Summary = ({ summary }) => {
  const [destination, setDestination] = useState('')
  const [phone, setPhone] = useState('')

  const handleCheckout = async (e) => {
    e.preventDefault()
    if (!phone || !destination) return

    const params = { phone, destination, ...summary }

    await toast.promise(
      new Promise(async (resolve, reject) => {
        await createOrder(params)
          .then(() => {
            onReset()
            clearCart()
            resolve()
          })
          .catch(() => reject())
      }),
      {
        pending: 'Approve transaction...',
        success:
          'Order placed, will reflect in your Order history within 30sec 🙌',
        error: 'Encountered error placing order 🤯',
      },
    )
  }

  const onReset = () => {
    setDestination('')
    setPhone('')
  }

  return (
    <div
      className="flex flex-col md:flex-row justify-center md:justify-between
      items-center flex-wrap space-x-2 md:w-2/3 w-full p-5 mx-auto"
    >
      <form className="w-4/5 md:w-2/5 my-2">
        <div className="mb-3">
          <label className="form-label inline-block mb-2 font-bold text-sm text-gray-700">
            Destination
          </label>

          <input
            type="text"
            className="form-control block w-full px-3 py-1.5 text-base font-normal
            text-gray-700 bg-white bg-clip-padding border border-solid border-gray-300
            rounded transition ease-in-out m-0 focus:text-gray-700 focus:bg-white
            focus:border-blue-600 focus:outline-none"
            placeholder="Your full address"
            name="destination"
            onChange={(e) => setDestination(e.target.value)}
            value={destination}
          />
        </div>

        <div className="mb-3">
          <label className="form-label inline-block mb-2 font-bold text-sm text-gray-700">
            Phone
          </label>

          <input
            type="text"
            className="form-control block w-full px-3 py-1.5 text-base font-normal
            text-gray-700 bg-white bg-clip-padding border border-solid border-gray-300
            rounded transition ease-in-out m-0 focus:text-gray-700 focus:bg-white
            focus:border-blue-600 focus:outline-none"
            placeholder="Phone"
            name="phone"
            onChange={(e) => setPhone(e.target.value)}
            value={phone}
          />
        </div>

        <div className="flex justify-between items-center mb-3">
          <button
            className="px-6 py-2.5 bg-transparent border-blue-800 text-blue-800 font-medium text-xs 
            leading-tight uppercase rounded shadow-md hover:bg-blue-900 hover:shadow-lg border
            focus:border-blue-900 focus:shadow-lg focus:outline-none focus:ring-0 active:border-blue-900 
            active:shadow-lg transition duration-150 ease-in-out hover:text-white w-full"
          >
            Back to Shopping
          </button>
        </div>
      </form>

      <div className="w-4/5 md:w-2/5 my-2">
        <div className="mb-3">
          <h4 className="mb-2 font-bold text-sm text-gray-700">
            Order Summary
          </h4>
        </div>

        <div className="flex justify-between items-center mb-3">
          <h4 className="mb-2 text-sm text-gray-700">Subtotal</h4>

          <small className="flex justify-start items-center space-x-1">
            <FaEthereum />
            <span className="text-gray-700">
              {(summary.grand - summary.tax).toFixed(3)} EHT
            </span>
          </small>
        </div>

        <div className="flex justify-between items-center mb-3">
          <h4 className="mb-2 text-sm text-gray-700">Tax</h4>

          <small className="flex justify-start items-center space-x-1">
            <FaEthereum />
            <span className="text-gray-700">{summary.tax.toFixed(3)} EHT</span>
          </small>
        </div>

        <div className="flex justify-between items-center mb-3">
          <h4 className="mb-2 text-sm text-gray-700 font-bold">Grand Total</h4>

          <small className="flex justify-start items-center space-x-1">
            <FaEthereum />
            <span className="text-gray-700 font-bold">
              {summary.grand.toFixed(3)} EHT
            </span>
          </small>
        </div>

        <div className="flex justify-between items-center mb-3">
          <button
            className="px-6 py-2.5 bg-blue-800 text-white font-medium text-xs 
          leading-tight uppercase rounded shadow-md hover:bg-blue-900 hover:shadow-lg
          focus:bg-blue-900 focus:shadow-lg focus:outline-none focus:ring-0 active:bg-blue-900 
          active:shadow-lg transition duration-150 ease-in-out w-full"
            onClick={handleCheckout}
          >
            Place Order Now
          </button>
        </div>
      </div>
    </div>
  )
}

export default Summary

The Stats Components

This section handles funding and withdrawals from your store. For a complete understanding, refer to the codes below.

const Treasury = ({ stats }) => {
  return (
    <div className="flex flex-col sm:flex-row justify-center items-center p-5">
      <div className="flex flex-col justify-center items-center h-20 border border-gray-200 shadow-md w-full">
        <span className="text-lg font-bold text-black leading-5">{0} ETH</span>
        <span>Paid</span>
      </div>
      <div className="flex flex-col justify-center items-center h-20 border border-gray-200 shadow-md w-full">
        <span className="text-lg font-bold text-black leading-5">{stats.balance} ETH</span>
        <span>Balance</span>
      </div>
      <div className="flex justify-center items-center h-20 space-x-2 border border-gray-200 shadow-md w-full">
        <button
          type="button"
          className="inline-block px-6 py-2.5 bg-blue-600
            text-white font-medium text-xs leading-tight
            uppercase rounded-full shadow-md hover:bg-blue-700
            hover:shadow-lg focus:bg-blue-700 focus:shadow-lg
            focus:outline-none focus:ring-0 active:bg-blue-800
            active:shadow-lg transition duration-150 ease-in-out"
        >
          Withdraw Fund
        </button>
        <button
          type="button"
          className="inline-block px-6 py-2.5 bg-blue-600
            text-white font-medium text-xs leading-tight
            uppercase rounded-full shadow-md hover:bg-blue-700
            hover:shadow-lg focus:bg-blue-700 focus:shadow-lg
            focus:outline-none focus:ring-0 active:bg-blue-800
            active:shadow-lg transition duration-150 ease-in-out"
        >
          Fund Account
        </button>
      </div>
    </div>
  )
}

export default Treasury

And there you have it for all the chunks of components.

Pages

It is time to put together all the components on their respective pages. On the root of your project, head to **src** folder and create a new folder called **views**. Now all the created components in this section must all be included in this views folder.

The Home Page

This page bundles up the banner, shop stats, and the cards components together, see the codes below.

import Banner from '../components/Banner'
import ShopStats from '../components/ShopStats'
import Cards from '../components/Cards'
import { useGlobalState } from '../store'
import { loadProducts } from '../Blockchain.Service'
import { useEffect, useState } from 'react'


const Home = () => {
  const [products] = useGlobalState('products')
  const [stats] = useGlobalState('stats')
  const [loaded, setLoaded] = useState(false)

  useEffect(async () => {
    await loadProducts().then(() => setLoaded(true))
  }, [])

  return loaded ? (
    <>
      <Banner />
      <ShopStats stats={stats} />
      <div className="h-20"></div>
      <Cards products={products} title="Global Shop" />
    </>
  ) : null
}

export default Home

The Shopping Cart Page

This page features two components, the cart, and the summary components, they both help a customer place orders. The customer pays in ethers, see the codes below.

import Cart from '../components/Cart'
import { useGlobalState } from '../store'

const ShoppingCart = () => {
  const [cart] = useGlobalState('cart')
  const [summary] = useGlobalState('summary')

  return (
    <>
      <div className="h-10"></div>
      {cart.length > 0 ? (
        <Cart cart={cart} summary={summary} />
      ) : (
        <div className="flex flex-col justify-between items-center space-x-2 md:w-2/3 w-full p-5 mx-auto">
          <h4 className="text-center uppercase mb-8">Cart Empty</h4>
          <p>Add some products to your cart...</p>
        </div>
      )}
    </>
  )
}

export default ShoppingCart

The Product Page

The product page contains two essential components for displaying details pertaining to a specific game product. See the codes below.

import { useEffect, useState } from 'react'
import { useParams } from 'react-router-dom'
import { loadProduct } from '../Blockchain.Service'
import { useGlobalState } from '../store'
import Buyers from '../components/Buyers'
import Details from '../components/Details'

const Product = () => {
  const { id } = useParams()
  const [product] = useGlobalState('product')
  const [buyers] = useGlobalState('buyers')
  const [loaded, setLoaded] = useState(false)

  useEffect(async () => {
    await loadProduct(id).then(() => setLoaded(true))
  }, [])

  return loaded ? (
    <>
      <Details product={product} />
      <Buyers buyers={buyers} />
    </>
  ) : null
}

export default Product

The Orders and Sales Page

The orders page uses the order component to render a list of orders for a buyer of a product that he can see from his order history.

Replicate the page by creating the component below inside the views folder. See the codes below.

import { useEffect } from "react"
import { loadOrders } from "../Blockchain.Service"
import { useGlobalState } from "../store"
import Order from "../components/Order"

const Orders = () => {
  const [orders] = useGlobalState('orders')
  useEffect(async () => {
    await loadOrders()
  }, [])

  return (
    <>
      <Order orders={orders} title="Orders" />
    </>
  )
}

export default Orders
import { useEffect } from "react"
import { loadOrders } from "../Blockchain.Service"
import { useGlobalState } from "../store"
import Order from "../components/Order"

const Sales = () => {
  const [orders] = useGlobalState('orders')

  useEffect(async () => {
    await loadOrders()
  }, [])

  return (
    <>
      <Order orders={orders} title={'Sales'} seller />
    </>
  )
}

export default Sales

Chat Page

Buyer and seller using the CometChat SDK

This page enables a buyer to chat with a seller of a product, this was made possible with the CometChat SDK.

Chat with Seller CometChat SDK

Each seller must be authenticated anonymously with this chat service before receiving chats from their buyers. To enjoy this service, you must have configured the CometChat SDK which has been discussed above. See the codes below.

import Identicon from 'react-identicons'
import React, { useEffect, useState } from 'react'
import { FaTimes } from 'react-icons/fa'
import { useNavigate, useParams } from 'react-router-dom'
import { truncate, useGlobalState } from '../store'
import { sendMessage, CometChat, getMessages } from '../Chat.Service'
import { toast } from 'react-toastify'

const Chat = () => {
  const { id } = useParams()
  const [currentUser] = useGlobalState('currentUser')
  const navigate = useNavigate()

  useEffect(async () => {
    if (currentUser) {
      await getConversations().then((list) => setUsers(list))
    } else {
      toast('Please authenticate with the chat feature first!')
      navigate('/')
    }
  }, [])

  return currentUser ? (
    <>
      <ChatHeader id={id} />
      <Messages id={id} />
    </>
  ) : null
}

const ChatHeader = ({ id }) => {
  const navigate = useNavigate()

  return (
    <div className="flex justify-between items-start w-full md:w-2/3 p-5 mx-auto">
      <span
        className="rounded-full text-gray-500 bg-gray-200 font-semibold text-sm
        flex align-center cursor-pointer active:bg-gray-300
        transition duration-300 ease w-max"
      >
        <Identicon
          string={id}
          size={35}
          className="w-11 h-11 max-w-none object-contain rounded-full"
        />
        <span className="flex items-center px-3 py-2">
          {truncate(id, 4, 4, 11)}
        </span>
      </span>

      <span
        onClick={() => navigate('/product/' + 1)}
        className="rounded-full text-gray-500 bg-gray-200 font-semibold text-sm
        flex align-center cursor-pointer active:bg-gray-300
        transition duration-300 ease w-max"
      >
        <span className="flex items-center px-3 py-2">Exit</span>
        <button className="bg-transparent hover focus:outline-none pr-2">
          <FaTimes size={15} />
        </button>
      </span>
    </div>
  )
}

const Messages = ({ id }) => {
  const [connectedAccount] = useGlobalState('connectedAccount')
  const [message, setMessage] = useState('')
  const [messages, setMessages] = useState([])

  const handleSubmit = async (e) => {
    e.preventDefault()
    sendMessage(id, 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 scrollToEnd = () => {
    const element = document.getElementById('messages-container')
    element.scrollTop = element.scrollHeight
  }

  useEffect(async () => {
    listenForMessage(id)
    await getMessages(id).then((messages) =>
      setMessages(messages.filter((msg) => msg.category == 'message')),
    )
  }, [id])

  return (
    <div className="w-full lg:w-2/3 p-5 mx-auto">
      <div
        id="messages-container"
        className="h-[calc(100vh_-_18rem)] overflow-y-auto mb-8"
      >
        {messages.map((message, i) =>
          message.sender.uid != connectedAccount ? (
            <LeftMessage msg={message} key={i} />
          ) : (
            <RightMessage msg={message} key={i} />
          ),
        )}
      </div>
      <form onSubmit={handleSubmit} className="flex w-full">
        <input
          className="w-full bg-gray-200 rounded-lg p-4 
          focus:ring-0 focus:outline-none border-gray-500"
          type="text"
          placeholder="Write a message..."
          onChange={(e) => setMessage(e.target.value)}
          value={message}
          required
        />
        <button type="submit" hidden>
          Send
        </button>
      </form>
    </div>
  )
}

const RightMessage = ({ msg }) => (
  <div className="flex flex-row justify-end my-2">
    <div className="flex justify-center items-end space-x-2">
      <div
        className="flex flex-col bg-blue-600 w-80 p-3 px-5 rounded-t-3xl
        rounded-bl-3xl shadow shadow-black text-white font-semibold"
      >
        <div className="flex flex-row justify-start items-center space-x-2">
          <span>@You</span>
          <small>
            {new Date(msg.sentAt * 1000).toLocaleDateString()}{' '}
            {new Date(msg.sentAt * 1000).toLocaleTimeString()}
          </small>
        </div>
        <small className="leading-tight my-2">{msg.text}</small>
      </div>
    </div>
  </div>
)

const LeftMessage = ({ msg }) => (
  <div className="flex flex-row justify-start my-2">
    <div className="flex justify-center items-end space-x-2">
      <div
        className="flex flex-col bg-transparent w-80 p-3 px-5 rounded-t-3xl
            rounded-br-3xl shadow shadow-gray-500"
      >
        <div className="flex flex-row justify-start items-center space-x-2">
          <span>@{truncate(msg.sender.uid, 4, 4, 11)}</span>
          <small>
            {new Date(msg.sentAt * 1000).toLocaleDateString()}{' '}
            {new Date(msg.sentAt * 1000).toLocaleTimeString()}
          </small>
        </div>
        <small className="leading-tight my-2">{msg.text}</small>
      </div>
    </div>
  </div>
)

export default Chat

The Recent Chat Page

This page shows you a list of buyers who wants to contact you for extra information about your listed products. The CometChat SDK enables all these chatting functionalities to take place, you will have to sign in or up specifically for the chat feature before you utilize it on your account.

Sellers who don’t opt-in for this service cannot receive chats from their customers. See the code below.

import { useEffect, useState } from 'react'
import Identicon from 'react-identicons'
import { useNavigate } from 'react-router-dom'
import { getConversations } from '../Chat.Service'
import { truncate, useGlobalState } from '../store'
import { toast } from 'react-toastify'

const Recent = () => {
  const [users, setUsers] = useState([])
  const [currentUser] = useGlobalState('currentUser')
  const navigate = useNavigate()

  useEffect(async () => {
    if (currentUser) {
      await getConversations().then((list) => setUsers(list))
    } else {
      toast('Please authenticate with the chat feature first!')
      navigate('/')
    }
  }, [])

  return currentUser ? (
    <>
      <div className="h-20"></div>
      <div className="flex flex-col justify-between items-center space-x-2 md:w-2/3 w-full p-5 mx-auto">
        <h4 className="text-center uppercase mb-8">Recent Chats</h4>
        <div className="max-h-[calc(100vh_-_20rem)] overflow-y-auto shadow-md rounded-md w-full">
          {users.map((user, i) => (
            <Conversation conversation={user.lastMessage} key={i} />
          ))}
        </div>

        <div className="flex justify-between items-center my-4">
          <button
            className="px-6 py-2.5 bg-transparent border-blue-800 text-blue-800 font-medium text-xs 
            leading-tight uppercase rounded shadow-md hover:bg-blue-900 hover:shadow-lg border
            focus:border-blue-900 focus:shadow-lg focus:outline-none focus:ring-0 active:border-blue-900 
            active:shadow-lg transition duration-150 ease-in-out hover:text-white w-full"
          >
            Back to Home
          </button>
        </div>
      </div>
    </>
  ) : null
}

const Conversation = ({ conversation }) => {
  const navigate = useNavigate()
  const [connectedAccount] = useGlobalState('connectedAccount')

  const uid = (conversation) => {
    return conversation.sender.uid == connectedAccount
      ? conversation.receiver.uid
      : conversation.sender.uid
  }

  return (
    <button
      type="button"
      data-mdb-ripple="true"
      data-mdb-ripple-color="light"
      className="px-6 py-2.5 bg-white text-black font-medium text-xs leading-tight
      rounded shadow-md hover:bg-blue-700 hover:shadow-lg focus:bg-blue-700
      focus:shadow-lg focus:outline-none focus:ring-0 active:bg-blue-800 hover:text-white
      active:shadow-lg transition duration-150 ease-in-out w-full text-left my-2"
      onClick={() => navigate('/chat/' + uid(conversation))}
    >
      <div className="flex justify-start items-center space-x-4">
        <Identicon
          string={uid(conversation)}
          size={30}
          className="h-10 w-10 object-contain rounded-fullbg-white cursor-pointer"
        />

        <div className="flex flex-col justify-start space-y-2">
          <h4 className="font-bold text-md">
            {truncate(uid(conversation), 4, 4, 11)}
          </h4>
          <span className="text-sm">{conversation.text}</span>
          <small className="font-bold">
            {new Date(conversation.sentAt * 1000).toLocaleDateString()}{' '}
            {new Date(conversation.sentAt * 1000).toLocaleTimeString()}
          </small>
        </div>
      </div>
    </button>
  )
}

export default Recent

The Seller and Stats page

My Products and Stats Page

The last two pages are dedicated to listing specific sellers’ products as well as some shop statistics. The statistics show how well the seller is doing in this market. Still, in the views component, create these two pages. See the codes listed below.

import { useParams } from 'react-router-dom'
import Cards from '../components/Cards'

const Seller = () => {
  const { id } = useParams()

  return (
    <>
      <div className="h-20"></div>
      <Cards products={[]} title="Seller Shop" seller={id} />
    </>
  )
}

export default Seller
import { useEffect, useState } from 'react'
import { loadStats } from '../Blockchain.Service'
import ShopStats from '../components/ShopStats'
import Treasury from '../components/Treasury'
import { useGlobalState } from '../store'

const Stats = () => {
  const [stats] = useGlobalState('myStats')
  const [loaded, setLoaded] = useState(false)
  useEffect(async () => {
    await loadStats().then(() => setLoaded(true))
  }, [])

  return loaded ? (
    <>
      <div className="h-20"></div>
      <h4 className="text-center uppercase mb-8">Your Stats</h4>
      <ShopStats stats={stats} />
      <Treasury stats={stats} />
      <div className="flex justify-center items-center my-4">
        <button
          className="px-6 py-2.5 bg-transparent border-blue-800 text-blue-800 font-medium text-xs 
            leading-tight uppercase rounded shadow-md hover:bg-blue-900 hover:shadow-lg border
            focus:border-blue-900 focus:shadow-lg focus:outline-none focus:ring-0 active:border-blue-900 
            active:shadow-lg transition duration-150 ease-in-out hover:text-white"
        >
          Back to Home
        </button>
      </div>
    </>
  ) : null
}

export default Stats

Fantastic, that will be all for the pages, let’s proceed to other essential components of this application.

Configuring Other Components

There are other components that complete this application and in this part, we will be working on them one step at a time.

The App.jsx File
Head to the src folder and open the **App.jsx** file and replace its content with the codes below.

import { Route, Routes } from 'react-router-dom'
import { useEffect, useState } from 'react'
import { isWallectConnected } from './Blockchain.Service'
import { ToastContainer } from 'react-toastify'
import { checkStorage } from './Cart.Service'
import Header from './components/Header'
import AddButton from './components/AddButton'
import CreateProduct from './components/CreateProduct'
import UpateProduct from './components/UpateProduct'
import Menu from './components/Menu'
import Home from './views/Home'
import Product from './views/Product'
import Orders from './views/Orders'
import Chat from './views/Chat'
import Seller from './views/Seller'
import Recent from './views/Recent'
import Stats from './views/Stats'
import Sales from './views/Sales'
import ShoppingCart from './views/ShoppingCart'
import DeleteProduct from './components/DeleteProduct'
import ChatModal from './components/ChatModal'
import { isUserLoggedIn } from './Chat.Service'

const App = () => {
  const [loaded, setLoaded] = useState(false)

  useEffect(async () => {
    await isWallectConnected().then(async () => {
      checkStorage()
      await isUserLoggedIn()
      setLoaded(true)
      console.log('Blockchain Loaded')
    })
  }, [])

  return loaded ? (
    <div className="min-h-screen">
      <Header />

      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/cart" element={<ShoppingCart />} />
        <Route path="/product/:id" element={<Product />} />
        <Route path="/orders/" element={<Orders />} />
        <Route path="/sales/" element={<Sales />} />
        <Route path="/chat/:id" element={<Chat />} />
        <Route path="/recents" element={<Recent />} />
        <Route path="/seller/:id" element={<Seller />} />
        <Route path="/stats/:id" element={<Stats />} />
      </Routes>

      <AddButton />
      <CreateProduct />
      <UpateProduct />
      <DeleteProduct />
      <Menu />
      <ChatModal />
      <ToastContainer
        position="bottom-center"
        autoClose={5000}
        hideProgressBar={false}
        newestOnTop={false}
        closeOnClick
        rtl={false}
        pauseOnFocusLoss
        draggable
        pauseOnHover
        theme="dark"
      />
    </div>
  ) : null
}

export default App

The above codes will ensure that all the components and pages are represented properly.

State Management Service
You will need a state management library to work with the blockchain and link all the various components together. For the sake of simplicity, we are using a react-hooks-global-state.

Navigate to the **project** >> **src** and create a new folder called the store. Inside this store folder, create a new file called **index.jsx** and paste the codes below inside and save.

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

const { setGlobalState, useGlobalState, getGlobalState } = createGlobalState({
  chatModal: 'scale-0',
  deleteModal: 'scale-0',
  updateModal: 'scale-0',
  modal: 'scale-0',
  menu: 'scale-0',
  connectedAccount: '',
  currentUser: null,
  contract: null,
  stats: null,
  myStats: null,
  buyers: [],
  orders: [],
  sales: [],
  products: [],
  product: null,
  cart: [],
  summary: { total: 0, grand: 0, tax: 0, qtys: [], ids: [] },
})

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
}

export { useGlobalState, setGlobalState, getGlobalState, truncate }

All the data coming from the blockchain will be stored in the above file and used across the entire application.

The Blockchain Service
This file contains all the EthersJs procedures for communicating with your smart contract that lives on the blockchain. In the src folder, create a file called **Blockchain.services.jsx** and paste the codes below and save.

import abi from './abis/src/contracts/Shop.sol/Shop.json'
import address from './abis/contractAddress.json'
import { getGlobalState, setGlobalState } from './store'
import { ethers } from 'ethers'
import { logOutWithCometChat } from './Chat.Service'

const toWei = (num) => ethers.utils.parseEther(num.toString())

const { ethereum } = window
const contractAddress = address.address
const contractAbi = abi.abi
const fee = toWei('0.002')

const getEtheriumContract = () => {
  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' })

    window.ethereum.on('chainChanged', (chainId) => {
      window.location.reload()
    })

    window.ethereum.on('accountsChanged', async () => {
      setGlobalState('connectedAccount', accounts[0].toLowerCase())
      await logOutWithCometChat()
      await isWallectConnected()
    })

    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 createProduct = async ({
  sku,
  name,
  description,
  imageURL,
  price,
  stock,
}) => {
  try {
    if (!ethereum) return alert('Please install Metamask')
    const connectedAccount = getGlobalState('connectedAccount')
    const contract = getEtheriumContract()
    price = toWei(price)

    await contract.createProduct(
      sku,
      name,
      description,
      imageURL,
      price,
      stock,
      {
        from: connectedAccount,
        value: fee._hex,
      },
    )
  } catch (error) {
    reportError(error)
  }
}

const updateProduct = async ({
  id,
  name,
  description,
  imageURL,
  price,
  stock,
}) => {
  try {
    if (!ethereum) return alert('Please install Metamask')
    const connectedAccount = getGlobalState('connectedAccount')
    const contract = getEtheriumContract()
    price = toWei(price)

    await contract.updateProduct(
      id,
      name,
      description,
      imageURL,
      price,
      stock,
      {
        from: connectedAccount,
      },
    )
  } catch (error) {
    reportError(error)
  }
}

const deleteProduct = async (id) => {
  try {
    if (!ethereum) return alert('Please install Metamask')
    const connectedAccount = getGlobalState('connectedAccount')
    const contract = getEtheriumContract()
    await contract.deleteProduct(id, { from: connectedAccount })
  } catch (error) {
    reportError(error)
  }
}

const createOrder = async ({ ids, qtys, phone, destination, grand }) => {
  try {
    if (!ethereum) return alert('Please install Metamask')
    const connectedAccount = getGlobalState('connectedAccount')
    const contract = getEtheriumContract()
    grand = toWei(grand)

    await contract.createOrder(ids, qtys, destination, phone, {
      from: connectedAccount,
      value: grand._hex,
    })
  } catch (error) {
    reportError(error)
  }
}

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

    const connectedAccount = getGlobalState('connectedAccount')
    const contract = getEtheriumContract()
    const products = await contract.getProducts()
    const stats = await contract.stats()
    const myStats = await contract.statsOf(connectedAccount)

    setGlobalState('products', structuredProducts(products))
    setGlobalState('stats', structureStats(stats))
    setGlobalState('myStats', structureStats(myStats))
  } catch (error) {
    reportError(error)
  }
}

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

    const contract = getEtheriumContract()
    const product = await contract.getProduct(id)
    const buyers = await contract.getBuyers(id)

    setGlobalState('product', structuredProducts([product])[0])
    setGlobalState('buyers', structuredBuyers(buyers))
  } catch (error) {
    reportError(error)
  }
}

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

    const orders = await contract.getOrders()
    setGlobalState('orders', structuredOrders(orders))
  } catch (error) {
    reportError(error)
  }
}

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

    const connectedAccount = getGlobalState('connectedAccount')
    const contract = getEtheriumContract()
    const myStats = await contract.statsOf(connectedAccount)

    setGlobalState('myStats', structureStats(myStats))
  } catch (error) {
    reportError(error)
  }
}

const delieverOrder = async (pid, id) => {
  try {
    if (!ethereum) return alert('Please install Metamask')
    const connectedAccount = getGlobalState('connectedAccount')
    const contract = getEtheriumContract()
    await contract.deliverOrder(pid, id, { from: connectedAccount })
  } catch (error) {
    reportError(error)
  }
}

const cancelOrder = async (pid, id) => {
  try {
    if (!ethereum) return alert('Please install Metamask')
    const connectedAccount = getGlobalState('connectedAccount')
    const contract = getEtheriumContract()
    await contract.cancelOrder(pid, id, { from: connectedAccount })
  } catch (error) {
    reportError(error)
  }
}

const reportError = (error) => {
  console.log(error.message)
  throw new Error('No ethereum object.')
}

const structuredProducts = (products) =>
  products
    .map((product) => ({
      id: Number(product.id),
      sku: product.sku,
      seller: product.seller.toLowerCase(),
      name: product.name,
      description: product.description,
      imageURL: product.imageURL,
      stock: Number(product.stock),
      price: parseInt(product.price._hex) / 10 ** 18,
      deleted: product.deleted,
      timestamp: new Date(product.timestamp).getTime(),
    }))
    .reverse()

const structuredOrders = (orders) =>
  orders
    .map((order) => ({
      pid: Number(order.pid),
      id: Number(order.id),
      name: order.name,
      sku: order.sku,
      seller: order.seller.toLowerCase(),
      buyer: order.buyer.toLowerCase(),
      destination: order.destination,
      phone: order.phone,
      imageURL: order.imageURL,
      qty: Number(order.qty),
      status: Number(order.status),
      total: parseInt(order.total._hex) / 10 ** 18,
      timestamp: new Date(order.timestamp.toNumber()).getTime(),
    }))
    .reverse()

const structuredBuyers = (buyers) =>
  buyers
    .map((buyer) => ({
      buyer: buyer.buyer.toLowerCase(),
      qty: Number(buyer.qty),
      price: parseInt(buyer.price._hex) / 10 ** 18,
      timestamp: new Date(buyer.timestamp.toNumber() * 1000).toDateString(),
    }))
    .reverse()

const structureStats = (stats) => ({
  balance: Number(stats.balance),
  orders: Number(stats.orders),
  products: Number(stats.products),
  sales: Number(stats.sales),
  paid: Number(stats.paid._hex),
  sellers: Number(stats.sellers),
})

export {
  isWallectConnected,
  connectWallet,
  createProduct,
  updateProduct,
  deleteProduct,
  loadProducts,
  loadProduct,
  createOrder,
  loadOrders,
  loadStats,
  delieverOrder,
  cancelOrder,
}

The Cart Service
This file contains the codes that calibrate our cart system, it ensures that every change in price and quantity of items is reflected in the sub and grand total of our cart.

On the **src** directory, create a new file named **Cart.Services.jsx**, copy the codes below and paste them into it and save.

import { getGlobalState, setGlobalState } from './store'

const addToCart = (product) => {
  const products = getGlobalState('cart')
  if (!products.find((p) => product.id == p.id)) {
    setGlobalState('cart', [...products, { ...product, qty: 1 }])
    localStorage.setItem(
      'cart',
      JSON.stringify([...products, { ...product, qty: 1 }]),
    )
    summarizeCart()
  }
}

const remFromCart = (product) => {
  let products = getGlobalState('cart')
  products = products.filter((p) => p.id != product.id)
  setGlobalState('cart', products)
  localStorage.setItem('cart', JSON.stringify(products))
  summarizeCart()
}

const updateCart = (product) => {
  const products = getGlobalState('cart')
  products.forEach((p) => {
    if (p.id == product.id) p = product
  })
  setGlobalState('cart', products)
  localStorage.setItem('cart', JSON.stringify(products))
  summarizeCart()
}

const clearCart = () => {
  setGlobalState('cart', [])
  localStorage.removeItem('cart')
  summarizeCart()
}

const summarizeCart = () => {
  const products = getGlobalState('cart')
  const summary = getGlobalState('summary')
  products.forEach((p, i) => {
    summary.total += p.qty * p.price
    if (summary.ids.includes(p.id)) {
      summary.qtys[i] = p.qty
    } else {
      summary.ids[i] = p.id
      summary.qtys[i] = p.qty
    }
  })
  summary.tax = 0.002
  summary.grand = summary.total + summary.tax
  setGlobalState('summary', summary)
  summary.total = 0
  // summary.grand = 0
}

const checkStorage = () => {
  let products = JSON.parse(localStorage.getItem('cart'))
  if (products?.length) {
    setGlobalState('cart', JSON.parse(localStorage.getItem('cart')))
    summarizeCart()
  }
}

export { addToCart, remFromCart, updateCart, checkStorage, clearCart }

The Chat Service
This file contains the codes for interacting with the CometChat SDK. In the **src** folder, create a new file named **Chat.Services.jsx**. Now, copy the codes below, paste them into the file, and save.

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
  return await CometChat.login(UID, authKey)
    .then((user) => {
      setGlobalState('currentUser', user)
      return true
    })
    .catch((error) => error)
}

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

  return await CometChat.createUser(user, authKey)
    .then((user) => {
      console.log('Signed In: ', user)
      return true
    })
    .catch((error) => error)
}

const logOutWithCometChat = async () => {
  return await CometChat.logout()
    .then(() => setGlobalState('currentUser', null))
    .catch((error) => error)
}

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

const getUser = async (UID) => {
  return await CometChat.getUser(UID)
    .then((user) => user)
    .catch((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)
    .catch((error) => error)
}

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

Finally, click the link below to download the image. If the asset folder does not already exist in your src directory, create one.

https://github.com/Daltonic/gameshop/blob/master/src/assets/banner.png?raw=true

With all that setup, run the command below to have the project running on your local machine.

yarn start

This will open the project on the browser at **localhost:3000**.

Conclusion

That concludes the tutorial for this build, you have learned how to create a decentralized eCommerce platform that allows you to list game products on a marketplace.

Buyers can purchase your game product and on delivery, the money is released to the seller of the product.

This is one powerful use case for developing a real-life decentralized web3 application. More of these kinds of builds can be found here on my account.

You can also watch my free videos on my YouTube channel. Or book your private web3 classes with me to speed up your web3 learning process.

With that said, I will catch you next time, 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.

L O A D I N G
. . . comments & more!

FOLLOW US ON GOOGLE NEWS

Read original article here

Denial of responsibility! Techno Blender is an automatic aggregator of the all world’s media. In each content, the hyperlink to the primary source is specified. All trademarks belong to their rightful owners, all materials to their authors. If you are the owner of the content and do not want us to publish your materials, please contact us by email – [email protected]. The content will be deleted within 24 hours.

Leave a comment