Watch Youtube Streams and Chat with React

Last updated June 22, 2021 by Jacob Paris

In this tutorial I'm going to show you how to bootstrap a React project from scratch using NPM and Webpack, integrate Google Login, grab data from the Youtube API, and deploy your code to Netlify

We will be building an application that shows a list of active Youtube Live Streams and lets the user watch them.

Part 2 will include adding a custom serverless websocket based chat that your users can use to talk with one another while watching the videos.

The complete demo can be found here: TalkTV on Netlify

If you get an error saying the application is not authorized, I just didn't bother submitting it to Google for verification since it's a demo app. You can safely skip it.

The codebase can be found here: TalkTV on GitHub

Getting Started

First we need to create a new repository for our project, so head over to GitHub. Thanks to Microsoft we can now create free private repositories, so you can keep your code secret if you like.

My Github is about 70/30 private/public, which used to be just a ratio and now is pretty close to the actual quantity.

Alt Text

Once you're set up, click that green button in the top right and get the address to clone your repo to your local computer.

Alt Text

Now some people like to make a local repository on their computer with git init and then set the upstream path to point to GitHub — and that works, at least for people who learned how to do it that way.

I choose the easier method:

1git clone https://github.com/JacobParis/talktv.git
2

With your repository set up on your local machine, open it up in your IDE of choice and we're ready to start!

Install Dependencies

React can be a little tricky to get going if you aren't familiar with modern build tooling. CreateReactApp was created to solve this issue, but it's very opinionated and locks away a lot of useful configuration options. Gatsby and Next.js are two other popular options, but I'll walk you through setting up your own.

1npm init
2

Initializing npm will create a package.json file that will store a list of our dependencies and our build scripts.

When we install packages, the --save flag will make sure they get added under dependencies in the package.json file. If we do --save-dev instead, they'll be added under devDependencies.

Later, anyone using this repository can install all packages with a simple npm install

First we install React. npm i is shorthand for npm install

1npm i --save react react-dom react-router-dom styled-components
2

Then webpack, which we'll use to compile and bundle our project

1npm i --save-dev webpack webpack-cli
2

And Babel, which transforms the easy code we write into the complicated code that works on everyone's devices

1npm i --save-dev babel-loader @babel/core @babel/preset-env @babel/preset-react
2

Some plugins to read our HTML and CSS

1npm i --save-dev css-loader html-webpack-plugin mini-css-extract-plugin
2

And a loader for our SCSS code (optional)

1npm i --save-dev node-sass sass-loader
2

You're also going to want to create a .gitignore file with the following line. This will stop git from tracking our packages as if they were our own code.

node_modules

Configuring Webpack

There are a lot of resources out there for learning webpack, including this excellent article by David Gilbertson which taught me pretty much everything I know.

I've been carrying the same boilerplate webpack configuration file forward for a while now, so feel free to steal it here. Create a webpack.config.js file in your root directory and copy this code into it.

{% gist https://gist.github.com/JacobParis/73cbb826efea3e172991e373eac938f2 %}

Building the app

To get started, we need some HTML and Javascript. We'll start with the most basic index.html file, which we'll place in an app directory to keep it separate from the configuration boilerplate.

1<!DOCTYPE html>
2<html>
3 <head>
4 <title>📺 Talk TV</title>
5 <link
6 href="https://fonts.googleapis.com/css?family=Montserrat&display=swap"
7 rel="stylesheet"
8 />
9 </head>
10 <body>
11 <div id="root"></div>
12 </body>
13</html>
14

Check through Google Fonts and pick a font you like. They're all free for both commercial and non-commercial purposes. Replace the <link /> tag with one you're happy with, then create a new app/index.js file in the same directory.

This is where we start working with React, but we'll want to keep it as simple as possible for now. Our goal is to get something functional displayed on-screen before we start fleshing out the product.

1import React from 'react'
2import ReactDOM from 'react-dom'
3
4function App() {
5 return <h1> 📺 Talk TV</h1>
6}
7
8ReactDOM.render(<App />, document.getElementById('root'))
9

Head back to our package.json file and add a new build script. We'll be able to build our app with the command npm run build no matter what we change this script to be later. Right now it's not very important if you'd prefer to type npx webpack instead, but some of our later scripts are going to be more complicated and I prefer consistency across the board.

1"scripts": {
2 [...]
3 "build": "npx webpack"
4}
5

NPX is a utility by npm that lets you execute packages without installing them globally. If you run the command npm i -g webpack, it will add the webpack command to your PATH. While this normally won't cause any issues, it forces you to use the same version of webpack for every project on your machine. Using npx allows your project to remain agnostic of the machine it's running on.

Modify our .gitignore file to add our dist directory

dist node_modules

Run our new build script and you should see a number of files appear inside the dist directory. If so, success! We're ready to test our site live.

1npm run build
2

Running Locally

If we open up our index.html file in a browser, everything should work fine for now, but routing won't work well later once we implement that. Luckily the team at Zeit has created the excellent serve package for spawning a quick webserver on our local machine.

We'll call it with the -s flag to hint that we're running a single page application, which will continue to serve our index.html file instead of giving us 404 errors.

Add a serve script to our package.json file and then run it.

1"scripts": {
2 [...]
3 "serve": "npx serve -s dist"
4}
5
1npm run serve
2

Navigate to localhost:5000 (or any other port you may have chosen) in your browser, and see what we see!

Alt Text

Running on Netlify (Optional)

Local is great for development, but there isn't much of a point building a site if nobody else gets to use it. I recommend Netlify a hundred times over for hosting static websites for free. When I started using it, it was the clear winner in the category. Nowadays, GitHub Pages is a very strong contender and is likely even easier to set up since you're already using GitHub for your repository.

I'll be using Netlify because it's great at what it does and it's what I'm used to.

Feel free to skip this section if you aren't interested in public hosting or if you want to set up on your own. Otherwise, head on over to Netlify and log in or sign up!

Alt Text

Create a new site from Git

Alt Text

Choose GitHub, or any other VCS provider you may be using

Alt Text

Give Netlify permission to access your code

Alt Text Alt Text

Now every time you commit and push the changes you've made to your application, and Netlify will automatically start deploying them.

Alt Text Alt Text

And once it's ready you can see it live at the URL

Alt Text

Create the Login Scene

Okay! So when a user gets to our site, they need to be prompted to log in. We can map that flow with a simple flowchart like this

1Arrives on Site:
2 - Is logged in:
3 Show Gallery Scene
4 - Is not logged in:
5 Show Login Scene:
6 - Prompt for login
7 - If successful, refresh
8``` numbered
9
10By refreshing after login, we avoid setting up any duplicate routing commands. The user simply goes through the normal flow a second time with the access to get to the right place.
11
12The minimal implementation here is an `isLoggedIn` state that will show either scene. We don't even need to implement the login yet. One thing at a time.
13
14Create a `scenes` folder and a `login.js` file inside. This will be a super simple scene to start.
15
16```jsx numbered
17import React from "react";
18
19 export default function() {
20 return (
21 <h1> Please log in! </h1>
22 );
23}
24

And then back in our index.js we import the scene and set our state

1import LoginScene from './scenes/login'
2
3function App() {
4 const isSignedIn = false
5
6 return isSignedIn ? <h1> 📺 Talk TV</h1> : <LoginScene />
7}
8

Rebuild, and the app should greet you with our new scene!

Alt Text

Test by changing isSignedIn to true and see if you get the old homepage. That means everything is working so far!

Start building our UI

You can follow this section as loosely as you want — it's your app to make look however you want. Functionally the next thing we need to do is implement the actual google authentication, but for that we need a button. And if we're making buttons already, we might as well make them look nice.

Make a new folder to store our components. These will be reusable combinations of HTML and CSS, so we avoid a lot of rewritten code. I like Styled Components for this because it shoves your own code reuse in your face. You become very encouraged to use the same components again rather than making another one that's almost identical.

In components/containers.js add this code:

1import styled from 'styled-components'
2
3export const Container = styled.div`
4 padding: 2rem;
5 display: flex;
6 justify-content: center;
7`
8

This is a small flex container that will center any elements placed within it. We'll wrap our Login scene text with it

1import {Container} from '../../components/containers'
2
3export default function () {
4 return (
5 <Container>
6 <h1> Please log in! </h1>
7 </Container>
8 )
9}
10

If you rebuild you should see some center aligned text!

Change the font

But Times New Roman is ugly, so it's time to set our actual font. If you'll remember our index.html file has a Google Font import in the header for Montserrat. If you used a different font you'll want to do that here too.

Beside our index.html and index.js files lets make an index.scss file. This will hold our global application styles that we don't want to leave up to the component.

1html {
2 font-family: 'Montserrat', sans-serif;
3}
4
5body {
6 margin: 0;
7}
8

And then at the very top of our index.js we need to import it.

1import './index.scss'
2

It might seem weird to import a CSS file, but this import is how webpack is able to find it and process it into regular CSS. Webpack starts at our entry point (index.js) and then branches through every import in every file connected to it.

Create a card component

Having styled text in the center of your screen works for a very minimalist aesthetic, but I'm feeling more fond of a dedicated card to greet our new users. Add a new component called cards.js

1import styled from 'styled-components'
2
3export const Card = styled.div`
4 color: #333;
5 background-color: #fff;
6 border: 1px solid black;
7 border-radius: 0.5rem;
8 padding: 1.5rem;
9 width: 90%;
10 max-width: 300px;
11 text-align: center;
12`
13

Here we have a container with a nice border, rounded edges, centered text and it grows to 90% of its parent width to a maximum of 300px. On really narrow devices, like portrait smartphones, this gives us a nice 5% margin on either side.

I was playing around here for a little bit and I also decided to add two more components in the same file

A subtle dividing line for our card

1export const Divider = styled.hr`
2 width: 50%;
3 opacity: 0.2;
4 margin-bottom: 2rem;
5 margin-top: 0;
6`
7

And a large icon for our logo, which is just an emoji

1export const Icon = styled.p`
2 font-size: 10rem;
3 margin: 0;
4 user-select: none;
5`
6

Go back to our login.js and replace our please log in plea with our new components.

1import {Card, Divider, Icon} from '../../components/cards'
2
3export default function () {
4 return (
5 <Container>
6 <LoginCard />
7 </Container>
8 )
9}
10
11function LoginCard() {
12 return (
13 <Card>
14 <header>
15 <Icon>📺</Icon>
16 <h1>Talk TV</h1>
17 </header>
18 <Divider />
19 <div>PLEASE LOG IN</div>
20 </Card>
21 )
22}
23

You could easily just add the LoginCard contents directly in our Container, but I like to separate distinct components as much as I can. If you run it, your site should be looking almost like this. I actually forgot to take a screenshot here so I'm a bit ahead of you.

Alt Text

The biggest change I think should be the blue border, which we'll handle now.

Adding a theme config file

When you reuse the same colours, styles, and sizes throughout your app, it can be hard to keep them consistent if you change them all later. Luckily, Styled Components makes it really easy to keep a central store of our application styling.

Create a new file called app/theme.js next to our index files, and add some basic styles to it

1export default {
2 background: '#ffffff',
3 baseFontColor: '#000000',
4 baseFontSize: '16px',
5 baseRadius: '1rem',
6 primary: '#2196f3', // MD Light Blue 500
7}
8

Back in our card component, we can access the Theme file like any other bit of javascript

1import Theme from '../theme'
2

And then replace our border styles with this

1border-radius: ${props => Theme.baseRadius};
2border: ${props => `1px solid ${Theme.primary}33`};
3

The argument to that function is called props because Styled Components lets us access the props in our styles. We don't need that right now, so you can replace it with () => or _ => like people often do when they don't need arguments.

If you rebuild, you should be looking like my last screenshot there now!

Adding a Login button

Now we still don't have an actual clickable button, so we'll want to create a new components/button.js file. Buttons are some of the most versatile components out there, so instead of building one from scratch I stole a component from a previous project which I will provide for you to steal also!

Here I make significant use of the props argument I mentioned above, for example in this line:

1cursor: ${props => props.disabled ? "not-allowed" : "pointer"};
2

And you can trigger that by adding the prop attribute.

1<button disabled>NOT ALLOWED</button>
2

In our login.js file, once again replace our please log in text with a component. It might seem odd to do everything in little bite sized steps like this, but using text placeholders for components solves the problem that every part of our app is going to depend on something else downstream.

It's better to focus on getting one dependency working at a time and then moving on to build the next step.

1
2import { Button } from "../../components/button";
3
4function LoginCard() {
5 return (
6 <Card>
7 <header>
8 <Icon>📺</Icon>
9 <h1>Talk TV</h1>
10 </header>
11 <Divider />
12 <Button primary>LOG IN</Button>
13 </Card>
14 );
15}
16``` numbered
17
18And now we have a button!
19
20![Alt Text](https://thepracticaldev.s3.amazonaws.com/i/mrojv8qkueotyz69n9xm.png)
21
22## Google Authentication
23
24With our button in place to change our `isLoggedIn` state and the mechanics to change our route based on the state, all that's left to do is tie them together by integrating Google auth. It isn't too difficult but it can be tough to navigate the docs and API Console and get where you need to be.
25
26Head on over to the [Google API Console](https://console.developers.google.com/apis/dashboard) and click the New Project button in the top left
27
28![Alt Text](https://thepracticaldev.s3.amazonaws.com/i/5r71vlqrjsrdqnrtwfvh.png)
29
30![Alt Text](https://thepracticaldev.s3.amazonaws.com/i/y3k9tek5nmuoafaj9lvb.png)
31
32Once your project is created, go to **OAuth Consent Screen** in the left sidebar and set our Application Name and Authorized domain.
33
34![Alt Text](https://thepracticaldev.s3.amazonaws.com/i/5in9x9urmal9jqd9lplu.png)
35
36![Alt Text](https://thepracticaldev.s3.amazonaws.com/i/rz8xt6hwrxp9eabazegx.png)
37
38We're going to need two sets of keys. In order to log in, we will need oAuth2 keys. In order to pull data from the Youtube API, we will need an API key. Go to **Credentials** -> **Create Credentials** -> **OAuth Client** ID
39
40![Alt Text](https://thepracticaldev.s3.amazonaws.com/i/loj0e02uiye1qlostqkw.png)
41
42Select **Web Application**, put in our application name again and then add our javascript origins. Google will reject requests that don't come from a verified origin. We'll want to add both our Netlify URL and our localhost origin for development.
43
44![Alt Text](https://thepracticaldev.s3.amazonaws.com/i/vml4wipxcw8ir9uw1pbj.png)
45
46Now in **Dashboard** -> **Enable APIs and Services** search for the Youtube Data API
47
48![Alt Text](https://thepracticaldev.s3.amazonaws.com/i/u33hqv7jzaalxhlq6xep.png)
49
50![Alt Text](https://thepracticaldev.s3.amazonaws.com/i/ni84kuntoyfe2ex9fbd3.png)
51
52
53Add a new API Key. We'll use this to connect to the Youtube API.
54
55![Alt Text](https://thepracticaldev.s3.amazonaws.com/i/cw8c9ltwv1lr4hbz1nhg.png)
56
57![Alt Text](https://thepracticaldev.s3.amazonaws.com/i/r2225piu6l3u7f9nys0e.png)
58
59If you head back to **Credentials**, you can see both our API Key and our Client ID ready to go
60
61## Integrating into the app
62
63In order to connect to the Google API, we need to import the Google JS API into our project. There are a number of ways to do this but the easiest by far is to simply include it in the `<head>` tag of our `index.html` file at the root of our app.
64
65```html numbered
66<head>
67 <title>📺 Talk TV</title>
68 <link href="https://fonts.googleapis.com/css?family=Montserrat&display=swap" rel="stylesheet">
69 <script src="https://apis.google.com/js/api.js"></script>
70</head>
71

Next we need to add our keys to our index.js file. I like to keep these constants just above the main App declaration.

1const CLIENT_ID = ASDFASDFASDFASDF;
2const API_KEY = QWERQWERQWERQWER;
3
4function App() {
5

Before we can use the Google API, we need to initialize it. One way to do this would be to check its status before each method call, and if needed initialize first, but that is a lot of unnecessary checking.

Due to the way React works, we can track its ready status in a state, and choose to only render the app when Google is ready. None of our child components can call the API unless they're rendered, so we shouldn't run into issues.

When we included the API into our <head> tag, it exposed the global keyword gapi to all of our javascript files, and we'll use that to interact with it.

In our index.js make a new function called Preloader and change the ReactDOM render method at the bottom of the file to call the preloader instead.

1function Preloader() {
2 const [isGoogleReady, setGoogleReady] = React.useState(false)
3
4 return isGoogleReady ? <App /> : <div>Loading...</div>
5}
6
7ReactDOM.render(<Preloader />, document.getElementById('root'))
8

By switching isGoogleReady from true to false you will see either the app or the loading screen.

Since the google API is outside of our component, we'll want to wrap the code that interacts with it inside a useEffect hook. We initialize the API and then set our state when it's ready

1React.useEffect(() => {
2 const authPromise = gapi.auth2.init({
3 clientId: CLIENT_ID,
4 })
5
6 authPromise.then(() => {
7 setGoogleReady(true)
8 })
9}, [])
10
11return isGoogleReady ? <App /> : <div>Loading...</div>
12

If you run this now, you should see the Loading screen appear briefly before the main app does. If so, great! But there are still two issues with our code.

The first is that sometimes our component will load for the first time and the Google API could already be initialized. If that's the case, we don't need to redo it.

We can test for this by the presence of the auth2 field in the gapi object. Update our initial state declaration with the following:

1const wasGoogleReady = !!gapi.auth2
2const [isGoogleReady, setGoogleReady] = React.useState(wasGoogleReady)
3

The second issue is that by the time we finish initializing, our component might have re-rendered already. When a component re-renders, it's a completely new function in a new context that can't use our old state variables, so trying to set them will cause an error. React has very descriptive errors that will let you know exactly when this is the case, we can solve it now anyway.

The useEffect hook supports a return argument of a second function to return when the component unmounts. This lets us clean up any unfinished business, but in particular we'll use it here to break our promise.

1React.useEffect(() => {
2 const isSubscribed = true
3
4 const authPromise = gapi.auth2.init({
5 clientId: CLIENT_ID,
6 })
7
8 authPromise.then(() => {
9 if (isSubscribed) setGoogleReady(true)
10 })
11
12 return () => (isSubscribed = false)
13}, [])
14

We track a variable that remembers if we are still subscribed to the result of this promise. If not, we just don't anything with the results.

Wiring up the Login Button

In login.js, I decided to make a new hook just to reduce the verbosity of the gapi declaration. This is entirely optional but I think it makes for some cleaner code.

1function useAuth() {
2 return gapi.auth2.getAuthInstance()
3}
4

We'll now make a callback to trigger a sign in attempt and pass it down to our button

1export default function () {
2 const onLogin = React.useCallback(() => useAuth().signIn(), [])
3
4 return (
5 <Container>
6 <LoginCard onLogin={onLogin} />
7 </Container>
8 )
9}
10

And we'll grab the prop in our arguments and pass down to the button

1function LoginCard({onLogin}) {
2 return (
3 <Card>
4 <header>
5 <Icon>📺</Icon>
6 <h1>Talk TV</h1>
7 </header>
8 <Divider />
9 <Button primary onClick={onLogin}>
10 LOG IN
11 </Button>
12 </Card>
13 )
14}
15

Now if we rebuild and click our button, we should be passed through Google Auth

Alt Text

And then once we log in, refresh the page and it should now take us through the flow to our homepage

Alt Text

Perfect! One last thing — we should make it automatically refresh after logging in. Replace your onLogin callback with this

1const onLogin = React.useCallback(
2 () =>
3 useAuth()
4 .signIn()
5 .then(() => {
6 // Refresh after sign-in
7 location.reload()
8 }),
9 [],
10)
11

Adding a header

We won't be able to test the login refresh properly until we have a sign out button, so next we'll build a header component that includes one.

Make a new file called app/components/header.js and add a basic layout.

1import React from 'react'
2import styled from 'styled-components'
3
4const Container = styled.div`
5 display: flex;
6 justify-content: center;
7 position: relative;
8`
9
10const HeaderText = styled.h1`
11 margin: 0.25rem 0;
12`
13
14export function Header() {
15 return (
16 <Container>
17 <HeaderText> 📺 Talk TV </HeaderText>
18 </Container>
19 )
20}
21

and then add it to our home.js

1import {Header} from '../../components/header'
2
3export default function () {
4 return (
5 <div>
6 <Header />
7 <h1> Welcome home, logged in user!</h1>
8 </div>
9 )
10}
11

I didn't take a screenshot before I added the Sign Out button, but we should be pretty close to this now.

Alt Text

The Sign Out Button

Back in our header.js component add an actions section for our button and implement a callback. This should be pretty familiar, since it's more or less the same code for our login button back in home.js.

1import {Button} from '../components/button'
2
3const HeaderActions = styled.div`
4 position: absolute;
5 right: 1rem;
6 top: 0.25rem;
7 bottom: 0;
8`
9
10function SignoutButton() {
11 const signOut = React.useCallback(
12 () =>
13 useAuth()
14 .signOut()
15 .then(() => {
16 // Refresh after signout
17 location.reload()
18 }),
19 [],
20 )
21
22 return (
23 <Button inverted onClick={signOut}>
24 Sign Out
25 </Button>
26 )
27}
28
29function useAuth() {
30 return gapi.auth2.getAuthInstance()
31}
32

And then we'll add it to our render return

1export function Header() {
2 return (
3 <Container>
4 <HeaderText> 📺 Talk TV </HeaderText>
5 <HeaderActions>
6 <SignoutButton />
7 </HeaderActions>
8 </Container>
9 )
10}
11

Now if you rebuild it should look like that last screenshot. Click the sign-out button and you should end up on the login scene, ready to log back in without needing to refresh.

Loading...

It's painfully obvious how ugly our loading spinner is, so now is a good time to upgrade it. Luckily the React community has created a number of packages we can use.

Install the react-loader-spinner package

1npm i --save react-loader-spinner
2

and make a new component called loading.js

1import React from 'react'
2import {Container} from './containers'
3import Loader from 'react-loader-spinner'
4import Theme from '../theme'
5
6export function Loading() {
7 return (
8 <Container>
9 <Loader type="Bars" color={Theme.primary} height={100} width={100} />
10 </Container>
11 )
12}
13

Add it to our index.js where our loading text used to be

1import {Loading} from './components/loading'
2
1return isGoogleReady ? <App /> : <Loading />
2

Alt Text

Implementing the Youtube API

Our authentication flow is complete, which means our next step is to start pulling data in from youtube. We don't actually have permission to access anything yet since we didn't request it on login, so we'll fix that first.

Near the top of your login.js file add a constant declaring which scope we want to use. This is a set of permissions that we'll let the user consent to on sign-in.

1const YOUTUBE_SCOPE = 'https://www.googleapis.com/auth/youtube.readonly'
2

and then update the sign in function to use it

1const onLogin = React.useCallback(() => useAuth().signIn({
2 scope: YOUTUBE_SCOPE
3}).then(() => {
4

Log out and back in and it'll ask to get some basic read only permissions to your Youtube account.

Head over to home.js and we'll request a list of live videos from the youtube API. I'm always pretty heavy on the console.logs when adding new code, especially when it comes to an API I've never used before. Add this to the bottom of the file

1function getVideos() {
2 return new Promise((resolve, reject) => {
3 gapi.client.youtube.search
4 .list({
5 part: 'snippet',
6 eventType: 'live',
7 maxResults: 12,
8 q: 'game',
9 type: 'video',
10 })
11 .then((response) => {
12 console.log('GET VIDEOS', response)
13 const items = response.result.items
14
15 if (items) {
16 resolve(items)
17 } else {
18 reject()
19 }
20 })
21 .catch((error) => {
22 console.log('ERROR VIDEOS', error)
23 reject()
24 })
25 })
26}
27

and then implement it with our same subscribed effect pattern we used earlier

1export default function() {
2 React.useEffect(() => {
3 let isSubscribed = true;
4
5 getVideos().then(videos => {
6 if (isSubscribed) {
7 console.log(videos);
8 }
9 });
10
11 return () => isSubscribed = false;
12 }, []);
13

Run that and check your log to see if you get an array of videos. If so, great! If you don't update the login scope (like I forgot to the first time) then you'll get this error

Alt Text

Displaying the Videos

You can design this part any way you want, but I'll just go through the way I built mine step by step

Make a new component called app/components/youtube.js

First we'll need a container to hold them. I'm going to use Flex, but Grid is another viable option. I'm choosing flex because we don't need to structure the elements in specific rows and columns.

If we just used inline-block elements, we could run out a long line of videos that'd wrap to the next line and work perfectly on every resolution. Flex lets us do the same thing, but also expand each element to take up empty space.

All of these will be going into the same file.

1import styled from 'styled-components'
2
3export const BoxGrid = styled.ul`
4 display: flex;
5 flex-wrap: wrap;
6 padding: 1rem;
7`
8

We'll want a clickable container for each video thumbnail. It's possible to add a click listener on the element, but then we need to add our own tab index and it's easier to just use elements designed for navigation. Like links.

1import {Link} from 'react-router-dom'
2import Theme from '../theme'
3
4const Container = styled(Link)`
5 max-width: 100%;
6 flex: 1 0 280px;
7 border-radius: ${(props) => Theme.baseRadius};
8 margin: ${(props) => Theme.baseRadius};
9 position: relative;
10`
11

Each thumbnail will need an image

1const Thumbnail = styled.img`
2 width: 100%;
3 border-radius: ${(props) => Theme.baseRadius};
4 border: ${(props) => `1px solid ${Theme.primary}33`};
5`
6

And below each thumbnail we want to be able to display the title and a watch now button

1import {Button} from './button'
2
3const Details = styled.div`
4 padding: 0.5rem;
5 flex: 0;
6 justify-content: space-between;
7 align-items: center;
8 display: flex;
9`
10
11const Title = styled.span`
12 font-weight: bold;
13`
14
15const Action = styled(Button)`
16 flex: 0;
17`
18

Then we put them together in a component

1function YoutubeThumbnail({id, thumbnail, title}) {
2 return (
3 <Container to={`/watch/${id}`}>
4 <Thumbnail src={thumbnail.url} />
5 <Details>
6 <Title>{title}</Title>
7 <Action inverted>WATCH</Action>
8 </Details>
9 </Container>
10 )
11}
12

Finally, we'll want to export an array of our thumbnails based on the data we got from the API

1import {Loading} from './loading'
2
3export function YoutubeGallery({videos}) {
4 const hasVideos = videos && videos.length
5
6 return hasVideos ? (
7 videos.map((video) => (
8 <YoutubeThumbnail
9 id={video.id.videoId}
10 thumbnail={video.snippet.thumbnails.medium}
11 title={video.snippet.channelTitle}
12 />
13 ))
14 ) : (
15 <Loading wide />
16 )
17}
18

In our Home scene, we'll import these components and update our effect to put the API data into state

1import {BoxGrid, YoutubeGallery} from '../../components/youtube'
2
3export default function () {
4 const [videos, setVideos] = React.useState([])
5
6 React.useEffect(() => {
7 let isSubscribed = true
8
9 getVideos().then((videos) => {
10 if (isSubscribed) setVideos(videos)
11 })
12
13 return () => (isSubscribed = false)
14 })
15
16 return (
17 <div>
18 <Header />
19 <BoxGrid>
20 <YoutubeGallery videos={videos} />
21 </BoxGrid>
22 </div>
23 )
24}
25

Which should look like this when you're all done

Alt Text

The Watch Scene

If you're paying close attention, you'll notice that each thumbnail now links to /watch/${id}

That route doesn't exist yet, but it's about to.

Add a new file in scenes/watch.js and give it a basic component so we can test our routing

1import React from 'react'
2
3export default function () {
4 return <span>Watch Scene!</span>
5}
6

And then add it to our route definitions in index.js

1import WatchScene from './scenes/watch'
2
1<Switch>
2 <Route path="/watch/:id" component={WatchScene} />
3 <Route path="/watch" component={HomeScene} />
4 <Redirect from="/" to="/watch" />
5</Switch>
6

Clicking on any of our thumbnails should give us this now

Alt Text

Lets give our components/youtube.js file one more export

1export const VideoFrame = styled.iframe.attrs(({id}) => ({
2 width: 560,
3 height: 349,
4 frameborder: '0',
5 allowFullScreen: true,
6 src: getEmbedURL(id),
7}))`
8 border-radius: ${(props) => Theme.baseRadius};
9 border: ${(props) => `1px solid ${Theme.primary}33`};
10`
11
12function getEmbedURL(channelId) {
13 return `https://www.youtube.com/embed/${channelId}`
14}
15

and then add it to our watch scene to complete this half of the project

1import {Header} from '../../components/header'
2import {Container} from '../../components/containers'
3import {VideoFrame} from '../../components/youtube'
4
5export default function () {
6 const channelId = document.location.pathname.split('/').pop()
7
8 return (
9 <div>
10 <Header />
11 <Container>
12 <VideoFrame id={channelId} />
13 </Container>
14 </div>
15 )
16}
17

Alt Text

Conclusion

In this tutorial, we've built an application that lets a user log in with their google account, view a list of active live streams, and choose one to watch

In part 2 we'll build our own chat system that the users of your site can use to communicate while watching the videos

The demo for the completed product can be found here: Talk TV on Netlify