In the previous chapter, we discussed how to create REST APIs using Express.js. You may have noticed that we only briefly covered how to implement the APIs without discussing how to call them in the frontend. That was, in fact, by design. Micromanaging all the details using plain JavaScript would have been too overwhelming.
However, now that we've covered React.js, our first frontend JavaScript framework, we can revisit this topic, and this time, we are going to create a fully functional blog application.
Designing the application structure
As usual, you should have a big picture of the entire project. The project will be divided into two applications, a frontend app and a backend app.
The backend part should be handling the database connections and interactions, such as searching, adding, modifying, and deleting records based on different requests. And, of course, it should have the routes and controllers to handle these requests. For our blog app, it should be able to process the following requests:
GET /posts
: Lists all posts.POST /posts
: Creates a new post.GET /posts/:id
: Retrieves a specific post by ID.PUT /posts/:id
: Updates a specific post by ID.DELETE /posts/:id
: Deletes a specific post by ID.
This is very similar to the blog app we created before using Express.js, only this time, we don't need the view layer to display the user interface, that is the job for the frontend. And instead of rendering the page using the render()
method, your backend app should respond with a proper structured format such as JSON.
As for the frontend app, it replaces the role of the old view layer, and as an independent app, it offers greater flexibility and more advanced features. It should fetch/post data via the APIs we discussed above, and use that data to display the corresponding webpages. For our blog app, there should be 4 pages:
- Home page: Displays a list of posts.
- Create page: Displays a form used to create a new post.
- Update page: Displays a form used to modify an existing post and a delete button to delete that post.
- Post page: Displays the title and content of a post.
Setting up the backend with Express.js
Without further ado, let's start with the backend. Go to your work directory and create a backend
folder.
1mkdir backend
Change into it, and then initialize a new project:
1cd backend
1npm init
A package.json
file should be generated, remember to add "type": "module"
to enable ES modules.
package.json
1{
2 "name": "backend",
3 "type": "module",
4 . . .
5}
Install the express
package:
1npm install express
Then, set up your project structure like this:
1backend
2├── controllers
3│ └── postController.js
4├── index.js
5├── libs
6│ └── prisma.js
7├── package-lock.json
8└── package.json
Of course, Express is flexible enough to allow you to arrange this however you want, as long as you keep everything organized.
Next, initialize Prisma as our ORM framework, and we are still using SQLite database here:
1npx prisma init --datasource-provider sqlite
A prisma
directory should be generated, and there should be a schema.prisma
file inside. Add a Post
model in the schema:
prisma/schema.prisma
1// This is your Prisma schema file,
2// learn more about it in the docs: https://pris.ly/d/prisma-schema
3
4generator client {
5 provider = "prisma-client-js"
6}
7
8datasource db {
9 provider = "sqlite"
10 url = env("DATABASE_URL")
11}
12
13model Post {
14 id Int @id @default(autoincrement())
15 title String
16 content String
17}
Go to your .env
, and make sure the DATABASE_URL
key points to the desired location. The following example is a relative path, relative to the schema file schema.prisma
.
.env
1DATABASE_URL="file:./database.sqlite"
And next, run the migrations to apply the changes.
1npx prisma migrate dev
A database.sqlite
database should be generated with the following structure:
Lastly, don't forget to set up the Prisma client.
1npm install @prisma/client
libs/prisma.js
1import { PrismaClient } from "@prisma/client";
2
3const prisma = new PrismaClient();
4
5export default prisma;
Creating routes and controllers
And now, it is time to get down to coding. Go to index.js
, the entry point of our app, and create the following routes.
index.js
1import express from "express";
2import postController from "../backend/controllers/postController.js"; // Import the controller
3
4const app = express();
5const port = 3001;
6
7app.use(express.json());
8app.use(express.urlencoded({ extended: true }));
9
10app.post("/posts", postController.create); // Create a new post
11app.get("/posts/:id", postController.retrieve); // Retrieve a single post according to id
12app.put("/posts/:id", postController.update); // Update a post according to id
13app.delete("/posts/:id", postController.delete); // Delete a post according to id
14app.get("/posts", postController.list); // Retrieve a list of posts
15
16app.listen(port, () => {
17 console.log(
18 `Blog application listening on port ${port}. Visit http://localhost:${port}.`
19 );
20});
The post()
method works with the POST
request, the get()
method works with the GET
method, and so on. For instance, when a POST
request is sent to route /posts
, this request will be routed and processed by postController.create()
, which we are going to discuss later.
The :id
defines a parameter named id
. This parameter will be passed to the corresponding controller method, and can then be accessed via req.params.id
. For example, if a GET
request is sent to /posts/2
, then the id
parameter will be 2
.
And then, let's take a look at the controllers:
controllers/postController.js
1import prisma from "../libs/prisma.js";
2
3const postController = {
4 // The create action
5 create: async function (req, res) {
6 const { title, content } = req.body;
7 try {
8 const post = await prisma.post.create({
9 data: {
10 title: title,
11 content: content,
12 },
13 });
14
15 res.status(201).json({
16 message: "Post created.",
17 post: {
18 id: post.id,
19 title: post.title,
20 content: post.content,
21 },
22 });
23 } catch (e) {
24 res.status(500).json({ message: "Something went wrong." });
25 }
26 },
27
28 . . .
29};
30
31export default postController;
When a POST
request is sent to /posts
, this create()
method will be executed. This method assumes the request body has a title
and content
(line 7), and then use that data to update the database (line 8 to 13), and finally returns a 201
response, along with the post data we just created (line 15 to 22).
If anything goes wrong in the process, a 500
error response will be returned.
As for the retrieve()
method, it will access the id
parameter using req.params.id
, and then use that id
to locate the corresponding post.
1import prisma from "../libs/prisma.js";
2
3const postController = {
4 // The retrieve action
5 retrieve: async function (req, res) {
6 try {
7 const post = await prisma.post.findUnique({
8 where: {
9 id: Number(req.params.id),
10 },
11 });
12 res.status(200).json({
13 post: {
14 id: post.id,
15 title: post.title,
16 content: post.content,
17 },
18 });
19 } catch (e) {
20 res.status(404).json({ message: "Cannot find the requested post." });
21 }
22 },
23
24 . . .
25};
26
27export default postController;
The update()
, delete()
, and list()
methods work in a similar way.
1import prisma from "../libs/prisma.js";
2
3const postController = {
4 . . .
5
6 // The update action
7 update: async function (req, res) {
8 const { title, content } = req.body;
9 try {
10 const post = await prisma.post.update({
11 where: {
12 id: Number(req.params.id),
13 },
14 data: {
15 title: title,
16 content: content,
17 },
18 });
19 res.status(200).json({
20 message: "Post updated.",
21 post: {
22 id: post.id,
23 title: post.title,
24 content: post.content,
25 },
26 });
27 } catch (e) {
28 res.status(500).json({ message: "Something went wrong." });
29 }
30 },
31
32 . . .
33};
34
35export default postController;
1import prisma from "../libs/prisma.js";
2
3const postController = {
4 . . .
5
6 // The delete action
7 delete: async function (req, res) {
8 try {
9 const post = await prisma.post.delete({
10 where: {
11 id: Number(req.params.id),
12 },
13 });
14 res.status(200).json({ message: "Post deleted." });
15 } catch (e) {
16 res.status(500).json({ message: "Something went wrong." });
17 }
18 },
19
20 . . .
21};
22
23export default postController;
1import prisma from "../libs/prisma.js";
2
3const postController = {
4 . . .
5
6 // List all articles
7 list: async function (req, res) {
8 try {
9 const posts = await prisma.post.findMany();
10 res.status(200).json({
11 posts: posts,
12 });
13 } catch (e) {
14 res.status(500).json({ message: "Something went wrong." });
15 }
16 },
17
18 . . .
19};
20
21export default postController;
Cross-Origin Resource Sharing (CORS)
Before we move onto the frontend, there is one more thing we must take care of, and that is Cross-Origin Resource Sharing (CORS).
Since our frontend and backend are two separate applications, which means the backend will no longer be rendering the webpage, and instead, the frontend will be fetching data from a different domain.
But, for security reasons, this behavior is forbidden by default.
To allow Cross-Origin Resource Sharing, we must install another package called cors
.
1npm install cors
Then enable it with the use()
method.
index.js
1import express from "express";
2import postController from "../backend/controllers/postController.js";
3import cors from "cors";
4
5const app = express();
6const port = 3001;
7
8app.use(cors());
9
10app.use(express.json());
11app.use(express.urlencoded({ extended: true }));
12
13. . .
14
15app.listen(port, () => {
16 console.log(
17 `Blog application listening on port ${port}. Visit http://localhost:${port}.`
18 );
19});
And that's it, our backend app is ready. Next, we are going to create the frontend using React.js
Setting up the frontend with React.js
Again, let's start by initializing a new project. Go back to your work directory and create a new React project.
1npm create vite@latest frontend
1? Select a framework: › - Use arrow-keys. Return to submit.
2 Vanilla
3 Vue
4❯ React
5 Preact
6 Lit
7 Svelte
8 Solid
9 Qwik
10 Others
1? Select a variant: › - Use arrow-keys. Return to submit.
2 TypeScript
3 TypeScript + SWC
4 JavaScript
5❯ JavaScript + SWC
6 Remix ↗
1✔ Select a framework: › React
2✔ Select a variant: › JavaScript + SWC
3
4Scaffolding project in /. . ./react-demo/react-demo...
5
6Done. Now run:
7
8 cd <my_project>
9 npm install
10 npm run dev
Change into the frontend directory and install the necessary packages.
1cd frontend
1npm install
And for demonstration purposes, we are going to use TailwindCSS for styling.
1npm install -D tailwindcss postcss autoprefixer
1npx tailwindcss init -p
tailwind.config.js
1/** @type {import('tailwindcss').Config} */
2export default {
3 content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
4 theme: {
5 extend: {},
6 },
7 plugins: [],
8};
src/index.css
1@tailwind base;
2@tailwind components;
3@tailwind utilities;
Client-side routing
So far, all the code demos and projects we discussed are single-page applications, meaning all the operations happen on the root page, /
. We never had the need to jump between different pages, until now.
As discussed above, the frontend app should have 4 different pages: Home, Create, Update, and Post. These pages should be hosted on different routes.
To achieve this, you must enable client-side routing. React doesn't come with client-side routing features, but luckily, there are a few third-party options we can choose. For this tutorial, we are going to use React Router, which can be installed with the following command:
1npm install react-router-dom
A lot of client-side routers require you to follow a strict file structure, and they will automatically translate the file structure into different routes. For instance:
1.
2├── page.jsx <===== Home page /
3└── community
4 ├── post
5 │ └── page.jsx <===== Community post page /community/post
6 └── page.jsx <===== Community page /community
Technically, the React Router doesn't require us to follow a strict file structure, but it is still better to keep organized. Navigate into src
and create a routes
directory:
1src
2├── assets
3│ └── react.svg
4├── components
5│ ├── Footer.jsx
6│ └── Navbar.jsx
7├── index.css
8├── main.jsx
9└── routes <===== Contains all the pages
10 ├── posts
11 │ ├── new.jsx <===== Corresponds to /posts/new
12 │ ├── show.jsx <===== Corresponds to /posts/show
13 │ └── update.jsx <===== Corresponds to /posts/update
14 └── root.jsx <===== Corresponds to /
This is the place where we are going to define all the pages, and the way you organize the files should match their corresponding route.
In this example, root.jsx
is the home page, posts/new.jsx
shows the new post page, posts/show.jsx
corresponds to the post page, and posts/update.jsx
displays the post update page.
These pages work exactly the same as React components, as we are going to see later.
To understand how the router works, redirect your attention to src/main.js
, the entry point of the frontend app. This is where we integrate React Router with our existing project.
main.jsx
1import { StrictMode } from "react";
2import { createRoot } from "react-dom/client";
3import "./index.css";
4
5import Navbar from "./components/Navbar";
6import Footer from "./components/Footer";
7
8// Import React router
9import { createBrowserRouter, RouterProvider } from "react-router-dom";
10
11// Import the pages
12import Root from "./routes/root";
13import New from "./routes/posts/new";
14import Show from "./routes/posts/show";
15import Update from "./routes/posts/update";
16
17// Create a browser router
18const router = createBrowserRouter([
19 {
20 path: "/",
21 element: <Root />,
22 },
23 {
24 path: "/posts/:id",
25 element: <Show />,
26 },
27 {
28 path: "/posts/new",
29 element: <New />,
30 },
31 {
32 path: "/posts/update/:id",
33 element: <Update />,
34 },
35]);
36
37createRoot(document.getElementById("root")).render(
38 <StrictMode>
39 <Navbar />
40 <RouterProvider router={router} />
41 <Footer />
42 </StrictMode>
43);
There are a few things we must do here. First, line 9, import createBrowserRouter
and RouterProvider
.
Second, line 12 to 15, import the pages. We'll discuss the individual pages later.
Line 18 to 35, the createBrowserRouter()
method defines the routes and their corresponding pages. The :id
syntax should be familiar to you, as it is the same as Express.js we covered before. They pass a segment of the URL to the corresponding page as a parameter.
And lastly, line 40, add the RouterProvider
and specify the router, which we just created. This time, instead of rendering <App />
, React Router will decide which page should be displayed according to the route.
Displaying a list of posts
Next, let's take a look at the individual pages. We'll start with the home page:
routes/root.jsx
1import { useState, useEffect } from "react";
2
3export default function Root() {
4 const [posts, setPosts] = useState([]);
5 const [loading, setLoading] = useState(true);
6 const [error, setError] = useState(null);
7
8 useEffect(() => {
9 const fetchPosts = async () => {
10 try {
11 const response = await fetch("http://localhost:3001/posts");
12 if (!response.ok) {
13 throw new Error("Failed to fetch posts");
14 }
15 const data = await response.json();
16 setPosts(data.posts);
17 } catch (err) {
18 setError(err.message);
19 } finally {
20 setLoading(false);
21 }
22 };
23
24 fetchPosts();
25 }, []);
26
27 if (loading) return <p>Loading...</p>;
28 if (error) return <p>Error: {error}</p>;
29
30 return (
31 <div className="max-w-4xl mx-auto p-4 font-serif">
32 <h1 className="text-3xl mb-4 text-center">Posts</h1>
33 <ul className="space-y-4">
34 {posts.map((post) => (
35 <li
36 key={post.id}
37 className="p-4 border rounded-lg shadow-sm bg-white">
38 <a href={`/posts/${post.id}`}>
39 <h2 className="text-2xl mb-2">{post.title}</h2>
40 </a>
41 <p className="text-gray-700">{post.content}</p>
42 </li>
43 ))}
44 </ul>
45 </div>
46 );
47}
In this example, the useEffect()
hook defines side effect, which will be executed when the page is loaded for the first time.
1useEffect(() => {
2 const fetchPosts = async () => {
3 try {
4 const response = await fetch("http://localhost:3001/posts");
5 if (!response.ok) {
6 throw new Error("Failed to fetch posts");
7 }
8 const data = await response.json();
9 setPosts(data.posts);
10 } catch (err) {
11 setError(err.message);
12 } finally {
13 setLoading(false);
14 }
15 };
16
17 fetchPosts();
18}, []);
Line 4, fetch("http://localhost:3001/posts")
sends a GET
request to http://localhost:3001/posts
. According to the backend routes we created previously, this request will be processed by the list()
method.
1app.post("/posts", postController.create);
2app.get("/posts/:id", postController.retrieve);
3app.put("/posts/:id", postController.update);
4app.delete("/posts/:id", postController.delete);
5app.get("/posts", postController.list); // <=====
1list: async function (req, res) {
2 try {
3 const posts = await prisma.post.findMany();
4 res.status(200).json({
5 posts: posts,
6 });
7 } catch (e) {
8 res.status(500).json({ message: "Something went wrong." });
9 }
10},
The list()
method will retrieve all the posts in the database (line 3), and return them in JSON format (line 4 to 6).
This response will then be passed to response
in the frontend. The response body is then parsed, processed, and then assigned to data
. And finally, data.posts
updates the posts
state.
The posts
state is then used to create a list of posts using JSX like this:
1{
2 posts.map((post) => (
3 <li key={post.id} className="p-4 border rounded-lg shadow-sm bg-white">
4 <a href={`/posts/${post.id}`}>
5 <h2 className="text-2xl mb-2">{post.title}</h2>
6 </a>
7 <p className="text-gray-700">{post.content}</p>
8 </li>
9 ));
10}
Creating a new post
The new post page contains a form. When the form is submitted, the event handler handleSubmit() will be executed:
routes/posts/new.jsx
1import { useState } from "react";
2
3export default function New() {
4 const [title, setTitle] = useState("");
5 const [content, setContent] = useState("");
6 const [successMessage, setSuccessMessage] = useState("");
7 const [errorMessage, setErrorMessage] = useState("");
8
9 const handleSubmit = async (e) => {
10 e.preventDefault();
11
12 try {
13 const response = await fetch("http://localhost:3001/posts", {
14 method: "POST",
15 headers: { "Content-Type": "application/json" },
16 body: JSON.stringify({ title, content }),
17 });
18
19 if (!response.ok) {
20 throw new Error("Failed to create post");
21 }
22
23 const data = await response.json();
24 setSuccessMessage(`Post created successfully with ID: ${data.post.id}`);
25 setTitle("");
26 setContent("");
27 } catch (err) {
28 setErrorMessage(err.message);
29 }
30 };
31
32 return (
33 <div className="max-w-3xl mx-auto p-6">
34 <h1 className="text-2xl font-bold mb-6 text-center">Create a New Post</h1>
35 {successMessage && (
36 <p className="text-green-600 mb-4">{successMessage}</p>
37 )}
38 {errorMessage && <p className="text-red-600 mb-4">{errorMessage}</p>}
39 <form onSubmit={handleSubmit} className="space-y-4">
40 <input
41 type="text"
42 id="title"
43 value={title}
44 placeholder="Title"
45 onChange={(e) => setTitle(e.target.value)}
46 className="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2 focus:ring focus:ring-indigo-300"
47 required
48 />
49 <textarea
50 id="content"
51 value={content}
52 placeholder="Content"
53 onChange={(e) => setContent(e.target.value)}
54 className="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2 focus:ring focus:ring-indigo-300"
55 rows="5"
56 required></textarea>
57 <button
58 type="submit"
59 className="w-full bg-indigo-600 text-white font-bold py-2 px-4 rounded-md hover:bg-indigo-700 transition">
60 Create Post
61 </button>
62 </form>
63 </div>
64 );
65}
In this case, you need to send a POST
request to http://localhost:3001/posts
like this:
1const response = await fetch("http://localhost:3001/posts", {
2 method: "POST",
3 headers: { "Content-Type": "application/json" },
4 body: JSON.stringify({ title, content }),
5});
You must specify the method and request body, and the header should have a Content-Type
key, specifying the format of the request body, which is JSON in our example.
Retrieving and displaying a post
routes/posts/show.jsx
1import { useState, useEffect } from "react";
2import { useParams } from "react-router-dom";
3
4export default function Show() {
5 const { id } = useParams();
6 const [post, setPost] = useState([]);
7 const [loading, setLoading] = useState(true);
8 const [error, setError] = useState(null);
9
10 useEffect(() => {
11 const fetchPost = async () => {
12 try {
13 const response = await fetch(`http://localhost:3001/posts/${id}`);
14 if (!response.ok) {
15 throw new Error("Failed to fetch posts");
16 }
17 const data = await response.json();
18 setPost(data.post);
19 } catch (err) {
20 setError(err.message);
21 } finally {
22 setLoading(false);
23 }
24 };
25
26 fetchPost();
27 }, []);
28
29 if (loading) return <p>Loading...</p>;
30 if (error) return <p>Error: {error}</p>;
31
32 return (
33 <div className="max-w-4xl mx-auto p-4 font-serif flex flex-col gap-4">
34 <h2 className="text-2xl mb-2">{post.title}</h2>
35 <p className="text-gray-700">{post.content}</p>
36 <a
37 href={`/posts/update/${id}`}
38 className="w-full bg-indigo-600 text-white py-2 px-4 rounded-md hover:bg-indigo-700 transition">
39 Update Post
40 </a>
41 </div>
42 );
43}
As for the post page, the only thing you must pay attention to is how to retrieve the id
parameter. Recall that we specified an id
parameter when defining the routes:
1const router = createBrowserRouter([
2 {
3 path: "/",
4 element: <Root />,
5 },
6 {
7 path: "/posts/:id",
8 element: <Show />,
9 },
10 {
11 path: "/posts/new",
12 element: <New />,
13 },
14 {
15 path: "/posts/update/:id",
16 element: <Update />,
17 },
18]);
To access this id
in the <Show />
page, import useParams
:
1import { useParams } from "react-router-dom";
This hook returns an object of parameters, and in our case, the id
parameter can be accessed like this:
1const { id } = useParams();
Updating and deleting a post
The post update page is essentially the combination of the create and post page. To create the update page, you need to retrieve the corresponding post based on id
, and then use the retrieved data to fill the form.
routes/posts/update.jsx
1import { useState, useEffect } from "react";
2import { useParams } from "react-router-dom";
3
4export default function Update() {
5 const { id } = useParams();
6 const [title, setTitle] = useState("");
7 const [content, setContent] = useState("");
8 const [successMessage, setSuccessMessage] = useState("");
9 const [errorMessage, setErrorMessage] = useState("");
10
11 useEffect(() => {
12 const fetchPost = async () => {
13 try {
14 const response = await fetch(`http://localhost:3001/posts/${id}`);
15 if (!response.ok) {
16 throw new Error("Failed to fetch posts");
17 }
18 const data = await response.json();
19 setTitle(data.post.title);
20 setContent(data.post.content);
21 } catch (err) {
22 setError(err.message);
23 }
24 };
25
26 fetchPost();
27 }, []);
28
29 const handleSubmit = async (e) => {
30 e.preventDefault();
31
32 try {
33 const response = await fetch(`http://localhost:3001/posts/${id}`, {
34 method: "PUT",
35 headers: { "Content-Type": "application/json" },
36 body: JSON.stringify({ title, content }),
37 });
38
39 if (!response.ok) {
40 throw new Error("Failed to update post");
41 }
42
43 const data = await response.json();
44 setSuccessMessage(`Post ${data.post.title} updated.`);
45 setTitle("");
46 setContent("");
47 } catch (err) {
48 setErrorMessage(err.message);
49 }
50 };
51
52 return (
53 <div className="max-w-3xl mx-auto p-6">
54 <h1 className="text-2xl font-bold mb-6 text-center">Update "{title}"</h1>
55 {successMessage && (
56 <p className="text-green-600 mb-4">{successMessage}</p>
57 )}
58 {errorMessage && <p className="text-red-600 mb-4">{errorMessage}</p>}
59 <form onSubmit={handleSubmit} className="space-y-4">
60 <input
61 type="text"
62 id="title"
63 value={title}
64 placeholder="Title"
65 onChange={(e) => setTitle(e.target.value)}
66 className="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2 focus:ring focus:ring-indigo-300"
67 required
68 />
69
70 <textarea
71 id="content"
72 value={content}
73 placeholder="Content"
74 onChange={(e) => setContent(e.target.value)}
75 className="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2 focus:ring focus:ring-indigo-300"
76 rows="5"
77 required></textarea>
78
79 <button
80 type="submit"
81 className="w-full bg-indigo-600 text-white font-bold py-2 px-4 rounded-md hover:bg-indigo-700 transition">
82 Update Post
83 </button>
84 </form>
85 </div>
86 );
87}
And finally, you can add a delete button on this page to delete a post based on id
.
routes/posts/update.jsx
1import { useState, useEffect } from "react";
2import { useParams } from "react-router-dom";
3
4export default function Update() {
5 const { id } = useParams();
6 const [title, setTitle] = useState("");
7 const [content, setContent] = useState("");
8 const [successMessage, setSuccessMessage] = useState("");
9 const [errorMessage, setErrorMessage] = useState("");
10
11 . . .
12
13 const handleDelete = async () => {
14 try {
15 const response = await fetch(`http://localhost:3001/posts/${id}`, {
16 method: "DELETE",
17 });
18
19 if (!response.ok) {
20 throw new Error("Failed to delete post");
21 }
22
23 const data = await response.json();
24 setSuccessMessage(`Post deleted.`);
25 setTitle("");
26 setContent("");
27 } catch (err) {
28 setErrorMessage(err.message);
29 }
30 };
31
32 return (
33 <div className="max-w-3xl mx-auto p-6">
34 <h1 className="text-2xl font-bold mb-6 text-center">Update "{title}"</h1>
35 {successMessage && (
36 <p className="text-green-600 mb-4">{successMessage}</p>
37 )}
38 {errorMessage && <p className="text-red-600 mb-4">{errorMessage}</p>}
39
40 {/* . . . */}
41
42 <button
43 onClick={handleDelete}
44 className="w-full bg-red-600 text-white font-bold py-2 px-4 rounded-md hover:bg-red-700 transition">
45 Delete
46 </button>
47 </div>
48 );
49}