DEV Community

Jacob Paris for Devcord

Posted on

Talk TV Tutorial: Watch Youtube Streams and Chat with React

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:

git clone https://github.com/JacobParis/talktv.git
Enter fullscreen mode Exit fullscreen mode

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.

npm init
Enter fullscreen mode Exit fullscreen mode

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

npm i --save react react-dom react-router-dom styled-components
Enter fullscreen mode Exit fullscreen mode

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

npm i --save-dev webpack webpack-cli
Enter fullscreen mode Exit fullscreen mode

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

npm i --save-dev babel-loader @babel/core @babel/preset-env @babel/preset-react
Enter fullscreen mode Exit fullscreen mode

Some plugins to read our HTML and CSS

npm i --save-dev css-loader html-webpack-plugin mini-css-extract-plugin
Enter fullscreen mode Exit fullscreen mode

And a loader for our SCSS code (optional)

npm i --save-dev node-sass sass-loader
Enter fullscreen mode Exit fullscreen mode

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

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.

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.

<!DOCTYPE html>
<html>
    <head>
        <title>๐Ÿ“บ Talk TV</title>
        <link href="https://fonts.googleapis.com/css?family=Montserrat&display=swap" rel="stylesheet">
    </head>
    <body>
        <div id="root"></div>
    </body>
</html> 
Enter fullscreen mode Exit fullscreen mode

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.

import React from "react";
import ReactDOM from "react-dom";

function App() {
    return (
        <h1> ๐Ÿ“บ Talk TV</h1>
    );
}

ReactDOM.render(<App />, document.getElementById("root")); 
Enter fullscreen mode Exit fullscreen mode

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.

"scripts": {
    [...]
    "build": "npx webpack"
}
Enter fullscreen mode Exit fullscreen mode

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

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.

npm run build
Enter fullscreen mode Exit fullscreen mode

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.

"scripts": {
    [...]
    "serve": "npx serve -s dist"
}
Enter fullscreen mode Exit fullscreen mode
npm run serve
Enter fullscreen mode Exit fullscreen mode

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

Arrives on Site:
  - Is logged in:
      Show Gallery Scene
  - Is not logged in:
      Show Login Scene:
        - Prompt for login
        - If successful, refresh
Enter fullscreen mode Exit fullscreen mode

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

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

Create a scenes folder and a login.js file inside. This will be a super simple scene to start.

import React from "react";

 export default function() {
    return (
        <h1> Please log in! </h1>
    );
} 
Enter fullscreen mode Exit fullscreen mode

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

import LoginScene from "./scenes/login";

function App() {
    const isSignedIn = false;

    return isSignedIn ? (
        <h1> ๐Ÿ“บ Talk TV</h1>
    ) : <LoginScene />;
}
Enter fullscreen mode Exit fullscreen mode

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:

import styled from "styled-components";

export const Container = styled.div`
    padding: 2rem;
    display: flex;
    justify-content: center;
`; 
Enter fullscreen mode Exit fullscreen mode

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

import { Container } from "../../components/containers";

export default function() {

    return (
        <Container>
            <h1> Please log in! </h1>
        </Container>
    );
} 
Enter fullscreen mode Exit fullscreen mode

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.

html {
    font-family: 'Montserrat', sans-serif;
}

body {
    margin: 0;
}
Enter fullscreen mode Exit fullscreen mode

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

import "./index.scss";
Enter fullscreen mode Exit fullscreen mode

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

import styled from "styled-components";

export const Card = styled.div`
    color: #333;
    background-color: #fff;
    border: 1px solid black;
    border-radius: 0.5rem;
    padding: 1.5rem;
    width: 90%; 
    max-width: 300px;
    text-align: center;
`;
Enter fullscreen mode Exit fullscreen mode

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

export const Divider = styled.hr`
  width: 50%;
  opacity: 0.2;
  margin-bottom: 2rem;
  margin-top: 0;
`;
Enter fullscreen mode Exit fullscreen mode

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

export const Icon = styled.p`
    font-size: 10rem;
    margin: 0;
    user-select: none;
`;
Enter fullscreen mode Exit fullscreen mode

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

import { Card, Divider, Icon } from "../../components/cards";

export default function() {
    return (
        <Container>
            <LoginCard />
        </Container>
    );
}

function LoginCard() {
    return (
        <Card>
            <header>
                <Icon>๐Ÿ“บ</Icon>
                <h1>Talk TV</h1>
            </header>
            <Divider />
            <div>
                PLEASE LOG IN
            </div>
        </Card>
    );
}
Enter fullscreen mode Exit fullscreen mode

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

export default {
    background: '#ffffff',
    baseFontColor: '#000000',
    baseFontSize: '16px',
    baseRadius: '1rem',
    primary: '#2196f3', // MD Light Blue 500
}
Enter fullscreen mode Exit fullscreen mode

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

import Theme from "../theme";
Enter fullscreen mode Exit fullscreen mode

And then replace our border styles with this

border-radius: ${props => Theme.baseRadius};
border: ${props => `1px solid ${Theme.primary}33`};
Enter fullscreen mode Exit fullscreen mode

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:

cursor: ${props => props.disabled ? "not-allowed" : "pointer"};
Enter fullscreen mode Exit fullscreen mode

And you can trigger that by adding the prop attribute.

<Button disabled>NOT ALLOWED</Button>
Enter fullscreen mode Exit fullscreen mode

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.


import { Button } from "../../components/button";

function LoginCard() {
    return (
        <Card>
            <header>
                <Icon>๐Ÿ“บ</Icon>
                <h1>Talk TV</h1>
            </header>
            <Divider />
            <Button primary>LOG IN</Button>
        </Card>
    );
}
Enter fullscreen mode Exit fullscreen mode

And now we have a button!

Alt Text

Google Authentication

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

Head on over to the Google API Console and click the New Project button in the top left

Alt Text

Alt Text

Once your project is created, go to OAuth Consent Screen in the left sidebar and set our Application Name and Authorized domain.

Alt Text

Alt Text

We'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

Alt Text

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

Alt Text

Now in Dashboard -> Enable APIs and Services search for the Youtube Data API

Alt Text

Alt Text

Add a new API Key. We'll use this to connect to the Youtube API.

Alt Text

Alt Text

If you head back to Credentials, you can see both our API Key and our Client ID ready to go

Integrating into the app

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

<head>
    <title>๐Ÿ“บ Talk TV</title>
    <link href="https://fonts.googleapis.com/css?family=Montserrat&display=swap" rel="stylesheet">
    <script src="https://apis.google.com/js/api.js"></script>
</head>
Enter fullscreen mode Exit fullscreen mode

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

const CLIENT_ID = ASDFASDFASDFASDF;
const API_KEY = QWERQWERQWERQWER;

function App() {
Enter fullscreen mode Exit fullscreen mode

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.

function Preloader() {
    const [isGoogleReady, setGoogleReady] = React.useState(false);

    return isGoogleReady ? <App /> : <div>Loading...</div>;
}

ReactDOM.render(<Preloader />, document.getElementById("root"));
Enter fullscreen mode Exit fullscreen mode

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

React.useEffect(() => {
    const authPromise = gapi.auth2.init({
        clientId: CLIENT_ID,
    });

    authPromise.then(() => {
      setGoogleReady(true);
    });
}, []);

return isGoogleReady ? <App /> : <div>Loading...</div>;
Enter fullscreen mode Exit fullscreen mode

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:

const wasGoogleReady = !!gapi.auth2;
const [isGoogleReady, setGoogleReady] = React.useState(wasGoogleReady);
Enter fullscreen mode Exit fullscreen mode

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.

React.useEffect(() => {
    const isSubscribed = true;

    const authPromise = gapi.auth2.init({
        clientId: CLIENT_ID,
    });

    authPromise.then(() => {
      if (isSubscribed) setGoogleReady(true);
    });

    return () => isSubscribed = false;
}, []);
Enter fullscreen mode Exit fullscreen mode

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.

function useAuth() {
    return gapi.auth2.getAuthInstance();
}
Enter fullscreen mode Exit fullscreen mode

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

export default function() {
    const onLogin = React.useCallback(() => useAuth().signIn(), []);

    return (
        <Container>
            <LoginCard onLogin={onLogin} />
        </Container>
    );
}
Enter fullscreen mode Exit fullscreen mode

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

function LoginCard({onLogin}) {
    return (
        <Card>
            <header>
                <Icon>๐Ÿ“บ</Icon>
                <h1>Talk TV</h1>
            </header>
            <Divider />
            <Button primary onClick={onLogin}>LOG IN</Button>
        </Card>
    );
}
Enter fullscreen mode Exit fullscreen mode

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

const onLogin = React.useCallback(() => useAuth().signIn().then(() => {
    // Refresh after sign-in
    location.reload();
}), []);
Enter fullscreen mode Exit fullscreen mode

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.

import React from 'react';
import styled from 'styled-components';

const Container = styled.div`
    display: flex;
    justify-content: center;
    position: relative;
`;

const HeaderText = styled.h1`
    margin: 0.25rem 0;
`;

export function Header() {
    return (
        <Container>
            <HeaderText> ๐Ÿ“บ Talk TV </HeaderText>
        </Container>
    )
}
Enter fullscreen mode Exit fullscreen mode

and then add it to our home.js

import { Header } from "../../components/header";

export default function() {
    return (
        <div>
            <Header />
            <h1> Welcome home, logged in user!</h1>
        </div>
    );
}
Enter fullscreen mode Exit fullscreen mode

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.

import { Button } from "../components/button";

const HeaderActions = styled.div`
    position: absolute;
    right: 1rem;
    top: 0.25rem;
    bottom: 0;
`;

function SignoutButton() {
    const signOut = React.useCallback(() => useAuth().signOut().then(() => {
        // Refresh after signout
        location.reload();
    }), []);

    return (
        <Button inverted onClick={signOut}>Sign Out</Button>
    );
}

function useAuth() {
    return gapi.auth2.getAuthInstance();
}
Enter fullscreen mode Exit fullscreen mode

And then we'll add it to our render return

export function Header() {
    return (
        <Container>
            <HeaderText> ๐Ÿ“บ Talk TV </HeaderText>
            <HeaderActions>
                <SignoutButton />
            </HeaderActions>
        </Container>
    )
}
Enter fullscreen mode Exit fullscreen mode

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

npm i --save react-loader-spinner
Enter fullscreen mode Exit fullscreen mode

and make a new component called loading.js

import React from "react";
import { Container } from "./containers";
import Loader from "react-loader-spinner";
import Theme from "../theme";

export function Loading() {
    return (
        <Container>
            <Loader
                type="Bars"
                color={Theme.primary}
                height={100}
                width={100}
            />
        </Container>
    )
}
Enter fullscreen mode Exit fullscreen mode

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

import { Loading } from "./components/loading";
Enter fullscreen mode Exit fullscreen mode
return isGoogleReady ? <App /> : <Loading />;
Enter fullscreen mode Exit fullscreen mode

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.

const YOUTUBE_SCOPE = "https://www.googleapis.com/auth/youtube.readonly";
Enter fullscreen mode Exit fullscreen mode

and then update the sign in function to use it

const onLogin = React.useCallback(() => useAuth().signIn({
    scope: YOUTUBE_SCOPE
}).then(() => {
Enter fullscreen mode Exit fullscreen mode

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

function getVideos() {
    return new Promise((resolve, reject) => {
        gapi.client.youtube.search.list({
            "part": "snippet",
            "eventType": "live",
            "maxResults": 12,
            "q": "game",
            "type": "video"
        }).then(response => {
            console.log("GET VIDEOS", response);
            const items = response.result.items;

            if(items) {
                resolve(items);
            } else {
                reject();
            }
        }).catch(error => {
            console.log("ERROR VIDEOS", error);
            reject();
        });
    });
} 
Enter fullscreen mode Exit fullscreen mode

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

export default function() {
    React.useEffect(() => {
        let isSubscribed = true;

        getVideos().then(videos => {
            if (isSubscribed) {
                console.log(videos);
            }
        });

        return () => isSubscribed = false;
    }, []);
Enter fullscreen mode Exit fullscreen mode

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.

import styled from "styled-components";

export const BoxGrid = styled.ul`
    display: flex;
    flex-wrap: wrap;
    padding: 1rem;
`;
Enter fullscreen mode Exit fullscreen mode

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.

import { Link } from "react-router-dom";
import Theme from "../theme";

const Container = styled(Link)`
    max-width: 100%;
    flex: 1 0 280px;
    border-radius: ${props => Theme.baseRadius};
    margin: ${props => Theme.baseRadius};
    position: relative;
`;
Enter fullscreen mode Exit fullscreen mode

Each thumbnail will need an image

const Thumbnail = styled.img`
    width: 100%;
    border-radius: ${props => Theme.baseRadius};
    border: ${props => `1px solid ${Theme.primary}33`};
`;
Enter fullscreen mode Exit fullscreen mode

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

import { Button } from "./button";

const Details = styled.div`
    padding: 0.5rem; 
    flex: 0;
    justify-content: space-between;
    align-items: center;
    display: flex;
`;

const Title = styled.span`
    font-weight: bold;
`;

const Action = styled(Button)`
    flex: 0;
`;
Enter fullscreen mode Exit fullscreen mode

Then we put them together in a component

function YoutubeThumbnail({id, thumbnail, title}) {
    return (
        <Container to={`/watch/${id}`}>
            <Thumbnail src={thumbnail.url} />
            <Details>
                <Title>{title}</Title>
                <Action inverted>WATCH</Action>
            </Details>
        </Container>
    );
}
Enter fullscreen mode Exit fullscreen mode

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

import { Loading } from "./loading";

export function YoutubeGallery({videos}) {
    const hasVideos = videos && videos.length;

    return hasVideos ? videos.map(video => (
        <YoutubeThumbnail
            id={video.id.videoId}
            thumbnail={video.snippet.thumbnails.medium}
            title={video.snippet.channelTitle}
        />
    )) : <Loading wide/>;
} 
Enter fullscreen mode Exit fullscreen mode

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

import { BoxGrid, YoutubeGallery } from "../../components/youtube";

export default function() {
    const [videos, setVideos] = React.useState([]);

    React.useEffect(() => {
        let isSubscribed = true;

        getVideos().then(videos => {
            if (isSubscribed) setVideos(videos);
        });

        return () => isSubscribed = false;
    });

    return (
        <div>
            <Header />
            <BoxGrid>
                <YoutubeGallery videos={videos} />
            </BoxGrid>
        </div>
    );
}
Enter fullscreen mode Exit fullscreen mode

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

import React from "react";

export default function() {
    return (
        <span>Watch Scene!</span>
    );
}
Enter fullscreen mode Exit fullscreen mode

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

import WatchScene from "./scenes/watch";
Enter fullscreen mode Exit fullscreen mode
<Switch>
    <Route path="/watch/:id" component={WatchScene} />
    <Route path="/watch" component={HomeScene} />
    <Redirect from="/" to="/watch" />
</Switch>
Enter fullscreen mode Exit fullscreen mode

Clicking on any of our thumbnails should give us this now

Alt Text

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

export const VideoFrame = styled.iframe.attrs(({id}) => ({
    width: 560,
    height: 349,
    frameborder: "0",
    allowFullScreen: true,
    src: getEmbedURL(id)
}))`
    border-radius: ${props => Theme.baseRadius};
    border: ${props => `1px solid ${Theme.primary}33`};
`;

function getEmbedURL(channelId) {
    return `https://www.youtube.com/embed/${channelId}`;
}
Enter fullscreen mode Exit fullscreen mode

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

import { Header } from "../../components/header";
import { Container } from "../../components/containers";
import { VideoFrame } from "../../components/youtube";

export default function() {
    const channelId = document.location.pathname.split('/').pop();

    return (
        <div>
            <Header />
            <Container>
                <VideoFrame id={channelId} />
            </Container>
        </div>
    );
} 
Enter fullscreen mode Exit fullscreen mode

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

Top comments (0)