DEV Community

Cover image for How to Build an Exciting Blockchain Voting System with React, Solidity, and CometChat
Gospel Darlington
Gospel Darlington

Posted on

7

How to Build an Exciting Blockchain Voting System with React, Solidity, and CometChat

What you will be building, see the demo on the Goerli test network and git repo here.

Blockchain Voting System

Introduction

Now, it's time for you to learn the ultimate trick in building a decentralized voting system. In this tutorial, you will learn how to make a blockchain voting application featuring the use of solidity’s Smart Contracts, React frontend designed with Tailwind CSS, and CometChat SDK.

By the way, Subscribe to my YouTube channel to learn how to build a Web3 app from scratch. I also offer private and specialized classes for serious folks who want to learn one-on-one from a mentor. Book your Web3 classes here.

If you are ready to crush this build, then let’s get started.

Prerequisite

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

  • NodeJs (Super important)
  • EthersJs
  • Hardhat
  • React
  • Tailwind CSS
  • CometChat SDK
  • Metamask
  • Yarn

Installing Dependencies

Have the starter kit below cloned using the command below:

git clone https://github.com/Daltonic/tailwind_ethers_starter_kit <PROJECT_NAME>
cd <PROJECT_NAME>
Enter fullscreen mode Exit fullscreen mode

Now, open the project in VS Code or on your preferred code editor. Locate the package.json file and update it with the codes below.

{
"name": "BlueVotes",
"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"
},
"dependencies": {
"@cometchat-pro/chat": "^3.0.10",
"@nomiclabs/hardhat-ethers": "^2.1.0",
"@nomiclabs/hardhat-waffle": "^2.0.3",
"ethereum-waffle": "^3.4.4",
"ethers": "^5.6.9",
"hardhat": "^2.10.1",
"ipfs-http-client": "^57.0.3",
"moment": "^2.29.4",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-hooks-global-state": "^1.0.2",
"react-icons": "^4.3.1",
"react-identicons": "^1.2.5",
"react-moment": "^1.1.2",
"react-router-dom": "6",
"react-scripts": "5.0.0",
"react-toastify": "^9.1.1",
"web-vitals": "^2.1.4"
},
"devDependencies": {
"@openzeppelin/contracts": "^4.5.0",
"@tailwindcss/forms": "0.4.0",
"assert": "^2.0.0",
"autoprefixer": "10.4.2",
"babel-polyfill": "^6.26.0",
"babel-preset-env": "^1.7.0",
"babel-preset-es2015": "^6.24.1",
"babel-preset-stage-2": "^6.24.1",
"babel-preset-stage-3": "^6.24.1",
"babel-register": "^6.26.0",
"buffer": "^6.0.3",
"chai": "^4.3.6",
"chai-as-promised": "^7.1.1",
"crypto-browserify": "^3.12.0",
"dotenv": "^16.0.0",
"https-browserify": "^1.0.0",
"mnemonics": "^1.1.3",
"os-browserify": "^0.3.0",
"postcss": "8.4.5",
"process": "^0.11.10",
"react-app-rewired": "^2.1.11",
"stream-browserify": "^3.0.0",
"stream-http": "^3.2.0",
"tailwindcss": "3.0.18",
"url": "^0.11.0"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}
view raw package.json hosted with ❤ by GitHub

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

Configuring CometChat SDK

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

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

Register a new CometChat account if you do not have one

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

Log in to the CometChat Dashboard with your created account

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

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_COMET_CHAT_REGION=**
REACT_APP_COMET_CHAT_APP_ID=**************
REACT_APP_COMET_CHAT_AUTH_KEY=******************************
Enter fullscreen mode Exit fullscreen mode

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

Configuring Alchemy App

STEP 1: Head to Alchemy.

STEP 2: Create an account.

Login to Alchemey

STEP 3:
From the dashboard create a new project.

Creating a Project

STEP 4:
Copy the Goerli test network WebSocket or HTTPS endpoint URL to your .env file.

Goerli Testnet Key

After that, enter the private key of your preferred Metamask account to the DEPLOYER_KEY in your environment variables and save. If you followed the instructions correctly, your environment variables should now look like this.

ENDPOINT_URL=***************************
DEPLOYER_KEY=**********************

REACT_APP_COMET_CHAT_REGION=**
REACT_APP_COMET_CHAT_APP_ID=**************
REACT_APP_COMET_CHAT_AUTH_KEY=******************************
Enter fullscreen mode Exit fullscreen mode

See the section below if you don't know how to access your private key.

Extracting Your Metamask Private Key

STEP 1:
Make sure Goerli is selected as the test network in your Metamask browser extension, Rinkeby and the older test nets have now been depreciated.

Next, on the preferred account, click the vertical dotted line and choose account details. Please see the image below.

Step One

STEP 2:
Enter your password on the field provided and click the confirm button, this will enable you to access your account private key.

Step Two

STEP 3:
Click on "export private key" to see your private key. Make sure you never expose your keys on a public page such as Github. That is why we are appending it as an environment variable.

Step Three

STEP 4:
Copy your private key to your .env file. See the image and code snippet below:

Step Four

ENDPOINT_URL=***************************
DEPLOYER_KEY=**********************

REACT_APP_COMET_CHAT_REGION=**
REACT_APP_COMET_CHAT_APP_ID=**************
REACT_APP_COMET_CHAT_AUTH_KEY=******************************
Enter fullscreen mode Exit fullscreen mode

Configuring the Hardhat script

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

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

The above script instructs hardhat on these three important rules.

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

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

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

Configuring the Deployment Script

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

const { ethers } = require('hardhat')
const fs = require('fs')
async function main() {
const Contract = await ethers.getContractFactory('BlueVotes')
const contract = await Contract.deploy()
await contract.deployed()
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

When run as a Hardhat command, the above script will deploy the **BlueVotes.sol** smart contract to any network of your choice.

Following the above instructions, open a terminal pointing to this project and run the commands listed below separately on two terminals. You can do this directly from your editor in VS Code. Look at the command below.

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

If the preceding commands were successfully executed, you should see the following activity on your terminal. Please see the image below.

Activities of Deployment on the Terminal

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

Deploy Fullstack DApp

The Blockchain Service File

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

Create a new file called **BlueVotes.sol** within this contracts folder; this file will contain all of the logic that govern the smart contract's activities. Copy, paste, and save the following codes into the **BlueVotes.sol** file. See the complete code below.

//SPDX-License-Identifier: MIT
pragma solidity >=0.7.0 <0.9.0;
contract BlueVotes {
struct PollStruct {
uint id;
string image;
string title;
string description;
uint votes;
uint contestants;
bool deleted;
address director;
uint startsAt;
uint endsAt;
uint timestamp;
}
struct VoterStruct {
uint id;
string image;
string fullname;
address voter;
uint votes;
address[] voters;
}
uint totalPolls;
uint totalUsers;
PollStruct[] polls;
mapping(address => VoterStruct) public users;
mapping(uint => mapping(address => bool)) voted;
mapping(uint => mapping(address => bool)) contested;
mapping(uint => VoterStruct[]) contestantsIn;
mapping(uint => bool) pollExist;
event Voted (
string fullname,
address indexed voter,
uint timestamp
);
modifier userOnly() {
require(users[msg.sender].voter == msg.sender, "You've gotta register first");
_;
}
function createPoll(
string memory image,
string memory title,
string memory description,
uint startsAt,
uint endsAt
) public userOnly {
require(bytes(title).length > 0, "Title cannot be empty");
require(bytes(description).length > 0, "Description cannot be empty");
require(bytes(image).length > 0, "Image URL cannot be empty");
require(startsAt > 0 && endsAt > startsAt, "End date must be greater than start date");
PollStruct memory poll;
poll.id = totalPolls++;
poll.title = title;
poll.description = description;
poll.image = image;
poll.startsAt = startsAt;
poll.endsAt = endsAt;
poll.director = msg.sender;
poll.timestamp = block.timestamp;
polls.push(poll);
pollExist[poll.id] = true;
}
function updatePoll(
uint id,
string memory image,
string memory title,
string memory description,
uint startsAt,
uint endsAt
) public userOnly {
require(pollExist[id], "Poll not found");
require(polls[id].director == msg.sender, "Unauthorized entity");
require(bytes(title).length > 0, "Title cannot be empty");
require(bytes(description).length > 0, "Description cannot be empty");
require(bytes(image).length > 0, "Image URL cannot be empty");
require(!polls[id].deleted, "Polling already started");
require(endsAt > startsAt, "End date must be greater than start date");
polls[id].title = title;
polls[id].description = description;
polls[id].startsAt = startsAt;
polls[id].endsAt = endsAt;
polls[id].image = image;
}
function deletePoll(uint id) public userOnly {
require(pollExist[id], "Poll not found");
require(polls[id].director == msg.sender, "Unauthorized entity");
polls[id].deleted = true;
}
function getPoll(uint id) public view returns (PollStruct memory) {
return polls[id];
}
function getPolls() public view returns (PollStruct[] memory) {
return polls;
}
function register(
string memory image,
string memory fullname
) public {
VoterStruct memory user;
user.id = totalUsers++;
user.image = image;
user.fullname = fullname;
user.voter = msg.sender;
users[msg.sender] = user;
}
function contest(uint id) public userOnly {
require(pollExist[id], "Poll not found");
require(!contested[id][msg.sender], "Already contested");
VoterStruct memory user = users[msg.sender];
contestantsIn[id].push(user);
contested[id][msg.sender] = true;
polls[id].contestants++;
}
function listContestants(uint id) public view returns (VoterStruct[] memory) {
require(pollExist[id], "Poll not found");
return contestantsIn[id];
}
function vote(uint id, uint cid) public userOnly {
require(pollExist[id], "Poll not found");
require(!voted[id][msg.sender], "Already voted");
require(!polls[id].deleted, "Polling already started");
require(polls[id].endsAt > polls[id].startsAt, "End date must be greater than start date");
polls[id].votes++;
contestantsIn[id][cid].votes++;
contestantsIn[id][cid].voters.push(msg.sender);
voted[id][msg.sender] = true;
emit Voted (
users[msg.sender].fullname,
msg.sender,
block.timestamp
);
}
}
view raw BlueVotes.sol hosted with ❤ by GitHub

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

Structs

  • PollStruct: This describes the content of each poll created in our platform.
  • VoterStruct: This models the information of a voter, user, or contestant on the platform.

State Variables

  • TotalPolls: This keeps track of the number of polls created on the smart contract.
  • TotalUsers: This carries the total number of users registered on the platform.

Mappings

  • Users: This maps users' addresses to their respective data using the VoterStruct.
  • Voted: This keeps track of the voting status of each user on different polls.
  • Contested: This tells if a contestant has or has not contested for a particular poll.
  • ContestantsIn: This holds the data for every contestant in a given poll.
  • PollExist: This checks if a specific poll Id exists or not on the platform.

Events and Modifiers

  • Voted: This emits information about the current user who voted.
  • UserOnly: This modifier prevents unregistered users from accessing unauthorized functions.

Poll Functions

  • CreatePoll: This takes data about a poll from a registered user and creates a poll after validating that the information meets standards.
  • UpdatePoll: This function modifies the content of a specific poll, given that the caller is the poll creator and the poll Id exists.
  • DeletePoll: This function enables the poll creator to toggle the deleted key to true, thereby making the poll unavailable for circulation.
  • GetPolls: This returns all the polls created by every user on the platform.
  • GetPoll: This returns information about a specific poll from our platform.

User Oriented Functions

  • Register: This function enables a user to sign up with his full name and image avatar.
  • Contest: This function gives a registered user the chance to become a contestant on a given poll provided that the poll has not started.
  • ListContestants: This function lists out all the contestants who contested for a particular poll.
  • Vote: This function enables a user to vote for one contestant per poll within the period stipulated for voting.

Do you want to improve your knowledge of smart contracts? Watch this video to learn how to use Hardhat for smart contract test-driven development.

Learn test-driven development

Developing the Frontend

Now that we have our smart contract on the network and all of our artifacts (bytecodes and ABI) generated, let's take a step-by-step approach to build the front end with React.

Components

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

Header component

Header Component

This component clearly displays only two pieces of information, the website's logo, and the connected account button. See the codes below.

import { Link } from 'react-router-dom'
import { connectWallet } from '../Blockchain.services'
import { truncate, useGlobalState } from '../store'
const Header = () => {
const [connectedAccount] = useGlobalState('connectedAccount')
return (
<div className=" flex justify-between items-center p-5 shadow-md shadow-gray-300 ">
<Link to="/" className="font-bold text-2xl">
<span className="text-blue-700">Blue</span>Votes
</Link>
{connectedAccount ? (
<button
type="button"
className="inline-block px-6 py-2.5 bg-blue-600 text-white font-medium
text-xs leading-tight rounded shadow-md hover:bg-blue-700
hover:shadow-lg focus:bg-blue-700 focus:shadow-lg focus:outline-none
focus:ring-0 active:bg-blue-800 active:shadow-lg transition duration-150 ease-in-out"
>
{truncate(connectedAccount, 4, 4, 11)}
</button>
) : (
<button
type="button"
className="inline-block px-6 py-2.5 bg-blue-600 text-white font-medium
text-xs leading-tight rounded shadow-md hover:bg-blue-700
hover:shadow-lg focus:bg-blue-700 focus:shadow-lg focus:outline-none
focus:ring-0 active:bg-blue-800 active:shadow-lg transition duration-150 ease-in-out"
onClick={connectWallet}
>
Connect Wallet
</button>
)}
</div>
)
}
export default Header
view raw Header.jsx hosted with ❤ by GitHub

Hero Component

Hero Component

The hero component allows you to launch other components such as the registration and the poll creation components. This component also allows a user to log in to their account if already registered, using the CometChat SDK under the hood. See the codes below.

import { toast } from 'react-toastify'
import { loginWithCometChat } from '../Chat.services'
import { setGlobalState, useGlobalState } from '../store/index'
const Hero = () => {
const [user] = useGlobalState('user')
const [currentUser] = useGlobalState('currentUser')
const [connectedAccount] = useGlobalState('connectedAccount')
const handleSubmit = async () => {
await toast.promise(
new Promise(async (resolve, reject) => {
await loginWithCometChat()
.then(() => resolve())
.catch(() => reject())
}),
{
pending: 'Signing in...',
success: 'Logged in successful 👌',
error: 'Encountered error 🤯',
},
)
}
return (
<div className="text-center mt-10 p-4">
<h1 className="text-5xl text-black-600 font-bold">
{' '}
Vote Without <span className="text-blue-600">Rigging</span>
</h1>
<p className="pt-5 text-gray-600 text-xl font-medium">
{' '}
This online voting system offers the highest level of transparency,
control, security <br></br>and efficiency of election processes using
the <strong>Blockchain Technology</strong>{' '}
</p>
<div className="flex justify-center pt-10">
{user?.fullname ? (
<div className="space-x-2">
{!currentUser ? (
<button
type="button"
className="inline-block px-6 py-2.5 bg-transparent text-blue-600 font-medium text-xs
leading-tight uppercase rounded shadow-md hover:bg-blue-700 hover:shadow-lg hover:text-white
focus:bg-blue-700 focus:shadow-lg focus:outline-none focus:ring-0 active:bg-blue-800
active:shadow-lg transition duration-150 ease-in-out border border-blue-600"
onClick={handleSubmit}
>
Login
</button>
) : (
<button
type="button"
className="inline-block px-6 py-2.5 bg-blue-600 text-white font-medium text-xs
leading-tight uppercase rounded shadow-md hover:bg-blue-700 hover:shadow-lg
focus:bg-blue-700 focus:shadow-lg focus:outline-none focus:ring-0 active:bg-blue-800
active:shadow-lg transition duration-150 ease-in-out border border-blue-600"
onClick={() => setGlobalState('createPollModal', 'scale-100')}
>
Create Poll
</button>
)}
</div>
) : (
<button
type="button"
className="inline-block px-6 py-2 border-2 border-blue-600 text-blue-600 font-medium
text-xs leading-tight uppercase rounded hover:bg-black hover:bg-opacity-5 focus:outline-none
focus:ring-0 transition duration-150 ease-in-out"
onClick={() => setGlobalState('contestModal', 'scale-100')}
disabled={!connectedAccount}
title={!connectedAccount ? 'Please connect wallet first' : null}
>
Register
</button>
)}
</div>
</div>
)
}
export default Hero
view raw Hero.jsx hosted with ❤ by GitHub

Polls Component

Polls Component

This component grabs all active polls and lists them on our platform. Within this component also lies a single component responsible for rendering each specific poll. See the codes below.

import { useEffect, useState } from 'react'
import Moment from 'react-moment'
import { useNavigate } from 'react-router-dom'
import { truncate } from '../store'
const Polls = ({ polls }) => {
const [end, setEnd] = useState(4)
const [count] = useState(4)
const [collection, setCollection] = useState([])
const getCollection = () => {
return polls.slice(0, end)
}
useEffect(() => {
setCollection(getCollection())
}, [polls, end])
return (
<div className="pt-10">
<div
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3
xl:grid-cols-4 gap-6 md:gap-4 lg:gap-4 xl:gap-3 py-2.5 w-4/5
mx-auto"
>
{collection.map((poll, i) =>
poll?.deleted ? null : <Poll key={i} poll={poll} />,
)}
</div>
{collection.length > 0 && polls.length > collection.length ? (
<div className=" flex justify-center mt-20">
<button
type="button"
className=" inline-block px-6 py-2.5 bg-blue-600 text-white font-medium text-xs
leading-tight uppercase rounded shadow-md hover:bg-blue-700 hover:shadow-lg
focus:bg-blue-700 focus:shadow-lg focus:outline-none focus:ring-0 active:bg-blue-800
active:shadow-lg transition duration-150 ease-in-out"
onClick={() => setEnd(end + count)}
>
Load More
</button>
</div>
) : null}
</div>
)
}
const Poll = ({ poll }) => {
const navigate = useNavigate()
return (
<div className="flex justify-center">
<div className="rounded-lg shadow-lg bg-white max-w-sm">
<img
className="rounded-t-lg object-cover h-48 w-full"
src={poll.image}
alt={poll.title}
/>
<div className="p-6">
<h5 className="text-gray-900 text-xl font-medium">{poll.title}</h5>
<small className="font-bold mb-4 text-xs">
{new Date().getTime() > Number(poll.startsAt + '000') &&
Number(poll.endsAt + '000') > Number(poll.startsAt + '000') ? (
<span className="text-green-700">Started</span>
) : new Date().getTime() > Number(poll.endsAt + '000') ? (
<Moment className="text-red-700" unix format="ddd DD MMM, YYYY">
{poll.endsAt}
</Moment>
) : (
<Moment className="text-gray-500" unix format="ddd DD MMM, YYYY">
{poll.startsAt}
</Moment>
)}
</small>
<p className="text-gray-700 text-base mb-4">
{truncate(poll.description, 100, 0, 103)}
</p>
<button
type="button"
className=" inline-block px-6 py-2.5 bg-blue-600 text-white font-medium text-xs
leading-tight uppercase rounded shadow-md hover:bg-blue-700 hover:shadow-lg
focus:bg-blue-700 focus:shadow-lg focus:outline-none focus:ring-0 active:bg-blue-800
active:shadow-lg transition duration-150 ease-in-out"
onClick={() => navigate('/polls/' + poll.id)}
>
Enter
</button>
</div>
</div>
</div>
)
}
export default Polls
view raw Polls.jsx hosted with ❤ by GitHub

CreatePoll Component

Create Poll Component

This component is used for creating fresh new polls on our platform. It takes essential information like a poll title, description, image URL, and start and end date to create a poll. See the codes below.

import { setGlobalState, useGlobalState } from '../store'
import { FaTimes } from 'react-icons/fa'
import { useState } from 'react'
import { createPoll } from '../Blockchain.services'
import { toast } from 'react-toastify'
const CreatePoll = () => {
const [createPollModal] = useGlobalState('createPollModal')
const [title, setTitle] = useState('')
const [startsAt, setStartsAt] = useState('')
const [endsAt, setEndsAt] = useState('')
const [description, setDescription] = useState('')
const [image, setImage] = useState('')
const closeModal = () => {
setGlobalState('createPollModal', 'scale-0')
}
const toTimestamp = (strDate) => {
const datum = Date.parse(strDate)
return datum / 1000
}
const handleSubmit = async (e) => {
e.preventDefault()
if (!title || !image || !startsAt || !endsAt || !description) return
const params = {
title,
image,
startsAt: toTimestamp(startsAt),
endsAt: toTimestamp(endsAt),
description,
}
await toast.promise(
new Promise(async (resolve, reject) => {
await createPoll(params)
.then(() => resolve())
.catch(() => reject())
}),
{
pending: 'Approve transaction...',
success: 'Created, will reflect within 30sec 👌',
error: 'Encountered error 🤯',
},
)
closeModal()
resetForm()
}
const resetForm = () => {
setTitle('')
setImage('')
setDescription('')
setStartsAt('')
setEndsAt('')
}
return (
<div
className={`fixed top-0 left-0 w-screen h-screen flex items-center z-50
justify-center bg-black bg-opacity-50 transform transition-transform
duration-300 ${createPollModal}`}
>
<div className="bg-white shadow-xl shadow-black 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 text-black">Add New Poll</p>
<button
type="button"
onClick={closeModal}
className="border-0 bg-transparent focus:outline-none"
>
<FaTimes className="text-black" />
</button>
</div>
{image ? (
<div className="flex flex-row justify-center items-center rounded-xl mt-5">
<div className="shrink-0 rounded-xl overflow-hidden h-20 w-20">
<img
alt="Contestant"
className="h-full w-full object-cover cursor-pointer"
src={image}
/>
</div>
</div>
) : null}
<div className="flex flex-row justify-between items-center bg-gray-300 rounded-xl mt-5">
<input
className="block w-full text-sm
text-slate-500 bg-transparent border-0
focus:outline-none focus:ring-0"
type="text"
name="title"
placeholder="Title"
onChange={(e) => setTitle(e.target.value)}
value={title}
required
/>
</div>
<div className="flex flex-row justify-between items-center bg-gray-300 rounded-xl mt-5">
<input
className="block w-full text-sm
text-slate-500 bg-transparent border-0
focus:outline-none focus:ring-0"
type="date"
name="date"
placeholder="Date"
onChange={(e) => setStartsAt(e.target.value)}
value={startsAt}
required
/>
</div>
<div className="flex flex-row justify-between items-center bg-gray-300 rounded-xl mt-5">
<input
className="block w-full text-sm
text-slate-500 bg-transparent border-0
focus:outline-none focus:ring-0"
type="date"
name="date"
placeholder="Date"
onChange={(e) => setEndsAt(e.target.value)}
value={endsAt}
required
/>
</div>
<div className="flex flex-row justify-between items-center bg-gray-300 rounded-xl mt-5">
<input
className="block w-full text-sm
text-slate-500 bg-transparent border-0
focus:outline-none focus:ring-0"
type="url"
name="image"
placeholder="Image URL"
onChange={(e) => setImage(e.target.value)}
pattern="^(http(s)?:\/\/)+[\w\-\._~:\/?#[\]@!\$&'\(\)\*\+,;=.]+$"
value={image}
required
/>
</div>
<div className="flex flex-row justify-between items-center bg-gray-300 rounded-xl mt-5">
<textarea
className="block w-full text-sm resize-none
text-slate-500 bg-transparent border-0
focus:outline-none focus:ring-0 h-20"
type="text"
name="description"
placeholder="Description"
onChange={(e) => setDescription(e.target.value)}
value={description}
required
></textarea>
</div>
<button
type="submit"
className="flex flex-row justify-center items-center
w-full text-white text-md bg-blue-500
py-2 px-5 rounded-full drop-shadow-xl
border-transparent border
hover:bg-transparent hover:text-blue-500
hover:border hover:border-blue-500
focus:outline-none focus:ring mt-5"
>
Create Poll
</button>
</form>
</div>
</div>
)
}
export default CreatePoll
view raw CreatePoll.jsx hosted with ❤ by GitHub

Update Poll Component

Update Component

This component follows the same approach as the create poll component. See the codes below.

import { setGlobalState, useGlobalState, toDate } from '../store'
import { FaTimes } from 'react-icons/fa'
import { useEffect, useState } from 'react'
import { updatePoll } from '../Blockchain.services'
import { toast } from 'react-toastify'
const UpdatePoll = () => {
const [updatePollModal] = useGlobalState('updatePollModal')
const [poll] = useGlobalState('poll')
const [title, setTitle] = useState('')
const [startsAt, setStartsAt] = useState('')
const [endsAt, setEndsAt] = useState('')
const [description, setDescription] = useState('')
const [image, setImage] = useState('')
useEffect(() => {
setTitle(poll?.title)
setDescription(poll?.description)
setImage(poll?.image)
setStartsAt(toDate(poll?.startsAt.toNumber() * 1000))
setEndsAt(toDate(poll?.endsAt.toNumber() * 1000))
}, [poll])
const closeModal = () => {
setGlobalState('updatePollModal', 'scale-0')
}
const toTimestamp = (strDate) => {
const datum = Date.parse(strDate)
return datum / 1000
}
const handleSubmit = async (e) => {
e.preventDefault()
if (!title || !image || !startsAt || !endsAt || !description) return
const params = {
id: poll?.id,
title,
image,
startsAt: toTimestamp(startsAt),
endsAt: toTimestamp(endsAt),
description,
}
await toast.promise(
new Promise(async (resolve, reject) => {
await updatePoll(params)
.then(() => resolve())
.catch(() => reject())
}),
{
pending: 'Approve transaction...',
success: 'Updated, will reflect within 30sec 👌',
error: 'Encountered error 🤯',
},
)
closeModal()
}
return (
<div
className={`fixed top-0 left-0 w-screen h-screen flex items-center z-50
justify-center bg-black bg-opacity-50 transform transition-transform
duration-300 ${updatePollModal}`}
>
<div className="bg-white shadow-xl shadow-black 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 text-black">Edit Poll</p>
<button
type="button"
onClick={closeModal}
className="border-0 bg-transparent focus:outline-none"
>
<FaTimes className="text-black" />
</button>
</div>
{image ? (
<div className="flex flex-row justify-center items-center rounded-xl mt-5">
<div className="shrink-0 rounded-xl overflow-hidden h-20 w-20">
<img
alt="Contestant"
className="h-full w-full object-cover cursor-pointer"
src={image}
/>
</div>
</div>
) : null}
<div className="flex flex-row justify-between items-center bg-gray-300 rounded-xl mt-5">
<input
className="block w-full text-sm
text-slate-500 bg-transparent border-0
focus:outline-none focus:ring-0"
type="text"
name="title"
placeholder="Title"
onChange={(e) => setTitle(e.target.value)}
value={title || ''}
required
/>
</div>
<div className="flex flex-row justify-between items-center bg-gray-300 rounded-xl mt-5">
<input
className="block w-full text-sm
text-slate-500 bg-transparent border-0
focus:outline-none focus:ring-0"
type="date"
name="date"
placeholder="Date"
onChange={(e) => setStartsAt(e.target.value)}
value={startsAt || ''}
required
/>
</div>
<div className="flex flex-row justify-between items-center bg-gray-300 rounded-xl mt-5">
<input
className="block w-full text-sm
text-slate-500 bg-transparent border-0
focus:outline-none focus:ring-0"
type="date"
name="date"
placeholder="Date"
onChange={(e) => setEndsAt(e.target.value)}
value={endsAt || ''}
required
/>
</div>
<div className="flex flex-row justify-between items-center bg-gray-300 rounded-xl mt-5">
<input
className="block w-full text-sm
text-slate-500 bg-transparent border-0
focus:outline-none focus:ring-0"
type="url"
name="image"
placeholder="Image URL"
onChange={(e) => setImage(e.target.value)}
pattern="^(http(s)?:\/\/)+[\w\-\._~:\/?#[\]@!\$&'\(\)\*\+,;=.]+$"
value={image || ''}
required
/>
</div>
<div className="flex flex-row justify-between items-center bg-gray-300 rounded-xl mt-5">
<textarea
className="block w-full text-sm resize-none
text-slate-500 bg-transparent border-0
focus:outline-none focus:ring-0 h-20"
type="text"
name="description"
placeholder="Description"
onChange={(e) => setDescription(e.target.value)}
value={description || ''}
required
></textarea>
</div>
<button
type="submit"
className="flex flex-row justify-center items-center
w-full text-white text-md bg-blue-500
py-2 px-5 rounded-full drop-shadow-xl
border-transparent border
hover:bg-transparent hover:text-blue-500
hover:border hover:border-blue-500
focus:outline-none focus:ring mt-5"
>
Update Poll
</button>
</form>
</div>
</div>
)
}
export default UpdatePoll
view raw UpdatePoll.jsx hosted with ❤ by GitHub

DeletePoll Component

Delete Poll Component

This component simply allows you to delete an existing poll from being listed. After deletion, the poll will be removed from circulation. See the code below.

import { FaTimes } from 'react-icons/fa'
import { useNavigate } from 'react-router-dom'
import { toast } from 'react-toastify'
import { deletePoll } from '../Blockchain.services'
import { setGlobalState, useGlobalState } from '../store'
const DeletePoll = () => {
const navigate = useNavigate()
const [poll] = useGlobalState('poll')
const [deletePollModal] = useGlobalState('deletePollModal')
const handleSubmit = async (e) => {
e.preventDefault()
await toast.promise(
new Promise(async (resolve, reject) => {
await deletePoll(poll.id)
.then(() => resolve())
.catch(() => reject())
}),
{
pending: 'Approve transaction...',
success: 'Deleted, will reflect within 30sec 👌',
error: 'Encountered error 🤯',
},
)
setGlobalState('deletePollModal', 'scale-0')
console.log('Poll Deleted!')
navigate('/')
}
const closeModal = () => {
setGlobalState('deletePollModal', 'scale-0')
}
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 ${deletePollModal}`}
>
<div className="bg-white shadow-xl shadow-black rounded-xl w-11/12 md:w-2/5 h-7/12 p-6">
<form className="flex flex-col">
<div className="flex flex-row justify-between items-center">
<p className="font-semibold text-black">#{poll?.title}</p>
<button
type="button"
onClick={closeModal}
className="border-0 bg-transparent focus:outline-none"
>
<FaTimes className="text-black" />
</button>
</div>
<div className="flex flex-row justify-center items-center rounded-xl mt-5">
<div className="shrink-0 rounded-xl overflow-hidden h-20 w-20">
<img
alt="Project"
className="h-full w-full object-cover cursor-pointer"
src={poll?.image}
/>
</div>
</div>
<div className="flex flex-col justify-center items-center mt-5">
<p>Are you sure?</p>
<small className="text-red-400">This is irriversible!</small>
</div>
<button
type="submit"
onClick={handleSubmit}
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-transparent border
hover:bg-transparent hover:text-red-500
hover:border hover:border-red-500
focus:outline-none focus:ring mt-5"
>
Delete Poll
</button>
</form>
</div>
</div>
)
}
export default DeletePoll
view raw DeletePoll.jsx hosted with ❤ by GitHub

Messages Component

The Message Component

Using the CometChat SDK, this component is in charge of displaying a collection of group chat messages for each poll. Look at the code below.

import { useEffect, useState } from 'react'
import Identicon from 'react-identicons'
import { CometChat, getMessages, sendMessage } from '../Chat.services'
import { truncate, useGlobalState } from '../store'
const Messages = ({ guid }) => {
const [message, setMessage] = useState('')
const [messages, setMessages] = useState([])
const [connectedAccount] = useGlobalState('connectedAccount')
useEffect(() => {
getMessages(guid).then((msgs) => {
if (!!!msgs.code)
setMessages(msgs.filter((msg) => msg.category == 'message'))
})
listenForMessage(guid)
}, [guid])
const listenForMessage = (listenerID) => {
CometChat.addMessageListener(
listenerID,
new CometChat.MessageListener({
onTextMessageReceived: (message) => {
setMessages((prevState) => [...prevState, message])
scrollToEnd()
},
}),
)
}
const handleMessage = async (e) => {
e.preventDefault()
await sendMessage(guid, message).then((msg) => {
if (!!!msg.code) {
setMessages((prevState) => [...prevState, msg])
setMessage('')
scrollToEnd()
}
})
}
const scrollToEnd = () => {
const elmnt = document.getElementById('messages-container')
elmnt.scrollTop = elmnt.scrollHeight
}
return (
<div
className="w-full mx-auto rounded-lg py-4 px-6 my-2
bg-white shadow-lg"
>
<div
id="messages-container"
className="w-full h-[calc(100vh_-_30rem)] overflow-y-auto"
>
{messages.map((msg, i) => (
<Message
key={i}
message={msg.text}
timestamp={new Date().toDateString()}
owner={msg.sender.uid}
isOwner={msg.sender.uid == connectedAccount}
/>
))}
</div>
<form onSubmit={handleMessage} className="flex w-full">
<input
className="w-full bg-gray-200 rounded-lg p-4
focus:ring-0 focus:outline-none border-gray-500"
type="text"
placeholder="Write a message..."
value={message}
onChange={(e) => setMessage(e.target.value)}
required
/>
<button type="submit" hidden>
Send
</button>
</form>
</div>
)
}
const Message = ({ message, timestamp, owner, isOwner }) => (
<div className="flex flex-row justify-start w-2/5 my-2">
<div className="flex justify-center items-end space-x-2">
<div className="flex flex-col">
<div className="flex justify-start items-center space-x-1">
<div className="flex justify-start items-center space-x-1">
<Identicon
string={owner}
size={20}
className="h-10 w-10 object-contain rounded-full"
/>
<span className="font-bold text-xs">
{isOwner ? '@You' : truncate(owner, 4, 4, 11)}
</span>
</div>
<span className="text-gray-800 text-[10px]">{timestamp}</span>
</div>
<small className="leading-tight text-md my-1">{message}</small>
</div>
</div>
</div>
)
export default Messages
view raw Messages.jsx hosted with ❤ by GitHub

Footer Component

Footer Component

This is a simple component that essentially enhances the aesthetics and design of our application. Look at the code below.

view raw Footer.jsx hosted with ❤ by GitHub

Views

On the **src** directory, create a new folder called **views** and create the following components one after the other inside of it.

Home Page

Home Page

This page brings together the hero and polls components in one beautiful interface. See the code snippet below.

import Hero from '../components/Hero'
import Polls from '../components/Polls'
import { useGlobalState } from '../store'
const Home = () => {
const [polls] = useGlobalState('polls')
return (
<div>
<Hero />
<Polls polls={polls.filter((poll) => !poll.deleted)} />
</div>
)
}
export default Home
view raw Home.jsx hosted with ❤ by GitHub

Vote Page

VotesPage

This page contains buttons for launching the edit or delete poll components, and also gives one the opportunity to contest, vote, or chat with other voters. See the codes below.

import { useEffect, useState } from 'react'
import { toast } from 'react-toastify'
import { useNavigate, useParams } from 'react-router-dom'
import { getPoll, contest, listContestants, vote } from '../Blockchain.services'
import { useGlobalState, setGlobalState, truncate } from '../store'
import Moment from 'react-moment'
import Identicon from 'react-identicons'
import Messages from '../components/Messages'
import { createNewGroup, getGroup, joinGroup } from '../Chat.services'
const Vote = () => {
const { id } = useParams()
const navigate = useNavigate()
const [poll] = useGlobalState('poll')
const [connectedAccount] = useGlobalState('connectedAccount')
const [currentUser] = useGlobalState('currentUser')
const [contestants] = useGlobalState('contestants')
const [group, setGroup] = useState(null)
const handleContest = async () => {
await toast.promise(
new Promise(async (resolve, reject) => {
await contest(id)
.then(() => resolve())
.catch(() => reject())
}),
{
pending: 'Approve transaction...',
success: 'Contested, will reflect within 30sec 👌',
error: 'Encountered error 🤯',
},
)
}
const handCreateGroup = async () => {
await toast.promise(
new Promise(async (resolve, reject) => {
await createNewGroup(`pid_${id}`, poll?.title)
.then(() => resolve())
.catch(() => reject())
}),
{
pending: 'Creating...',
success: 'Chat group successfully created. 👌',
error: 'Encountered error 🤯',
},
)
}
const handleGroup = async () => {
await getGroup(`pid_${id}`).then(async (res) => {
if (!res.code && !res.hasJoined) {
await joinGroup(`pid_${id}`)
setGroup(res)
} else if (!res.code) {
setGroup(res)
}
})
}
useEffect(async () => {
await getPoll(id)
await listContestants(id)
if (!currentUser) {
toast('Please, register and login in first...')
navigate('/')
}
await handleGroup()
}, [])
return (
<div className="w-full md:w-4/5 mx-auto p-4">
<div className="text-center my-5">
<img
className="w-full h-40 object-cover mb-4"
src={poll?.image}
alt={poll?.title}
/>
<h1 className="text-5xl text-black-600 font-bold">{poll?.title}</h1>
<p className="pt-5 text-gray-600 text-xl font-medium">
{poll?.description}
</p>
<div className="flex justify-center items-center space-x-2 my-2 text-sm">
<Moment className="text-gray-500" unix format="ddd DD MMM, YYYY">
{poll?.startsAt}
</Moment>
<span> - </span>
<Moment className="text-gray-500" unix format="ddd DD MMM, YYYY">
{poll?.endsAt}
</Moment>
</div>
<div className="flex justify-center items-center space-x-2 text-sm">
<Identicon
string={poll?.director}
size={25}
className="h-10 w-10 object-contain rounded-full"
/>
<span className="font-bold">
{poll?.director ? truncate(poll?.director, 4, 4, 11) : '...'}
</span>
</div>
<div className="flex justify-center items-center space-x-2 my-2 text-sm">
<span className="text-gray-500">{poll?.votes} Votes</span>
<span className="text-gray-500">{poll?.contestants} Contestants</span>
</div>
<div className="flex justify-center my-3">
{new Date().getTime() >
Number(poll?.startsAt + '000') ? null : poll?.deleted ? null : (
<div className="flex space-x-2">
<button
type="button"
className="inline-block px-6 py-2 border-2 border-blue-600 text-blue-600
font-medium text-xs leading-tight uppercase rounded hover:bg-black hover:bg-opacity-5
focus:outline-none focus:ring-0 transition duration-150 ease-in-out"
onClick={handleContest}
>
Contest
</button>
{connectedAccount == poll?.director ? (
<>
{!group ? (
<button
type="button"
className="inline-block px-6 py-2 border-2 border-gray-600 text-gray-600
font-medium text-xs leading-tight uppercase rounded hover:bg-black hover:bg-opacity-5
focus:outline-none focus:ring-0 transition duration-150 ease-in-out"
onClick={handCreateGroup}
>
Create Group
</button>
) : null}
<button
type="button"
className="inline-block px-6 py-2 border-2 border-gray-600 text-gray-600
font-medium text-xs leading-tight uppercase rounded hover:bg-black hover:bg-opacity-5
focus:outline-none focus:ring-0 transition duration-150 ease-in-out"
onClick={() =>
setGlobalState('updatePollModal', 'scale-100')
}
>
Edit
</button>
<button
type="button"
className="inline-block px-6 py-2 border-2 border-red-600 text-red-600
font-medium text-xs leading-tight uppercase rounded hover:bg-black hover:bg-opacity-5
focus:outline-none focus:ring-0 transition duration-150 ease-in-out"
onClick={() =>
setGlobalState('deletePollModal', 'scale-100')
}
>
Delete
</button>
</>
) : null}
</div>
)}
</div>
</div>
<div className="flex flex-col w-full lg:w-3/4 mx-auto">
<div className="flex flex-col items-center">
{contestants.length > 0 ? (
<h4 className="text-lg font-medium uppercase mt-6 mb-3">
Contestants
</h4>
) : null}
{contestants.map((contestant, i) => (
<Votee key={i} contestant={contestant} poll={poll} />
))}
</div>
{group ? (
<div className="flex flex-col items-center">
<h4 className="text-lg font-medium uppercase mt-6 mb-3">
Live Chats
</h4>
<Messages guid={`pid_${id}`} />
</div>
) : null}
</div>
</div>
)
}
const Votee = ({ contestant, poll }) => {
const handleVote = async (id, cid) => {
await toast.promise(
new Promise(async (resolve, reject) => {
await vote(id, cid)
.then(() => resolve())
.catch(() => reject())
}),
{
pending: 'Approve transaction...',
success: 'Voted, will reflect within 30sec 👌',
error: 'Encountered error 🤯',
},
)
}
return (
<div className="flex justify-start w-full mx-auto rounded-lg bg-white shadow-lg my-2">
<div>
<img
className="w-40 h-full object-cover rounded-lg md:rounded-none"
src={contestant?.image}
alt={contestant?.fullname}
/>
</div>
<div className="p-6 flex flex-col justify-start ">
<p className="text-gray-700 text-base font-bold">
{contestant?.fullname}
</p>
<div className="flex justify-start items-center space-x-2 text-sm my-2">
<Identicon
string={contestant?.voter}
size={20}
className="h-10 w-10 object-contain rounded-full"
/>
<span className="font-bold">
{truncate(contestant?.voter, 4, 4, 11)}
</span>
</div>
<div className="flex justify-start items-center">
<span className="text-gray-600 text-sm">
{contestant?.votes} votes
</span>
{new Date().getTime() > Number(poll?.startsAt + '000') &&
Number(poll?.endsAt + '000') > new Date().getTime() ? (
<button
type="button"
className="inline-block px-3 py-1 border-2 border-gray-800 text-gray-800
font-medium text-xs leading-tight uppercase rounded-full hover:bg-black
hover:bg-opacity-5 focus:outline-none focus:ring-0 transition duration-150
ease-in-out ml-8"
onClick={() => handleVote(poll?.id, contestant?.id)}
>
Vote
</button>
) : null}
</div>
</div>
</div>
)
}
export default Vote
view raw Vote.jsx hosted with ❤ by GitHub

The App.jsx file

Now, let’s tackle the App.jsx file responsible for bundling all our components and pages, see its codes below.

import { useEffect, useState } from 'react'
import { Routes, Route } from 'react-router-dom'
import { getPolls, getUser, isWallectConnected } from './Blockchain.services'
import { ToastContainer } from 'react-toastify'
import { checkAuthState } from './Chat.services'
import CreatePoll from './components/CreatePoll'
import DeletePoll from './components/DeletePoll'
import Footer from './components/Footer'
import Header from './components/Header'
import Register from './components/Register'
import UpdatePoll from './components/UpdatePoll'
import Home from './views/Home'
import Vote from './views/Vote'
const App = () => {
const [loaded, setLoaded] = useState(false)
useEffect(async () => {
await isWallectConnected()
await getPolls()
await getUser()
await checkAuthState()
setLoaded(true)
console.log('Blockchain loaded')
}, [])
return (
<div className="min-h-screen">
<Header />
{loaded ? (
<Routes>
<Route path="/" element={<Home />} />
<Route path="/polls/:id" element={<Vote />} />
</Routes>
) : null}
<Register />
<DeletePoll />
<CreatePoll />
<UpdatePoll />
<Footer />
<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

Other Essential Services

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

The Store Service
We're using the react-hooks-global-state library to create a centralized storage space in our app that serves as a state management service.

Make a "store" folder in the src folder. Next, within this store folder, create a file called index.jsx and paste and save the following codes into it.

import { createGlobalState } from 'react-hooks-global-state'
import moment from 'moment'
const { getGlobalState, useGlobalState, setGlobalState } = createGlobalState({
contestModal: 'scale-0',
createPollModal: 'scale-0',
updatePollModal: 'scale-0',
deletePollModal: 'scale-0',
connectedAccount: '',
currentUser: null,
contract: null,
user: null,
polls: [],
poll: null,
contestants: [],
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 toDate = (timestamp) => {
const date = new Date(timestamp)
const dd = date.getDate() > 9 ? date.getDate() : `0${date.getDate()}`
const mm =
date.getMonth() + 1 > 9 ? date.getMonth() + 1 : `0${date.getMonth() + 1}`
const yyyy = date.getFullYear()
return `${yyyy}-${mm}-${dd}`
}
const toHex = (str) => {
let result = ''
for (let i = 0; i < str.length; i++) {
result += str.charCodeAt(i).toString(16)
}
return result.slice(0, 6)
}
export {
getGlobalState,
useGlobalState,
setGlobalState,
truncate,
toDate,
toHex,
}
view raw index.jsx hosted with ❤ by GitHub

The Blockchain Service
In the src folder, create a file called Blockchain.services.jsx and paste and save the file below inside it.

import abi from './abis/src/contracts/BlueVotes.sol/BlueVotes.json'
import address from './abis/contractAddress.json'
import { getGlobalState, setGlobalState } from './store'
import { ethers } from 'ethers'
import { checkAuthState, logOutWithCometChat } from './Chat.services'
const { ethereum } = window
const contractAddress = address.address
const contractAbi = abi.abi
const getEtheriumContract = () => {
const connectedAccount = getGlobalState('connectedAccount')
if (connectedAccount) {
const provider = new ethers.providers.Web3Provider(ethereum)
const signer = provider.getSigner()
const contract = new ethers.Contract(contractAddress, contractAbi, signer)
return contract
} else {
return getGlobalState('contract')
}
}
const isWallectConnected = async () => {
try {
if (!ethereum) return alert('Please install Metamask')
const accounts = await ethereum.request({ method: 'eth_accounts' })
setGlobalState('connectedAccount', accounts[0]?.toLowerCase())
window.ethereum.on('chainChanged', (chainId) => {
window.location.reload()
})
window.ethereum.on('accountsChanged', async () => {
setGlobalState('connectedAccount', accounts[0]?.toLowerCase())
// await isWallectConnected()
await logOutWithCometChat()
// await checkAuthState()
// await getUser()
window.location.reload()
})
if (accounts.length) {
setGlobalState('connectedAccount', accounts[0]?.toLowerCase())
} else {
alert('Please connect wallet.')
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]?.toLowerCase())
} catch (error) {
reportError(error)
}
}
const createPoll = async ({ title, image, startsAt, endsAt, description }) => {
try {
if (!ethereum) return alert('Please install Metamask')
const connectedAccount = getGlobalState('connectedAccount')
const contract = getEtheriumContract()
await contract.createPoll(image, title, description, startsAt, endsAt, {
from: connectedAccount,
})
await getPolls()
} catch (error) {
reportError(error)
}
}
const updatePoll = async ({
id,
title,
image,
startsAt,
endsAt,
description,
}) => {
try {
if (!ethereum) return alert('Please install Metamask')
const connectedAccount = getGlobalState('connectedAccount')
const contract = getEtheriumContract()
await contract.updatePoll(id, image, title, description, startsAt, endsAt, {
from: connectedAccount,
})
await getPolls()
} catch (error) {
reportError(error)
}
}
const deletePoll = async (id) => {
try {
if (!ethereum) return alert('Please install Metamask')
const connectedAccount = getGlobalState('connectedAccount')
const contract = getEtheriumContract()
await contract.deletePoll(id, {
from: connectedAccount,
})
} catch (error) {
reportError(error)
}
}
const registerUser = async ({ fullname, image }) => {
try {
if (!ethereum) return alert('Please install Metamask')
const connectedAccount = getGlobalState('connectedAccount')
const contract = getEtheriumContract()
await contract.register(image, fullname, { from: connectedAccount })
await getUser()
} catch (error) {
reportError(error)
}
}
const getUser = async () => {
try {
if (!ethereum) return alert('Please install Metamask')
const connectedAccount = getGlobalState('connectedAccount')
const contract = getEtheriumContract()
const user = await contract.users(connectedAccount)
setGlobalState('user', user)
} catch (error) {
reportError(error)
}
}
const getPolls = async () => {
try {
if (!ethereum) return alert('Please install Metamask')
const contract = getEtheriumContract()
const polls = await contract.getPolls()
setGlobalState('polls', structuredPolls(polls))
} catch (error) {
reportError(error)
}
}
const getPoll = async (id) => {
try {
if (!ethereum) return alert('Please install Metamask')
const contract = getEtheriumContract()
const poll = await contract.getPoll(id)
setGlobalState('poll', structuredPolls([poll])[0])
} catch (error) {
reportError(error)
}
}
const contest = async (id) => {
try {
if (!ethereum) return alert('Please install Metamask')
const connectedAccount = getGlobalState('connectedAccount')
const contract = getEtheriumContract()
await contract.contest(id, { from: connectedAccount })
await getPoll(id)
} catch (error) {
reportError(error)
}
}
const vote = async (id, cid) => {
try {
if (!ethereum) return alert('Please install Metamask')
const connectedAccount = getGlobalState('connectedAccount')
const contract = getEtheriumContract()
await contract.vote(id, cid, { from: connectedAccount })
await getPoll(id)
await listContestants(id)
} catch (error) {
reportError(error)
}
}
const listContestants = async (id) => {
try {
if (!ethereum) return alert('Please install Metamask')
const contract = getEtheriumContract()
const contestants = await contract.listContestants(id)
setGlobalState('contestants', structuredContestants(contestants))
} catch (error) {
reportError(error)
}
}
const structuredPolls = (polls) =>
polls
.map((poll) => ({
id: Number(poll.id),
title: poll.title,
votes: Number(poll.votes),
startsAt: poll.startsAt,
endsAt: poll.endsAt,
contestants: Number(poll.contestants),
director: poll.director?.toLowerCase(),
image: poll.image,
deleted: poll.deleted,
description: poll.description,
timestamp: new Date(poll.timestamp.toNumber()).getTime(),
}))
.reverse()
const structuredContestants = (contestants, connectedAccount) =>
contestants
.map((contestant) => ({
id: Number(contestant.id),
fullname: contestant.fullname,
image: contestant.image,
voter: contestant.voter?.toLowerCase(),
voters: contestant.voters.map((v) => v?.toLowerCase()),
votes: Number(contestant.votes),
}))
.sort((a, b) => b.votes - a.votes)
const reportError = (error) => {
console.log(error.message)
throw new Error('No ethereum object.')
}
export {
isWallectConnected,
connectWallet,
registerUser,
getUser,
createPoll,
updatePoll,
deletePoll,
getPolls,
getPoll,
contest,
listContestants,
vote,
}

The Chat Service
Make a file called "Chat.services.jsx" in the src folder and paste and save the codes below inside it.

import { CometChat } from '@cometchat-pro/chat'
import { getGlobalState, 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 () => {
const authKey = CONSTANTS.Auth_Key
const UID = getGlobalState('connectedAccount')
await CometChat.login(UID, authKey)
.then((user) => setGlobalState('currentUser', user))
.catch((error) => console.log(JSON.stringify(error)))
}
const signUpWithCometChat = async (name) => {
const authKey = CONSTANTS.Auth_Key
const UID = getGlobalState('connectedAccount')
const user = new CometChat.User(UID)
user.setName(name)
return await CometChat.createUser(user, authKey)
.then((user) => user)
.catch((error) => error)
}
const logOutWithCometChat = async () => {
await CometChat.logout()
.then(() => {
setGlobalState('currentUser', null)
console.log('Logged Out Successfully')
})
.catch((error) => console.log(error))
}
const checkAuthState = async () => {
await CometChat.getLoggedinUser()
.then((user) => setGlobalState('currentUser', user))
.catch((error) => console.log(error))
}
const createNewGroup = async (GUID, groupName) => {
const groupType = CometChat.GROUP_TYPE.PUBLIC
const password = ''
const group = new CometChat.Group(GUID, groupName, groupType, password)
await CometChat.createGroup(group)
.then((group) => setGlobalState('group', group))
.catch((error) => console.log(error))
}
const getGroup = async (GUID) => {
return await CometChat.getGroup(GUID)
.then((group) => {
setGlobalState('group', group)
return group
})
.catch((error) => error)
}
const joinGroup = async (GUID) => {
const groupType = CometChat.GROUP_TYPE.PUBLIC
const password = ''
await CometChat.joinGroup(GUID, groupType, password)
.then((group) => getGroup(group.guid))
.catch((error) => console.log(error))
}
const getMessages = async (UID) => {
const limit = 30
const messagesRequest = new CometChat.MessagesRequestBuilder()
.setGUID(UID)
.setLimit(limit)
.build()
return await messagesRequest
.fetchPrevious()
.then((messages) => messages)
.catch((error) => error)
}
const sendMessage = async (receiverID, messageText) => {
const receiverType = CometChat.RECEIVER_TYPE.GROUP
const textMessage = new CometChat.TextMessage(
receiverID,
messageText,
receiverType,
)
return await CometChat.sendMessage(textMessage)
.then((message) => message)
.catch((error) => error)
}
export {
initCometChat,
loginWithCometChat,
signUpWithCometChat,
logOutWithCometChat,
getMessages,
sendMessage,
checkAuthState,
createNewGroup,
getGroup,
joinGroup,
CometChat,
}

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

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

Lastly, run the following commands on two terminals to spin up the server on your browser, but make sure you already have Metamask installed.

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

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

If you're confused about web3 development and want visual materials, here's one of my videos that will teach you how to create an NFT marketplace.

Build NFT marketplace

Conclusion

The decentralized web and the blockchain have come to stay, and building real-life use cases are a sure way to accelerate your web3 development career.

In this tutorial, you have learned how to build a blockchain-based voting system that utilizes smart contracts to ensure that rigging and cheating are prohibited. We also incorporated the awesome CometChat SDK that allows us to have a group chat for each poll.

If you are ready to go deeper into web3 development, watch my free videos on my YouTube channel. Or book your private web3 classes with me to speed up your web3 learning process.

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

About the Author

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

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

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

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

Image of Timescale

🚀 pgai Vectorizer: SQLAlchemy and LiteLLM Make Vector Search Simple

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

Read more

Top comments (0)