visit
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.yarn add firebase #Database SDK
yarn add @cometchat-pro/chat #Chat SDK
yarn add @material-tailwind/react #UI Kit
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 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.
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.
On the project’s overview page, select the add app option and pick web as the platform.
Return to the project overview page after completing the SDK config registration, as shown in the image below.
Now you click on the project settings to copy your SDK configuration 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 and signup if you don’t have an account with them. Next, log in and you will be presented with the screen below.
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.
Following the creation of your app, you will be directed to your dashboard, which should look something like this.
You must also copy these keys to the .env file. Finally, delete the preloaded users, and groups as shown in the images below.
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,
}
Let’s start crafting out all the components one after the other, always refer to the if you have any challenges.
The 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
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
The 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
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.
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
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
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
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.
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
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
The Edit Product View
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
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
The ChatList 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
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
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
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
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')
)
})
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(
'//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 }
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.