How to Build a Podcast App with Next.js and Strapi

How to Build a Podcast App with Next.js and Strapi

This tutorial will build a simple podcast app to demonstrate how we can host a podcast API on Strapi and fetch from a Next.js app.

Author: Isaac Okoro

This tutorial will build a simple podcast app to demonstrate how we can host a podcast API on Strapi and fetch from a Next.js app.

Goals

In this article, we will learn how to:

  • Bootstrap a Next.js app,
  • Scaffold a Strapi backend,
  • Build complex collections on Strapi, and
  • Connect the Strapi backend with a Next.js frontend.

Prerequisites

Before we begin, make sure you have the below tools installed in your machine.

  • Node.js: This is the first tool you will need because both Next.js and Strapi are based on Nodejs. You can download it here.

  • NPM: This is a Node package manager. It comes bundled with the Nodejs binary.

  • Yarn: This is a high-speed Node package manager. We will need it when scaffolding the Strapi backend.

  • VS Code: The world's most used web editor. You can use any web editor if you have one. VS Code is specially made for web development and makes work very easy. You can download it here.

What is Strapi?

Strapi is a headless CMS (Content Management System) that's based on Node.js and used to build APIs. Strapi provides a UI where we can develop our APIs using what is called collections. A collection offers endpoints where we can perform CRUD actions on the resource.

Strapi is self-hosted in the sense that it hosts the endpoints, the server code, and the backend for us. And you can use your database because Strapi provides a configuration to connect to another backend. Strapi is just like a server and database bundled together.

Also, Strapi is open-source. It is maintained by hundreds of contributors worldwide. So Strapi has enormous support and constantly maintained.

By default, Strapi provides the collections in RESTful endpoints, but it also supports GraphQL. With the Strapi GraphQL plugin, it will serve the collections in GraphQL endpoints.

The endpoints can be used from mobile, desktop, or web. In our case, our web is built using React.js so that we will communicate with the endpoints from our frontend using an HTTP library.

Setting up Strapi Backend

Let's set up the main folder that will contain our Strapi backend project and our React frontend project.

    mkdir podcast-app

Move inside the folder:

    cd podcast-app
``

Now, let's scaffold our Strapi project:

```bash
    yarn create strapi-app podcast-api --quickstart

What your scren should look like

What your scren should look like

This creates a Strapi folder in the podcast-api folder. Strapi will go to install the dependencies and also run strapi develop to start the Strapi server. Strapi will open a browser and will navigate to http://localhost:1337/admin/auth/register-admin

What your screen should look like

This form is where we will sign up before we use Strapi. Fill in the input boxes and click on “LET’S START” button. The admin panel loads.

What your screen should look like

Creating Collections

We will begin creating our collections, but before we do that, let's see the model of our collections.

Podcast Model

Our podcast model will be this:

    podcast {
        name
        author
        imageUrl
        episodes [{
            name
            mp3Link
        }]
    }

Each podcast will have a name, an image on the internet, then episodes. Each episode will have a name and an mp3Link on the internet.

The relationship between podcasts and episodes is a one-to-many relationship. A podcast will have many episodes. So we will create an "episodes" collection and a "podcasts" collection. Then, we will use the Strapi relation field to set the relationship.

Create Episodes Collection

Let's create the "episodes" collection first.

  • Click on the "CREATE YOUR FIRST CONTENT-TYPE" button to create a new collection. A "Create a collection type" modal will appear in the "Display name" input box type in "episodes."

  • Then, click on "Continue". What your screen should look like

  • Now, we begin to set the fields and their field types for our "episodes" collection. On the UI that appears, select "Text,"

What your screen should look like

  • On the "Add new Text field," type "name" on the input box. What your screen should look like

  • Then, click on “+ Add another field”.

  • Select “Text”.

What your screen should look like

  • Type “mp3Link”.

What your screen should look like

  • Click on “Finish” on the “Episodes” page that appears.

What your screen should look like

  • Click on “Save”. This saves the “episodes” collection.

What your screen should look like

Now, we create the “podcasts” collection.

Create Podcast Collection

  • On the “Episodes” page we are at now, click on “+ Create new collection type”.
  • On the “Create a collection type” modal that shows up, type “podcasts”

What your screen should look like

  • Click on “Continue”.
  • On the “Podcasts” modal, click on “Text”

What your screen should look like

  • On the “Add new Text field”, type “name” on the input box.

What your screen should look like

  • Then, click on “+ Add another field”.

What your screen should look like

  • Select “Text” and in the next UI type “author”.

What your screen should look like

  • Do the same for “imageUrl”.

What your screen should look like

  • On the “Select a field for your collection type” that shows up, click on “Relation”.

What your screen should look like

  • See the links between the "Podcasts" and "Episodes." That's all the relationships that we can choose. Click on the last link. It is the relationship link to make a podcast to have many episodes.

What your screen should look like

  • See the text changes to "Podcast has many Episodes." That's the relationship we want.
  • Click on "Finish."

What your screen should look like

  • Click on the "Save" button on the top-right to save our collection fields. We have created our collections with relationships. Let's open access for both authenticated users and the public.

Open Access

  • On the sidebar, click on "Settings," then on the second sidebar that opens to the right, click on "Roles” under the "Users & Permissions Plugin” section.

A Sample Screenshot

  • Then, click on "Public," and then scroll down to the "Permissions" section, and check the checkboxes on both "EPISODES" and "PODCASTS."

A Sample Screenshot

  • Click on the “Save” button to save the permissions.

A Sample Screenshot

Seed the Database with Mock Data

We have to insert fake data into our database. We will first have to add episode data. To do that:

  • Go to “Content Manager” and click on the "episode" link on the sidebar.

A Sample Screenshot

  • Click on the "+ Add New Episodes" button on the top-right. Now, we enter our episode data.

A Sample Screenshot

name -> Episode 1 - React
mp3Link -> mp3-link.mp3
  • Click on the “Save” and then the “Publish” button.

A Sample Screenshot

This makes our changes go live.

  • Click on the “<-” button on the top-left to go back.

A Sample Screenshot

See that our input is there.

  • Add the below episodes yourself.
    name -> Episode 2 - React
    mp3Link -> mp3-link.mp3

    name -> Episode 3 - React
    mp3Link -> mp3-link.mp3

    name -> Episode 4 - React
    mp3Link -> mp3-link.mp3

    name -> Episode 4 - Angular
    mp3Link -> mp3-link.mp3

    name -> Episode 4 - Angular
    mp3Link -> mp3-link.mp3

A Sample Screenshot

Now, we add podcasts data.

  • Click on the “podcast” link on the sidebar.

A Sample Screenshot

  • Click on the “+ Add New Podcasts”.
  • Type in the data:
    name -> React Podcast
    author -> Chidume Nnamdi
    imageUrl -> https://terrigen-cdn-dev.marvel.com/content/prod/1x/ae_digital_packshot.jpg

A Sample Screenshot

We have to add the episodes. See that in the middle-right section, we have a "Episodes (0)" box. This is where we will add the episodes to the podcast.

  • Click on the dropdown box, and you will see all our episodes data cascade down.

A Sample Screenshot

  • Click on an episode to add it to the podcast.

A Sample Screenshot

  • Now, add all “React” episodes.

A Sample Screenshot

  • Now, click on the “Save” button, and then “Publish” button when it becomes enabled.

A Sample Screenshot

  • Let’s add another podcast, go back and click on the “+ Add New Podcasts”.

  • Add the data:

name -> Angular Podcast

imageUrl -> encrypted-tbn1.gstatic.com/images?q=tbn:ANd..

author -> Chidume Nnamdi

A Sample Screenshot

  • On episodes, add the “Angular” episodes.

A Sample Screenshot

  • Click on “Save” and then on “Publish

A Sample Screenshot

Testing the Strapi Endpoints

In this stage, we will be using Postman.

Get All Podcasts
  • Type in http://localhost:1337/api/podcasts?populate=* and click on "Send".

    N.B: the ?populate=* flag makes it possible to retrieve the relationships i.e the episodes for each podcast

A Sample Screenshot

A Sample Screenshot

Understanding Our JSON Response

Our get request returns a json response of an object of data. The data object contains an array of objects(podcasts). Retreiving each podcast will require iterating into the artrributes object in each podcast.

Get a Podcast
  • Let’s get the “Angular” podcast. Its ID is 3, so we type http://localhost:1337/api/podcasts4?populate=* and click "Send".

A Sample Screenshot

Setting up Next.js Project

We are done with the backend; it's time to build our frontend and see how we will call the endpoints provided to us by Strapi.

  • So we begin by creating a Next.js project:

      yarn create next-app podcast-strapi
    

    This will create a Next.js project on a podcast-strapi folder with all its dependencies installed.

  • Move into the folder:

      cd podcast-strapi
    
  • Start the Next.js development server.

      yarn dev
    

    This will server our Next.app at localhost:3000.

  • Open your browser and navigate to localhost:3000; you will see the Next.js default page.

A Sample Screenshot

Building the Next.js App

Now, we'll build the app, which will have two routes:

  • /: This is the default home route. This page will display all the podcasts.
  • podcast/:id: This route will render a page that will display a podcast with its episodes.

This is how our app will look like when we are done:

Podcasts List View

A Sample Screenshot

View a Podcast

A Sample Screenshot

Add a Podcast

A Sample Screenshot

We will break our app into components before we start building, including the following:

  • Header Component: This will hold the header section of our app.
  • Postcard Component: This component will render a single podcast on the default page that renders all podcasts. This component will display a mini-detail about a podcast.
  • EpisodeCard Component: This component renders a single episode with its complete details.
  • AddPodcastDialog Component: This component is a dialog component that will render UI used to add new podcasts.

A Sample Screenshot

A Sample Screenshot

A Sample Screenshot

In our pages folder, we will have index.js files there. This will render the default homepage. We will edit this file to render the podcasts.

Next, in the pages, we will create a folder podcast, and inside the folder, we will create a [id].js.

First, we will need the Axios module for making HTTP requests.

    yarn add axios
pages/index.js

This file has the Home component. We will fetch all the podcasts using Axios from our Strapi when the component loads and render them in a list.

We will have a button when clicked it will display the AddPodcastDialog modal.

    import Head from "next/head";
    import styles from "../styles/Home.module.css";
    import PodCard from "../components/PodCard";
    import { useEffect, useState } from "react";
    import axios from "axios";
    import AddPodcastDialog from "../components/AddPodcastDialog";

    export default function Home() {
      const [podcasts, setPodcasts] = useState([]);
      const [showModal, setShowModal] = useState(false);
      useEffect(async () => {
        const data = await axios.get(
          "http://localhost:1337/api/podcasts?populate=*"
        );
        setPodcasts(data?.data?.data);
      }, []);
      function showAddPodcastDialog() {
        setShowModal(!showModal);
      }
      return (
        <div className={styles.container}>
          <Head>
            <title>Podcast</title>
            <link rel="icon" href="/favicon.ico" />
          </Head>
          <main className={styles.main}>
            <div className={styles.breadcrumb}>
              <h2>Hello, Good Day.</h2>
              <span>
                <button onClick={showAddPodcastDialog}>Add Podcast</button>
              </span>
            </div>
            <div className={styles.podcontainer}>
              <div className={styles.yourpodcasts}>
                <h3>Your Podcasts</h3>
              </div>
              <div>
                {podcasts.map((podcast, i) => (
                  <PodCard key={i} id={podcast.id} podcast={podcast.attributes} />
                ))}
              </div>
            </div>
            {showModal ? (
              <AddPodcastDialog closeModal={showAddPodcastDialog} />
            ) : null}
          </main>
        </div>
      );
    }

We will come to the PodCard and AddPodcastDialog components later. We have two states that hold the podcast array and the modal show state.

The podcasts are fetched inside the useEffect callback and stored in the podcast state array. See that we used the GET HTTP method, and the URL "http://localhost:1337/api/podcasts?populate=*" is passed. This gets all the podcasts in our Strapi.

The showAddPodCastDialog function toggles the modal's show state. It makes the modal appear and disappear.

The podcasts are mapped through, and each is displayed on the PodCard component. Each podcast is passed to the PodCard component via its podcast input.

The showModal boolean state is used to determine whether to display the AddPodcastDialog modal or not. The showAddPodcastDialog function is passed to it via closeModal. With this, the component can close itself by calling the closeModal.

The styling for this page component is at styles/Home.module.css. Open it and paste the CSS code:

    .container {
      min-height: 100vh;
      display: flex;
      flex-direction: column;
      justify-content: center;
      align-items: center;
      background-color: rgba(234, 238, 243, 1);
    }
    .main {
      flex: 1;
      display: flex;
      flex-direction: column;
      width: 62%;
    }
    .podcontainer {
      background-color: white;
      padding: 15px;
    }
    .yourpodcasts {
      color: darkgrey;
      border-bottom: 1px solid rgba(232, 232, 232, 1);
      padding-bottom: 5px;
    }
    .breadcrumb {
      display: flex;
      justify-content: space-between;
      align-items: center;
    }

We now see how we communicate with our Strapi backend to fetch data to display on our frontend.

pages/podcast/[id].js

Now, let’s code the podcast view component. The [id], tells Next.js that this is a dynamic route and that this file should be loaded when the routes like below are navigated to.

This component will retrieve the id param value using the useRouter hook. Then, we will use the id value to fetch the podcast from the Strapi podcasts http://localhost:1337/api/podcasts/${id}?populate=* endpoint via the GET HTTP method. With the podcast data, we render all the details.

    import styles from "../../styles/PodCastView.module.css";
    import { useRouter } from "next/router";
    import EpisodeCard from "../../components/EpisodeCard";
    import axios from "axios";
    import { useEffect, useState } from "react";

    export default function PodCastView() {
      const router = useRouter();
      const {
        query: { id },
      } = router;

      const [podcast, setPodcast] = useState();
      useEffect(async () => {
        const data = await axios.get(
          `http://localhost:1337/api/podcasts/${id}?populate=*`
        );
        setPodcast(data?.data?.data);
      }, [id]);

      async function deletePodcast() {
        if (confirm("Do you really want to delete this podcast?")) {
          // delete podcast episodes
          const episodes = podcast?.attributes?.episodes.data;
          for (let index = 0; index < episodes.length; index++) {
            const episode = episodes[index];
            await axios.delete("http://localhost:1337/api/episodes/" + episode?.id);
          }
          await axios.delete("http://localhost:1337/api/podcasts/" + id);
          router.push("/");
        }
      }

      return (
        <div className={styles.podcastviewcontainer}>
          <div className={styles.podcastviewmain}>
            <div
              style={{ backgroundImage: `url(${podcast?.attributes.imageUrl})` }}
              className={styles.podcastviewimg}
            ></div>
            <div style={{ width: "100%" }}>
              <div className={styles.podcastviewname}>
                <h1>{podcast?.attributes.name}</h1>
              </div>
              <div className={styles.podcastviewminidet}>
                <div>
                  <span style={{ marginRight: "4px", color: "rgb(142 142 142)" }}>
                    Created by:
                  </span>
                  <span style={{ fontWeight: "600" }}>
                    {podcast?.attributes.author}
                  </span>
                </div>
                <div style={{ padding: "14px 0" }}>
                  <span>
                    <button onClick={deletePodcast} className="btn-danger">
                      Delete
                    </button>
                  </span>
                </div>
              </div>
              <div className={styles.podcastviewepisodescont}>
                <div className={styles.podcastviewepisodes}>
                  <h2>Episodes</h2>
                </div>
                <div className={styles.podcastviewepisodeslist}>
                  {podcast?.attributes.episodes.data.map((episode, i) => (
                    <EpisodeCard key={i} episode={episode.attributes} />
                  ))}
                </div>
              </div>
            </div>
          </div>
        </div>
      );
    }

We retrieved the id param value using the useRouter hook, then set a state to hold the podcast value.

See that in the useEffect callback function, we used the podcast to get the podcast's details from the Strapi http://localhost:1337/api/podcasts/${id}?populate=* URL, the data is then set in the podcast state.

The deletePodcast function deletes the podcast and its episodes. The episodes are looped through and each is deleted by calling the Strapi URL "http://localhost:1337/episodes/" + episode?.id via the HTTP DELETE method.

Then, the podcast is deleted also by calling the Strapi URL "http://localhost:1337/podcasts/" + id via the HTTP DELETE method. Then, the default page is loaded since the podcast is no longer available.

The UI renders the podcast details. The episodes are mapped through and each episode is rendered in the EpisodeCard component. The episode.attribute is passed to it via the episode input. The EpisodeCard component will access the episode details via the episode in its props to display the info.

The Delete button calls the deletePodcast function to delete the podcast.

This page component has it own module styling at styles folder styles/PodCastView.module.css.

We retrieved the id param value using the useRouter hook, then set a state to hold the podcast value.

See that in the useEffect callback function. We used the podcast to get the podcast's details from the Strapi http://localhost:1337/api/podcasts/${id}?populate=* URL. The data is then set in the podcast state.

The deletePodcast function deletes the podcast and its episodes. The episodes are looped through, and each is deleted by calling the Strapi URL "http://localhost:1337/episodes/" + episode?.id via the HTTP DELETE method.

Then, the podcast is also deleted by calling the Strapi URL "http://localhost:1337/podcasts/" + id via the HTTP DELETE method. Then, the default page is loaded since the podcast is no longer available.

The UI renders the podcast details. The episodes are mapped through, and each episode is rendered in the EpisodeCard component. The episode is passed to it via the episode input. The EpisodeCard component will access the episode details via the episode in its props to display the info.

The Delete button calls the deletePodcast function to delete the podcast.

This page component has it own module styling at styles folder styles/PodCastView.module.css.

Open it and add the styling:

    .podcastviewcontainer {
      min-height: 100vh;
      display: flex;
      flex-direction: column;
      justify-content: center;
      align-items: center;
      background-color: rgba(234, 238, 243, 1);
    }
    .podcastviewimg {
      width: 200px;
      height: 300px;
      background-color: darkgray;
      margin-right: 9px;
      margin-top: 28px;
      background-repeat: no-repeat;
      background-size: cover;
      background-position: center;
    }
    .podcastviewmain {
      flex: 1;
      display: flex;
      flex-direction: row;
      width: 62%;
    }
    .podcastviewepisodeslist {
      background-color: white;
      padding: 15px;
    }

Now, let’s look at the components.

First, create a components folder at the root folder.

AddPostcastDialog Component
  • Create a AddPodcastDialog folder, and inside the folder create a index.js file. Inside this file, add the code:
    import { useState } from "react";
    import EpisodeCard from "../EpisodeCard";
    import axios from "axios";

    export default function AddPodcastDialog({ closeModal }) {
      const [episodes, setEpisode] = useState([]);
      const [disable, setDisable] = useState(false);
      async function savePodcast() {
        setDisable(true);
        const podcastName = window.podcastName.value;
        const podcastImageUrl = window.podcastImageUrl.value;
        const podcastAuthor = window.podcastAuthor.value;
        const episodeIds = [];
        // add all the episodes, get their ids and use it to save the podcast
        for (let index = 0; index < episodes.length; index++) {
          const episode = episodes[index];
          const data = await axios.post("http://localhost:1337/api/episodes", {
            data: {
              ...episode,
            },
          });
          episodeIds.push(data?.data.data.id);
        }
        // add podcast
        addPodcast(episodeIds, podcastName, podcastAuthor, podcastImageUrl);
      }
      function addPodcast(episodeIds, podcastName, podcastAuthor, podcastImageUrl) {
        axios.post("http://localhost:1337/api/podcasts", {
          data: {
            name: podcastName,
            author: podcastAuthor,
            imageUrl: podcastImageUrl,
            episodes: episodeIds,
          },
        });
        setDisable(false);
        closeModal();
        location.reload();
      }
      function addEpisode() {
        const episodeName = window.episodeName.value;
        const episodeMp3Link = window.episodeMp3Link.value;
        setEpisode([...episodes, { name: episodeName, mp3Link: episodeMp3Link }]);
      }
      function removeEpisode(index) {
        setEpisode(episodes.filter((episode, i) => i != index));
      }
      return (
        <div className="modal">
          <div className="modal-backdrop" onClick={closeModal}></div>
          <div className="modal-content">
            <div className="modal-header">
              <h3>Add New Podcast</h3>
              <span
                style={{ padding: "10px", cursor: "pointer" }}
                onClick={closeModal}
              >
                X
              </span>
            </div>
            <div className="modal-body content">
              <div style={{ display: "flex", flexWrap: "wrap" }}>
                <div className="inputField">
                  <div className="label">
                    <label>Name</label>
                  </div>
                  <div>
                    <input id="podcastName" type="text" />
                  </div>
                </div>
                <div className="inputField">
                  <div className="label">
                    <label>ImageUrl</label>
                  </div>
                  <div>
                    <input id="podcastImageUrl" type="text" />
                  </div>
                </div>
                <div className="inputField">
                  <div className="label">
                    <label>Author</label>
                  </div>
                  <div>
                    <input id="podcastAuthor" type="text" />
                  </div>
                </div>
              </div>
              <div style={{ display: "flex", flexDirection: "column" }}>
                <div>
                  <h4>Add Episodes</h4>
                </div>
                <div
                  style={{
                    display: "flex",
                    alignItems: "flex-end",
                    border: "1px solid rgb(212 211 211)",
                    paddingBottom: "4px",
                  }}
                >
                  <div className="inputField">
                    <div className="label">
                      <label>Episode Name</label>
                    </div>
                    <div>
                      <input id="episodeName" type="text" />
                    </div>
                  </div>
                  <div className="inputField">
                    <div className="label">
                      <label>MP3 Link</label>
                    </div>
                    <div>
                      <input id="episodeMp3Link" type="text" />
                    </div>
                  </div>
                  <div style={{ flex: "0" }} className="inputField">
                    <button onClick={addEpisode}>Add</button>
                  </div>
                </div>
                <div
                  style={{
                    height: "200px",
                    overflowY: "scroll",
                    borderTop: "1px solid darkgray",
                    borderBottom: "1px solid darkgray",
                    margin: "8px 0",
                  }}
                >
                  {episodes?.map((episode, i) => (
                    <div
                      style={{
                        display: "flex",
                        alignItems: "center",
                        justifyContent: "space-between",
                      }}
                    >
                      <EpisodeCard episode={episode} key={i} />
                      <div>
                        <button
                          className="btn-danger"
                          onClick={() => removeEpisode(i)}
                        >
                          Del
                        </button>
                      </div>
                    </div>
                  ))}
                </div>
              </div>
            </div>
            <div className="modal-footer">
              <button
                disabled={disable}
                className="btn-danger"
                onClick={closeModal}
              >
                Cancel
              </button>
              <button disabled={disable} className="btn" onClick={savePodcast}>
                Save Podcast
              </button>
            </div>
          </div>
        </div>
      );
    }

This component will add a new podcast along with its episodes to our Strapi backend. We set two states, episodes: this will hold the episodes added to the podcast, and disable state will either disable buttons when adding a podcast or enable it.

We have a savePodcast function. This function gets the podcast's data from the UI: name, imageUrl, author.

Then, the episodes must have already been set in the episodes state array, the array is looped through and each episode is added to the backend by performing an HTTP POST request in the "http://localhost:1337/api/episodes" Strapi URL and passing the episode name and mp3 link received from the UI as payload, the episode ids generated are stored in an array.

Next, we create a new podcast by performing an HTTP POST request to the "http://localhost:1337/api/podcasts" Strapi URL, passing in the podcast's name, image URL, author, and the array of episode ids as payload.

The episode's ids will make the podcast to be linked to the episodes whose id is in the ids. The button is enabled, the modal is closed, and the page is reloaded, so we see the newly added podcast.

The addEpisode function adds each episode to the episode's states.

The removeEpisode function removes an episode from the episodes array.

The UI has input boxes to input the podcast's name, imageURL, and author. The Add Episodes section is where we add episodes to the podcast.

When clicked, the Save Podcast button calls the savePodcast function, which creates the episodes in the Strapi backend from episodes in the episodes state array and creates the podcast.

EpisodeCard Component
  • Create an EpisodeCard folder in the components folder. Add index.js and EpisodeCard.module.css.
  • Open index.js and paste the code:
    import styles from "./EpisodeCard.module.css";
    export default function EpisodeCard({ episode }) {
      const { name, mp3Link } = episode;
      return (
        <div className={styles.episodeCard}>
          <div className={styles.episodeCardImg}></div>
          <div className={styles.episodeCardDetails}>
            <div className={styles.episodeCardName}>
              <h4>{name}</h4>
            </div>
            <div className={styles.episodeCardAudio}>
              <audio controls src={mp3Link} />
            </div>
          </div>
        </div>
      );
    }

We destructured the episode from the parameter. The episode will pass an episode to the component.

Next, we destructured the episode details from the episode variable. name, and mp3Link is what we expect to be in the episode object.

Now, we render the name, and we render the audio element, pass the mp3Link to its src attribute, and set the controls attribute. The audio element will play the mp3 in the mp3Link in our browser, so we listen to the podcast.

Open the EpisodeCard.module.css file and paste the styling code:

    .episodeCard {
      display: flex;
      border-bottom: 1px solid darkgray;
      padding-bottom: 10px;
      margin: 19px 0px;
    }
    .episodeCardImg {
      width: 55px;
      background-color: darkslategray;
      margin-right: 7px;
    }
    .episodeCardDetails {
      display: flex;
      flex-direction: column;
    }
    .episodeCardName h4 {
      font-weight: 300;
      margin: 5px 0;
      margin-top: 0;
    }
    .episodeCardAudio audio {
      height: 20px;
    }
Header Component
  • Create a Header folder at components and create index.js and Header.module.css. Open the index.js and paste the code:
    import { header, headerName } from "./Header.module.css";
    export default function Header() {
      return (
        <section className={header}>
          <div className={headerName}>PodCast</div>
        </section>
      );
    }

Just a simple UI that displays “PodCast”.

  • Open Header.module.css and paste the styling code:
    .header {
      height: 54px;
      background-color: black;
      color: white;
      display: flex;
      align-items: center;
      padding: 10px;
      font-family: sans-serif;
      width: 100%;
      padding-left: 19%;
    }
    .headerName {
      font-size: 1.8em;
    }
PodCard Component
  • Create a PodCard folder at components and create index.js and PodCard.module.css.
  • Open the index.js and paste the code:
    import styles from "./PodCard.module.css";
    import Link from "next/link";

    export default function PodCard({ podcast, id }) {
      const { name, author, episodes, createdAt, imageUrl } = podcast;
      return (
        <Link href={`podcast/${id}`}>
          <div className={styles.podcard}>
            <div
              style={{ backgroundImage: `url(${imageUrl})` }}
              className={styles.podcardimg}
            ></div>
            <div className={styles.podcarddetails}>
              <div className={styles.podcardname}>
                <h3>{name}</h3>
              </div>
              <div className={styles.podcardauthor}>
                <span>{author}</span>
              </div>
              <div className={styles.podcardminidet}>
                <span>{episodes.data.length} episode(s)</span>
                <span>Created {createdAt}</span>
              </div>
            </div>
          </div>
        </Link>
      );
    }

The component expects a podcast and an id in its props. So we destructure it and also destructure name, author, episodes, createdAt, imageUrl from the podcast object. Then, we simply render them on the UI.

The UI is simple, just a card that renders the mini-details of a podcast. Open PodCard.module.css and paste the styling code:

    .podcard {
      display: flex;
      border-bottom: 1px solid rgba(232, 232, 2321);
      padding-bottom: 12px;
      margin: 20px 0;
      cursor: pointer;
    }
    .podcardimg {
      width: 79px;
      background-color: darkgray;
      margin-right: 11px;
      background-repeat: no-repeat;
      background-size: cover;
      background-position: center;
    }
    .podcarddetails {
      display: flex;
      flex-direction: column;
    }
    .podcardname h3 {
      margin-top: 0;
      margin-bottom: 5px;
    }
    .podcardauthor {
      color: darkgray;
      padding: 7px 0;
    }
    .podcardminidet {
      font-weight: 100;
    }
    .podcardminidet :nth-child(1) {
      padding-right: 14px;
    }
    .podcardminidet :nth-child(2) {
      padding-right: 3px;
    }
globals.css

Now, we add our global styles. These styles affect the modal, input box, and button. Open styles/globals.css and paste the code:

    html,
    body {
      padding: 0;
      margin: 0;
      font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu,
        Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
      background-color: rgba(234, 238, 243, 1);
    }
    a {
      color: inherit;
      text-decoration: none;
    }
    * {
      box-sizing: border-box;
    }
    button {
      height: 30px;
      padding: 0px 15px 2px;
      font-weight: 400;
      font-size: 1rem;
      line-height: normal;
      border-radius: 2px;
      cursor: pointer;
      outline: 0px;
      background-color: rgb(0, 126, 255);
      border: 1px solid rgb(0, 126, 255);
      color: rgb(255, 255, 255);
      text-align: center;
    }
    .btn-danger {
      background-color: rgb(195 18 18);
      border: 1px solid rgb(195 18 18);
    }
    .header {
      height: 54px;
      background-color: black;
      color: white;
      display: flex;
      align-items: center;
      padding: 10px;
      font-family: sans-serif;
      width: 100%;
      padding-left: 19%;
    }
    .headerName {
      font-size: 1.8em;
    }
    .modal {
      position: fixed;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      display: flex;
      flex-direction: column;
      align-items: center;
      z-index: 1000;
      font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
    }
    .modal-backdrop {
      opacity: 0.5;
      width: inherit;
      height: inherit;
      background-color: grey;
      position: fixed;
    }
    .modal-body {
      padding: 5px;
      padding-top: 15px;
      padding-bottom: 15px;
    }
    .modal-footer {
      padding: 15px 5px;
      display: flex;
      justify-content: space-between;
    }
    .modal-header {
      display: flex;
      justify-content: space-between;
      align-items: center;
    }
    .modal-header h3 {
      margin: 0;
    }
    .modal-content {
      background-color: white;
      z-index: 1;
      padding: 10px;
      margin-top: 10px;
      width: 520px;
      box-shadow: 0px 11px 15px -7px rgba(0, 0, 0, 0.2), 0px 24px 38px 3px rgba(0, 0, 0, 0.14),
        0px 9px 46px 8px rgba(0, 0, 0, 0.12);
      border-radius: 4px;
    }
    input[type="text"] {
      width: 100%;
      padding: 9px;
      font-weight: 400;
      cursor: text;
      outline: 0px;
      border: 1px solid rgb(227, 233, 243);
      border-radius: 2px;
      color: rgb(51, 55, 64);
      background-color: transparent;
      box-sizing: border-box;
    }
    .label {
      padding: 4px 0;
      font-size: small;
      color: rgb(51, 55, 64);
    }
    .content {
      display: flex;
      flex-wrap: wrap;
      flex-direction: column;
    }
    .inputField {
      margin: 3px 7px;
      flex: 1 40%;
    }
    .disable {
      opacity: 0.5;
      cursor: not-allowed;
    }

Now, our frontend is set, we will test it.

Testing the App

Now, let’s test our frontend app.

  • Go to the default / page and click on the Add Podcast button.
  • In the modal that shows up, add the data.
    Name -> Rust Podcast
    ImageUrl -> https://www.clipartkey.com/mpngs/m/194-1949306_hax0rferriswithkey-rust-programming-language-logo.png
    Author -> Sam Victor

    Episodes
    --------
    Episode Name -> Rustlang 2.0 - Episode 1
    MP3 Link -> mp3-link.mp3

Note: Add a real podcast mp3 link. This one is for mock.

A Sample Screenshot

  • Click on the “Add” button to add the episode on the “Add Episodes” section.

A Sample Screenshot

Add More Episodes

    Episode Name -> Rustlang 3.0 - Episode 2
    MP3 Link -> mp3-link.mp3

    Episode Name -> Rustlang 4.0 - Episode 3
    MP3 Link -> mp3-link.mp3

    Episode Name -> Rustlang 5.0 - Episode 4
    MP3 Link -> mp3-link.mp3

A Sample Screenshot

A Sample Screenshot

  • Click on the “Save Podcast” button to create a new podcast.

A Sample Screenshot

  • See the new “Rust Podcast” podcast is created. See that the PodCard component says it has 4 episodes and was created by Sam Victor. 😁

  • Click on the podcast:

A Sample Screenshot

All the episodes are listed with audio control present to play each podcast. Clicking on the play control will make the audio element play the mp3 file.

Let’s delete this podcast, click on the “Delete” button. A confirm dialog shows up.

A Sample Screenshot

  • Click on “OK”.

A Sample Screenshot

Boom!! The podcast is gone.

Add More Features

There are many features to be added to this app. You can add more functionality, for example,

  • We can add a login/sign up
  • Authenticated users can listen to premium podcasts.
  • Add an editing feature for a podcast and its episodes.
  • Refactor the deleting of a podcast and its episodes.
  • many more

You can learn more about UI/UK and Strapi by adding these features. I leave it to you to do that.

You can clone both the backend and frontend code from the below "Source code" section. I will like to see what you can come up with.

Source Code

Conclusion

We learned about Strapi, how to scaffold a Strapi project and how to build collections. Next, we learned how to establish relationships between collections in the UI.

Further, we created a Next.js project, built the components, and learned how to communicate with the Strapi API endpoints from a Next.js project. Strapi is fantastic, no doubt.

If you have any questions regarding this or anything I should add, correct, or remove, please leave a comment.