How to Build a Blog with React

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.

bash
1mkdir backend

Change into it, and then initialize a new project:

bash
1cd backend
bash
1npm init

A package.json file should be generated, remember to add "type": "module" to enable ES modules.

package.json

json
1{
2  "name": "backend",
3  "type": "module",
4  . . .
5}

Install the express package:

bash
1npm install express

Then, set up your project structure like this:

text
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:

bash
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

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

env
1DATABASE_URL="file:./database.sqlite"

And next, run the migrations to apply the changes.

bash
1npx prisma migrate dev

A database.sqlite database should be generated with the following structure:

database structure

Lastly, don't forget to set up the Prisma client.

bash
1npm install @prisma/client

libs/prisma.js

javascript
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

javascript
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

javascript
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.

javascript
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.

javascript
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;
javascript
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;
javascript
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.

bash
1npm install cors

Then enable it with the use() method.

index.js

javascript
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.

bash
1npm create vite@latest frontend
text
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
text
1? Select a variant: › - Use arrow-keys. Return to submit.
2    TypeScript
3    TypeScript + SWC
4    JavaScript
5❯   JavaScript + SWC
6    Remix ↗
text
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.

bash
1cd frontend
bash
1npm install

And for demonstration purposes, we are going to use TailwindCSS for styling.

bash
1npm install -D tailwindcss postcss autoprefixer
bash
1npx tailwindcss init -p

tailwind.config.js

javascript
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

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:

bash
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:

text
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:

text
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

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

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.

javascript
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.

javascript
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); // <=====
javascript
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:

jsx
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

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:

javascript
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

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:

javascript
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:

javascript
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:

javascript
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

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

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}