想象一个技术与民主交叉的世界,去中心化系统的力量与人民的声音相遇。这就是投票的未来,您可以帮助塑造它。
在本指南中,我们将教您如何使用 Next.js、TypeScript、Tailwind CSS 和 CometChat 构建自己的去中心化投票 dapp。这些尖端技术将使您能够创建一个任何人都可以使用的安全、用户友好且有吸引力的投票系统。
无论您是编码初学者还是经验丰富的开发人员,本指南都适合您。我们将首先解释去中心化投票的基础知识,然后引导您逐步完成构建自己的 dapp 的过程。
读完本指南后,您将具备创建可以改变世界的去中心化投票 dapp 所需的技能。
你将学到什么
本指南的目标读者
本指南适用于任何想要学习如何构建去中心化投票 dapp 的人。无论您是编码初学者还是经验丰富的开发人员,您都会在本指南中找到有用的内容。
我们很高兴能帮助您构建投票的未来。让我们开始吧!
您需要安装以下工具才能与我一起构建:
克隆入门工具包并使用以下命令在 VS Code 中打开它:
git clone https://github.com/Daltonic/tailwind_ethers_starter_kit dappVotecd dappVote
接下来,package.json使用下面的代码片段更新。
{ "name": "starter_kit", "description": "A Next.js starter that includes all you need to build amazing projects", "version": "1.0.0", "private": true, "author": "darlington gospel<darlingtongospel@gmail.com>", "license": "MIT", "keywords": [ "nextjs", "starter", "typescript" ], "scripts": { "dev": "next", "build": "next build", "start": "next start", "export": "next build && next export", "lint": "next lint", "format": "prettier --ignore-path .gitignore \"pages/**/*.+(ts|js|tsx)\" --write", "postinstall": "husky install" }, "lint-staged": { "./src/**/*.{ts,js,jsx,tsx}": [ "yarn lint --fix", "yarn format" ] }, "dependencies": { "@cometchat-pro/chat": "3.0.13", "@headlessui/react": "1.7.17", "@openzeppelin/test-helpers": "0.5.16", "@reduxjs/toolkit": "1.9.3", "ethers": "^5.4.7", "next": "13.1.2", "react": "18.2.0", "react-dom": "18.2.0", "react-icons": "4.8.0", "react-identicons": "1.2.5", "react-redux": "8.0.5", "react-toastify": "9.1.2", "sharp": "0.32.5" }, "devDependencies": { "@emotion/react": "11.10.5", "@emotion/styled": "11.10.5", "@ethersproject/abi": "^5.4.7", "@ethersproject/providers": "^5.4.7", "@faker-js/faker": "7.6.0", "@nomicfoundation/hardhat-chai-matchers": "^1.0.0", "@nomicfoundation/hardhat-network-helpers": "^1.0.0", "@nomicfoundation/hardhat-toolbox": "^2.0.0", "@nomiclabs/hardhat-ethers": "^2.0.0", "@nomiclabs/hardhat-etherscan": "^3.0.0", "@nomiclabs/hardhat-waffle": "2.0.3", "@openzeppelin/contracts": "4.8.1", "@typechain/ethers-v5": "^10.1.0", "@typechain/hardhat": "^6.1.2", "@types/node": "18.11.18", "@types/react": "18.0.26", "@types/react-dom": "18.0.10", "@typescript-eslint/eslint-plugin": "5.48.1", "@typescript-eslint/parser": "5.48.1", "autoprefixer": "10.4.13", "chai": "^4.2.0", "dotenv": "16.0.3", "eslint": "8.32.0", "eslint-config-alloy": "4.9.0", "eslint-config-next": "13.1.2", "hardhat": "2.12.7", "hardhat-gas-reporter": "^1.0.8", "husky": "8.0.3", "lint-staged": "13.1.0", "postcss": "8.4.21", "prettier": "2.8.3", "solidity-coverage": "^0.8.0", "tailwindcss": "3.2.4", "typechain": "^8.1.0", "typescript": "4.9.4" }}
yarn install接下来,在终端中运行命令来安装该项目的依赖项。
要配置CometChat SDK,请按照以下步骤操作。完成后,请确保将生成的密钥保存为环境变量以供将来使用。
第 1 步:
前往CometChat Dashboard 并创建一个帐户。
第 2 步:注册后才能
登录CometChat仪表板。
第 3 步:
从仪表板添加一个名为Play-To-Earn的新应用程序。
步骤 4:
从列表中选择您刚刚创建的应用程序。
从快速入门中将APP_ID、REGION、 和AUTH_KEY, 复制到您的.env文件中。请参阅图像和代码片段。
将占位符键替换NEXT_PUBLIC_COMET_CHAT为适当的值。
NEXT_PUBLIC_COMET_CHAT_APP_ID=****************NEXT_PUBLIC_COMET_CHAT_AUTH_KEY=******************************NEXT_PUBLIC_COMET_CHAT_REGION=**
该.env文件应创建在项目的根目录下。
导航到项目的根目录并打开“ hardhat.config.js”文件。使用提供的设置替换文件的现有内容。
require('@nomiclabs/hardhat-waffle')require('dotenv').config()module.exports = { defaultNetwork: 'localhost', networks: { localhost: { url: 'http://127.0.0.1:8545', }, }, solidity: { version: '0.8.11', settings: { optimizer: { enabled: true, runs: 200, }, }, }, paths: { sources: './src/contracts', artifacts: './src/abis', }, mocha: { timeout: 40000, },}
此代码为您的项目配置 Hardhat。它通过导入必要的插件、设置网络(默认为 localhost)、指定 Solidity 编译器版本、定义合约和工件的路径以及设置 Mocha 测试的超时来实现这一点。
接下来的部分概述了为 DappVotes 项目制作智能合约文件的过程。在深入研究以下步骤之前,请contracts在该项目的根目录下创建一个名为 的新文件夹,并在其中创建一个名为DappVotes.sol.
接下来,在刚刚创建的文件中执行以下操作:
这是智能合约逻辑,我们将了解每个函数和变量的作用。
//SPDX-License-Identifier: MITpragma solidity >=0.7.0 <0.9.0;import '@openzeppelin/contracts/utils/Counters.sol';contract DappVotes { using Counters for Counters.Counter; Counters.Counter private totalPolls; Counters.Counter private totalContestants; struct PollStruct { uint id; string image; string title; string description; uint votes; uint contestants; bool deleted; address director; uint startsAt; uint endsAt; uint timestamp; address[] voters; string[] avatars; } struct ContestantStruct { uint id; string image; string name; address voter; uint votes; address[] voters; } mapping(uint => bool) pollExist; mapping(uint => PollStruct) polls; mapping(uint => mapping(address => bool)) voted; mapping(uint => mapping(address => bool)) contested; mapping(uint => mapping(uint => ContestantStruct)) contestants; event Voted(address indexed voter, uint timestamp); function createPoll( string memory image, string memory title, string memory description, uint startsAt, uint endsAt ) public { 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, 'Start date must be greater than 0'); require(endsAt > startsAt, 'End date must be greater than start date'); totalPolls.increment(); PollStruct memory poll; poll.id = totalPolls.current(); poll.title = title; poll.description = description; poll.image = image; poll.startsAt = startsAt; poll.endsAt = endsAt; poll.director = msg.sender; poll.timestamp = currentTime(); polls[poll.id] = poll; pollExist[poll.id] = true; } function updatePoll( uint id, string memory image, string memory title, string memory description, uint startsAt, uint endsAt ) public { 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 deleted'); require(polls[id].votes < 1, 'Poll has votes already'); 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 { require(pollExist[id], 'Poll not found'); require(polls[id].director == msg.sender, 'Unauthorized entity'); require(polls[id].votes < 1, 'Poll has votes already'); polls[id].deleted = true; } function getPoll(uint id) public view returns (PollStruct memory) { return polls[id]; } function getPolls() public view returns (PollStruct[] memory Polls) { uint available; for (uint i = 1; i <= totalPolls.current(); i++) { if(!polls[i].deleted) available++; } Polls = new PollStruct[](available); uint index; for (uint i = 1; i <= totalPolls.current(); i++) { if(!polls[i].deleted) { Polls[index++] = polls[i]; } } } function contest(uint id, string memory name, string memory image) public { require(pollExist[id], 'Poll not found'); require(bytes(name).length > 0, 'name cannot be empty'); require(bytes(image).length > 0, 'image cannot be empty'); require(polls[id].votes < 1, 'Poll has votes already'); require(!contested[id][msg.sender], 'Already contested'); totalContestants.increment(); ContestantStruct memory contestant; contestant.name = name; contestant.image = image; contestant.voter = msg.sender; contestant.id = totalContestants.current(); contestants[id][contestant.id] = contestant; contested[id][msg.sender] = true; polls[id].avatars.push(image); polls[id].contestants++; } function getContestant(uint id, uint cid) public view returns (ContestantStruct memory) { return contestants[id][cid]; } function getContestants(uint id) public view returns (ContestantStruct[] memory Contestants) { uint available; for (uint i = 1; i <= totalContestants.current(); i++) { if(contestants[id][i].id == i) available++; } Contestants = new ContestantStruct[](available); uint index; for (uint i = 1; i <= totalContestants.current(); i++) { if(contestants[id][i].id == i) { Contestants[index++] = contestants[id][i]; } } } function vote(uint id, uint cid) public { require(pollExist[id], 'Poll not found'); require(!voted[id][msg.sender], 'Already voted'); require(!polls[id].deleted, 'Polling not available'); require(polls[id].contestants > 1, 'Not enough contestants'); require( currentTime() >= polls[id].startsAt && currentTime() < polls[id].endsAt, 'Voting must be in session' ); polls[id].votes++; polls[id].voters.push(msg.sender); contestants[id][cid].votes++; contestants[id][cid].voters.push(msg.sender); voted[id][msg.sender] = true; emit Voted(msg.sender, currentTime()); } function currentTime() internal view returns (uint256) { return (block.timestamp * 1000) + 1000; }}
DappVotes 合约包含构成其功能的基本结构:
映射在管理合约数据方面发挥着至关重要的作用:
Voted为了促进用户交互和透明度,每当投票时合约都会发出事件,捕获投票者的地址和当前时间戳。
该合约包含各种功能,可以创建、管理和检索民意调查和参赛者数据:
通过执行这些步骤,您将为 DappVotes 智能合约建立一个功能结构,准备无缝管理民意调查、参赛者和投票交互。
DappVotes 测试脚本经过精心设计,旨在全面评估和验证 DappVotes 智能合约的功能和行为。以下是脚本中涵盖的主要测试和功能的系统细分:
通过这种有组织且详细的分解,解释了 DappVotes 测试脚本的关键功能,说明了每个测试场景的目的和预期结果。测试脚本执行后,将全面验证 DappVotes 智能合约的行为。
在项目的根目录下,创建一个名为“ test ”的文件夹(如果不存在),将下面的代码复制并粘贴到其中。
const { expect } = require('chai')const { expectRevert } = require('@openzeppelin/test-helpers')describe('Contracts', () => { let contract, result const description = 'Lorem Ipsum' const title = 'Republican Primary Election' const image = 'https://image.png' const starts = Date.now() - 10 * 60 * 1000 const ends = Date.now() + 10 * 60 * 1000 const pollId = 1 const contestantId = 1 const avater1 = 'https://avatar1.png' const name1 = 'Nebu Ballon' const avater2 = 'https://avatar2.png' const name2 = 'Kad Neza' beforeEach(async () => { const Contract = await ethers.getContractFactory('DappVotes') ;[deployer, contestant1, contestant2, voter1, voter2, voter3] = await ethers.getSigners() contract = await Contract.deploy() await contract.deployed() }) describe('Poll Management', () => { describe('Success', () => { it('should confirm poll creation success', async () => { result = await contract.getPolls() expect(result).to.have.lengthOf(0) await contract.createPoll(image, title, description, starts, ends) result = await contract.getPolls() expect(result).to.have.lengthOf(1) result = await contract.getPoll(pollId) expect(result.title).to.be.equal(title) expect(result.director).to.be.equal(deployer.address) }) it('should confirm poll update success', async () => { await contract.createPoll(image, title, description, starts, ends) result = await contract.getPoll(pollId) expect(result.title).to.be.equal(title) await contract.updatePoll(pollId, image, 'New Title', description, starts, ends) result = await contract.getPoll(pollId) expect(result.title).to.be.equal('New Title') }) it('should confirm poll deletion success', async () => { await contract.createPoll(image, title, description, starts, ends) result = await contract.getPolls() expect(result).to.have.lengthOf(1) result = await contract.getPoll(pollId) expect(result.deleted).to.be.equal(false) await contract.deletePoll(pollId) result = await contract.getPolls() expect(result).to.have.lengthOf(0) result = await contract.getPoll(pollId) expect(result.deleted).to.be.equal(true) }) }) describe('Failure', () => { it('should confirm poll creation failures', async () => { await expectRevert( contract.createPoll('', title, description, starts, ends), 'Image URL cannot be empty' ) await expectRevert( contract.createPoll(image, title, description, 0, ends), 'Start date must be greater than 0' ) }) it('should confirm poll update failures', async () => { await expectRevert( contract.updatePoll(100, image, 'New Title', description, starts, ends), 'Poll not found' ) }) it('should confirm poll deletion failures', async () => { await expectRevert(contract.deletePoll(100), 'Poll not found') }) }) }) describe('Poll Contest', () => { beforeEach(async () => { await contract.createPoll(image, title, description, starts, ends) }) describe('Success', () => { it('should confirm contest entry success', async () => { result = await contract.getPoll(pollId) expect(result.contestants.toNumber()).to.be.equal(0) await contract.connect(contestant1).contest(pollId, name1, avater1) await contract.connect(contestant2).contest(pollId, name2, avater2) result = await contract.getPoll(pollId) expect(result.contestants.toNumber()).to.be.equal(2) result = await contract.getContestants(pollId) expect(result).to.have.lengthOf(2) }) }) describe('Failure', () => { it('should confirm contest entry failure', async () => { await expectRevert(contract.contest(100, name1, avater1), 'Poll not found') await expectRevert(contract.contest(pollId, '', avater1), 'name cannot be empty') await contract.connect(contestant1).contest(pollId, name1, avater1) await expectRevert( contract.connect(contestant1).contest(pollId, name1, avater1), 'Already contested' ) }) }) }) describe('Poll Voting', () => { beforeEach(async () => { await contract.createPoll(image, title, description, starts, ends) await contract.connect(contestant1).contest(pollId, name1, avater1) await contract.connect(contestant2).contest(pollId, name2, avater2) }) describe('Success', () => { it('should confirm contest entry success', async () => { result = await contract.getPoll(pollId) expect(result.votes.toNumber()).to.be.equal(0) await contract.connect(contestant1).vote(pollId, contestantId) await contract.connect(contestant2).vote(pollId, contestantId) result = await contract.getPoll(pollId) expect(result.votes.toNumber()).to.be.equal(2) expect(result.voters).to.have.lengthOf(2) expect(result.avatars).to.have.lengthOf(2) result = await contract.getContestants(pollId) expect(result).to.have.lengthOf(2) result = await contract.getContestant(pollId, contestantId) expect(result.voters).to.have.lengthOf(2) expect(result.voter).to.be.equal(contestant1.address) }) }) describe('Failure', () => { it('should confirm contest entry failure', async () => { await expectRevert(contract.vote(100, contestantId), 'Poll not found') await contract.deletePoll(pollId) await expectRevert(contract.vote(pollId, contestantId), 'Polling not available') }) }) })})
运行以下命令来运行本地区块链服务器并测试智能合约:
通过在两个单独的终端上运行这些命令,您将测试该智能合约的所有基本功能。
DappVotes 部署脚本旨在使用 Hardhat 开发环境将 DappVotes 智能合约部署到以太坊网络。以下是该脚本的概述:
此 DappVotes 部署脚本简化了部署智能合约的过程,并生成一个包含已部署合约地址的 JSON 文件,以便在项目中进一步使用。
在项目的根目录中,创建一个名为“ scripts ”的文件夹,并在其中创建另一个文件deploy.js(如果尚不存在)。将下面的代码复制并粘贴到其中。
const { ethers } = require('hardhat')const fs = require('fs')async function main() { const contract_name = 'DappVotes' const Contract = await ethers.getContractFactory(contract_name) const contract = await Contract.deploy() await contract.deployed() const address = JSON.stringify({ address: contract.address }, null, 4) fs.writeFile('./artifacts/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})
要执行脚本,请yarn hardhat run scripts/deploy.js在终端中运行,确保您的区块链节点已在另一个终端中运行。
要开始开发应用程序的前端,我们将components在该项目的根目录下创建一个名为的新文件夹。该文件夹将包含我们项目所需的所有组件。
对于下面列出的每个组件,您需要在components文件夹内创建相应的文件并将其代码粘贴到其中。
该Navbar组件提供导航和钱包连接。它显示一个标题为“DappVotes”的导航栏和一个连接钱包的按钮。该按钮在悬停时会更改外观。该组件使用 Redux 和区块链服务进行钱包连接和状态管理。请遵守以下代码:
import { connectWallet, truncate } from '@/services/blockchain'import { RootState } from '@/utils/types'import Link from 'next/link'import React from 'react'import { useSelector } from 'react-redux'const Navbar = () => { const { wallet } = useSelector((states: RootState) => states.globalStates) return ( <nav className="h-[80px] flex justify-between items-center border border-gray-400 px-5 rounded-full" > <Link href="/" className="text-[20px] text-blue-800 sm:text-[24px]"> Dapp<span className="text-white font-bold">Votes</span> </Link> {wallet ? ( <button className="h-[48px] w-[130px] sm:w-[148px] px-3 rounded-full text-sm font-bold transition-all duration-300 bg-[#1B5CFE] hover:bg-blue-500" > {truncate({ text: wallet, startChars: 4, endChars: 4, maxLength: 11 })} </button> ) : ( <button className="h-[48px] w-[130px] sm:w-[148px] px-3 rounded-full text-sm font-bold transition-all duration-300 bg-[#1B5CFE] hover:bg-blue-500" onClick={connectWallet} > Connect wallet </button> )} </nav> )}export default Navbar
该Banner组件是一个 React 组件,它显示一个带有主标题和描述的居中横幅。标题强调了“无操纵投票”的概念。该描述解释了选美比赛的性质。
描述下方有一个标有“创建投票”的按钮。单击此按钮后,如果连接了钱包,则会触发打开创建投票模式的操作。如果未连接钱包,则会显示警告消息,提醒用户连接钱包。该组件使用 Redux 进行状态管理,并使用 React Toastify 来显示通知。请参阅下面的代码:
import { globalActions } from '@/store/globalSlices'import { RootState } from '@/utils/types'import React from 'react'import { useDispatch, useSelector } from 'react-redux'import { toast } from 'react-toastify'const Banner = () => { const dispatch = useDispatch() const { setCreateModal } = globalActions const { wallet } = useSelector((states: RootState) => states.globalStates) const onPressCreate = () => { if (wallet === '') return toast.warning('Connect wallet first!') dispatch(setCreateModal('scale-100')) } return ( <main className="mx-auto text-center space-y-8"> <h1 className="text-[45px] font-[600px] text-center leading-none">Vote Without Rigging</h1> <p className="text-[16px] font-[500px] text-center"> A beauty pageantry is a competition that has traditionally focused on judging and ranking the physical... </p> <button className="text-black h-[45px] w-[148px] rounded-full transition-all duration-300 border border-gray-400 bg-white hover:bg-opacity-20 hover:text-white" onClick={onPressCreate} > Create poll </button> </main> )}export default Banner
该CreatePoll组件为用户提供了创建民意调查的模式表单。它收集民意调查详细信息的输入,例如标题、开始和结束日期、横幅 URL 和描述。提交后,它会验证、转换数据、用动画显示创建状态并处理错误。用户友好的界面有助于在应用程序内创建民意调查。请参阅下面的代码:
import { createPoll } from '@/services/blockchain'import { globalActions } from '@/store/globalSlices'import { PollParams, RootState } from '@/utils/types'import React, { ChangeEvent, FormEvent, useState } from 'react'import { FaTimes } from 'react-icons/fa'import { useDispatch, useSelector } from 'react-redux'import { toast } from 'react-toastify'const CreatePoll: React.FC = () => { const dispatch = useDispatch() const { setCreateModal } = globalActions const { wallet, createModal } = useSelector((states: RootState) => states.globalStates) const [poll, setPoll] = useState<PollParams>({ image: '', title: '', description: '', startsAt: '', endsAt: '', }) const handleSubmit = async (e: FormEvent) => { e.preventDefault() if (!poll.image || !poll.title || !poll.description || !poll.startsAt || !poll.endsAt) return if (wallet === '') return toast.warning('Connect wallet first!') poll.startsAt = new Date(poll.startsAt).getTime() poll.endsAt = new Date(poll.endsAt).getTime() await toast.promise( new Promise<void>((resolve, reject) => { createPoll(poll) .then((tx) => { closeModal() console.log(tx) resolve(tx) }) .catch((error) => reject(error)) }), { pending: 'Approve transaction...', success: 'Poll created successfully ', error: 'Encountered error ', } ) } const handleChange = (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => { const { name, value } = e.target setPoll((prevState) => ({ ...prevState, [name]: value, })) } const closeModal = () => { dispatch(setCreateModal('scale-0')) setPoll({ image: '', title: '', description: '', startsAt: '', endsAt: '', }) } return ( <div className={`fixed top-0 left-0 w-screen h-screen flex items-center justify-center bg-black bg-opacity-50 transform z-50 transition-transform duration-300 ${createModal}`} > <div className="bg-[#0c0c10] text-[#BBBBBB] shadow-lg shadow-[#1B5CFE] 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">Add Poll</p> <button onClick={closeModal} className="border-0 bg-transparent focus:outline-none"> <FaTimes /> </button> </div> <form onSubmit={handleSubmit} className="flex flex-col justify-center items-start rounded-xl mt-5 mb-5" > <div className="py-4 w-full border border-[#212D4A] rounded-full flex items-center px-4 mb-3 mt-2"> <input placeholder="Poll Title" className="bg-transparent outline-none w-full placeholder-[#929292] text-sm" name="title" value={poll.title} onChange={handleChange} required /> </div> <div className="py-4 w-full border border-[#212D4A] rounded-full flex items-center px-4 mb-3 mt-2 space-x-2 relative" > <span className="bg-[#1B5CFE] bg-opacity-20 text-[#4C6AD7] absolute left-[2.5px] py-3 rounded-full px-5 w-48" > <span className="text-transparent">.</span> </span> <input className="bg-transparent outline-none w-full placeholder-transparent text-sm" name="startsAt" type="datetime-local" placeholder="Start Date" value={poll.startsAt} onChange={handleChange} required /> </div> <div className="py-4 w-full border border-[#212D4A] rounded-full flex items-center px-4 mb-3 mt-2 space-x-2 relative" > <span className="bg-[#1B5CFE] bg-opacity-20 text-[#4C6AD7] absolute left-[2.5px] py-3 rounded-full px-5 w-48" > <span className="text-transparent">.</span> </span> <input className="bg-transparent outline-none w-full placeholder-[#929292] text-sm" name="endsAt" type="datetime-local" value={poll.endsAt} onChange={handleChange} required /> </div> <div className="py-4 w-full border border-[#212D4A] rounded-full flex items-center px-4 mb-3 mt-2"> <input placeholder="Banner URL" type="url" className="bg-transparent outline-none w-full placeholder-[#929292] text-sm" name="image" accept="image/*" value={poll.image} onChange={handleChange} required /> </div> <div className="py-4 w-full border border-[#212D4A] rounded-xl flex items-center px-4 h-20 mt-2"> <textarea placeholder="Poll Description" className="bg-transparent outline-none w-full placeholder-[#929292] text-sm" name="description" value={poll.description} onChange={handleChange} required /> </div> <button className="h-[48px] w-full block mt-2 px-3 rounded-full text-sm font-bold transition-all duration-300 bg-[#1B5CFE] hover:bg-blue-500" > Create Poll </button> </form> </div> </div> </div> )}export default CreatePoll
该Polls组件以网格布局显示民意调查列表。每个民意调查都包括标题、简短描述、开始日期、民意调查主管地址和“输入”按钮。投票头像显示在信息旁边。单击“输入”按钮会将用户重定向到投票的详细页面。该组件还使用辅助函数来格式化和截断文本。该Polls组件将一组PollStruct对象作为 prop,并通过它们进行映射以创建Poll具有相关轮询数据的单独组件。请参阅下面的代码:
/* eslint-disable @next/next/no-img-element */import { formatDate, truncate } from '@/services/blockchain'import { PollStruct } from '@/utils/types'import { useRouter } from 'next/router'import React from 'react'const Polls: React.FC<{ polls: PollStruct[] }> = ({ polls }) => { return ( <div> <h1 className="text-center text-[34px] font-[550px] mb-5">Start Voting</h1> <div className="grid grid-cols-1 xl:grid-cols-2 pb-7 gap-[62px] sm:w-2/3 xl:w-5/6 mx-auto"> {polls.map((poll, i) => ( <Poll key={i} poll={poll} /> ))} </div> </div> )}const Poll: React.FC<{ poll: PollStruct }> = ({ poll }) => { const navigate = useRouter() return ( <div className="grid grid-cols-1 md:grid-cols-2 mx-auto w-full"> <div className="h-[392px] gap-[10px] md:w-[580px] md:h-[280px] grid grid-cols-1 md:flex justify-start w-full" > <div className="w-full flex justify-between space-y-0 sm:space-y-2 sm:flex-col md:w-[217px]"> {[...poll.avatars, '/assets/images/question.jpeg', '/assets/images/question.jpeg'] .slice(0, 2) .map((avatar, i) => ( <img key={i} src={avatar} alt={poll.title} className="w-[160px] md:w-full h-[135px] rounded-[20px] object-cover" /> ))} </div> <div className="w-full h-[257px] gap-[14px] rounded-[24px] space-y-5 md:w-[352px] md:h-[280px] bg-[#151515] px-[15px] py-[18px] md:px-[22px]" > <h1 className="text-[18px] font-[600px] capitalize"> {truncate({ text: poll.title, startChars: 30, endChars: 0, maxLength: 33 })} </h1> <p className="text-[14px] font-[400px]"> {truncate({ text: poll.description, startChars: 104, endChars: 0, maxLength: 107 })} </p> <div className="flex justify-between items-center gap-[8px]"> <div className="h-[26px] bg-[#2c2c2c] rounded-full py-[4px] px-[12px] text-[12px] font-[400px]" > {formatDate(poll.startsAt)} </div> <div className="h-[32px] w-[119px] gap-[5px] flex items-center"> <div className="h-[32px] w-[32px] rounded-full bg-[#2c2c2c]" /> <p className="text-[12px] font-[400px]"> {truncate({ text: poll.director, startChars: 4, endChars: 4, maxLength: 11 })} </p> </div> </div> <button onClick={() => navigate.push('/polls/' + poll.id)} className="h-[44px] w-full rounded-full transition-all duration-300 bg-[#1B5CFE] hover:bg-blue-500" > Enter </button> </div> </div> </div> )}export default Polls
该Footer组件显示网页的社交媒体图标和版权信息。这些图标链接到 LinkedIn、YouTube、GitHub 和 Twitter 个人资料。版权文本显示当前年份和消息“With Love ❤️ by Daltonic”。该组件的布局响应灵敏且具有视觉吸引力。
import React from 'react'import { FaGithub, FaLinkedinIn, FaTwitter, FaYoutube } from 'react-icons/fa'const Footer = () => { return ( <footer className="w-full h-[192px] py-[37px] rounded-t-[24px] flex flex-col items-center justify-center bg-white bg-opacity-20 px-5" > <div className="flex justify-center items-center space-x-4"> <FaLinkedinIn size={27} /> <FaYoutube size={27} /> <FaGithub size={27} /> <FaTwitter size={27} /> </div> <hr className="w-full sm:w-[450px] border-t border-gray-400 mt-3" /> <p className="text-sm font-[500px] mt-5">©️{new Date().getFullYear()}</p> <p className="text-sm font-[500px]">With Love ❤️ by Daltonic</p> </footer> )}export default Footer
该Details组件显示有关投票的详细信息,包括投票图像、标题、描述、开始和结束日期、主管、投票和参赛者计数以及编辑和删除按钮(如果用户是主管并且没有投票)。如果没有投票,它还会显示“竞赛”按钮,这将打开竞赛模式。该组件使用 Redux 进行全局状态管理,使用ImageNext.js 的组件进行响应式图像。见下图:
import { formatDate, truncate } from '@/services/blockchain'import { globalActions } from '@/store/globalSlices'import { PollStruct, RootState } from '@/utils/types'import Image from 'next/image'import React from 'react'import { MdModeEdit, MdDelete } from 'react-icons/md'import { useDispatch, useSelector } from 'react-redux'import { toast } from 'react-toastify'const Details: React.FC<{ poll: PollStruct }> = ({ poll }) => { const dispatch = useDispatch() const { setContestModal, setUpdateModal, setDeleteModal } = globalActions const { wallet } = useSelector((states: RootState) => states.globalStates) const onPressContest = () => { if (wallet === '') return toast.warning('Connect wallet first!') dispatch(setContestModal('scale-100')) } return ( <> <div className="w-full h-[240px] rounded-[24px] flex items-center justify-center overflow-hidden" > <Image className="w-full h-full object-cover" width={3000} height={500} src={poll.image} alt={poll.title} /> </div> <div className="flex flex-col items-center justify-center space-y-6 mt-5 w-full md:max-w-[736px] mx-auto" > <h1 className="text-[47px] font-[600px] text-center leading-none">{poll.title}</h1> <p className="text-[16px] font-[500px] text-center">{poll.description}</p> <div className=" h-[136px] gap-[16px] flex flex-col items-center mt-4"> <div className="h-[36px] py-[6px] px-[12px] rounded-full gap-[4px] border border-gray-400 bg-white bg-opacity-20" > <p className="text-[14px] font-[500px] text-center md:text-[16px]"> {formatDate(poll.startsAt)} - {formatDate(poll.endsAt)} </p> </div> <div className="flex items-center justify-center w-[133px] h-[32px] py-[20px] rounded-[10px] gap-[12px]" > <div className="w-[32px] h-[32px] rounded-full bg-[#1B5CFE]" /> <p className="text-[14px] font-[500px]"> {truncate({ text: poll.director, startChars: 4, endChars: 4, maxLength: 11 })} </p> </div> <div className="h-[36px] gap-[4px] flex justify-center items-center"> <button className="py-[6px] px-[12px] border border-gray-400 bg-white bg-opacity-20 rounded-full text-[12px] md:text-[16px]" > {poll.votes} votes </button> <button className="py-[6px] px-[12px] border border-gray-400 bg-white bg-opacity-20 rounded-full text-[12px] md:text-[16px]" > {poll.contestants} contestants </button> {wallet && wallet === poll.director && poll.votes < 1 && ( <button className="py-[6px] px-[12px] border border-gray-400 bg-white bg-opacity-20 rounded-full text-[12px] md:text-[16px] gap-[8px] flex justify-center items-center" onClick={() => dispatch(setUpdateModal('scale-100'))} > <MdModeEdit size={20} className="text-[#1B5CFE]" /> Edit poll </button> )} {wallet && wallet === poll.director && poll.votes < 1 && ( <button className="py-[6px] px-[12px] border border-gray-400 bg-white bg-opacity-20 rounded-full text-[12px] md:text-[16px] gap-[8px] flex justify-center items-center" onClick={() => dispatch(setDeleteModal('scale-100'))} > <MdDelete size={20} className="text-[#fe1b1b]" /> Delete poll </button> )} </div> {poll.votes < 1 && ( <button className="text-black h-[45px] w-[148px] rounded-full transition-all duration-300 border border-gray-400 bg-white hover:bg-opacity-20 hover:text-white py-2" onClick={onPressContest} > Contest </button> )} </div> </div> </> )}export default Details
该UpdatePoll组件提供了一个模式表单来编辑现有民意调查的详细信息。用户可以修改投票的图像、标题、描述、开始日期和结束日期。该组件获取并显示所选轮询的当前数据。
提交后,它会验证输入,更新区块链上的民意调查信息,并通过 Toast 通知提供交易状态反馈。该组件有效地与区块链服务、Redux 状态和用户输入交互,以促进轮询更新。请参阅下面的代码:
import { formatTimestamp, updatePoll } from '@/services/blockchain'import { globalActions } from '@/store/globalSlices'import { PollParams, PollStruct, RootState } from '@/utils/types'import React, { ChangeEvent, FormEvent, useEffect, useState } from 'react'import { FaTimes } from 'react-icons/fa'import { useDispatch, useSelector } from 'react-redux'import { toast } from 'react-toastify'const UpdatePoll: React.FC<{ pollData: PollStruct }> = ({ pollData }) => { const dispatch = useDispatch() const { setUpdateModal } = globalActions const { wallet, updateModal } = useSelector((states: RootState) => states.globalStates) const [poll, setPoll] = useState<PollParams>({ image: '', title: '', description: '', startsAt: '', endsAt: '', }) useEffect(() => { if (pollData) { const { image, title, description, startsAt, endsAt } = pollData setPoll({ image, title, description, startsAt: formatTimestamp(startsAt), endsAt: formatTimestamp(endsAt), }) } }, [pollData]) const handleUpdate = async (e: FormEvent) => { e.preventDefault() if (!poll.image || !poll.title || !poll.description || !poll.startsAt || !poll.endsAt) return if (wallet === '') return toast.warning('Connect wallet first!') poll.startsAt = new Date(poll.startsAt).getTime() poll.endsAt = new Date(poll.endsAt).getTime() await toast.promise( new Promise<void>((resolve, reject) => { updatePoll(pollData.id, poll) .then((tx) => { closeModal() console.log(tx) resolve(tx) }) .catch((error) => reject(error)) }), { pending: 'Approve transaction...', success: 'Poll updated successfully ', error: 'Encountered error ', } ) } const handleChange = (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => { const { name, value } = e.target setPoll((prevState) => ({ ...prevState, [name]: value, })) } const closeModal = () => { dispatch(setUpdateModal('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 ${updateModal}`} > <div className="bg-[#0c0c10] text-[#BBBBBB] shadow-lg shadow-[#1B5CFE] 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">Edit Poll</p> <button onClick={closeModal} className="border-0 bg-transparent focus:outline-none"> <FaTimes /> </button> </div> <form onSubmit={handleUpdate} className="flex flex-col justify-center items-start rounded-xl mt-5 mb-5" > <div className="py-4 w-full border border-[#212D4A] rounded-full flex items-center px-4 mb-3 mt-2"> <input placeholder="Poll Title" className="bg-transparent outline-none w-full placeholder-[#929292] text-sm" name="title" value={poll.title} onChange={handleChange} required /> </div> <div className="py-4 w-full border border-[#212D4A] rounded-full flex items-center px-4 mb-3 mt-2 space-x-2 relative" > <span className="bg-[#1B5CFE] bg-opacity-20 text-[#4C6AD7] absolute left-[2.5px] py-3 rounded-full px-5 w-48" > <span className="text-transparent">.</span> </span> <input className="bg-transparent outline-none w-full placeholder-transparent text-sm" name="startsAt" type="datetime-local" placeholder="Start Date" value={poll.startsAt} onChange={handleChange} required /> </div> <div className="py-4 w-full border border-[#212D4A] rounded-full flex items-center px-4 mb-3 mt-2 space-x-2 relative" > <span className="bg-[#1B5CFE] bg-opacity-20 text-[#4C6AD7] absolute left-[2.5px] py-3 rounded-full px-5 w-48" > <span className="text-transparent">.</span> </span> <input className="bg-transparent outline-none w-full placeholder-[#929292] text-sm" name="endsAt" type="datetime-local" value={poll.endsAt} onChange={handleChange} required /> </div> <div className="py-4 w-full border border-[#212D4A] rounded-full flex items-center px-4 mb-3 mt-2"> <input placeholder="Banner URL" type="url" className="bg-transparent outline-none w-full placeholder-[#929292] text-sm" name="image" accept="image/*" value={poll.image} onChange={handleChange} required /> </div> <div className="py-4 w-full border border-[#212D4A] rounded-xl flex items-center px-4 h-20 mt-2"> <textarea placeholder="Poll Description" className="bg-transparent outline-none w-full placeholder-[#929292] text-sm" name="description" value={poll.description} onChange={handleChange} required /> </div> <button className="h-[48px] w-full block mt-2 px-3 rounded-full text-sm font-bold transition-all duration-300 bg-[#1B5CFE] hover:bg-blue-500" > Update Poll </button> </form> </div> </div> </div> )}export default UpdatePoll
该DeletePoll组件提供了删除特定民意调查的确认模式。当用户单击删除按钮时,该组件与区块链服务交互以删除所选民意调查的数据。它利用 Redux 状态进行用户身份验证和模式状态管理。
删除投票后,该组件将用户重定向到主页,并通过 Toast 通知提供交易状态反馈。该组件有效地处理民意调查删除、与区块链服务交互、管理模式显示并提供用户通知。请参阅下面的代码:
import { deletePoll } from '@/services/blockchain'import { globalActions } from '@/store/globalSlices'import { PollStruct, RootState } from '@/utils/types'import { BsTrash3Fill } from 'react-icons/bs'import React from 'react'import { FaTimes } from 'react-icons/fa'import { useDispatch, useSelector } from 'react-redux'import { toast } from 'react-toastify'import { useRouter } from 'next/router'const DeletePoll: React.FC<{ poll: PollStruct }> = ({ poll }) => { const dispatch = useDispatch() const { setDeleteModal } = globalActions const { wallet, deleteModal } = useSelector((states: RootState) => states.globalStates) const router = useRouter() const handleDelete = async () => { if (wallet === '') return toast.warning('Connect wallet first!') await toast.promise( new Promise<void>((resolve, reject) => { deletePoll(poll.id) .then((tx) => { closeModal() console.log(tx) router.push('/') resolve(tx) }) .catch((error) => reject(error)) }), { pending: 'Approve transaction...', success: 'Poll deleted successfully ', error: 'Encountered error ', } ) } const closeModal = () => { dispatch(setDeleteModal('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 ${deleteModal}`} > <div className="bg-[#0c0c10] text-[#BBBBBB] shadow-lg shadow-[#1B5CFE] 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 Poll</p> <button onClick={closeModal} className="border-0 bg-transparent focus:outline-none"> <FaTimes /> </button> </div> <div className="flex flex-col justify-center items-center rounded-xl mt-5 mb-5"> <div className="flex flex-col justify-center items-center rounded-xl my-5 space-y-2"> <BsTrash3Fill className="text-red-600" size={50} /> <h4 className="text-[22.65px]">Delete Poll</h4> <p className="text-[14px]">Are you sure you want to delete this question?</p> <small className="text-xs italic">{poll?.title}</small> </div> <button className="h-[48px] w-full block mt-2 px-3 rounded-full text-sm font-bold transition-all duration-300 bg-red-600 hover:bg-red-500" onClick={handleDelete} > Delete Poll </button> </div> </div> </div> </div> )}export default DeletePoll
该ContestPoll组件显示一个模式表单,供用户参加特定民意调查的竞赛。用户输入参赛者姓名和头像 URL。提交后,它会验证输入,启动竞赛交易,并用动画显示交易状态。该组件与应用程序的区块链服务和 Redux 状态管理交互,提供无缝的竞赛输入体验。请参阅下面的代码:
import { contestPoll } from '@/services/blockchain'import { globalActions } from '@/store/globalSlices'import { PollStruct, RootState } from '@/utils/types'import React, { ChangeEvent, FormEvent, useState } from 'react'import { FaTimes } from 'react-icons/fa'import { useDispatch, useSelector } from 'react-redux'import { toast } from 'react-toastify'const ContestPoll: React.FC<{ poll: PollStruct }> = ({ poll }) => { const dispatch = useDispatch() const { setContestModal } = globalActions const { wallet, contestModal } = useSelector((states: RootState) => states.globalStates) const [contestant, setContestant] = useState({ name: '', image: '', }) const handleChange = (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => { const { name, value } = e.target setContestant((prevState) => ({ ...prevState, [name]: value, })) } const handleSubmit = async (e: FormEvent) => { e.preventDefault() if (!contestant.name || !contestant.image) return if (wallet === '') return toast.warning('Connect wallet first!') await toast.promise( new Promise<void>((resolve, reject) => { contestPoll(poll.id, contestant.name, contestant.image) .then((tx) => { closeModal() console.log(tx) resolve(tx) }) .catch((error) => reject(error)) }), { pending: 'Approve transaction...', success: 'Poll contested successfully ', error: 'Encountered error ', } ) } const closeModal = () => { dispatch(setContestModal('scale-0')) setContestant({ name: '', image: '', }) } 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 ${contestModal}`} > <div className="bg-[#0c0c10] text-[#BBBBBB] shadow-lg shadow-[#1B5CFE] 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">Become a Contestant</p> <button onClick={closeModal} className="border-0 bg-transparent focus:outline-none"> <FaTimes /> </button> </div> <form onClick={handleSubmit} className="flex flex-col justify-center items-start rounded-xl mt-5 mb-5" > <div className="py-4 w-full border border-[#212D4A] rounded-full flex items-center px-4 mb-3 mt-2"> <input placeholder="Contestant Name" className="bg-transparent outline-none w-full placeholder-[#929292] text-sm" name="name" value={contestant.name} onChange={handleChange} required /> </div> <div className="py-4 w-full border border-[#212D4A] rounded-full flex items-center px-4 mb-3 mt-2"> <input placeholder="Avater URL" type="url" className="bg-transparent outline-none w-full placeholder-[#929292] text-sm" name="image" accept="image/*" value={contestant.image} onChange={handleChange} required /> </div> <button className="h-[48px] w-full block mt-2 px-3 rounded-full text-sm font-bold transition-all duration-300 bg-[#1B5CFE] hover:bg-blue-500" > Contest Now </button> </form> </div> </div> </div> )}export default ContestPoll
该Contestants组件呈现投票的参赛者列表,包括他们的图像、姓名、选民信息、投票按钮和投票计数。它使用contestants数组和poll对象来呈现每个参赛者的信息。
每个Contestant子组件代表一个单独的参赛者,包括以下内容:
该组件还包括错误处理和视觉反馈,以防止在特定情况下投票。该组件允许用户查看并参与投票中参赛者的投票过程。请参阅下面的代码:
import { truncate, voteCandidate } from '@/services/blockchain'import { ContestantStruct, PollStruct, RootState } from '@/utils/types'import Image from 'next/image'import React from 'react'import { BiUpvote } from 'react-icons/bi'import { useSelector } from 'react-redux'import { toast } from 'react-toastify'const Contestants: React.FC<{ contestants: ContestantStruct[]; poll: PollStruct }> = ({ contestants, poll,}) => { return ( <div className="space-y-2"> <h1 className="text-center text-[48px] font-[600px]">Contestants</h1> <div className="grid grid-cols-1 xl:grid-cols-2 pb-7 gap-[62px] sm:w-2/3 xl:w-11/12 mx-auto"> {contestants.map((contestant, i) => ( <Contestant poll={poll} contestant={contestant} key={i} /> ))} </div> </div> )}const Contestant: React.FC<{ contestant: ContestantStruct; poll: PollStruct }> = ({ contestant, poll,}) => { const { wallet } = useSelector((states: RootState) => states.globalStates) const voteContestant = async () => { if (wallet === '') return toast.warning('Connect wallet first!') await toast.promise( new Promise<void>((resolve, reject) => { voteCandidate(poll.id, contestant.id) .then((tx) => { console.log(tx) resolve(tx) }) .catch((error) => reject(error)) }), { pending: 'Approve transaction...', success: 'Poll contested successfully ', error: 'Encountered error ', } ) } return ( <div className="flex justify-start items-center space-x-2 md:space-x-8 mt-5 md:mx-auto"> <div className="w-[187px] sm:w-[324px] h-[229px] sm:h-[180px] rounded-[24px] overflow-hidden"> <Image className="w-full h-full object-cover" width={3000} height={500} src={contestant.image} alt={contestant.name} /> </div> <div className="bg-[#151515] h-[229px] w-[186px] sm:w-[253px] sm:h-fit rounded-[24px] space-y-2 flex justify-center items-center flex-col pt-2 pb-2 px-3" > <h1 className="text-[16px] sm:text-[20px] font-[600px] capitalize">{contestant.name}</h1> <div className="flex items-center justify-center w-full rounded-[10px] space-x-2" > <div className="w-[32px] h-[32px] rounded-full bg-[#2C2C2C]" /> <p className="text-[14px] font-[500px]"> {truncate({ text: contestant.voter, startChars: 4, endChars: 4, maxLength: 11 })} </p> </div> <button onClick={voteContestant} disabled={ wallet ? contestant.voters.includes(wallet) || Date.now() < poll.startsAt || Date.now() >= poll.endsAt : true } className={`w-[158px] sm:w-[213px] h-[48px] rounded-[30.5px] ${ (wallet && poll.voters.includes(wallet)) || Date.now() < poll.startsAt || Date.now() >= poll.endsAt ? 'bg-[#B0BAC9] cursor-not-allowed' : 'bg-[#1B5CFE]' }`} > {wallet && contestant.voters.includes(wallet) ? 'Voted' : 'Vote'} </button> <div className="w-[86px] h-[32px] flex items-center justify-center gap-3"> <div className="w-[32px] h-[32px] rounded-[9px] py-[8px] px-[9px] bg-[#0E1933]"> <BiUpvote size={20} className="text-[#1B5CFE]" /> </div> <p className="text-[14px] font-[600px]">{contestant.votes} vote</p> </div> </div> </div> )}export default Contestants
该**ChatButton**组件根据用户的身份验证状态和民意调查的组状态为各种与聊天相关的操作提供动态下拉菜单。
用户可以执行注册、登录、创建群组、加入群组、查看聊天和注销等操作。这些操作与 CometChat 服务交互以进行用户和组管理,并且该组件通过 toast 通知确保正确的反馈。
它还使用 Redux 管理用户和组数据的全局状态,增强用户与应用程序内聊天功能的交互。请参阅下面的代码:
import React from 'react'import { FaUserPlus } from 'react-icons/fa'import { RiArrowDropDownLine } from 'react-icons/ri'import { FiLogIn } from 'react-icons/fi'import { HiLogin, HiUserGroup, HiChat } from 'react-icons/hi'import { SiGnuprivacyguard } from 'react-icons/si'import { Menu } from '@headlessui/react'import { toast } from 'react-toastify'import { createNewGroup, joinGroup, logOutWithCometChat, loginWithCometChat, signUpWithCometChat,} from '../services/chat'import { useDispatch, useSelector } from 'react-redux'import { globalActions } from '@/store/globalSlices'import { PollStruct, RootState } from '@/utils/types'const ChatButton: React.FC<{ poll: PollStruct; group: any }> = ({ poll, group }) => { const dispatch = useDispatch() const { setCurrentUser, setChatModal, setGroup } = globalActions const { wallet, currentUser } = useSelector((states: RootState) => states.globalStates) const handleSignUp = async () => { if (wallet === '') return toast.warning('Connect wallet first!') await toast.promise( new Promise((resolve, reject) => { signUpWithCometChat(wallet) .then((user) => resolve(user)) .catch((error) => { alert(JSON.stringify(error)) reject(error) }) }), { pending: 'Signning up...', success: 'Signed up successfully, please login ', error: 'Encountered error ', } ) } const handleLogin = async () => { if (wallet === '') return toast.warning('Connect wallet first!') await toast.promise( new Promise((resolve, reject) => { loginWithCometChat(wallet) .then((user) => { dispatch(setCurrentUser(JSON.parse(JSON.stringify(user)))) resolve(user) }) .catch((error) => { alert(JSON.stringify(error)) reject(error) }) }), { pending: 'Logging...', success: 'Logged in successfully ', error: 'Encountered error ', } ) } const handleLogout = async () => { if (wallet === '') return toast.warning('Connect wallet first!') await toast.promise( new Promise((resolve, reject) => { logOutWithCometChat() .then(() => { dispatch(setCurrentUser(null)) resolve(null) }) .catch((error) => { alert(JSON.stringify(error)) reject(error) }) }), { pending: 'Leaving...', success: 'Logged out successfully ', error: 'Encountered error ', } ) } const handleCreateGroup = async () => { if (wallet === '') return toast.warning('Connect wallet first!') await toast.promise( new Promise((resolve, reject) => { createNewGroup(`guid_${poll.id}`, poll.title) .then((group) => { dispatch(setGroup(JSON.parse(JSON.stringify(group)))) resolve(group) }) .catch((error) => { alert(JSON.stringify(error)) reject(error) }) }), { pending: 'Creating group...', success: 'Group created successfully ', error: 'Encountered error ', } ) } const handleJoinGroup = async () => { if (wallet === '') return toast.warning('Connect wallet first!') await toast.promise( new Promise((resolve, reject) => { joinGroup(`guid_${poll.id}`) .then((group) => { dispatch(setGroup(JSON.parse(JSON.stringify(group)))) resolve(group) }) .catch((error) => { alert(JSON.stringify(error)) reject(error) }) }), { pending: 'Joining group...', success: 'Group joined successfully ', error: 'Encountered error ', } ) } return ( <Menu as="div" className="inline-block text-left mx-auto fixed right-5 bottom-[80px]"> <Menu.Button className="bg-[#1B5CFE] hover:bg-blue-700 text-white font-bold rounded-full transition-all duration-300 p-3 focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75 shadow-md shadow-black" as="button" > <RiArrowDropDownLine size={20} /> </Menu.Button> <Menu.Items className="absolute right-0 bottom-14 mt-2 w-56 origin-top-right divide-y divide-gray-100 rounded-md bg-white shadow-lg shadow-black ing-1 ring-black ring-opacity-5 focus:outline-none" > {!currentUser ? ( <> <Menu.Item> {({ active }) => ( <button className={`flex justify-start items-center space-x-1 ${ active ? 'bg-gray-200 text-black' : 'text-red-500' } group flex w-full items-center rounded-md px-2 py-2 text-sm`} onClick={handleSignUp} > <SiGnuprivacyguard size={17} /> <span>SignUp</span> </button> )} </Menu.Item> <Menu.Item> {({ active }) => ( <button className={`flex justify-start items-center space-x-1 ${ active ? 'bg-gray-200 text-black' : 'text-gray-900' } group flex w-full items-center rounded-md px-2 py-2 text-sm`} onClick={handleLogin} > <FiLogIn size={17} /> <span>Login</span> </button> )} </Menu.Item> </> ) : ( <> {!group && wallet === poll.director && ( <Menu.Item> {({ active }) => ( <button className={`flex justify-start items-center space-x-1 ${ active ? 'bg-gray-200 text-black' : 'text-gray-900' } group flex w-full items-center rounded-md px-2 py-2 text-sm`} onClick={() => handleCreateGroup()} > <HiUserGroup size={17} /> <span>Create Group</span> </button> )} </Menu.Item> )} {group && !group.hasJoined && wallet !== poll.director && ( <Menu.Item> {({ active }) => ( <button className={`flex justify-start items-center space-x-1 ${ active ? 'bg-gray-200 text-black' : 'text-gray-900' } group flex w-full items-center rounded-md px-2 py-2 text-sm`} onClick={() => handleJoinGroup()} > <FaUserPlus size={17} /> <span>Join Group</span> </button> )} </Menu.Item> )} {group?.hasJoined && ( <Menu.Item> {({ active }) => ( <button className={`flex justify-start items-center space-x-1 ${ active ? 'bg-gray-200 text-black' : 'text-gray-900' } group flex w-full items-center rounded-md px-2 py-2 text-sm`} onClick={() => dispatch(setChatModal('scale-100'))} > <HiChat size={17} /> <span>Chats</span> </button> )} </Menu.Item> )} <Menu.Item> {({ active }) => ( <button className={`flex justify-start items-center space-x-1 ${ active ? 'bg-gray-200 text-black' : 'text-gray-900' } group flex w-full items-center rounded-md px-2 py-2 text-sm`} onClick={handleLogout} > <HiLogin size={17} /> <span>Logout</span> </button> )} </Menu.Item> </> )} </Menu.Items> </Menu> )}export default ChatButton
该ChatModal组件为群组内的实时聊天提供了一个用户友好的界面。它获取并显示现有消息,侦听新消息,并允许用户发送和接收消息。它还提供消息时间戳、发件人标识和标识符。该组件与 Redux 集成来管理聊天模式的显示状态,并通过 toast 通知维护正确的用户体验。请参阅下面的代码:
import Identicon from 'react-identicons'import { globalActions } from '@/store/globalSlices'import { RootState } from '@/utils/types'import React, { FormEvent, useEffect, useState } from 'react'import { FaTimes } from 'react-icons/fa'import { useDispatch, useSelector } from 'react-redux'import { truncate } from '@/services/blockchain'import { getMessages, listenForMessage, sendMessage } from '@/services/chat'import { toast } from 'react-toastify'const ChatModal: React.FC<{ group: any }> = ({ group }) => { const dispatch = useDispatch() const { setChatModal } = globalActions const { wallet, chatModal } = useSelector((states: RootState) => states.globalStates) const [message, setMessage] = useState<string>('') const [messages, setMessages] = useState<any[]>([]) const [shouldAutoScroll, setShouldAutoScroll] = useState<boolean>(true) useEffect(() => { const handleListing = () => { listenForMessage(group?.guid).then((msg) => { setMessages((prevMsgs) => [...prevMsgs, msg]) setShouldAutoScroll(true) }) } const handleMessageRetrieval = () => { getMessages(group?.guid).then((msgs) => { setMessages(msgs as any[]) setShouldAutoScroll(true) }) } setTimeout(async () => { if (typeof window !== 'undefined') { handleMessageRetrieval() handleListing() } }, 500) }, [group?.guid]) useEffect(() => { if (shouldAutoScroll) { scrollToEnd() } }, [messages, shouldAutoScroll]) const handleSubmit = async (e: FormEvent) => { e.preventDefault() if (!message) return if (wallet === '') return toast.warning('Connect wallet first!') await sendMessage(group?.guid, message) .then((msg) => { setMessages((prevMsgs) => [...prevMsgs, msg]) setShouldAutoScroll(true) scrollToEnd() setMessage('') }) .catch((error) => console.log(error)) } const scrollToEnd = () => { const elmnt: HTMLElement | null = document.getElementById('messages-container') if (elmnt) elmnt.scrollTop = elmnt.scrollHeight } const closeModal = () => { dispatch(setChatModal('scale-0')) setMessage('') scrollToEnd() } 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-[#0c0c10] text-[#BBBBBB] shadow-lg shadow-[#1B5CFE] 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">Chat</p> <button onClick={closeModal} className="border-0 bg-transparent focus:outline-none"> <FaTimes /> </button> </div> <div id="messages-container" className="flex flex-col justify-center items-start rounded-xl my-5 pt-5 max-h-[20rem] overflow-y-auto" > <div className="py-4" /> {messages.map((msg: any, i: number) => ( <Message text={msg.text} owner={msg.sender.uid} time={Number(msg.sendAt + '000')} you={wallet === msg.sender.uid} key={i} /> ))} </div> <form onSubmit={handleSubmit} className="flex flex-col justify-center items-start rounded-xl mt-5 mb-5" > <div className="py-4 w-full border border-[#212D4A] rounded-full flex items-center px-4 mb-3 mt-2"> <input placeholder="Send message..." className="bg-transparent outline-none w-full placeholder-[#929292] text-sm" name="message" value={message} onChange={(e) => setMessage(e.target.value)} required /> </div> </form> </div> </div> </div> )}const Message = ({ text, time, owner, you }) => { return ( <div className="flex justify-start space-x-4 px-6 mb-4 w-full"> <div className="flex justify-start items-center w-full"> <Identicon className="w-12 h-12 rounded-full object-cover mr-4 shadow-md bg-gray-400" string={owner} size={30} /> <div className="w-full"> <h3 className="text-md font-bold"> {you ? '@You' : truncate({ text: owner, startChars: 4, endChars: 4, maxLength: 11 })} </h3> <p className="text-gray-500 text-xs font-semibold space-x-2 w-4/5">{text}</p> </div> </div> </div> )}export default ChatModal
最后是组件,CometChatNoSSR它初始化 CometChat 并在客户端检查用户的身份验证状态。它将用户数据分派到 Redux 以便在应用程序中进一步使用。请参阅下面的代码:
import { initCometChat, checkAuthState } from '@/services/chat'import { useEffect } from 'react'import { globalActions } from '@/store/globalSlices'import { useDispatch } from 'react-redux'const CometChatNoSSR = () => { const { setCurrentUser } = globalActions const dispatch = useDispatch() useEffect(() => { setTimeout(async () => { if (typeof window !== 'undefined') { await initCometChat() checkAuthState().then((user) => { dispatch(setCurrentUser(JSON.parse(JSON.stringify(user)))) }) } }, 500) }, [dispatch, setCurrentUser]) return null}export default CometChatNoSSR
现在我们已经涵盖了该应用程序中的所有组件,是时候开始连接不同的页面了。我们先从主页开始。
要开始开发应用程序的页面,请转到pages项目根目录的文件夹。该文件夹将包含我们项目所需的所有页面。
该Home组件呈现该应用程序的主页。它使用 Redux 来管理全局状态,从服务器获取轮询数据,并定义页面的 HTML 结构。此页面将导航栏、横幅、CreatePoll、投票和页脚组件捆绑在一起。
要跟进此组件,请将文件夹index.tsx中的文件内容替换pages为以下代码:
import Banner from '@/components/Banner'import CreatePoll from '@/components/CreatePoll'import Footer from '@/components/Footer'import Navbar from '@/components/Navbar'import Polls from '@/components/Polls'import { getPolls } from '@/services/blockchain'import { globalActions } from '@/store/globalSlices'import { PollStruct, RootState } from '@/utils/types'import Head from 'next/head'import { useEffect } from 'react'import { useDispatch, useSelector } from 'react-redux'export default function Home({ pollsData }: { pollsData: PollStruct[] }) { const dispatch = useDispatch() const { setPolls } = globalActions const { polls } = useSelector((states: RootState) => states.globalStates) useEffect(() => { dispatch(setPolls(pollsData)) }, [dispatch, setPolls, pollsData]) return ( <> <Head> <title>Available Polls</title> <link rel="icon" href="/favicon.ico" /> </Head> <div className="min-h-screen relative backdrop-blur"> <div className="absolute inset-0 before:absolute before:inset-0 before:w-full before:h-full before:bg-[url('/assets/images/bg.jpeg')] before:blur-sm before:z-[-1] before:bg-no-repeat before:bg-cover" /> <section className="relative px-5 py-10 space-y-16 text-white sm:p-10"> <Navbar /> <Banner /> <Polls polls={polls} /> <Footer /> </section> <CreatePoll /> </div> </> )}export const getServerSideProps = async () => { const pollsData: PollStruct[] = await getPolls() return { props: { pollsData: JSON.parse(JSON.stringify(pollsData)) }, }}
该Polls组件是一个动态页面,显示有关特定投票的详细信息。它使用 Redux 来管理全局状态,从服务器获取投票和参赛者数据,并定义页面的 HTML 结构。
该组件捆绑了**Footer**、**Navbar**、**Details**、**Contestants**、**UpdatePoll**、**DeletePoll**、**ContestPoll**、**ChatModal**和**ChatButton**,构成了投票页面的结构。
polls要继续,请在目录内创建一个名为inside的新文件夹pages。然后,创建一个名为[id].tsx. 确保使用准确的模式,因为 Next.js 创建动态页面需要此模式。请参阅下面的代码:
import Footer from '@/components/Footer'import Navbar from '@/components/Navbar'import Details from '@/components/Details'import Contestants from '@/components/Contestants'import Head from 'next/head'import ContestPoll from '@/components/ContestPoll'import { GetServerSidePropsContext } from 'next'import { getContestants, getPoll } from '@/services/blockchain'import { ContestantStruct, PollStruct, RootState } from '@/utils/types'import { useDispatch, useSelector } from 'react-redux'import { globalActions } from '@/store/globalSlices'import { useEffect } from 'react'import UpdatePoll from '@/components/UpdatePoll'import DeletePoll from '@/components/DeletePoll'import ChatButton from '@/components/ChatButton'import ChatModal from '@/components/ChatModal'import { getGroup } from '@/services/chat'import { useRouter } from 'next/router'export default function Polls({ pollData, contestantData,}: { pollData: PollStruct contestantData: ContestantStruct[]}) { const dispatch = useDispatch() const { setPoll, setContestants, setGroup } = globalActions const { poll, contestants, currentUser, group } = useSelector( (states: RootState) => states.globalStates ) const router = useRouter() const { id } = router.query useEffect(() => { dispatch(setPoll(pollData)) dispatch(setContestants(contestantData)) const fetchData = async () => { if (typeof window !== 'undefined') { setTimeout(async () => { const groupData = await getGroup(`guid_${id}`) if (groupData) dispatch(setGroup(JSON.parse(JSON.stringify(groupData)))) }, 500) } } fetchData() }, [ dispatch, setPoll, setContestants, setGroup, contestantData, pollData, id, currentUser, group, ]) return ( <> {poll && ( <Head> <title>Poll | {poll.title}</title> <link rel="icon" href="/favicon.ico" /> </Head> )} <div className="min-h-screen relative backdrop-blur"> <div className="absolute inset-0 before:absolute before:inset-0 before:w-full before:h-full before:bg-[url('/assets/images/bg.jpeg')] before:blur-sm before:z-[-1] before:bg-no-repeat before:bg-cover" /> <section className="relative px-5 py-10 space-y-16 text-white sm:p-10"> <Navbar /> {poll && <Details poll={poll} />} {poll && contestants && <Contestants poll={poll} contestants={contestants} />} <Footer /> </section> {poll && ( <> <UpdatePoll pollData={poll} /> <DeletePoll poll={poll} /> <ContestPoll poll={poll} /> <ChatModal group={group} /> <ChatButton poll={poll} group={group} /> </> )} </div> </> )}export const getServerSideProps = async (context: GetServerSidePropsContext) => { const { id } = context.query const pollData = await getPoll(Number(id)) const contestantData = await getContestants(Number(id)) return { props: { pollData: JSON.parse(JSON.stringify(pollData)), contestantData: JSON.parse(JSON.stringify(contestantData)), }, }}
跨组件和页面管理来自区块链和智能合约的数据可能很困难。这就是我们使用 Redux Toolkit 来管理所有组件和脚本中的数据的原因。这是 Redux Toolkit 的独特设置:
第 1 步:定义 Redux 状态在项目根目录下
创建一个名为的文件夹。store创建另一个名为statesinside 的store文件夹。globalState.ts在文件夹内创建一个名为的文件states,并将以下代码粘贴到其中。
import { GlobalState } from '@/utils/types'export const globalStates: GlobalState = { wallet: '', createModal: 'scale-0', updateModal: 'scale-0', deleteModal: 'scale-0', contestModal: 'scale-0', chatModal: 'scale-0', polls: [], poll: null, group: null, contestants: [], currentUser: null,}
这段代码定义了 Redux 的初始状态。它包括wallet用户以太坊钱包信息、各种模式的可见性状态、选定民意调查数据、polls聊天组信息、民意调查参赛者以及登录用户详细信息的数组等属性。这些属性存储可以跨应用程序全局访问和更新的数据。pollgroupcontestantscurrentUser
第 2 步:定义 Redux 操作
创建另一个名为actionsinside 的store文件夹。globalActions.ts在文件夹内创建一个名为的文件actions,并将以下代码粘贴到其中。
import { ContestantStruct, GlobalState, PollStruct } from '@/utils/types'import { PayloadAction } from '@reduxjs/toolkit'export const globalActions = { setWallet: (state: GlobalState, action: PayloadAction<string>) => { state.wallet = action.payload }, setCreateModal: (state: GlobalState, action: PayloadAction<string>) => { state.createModal = action.payload }, setUpdateModal: (state: GlobalState, action: PayloadAction<string>) => { state.updateModal = action.payload }, setDeleteModal: (state: GlobalState, action: PayloadAction<string>) => { state.deleteModal = action.payload }, setContestModal: (state: GlobalState, action: PayloadAction<string>) => { state.contestModal = action.payload }, setChatModal: (state: GlobalState, action: PayloadAction<string>) => { state.chatModal = action.payload }, setPolls: (state: GlobalState, action: PayloadAction<PollStruct[]>) => { state.polls = action.payload }, setPoll: (state: GlobalState, action: PayloadAction<PollStruct>) => { state.poll = action.payload }, setGroup: (state: GlobalState, action: PayloadAction<any>) => { state.group = action.payload }, setContestants: (state: GlobalState, action: PayloadAction<ContestantStruct[]>) => { state.contestants = action.payload }, setCurrentUser: (state: GlobalState, action: PayloadAction<any>) => { state.currentUser = action.payload },}
这些 Redux 操作定义了修改全局状态的函数。它们从操作接收状态对象和有效负载。每个操作对应于一个特定的状态属性并将其设置为有效负载的值。这些操作在整个应用程序中用于更新各种全局状态属性,例如wallet、modalstates、polls、group、contestants和currentUser,从而根据需要动态更改应用程序的状态。
步骤 3:将状态和操作捆绑在切片中创建一个在文件夹内
调用的文件,并将以下代码粘贴到其中。globalSlices.tsstore
import { createSlice } from '@reduxjs/toolkit'import { globalActions as GlobalActions } from './actions/globalActions'import { globalStates as GlobalStates } from './states/globalState'export const globalSlices = createSlice({ name: 'global', initialState: GlobalStates, reducers: GlobalActions,})export const globalActions = globalSlices.actionsexport default globalSlices.reducer
这个名为 Redux 的切片global结合了初始状态和来自globalActions和 的减速器GlobalStates。它为全局状态创建动作和减速器。该globalActions对象包含操作创建者,并globalSlices.reducer根据这些操作处理状态更新,从而简化了global应用程序切片的状态管理。
步骤 4:将切片配置为存储服务在文件夹内
创建一个名为的文件,并将以下代码粘贴到其中。index.tsstore
import { configureStore } from '@reduxjs/toolkit'import globalSlices from './globalSlices'export const store = configureStore({ reducer: { globalStates: globalSlices, },})
此 Redux 存储是使用配置的@reduxjs/toolkit。它将globalSlices减速器合并到存储中,允许在整个应用程序中访问全局状态管理。该configureStore函数使用指定的减速器初始化存储,从而通过 Redux 实现高效的状态处理。
该应用程序中使用的接口在项目根目录下的文件夹type.d.ts中的文件中定义。utils该文件定义了应用程序中使用的数据结构。创建一个名为 的文件夹utils,并在其中创建一个名为 的文件type.d.ts,并将以下代码粘贴到其中。
export interface TruncateParams { text: string startChars: number endChars: number maxLength: number}export interface PollParams { image: string title: string description: string startsAt: number | string endsAt: number | string}export interface PollStruct { id: number image: string title: string description: string votes: number contestants: number deleted: boolean director: string startsAt: number endsAt: number timestamp: number avatars: string[] voters: string[]}export interface ContestantStruct { id: number image: string name: string voter: string votes: number voters: string[]}export interface GlobalState { wallet: string createModal: string updateModal: string deleteModal: string contestModal: string chatModal: string polls: PollStruct[] poll: PollStruct | null group: PollStruct | null contestants: ContestantStruct[] currentUser: PollStruct | null}export interface RootState { globalStates: GlobalState}
要将 web3 和聊天功能添加到我们的应用程序中,我们需要创建一些服务。在项目的根目录创建一个名为的文件夹services,并在其中创建以下脚本:
确保将以下代码复制并粘贴到各自的文件中。
区块链服务
该区块链服务使用以太坊与智能合约进行交互。它包括连接以太坊钱包、创建、更新和删除民意调查、参与民意调查、为参赛者投票以及获取民意调查相关数据等功能。请参阅下面的代码:
import { store } from '@/store'import { ethers } from 'ethers'import { globalActions } from '@/store/globalSlices'import address from '@/artifacts/contractAddress.json'import abi from '@/artifacts/contracts/DappVotes.sol/DappVotes.json'import { ContestantStruct, PollParams, PollStruct, TruncateParams } from '@/utils/types'import { logOutWithCometChat } from './chat'const { setWallet, setPolls, setPoll, setContestants, setCurrentUser } = globalActionsconst ContractAddress = address.addressconst ContractAbi = abi.abilet ethereum: anylet tx: anyif (typeof window !== 'undefined') { ethereum = (window as any).ethereum}const getEthereumContract = async () => { const accounts = await ethereum?.request?.({ method: 'eth_accounts' }) const provider = accounts?.[0] ? new ethers.providers.Web3Provider(ethereum) : new ethers.providers.JsonRpcProvider(process.env.NEXT_APP_RPC_URL) const wallet = accounts?.[0] ? null : ethers.Wallet.createRandom() const signer = provider.getSigner(accounts?.[0] ? undefined : wallet?.address) const contract = new ethers.Contract(ContractAddress, ContractAbi, signer) return contract}const connectWallet = async () => { try { if (!ethereum) return reportError('Please install Metamask') const accounts = await ethereum.request?.({ method: 'eth_requestAccounts' }) store.dispatch(setWallet(accounts?.[0])) } catch (error) { reportError(error) }}const checkWallet = async () => { try { if (!ethereum) return reportError('Please install Metamask') const accounts = await ethereum.request?.({ method: 'eth_accounts' }) ethereum.on('chainChanged', () => { window.location.reload() }) ethereum.on('accountsChanged', async () => { store.dispatch(setWallet(accounts?.[0])) await checkWallet() await logOutWithCometChat() store.dispatch(setCurrentUser(null)) }) if (accounts?.length) { store.dispatch(setWallet(accounts[0])) } else { store.dispatch(setWallet('')) reportError('Please connect wallet, no accounts found.') } } catch (error) { reportError(error) }}const createPoll = async (data: PollParams) => { if (!ethereum) { reportError('Please install Metamask') return Promise.reject(new Error('Metamask not installed')) } try { const contract = await getEthereumContract() const { image, title, description, startsAt, endsAt } = data const tx = await contract.createPoll(image, title, description, startsAt, endsAt) await tx.wait() const polls = await getPolls() store.dispatch(setPolls(polls)) return Promise.resolve(tx) } catch (error) { reportError(error) return Promise.reject(error) }}const updatePoll = async (id: number, data: PollParams) => { if (!ethereum) { reportError('Please install Metamask') return Promise.reject(new Error('Metamask not installed')) } try { const contract = await getEthereumContract() const { image, title, description, startsAt, endsAt } = data const tx = await contract.updatePoll(id, image, title, description, startsAt, endsAt) await tx.wait() const poll = await getPoll(id) store.dispatch(setPoll(poll)) return Promise.resolve(tx) } catch (error) { reportError(error) return Promise.reject(error) }}const deletePoll = async (id: number) => { if (!ethereum) { reportError('Please install Metamask') return Promise.reject(new Error('Metamask not installed')) } try { const contract = await getEthereumContract() const tx = await contract.deletePoll(id) await tx.wait() return Promise.resolve(tx) } catch (error) { reportError(error) return Promise.reject(error) }}const contestPoll = async (id: number, name: string, image: string) => { if (!ethereum) { reportError('Please install Metamask') return Promise.reject(new Error('Metamask not installed')) } try { const contract = await getEthereumContract() const tx = await contract.contest(id, name, image) await tx.wait() const poll = await getPoll(id) store.dispatch(setPoll(poll)) const contestants = await getContestants(id) store.dispatch(setContestants(contestants)) return Promise.resolve(tx) } catch (error) { reportError(error) return Promise.reject(error) }}const voteCandidate = async (id: number, cid: number) => { if (!ethereum) { reportError('Please install Metamask') return Promise.reject(new Error('Metamask not installed')) } try { const contract = await getEthereumContract() const tx = await contract.vote(id, cid) await tx.wait() const poll = await getPoll(id) store.dispatch(setPoll(poll)) const contestants = await getContestants(id) store.dispatch(setContestants(contestants)) return Promise.resolve(tx) } catch (error) { reportError(error) return Promise.reject(error) }}const getPolls = async (): Promise<PollStruct[]> => { const contract = await getEthereumContract() const polls = await contract.getPolls() return structurePolls(polls)}const getPoll = async (id: number): Promise<PollStruct> => { const contract = await getEthereumContract() const polls = await contract.getPoll(id) return structurePolls([polls])[0]}const getContestants = async (id: number): Promise<ContestantStruct[]> => { const contract = await getEthereumContract() const contestants = await contract.getContestants(id) return structureContestants(contestants)}const truncate = ({ text, startChars, endChars, maxLength }: TruncateParams): string => { if (text.length > maxLength) { let start = text.substring(0, startChars) let end = text.substring(text.length - endChars, text.length) while (start.length + end.length < maxLength) { start = start + '.' } return start + end } return text}const formatDate = (timestamp: number): string => { const date = new Date(timestamp) const daysOfWeek = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'] const months = [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec', ] const dayOfWeek = daysOfWeek[date.getUTCDay()] const month = months[date.getUTCMonth()] const day = date.getUTCDate() const year = date.getUTCFullYear() return `${dayOfWeek}, ${month} ${day}, ${year}`}const formatTimestamp = (timestamp: number) => { const date = new Date(timestamp) const year = date.getFullYear() const month = String(date.getMonth() + 1).padStart(2, '0') const day = String(date.getDate()).padStart(2, '0') const hours = String(date.getHours()).padStart(2, '0') const minutes = String(date.getMinutes()).padStart(2, '0') return `${year}-${month}-${day}T${hours}:${minutes}`}const structurePolls = (polls: any[]): PollStruct[] => polls .map((poll) => ({ id: Number(poll.id), image: poll.image, title: poll.title, description: poll.description, votes: Number(poll.votes), contestants: Number(poll.contestants), deleted: poll.deleted, director: poll.director.toLowerCase(), startsAt: Number(poll.startsAt), endsAt: Number(poll.endsAt), timestamp: Number(poll.timestamp), voters: poll.voters.map((voter: string) => voter.toLowerCase()), avatars: poll.avatars, })) .sort((a, b) => b.timestamp - a.timestamp)const structureContestants = (contestants: any[]): ContestantStruct[] => contestants .map((contestant) => ({ id: Number(contestant.id), image: contestant.image, name: contestant.name, voter: contestant.voter.toLowerCase(), votes: Number(contestant.votes), voters: contestant.voters.map((voter: string) => voter.toLowerCase()), })) .sort((a, b) => b.votes - a.votes)export { connectWallet, checkWallet, truncate, formatDate, formatTimestamp, createPoll, updatePoll, deletePoll, getPolls, getPoll, contestPoll, getContestants, voteCandidate,}
该服务的关键组件包括:
该服务在应用程序与以太坊区块链的交互中发挥着至关重要的作用,使用户能够安全、透明地参与投票和竞赛。
聊天服务
该聊天服务将 CometChat SDK 集成到应用程序中以实现实时聊天功能。请参阅下面的代码:
let CometChat: anyif (typeof window !== 'undefined') { import('@cometchat-pro/chat').then((cometChatModule) => { CometChat = cometChatModule.CometChat }) console.log('CometChat Loaded...')}const CONSTANTS = { APP_ID: process.env.NEXT_PUBLIC_COMET_CHAT_APP_ID, REGION: process.env.NEXT_PUBLIC_COMET_CHAT_REGION, Auth_Key: process.env.NEXT_PUBLIC_COMET_CHAT_AUTH_KEY,}const initCometChat = async () => { const appID = CONSTANTS.APP_ID const region = CONSTANTS.REGION const appSetting = new CometChat.AppSettingsBuilder() .subscribePresenceForAllUsers() .setRegion(region) .autoEstablishSocketConnection(true) .build() CometChat.init(appID, appSetting) .then(() => console.log('Initialization completed successfully')) .catch((error: any) => console.log(error))}const loginWithCometChat = async (UID: string) => { const authKey = CONSTANTS.Auth_Key return new Promise((resolve, reject) => { CometChat.login(UID, authKey) .then((user: any) => resolve(user)) .catch((error: any) => reject(error)) })}const signUpWithCometChat = async (UID: string) => { const authKey = CONSTANTS.Auth_Key const user = new CometChat.User(UID) user.setName(UID) return new Promise((resolve, reject) => { CometChat.createUser(user, authKey) .then((user: any) => resolve(user)) .catch((error: any) => reject(error)) })}const logOutWithCometChat = async () => { return new Promise((resolve, reject) => { CometChat.logout() .then(() => resolve(null)) .catch((error: any) => reject(error)) })}const checkAuthState = async () => { return new Promise((resolve, reject) => { CometChat.getLoggedinUser() .then((user: any) => resolve(user)) .catch((error: any) => reject(error)) })}const createNewGroup = async (GUID: string, groupName: string) => { const groupType = CometChat.GROUP_TYPE.PUBLIC const password = '' const group = new CometChat.Group(GUID, groupName, groupType, password) return new Promise((resolve, reject) => { CometChat.createGroup(group) .then((group: any) => resolve(group)) .catch((error: any) => reject(error)) })}const getGroup = async (GUID: string) => { return new Promise((resolve, reject) => { CometChat.getGroup(GUID) .then((group: any) => resolve(group)) .catch((error: any) => reject(error)) })}const joinGroup = async (GUID: string) => { const groupType = CometChat.GROUP_TYPE.PUBLIC const password = '' return new Promise((resolve, reject) => { CometChat.joinGroup(GUID, groupType, password) .then((group: any) => resolve(group)) .catch((error: any) => reject(error)) })}const getMessages = async (GUID: string) => { const limit = 30 const messagesRequest = new CometChat.MessagesRequestBuilder() .setGUID(GUID) .setLimit(limit) .build() return new Promise((resolve, reject) => { messagesRequest .fetchPrevious() .then((messages: any[]) => resolve(messages.filter((msg) => msg.type === 'text'))) .catch((error: any) => reject(error)) })}const sendMessage = async (receiverID: string, messageText: string) => { const receiverType = CometChat.RECEIVER_TYPE.GROUP const textMessage = new CometChat.TextMessage(receiverID, messageText, receiverType) return new Promise((resolve, reject) => { CometChat.sendMessage(textMessage) .then((message: any) => resolve(message)) .catch((error: any) => reject(error)) })}const listenForMessage = async (listenerID: string) => { return new Promise((resolve) => { CometChat.addMessageListener( listenerID, new CometChat.MessageListener({ onTextMessageReceived: (message: any) => resolve(message), }) ) })}export { initCometChat, loginWithCometChat, signUpWithCometChat, logOutWithCometChat, checkAuthState, createNewGroup, getGroup, getMessages, joinGroup, sendMessage, listenForMessage,}
它执行几个关键功能:
该服务使用户能够在应用程序内进行实时群聊,增强用户体验并促进参与者之间的沟通。
该组件管理该 NextJs 应用程序中的页面和子组件。它根据showChild状态有条件地渲染子组件。它初始化 CometChat,提供 Redux 存储访问,并通过ToastContainer. 这种条件渲染确保 CometChat 仅在客户端初始化。
前往pages文件夹,打开_app.tsx文件并将其内容替换为以下代码:
import { AppProps } from 'next/app'import '@/styles/global.css'import { Provider } from 'react-redux'import { store } from '@/store'import { ToastContainer } from 'react-toastify'import 'react-toastify/dist/ReactToastify.css'import { useEffect, useState } from 'react'import { checkWallet } from '@/services/blockchain'import CometChatNoSSR from '@/components/CometChatNoSSR'export default function MyApp({ Component, pageProps }: AppProps) { const [showChild, setShowChild] = useState<boolean>(false) useEffect(() => { checkWallet() setShowChild(true) }, []) if (!showChild || typeof window === 'undefined') { return null } else { return ( <Provider store={store}> <CometChatNoSSR /> <Component {...pageProps} /> <ToastContainer position="bottom-center" autoClose={5000} hideProgressBar={false} newestOnTop={false} closeOnClick rtl={false} pauseOnFocusLoss draggable pauseOnHover theme="dark" /> </Provider> ) }}
在完成项目之前,创建一个名为 的文件夹assets/images,并将在此链接中找到的图像添加到该文件夹中。
恭喜您学习了本教程,了解如何使用 Next.js、TypeScript、Tailwind CSS 和 CometChat 构建去中心化投票 dapp!