visit
What you will be building: see the on the Goerli test network and here.
git clone //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"
]
}
}
yarn install
Follow the steps below to configure the ; at the end, you must save these keys as an environment variable.
STEP 1: Head to Dashboard and create an account.
STEP 2: Log in to the dashboard, only after registering.
STEP 3: From the dashboard, add a new app called GameShop.
STEP 4: Select the app you just created from the list.
STEP 5:
From the Quick Start copy the APP_ID
, REGION
, and AUTH_KEY
, to your .env
file. See the image and code snippet.
Replace the REACT_COMET_CHAT
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.
STEP 1:
Head to , 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=******************************
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.
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=******************************
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: "//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
}
}
bytecodes
and abi
.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.
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!
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.
yarn hardhat node # Terminal #1
yarn hardhat run scripts/deploy.js --network localhost # Terminal #2
Components Create a new folder called components in the src directory, which will house all of the React components.
Header component
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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 .
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 }
The Blockchain Service
This file contains all the 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.
yarn start
This will open the project on the browser at **localhost:3000**
.
You can also . Or to speed up your web3 learning process.
Gospel Darlington is a full-stack blockchain developer with 6+
years of experience in the software development industry.
His stacks include JavaScript
, React
, Vue
, Angular
, Node
, React Native
, NextJs
, Solidity
, and more.