paint-brush
如何使用 React、Solidity 和 CometChat 构建盈利的 NFT 市场经过@daltonic
13,756 讀數
13,756 讀數

如何使用 React、Solidity 和 CometChat 构建盈利的 NFT 市场

经过 Darlington Gospel 74m2022/07/23
Read on Terminal Reader
Read this story w/o Javascript

太長; 讀書

本教程将教您如何使用聊天功能创建一个盈利且设计良好的 NFT 市场。它将使用 web3 技术创建不可替代的代币 (NFT) 并将资产数字化,同时保留所有权。您将需要以下工具来成功粉碎此构建:Node、Ganache-Cli 和 Truffle。

Companies Mentioned

Mention Thumbnail
Mention Thumbnail

Coin Mentioned

Mention Thumbnail
featured image - 如何使用 React、Solidity 和 CometChat 构建盈利的 NFT 市场
Darlington Gospel  HackerNoon profile picture


您将要构建的内容,请参阅 Rinkeby 测试网络上的演示和此处的git repo ……


介绍

Web3 是互联网的未来,我们都必须学习并接受这项技术。它的用例不断涌现,并对世界产生积极影响。


web3 技术的一个重要应用是创建不可替代的代币 (NFT),这是一种在保留所有权的同时实现资产数字化的可行解决方案。



本教程将教你如何使用聊天功能创建一个盈利且设计良好的 NFT 市场。


如果您需要有人帮助您更快地学习 web3 开发,请与我预订您的私人课程


话虽如此,让我们开始吧......


立即查看我的 Youtube 频道以获取免费的 web3 教程.

先决条件

您将需要安装以下工具才能成功粉碎此构建:

  • 节点
  • Ganache-Cli
  • 松露
  • 反应
  • 英富拉
  • 顺风 CSS
  • CometChat SDK
  • 元掩码

安装依赖

NodeJs 安装检查 NodeJs 是否已经安装在您的机器上,如果没有,请从HERE安装它。在终端上再次运行代码以确保它已安装。


Yarn、Ganache-cli 和 Truffle 安装在终端中运行以下命令以全局安装这些关键软件包。


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


要确认安装,请在终端中输入以下代码。


 yarn --version && ganache-cli --version && truffle version 

克隆 Web3 入门项目 使用以下命令克隆 web 3.0 入门项目。这确保我们都在同一个页面上并使用相同的软件。


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


太好了,请将**package.json**文件替换为以下内容:


 { "name": "TimelessNFT", "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.9", "ipfs-http-client": "^56.0.0", "moment": "^2.29.4", "moment-timezone": "^0.5.34", "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", "web-vitals": "^2.1.4", "web3": "^1.7.1" }, "devDependencies": { "@faker-js/faker": "^6.0.0-alpha.5", "@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", "sharp": "^0.30.1", "stream-browserify": "^3.0.0", "stream-http": "^3.2.0", "tailwindcss": "3.0.18", "url": "^0.11.0" }, "browserslist": { "production": [ ">0.2%", "not dead", "not op_mini all" ], "development": [ "last 1 chrome version", "last 1 firefox version", "last 1 safari version" ] } }


按照指示替换软件包后,在终端上运行**yarn install**以加载具有指定版本的所有软件包。

配置 CometChat SDK

按照以下步骤配置CometChat SDK ;最后,您必须将这些键保存为环境变量。


第 1 步:前往CometChat仪表板并创建一个帐户。


第 2步:仅在注册后登录CometChat仪表板。


第 3 步:在仪表板中,添加一个名为timelessNFT 的新应用程序。


第 4 步:从列表中选择您刚刚创建的应用程序。


第 5 步:从快速入门中将APP_IDREGIONAUTH_KEY复制到您的.env文件中。请参阅图像和代码片段。


REACT_COMET_CHAT占位符键替换为相应的值。

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

配置 Infura 应用程序

第 1 步:前往Infura并创建一个帐户。



第 2 步:从仪表板创建一个新项目。



第 3 步:Rinkeby测试网络 WebSocket 端点 URL 复制到您的.env文件。



之后,输入您的 Metamask 密码短语和首选帐户的私钥。如果您正确地按照说明进行操作,您的环境变量现在应该如下所示。


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


如果您不知道如何访问您的私钥,请参阅下面的部分。

访问您的 Metamask 私钥

第 1 步:确保在您的 Metamask 浏览器扩展中选择 Rinkeby 作为测试网络。然后,在首选帐户上,单击垂直虚线并选择帐户详细信息。请看下图。


第 2 步:在提供的字段中输入您的密码,然后单击确认按钮,这将使您能够访问您的帐户私钥。


第 3 步:单击“导出私钥”以查看您的私钥。确保您永远不会在诸如Github之类的公共页面上公开您的密钥。这就是为什么我们将其附加为环境变量。


第 4 步:将您的私钥复制到您的.env文件中。请参阅下面的图像和代码片段。



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


至于您的SECRET_KEY ,您需要将您的Metamask密码短语粘贴到环境文件中提供的空间中。

永恒的 NFT 智能合约

这是完整的智能合约代码;让我们一一回顾所有的函数和变量。


 // SPDX-License-Identifier: MIT pragma solidity >=0.7.0 <0.9.0; import "./ERC721.sol"; import "./ERC721Enumerable.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; contract TimelessNFT is ERC721Enumerable, Ownable { using Strings for uint256; mapping(string => uint8) existingURIs; mapping(uint256 => address) public holderOf; address public artist; uint256 public royalityFee; uint256 public supply = 0; uint256 public totalTx = 0; uint256 public cost = 0.01 ether; event Sale( uint256 id, address indexed owner, uint256 cost, string metadataURI, uint256 timestamp ); struct TransactionStruct { uint256 id; address owner; uint256 cost; string title; string description; string metadataURI; uint256 timestamp; } TransactionStruct[] transactions; TransactionStruct[] minted; constructor( string memory _name, string memory _symbol, uint256 _royalityFee, address _artist ) ERC721(_name, _symbol) { royalityFee = _royalityFee; artist = _artist; } function payToMint( string memory title, string memory description, string memory metadataURI, uint256 salesPrice ) external payable { require(msg.value >= cost, "Ether too low for minting!"); require(existingURIs[metadataURI] == 0, "This NFT is already minted!"); require(msg.sender != owner(), "Sales not allowed!"); uint256 royality = (msg.value * royalityFee) / 100; payTo(artist, royality); payTo(owner(), (msg.value - royality)); supply++; minted.push( TransactionStruct( supply, msg.sender, salesPrice, title, description, metadataURI, block.timestamp ) ); emit Sale( supply, msg.sender, msg.value, metadataURI, block.timestamp ); _safeMint(msg.sender, supply); existingURIs[metadataURI] = 1; holderOf[supply] = msg.sender; } function payToBuy(uint256 id) external payable { require(msg.value >= minted[id - 1].cost, "Ether too low for purchase!"); require(msg.sender != minted[id - 1].owner, "Operation Not Allowed!"); uint256 royality = (msg.value * royalityFee) / 100; payTo(artist, royality); payTo(minted[id - 1].owner, (msg.value - royality)); totalTx++; transactions.push( TransactionStruct( totalTx, msg.sender, msg.value, minted[id - 1].title, minted[id - 1].description, minted[id - 1].metadataURI, block.timestamp ) ); emit Sale( totalTx, msg.sender, msg.value, minted[id - 1].metadataURI, block.timestamp ); minted[id - 1].owner = msg.sender; } function changePrice(uint256 id, uint256 newPrice) external returns (bool) { require(newPrice > 0 ether, "Ether too low!"); require(msg.sender == minted[id - 1].owner, "Operation Not Allowed!"); minted[id - 1].cost = newPrice; return true; } function payTo(address to, uint256 amount) internal { (bool success, ) = payable(to).call{value: amount}(""); require(success); } function getAllNFTs() external view returns (TransactionStruct[] memory) { return minted; } function getNFT(uint256 id) external view returns (TransactionStruct memory) { return minted[id - 1]; } function getAllTransactions() external view returns (TransactionStruct[] memory) { return transactions; } }


代码导入和合约信息在下面的代码中,我们通知了solidity 编译器许可标识符和编译此代码的编译器版本。


 // SPDX-License-Identifier: MIT pragma solidity >=0.7.0 <0.9.0; import "./ERC721.sol"; import "./ERC721Enumerable.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; contract TimelessNFT is ERC721Enumerable, Ownable { // codes goes in here... }


此外,该智能合约使用了一些openzepplin's ERC721 智能合约。您必须确保将它们放在下图所示的同一目录中。


访问此链接并下载这些智能合约,如上图所示。


状态变量声明

using Strings for uint256; mapping(string => uint8) existingURIs; mapping(uint256 => address) public holderOf; address public artist; uint256 public royalityFee; uint256 public supply = 0; uint256 public totalTx = 0; uint256 public cost = 0.01 ether;


我们指定我们使用字符串库来执行uintstring的操作。接下来,我们还声明了用于记录铸造 NFT 艺术品以及了解代币当前所有者的映射。


然后,我们指定了其他基本变量来获取艺术家账户、版税、当前供应量、平台上的总交易量以及 NFT 的铸币成本。


设置事件和结构

event Sale( uint256 id, address indexed owner, uint256 cost, string metadataURI, uint256 timestamp ); struct TransactionStruct { uint256 id; address owner; uint256 cost; string title; string description; string metadataURI; uint256 timestamp; } TransactionStruct[] transactions; TransactionStruct[] minted;


在前面的代码中,我们有一个销售事件,它从智能合约上发生的任何交易中发出数据,无论是在铸币还是 NFT 转账。


我们设计了一个交易结构来收集有关铸造或转让 NFT 的数据。使用我们定义的交易结构,我们创建了两个变量,称为transactionsminted


初始化构造函数

constructor( string memory _name, string memory _symbol, uint256 _royalityFee, address _artist ) ERC721(_name, _symbol) { royalityFee = _royalityFee; artist = _artist; }


构造函数接受四个参数来初始化智能合约。代币名称、符号、艺术家帐户和每笔交易的版税。然后在部署期间将代币名称和符号传递到ERC721智能合约中。


mint函数算法

function payToMint( string memory title, string memory description, string memory metadataURI, uint256 salesPrice ) external payable { require(msg.value >= cost, "Ether too low for minting!"); require(existingURIs[metadataURI] == 0, "This NFT is already minted!"); require(msg.sender != owner(), "Sales not allowed!"); uint256 royality = (msg.value * royalityFee) / 100; payTo(artist, royality); payTo(owner(), (msg.value - royality)); supply++; minted.push( TransactionStruct( supply, msg.sender, salesPrice, title, description, metadataURI, block.timestamp ) ); emit Sale( supply, msg.sender, msg.value, metadataURI, block.timestamp ); _safeMint(msg.sender, supply); existingURIs[metadataURI] = 1; holderOf[supply] = msg.sender; }


上述函数负责在智能合约上铸造新的代币。此方法的调用者必须提供四个参数,其中包括: NFT 标题、描述、元数据 URI 和 NFT 在铸造后的售价。


进行验证以确保 NFT 正在铸造,并相应地完成每次铸造的付款。此外,验证确保每件艺术品都与一个代币唯一链接,并且没有其他代币带有相同的艺术品。最后为了验证,我们确保这个方法的调用者不是智能合约的部署者,这是为了确保我们不会把事情搞混。


函数中的下一个是支付共享规则。版税百分比归艺术家所有,其余的以太币归所有者所有。


之后,我们在 minted 数组中记录了 NFT 并发出了一个销售事件。最后,我们在记录调用者地址作为代币所有者的同时铸造了 NFT。


NFT传递函数算法

function payToBuy(uint256 id) external payable { require(msg.value >= minted[id - 1].cost, "Ether too low for purchase!"); require(msg.sender != minted[id - 1].owner, "Operation Not Allowed!"); uint256 royality = (msg.value * royalityFee) / 100; payTo(artist, royality); payTo(minted[id - 1].owner, (msg.value - royality)); totalTx++; transactions.push( TransactionStruct( totalTx, msg.sender, msg.value, minted[id - 1].title, minted[id - 1].description, minted[id - 1].metadataURI, block.timestamp ) ); emit Sale( totalTx, msg.sender, msg.value, minted[id - 1].metadataURI, block.timestamp ); minted[id - 1].owner = msg.sender; }


上述函数采用 NFT id 并根据铸币者(所有者)设定的价格购买 NFT。


进行了必要的验证以阻止所有者购买他们的 NFT 和其他人以零以太币购买。


接下来,将向艺术家账户发送版税,NFT 的当前所有者可以保留其余部分。


每个代币转移都记录在一个交易数组中,以跟踪平台上完成的所有交易。


之后,再次为此次购买发出一个销售事件,以丰富 EVM 上的记录数据。


其他基本功能

// changes the price of an NFT function changePrice(uint256 id, uint256 newPrice) external returns (bool) { require(newPrice > 0 ether, "Ether too low!"); require(msg.sender == minted[id - 1].owner, "Operation Not Allowed!"); minted[id - 1].cost = newPrice; return true; } // sends ethers to a specific account function payTo(address to, uint256 amount) internal { (bool success, ) = payable(to).call{value: amount}(""); require(success); } // returns all minted NFTs function getAllNFTs() external view returns (TransactionStruct[] memory) { return minted; } // returns a specific NFT by token id function getNFT(uint256 id) external view returns (TransactionStruct memory) { return minted[id - 1]; } // returns all transactions function getAllTransactions() external view returns (TransactionStruct[] memory) { return transactions; }


有了它来开发智能合约,我们接下来将深入研究使用 ReactJs 构建 UI 组件。

配置部署脚本

与智能合约有关的另一件事是配置部署脚本。

在项目头上的迁移文件夹 >> 2_deploy_contracts.js并使用下面的代码片段对其进行更新。


 const TimelessNFT = artifacts.require('TimelessNFT') module.exports = async (deployer) => { const accounts = await web3.eth.getAccounts() await deployer.deploy( TimelessNFT, 'Timeless NFTs', 'TNT', 10, accounts[1] ) }


太棒了,我们刚刚完成了应用程序的智能合约;现在是时候开始 DApp 界面了。如果您需要私人导师帮助您学习智能合约开发,请与我预约课程

开发前端

前端由许多组件和零件组成。所有组件、视图和外围设备都将由我们创建。


标题组件


该组件是使用 tailwind CSS 创建的,并使用粉红色的Connect Wallet按钮来访问 Metamask 钱包。下面的代码演示了编程。


 import { useGlobalState } from '../store' import timelessLogo from '../assets/timeless.png' import { connectWallet } from '../TimelessNFT' const Header = () => { const [connectedAccount] = useGlobalState('connectedAccount') return ( <nav className="w-4/5 flex md:justify-center justify-between items-center py-4 mx-auto"> <div className="md:flex-[0.5] flex-initial justify-center items-center"> <img className="w-32 cursor-pointer" src={timelessLogo} alt="Timeless Logo" /> </div> <ul className="md:flex-[0.5] text-white md:flex hidden list-none flex-row justify-between items-center flex-initial" > <li className="mx-4 cursor-pointer">Market</li> <li className="mx-4 cursor-pointer">Artist</li> <li className="mx-4 cursor-pointer">Features</li> <li className="mx-4 cursor-pointer">Community</li> </ul> {!connectedAccount ? ( <button className="shadow-xl shadow-black text-white bg-[#e32970] hover:bg-[#bd255f] md:text-xs p-2 rounded-full cursor-pointer" onClick={connectWallet} > Connect Wallet </button> ) : ( <></> )} </nav> ) } export default Header


英雄组件


该组件负责显示连接的钱包,还负责启动用于创建新 NFT 的模式。此外,它还负责登录或注册用户,以便与 NFT 卖家进行一对一聊天。这是负责这些操作的代码。


 import Identicon from 'react-identicons' import { setGlobalState, useGlobalState, truncate } from '../store' import { getConversations, loginWithCometChat, signUpWithCometChat } from '../CometChat' import ChatList from './ChatList' import { useState } from 'react' const Hero = () => { const [connectedAccount] = useGlobalState('connectedAccount') const [currentUser] = useGlobalState('currentUser') const [recentOpened] = useGlobalState('recentOpened') const [conversations, setConversations] = useState([]) const onCreatedNFT = () => { if (currentUser?.uid.toLowerCase() != connectedAccount.toLowerCase()) return alert('Please login to receive chats from buyers!') setGlobalState('modal', 'scale-100') } const onLunchRecent = () => { getConversations().then((convs) => { setConversations(convs) setGlobalState('recentOpened', true) }) } return ( <div className="flex flex-col md:flex-row w-4/5 justify-between items-center mx-auto py-10" > <div className="md:w-3/6 w-full"> <div> <h1 className="text-white text-5xl font-bold"> Buy and Sell <br /> Digital Arts, <br /> <span className="text-gradient">NFTs</span> Collections </h1> <p className="text-gray-500 font-semibold text-sm mt-3"> Mint and collect the hottest NFTs around. </p> </div> <div className="flex flex-row mt-5"> {connectedAccount ? ( <> <button className="shadow-xl shadow-black text-white bg-[#e32970] hover:bg-[#bd255f] rounded-full cursor-pointer p-2" onClick={onCreatedNFT} > Create NFT </button> <> {currentUser?.uid.toLowerCase() == connectedAccount.toLowerCase() ? ( <button className="text-white border border-gray-500 hover:border-[#e32970] hover:bg-[#bd255f] cursor-pointer rounded-full p-2 mx-3" onClick={onLunchRecent} > Recent Chats </button> ) : ( <> <button className="text-white border border-gray-500 hover:border-[#e32970] hover:bg-[#bd255f] cursor-pointer rounded-full p-2 mx-3" onClick={() => loginWithCometChat(connectedAccount)} > Login for Chat </button> <button className="text-white border border-gray-500 hover:border-[#e32970] hover:bg-[#bd255f] cursor-pointer rounded-full p-2 mx-3" onClick={() => signUpWithCometChat(connectedAccount, connectedAccount)} > Signup for Chat </button> </> )} </> </> ) : null} </div> <div className="w-3/4 flex justify-between items-center mt-5"> <div> <p className="text-white font-bold">1231k</p> <small className="text-gray-300">User</small> </div> <div> <p className="text-white font-bold">152k</p> <small className="text-gray-300">Artwork</small> </div> <div> <p className="text-white font-bold">200k</p> <small className="text-gray-300">Artist</small> </div> </div> </div> <div className="shadow-xl shadow-black md:w-2/5 w-full mt-10 md:mt-0 rounded-md overflow-hidden bg-gray-800" > <img src="https://images.cointelegraph.com/images/1434_aHR0cHM6Ly9zMy5jb2ludGVsZWdyYXBoLmNvbS91cGxvYWRzLzIwMjEtMDYvNGE4NmNmOWQtODM2Mi00YmVhLThiMzctZDEyODAxNjUxZTE1LmpwZWc=.jpg" alt="NFT Art" className="h-60 w-full object-cover" /> <div className="flex justify-start items-center p-3"> <Identicon string={ connectedAccount ? connectedAccount.toLowerCase() : 'Connect Your Wallet' } size={50} className="h-10 w-10 object-contain rounded-full mr-3" /> <div> <p className="text-white font-semibold"> {connectedAccount ? truncate(connectedAccount, 4, 4, 11) : 'Connect Your Wallet'} </p> <small className="text-pink-800 font-bold">@you</small> </div> </div> </div> {recentOpened ? <ChatList users={conversations} /> : null} </div> ) } export default Hero


艺术品组件

该组件负责使用精心制作的顺风 CSS 卡渲染平台上铸造的 NFT 列表。每张卡片都有一个 NFT 图像、标题、描述、价格和所有者。有关其实现,请参见下面的代码。


 import { useEffect, useState } from 'react' import { setGlobalState, useGlobalState, truncate } from '../store' const Artworks = () => { const [nfts] = useGlobalState('nfts') const [end, setEnd] = useState(4) const [count] = useState(4) const [collection, setCollection] = useState([]) const setNFT = (nft) => { setGlobalState('nft', nft) setGlobalState('showModal', 'scale-100') } const getCollection = () => { return nfts.slice(0, end) } useEffect(() => { setCollection(getCollection()) }, [nfts, end]) return ( <div className="bg-[#151c25] gradient-bg-artworks"> <div className="w-4/5 py-10 mx-auto"> <h4 className="text-white text-3xl font-bold uppercase text-gradient"> {collection.length > 0 ? 'Latest Artworks' : 'No Artworks Yet'} </h4> <div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-6 md:gap-4 lg:gap-3 py-2.5"> {collection.map((nft) => ( <div key={nft.id} className="w-full shadow-xl shadow-black rounded-md overflow-hidden bg-gray-800 my-2 p-3" > <img src={nft.metadataURI} alt={truncate(nft.title, 6)} className="h-60 w-full object-cover shadow-lg shadow-black rounded-lg mb-3" /> <h4 className="text-white font-semibold">{nft.title}</h4> <p className="text-gray-400 text-xs my-1"> {truncate(nft.description)} </p> <div className="flex justify-between items-center mt-3 text-white"> <div className="flex flex-col"> <small className="text-xs">Current Price</small> <p className="text-sm font-semibold">{nft.cost} ETH</p> </div> <button onClick={() => setNFT(nft)} className="shadow-lg shadow-black text-white text-sm bg-[#e32970] hover:bg-[#bd255f] cursor-pointer rounded-full px-1.5 py-1" > View Details </button> </div> </div> ))} </div> {collection.length > 0 && nfts.length > collection.length ? ( <div className="text-center my-5"> <button className="shadow-xl shadow-black text-white bg-[#e32970] hover:bg-[#bd255f] rounded-full cursor-pointer p-2" onClick={() => setEnd(end + count)} > Load More </button> </div> ) : null} </div> </div> ) } export default Artworks


交易组件

该组件负责渲染我们智能合约中发生的所有交易。例如,一项交易是艾莉森从杜克购买 NFT。此购买将作为交易记录在此组件中。请参阅下面的片段。


 import { useEffect, useState } from 'react' import { BiTransfer } from 'react-icons/bi' import { MdOpenInNew } from 'react-icons/md' import { useGlobalState, truncate } from '../store' const Transactions = () => { const [transactions] = useGlobalState('transactions') const [end, setEnd] = useState(3) const [count] = useState(3) const [collection, setCollection] = useState([]) const getCollection = () => { return transactions.slice(0, end) } useEffect(() => { setCollection(getCollection()) }, [transactions, end]) return ( <div className="bg-[#151c25]"> <div className="w-4/5 py-10 mx-auto"> <h4 className="text-white text-3xl font-bold uppercase text-gradient"> {collection.length > 0 ? 'Latest Transactions' : 'No Transaction Yet'} </h4> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 md:gap-4 lg:gap-2 py-2.5"> {collection .map((tx) => ( <div key={tx.id} className="flex justify-between items-center border border-pink-500 text-gray-400 w-full shadow-xl shadow-black rounded-md overflow-hidden bg-gray-800 my-2 p-3" > <div className="rounded-md shadow-sm shadow-pink-500 p-2"> <BiTransfer /> </div> <div> <h4 className="text-sm">{tx.title} Transfered</h4> <small className="flex flex-row justify-start items-center"> <span className="mr-1">Received by</span> <a href="#" className="text-pink-500 mr-2"> {truncate(tx.owner, 4, 4, 11)} </a> <a href="#"> <MdOpenInNew /> </a> </small> </div> <p className="text-sm font-medium">{tx.cost}ETH</p> </div> ))} </div> {collection.length > 0 && transactions.length > collection.length ? ( <div className="text-center my-5"> <button className="shadow-xl shadow-black text-white bg-[#e32970] hover:bg-[#bd255f] rounded-full cursor-pointer p-2" onClick={() => setEnd(end + count)} > Load More </button> </div> ) : null} </div> </div> ) } export default Transactions


页脚组件


这个组件只是在页面底部显示一些漂亮的链接,它在功能方面并没有做太多,但补充了用户界面。它的代码写在下面。


 import timelessLogo from '../assets/timeless.png' const Footer = () => ( <div className="w-full flex md:justify-center justify-between items-center flex-col p-4 gradient-bg-footer"> <div className="w-full flex sm:flex-row flex-col justify-between items-center my-4"> <div className="flex flex-[0.25] justify-center items-center"> <img src={timelessLogo} alt="logo" className="w-32" /> </div> <div className="flex flex-1 justify-evenly items-center flex-wrap sm:mt-0 mt-5 w-full"> <p className="text-white text-base text-center mx-2 cursor-pointer"> Market </p> <p className="text-white text-base text-center mx-2 cursor-pointer"> Artist </p> <p className="text-white text-base text-center mx-2 cursor-pointer"> Features </p> <p className="text-white text-base text-center mx-2 cursor-pointer"> Community </p> </div> <div className="flex flex-[0.25] justify-center items-center"> <p className="text-white text-right text-xs">&copy;2022 All rights reserved</p> </div> </div> </div> ) export default Footer


太棒了,这就是明显的组件,让我们包括仅通过模态界面调用的隐藏组件。


创建 NFT 组件


该组件肩负着通过提供图像、标题、价格和描述来铸造新 NFT 的责任。单击Mint Now按钮后,图像将上传到 IPFS(星际文件系统)并返回图像 URL。


在用户使用 Metamask 钱包授权交易后,返回的图像 URL 以及表单中提供的 NFT 数据将立即发送到我们的智能合约进行铸造。


交易完成后,NFT 就会被列在艺术品中,感兴趣的买家可以购买它们,甚至可以更改它们的价格。有关详细信息,请参阅下面的代码。


 import { useGlobalState, setGlobalState, setLoadingMsg, setAlert, } from '../store' import { mintNFT } from '../TimelessNFT' import { useState } from 'react' import { FaTimes } from 'react-icons/fa' import { create } from 'ipfs-http-client' const client = create('https://ipfs.infura.io:5001/api/v0') const CreateNFT = () => { const [modal] = useGlobalState('modal') const [title, setTitle] = useState('') const [price, setPrice] = useState('') const [description, setDescription] = useState('') const [fileUrl, setFileUrl] = useState('') const [imgBase64, setImgBase64] = useState(null) const onChange = async (e) => { const reader = new FileReader() if (e.target.files[0]) reader.readAsDataURL(e.target.files[0]) reader.onload = (readerEvent) => { const file = readerEvent.target.result setImgBase64(file) setFileUrl(e.target.files[0]) } } const handleSubmit = async (e) => { e.preventDefault() if (!title || !price || !description) return setGlobalState('modal', 'scale-0') setGlobalState('loading', { show: true, msg: 'Uploading IPFS data...' }) try { const created = await client.add(fileUrl) const metadataURI = `https://ipfs.infura.io/ipfs/${created.path}` const nft = { title, price, description, metadataURI } setLoadingMsg('Intializing transaction...') mintNFT(nft).then((res) => { if (res) { setFileUrl(metadataURI) resetForm() setAlert('Minting completed...', 'green') window.location.reload() } }) } catch (error) { console.log('Error uploading file: ', error) setAlert('Minting failed...', 'red') } } const closeModal = () => { setGlobalState('modal', 'scale-0') resetForm() } const resetForm = () => { setFileUrl('') setImgBase64(null) setTitle('') setPrice('') setDescription('') } return ( <div className={`fixed top-0 left-0 w-screen h-screen flex items-center justify-center bg-black bg-opacity-50 transform transition-transform duration-300 ${modal}`} > <div className="bg-[#151c25] shadow-xl shadow-[#e32970] rounded-xl w-11/12 md:w-2/5 h-7/12 p-6"> <form className="flex flex-col"> <div className="flex flex-row justify-between items-center"> <p className="font-semibold text-gray-400">Add NFT</p> <button type="button" onClick={closeModal} className="border-0 bg-transparent focus:outline-none" > <FaTimes className="text-gray-400" /> </button> </div> <div className="flex flex-row justify-center items-center rounded-xl mt-5"> <div className="shrink-0 rounded-xl overflow-hidden h-20 w-20"> <img alt="NFT" className="h-full w-full object-cover cursor-pointer" src={ imgBase64 || 'https://images.unsplash.com/photo-1580489944761-15a19d654956?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1361&q=80' } /> </div> </div> <div className="flex flex-row justify-between items-center bg-gray-800 rounded-xl mt-5"> <label className="block"> <span className="sr-only">Choose profile photo</span> <input type="file" accept="image/png, image/gif, image/jpeg, image/webp" className="block w-full text-sm text-slate-500 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-[#19212c] file:text-gray-400 hover:file:bg-[#1d2631] cursor-pointer focus:ring-0 focus:outline-none" onChange={onChange} required /> </label> </div> <div className="flex flex-row justify-between items-center bg-gray-800 rounded-xl mt-5"> <input className="block w-full text-sm text-slate-500 bg-transparent border-0 focus:outline-none focus:ring-0" type="text" name="title" placeholder="Title" onChange={(e) => setTitle(e.target.value)} value={title} required /> </div> <div className="flex flex-row justify-between items-center bg-gray-800 rounded-xl mt-5"> <input className="block w-full text-sm text-slate-500 bg-transparent border-0 focus:outline-none focus:ring-0" type="number" step={0.01} min={0.01} name="price" placeholder="Price (Eth)" onChange={(e) => setPrice(e.target.value)} value={price} required /> </div> <div className="flex flex-row justify-between items-center bg-gray-800 rounded-xl mt-5"> <textarea className="block w-full text-sm resize-none text-slate-500 bg-transparent border-0 focus:outline-none focus:ring-0 h-20" type="text" name="description" placeholder="Description" onChange={(e) => setDescription(e.target.value)} value={description} required ></textarea> </div> <button type="submit" onClick={handleSubmit} className="flex flex-row justify-center items-center w-full text-white text-md bg-[#e32970] hover:bg-[#bd255f] py-2 px-5 rounded-full drop-shadow-xl border border-transparent hover:bg-transparent hover:text-[#e32970] hover:border hover:border-[#bd255f] focus:outline-none focus:ring mt-5" > Mint Now </button> </form> </div> </div> ) } export default CreateNFT


ShowNFT 组件

该组件显示有关特定 NFT 的更多信息,为所有者提供一个更改价格的按钮,并为买家提供一个购买 NFT 或与卖家聊天的按钮。有关详细信息,请参阅下面的代码。


 import Chat from './Chat' import Identicon from 'react-identicons' import { FaTimes } from 'react-icons/fa' import { buyNFT } from '../TimelessNFT' import { useGlobalState, setGlobalState, truncate, setAlert } from '../store' import { useState } from 'react' import { getMessages } from '../CometChat' const ShowNFT = () => { const [showModal] = useGlobalState('showModal') const [chatOpened] = useGlobalState('chatOpened') const [currentUser] = useGlobalState('currentUser') const [connectedAccount] = useGlobalState('connectedAccount') const [nft] = useGlobalState('nft') const [messages, setMessages] = useState([]) const onChangePrice = () => { setGlobalState('nft', nft) setGlobalState('showModal', 'scale-0') setGlobalState('updateModal', 'scale-100') } const onChatSeller = () => { if (currentUser?.uid.toLowerCase() != connectedAccount.toLowerCase()) return alert('Please login to receive chats from buyers!') getMessages(nft.owner).then((msgs) => { setMessages( msgs.filter((msg) => { return ( !!!msg?.deletedAt && !!!msg?.action && (msg?.conversationId == `${msg?.rawMessage.receiver}_user_${msg?.rawMessage.sender}` || msg?.conversationId == `${msg?.rawMessage.sender}_user_${msg?.rawMessage.receiver}`) ) }) ) setGlobalState('nft', nft) setGlobalState('chatOpened', true) }) } const handleNFTPurchase = () => { setGlobalState('showModal', 'scale-0') setGlobalState('loading', { show: true, msg: 'Initializing NFT transfer...', }) try { buyNFT(nft).then((res) => { if (res) { setAlert('Transfer completed...', 'green') window.location.reload() } }) } catch (error) { console.log('Error transfering NFT: ', error) setAlert('Purchase failed...', 'red') } } return ( <div> {chatOpened ? ( <Chat receiver={nft.owner} chats={messages} /> ) : ( <div className={`fixed top-0 left-0 w-screen h-screen flex items-center justify-center bg-black bg-opacity-50 transform transition-transform duration-300 ${showModal}`} > <div className="bg-[#151c25] shadow-xl shadow-[#e32970] 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 text-gray-400">Buy NFT</p> <button type="button" onClick={() => setGlobalState('showModal', 'scale-0')} className="border-0 bg-transparent focus:outline-none" > <FaTimes className="text-gray-400" /> </button> </div> <div className="flex flex-row justify-center items-center rounded-xl mt-5"> <div className="shrink-0 rounded-xl overflow-hidden h-40 w-40"> <img className="h-full w-full object-cover cursor-pointer" src={nft?.metadataURI} alt={nft?.title} /> </div> </div> <div className="flex flex-col justify-start rounded-xl mt-5"> <h4 className="text-white font-semibold">{nft?.title}</h4> <p className="text-gray-400 text-xs my-1">{nft?.description}</p> <div className="flex justify-between items-center mt-3 text-white"> <div className="flex justify-start items-center"> <Identicon string={nft?.owner.toLowerCase()} size={50} className="h-10 w-10 object-contain rounded-full mr-3" /> <div className="flex flex-col justify-center items-start"> <small className="text-white font-bold">@owner</small> <small className="text-pink-800 font-semibold"> {nft?.owner ? truncate(nft.owner, 4, 4, 11) : '...'} </small> </div> </div> <div className="flex flex-col"> <small className="text-xs">Current Price</small> <p className="text-sm font-semibold">{nft?.cost} ETH</p> </div> </div> </div> {connectedAccount != nft?.owner ? ( <div className="flex flex-row justify-between items-center"> <button className="flex flex-row justify-center items-center w-full text-white text-md bg-[#e32970] hover:bg-[#bd255f] py-2 px-5 rounded-full drop-shadow-xl border border-transparent hover:bg-transparent hover:text-[#e32970] hover:border hover:border-[#bd255f] focus:outline-none focus:ring mt-5" onClick={handleNFTPurchase} > Purchase Now </button> <button className="flex flex-row justify-center items-center w-full text-white text-md bg-transparent py-2 px-5 rounded-full drop-shadow-xl border border-transparent hover:bg-transparent hover:text-[#e32970] focus:outline-none focus:ring mt-5" onClick={onChatSeller} > Chat with Seller </button> </div> ) : ( <button className="flex flex-row justify-center items-center w-full text-white text-md bg-[#e32970] hover:bg-[#bd255f] py-2 px-5 rounded-full drop-shadow-xl border border-transparent hover:bg-transparent hover:text-[#e32970] hover:border hover:border-[#bd255f] focus:outline-none focus:ring mt-5" onClick={onChangePrice} > Change Price </button> )} </div> </div> </div> )} </div> ) } export default ShowNFT


更新 NFT 组件

该组件的任务是改变 NFT 的价格。此操作只能由 NFT 的所有者执行。虽然此选项可用,但需要一些汽油费才能实现这些更改。一旦 NFT 与另一个买家交易,新所有者可能会决定提高价格,这就是提供此选项的原因。请参阅下面的代码片段。


 import { useGlobalState, setGlobalState, setLoadingMsg, setAlert, } from '../store' import { updateNFT } from '../TimelessNFT' import { useState } from 'react' import { FaTimes } from 'react-icons/fa' const UpdateNFT = () => { const [modal] = useGlobalState('updateModal') const [nft] = useGlobalState('nft') const [price, setPrice] = useState('') const handleSubmit = async (e) => { e.preventDefault() if (!price || price <= 0) return setGlobalState('modal', 'scale-0') setGlobalState('loading', { show: true, msg: 'Initiating price update...' }) try { setLoadingMsg('Price updating...') setGlobalState('updateModal', 'scale-0') updateNFT({...nft, cost: price}).then((res) => { if (res) { setAlert('Price updated...', 'green') window.location.reload() } }) } catch (error) { console.log('Error updating file: ', error) setAlert('Update failed...', 'red') } } return ( <div className={`fixed top-0 left-0 w-screen h-screen flex items-center justify-center bg-black bg-opacity-50 transform transition-transform duration-300 ${modal}`} > <div className="bg-[#151c25] shadow-xl shadow-[#e32970] rounded-xl w-11/12 md:w-2/5 h-7/12 p-6"> <form className="flex flex-col"> <div className="flex flex-row justify-between items-center"> <p className="font-semibold text-gray-400">{nft?.title}</p> <button type="button" onClick={() => setGlobalState('updateModal', 'scale-0')} className="border-0 bg-transparent focus:outline-none" > <FaTimes className="text-gray-400" /> </button> </div> <div className="flex flex-row justify-center items-center rounded-xl mt-5"> <div className="shrink-0 rounded-xl overflow-hidden h-20 w-20"> <img alt="NFT" className="h-full w-full object-cover cursor-pointer" src={nft?.metadataURI} /> </div> </div> <div className="flex flex-row justify-between items-center bg-gray-800 rounded-xl mt-5"> <input className="block w-full text-sm text-slate-500 bg-transparent border-0 focus:outline-none focus:ring-0" type="number" step={0.01} min={0.01} name="price" placeholder="Price (Eth)" onChange={(e) => setPrice(e.target.value)} required /> </div> <button type="submit" onClick={handleSubmit} className="flex flex-row justify-center items-center w-full text-white text-md bg-[#e32970] hover:bg-[#bd255f] py-2 px-5 rounded-full drop-shadow-xl border border-transparent hover:bg-transparent hover:text-[#e32970] hover:border hover:border-[#bd255f] focus:outline-none focus:ring mt-5" > Update Now </button> </form> </div> </div> ) } export default UpdateNFT


聊天列表组件

该组件显示用户最近在平台上与卖家或买家进行的聊天。该组件还捕获在他们的对话中发送的最后一条消息。单击每个对话将导致聊天界面。请参阅下面的代码。


 import Chat from './Chat' import Moment from 'react-moment' import { useState } from 'react' import { FaTimes } from 'react-icons/fa' import { setGlobalState, truncate, useGlobalState } from '../store' import { getMessages } from '../CometChat' const ChatList = ({ users }) => { const [messages, setMessages] = useState([]) const [receiver, setReceiver] = useState('') const [recentChatOpened] = useGlobalState('recentChatOpened') const [connectedAccount] = useGlobalState('connectedAccount') const onEnterChat = (receiver) => { setReceiver(receiver) getMessages(receiver).then((msgs) => { setMessages( msgs.filter((msg) => { return ( !!!msg?.deletedAt && !!!msg?.action && msg?.conversationId == `${msg?.rawMessage.receiver}_user_${msg?.rawMessage.sender}` ) }) ) setGlobalState('recentChatOpened', true) }) } return ( <div> {recentChatOpened ? ( <Chat receiver={receiver} chats={messages} /> ) : ( <div className={`fixed top-0 left-0 w-screen h-screen flex items-center justify-center bg-black bg-opacity-50 transform transition-transform duration-300 scale-100`} > <div className="bg-[#151c25] shadow-xl shadow-[#e32970] rounded-xl w-11/12 md:w-2/5 h-7/12 p-6"> <div className="flex flex-col text-gray-400"> <div className="flex flex-row justify-between items-center"> <p className="font-semibold text-gray-400">Conversations</p> <button type="button" onClick={() => setGlobalState('recentOpened', false)} className="border-0 bg-transparent focus:outline-none" > <FaTimes /> </button> </div> <div className="h-[calc(100vh_-_20rem)] overflow-y-auto sm:pr-4 my-3"> {users.map((user, i) => ( <button key={i} className="flex flex-row justify-between w-full items-center bg-gray-800 hover:bg-gray-900 rounded-md px-4 py-3 my-1 cursor-pointer transform transition-transform duration-300" onClick={() => onEnterChat(user?.lastMessage.sender.uid)} > <div className="flex flex-col text-left"> <h4 className="text-sm text-[#e32970] font-semiBold"> @{truncate(user?.lastMessage.sender.uid, 4, 4, 11)} </h4> <p className="text-xs"> {user?.lastMessage.text} </p> </div> <Moment className="text-xs font-bold" unix date={user?.lastMessage.sentAt} format="YYYY/MM/D hh:mm A" /> </button> ))} </div> </div> </div> </div> )} </div> ) } export default ChatList


聊天组件

该组件负责让两个用户进行一对一的聊天。上图显示了平台上买家和卖家之间的对话,来自两个不同的浏览器。有关其实现,请参见下面的代码。


 import Identicon from 'react-identicons' import { useGlobalState, setGlobalState, truncate } from '../store' import { sendMessage, CometChat } from '../CometChat' import { useEffect, useState } from 'react' import { FaTimes } from 'react-icons/fa' const Chat = ({ receiver, chats }) => { const [connectedAccount] = useGlobalState('connectedAccount') const [message, setMessage] = useState('') const [messages, setMessages] = useState(chats) const handleSubmit = async (e) => { e.preventDefault() sendMessage(receiver, message).then((msg) => { setMessages((prevState) => [...prevState, msg]) setMessage('') scrollToEnd() }) } const listenForMessage = (listenerID) => { CometChat.addMessageListener( listenerID, new CometChat.MessageListener({ onTextMessageReceived: (message) => { setMessages((prevState) => [...prevState, message]) scrollToEnd() }, }) ) } const onClose = () => { setGlobalState('chatOpened', false) setGlobalState('recentChatOpened', false) } const scrollToEnd = () => { const element = document.getElementById('messages-container') element.scrollTop = element.scrollHeight } useEffect(() => { listenForMessage(receiver) }, [receiver]) return ( <div className={`fixed top-0 left-0 w-screen h-screen flex items-center justify-center bg-black bg-opacity-50 transform transition-transform duration-300 scale-100`} > <div className="bg-[#151c25] shadow-xl shadow-[#e32970] rounded-xl w-5/6 h-5/6 p-6"> <div className="flex flex-col text-gray-400"> <div className="flex flex-row justify-between items-center"> <div className="flex flex-row justify-center items-center"> <div className="shrink-0 rounded-full overflow-hidden h-10 w-10 mr-3"> <Identicon string={receiver.toLowerCase()} size={50} className="h-full w-full object-cover cursor-pointer rounded-full" /> </div> <p className="font-bold">@{receiver ? truncate(receiver, 4, 4, 11) : '...'}</p> </div> <button type="button" onClick={onClose} className="border-0 bg-transparent focus:outline-none" > <FaTimes /> </button> </div> <div id="messages-container" className="h-[calc(100vh_-_20rem)] overflow-y-auto sm:pr-4 my-3" > {messages.map((msg, i) => msg?.receiverId?.toLowerCase() == connectedAccount.toLowerCase() ? ( <div key={i} className="flex flex-row justify-start items-center mt-5" > <div className="flex flex-col justify-start items-start"> <h4 className="text-[#e32970]"> @{receiver ? truncate(receiver, 4, 4, 11) : '...'} </h4> <p className="text-xs">{msg.text}</p> </div> </div> ) : ( <div key={i} className="flex flex-row justify-end items-center mt-5" > <div className="flex flex-col justify-start items-end"> <h4 className="text-[#e32970]">@you</h4> <p className="text-xs">{msg.text}</p> </div> </div> ) )} </div> <form onSubmit={handleSubmit} className="flex flex-row justify-between items-center bg-gray-800 rounded-xl mt-5" > <input className="block w-full text-sm resize-none text-slate-500 bg-transparent border-0 focus:outline-none focus:ring-0 h-20" type="text" name="message" placeholder="Write message..." onChange={(e) => setMessage(e.target.value)} value={message} required /> </form> </div> </div> </div> ) } export default Chat


很好,既然我们已经包含了那些很棒的组件,让我们用最后两个来完成它。


加载组件

该组件仅在事务处理过程中显示当前活动和状态。请参阅下面的代码。


 import { useGlobalState } from '../store' const Loading = () => { const [loading] = useGlobalState('loading') return ( <div className={`fixed top-0 left-0 w-screen h-screen flex items-center justify-center bg-black bg-opacity-50 transform transition-transform duration-300 ${loading.show ? 'scale-100' : 'scale-0'}`} > <div className="flex flex-col justify-center items-center bg-[#151c25] shadow-xl shadow-[#e32970] rounded-xl min-w-min px-10 pb-2" > <div className="flex flex-row justify-center items-center"> <div className="lds-dual-ring scale-50"></div> <p className="text-lg text-white">Processing...</p> </div> <small className="text-white">{loading.msg}</small> </div> </div> ) } export default Loading


应用程序组件该文件捆绑了本工作中讨论的上述组件。这就是 ReactJs 架构的工作原理。请参阅下面的代码。


 import Alert from './components/Alert' import Artworks from './components/Artworks' import CreateNFT from './components/CreateNFT' import Footer from './components/Footer' import Header from './components/Header' import Hero from './components/Hero' import Loading from './components/Loading' import ShowNFT from './components/ShowNFT' import Transactions from './components/Transactions' import UpdateNFT from './components/UpdateNFT' import { isUserLoggedIn } from './CometChat' import { loadWeb3 } from './TimelessNFT' import { useEffect } from 'react' const App = () => { useEffect(() => { loadWeb3() isUserLoggedIn() }, []) return ( <div className="min-h-screen"> <div className="gradient-bg-hero"> <Header /> <Hero /> </div> <Artworks /> <Transactions /> <CreateNFT /> <UpdateNFT /> <ShowNFT /> <Footer /> <Alert /> <Loading /> </div> ) } export default App


太棒了,我们刚刚完成了各个组件的集成,让我们将它与这个项目的其他部分密封起来。

其他基本文件

此应用程序利用状态管理存储、CometChat SDK 和合同服务文件。让我们一个接一个地看看它们。


Store这个状态管理文件使用react-hooks-global-state npm 包。它比 Redux 简单、快速且容易。这个应用程序中使用的所有全局变量和函数都是在这个商店中创建的。


在项目的根目录下,转到src目录并创建一个名为store的文件夹。现在,在这个 store 文件夹中,创建一个名为index.js的文件并将下面的代码粘贴到其中。


 import { createGlobalState } from 'react-hooks-global-state' const { setGlobalState, useGlobalState, getGlobalState } = createGlobalState({ modal: 'scale-0', updateModal: 'scale-0', mintModal: '', alert: { show: false, msg: '', color: '' }, loading: { show: false, msg: '' }, showModal: 'scale-0', chatOpened: false, recentChatOpened: false, recentOpened: false, chatList: 'scale-0', connectedAccount: '', currentUser: null, nft: null, nfts: [], transactions: [], contract: null, }) const setAlert = (msg, color = 'green') => { setGlobalState('loading', false) setGlobalState('alert', { show: true, msg, color }) setTimeout(() => { setGlobalState('alert', { show: false, msg: '', color }) }, 6000) } const setLoadingMsg = (msg) => { const loading = getGlobalState('loading') setGlobalState('loading', { ...loading, msg }) } const truncate = (text, startChars, endChars, maxLength) => { if (text.length > maxLength) { var start = text.substring(0, startChars) var end = text.substring(text.length - endChars, text.length) while (start.length + end.length < maxLength) { start = start + '.' } return start + end } return text } export { useGlobalState, setGlobalState, getGlobalState, setAlert, setLoadingMsg, truncate, }


CometChat 服务该文件包含与 CometChat SDK 通信的所有基本功能。请参阅下面的代码。


 import { CometChat } from '@cometchat-pro/chat' import { setGlobalState } from './store' const CONSTANTS = { APP_ID: process.env.REACT_APP_COMET_CHAT_APP_ID, REGION: process.env.REACT_APP_COMET_CHAT_REGION, Auth_Key: process.env.REACT_APP_COMET_CHAT_AUTH_KEY, } const initCometChat = async () => { const appID = CONSTANTS.APP_ID const region = CONSTANTS.REGION const appSetting = new CometChat.AppSettingsBuilder() .subscribePresenceForAllUsers() .setRegion(region) .build() await CometChat.init(appID, appSetting) .then(() => console.log('Initialization completed successfully')) .catch((error) => error) } const loginWithCometChat = async (UID) => { const authKey = CONSTANTS.Auth_Key await CometChat.login(UID, authKey) .then((user) => setGlobalState('currentUser', user)) .catch((error) => { alert(error.message) console.log(error) }) return true } const signUpWithCometChat = async (UID, name) => { let authKey = CONSTANTS.Auth_Key const user = new CometChat.User(UID) user.setName(name) await CometChat.createUser(user, authKey) .then((user) => { alert('Signed up successfully, click login now!') console.log('Logged In: ', user) }) .catch((error) => { alert(error.message) console.log(error) }) return true } const logOutWithCometChat = async () => { return await CometChat.logout() .then(() => console.log('Logged Out Successfully')) .catch((error) => error) } const isUserLoggedIn = async () => { await CometChat.getLoggedinUser() .then((user) => setGlobalState('currentUser', user)) .catch((error) => console.log('error:', error)) } const getMessages = async (UID) => { const limit = 30 const messagesRequest = new CometChat.MessagesRequestBuilder() .setUID(UID) .setLimit(limit) .build() return await messagesRequest .fetchPrevious() .then((messages) => messages) .catch((error) => error) } const sendMessage = async (receiverID, messageText) => { const receiverType = CometChat.RECEIVER_TYPE.USER const textMessage = new CometChat.TextMessage( receiverID, messageText, receiverType ) return await CometChat.sendMessage(textMessage) .then((message) => message) .catch((error) => error) } const getConversations = async () => { const limit = 30 const conversationsRequest = new CometChat.ConversationsRequestBuilder() .setLimit(limit) .build() return await conversationsRequest .fetchNext() .then((conversationList) => conversationList) } export { initCometChat, loginWithCometChat, signUpWithCometChat, logOutWithCometChat, getMessages, sendMessage, getConversations, isUserLoggedIn, CometChat, }


合约服务文件该文件包含负责使用 Web3 库与区块链上的智能合约交互的所有函数和过程。请参阅下面的代码。


 import Web3 from 'web3' import { setGlobalState, getGlobalState, setAlert } from './store' import TimelessNFT from './abis/TimelessNFT.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) { setAlert(JSON.stringify(error), 'red') } } const structuredNfts = (nfts) => { return nfts .map((nft) => ({ id: Number(nft.id), owner: nft.owner, cost: window.web3.utils.fromWei(nft.cost), title: nft.title, description: nft.description, metadataURI: nft.metadataURI, timestamp: nft.timestamp, })) .reverse() } const loadWeb3 = async () => { try { if (!ethereum) return alert('Please install Metamask') window.web3 = new Web3(ethereum) 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 = TimelessNFT.networks[networkId] if (networkData) { const contract = new web3.eth.Contract( TimelessNFT.abi, networkData.address ) const nfts = await contract.methods.getAllNFTs().call() const transactions = await contract.methods.getAllTransactions().call() setGlobalState('nfts', structuredNfts(nfts)) setGlobalState('transactions', structuredNfts(transactions)) setGlobalState('contract', contract) } else { window.alert('TimelessNFT contract not deployed to detected network.') } } catch (error) { alert('Please connect your metamask wallet!') } } const mintNFT = async ({ title, description, metadataURI, price }) => { try { price = window.web3.utils.toWei(price.toString(), 'ether') const contract = getGlobalState('contract') const account = getGlobalState('connectedAccount') const mintPrice = window.web3.utils.toWei('0.01', 'ether') await contract.methods .payToMint(title, description, metadataURI, price) .send({ from: account, value: mintPrice }) return true } catch (error) { setAlert(error.message, 'red') } } const buyNFT = async ({ id, cost }) => { try { cost = window.web3.utils.toWei(cost.toString(), 'ether') const contract = getGlobalState('contract') const buyer = getGlobalState('connectedAccount') await contract.methods.payToBuy(Number(id)).send({ from: buyer, value: cost }) return true } catch (error) { setAlert(error.message, 'red') } } const updateNFT = async ({ id, cost }) => { try { cost = window.web3.utils.toWei(cost.toString(), 'ether') const contract = getGlobalState('contract') const buyer = getGlobalState('connectedAccount') await contract.methods.changePrice(Number(id), cost).send({ from: buyer }) return true } catch (error) { setAlert(error.message, 'red') } } export { loadWeb3, connectWallet, mintNFT, buyNFT, updateNFT }


项目资产下载此徽标并将其包含在根目录的资产文件夹中。这样,您就成功地包含了运行此应用程序所需的所有内容。

启动服务器

要继续此步骤,请将智能合约迁移到 Web,以便您可以与之交互。在您的终端上运行以下代码。


 truffle migrate --network rinkeby


上面的代码将使用 Infura RPC 将您的智能合约发送到服务器。


您还可以使用我们在本教程开始时设置的 ganache-cli 服务器设置本地区块链。如果您喜欢这种方式,只需运行以下代码即可将其发送到您的本地区块链服务器。


打开一个终端运行**ganache-cli -d**并在另一个终端上运行**truffle migrate****truffle deploy**


注意,如果您使用ganache-cli作为您的 EVM,您还必须将 localhost 服务器添加到 Metamask,并导入 ganache 生成的私钥。 有关指导,请参阅启动开发环境


如果您需要我帮助解决项目中的问题,请在此页面上咨询我

现在,运行yarn start来启动你的 react 应用程序。在 NFT 市场上,你已经有了这个构建。


立即在 Youtube 上观看我的免费 web3 教程.

结论

我们已经走到了这个 NFT 构建的终点线,我知道你和我一起建立了大量的价值。


无论你是什么级别,如果你想在 web3 开发技能上更快地成长,请进入我的私人课程


直到下一次,继续粉碎它!


关于作者

Gospel Darlington 是一位在软件开发行业拥有6+年以上经验的全栈区块链开发人员。


通过结合软件开发、写作和教学,他演示了如何在与 EVM 兼容的区块链网络上构建去中心化应用程序。


他的技术栈包括JavaScriptReactVueAngularNodeReact NativeNextJsSolidity等。


有关他的更多信息,请访问并关注他在TwitterGitHubLinkedIn或他的网站上的页面。