Building a Project with SE-2 | Crowd Fund | Part Three | The Components
The last blog went over the Contract. This post will cover the frontend components.
If you are new to Scaffold-ETH-2 (SE-2), check out my Intro into Scaffold-ETH-2 blog.
Want to learn a little bit more before you get started? Here’s a post about writing, reading, and listening to Smart Contracts.
Entry Point
It isn’t necessary to add this component to your own project, but it is good for learning-purposes. It simply shows you the most simplistic aspects of The RainbowKit Connection. You can also look into _app.tsx to see how RainbowKitProvider comes into play. We’re a little too under-the-hood at the moment, but if you want to truly understand how SE-2 is offering so much blockchain functionality, those are some good areas to start (as well as the WagmiConfig in _app.tsx).
Zooming out a bit, we can see that Entry Point serves the purpose of showing us some buttons that we can only use after we have connected our wallet:
//entry to dapp
return (
<>
<Link href="/crowdfund/start-fund-run" passHref className="link">
<div className="tooltip tooltip-primary" data-tip="Start your Fund Run today!">
<button className="m-2 btn btn-primary">Start Fund Run</button>
</div>
</Link>
<Link href="/crowdfund/browse-fund-runs" passHref className="link">
<div className="tooltip tooltip-primary" data-tip="Donate to projects">
<button className="m-2 btn btn-primary">Start Donating</button>
</div>
</Link>
</>
);
})()}
This ☝️ returns these buttons 👇
Create a Fund Run
Now, let’s learn how our frontend can call the Smart Contract from Part Two.
All it takes ( THANKS TO SE-2 🥳 ) to call our function createFundRun
on the Smart Contract is this…
const { writeAsync, isLoading } = useScaffoldContractWrite({
contractName: "CrowdFund",
functionName: "createFundRun",
args: [titleInput, descInput, targetInput, deadlineInput],
onBlockConfirmation: txnReceipt => {
console.log("📦 Transaction blockHash", txnReceipt.blockHash);
if (txnReceipt.status === "success") {
console.log(txnReceipt);
}
},
});
☝️☝️☝️Note that we are defining the Contract Name as well as the Function Name we are writing to. In this particular instance, we are also sending the args: Title, Description, Target, and Deadline.
This is a non-payable function (on the contract) that we are calling (createFundRun
) with SE-2’s custom hook useScaffoldContractWrite, but the same hook can also be used for payable functions as well. You can see in the code-snippet below that there is little difference between the previous example and how we call a payable function (Line 40):
const { writeAsync, isLoading } = useScaffoldContractWrite({
contractName: "CrowdFund",
functionName: "donateToFundRun",
args: [fundRunSingle?.id],
onBlockConfirmation: txnReceipt => {
console.log("📦 Transaction blockHash", txnReceipt.blockHash);
},
value: donationInput,
});
☝️☝️☝️Here we are still using useScaffoldContractWrite, but now we are sending it the ‘Value’ as well (donationInput
), and that is how you call a payable function with SE-2.
Use writeAsync like this: writeAsync();
isLoading is a boolean you can use to show progress to the user (with spinners), or you can use it to disable the button that was clicked...
{isLoading ?
<span className="loading loading-spinner loading-sm"></span>
:
<>
DONATE NOW
</>
}
Subscribing to Events with SE-2
When a user creates a Fund Run, we’ll need to redirect them to their new Fund Run. To do this, we’ll subscribe to the FundRunCreated Event with SE-2’s useScaffoldEventSubscriber.
IF we get a new ‘Fund Run Created’ event, then we will want to see if the new Fund Run has an owner that is … well, the user’s address … like this:
useScaffoldEventSubscriber({
contractName: "CrowdFund",
eventName: "FundRunCreated",
listener: logs => {
logs.map(log => {
const { id, owner, title, target } = log.args;
console.log(
"📡 New Fund Run Event \ncreator:",
owner,
"\nID: ",
id,
"\nTitle: ",
title,
"\n with a target of: ",
target,
);
if (userAccount.address == owner) router.push(`/crowdfund/${id}`);
});
},
});
☝️The last line checks if the user’s wallet address is the owner of this (newly-created) Fund Run; IF it is, then it sends the user to that Fund Run’s page.
The Withdrawals
They both look very similar in this scenario…
Here is a bit of the Donor Withdrawal Component:
const { writeAsync, isLoading } = useScaffoldContractWrite({
contractName: "CrowdFund",
functionName: "fundRunDonorWithdraw",
args: [owner.id],
onBlockConfirmation: txnReceipt => {
console.log("📦 Transaction blockHash", txnReceipt.blockHash);
},
});
return (
<>
<button className="btn btn-primary" onClick={() => writeAsync()} disabled={isLoading}>
{isLoading ? <span className="loading loading-spinner loading-sm"></span> : <>Donor Withdraw</>}
</button>
</>
);
And also the Owner Withdrawal Component:
const { writeAsync, isLoading } = useScaffoldContractWrite({
contractName: "CrowdFund",
functionName: "fundRunOwnerWithdraw",
args: [owner.id],
onBlockConfirmation: txnReceipt => {
console.log("📦 Transaction blockHash", txnReceipt.blockHash);
},
});
return (
<>
<button className="btn btn-primary" onClick={() => writeAsync()} disabled={isLoading}>
{isLoading ? <span className="loading loading-spinner loading-sm"></span> : <>Owner Withdraw</>}
</button>
</>
);
As you can see they both use useScaffoldContractWrite, with the only difference being the function they are calling.
Reading from our contract
The useScaffoldContractRead hook is particularly handy. Simply give it a Function Name and a Contract Name:
const { data: fundRuns, isLoading: isListLoading } = useScaffoldContractRead({
contractName: "CrowdFund",
functionName: "getFundRuns",
});
The resulting data will be in fundRuns
, and isLoading
offers a boolean that I am using with a progress-spinner here:
if (isListLoading) {
return (
<div className="flex flex-col gap-2 p-2 m-4 mx-auto border shadow-xl border-base-300 bg-base-200 sm:rounded-lg">
<Spinner width="150px" height="150px" />
</div>
);
} else {
return (
<>
{fundRuns?.map(fund => (
<div
key={fund.id.toString()}
...
.....
☝️when done loading, the fundRuns
array gets displayed to the user.
The List Component has the Single Components (a .tsx primarily composed of html/tailwind):
return (
<>
{fundRuns?.map(fund => (
<div
key={fund.id.toString()}
className="flex flex-col gap-2 p-2 m-4 border shadow-xl border-base-300 bg-base-200 sm:rounded-lg"
>
<FundRun
title={fund.title}
description={fund.description}
target={fund.target}
deadline={fund.deadline.toString()}
amountCollected={fund.amountCollected}
amountWithdrawn={fund.amountWithdrawn}
isActive={fund.isActive}
/>
<div className="justify-end card-actions">
<Link href={`/crowdfund/${fund.id}`} passHref className="link">
<div className="tooltip tooltip-primary" data-tip="donate...">
<button className="btn btn-primary">View Fund Run</button>
</div>
</Link>
</div>
</div>
))}
</>
);
Similarly, retrieving just one Fund Run uses a Single FR Component as well.
Here is how to get a single Fund Run by its ID (from the contract):
const { data: fundRunSingle } = useScaffoldContractRead({
contractName: "CrowdFund",
functionName: "getFundRun",
args: fundRun,
});
Part Four is here, with tons of changes. In the next part, the contract will be altered to allow for a Fund Run to have multiple owners; therefore, simple ‘Owner Withdrawals’ will not be sufficient — these owners will have to create/support/partake in a proposals system where they all have to agree upon each transaction, before it can be sent. Click here to check it out.