使用 Next、IPFS、The Graph、Solidity 和 Polygon 构建全栈 Web3 YouTube 克隆

每天都有越来越多的人过渡到 Web3。对开发人员的需求正在增加,区块链开发技能是科技行业最需要的技能之一。

提高 Web3 技能的最佳方法是使用它们来创建项目。在本文中,您将使用以下技术堆栈在 Polygon 区块链之上构建一个完整的 YouTube 克隆。

  • 前端框架:Next.js
  • 智能合约:Solidity
  • 以太坊网络客户端库:Ethers.js
  • 文件存储:IPFS
  • 查询数据:图表
  • CSS 框架:TailwindCSS
  • 以太坊开发环境:Hardhat
  • 第 2 层区块链:多边形

先决条件

在开始本教程之前,请确保您有Node.js v14 或更高版本,并在您的机器上安装了Metamask浏览器扩展。

设置 Next.js 应用程序

第一步是设置 next.js 应用程序并安装所需的依赖项。为此,您需要在终端中运行以下命令。

mkdir web3-youtube && cd web3-youtube && npx create-next-app .

以下命令创建一个名为 的新目录web3-youtube,然后导航到该目录并创建一个 next.js 应用程序。

成功创建项目后,运行以下命令来安装一些其他依赖项。

npm install react-icons plyr-react moment ipfs-http-client ethers @apollo/client graphql dotenv

  • react-icons是我们将在应用程序中使用的图标库。
  • plyr-react是一个具有丰富插件和功能的视频播放器组件。
  • moment是一个用于解析、验证、操作和格式化日期的 JavaScript 日期库。
  • ipfs-http-client用于将视频和缩略图上传到 IPFS。
  • ethers是一个以太坊客户端文学,将用于与智能合约进行交互

您还可以运行以下命令将 Hardhat 作为开发依赖项安装到您的项目中。

npm install --dev hardhat @nomicfoundation/hardhat-toolbox

初始化本地以太坊环境

接下来,是时候使用 Hardhat 初始化本地智能合约开发了。为此,只需在终端中运行以下命令。

npx hardhat

上面的命令将搭建基本的 Solidity 开发环境。您应该在下面看到项目目录中生成的新文件/文件夹。

test: 该文件夹包含一个用 Chai 编写的测试脚本,用于测试智能合约。

hardhat.config.js: 此文件包含 Hardhat 的配置。

scripts:此文件夹包含一个示例脚本,用于显示部署智能合约。

contracts:这是包含我们编写智能合约代码的文件的文件夹。

添加 TailwindCSS

Tailwind CSS 是一个实用程序优先的 CSS 框架,用于快速构建用户界面。我们将使用它来设计我们的应用程序。运行以下命令来安装 tailwindcss 及其依赖项。

npm install --dev tailwindcss postcss autoprefixer

安装依赖项后,我们需要启动 Tailwind CSS。为此,请在终端中运行以下代码。

npx tailwind init -p

上面的命令会生成两个名为tailwind.config.js和的文件postcss.config.js。接下来,在任何代码编辑器中打开项目并将里面的代码替换为tailwind.config.js以下代码。

module.exports = {
  content: [
    "./pages/**/*.{js,ts,jsx,tsx}",
    "./components/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}

最后,将 Tailwind 的每个层的 tailwind 指令添加到./styles/globals.css文件中。

@tailwind base;
@tailwind components;
@tailwind utilities;

您还可以通过更新pages/index.js文件内的代码来检查 Tailwind CSS 是否已成功集成。

import React from "react";

export default function index() {
  return (
    <div className="flex flex-col justify-center items-center h-screen">
      <h1 className="text-6xl font-bold text-slate-900">Web3 YouTube Clone</h1>
      <h3 className="text-2xl mt-8 text-slate-900">
        Next.js, TailwindCSS, Solidity, IPFS, The Graph and Polygon
      </h3>
    </div>
  );
}

保存文件并运行npm run dev以启动 next.js 应用程序,您应该会看到类似的页面。

智能合约

现在项目设置已完成,我们可以开始为我们的应用程序编写智能合约。在本文中,我将使用 Solidity。

智能合约是一个去中心化的程序,它通过执行业务逻辑来响应事件。

在 contracts 文件夹中,创建一个名为的新文件Youtube.sol并将以下代码添加到其中。

//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;

contract YouTube {
    // Declaring the videoCount 0 by default
    uint256 public videoCount = 0;
    // Name of your contract
    string public name = "YouTube";
    // Creating a mapping of videoCount to Video
    mapping(uint256 => Video) public videos;

    //  Create a struct called 'Video' with the following properties:
    struct Video {
        uint256 id;
        string hash;
        string title;
        string description;
        string location;
        string category;
        string thumbnailHash;
        string date;
        address author;
    }

    // Create a 'VideoUploaded' event that emits the properties of the video
    event VideoUploaded(
        uint256 id,
        string hash,
        string title,
        string description,
        string location,
        string category,
        string thumbnailHash,
        string date,
        address author
    );

    constructor() {}

    // Function to upload a video
    function uploadVideo(
        string memory _videoHash,
        string memory _title,
        string memory _description,
        string memory _location,
        string memory _category,
        string memory _thumbnailHash,
        string memory _date
    ) public {
        // Validating the video hash, title and author's address
        require(bytes(_videoHash).length > 0);
        require(bytes(_title).length > 0);
        require(msg.sender != address(0));

        // Incrementing the video count
        videoCount++;
        // Adding the video to the contract
        videos[videoCount] = Video(
            videoCount,
            _videoHash,
            _title,
            _description,
            _location,
            _category,
            _thumbnailHash,
            _date,
            msg.sender
        );
        // Triggering the event
        emit VideoUploaded(
            videoCount,
            _videoHash,
            _title,
            _description,
            _location,
            _category,
            _thumbnailHash,
            _date,
            msg.sender
        );
    }
}

修改安全帽配置

现在,我们需要对 Hardhat 配置文件进行一些修改,以便部署我们的智能合约。在代码编辑器中打开 hardhat.config.js并将 module.exports 对象更新为以下代码。

require("@nomicfoundation/hardhat-toolbox");
require("dotenv").config();

module.exports = {
  solidity: "0.8.9",
  networks: {
    mumbai: {
      url: "https://rpc-mumbai.maticvigil.com",
      accounts: process.env.PRIVATE_KEY,
    },
  },
  paths: {
    artifacts: "./artifacts",
  },
};

要部署我们的合约,我们需要一个私钥。在浏览器中打开 Metamask,然后单击右上角的三个并选择帐户详细信息。

然后,单击“导出私钥”。系统将提示您输入 Metamask 密码。输入您的密码并点击确认。

您应该在红色框中看到您的私钥。

在项目根目录中创建一个.env文件并添加您的私钥。

PRIVATE_KEY="YOUR_METAMASK_PRIVATE_KEY"

永远不要共享您的私钥。任何拥有您私钥的人都可以窃取您账户中持有的任何资产。

使用 Hardhat 编译智能合约

现在我们的智能合约已经完成,让我们继续编译它们。您可以使用下面的命令编译它。

npx hardhat compile

如果您遇到错误 HH801: Plugin @nomicfoundation/hardhat-toolbox requires the following dependencies to be installed。运行以下命令以安装安全帽依赖项

npm install --save-dev "@nomicfoundation/hardhat-network-helpers@^1.0.0" "@nomicfoundation/hardhat-chai-matchers@^1.0.0" "@nomiclabs/hardhat-ethers@^2.0.0" "@nomiclabs/hardhat-etherscan@^3.0.0" "@types/chai@^4.2.0" "@types/mocha@^9.1.0" "@typechain/ethers-v5@^10.1.0" "@typechain/hardhat@^6.1.2" "chai@^4.2.0" "hardhat-gas-reporter@^1.0.8" "solidity-coverage@^0.7.21" "ts-node@>=8.0.0" "typechain@^8.1.0" "typescript@>=4.5.0"

安装包后,重新运行上面的编译命令。

编译成功完成后,您应该会看到artifacts在您的项目目录中创建了一个名为的新目录。

Artifacts 包含我们的智能合约的 JSON 格式的编译版本。此 JSON 文件包含一个名为 ABI 的数组。ABI 或应用程序二进制接口是我们将客户端(下一个应用程序)与我们编译的智能合约连接起来所需要的。

在 Polygon 上部署智能合约

现在,我们可以在 Polygon Mumbai 上部署我们的智能合约。我们已经添加了 RPC 和 Metamask 私钥,所以我们不需要再做一次。但是,您需要一些 $MATIC 才能部署智能合约。

导航到https://faucet.polygon.technology/并粘贴您的钱包地址。单击确认,您的钱包中应该会收到 0.2 MATIC。

默认情况下,Metamask 在网络列表中没有 Polygon 区块链,因此我们需要手动添加它。转到 Metamask 设置并选择手动添加网络。使用以下信息将 Polygon Mumbai 添加到 Metamask。

Network Name: Mumbai Testnet
New RPC URL: <https://rpc-mumbai.maticvigil.com/>
Chain ID: 80001
Currency Symbol: MATIC
Block Explorer URL: <https://polygonscan.com/>

保存它,你应该会在 Metamask 钱包上看到 0.2 MATIC。

接下来,用scripts/deploy.js下面的代码替换里面的代码。

// We require the Hardhat Runtime Environment explicitly here. This is optional
// but useful for running the script in a standalone fashion through `node <script>`.
//
// When running the script with `npx hardhat run <script>` you'll find the Hardhat
// Runtime Environment's members available in the global scope.
const hre = require("hardhat");

async function main() {
  // Hardhat always runs the compile task when running scripts with its command
  // line interface.
  //
  // If this script is run directly using `node` you may want to call compile
  // manually to make sure everything is compiled
  // await hre.run('compile');

  // We get the contract to deploy
  const YouTube = await hre.ethers.getContractFactory("YouTube");
  const youtube = await YouTube.deploy();

  await youtube.deployed();

  console.log("YouTube deployed to:", youtube.address);
}

// We recommend this pattern to be able to use async/await everywhere
// and properly handle errors.
main()
  .then(() => process.exit(0))
  .catch((error) => {
    console.error(error);
    process.exit(1);
  });

最后,运行以下命令来部署您的智能合约。

npx hardhat run scripts/deploy.js --network mumbai

此命令需要一些时间,但一旦完成,您应该会看到类似以下的消息:

YouTube deployed to: 0x0AE42f411420b2710474e5e4f2F551b36350F9D1

这意味着我们的合约已成功部署🎉

设置图表

您可以在 ethers.js 等软件包的帮助下使用智能合约事件,也可以使用 The Graph 从区块链查询数据。The Graph 是一种链下索引解决方案,可以帮助您以更简单的方式查询数据。

在本教程中,我们将使用 The Graph 从区块链中查询视频,因为它非常简单并且使用 GraphQL 查询语言。

创建子图

子图从区块链中提取数据,对其进行处理和存储,以便可以通过 GraphQL 轻松查询。

要创建子图,您首先需要安装 The Graph CLI。Graph CLI 是用 JavaScript 编写的,您需要安装 yarn 或 npm 才能使用它。您可以运行以下命令来安装它。

npm install -g @graphprotocol/graph-cli

安装后,运行graph init以初始化项目中的子图。系统会提示您一些问题。您可以按照以下代码获取答案:

✔ Protocol · ethereum
✔ Product for which to initialize · hosted-service
✔ Subgraph name · suhailkakar/blog-yt-clone
✔ Directory to create the subgraph in · indexer
✔ Contract address · 0x0AE42f411420b2710474e5e4f2F551b36350F9D1
✖ Failed to fetch ABI from Etherscan: ABI not found, try loading it from a local file
✔ ABI file (path) · /Users/suhail/Desktop/web3-youtube/frontend/artifacts/contracts/Youtube.sol/YouTube.json
✔ Contract Name · YouTube
✔ Add another contract? (y/N) · false

确保更新合约地址、名称和 ABI。

接下来,让我们为我们的应用程序声明模式。用以下代码替换schema.graphql索引器目录内部的代码。

type Video @entity {
  id: ID!
  hash: String! # string
  title: String! # string
  description: String # string
  location: String # string
  category: String # string
  thumbnailHash: String! # string
  date: String # string
  author: Bytes! # address
  createdAt: BigInt! # timestamp
}

现在,用you-tube.ts下面的代码替换里面的代码。

import { VideoUploaded as VideoUploadedEvent } from "../generated/YouTube/YouTube";
import { Video } from "../generated/schema";

export function handleVideoUploaded(event: VideoUploadedEvent): void {
  let video = new Video(event.params.id.toString());
  video.hash = event.params.hash;
  video.title = event.params.title;
  video.description = event.params.description;
  video.location = event.params.location;
  video.category = event.params.category;
  video.thumbnailHash = event.params.thumbnailHash;
  video.date = event.params.date;
  video.author = event.params.author;
  video.createdAt = event.block.timestamp;
  video.save();
}

导航到索引器目录并运行yarn codegen以从您的 GraphQL 操作和模式生成代码。

构建子图

在我们部署子图之前,我们需要构建它。为此,只需在终端中运行以下命令。

yarn build

接下来,为了部署我们的子图,我们需要在 The Graph 上创建一个帐户。

部署子图

继续创建一个帐户,然后导航到https://thegraph.com/hosted-service/dashboard。单击添加子图按钮。

接下来,屏幕填写与您的子图相关的信息并在屏幕底部创建子图按钮

创建子图后,复制它的访问令牌,因为我们稍后需要它。在您的终端运行graph auth并选择托管服务。在部署密钥中,粘贴您之前复制的密钥。

最后,运行以下命令来部署您的子图。

yarn deploy

如果一切顺利,您应该会看到类似于以下输出的子图链接。🎉

Build completed: QmV19RJaCXCcKKBe3BTyrL8cGqKNaEo9kpwxMTgrPnDKYA

Deployed to https://thegraph.com/explorer/subgraph/suhailkakar/test-blog-yt

Queries (HTTP):     https://api.thegraph.com/subgraphs/name/suhailkakar/test-blog-yt

前端

现在我们已经完成了智能合约,是时候在应用程序的前端工作了。让我们从应用程序的身份验证开始。

验证

第一步是在我们的应用程序中设置身份验证,允许用户连接他们的钱包。landing在 pages 文件夹内创建一个名为的新文件夹,并在其中创建一个名为 index.js 的新文件。该文件将包含我们应用程序中登录页面的代码,这也将允许用户连接他们的钱包。

擦除index.js页面目录中的所有内容并将文件导入Landing文件中。这是您的 index.js 文件的外观。

import React from "react";
import Landing from "./landing";

export default function index() {
  return (
   <Landing />
  );
}

现在,在登陆页面上,我们将创建一个带有连接钱包按钮的简单英雄组件,允许用户连接他们的钱包并访问我们的应用程序。

将以下代码添加到登录页面。我已经添加了评论,以便您可以正确理解它们。

import React, { useState } from "react";

function Landing() {
  // Creating a function to connect user's wallet
  const connectWallet = async () => {
    try {
      const { ethereum } = window;

      // Checking if user have Metamask installed
      if (!ethereum) {
        // If user doesn't have Metamask installed, throw an error
        alert("Please install MetaMask");
        return;
      }

      // If user has Metamask installed, connect to the user's wallet
      const accounts = await ethereum.request({
        method: "eth_requestAccounts",
      });

      // At last save the user's wallet address in browser's local storage
      localStorage.setItem("walletAddress", accounts[0]);
    } catch (error) {
      console.log(error);
    }
  };

  return (
    <>
      {/* Creating a hero component with black background and centering everything in the screen */}
      <section className="relative bg-black flex flex-col h-screen justify-center items-center">
        <div className="max-w-7xl mx-auto px-4 sm:px-6">
          <div className="pt-32 pb-12 md:pt-40 md:pb-20">
            <div className="text-center pb-12 md:pb-16">
              <h1
                className="text-5xl text-white md:text-6xl font-extrabold leading-tighter tracking-tighter mb-4"
                data-aos="zoom-y-out"
              >
                It is YouTube, but{" "}
                <span className="bg-clip-text text-transparent bg-gradient-to-r from-blue-500 to-teal-400">
                  Decentralized
                </span>
              </h1>
              <div className="max-w-3xl mx-auto">
                <p
                  className="text-xl text-gray-400 mb-8"
                  data-aos="zoom-y-out"
                  data-aos-delay="150"
                >
                  A YouTube Clone built on top of Polygon network, allow users
                  to create, share and watch videos, without worrying about
                  their privacy.
                </p>
                <button
                  className="items-center  bg-white rounded-full font-medium  p-4 shadow-lg"
                  onClick={() => {
                    // Calling the connectWallet function when user clicks on the button
                    connectWallet();
                  }}
                >
                  <span>Connect wallet</span>
                </button>
              </div>
            </div>
          </div>
        </div>
      </section>
    </>
  );
}

export default Landing;

如果一切顺利,您应该会看到类似的屏幕。您还应该能够连接您的 MetaMask 钱包。

上传视频

现在用户可以连接他们的钱包,是时候为我们的应用程序添加上传视频功能了。

在名为 pages 的目录中创建一个新文件夹,upload并添加一个名为 index.js. 在文件内部添加以下代码。同样,我已经在代码上添加了注释,所以我希望能帮助你理解它。

import React, { useState, useRef } from "react";
import { BiCloud, BiMusic, BiPlus } from "react-icons/bi";
import { create } from "ipfs-http-client";

export default function Upload() {
  // Creating state for the input field
  const [title, setTitle] = useState("");
  const [description, setDescription] = useState("");
  const [category, setCategory] = useState("");
  const [location, setLocation] = useState("");
  const [thumbnail, setThumbnail] = useState("");
  const [video, setVideo] = useState("");

  //  Creating a ref for thumbnail and video
  const thumbnailRef = useRef();
  const videoRef = useRef();

  return (
    <div className="w-full h-screen bg-[#1a1c1f] flex flex-row">
      <div className="flex-1 flex flex-col">
        <div className="mt-5 mr-10 flex  justify-end">
          <div className="flex items-center">
            <button className="bg-transparent  text-[#9CA3AF] py-2 px-6 border rounded-lg  border-gray-600  mr-6">
              Discard
            </button>
            <button
              onClick={() => {
                handleSubmit();
              }}
              className="bg-blue-500 hover:bg-blue-700 text-white  py-2  rounded-lg flex px-4 justify-between flex-row items-center"
            >
              <BiCloud />
              <p className="ml-2">Upload</p>
            </button>
          </div>
        </div>
        <div className="flex flex-col m-10     mt-5  lg:flex-row">
          <div className="flex lg:w-3/4 flex-col ">
            <label className="text-[#9CA3AF]  text-sm">Title</label>
            <input
              value={title}
              onChange={(e) => setTitle(e.target.value)}
              placeholder="Rick Astley - Never Gonna Give You Up (Official Music Video)"
              className="w-[90%] text-white placeholder:text-gray-600  rounded-md mt-2 h-12 p-2 border  bg-[#1a1c1f] border-[#444752] focus:outline-none"
            />
            <label className="text-[#9CA3AF] mt-10">Description</label>
            <textarea
              value={description}
              onChange={(e) => setDescription(e.target.value)}
              placeholder="Never Gonna Give You Up was a global smash on its release in July 1987, topping the charts in 25 countries including Rick’s native UK and the US Billboard Hot 100.  It also won the Brit Award for Best single in 1988. Stock Aitken and Waterman wrote and produced the track which was the lead-off single and lead track from Rick’s debut LP “Whenever You Need Somebody."
              className="w-[90%] text-white h-32 placeholder:text-gray-600  rounded-md mt-2 p-2 border  bg-[#1a1c1f] border-[#444752] focus:outline-none"
            />

            <div className="flex flex-row mt-10 w-[90%]  justify-between">
              <div className="flex flex-col w-2/5    ">
                <label className="text-[#9CA3AF]  text-sm">Location</label>
                <input
                  value={location}
                  onChange={(e) => setLocation(e.target.value)}
                  type="text"
                  placeholder="Bali - Indonesia"
                  className="w-[90%] text-white placeholder:text-gray-600  rounded-md mt-2 h-12 p-2 border  bg-[#1a1c1f] border-[#444752] focus:outline-none"
                />
              </div>
              <div className="flex flex-col w-2/5    ">
                <label className="text-[#9CA3AF]  text-sm">Category</label>
                <select
                  value={category}
                  onChange={(e) => setCategory(e.target.value)}
                  className="w-[90%] text-white placeholder:text-gray-600  rounded-md mt-2 h-12 p-2 border  bg-[#1a1c1f] border-[#444752] focus:outline-none"
                >
                  <option>Music</option>
                  <option>Sports</option>
                  <option>Gaming</option>
                  <option>News</option>
                  <option>Entertainment</option>
                  <option>Education</option>
                  <option>Science & Technology</option>
                  <option>Travel</option>
                  <option>Other</option>
                </select>
              </div>
            </div>
            <label className="text-[#9CA3AF]  mt-10 text-sm">Thumbnail</label>

            <div
              onClick={() => {
                thumbnailRef.current.click();
              }}
              className="border-2 w-64 border-gray-600  border-dashed rounded-md mt-2 p-2  h-36 items-center justify-center flex"
            >
              {thumbnail ? (
                <img
                  onClick={() => {
                    thumbnailRef.current.click();
                  }}
                  src={URL.createObjectURL(thumbnail)}
                  alt="thumbnail"
                  className="h-full rounded-md"
                />
              ) : (
                <BiPlus size={40} color="gray" />
              )}
            </div>

            <input
              type="file"
              className="hidden"
              ref={thumbnailRef}
              onChange={(e) => {
                setThumbnail(e.target.files[0]);
              }}
            />
          </div>

          <div
            onClick={() => {
              videoRef.current.click();
            }}
            className={
              video
                ? " w-96   rounded-md  h-64 items-center justify-center flex"
                : "border-2 border-gray-600  w-96 border-dashed rounded-md mt-8   h-64 items-center justify-center flex"
            }
          >
            {video ? (
              <video
                controls
                src={URL.createObjectURL(video)}
                className="h-full rounded-md"
              />
            ) : (
              <p className="text-[#9CA3AF]">Upload Video</p>
            )}
          </div>
        </div>
        <input
          type="file"
          className="hidden"
          ref={videoRef}
          accept={"video/*"}
          onChange={(e) => {
            setVideo(e.target.files[0]);
            console.log(e.target.files[0]);
          }}
        />
      </div>
    </div>
  );
}

如果您导航到 ,您应该会看到类似的屏幕http://localhost:3000/upload

这是一个基本的上传页面,现在,我们只有输入并将它们的值保存在状态中。

在处理句柄提交功能之前,创建一个名为的新文件夹utils,并在其中创建一个名为getContract. 该文件将用于在上传页面上与您的合约进行交互。将以下代码添加到其中,并确保将合约地址替换为您的合约地址。

import ContractAbi from "../artifacts/contracts/YouTube.sol/YouTube.json";
import { ethers } from "ethers";

export default function getContract() {
  // Creating a new provider
  const provider = new ethers.providers.Web3Provider(window.ethereum);
  // Getting the signer
  const signer = provider.getSigner();
  // Creating a new contract factory with the signer, address and ABI
  let contract = new ethers.Contract(
    "0xf6F03b0837569eec33e0Af7f3F43B362916e5de1",
    ContractAbi.abi,
    signer
  );
  // Returning the contract
  return contract;
}

现在我们需要一个 IPFS 客户端来上传视频和缩略图。有许多提供 IPFS 服务的服务,您可以在代码下方注册并粘贴您的 IPFS URL。

回到上传页面(pages/upload/index.js),我们首先创建一个 IPFS 客户端来上传视频和缩略图。

  const client = create("YOU_IPFS_CLIENT_LINK_HERE");

现在让我们在上传页面中声明 4 个函数。

  // When user clicks on the upload button
  const handleSubmit = async () => {
    // Checking if user has filled all the fields
    if (
      title === "" ||
      description === "" ||
      category === "" ||
      location === "" ||
      thumbnail === "" ||
      video === ""
    ) {
      // If user has not filled all the fields, throw an error
      alert("Please fill all the fields");
      return;
    }
    // If user has filled all the fields, upload the thumbnail to IPFS
    uploadThumbnail(thumbnail);
  };

  const uploadThumbnail = async (thumbnail) => {
    try {
      // Uploading the thumbnail to IPFS
      const added = await client.add(thumbnail);
      // Getting the hash of the uploaded thumbnail and passing it to the uploadVideo function
      uploadVideo(added.path);
    } catch (error) {
      console.log("Error uploading file: ", error);
    }
  };

  const uploadVideo = async (thumbnail) => {
    try {
      // Uploading the video to IPFS
      const added = await client.add(video);
      // Getting the hash of the uploaded video and passing both video and thumbnail to the saveVideo function
      await saveVideo(added.path, thumbnail);
    } catch (error) {
      console.log("Error uploading file: ", error);
    }
  };

  const saveVideo = async (video, thumbnail) => {
    // Get the contract from the getContract function
    let contract = await getContract();
    // Get todays date
    let UploadedDate = String(new Date());
    // Upload the video to the contract
    await contract.uploadVideo(
      video,
      title,
      description,
      location,
      category,
      thumbnail,
      UploadedDate
    );
  };

我已经对代码的每一行进行了注释,以便您了解发生了什么。

保存文件并 BOOM!!我们完成了上传功能。您现在应该可以将视频上传到合同。

连接图表

为了从 The Graph 中获取视频,我们需要设置一个 graphQL 客户端。在根目录中创建一个名为的新文件client.js,并在其中添加以下代码。

import { ApolloClient, InMemoryCache } from "@apollo/client";

const client = new ApolloClient({
uri: "YOUR_GRAPHQL_URL_HERE",
cache: new InMemoryCache(),
});

export default client;

确保将 URI 替换为您的图形 URL。并将页面目录中的代码替换_app.js为以下代码。

import { ApolloProvider } from "@apollo/client";
import client from "../client";
import "../styles/globals.css";

function MyApp({ Component, pageProps }) {
  return (
    <ApolloProvider client={client}>
      <Component {...pageProps} />
    </ApolloProvider>
  );
}

export default MyApp;

在上面的代码中,我们已经包装了我们的代码,ApolloProvider并提供了我们之前创建的客户端作为道具。

从区块链获取视频

index.js在名为home. 现在您可以将以下代码添加到文件中。

import React, { useEffect, useState } from "react";
import { useApolloClient, gql } from "@apollo/client";

export default function Main() {
  // Creating a state to store the uploaded video
  const [videos, setVideos] = useState([]);

  // Get the client from the useApolloClient hook
  const client = useApolloClient();

  // Query the videos from the the graph
  const GET_VIDEOS = gql`
    query videos(
      $first: Int
      $skip: Int
      $orderBy: Video_orderBy
      $orderDirection: OrderDirection
      $where: Video_filter
    ) {
      videos(
        first: $first
        skip: $skip
        orderBy: $orderBy
        orderDirection: $orderDirection
        where: $where
      ) {
        id
        hash
        title
        description
        location
        category
        thumbnailHash
        isAudio
        date
        author
        createdAt
      }
    }
  `;

  // Function to get the videos from the graph
  const getVideos = async () => {
    // Query the videos from the graph
    client
      .query({
        query: GET_VIDEOS,
        variables: {
          first: 200,
          skip: 0,
          orderBy: "createdAt",
          orderDirection: "desc",
        },
        fetchPolicy: "network-only",
      })
      .then(({ data }) => {
        // Set the videos to the state
        setVideos(data.videos);
      })
      .catch((err) => {
        alert("Something went wrong. please try again.!", err.message);
      });
  };

  useEffect(() => {
    // Runs the function getVideos when the component is mounted
    getVideos();
  }, []);
  return (
    <div className="w-full bg-[#1a1c1f] flex flex-row">
      <div className="flex-1 h-screen flex flex-col">
        <div className="flex flex-row flex-wrap">
          {videos.map((video) => (
            <div className="w-80">
              <p>{video.title}</p>
            </div>
          ))}
        </div>
      </div>
    </div>
  );
}

保存文件,您应该会看到类似的输出。

正如你现在所看到的,我们只是在获取视频标题。因此,让我们创建一个可重用的组件来很好地显示视频。

确保上传一些视频,以便您可以看到上面的输出

创建一个名为 的文件夹components,然后在Video.js其中创建一个名为的新文件。在文件中添加以下代码。它是一个非常基本的视频组件。

import React from "react";
import { BiCheck } from "react-icons/bi";
import moment from "moment";

export default function Video({ horizontal, video }) {
  return (
    <div
      className={`${
        horizontal
          ? "flex flex-row mx-5 mb-5  item-center justify-center"
          : "flex flex-col m-5"
      } `}
    >
      <img
        className={
          horizontal
            ? "object-cover rounded-lg w-60  "
            : "object-cover rounded-lg w-full h-40"
        }
        src={`https://ipfs.io/ipfs/${video.thumbnailHash}`}
        alt=""
      />
      <div className={horizontal && "ml-3  w-80"}>
        <h4 className="text-md font-bold dark:text-white mt-3">
          {video.title}
        </h4>
        <p className="text-sm flex items-center text-[#878787] mt-1">
          {video.category + " • " + moment(video.createdAt * 1000).fromNow()}
        </p>
        <p className="text-sm flex items-center text-[#878787] mt-1">
          {video?.author?.slice(0, 9)}...{" "}
          <BiCheck size="20px" color="green" className="ml-1" />
        </p>
      </div>
    </div>
  );
}

将 Video 组件导入到 home 文件中,并将 map 函数替换为以下代码。

{videos.map((video) => (
        <div 
            className="w-80"
            onClick={() => {
                // Navigation to the video screen (which we will create later)
                window.location.href = `/video?id=${video.id}`;
       }}
            >
                <Video video={video} />
        </div>
))}

保存文件,现在您应该会看到一个漂亮的主页,类似于下图。

视频页面

现在我们可以在主屏幕中获取视频了。让我们在用户点击任何视频组件时将被重定向的视频页面上工作。

在名为 components 的文件夹中创建一个新文件,Player并将以下代码添加到其中。我们正在使用react plyr创建一个视频播放器组件。

import Plyr from "plyr-react";
import "plyr-react/plyr.css";

export default function Player({ hash }) {
  let url = `https://ipfs.io/ipfs/${hash}`;
  return (
    <Plyr
      source={{
        type: "video",
        title: "Example title",
        sources: [
          {
            src: url,
            type: "video/mp4",
          },
        ],
      }}
      options={{
        autoplay: true,
      }}
      autoPlay={true}
    />
  );
}

在同一目录中创建另一个名为 VideoContainer. 将此组件想象为 youtube 视频页面的左侧,其中包含播放器、视频标题、上传日期和说明。将以下代码添加到文件中。

import React from "react";
import Player from "./Player";

export default function VideoComponent({ video }) {
  return (
    <div>
      <Player hash={video.hash} />
      <div className="flex justify-between flex-row py-4">
        <div>
          <h3 className="text-2xl dark:text-white">{video.title}</h3>
          <p className="text-gray-500 mt-1">
            {video.category} •{" "}
            {new Date(video.createdAt * 1000).toLocaleString("en-IN")}
          </p>
        </div>
      </div>
    </div>
  );
}

最后在 pages 文件夹中创建一个名为 video 的新文件夹并创建一个新文件index.js。

现在,您可以将以下代码添加到文件中。

import React, { useEffect, useState } from "react";
import { useApolloClient, gql } from "@apollo/client";
import Video from "../../components/Video";
import VideoComponent from "../../components/VideoContainer";

export default function VideoPage() {
  const [video, setVideo] = useState(null);
  const [relatedVideos, setRelatedVideos] = useState([]);

  const client = useApolloClient();
  const getUrlVars = () => {
    var vars = {};
    var parts = window.location.href.replace(
      /[?&]+([^=&]+)=([^&]*)/gi,
      function (m, key, value) {
        vars[key] = value;
      }
    );
    return vars;
  };

  const GET_VIDEOS = gql`
    query videos(
      $first: Int
      $skip: Int
      $orderBy: Video_orderBy
      $orderDirection: OrderDirection
      $where: Video_filter
    ) {
      videos(
        first: $first
        skip: $skip
        orderBy: $orderBy
        orderDirection: $orderDirection
        where: $where
      ) {
        id
        hash
        title
        description
        location
        category
        thumbnailHash
        isAudio
        date
        author
        createdAt
      }
    }
  `;

  const getRelatedVideos = () => {
    client
      .query({
        query: GET_VIDEOS,
        variables: {
          first: 20,
          skip: 0,
          orderBy: "createdAt",
          orderDirection: "desc",
          where: {},
        },
        fetchPolicy: "network-only",
      })
      .then(({ data }) => {
        setRelatedVideos(data.videos);
        const video = data?.videos?.find(
          (video) => video.id === getUrlVars().id
        );
        setVideo(video);
      })
      .catch((err) => {
        alert("Something went wrong. please try again.!", err.message);
      });
  };

  useEffect(() => {
    getRelatedVideos();
  }, []);

  return (
    <div className="w-full   bg-[#1a1c1f]  flex flex-row">
      <div className="flex-1 flex flex-col">
        {video && (
          <div className="flex flex-col m-10 justify-between      lg:flex-row">
            <div className="lg:w-4/6 w-6/6">
              <VideoComponent video={video} />
            </div>
            <div className="w-2/6">
              <h4 className="text-md font-bold text-white ml-5 mb-3">
                Related Videos
              </h4>
              {relatedVideos.map((video) => (
                <div
                  onClick={() => {
                    setVideo(video);
                  }}
                  key={video.id}
                >
                  <Video video={video} horizontal={true} />
                </div>
              ))}
            </div>
          </div>
        )}
      </div>
    </div>
  );
}

保存文件并单击主屏幕上的任何视频。您应该被重定向到类似于以下页面的视频屏幕。

搜索功能

现在我们几乎完成了应用程序的功能。让我们也添加一个搜索功能。

在 components 文件夹中,创建一个名为Header.js. 现在,您可以添加以下代码。

import React from "react";
import { AiOutlinePlusCircle } from "react-icons/ai";

export const Header = ({ search }) => {
  return (
    <header className="w-full flex justify-between h-20 items-center border-b p-4 border-[#202229]">
      <div className=" w-1/3    ">
        <img
          width={80}
          src={"https://i.ibb.co/JHn1pjz/logo.png"}
          alt="YouTube Logo"
        />
      </div>
      <div className=" w-1/3 flex justify-center items-center">
        {search ? (
          <input
            type="text"
            onChange={(e) => search(e.target.value)}
            placeholder="Type to search"
            className=" border-0 bg-transparent focus:outline-none text-white"
          />
        ) : null}
      </div>
      <div className=" w-1/3 flex justify-end">
        <AiOutlinePlusCircle
          onClick={() => {
            window.location.href = "/upload";
          }}
          size="30px"
          className="mr-8 fill-whiteIcons dark:fill-white cursor-pointer"
        />
      </div>
    </header>
  );
};

这是一个非常简单的组件,分为 3 个部分。在左侧,我们有一个应用程序的徽标,在中间,我们声明了一个用户可以输入以进行搜索的输入,最后我们有一个图标,可以将用户导航到上传屏幕。

返回首页(pages/home/index.js)导入Header组件,在第73行后添加if

// <div className="flex-1 h-screen flex flex-col">
        <Header
          search={(e) => {
            console.log(e);
          }}
        />
// <div className="flex flex-row flex-wrap">

现在您应该在主页中看到一个标题组件。

在第 8 行之后的主页上声明一个新状态以捕获搜索屏幕中的值。

const [search, setSearch] = useState("");

您还可以更新 Header 组件以设置上述 useState 中输入的值。

<Header
    search={(e) => {
        setSearch(e);
    }}
 />

让我们也更新getVideos搜索视频的功能,以防状态中有一些价值。

const getVideos = async () => {
    // Query the videos from the graph
    client
      .query({
        query: GET_VIDEOS,
        variables: {
          first: 200,
          skip: 0,
          orderBy: "createdAt",
          orderDirection: "desc",
                    // NEW: Added where in order to search for videos
          where: {
            ...(search && {
              title_contains_nocase: search,
            }),
          },
        },
        fetchPolicy: "network-only",
      })
      .then(({ data }) => {
        // Set the videos to the state
        setVideos(data.videos);
      })
      .catch((err) => {
        alert("Something went wrong. please try again.!", err.message);
      });
  };

在上面的函数中,我们只是添加了一个where对象来搜索视频,以防状态中有值。

最后,更新 useEffect 函数以在搜索状态发生变化时也运行该函数。

useEffect(() => {
    // Runs the function getVideos when the component is mounted and also if there is a change in the search stae
        getVideos();
  }, [search]);

现在,如果您搜索任何内容,您应该会看到视频自动过滤。耶🎉

下一步是什么?

如果您已经走到这一步,则意味着您对构建 Web3 应用程序充满热情。如果您有兴趣,可以将以下一些其他功能/改进添加到应用程序中。

  • 允许用户根据视频类别搜索视频。
  • 尝试使用 Arweave 代替 IFPS,看看它是如何工作的。
  • 尝试向应用程序添加灯光模式并允许用户切换
  • 您还可以使应用程序响应

文章来源:https://blog.suhailkakar.com/building-a-full-stack-web3-youtube-clone-with-next-ipfs-the-graph-solidity-and-polygon

©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343

推荐阅读更多精彩内容