DEV Community

Cover image for How to Build a Decentralized House Rental Platform with Next.js, Redux, and Solidity
Gospel Darlington
Gospel Darlington

Posted on

8

How to Build a Decentralized House Rental Platform with Next.js, Redux, and Solidity

What you will be building, see the live demo at Bitfinity test network and the git repo.

House Rental Marketplace

House Rental Marketplace

Introduction

Welcome to our second comprehensive guide on “Building a Decentralized House Rental Platform with Next.js, Redux, and Solidity”. We'll be developing a Decentralized House Rental Platform using Next.js, Solidity, and TypeScript. Throughout this tutorial, you'll gain a clear understanding of the following:

- Building dynamic interfaces with Next.js
- Crafting Ethereum smart contracts with Solidity
- Incorporating static type checking using TypeScript
- Deploying and interacting with your smart contracts
- Understanding the fundamentals of blockchain-based rental platforms
Enter fullscreen mode Exit fullscreen mode

By the end of this guide, you will have a functioning decentralized platform where users can list and rent houses, all managed and secured by Ethereum smart contracts.

As an added incentive for participating in this tutorial, we're giving away a copy of our prestigious book on becoming an in-demand Solidity developer. This offer is free for the first 300 participants. For instructions on how to claim your copy, please watch the short video below.

Capturing Smart Contract Development

Prerequisites

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

  • Node.js
  • Yarn
  • Git Bash
  • MetaMask
  • Next.js
  • Solidity
  • Redux Toolkit
  • Tailwind CSS

To set up MetaMask for this tutorial, please watch the instructional video below:

Once you have successfully completed the setup, you are eligible to receive a free copy of our book. To claim your book, please fill out the form to submit your proof-of-work.

Watch the following instructional videos to receive up to 3-months of free premium courses on Dapp Mentors Academy, including:

Continue your Bitfinity journey by learning how to build easy Play-to-Earn dApps. Discover the potential of blockchain gaming in the next module. Dive deeper into Bitfinity and deploy your smart contracts to the network.

With that said, let’s jump into the tutorial and set up our project.

Setup

We'll start by cloning a prepared frontend repository and setting up the environment variables. Run the following commands:

git clone https://github.com/Daltonic/dappBnbX
cd dappBnbX
yarn install
git checkout no_redux
Enter fullscreen mode Exit fullscreen mode

Next, create a .env file at the root of the project and include the following keys:

NEXT_PUBLIC_RPC_URL=http://127.0.0.1:8545
NEXT_PUBLIC_ALCHEMY_ID=<YOUR_ALCHEMY_PROJECT_ID>
NEXT_PUBLIC_PROJECT_ID=<WALLET_CONNECT_PROJECT_ID>
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=somereallysecretsecret
Enter fullscreen mode Exit fullscreen mode

Replace <YOUR_ALCHEMY_PROJECT_ID> and <WALLET_CONNECT_PROJECT_ID> with your respective project IDs.

YOUR_ALCHEMY_PROJECT_ID: Get Key Here
WALLET_CONNECT_PROJECT_ID: Get Key Here

Finally, run yarn dev to start the project.

Dummy Data

Our frontend is ready for smart contract integration, but we need to implement Redux to enable shared data space.

Building the Redux Store

Store Structure

The above image represents the structure of our Redux store, it will be simple since we are not creating some overly complex project.

We'll set up Redux to manage our application's global state. Follow these steps:

  1. Create a store folder at the project root.
  2. Inside store, create two folders: actions and states.
  3. Inside states, create a globalStates.js file.
export const globalStates = {
appartments: [],
appartment: null,
reviews: [],
reviewModal: 'scale-0',
securityFee: 0,
bookings: [],
booking: null,
timestamps: [],
}
view raw globalStates.js hosted with ❤ by GitHub
  1. Inside actions, create a globalActions.js file.
export const globalActions = {
setApartments: (state, action) => {
state.apartments = action.payload
},
setApartment: (state, action) => {
state.apartment = action.payload
},
setReviews: (state, action) => {
state.reviews = action.payload
},
setReviewModal: (state, action) => {
state.reviewModal = action.payload
},
setSecurityFee: (state, action) => {
state.securityFee = action.payload
},
setBookings: (state, action) => {
state.bookings = action.payload
},
setBooking: (state, action) => {
state.booking = action.payload
},
setTimestamps: (state, action) => {
state.timestamps = action.payload
},
}
  1. Create a globalSlices.js file inside the store folder.
import { createSlice } from '@reduxjs/toolkit'
import { globalStates as GlobalStates } from './states/globalStates'
import { globalActions as GlobalActions } from './actions/globalActions'
export const globalSlices = createSlice({
name: 'global',
initialState: GlobalStates,
reducers: GlobalActions,
})
export const globalActions = globalSlices.actions
export default globalSlices.reducer
view raw globalSlices.js hosted with ❤ by GitHub
  1. Create an index.js file inside the store folder.
import { configureStore } from '@reduxjs/toolkit'
import globalSlices from './globalSlices'
export const store = configureStore({
reducer: {
globalStates: globalSlices,
},
})
view raw index.js hosted with ❤ by GitHub
  1. Update the pages/_app.jsx file with the Redux provider.
import { ToastContainer } from 'react-toastify'
import '@/styles/globals.css'
import 'react-toastify/dist/ReactToastify.css'
import '@rainbow-me/rainbowkit/styles.css'
import 'react-datepicker/dist/react-datepicker.css'
import { useEffect, useState } from 'react'
import Providers from '@/services/provider'
import { Footer, Header } from '@/components'
export default function App({ Component, pageProps }) {
const [showChild, setShowChild] = useState(false)
useEffect(() => {
setShowChild(true)
}, [])
if (!showChild || typeof window === 'undefined') {
return null
} else {
return (
<Providers pageProps={pageProps}>
<div className="relative h-screen min-w-screen">
<Header />
<Component {...pageProps} />
<div className="h-20"></div>
<Footer />
</div>
<ToastContainer
position="bottom-center"
autoClose={5000}
hideProgressBar={false}
newestOnTop={false}
closeOnClick
rtl={false}
pauseOnFocusLoss
draggable
pauseOnHover
theme="dark"
/>
</Providers>
)
}
}
view raw _app.jsx hosted with ❤ by GitHub

We have implemented Redux toolkit in our application and plan to revisit its usage when integrating the backend with the frontend.

Smart Contract Development

Next, we'll develop the smart contract for our platform:

  1. Create a contracts folder at the project root.
  2. Inside contracts, create a DappBnb.sol file and add the contract 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 location;
string images;
uint rooms;
uint price;
address owner;
bool booked;
bool deleted;
uint timestamp;
}
struct BookingStruct {
uint id;
uint aid;
address tenant;
uint date;
uint price;
bool checked;
bool cancelled;
}
struct ReviewStruct {
uint id;
uint aid;
string reviewText;
uint timestamp;
address owner;
}
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 location,
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(location).length > 0, 'Location 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.location = location;
lodge.images = images;
lodge.rooms = rooms;
lodge.price = price;
lodge.owner = msg.sender;
lodge.timestamp = currentTime();
appartmentExist[lodge.id] = true;
apartments[_totalAppartments.current()] = lodge;
}
function updateAppartment(
uint id,
string memory name,
string memory description,
string memory location,
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(location).length > 0, 'Location 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.location = location;
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 available;
for (uint i = 1; i <= _totalAppartments.current(); i++) {
if (!apartments[i].deleted) available++;
}
Apartments = new ApartmentStruct[](available);
uint256 index;
for (uint i = 1; i <= _totalAppartments.current(); i++) {
if (!apartments[i].deleted) {
Apartments[index++] = apartments[i];
}
}
}
function getApartment(uint id) public view returns (ApartmentStruct memory) {
return apartments[id];
}
function bookApartment(uint aid, uint[] memory dates) public payable {
require(appartmentExist[aid], 'Apartment not found!');
require(
msg.value >=
(apartments[aid].price * dates.length) +
(((apartments[aid].price * dates.length) * securityFee) / 100),
'Insufficient fund!'
);
require(datesAreCleared(aid, dates), 'Booked date found among dates!');
for (uint i = 0; i < dates.length; i++) {
BookingStruct memory booking;
booking.aid = aid;
booking.id = bookingsOf[aid].length;
booking.tenant = msg.sender;
booking.date = dates[i];
booking.price = apartments[aid].price;
bookingsOf[aid].push(booking);
isDateBooked[aid][dates[i]] = true;
bookedDates[aid].push(dates[i]);
}
}
function datesAreCleared(uint aid, uint[] memory dates) internal view returns (bool) {
bool lastCheck = true;
for (uint i = 0; i < dates.length; i++) {
for (uint j = 0; j < bookedDates[aid].length; j++) {
if (dates[i] == bookedDates[aid][j]) lastCheck = false;
}
}
return lastCheck;
}
function checkInApartment(uint aid, uint bookingId) public {
BookingStruct memory booking = bookingsOf[aid][bookingId];
require(msg.sender == booking.tenant, 'Unauthorized tenant!');
require(!booking.checked, 'Apartment already checked on this date!');
bookingsOf[aid][bookingId].checked = true;
uint tax = (booking.price * taxPercent) / 100;
uint fee = (booking.price * securityFee) / 100;
hasBooked[msg.sender][aid] = true;
payTo(apartments[aid].owner, (booking.price - tax));
payTo(owner(), tax);
payTo(msg.sender, fee);
}
function claimFunds(uint aid, uint bookingId) public {
require(msg.sender == apartments[aid].owner, 'Unauthorized entity');
require(!bookingsOf[aid][bookingId].checked, 'Apartment already checked on this date!');
uint price = bookingsOf[aid][bookingId].price;
uint fee = (price * taxPercent) / 100;
payTo(apartments[aid].owner, (price - fee));
payTo(owner(), fee);
payTo(msg.sender, securityFee);
}
function refundBooking(uint aid, uint bookingId) public nonReentrant {
BookingStruct memory booking = bookingsOf[aid][bookingId];
require(!booking.checked, 'Apartment already checked on this date!');
require(isDateBooked[aid][booking.date], 'Did not book on this date!');
if (msg.sender != owner()) {
require(msg.sender == booking.tenant, 'Unauthorized tenant!');
require(booking.date > currentTime(), 'Can no longer refund, booking date started');
}
bookingsOf[aid][bookingId].cancelled = true;
isDateBooked[aid][booking.date] = false;
uint lastIndex = bookedDates[aid].length - 1;
uint lastBookingId = bookedDates[aid][lastIndex];
bookedDates[aid][bookingId] = lastBookingId;
bookedDates[aid].pop();
uint fee = (booking.price * securityFee) / 100;
uint collateral = fee / 2;
payTo(apartments[aid].owner, collateral);
payTo(owner(), collateral);
payTo(msg.sender, booking.price);
}
function getUnavailableDates(uint aid) public view returns (uint[] memory) {
return bookedDates[aid];
}
function getBookings(uint aid) public view returns (BookingStruct[] memory) {
return bookingsOf[aid];
}
function getQualifiedReviewers(uint aid) public view returns (address[] memory Tenants) {
uint256 available;
for (uint i = 0; i < bookingsOf[aid].length; i++) {
if (bookingsOf[aid][i].checked) available++;
}
Tenants = new address[](available);
uint256 index;
for (uint i = 0; i < bookingsOf[aid].length; i++) {
if (bookingsOf[aid][i].checked) {
Tenants[index++] = bookingsOf[aid][i].tenant;
}
}
}
function getBooking(uint aid, uint bookingId) public view returns (BookingStruct memory) {
return bookingsOf[aid][bookingId];
}
function payTo(address to, uint256 amount) internal {
(bool success, ) = payable(to).call{ value: amount }('');
require(success);
}
function addReview(uint aid, string memory reviewText) public {
require(appartmentExist[aid], 'Appartment not available');
require(hasBooked[msg.sender][aid], 'Book first before review');
require(bytes(reviewText).length > 0, 'Review text cannot be empty');
ReviewStruct memory review;
review.aid = aid;
review.id = reviewsOf[aid].length;
review.reviewText = reviewText;
review.timestamp = currentTime();
review.owner = msg.sender;
reviewsOf[aid].push(review);
}
function getReviews(uint aid) public view returns (ReviewStruct[] memory) {
return reviewsOf[aid];
}
function tenantBooked(uint appartmentId) public view returns (bool) {
return hasBooked[msg.sender][appartmentId];
}
function currentTime() internal view returns (uint256) {
return (block.timestamp * 1000) + 1000;
}
}
view raw DappBnb.sol hosted with ❤ by GitHub

The DappBnb contract represents a decentralized house rental platform where users can list apartments for rent, book available apartments, and leave reviews.

Let's go through each part of the contract:

  1. Contract Setup and Libraries: The contract imports Ownable, Counters, and ReentrancyGuard from the OpenZeppelin library. Ownable provides basic authorization control functions, Counters is used for counting variables, and ReentrancyGuard helps prevent re-entrancy attacks.

  2. Struct Definitions: The contract defines three structs: ApartmentStruct, BookingStruct, and ReviewStruct. These define the data structures for apartments, bookings, and reviews respectively.

  3. State Variables: The contract declares several state variables, including security fees, tax percentages, and mappings to keep track of apartments, bookings, reviews, and other related data.

  4. Constructor: The constructor function initializes the taxPercent and securityFee state variables.

  5. Apartment Functions: Functions like createApartment, updateApartment, deleteApartment, getApartments, and getApartment are used to manage apartment listings on the platform.

  6. Booking Functions: Functions like bookApartment, checkInApartment, claimFunds, refundBooking, getUnavailableDates, getBookings, getQualifiedReviewers, and getBooking are used to manage the booking process and interactions between tenants and apartment owners.

  7. Review Functions: The addReview and getReviews functions manage the review process, allowing tenants to leave reviews for apartments they've booked.

  8. Utility Functions: The payTo and currentTime functions are utility functions used in multiple other functions. payTo is used to transfer funds between addresses, and currentTime returns the current block timestamp.

Contract Deployment and Seeding

Now, let's deploy our smart contract and populate it with some dummy data:

  1. Create a scripts folder at the project root.
  2. Inside scripts, create a deploy.js and a seed.js file and add the following codes.

Deploy Script

const { ethers } = require('hardhat')
const fs = require('fs')
async function deployContract() {
let contract
const taxPercent = 7
const securityFeePercent = 5
try {
contract = await ethers.deployContract('DappBnb', [taxPercent, securityFeePercent])
await contract.waitForDeployment()
console.log('Contracts deployed successfully.')
return contract
} catch (error) {
console.error('Error deploying contracts:', error)
throw error
}
}
async function saveContractAddress(contract) {
try {
const address = JSON.stringify(
{
dappBnbContract: contract.target,
},
null,
4
)
fs.writeFile('./contracts/contractAddress.json', address, 'utf8', (error) => {
if (error) {
console.error('Error saving contract address:', err)
} else {
console.log('Deployed contract address:', address)
}
})
} catch (error) {
console.error('Error saving contract address:', error)
throw error
}
}
async function main() {
let contract
try {
contract = await deployContract()
await saveContractAddress(contract)
console.log('Contract deployment completed successfully.')
} catch (error) {
console.error('Unhandled error:', error)
}
}
main().catch((error) => {
console.error('Unhandled error:', error)
process.exitCode = 1
})
view raw deploy.js hosted with ❤ by GitHub

Seed Script

const { faker } = require('@faker-js/faker')
const { ethers } = require('hardhat')
const fs = require('fs')
const toWei = (num) => ethers.parseEther(num.toString())
const dataCount = 5
const maxPrice = 3.5
const imagesUrls = [
'https://a0.muscache.com/im/pictures/miso/Hosting-3524556/original/24e9b114-7db5-4fab-8994-bc16f263ad1d.jpeg?im_w=720',
'https://a0.muscache.com/im/pictures/miso/Hosting-5264493/original/10d2c21f-84c2-46c5-b20b-b51d1c2c971a.jpeg?im_w=720',
'https://a0.muscache.com/im/pictures/prohost-api/Hosting-584469386220279136/original/227d4c26-43d5-42da-ad84-d039515c0bad.jpeg?im_w=720',
'https://a0.muscache.com/im/pictures/miso/Hosting-610511843622686196/original/253bfa1e-8c53-4dc0-a3af-0a75728c0708.jpeg?im_w=720',
'https://a0.muscache.com/im/pictures/miso/Hosting-535385560957380751/original/90cc1db6-d31c-48d5-80e8-47259e750d30.jpeg?im_w=720',
'https://a0.muscache.com/im/pictures/b7756897-ef31-4080-b881-c4c7b9ec0df7.jpg?im_w=720',
'https://a0.muscache.com/im/pictures/337660c5-939a-439b-976f-19219dbc80c7.jpg?im_w=720',
'https://a0.muscache.com/im/pictures/7739bab3-6dd7-40bb-82e1-50ae68719b7b.jpg?im_w=720',
'https://a0.muscache.com/im/pictures/49ee362b-b47f-49fa-b8c0-18a41dbd4c4d.jpg?im_w=720',
'https://a0.muscache.com/im/pictures/miso/Hosting-569315897060112509/original/7db7c768-fb46-4934-904e-74a9771f9a60.jpeg?im_w=720',
'https://a0.muscache.com/im/pictures/309bee53-311d-4f07-a2e7-14daadbbfb77.jpg?im_w=720',
'https://a0.muscache.com/im/pictures/miso/Hosting-660654516377752568/original/be407e38-ad1e-4b2b-a547-2185068229f6.jpeg?im_w=720',
'https://a0.muscache.com/im/pictures/miso/Hosting-10989371/original/46c0c87f-d9bc-443c-9b64-24d9e594b54c.jpeg?im_w=1200',
'https://a0.muscache.com/im/pictures/miso/Hosting-653943444831285144/original/73346136-e0bb-46a8-8ce4-a9fb5229e6b3.jpeg?im_w=720',
'https://a0.muscache.com/im/pictures/71993873/b158891b_original.jpg?im_w=720',
'https://a0.muscache.com/im/pictures/prohost-api/Hosting-686901689015576288/original/2cd072fa-8c03-4ef3-a061-268b9b957e28.jpeg?im_w=720',
'https://a0.muscache.com/im/pictures/b88162e9-9ce3-4254-8129-2ea8719ab2c3.jpg?im_w=720',
'https://a0.muscache.com/im/pictures/prohost-api/Hosting-585362898291824332/original/8a92bd09-9795-4586-bc32-6ab474d0922b.jpeg?im_w=720',
'https://a0.muscache.com/im/pictures/3757edd0-8d4d-4d51-9d2e-3000e8c3797e.jpg?im_w=720',
'https://a0.muscache.com/im/pictures/b7811ddd-b5e6-43ee-aa41-1fa28cf5ef95.jpg?im_w=720',
]
const shuffleArray = (array) => {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1))
;[array[i], array[j]] = [array[j], array[i]]
}
return array
}
const generateFakeApartment = (count) => {
const apartments = []
for (let i = 0; i < count; i++) {
const id = i + 1
const name = faker.word.words(5)
const deleted = faker.datatype.boolean()
const description = faker.lorem.paragraph()
const location = faker.lorem.word()
const price = faker.number.float({
min: 0.1,
max: maxPrice,
precision: 0.01,
})
const rooms = faker.number.int({ min: 2, max: 5 })
const owner = faker.string.hexadecimal({
length: { min: 42, max: 42 },
prefix: '0x',
})
const timestamp = faker.date.past().getTime()
const images = []
for (let i = 0; i < 5; i++) {
images.push(shuffleArray(imagesUrls)[0])
}
apartments.push({
id,
name,
description,
location,
price: toWei(price),
images: images.join(', '),
rooms,
owner,
timestamp,
deleted,
})
}
return apartments
}
async function createApartments(contract, apartment) {
const tx = await contract.createAppartment(
apartment.name,
apartment.description,
apartment.location,
apartment.images,
apartment.rooms,
apartment.price
)
await tx.wait()
}
async function bookApartments(contract, aid, dates) {
const tx = await contract.bookApartment(aid, dates, { value: toWei(maxPrice * dates.length) })
await tx.wait()
}
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
async function main() {
let dappBnbContract
try {
const contractAddresses = fs.readFileSync('./contracts/contractAddress.json', 'utf8')
const { dappBnbContract: dappBnbAddress } = JSON.parse(contractAddresses)
dappBnbContract = await ethers.getContractAt('DappBnb', dappBnbAddress)
const dates1 = [1678492800000, 1678579200000, 1678665600000]
// Process #1
await Promise.all(
generateFakeApartment(dataCount).map(async (apartment) => {
await createApartments(dappBnbContract, apartment)
})
)
await delay(2500) // Wait for 2.5 seconds
// Process #2
await Promise.all(
Array(dataCount)
.fill()
.map(async (_, i) => {
await bookApartments(dappBnbContract, i + 1, dates1)
})
)
console.log('Items dummy data seeded...')
} catch (error) {
console.error('Unhandled error:', error)
}
}
main().catch((error) => {
console.error('Unhandled error:', error)
process.exitCode = 1
})
view raw seed.js hosted with ❤ by GitHub
  1. Run the following commands to deploy the contract and seed it with data:

    yarn hardhat node # Run in terminal 1
    yarn hardhat run scripts/deploy.js # Run in terminal 2
    yarn hardhat run scripts/seed.js # Run in terminal 2

If you did that correctly, you should see a similar output like the one below:

Deployment

At this point we can start the integration of our smart contract to our frontend.

Frontend Integration

First, create a services folder at the project root, and inside it, create a blockchain.jsx file. This file will contain functions to interact with our smart contract.

import { ethers } from 'ethers'
import { store } from '@/store'
import { globalActions } from '@/store/globalSlices'
import address from '@/contracts/contractAddress.json'
import dappBnbAbi from '@/artifacts/contracts/DappBnb.sol/DappBnb.json'
const toWei = (num) => ethers.parseEther(num.toString())
const fromWei = (num) => ethers.formatEther(num)
let ethereum, tx
if (typeof window !== 'undefined') ethereum = window.ethereum
const { setBookings, setTimestamps, setReviews } = globalActions
const getEthereumContracts = async () => {
const accounts = await ethereum?.request?.({ method: 'eth_accounts' })
if (accounts?.length > 0) {
const provider = new ethers.BrowserProvider(ethereum)
const signer = await provider.getSigner()
const contracts = new ethers.Contract(address.dappBnbContract, dappBnbAbi.abi, signer)
return contracts
} else {
const provider = new ethers.JsonRpcProvider(process.env.NEXT_PUBLIC_RPC_URL)
const wallet = ethers.Wallet.createRandom()
const signer = wallet.connect(provider)
const contracts = new ethers.Contract(address.dappBnbContract, dappBnbAbi.abi, signer)
return contracts
}
}
const getApartments = async () => {
const contract = await getEthereumContracts()
const apartments = await contract.getApartments()
return structureAppartments(apartments)
}
const getApartment = async (id) => {
const contract = await getEthereumContracts()
const apartment = await contract.getApartment(id)
return structureAppartments([apartment])[0]
}
const getBookings = async (id) => {
const contract = await getEthereumContracts()
const bookings = await contract.getBookings(id)
return structuredBookings(bookings)
}
const getQualifiedReviewers = async (id) => {
const contract = await getEthereumContracts()
const bookings = await contract.getQualifiedReviewers(id)
return bookings
}
const getReviews = async (id) => {
const contract = await getEthereumContracts()
const reviewers = await contract.getReviews(id)
return structuredReviews(reviewers)
}
const getBookedDates = async (id) => {
const contract = await getEthereumContracts()
const bookings = await contract.getUnavailableDates(id)
const timestamps = bookings.map((timestamp) => Number(timestamp))
return timestamps
}
const getSecurityFee = async () => {
const contract = await getEthereumContracts()
const fee = await contract.securityFee()
return Number(fee)
}
const createApartment = async (apartment) => {
if (!ethereum) {
reportError('Please install a browser provider')
return Promise.reject(new Error('Browser provider not installed'))
}
try {
const contract = await getEthereumContracts()
tx = await contract.createAppartment(
apartment.name,
apartment.description,
apartment.location,
apartment.images,
apartment.rooms,
toWei(apartment.price)
)
await tx.wait()
return Promise.resolve(tx)
} catch (error) {
reportError(error)
return Promise.reject(error)
}
}
const updateApartment = async (apartment) => {
if (!ethereum) {
reportError('Please install a browser provider')
return Promise.reject(new Error('Browser provider not installed'))
}
try {
const contract = await getEthereumContracts()
tx = await contract.updateAppartment(
apartment.id,
apartment.name,
apartment.description,
apartment.location,
apartment.images,
apartment.rooms,
toWei(apartment.price)
)
await tx.wait()
return Promise.resolve(tx)
} catch (error) {
reportError(error)
return Promise.reject(error)
}
}
const deleteApartment = async (aid) => {
if (!ethereum) {
reportError('Please install a browser provider')
return Promise.reject(new Error('Browser provider not installed'))
}
try {
const contract = await getEthereumContracts()
tx = await contract.deleteAppartment(aid)
await tx.wait()
return Promise.resolve(tx)
} catch (error) {
reportError(error)
return Promise.reject(error)
}
}
const bookApartment = async ({ aid, timestamps, amount }) => {
if (!ethereum) {
reportError('Please install a browser provider')
return Promise.reject(new Error('Browser provider not installed'))
}
try {
const contract = await getEthereumContracts()
tx = await contract.bookApartment(aid, timestamps, {
value: toWei(amount),
})
await tx.wait()
const bookedDates = await getBookedDates(aid)
store.dispatch(setTimestamps(bookedDates))
return Promise.resolve(tx)
} catch (error) {
reportError(error)
return Promise.reject(error)
}
}
const checkInApartment = async (aid, timestamps) => {
if (!ethereum) {
reportError('Please install a browser provider')
return Promise.reject(new Error('Browser provider not installed'))
}
try {
const contract = await getEthereumContracts()
tx = await contract.checkInApartment(aid, timestamps)
await tx.wait()
const bookings = await getBookings(aid)
store.dispatch(setBookings(bookings))
return Promise.resolve(tx)
} catch (error) {
reportError(error)
return Promise.reject(error)
}
}
const refundBooking = async (aid, bookingId) => {
if (!ethereum) {
reportError('Please install a browser provider')
return Promise.reject(new Error('Browser provider not installed'))
}
try {
const contract = await getEthereumContracts()
tx = await contract.refundBooking(aid, bookingId)
await tx.wait()
const bookings = await getBookings(aid)
store.dispatch(setBookings(bookings))
return Promise.resolve(tx)
} catch (error) {
reportError(error)
return Promise.reject(error)
}
}
const addReview = async (aid, comment) => {
if (!ethereum) {
reportError('Please install a browser provider')
return Promise.reject(new Error('Browser provider not installed'))
}
try {
const contract = await getEthereumContracts()
tx = await contract.addReview(aid, comment)
await tx.wait()
const reviews = await getReviews(aid)
store.dispatch(setReviews(reviews))
return Promise.resolve(tx)
} catch (error) {
reportError(error)
return Promise.reject(error)
}
}
const structureAppartments = (appartments) =>
appartments.map((appartment) => ({
id: Number(appartment.id),
name: appartment.name,
owner: appartment.owner,
description: appartment.description,
location: appartment.location,
price: fromWei(appartment.price),
deleted: appartment.deleted,
images: appartment.images.split(','),
rooms: Number(appartment.rooms),
timestamp: Number(appartment.timestamp),
booked: appartment.booked,
}))
const structuredBookings = (bookings) =>
bookings.map((booking) => ({
id: Number(booking.id),
aid: Number(booking.aid),
tenant: booking.tenant,
date: Number(booking.date),
price: fromWei(booking.price),
checked: booking.checked,
cancelled: booking.cancelled,
}))
const structuredReviews = (reviews) =>
reviews.map((review) => ({
id: Number(review.id),
aid: Number(review.aid),
text: review.reviewText,
owner: review.owner,
timestamp: Number(review.timestamp),
}))
export {
getApartments,
getApartment,
getBookings,
getBookedDates,
createApartment,
updateApartment,
deleteApartment,
bookApartment,
checkInApartment,
refundBooking,
addReview,
getReviews,
getQualifiedReviewers,
getSecurityFee,
}
view raw blockchain.jsx hosted with ❤ by GitHub

The above script is a service that interacts with a smart contract deployed on the chain. In a joint effort with EthersJs, it provides functions to read and write data on the blockchain via the contract's methods.

Here's a breakdown of the functions:

  1. getEthereumContracts: This function establishes a connection to the Ethereum network either through a browser provider (if available) or via a JSON-RPC endpoint. It then creates an instance of the smart contract using its ABI and address. If a user is connected via their wallet, the contract instance is linked to the user's account. Otherwise, a random account is generated and linked to the contract.

  2. getApartments, getApartment: These functions fetch all apartments and a specific apartment respectively from the smart contract.

  3. getBookings: Fetches all bookings for a specific apartment from the smart contract.

  4. getQualifiedReviewers: Fetches all addresses that are qualified to leave reviews for a specific apartment.

  5. getReviews: Fetches all reviews for a specific apartment from the smart contract.

  6. getBookedDates: Fetches all dates that are booked for a specific apartment from the smart contract.

  7. getSecurityFee: Fetches the security fee from the smart contract.

  8. createApartment, updateApartment, deleteApartment: These functions allow a user to create, update, and delete apartments respectively.

  9. bookApartment: This function allows a user to book an apartment for specific dates.

  10. checkInApartment: This function allows a user to check into an apartment.

  11. refundBooking: This function allows a user to get a refund for a booking.

  12. addReview: This function allows a user to add a review for an apartment.

Update the provider.jsx file inside services to include the bitfinity network using the following codes.

import React, { useState, useEffect } from 'react'
import { RainbowKitSiweNextAuthProvider } from '@rainbow-me/rainbowkit-siwe-next-auth'
import { WagmiConfig, configureChains, createConfig } from 'wagmi'
import { RainbowKitProvider, connectorsForWallets, darkTheme } from '@rainbow-me/rainbowkit'
import {
metaMaskWallet,
trustWallet,
coinbaseWallet,
rainbowWallet,
} from '@rainbow-me/rainbowkit/wallets'
import { mainnet, hardhat } from 'wagmi/chains'
import { alchemyProvider } from 'wagmi/providers/alchemy'
import { publicProvider } from 'wagmi/providers/public'
import { SessionProvider } from 'next-auth/react'
const bitfinity = {
id: 355113,
name: 'Bitfinity',
network: 'bitfinity',
iconUrl: 'https://bitfinity.network/logo.png',
iconBackground: '#000000',
nativeCurrency: {
decimals: 18,
name: 'Bitfinity',
symbol: 'BFT',
},
rpcUrls: {
public: { http: ['https://testnet.bitfinity.network'] },
default: { http: ['https://testnet.bitfinity.network'] },
},
blockExplorers: {
default: { name: 'Bitfinity Block Explorer', url: 'https://explorer.bitfinity.network/' },
etherscan: { name: 'Bitfinity Block Explorer', url: 'https://explorer.bitfinity.network/' },
},
testnet: true,
}
const { chains, publicClient } = configureChains(
[mainnet, bitfinity, hardhat],
[alchemyProvider({ apiKey: process.env.NEXT_PUBLIC_ALCHEMY_ID }), publicProvider()]
)
const projectId = process.env.NEXT_PUBLIC_PROJECT_ID
const connectors = connectorsForWallets([
{
groupName: 'Recommended',
wallets: [
metaMaskWallet({ projectId, chains }),
trustWallet({ projectId, chains }),
coinbaseWallet({ appName: 'Coinbase', chains }),
rainbowWallet({ projectId, chains }),
],
},
])
const wagmiConfig = createConfig({
autoConnect: true,
connectors,
publicClient,
})
const demoAppInfo = {
appName: 'DappBnb dApp',
}
const getSiweMessageOptions = () => ({
statement: `
Once you're signed in, you'll be able to access all of our dApp's features.
Thank you for partnering with DappBnb!`,
})
const Providers = ({ children, pageProps }) => {
const [mounted, setMounted] = useState(false)
useEffect(() => setMounted(true), [])
return (
<WagmiConfig config={wagmiConfig}>
<SessionProvider refetchInterval={0} session={pageProps.session}>
<RainbowKitSiweNextAuthProvider getSiweMessageOptions={getSiweMessageOptions}>
<RainbowKitProvider theme={darkTheme()} chains={chains} appInfo={demoAppInfo}>
{mounted && children}
</RainbowKitProvider>
</RainbowKitSiweNextAuthProvider>
</SessionProvider>
</WagmiConfig>
)
}
export default Providers
view raw provider.jsx hosted with ❤ by GitHub

Page Interacting with Smart Contract

Next, we'll link the functions in the blockchain service to their respective interfaces in the frontend:

No 1: Displaying Available Apartments
Update pages/index.jsx to get data from the getApartments() function.

import Head from 'next/head'
import { getApartments } from '@/services/blockchain'
import { Category, Collection } from '@/components'
export default function Home({ apartmentsData }) {
return (
<div>
<Head>
<title>Home Page</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<Category />
<Collection appartments={apartmentsData} />
</div>
)
}
export const getServerSideProps = async () => {
const apartmentsData = await getApartments()
return {
props: {
apartmentsData: JSON.parse(JSON.stringify(apartmentsData)),
},
}
}
view raw index.jsx hosted with ❤ by GitHub

No 2: Displaying Single Apartment
Update pages/room/[roomId].jsx to use the getServerSideProps(), getApartment(), getBookedDates() and the other functions to retrieve apartment and booking details by the room’s Id.

import Head from 'next/head'
import { useEffect } from 'react'
import { useRouter } from 'next/router'
import { globalActions } from '@/store/globalSlices'
import { useDispatch, useSelector } from 'react-redux'
import { Title, ImageGrid, Description, Calendar, Actions, Review, AddReview } from '@/components'
import {
getReviews,
getApartment,
getBookedDates,
getSecurityFee,
getQualifiedReviewers,
} from '@/services/blockchain'
import { useAccount } from 'wagmi'
export default function Room({
apartmentData,
timestampsData,
reviewsData,
securityFee,
qualifiedReviewers,
}) {
const router = useRouter()
const { roomId } = router.query
const dispatch = useDispatch()
const { address } = useAccount()
const { setApartment, setTimestamps, setReviewModal, setReviews, setSecurityFee } = globalActions
const { apartment, timestamps, reviews } = useSelector((states) => states.globalStates)
useEffect(() => {
dispatch(setApartment(apartmentData))
dispatch(setTimestamps(timestampsData))
dispatch(setReviews(reviewsData))
dispatch(setSecurityFee(securityFee))
}, [
dispatch,
setApartment,
apartmentData,
setTimestamps,
timestampsData,
setReviews,
reviewsData,
setSecurityFee,
securityFee,
])
const handleReviewOpen = () => {
dispatch(setReviewModal('scale-100'))
}
return (
<>
<Head>
<title>Room | {apartment?.name}</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<div className="py-8 px-10 sm:px-20 md:px-32 space-y-8">
<Title apartment={apartment} />
<ImageGrid
first={apartment?.images[0]}
second={apartment?.images[1]}
third={apartment?.images[2]}
forth={apartment?.images[3]}
fifth={apartment?.images[4]}
/>
<Description apartment={apartment} />
<Calendar apartment={apartment} timestamps={timestamps} />
<Actions apartment={apartment} />
<div className="flex flex-col justify-between flex-wrap space-y-2">
<div className="flex justify-start items-center space-x-2">
<h1 className="text-xl font-semibold">Reviews</h1>
{qualifiedReviewers?.includes(address) && (
<button
className="cursor-pointer text-pink-500 hover:text-pink-700"
onClick={handleReviewOpen}
>
Drop your review
</button>
)}
</div>
<div>
{reviews.map((review, i) => (
<Review key={i} review={review} />
))}
{reviews.length < 1 && 'No reviews yet!'}
</div>
</div>
</div>
<AddReview roomId={roomId} />
</>
)
}
export const getServerSideProps = async (context) => {
const { roomId } = context.query
const apartmentData = await getApartment(roomId)
const timestampsData = await getBookedDates(roomId)
const qualifiedReviewers = await getQualifiedReviewers(roomId)
const reviewsData = await getReviews(roomId)
const securityFee = await getSecurityFee()
return {
props: {
apartmentData: JSON.parse(JSON.stringify(apartmentData)),
timestampsData: JSON.parse(JSON.stringify(timestampsData)),
reviewsData: JSON.parse(JSON.stringify(reviewsData)),
qualifiedReviewers: JSON.parse(JSON.stringify(qualifiedReviewers)),
securityFee: JSON.parse(JSON.stringify(securityFee)),
},
}
}
view raw [roomId].jsx hosted with ❤ by GitHub

No 3: Creating New Apartments
Update pages/room/add.jsx to use the createApartment() function for form submission.

import { useState } from 'react'
import { FaTimes } from 'react-icons/fa'
import { truncate } from '@/store'
import { toast } from 'react-toastify'
import { useRouter } from 'next/router'
import { useAccount } from 'wagmi'
import { createApartment } from '@/services/blockchain'
export default function Add() {
const { address } = useAccount()
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 = useRouter()
const handleSubmit = async (e) => {
e.preventDefault()
if (!name || !location || !description || !rooms || links.length != 5 || !price) return
const params = {
name,
description,
location,
rooms,
images: links.slice(0, 5).join(','),
price,
}
await toast.promise(
new Promise(async (resolve, reject) => {
await createApartment(params)
.then(async () => {
navigate.push('/')
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])
}
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>
)}
</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 border
hover:hover:text-[#ff385c]
hover:border-[#ff385c]
mt-5 transition-all duration-500 ease-in-out ${
!address ? 'opacity-50 cursor-not-allowed' : ''
}`}
disabled={!address}
>
Add Appartment
</button>
</form>
</div>
</div>
)
}
view raw add.jsx hosted with ❤ by GitHub

No 4: Editing Existing Apartments
Update pages/room/edit/[roomId].jsx to use the getApartment() function to retrieve apartment by Id and populate the form fields.

import { useState } from 'react'
import { truncate } from '@/store'
import { useAccount } from 'wagmi'
import { toast } from 'react-toastify'
import { useRouter } from 'next/router'
import { FaTimes } from 'react-icons/fa'
import { getApartment, updateApartment } from '@/services/blockchain'
export default function Edit({ apartment }) {
const { address } = useAccount()
const [name, setName] = useState(apartment.name)
const [description, setDescription] = useState(apartment.description)
const [location, setLocation] = useState(apartment.location)
const [rooms, setRooms] = useState(apartment.rooms)
const [images, setImages] = useState('')
const [price, setPrice] = useState(apartment.price)
const [links, setLinks] = useState(apartment.images)
const navigate = useRouter()
const handleSubmit = async (e) => {
e.preventDefault()
if (!name || !location || !description || !rooms || links.length != 5 || !price) return
const params = {
...apartment,
name,
description,
location,
rooms,
images: links.slice(0, 5).join(','),
price,
}
await toast.promise(
new Promise(async (resolve, reject) => {
await updateApartment(params)
.then(async () => {
navigate.push('/room/' + apartment.id)
resolve()
})
.catch(() => reject())
}),
{
pending: 'Approve transaction...',
success: 'Apartment updated successfully 👌',
error: 'Encountered error 🤯',
}
)
}
const addImage = () => {
if (links.length != 5) {
setLinks((prevState) => [...prevState, images])
}
setImages('')
}
const removeImage = (index) => {
links.splice(index, 1)
setLinks(() => [...links])
}
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 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>
)}
</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 border
hover:hover:text-[#ff385c]
hover:border-[#ff385c]
mt-5 transition-all duration-500 ease-in-out ${
!address ? 'opacity-50 cursor-not-allowed' : ''
}`}
disabled={!address}
>
Update Apartment
</button>
</form>
</div>
</div>
)
}
export const getServerSideProps = async (context) => {
const { roomId } = context.query
const apartment = await getApartment(roomId)
return {
props: {
apartment: JSON.parse(JSON.stringify(apartment))
},
}
}
view raw [roomId].jsx hosted with ❤ by GitHub

No 5: Displaying All Bookings
Update pages/bookings/roomId.jsx to get data from the getApartment() and getBookings() functions.

import { useEffect } from 'react'
import { Booking } from '@/components'
import { useRouter } from 'next/router'
import { globalActions } from '@/store/globalSlices'
import { useDispatch, useSelector } from 'react-redux'
import { getBookings, getApartment } from '@/services/blockchain'
const Bookings = ({ apartmentData, bookingsData }) => {
const router = useRouter()
const { roomId } = router.query
const dispatch = useDispatch()
const { setApartment, setBookings } = globalActions
const { apartment, bookings } = useSelector((states) => states.globalStates)
useEffect(() => {
dispatch(setApartment(apartmentData))
dispatch(setBookings(bookingsData))
}, [dispatch, setApartment, apartmentData, setBookings, bookingsData])
return (
<div className="w-full sm:w-3/5 mx-auto mt-8">
<h1 className="text-center text-3xl text-black font-bold">Bookings</h1>
{bookings.length < 1 && <div>No bookings for this apartment yet</div>}
{bookings.map((booking, i) => (
<Booking key={i} id={roomId} booking={booking} apartment={apartment} />
))}
</div>
)
}
export default Bookings
export const getServerSideProps = async (context) => {
const { roomId } = context.query
const apartmentData = await getApartment(roomId)
const bookingsData = await getBookings(roomId)
return {
props: {
apartmentData: JSON.parse(JSON.stringify(apartmentData)),
bookingsData: JSON.parse(JSON.stringify(bookingsData)),
},
}
}
view raw [roomId].jsx hosted with ❤ by GitHub

Components with Smart Contract

Like we did with the pages above, let’s Update the following components to interact with the smart contract:

No 1: Handling Apartment Deletion
Update components/Actions.jsx file to use the handleDelete() function to call the deleteApartment() functions.

import Link from 'next/link'
import { useAccount } from 'wagmi'
import { useRouter } from 'next/router'
import { CiEdit } from 'react-icons/ci'
import { MdDeleteOutline } from 'react-icons/md'
import { deleteApartment } from '@/services/blockchain'
import { toast } from 'react-toastify'
const Actions = ({ apartment }) => {
const navigate = useRouter()
const { address } = useAccount()
const handleDelete = async () => {
if (confirm(`Are you sure you want to delete Apartment ${apartment?.id}?`)) {
await toast.promise(
new Promise(async (resolve, reject) => {
await deleteApartment(apartment?.id)
.then(async () => {
navigate.push('/')
resolve()
})
.catch(() => reject())
}),
{
pending: 'Approve transaction...',
success: 'Apartment deleted successfully 👌',
error: 'Encountered error 🤯',
}
)
}
}
return (
<div className="flex justify-start items-center space-x-3 border-b-2 border-b-slate-200 pb-6">
{address == apartment?.owner && (
<>
<Link
href={'/room/edit/' + apartment?.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>
</>
)}
</div>
)
}
export default Actions
view raw Actions.jsx hosted with ❤ by GitHub

No 2: Adding Reviews to Apartment
Update components/AddReview.jsx modal to use the handleSubmit() function to call the addReview() function.

import { useState } from 'react'
import { toast } from 'react-toastify'
import { FaTimes } from 'react-icons/fa'
import { addReview } from '@/services/blockchain'
import { globalActions } from '@/store/globalSlices'
import { useDispatch, useSelector } from 'react-redux'
const AddReview = ({ roomId }) => {
const [reviewText, setReviewText] = useState('')
const dispatch = useDispatch()
const { setReviewModal } = globalActions
const { reviewModal } = useSelector((states) => states.globalStates)
const resetForm = () => {
setReviewText('')
}
const handleSubmit = async (e) => {
e.preventDefault()
if (!reviewText) return
await toast.promise(
new Promise(async (resolve, reject) => {
await addReview(roomId, reviewText)
.then(async (tx) => {
dispatch(setReviewModal('scale-0'))
resetForm()
resolve(tx)
})
.catch(() => reject())
}),
{
pending: 'Approve transaction...',
success: 'Review submitted 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={() => dispatch(setReviewModal('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>
</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-14"
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

Observe how Redux was used to open and close the review modal.

No 3: Adding Reviews to Apartment
Update components/Booking.jsx file to use the handleCheckIn() and handleRefund() functions.

import Link from 'next/link'
import { useAccount } from 'wagmi'
import { toast } from 'react-toastify'
import Identicon from 'react-identicons'
import { formatDate, truncate } from '@/utils/helper'
import { checkInApartment, refundBooking } from '@/services/blockchain'
const Booking = ({ booking }) => {
const { address } = useAccount()
const handleCheckIn = async () => {
await toast.promise(
new Promise(async (resolve, reject) => {
await checkInApartment(booking.aid, booking.id)
.then(async (tx) => {
console.log(tx)
resolve(tx)
})
.catch((error) => reject(error))
}),
{
pending: 'Approve transaction...',
success: 'Checked In successfully 👌',
error: 'Encountered error 🤯',
}
)
}
const handleRefund = async () => {
await toast.promise(
new Promise(async (resolve, reject) => {
await refundBooking(booking.aid, booking.id)
.then(async () => {
resolve()
})
.catch(() => reject())
}),
{
pending: 'Approve transaction...',
success: 'Refunded successfully 👌',
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
}
const functions = {
bookedDayStatus,
handleCheckIn,
handleRefund,
}
return <TenantView booking={booking} functions={functions} owner={address} />
}
const TenantView = ({ booking, functions, owner }) => {
return (
<div className="w-full flex justify-between items-center my-3 bg-gray-100 p-3">
<Link
className="flex justify-start items-center
space-x-2 font-medium"
href={'/room/' + booking.aid}
>
<Identicon
string={booking.tenant}
size={30}
className="rounded-full shadow-gray-500 shadow-sm"
/>
<div className="flex flex-col">
<span>{formatDate(booking.date)}</span>
<span className="text-gray-500 text-sm">{truncate(booking.tenant, 4, 4, 11)}</span>
</div>
</Link>
{booking.tenant == owner && !booking.checked && !booking.cancelled && (
<div className="flex space-x-2">
<button
className="p-2 bg-green-500 text-white rounded-full text-sm px-4"
onClick={functions.handleCheckIn}
>
Check In
</button>
<button
className="p-2 bg-red-500 text-white rounded-full text-sm px-4"
onClick={functions.handleRefund}
>
Refund
</button>
</div>
)}
{booking.tenant == owner && booking.checked && !booking.cancelled && (
<button
className="p-2 bg-yellow-500 text-white font-medium italic
rounded-full text-sm px-4"
>
Checked In
</button>
)}
{booking.tenant != owner && !booking.cancelled && (
<button
className="p-2 bg-orange-500 text-white font-medium italic
rounded-full text-sm px-4"
>
Booked
</button>
)}
{booking.cancelled && (
<button
className="p-2 bg-yellow-500 text-white font-medium italic
rounded-full text-sm px-4"
>
Cancelled
</button>
)}
</div>
)
}
export default Booking
view raw Booking.jsx hosted with ❤ by GitHub

No 4: Adding Reviews to Apartment
Lastly, update components/Calendar.jsx file to use the handleSubmit() function to call the bookApartment() function.

import moment from 'moment'
import Link from 'next/link'
import { useState } from 'react'
import { toast } from 'react-toastify'
import { useSelector } from 'react-redux'
import DatePicker from 'react-datepicker'
import { FaEthereum } from 'react-icons/fa'
import { bookApartment } from '@/services/blockchain'
const Calendar = ({ apartment, timestamps }) => {
const [checkInDate, setCheckInDate] = useState(null)
const [checkOutDate, setCheckOutDate] = useState(null)
const { securityFee } = useSelector((states) => states.globalStates)
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 = {
aid: apartment?.id,
timestamps: timestampArray,
amount:
apartment?.price * timestampArray.length +
(apartment?.price * timestampArray.length * securityFee) / 100,
}
await toast.promise(
new Promise(async (resolve, reject) => {
await bookApartment(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">
{apartment?.price} <small>per night</small>
</span>
</div>
</div>
<DatePicker
id="checkInDate"
selected={checkInDate}
onChange={setCheckInDate}
placeholderText="YYYY-MM-DD (Check In)"
dateFormat="yyyy-MM-dd"
minDate={new Date()}
excludeDates={timestamps}
required
className="rounded-lg w-full border border-gray-400 p-2"
/>
<DatePicker
id="checkOutDate"
selected={checkOutDate}
onChange={setCheckOutDate}
placeholderText="YYYY-MM-DD (Check out)"
dateFormat="yyyy-MM-dd"
minDate={checkInDate}
excludeDates={timestamps}
required
className="rounded-lg w-full border border-gray-400 p-2"
/>
<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 href={`/room/bookings/${apartment?.id}`} className="text-pink-500">
Check your bookings
</Link>
</form>
)
}
export default Calendar
view raw Calendar.jsx hosted with ❤ by GitHub

Also notice that we are using Redux to retrieve the security fee which is necessary to calculate the cost of booking an apartment.

The project is now complete as all components and pages are connected to the smart contract through the implementation of these updates.

If your Next.js server was offline, you can bring it back up by executing the command yarn dev.

For further learning, we recommends watch the full video of this build on our YouTube channel and visiting our website for additional resources.

Conclusion

In this tutorial, we've built a Decentralized House Rental Platform using Next.js, Redux, and Solidity. We set up the development environment, built the Redux store, and deployed the smart contract to the blockchain. By integrating the smart contract with the frontend, we created a seamless user experience.

Throughout the tutorial, you gained valuable skills in building Web3 applications, crafting smart contracts, and incorporating static type checking. You also learned how to use Redux for shared data space and interact with smart contracts from the frontend.

Now, you're ready to create your own Decentralized House Rental Platform. Happy coding and unleash your innovation in the world of Web3!

About Author

I am a web3 developer and the founder of Dapp Mentors, a company that helps businesses and individuals build and launch decentralized applications. I have over 7 years of experience in the software industry, and I am passionate about using blockchain technology to create new and innovative applications. I run a YouTube channel called Dapp Mentors where I share tutorials and tips on web3 development, and I regularly post articles online about the latest trends in the blockchain space.

Stay connected with us, join communities on
Discord: Join
X-Twitter: Follow
LinkedIn: Connect
GitHub: Explore
Website: Visit

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 (1)

Collapse
 
kanewilliamson profile image
kanewilliamson

OMG. Worth reading indeed. Infact its an amazing idea. Especially with the evolution of AI a good developer can do wonders in this field. Going to share it at vacationrentallicense.com. Its one of my airbnb rental licensing service base website.