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.
In this article, we will learn how to:
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.
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.
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.
We will begin creating our collections, but before we do that, let's see the model of our collections.
Our podcast model will be this:
1 podcast {
2 name
3 author
4 imageUrl
5 episodes [{
6 name
7 mp3Link
8 }]
9 }
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.
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”.
Now, we create the “podcasts” collection.
We have to insert fake data into our database. We will first have to add episode data. To do that:
1name -> Episode 1 - React
2mp3Link -> mp3-link.mp3
This makes our changes go live.
See that our input is there.
1 name -> Episode 2 - React
2 mp3Link -> mp3-link.mp3
3
4 name -> Episode 3 - React
5 mp3Link -> mp3-link.mp3
6
7 name -> Episode 4 - React
8 mp3Link -> mp3-link.mp3
9
10 name -> Episode 4 - Angular
11 mp3Link -> mp3-link.mp3
12
13 name -> Episode 4 - Angular
14 mp3Link -> mp3-link.mp3
Now, we add podcasts data.
1 name -> React Podcast
2 author -> Chidume Nnamdi
3 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.
Let’s add another podcast, go back and click on the “+ Add New Podcasts”.
Add the data:
1name -> Angular Podcast
imageUrl -> https://encrypted-tbn1.gstatic.com/images?q=tbn:ANd9GcTp0qlAoWcOOswIkL_qpjYzJqCCDmWXiBzCXiqbE43Obo8c0Z-s
author -> Chidume Nnamdi
In this stage, we will be using Postman.
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
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.
http://localhost:1337/api/podcasts4?populate=*
and
click "Send".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.
yarn create next-app podcast-strapi
This will create a Next.js project on a podcast-strapi
folder with all its dependencies installed.
cd podcast-strapi
yarn dev
This will server our Next.app at localhost:3000
.
localhost:3000
; you will see the Next.js default page.Now, we'll build the app, which will have two routes:
This is how our app will look like when we are done:
We will break our app into components before we start building, including the following:
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
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.
1 import Head from "next/head";
2 import styles from "../styles/Home.module.css";
3 import PodCard from "../components/PodCard";
4 import { useEffect, useState } from "react";
5 import axios from "axios";
6 import AddPodcastDialog from "../components/AddPodcastDialog";
7
8 export default function Home() {
9 const [podcasts, setPodcasts] = useState([]);
10 const [showModal, setShowModal] = useState(false);
11 useEffect(async () => {
12 const data = await axios.get(
13 "http://localhost:1337/api/podcasts?populate=*"
14 );
15 setPodcasts(data?.data?.data);
16 }, []);
17 function showAddPodcastDialog() {
18 setShowModal(!showModal);
19 }
20 return (
21 <div className={styles.container}>
22 <Head>
23 <title>Podcast</title>
24 <link rel="icon" href="/favicon.ico" />
25 </Head>
26 <main className={styles.main}>
27 <div className={styles.breadcrumb}>
28 <h2>Hello, Good Day.</h2>
29 <span>
30 <button onClick={showAddPodcastDialog}>Add Podcast</button>
31 </span>
32 </div>
33 <div className={styles.podcontainer}>
34 <div className={styles.yourpodcasts}>
35 <h3>Your Podcasts</h3>
36 </div>
37 <div>
38 {podcasts.map((podcast, i) => (
39 <PodCard key={i} id={podcast.id} podcast={podcast.attributes} />
40 ))}
41 </div>
42 </div>
43 {showModal ? (
44 <AddPodcastDialog closeModal={showAddPodcastDialog} />
45 ) : null}
46 </main>
47 </div>
48 );
49 }
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:
1 .container {
2 min-height: 100vh;
3 display: flex;
4 flex-direction: column;
5 justify-content: center;
6 align-items: center;
7 background-color: rgba(234, 238, 243, 1);
8 }
9 .main {
10 flex: 1;
11 display: flex;
12 flex-direction: column;
13 width: 62%;
14 }
15 .podcontainer {
16 background-color: white;
17 padding: 15px;
18 }
19 .yourpodcasts {
20 color: darkgrey;
21 border-bottom: 1px solid rgba(232, 232, 232, 1);
22 padding-bottom: 5px;
23 }
24 .breadcrumb {
25 display: flex;
26 justify-content: space-between;
27 align-items: center;
28 }
We now see how we communicate with our Strapi backend to fetch data to display on our frontend.
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.
1 import styles from "../../styles/PodCastView.module.css";
2 import { useRouter } from "next/router";
3 import EpisodeCard from "../../components/EpisodeCard";
4 import axios from "axios";
5 import { useEffect, useState } from "react";
6
7 export default function PodCastView() {
8 const router = useRouter();
9 const {
10 query: { id },
11 } = router;
12
13 const [podcast, setPodcast] = useState();
14 useEffect(async () => {
15 const data = await axios.get(
16 `http://localhost:1337/api/podcasts/${id}?populate=*`
17 );
18 setPodcast(data?.data?.data);
19 }, [id]);
20
21 async function deletePodcast() {
22 if (confirm("Do you really want to delete this podcast?")) {
23 // delete podcast episodes
24 const episodes = podcast?.attributes?.episodes.data;
25 for (let index = 0; index < episodes.length; index++) {
26 const episode = episodes[index];
27 await axios.delete("http://localhost:1337/api/episodes/" + episode?.id);
28 }
29 await axios.delete("http://localhost:1337/api/podcasts/" + id);
30 router.push("/");
31 }
32 }
33
34 return (
35 <div className={styles.podcastviewcontainer}>
36 <div className={styles.podcastviewmain}>
37 <div
38 style={{ backgroundImage: `url(${podcast?.attributes.imageUrl})` }}
39 className={styles.podcastviewimg}
40 ></div>
41 <div style={{ width: "100%" }}>
42 <div className={styles.podcastviewname}>
43 <h1>{podcast?.attributes.name}</h1>
44 </div>
45 <div className={styles.podcastviewminidet}>
46 <div>
47 <span style={{ marginRight: "4px", color: "rgb(142 142 142)" }}>
48 Created by:
49 </span>
50 <span style={{ fontWeight: "600" }}>
51 {podcast?.attributes.author}
52 </span>
53 </div>
54 <div style={{ padding: "14px 0" }}>
55 <span>
56 <button onClick={deletePodcast} className="btn-danger">
57 Delete
58 </button>
59 </span>
60 </div>
61 </div>
62 <div className={styles.podcastviewepisodescont}>
63 <div className={styles.podcastviewepisodes}>
64 <h2>Episodes</h2>
65 </div>
66 <div className={styles.podcastviewepisodeslist}>
67 {podcast?.attributes.episodes.data.map((episode, i) => (
68 <EpisodeCard key={i} episode={episode.attributes} />
69 ))}
70 </div>
71 </div>
72 </div>
73 </div>
74 </div>
75 );
76 }
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:
1 .podcastviewcontainer {
2 min-height: 100vh;
3 display: flex;
4 flex-direction: column;
5 justify-content: center;
6 align-items: center;
7 background-color: rgba(234, 238, 243, 1);
8 }
9 .podcastviewimg {
10 width: 200px;
11 height: 300px;
12 background-color: darkgray;
13 margin-right: 9px;
14 margin-top: 28px;
15 background-repeat: no-repeat;
16 background-size: cover;
17 background-position: center;
18 }
19 .podcastviewmain {
20 flex: 1;
21 display: flex;
22 flex-direction: row;
23 width: 62%;
24 }
25 .podcastviewepisodeslist {
26 background-color: white;
27 padding: 15px;
28 }
Now, let’s look at the components.
First, create a components
folder at the root folder.
AddPodcastDialog
folder, and inside the folder create a index.js
file. Inside this file, add the code:1 import { useState } from "react";
2 import EpisodeCard from "../EpisodeCard";
3 import axios from "axios";
4
5 export default function AddPodcastDialog({ closeModal }) {
6 const [episodes, setEpisode] = useState([]);
7 const [disable, setDisable] = useState(false);
8 async function savePodcast() {
9 setDisable(true);
10 const podcastName = window.podcastName.value;
11 const podcastImageUrl = window.podcastImageUrl.value;
12 const podcastAuthor = window.podcastAuthor.value;
13 const episodeIds = [];
14 // add all the episodes, get their ids and use it to save the podcast
15 for (let index = 0; index < episodes.length; index++) {
16 const episode = episodes[index];
17 const data = await axios.post("http://localhost:1337/api/episodes", {
18 data: {
19 ...episode,
20 },
21 });
22 episodeIds.push(data?.data.data.id);
23 }
24 // add podcast
25 addPodcast(episodeIds, podcastName, podcastAuthor, podcastImageUrl);
26 }
27 function addPodcast(episodeIds, podcastName, podcastAuthor, podcastImageUrl) {
28 axios.post("http://localhost:1337/api/podcasts", {
29 data: {
30 name: podcastName,
31 author: podcastAuthor,
32 imageUrl: podcastImageUrl,
33 episodes: episodeIds,
34 },
35 });
36 setDisable(false);
37 closeModal();
38 location.reload();
39 }
40 function addEpisode() {
41 const episodeName = window.episodeName.value;
42 const episodeMp3Link = window.episodeMp3Link.value;
43 setEpisode([...episodes, { name: episodeName, mp3Link: episodeMp3Link }]);
44 }
45 function removeEpisode(index) {
46 setEpisode(episodes.filter((episode, i) => i != index));
47 }
48 return (
49 <div className="modal">
50 <div className="modal-backdrop" onClick={closeModal}></div>
51 <div className="modal-content">
52 <div className="modal-header">
53 <h3>Add New Podcast</h3>
54 <span
55 style={{ padding: "10px", cursor: "pointer" }}
56 onClick={closeModal}
57 >
58 X
59 </span>
60 </div>
61 <div className="modal-body content">
62 <div style={{ display: "flex", flexWrap: "wrap" }}>
63 <div className="inputField">
64 <div className="label">
65 <label>Name</label>
66 </div>
67 <div>
68 <input id="podcastName" type="text" />
69 </div>
70 </div>
71 <div className="inputField">
72 <div className="label">
73 <label>ImageUrl</label>
74 </div>
75 <div>
76 <input id="podcastImageUrl" type="text" />
77 </div>
78 </div>
79 <div className="inputField">
80 <div className="label">
81 <label>Author</label>
82 </div>
83 <div>
84 <input id="podcastAuthor" type="text" />
85 </div>
86 </div>
87 </div>
88 <div style={{ display: "flex", flexDirection: "column" }}>
89 <div>
90 <h4>Add Episodes</h4>
91 </div>
92 <div
93 style={{
94 display: "flex",
95 alignItems: "flex-end",
96 border: "1px solid rgb(212 211 211)",
97 paddingBottom: "4px",
98 }}
99 >
100 <div className="inputField">
101 <div className="label">
102 <label>Episode Name</label>
103 </div>
104 <div>
105 <input id="episodeName" type="text" />
106 </div>
107 </div>
108 <div className="inputField">
109 <div className="label">
110 <label>MP3 Link</label>
111 </div>
112 <div>
113 <input id="episodeMp3Link" type="text" />
114 </div>
115 </div>
116 <div style={{ flex: "0" }} className="inputField">
117 <button onClick={addEpisode}>Add</button>
118 </div>
119 </div>
120 <div
121 style={{
122 height: "200px",
123 overflowY: "scroll",
124 borderTop: "1px solid darkgray",
125 borderBottom: "1px solid darkgray",
126 margin: "8px 0",
127 }}
128 >
129 {episodes?.map((episode, i) => (
130 <div
131 style={{
132 display: "flex",
133 alignItems: "center",
134 justifyContent: "space-between",
135 }}
136 >
137 <EpisodeCard episode={episode} key={i} />
138 <div>
139 <button
140 className="btn-danger"
141 onClick={() => removeEpisode(i)}
142 >
143 Del
144 </button>
145 </div>
146 </div>
147 ))}
148 </div>
149 </div>
150 </div>
151 <div className="modal-footer">
152 <button
153 disabled={disable}
154 className="btn-danger"
155 onClick={closeModal}
156 >
157 Cancel
158 </button>
159 <button disabled={disable} className="btn" onClick={savePodcast}>
160 Save Podcast
161 </button>
162 </div>
163 </div>
164 </div>
165 );
166 }
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
folder in the components
folder. Add index.js
and EpisodeCard.module.css
.index.js
and paste the code:1 import styles from "./EpisodeCard.module.css";
2 export default function EpisodeCard({ episode }) {
3 const { name, mp3Link } = episode;
4 return (
5 <div className={styles.episodeCard}>
6 <div className={styles.episodeCardImg}></div>
7 <div className={styles.episodeCardDetails}>
8 <div className={styles.episodeCardName}>
9 <h4>{name}</h4>
10 </div>
11 <div className={styles.episodeCardAudio}>
12 <audio controls src={mp3Link} />
13 </div>
14 </div>
15 </div>
16 );
17 }
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:
1 .episodeCard {
2 display: flex;
3 border-bottom: 1px solid darkgray;
4 padding-bottom: 10px;
5 margin: 19px 0px;
6 }
7 .episodeCardImg {
8 width: 55px;
9 background-color: darkslategray;
10 margin-right: 7px;
11 }
12 .episodeCardDetails {
13 display: flex;
14 flex-direction: column;
15 }
16 .episodeCardName h4 {
17 font-weight: 300;
18 margin: 5px 0;
19 margin-top: 0;
20 }
21 .episodeCardAudio audio {
22 height: 20px;
23 }
Header
folder at components
and create index.js
and Header.module.css
. Open the index.js
and paste the code:1 import { header, headerName } from "./Header.module.css";
2 export default function Header() {
3 return (
4 <section className={header}>
5 <div className={headerName}>PodCast</div>
6 </section>
7 );
8 }
Just a simple UI that displays “PodCast”.
Header.module.css
and paste the styling code:1 .header {
2 height: 54px;
3 background-color: black;
4 color: white;
5 display: flex;
6 align-items: center;
7 padding: 10px;
8 font-family: sans-serif;
9 width: 100%;
10 padding-left: 19%;
11 }
12 .headerName {
13 font-size: 1.8em;
14 }
PodCard
folder at components
and create index.js
and PodCard.module.css
.index.js
and paste the code:1 import styles from "./PodCard.module.css";
2 import Link from "next/link";
3
4 export default function PodCard({ podcast, id }) {
5 const { name, author, episodes, createdAt, imageUrl } = podcast;
6 return (
7 <Link href={`podcast/${id}`}>
8 <div className={styles.podcard}>
9 <div
10 style={{ backgroundImage: `url(${imageUrl})` }}
11 className={styles.podcardimg}
12 ></div>
13 <div className={styles.podcarddetails}>
14 <div className={styles.podcardname}>
15 <h3>{name}</h3>
16 </div>
17 <div className={styles.podcardauthor}>
18 <span>{author}</span>
19 </div>
20 <div className={styles.podcardminidet}>
21 <span>{episodes.data.length} episode(s)</span>
22 <span>Created {createdAt}</span>
23 </div>
24 </div>
25 </div>
26 </Link>
27 );
28 }
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:
1 .podcard {
2 display: flex;
3 border-bottom: 1px solid rgba(232, 232, 2321);
4 padding-bottom: 12px;
5 margin: 20px 0;
6 cursor: pointer;
7 }
8 .podcardimg {
9 width: 79px;
10 background-color: darkgray;
11 margin-right: 11px;
12 background-repeat: no-repeat;
13 background-size: cover;
14 background-position: center;
15 }
16 .podcarddetails {
17 display: flex;
18 flex-direction: column;
19 }
20 .podcardname h3 {
21 margin-top: 0;
22 margin-bottom: 5px;
23 }
24 .podcardauthor {
25 color: darkgray;
26 padding: 7px 0;
27 }
28 .podcardminidet {
29 font-weight: 100;
30 }
31 .podcardminidet :nth-child(1) {
32 padding-right: 14px;
33 }
34 .podcardminidet :nth-child(2) {
35 padding-right: 3px;
36 }
Now, we add our global styles. These styles affect the modal, input box, and button.
Open styles/globals.css
and paste the code:
1 html,
2 body {
3 padding: 0;
4 margin: 0;
5 font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu,
6 Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
7 background-color: rgba(234, 238, 243, 1);
8 }
9 a {
10 color: inherit;
11 text-decoration: none;
12 }
13 * {
14 box-sizing: border-box;
15 }
16 button {
17 height: 30px;
18 padding: 0px 15px 2px;
19 font-weight: 400;
20 font-size: 1rem;
21 line-height: normal;
22 border-radius: 2px;
23 cursor: pointer;
24 outline: 0px;
25 background-color: rgb(0, 126, 255);
26 border: 1px solid rgb(0, 126, 255);
27 color: rgb(255, 255, 255);
28 text-align: center;
29 }
30 .btn-danger {
31 background-color: rgb(195 18 18);
32 border: 1px solid rgb(195 18 18);
33 }
34 .header {
35 height: 54px;
36 background-color: black;
37 color: white;
38 display: flex;
39 align-items: center;
40 padding: 10px;
41 font-family: sans-serif;
42 width: 100%;
43 padding-left: 19%;
44 }
45 .headerName {
46 font-size: 1.8em;
47 }
48 .modal {
49 position: fixed;
50 top: 0;
51 left: 0;
52 width: 100%;
53 height: 100%;
54 display: flex;
55 flex-direction: column;
56 align-items: center;
57 z-index: 1000;
58 font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
59 }
60 .modal-backdrop {
61 opacity: 0.5;
62 width: inherit;
63 height: inherit;
64 background-color: grey;
65 position: fixed;
66 }
67 .modal-body {
68 padding: 5px;
69 padding-top: 15px;
70 padding-bottom: 15px;
71 }
72 .modal-footer {
73 padding: 15px 5px;
74 display: flex;
75 justify-content: space-between;
76 }
77 .modal-header {
78 display: flex;
79 justify-content: space-between;
80 align-items: center;
81 }
82 .modal-header h3 {
83 margin: 0;
84 }
85 .modal-content {
86 background-color: white;
87 z-index: 1;
88 padding: 10px;
89 margin-top: 10px;
90 width: 520px;
91 box-shadow: 0px 11px 15px -7px rgba(0, 0, 0, 0.2), 0px 24px 38px 3px rgba(0, 0, 0, 0.14),
92 0px 9px 46px 8px rgba(0, 0, 0, 0.12);
93 border-radius: 4px;
94 }
95 input[type="text"] {
96 width: 100%;
97 padding: 9px;
98 font-weight: 400;
99 cursor: text;
100 outline: 0px;
101 border: 1px solid rgb(227, 233, 243);
102 border-radius: 2px;
103 color: rgb(51, 55, 64);
104 background-color: transparent;
105 box-sizing: border-box;
106 }
107 .label {
108 padding: 4px 0;
109 font-size: small;
110 color: rgb(51, 55, 64);
111 }
112 .content {
113 display: flex;
114 flex-wrap: wrap;
115 flex-direction: column;
116 }
117 .inputField {
118 margin: 3px 7px;
119 flex: 1 40%;
120 }
121 .disable {
122 opacity: 0.5;
123 cursor: not-allowed;
124 }
Now, our frontend is set, we will test it.
Now, let’s test our frontend app.
/
page and click on the Add Podcast
button.1 Name -> Rust Podcast
2 ImageUrl -> https://www.clipartkey.com/mpngs/m/194-1949306_hax0rferriswithkey-rust-programming-language-logo.png
3 Author -> Sam Victor
4
5 Episodes
6 --------
7 Episode Name -> Rustlang 2.0 - Episode 1
8 MP3 Link -> mp3-link.mp3
Note: Add a real podcast mp3 link. This one is for mock.
1 Episode Name -> Rustlang 3.0 - Episode 2
2 MP3 Link -> mp3-link.mp3
3
4 Episode Name -> Rustlang 4.0 - Episode 3
5 MP3 Link -> mp3-link.mp3
6
7 Episode Name -> Rustlang 5.0 - Episode 4
8 MP3 Link -> mp3-link.mp3
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.
Boom!! The podcast is gone.
There are many features to be added to this app. You can add more functionality, for example,
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.
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.
Frontend Software Engineer, Technical Writer & open-source contributor.