DEV Community

Cover image for How to Build a Web3 Airbnb 2.0 Clone with React, Solidity, and CometChat
Gospel Darlington
Gospel Darlington

Posted on

6

How to Build a Web3 Airbnb 2.0 Clone with React, Solidity, and CometChat

What you will be building see the live demo and the git repo.

Introduction

Are you looking to create a cutting-edge platform that leverages the power of web3 to transform the way people book and share accommodations? If so, this tutorial on building a web3 Airbnb clone using React, Solidity, and CometChat is for you

By integrating blockchain technology, real-time communication, and user-generated content, you can create an interactive platform that revolutionizes the traditional apartment booking experience.

Whether you're an experienced developer or just starting out, this step-by-step guide will walk you through the process of bringing your vision to life. So why not start building your own Web3 Airbnb clone today and disrupt the travel industry?

By the way, Subscribe to my YouTube channel to learn how to build a Web3 app from scratch. I also offer a wide range of premium web3 content and services, be it private tutorship, hourly consultancy, other development services, or web3 educational material production, you can book my services here.

Now, let’s jump into this tutorial.

Prerequisites

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

  • Nodejs (Important)
  • Ethers js
  • Hardhat
  • Yarn
  • Metamask
  • React
  • Tailwind CSS
  • CometChat SDK

Installing Dependencies

Clone the starter kit and open it in VS Code using the command below:

git clone https://github.com/Daltonic/tailwind_ethers_starter_kit <PROJECT_NAME>
cd <PROJECT_NAME>
Enter fullscreen mode Exit fullscreen mode
{
"name": "DappBnbApp",
"private": true,
"version": "0.0.0",
"scripts": {
"start": "react-app-rewired start",
"build": "react-app-rewired build",
"test": "react-app-rewired test",
"eject": "react-scripts eject",
"dev" : "yarn hardhat run scripts/deploy.js && yarn start"
},
"dependencies": {
"@cometchat-pro/chat": "3.0.11",
"@faker-js/faker": "^7.6.0",
"@nomiclabs/hardhat-ethers": "^2.1.0",
"@nomiclabs/hardhat-waffle": "^2.0.3",
"ethereum-waffle": "^3.4.4",
"ethers": "^5.6.9",
"hardhat": "^2.10.1",
"ipfs-http-client": "^57.0.3",
"moment": "^2.29.4",
"react": "^17.0.2",
"react-datepicker": "^4.10.0",
"react-dom": "^17.0.2",
"react-hooks-global-state": "^1.0.2",
"react-icons": "^4.3.1",
"react-identicons": "^1.2.5",
"react-moment": "^1.1.2",
"react-router-dom": "6",
"react-scripts": "5.0.0",
"react-toastify": "^9.1.1",
"swiper": "8.4.4",
"web-vitals": "^2.1.4"
},
"devDependencies": {
"@openzeppelin/contracts": "^4.5.0",
"@tailwindcss/forms": "0.4.0",
"assert": "^2.0.0",
"autoprefixer": "10.4.2",
"babel-polyfill": "^6.26.0",
"babel-preset-env": "^1.7.0",
"babel-preset-es2015": "^6.24.1",
"babel-preset-stage-2": "^6.24.1",
"babel-preset-stage-3": "^6.24.1",
"babel-register": "^6.26.0",
"buffer": "^6.0.3",
"chai": "^4.3.6",
"chai-as-promised": "^7.1.1",
"crypto-browserify": "^3.12.0",
"dotenv": "^16.0.0",
"https-browserify": "^1.0.0",
"mnemonics": "^1.1.3",
"os-browserify": "^0.3.0",
"postcss": "8.4.5",
"process": "^0.11.10",
"react-app-rewired": "^2.1.11",
"stream-browserify": "^3.0.0",
"stream-http": "^3.2.0",
"tailwindcss": "3.0.18",
"url": "^0.11.0"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}
view raw package.json hosted with ❤ by GitHub

Now, run **yarn install** on the terminal to have all the dependencies for this project installed.

Configuring CometChat SDK

Follow the steps below to configure the CometChat SDK; at the end, you must save these keys as an environment variable.

STEP 1:
Head to CometChat Dashboard and create an account.

Register a new CometChat account if you do not have one

STEP 2:
Log in to the CometChat dashboard, only after registering.

Log in to the CometChat Dashboard with your created account

STEP 3:
From the dashboard, add a new app called DappBnb.

Create a new CometChat app - Step 1

Create a new CometChat app - Step 2

STEP 4:
Select the app you just created from the list.

Select your created app

STEP 5:
From the Quick Start copy the APP_ID, REGION, and AUTH_KEY, to your .env file. See the image and code snippet.

Copy the the APP_ID, REGION, and AUTH_KEY

Replace the REACT_COMET_CHAT placeholder keys with their appropriate values.

REACT_APP_COMETCHAT_APP_ID=****************
REACT_APP_COMETCHAT_AUTH_KEY=******************************
REACT_APP_COMETCHAT_REGION=**
Enter fullscreen mode Exit fullscreen mode

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

Configuring the Hardhat script

At the root of this project, open the hardhat.config.js file and replace its content with the following settings.

require("@nomiclabs/hardhat-waffle");
require('dotenv').config()
module.exports = {
defaultNetwork: "localhost",
networks: {
hardhat: {
},
localhost: {
url: "http://127.0.0.1:8545"
},
},
solidity: {
version: '0.8.11',
settings: {
optimizer: {
enabled: true,
runs: 200
}
}
},
paths: {
sources: "./src/contracts",
artifacts: "./src/abis"
},
mocha: {
timeout: 40000
}
}

The above script instructs hardhat on these three important rules.

  • Networks: This block contains the configurations for your choice of networks. On deployment, hardhat will require you to specify a network for shipping your smart contracts.

  • Solidity: This describes the version of the compiler to be used by hardhat for compiling your smart contract codes into bytecodes and abi.

  • Paths: This simply informs hardhat of the location of your smart contracts and also a place to dump the output of the compiler which is the ABI.

Configuring the Deployment Script

Navigate to the scripts folder and then to your deploy.js file and paste the code below into it. If you can't find a script folder, make one, create a deploy.js file, and paste the following code into it.

require('@nomiclabs/hardhat-waffle')
require('dotenv').config()
module.exports = {
defaultNetwork: 'localhost',
networks: {
hardhat: {},
localhost: {
url: 'http://127.0.0.1:8545',
},
},
solidity: {
version: '0.8.11',
settings: {
optimizer: {
enabled: true,
runs: 200,
},
},
},
paths: {
sources: './src/contracts',
artifacts: './src/abis',
},
mocha: {
timeout: 40000,
},
}

When run as a Hardhat deployment command, the above script will deploy your specified smart contract to the network of your choice.

Check out this video to learn how to properly set up a web3 project with ReactJs.

The Smart Contract File

Now that we've completed the initial configurations, let's create the smart contract for this build. Create a new folder called **contracts** in your project's **src** directory.

Create a new file called **DappBnb****.sol** within this contracts folder; this file will contain all of the logic that governs the smart contract.

Copy, paste, and save the following codes into the **DappBnb****.sol** file. See the complete code below.

// SPDX-License-Identifier: MIT
pragma solidity >=0.7.0 <0.9.0;
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract DappBnb is Ownable, ReentrancyGuard {
using Counters for Counters.Counter;
Counters.Counter private _totalAppartments;
struct ApartmentStruct {
uint id;
string name;
string description;
string images;
uint rooms;
uint price;
address owner;
bool booked;
bool deleted;
bool availablity;
uint timestamp;
}
struct BookingStruct {
uint id;
address tenant;
uint date;
uint price;
bool checked;
bool cancelled;
}
struct ReviewStruct {
uint id;
uint appartmentId;
string reviewText;
uint timestamp;
address owner;
}
event SecurityFeeUpdated(uint newFee);
uint public securityFee;
uint public taxPercent;
mapping(uint => ApartmentStruct) apartments;
mapping(uint => BookingStruct[]) bookingsOf;
mapping(uint => ReviewStruct[]) reviewsOf;
mapping(uint => bool) appartmentExist;
mapping(uint => uint[]) bookedDates;
mapping(uint => mapping(uint => bool)) isDateBooked;
mapping(address => mapping(uint => bool)) hasBooked;
constructor(uint _taxPercent, uint _securityFee) {
taxPercent = _taxPercent;
securityFee = _securityFee;
}
function createAppartment(
string memory name,
string memory description,
string memory images,
uint rooms,
uint price
) public {
require(bytes(name).length > 0, "Name cannot be empty");
require(bytes(description).length > 0, "Description cannot be empty");
require(bytes(images).length > 0, "Images cannot be empty");
require(rooms > 0, "Rooms cannot be zero");
require(price > 0 ether, "Price cannot be zero");
_totalAppartments.increment();
ApartmentStruct memory lodge;
lodge.id = _totalAppartments.current();
lodge.name = name;
lodge.description = description;
lodge.images = images;
lodge.rooms = rooms;
lodge.price = price;
lodge.owner = msg.sender;
lodge.availablity = true;
lodge.timestamp = block.timestamp;
appartmentExist[lodge.id] = true;
apartments[_totalAppartments.current()] = lodge;
}
function updateAppartment
(
uint id,
string memory name,
string memory description,
string memory images,
uint rooms,
uint price
) public {
require(appartmentExist[id] == true, "Appartment not found");
require(msg.sender == apartments[id].owner, "Unauthorized personnel, owner only");
require(bytes(name).length > 0, "Name cannot be empty");
require(bytes(description).length > 0, "Description cannot be empty");
require(bytes(images).length > 0, "Images cannot be empty");
require(rooms > 0, "Rooms cannot be zero");
require(price > 0 ether, "Price cannot be zero");
ApartmentStruct memory lodge = apartments[id];
lodge.name = name;
lodge.description = description;
lodge.images = images;
lodge.rooms = rooms;
lodge.price = price;
apartments[id] = lodge;
}
function deleteAppartment(uint id) public {
require(appartmentExist[id] == true, "Appartment not found");
require(apartments[id].owner == msg.sender, "Unauthorized entity");
appartmentExist[id] = false;
apartments[id].deleted = true;
}
function getApartments() public view returns (ApartmentStruct[] memory Apartments) {
uint256 totalSpace;
for (uint i = 1; i <= _totalAppartments.current(); i++) {
if(!apartments[i].deleted) totalSpace++;
}
Apartments = new ApartmentStruct[](totalSpace);
uint256 j = 0;
for (uint i = 1; i <= _totalAppartments.current(); i++) {
if(!apartments[i].deleted) {
Apartments[j] = apartments[i];
j++;
}
}
}
function getApartment(uint id) public view returns (ApartmentStruct memory) {
return apartments[id];
}
function bookApartment(uint id, uint[] memory dates) public payable {
require(appartmentExist[id], "Apartment not found!");
require(msg.value >= apartments[id].price * dates.length + securityFee, "Insufficient fund!");
require(datesAreCleared(id, dates), "Booked date found among dates!");
for (uint i = 0; i < dates.length; i++) {
BookingStruct memory booking;
booking.id = bookingsOf[id].length;
booking.tenant = msg.sender;
booking.date = dates[i];
booking.price = apartments[id].price;
bookingsOf[id].push(booking);
isDateBooked[id][dates[i]] = true;
bookedDates[id].push(dates[i]);
}
}
function datesAreCleared(uint id, uint[] memory dates) internal view returns (bool) {
bool lastCheck = true;
for(uint i=0; i < dates.length; i++) {
for(uint j=0; j < bookedDates[id].length; j++) {
if(dates[i] == bookedDates[id][j]) lastCheck = false;
}
}
return lastCheck;
}
function checkInApartment(uint id, uint bookingId) public {
require(msg.sender == bookingsOf[id][bookingId].tenant, "Unauthorized tenant!");
require(!bookingsOf[id][bookingId].checked, "Apartment already checked on this date!");
bookingsOf[id][bookingId].checked = true;
uint price = bookingsOf[id][bookingId].price;
uint fee = (price * taxPercent) / 100;
hasBooked[msg.sender][id] = true;
payTo(apartments[id].owner, (price - fee));
payTo(owner(), fee);
payTo(msg.sender, securityFee);
}
function claimFunds(uint id, uint bookingId) public {
require(msg.sender == apartments[id].owner, "Unauthorized entity");
require(!bookingsOf[id][bookingId].checked, "Apartment already checked on this date!");
uint price = bookingsOf[id][bookingId].price;
uint fee = (price * taxPercent) / 100;
payTo(apartments[id].owner, (price - fee));
payTo(owner(), fee);
payTo(msg.sender, securityFee);
}
function refundBooking(uint id, uint bookingId, uint date) public nonReentrant {
require(!bookingsOf[id][bookingId].checked, "Apartment already checked on this date!");
if(msg.sender != owner()) {
require(msg.sender == bookingsOf[id][bookingId].tenant, "Unauthorized tenant!");
require(bookingsOf[id][bookingId].date > currentTime(), "Can no longer refund, booking date started");
}
bookingsOf[id][bookingId].cancelled = true;
isDateBooked[id][date] = false;
uint lastIndex = bookedDates[id].length - 1;
uint lastBookingId = bookedDates[id][lastIndex];
bookedDates[id][bookingId] = lastBookingId;
bookedDates[id].pop();
uint price = bookingsOf[id][bookingId].price;
uint fee = securityFee * taxPercent / 100;
payTo(apartments[id].owner, (securityFee - fee));
payTo(owner(), fee);
payTo(msg.sender, price);
}
function hasBookedDateReached(uint id,uint bookingId) public view returns(bool) {
return bookingsOf[id][bookingId].date < currentTime();
}
function getUnavailableDates(uint id) public view returns (uint[] memory) {
return bookedDates[id];
}
function getBookings(uint id) public view returns (BookingStruct[] memory) {
return bookingsOf[id];
}
function getBooking(uint id, uint bookingId) public view returns (BookingStruct memory) {
return bookingsOf[id][bookingId];
}
function updateSecurityFee(uint newFee) public onlyOwner {
require(newFee > 0);
securityFee = newFee;
emit SecurityFeeUpdated(newFee);
}
function updateTaxPercent(uint newTaxPercent) public onlyOwner {
taxPercent = newTaxPercent;
}
function payTo(address to, uint256 amount) internal {
(bool success, ) = payable(to).call{value: amount}("");
require(success);
}
function addReview(uint appartmentId, string memory reviewText) public {
require(appartmentExist[appartmentId],"Appartment not available");
require(hasBooked[msg.sender][appartmentId],"Book first before review");
require(bytes(reviewText).length > 0, "Review text cannot be empty");
ReviewStruct memory review;
review.id = reviewsOf[appartmentId].length;
review.appartmentId = appartmentId;
review.reviewText = reviewText;
review.timestamp = block.timestamp;
review.owner = msg.sender;
reviewsOf[appartmentId].push(review);
}
function getReviews(uint appartmentId) public view returns (ReviewStruct[] memory) {
return reviewsOf[appartmentId];
}
function tenantBooked(uint appartmentId) public view returns (bool) {
return hasBooked[msg.sender][appartmentId];
}
function currentTime() internal view returns (uint256) {
uint256 newNum = (block.timestamp * 1000) + 1000;
return newNum;
}
}
view raw DappBnb.sol hosted with ❤ by GitHub

I have a book to help your master the web3 language (Solidity), grab your copy here.

Capturing Smart Contract Development

Now, let's go over some of the details of what's going on in the smart contract above. We have the following items:

Imported Dependencies
The "import" statements in this smart contract are used to import external dependencies from the OpenZeppelin library, which is a widely-used and trusted collection of pre-built smart contract components.

The first dependency, "@openzeppelin/contracts/access/Ownable.sol", is used to access the deployer of the contract. The "Ownable" contract provides a basic access control mechanism where there is an account that is designated as the owner, and this owner can modify the state of the contract. The deployer of the contract is typically the owner by default, and this dependency allows the contract to identify and interact with the owner.

The second dependency, "@openzeppelin/contracts/utils/Counters.sol", is used to attach unique IDs to apartments. The "Counters" library provides a simple way to increment and decrement counters, which is useful for creating unique IDs for each apartment listed on the platform.

The third dependency, "@openzeppelin/contracts/security/ReentrancyGuard.sol", is used to protect a specific function from reentrancy attacks. Reentrancy is a type of attack where an attacker can call a function multiple times before the first call has finished executing, which can lead to unexpected behavior and security vulnerabilities. The "ReentrancyGuard" contract provides a simple way to protect functions from this type of attack, which is important for ensuring the security and integrity of the platform.

STRUCTS
AppartmentStruct: This contains the necessary information about every apartment that is been posted on the platform.
BookingStruct: This contains details about every booking done on the platform.
ReviewStruct: This contains the reviews for each apartment by other users of the platform.

STATE VARIABLES
_totalAppartments: This variable uses OpenZeppelin’s Counterlibrary to initialize the counter and assign unique ids to newly created apartments.

TaxPercent: This variable holds the percentage that the owner of the contract gets from every booked apartment.

SecurityFee: This variable holds the amount that the apartment owner holds when a user books the apartment.

Mappings
apartments: This mapping variable stores a newly created apartment with a specific Id.
bookingsOf: This mapping variable holds the total bookings for a particular apartment.
reviewsOf: This mapping variable holds the reviews an apartment has
apartmentExist: This mapping variable checks for the existence of an apartment
bookedDates: This mapping variable holds the list of dates booked by users for a particular apartment.
isDateBooked: This mapping checks if the date of booking for an apartment has been taken.
hasBooked: This mapping checks if a user has at least booked an apartment once.

Constructor
This is used to initialize the state of the smart contract variables and other essential operations. In this example, we assigned the tax percent and security fee a value.

Events
SecurityFeeUpdated: This event fires when the deployer updates the security fee.

Apartment Functions
CreateApartment: This function is used to add an apartment to the platform
UpdateApartment: This function is used to edit certain information about an apartment on the platform supplied by the owner of the apartment.
DeleteApartment: This function is used to delete an apartment from the platform supplied by the owner of the apartment.
GetApartments: This function is used to list the available apartments on the platform.
GetApartment: This function is used to get a single apartment on the platform.

Booking functions
BookApartment: This function is used to book a specific apartment for a number of days, it is used to secure a date or dates for the apartment, and some necessary funds are been sent for the booking of the apartment to the platform.
DatesAreCleared: This function checks if the dates a user is booking an apartment are free.
HasBookedDateReached: This function checks if the date a user booked an apartment
GetUnavailableDates: This function returns all the days that have been booked on the platform.
GetBookings: This function lists the total bookings for an apartment.
GetBooking: This function returns a single booking for an apartment.
TenantBooked: This function returns true or false if a user has checked into an apartment.

Reviews Function
AddReview: This function receives review data from other users from the platform reviewing an apartment on the platform.
GetReviews: This returns the total reviews for an apartment.

Payment Functions
CheckInApartment: This function is used to check-In on the day the apartment booking was anticipated, and it disburses the funds to necessary accounts.
RefundBooking: This function is used to reclaim the funds before the day that was booked by the user who is anticipating the apartment.
ClaimFunds: This function is used by the apartment owner to disburse funds in the apartment when the day the booker anticipated passes without him or her checking-In.
PayTo: This function sends money to an account.

TaxPercent and Security Fee Functions
UpdateSecurityfee: This function is used to edit or change the security fee value.
UpdateTaxPercent: This function is used to edit or change the tax percent value.

Time function
CurrentTime: This function adjusts the date returned by the solidity block.timestamp to avoid conflicts when it’s retrieved by the React frontend because the timestamp returned by solidity’s block.timestamp is three (3) digits less than the javascript time function.

With all the above functions understood, copy them into a file named **DappBnb****.sol** in the contracts folder within the src directory.

Next, run the commands below to deploy the smart contract into the network.

yarn hardhat node # Terminal #1
yarn hardhat run scripts/deploy.js # Terminal #2
Enter fullscreen mode Exit fullscreen mode

Activities of Deployment on the Terminal

If you need further help configuring Hardhat or deploying your Fullstack DApp, watch this video.

Developing the Frontend

Now that we have our smart contract on the network and all of our artifacts (bytecodes and ABI) generated, let's get the front end ready with React.

Components

In the src directory, create a new folder called **components** to house all of the React components for this project.

Header component

Header Component

This component contains the logo and relevant navigations and a connect wallet button, see the code below.

import { FaAirbnb, FaSearch } from 'react-icons/fa'
import { Link, useNavigate } from 'react-router-dom'
import { connectWallet } from '../Blockchain.services'
import { setGlobalState, truncate, useGlobalState } from '../store'
const Header = () => {
const [connectedAccount] = useGlobalState('connectedAccount')
return (
<header className="flex justify-between items-center p-4 px-8 sm:px-10 md:px-14 border-b-2 border-b-slate-200 w-full">
<Link to={'/'}>
<p className="text-[#ff385c] flex items-center text-xl">
<FaAirbnb className=" font-semibold" />
DappBnb
</p>
</Link>
<ButtonGroup />
{connectedAccount ? (
<button className="p-2 bg-[#ff385c] text-white rounded-full text-sm">
{truncate(connectedAccount, 4, 4, 11)}
</button>
) : (
<button
onClick={connectWallet}
className="p-2 bg-[#ff385c] text-white rounded-full text-sm"
>
Connect wallet
</button>
)}
</header>
)
}
const ButtonGroup = () => {
const [currentUser] = useGlobalState('currentUser')
const navigate = useNavigate()
const handleNavigate = () => {
if (currentUser) {
navigate('/recentconversations')
} else {
setGlobalState('authModal', 'scale-100')
}
}
return (
<div
className="md:flex hidden items-center justify-center shadow-gray-400
shadow-sm overflow-hidden rounded-full cursor-pointer"
>
<div className="inline-flex" role="group">
<button
onClick={handleNavigate}
className="
rounded-l-full
px-5
md:py-2 py-1
border border-slate-200
text-[#ff385c]
font-medium
text-sm
leading-tight
hover:bg-black hover:bg-opacity-5
focus:outline-none focus:ring-0
transition
duration-150
ease-in-out
"
>
Customers
</button>
<Link to={'/addRoom'}>
<button
type="button"
className="
px-5
md:py-2 py-1
border border-slate-200
text-[#ff385c]
font-medium
text-sm
leading-tight
hover:bg-black hover:bg-opacity-5
focus:outline-none focus:ring-0
transition
duration-150
ease-in-out
"
>
Add Rooms
</button>
</Link>
<button
onClick={handleNavigate}
className="
rounded-r-full
px-5
md:py-2 py-1
border border-slate-200
text-[#ff385c]
font-medium
text-sm
leading-tight
hover:bg-black hover:bg-opacity-5
focus:outline-none focus:ring-0
transition
duration-150
ease-in-out
"
>
<p className="flex items-center">Chats</p>
</button>
</div>
</div>
)
}
export default Header
view raw Header.jsx hosted with ❤ by GitHub

Within the components folder, create a file called Header.jsx and paste the above codes into it.

Category Component

Category Component

This component carries rental categories, as shown above, see the code below.

import React from 'react'
import {TbBeach} from 'react-icons/tb'
import {GiCampingTent, GiIsland} from 'react-icons/gi'
import {BsSnow2} from 'react-icons/bs'
import {RiHotelLine} from 'react-icons/ri'
const Category = () => {
return (
<div className='flex justify-center space-x-5 sm:space-x-14 p-4 px-4 border-b-2 border-b-slate-200 text-gray-600'>
<p className='flex flex-col items-center hover:text-black border-b-2 border-transparent hover:border-black hover:cursor-pointer pb-2'>
<TbBeach className='text-3xl'/>
Beach
</p>
<p className='flex flex-col items-center hover:text-black border-b-2 border-transparent hover:border-black hover:cursor-pointer pb-2'>
<GiIsland className='text-3xl'/>
Island
</p>
<p className='flex flex-col items-center hover:text-black border-b-2 border-transparent hover:border-black hover:cursor-pointer pb-2'>
<BsSnow2 className='text-3xl' />
Arctic
</p>
<p className='flex flex-col items-center hover:text-black border-b-2 border-transparent hover:border-black hover:cursor-pointer pb-2'>
<GiCampingTent className='text-3xl'/>
Camping
</p>
<p className='flex flex-col items-center hover:text-black border-b-2 border-transparent hover:border-black hover:cursor-pointer pb-2'>
<RiHotelLine className='text-3xl'/>
Hotel
</p>
</div>
)
}
export default Category
view raw Category.jsx hosted with ❤ by GitHub

Again, in the components folder, create a new file called Category.jsx and paste the above codes into it.

Card Component

Card Component

This component contains the images of the apartment in slides and some other relevant information as seen above. see the code below.

import React from 'react'
import { FaStar, FaEthereum } from 'react-icons/fa'
import { Link } from 'react-router-dom'
import ImageSlider from './ImageSlider'
const Card = ({ appartment }) => {
return (
<div className="shadow-md w-96 text-xl pb-5 rounded-b-2xl mb-20">
<Link to={'/room/' + appartment.id}>
<ImageSlider images={appartment.images} />
</Link>
<div className="px-4">
<div className="flex justify-between items-start mt-2">
<p className="font-semibold capitalize text-[15px]">
{appartment.name}
</p>
<p className="flex justify-start items-center space-x-2 text-sm">
<FaStar />
<span>New</span>
</p>
</div>
<div className="flex justify-between items-center text-sm">
<p className="text-gray-700">{appartment.timestamp}</p>
<b className="flex justify-start items-center space-x-1 font-semibold">
<FaEthereum />
<span>
{appartment.price} night {appartment.deleted}
</span>
</b>
</div>
</div>
</div>
)
}
export default Card
view raw Card.jsx hosted with ❤ by GitHub

Now again, in the components folder, create a new file called Card.jsx and paste the above codes into it.

ImageSlider component

Image Slider

This component contains a SwiperJs slider which is used for the apartment images display, see code below.

import { Swiper, SwiperSlide } from "swiper/react";
import { Autoplay, Pagination, Navigation } from "swiper";
const ImageSlider = ({ images }) => {
return (
<Swiper
spaceBetween={30}
centeredSlides={true}
autoplay={{
delay: 2500,
disableOnInteraction: false,
}}
pagination={{
clickable: true,
}}
navigation={false}
modules={[Autoplay, Pagination, Navigation]}
className="w-96 h-52 rounded-t-2xl overflow-hidden"
>
{images.map((url, i) => (
<SwiperSlide key={i}>
<img
className="w-full"
src={url}
alt="image slide 1"
/>
</SwiperSlide>
))}
</Swiper>
);
};
export default ImageSlider;
view raw ImageSlider.jsx hosted with ❤ by GitHub

In the components folder, create a new file called ImageSlider.jsx and paste the above codes into it.

CardCollection Component

Card Collection

This component loads collections of different posted apartments see the code below.

import Card from './Card'
const CardCollection = ({ appartments }) => {
return (
<div className="py-8 px-14 flex justify-center flex-wrap space-x-4 w-full">
{
appartments.length > 0 ?
appartments.map((room, i) =>
<Card appartment={room} key={i}/>
)
: 'No appartments yet!'
}
</div>
)
}
export default CardCollection

This time again, in the components folder, create a new file called CardCollection.jsx and paste the above codes into it.

AddReview Component

Add Review Component

This component is a modal component for adding reviews see the code below.

import { useState } from 'react'
import { useGlobalState, setGlobalState } from '../store'
import { FaTimes } from 'react-icons/fa'
import { toast } from 'react-toastify'
import { addReview, loadReviews } from '../Blockchain.services.js'
import { useParams } from 'react-router-dom'
const AddReview = () => {
const [reviewText, setReviewText] = useState('')
const [reviewModal] = useGlobalState('reviewModal')
const { id } = useParams()
const resetForm = () => {
setReviewText('')
}
const handleSubmit = async (e) => {
e.preventDefault()
if (!reviewText) return
await toast.promise(
new Promise(async (resolve, reject) => {
await addReview(id, reviewText)
.then(async () => {
setGlobalState('reviewModal', 'scale-0')
resetForm()
await loadReviews(id)
resolve()
})
.catch(() => reject())
}),
{
pending: 'Approve transaction...',
success: 'review posted successfully 👌',
error: 'Encountered error 🤯',
}
)
}
return (
<div
className={`fixed top-0 left-0 w-screen h-screen flex items-center justify-center
bg-black bg-opacity-50 transform z-[3000] transition-transform duration-300 ${reviewModal}`}
>
<div className="bg-white shadow-lg shadow-slate-900 rounded-xl w-11/12 md:w-2/5 h-7/12 p-6">
<form className="flex flex-col" onSubmit={handleSubmit}>
<div className="flex flex-row justify-between items-center">
<p className="font-semibold">Add a review today</p>
<button
type="button"
className="border-0 bg-transparent focus:outline-none"
onClick={() => setGlobalState('reviewModal', 'scale-0')}
>
<FaTimes className="text-gray-400" />
</button>
</div>
<div className="flex flex-col justify-center items-center rounded-xl mt-5">
<div
className="flex justify-center items-center rounded-full overflow-hidden
h-10 w-40 shadow-md shadow-slate-300 p-4"
>
<p className="text-lg font-bold text-slate-700"> DappBnB</p>
</div>
<p className="p-2">Add your review below</p>
</div>
<div className="flex flex-row justify-between items-center bg-gray-300 rounded-xl mt-5 p-2">
<textarea
className="block w-full text-sm resize-none text-slate-500
bg-transparent border-0 focus:outline-none focus:ring-0 h-20"
type="text"
name="comment"
placeholder="Drop your review..."
value={reviewText}
onChange={(e) => setReviewText(e.target.value)}
required
></textarea>
</div>
<button
type="submit"
className="flex flex-row justify-center items-center w-full text-white text-md
bg-[#ff385c] py-2 px-5 rounded-full drop-shadow-xl border
focus:outline-none focus:ring mt-5"
>
Submit
</button>
</form>
</div>
</div>
)
}
export default AddReview
view raw AddReview.jsx hosted with ❤ by GitHub

Create a new file called AddReview.jsx in the components folder and paste the above codes into it.

AuthModal Component

Auth Modal Component

This component authenticates a user either to log in or sign up before being able to chat it uses the CometChat SDK for authentication, see the code below.

import { FaTimes } from 'react-icons/fa'
import { useNavigate } from 'react-router-dom'
import { toast } from 'react-toastify'
import { loginWithCometChat, signUpWithCometChat } from '../services/Chat'
import { setGlobalState, useGlobalState } from '../store'
const AuthModal = () => {
const [authModal] = useGlobalState('authModal')
const navigate = useNavigate()
const handleSignUp = async () => {
await toast.promise(
new Promise(async (resolve, reject) => {
await signUpWithCometChat()
.then(() => resolve())
.catch((error) => reject(error))
}),
{
pending: 'Registering...',
success: 'Account created, please login 👌',
error: 'Encountered error 🤯',
}
)
}
const handleLogin = async () => {
await toast.promise(
new Promise(async (resolve, reject) => {
await loginWithCometChat()
.then(async (user) => {
setGlobalState('currentUser', user)
setGlobalState('authModal', 'scale-0')
navigate('/recentconversations')
resolve(user)
})
.catch((error) => reject(error))
}),
{
pending: 'Authenticating...',
success: 'Logged in successfully 👌',
error: 'Encountered error 🤯',
}
)
}
return (
<div
className={`fixed top-0 left-0 w-screen h-screen flex
items-center justify-center bg-black bg-opacity-50 z-50
transform transition-transform duration-300 ${authModal}`}
>
<div
className="bg-white shadow-xl shadow-[#b2253f] rounded-xl
w-11/12 md:w-2/5 h-7/12 p-6"
>
<div className="flex flex-col">
<div className="flex justify-between items-center">
<p className="font-semibold">Login to Chat</p>
<button
onClick={() => setGlobalState('authModal', 'scale-0')}
type="button"
className="border-0 bg-transparent focus:outline-none"
>
<FaTimes />
</button>
</div>
<div className="flex justify-start items-center space-x-2 mt-5">
<button
onClick={handleSignUp}
className="bg-[#ff385c] p-2 px-6 rounded-full text-white shadow-md
shadow-gray-300 transform transition-transform duration-30 w-fit"
>
Sign up
</button>
<button
onClick={handleLogin}
className="border border-[#ff385c] text-[#ff385c] p-2 px-6 rounded-full shadow-md
shadow-gray-300 transform transition-transform duration-30 w-fit"
>
Login
</button>
</div>
</div>
</div>
</div>
)
}
export default AuthModal
view raw AuthModal.jsx hosted with ❤ by GitHub

Add it to the list by creating a new file called AddModal.jsx in the components folder and paste the above codes into it.

Footer Component

Footer Component

This component carries certain information like site name and copywriting information.

view raw Footer.jsx hosted with ❤ by GitHub

Create another file called Footer.jsx in the components folder and paste the above codes into it.

Views

Create the views folder inside the src directory and sequentially add the following pages within it.

Home view

Home View

This page contains the available apartments on the platform see the code below.

import CardCollection from "../components/CardCollection"
import Category from "../components/Category"
import { useGlobalState } from "../store"
const Home = () => {
const [appartments] = useGlobalState("appartments")
return (
<div>
<Category />
<CardCollection appartments={appartments} />
</div>
)
}
export default Home
view raw Home.jsx hosted with ❤ by GitHub

Create a file called Home.jsx in the views folder and paste the above codes into it.

AddRoom View

Add Room View

This page contains a form that is used for adding apartments to the platform. See the code below.

import { useState } from 'react'
import { FaTimes } from 'react-icons/fa'
import { createAppartment,loadAppartments } from '../Blockchain.services'
import { truncate } from '../store'
import { toast } from 'react-toastify'
import { useNavigate } from 'react-router-dom'
const AddRoom = () => {
const [name, setName] = useState('')
const [description, setDescription] = useState('')
const [location, setLocation] = useState('')
const [rooms, setRooms] = useState('')
const [images, setImages] = useState('')
const [price, setPrice] = useState('')
const [links, setLinks] = useState([])
const navigate = useNavigate()
const handleSubmit = async (e) => {
e.preventDefault()
if (!name || !location || !description || !rooms || links.length != 5 || !price)
return
const params = {
name: `${name}, ${location}`,
description,
rooms,
images: links.slice(0, 5).join(','),
price,
}
await toast.promise(
new Promise(async (resolve, reject) => {
await createAppartment(params)
.then(async () => {
onReset();
navigate('/')
loadAppartments();
resolve();
})
.catch(() => reject());
}),
{
pending: "Approve transaction...",
success: "apartment added successfully 👌",
error: "Encountered error 🤯",
}
);
}
const addImage = () => {
if (links.length != 5) {
setLinks((prevState) => [...prevState, images])
}
setImages('')
}
const removeImage = (index) => {
links.splice(index, 1)
setLinks(() => [...links])
}
const onReset = () => {
setName('')
setDescription('')
setLocation('')
setRooms('')
setPrice('')
setImages('')
setLinks([])
}
return (
<div className="h-screen flex justify-center mx-auto">
<div className="w-11/12 md:w-2/5 h-7/12 p-6">
<form onSubmit={handleSubmit} className="flex flex-col">
<div className="flex justify-center items-center">
<p className="font-semibold text-black">Add Room</p>
</div>
<div className="flex flex-row justify-between items-center border border-gray-300 p-2 rounded-xl mt-5">
<input
className="block w-full text-sm
text-slate-500 bg-transparent border-0
focus:outline-none focus:ring-0"
type="text"
name="name"
placeholder="Room Name "
onChange={(e) => setName(e.target.value)}
value={name}
required
/>
</div>
<div className="flex flex-row justify-between items-center border border-gray-300 p-2 rounded-xl mt-5">
<input
className="block w-full text-sm
text-slate-500 bg-transparent border-0
focus:outline-none focus:ring-0"
type="number"
step={0.01}
min={0.01}
name="price"
placeholder="price (Eth)"
onChange={(e) => setPrice(e.target.value)}
value={price}
required
/>
</div>
<div className="flex flex-row justify-between items-center border border-gray-300 p-2 rounded-xl mt-5">
<input
className="block flex-1 text-sm
text-slate-500 bg-transparent border-0
focus:outline-none focus:ring-0"
type="url"
name="images"
placeholder="Images"
onChange={(e) => setImages(e.target.value)}
value={images}
/>
{links.length != 5 ? (
<button
onClick={addImage}
type="button"
className="p-2 bg-[#ff385c] text-white rounded-full text-sm"
>
Add image link
</button>
) : null}
</div>
<div className="flex flex-row justify-start items-center rounded-xl mt-5 space-x-1 flex-wrap">
{links.map((link, i) => (
<div
key={i}
className="p-2 rounded-full text-gray-500 bg-gray-200 font-semibold
flex items-center w-max cursor-pointer active:bg-gray-300
transition duration-300 ease space-x-2 text-xs"
>
<span>{truncate(link, 4, 4, 11)}</span>
<button
onClick={() => removeImage(i)}
type="button"
className="bg-transparent hover focus:outline-none"
>
<FaTimes />
</button>
</div>
))}
</div>
<div className="flex flex-row justify-between items-center border border-gray-300 p-2 rounded-xl mt-5">
<input
className="block w-full text-sm
text-slate-500 bg-transparent border-0
focus:outline-none focus:ring-0"
type="text"
name="location"
placeholder="Location"
onChange={(e) => setLocation(e.target.value)}
value={location}
required
/>
</div>
<div className="flex flex-row justify-between items-center border border-gray-300 p-2 rounded-xl mt-5">
<input
className="block w-full text-sm
text-slate-500 bg-transparent border-0
focus:outline-none focus:ring-0"
type="text"
name="rooms"
placeholder="Number of room"
onChange={(e) => setRooms(e.target.value)}
value={rooms}
required
/>
</div>
<div className="flex flex-row justify-between items-center border border-gray-300 p-2 rounded-xl mt-5">
<textarea
className="block w-full text-sm resize-none
text-slate-500 bg-transparent border-0
focus:outline-none focus:ring-0 h-20"
type="text"
name="description"
placeholder="Room Description"
onChange={(e) => setDescription(e.target.value)}
value={description}
required
></textarea>
</div>
<button
type="submit"
className="flex flex-row justify-center items-center
w-full text-white text-md bg-[#ff385c]
py-2 px-5 rounded-full drop-shadow-xl hover:bg-white
border-transparent
hover:hover:text-[#ff385c]
hover:border-2 hover:border-[#ff385c]
mt-5"
>
Add Appartment
</button>
</form>
</div>
</div>
)
}
export default AddRoom
view raw AddRoom.jsx hosted with ❤ by GitHub

Ensure that you create a file called AddRoom.jsx in the views folder and paste the above codes into it.

UpdateRoom Component

Update View

This page contains a form that is used for editing an apartment by the apartment owner.
see the code below.

import { FaTimes } from 'react-icons/fa'
import { useEffect, useState } from 'react'
import { useParams, useNavigate } from 'react-router-dom'
import { loadAppartment, updateApartment } from '../Blockchain.services'
import { truncate, useGlobalState } from '../store'
import { toast } from 'react-toastify'
const UpdateRoom = () => {
const { id } = useParams()
const [appartment] = useGlobalState('appartment')
const [name, setName] = useState('')
const [description, setDescription] = useState('')
const [location, setLocation] = useState('')
const [rooms, setRooms] = useState('')
const [images, setImages] = useState('')
const [price, setPrice] = useState('')
const [links, setLinks] = useState([])
const navigate = useNavigate()
useEffect(async () => {
await loadAppartment(id)
if (!name) {
setName(appartment?.name.split(',')[0])
setLocation(appartment?.name.split(',')[1])
setDescription(appartment?.description)
setRooms(appartment?.rooms)
setPrice(appartment?.price)
setLinks(appartment?.images)
}
}, [appartment])
const addImage = () => {
if (links.length != 5) {
setLinks((prevState) => [...prevState, images])
}
setImages('')
}
const removeImage = (index) => {
links.splice(index, 1)
setLinks(() => [...links])
}
const handleSubmit = async (e) => {
e.preventDefault()
if (
!name ||
!location ||
!description ||
!rooms ||
links.length != 5 ||
!price
)
return
const params = {
id,
name: `${name}, ${location}`,
description,
rooms,
images: links.slice(0, 5).join(','),
price,
}
await toast.promise(
new Promise(async (resolve, reject) => {
await updateApartment(params)
.then(async () => {
onReset()
loadAppartment(id)
navigate(`/room/${id}`)
resolve()
})
.catch(() => reject())
}),
{
pending: 'Approve transaction...',
success: 'apartment updated successfully 👌',
error: 'Encountered error 🤯',
}
)
console.log(links)
}
const onReset = () => {
setName('')
setDescription('')
setLocation('')
setRooms('')
setPrice('')
setImages('')
}
return (
<div className="h-screen flex justify-center mx-auto">
<div className="w-11/12 md:w-2/5 h-7/12 p-6">
<form onSubmit={handleSubmit} className="flex flex-col">
<div className="flex justify-center items-center">
<p className="font-semibold text-black">Edit Room</p>
</div>
<div className="flex flex-row justify-between items-center border border-gray-300 p-2 rounded-xl mt-5">
<input
className="block w-full text-sm
text-slate-500 bg-transparent border-0
focus:outline-none focus:ring-0"
type="text"
name="name"
placeholder="Room Name "
onChange={(e) => setName(e.target.value)}
value={name || ''}
required
/>
</div>
<div className="flex flex-row justify-between items-center border border-gray-300 p-2 rounded-xl mt-5">
<input
className="block w-full text-sm
text-slate-500 bg-transparent border-0
focus:outline-none focus:ring-0"
type="number"
step={0.01}
min={0.01}
name="price"
placeholder="price (Eth)"
onChange={(e) => setPrice(e.target.value)}
value={price || ''}
required
/>
</div>
<div className="flex flex-row justify-between items-center border border-gray-300 p-2 rounded-xl mt-5">
<input
className="block text-sm flex-1
text-slate-500 bg-transparent border-0
focus:outline-none focus:ring-0"
type="url"
name="images"
placeholder="Images"
onChange={(e) => setImages(e.target.value)}
value={images || ''}
/>
{links?.length != 5 ? (
<button
onClick={addImage}
type="button"
className="p-2 bg-[#ff385c] text-white rounded-full text-sm"
>
Add image link
</button>
) : null}
</div>
<div className="flex flex-row justify-start items-center rounded-xl mt-5 space-x-1 flex-wrap">
{links?.map((link, i) => (
<div
key={i}
className="p-2 rounded-full text-gray-500 bg-gray-200 font-semibold
flex items-center w-max cursor-pointer active:bg-gray-300
transition duration-300 ease space-x-2 text-xs"
>
<span>{truncate(link, 4, 4, 11)}</span>
<button
onClick={() => removeImage(i)}
type="button"
className="bg-transparent hover focus:outline-none"
>
<FaTimes />
</button>
</div>
))}
</div>
<div className="flex flex-row justify-between items-center border border-gray-300 p-2 rounded-xl mt-5">
<input
className="block w-full text-sm
text-slate-500 bg-transparent border-0
focus:outline-none focus:ring-0"
type="text"
name="location"
placeholder="Location"
onChange={(e) => setLocation(e.target.value)}
value={location || ''}
required
/>
</div>
<div className="flex flex-row justify-between items-center border border-gray-300 p-2 rounded-xl mt-5">
<input
className="block w-full text-sm
text-slate-500 bg-transparent border-0
focus:outline-none focus:ring-0"
type="text"
name="rooms"
placeholder="Number of room"
onChange={(e) => setRooms(e.target.value)}
value={rooms || ''}
required
/>
</div>
<div className="flex flex-row justify-between items-center border border-gray-300 p-2 rounded-xl mt-5">
<textarea
className="block w-full text-sm resize-none
text-slate-500 bg-transparent border-0
focus:outline-none focus:ring-0 h-20"
type="text"
name="description"
placeholder="Room Description"
onChange={(e) => setDescription(e.target.value)}
value={description || ''}
required
></textarea>
</div>
<button
type="submit"
className="flex flex-row justify-center items-center
w-full text-white text-md bg-[#ff385c]
py-2 px-5 rounded-full drop-shadow-xl hover:bg-white
border-transparent
hover:hover:text-[#ff385c]
hover:border-2 hover:border-[#ff385c]
mt-5"
>
Update Appartment
</button>
</form>
</div>
</div>
)
}
export default UpdateRoom
view raw UpdateRoom.jsx hosted with ❤ by GitHub

Again, make sure that you create a file called UpdateRoom.jsx in the views folder and paste the above codes into it.

Room page

Room View

This page displays a single apartment and its information, it also carries the form for booking and also a button for getting the bookings of a particular user and relevant pieces of information like reviews. See the code below.

import { useEffect, useState } from 'react'
import { FaEthereum } from 'react-icons/fa'
import { CiEdit } from 'react-icons/ci'
import { FiCalendar } from 'react-icons/fi'
import { MdDeleteOutline } from 'react-icons/md'
import { BiBookOpen, BiMedal } from 'react-icons/bi'
import { BsChatLeft } from 'react-icons/bs'
import Identicon from 'react-identicons'
import { Link, useParams, useNavigate } from 'react-router-dom'
import DatePicker from 'react-datepicker'
import { useGlobalState, setGlobalState, truncate } from '../store'
import moment from 'moment'
import AddReview from '../components/AddReview'
import { toast } from 'react-toastify'
import { loginWithCometChat, signUpWithCometChat } from '../services/Chat'
import {
deleteAppartment,
loadAppartment,
loadReviews,
loadAppartments,
appartmentBooking,
getUnavailableDates,
} from '../Blockchain.services'
const Room = () => {
const { id } = useParams()
const [appartment] = useGlobalState('appartment')
const [reviews] = useGlobalState('reviews')
const [booked] = useGlobalState('booked')
const handleReviewOpen = () => {
setGlobalState('reviewModal', 'scale-100')
}
useEffect(async () => {
await loadAppartment(id)
await loadReviews(id)
}, [])
return (
<>
<div className="py-8 px-10 sm:px-20 md:px-32 space-y-8">
<RoomHeader name={appartment?.name} rooms={appartment?.rooms} />
<RoomGrid
first={appartment?.images[0]}
second={appartment?.images[1]}
third={appartment?.images[2]}
forth={appartment?.images[3]}
fifth={appartment?.images[4]}
/>
<RoomDescription description={appartment?.description} />
<RoomCalendar price={appartment?.price} />
<RoomButtons id={appartment?.id} owner={appartment?.owner} />
<div className="flex flex-col justify-between flex-wrap space-y-2">
<h1 className="text-xl font-semibold">Reviews</h1>
<div>
{reviews.length > 0
? reviews.map((review, index) => (
<RoomReview key={index} review={review} />
))
: 'No reviews yet!'}
</div>
</div>
{booked ? (
<p
className="underline mt-11 cursor-pointer hover:text-blue-700"
onClick={handleReviewOpen}
>
Drop your review
</p>
) : null}
</div>
<AddReview />
</>
)
}
const RoomHeader = ({ name, rooms }) => {
return (
<div>
<h1 className="text-3xl font-semibold">{name}</h1>
<div className="flex justify-between">
<div className="flex items-center mt-2 space-x-2 text-lg text-slate-500">
<span>
{rooms} {rooms == 1 ? 'room' : 'rooms'}
</span>
</div>
</div>
</div>
)
}
const RoomButtons = ({ id, owner }) => {
const [currentUser] = useGlobalState('currentUser')
const [connectedAccount] = useGlobalState('connectedAccount')
const navigate = useNavigate()
const handleDelete = async () => {
if (confirm('Are you sure you want to delete?')) {
await toast.promise(
new Promise(async (resolve, reject) => {
await deleteAppartment(id)
.then(async () => {
navigate('/')
await loadAppartments()
resolve()
})
.catch(() => reject())
}),
{
pending: 'Approve transaction...',
success: 'apartment deleted successfully 👌',
error: 'Encountered error 🤯',
}
)
}
}
const handleSignUp = async () => {
await toast.promise(
new Promise(async (resolve, reject) => {
await signUpWithCometChat()
.then(() => resolve())
.catch((error) => reject(error))
}),
{
pending: 'Registering...',
success: 'Account created, please login 👌',
error: 'Encountered error 🤯',
}
)
}
const handleLogin = async () => {
await toast.promise(
new Promise(async (resolve, reject) => {
await loginWithCometChat()
.then(async (user) => {
setGlobalState('currentUser', user)
resolve(user)
})
.catch((error) => reject(error))
}),
{
pending: 'Authenticating...',
success: 'Logged in successfully 👌',
error: 'Encountered error 🤯',
}
)
}
return (
<div className="flex justify-start items-center space-x-3 border-b-2 border-b-slate-200 pb-6">
{currentUser && currentUser.status != 'offline' ? (
<Link
to={`/chats/${owner}`}
className="p-2 rounded-md shadow-lg border-[0.1px]
border-gray-300 flex justify-start items-center space-x-1
bg-white hover:bg-gray-100"
>
<BsChatLeft size={15} className="text-pink-500" />
<small>Chats</small>
</Link>
) : (
<>
<button
className="p-2 rounded-md shadow-lg border-[0.1px]
border-gray-300 flex justify-start items-center space-x-1
bg-white hover:bg-gray-100"
onClick={handleSignUp}
>
<small>Sign up</small>
</button>
<button
className="p-2 rounded-md shadow-lg border-[0.1px]
border-gray-300 flex justify-start items-center space-x-1
bg-white hover:bg-gray-100"
onClick={handleLogin}
>
<small>Login to chat</small>
</button>
</>
)}
{connectedAccount == owner ? (
<>
<Link
to={'/editRoom/' + id}
className="p-2 rounded-md shadow-lg border-[0.1px]
border-gray-500 flex justify-start items-center space-x-1
bg-gray-500 hover:bg-transparent hover:text-gray-500 text-white"
>
<CiEdit size={15} />
<small>Edit</small>
</Link>
<button
className="p-2 rounded-md shadow-lg border-[0.1px]
border-pink-500 flex justify-start items-center space-x-1
bg-pink-500 hover:bg-transparent hover:text-pink-500 text-white"
onClick={handleDelete}
>
<MdDeleteOutline size={15} />
<small>Delete</small>
</button>
</>
) : null}
</div>
)
}
const RoomGrid = ({ first, second, third, forth, fifth }) => {
return (
<div className="mt-8 h-[32rem] flex rounded-2xl overflow-hidden">
<div className="md:w-1/2 w-full overflow-hidden">
<img className="object-cover w-full h-full" src={first} />
</div>
<div className="w-1/2 md:flex hidden flex-wrap">
<img src={second} className="object-cover w-1/2 h-64 pl-2 pb-1 pr-1" />
<img src={third} alt="" className="object-cover w-1/2 h-64 pl-1 pb-1" />
<img src={forth} className="object-cover w-1/2 h-64 pt-1 pl-2 pr-1" />
<img
src={fifth}
className="object-cover sm:w-2/5 md:w-1/2 h-64 pl-1 pt-1"
/>
</div>
</div>
)
}
const RoomDescription = ({ description }) => {
return (
<div className="py-5 border-b-2 border-b-slate-200 space-y-4">
<h1 className="text-xl font-semibold">Description</h1>
<p className="text-slate-500 text-lg w-full sm:w-4/5">{description}</p>
<div className=" flex space-x-4 ">
<BiBookOpen className="text-4xl" />
<div>
<h1 className="text-xl font-semibold">Featured in</h1>
<p className="cursor-pointer">Condé Nast Traveler, June 2023</p>
</div>
</div>
<div className=" flex space-x-4">
<BiMedal className="text-4xl" />
<div>
<h1 className="text-xl font-semibold">
Vittorio Emanuele is a Superhost
</h1>
<p>
Superhosts are experienced, highly rated hosts who are committed to
providing great stays for guests.
</p>
</div>
</div>
<div className=" flex space-x-4">
<FiCalendar className="text-4xl" />
<div>
<h1 className="text-xl font-semibold">
Free cancellation before Oct 17.
</h1>
</div>
</div>
</div>
)
}
const RoomCalendar = ({ price }) => {
const [checkInDate, setCheckInDate] = useState(null)
const [checkOutDate, setCheckOutDate] = useState(null)
const { id } = useParams()
const [timestamps] = useGlobalState('timestamps')
useEffect(async () => await getUnavailableDates(id))
const handleCheckInDateChange = (date) => {
setCheckInDate(date)
}
const handleCheckOutDateChange = (date) => {
setCheckOutDate(date)
}
const handleSubmit = async (e) => {
e.preventDefault()
if (!checkInDate || !checkOutDate) return
const start = moment(checkInDate)
const end = moment(checkOutDate)
const timestampArray = []
while (start <= end) {
timestampArray.push(start.valueOf())
start.add(1, 'days')
}
const params = {
id,
datesArray: timestampArray,
amount: price * timestampArray.length,
}
await toast.promise(
new Promise(async (resolve, reject) => {
await appartmentBooking(params)
.then(async () => {
resetForm()
resolve()
})
.catch(() => reject())
}),
{
pending: 'Approve transaction...',
success: 'apartment booked successfully 👌',
error: 'Encountered error 🤯',
}
)
}
const resetForm = () => {
setCheckInDate(null)
setCheckOutDate(null)
}
return (
<form
onSubmit={handleSubmit}
className="sm:w-[25rem] border-[0.1px] p-6
border-gray-400 rounded-lg shadow-lg flex flex-col
space-y-4"
>
<div className="flex justify-between">
<div className="flex justify-center items-center">
<FaEthereum className="text-lg text-gray-500" />
<span className="text-lg text-gray-500">
{price} <small>per night</small>
</span>
</div>
</div>
<DatePicker
id="checkInDate"
selected={checkInDate}
onChange={handleCheckInDateChange}
placeholderText={'Check In'}
dateFormat="yyyy-MM-dd"
minDate={new Date()}
excludeDates={timestamps}
required
className="rounded-lg w-full"
/>
<DatePicker
id="checkOutDate"
selected={checkOutDate}
onChange={handleCheckOutDateChange}
placeholderText={'Check out'}
dateFormat="yyyy-MM-dd"
minDate={checkInDate}
excludeDates={timestamps}
required
className="rounded-lg w-full"
/>
<button
className="p-2 border-none bg-gradient-to-l from-pink-600
to-gray-600 text-white w-full rounded-md focus:outline-none
focus:ring-0"
>
Book
</button>
<Link to={`/bookings/${id}`} className="text-pink-500">
Check your bookings
</Link>
</form>
)
}
const RoomReview = ({ review }) => {
return (
<div className="w-1/2 pr-5 space-y-2">
<div className="pt-2 flex items-center space-x-2">
<Identicon
string={review.owner}
size={20}
className="rounded-full shadow-gray-500 shadow-sm"
/>
<div className="flex justify-start items-center space-x-2">
<p className="text-md font-semibold">
{truncate(review.owner, 4, 4, 11)}{' '}
</p>
<p className="text-slate-500 text-sm">{review.timestamp}</p>
</div>
</div>
<p className="text-slate-500 text-sm w-full sm:w-4/5">
{review.reviewText}
</p>
</div>
)
}
export default Room
view raw Room.jsx hosted with ❤ by GitHub

Don’t forget to create a file called UpdateRoom.jsx in the views folder and paste the above codes into it.

Bookings Page

Bookings Page

Bookings Page 2

This page displays information depending on who the user is, if the user is the apartment owner it displays booking requests if not it displays the booking a user has done
see the code below.

import { useState, useEffect } from 'react'
import { Link, useParams } from 'react-router-dom'
import { useGlobalState, getGlobalState } from '../store'
import { toast } from 'react-toastify'
import {
getBookings,
getUnavailableDates,
hasBookedDateReached,
refund,
loadAppartment,
claimFunds,
checkInApartment,
} from '../Blockchain.services'
const Bookings = () => {
const [loaded, setLoaded] = useState(false)
const connectedAccount = getGlobalState('connectedAccount')
const [bookings] = useGlobalState('bookings')
const [appartment] = useGlobalState('appartment')
const { id } = useParams()
useEffect(async () => {
await getBookings(id).then(() => setLoaded(true))
await loadAppartment(id)
}, [])
const isDayAfter = (booking) => {
const bookingDate = new Date(booking.date).getTime()
const today = new Date().getTime()
const oneDay = 24 * 60 * 60 * 1000
return today > bookingDate + oneDay && !booking.checked
}
const handleClaimFunds = async (booking) => {
const params = {
id,
bookingId: booking.id,
}
await toast.promise(
new Promise(async (resolve, reject) => {
await claimFunds(params)
.then(async () => {
await getUnavailableDates(id)
await getBookings(id)
resolve()
})
.catch(() => reject())
}),
{
pending: 'Approve transaction...',
success: 'funds claimed successfully 👌',
error: 'Encountered error 🤯',
}
)
}
return loaded ? (
<div className="w-full sm:w-3/5 mx-auto mt-8">
{appartment?.owner != connectedAccount.toLowerCase() ? (
<h1 className="text-center text-3xl text-black font-bold">
Your bookings
</h1>
) : null}
{bookings.length > 0 ? (
bookings.map((booking, index) => (
<BookingDisplay key={index} booking={booking} />
))
) : (
<div>No bookings for this appartment yet</div>
)}
{appartment?.owner == connectedAccount.toLowerCase() ? (
<div className="w-full sm:w-3/5 mx-auto mt-8">
<h1 className="text-3xl text-center font-bold">
View booking requests
</h1>
{bookings.length > 0
? bookings.map((booking, index) => (
<div
key={index}
className="w-full my-3 border-b border-b-gray-100 p-3 bg-gray-100"
>
<div>{booking.date}</div>
{isDayAfter(booking) ? (
<button
className="p-2 bg-green-500 text-white rounded-full text-sm"
onClick={() => handleClaimFunds(booking)}
>
claim
</button>
) : null}
</div>
))
: 'No bookings yet'}
</div>
) : null}
</div>
) : null
}
const BookingDisplay = ({ booking }) => {
const { id } = useParams()
const connectedAccount = getGlobalState('connectedAccount')
useEffect(async () => {
const params = {
id,
bookingId: booking.id,
}
await hasBookedDateReached(params)
}, [])
const handleCheckIn = async () => {
await toast.promise(
new Promise(async (resolve, reject) => {
await checkInApartment(id, booking.id)
.then(async () => {
await getBookings(id)
resolve()
})
.catch(() => reject())
}),
{
pending: 'Approve transaction...',
success: 'Checked In successfully 👌',
error: 'Encountered error 🤯',
}
)
}
const handleRefund = async () => {
const params = {
id,
bookingId: booking.id,
date: new Date(booking.date).getTime(),
}
await toast.promise(
new Promise(async (resolve, reject) => {
await refund(params)
.then(async () => {
await getUnavailableDates(id)
await getBookings(id)
resolve()
})
.catch(() => reject())
}),
{
pending: 'Approve transaction...',
success: 'refund successful 👌',
error: 'Encountered error 🤯',
}
)
}
const bookedDayStatus = (booking) => {
const bookedDate = new Date(booking.date).getTime()
const current = new Date().getTime()
const bookedDayStatus = bookedDate < current && !booking.checked
return bookedDayStatus
}
return (
<>
{booking.tenant != connectedAccount.toLowerCase() ||
booking.cancelled == true ? null : (
<div className="w-full flex justify-between items-center my-3 bg-gray-100 p-3">
<Link className=" font-medium underline" to={'/room/' + id}>
{booking.date}
</Link>
{bookedDayStatus(booking) ? (
<button
className="p-2 bg-green-500 text-white rounded-full text-sm px-4"
onClick={handleCheckIn}
>
Check In
</button>
) : booking.checked ? (
<button className="p-2 bg-yellow-500 text-white font-medium italic rounded-full text-sm px-4">
Checked In
</button>
) : (
<button
className="p-2 bg-[#ff385c] text-white rounded-full text-sm px-4"
onClick={handleRefund}
>
Refund
</button>
)}
</div>
)}
</>
)
}
export default Bookings
view raw Bookings.jsx hosted with ❤ by GitHub

As always, create a file called UpdateRoom.jsx in the views folder and paste the above codes into it.

Recent Conversations View

Recent Conversations View

This view is only accessible by the apartment to check for messages sent to him or her, see the code below.

import { useEffect } from 'react'
import { getConversations } from '../services/Chat'
import { useNavigate, Link } from 'react-router-dom'
import { setGlobalState, useGlobalState, truncate } from '../store'
import Identicon from 'react-identicons'
import { toast } from 'react-toastify'
const RecentConversations = () => {
const navigate = useNavigate()
const [recentConversations] = useGlobalState('recentConversations')
useEffect(async () => {
await getConversations()
.then((users) => setGlobalState('recentConversations', users))
.catch((error) => {
if (error.code == 'USER_NOT_LOGED_IN') {
navigate('/')
toast.warning('You should login first...')
}
})
}, [])
return (
<div className="w-full sm:w-3/5 mx-auto mt-8">
<h1 className="text-2xl font-bold text-center">Your Recent chats</h1>
{recentConversations?.length > 0
? recentConversations?.map((conversation, index) => (
<Link
className="flex items-center space-x-3 w-full my-3
border-b border-b-gray-100 p-3 bg-gray-100"
to={`/chats/${conversation.conversationWith.uid}`}
key={index}
>
<Identicon
className="rounded-full shadow-gray-500 shadow-sm bg-white"
string={conversation.conversationWith.uid}
size={20}
/>
<p>{truncate(conversation.conversationWith.name, 4, 4, 11)}</p>
</Link>
))
: "you don't have any recent chats"}
</div>
)
}
export default RecentConversations

Again, create a file called RecentConversations.jsx in the views folder and paste the above codes into it.

Chats View

The Chat View

This is where all the chatting happens on the platform between apartment owners and other users. See the code below.

import { useState, useEffect } from 'react'
import { useNavigate, useParams } from 'react-router-dom'
import { setGlobalState, useGlobalState, truncate } from '../store'
import Identicon from 'react-identicons'
import {
getMessages,
sendMessage,
listenForMessage,
isUserLoggedIn,
} from '../services/Chat'
import { toast } from 'react-toastify'
const Chats = () => {
const { id } = useParams()
const [messages] = useGlobalState('messages')
const [message, setMessage] = useState('')
const navigate = useNavigate()
useEffect(async () => {
await isUserLoggedIn()
.then(async () => {
await getMessages(id).then((msgs) => setGlobalState('messages', msgs))
await handleListener()
})
.catch((error) => {
if (error.code == 'USER_NOT_LOGED_IN') {
navigate('/')
toast.warning('You must be logged in first')
}
})
}, [])
const onSendMessage = async (e) => {
e.preventDefault()
if (!message) return
await sendMessage(id, message).then((msg) => {
setGlobalState('messages', (prevState) => [...prevState, msg])
setMessage('')
scrollToEnd()
})
}
const handleListener = async () => {
await listenForMessage(id).then((msg) => {
setGlobalState('messages', (prevState) => [...prevState, msg])
scrollToEnd()
})
}
const scrollToEnd = () => {
const elmnt = document.getElementById('messages-container')
elmnt.scrollTop = elmnt.scrollHeight
}
return (
<div
className="bg-gray-100 rounded-2xl h-[calc(100vh_-_13rem)]
w-4/5 flex flex-col justify-between relative mx-auto mt-8 border-t border-t-gray-100"
>
<h1
className="text-2xl font-bold text-center absolute top-0
bg-white w-full shadow-sm py-2"
>
Chats
</h1>
<div
id="messages-container"
className="h-[calc(100vh_-_20rem)] overflow-y-scroll w-full p-4 pt-16"
>
{messages.length > 0
? messages.map((msg, index) => (
<Message message={msg.text} uid={msg.sender.uid} key={index} />
))
: 'No message yet'}
</div>
<form onSubmit={onSendMessage} className="w-full">
<input
type="text"
value={message}
onChange={(e) => setMessage(e.target.value)}
className="h-full w-full py-5 focus:outline-none focus:ring-0 rounded-md
border-none bg-[rgba(0,0,0,0.7)] text-white placeholder-white"
placeholder="Leave a message..."
/>
</form>
</div>
)
}
const Message = ({ message, uid }) => {
const [connectedAccount] = useGlobalState('connectedAccount')
return uid == connectedAccount ? (
<div className="flex justify-end items-center space-x-4 mb-3">
<div
className="flex flex-col bg-white py-2 px-4 space-y-2
rounded-full rounded-br-none shadow-sm"
>
<div className="flex items-center space-x-2">
<Identicon
string={uid}
size={20}
className="rounded-full bg-white shadow-sm"
/>
<p className="font-bold text-sm">{truncate(uid, 4, 4, 11)}</p>
</div>
<p className="text-sm">{message}</p>
</div>
</div>
) : (
<div className="flex justify-start items-center space-x-4 mb-3">
<div
className="flex flex-col bg-white py-2 px-4 space-y-2
rounded-full rounded-bl-none shadow-sm"
>
<div className="flex items-center space-x-2">
<Identicon
string={uid}
size={20}
className="rounded-full bg-white shadow-sm"
/>
<p className="font-bold text-sm">{truncate(uid, 4, 4, 11)}</p>
</div>
<p className="text-sm">{message}</p>
</div>
</div>
)
}
export default Chats
view raw Chats.jsx hosted with ❤ by GitHub

Now as before, create another file called Chats.jsx in the views folder and paste the above codes into it.

The App.jsx file

We'll look at the App.jsx file, which bundles our components and pages.

import { useEffect } from 'react'
import { Route, Routes } from 'react-router-dom'
import Card from './components/Card'
import Footer from './components/Footer'
import Header from './components/Header'
import Home from './views/Home'
import Room from './views/Room'
import AddRoom from './views/AddRoom'
import { isWallectConnected, loadAppartments } from './Blockchain.services'
import UpdateRoom from './views/UpdateRoom'
import { ToastContainer } from 'react-toastify'
import 'react-toastify/dist/ReactToastify.css'
import Bookings from './views/Bookings'
import Chats from './views/Chats'
import RecentConversations from './views/RecentConversations'
import { setGlobalState, useGlobalState } from './store'
import { isUserLoggedIn } from './services/Chat'
import AuthModal from './components/AuthModal'
const App = () => {
const [connectedAccount] = useGlobalState('connectedAccount')
useEffect(async () => {
await isWallectConnected()
await loadAppartments()
await isUserLoggedIn().then((user) => setGlobalState('currentUser', user))
}, [connectedAccount])
return (
<div className="relative h-screen min-w-screen">
<Header />
<Routes>
<Route path="/" element={<Home />} />
<Route path="/room/:id" element={<Room />} />
<Route path="/card" element={<Card />} />
<Route path="/addRoom" element={<AddRoom />} />
<Route path="/editRoom/:id" element={<UpdateRoom />} />
<Route path="/bookings/:id" element={<Bookings />} />
<Route path="/chats/:id" element={<Chats />} />
<Route path="/recentconversations" element={<RecentConversations />} />
</Routes>
<div className="h-20"></div>
<Footer />
<AuthModal />
<ToastContainer
position="bottom-center"
autoClose={5000}
hideProgressBar={false}
newestOnTop={false}
closeOnClick
rtl={false}
pauseOnFocusLoss
draggable
pauseOnHover
theme="dark"
/>
</div>
)
}
export default App
view raw App.jsx hosted with ❤ by GitHub

Update the App.jsx file with the above codes.

Other Essential Services

The services listed below are critical to the smooth operation of our application.

The Store Service
The application relies on critical services, including the “Store Service” which uses the react-hooks-global-state library to manage the application's state. To set up the Store Service, create a "store" folder within the "src" folder and create an "index.jsx" file within it, then paste and save the provided code.

import { createGlobalState } from "react-hooks-global-state";
const { setGlobalState, useGlobalState, getGlobalState } = createGlobalState({
appartments: [],
appartment: null,
reviews: [],
connectedAccount: "",
authModal: "scale-0",
reviewModal: "scale-0",
securityFee: null,
bookings: [],
booking: null,
booked: false,
status: null,
timestamps: [],
currentUser: null,
recentConversations: [],
messages: []
});
const truncate = (text, startChars, endChars, maxLength) => {
if (text.length > maxLength) {
let start = text.substring(0, startChars);
let end = text.substring(text.length - endChars, text.length);
while (start.length + end.length < maxLength) {
start = start + ".";
}
return start + end;
}
return text;
};
export { setGlobalState, useGlobalState, getGlobalState, truncate };
view raw index.jsx hosted with ❤ by GitHub

The Blockchain Service
Create a file named "Blockchain.service.js" , and save the provided code inside the file.

import abi from './abis/src/contracts/DappBnb.sol/DappBnb.json'
import address from './abis/contractAddress.json'
import { getGlobalState, setGlobalState } from './store'
import { ethers } from 'ethers'
import { logOutWithCometChat } from './services/Chat'
const { ethereum } = window
const contractAddress = address.address
const contractAbi = abi.abi
let tx
const toWei = (num) => ethers.utils.parseEther(num.toString())
const fromWei = (num) => ethers.utils.formatEther(num)
const getEtheriumContract = async () => {
const provider = new ethers.providers.Web3Provider(ethereum)
const signer = provider.getSigner()
const contract = new ethers.Contract(contractAddress, contractAbi, signer)
return contract
}
const isWallectConnected = async () => {
try {
if (!ethereum) return alert('Please install Metamask')
const accounts = await ethereum.request({ method: 'eth_accounts' })
window.ethereum.on('chainChanged', (chainId) => window.location.reload())
window.ethereum.on('accountsChanged', async () => {
setGlobalState('connectedAccount', accounts[0])
await isWallectConnected()
await logOutWithCometChat()
setGlobalState('currentUser', null)
})
if (accounts.length) {
setGlobalState('connectedAccount', accounts[0])
} else {
console.log('No accounts found.')
setGlobalState('connectedAccount', '')
}
} catch (error) {
reportError(error)
}
}
const connectWallet = async () => {
try {
if (!ethereum) return alert('Please install Metamask')
const accounts = await ethereum.request({ method: 'eth_requestAccounts' })
setGlobalState('connectedAccount', accounts[0])
} catch (error) {
reportError(error)
}
}
const createAppartment = async ({
name,
description,
rooms,
images,
price,
}) => {
try {
const connectedAccount = getGlobalState('connectedAccount')
const contract = await getEtheriumContract()
price = toWei(price)
tx = await contract.createAppartment(
name,
description,
images,
rooms,
price,
{
from: connectedAccount,
}
)
await tx.wait()
} catch (err) {
console.log(err)
}
}
const updateApartment = async ({
id,
name,
description,
rooms,
images,
price,
}) => {
try {
const connectedAccount = getGlobalState('connectedAccount')
const contract = await getEtheriumContract()
price = toWei(price)
tx = await contract.updateAppartment(
id,
name,
description,
images,
rooms,
price,
{
from: connectedAccount,
}
)
await tx.wait()
} catch (err) {
console.log(err)
}
}
const deleteAppartment = async (id) => {
try {
const connectedAccount = getGlobalState('connectedAccount')
const contract = await getEtheriumContract()
tx = await contract.deleteAppartment(id, { from: connectedAccount })
await tx.wait()
} catch (err) {
reportError(err)
}
}
const loadAppartments = async () => {
try {
const contract = await getEtheriumContract()
const appartments = await contract.getApartments()
const securityFee = await contract.securityFee()
setGlobalState('appartments', structureAppartments(appartments))
setGlobalState('securityFee', fromWei(securityFee))
} catch (err) {
reportError(err)
}
}
const loadAppartment = async (id) => {
try {
const contract = await getEtheriumContract()
const appartment = await contract.getApartment(id)
const booked = await contract.tenantBooked(id)
setGlobalState('appartment', structureAppartments([appartment])[0])
setGlobalState('booked', booked)
} catch (error) {
reportError(error)
}
}
const appartmentBooking = async ({ id, datesArray, amount }) => {
try {
const contract = await getEtheriumContract()
const connectedAccount = getGlobalState('connectedAccount')
const securityFee = getGlobalState('securityFee')
tx = await contract.bookApartment(id, datesArray, {
from: connectedAccount,
value: toWei(Number(amount) + Number(securityFee)),
})
await tx.wait()
await getUnavailableDates(id)
} catch (err) {
console.log(err)
}
}
const refund = async ({ id, bookingId, date }) => {
try {
const connectedAccount = getGlobalState('connectedAccount')
const contract = await getEtheriumContract()
tx = await contract.refundBooking(id, bookingId, date, {
from: connectedAccount,
})
await tx.wait()
await getUnavailableDates(id)
} catch (err) {
reportError(err)
}
}
const claimFunds = async ({ id, bookingId }) => {
try {
const connectedAccount = getGlobalState('connectedAccount')
const contract = await getEtheriumContract()
tx = await contract.claimFunds(id, bookingId, {
from: connectedAccount,
})
await tx.wait()
} catch (err) {
reportError
}
}
const checkInApartment = async (id, bookingId) => {
try {
const connectedAccount = getGlobalState('connectedAccount')
const contract = await getEtheriumContract()
tx = await contract.checkInApartment(id, bookingId, {
from: connectedAccount,
})
await tx.wait()
} catch (err) {
reportError(err)
}
}
const getUnavailableDates = async (id) => {
const contract = await getEtheriumContract()
const unavailableDates = await contract.getUnavailableDates(id)
const timestamps = unavailableDates.map((timestamp) => Number(timestamp))
setGlobalState('timestamps', timestamps)
}
const hasBookedDateReached = async ({ id, bookingId }) => {
try {
const connectedAccount = getGlobalState('connectedAccount')
const contract = await getEtheriumContract()
const result = await contract.hasBookedDateReached(id, bookingId, {
from: connectedAccount,
})
setGlobalState('status', result)
} catch (err) {
reportError(err)
}
}
const getBookings = async (id) => {
try {
const connectedAccount = getGlobalState('connectedAccount')
const contract = await getEtheriumContract()
const bookings = await contract.getBookings(id, {
from: connectedAccount,
})
setGlobalState('bookings', structuredBookings(bookings))
} catch (err) {
reportError(err)
}
}
const getBooking = async ({ id, bookingId }) => {
try {
const connectedAccount = getGlobalState('connectedAccount')
const contract = await getEtheriumContract()
const booking = await contract.getBooking(id, bookingId, {
from: connectedAccount,
})
setGlobalState('bookings', structuredBookings([booking])[0])
} catch (err) {
reportError(err)
}
}
const addReview = async (id, reviewText) => {
try {
if (!ethereum) return alert('Please install Metamask')
const contract = await getEtheriumContract()
tx = await contract.addReview(id, reviewText)
await tx.wait()
await loadReviews(id)
} catch (err) {
reportError(err)
}
}
const loadReviews = async (id) => {
try {
const contract = await getEtheriumContract()
const reviews = await contract.getReviews(id)
setGlobalState('reviews', structuredReviews(reviews))
} catch (error) {
console.log(error)
}
}
const structureAppartments = (appartments) =>
appartments.map((appartment) => ({
id: Number(appartment.id),
name: appartment.name,
owner: appartment.owner.toLowerCase(),
description: appartment.description,
price: parseInt(appartment.price._hex) / 10 ** 18,
deleted: appartment.deleted,
images: appartment.images.split(','),
rooms: Number(appartment.rooms),
timestamp: new Date(appartment.timestamp * 1000).toDateString(),
booked: appartment.booked,
}))
const structuredReviews = (reviews) =>
reviews.map((review) => ({
id: review.id.toNumber(),
appartmentId: review.appartmentId.toNumber(),
reviewText: review.reviewText,
owner: review.owner.toLowerCase(),
timestamp: new Date(review.timestamp * 1000).toDateString(),
}))
const structuredBookings = (bookings) =>
bookings.map((booking) => ({
id: booking.id.toNumber(),
tenant: booking.tenant.toLowerCase(),
date: new Date(booking.date.toNumber()).toDateString(),
price: parseInt(booking.price._hex) / 10 ** 18,
checked: booking.checked,
cancelled: booking.cancelled,
}))
export {
isWallectConnected,
connectWallet,
createAppartment,
loadAppartments,
loadAppartment,
updateApartment,
deleteAppartment,
appartmentBooking,
loadReviews,
addReview,
getUnavailableDates,
getBookings,
getBooking,
hasBookedDateReached,
refund,
checkInApartment,
claimFunds,
}

The Chat Service
Create a file named "Chat.jsx" within the "services" folder and copy the provided code into the file before saving it.

import { CometChat } from '@cometchat-pro/chat'
import { getGlobalState } from '../store'
const CONSTANTS = {
APP_ID: process.env.REACT_APP_COMET_CHAT_APP_ID,
REGION: process.env.REACT_APP_COMET_CHAT_REGION,
Auth_Key: process.env.REACT_APP_COMET_CHAT_AUTH_KEY,
}
const initCometChat = async () => {
const appID = CONSTANTS.APP_ID
const region = CONSTANTS.REGION
const appSetting = new CometChat.AppSettingsBuilder()
.subscribePresenceForAllUsers()
.setRegion(region)
.build()
await CometChat.init(appID, appSetting)
.then(() => console.log('Initialization completed successfully'))
.catch((error) => error)
}
const loginWithCometChat = async () => {
const authKey = CONSTANTS.Auth_Key
const UID = getGlobalState('connectedAccount')
return new Promise(async (resolve, reject) => {
await CometChat.login(UID, authKey)
.then((user) => resolve(user))
.catch((error) => reject(error))
})
}
const signUpWithCometChat = async () => {
const authKey = CONSTANTS.Auth_Key
const UID = getGlobalState('connectedAccount')
const user = new CometChat.User(UID)
user.setName(UID)
return new Promise(async (resolve, reject) => {
await CometChat.createUser(user, authKey)
.then((user) => resolve(user))
.catch((error) => reject(error))
})
}
const logOutWithCometChat = async () => {
return new Promise(async (resolve, reject) => {
await CometChat.logout()
.then(() => resolve())
.catch(() => reject())
})
}
const isUserLoggedIn = async () => {
return new Promise(async (resolve, reject) => {
await CometChat.getLoggedinUser()
.then((user) => resolve(user))
.catch((error) => reject(error))
})
}
const getUser = async (UID) => {
return new Promise(async (resolve, reject) => {
await CometChat.getUser(UID)
.then((user) => resolve(user))
.catch((error) => reject(error))
})
}
const getMessages = async (UID) => {
const limit = 30
const messagesRequest = new CometChat.MessagesRequestBuilder()
.setUID(UID)
.setLimit(limit)
.build()
return new Promise(async (resolve, reject) => {
await messagesRequest
.fetchPrevious()
.then((messages) => resolve(messages.filter((msg) => msg.type == 'text')))
.catch((error) => reject(error))
})
}
const sendMessage = async (receiverID, messageText) => {
const receiverType = CometChat.RECEIVER_TYPE.USER
const textMessage = new CometChat.TextMessage(
receiverID,
messageText,
receiverType
)
return new Promise(async (resolve, reject) => {
await CometChat.sendMessage(textMessage)
.then((message) => resolve(message))
.catch((error) => reject(error))
})
}
const getConversations = async () => {
const limit = 30
const conversationsRequest = new CometChat.ConversationsRequestBuilder()
.setLimit(limit)
.build()
return new Promise(async (resolve, reject) => {
await conversationsRequest
.fetchNext()
.then((conversationList) => resolve(conversationList))
.catch((error) => reject(error))
})
}
const listenForMessage = async (listenerID) => {
return new Promise(async (resolve, reject) => {
CometChat.addMessageListener(
listenerID,
new CometChat.MessageListener({
onTextMessageReceived: (message) => resolve(message),
})
);
});
};
export {
initCometChat,
loginWithCometChat,
signUpWithCometChat,
logOutWithCometChat,
getMessages,
sendMessage,
getConversations,
isUserLoggedIn,
getUser,
listenForMessage,
}
view raw Chat.jsx hosted with ❤ by GitHub

The Index.jsx file
Now update the index entry file with the following codes.

import React from 'react'
import ReactDOM from 'react-dom'
import { BrowserRouter } from 'react-router-dom'
import App from './App'
import "swiper/css";
import "swiper/css/pagination";
import "swiper/css/navigation";
import 'react-datepicker/dist/react-datepicker.css'
import './index.css'
import { initCometChat } from './services/Chat'
initCometChat().then(() => {
ReactDOM.render(
<BrowserRouter>
<App />
</BrowserRouter>,
document.getElementById('root')
)
})
view raw index.jsx hosted with ❤ by GitHub

To start the server on your browser, run these commands on two terminals, assuming you have already installed Metamask.

# Terminal one:
yarn hardhat node
# Terminal Two
yarn hardhat run scripts/deploy.js
yarn start
Enter fullscreen mode Exit fullscreen mode

Running the above commands as instructed will open your project on your browser. And there you have it for how to build a blockchain voting system with React, Solidity, and CometChat.

If you're confused about web3 development and want visual materials, get my NFT Marketplace and Minting courses.

NFT Marketplace Course

Take the first step towards becoming a highly sought-after smart contract developer by enrolling in my courses on NFTs Minting and Marketplace. Enroll now and let's embark on this exciting journey together!

Conclusion

In conclusion, the decentralized web and blockchain technology are here to stay, and creating practical applications is a great way to advance your career in web3 development.

This tutorial has shown you how to create a web3 version of the popular Airbnb application using smart contracts to facilitate payments, along with the CometChat SDK for one-on-one discussions.

That being said, I'll see you next time, and have a wonderful day!

About the Author

Gospel Darlington is a full-stack blockchain developer with 7+ years of experience in the software development industry.

By combining Software Development, writing, and teaching, he demonstrates how to build decentralized applications on EVM-compatible blockchain networks.

His stacks include JavaScript, React, Vue, Angular, Node, React Native, NextJs, Solidity, and more.

For more information about him, kindly visit and follow his page on Twitter, Github, LinkedIn, or his website.

Image of Timescale

🚀 pgai Vectorizer: SQLAlchemy and LiteLLM Make Vector Search Simple

We built pgai Vectorizer to simplify embedding management for AI applications—without needing a separate database or complex infrastructure. Since launch, developers have created over 3,000 vectorizers on Timescale Cloud, with many more self-hosted.

Read full post →

Top comments (0)