Как создать систему голосования на блокчейне с помощью React, Solidity и CometChat

Как создать систему голосования на блокчейне с помощью React, Solidity и CometChat

24 ноября 2022 г.

Вот что вы будете создавать: посмотрите демонстрацию в тестовой сети Goerli и < strong>репозиторий git здесь.

Blockchain Voting System

Введение

Теперь пришло время научиться создавать децентрализованную систему голосования. В этом руководстве вы узнаете, как создать приложение для голосования на блокчейне с использованием смарт-контрактов Solidity, внешнего интерфейса React, разработанного с помощью Tailwind CSS, и CometChat SDK. .

Если вы готовы сокрушить эту сборку, давайте начнем.

Необходимое условие

Для сборки вместе со мной вам потребуются следующие инструменты:

  • NodeJs (очень важно)
  • Эфиры
  • Каска
  • Реагировать
  • Попутный ветер CSS
  • Кометчат SDK
  • Метамаска
  • Пряжа

Установка зависимостей

Клонируйте приведенный ниже стартовый комплект с помощью следующей команды:

git clone https://github.com/Daltonic/tailwind_ethers_starter_kit <PROJECT_NAME>
cd <PROJECT_NAME>

Теперь откройте проект в VS Code или в предпочитаемом вами редакторе кода. Найдите файл package.json и обновите его с помощью приведенных ниже кодов.

{
  "name": "BlueVotes",
  "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.10",
    "@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"
    ]
  }
}

Теперь запустите **yarn install** на терминале, чтобы установить все зависимости для этого проекта.

Настройка SDK CometChat

Выполните следующие действия, чтобы настроить CometChat SDK; в конце вы должны сохранить эти ключи как переменную среды.

ШАГ 1: Перейдите на Панель управления CometChat и создайте учетную запись.

Register a new CometChat account if you do not have one

ШАГ 2: Войдите в панель управления CometChat только после регистрации.

Log in to the CometChat Dashboard with your created account

ШАГ 3: На панели управления добавьте новое приложение под названием BlueVotes

.

Create a new CometChat app - Step 1

Create a new CometChat app - Step 2

ШАГ 4: Выберите только что созданное приложение из списка.

Select your created app

ШАГ 5: Из краткого руководства скопируйте APP_ID, REGION и AUTH_KEY в свой файл .env. См. изображение и фрагмент кода.

Copy the the APP_ID, REGION, and AUTH_KEY

Замените ключи-заполнители REACT_COMET_CHAT соответствующими значениями.

REACT_APP_COMET_CHAT_REGION=**
REACT_APP_COMET_CHAT_APP_ID=**************
REACT_APP_COMET_CHAT_AUTH_KEY=******************************

Файл **.env** должен быть создан в корне вашего проекта.

Настройка приложения Alchemy

ШАГ 1: перейдите на сайт Alchemy .

ШАГ 2. Создайте учетную запись.

Login to Alchemey

ШАГ 3:

На панели инструментов создайте новый проект.

Creating a Project

ШАГ 4:

Скопируйте URL-адрес конечной точки WebSocket или HTTPS тестовой сети Goerli в свой файл .env.

Goerli Testnet Key

После этого введите закрытый ключ предпочитаемой учетной записи Metamask в DEPLOYER_KEY в переменных среды и сохраните. Если вы правильно следовали инструкциям, ваши переменные среды теперь должны выглядеть так.

ENDPOINT_URL=***************************
DEPLOYER_KEY=**********************

REACT_APP_COMET_CHAT_REGION=**
REACT_APP_COMET_CHAT_APP_ID=**************
REACT_APP_COMET_CHAT_AUTH_KEY=******************************

См. раздел ниже, если вы не знаете, как получить доступ к своему закрытому ключу.

Извлечение закрытого ключа метамаски

ШАГ 1: Убедитесь, что в расширении браузера Metamask в качестве тестовой сети выбран Goerli, ~~Rinkeby~~ и старые тестовые сети устарели.

Затем в нужной учетной записи щелкните вертикальную пунктирную линию и выберите данные учетной записи. Пожалуйста, смотрите изображение ниже.

Step One

ШАГ 2: Введите свой пароль в соответствующее поле и нажмите кнопку подтверждения, это позволит вам получить доступ к закрытому ключу вашей учетной записи.

Step Two

ШАГ 3: Нажмите "экспортировать закрытый ключ", чтобы увидеть свой закрытый ключ. Убедитесь, что вы никогда не публикуете свои ключи на общедоступной странице, такой как Github. Вот почему мы добавляем его как переменную среды.

Step Three

ШАГ 4: Скопируйте свой закрытый ключ в файл .env. См. изображение и фрагмент кода ниже:

Step Four

ENDPOINT_URL=***************************
DEPLOYER_KEY=**********************

REACT_APP_COMET_CHAT_REGION=**
REACT_APP_COMET_CHAT_APP_ID=**************
REACT_APP_COMET_CHAT_AUTH_KEY=******************************

Настройка скрипта Hardhat

В корне проекта откройте файл hardhat.config.js и замените его содержимое следующими настройками.

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
  }
}

Приведенный выше скрипт инструктирует каску по этим трем важным правилам.

  • Сети: этот блок содержит настройки для выбранных вами сетей. При развертывании вам потребуется указать сеть для доставки смарт-контрактов.
  • Solidity: здесь описывается версия компилятора, которую будет использовать каска для компиляции кодов смарт-контрактов в bytecodes и abi.< /li>
  • Пути: это просто информирует hardhat о расположении ваших смарт-контрактов, а также о местоположении для дампа вывода компилятора, который является ABI.

Настройка сценария развертывания

Перейдите в папку scripts, а затем в файл deploy.js и вставьте в него приведенный ниже код. Если вы не можете найти папку сценария, создайте ее, создайте файл deploy.js и вставьте в него следующий код.

const { ethers } = require('hardhat')
const fs = require('fs')

async function main() {
  const Contract = await ethers.getContractFactory('BlueVotes')
  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
})

При запуске в качестве команды Hardhat приведенный выше сценарий развернет смарт-контракт **BlueVotes.sol** в любой сети по вашему выбору.

Следуя приведенным выше инструкциям, откройте терминал, указывающий на этот проект, и запустите перечисленные ниже команды отдельно на двух терминалах. Вы можете сделать это прямо из своего редактора в VS Code.

Посмотрите на команду ниже.

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

Если предыдущие команды были успешно выполнены, вы должны увидеть следующую активность на своем терминале. Пожалуйста, смотрите изображение ниже.

Activities of Deployment on the Terminal

Если вам нужна дополнительная помощь по настройке Hardhat или развертыванию полнофункционального DApp, посмотрите это видео.

https://www.youtube.com/watch?v=hsec2erdLOI& ;&t=588s?embedable=true

Служебный файл блокчейна

Теперь, когда мы завершили предыдущие настройки, давайте создадим смарт-контракт для этой сборки. Создайте новую папку с именем **contracts** в каталоге **src** вашего проекта.

Создайте новый файл с именем **BlueVotes.sol** в этой папке с контрактами; этот файл будет содержать всю логику, управляющую действиями смарт-контракта. Скопируйте, вставьте и сохраните следующие коды в файл **BlueVotes.sol**. См. полный код ниже.

//SPDX-License-Identifier: MIT
pragma solidity >=0.7.0 <0.9.0;

contract BlueVotes {
    struct PollStruct {
        uint id;
        string image;
        string title;
        string description;
        uint votes;
        uint contestants;
        bool deleted;
        address director;
        uint startsAt;
        uint endsAt;
        uint timestamp;
    }

    struct VoterStruct {
        uint id;
        string image;
        string fullname;
        address voter;
        uint votes;
        address[] voters;
    }

    uint totalPolls;
    uint totalUsers;
    PollStruct[] polls;

    mapping(address => VoterStruct) public users;
    mapping(uint =>  mapping(address => bool)) voted;
    mapping(uint =>  mapping(address => bool)) contested;
    mapping(uint =>  VoterStruct[]) contestantsIn;
    mapping(uint =>  bool) pollExist;

    event Voted (
        string fullname,
        address indexed voter,
        uint timestamp
    );

    modifier userOnly() {
        require(users[msg.sender].voter == msg.sender, "You've gotta register first");
        _;
    }

    function createPoll(
        string memory image,
        string memory title,
        string memory description,
        uint startsAt,
        uint endsAt
    ) public userOnly {
        require(bytes(title).length > 0, "Title cannot be empty");
        require(bytes(description).length > 0, "Description cannot be empty");
        require(bytes(image).length > 0, "Image URL cannot be empty");
        require(startsAt > 0 && endsAt > startsAt, "End date must be greater than start date");

        PollStruct memory poll;
        poll.id = totalPolls++;
        poll.title = title;
        poll.description = description;
        poll.image = image;
        poll.startsAt = startsAt;
        poll.endsAt = endsAt;
        poll.director = msg.sender;
        poll.timestamp = block.timestamp;

        polls.push(poll);
        pollExist[poll.id] = true;
    }

    function updatePoll(
        uint id,
        string memory image,
        string memory title,
        string memory description,
        uint startsAt,
        uint endsAt
    ) public userOnly {
        require(pollExist[id], "Poll not found");
        require(polls[id].director == msg.sender, "Unauthorized entity");
        require(bytes(title).length > 0, "Title cannot be empty");
        require(bytes(description).length > 0, "Description cannot be empty");
        require(bytes(image).length > 0, "Image URL cannot be empty");
        require(!polls[id].deleted, "Polling already started");
        require(endsAt > startsAt, "End date must be greater than start date");

        polls[id].title = title;
        polls[id].description = description;
        polls[id].startsAt = startsAt;
        polls[id].endsAt = endsAt;
        polls[id].image = image;
    }

    function deletePoll(uint id) public userOnly {
        require(pollExist[id], "Poll not found");
        require(polls[id].director == msg.sender, "Unauthorized entity");
        polls[id].deleted = true;
    }

    function getPoll(uint id) public view returns (PollStruct memory) {
        return polls[id];
    }

    function getPolls() public view returns (PollStruct[] memory) {
        return polls;
    }

    function register(
        string memory image,
        string memory fullname
    ) public {
        VoterStruct memory user;
        user.id = totalUsers++;
        user.image = image;
        user.fullname = fullname;
        user.voter = msg.sender;
        users[msg.sender] = user;
    }

    function contest(uint id) public userOnly {
        require(pollExist[id], "Poll not found");
        require(!contested[id][msg.sender], "Already contested");

        VoterStruct memory user = users[msg.sender];
        contestantsIn[id].push(user);
        contested[id][msg.sender] = true;
        polls[id].contestants++;
    }

    function listContestants(uint id) public view returns (VoterStruct[] memory) {
        require(pollExist[id], "Poll not found");
        return contestantsIn[id];
    }

    function vote(uint id, uint cid) public userOnly {
        require(pollExist[id], "Poll not found");
        require(!voted[id][msg.sender], "Already voted");
        require(!polls[id].deleted, "Polling already started");
        require(polls[id].endsAt > polls[id].startsAt, "End date must be greater than start date");

        polls[id].votes++;
        contestantsIn[id][cid].votes++;
        contestantsIn[id][cid].voters.push(msg.sender);
        voted[id][msg.sender] = true;

        emit Voted (
            users[msg.sender].fullname,
            msg.sender,
            block.timestamp
        );
    }
}

Теперь давайте рассмотрим некоторые детали того, что происходит в приведенном выше смарт-контракте. У нас есть следующие предметы:

Структуры

  • PollStruct: описывает содержание каждого опроса, созданного на нашей платформе.
  • VoterStruct: моделирует информацию об избирателе, пользователе или участнике на платформе.

Переменные состояния

  • TotalPolls: отслеживает количество опросов, созданных в смарт-контракте.
  • TotalUsers: общее количество пользователей, зарегистрированных на платформе.

Сопоставления

  • Пользователи. Сопоставляет адреса пользователей с их соответствующими данными с помощью VoterStruct.
  • Проголосовали: здесь отслеживается статус голосования каждого пользователя в разных опросах.
  • Оспорено: здесь указывается, участвовал ли участник в конкретном опросе или нет.
  • ContestantsIn: содержит данные для каждого участника данного опроса.
  • PollExist: проверяет, существует ли определенный идентификатор опроса на платформе.

События и модификаторы

  • Voted: выдает информацию о текущем пользователе, который проголосовал.
  • UserOnly: этот модификатор предотвращает доступ незарегистрированных пользователей к несанкционированным функциям.

Функции опроса

  • CreatePoll: получает данные об опросе от зарегистрированного пользователя и создает опрос после проверки соответствия информации стандартам.
  • UpdatePoll: эта функция изменяет содержимое определенного опроса, если вызывающий абонент является создателем опроса и существует идентификатор опроса.
  • DeletePoll: эта функция позволяет создателю опроса установить для удаленного ключа значение true, тем самым сделав опрос недоступным для распространения.
  • GetPolls: возвращаются все опросы, созданные каждым пользователем на платформе.
  • GetPoll: возвращает информацию об определенном опросе с нашей платформы.

Функции, ориентированные на пользователя

  • Регистрация. Эта функция позволяет пользователю зарегистрироваться, указав свое полное имя и изображение аватара.
  • Конкурс: эта функция дает зарегистрированному пользователю возможность стать участником данного опроса, если опрос еще не начался.
  • ListContestants: эта функция выводит список всех участников, участвовавших в конкретном опросе.
  • Голосование: эта функция позволяет пользователю голосовать за одного участника в каждом опросе в течение периода, установленного для голосования.

Хотите улучшить свои знания о смарт-контрактах? Посмотрите это видео, чтобы узнать, как использовать Hardhat для разработки смарт-контрактов на основе тестирования.

https://www.youtube.com/watch?v=RadB8dan0Bs& ;&t=744s?embedable=true

Разработка внешнего интерфейса

Теперь, когда у нас есть наш смарт-контракт в сети и все наши артефакты (байт-коды и ABI) сгенерированы, давайте пошагово создадим внешний интерфейс с помощью React.

Компоненты

В каталоге src создайте новую папку с именем **components** для размещения всех компонентов React.

Компонент заголовка

Header Component

Этот компонент четко отображает только две части информации: логотип веб-сайта и кнопку подключенной учетной записи. См. приведенные ниже коды.

import { Link } from 'react-router-dom'
import { connectWallet } from '../Blockchain.services'
import { truncate, useGlobalState } from '../store'

const Header = () => {
  const [connectedAccount] = useGlobalState('connectedAccount')

  return (
    <div className=" flex justify-between items-center p-5 shadow-md shadow-gray-300 ">
      <Link to="/" className="font-bold text-2xl">
        <span className="text-blue-700">Blue</span>Votes
      </Link>

      {connectedAccount ? (
        <button
          type="button"
          className="inline-block px-6 py-2.5 bg-blue-600 text-white font-medium
          text-xs leading-tight rounded shadow-md hover:bg-blue-700
          hover:shadow-lg focus:bg-blue-700 focus:shadow-lg focus:outline-none
          focus:ring-0 active:bg-blue-800 active:shadow-lg transition duration-150 ease-in-out"
        >
          {truncate(connectedAccount, 4, 4, 11)}
        </button>
      ) : (
        <button
          type="button"
          className="inline-block px-6 py-2.5 bg-blue-600 text-white font-medium
          text-xs leading-tight rounded shadow-md hover:bg-blue-700
          hover:shadow-lg focus:bg-blue-700 focus:shadow-lg focus:outline-none
          focus:ring-0 active:bg-blue-800 active:shadow-lg transition duration-150 ease-in-out"
          onClick={connectWallet}
        >
          Connect Wallet
        </button>
      )}
    </div>
  )
}

export default Header

Основной компонент

Hero Component

Компонент героя позволяет запускать другие компоненты, такие как компоненты регистрации и создания опроса. Этот компонент также позволяет пользователю войти в свою учетную запись, если она уже зарегистрирована, с помощью CometChat SDK. См. приведенные ниже коды.

import { toast } from 'react-toastify'
import { loginWithCometChat } from '../Chat.services'
import { setGlobalState, useGlobalState } from '../store/index'

const Hero = () => {
  const [user] = useGlobalState('user')
  const [currentUser] = useGlobalState('currentUser')
  const [connectedAccount] = useGlobalState('connectedAccount')

  const handleSubmit = async () => {
    await toast.promise(
      new Promise(async (resolve, reject) => {
        await loginWithCometChat()
          .then(() => resolve())
          .catch(() => reject())
      }),
      {
        pending: 'Signing in...',
        success: 'Logged in successful 👌',
        error: 'Encountered error 🤯',
      },
    )
  }

  return (
    <div className="text-center mt-10 p-4">
      <h1 className="text-5xl text-black-600 font-bold">
        {' '}
        Vote Without <span className="text-blue-600">Rigging</span>
      </h1>
      <p className="pt-5 text-gray-600 text-xl font-medium">
        {' '}
        This online voting system offers the highest level of transparency,
        control, security <br></br>and efficiency of election processes using
        the <strong>Blockchain Technology</strong>{' '}
      </p>
      <div className="flex justify-center pt-10">
        {user?.fullname ? (
          <div className="space-x-2">
            {!currentUser ? (
              <button
                type="button"
                className="inline-block px-6 py-2.5 bg-transparent text-blue-600 font-medium text-xs
                leading-tight uppercase rounded shadow-md hover:bg-blue-700 hover:shadow-lg hover:text-white
                focus:bg-blue-700 focus:shadow-lg focus:outline-none focus:ring-0 active:bg-blue-800
                active:shadow-lg transition duration-150 ease-in-out border border-blue-600"
                onClick={handleSubmit}
              >
                Login
              </button>
            ) : (
              <button
                type="button"
                className="inline-block px-6 py-2.5 bg-blue-600 text-white font-medium text-xs
                leading-tight uppercase rounded shadow-md hover:bg-blue-700 hover:shadow-lg
                focus:bg-blue-700 focus:shadow-lg focus:outline-none focus:ring-0 active:bg-blue-800
                active:shadow-lg transition duration-150 ease-in-out border border-blue-600"
                onClick={() => setGlobalState('createPollModal', 'scale-100')}
              >
                Create Poll
              </button>
            )}
          </div>
        ) : (
          <button
            type="button"
            className="inline-block px-6 py-2 border-2 border-blue-600 text-blue-600 font-medium
            text-xs leading-tight uppercase rounded hover:bg-black hover:bg-opacity-5 focus:outline-none
            focus:ring-0 transition duration-150 ease-in-out"
            onClick={() => setGlobalState('contestModal', 'scale-100')}
            disabled={!connectedAccount}
            title={!connectedAccount ? 'Please connect wallet first' : null}
          >
            Register
          </button>
        )}
      </div>
    </div>
  )
}

export default Hero

Компонент опросов

Polls Component

Этот компонент собирает все активные опросы и отображает их на нашей платформе. Внутри этого компонента также находится один компонент, отвечающий за рендеринг каждого конкретного опроса. См. приведенные ниже коды.

import { useEffect, useState } from 'react'
import Moment from 'react-moment'
import { useNavigate } from 'react-router-dom'
import { truncate } from '../store'

const Polls = ({ polls }) => {
  const [end, setEnd] = useState(4)
  const [count] = useState(4)
  const [collection, setCollection] = useState([])

  const getCollection = () => {
    return polls.slice(0, end)
  }

  useEffect(() => {
    setCollection(getCollection())
  }, [polls, end])

  return (
    <div className="pt-10">
      <div
        className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3
        xl:grid-cols-4 gap-6 md:gap-4 lg:gap-4 xl:gap-3 py-2.5 w-4/5
        mx-auto"
      >
        {collection.map((poll, i) =>
          poll?.deleted ? null : <Poll key={i} poll={poll} />,
        )}
      </div>

      {collection.length > 0 && polls.length > collection.length ? (
        <div className=" flex justify-center mt-20">
          <button
            type="button"
            className=" inline-block px-6 py-2.5 bg-blue-600 text-white font-medium text-xs
            leading-tight uppercase rounded shadow-md hover:bg-blue-700 hover:shadow-lg
            focus:bg-blue-700 focus:shadow-lg focus:outline-none focus:ring-0 active:bg-blue-800
            active:shadow-lg transition duration-150 ease-in-out"
            onClick={() => setEnd(end + count)}
          >
            Load More
          </button>
        </div>
      ) : null}
    </div>
  )
}

const Poll = ({ poll }) => {
  const navigate = useNavigate()

  return (
    <div className="flex justify-center">
      <div className="rounded-lg shadow-lg bg-white max-w-sm">
        <img
          className="rounded-t-lg object-cover h-48 w-full"
          src={poll.image}
          alt={poll.title}
        />
        <div className="p-6">
          <h5 className="text-gray-900 text-xl font-medium">{poll.title}</h5>
          <small className="font-bold mb-4 text-xs">
            {new Date().getTime() > Number(poll.startsAt + '000') &&
            Number(poll.endsAt + '000') > Number(poll.startsAt + '000') ? (
              <span className="text-green-700">Started</span>
            ) : new Date().getTime() > Number(poll.endsAt + '000') ? (
              <Moment className="text-red-700" unix format="ddd DD MMM, YYYY">
                {poll.endsAt}
              </Moment>
            ) : (
              <Moment className="text-gray-500" unix format="ddd DD MMM, YYYY">
                {poll.startsAt}
              </Moment>
            )}
          </small>
          <p className="text-gray-700 text-base mb-4">
            {truncate(poll.description, 100, 0, 103)}
          </p>
          <button
            type="button"
            className=" inline-block px-6 py-2.5 bg-blue-600 text-white font-medium text-xs
          leading-tight uppercase rounded shadow-md hover:bg-blue-700 hover:shadow-lg
          focus:bg-blue-700 focus:shadow-lg focus:outline-none focus:ring-0 active:bg-blue-800
          active:shadow-lg transition duration-150 ease-in-out"
            onClick={() => navigate('/polls/' + poll.id)}
          >
            Enter
          </button>
        </div>
      </div>
    </div>
  )
}

export default Polls

Компонент CreatePoll

Create Poll Component

Этот компонент используется для создания новых опросов на нашей платформе. Для создания опроса требуется важная информация, такая как название опроса, описание, URL-адрес изображения, а также даты начала и окончания. См. приведенные ниже коды.

import { setGlobalState, useGlobalState } from '../store'
import { FaTimes } from 'react-icons/fa'
import { useState } from 'react'
import { createPoll } from '../Blockchain.services'
import { toast } from 'react-toastify'

const CreatePoll = () => {
  const [createPollModal] = useGlobalState('createPollModal')
  const [title, setTitle] = useState('')
  const [startsAt, setStartsAt] = useState('')
  const [endsAt, setEndsAt] = useState('')
  const [description, setDescription] = useState('')
  const [image, setImage] = useState('')

  const closeModal = () => {
    setGlobalState('createPollModal', 'scale-0')
  }

  const toTimestamp = (strDate) => {
    const datum = Date.parse(strDate)
    return datum / 1000
  }

  const handleSubmit = async (e) => {
    e.preventDefault()

    if (!title || !image || !startsAt || !endsAt || !description) return

    const params = {
      title,
      image,
      startsAt: toTimestamp(startsAt),
      endsAt: toTimestamp(endsAt),
      description,
    }

    await toast.promise(
      new Promise(async (resolve, reject) => {
        await createPoll(params)
          .then(() => resolve())
          .catch(() => reject())
      }),
      {
        pending: 'Approve transaction...',
        success: 'Created, will reflect within 30sec 👌',
        error: 'Encountered error 🤯',
      },
    )
    closeModal()
    resetForm()
  }

  const resetForm = () => {
    setTitle('')
    setImage('')
    setDescription('')
    setStartsAt('')
    setEndsAt('')
  }

  return (
    <div
      className={`fixed top-0 left-0 w-screen h-screen flex items-center z-50
      justify-center bg-black bg-opacity-50 transform transition-transform
      duration-300 ${createPollModal}`}
    >
      <div className="bg-white shadow-xl shadow-black 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 text-black">Add New Poll</p>
            <button
              type="button"
              onClick={closeModal}
              className="border-0 bg-transparent focus:outline-none"
            >
              <FaTimes className="text-black" />
            </button>
          </div>

          {image ? (
            <div className="flex flex-row justify-center items-center rounded-xl mt-5">
              <div className="shrink-0 rounded-xl overflow-hidden h-20 w-20">
                <img
                  alt="Contestant"
                  className="h-full w-full object-cover cursor-pointer"
                  src={image}
                />
              </div>
            </div>
          ) : null}

          <div className="flex flex-row justify-between items-center bg-gray-300 rounded-xl mt-5">
            <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="Title"
              onChange={(e) => setTitle(e.target.value)}
              value={title}
              required
            />
          </div>

          <div className="flex flex-row justify-between items-center bg-gray-300 rounded-xl mt-5">
            <input
              className="block w-full text-sm
            text-slate-500 bg-transparent border-0
              focus:outline-none focus:ring-0"
              type="date"
              name="date"
              placeholder="Date"
              onChange={(e) => setStartsAt(e.target.value)}
              value={startsAt}
              required
            />
          </div>

          <div className="flex flex-row justify-between items-center bg-gray-300 rounded-xl mt-5">
            <input
              className="block w-full text-sm
            text-slate-500 bg-transparent border-0
              focus:outline-none focus:ring-0"
              type="date"
              name="date"
              placeholder="Date"
              onChange={(e) => setEndsAt(e.target.value)}
              value={endsAt}
              required
            />
          </div>

          <div className="flex flex-row justify-between items-center bg-gray-300 rounded-xl mt-5">
            <input
              className="block w-full text-sm
                text-slate-500 bg-transparent border-0
                focus:outline-none focus:ring-0"
              type="url"
              name="image"
              placeholder="Image URL"
              onChange={(e) => setImage(e.target.value)}
              pattern="^(http(s)?://)+[w-._~:/?#[]@!$&'()*+,;=.]+$"
              value={image}
              required
            />
          </div>

          <div className="flex flex-row justify-between items-center bg-gray-300 rounded-xl mt-5">
            <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"
              onChange={(e) => setDescription(e.target.value)}
              value={description}
              required
            ></textarea>
          </div>

          <button
            type="submit"
            className="flex flex-row justify-center items-center
              w-full text-white text-md bg-blue-500
              py-2 px-5 rounded-full drop-shadow-xl
              border-transparent border
              hover:bg-transparent hover:text-blue-500
              hover:border hover:border-blue-500
              focus:outline-none focus:ring mt-5"
          >
            Create Poll
          </button>
        </form>
      </div>
    </div>
  )
}

export default CreatePoll

Обновить компонент опроса

Update Component

Этот компонент использует тот же подход, что и компонент создания опроса. См. приведенные ниже коды.

import { setGlobalState, useGlobalState, toDate } from '../store'
import { FaTimes } from 'react-icons/fa'
import { useEffect, useState } from 'react'
import { updatePoll } from '../Blockchain.services'
import { toast } from 'react-toastify'

const UpdatePoll = () => {
  const [updatePollModal] = useGlobalState('updatePollModal')
  const [poll] = useGlobalState('poll')
  const [title, setTitle] = useState('')
  const [startsAt, setStartsAt] = useState('')
  const [endsAt, setEndsAt] = useState('')
  const [description, setDescription] = useState('')
  const [image, setImage] = useState('')

  useEffect(() => {
    setTitle(poll?.title)
    setDescription(poll?.description)
    setImage(poll?.image)
    setStartsAt(toDate(poll?.startsAt.toNumber() * 1000))
    setEndsAt(toDate(poll?.endsAt.toNumber() * 1000))
  }, [poll])

  const closeModal = () => {
    setGlobalState('updatePollModal', 'scale-0')
  }

  const toTimestamp = (strDate) => {
    const datum = Date.parse(strDate)
    return datum / 1000
  }

  const handleSubmit = async (e) => {
    e.preventDefault()

    if (!title || !image || !startsAt || !endsAt || !description) return

    const params = {
      id: poll?.id,
      title,
      image,
      startsAt: toTimestamp(startsAt),
      endsAt: toTimestamp(endsAt),
      description,
    }

    await toast.promise(
      new Promise(async (resolve, reject) => {
        await updatePoll(params)
          .then(() => resolve())
          .catch(() => reject())
      }),
      {
        pending: 'Approve transaction...',
        success: 'Updated, will reflect within 30sec 👌',
        error: 'Encountered error 🤯',
      },
    )
    closeModal()
  }

  return (
    <div
      className={`fixed top-0 left-0 w-screen h-screen flex items-center z-50
      justify-center bg-black bg-opacity-50 transform transition-transform
      duration-300 ${updatePollModal}`}
    >
      <div className="bg-white shadow-xl shadow-black 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 text-black">Edit Poll</p>
            <button
              type="button"
              onClick={closeModal}
              className="border-0 bg-transparent focus:outline-none"
            >
              <FaTimes className="text-black" />
            </button>
          </div>

          {image ? (
            <div className="flex flex-row justify-center items-center rounded-xl mt-5">
              <div className="shrink-0 rounded-xl overflow-hidden h-20 w-20">
                <img
                  alt="Contestant"
                  className="h-full w-full object-cover cursor-pointer"
                  src={image}
                />
              </div>
            </div>
          ) : null}

          <div className="flex flex-row justify-between items-center bg-gray-300 rounded-xl mt-5">
            <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="Title"
              onChange={(e) => setTitle(e.target.value)}
              value={title || ''}
              required
            />
          </div>

          <div className="flex flex-row justify-between items-center bg-gray-300 rounded-xl mt-5">
            <input
              className="block w-full text-sm
            text-slate-500 bg-transparent border-0
              focus:outline-none focus:ring-0"
              type="date"
              name="date"
              placeholder="Date"
              onChange={(e) => setStartsAt(e.target.value)}
              value={startsAt || ''}
              required
            />
          </div>

          <div className="flex flex-row justify-between items-center bg-gray-300 rounded-xl mt-5">
            <input
              className="block w-full text-sm
            text-slate-500 bg-transparent border-0
              focus:outline-none focus:ring-0"
              type="date"
              name="date"
              placeholder="Date"
              onChange={(e) => setEndsAt(e.target.value)}
              value={endsAt || ''}
              required
            />
          </div>

          <div className="flex flex-row justify-between items-center bg-gray-300 rounded-xl mt-5">
            <input
              className="block w-full text-sm
                text-slate-500 bg-transparent border-0
                focus:outline-none focus:ring-0"
              type="url"
              name="image"
              placeholder="Image URL"
              onChange={(e) => setImage(e.target.value)}
              pattern="^(http(s)?://)+[w-._~:/?#[]@!$&'()*+,;=.]+$"
              value={image || ''}
              required
            />
          </div>

          <div className="flex flex-row justify-between items-center bg-gray-300 rounded-xl mt-5">
            <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"
              onChange={(e) => setDescription(e.target.value)}
              value={description || ''}
              required
            ></textarea>
          </div>

          <button
            type="submit"
            className="flex flex-row justify-center items-center
              w-full text-white text-md bg-blue-500
              py-2 px-5 rounded-full drop-shadow-xl
              border-transparent border
              hover:bg-transparent hover:text-blue-500
              hover:border hover:border-blue-500
              focus:outline-none focus:ring mt-5"
          >
            Update Poll
          </button>
        </form>
      </div>
    </div>
  )
}

export default UpdatePoll

Компонент DeletePoll

Delete Poll Component

Этот компонент просто позволяет вам удалить существующий опрос из списка. После удаления опрос будет удален из обращения. См. код ниже.

import { FaTimes } from 'react-icons/fa'
import { useNavigate } from 'react-router-dom'
import { toast } from 'react-toastify'
import { deletePoll } from '../Blockchain.services'
import { setGlobalState, useGlobalState } from '../store'

const DeletePoll = () => {
  const navigate = useNavigate()
  const [poll] = useGlobalState('poll')
  const [deletePollModal] = useGlobalState('deletePollModal')

  const handleSubmit = async (e) => {
    e.preventDefault()

    await toast.promise(
      new Promise(async (resolve, reject) => {
        await deletePoll(poll.id)
          .then(() => resolve())
          .catch(() => reject())
      }),
      {
        pending: 'Approve transaction...',
        success: 'Deleted, will reflect within 30sec 👌',
        error: 'Encountered error 🤯',
      },
    )

    setGlobalState('deletePollModal', 'scale-0')
    console.log('Poll Deleted!')
    navigate('/')
  }

  const closeModal = () => {
    setGlobalState('deletePollModal', '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 ${deletePollModal}`}
    >
      <div className="bg-white shadow-xl shadow-black rounded-xl w-11/12 md:w-2/5 h-7/12 p-6">
        <form className="flex flex-col">
          <div className="flex flex-row justify-between items-center">
            <p className="font-semibold text-black">#{poll?.title}</p>
            <button
              type="button"
              onClick={closeModal}
              className="border-0 bg-transparent focus:outline-none"
            >
              <FaTimes className="text-black" />
            </button>
          </div>

          <div className="flex flex-row justify-center items-center rounded-xl mt-5">
            <div className="shrink-0 rounded-xl overflow-hidden h-20 w-20">
              <img
                alt="Project"
                className="h-full w-full object-cover cursor-pointer"
                src={poll?.image}
              />
            </div>
          </div>

          <div className="flex flex-col justify-center items-center mt-5">
            <p>Are you sure?</p>
            <small className="text-red-400">This is irriversible!</small>
          </div>

          <button
            type="submit"
            onClick={handleSubmit}
            className="flex flex-row justify-center items-center w-full 
              text-white text-md bg-red-500
              py-2 px-5 rounded-full drop-shadow-xl
              border-transparent border
              hover:bg-transparent hover:text-red-500
              hover:border hover:border-red-500
              focus:outline-none focus:ring mt-5"
          >
            Delete Poll
          </button>
        </form>
      </div>
    </div>
  )
}

export default DeletePoll

Компонент сообщений

The Message Component

Используя CometChat SDK, этот компонент отвечает за отображение набора сообщений группового чата для каждого опроса. Посмотрите на код ниже.

import { useEffect, useState } from 'react'
import Identicon from 'react-identicons'
import { CometChat, getMessages, sendMessage } from '../Chat.services'
import { truncate, useGlobalState } from '../store'
const Messages = ({ guid }) => {
  const [message, setMessage] = useState('')
  const [messages, setMessages] = useState([])
  const [connectedAccount] = useGlobalState('connectedAccount')

  useEffect(() => {
    getMessages(guid).then((msgs) => {
      if (!!!msgs.code)
        setMessages(msgs.filter((msg) => msg.category == 'message'))
    })

    listenForMessage(guid)
  }, [guid])

  const listenForMessage = (listenerID) => {
    CometChat.addMessageListener(
      listenerID,
      new CometChat.MessageListener({
        onTextMessageReceived: (message) => {
          setMessages((prevState) => [...prevState, message])
          scrollToEnd()
        },
      }),
    )
  }

  const handleMessage = async (e) => {
    e.preventDefault()
    await sendMessage(guid, message).then((msg) => {
      if (!!!msg.code) {
        setMessages((prevState) => [...prevState, msg])
        setMessage('')
        scrollToEnd()
      }
    })
  }

  const scrollToEnd = () => {
    const elmnt = document.getElementById('messages-container')
    elmnt.scrollTop = elmnt.scrollHeight
  }

  return (
    <div
      className="w-full mx-auto rounded-lg py-4 px-6 my-2
    bg-white shadow-lg"
    >
      <div
        id="messages-container"
        className="w-full h-[calc(100vh_-_30rem)] overflow-y-auto"
      >
        {messages.map((msg, i) => (
          <Message
            key={i}
            message={msg.text}
            timestamp={new Date().toDateString()}
            owner={msg.sender.uid}
            isOwner={msg.sender.uid == connectedAccount}
          />
        ))}
      </div>

      <form onSubmit={handleMessage} className="flex w-full">
        <input
          className="w-full bg-gray-200 rounded-lg p-4 
          focus:ring-0 focus:outline-none border-gray-500"
          type="text"
          placeholder="Write a message..."
          value={message}
          onChange={(e) => setMessage(e.target.value)}
          required
        />
        <button type="submit" hidden>
          Send
        </button>
      </form>
    </div>
  )
}

const Message = ({ message, timestamp, owner, isOwner }) => (
  <div className="flex flex-row justify-start w-2/5 my-2">
    <div className="flex justify-center items-end space-x-2">
      <div className="flex flex-col">
        <div className="flex justify-start items-center space-x-1">
          <div className="flex justify-start items-center space-x-1">
            <Identicon
              string={owner}
              size={20}
              className="h-10 w-10 object-contain rounded-full"
            />
            <span className="font-bold text-xs">
              {isOwner ? '@You' : truncate(owner, 4, 4, 11)}
            </span>
          </div>
          <span className="text-gray-800 text-[10px]">{timestamp}</span>
        </div>
        <small className="leading-tight text-md my-1">{message}</small>
      </div>
    </div>
  </div>
)

export default Messages

Компонент нижнего колонтитула

Footer Component

Это простой компонент, который существенно улучшает эстетику и дизайн нашего приложения. Посмотрите на код ниже.

import { AiFillGithub } from 'react-icons/ai'
import { CgYoutube } from 'react-icons/cg'
import { FaTwitter } from 'react-icons/fa'
import { GrLinkedin } from 'react-icons/gr'

const Footer = () => {
  return (
    <footer className="text-center bg-gray-900 text-white mt-20">
      <div className="flex justify-center items-center mx-auto space-x-3 p-4">
        <a
          href="https://www.linkedin.com/in/darlington-gospel-aa626b125/"
          target="_blank"
          type="button"
          className="rounded-full text-white leading-normal uppercase hover:bg-black
          hover:bg-opacity-5 focus:outline-none focus:ring-0
          transition duration-150 ease-in-out w-9 h-9"
        >
          <GrLinkedin size={35} />
        </a>
        <a
          href="https://www.youtube.com/@dappmentors?sub_confirmation=1"
          target="_blank"
          type="button"
          className="rounded-full text-white leading-normal uppercase hover:bg-black
          hover:bg-opacity-5 focus:outline-none focus:ring-0
          transition duration-150 ease-in-out w-9 h-9"
        >
          <CgYoutube size={35} />
        </a>

        <a
          href="https://github.com/Daltonic"
          target="_blank"
          type="button"
          className="rounded-full text-white leading-normal uppercase hover:bg-black
          hover:bg-opacity-5 focus:outline-none focus:ring-0
          transition duration-150 ease-in-out w-9 h-9"
        >
          <AiFillGithub size={35} />
        </a>
        <a
          href="https://twitter.com/iDaltonic"
          target="_blank"
          type="button"
          className="rounded-full text-white leading-normal uppercase hover:bg-black
          hover:bg-opacity-5 focus:outline-none focus:ring-0
          transition duration-150 ease-in-out w-9 h-9"
        >
          <FaTwitter size={35} />
        </a>
      </div>
      <div
        className="flex flex-col justify-center items-center text-center p-4"
        style={{ backgroundColor: 'rgba(0, 0, 0, 0.2)' }}
      >
        © 2022
        <a
          className="flex space-x-2 text-whitehite"
          href="https://daltonic.github.io/"
        >
          <span>With Love ❤️</span>
          <span className="text-orange-700">Daltonic</span>
        </a>
      </div>
    </footer>
  )
}

export default Footer

Просмотры

В каталоге **src** создайте новую папку с именем **views** и создайте в ней следующие компоненты один за другим.< /p>

Главная страница

Home Page

Эта страница объединяет компоненты героя и опроса в одном красивом интерфейсе. См. фрагмент кода ниже.

import Hero from '../components/Hero'
import Polls from '../components/Polls'
import { useGlobalState } from '../store'

const Home = () => {
  const [polls] = useGlobalState('polls')

  return (
    <div>
      <Hero />
      <Polls polls={polls.filter((poll) => !poll.deleted)} />
    </div>
  )
}

export default Home

Страница голосования

VotesPage

Эта страница содержит кнопки для запуска компонентов редактирования или удаления опроса, а также дает возможность оспаривать, голосовать или общаться с другими избирателями. См. приведенные ниже коды.

import { useEffect, useState } from 'react'
import { toast } from 'react-toastify'
import { useNavigate, useParams } from 'react-router-dom'
import { getPoll, contest, listContestants, vote } from '../Blockchain.services'
import { useGlobalState, setGlobalState, truncate } from '../store'
import Moment from 'react-moment'
import Identicon from 'react-identicons'
import Messages from '../components/Messages'
import { createNewGroup, getGroup, joinGroup } from '../Chat.services'

const Vote = () => {
  const { id } = useParams()
  const navigate = useNavigate()
  const [poll] = useGlobalState('poll')
  const [connectedAccount] = useGlobalState('connectedAccount')
  const [currentUser] = useGlobalState('currentUser')
  const [contestants] = useGlobalState('contestants')
  const [group, setGroup] = useState(null)

  const handleContest = async () => {
    await toast.promise(
      new Promise(async (resolve, reject) => {
        await contest(id)
          .then(() => resolve())
          .catch(() => reject())
      }),
      {
        pending: 'Approve transaction...',
        success: 'Contested, will reflect within 30sec 👌',
        error: 'Encountered error 🤯',
      },
    )
  }

  const handCreateGroup = async () => {
    await toast.promise(
      new Promise(async (resolve, reject) => {
        await createNewGroup(`pid_${id}`, poll?.title)
          .then(() => resolve())
          .catch(() => reject())
      }),
      {
        pending: 'Creating...',
        success: 'Chat group successfully created. 👌',
        error: 'Encountered error 🤯',
      },
    )
  }

  const handleGroup = async () => {
    await getGroup(`pid_${id}`).then(async (res) => {
      if (!res.code && !res.hasJoined) {
        await joinGroup(`pid_${id}`)
        setGroup(res)
      } else if (!res.code) {
        setGroup(res)
      }
    })
  }

  useEffect(async () => {
    await getPoll(id)
    await listContestants(id)
    if (!currentUser) {
      toast('Please, register and login in first...')
      navigate('/')
    }
    await handleGroup()
  }, [])

  return (
    <div className="w-full md:w-4/5 mx-auto p-4">
      <div className="text-center my-5">
        <img
          className="w-full h-40 object-cover mb-4"
          src={poll?.image}
          alt={poll?.title}
        />
        <h1 className="text-5xl text-black-600 font-bold">{poll?.title}</h1>
        <p className="pt-5 text-gray-600 text-xl font-medium">
          {poll?.description}
        </p>

        <div className="flex justify-center items-center space-x-2 my-2 text-sm">
          <Moment className="text-gray-500" unix format="ddd DD MMM, YYYY">
            {poll?.startsAt}
          </Moment>
          <span> - </span>
          <Moment className="text-gray-500" unix format="ddd DD MMM, YYYY">
            {poll?.endsAt}
          </Moment>
        </div>

        <div className="flex justify-center items-center space-x-2 text-sm">
          <Identicon
            string={poll?.director}
            size={25}
            className="h-10 w-10 object-contain rounded-full"
          />
          <span className="font-bold">
            {poll?.director ? truncate(poll?.director, 4, 4, 11) : '...'}
          </span>
        </div>

        <div className="flex justify-center items-center space-x-2 my-2 text-sm">
          <span className="text-gray-500">{poll?.votes} Votes</span>
          <span className="text-gray-500">{poll?.contestants} Contestants</span>
        </div>

        <div className="flex justify-center my-3">
          {new Date().getTime() >
          Number(poll?.startsAt + '000') ? null : poll?.deleted ? null : (
            <div className="flex space-x-2">
              <button
                type="button"
                className="inline-block px-6 py-2 border-2 border-blue-600 text-blue-600
                font-medium text-xs leading-tight uppercase rounded hover:bg-black hover:bg-opacity-5
                focus:outline-none focus:ring-0 transition duration-150 ease-in-out"
                onClick={handleContest}
              >
                Contest
              </button>

              {connectedAccount == poll?.director ? (
                <>
                  {!group ? (
                    <button
                      type="button"
                      className="inline-block px-6 py-2 border-2 border-gray-600 text-gray-600
                      font-medium text-xs leading-tight uppercase rounded hover:bg-black hover:bg-opacity-5
                      focus:outline-none focus:ring-0 transition duration-150 ease-in-out"
                      onClick={handCreateGroup}
                    >
                      Create Group
                    </button>
                  ) : null}
                  <button
                    type="button"
                    className="inline-block px-6 py-2 border-2 border-gray-600 text-gray-600
                 font-medium text-xs leading-tight uppercase rounded hover:bg-black hover:bg-opacity-5
                 focus:outline-none focus:ring-0 transition duration-150 ease-in-out"
                    onClick={() =>
                      setGlobalState('updatePollModal', 'scale-100')
                    }
                  >
                    Edit
                  </button>
                  <button
                    type="button"
                    className="inline-block px-6 py-2 border-2 border-red-600 text-red-600
                 font-medium text-xs leading-tight uppercase rounded hover:bg-black hover:bg-opacity-5
                 focus:outline-none focus:ring-0 transition duration-150 ease-in-out"
                    onClick={() =>
                      setGlobalState('deletePollModal', 'scale-100')
                    }
                  >
                    Delete
                  </button>
                </>
              ) : null}
            </div>
          )}
        </div>
      </div>

      <div className="flex flex-col w-full lg:w-3/4 mx-auto">
        <div className="flex flex-col items-center">
          {contestants.length > 0 ? (
            <h4 className="text-lg font-medium uppercase mt-6 mb-3">
              Contestants
            </h4>
          ) : null}

          {contestants.map((contestant, i) => (
            <Votee key={i} contestant={contestant} poll={poll} />
          ))}
        </div>
        {group ? (
          <div className="flex flex-col items-center">
            <h4 className="text-lg font-medium uppercase mt-6 mb-3">
              Live Chats
            </h4>
            <Messages guid={`pid_${id}`} />
          </div>
        ) : null}
      </div>
    </div>
  )
}

const Votee = ({ contestant, poll }) => {
  const handleVote = async (id, cid) => {
    await toast.promise(
      new Promise(async (resolve, reject) => {
        await vote(id, cid)
          .then(() => resolve())
          .catch(() => reject())
      }),
      {
        pending: 'Approve transaction...',
        success: 'Voted, will reflect within 30sec 👌',
        error: 'Encountered error 🤯',
      },
    )
  }

  return (
    <div className="flex justify-start w-full mx-auto rounded-lg bg-white shadow-lg my-2">
      <div>
        <img
          className="w-40 h-full object-cover rounded-lg md:rounded-none"
          src={contestant?.image}
          alt={contestant?.fullname}
        />
      </div>

      <div className="p-6 flex flex-col justify-start ">
        <p className="text-gray-700 text-base font-bold">
          {contestant?.fullname}
        </p>

        <div className="flex justify-start items-center space-x-2 text-sm my-2">
          <Identicon
            string={contestant?.voter}
            size={20}
            className="h-10 w-10 object-contain rounded-full"
          />
          <span className="font-bold">
            {truncate(contestant?.voter, 4, 4, 11)}
          </span>
        </div>

        <div className="flex justify-start items-center">
          <span className="text-gray-600 text-sm">
            {contestant?.votes} votes
          </span>
          {new Date().getTime() > Number(poll?.startsAt + '000') &&
          Number(poll?.endsAt + '000') > new Date().getTime() ? (
            <button
              type="button"
              className="inline-block px-3 py-1 border-2 border-gray-800 text-gray-800
                  font-medium text-xs leading-tight uppercase rounded-full hover:bg-black
                  hover:bg-opacity-5 focus:outline-none focus:ring-0 transition duration-150
                  ease-in-out ml-8"
              onClick={() => handleVote(poll?.id, contestant?.id)}
            >
              Vote
            </button>
          ) : null}
        </div>
      </div>
    </div>
  )
}

export default Vote

Файл App.jsx

Теперь давайте займемся файлом App.jsx, отвечающим за объединение всех наших компонентов и страниц, см. его коды ниже.

import { useEffect, useState } from 'react'
import { Routes, Route } from 'react-router-dom'
import { getPolls, getUser, isWallectConnected } from './Blockchain.services'
import { ToastContainer } from 'react-toastify'
import { checkAuthState } from './Chat.services'
import CreatePoll from './components/CreatePoll'
import DeletePoll from './components/DeletePoll'
import Footer from './components/Footer'
import Header from './components/Header'
import Register from './components/Register'
import UpdatePoll from './components/UpdatePoll'
import Home from './views/Home'
import Vote from './views/Vote'

const App = () => {
  const [loaded, setLoaded] = useState(false)
  useEffect(async () => {
    await isWallectConnected()
    await getPolls()
    await getUser()
    await checkAuthState()
    setLoaded(true)
    console.log('Blockchain loaded')
  }, [])

  return (
    <div className="min-h-screen">
      <Header />
      {loaded ? (
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/polls/:id" element={<Vote />} />
        </Routes>
      ) : null}

      <Register />
      <DeletePoll />
      <CreatePoll />
      <UpdatePoll />
      <Footer />
      <ToastContainer
        position="bottom-center"
        autoClose={5000}
        hideProgressBar={false}
        newestOnTop={false}
        closeOnClick
        rtl={false}
        pauseOnFocusLoss
        draggable
        pauseOnHover
        theme="dark"
      />
    </div>
  )
}

export default App

Другие основные услуги

Службы, перечисленные ниже, имеют решающее значение для бесперебойной работы нашего приложения.

Служба магазина Мы используем библиотеку react-hooks-global-state для создания централизованного хранилища в наше приложение, которое служит службой управления состоянием.

Создайте папку store в папке src. Затем в этой папке хранилища создайте файл с именем index.jsx, вставьте и сохраните в него следующие коды.

import { createGlobalState } from 'react-hooks-global-state'
import moment from 'moment'

const { getGlobalState, useGlobalState, setGlobalState } = createGlobalState({
  contestModal: 'scale-0',
  createPollModal: 'scale-0',
  updatePollModal: 'scale-0',
  deletePollModal: 'scale-0',
  connectedAccount: '',
  currentUser: null,
  contract: null,
  user: null,
  polls: [],
  poll: null,
  contestants: [],
  group: null
})

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 toDate = (timestamp) => {
  const date = new Date(timestamp)
  const dd = date.getDate() > 9 ? date.getDate() : `0${date.getDate()}`
  const mm =
    date.getMonth() + 1 > 9 ? date.getMonth() + 1 : `0${date.getMonth() + 1}`
  const yyyy = date.getFullYear()
  return `${yyyy}-${mm}-${dd}`
}

const toHex = (str) => {
  let result = ''
  for (let i = 0; i < str.length; i++) {
    result += str.charCodeAt(i).toString(16)
  }
  return result.slice(0, 6)
}

export {
  getGlobalState,
  useGlobalState,
  setGlobalState,
  truncate,
  toDate,
  toHex,
}

Служба блокчейн В папке src создайте файл с именем Blockchain.services.jsx, вставьте и сохраните в него файл ниже.

import abi from './abis/src/contracts/BlueVotes.sol/BlueVotes.json'
import address from './abis/contractAddress.json'
import { getGlobalState, setGlobalState } from './store'
import { ethers } from 'ethers'
import { checkAuthState, logOutWithCometChat } from './Chat.services'

const { ethereum } = window
const contractAddress = address.address
const contractAbi = abi.abi

const getEtheriumContract = () => {
  const connectedAccount = getGlobalState('connectedAccount')

  if (connectedAccount) {
    const provider = new ethers.providers.Web3Provider(ethereum)
    const signer = provider.getSigner()
    const contract = new ethers.Contract(contractAddress, contractAbi, signer)

    return contract
  } else {
    return getGlobalState('contract')
  }
}

const isWallectConnected = async () => {
  try {
    if (!ethereum) return alert('Please install Metamask')
    const accounts = await ethereum.request({ method: 'eth_accounts' })
    setGlobalState('connectedAccount', accounts[0]?.toLowerCase())

    window.ethereum.on('chainChanged', (chainId) => {
      window.location.reload()
    })

    window.ethereum.on('accountsChanged', async () => {
      setGlobalState('connectedAccount', accounts[0]?.toLowerCase())
      // await isWallectConnected()
      await logOutWithCometChat()
      // await checkAuthState()
      // await getUser()
      window.location.reload()
    })

    if (accounts.length) {
      setGlobalState('connectedAccount', accounts[0]?.toLowerCase())
    } else {
      alert('Please connect wallet.')
      console.log('No accounts found.')
    }
  } catch (error) {
    reportError(error)
  }
}

const connectWallet = async () => {
  try {
    if (!ethereum) return alert('Please install Metamask')
    const accounts = await ethereum.request({ method: 'eth_requestAccounts' })
    setGlobalState('connectedAccount', accounts[0]?.toLowerCase())
  } catch (error) {
    reportError(error)
  }
}

const createPoll = async ({ title, image, startsAt, endsAt, description }) => {
  try {
    if (!ethereum) return alert('Please install Metamask')
    const connectedAccount = getGlobalState('connectedAccount')
    const contract = getEtheriumContract()
    await contract.createPoll(image, title, description, startsAt, endsAt, {
      from: connectedAccount,
    })
    await getPolls()
  } catch (error) {
    reportError(error)
  }
}

const updatePoll = async ({
  id,
  title,
  image,
  startsAt,
  endsAt,
  description,
}) => {
  try {
    if (!ethereum) return alert('Please install Metamask')
    const connectedAccount = getGlobalState('connectedAccount')
    const contract = getEtheriumContract()
    await contract.updatePoll(id, image, title, description, startsAt, endsAt, {
      from: connectedAccount,
    })
    await getPolls()
  } catch (error) {
    reportError(error)
  }
}

const deletePoll = async (id) => {
  try {
    if (!ethereum) return alert('Please install Metamask')
    const connectedAccount = getGlobalState('connectedAccount')
    const contract = getEtheriumContract()
    await contract.deletePoll(id, {
      from: connectedAccount,
    })
  } catch (error) {
    reportError(error)
  }
}

const registerUser = async ({ fullname, image }) => {
  try {
    if (!ethereum) return alert('Please install Metamask')
    const connectedAccount = getGlobalState('connectedAccount')
    const contract = getEtheriumContract()
    await contract.register(image, fullname, { from: connectedAccount })
    await getUser()
  } catch (error) {
    reportError(error)
  }
}

const getUser = async () => {
  try {
    if (!ethereum) return alert('Please install Metamask')
    const connectedAccount = getGlobalState('connectedAccount')
    const contract = getEtheriumContract()
    const user = await contract.users(connectedAccount)
    setGlobalState('user', user)
  } catch (error) {
    reportError(error)
  }
}

const getPolls = async () => {
  try {
    if (!ethereum) return alert('Please install Metamask')
    const contract = getEtheriumContract()
    const polls = await contract.getPolls()
    setGlobalState('polls', structuredPolls(polls))
  } catch (error) {
    reportError(error)
  }
}

const getPoll = async (id) => {
  try {
    if (!ethereum) return alert('Please install Metamask')
    const contract = getEtheriumContract()
    const poll = await contract.getPoll(id)
    setGlobalState('poll', structuredPolls([poll])[0])
  } catch (error) {
    reportError(error)
  }
}

const contest = async (id) => {
  try {
    if (!ethereum) return alert('Please install Metamask')
    const connectedAccount = getGlobalState('connectedAccount')
    const contract = getEtheriumContract()
    await contract.contest(id, { from: connectedAccount })
    await getPoll(id)
  } catch (error) {
    reportError(error)
  }
}

const vote = async (id, cid) => {
  try {
    if (!ethereum) return alert('Please install Metamask')
    const connectedAccount = getGlobalState('connectedAccount')
    const contract = getEtheriumContract()
    await contract.vote(id, cid, { from: connectedAccount })
    await getPoll(id)
    await listContestants(id)
  } catch (error) {
    reportError(error)
  }
}

const listContestants = async (id) => {
  try {
    if (!ethereum) return alert('Please install Metamask')
    const contract = getEtheriumContract()
    const contestants = await contract.listContestants(id)
    setGlobalState('contestants', structuredContestants(contestants))
  } catch (error) {
    reportError(error)
  }
}

const structuredPolls = (polls) =>
  polls
    .map((poll) => ({
      id: Number(poll.id),
      title: poll.title,
      votes: Number(poll.votes),
      startsAt: poll.startsAt,
      endsAt: poll.endsAt,
      contestants: Number(poll.contestants),
      director: poll.director?.toLowerCase(),
      image: poll.image,
      deleted: poll.deleted,
      description: poll.description,
      timestamp: new Date(poll.timestamp.toNumber()).getTime(),
    }))
    .reverse()

const structuredContestants = (contestants, connectedAccount) =>
  contestants
    .map((contestant) => ({
      id: Number(contestant.id),
      fullname: contestant.fullname,
      image: contestant.image,
      voter: contestant.voter?.toLowerCase(),
      voters: contestant.voters.map((v) => v?.toLowerCase()),
      votes: Number(contestant.votes),
    }))
    .sort((a, b) => b.votes - a.votes)

const reportError = (error) => {
  console.log(error.message)
  throw new Error('No ethereum object.')
}

export {
  isWallectConnected,
  connectWallet,
  registerUser,
  getUser,
  createPoll,
  updatePoll,
  deletePoll,
  getPolls,
  getPoll,
  contest,
  listContestants,
  vote,
}

Служба чата Создайте файл с именем "Chat.services.jsx" в папке src, вставьте и сохраните в него приведенные ниже коды.

import { CometChat } from '@cometchat-pro/chat'
import { getGlobalState, setGlobalState } from './store'

const CONSTANTS = {
  APP_ID: process.env.REACT_APP_COMET_CHAT_APP_ID,
  REGION: process.env.REACT_APP_COMET_CHAT_REGION,
  Auth_Key: process.env.REACT_APP_COMET_CHAT_AUTH_KEY,
}

const initCometChat = async () => {
  const appID = CONSTANTS.APP_ID
  const region = CONSTANTS.REGION

  const appSetting = new CometChat.AppSettingsBuilder()
    .subscribePresenceForAllUsers()
    .setRegion(region)
    .build()

  await CometChat.init(appID, appSetting)
    .then(() => console.log('Initialization completed successfully'))
    .catch((error) => console.log(error))
}

const loginWithCometChat = async () => {
  const authKey = CONSTANTS.Auth_Key
  const UID = getGlobalState('connectedAccount')

  await CometChat.login(UID, authKey)
    .then((user) => setGlobalState('currentUser', user))
    .catch((error) => console.log(JSON.stringify(error)))
}

const signUpWithCometChat = async (name) => {
  const authKey = CONSTANTS.Auth_Key
  const UID = getGlobalState('connectedAccount')
  const user = new CometChat.User(UID)

  user.setName(name)
  return await CometChat.createUser(user, authKey)
    .then((user) => user)
    .catch((error) => error)
}

const logOutWithCometChat = async () => {
  await CometChat.logout()
    .then(() => {
      setGlobalState('currentUser', null)
      console.log('Logged Out Successfully')
    })
    .catch((error) => console.log(error))
}

const checkAuthState = async () => {
  await CometChat.getLoggedinUser()
    .then((user) => setGlobalState('currentUser', user))
    .catch((error) => console.log(error))
}

const createNewGroup = async (GUID, groupName) => {
  const groupType = CometChat.GROUP_TYPE.PUBLIC
  const password = ''
  const group = new CometChat.Group(GUID, groupName, groupType, password)

  await CometChat.createGroup(group)
    .then((group) => setGlobalState('group', group))
    .catch((error) => console.log(error))
}

const getGroup = async (GUID) => {
  return await CometChat.getGroup(GUID)
    .then((group) => {
      setGlobalState('group', group)
      return group
    })
    .catch((error) => error)
}

const joinGroup = async (GUID) => {
  const groupType = CometChat.GROUP_TYPE.PUBLIC
  const password = ''

  await CometChat.joinGroup(GUID, groupType, password)
    .then((group) => getGroup(group.guid))
    .catch((error) => console.log(error))
}

const getMessages = async (UID) => {
  const limit = 30
  const messagesRequest = new CometChat.MessagesRequestBuilder()
    .setGUID(UID)
    .setLimit(limit)
    .build()

  return await messagesRequest
    .fetchPrevious()
    .then((messages) => messages)
    .catch((error) => error)
}

const sendMessage = async (receiverID, messageText) => {
  const receiverType = CometChat.RECEIVER_TYPE.GROUP
  const textMessage = new CometChat.TextMessage(
    receiverID,
    messageText,
    receiverType,
  )

  return await CometChat.sendMessage(textMessage)
    .then((message) => message)
    .catch((error) => error)
}

export {
  initCometChat,
  loginWithCometChat,
  signUpWithCometChat,
  logOutWithCometChat,
  getMessages,
  sendMessage,
  checkAuthState,
  createNewGroup,
  getGroup,
  joinGroup,
  CometChat,
}

Файл Index.jsx Теперь обновите файл записей индекса, указав следующие коды.

import React from 'react'
import ReactDOM from 'react-dom'
import { BrowserRouter } from 'react-router-dom'
import './index.css'
import 'react-toastify/dist/ReactToastify.css'
import App from './App'
import { initCometChat } from './Chat.services'

initCometChat().then(() => {
  ReactDOM.render(
    <BrowserRouter>
      <App />
    </BrowserRouter>,
    document.getElementById('root'),
  )
})

Наконец, выполните следующие команды на двух терминалах, чтобы запустить сервер в браузере, но убедитесь, что у вас уже установлен Metamask.

# Terminal one:
yarn hardhat node
# Terminal Two

yarn hardhat run scripts/deploy.js
yarn start

Выполнение приведенных выше команд в соответствии с инструкциями откроет ваш проект в вашем браузере. И вот вы знаете, как создать систему голосования на блокчейне с помощью React, Solidity и CometChat.

Если вы запутались в разработке web3 и вам нужны наглядные материалы, вот одно из моих видео, которое научит вас создавать торговую площадку NFT.

https://www.youtube.com/watch?v=fJIiqeevqoU& ;&t=7667s?embedable=true

Заключение

Децентрализованная сеть и блокчейн остались, а создание реальных сценариев использования – верный способ ускорить вашу карьеру в области веб-разработки.

В этом руководстве вы узнали, как создать систему голосования на основе блокчейна, которая использует смарт-контракты, чтобы гарантировать, что мошенничество и мошенничество запрещены. Мы также включили замечательный CometChat SDK, который позволяет нам создавать групповой чат для каждого опроса.

Если вы готовы углубиться в разработку Web3, посмотрите мои бесплатные видео на моем канале YouTube. Или забронируйте у меня частные занятия по веб3, чтобы ускорить процесс обучения веб3.< /p>

При этом до встречи в следующий раз и хорошего дня!

Об авторе

Госпел Дарлингтон – разработчик комплексного блокчейна с 6+ летним опытом работы в индустрии разработки программного обеспечения.

Сочетая разработку программного обеспечения, написание текстов и преподавание, он демонстрирует, как создавать децентрализованные приложения в сетях блокчейнов, совместимых с EVM.

Его стеки включают JavaScript, React, Vue, Angular, Node, React Native, NextJs, Solidity и другие.

Для получения дополнительной информации о нем посетите и подпишитесь на его страницу в Twitter, Github, LinkedIn или его веб-сайт.


Оригинал
PREVIOUS ARTICLE
NEXT ARTICLE