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.
What we will learn
How to:
Requirements Before we begin, make sure you have the below tools installed in your machine.
What is Strapi?
Strapi is a headless CMS (Content Management System) based on Node.js 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.
1mkdir podcast-app
Move inside the folder:
1cd podcast-app
Now, let's scaffold our Strapi project:
1yarn 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:
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:
1podcast {
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.
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”
And 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."
Then, on the right side, click on "Public," and scroll down to the "Permissions" section, and check the checkboxes on both "EPISODES" and "PODCASTS."
Scroll up and click on the “Save” button.
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, click on the "Episodes" link on the sidebar.
Click on the "+ Add New Episodes" button on the top-right. Now we enter our episode data.
1name -> Episode 1 - React
2mp3Link -> mp3-link.mp3
Click on the “Save” button.
Click on 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. So add the below episodes yourself.
1name -> Episode 2 - React
2mp3Link -> mp3-link.mp3
3
4name -> Episode 3 - React
5mp3Link -> mp3-link.mp3
6
7name -> Episode 4 - React
8mp3Link -> mp3-link.mp3
9
10name -> Episode 4 - Angular
11mp3Link -> mp3-link.mp3
12
13name -> Episode 4 - Angular
14mp3Link -> mp3-link.mp3
Now, we add podcasts data. Click on the “Podcasts” link on the sidebar.
Click on the “+ Add New Podcasts”.
Type in the data:
1name -> React Podcast
2author -> Chidume Nnamdi
3imageUrl -> 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 will add it to the podcast.
Now, add all “React” episodes.
Now, click on the “Save” button, and the “Publish” button when it becomes enabled.
Let’s add another podcast, go back and click on the “+ Add New Podcasts”.
Add the data:
1name -> Angular Podcast
2imageUrl -> https://encrypted-tbn1.gstatic.com/images?q=tbn:ANd9GcTp0qlAoWcOOswIkL_qpjYzJqCCDmWXiBzCXiqbE43Obo8c0Z-s
3author -> Chidume Nnamdi
On episodes, add the “Angular” episodes.
Click on “Save” and then on “Publish”
Testing the Strapi endpoints
We will be using Postman.
Get all podcasts
Type in http://localhost:1337/podcasts
and click on "Send".
Get a podcast
Let’s get the “Angular” podcast. Its ID is 3, so we type http://localhost:1337/podcasts/3
and click "Send".
Setting up Next.js project
We are done with the backend, its time to build our frontend and see how we will call the endpoints provided to us by Strapi.
Se we begin by creating a Next.js project:
1yarn 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:
1cd podcast-strapi
Start the Next.js development server.
1yarn 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 build the app.
Our app will have two routes:
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.
We will have the:
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.
1yarn 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.
1import Head from "next/head";
2import styles from "../styles/Home.module.css";
3import Header from "../components/Header";
4import PodCard from "../components/PodCard";
5import { useEffect, useState } from "react";
6import axios from "axios";
7import AddPodcastDialog from "../components/AddPodcastDialog";
8export default function Home() {
9 const [podcasts, setPodcasts] = useState([]);
10 const [showModal, setShowModal] = useState(false);
11 useEffect(async () => {
12 const data = await axios.get("http://localhost:1337/podcasts");
13 setPodcasts(data?.data);
14 }, []);
15 function showAddPodcastDialog() {
16 setShowModal(!showModal);
17 }
18 return (
19 <div className={styles.container}>
20 <Head>
21 <title>Podcast</title>
22 <link rel="icon" href="/favicon.ico" />
23 </Head>
24 <main className={styles.main}>
25 <div className={styles.breadcrumb}>
26 <h2>Hello, Good Day.</h2>
27 <span>
28 <button onClick={showAddPodcastDialog}>Add Podcast</button>
29 </span>
30 </div>
31 <div className={styles.podcontainer}>
32 <div className={styles.yourpodcasts}>
33 <h3>Your Podcasts</h3>
34 </div>
35 <div>
36 {podcasts.map((podcast, i) => (
37 <PodCard key={i} podcast={podcast} />
38 ))}
39 </div>
40 </div>
41 {showModal ? (
42 <AddPodcastDialog closeModal={showAddPodcastDialog} />
43 ) : null}
44 </main>
45 </div>
46 );
47}
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 podcasts state array. See that we used the GET HTTP method, and the URL "http://localhost:1337/podcasts"
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 comm with our Strapi backend to fetch data to display in 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.
E.g
You can test with different unique numbers such as localhost:3000/podcast/1
.
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/podcasts/" + id
endpoint via the GET HTTP method. With the podcast data, we render all the details.
1import styles from "../../styles/PodCastView.module.css";
2import { useRouter } from "next/router";
3import EpisodeCard from "../../components/EpisodeCard";
4import axios from "axios";
5import { useEffect, useState } from "react";
6export default function PodCastView() {
7 const router = useRouter();
8 const {
9 query: { id },
10 } = router;
11 const [podcast, setPodcast] = useState();
12 useEffect(async () => {
13 const data = await axios.get("http://localhost:1337/podcasts/" + id);
14 setPodcast(data?.data);
15 }, [id]);
16 async function deletePodcast() {
17 if (confirm("Do you really want to delete this podcast?")) {
18 // delete podcast episodes
19 const episodes = podcast?.episodes;
20 for (let index = 0; index < episodes.length; index++) {
21 const episode = episodes[index];
22 await axios.delete("http://localhost:1337/episodes/" + episode?.id);
23 }
24 await axios.delete("http://localhost:1337/podcasts/" + id);
25 router.push("/");
26 }
27 }
28 return (
29 <div className={styles.podcastviewcontainer}>
30 <div className={styles.podcastviewmain}>
31 <div
32 style={{ backgroundImage: `url(${podcast?.imageUrl})` }}
33 className={styles.podcastviewimg}
34 ></div>
35 <div style={{ width: "100%" }}>
36 <div className={styles.podcastviewname}>
37 <h1>{podcast?.name}</h1>
38 </div>
39 <div className={styles.podcastviewminidet}>
40 <div>
41 <span style={{ marginRight: "4px", color: "rgb(142 142 142)" }}>
42 Created by:
43 </span>
44 <span style={{ fontWeight: "600" }}>{podcast?.author}</span>
45 </div>
46 <div style={{ padding: "14px 0" }}>
47 <span>
48 <button onClick={deletePodcast} className="btn-danger">
49 Delete
50 </button>
51 </span>
52 </div>
53 </div>
54 <div className={styles.podcastviewepisodescont}>
55 <div className={styles.podcastviewepisodes}>
56 <h2>Episodes</h2>
57 </div>
58 <div className={styles.podcastviewepisodeslist}>
59 {podcast?.episodes.map((episode, i) => (
60 <EpisodeCard key={i} episode={episode} />
61 ))}
62 </div>
63 </div>
64 </div>
65 </div>
66 </div>
67 );
68}
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/podcasts/" + id
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 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/podcasts/" + id
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.
AddPostcastDialog component
Create a AddPodcastDialog
folder, and inside the folder create a index.js
file. Inside this file, add the code:
1import { useState } from "react";
2import EpisodeCard from "../EpisodeCard";
3import axios from "axios";
4export default function AddPodcastDialog({ closeModal }) {
5 const [episodes, setEpisode] = useState([]);
6 const [disable, setDisable] = useState(false);
7 async function savePodcast() {
8 setDisable(true);
9 const podcastName = window.podcastName.value;
10 const podcastImageUrl = window.podcastImageUrl.value;
11 const podcastAuthor = window.podcastAuthor.value;
12 const episodeIds = [];
13 // add all the episodes, get their ids and use it to save the podcast
14 for (let index = 0; index < episodes.length; index++) {
15 const episode = episodes[index];
16 const data = await axios.post("http://localhost:1337/episodes", {
17 ...episode,
18 });
19 episodeIds.push(data?.data?.id);
20 }
21 // add podcast
22 await axios.post("http://localhost:1337/podcasts", {
23 name: podcastName,
24 author: podcastAuthor,
25 imageUrl: podcastImageUrl,
26 episodes: episodeIds,
27 });
28 setDisable(false);
29 closeModal();
30 location.reload();
31 }
32 function addEpisode() {
33 const episodeName = window.episodeName.value;
34 const episodeMp3Link = window.episodeMp3Link.value;
35 setEpisode([...episodes, { name: episodeName, mp3Link: episodeMp3Link }]);
36 }
37 function removeEpisode(index) {
38 setEpisode(episodes.filter((episode, i) => i != index));
39 }
40 return (
41 <div className="modal">
42 <div className="modal-backdrop" onClick={closeModal}></div>
43 <div className="modal-content">
44 <div className="modal-header">
45 <h3>Add New Podcast</h3>
46 <span
47 style={{ padding: "10px", cursor: "pointer" }}
48 onClick={closeModal}
49 >
50 X
51 </span>
52 </div>
53 <div className="modal-body content">
54 <div style={{ display: "flex", flexWrap: "wrap" }}>
55 <div className="inputField">
56 <div className="label">
57 <label>Name</label>
58 </div>
59 <div>
60 <input id="podcastName" type="text" />
61 </div>
62 </div>
63 <div className="inputField">
64 <div className="label">
65 <label>ImageUrl</label>
66 </div>
67 <div>
68 <input id="podcastImageUrl" type="text" />
69 </div>
70 </div>
71 <div className="inputField">
72 <div className="label">
73 <label>Author</label>
74 </div>
75 <div>
76 <input id="podcastAuthor" type="text" />
77 </div>
78 </div>
79 </div>
80 <div style={{ display: "flex", flexDirection: "column" }}>
81 <div>
82 <h4>Add Episodes</h4>
83 </div>
84 <div
85 style={{
86 display: "flex",
87 alignItems: "flex-end",
88 border: "1px solid rgb(212 211 211)",
89 paddingBottom: "4px",
90 }}
91 >
92 <div className="inputField">
93 <div className="label">
94 <label>Episode Name</label>
95 </div>
96 <div>
97 <input id="episodeName" type="text" />
98 </div>
99 </div>
100 <div className="inputField">
101 <div className="label">
102 <label>MP3 Link</label>
103 </div>
104 <div>
105 <input id="episodeMp3Link" type="text" />
106 </div>
107 </div>
108 <div style={{ flex: "0" }} className="inputField">
109 <button onClick={addEpisode}>Add</button>
110 </div>
111 </div>
112 <div
113 style={{
114 height: "200px",
115 overflowY: "scroll",
116 borderTop: "1px solid darkgray",
117 borderBottom: "1px solid darkgray",
118 margin: "8px 0",
119 }}
120 >
121 {episodes?.map((episode, i) => (
122 <div
123 style={{
124 display: "flex",
125 alignItems: "center",
126 justifyContent: "space-between",
127 }}
128 >
129 <EpisodeCard episode={episode} key={i} />
130 <div>
131 <button
132 className="btn-danger"
133 onClick={() => removeEpisode(i)}
134 >
135 Del
136 </button>
137 </div>
138 </div>
139 ))}
140 </div>
141 </div>
142 </div>
143 <div className="modal-footer">
144 <button
145 disabled={disable}
146 className="btn-danger"
147 onClick={closeModal}
148 >
149 Cancel
150 </button>
151 <button disabled={disable} className="btn" onClick={savePodcast}>
152 Save Podcast
153 </button>
154 </div>
155 </div>
156 </div>
157 );
158}
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, already the episodes must have 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/episodes"
Strapi URL and passing the episode name and mp3 link gotten from the UI as payload, the episode ids generated is stored in an array.
Next, we create a new podcast by performing an HTTP POST request to the "http://localhost:1337/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:
1import styles from "./EpisodeCard.module.css";
2export 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 component
Create a Header
folder at components
and create index.js
and Header.module.css
. Open the index.js
and paste the code:
1import { header, headerName } from "./Header.module.css";
2export 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”.
Open 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 component
Create a PodCard
folder at components
and create index.js
and PodCard.module.css
.
Open the index.js
and paste the code:
1import styles from "./PodCard.module.css";
2import Link from "next/link";
3export default function PodCard({ podcast }) {
4 const { id, name, author, episodes, created_at, imageUrl } = podcast;
5 return (
6 <Link href={`podcast/${id}`}>
7 <div className={styles.podcard}>
8 <div
9 style={{ backgroundImage: `url(${imageUrl})` }}
10 className={styles.podcardimg}
11 ></div>
12 <div className={styles.podcarddetails}>
13 <div className={styles.podcardname}>
14 <h3>{name}</h3>
15 </div>
16 <div className={styles.podcardauthor}>
17 <span>{author}</span>
18 </div>
19 <div className={styles.podcardminidet}>
20 <span>{episodes.length} episode(s)</span>
21 <span>Created {created_at}</span>
22 </div>
23 </div>
24 </div>
25 </Link>
26 );
27}
The component expects a podcast
in its props. So we destructure it and also destructure id, name, author, episodes, created_at, 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}
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:
1html,
2body {
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}
9a {
10 color: inherit;
11 text-decoration: none;
12}
13* {
14 box-sizing: border-box;
15}
16button {
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}
95input[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.
Testing the app Now, let’s test our frontend app.
Now, go to the default /
page and click on the Add Podcast
button.
In the modal that shows up, add the data.
1Name -> Vuejs Podcast
2ImageUrl -> https://upload.wikimedia.org/wikipedia/commons/thumb/9/95/Vue.js_Logo_2.svg/1024px-Vue.js_Logo_2.svg.png
3Author -> M. Hry
4
5Episodes
6--------
7Episode Name -> Vuejs - Episode 1
8MP3 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
1Episode Name -> Vuejs - Episode 2
2MP3 Link -> mp3-link.mp3
3
4Episode Name -> Vuejs - Episode 3
5MP3 Link -> mp3-link.mp3
6
7Episode Name -> Vuejs - Episode 4
8MP3 Link -> mp3-link.mp3
Click on the “Save Podcast” button to create a new podcast.
See the new “Vuejs Podcast” podcast is created. See that the PodCard
component says it has 4 episodes and was created by M. Hry
. 😁
Click on the podcast:
See the http://localhost:3000/podcast/18
page is loaded, show our "Veujs Podcast" in its full entirety.
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,
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.
Author of "Understanding JavaScript", Chidume is also an awesome writer about JavaScript, Angular, React and other web technologies.