How to create a Youtube clone for web 3.0

Esteban Suárez

In this article you will have a step by step guide on how to build a YouTube clone that works on top of the Polygon blockchain. For this we will use Metamask, Node.js, IPFS, Next.js, Solidity, The Graph, among others, at the end you will obtain a functional website with an appearance similar to YouTube that will work on web 3.0.

Every day more people are moving towards Web 3.0, either because of its decentralized nature or because of the privacy it offers its users What is Web 3.0?.

Debido a esto la demanda de desarrolladores está aumentando y las habilidades en el desarrollo de blockchain se encuentran entre las más solicitadas en la industria tecnológica. In order to do this we will use:

  • Framework: Next.js
  • Smart contracts: Solidity
  • Ethereum web client library: Ethers.js
  • File storage: IPFS
  • Data query: The Graph
  • CSS: Tailwind CSS
  • Ethereum development environment: Hardhat
  • Layer 2 Blockchain: Polygon

System requirements

Before starting the tutorial, make sure you have Node.js v14 or higher, and the Metamask browser extension installed on your machine.

Next.js application configuration

The first step is to set up a next.js application How to install Next.js and install the necessary dependencies. To do that, you would need to run the following command in your terminal:

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

This command creates a new directory called web3-youtube, then navigates to that directory and creates a next.js app.

Once the project has been successfully created, run the following command to install some other dependencies.

 
npm install react-icons plyr-react moment ipfs-http-client ethers @apollo/client graphql dotenv
  • react-icons is a library of icons that we will use in the app.
  • plyr-react is a video player component with extensive plugins and functionality.
  • 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 a literary Ethereum client that will be used to interact with smart contracts.

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

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

Inicializar el entorno local de Ethereum

Next, it’s time to initialize a local smart contract build using Hardhat. To do that, just run the following command in your terminal.

 
npx hardhat

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

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

hardhat.config.js - This file contains the Hardhat configuration.

scripts – This folder contains a sample script to demonstrate the implementation of a smart contract.

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

Adding TailwindCSS

Tailwind CSS is a first-use CSS framework for building user interfaces quickly. We will use it to design our applications.

Run the following command to install tailwindcss and its dependencies.

 
npm install --dev tailwindcss postcss autoprefixer

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

 
npx tailwind init -p

The above command will generate two files called 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 following code.

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

Finally, add the Tailwind directives for each of the Tailwind layers to the ./styles/globals.css file.

 

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

You can also check if Tailwind CSS was integrated correctly by updating the code inside the pages/index.js file.

 
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 launch the next.js application and you should see a page similar to the following:

The smart contract

Now that the project setup is complete, we can start writing smart contracts for our app. In this article, I will use Solidity What is solidity.

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

In the contracts folder, create a new file called 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
        );
    }
}

Modifying Hardhat Settings

Now, we need to make some modifications to the Hardhat configuration file to implement our smart contract. Open hardhat.config.js in your code editor and update the module.exports object to the following 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",
  },
};

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

Then click “Export Private Key”. You will be prompted to enter your Metamask password. Enter your password and click Confirm.

You should see your private key inside a red box.

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

 
PRIVATE_KEY="YOUR_METAMASK_PRIVATE_KEY"

Never share your private key. Anyone who has your private keys can steal any assets you have in your account.

Compiling smart contracts with Hardhat

Now that our smart contract is complete, let’s go ahead and compile them. You can compile it using the following command.

 

npx hardhat compile

If you have encountered the error HH801: The plugin @nomicfoundation/hardhat-toolbox requires dependencies to be installed that were not found on the system, to do this you should proceed to execute the following command:

 

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, run the above build command again.

 

npx hardhat compile

After the build completes successfully, you should see a new directory called artifacts created in your projects directory.

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

Smart contract implementation in Polygon

Now, we can implement our smart contract using Polygon. First we must add the Polygon network in our Metamask wallet How to add Poligon to Metamask and then we will need the token native $MATIC to be able to implement a smart contract.

To do this, we are going to go to [Polygon Faucet] (https://faucet.polygon.technology/) and copy the address of our wallet there. In this way we will receive 0.2 MATIC, enough to be able to create our smart contract.

The next step would be to replace the code inside scripts/deploy.js with the following 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);
  });

As a final step we are going to execute our smart contract with the following command:

 

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

This command will take a moment to compile and once it is done, it will return a message like this to the screen:

 

YouTube deployed to: 0x0AE42f411420b2710474e5e4f2F551b36350F9D1

Configuring the Graph

You can use smart contract events with the help of packages like ethers.js or you can use The Graph (https://www.coinbase.com/en/price/the-graph) to query Blockchain data. The Graph is an off-chain indexing solution that can help you query data in a much easier way.

In this tutorial, we will use The Graph to query the blockchain videos, because it makes it very easy and uses the GraphQL query language.

Create a subgraph

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

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

 

npm install -g @graphprotocol/graph-cli

When it is finally installed you must run run graph init in order to initialize the project subgraph. At that time you will be asked some questions. You can answer them using the data in the following code.

 

 Protocol · ethereum
 Product for which to initialize · hosted-service
 Subgraph name · satoshi/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) · /home/satoshi/web3-youtube/frontend/artifacts/contracts/Youtube.sol/YouTube.json
 Contract Name · YouTube
 Add another contract? (y/N) · false

Note that you can change the name of the subgraph and the address of the ABI path.

The next step we need to take is to declare the schema of our application. To do this you are going to go to the schema.graphql file inside the index directory with the following 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 you need to find the you-tube.ts file and replace it with the following 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();
}

Now navigate to the index directory and run the code to generate the GraphqL code and schema.

Creating the subgraph

Before executing the subgraph we need to build it, to do that you just have to execute the following code through the terminal:

 

yarn build

Now in order to run our subgraph we need to create an account on The Graph. Go to The Graph website and create a new account, log in and click Add Subgraph .

In the next window you will need to enter the essential data about the subgraph you are creating.

Once the subgraph is created, you’ll need to copy the access token, as we’d need it later. On your terminal, run graphical authentication and choose the hosted service. In the deployment key, paste the key you copied earlier (access token), Lastly, run the following command to deploy your subgraph.

 

yarn deploy

and if you have carried out all the steps so far as described in this article, you will get a message like this on the screen:

 

Build completed: QmV19RJaCXCcKKBe3BTyrL8cGqKNaEo9kpwxMTgrPnDKYA

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

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

Interface

Now that we’ve completed the smart contracts, it’s time to work on the front-end of the app. Let’s start with app authentication.

Autenticación

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

Delete everything inside index.js in the pages directory and import the contents of the landing file into index.js. You should get something like this in index.js.

 

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

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

Now on the landing page we are going to create a component that allows us to connect the wallets of the users who use our application, add the following code:

 

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 has gone well up to this step, you will obtain an image like the one shown here:

uploading videos

Now that users can connect their wallets it’s time to add video posting functionality to our app.

To do this, create a new folder inside pages called upload and inside it they will create a file called 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>
  );
}

Now you might see a screen similar to this if you navigate to http://localhost:3000/upload.

This is a basic file upload page, for now you only have the option to save the basic files and data.

Before working on the file upload, create a new folder called utils and inside it create a file called getContract, that file will be used to interact with the smart contract on the page. the climb. To do this add the code below and note that you have to replace the address of the smart contract with the one of the contract you just created.

 

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 the videos and their thumbnails. There are many services that offer IPFS, you can create a user and paste the url of your IPFS node below.

Go back to (pages/upload/index.js), first create the IPFS client to upload the videos and thumbnails.

 
 const client = create("YOU_IPFS_CLIENT_LINK_HERE");

Ahora vamos a declarar estas 4 funciones en la página upload.

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

Save the changes to the file and you will have the page to upload the videos to the smart contract working.

Connecting with The Graph

In order to capture the videos from The Graph we need to configure the graphQL client, for this we are going to create a file called client.js in the root and add the code inside it.

 

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

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

export default client;

Replace the URI with the url of your graph. You should also replace the code inside _app.js inside the page folder with the following 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;

In the above code we have copied our code with ApolloProvider and it provides the client we created earlier as a property.

Get the videos from the Blockchain

Create a new file called index.js inside a new folder called home and add the following code.

 

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

Now you will get a result like the one shown in this image.

As you can see, it is something very basic, for the moment you can only get the title of the video, so we are going to create a reusable component that allows us to see these results in a more pleasant way.

For them create a folder called components and then create a file called Video.js inside it and copy the following code.

 

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 videos component to the home file and replace the map function with the code below.

 

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

When saving the file you will find a new home page that will be very similar to the one shown below.

Video Page

Now that we can find the videos in the main window we need to work on the page where users will be redirected when clicking on one of those components.

Create a new file in the components folder called Player and add the following code. For this we will use react plyr to create a component to play the videos.

 

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 folder called VideoContainer, with this we will be creating what in Youtube would be the window that appears on the left side of the videos that contain the Title, the date of upload, the description of the video .

Now, you can add this code:

 

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

Finally create a folder called video inside the pages folder and create a new file inside it called index.js, then copy the following 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>
  );
}

Save the file and by selecting or choosing any of the videos on the main page you will be redirected to a screen similar to this:

Search functionality

Up to this point we have almost completed the functionalities of our application, now we must add the search functionality and for this inside the folder components the file 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>
  );
};

This is a simple component that is divided into 3 parts, On the left side we will have the logo of our application, in the middle we declare an input where users can type and search and at the end we will have an icon that takes the user to the page of file upload. We return to the main page (pages/home/index.js) you must import the component Header add from line 73.

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

Now you can see this component at the top of our page like this:

Declare a new state on the main page after line 8 to capture the value returned on the search page.

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

You can also update the Header component to write the input values into the **usesState variable.

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

Now we need to update the getVideos function to fetch the videos in case any value is returned in the state variable.

 

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 function we have added an object to search for videos that are in the state variable.

Finally we are going to update the function useEffect so that it is executed every time there is a change in the search.

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

Whats Next?

If you have come this far it means that you are passionate about new technologies and about building Web 3.0 and here you can find some functions that you can add to your application:

  1. Allow users to search for videos based on categories.
  2. Try to use Arweave to compare the result with respect to the IPFS.
  3. Try to add dark mode and light mode with a trigger for users.
  4. Improve the application.

Conclusions

This article shows how to create an application similar to youtube but decentralized from the use of IPFS and the Polygon network.