WEB3DEV

Cover image for Como construir uma plataforma Answer-to-Earn (responda para ganhar) com React, Solidity e CometChat
Isabela Curado Nehme
Isabela Curado Nehme

Posted on • Atualizado em

Como construir uma plataforma Answer-to-Earn (responda para ganhar) com React, Solidity e CometChat

26 de fevereiro de 2023

# Construa uma plataforma Answer-to-earn.

O que você irá construir, veja a demonstração na rede de teste Sepolia e no repositório GitHub.


Sumário

1.0 Introdução

2.0 O que é necessário

3.0 Configurando o CometChat SDK

4.0 Configurando o script do Hardhat

5.0 Configurando o script de implantação

6.0 O arquivo de contrato inteligente

7.0 Aprendendo o desenvolvimento de contratos inteligentes
7.1 Estruturas
7.2 Variáveis de estado
7.3 Construtor
7.4 Eventos e Modificadores
7.5 Funções de perguntas
7.6 Funções de comentários
7.7 Funções de fundos
7.8 O arquivo de contrato inteligente

8.0 Desenvolvendo o Front End
8.1.1 Componentes
8.1.2 Componente de cabeçalho
8.1.3 Componente Título da pergunta
8.1.4 Componente Adicionar pergunta
8.1.5 Componente Pergunta única
8.1.6 Componente Comentários de pergunta
8.1.7 Componente Adicionar comentário
8.1.8 Componente Atualizar pergunta
8.1.9 Componente Atualizar comentário
8.1.10 Componente Excluir pergunta
8.1.11 Componente Excluir comentário
8.1.12 Componente Modal de conversa
8.1.13 Componente Autenticação de conversa
8.1.14 Componente Comando de conversa

9.0 Visualizações
9.1 Página inicial
9.2 Página de perguntas

10.0 O arquivo App
10.1 O serviço de blockchain
10.2 O serviço de chat
10.3 O arquivo Index

11.0 Conclusão


Introdução

Se você procura criar uma plataforma inovadora que motive os usuários a compartilhar seus conhecimentos e habilidades, este tutorial sobre desenvolvimento de uma plataforma Answer-to-Earn (responda para ganhar) usando React, Solidity e CometChat pode ser exatamente o que você precisa.

Este tutorial combina a tecnologia blockchain, comunicação em tempo real e conteúdo gerado pelo usuário para criar uma plataforma interativa que recompense os usuários por suas contribuições e encoraje o engajamento e a cooperação.

Seja você um desenvolvedor experiente ou um iniciante, este guia passo-a-passo o ajudará a trazer sua visão à vida e transformar o jeito que as pessoas compartilham informação. Então, por que não começar construindo sua própria plataforma Answer-to-Earn hoje e revolucionar o aprendizado e o compartilhamento de informações?

A propósito, inscreva-se no meu canal no YouTube para aprender como criar um aplicativo na Web3 do zero. Também ofereço uma ampla gama de serviços, seja tutoria particular, consultoria por hora, outros serviços de desenvolvimento ou produção de material educativo para Web3, você pode reservar meus serviços aqui.

Agora, vamos mergulhar neste tutorial.

O que é necessário

Você precisará das seguintes ferramentas instaladas para construir junto comigo:

  • Node.js (Importante)
  • Ethers.js
  • Hardhat
  • Yarn
  • Metamask
  • React
  • Tailwind CSS
  • CometChat SDK

Instalando dependências

Clone o kit para iniciantes e abra-o no VS Code usando o comando abaixo:

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"
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

Agora, execute a instalação Yarn no terminal para ter todas as dependências para este projeto instaladas.

Configurando o CometChat SDK

Siga os passos abaixo para configurar o CometChat SDK; ao final, você deve salvar essas chaves como uma variável de ambiente.

PASSO 1:

Vá para o painel do CometChat e crie uma conta.

PASSO 2:

Faça login no painel do CometChat, apenas depois de se registrar.

PASSO 3:

Pelo painel, adicione um novo aplicativo chamado A2E.

PASSO 4:

Selecione o aplicativo que você acabou de criar na lista.

PASSO 5:

No Quick Start, copie APP_ID, REGION e AUTH_KEY para seu arquivo .env. Veja a imagem e o trecho do código.

Substitua as chaves de espaço reservado REACT_COMET_CHAT por seus valores apropriados.

REACT_APP_COMETCHAT_APP_ID=****************
REACT_APP_COMETCHAT_AUTH_KEY=******************************
REACT_APP_COMETCHAT_REGION=**
Enter fullscreen mode Exit fullscreen mode

O arquivo .env deve ser criado na raiz do seu projeto.

Configurando o script do Hardhat

Na raiz do seu projeto, abra o arquivo hardhat.config.js e substitua seu conteúdo pelas seguintes configurações.

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

O script acima instrui o Hardhat nestas três importantes regras.

  • Networks (redes): este bloco contém as configurações para sua escolha de redes. Na implantação, o Hardhat exigirá que você especifique uma rede para enviar seus contratos inteligentes.
  • Solidity: isto descreve a versão do compilador a ser usado pelo Hardhat para compilar seus códigos de contrato inteligente em bytecodes e ABI.
  • Paths: isto simplesmente informa o Hardhat da localização dos seus contratos inteligentes e também um local para descarregar a saída do compilador, que é a ABI.

Configurando o script de implantação

Navegue até a pasta de scripts e, em seguida, para o seu arquivo deploy.js e cole o código abaixo nele. Se você não conseguir encontrar uma pasta de script, faça uma, crie um arquivo deploy.js e cole o código a seguir dentro dele.


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
})
Enter fullscreen mode Exit fullscreen mode

Quando executado como um comando de implantação do Hardhat, o script acima implantará seu contrato inteligente especificado na rede de sua escolha.

Confira este vídeo para aprender como configurar adequadamente um projeto Web3 com ReactJs.

https://youtu.be/-q_6CyGNEDw

O arquivo de contrato inteligente

Agora que concluímos as configurações iniciais, vamos criar o contrato inteligente para esta construção. Crie uma nova pasta chamada contracts (contratos) no diretório SRC do seu projeto.

Crie um novo arquivo chamado Answer2Earn.sol dentro dessa pasta de contratos; esse arquivo conterá toda a lógica que governa o contrato inteligente.

Copie, cole e salve os seguintes código no arquivo Answer2Earn.sol. Veja o código completo abaixo.

//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;
    }
}
Enter fullscreen mode Exit fullscreen mode

Tenho um livro para ajudá-lo a dominar a linguagem Web3 (Solidity), pegue sua cópia aqui.

# Aprendendo o desenvolvimento de contratos inteligentes

Agora, vamos examinar alguns dos detalhes do que está acontecendo no contrato inteligente acima. Temos os seguintes itens:

Estruturas

  • QuestionStruct: contém as informações necessárias sobre todas as perguntas feitas na plataforma.
  • CommentStuct: carrega informações sobre todos os comentários em nossa plataforma.

Variáveis de estado

  • Owner: acompanha o implantador do contrato.
  • platformCharge: detém a taxa percentual da plataforma em cada pagamento realizado na plataforma.
  • totalQuestion: armazena o número de perguntas que não foram excluídas da plataforma.

Mapeamentos

  • questionExists: verifica se uma pergunta existe ou não foi excluída da plataforma.
  • commentsOf: detém o total de comentários de uma pergunta específica.

Construtor

É usado para inicializar o estado das variáveis de contrato inteligente e outras operações essenciais. Neste exemplo, atribuímos o implantador do contrato inteligente como o proprietário (owner).

Eventos e Modificadores

  • Action: registra as informações de todas as ações realizadas na plataforma.
  • ownerOnly: impede que todos os outros usuários, exceto o proprietário, acessem algumas funções na plataforma.

Funções de perguntas

  • addQuestion: essa função pega uma pergunta de recompensa de um usuário e a torna disponível para outros competirem pela melhor resposta.
  • updateQuestion: essa função é usada para editar a pergunta fornecida pelo proprietário da pergunta.
  • deleteQuestion: essa função é usada para excluir a pergunta fornecida pelo proprietário da pergunta.
  • showQuestion: essa função é responsável por exibir todas as perguntas na plataforma. A pergunta deve existir previamente e não estar deletada.
  • isQuestionOwner: essa função retorna verdadeiro/falso se um usuário é dono de uma pergunta.

Funções de comentários

  • addComment: essa função permite aos usuários fornecer uma resposta para uma pergunta em particular, na esperança de ganhar o prêmio pela pergunta.
  • udpdateComment: essa função é usada para editar uma resposta, desde que o chamador seja o proprietário da resposta/comentário.
  • deleteComment: essa função é usada para excluir um comentário, desde que o chamador seja o proprietário da resposta/comentário.
  • getComments: essa função retorna todos os comentários/respostas para uma pergunta específica.
  • getComment: retorna um único comentário para uma pergunta específica na plataforma.
  • isCommentOwner: essa função retorna verdadeiro/falso se um usuário é proprietário do comentário.

Funções de fundos

  • payBestComment: essa função é usada pelo proprietário de uma pergunta para pagar o respondente pela resposta preferida.
  • refund: essa função é usada para solicitar um reembolso por um proprietário de uma pergunta, desde que as respostas não tenham sido submetidas ainda.
  • payTo: essa função é usada internamente para realizar pagamentos ao endereço fornecido.
  • changeFee: essa função é usada para alterar a taxa da plataforma.

Com todas as funções acima compreendidas, copie-as para dentro de um arquivo chamado Answer2Earn.sol na pasta de contratos dentro do diretório SRC.

Em seguida, execute os comandos abaixo para implantar o contrato inteligente na rede.

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

Se precisar de mais ajuda para configurar o Hardhat ou implantar seu Fullstack dApp (dApp de pilha completa), assista a este vídeo.

https://youtu.be/hsec2erdLOI

Desenvolvendo o Front End

Agora que temos nosso contrato inteligente na rede e todos os nossos artefatos (bytecodes e ABI) gerados, vamos deixar o front-end pronto com o React.

Componentes

No diretório SRC, crie uma pasta chamada components (componentes) para abrigar todos os componentes do React para este projeto.

Componente de cabeçalho

O componente de cabeçalho inclui o logotipo da plataforma, uma barra de pesquisa e um botão com o endereço da conta conectada. Veja o código abaixo.

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

Componente Título da pergunta

Este componente inclui um cabeçalho, um botão para fazer perguntas e informações sobre todas as outras perguntas. Aqui está o código:

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

Componente Adicionar pergunta

Este é um modal que permite que os usuários criem perguntas na plataforma. Para enviar uma pergunta com sucesso, os usuários precisam fornecer um título, um prêmio de recompensa, uma etiqueta e a descrição da pergunta. Aqui estão os códigos relevantes:

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

Componente Pergunta única

Este componente inclui detalhes da pergunta, como o proprietário da pergunta e o prêmio.

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

Componente Comentários de pergunta

Este código mostra os comentários ou respostas do usuário ao proprietário da pergunta.

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

Componente Adicionar comentário

Este componente permite que os usuários respondam a uma pergunta específica através de uma interface. Veja o código abaixo:

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

Componente Atualizar pergunta

Este componente permite que o proprietário da pergunta faça edições na sua pergunta. É um componente modal que aplica ajustes. Aqui está o código:

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

Componente Atualizar comentário

Este código permite que um usuário edite comentários ao responder uma pergunta, mas a propriedade ainda será verificada.

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

Componente Excluir pergunta

Este componente solicita a confirmação do proprietário da pergunta antes de excluir uma pergunta e é projetado como um componente modal. Por favor, dirija-se ao código abaixo.

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

Componente Excluir comentário

Este componente solicita aos proprietários do comentário confirmação antes de excluir os comentários e é modal. O código acompanhante é mostrado abaixo.

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

Componente Modal de conversa

Este componente modal faz uso do CometChat SDK para lidar com conversas entre os usuários.

Veja o código abaixo:

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

Componente Autenticação de conversa

Este componente lida com a autenticação do usuário (registro e login) antes de permitir que eles conversem. Veja o código abaixo:

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

Componente Comando de conversa

Este componente permite que os proprietários da pergunta criem um grupo de conversa (bate-papo) para que os respondentes participem e ganhem recompensas. Código:

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

Visualizações

Crie a pasta views (visualizações) dentro do diretório SRC e adicione sequencialmente as seguintes páginas dentro dele.

Página inicial

Este componente cria uma interface visualmente atraente mesclando os componentes QuestionTitle e QuestionSingle. Por favor, consulte o código abaixo para obter detalhes.

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

Página de perguntas

Esta página tem muitos componentes para comentários, pagamentos e bate-papo. Veja o código abaixo.

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

O arquivo App

Vamos olhar para o arquivo App.jsx, que agrupa nossos components (componentes) e pages (páginas).

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

Outros serviços essenciais

Os serviços listados abaixo são fundamentais para o funcionamento tranquilo de nosso aplicativo.

O serviço da loja

O aplicativo conta com serviços fundamentais, incluindo ”Store Service” (serviço de loja), que usa a biblioteca react-hooks-global-state para gerenciar o estado do aplicativo. Para configurar o serviço de loja, crie uma pasta ”store” dentro da pasta “SRC” e crie um arquivo ”index.jsx” dentro dela. Em seguida, cole e salve o código fornecido.

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,
}
Enter fullscreen mode Exit fullscreen mode

O serviço de blockchain

Crie uma pasta chamada ”services” (serviços) dentro da pasta ”SRC”. Dentro da pasta ”services”, crie um arquivo chamado ”blockchain.jsx” e salve o código fornecido dentro do arquivo.

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),
    })
    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)
    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)
    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)
    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)
    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,
    })
    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,
}
Enter fullscreen mode Exit fullscreen mode

O serviço de chat

Crie um arquivo chamado ”chat.jsx” dentro da pasta ”services” e copie o código fornecido dentro do arquivo antes de salvá-lo.

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,
};
Enter fullscreen mode Exit fullscreen mode

O arquivo Index

Agora, atualize o arquivo de entrada do índice (index) com os códigos a seguir.

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')
  )
})
Enter fullscreen mode Exit fullscreen mode

Para iniciar o servidor em seu navegador, execute estes comandos em dois terminais, supondo que você já tenha instalado a Metamask.

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

A execução dos comandos acima conforme as instruções abrirá seu projeto em seu navegador. E aí está como construir um sistema de votação em blockchain com React, Solidity e CometChat.

Se você está confuso sobre o desenvolvimento em Web3 e deseja materiais visuais, aqui está um dos meus vídeos que o ensinará como criar um site de cunhagem NFT.

Construa um NFT da Web3 cunhando o dApp com React, Tailwind, e Solidity: https://youtu.be/QN3wb_mXBjY

Conclusão

Em conclusão, a web descentralizada e a tecnologia blockchain estão aqui para ficar, e criar aplicativos práticos é uma ótima maneira de avançar em sua carreira no desenvolvimento Web3.

O tutorial mostrou como criar um sistema Answer-to-earn (responda para ganhar) usando contratos inteligentes para facilitar pagamentos, juntamente com o CometChat SDK para discussões em grupo.

Se você está pronto para mergulhar profundamente no desenvolvimento na Web3, assista aos meus vídeos gratuitos no meu canal do Youtube. Ou reserve suas aulas particulares de Web3 comigo para acelerar seu processo de aprendizagem na Web3.

Dito isso, te vejo na próxima e tenha um dia maravilhoso!

Sobre o autor

Gospel Darlington é um desenvolvedor de blockchain de pilha completa (full-stack) com mais de 6 anos de experiência na indústria de desenvolvimento de software.

Ao combinar desenvolvimento de software, escrita e ensino, ele demonstra como construir aplicativos descentralizados em redes de blockchain compatíveis com EVM (Ethereum Virtual Machine ou máquina virtual da Ethereum).

Suas pilhas incluem JavaScript, React, Vue, Angular, Node, React Native, NextJs, Solidity e mais.

Para mais informações sobre ele, faça a gentileza de visitar e seguir sua página no Twitter, Github, LinkedIn ou seu site.

Esse artigo foi escrito por Darlington Gospel e traduzido por Isabela Curado Nehme. Seu original pode ser lido aqui.

Top comments (0)