paint-brush
How to Build a Blockchain Voting System with React, Solidity, and CometChatby@daltonic
3,923 reads
3,923 reads

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

by Darlington Gospel November 24th, 2022
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

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.

Companies Mentioned

Mention Thumbnail
Mention Thumbnail

Coins Mentioned

Mention Thumbnail
Mention Thumbnail
featured image - How to Build a Blockchain Voting System with React, Solidity, and CometChat
Darlington Gospel  HackerNoon profile picture

Here’s 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 how to build 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.


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>


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"
    ]
  }
}


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=******************************

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=******************************

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=******************************

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
})


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


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.


https://www.youtube.com/embed/hsec2erdLOI

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
        );
    }
}


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.


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


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


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


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


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


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


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


Footer Component

Footer Component


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


import { AiFillGithub } from 'react-icons/ai'
import { CgYoutube } from 'react-icons/cg'
import { FaTwitter } from 'react-icons/fa'
import { GrLinkedin } from 'react-icons/gr'

const Footer = () => {
  return (
    <footer className="text-center bg-gray-900 text-white mt-20">
      <div className="flex justify-center items-center mx-auto space-x-3 p-4">
        <a
          href="https://www.linkedin.com/in/darlington-gospel-aa626b125/"
          target="_blank"
          type="button"
          className="rounded-full text-white leading-normal uppercase hover:bg-black
          hover:bg-opacity-5 focus:outline-none focus:ring-0
          transition duration-150 ease-in-out w-9 h-9"
        >
          <GrLinkedin size={35} />
        </a>
        <a
          href="https://www.youtube.com/@dappmentors?sub_confirmation=1"
          target="_blank"
          type="button"
          className="rounded-full text-white leading-normal uppercase hover:bg-black
          hover:bg-opacity-5 focus:outline-none focus:ring-0
          transition duration-150 ease-in-out w-9 h-9"
        >
          <CgYoutube size={35} />
        </a>

        <a
          href="https://github.com/Daltonic"
          target="_blank"
          type="button"
          className="rounded-full text-white leading-normal uppercase hover:bg-black
          hover:bg-opacity-5 focus:outline-none focus:ring-0
          transition duration-150 ease-in-out w-9 h-9"
        >
          <AiFillGithub size={35} />
        </a>
        <a
          href="https://twitter.com/iDaltonic"
          target="_blank"
          type="button"
          className="rounded-full text-white leading-normal uppercase hover:bg-black
          hover:bg-opacity-5 focus:outline-none focus:ring-0
          transition duration-150 ease-in-out w-9 h-9"
        >
          <FaTwitter size={35} />
        </a>
      </div>
      <div
        className="flex flex-col justify-center items-center text-center p-4"
        style={{ backgroundColor: 'rgba(0, 0, 0, 0.2)' }}
      >
        © 2022
        <a
          className="flex space-x-2 text-whitehite"
          href="https://daltonic.github.io/"
        >
          <span>With Love ❤️</span>
          <span className="text-orange-700">Daltonic</span>
        </a>
      </div>
    </footer>
  )
}

export default Footer

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


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

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

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,
}


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'),
  )
})


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


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.


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