DEV Community

Cover image for How to Build a Glorious Web3.0 DAO with React, Solidity, and CometChat
Gospel Darlington
Gospel Darlington

Posted on • Edited on

10 3

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

What you will be building, see demo and git repo here…

Dominion DAO Demo

The Chat Interface

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. Book your private classes with me.

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…

Check out my YouTube channel for FREE web3 tutorials now.

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, and if you haven’t, install it from HERE. Next, run the code on the terminal to confirm it is installed.

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

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

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"
]
}
}
view raw package.json hosted with ❤ by GitHub

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.

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

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 placeholders keys with their appropriate values.

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

Configuring Infuria App

STEP 1:
Head to Infuria, create an account.

Login to your infuria account

STEP 2:
From the dashboard create a new project.

Create a new project step 1

Create a new project step 2

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

Rinkeby Testnet Keys

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

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

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

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;
}
}
view raw DominionDAO.sol hosted with ❤ by GitHub

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

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";
Enter fullscreen mode Exit fullscreen mode

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

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

raisedProposals keep track of all proposals submitted to our smart contract. stakeholderVotes as its name imply keep 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;
}
Enter fullscreen mode Exit fullscreen mode

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
);
Enter fullscreen mode Exit fullscreen mode

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);
    _;
}
Enter fullscreen mode Exit fullscreen mode

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
    );
}
Enter fullscreen mode Exit fullscreen mode

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
    );
}
Enter fullscreen mode Exit fullscreen mode

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");
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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
        );
    }
Enter fullscreen mode Exit fullscreen mode

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];
    }
}
Enter fullscreen mode Exit fullscreen mode

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

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

This function retrieves a particular proposal by Id.

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

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];
}
Enter fullscreen mode Exit fullscreen mode

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];
}
Enter fullscreen mode Exit fullscreen mode

This returns the amount of money contributed by stakeholders.

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

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];
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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];
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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

Configuring the Deployment Script

One more thing to do with for 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)
}
Enter fullscreen mode Exit fullscreen mode

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

Dark Mode

Light Mode

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
view raw Header.jsx hosted with ❤ by GitHub

Banner Component

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
view raw Banner.jsx hosted with ❤ by GitHub

Proposals Component

Proposals Components

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
view raw Proposals.jsx hosted with ❤ by GitHub

The Proposal Details Component

Proposal Details

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 chat 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

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
view raw Voters.jsx hosted with ❤ by GitHub

Messages Component

The 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
view raw Messages.jsx hosted with ❤ by GitHub

Create Proposal Component

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

Login Chat 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
view raw ChatLogin.jsx hosted with ❤ by GitHub

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

The Home View

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

The Proposal View

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

The Chat View

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

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

On the src >> directory paste the following codes in there 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')
  )
})
Enter fullscreen mode Exit fullscreen mode

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

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,
}
view raw Dominion.js hosted with ❤ by GitHub

Starting Up the Development Environment

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

ganache-cli -a
Enter fullscreen mode Exit fullscreen mode

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:

Generated Private Keys

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

Localhost Network

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

Step one

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

Importing Private Keys from ganache cli

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.

Free Test Ethers

Smart Contract Deployment

Now open a new terminal and run the command below.

truffle migrate
# or 
truffle migrate --network rinkeby
Enter fullscreen mode Exit fullscreen mode

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.

Watch my FREE web3 tutorials on Youtube now.

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.

Sentry image

See why 4M developers consider Sentry, “not bad.”

Fixing code doesn’t have to be the worst part of your day. Learn how Sentry can help.

Learn more

Top comments (6)

Collapse
 
dockbuddies profile image
Duckbuddies | live on opensea

bro nice project man really cool I just hove question actually two is there anyway I can make it that wen a user buys an nft he or she has the choice to either list the nft or keep it and is there anyway I can get percentage fee from sales like for example let's say you sell your nft just like as opensea takes %2.5 percentage is there anyway I can also do that.. thanks for your help in advance I will really like if e help me ❤️

Collapse
 
daltonic profile image
Gospel Darlington

Yes, that is totally implementable. ☺

Collapse
 
dockbuddies profile image
Duckbuddies | live on opensea

how do i go about it i dm you on twitter check

Thread Thread
 
daltonic profile image
Gospel Darlington

I do have a private class and a consultancy service in the link below.

You can lear or ask me any questions there when you book a time with me.

Thanks
buymeacoffee.com/web3classes

Thread Thread
 
dockbuddies profile image
Duckbuddies | live on opensea

That's awesome 😎 bro nice when I have the money I will really love 😘 to buy it but another thing you can also create a YouTube channel were you make videos like this you will go viral easily because you are very talented. Another thing bro I live in port Harcourt as you ❤️❤️

Thread Thread
 
daltonic profile image
Gospel Darlington

Thanks man, my YouTube videos are coming soon.

Image of Docusign

🛠️ Bring your solution into Docusign. Reach over 1.6M customers.

Docusign is now extensible. Overcome challenges with disconnected products and inaccessible data by bringing your solutions into Docusign and publishing to 1.6M customers in the App Center.

Learn more