paint-brush
How to Build an Awesome Web3 Crowd-Funding Platform with React, Solidity, and CometChatby@daltonic
2,382 reads
2,382 reads

How to Build an Awesome Web3 Crowd-Funding Platform with React, Solidity, and CometChat

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

Too Long; Didn't Read

A valuable skill in Web3 development is all you need to secure your chances of being relevant in today’s tech world. The Web3 economy is booming and the best thing you can do for yourself right now is to learn the craft.

Companies Mentioned

Mention Thumbnail
Mention Thumbnail

Coin Mentioned

Mention Thumbnail
featured image - How to Build an Awesome Web3 Crowd-Funding Platform with React, Solidity, and CometChat
Darlington Gospel  HackerNoon profile picture

A valuable skill in Web3 development is all you need to secure your chances of being relevant in today’s tech-world.


The Web3 economy is booming and the best thing you can do for yourself right now is to learn the craft.


How do you develop a smart contract for your Dapp? How do you best design the Dapp interface? How do you connect the frontend to the backend that sits on the blockchain? All these questions will be answered in this tutorial.


Here’s what you will be building, see demo on the Rinkeby test network and git repo here…


Add Project



Before we proceed, don’t forget to Subscribe to my YouTube channel to learn how to build a Web3 app from scratch.


I also offer private and specialized classes for serious folks who want to learn one-on-one from a mentor. Book your Web3 classes here.


With that said, let’s jump into the tutorial.

Prerequisite

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


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

Installing Dependencies

Clone the project from the repository below and run the following commands.


git clone https://github.com/Daltonic/genesis <PROJECT_NAME>
cd <PROJECT_NAME>
yarn install


Executing the codes below will add the following dependencies and versions to your project.


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

Configuring CometChat SDK

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

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

Register a new CometChat account if you do not have one


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

Log in to the CometChat Dashboard with your created account


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

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

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

Configuring Infuria App

STEP 1: Head to Infuria, and 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 or HTTPS endpoint URL to your .env file.

Rinkeby Testnet Keys


After that, enter your Metamask secret phrase and preferred account's private key. If you followed the instructions correctly, your environment variables should now look like this.


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

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

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

Accessing Your Metamask Private Key

STEP 1: Make sure Rinkeby is selected as the test network in your Metamask browser extension. Then, on the preferred account, click the vertical dotted line and choose account details. Please see the image below.


Step One

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

Step Two

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


Step Three

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

Step Four

ENDPOINT_URL=***************************
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 Genesis Smart Contract

Now let’s create the smart contract for this project. Before doing that, you need to understand the scope of this project.


We’re creating a crowdfunding platform where startups and projects with a great cause can raise funds. This platform help founders with the capital to start up their business, it is called Genesis, which means the BEGINING!


Create a folder called contracts in the src directory of your project. Now, head to src >> contracts and create a file named Genesis.sol and paste the code below inside of it.


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

contract Genesis {
    address public owner;
    uint public projectTax;
    uint public projectCount;
    uint public balance;
    statsStruct public stats;
    projectStruct[] projects;

    mapping(address => projectStruct[]) projectsOf;
    mapping(uint => backerStruct[]) backersOf;
    mapping(uint => bool) public projectExist;

    enum statusEnum {
        OPEN,
        APPROVED,
        REVERTED,
        DELETED,
        PAIDOUT
    }

    struct statsStruct {
        uint totalProjects;
        uint totalBacking;
        uint totalDonations;
    }

    struct backerStruct {
        address owner;
        uint contribution;
        uint timestamp;
        bool refunded;
    }

    struct projectStruct {
        uint id;
        address owner;
        string title;
        string description;
        string imageURL;
        uint cost;
        uint raised;
        uint timestamp;
        uint expiresAt;
        uint backers;
        statusEnum status;
    }

    modifier ownerOnly(){
        require(msg.sender == owner, "Owner reserved only");
        _;
    }

    event Action (
        uint256 id,
        string actionType,
        address indexed executor,
        uint256 timestamp
    );

    constructor(uint _projectTax) {
        owner = msg.sender;
        projectTax = _projectTax;
    }

    function createProject(
        string memory title,
        string memory description,
        string memory imageURL,
        uint cost,
        uint expiresAt
        ) public returns (bool) {
        require(bytes(title).length > 0, "Title cannot be empty");
        require(bytes(description).length > 0, "Description cannot be empty");
        require(bytes(imageURL).length > 0, "ImageURL cannot be empty");

        projectStruct memory project;
        project.id = projectCount;
        project.owner = msg.sender;
        project.title = title;
        project.description = description;
        project.imageURL = imageURL;
        project.cost = cost;
        project.timestamp = block.timestamp;
        project.expiresAt = expiresAt;

        projects.push(project);
        projectExist[projectCount] = true;
        projectsOf[msg.sender].push(project);
        stats.totalProjects += 1;

        emit Action (
            projectCount++,
            "PROJECT CREATED",
            msg.sender,
            block.timestamp
        );
        return true;
    }

    function updateProject(
        uint id,
        string memory title,
        string memory description,
        string memory imageURL,
        uint expiresAt
        ) public returns (bool) {
        require(msg.sender == projects[id].owner, "Unauthorized Entity");
        require(projectExist[id], "Project not found");
        require(bytes(title).length > 0, "Title cannot be empty");
        require(bytes(description).length > 0, "Description cannot be empty");
        require(bytes(imageURL).length > 0, "ImageURL cannot be empty");

        projects[id].title = title;
        projects[id].description = description;
        projects[id].imageURL = imageURL;
        projects[id].expiresAt = expiresAt;

        emit Action (
            id,
            "PROJECT UPDATED",
            msg.sender,
            block.timestamp
        );

        return true;
    }

    function deleteProject(uint id) public returns (bool) {
        require(projectExist[id], "Project not found");
        require(projects[id].status == statusEnum.OPEN, "Project no longer opened");
        require(
            msg.sender == projects[id].owner ||
            msg.sender == owner,
            "Unauthorized Entity"
        );

        projects[id].status = statusEnum.DELETED;
        performRefund(id);

        emit Action (
            id,
            "PROJECT DELETED",
            msg.sender,
            block.timestamp
        );

        return true;
    }

    function performPayout(uint id) internal {
        uint raised = (projects[id].raised * projectTax) / 100;
        uint tax = (raised * projectTax) / 100;

        projects[id].status = statusEnum.PAIDOUT;

        payTo(projects[id].owner, tax);
        payTo(owner, (raised - tax));

        balance -= projects[id].raised;

        emit Action (
            id,
            "PROJECT PAID OUT",
            msg.sender,
            block.timestamp
        );
    }

    function payOutProject(uint id) public returns (bool) {
        require(projects[id].status == statusEnum.APPROVED, "Project no longer APPROVED");
        require(projects[id].raised >= balance, "Insufficient Fund");
        require(
            msg.sender == projects[id].owner ||
            msg.sender == owner,
            "Unauthorized Entity"
        );

        performPayout(id);
        return true;
    }

    function performRefund(uint id) internal {
        for(uint i = 0; i < backersOf[id].length; i++) {
            address _owner = backersOf[id][i].owner;
            uint _contribution = backersOf[id][i].contribution;
            
            backersOf[id][i].refunded = true;
            backersOf[id][i].timestamp = block.timestamp;
            payTo(_owner, _contribution);

            stats.totalBacking -= 1;
            stats.totalDonations -= _contribution;
        }
    }

    function backProject(uint id) public payable returns (bool) {
        require(msg.value > 0 ether, "Ether must be greater than zero");
        require(projectExist[id], "Project not found");
        require(projects[id].status == statusEnum.OPEN, "Project no longer opened");

        stats.totalBacking += 1;
        stats.totalDonations += msg.value;
        projects[id].raised += msg.value;
        projects[id].backers += 1;

        backersOf[id].push(
            backerStruct(
                msg.sender,
                msg.value,
                block.timestamp,
                false
            )
        );

        emit Action (
            id,
            "PROJECT BACKED",
            msg.sender,
            block.timestamp
        );

        if(projects[id].raised >= projects[id].cost) {
            projects[id].status = statusEnum.APPROVED;
            balance += projects[id].raised;
            performPayout(id);
            return true;
        }

        if(block.timestamp >= projects[id].expiresAt) {
            projects[id].status = statusEnum.REVERTED;
            performRefund(id);
            return true;
        }

        return true;
    }

    function requestRefund(uint id) public returns (bool) {
        require(
            projects[id].status != statusEnum.REVERTED ||
            projects[id].status != statusEnum.DELETED,
            "Project not marked as revert or delete"
        );
        
        projects[id].status = statusEnum.REVERTED;
        performRefund(id);
        return true;
    }

    function changeTax(uint _taxPct) public ownerOnly {
        projectTax = _taxPct;
    }

    function getProject(uint id) public view returns (projectStruct memory) {
        require(projectExist[id], "Project not found");

        return projects[id];
    }
    
    function getProjects() public view returns (projectStruct[] memory) {
        return projects;
    }
    
    function getBackers(uint id) public view returns (backerStruct[] memory) {
        return backersOf[id];
    }

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


Now let’s discuss the content of this smart contract segment by segment.


Defining Essential Variables

address public owner;
uint public projectTax;
uint public projectCount;
uint public balance;

statsStruct public stats;
projectStruct[] projects;

mapping(address => projectStruct[]) projectsOf;
mapping(uint => backerStruct[]) backersOf;
mapping(uint => bool) public projectExist;


In the above block of codes, we defined the various data types and structures for storing data on the blockchain.


For the single data types, we have the owner variable, project tax, count, and the available balance in our smart contract.


For the multi-data types, we have **statsStruct** and **projectStruct** which defines the model of each project and the statistics in our smart contract.


The mapping bear records of the project owners, backers, also the existence of a project.


If you are new to Solidity, I have a full FREE course on YouTube called, Mastering Solidity Basics. So do check it out, like, and subscribe!


Setting up the Essential Structs and Event

enum statusEnum {
    OPEN,
    APPROVED,
    REVERTED,
    DELETED,
    PAIDOUT
}

struct statsStruct {
    uint totalProjects;
    uint totalBacking;
    uint totalDonations;
}

struct backerStruct {
    address owner;
    uint contribution;
    uint timestamp;
    bool refunded;
}

struct projectStruct {
    uint id;
    address owner;
    string title;
    string description;
    string imageURL;
    uint cost;
    uint raised;
    uint timestamp;
    uint expiresAt;
    uint backers;
    statusEnum status;
}

 event Action (
    uint256 id,
    string actionType,
    address indexed executor,
    uint256 timestamp
);


The above code contains structures for declaring enums, structs, and events. The enum was defined to contain the various states a project can have in our platform.


The **statsStruct** contains the code a statistic should have, such as the total number of donations, backings, and projects.


The **backerStruct** on the other hand, contains data types each person backing a project should have. A backer should definitely have an address, how much he is donating, the time of donation, and a refund status to show if the owner was refunded his money.


The **projectStruct** describes what each project must contain and lastly, the event is a dynamic one to output information according to the calling function.


The Constructor and Owner Modifier function

modifier ownerOnly(){
    require(msg.sender == owner, "Owner reserved only");
    _;
}

constructor(uint _projectTax) {
    owner = msg.sender;
    projectTax = _projectTax;
}


Whenever the **ownerOnly()** modifier is attached to a calling function, it restricts its accessibility to the deployer of the smart contract only.


The **constructor()** function, on the other hand, initializes the owner state variable along with the tax per project. This tax is what will be charged per an approved project.


The Project Create Function

function createProject(
    string memory title,
    string memory description,
    string memory imageURL,
    uint cost,
    uint expiresAt
    ) public returns (bool) {
    require(bytes(title).length > 0, "Title cannot be empty");
    require(bytes(description).length > 0, "Description cannot be empty");
    require(bytes(imageURL).length > 0, "ImageURL cannot be empty");
    
    projectStruct memory project;
    project.id = projectCount;
    project.owner = msg.sender;
    project.title = title;
    project.description = description;
    project.imageURL = imageURL;
    project.cost = cost;
    project.timestamp = block.timestamp;
    project.expiresAt = expiresAt;
    projects.push(project);
    projectExist[projectCount] = true;
    projectsOf[msg.sender].push(project);
    stats.totalProjects += 1;

    emit Action (
        projectCount++,
        "PROJECT CREATED",
        msg.sender,
        block.timestamp
    );
    return true;
}


This method takes in project information and pushes it to the project's array. Next, it sets other records such as the statistics and project owners. Lastly, an event is emitted bearing some records of the just created project.


The Project Updation Function

function updateProject(
    uint id,
    string memory title,
    string memory description,
    string memory imageURL,
    uint expiresAt
    ) public returns (bool) {
    require(msg.sender == projects[id].owner, "Unauthorized Entity");
    require(projectExist[id], "Project not found");
    require(bytes(title).length > 0, "Title cannot be empty");
    require(bytes(description).length > 0, "Description cannot be empty");
    require(bytes(imageURL).length > 0, "ImageURL cannot be empty");

    projects[id].title = title;
    projects[id].description = description;
    projects[id].imageURL = imageURL;
    projects[id].expiresAt = expiresAt;

    emit Action (
        id,
        "PROJECT UPDATED",
        msg.sender,
        block.timestamp
    );
    return true;
}


This function updates a project according to the project Id using the title, description, image URL, and expiration time. On completion, it emits a Project Updated event.


The Deletion Function

function deleteProject(uint id) public returns (bool) {
    require(projectExist[id], "Project not found");
    require(projects[id].status == statusEnum.OPEN, "Project no longer opened");
    require(
        msg.sender == projects[id].owner ||
        msg.sender == owner,
        "Unauthorized Entity"
    );

    projects[id].status = statusEnum.DELETED;
    performRefund(id);

    emit Action (
        id,
        "PROJECT DELETED",
        msg.sender,
        block.timestamp
    );
    return true;
}


The function above marks a project as completed, which by so doing, performs a refund operation sending back all the donations to the appropriate donors.


Other Functions These are the duties of the following functions;

  • PerformPayout(): Releases the contributed money to the project owner as well as paying the platform’s task.
  • PayOutProject(): Performs criteria checks and calls the performPayout() function internally.
  • PerformRefund(): Returns money to the backer of a specific project.
  • RequestRefund(): Performs criteria checks and calls the performRefund() function.
  • BackProject(): Makes donation to a specific project.
  • ChangeTax(): Changes the platform’s task for projects.
  • GetProject(): Returns a particular project detail by Id.
  • GetProjects(): Returns all projects on the smart contract.
  • GetBackers(): Returns a list of backers for a particular project.
  • PayTo(): Sends a specific amount of money to a specific address.

Fantastic, there you have it for the smart contract, now let’s get into merging it with the React frontend.

Configuring the Deployment Script

One more thing to do with the smart contract is to configure the deployment script. Head to the scripts >> deploy.js and paste the codes below inside of it.


const hre = require('hardhat')
const fs = require('fs')

async function main() {
  const taxFee = 5 // unit is in percent
  const Contract = await hre.ethers.getContractFactory('Genesis')
  const contract = await Contract.deploy(taxFee)
  await contract.deployed()
  const address = JSON.stringify({ address: contract.address }, null, 4)

  fs.writeFile('./src/abis/contractAddress.json', address, 'utf8', (err) => {
    if (err) {
      console.error(err)
      return
    }
    console.log('Deployed contract address', contract.address)
  })
}

main().catch((error) => {
  console.error(error)
  process.exitCode = 1
})


The code above when executed compiles the smart contract and deploys it to the specified network.


Reviewing the Hardhat Config File Head to the root of the project and open up hardhat.config.js.


require("@nomiclabs/hardhat-waffle");
require('dotenv').config()

module.exports = {
  defaultNetwork: "localhost",
  networks: {
    hardhat: {
    },
    localhost: {
      url: "http://127.0.0.1:8545"
    },
    rinkeby: {
      url: process.env.ENDPOINT_URL,
      accounts: [process.env.DEPLOYER_KEY]
    }
  },
  solidity: {
    version: '0.8.11',
    settings: {
      optimizer: {
        enabled: true,
        runs: 200
      }
    }
  },
  paths: {
    sources: "./src/contracts",
    artifacts: "./src/abis"
  },
  mocha: {
    timeout: 40000
  }
}


Pay heed to the two objects called networks and paths. We specified two networks which are the localhost and the Rinkeby test networks. The localhost is for development purposes and the Rinkeby is for productions/staging purposes.


The environment variables called ENDPOINT_URL and DEVELOPER_KEY were gotten from Infuria and Metamask respectively.


For the path, we are specifying where our smart contracts live and where to dump the generated artifacts. For this case, we specified that they should be kept in the src folder.


Great, lastly spin up your hardhat blockchain server and deploy the smart contract using the commands below.


yarn hardhat node
yarn hardhat run scripts/deploy.js --network localhost

Developing the Frontend

Let’s put together the various components of the React frontend step by step.


Components Now create a folder called component in the src directory. This is where all the components will live.


Header Component

Header Component


This component beautifully features two items, the logo which helps you navigate back to the home page, and the **connect wallet** button which helps you connect to your Metamask wallet. Create a file named Header.jsx and paste the codes below inside of it.


import { Link } from 'react-router-dom'
import { TbBusinessplan } from 'react-icons/tb'
import { connectWallet } from '../Genesis'
import { useGlobalState, truncate } from '../store'

const Header = () => {
  const [connectedAccount] = useGlobalState('connectedAccount')

  return (
    <header
      className="w-full flex flex-wrap items-center justify-between p-5 bg-white
    text-gray-500 hover:text-gray-700 focus:text-gray-700 shadow-lg fixed top-0 left-0 right-0"
    >
      <div className="container-fluid w-full flex flex-wrap items-center justify-between">
        <div className="container-fluid">
          <Link
            to="/"
            className="flex justify-start items-center text-xl text-black"
          >
            <span className="font-body mr-1">Genesis</span>
            <TbBusinessplan />
          </Link>
        </div>
        <div className="flex space-x-2 justify-center">
          {connectedAccount ? (
            <button
              type="button"
              data-mdb-ripple="true"
              data-mdb-ripple-color="light"
              className="inline-block px-6 py-2.5 bg-green-600 text-white font-medium text-xs 
              leading-tight uppercase rounded-full shadow-md hover:bg-green-700 hover:shadow-lg
              focus:bg-green-700 focus:shadow-lg focus:outline-none focus:ring-0
              active:bg-green-800 active:shadow-lg transition duration-150 ease-in-out"
            >
              {truncate(connectedAccount, 4, 4, 11)}
            </button>
          ) : (
            <button
              type="button"
              data-mdb-ripple="true"
              data-mdb-ripple-color="light"
              className="inline-block px-6 py-2.5 bg-green-600 text-white font-medium text-xs 
              leading-tight uppercase rounded-full shadow-md hover:bg-green-700 hover:shadow-lg
              focus:bg-green-700 focus:shadow-lg focus:outline-none focus:ring-0
              active:bg-green-800 active:shadow-lg transition duration-150 ease-in-out"
              onClick={connectWallet}
            >
              Connect Wallet
            </button>
          )}
        </div>
      </div>
    </header>
  )
}

export default Header


Hero Component

The Hero Component


This component is responsible for displaying statistics about the projects created on the platform including the total number of backings and donations.


To replicate, create a component called Hero.jsx in the components folder and paste the codes below inside of it.


import { setGlobalState, useGlobalState } from '../store'

const Hero = () => {
  const [stats] = useGlobalState('stats')

  const scrollToProjects = () => {
    document.getElementById('projects').scrollIntoView()
  }

  return (
    <div className="text-center bg-white text-gray-800 py-24 px-6">
      <h1 className="text-5xl md:text-6xl xl:text-7xl font-bold tracking-tight mb-12">
        <span className="capitalize">Bring creative projects to life on</span>
        <br />
        <span className="text-green-600">GENESIS.</span>
      </h1>
      <button
        className="inline-block px-7 py-3 mr-2 bg-green-600 text-white font-medium text-sm
        leading-snug uppercase shadow-md hover:bg-green-700 hover:shadow-lg
        focus:bg-green-700 focus:shadow-lg focus:outline-none focus:ring-0 rounded-full
        active:bg-green-800 active:shadow-lg transition duration-150 ease-in-out"
        data-mdb-ripple="true"
        data-mdb-ripple-color="light"
        onClick={() => setGlobalState('modal', 'scale-100')}
      >
        Add Project
      </button>
      <button
        className="inline-block px-7 py-3 bg-transparent text-green-600 font-medium text-sm
        leading-snug uppercase hover:text-green-700 hover:bg-gray-100
        focus:bg-gray-100 focus:outline-none focus:ring-0 active:bg-gray-200
        transition duration-150 ease-in-out rounded-full"
        data-mdb-ripple="true"
        data-mdb-ripple-color="light"
        onClick={scrollToProjects}
      >
        Back Projects
      </button>

      <div className="flex justify-center items-center mt-10">
        <div className="flex flex-col justify-center items-center h-20 border border-gray-200 shadow-md w-full">
          <span className="text-lg font-bold text-green-900 leading-5">
            {stats.totalProjects}
          </span>
          <span>Projects</span>
        </div>
        <div className="flex flex-col justify-center items-center h-20 border border-gray-200 shadow-md w-full">
          <span className="text-lg font-bold text-green-900 leading-5">
            {stats.totalBacking}
          </span>
          <span>Backing</span>
        </div>
        <div className="flex flex-col justify-center items-center h-20 border border-gray-200 shadow-md w-full">
          <span className="text-lg font-bold text-green-900 leading-5">
            {stats.totalDonations} ETH
          </span>
          <span>Donated</span>
        </div>
      </div>
    </div>
  )
}

export default Hero


Projects and ProjectCard components

Projects Component


This component is responsible for rendering individual cards of projects. On the components directory, create a file called Projects.jsx and paste the codes below inside of it.


import { useEffect, useState } from 'react'
import ProjectCard from "./ProjectCard"

const Projects = ({projects}) => {
  const [end, setEnd] = useState(4)
  const [count] = useState(4)

  const [collection, setCollection] = useState([])

  const getCollection = () => {
    return projects.slice(0, end)
  }

  useEffect(() => {
    setCollection(getCollection())
  }, [projects, end])

  return (
    <div className="flex flex-col">
      <div className="flex justify-center items-center flex-wrap">
        {collection.map((project, i) => (
          <ProjectCard project={project} key={i} />
        ))}
      </div>

      {projects.length > 0 && projects.length > collection.length ? (
        <div className="flex justify-center items-center my-5">
          <button
            type="button"
            data-mdb-ripple="true"
            data-mdb-ripple-color="light"
            className="inline-block px-6 py-2.5 bg-green-600 text-white font-medium text-xs 
            leading-tight uppercase rounded-full shadow-md hover:bg-green-700 hover:shadow-lg
            focus:bg-green-700 focus:shadow-lg focus:outline-none focus:ring-0
            active:bg-green-800 active:shadow-lg transition duration-150 ease-in-out"
            onClick={() => setEnd(end + count)}
          >
            Load More
          </button>
        </div>
      ) : null}
    </div>
  )
}

export default Projects


Again, create another file called **ProjectCard.jsx** still inside the components folder and paste the following codes in it.


import Identicon from 'react-identicons'
import { Link } from 'react-router-dom'
import { FaEthereum } from 'react-icons/fa'
import { daysRemaining, truncate } from '../store'

const ProjectCard = ({ project }) => (
  <div id="projects" className="rounded-lg shadow-lg bg-white w-64 m-4">
    <Link
      to={`/projects/${project.id}`}
      data-mdb-ripple="true"
      data-mdb-ripple-color="light"
    >
      <img
        className="rounded-xl h-64 w-full object-cover"
        src={project.imageURL}
        alt={project.title}
      />

      <div className="p-4">
        <div className="flex justify-between items-center">
          <h5 className="text-gray-900 text-sm font-medium mb-2">
            {project.title}
          </h5>
        </div>
        <div className="flex justify-between items-center mb-3">
          <div className='flex justify-start items-center space-x-2'>
            <Identicon
              className="rounded-full shadow-md"
              string={project.owner}
              size={15}
            />
            <small className="text-gray-700">
              {truncate(project.owner, 4, 4, 11)}
            </small>
          </div>

          <small className="text-gray-500">
            {new Date().getTime() > Number(project.expiresAt + '000')
                ? 'Expired'
                : daysRemaining(project.expiresAt)}{' '}
              left
          </small>
        </div>
        <div className="w-full bg-gray-300 rounded-full">
          <div
            className="bg-green-600 text-xs font-medium text-green-100 text-center
              p-0.5 leading-none rounded-l-full h-1"
            style={{ width: `${(project.raised / project.cost) * 100}%` }}
          ></div>
        </div>
        <div className="flex justify-between items-center font-bold mt-1 mb-2">
          <small className="text-green-700">{project.raised} ETH Raised</small>
          <small className="flex justify-start items-center">
            <FaEthereum />
            <span className="text-gray-700">{project.cost} EHT</span>
          </small>
        </div>
        <div className="flex justify-between items-center flex-wrap mt-4 mb-2">
          <small className="text-gray-500 font-bold">
            {project.backers} Backing{project.backer == 1 ? '' : 's'}
          </small>

          <div>
            {project.status == 0 ? (
              <small className="text-gray-500 font-bold">Open</small>
            ) : null}
            {project.status == 1 ? (
              <small className="text-green-500 font-bold">Accepted</small>
            ) : null}
            {project.status == 2 ? (
              <small className="text-gray-500 font-bold">Reverted</small>
            ) : null}
            {project.status == 3 ? (
              <small className="text-red-500 font-bold">Deleted</small>
            ) : null}
            {project.status == 4 ? (
              <small className="text-orange-500 font-bold">Paid</small>
            ) : null}
          </div>
        </div>
      </div>
    </Link>
  </div>
)

export default ProjectCard


Great, we are moving forward.


AddButton and CreateProject Components

Create Project Component

This component together enables us to create new projects. Good use of Tailwind’s CSS was what it took to craft out that beautiful modal.


In the components folder, create a component called **AddButton.jsx** and paste the codes below into it.


import { BsPlusLg } from 'react-icons/bs'
import { setGlobalState } from '../store'

const AddButton = () => {
  return (
    <div className="fixed right-10 bottom-10 flex space-x-2 justify-center">
      <div>
        <button
          type="button"
          className="flex justify-center items-center rounded-full bg-green-600
          text-white leading-normal uppercase shadow-md hover:bg-green-700
          hover:shadow-lg focus:bg-green-700 focus:shadow-lg focus:outline-none
          focus:ring-0 active:bg-green-800 active:shadow-lg transition
          duration-150 ease-in-out w-9 h-9"
          onClick={() => setGlobalState('modal', 'scale-100')}
        >
          <BsPlusLg className='font-bold' size={20} />
        </button>
      </div>
    </div>
  )
}

export default AddButton


Next, create another component called **CreateProject.jsx** which is configured to be launched by the **AddButton.jsx** component. Paste the codes below within it.


import { useState } from 'react'
import { FaTimes } from 'react-icons/fa'
import { setGlobalState, useGlobalState } from '../store'
import { createProject, loadProjects } from '../Genesis'

const CreateProject = () => {
  const [modal] = useGlobalState('modal')
  const [title, setTitle] = useState('')
  const [cost, setCost] = useState('')
  const [date, setDate] = useState('')
  const [description, setDescription] = useState('')
  const [imageURL, setImageURL] = useState('')

  const toTimestamp = (strDate) => {
    const datum = Date.parse(strDate)
    return datum / 1000
  }

  const handleSubmit = async (e) => {
    e.preventDefault()

    if (!title || !cost || !imageURL || !description || !date) return
    const params = {
      title,
      description,
      cost,
      expiresAt: toTimestamp(date),
      imageURL,
    }

    createProject(params)
      .then(() => {
        setGlobalState('modal', 'scale-0')
        console.log('Project Created!')
        resetForm()
        loadProjects()
      })
      .catch((error) => console.log(error))
  }

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

  const resetForm = () => {
    setImageURL('')
    setTitle('')
    setDate('')
    setCost('')
    setDescription('')
  }

  return (
    <div
      className={`fixed top-0 left-0 w-screen h-screen flex items-center
        justify-center bg-black bg-opacity-50 transform
        transition-transform duration-300 ${modal}`}
    >
      <div className="bg-white shadow-xl shadow-black rounded-xl w-11/12 md:w-2/5 h-7/12 p-6">
        <form className="flex flex-col">
          <div className="flex flex-row justify-between items-center">
            <p className="font-semibold text-black">Add Project</p>
            <button
              type="button"
              onClick={closeModal}
              className="border-0 bg-transparent focus:outline-none"
            >
              <FaTimes className="text-black" />
            </button>
          </div>

          <div className="flex flex-row justify-center items-center rounded-xl mt-5">
            <div className="shrink-0 rounded-xl overflow-hidden h-20 w-20">
              <img
                alt="Project"
                className="h-full w-full object-cover cursor-pointer"
                src={
                  imageURL ||
                  'https://daltonic.github.io/assets/images/hero.jpg'
                }
              />
            </div>
          </div>

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

          <div className="flex flex-row justify-between items-center bg-gray-300 rounded-xl mt-5">
            <input
              className="block w-full text-sm
                text-slate-500 bg-transparent border-0
                focus:outline-none focus:ring-0"
              type="number"
              step={0.01}
              min={0.01}
              name="cost"
              placeholder="cost (Eth)"
              onChange={(e) => setCost(e.target.value)}
              value={cost}
              required
            />
          </div>

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

          <div className="flex flex-row justify-between items-center bg-gray-300 rounded-xl mt-5">
            <input
              className="block w-full text-sm
                text-slate-500 bg-transparent border-0
                focus:outline-none focus:ring-0"
              type="url"
              name="imageURL"
              placeholder="ImageURL"
              onChange={(e) => setImageURL(e.target.value)}
              pattern="^(http(s)?:\/\/)+[\w\-\._~:\/?#[\]@!\$&'\(\)\*\+,;=.]+$"
              value={imageURL}
              required
            />
          </div>

          <div className="flex flex-row justify-between items-center bg-gray-300 rounded-xl mt-5">
            <textarea
              className="block w-full text-sm resize-none
                text-slate-500 bg-transparent border-0
                focus:outline-none focus:ring-0 h-20"
              type="text"
              name="description"
              placeholder="Description"
              onChange={(e) => setDescription(e.target.value)}
              value={description}
              required
            ></textarea>
          </div>

          <button
            type="submit"
            onClick={handleSubmit}
            className="flex flex-row justify-center items-center
              w-full text-white text-md bg-green-500
              py-2 px-5 rounded-full drop-shadow-xl
              border-transparent border
              hover:bg-transparent hover:text-green-500
              hover:border hover:border-green-500
              focus:outline-none focus:ring mt-5"
          >
            Submit Project
          </button>
        </form>
      </div>
    </div>
  )
}

export default CreateProject


ProjectDetails Component

Project Details


This component is responsible for displaying details of a particular project and also providing some critical buttons to summon other components. To replicate this component, create a file called **ProjectDetails.jsx** in the components directory and paste the codes below into it.


import Identicon from 'react-identicons'
import { FaEthereum } from 'react-icons/fa'
import { useNavigate } from 'react-router-dom'
import { payoutProject } from '../Genesis'
import {
  daysRemaining,
  setGlobalState,
  truncate,
  useGlobalState,
} from '../store'

const ProjectDetails = ({ project }) => {
  const navigate = useNavigate()
  const [connectedAccount] = useGlobalState('connectedAccount')
  const [group] = useGlobalState('group')

  const handleChat = () => {
    if (group?.hasJoined) {
      navigate(`/chats/` + project.id)
    } else {
      setGlobalState('chatModal', 'scale-100')
    }
  }

  return (
    <div className="flex justify-center items-center flex-col">
      <div className="flex justify-start items-start sm:space-x-4 flex-wrap">
        <img
          className="rounded-xl h-64 object-cover sm:w-1/3 w-full"
          src={project.imageURL}
          alt={project.title}
        />
        <div className="flex-1 sm:py-0 py-4">
          <div className="flex flex-col justify-start flex-wrap">
            <h5 className="text-gray-900 text-sm font-medium mb-2">
              {project.title}
            </h5>
            <small className="text-gray-500">
              {new Date().getTime() > Number(project.expiresAt + '000')
                ? 'Expired'
                : daysRemaining(project.expiresAt)}{' '}
              left
            </small>
          </div>
          <div className="flex justify-start items-center space-x-2 mb-3">
            <Identicon
              className="rounded-full shadow-md"
              string={project.owner}
              size={15}
            />
            <div className="flex justify-between items-center w-full">
              <div className="flex justify-start items-center space-x-2">
                <small className="text-gray-700">
                  {truncate(project.owner, 4, 4, 11)}
                </small>
                <small className="text-gray-500 font-bold">
                  {project.backers} Backing{project.backer == 1 ? '' : 's'}
                </small>
              </div>

              <div>
                {project.status == 0 ? (
                  <small className="text-gray-500 font-bold">Open</small>
                ) : null}
                {project.status == 1 ? (
                  <small className="text-green-500 font-bold">Accepted</small>
                ) : null}
                {project.status == 2 ? (
                  <small className="text-gray-500 font-bold">Reverted</small>
                ) : null}
                {project.status == 3 ? (
                  <small className="text-red-500 font-bold">Deleted</small>
                ) : null}
                {project.status == 4 ? (
                  <small className="text-orange-500 font-bold">Paid</small>
                ) : null}
              </div>
            </div>
          </div>
          <p className="text-sm font-light">{project.description}</p>
          <div className="w-full bg-gray-300 rounded-full mt-4">
            <div
              className="bg-green-600 text-xs font-medium
              text-green-100 text-center p-0.5 leading-none
              rounded-l-full h-1 overflow-hidden max-w-full"
              style={{ width: `${(project.raised / project.cost) * 100}%` }}
            ></div>
          </div>
          <div className="flex justify-between items-center font-bold mt-2">
            <small className="text-green-700">
              {project.raised} ETH Raised
            </small>
            <small className="flex justify-start items-center">
              <FaEthereum />
              <span className="text-gray-700">{project.cost} EHT</span>
            </small>
          </div>

          <div className="flex justify-start flex-wrap items-center space-x-2 font-bold mt-4 w-full">
            {project.status == 0 ? (
              <button
                type="button"
                data-mdb-ripple="true"
                data-mdb-ripple-color="light"
                className="inline-block px-6 py-2.5 bg-green-600 text-white font-medium text-xs 
                  leading-tight uppercase rounded-full shadow-md hover:bg-green-700 hover:shadow-lg
                  focus:bg-green-700 focus:shadow-lg focus:outline-none focus:ring-0
                  active:bg-green-800 active:shadow-lg transition duration-150 ease-in-out"
                onClick={() => setGlobalState('backModal', 'scale-100')}
              >
                Back This Project
              </button>
            ) : null}

            {connectedAccount.toLowerCase() == project.owner.toLowerCase() ? (
              project.status != 3 ? (
                project.status == 1 ? (
                  <button
                    type="button"
                    data-mdb-ripple="true"
                    data-mdb-ripple-color="light"
                    className="inline-block px-6 py-2.5 bg-orange-600 text-white font-medium text-xs 
                    leading-tight uppercase rounded-full shadow-md hover:bg-orange-700 hover:shadow-lg
                    focus:bg-orange-700 focus:shadow-lg focus:outline-none focus:ring-0
                    active:bg-orange-800 active:shadow-lg transition duration-150 ease-in-out"
                    onClick={() => payoutProject(project.id)}
                  >
                    Payout
                  </button>
                ) : project.status != 4 ? (
                  <>
                    <button
                      type="button"
                      data-mdb-ripple="true"
                      data-mdb-ripple-color="light"
                      className="inline-block px-6 py-2.5 bg-gray-600 text-white font-medium text-xs 
                    leading-tight uppercase rounded-full shadow-md hover:bg-gray-700 hover:shadow-lg
                    focus:bg-gray-700 focus:shadow-lg focus:outline-none focus:ring-0
                    active:bg-gray-800 active:shadow-lg transition duration-150 ease-in-out"
                      onClick={() => setGlobalState('updateModal', 'scale-100')}
                    >
                      Edit
                    </button>
                    <button
                      type="button"
                      data-mdb-ripple="true"
                      data-mdb-ripple-color="light"
                      className="inline-block px-6 py-2.5 bg-red-600 text-white font-medium text-xs 
                    leading-tight uppercase rounded-full shadow-md hover:bg-red-700 hover:shadow-lg
                    focus:bg-red-700 focus:shadow-lg focus:outline-none focus:ring-0
                    active:bg-red-800 active:shadow-lg transition duration-150 ease-in-out"
                      onClick={() => setGlobalState('deleteModal', 'scale-100')}
                    >
                      Delete
                    </button>
                  </>
                ) : (
                  <button
                    type="button"
                    data-mdb-ripple="true"
                    data-mdb-ripple-color="light"
                    className="inline-block px-6 py-2.5 bg-gray-600 text-white font-medium text-xs 
                  leading-tight uppercase rounded-full shadow-md hover:bg-gray-700 hover:shadow-lg
                  focus:bg-gray-700 focus:shadow-lg focus:outline-none focus:ring-0
                  active:bg-gray-800 active:shadow-lg transition duration-150 ease-in-out"
                  >
                    project Closed
                  </button>
                )
              ) : null
            ) : null}

            <button
              type="button"
              data-mdb-ripple="true"
              data-mdb-ripple-color="light"
              className="inline-block px-6 py-2.5 bg-orange-600 text-white font-medium text-xs 
                leading-tight uppercase rounded-full shadow-md hover:bg-orange-700 hover:shadow-lg
                focus:bg-orange-700 focus:shadow-lg focus:outline-none focus:ring-0
                active:bg-orange-800 active:shadow-lg transition duration-150 ease-in-out"
              onClick={handleChat}
            >
              Chat
            </button>
          </div>
        </div>
      </div>
    </div>
  )
}

export default ProjectDetails


UpdateProject Component

UpdateProject Component


As intuitive as its name sound, this component helps us update projects provided that the user has the right clearance. In the components folder, create a file called **UpdateProject.jsx** and paste the codes below into it.


import { useState } from 'react'
import { FaTimes } from 'react-icons/fa'
import { setGlobalState, useGlobalState } from '../store'
import { updateProject } from '../Genesis'

const UpdateProject = ({ project }) => {
  const [updateModal] = useGlobalState('updateModal')
  const [title, setTitle] = useState(project.title)
  const [date, setDate] = useState(project.date)
  const [description, setDescription] = useState(project.description)
  const [imageURL, setImageURL] = useState(project.imageURL)

  const toTimestamp = (strDate) => {
    const datum = Date.parse(strDate)
    return datum / 1000
  }

  const handleSubmit = async (e) => {
    e.preventDefault()

    if (!title || !imageURL || !description || !date) return
    const params = {
      id: project.id,
      title,
      description,
      expiresAt: toTimestamp(date),
      imageURL,
    }

    updateProject(params).then(() => {
      closeModal()
    })
  }

  const closeModal = () => {
    setGlobalState('updateModal', '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
        transition-transform duration-300 ${updateModal}`}
    >
      <div className="bg-white shadow-xl shadow-black rounded-xl w-11/12 md:w-2/5 h-7/12 p-6">
        <form className="flex flex-col">
          <div className="flex flex-row justify-between items-center">
            <p className="font-semibold text-black">Edit Project</p>
            <button
              type="button"
              onClick={closeModal}
              className="border-0 bg-transparent focus:outline-none"
            >
              <FaTimes className="text-black" />
            </button>
          </div>

          <div className="flex flex-row justify-center items-center rounded-xl mt-5">
            <div className="shrink-0 rounded-xl overflow-hidden h-20 w-20">
              <img
                alt="Project"
                className="h-full w-full object-cover cursor-pointer"
                src={imageURL}
              />
            </div>
          </div>

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

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

          <div className="flex flex-row justify-between items-center bg-gray-300 rounded-xl mt-5">
            <input
              className="block w-full text-sm
                text-slate-500 bg-transparent border-0
                focus:outline-none focus:ring-0"
              type="url"
              name="imageURL"
              placeholder="ImageURL"
              onChange={(e) => setImageURL(e.target.value)}
              pattern="^(http(s)?:\/\/)+[\w\-\._~:\/?#[\]@!\$&'\(\)\*\+,;=.]+$"
              value={imageURL}
              required
            />
          </div>

          <div className="flex flex-row justify-between items-center bg-gray-300 rounded-xl mt-5">
            <textarea
              className="block w-full text-sm resize-none
                text-slate-500 bg-transparent border-0
                focus:outline-none focus:ring-0 h-20"
              type="text"
              name="description"
              placeholder="Description"
              onChange={(e) => setDescription(e.target.value)}
              value={description}
              required
            ></textarea>
          </div>

          <button
            type="submit"
            onClick={handleSubmit}
            className="flex flex-row justify-center items-center
              w-full text-white text-md bg-green-500
              py-2 px-5 rounded-full drop-shadow-xl
              border-transparent border
              hover:bg-transparent hover:text-green-500
              hover:border hover:border-green-500
              focus:outline-none focus:ring mt-5"
          >
            Update Project
          </button>
        </form>
      </div>
    </div>
  )
}

export default UpdateProject


DeleteProject Component

DeleteProject Component


Like the UpdateProject component, this component can only be utilized if you are the owner of the project or if you are the deployer of the smart contract. Head on to the components folder and create a new file called DeleteProject.jsx and paste the codes below into it.


import { FaTimes } from 'react-icons/fa'
import { setGlobalState, useGlobalState } from '../store'
import { deleteProject } from '../Genesis'

const DeleteProject = ({ project }) => {
  const [deleteModal] = useGlobalState('deleteModal')

  const handleSubmit = async (e) => {
    e.preventDefault()

    deleteProject(project.id).then(() => {
      setGlobalState('deleteModal', 'scale-0')
      console.log('Project Deleted!')
    })
  }

  const closeModal = () => {
    setGlobalState('deleteModal', '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
        transition-transform duration-300 ${deleteModal}`}
    >
      <div className="bg-white shadow-xl shadow-black rounded-xl w-11/12 md:w-2/5 h-7/12 p-6">
        <form className="flex flex-col">
          <div className="flex flex-row justify-between items-center">
            <p className="font-semibold text-black">#{project.title}</p>
            <button
              type="button"
              onClick={closeModal}
              className="border-0 bg-transparent focus:outline-none"
            >
              <FaTimes className="text-black" />
            </button>
          </div>

          <div className="flex flex-row justify-center items-center rounded-xl mt-5">
            <div className="shrink-0 rounded-xl overflow-hidden h-20 w-20">
              <img
                alt="Project"
                className="h-full w-full object-cover cursor-pointer"
                src={project.imageURL}
              />
            </div>
          </div>

          <div className="flex flex-col justify-center items-center mt-5">
            <p>Are you sure?</p>
            <small className='text-red-400'>This is irriversible!</small>
          </div>

          <button
            type="submit"
            onClick={handleSubmit}
            className="flex flex-row justify-center items-center w-full 
              text-white text-md bg-red-500
              py-2 px-5 rounded-full drop-shadow-xl
              border-transparent border
              hover:bg-transparent hover:text-red-500
              hover:border hover:border-red-500
              focus:outline-none focus:ring mt-5"
          >
            Delete Project
          </button>
        </form>
      </div>
    </div>
  )
}

export default DeleteProject


ChatAuth Component

Chat Auth

This little component is responsible for authenticating users for anonymous chat. Also through this component, owners of projects can create new projects and others can join.

Create a file called **ChatAuth.jsx** and paste the codes below inside of it.


import { useNavigate } from 'react-router-dom'
import { FaTimes } from 'react-icons/fa'
import { setGlobalState, useGlobalState } from '../store'
import {
  createNewGroup,
  joinGroup,
  loginWithCometChat,
  signInWithCometChat,
} from '../CometChat'

const ChatAuth = ({ project }) => {
  const [chatModal] = useGlobalState('chatModal')
  const [currentUser] = useGlobalState('currentUser')

  return (
    <div
      className={`fixed top-0 left-0 w-screen h-screen flex items-center
        justify-center bg-black bg-opacity-50 transform
        transition-transform duration-300 ${chatModal}`}
    >
      <div className="bg-white shadow-xl shadow-black rounded-xl w-11/12 md:w-2/5 h-7/12 p-6">
        {currentUser ? <Actions project={project} /> : <Auth />}
      </div>
    </div>
  )
}

const Auth = () => {
  const handleSignUp = () => {
    signInWithCometChat().then(() => handleLogin())
  }

  const handleLogin = () => {
    loginWithCometChat().then(() => {
      // perform duties...
      if (currentUser) console.log('Current user', currentUser)
    })
  }

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

  return (
    <div className="flex flex-col">
      <div className="flex flex-row justify-between items-center">
        <p className="font-semibold text-black">Authenticate Chat</p>
        <button
          type="button"
          onClick={closeModal}
          className="border-0 bg-transparent focus:outline-none"
        >
          <FaTimes className="text-black" />
        </button>
      </div>

      <div className="flex flex-col justify-center items-center mt-5">
        <p>Sign in or up to participate in chat.</p>
      </div>

      <div className="flex flex-row justify-center items-center space-x-2 w-full">
        <button
          className=" text-white text-md bg-green-500
              py-2 px-5 rounded-full drop-shadow-xl
              border-transparent border
              hover:bg-transparent hover:text-green-500
              hover:border hover:border-green-500
              focus:outline-none focus:ring mt-5"
          onClick={handleLogin}
        >
          Login
        </button>
        <button
          className=" text-white text-md bg-green-500
              py-2 px-5 rounded-full drop-shadow-xl
              border-transparent border
              hover:bg-transparent hover:text-green-500
              hover:border hover:border-green-500
              focus:outline-none focus:ring mt-5"
          onClick={handleSignUp}
        >
          Sign up
        </button>
      </div>
    </div>
  )
}

const Actions = ({ project }) => {
  const navigate = useNavigate()
  const [connectedAccount] = useGlobalState('connectedAccount')
  const [group] = useGlobalState('group')

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

  const handleGroupCreation = () => {
    createNewGroup(`pid_${project.id}`, project.title)
  }

  const handleGroupJoin = () => {
    joinGroup(`pid_${project.id}`).then(() => navigate(`/chats/` + project.id))
  }

  return (
    <div className="flex flex-col">
      <div className="flex flex-row justify-between items-center">
        <p className="font-semibold text-black">Authenticate Chat</p>
        <button
          type="button"
          onClick={closeModal}
          className="border-0 bg-transparent focus:outline-none"
        >
          <FaTimes className="text-black" />
        </button>
      </div>

      <div className="flex flex-row justify-center items-center space-x-2 w-full">
        {connectedAccount.toLowerCase() != project.owner.toLowerCase() ? (
          group ? (
            <button
              className=" text-white text-md bg-green-500
            py-2 px-5 rounded-full drop-shadow-xl
            border-transparent border
            hover:bg-transparent hover:text-green-500
            hover:border hover:border-green-500
            focus:outline-none focus:ring mt-5"
              onClick={handleGroupJoin}
            >
              Enter Group
            </button>
          ) : (
            <div className="flex flex-col justify-center items-center">
              <p>No group created for this projected</p>
              <button
                className=" text-white text-md bg-gray-500
                py-2 px-5 rounded-full drop-shadow-xl
                border-transparent border
                hover:bg-transparent hover:text-gray-500
                hover:border hover:border-gray-500
                focus:outline-none focus:ring mt-5"
                onClick={closeModal}
              >
                Close
              </button>
            </div>
          )
        ) : (
          <button
            className=" text-white text-md bg-green-500
            py-2 px-5 rounded-full drop-shadow-xl
            border-transparent border
            hover:bg-transparent hover:text-green-500
            hover:border hover:border-green-500
            focus:outline-none focus:ring mt-5"
            onClick={handleGroupCreation}
          >
            Create Group
          </button>
        )}
      </div>
    </div>
  )
}

export default ChatAuth


BackProject Component

BackProject Component


This component helps you to specify and donate the number of ethers you are willing to contribute to the project. To replicate, let’s create another component called **BackProject.jsx** and paste the codes in it.


import { useState } from 'react'
import { FaTimes } from 'react-icons/fa'
import { setGlobalState, useGlobalState } from '../store'
import { backProject } from '../Genesis'

const BackProject = ({project}) => {
  const [backModal] = useGlobalState('backModal')
  const [amount, setAmount] = useState('')

  const handleSubmit = async (e) => {
    e.preventDefault()

    if (!amount) return

    backProject(project.id, amount)
      .then(() => {
        closeModal()
        console.log('Project Backed!')
        setAmount('')
      })
  }

  const closeModal = () => {
    setGlobalState('backModal', '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
        transition-transform duration-300 ${backModal}`}
    >
      <div className="bg-white shadow-xl shadow-black rounded-xl w-11/12 md:w-2/5 h-7/12 p-6">
        <form className="flex flex-col">
          <div className="flex flex-row justify-between items-center">
            <p className="font-semibold text-black">#{project.title}</p>
            <button
              type="button"
              onClick={closeModal}
              className="border-0 bg-transparent focus:outline-none"
            >
              <FaTimes className="text-black" />
            </button>
          </div>

          <div className="flex flex-row justify-center items-center rounded-xl mt-5">
            <div className="shrink-0 rounded-xl overflow-hidden h-20 w-20">
              <img
                alt="Project"
                className="h-full w-full object-cover cursor-pointer"
                src={project.imageURL}
              />
            </div>
          </div>

          <div className="flex flex-row justify-between items-center bg-gray-300 rounded-xl mt-5">
            <input
              className="block w-full text-sm
                text-slate-500 bg-transparent border-0
                focus:outline-none focus:ring-0"
              type="number"
              step={0.01}
              min={0.01}
              name="amount"
              placeholder="Amount (Eth)"
              onChange={(e) => setAmount(e.target.value)}
              value={amount}
              required
            />
          </div>

          <button
            type="submit"
            onClick={handleSubmit}
            className="flex flex-row justify-center items-center
              w-full text-white text-md bg-green-500
              py-2 px-5 rounded-full drop-shadow-xl
              border-transparent border
              hover:bg-transparent hover:text-green-500
              hover:border hover:border-green-500
              focus:outline-none focus:ring mt-5"
          >
            Back Project
          </button>
        </form>
      </div>
    </div>
  )
}

export default BackProject


ProjectBackers Component

Project Backers component


This component lists out all the backers of a particular project. Create a file named **ProjectBackers.jsx** and paste the codes inside of it.


import Identicon from 'react-identicons'
import Moment from 'react-moment'
import { FaEthereum } from 'react-icons/fa'
import { truncate } from '../store'

const ProjectBackers = ({ backers }) => {
  return (
    <div className="flex justify-center flex-col items-start w-full">
      <div className="max-h-[calc(100vh_-_20rem)] overflow-y-auto shadow-md rounded-md w-full">
        <table className="min-w-full">
          <thead className="border-b">
            <tr>
              <th
                scope="col"
                className="text-sm font-medium px-6 py-4 text-left"
              >
                Backer
              </th>
              <th
                scope="col"
                className="text-sm font-medium px-6 py-4 text-left"
              >
                Donation
              </th>
              <th
                scope="col"
                className="text-sm font-medium px-6 py-4 text-left"
              >
                Refunded
              </th>
              <th
                scope="col"
                className="text-sm font-medium px-6 py-4 text-left"
              >
                Time
              </th>
            </tr>
          </thead>
          <tbody>
            {backers.map((backer, i) => (
              <tr
                key={i}
                className="border-b border-gray-200 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={backer.owner}
                      size={25}
                      className="h-10 w-10 object-contain rounded-full mr-3"
                    />
                    <span>{truncate(backer.owner, 4, 4, 11)}</span>
                  </div>
                </td>
                <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
                  <small className="flex justify-start items-center">
                    <FaEthereum />
                    <span className="text-gray-700">
                      {backer.contribution} EHT
                    </span>
                  </small>
                </td>
                <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
                  {backer.refunded ? (
                    <span className="text-red-700 font-bold">Yes</span>
                  ) : (
                    <span className="text-gray-700 font-bold">No</span>
                  )}
                </td>
                <td className="text-sm font-light px-6 py-4 whitespace-nowrap">
                  <Moment fromNow>{backer.timestamp}</Moment>
                </td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>
    </div>
  )
}

export default ProjectBackers


Messages Component

Messages Component


This component is responsible for rendering chat content to the view. First, create a file called **Messages.jsx** and paste the following codes inside of it.


import { useEffect, useState } from 'react'
import { getMessages, sendMessage, CometChat } from '../CometChat'
import { truncate, useGlobalState } from '../store'

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

  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
        id="messages-container"
        className="w-full h-[calc(100vh_-_16rem)] overflow-y-auto sm:px-2 my-3"
      >
        {messages.map((msg, i) =>
          msg.sender.uid.toLowerCase() != connectedAccount.toLowerCase() ? (
            <LeftMessage
              key={i}
              message={msg.text}
              timestamp={dateToTime(new Date(msg.sentAt * 1000))}
              owner={truncate(msg.sender.uid, 4, 4, 11)}
            />
          ) : (
            <RightMessage
              key={i}
              message={msg.text}
              timestamp={dateToTime(new Date(msg.sentAt * 1000))}
              owner={truncate(msg.sender.uid, 4, 4, 11)}
            />
          )
        )}
      </div>

      <form onSubmit={handleMessage} className="flex w-full">
        <input
          className="w-full bg-gray-200 rounded-lg p-4 
          focus:ring-0 focus:outline-none border-gray-500"
          type="text"
          placeholder="Write a message..."
          value={message}
          onChange={(e) => setMessage(e.target.value)}
          required
        />
        <button type="submit" hidden>
          Send
        </button>
      </form>
    </>
  )
}

const RightMessage = ({ message, timestamp, owner }) => (
  <div className="flex flex-row justify-end my-2">
    <div className="flex justify-center items-end space-x-2">
      <div
        className="flex flex-col bg-transparent w-80 p-3 px-5 rounded-t-3xl
    rounded-bl-3xl shadow shadow-green-500"
      >
        <div className="flex flex-row justify-start items-center space-x-2">
          <span>@You</span>
          <small>{timestamp}</small>
        </div>
        <small className="leading-tight my-2">{message}</small>
      </div>
    </div>
  </div>
)

const LeftMessage = ({ message, timestamp, owner }) => (
  <div className="flex flex-row justify-start my-2">
    <div className="flex justify-center items-end space-x-2">
      <div
        className="flex flex-col bg-transparent w-80 p-3 px-5 rounded-t-3xl
          rounded-br-3xl shadow shadow-gray-500"
      >
        <div className="flex flex-row justify-start items-center space-x-2">
          <span>@{owner}</span>
          <small>{timestamp}</small>
        </div>
        <small className="leading-tight my-2">{message}</small>
      </div>
    </div>
  </div>
)

export default Messages


The Views We have three major views on this project, they include the Home, Product, and Chat pages. Let’s have them created one after the other.


On the src directory create a new folder called views and create the following files within it.


Home Page

Home Component


This component bundles the smaller components and services that make up the home page. Create a file in the views directory called **Home.jsx** and paste the codes below into it.


import { useEffect, useState } from 'react'
import { loadProjects } from '../Genesis'
import { useGlobalState } from '../store'
import Hero from '../components/Hero'
import Projects from '../components/Projects'

const Home = () => {
  const [loaded, setLoaded] = useState(false)
  const [projects] = useGlobalState('projects')

  useEffect(() => {
    loadProjects().then(() => setLoaded(true))
  }, [])

  return loaded ? (
    <>
      <Hero />
      <Projects projects={projects} />
    </>
  ) : null
}

export default Home


Project View

Project Page

There is a lot of logic that goes into the making of this page which can be seen by the number of actionable buttons integrated with it. To implement, create a new file called Project.jsx in the src >> views and paste the codes below inside of it.


import ChatAuth from '../components/ChatAuth'
import BackProject from '../components/BackProject'
import UpdateProject from '../components/UpdateProject'
import ProjectDetails from '../components/ProjectDetails'
import ProjectBackers from '../components/ProjectBackers'
import DeleteProject from '../components/DeleteProject'
import { loadProject } from '../Genesis'
import { useGlobalState } from '../store'
import { useEffect, useState } from 'react'
import { useParams } from 'react-router-dom'
import { getGroup } from '../CometChat'

const Project = () => {
  const { id } = useParams()
  const [loaded, setLoaded] = useState(false)
  const [project] = useGlobalState('project')
  const [backers] = useGlobalState('backers')
  const [group] = useGlobalState('group')
  const [currentUser] = useGlobalState('currentUser')

  useEffect(() => {
    if (currentUser) getGroup('pid_' + id)
    loadProject(id).then(() => setLoaded(true))
  }, [currentUser])

  return loaded ? (
    <div className="flex flex-col lg:w-4/5 w-full mx-auto px-5">
      <div className="my-5"></div>
      <ProjectDetails group={group} project={project} />
      
      <div className="my-5"></div>
      {backers.length > 0 ? <ProjectBackers backers={backers} /> : null}

      <UpdateProject project={project} />
      <DeleteProject project={project} />
      <BackProject project={project} />

      <ChatAuth project={project} />
      <div className="my-5"></div>
    </div>
  ) : null
}

export default Project


Chat View

Chat View


This view allows you to engage in a one-to-many chat using the CometChat SDK. This is how to replicate it, jump into the views directory, create a file called Chat.jsx, and paste the codes below inside of it.


import Identicon from 'react-identicons'
import { useNavigate, useParams } from 'react-router-dom'
import { truncate, useGlobalState } from '../store'
import Messages from '../components/Messages'

const Chat = () => {
  const { id } = useParams()
  const navigate = useNavigate()
  const [connectedAccount] = useGlobalState('connectedAccount')

  return (
    <div className="flex justify-center items-center w-full flex-col flex-wrap p-6 sm:w-3/4 mx-auto">
      <div className="flex justify-between items-center w-full">
        <div className="flex justify-start items-center space-x-2">
          <Identicon
            className="rounded-full shadow-md"
            string={connectedAccount}
            size={25}
          />
          <small className="text-sm font-bold">
            {truncate(connectedAccount, 4, 4, 11)}
          </small>
        </div>
        <button
          type="button"
          data-mdb-ripple="true"
          data-mdb-ripple-color="light"
          className="inline-block px-6 py-2.5 bg-gray-800 text-white border-gray-600 font-medium text-xs 
            leading-tight uppercase shadow-md hover:bg-transparent hover:shadow-lg hover:text-gray-700
            focus:bg-gray-700 focus:shadow-lg focus:outline-none focus:ring-0 border
            active:bg-gray-800 active:shadow-lg transition duration-150 ease-in-out rounded-full"
          onClick={() => navigate(`/projects/` + id)}
        >
          Back to Project
        </button>
      </div>
      <Messages gid={`pid_${id}`} />
    </div>
  )
}

export default Chat


The App Component This mother component bundles and regulates all the pages and components in this application. In the src folder, replace the codes inside the file named **App.jsx** with the one below.


import { useEffect, useState } from 'react'
import { isWallectConnected } from './Genesis'
import { Route, Routes } from 'react-router-dom'
import { useGlobalState } from './store'
import { checkAuthState } from './CometChat'
import Header from './components/Header'
import AddButton from './components/AddButton'
import CreateProject from './components/CreateProject'
import Home from './views/Home'
import Project from './views/Project'
import Chat from './views/Chat'

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

  useEffect(() => {
    checkAuthState()
    isWallectConnected().then(() => {
      console.log('Blockchain Loaded')
      setLoaded(true)
    })
  }, [])

  return (
    <div className="min-h-screen relative">
      <Header />
      <div className="mt-20"></div>
      
      {loaded ? (
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/projects/:id" element={<Project />} />
          <Route path="/chats/:id" element={<Chat />} />
        </Routes>
      ) : null}

      {connectedAccount ? (
        <>
          <AddButton />
          <CreateProject />
        </>
      ) : null}
    </div>
  )
}

export default App


That completes the views and components for this project, now let’s add up the rest of the essential files.

Other Essential Files

The Store Service We are using a moderate state management library called react-hooks-global-state to store the data coming from the blockchain. Doing this method greatly simplifies the code.


In your src directory, create a folder and a file called store >> index.jsx and paste the codes below inside.


import moment from 'moment'
import { createGlobalState } from 'react-hooks-global-state'

const { setGlobalState, useGlobalState, getGlobalState } = createGlobalState({
  modal: 'scale-0',
  updateModal: 'scale-0',
  deleteModal: 'scale-0',
  backModal: 'scale-0',
  chatModal: 'scale-0',
  connectedAccount: '',
  backers: [],
  projects: [],
  project: null,
  contract: null,
  stats: null,
  currentUser: null,
  group: null,
})

const truncate = (text, startChars, endChars, maxLength) => {
  if (text.length > maxLength) {
    let start = text.substring(0, startChars)
    let end = text.substring(text.length - endChars, text.length)
    while (start.length + end.length < maxLength) {
      start = start + '.'
    }
    return start + end
  }
  return text
}

const 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'
}

export { useGlobalState, setGlobalState, getGlobalState, truncate, daysRemaining }


The Blockchain Service

Now, is one of the most important files in this project, and all the codes to interact with our smart contract are written here. Create a file called **Genesis.jsx** in the src folder and paste the codes below.


import abi from './abis/src/contracts/Genesis.sol/Genesis.json'
import address from './abis/contractAddress.json'
import { getGlobalState, setGlobalState } from './store'
import { ethers } from 'ethers'
import { logOutWithCometChat } from './CometChat'

const { ethereum } = window
const contractAddress = address.address
const contractAbi = abi.abi

const getEtheriumContract = () => {
  const connectedAccount = getGlobalState('connectedAccount')

  if (connectedAccount) {
    const provider = new ethers.providers.Web3Provider(ethereum)
    const signer = provider.getSigner()
    const contract = new ethers.Contract(contractAddress, contractAbi, signer)

    return contract
  } else {
    return getGlobalState('contract')
  }
}

const isWallectConnected = async () => {
  try {
    if (!ethereum) return alert('Please install Metamask')
    const accounts = await ethereum.request({ method: 'eth_accounts' })

    window.ethereum.on('chainChanged', (chainId) => {
      window.location.reload()
    })

    window.ethereum.on('accountsChanged', async () => {
      setGlobalState('connectedAccount', accounts[0])
      await logOutWithCometChat()
      await isWallectConnected()
    })

    if (accounts.length) {
      setGlobalState('connectedAccount', accounts[0])
    } else {
      alert('Please connect wallet.')
      console.log('No accounts found.')
    }
  } catch (error) {
    reportError(error)
  }
}

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

const createProject = async ({
  title,
  description,
  imageURL,
  cost,
  expiresAt,
}) => {
  try {
    if (!ethereum) return alert('Please install Metamask')

    const contract = getEtheriumContract()
    cost = ethers.utils.parseEther(cost)
    await contract.createProject(title, description, imageURL, cost, expiresAt)

    await loadProjects() // The blockchain is too slow for this...
    window.location.reload() // That's why we used this...
  } catch (error) {
    reportError(error)
  }
}

const updateProject = async ({
  id,
  title,
  description,
  imageURL,
  expiresAt,
}) => {
  try {
    if (!ethereum) return alert('Please install Metamask')

    const contract = getEtheriumContract()
    await contract.updateProject(id, title, description, imageURL, expiresAt)

    await loadProjects() // The blockchain is too slow for this...
    window.location.reload() // That's why we used this...
  } catch (error) {
    reportError(error)
  }
}

const backProject = async (id, amount) => {
  try {
    if (!ethereum) return alert('Please install Metamask')
    const connectedAccount = getGlobalState('connectedAccount')
    const contract = getEtheriumContract()
    amount = ethers.utils.parseEther(amount)

    await contract.backProject(id, {
      from: connectedAccount,
      value: amount._hex,
    })

    await loadProjects() // The blockchain is too slow for this...
    window.location.reload() // That's why we used this...
  } catch (error) {
    reportError(error)
  }
}

const refundProject = async (id) => {
  try {
    if (!ethereum) return alert('Please install Metamask')
    const connectedAccount = getGlobalState('connectedAccount')
    const contract = getEtheriumContract()

    await contract.requestRefund(id, {
      from: connectedAccount,
    })

    await loadProjects() // The blockchain is too slow for this...
    window.location.reload() // That's why we used this...
  } catch (error) {
    reportError(error)
  }
}

const payoutProject = async (id) => {
  try {
    if (!ethereum) return alert('Please install Metamask')
    const connectedAccount = getGlobalState('connectedAccount')
    const contract = getEtheriumContract()

    await contract.payOutProject(id, {
      from: connectedAccount,
    })

    await loadProjects() // The blockchain is too slow for this...
    window.location.reload() // That's why we used this...
  } catch (error) {
    reportError(error)
  }
}

const getBackers = async (id) => {
  try {
    if (!ethereum) return alert('Please install Metamask')
    const contract = getEtheriumContract()
    let backers = await contract.getBackers(id)

    setGlobalState('backers', structuredBackers(backers))
  } catch (error) {
    reportError(error)
  }
}

const deleteProject = async (id) => {
  try {
    if (!ethereum) return alert('Please install Metamask')
    const contract = getEtheriumContract()
    await contract.deleteProject(id)

    await loadProjects() // The blockchain is too slow for this...
    window.location.reload() // That's why we used this...
  } catch (error) {
    reportError(error)
  }
}

const loadProject = async (id) => {
  try {
    if (!ethereum) return alert('Please install Metamask')
    const contract = getEtheriumContract()
    let project = await contract.getProject(id)
    project = structuredProjects([project])[0]

    setGlobalState('project', project)
    await getBackers(id)
    console.log("Project Loaded...")
  } catch (error) {
    alert(JSON.stringify(error.message))
    reportError(error)
  }
}

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

    const contract = getEtheriumContract()
    const projects = await contract.getProjects()
    const stats = await contract.stats()

    setGlobalState('stats', structureStats(stats))
    setGlobalState('projects', structuredProjects(projects))
  } catch (error) {
    reportError(error)
  }
}

const structuredBackers = (backers) =>
  backers
    .map((backer) => ({
      owner: backer.owner,
      refunded: backer.refunded,
      timestamp: new Date(backer.timestamp.toNumber() * 1000).toJSON(),
      contribution: parseInt(backer.contribution._hex) / 10 ** 18,
    }))
    .reverse()

const structuredProjects = (projects) =>
  projects
    .map((project) => ({
      id: project.id.toNumber(),
      owner: project.owner,
      title: project.title,
      description: project.description,
      timestamp: new Date(project.timestamp.toNumber()).getTime(),
      expiresAt: new Date(project.expiresAt.toNumber()).getTime(),
      date: toDate(project.expiresAt.toNumber() * 1000),
      imageURL: project.imageURL,
      raised: parseInt(project.raised._hex) / 10 ** 18,
      cost: parseInt(project.cost._hex) / 10 ** 18,
      backers: project.backers.toNumber(),
      status: project.status,
    }))
    .reverse()

const toDate = (timestamp) => {
  const date = new Date(timestamp)
  const dd = date.getDate() > 9 ? date.getDate() : `0${date.getDate()}`
  const mm =
    date.getMonth() + 1 > 9 ? date.getMonth() + 1 : `0${date.getMonth() + 1}`
  const yyyy = date.getFullYear()
  return `${yyyy}-${mm}-${dd}`
}

const structureStats = (stats) => ({
  totalProjects: stats.totalProjects.toNumber(),
  totalBacking: stats.totalBacking.toNumber(),
  totalDonations: parseInt(stats.totalDonations._hex) / 10 ** 18,
})

const reportError = (error) => {
  console.log(error.message)
  throw new Error('No ethereum object.')
}

export {
  getEtheriumContract,
  isWallectConnected,
  connectWallet,
  createProject,
  updateProject,
  deleteProject,
  loadProjects,
  loadProject,
  backProject,
  payoutProject,
  refundProject,
}


The CometChat Services This file contains all the functions needed to establish chat communications with the CometChat SDK. On your src folder create a file named **CometChat.jsx** and paste the codes below inside.


import abi from './abis/src/contracts/Genesis.sol/Genesis.json'
import address from './abis/contractAddress.json'
import { getGlobalState, setGlobalState } from './store'
import { ethers } from 'ethers'
import { logOutWithCometChat } from './CometChat'

const { ethereum } = window
const contractAddress = address.address
const contractAbi = abi.abi

const getEtheriumContract = () => {
  const connectedAccount = getGlobalState('connectedAccount')

  if (connectedAccount) {
    const provider = new ethers.providers.Web3Provider(ethereum)
    const signer = provider.getSigner()
    const contract = new ethers.Contract(contractAddress, contractAbi, signer)

    return contract
  } else {
    return getGlobalState('contract')
  }
}

const isWallectConnected = async () => {
  try {
    if (!ethereum) return alert('Please install Metamask')
    const accounts = await ethereum.request({ method: 'eth_accounts' })

    window.ethereum.on('chainChanged', (chainId) => {
      window.location.reload()
    })

    window.ethereum.on('accountsChanged', async () => {
      setGlobalState('connectedAccount', accounts[0])
      await logOutWithCometChat()
      await isWallectConnected()
    })

    if (accounts.length) {
      setGlobalState('connectedAccount', accounts[0])
    } else {
      alert('Please connect wallet.')
      console.log('No accounts found.')
    }
  } catch (error) {
    reportError(error)
  }
}

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

const createProject = async ({
  title,
  description,
  imageURL,
  cost,
  expiresAt,
}) => {
  try {
    if (!ethereum) return alert('Please install Metamask')

    const contract = getEtheriumContract()
    cost = ethers.utils.parseEther(cost)
    await contract.createProject(title, description, imageURL, cost, expiresAt)

    await loadProjects() // The blockchain is too slow for this...
    window.location.reload() // That's why we used this...
  } catch (error) {
    reportError(error)
  }
}

const updateProject = async ({
  id,
  title,
  description,
  imageURL,
  expiresAt,
}) => {
  try {
    if (!ethereum) return alert('Please install Metamask')

    const contract = getEtheriumContract()
    await contract.updateProject(id, title, description, imageURL, expiresAt)

    await loadProjects() // The blockchain is too slow for this...
    window.location.reload() // That's why we used this...
  } catch (error) {
    reportError(error)
  }
}

const backProject = async (id, amount) => {
  try {
    if (!ethereum) return alert('Please install Metamask')
    const connectedAccount = getGlobalState('connectedAccount')
    const contract = getEtheriumContract()
    amount = ethers.utils.parseEther(amount)

    await contract.backProject(id, {
      from: connectedAccount,
      value: amount._hex,
    })

    await loadProjects() // The blockchain is too slow for this...
    window.location.reload() // That's why we used this...
  } catch (error) {
    reportError(error)
  }
}

const refundProject = async (id) => {
  try {
    if (!ethereum) return alert('Please install Metamask')
    const connectedAccount = getGlobalState('connectedAccount')
    const contract = getEtheriumContract()

    await contract.requestRefund(id, {
      from: connectedAccount,
    })

    await loadProjects() // The blockchain is too slow for this...
    window.location.reload() // That's why we used this...
  } catch (error) {
    reportError(error)
  }
}

const payoutProject = async (id) => {
  try {
    if (!ethereum) return alert('Please install Metamask')
    const connectedAccount = getGlobalState('connectedAccount')
    const contract = getEtheriumContract()

    await contract.payOutProject(id, {
      from: connectedAccount,
    })

    await loadProjects() // The blockchain is too slow for this...
    window.location.reload() // That's why we used this...
  } catch (error) {
    reportError(error)
  }
}

const getBackers = async (id) => {
  try {
    if (!ethereum) return alert('Please install Metamask')
    const contract = getEtheriumContract()
    let backers = await contract.getBackers(id)

    setGlobalState('backers', structuredBackers(backers))
  } catch (error) {
    reportError(error)
  }
}

const deleteProject = async (id) => {
  try {
    if (!ethereum) return alert('Please install Metamask')
    const contract = getEtheriumContract()
    await contract.deleteProject(id)

    await loadProjects() // The blockchain is too slow for this...
    window.location.reload() // That's why we used this...
  } catch (error) {
    reportError(error)
  }
}

const loadProject = async (id) => {
  try {
    if (!ethereum) return alert('Please install Metamask')
    const contract = getEtheriumContract()
    let project = await contract.getProject(id)
    project = structuredProjects([project])[0]

    setGlobalState('project', project)
    await getBackers(id)
    console.log("Project Loaded...")
  } catch (error) {
    alert(JSON.stringify(error.message))
    reportError(error)
  }
}

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

    const contract = getEtheriumContract()
    const projects = await contract.getProjects()
    const stats = await contract.stats()

    setGlobalState('stats', structureStats(stats))
    setGlobalState('projects', structuredProjects(projects))
  } catch (error) {
    reportError(error)
  }
}

const structuredBackers = (backers) =>
  backers
    .map((backer) => ({
      owner: backer.owner,
      refunded: backer.refunded,
      timestamp: new Date(backer.timestamp.toNumber() * 1000).toJSON(),
      contribution: parseInt(backer.contribution._hex) / 10 ** 18,
    }))
    .reverse()

const structuredProjects = (projects) =>
  projects
    .map((project) => ({
      id: project.id.toNumber(),
      owner: project.owner,
      title: project.title,
      description: project.description,
      timestamp: new Date(project.timestamp.toNumber()).getTime(),
      expiresAt: new Date(project.expiresAt.toNumber()).getTime(),
      date: toDate(project.expiresAt.toNumber() * 1000),
      imageURL: project.imageURL,
      raised: parseInt(project.raised._hex) / 10 ** 18,
      cost: parseInt(project.cost._hex) / 10 ** 18,
      backers: project.backers.toNumber(),
      status: project.status,
    }))
    .reverse()

const toDate = (timestamp) => {
  const date = new Date(timestamp)
  const dd = date.getDate() > 9 ? date.getDate() : `0${date.getDate()}`
  const mm =
    date.getMonth() + 1 > 9 ? date.getMonth() + 1 : `0${date.getMonth() + 1}`
  const yyyy = date.getFullYear()
  return `${yyyy}-${mm}-${dd}`
}

const structureStats = (stats) => ({
  totalProjects: stats.totalProjects.toNumber(),
  totalBacking: stats.totalBacking.toNumber(),
  totalDonations: parseInt(stats.totalDonations._hex) / 10 ** 18,
})

const reportError = (error) => {
  console.log(error.message)
  throw new Error('No ethereum object.')
}

export {
  getEtheriumContract,
  isWallectConnected,
  connectWallet,
  createProject,
  updateProject,
  deleteProject,
  loadProjects,
  loadProject,
  backProject,
  payoutProject,
  refundProject,
}


If you need my help resolving issues on your project, consult me on this page.


The Index.jsx File This last step is responsible for initializing your CometChat service. On the src folder, open the **index.jsx** and replace its code with the one below.


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


And there you have it, you just completed the entire build.

Run the command below to start up your server.


yarn start



Conclusion

We've reached the end of this Crowd Funding build, and I know you've gained a lot of value from working with me.


Whatever level you are, if you want to grow faster in your web3 development skills, get into my private class.


Till next time, keep building!

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.