How to Build a Glorious Web3.0 DAO with React, Solidity, and CometChat

Written by daltonic | Published 2022/06/22
Tech Story Tags: web3 | decentralized-apps | solidity | blockchain-development | smart-contracts | react | web3-writing-contest | hackernoon-top-story

TLDRDarlington Gospel is a Dapp Mentor helping developers from Web 2.0 to Web 3.0.0. In this tutorial, you will learn step-by-step how to implement a decentralized autonomous organization (DAO) with anonymous chat features. In the tutorial below, clone the Web3 Starter Project below. This will ensure that we’re all on the same page and are using the same packages. The code on the terminal to confirm it is installed globally to install these essential packages globally.via the TL;DR App

See a demo and git repo here for what you will be building!

Introduction

I’m super excited to release this web3.0 build to you, I know you’ve been looking for a great example to get you started in developing decentralized applications.

If you are new here, I’m Darlington Gospel, a Dapp Mentor helping transition developers like you from Web 2.0 to Web 3.0.

In this tutorial, you will learn step-by-step how to implement a decentralized autonomous organization (DAO) with anonymous chat features.

If you are pumped for this build, let’s jump into the tutorial…

Prerequisite

You will need the following tools installed to successfully crush this build:

  • Node
  • Ganache-Cli
  • Truffle
  • React
  • Infuria
  • Tailwind CSS
  • CometChat SDK
  • Metamask
  • Yarn

Installing Dependencies

NodeJs Installation Make sure you have NodeJs installed on your machine already. Next, run the code on the terminal to confirm it is installed.

Yarn, Ganache-cli and Truffle Installation Run the following codes on your terminal to install these essential packages globally.

npm i -g yarn
npm i -g truffle
npm i -g ganache-cli

Cloning Web3 Starter Project Using the commands below, clone the web 3.0 starter project below. This will ensure that we’re all on the same page and are using the same packages.

git clone https://github.com/Daltonic/dominionDAO

Fantastic, let us replace the package.json file with the one below:

{
  "name": "dominionDAO",
  "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.6",
    "moment": "^2.29.3",
    "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.0.1",
    "recharts": "^2.1.9",
    "web-vitals": "^2.1.4",
    "web3": "^1.7.1"
  },
  "devDependencies": {
    "@openzeppelin/contracts": "^4.5.0",
    "@tailwindcss/forms": "0.4.0",
    "@truffle/hdwallet-provider": "^2.0.4",
    "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"
    ]
  }
}

Great, replace your package.json file with the above code and then run yarn install on your terminal.

With that all installed, let’s start with writing the Dominion DAO smart contract.

Configuring CometChat SDK

To configure the CometChat SDK, follow the steps below, at the end, you need to store these keys as an environment variable.

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

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

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

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

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

Replace the REACT_COMET_CHAT placeholders keys with their appropriate values.

REACT_APP_COMET_CHAT_REGION=**
REACT_APP_COMET_CHAT_APP_ID=**************
REACT_APP_COMET_CHAT_AUTH_KEY=******************************

Configuring Infuria App

STEP 1: Head to Infuria, and create an account.

STEP 2: From the dashboard create a new project.

STEP 3: Copy the Rinkeby test network WebSocket endpoint URL to your .env file.

Next, add your Metamask secret phrase and your preferred account private key. If you have done those 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=******************************

If you don’t know how to access your private key, see the section below.

Accessing Your Metamask Private Key

STEP 1: Click on your Metamask browser extension, and make sure Rinkeby is selected as the test network. Next, on the preferred account, click on the vertical dotted line and select account details. See the image below.

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 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 4: Copy your private key to your .env file. See the image and code snippet below:

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

REACT_APP_COMET_CHAT_REGION=**
REACT_APP_COMET_CHAT_APP_ID=**************
REACT_APP_COMET_CHAT_AUTH_KEY=******************************

As for your SECRET_KEY, you are required to paste your Metamask secret phrase in the space provided in the environment file.

The Dominion DAO Smart Contract

Here is the full code for the smart contract, I will explain all the functions and variables one after the other.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;

import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

contract DominionDAO is ReentrancyGuard, AccessControl {
    bytes32 private immutable CONTRIBUTOR_ROLE = keccak256("CONTRIBUTOR");
    bytes32 private immutable STAKEHOLDER_ROLE = keccak256("STAKEHOLDER");
    uint32 immutable MIN_VOTE_DURATION = 1 weeks;
    uint256 totalProposals;
    uint256 public daoBalance;

    mapping(uint256 => ProposalStruct) private raisedProposals;
    mapping(address => uint256[]) private stakeholderVotes;
    mapping(uint256 => VotedStruct[]) private votedOn;
    mapping(address => uint256) private contributors;
    mapping(address => uint256) private stakeholders;

    struct ProposalStruct {
        uint256 id;
        uint256 amount;
        uint256 duration;
        uint256 upvotes;
        uint256 downvotes;
        string title;
        string description;
        bool passed;
        bool paid;
        address payable beneficiary;
        address proposer;
        address executor;
    }

    struct VotedStruct {
        address voter;
        uint256 timestamp;
        bool choosen;
    }

    event Action(
        address indexed initiator,
        bytes32 role,
        string message,
        address indexed beneficiary,
        uint256 amount
    );

    modifier stakeholderOnly(string memory message) {
        require(hasRole(STAKEHOLDER_ROLE, msg.sender), message);
        _;
    }

    modifier contributorOnly(string memory message) {
        require(hasRole(CONTRIBUTOR_ROLE, msg.sender), message);
        _;
    }

    function createProposal(
        string calldata title,
        string calldata description,
        address beneficiary,
        uint256 amount
    )external
     stakeholderOnly("Proposal Creation Allowed for Stakeholders only")
    {
        uint256 proposalId = totalProposals++;
        ProposalStruct storage proposal = raisedProposals[proposalId];

        proposal.id = proposalId;
        proposal.proposer = payable(msg.sender);
        proposal.title = title;
        proposal.description = description;
        proposal.beneficiary = payable(beneficiary);
        proposal.amount = amount;
        proposal.duration = block.timestamp + MIN_VOTE_DURATION;

        emit Action(
            msg.sender,
            CONTRIBUTOR_ROLE,
            "PROPOSAL RAISED",
            beneficiary,
            amount
        );
    }

    function performVote(uint256 proposalId, bool choosen)
        external
        stakeholderOnly("Unauthorized: Stakeholders only")
    {
        ProposalStruct storage proposal = raisedProposals[proposalId];

        handleVoting(proposal);

        if (choosen) proposal.upvotes++;
        else proposal.downvotes++;

        stakeholderVotes[msg.sender].push(proposal.id);

        votedOn[proposal.id].push(
            VotedStruct(
                msg.sender,
                block.timestamp,
                choosen
            )
        );

        emit Action(
            msg.sender,
            STAKEHOLDER_ROLE,
            "PROPOSAL VOTE",
            proposal.beneficiary,
            proposal.amount
        );
    }

    function handleVoting(ProposalStruct storage proposal) private {
        if (
            proposal.passed ||
            proposal.duration <= block.timestamp
        ) {
            proposal.passed = true;
            revert("Proposal duration expired");
        }

        uint256[] memory tempVotes = stakeholderVotes[msg.sender];
        for (uint256 votes = 0; votes < tempVotes.length; votes++) {
            if (proposal.id == tempVotes[votes])
                revert("Double voting not allowed");
        }
    }

    function payBeneficiary(uint256 proposalId)
        external
        stakeholderOnly("Unauthorized: Stakeholders only")
        returns (bool)
    {
        ProposalStruct storage proposal = raisedProposals[proposalId];
        require(daoBalance >= proposal.amount, "Insufficient fund");
        require(block.timestamp > proposal.duration, "Proposal still ongoing");

        if (proposal.paid) revert("Payment sent before");

        if (proposal.upvotes <= proposal.downvotes)
            revert("Insufficient votes");

        payTo(proposal.beneficiary, proposal.amount);

        proposal.paid = true;
        proposal.executor = msg.sender;
        daoBalance -= proposal.amount;

        emit Action(
            msg.sender,
            STAKEHOLDER_ROLE,
            "PAYMENT TRANSFERED",
            proposal.beneficiary,
            proposal.amount
        );

        return true;
    }

    function contribute() payable external {

        if (!hasRole(STAKEHOLDER_ROLE, msg.sender)) {
            uint256 totalContribution =
                contributors[msg.sender] + msg.value;

            if (totalContribution >= 5 ether) {
                stakeholders[msg.sender] = totalContribution;
                contributors[msg.sender] += msg.value;
                _setupRole(STAKEHOLDER_ROLE, msg.sender);
                _setupRole(CONTRIBUTOR_ROLE, msg.sender);
            } else {
                contributors[msg.sender] += msg.value;
                _setupRole(CONTRIBUTOR_ROLE, msg.sender);
            }
        } else {
            contributors[msg.sender] += msg.value;
            stakeholders[msg.sender] += msg.value;
        }
        
        daoBalance += msg.value;

        emit Action(
            msg.sender,
            STAKEHOLDER_ROLE,
            "CONTRIBUTION RECEIVED",
            address(this),
            msg.value
        );
    }

    function getProposals()
        external
        view
        returns (ProposalStruct[] memory props)
    {
        props = new ProposalStruct[](totalProposals);

        for (uint256 i = 0; i < totalProposals; i++) {
            props[i] = raisedProposals[i];
        }
    }

    function getProposal(uint256 proposalId)
        external
        view
        returns (ProposalStruct memory)
    {
        return raisedProposals[proposalId];
    }
    
    function getVotesOf(uint256 proposalId)
        external
        view
        returns (VotedStruct[] memory)
    {
        return votedOn[proposalId];
    }

    function getStakeholderVotes()
        external
        view
        stakeholderOnly("Unauthorized: not a stakeholder")
        returns (uint256[] memory)
    {
        return stakeholderVotes[msg.sender];
    }

    function getStakeholderBalance()
        external
        view
        stakeholderOnly("Unauthorized: not a stakeholder")
        returns (uint256)
    {
        return stakeholders[msg.sender];
    }

    function isStakeholder() external view returns (bool) {
        return stakeholders[msg.sender] > 0;
    }

    function getContributorBalance()
        external
        view
        contributorOnly("Denied: User is not a contributor")
        returns (uint256)
    {
        return contributors[msg.sender];
    }

    function isContributor() external view returns (bool) {
        return contributors[msg.sender] > 0;
    }

    function getBalance() external view returns (uint256) {
        return contributors[msg.sender];
    }

    function payTo(
        address to, 
        uint256 amount
    ) internal returns (bool) {
        (bool success,) = payable(to).call{value: amount}("");
        require(success, "Payment failed");
        return true;
    }
}

In the project you just cloned, head to src >> contract directory and create a file named DominionDAO.sol, then paste the above codes inside of it.

Explanation:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;

Solidity requires a license identifier to compile your code, else it will produce a warning asking you to specify one. Also, Solidity requires that you specify the version of the compiler for your smart contract. That is what the word pragma represents.

import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

In the above code block, we are utilizing two openzeppelin's smart contracts for specifying roles and guarding our smart contract against reentrancy attacks.

bytes32 private immutable CONTRIBUTOR_ROLE = keccak256("CONTRIBUTOR");
bytes32 private immutable STAKEHOLDER_ROLE = keccak256("STAKEHOLDER");
uint32 immutable MIN_VOTE_DURATION = 1 weeks;
uint256 totalProposals;
uint256 public daoBalance;

We set up some state variables for stakeholder and contributor roles and specified the minimum vote duration to be one week. And we also initialized the total proposal counter and a variable to keep a record of our available balance.

mapping(uint256 => ProposalStruct) private raisedProposals;
mapping(address => uint256[]) private stakeholderVotes;
mapping(uint256 => VotedStruct[]) private votedOn;
mapping(address => uint256) private contributors;
mapping(address => uint256) private stakeholders;

raisedProposals keep track of all proposals submitted to our smart contract. stakeholderVotes as its name implies keeping track of votes made by stakeholders. votedOn keeps track of all the votes associated with a proposal. While contributors keep track of anyone who donated to our platform, stakeholders on the other hand keep track of people that have contributed up to 1 ether.

struct ProposalStruct {
    uint256 id;
    uint256 amount;
    uint256 duration;
    uint256 upvotes;
    uint256 downvotes;
    string title;
    string description;
    bool passed;
    bool paid;
    address payable beneficiary;
    address proposer;
    address executor;
}

struct VotedStruct {
    address voter;
    uint256 timestamp;
    bool choosen;
}

proposalStruct describes the content of each proposal whereas votedStruct describes the content of each vote.

event Action(
    address indexed initiator,
    bytes32 role,
    string message,
    address indexed beneficiary,
    uint256 amount
);

This is a dynamic event called Action. This will help us enrich the information logged out per transaction.

modifier stakeholderOnly(string memory message) {
    require(hasRole(STAKEHOLDER_ROLE, msg.sender), message);
    _;
}

modifier contributorOnly(string memory message) {
    require(hasRole(CONTRIBUTOR_ROLE, msg.sender), message);
    _;
}

The above modifiers help us identify users by role and also prevent them from accessing some unauthorized resources.

function createProposal(
    string calldata title,
    string calldata description,
    address beneficiary,
    uint256 amount
)external
 stakeholderOnly("Proposal Creation Allowed for Stakeholders only")
{
    uint256 proposalId = totalProposals++;
    ProposalStruct storage proposal = raisedProposals[proposalId];

    proposal.id = proposalId;
    proposal.proposer = payable(msg.sender);
    proposal.title = title;
    proposal.description = description;
    proposal.beneficiary = payable(beneficiary);
    proposal.amount = amount;
    proposal.duration = block.timestamp + MIN_VOTE_DURATION;

    emit Action(
        msg.sender,
        CONTRIBUTOR_ROLE,
        "PROPOSAL RAISED",
        beneficiary,
        amount
    );
}

The above function takes a proposal's title, description, amount, and the beneficiary’s wallet address and creates a proposal. The function only permits stakeholders to create proposals. Stakeholders are users who have made at least a contribution of 1 ether.

function performVote(uint256 proposalId, bool choosen)
    external
    stakeholderOnly("Unauthorized: Stakeholders only")
{
    ProposalStruct storage proposal = raisedProposals[proposalId];

    handleVoting(proposal);

    if (choosen) proposal.upvotes++;
    else proposal.downvotes++;

    stakeholderVotes[msg.sender].push(proposal.id);

    votedOn[proposal.id].push(
        VotedStruct(
            msg.sender,
            block.timestamp,
            choosen
        )
    );

    emit Action(
        msg.sender,
        STAKEHOLDER_ROLE,
        "PROPOSAL VOTE",
        proposal.beneficiary,
        proposal.amount
    );
}

This function accepts two arguments, a proposal Id, and a preferred choice represented by a Boolean value. True means you accepted the vote and False represents a rejection.

function handleVoting(ProposalStruct storage proposal) private {
    if (
        proposal.passed ||
        proposal.duration <= block.timestamp
    ) {
        proposal.passed = true;
        revert("Proposal duration expired");
    }

    uint256[] memory tempVotes = stakeholderVotes[msg.sender];
    for (uint256 votes = 0; votes < tempVotes.length; votes++) {
        if (proposal.id == tempVotes[votes])
            revert("Double voting not allowed");
    }
}

This function performs the actual voting including checking if a user is a stakeholder and qualified to vote.

function payBeneficiary(uint256 proposalId)
    external
    stakeholderOnly("Unauthorized: Stakeholders only")
    returns (bool)
{
    ProposalStruct storage proposal = raisedProposals[proposalId];
    require(daoBalance >= proposal.amount, "Insufficient fund");
    require(block.timestamp > proposal.duration, "Proposal still ongoing");

    if (proposal.paid) revert("Payment sent before");

    if (proposal.upvotes <= proposal.downvotes)
        revert("Insufficient votes");

    payTo(proposal.beneficiary, proposal.amount);

    proposal.paid = true;
    proposal.executor = msg.sender;
    daoBalance -= proposal.amount;

    emit Action(
        msg.sender,
        STAKEHOLDER_ROLE,
        "PAYMENT TRANSFERED",
        proposal.beneficiary,
        proposal.amount
    );

    return true;
}

This function is responsible for paying the beneficiary attached to a proposal based on certain criteria.

  • One, the beneficiary must not already be paid.
  • Two, the proposal duration must have expired.
  • Three, the available balance must be able to pay the beneficiary.
  • Four, there must be no tie in the number of votes.

function contribute() payable external {
    if (!hasRole(STAKEHOLDER_ROLE, msg.sender)) {
        uint256 totalContribution =
            contributors[msg.sender] + msg.value;

        if (totalContribution >= 5 ether) {
            stakeholders[msg.sender] = totalContribution;
            contributors[msg.sender] += msg.value;
            _setupRole(STAKEHOLDER_ROLE, msg.sender);
            _setupRole(CONTRIBUTOR_ROLE, msg.sender);
        } else {
            contributors[msg.sender] += msg.value;
            _setupRole(CONTRIBUTOR_ROLE, msg.sender);
        }
    } else {
        contributors[msg.sender] += msg.value;
        stakeholders[msg.sender] += msg.value;
    }
    
    daoBalance += msg.value;

    emit Action(
        msg.sender,
        STAKEHOLDER_ROLE,
        "CONTRIBUTION RECEIVED",
        address(this),
        msg.value
    );
}

This function is responsible for collecting contributions from donors and those interested in becoming stakeholders.

function getProposals()
    external
    view
    returns (ProposalStruct[] memory props)
{
    props = new ProposalStruct[](totalProposals);

    for (uint256 i = 0; i < totalProposals; i++) {
        props[i] = raisedProposals[i];
    }
}

This function retrieves an array of proposals recorded on this smart contract.

function getProposal(uint256 proposalId)
    external
    view
    returns (ProposalStruct memory)
{
    return raisedProposals[proposalId];
}

This function retrieves a particular proposal by Id.

function getVotesOf(uint256 proposalId)
    external
    view
    returns (VotedStruct[] memory)
{
    return votedOn[proposalId];
}

This returns a list of votes associated with a particular proposal.

function getStakeholderVotes()
    external
    view
    stakeholderOnly("Unauthorized: not a stakeholder")
    returns (uint256[] memory)
{
    return stakeholderVotes[msg.sender];
}

This returns the list of stakeholders on the smart contract and only a stakeholder can call this function.

function getStakeholderBalance()
    external
    view
    stakeholderOnly("Unauthorized: not a stakeholder")
    returns (uint256)
{
    return stakeholders[msg.sender];
}

This returns the amount of money contributed by stakeholders.

function isStakeholder() external view returns (bool) {
    return stakeholders[msg.sender] > 0;
}

Returns True or False if a user is a stakeholder.

function getContributorBalance()
    external
    view
    contributorOnly("Denied: User is not a contributor")
    returns (uint256)
{
    return contributors[msg.sender];
}

This returns the balance of a contributor and is only accessible to the contributor.

function isContributor() external view returns (bool) {
    return contributors[msg.sender] > 0;
}

This checks if a user is a contributor or not and it is represented with True or False.

function getBalance() external view returns (uint256) {
    return contributors[msg.sender];
}

Returns the balance of the calling user regardless of his role.

function payTo(
    address to, 
    uint256 amount
) internal returns (bool) {
    (bool success,) = payable(to).call{value: amount}("");
    require(success, "Payment failed");
    return true;
}

This function performs a payment with both a specified amount and account.

Configuring the Deployment Script

One more thing to do with the smart contract is to configure the deployment script.

On the project head to the migrations folder, >> 2_deploy_contracts.js, and update it with the code snippet below.

const DominionDAO = artifacts.require('DominionDAO')
  module.exports = async function (deployer) {
  await deployer.deploy(DominionDAO)
}

Fantastic, we just finished up with the smart contract for our application, it's time to start building the Dapp interface.

Developing the Frontend

The front end comprises many components and parts. We will be creating all the components, views, and the rest of the peripherals.

Header Component

This component captures information about the current user and carries a theme toggling button for light and dark modes. And if you wondered how I did that, it was through Tailwind CSS, see the code below.

import { useState, useEffect } from 'react'
import { FaUserSecret } from 'react-icons/fa'
import { MdLightMode } from 'react-icons/md'
import { FaMoon } from 'react-icons/fa'
import { Link } from 'react-router-dom'
import { connectWallet } from '../Dominion'
import { useGlobalState, truncate } from '../store'

const Header = () => {
  const [theme, setTheme] = useState(localStorage.theme)
  const themeColor = theme === 'dark' ? 'light' : 'dark'
  const darken = theme === 'dark' ? true : false
  const [connectedAccount] = useGlobalState('connectedAccount')

  useEffect(() => {
    const root = window.document.documentElement
    root.classList.remove(themeColor)
    root.classList.add(theme)
    localStorage.setItem('theme', theme)
  }, [themeColor, theme])

  const toggleLight = () => {
    const root = window.document.documentElement
    root.classList.remove(themeColor)
    root.classList.add(theme)
    localStorage.setItem('theme', theme)
    setTheme(themeColor)
  }

  return (
    <header className="sticky top-0 z-50 dark:text-blue-500">
      <nav className="navbar navbar-expand-lg shadow-md py-2 relative flex items-center w-full justify-between bg-white dark:bg-[#212936]">
        <div className="px-6 w-full flex flex-wrap items-center justify-between">
          <div className="navbar-collapse collapse grow flex flex-row justify-between items-center p-2">
            <Link
              to={'/'}
              className="flex flex-row justify-start items-center space-x-3"
            >
              <FaUserSecret className="cursor-pointer" size={25} />
              <span className="invisible md:visible dark:text-gray-300">
                Dominion
              </span>
            </Link>

            <div className="flex flex-row justify-center items-center space-x-5">
              {darken ? (
                <MdLightMode
                  className="cursor-pointer"
                  size={25}
                  onClick={toggleLight}
                />
              ) : (
                <FaMoon
                  className="cursor-pointer"
                  size={25}
                  onClick={toggleLight}
                />
              )}

              {connectedAccount ? (
                <button
                  className="px-4 py-2.5 bg-blue-600 text-white
                  font-medium text-xs leading-tight uppercase
                  rounded-full 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 dark:text-blue-500
                  dark:border dark:border-blue-500 dark:bg-transparent"
                >
                  {truncate(connectedAccount, 4, 4, 11)}
                </button>
              ) : (
                <button
                  className="px-4 py-2.5 bg-blue-600 text-white
                  font-medium text-xs leading-tight uppercase
                  rounded-full 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 dark:text-blue-500
                  dark:border dark:border-blue-500 dark:bg-transparent"
                  onClick={connectWallet}
                >
                  Connect Wallet
                </button>
              )}
            </div>
          </div>
        </div>
      </nav>
    </header>
  )
}

export default Header

Banner Component

This component contains information about the DAO's current state, such as the total balance and the number of open proposals.

This component also includes the ability to use the contribute function to generate a new proposal. Look at the code below.

import { useState } from 'react'
import { setGlobalState, useGlobalState } from '../store'
import { performContribute } from '../Dominion'
import { toast } from 'react-toastify'

const Banner = () => {
  const [isStakeholder] = useGlobalState('isStakeholder')
  const [proposals] = useGlobalState('proposals')
  const [connectedAccount] = useGlobalState('connectedAccount')
  const [currentUser] = useGlobalState('currentUser')
  const [balance] = useGlobalState('balance')
  const [mybalance] = useGlobalState('mybalance')
  const [amount, setAmount] = useState('')

  const onPropose = () => {
    if (!isStakeholder) return
    setGlobalState('createModal', 'scale-100')
  }

  const onContribute = () => {
    if (!!!amount || amount == '') return
    toast.info('Contribution in progress...')

    performContribute(amount).then((bal) => {
      if (!!!bal.message) {
        setGlobalState('balance', Number(balance) + Number(bal))
        setGlobalState('mybalance', Number(mybalance) + Number(bal))
        setAmount('')
        toast.success('Contribution received')
      }
    })
  }

  const opened = () =>
    proposals.filter(
      (proposal) => new Date().getTime() < Number(proposal.duration + '000')
    ).length

  return (
    <div className="p-8">
      <h2 className="font-semibold text-3xl mb-5">
        {opened()} Proposal{opened() == 1 ? '' : 's'} Currenly Opened
      </h2>
      <p>
        Current DAO Balance: <strong>{balance} Eth</strong> <br />
        Your contributions:{' '}
        <span>
          <strong>{mybalance} Eth</strong>
          {isStakeholder ? ', and you are now a stakeholder 😊' : null}
        </span>
      </p>
      <hr className="my-6 border-gray-300 dark:border-gray-500" />
      <p>
        {isStakeholder
          ? 'You can now raise proposals on this platform 😆'
          : 'Hey, when you contribute upto 1 ether you become a stakeholder 😎'}
      </p>
      <div className="flex flex-row justify-start items-center md:w-1/3 w-full mt-4">
        <input
          type="number"
          className="form-control block w-full px-3 py-1.5
          text-base font-normaltext-gray-700
          bg-clip-padding border border-solid border-gray-300
          rounded transition ease-in-out m-0 shadow-md
          focus:text-gray-500 focus:outline-none
          dark:border-gray-500 dark:bg-transparent"
          placeholder="e.g 2.5 Eth"
          onChange={(e) => setAmount(e.target.value)}
          value={amount}
          required
        />
      </div>
      <div
        className="flex flex-row justify-start items-center space-x-3 mt-4"
        role="group"
      >
        <button
          type="button"
          className={`inline-block px-6 py-2.5
          bg-blue-600 text-white font-medium text-xs
          leading-tight uppercase shadow-md rounded-full
          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 dark:text-blue-500
          dark:border dark:border-blue-500 dark:bg-transparent`}
          data-mdb-ripple="true"
          data-mdb-ripple-color="light"
          onClick={onContribute}
        >
          Contribute
        </button>

        {isStakeholder ? (
          <button
            type="button"
            className={`inline-block px-6 py-2.5
            bg-blue-600 text-white font-medium text-xs
            leading-tight uppercase shadow-md rounded-full
            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 dark:text-blue-500
            dark:border dark:border-blue-500 dark:bg-transparent`}
            data-mdb-ripple="true"
            data-mdb-ripple-color="light"
            onClick={onPropose}
          >
            Propose
          </button>
        ) : null}
        {currentUser &&
        currentUser.uid == connectedAccount.toLowerCase() ? null : (
          <button
            type="button"
            className={`inline-block px-6 py-2.5
            bg-blue-600 text-white font-medium text-xs
            leading-tight uppercase shadow-md rounded-full
            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 dark:border dark:border-blue-500`}
            data-mdb-ripple="true"
            data-mdb-ripple-color="light"
            onClick={() => setGlobalState('loginModal', 'scale-100')}
          >
            Login Chat
          </button>
        )}
      </div>
    </div>
  )
}

export default Banner

Proposals Component

This component contains a list of proposals in our smart contract. Also, enables you to filter between closed and open proposals. At the expiry of a proposal, a payout button becomes available which gives a stakeholder the option of paying out the amount associated with the proposal. See the code below.

import Identicon from 'react-identicons'
import { useState } from 'react'
import { Link } from 'react-router-dom'
import { truncate, useGlobalState, daysRemaining } from '../store'
import { payoutBeneficiary } from '../Dominion'
import { toast } from 'react-toastify'

const Proposals = () => {
  const [data] = useGlobalState('proposals')
  const [proposals, setProposals] = useState(data)

  const deactive = `bg-transparent
  text-blue-600 font-medium text-xs leading-tight
  uppercase hover:bg-blue-700 focus:bg-blue-700
  focus:outline-none focus:ring-0 active:bg-blue-600
  transition duration-150 ease-in-out overflow-hidden
  border border-blue-600 hover:text-white focus:text-white`

  const active = `bg-blue-600
  text-white font-medium text-xs leading-tight
  uppercase hover:bg-blue-700 focus:bg-blue-700
  focus:outline-none focus:ring-0 active:bg-blue-800
  transition duration-150 ease-in-out overflow-hidden
  border border-blue-600`

  const getAll = () => setProposals(data)

  const getOpened = () =>
    setProposals(
      data.filter(
        (proposal) => new Date().getTime() < Number(proposal.duration + '000')
      )
    )

  const getClosed = () =>
    setProposals(
      data.filter(
        (proposal) => new Date().getTime() > Number(proposal.duration + '000')
      )
    )

  const handlePayout = (id) => {
    payoutBeneficiary(id).then((res) => {
      if (!!!res.code) {
        toast.success('Beneficiary successfully Paid Out!')
        window.location.reload()
      }
    })
  }

  return (
    <div className="flex flex-col p-8">
      <div className="flex flex-row justify-center items-center" role="group">
        <button
          aria-current="page"
          className={`rounded-l-full px-6 py-2.5 ${active}`}
          onClick={getAll}
        >
          All
        </button>
        <button
          aria-current="page"
          className={`px-6 py-2.5 ${deactive}`}
          onClick={getOpened}
        >
          Open
        </button>
        <button
          aria-current="page"
          className={`rounded-r-full px-6 py-2.5 ${deactive}`}
          onClick={getClosed}
        >
          Closed
        </button>
      </div>
      <div className="overflow-x-auto sm:-mx-6 lg:-mx-8">
        <div className="py-2 inline-block min-w-full sm:px-6 lg:px-8">
          <div className="h-[calc(100vh_-_20rem)] overflow-y-auto shadow-md rounded-md">
            <table className="min-w-full">
              <thead className="border-b dark:border-gray-500">
                <tr>
                  <th
                    scope="col"
                    className="text-sm font-medium px-6 py-4 text-left"
                  >
                    Created By
                  </th>
                  <th
                    scope="col"
                    className="text-sm font-medium px-6 py-4 text-left"
                  >
                    Title
                  </th>
                  <th
                    scope="col"
                    className="text-sm font-medium px-6 py-4 text-left"
                  >
                    Expires
                  </th>
                  <th
                    scope="col"
                    className="text-sm font-medium px-6 py-4 text-left"
                  >
                    Action
                  </th>
                </tr>
              </thead>
              <tbody>
                {proposals.map((proposal) => (
                  <tr
                    key={proposal.id}
                    className="border-b dark:border-gray-500"
                  >
                    <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
                      <div className="flex flex-row justify-start items-center space-x-3">
                        <Identicon
                          string={proposal.proposer.toLowerCase()}
                          size={25}
                          className="h-10 w-10 object-contain rounded-full mr-3"
                        />
                        <span>{truncate(proposal.proposer, 4, 4, 11)}</span>
                      </div>
                    </td>
                    <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
                      {proposal.title.substring(0, 80) + '...'}
                    </td>
                    <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
                      {new Date().getTime() > Number(proposal.duration + '000')
                        ? 'Expired'
                        : daysRemaining(proposal.duration)}
                    </td>
                    <td
                      className="flex justify-start items-center space-x-3
                      text-sm font-light px-6 py-4 whitespace-nowrap"
                    >
                      <Link
                        to={'/proposal/' + proposal.id}
                        className="dark:border rounded-full px-6 py-2.5 dark:border-blue-600
                          dark:text-blue-600 dark:bg-transparent font-medium text-xs leading-tight
                          uppercase hover:border-blue-700 focus:border-blue-700
                          focus:outline-none focus:ring-0 active:border-blue-800
                          transition duration-150 ease-in-out text-white bg-blue-600"
                      >
                        View
                      </Link>

                      {new Date().getTime() >
                      Number(proposal.duration + '000') ? (
                        proposal.upvotes > proposal.downvotes ? (
                          !proposal.paid ? (
                            <button
                              className="dark:border rounded-full px-6 py-2.5 dark:border-red-600
                                dark:text-red-600 dark:bg-transparent font-medium text-xs leading-tight
                                uppercase hover:border-red-700 focus:border-red-700
                                focus:outline-none focus:ring-0 active:border-red-800
                                transition duration-150 ease-in-out text-white bg-red-600"
                              onClick={() => handlePayout(proposal.id)}
                            >
                              Payout
                            </button>
                          ) : (
                            <button
                              className="dark:border rounded-full px-6 py-2.5 dark:border-green-600
                                  dark:text-green-600 dark:bg-transparent font-medium text-xs leading-tight
                                  uppercase hover:border-green-700 focus:border-green-700
                                  focus:outline-none focus:ring-0 active:border-green-800
                                  transition duration-150 ease-in-out text-white bg-green-600"
                            >
                              Paid
                            </button>
                          )
                        ) : (
                          <button
                              className="dark:border rounded-full px-6 py-2.5 dark:border-red-600
                                  dark:text-red-600 dark:bg-transparent font-medium text-xs leading-tight
                                  uppercase hover:border-red-700 focus:border-red-700
                                  focus:outline-none focus:ring-0 active:border-red-800
                                  transition duration-150 ease-in-out text-white bg-red-600"
                            >
                              Rejected
                            </button>
                        )
                      ) : null}
                    </td>
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
        </div>
      </div>
    </div>
  )
}

export default Proposals

The Proposal Details Component

This component displays information about the current proposal, including the cost. This component allows stakeholders to accept or reject a proposal.

The proposer can form a group, and other platform users can engage in web3.0-style anonymous chat.

This component also includes a bar chart that allows you to see the ratio of acceptees to rejectees. Look at the code below.

import moment from 'moment'
import { useEffect, useState } from 'react'
import { useParams, useNavigate } from 'react-router-dom'
import { toast } from 'react-toastify'
import { getGroup, createNewGroup, joinGroup } from '../CometChat'
import {
  BarChart,
  Bar,
  CartesianGrid,
  XAxis,
  YAxis,
  Legend,
  Tooltip,
} from 'recharts'
import { getProposal, voteOnProposal } from '../Dominion'
import { useGlobalState } from '../store'

const ProposalDetails = () => {
  const { id } = useParams()
  const navigator = useNavigate()
  const [proposal, setProposal] = useState(null)
  const [group, setGroup] = useState(null)
  const [data, setData] = useState([])
  const [isStakeholder] = useGlobalState('isStakeholder')
  const [connectedAccount] = useGlobalState('connectedAccount')
  const [currentUser] = useGlobalState('currentUser')

  useEffect(() => {
    retrieveProposal()
    getGroup(`pid_${id}`).then((group) => {
      if (!!!group.code) setGroup(group)
      console.log(group)
    })
  }, [id])

  const retrieveProposal = () => {
    getProposal(id).then((res) => {
      setProposal(res)
      setData([
        {
          name: 'Voters',
          Acceptees: res?.upvotes,
          Rejectees: res?.downvotes,
        },
      ])
    })
  }

  const onVote = (choice) => {
    if (new Date().getTime() > Number(proposal.duration + '000')) {
      toast.warning('Proposal expired!')
      return
    }

    voteOnProposal(id, choice).then((res) => {
      if (!!!res.code) {
        toast.success('Voted successfully!')
        window.location.reload()
      }
    })
  }

  const daysRemaining = (days) => {
    const todaysdate = moment()
    days = Number((days + '000').slice(0))
    days = moment(days).format('YYYY-MM-DD')
    days = moment(days)
    days = days.diff(todaysdate, 'days')
    return days == 1 ? '1 day' : days + ' days'
  }

  const onEnterChat = () => {
    if (group.hasJoined) {
      navigator(`/chat/${`pid_${id}`}`)
    } else {
      joinGroup(`pid_${id}`).then((res) => {
        if (!!res) {
          navigator(`/chat/${`pid_${id}`}`)
          console.log('Success joining: ', res)
        } else {
          console.log('Error Joining Group: ', res)
        }
      })
    }
  }

  const onCreateGroup = () => {
    createNewGroup(`pid_${id}`, proposal.title).then((group) => {
      if (!!!group.code) {
        toast.success('Group created successfully!')
        setGroup(group)
      } else {
        console.log('Error Creating Group: ', group)
      }
    })
  }

  return (
    <div className="p-8">
      <h2 className="font-semibold text-3xl mb-5">{proposal?.title}</h2>
      <p>
        This proposal is to payout <strong>{proposal?.amount} Eth</strong> and
        currently have{' '}
        <strong>{proposal?.upvotes + proposal?.downvotes} votes</strong> and
        will expire in <strong>{daysRemaining(proposal?.duration)}</strong>
      </p>
      <hr className="my-6 border-gray-300" />
      <p>{proposal?.description}</p>
      <div className="flex flex-row justify-start items-center w-full mt-4 overflow-auto">
        <BarChart width={730} height={250} data={data}>
          <CartesianGrid strokeDasharray="3 3" />
          <XAxis dataKey="name" />
          <YAxis />
          <Tooltip />
          <Legend />
          <Bar dataKey="Acceptees" fill="#2563eb" />
          <Bar dataKey="Rejectees" fill="#dc2626" />
        </BarChart>
      </div>
      <div
        className="flex flex-row justify-start items-center space-x-3 mt-4"
        role="group"
      >
        {isStakeholder ? (
          <>
            <button
              type="button"
              className="inline-block px-6 py-2.5
            bg-blue-600 text-white font-medium text-xs
              leading-tight uppercase rounded-full 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 dark:text-gray-300
              dark:border dark:border-gray-500 dark:bg-transparent"
              data-mdb-ripple="true"
              data-mdb-ripple-color="light"
              onClick={() => onVote(true)}
            >
              Accept
            </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-full 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
              dark:border dark:border-gray-500 dark:bg-transparent"
              data-mdb-ripple="true"
              data-mdb-ripple-color="light"
              onClick={() => onVote(false)}
            >
              Reject
            </button>

            {currentUser &&
            currentUser.uid.toLowerCase() == proposal?.proposer.toLowerCase() &&
            !group ? (
              <button
                type="button"
                className="inline-block px-6 py-2.5
                bg-blue-600 text-white font-medium text-xs
                leading-tight uppercase rounded-full 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
                dark:border dark:border-blue-500"
                data-mdb-ripple="true"
                data-mdb-ripple-color="light"
                onClick={onCreateGroup}
              >
                Create Group
              </button>
            ) : null}
          </>
        ) : null}

        {currentUser && currentUser.uid.toLowerCase() == connectedAccount.toLowerCase() && !!!group?.code && group != null ? (
          <button
            type="button"
            className="inline-block px-6 py-2.5
            bg-blue-600 text-white font-medium text-xs
            leading-tight uppercase rounded-full 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
            dark:border dark:border-blue-500"
            data-mdb-ripple="true"
            data-mdb-ripple-color="light"
            onClick={onEnterChat}
          >
            Chat
          </button>
        ) : null}

        {proposal?.proposer.toLowerCase() != connectedAccount.toLowerCase() &&
        !!!group ? (
          <button
            type="button"
            className="inline-block px-6 py-2.5 bg-blue-600
            dark:bg-transparent text-white font-medium text-xs
            leading-tight uppercase rounded-full shadow-md
            hover:border-blue-700 hover:shadow-lg focus:border-blue-700
            focus:shadow-lg focus:outline-none focus:ring-0
            active:border-blue-800 active:shadow-lg transition
            duration-150 ease-in-out dark:text-blue-500
            dark:border dark:border-blue-500 disabled:bg-blue-300"
            data-mdb-ripple="true"
            data-mdb-ripple-color="light"
            disabled
          >
            Group N/A
          </button>
        ) : null}
      </div>
    </div>
  )
}

export default ProposalDetails

Voters Component

This component simply lists out the stakeholders that have voted on a proposal. The component also affords a user the chance to filter between rejectees and acceptees. See the code below.

import Identicon from 'react-identicons'
import moment from 'moment'
import { useState, useEffect } from 'react'
import { useParams } from 'react-router-dom'
import { truncate } from '../store'
import { listVoters } from '../Dominion'

const Voters = () => {
  const [voters, setVoters] = useState([])
  const [data, setData] = useState([])
  const { id } = useParams()

  const timeAgo = (timestamp) => moment(Number(timestamp + '000')).fromNow()

  const deactive = `bg-transparent
  text-blue-600 font-medium text-xs leading-tight
  uppercase hover:bg-blue-700 focus:bg-blue-700
  focus:outline-none focus:ring-0 active:bg-blue-600
  transition duration-150 ease-in-out overflow-hidden
  border border-blue-600 hover:text-white focus:text-white`

  const active = `bg-blue-600
  text-white font-medium text-xs leading-tight
  uppercase hover:bg-blue-700 focus:bg-blue-700
  focus:outline-none focus:ring-0 active:bg-blue-800
  transition duration-150 ease-in-out overflow-hidden
  border border-blue-600`

  useEffect(() => {
    listVoters(id).then((res) => {
      setVoters(res)
      setData(res)
    })
  }, [id])

  const getAll = () => setVoters(data)

  const getAccepted = () => setVoters(data.filter((vote) => vote.choosen))

  const getRejected = () => setVoters(data.filter((vote) => !vote.choosen))

  return (
    <div className="flex flex-col p-8">
      <div className="flex flex-row justify-center items-center" role="group">
        <button
          aria-current="page"
          className={`rounded-l-full px-6 py-2.5 ${active}`}
          onClick={getAll}
        >
          All
        </button>
        <button
          aria-current="page"
          className={`px-6 py-2.5 ${deactive}`}
          onClick={getAccepted}
        >
          Acceptees
        </button>
        <button
          aria-current="page"
          className={`rounded-r-full px-6 py-2.5 ${deactive}`}
          onClick={getRejected}
        >
          Rejectees
        </button>
      </div>
      <div className="overflow-x-auto sm:-mx-6 lg:-mx-8">
        <div className="py-2 inline-block min-w-full sm:px-6 lg:px-8">
          <div className="h-[calc(100vh_-_20rem)] overflow-y-auto  shadow-md rounded-md">
            <table className="min-w-full">
              <thead className="border-b dark:border-gray-500">
                <tr>
                  <th
                    scope="col"
                    className="text-sm font-medium px-6 py-4 text-left"
                  >
                    Voter
                  </th>
                  <th
                    scope="col"
                    className="text-sm font-medium px-6 py-4 text-left"
                  >
                    Voted
                  </th>
                  <th
                    scope="col"
                    className="text-sm font-medium px-6 py-4 text-left"
                  >
                    Vote
                  </th>
                </tr>
              </thead>
              <tbody>
                {voters.map((voter, i) => (
                  <tr
                    key={i}
                    className="border-b dark:border-gray-500 transition duration-300 ease-in-out"
                  >
                    <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
                      <div className="flex flex-row justify-start items-center space-x-3">
                        <Identicon
                          string={voter.voter.toLowerCase()}
                          size={25}
                          className="h-10 w-10 object-contain rounded-full mr-3"
                        />
                        <span>{truncate(voter.voter, 4, 4, 11)}</span>
                      </div>
                    </td>
                    <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
                      {timeAgo(voter.timestamp)}
                    </td>
                    <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
                      {voter.choosen ? (
                        <button
                          className="border-2 rounded-full px-6 py-2.5 border-blue-600
                          text-blue-600 font-medium text-xs leading-tight
                          uppercase hover:border-blue-700 focus:border-blue-700
                          focus:outline-none focus:ring-0 active:border-blue-800
                          transition duration-150 ease-in-out"
                        >
                          Accepted
                        </button>
                      ) : (
                        <button
                          className="border-2 rounded-full px-6 py-2.5 border-red-600
                          text-red-600 font-medium text-xs leading-tight
                          uppercase hover:border-red-700 focus:border-red-700
                          focus:outline-none focus:ring-0 active:border-red-800
                          transition duration-150 ease-in-out"
                        >
                          Rejected
                        </button>
                      )}
                    </td>
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
        </div>
      </div>
      <div className="mt-4 text-center">
        {voters.length >= 10 ? (
          <button
            aria-current="page"
            className="rounded-full px-6 py-2.5 bg-blue-600
            font-medium text-xs leading-tight
            uppercase hover:bg-blue-700 focus:bg-blue-700
            focus:outline-none focus:ring-0 active:bg-blue-800
            transition duration-150 ease-in-out dark:text-gray-300
            dark:border dark:border-gray-500 dark:bg-transparent"
          >
            Load More
          </button>
        ) : null}
      </div>
    </div>
  )
}

export default Voters

Messages Component

With the power of CometChat SDK combined with this component, users can engage in a one-to-many chat anonymously. Contributors and stakeholders can discuss a proposal further in their decision-making process here. All users maintain their anonymity and are represented by their Identicons.

import Identicon from 'react-identicons'
import { useState, useEffect } from 'react'
import { useNavigate } from 'react-router-dom'
import { truncate, useGlobalState } from '../store'
import { getMessages, sendMessage, CometChat } from '../CometChat'

const Messages = ({ gid }) => {
  const navigator = useNavigate()
  const [connectedAccount] = useGlobalState('connectedAccount')
  const [message, setMessage] = useState('')
  const [messages, setMessages] = useState([])

  useEffect(() => {
    getMessages(gid).then((msgs) => {
      if (!!!msgs.code)
        setMessages(msgs.filter((msg) => msg.category == 'message'))
    })
    listenForMessage(gid)
  }, [gid])

  const listenForMessage = (listenerID) => {
    CometChat.addMessageListener(
      listenerID,
      new CometChat.MessageListener({
        onTextMessageReceived: (message) => {
          setMessages((prevState) => [...prevState, message])
          scrollToEnd()
        },
      })
    )
  }

  const handleMessage = (e) => {
    e.preventDefault()
    sendMessage(gid, message).then((msg) => {
      if (!!!msg.code) {
        setMessages((prevState) => [...prevState, msg])
        setMessage('')
        scrollToEnd()
      }
    })
  }

  const scrollToEnd = () => {
    const elmnt = document.getElementById('messages-container')
    elmnt.scrollTop = elmnt.scrollHeight
  }

  const dateToTime = (date) => {
    let hours = date.getHours()
    let minutes = date.getMinutes()
    let ampm = hours >= 12 ? 'pm' : 'am'
    hours = hours % 12
    hours = hours ? hours : 12
    minutes = minutes < 10 ? '0' + minutes : minutes
    let strTime = hours + ':' + minutes + ' ' + ampm
    return strTime
  }

  return (
    <div className="p-8">
      <div className="flex flex-row justify-start">
        <button
          className="px-4 py-2.5 bg-transparent hover:text-white
        font-bold text-xs leading-tight uppercase
        rounded-full 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={() => navigator(`/proposal/${gid.substr(4)}`)}
        >
          Exit Chat
        </button>
      </div>

      <div
        id="messages-container"
        className="h-[calc(100vh_-_16rem)] overflow-y-auto sm:pr-4 my-3"
      >
        {messages.map((message, i) =>
          message.sender.uid.toLowerCase() != connectedAccount.toLowerCase() ? (
            <div key={i} className="flex flex-row justify-start my-2">
              <div className="flex flex-col bg-transparent w-80 p-3 px-5 rounded-3xl shadow-md">
                <div className="flex flex-row justify-start items-center space-x-2">
                  <Identicon
                    string={message.sender.uid.toLowerCase()}
                    size={25}
                    className="h-10 w-10 object-contain shadow-md rounded-full mr-3"
                  />
                  <span>@{truncate(message.sender.uid, 4, 4, 11)}</span>
                  <small>{dateToTime(new Date(message.sentAt * 1000))}</small>
                </div>
                <small className="leading-tight my-2">{message.text}</small>
              </div>
            </div>
          ) : (
            <div key={i} className="flex flex-row justify-end my-2">
              <div className="flex flex-col bg-transparent w-80 p-3 px-5 rounded-3xl shadow-md shadow-blue-300">
                <div className="flex flex-row justify-start items-center space-x-2">
                  <Identicon
                    string={connectedAccount.toLowerCase()}
                    size={25}
                    className="h-10 w-10 object-contain shadow-md rounded-full mr-3"
                  />
                  <span>@you</span>
                  <small>{dateToTime(new Date(message.sentAt * 1000))}</small>
                </div>
                <small className="leading-tight my-2">{message.text}</small>
              </div>
            </div>
          )
        )}
      </div>

      <form onSubmit={handleMessage} className="flex flex-row">
        <input
          className="w-full bg-transparent 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>
  )
}

export default Messages

Create Proposal Component

This component simply lets you raise a proposal by supplying information on the fields seen in the image above. See the code below.

import { useState } from 'react'
import { FaTimes } from 'react-icons/fa'
import { raiseProposal } from '../Dominion'
import { setGlobalState, useGlobalState } from '../store'
import { toast } from 'react-toastify'

const CreateProposal = () => {
  const [createModal] = useGlobalState('createModal')
  const [title, setTitle] = useState('')
  const [amount, setAmount] = useState('')
  const [beneficiary, setBeneficiary] = useState('')
  const [description, setDescription] = useState('')

  const handleSubmit = (e) => {
    e.preventDefault()
    if (!title || !description || !beneficiary || !amount) return
    const proposal = { title, description, beneficiary, amount }

    raiseProposal(proposal).then((proposed) => {
      if (proposed) {
        toast.success('Proposal created, reloading in progress...')
        closeModal()
        window.location.reload()
      }
    })
  }

  const closeModal = () => {
    setGlobalState('createModal', 'scale-0')
    resetForm()
  }

  const resetForm = () => {
    setTitle('')
    setAmount('')
    setBeneficiary('')
    setDescription('')
  }

  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 ${createModal}`}
    >
      <div className="bg-white dark:bg-[#212936] shadow-xl shadow-[#122643] dark:shadow-gray-500 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">Raise Proposal</p>
            <button
              type="button"
              onClick={closeModal}
              className="border-0 bg-transparent focus:outline-none"
            >
              <FaTimes />
            </button>
          </div>

          <div className="flex flex-row justify-between items-center border border-gray-500 dark:border-gray-500 rounded-xl mt-5">
            <input
              className="block w-full text-sm
              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 border border-gray-500 dark:border-gray-500 rounded-xl mt-5">
            <input
              className="block w-full text-sm
              bg-transparent border-0
              focus:outline-none focus:ring-0"
              type="text"
              name="amount"
              placeholder="e.g 2.5 Eth"
              onChange={(e) => setAmount(e.target.value)}
              value={amount}
              required
            />
          </div>

          <div className="flex flex-row justify-between items-center border border-gray-500 dark:border-gray-500 rounded-xl mt-5">
            <input
              className="block w-full text-sm
              bg-transparent border-0
              focus:outline-none focus:ring-0"
              type="text"
              name="beneficiary"
              placeholder="Beneficiary Address"
              onChange={(e) => setBeneficiary(e.target.value)}
              value={beneficiary}
              required
            />
          </div>

          <div className="flex flex-row justify-between items-center border border-gray-500 dark:border-gray-500 rounded-xl mt-5">
            <textarea
              className="block w-full text-sm resize-none
              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
            className="rounded-lg px-6 py-2.5 bg-blue-600
              text-white font-medium text-xs leading-tight
              uppercase hover:bg-blue-700 focus:bg-blue-700
              focus:outline-none focus:ring-0 active:bg-blue-800
              transition duration-150 ease-in-out mt-5"
            onClick={handleSubmit}
          >
            Submit Proposal
          </button>
        </form>
      </div>
    </div>
  )
}

export default CreateProposal

Authentication Component

This component helps you participate in the chat features. You need to create an account or login in if you’ve already signed up. By logging in, you can be able to participate in a group chat and have some anonymous talk with other participants in a proposal in a web3.0 style. See the code below.

import { FaTimes } from 'react-icons/fa'
import { loginWithCometChat, signInWithCometChat } from '../CometChat'
import { setGlobalState, useGlobalState } from '../store'
import { toast } from 'react-toastify'

const ChatLogin = () => {
  const [loginModal] = useGlobalState('loginModal')
  const [connectedAccount] = useGlobalState('connectedAccount')

  const handleSignUp = () => {
    signInWithCometChat(connectedAccount, connectedAccount).then((user) => {
      if (!!!user.code) {
        toast.success('Account created, now click the login button.')
      } else {
        toast.error(user.message)
      }
    })
  }

  const handleLogin = () => {
    loginWithCometChat(connectedAccount).then((user) => {
      if (!!!user.code) {
        setGlobalState('currentUser', user)
        toast.success('Logged in successful!')
        closeModal()
      } else {
        toast.error(user.message)
      }
    })
  }

  const closeModal = () => {
    setGlobalState('loginModal', '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 ${loginModal}`}
    >
      <div className="bg-white dark:bg-[#212936] shadow-xl shadow-[#122643] dark:shadow-gray-500 rounded-xl w-11/12 md:w-2/5 h-7/12 p-6">
        <div className="flex flex-col">
          <div className="flex flex-row justify-between items-center">
            <p className="font-semibold">Authenticate</p>
            <button
              type="button"
              onClick={closeModal}
              className="border-0 bg-transparent focus:outline-none"
            >
              <FaTimes />
            </button>
          </div>

          <div className="my-2 font-light">
            <span>
              Once you login, you will be enabled to chat with other
              stakeholders to make a well-informed voting.
            </span>
          </div>

          <div
            className="flex flex-row justify-between items-center mt-2"
            role="group"
          >
            <button
              className="rounded-lg px-6 py-2.5 bg-blue-600
              text-white font-medium text-xs leading-tight
              uppercase hover:bg-blue-700 focus:bg-blue-700
              focus:outline-none focus:ring-0 active:bg-blue-800
              transition duration-150 ease-in-out mt-5"
              onClick={handleLogin}
            >
              Login
            </button>

            <button
              className="rounded-lg px-6 py-2.5 bg-transparent
              text-blue-600 font-medium text-xs leading-tight
              uppercase hover:bg-blue-700 hover:text-white focus:bg-blue-700
              focus:outline-none focus:ring-0 active:bg-blue-800
              transition duration-150 ease-in-out mt-5
              border-blue-600"
              onClick={handleSignUp}
            >
              Create Account
            </button>
          </div>
        </div>
      </div>
    </div>
  )
}

export default ChatLogin

Fantastic, let’s make sure the views are well represented…

The Home View

This view includes the header, banner, and proposals components for providing an exceptional DAO user experience. We also used the power of Tailwind CSS to achieve this look. Look at the code below.

import Banner from '../components/Banner'
import ChatLogin from '../components/ChatLogin'
import CreateProposal from '../components/CreateProposal'
import Header from '../components/Header'
import Proposals from '../components/Proposals'
const Home = () => {
  return (
    <>
      <Header />
      <Banner />
      <Proposals />
      <CreateProposal />
      <ChatLogin />
    </>
  )
}
export default Home

The Proposal View

This view couples together the header, proposal details, and voters component for rendering a smooth presentation of a singular component. See the code below.

import Header from '../components/Header'
import ProposalDetails from '../components/ProposalDetails'
import Voters from '../components/Voters'
const Proposal = () => {
  return (
    <>
      <Header />
      <ProposalDetails />
      <Voters />
    </>
  )
}
export default Proposal

The Chat View

Lastly, the chat view incorporates the header and messages component for rendering a quality chat interface. See the code below.

import { useParams, useNavigate } from 'react-router-dom'
import { useEffect, useState } from 'react'
import { getGroup } from '../CometChat'
import { toast } from 'react-toastify'
import Header from '../components/Header'
import Messages from '../components/Messages'
const Chat = () => {
  const { gid } = useParams()
  const navigator = useNavigate()
  const [group, setGroup] = useState(null)
  useEffect(() => {
    getGroup(gid).then((group) => {
      if (!!!group.code) {
        setGroup(group)
      } else {
        toast.warning('Please join the group first!')
        navigator(`/proposal/${gid.substr(4)}`)
      }
    })
  }, [gid])
  return (
    <>
      <Header />
      <Messages gid={gid} />
    </>
  )
}
export default Chat

Amazing, don’t forget to update the App.jsx file too.

The App Component Replace the App component with the code below.

import { useEffect, useState } from 'react'
import { Routes, Route } from 'react-router-dom'
import { loadWeb3 } from './Dominion'
import { ToastContainer } from 'react-toastify'
import { isUserLoggedIn } from './CometChat'
import Home from './views/Home'
import Proposal from './views/Proposal'
import Chat from './views/Chat'
import 'react-toastify/dist/ReactToastify.min.css'

const App = () => {
  const [loaded, setLoaded] = useState(false)

  useEffect(() => {
    loadWeb3().then((res) => {
      if (res) setLoaded(true)
    })
    isUserLoggedIn()
  }, [])

  return (
    <div className="min-h-screen bg-white text-gray-900 dark:bg-[#212936] dark:text-gray-300">

      {loaded ? (
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="proposal/:id" element={<Proposal />} />
          <Route path="chat/:gid" element={<Chat />} />
        </Routes>
      ) : null}

      <ToastContainer
        position="top-center"
        autoClose={5000}
        hideProgressBar={false}
        newestOnTop={false}
        closeOnClick
        rtl={false}
        pauseOnFocusLoss
        draggable
        pauseOnHover
      />
    </div>
  )
}
export default App

On the src, >> directory paste the following codes in their respective files.

Index.jsx File

import React from 'react'
import ReactDOM from 'react-dom'
import { BrowserRouter } from 'react-router-dom'
import './index.css'
import App from './App'
import { initCometChat } from './CometChat'
initCometChat().then(() => {
  ReactDOM.render(
    <BrowserRouter>
      <App />
    </BrowserRouter>,
    document.getElementById('root')
  )
})

Index.css File

@import url('https://fonts.googleapis.com/css2?family=Open+Sans:wght@300;400;500;600;700&display=swap');
* html {
  padding: 0;
  margin: 0;
  box-sizing: border-box;
}
body {
  margin: 0;
  font-family: 'Open Sans', sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}
@tailwind base;
@tailwind components;
@tailwind utilities;

CometChat.jsx

import Web3 from 'web3'
import { setGlobalState, getGlobalState } from './store'
import DominionDAO from './abis/DominionDAO.json'

const { ethereum } = window

const connectWallet = async () => {
  try {
    if (!ethereum) return alert('Please install Metamask')
    const accounts = await ethereum.request({ method: 'eth_requestAccounts' })
    setGlobalState('connectedAccount', accounts[0])
  } catch (error) {
    console.log(JSON.stringify(error))
  }
}

const raiseProposal = async ({ title, description, beneficiary, amount }) => {
  try {
    amount = window.web3.utils.toWei(amount.toString(), 'ether')
    const contract = getGlobalState('contract')
    const account = getGlobalState('connectedAccount')

    let proposal = await contract.methods
      .createProposal(title, description, beneficiary, amount)
      .send({ from: account })

    return proposal
  } catch (error) {
    console.log(error.message)
    return error
  }
}

const performContribute = async (amount) => {
  try {
    amount = window.web3.utils.toWei(amount.toString(), 'ether')
    const contract = getGlobalState('contract')
    const account = getGlobalState('connectedAccount')

    let balance = await contract.methods
      .contribute()
      .send({ from: account, value: amount })
    balance = window.web3.utils.fromWei(
      balance.events.Action.returnValues.amount
    )
    return balance
  } catch (error) {
    console.log(error.message)
    return error
  }
}

const retrieveProposal = async (id) => {
  const web3 = window.web3
  try {
    const contract = getGlobalState('contract')
    const proposal = await contract.methods.getProposal(id).call().wait()
    return {
      id: proposal.id,
      amount: web3.utils.fromWei(proposal.amount),
      title: proposal.title,
      description: proposal.description,
      paid: proposal.paid,
      passed: proposal.passed,
      proposer: proposal.proposer,
      upvotes: Number(proposal.upvotes),
      downvotes: Number(proposal.downvotes),
      beneficiary: proposal.beneficiary,
      executor: proposal.executor,
      duration: proposal.duration,
    }
  } catch (error) {
    console.log(error)
  }
}

const reconstructProposal = (proposal) => {
  return {
    id: proposal.id,
    amount: window.web3.utils.fromWei(proposal.amount),
    title: proposal.title,
    description: proposal.description,
    paid: proposal.paid,
    passed: proposal.passed,
    proposer: proposal.proposer,
    upvotes: Number(proposal.upvotes),
    downvotes: Number(proposal.downvotes),
    beneficiary: proposal.beneficiary,
    executor: proposal.executor,
    duration: proposal.duration,
  }
}

const getProposal = async (id) => {
  try {
    const proposals = getGlobalState('proposals')
    return proposals.find((proposal) => proposal.id == id)
  } catch (error) {
    console.log(error)
  }
}

const voteOnProposal = async (proposalId, supported) => {
  try {
    const contract = getGlobalState('contract')
    const account = getGlobalState('connectedAccount')
    const vote = await contract.methods
      .performVote(proposalId, supported)
      .send({ from: account })
    return vote
  } catch (error) {
    console.log(error)
    return error
  }
}

const listVoters = async (id) => {
  try {
    const contract = getGlobalState('contract')
    const votes = await contract.methods.getVotesOf(id).call()
    return votes
  } catch (error) {
    console.log(error)
  }
}

const payoutBeneficiary = async (id) => {
  try {
    const contract = getGlobalState('contract')
    const account = getGlobalState('connectedAccount')
    const balance = await contract.methods
      .payBeneficiary(id)
      .send({ from: account })
    return balance
  } catch (error) {
    return error
  }
}

const loadWeb3 = async () => {
  try {
    if (!ethereum) return alert('Please install Metamask')

    window.web3 = new Web3(ethereum)
    await ethereum.request({ method: 'eth_requestAccounts' })
    window.web3 = new Web3(window.web3.currentProvider)

    const web3 = window.web3
    const accounts = await web3.eth.getAccounts()
    setGlobalState('connectedAccount', accounts[0])

    const networkId = await web3.eth.net.getId()
    const networkData = DominionDAO.networks[networkId]

    if (networkData) {
      const contract = new web3.eth.Contract(
        DominionDAO.abi,
        networkData.address
      )
      const isStakeholder = await contract.methods
        .isStakeholder()
        .call({ from: accounts[0] })
      const proposals = await contract.methods.getProposals().call()
      const balance = await contract.methods.daoBalance().call()
      const mybalance = await contract.methods
        .getBalance()
        .call({ from: accounts[0] })

      setGlobalState('contract', contract)
      setGlobalState('balance', web3.utils.fromWei(balance))
      setGlobalState('mybalance', web3.utils.fromWei(mybalance))
      setGlobalState('isStakeholder', isStakeholder)
      setGlobalState('proposals', structuredProposals(proposals))
    } else {
      window.alert('DominionDAO contract not deployed to detected network.')
    }
    return true
  } catch (error) {
    alert('Please connect your metamask wallet!')
    console.log(error)
    return false
  }
}

const structuredProposals = (proposals) => {
  const web3 = window.web3
  return proposals
    .map((proposal) => ({
      id: proposal.id,
      amount: web3.utils.fromWei(proposal.amount),
      title: proposal.title,
      description: proposal.description,
      paid: proposal.paid,
      passed: proposal.passed,
      proposer: proposal.proposer,
      upvotes: Number(proposal.upvotes),
      downvotes: Number(proposal.downvotes),
      beneficiary: proposal.beneficiary,
      executor: proposal.executor,
      duration: proposal.duration,
    }))
    .reverse()
}

export {
  loadWeb3,
  connectWallet,
  performContribute,
  raiseProposal,
  retrieveProposal,
  voteOnProposal,
  getProposal,
  listVoters,
  payoutBeneficiary,
}

Starting Up the Development Environment

STEP 1: Spin up some test account with ganache-cli using the command below:

ganache-cli -a

This will create some test accounts with 100 fake ethers loaded into each account, of course, these are for testing purposes only. See the image below:

STEP 2: Add a local test network with Metamask as seen in the image below.

STEP 3: Click on the account icon and select import account.

Copy about five of the private keys and add them one after the other to your local test network. See the image below.

Observe the new account added to your local test network with 100 ETH preloaded. Make sure you add about five accounts so you can do a maximum test. See the image below.

Smart Contract Deployment

Now open a new terminal and run the command below.

truffle migrate
# or 
truffle migrate --network rinkeby

The above command will deploy your smart contract to your local or the Infuria rinkeby test network.

Next, open up another terminal and spin up the react app with yarn start.

Conclusion

Hurray, we’ve just completed an amazing tutorial for developing a decentralized autonomous organization.

If you enjoyed this tutorial and would like to have me as your private mentor, kindly book your classes with me.

Till next time, all the best.

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 on his website.


Written by daltonic | Youtuber | Blockchain Developer | Writer | Instructor
Published by HackerNoon on 2022/06/22