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
-
- 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.
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.
Then, you will be asked to select the smart contract development environment tool. We will be using hardhat for this tutorial.
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.
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:
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
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}
>
×
</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}
>
×
</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: