Chapter 7: Streaming

Strongly Typed Next.js

link Stream Flow

By the end of this lesson, developers will be able to:

  • Create components and pages to handle data fetching
  • Create a streaming flow for creating and viewing streams

Introduction

With the authentication flow completed, we will move on to create a streaming flow. Bear in mind, we haven't implemented a streaming flow. First, we will scaffold the components to create and view streams.

link Posts Component

To display the users posted streams, we will create a list component called Posts.

Create a new file, app/components/Posts.tsx, and insert the following:

import Typography from '@material-ui/core/Typography';
import Grid from '@material-ui/core/Grid';
import Card from '@material-ui/core/Card';
import CardActionArea from '@material-ui/core/CardActionArea';
import CardContent from '@material-ui/core/CardContent';
import CardMedia from '@material-ui/core/CardMedia';
import Hidden from '@material-ui/core/Hidden';
import Link from 'next/link';
import { makeStyles, Theme } from '@material-ui/core/styles';
import { Stream } from '../lib/graphql/streams.graphql';

interface Props {
  streams: Stream[];
}

export default function Posts(props: Props) {
  const styles = useStyles();
  const { streams } = props;

  return (
    <Grid container className={styles.container} spacing={4}>
      {streams.map((post) => (
        <Grid item key={post._id} xs={12} md={6}>
          <Link href={`/streams/${post._id}`}>
            <CardActionArea component="a" href="#">
              <Card className={styles.card}>
                <div className={styles.cardDetails}>
                  <CardContent>
                    <Typography
                      component="h2"
                      variant="h5"
                      className={styles.cardText}
                    >
                      {post.title}
                    </Typography>
                    <Typography
                      noWrap={true}
                      variant="subtitle1"
                      color="textSecondary"
                      className={styles.cardText}
                    >
                      {post.url}
                    </Typography>
                    <Typography
                      variant="subtitle1"
                      paragraph
                      className={styles.cardText}
                    >
                      {post.description}
                    </Typography>
                  </CardContent>
                </div>
                <Hidden xsDown>
                  <CardMedia
                    className={styles.cardMedia}
                    image="https://source.unsplash.com/random"
                    title="Image title"
                  />
                </Hidden>
              </Card>
            </CardActionArea>
          </Link>
        </Grid>
      ))}
    </Grid>
  );
}

const useStyles = makeStyles((theme: Theme) => ({
  container: {
    marginTop: theme.spacing(4),
  },
  card: {
    display: 'flex',
  },
  cardDetails: {
    flex: 1,
  },
  cardText: {
    maxWidth: '26rem',
  },
  cardMedia: {
    width: 160,
  },
}));

link Hero Component

On each stream's detail page, we will display a banner or "hero" component called Hero.

Create a new file, app/components/Hero.tsx, and insert the following:

import Typography from '@material-ui/core/Typography';
import Link from 'next/link';
import Button from '@material-ui/core/Button';
import Box from '@material-ui/core/Box';
import Grid from '@material-ui/core/Grid';
import Paper from '@material-ui/core/Paper';
import { makeStyles } from '@material-ui/core/styles';
import { Stream } from '../lib/graphql/streams.graphql';
import { useAuth } from 'lib/useAuth';

interface Props {
  stream: Stream;
}

export default function Hero({ stream }: Props) {
  const styles = useStyles();
  const { user } = useAuth();

  const showEdit =
    user &&
    user._id === stream.author._id;

  return (
    <Paper className={styles.mainFeaturedPost}>
      <div className={styles.overlay} />
      <Grid container>
        <Grid item md={6}>
          <div className={styles.mainFeaturedPostContent}>
            <Typography
              component="h1"
              variant="h3"
              color="inherit"
              gutterBottom
            >
              {stream.title}
            </Typography>
            <Typography variant="h5" color="inherit" paragraph>
              {stream.description}
            </Typography>
            <Box pb={1} />
            {showEdit && (
              <Link href={`edit/${stream._id}`}>
                <Button variant="outlined" color="inherit">
                  Edit Stream
                </Button>
              </Link>
            )}
          </div>
        </Grid>
      </Grid>
    </Paper>
  );
}

const useStyles = makeStyles((theme) => ({
  toolbar: {
    borderBottom: `1px solid ${theme.palette.divider}`,
  },
  toolbarTitle: {
    flex: 1,
  },
  mainFeaturedPost: {
    position: 'relative',
    backgroundColor: theme.palette.grey[800],
    color: theme.palette.common.white,
    marginBottom: theme.spacing(4),
    backgroundImage: 'url(https://source.unsplash.com/random)',
    backgroundSize: 'cover',
    backgroundRepeat: 'no-repeat',
    backgroundPosition: 'center',
  },
  overlay: {
    position: 'absolute',
    top: 0,
    bottom: 0,
    right: 0,
    left: 0,
    backgroundColor: 'rgba(0,0,0,.7)',
  },
  mainFeaturedPostContent: {
    position: 'relative',
    padding: theme.spacing(3),
    [theme.breakpoints.up('md')]: {
      padding: theme.spacing(6),
      paddingRight: 0,
    },
  },
}));

link Content Component

On each stream's detail page, we will also display content based on the current stream's url, called Content.

Create a new file, app/components/Content.tsx, and insert the following:

import { makeStyles } from '@material-ui/core/styles';

type VideoProps = {
  url: string;
};

export default function Video({ url }: VideoProps) {
  const classes = useStyles();

  return (
    <div className={classes.container}>
      <iframe
        className={classes.iframe}
        src={url}
        frameBorder="0"
        allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
        allowFullScreen
        loading="lazy"
      />
    </div>
  );
}

const useStyles = makeStyles(() => ({
  container: {
    // overflow: 'hidden',
    /* 16:9 aspect ratio */
    paddingTop: '56.25%',
    position: 'relative',
  },
  iframe: {
    border: '0',
    height: '100%',
    left: '0',
    position: 'absolute',
    top: '0',
    width: '100%',
  },
}));

With our components completed, we can move onto their supporting screens.

link Streams Page

Create a new directory and file, app/pages/streams and app/pages/streams/index.tsx, and insert the following:

import { useEffect } from 'react';
import Container from '@material-ui/core/Container';
import Typography from '@material-ui/core/Typography';
import Box from '@material-ui/core/Box';

import Posts from 'components/Posts';
import { useStreamsQuery, Stream } from 'lib/graphql/streams.graphql';

export default function Streams() {
  const { data, loading, refetch } = useStreamsQuery({ errorPolicy: 'ignore' });

  useEffect(() => {
    refetch();
  }, []);

  return (
    <Container maxWidth="lg">
      <Box my={4}>
        <Typography variant="h4">Streams</Typography>
      </Box>
      {!loading && data && data.streams && (
        <Posts streams={data.streams as Stream[]} />
      )}
    </Container>
  );
}

link Stream Detail Page

Create a new directory and file, app/pages/streams/[id] and app/pages/streams/[id]/index.tsx, and insert the following:

import Container from '@material-ui/core/Container';

import Hero from 'components/Hero';
import Content from 'components/Content';
import { useStreamQuery, Stream } from 'lib/graphql/stream.graphql';

export default function StreamDetail({ id }) {
  const { data, loading } = useStreamQuery({
    variables: { streamId: id },
  });

  if (!loading && data && data.stream) {
    return (
      <Container maxWidth="lg">
        <Hero stream={data.stream as Stream} />
        <Content url={data.stream.url} />
      </Container>
    );
  }

  return null;
}

StreamDetail.getInitialProps = ({ query: { id } }) => {
  return { id };
};

link Stream Create Page

Create a new file, app/pages/streams/new.tsx and insert the following:

import React, { useState } from 'react';
import { useRouter } from 'next/router';
import { useCreateStreamMutation } from 'lib/graphql/createStream.graphql';
import Container from '@material-ui/core/Container';
import TextField from '@material-ui/core/TextField';
import Typography from '@material-ui/core/Typography';
import Box from '@material-ui/core/Box';
import Button from '@material-ui/core/Button';

export default function CreateStream() {
  const [title, setTitle] = useState('');
  const [description, setDescription] = useState('');
  const [url, setUrl] = useState('');
  const router = useRouter();

  // Signing In
  const [createStream] = useCreateStreamMutation();

  const onSubmit = async (event) => {
    event.preventDefault();

    try {
      const { data } = await createStream({
        variables: { input: { title, description, url } },
      });
      if (data.addStream._id) {
        router.push('/streams');
      }
    } catch (err) {
      console.log(err);
    }
  };

  return (
    <Container maxWidth="sm">
      <Box my={4}>
        <Typography variant="h4">Create Stream</Typography>
        <form onSubmit={onSubmit}>
          <Box pb={2.5} />
          <TextField
            autoFocus
            label="Title"
            value={title}
            onChange={(e) => setTitle(e.target.value)}
            required
          />
          <Box pb={2.5} />
          <TextField
            label="Description"
            value={description}
            onChange={(e) => setDescription(e.target.value)}
            required
          />
          <Box pb={2.5} />
          <TextField
            label="URL"
            value={url}
            onChange={(e) => setUrl(e.target.value)}
            required
          />
          <Box pb={2.5} />
          <Button type="submit" variant="contained" color="primary">
            Create Stream
          </Button>
        </form>
      </Box>
    </Container>
  );
}

link Stream Edit Page

Create a new directory and file, app/pages/streams/edit/[id] and app/pages/streams/edit/[id]/index.tsx, and insert the following:

import React, { useState, useEffect } from 'react';
import { useRouter } from 'next/router';
import { initializeApollo } from 'lib/apollo';
import { useEditStreamMutation } from 'lib/graphql/editStream.graphql';
import { useDeleteStreamMutation } from 'lib/graphql/deleteStream.graphql';
import { StreamDocument } from 'lib/graphql/stream.graphql';
import Container from '@material-ui/core/Container';
import TextField from '@material-ui/core/TextField';
import Typography from '@material-ui/core/Typography';
import Box from '@material-ui/core/Box';
import Button from '@material-ui/core/Button';

export default function EditStream({ id }) {
  const router = useRouter();
  const [editStream] = useEditStreamMutation();
  const [deleteStream] = useDeleteStreamMutation();

  const [state, setState] = useState({
    _id: '',
    title: '',
    description: '',
    url: '',
  });

  const { _id, title, description, url } = state;

  const fetchStream = async () => {
    const apollo = initializeApollo();
    const { data } = await apollo.query({
      query: StreamDocument,
      variables: { streamId: id },
    });
    setState(data.stream);
  };

  useEffect(() => {
    fetchStream();
  }, []);

  const onSubmit = async (event) => {
    event.preventDefault();

    try {
      const { data } = await editStream({
        variables: { input: { id: _id, title, description, url } },
      });
      if (data.editStream._id) {
        router.push('/streams');
      }
    } catch (err) {
      console.log(err);
    }
  };

  const onDelete = async (event) => {
    event.preventDefault();

    try {
      const { data } = await deleteStream({
        variables: { id },
      });
      if (data.deleteStream) {
        router.push('/streams');
      }
    } catch (err) {
      console.log(err);
    }
  };

  return (
    <Container maxWidth="sm">
      <Box my={4}>
        <Typography variant="h4">Edit Stream</Typography>
        <form onSubmit={onSubmit}>
          <Box pb={2.5} />
          <TextField
            autoFocus
            label="Title"
            value={title}
            onChange={(e) => setState({ ...state, title: e.target.value })}
            required
          />
          <Box pb={2.5} />
          <TextField
            label="Description"
            value={description}
            onChange={(e) =>
              setState({ ...state, description: e.target.value })
            }
            required
          />
          <Box pb={2.5} />
          <TextField
            label="URL"
            value={url}
            onChange={(e) => setState({ ...state, url: e.target.value })}
            required
          />
          <Box pb={2.5} />
          <Button type="submit" variant="contained" color="primary">
            Save
          </Button>
          <Box pb={2.5} />
          <Button onClick={onDelete} variant="contained">
            Delete
          </Button>
        </form>
      </Box>
    </Container>
  );
}

EditStream.getInitialProps = ({ query: { id } }) => {
  return { id };
};

link Test Streaming

It's time to fire up the Next.js app, GraphQL server and test the new streams flow at http://localhost:3000/streams

Heads Up! Your GraphQL API server needs to be running in order to test the frontend.

npm run dev

Try reloading the Next.js application, and you will be able to navigate to a "Create Stream" page.

Be sure you are able to:

  • Create a new stream
  • View your list of streams
  • View an individual stream

Congratulations!

Today we built data-fetching queries and created the streaming flow. Up next, we will prepare our application for deployment.

link References

format_list_bulleted
help_outline