How to create a decentralized Job Board using Solidity, Nextjs, and Netlify.

How to create a decentralized Job Board using Solidity, Nextjs, and Netlify.

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:

github.com

1.png

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.

Screenshot from 2022-02-24 15-24-47.png

And if this is successful then

3.png

Deploy your Dapp to Netlify

create your account

Screenshot from 2022-02-26 07-10-59.png

Click on Add new site and then Import an existing project

Screenshot from 2022-02-26 07-17-43.png

Now Connect your Github and select your Code repository.

Screenshot from 2022-02-26 07-18-31.png

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

open Remix ID

Create a a new solidity file

Screenshot from 2022-02-26 07-25-35.png

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

8.png

Go to DEPLOY & RUN TRANSACTIONS tab, and select injected web3 under ENVIRONMENT and click Deploy

9.png

Now copy your ABI and deployed Smart Contract address and save it somewhere. We use this later

12.png

11.png

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

Screenshot from 2022-02-26 08-13-53.png

yarn add react-icons react-toastify dotenv ethers

Now to This Github Repo

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

Screenshot from 2022-02-26 08-21-11.png

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&apos;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>
  );
}

Source Code is here

Thanks for reading This if You like please let me know and don't then Please I really want to improve