构建 NFT 藏品
现在是时候推出您自己的 NFT 收藏了 - Crypto Devs。
要求
- 应该只存在 20 个 Crypto Dev NFT,并且每个都应该是唯一的。
 - 用户应该能够通过一笔交易仅铸造 1 个 NFT。
 - 白名单用户应在实际销售前有 5 分钟的预售期,保证每笔交易获得 1 NFT。
 - 你的 NFT 收藏应该有一个网站。
 
让我们开始建造 🚀
先决条件
- 您应该已经完成了之前的白名单 dApp 教程。
 
理论
- 什么是不可替代代币? Fungible 意味着相同或可互换,例如。 Eth 是可替代的。考虑到这一点,NFT 是独一无二的。每一个都是不同的。每个令 牌都有独特的特征和价值。它们都可以相互区分并且不可互换,例如独特的艺术
 - 什么是 ERC-721? ERC-721 是一个开放标准,描述了如何在 EVM(以太坊虚拟机)兼容的区块链上构建不可替代的代币;它是不可替代代币的标准接口;它有一套规则,可以很容易地使用 NFT。在继续之前,先看看 ERC721 支持的所有功能
 
建造
喜欢视频?
如果您想从视频中学习,我们的 YouTube 上有本教程的录音。单击下面的屏幕截图观看视频,或继续阅读教程!
智能合约
我们将使用来自 Openzeppelin 的 Ownable.sol,它可以帮助您管理合约的所有权
- 默认情况下,Ownable 合约的所有者是部署它的帐户,这通常正是您想要的。
 - Ownable 还可以让您:
- 将所有权从所有者帐户转移到新帐户,以及
 - renounceOwnership 让所有者放弃此管理特权,这是集中管理初始阶段结束后的常见模式。
 
 
我们还将使用 ERC721 的扩展,称为 ERC721 Enumerable
- ERC721 Enumerable 可帮助您跟踪合约中的所有 tokenIds 以及给定合约的地址持有的 tokensIds。
 - 在继续之前,请先看看它实现的功能
 
为了构建智能合约,我们将使用 Hardhat。 Hardhat 是一个以太坊开发环境和框架,专 为 Solidity 中的全栈开发而设计。简单来说,您可以编写智能合约、部署它们、运行测试和调试代码。
要设置安全帽项目,请打开终端并执行以下命令
mkdir NFT-Collection
cd NFT-Collection
mkdir hardhat-tutorial
cd hardhat-tutorial
npm init --yes
npm install --save-dev hardhat
在安装 Hardhat 的同一目录中运行:
npx hardhat
确保选择创建 Javascript 项目,然后按照终端中的步骤完成安全帽设置。
现在在同一个终端中安装 @openzeppelin/contracts,因为我们将在我们的 CryptoDevs 合约中导入 Openzeppelin 的 ERC721Enumerable 合约。
npm install @openzeppelin/contracts
我们将需要调用您为之前的级别部署的白名单合约,以检查被列入白名单的地址并授予他们预售访问权限。因为我们只需要调用 mapping(address => bool) public whitelistedAddresses;我们可以为白名单合约创建一个接口,只为这个映射提供一个函数,这样我们就可以节省气体,因为我们不需要继承和部署整个白名单合约,而只需要继承和部署其中的一部分。
在 contracts 目录中创建一个新文件并将其命名为 IWhitelist.sol。
注意:只包含接口的 Solidity 文件通常以 I 为前缀,表示它们只是一个接口。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
interface IWhitelist {
    function whitelistedAddresses(address) external view returns (bool);
}
现在让我们在 contracts 目录中创建一个新文件并将其命名为 CryptoDevs.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "./IWhitelist.sol";
contract CryptoDevs is ERC721Enumerable, Ownable {
    /**
      * @dev _baseTokenURI for computing {tokenURI}. If set, the resulting URI for each
      * token will be the concatenation of the `baseURI` and the `tokenId`.
      */
    string _baseTokenURI;
    //  _price is the price of one Crypto Dev NFT
    uint256 public _price = 0.01 ether;
    // _paused is used to pause the contract in case of an emergency
    bool public _paused;
    // max number of CryptoDevs
    uint256 public maxTokenIds = 20;
    // total number of tokenIds minted
    uint256 public tokenIds;
    // Whitelist contract instance
    IWhitelist whitelist;
    // boolean to keep track of whether presale started or not
    bool public presaleStarted;
    // timestamp for when presale would end
    uint256 public presaleEnded;
    modifier onlyWhenNotPaused {
        require(!_paused, "Contract currently paused");
        _;
    }
    /**
      * @dev ERC721 constructor takes in a `name` and a `symbol` to the token collection.
      * name in our case is `Crypto Devs` and symbol is `CD`.
      * Constructor for Crypto Devs takes in the baseURI to set _baseTokenURI for the collection.
      * It also initializes an instance of whitelist interface.
      */
    constructor (string memory baseURI, address whitelistContract) ERC721("Crypto Devs", "CD") {
        _baseTokenURI = baseURI;
        whitelist = IWhitelist(whitelistContract);
    }
    /**
    * @dev startPresale starts a presale for the whitelisted addresses
      */
    function startPresale() public onlyOwner {
        presaleStarted = true;
        // Set presaleEnded time as current timestamp + 5 minutes
        // Solidity has cool syntax for timestamps (seconds, minutes, hours, days, years)
        presaleEnded = block.timestamp + 5 minutes;
    }
    /**
      * @dev presaleMint allows a user to mint one NFT per transaction during the presale.
      */
    function presaleMint() public payable onlyWhenNotPaused {
        require(presaleStarted && block.timestamp < presaleEnded, "Presale is not running");
        require(whitelist.whitelistedAddresses(msg.sender), "You are not whitelisted");
        require(tokenIds < maxTokenIds, "Exceeded maximum Crypto Devs supply");
        require(msg.value >= _price, "Ether sent is not correct");
        tokenIds += 1;
        //_safeMint is a safer version of the _mint function as it ensures that
        // if the address being minted to is a contract, then it knows how to deal with ERC721 tokens
        // If the address being minted to is not a contract, it works the same way as _mint
        _safeMint(msg.sender, tokenIds);
    }
    /**
    * @dev mint allows a user to mint 1 NFT per transaction after the presale has ended.
    */
    function mint() public payable onlyWhenNotPaused {
        require(presaleStarted && block.timestamp >=  presaleEnded, "Presale has not ended yet");
        require(tokenIds < maxTokenIds, "Exceed maximum Crypto Devs supply");
        require(msg.value >= _price, "Ether sent is not correct");
        tokenIds += 1;
        _safeMint(msg.sender, tokenIds);
    }
    /**
    * @dev _baseURI overides the Openzeppelin's ERC721 implementation which by default
    * returned an empty string for the baseURI
    */
    function _baseURI() internal view virtual override returns (string memory) {
        return _baseTokenURI;
    }
    /**
    * @dev setPaused makes the contract paused or unpaused
      */
    function setPaused(bool val) public onlyOwner {
        _paused = val;
    }
    /**
    * @dev withdraw sends all the ether in the contract
    * to the owner of the contract
      */
    function withdraw() public onlyOwner  {
        address _owner = owner();
        uint256 amount = address(this).balance;
        (bool sent, ) =  _owner.call{value: amount}("");
        require(sent, "Failed to send Ether");
    }
      // Function to receive Ether. msg.data must be empty
    receive() external payable {}
    // Fallback function is called when msg.data is not empty
    fallback() external payable {}
}
现在让我们安装 dotenv 包,以便能够导入 env 文件并在我们的配置中使用它。打开指向 hardhat-tutorial 目录的终端并执行此命令
npm install dotenv
现在在 hardhat-tutorial 文件夹中创建一个 .env 文件并添加以下行。请按照以下说明进行操作。
转到 Quicknode 并注册一个帐户。如果您已经有一个帐户,请登录。Quicknode 是一个节点提供商,可让您连接到各种不同的区块链。我们将使用它通过 Hardhat 部署我们的合约。创建账户后,在 Quicknode 上创建一个 endpoint,选择 Ethereum,然后选择 Goerli 网络。单击右下角的继续,然后单击创建端点。复制 HTTP Provider 中提供给您的链接,并将其添加到 QUICKNODE_HTTP_URL 下面的 .env 文件中。
注意:如果您之前在新生跟踪期间在 Quicknode 上设置了 Goerli Endpoint,则可以使用与之前相同的 URL。无需删除它并设置一个新的。
要获取您的私钥,您需要从 Metamask 中导出它。打开 Metamask,点击三个点,点击 Account Details,然后点击 Export Private Key。确保您使用的是没有主网资金的测试帐户。在您的 .env 文件中为 PRIVATE_KEY 变量添加此私钥。
QUICKNODE_HTTP_URL="添加-quicknode-http-provider-url-这里"
PRIVATE_KEY="在此处添加私钥"
让我们将合约部署到 goerli 网络。在脚本文件夹下创建一个新文件,或替换默认文件,名为 deploy.js
让我们编写一些代码来在 deploy.js 文件中部署合约。
const { ethers } = require("hardhat");
require("dotenv").config({ path: ".env" });
const { WHITELIST_CONTRACT_ADDRESS, METADATA_URL } = require("../constants");
async function main() {
  // Address of the whitelist contract that you deployed in the previous module
  const whitelistContract = WHITELIST_CONTRACT_ADDRESS;
  // URL from where we can extract the metadata for a Crypto Dev NFT
  const metadataURL = METADATA_URL;
  /*
  A ContractFactory in ethers.js is an abstraction used to deploy new smart contracts,
  so cryptoDevsContract here is a factory for instances of our CryptoDevs contract.
  */
  const cryptoDevsContract = await ethers.getContractFactory("CryptoDevs");
  // deploy the contract
  const deployedCryptoDevsContract = await cryptoDevsContract.deploy(
    metadataURL,
    whitelistContract
  );
  // print the address of the deployed contract
  console.log(
    "Crypto Devs Contract Address:",
    deployedCryptoDevsContract.address
  );
}
// Call the main function and catch if there is any error
main()
  .then(() => process.exit(0))
  .catch((error) => {
    console.error(error);
    process.exit(1);
  });
如您所见,deploy.js 需要一些常量。 让我们在 hardhat-tutorial 文件夹下创建一个名为 constants 的文件夹。 在常量文件夹中创建一个 index.js 文件,并将以下行添加到文件中。 将“address-of-the-whitelist-contract”替换为您在上一教程中部署的白名单合约的地址。 对于 Metadata_URL,只需复制已提供的示例。 我们将在教程中进一步替换它。
// Address of the Whitelist Contract that you deployed
const WHITELIST_CONTRACT_ADDRESS = "address-of-the-whitelist-contract";
// URL to extract Metadata for a Crypto Dev NFT
const METADATA_URL = "https://nft-collection-sneh1999.vercel.app/api/";
module.exports = { WHITELIST_CONTRACT_ADDRESS, METADATA_URL };
现在打开 hardhat.config.js 文件,我们将在这里设置 goerli 网络,以便我们可以将合约部署到 Goerli 网络。 用下面给出的行替换 hardhat.config.js 文件中的所有行
require("@nomicfoundation/hardhat-toolbox");
require("dotenv").config({ path: ".env" });
const QUICKNODE_HTTP_URL = process.env.QUICKNODE_HTTP_URL;
const PRIVATE_KEY = process.env.PRIVATE_KEY;
module.exports = {
  solidity: "0.8.4",
  networks: {
    goerli: {
      url: QUICKNODE_HTTP_URL,
      accounts: [PRIVATE_KEY],
    },
  },
};
编译合约,打开指向 hardhat-tutorial 目录的终端并执行此命令
npx hardhat compile
要部署,请打开指向 hardhat-tutorial 目录的终端并执行此命令
npx hardhat run scripts/deploy.js --network goerli
将打印在终端上的 Crypto Devs 合约地址保存在记事本中,您将在教程中进一步使用它。
网站
为了开发网站,我们将使用 React 和 Next Js。 React 是一个用于制作网站的 javascript 框架,Next Js 构建在 React 之上。 首先,您需要创建一个新的下一个应用程序。 您的文件夹结构应该类似于
- NFT-Collection
       - hardhat-tutorial
       - my-app
要创建这个我的应用程序,在终端指向 NFT-Collection 文件夹并输入
npx create-next-app@latest
并按回车键所有问题
现在运行应用程序,在终端中执行这些命令
cd my-app
npm run dev
现在转到 http://localhost:3000,您的应用程序应该正在运行 🤘
现在让我们安装 Web3Modal 库(https://github.com/Web3Modal/web3modal)。 Web3Modal 是一个易于使用的库,可帮助开发人员通过简单的可自定义配置在其应用程序中添加对多个提供程序的支持。 默认情况下,Web3Modal 库支持注入的提供程序,例如(Metamask、Dapper、Gnosis Safe、Frame、Web3 浏览器等),您还可以轻松配置库以支持 Portis、Fortmatic、Squarelink、Torus、Authereum、D'CENT 钱包和 Arkane。 打开指向 atmy-app 目录的终端并执行此命令
npm install web3modal
同样的安装 ethers.js
npm install ethers
在您的公共文件夹中,下载此文件夹及其中的所有图像(下载链接)。 确保下载的文件夹的名称是 cryptodevs
现在转到样式文件夹并将 Home.modules.css 文件的所有内容替换为以下代码,这将为您的 dapp 添加一些样式:
.main {
  min-height: 90vh;
  display: flex;
  flex-direction: row;
  justify-content: center;
  align-items: center;
  font-family: "Courier New", Courier, monospace;
}
.footer {
  display: flex;
  padding: 2rem 0;
  border-top: 1px solid #eaeaea;
  justify-content: center;
  align-items: center;
}
.image {
  width: 70%;
  height: 50%;
  margin-left: 20%;
}
.title {
  font-size: 2rem;
  margin: 2rem 0;
}
.description {
  line-height: 1;
  margin: 2rem 0;
  font-size: 1.2rem;
}
.button {
  border-radius: 4px;
  background-color: blue;
  border: none;
  color: #ffffff;
  font-size: 15px;
  padding: 20px;
  width: 200px;
  cursor: pointer;
  margin-bottom: 2%;
}
@media (max-width: 1000px) {
  .main {
    width: 100%;
    flex-direction: column;
    justify-content: center;
    align-items: center;
  }
}
打开 pages 文件夹下的 index.js 文件并粘贴以下代码,代码解释可以在评论中找到。
import { Contract, providers, utils } from "ethers";
import Head from "next/head";
import React, { useEffect, useRef, useState } from "react";
import Web3Modal from "web3modal";
import { abi, NFT_CONTRACT_ADDRESS } from "../constants";
import styles from "../styles/Home.module.css";
export default function Home() {
  // walletConnected keep track of whether the user's wallet is connected or not
  const [walletConnected, setWalletConnected] = useState(false);
  // presaleStarted keeps track of whether the presale has started or not
  const [presaleStarted, setPresaleStarted] = useState(false);
  // presaleEnded keeps track of whether the presale ended
  const [presaleEnded, setPresaleEnded] = useState(false);
  // loading is set to true when we are waiting for a transaction to get mined
  const [loading, setLoading] = useState(false);
  // checks if the currently connected MetaMask wallet is the owner of the contract
  const [isOwner, setIsOwner] = useState(false);
  // tokenIdsMinted keeps track of the number of tokenIds that have been minted
  const [tokenIdsMinted, setTokenIdsMinted] = useState("0");
  // Create a reference to the Web3 Modal (used for connecting to Metamask) which persists as long as the page is open
  const web3ModalRef = useRef();
  /**
   * presaleMint: Mint an NFT during the presale
   */
  const presaleMint = async () => {
    try {
      // We need a Signer here since this is a 'write' transaction.
      const signer = await getProviderOrSigner(true);
      // Create a new instance of the Contract with a Signer, which allows
      // update methods
      const nftContract = new Contract(NFT_CONTRACT_ADDRESS, abi, signer);
      // call the presaleMint from the contract, only whitelisted addresses would be able to mint
      const tx = await nftContract.presaleMint({
        // value signifies the cost of one crypto dev which is "0.01" eth.
        // We are parsing `0.01` string to ether using the utils library from ethers.js
        value: utils.parseEther("0.01"),
      });
      setLoading(true);
      // wait for the transaction to get mined
      await tx.wait();
      setLoading(false);
      window.alert("You successfully minted a Crypto Dev!");
    } catch (err) {
      console.error(err);
    }
  };
  /**
   * publicMint: Mint an NFT after the presale
   */
  const publicMint = async () => {
    try {
      // We need a Signer here since this is a 'write' transaction.
      const signer = await getProviderOrSigner(true);
      // Create a new instance of the Contract with a Signer, which allows
      // update methods
      const nftContract = new Contract(NFT_CONTRACT_ADDRESS, abi, signer);
      // call the mint from the contract to mint the Crypto Dev
      const tx = await nftContract.mint({
        // value signifies the cost of one crypto dev which is "0.01" eth.
        // We are parsing `0.01` string to ether using the utils library from ethers.js
        value: utils.parseEther("0.01"),
      });
      setLoading(true);
      // wait for the transaction to get mined
      await tx.wait();
      setLoading(false);
      window.alert("You successfully minted a Crypto Dev!");
    } catch (err) {
      console.error(err);
    }
  };
  /*
      connectWallet: Connects the MetaMask wallet
    */
  const connectWallet = async () => {
    try {
      // Get the provider from web3Modal, which in our case is MetaMask
      // When used for the first time, it prompts the user to connect their wallet
      await getProviderOrSigner();
      setWalletConnected(true);
    } catch (err) {
      console.error(err);
    }
  };
  /**
   * startPresale: starts the presale for the NFT Collection
   */
  const startPresale = async () => {
    try {
      // We need a Signer here since this is a 'write' transaction.
      const signer = await getProviderOrSigner(true);
      // Create a new instance of the Contract with a Signer, which allows
      // update methods
      const nftContract = new Contract(NFT_CONTRACT_ADDRESS, abi, signer);
      // call the startPresale from the contract
      const tx = await nftContract.startPresale();
      setLoading(true);
      // wait for the transaction to get mined
      await tx.wait();
      setLoading(false);
      // set the presale started to true
      await checkIfPresaleStarted();
    } catch (err) {
      console.error(err);
    }
  };
  /**
   * checkIfPresaleStarted: checks if the presale has started by quering the `presaleStarted`
   * variable in the contract
   */
  const checkIfPresaleStarted = async () => {
    try {
      // Get the provider from web3Modal, which in our case is MetaMask
      // No need for the Signer here, as we are only reading state from the blockchain
      const provider = await getProviderOrSigner();
      // We connect to the Contract using a Provider, so we will only
      // have read-only access to the Contract
      const nftContract = new Contract(NFT_CONTRACT_ADDRESS, abi, provider);
      // call the presaleStarted from the contract
      const _presaleStarted = await nftContract.presaleStarted();
      if (!_presaleStarted) {
        await getOwner();
      }
      setPresaleStarted(_presaleStarted);
      return _presaleStarted;
    } catch (err) {
      console.error(err);
      return false;
    }
  };
  /**
   * checkIfPresaleEnded: checks if the presale has ended by quering the `presaleEnded`
   * variable in the contract
   */
  const checkIfPresaleEnded = async () => {
    try {
      // Get the provider from web3Modal, which in our case is MetaMask
      // No need for the Signer here, as we are only reading state from the blockchain
      const provider = await getProviderOrSigner();
      // We connect to the Contract using a Provider, so we will only
      // have read-only access to the Contract
      const nftContract = new Contract(NFT_CONTRACT_ADDRESS, abi, provider);
      // call the presaleEnded from the contract
      const _presaleEnded = await nftContract.presaleEnded();
      // _presaleEnded is a Big Number, so we are using the lt(less than function) instead of `<`
      // Date.now()/1000 returns the current time in seconds
      // We compare if the _presaleEnded timestamp is less than the current time
      // which means presale has ended
      const hasEnded = _presaleEnded.lt(Math.floor(Date.now() / 1000));
      if (hasEnded) {
        setPresaleEnded(true);
      } else {
        setPresaleEnded(false);
      }
      return hasEnded;
    } catch (err) {
      console.error(err);
      return false;
    }
  };
  /**
   * getOwner: calls the contract to retrieve the owner
   */
  const getOwner = async () => {
    try {
      // Get the provider from web3Modal, which in our case is MetaMask
      // No need for the Signer here, as we are only reading state from the blockchain
      const provider = await getProviderOrSigner();
      // We connect to the Contract using a Provider, so we will only
      // have read-only access to the Contract
      const nftContract = new Contract(NFT_CONTRACT_ADDRESS, abi, provider);
      // call the owner function from the contract
      const _owner = await nftContract.owner();
      // We will get the signer now to extract the address of the currently connected MetaMask account
      const signer = await getProviderOrSigner(true);
      // Get the address associated to the signer which is connected to  MetaMask
      const address = await signer.getAddress();
      if (address.toLowerCase() === _owner.toLowerCase()) {
        setIsOwner(true);
      }
    } catch (err) {
      console.error(err.message);
    }
  };
  /**
   * getTokenIdsMinted: gets the number of tokenIds that have been minted
   */
  const getTokenIdsMinted = async () => {
    try {
      // Get the provider from web3Modal, which in our case is MetaMask
      // No need for the Signer here, as we are only reading state from the blockchain
      const provider = await getProviderOrSigner();
      // We connect to the Contract using a Provider, so we will only
      // have read-only access to the Contract
      const nftContract = new Contract(NFT_CONTRACT_ADDRESS, abi, provider);
      // call the tokenIds from the contract
      const _tokenIds = await nftContract.tokenIds();
      //_tokenIds is a `Big Number`. We need to convert the Big Number to a string
      setTokenIdsMinted(_tokenIds.toString());
    } catch (err) {
      console.error(err);
    }
  };
  /**
   * Returns a Provider or Signer object representing the Ethereum RPC with or without the
   * signing capabilities of metamask attached
   *
   * A `Provider` is needed to interact with the blockchain - reading transactions, reading balances, reading state, etc.
   *
   * A `Signer` is a special type of Provider used in case a `write` transaction needs to be made to the blockchain, which involves the connected account
   * needing to make a digital signature to authorize the transaction being sent. Metamask exposes a Signer API to allow your website to
   * request signatures from the user using Signer functions.
   *
   * @param {*} needSigner - True if you need the signer, default false otherwise
   */
  const getProviderOrSigner = async (needSigner = false) => {
    // Connect to Metamask
    // Since we store `web3Modal` as a reference, we need to access the `current` value to get access to the underlying object
    const provider = await web3ModalRef.current.connect();
    const web3Provider = new providers.Web3Provider(provider);
    // If user is not connected to the Goerli network, let them know and throw an error
    const { chainId } = await web3Provider.getNetwork();
    if (chainId !== 5) {
      window.alert("Change the network to Goerli");
      throw new Error("Change network to Goerli");
    }
    if (needSigner) {
      const signer = web3Provider.getSigner();
      return signer;
    }
    return web3Provider;
  };
  // useEffects are used to react to changes in state of the website
  // The array at the end of function call represents what state changes will trigger this effect
  // In this case, whenever the value of `walletConnected` changes - this effect will be called
  useEffect(() => {
    // if wallet is not connected, create a new instance of Web3Modal and connect the MetaMask wallet
    if (!walletConnected) {
      // Assign the Web3Modal class to the reference object by setting it's `current` value
      // The `current` value is persisted throughout as long as this page is open
      web3ModalRef.current = new Web3Modal({
        network: "goerli",
        providerOptions: {},
        disableInjectedProvider: false,
      });
      connectWallet();
      // Check if presale has started and ended
      const _presaleStarted = checkIfPresaleStarted();
      if (_presaleStarted) {
        checkIfPresaleEnded();
      }
      getTokenIdsMinted();
      // Set an interval which gets called every 5 seconds to check presale has ended
      const presaleEndedInterval = setInterval(async function () {
        const _presaleStarted = await checkIfPresaleStarted();
        if (_presaleStarted) {
          const _presaleEnded = await checkIfPresaleEnded();
          if (_presaleEnded) {
            clearInterval(presaleEndedInterval);
          }
        }
      }, 5 * 1000);
      // set an interval to get the number of token Ids minted every 5 seconds
      setInterval(async function () {
        await getTokenIdsMinted();
      }, 5 * 1000);
    }
  }, [walletConnected]);
  /*
      renderButton: Returns a button based on the state of the dapp
    */
  const renderButton = () => {
    // If wallet is not connected, return a button which allows them to connect their wllet
    if (!walletConnected) {
      return (
        <button onClick={connectWallet} className={styles.button}>
          Connect your wallet
        </button>
      );
    }
    // If we are currently waiting for something, return a loading button
    if (loading) {
      return <button className={styles.button}>Loading...</button>;
    }
    // If connected user is the owner, and presale hasnt started yet, allow them to start the presale
    if (isOwner && !presaleStarted) {
      return (
        <button className={styles.button} onClick={startPresale}>
          Start Presale!
        </button>
      );
    }
    // If connected user is not the owner but presale hasn't started yet, tell them that
    if (!presaleStarted) {
      return (
        <div>
          <div className={styles.description}>Presale hasnt started!</div>
        </div>
      );
    }
    // If presale started, but hasn't ended yet, allow for minting during the presale period
    if (presaleStarted && !presaleEnded) {
      return (
        <div>
          <div className={styles.description}>
            Presale has started!!! If your address is whitelisted, Mint a Crypto
            Dev 🥳
          </div>
          <button className={styles.button} onClick={presaleMint}>
            Presale Mint 🚀
          </button>
        </div>
      );
    }
    // If presale started and has ended, its time for public minting
    if (presaleStarted && presaleEnded) {
      return (
        <button className={styles.button} onClick={publicMint}>
          Public Mint 🚀
        </button>
      );
    }
  };
  return (
    <div>
      <Head>
        <title>Crypto Devs</title>
        <meta name="description" content="Whitelist-Dapp" />
        <link rel="icon" href="/favicon.ico" />
      </Head>
      <div className={styles.main}>
        <div>
          <h1 className={styles.title}>Welcome to Crypto Devs!</h1>
          <div className={styles.description}>
            Its an NFT collection for developers in Crypto.
          </div>
          <div className={styles.description}>
            {tokenIdsMinted}/20 have been minted
          </div>
          {renderButton()}
        </div>
        <div>
          <img className={styles.image} src="./cryptodevs/0.svg" />
        </div>
      </div>
      <footer className={styles.footer}>
        Made with ❤ by Crypto Devs
      </footer>
    </div>
  );
}
现在在 my-app 文件夹下创建一个新文件夹并将其命名为常量。在常量文件夹中创建一个文件 index.js 并粘贴以下代码。
将“您的 NFT 合约地址”替换为您部署并保存到记事本的 CryptoDevs 合约的地址。将 ---your abi--- 替换为 CryptoDevs 合约的 abi。要获取合约的 abi, 请转到您的 hardhat-tutorial/artifacts/contracts/CryptoDevs.sol 文件夹,然后从 CryptoDevs.json 文件中获取标记为“abi”键的数组。
export const abi =---your abi---
export const NFT_CONTRACT_ADDRESS = "address of your NFT contract"
现在在指向 my-app 文件夹的终端中,执行
npm run dev
您的 Crypto Devs NFT dapp 现在应该可以正常运行了 🚀
推送到 github
确保在继续之前您已将所有代码推送到 github :)
部署你的 dApp
我们现在将部署您的 dApp,以便每个人都可以看到您的网站,并且您可以与所有 LearnWeb3 DAO 朋友分享它。
- 转到 https://vercel.com/ 并使用您的 GitHub 登录
 - 然后点击 New Project 按钮,然后选择你的 NFT-Collection repo
 - 配置新项目时,Vercel 将允许您自定义根目录
 - 单击根目录旁边的编辑并将其设置为 my-app
 - 选择框架为 Next.js
 - 单击部署映像
 
现在,您可以通过转到仪表板、选择您的项目并从那里复制域来查看您部署的网站!将域保存在记事本上,稍后您将需要它。
在 Opensea 上查看您的收藏
现在让您的收藏在 Opensea 上可用
为了使该集合在 Opensea 上可用,我们需要创建一个元数据端点。该端点将返回给定其 tokenId 的 NFT 的元数据。
打开你的 my-app 文件夹和 underpages/api 文件夹,创建一个名为 [tokenId].js 的新文件(确保名称也包含括号)。添加括号有助于在下一个 js 中创建动态路由
将以下行添加到 [tokenId].js 文件。在此处阅读有关在下一个 js 中添加 API 路由的信息
export default function handler(req, res) {
  // get the tokenId from the query params
  const tokenId = req.query.tokenId;
  // As all the images are uploaded on github, we can extract the images from github directly.
  const image_url =
    "https://raw.githubusercontent.com/LearnWeb3DAO/NFT-Collection/main/my-app/public/cryptodevs/";
  // The api is sending back metadata for a Crypto Dev
  // To make our collection compatible with Opensea, we need to follow some Metadata standards
  // when sending back the response from the api
  // More info can be found here: https://docs.opensea.io/docs/metadata-standards
  res.status(200).json({
    name: "Crypto Dev #" + tokenId,
    description: "Crypto Dev is a collection of developers in crypto",
    image: image_url + tokenId + ".svg",
  });
}
现在你有了一个 API 路由,OpenSea 和其他网站可以调用它来检索 NFT 的元数据。这些网站首先在智能合约上调用 tokenURI 以获取 NFT 元数据存储位置的链接。 tokenURI 将为他们提供我们刚刚创建的 API 路由。然后,他们可以调用此 API 路由来获取 NFT 的名称、描述和图像。
让我们用这个新的 API 路由作为你的 METADATA_URL 部署一个新版本的 Crypto Devs 合约
打开您的 hardhat-tutorial/constants 文件夹并在 index.js 文件中,将“https://nft-collection-sneh1999.vercel.app/api/”替换为您保存到记事本的域并添加“/api/”到尽头。
保存文件并打开一个指向 hardhat-tutorial 文件夹的新终端并部署新合约
npx hardhat run scripts/deploy.js --network goerli
将新的 NFT 合约地址保存到记事本中,因为我们稍后将需要它。打开“my-app/constants”文件夹并在 index.js 文件中将旧的 NFT 合约地址替换为新的
将所有代码推送到 github,等待 Vercel 部署新代码。在 vercel 部署您的代码后,打开您的网站并铸造一个 NFT。交易成功后,在浏览器中打开此链接,将 your-nft-contract-address 替换为 NFT 合约的地址(https://testnets.opensea.io/assets/goerli/your-nft-contract-地址1)
您的 NFT 现在可以在 Opensea 上使用 🚀 🥳 在 discord 上与所有人分享您的 Opensea 链接 :) 并传播快乐。