DEV Community

Cover image for How to Build an Answer-to-Earn Platform with React, Solidity, and CometChat
Gospel Darlington
Gospel Darlington

Posted on

5

How to Build an Answer-to-Earn Platform with React, Solidity, and CometChat

What you will be building, see the demo Sepolia Testnet and git repo.

Paying out the winner of the right answer

Live Chat with CometChat SDK

Introduction

If you're looking to create an innovative platform that motivates users to share their knowledge and skills, this tutorial on developing an Answer-to-Earn platform using React, Solidity, and CometChat could be just what you need.

This tutorial combines blockchain technology, real-time communication, and user-generated content to create an interactive platform that rewards users for their contributions and encourages engagement and cooperation.

Whether you're a seasoned developer or a beginner, this step-by-step guide will help you bring your vision to life and transform the way people share information. So why not begin building your own Answer-to-Earn platform today and revolutionize learning and information sharing?

By the way, Subscribe to my YouTube channel to learn how to build a Web3 app from scratch. I also offer a wide range of services, be it private tutorship, hourly consultancy, other development services, or web3 educational material production, you can book my services here.

Now, let’s jump into this tutorial.

Prerequisites

You will need the following tools installed to build along with me:

  • Nodejs (Important)
  • Ethers js
  • Hardhat
  • Yarn
  • Metamask
  • React
  • Tailwind CSS
  • CometChat SDK

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 <PROJECT_NAME>
cd <PROJECT_NAME>
Enter fullscreen mode Exit fullscreen mode
{
"name": "A2E",
"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.11",
"@nomiclabs/hardhat-ethers": "^2.1.0",
"@nomiclabs/hardhat-waffle": "^2.0.3",
"ethereum-waffle": "^3.4.4",
"ethers": "^5.6.9",
"hardhat": "^2.10.1",
"ipfs-http-client": "^57.0.3",
"moment": "^2.29.4",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"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.1",
"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"
]
}
}
view raw package.json hosted with ❤ by GitHub

Now, run **yarn install** on the terminal to have all the dependencies for this project installed.

Configuring CometChat SDK

Follow the steps below to configure the CometChat SDK; at the end, you must save these keys as an environment variable.

STEP 1:
Head to CometChat Dashboard and create an account.

Register a new CometChat account if you do not have one

STEP 2:
Log in to the CometChat dashboard, only after registering.

Log in to the CometChat Dashboard with your created account

STEP 3:
From the dashboard, add a new app called A2E.

Create a new CometChat app - Step 1

Create a new CometChat app - Step 2

STEP 4:
Select the app you just created from the list.

Select your created app

STEP 5:
From the Quick Start copy the APP_ID, REGION, and AUTH_KEY, to your .env file. See the image and code snippet.

Copy the the APP_ID, REGION, and AUTH_KEY

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=**
Enter fullscreen mode Exit fullscreen mode

The **.env** file should be created at the root of your project.

Configuring the Hardhat script

At the root of this project, open the hardhat.config.js file and replace its content with the following settings.

require("@nomiclabs/hardhat-waffle");
require('dotenv').config()
module.exports = {
defaultNetwork: "localhost",
networks: {
hardhat: {
},
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
}
}

The above script instructs hardhat on these three important rules.

  • Networks: This block contains the configurations for your choice of networks. On deployment, hardhat will require you to specify a network for shipping your smart contracts.

  • Solidity: This describes the version of the compiler to be used by hardhat for compiling your smart contract codes into bytecodes and abi.

  • Paths: This simply informs hardhat of the location of your smart contracts and also a place to dump the output of the compiler which is the ABI.

Configuring the Deployment Script

Navigate to the scripts folder and then to your deploy.js file and paste the code below into it. If you can't find a script folder, make one, create a deploy.js file, and paste the following code into it.

const hre = require('hardhat')
const fs = require('fs')
async function main() {
const Contract = await hre.ethers.getContractFactory('AnswerToEarn')
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
})
view raw deploy.js hosted with ❤ by GitHub

When run as a Hardhat deployment command, the above script will deploy your specified smart contract to the network of your choice.

Check out this video to learn how to properly set up a web3 project with ReactJs.


Watch the video

The Smart Contract File

Now that we've completed the initial configurations, let's create the smart contract for this build. Create a new folder called **contracts** in your project's **src** directory.

Create a new file called **A****nswer2Earn.sol** within this contracts folder; this file will contain all of the logic that governs the smart contract.

Copy, paste, and save the following codes into the **A****nswer2Earn.sol** file. See the complete code below.

//SPDX-License-Identifier:MIT
pragma solidity >= 0.7.0 < 0.9.0;
contract AnswerToEarn {
struct QuestionStruct {
uint id;
string questionTitle;
string questionDescription;
address owner;
address winner;
bool paidout;
bool refunded;
bool deleted;
uint updated;
uint created;
uint answers;
string tags;
uint256 prize;
}
struct CommentStruct {
uint id;
uint questionId;
string commentText;
address owner;
bool deleted;
uint created;
uint updated;
}
event Action (
uint id,
string actionType,
address indexed executor,
uint256 timestamp
);
modifier ownerOnly() {
require(msg.sender == owner,"Reserved for owner only");
_;
}
address public owner;
uint256 public platformCharge = 5;
uint public totalQuestion;
mapping(uint => bool) questionExists;
mapping(uint => CommentStruct[]) commentsOf;
QuestionStruct[] public questions;
constructor() {
owner = msg.sender;
}
function addQuestion(
string memory questionTitle,
string memory questionDescription,
string memory tags
) public payable {
require(bytes(questionTitle).length > 0,"Fill up empty fields");
require(bytes(questionDescription).length > 0,"Fill up empty fields");
require(bytes(tags).length > 0,"Fill up empty fields");
require(msg.value > 0 ether, "Insufficient fund");
QuestionStruct memory question;
questionExists[questions.length] = true;
totalQuestion++;
question.id = questions.length;
question.questionTitle = questionTitle;
question.questionDescription = questionDescription;
question.tags = tags;
question.prize = msg.value;
question.owner = msg.sender;
question.updated = block.timestamp;
question.created = block.timestamp;
questions.push(question);
emit Action (
questions.length,
"Question created",
msg.sender,
block.timestamp
);
}
function updateQuestion(
uint id,
string memory questionTitle,
string memory questionDescription,
string memory tags
) public returns(bool) {
require(questionExists[id] == true,"Question does not exist or have been removed!");
require(bytes(questionTitle).length > 0,"Fill up empty fields");
require(bytes(tags).length > 0,"Fill up empty fields");
require(bytes(questionDescription).length > 0,"Fill up empty fields");
require(msg.sender == questions[id].owner , "Invalid action!");
questions[id].questionTitle = questionTitle;
questions[id].tags = tags;
questions[id].questionDescription = questionDescription;
questions[id].updated = block.timestamp;
emit Action (
id,
"Question updated",
msg.sender,
block.timestamp
);
return true;
}
function deleteQuestion(uint id) public returns (bool) {
require(msg.sender == questions[id].owner , "Invalid action!");
require(questionExists[id] == true,"Question does not exist or have been removed!");
totalQuestion--;
questions[id].deleted = true;
questionExists[id] = false;
emit Action (
id,
"Question deleted",
msg.sender,
block.timestamp
);
return true;
}
function showQuestions() public view returns(QuestionStruct[] memory propQuestion) {
uint count = 0;
for(uint i = 0; i < questions.length; i++) {
if(!questions[i].deleted) {
count++;
}
}
propQuestion = new QuestionStruct[](count);
uint index = 0;
for(uint i = 0; i < questions.length; i++) {
if(!questions[i].deleted) {
propQuestion[index] = questions[i];
index++;
}
}
}
function showQuestion(uint id) public view returns(QuestionStruct memory ) {
return questions[id];
}
function isQuestionOwner(uint id) public view returns (bool) {
return msg.sender == questions[id].owner;
}
function addComment(
uint questionId,
string memory _commentText
) public returns (bool) {
require(bytes(_commentText).length > 1,"Required field!");
require(questionExists[questionId] == true,"Question does not exist or have been removed!");
CommentStruct memory comment;
comment.id = commentsOf[questionId].length;
comment.questionId = questionId;
comment.commentText = _commentText;
comment.owner = msg.sender;
comment.created = block.timestamp;
comment.updated = block.timestamp;
questions[questionId].answers++;
commentsOf[questionId].push(comment);
emit Action (
comment.id,
"comment created",
msg.sender,
block.timestamp
);
return true;
}
function updateComment(
uint questionId,
uint id,
string memory _commentText
) public returns (bool) {
require(questionExists[questionId], "Question not found");
require(msg.sender == commentsOf[questionId][id].owner, "Unauthorized entity");
require(bytes(_commentText).length > 0,"Required field!");
commentsOf[questionId][id].commentText = _commentText;
commentsOf[questionId][id].updated = block.timestamp;
emit Action (
id,
"comment updated",
msg.sender,
block.timestamp
);
return true;
}
function deleteComment(uint questionId, uint id) public returns (bool) {
require(questionExists[questionId], "Question not found");
require(msg.sender == commentsOf[questionId][id].owner, "Unauthorized entity");
commentsOf[questionId][id].deleted = true;
commentsOf[questionId][id].updated = block.timestamp;
questions[questionId].answers--;
emit Action (
id,
"comment deleted",
msg.sender,
block.timestamp
);
return true;
}
function getComments(uint questionId) public view returns(CommentStruct[] memory) {
return commentsOf[questionId];
}
function getcomment(uint questionId,uint id) public view returns(CommentStruct memory) {
return commentsOf[questionId][id];
}
function isCommentOwner(uint id,uint questionId) public view returns (bool) {
return msg.sender == commentsOf[questionId][id].owner;
}
function payBestComment(uint questionId, uint id) public {
require(questionExists[questionId], "Question not found");
require(!questions[questionId].paidout, "Question already paid out");
require(msg.sender == questions[questionId].owner, "Unauthorized entity");
uint256 reward = questions[questionId].prize;
uint256 tax = reward * platformCharge / 100;
address winner = commentsOf[questionId][id].owner;
questions[questionId].paidout = true;
questions[questionId].winner = winner;
payTo(winner, reward - tax);
payTo(owner, tax);
}
function refund(uint questionId) public {
require(questionExists[questionId], "Question not found");
require(!questions[questionId].paidout, "Question already paid out");
require(!questions[questionId].refunded, "Owner already refunded");
require(msg.sender == questions[questionId].owner, "Unauthorized entity");
uint256 reward = questions[questionId].prize;
questions[questionId].refunded = true;
payTo(questions[questionId].owner, reward);
}
function payTo(address to, uint amount) internal {
(bool success, ) = payable(to).call{value: amount}("");
require(success);
}
function changeFee(uint256 fee) public {
require(msg.sender == owner, "Unauthorized entity");
require(fee > 0 && fee <= 100, "Fee must be between 1 - 100");
platformCharge = fee;
}
}
view raw a2e.sol hosted with ❤ by GitHub

I have a book to help your master the web3 language (Solidity), grab your copy here.

Capturing Smart Contract Development

Now, let's go over some of the details of what's going on in the smart contract above. We have the following items:

Structs

  • QuestionStruct: This contains necessary information about every question asked on the platform.
  • CommentStruct: This carries information about every comment on our platform.

State Variables

  • Owner: This keeps track of the deployer of the contract.
  • platformCharge: This holds the platform’s percentage fee on every payment made on the platform.
  • totalQuestion: This stores the number of questions that have not been deleted from the platform.

Mappings

  • questionExists: This checks if a question exists or has not been deleted from the platform.
  • commentsOf: This holds the total comments of a particular question.

Constructor
This is used to initialize the state of the smart contract variables and other essential operations. In this example, we assigned the deployer of the smart contract as the owner.

Events and Modifiers

  • Action: This logs out the information of every action carried out on the platform.
  • ownerOnly: This prevents every other user except the owner from accessing some functions in the platform.

Question Functions

  • addQuestion: This function takes a bounty question from a user and makes it available for others to compete for the best answer.
  • updateQuestion: This function is used to edit the question supplied by the owner of the question.
  • deleteQuestion: This function is used to delete the question supplied by the owner of the question.
  • showQuestions: This function is responsible for displaying all questions in the platform. The question must already be existing and is not deleted.
  • showQuestion: This function returns a single question by its unique Id.
  • isQuestionOwner: This function returns true/false if a user owns a question.

Comment Functions

  • addComment: This function allows users to supply an answer to a particular question in hopes of winning the prize on the question.
  • updateComment: This function is used to edit an answer provided that the caller is the owner of the answer/comment.
  • deleteComment: This function is used to delete a comment provided that the caller is the owner of the answer/comment.
  • getComments: This function returns all comments/answers to a particular question.
  • getComment: This returns a single comment for a particular question on the platform.
  • isCommentOwner: This function returns true/false if a user owns a comment.

Funds functions

  • payBestComment: This function is used by a question owner to pay the respondent of the most preferred answer.
  • refund: This function is used for requesting a refund by a question owner provided that answers have not been submitted yet.
  • payTo: This function is used internally to make payments to the address provided.
  • changeFee: This function is used to change the platform’s fee.

With all the above functions understood, copy them into a file named **Answer2Earn.sol** in the contracts folder within the src directory.

Next, run the commands below to deploy the smart contract into the network.

yarn hardhat node # Terminal #1
yarn hardhat run scripts/deploy.js --network localhost # Terminal #2
Enter fullscreen mode Exit fullscreen mode

Activities of Deployment on the Terminal

If you need further help configuring Hardhat or deploying your Fullstack DApp, watch this video.


Watch the video

Developing the Frontend

Now that we have our smart contract on the network and all of our artifacts (bytecodes and ABI) generated, let's get the front end ready with React.

Components

In the src directory, create a new folder called **components** to house all of the React components for this project.

Header Component

The header  component

The Header component includes the platform logo, a search bar, and a button with the connected account address. See the code below.

import { FaSearch } from "react-icons/fa";
import { Link } from "react-router-dom";
import { useGlobalState, truncate } from "../store";
import { connectWallet } from "../services/blockchain";
const Header = () => {
const [connectedAccount] = useGlobalState("connectedAccount");
return (
<div>
<header className="flex justify-between items-center h-20 shadow-md p-5 fixed z-auto top-0 right-0 left-0 w-full bg-black text-gray-200 border-t-4 border-orange-500">
<div className="flex justify-between items-center">
<Link
to={"/"}
className=" text-2xl font-bold hover:text-orange-500 cursor-Linkointer"
>
A<b className="text-orange-500 hover:text-white">2</b>E
</Link>
</div>
<div className="sm:flex items-center justify-between px-2 py-1 bg-slate-600 rounded-lg hidden sm:w-3/5 md:w-1/2">
<input
className="w-full border-0 outline-0 px-6 relative text-md text-white bg-transparent hover:ouline-none focus:outline-none focus:ring-0"
type="text"
id="search-box"
placeholder="search here..."
required
/>
<FaSearch className="absolute hover:text-orange-500 cursor-pointer" />
</div>
{connectedAccount ? (
<button className="bg-orange-500 p-2 rounded-lg text-white cursor-pointer hover:bg-orange-600 hover:text-slate-200 md:text-xs">
{truncate(connectedAccount, 4, 4, 11)}
</button>
) : (
<button
className="bg-orange-500 p-2 rounded-lg text-white cursor-pointer hover:bg-orange-600 hover:text-slate-200 md:text-xs"
onClick={connectWallet}
>
Connect wallet
</button>
)}
</header>
<div className="h-20"></div>
</div>
);
};
export default Header;
view raw Header.jsx hosted with ❤ by GitHub

QuestionTitle Component

This component includes a header, a button for asking questions, and information on all other questions. Here is the code:

import { setGlobalState } from '../store'
const QuestionTitle = ({ title, caption }) => {
return (
<div
className="w-full flex justify-between items-center space-x-2
border-b border-gray-300 border-b-gray-300 pb-4"
>
<div className='flex flex-col flex-wrap w-5/6'>
<h1 className="sm:text-3xl md:2xl text-xl font-medium">{title}</h1>
<p className="text-md mt-2">{caption}</p>
</div>
<button
type="button"
className="p-2 px-4 py-3 bg-blue-500 text-white font-medium rounded-md
text-xs leading-tight capitalize hover:bg-blue-600 focus:outline-none
focus:ring-0 transition duration-150 ease-in-out"
onClick={() => setGlobalState('addModal', 'scale-100')}
>
Ask question
</button>
</div>
)
}
export default QuestionTitle

AddQuestion Component

Asking Questions

This is a modal that lets users create questions on the platform. To successfully submit a question, users will need to provide a title, bounty prize, tag, and question description. Here are the relevant codes:

import { FaTimes } from 'react-icons/fa'
import { setGlobalState, useGlobalState } from '../store'
import { createQuestion, getQuestions } from '../services/blockchain'
import { toast } from 'react-toastify'
import { useState } from 'react'
const AddQuestion = () => {
const [addModal] = useGlobalState('addModal')
const [question, setQuestion] = useState('')
const [title, setTitle] = useState('')
const [tags, setTags] = useState('')
const [prize, setPrize] = useState('')
const handleSubmit = async (e) => {
e.preventDefault()
if (title == '' || question == '' || tags == '' || prize == '') return
await toast.promise(
new Promise(async (resolve, reject) => {
await createQuestion({ title, question, tags, prize })
.then(async () => {
setGlobalState('addModal', 'scale-0')
resetForm()
resolve()
await getQuestions()
})
.catch(() => reject())
}),
{
pending: 'Approve transaction...',
success: 'question posted successfully 👌',
error: 'Encountered error 🤯',
},
)
}
const resetForm = () => {
setQuestion('')
setTitle('')
setTags('')
setPrize('')
}
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 ${addModal}`}
>
<div className="bg-white shadow-lg shadow-slate-900 rounded-xl w-11/12 md:w-2/5 h-7/12 p-6">
<form onSubmit={handleSubmit} className="flex flex-col">
<div className="flex flex-row justify-between items-center">
<p className="font-semibold">Add Question</p>
<button
type="button"
className="border-0 bg-transparent focus:outline-none"
onClick={() => setGlobalState('addModal', 'scale-0')}
>
<FaTimes className="text-gray-400 hover:text-blue-400" />
</button>
</div>
<div className="flex flex-col justify-center items-center rounded-xl mt-5 mb-5">
<div
className="flex justify-center items-center rounded-full overflow-hidden
h-10 w-40 shadow-md shadow-slate-300 p-4"
>
<p className="text-lg font-bold text-slate-700">
{' '}
A<b className="text-orange-500">2</b>E
</p>
</div>
<p className="p-2">Add your question below</p>
</div>
<div
className="flex flex-row justify-between items-center
bg-gray-300 rounded-xl mt-5 p-2"
>
<input
className="block w-full text-sm text-slate-500 bg-transparent
border-0 focus:outline-none focus:ring-0"
type="text"
name="title"
placeholder="Question Title"
value={title}
onChange={(e) => setTitle(e.target.value)}
required
/>
</div>
<div className="flex flex-row justify-between items-center bg-gray-300 rounded-xl mt-5 p-2">
<input
className="block w-full text-sm text-slate-500 bg-transparent
border-0 focus:outline-none focus:ring-0"
type="number"
min={0.0001}
step={0.0001}
name="prize"
placeholder="ETH e.g 1.3"
value={prize}
onChange={(e) => setPrize(e.target.value)}
required
/>
</div>
<div className="flex flex-row justify-between items-center bg-gray-300 rounded-xl mt-5 p-2">
<input
className="block w-full text-sm text-slate-500 bg-transparent
border-0 focus:outline-none focus:ring-0"
type="text"
name="title"
placeholder="separate tags with commas, eg. php,css,html"
value={tags}
onChange={(e) => setTags(e.target.value)}
required
/>
</div>
<div className="flex flex-row justify-between items-center bg-gray-300 rounded-xl mt-5 p-2">
<textarea
className="block w-full text-sm resize-none text-slate-500 bg-transparent
border-0 focus:outline-none focus:ring-0 h-20"
type="text"
name="question"
placeholder="Drop your question here."
value={question}
onChange={(e) => setQuestion(e.target.value)}
required
></textarea>
</div>
<button
type="submit"
className="flex flex-row justify-center items-center w-full text-white text-md
bg-blue-400 py-2 px-5 rounded-full drop-shadow-xl border border-transparent
hover:bg-transparent hover:text-blue-400 hover:border hover:border-blue-400
focus:outline-none focus:ring mt-5"
>
Submit
</button>
</form>
</div>
</div>
)
}
export default AddQuestion
view raw AddQuestion.jsx hosted with ❤ by GitHub

QuestionSingle Component

Single Question Component

This component includes question details such as the question owner and prize.

import { Link } from 'react-router-dom'
import Identicon from 'react-identicons'
import { truncate } from '../store'
import { FaEthereum, FaPenAlt, FaTrashAlt } from 'react-icons/fa'
import { setGlobalState } from '../store'
const QuestionSingle = ({ question, titled, editable }) => {
const handleEdit = (question) => {
setGlobalState('singleQuestion', question)
setGlobalState('updateModal', 'scale-100')
}
const handleDelete = (question) => {
setGlobalState('singleQuestion', question)
setGlobalState('deleteQuestionModal', 'scale-100')
}
return (
<div className="text-start mb-5 w-full">
{titled ? (
<Link to={`/question/${question.id}`}>
<h1 className="text-xl font-medium mb-2">{question.title}</h1>
</Link>
) : null}
<p className="text-sm">{question.description}</p>
<div className="flex space-x-3 my-3">
{editable ? (
<>
<FaPenAlt
className="text-xs text-blue-500 cursor-pointer"
onClick={() => handleEdit(question)}
/>
<FaTrashAlt
className="text-xs text-red-500 cursor-pointer"
onClick={() => handleDelete(question)}
/>
</>
) : null}
</div>
<div className="flex justify-between items-center flex-wrap my-4 w-full">
<div className="flex space-x-2 justify-center">
{question.tags.split(',').map((tag, index) => (
<span
key={index}
className="px-2 py-1 rounded bg-blue-100 text-blue-400
font-medium text-xs flex align-center w-max cursor-pointer
active:bg-blue-300 transition duration-300 ease"
>
{tag}
</span>
))}
{question.paidout ? (
<button
className="flex justify-center items-center px-2 py-1 rounded border-green-500
font-medium text-xs align-center w-max border
transition duration-300 ease space-x-1 text-green-500"
>
<FaEthereum className="text-xs cursor-pointer" />
<span>{question.prize} Paid</span>
</button>
) : (
<button
className="flex justify-center items-center px-2 py-1 rounded border-orange-500
font-medium text-xs align-center w-max border
transition duration-300 ease space-x-1 text-orange-500"
>
<FaEthereum className="text-xs cursor-pointer" />
<span>{question.prize} Prize</span>
</button>
)}
</div>
<div className="flex justify-center items-center space-x-2">
<Identicon
string={question.owner}
size={20}
className="rounded-full"
/>
<p className="text-sm font-semibold">
{truncate(question.owner, 4, 4, 11)}
</p>
</div>
</div>
</div>
)
}
export default QuestionSingle

QuestionComments Component

Question Respondent View

Question Owner View

This code shows the user's comments or answers to the question owner.

import { FaEthereum, FaPenAlt, FaTrashAlt } from 'react-icons/fa'
import Identicon from 'react-identicons'
import { setGlobalState, useGlobalState, truncate } from '../store'
import { getComments, getQuestion, payWinner } from '../services/blockchain.jsx'
import { useState, useEffect } from 'react'
import { useParams } from 'react-router-dom'
import { toast } from 'react-toastify'
const QuestionComments = () => {
const [comments] = useGlobalState('comments')
const [question] = useGlobalState('question')
const [loaded, setLoaded] = useState(false)
const { id } = useParams()
useEffect(async () => {
await getComments(id).then(() => setLoaded(true))
}, [])
return loaded ? (
<div className="my-4">
{comments.map((comment, i) =>
!comment.deleted ? (
<Comment comment={comment} question={question} key={i} />
) : null,
)}
</div>
) : null
}
const Comment = ({ comment, question }) => {
const [connectedAccount] = useGlobalState('connectedAccount')
const handleEdit = () => {
setGlobalState('comment', comment)
setGlobalState('updateCommentModal', 'scale-100')
}
const handleDelete = () => {
setGlobalState('comment', comment)
setGlobalState('deleteCommentModal', 'scale-100')
}
const handlePayment = async () => {
await toast.promise(
new Promise(async (resolve, reject) => {
await payWinner(question.id, comment.id)
.then(async () => {
await getComments(question.id)
await getQuestion(question.id)
resolve()
})
.catch(() => reject())
}),
{
pending: 'Approve transaction...',
success: 'Winner payed out 👌',
error: 'Encountered error 🤯',
},
)
}
const formatTimestamp = (timestamp) => {
const date = new Date(timestamp)
const options = { day: 'numeric', month: 'short', year: 'numeric' }
return date.toLocaleDateString('en-US', options)
}
return (
<div className="border-b border-b-gray-300 py-2 flex flex-col justify-center items-start space-y-2">
<h2 className="text-xs">{comment.commentText}</h2>
<div className="flex justify-between items-center w-full">
<div className="flex justify-start items-center space-x-2">
<p className="text-slate-500 text-sm font-lg">
{formatTimestamp(comment.createdAt)}
</p>
{connectedAccount == comment.owner ? (
<>
<button
className="flex justify-center items-center px-2 py-1 rounded
border-gray-500 border text-gray-500 space-x-1
font-medium text-xs align-center w-max cursor-pointer
transition duration-300 ease"
onClick={handleEdit}
>
<FaPenAlt className="text-xs cursor-pointer" />
<span>Edit</span>
</button>
<button
className="flex justify-center items-center px-2 py-1 rounded
border-red-500 border text-red-500 space-x-1
font-medium text-xs align-center w-max cursor-pointer
transition duration-300 ease"
onClick={handleDelete}
>
<FaTrashAlt className="text-xs cursor-pointer" />
<span>Delete</span>
</button>
</>
) : null}
{connectedAccount == question.owner && !question.paidout ? (
<button
className="flex justify-center items-center px-2 py-1 rounded border
border-orange-500 text-orange-500
font-medium text-xs align-center w-max cursor-pointer
transition duration-300 ease space-x-1"
onClick={handlePayment}
>
<FaEthereum className="text-xs cursor-pointer" />
<span>Pay Now</span>
</button>
) : null}
{question.paidout && comment.owner == question.winner ? (
<span
className="flex justify-center items-center px-2 py-1 rounded border
border-orange-500 text-orange-500
font-medium text-xs align-center w-max
transition duration-300 ease space-x-1"
>
<FaEthereum className="text-xs cursor-pointer" />
<span>Winner</span>
</span>
) : null}
</div>
<div className="flex justify-start items-center space-x-2">
<Identicon
string={comment.owner}
size={20}
className="rounded-full"
/>
<p className="text-sm font-semibold">
{truncate(comment.owner, 4, 4, 11)}
</p>
</div>
</div>
</div>
)
}
export default QuestionComments

AddComment Component

Adding Comments

This component allows users to answer a specific question through an interface. See the code below:

import { useState } from "react";
import { FaTimes } from "react-icons/fa";
import { setGlobalState, useGlobalState } from "../store";
import { toast } from "react-toastify";
import { createComment, getComments } from "../services/blockchain.jsx";
import { useParams } from "react-router-dom";
const AddComment = () => {
const [addComment] = useGlobalState("addComment");
const [commentText, setComment] = useState("");
const { id } = useParams();
const handleSubmit = async (e) => {
e.preventDefault();
if (commentText == "") return;
let questionId = id;
await toast.promise(
new Promise(async (resolve, reject) => {
await createComment({ questionId, commentText })
.then(async () => {
setGlobalState("addComment", "scale-0");
setComment("");
resolve();
getComments(questionId);
})
.catch(() => reject());
}),
{
pending: "Approve transaction...",
success: "comment posted 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 ${addComment}`}
>
<div className="bg-white shadow-lg shadow-slate-900 rounded-xl w-11/12 md:w-2/5 h-7/12 p-6">
<form onSubmit={handleSubmit} className="flex flex-col">
<div className="flex flex-row justify-between items-center">
<p className="font-semibold">Add Comment</p>
<button
type="button"
className="border-0 bg-transparent focus:outline-none"
onClick={() => setGlobalState("addComment", "scale-0")}
>
<FaTimes className="text-gray-400 hover:text-blue-400" />
</button>
</div>
<div className="flex flex-col justify-center items-center rounded-xl mt-5 mb-5">
<div className="flex justify-center items-center rounded-full overflow-hidden h-10 w-40 shadow-md shadow-slate-300 p-4">
<p className="text-lg font-bold text-slate-700">
{" "}
A<b className="text-orange-500">2</b>E
</p>
</div>
<p className="p-2">Add your comment below</p>
</div>
<div className="flex flex-row justify-between items-center bg-gray-300 rounded-xl mt-5 p-2">
<textarea
className="block w-full text-sm resize-none text-slate-500
bg-transparent border-0 focus:outline-none focus:ring-0 h-20"
type="text"
name="comment"
placeholder="Comment"
value={commentText}
onChange={(e) => setComment(e.target.value)}
required
></textarea>
</div>
<button
type="submit"
className="flex flex-row justify-center items-center w-full text-white text-md bg-blue-400 py-2 px-5 rounded-full drop-shadow-xl border border-transparent hover:bg-transparent hover:text-blue-400 hover:border hover:border-blue-400 focus:outline-none focus:ring mt-5"
>
Submit
</button>
</form>
</div>
</div>
);
};
export default AddComment;
view raw AddComment.jsx hosted with ❤ by GitHub

UpdateQuestion Component

This component allows the question owner to make edits to their question. It is a modal component that applies adjustments. Here's the code:

import { FaTimes } from "react-icons/fa";
import { setGlobalState, useGlobalState } from "../store";
import { editQuestion, getQuestions } from "../services/blockchain";
import { toast } from "react-toastify";
import { useState, useEffect } from "react";
const UpdateQuestion = () => {
const [singleQuestion] = useGlobalState("singleQuestion");
const [updateModal] = useGlobalState("updateModal");
const [question, setQuestion] = useState("");
const [title, setTitle] = useState("");
const [tags, setTags] = useState("");
useEffect(() => {
if (singleQuestion) {
setQuestion(singleQuestion.description);
setTitle(singleQuestion.title);
setTags(singleQuestion.tags);
}
}, [singleQuestion]);
const resetForm = () => {
setQuestion("");
setTitle("");
setTags("");
};
const handleClose = () => {
setGlobalState("singleQuestion", null);
setGlobalState("updateModal", "scale-0");
resetForm();
};
const handleSubmit = async (e) => {
e.preventDefault();
if (question == "" || title == "" || tags == "") return;
const params = {
id: singleQuestion.id,
title,
question,
tags,
};
await toast.promise(
new Promise(async (resolve, reject) => {
await editQuestion(params)
.then(async () => {
setGlobalState("updateModal", "scale-0");
await getQuestions();
handleClose();
resolve();
})
.catch(() => reject());
}),
{
pending: "Approve transaction...",
success: "question 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 shadow-lg shadow-slate-900 rounded-xl w-11/12 md:w-2/5 h-7/12 p-6">
<form onSubmit={handleSubmit} className="flex flex-col">
<div className="flex flex-row justify-between items-center">
<p className="font-semibold">Edit Question</p>
<button
type="button"
className="border-0 bg-transparent focus:outline-none"
onClick={handleClose}
>
<FaTimes className="text-gray-400 hover:text-blue-400" />
</button>
</div>
<div className="flex flex-col justify-center items-center rounded-xl mt-5 mb-5">
<div className="flex justify-center items-center rounded-full overflow-hidden h-10 w-40 shadow-md shadow-slate-300 p-4">
<p className="text-lg font-bold text-slate-700">
{" "}
A<b className="text-orange-500">2</b>E
</p>
</div>
<p className="p-2">Edit your question below</p>
</div>
<div className="flex flex-row justify-between items-center bg-gray-300 rounded-xl mt-5 p-2">
<input
className="block w-full text-sm text-slate-500 bg-transparent border-0 focus:outline-none focus:ring-0"
type="text"
name="title"
placeholder="Question Title"
value={title}
onChange={(e) => setTitle(e.target.value)}
required
/>
</div>
<div className="flex flex-row justify-between items-center bg-gray-300 rounded-xl mt-5 p-2">
<input
className="block w-full text-sm text-slate-500 bg-transparent border-0 focus:outline-none focus:ring-0"
type="text"
name="title"
placeholder="separate tags with commas, eg. php,css,html"
value={tags}
onChange={(e) => setTags(e.target.value)}
required
/>
</div>
<div className="flex flex-row justify-between items-center bg-gray-300 rounded-xl mt-5 p-2">
<textarea
className="block w-full text-sm resize-none text-slate-500 bg-transparent border-0 focus:outline-none focus:ring-0 h-20"
type="text"
name="question"
placeholder="Drop your question here."
value={question}
onChange={(e) => setQuestion(e.target.value)}
required
></textarea>
</div>
<button
type="submit"
className="flex flex-row justify-center items-center w-full text-white text-md bg-blue-400 py-2 px-5 rounded-full drop-shadow-xl border border-transparent hover:bg-transparent hover:text-blue-400 hover:border hover:border-blue-400 focus:outline-none focus:ring mt-5"
>
Submit
</button>
</form>
</div>
</div>
);
};
export default UpdateQuestion;

UpdateComment Component

This code allows a user to edit comments when answering a question, but ownership will still be checked.

import { FaGithub, FaTimes } from 'react-icons/fa'
import { setGlobalState, useGlobalState } from '../store'
import { useEffect, useState } from 'react'
import { editComment, getComments } from '../services/blockchain.jsx'
import { toast } from 'react-toastify'
const UpdateComment = () => {
const [updateCommentModal] = useGlobalState('updateCommentModal')
const [comment] = useGlobalState('comment')
const [commentText, setCommentText] = useState('')
const onClose = () => {
setGlobalState('updateCommentModal', 'scale-0')
setCommentText('')
setGlobalState('comment', null)
}
useEffect(() => {
if (comment) {
setCommentText(comment.commentText)
}
}, [comment])
const handleSubmit = async (e) => {
e.preventDefault()
if (commentText == '') return
const params = {
questionId: comment.questionId,
commentId: comment.id,
commentText,
}
await toast.promise(
new Promise(async (resolve, reject) => {
await editComment(params)
.then(async () => {
setGlobalState('updateCommentModal', 'scale-0')
getComments(comment.questionId)
setCommentText('')
onClose()
resolve()
})
.catch(() => reject())
}),
{
pending: 'Approve transaction...',
success: 'comment 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 ${updateCommentModal}`}
>
<div className="bg-white shadow-lg shadow-slate-900 rounded-xl w-11/12 md:w-2/5 h-7/12 p-6">
<form onSubmit={handleSubmit} className="flex flex-col">
<div className="flex flex-row justify-between items-center">
<p className="font-semibold">Update Comment</p>
<button
type="button"
className="border-0 bg-transparent focus:outline-none"
onClick={onClose}
>
<FaTimes className="text-gray-400" />
</button>
</div>
<div className="flex flex-col justify-center items-center rounded-xl mt-5 mb-5">
<div className="flex justify-center items-center rounded-full overflow-hidden h-10 w-40 shadow-md shadow-slate-300 p-4">
<p className="text-lg font-bold text-slate-700">
{' '}
A<b className="text-orange-500">2</b>E
</p>
</div>
<p className="p-2">Update your comment below</p>
</div>
<div className="flex flex-row justify-between items-center bg-gray-300 rounded-xl mt-5 p-2">
<textarea
className="block w-full text-sm resize-none text-slate-500 bg-transparent border-0 focus:outline-none focus:ring-0 h-20"
type="text"
name="description"
placeholder="Description"
value={commentText}
onChange={(e) => setCommentText(e.target.value)}
required
></textarea>
</div>
<button
type="submit"
className="flex flex-row justify-center items-center w-full text-white text-md bg-blue-400 py-2 px-5 rounded-full drop-shadow-xl border border-transparent hover:bg-transparent hover:text-blue-400 hover:border hover:border-blue-400 focus:outline-none focus:ring mt-5"
>
Submit
</button>
</form>
</div>
</div>
)
}
export default UpdateComment

DeleteQuestion Component

This component asks for confirmation from the question owner before deleting a question, and it is designed as a modal component. Please refer to the code below.

import { useState, useEffect } from "react";
import { deleteQuestion, getQuestions } from "../services/blockchain";
import { setGlobalState, useGlobalState } from "../store";
import { FaTimes } from "react-icons/fa";
import { toast } from "react-toastify";
import { RiErrorWarningFill } from "react-icons/ri";
import { useNavigate } from "react-router-dom";
const DeleteQuestion = () => {
const [singleQuestion] = useGlobalState("singleQuestion");
const [deleteQuestionModal] = useGlobalState("deleteQuestionModal");
const navigate = useNavigate();
const handleClose = () => {
setGlobalState("singleQuestion", null);
setGlobalState("deleteQuestionModal", "scale-0");
};
const handleSubmit = async (e) => {
e.preventDefault();
await toast.promise(
new Promise(async (resolve, reject) => {
await deleteQuestion(singleQuestion.id)
.then(async () => {
getQuestions();
handleClose();
resolve();
navigate("/");
})
.catch(() => reject());
}),
{
pending: "Approve transaction...",
success: "question 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 ${deleteQuestionModal}`}
>
<div className="bg-white shadow-lg shadow-slate-900 rounded-xl w-11/12 md:w-2/5 h-7/12 p-6">
<form onSubmit={handleSubmit} className="flex flex-col">
<div className="flex flex-row justify-between items-center">
<p className="font-semibold">Delete Comment</p>
</div>
<div className="flex flex-col justify-center items-center rounded-xl mt-5 mb-5">
<div className="flex justify-center items-center rounded-full overflow-hidden h-10 w-40 shadow-md shadow-slate-300 p-4 mb-4">
<p className="text-lg font-bold text-slate-700">
{" "}
A<b className="text-orange-500">2</b>E
</p>
</div>
<RiErrorWarningFill className="text-6xl text-red-700 " />
<p className="p-2">Are you sure you want to delete this comment</p>
</div>
<div className="flex space-x-4 justify-between">
<button
className=" py-2 px-4 bg-orange-500 text-white rounded-sm"
onClick={handleClose}
>
Cancel
</button>
<button className="py-2 px-4 bg-red-500 text-white rounded-sm">
Confirm
</button>
</div>
</form>
</div>
</div>
);
};
export default DeleteQuestion;

DeleteComment Component

This component asks comment owners for confirmation before deleting comments and is modal. The accompanying code is shown below.

import { RiErrorWarningFill } from 'react-icons/ri'
import { setGlobalState, useGlobalState } from '../store'
import { toast } from 'react-toastify'
import { deleteComment, getComments } from '../services/blockchain'
const DeleteComment = () => {
const [deleteCommentModal] = useGlobalState('deleteCommentModal')
const [comment] = useGlobalState('comment')
const handleSubmit = async (e) => {
e.preventDefault()
const params = {
questionId: comment.questionId,
commentId: comment.id,
}
await toast.promise(
new Promise(async (resolve, reject) => {
await deleteComment(params)
.then(async () => {
setGlobalState('deleteCommentModal', 'scale-0')
getComments(comment.questionId)
handleClose()
resolve()
})
.catch(() => reject())
}),
{
pending: 'Approve transaction...',
success: 'Comment deleted successfully 👌',
error: 'Encountered error 🤯',
},
)
}
const handleClose = () => {
setGlobalState('deleteCommentModal', 'scale-0')
setGlobalState('comment', null)
}
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 ${deleteCommentModal}`}
>
<div className="bg-white shadow-lg shadow-slate-900 rounded-xl w-11/12 md:w-2/5 h-7/12 p-6">
<div className="flex flex-col">
<div className="flex flex-row justify-between items-center">
<p className="font-semibold">Delete Comment</p>
</div>
<div className="flex flex-col justify-center items-center rounded-xl mt-5 mb-5">
<div className="flex justify-center items-center rounded-full overflow-hidden h-10 w-40 shadow-md shadow-slate-300 p-4 mb-4">
<p className="text-lg font-bold text-slate-700">
{' '}
A<b className="text-orange-500">2</b>E
</p>
</div>
<RiErrorWarningFill className="text-6xl text-red-700 " />
<p className="p-2">Are you sure you want to delete this comment</p>
</div>
<div className="flex space-x-4 justify-between">
<button
className=" py-2 px-4 bg-orange-500 text-white rounded-sm"
onClick={handleClose}
>
Cancel
</button>
<button
onClick={handleSubmit}
className="py-2 px-4 bg-red-500 text-white rounded-sm"
>
Confirm
</button>
</div>
</div>
</div>
</div>
)
}
export default DeleteComment

ChatModal Component

Chats

This modal component makes use of cometChat SDK for handling chats between users.
See the code below:

import { FaTimes } from 'react-icons/fa'
import { setGlobalState, useGlobalState } from '../store'
import Identicon from 'react-identicons'
import { truncate } from '../store'
import { getMessages, sendMessage, listenForMessage } from '../services/Chat'
import { useParams } from 'react-router-dom'
import { useState, useEffect } from 'react'
const ChatModal = () => {
const [chatModal] = useGlobalState('chatModal')
const { id } = useParams()
const [message, setMessage] = useState('')
const [messages] = useGlobalState('messages')
const onSendMessage = async (e) => {
e.preventDefault()
if (!message) return
new Promise(async (resolve, reject) => {
await sendMessage(`guid_${id}`, message)
.then((msg) => {
setGlobalState('messages', (prevMessages) => [...prevMessages, msg])
setMessage('')
resolve(msg)
})
.catch(() => reject())
})
}
useEffect(async () => {
await getMessages(`guid_${id}`).then((msgs) => {
if (msgs.length > 0) {
setGlobalState('messages', msgs)
} else {
console.log('empty')
}
})
await listenForMessage(`guid_${id}`).then((msg) => {
setGlobalState('messages', (prevMessages) => [...prevMessages, msg])
})
}, [])
const handleClose = () => {
setGlobalState('chatModal', 'scale-0')
}
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 ${chatModal}`}
>
<div className="bg-slate-200 shadow-lg shadow-slate-900 rounded-xl w-11/12 md:w-3/5 h-[30rem] p-6 relative">
<div className="flex justify-between items-center">
<h2 className="capitalize">Join the live chat session</h2>
<FaTimes className="cursor-pointer" onClick={handleClose} />
</div>
<div className="overflow-y-scroll overflow-x-hidden h-[20rem] scroll-bar mt-5 px-4 py-3">
<div className="w-11/12">
{messages.length > 0 ? (
messages.map((msg, i) => (
<Message message={msg.text} uid={msg.sender.uid} key={i} />
))
) : (
<div> Leave a comment </div>
)}
</div>
</div>
<form
className="absolute bottom-5 left-[2%] h-[2rem] w-11/12 "
onSubmit={onSendMessage}
>
<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>
</div>
)
}
const Message = ({ message, uid }) => {
return (
<div className="flex items-center space-x-4 mb-1">
<div className="flex items-center space-x-2">
<Identicon string={uid} size={15} className="rounded-full" />
<p className="font-bold text-sm">{truncate(uid, 4, 4, 11)}</p>
</div>
<p className="text-sm">{message}</p>
</div>
)
}
export default ChatModal
{
/* salt page unable inject planet clap blame legend wild blade wine casual */
}
view raw ChatModal.jsx hosted with ❤ by GitHub

AuthChat Component

Chat Authentication Component

This component handles user authentication (signup and login) before allowing them to chat. See the code below:

import { FaTimes } from 'react-icons/fa'
import { setGlobalState, useGlobalState } from '../store'
import { signUpWithCometChat, loginWithCometChat } from '../services/Chat'
import { toast } from 'react-toastify'
const AuthChat = () => {
const [authChatModal] = useGlobalState('authChatModal')
const handleClose = () => {
setGlobalState('authChatModal', 'scale-0')
}
const handleSignUp = async () => {
await toast.promise(
new Promise(async (resolve, reject) => {
await signUpWithCometChat()
.then((user) => {
setGlobalState('currentUser', user)
resolve()
})
.catch(() => reject())
}),
{
pending: 'processing...',
success: 'Account created, please login 👌',
error: 'Encountered error 🤯',
},
)
}
const handleLogin = async () => {
await toast.promise(
new Promise(async (resolve, reject) => {
await loginWithCometChat()
.then(async (user) => {
setGlobalState('currentUser', user)
resolve()
window.location.reload()
})
.catch(() => reject())
}),
{
pending: 'processing...',
success: 'login successfull 👌',
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 ${authChatModal}`}
>
<div className="bg-white shadow-lg shadow-slate-900 rounded-xl w-11/12 md:w-2/5 p-6 relative">
<div className="flex items-center justify-between">
<h2>Auth</h2>
<FaTimes className="cursor-pointer" onClick={handleClose} />
</div>
<div className="flex items-center justify-center space-x-4">
<button
className="p-2 bg-blue-500 rounded-md text-white focus:outline-none focus:ring-0"
onClick={handleLogin}
>
Login
</button>
<button
className="p-2 bg-gray-600 rounded-md text-white focus:outline-none focus:ring-0"
onClick={handleSignUp}
>
Sign up
</button>
</div>
</div>
</div>
)
}
export default AuthChat
view raw AuthChat.jsx hosted with ❤ by GitHub

ChatCommand Component

Creating Groups

Joining Groups

This component lets question owners create a group chat for answerers to join and earn rewards. Code:

import { FaTimes } from 'react-icons/fa'
import { setGlobalState, useGlobalState } from '../store'
import Identicon from 'react-identicons'
import { truncate } from '../store'
import { getMessages, sendMessage, listenForMessage } from '../services/Chat'
import { useParams } from 'react-router-dom'
import { useState, useEffect } from 'react'
const ChatModal = () => {
const [chatModal] = useGlobalState('chatModal')
const { id } = useParams()
const [message, setMessage] = useState('')
const [messages] = useGlobalState('messages')
const onSendMessage = async (e) => {
e.preventDefault()
if (!message) return
new Promise(async (resolve, reject) => {
await sendMessage(`guid_${id}`, message)
.then((msg) => {
setGlobalState('messages', (prevMessages) => [...prevMessages, msg])
setMessage('')
scrollToEnd()
resolve(msg)
})
.catch(() => reject())
})
}
useEffect(async () => {
await getMessages(`guid_${id}`).then((msgs) => {
if (msgs.length > 0) {
setGlobalState('messages', msgs)
} else {
console.log('empty')
}
scrollToEnd()
})
await listenForMessage(`guid_${id}`).then((msg) => {
setGlobalState('messages', (prevMessages) => [...prevMessages, msg])
scrollToEnd()
})
}, [])
const handleClose = () => {
setGlobalState('chatModal', 'scale-0')
}
const scrollToEnd = () => {
const elmnt = document.getElementById('messages-container')
elmnt.scrollTop = elmnt.scrollHeight
}
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 ${chatModal}`}
>
<div className="bg-slate-200 shadow-lg shadow-slate-900 rounded-xl w-11/12 md:w-3/5 h-[30rem] p-6 relative">
<div className="flex justify-between items-center">
<h2 className="capitalize">Join the live chat session</h2>
<FaTimes className="cursor-pointer" onClick={handleClose} />
</div>
<div id="messages-container" className="overflow-y-scroll overflow-x-hidden h-[20rem] scroll-bar mt-5 px-4 py-3">
<div className="w-11/12">
{messages.length > 0 ? (
messages.map((msg, i) => (
<Message message={msg.text} uid={msg.sender.uid} key={i} />
))
) : (
<div> Leave a comment </div>
)}
</div>
</div>
<form
className="absolute bottom-5 left-[2%] h-[2rem] w-11/12 "
onSubmit={onSendMessage}
>
<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>
</div>
)
}
const Message = ({ message, uid }) => {
return (
<div className="flex items-center space-x-4 mb-1">
<div className="flex items-center space-x-2">
<Identicon string={uid} size={15} className="rounded-full" />
<p className="font-bold text-sm">{truncate(uid, 4, 4, 11)}</p>
</div>
<p className="text-sm">{message}</p>
</div>
)
}
export default ChatModal
view raw ChatModal.jsx hosted with ❤ by GitHub

Views

Create the views folder inside the src directory and sequentially add the following pages within it.

HomePage

Listing out the questions

This component creates a visually appealing interface by merging the QuestionTitle and QuestionSingle components. Please refer to the code below for details.

import QuestionTitle from '../components/QuestionTitle'
import { useState, useEffect } from 'react'
import { useGlobalState } from '../store'
import { getQuestions } from '../services/blockchain'
import QuestionSingle from '../components/QuestionSingle'
const Home = () => {
const [loaded, setLoaded] = useState(false)
const [questions] = useGlobalState('questions')
useEffect(async () => {
await getQuestions().then(() => {
setLoaded(true)
})
}, [])
return loaded ? (
<div className="sm:px-20 px-5 my-4">
<QuestionTitle
title={'Ask Questions'}
caption={`${
questions.length > 1
? questions.length + ' Questions'
: questions.length + ' Question'
}`}
/>
<div className="my-4">
{questions.map((question, i) => (
<QuestionSingle question={question} titled key={i} />
))}
</div>
</div>
) : null
}
export default Home
view raw Home.jsx hosted with ❤ by GitHub

Question Page

The single question page with comments

This page has many components for commenting, payingout, and chatting. See the code below.

import QuestionSingle from '../components/QuestionSingle'
import QuestionTitle from '../components/QuestionTitle'
import { useGlobalState, returnTime, setGlobalState } from '../store'
import { getQuestion } from '../services/blockchain'
import { useParams } from 'react-router-dom'
import { useEffect, useState } from 'react'
import QuestionComments from '../components/QuestionComments'
import ChatModal from '../components/ChatModal'
import AuthChat from '../components/AuthChat'
import AddComment from '../components/AddComment'
import { getGroup } from '../services/Chat'
import ChatCommand from '../components/ChatCommand'
import UpdateComment from '../components/UpdateComment'
import DeleteComment from '../components/DeleteComment'
const Question = () => {
const [question] = useGlobalState('question')
const [group] = useGlobalState('group')
const [comment] = useGlobalState('comment')
const [currentUser] = useGlobalState('currentUser')
const [loaded, setLoaded] = useState(false)
const [isOnline, setIsOnline] = useState(false)
const [connectedAccount] = useGlobalState('connectedAccount')
const { id } = useParams()
useEffect(async () => {
setIsOnline(currentUser?.uid.toLowerCase() == connectedAccount)
await getQuestion(id).then(() => setLoaded(true))
await getGroup(`guid_${id}`).then((Group) => {
setGlobalState('group', Group)
})
}, [])
const handleChat = () => {
if (isOnline && (!group || !group.hasJoined)) {
setGlobalState('chatCommandModal', 'scale-100')
} else if (isOnline && group && group.hasJoined) {
setGlobalState('chatModal', 'scale-100')
} else {
setGlobalState('authChatModal', 'scale-100')
}
}
return loaded ? (
<div className="sm:px-20 px-5 my-4">
<QuestionTitle
title={question.title}
caption={`Asked ${returnTime(question.createdAt)}`}
/>
<div className="my-4">
<QuestionSingle
editable={question.owner == connectedAccount}
question={question}
/>
</div>
<div className="flex space-x-5 border-b border-b-gray-300 pb-4">
<button
className="mt-5 text-blue-500 focus:outline-none focus:ring-0"
onClick={() => setGlobalState('addComment', 'scale-100')}
>
Add Comment
</button>
<button
className="mt-5 text-blue-500 focus:outline-none focus:ring-0"
onClick={handleChat}
>
Chat
</button>
</div>
<AddComment />
<QuestionComments />
{comment ? (
<>
<UpdateComment />
<DeleteComment />
</>
) : null}
<AuthChat />
<ChatCommand question={question} />
<ChatModal />
</div>
) : null
}
export default Question
view raw Question.jsx hosted with ❤ by GitHub

The App.jsx file

We'll look at the App.jsx file, which bundles our components and pages.

import { Routes, Route } from 'react-router-dom'
import Header from './components/Header'
import Home from './views/Home'
import Question from './views/Question'
import AddQuestion from './components/AddQuestion'
import { ToastContainer } from 'react-toastify'
import 'react-toastify/dist/ReactToastify.css'
import { setGlobalState, useGlobalState } from './store'
import { WalletConnectedStatus } from './services/blockchain'
import { checkAuthState } from './services/Chat'
import { useEffect, useState } from 'react'
import UpdateQuestion from './components/UpdateQuestion'
import DeleteQuestion from './components/DeleteQuestion'
const App = () => {
const [loaded, setLoaded] = useState(false)
const [connectedAccount] = useGlobalState('connectedAccount')
useEffect(async () => {
await WalletConnectedStatus().then(async () => {
console.log('Blockchain Loaded')
await checkAuthState().then((user) => {
setGlobalState('currentUser', user)
})
setLoaded(true)
})
}, [])
return (
<div className="min-h-screen">
<Header />
{loaded ? (
<Routes>
<Route path={'/'} element={<Home />} />
<Route path={'/question/:id'} element={<Question />} />
</Routes>
) : null}
<UpdateQuestion />
<DeleteQuestion />
{connectedAccount ? <AddQuestion /> : null}
<ToastContainer
position="bottom-center"
autoClose={5000}
hideProgressBar={false}
newestOnTop={false}
closeOnClick
rtl={false}
pauseOnFocusLoss
draggable
pauseOnHover
theme="dark"
/>
</div>
)
}
export default App
view raw App.jsx hosted with ❤ by GitHub

Other Essential Services

The services listed below are critical to the smooth operation of our application.

The Store Service
The application relies on critical services, including the “Store Service” which uses the react-hooks-global-state library to manage the application's state. To set up the Store Service, create a "store" folder within the "src" folder and create an "index.jsx" file within it, then paste and save the provided code.

import { createGlobalState } from 'react-hooks-global-state'
import moment from 'moment'
const { setGlobalState, useGlobalState, getGlobalState } = createGlobalState({
addModal: 'scale-0',
updateModal: 'scale-0',
addComment: 'scale-0',
deleteQuestionModal: 'scale-0',
deleteCommentModal: 'scale-0',
updateCommentModal: 'scale-0',
chatModal: 'scale-0',
chatCommandModal: 'scale-0',
authChatModal: 'scale-0',
paymentModal: 'scale-0',
connectedAccount: '',
questions: [],
question: null,
singleQuestion: null,
comments: [],
comment: null,
contract: null,
group: null,
currentUser: null,
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 daysRemaining = (days) => {
const todaysdate = moment()
days = Number((days + '000').slice(0))
days = moment(days).format('YYYY-MM-DD')
days = moment(days)
days = days.diff(todaysdate, 'days')
return days == 1 ? '1 day' : days + ' days'
}
function returnTime(timestamp) {
const currentTime = new Date()
let difference = currentTime - timestamp
let seconds = Math.floor(difference / 1000)
let minutes = Math.floor(seconds / 60)
let hours = Math.floor(minutes / 60)
let days = Math.floor(hours / 24)
let weeks = Math.floor(days / 7)
let months = Math.floor(weeks / 4)
let years = Math.floor(months / 12)
if (seconds < 60) {
return seconds + (seconds === 1 ? ' second' : ' seconds')
} else if (minutes < 60) {
return minutes + (minutes === 1 ? ' minute' : ' minutes')
} else if (hours < 24) {
return hours + (hours === 1 ? ' hour' : ' hours')
} else if (days < 7) {
return days + (days === 1 ? ' day' : ' days')
} else if (weeks < 4) {
return weeks + (weeks === 1 ? ' week' : ' weeks')
} else if (months < 12) {
return months + (months === 1 ? ' month' : ' months')
} else {
return years + (years === 1 ? ' year' : ' years')
}
}
export {
setGlobalState,
useGlobalState,
getGlobalState,
truncate,
daysRemaining,
returnTime,
}
view raw index.jsx hosted with ❤ by GitHub

The Blockchain Service
Create a folder called "services" inside the "src" folder. Within the "services" folder, create a file named "blockchain.jsx" and save the provided code inside the file.

import abi from '../abis/src/contracts/AnswerToEarn.sol/AnswerToEarn.json'
import address from '../abis/contractAddress.json'
import { getGlobalState, setGlobalState } from '../store'
import { ethers } from 'ethers'
import { logOutWithCometChat } from './Chat'
const toWei = (num) => ethers.utils.parseEther(num.toString())
const fromWei = (num) => ethers.utils.formatEther(num)
const { ethereum } = window
const contractAddress = address.address
const contractAbi = abi.abi
let tx
const getEthereumContract = async () => {
const connectedAccount = getGlobalState('connectedAccount')
if (connectedAccount) {
const provider = new ethers.providers.Web3Provider(ethereum)
const signer = provider.getSigner()
const contracts = new ethers.Contract(contractAddress, contractAbi, signer)
return contracts
} else {
return getGlobalState('contract')
}
}
const WalletConnectedStatus = async () => {
try {
if (!ethereum) return alert('Please install metamask')
const accounts = await ethereum.request({ method: 'eth_accounts' })
window.ethereum.on('chainChanged', function (chainId) {
window.location.reload()
})
window.ethereum.on('accountsChanged', async function () {
setGlobalState('connectedAccount', accounts[0])
await WalletConnectedStatus()
await logOutWithCometChat().then(() => {
setGlobalState('currentUser', null)
})
})
if (accounts.length) {
setGlobalState('connectedAccount', accounts[0])
} else {
alert('Please connect wallet')
console.log('No accounts found')
}
} catch (err) {
reportError(err)
}
}
const connectWallet = async () => {
try {
if (!ethereum) return alert('Please install metamask')
const accounts = await ethereum.request({ method: 'eth_requestAccounts' })
setGlobalState('connectedAccount', accounts[0])
} catch (err) {
reportError(err)
}
}
const createQuestion = async ({ title, question, tags, prize }) => {
try {
if (!ethereum) return alert('Please install metamask')
const contract = await getEthereumContract()
tx = await contract.addQuestion(title, question, tags, {
value: toWei(prize),
})
await tx.wait()
await getQuestions()
} catch (err) {
reportError(err)
}
}
const editQuestion = async ({ id, title, question, tags }) => {
try {
if (!ethereum) return alert('Please install metamask')
const contract = await getEthereumContract()
tx = await contract.updateQuestion(id, title, question, tags)
await tx.wait()
await getQuestions()
} catch (err) {
reportError(err)
}
}
const deleteQuestion = async (id) => {
try {
if (!ethereum) return alert('Please install metamask')
const contract = await getEthereumContract()
tx = await contract.deleteQuestion(id)
await tx.wait()
await getQuestions()
} catch (err) {
reportError(err)
}
}
const createComment = async ({ questionId, commentText }) => {
try {
if (!ethereum) return alert('Please install metamask')
const contract = await getEthereumContract()
tx = await contract.addComment(questionId, commentText)
await tx.wait()
await getComments(questionId)
await getQuestion(questionId)
await getQuestions()
} catch (err) {
reportError(err)
}
}
const editComment = async ({ questionId, commentId, commentText }) => {
try {
if (!ethereum) return alert('Please install metamask')
const contract = await getEthereumContract()
tx = await contract.updateComment(questionId, commentId, commentText)
await tx.wait()
await getComments(questionId)
} catch (err) {
reportError(err)
}
}
const deleteComment = async ({ questionId, commentId }) => {
try {
if (!ethereum) return alert('Please install metamask')
const contract = await getEthereumContract()
tx = await contract.deleteComment(questionId, commentId)
tx.wait()
await getComments(questionId)
} catch (err) {
reportError(err)
}
}
const getQuestions = async () => {
try {
if (!ethereum) return alert('Please install metamask')
const contract = await getEthereumContract()
const questions = await contract.showQuestions()
setGlobalState('questions', structuredQuestion(questions))
} catch (err) {
reportError(err)
}
}
const getQuestion = async (questionId) => {
try {
if (!ethereum) return alert('Please install metamask')
const contract = await getEthereumContract()
const question = await contract.showQuestion(questionId)
setGlobalState('question', structuredQuestion([question])[0])
await getComments(questionId)
} catch (err) {
reportError(err)
}
}
const getComments = async (questionId) => {
try {
if (!ethereum) return alert('Please install metamask')
const contract = await getEthereumContract()
const comments = await contract.getComments(questionId)
setGlobalState('comments', structuredComment(comments))
} catch (err) {
reportError(err)
}
}
const structuredQuestion = (questions) =>
questions
.map((question) => ({
id: question.id.toNumber(),
title: question.questionTitle,
description: question.questionDescription,
owner: question.owner.toLowerCase(),
createdAt: Number(question.created + '000'),
updated: Number(question.updated + '000'),
answers: question.answers.toNumber(),
prize: fromWei(question.prize),
tags: question.tags,
paidout: question.paidout,
winner: question.winner.toLowerCase(),
refunded: question.refunded,
}))
.reverse()
const structuredComment = (comments) =>
comments
.map((comment) => ({
id: comment.id.toNumber(),
questionId: comment.questionId.toNumber(),
commentText: comment.commentText,
owner: comment.owner.toLowerCase(),
deleted: comment.deleted,
createdAt: Number(comment.created + '000'),
updatedAt: Number(comment.updated + '000'),
}))
.reverse()
const payWinner = async (questionId, commentId) => {
try {
if (!ethereum) return alert('Please install metamask')
const contract = await getEthereumContract()
const connectedAccount = getGlobalState('connectedAccount')
tx = await contract.payBestComment(questionId, commentId, {
from: connectedAccount,
})
await tx.wait()
await getComments(questionId)
await getQuestion(questionId)
await getQuestions()
} catch (err) {
reportError(err)
}
}
export {
getEthereumContract,
WalletConnectedStatus,
connectWallet,
createQuestion,
editQuestion,
deleteQuestion,
createComment,
editComment,
deleteComment,
getQuestions,
getQuestion,
getComments,
payWinner,
}
view raw Blockchain.jsx hosted with ❤ by GitHub

The Chat Service
Create a file named "chat.jsx" within the "services" folder and copy the provided code into the file before saving it.

import { CometChat } from "@cometchat-pro/chat";
import { getGlobalState } from "../store";
const CONSTANTS = {
APP_ID: process.env.REACT_APP_COMETCHAT_APP_ID,
REGION: process.env.REACT_APP_COMETCHAT_REGION,
Auth_Key: process.env.REACT_APP_COMETCHAT_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) => console.log(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 checkAuthState = async () => {
return new Promise(async (resolve, reject) => {
await CometChat.getLoggedinUser()
.then((user) => resolve(user))
.catch((error) => reject(error));
});
};
const createNewGroup = async (GUID, groupName) => {
const groupType = CometChat.GROUP_TYPE.PUBLIC;
const password = "";
const group = new CometChat.Group(GUID, groupName, groupType, password);
return new Promise(async (resolve, reject) => {
await CometChat.createGroup(group)
.then((group) => resolve(group))
.catch((error) => reject(error));
});
};
const getGroup = async (GUID) => {
return new Promise(async (resolve, reject) => {
await CometChat.getGroup(GUID)
.then((group) => resolve(group))
.catch((error) => reject(error));
});
};
const joinGroup = async (GUID) => {
const groupType = CometChat.GROUP_TYPE.PUBLIC;
const password = "";
return new Promise(async (resolve, reject) => {
await CometChat.joinGroup(GUID, groupType, password)
.then((group) => resolve(group))
.catch((error) => reject(error));
});
};
const getMessages = async (UID) => {
const limit = 30;
const messagesRequest = new CometChat.MessagesRequestBuilder()
.setGUID(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.GROUP;
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 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,
checkAuthState,
createNewGroup,
getGroup,
joinGroup,
listenForMessage,
};
view raw Chat.jsx hosted with ❤ by GitHub

The Index.jsx file
Now update the index entry file with the following codes.

import React from 'react'
import ReactDOM from 'react-dom'
import { BrowserRouter } from 'react-router-dom'
import './index.css'
import App from './App'
import { initCometChat } from './services/Chat'
initCometChat().then(() => {
ReactDOM.render(
<BrowserRouter>
<App />
</BrowserRouter>,
document.getElementById('root')
)
})
view raw index.js hosted with ❤ by GitHub

To start the server on your browser, run these commands on two terminals, assuming you have already installed Metamask.

# Terminal one:
yarn hardhat node
# Terminal Two
yarn hardhat run scripts/deploy.js
yarn start
Enter fullscreen mode Exit fullscreen mode

Running the above commands as instructed will open your project on your browser. And there you have it for how to build a blockchain voting system with React, Solidity, and CometChat.

If you're confused about web3 development and want visual materials, here's one of my videos that will teach you how to create an NFT Minting website.


Watch the video

Conclusion

In conclusion, the decentralized web and blockchain technology are here to stay, and creating practical applications is a great way to advance your career in web3 development.

The tutorial showed how to create an answer-to-earn system using smart contracts to facilitate payments, along with the CometChat SDK for group discussions.

If you are ready to dive deeper into web3 development, watch my free videos on my YouTube channel. Or book your private web3 classes with me to speed up your web3 learning process.

That being said, I'll see you next time, and have a wonderful day!

About the Author

Gospel Darlington is a full-stack blockchain developer with 6+ years of experience in the software development industry.

By combining Software Development, writing, and teaching, he demonstrates how to build decentralized applications on EVM-compatible blockchain networks.

His stacks include JavaScript, React, Vue, Angular, Node, React Native, NextJs, Solidity, and more.

For more information about him, kindly visit and follow his page on Twitter, Github, LinkedIn, or his website.

Heroku

This site is built on Heroku

Join the ranks of developers at Salesforce, Airbase, DEV, and more who deploy their mission critical applications on Heroku. Sign up today and launch your first app!

Get Started

Top comments (0)