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
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
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.
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".
Now, we begin to set the fields and their field types for our "episodes" collection. On the UI that appears, select "Text,"
On the "Add new Text field," type "name" on the input box.
Then, click on “+ Add another field”.
- Select “Text”.
- Type “mp3Link”.
- Click on “Finish” on the “Episodes” page that appears.
- Click on “Save”. This saves the “episodes” collection.
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”
- Click on “Continue”.
- On the “Podcasts” modal, click on “Text”
- On the “Add new Text field”, type “name” on the input box.
- Then, click on “+ Add another field”.
- Select “Text” and in the next UI type “author”.
- Do the same for “imageUrl”.
- On the “Select a field for your collection type” that shows up, click on “Relation”.
- 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.
- See the text changes to "Podcast has many Episodes." That's the relationship we want.
- Click on "Finish."
- 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.
- Then, click on "Public," and then scroll down to the "Permissions" section, and check the checkboxes on both "EPISODES" and "PODCASTS."
- Click on the “Save” button to save the permissions.
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.
- Click on the "+ Add New Episodes" button on the top-right. Now, we enter our episode data.
name -> Episode 1 - React
mp3Link -> mp3-link.mp3
- Click on the “Save” and then the “Publish” button.
This makes our changes go live.
- Click on the “<-” button on the top-left to go back.
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
Now, we add podcasts data.
- Click on the “podcast” link on the sidebar.
- 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
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.
- Click on an episode to add it to the podcast.
- Now, add all “React” episodes.
- Now, click on the “Save” button, and then “Publish” button when it becomes enabled.
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
- On episodes, add the “Angular” episodes.
- Click on “Save” and then on “Publish”
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
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".
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.
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
View a Podcast
Add a Podcast
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.
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 aindex.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 thecomponents
folder. Addindex.js
andEpisodeCard.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 atcomponents
and createindex.js
andHeader.module.css
. Open theindex.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 atcomponents
and createindex.js
andPodCard.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 theAdd 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.
- Click on the “Add” button to add the episode on the “Add Episodes” section.
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
- Click on the “Save Podcast” button to create a new podcast.
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:
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.
- Click on “OK”.
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.