🚀 Introduction
In this tutorial, we’ll walk through how to create a decentralized job board using React, Solidity, and RainbowKit/wagmi. This project allows clients to post jobs and developers to apply, with smart contracts managing the backend logic on a blockchain like Ethereum or Base.
🛠️ Step 1: Environment Setup
Prerequisites
- Node.js & npm
- Hardhat or Foundry for Solidity
- React (Vite or Create React App)
- wagmi, RainbowKit, ethers
Initialize the Project
npm create vite@latest job-board --template react-ts
cd job-board
npm install
npm install wagmi viem @rainbow-me/rainbowkit ethers
📜 Step 2: Create the Smart Contract
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
contract JobBoard {
uint public jobCount;
struct Job {
uint id;
address client;
string description;
uint256 budget;
bool isOpen;
address developer;
bool isPaid;
}
struct Application {
address developer;
string github;
string pitch;
bool hired;
}
mapping(uint => Job) public jobs;
mapping(uint => Application[]) public jobApplications;
event JobPosted(uint jobId, address client, uint amount);
event AppliedToJob(uint jobId, address developer);
event DeveloperHired(uint jobId, address developer, uint amount);
/// @notice Post a job and fund it with ETH
function postJob(string calldata _description) external payable {
require(msg.value > 0, "Payment required");
jobCount++;
jobs[jobCount] = Job({
id: jobCount,
client: msg.sender,
description: "_description,"
budget: msg.value,
isOpen: true,
developer: address(0),
isPaid: false
});
emit JobPosted(jobCount, msg.sender, msg.value);
}
/// @notice Developer applies to a job
function applyToJob(uint _jobId, string calldata _github, string calldata _pitch) external {
Job storage job = jobs[_jobId];
require(job.isOpen, "Job is closed");
jobApplications[_jobId].push(Application({
developer: msg.sender,
github: _github,
pitch: _pitch,
hired: false
}));
emit AppliedToJob(_jobId, msg.sender);
}
/// @notice Client selects a developer and funds are released
function hireDeveloper(uint _jobId, uint _applicantIndex) external {
Job storage job = jobs[_jobId];
require(msg.sender == job.client, "Not job owner");
require(job.isOpen, "Job already closed");
require(_applicantIndex < jobApplications[_jobId].length, "Invalid applicant");
Application storage chosenApp = jobApplications[_jobId][_applicantIndex];
job.developer = chosenApp.developer;
job.isOpen = false;
job.isPaid = true;
chosenApp.hired = true;
(bool success, ) = payable(chosenApp.developer).call{value: job.budget}("");
require(success, "Payment failed");
emit DeveloperHired(_jobId, chosenApp.developer, job.budget);
}
/// @notice View applicants for a job
function getApplicants(uint _jobId) external view returns (Application[] memory) {
return jobApplications[_jobId];
}
/// @notice View all jobs
function getAllJobs() public view returns (Job[] memory) {
Job[] memory allJobs = new Job[](jobCount);
for (uint256 i = 1; i <= jobCount; i++) {
allJobs[i - 1] = jobs[i];
}
return allJobs;
}
/// @notice Cancel a job and refund client
function cancelJob(uint _jobId) external {
Job storage job = jobs[_jobId];
require(msg.sender == job.client, "Unauthorized");
require(job.isOpen, "Job already closed");
job.isOpen = false;
(bool success, ) = payable(job.client).call{value: job.budget}("");
require(success, "Refund failed");
}
}
Deploy this contract to a testnet using Hardhat.
🔧 Step 3: Hooking Up the Contract
Create constants.ts:
export const JOB_BOARD_ABI = [ /* your ABI */ ];
export const JOB_BOARD_ADDRESS = "0x...";
Create hooks with wagmi:
export function useGetAllJobs() {
return useReadContract({
abi: JOB_BOARD_ABI,
address: JOB_BOARD_ADDRESS,
functionName: 'getAllJobs',
});
}
export function useGetApplicants(jobId: number | undefined) {
return useReadContract({
abi: JOB_BOARD_ABI,
address: JOB_BOARD_ADDRESS,
functionName: 'getApplicants',
args: jobId !== undefined ? [jobId] : undefined,
}) as {
data: Applicant[] | undefined;
isLoading: boolean;
error: any;
};;
}
export function useApplyToJob() {
const { writeContractAsync, isPending, error, isSuccess } = useWriteContract();
const applyToJob = async (jobId: number, github: string, pitch: string) => {
await writeContractAsync({
address: JOB_BOARD_ADDRESS as `0x${string}`,
abi: JOB_BOARD_ABI,
functionName: 'applyToJob',
args: [BigInt(jobId), github, pitch],
});
};
return { applyToJob, isPending, error, isSuccess };
}
👨💼 Step 4: Build the Frontend
Main Pages
AllJobsPage.tsx: List of all jobs
JobDetailsPage.tsx: View job info + applicants
PostJobPage.tsx: Form for creating jobs
ApplicantsPage.tsx: See who applied
HireDeveloperPage.tsx: Select & hire developer
Each page interacts with hooks and uses Tailwind CSS for styling.
✨ Step 5: Connect Wallet
Use RainbowKit:
import { ConnectButton } from '@rainbow-me/rainbowkit';
const Header = () => (
<header className="p-4 shadow">
<ConnectButton />
</header>
);
Wrap your app in Wagmi + RainbowKit providers in main.tsx.
🚪 Step 6: Test on a Testnet
- Use Sepolia or Base Goerli
- Fund your wallet with test ETH
- Deploy contract
- Use the app: post a job, apply, hire, mark paid
🚀 Bonus: Advanced Ideas
- Use The Graph for fast querying
- Add Push Protocol notifications
- Support USDC payments
- Integrate ENS names
- Add client/developer dashboard
🖋️ Conclusion
You now have a fully functional blockchain-powered job board. This system can be the foundation for Web3 gig platforms, bounty systems, and much more. Feel free to extend it or fork it into your own project.
Happy building! ✨🚀
Want the full codebase or a video walkthrough? Drop a comment or connect on X (@favebs )!
Top comments (0)