farheen_blog_app 1.0.6 → 1.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "farheen_blog_app",
3
3
  "private": false,
4
- "version": "1.0.6",
4
+ "version": "1.0.7",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "dev": "vite",
@@ -3,7 +3,6 @@ import { createPost } from "../services/postService";
3
3
 
4
4
  const CreatePost = () => {
5
5
  const [formData, setFormData] = useState({
6
- title: "",
7
6
  content: "",
8
7
  image: null
9
8
  });
@@ -29,11 +28,6 @@ const CreatePost = () => {
29
28
 
30
29
  const data = new FormData();
31
30
 
32
- data.append(
33
- "title",
34
- formData.title
35
- );
36
-
37
31
  data.append(
38
32
  "content",
39
33
  formData.content
@@ -51,7 +45,6 @@ const CreatePost = () => {
51
45
  alert("Post Created");
52
46
 
53
47
  setFormData({
54
- title: "",
55
48
  content: "",
56
49
  image: null
57
50
  });
@@ -78,16 +71,6 @@ const CreatePost = () => {
78
71
  className="flex flex-col gap-4"
79
72
  >
80
73
 
81
- <input
82
- type="text"
83
- name="title"
84
- placeholder="Enter Title"
85
- className="border p-3 rounded"
86
- value={formData.title}
87
- onChange={handleChange}
88
- required
89
- />
90
-
91
74
  <textarea
92
75
  name="content"
93
76
  placeholder="Enter Content"
@@ -0,0 +1,100 @@
1
+ import { useState, useEffect } from "react";
2
+ import { useParams, useNavigate } from "react-router-dom";
3
+ import api from "../../services/api";
4
+
5
+ const EditBlog = () => {
6
+ const { id } = useParams();
7
+ const navigate = useNavigate();
8
+
9
+ const [formData, setFormData] = useState({
10
+ content: "",
11
+ image: null,
12
+ });
13
+ const [existingImage, setExistingImage] = useState(null);
14
+ const [loading, setLoading] = useState(false);
15
+
16
+ useEffect(() => {
17
+ const fetchPost = async () => {
18
+ try {
19
+ const res = await api.get(`/posts/${id}`);
20
+ setFormData({
21
+ content: res.data.content || "",
22
+ image: null
23
+ });
24
+ setExistingImage(res.data.image_url ? `http://localhost:3000${res.data.image_url}` : null);
25
+ } catch (error) {
26
+ console.log(error);
27
+ alert("Failed to fetch post details");
28
+ }
29
+ };
30
+ fetchPost();
31
+ }, [id]);
32
+
33
+ const handleChange = (e) => {
34
+ const { name, value, files } = e.target;
35
+ setFormData({
36
+ ...formData,
37
+ [name]: files ? files[0] : value
38
+ });
39
+ };
40
+
41
+ const handleSubmit = async (e) => {
42
+ e.preventDefault();
43
+ try {
44
+ setLoading(true);
45
+ const data = new FormData();
46
+ data.append("content", formData.content);
47
+ if (formData.image) {
48
+ data.append("image", formData.image);
49
+ }
50
+
51
+ await api.put(`/posts/${id}`, data);
52
+ alert("Post Updated Successfully");
53
+ navigate("/admin/blogs");
54
+ } catch (error) {
55
+ console.log(error);
56
+ alert(error.response?.data?.message || "Something went wrong");
57
+ } finally {
58
+ setLoading(false);
59
+ }
60
+ };
61
+
62
+ return (
63
+ <div className="max-w-xl mx-auto mt-10 p-6 bg-white rounded-lg shadow">
64
+ <h1 className="text-2xl font-bold mb-6 text-gray-800">Edit Blog</h1>
65
+ <form onSubmit={handleSubmit} className="flex flex-col gap-4">
66
+ {/* Title removed as it's not used in schema */}
67
+ <textarea
68
+ name="content"
69
+ placeholder="Enter Content"
70
+ className="border border-gray-300 p-3 rounded h-40 focus:outline-none focus:ring-2 focus:ring-blue-500"
71
+ value={formData.content}
72
+ onChange={handleChange}
73
+ required
74
+ />
75
+ {existingImage && !formData.image && (
76
+ <div className="mb-2">
77
+ <p className="text-sm text-gray-500 mb-1">Current Image:</p>
78
+ <img src={existingImage} alt="Current" className="h-32 object-cover rounded" />
79
+ </div>
80
+ )}
81
+ <input
82
+ type="file"
83
+ name="image"
84
+ accept="image/*"
85
+ onChange={handleChange}
86
+ className="text-gray-600"
87
+ />
88
+ <button
89
+ type="submit"
90
+ disabled={loading}
91
+ className="bg-blue-600 hover:bg-blue-700 text-white font-semibold py-3 rounded transition-colors disabled:opacity-50"
92
+ >
93
+ {loading ? "Updating..." : "Update Post"}
94
+ </button>
95
+ </form>
96
+ </div>
97
+ );
98
+ };
99
+
100
+ export default EditBlog;
@@ -1,28 +1,52 @@
1
1
  import { useEffect, useState } from "react";
2
+ import { Link } from "react-router-dom";
2
3
  import api from "../../services/api";
3
4
  import useDebounce from "../../hooks/useDebounce";
5
+ import { getPosts } from "../../services/postService";
4
6
 
5
7
  const ManageBlogs = () => {
6
8
 
7
9
  // for pagination and debounce search
8
10
  const [page, setPage] = useState(1);
9
- const [totalPage, setTotalPage] = useState(1);
11
+ const [totalPages, setTotalPages] = useState(1);
10
12
  const [search, setSearch] = useState("");
11
13
 
12
14
  const debouncedSearch = useDebounce(search, 500);
13
15
 
14
16
  const [blogs, setBlogs] = useState([]);
17
+ const [loading, setLoading] = useState(false);
15
18
 
16
19
  const getBlogs = async () => {
17
20
  try {
18
-
19
- const res = await api.get(`/admin/blogs/posts?page=${page}&search=${debouncedSearch}`);
20
-
21
- setBlogs(res.data);
22
- setTotalPage(res.data.totalPges);
21
+ setLoading(true);
22
+ // Fallback to postService getPosts if no pagination is needed, or keep api.get
23
+ const res = await api.get(`/admin/blogs/posts?page=${page}&search=${debouncedSearch}`).catch(async () => {
24
+ // If admin route doesn't exist, use the regular getPosts
25
+ return { data: await getPosts() };
26
+ });
27
+
28
+ const fetchedBlogs = res.data?.posts || res.data || [];
29
+
30
+ setBlogs(Array.isArray(fetchedBlogs) ? fetchedBlogs : []);
31
+ setTotalPages(res.data?.totalPages || 1);
23
32
 
24
33
  } catch (error) {
25
- console.log(error);
34
+ console.log("Failed to fetch blogs", error);
35
+ setBlogs([]);
36
+ } finally {
37
+ setLoading(false);
38
+ }
39
+ };
40
+
41
+ const handleDelete = async (id) => {
42
+ if (window.confirm("Are you sure you want to delete this blog?")) {
43
+ try {
44
+ await api.delete(`/posts/${id}`);
45
+ getBlogs();
46
+ } catch (error) {
47
+ console.log(error);
48
+ alert("Failed to delete blog");
49
+ }
26
50
  }
27
51
  };
28
52
 
@@ -31,60 +55,100 @@ const ManageBlogs = () => {
31
55
  }, [page, debouncedSearch]);
32
56
 
33
57
  return (
34
- <div>
35
- <input type="text" value={search} placeholder="search" onChange={(e) => setSearch(e.target.value)} />
36
-
37
- <h1>Manage Blogs</h1>
38
-
39
- {
40
- blogs.map((blog) => (
41
- <div key={blog._id}>
42
-
43
- <h3>{blog.title}</h3>
44
-
45
- <p>
46
- {blog.createdBy?.name}
47
- </p>
48
-
49
- </div>
50
- ))
51
- }
52
-
53
- {/* pagination */}
54
- <div>
55
- <button
56
- disabled={
57
- page === 1
58
- }
59
- onClick={() =>
60
- setPage(
61
- page - 1
62
- )
63
- }
64
- >
65
- Prev
66
- </button>
58
+ <div className="p-6 max-w-6xl mx-auto">
59
+ <div className="flex justify-between items-center mb-6">
60
+ <h1 className="text-3xl font-bold text-gray-800">Manage Blogs</h1>
61
+
62
+ <div className="relative">
63
+ <input
64
+ type="text"
65
+ value={search}
66
+ placeholder="Search blogs..."
67
+ onChange={(e) => setSearch(e.target.value)}
68
+ className="border border-gray-300 rounded-lg pl-4 pr-10 py-2 w-64 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent shadow-sm"
69
+ />
70
+ </div>
71
+ </div>
67
72
 
68
- <span>
69
- {" "}
70
- {page} / {
71
- totalPages
72
- }{" "}
73
- </span>
73
+ <div className="bg-white rounded-lg shadow overflow-hidden">
74
+ {loading ? (
75
+ <div className="p-10 text-center text-gray-500">Loading blogs...</div>
76
+ ) : (
77
+ <table className="w-full text-left border-collapse">
78
+ <thead className="bg-gray-50 border-b border-gray-200">
79
+ <tr>
80
+ <th className="p-4 text-sm font-semibold text-gray-600">Image</th>
81
+ <th className="p-4 text-sm font-semibold text-gray-600">Content</th>
82
+ <th className="p-4 text-sm font-semibold text-gray-600">Author</th>
83
+ <th className="p-4 text-sm font-semibold text-gray-600">Date</th>
84
+ <th className="p-4 text-sm font-semibold text-gray-600">Status</th>
85
+ <th className="p-4 text-sm font-semibold text-gray-600 text-right">Actions</th>
86
+ </tr>
87
+ </thead>
88
+ <tbody className="divide-y divide-gray-200">
89
+ {blogs.length === 0 ? (
90
+ <tr>
91
+ <td colSpan="6" className="p-8 text-center text-gray-500">
92
+ No blogs found.
93
+ </td>
94
+ </tr>
95
+ ) : (
96
+ blogs.map((blog) => (
97
+ <tr key={blog.id} className="hover:bg-gray-50 transition-colors">
98
+ <td className="p-4">
99
+ {blog.image_url ? (
100
+ <img src={`http://localhost:3000${blog.image_url}`} alt="Blog post" className="h-12 w-12 object-cover rounded shadow-sm" />
101
+ ) : (
102
+ <div className="h-12 w-12 bg-gray-100 rounded flex items-center justify-center text-xs text-gray-400">N/A</div>
103
+ )}
104
+ </td>
105
+ <td className="p-4">
106
+ <p className="font-medium text-gray-800 line-clamp-2">{blog.content || 'No content'}</p>
107
+ </td>
108
+ <td className="p-4 text-gray-600">
109
+ {blog.User?.name || 'Unknown'}
110
+ </td>
111
+ <td className="p-4 text-gray-500 text-sm">
112
+ {blog.created_at ? new Date(blog.created_at).toLocaleDateString() : 'N/A'}
113
+ </td>
114
+ <td className="p-4">
115
+ <span className="px-3 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800">
116
+ Published
117
+ </span>
118
+ </td>
119
+ <td className="p-4 text-right whitespace-nowrap">
120
+ <Link to={`/admin/blogs/edit/${blog.id}`} className="text-blue-600 hover:text-blue-900 mr-4 font-medium text-sm">Edit</Link>
121
+ <button onClick={() => handleDelete(blog.id)} className="text-red-600 hover:text-red-900 font-medium text-sm">Delete</button>
122
+ </td>
123
+ </tr>
124
+ )))}
125
+ </tbody>
126
+ </table>
127
+ )}
128
+ </div>
74
129
 
75
- <button
76
- disabled={
77
- page ===
78
- totalPages
79
- }
80
- onClick={() =>
81
- setPage(
82
- page + 1
83
- )
84
- }
85
- >
86
- Next
87
- </button>
130
+ {/* Pagination */}
131
+ <div className="flex items-center justify-between mt-6">
132
+ <p className="text-sm text-gray-600">
133
+ Showing page <span className="font-semibold">{page}</span> of <span className="font-semibold">{totalPages}</span>
134
+ </p>
135
+ <div className="flex gap-2">
136
+ <button
137
+ disabled={page === 1}
138
+ onClick={() => setPage(page - 1)}
139
+ className="px-4 py-2 border border-gray-300 rounded-md text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
140
+ >
141
+ Previous
142
+ </button>
143
+
144
+ <button
145
+ disabled={page >= totalPages}
146
+ onClick={() => setPage(page + 1)}
147
+ className="px-4 py-2 border border-gray-300 rounded-md text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
148
+ >
149
+ Next
150
+ </button>
151
+ </div>
88
152
  </div>
89
153
 
90
154
  </div>
@@ -16,6 +16,7 @@ import Register from "../pages/Register";
16
16
  import { ProtectedRoute } from "../middleware/ProtectedRoute";
17
17
  import SinglePost from "../pages/SinglePost";
18
18
  import CreateUser from "../pages/admin/CreateUser";
19
+ import EditBlog from "../pages/admin/EditBlog";
19
20
 
20
21
  function AppRoutes() {
21
22
  return (
@@ -61,6 +62,10 @@ function AppRoutes() {
61
62
  path="blogs"
62
63
  element={<ManageBlogs />}
63
64
  />
65
+ <Route
66
+ path="blogs/edit/:id"
67
+ element={<EditBlog />}
68
+ />
64
69
  <Route
65
70
  path="create"
66
71
  element={<CreatePost />}