paint-brush
Comment créer un DAO Web3.0 glorieux avec React, Solidity et CometChatpar@daltonic
3,522 lectures
3,522 lectures

Comment créer un DAO Web3.0 glorieux avec React, Solidity et CometChat

par Darlington Gospel 2022/06/22
Read on Terminal Reader
Read this story w/o Javascript

Trop long; Pour lire

Darlington Gospel est un mentor Dapp qui aide les développeurs du Web 2.0 au Web 3.0.0. Dans ce didacticiel, vous apprendrez étape par étape comment implémenter une organisation autonome décentralisée (DAO) avec des fonctionnalités de chat anonyme. Dans le didacticiel ci-dessous, clonez le projet de démarrage Web3 ci-dessous. Cela garantira que nous sommes tous sur la même page et que nous utilisons les mêmes packages. Le code sur le terminal pour confirmer qu'il est installé globalement pour installer ces packages essentiels globalement.

Companies Mentioned

Mention Thumbnail
Mention Thumbnail

Coins Mentioned

Mention Thumbnail
Mention Thumbnail
featured image - Comment créer un DAO Web3.0 glorieux avec React, Solidity et CometChat
Darlington Gospel  HackerNoon profile picture

Voir une démo et un dépôt git ici pour ce que vous allez construire !


Démo Dominion DAO


L'interface de discussion

Introduction

Je suis très heureux de vous proposer cette version Web3.0, je sais que vous cherchiez un excellent exemple pour vous lancer dans le développement d'applications décentralisées.


Si vous êtes nouveau ici, je suis Darlington Gospel, un mentor Dapp qui aide les développeurs à faire la transition comme vous du Web 2.0 au Web 3.0.


Dans ce didacticiel, vous apprendrez étape par étape comment implémenter une organisation autonome décentralisée (DAO) avec des fonctionnalités de chat anonyme.


Si vous êtes motivé pour cette version, passons au didacticiel…

Prérequis

Vous aurez besoin des outils suivants installés pour écraser avec succès cette version :

  • Nœud
  • Ganache-Cli
  • Truffe
  • Réagir
  • furie
  • CSS vent arrière
  • SDK CometChat
  • Métamasque
  • Fil

Installation des dépendances

Installation de NodeJs Assurez-vous que NodeJs est déjà installé sur votre machine. Ensuite, exécutez le code sur le terminal pour confirmer qu'il est installé.


Nœud installé

Installation de Yarn, Ganache-cli et Truffle Exécutez les codes suivants sur votre terminal pour installer globalement ces packages essentiels.


 npm i -g yarn npm i -g truffle npm i -g ganache-cli


Clonage du projet de démarrage Web3 À l'aide des commandes ci-dessous, clonez le projet de démarrage Web 3.0 ci-dessous. Cela garantira que nous sommes tous sur la même longueur d'onde et que nous utilisons les mêmes packages.


 git clone https://github.com/Daltonic/dominionDAO


Fantastique, remplaçons le fichier package.json par celui ci-dessous :


 { "name": "dominionDAO", "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.6", "moment": "^2.29.3", "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.0.1", "recharts": "^2.1.9", "web-vitals": "^2.1.4", "web3": "^1.7.1" }, "devDependencies": { "@openzeppelin/contracts": "^4.5.0", "@tailwindcss/forms": "0.4.0", "@truffle/hdwallet-provider": "^2.0.4", "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" ] } }


Super, remplacez votre fichier package.json par le code ci-dessus, puis lancez yarn install sur votre terminal.


Avec tout cela installé, commençons par écrire le contrat intelligent Dominion DAO.

Configuration du SDK CometChat

Pour configurer le SDK CometChat , suivez les étapes ci-dessous, à la fin, vous devez stocker ces clés en tant que variable d'environnement.


ÉTAPE 1 : Rendez-vous sur le tableau de bord CometChat et créez un compte.


Enregistrez un nouveau compte CometChat si vous n'en avez pas


ÉTAPE 2 : Connectez-vous au tableau de bord CometChat , uniquement après votre inscription.


Connectez-vous au tableau de bord CometChat avec votre compte créé


ÉTAPE 3 : À partir du tableau de bord, ajoutez une nouvelle application appelée dominionDAO.


Créer une nouvelle application CometChat - Étape 1

Créer une nouvelle application CometChat - Étape 2


ÉTAPE 4 : Sélectionnez l'application que vous venez de créer dans la liste.


Sélectionnez votre application créée


ÉTAPE 5 : À partir du démarrage rapide, copiez APP_ID , REGION et AUTH_KEY dans votre fichier .env . Voir l'image et l'extrait de code.


Copiez les APP_ID, REGION et AUTH_KEY


Remplacez les clés d'espace réservé REACT_COMET_CHAT par leurs valeurs appropriées.


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

Configuration de l'application Infuria

ÉTAPE 1 : Rendez-vous sur Infuria et créez un compte.


Connectez-vous à votre compte infuria


ÉTAPE 2 : À partir du tableau de bord, créez un nouveau projet.


Créer un nouveau projet : étape 1

Créer un nouveau projet : étape 2


ÉTAPE 3 : Copiez l'URL du point de terminaison WebSocket du réseau de test Rinkeby dans votre fichier .env .


Clés Rinkeby Testnet


Ensuite, ajoutez votre phrase secrète Metamask et la clé privée de votre compte préféré. Si vous avez fait cela correctement, vos variables d'environnement devraient maintenant ressembler à ceci.


 ENDPOINT_URL=*************************** DEPLOYER_KEY=********************** REACT_APP_COMET_CHAT_REGION=** REACT_APP_COMET_CHAT_APP_ID=************** REACT_APP_COMET_CHAT_AUTH_KEY=******************************


Si vous ne savez pas comment accéder à votre clé privée, consultez la section ci-dessous.

Accéder à votre clé privée Metamask

ÉTAPE 1 : Cliquez sur votre extension de navigateur Metamask et assurez-vous que Rinkeby est sélectionné comme réseau de test. Ensuite, sur le compte préféré, cliquez sur la ligne pointillée verticale et sélectionnez les détails du compte. Voir l'image ci-dessous.


La première étape


ÉTAPE 2 : Saisissez votre mot de passe dans le champ prévu à cet effet et cliquez sur le bouton de confirmation, cela vous permettra d'accéder à la clé privée de votre compte.


Deuxième étape


ÉTAPE 3 : Cliquez sur "exporter la clé privée" pour voir votre clé privée. Assurez-vous de ne jamais exposer vos clés sur une page publique telle que Github . C'est pourquoi nous l'ajoutons en tant que variable d'environnement.


Troisième étape


ÉTAPE 4 : Copiez votre clé privée dans votre fichier .env. Voir l'image et l'extrait de code ci-dessous :


Étape 4


 ENDPOINT_URL=*************************** SECRET_KEY=****************** DEPLOYER_KEY=********************** REACT_APP_COMET_CHAT_REGION=** REACT_APP_COMET_CHAT_APP_ID=************** REACT_APP_COMET_CHAT_AUTH_KEY=******************************


En ce qui concerne votre SECRET_KEY , vous devez coller votre phrase secrète Metamask dans l'espace prévu dans le fichier d'environnement.

Le contrat intelligent Dominion DAO

Voici le code complet du smart contract, je vais vous expliquer toutes les fonctions et variables les unes après les autres.


 // SPDX-License-Identifier: MIT pragma solidity ^0.8.7; import "@openzeppelin/contracts/access/AccessControl.sol"; import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; contract DominionDAO is ReentrancyGuard, AccessControl { bytes32 private immutable CONTRIBUTOR_ROLE = keccak256("CONTRIBUTOR"); bytes32 private immutable STAKEHOLDER_ROLE = keccak256("STAKEHOLDER"); uint32 immutable MIN_VOTE_DURATION = 1 weeks; uint256 totalProposals; uint256 public daoBalance; mapping(uint256 => ProposalStruct) private raisedProposals; mapping(address => uint256[]) private stakeholderVotes; mapping(uint256 => VotedStruct[]) private votedOn; mapping(address => uint256) private contributors; mapping(address => uint256) private stakeholders; struct ProposalStruct { uint256 id; uint256 amount; uint256 duration; uint256 upvotes; uint256 downvotes; string title; string description; bool passed; bool paid; address payable beneficiary; address proposer; address executor; } struct VotedStruct { address voter; uint256 timestamp; bool choosen; } event Action( address indexed initiator, bytes32 role, string message, address indexed beneficiary, uint256 amount ); modifier stakeholderOnly(string memory message) { require(hasRole(STAKEHOLDER_ROLE, msg.sender), message); _; } modifier contributorOnly(string memory message) { require(hasRole(CONTRIBUTOR_ROLE, msg.sender), message); _; } function createProposal( string calldata title, string calldata description, address beneficiary, uint256 amount )external stakeholderOnly("Proposal Creation Allowed for Stakeholders only") { uint256 proposalId = totalProposals++; ProposalStruct storage proposal = raisedProposals[proposalId]; proposal.id = proposalId; proposal.proposer = payable(msg.sender); proposal.title = title; proposal.description = description; proposal.beneficiary = payable(beneficiary); proposal.amount = amount; proposal.duration = block.timestamp + MIN_VOTE_DURATION; emit Action( msg.sender, CONTRIBUTOR_ROLE, "PROPOSAL RAISED", beneficiary, amount ); } function performVote(uint256 proposalId, bool choosen) external stakeholderOnly("Unauthorized: Stakeholders only") { ProposalStruct storage proposal = raisedProposals[proposalId]; handleVoting(proposal); if (choosen) proposal.upvotes++; else proposal.downvotes++; stakeholderVotes[msg.sender].push(proposal.id); votedOn[proposal.id].push( VotedStruct( msg.sender, block.timestamp, choosen ) ); emit Action( msg.sender, STAKEHOLDER_ROLE, "PROPOSAL VOTE", proposal.beneficiary, proposal.amount ); } function handleVoting(ProposalStruct storage proposal) private { if ( proposal.passed || proposal.duration <= block.timestamp ) { proposal.passed = true; revert("Proposal duration expired"); } uint256[] memory tempVotes = stakeholderVotes[msg.sender]; for (uint256 votes = 0; votes < tempVotes.length; votes++) { if (proposal.id == tempVotes[votes]) revert("Double voting not allowed"); } } function payBeneficiary(uint256 proposalId) external stakeholderOnly("Unauthorized: Stakeholders only") returns (bool) { ProposalStruct storage proposal = raisedProposals[proposalId]; require(daoBalance >= proposal.amount, "Insufficient fund"); require(block.timestamp > proposal.duration, "Proposal still ongoing"); if (proposal.paid) revert("Payment sent before"); if (proposal.upvotes <= proposal.downvotes) revert("Insufficient votes"); payTo(proposal.beneficiary, proposal.amount); proposal.paid = true; proposal.executor = msg.sender; daoBalance -= proposal.amount; emit Action( msg.sender, STAKEHOLDER_ROLE, "PAYMENT TRANSFERED", proposal.beneficiary, proposal.amount ); return true; } function contribute() payable external { if (!hasRole(STAKEHOLDER_ROLE, msg.sender)) { uint256 totalContribution = contributors[msg.sender] + msg.value; if (totalContribution >= 5 ether) { stakeholders[msg.sender] = totalContribution; contributors[msg.sender] += msg.value; _setupRole(STAKEHOLDER_ROLE, msg.sender); _setupRole(CONTRIBUTOR_ROLE, msg.sender); } else { contributors[msg.sender] += msg.value; _setupRole(CONTRIBUTOR_ROLE, msg.sender); } } else { contributors[msg.sender] += msg.value; stakeholders[msg.sender] += msg.value; } daoBalance += msg.value; emit Action( msg.sender, STAKEHOLDER_ROLE, "CONTRIBUTION RECEIVED", address(this), msg.value ); } function getProposals() external view returns (ProposalStruct[] memory props) { props = new ProposalStruct[](totalProposals); for (uint256 i = 0; i < totalProposals; i++) { props[i] = raisedProposals[i]; } } function getProposal(uint256 proposalId) external view returns (ProposalStruct memory) { return raisedProposals[proposalId]; } function getVotesOf(uint256 proposalId) external view returns (VotedStruct[] memory) { return votedOn[proposalId]; } function getStakeholderVotes() external view stakeholderOnly("Unauthorized: not a stakeholder") returns (uint256[] memory) { return stakeholderVotes[msg.sender]; } function getStakeholderBalance() external view stakeholderOnly("Unauthorized: not a stakeholder") returns (uint256) { return stakeholders[msg.sender]; } function isStakeholder() external view returns (bool) { return stakeholders[msg.sender] > 0; } function getContributorBalance() external view contributorOnly("Denied: User is not a contributor") returns (uint256) { return contributors[msg.sender]; } function isContributor() external view returns (bool) { return contributors[msg.sender] > 0; } function getBalance() external view returns (uint256) { return contributors[msg.sender]; } function payTo( address to, uint256 amount ) internal returns (bool) { (bool success,) = payable(to).call{value: amount}(""); require(success, "Payment failed"); return true; } }


Dans le projet que vous venez de cloner, dirigez-vous vers le répertoire src >> contract et créez un fichier nommé DominionDAO.sol , puis collez les codes ci-dessus à l'intérieur.


Explication:


 // SPDX-License-Identifier: MIT pragma solidity ^0.8.7;


Solidity nécessite un identifiant de licence pour compiler votre code, sinon il produira un avertissement vous demandant d'en spécifier un. De plus, Solidity exige que vous spécifiiez la version du compilateur pour votre contrat intelligent. C'est ce que représente le mot pragma .


 import "@openzeppelin/contracts/access/AccessControl.sol"; import "@openzeppelin/contracts/security/ReentrancyGuard.sol";


Dans le bloc de code ci-dessus, nous utilisons deux contrats intelligents openzeppelin's pour spécifier les rôles et protéger notre contrat intelligent contre les attaques de réentrance.


 bytes32 private immutable CONTRIBUTOR_ROLE = keccak256("CONTRIBUTOR"); bytes32 private immutable STAKEHOLDER_ROLE = keccak256("STAKEHOLDER"); uint32 immutable MIN_VOTE_DURATION = 1 weeks; uint256 totalProposals; uint256 public daoBalance;


Nous avons défini des variables d'état pour les rôles des parties prenantes et des contributeurs et spécifié que la durée minimale du vote était d'une semaine. Et nous avons également initialisé le compteur de proposition totale et une variable pour garder une trace de notre solde disponible.


 mapping(uint256 => ProposalStruct) private raisedProposals; mapping(address => uint256[]) private stakeholderVotes; mapping(uint256 => VotedStruct[]) private votedOn; mapping(address => uint256) private contributors; mapping(address => uint256) private stakeholders;


raisedProposals garde une trace de toutes les propositions soumises à notre contrat intelligent. stakeholderVotes , comme son nom l'indique, permet de suivre les votes des parties prenantes. votedOn garde une trace de tous les votes associés à une proposition. Alors que les contributeurs gardent une trace de tous ceux qui ont fait un don à notre plateforme, les parties prenantes, quant à elles, gardent une trace des personnes qui ont contribué jusqu'à 1 ether .


 struct ProposalStruct { uint256 id; uint256 amount; uint256 duration; uint256 upvotes; uint256 downvotes; string title; string description; bool passed; bool paid; address payable beneficiary; address proposer; address executor; } struct VotedStruct { address voter; uint256 timestamp; bool choosen; }


proposalStruct décrit le contenu de chaque proposition tandis que votedStruct décrit le contenu de chaque vote.


 event Action( address indexed initiator, bytes32 role, string message, address indexed beneficiary, uint256 amount );


Il s'agit d'un événement dynamique appelé Action. Cela nous aidera à enrichir les informations déconnectées par transaction.


 modifier stakeholderOnly(string memory message) { require(hasRole(STAKEHOLDER_ROLE, msg.sender), message); _; } modifier contributorOnly(string memory message) { require(hasRole(CONTRIBUTOR_ROLE, msg.sender), message); _; }


Les modificateurs ci-dessus nous aident à identifier les utilisateurs par rôle et les empêchent également d'accéder à certaines ressources non autorisées.


 function createProposal( string calldata title, string calldata description, address beneficiary, uint256 amount )external stakeholderOnly("Proposal Creation Allowed for Stakeholders only") { uint256 proposalId = totalProposals++; ProposalStruct storage proposal = raisedProposals[proposalId]; proposal.id = proposalId; proposal.proposer = payable(msg.sender); proposal.title = title; proposal.description = description; proposal.beneficiary = payable(beneficiary); proposal.amount = amount; proposal.duration = block.timestamp + MIN_VOTE_DURATION; emit Action( msg.sender, CONTRIBUTOR_ROLE, "PROPOSAL RAISED", beneficiary, amount ); }


La fonction ci-dessus prend le titre, la description, le montant et l'adresse du portefeuille du bénéficiaire d'une proposition et crée une proposition. La fonction permet uniquement aux parties prenantes de créer des propositions. Les parties prenantes sont les utilisateurs qui ont apporté au moins une contribution de 1 ether .


 function performVote(uint256 proposalId, bool choosen) external stakeholderOnly("Unauthorized: Stakeholders only") { ProposalStruct storage proposal = raisedProposals[proposalId]; handleVoting(proposal); if (choosen) proposal.upvotes++; else proposal.downvotes++; stakeholderVotes[msg.sender].push(proposal.id); votedOn[proposal.id].push( VotedStruct( msg.sender, block.timestamp, choosen ) ); emit Action( msg.sender, STAKEHOLDER_ROLE, "PROPOSAL VOTE", proposal.beneficiary, proposal.amount ); }


Cette fonction accepte deux arguments, un ID de proposition et un choix préféré représenté par une valeur booléenne. Vrai signifie que vous avez accepté le vote et Faux représente un rejet.


 function handleVoting(ProposalStruct storage proposal) private { if ( proposal.passed || proposal.duration <= block.timestamp ) { proposal.passed = true; revert("Proposal duration expired"); } uint256[] memory tempVotes = stakeholderVotes[msg.sender]; for (uint256 votes = 0; votes < tempVotes.length; votes++) { if (proposal.id == tempVotes[votes]) revert("Double voting not allowed"); } }


Cette fonction effectue le vote réel, y compris la vérification si un utilisateur est une partie prenante et qualifié pour voter.


 function payBeneficiary(uint256 proposalId) external stakeholderOnly("Unauthorized: Stakeholders only") returns (bool) { ProposalStruct storage proposal = raisedProposals[proposalId]; require(daoBalance >= proposal.amount, "Insufficient fund"); require(block.timestamp > proposal.duration, "Proposal still ongoing"); if (proposal.paid) revert("Payment sent before"); if (proposal.upvotes <= proposal.downvotes) revert("Insufficient votes"); payTo(proposal.beneficiary, proposal.amount); proposal.paid = true; proposal.executor = msg.sender; daoBalance -= proposal.amount; emit Action( msg.sender, STAKEHOLDER_ROLE, "PAYMENT TRANSFERED", proposal.beneficiary, proposal.amount ); return true; }


Cette fonction est chargée de payer le bénéficiaire attaché à une proposition en fonction de certains critères.


  • Premièrement, le bénéficiaire ne doit pas déjà être payé.
  • Deuxièmement, la durée de la proposition doit avoir expiré.
  • Troisièmement, le solde disponible doit être en mesure de payer le bénéficiaire.
  • Quatrièmement, il ne doit y avoir aucune égalité dans le nombre de votes.


 function contribute() payable external { if (!hasRole(STAKEHOLDER_ROLE, msg.sender)) { uint256 totalContribution = contributors[msg.sender] + msg.value; if (totalContribution >= 5 ether) { stakeholders[msg.sender] = totalContribution; contributors[msg.sender] += msg.value; _setupRole(STAKEHOLDER_ROLE, msg.sender); _setupRole(CONTRIBUTOR_ROLE, msg.sender); } else { contributors[msg.sender] += msg.value; _setupRole(CONTRIBUTOR_ROLE, msg.sender); } } else { contributors[msg.sender] += msg.value; stakeholders[msg.sender] += msg.value; } daoBalance += msg.value; emit Action( msg.sender, STAKEHOLDER_ROLE, "CONTRIBUTION RECEIVED", address(this), msg.value ); }


Cette fonction est chargée de collecter les contributions des donateurs et des personnes intéressées à devenir parties prenantes.


 function getProposals() external view returns (ProposalStruct[] memory props) { props = new ProposalStruct[](totalProposals); for (uint256 i = 0; i < totalProposals; i++) { props[i] = raisedProposals[i]; } }


Cette fonction récupère un tableau de propositions enregistrées sur ce contrat intelligent.


 function getProposal(uint256 proposalId) external view returns (ProposalStruct memory) { return raisedProposals[proposalId]; }


Cette fonction récupère une proposition particulière par Id.


 function getVotesOf(uint256 proposalId) external view returns (VotedStruct[] memory) { return votedOn[proposalId]; }


Cela renvoie une liste de votes associés à une proposition particulière.


 function getStakeholderVotes() external view stakeholderOnly("Unauthorized: not a stakeholder") returns (uint256[] memory) { return stakeholderVotes[msg.sender]; }


Cela renvoie la liste des parties prenantes sur le contrat intelligent et seule une partie prenante peut appeler cette fonction.


 function getStakeholderBalance() external view stakeholderOnly("Unauthorized: not a stakeholder") returns (uint256) { return stakeholders[msg.sender]; }


Cela renvoie le montant d'argent contribué par les parties prenantes.


 function isStakeholder() external view returns (bool) { return stakeholders[msg.sender] > 0; }


Renvoie True ou False si un utilisateur est une partie prenante.


 function getContributorBalance() external view contributorOnly("Denied: User is not a contributor") returns (uint256) { return contributors[msg.sender]; }


Cela renvoie le solde d'un contributeur et n'est accessible qu'au contributeur.


 function isContributor() external view returns (bool) { return contributors[msg.sender] > 0; }


Cela vérifie si un utilisateur est un contributeur ou non et il est représenté par True ou False.


 function getBalance() external view returns (uint256) { return contributors[msg.sender]; }


Renvoie le solde de l'utilisateur appelant quel que soit son rôle.


 function payTo( address to, uint256 amount ) internal returns (bool) { (bool success,) = payable(to).call{value: amount}(""); require(success, "Payment failed"); return true; }


Cette fonction effectue un paiement avec un montant et un compte spécifiés.

Configuration du script de déploiement

Une autre chose à faire avec le contrat intelligent est de configurer le script de déploiement.

Sur le projet, dirigez-vous vers le dossier migrations , >> 2_deploy_contracts.js, et mettez-le à jour avec l'extrait de code ci-dessous.


 const DominionDAO = artifacts.require('DominionDAO') module.exports = async function (deployer) { await deployer.deploy(DominionDAO) }


Fantastique, nous venons de terminer le contrat intelligent pour notre application, il est temps de commencer à créer l'interface Dapp.

Développer le front-end

La partie avant comprend de nombreux composants et pièces. Nous allons créer tous les composants, les vues et le reste des périphériques.


Composant d'en-tête

Mode sombre

Mode lumière


Ce composant capture des informations sur l'utilisateur actuel et comporte un bouton de basculement de thème pour les modes clair et sombre. Et si vous vous demandez comment j'ai fait cela, c'est via Tailwind CSS, voir le code ci-dessous.


 import { useState, useEffect } from 'react' import { FaUserSecret } from 'react-icons/fa' import { MdLightMode } from 'react-icons/md' import { FaMoon } from 'react-icons/fa' import { Link } from 'react-router-dom' import { connectWallet } from '../Dominion' import { useGlobalState, truncate } from '../store' const Header = () => { const [theme, setTheme] = useState(localStorage.theme) const themeColor = theme === 'dark' ? 'light' : 'dark' const darken = theme === 'dark' ? true : false const [connectedAccount] = useGlobalState('connectedAccount') useEffect(() => { const root = window.document.documentElement root.classList.remove(themeColor) root.classList.add(theme) localStorage.setItem('theme', theme) }, [themeColor, theme]) const toggleLight = () => { const root = window.document.documentElement root.classList.remove(themeColor) root.classList.add(theme) localStorage.setItem('theme', theme) setTheme(themeColor) } return ( <header className="sticky top-0 z-50 dark:text-blue-500"> <nav className="navbar navbar-expand-lg shadow-md py-2 relative flex items-center w-full justify-between bg-white dark:bg-[#212936]"> <div className="px-6 w-full flex flex-wrap items-center justify-between"> <div className="navbar-collapse collapse grow flex flex-row justify-between items-center p-2"> <Link to={'/'} className="flex flex-row justify-start items-center space-x-3" > <FaUserSecret className="cursor-pointer" size={25} /> <span className="invisible md:visible dark:text-gray-300"> Dominion </span> </Link> <div className="flex flex-row justify-center items-center space-x-5"> {darken ? ( <MdLightMode className="cursor-pointer" size={25} onClick={toggleLight} /> ) : ( <FaMoon className="cursor-pointer" size={25} onClick={toggleLight} /> )} {connectedAccount ? ( <button className="px-4 py-2.5 bg-blue-600 text-white font-medium text-xs leading-tight uppercase rounded-full 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 dark:text-blue-500 dark:border dark:border-blue-500 dark:bg-transparent" > {truncate(connectedAccount, 4, 4, 11)} </button> ) : ( <button className="px-4 py-2.5 bg-blue-600 text-white font-medium text-xs leading-tight uppercase rounded-full 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 dark:text-blue-500 dark:border dark:border-blue-500 dark:bg-transparent" onClick={connectWallet} > Connect Wallet </button> )} </div> </div> </div> </nav> </header> ) } export default Header


Composant de bannière

Composant de bannière


Ce composant contient des informations sur l'état actuel du DAO, telles que le solde total et le nombre de propositions ouvertes.


Ce composant inclut également la possibilité d'utiliser la fonction de contribution pour générer une nouvelle proposition. Regardez le code ci-dessous.


 import { useState } from 'react' import { setGlobalState, useGlobalState } from '../store' import { performContribute } from '../Dominion' import { toast } from 'react-toastify' const Banner = () => { const [isStakeholder] = useGlobalState('isStakeholder') const [proposals] = useGlobalState('proposals') const [connectedAccount] = useGlobalState('connectedAccount') const [currentUser] = useGlobalState('currentUser') const [balance] = useGlobalState('balance') const [mybalance] = useGlobalState('mybalance') const [amount, setAmount] = useState('') const onPropose = () => { if (!isStakeholder) return setGlobalState('createModal', 'scale-100') } const onContribute = () => { if (!!!amount || amount == '') return toast.info('Contribution in progress...') performContribute(amount).then((bal) => { if (!!!bal.message) { setGlobalState('balance', Number(balance) + Number(bal)) setGlobalState('mybalance', Number(mybalance) + Number(bal)) setAmount('') toast.success('Contribution received') } }) } const opened = () => proposals.filter( (proposal) => new Date().getTime() < Number(proposal.duration + '000') ).length return ( <div className="p-8"> <h2 className="font-semibold text-3xl mb-5"> {opened()} Proposal{opened() == 1 ? '' : 's'} Currenly Opened </h2> <p> Current DAO Balance: <strong>{balance} Eth</strong> <br /> Your contributions:{' '} <span> <strong>{mybalance} Eth</strong> {isStakeholder ? ', and you are now a stakeholder 😊' : null} </span> </p> <hr className="my-6 border-gray-300 dark:border-gray-500" /> <p> {isStakeholder ? 'You can now raise proposals on this platform 😆' : 'Hey, when you contribute upto 1 ether you become a stakeholder 😎'} </p> <div className="flex flex-row justify-start items-center md:w-1/3 w-full mt-4"> <input type="number" className="form-control block w-full px-3 py-1.5 text-base font-normaltext-gray-700 bg-clip-padding border border-solid border-gray-300 rounded transition ease-in-out m-0 shadow-md focus:text-gray-500 focus:outline-none dark:border-gray-500 dark:bg-transparent" placeholder="eg 2.5 Eth" onChange={(e) => setAmount(e.target.value)} value={amount} required /> </div> <div className="flex flex-row justify-start items-center space-x-3 mt-4" role="group" > <button type="button" className={`inline-block px-6 py-2.5 bg-blue-600 text-white font-medium text-xs leading-tight uppercase shadow-md rounded-full 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 dark:text-blue-500 dark:border dark:border-blue-500 dark:bg-transparent`} data-mdb-ripple="true" data-mdb-ripple-color="light" onClick={onContribute} > Contribute </button> {isStakeholder ? ( <button type="button" className={`inline-block px-6 py-2.5 bg-blue-600 text-white font-medium text-xs leading-tight uppercase shadow-md rounded-full 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 dark:text-blue-500 dark:border dark:border-blue-500 dark:bg-transparent`} data-mdb-ripple="true" data-mdb-ripple-color="light" onClick={onPropose} > Propose </button> ) : null} {currentUser && currentUser.uid == connectedAccount.toLowerCase() ? null : ( <button type="button" className={`inline-block px-6 py-2.5 bg-blue-600 text-white font-medium text-xs leading-tight uppercase shadow-md rounded-full 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 dark:border dark:border-blue-500`} data-mdb-ripple="true" data-mdb-ripple-color="light" onClick={() => setGlobalState('loginModal', 'scale-100')} > Login Chat </button> )} </div> </div> ) } export default Banner


Composante Propositions

Composants des propositions

Ce composant contient une liste de propositions dans notre contrat intelligent. Vous permet également de filtrer entre les propositions fermées et ouvertes. À l'expiration d'une proposition, un bouton de paiement devient disponible, ce qui donne à une partie prenante la possibilité de payer le montant associé à la proposition. Voir le code ci-dessous.


 import Identicon from 'react-identicons' import { useState } from 'react' import { Link } from 'react-router-dom' import { truncate, useGlobalState, daysRemaining } from '../store' import { payoutBeneficiary } from '../Dominion' import { toast } from 'react-toastify' const Proposals = () => { const [data] = useGlobalState('proposals') const [proposals, setProposals] = useState(data) const deactive = `bg-transparent text-blue-600 font-medium text-xs leading-tight uppercase hover:bg-blue-700 focus:bg-blue-700 focus:outline-none focus:ring-0 active:bg-blue-600 transition duration-150 ease-in-out overflow-hidden border border-blue-600 hover:text-white focus:text-white` const active = `bg-blue-600 text-white font-medium text-xs leading-tight uppercase hover:bg-blue-700 focus:bg-blue-700 focus:outline-none focus:ring-0 active:bg-blue-800 transition duration-150 ease-in-out overflow-hidden border border-blue-600` const getAll = () => setProposals(data) const getOpened = () => setProposals( data.filter( (proposal) => new Date().getTime() < Number(proposal.duration + '000') ) ) const getClosed = () => setProposals( data.filter( (proposal) => new Date().getTime() > Number(proposal.duration + '000') ) ) const handlePayout = (id) => { payoutBeneficiary(id).then((res) => { if (!!!res.code) { toast.success('Beneficiary successfully Paid Out!') window.location.reload() } }) } return ( <div className="flex flex-col p-8"> <div className="flex flex-row justify-center items-center" role="group"> <button aria-current="page" className={`rounded-l-full px-6 py-2.5 ${active}`} onClick={getAll} > All </button> <button aria-current="page" className={`px-6 py-2.5 ${deactive}`} onClick={getOpened} > Open </button> <button aria-current="page" className={`rounded-r-full px-6 py-2.5 ${deactive}`} onClick={getClosed} > Closed </button> </div> <div className="overflow-x-auto sm:-mx-6 lg:-mx-8"> <div className="py-2 inline-block min-w-full sm:px-6 lg:px-8"> <div className="h-[calc(100vh_-_20rem)] overflow-y-auto shadow-md rounded-md"> <table className="min-w-full"> <thead className="border-b dark:border-gray-500"> <tr> <th scope="col" className="text-sm font-medium px-6 py-4 text-left" > Created By </th> <th scope="col" className="text-sm font-medium px-6 py-4 text-left" > Title </th> <th scope="col" className="text-sm font-medium px-6 py-4 text-left" > Expires </th> <th scope="col" className="text-sm font-medium px-6 py-4 text-left" > Action </th> </tr> </thead> <tbody> {proposals.map((proposal) => ( <tr key={proposal.id} className="border-b dark:border-gray-500" > <td className="text-sm font-light px-6 py-4 whitespace-nowrap"> <div className="flex flex-row justify-start items-center space-x-3"> <Identicon string={proposal.proposer.toLowerCase()} size={25} className="h-10 w-10 object-contain rounded-full mr-3" /> <span>{truncate(proposal.proposer, 4, 4, 11)}</span> </div> </td> <td className="text-sm font-light px-6 py-4 whitespace-nowrap"> {proposal.title.substring(0, 80) + '...'} </td> <td className="text-sm font-light px-6 py-4 whitespace-nowrap"> {new Date().getTime() > Number(proposal.duration + '000') ? 'Expired' : daysRemaining(proposal.duration)} </td> <td className="flex justify-start items-center space-x-3 text-sm font-light px-6 py-4 whitespace-nowrap" > <Link to={'/proposal/' + proposal.id} className="dark:border rounded-full px-6 py-2.5 dark:border-blue-600 dark:text-blue-600 dark:bg-transparent font-medium text-xs leading-tight uppercase hover:border-blue-700 focus:border-blue-700 focus:outline-none focus:ring-0 active:border-blue-800 transition duration-150 ease-in-out text-white bg-blue-600" > View </Link> {new Date().getTime() > Number(proposal.duration + '000') ? ( proposal.upvotes > proposal.downvotes ? ( !proposal.paid ? ( <button className="dark:border rounded-full px-6 py-2.5 dark:border-red-600 dark:text-red-600 dark:bg-transparent font-medium text-xs leading-tight uppercase hover:border-red-700 focus:border-red-700 focus:outline-none focus:ring-0 active:border-red-800 transition duration-150 ease-in-out text-white bg-red-600" onClick={() => handlePayout(proposal.id)} > Payout </button> ) : ( <button className="dark:border rounded-full px-6 py-2.5 dark:border-green-600 dark:text-green-600 dark:bg-transparent font-medium text-xs leading-tight uppercase hover:border-green-700 focus:border-green-700 focus:outline-none focus:ring-0 active:border-green-800 transition duration-150 ease-in-out text-white bg-green-600" > Paid </button> ) ) : ( <button className="dark:border rounded-full px-6 py-2.5 dark:border-red-600 dark:text-red-600 dark:bg-transparent font-medium text-xs leading-tight uppercase hover:border-red-700 focus:border-red-700 focus:outline-none focus:ring-0 active:border-red-800 transition duration-150 ease-in-out text-white bg-red-600" > Rejected </button> ) ) : null} </td> </tr> ))} </tbody> </table> </div> </div> </div> </div> ) } export default Proposals


Le composant Détails de la proposition

Détails de la proposition


Ce composant affiche des informations sur la proposition actuelle, y compris le coût. Ce composant permet aux parties prenantes d'accepter ou de rejeter une proposition.


Le proposant peut former un groupe et les autres utilisateurs de la plate-forme peuvent s'engager dans une discussion anonyme de style web3.0.


Ce composant comprend également un graphique à barres qui vous permet de voir le rapport entre les acceptés et les rejetés. Regardez le code ci-dessous.


 import moment from 'moment' import { useEffect, useState } from 'react' import { useParams, useNavigate } from 'react-router-dom' import { toast } from 'react-toastify' import { getGroup, createNewGroup, joinGroup } from '../CometChat' import { BarChart, Bar, CartesianGrid, XAxis, YAxis, Legend, Tooltip, } from 'recharts' import { getProposal, voteOnProposal } from '../Dominion' import { useGlobalState } from '../store' const ProposalDetails = () => { const { id } = useParams() const navigator = useNavigate() const [proposal, setProposal] = useState(null) const [group, setGroup] = useState(null) const [data, setData] = useState([]) const [isStakeholder] = useGlobalState('isStakeholder') const [connectedAccount] = useGlobalState('connectedAccount') const [currentUser] = useGlobalState('currentUser') useEffect(() => { retrieveProposal() getGroup(`pid_${id}`).then((group) => { if (!!!group.code) setGroup(group) console.log(group) }) }, [id]) const retrieveProposal = () => { getProposal(id).then((res) => { setProposal(res) setData([ { name: 'Voters', Acceptees: res?.upvotes, Rejectees: res?.downvotes, }, ]) }) } const onVote = (choice) => { if (new Date().getTime() > Number(proposal.duration + '000')) { toast.warning('Proposal expired!') return } voteOnProposal(id, choice).then((res) => { if (!!!res.code) { toast.success('Voted successfully!') window.location.reload() } }) } const daysRemaining = (days) => { const todaysdate = moment() days = Number((days + '000').slice(0)) days = moment(days).format('YYYY-MM-DD') days = moment(days) days = days.diff(todaysdate, 'days') return days == 1 ? '1 day' : days + ' days' } const onEnterChat = () => { if (group.hasJoined) { navigator(`/chat/${`pid_${id}`}`) } else { joinGroup(`pid_${id}`).then((res) => { if (!!res) { navigator(`/chat/${`pid_${id}`}`) console.log('Success joining: ', res) } else { console.log('Error Joining Group: ', res) } }) } } const onCreateGroup = () => { createNewGroup(`pid_${id}`, proposal.title).then((group) => { if (!!!group.code) { toast.success('Group created successfully!') setGroup(group) } else { console.log('Error Creating Group: ', group) } }) } return ( <div className="p-8"> <h2 className="font-semibold text-3xl mb-5">{proposal?.title}</h2> <p> This proposal is to payout <strong>{proposal?.amount} Eth</strong> and currently have{' '} <strong>{proposal?.upvotes + proposal?.downvotes} votes</strong> and will expire in <strong>{daysRemaining(proposal?.duration)}</strong> </p> <hr className="my-6 border-gray-300" /> <p>{proposal?.description}</p> <div className="flex flex-row justify-start items-center w-full mt-4 overflow-auto"> <BarChart width={730} height={250} data={data}> <CartesianGrid strokeDasharray="3 3" /> <XAxis dataKey="name" /> <YAxis /> <Tooltip /> <Legend /> <Bar dataKey="Acceptees" fill="#2563eb" /> <Bar dataKey="Rejectees" fill="#dc2626" /> </BarChart> </div> <div className="flex flex-row justify-start items-center space-x-3 mt-4" role="group" > {isStakeholder ? ( <> <button type="button" className="inline-block px-6 py-2.5 bg-blue-600 text-white font-medium text-xs leading-tight uppercase rounded-full 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 dark:text-gray-300 dark:border dark:border-gray-500 dark:bg-transparent" data-mdb-ripple="true" data-mdb-ripple-color="light" onClick={() => onVote(true)} > Accept </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-full 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 dark:border dark:border-gray-500 dark:bg-transparent" data-mdb-ripple="true" data-mdb-ripple-color="light" onClick={() => onVote(false)} > Reject </button> {currentUser && currentUser.uid.toLowerCase() == proposal?.proposer.toLowerCase() && !group ? ( <button type="button" className="inline-block px-6 py-2.5 bg-blue-600 text-white font-medium text-xs leading-tight uppercase rounded-full 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 dark:border dark:border-blue-500" data-mdb-ripple="true" data-mdb-ripple-color="light" onClick={onCreateGroup} > Create Group </button> ) : null} </> ) : null} {currentUser && currentUser.uid.toLowerCase() == connectedAccount.toLowerCase() && !!!group?.code && group != null ? ( <button type="button" className="inline-block px-6 py-2.5 bg-blue-600 text-white font-medium text-xs leading-tight uppercase rounded-full 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 dark:border dark:border-blue-500" data-mdb-ripple="true" data-mdb-ripple-color="light" onClick={onEnterChat} > Chat </button> ) : null} {proposal?.proposer.toLowerCase() != connectedAccount.toLowerCase() && !!!group ? ( <button type="button" className="inline-block px-6 py-2.5 bg-blue-600 dark:bg-transparent text-white font-medium text-xs leading-tight uppercase rounded-full shadow-md hover:border-blue-700 hover:shadow-lg focus:border-blue-700 focus:shadow-lg focus:outline-none focus:ring-0 active:border-blue-800 active:shadow-lg transition duration-150 ease-in-out dark:text-blue-500 dark:border dark:border-blue-500 disabled:bg-blue-300" data-mdb-ripple="true" data-mdb-ripple-color="light" disabled > Group N/A </button> ) : null} </div> </div> ) } export default ProposalDetails


Composante des électeurs

Composante des électeurs


Ce composant répertorie simplement les parties prenantes qui ont voté sur une proposition. Le composant offre également à un utilisateur la possibilité de filtrer entre les personnes rejetées et acceptées. Voir le code ci-dessous.


 import Identicon from 'react-identicons' import moment from 'moment' import { useState, useEffect } from 'react' import { useParams } from 'react-router-dom' import { truncate } from '../store' import { listVoters } from '../Dominion' const Voters = () => { const [voters, setVoters] = useState([]) const [data, setData] = useState([]) const { id } = useParams() const timeAgo = (timestamp) => moment(Number(timestamp + '000')).fromNow() const deactive = `bg-transparent text-blue-600 font-medium text-xs leading-tight uppercase hover:bg-blue-700 focus:bg-blue-700 focus:outline-none focus:ring-0 active:bg-blue-600 transition duration-150 ease-in-out overflow-hidden border border-blue-600 hover:text-white focus:text-white` const active = `bg-blue-600 text-white font-medium text-xs leading-tight uppercase hover:bg-blue-700 focus:bg-blue-700 focus:outline-none focus:ring-0 active:bg-blue-800 transition duration-150 ease-in-out overflow-hidden border border-blue-600` useEffect(() => { listVoters(id).then((res) => { setVoters(res) setData(res) }) }, [id]) const getAll = () => setVoters(data) const getAccepted = () => setVoters(data.filter((vote) => vote.choosen)) const getRejected = () => setVoters(data.filter((vote) => !vote.choosen)) return ( <div className="flex flex-col p-8"> <div className="flex flex-row justify-center items-center" role="group"> <button aria-current="page" className={`rounded-l-full px-6 py-2.5 ${active}`} onClick={getAll} > All </button> <button aria-current="page" className={`px-6 py-2.5 ${deactive}`} onClick={getAccepted} > Acceptees </button> <button aria-current="page" className={`rounded-r-full px-6 py-2.5 ${deactive}`} onClick={getRejected} > Rejectees </button> </div> <div className="overflow-x-auto sm:-mx-6 lg:-mx-8"> <div className="py-2 inline-block min-w-full sm:px-6 lg:px-8"> <div className="h-[calc(100vh_-_20rem)] overflow-y-auto shadow-md rounded-md"> <table className="min-w-full"> <thead className="border-b dark:border-gray-500"> <tr> <th scope="col" className="text-sm font-medium px-6 py-4 text-left" > Voter </th> <th scope="col" className="text-sm font-medium px-6 py-4 text-left" > Voted </th> <th scope="col" className="text-sm font-medium px-6 py-4 text-left" > Vote </th> </tr> </thead> <tbody> {voters.map((voter, i) => ( <tr key={i} className="border-b dark:border-gray-500 transition duration-300 ease-in-out" > <td className="text-sm font-light px-6 py-4 whitespace-nowrap"> <div className="flex flex-row justify-start items-center space-x-3"> <Identicon string={voter.voter.toLowerCase()} size={25} className="h-10 w-10 object-contain rounded-full mr-3" /> <span>{truncate(voter.voter, 4, 4, 11)}</span> </div> </td> <td className="text-sm font-light px-6 py-4 whitespace-nowrap"> {timeAgo(voter.timestamp)} </td> <td className="text-sm font-light px-6 py-4 whitespace-nowrap"> {voter.choosen ? ( <button className="border-2 rounded-full px-6 py-2.5 border-blue-600 text-blue-600 font-medium text-xs leading-tight uppercase hover:border-blue-700 focus:border-blue-700 focus:outline-none focus:ring-0 active:border-blue-800 transition duration-150 ease-in-out" > Accepted </button> ) : ( <button className="border-2 rounded-full px-6 py-2.5 border-red-600 text-red-600 font-medium text-xs leading-tight uppercase hover:border-red-700 focus:border-red-700 focus:outline-none focus:ring-0 active:border-red-800 transition duration-150 ease-in-out" > Rejected </button> )} </td> </tr> ))} </tbody> </table> </div> </div> </div> <div className="mt-4 text-center"> {voters.length >= 10 ? ( <button aria-current="page" className="rounded-full px-6 py-2.5 bg-blue-600 font-medium text-xs leading-tight uppercase hover:bg-blue-700 focus:bg-blue-700 focus:outline-none focus:ring-0 active:bg-blue-800 transition duration-150 ease-in-out dark:text-gray-300 dark:border dark:border-gray-500 dark:bg-transparent" > Load More </button> ) : null} </div> </div> ) } export default Voters


Composant de messages

Le composant Messages


Grâce à la puissance de CometChat SDK combinée à ce composant, les utilisateurs peuvent s'engager dans une conversation un à plusieurs de manière anonyme. Les contributeurs et les parties prenantes peuvent discuter d'une proposition plus loin dans leur processus de prise de décision ici. Tous les utilisateurs conservent leur anonymat et sont représentés par leurs Identicons.


 import Identicon from 'react-identicons' import { useState, useEffect } from 'react' import { useNavigate } from 'react-router-dom' import { truncate, useGlobalState } from '../store' import { getMessages, sendMessage, CometChat } from '../CometChat' const Messages = ({ gid }) => { const navigator = useNavigate() const [connectedAccount] = useGlobalState('connectedAccount') const [message, setMessage] = useState('') const [messages, setMessages] = useState([]) useEffect(() => { getMessages(gid).then((msgs) => { if (!!!msgs.code) setMessages(msgs.filter((msg) => msg.category == 'message')) }) listenForMessage(gid) }, [gid]) const listenForMessage = (listenerID) => { CometChat.addMessageListener( listenerID, new CometChat.MessageListener({ onTextMessageReceived: (message) => { setMessages((prevState) => [...prevState, message]) scrollToEnd() }, }) ) } const handleMessage = (e) => { e.preventDefault() sendMessage(gid, message).then((msg) => { if (!!!msg.code) { setMessages((prevState) => [...prevState, msg]) setMessage('') scrollToEnd() } }) } const scrollToEnd = () => { const elmnt = document.getElementById('messages-container') elmnt.scrollTop = elmnt.scrollHeight } const dateToTime = (date) => { let hours = date.getHours() let minutes = date.getMinutes() let ampm = hours >= 12 ? 'pm' : 'am' hours = hours % 12 hours = hours ? hours : 12 minutes = minutes < 10 ? '0' + minutes : minutes let strTime = hours + ':' + minutes + ' ' + ampm return strTime } return ( <div className="p-8"> <div className="flex flex-row justify-start"> <button className="px-4 py-2.5 bg-transparent hover:text-white font-bold text-xs leading-tight uppercase rounded-full 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={() => navigator(`/proposal/${gid.substr(4)}`)} > Exit Chat </button> </div> <div id="messages-container" className="h-[calc(100vh_-_16rem)] overflow-y-auto sm:pr-4 my-3" > {messages.map((message, i) => message.sender.uid.toLowerCase() != connectedAccount.toLowerCase() ? ( <div key={i} className="flex flex-row justify-start my-2"> <div className="flex flex-col bg-transparent w-80 p-3 px-5 rounded-3xl shadow-md"> <div className="flex flex-row justify-start items-center space-x-2"> <Identicon string={message.sender.uid.toLowerCase()} size={25} className="h-10 w-10 object-contain shadow-md rounded-full mr-3" /> <span>@{truncate(message.sender.uid, 4, 4, 11)}</span> <small>{dateToTime(new Date(message.sentAt * 1000))}</small> </div> <small className="leading-tight my-2">{message.text}</small> </div> </div> ) : ( <div key={i} className="flex flex-row justify-end my-2"> <div className="flex flex-col bg-transparent w-80 p-3 px-5 rounded-3xl shadow-md shadow-blue-300"> <div className="flex flex-row justify-start items-center space-x-2"> <Identicon string={connectedAccount.toLowerCase()} size={25} className="h-10 w-10 object-contain shadow-md rounded-full mr-3" /> <span>@you</span> <small>{dateToTime(new Date(message.sentAt * 1000))}</small> </div> <small className="leading-tight my-2">{message.text}</small> </div> </div> ) )} </div> <form onSubmit={handleMessage} className="flex flex-row"> <input className="w-full bg-transparent 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> ) } export default Messages


Créer un composant de proposition

Créer un composant de proposition

Ce composant vous permet simplement de faire une proposition en fournissant des informations sur les champs vus dans l'image ci-dessus. Voir le code ci-dessous.


 import { useState } from 'react' import { FaTimes } from 'react-icons/fa' import { raiseProposal } from '../Dominion' import { setGlobalState, useGlobalState } from '../store' import { toast } from 'react-toastify' const CreateProposal = () => { const [createModal] = useGlobalState('createModal') const [title, setTitle] = useState('') const [amount, setAmount] = useState('') const [beneficiary, setBeneficiary] = useState('') const [description, setDescription] = useState('') const handleSubmit = (e) => { e.preventDefault() if (!title || !description || !beneficiary || !amount) return const proposal = { title, description, beneficiary, amount } raiseProposal(proposal).then((proposed) => { if (proposed) { toast.success('Proposal created, reloading in progress...') closeModal() window.location.reload() } }) } const closeModal = () => { setGlobalState('createModal', 'scale-0') resetForm() } const resetForm = () => { setTitle('') setAmount('') setBeneficiary('') setDescription('') } return ( <div className={`fixed top-0 left-0 w-screen h-screen flex items-center justify-center bg-black bg-opacity-50 transform z-50 transition-transform duration-300 ${createModal}`} > <div className="bg-white dark:bg-[#212936] shadow-xl shadow-[#122643] dark:shadow-gray-500 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">Raise Proposal</p> <button type="button" onClick={closeModal} className="border-0 bg-transparent focus:outline-none" > <FaTimes /> </button> </div> <div className="flex flex-row justify-between items-center border border-gray-500 dark:border-gray-500 rounded-xl mt-5"> <input className="block w-full text-sm 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 border border-gray-500 dark:border-gray-500 rounded-xl mt-5"> <input className="block w-full text-sm bg-transparent border-0 focus:outline-none focus:ring-0" type="text" name="amount" placeholder="eg 2.5 Eth" onChange={(e) => setAmount(e.target.value)} value={amount} required /> </div> <div className="flex flex-row justify-between items-center border border-gray-500 dark:border-gray-500 rounded-xl mt-5"> <input className="block w-full text-sm bg-transparent border-0 focus:outline-none focus:ring-0" type="text" name="beneficiary" placeholder="Beneficiary Address" onChange={(e) => setBeneficiary(e.target.value)} value={beneficiary} required /> </div> <div className="flex flex-row justify-between items-center border border-gray-500 dark:border-gray-500 rounded-xl mt-5"> <textarea className="block w-full text-sm resize-none 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 className="rounded-lg px-6 py-2.5 bg-blue-600 text-white font-medium text-xs leading-tight uppercase hover:bg-blue-700 focus:bg-blue-700 focus:outline-none focus:ring-0 active:bg-blue-800 transition duration-150 ease-in-out mt-5" onClick={handleSubmit} > Submit Proposal </button> </form> </div> </div> ) } export default CreateProposal


Composant d'authentification

Composant d'authentification de chat


Ce composant vous aide à participer aux fonctionnalités de chat. Vous devez créer un compte ou vous connecter si vous êtes déjà inscrit. En vous connectant, vous pouvez participer à une discussion de groupe et avoir des conversations anonymes avec d'autres participants à une proposition dans un style web3.0. Voir le code ci-dessous.


 import { FaTimes } from 'react-icons/fa' import { loginWithCometChat, signInWithCometChat } from '../CometChat' import { setGlobalState, useGlobalState } from '../store' import { toast } from 'react-toastify' const ChatLogin = () => { const [loginModal] = useGlobalState('loginModal') const [connectedAccount] = useGlobalState('connectedAccount') const handleSignUp = () => { signInWithCometChat(connectedAccount, connectedAccount).then((user) => { if (!!!user.code) { toast.success('Account created, now click the login button.') } else { toast.error(user.message) } }) } const handleLogin = () => { loginWithCometChat(connectedAccount).then((user) => { if (!!!user.code) { setGlobalState('currentUser', user) toast.success('Logged in successful!') closeModal() } else { toast.error(user.message) } }) } const closeModal = () => { setGlobalState('loginModal', '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 ${loginModal}`} > <div className="bg-white dark:bg-[#212936] shadow-xl shadow-[#122643] dark:shadow-gray-500 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">Authenticate</p> <button type="button" onClick={closeModal} className="border-0 bg-transparent focus:outline-none" > <FaTimes /> </button> </div> <div className="my-2 font-light"> <span> Once you login, you will be enabled to chat with other stakeholders to make a well-informed voting. </span> </div> <div className="flex flex-row justify-between items-center mt-2" role="group" > <button className="rounded-lg px-6 py-2.5 bg-blue-600 text-white font-medium text-xs leading-tight uppercase hover:bg-blue-700 focus:bg-blue-700 focus:outline-none focus:ring-0 active:bg-blue-800 transition duration-150 ease-in-out mt-5" onClick={handleLogin} > Login </button> <button className="rounded-lg px-6 py-2.5 bg-transparent text-blue-600 font-medium text-xs leading-tight uppercase hover:bg-blue-700 hover:text-white focus:bg-blue-700 focus:outline-none focus:ring-0 active:bg-blue-800 transition duration-150 ease-in-out mt-5 border-blue-600" onClick={handleSignUp} > Create Account </button> </div> </div> </div> </div> ) } export default ChatLogin


Fantastique, assurons-nous que les points de vue sont bien représentés…


La vue d'accueil

La vue d'accueil


Cette vue inclut les composants d'en- header , de banner et de proposals pour fournir une expérience utilisateur DAO exceptionnelle. Nous avons également utilisé la puissance de Tailwind CSS pour obtenir ce look. Regardez le code ci-dessous.


 import Banner from '../components/Banner' import ChatLogin from '../components/ChatLogin' import CreateProposal from '../components/CreateProposal' import Header from '../components/Header' import Proposals from '../components/Proposals' const Home = () => { return ( <> <Header /> <Banner /> <Proposals /> <CreateProposal /> <ChatLogin /> </> ) } export default Home


La vue Proposition

La vue Proposition


Cette vue associe l'en-tête, les détails de la proposition et le composant votants pour rendre une présentation fluide d'un composant singulier. Voir le code ci-dessous.


 import Header from '../components/Header' import ProposalDetails from '../components/ProposalDetails' import Voters from '../components/Voters' const Proposal = () => { return ( <> <Header /> <ProposalDetails /> <Voters /> </> ) } export default Proposal


La vue de chat

La vue de chat


Enfin, la vue de chat intègre le composant d'en-tête et de messages pour rendre une interface de chat de qualité. Voir le code ci-dessous.


 import { useParams, useNavigate } from 'react-router-dom' import { useEffect, useState } from 'react' import { getGroup } from '../CometChat' import { toast } from 'react-toastify' import Header from '../components/Header' import Messages from '../components/Messages' const Chat = () => { const { gid } = useParams() const navigator = useNavigate() const [group, setGroup] = useState(null) useEffect(() => { getGroup(gid).then((group) => { if (!!!group.code) { setGroup(group) } else { toast.warning('Please join the group first!') navigator(`/proposal/${gid.substr(4)}`) } }) }, [gid]) return ( <> <Header /> <Messages gid={gid} /> </> ) } export default Chat


Incroyable, n'oubliez pas de mettre à jour le fichier App.jsx aussi.


Le composant App Remplacez le composant App par le code ci-dessous.


 import { useEffect, useState } from 'react' import { Routes, Route } from 'react-router-dom' import { loadWeb3 } from './Dominion' import { ToastContainer } from 'react-toastify' import { isUserLoggedIn } from './CometChat' import Home from './views/Home' import Proposal from './views/Proposal' import Chat from './views/Chat' import 'react-toastify/dist/ReactToastify.min.css' const App = () => { const [loaded, setLoaded] = useState(false) useEffect(() => { loadWeb3().then((res) => { if (res) setLoaded(true) }) isUserLoggedIn() }, []) return ( <div className="min-h-screen bg-white text-gray-900 dark:bg-[#212936] dark:text-gray-300"> {loaded ? ( <Routes> <Route path="/" element={<Home />} /> <Route path="proposal/:id" element={<Proposal />} /> <Route path="chat/:gid" element={<Chat />} /> </Routes> ) : null} <ToastContainer position="top-center" autoClose={5000} hideProgressBar={false} newestOnTop={false} closeOnClick rtl={false} pauseOnFocusLoss draggable pauseOnHover /> </div> ) } export default App


Sur le répertoire src, >> collez les codes suivants dans leurs fichiers respectifs.


Fichier Index.jsx

 import React from 'react' import ReactDOM from 'react-dom' import { BrowserRouter } from 'react-router-dom' import './index.css' import App from './App' import { initCometChat } from './CometChat' initCometChat().then(() => { ReactDOM.render( <BrowserRouter> <App /> </BrowserRouter>, document.getElementById('root') ) })


Fichier index.css

 @import url('https://fonts.googleapis.com/css2?family=Open+Sans:wght@300;400;500;600;700&display=swap'); * html { padding: 0; margin: 0; box-sizing: border-box; } body { margin: 0; font-family: 'Open Sans', sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } @tailwind base; @tailwind components; @tailwind utilities;


CometChat.jsx

 import Web3 from 'web3' import { setGlobalState, getGlobalState } from './store' import DominionDAO from './abis/DominionDAO.json' const { ethereum } = window const connectWallet = async () => { try { if (!ethereum) return alert('Please install Metamask') const accounts = await ethereum.request({ method: 'eth_requestAccounts' }) setGlobalState('connectedAccount', accounts[0]) } catch (error) { console.log(JSON.stringify(error)) } } const raiseProposal = async ({ title, description, beneficiary, amount }) => { try { amount = window.web3.utils.toWei(amount.toString(), 'ether') const contract = getGlobalState('contract') const account = getGlobalState('connectedAccount') let proposal = await contract.methods .createProposal(title, description, beneficiary, amount) .send({ from: account }) return proposal } catch (error) { console.log(error.message) return error } } const performContribute = async (amount) => { try { amount = window.web3.utils.toWei(amount.toString(), 'ether') const contract = getGlobalState('contract') const account = getGlobalState('connectedAccount') let balance = await contract.methods .contribute() .send({ from: account, value: amount }) balance = window.web3.utils.fromWei( balance.events.Action.returnValues.amount ) return balance } catch (error) { console.log(error.message) return error } } const retrieveProposal = async (id) => { const web3 = window.web3 try { const contract = getGlobalState('contract') const proposal = await contract.methods.getProposal(id).call().wait() return { id: proposal.id, amount: web3.utils.fromWei(proposal.amount), title: proposal.title, description: proposal.description, paid: proposal.paid, passed: proposal.passed, proposer: proposal.proposer, upvotes: Number(proposal.upvotes), downvotes: Number(proposal.downvotes), beneficiary: proposal.beneficiary, executor: proposal.executor, duration: proposal.duration, } } catch (error) { console.log(error) } } const reconstructProposal = (proposal) => { return { id: proposal.id, amount: window.web3.utils.fromWei(proposal.amount), title: proposal.title, description: proposal.description, paid: proposal.paid, passed: proposal.passed, proposer: proposal.proposer, upvotes: Number(proposal.upvotes), downvotes: Number(proposal.downvotes), beneficiary: proposal.beneficiary, executor: proposal.executor, duration: proposal.duration, } } const getProposal = async (id) => { try { const proposals = getGlobalState('proposals') return proposals.find((proposal) => proposal.id == id) } catch (error) { console.log(error) } } const voteOnProposal = async (proposalId, supported) => { try { const contract = getGlobalState('contract') const account = getGlobalState('connectedAccount') const vote = await contract.methods .performVote(proposalId, supported) .send({ from: account }) return vote } catch (error) { console.log(error) return error } } const listVoters = async (id) => { try { const contract = getGlobalState('contract') const votes = await contract.methods.getVotesOf(id).call() return votes } catch (error) { console.log(error) } } const payoutBeneficiary = async (id) => { try { const contract = getGlobalState('contract') const account = getGlobalState('connectedAccount') const balance = await contract.methods .payBeneficiary(id) .send({ from: account }) return balance } catch (error) { return error } } const loadWeb3 = async () => { try { if (!ethereum) return alert('Please install Metamask') window.web3 = new Web3(ethereum) await ethereum.request({ method: 'eth_requestAccounts' }) window.web3 = new Web3(window.web3.currentProvider) const web3 = window.web3 const accounts = await web3.eth.getAccounts() setGlobalState('connectedAccount', accounts[0]) const networkId = await web3.eth.net.getId() const networkData = DominionDAO.networks[networkId] if (networkData) { const contract = new web3.eth.Contract( DominionDAO.abi, networkData.address ) const isStakeholder = await contract.methods .isStakeholder() .call({ from: accounts[0] }) const proposals = await contract.methods.getProposals().call() const balance = await contract.methods.daoBalance().call() const mybalance = await contract.methods .getBalance() .call({ from: accounts[0] }) setGlobalState('contract', contract) setGlobalState('balance', web3.utils.fromWei(balance)) setGlobalState('mybalance', web3.utils.fromWei(mybalance)) setGlobalState('isStakeholder', isStakeholder) setGlobalState('proposals', structuredProposals(proposals)) } else { window.alert('DominionDAO contract not deployed to detected network.') } return true } catch (error) { alert('Please connect your metamask wallet!') console.log(error) return false } } const structuredProposals = (proposals) => { const web3 = window.web3 return proposals .map((proposal) => ({ id: proposal.id, amount: web3.utils.fromWei(proposal.amount), title: proposal.title, description: proposal.description, paid: proposal.paid, passed: proposal.passed, proposer: proposal.proposer, upvotes: Number(proposal.upvotes), downvotes: Number(proposal.downvotes), beneficiary: proposal.beneficiary, executor: proposal.executor, duration: proposal.duration, })) .reverse() } export { loadWeb3, connectWallet, performContribute, raiseProposal, retrieveProposal, voteOnProposal, getProposal, listVoters, payoutBeneficiary, }

Démarrage de l'environnement de développement

ÉTAPE 1 : Créez un compte de test avec ganache-cli à l'aide de la commande ci-dessous :


 ganache-cli -a


Cela créera des comptes de test avec 100 faux éthers chargés dans chaque compte, bien sûr, ceux-ci sont uniquement à des fins de test. Voir l'image ci-dessous :


Clés privées générées

ÉTAPE 2 : Ajoutez un réseau de test local avec Metamask comme indiqué dans l'image ci-dessous.


Réseau d'hôte local

ÉTAPE 3 : Cliquez sur l'icône du compte et sélectionnez importer le compte.


La première étape

Copiez environ cinq des clés privées et ajoutez-les l'une après l'autre à votre réseau de test local. Voir l'image ci-dessous.


Importation de clés privées à partir de ganache cli


Observez le nouveau compte ajouté à votre réseau de test local avec 100 ETH préchargés. Assurez-vous d'ajouter environ cinq comptes afin de pouvoir faire un test maximum. Voir l'image ci-dessous.


Éthers de test gratuits

Déploiement de contrat intelligent

Ouvrez maintenant un nouveau terminal et exécutez la commande ci-dessous.


 truffle migrate # or truffle migrate --network rinkeby


La commande ci-dessus déploiera votre contrat intelligent sur votre réseau de test local ou Infuria rinkeby.


Ensuite, ouvrez un autre terminal et lancez l'application de réaction avec le yarn start .

Conclusion

Hourra, nous venons de terminer un tutoriel incroyable pour développer une organisation autonome décentralisée.


Si vous avez apprécié ce tutoriel et que vous souhaitez m'avoir comme mentor privé, veuillez réserver vos cours avec moi .


Jusqu'à la prochaine fois, tout le meilleur.

A propos de l'auteur

Gospel Darlington est un développeur de blockchain complet avec plus de 6+ ans d'expérience dans l'industrie du développement de logiciels.


En combinant le développement de logiciels, l'écriture et l'enseignement, il montre comment créer des applications décentralisées sur des réseaux blockchain compatibles EVM.


Ses piles incluent JavaScript , React , Vue , Angular , Node , React Native , NextJs , Solidity , etc.


Pour plus d'informations à son sujet, veuillez visiter et suivre sa page sur Twitter , Github , LinkedIn ou sur son site Web .