What you will be building, see the live demo at sepolia test net and the git repo.




Learn how to build a decentralized freelance marketplace like Upwork using React, Solidity, and CometChat.
In this step-by-step tutorial, we'll teach you:
- How to build React interfaces for job listing
- How to create solidity smart contracts for job managment
- How to integrate CometChat for one-on-one real-time communication
- How to make the marketplace decentralized and annoymous
Whether you're a seasoned developer or new to Web3, this tutorial will give you the skills to create your own freelance marketplace. Let's get started!
Prerequisites
You will need the following tools installed to build along with me:
- Node.js
- Yarn
- MetaMask
- React
- Solidity
- CometChat SDK
- Tailwind CSS
I recommend watching the videos below to learn how to set up your MetaMask for this project.
Installing Dependencies
Clone the starter kit and open it in VS Code using the command below:
git clone https://github.com/Daltonic/tailwind_ethers_starter_kit dappworks
cd dappworks
Next, update the package.json
with the snippet below.
{ | |
"name": "demo", | |
"private": true, | |
"version": "0.0.0", | |
"scripts": { | |
"start": "react-app-rewired start", | |
"build": "react-app-rewired build", | |
"test": "react-app-rewired test", | |
"eject": "react-scripts eject" | |
}, | |
"dependencies": { | |
"@cometchat-pro/chat": "3.0.13", | |
"@headlessui/react": "^1.7.16", | |
"@nomiclabs/hardhat-ethers": "^2.1.0", | |
"@nomiclabs/hardhat-waffle": "^2.0.3", | |
"date-fns": "^2.30.0", | |
"emojtcha-react": "^1.0.6", | |
"ethereum-waffle": "^3.4.4", | |
"ethers": "^5.6.9", | |
"hardhat": "^2.10.1", | |
"moment": "^2.29.4", | |
"react": "^18.2.0", | |
"react-dom": "^18.2.0", | |
"react-hooks-global-state": "^1.0.2", | |
"react-icons": "^4.3.1", | |
"react-identicons": "^1.2.5", | |
"react-moment": "^1.1.2", | |
"react-router-dom": "6", | |
"react-scripts": "5.0.0", | |
"react-toastify": "^9.1.3", | |
"web-vitals": "^2.1.4" | |
}, | |
"devDependencies": { | |
"@openzeppelin/contracts": "^4.5.0", | |
"@tailwindcss/forms": "0.4.0", | |
"assert": "^2.0.0", | |
"autoprefixer": "10.4.2", | |
"babel-polyfill": "^6.26.0", | |
"babel-preset-env": "^1.7.0", | |
"babel-preset-es2015": "^6.24.1", | |
"babel-preset-stage-2": "^6.24.1", | |
"babel-preset-stage-3": "^6.24.1", | |
"babel-register": "^6.26.0", | |
"buffer": "^6.0.3", | |
"chai": "^4.3.6", | |
"chai-as-promised": "^7.1.1", | |
"crypto-browserify": "^3.12.0", | |
"dotenv": "^16.0.0", | |
"https-browserify": "^1.0.0", | |
"mnemonics": "^1.1.3", | |
"os-browserify": "^0.3.0", | |
"postcss": "8.4.5", | |
"process": "^0.11.10", | |
"react-app-rewired": "^2.1.11", | |
"stream-browserify": "^3.0.0", | |
"stream-http": "^3.2.0", | |
"tailwindcss": "3.0.18", | |
"url": "^0.11.0" | |
}, | |
"browserslist": { | |
"production": [ | |
">0.2%", | |
"not dead", | |
"not op_mini all" | |
], | |
"development": [ | |
"last 1 chrome version", | |
"last 1 firefox version", | |
"last 1 safari version" | |
] | |
} | |
} |
Please run the command yarn install
in your terminal to install the dependencies for this project.
Configuring CometChat SDK
To configure the CometChat SDK, please follow the steps provided below. Once completed, make sure to save the generated keys as environment variables for future use.
STEP 1:
Head to CometChat Dashboard and create an account.
STEP 2:
Log in to the CometChat dashboard, only after registering.
STEP 3:
From the dashboard, add a new app called Play-To-Earn.
STEP 4:
Select the app you just created from the list.
From the Quick Start copy the APP_ID
, REGION
, and AUTH_KEY
, to your .env
file. See the image and code snippet.
Replace the REACT_COMET_CHAT
placeholder keys with their appropriate values.
REACT_APP_COMETCHAT_APP_ID=****************
REACT_APP_COMETCHAT_AUTH_KEY=******************************
REACT_APP_COMETCHAT_REGION=**
The .env
file should be created at the root of your project.
Configuring the Hardhat script
Navigate to the root directory of the project and open the "hardhat.config.js
" file. Replace the existing content of the file with the provided settings.
require('@nomiclabs/hardhat-waffle') | |
require('dotenv').config() | |
module.exports = { | |
defaultNetwork: 'localhost', | |
networks: { | |
localhost: { | |
url: 'http://127.0.0.1:8545', | |
}, | |
}, | |
solidity: { | |
version: '0.8.11', | |
settings: { | |
optimizer: { | |
enabled: true, | |
runs: 200, | |
}, | |
}, | |
}, | |
paths: { | |
sources: './src/contracts', | |
artifacts: './src/abis', | |
}, | |
mocha: { | |
timeout: 40000, | |
}, | |
} |
This code configures Hardhat for your project. It includes importing necessary plugins, setting up networks (with localhost as the default), specifying the Solidity compiler version, defining paths for contracts and artifacts, and setting a timeout for Mocha tests.
The Smart Contract File
The following steps will guide you through the process of creating the smart contract file for this project:
- Create a new folder named
contracts
inside thesrc
folder. - Create a new file named
Dappworks
.sol
inside thecontracts
folder. - Copy the provided codes below and paste it into their respective files and save.
By following these steps, you will have successfully set up the necessary directory structure and created the Dappworks
.sol
file, which will serve as the foundation for implementing the logic of the smart contract.
//SPDX-License-Identifier:MIT | |
pragma solidity >=0.7.0 <0.9.0; | |
import "@openzeppelin/contracts/access/Ownable.sol"; | |
import "@openzeppelin/contracts/utils/Counters.sol"; | |
import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; | |
import "@openzeppelin/contracts/utils/math/SafeMath.sol"; | |
contract DappWorks is Ownable, ReentrancyGuard { | |
using Counters for Counters.Counter; | |
Counters.Counter private _jobCounter; | |
struct JobStruct { | |
uint id; | |
address owner; | |
address freelancer; | |
string jobTitle; | |
string description; | |
string tags; | |
uint prize; | |
bool paidOut; | |
uint timestamp; | |
bool listed; | |
bool disputed; | |
address[] bidders; | |
} | |
struct FreelancerStruct { | |
uint id; | |
uint jId; | |
address account; | |
bool isAssigned; | |
} | |
struct BidStruct { | |
uint id; | |
uint jId; | |
address account; | |
} | |
uint public platformCharge = 5; | |
mapping(uint => JobStruct) jobListings; | |
mapping(uint => FreelancerStruct[]) freelancers; | |
mapping(uint => BidStruct[]) jobBidders; | |
mapping(uint => bool) jobListingExists; | |
mapping(uint => mapping(address => bool)) public hasPlacedBid; | |
modifier onlyJobOwner(uint id) { | |
require(jobListings[id].owner == msg.sender, "Unauthorized entity"); | |
_; | |
} | |
function addJobListing( | |
string memory jobTitle, | |
string memory description, | |
string memory tags | |
) public payable { | |
require(bytes(jobTitle).length > 0, "Please provide a job title"); | |
require(bytes(description).length > 0, "Please provide a description"); | |
require(bytes(tags).length > 0, "Please provide tags"); | |
require(msg.value > 0 ether, "Insufficient funds"); | |
// Increment the counter before using the current value | |
_jobCounter.increment(); | |
uint jobId = _jobCounter.current(); | |
JobStruct memory jobListing; | |
jobListing.id = jobId; | |
jobListing.owner = msg.sender; | |
jobListing.jobTitle = jobTitle; | |
jobListing.description = description; | |
jobListing.tags = tags; | |
jobListing.prize = msg.value; | |
jobListing.listed = true; | |
jobListing.timestamp = currentTime(); | |
jobListings[jobId] = jobListing; | |
jobListingExists[jobId] = true; | |
} | |
function deleteJob(uint id) public { | |
require(jobListingExists[id], "This job listing doesn't exist"); | |
require(jobListings[id].listed, "This job has been taken"); | |
require(!jobListings[id].paidOut, "This job has been paid out"); | |
jobListingExists[id] = false; | |
payTo(jobListings[id].owner, jobListings[id].prize); | |
} | |
function updateJob( | |
uint id, | |
string memory jobTitle, | |
string memory description, | |
string memory tags | |
) public { | |
require(jobListingExists[id], "This job listing doesn't exist"); | |
require(jobListings[id].listed, "This job has been taken"); | |
require(!jobListings[id].paidOut, "This job has been paid out"); | |
jobListings[id].jobTitle = jobTitle; | |
jobListings[id].description = description; | |
jobListings[id].tags = tags; | |
} | |
function bidForJob(uint id) public { | |
require(jobListingExists[id], "This job listing doesn't exist"); | |
require(jobListings[id].owner != msg.sender, "Forbidden action!"); | |
require(!jobListings[id].paidOut, "This job has been paid out"); | |
require(jobListings[id].listed, "This job have been taken"); | |
require(!hasPlacedBid[id][msg.sender], "You have placed a bid already"); | |
BidStruct memory bid; | |
bid.id = jobBidders[id].length + 1; | |
bid.jId = id; | |
bid.account = msg.sender; | |
hasPlacedBid[id][msg.sender] = true; | |
jobListings[id].bidders.push(msg.sender); | |
jobBidders[id].push(bid); | |
} | |
function acceptBid( | |
uint id, | |
uint jId, | |
address bidder | |
) public onlyJobOwner(jId) { | |
require(jobListingExists[jId], "This job listing doesn't exist"); | |
require(jobListings[jId].listed, "This job have been taken"); | |
require(!jobListings[jId].paidOut, "This job has been paid out"); | |
require(hasPlacedBid[jId][bidder], "UnIdentified bidder"); | |
FreelancerStruct memory freelancer; | |
freelancer.id = freelancers[jId].length; | |
freelancer.jId = jId; | |
freelancer.account = bidder; | |
freelancer.isAssigned = true; | |
freelancers[jId].push(freelancer); | |
jobListings[jId].freelancer = bidder; | |
for (uint i = 0; i < jobBidders[jId].length; i++) { | |
if (jobBidders[jId][i].id != id) { | |
hasPlacedBid[jId][jobBidders[jId][i].account] = false; | |
} | |
} | |
jobListings[jId].listed = false; | |
} | |
function bidStatus(uint id) public view returns (bool) { | |
return hasPlacedBid[id][msg.sender]; | |
} | |
function dispute(uint id) public onlyJobOwner(id) { | |
require(jobListingExists[id], "This job listing doesn't exist"); | |
require(!jobListings[id].disputed, "This job already disputed"); | |
require(!jobListings[id].paidOut, "This job has been paid out"); | |
jobListings[id].disputed = true; | |
} | |
function revoke(uint jId, uint id) public onlyOwner { | |
require(jobListingExists[jId], "This job listing doesn't exist"); | |
require(jobListings[jId].disputed, "This job must be on dispute"); | |
require(!jobListings[jId].paidOut, "This job has been paid out"); | |
// Use two separate indexes to access the FreelancerStruct | |
FreelancerStruct storage freelancer = freelancers[jId][id]; | |
freelancer.isAssigned = false; | |
jobListings[jId].freelancer = address(0); | |
payTo(jobListings[jId].owner, jobListings[jId].prize); | |
jobListings[jId].listed = true; | |
} | |
function resolved(uint id) public onlyOwner { | |
require(jobListingExists[id], "This job listing doesn't exist"); | |
require(jobListings[id].disputed, "This job must be on dispute"); | |
require(!jobListings[id].paidOut, "This job has been paid out"); | |
jobListings[id].disputed = false; | |
} | |
function payout(uint id) public nonReentrant onlyJobOwner(id) { | |
require(jobListingExists[id], "This job listing doesn't exist"); | |
require(!jobListings[id].listed, "This job has not been taken"); | |
require(!jobListings[id].disputed, "This job must not be on dispute"); | |
require(!jobListings[id].paidOut, "This job has been paid out"); | |
uint reward = jobListings[id].prize; | |
uint tax = (reward * platformCharge) / 100; | |
payTo(jobListings[id].freelancer, reward - tax); | |
payTo(owner(), tax); | |
jobListings[id].paidOut = true; | |
} | |
function getBidders( | |
uint id | |
) public view returns (BidStruct[] memory Bidders) { | |
if (jobListings[id].listed && jobListingExists[id]) { | |
Bidders = jobBidders[id]; | |
} else { | |
Bidders = new BidStruct[](0); | |
} | |
} | |
function getFreelancers( | |
uint id | |
) public view returns (FreelancerStruct[] memory) { | |
return freelancers[id]; | |
} | |
function getAcceptedFreelancer( | |
uint id | |
) public view returns (FreelancerStruct memory) { | |
require(jobListingExists[id], "This job listing doesn't exist"); | |
for (uint i = 0; i < freelancers[id].length; i++) { | |
if (freelancers[id][i].isAssigned) { | |
return freelancers[id][i]; | |
} | |
} | |
// If no freelancer is assigned, return an empty struct or handle it as needed. | |
FreelancerStruct memory emptyFreelancer; | |
return emptyFreelancer; | |
} | |
function getJobs() public view returns (JobStruct[] memory ActiveJobs) { | |
uint available; | |
uint currentIndex = 0; | |
for (uint256 i = 1; i <= _jobCounter.current(); i++) { | |
if ( | |
jobListingExists[i] && | |
jobListings[i].listed && | |
!jobListings[i].paidOut | |
) { | |
available++; | |
} | |
} | |
ActiveJobs = new JobStruct[](available); | |
for (uint256 i = 1; i <= _jobCounter.current(); i++) { | |
if ( | |
jobListingExists[i] && | |
jobListings[i].listed && | |
!jobListings[i].paidOut | |
) { | |
ActiveJobs[currentIndex++] = jobListings[i]; | |
} | |
} | |
} | |
function getMyJobs() public view returns (JobStruct[] memory MyJobs) { | |
uint available; | |
uint currentIndex = 0; | |
for (uint256 i = 1; i <= _jobCounter.current(); i++) { | |
if (jobListingExists[i] && jobListings[i].owner == msg.sender) { | |
available++; | |
} | |
} | |
MyJobs = new JobStruct[](available); | |
for (uint256 i = 1; i <= _jobCounter.current(); i++) { | |
if (jobListingExists[i] && jobListings[i].owner == msg.sender) { | |
MyJobs[currentIndex++] = jobListings[i]; | |
} | |
} | |
} | |
function getJob(uint id) public view returns (JobStruct memory) { | |
return jobListings[id]; | |
} | |
function getAssignedJobs() | |
public | |
view | |
returns (JobStruct[] memory AssignedJobs) | |
{ | |
uint available; | |
for (uint256 i = 1; i <= _jobCounter.current(); i++) { | |
if ( | |
jobListingExists[i] && | |
!jobListings[i].paidOut && | |
jobListings[i].freelancer == msg.sender | |
) { | |
available++; | |
} | |
} | |
AssignedJobs = new JobStruct[](available); | |
uint currentIndex = 0; | |
for (uint256 i = 1; i <= _jobCounter.current(); i++) { | |
if ( | |
jobListingExists[i] && | |
!jobListings[i].paidOut && | |
jobListings[i].freelancer == msg.sender | |
) { | |
AssignedJobs[currentIndex++] = jobListings[i]; | |
} | |
} | |
return AssignedJobs; | |
} | |
function getBidsForBidder() public view returns (BidStruct[] memory Bids) { | |
// Create a dynamic array to store the bids | |
BidStruct[] memory allBids = new BidStruct[](_jobCounter.current()); | |
uint currentIndex = 0; | |
for (uint i = 1; i <= _jobCounter.current(); i++) { | |
if ( | |
jobListingExists[i] && | |
jobListings[i].listed && | |
!jobListings[i].paidOut | |
) { | |
if (hasPlacedBid[i][msg.sender]) { | |
// Iterate over the bids for the current job and add matching bids to the array | |
for (uint j = 0; j < jobBidders[i].length; j++) { | |
if (jobBidders[i][j].account == msg.sender) { | |
allBids[currentIndex] = jobBidders[i][j]; | |
currentIndex++; | |
} | |
} | |
} | |
} | |
} | |
// Create a new array with only the relevant bids | |
Bids = new BidStruct[](currentIndex); | |
for (uint k = 0; k < currentIndex; k++) { | |
Bids[k] = allBids[k]; | |
} | |
return Bids; | |
} | |
function getJobsForBidder() | |
public | |
view | |
returns (JobStruct[] memory bidderJobs) | |
{ | |
// Create a dynamic array to store the jobs | |
JobStruct[] memory matchingJobs = new JobStruct[]( | |
_jobCounter.current() | |
); | |
uint currentIndex = 0; | |
for (uint i = 1; i <= _jobCounter.current(); i++) { | |
if ( | |
jobListingExists[i] && | |
jobListings[i].listed && | |
!jobListings[i].paidOut | |
) { | |
if (hasPlacedBid[i][msg.sender]) { | |
matchingJobs[currentIndex] = jobListings[i]; | |
currentIndex++; | |
} | |
} | |
} | |
// Create a new array with only the relevant jobs | |
bidderJobs = new JobStruct[](currentIndex); | |
for (uint k = 0; k < currentIndex; k++) { | |
bidderJobs[k] = matchingJobs[k]; | |
} | |
return bidderJobs; | |
} | |
// private function | |
function currentTime() internal view returns (uint256) { | |
return (block.timestamp * 1000) + 1000; | |
} | |
function payTo(address to, uint256 amount) internal { | |
(bool success, ) = payable(to).call{value: amount}(""); | |
require(success); | |
} | |
} |
The DappWorks smart contract is designed to facilitate a decentralized job marketplace, where users can create job listings, place bids on jobs, and manage the entire job lifecycle. Let's explore the key components and functionalities of this contract.
-
Contract Inheritance and Libraries:
- The contract inherits from the
Ownable
andReentrancyGuard
contracts, which provide access control and protection against reentrancy attacks. - It imports several libraries from the OpenZeppelin framework, including
Counters
for managing counters,SafeMath
for secure mathematical operations, andOwnable
andReentrancyGuard
for access control and reentrancy protection.
- The contract inherits from the
-
State Variables:
- The contract includes several state variables to manage job listings, freelancers, and bids. These include counters, structs, and mappings to store relevant data.
-
Structs:
- The contract defines several structs, including
JobStruct
,FreelancerStruct
, andBidStruct
, to structure and store information about jobs, freelancers, and bids.
- The contract defines several structs, including
-
Modifiers:
- The contract defines a modifier called
onlyJobOwner(uint id)
that restricts certain functions to the owner of a specific job listing.
- The contract defines a modifier called
-
Functions:
- The contract includes various functions for managing job listings and bids, including:
-
addJobListing
: Allows users to create a new job listing by providing a title, description, and tags. Users must also attach ether as a prize for the job. -
deleteJob
: Allows the owner of a job listing to delete it and receive the prize. -
updateJob
: Allows the owner of a job listing to update its details. -
bidForJob
: Allows users to place bids on job listings. -
acceptBid
: Allows the owner of a job listing to accept a specific bid from a freelancer, assigning the job to them. -
dispute
: Allows the owner of a job listing to initiate a dispute. -
revoke
: Allows the contract owner to revoke an assignment during a dispute. -
resolved
: Allows the contract owner to mark a dispute as resolved. -
payout
: Allows the owner of a job listing to pay the assigned freelancer, deducting a platform fee. - Various getter functions to retrieve information about jobs, bidders, freelancers, and more.
-
- The contract includes various functions for managing job listings and bids, including:
-
Internal Functions:
- The contract includes internal functions for handling time and making payments.
-
Events:
- The contract emits events to log various actions and state changes.
-
Constructor:
- The contract does not have a constructor, indicating that it can be deployed as is.
Overall, this smart contract is designed to facilitate the creation, management, and completion of job listings on the Ethereum blockchain. Users can create job listings, place bids, and assign jobs to freelancers. It also includes features for dispute resolution and a platform fee for job payouts.
🚀 Blockchain has real use cases, Join Dapp Mentors Academy for $8.44/month to learn all you can.
Subscribe to Dapp Mentors Academy today and get exclusive access to over 40 hours of web3 content, including courses on NFT minting, blockchain development, and more!
The Test Script
The DappWorks test script is thoughtfully crafted to thoroughly evaluate and confirm the functionalities and behaviors of the DappWorks smart contract. Here's an organized breakdown of the primary tests and functions encompassed within the script:
const { equal } = require('assert') | |
const { expect } = require('chai') | |
const toWei = (num) => ethers.utils.parseEther(num.toString()) | |
describe('Contracts', () => { | |
let contract, result | |
let id = 1 | |
let jobTitle = 'Content creator', | |
description = 'I need someone with a good professional writing skill, who understands writing dynamics', | |
tags = 'Professional, writer, Time management, Dynamics', | |
price = 0.3 | |
let Newtags = "Professional, writer, Time management, Resilience" | |
beforeEach(async () => { | |
const Contract = await ethers.getContractFactory('DappWorks') | |
;[deployer, client1, client2, freelancer1, freelancer2] = await ethers.getSigners() | |
contract = await Contract.deploy(); | |
await contract.deployed() | |
}) | |
beforeEach(async ()=> { | |
await contract.connect(client1).addJobListing(jobTitle, description, tags, { | |
value: toWei(price), | |
}); | |
}) | |
describe('Job Creation', ()=> { | |
it('should confirm fetching job listings', async ()=> { | |
result = await contract.getJobs() | |
expect(result).to.have.lengthOf(1) | |
}) | |
it('should confirm fetching a single job listing', async ()=> { | |
result = await contract.getJob(id); | |
expect(result.id).to.be.equal(1); | |
}) | |
it('should confirm updating of job', async ()=> { | |
result = await contract.getJob(1) | |
expect(result.tags).to.be.equal(tags) | |
await contract.updateJob(id, jobTitle, description, Newtags) | |
result = await contract.getJob(1); | |
expect(result.tags).to.be.equal(Newtags); | |
}) | |
it('should confirm job deletion', async ()=> { | |
result = await contract.getJobs(); | |
expect(result).to.be.have.lengthOf(1); | |
await contract.deleteJob(id) | |
result = await contract.getJobs(); | |
expect(result).to.be.have.lengthOf(0); | |
}) | |
it('should confirm bidding for job', async ()=> { | |
await contract.connect(freelancer1).bidForJob(id); | |
result = await contract.getBidders(id) | |
expect(result).to.have.lengthOf(1) | |
}) | |
it('should confirm accepting job bid', async ()=> { | |
await contract.connect(freelancer1).bidForJob(id); | |
result = await contract.connect(freelancer1).getAssignedJobs() | |
expect(result).to.have.lengthOf(0) | |
await contract.connect(client1).acceptBid(0, id, freelancer1.address) | |
result = await contract.connect(freelancer1).getAssignedJobs() | |
expect(result).to.have.lengthOf(1) | |
result = await contract.connect(client1).getFreelancers(id); | |
expect(result).to.have.lengthOf(1) | |
}) | |
it("should confirm disputing a job", async () => { | |
await contract.connect(client1).dispute(id); | |
result = await contract.getJob(id); | |
expect(result.disputed).to.be.true; | |
}); | |
it("should confirm revoking a disputed job", async () => { | |
// Place a bid by a freelancer | |
await contract.connect(freelancer1).bidForJob(id); | |
// Accept the bid by the client | |
await contract.connect(client1).acceptBid(0, id, freelancer1.address); | |
// Dispute the job | |
await contract.connect(client1).dispute(id); | |
// Revoke the job after it's been disputed | |
await contract.connect(deployer).revoke(id, 0); // Index starts from 0 | |
result = await contract.getJob(id); | |
expect(result.listed).to.be.true; | |
// Ensure that the assigned freelancer's isAssigned is set to false | |
const freelancers = await contract.getFreelancers(id); | |
for (let i = 0; i < freelancers.length; i++) { | |
if (result[i].id == 0) { | |
expect(freelancers[id].isAssigned).to.be.false; | |
} | |
} | |
}); | |
it("should confirm resolving a disputed job", async () => { | |
await contract.connect(client1).dispute(id); // Dispute the job first | |
await contract.connect(deployer).resolved(id); | |
result = await contract.getJob(id); | |
expect(result.disputed).to.be.false; | |
}); | |
it("should confirm payout of a job", async () => { | |
await contract.connect(freelancer1).bidForJob(id); | |
await contract.connect(client1).acceptBid(0, id, freelancer1.address); | |
await contract.connect(client1).payout(id); | |
result = await contract.getJob(id); | |
expect(result.paidOut).to.be.true; | |
}); | |
}) | |
}) | |
-
Setup and Initialization:
- The script imports required libraries and defines a utility function
**toWei**
to convert ether amounts to wei. - It defines a test suite using Mocha's
**describe**
function and sets up initial variables and contract instances in the**beforeEach**
hook.
- The script imports required libraries and defines a utility function
-
Job Creation Tests:
- The script includes several test cases to verify the creation and management of job listings.
-
**should confirm fetching job listings**
: Checks if a job listing can be retrieved and confirms that there is one job listing. -
**should confirm fetching a single job listing**
: Retrieves a specific job listing and verifies its properties. -
**should confirm updating of job**
: Tests updating the details of a job listing and checks if the changes are reflected correctly. -
**should confirm job deletion**
: Adds and then deletes a job listing, verifying that the job listing count becomes zero. -
**should confirm bidding for job**
: Tests placing a bid on a job listing and confirms that there is one bidder. -
**should confirm accepting job bid**
: Places a bid, accepts the bid, and confirms the assignment of the job to a freelancer. -
**should confirm disputing a job**
: Initiates a dispute for a job listing and verifies that the job is marked as disputed. -
**should confirm revoking a disputed job**
: Places a bid, accepts the bid, disputes the job, and then revokes it. Checks if the job is listed again and the freelancer's assignment is revoked. -
**should confirm resolving a disputed job**
: Initiates a dispute and then resolves it, ensuring that the job is no longer marked as disputed. -
**should confirm payout of a job**
: Places a bid, accepts the bid, and performs a payout, confirming that the job is marked as paid out.
Each test case uses Chai's **expect**
assertion to verify the expected outcomes of various contract interactions. The test script thoroughly tests the functionality of the DappWorks
smart contract, covering job creation, bidding, assignment, dispute handling, payout, and more.
const { equal } = require('assert') | |
const { expect } = require('chai') | |
const toWei = (num) => ethers.utils.parseEther(num.toString()) | |
describe('Contracts', () => { | |
let contract, result | |
let id = 1 | |
let jobTitle = 'Content creator', | |
description = 'I need someone with a good professional writing skill, who understands writing dynamics', | |
tags = 'Professional, writer, Time management, Dynamics', | |
price = 0.3 | |
let Newtags = "Professional, writer, Time management, Resilience" | |
beforeEach(async () => { | |
const Contract = await ethers.getContractFactory('DappWorks') | |
;[deployer, client1, client2, freelancer1, freelancer2] = await ethers.getSigners() | |
contract = await Contract.deploy(); | |
await contract.deployed() | |
}) | |
beforeEach(async ()=> { | |
await contract.connect(client1).addJobListing(jobTitle, description, tags, { | |
value: toWei(price), | |
}); | |
}) | |
describe('Job Creation', ()=> { | |
it('should confirm fetching job listings', async ()=> { | |
result = await contract.getJobs() | |
expect(result).to.have.lengthOf(1) | |
}) | |
it('should confirm fetching a single job listing', async ()=> { | |
result = await contract.getJob(id); | |
expect(result.id).to.be.equal(1); | |
}) | |
it('should confirm updating of job', async ()=> { | |
result = await contract.getJob(1) | |
expect(result.tags).to.be.equal(tags) | |
await contract.updateJob(id, jobTitle, description, Newtags) | |
result = await contract.getJob(1); | |
expect(result.tags).to.be.equal(Newtags); | |
}) | |
it('should confirm job deletion', async ()=> { | |
result = await contract.getJobs(); | |
expect(result).to.be.have.lengthOf(1); | |
await contract.deleteJob(id) | |
result = await contract.getJobs(); | |
expect(result).to.be.have.lengthOf(0); | |
}) | |
it('should confirm bidding for job', async ()=> { | |
await contract.connect(freelancer1).bidForJob(id); | |
result = await contract.getBidders(id) | |
expect(result).to.have.lengthOf(1) | |
}) | |
it('should confirm accepting job bid', async ()=> { | |
await contract.connect(freelancer1).bidForJob(id); | |
result = await contract.connect(freelancer1).getAssignedJobs() | |
expect(result).to.have.lengthOf(0) | |
await contract.connect(client1).acceptBid(0, id, freelancer1.address) | |
result = await contract.connect(freelancer1).getAssignedJobs() | |
expect(result).to.have.lengthOf(1) | |
result = await contract.connect(client1).getFreelancers(id); | |
expect(result).to.have.lengthOf(1) | |
}) | |
it("should confirm payout of a job", async () => { | |
await contract.connect(freelancer1).bidForJob(id); | |
await contract.connect(client1).acceptBid(0, id, freelancer1.address); | |
await contract.connect(client1).payout(id); | |
result = await contract.getJob(id); | |
expect(result.paidOut).to.be.true; | |
}); | |
}) | |
}) | |
By running **yarn hardhat test**
on the terminal will test out all the essential function of this smart contract.
The Deployment Scripts
The DappWorks deployment script is responsible for deploying the DappWorks smart contract to the Ethereum network using the Hardhat development environment. Here's an overview of the script:
Key Components and Functionality
- Imports the
ethers
andfs
dependencies. - Defines an asynchronous
main()
function to deploy the contract. - Gets the contract factory for
DappWorks
. - Deploys the contract and stores the contract instance in the
contract
variable. - Waits for the deployment to complete.
- Extracts the contract address and stores it in the
address
variable. - Writes the address to a JSON file in the
src/abis/
directory. - Handles any errors that may occur during execution.
- Invokes the
main()
function. - Logs the deployed contract address to the console if the deployment is successful.
The DappWorks deployment script simplifies the deployment of the DappWorks smart contract and generates a JSON file, **contractAddress.json**
, containing the deployed contract's address. This file can be used for further integration and interaction with the deployed contract within the project.
To utilize this script, create a folder named "scripts" in the root directory of your project if it doesn't already exist. Inside the "scripts" folder, create a JavaScript file named **deploy.js**
, and then copy and paste the provided deployment script into this file.
const { ethers } = require('hardhat') | |
const fs = require('fs') | |
const toWei = (num) => ethers.utils.parseEther(num.toString()) | |
async function main() { | |
const contract_name = 'DappWorks' | |
const Contract = await ethers.getContractFactory(contract_name) | |
const contract = await Contract.deploy() | |
await contract.deployed() | |
const address = JSON.stringify({ address: contract.address }, null, 4) | |
fs.writeFile('./src/abis/contractAddress.json', address, 'utf8', (err) => { | |
if (err) { | |
console.error(err) | |
return | |
} | |
console.log('Deployed contract address', contract.address) | |
}) | |
} | |
main().catch((error) => { | |
console.error(error) | |
process.exitCode = 1 | |
}) |
Next, run the **yarn hardhat run scripts/deploy.js**
to deploy the smart contract into the network on a terminal.
If you require additional assistance with setting up Hardhat or deploying your Fullstack DApp, I recommend watching this informative video that provides guidance and instructions.
Developing the Frontend
To start developing the frontend of our application, we will create a new folder called components
inside the src
directory. This folder will hold all the components needed for our project.
For each of the components listed below, you will need to create a corresponding file inside the src/components
folder and paste its codes inside it.
Header Component
The DappWorks header component is a central part of the user interface. It features the DappWorks logo, navigation links for easy access to various app sections, and a wallet connection button. The mobile-friendly design includes a toggle button for a compact menu. This component enhances user navigation and wallet integration.
import React, { useState } from 'react' | |
import { Link } from 'react-router-dom' | |
import { connectWallet } from '../services/blockchain' | |
import { truncate, useGlobalState } from '../store' | |
import { BsList, BsX } from 'react-icons/bs' | |
import MobileHeader from './MobileHeader' | |
const Header = () => { | |
const [connectedAccount] = useGlobalState('connectedAccount') | |
const [isOpen, setIsOpen] = useState(false) | |
const handleToggle = () => { | |
setIsOpen(!isOpen) | |
} | |
return ( | |
<header className="bg-white w-full mx-auto p-5 flex justify-between items-center flex-wrap shadow-md"> | |
<Link className="text-green-600 font-[risque] text-2xl" to={'/'}> | |
Dappworks | |
</Link> | |
<div className="items-center space-x-5 md:block hidden"> | |
<Link to={'/mybids'} className="text-gray-600"> | |
My Bids | |
</Link> | |
<Link to={'/myjobs'} className="text-gray-600"> | |
My Jobs | |
</Link> | |
<Link to={'/myprojects'} className="text-gray-600"> | |
My Projects | |
</Link> | |
<Link to={'/messages'} className="text-gray-600"> | |
Messages | |
</Link> | |
{connectedAccount ? ( | |
<button className="bg-green-500 text-white py-1 px-5 rounded-full"> | |
{truncate(connectedAccount, 4, 4, 11)} | |
</button> | |
) : ( | |
<button | |
className="bg-green-500 text-white py-1 px-5 rounded-full" | |
onClick={connectWallet} | |
> | |
connect wallet | |
</button> | |
)} | |
</div> | |
<div className="md:hidden block relative" onClick={handleToggle}> | |
{!isOpen ? ( | |
<BsList className="text-2xl cursor-pointer" /> | |
) : ( | |
<BsX className="text-2xl cursor-pointer" /> | |
)} | |
<MobileHeader toggle={isOpen} /> | |
</div> | |
</header> | |
) | |
} | |
export default Header |
Job Listing Card Component
The JobListingCard
component renders a card displaying information about a job listing, including the title, price, tags, and description. Users can interact with the card to place bids or manage job management tasks. The component handles user interactions and provides visual feedback through toast notifications. It also checks the user's account status to determine the appropriate actions to display on the card.
import React from 'react' | |
import { FaEthereum } from 'react-icons/fa' | |
import { bidForJob, bidStatus } from '../services/blockchain' | |
import { toast } from 'react-toastify' | |
import { useGlobalState } from '../store' | |
import { useNavigate } from 'react-router-dom' | |
const JobListingCard = ({ jobListing }) => { | |
const [connectedAccount] = useGlobalState('connectedAccount') | |
const navigate = useNavigate() | |
const handleBidding = async (id) => { | |
await toast.promise( | |
new Promise(async (resolve, reject) => { | |
await bidForJob(id) | |
.then(async () => { | |
await bidStatus(id) | |
resolve() | |
}) | |
.catch(() => reject()) | |
}), | |
{ | |
pending: 'Approve transaction...', | |
success: 'Application successful 👌', | |
error: 'Encountered error 🤯', | |
} | |
) | |
} | |
const manageAdminTasks = () => { | |
navigate('/myprojects') | |
} | |
return ( | |
<div className="border-b border-l border-r border-gray-300 py-6 px-5"> | |
<h4>{jobListing.jobTitle}</h4> | |
<div className="flex mt-2 items-center"> | |
<FaEthereum className="text-md cursor-pointer" /> | |
<span className="text-md"> | |
{parseFloat(jobListing.prize).toFixed(2)} | |
</span> | |
</div> | |
<div className="flex items-center mt-3 text-sm flex-wrap gap-3"> | |
{jobListing.tags.length > 0 | |
? jobListing.tags.map((tag, i) => ( | |
<button key={i} className="px-4 py-1 bg-gray-200 rounded-lg mr-2"> | |
{tag} | |
</button> | |
)) | |
: null} | |
</div> | |
<p className="pr-7 mt-5 text-sm">{jobListing.description}</p> | |
{connectedAccount != jobListing.owner && | |
!jobListing.bidders.includes(connectedAccount) ? ( | |
<button | |
onClick={() => handleBidding(jobListing.id)} | |
className="bg-green-500 px-3 py-1 text-sm text-white rounded-md mt-5" | |
> | |
Place Bid | |
</button> | |
) : connectedAccount != jobListing.owner && | |
jobListing.bidders.includes(connectedAccount) ? ( | |
<button className="mt-5 text-sm bg-green-200 px-3 py-2 rounded-sm text-gray-600"> | |
Your request is pending | |
</button> | |
) : ( | |
<button | |
onClick={manageAdminTasks} | |
className="mt-5 text-sm bg-green-400 px-3 py-2 rounded-sm text-white" | |
> | |
Manage | |
</button> | |
)} | |
</div> | |
) | |
} | |
export default JobListingCard |
Watch my new YouTube tutorial to learn how to integrate RainbowKit's multi-wallet connector into your dApp to improve the quality and user experience.
CreateJob Component
The CreateJob component enables users to create job listings through a user-friendly modal interface. Users input job details, including title, prize, skills, and description. The component supports up to five featured skills, provides modal controls, and offers transaction feedback via toast notifications. It seamlessly integrates with global state management for a smooth user experience.
import React, { useState } from 'react' | |
import { setGlobalState, truncate, useGlobalState } from '../store' | |
import { FaTimes } from 'react-icons/fa' | |
import { addJobListing } from '../services/blockchain' | |
import { toast } from 'react-toastify' | |
const CreateJob = () => { | |
const [createModal] = useGlobalState('createModal') | |
const [jobTitle, setJobTitle] = useState('') | |
const [prize, setPrize] = useState('') | |
const [description, setDescription] = useState('') | |
const [skill, setSkill] = useState('') | |
const [skills, setSkills] = useState([]) | |
const addSkills = () => { | |
if (skills.length != 5) { | |
setSkills((prevState) => [...prevState, skill]) | |
} | |
setSkill('') | |
} | |
const removeSkill = (index) => { | |
skills.splice(index, 1) | |
setSkills(() => [...skills]) | |
} | |
const closeModal = () => { | |
setGlobalState('createModal', 'scale-0') | |
setJobTitle('') | |
setPrize('') | |
setSkills([]) | |
setSkill('') | |
setDescription('') | |
} | |
const handleSubmit = async (e) => { | |
e.preventDefault() | |
if (jobTitle == '' || prize == '' || skills.length < 3 || description == '') | |
return | |
const params = { | |
jobTitle, | |
description, | |
tags: skills.slice(0, 5).join(','), | |
description, | |
prize, | |
} | |
await toast.promise( | |
new Promise(async (resolve, reject) => { | |
await addJobListing(params) | |
.then(async (tx) => { | |
closeModal() | |
resolve(tx) | |
}) | |
.catch(() => reject()) | |
}), | |
{ | |
pending: 'Approve transaction...', | |
success: 'job added successfully 👌', | |
error: 'Encountered error 🤯', | |
} | |
) | |
} | |
return ( | |
<div | |
className={`fixed top-0 left-0 w-screen h-screen flex items-center justify-center | |
bg-black bg-opacity-50 transform z-50 transition-transform duration-300 ${createModal}`} | |
> | |
<div className="bg-white text-black shadow-md shadow-green-500 rounded-xl w-11/12 md:w-2/5 h-7/12 p-6"> | |
<div className="relative"> | |
<button | |
onClick={closeModal} | |
className="border-0 bg-transparent focus:outline-none absolute -top-2 -right-2" | |
> | |
<FaTimes /> | |
</button> | |
<div> | |
<h3 className="text-xl mb-8">Create a Job</h3> | |
<form className="" onSubmit={handleSubmit}> | |
<div className="mb-5 flex flex-col space-y-1"> | |
<label htmlFor="jt">Job Title</label> | |
<input | |
id="jt" | |
value={jobTitle} | |
placeholder="e.g. content writer..." | |
type="text" | |
className="rounded-md text-sm" | |
onChange={(e) => setJobTitle(e.target.value)} | |
required | |
/> | |
</div> | |
<div className="mb-5 flex flex-col space-y-1"> | |
<label htmlFor="desc">Prize</label> | |
<input | |
id="number" | |
value={prize} | |
placeholder="eg. 0.04" | |
step={0.0001} | |
type="text" | |
className="rounded-md text-sm" | |
onChange={(e) => setPrize(e.target.value)} | |
required | |
/> | |
</div> | |
<div className="mb-1 flex flex-col space-y-1 relative"> | |
<label htmlFor="desc">Featured skills</label> | |
<input | |
id="text" | |
step={0.0001} | |
type="text" | |
value={skill} | |
className="rounded-md text-sm" | |
placeholder="Range (3 - 5) skills" | |
onChange={(e) => setSkill(e.target.value)} | |
/> | |
{skills.length != 5 ? ( | |
<span | |
className="cursor-pointer absolute top-[29px] right-1 py-1 px-4 bg-green-500 text-white text-sm rounded-md" | |
onClick={addSkills} | |
> | |
add | |
</span> | |
) : null} | |
</div> | |
<div className="flex items-center flex-wrap rounded-xl mt-2 mb-4 "> | |
{skills.map((skill, i) => ( | |
<div | |
key={i} | |
className="p-2 rounded-full text-gray-500 bg-gray-200 font-semibold | |
flex items-center w-max cursor-pointer active:bg-gray-300 | |
transition duration-300 ease space-x-2 text-xs mr-2 mt-2" | |
> | |
<span>{truncate(skill, 4, 4, 11)}</span> | |
<button | |
onClick={() => removeSkill(i)} | |
type="button" | |
className="bg-transparent hover focus:outline-none" | |
> | |
<FaTimes /> | |
</button> | |
</div> | |
))} | |
</div> | |
<div className="mb-5 flex flex-col space-y-1"> | |
<label htmlFor="desc">Description</label> | |
<textarea | |
id="desc" | |
value={description} | |
type="text" | |
placeholder="write something beautiful..." | |
className="rounded-b-md focus:outline-none focus:ring-0 text-sm" | |
onChange={(e) => setDescription(e.target.value)} | |
required | |
></textarea> | |
</div> | |
<div> | |
<button className="px-9 py-2 bg-green-500 text-white rounded-md"> | |
Create | |
</button> | |
</div> | |
</form> | |
</div> | |
</div> | |
</div> | |
</div> | |
) | |
} | |
export default CreateJob |
DeleteJob Component
The DeleteJob component is responsible for displaying a confirmation modal when users attempt to delete a job listing. It provides clear visual cues, including a trash icon, a confirmation message, and warning about irreversible actions. Users can either cancel or proceed with the deletion, with toast notifications providing feedback on the outcome of the transaction.
import React from 'react' | |
import { useGlobalState, setGlobalState } from '../store' | |
import { FaTrashAlt } from 'react-icons/fa' | |
import { toast } from 'react-toastify' | |
import { deleteJob } from '../services/blockchain' | |
const DeleteJob = () => { | |
const [deleteModal] = useGlobalState('deleteModal') | |
const [jobListing] = useGlobalState('jobListing') | |
const closeModal = () => { | |
setGlobalState('deleteModal', 'scale-0') | |
setGlobalState('jobListing', null) | |
} | |
const handleDelete = async () => { | |
await toast.promise( | |
new Promise(async (resolve, reject) => { | |
await deleteJob(jobListing.id) | |
.then(async () => { | |
closeModal() | |
resolve() | |
}) | |
.catch(() => reject()) | |
}), | |
{ | |
pending: 'Approve transaction...', | |
success: 'job deleted successfully 👌', | |
error: 'Encountered error 🤯', | |
} | |
) | |
} | |
return ( | |
<div | |
className={`fixed top-0 left-0 w-screen h-screen flex items-center justify-center | |
bg-black bg-opacity-50 transform z-50 transition-transform duration-300 ${deleteModal}`} | |
> | |
<div className="bg-white text-black rounded-xl w-11/12 md:w-2/5 h-7/12 px-6 py-3"> | |
<div className="relative"> | |
<div> | |
<div className="text-center flex justify-center items-center p-2 my-3"> | |
<FaTrashAlt className="text-2xl text-red-500" /> | |
</div> | |
<p className="text-center my-1"> | |
Are you sure, you want to delete this? | |
</p> | |
<h4 className="text-sm text-center">This action can't be undone</h4> | |
<div className="flex justify-between items-center mt-5"> | |
<button | |
onClick={closeModal} | |
className="py-1 px-4 bg-green-600 text-white rounded-md" | |
> | |
Cancel | |
</button> | |
<button | |
onClick={handleDelete} | |
className="py-1 px-4 bg-red-500 text-white rounded-md" | |
> | |
Proceed | |
</button> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
) | |
} | |
export default DeleteJob |
UpdateJob Component
The UpdateJob component is responsible for displaying a modal that allows users to update an existing job listing. It retrieves the job details, pre-fills the form fields, and provides an interface for users to make changes. Upon submission, it triggers the update process on the blockchain and displays toast notifications to indicate the outcome of the transaction.
import React, { useEffect, useState } from 'react' | |
import { setGlobalState, truncate, useGlobalState } from '../store' | |
import { FaTimes } from 'react-icons/fa' | |
import { updateJob } from '../services/blockchain' | |
import { toast } from 'react-toastify' | |
const UpdateJob = () => { | |
const [jobListing] = useGlobalState('jobListing') | |
const [jobTitle, setJobTitle] = useState('') | |
const [prize, setPrize] = useState('') | |
const [description, setDescription] = useState('') | |
const [skill, setSkill] = useState('') | |
const [skills, setSkills] = useState([]) | |
const [updateModal] = useGlobalState('updateModal') | |
useEffect(() => { | |
setJobTitle(jobListing?.jobTitle) | |
setPrize(jobListing?.prize) | |
setDescription(jobListing?.description) | |
setSkills(jobListing?.tags) | |
}, [jobListing]) | |
const addSkills = () => { | |
if (skills.length != 5) { | |
setSkills((prevState) => [...prevState, skill]) | |
} | |
setSkill('') | |
} | |
const removeSkill = (index) => { | |
skills.splice(index, 1) | |
setSkills(() => [...skills]) | |
} | |
const closeModal = () => { | |
setGlobalState('updateModal', 'scale-0') | |
setJobTitle('') | |
setPrize('') | |
setDescription('') | |
setGlobalState('jobListing', null) | |
} | |
const handleSubmit = async (e) => { | |
e.preventDefault() | |
if (jobTitle == '' || prize == '' || skills.length < 3 || description == '') | |
return | |
const params = { | |
id: jobListing.id, | |
jobTitle, | |
description, | |
tags: skills.slice(0, 5).join(','), | |
description, | |
prize, | |
} | |
await toast.promise( | |
new Promise(async (resolve, reject) => { | |
await updateJob(params) | |
.then(async () => { | |
closeModal() | |
resolve() | |
}) | |
.catch(() => reject()) | |
}), | |
{ | |
pending: 'Approve transaction...', | |
success: 'job updated successfully 👌', | |
error: 'Encountered error 🤯', | |
} | |
) | |
} | |
return ( | |
<div | |
className={`fixed top-0 left-0 w-screen h-screen flex items-center justify-center | |
bg-black bg-opacity-50 transform z-50 transition-transform duration-300 ${updateModal}`} | |
> | |
<div className="bg-white text-black shadow-md shadow-green-500 rounded-xl w-11/12 md:w-2/5 h-7/12 p-6"> | |
<div className="relative"> | |
<button | |
onClick={closeModal} | |
className="border-0 bg-transparent focus:outline-none absolute -top-2 -right-2" | |
> | |
<FaTimes /> | |
</button> | |
<div> | |
<h3 className="text-xl mb-8">Update Job</h3> | |
<form className="" onSubmit={handleSubmit}> | |
<div className="mb-5 flex flex-col space-y-1"> | |
<label htmlFor="jt">Job Title</label> | |
<input | |
id="jt" | |
value={jobTitle} | |
placeholder="e.g. content writer..." | |
type="text" | |
className="rounded-md text-sm" | |
onChange={(e) => setJobTitle(e.target.value)} | |
required | |
/> | |
</div> | |
<div className="mb-5 flex flex-col space-y-1"> | |
<label htmlFor="desc">Prize</label> | |
<input | |
id="number" | |
value={prize} | |
placeholder="eg. 0.04" | |
step={0.0001} | |
type="text" | |
className="rounded-md text-sm bg-gray-200" | |
onChange={(e) => setPrize(e.target.value)} | |
disabled | |
readOnly | |
/> | |
</div> | |
<div className="mb-1 flex flex-col space-y-1 relative"> | |
<label htmlFor="desc">Featured skills</label> | |
<input | |
id="number" | |
step={0.0001} | |
type="text" | |
value={skill} | |
className="rounded-md text-sm" | |
placeholder="Range (3 - 5) skills" | |
onChange={(e) => setSkill(e.target.value)} | |
/> | |
{skills?.length != 5 ? ( | |
<button | |
className="absolute top-[29px] right-1 py-1 px-4 bg-green-500 text-white text-sm rounded-md" | |
onClick={addSkills} | |
> | |
add | |
</button> | |
) : null} | |
</div> | |
<div className="grid lg:grid-cols-4 md:grid-cols-3 grid-cols-2 gap-2 rounded-xl mt-2 mb-4 "> | |
{skills?.map((skill, i) => ( | |
<div | |
key={i} | |
className="p-2 rounded-full text-gray-500 bg-gray-200 font-semibold | |
flex items-center w-max cursor-pointer active:bg-gray-300 | |
transition duration-300 ease space-x-2 text-xs mr-2 mt-2" | |
> | |
<span>{truncate(skill, 4, 4, 11)}</span> | |
<button | |
onClick={() => removeSkill(i)} | |
type="button" | |
className="bg-transparent hover focus:outline-none" | |
> | |
<FaTimes /> | |
</button> | |
</div> | |
))} | |
</div> | |
<div className="mb-5 flex flex-col space-y-1"> | |
<label htmlFor="desc">Description</label> | |
<textarea | |
id="desc" | |
value={description} | |
type="text" | |
placeholder="write something beautiful..." | |
className="rounded-b-md focus:outline-none focus:ring-0 text-sm" | |
onChange={(e) => setDescription(e.target.value)} | |
></textarea> | |
</div> | |
<div> | |
<button className="px-9 py-2 bg-green-500 text-white rounded-md"> | |
Update | |
</button> | |
</div> | |
</form> | |
</div> | |
</div> | |
</div> | |
</div> | |
) | |
} | |
export default UpdateJob |
JobListingOwnerActions Component
This component renders a set of actions that an owner of a job listing can perform. It displays information about the job listing, including the title, price, tags, and description. Depending on the status of the job listing, it allows the owner to perform actions such as updating, deleting, viewing bidders, paying, and chatting with a freelancer. The component also handles different visual representations based on the state of the job listing, such as showing a "Completed" status when the job has been paid out.
import React, { useEffect } from 'react' | |
import { | |
FaEthereum, | |
FaPenAlt, | |
FaTrashAlt, | |
FaMoneyBill, | |
FaArrowRight, | |
} from 'react-icons/fa' | |
import { IoMdCheckmarkCircleOutline } from 'react-icons/io' | |
import { setGlobalState } from '../store' | |
import { Link, useNavigate } from 'react-router-dom' | |
import { getAcceptedFreelancer } from '../services/blockchain' | |
const JobListingOwnerActions = ({ jobListing, editable }) => { | |
const getFreelancer = async () => { | |
await getAcceptedFreelancer(jobListing?.id) | |
} | |
useEffect(() => { | |
getFreelancer() | |
}, []) | |
const navigate = useNavigate() | |
const openUpdateModal = () => { | |
setGlobalState('updateModal', 'scale-100') | |
setGlobalState('jobListing', jobListing) | |
} | |
const openPayoutModal = () => { | |
setGlobalState('payoutModal', 'scale-100') | |
setGlobalState('jobListing', jobListing) | |
} | |
const openDeleteModal = () => { | |
setGlobalState('deleteModal', 'scale-100') | |
setGlobalState('jobListing', jobListing) | |
} | |
const viewBidders = (id) => { | |
navigate(`/viewbidders/${id}`) | |
} | |
return ( | |
<div className="border-t border-b border-l border-r border-gray-300 py-3 px-5 mt-2"> | |
<h4>{jobListing.jobTitle}</h4> | |
<div className="flex mt-2 items-center"> | |
<FaEthereum className="text-md cursor-pointer" /> | |
<span className="text-md">{jobListing.prize}</span> | |
</div> | |
<div className="flex items-center mt-3 text-sm flex-wrap gap-3"> | |
{jobListing.tags.length > 0 | |
? jobListing.tags.map((tag, i) => ( | |
<button key={i} className="px-4 py-1 bg-gray-200 rounded-lg mr-2"> | |
{tag} | |
</button> | |
)) | |
: null} | |
</div> | |
<p className="pr-7 mt-5 text-sm">{jobListing.description}</p> | |
<div className="flex space-x-2"> | |
{editable && !jobListing.paidOut && ( | |
<div className="flex mt-5 space-x-3"> | |
{jobListing.listed && ( | |
<> | |
<button | |
onClick={openUpdateModal} | |
className="flex items-center px-3 py-1 border-[1px] border-green-500 text-green-500 space-x-2 rounded-md" | |
> | |
<FaPenAlt /> | |
<span className="text-sm">Update</span> | |
</button> | |
<button | |
onClick={openDeleteModal} | |
className="flex items-center px-2 py-1 border-[1px] border-red-500 text-red-500 space-x-2 rounded-md text-sm" | |
> | |
<FaTrashAlt /> | |
<span className="text-sm">Delete</span> | |
</button> | |
{jobListing.freelancer != | |
'0x0000000000000000000000000000000000000000' && ( | |
<Link | |
to={`/chats/${jobListing.freelancer}`} | |
className="flex items-center px-3 py-1 border-[1px] border-green-500 text-green-500 space-x-2 rounded-md" | |
> | |
<span className="text-sm">Chat with freelancer</span> | |
</Link> | |
)} | |
</> | |
)} | |
{jobListing.listed && ( | |
<button | |
className="text-sm py-1 px-3 bg-green-400 text-white flex items-center space-x-3 rounded-md" | |
onClick={() => viewBidders(jobListing.id)} | |
> | |
<span>View bidders</span> | |
<FaArrowRight className="-rotate-45" /> | |
</button> | |
)} | |
{!jobListing.listed && !jobListing.paidOut && ( | |
<> | |
<button | |
onClick={openPayoutModal} | |
className="flex items-center px-3 py-1 border-[1px] border-sky-500 text-sky-500 space-x-2 rounded-md" | |
> | |
<FaMoneyBill /> | |
<span className="text-sm">Pay</span> | |
</button> | |
{jobListing.freelancer != | |
'0x0000000000000000000000000000000000000000' && ( | |
<Link | |
to={`/chats/${jobListing.freelancer}`} | |
className="flex items-center px-3 py-1 border-[1px] border-green-500 text-green-500 space-x-2 rounded-md" | |
> | |
<span className="text-sm">Chat with freelancer</span> | |
</Link> | |
)} | |
</> | |
)} | |
</div> | |
)} | |
{editable && jobListing.paidOut == true && ( | |
<div className=""> | |
<button className="text-sm px-2 py-1 text-green-600 mt-3 flex items-center space-x-1"> | |
<span>Completed</span> | |
<IoMdCheckmarkCircleOutline /> | |
</button> | |
</div> | |
)} | |
</div> | |
</div> | |
) | |
} | |
export default JobListingOwnerActions |
JobBid Component
This component displays information about a job listing for potential bidders. It renders the job title, price (in Ethereum), tags, and job description. Additionally, it provides a "Chat with freelancer" button that allows users to initiate a chat with the owner of the job listing. It serves as a preview of the job for individuals interested in bidding on it.
import React from 'react' | |
import { FaEthereum } from 'react-icons/fa' | |
import { Link } from 'react-router-dom' | |
const JobBid = ({ jobListing }) => { | |
return ( | |
<div className="border-t border-b border-l border-r border-gray-300 py-6 px-5"> | |
<h4>{jobListing.jobTitle}</h4> | |
<div className="flex mt-2 items-center"> | |
<FaEthereum className="text-md cursor-pointer" /> | |
<span className="text-md">{jobListing.prize}</span> | |
</div> | |
<div className="flex items-center mt-3 text-sm flex-wrap gap-3"> | |
{jobListing.tags.length > 0 | |
? jobListing.tags.map((tag, i) => ( | |
<button key={i} className="px-4 py-1 bg-gray-200 rounded-lg mr-2"> | |
{tag} | |
</button> | |
)) | |
: null} | |
</div> | |
<p className="pr-7 mt-5 text-sm">{jobListing.description}</p> | |
<div className='flex mt-5'> | |
<Link | |
to={`/chats/${jobListing.owner}`} | |
className="flex items-center px-3 py-1 border-[1px] border-green-500 text-green-500 space-x-2 rounded-md" | |
> | |
<span className="text-sm">Chat with owner</span> | |
</Link> | |
</div> | |
</div> | |
) | |
} | |
export default JobBid |
ApplicantsCard Component
This component display information about applicants or bidders for a job listing. It shows the bidder's truncated Ethereum account address, along with buttons for initiating a chat with the bidder and accepting their bid. The "Chat" button links to a chat page with the bidder, and the "Accept" button allows the job owner to accept the bidder's offer. It provides a user-friendly interface for managing job applications.
import React from 'react' | |
import { truncate } from '../store' | |
import { acceptBid } from '../services/blockchain' | |
import { toast } from 'react-toastify' | |
import { MdOutlineChat } from 'react-icons/md' | |
import { Link } from 'react-router-dom' | |
const ApplicantsCard = ({ bidder }) => { | |
const handleAcceptingBid = async (bid, jid, account) => { | |
await toast.promise( | |
new Promise(async (resolve, reject) => { | |
await acceptBid(bid, jid, account) | |
.then(async () => resolve()) | |
.catch(() => reject()) | |
}), | |
{ | |
pending: 'Approve transaction...', | |
success: 'bid accepted successfully 👌', | |
error: 'Encountered error 🤯', | |
} | |
) | |
} | |
return ( | |
<div | |
className="my-3 bg-white shadow-lg p-3 rounded-lg flex justify-between | |
items-center border-[1px] border-gray-300 flex-wrap" | |
> | |
<h4>{truncate(bidder.account, 4, 4, 11)}</h4> | |
<div className="flex items-center space-x-3"> | |
<Link | |
to={`/chats/${bidder.account}`} | |
className="flex justify-center items-center space-x-1 py-1 px-5 rounded-full | |
bg-blue-500 text-white max-sm:text-sm" | |
> | |
<MdOutlineChat size={20} /> | |
<span>Chat</span> | |
</Link> | |
<button | |
onClick={() => | |
handleAcceptingBid(bidder.id, bidder.jId, bidder.account) | |
} | |
className="py-1 px-5 rounded-full bg-green-500 text-white max-sm:text-sm" | |
> | |
Accept | |
</button> | |
</div> | |
</div> | |
) | |
} | |
export default ApplicantsCard |
Payout Components
The Payout
component is used to facilitate the payout process for a job listing. It displays a modal dialog with an option to initiate a payment. Users can either proceed with the payout or cancel it. Upon initiating the payout, it communicates with the blockchain using the payout
function and displays toast notifications to inform the user about the transaction status. This component provides a user-friendly interface for handling payouts for completed jobs.
import React from "react"; | |
import { useGlobalState, setGlobalState } from "../store"; | |
import { MdAttachMoney } from "react-icons/md"; | |
import { toast } from "react-toastify"; | |
import { payout } from "../services/blockchain"; | |
const Payout = () => { | |
const [payoutModal] = useGlobalState("payoutModal"); | |
const [jobListing] = useGlobalState("jobListing"); | |
const closeModal = () => { | |
setGlobalState("payoutModal", "scale-0"); | |
setGlobalState("jobListing", null); | |
}; | |
const handlePayout = async () => { | |
await toast.promise( | |
new Promise(async (resolve, reject) => { | |
await payout(jobListing.id) | |
.then(async () => { | |
closeModal(); | |
resolve(); | |
}) | |
.catch(() => reject()); | |
}), | |
{ | |
pending: "Approve transaction...", | |
success: "payment successfully 👌", | |
error: "Encountered error 🤯", | |
} | |
); | |
}; | |
return ( | |
<div | |
className={`fixed top-0 left-0 w-screen h-screen flex items-center justify-center | |
bg-black bg-opacity-50 transform z-50 transition-transform duration-300 ${payoutModal}`} | |
> | |
<div className="bg-white text-black rounded-xl w-11/12 md:w-2/5 h-7/12 px-6 py-3"> | |
<div className="relative"> | |
<div> | |
<div className="text-center flex justify-center items-center p-2 my-3"> | |
<MdAttachMoney className="text-3xl text-blue-600" /> | |
</div> | |
<p className="text-center my-1"> | |
Are you sure, you want to initiate this payment? | |
</p> | |
<div className="flex justify-between items-center mt-5"> | |
<button | |
onClick={closeModal} | |
className="py-1 px-4 bg-green-600 text-white rounded-md" | |
> | |
Cancel | |
</button> | |
<button | |
onClick={handlePayout} | |
className="py-1 px-4 bg-blue-600 text-white rounded-md" | |
> | |
Proceed | |
</button> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
); | |
}; | |
export default Payout; |
Want to learn how to build an Answer-To-Earn DApp with Next.js, TypeScript, Tailwind CSS, and Solidity? Watch this video now!
This video is a great resource for anyone who wants to learn how to build decentralized applications and earn ethers.
Now that we have covered all the components in this application, it is time to start coupling the various pages together. Let's start with the homepage.
To begin developing the pages of our application, we will create a new folder called pages
inside the src
directory. This folder will hold all the pages needed for our project.
For each of the pages listed below, you will need to create a corresponding file inside the src/pages
folder, just as you did before with the components.
Home Page
The Home
page combines other components, including Header
, Hero
, and CreateJob
, to create the user interface for the home page. The Header
typically contains navigation links and branding, the Hero
section might display introductory content or visuals, and the CreateJob
section allows users to create new job listings. Overall, this component assembles these parts to create the complete homepage of the application.
import React from 'react' | |
import { Header, Hero, CreateJob } from '../components' | |
const Home = () => { | |
return ( | |
<div> | |
<Header /> | |
<Hero /> | |
<CreateJob /> | |
</div> | |
) | |
} | |
export default Home |
MyProjects Page
The MyProjects
page is where users can manage their own job listings and take various actions on them. It includes components such as Header
for navigation, JobListingOwnerActions
to display and manage job listings, UpdateJob
for updating job details, DeleteJob
for deleting job listings, and Payout
for initiating payments. Users can view their posted jobs, edit or delete them, and initiate payouts to freelancers. If no jobs are posted, it displays a message indicating that no jobs are available.
import React from 'react' | |
import { | |
DeleteJob, | |
Header, | |
JobListingOwnerActions, | |
Payout, | |
UpdateJob, | |
} from '../components' | |
import { useGlobalState } from '../store' | |
const MyProjects = () => { | |
const [myjobs] = useGlobalState('myjobs') | |
const [connectedAccount] = useGlobalState('connectedAccount') | |
return ( | |
<div> | |
<Header /> | |
<div className="px-5 my-7"> | |
{myjobs.map((myjob, i) => ( | |
<JobListingOwnerActions | |
key={i} | |
jobListing={myjob} | |
editable={myjob.owner == connectedAccount} | |
/> | |
))} | |
{myjobs.length < 1 && ( | |
<h2 className="text-lg text-gray-500">No Posted Jobs Yet</h2> | |
)} | |
<UpdateJob /> | |
<DeleteJob /> | |
<Payout /> | |
</div> | |
</div> | |
) | |
} | |
export default MyProjects |
MyJobs Page
The MyJobs
component is where users can view the tasks or jobs that have been assigned to them. It includes a Header
for navigation and a list of JobBid
components that display information about each assigned task. If there are assigned tasks, it shows them with their details; otherwise, it displays a message indicating that there are no assigned tasks for the user.
import React from 'react' | |
import { Header, JobBid } from '../components' | |
import { useGlobalState } from '../store' | |
const MyJobs = () => { | |
const [mygigs] = useGlobalState('mygigs') | |
return ( | |
<div className=""> | |
<Header /> | |
<div className="mt-11 px-4"> | |
<h3 className="text-xl px-4 my-4"> | |
{mygigs.length > 0 | |
? 'Assigned Tasks.' | |
: "You Don't Have Any Assigned task."} | |
</h3> | |
<div className="px-3"> | |
{mygigs.map((mygig, i) => ( | |
<JobBid key={i} jobListing={mygig} /> | |
))} | |
</div> | |
</div> | |
</div> | |
) | |
} | |
export default MyJobs |
MyBids Page
The MyBids
page is where users can view the jobs they have applied for by placing bids. It includes a Header
for navigation and a list of JobBid
components that display information about each job the user has bid on. If there are jobs the user has bid on, it shows them with their details; otherwise, it displays a message indicating that the user hasn't bid on any jobs yet.
import React from 'react' | |
import { Header, JobBid } from '../components' | |
import { useGlobalState } from '../store' | |
const MyBids = () => { | |
const [mybidjobs] = useGlobalState('mybidjobs') | |
return ( | |
<div className=""> | |
<Header /> | |
<div className="mt-11 px-4"> | |
<h3 className="text-xl px-4 my-4"> | |
{mybidjobs.length > 0 | |
? 'Jobs You Applied For' | |
: "You Haven't Bid on Any Jobs Yet."} | |
</h3> | |
<div className="px-3"> | |
{mybidjobs.length > 0 | |
? mybidjobs.map((mybidjob, i) => ( | |
<JobBid key={i} jobListing={mybidjob} /> | |
)) | |
: null} | |
</div> | |
</div> | |
</div> | |
) | |
} | |
export default MyBids |
ViewBidders Page
The ViewBidders
page is where users can view the list of applicants for a specific job listing. It utilizes the useParams
hook to extract the job ID from the URL, fetches the list of bidders and job details from the blockchain, and displays them. Depending on whether there are applicants or if the position is filled or vacant, it displays relevant information such as applicants' cards or status messages. It also includes a Header
component for navigation.
import React, { useEffect } from 'react' | |
import { getBidders, getJob } from '../services/blockchain' | |
import { useParams } from 'react-router-dom' | |
import { useGlobalState } from '../store' | |
import { ApplicantsCard, Header } from '../components' | |
const ViewBidders = () => { | |
const { id } = useParams() | |
const [bidders] = useGlobalState('bidders') | |
const [job] = useGlobalState('job') | |
const fetchBidders = async () => { | |
await getBidders(id) | |
await getJob(id) | |
} | |
useEffect(() => { | |
fetchBidders() | |
}, []) | |
return ( | |
<div> | |
<Header /> | |
<div className="px-20 max-sm:px-4 mt-20"> | |
<h2 className="text-2xl my-3 px-3"> | |
{bidders?.length > 0 | |
? 'Applicants' | |
: !job?.listed | |
? 'Position filled' | |
: 'No Applicants yet.'} | |
</h2> | |
{bidders?.length > 0 | |
? bidders.map((bidder, i) => ( | |
<ApplicantsCard key={i} bidder={bidder} /> | |
)) | |
: null} | |
</div> | |
</div> | |
) | |
} | |
export default ViewBidders |
Authenticate Page
The Authenticate
page is for user authentication for chatting within the application. It allows users to either log in or sign up for chat functionality using the CometChat service. Upon successful authentication, it displays a success message and navigates the user to the chat messages page. It also includes a Header
component for navigation and provides feedback to the user through toast notifications.
import { toast } from 'react-toastify' | |
import { Header } from '../components' | |
import { loginWithCometChat, signUpWithCometChat } from '../services/chat' | |
import { setGlobalState, useGlobalState } from '../store' | |
import { useNavigate } from 'react-router-dom' | |
const Authenticate = () => { | |
const [connectedAccount] = useGlobalState('connectedAccount') | |
const navigate = useNavigate() | |
const handleSignUp = async () => { | |
await toast.promise( | |
new Promise(async (resolve, reject) => { | |
await signUpWithCometChat(connectedAccount) | |
.then((user) => resolve(user)) | |
.catch((error) => { | |
alert(JSON.stringify(error)) | |
reject(error) | |
}) | |
}), | |
{ | |
pending: 'Signing up...', | |
success: 'Signed up successfully, please login 👌', | |
error: 'Encountered error 🤯', | |
} | |
) | |
} | |
const handleLogin = async () => { | |
await toast.promise( | |
new Promise(async (resolve, reject) => { | |
await loginWithCometChat(connectedAccount) | |
.then((user) => { | |
setGlobalState('currentUser', user) | |
navigate('/messages') | |
resolve(user) | |
}) | |
.catch((error) => { | |
alert(JSON.stringify(error)) | |
reject(error) | |
}) | |
}), | |
{ | |
pending: 'Logging...', | |
success: 'Logged in successfully 👌', | |
error: 'Encountered error 🤯', | |
} | |
) | |
} | |
return ( | |
<> | |
<Header /> | |
<div className="w-full sm:w-3/5 mx-auto mt-8 px-3"> | |
<h1 className="text-2xl font-bold text-center">Chats Authentication</h1> | |
<p className="text-center"> | |
Login or sign up to chat with your client. | |
</p> | |
<div className="flex justify-center items-center space-x-3 mt-5"> | |
<button | |
onClick={handleLogin} | |
className="flex justify-center items-center space-x-1 py-1 px-5 rounded-full | |
bg-blue-500 text-white max-sm:text-sm" | |
> | |
Login | |
</button> | |
<button | |
onClick={handleSignUp} | |
className="py-1 px-5 rounded-full bg-green-500 text-white max-sm:text-sm" | |
> | |
Sign up | |
</button> | |
</div> | |
</div> | |
</> | |
) | |
} | |
export default Authenticate |
RecentConversations Page
The "RecentConversations" page in the DappWorks app displays a list of recent chat conversations. It fetches and displays these conversations, allowing users to click on a conversation to navigate to the chat interface with that user. Each conversation displays the user's name and a unique Identicon for easy identification. If there are no recent chats, a message notifies the user. This page provides a convenient way to access and manage recent chat interactions.
import { useEffect } from 'react' | |
import { getConversations } from '../services/chat' | |
import { Link } from 'react-router-dom' | |
import { setGlobalState, useGlobalState, truncate } from '../store' | |
import Identicon from 'react-identicons' | |
import { Header } from '../components' | |
const RecentConversations = () => { | |
const [recentConversations] = useGlobalState('recentConversations') | |
const [currentUser] = useGlobalState('currentUser') | |
useEffect(() => { | |
getConversations().then((users) => | |
setGlobalState('recentConversations', users) | |
) | |
}, [currentUser]) | |
return ( | |
<> | |
<Header /> | |
<div className="w-full sm:w-3/5 mx-auto mt-8 px-3"> | |
<h1 className="text-2xl font-bold text-center">Your Recent chats</h1> | |
{recentConversations?.map((conversation, index) => ( | |
<Link | |
className="flex items-center space-x-3 w-full my-3 | |
border-b border-b-gray-100 p-3 bg-gray-100" | |
to={`/chats/${conversation.conversationWith.uid}`} | |
key={index} | |
> | |
<Identicon | |
className="rounded-full shadow-gray-500 shadow-sm bg-white" | |
string={conversation.conversationWith.uid} | |
size={20} | |
/> | |
<p>{truncate(conversation.conversationWith.name, 4, 4, 11)}</p> | |
</Link> | |
))} | |
{recentConversations.length < 1 && ( | |
<p className="text-center">you don't have any recent chats</p> | |
)} | |
</div> | |
</> | |
) | |
} | |
export default RecentConversations |
Chats Page
The Chats
page is responsible for managing and displaying chat messages between users. It retrieves and displays messages based on the selected user, allows users to send messages, and automatically updates the chat in real-time. Users can see their own messages on the right side and messages from the other user on the left side, with profile icons and message content. The component also includes a Header
for navigation and maintains a scrolling chat container for easy viewing of messages.
import { useState, useEffect } from 'react' | |
import { useParams } from 'react-router-dom' | |
import { setGlobalState, useGlobalState, truncate } from '../store' | |
import Identicon from 'react-identicons' | |
import { getMessages, sendMessage, listenForMessage } from '../services/chat' | |
import { Header } from '../components' | |
const Chats = () => { | |
const { id } = useParams() | |
const [messages] = useGlobalState('messages') | |
const [currentUser] = useGlobalState('currentUser') | |
const [message, setMessage] = useState('') | |
useEffect(() => { | |
getMessages(id).then((msgs) => setGlobalState('messages', msgs)) | |
handleListener() | |
}, [currentUser]) | |
const onSendMessage = async (e) => { | |
e.preventDefault() | |
if (!message) return | |
await sendMessage(id, message).then((msg) => { | |
setGlobalState('messages', (prevState) => [...prevState, msg]) | |
setMessage('') | |
scrollToEnd() | |
}) | |
} | |
const handleListener = async () => { | |
await listenForMessage(id).then((msg) => { | |
setGlobalState('messages', (prevState) => [...prevState, msg]) | |
scrollToEnd() | |
}) | |
} | |
const scrollToEnd = () => { | |
const elmnt = document.getElementById('messages-container') | |
elmnt.scrollTop = elmnt.scrollHeight | |
} | |
return ( | |
<> | |
<Header /> | |
<div | |
className="bg-gray-100 rounded-2xl h-[calc(100vh_-_13rem)] | |
w-4/5 flex flex-col justify-between relative mx-auto mt-8 border-t border-t-gray-100" | |
> | |
<h1 | |
className="text-2xl font-bold text-center absolute top-0 | |
bg-white w-full shadow-sm py-2" | |
> | |
Chats | |
</h1> | |
<div | |
id="messages-container" | |
className="h-[calc(100vh_-_20rem)] overflow-y-scroll w-full p-4 pt-16" | |
> | |
{messages.length > 0 | |
? messages.map((msg, index) => ( | |
<Message message={msg.text} uid={msg.sender.uid} key={index} /> | |
)) | |
: 'No message yet'} | |
</div> | |
<form onSubmit={onSendMessage} className="w-full"> | |
<input | |
type="text" | |
value={message} | |
onChange={(e) => setMessage(e.target.value)} | |
className="h-full w-full py-5 focus:outline-none focus:ring-0 rounded-md | |
border-none bg-[rgba(0,0,0,0.7)] text-white placeholder-white" | |
placeholder="Leave a message..." | |
/> | |
</form> | |
</div> | |
</> | |
) | |
} | |
const Message = ({ message, uid }) => { | |
const [connectedAccount] = useGlobalState('connectedAccount') | |
return uid == connectedAccount ? ( | |
<div className="flex justify-end items-center space-x-4 mb-3"> | |
<div | |
className="flex flex-col bg-white py-2 px-4 space-y-2 | |
rounded-full rounded-br-none shadow-sm" | |
> | |
<div className="flex items-center space-x-2"> | |
<Identicon | |
string={uid} | |
size={20} | |
className="rounded-full bg-white shadow-sm" | |
/> | |
<p className="font-bold text-sm">{truncate(uid, 4, 4, 11)}</p> | |
</div> | |
<p className="text-sm">{message}</p> | |
</div> | |
</div> | |
) : ( | |
<div className="flex justify-start items-center space-x-4 mb-3"> | |
<div | |
className="flex flex-col bg-white py-2 px-4 space-y-2 | |
rounded-full rounded-bl-none shadow-sm" | |
> | |
<div className="flex items-center space-x-2"> | |
<Identicon | |
string={uid} | |
size={20} | |
className="rounded-full bg-white shadow-sm" | |
/> | |
<p className="font-bold text-sm">{truncate(uid, 4, 4, 11)}</p> | |
</div> | |
<p className="text-sm">{message}</p> | |
</div> | |
</div> | |
) | |
} | |
export default Chats |
The Blockchain Service
This script is a JavaScript module that interacts with a blockchain smart contract using the Ethereum blockchain. It provides functions for various actions related to the blockchain and stores or retrieves data from the blockchain. Note that you will have to create a file called blockchain.jsx
within the src >> services
folder and paste the code below inside.
import { setGlobalState } from '../store' | |
import abi from '../abis/src/contracts/DappWorks.sol/DappWorks.json' | |
import address from '../abis/contractAddress.json' | |
import { ethers } from 'ethers' | |
import { logOutWithCometChat } from './chat' | |
const { ethereum } = window | |
const ContractAddress = address.address | |
const ContractAbi = abi.abi | |
let tx | |
const toWei = (num) => ethers.utils.parseEther(num.toString()) | |
const fromWei = (num) => ethers.utils.formatEther(num) | |
const getEthereumContract = async () => { | |
const accounts = await ethereum.request({ method: 'eth_accounts' }) | |
const provider = accounts[0] | |
? new ethers.providers.Web3Provider(ethereum) | |
: new ethers.providers.JsonRpcProvider(process.env.REACT_APP_RPC_URL) | |
const wallet = accounts[0] ? null : ethers.Wallet.createRandom() | |
const signer = provider.getSigner(accounts[0] ? undefined : wallet.address) | |
const contract = new ethers.Contract(ContractAddress, ContractAbi, signer) | |
return contract | |
} | |
const isWalletConnected = async () => { | |
try { | |
if (!ethereum) { | |
reportError('Please install Metamask') | |
return Promise.reject(new Error('Metamask not installed')) | |
} | |
const accounts = await ethereum.request({ method: 'eth_accounts' }) | |
if (accounts.length) { | |
setGlobalState('connectedAccount', accounts[0]) | |
} else { | |
console.log('No accounts found.') | |
} | |
window.ethereum.on('chainChanged', (chainId) => { | |
window.location.reload() | |
}) | |
window.ethereum.on('accountsChanged', async () => { | |
setGlobalState('connectedAccount', accounts[0]) | |
console.log('Account changed: ', accounts[0]) | |
await loadData() | |
await isWalletConnected() | |
logOutWithCometChat() | |
}) | |
await loadData() | |
if (accounts.length) { | |
setGlobalState('connectedAccount', accounts[0]) | |
} else { | |
setGlobalState('connectedAccount', '') | |
console.log('No accounts found') | |
} | |
} catch (error) { | |
reportError(error) | |
} | |
} | |
const connectWallet = async () => { | |
try { | |
if (!ethereum) return alert('Please install Metamask') | |
const accounts = await ethereum.request({ method: 'eth_requestAccounts' }) | |
setGlobalState('connectedAccount', accounts[0]) | |
} catch (error) { | |
reportError(error) | |
} | |
} | |
const addJobListing = async ({ jobTitle, description, tags, prize }) => { | |
if (!ethereum) return alert('Please install Metamask') | |
return new Promise(async (resolve, reject) => { | |
try { | |
const contract = await getEthereumContract() | |
tx = await contract.addJobListing(jobTitle, description, tags, { | |
value: toWei(prize), | |
}) | |
await tx.wait() | |
await loadData() | |
resolve(tx) | |
} catch (err) { | |
reportError(err) | |
reject(err) | |
} | |
}) | |
} | |
const updateJob = async ({ id, jobTitle, description, tags }) => { | |
if (!ethereum) return alert('Please install Metamask') | |
return new Promise(async (resolve, reject) => { | |
try { | |
const contract = await getEthereumContract() | |
tx = await contract.updateJob(id, jobTitle, description, tags) | |
await tx.wait() | |
await loadData() | |
resolve(tx) | |
} catch (err) { | |
reportError(err) | |
reject(err) | |
} | |
}) | |
} | |
const deleteJob = async (id) => { | |
if (!ethereum) return alert('Please install Metamask') | |
return new Promise(async (resolve, reject) => { | |
try { | |
const contract = await getEthereumContract() | |
tx = await contract.deleteJob(id) | |
await tx.wait() | |
await loadData() | |
resolve(tx) | |
} catch (err) { | |
reportError(err) | |
reject(err) | |
} | |
}) | |
} | |
const bidForJob = async (id) => { | |
if (!ethereum) return alert('Please install Metamask') | |
return new Promise(async (resolve, reject) => { | |
try { | |
const contract = await getEthereumContract() | |
tx = await contract.bidForJob(id) | |
await tx.wait() | |
await getJobs() | |
resolve(tx) | |
} catch (err) { | |
reportError(err) | |
reject(err) | |
} | |
}) | |
} | |
const acceptBid = async (id, jId, bidder) => { | |
if (!ethereum) return alert('Please install Metamask') | |
return new Promise(async (resolve, reject) => { | |
try { | |
const contract = await getEthereumContract() | |
tx = await contract.acceptBid(id, jId, bidder) | |
await tx.wait() | |
await loadData() | |
await getBidders(jId) | |
resolve(tx) | |
} catch (err) { | |
reportError(err) | |
reject(err) | |
} | |
}) | |
} | |
const dispute = async (id) => { | |
if (!ethereum) return alert('Please install Metamask') | |
return new Promise(async (resolve, reject) => { | |
try { | |
const contract = await getEthereumContract() | |
tx = await contract.dispute(id) | |
await tx.wait() | |
await getJob(id) | |
resolve(tx) | |
} catch (err) { | |
reportError(err) | |
reject(err) | |
} | |
}) | |
} | |
const resolved = async (id) => { | |
if (!ethereum) return alert('Please install Metamask') | |
return new Promise(async (resolve, reject) => { | |
try { | |
const contract = await getEthereumContract() | |
tx = await contract.resolved(id) | |
await tx.wait() | |
await getJob(id) | |
resolve(tx) | |
} catch (err) { | |
reportError(err) | |
reject(err) | |
} | |
}) | |
} | |
const revoke = async (jId, id) => { | |
if (!ethereum) return alert('Please install Metamask') | |
return new Promise(async (resolve, reject) => { | |
try { | |
const contract = await getEthereumContract() | |
tx = await contract.revoke(jId, id) | |
await tx.wait() | |
await getJob(id) | |
resolve(tx) | |
} catch (err) { | |
reportError(err) | |
reject(err) | |
} | |
}) | |
} | |
const payout = async (id) => { | |
if (!ethereum) return alert('Please install Metamask') | |
return new Promise(async (resolve, reject) => { | |
try { | |
const contract = await getEthereumContract() | |
tx = await contract.payout(id) | |
await tx.wait() | |
await getMyJobs() | |
resolve(tx) | |
} catch (err) { | |
reportError(err) | |
reject(err) | |
} | |
}) | |
} | |
const bidStatus = async (id) => { | |
if (!ethereum) return alert('Please install Metamask') | |
try { | |
const contract = await getEthereumContract() | |
const status = await contract.bidStatus(id) | |
setGlobalState('status', status) | |
} catch (err) { | |
reportError(err) | |
} | |
} | |
const getBidders = async (id) => { | |
if (!ethereum) return alert('Please install Metamask') | |
try { | |
const contract = await getEthereumContract() | |
const bidders = await contract.getBidders(id) | |
setGlobalState('bidders', structuredBidder(bidders)) | |
} catch (err) { | |
reportError(err) | |
} | |
} | |
const getFreelancers = async (id) => { | |
if (!ethereum) return alert('Please install Metamask') | |
try { | |
const contract = await getEthereumContract() | |
const freelancers = await contract.getFreelancers(id) | |
setGlobalState('freelancers', structuredFreelancers(freelancers)) | |
} catch (err) { | |
reportError(err) | |
} | |
} | |
const getAcceptedFreelancer = async (id) => { | |
if (!ethereum) return alert('Please install Metamask') | |
try { | |
const contract = await getEthereumContract() | |
const freelancer = await contract.getAcceptedFreelancer(id) | |
setGlobalState('freelancer', structuredFreelancers([freelancer])[0]) | |
} catch (err) { | |
reportError(err) | |
} | |
} | |
const getJobs = async () => { | |
if (!ethereum) return alert('Please install Metamask') | |
try { | |
const contract = await getEthereumContract() | |
const jobs = await contract.getJobs() | |
setGlobalState('jobs', structuredJobs(jobs)) | |
} catch (err) { | |
reportError(err) | |
} | |
} | |
const getMyJobs = async () => { | |
if (!ethereum) return alert('Please install Metamask') | |
try { | |
const contract = await getEthereumContract() | |
const jobs = await contract.getMyJobs() | |
setGlobalState('myjobs', structuredJobs(jobs)) | |
} catch (err) { | |
reportError(err) | |
} | |
} | |
const getMyGigs = async () => { | |
if (!ethereum) return alert('Please install Metamask') | |
try { | |
const contract = await getEthereumContract() | |
const jobs = await contract.getAssignedJobs() | |
setGlobalState('mygigs', structuredJobs(jobs)) | |
} catch (err) { | |
reportError(err) | |
} | |
} | |
const getMyBidJobs = async () => { | |
if (!ethereum) return alert('Please install Metamask') | |
try { | |
const contract = await getEthereumContract() | |
const jobs = await contract.getJobsForBidder() | |
setGlobalState('mybidjobs', structuredJobs(jobs)) | |
} catch (err) { | |
reportError(err) | |
} | |
} | |
const getJob = async (id) => { | |
if (!ethereum) return alert('Please install Metamask') | |
try { | |
const contract = await getEthereumContract() | |
const job = await contract.getJob(id) | |
setGlobalState('job', structuredJobs([job])[0]) | |
} catch (err) { | |
reportError(err) | |
} | |
} | |
const loadData = async () => { | |
await getJobs() | |
await getMyJobs() | |
await getMyGigs() | |
await getMyBidJobs() | |
} | |
const structuredJobs = (jobs) => | |
jobs | |
.map((job) => ({ | |
id: job.id.toNumber(), | |
owner: job.owner.toLowerCase(), | |
freelancer: job.freelancer.toLowerCase(), | |
jobTitle: job.jobTitle, | |
description: job.description, | |
tags: job.tags.split(','), | |
prize: fromWei(job.prize), | |
paidOut: job.paidOut, | |
timestamp: job.timestamp, | |
listed: job.listed, | |
disputed: job.disputed, | |
bidders: job.bidders.map((address) => address.toLowerCase()), | |
})) | |
.sort((a, b) => b.timestamp - a.timestamp) | |
const structuredBidder = (bidders) => | |
bidders.map((bidder) => ({ | |
id: bidder.id.toNumber(), | |
jId: bidder.jId.toNumber(), | |
account: bidder.account.toLowerCase(), | |
})) | |
const structuredFreelancers = (freelancers) => | |
freelancers.map((freelancer) => ({ | |
id: freelancer.id.toNumber(), | |
jId: freelancer.jId.toNumber(), | |
account: freelancer.account.toLowerCase(), | |
bool: freelancer.isAssigned, | |
})) | |
export { | |
connectWallet, | |
isWalletConnected, | |
addJobListing, | |
updateJob, | |
deleteJob, | |
bidForJob, | |
acceptBid, | |
dispute, | |
resolved, | |
revoke, | |
payout, | |
bidStatus, | |
getBidders, | |
getFreelancers, | |
getAcceptedFreelancer, | |
getJobs, | |
getMyJobs, | |
getJob, | |
getMyBidJobs, | |
getMyGigs, | |
loadData, | |
} |
Here's an overview of what this script does:
- It imports necessary dependencies, including contract ABI (Application Binary Interface), contract addresses, and Ethereum-related libraries.
- Defines utility functions like
toWei
andfromWei
to convert between Ether and Wei. - Defines a function
getEthereumContract
to establish a connection to the Ethereum blockchain and retrieve a contract instance. -
isWalletConnected
checks if a user's Ethereum wallet (e.g., MetaMask) is connected and handles account changes and chain changes. - Functions like
connectWallet
,addJobListing
,updateJob
,deleteJob
,bidForJob
,acceptBid
,dispute
,resolved
,revoke
,payout
,bidStatus
,getBidders
,getFreelancers
,getAcceptedFreelancer
,getJobs
,getMyJobs
,getJob
,getMyBidJobs
,getMyGigs
, andloadData
are defined to interact with the smart contract for various actions such as creating jobs, bidding, accepting bids, and more. -
structuredJobs
,structuredBidder
, andstructuredFreelancers
functions format data retrieved from the blockchain into structured objects for easier management and display.
Overall, this script acts as an interface between the front-end application and the Ethereum blockchain, allowing users to perform actions and retrieve data related to jobs and bids on the blockchain. It integrates with a smart contract using the contract's ABI and address. See the script above.
Please ensure that you update the environment variables to look like this:
REACT_APP_COMETCHAT_APP_ID=****************
REACT_APP_COMETCHAT_AUTH_KEY=******************************
REACT_APP_COMETCHAT_REGION=**
REACT_APP_RPC_URL=http://127.0.0.1:8545
The Chat Service
This script is a JavaScript module that provides functionality for integrating and interacting with the CometChat Pro service within a web application. CometChat Pro is a chat and messaging platform that allows developers to add real-time chat features to their applications. Like with the previous service you will have to create another file called chat.jsx
within the src >> services
folder and paste the code below inside.
import { CometChat } from "@cometchat-pro/chat"; | |
import { getGlobalState } from "../store"; | |
const CONSTANTS = { | |
APP_ID: process.env.REACT_APP_COMET_CHAT_APP_ID, | |
REGION: process.env.REACT_APP_COMET_CHAT_REGION, | |
Auth_Key: process.env.REACT_APP_COMET_CHAT_AUTH_KEY, | |
}; | |
const initCometChat = async () => { | |
const appID = CONSTANTS.APP_ID; | |
const region = CONSTANTS.REGION; | |
const appSetting = new CometChat.AppSettingsBuilder() | |
.subscribePresenceForAllUsers() | |
.setRegion(region) | |
.build(); | |
await CometChat.init(appID, appSetting) | |
.then(() => console.log("Initialization completed successfully")) | |
.catch((error) => error); | |
}; | |
const loginWithCometChat = async () => { | |
const authKey = CONSTANTS.Auth_Key; | |
const UID = getGlobalState("connectedAccount"); | |
return new Promise(async (resolve, reject) => { | |
await CometChat.login(UID, authKey) | |
.then((user) => resolve(user)) | |
.catch((error) => reject(error)); | |
}); | |
}; | |
const signUpWithCometChat = async () => { | |
const authKey = CONSTANTS.Auth_Key; | |
const UID = getGlobalState("connectedAccount"); | |
const user = new CometChat.User(UID); | |
user.setName(UID); | |
return new Promise(async (resolve, reject) => { | |
await CometChat.createUser(user, authKey) | |
.then((user) => resolve(user)) | |
.catch((error) => reject(error)); | |
}); | |
}; | |
const logOutWithCometChat = async () => { | |
return new Promise(async (resolve, reject) => { | |
await CometChat.logout() | |
.then(() => resolve()) | |
.catch(() => reject()); | |
}); | |
}; | |
const isUserLoggedIn = async () => { | |
return new Promise(async (resolve, reject) => { | |
await CometChat.getLoggedinUser() | |
.then((user) => resolve(user)) | |
.catch((error) => reject(error)); | |
}); | |
}; | |
const getUser = async (UID) => { | |
return new Promise(async (resolve, reject) => { | |
await CometChat.getUser(UID) | |
.then((user) => resolve(user)) | |
.catch((error) => reject(error)); | |
}); | |
}; | |
const getMessages = async (UID) => { | |
const limit = 30; | |
const messagesRequest = new CometChat.MessagesRequestBuilder() | |
.setUID(UID) | |
.setLimit(limit) | |
.build(); | |
return new Promise(async (resolve, reject) => { | |
await messagesRequest | |
.fetchPrevious() | |
.then((messages) => resolve(messages.filter((msg) => msg.type == "text"))) | |
.catch((error) => reject(error)); | |
}); | |
}; | |
const sendMessage = async (receiverID, messageText) => { | |
const receiverType = CometChat.RECEIVER_TYPE.USER; | |
const textMessage = new CometChat.TextMessage( | |
receiverID, | |
messageText, | |
receiverType | |
); | |
return new Promise(async (resolve, reject) => { | |
await CometChat.sendMessage(textMessage) | |
.then((message) => resolve(message)) | |
.catch((error) => reject(error)); | |
}); | |
}; | |
const getConversations = async () => { | |
const limit = 30; | |
const conversationsRequest = new CometChat.ConversationsRequestBuilder() | |
.setLimit(limit) | |
.build(); | |
return new Promise(async (resolve, reject) => { | |
await conversationsRequest | |
.fetchNext() | |
.then((conversationList) => resolve(conversationList)) | |
.catch((error) => reject(error)); | |
}); | |
}; | |
const listenForMessage = async (listenerID) => { | |
return new Promise(async (resolve, reject) => { | |
CometChat.addMessageListener( | |
listenerID, | |
new CometChat.MessageListener({ | |
onTextMessageReceived: (message) => resolve(message), | |
}) | |
); | |
}); | |
}; | |
export { | |
initCometChat, | |
loginWithCometChat, | |
signUpWithCometChat, | |
logOutWithCometChat, | |
getMessages, | |
sendMessage, | |
getConversations, | |
isUserLoggedIn, | |
getUser, | |
listenForMessage, | |
}; |
Here is a breakdown of how this script works:
It imports the CometChat library (
CometChat
) and a function (getGlobalState
) from a custom store.It defines constants (
CONSTANTS
) for the CometChat application settings, including the App ID, region, and authentication key. These settings are typically stored in environment variables for security.The
initCometChat
function initializes CometChat with the provided application settings. It subscribes to user presence updates and sets the region. Initialization is done asynchronously, and any errors encountered during initialization are caught and logged.The
loginWithCometChat
function performs user login with CometChat. It takes the user's unique identifier (UID) and authentication key as parameters, attempts to log in, and returns a promise that resolves to the user object upon successful login or rejects with an error in case of failure.The
signUpWithCometChat
function creates a new user on CometChat. It also takes the user's UID and authentication key as parameters, sets the user's name, and returns a promise that resolves to the newly created user upon success or rejects with an error in case of failure.The
logOutWithCometChat
function logs the current user out of CometChat. It returns a promise that resolves upon successful logout or rejects in case of an error.The
isUserLoggedIn
function checks if a user is currently logged in to CometChat and returns a promise that resolves to the logged-in user or rejects with an error.The
getUser
function retrieves user information from CometChat based on the provided UID. It returns a promise that resolves to the user object or rejects with an error.The
getMessages
function fetches messages for a specific user (UID) from CometChat. It sets a message limit and returns a promise that resolves to an array of text messages or rejects with an error.The
sendMessage
function sends a text message to a specified receiver (receiverID) through CometChat. It returns a promise that resolves to the sent message or rejects with an error.The
getConversations
function retrieves a list of conversations from CometChat. It sets a conversation limit and returns a promise that resolves to the list of conversations or rejects with an error.The
listenForMessage
function sets up a listener to receive incoming text messages in real-time. It takes a listenerID (typically a user's UID) and returns a promise that resolves to the received message or rejects with an error.
Overall, this script enables the integration of CometChat Pro features into a web application, including user management, message retrieval, sending messages, and real-time message listening. It abstracts the CometChat functionality into convenient functions for use within the application.
Excellent! Now, let's work on the store
file, which serves as a state management library.
The Store File
This script provides a central state management system for a React application using the react-hooks-global-state
library. It also includes utility functions for text truncation and date formatting. You will have to create a file called index.jsx
within the src >> store
folder and paste the code below inside.
import { createGlobalState } from 'react-hooks-global-state' | |
const { setGlobalState, useGlobalState, getGlobalState } = createGlobalState({ | |
connectedAccount: '', | |
currentUser: null, | |
createModal: 'scale-0', | |
updateModal: 'scale-0', | |
deleteModal: 'scale-0', | |
payoutModal: 'scale-0', | |
jobs: [], | |
jobListing: null, | |
myjobs: [], | |
mygigs: [], | |
mybidjobs: [], | |
job: null, | |
bidders: [], | |
freelancers: [], | |
freelancer: null, | |
status: null, | |
recentConversations: [], | |
messages: [], | |
}) | |
const truncate = (text, startChars, endChars, maxLength) => { | |
if (text.length > maxLength) { | |
let start = text.substring(0, startChars) | |
let end = text.substring(text.length - endChars, text.length) | |
while (start.length + end.length < maxLength) { | |
start = start + '.' | |
} | |
return start + end | |
} | |
return text | |
} | |
const formatDate = (timestamp) => { | |
const date = new Date(timestamp) | |
const options = { | |
year: 'numeric', | |
month: 'long', | |
day: 'numeric', | |
} | |
return date.toLocaleDateString(undefined, options) | |
} | |
const timestampToDate = (timestamp) => { | |
const date = new Date(timestamp) | |
const options = { | |
year: 'numeric', | |
month: 'long', | |
day: 'numeric', | |
hour: 'numeric', | |
minute: 'numeric', | |
} | |
return date.toLocaleDateString('en-US', options) | |
} | |
export { | |
setGlobalState, | |
useGlobalState, | |
getGlobalState, | |
truncate, | |
formatDate, | |
timestampToDate, | |
} |
Here's an overview of what this script does:
It imports the
createGlobalState
function from thereact-hooks-global-state
library.-
It uses the
createGlobalState
function to create a global state container with initial state values. This global state container is then deconstructed into three functions:setGlobalState
,useGlobalState
, andgetGlobalState
.-
setGlobalState
: A function used to set or update global state values. It takes a key and a new value as arguments. -
useGlobalState
: A custom hook that allows components to access and subscribe to global state values. It returns the current value associated with a specified key and automatically updates the component when that value changes. -
getGlobalState
: A function that retrieves the current value associated with a specified key from the global state.
-
It defines a
truncate
function that takes a text string, start and end character counts, and a maximum length as arguments. This function truncates the text and adds ellipses (...) in the middle if the text exceeds the specified maximum length. It's used for shortening text for display.It defines a
formatDate
function that takes a timestamp as an argument and returns a formatted date string in the format "Month Day, Year." This function is used for formatting timestamps into more human-readable date strings.It defines a
timestampToDate
function that takes a timestamp as an argument and returns a formatted date string in the format "Month Day, Year Hour:Minute." This function is used for formatting timestamps into more detailed date and time strings.
Overall, this script serves as a global state management solution for a React application, allowing different components to share and access state data easily. It also provides utility functions for common text and date formatting operations.
The Index files
The index.jsx
file is the entry point for the application. It initializes the CometChat service, sets up dependencies, and renders the React application using the App
component within a BrowserRouter
. It creates a root element for rendering and sets up the necessary configurations for the application to start running.
To use this code, you will need to replace the code below inside of the index.jsx
and index.css
files in the src
folder of your project.
@import url('https://fonts.googleapis.com/css2?family=Open+Sans:wght@300;400;500;600;700&display=swap'); | |
* html { | |
padding: 0; | |
margin: 0; | |
box-sizing: border-box; | |
} | |
body { | |
margin: 0; | |
font-family: 'Open Sans', sans-serif; | |
-webkit-font-smoothing: antialiased; | |
-moz-osx-font-smoothing: grayscale; | |
} | |
@font-face { | |
font-family: "poppins"; | |
src: url("./font/Poppins-Regular.ttf"); | |
} | |
@font-face { | |
font-family: "marko"; | |
src: url("./font/MarkoOne-Regular.ttf"); | |
} | |
@font-face { | |
font-family: "risque"; | |
src: url("./font/Risque-Regular.ttf"); | |
} | |
@tailwind base; | |
@tailwind components; | |
@tailwind utilities; |
import React from 'react' | |
import ReactDOM from 'react-dom/client' | |
import { BrowserRouter } from 'react-router-dom' | |
import 'react-toastify/dist/ReactToastify.css' | |
import './index.css' | |
import App from './App' | |
import { initCometChat } from './services/chat' | |
const root = ReactDOM.createRoot(document.getElementById('root')) | |
initCometChat().then(()=> { | |
root.render( | |
<React.StrictMode> | |
<BrowserRouter> | |
<App /> | |
</BrowserRouter> | |
</React.StrictMode> | |
) | |
}) |
Now you are officially done with the build, just execute the following commands to have the application running on the browser.
- Terminal #1:
**yarn hardhat node**
- Terminal #2:
**yarn hardhat run scripts/deploy.js**
and then**yarn start**
Congratulations on building a Web3 Upwork clone with React, Solidity, and CometChat! 🚀
Your achievement is a testament to your prowess in combining these cutting-edge technologies to craft an innovative and engaging application. By harnessing React for the frontend, Solidity for your smart contracts, and integrating CometChat for real-time communication, you've showcased a versatile skill set in both blockchain development and interactive user experiences.
Your Web3 Upwork clone stands as a remarkable project, bringing together the power of blockchain, modern web development, and real-time communication to create a platform that empowers users and facilitates collaboration in a decentralized manner.
Keep up the great work, and continue exploring the endless possibilities of technology! Your journey in the world of Web3 development is bound to lead to even more exciting and impactful projects in the future.
For more web3 resources, check out this video that teaches how to create a decentralized app by building a web3 lottery dapp, I recommend that you it.
The video provides a hands-on tutorial on how to build a lottery dapp using NextJs, Tailwind CSS, and Solidity.
Conclusion
Congratulations on completing the journey of building a Web3 Upwork clone with React, Solidity, and CometChat. This comprehensive guide has empowered you to create a cutting-edge platform that combines the power of React for the frontend, Solidity for smart contracts, and CometChat for real-time communication. By undertaking this project, you've demonstrated your prowess in blockchain development and interactive user experiences.
This tutorial has unveiled the potential of web3 technology in transforming the gig economy, offering secure and transparent transactions while enhancing user engagement through dynamic real-time interactions. The smart contracts have been rigorously tested to ensure reliability, making your Upwork clone a robust and trustworthy platform for users.
As you conclude this project, you're not only equipped with technical skills but also with a vision of the future where decentralized applications redefine traditional industries. Your Upwork clone is a testament to the possibilities that emerge when innovative technologies converge.
To further enhance your knowledge and stay updated on the latest developments in blockchain and web3, consider subscribing to our YouTube channel and exploring our website for additional resources.
Best wishes on your journey of innovation and discovery in the world of web3 and decentralized applications!
About Author
I am a web3 developer and the founder of Dapp Mentors, a company that helps businesses and individuals build and launch decentralized applications. I have over 7 years of experience in the software industry, and I am passionate about using blockchain technology to create new and innovative applications. I run a YouTube channel called Dapp Mentors where I share tutorials and tips on web3 development, and I regularly post articles online about the latest trends in the blockchain space.
Stay connected with us, join communities on
Discord: Join
Twitter: Follow
LinkedIn: Connect
GitHub: Explore
Website: Visit
Top comments (0)