Building a Full Stack Web3 YouTube Clone with Next, IPFS, The Graph, Solidity, and Polygon

Suhail Kakar - Sep 13 '22 - - Dev Community

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 v14 or greater, and the Metamask 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.

mkdir web3-youtube && cd web3-youtube && npx create-next-app .
Enter fullscreen mode Exit fullscreen mode

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.

npm install react-icons plyr-react moment ipfs-http-client ethers @apollo/client graphql dotenv
Enter fullscreen mode Exit fullscreen mode
  • 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

npm install --dev tailwindcss postcss autoprefixer
Enter fullscreen mode Exit fullscreen mode

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

npx tailwind init -p
Enter fullscreen mode Exit fullscreen mode

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.

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

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

@tailwind base;
@tailwind components;
@tailwind utilities;
Enter fullscreen mode Exit fullscreen mode

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

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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

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.

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

Enter fullscreen mode Exit fullscreen mode

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.

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",
  },
};
Enter fullscreen mode Exit fullscreen mode

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

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

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

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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

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"
Enter fullscreen mode Exit fullscreen mode

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/ 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

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/>
Enter fullscreen mode Exit fullscreen mode

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

https://cdn.hashnode.com/res/hashnode/image/upload/v1661684779491/S4umAe0tq.png

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

// 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);
  });
Enter fullscreen mode Exit fullscreen mode

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

npx hardhat run scripts/deploy.js --network mumbai
Enter fullscreen mode Exit fullscreen mode

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

YouTube deployed to: 0x0AE42f411420b2710474e5e4f2F551b36350F9D1
Enter fullscreen mode Exit fullscreen mode

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.

npm install -g @graphprotocol/graph-cli
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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. Click on Add Subgraph button.

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

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
Enter fullscreen mode Exit fullscreen mode

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

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
Enter fullscreen mode Exit fullscreen mode

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.

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

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

Enter fullscreen mode Exit fullscreen mode

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.

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;

Enter fullscreen mode Exit fullscreen mode

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

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.

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

Enter fullscreen mode Exit fullscreen mode

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

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.

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

Enter fullscreen mode Exit fullscreen mode

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.

  const client = create("YOU_IPFS_CLIENT_LINK_HERE");
Enter fullscreen mode Exit fullscreen mode

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

  // 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
    );
  };
Enter fullscreen mode Exit fullscreen mode

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

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.

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

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

export default client;
Enter fullscreen mode Exit fullscreen mode

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.

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;
Enter fullscreen mode Exit fullscreen mode

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.

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

Save the file and you should see a similar output.

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.

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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

{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>
))}
Enter fullscreen mode Exit fullscreen mode

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

Untitled.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.

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

Enter fullscreen mode Exit fullscreen mode

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.

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

Enter fullscreen mode Exit fullscreen mode

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.

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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

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.

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>
  );
};
Enter fullscreen mode Exit fullscreen mode

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

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

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

Untitled 2.png

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

const [search, setSearch] = useState("");
Enter fullscreen mode Exit fullscreen mode

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

<Header
    search={(e) => {
        setSearch(e);
    }}
 />
Enter fullscreen mode Exit fullscreen mode

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

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);
      });
  };
Enter fullscreen mode Exit fullscreen mode

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.

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

And now if you search anything, you should see the videos automatically filters. Yayy 🎉

Untitled 3.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.

Let's connect on Twitter and LinkedIn.

👋 Thanks for reading, See you next time

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .