DEV Community

Cover image for How to Build a Cinema Ticket Booking Dapp with React, Solidity, and CometChat
Gospel Darlington
Gospel Darlington

Posted on

5

How to Build a Cinema Ticket Booking Dapp with React, Solidity, and CometChat

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

Dapp Cinemas

Customers Live Chatting

Introduction

Are you a movie fan who wants to be able to book tickets to your favorite films without having to go through a third-party website? Or are you someone who is involved in the movie business and wants to build a more secure and transparent way to sell tickets? If so, then this tutorial is for you.

We will use React, Solidity, and CometChat to build our dapp. React is a JavaScript library that is used to build user interfaces. Solidity is a programming language that is used to write smart contracts on the Ethereum blockchain. CometChat is a real-time communication platform that allows users to chat with each other in real time.

By the end of this tutorial, you will have learned how to:

  • Build a React app
  • Deploy a Solidity smart contract
  • Use CometChat to add chat functionality to a DApp

So what are you waiting for? Let's get started!

Prerequisites

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

  • Node.js
  • Yarn
  • MetaMask
  • React
  • Solidity
  • CometChat SDK
  • Tailwind CSS

To set up your MetaMask for this project, I recommend that you can watch the video below.

I hope you enjoy this tutorial!

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 dappCinemas
cd dappCinemas
Enter fullscreen mode Exit fullscreen mode

Next, update the package.json with the snippet below.

{
"name": "dappcinemas",
"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",
"@emotion/react": "^11.10.5",
"@emotion/styled": "^11.10.5",
"@headlessui/react": "^1.7.15",
"@mui/icons-material": "^5.11.0",
"@mui/material": "^5.11.6",
"@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",
"moment": "^2.29.4",
"react": "^18.2.0",
"react-datepicker": "^4.10.0",
"react-dom": "^18.2.0",
"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",
"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

Next, execute the command “yarn install" in your terminal to install the above dependencies for this project.

Configuring CometChat SDK

To configure the CometChat SDK, please follow the steps provided below. Once completed, make sure to save the generated keys as environment variables for future use.

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 DappCinemas.

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

Navigate to the root directory of the project and open the "hardhat.config.js" file. Replace the existing content of the file with the provided settings.

require("@nomiclabs/hardhat-waffle");
require('dotenv').config()
module.exports = {
defaultNetwork: "localhost",
networks: {
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
}
}

This code configures Hardhat for your project. It includes importing necessary plugins, setting up networks (with localhost as the default), specifying the Solidity compiler version, defining paths for contracts and artifacts, and setting a timeout for Mocha tests.

The Smart Contract File

To proceed with creating the smart contract for this project, please follow these steps:

  1. In the root directory of your project, create a new folder named contracts inside the src folder. This folder will hold all the smart contract files.
  2. Inside the contracts folder, create a new file named DappCinemas.sol. This file will contain the code that defines the functionality of the smart contract.
  3. Copy the provided code and paste it into the DappCinemas.sol file.
  4. Save the file to ensure that the changes are applied.

By following these steps, you will have successfully set up the necessary directory structure and created the DappCinemas.sol file, which will serve as the foundation for implementing the logic of the smart contract.

//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 DappCinemas is Ownable {
using Counters for Counters.Counter;
Counters.Counter private _totalMovies;
Counters.Counter private _totalTickets;
Counters.Counter private _totalSlots;
struct MovieStruct {
uint256 id;
string name;
string imageUrl;
string genre;
string description;
uint256 timestamp;
bool deleted;
}
struct TicketStruct {
uint256 id;
uint256 movieId;
uint256 slotId;
address owner;
uint256 cost;
uint256 timestamp;
uint256 day;
bool refunded;
}
struct TimeSlotStruct {
uint256 id;
uint256 movieId;
uint256 ticketCost;
uint256 startTime;
uint256 endTime;
uint256 capacity;
uint256 seats;
bool deleted;
bool completed;
uint256 day;
uint256 balance;
}
event Action(string actionType);
uint256 public balance;
mapping(uint256 => bool) movieExists;
mapping(uint256 => MovieStruct) movies;
mapping(uint256 => TimeSlotStruct) movieTimeSlot;
mapping(uint256 => mapping(uint256 => address[])) ticketHolder;
function addMovie(
string memory name,
string memory imageUrl,
string memory genre,
string memory description
) public onlyOwner {
require(bytes(name).length > 0, "Movie name required");
require(bytes(imageUrl).length > 0, "Movie image url required");
require(bytes(genre).length > 0, "Movie genre required");
require(bytes(description).length > 0, "Movie description required");
_totalMovies.increment();
MovieStruct memory movie;
movie.id = _totalMovies.current();
movie.name = name;
movie.imageUrl = imageUrl;
movie.genre = genre;
movie.description = description;
movie.timestamp = currentTime();
movies[movie.id] = movie;
movieExists[movie.id] = true;
emit Action("Movie added successfully");
}
function updateMovie(
uint256 movieId,
string memory name,
string memory imageUrl,
string memory genre,
string memory description
) public onlyOwner {
require(movieExists[movieId], "Movie doesn't exist!");
require(bytes(name).length > 0, "Movie name required");
require(bytes(imageUrl).length > 0, "Movie image URL required");
require(bytes(genre).length > 0, "Movie genre required");
require(bytes(description).length > 0, "Movie description required");
for (uint256 slotId = 1; slotId <= _totalSlots.current(); slotId++) {
address[] storage holders = ticketHolder[movieId][slotId];
require(
holders.length == 0,
"Cannot update movie with purchased tickets"
);
}
movies[movieId].name = name;
movies[movieId].imageUrl = imageUrl;
movies[movieId].genre = genre;
movies[movieId].description = description;
emit Action("Movie updated successfully");
}
function deleteMovie(uint256 movieId) public onlyOwner {
require(movieExists[movieId], "Movie doesn't exist!");
for (uint256 slotId = 1; slotId <= _totalSlots.current(); slotId++) {
address[] memory holders = ticketHolder[movieId][slotId];
require(
holders.length == 0,
"Cannot delete movie with purchased tickets"
);
}
movies[movieId].deleted = true;
movieExists[movieId] = false;
}
function getMovies() public view returns (MovieStruct[] memory Movies) {
uint256 totalMovies;
for (uint256 i = 1; i <= _totalMovies.current(); i++) {
if (!movies[i].deleted) totalMovies++;
}
Movies = new MovieStruct[](totalMovies);
uint256 j = 0;
for (uint256 i = 1; i <= _totalMovies.current(); i++) {
if (!movies[i].deleted) {
Movies[j] = movies[i];
j++;
}
}
}
function getMovie(uint256 id) public view returns (MovieStruct memory) {
return movies[id];
}
function addTimeslot(
uint256 movieId,
uint256[] memory ticketCosts,
uint256[] memory startTimes,
uint256[] memory endTimes,
uint256[] memory capacities,
uint256[] memory viewingDays
) public onlyOwner {
require(movieExists[movieId], "Movie not found");
require(ticketCosts.length > 0, "Tickets cost must not be empty");
require(capacities.length > 0, "Capacities must not be empty");
require(startTimes.length > 0, "Start times cost must not be empty");
require(endTimes.length > 0, "End times cost must not be empty");
require(viewingDays.length > 0, "Viewing days must not be empty");
require(
ticketCosts.length == viewingDays.length &&
viewingDays.length == capacities.length &&
capacities.length == startTimes.length &&
startTimes.length == endTimes.length &&
endTimes.length == viewingDays.length,
"All parameters must have equal array length"
);
for (uint i = 0; i < viewingDays.length; i++) {
_totalSlots.increment();
TimeSlotStruct memory slot;
slot.id = _totalSlots.current();
slot.movieId = movieId;
slot.ticketCost = ticketCosts[i];
slot.startTime = startTimes[i];
slot.endTime = endTimes[i];
slot.day = viewingDays[i];
slot.capacity = capacities[i];
movieTimeSlot[slot.id] = slot;
}
}
function deleteTimeSlot(uint256 movieId, uint256 slotId) public onlyOwner {
require(
movieExists[movieId] && movieTimeSlot[slotId].movieId == movieId,
"Movie not found"
);
require(!movieTimeSlot[slotId].deleted, "Timeslot already deleted");
for (uint i = 0; i < ticketHolder[movieId][slotId].length; i++) {
payTo(
ticketHolder[movieId][slotId][i],
movieTimeSlot[slotId].ticketCost
);
}
movieTimeSlot[slotId].deleted = true;
movieTimeSlot[slotId].balance -=
movieTimeSlot[slotId].ticketCost *
ticketHolder[movieId][slotId].length;
delete ticketHolder[movieId][slotId];
}
function markTimeSlot(uint256 movieId, uint256 slotId) public onlyOwner {
require(
movieExists[movieId] && movieTimeSlot[slotId].movieId == movieId,
"Movie not found"
);
require(!movieTimeSlot[slotId].deleted, "Timeslot already deleted");
movieTimeSlot[slotId].completed = true;
balance += movieTimeSlot[slotId].balance;
movieTimeSlot[slotId].balance = 0;
}
function getTimeSlotsByDay(
uint256 day
) public view returns (TimeSlotStruct[] memory MovieSlots) {
uint256 available;
for (uint i = 0; i < _totalSlots.current(); i++) {
if (
movieTimeSlot[i + 1].day == day && !movieTimeSlot[i + 1].deleted
) {
available++;
}
}
MovieSlots = new TimeSlotStruct[](available);
uint256 index;
for (uint i = 0; i < _totalSlots.current(); i++) {
if (
movieTimeSlot[i + 1].day == day && !movieTimeSlot[i + 1].deleted
) {
MovieSlots[index].startTime = movieTimeSlot[i + 1].startTime;
MovieSlots[index++].endTime = movieTimeSlot[i + 1].endTime;
}
}
}
function getTimeSlot(
uint256 slotId
) public view returns (TimeSlotStruct memory) {
return movieTimeSlot[slotId];
}
function getTimeSlots(
uint256 movieId
) public view returns (TimeSlotStruct[] memory MovieSlots) {
uint256 available;
for (uint i = 0; i < _totalSlots.current(); i++) {
if (
movieTimeSlot[i + 1].movieId == movieId &&
!movieTimeSlot[i + 1].deleted
) {
available++;
}
}
MovieSlots = new TimeSlotStruct[](available);
uint256 index;
for (uint i = 0; i < _totalSlots.current(); i++) {
if (
movieTimeSlot[i + 1].movieId == movieId &&
!movieTimeSlot[i + 1].deleted
) {
MovieSlots[index++] = movieTimeSlot[i + 1];
}
}
}
function buyTicket(
uint256 movieId,
uint256 slotId,
uint256 tickets
) public payable {
require(
movieExists[movieId] && movieTimeSlot[slotId].movieId == movieId,
"Movie not found"
);
require(
msg.value >= movieTimeSlot[slotId].ticketCost * tickets,
"Insufficient amount"
);
require(
movieTimeSlot[slotId].capacity > movieTimeSlot[slotId].seats,
"Out of capacity"
);
for (uint i = 0; i < tickets; i++) {
_totalTickets.increment();
TicketStruct memory ticket;
ticket.id = _totalTickets.current();
ticket.cost = movieTimeSlot[slotId].ticketCost;
ticket.day = movieTimeSlot[slotId].day;
ticket.slotId = slotId;
ticket.owner = msg.sender;
ticket.timestamp = currentTime();
ticketHolder[movieId][slotId].push(msg.sender);
}
movieTimeSlot[slotId].seats += tickets;
movieTimeSlot[slotId].balance +=
movieTimeSlot[slotId].ticketCost *
tickets;
}
function getMovieTicketHolders(
uint256 movieId,
uint256 slotId
) public view returns (address[] memory) {
return ticketHolder[movieId][slotId];
}
function withdrawTo(address to, uint256 amount) public onlyOwner {
require(balance >= amount, "Insufficient fund");
balance -= amount;
payTo(to, amount);
}
function payTo(address to, uint256 amount) internal {
(bool success, ) = payable(to).call{value: amount}("");
require(success);
}
function currentTime() internal view returns (uint256) {
uint256 newNum = (block.timestamp * 1000) + 1000;
return newNum;
}
}
view raw DappCinemas.sol hosted with ❤ by GitHub

Here is an overview of the key components and functions of the smart contract:

  1. **Ownable**: The contract inherits from the **Ownable** contract in the OpenZeppelin library, which provides basic access control by allowing only the contract owner to execute certain functions.

  2. **Counters**: The contract uses the **Counters** library from OpenZeppelin to keep track of the total number of movies, tickets, and time slots created.

  3. **MovieStruct**: This struct defines the properties of a movie, including **id**, **name**, **imageUrl**, **genre**, **description**, **timestamp**, and **deleted**.

  4. **TicketStruct**: This struct defines the properties of a ticket, including **id**, **movieId**, **slotId**, **owner**, **cost**, **timestamp**, **day**, and **refunded**.

  5. **TimeSlotStruct**: This struct defines the properties of a time slot for a movie screening, including **id**, **movieId**, **ticketCost**, **startTime**, **endTime**, **capacity**, **seats**, **deleted**, **completed**, **day**, and **balance**.

  6. **Action** event: This event is emitted to indicate successful actions performed within the contract.

  7. **balance** variable: This variable stores the contract's current balance.

  8. Various mappings: The contract uses mappings to store and retrieve data related to movies, time slots, and ticket holders.

The contract provides the following functions:

  • **addMovie**: Allows the contract owner to add a new movie to the system by providing the necessary details such as **name**, **imageUrl**, **genre**, and **description**.

  • **updateMovie**: Allows the contract owner to update the details of an existing movie by specifying the **movieId** and providing the updated information.

  • **deleteMovie**: Allows the contract owner to delete a movie from the system by specifying the **movieId**. However, it can only be deleted if there are no purchased tickets associated with it.

  • **getMovies**: Retrieves an array of all available movies in the system.

  • **getMovie**: Retrieves the details of a specific movie by providing its **id**.

  • **addTimeslot**: Allows the contract owner to add a time slot for a movie screening by specifying the **movieId** and providing arrays of **ticketCosts**, **startTimes**, **endTimes**, **capacities**, and **viewingDays** for the time slots.

  • **deleteTimeSlot**: Allows the contract owner to delete a time slot for a movie screening by specifying the **movieId** and **slotId**. The function refunds all ticket holders for the canceled time slot.

  • **markTimeSlot**: Allows the contract owner to mark a time slot as completed by specifying the **movieId** and **slotId**. The function adds the balance of the completed time slot to the contract's overall balance.

  • **getTimeSlotsByDay**: Retrieves an array of time slots available for a specific day by specifying the **day**.

  • **getTimeSlot**: Retrieves the details of a specific time slot by providing its **slotId**.

  • **getTimeSlots**: Retrieves an array of time slots available for a specific movie by specifying the **movieId**.

  • **buyTicket**: Allows users to purchase tickets for a movie by specifying the **movieId**, **slotId**, and the number of **tickets** to purchase. The function checks for availability, verifies the payment amount, and records the ticket details.

  • **getMovieTicketHolders**: Retrieves an array of ticket holders for a specific movie by providing its **movieId**.

  • **getTicket**: Retrieves the details of a specific ticket by providing its **ticketId**.

  • **refundTicket**: Allows the contract owner to refund a ticket by specifying the **ticketId**. The function refunds the ticket owner and updates the ticket's **refunded** status.

  • **getContractBalance**: Retrieves the current balance of the contract.

  • **withdrawBalance**: Allows the contract owner to withdraw the contract's balance.

In a nutshell, the DappCinemas smart contract provides functionality for managing a decentralized cinema ticket booking system on the Ethereum blockchain. It allows the cinema owner to add, update, and delete movies, create time slots for screenings, and enables users to purchase tickets for specific movie screenings. The contract keeps track of movie and time slot details, ticket holders, and manages the financial transactions related to ticket sales.

Here is an opportunity to enhance your proficiency in Solidity, the web3 language. Get hold of your copy of a comprehensive book that will guide you towards mastery in Solidity. Access it now by following this link.

Capturing Smart Contract Development

The Test Script

The test script provided below is used to test the functionality of the **DappCinemas** contract. It contains several test cases grouped into different sections.

In the "Movie Management" section, the script tests creating a movie, updating its details, and deleting a movie.

In the "Showtime Management" section, the script tests adding a showtime for a movie, deleting a showtime, and verifying the availability of showtimes.

In the "Ticket Booking" section, the script tests booking a ticket for a specific movie and showtime, calculating revenue generated by a movie, canceling a booked ticket, and withdrawing funds from the company balance.

The script uses Chai assertions for making assertions and the Hardhat framework for deploying and interacting with the contract. Before each test case, the contract is deployed, and necessary variables and initializations are performed.

At the root of the project, create a folder if not existing called “test”, copy and paste the code below inside of it.

const { expect } = require('chai')
const { ethers } = require('hardhat')
const toWei = (num) => ethers.utils.parseEther(num.toString())
const fromWei = (num) => ethers.utils.formatEther(num)
describe('Contracts', () => {
const name = 'Terminator salvation'
const imageUrl = 'https://image.png'
const genre = 'thrill'
const description = 'Really cool movie'
const movieId = 1
const slotId = 1
let contract, result
beforeEach(async () => {
const Contract = await ethers.getContractFactory('DappCinemas')
;[deployer, buyer1, buyer2] = await ethers.getSigners()
contract = await Contract.deploy()
await contract.deployed()
})
describe('Movie Management', () => {
beforeEach(async () => {
await contract.addMovie(name, imageUrl, genre, description)
})
it('should create a movie and verify its details', async () => {
result = await contract.getMovies()
expect(result).to.have.lengthOf(1)
result = await contract.getMovie(movieId)
expect(result.name).to.be.equal(name)
expect(result.imageUrl).to.be.equal(imageUrl)
expect(result.genre).to.be.equal(genre)
expect(result.description).to.be.equal(description)
})
it('should update movie details and verify the changes', async () => {
const updatedName = 'Terminator 2: Judgment Day'
const updatedImageUrl = 'https://updated-image.jpg'
const updatedGenre = 'Action'
const updatedDescription = 'One of the best action movies ever'
await contract.updateMovie(
movieId,
updatedName,
updatedImageUrl,
updatedGenre,
updatedDescription
)
const result = await contract.getMovie(movieId)
expect(result.name).to.be.equal(updatedName)
expect(result.imageUrl).to.be.equal(updatedImageUrl)
expect(result.genre).to.be.equal(updatedGenre)
expect(result.description).to.be.equal(updatedDescription)
})
it('should delete a movie and ensure it is no longer accessible', async () => {
result = await contract.getMovie(movieId)
expect(result.deleted).to.be.equal(false)
await contract.deleteMovie(movieId)
result = await contract.getMovie(movieId)
expect(result.deleted).to.be.equal(true)
})
})
describe('Showtime Management', () => {
const ticketCosts = [toWei(0.02), toWei(0.04)]
const days = [1687305600000, 1687405600000]
const startTimes = [1687309200000, 1687309200000]
const endTimes = [1687314600000, 1687314600000]
const capacities = [5, 7]
beforeEach(async () => {
await contract.addMovie(name, imageUrl, genre, description)
await contract.addTimeslot(
movieId,
ticketCosts,
startTimes,
endTimes,
capacities,
days
)
})
it('should add a showtime and ensure it is added successfully', async () => {
result = await contract.getTimeSlots(movieId)
expect(result).to.have.lengthOf(2)
result = await contract.getTimeSlotsByDay(days[0])
expect(result).to.have.lengthOf(1)
result = await contract.getTimeSlot(slotId)
expect(result.day).to.be.equal(days[0])
expect(result.startTime).to.be.equal(startTimes[0])
expect(result.endTime).to.be.equal(endTimes[0])
expect(result.ticketCost).to.be.equal(ticketCosts[0])
})
it('should delete a showtime and verify that it is no longer available.', async () => {
result = await contract.getTimeSlots(movieId)
expect(result).to.have.lengthOf(2)
result = await contract.getTimeSlot(slotId)
expect(result.deleted).to.be.equal(false)
await contract.deleteTimeSlot(movieId, slotId)
result = await contract.getTimeSlots(movieId)
expect(result).to.have.lengthOf(1)
result = await contract.getTimeSlot(slotId)
expect(result.deleted).to.be.equal(true)
})
})
describe('Ticket Booking', () => {
const ticketCosts = [toWei(0.02), toWei(0.04)]
const days = [1687305600000, 1687305600000]
const startTimes = [1687309200000, 1687309200000]
const endTimes = [1687314600000, 1687314600000]
const capacities = [5, 7]
beforeEach(async () => {
await contract.addMovie(name, imageUrl, genre, description)
await contract.addTimeslot(
movieId,
ticketCosts,
startTimes,
endTimes,
capacities,
days
)
})
it('should book a ticket for a specific movie and verify that it is booked successfully', async () => {
result = await contract.getMovieTicketHolders(movieId, slotId)
expect(result).to.have.lengthOf(0)
const tickets = 2
await contract.connect(buyer1).buyTicket(movieId, slotId, tickets, {
value: toWei(fromWei(ticketCosts[0]) * tickets),
})
result = await contract.getMovieTicketHolders(movieId, slotId)
expect(result).to.have.lengthOf(tickets)
})
it('should calculate the total revenue generated by a movie, verify and turn over to company balance', async () => {
result = await contract.balance()
expect(result).to.be.equal(0)
result = await contract.getTimeSlot(slotId)
expect(result.balance).to.be.equal(0)
const tickets = 2
await contract.connect(buyer1).buyTicket(movieId, slotId, tickets, {
value: toWei(fromWei(ticketCosts[0]) * tickets),
})
result = await contract.balance()
expect(result).to.be.equal(0)
result = await contract.getTimeSlot(slotId)
expect(result.balance).to.be.equal(
toWei(fromWei(ticketCosts[0]) * tickets)
)
await contract.markTimeSlot(movieId, slotId)
result = await contract.balance()
expect(result).to.be.equal(toWei(fromWei(ticketCosts[0]) * tickets))
result = await contract.getTimeSlot(slotId)
expect(result.balance).to.be.equal(0)
})
it('should Cancel a booked ticket and verify that it is no longer valid.', async () => {
result = await contract.getTimeSlot(slotId)
expect(result.balance).to.be.equal(0)
const tickets = 2
await contract.connect(buyer1).buyTicket(movieId, slotId, tickets, {
value: toWei(fromWei(ticketCosts[0]) * tickets),
})
result = await contract.getTimeSlot(slotId)
expect(result.balance).to.be.equal(
toWei(fromWei(ticketCosts[0]) * tickets)
)
await contract.deleteTimeSlot(movieId, slotId)
result = await contract.getTimeSlot(slotId)
expect(result.balance).to.be.equal(0)
})
it('should withdraw from company balance and verify account value', async () => {
result = await contract.balance()
expect(result).to.be.equal(0)
const tickets = 2
await contract.connect(buyer1).buyTicket(movieId, slotId, tickets, {
value: toWei(fromWei(ticketCosts[0]) * tickets),
})
await contract.markTimeSlot(movieId, slotId)
result = await contract.balance()
expect(result).to.be.equal(toWei(fromWei(ticketCosts[0]) * tickets))
await contract.withdrawTo(deployer.address, ticketCosts[0])
result = await contract.balance()
expect(result).to.be.equal(ticketCosts[0])
})
})
})

By running **yarn hardhat test** or **npx hardhat test** on the terminal will test out all the essential function of this smart contract

The Deployment Script

The hardhat deployment script provided below is responsible for deploying the **DappCinemas** contract to the blockchain using Hardhat.

It first obtains the contract factory using **ethers.getContractFactory**, deploys the contract, and waits for it to be deployed successfully. Then, it adds a movie to the deployed contract by invoking the **addMovie** function with the movie details. After that, it writes the contract address to a JSON file for future reference. Finally, it logs the deployed contract address to the console.

If any errors occur during the deployment process, they are logged, and the script exits with a non-zero exit code.

In the root of the project, create a folder called “scripts” and another file inside of it called “deploy.js” if it doesn’t yet exist. Copy and paste the code below inside of it.

const { ethers } = require('hardhat')
const fs = require('fs')
async function main() {
const Contract = await ethers.getContractFactory('DappCinemas')
const contract = await Contract.deploy()
await contract.deployed()
// You can remove this part if you wish
const name = 'Spider-Man: Across the Spider-Verse'
const imageUrl =
'https://weliveentertainment.com/wp-content/uploads/2023/05/across-spider-verse-banner-4.jpg'
const genre = 'Animated, Action, Adventure, Comedy, Sci-Fi'
const description =
'Miles Morales returns for an epic adventure across the multiverse, teaming up with Gwen Stacy and a new team of Spider-People to face a new threat.'
await contract.addMovie(name, imageUrl, genre, description)
// End...
const address = JSON.stringify({ address: contract.address }, null, 4)
fs.writeFile('./src/abis/contractAddress.json', address, 'utf8', (err) => {
if (err) {
console.error(err)
return
}
console.log('Deployed contract address', contract.address)
})
}
main().catch((error) => {
console.error(error)
process.exitCode = 1
})
view raw deploy.js hosted with ❤ by GitHub

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

Activities of Deployment on the Terminal

If you require additional assistance with setting up Hardhat or deploying your Fullstack DApp, I recommend watching this informative video that provides guidance and instructions.

Developing the Frontend

To begin developing the frontend of our application, we will create a new directory called components in the “src” directory. This folder will contain all the components required for our project. For each of the components listed below, you need to create the corresponding files within the src/components folder.

Sidebar Component

Sidebar Component

The above component represents a sidebar for application. It displays a navigation menu with options such as "Movies," "Manage movies," and "Add Movies." It also provides the functionality to connect a wallet and displays the connected account. The component is responsive and hidden on small screens.

Create this component in the components folder, copy and paste the codes below into it.

import { MdMovie } from 'react-icons/md'
import { BsPlusLg } from 'react-icons/bs'
import { BsFillGearFill } from 'react-icons/bs'
import { Link } from 'react-router-dom'
import { connectWallet } from '../services/blockchain'
import { truncate, useGlobalState } from '../store'
const Sidebar = () => {
const [connectedAccount] = useGlobalState('connectedAccount')
const [deployer] = useGlobalState('deployer')
return (
<div className="hidden sm:flex">
<div className=" w-64 h-screen bg-transparent border-r border-grey-200">
<div className="flex flex-col justify-between py-4 px-8 h-screen">
<div className="border-b border-gray-200">
<Link to="/">
Dapp<span className="text-red-500">Cinemas</span>
</Link>
<div className="mt-8">
<h6 className="text-xs mb-4">Dashboard</h6>
<div className="flex flex-col space-y-3 py-2">
<div className=" border-l-2 transparent hover:border-l-2 hover:border-red-400">
<Link
to="/"
className="ml-8 flex cursor-pointer text-gray-600 hover:text-red-700 "
>
<MdMovie size={20} />
<span className="ml-4 text-sm text-gray-700">Movies</span>
</Link>
</div>
{connectedAccount &&
connectedAccount == deployer?.toLowerCase() && (
<>
<div className="border-l-2 transparent hover:border-l-2 hover:border-red-400">
<Link
to="/manage/movies"
className="ml-8 flex cursor-pointer text-gray-600 hover:text-red-700"
>
<BsFillGearFill size={20} />
<span className="ml-4 text-sm text-gray-700">
Manage movies
</span>
</Link>
</div>
<div className="border-l-2 transparent hover:border-l-2 hover:border-red-400">
<Link
to="/add/movies"
className="ml-8 flex cursor-pointer text-gray-600 hover:text-red-700"
>
<BsPlusLg size={20} />
<span className="ml-4 text-sm text-gray-700">
Add Movies
</span>
</Link>
</div>
</>
)}
</div>
</div>
</div>
<div className="border-t border-gray-200">
<div className="flex mt-4 list-none">
{connectedAccount ? (
<button
type="button"
className="inline-block px-8 py-2 border-2 border-red-600 text-xs leading-tight uppercase rounded-full focus:outline-none focus:ring-0 bg-gradient-to-r from-cyan-500 to-red-500 text-white font-bold"
>
{truncate(connectedAccount, 4, 4, 11)}
</button>
) : (
<button
type="button"
className="inline-block px-6 py-2 border-2 border-red-600 font-medium text-xs leading-tight uppercase rounded-full hover:bg-opacity-5 focus:outline-none focus:ring-0 transition duration-150 ease-in-out hover:bg-gradient-to-r from-cyan-500 to-red-500 hover:text-white hover:border-white"
onClick={connectWallet}
>
Connect Wallet
</button>
)}
</div>
</div>
</div>
</div>
</div>
)
}
export default Sidebar
view raw Sidebar.jsx hosted with ❤ by GitHub

Header Component

The Header Component

The below code represents a header component for this application. It includes a responsive menu icon that toggles a menu when clicked. The menu contains options such as "Movies," "Manage Movie," and "Add Movie." It also includes a search bar for searching content. The component allows users to connect their wallets and displays the connected account. The menu is hidden on larger screens and displayed on smaller screens. See the codes below, ensure you copy and paste appropriately.

import { React, useState } from 'react'
import { Link } from 'react-router-dom'
import { MdMenu } from 'react-icons/md'
import { AiOutlineClose } from 'react-icons/ai'
import { TbSearch } from 'react-icons/tb'
import { truncate, useGlobalState } from '../store'
import { connectWallet } from '../services/blockchain'
const Header = () => {
const [toggleMenu, setToggleMenu] = useState(false)
return (
<div className="flex flex-col justify-start border-b border-gray-200 p-2">
<div className="flex content-center items-center justify-between w-full">
<div className="flex p-2 sm:hidden">
<Link to="/">
Dapp<span className="text-red-500">Cinemas</span>
</Link>
</div>
<div className="flex space-x-4 p-1">
<form>
<div className="hidden sm:flex border border-gray-200 text-gray-500 p-2 items-center rounded-full min-w-[25vw] max-w-[560px]">
<TbSearch size={20} className="hidden md:flex" />
<input
placeholder="Search everything"
className="border-none flex-1 text-m px-2 outline-none"
/>
</div>
</form>
</div>
<div className="flex m-4 sm:hidden">
{toggleMenu ? (
<AiOutlineClose
size={20}
onClick={() => setToggleMenu(false)}
className="cursor-pointer"
/>
) : (
<MdMenu
size={30}
onClick={() => setToggleMenu(true)}
className="cursor-pointer"
/>
)}
</div>
</div>
{toggleMenu && <Menu />}
</div>
)
}
export default Header
const Menu = () => {
const [connectedAccount] = useGlobalState('connectedAccount')
return (
<div className="flex flex-col justify-center w-full p-2 space-y-2 sm:hidden">
<div className="flex space-x-4 p-2 justify-center w-full shadow-md">
<form>
<div className="flex border border-gray-200 text-gray-500 p-2 items-center rounded-full min-w-[25vw] max-w-[560px]">
<TbSearch size={20} className="hidden md:flex" />
<input
placeholder="Search everything"
className="border-none flex-1 text-m px-2 outline-none"
/>
</div>
</form>
</div>
<div className="flex flex-col space-y-4 items-center text-center w-full ">
<Link className="p-2 shadow-md w-full bg-white" to="/">
Movies
</Link>
<Link className="p-2 shadow-md w-full bg-white" to="/manage/movies">
Manage Movie
</Link>
<Link className="p-2 shadow-md w-full bg-white" to="/add/movies">
Add Movie
</Link>
<div className="flex mt-4 list-none">
{connectedAccount ? (
<button
type="button"
className="inline-block px-8 py-2 border-2 border-red-600 text-xs leading-tight uppercase rounded-full focus:outline-none focus:ring-0 bg-gradient-to-r from-cyan-500 to-red-500 text-white font-bold"
>
{truncate(connectedAccount, 4, 4, 11)}
</button>
) : (
<button
type="button"
className="inline-block px-6 py-2 border-2 border-red-600 font-medium text-xs leading-tight uppercase rounded-full hover:bg-opacity-5 focus:outline-none focus:ring-0 transition duration-150 ease-in-out hover:bg-gradient-to-r from-cyan-500 to-red-500 hover:text-white hover:border-white"
onClick={connectWallet}
>
Connect Wallet
</button>
)}
</div>
</div>
</div>
)
}
view raw Header.jsx hosted with ❤ by GitHub

Banner Component

The Banner Component

The code below represents a component called **Banner** in this project. This component displays a banner with a background image. The banner contains a title and a subtitle rendered with specific styles. This component is designed to be responsive and has a flex layout to position the title, subtitle, and button within the banner. See the code below.

import Image from '../asset/heroimage.jpg'
const Banner = () => {
return (
<div
style={{ backgroundImage: 'url(' + Image + ')' }}
className="w-full h-full rounded-3xl bg-no-repeat bg-cover bg-center mb-4 flex flex-col"
>
<div className="text-white p-8 space-y-8">
<div className="space-y-2">
<h3 className="text-3xl font-bold">AVATAR</h3>
<p className="text-xl font-medium">THE WAY OF THE WATER</p>
</div>
<div>
<button className="bg-transparent font-bold border-2 border-red-600 py-2 px-8 text-black hover:bg-gradient-to-r from-cyan-500 to-red-500 rounded-full hover:border-white hover:text-white ">
WATCH
</button>
</div>
</div>
</div>
)
}
export default Banner
view raw Banner.jsx hosted with ❤ by GitHub

Movie Cards Component

The Movie Card Component

The **MovieCard** component is a reusable component in this project that renders a movie card with an image, title, and truncated description. It is wrapped in a **Link** component from React Router to enable navigation to the movie's details page. The movie image, title, and description are dynamically populated using the **movie** prop. The component applies responsive styling and visual effects such as rounded corners, shadows, and borders. See the code below.

import { Link } from "react-router-dom"
import { truncate } from "../store"
const MovieCard = ({ movie }) => {
return (
<Link
to={`/movie/${movie.id}`}
className="flex justify-start flex-row space-x-8 sm:space-x-0
sm:flex-col p-3 space-y-2 shadow-lg w-auto rounded-lg border-2 border-gray-200"
>
<div className="flex h-full w-auto">
<img
src={movie.imageUrl}
className="rounded-lg object-cover h-64 w-full"
/>
</div>
<div className="flex flex-col">
<h3 className="font-bold md:text-lg my-2">{movie.name}</h3>
<p className="text-gray-600 font-light text-md">
{truncate(movie.description, 74, 0, 77)}
</p>
</div>
</Link>
)
}
export default MovieCard
view raw MovieCard.jsx hosted with ❤ by GitHub

Movies Table and Action Components

The Movies Table Components

The **MoviesTable** component displays a table of movies with information such as image, title, genre, date added, and description. It uses utility functions to format data and includes actions like editing, deleting, and managing time slots for each movie.

The **MovieAction** component on the other hand renders a dropdown menu with options to edit, delete, add time slots, and view all time slots for a movie. It uses icons and React Router's **Link** component for visual representation and navigation.

Use the codes below.

import { Menu } from '@headlessui/react'
import { FiEdit } from 'react-icons/fi'
import { TbCalendarPlus } from 'react-icons/tb'
import { FaRegCalendarCheck } from 'react-icons/fa'
import { BsTrash3 } from 'react-icons/bs'
import { BiDotsVerticalRounded } from 'react-icons/bi'
import { setGlobalState } from '../store'
import { Link } from 'react-router-dom'
const MovieAction = ({ movie }) => {
const openEditMovie = () => {
setGlobalState('movie', movie)
setGlobalState('updateMovieModal', 'scale-100')
}
const openDeleteMovie = () => {
setGlobalState('movie', movie)
setGlobalState('deleteMovieModal', 'scale-100')
}
return (
<Menu as="div" className="inline-block text-left">
<Menu.Button
className="inline-flex w-full justify-center
rounded-md bg-black bg-opacity-10 px-4 py-2 text-sm
font-medium text-black hover:bg-opacity-30 focus:outline-none
focus-visible:ring-2 focus-visible:ring-white
focus-visible:ring-opacity-75"
>
<BiDotsVerticalRounded size={17} />
</Menu.Button>
<Menu.Items
className="absolute right-0 mt-2 w-56 origin-top-right
divide-y divide-gray-100 rounded-md bg-white shadow-md
ing-1 ring-black ring-opacity-5 focus:outline-none"
>
<Menu.Item>
{({ active }) => (
<button
className={`flex justify-start items-center space-x-1 ${
active ? 'bg-gray-200 text-black' : 'text-gray-900'
} group flex w-full items-center rounded-md px-2 py-2 text-sm`}
onClick={openEditMovie}
>
<FiEdit size={17} />
<span>Edit</span>
</button>
)}
</Menu.Item>
<Menu.Item>
{({ active }) => (
<button
className={`flex justify-start items-center space-x-1 ${
active ? 'bg-red-500 text-white' : 'text-red-500'
} group flex w-full items-center rounded-md px-2 py-2 text-sm`}
onClick={openDeleteMovie}
>
<BsTrash3 size={17} />
<span>Delete</span>
</button>
)}
</Menu.Item>
<Menu.Item>
{({ active }) => (
<Link
to={'/timeslot/add/' + movie.id}
className={`flex justify-start items-center space-x-1 ${
active ? 'bg-gray-200 text-black' : 'text-gray-900'
} group flex w-full items-center rounded-md px-2 py-2 text-sm`}
>
<TbCalendarPlus size={17} />
<span>Add Time Slot</span>
</Link>
)}
</Menu.Item>
<Menu.Item>
{({ active }) => (
<Link
to={'/timeslot/' + movie.id}
className={`flex justify-start items-center space-x-1 ${
active ? 'bg-gray-200 text-black' : 'text-gray-900'
} group flex w-full items-center rounded-md px-2 py-2 text-sm`}
>
<FaRegCalendarCheck size={17} />
<span>All Slots</span>
</Link>
)}
</Menu.Item>
</Menu.Items>
</Menu>
)
}
export default MovieAction
view raw MovieAction.jsx hosted with ❤ by GitHub
import React from 'react'
import { convertTimestampToDate, truncate } from '../store'
import { Link } from 'react-router-dom'
import MovieAction from './MovieActions'
const MoviesTable = ({ movies }) => {
return (
<div className="overflow-x-auto">
<table className="min-w-full bg-white">
<thead>
<tr className="border-b">
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Image
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Title
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Genre
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Date Added
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Description
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Actions
</th>
</tr>
</thead>
<tbody>
{movies.map((movie, i) => (
<tr key={i} className="border-b">
<td className="px-6 py-4 whitespace-nowrap">
<img
className="h-16 w-16 object-cover rounded"
src={movie.imageUrl}
alt={movie.name}
/>
</td>
<td className="px-6 py-4 whitespace-nowrap text-blue-500">
<Link to={'/movie/' + movie.id}>{movie.name}</Link>
</td>
<td className="px-6 py-4 whitespace-nowrap">{movie.genre}</td>
<td className="px-6 py-4 whitespace-nowrap">
{convertTimestampToDate(movie.timestamp)}
</td>
<td className="px-6 py-4 whitespace-nowrap">
{truncate(movie.description, 50, 0, 53)}
</td>
<td className="px-6 py-4 whitespace-nowrap">
<MovieAction movie={movie} />
</td>
</tr>
))}
</tbody>
</table>
</div>
)
}
export default MoviesTable
view raw MoviesTable.jsx hosted with ❤ by GitHub

If you're interested in learning how to create a decentralized app by building a web3 lottery dapp, I recommend that you watch this video:

This video provides a hands-on tutorial on how to build a lottery dapp using NextJs, Tailwind CSS, and Solidity.

Update Movie Component

The movie update component

The **UpdateMovie** component is a modal that allows users to edit movie details. It includes a form with input fields for image URL, movie name, genre, and description. The form validates required fields and displays a toast notification when the update is in progress, successful, or encounters an error. The component also handles the closing of the modal, updates the movie state, and triggers the updateMovie function. Use the code below.

import { useEffect, useState } from 'react'
import { useGlobalState, setGlobalState } from '../store'
import { FaTimes } from 'react-icons/fa'
import { updateMovie } from '../services/blockchain'
import { toast } from 'react-toastify'
const UpdateMovie = () => {
const [updateMovieModal] = useGlobalState('updateMovieModal')
const [movieData] = useGlobalState('movie')
const [movie, setMovie] = useState({
imageUrl: '',
name: '',
genre: '',
description: '',
})
useEffect(() => {
if (movieData) {
setMovie({
imageUrl: movieData.imageUrl,
name: movieData.name,
genre: movieData.genre,
description: movieData.description,
})
}
}, [movieData])
const closeModal = () => {
setGlobalState('updateMovieModal', 'scale-0')
setMovie({
imageUrl: '',
name: '',
genre: '',
description: '',
})
setGlobalState('movie', null)
}
const handleSubmit = async (e) => {
e.preventDefault()
if (!movie.imageUrl || !movie.name || !movie.genre || !movie.description)
return
await toast.promise(
new Promise(async (resolve, reject) => {
await updateMovie({ ...movie, id: movieData.id })
.then(async () => {
closeModal()
resolve()
})
.catch((error) => {
console.log(error)
reject(error)
})
}),
{
pending: 'Approve transaction...',
success: 'Movie updated successfully 👌',
error: 'Encountered error 🤯',
}
)
}
const handleChange = (e) => {
const { name, value } = e.target
setMovie((prevMovie) => ({
...prevMovie,
[name]: value,
}))
}
return (
<div
className={`fixed top-0 left-0 w-screen h-screen flex items-center justify-center
bg-black bg-opacity-50 transform z-50 transition-transform duration-300 ${updateMovieModal}`}
>
<div className="bg-white shadow-lg shadow-slate-900 rounded-xl w-11/12 md:w-2/5 h-7/12 p-6">
<form onSubmit={handleSubmit} className="flex flex-col">
<div className="flex flex-row justify-between items-center">
<p className="font-semibold">Edit Movie</p>
<button
type="button"
className="border-0 bg-transparent focus:outline-none"
onClick={closeModal}
>
<FaTimes className="text-gray-400" />
</button>
</div>
<div className="flex flex-col justify-center items-center rounded-xl mt-5 mb-5">
<div className="flex justify-center items-center rounded-full overflow-hidden h-10 w-64 shadow-md shadow-slate-300 p-4">
<p className="text-slate-700">
{' '}
Dapp <span className="text-red-700">Cinemas</span>
</p>
</div>
</div>
<div
className="flex flex-row justify-between items-center
bg-gray-300 rounded-xl mt-5 p-2"
>
<input
className="block w-full text-sm text-slate-500 bg-transparent
border-0 focus:outline-none focus:ring-0"
type="url"
name="image_url"
placeholder="image url"
value={movie.imageUrl}
onChange={handleChange}
required
/>
</div>
<div className="flex flex-row justify-between items-center bg-gray-300 rounded-xl mt-5 p-2">
<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="movie name"
value={movie.name}
onChange={handleChange}
required
/>
</div>
<div className="flex flex-row justify-between items-center bg-gray-300 rounded-xl mt-5 p-2">
<input
className="block w-full text-sm text-slate-500 bg-transparent
border-0 focus:outline-none focus:ring-0"
type="text"
name="genre"
placeholder="separate genre with commas, eg. hilarious, action, thrilling"
value={movie.genre}
onChange={handleChange}
required
/>
</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="description"
placeholder="description."
value={movie.description}
onChange={handleChange}
required
></textarea>
</div>
<button
type="submit"
className="flex flex-row justify-center items-center w-full text-white text-md
bg-red-500 py-2 px-5 rounded-full drop-shadow-xl border border-transparent
hover:bg-transparent hover:border-red-500 hover:text-red-500 focus:outline-none mt-5"
>
Update
</button>
</form>
</div>
</div>
)
}
export default UpdateMovie
view raw UpdateMovie.jsx hosted with ❤ by GitHub

Delete Movie Component

The delete movie component

The DeleteMovie component is a modal that confirms the deletion of a movie. It displays a warning icon and the name of the movie to be deleted. The component includes a form with a delete button. When the form is submitted, a toast notification is displayed to indicate the progress of the deletion. The component also handles the closing of the modal and triggers the deleteMovie function. Use the snippet below:

import { RiErrorWarningFill } from 'react-icons/ri'
import { useGlobalState, setGlobalState } from '../store'
import { toast } from 'react-toastify'
import { deleteMovie } from '../services/blockchain'
import { FaTimes } from 'react-icons/fa'
const DeleteMovie = () => {
const [deleteMovieModal] = useGlobalState('deleteMovieModal')
const [movie] = useGlobalState('movie')
const closeModal = () => {
setGlobalState('deleteMovieModal', 'scale-0')
setGlobalState('movie', null)
}
const handleSubmit = async (e) => {
e.preventDefault()
await toast.promise(
new Promise(async (resolve, reject) => {
await deleteMovie(movie.id)
.then((res) => {
closeModal()
resolve(res)
})
.catch((error) => {
console.log(error)
reject(error)
})
}),
{
pending: 'Approve transaction...',
success: 'Movie deleted 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-50 transition-transform duration-300 ${deleteMovieModal}`}
>
<div className="bg-white shadow-lg shadow-slate-900 rounded-xl w-11/12 md:w-2/5 h-7/12 p-6">
<form onSubmit={handleSubmit} className="flex flex-col">
<div className="flex flex-row justify-between items-center">
<p className="font-semibold">Delete Movie</p>
<button
type="button"
className="border-0 bg-transparent focus:outline-none"
onClick={closeModal}
>
<FaTimes className="text-gray-400" />
</button>
</div>
<div className="flex flex-col justify-center items-center rounded-xl mt-5 mb-5">
<RiErrorWarningFill className="text-6xl text-red-700 " />
<p className="p-2">
Are you sure you want to delete{' '}
<span className="italic">"{movie?.name}"</span>
</p>
</div>
<button
className="flex flex-row justify-center items-center w-full text-white text-md
bg-red-500 py-2 px-5 rounded-full drop-shadow-xl border border-transparent
hover:bg-transparent hover:border-red-500 hover:text-red-500 focus:outline-none mt-5"
>
Delete
</button>
</form>
</div>
</div>
)
}
export default DeleteMovie
view raw DeleteMovie.jsx hosted with ❤ by GitHub

Withdrawal Component

The Withdrawal Component

The **Withdrawal** component is a modal form that allows users to withdraw money. It utilizes React hooks such as **useState** and custom hooks like **useGlobalState** from the store. The component interacts with the blockchain through the **withdraw** function from the blockchain service. Toast notifications from **react-toastify** are used to display the status of the transaction. The component captures input values, handles form submission, and provides a close button to dismiss the modal. Use the code below.

import { useState } from 'react'
import { useGlobalState, setGlobalState } from '../store'
import { FaTimes } from 'react-icons/fa'
import { withdraw } from '../services/blockchain'
import { toast } from 'react-toastify'
const Withdrawal = () => {
const [withdrwalModal] = useGlobalState('withdrwalModal')
const [transfer, setTransfer] = useState({
account: '',
amount: '',
})
const closeModal = () => {
setGlobalState('withdrwalModal', 'scale-0')
setTransfer({
account: '',
amount: '',
})
}
const handleSubmit = async (e) => {
e.preventDefault()
if (!transfer.account || !transfer.amount) return
await toast.promise(
new Promise(async (resolve, reject) => {
await withdraw(transfer)
.then(async () => {
closeModal()
resolve()
})
.catch((error) => {
console.log(error)
reject(error)
})
}),
{
pending: 'Approve transaction...',
success: 'Transfer successful 👌',
error: 'Encountered error 🤯',
}
)
}
const handleChange = (e) => {
const { name, value } = e.target
setTransfer((prevMovie) => ({
...prevMovie,
[name]: value,
}))
}
return (
<div
className={`fixed top-0 left-0 w-screen h-screen flex items-center justify-center
bg-black bg-opacity-50 transform z-50 transition-transform duration-300 ${withdrwalModal}`}
>
<div className="bg-white shadow-lg shadow-slate-900 rounded-xl w-11/12 md:w-2/5 h-7/12 p-6">
<form onSubmit={handleSubmit} className="flex flex-col">
<div className="flex flex-row justify-between items-center">
<p className="font-semibold">Withraw Money</p>
<button
type="button"
className="border-0 bg-transparent focus:outline-none"
onClick={closeModal}
>
<FaTimes className="text-gray-400" />
</button>
</div>
<div className="flex flex-col justify-center items-center rounded-xl mt-5 mb-5">
<div className="flex justify-center items-center rounded-full overflow-hidden h-10 w-64 shadow-md shadow-slate-300 p-4">
<p className="text-slate-700">
{' '}
Dapp <span className="text-red-700">Cinemas</span>
</p>
</div>
</div>
<div
className="flex flex-row justify-between items-center
bg-gray-300 rounded-xl mt-5 p-2"
>
<input
className="block w-full text-sm text-slate-500 bg-transparent
border-0 focus:outline-none focus:ring-0"
type="text"
name="account"
minLength={42}
maxLength={42}
pattern="[A-Za-z0-9]+"
placeholder="ETH Account"
value={transfer.account}
onChange={handleChange}
required
/>
</div>
<div className="flex flex-row justify-between items-center bg-gray-300 rounded-xl mt-5 p-2">
<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="amount"
placeholder="Amount (ETH)"
value={transfer.amount}
onChange={handleChange}
required
/>
</div>
<button
type="submit"
className="flex flex-row justify-center items-center w-full text-white text-md
bg-red-500 py-2 px-5 rounded-full drop-shadow-xl border border-transparent
hover:bg-transparent hover:border-red-500 hover:text-red-500 focus:outline-none mt-5"
>
Wire Fund
</button>
</form>
</div>
</div>
)
}
export default Withdrawal
view raw Withdrawal.jsx hosted with ❤ by GitHub

The Chat Components

The Chat Options

The Chat Modal

The ChatActions component provides various chat options based on the user's authentication status and the movie's group status. It includes functionality for signing up, logging in, creating a group, and joining a group. Each action triggers an asynchronous operation and displays toast notifications to indicate the progress and outcome of the operation.

The ChatModal component represents a chat room modal. It displays messages exchanged in the chat room and allows the user to send new messages. The component includes functionality for fetching initial messages, listening for new messages, and scrolling to the latest message. It also includes a Message component that represents an individual chat message, displaying the sender's avatar and truncated username along with the message content.

The chat components, **ChatActions** and **ChatModal**, are built using the CometChat SDK from the chat service. CometChat provides a comprehensive set of features and functionalities for integrating real-time chat functionality into applications.

In the **ChatActions** component, functions such as **signUpWithCometChat**, **loginWithCometChat**, **createNewGroup**, and **joinGroup** are called asynchronously using the CometChat SDK. These functions handle the respective chat operations, such as signing up a user, logging in, creating a group, and joining a group. Toast notifications are used to indicate the status of these operations, such as pending, success, or error.

The **ChatModal** component utilizes the CometChat SDK to retrieve messages from a specific chat room (**getMessages**), send new messages (**sendMessage**), and listen for incoming messages (**listenForMessage**). The component also includes functionality to scroll to the latest message in the chat room.

By leveraging the CometChat SDK, these chat components enable users to interact with chat functionality, including signing up, logging in, creating or joining groups, and exchanging messages within a chat room.

Bring real-time messaging to your application with CometChat. Try our SDK and UI Kits today!

Now, let’s create these components into our application like we have been doing with the other components. Use the code below.

import { Menu } from '@headlessui/react'
import { FiEdit } from 'react-icons/fi'
import { BiLogInCircle } from 'react-icons/bi'
import { SiGnuprivacyguard } from 'react-icons/si'
import { BsChatLeftDots } from 'react-icons/bs'
import { FiChevronDown } from 'react-icons/fi'
import { MdOutlineJoinLeft } from 'react-icons/md'
import { toast } from 'react-toastify'
import {
createNewGroup,
joinGroup,
loginWithCometChat,
signUpWithCometChat,
} from '../services/chat'
import { setGlobalState, useGlobalState } from '../store'
const ChatActions = ({ movie, group }) => {
const [connectedAccount] = useGlobalState('connectedAccount')
const [owner] = useGlobalState('deployer')
const [currentUser] = useGlobalState('currentUser')
const handleSignUp = async () => {
await toast.promise(
new Promise(async (resolve, reject) => {
await signUpWithCometChat(connectedAccount)
.then((user) => resolve(user))
.catch((error) => {
alert(JSON.stringify(error))
reject(error)
})
}),
{
pending: 'Signning up...',
success: 'Signed up successfully, please login 👌',
error: 'Encountered error 🤯',
}
)
}
const handleLogin = async () => {
await toast.promise(
new Promise(async (resolve, reject) => {
await loginWithCometChat(connectedAccount)
.then((user) => {
setGlobalState('currentUser', user)
resolve(user)
window.location.reload()
})
.catch((error) => {
alert(JSON.stringify(error))
reject(error)
})
}),
{
pending: 'Logging...',
success: 'Logged in successfully 👌',
error: 'Encountered error 🤯',
}
)
}
const handleCreateGroup = async () => {
await toast.promise(
new Promise(async (resolve, reject) => {
await createNewGroup(`guid_${movie.id}`, movie.name)
.then((group) => {
setGlobalState('group', group)
resolve(group)
window.location.reload()
})
.catch((error) => {
alert(JSON.stringify(error))
reject(error)
})
}),
{
pending: 'Creating group...',
success: 'Group created successfully 👌',
error: 'Encountered error 🤯',
}
)
}
const handleJoinGroup = async () => {
await toast.promise(
new Promise(async (resolve, reject) => {
await joinGroup(`guid_${movie.id}`)
.then((group) => {
setGlobalState('group', group)
resolve()
window.location.reload()
})
.catch((error) => {
alert(JSON.stringify(error))
reject(error)
})
}),
{
pending: 'Joining group...',
success: 'Group joined successfully 👌',
error: 'Encountered error 🤯',
}
)
}
return (
<Menu as="div" className="relative inline-block text-left mx-auto">
<Menu.Button
className="inline-flex justify-center items-center space-x-2
rounded-md bg-black bg-opacity-10 px-4 py-2 text-sm
font-medium text-black hover:bg-opacity-30 focus:outline-none
focus-visible:ring-2 focus-visible:ring-white
focus-visible:ring-opacity-75"
>
<span>Chat Options</span>
<FiChevronDown size={17} />
</Menu.Button>
<Menu.Items
className="absolute right-0 mt-2 w-56 origin-top-right
divide-y divide-gray-100 rounded-md bg-white shadow-md
ing-1 ring-black ring-opacity-5 focus:outline-none"
>
{!currentUser && (
<>
<Menu.Item>
{({ active }) => (
<button
className={`flex justify-start items-center space-x-1 ${
active ? 'bg-gray-200 text-black' : 'text-red-500'
} group flex w-full items-center rounded-md px-2 py-2 text-sm`}
onClick={handleSignUp}
>
<SiGnuprivacyguard size={17} />
<span>Chat SignUp</span>
</button>
)}
</Menu.Item>
<Menu.Item>
{({ active }) => (
<button
className={`flex justify-start items-center space-x-1 ${
active ? 'bg-gray-200 text-black' : 'text-gray-900'
} group flex w-full items-center rounded-md px-2 py-2 text-sm`}
onClick={handleLogin}
>
<BiLogInCircle size={17} />
<span>Chat Login</span>
</button>
)}
</Menu.Item>
</>
)}
{currentUser && (
<>
{currentUser.uid != owner && group && !group.hasJoined && (
<Menu.Item>
{({ active }) => (
<button
className={`flex justify-start items-center space-x-1 ${
active ? 'bg-gray-200 text-black' : 'text-gray-900'
} group flex w-full items-center rounded-md px-2 py-2 text-sm`}
onClick={handleJoinGroup}
>
<MdOutlineJoinLeft size={17} />
<span>Join Group</span>
</button>
)}
</Menu.Item>
)}
{currentUser.uid == owner && !group && (
<Menu.Item>
{({ active }) => (
<button
className={`flex justify-start items-center space-x-1 ${
active ? 'bg-gray-200 text-black' : 'text-gray-900'
} group flex w-full items-center rounded-md px-2 py-2 text-sm`}
onClick={handleCreateGroup}
>
<FiEdit size={17} />
<span>Create Group</span>
</button>
)}
</Menu.Item>
)}
{group && group.hasJoined && (
<Menu.Item>
{({ active }) => (
<button
className={`flex justify-start items-center space-x-1 ${
active ? 'bg-gray-200 text-black' : 'text-gray-900'
} group flex w-full items-center rounded-md px-2 py-2 text-sm`}
onClick={() => setGlobalState('chatModal', 'scale-100')}
>
<BsChatLeftDots size={17} />
<span>Chat</span>
</button>
)}
</Menu.Item>
)}
</>
)}
</Menu.Items>
</Menu>
)
}
export default ChatActions
view raw ChatActions.jsx hosted with ❤ by GitHub
import { FaTimes } from 'react-icons/fa'
import { setGlobalState, useGlobalState } from '../store'
import Identicon from 'react-identicons'
import { truncate } from '../store'
import { getMessages, sendMessage, listenForMessage } from '../services/chat'
import { useState, useEffect } from 'react'
const ChatModal = ({ movie }) => {
const [chatModal] = useGlobalState('chatModal')
const [message, setMessage] = useState('')
const [messages] = useGlobalState('messages')
const onSendMessage = async (e) => {
e.preventDefault()
if (!message) return
await sendMessage('guid_' + movie.id, message).then((msg) => {
setGlobalState('messages', (prevState) => [...prevState, msg])
setMessage('')
scrollToEnd()
})
}
useEffect(() => {
const fetchData = async () => {
await getMessages('guid_' + movie.id).then((msgs) => {
setGlobalState('messages', msgs)
scrollToEnd()
})
await listenForMessage('guid_' + movie.id).then((msg) => {
setGlobalState('messages', (prevState) => [...prevState, msg])
scrollToEnd()
})
}
fetchData()
}, [])
const scrollToEnd = () => {
const elmnt = document.getElementById('messages-container')
elmnt.scrollTop = elmnt.scrollHeight
}
return (
<div
className={`fixed -top-4 left-0 w-screen h-screen flex items-center justify-center bg-black bg-opacity-50 transform z-50 transition-transform duration-300 ${chatModal}`}
>
<div className="bg-slate-200 shadow-lg shadow-slate-900 rounded-xl w-11/12 md:w-3/5 h-[30rem] p-6 relative">
<div className="flex justify-between items-center">
<h2 className="capitalize">{movie.name}: Chat Room</h2>
<FaTimes
className="cursor-pointer"
onClick={() => setGlobalState('chatModal', 'scale-0')}
/>
</div>
<div
id="messages-container"
className="overflow-y-scroll overflow-x-hidden h-[20rem] scroll-bar mt-5 px-4 py-3 bg-gray-300 rounded-md"
>
<div className="w-11/12">
{messages.map((msg, i) => (
<Message message={msg.text} uid={msg.sender.uid} key={i} />
))}
</div>
</div>
<form className="h-[4rem] w-full mt-4" onSubmit={onSendMessage}>
<input
value={message}
onChange={(e) => setMessage(e.target.value)}
className="h-full w-full p-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>
</div>
)
}
const Message = ({ message, uid }) => {
return (
<div className="flex justify-start items-center space-x-3 space-y-3">
<div className="flex items-center space-x-2">
<Identicon string={uid} size={15} className="rounded-full" />
<p className="font-semibold text-sm">{truncate(uid, 4, 4, 11)}</p>
</div>
<p className="text-xs">{message}</p>
</div>
)
}
export default ChatModal
view raw ChatModal.jsx hosted with ❤ by GitHub

The Timeslots Components

Timeslot Table and Actions Components

The TimeSlotTable component displays a table of time slots. It receives an array of slots as props and maps over them to generate table rows. Each row represents a time slot and displays information such as ID, day, ticket cost, balance, start time, end time, capacity, and actions. The convertTimestampToDate and convertTimestampToTime functions from the store are used to format the timestamp values. The actions column includes the TimeslotActions component.

The TimeslotActions component is a dropdown menu of actions available for each time slot. It provides options to delete a slot, mark a slot as completed, and view all ticket holders. The actions trigger corresponding functions like handleDelete and handleCompletion. The component uses icons from various libraries such as react-icons/tfi, react-icons/bs, and react-icons/bi. Toast notifications from react-toastify are used to display the status of the transaction. The menu is implemented using the Menu component from @headlessui/react, and the Link component is used for navigation.

Use the codes below for creating the both components.

import { Menu } from '@headlessui/react'
import { TfiTicket } from 'react-icons/tfi'
import { BsFileEarmarkCheck, BsTrash3 } from 'react-icons/bs'
import { BiDotsVerticalRounded } from 'react-icons/bi'
import { setGlobalState } from '../store'
import { Link } from 'react-router-dom'
import { toast } from 'react-toastify'
import { markSlot } from '../services/blockchain'
const TimeslotActions = ({ slot }) => {
const handleDelete = () => {
setGlobalState('slot', slot)
setGlobalState('deleteSlotModal', 'scale-100')
}
const handleCompletion = async () => {
await toast.promise(
new Promise(async (resolve, reject) => {
await markSlot(slot)
.then(() => resolve())
.catch((error) => {
console.log(error)
reject(error)
})
}),
{
pending: 'Approve transaction...',
success: 'Marked as completed 👌',
error: 'Encountered error 🤯',
}
)
}
return (
<Menu as="div" className="inline-block text-left">
<Menu.Button
className="inline-flex w-full justify-center
rounded-md bg-black bg-opacity-10 px-4 py-2 text-sm
font-medium text-black hover:bg-opacity-30 focus:outline-none
focus-visible:ring-2 focus-visible:ring-white
focus-visible:ring-opacity-75"
>
<BiDotsVerticalRounded size={17} />
</Menu.Button>
<Menu.Items
className="absolute right-0 mt-2 w-56 origin-top-right
divide-y divide-gray-100 rounded-md bg-white shadow-md
ing-1 ring-black ring-opacity-5 focus:outline-none"
>
{!slot.completed && (
<Menu.Item>
{({ active }) => (
<button
className={`flex justify-start items-center space-x-1 ${
active ? 'bg-red-500 text-white' : 'text-red-500'
} group flex w-full items-center rounded-md px-2 py-2 text-sm`}
onClick={handleDelete}
>
<BsTrash3 size={17} />
<span>Delete</span>
</button>
)}
</Menu.Item>
)}
{!slot.completed && slot.seats > 0 && (
<Menu.Item>
{({ active }) => (
<button
className={`flex justify-start items-center space-x-1 ${
active ? 'bg-gray-200 text-black' : 'text-gray-900'
} group flex w-full items-center rounded-md px-2 py-2 text-sm`}
onClick={handleCompletion}
>
<BsFileEarmarkCheck size={17} />
<span>Finish up</span>
</button>
)}
</Menu.Item>
)}
<Menu.Item>
{({ active }) => (
<Link
className={`flex justify-start items-center space-x-1 ${
active ? 'bg-gray-200 text-black' : 'text-gray-900'
} group flex w-full items-center rounded-md px-2 py-2 text-sm`}
to={`/timeslot/${slot.movieId}/${slot.id}`}
>
<TfiTicket size={17} />
<span>All Holders</span>
</Link>
)}
</Menu.Item>
</Menu.Items>
</Menu>
)
}
export default TimeslotActions
import React from 'react'
import { convertTimestampToDate, convertTimestampToTime } from '../store'
import TimeslotActions from './TimeslotActions'
import { Link } from 'react-router-dom'
const TimeSlotTable = ({ slots }) => {
return (
<div className="overflow-x-auto">
<table className="min-w-full bg-white">
<thead>
<tr className="border-b">
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Id
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Day
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Ticket
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Balance
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Starts
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Ends
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Capacity
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Actions
</th>
</tr>
</thead>
<tbody>
{slots.map((slot, i) => (
<tr key={i} className="border-b">
<td className="px-6 py-4 whitespace-nowrap">{i + 1}</td>
<td className="px-6 py-4 whitespace-nowrap text-blue-500">
<Link to={`/timeslot/${slot.movieId}/${slot.id}`}>
{convertTimestampToDate(slot.day)}
</Link>
</td>
<td className="px-6 py-4 whitespace-nowrap">
{slot.ticketCost} ETH
</td>
<td className="px-6 py-4 whitespace-nowrap">
{slot.balance} ETH
</td>
<td className="px-6 py-4 whitespace-nowrap">
{convertTimestampToTime(slot.startTime)}
</td>
<td className="px-6 py-4 whitespace-nowrap">
{convertTimestampToTime(slot.endTime)}
</td>
<td className="px-6 py-4 whitespace-nowrap">
{slot.seats} / {slot.capacity}
</td>
<td className="px-6 py-4 whitespace-nowrap">
<TimeslotActions slot={slot} />
</td>
</tr>
))}
</tbody>
</table>
</div>
)
}
export default TimeSlotTable

Timeslot Lits Component

The Timeslot List

The TimeSlotList component displays a list of available time slots, allowing users to purchase tickets. It uses the slots array as props to populate the list items with relevant information such as date, time, seat availability, ticket cost, and a "Buy Ticket" button. The component handles ticket purchases through the handleTicketPurchase function and displays transaction status using react-toastify notifications. The component also utilizes the FaEthereum icon from react-icons/fa and applies Tailwind CSS classes for styling.
Use the code below.

import React from 'react'
import { convertTimestampToDate, convertTimestampToTime } from '../store'
import { FaEthereum } from 'react-icons/fa'
import { toast } from 'react-toastify'
import { buyTicket } from '../services/blockchain'
const TimeSlotList = ({ slots }) => {
const handleTicketPurchase = async (slot) => {
await toast.promise(
new Promise(async (resolve, reject) => {
await buyTicket(slot)
.then((res) => resolve(res))
.catch((error) => {
console.log(error)
reject(error)
})
}),
{
pending: 'Approve transaction...',
success: 'Ticket successfully purchased 👌',
error: 'Encountered error 🤯',
}
)
}
return (
<div className="flex flex-col items-center mb-10 w-full sm:w-3/6 mx-auto">
<h2 className="text-2xl font-bold mb-2">Available Time Slots</h2>
{slots.length > 0 ? (
<ul className="space-y-4 w-full">
{slots.map((slot, i) => (
<li
key={i}
className="bg-gray-100 p-4 rounded-md w-full flex justify-between items-center"
>
<div>
<h3 className="text-lg font-semibold">
{convertTimestampToDate(slot.day)}
</h3>
<p className="text-sm font-medium">
{convertTimestampToTime(slot.startTime)} -{' '}
{convertTimestampToTime(slot.endTime)}
</p>
<div className="flex justify-center items-center space-x-1 text-sm font-light">
<span>
{slot.seats} / {slot.capacity} left
</span>
<span className="flex justify-center items-center">
<FaEthereum /> <span>{slot.ticketCost}</span>
</span>
</div>
</div>
<button
className="inline-block px-3 py-2 border-2 border-red-600 font-medium text-xs
leading-tight uppercase rounded-full hover:bg-opacity-5 focus:outline-none
focus:ring-0 transition duration-150 ease-in-out hover:bg-gradient-to-r
from-cyan-500 to-red-500 hover:text-white hover:border-white"
onClick={() => handleTicketPurchase(slot)}
>
Buy Ticket
</button>
</li>
))}
</ul>
) : (
<p className="text-gray-700 font-light">
Not available, check back later
</p>
)}
</div>
)
}
export default TimeSlotList

Delete Slot Component

The Delete Slot Component

The DeleteSlot component displays a modal to confirm the deletion of a time slot. It manages state using useGlobalState and setGlobalState, and imports icons from react-icons/ri and react-icons/fa. The modal shows a warning, confirms the selected slot's deletion, and has a "Delete" button. Deletion is handled by handleDelete, using deleteSlot from blockchain. Styling is done with Tailwind CSS classes. Overall, DeleteSlot provides a confirmation interface for deleting slots and handles related actions and state changes.
Use the code below:

import { RiErrorWarningFill } from 'react-icons/ri'
import {
useGlobalState,
setGlobalState,
convertTimestampToDate,
} from '../store'
import { FaTimes } from 'react-icons/fa'
import { deleteSlot } from '../services/blockchain'
import { toast } from 'react-toastify'
const DeleteSlot = () => {
const [deleteSlotModal] = useGlobalState('deleteSlotModal')
const [slot] = useGlobalState('slot')
const closeModal = () => {
setGlobalState('deleteSlotModal', 'scale-0')
}
const handleDelete = async () => {
await toast.promise(
new Promise(async (resolve, reject) => {
await deleteSlot(slot)
.then((res) => {
closeModal()
resolve(res)
})
.catch((error) => {
console.log(error)
reject(error)
})
}),
{
pending: 'Approve transaction...',
success: 'Slot deleted 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-50 transition-transform duration-300 ${deleteSlotModal}`}
>
<div className="bg-white shadow-lg shadow-slate-900 rounded-xl w-11/12 md:w-2/5 h-7/12 p-6">
<div className="flex flex-col">
<div className="flex flex-row justify-between items-center">
<p className="font-semibold">Delete Time Slot</p>
<button
type="button"
className="border-0 bg-transparent focus:outline-none"
onClick={closeModal}
>
<FaTimes className="text-gray-400" />
</button>
</div>
<div className="flex flex-col justify-center items-center rounded-xl mt-5 mb-5">
<RiErrorWarningFill className="text-6xl text-red-700 " />
<p className="text-center p-2">
Are you sure you want to delete slot for <br />
<span className="font-semibold">
{convertTimestampToDate(slot?.day)}
</span>
</p>
</div>
<button
onClick={handleDelete}
className="flex flex-row justify-center items-center w-full text-white text-md
bg-red-500 py-2 px-5 rounded-full drop-shadow-xl border border-transparent
hover:bg-transparent hover:border-red-500 hover:text-red-500 focus:outline-none mt-5"
>
Delete
</button>
</div>
</div>
</div>
)
}
export default DeleteSlot
view raw DeleteSlot.jsx hosted with ❤ by GitHub

Ticket Holders Component

The Ticket Holder Component

The TicketHoldersTable component renders a table that displays ticket holders' information. It receives the holders and slot props. The table structure consists of three columns: "Id," "Cost," and "Holder." The holders array is mapped to generate table rows, with each row displaying the corresponding holder's information. The "Id" column displays the index of the holder plus one, the "Cost" column shows the ticket cost from the slot prop, and the "Holder" column displays the holder's information. The component utilizes Tailwind CSS classes for styling and provides an organized representation of the ticket holders' data. See the codes below.

import React from 'react'
const TicketHoldersTable = ({ holders, slot }) => {
return (
<div className="overflow-x-auto">
<table className="min-w-full bg-white">
<thead>
<tr className="border-b">
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Id
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Cost
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Holder
</th>
</tr>
</thead>
<tbody>
{holders.map((holder, i) => (
<tr key={i} className="border-b">
<td className="px-6 py-4 whitespace-nowrap">{i + 1}</td>
<td className="px-6 py-4 whitespace-nowrap">{slot.ticketCost} ETH</td>
<td className="px-6 py-4 whitespace-nowrap">{holder}</td>
</tr>
))}
</tbody>
</table>
</div>
)
}
export default TicketHoldersTable

Find Holders Component

The Find Ticket Holder

Lastly for the components is the FindHolder component, which displays a modal window for searching ticket holders by their Ethereum address. It uses hooks and global state variables to manage the search functionality. The modal includes an input field for entering the address, and the handleSearch function updates the results. Matching addresses are shown along with the count. The component provides a user-friendly interface for searching ticket holders within a modal window. See the code below.

import { useState } from 'react'
import { useGlobalState, setGlobalState } from '../store'
import { FaTimes } from 'react-icons/fa'
import { TbSearch } from 'react-icons/tb'
const FindHolder = () => {
const [holderSearchModal] = useGlobalState('holderSearchModal')
const [holders] = useGlobalState('ticketHolders')
const [address, setAddress] = useState('')
const [addresses, setAddresses] = useState([])
const closeModal = () => {
setGlobalState('holderSearchModal', 'scale-0')
}
const handleSearch = (characters) => {
const sanitizedCharacters = characters.trim().toLowerCase()
setAddress(sanitizedCharacters)
setAddresses([])
if (sanitizedCharacters !== '') {
for (let i = 0; i < holders.length; i++) {
const walletAddress = holders[i].toLowerCase()
if (walletAddress.trim() !== '') {
const regex = /^0x[a-fA-F0-9]{40}$/ // Regex pattern for Ethereum address
if (
regex.test(walletAddress) &&
walletAddress.includes(sanitizedCharacters)
) {
setAddresses((prevState) => [walletAddress, ...prevState])
}
}
}
}
}
return (
<div
className={`fixed top-0 left-0 w-screen h-screen flex items-center justify-center
bg-black bg-opacity-50 transform z-50 transition-transform duration-300 ${holderSearchModal}`}
>
<div className="bg-white shadow-lg shadow-slate-900 rounded-xl w-11/12 md:w-2/5 h-7/12 p-6">
<div className="flex flex-col">
<div className="flex flex-row justify-between items-center">
<p className="font-semibold">Find Holder</p>
<button
type="button"
className="border-0 bg-transparent focus:outline-none"
onClick={closeModal}
>
<FaTimes className="text-gray-400" />
</button>
</div>
<div className="flex flex-col justify-center items-center rounded-xl mt-5 mb-5">
<div
className="flex border border-gray-200 text-gray-500 p-2
items-center rounded-full w-full"
>
<TbSearch size={20} className="hidden md:flex" />
<input
onChange={(e) => handleSearch(e.target.value)}
value={address}
placeholder="Search holders ETH address"
className="border-none flex-1 text-m px-2 outline-none"
/>
</div>
</div>
{addresses.length > 0 && (
<div className="flex flex-col justify-center items-center rounded-xl mb-5">
<div className="text-green-800 font-medium">
Found {addresses.length} match(es)
</div>
<div className="mt-2">
{addresses.map((address, i) => (
<pre key={i}>{address}</pre>
))}
</div>
</div>
)}
</div>
</div>
</div>
)
}
export default FindHolder
view raw FindHolder.jsx hosted with ❤ by GitHub

Want to learn how to build an NFT marketplace? Watch this video tutorial:

This video provides a step-by-step guide on how to build an NFT marketplace using React and Solidity. The video covers all of the essential concepts and techniques you need to know to build an NFT marketplace.

Now that we have covered all the components in this application, it’s time we couple the various pages together. Let’s start with the homepage.

To begin developing the pages of our application, we will create a new directory called pages in the “src” directory. This folder will contain all the pages required for our project. For each of the pages listed below, you need to create the corresponding files within the src/pages folder.

The List Movies Page

The List Movies Page

The ListMovies page component displays a list of movies. It utilizes the useGlobalState hook to retrieve the movie data from the global state. The component includes a Banner component at the top and a grid layout for displaying the movies using the MovieCard component. If there are movies available, they are rendered in the grid layout. Otherwise, a message indicating that there are no movies yet is displayed. The ListMovies component provides a visually appealing way to present a collection of movies. Use the code below.

import { useGlobalState } from '../store'
import MovieCard from '../components/MovieCard'
import Banner from '../components/Banner'
const ListMovies = () => {
const [movies] = useGlobalState('movies')
return (
<div className="flex flex-col w-full p-4">
<Banner />
<div>
<div className="flex flex-col">
<h2 className="text-xl font-semibold p-4">Movies</h2>
{movies.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-3 gap-4">
{movies.map((movie, i) => (
<MovieCard key={i} movie={movie} />
))}
</div>
) : (
<div className="mt-10">No movies yet</div>
)}
</div>
</div>
</div>
)
}
export default ListMovies
view raw ListMovies.jsx hosted with ❤ by GitHub

Movie Details Page

Specific Movie Page

The **MovieDetails** page component is responsible for displaying detailed information about a specific movie. It utilizes various hooks and services to fetch and manage the movie data, time slots, and chat group (CometChat SDK). The component retrieves the movie ID from the URL parameters using **useParams** from React Router.

Upon rendering, the component fetches the movie details and available time slots by calling the **getMovie** and **getSlots** functions from the blockchain service. It also retrieves the chat group associated with the movie using the **getGroup** function. The component manages the loading state with the **loaded** and **setLoaded** state variables.

Once the data is loaded, the component displays the movie image, name, genre, description, and chat actions. It also includes the **TimeSlotList** component, which filters and displays the available time slots based on the **isValidTimeslot** function. See the code below.

import { useEffect, useState } from 'react'
import { getMovie, getSlots } from '../services/blockchain'
import { useGlobalState } from '../store'
import { useParams } from 'react-router-dom'
import ChatModal from '../components/ChatModal'
import { getGroup } from '../services/chat'
import ChatActions from '../components/ChatActions'
import TimeSlotList from '../components/TimeSlotList'
const MovieDetails = () => {
const [loaded, setLoaded] = useState(false)
const [group, setGroup] = useState(null)
const [movie] = useGlobalState('movie')
const [slots] = useGlobalState('slotsForDay')
const { id } = useParams()
useEffect(() => {
const fetchData = async () => {
await getMovie(id)
await getSlots(id)
setLoaded(true)
const GROUP = await getGroup(`guid_${id}`)
setGroup(GROUP)
}
fetchData()
}, [])
const isValidTimeslot = (slot) => {
const currentDate = new Date()
currentDate.setHours(0, 0, 0, 0)
const currentTimestamp = currentDate.getTime()
if (
currentTimestamp <= slot.day &&
!slot.completed &&
slot.seats < slot.capacity
) {
return true
} else {
return false
}
}
return (
loaded && (
<div className="flex flex-col w-full p-4 space-y-4">
<div className="flex w-full ">
<img src={movie.imageUrl} className="w-full object-cover h-[30rem]" />
</div>
<div className="flex flex-col space-y-4 align-center text-center w-full">
<div className="flex flex-col space-y-6">
<h3 className="font-black text-2xl">{movie.name}</h3>
<div className="flex space-x-2 my-2 justify-center">
{movie.genre.split(',').map((genre, i) => (
<span
key={i}
className="inline-block px-4 py-2 rounded-full bg-cyan-500 text-white
text-sm font-medium shadow-md transition-colors duration-300
hover:bg-cyan-600 cursor-pointer"
>
{genre}
</span>
))}
</div>
<p className="text-gray-700 my-5 w-5/6 text-center mx-auto font-light">
{movie.description}
</p>
<ChatActions movie={movie} group={group} />
<TimeSlotList
slots={slots.filter((slot) => isValidTimeslot(slot))}
/>
</div>
</div>
<ChatModal movie={movie} />
</div>
)
)
}
export default MovieDetails

Manage Movies Page

The Manage Movies Component

The ManageMovies page allows users to manage movies. It displays a table of movies using the MoviesTable component. Users can add a new movie by clicking the "Add Movie" button, which navigates them to the "/add/movies" route. The page also provides a "Withdraw" button that triggers a withdrawal action and displays the current balance using the balance state. The withdrawal action is implemented using the setGlobalState function and opens the withdrawal modal. The page layout is responsive and utilizes spacing and styling to enhance the user interface. See the code below.

import { setGlobalState, useGlobalState } from '../store'
import MoviesTable from '../components/MoviesTable'
import { Link } from 'react-router-dom'
import { FaEthereum } from 'react-icons/fa'
const ManageMovies = () => {
const [movies] = useGlobalState('movies')
const [balance] = useGlobalState('balance')
return (
<div className="w-full min-h-[89vh] p-3 space-y-6">
<h3 className="my-3 text-3xl font-bold">Manage Movies</h3>
<MoviesTable movies={movies} />
<div className="flex space-x-2">
<Link
to="/add/movies"
className="bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded"
>
Add Movie
</Link>
<button
className="bg-blue-500 hover:bg-blue-700 text-white
font-bold py-2 px-4 rounded flex justify-center items-center"
onClick={() => setGlobalState('withdrwalModal', 'scale-100')}
>
<span>Withdraw</span>
<FaEthereum />
<span>{balance}</span>
</button>
</div>
</div>
)
}
export default ManageMovies

Add Movie Page

The Add Movie Page

The AddMovie page allows users to add a new movie. It provides a form where users can input the movie's image URL, name, genre, and description. Upon submission, the handleSubmit function is called, which performs input validation and calls the addMovie function from the blockchain service to add the movie.

The form submission process displays a toast notification to indicate the status of the transaction, including pending, success, or error messages. If all the required fields are filled, the movie is added successfully, and the user is navigated back to the home page ("/").

The page layout is centered and responsive, and the form input fields are styled using Tailwind classes to ensure a consistent and user-friendly interface. See the codes below.

import { useState } from 'react'
import { addMovie } from '../services/blockchain'
import { toast } from 'react-toastify'
import { useNavigate } from 'react-router-dom'
const Addmovie = () => {
const navigate = useNavigate()
const [movie, setMovie] = useState({
imageUrl: '',
name: '',
genre: '',
description: '',
})
const handleChange = (e) => {
const { name, value } = e.target
setMovie((prevMovie) => ({
...prevMovie,
[name]: value,
}))
}
const handleSubmit = async (e) => {
e.preventDefault()
if (!movie.imageUrl || !movie.name || !movie.genre || !movie.description)
return
await toast.promise(
new Promise(async (resolve, reject) => {
await addMovie(movie)
.then((tx) => {
navigate('/')
resolve(tx)
})
.catch((error) => reject(error))
}),
{
pending: 'Approve transaction...',
success: 'Movie created successfully 👌',
error: 'Encountered error 🤯',
}
)
}
return (
<div className="flex justify-center items-center py-12 m-auto w-full ">
<div className="block rounded-lg justify-center items-center m-auto shadow-lg shadow-gray-400 w-3/5">
<form className="p-6" onSubmit={handleSubmit}>
<div className="flex items-center justify-center mb-4">
<h2>Add Movies</h2>
</div>
<div className="mb-6 w-full">
<input
type="url"
className="form-control block w-full px-3 py-1.5 text-base font-normal text-gray-700
bg-white bg-clip-padding rounded-lg outline-none lg:w-full focus:border-red-700
focus:outline-none focus:ring-0"
placeholder="Image URL"
name="imageUrl"
value={movie.imageUrl}
onChange={handleChange}
/>
</div>
<div className="mb-6">
<input
type="text"
className="form-control block w-full px-3 py-1.5 text-base font-normal text-gray-700
bg-white bg-clip-padding rounded-lg outline-none lg:w-full focus:border-red-700
focus:outline-none focus:ring-0"
placeholder="Movie Name"
name="name"
value={movie.name}
onChange={handleChange}
/>
</div>
<div className="mb-6">
<input
type="text"
className="form-control block w-full px-3 py-1.5 text-base font-normal text-gray-700
bg-white bg-clip-padding rounded-lg outline-none lg:w-full focus:border-red-700
focus:outline-none focus:ring-0"
placeholder="e.g. hilarious, action, comedy..."
name="genre"
value={movie.genre}
onChange={handleChange}
/>
</div>
<div className="mb-6">
<textarea
className="form-control block w-full h-32 resize-none px-3 py-1.5 text-base font-normal text-gray-700
bg-white bg-clip-padding rounded-lg outline-none lg:w-full focus:border-red-700
focus:outline-none focus:ring-0"
placeholder="Movie Description"
name="description"
value={movie.description}
onChange={handleChange}
></textarea>
</div>
<div className="flex justify-end">
<button
type="submit"
className="w-42 px-6 py-2.5 bg-transparent text-black font-medium text-xs leading-tight focus:ring-0 focus:outline-none uppercase rounded-full shadow-md border-2 border-red-700 hover:bg-gradient-to-r from-cyan-500 to-red-500 hover:text-white hover:border-white"
>
Add Movie
</button>
</div>
</form>
</div>
</div>
)
}
export default Addmovie
view raw Addmovie.jsx hosted with ❤ by GitHub

Add Slot Page

The Add Slot Page

The AddTimeslot page allows users to add a time slot for a movie. It uses React for building the user interface. The component includes form inputs for selecting a day, start time, end time, ticket cost, and capacity.

The selected day is stored in the selectedDay state variable. When a day is selected, it triggers a call to getSlotsByDay function to fetch existing time slots for that day.

The form is submitted when all required fields are filled, and the data is stored in separate state variables for ticket costs, start times, end times, capacities, and viewing days.
There are functions to handle form submission, saving the time slot to the blockchain, resetting the form, and removing selected slots.

The component utilizes external libraries such as react-datepicker for date and time picking, react-router-dom for navigation, and react-toastify for displaying transaction status. See the codes below.

import React, { useEffect, useState } from 'react'
import DatePicker from 'react-datepicker'
import 'react-datepicker/dist/react-datepicker.css'
import { useParams, useNavigate } from 'react-router-dom'
import { toast } from 'react-toastify'
import { addTimeslot, getSlotsByDay, toWei } from '../services/blockchain'
import { convertTimestampToTime, useGlobalState } from '../store'
import { FaTimes } from 'react-icons/fa'
const AddTimeslot = () => {
const [currentSlots] = useGlobalState('currentSlots')
const [ticketCost, setTicketCost] = useState('')
const [startTime, setStartTime] = useState(null)
const [endTime, setEndTime] = useState(null)
const [capacity, setCapacity] = useState('')
const [selectedDay, setSelectedDay] = useState(null)
const [blockedStamps, setBlockedStamps] = useState([])
const [ticketCosts, setTicketCosts] = useState([])
const [startTimes, setStartTimes] = useState([])
const [endTimes, setEndTimes] = useState([])
const [capacities, setCapacities] = useState([])
const [viewingDays, setViewingDays] = useState([])
const { id } = useParams()
const router = useNavigate()
const timeInterval = 30
const handleSelectedDay = async (date) => {
const day = new Date(date)
const options = { year: 'numeric', month: '2-digit', day: '2-digit' }
const newDate = new Date(
`${day.toLocaleDateString('en-US', options).replace(/\//g, '-')}`
).getTime()
setSelectedDay(newDate)
}
useEffect(() => {
const fetchData = async () => {
await getSlotsByDay(selectedDay)
initAvailableSlot()
}
if (selectedDay) fetchData()
}, [selectedDay])
useEffect(() => {
if (currentSlots.length > 0) {
initAvailableSlot()
}
}, [currentSlots])
const dateMax = () => {
const startOfDay = new Date(selectedDay)
startOfDay.setHours(0, 0, 0, 0)
const minStartTime =
startOfDay.toLocaleDateString() === new Date().toLocaleDateString()
? new Date()
: startOfDay
const maxStartTime = new Date(selectedDay).setHours(23, 59, 59, 999)
const minEndTime = new Date(startTime)
const maxEndTime = new Date(selectedDay).setHours(23, 59, 59, 999)
return { startOfDay, minStartTime, maxStartTime, maxEndTime, minEndTime }
}
const initAvailableSlot = () => {
const timestamps = []
currentSlots.forEach((slot) => {
const { startTime, endTime } = slot
let currTime = new Date(startTime)
while (currTime < endTime) {
timestamps.push(currTime.getTime())
currTime.setMinutes(currTime.getMinutes() + 10)
}
})
setBlockedStamps(timestamps)
}
const handleSubmit = (e) => {
e.preventDefault()
if (!selectedDay || !startTime || !endTime || !capacity || !ticketCost)
return
setTicketCosts((prev) => [toWei(ticketCost), ...prev])
setStartTimes((prev) => [new Date(startTime).getTime(), ...prev])
setEndTimes((prev) => [new Date(endTime).getTime(), ...prev])
setCapacities((prev) => [Number(capacity), ...prev])
setViewingDays((prev) => [selectedDay, ...prev])
resetForm()
}
const saveMovieSlot = async () => {
if (
viewingDays.length < 1 ||
startTimes.length < 1 ||
endTimes.length < 1 ||
capacities.length < 1 ||
ticketCosts.length < 1
)
return
await toast.promise(
new Promise(async (resolve, reject) => {
const params = {
movieId: id,
ticketCosts,
startTimes,
endTimes,
capacities,
viewingDays,
}
await addTimeslot(params)
.then((res) => {
setTicketCosts([])
setStartTimes([])
setEndTimes([])
setCapacities([])
setViewingDays([])
router('/timeslot/' + id)
resolve(res)
})
.catch((error) => reject(error))
}),
{
pending: 'Approve transaction...',
success: 'Time slot added successfully 👌',
error: 'Encountered error 🤯',
}
)
}
const resetForm = () => {
setSelectedDay(null)
setStartTime(null)
setEndTime(null)
setTicketCost('')
setCapacity('')
}
const removeSlot = (index) => {
ticketCosts.splice(index, 1)
startTimes.splice(index, 1)
endTimes.splice(index, 1)
capacities.splice(index, 1)
viewingDays.splice(index, 1)
setTicketCosts((prevState) => [...prevState])
setStartTimes((prevState) => [...prevState])
setEndTimes((prevState) => [...prevState])
setCapacities((prevState) => [...prevState])
setViewingDays((prevState) => [...prevState])
}
return (
<div className="flex justify-center items-center py-12 m-auto w-full">
<div className="block rounded-lg justify-center items-center m-auto sm:shadow-md p-6 shadow-gray-400 w-full sm:w-3/5">
<form onSubmit={handleSubmit} className="flex flex-col">
<div className="flex items-center justify-center mb-4">
<h2>Add Time Slot</h2>
</div>
<div
className="flex flex-row justify-between items-center
bg-gray-300 rounded-xl mt-5 p-2"
>
<DatePicker
selected={selectedDay}
onChange={(date) => handleSelectedDay(date)}
dateFormat="dd/MM/yyyy"
placeholderText="Select Day..."
minDate={Date.now()}
className="block w-full text-sm text-slate-500 bg-transparent
border-0 focus:outline-none focus:ring-0"
/>
</div>
<div className="flex flex-row justify-between items-center bg-gray-300 rounded-xl mt-5 p-2">
<DatePicker
selected={startTime}
onChange={setStartTime}
showTimeSelect
showTimeSelectOnly
minDate={new Date(selectedDay)}
maxDate={new Date(selectedDay)}
minTime={dateMax().minStartTime}
maxTime={dateMax().maxStartTime}
timeCaption="Start Time"
excludeTimes={blockedStamps}
timeIntervals={timeInterval}
dateFormat="h:mm aa"
placeholderText="Select start time..."
className="block w-full text-sm text-slate-500 bg-transparent
border-0 focus:outline-none focus:ring-0"
/>
</div>
<div className="flex flex-row justify-between items-center bg-gray-300 rounded-xl mt-5 p-2">
<DatePicker
selected={endTime}
onChange={setEndTime}
showTimeSelect
showTimeSelectOnly
timeFormat="p"
timeIntervals={timeInterval}
excludeTimes={blockedStamps}
minDate={new Date(selectedDay)}
maxDate={new Date(selectedDay)}
minTime={dateMax().minEndTime}
maxTime={dateMax().maxEndTime}
timeCaption="End Time"
dateFormat="h:mm aa"
placeholderText="Select end time..."
className="block w-full text-sm text-slate-500 bg-transparent
border-0 focus:outline-none focus:ring-0"
/>
</div>
<div className="flex flex-row justify-between items-center bg-gray-300 rounded-xl mt-5 p-2">
<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="cost"
placeholder="Ticket Cost e.g. 0.02 ETH"
value={ticketCost}
onChange={(e) => setTicketCost(e.target.value)}
/>
</div>
<div className="flex flex-row justify-between items-center bg-gray-300 rounded-xl mt-5 p-2">
<input
className="block w-full text-sm text-slate-500 bg-transparent
border-0 focus:outline-none focus:ring-0"
type="number"
name="capacity"
placeholder="Capacity e.g. 20"
value={capacity}
onChange={(e) => setCapacity(e.target.value)}
/>
</div>
{startTimes.length > 0 && (
<div className="flex flex-wrap justify-start items-center mt-5 space-x-2 text-xs">
{startTimes.slice(0, 2).map((time, i) => (
<span
key={i}
className="flex space-x-1 px-2 py-1 mt-1 font-semibold text-gray-700 bg-gray-200 rounded-full"
>
<span>
{convertTimestampToTime(time)} -{' '}
{convertTimestampToTime(endTimes[i])}
</span>
<button onClick={() => removeSlot(i)}>
<FaTimes />
</button>
</span>
))}
{startTimes.length - startTimes.slice(0, 2).length > 0 && (
<span
className="flex items-center justify-center px-2 py-1
font-semibold text-gray-700 bg-gray-200 rounded-full
hover:bg-gray-300 mt-1"
>
+{startTimes.length - startTimes.slice(0, 2).length}
</span>
)}
</div>
)}
<div className="flex justify-between items-center mt-6">
<button
type="submit"
className="bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded"
>
Add Day
</button>
<button
onClick={saveMovieSlot}
type="button"
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
>
Submit({ticketCosts.length})
</button>
</div>
</form>
</div>
</div>
)
}
export default AddTimeslot
view raw AddTimeslot.jsx hosted with ❤ by GitHub

All Time slots Page

The Timeslot Page

The **TimeSlots** component is a page that displays the time slots for a specific movie. It retrieves the movie ID from the URL parameters using **useParams** and fetches the movie details and corresponding time slots from the blockchain using **getMovie** and **getSlots** functions.

The fetched movie information is stored in the **movie** state variable, while the time slots are stored in the **slots** state variable using the global state management system.
The component renders a heading displaying the movie name and a **TimeSlotTable** component to present the time slots in a table format.

Two links are provided: "Add Slots" redirects to a page where new time slots can be added for the movie, while "Back" navigates to the movie management page. Use the code below.

import React, { useEffect } from 'react'
import { Link, useParams } from 'react-router-dom'
import { getMovie, getSlots } from '../services/blockchain'
import { useGlobalState } from '../store'
import TimeSlotTable from '../components/TimeSlotTable'
const TimeSlots = () => {
const { id } = useParams()
const [movie] = useGlobalState('movie')
const [slots] = useGlobalState('slotsForDay')
useEffect(() => {
const fetchData = async () => {
await getMovie(id)
await getSlots(id)
}
fetchData()
}, [])
return (
<div className="w-full min-h-screen p-3 space-y-6 mb-10">
<h3 className="my-3 text-3xl font-bold">
Timeslots: <span className="text-gray-500">{movie?.name}</span>
</h3>
<TimeSlotTable slots={slots} />
<div className="flex space-x-2">
<Link
to={'/timeslot/add/' + id}
className="bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded"
>
Add Slots
</Link>
<Link
to={'/manage/movies'}
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
>
Back
</Link>
</div>
</div>
)
}
export default TimeSlots
view raw TimeSlots.jsx hosted with ❤ by GitHub

Ticket Holders Page

The Ticket Holders Page

Lastly, the **TicketHolders** component is a page that displays the ticket holders for a specific time slot of a movie. It retrieves the slot and movie details from the blockchain, renders a heading with the movie name and slot date, and presents the ticket holders in a table format.

The component includes a "Find Holder" button to search for a specific ticket holder and a "Back" link to navigate back to the time slots page. It uses React, **react-router-dom**, and global state management. Use the code below.

import React, { useEffect } from 'react'
import { Link, useParams } from 'react-router-dom'
import { getMovie, getSlot, getTicketHolders } from '../services/blockchain'
import {
convertTimestampToDate,
setGlobalState,
useGlobalState,
} from '../store'
import TicketHoldersTable from '../components/TicketHoldersTable'
const TicketHolders = () => {
const { slotId, movieId } = useParams()
const [slot] = useGlobalState('slot')
const [movie] = useGlobalState('movie')
const [holders] = useGlobalState('ticketHolders')
useEffect(() => {
const fetchData = async () => {
await getSlot(slotId)
await getMovie(movieId)
await getTicketHolders(movieId, slotId)
}
fetchData()
}, [])
return (
<div className="w-full min-h-screen p-3 space-y-6 mb-10">
<h3 className="my-3 text-3xl font-bold">
{movie?.name}:{' '}
<span className="text-gray-500">
{convertTimestampToDate(slot?.day)}
</span>
</h3>
<TicketHoldersTable holders={holders} slot={slot} />
<div className="flex space-x-2">
<button
onClick={() => setGlobalState('holderSearchModal', 'scale-100')}
className="bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded"
>
Find Holder
</button>
<Link
to={'/timeslot/' + movieId}
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
>
Back
</Link>
</div>
</div>
)
}
export default TicketHolders

Fantastic, we have so far worked on all the components and pages of this project, let’s couple them together with their respective services. Let’s start with the App component.

App Entry File

The **App** component serves as the main entry point for the Dapp Cinemas project. It sets up the application's routing using **react-router-dom** and includes various pages and components to manage movies, time slots, ticket holders, and other functionality.

The component also initializes the connection to the blockchain, loads blockchain data, and checks the authentication state. It renders a layout with a sidebar, header, and content area. The content area displays different pages based on the current route, such as listing movies, movie details, time slots, adding time slots, managing movies, etc.

Additionally, it includes several auxiliary components for updating movies, deleting movies and time slots, finding ticket holders, and making withdrawals. The component also utilizes the **react-toastify** library for displaying toast notifications.

Replace the existing App.jsx component with the code below.

import { useEffect } from 'react'
import { Routes, Route } from 'react-router-dom'
import { isWalletConnected, loadBlockchainData } from './services/blockchain'
import Sidebar from './components/Sidebar'
import Header from './components/Header'
import Addmovie from './pages/Addmovie'
import ListMovies from './pages/ListMovies'
import MovieDetails from './pages/MovieDetails'
import ManageMovies from './pages/ManageMovies'
import { ToastContainer } from 'react-toastify'
import UpdateMovie from './components/UpdateMovie'
import DeleteMovie from './components/DeleteMovie'
import { checkAuthState } from './services/chat'
import TimeSlots from './pages/TimeSlots'
import AddTimeslot from './pages/AddTimeslot'
import DeleteSlot from './components/DeleteSlot'
import TicketHolders from './pages/TicketHolders'
import FindHolder from './components/FindHolder'
import Withdrawal from './components/Withdrawal'
const App = () => {
useEffect(() => {
const fetchData = async () => {
await isWalletConnected()
await loadBlockchainData()
await checkAuthState()
}
fetchData()
}, [])
return (
<div className="h-screen flex">
<Sidebar />
<div className="flex-1 overflow-auto">
<Header />
<Routes>
<Route path="/" element={<ListMovies />} />
<Route path="/movie/:id" element={<MovieDetails />} />
<Route path="/timeslot/:id" element={<TimeSlots />} />
<Route path="/timeslot/add/:id" element={<AddTimeslot />} />
<Route
path="/timeslot/:movieId/:slotId"
element={<TicketHolders />}
/>
<Route path="/add/movies" element={<Addmovie />} />
<Route path="/manage/movies" element={<ManageMovies />} />
</Routes>
</div>
<UpdateMovie />
<DeleteMovie />
<DeleteSlot />
<FindHolder />
<Withdrawal />
<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

The Blockchain Service

The provided service is responsible for interacting with the Ethereum blockchain and managing the Dapp Cinemas smart contract. It allows the application to perform various actions such as adding movies, updating movie details, deleting movies, adding time slots, deleting time slots, buying tickets, and handling withdrawals.

The service utilizes the Ethereum provider (**Metamask**) to connect to the user's wallet and perform transactions using the **ethers.js** library. It includes helper functions for converting between Wei and Ether units and for structuring movie and timeslot data.

All the components and pages in the application leverage this service to interact with the blockchain and retrieve/update movie and timeslot information. They call the appropriate functions from this service to perform the desired actions, ensuring the application's functionality and data synchronization with the blockchain.

To utilize this code, create a folder called “services” in the “src” directory of the project and another file called “blockchain.js” within this folder. Now copy and paste the code below inside of it.

import abi from '../abis/src/contracts/DappCinemas.sol/DappCinemas.json'
import address from '../abis/contractAddress.json'
import { setGlobalState } from '../store'
import { ethers } from 'ethers'
import { logOutWithCometChat } from './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 getEthereumContract = async () => {
const accounts = await ethereum.request({ method: 'eth_accounts' })
const provider = accounts[0]
? new ethers.providers.Web3Provider(ethereum)
: new ethers.providers.JsonRpcProvider(process.env.REACT_APP_RPC_URL)
const wallet = accounts[0] ? null : ethers.Wallet.createRandom()
const signer = provider.getSigner(accounts[0] ? undefined : wallet.address)
const contract = new ethers.Contract(ContractAddress, ContractAbi, signer)
return contract
}
const isWalletConnected = async () => {
try {
if (!ethereum) return alert('Please install Metamask')
const accounts = await ethereum.request({ method: 'eth_accounts' })
if (accounts.length) {
setGlobalState('connectedAccount', accounts[0])
} else {
reportError('Please connect wallet.')
console.log('No accounts found.')
}
window.ethereum.on('chainChanged', (chainId) => {
window.location.reload()
})
window.ethereum.on('accountsChanged', async () => {
setGlobalState('connectedAccount', accounts[0])
await isWalletConnected()
await logOutWithCometChat()
})
if (accounts.length) {
setGlobalState('connectedAccount', accounts[0])
} else {
setGlobalState('connectedAccount', '')
console.log('No accounts found')
}
} 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 addMovie = async ({ name, imageUrl, genre, description }) => {
if (!ethereum) return alert('Please install metamask')
return new Promise(async (resolve, reject) => {
try {
const contract = await getEthereumContract()
tx = await contract.addMovie(name, imageUrl, genre, description)
await tx.wait()
await loadBlockchainData()
resolve(tx)
} catch (error) {
reportError(error)
reject(error)
}
})
}
const updateMovie = async ({ id, name, imageUrl, genre, description }) => {
if (!ethereum) return alert('Please install metamask')
return new Promise(async (resolve, reject) => {
try {
const contract = await getEthereumContract()
tx = await contract.updateMovie(id, name, imageUrl, genre, description)
await tx.wait()
await loadBlockchainData()
resolve(tx)
} catch (error) {
reportError(error)
reject(error)
}
})
}
const deleteMovie = async (movieId) => {
if (!ethereum) return alert('Please install metamask')
return new Promise(async (resolve, reject) => {
try {
const contract = await getEthereumContract()
tx = await contract.deleteMovie(movieId)
await tx.wait()
await loadBlockchainData()
resolve(tx)
} catch (error) {
reportError(error)
reject(error)
}
})
}
const addTimeslot = async ({
movieId,
ticketCosts,
startTimes,
endTimes,
capacities,
viewingDays,
}) => {
if (!ethereum) return alert('Please install metamask')
return new Promise(async (resolve, reject) => {
try {
const contract = await getEthereumContract()
tx = await contract.addTimeslot(
movieId,
ticketCosts,
startTimes,
endTimes,
capacities,
viewingDays
)
await tx.wait()
resolve(tx)
} catch (error) {
reportError(error)
reject(error)
}
})
}
const deleteSlot = async ({ movieId, id }) => {
if (!ethereum) return alert('Please install metamask')
return new Promise(async (resolve, reject) => {
try {
const contract = await getEthereumContract()
tx = await contract.deleteTimeSlot(movieId, id)
await tx.wait()
await getSlots(movieId)
resolve(tx)
} catch (error) {
reportError(error)
reject(error)
}
})
}
const markSlot = async ({ movieId, id }) => {
if (!ethereum) return alert('Please install metamask')
return new Promise(async (resolve, reject) => {
try {
const contract = await getEthereumContract()
tx = await contract.markTimeSlot(movieId, id)
await tx.wait()
await getSlots(movieId)
resolve(tx)
} catch (error) {
reportError(error)
reject(error)
}
})
}
const buyTicket = async ({ movieId, id, ticketCost }) => {
if (!ethereum) return alert('Please install metamask')
return new Promise(async (resolve, reject) => {
try {
const contract = await getEthereumContract()
tx = await contract.buyTicket(movieId, id, 1, {
value: toWei(ticketCost),
})
await tx.wait()
await getSlots(movieId)
resolve(tx)
} catch (error) {
reportError(error)
reject(error)
}
})
}
const withdraw = async ({ account, amount }) => {
if (!ethereum) return alert('Please install metamask')
return new Promise(async (resolve, reject) => {
try {
const contract = await getEthereumContract()
tx = await contract.withdrawTo(account, toWei(amount))
await tx.wait()
await getData()
resolve(tx)
} catch (error) {
reportError(error)
reject(error)
}
})
}
const getMovies = async () => {
if (!ethereum) return alert('Please install metamask')
try {
const contract = await getEthereumContract()
const movies = await contract.getMovies()
setGlobalState('movies', structuredMovie(movies))
} catch (err) {
reportError(err)
}
}
const getMovie = async (id) => {
if (!ethereum) return alert('Please install metamask')
try {
const contract = await getEthereumContract()
const movie = await contract.getMovie(id)
setGlobalState('movie', structuredMovie([movie])[0])
} catch (err) {
reportError(err)
}
}
const getSlots = async (movieId) => {
if (!ethereum) return alert('Please install metamask')
try {
const contract = await getEthereumContract()
const slots = await contract.getTimeSlots(movieId)
setGlobalState('slotsForDay', structuredTimeslot(slots))
} catch (err) {
reportError(err)
}
}
const getSlotsByDay = async (day) => {
if (!ethereum) return alert('Please install metamask')
try {
const contract = await getEthereumContract()
const slots = await contract.getTimeSlotsByDay(day)
setGlobalState('currentSlots', structuredTimeslot(slots))
} catch (err) {
reportError(err)
}
}
const getSlot = async (slotId) => {
if (!ethereum) return alert('Please install metamask')
try {
const contract = await getEthereumContract()
const slot = await contract.getTimeSlot(slotId)
setGlobalState('slot', structuredTimeslot([slot])[0])
} catch (err) {
reportError(err)
}
}
const getTicketHolders = async (movieId, id) => {
if (!ethereum) return alert('Please install metamask')
try {
const contract = await getEthereumContract()
const ticketHolders = await contract.getMovieTicketHolders(movieId, id)
setGlobalState('ticketHolders', ticketHolders)
} catch (error) {
reportError(error)
}
}
const getData = async () => {
if (!ethereum) return alert('Please install metamask')
try {
const contract = await getEthereumContract()
const deployer = await contract.owner()
const balance = await contract.balance()
setGlobalState('deployer', deployer.toLowerCase())
setGlobalState('balance', fromWei(balance))
} catch (err) {
reportError(err)
}
}
const loadBlockchainData = async () => {
await getMovies()
await getData()
}
const reportError = (error) => {
console.log(error)
}
const structuredMovie = (movies) =>
movies
.map((movie) => ({
id: Number(movie.id),
name: movie.name,
imageUrl: movie.imageUrl,
genre: movie.genre,
description: movie.description,
timestamp: Number(movie.timestamp),
deleted: movie.deleted,
}))
.sort((a, b) => b.timestamp - a.timestamp)
const structuredTimeslot = (slots) =>
slots
.map((slot) => ({
id: Number(slot.id),
movieId: Number(slot.movieId),
ticketCost: fromWei(slot.ticketCost),
startTime: Number(slot.startTime),
endTime: Number(slot.endTime),
capacity: Number(slot.capacity),
seats: Number(slot.seats),
deleted: slot.deleted,
completed: slot.completed,
day: Number(slot.day),
balance: fromWei(slot.balance),
}))
.sort((a, b) => {
if (a.day !== b.day) return a.day - b.day
return a.startTime - b.startTime
})
export {
getEthereumContract,
loadBlockchainData,
isWalletConnected,
getTicketHolders,
getSlotsByDay,
connectWallet,
deleteMovie,
addTimeslot,
updateMovie,
deleteSlot,
buyTicket,
withdraw,
addMovie,
getMovie,
getSlots,
markSlot,
getSlot,
toWei,
}
view raw blockchain.js hosted with ❤ by GitHub

Please ensure that you update the environment variables to look like this:

REACT_APP_COMETCHAT_APP_ID=****************
REACT_APP_COMETCHAT_AUTH_KEY=******************************
REACT_APP_COMETCHAT_REGION=**
REACT_APP_RPC_URL=http://127.0.0.1:8545
Enter fullscreen mode Exit fullscreen mode

The Chat Service

The chat service provided uses the CometChat Pro SDK to enable chat functionality in the application. It includes functions to initialize CometChat, handle user authentication, interact with chat groups, fetch and send messages, and listen for incoming messages.

The **initCometChat** function initializes CometChat with the app ID and region. User authentication is managed through the **loginWithCometChat** and **signUpWithCometChat** functions. The **logOutWithCometChat** function logs the user out and updates the global state.

Group management is handled by functions such as **createNewGroup**, **getGroup**, and **joinGroup**. Message-related operations are performed using functions like **getMessages**, **sendMessage**, and **listenForMessage**.

All components and pages in the application rely on these functions to leverage the chat service's capabilities, including user authentication, group interactions, and message handling.

Still inside the services folder, create a file named “chat.jsx”, copy and paste the code below into it.

import { CometChat } from '@cometchat-pro/chat'
import { setGlobalState } 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) => console.log(error))
}
const loginWithCometChat = async (UID) => {
const authKey = CONSTANTS.Auth_Key
return new Promise(async (resolve, reject) => {
await CometChat.login(UID, authKey)
.then((user) => resolve(user))
.catch((error) => reject(error))
})
}
const signUpWithCometChat = async (UID) => {
const authKey = CONSTANTS.Auth_Key
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(() => {
setGlobalState('currentUser', null)
resolve()
})
.catch(() => reject())
})
}
const checkAuthState = async () => {
return new Promise(async (resolve, reject) => {
await CometChat.getLoggedinUser()
.then((user) => {
setGlobalState('currentUser', user)
resolve(user)
})
.catch((error) => reject(error))
})
}
const createNewGroup = async (GUID, groupName) => {
const groupType = CometChat.GROUP_TYPE.PUBLIC
const password = ''
const group = new CometChat.Group(GUID, groupName, groupType, password)
return new Promise(async (resolve, reject) => {
await CometChat.createGroup(group)
.then((group) => resolve(group))
.catch((error) => reject(error))
})
}
const getGroup = async (GUID) => {
return new Promise(async (resolve, reject) => {
await CometChat.getGroup(GUID)
.then((group) => resolve(group))
.catch((error) => reject(error))
})
}
const joinGroup = async (GUID) => {
const groupType = CometChat.GROUP_TYPE.PUBLIC
const password = ''
return new Promise(async (resolve, reject) => {
await CometChat.joinGroup(GUID, groupType, password)
.then((group) => resolve(group))
.catch((error) => reject(error))
})
}
const getMessages = async (UID) => {
const limit = 30
const messagesRequest = new CometChat.MessagesRequestBuilder()
.setGUID(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.GROUP
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 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,
checkAuthState,
createNewGroup,
getGroup,
joinGroup,
listenForMessage,
}
view raw chat.jsx hosted with ❤ by GitHub

Great, now let’s work on the store file that serves as a state management library.

The Store File

The store service provided below is responsible for managing global state in the application using the [**react-hooks-global-state**](https://www.npmjs.com/package/react-hooks-global-state) library. It includes functions and variables for setting, retrieving, and using global state values. The initial global state includes properties such as **movies**, **messages**, **balance**, **currentSlots**, **ticketHolders**, and more.

Additionally, the service provides utility functions like **truncate** to truncate text based on a specified length, **convertTimestampToDate** to convert a timestamp to a formatted date, and **convertTimestampToTime** to convert a timestamp to a formatted time.

The functions **setGlobalState** and **useGlobalState** are used to set and access global state values, respectively, while **getGlobalState** retrieves the current global state.

The components and pages in the application leverage this store service to manage and share data across different parts of the application, ensuring synchronized state management and access to shared data.

Create a folder called “store” in the “src” directory of this project and another file called “**index.jsx**” inside of it. Now copy and paste the code below inside of it.

import { createGlobalState } from 'react-hooks-global-state'
const { setGlobalState, useGlobalState, getGlobalState } = createGlobalState({
movies: [],
movie: null,
messages: [],
balance: 0,
currentSlots: [],
slotsForDay: [],
slotsForMovie: [],
ticketHolders: [],
connectedAccount: '',
slot: null,
deployer: null,
withdrwalModal: 'scale-0',
updateMovieModal: 'scale-0',
deleteMovieModal: 'scale-0',
deleteSlotModal: 'scale-0',
slotsModal: 'scale-0',
addSlotModal: 'scale-0',
holderSearchModal: 'scale-0',
currentUser: null,
chatModal: 'scale-0',
chatCommandModal: 'scale-0',
authChatModal: 'scale-0',
group: null,
})
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
}
const convertTimestampToDate = (timestamp) => {
const date = new Date(timestamp)
const options = { month: 'long', day: 'numeric', year: 'numeric' }
return date.toLocaleDateString('en-US', options)
}
const convertTimestampToTime = (timestamp) => {
const date = new Date(timestamp)
let hours = date.getHours()
const minutes = date.getMinutes()
const amPm = hours >= 12 ? 'PM' : 'AM'
hours = hours % 12 || 12
const formattedTime = `${hours.toString().padStart(2, '0')}:${minutes
.toString()
.padStart(2, '0')} ${amPm}`
return formattedTime
}
export {
setGlobalState,
useGlobalState,
getGlobalState,
truncate,
convertTimestampToDate,
convertTimestampToTime,
}
view raw index.jsx hosted with ❤ by GitHub

The Index file

The **index.jsx** file serves as the entry point of the application. It initializes the CometChat service, sets up dependencies, and renders the React application using the **App** component within a **BrowserRouter**. It creates a root element for rendering and sets up the necessary configurations for the application to start running.

Replace the code below inside of the “**index.jsx**” file in the src folder of this project.

import React from 'react'
import ReactDOM from 'react-dom/client'
import { BrowserRouter } from 'react-router-dom'
import './index.css'
import 'react-toastify/dist/ReactToastify.css'
import App from './App'
import { initCometChat } from './services/chat'
const root = ReactDOM.createRoot(document.getElementById('root'))
initCometChat().then(() => {
root.render(
<React.StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>
)
})
view raw index.jsx hosted with ❤ by GitHub

The Static Assets

Create a folder called “assets” in the “src” directory and put the image below inside of it.

Banner Image

Now execute **yarn start** to have you application running on the browser.

Congratulations on building a web3 cinema ticket booking Dapp that features real-time chat communication with the CometChat SDK! If you're looking for a powerful and versatile chat SDK that can be used to add chat functionality to any application, I encourage you to try out CometChat. CometChat offers a wide range of chat features, including 1-on-1 chat, group chat, file sharing, and more. It's also very easy to integrate with other platforms, making it a great choice for developers of all skill levels.

Here is a link to the CometChat website where you can learn more about the SDK and how to get started.

Conclusion

Dapp Cinemas is a decentralized application that uses blockchain technology to revolutionize the movie industry. The project offers a comprehensive solution for movie management, showtime scheduling, ticket booking, and revenue tracking.

The smart contracts on the Ethereum blockchain ensure seamless and transparent operations. The front-end interface provides an intuitive user experience, and the real-time chat functionality enhances user engagement. Comprehensive testing ensures the reliability and correctness of the smart contracts.

Dapp Cinemas demonstrates the potential of blockchain technology in revolutionizing the movie industry, providing benefits such as increased transparency, secure transactions, and efficient revenue management.

If you're interested in learning more about web3 development, be sure to subscribe to our YouTube channel and check out website. We have a wide range of tutorials, courses, and books that can help you get started.

Till next time, all the best!

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
Twitter: Follow
LinkedIn: Connect
GitHub: Explore
Website: Visit

Top comments (5)

Collapse
 
femi_akinyemi profile image
Femi Akinyemi

Thanks For Sharing

Collapse
 
daltonic profile image
Gospel Darlington

You're welcome

Collapse
 
sanjayojha profile image
Sanjay Ojha

Saved. Love the detailed explanation.

Collapse
 
daltonic profile image
Gospel Darlington

Thanks man

Collapse
 
siy profile image
Sergiy Yevtushenko

You might find interesting to take a look at Scrypto.

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more