Crea tu clon de Youtube en la Web 3.0

En este artículo vas a tener una guía paso a paso de como construir un clon de YouTube que funciona sobre la cadena de bloques de Polygon. Para ello utilizaremos Metamask, Node.js, IPFS, Next.js, Solidity.

Crea tu clon de Youtube en la Web 3.0
Your own Youtube Sample

En este artículo vas a tener una guía paso a paso de como construir un clon de YouTube que funciona sobre la cadena de bloques de Polygon. Para ello utilizaremos Metamask, Node.js, IPFS, Next.js, Solidity, The Graph, entre otras, al finalizar obtendrás un sitio web funcional con una apariencia similar al de Youtube que funcionará en la web 3.0.

Cada día son más las personas que están transitando hacia la Web 3.0, ya sea por su caracter descentralizado o por la privacidad que ofrece a sus usuarios ¿Qué es la 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.

Para poder realizar esto vamos a utilizar:

  • Framework: Next.js
  • Contratos inteligentes: Solidity
  • Biblioteca de cliente web Ethereum: Ethers.js
  • Almacenamiento de archivos: IPFS
  • Consulta de datos: The Graph
  • CSS: Tailwind CSS
  • Entorno de desarrollo de Ethereum: Hardhat
  • Blockchain de capa 2: Polygon

Requerimientos del sistema

Antes de comenzar con el tutorial, asegúrese de tener Node.js v14 o superior, y la extensión del navegador Metamask instalada en su máquina.

Configuración de la aplicación Next.js

El primer paso es configurar una aplicación next.js Cómo instalar Next.js e instalar las dependencias necesarias. Para hacer eso, necesitaría ejecutar el siguiente comando en su terminal:

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

Este comando crea un nuevo directorio llamado web3-youtube, luego navega a ese directorio y crea una aplicación next.js.

Una vez que el proyecto se haya creado correctamente, ejecute el siguiente comando para instalar algunas otras dependencias.

npm install react-icons plyr-react moment ipfs-http-client ethers @apollo/client graphql dotenv
  • react-icons es una biblioteca de iconos que usaremos en la aplicación.
  • plyr-react es un componente de reproductor de video con amplios complementos y funcionalidades.
  • moment es una biblioteca de fechas de JavaScript para analizar, validar, manipular y formatear fechas.
  • ipfs-http-client se usa para cargar videos y miniaturas a IPFS.
  • ethers es un cliente literario de Ethereum que se utilizará para interactuar con los contratos inteligentes.

También puede ejecutar el siguiente comando para instalar Hardhat como una dependencia de desarrollo en su proyecto.

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

Inicializar el entorno local de Ethereum

A continuación, es hora de inicializar un desarrollo de contrato inteligente local utilizando Hardhat. Para hacer eso, simplemente ejecute el siguiente comando en su terminal.

npx hardhat

El comando anterior creará un andamiaje en el entorno de desarrollo básico de Solidity. Debería ver debajo los nuevos archivos/carpetas generados en el directorio de su proyecto.

test: esta carpeta contiene un script de prueba escrito en Chai y se usa para probar el contrato inteligente.

hardhat.config.js: este archivo contiene la configuración de Hardhat.

scripts: esta carpeta contiene un script de muestra para mostrar la implementación de un contrato inteligente.

contracts: Esta es la carpeta que incluye los archivos en los que escribimos nuestro código de contrato inteligente.

Agregando TailwindCSS

Tailwind CSS es un framework CSS de primera utilidad para construir interfaces de usuario rápidamente. Lo usaremos para diseñar nuestras aplicaciones.

Ejecute el siguiente comando para instalar tailwindcss y sus dependencias.

npm install --dev tailwindcss postcss autoprefixer

Una vez instaladas las dependencias, debemos iniciar Tailwind CSS. Para hacer eso, ejecute el siguiente código en su terminal.

npx tailwind init -p

El comando anterior generará dos archivos llamados tailwind.config.js y postcss.config.js. A continuación, abra el proyecto en cualquier editor de código y reemplace el código dentro de tailwind.config.js con el siguiente código.

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

Por último, agregue las directivas de Tailwind para cada una de las capas de Tailwind al archivo ./styles/globals.css.

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

También puede verificar si Tailwind CSS se integró correctamente actualizando el código dentro del archivo pages/index.js.


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

Guarde el archivo y ejecute npm run dev para iniciar la aplicación next.js y debería ver una página similar a la siguiente:

yt_clon_web3_1-1

El contrato inteligente

Ahora que se completó la configuración del proyecto, podemos comenzar a escribir contratos inteligentes para nuestra aplicación. En este artículo, usaré Solidity Qué es solidity.

Un contrato inteligente es un programa descentralizado que responde a los eventos mediante la ejecución de la lógica empresarial.

En la carpeta de contratos, cree un nuevo archivo llamado Youtube.sol y agréguele el siguiente código.


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

Modificación de las configuraciones de Hardhat

Ahora, necesitamos hacer algunas modificaciones al archivo de configuración de Hardhat para implementar nuestro contrato inteligente. Abra hardhat.config.js en su editor de código y actualice el objeto module.exports al siguiente código.


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

Para implementar nuestro contrato, necesitamos una clave privada. Abra Metamask en su navegador y haga clic en los tres puntos que aparecen arriba a la derecha y elija los detalles de la cuenta.

Screenshot-2022-10-16-at-19-42-55-metamask_1.png--Imagen-AVIF-3542---2362-p-xeles----Escalado--27---

Luego, haga clic en "Exportar clave privada". Se le pedirá que ingrese su contraseña de Metamask. Ingrese su contraseña y haga clic en Confirmar.

Screenshot-2022-10-16-at-19-46-33-metamask2.png--Imagen-AVIF-3542---2362-p-xeles----Escalado--27---

Cree un archivo .env en el directorio raíz de proyectos y agregue su clave privada.

PRIVATE_KEY="YOUR_METAMASK_PRIVATE_KEY"

Nunca comparta su clave privada. Cualquiera que tenga sus claves privadas puede robar cualquier activo que tenga en su cuenta.

Compilando contratos inteligentes con Hardhat

Ahora que nuestro contrato inteligente está completo, avancemos y compilémoslos. Puede compilarlo usando el siguiente comando.


npx hardhat compile

si ha encontrado el error HH801: el complemento @nomicfoundation/hardhat-toolbox requiere que se instalen las dependencias que no se han encontrado en el sistema, para hacerlo debe proceder a ejecutar el siguiente comando:


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"


Una vez que el paquete esté instalado, vuelva a ejecutar el comando de compilación anterior.


npx hardhat compile

Después de que la compilación se complete con éxito, debería ver un nuevo directorio llamado artifacts creado en su directorio de proyectos.

Artifacts contienen la versión compilada de nuestro contrato inteligente en formato JSON Qué es Json. Este archivo JSON contiene una matriz llamada ABI o Application Binary Interface esto es lo que vamos a necesitar para conectar nuestro cliente (Next app) con nuestro contrato inteligente compilado.

Implementación de contrato inteligente en Polygon

Ahora, podemos implementar nuestro contrato inteligente utilizando Polygon. Primeramente debemos agregar la red Polygon en nuestra wallet Metamask Como agregar Poligon a Metamask y luego vamos a necesitar el token nativo $MATIC para poder implementar un contrato inteligente.

Para ello nos vamos a dirigir a Polygon Faucet y copiaremos ahí la dirección de nuestra billetera. De esta forma recibiremos 0.2 MATIC, suficiente para poder crear nuestro contrato inteligente.

{{< figure src="https://cloudflare-ipfs.com/ipfs/bafkreifzmrjqa44ofdvjyjs5vw7foaqeutmvdbeasc3qhyzob6rf2itu6u/polygon_faucet.png" title="" >}}

El siguiente paso sería reemplazr el código que se encuentra dentro de scripts/deploy.js con el código siguiente:


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


Como paso final vamos a ejecutar nuestro contrato inteligente con el siguiente comando:


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

Este comando demorará un momento en compilarse y una vez que ya esté realizado te devolverá en pantalla un mensaje como este:


YouTube deployed to: 0x0AE42f411420b2710474e5e4f2F551b36350F9D1

Configurando the Graph

Puede usar eventos de contratos inteligentes con la ayuda de paquetes como ethers.js o puede usar The Graph Que es The Graph para consultar datos de Blockchain. The Graph es una solución de indexación fuera de la cadena que puede ayudarlo a consultar datos de una manera mucho más fácil.

En este tutorial, usaremos The Graph para consultar los videos de la cadena de bloques, porque lo hace muy fácil y usa el lenguaje de consulta GraphQL.

Crear un subgrafo

Un subgrafo extrae datos de una cadena de bloques, los procesa y los almacena para que se puedan consultar fácilmente a través de GraphQL.

Para crear un subgráfico, primero debe instalar The Graph CLI. Graph CLI está escrito en JavaScript y deberá instalar yarn Que es yarn o npm Que es npm para usarlo.
Puede ejecutar el siguiente comando para instalarlo.


npm install -g @graphprotocol/graph-cli

Cuando este finalmente instalado debes ejecutar run graph init para poder inicializar el subgrafo del proyecto. En ese momento se le consultaran algunas preguntas. Puedes responderlas utilizando los datos del siguiente código.


✔ 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 que se puedes cambiar el nombre del subgrafo y la dirección del ABI path.

El siguiente paso que debemos seguir es declarar el schema de nuestra aplicación. Para ello vas ir al archivo schema.graphql dentro del directorio indice con el código siguiente:


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
}

Ahora debe buscar el archivo you-tube.ts y reemplazarlo con el siguiente código:


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

Navega ahora hacia el directorio indice y ejecuta el código para generar el código de GraphqL y el schema.

Creando el subgrafo

Antes de ejecutar el subgrafo necesitamos construirlo, para hacer eso solo debes ejecutar a través de la terminal  el siguiente código:


yarn build

Ahora para poder ejecutar nuestro subgrafo necesitamos crear una cuenta en The Graph.
Dirigete al sitio web de The Graph y cree una nueva cuenta, inicie sesión y de clic en Add Subgraph .

the_graph

En la siguiente ventana deberá introducir los datos esenciales sobre el subgrafo que está creando.

datos_the_graph

Una vez que se crea el subgrafo, deberás copiar el token de acceso, ya que lo necesitaríamos más adelante. En su terminal, ejecute la autenticación gráfica y elija el servicio alojado. En la clave de implementación, pegue la clave que copió anteriormente (token de acceso), Por último, ejecute el siguiente comando para implementar su subgráfico.

yarn deploy

y si todos los pasos los has realizando hasta ahora tal y como se ha descrito en este artículo obtendrás em pantalla un mensaje como este:


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


Interfaz

Ahora que hemos completado los contratos inteligentes, es hora de trabajar en el front-end de la aplicación. Comencemos con la autenticación de la aplicación.

Autenticación

El primer paso es configurar la autenticación en nuestra aplicación que permite a los usuarios conectar sus billeteras. Cree una nueva carpeta llamada landing dentro de la carpeta pages y cree un nuevo archivo dentro de ella llamado index.js. Este archivo tendrá el código de la página de inicio en nuestra aplicación, que también permitirá a los usuarios conectar sus billeteras.

Borre todo lo que hay dentro de index.js en el directorio pages e importe el contenido del archivo landing en dentro de index.js. Debería obtener algo como esto en index.js.


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

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


Ahora en la pagina landing vamos a crear un componente que permita realizar la conexión de las billeteras de los usuarios que utilicen nuestra aplicación, añade el siguiente código:


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;

Si todo ha salido bien hasta este paso vas a obtener una imagen como la que aquí te muestro:

conexion

Subiendo videos

Ahora que los usuarios pueden conectar sus billeteras es el momento de agregar la funcionalidad de publicar videos en nuestra aplicación.

Para ello crea una nueva carpeta dentro de pages que se llame upload y dentro de ella van a crear un archivo que se llame 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>
  );
}


Ahora podrías ver una pantalla similar a esta si navegas a http://localhost:3000/upload.

upload

Esta es una página básica para la subida de archivos, por ahora solo tiene la opción para salvar los archivos y los datos básicos.

Antes de de trabajar en la subida de archivos crea una carpeta nueva llamada utils y dentro de la misma crea un archivo llamado getContract, ese archivo va a ser utilizado para interactuar con el contrato inteligente en la página de la subida. Para ello añade el código que aparece más abajo y ten en cuenta que tienes que reemplazar la dirección del contrato inteligente por la del contrato que acabas de crear.


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

Ahora necesitamos un cliente IPFS para subir los videos y las miniaturas de los mismos. Existen muchos servicios que ofrecen IPFS, tu puedes crear usuario y pegar el url de tu nodo IPFS debajo.

Vuelve a (pages/upload/index.js), primero crea el cliente IPFS para subir los videos y las miniaturas.

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

Guarda los cambios en el archivo y tendrás la página para subir los videos al contrato inteligente funcionando.

upload_contract

Conectando con The Graph

Para poder capturar los videos desde The Graph necesitamos configurar el cliente graphQL, para ello vamos a crear un archivo que se llamará client.js en la raíz y adicionamos el código dentro de el.


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

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

export default client;

Reemplaza el URI con la url de tu graph. Tambien debes reemplazar el código dentro de _app.js dentro de la carpeta  page con el código siguiente.


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;

En el código anterior hemos copiado nuestro código con ApolloProvider y provee el cliente con el que hemos creado anteriormente como una propiedad.

Obtener los videos desde el Blockchain

Crea un nuevo archivo que se llame index.js dentro de una nueva carpeta  llamada home y agrega el código siguiente.


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

Ahora vas a obtener un resultado como el que se presenta en esta imagen.

fetch_videos

Como puedes observar es algo muy básico, por el momento solo puedes obtener el titulo del video, entonces vamos a crear un componente reutilizable que nos permita ver estos resultados de una forma más amena.

Para ellos crea una carpeta que se llame components y entonces crea dentro un archivo llamado Video.js dentro de el y copia el código siguiente.


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

Importa el componente de videos al archivo home y reemplaza la función map con el código que se encuentra más abajo.


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

Al salvar el archivo vas a encontrar una nueva página de inicio que será muy parecida a la que te muestro aquí debajo.

homepage

Página de Videos

Ahora que podemos encontrar los videos en la ventana principal debemos trabajar en la página donde los usuarios serán redirigidos al dar clic en uno de esos componentes.

Se debe crear un nuevo archivo em la carpeta components que se llame Player y añade el siguiente código. Para ello utilizaremos react plyr para crear un componente para reproducir los 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}
    />
  );
}

Crea otro archivo en la misma carpeta que se llame VideoContainer, con esto estaremos creando lo que en Youtube sería la ventana que aparece en la parte izquierda de los videos que contienen el Título, la fecha de subida, la descripción del video.

Ahora, puedes agregar este código:


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

Por último crea una carpeta llamada video dentro de la carpeta pages y crea dentro de ella un nuevo archivo que se llamará index.js, luego copia el siguiente archivo.


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

Guarde al archivo y al selecionar o escoger cualquiera de los videos en la página principal serás redirigido a una pantalla similar a esta:

play_video

Funcionalidad de busqueda

Hasta este punto ya tenemos casi realizada las funcionalidades de nuestra aplicación, ahora debemos añadir la funcionalidad de busqueda y para ello dentro de la carpeta components el archivo 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>
  );
};

Esto es un componente simple que esta dividido en 3 partes, En la parte izquierda tendremos el logo de nuestra aplicación, en el medio declaramos una entrada donde los usuarios pueden escribir y buscar y al final tendremos un icono que lleva al usuario hacia la página de subida de archivos.
Regresamos a la página principal (pages/home/index.js) se debe importar el componente  Header añada a partír de la línea 73.

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

Ahora podrás ver este componente en la parte superior de nuestra página de la forma siguiente:

Declara un nuevo estado en la páginal principal despues de la línea 8 para capturar el valor que se obtenga en la página de búsqueda.

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

Puedes tambien actualizar el componente Header para escribir los valores de la entrada dentro de la variable **usesState.

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

Ahora debemos actualizar la función getVideos para buscar los vídeos en caso de que se obtenga algún valor en la variable de estado.


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

En la función hemos añadido un objeto para realizar la busqueda por videos que se encuentran en la variable de estado.

Finalmente vamos a actualizar la función useEffect para que se ejecute cada ves que exista un cambio en la busqueda.

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

¿Qué sigue?

filtros

Si has llegado hasta aquí significa que eres un apasionado de las nuevas tecnologías y acerca de construir la Web 3.0 y aquí podrás encontrar algunas funciones que le puedes poner a tu aplicación:

  1. Permitir a los usuarios la busqueda por videos basados en la categorías.
  2. Tratar de utilizar Arweave para comparar el resultado con respecto al IPFS.
  3. Tratar de adicionar Modo oscuro y modo claro con un disparador para los usuarios.
  4. Mejorar la aplicación.

Conclusiones

Con este artículo se muestra como crear una aplicación parecida a youtube pero descentralizada a partir del uso de IPFS y la red Polygon.