每天都有越来越多的人过渡到 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,看看它是如何工作的。
- 尝试向应用程序添加灯光模式并允许用户切换
- 您还可以使应用程序响应