Building a Full Stack Web3 YouTube Clone with Next, IPFS, The Graph, Solidity, and Polygon
Tags: web3, javascript, webdev
Published: 2022-09-13 09:25:24

Every day more and more people are transiting to Web3. The demand for developers is increasing and skills in blockchain development are among the most in-demand in the tech industry.

The greatest approach to improving your Web3 skills is to use them to create projects. In this article, you are going to build a full-stack YouTube clone on top of the Polygon blockchain using the below tech stack.

- Frontend framework: Next.js
- Smart contracts: Solidity
- Ethereum web client library: Ethers.js
- File storage: IPFS
- Querying data: The Graph
- CSS Framework: TailwindCSS
- Ethereum development environment: Hardhat
- Layer 2 blockchain: Polygon

## Prerequisites

Before you start with the tutorial make sure you have [Node.js](https://nodejs.org/en/) v14 or greater, and the [Metamask](https://metamask.io/download/) browser extension installed on your machine.

## Setting up Next.js App

The first step is to set up a next.js app and install the required dependencies. In order to do that, you would need to run the below command in your terminal.

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

The following command creates a new directory named `web3-youtube`, then navigates to that directory and creates a next.js app.

Once the project is created successfully, run the following command to install a few other dependencies.

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

- `react-icons` is an icon library that we will be using in the app.
- `plyr-react` is a video player component with rich plugins and functionalities.
- `moment` is a JavaScript date library for parsing, validating, manipulating, and formatting dates.
- `ipfs-http-client` is used to upload videos and thumbnails to IPFS.
- `ethers` is an Ethereum client literary that will be used to interact with the smart contracts

You can also run the below command to install Hardhat as a dev dependency in your project.

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

### Initialize local Ethereum environment

Next, it is time to initialize a local smart contract development using Hardhat. To do that, simply run the below command in your terminal.

```
npx hardhat
```

The above command will scaffold the basic Solidity development environment. You should see below the new files/folders generated in your project directory.

`test`: This folder contains a test script written in Chai and it is used to test the smart contract.

`hardhat.config.js`: This file contains the configuration for Hardhat.

`scripts`: This folder contains a sample script to show to deploy a smart contract.

`contracts`: This is the folder that includes the files in which we write our smart contract code.

### Adding TailwindCSS

Tailwind CSS is a utility-first CSS framework for building user interfaces rapidly. We will be using it for styling our applications. Run the below command to install tailwindcss and its dependencies.

```bash
npm install --dev tailwindcss postcss autoprefixer
```

Once the dependencies are installed, we need to initiate the Tailwind CSS. To do that, run the below code in your terminal.

```bash
npx tailwind init -p
```

The above command will generate two files named `tailwind.config.js` and `postcss.config.js`. Next, open the project in any code editor and replace the code inside `tailwind.config.js` with the below code.

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

At last, add the tailwind directives for each of Tailwind’s layers to the `./styles/globals.css` file.

```css
@tailwind base;
@tailwind components;
@tailwind utilities;
```

You can also check if Tailwind CSS is integrated successfully by updating the code inside of the `pages/index.js` file.

```jsx
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>
  );
}
```

Save the file and run `npm run dev` to start the next.js app and you should see a similar page.

![https://t7271546.p.clickup-attachments.com/t7271546/4eb47ccb-85c6-4606-8c06-7c2026709398/asdas.png](https://t7271546.p.clickup-attachments.com/t7271546/4eb47ccb-85c6-4606-8c06-7c2026709398/asdas.png)

## The smart contract

Now that the project setup is completed, we can start writing smart contracts for our application. In this article, I will be using Solidity.

> A smart contract is a decentralized program that responds to events by executing business logic.
> 

In the contracts folder, create a new file named `Youtube.sol` and add the following code to it.

```solidity
//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
        );
    }
}

```

### Modifying the Hardhat configurations

Now, we need to do some modifications to the Hardhat configuration file in order to deploy our smart contract. Open  `hardhat.config.js` in your code editor and update the module.exports object to the below code.

```jsx
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",
  },
};
```

To deploy our contract, we need a private key. Open Metamask in your browser and click on three top right and choose account details.

![https://cdn.hashnode.com/res/hashnode/image/upload/v1661683125127/zqJIH22Ps.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1661683125127/zqJIH22Ps.png)

Then, click on "Export Private Key". You will be prompted to enter your Metamask password. Enter your password and click on Confirm.

![https://cdn.hashnode.com/res/hashnode/image/upload/v1661683261027/74sZqhCme.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1661683261027/74sZqhCme.png)

You should see your private key inside a box in red color.

![https://cdn.hashnode.com/res/hashnode/image/upload/v1661683484847/inCLsS82j.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1661683484847/inCLsS82j.png)

Create a `.env` file in the projects root directory and add your private key.

```
PRIVATE_KEY="YOUR_METAMASK_PRIVATE_KEY"
```

> Never ever share your private key. Anyone with your private keys can steal any assets held in your account.
> 

### Compiling smart contracts with Hardhat

Now that our smart contract is completed, let's go ahead and compile them. You can compile it using the command below.

```
npx hardhat compile
```

if you have encountered Error HH801: `Plugin @nomicfoundation/hardhat-toolbox requires the following dependencies to be installed`. Run the below command to install hardhat dependencies

```bash
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"
```

Once the package is installed, re-run the above compile command.

After the compile successfully completes, you should see a new directory named `artifacts` created in your projects directory. 

Artifacts contain the compiled version of our smart contract in JSON format. This JSON file contains an array called ABI. ABI or Application Binary Interface is what we need to connect our client (Next app) with our compiled smart contract.

### Deploying smart contract on Polygon

Now, we can deploy our smart contract on a Polygon Mumbai. We have already added RPC and Metamask private keys, so we don't need to do it again. However, you need some $MATIC in order to deploy a smart contract.

Navigate to [https://faucet.polygon.technology/](https://faucet.polygon.technology/) and paste your wallet address. Click on confirm and you should receive 0.2 MATIC in your wallet.

![https://cdn.hashnode.com/res/hashnode/image/upload/v1661684544164/U-mHSx1lS.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1661684544164/U-mHSx1lS.png)

By default, Metamask doesn't have Polygon blockchain in the list of networks, so we need to add it manually. Go to Metamask settings and choose to Add a network manually. Use the below information to add Polygon Mumbai to the 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/>
```

Save it and you should see 0.2 MATIC on your Metamask wallet.

![https://cdn.hashnode.com/res/hashnode/image/upload/v1661684779491/S4umAe0tq.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1661684779491/S4umAe0tq.png)

Next, replace the code inside `scripts/deploy.js` with the below code.

```jsx
// 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);
  });
```

At last, run the below command to deploy your smart contract.

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

This command will take some time, but once it is completed, you should see a message similar to this:

```
YouTube deployed to: 0x0AE42f411420b2710474e5e4f2F551b36350F9D1
```

And that means our contract was successfully deployed 🎉

## Setting up The Graph

You can either use smart contract events with help of packages like ethers.js or you can use The Graph to query data from Blockchain. The Graph is an off-chain indexing solution that can help you query data in a much easier way.

In this tutorial, we will be using The Graph to query the videos from the blockchain, because it makes it so easy and it uses GraphQL query language.

### Creating a Subgraph

A subgraph extracts data from a blockchain, processing it and storing it so that it can be easily queried via GraphQL.

To create a subgraph, you first need to install The Graph CLI. The Graph CLI is written in JavaScript, and you will need to install either yarn or npm to use it. You can run the below command to install it.

```bash
npm install -g @graphprotocol/graph-cli
```

Once installed, run `graph init` in order to initialize the subgraph in the project. You will be prompted with some questions. You can follow the below code for the answers:

```
✔ 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
```

Make sure to update the contract address, name, and ABI.

Next, let’s declare the schema for our application. Replace the code inside of `schema.graphql` inside of the indexer directory with the below code.

```
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
}
```

Now, replace the code inside of `you-tube.ts` with the below code.

```
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();
}

```

Navigate to the indexer directory and run `yarn codegen` to generate code from your GraphQL operations and schema.

### Building Subgraph

Before we deploy the subgraph, we need to build it. To do that simply run the below command in your terminal.

```
yarn build
```

Next, in order to deploy our subgraph, we need to create an account on The Graph.

### Deploying Subgraph

Go ahead and create an account and then navigate to [https://thegraph.com/hosted-service/dashboard](https://thegraph.com/hosted-service/dashboard). Click on Add Subgraph button.

![https://cdn.hashnode.com/res/hashnode/image/upload/v1661706344789/jqaF1EVXm.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1661706344789/jqaF1EVXm.png)

On the next, screen fill in information related to your subgraph and create Subgraph Button at the bottom of the screen

![https://t7271546.p.clickup-attachments.com/t7271546/f40a7113-b2ab-49e3-9b35-5347f46b8ce1/XD6DmbE1m.png](https://t7271546.p.clickup-attachments.com/t7271546/f40a7113-b2ab-49e3-9b35-5347f46b8ce1/XD6DmbE1m.png)

Once the subgraph is created, copy its access token as we would need it later. In your terminal run `graph auth` and choose hosted service. In the deploy key, paste the key that you copied earlier.

At last, run the below command to deploy your subgraph.

```
yarn deploy
```

If everything goes fine, you should see your subgraph link similar to the below output. 🎉

```bash
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
```

## The Frontend

Now that we have completed smart contracts, it is time to work on the front end of the application.  Let’s start with the Authentication of the app. 

### Authentication

The first step is to set up authentication in our app that allows users to connect their wallets. Create a new folder named `landing` inside of the pages folder and create a new file inside of it named index.js. This file will have the code for the landing page in our application, which will also allow users to connect their wallets.

Erase everything inside of `index.js` in the page directory and inside import the `Landing` file to the file. Here is what your index.js file should look like.

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

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

```

Now, on the landing page, we will create a simple hero component with connect wallet button that will allow users to connect their wallets and access our application.

Add the below code to the landing page. I have already added comments so you can understand them properly.

```jsx
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;

```

If everything goes fine you should see a similar screen. You should also be able to connect your MetaMask wallet.

![https://cdn.hashnode.com/res/hashnode/image/upload/v1661941716345/AJGoGQEew.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1661941716345/AJGoGQEew.png)

### Uploading videos

Now that users are able to connect their wallets, it is time to add upload video functionality to our app.

Create a new folder in the pages directory named `upload` and add a file named  `index.js`. Inside of the file adds the below code. Again I have already added the comments on the code, so I hope that helps you to understand it.

```jsx
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>
  );
}

```

And you should see a similar screen if you navigate to [http://localhost:3000/upload](http://localhost:3000/upload).

![https://cdn.hashnode.com/res/hashnode/image/upload/v1661950450236/UF9styJpw.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1661950450236/UF9styJpw.png)

This is a basic upload page, for now, we just have the inputs and save their value of them in the state.

Before working on the handle submit function, create a new folder named `utils` and inside of it create a file named `getContract`. This file will be used to interact with your contract on the upload page. Add the below code to it and make sure to replace the contract address with your contract address. 

```jsx
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;
}

```

Now we need an IPFS client to upload videos and thumbnails. There are many services that offer IPFS service, you can signup and paste your IPFS URL below code.

Back to the upload page (`pages/upload/index.js`), let's first create an IPFS client to upload videos and thumbnails.

```jsx
  const client = create("YOU_IPFS_CLIENT_LINK_HERE");
```

Now let's declare 4 functions in the upload page.

```jsx
  // 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
    );
  };
```

I have already commented on each line of the code so you can understand what is going on.

Save the file and BOOM!!
We are done with the upload functionality. You should now be able to upload videos to the contract.

![https://cdn.hashnode.com/res/hashnode/image/upload/v1661952430514/BUSgXVGH9.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1661952430514/BUSgXVGH9.png)

### Connecting with The Graph

In order to fetch videos from The Graph, we need to set up a graphQL client. Create a new file named `client.js` in the root directory and add the following code inside of it.

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

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

export default client;
```

Make sure to replace the URI with your graph URL. And also replace the code inside of `_app.js` in the page directory with the below code.

```jsx
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;
```

In the above code, we have wrapped our code with `ApolloProvider` and provided the client which we created earlier as a prop. 

### Fetching videos from Blockchain

Create a new file named `index.js` inside of a new folder named `home`. And for now you can add the below code to the file.

```jsx
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>
  );
}
```

Save the file and you should see a similar output.

![https://cdn.hashnode.com/res/hashnode/image/upload/v1661952959457/31JZTlIzT.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1661952959457/31JZTlIzT.png)

As you can see for now we are just fetching the video title. So let's create a reusable component to display the videos nicely.

> Make sure to upload a few videos so you can see the above output

Create a folder named `components`, and then create a new file named `Video.js` inside of it. Add the below code the file. It is a very basic video component.

```jsx
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>
  );
}
```

Import the Video component to the home file and replace the map function with the below code.

```jsx
{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>
))}
```

Save the file and now you should see a nice-looking homepage, similar to below image.


![Untitled.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1662216234585/PiCmF_MwI.png)

### Video page

Now that we are able to fetch the videos in the home screen. Let's work on the video page where the user will be redirected if they click on any video component.

Create a new file in the components folder named `Player` and add the below code to it.
We are using `react plyr` to create a video player component.

```jsx
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}
    />
  );
}

```

Create another file in the same directory named  `VideoContainer` . Imagine this component as the left side of the youtube video page, which contains a player, video title, upload date, and description. Add the below code to the file. 

```jsx
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>
  );
}

```

At last create a new folder named video inside of pages folder and create a new file `index.js` of it. 

For now you can add the following code to the file. 

```jsx
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>
  );
}
```

Save the file and click on any videos on the home screen. You should be redirected to the video screen similar to the below page. 

![Untitled 1.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1662216213152/aBmEGvHak.png)

### Search Functionality

Now that we are almost completed with app’s functionality. Let’s also add a search functionality. 

In the components folder, create a new file named `Header.js`. For now, you can add the below code.

```jsx
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>
  );
};
```

This is a very simple component that is divided into 3 parts. On the left we have a logo of the application, on the middle we have declared a input that users can type to search and at the last we have a icon that navigates users to the upload screen. 

Back to the home page (`pages/home/index.js`) import the Header component and add if after line number 73

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

Now you should see a header component in the home page. 


![Untitled 2.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1662216197544/UkcDuYGN0.png)

Declare a new state on the home page after line 8 to capture the value in the search screen. 

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

You can also update the Header component to set the value of the input in the above useState. 

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

Let’s also update the `getVideos` function to search videos in case there is some value in the state. 

```jsx
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);
      });
  };
```

In the above function, we just added a `where` object to search for videos in case there is a value in the state. 

Finally, update the useEffect function to also run the function if there is a change in the search state. 

```jsx
useEffect(() => {
    // Runs the function getVideos when the component is mounted and also if there is a change in the search stae
	    getVideos();
  }, [search]);
```
And now if you search anything, you should see the videos automatically filters. Yayy 🎉

![Untitled 3.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1662216176149/qXYUoFJFK.png)



## What’s Next?

If you have come this far, it means that you have very passionate about building Web3 applications. here are a few other functionalities/improvements which you can add to the application if you are interested. 

- Allowing users to search for videos based on video category. (Check this repo, if you need a reference)
- Trying to use Arweave instead of IFPS and see how it works.
- Trying adding light mode to the application and allow users to toggle
- You can also make the application responsive

## Conclusion

That is it for this article. I hope you found this article useful, if you need any help please let me know in the comment section or [DM me on Twitter](https://twitter.com/SuhailKakar).

Let's connect on  [Twitter](https://twitter.com/suhailkakar)  and  [LinkedIn](https://linkedin.com/in/suhailkakar/).

👋 Thanks for reading, See you next time