Building a Decentralized Crowdfunding Platform on Celo using Celo-Composer

Building a Decentralized Crowdfunding Platform on Celo using Celo-Composer https://celo.academy/uploads/default/optimized/2X/d/d58078c859c496b75b8705fe4f080f1a6f55fe9c_2_1024x576.png
none 0.0 0

Introduction

Crowdfunding has democratized the fundraising process, allowing anyone with an idea to potentially access capital and bring their vision to life.

In this tutorial, we will be walking through the basic steps of setting up the project using Celo-Composer, developing the crowdfunding smart contract, deploying the smart contract to the Celo blockchain, and building the dapp using React, TailwindCSS, and react-celo.

Prerequisites

For the purpose of this tutorial, you will need to have some level of knowledge of Javascript, React, tailwindcss, and Solidity.

Requirements

  • A code editor VScode preferably
  • A chrome browser
  • A crypto wallet
    1. Fund your wallet using the Celo Faucet. To do this, copy the wallet address and visit the Celo Faucet. Paste your wallet address into the designated field and confirm.
  • Nodejs is installed on your computer. If not, download it from here

Check out this video on how to install and set up the celo-wallet-extension.

Project Initialization

In your terminal run

npx @celo/celo-composer create

This will prompt you to select the framework and template you want to use. For the purpose of this tutorial, select React as the framework you will be using.

226510672-8845faf7-8252-4e98-a748-53a96c6ce9fc

Once you have selected the framework, you will be asked to select the web3 library you will be using for the react-app. For the purpose of this tutorial, we will be using react-celo as our preferred web3 library.

226184100-a6b77a72-9363-4178-b547-9370b8ebc40f

Then, you will be asked to select the smart contract development environment tool. We will be using hardhat for this tutorial.

226184163-6e7f7829-c21f-4d77-bc36-3529c9d69350

After selecting the tool, you will be asked if you want subgraph support for your dApp. Select no as we will not be using a subgraph for the dApp in this tutorial.

226184211-f98ece16-ade5-4bf4-84ac-6fe30cb0c167

Finally, you will be asked to enter the name of the project folder that will house your dApp. Type in any name of your choice and click the enter key.

Open the project folder you created in a code editor (Vs code). The content of the folder should look like this:

project-file-structure

From the above image, my root project folder is named crowdfunding-dapp

In the packages folder, you should see a hardhat and react-app folder. The hardhat folder contains a hardhat-project with the necessary setup we need to create and deploy our simple smart contract. While the react-app folder houses the react starter files for our dApp.

In your terminal pointing at the root project folder, run this command to install the dependencies in the package.json files

yarn install

After all dependencies have been installed, we can now go ahead and create our smart contract.

The Smart Contract

In your hardhat folder, you will find a contracts folder, go ahead and delete the solidity files present in the contracts folder. Then create a new file in the contracts folder and name it Crowdfunding.sol.

In the Crowdfunding.sol file, copy and paste this code.

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

contract Crowdfunding {
    // Struct to represent a campaign
    struct Campaign {
        address payable creator;
        string name;
        string description;
        string image;
        uint256 goal;
        uint256 raised;
        uint256 deadline;
        bool completed;
    }

    // Maps a campaign ID to a nested mapping of addresses to donations
    mapping(uint256 => mapping(address => uint256)) public donations;

    // Array to hold all campaigns
    Campaign[] public campaigns;

    // Event to signal the creation of a new campaign
    event NewCampaign(
        uint256 campaignId,
        string name,
        address creator,
        uint256 goal,
        uint256 deadline
    );

    // Function to create a new campaign
    function createCampaign(
        string memory _name,
        uint256 _goal,
        uint256 duration,
        string memory _description,
        string memory _image
    ) public {
        uint256 deadline = block.timestamp + duration;
        campaigns.push(
            Campaign(
                payable(msg.sender),
                _name,
                _description,
                _image,
                _goal,
                0,
                deadline,
                false
            )
        );
        uint256 campaignId = campaigns.length - 1;
        emit NewCampaign(campaignId, _name, msg.sender, _goal, deadline);
    }

    // Function to donate to a campaign
    function donate(uint256 campaignId) public payable {
        Campaign storage campaign = campaigns[campaignId];
        require(!campaign.completed, "Campaign has already completed");
        require(
            block.timestamp < campaign.deadline,
            "Campaign deadline has passed"
        );
        donations[campaignId][msg.sender] += msg.value;
        campaign.raised += msg.value;
    }

    // Function to check the amount raised by a campaign
    function getAmountRaised(uint256 campaignId) public view returns (uint256) {
        Campaign storage campaign = campaigns[campaignId];
        return campaign.raised;
    }

    // Function to check if a campaign has reached its goal
    function hasReachedGoal(uint256 campaignId) public view returns (bool) {
        Campaign storage campaign = campaigns[campaignId];
        return campaign.raised >= campaign.goal;
    }

    // Function to complete a campaign and transfer funds to the creator
    function completeCampaign(uint256 campaignId) public {
        Campaign storage campaign = campaigns[campaignId];
        require(!campaign.completed, "Campaign has already completed");
        require(
            hasReachedGoal(campaignId),
            "Campaign has not reached its goal"
        );
        campaign.completed = true;
        campaign.creator.transfer(campaign.raised);
    }

    function getCampaigns() public view returns (Campaign[] memory) {
        return campaigns;
    }
}

In the code above, we declared a contract and called it Crowdfunding.

struct Campaign {
    address payable creator;
    uint256 goal;
    uint256 raised;
    uint256 deadline;
    bool completed;
}

Then we created a Campaign struct. This struct defines the properties of a campaign. Each campaign has a creator (an Ethereum address), a fundraising goal, the amount raised, a deadline (specified as a timestamp), and a completion status.\

mapping(uint256 => mapping(address => uint256)) public donations;

The mapping associates each campaign ID with a nested mapping that maps addresses to donation amounts. It allows tracking of the donations made by different addresses to different campaigns.

campaign[] public campaigns;

This array holds all the created campaigns, allowing the contract to store and manage multiple crowdfunding campaigns.

event NewCampaign(
        uint256 campaignId,
        string name,
        address creator,
        uint256 goal,
        uint256 deadline
    );

This event is emitted when a new campaign is created. It provides information about the campaign ID, campaign name, the creator’s address, the fundraising goal, and the deadline.

function createCampaign(
        string memory _name,
        uint256 _goal,
        uint256 duration,
        string memory _description,
        string memory _image
    ) public {
        uint256 deadline = block.timestamp + duration;
        campaigns.push(
            Campaign(
                payable(msg.sender),
                _name,
                _description,
                _image,
                _goal,
                0,
                deadline,
                false
            )
        );
        uint256 campaignId = campaigns.length - 1;
        emit NewCampaign(campaignId, _name, msg.sender, _goal, deadline);
    }

This function allows users to create a new crowdfunding campaign. It takes the fundraising goal and deadline, description, and image as parameters. The function creates a new Campaign struct with the provided parameters, sets the initial amount raised to 0, marks it as incomplete, and adds it to the campaigns array. Finally, it emits the NewCampaign event with the relevant campaign details.

function donate(uint256 campaignId) public payable {
    Campaign storage campaign = campaigns[campaignId];
    require(!campaign.completed, "Campaign has already completed");
    require(block.timestamp < campaign.deadline, "Campaign deadline has passed");
    donations[campaignId][msg.sender] += msg.value;
    campaign.raised += msg.value;
}

This function allows users to donate funds to a specific campaign. The campaignId parameter determines the target campaign. The function checks that the campaign has not already been completed and that the current timestamp is before the campaign deadline. It then updates the donations mapping with the donated amount for the respective campaign and sender. Additionally, it increases the raised amount of the campaign by the donated value.

function getAmountRaised(uint256 campaignId) public view returns (uint256) {
    Campaign storage campaign = campaigns[campaignId];
    return campaign.raised;
}

This function allows users to retrieve the total amount raised by a specific campaign. It takes the campaignId as a parameter and returns the corresponding campaign’s raised value.

function hasReachedGoal(uint256 campaignId) public view returns (bool) {
    Campaign storage campaign = campaigns[campaignId];
    return campaign.raised >= campaign.goal;
}

This function checks whether a campaign has reached its fundraising goal. It takes the campaignId as a parameter and returns a boolean value indicating whether the raised amount of the campaign is greater than or equal to the goal amount.

function completeCampaign(uint256 campaignId) public {
    Campaign storage campaign = campaigns[campaignId];
    require(!campaign.completed, "Campaign has already completed");
    require(hasReachedGoal(campaignId), "Campaign has not reached its goal");
    campaign.completed = true;
    campaign.creator.transfer(campaign.raised);
}

This function allows the creator of a campaign to complete it and transfer the raised funds to their address. It first checks that the campaign is not already completed and that it has reached its goal. If both conditions are met, it sets the completed flag of the campaign to true and transfers the raised amount to the creator address using the transfer function.

Compiling The Smart Contract

Before deploying your smart contract to the Celo blockchain, you will have to compile the smart contract.

To compile the smart contract, run these commands to change the directory of your terminal to point to the hardhat folder.

cd packages
cd hardhat

Then create a .env file in the hardhat folder and add your MNEMONIC key in the .env file like this

MNEMONIC=//add your wallet seed phrase here

Now you will install dotenv package to be able to import the env file and use it in our config.

Enter this command in your terminal.

yarn add dotenv

Next, open the hardhat.config.js file and replace the content of the file with this code.

require("@nomicfoundation/hardhat-chai-matchers")
require('dotenv').config({path: '.env'});
require('hardhat-deploy');

// You need to export an object to set up your config
// Go to https://hardhat.org/config/ to learn more

// Prints the Celo accounts associated with the mnemonic in .env
task("accounts", "Prints the list of accounts", async (taskArgs, hre) => {
  const accounts = await hre.ethers.getSigners();

  for (const account of accounts) {
    console.log(account.address);
  }
});

/**
 * @type import('hardhat/config').HardhatUserConfig
 */
module.exports = {
  defaultNetwork: "alfajores",
  networks: {
    localhost: {
        url: "http://127.0.0.1:7545"
    },
    alfajores: {
      gasPrice: 200000000000,
      gas: 41000000000,
      url: "https://alfajores-forno.celo-testnet.org",
      accounts: {
        mnemonic: process.env.MNEMONIC,
        path: "m/44'/52752'/0'/0"
      },
      //chainId: 44787
    },
    celo: {
      url: "https://forno.celo.org",
      accounts: {
        mnemonic: process.env.MNEMONIC,
        path: "m/44'/52752'/0'/0"
      },
      chainId: 42220
    },     
  },
  solidity: "0.8.4",
};

Now, to compile the contract, run this command in your terminal.

npx hardhat compile

Once the compilation is successful, you should see this message on your terminal

compilation-success

Deploying The Smart Contract

Now, you will be deploying the smart contract to the Alfajores testnet, to do this, replace the content of the 00-deploy.js file with the code below.

const hre = require("hardhat");

async function main() {
  const Crowdfunding = await hre.ethers.getContractFactory("Crowdfunding");
  const crowdfunding = await Crowdfunding.deploy();

  await crowdfunding.deployed();

  console.log("Crowdfunding deployed to:", crowdfunding.address);
}

main()
  .then(() => process.exit(0))
  .catch((error) => {
    console.error(error);
    process.exit(1);
  });

Then, run the following command in your terminal to deploy the smart contract to the Celo blockchain.

npx hardhat run deploy/00-deploy.js --network alfajores

Save the smart contract address that will be printed on your terminal when deployment is successful somewhere because you will need it further down in the tutorial.

Building The Dapp

In this section, we will be building a simple UI for our Dapp, connecting our wallet to the Dapp, and also communicating with our smart contract deployed to the Celo blockchain.

First, we need to install the dependencies we will be using in this section.

Change the directory of your terminal to point to the react-app folder and run this command in the terminal to install the dependencies

yarn add bignumber.js

In the react-app folder, create a new JavaScript file and name it crowdfunding.abi.js. Then, go into your hardhat folder. Under the hardhat folder, there is an artifact folder, and in that artifact folder, there is also a contracts folder, which has a Crowdfunding.json file. In that file, copy the abi value and paste it into the crowdfunding.abi.js file you created like this.

[
    {
      "anonymous": false,
      "inputs": [
        {
          "indexed": false,
          "internalType": "uint256",
          "name": "campaignId",
          "type": "uint256"
        },
        {
          "indexed": false,
          "internalType": "string",
          "name": "name",
          "type": "string"
        },
        {
          "indexed": false,
          "internalType": "address",
          "name": "creator",
          "type": "address"
        },
        {
          "indexed": false,
          "internalType": "uint256",
          "name": "goal",
          "type": "uint256"
        },
        {
          "indexed": false,
          "internalType": "uint256",
          "name": "deadline",
          "type": "uint256"
        }
      ],
      "name": "NewCampaign",
      "type": "event"
    },
    {
      "inputs": [
        {
          "internalType": "uint256",
          "name": "",
          "type": "uint256"
        }
      ],
      "name": "campaigns",
      "outputs": [
        {
          "internalType": "address payable",
          "name": "creator",
          "type": "address"
        },
        {
          "internalType": "string",
          "name": "name",
          "type": "string"
        },
        {
          "internalType": "string",
          "name": "description",
          "type": "string"
        },
        {
          "internalType": "string",
          "name": "image",
          "type": "string"
        },
        {
          "internalType": "uint256",
          "name": "goal",
          "type": "uint256"
        },
        {
          "internalType": "uint256",
          "name": "raised",
          "type": "uint256"
        },
        {
          "internalType": "uint256",
          "name": "deadline",
          "type": "uint256"
        },
        {
          "internalType": "bool",
          "name": "completed",
          "type": "bool"
        }
      ],
      "stateMutability": "view",
      "type": "function"
    },
    {
      "inputs": [
        {
          "internalType": "uint256",
          "name": "campaignId",
          "type": "uint256"
        }
      ],
      "name": "completeCampaign",
      "outputs": [],
      "stateMutability": "nonpayable",
      "type": "function"
    },
    {
      "inputs": [
        {
          "internalType": "string",
          "name": "_name",
          "type": "string"
        },
        {
          "internalType": "uint256",
          "name": "_goal",
          "type": "uint256"
        },
        {
          "internalType": "uint256",
          "name": "duration",
          "type": "uint256"
        },
        {
          "internalType": "string",
          "name": "_description",
          "type": "string"
        },
        {
          "internalType": "string",
          "name": "_image",
          "type": "string"
        }
      ],
      "name": "createCampaign",
      "outputs": [],
      "stateMutability": "nonpayable",
      "type": "function"
    },
    {
      "inputs": [
        {
          "internalType": "uint256",
          "name": "campaignId",
          "type": "uint256"
        }
      ],
      "name": "donate",
      "outputs": [],
      "stateMutability": "payable",
      "type": "function"
    },
    {
      "inputs": [
        {
          "internalType": "uint256",
          "name": "",
          "type": "uint256"
        },
        {
          "internalType": "address",
          "name": "",
          "type": "address"
        }
      ],
      "name": "donations",
      "outputs": [
        {
          "internalType": "uint256",
          "name": "",
          "type": "uint256"
        }
      ],
      "stateMutability": "view",
      "type": "function"
    },
    {
      "inputs": [
        {
          "internalType": "uint256",
          "name": "campaignId",
          "type": "uint256"
        }
      ],
      "name": "getAmountRaised",
      "outputs": [
        {
          "internalType": "uint256",
          "name": "",
          "type": "uint256"
        }
      ],
      "stateMutability": "view",
      "type": "function"
    },
    {
      "inputs": [],
      "name": "getCampaigns",
      "outputs": [
        {
          "components": [
            {
              "internalType": "address payable",
              "name": "creator",
              "type": "address"
            },
            {
              "internalType": "string",
              "name": "name",
              "type": "string"
            },
            {
              "internalType": "string",
              "name": "description",
              "type": "string"
            },
            {
              "internalType": "string",
              "name": "image",
              "type": "string"
            },
            {
              "internalType": "uint256",
              "name": "goal",
              "type": "uint256"
            },
            {
              "internalType": "uint256",
              "name": "raised",
              "type": "uint256"
            },
            {
              "internalType": "uint256",
              "name": "deadline",
              "type": "uint256"
            },
            {
              "internalType": "bool",
              "name": "completed",
              "type": "bool"
            }
          ],
          "internalType": "struct Crowdfunding.Campaign[]",
          "name": "",
          "type": "tuple[]"
        }
      ],
      "stateMutability": "view",
      "type": "function"
    },
    {
      "inputs": [
        {
          "internalType": "uint256",
          "name": "campaignId",
          "type": "uint256"
        }
      ],
      "name": "hasReachedGoal",
      "outputs": [
        {
          "internalType": "bool",
          "name": "",
          "type": "bool"
        }
      ],
      "stateMutability": "view",
      "type": "function"
    }
  ]

The array above is the smart contract’s ABI (Application Binary Interface). The ABI defines the methods and variables that are available in a smart contract and that we can use to interact with that smart contract.

Still pointing to the react-app folder on your terminal, run this command to start up the development server

yarn run dev

This starts the development server at localhost:3000. You can view the application on this port.

This is what it should look like as provided by celo-composer

We will be adding a few touches of our own to the webpage and we will be starting off with the header.

To do this, replace the content of the header.tsx file located in the components folder with this code.

import { useCelo } from "@celo/react-celo";
import Image from "next/image";
import { useEffect, useState, useCallback } from "react";
import { BigNumber } from "bignumber.js";

import CreateCampaignModal from "./modals/createCampaign";

export default function Header() {
  const [userBalance, setUserBalance] = useState("");
  const [openCreatCampaignModal, setOpenCreatCampaignModal] = useState(false);

  let { address, kit, connect, disconnect } = useCelo();

  const onClose = () => {
    setOpenCreatCampaignModal(false);
  }

  const getUserBalance = useCallback(async () => {
    const celoToken = await kit.contracts.getGoldToken();
    const userBalance = await celoToken.balanceOf(address!);
    const celoTokenBalance = new BigNumber(userBalance)
      .shiftedBy(-18)
      .toFixed(2);
    setUserBalance(celoTokenBalance);
  }, [address, kit.contracts]);

  useEffect(() => {
    if (address) {
      getUserBalance();
    }
  }, [getUserBalance, address]);

  return (
    <div className="flex justify-between p-4">
      <h2 className="font-bold text-xl">Cel-Funding</h2>
      {address ? (
        <div className="flex justify-between w-[330px]">
          <div className="flex flex-col justify-center items-center">
            {userBalance ? `${userBalance}celo` : "0celo"}
          </div>
          <button
            className="bg-purple-500 text-white p-2 rounded-md"
            onClick={() => setOpenCreatCampaignModal(true)}
          >
            Create Campaign
          </button>
          <button
            className="bg-red-500 text-white rounded-md p-2"
            onClick={disconnect}
          >
            Disconnect
          </button>
        </div>
      ) : (
        <button
          className="bg-blue-500 text-white rounded-md p-3.5"
          onClick={connect}
        >
          Connect Wallet
        </button>
      )}
      {openCreatCampaignModal ? (
        <CreateCampaignModal onClose={onClose}/>
      ) : null}
    </div>
  );
}

Now your Header should look like this

Once a user clicks on the connect wallet button, and successfully connects their wallet, the header should now look like this.

Users can create a new crowdfunding campaign, by clicking on the Create Campaign button, which opens a form modal for users to enter the crowdfunding details.

To create the form modal for the creation of a new crowdfunding campaign, in the components folder, create a folder and name it modals. Then, in the modals folder, create a file and name it createCampaign.tsx

In the createCampaign.tsx copy and paste this code

import { useState } from "react";
import { useCelo } from "@celo/react-celo";
import { AbiItem } from "web3-utils";

import crowdFundingAbi from "../../crowdfunding.abi";

type ModalProps = {
  onClose: (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
};

const CreateCampaignModal = ({ onClose }: ModalProps) => {
  const [name, setName] = useState("");
  const [imageUrl, setImageUrl] = useState("");
  const [description, setDescription] = useState("");
  const [startTime, setStartTime] = useState("");
  const [endTime, setEndTime] = useState("");
  const [goal, setGoal] = useState("");

  const { kit, address } = useCelo();

  const contractAddress = "0x3f9ee235e10948B45dd4B3c52FE97401bcbC9eF7";

  const createCampaign = async (e: React.SyntheticEvent) => {
    e.preventDefault();
    console.log(name, imageUrl, description, startTime, endTime, goal);

    const startTimeArray = startTime.split(":");
    const endTimeArray = endTime.split(":");

    const startTimeInSeconds =
      parseInt(startTimeArray[0], 10) * 60 * 60 +
      parseInt(startTimeArray[1], 10) * 60;
    const endTimeInSeconds =
      parseInt(endTimeArray[0], 10) * 60 * 60 +
      parseInt(endTimeArray[1], 10) * 60;

    const timeInSeconds = endTimeInSeconds - startTimeInSeconds;

    console.log(startTimeInSeconds, endTimeInSeconds, timeInSeconds);

    const contract = new kit.connection.web3.eth.Contract(
      crowdFundingAbi as AbiItem[],
      contractAddress
    );

    await contract.methods
      .createCampaign(name, goal, timeInSeconds, description, imageUrl)
      .send({ from: address });
  };

  return (
    <div className="fixed flex justify-center items-center bg-[rgba(0,0,0,0.5)] z-50 top-0 left-0 bottom-0 right-0 w-full h-full">
      <div className="bg-white p-4 rounded">
        <div className="flex justify-between w-[400px]">
          <h2 className="text-2xl font-medium">Create Auction</h2>
          <div
            className="text-[28px] font-medium cursor-pointer"
            onClick={onClose}
          >
            &times;
          </div>
        </div>
        <form className="mt-4" onSubmit={createCampaign}>
          <div>
            <div className="w-full mb-3">
              <input
                type="text"
                className="border border-solid border-black p-2 w-full rounded"
                placeholder="Enter name of campaign"
                onChange={(e) => setName(e.target.value)}
              />
            </div>
            <div className="w-full mb-3">
              <input
                type="text"
                className="border border-solid border-black p-2 w-full rounded"
                placeholder="Enter image url"
                onChange={(e) => setImageUrl(e.target.value)}
              />
            </div>
            <div className="w-full mb-3">
              <input
                type="text"
                className="border border-solid border-black p-2 w-full rounded"
                placeholder="Enter description"
                onChange={(e) => setDescription(e.target.value)}
              />
            </div>
            <div className="w-full mb-3">
              <label>Enter campaign start time</label>
              <input
                type="time"
                className="border border-solid border-black p-2 w-full rounded"
                placeholder="Enter campaign end time"
                onChange={(e) => setStartTime(e.target.value)}
              />
            </div>
            <div className="w-full mb-3">
              <label>Enter campaign end time</label>
              <input
                type="time"
                className="border border-solid border-black p-2 w-full rounded"
                placeholder="Enter campaign end time"
                onChange={(e) => setEndTime(e.target.value)}
              />
            </div>
            <div className="w-full mb-3">
              <input
                type="text"
                className="border border-solid border-black p-2 w-full rounded"
                placeholder="Enter campaign goal"
                onChange={(e) => setGoal(e.target.value)}
              />
            </div>
          </div>
          <div className="text-center mt-4">
            <button
              type="submit"
              className="bg-green-500 text-white rounded-md p-2"
            >
              Submit
            </button>
          </div>
        </form>
      </div>
    </div>
  );
};

export default CreateCampaignModal;

The code above defines a modal component that displays a form for creating a new campaign. It captures the input values, performs some processing, and then uses Web3 to interact with a smart contract to create the campaign.

In the createCampaign function, a new instance of the web3 contract we developed is created using the crowdFundingAbi and contractAddress.

In the modals folder, create another file and name it contribute.tsx

In the contribute.tsx file copy and paste this code

import { useState } from "react";
import { useCelo } from "@celo/react-celo";
import { BigNumber } from "bignumber.js";

import crowdFundingAbi from "../../crowdfunding.abi";
import {AbiItem} from "web3-utils"

type ContributeModalProps = {
  onClose: (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
  index: number | undefined
}

const ContributeToCampaign = ({onClose, index} : ContributeModalProps) => {
  const [amount, setAmount] = useState("")

  const {address, kit} = useCelo();

  const contractAddress = "0x3f9ee235e10948B45dd4B3c52FE97401bcbC9eF7";

  const donate = async (e: React.SyntheticEvent) => {
    e.preventDefault();

    const contract = new kit.connection.web3.eth.Contract(crowdFundingAbi as AbiItem[], contractAddress);

    await contract.methods.donate(index).send({from: address, value: amount});
  }
  
  return (
    <div className="fixed flex justify-center items-center bg-[rgba(0,0,0,0.5)] z-50 top-0 left-0 bottom-0 right-0 w-full h-full">
      <div className="bg-white p-4 rounded">
        <div className="flex justify-between w-[400px]">
          <h2 className="text-2xl font-medium">Create Auction</h2>
          <div
            className="text-[28px] font-medium cursor-pointer"
            onClick={onClose}
          >
            &times;
          </div>
        </div>
        <form className="mt-4" onSubmit={donate}>
          <div>
            <div className="w-full mb-3">
              <input
                type="text"
                className="border border-solid border-black p-2 w-full rounded"
                placeholder="Enter donation Amount"
                onChange={(e) => setAmount(e.target.value)}
                required
              />
            </div>
          </div>
          <div className="text-center mt-4">
            <button
              type="submit"
              className="bg-green-500 text-white rounded-md p-2"
            >
              Contribute
            </button>
          </div>
        </form>
      </div>
    </div>
  );
};

export default ContributeToCampaign;

The above code sets up a modal component in React that allows users to contribute to a crowdfunding campaign by interacting with the smart contract.

Next, in the index.tsx file located in the pages folder, copy and paste the code below.

import Image from "next/image";
import { useState, useEffect, useCallback } from "react";
import { useCelo } from "@celo/react-celo";
import { AbiItem } from "web3-utils";

import ContributeToCampaign from "@/components/modals/contribute";
import crowdFundingAbi from "../crowdfunding.abi";

export default function Home() {
  const [openContributeForm, setOpenContributeForm] = useState(false);
  const [campaignIndex, setCampaignIndex] = useState<number>();
  const [campaigns, setCampaigns] = useState<any[]>();

  const { address, kit } = useCelo();

  const contractAddress = "0x3f9ee235e10948B45dd4B3c52FE97401bcbC9eF7";

  let deadlines: string[] = [];

  if (campaigns) {
    campaigns.map((campaign) => {
      const dateFormat = new Date(+campaign.deadline * 1000);
      const dealine: string = `${dateFormat.getDate()} ${dateFormat.toLocaleString(
        "default",
        { month: "short" }
      )} ${dateFormat.getFullYear()} ${dateFormat.getHours()}:${dateFormat.getMinutes()}:${dateFormat.getSeconds()}`;

      deadlines.push(dealine);
    });
  }

  const onClose = () => {
    setOpenContributeForm(false);
  };

  function contributeForm(index: number) {
    setCampaignIndex(index);
    setOpenContributeForm(true);
  }

  const getCampaigns = useCallback(async () => {
    const contract = new kit.connection.web3.eth.Contract(
      crowdFundingAbi as AbiItem[],
      contractAddress
    );

    const campaigns: [] | undefined = await contract.methods
      .getCampaigns()
      .call();

    setCampaigns(campaigns);
  }, [kit.connection.web3.eth.Contract]);

  useEffect(() => {
    getCampaigns();
  }, [getCampaigns]);

  async function withdrawFunds(index: number) {
    const contract = new kit.connection.web3.eth.Contract(
      crowdFundingAbi as AbiItem[],
      contractAddress
    );

    await contract.methods.completeCampaign(index).call();
  }

  return (
    <div className="relative">
      <div className="">
        <div className="grid lg:grid-cols-3 md:grid-cols-2 sm:grid-cols-1 gap-10">
          {campaigns
            ? campaigns.map((campaign, index) => (
                <div
                  className="shadow-xl rounded-md relative h-fit"
                  key={index}
                >
                  <div className="w-full">
                    <Image
                      src="https://images.pexels.com/photos/974314/pexels-photo-974314.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1"
                      alt=""
                      width={350}
                      height={350}
                      className="w-full object-contain relative"
                    />
                  </div>
                  <div className="p-3">
                    <div>
                      <h3 className="font-bold text-lg mb-2">
                        {campaign.name}
                      </h3>
                      <p className="">{campaign.description}</p>
                    </div>
                    <div className="text-sm mt-5">
                      dealine: {deadlines[index]}
                    </div>
                    <div className="mt-5">
                      <div className="flex justify-between">
                        <div>
                          <p>Goal: {campaign.goal} Celo</p>
                        </div>
                        <div>
                          <p>Raised: {campaign.raised} Celo</p>
                        </div>
                      </div>
                      <div className="text-sm">
                        {/* <p>{auctionDurations[index]}</p> */}
                      </div>

                      {address === campaign.creator ? (
                        <div className="flex justify-between">
                          <button
                            className="bg-green-500 text-white p-2 rounded mt-4 w-[40%]"
                            onClick={() => contributeForm(0)}
                          >
                            Donate
                          </button>
                          <button
                            className="bg-red-500 text-white p-2 rounded mt-4 w-[40%]"
                            onClick={() => withdrawFunds(index)}
                          >
                            Withdraw Funds
                          </button>{" "}
                        </div>
                      ) : (
                        <div className="">
                          {" "}
                          <button
                            className="bg-green-500 text-white p-2 rounded mt-4 w-full"
                            onClick={() => contributeForm(index)}
                          >
                            Donate
                          </button>{" "}
                        </div>
                      )}
                    </div>
                  </div>
                </div>
              ))
            : null}
        </div>
      </div>
      {openContributeForm ? (
        <ContributeToCampaign onClose={onClose} index={campaignIndex} />
      ) : null}
    </div>
  );
}

The code above sets up our homepage. The homepage displays a grid of campaign cards and allows users to contribute to or withdraw funds from a campaign through a modal component.

The getCampaigns function is defined using the useCallback hook. It creates a new instance of a web3 contract using the crowdFundingAbi and the contractAddress. It then calls the getCampaigns method on the contract and sets the campaigns state variable with the result.

This is what your dapp should look like ones you have created a champaign

Conclusion

So far, we have been able to create a crowdfunding smart contract, deploy the contract to the Celo blockchain, and create a dapp to interact with the crowdfunding smart contract using Celo-Composer.

About The Author

I am a React frontend developer with over 3 years of experience building for the web, a web3 developer, and a technical writer. Visit my GitHub profile to see some of the projects I have worked on and currently working on and also check out my profile on LinkedIn.

References

If you wish to read more on celo-composer and react-celo check out these docs:

4 Likes

Congratulations on being among the top voted proposals this week! This is now approved by @Celo_Academy for you to get started whenever you’d like. :mortar_board: :seedling:

4 Likes

@4undRaiser This is the correct tutorial for this content.

1 Like

okay. you can make those fixes i mentioned here

1 Like

I have made the corrections already

1 Like

@Joshy you can move to publish

2 Likes

Nice content @Joshy . Thanks :+1: