paint-brush
How To Build A Web3 e-Commerce Platform with React and Solidity: (PART II)by@daltonic
11,633 reads
11,633 reads

How To Build A Web3 e-Commerce Platform with React and Solidity: (PART II)

by Darlington Gospel 1mMarch 26th, 2022
Read on Terminal Reader
Read this story w/o Javascript

Too Long; Didn't Read

In this tutorial, we'll use Firebase and CometChat to create an app with their services. Firebase is limited but free, it's more than enough to help you learn full-stack development. We’ll begin by installing the rest of the dependencies for this application. We'll use the Firebase SDK to create a new project called **freshers.app. Then activate the Google authentication service, as detailed below. Let’s build the frontend for interacting with CometChat and Firebase.

Companies Mentioned

Mention Thumbnail
Mention Thumbnail

Coins Mentioned

Mention Thumbnail
Mention Thumbnail
featured image - How To Build A Web3 e-Commerce Platform with React and Solidity: (PART II)
Darlington Gospel  HackerNoon profile picture


What you will be building, see the live demo and GitHub repo for more info.


Add New Product

Pay With Ethers

Chat With Seller

Introduction

In the PART-ONE of this tutorial, we built the smart contract that powers our application. Now let’s build the frontend for interacting with it as you can see above.

Not much talking, let’s get coding… We’ll begin by installing the rest of the dependencies for this application.

Installing App Dependencies

On your terminal, run the following commands…

yarn add firebase #Database SDK
yarn add @cometchat-pro/chat #Chat SDK 
yarn add @material-tailwind/react #UI Kit


If you have successfully executed the above commands, let’s move to create some private keys for Firebase and CometChat.

Creating Private Keys

To utilize the Firebase or CometChat SDK, we need to create an app with their services. Don’t worry, this won't cost you a dime. Firebase is limited but free, it's more than enough to help you learn full-stack development. CometChat offers its users a trial version for testing out their SDK and getting acquainted with how their technology works.


Creating an App with Firebase Use this example If you do not already have a Firebase account, create one for yourself. After that, go to Firebase and create a new project called freshers, then activate the Google authentication service, as detailed below.


Firebase Projects page

Step 1 Step 2 Step 3

Firebase supports authentication via a variety of providers. For example, social authentication, phone numbers, and the traditional email and password method. Because we'll be using the Google authentication in this tutorial, we'll need to enable it for the project we created in Firebase, as it's disabled by default. Click the sign-in method under the authentication tab for your project, and you should see a list of providers currently supported by Firebase.


Firebase Authentication Service

Step 1 Step 2 Step 3

Super, that will be all for the firebase authentication, let's generate the Firebase SDK configuration keys.

You need to go and register your application under your Firebase project.


Project Overview Page

On the project’s overview page, select the add app option and pick web as the platform.


Registering a Firebase SDK Step 1 Registering a Firebase SDK Step 2

Return to the project overview page after completing the SDK config registration, as shown in the image below.


Project overview page

Now you click on the project settings to copy your SDK configuration setups.


Project Setups

The configuration keys shown in the image above must be copied to the .env file. We will later use it in this project.


Create a file called firebase.js in the src folder of this project and paste the following codes into it before saving.


import { initializeApp } from 'firebase/app'
import { setAlert } from './store'
import {
  getAuth,
  signInWithEmailAndPassword,
  createUserWithEmailAndPassword,
  signOut,
  onAuthStateChanged,
} from 'firebase/auth'

import {
  getFirestore,
  query,
  getDocs,
  updateDoc,
  collection,
  collectionGroup,
  orderBy,
  deleteDoc,
  addDoc,
  doc,
  setDoc,
  serverTimestamp,
} from 'firebase/firestore'

const firebaseConfig = {
  apiKey: process.env.REACT_APP_FB_AUTH_KEY,
  authDomain: 'fresher-a5113.firebaseapp.com',
  projectId: 'fresher-a5113',
  storageBucket: 'fresher-a5113.appspot.com',
  messagingSenderId: '443136794867',
  appId: process.env.REACT_APP_FB_APP_ID,
}

const app = initializeApp(firebaseConfig)
const auth = getAuth(app)
const db = getFirestore(app)

const logInWithEmailAndPassword = async (email, password) => {
  try {
    return await signInWithEmailAndPassword(auth, email, password).then(
      (res) => res.user
    )
  } catch (error) {
    setAlert(JSON.stringify(error), 'red')
  }
}

const registerWithEmailAndPassword = async (
  email,
  password,
  fullname,
  phone,
  account,
  address
) => {
  try {
    const res = await createUserWithEmailAndPassword(auth, email, password)
    const user = res.user
    const userDocRef = doc(db, 'users', user.email)

    await setDoc(userDocRef, {
      uid: user.uid,
      fullname,
      email,
      phone,
      account,
      address,
    })

    return user
  } catch (error) {
    setAlert(JSON.stringify(error), 'red')
  }
}

const logout = async () => {
  try {
    await signOut(auth)
    return true
  } catch (error) {
    setAlert(JSON.stringify(error), 'red')
  }
}

const addToOrders = async (cart) => {
  try {
    const order = {
      order: Math.random().toString(36).substring(2, 9).toUpperCase(),
      timestamp: serverTimestamp(),
      cart,
    }

    await addDoc(
      collection(db, `users/${auth.currentUser.email}`, 'orders'),
      order
    )
    return order
  } catch (error) {
    setAlert(JSON.stringify(error), 'red')
  }
}

const addProduct = async (product) => {
  try {
    await addDoc(
      collection(db, `users/${auth.currentUser.email}`, 'products'),
      {
        name: product.name,
        uid: auth.currentUser.uid,
        email: auth.currentUser.email,
        price: product.price,
        description: product.description,
        account: product.account,
        imgURL: product.imgURL,
        stock: ((Math.random() * 10) | 0) + 1,
        timestamp: serverTimestamp(),
      }
    )
  } catch (error) {
    setAlert(JSON.stringify(error), 'red')
  }
}

const getProducts = async () => {
  try {
    const products = query(
      collectionGroup(db, 'products'),
      orderBy('timestamp', 'desc')
    )
    const snapshot = await getDocs(products)

    return snapshot.docs.map((doc) => ({
      id: doc.id,
      ...doc.data(),
      price: Number(doc.data().price),
    }))
  } catch (error) {
    setAlert(JSON.stringify(error), 'red')
  }
}

const getProduct = async (id) => {
  try {
    const products = query(
      collectionGroup(db, 'products'),
      orderBy('timestamp', 'desc')
    )
    const snapshot = await getDocs(products)

    const product = snapshot.docs.find((doc) => doc.id == id)
    return {
      id: product.id,
      ...product.data(),
      price: Number(product.data().price),
    }
  } catch (error) {
    setAlert(JSON.stringify(error), 'red')
  }
}

const updateProduct = async (product) => {
  const productRef = doc(db, `users/${product.email}/products`, product.id)
  try {
    await updateDoc(productRef, product)
  } catch (error) {
    setAlert(JSON.stringify(error), 'red')
  }
}

const deleteProduct = async (product) => {
  const productRef = doc(db, `users/${product.email}/products`, product.id)
  try {
    await deleteDoc(productRef)
  } catch (error) {
    setAlert(JSON.stringify(error), 'red')
  }
}

export {
  auth,
  db,
  logInWithEmailAndPassword,
  registerWithEmailAndPassword,
  logout,
  onAuthStateChanged,
  addProduct,
  addToOrders,
  getProducts,
  getProduct,
  updateProduct,
  deleteProduct,
}


You are awesome if you followed everything correctly. We'll do something similar for CometChat next.


Creating an App with CometChat Head to CometChat and signup if you don’t have an account with them. Next, log in and you will be presented with the screen below.


CometChat Dashboard

Use this as an example to create a new app with the name freshers by clicking the Add New App button. You will be presented with a modal where you can enter the app details. The image below shows an example.


Add New App Modal

Following the creation of your app, you will be directed to your dashboard, which should look something like this.


API Key Here Rest API Key Here

You must also copy these keys to the .env file.

Finally, delete the preloaded users, and groups as shown in the images below.


Users List Group List

Awesome, that will be enough for the setups. Use this template to ensure your .env file is following our convention.


ENDPOINT_URL=<PROVIDER_URL>
SECRET_KEY=<SECRET_PHRASE>
DEPLOYER_KEY=<YOUR_PRIVATE_KEY>

REACT_APP_COMET_CHAT_REGION=<YOUR_COMET_CHAT_REGION>
REACT_APP_COMET_CHAT_APP_ID=<YOUR_COMET_CHAT_APP_ID>
REACT_APP_COMET_CHAT_AUTH_KEY=<YOUR_COMET_CHAT_AUTH_KEY>

REACT_APP_FB_AUTH_KEY=<YOUR_FIREBASE_AUTH_KEY>
REACT_APP_FB_APP_ID=<YOUR_FIREBASE_APP_ID>


Lastly, create a file name cometChat.js in the src folder of this project and paste the code below into it.


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

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 () => {
  try {
    const appID = CONSTANTS.APP_ID
    const region = CONSTANTS.REGION

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

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

const loginWithCometChat = async (UID) => {
  try {
    const authKey = CONSTANTS.Auth_Key
    await CometChat.login(UID, authKey).then((user) =>
      console.log('Login Successful:', { user })
    )
  } catch (error) {
    console.log(error)
  }
}

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

    return await CometChat.createUser(user, authKey).then((user) => user)
  } catch (error) {
    console.log(error)
  }
}

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

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

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

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

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

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

    return await conversationsRequest
      .fetchNext()
      .then((conversationList) => conversationList)
  } catch (error) {
    console.log(error)
  }
}

export {
  initCometChat,
  loginWithCometChat,
  signInWithCometChat,
  logOutWithCometChat,
  getMessages,
  sendMessage,
  getConversations,
}


Cool, let’s start integrating them all into our application, we will start with the components.

Building The Components

Let’s start crafting out all the components one after the other, always refer to the git repo if you have any challenges.


The Register Component

Register Component

This component is responsible for saving new users into Firebase. Navigate to the src >> components and create a file named Register.jsx.


import { useState } from 'react'
import Button from '@material-tailwind/react/Button'
import { Link, useNavigate } from 'react-router-dom'
import { registerWithEmailAndPassword, logout } from '../firebase'
import { signInWithCometChat } from '../cometChat'
import { setAlert } from '../store'

const Register = () => {
  const [fullname, setFullname] = useState('')
  const [email, setEmail] = useState('')
  const [password, setPassword] = useState('')
  const [phone, setPhone] = useState('')
  const [address, setAddress] = useState('')
  const [account, setAccount] = useState('')
  const navigate = useNavigate()

  const handleRegister = async (e) => {
    e.preventDefault()
    if (
      email == '' ||
      password == '' ||
      fullname == '' ||
      phone == '' ||
      account == '' ||
      address == ''
    )
      return
    registerWithEmailAndPassword(
      email,
      password,
      fullname,
      phone,
      account,
      address
    ).then((user) => {
      if (user) {
        logout().then(() => {
          signInWithCometChat(user.uid, fullname).then(() => {
            resetForm()
            setAlert('Registeration in successfully')
            navigate('/signin')
          })
        })
      }
    })
  }

  const resetForm = () => {
    setFullname('')
    setEmail('')
    setPassword('')
    setPhone('')
    setAccount('')
    setAddress('')
  }

  return (
    <div className="relative flex flex-col justify-center items-center">
      <div className="mt-10 ">
        <form
          onSubmit={handleRegister}
          className="relative flex w-full flex-wrap items-stretch w-96 px-8"
        >
          <div className="relative flex w-full flex-wrap items-stretch mb-3">
            <input
              type="text"
              className="px-3 py-3 placeholder-blueGray-300 text-blueGray-600 relative bg-white bg-white rounded text-sm border border-blueGray-300 outline-none focus:outline-none focus:ring w-full pl-10"
              placeholder="Fullname"
              value={fullname}
              onChange={(e) => setFullname(e.target.value)}
              required
            />
          </div>
          <div className="relative flex w-full flex-wrap items-stretch mb-3">
            <input
              type="email"
              className="px-3 py-3 placeholder-blueGray-300 text-blueGray-600 relative bg-white bg-white rounded text-sm border border-blueGray-300 outline-none focus:outline-none focus:ring w-full pl-10"
              placeholder="Email"
              value={email}
              onChange={(e) => setEmail(e.target.value)}
              required
            />
          </div>
          <div className="relative flex w-full flex-wrap items-stretch mb-3">
            <input
              type="password"
              className="px-3 py-3 placeholder-blueGray-300 text-blueGray-600 relative bg-white bg-white rounded text-sm border border-blueGray-300 outline-none focus:outline-none focus:ring w-full pl-10"
              placeholder="************"
              value={password}
              onChange={(e) => setPassword(e.target.value)}
              required
            />
          </div>
          <div className="relative flex w-full flex-wrap items-stretch mb-3">
            <input
              type="number"
              className="px-3 py-3 placeholder-blueGray-300 text-blueGray-600 relative bg-white bg-white rounded text-sm border border-blueGray-300 outline-none focus:outline-none focus:ring w-full pl-10"
              placeholder="081 056 8262"
              value={phone}
              onChange={(e) => setPhone(e.target.value)}
              required
            />
          </div>
          <div className="relative flex w-full flex-wrap items-stretch mb-3">
            <input
              type="text"
              className="px-3 py-3 placeholder-blueGray-300 text-blueGray-600 relative bg-white bg-white rounded text-sm border border-blueGray-300 outline-none focus:outline-none focus:ring w-full pl-10"
              placeholder="Wallet Address"
              value={account}
              onChange={(e) => setAccount(e.target.value)}
              required
            />
          </div>
          <div className="relative flex w-full flex-wrap items-stretch mb-3">
            <input
              type="text"
              className="px-3 py-3 placeholder-blueGray-300 text-blueGray-600 relative bg-white bg-white rounded text-sm border border-blueGray-300 outline-none focus:outline-none focus:ring w-full pl-10"
              placeholder="Address"
              value={address}
              onChange={(e) => setAddress(e.target.value)}
              required
            />
          </div>
          <div className="relative flex w-full flex-wrap items-stretch justify-between items-center">
            <Link className="text-green-500" to="/signin">
              Already a member? sign in
            </Link>
            <Button color="green" ripple="light" type="submit">
              Sign Up
            </Button>
          </div>
        </form>
      </div>
    </div>
  )
}

export default Register


Awesome!!!


The Login Component

The Login Component

Let’s also create another component called Login.jsx in the src >> components folder and paste the code below in it.


import { useState } from 'react'
import { Link, useNavigate } from 'react-router-dom'
import { logInWithEmailAndPassword } from '../firebase'
import { loginWithCometChat } from '../cometChat'
import { setAlert } from '../store'
import Button from '@material-tailwind/react/Button'

const Login = () => {
  const [email, setEmail] = useState('')
  const [password, setPassword] = useState('')
  const navigate = useNavigate()

  const handleLogin = async (e) => {
    e.preventDefault()
    if (email == '' || password == '') return
    logInWithEmailAndPassword(email, password).then((user) => {
      if (user) {
        loginWithCometChat(user.uid).then(() => {
          resetForm()
          setAlert('Logged in successfully')
          navigate('/')
        })
      }
    })
  }

  const resetForm = () => {
    setEmail('')
    setPassword('')
  }

  return (
    <div className="relative flex flex-col justify-center items-center">
      <div className="mt-10 ">
        <form
          onSubmit={handleLogin}
          className="relative flex w-full flex-wrap items-stretch w-96 px-8"
        >
          <h4 className="font-semibold text-xl my-4">Login</h4>
          <div className="relative flex w-full flex-wrap items-stretch mb-3">
            <input
              type="email"
              className="px-3 py-3 placeholder-blueGray-300 text-blueGray-600 relative bg-white bg-white rounded text-sm border border-blueGray-300 outline-none focus:outline-none focus:ring w-full pl-10"
              placeholder="Email"
              value={email}
              onChange={(e) => setEmail(e.target.value)}
              required
            />
          </div>
          <div className="relative flex w-full flex-wrap items-stretch mb-3">
            <input
              type="password"
              className="px-3 py-3 placeholder-blueGray-300 text-blueGray-600 relative bg-white bg-white rounded text-sm border border-blueGray-300 outline-none focus:outline-none focus:ring w-full pl-10"
              placeholder="************"
              value={password}
              onChange={(e) => setPassword(e.target.value)}
              required
            />
          </div>

          <div className="relative flex w-full flex-wrap items-stretch justify-between items-center">
            <Link className="text-green-500" to="/signup">
              New user sign up
            </Link>
            <Button color="green" ripple="light" type="submit">
              Sign In
            </Button>
          </div>
        </form>
      </div>
    </div>
  )
}

export default Login


Cool, these two components make up the authentication aspect of this application. We will later fuse them into their respective views.


The Header Component

Header Component

This component encapsulates the pages on our application. It was crafted with the free Creative TIm Tailwind-Material UI Kit. Create a file named Header.jsx inside the src >> components directory and paste the codes below in it.


import { useState } from 'react'
import { Link, useNavigate } from 'react-router-dom'
import { setAlert, useGlobalState } from '../store'
import { logout } from '../firebase'
import { logOutWithCometChat } from '../cometChat'
import { connectWallet } from '../shared/Freshers'
import Navbar from '@material-tailwind/react/Navbar'
import NavbarContainer from '@material-tailwind/react/NavbarContainer'
import NavbarWrapper from '@material-tailwind/react/NavbarWrapper'
import NavbarBrand from '@material-tailwind/react/NavbarBrand'
import NavbarToggler from '@material-tailwind/react/NavbarToggler'
import NavbarCollapse from '@material-tailwind/react/NavbarCollapse'
import Nav from '@material-tailwind/react/Nav'
import NavItem from '@material-tailwind/react/NavItem'

const Header = () => {
  const [openNavbar, setOpenNavbar] = useState(false)
  const [cart] = useGlobalState('cart')
  const [isLoggedIn] = useGlobalState('isLoggedIn')
  const [connectedAccount] = useGlobalState('connectedAccount')
  const navigate = useNavigate()

  const handleSignOut = () => {
    logout().then((res) => {
      if (res) {
        logOutWithCometChat().then(() => {
          setAlert('Logged out successfully')
          navigate('/signin')
        })
      }
    })
  }

  return (
    <Navbar color="green" navbar>
      <NavbarContainer>
        <NavbarWrapper>
          <Link to="/">
            <NavbarBrand>Freshers</NavbarBrand>
          </Link>
          <NavbarToggler
            color="white"
            onClick={() => setOpenNavbar(!openNavbar)}
            ripple="white"
          />
        </NavbarWrapper>

        <NavbarCollapse open={openNavbar}>
          {isLoggedIn ? (
            <Nav leftSide>
              <NavItem ripple="light">
                <Link to="/customers">customers</Link>
              </NavItem>
              <NavItem ripple="light">
                <Link to="/product/add">Add Product</Link>
              </NavItem>
            </Nav>
          ) : (
            <></>
          )}
          <Nav rightSide>
            {isLoggedIn ? (
              <>
                {connectedAccount ? null : (
                  <NavItem
                    onClick={connectWallet}
                    active="light"
                    ripple="light"
                  >
                    <span className="cursor-pointer">Connect Wallet</span>
                  </NavItem>
                )}
                <NavItem onClick={handleSignOut} ripple="light">
                  <span className="cursor-pointer">Logout</span>
                </NavItem>
              </>
            ) : (
              <NavItem ripple="light">
                <Link to="/signin" className="cursor-pointer">
                  Login
                </Link>
              </NavItem>
            )}

            <NavItem ripple="light">
              <Link to="/cart">{cart.length} Cart</Link>
            </NavItem>
          </Nav>
        </NavbarCollapse>
      </NavbarContainer>
    </Navbar>
  )
}

export default Header


The Food Component This component renders the particular food properties to screen in a beautifully crafted card from tailwind CSS and Material design. Create a file called Food.jsx still in the components folder and paste the following codes in it.

Each card renders the name, image, description, price, and the remaining stocks of a food product. Here is the code for it.


import React from 'react'
import Card from '@material-tailwind/react/Card'
import CardImage from '@material-tailwind/react/CardImage'
import CardBody from '@material-tailwind/react/CardBody'
import CardFooter from '@material-tailwind/react/CardFooter'
import H6 from '@material-tailwind/react/Heading6'
import Paragraph from '@material-tailwind/react/Paragraph'
import Button from '@material-tailwind/react/Button'
import { setAlert, setGlobalState, useGlobalState } from '../store'
import { Link } from 'react-router-dom'

const Food = ({ item }) => {
  const [cart] = useGlobalState('cart')

  const addToCart = (item) => {
    item.added = true
    let cartItems = [...cart]
    const newItem = { ...item, qty: (item.qty += 1), stock: (item.stock -= 1) }
    if (cart.find((_item) => _item.id == item.id)) {
      cartItems[item] = newItem
      setGlobalState('cart', [...cartItems])
    } else {
      setGlobalState('cart', [...cartItems, newItem])
    }
    setAlert(`${item.name} added to cart!`)
  }

  const toCurrency = (num) =>
    num.toLocaleString('en-US', {
      style: 'currency',
      currency: 'USD',
    })

  return (
    <div className="mx-4 my-6 w-64">
      <Card>
        <Link to={`/product/` + item.id}>
          <CardImage src={item.imgURL} alt={item.name} />
        </Link>

        <CardBody>
          <Link to={`/product/` + item.id}>
            <H6 color="gray">{item.name}</H6>
          </Link>

          <Paragraph color="gray">
            Don't be scared of the truth because we need to...
          </Paragraph>

          <div
            color="black"
            className="flex flex-row justify-between items-center"
          >
            <span className="font-semibold text-green-500">
              {toCurrency(item.price)}
            </span>
            <span className="text-xs text-black">{item.stock} in stock</span>
          </div>
        </CardBody>

        <CardFooter>
          {item.stock > 0 ? (
            <Button
              onClick={() => addToCart(item)}
              color="green"
              size="md"
              ripple="light"
              disabled={item.stock == 0}
            >
              Add To Cart
            </Button>
          ) : (
            <Button
              color="green"
              size="md"
              buttonType="outline"
              ripple="light"
              disabled
            >
              Out of Stock
            </Button>
          )}
        </CardFooter>
      </Card>
    </div>
  )
}

export default Food


Next, let’s look at the foods component.


The Foods Components This component is responsible for rendering the entire collection of food data in our database. Let’s look at its code snippet.

The Foods Component

Still, in the components directory, create another file called Foods.jsx and paste the codes below in it.


import Food from './Food'

const Foods = ({ products }) => {
  return (
    <div className="flex flex-wrap justify-center items-center space-x-3 space-y-3 mt-12 overflow-x-hidden">
      {products.map((item, i) => (
        <Food item={item} key={i} />
      ))}
    </div>
  )
}

export default Foods


Lastly, let’s look at the CartItem component.


The CartItem Component

CartItem Component

This component is responsible for showing a single item in our cart collection. Here is the code responsible for it.


import { useState } from 'react'
import Card from '@material-tailwind/react/Card'
import CardStatusFooter from '@material-tailwind/react/CardStatusFooter'
import { Link } from 'react-router-dom'
import { Image, Button } from '@material-tailwind/react'
import { setGlobalState, useGlobalState } from '../store'

const CartItem = ({ item }) => {
  const [qty, setQty] = useState(item.qty)
  const [cart] = useGlobalState('cart')

  const toCurrency = (num) =>
    num.toLocaleString('en-US', {
      style: 'currency',
      currency: 'USD',
    })

  const increaseQty = () => {
    let cartItems = [...cart]
    const newItem = { ...item, qty: (item.qty += 1), stock: (item.stock -= 1) }
    cartItems[item] = newItem
    setGlobalState('cart', cartItems)
    setQty(newItem.qty)
  }

  const decreaseQty = () => {
    let cartItems = [...cart]
    if (qty == 1) {
      const index = cartItems.indexOf(item)
      cartItems.splice(index, 1)
    } else {
      const newItem = {
        ...item,
        qty: (item.qty -= 1),
        stock: (item.stock += 1),
      }
      cartItems[item] = newItem
      setQty(newItem.qty)
    }
    setGlobalState('cart', cartItems)
  }

  return (
    <Card className="flex flex-row justify-between items-end my-4">
      <Link
        to={'/product/' + item.id}
        className="h-12 w-12 object-contain mr-4"
      >
        <Image
          src={item.imgURL}
          alt={item.name}
          rounded={false}
          raised={true}
        />
      </Link>

      <CardStatusFooter
        color="green"
        amount={toCurrency(item.price)}
        date={item.name}
      >
        <div className="flex flex-row justify-center items-center mx-4">
          <Button
            color="green"
            buttonType="filled"
            size="sm"
            rounded={false}
            block={false}
            iconOnly={false}
            ripple="dark"
            onClick={decreaseQty}
          >
            -
          </Button>
          <span className="mx-4">{qty}</span>
          <Button
            color="green"
            buttonType="filled"
            size="sm"
            rounded={false}
            block={false}
            iconOnly={false}
            ripple="dark"
            onClick={increaseQty}
            disabled={item.stock == 0}
          >
            +
          </Button>
        </div>
      </CardStatusFooter>

      <span className="text-sm text-gray-500">
        Sub Total: {toCurrency(item.price * qty)}
      </span>
    </Card>
  )
}

export default CartItem


Congratulations, you just finished coding the components, let’s move on to creating the views…

The Views

Now that we’ve created the components supporting the various views, let’s proceed next by creating the individual pages.


The Home View

The Home View

This view renders the Food component structure. This is to say, the home view retrieves all the food collection from firebase and shows them on screen. Let’s take a look at the codes responsible for it.


Navigate to the views directory and create a file named Home.jsx, then, paste the code below inside of it. In fact, you will create all these files in the views folder.


import { useEffect, useState } from 'react'
import Header from '../components/Header'
import Foods from '../components/Foods'
import { getProducts } from '../firebase'

const Home = () => {
  const [products, setProducts] = useState([])

  useEffect(() => {
    getProducts().then((products) => {
      products.filter((item) => {
        item.price = Number(item.price)
        item.qty = 0
      })
      setProducts(products)
    })
  }, [])

  return (
    <div className="home">
      <Header />
      <Foods products={products} />
    </div>
  )
}

export default Home


The Product View

The Product View

This view is responsible for showcasing in detail the information about a product. From this page, users can view, edit, and delete products as well as chat with the seller, or quickly purchase the food item with Ethereum.


Here is the code for it…


import Header from '../components/Header'
import { useEffect, useState } from 'react'
import { useParams, useNavigate } from 'react-router-dom'
import { Button, CardImage } from '@material-tailwind/react'
import { getProduct, deleteProduct, auth } from '../firebase'
import { setGlobalState, useGlobalState, setAlert } from '../store'
import { payWithEthers } from '../shared/Freshers'

const Product = () => {
  const { id } = useParams()
  const navigate = useNavigate()
  const [product, setProduct] = useState(null)
  const [cart] = useGlobalState('cart')
  const [isLoggedIn] = useGlobalState('isLoggedIn')
  const [buyer] = useGlobalState('connectedAccount')
  const [ethToUsd] = useGlobalState('ethToUsd')

  const addToCart = () => {
    const item = product
    item.added = true
    let cartItems = [...cart]
    const newItem = { ...item, qty: (item.qty += 1), stock: (item.stock -= 1) }
    if (cart.find((_item) => _item.id == item.id)) {
      cartItems[item] = newItem
      setGlobalState('cart', [...cartItems])
    } else {
      setGlobalState('cart', [...cartItems, newItem])
    }
    setAlert('Product added to cart')
  }

  const handlePayWithEthers = () => {
    const item = { ...product, buyer, price: (product.price / ethToUsd).toFixed(4) }
    payWithEthers(item).then((res) => {
      if (res) setAlert('Product purchased!')
    })
  }

  const handleDeleteProduct = () => {
    deleteProduct(product).then(() => {
      setAlert('Product deleted!')
      navigate('/')
    })
  }

  const toCurrency = (num) =>
    num.toLocaleString('en-US', {
      style: 'currency',
      currency: 'USD',
    })

  useEffect(() => {
    getProduct(id).then((data) => setProduct({ ...data, qty: 1 }))
  }, [id])

  return (
    <div className="product">
      <Header />
      {!!product ? (
        <div className="flex flex-wrap justify-start items-center p-10">
          <div className="mt-4 w-64">
            <CardImage src={product.imgURL} alt={product.name} />
          </div>

          <div className="mt-4 lg:mt-0 lg:row-span-6 mx-4">
            <div>
              <h1 className="text-2xl font-extrabold tracking-tight text-gray-900 sm:text-3xl">
                {product.name}
              </h1>
              <h2 className="sr-only">Product information</h2>
              <div className="flex flex-row justify-start items-center">
                <span className="text-xl font-bold text-green-500">
                  {toCurrency(product.price)}
                </span>
                <span className="text-xs mx-4">
                  {product.stock} left in stock
                </span>
              </div>

              <div className="mt-2 space-y-6">
                <p className="text-base text-gray-900">{product.description}</p>
              </div>
            </div>

            <div className="mt-4 flex flex-row justify-start items-center space-x-2">
              <Button
                onClick={addToCart}
                color="green"
                size="md"
                ripple="light"
              >
                Add To Cart
              </Button>

              {isLoggedIn ? (
                <>
                  {auth.currentUser.uid != product.uid &&
                  product.account != buyer ? (
                    <Button
                      onClick={handlePayWithEthers}
                      color="amber"
                      size="md"
                      ripple="light"
                    >
                      Buy with ETH
                    </Button>
                  ) : null}

                  {auth.currentUser.uid == product.uid ? null : (
                    <Button
                      onClick={() => navigate('/chat/' + product.uid)}
                      buttonType="link"
                      color="green"
                      size="md"
                      ripple="light"
                    >
                      Chat WIth Seller
                    </Button>
                  )}
                </>
              ) : null}
              {isLoggedIn && auth.currentUser.uid == product.uid ? (
                <>
                  <Button
                    onClick={() => navigate('/product/edit/' + id)}
                    buttonType="link"
                    color="green"
                    size="md"
                    ripple="light"
                  >
                    Edit Product
                  </Button>
                  <Button
                    onClick={handleDeleteProduct}
                    buttonType="link"
                    color="red"
                    size="md"
                    ripple="light"
                  >
                    Delete
                  </Button>
                </>
              ) : null}
            </div>
          </div>
        </div>
      ) : null}
    </div>
  )
}

export default Product


The AddProduct View

Add Product View

As the name implies, this view is responsible for storing new food items into our Firestore collection. Observe the code snippet below…


import { useState } from 'react'
import { Link, useNavigate } from 'react-router-dom'
import { addProduct } from '../firebase'
import { setAlert } from '../store'
import { useGlobalState } from '../store'
import Button from '@material-tailwind/react/Button'
import Header from '../components/Header'

const AddProduct = () => {
  const [name, setName] = useState('')
  const [price, setPrice] = useState('')
  const [imgURL, setImgURL] = useState('')
  const [description, setDescription] = useState('')
  const [account] = useGlobalState('connectedAccount')
  const navigate = useNavigate()

  const handleAddProduct = (e) => {
    e.preventDefault()
    if (!account) {
      setAlert('Please connect your metamask account!', 'red')
      return
    }

    if (name == '' || price == '' || imgURL == '' || description == '') return
    addProduct({ name, price, imgURL, description, account }).then(() => {
      setAlert('Product created successfully')
      navigate('/')
    })
  }

  return (
    <div className="addProduct">
      <Header />
      <div className="relative flex flex-col justify-center items-center">
        <div className="mt-10 ">
          <form
            onSubmit={handleAddProduct}
            className="relative flex w-full flex-wrap items-stretch w-96 px-8"
          >
            <h4 className="font-semibold text-xl my-4">Add Product</h4>
            <div className="relative flex w-full flex-wrap items-stretch mb-3">
              <input
                type="text"
                className="px-3 py-3 placeholder-blueGray-300 text-blueGray-600 relative bg-white bg-white rounded text-sm border border-blueGray-300 outline-none focus:outline-none focus:ring w-full pl-10"
                placeholder="Product Name"
                value={name}
                onChange={(e) => setName(e.target.value)}
                required
              />
            </div>
            <div className="relative flex w-full flex-wrap items-stretch mb-3">
              <input
                type="number"
                min={1}
                step={0.01}
                className="px-3 py-3 placeholder-blueGray-300 text-blueGray-600 relative bg-white bg-white rounded text-sm border border-blueGray-300 outline-none focus:outline-none focus:ring w-full pl-10"
                placeholder="Product Price"
                value={price}
                onChange={(e) => setPrice(e.target.value)}
                required
              />
            </div>
            <div className="relative flex w-full flex-wrap items-stretch mb-3">
              <input
                type="url"
                className="px-3 py-3 placeholder-blueGray-300 text-blueGray-600 relative bg-white bg-white rounded text-sm border border-blueGray-300 outline-none focus:outline-none focus:ring w-full pl-10"
                placeholder="Product Image URL"
                value={imgURL}
                onChange={(e) => setImgURL(e.target.value)}
                required
              />
            </div>
            <div className="relative flex w-full flex-wrap items-stretch mb-3">
              <input
                type="text"
                className="px-3 py-3 placeholder-blueGray-300 text-blueGray-600 relative bg-white bg-white rounded text-sm border border-blueGray-300 outline-none focus:outline-none focus:ring w-full pl-10"
                placeholder="Product Description"
                value={description}
                onChange={(e) => setDescription(e.target.value)}
                required
              />
            </div>

            <div className="relative flex w-full flex-wrap items-stretch justify-between items-center">
              <Link className="text-green-500" to="/">
                Back to Home
              </Link>
              <Button color="green" ripple="light" type="submit">
                Save Product
              </Button>
            </div>
          </form>
        </div>
      </div>
    </div>
  )
}

export default AddProduct


Awesome, we are moving forward, let’s see the edit product view…


The Edit Product View

Edit Product

This view enables us to edit our existing food items. Of course, you need to be the one who initially added the food product to the shop before you can edit. Only product owners can edit, let’s look at the codes performing this action.


import Header from '../components/Header'
import Button from '@material-tailwind/react/Button'
import { useEffect, useState } from 'react'
import { Link, useParams, useNavigate } from 'react-router-dom'
import { updateProduct, getProduct, auth } from '../firebase'
import { setAlert } from '../store'
import { useGlobalState } from '../store'

const EditProduct = () => {
  const { id } = useParams()
  const navigate = useNavigate()
  const [product, setProduct] = useState(null)
  const [name, setName] = useState('')
  const [price, setPrice] = useState('')
  const [imgURL, setImgURL] = useState('')
  const [description, setDescription] = useState('')
  const [account] = useGlobalState('connectedAccount')

  useEffect(() => {
    getProduct(id).then((data) => {
      if (auth.currentUser.uid != data.uid) navigate('/')
      setProduct(data)
      setName(data.name)
      setPrice(Number(data.price))
      setImgURL(data.imgURL)
      setDescription(data.description)
    })
  }, [id])

  const handleProductUpdate = (e) => {
    e.preventDefault()
    if (!account) {
      setAlert('Please connect your metamask account!', 'red')
      return
    }

    if (name == '' || price == '' || imgURL == '' || description == '') return
    updateProduct({
      ...product,
      name,
      price,
      imgURL,
      description,
      account,
    }).then(() => {
      setAlert('Product updated successfully')
      navigate('/product/' + product.id)
    })
  }

  return (
    <div className="editProduct">
      <Header />
      <div className="relative flex flex-col justify-center items-center">
        <div className="mt-10 ">
          <form
            onSubmit={handleProductUpdate}
            className="relative flex w-full flex-wrap items-stretch w-96 px-8"
          >
            <h4 className="font-semibold text-xl my-4">Update Product</h4>
            <div className="relative flex w-full flex-wrap items-stretch mb-3">
              <input
                type="text"
                className="px-3 py-3 placeholder-blueGray-300 text-blueGray-600 relative bg-white bg-white rounded text-sm border border-blueGray-300 outline-none focus:outline-none focus:ring w-full pl-10"
                placeholder="Product Name"
                value={name}
                onChange={(e) => setName(e.target.value)}
                required
              />
            </div>
            <div className="relative flex w-full flex-wrap items-stretch mb-3">
              <input
                type="number"
                min={1}
                step={0.01}
                className="px-3 py-3 placeholder-blueGray-300 text-blueGray-600 relative bg-white bg-white rounded text-sm border border-blueGray-300 outline-none focus:outline-none focus:ring w-full pl-10"
                placeholder="Product Price"
                value={price}
                onChange={(e) => setPrice(e.target.value)}
                required
              />
            </div>
            <div className="relative flex w-full flex-wrap items-stretch mb-3">
              <input
                type="url"
                className="px-3 py-3 placeholder-blueGray-300 text-blueGray-600 relative bg-white bg-white rounded text-sm border border-blueGray-300 outline-none focus:outline-none focus:ring w-full pl-10"
                placeholder="Product Image URL"
                value={imgURL}
                onChange={(e) => setImgURL(e.target.value)}
                required
              />
            </div>
            <div className="relative flex w-full flex-wrap items-stretch mb-3">
              <input
                type="text"
                className="px-3 py-3 placeholder-blueGray-300 text-blueGray-600 relative bg-white bg-white rounded text-sm border border-blueGray-300 outline-none focus:outline-none focus:ring w-full pl-10"
                placeholder="Product Description"
                value={description}
                onChange={(e) => setDescription(e.target.value)}
                required
              />
            </div>

            <div className="relative flex w-full flex-wrap items-stretch justify-between items-center">
              <Link className="text-green-500" to={`/product/` + id}>
                Back to product
              </Link>
              <Button color="green" ripple="light" type="submit">
                Update
              </Button>
            </div>
          </form>
        </div>
      </div>
    </div>
  )
}

export default EditProduct


Lastly, for the cases relating to products, let’s look at the cart view…


The Cart View

The Cart View

In this view, you can modify and place your orders. Once you place your order, it is immediately saved in Firestore. Below is how the code is written.


import CartItem from '../components/CartItem'
import Header from '../components/Header'
import { Link } from 'react-router-dom'
import { Button } from '@material-tailwind/react'
import { useEffect, useState } from 'react'
import { addToOrders } from '../firebase'
import { setAlert, setGlobalState, useGlobalState } from '../store'

const Cart = () => {
  const [cart] = useGlobalState('cart')
  const [isLoggedIn] = useGlobalState('isLoggedIn')
  const [total, setTotal] = useState(0)

  const getTotal = () => {
    let total = 0
    cart.forEach((item) => (total += item.qty * item.price))
    setTotal(total)
  }

  const placeOrder = () => {
    if (!isLoggedIn) return

    addToOrders(cart).then((data) => {
      setGlobalState('cart', [])
      setAlert(`Order Placed with Id: ${data.order}`)
    })
  }

  const clearCart = () => {
    setGlobalState('cart', [])
  }

  const toCurrency = (num) =>
    num.toLocaleString('en-US', {
      style: 'currency',
      currency: 'USD',
    })

  useEffect(() => getTotal(), [cart])

  return (
    <div className="addProduct">
      <Header />
      <div className="relative flex flex-col justify-center items-center">
        {cart.length > 0 ? (
          <div className="mt-10 ">
            <div className="relative flex w-full flex-wrap items-stretch px-8">
              <div className="flex flex-wrap justify-center items-center h-64 overflow-y-scroll">
                {cart.map((item, i) => (
                  <CartItem key={i} item={item} />
                ))}
              </div>
            </div>
            <div className="flex flex-row justify-between items-center my-4 px-8">
              <h4>Grand Total:</h4>
              <span className="text-sm text-green-500">
                {toCurrency(total)}
              </span>
            </div>
            <div className="flex flex-row justify-between items-center my-4 px-8">
              <Button
                onClick={clearCart}
                color="red"
                ripple="light"
                type="submit"
              >
                Clear Cart
              </Button>
              {isLoggedIn ? (
                <Button
                  onClick={placeOrder}
                  color="green"
                  ripple="light"
                  type="submit"
                >
                  Place Order
                </Button>
              ) : null}
            </div>
          </div>
        ) : (
          <div className="mt-10 text-center">
            <h4 className="mb-4">Cart empty, add some items to your cart</h4>
            <Link to="/" className="text-green-500">
              Choose Product
            </Link>
          </div>
        )}
      </div>
    </div>
  )
}

export default Cart


Next, let’s take care of the last four views in our tray…


The ChatList View

The Chat List View

This view simply lists out the recent conversations you’ve had with your customers so far. This is possible with the help of CometChat SDK, the codes below show you how it was implemented.


import Header from '../components/Header'
import { getConversations } from '../cometChat'
import { useEffect, useState } from 'react'
import { Link } from 'react-router-dom'
import { auth } from '../firebase'

const ChatList = () => {
  const [customers, setCustomers] = useState([])
  const [loaded, setLoaded] = useState(false)

  useEffect(() => {
    getConversations().then((conversation) => {
      console.log(conversation)
      setCustomers(conversation)
      setLoaded(true)
    })
  }, [])

  return (
    <div className="chatList">
      <Header />
      <div className="flex justify-center items-center p-10">
        <div className="relative mx-auto w-full">
          <div className="border-0 rounded-lg relative flex flex-col w-full">
            <div className="flex items-start justify-between my-4">
              <h3 className="text-md font-semibold">Recent Chats</h3>
            </div>
            {loaded
              ? customers.map((customer, i) => (
                  <Conversation
                    key={i}
                    currentUser={auth.currentUser.uid.toLowerCase()}
                    owner={customer.lastMessage.receiverId.toLowerCase()}
                    conversation={customer.lastMessage}
                  />
                ))
              : null}
          </div>
        </div>
      </div>
    </div>
  )
}

const Conversation = ({ conversation, currentUser, owner }) => {
  const possessor = (key) => {
    return currentUser == owner
      ? conversation.sender[key]
      : conversation.receiver[key]
  }

  const timeAgo = (date) => {
    let seconds = Math.floor((new Date() - date) / 1000)
    let interval = seconds / 31536000

    if (interval > 1) {
      return Math.floor(interval) + 'yr'
    }
    interval = seconds / 2592000
    if (interval > 1) {
      return Math.floor(interval) + 'mo'
    }
    interval = seconds / 86400
    if (interval > 1) {
      return Math.floor(interval) + 'd'
    }
    interval = seconds / 3600
    if (interval > 1) {
      return Math.floor(interval) + 'h'
    }
    interval = seconds / 60
    if (interval > 1) {
      return Math.floor(interval) + 'm'
    }
    return Math.floor(seconds) + 's'
  }

  return (
    <Link
      to={'/chat/' + possessor('uid')}
      className="flex flex-row justify-between items-center 
            mb-2 py-2 px-4 bg-gray-100 rounded-lg cursor-pointer"
    >
      <div className="">
        <h4 className="text-sm font-semibold">{possessor('name')}</h4>
        <p className="text-sm text-gray-500">{conversation.text}</p>
      </div>
      <span className="text-sm">
        {timeAgo(new Date(Number(conversation.sentAt) * 1000).getTime())}
      </span>
    </Link>
  )
}

export default ChatList


The Chat View

The Chat View

This is a one-on-one chat view for a seller and a buyer to communicate. The CometChat SDK makes this easier for us. The following code demonstrates how it works pretty well.


import { CometChat } from '@cometchat-pro/chat'
import { sendMessage, getMessages } from '../cometChat'
import { useEffect, useState } from 'react'
import { useParams } from 'react-router-dom'
import Header from '../components/Header'

const Chat = () => {
  const { receiverID } = useParams()
  const [message, setMessage] = useState('')
  const [messages, setMessages] = useState([])

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

  const handleGetMessages = () => {
    getMessages(receiverID).then((msgs) => {
      setMessages(msgs)
      scrollToEnd()
    })
  }

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

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

  useEffect(() => {
    handleGetMessages()
    listenForMessage(receiverID)
  }, [receiverID])

  return (
    <div className="chat">
      <Header />
      <div className="flex justify-center items-center p-10">
        <div className="relative mx-auto w-full">
          <div className="border-0 rounded-lg relative flex flex-col w-full">
            <div className="flex items-start justify-between p-5">
              <h3 className="text-md font-semibold">Chat</h3>
            </div>

            <div
              id="messages-container"
              className="relative p-6 flex-auto h-64 overflow-y-scroll"
              style={{ height: '20rem' }}
            >
              <div className="flex flex-col justify-center items-center">
                {messages.map((msg, i) =>
                  msg?.receiverId?.toLowerCase() != receiverID.toLowerCase() ? (
                    <div
                      key={i}
                      className="flex flex-col justify-center items-start w-full mb-4"
                    >
                      <div className="rounded-lg p-2 bg-green-100">
                        <p>{msg.text}</p>
                      </div>
                    </div>
                  ) : (
                    <div
                      key={i}
                      className="flex flex-col justify-center items-end w-full mb-4"
                    >
                      <div className="rounded-lg p-2 bg-gray-100">
                        <p>{msg.text}</p>
                      </div>
                    </div>
                  )
                )}
              </div>
            </div>

            <form
              onSubmit={handleSendMsg}
              className="flex flex-row justify-center items-center mt-4 py-4"
            >
              <input
                type="text"
                placeholder="Type Message..."
                className="px-3 py-8 placeholder-blueGray-300 text-blueGray-600 relative 
                            bg-green-100 rounded text-sm border border-blueGray-300 
                            outline-none focus:outline-none focus:ring w-full flex-1 border-0"
                value={message}
                onChange={(e) => setMessage(e.target.value)}
              />
            </form>
          </div>
        </div>
      </div>
    </div>
  )
}

export default Chat


The SignUp View Create a new file named SignUp.jsx and paste the codes below inside of it.


import Header from '../components/Header'
import Register from '../components/Register'

const SignUp = () => {
  return (
    <div className="signup">
      <Header />
      <Register />
    </div>
  )
}

export default SignUp


The SignIn View Let’s do the same for the SignIn view, create a new file called SignIn.jsx and paste the codes below inside of it.


import Header from '../components/Header'
import Login from '../components/Login'

const SignIn = () => {
  return (
    <div className="signIn">
      <Header />
      <Login />
    </div>
  )
}

export default SignIn


Amazing, we’ve just added all the essential views in our application, let’s tidy up the rest of the code…

The App.jsx File

This is the first file that runs before every other view and component in our application. In your App.jsx file, paste the following codes inside of it.


import { useEffect, useState } from 'react'
import { Routes, Route } from 'react-router-dom'
import { useGlobalState, setGlobalState, latestPrice } from './store'
import { auth, onAuthStateChanged } from './firebase'
import Product from './views/Product'
import Home from './views/Home'
import SignUp from './views/SignUp'
import SignIn from './views/SignIn'
import AuthGuard from './AuthGuard'
import EditProduct from './views/EditProduct'
import AddProduct from './views/AddProduct'
import Cart from './views/Cart'
import Chat from './views/Chat'
import ChatList from './views/ChatList'
import { loadWeb3 } from './shared/Freshers'

function App() {
  const [user, setUser] = useState(null)
  const [isLoaded, setIsLoaded] = useState(false)
  const [alert] = useGlobalState('alert')

  useEffect(() => {
    loadWeb3()
    onAuthStateChanged(auth, (user) => {
      if (user) {
        setUser(user)
        setGlobalState('isLoggedIn', true)
      } else {
        setUser(null)
        setGlobalState('isLoggedIn', false)
      }
      setIsLoaded(true)
    })
    latestPrice()
  }, [])

  return (
    <div className="App">
      {isLoaded ? (
        <>
          {alert.show ? (
            <div
              className={`text-white px-6 py-2 border-0 rounded relative bg-${alert.color}-500`}
            >
              <span className="text-xl inline-block mr-5 align-middle">
                <i className="fas fa-bell" />
              </span>
              <span className="inline-block align-middle mx-4">
                <b className="capitalize">Alert!</b> {alert.msg}!
              </span>
              <button
                onClick={() =>
                  setGlobalState('alert', { show: false, msg: '' })
                }
                className="absolute bg-transparent text-2xl font-semibold leading-none right-0 top-0 mt-2 mr-6 outline-none focus:outline-none"
              >
                <span>×</span>
              </button>
            </div>
          ) : null}
          <Routes>
            <Route path="/" element={<Home />} />
            <Route path="product/:id" element={<Product />} />
            <Route
              path="product/edit/:id"
              element={
                <AuthGuard user={user}>
                  <EditProduct />
                </AuthGuard>
              }
            />
            <Route
              path="product/add"
              element={
                <AuthGuard user={user}>
                  <AddProduct />
                </AuthGuard>
              }
            />
            <Route
              path="chat/:receiverID"
              element={
                <AuthGuard user={user}>
                  <Chat />
                </AuthGuard>
              }
            />
            <Route
              path="customers"
              element={
                <AuthGuard user={user}>
                  <ChatList />
                </AuthGuard>
              }
            />
            <Route path="cart" element={<Cart />} />
            <Route path="signin" element={<SignIn />} />
            <Route path="signup" element={<SignUp />} />
          </Routes>
        </>
      ) : null}
    </div>
  )
}

export default App

The AuthGuard.jsx File

This file contains the logic for bouncing out unauthenticated users from accessing secured routes in our application. Create a new file in the src folder and name it AuthGuard.jsx, then paste the following codes within it.


import { Navigate } from 'react-router-dom'

const AuthGuard = ({ user, children, redirectPath = '/signin' }) => {
  if (!user) {
    return <Navigate to={redirectPath} replace />
  }

  return children
}

export default AuthGuard

The Index.jsx File

Paste the following codes inside of the index.jsx file and save…


import React from 'react'
import ReactDOM from 'react-dom'
import { BrowserRouter } from 'react-router-dom'
import '@material-tailwind/react/tailwind.css'
import { initCometChat } from './cometChat'
import App from './App'

initCometChat().then(() => {
  ReactDOM.render(
    <BrowserRouter>
      <App />
    </BrowserRouter>,
    document.getElementById('root')
  )
})

The Store

Using the power of the react-hooks-global-state library, let's create a store to manage some of our global state variables. In the src directory, >> store create a file named index.jsx and paste the codes below inside of it.


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

const { setGlobalState, useGlobalState } = createGlobalState({
  isLoggedIn: false,
  alert: { show: false, msg: '', color: '' },
  cart: [],
  contract: null,
  connectedAccount: '',
  ethToUsd: 0,
})

const setAlert = (msg, color = 'amber') => {
  setGlobalState('alert', { show: true, msg, color })
  setTimeout(() => {
    setGlobalState('alert', { show: false, msg: '', color })
  }, 5000)
}

const latestPrice = async () => {
  await fetch(
    'https://min-api.cryptocompare.com/data/price?fsym=ETH&tsyms=USD'
  )
    .then((data) => data.json())
    .then((res) => setGlobalState('ethToUsd', res.USD))
}

export { useGlobalState, setGlobalState, setAlert, latestPrice }

The ABI Connector File

Lastly, we have the fresher.jsx file that serves as an interface between our smart contract’s Abi and the frontend. All the codes needed to interact with our smart contract are stored in this file, here is the code for it.


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

const { ethereum } = window

const getContract = async () => {
  const web3 = window.web3
  const networkId = await web3.eth.net.getId()
  const networkData = Store.networks[networkId]

  if (networkData) {
    const contract = new web3.eth.Contract(Store.abi, networkData.address)
    return contract
  } else {
    window.alert('Store contract not deployed to detected network.')
  }
}

const payWithEthers = async (product) => {
  try {
    const web3 = window.web3
    const seller = product.account
    const buyer = product.buyer
    const amount = web3.utils.toWei(product.price.toString(), 'ether')
    const purpose = `Sales of ${product.name}`

    const contract = await getContract()
    await contract.methods
      .payNow(seller, purpose)
      .send({ from: buyer, value: amount })
    return true
  } catch (error) {
    setAlert(error.message, 'red')
  }
}

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

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

    window.web3 = new Web3(ethereum)
    await ethereum.enable()

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

    const web3 = window.web3
    const accounts = await web3.eth.getAccounts()
    setGlobalState('connectedAccount', accounts[0])
  } catch (error) {
    alert('Please connect your metamask wallet!')
  }
}

export { loadWeb3, connectWallet, payWithEthers }


Within this shared folder, we have another folder called abis that contained the generated ABI code for our deployed store. Truffle generated these codes for us when we deployed the smart contract in the PART-ONE of this article.


Make sure you have included the .env file in the .gitignore file, this is very important so you don’t expose your private keys online.


If that’s all taken care of, then you should know that you have completed this project.

Congratulations!!!

Conclusion

Blockchain technology has come to stay, in this new world of smart contracts, DAO, NFTs, and DeFi applications, it is very important to arm yourself with blockchain development skills.

Can’t wait to see you in the next article, check the live demo and GitHub repo for more info.

Till next time, all the best!

About the Author

Gospel Darlington kick-started his journey as a software engineer in 2016. Over the years, he has grown full-blown skills in JavaScript stacks such as React, ReactNative, VueJs, and more.


He is currently freelancing, building apps for clients, writing technical tutorials teaching others how to do what he does.


Gospel Darlington is open and available to hear from you. You can reach him on LinkedIn, Facebook, Github, or on his website.