Last updated: September 18, 2024

<span aria-hidden="true" id="build-your-own-youtube-like-site-with-uppy-and-firebase"></span>

# Build your own YouTube-like site with Uppy and Firebase

![Samuel Ogundipe](/assets/images/teammates/samuel.png?dpl=dpl_DM8JrFduDL1oLK4J3qc9o5gMffMy)

**Samuel Ogundipe**

Technical Writer · Lagos, Nigeria · Show bio

[](https://github.com/samuelayo)

Have you ever wondered what it would be like to build your own YouTube-like site where you can upload videos and users can watch in different formats, while having discussions on the video page?

With the increasing worldwide demand for video content, and a growing concern about data ownership, building something like that might be a fun project to take on during the holidays 🎄 And a very[IndieWeb⁠](https://indieweb.org/) project at that!

Now, I’m not saying that one person can build a true YouTube clone in just two hours. I’m just wondering: how far can we get in two hours? This is a bit of a lengthy one, so get comfortable! :)

<span aria-hidden="true" id="taking-a-look-at-our-toolset"></span>

## Taking a look at our toolset

There are some powerful building blocks out at my disposal that will greatly help to cut down on time spent fleshing out the basic functionalities of YouTube. These tools are[Node.js⁠](https://nodejs.org/en/), [Uppy⁠](https://uppy.io/), [Transloadit](/index.md), and[Firebase⁠](https://firebase.google.com/). For our comments section, we’ll be using[Disqus⁠](https://disqus.com/). This may not be ideal, but fine given the scope of this project.

Before we get started, a short introduction for those of us who might not be familiar with Uppy, Transloadit, or Firebase.

[Uppy⁠](https://uppy.io/) is a sleek, modular file uploader that integrates seamlessly with any website or app. It fetches files from a local disk, Google Drive, Dropbox, Instagram, remote URLs, cameras etc., and then uploads them to the final destination. It’s fast, easy to use and lets you worry about more important problems than building a file uploader.

One of the reasons we will be using Uppy is because of its seamless integration with:

[Transloadit](/index.md), a file uploading and processing service that developers can integrate with to handle file uploads, resize, crop and watermark images, make GIFs, transcode videos, extract thumbnails, generate audio wave-forms, process documents, and much more.

We’re using [Firebase⁠](https://firebase.google.com/) to host our project. In light of our IndieWeb reference and the recent[monopoly concerns⁠](https://www.nytimes.com/2018/12/11/business/sundar-pichai-google-house-hearing.html), it may be a bit ironic that we are building this on Google infra. it’s just that I hadn’t gotten my hands dirty on Firebase yet. Rest assured that you can just as well host this code on any other platform, such as Digital Ocean VPS, Heroku, or even your own Raspberry-Pi-powered iron!

Now that we’re familiar with the lego bricks on the table, let’s dive into using them to build our app.

![A screenshot of the final product, showing a webpage with a video clip.](/_next/static/immutable/media/opengraph-image.01ftmzelbndzf.png)

<span aria-hidden="true" id="setting-up-firebase-and-grabbing-credentials"></span>

## Setting up Firebase and grabbing credentials

First off, if we do not already have one, we need to sign up for a[Firebase⁠](https://console.firebase.google.com) account. After signing up, we need to create a new project. Once you create a new project, navigate to **settings** → **Service accounts**, then click the **Generate new private key** button to save a `serviceaccount.json` file.

![A screenshot of the Firebase settings page, on the Service accounts tab.](/_next/static/immutable/media/2018-12-20firebase-serviceaccounts.3azfodz4-ycay.png)

We also need to grab Firebase storage details as we will use them to export the videos that Transloadit encoded. Navigate to **storage**, click on **Get started**, after which a modal will pop up to inform you about security rules. Click 'Got it', go to **rules**, and replace the existing rules with the one provided below, and click **publish**.

```cloud-firestore-security-rules
  service firebase.storage {
    match /b/{bucket}/o {
      match /{allPaths=**} {
        allow read, write;
      }
    }
  }

```

(making this more restrictive is probably a good idea!)

You should write down your folder URL from the **files** tab as Transloadit will need it. Another thing that Transloadit requires when creating storage credentials is your Project Id. To grab this, navigate to **settings** → **General**, and write down the Project Id.

The last thing we need to set up on Firebase is Firestore, since we will be using it as our database. Navigate to **Database**, then click on the **Create Database** button under the cloud Firestore. A modal will now pop up, asking you what mode you want to start with. For this tutorial,**select Test Mode** and click the enable button.

<span aria-hidden="true" id="setting-up-transloadit"></span>

## Setting up Transloadit

Setting up Transloadit is very straightforward. First, we need to[sign up for an account](/c/signup/). I’m choosing **Login via GitHub**. One click later, I have a working account with 2GB of free encoding credits.

I then select **Template Credentials** by the sidebar. This should present you with a drop-down that says `choose a third party service`. Since Firebase storage is basically the same as Google Cloud storage, we'll select `Google Storage` from the drop-down. This then provides us with three more inputs to fill out:

* our Project Id, where we provide a name for this key, which we wrote down during the Firebase Setup,
* our storage bucket, which is the folder URL we copied from the Firebase step, and
* our `serviceaccount.json` file, which we also downloaded during the Firebase step.

![The Transloadit Template Credentials page, where a new Google Storage credential is being made.](/_next/static/immutable/media/2018-12-20-transloadit_template_credential.1yyk6k8syfmml.png)

Next, we need to set up our Template which will hold some JSON data. To do this, I select**Templates** in the sidebar. This should present you with a new page that has a`CREATE NEW TEMPLATE` button. Click that button and paste the following JSON code into theASSEMBLY INSTRUCTIONS box:

```json
{
  "steps": {
    ":original": {
      "robot": "/upload/handle"
    },
    "files_filtered": {
      "use": [":original"],
      "robot": "/file/filter",
      "result": true,
      "declines": [["${file.size}", ">", "20971520"]],
      "error_on_decline": true
    },
    "webm_720p_encoded": {
      "use": [":original"],
      "robot": "/video/encode",
      "result": true,
      "ffmpeg_stack": "{{stacks.ffmpeg.recommended_version}}",
      "preset": "webm",
      "width": 1280,
      "height": 720
    },
    "thumbnailed": {
      "use": [":original"],
      "robot": "/video/thumbs",
      "result": true,
      "ffmpeg_stack": "{{stacks.ffmpeg.recommended_version}}"
    },
    "browser1080_encoded": {
      "use": [":original"],
      "robot": "/video/encode",
      "result": true,
      "ffmpeg_stack": "{{stacks.ffmpeg.recommended_version}}",
      "height": 1080,
      "preset": "ipad-high",
      "width": 1920
    },
    "browser1440_encoded": {
      "use": [":original"],
      "robot": "/video/encode",
      "result": true,
      "ffmpeg_stack": "{{stacks.ffmpeg.recommended_version}}",
      "height": 1440,
      "preset": "ipad-high",
      "width": 2560
    },
    "exported": {
      "use": ["webm_720p_encoded", "browser1080_encoded", "browser1440_encoded", "thumbnailed"],
      "robot": "/google/store",
      "credentials": "firestore_cred"
    }
  },
  "allow_steps_override": false
}

```

After pasting the code above, give it any name of your choice and save the Template. Once this is done, Transloadit will generate a Template ID, which we’ll need to copy as well. I will go into more detail about this code snippet when we get to the point of using it.

> Note: in the root of the JSON, we set `allow_steps_override` to false. This is because we want to disallow any overriding of Steps, ensuring that a browser will never modify the behavior of our Template in any way. Adding[sigature authentication](/docs/api/authentication.md) would be even better.

Lastly, we will need to grab our Transloadit keys, which can be found by clicking the `API Settings`in the sidebar. This will present us with a page to copy both our Auth and Secret Key.

<span aria-hidden="true" id="setting-up-the-api"></span>

## Setting up the API

To get started with our API, we first need to install the Firebase CLI tool. We can do that by running:

```bash
npm install -g firebase-tools

```

Next, we need to log in with our Firebase user account:

```bash
firebase login

```

After logging in, it’s time to create our project:

```bash
# Create a directory which will house our API
mkdir youtube_clone_api
# Move into the API folder
cd youtube_clone_api
# Initiate Firebase
firebase init

```

Firebase will now ask which of its features you want to set up for this folder. We will select`functions`. Next, you will be asked to either create or select a default Firebase project. Here, select the project which we created in our Firebase setup step. Select JavaScript as your preferred language, then decide if you want to use eslint or not. Finally, choose to **install dependencies now**.

What we have now is a bare Firebase functions setup. Now it's time to add our own functions to it. For this, we will use [Express⁠](https://expressjs.com/) alongside Firebase functions. These two play along very nicely.

```bash
# Move into the functions folder
cd functions
# Install required libraries
npm install --save body-parser cors express firebase-admin transloadit

```

Here, we have some libraries to use for creating our API. Now we can replace the content of`functions/index.js` with:

```javascript
const functions = require('firebase-functions')
const express = require('express')
const bodyParser = require('body-parser')
const app = express()
const cors = require('cors')
const fs = require('fs')
const { Transloadit } = require('transloadit')
const admin = require('firebase-admin')
const serviceAccount = require('PATH_TO_SERVICE_ACCOUNT.JSON')

admin.initializeApp({
  credential: admin.credential.cert(serviceAccount),
})
const settings = { timestampsInSnapshots: true }
const db = admin.firestore()
db.settings(settings)
const transloadit = new Transloadit({
  authKey: 'YOUR_TRANSLOADIT_AUTH_KEY',
  authSecret: 'YOUR_TRANSLOADIT_SECRET_KEY',
})

app.use(cors())
app.use(bodyParser.urlencoded({ extended: true }))
app.use(bodyParser.json())

app.get('/api/videos', function (req, res) {
  let videos = []
  db.collection('youtube_clone')
    .get()
    .then((snapshot) => {
      snapshot.forEach((doc) => {
        videos.push({ id: doc.outputPath, data: doc.data() })
      })
      res.json(videos)
    })
    .catch((err) => {
      console.log(err)
      res.json({ err })
    })
})

app.get('/api/video/:id', function (req, res) {
  db.collection('youtube_clone')
    .doc(req.params.id)
    .get()
    .then((snapshot) => {
      res.json({ id: snapshot.outputPath, data: snapshot.data() })
    })
    .catch((err) => {
      console.log(err)
      res.json({ err })
    })
})

app.post('/api/save-video/:assemblyId', function (req, res) {
  // https://api2.transloadit.com/assemblies
  transloadit.getAssembly(req.params.assemblyId, function (err, status) {
    if (err) {
      console.log(err)
      return res.json({ message: 'Could not fetch Assembly' })
    }

    const data = {
      title: req.body.title || req.body.name,
      license: req.body.license || 'IIC',
      caption: req.body.caption || 'No caption provided, enjoy the video',
      poster: status.results.thumbnailed[0].ssl_url,
      sources: [
        {
          type: status.results.webm_720p_encoded[0].mime,
          size: 720,
          src: status.results.webm_720p_encoded[0].ssl_url,
        },
        {
          type: status.results.browser1080_encoded[0].mime,
          size: 1080,
          src: status.results.browser1080_encoded[0].ssl_url,
        },
        {
          type: status.results.browser1440_encoded[0].mime,
          size: 1440,
          src: status.results.browser1440_encoded[0].ssl_url,
        },
      ],
    }

    const id = req.params.assemblyId + new Date().getTime()
    const docRef = db.collection('youtube_clone').doc()
    docRef.set(data)
    res.json({ message: 'Succesfully updated videos list', data, id })
  })
})

const api1 = functions.https.onRequest(app)
module.exports = {
  api1,
}

```

So, what’s happening exactly in the code block above?

We have a basic Express setup using popular Express libraries, such as Cors and Body-parser, to deal with request handling. We set up `Firebase-admin`, `Firestore`, and `Transloadit` to manage encoding and indexing media.

We defined the following three endpoints:

* `/api/videos` - this endpoint fetches all the videos available in our Firestore collection. For this tutorial, our Firestore collection is named `youtube_clone`.
* `/api/video/:id` - this endpoint will receive the details of a single video. It will be called when the video is ready to be displayed.
* `/api/save-video/:assemblyId` - this endpoint saves our videos during encoding. Here, it might seem like magic is happening, so allow me to give you a breakdown:

> When Transloadit encodes a video, it creates an Assembly, which is identified by itsAssembly ID. In our approach, the videos will be sent directly to Transloadit via Uppy, after which we will send the Assembly ID back to the server. The server then uses theAssembly ID to fetch the results of the Assembly.

Once the results of the Assembly have been retrieved, each of our Assembly Steps will have a key in the `results` key of our results. Let’s grab the information needed from those keys and then save them into our database!

Now we are finally ready to call `functions.https.onRequest`, passing in our Express instance and allowing Firebase functions to run this entire API. Isn’t that nice?

To serve the app locally, we need to move out of the functions folder and call `firebase serve` as follows:

```bash
cd ../
firebase serve

```

<span aria-hidden="true" id="setting-up-the-client-app"></span>

## Setting up the client app

For our client app, to get started easily, we will be using a Vue framework called[Quasar⁠](https://quasar-framework.org). Install like so:

```bash
# Install the Vue CLI globally
npm install -g vue-cli
# Install the quasar CLI globally
npm install -g quasar-cli

```

After installing Quasar, let’s proceed to create our app by running:

```bash
quasar init youtube_clone_client

```

Then follow the prompt to set up the app properly.

Once the app has been set up, it's time to install the libraries we will be using in our app:

```bash
npm install --save @uppy/core @uppy/dashboard @uppy/dropbox @uppy/google-drive @uppy/instagram @uppy/transloadit @uppy/url @uppy/webcam plyr vue-disqus

```

In the code snippet above, we have installed the Uppy package alongside its plugins for Dropbox, Google Drive, Instagram, URL, Transloadit, and Webcam.

We have also installed `plyr.io`, which will play our videos and `vue-disqus` for implementing our comments section.

**Building the Layout Page** Let’s dive into more code writing! Open up your`src/components/layout/layout.vue` and replace the template aspect with:

```html
<template>
  <q-layout view="lHh Lpr lFf">
    <q-layout-header>
      <q-toolbar color="primary" :inverted="$q.theme === 'ios'">
        <q-toolbar-title>
          <router-link :to="'/'" style="color: white; text-decoration:none;">
            2hr YouTube “Clone”
          </router-link>
          <div slot="subtitle">
            Powered by <a href="">Transloadit</a>, <a href="https://uppy.io">Uppy</a> and Firebase
          </div>
          <q-btn
            color="standard"
            @click="openUppyModal"
            label="Upload Videos"
            style="float:right;"
          />
        </q-toolbar-title>
      </q-toolbar>
    </q-layout-header>
    <q-page-container>
      <router-view />
    </q-page-container>
  </q-layout>
</template>

```

So, what has changed in here? In the new code snippet, we have removed the default drawer navigation that comes with Quasar, and edited our header to have a button by the right-hand side for uploading videos. **Note that** the `q-btn` element has a `click` directive that calls the `openUppyModal`function. Let’s find out in the next code block what this function does and where it is defined?.

Staying in the same `src/components/layout/layout.vue` file, let’s replace the script aspect with:

```javascript
import axios from 'axios'
import Uppy from '@uppy/core'
import Transloadit from '@uppy/transloadit'
import Dashboard from '@uppy/dashboard'
import Webcam from '@uppy/webcam'
import Instagram from '@uppy/instagram'
import Dropbox from '@uppy/dropbox'
import GoogleDrive from '@uppy/google-drive'
import Url from '@uppy/url'

require('@uppy/core/css/style.css')
require('@uppy/dashboard/css/style.css')
require('@uppy/webcam/css/style.css')
require('@uppy/url/css/style.css')

export default {
  name: 'MyLayout',
  data: function data() {
    return {
      opened: false,
      uppy: '',
      results: {},
    }
  },
  methods: {
    openUppyModal: function (event) {
      this.uppy.getPlugin('Dashboard').openModal()
    },
    closeUppyModal: function (event) {
      this.uppy.getPlugin('Dashboard').closeModal()
    },
  },
  mounted: function mounted() {
    axios.defaults.headers.common = {
      'Content-Type': 'application/json',
      Accept: 'application/json',
    }
    this.uppy = Uppy({
      id: 'uppy',
      autoProceed: false,
      allowMultipleUploads: true,
      debug: false,
      restrictions: {
        maxFileSize: 20971520,
        maxNumberOfFiles: 20,
        minNumberOfFiles: 1,
        allowedFileTypes: ['video/*'],
      },
    })
      .use(Dashboard, {
        // inline: true,
        // target: this.$refs['select-files'],
        disablePageScrollWhenModalOpen: false,
        closeModalOnClickOutside: true,
        note: 'Maximum size for this demo is 20MB. Please Edit the description of each video before uploading',
        metaFields: [
          { id: 'title', name: 'Title', placeholder: 'Video Title' },
          { id: 'license', name: 'License', placeholder: 'Specify license' },
          { id: 'caption', name: 'Caption', placeholder: 'Describe what the video is about' },
        ],
      })
      .use(Webcam, { target: Dashboard })
      .use(Instagram, {
        target: Dashboard,
        serverUrl: 'https://api2.transloadit.com/companion',
        serverPattern: /\.transloadit\.com$/,
      })
      .use(GoogleDrive, {
        target: Dashboard,
        serverUrl: 'https://api2.transloadit.com/companion',
        serverPattern: /\.transloadit\.com$/,
      })
      .use(Dropbox, {
        target: Dashboard,
        serverUrl: 'https://api2.transloadit.com/companion',
        serverPattern: /\.transloadit\.com$/,
      })
      .use(Url, {
        target: Dashboard,
        serverUrl: 'https://api2.transloadit.com/companion',
        serverPattern: /\.transloadit\.com$/,
      })
      .use(Transloadit, {
        params: {
          auth: {
            key: 'YOUR_TRANSLOADIT_AUTH_KEY',
          },
          // it’s more secure to use a template_id and enable
          // Signature Authentication
          template_id: 'YOUR_TRANSLOADIT_TEMPLATE_ID',
        },
        waitForEncoding: true,
      })
    this.uppy.on('complete', (result) => {
      this.$q.loading.show({
        message: 'Saving encoded videos to firebase',
        messageColor: 'black',
        spinnerColor: 'white',
      })
      result.successful.forEach((element, index) => {
        axios
          .post(`${window.base_url}/api/save-video/${element.transloadit.assembly}`, element.meta)
          .then((response) => {
            // this.vidoes = response.data.data
            if (index + 1 == result.successful.length) {
              this.$q.loading.hide()
              this.$q.notify({
                color: 'positive',
                position: 'top',
                message: 'All files uploaded. videos ready. Please visit the home page',
                icon: 'report_problem',
              })
              this.uppy.getPlugin('Dashboard').closeModal()
            }
          })
          .catch(() => {
            this.$q.loading.hide()
            this.$q.notify({
              color: 'negative',
              position: 'top',
              message: 'Loading failed',
              icon: 'report_problem',
            })
          })
      })
    })
  },
}

```

This is where the bulk of the work is going on.

First, we imported the Uppy core library and all of its plugins which we installed previously. Next, we imported [their respective styles⁠](https://uppy.io/docs/dashboard/#CSS).

In the methods section, we defined the open and close methods of the Uppy Dashboard plugin.

In the mounted section, we defined an instance of Uppy, passing in some options. One of the options we passed is called `restrictions`, which comprises:

* `maxFileSize` - The maximum size of a file that can be accepted. Here, it is 20MB
* `maxNumberOfFiles` - The maximum number of files that can be uploaded in one go, which we set at 20.
* `minNumberOfFiles` - The minimum number of files that can be uploaded in one go, which we set to 1.
* `allowedFileTypes` - The files we want to allow for uploading. In our case, since we are building a video site, it is set to video.

We then called the `.use` function, which is used to notify Uppy of the plugins we want to use — passing in, for instance, the Dashboard plugin. We also passed in some options such as `note`, which informs users of any special uploading requirements, and the `metaFields` option, an array of fields that we would like to collect alongside every uploaded file. In our case, we want a title, the license and the description of the file.

Going further, we made a couple of calls to the `use` function, passing in our plugins, targeting them to the Dashboard and passing in the `serverUrl` to which they should be uploaded. Let’s take a further look at the `use` call to the Transloadit Uppy plugin.

If you are used to Transloadit, the above should look familiar. For those new to Transloadit, these options passed to the Transloadit plugin are called[Assembly Instructions](/docs/topics/assembly-instructions.md): a simple JSON string holds a recipe for what Transloadit should do with your files.

Inspecting the Assembly Instructions, we supplied an `auth` param, passing in our Transloadit auth key. Next, we passed in the `template_id` that we created while setting up Transloadit. In our saved Template, we defined a few `steps` for our files to go through. Here, we have asked Transloadit to reject any file above 20MB in size, to encode our videos into three different sizes: 720p, 1080p and 1440p, and to give us screenshots of the video, which we will use as poster/banner images later on in the app.

> **Note that** you can define as many Steps as you want for your videos. For example, you can encode to 360p, 560p, and even 4k formats, or create an animated GIF for previewing. If you want to be the next YouTube for audio files, you can consider generating waveforms as previews.

We then proceeded by asking Uppy to `wait` for encoding to be done before notifying us the upload process is completed.

And lastly, we added a listener to know when a file upload process has completed. Once completed, we display a loader while we send the new Assembly ID to the API we created earlier, so it can save our new details in the database.

> By default, the call to `this.$q.notify` would cause errors, as the notify plugin isn’t loaded. To solve this, we have to edit the `quasar.conf.js` file at the root of our folder, then add it to the framework plugins.

**Building the homepage** We need a page that lists all of our videos, so we can select which one we want to view. Let’s replace the content of `src/components/pages/index.vue` with:

```html
<template>
  <q-page class="flex">
    <div class="q-container">
      <q-card v-for="vid in vidoes" v-bind:key="vid.id">
        <router-link :to="'/video/'+vid.id" class="video-link">
          <q-card-media>
            <img :src="vid.data.poster" />
          </q-card-media>
          <q-card-main>
            <q-card-title>{{vid.data.title}}</q-card-title>
            <p class="text-faded">{{vid.data.caption}}</p>
            <p class="text-faded">License: {{vid.data.license}}</p>
          </q-card-main>
        </router-link>
      </q-card>
      <h3 v-if="!vidoes.length">We have no videos in store currently. Please check later 👌</h3>
    </div>
  </q-page>
</template>
<script>
  import axios from 'axios'
  export default {
    data: () => ({
      vidoes: [],
    }),
    mounted: function mounted() {
      this.$q.loading.show({
        message: 'Loading available Videos',
        messageColor: 'black',
        spinnerColor: 'white',
      })
      axios
        .get(`${window.base_url}/api/videos`)
        .then((response) => {
          this.vidoes = response.data.data
          this.$q.loading.hide()
        })
        .catch(() => {
          this.$q.notify({
            color: 'negative',
            position: 'top',
            message: 'Loading failed',
            icon: 'report_problem',
          })
          this.$q.loading.hide()
        })
    },
  }
</script>

```

What’s happening here? First, we display a pre-loader while we are fetching all videos, then we make an AJAX call to our API via Axios to fetch us the videos. Once we get the videos, we then display them in a loop, displaying the poster, caption, and description.

Here, **note that** the `router-link` call attempts to move to a route defined as `/video/:id`, while no such route exists yet.

Some components which we have used such as `QCard`, `QCardTitle`, `QCardMain`, `QCardMedia` are not made available by Quasar by default. To make them available, we have to edit the `quasar.conf.js`file at the root of our folder, then add it to the framework component as follows:

```javascript
framework: {
  components: [
    'QLayout',
    'QLayoutHeader',
    'QLayoutDrawer',
    'QPageContainer',
    'QPage',
    'QToolbar',
    'QToolbarTitle',
    'QBtn',
    'QIcon',
    'QList',
    'QListHeader',
    'QItem',
    'QItemMain',
    'QItemSide',
    'QCard',
    'QCardTitle',
    'QCardMain',
    'QCardMedia',
  ]
  // …
}

```

**Defining the video routes** Let’s open up our `src/router/routes.js` and replace the content with:

```javascript
window.base_url = 'https://localhost:5000/fir-firestore-trans-sam/us-central1/api1'
const routes = [
  {
    path: '/',
    component: () => import('layouts/MyLayout.vue'),
    children: [
      { path: '', component: () => import('pages/Index.vue') },
      { path: '/video/:id', component: () => import('pages/single.vue') },
    ],
  },
]
// Always leave this as last one
if (process.env.MODE !== 'ssr') {
  routes.push({
    path: '*',
    component: () => import('pages/Error404.vue'),
  })
}
export default routes

```

Looking at the code block above, note the two changes were made to the routes file, which are:

* the declaration of a `window.base_url`. If you notice, there has been a call to the `base_url`with our axios calls. In Quasar, the `main.js` / `app.js` file is generated by default, which can lead to changes being overwritten. One place where we can declare global variables happens to be the routes file.
* adding a new child to the initial part, which accepts the `video/:id` route and also imports the single component that we have yet to create.

**Creating the single component** In the routes above, an import to the `pages/single.vue` file was made. Now it’s time to create that file. Create a new file called `single.vue` in your`src/components/pages` folder and add:

```html
<template>
  <q-page>
    <div style="margin: 30px auto; width: 700px;">
      <video
        v-if="video.id"
        id="my-player"
        ref="my-player"
        controls
        crossorigin
        playsinline
        data-plyr-config='{ "title": "This is an example video", "volume": 1, "debug": true }'
        :poster="video.data.poster"
      >
        <source
          v-for="(src, index) in video.data.sources"
          v-bind:key="index"
          :src="src.src"
          :type="src.type"
          :size="src.size"
        />
      </video>
      <h3 v-if="!video.id">Cannot fetch video</h3>
    </div>
    <div class="comments">
      <vue-disqus shortname="youclone" url=""></vue-disqus>
    </div>
  </q-page>
</template>
<script>
  import Plyr from 'plyr'
  import axios from 'axios'
  import 'plyr/dist/plyr.css'
  export default {
    data: function data() {
      return {
        player: '',
        video: {},
      }
    },
    components: {},
    mounted: function mounted() {
      this.$q.loading.show({
        message: 'Preparing all video formats for your graceful view',
        messageColor: 'black',
        spinnerColor: 'white',
      })
      axios
        .get(`${window.base_url}/api/video/${this.$route.params.id}`)
        .then((response) => {
          this.video = response.data.data
          setTimeout(() => {
            this.player = new Plyr(this.$refs['my-player'])
            this.$q.loading.hide()
          }, 50)
        })
        .catch(() => {
          this.$q.loading.hide()
          this.$q.notify({
            color: 'negative',
            position: 'top',
            message: 'Loading failed',
            icon: 'report_problem',
          })
        })
    },
  }
</script>
<style>
  .plyr--video {
    max-height: 100%;
    width: 100%;
    margin: 2em;
  }
  .plyr__video-wrapper {
    background: #000;
    border-radius: inherit;
    overflow: hidden;
    position: relative;
    z-index: 0;
    object-fit: fill;
  }
</style>

```

In the code block above, we have imported `plyr.io` for displaying our video, and it was done in the following way:

* Creating a Video HTML tag in the template, setting its poster to our thumbnail image.
* Looping over our different video types, using them as sources. We also added a size attribute, so`plyr.io` can display a quality switcher. If the size attribute is not added, the quality switcher would not show up.
* In the script section, we use `axios` to fetch our video using its id.
* Once the video is fetched, we wait for a 50ms period before initializing `plyr.io`, so we are sure everything has rendered successfully.

**Note**

We made a call to `vue-disqus` in the Template. The thing is,`vue-disqus` is not available to us as it requires an app-wide integration, not just on a component level. In Quasar, your `main.js` is automatically generated and any changes you make would be overwritten. So how do we make it available? By declaring it as a Quasar plugin.

**Adding a plugin for Vue-Disqus**

Adding a plugin to Quasar is quite straightforward. Run the following in the terminal:

```bash
quasar new plugin disqus

```

**Note**

the name `disqus` here should be the name of your plugin. You can give it any name you want.

This command creates a new file under `src/plugins`, called `disqus.js` . Replace the content of the file with:

```javascript
// import something here
import VueDisqus from 'vue-disqus'
// leave the export, even if you don't use it
export default ({ Vue }) => {
  // something to do
  Vue.use(VueDisqus)
}

```

We have now imported the `vue-disqus` plugin and asked the `Vue` instance to use it as a plugin.

At this point, Quasar would not load this plugin until we specify it in the way we did for the`notify` plugin. To do that, we have to edit the `quasar.conf.js` file at the root of our folder, then add it to the framework plugins, so it looks like this:

```javascript
framework: {
  components: // …,
  directives: // …,
  // Quasar plugins
  plugins: [
    'Notify',
    'Loading',
  ]
}

```

**Adding styles to our app** Our app could still do a bit better in terms of styling. To add some more slickness to our app, let’s replace the content of `src/css/app.styl` with:

```styl
body {
  background-color: #f5f5f5;
}

p {
  margin: 0 0 8px;
}

a {
  text-decoration: none;
}

.video-link {
  display: block;
}

.q-container {
  max-width: 1040px;
  margin: 0 auto;
  padding: 0 20px;
  overflow: hidden;
}

.q-container:empty {
  display: none;
}

.q-container h3 {
  font-size: 25px;
}

.q-card {
  background-color: #fff;
  padding: 15px 15px 8px;
  margin-top: 1.5em;
  margin-bottom: 1.5em;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
  border: 1px solid #e8e8e8;
  max-width: 600px;
  border-radius: 5px;
}

.q-card-main.q-card-container {
  padding: 0;
}

.q-card-primary.q-card-container {
  margin-top: 7px;
  padding: 0;
}

.q-card-title {
  font-size: 16px;
  font-weight: 500;
}

.q-toolbar-subtitle a {
  // text-decoration: underline;
  color: #fff;
}

.q-toolbar-subtitle a:hover {
  text-decoration: underline;
}

// Uppy-specific styles

.uppy-transloadit-youtube .uppy-Dashboard-note {
  max-width: 430px !important;
  font-size: 14px !important;
  line-height: 1.5 !important;
  margin: auto !important;
}

.uppy-transloadit-youtube .uppy-size--md {
  font-size: 14px;
}

.uppy-transloadit-youtube .uppy-Dashboard--modal .uppy-Dashboard-overlay {
  background-color: rgba(0, 0, 0, 0.75) !important;
  z-index: 2005 !important;
}

.uppy-transloadit-youtube .uppy-Dashboard--modal .uppy-Dashboard-inner {
  z-index: 2010 !important;
}

.uppy-Dashboard-isFixed .q-layout-header {
  z-index: 1000;
}

```

<span aria-hidden="true" id="what-our-app-looks-like"></span>

## What our app looks like

Here is a video demonstrating what our little app looks like at this point.

Here’s the [GitHub Repo⁠](https://github.com/transloadit/letsbuild-youtube-clone/) and[here⁠](https://fir-firestore-trans-sam.firebaseapp.com/) is the actual app.

<span aria-hidden="true" id="conclusion"></span>

## Conclusion

Hopefully, I have been able to show you how surprisingly easy it is to get your own video site rolling in just a couple of hours. Can you build a better iteration on top of this over the christmas break? 😄

If you have any further questions, please feel free to reach out to us. Happy Holidays!

[#letsbuild](/blog/tags/letsbuild.md)[#walkthrough](/blog/tags/walkthrough.md)

### 👩‍💻 Join 20k+ developers

Sign up for our [monthly newsletter](/newsletters.md) to receive direct links to 3 exclusive tech — and 2 product updates. No less, no more.

Your email:

Get access

## File uploading and encoding. Made simple.

Transloadit streamlines file handling for developers, trusted by brands like Coursera and The New York Times. We’re known for a reliable API, top-notch support, and a strong commitment to open source, with projects like [Uppy⁠](https://uppy.io) and [Tus⁠](https://tus.io) setting standards in file processing.

[Sign up](/c/)[Book a Demo](https://survey.typeform.com/to/kRg47Xi5)

No credit card needed

Cancel anytime
