Live Demo
Introduction
This blog post is a guide on how to create a decentralized job board on the Ethereum blockchain using Solidity, Next.js, and Netlify I'll also show you how to deploy it for free using Netlify's Github integration.
Problem
Why I decided to builds this decentralized app "Dapp" recently Stackoverflow announced
On March 31, 2022, we will discontinue Stack Overflow Jobs and Developer Story. This includes all job listings, saved searches, applications, messages, recommended job matches, job ads, developer stories, saved resumes, and the salary calculator. Read Full Story here
Stackoverflow is a platform for developers to share their knowledge and get jobs from that it was a good place for a developer and companies to get the talent they needed or post jobs the very calm and simple marketplace for developers and companies looking for talent.
But at that time, StackOverflow decided to close their job section creating a sudden need for other credible platforms, but those platforms are not as good as StackOverflow and they do nasty things like increasing the price per job posting and need you to have an account with them like Linkedin before you can post job postings.
Solution - A decentralised job board
So I decided to build DAPP which removes both of the above mention issues quite well.
A simple job board website built with smart contract AKA blockchain and Nextjs so it doesn't Have any centralized database. anyone can post their jobs with just 0.005ETH = 15$
it is that simple well as Developer I can't change its price because it is very nature of Blockchain is immutable
Table of content:
Setting up Next.JS
How to set up and create your project using Github.
How to deploy your Dapp using Netlify
How to write your Smart Contracts, using Remix ID and Deploy it to the testnets.
How to connect your Smart Contract with Next.JS
NextJS Setup
Require Node.js 12.22.0 or later
- check your NodeJs version
node -v
Install yarn "Do it now and thanks me later"
npm install -g yarn
Create Next.JS
yarn create next-app YOUR_APP_NAME
Start your app locally
yarn run dev
At this point, you app is running up.🤞
Github Setup
GitHub is a repository hosting platform, offering everything a developer might need in terms of issue tracking and code management in one convenient free package.
Just make your account from here:
Run these commands to upload your code to GitHub:
git init
git add .
git commit -m "Initial commit"
git branch -M main
git remote add origin YOUR_GITHUB_REPOSITORY_LINK
git push -u origin main
If your computer is not connected to Github or this is your first time and Then Go this URL github.com/settings/tokens and create access token and enter it in place of password if it asked.
And if this is successful then
Deploy your Dapp to Netlify
Click on Add new site and then Import an existing project
Now Connect your Github and select your Code repository.
And Click on deploy site.
Lets build our Smart Contract
Smart contracts are immutable pieces of software running on a decentralized network and are responsible to store, read, manipulate, and modifying data on the blockchain
Now it's time to deploy your Smart Contract to Rinkeby testnet using remix IDE
Create a a new solidity file
WRITE This code your File JOB BOARD
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/access/Ownable.sol";
contract JobBoard is Ownable {
uint256 public JOB_ID = 0; // Primary key
address ADMIN = msg.sender;
// Job datatype
struct Job {
uint256 jobId;
string companyName;
string position;
string description;
string employmentType;
string location;
string companyWebsiteUrl;
address employer;
}
Job[] public jobs;
mapping (address=>address[]) public applicants;
// add a job
// default parameter is not there in Solidity
function addJob(
string memory _companyName,
string memory _position,
string memory _description,
string memory employmentType,
string memory _location,
string memory _companyWebsiteUrl
) public payable {
require(msg.value == 5 * 10**15 );
Job memory job = Job({
jobId: JOB_ID,
companyName: _companyName,
position: _position,
description: _description,
employmentType: employmentType,
location: _location,
companyWebsiteUrl: _companyWebsiteUrl,
employer: msg.sender
});
jobs.push(job);
JOB_ID++;
}
// return all jobs
function allJobs() public view returns(Job[] memory) {
return jobs;
}
// delete a job
function deleteJob(uint256 _jobId) public {
require(
msg.sender == jobs[_jobId].employer || msg.sender == ADMIN
);
// delete jobs[_jobId]; // this creates the gap means data at this location is null but
// index still returns null
// Alternative way which shifts the items from array
if(_jobId >= jobs.length) return;
for (uint i = _jobId; i<jobs.length-1; i++){
jobs[i] = jobs[i+1];
jobs[i].jobId = i;
}
delete jobs[jobs.length-1];
JOB_ID--;
}
// Apply for a job
function applyForJob(uint256 _jobid) public {
// add sender address to job applicants
// applicants[jobs[_jobid].employer].push(_jobid);
applicants[jobs[_jobid].employer].push(msg.sender);
}
// return this
function admin() public view returns(address) {
return ADMIN;
}
function withdraw(address payable _adminAddress) public onlyOwner{
_adminAddress.transfer(address(this).balance);
}
function totalJobs() public view returns(uint256){
return jobs.length;
}
}
Now open your MM and select Rinkeby
Go to DEPLOY & RUN TRANSACTIONS tab, and select injected web3 under ENVIRONMENT and click Deploy
Now copy your ABI and deployed Smart Contract address and save it somewhere. We use this later
voila! now your Smart contract is deployed. Lets work on the Front-end part of it
Lets work on the Front-end part
Install these packages into your app
yarn add react-icons react-toastify dotenv ethers
Start building things But with Understanding I suggest Writing as much code as possible
You don't Code You don't learn
Here how code structure looks like
Make a
env.local
file similar to env.local.example and change NEXT_PUBLIC_JOBBOARD_ADDRESS with yours
NEXT_PUBLIC_JOBBOARD_ADDRESS=YOUR_SMART_CONTRACT_ADDRESS
Copy
jobBoard.json
and Replace this with your ABI we copied earlier.Copy utils file code and styles folder codes as well
These are some helpful functions and stryle for your app
Now go the pages folder and Create same Files As above Repository
Create utils.js
utlity functions for Detecting Metamask is Installed or not
// Check for MetaMask wallet browser extension
const hasEthereum = () => {
return (
typeof window !== "undefined" && typeof window.ethereum !== "undefined"
);
};
async function requestAccount() {
await window.ethereum.request({ method: "eth_requestAccounts" });
}
module.exports = {
hasEthereum,
requestAccount,
};
Create index.js
import React from "react";
import Head from "next/head";
import { useState, useEffect } from "react";
// Is is used to connecte frontend to Smart contract
import { ethers } from "ethers";
import JobBoardABI from "../jobBoard.json";
import { hasEthereum, requestAccount } from "../utils";
import Board from "../src/components/Board";
import toast from "../src/components/Toast";
export default function Home() {
// States
const [connectedWalletAddress, setConnectedWalletAddressState] = useState("");
const [allJobsState, setAllJobsState] = useState();
const [loading, setLoading] = useState(true);
const [isAdmin, setIsAdmin] = useState(false);
// If wallet is already connected...
useEffect(() => {
if (!hasEthereum()) {
notify("error", "MetaMask unavailable");
setConnectedWalletAddressState(`MetaMask unavailable`);
setLoading(false);
return;
}
async function setConnectedWalletAddress() {
const provider = new ethers.providers.Web3Provider(window.ethereum);
const signer = provider.getSigner();
try {
await requestAccount();
const signerAddress = await signer.getAddress();
console.log("HEYYY", signerAddress);
setConnectedWalletAddressState(signerAddress);
} catch {
console.log("ERROR");
setConnectedWalletAddressState("No wallet connected");
return;
}
}
// setConnectedWalletAddress();
getAdmin();
fetchAllJobs();
}, []);
const notify = React.useCallback((type, message) => {
toast({ type, message });
}, []);
async function getAdmin() {
if (!hasEthereum()) {
setConnectedWalletAddressState(`MetaMask unavailable`);
notify("error", "MetaMask unavailable");
return;
}
await requestAccount();
const provider = new ethers.providers.Web3Provider(window.ethereum);
const contract = new ethers.Contract(
process.env.NEXT_PUBLIC_JOBBOARD_ADDRESS,
JobBoardABI,
provider
);
try {
const data = await contract.admin();
const signer = provider.getSigner();
const signerAddress = await signer.getAddress();
if (data == signerAddress) {
console.log("HELLO ADMIN");
setIsAdmin(true);
}
} catch (error) {
console.log("HERE IS BIG ERROR IN ADMIN");
console.log(error);
}
}
// Call smart contract, fetch all jobs
async function fetchAllJobs() {
if (!hasEthereum()) {
setConnectedWalletAddressState(`MetaMask unavailable`);
return;
}
const provider = new ethers.providers.Web3Provider(window.ethereum);
const contract = new ethers.Contract(
process.env.NEXT_PUBLIC_JOBBOARD_ADDRESS,
JobBoardABI,
provider
);
try {
// await requestAccount();
const data = await contract.allJobs();
console.log("ALL JOBS", data);
setAllJobsState(data);
} catch (error) {
console.log("HERE IS BIG ERROR IN ALL JOBS");
console.log(error);
}
setLoading(false);
}
return (
<div className="App">
<Head>
<title>web3-job-board</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<div className="recipe">
hey, I am a simple job board website built with smart contract AKA
blockchain and Nextjs so I don't Have any centralized database. use
me to post your jobs with just 0.005ETH = 15$
<p className="error">Still running on Rinkeby Testnet</p>
</div>
{loading ? (
<div className="loader-center">
<div className="loader"></div>
</div>
) : (
allJobsState &&
allJobsState.map(
(job, index) =>
job.employer != "0x0000000000000000000000000000000000000000" && (
<div key={index}>
<Board
id={index}
companyName={job.companyName}
position={job.position}
employmentType={job.employmentType}
location={job.location}
companyWebsiteUrl={job.companyWebsiteUrl}
/>
<div style={{ marginTop: "15px" }}></div>
</div>
)
)
)}
</div>
);
}
Create Job Posting component InputForm.js
import React from "react";
import { useState } from "react";
import { ethers } from "ethers";
import JobBoardABI from "../../jobBoard.json";
import { hasEthereum, requestAccount } from "../../utils";
import toast from "./Toast";
export default function InputForm() {
const [loading, setLoading] = useState(false);
const notify = React.useCallback((type, message) => {
toast({ type, message });
}, []);
const dismiss = React.useCallback(() => {
toast.dismiss();
}, []);
const [formValues, setFormValues] = useState({
companyName: "",
position: "",
description: "",
employmentType: "",
location: "",
companyWebsiteUrl: "",
employer: "",
});
const [formErrors, setFormErrors] = useState({});
const validateForm = (_formValues) => {
const errors = {};
if (!_formValues.companyName) {
errors.companyName = "Company name is required";
}
if (!_formValues.position) {
errors.position = "Position is required";
}
if (!_formValues.description) {
errors.description = "Description is required";
}
if (!_formValues.employmentType) {
errors.employmentType = "Employment type is required";
}
if (!_formValues.location) {
errors.location = "Location is required";
}
if (!_formValues.companyWebsiteUrl) {
errors.companyWebsiteUrl = "Apply website URL is required";
}
return errors;
};
// create a function which set the values of form field
const handleOnChange = (e) => {
setFormValues({ ...formValues, [e.target.name]: e.target.value });
};
const handleSubmit = (e, obj) => {
e.preventDefault();
let _errors = validateForm(formValues);
setFormErrors(_errors);
if (Object.keys(_errors).length === 0) {
console.log("NOOOOO ERRORS");
createJobPost();
}
};
// create a job
async function createJobPost() {
if (!hasEthereum()) {
notify("warning", "MetaMask unavailable");
return;
}
setLoading(true);
await requestAccount();
const provider = new ethers.providers.Web3Provider(window.ethereum);
const signer = provider.getSigner();
const signerAddress = await signer.getAddress();
const contract = new ethers.Contract(
process.env.NEXT_PUBLIC_JOBBOARD_ADDRESS,
JobBoardABI,
signer
);
let overrides = {
// To convert Ether to Wei:
value: ethers.utils.parseEther("0.005"), // ether in this case MUST be a string
};
const transaction = await contract.addJob(
formValues.companyName,
formValues.position,
formValues.description,
formValues.employmentType,
formValues.location,
formValues.companyWebsiteUrl,
overrides
);
await transaction.wait();
setLoading(false);
console.log("JOB CREATED SUCCESSFULLY");
}
return (
<form>
{formErrors.companyName && (
<span className="error">{formErrors.companyName}</span>
)}
<label>
COMPANY NAME*
<input
required
type="text"
name={Object.keys(formValues)[0]}
value={formValues.companyName}
onChange={handleOnChange}
></input>
</label>
{formErrors.position && (
<span className="error">{formErrors.position}</span>
)}
<label>
POSITION*
<input
name={Object.keys(formValues)[1]}
type="text"
value={formValues.position}
onChange={handleOnChange}
></input>
</label>
{formErrors.description && (
<span className="error">{formErrors.description}</span>
)}
<label>
DESCRIPTION*
<input
name={Object.keys(formValues)[2]}
type="text"
value={formValues.description}
onChange={handleOnChange}
></input>
</label>
{formErrors.employmentType && (
<span className="error">{formErrors.employmentType}</span>
)}
<label>
EMPLOYMENT TYPE*
<input
name={Object.keys(formValues)[3]}
type="text"
value={formValues.employmentType}
onChange={handleOnChange}
placeholder="eg: Full Time"
></input>
</label>
{formErrors.location && (
<span className="error">{formErrors.location}</span>
)}
<label>
LOCATION*
<input
name={Object.keys(formValues)[4]}
type="text"
value={formValues.location}
onChange={handleOnChange}
></input>
</label>
{formErrors.companyWebsiteUrl && (
<span className="error">{formErrors.companyWebsiteUrl}</span>
)}
<label>
APPLY URL*
<input
name={Object.keys(formValues)[5]}
type="text"
value={formValues.companyWebsiteUrl}
onChange={handleOnChange}
></input>
</label>
{loading ? (
<div className="loader"></div>
) : (
<button onClick={(e) => handleSubmit(e, formValues)}>
Post your job
</button>
)}
</form>
);
}
Thanks for reading This if You like please let me know and don't then Please I really want to improve