What you will be building, see the and for more info. live demo GitHub repo Introduction In the 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. PART-ONE 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. 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 , then activate the Google authentication service, as detailed below. Creating an App with Firebase freshers 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 as the platform. web 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 file. We will later use it in this project. .env Create a file called in the folder of this project and paste the following codes into it before saving. firebase.js src 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 next. 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. Creating an App with CometChat CometChat Use this as an example to create a new app with the name by clicking the . You will be presented with a modal where you can enter the app details. The image below shows an example. freshers Add New App button 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 file is following our convention. .env 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 folder of this project and paste the code below into it. src 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 if you have any challenges. git repo The Register Component This component is responsible for saving new users into Firebase. Navigate to the and create a file named . src >> components 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 Let’s also create another component called in the folder and paste the code below in it. Login.jsx src >> components 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 This component encapsulates the pages on our application. It was crafted with the free . Create a file named inside the directory and paste the codes below in it. Creative TIm Tailwind-Material UI Kit Header.jsx src >> components 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 This component renders the particular food properties to screen in a beautifully crafted card from tailwind CSS and Material design. Create a file called still in the and paste the following codes in it. The Food Component Food.jsx components folder 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. This component is responsible for rendering the entire collection of food data in our database. Let’s look at its code snippet. The Foods Components Still, in the , create another file called and paste the codes below in it. components directory Foods.jsx 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 component. CartItem 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 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 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 and create a file named , then, paste the code below inside of it. In fact, you will create all these files in the views folder. views directory Home.jsx 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. 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 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 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 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 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 Create a new file named and paste the codes below inside of it. The SignUp View SignUp.jsx import Header from '../components/Header' import Register from '../components/Register' const SignUp = () => { return ( <div className="signup"> <Header /> <Register /> </div> ) } export default SignUp Let’s do the same for the SignIn view, create a new file called and paste the codes below inside of it. The SignIn View SignIn.jsx 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 folder and name it , then paste the following codes within it. src AuthGuard.jsx 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 file and save… index.jsx 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 , >> create a file named and paste the codes below inside of it. src directory store index.jsx 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 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. fresher.jsx 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 that contained the generated ABI code for our deployed store. Truffle generated these codes for us when we deployed the smart contract in the of this article. abis PART-ONE Make sure you have included the file in the file, this is very important so you don’t expose your private keys online. .env .gitignore 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 and for more info. live demo GitHub repo 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 , , , or on his . LinkedIn Facebook Github website