farheen_blog_app 1.0.5 → 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/info.md +13 -0
- package/package.json +1 -1
- package/src/components/CommentTable.jsx +28 -0
- package/src/components/admin/AdminSidebar.jsx +49 -15
- package/src/components/admin/UserTable.jsx +19 -17
- package/src/middleware/ProtectedRoute.jsx +1 -1
- package/src/pages/CreatePost.jsx +0 -17
- package/src/pages/Register.jsx +1 -1
- package/src/pages/SinglePost.jsx +40 -0
- package/src/pages/admin/CreateUser.jsx +51 -0
- package/src/pages/admin/EditBlog.jsx +100 -157
- package/src/pages/admin/ManageBlogs.jsx +123 -59
- package/src/pages/admin/ManageUsers.jsx +3 -11
- package/src/routes/AppRoutes.jsx +22 -7
- package/src/services/{authService → authService.js} +15 -15
- package/src/utils/checkPermission.js +28 -0
- package/src/pages/admin/EditUser.jsx +0 -106
package/info.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Project Information
|
|
2
|
+
|
|
3
|
+
## Pages Created
|
|
4
|
+
- **Create User Page**: Added `CreateUser.jsx` inside the admin folder to allow admins to create new users. Added its route at `/admin/users/create`.
|
|
5
|
+
- **Single Post Page**: Added `SinglePost.jsx` page to view a specific post in detail along with its comments. Added its route at `/post/:id`.
|
|
6
|
+
- **Comment Table**: Created `CommentTable.jsx` component to cleanly display the list of comments on the `SinglePost` page.
|
|
7
|
+
|
|
8
|
+
## Components Updated
|
|
9
|
+
- **User Table**: Modified `UserTable.jsx` to dynamically render user data passed via props.
|
|
10
|
+
- **Manage Users**: Updated `ManageUsers.jsx` to utilize the improved `UserTable` instead of simply rendering user data in `div` elements.
|
|
11
|
+
|
|
12
|
+
## Clean Up
|
|
13
|
+
- Removed unused files from the admin section (`EditBlog.jsx` and `EditUser.jsx`) to clean up the codebase.
|
package/package.json
CHANGED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
const CommentTable = ({ comments }) => {
|
|
2
|
+
if (!comments || comments.length === 0) {
|
|
3
|
+
return <p>No comments yet.</p>;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
return (
|
|
7
|
+
<table className="w-full text-left border-collapse border border-gray-200">
|
|
8
|
+
<thead className="bg-gray-100">
|
|
9
|
+
<tr>
|
|
10
|
+
<th className="p-3 border-b">User</th>
|
|
11
|
+
<th className="p-3 border-b">Comment</th>
|
|
12
|
+
<th className="p-3 border-b">Date</th>
|
|
13
|
+
</tr>
|
|
14
|
+
</thead>
|
|
15
|
+
<tbody>
|
|
16
|
+
{comments.map((comment) => (
|
|
17
|
+
<tr key={comment._id} className="border-b hover:bg-gray-50">
|
|
18
|
+
<td className="p-3">{comment.user?.name || "Anonymous"}</td>
|
|
19
|
+
<td className="p-3">{comment.text}</td>
|
|
20
|
+
<td className="p-3">{new Date(comment.createdAt).toLocaleDateString()}</td>
|
|
21
|
+
</tr>
|
|
22
|
+
))}
|
|
23
|
+
</tbody>
|
|
24
|
+
</table>
|
|
25
|
+
);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export default CommentTable;
|
|
@@ -1,29 +1,63 @@
|
|
|
1
1
|
import { NavLink } from "react-router-dom";
|
|
2
|
+
import { hasAnyPermission } from "../../utils/checkPermission";
|
|
2
3
|
|
|
3
4
|
const AdminSidebar = () => {
|
|
5
|
+
|
|
6
|
+
const menuItems = [
|
|
7
|
+
{
|
|
8
|
+
path: "/admin",
|
|
9
|
+
title: "Dashboard",
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
path: "/admin/users",
|
|
13
|
+
title: "Manage Users",
|
|
14
|
+
module: "USERS",
|
|
15
|
+
actions: ["READ", "UPDATE", "DELETE"]
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
path: "/admin/users/create",
|
|
19
|
+
title: "Create User",
|
|
20
|
+
module: "USERS",
|
|
21
|
+
actions: ["CREATE"]
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
path: "/admin/blogs",
|
|
25
|
+
title: "Manage Blogs",
|
|
26
|
+
module: "POSTS",
|
|
27
|
+
actions: ["READ", "UPDATE", "DELETE"]
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
path: "/admin/create",
|
|
31
|
+
title: "Create Post",
|
|
32
|
+
module: "POSTS",
|
|
33
|
+
actions: ["CREATE"]
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
path: "/admin/settings",
|
|
37
|
+
title: "Settings",
|
|
38
|
+
}
|
|
39
|
+
];
|
|
40
|
+
|
|
4
41
|
return (
|
|
5
42
|
<aside className="sidebar">
|
|
6
43
|
<h2 className="logo">Blogify</h2>
|
|
7
44
|
<nav>
|
|
45
|
+
{menuItems.map((item, index) => {
|
|
46
|
+
let hasAccess = true;
|
|
8
47
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
<NavLink to="/admin/users">
|
|
14
|
-
Users
|
|
15
|
-
</NavLink>
|
|
48
|
+
if (item.module && item.actions) {
|
|
49
|
+
hasAccess = hasAnyPermission(item.module, item.actions);
|
|
50
|
+
}
|
|
16
51
|
|
|
17
|
-
|
|
18
|
-
Blogs
|
|
19
|
-
</NavLink>
|
|
20
|
-
|
|
21
|
-
<NavLink to="/admin/settings">
|
|
22
|
-
Settings
|
|
23
|
-
</NavLink>
|
|
52
|
+
if (!hasAccess) return null;
|
|
24
53
|
|
|
54
|
+
return (
|
|
55
|
+
<NavLink key={index} to={item.path} end>
|
|
56
|
+
{item.title}
|
|
57
|
+
</NavLink>
|
|
58
|
+
);
|
|
59
|
+
})}
|
|
25
60
|
</nav>
|
|
26
|
-
|
|
27
61
|
</aside>
|
|
28
62
|
);
|
|
29
63
|
};
|
|
@@ -1,26 +1,28 @@
|
|
|
1
|
-
const UserTable = () => {
|
|
1
|
+
const UserTable = ({ users }) => {
|
|
2
2
|
return (
|
|
3
|
-
<table className="user-table">
|
|
3
|
+
<table className="user-table w-full text-left border-collapse">
|
|
4
4
|
<thead>
|
|
5
|
-
<tr>
|
|
6
|
-
<th>Name</th>
|
|
7
|
-
<th>Email</th>
|
|
8
|
-
<th>Role</th>
|
|
9
|
-
<th>Action</th>
|
|
5
|
+
<tr className="border-b">
|
|
6
|
+
<th className="p-3">Name</th>
|
|
7
|
+
<th className="p-3">Email</th>
|
|
8
|
+
<th className="p-3">Role</th>
|
|
9
|
+
<th className="p-3">Action</th>
|
|
10
10
|
</tr>
|
|
11
11
|
</thead>
|
|
12
12
|
|
|
13
13
|
<tbody>
|
|
14
|
-
|
|
15
|
-
<
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
<
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
14
|
+
{users?.map(user => (
|
|
15
|
+
<tr key={user._id} className="border-b">
|
|
16
|
+
<td className="p-3">{user.name}</td>
|
|
17
|
+
<td className="p-3">{user.email}</td>
|
|
18
|
+
<td className="p-3">{user.role}</td>
|
|
19
|
+
<td className="p-3">
|
|
20
|
+
<button className="delete-btn bg-red-500 text-white px-3 py-1 rounded">
|
|
21
|
+
Delete
|
|
22
|
+
</button>
|
|
23
|
+
</td>
|
|
24
|
+
</tr>
|
|
25
|
+
))}
|
|
24
26
|
</tbody>
|
|
25
27
|
</table>
|
|
26
28
|
)
|
package/src/pages/CreatePost.jsx
CHANGED
|
@@ -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"
|
package/src/pages/Register.jsx
CHANGED
package/src/pages/SinglePost.jsx
CHANGED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { useParams } from "react-router-dom";
|
|
3
|
+
import api from "../services/api";
|
|
4
|
+
import CommentTable from "../components/CommentTable";
|
|
5
|
+
|
|
6
|
+
const SinglePost = () => {
|
|
7
|
+
const { id } = useParams();
|
|
8
|
+
const [post, setPost] = useState(null);
|
|
9
|
+
const [comments, setComments] = useState([]);
|
|
10
|
+
|
|
11
|
+
const fetchPostAndComments = async () => {
|
|
12
|
+
try {
|
|
13
|
+
const postRes = await api.get(`/posts/${id}`);
|
|
14
|
+
setPost(postRes.data);
|
|
15
|
+
const commentsRes = await api.get(`/posts/${id}/comments`);
|
|
16
|
+
setComments(commentsRes.data);
|
|
17
|
+
} catch (error) {
|
|
18
|
+
console.log("Error fetching post or comments", error);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
fetchPostAndComments();
|
|
24
|
+
}, [id]);
|
|
25
|
+
|
|
26
|
+
if (!post) return <div className="text-center p-10">Loading...</div>;
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<div className="max-w-4xl mx-auto p-4 mt-10">
|
|
30
|
+
<h1 className="text-4xl font-bold mb-4">{post.title}</h1>
|
|
31
|
+
{post.image && <img src={post.image} alt={post.title} className="w-full h-96 object-cover mb-8 rounded shadow" />}
|
|
32
|
+
<div className="text-gray-800 text-lg mb-12 whitespace-pre-wrap">{post.content}</div>
|
|
33
|
+
|
|
34
|
+
<h2 className="text-2xl font-bold mb-4">Comments</h2>
|
|
35
|
+
<CommentTable comments={comments} />
|
|
36
|
+
</div>
|
|
37
|
+
);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export default SinglePost;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import api from "../../services/api";
|
|
3
|
+
|
|
4
|
+
const CreateUser = () => {
|
|
5
|
+
const [formData, setFormData] = useState({
|
|
6
|
+
name: "",
|
|
7
|
+
email: "",
|
|
8
|
+
password: "",
|
|
9
|
+
role: "user"
|
|
10
|
+
});
|
|
11
|
+
const [loading, setLoading] = useState(false);
|
|
12
|
+
|
|
13
|
+
const handleChange = (e) => {
|
|
14
|
+
setFormData({ ...formData, [e.target.name]: e.target.value });
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const handleSubmit = async (e) => {
|
|
18
|
+
e.preventDefault();
|
|
19
|
+
try {
|
|
20
|
+
setLoading(true);
|
|
21
|
+
await api.post("/admin/users", formData);
|
|
22
|
+
alert("User created successfully");
|
|
23
|
+
setFormData({ name: "", email: "", password: "", role: "user" });
|
|
24
|
+
} catch (error) {
|
|
25
|
+
console.log(error);
|
|
26
|
+
alert("Failed to create user");
|
|
27
|
+
} finally {
|
|
28
|
+
setLoading(false);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<div className="max-w-xl mx-auto mt-10">
|
|
34
|
+
<h1 className="text-2xl font-bold mb-4">Create User</h1>
|
|
35
|
+
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
|
|
36
|
+
<input type="text" name="name" placeholder="Name" value={formData.name} onChange={handleChange} required className="border p-3 rounded" />
|
|
37
|
+
<input type="email" name="email" placeholder="Email" value={formData.email} onChange={handleChange} required className="border p-3 rounded" />
|
|
38
|
+
<input type="password" name="password" placeholder="Password" value={formData.password} onChange={handleChange} required className="border p-3 rounded" />
|
|
39
|
+
<select name="role" value={formData.role} onChange={handleChange} className="border p-3 rounded">
|
|
40
|
+
<option value="user">User</option>
|
|
41
|
+
<option value="admin">Admin</option>
|
|
42
|
+
</select>
|
|
43
|
+
<button type="submit" disabled={loading} className="bg-black text-white p-3 rounded">
|
|
44
|
+
{loading ? "Creating..." : "Create User"}
|
|
45
|
+
</button>
|
|
46
|
+
</form>
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export default CreateUser;
|
|
@@ -1,157 +1,100 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
await updatePost(
|
|
102
|
-
id,
|
|
103
|
-
data
|
|
104
|
-
);
|
|
105
|
-
|
|
106
|
-
navigate(
|
|
107
|
-
"/admin/blogs"
|
|
108
|
-
);
|
|
109
|
-
};
|
|
110
|
-
|
|
111
|
-
return (
|
|
112
|
-
<form
|
|
113
|
-
onSubmit={
|
|
114
|
-
handleSubmit
|
|
115
|
-
}
|
|
116
|
-
className="flex flex-col gap-4"
|
|
117
|
-
>
|
|
118
|
-
|
|
119
|
-
<input
|
|
120
|
-
name="title"
|
|
121
|
-
value={
|
|
122
|
-
formData.title
|
|
123
|
-
}
|
|
124
|
-
onChange={
|
|
125
|
-
handleChange
|
|
126
|
-
}
|
|
127
|
-
/>
|
|
128
|
-
|
|
129
|
-
<textarea
|
|
130
|
-
name="content"
|
|
131
|
-
value={
|
|
132
|
-
formData.content
|
|
133
|
-
}
|
|
134
|
-
onChange={
|
|
135
|
-
handleChange
|
|
136
|
-
}
|
|
137
|
-
/>
|
|
138
|
-
|
|
139
|
-
<input
|
|
140
|
-
type="file"
|
|
141
|
-
name="image"
|
|
142
|
-
onChange={
|
|
143
|
-
handleChange
|
|
144
|
-
}
|
|
145
|
-
/>
|
|
146
|
-
|
|
147
|
-
<button
|
|
148
|
-
type="submit"
|
|
149
|
-
>
|
|
150
|
-
Update Blog
|
|
151
|
-
</button>
|
|
152
|
-
|
|
153
|
-
</form>
|
|
154
|
-
);
|
|
155
|
-
};
|
|
156
|
-
|
|
157
|
-
export default EditPost;
|
|
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 [
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
<
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
)
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useEffect, useState } from "react";
|
|
2
2
|
import api from "../../services/api";
|
|
3
|
+
import UserTable from "../../components/admin/UserTable";
|
|
3
4
|
|
|
4
5
|
const ManageUsers = () => {
|
|
5
6
|
|
|
@@ -24,18 +25,9 @@ const ManageUsers = () => {
|
|
|
24
25
|
return (
|
|
25
26
|
<div>
|
|
26
27
|
|
|
27
|
-
<h1>Manage Users</h1>
|
|
28
|
+
<h1 className="text-2xl font-bold mb-4">Manage Users</h1>
|
|
28
29
|
|
|
29
|
-
{
|
|
30
|
-
users.map((user)=>(
|
|
31
|
-
<div key={user._id}>
|
|
32
|
-
|
|
33
|
-
<h3>{user.name}</h3>
|
|
34
|
-
<p>{user.email}</p>
|
|
35
|
-
|
|
36
|
-
</div>
|
|
37
|
-
))
|
|
38
|
-
}
|
|
30
|
+
<UserTable users={users} />
|
|
39
31
|
|
|
40
32
|
</div>
|
|
41
33
|
);
|
package/src/routes/AppRoutes.jsx
CHANGED
|
@@ -14,6 +14,9 @@ import AdminDashboard from "../pages/admin/AdminDashboard";
|
|
|
14
14
|
import AdminLayout from "../layout/AdminLayout";
|
|
15
15
|
import Register from "../pages/Register";
|
|
16
16
|
import { ProtectedRoute } from "../middleware/ProtectedRoute";
|
|
17
|
+
import SinglePost from "../pages/SinglePost";
|
|
18
|
+
import CreateUser from "../pages/admin/CreateUser";
|
|
19
|
+
import EditBlog from "../pages/admin/EditBlog";
|
|
17
20
|
|
|
18
21
|
function AppRoutes() {
|
|
19
22
|
return (
|
|
@@ -23,22 +26,22 @@ function AppRoutes() {
|
|
|
23
26
|
<Routes>
|
|
24
27
|
<Route
|
|
25
28
|
path="/"
|
|
26
|
-
element={<
|
|
29
|
+
element={<Login />}
|
|
27
30
|
/>
|
|
28
31
|
|
|
29
32
|
<Route
|
|
30
|
-
path="/
|
|
31
|
-
element={<
|
|
33
|
+
path="/home"
|
|
34
|
+
element={<Home />}
|
|
32
35
|
/>
|
|
33
36
|
|
|
34
37
|
<Route
|
|
35
|
-
path="/
|
|
36
|
-
element={<
|
|
38
|
+
path="/register"
|
|
39
|
+
element={<Register />}
|
|
37
40
|
/>
|
|
38
41
|
|
|
39
42
|
<Route
|
|
40
|
-
path="/
|
|
41
|
-
element={<
|
|
43
|
+
path="/post/:id"
|
|
44
|
+
element={<SinglePost />}
|
|
42
45
|
/>
|
|
43
46
|
|
|
44
47
|
<Route
|
|
@@ -51,10 +54,22 @@ function AppRoutes() {
|
|
|
51
54
|
path="users"
|
|
52
55
|
element={<ManageUsers />}
|
|
53
56
|
/>
|
|
57
|
+
<Route
|
|
58
|
+
path="users/create"
|
|
59
|
+
element={<CreateUser />}
|
|
60
|
+
/>
|
|
54
61
|
<Route
|
|
55
62
|
path="blogs"
|
|
56
63
|
element={<ManageBlogs />}
|
|
57
64
|
/>
|
|
65
|
+
<Route
|
|
66
|
+
path="blogs/edit/:id"
|
|
67
|
+
element={<EditBlog />}
|
|
68
|
+
/>
|
|
69
|
+
<Route
|
|
70
|
+
path="create"
|
|
71
|
+
element={<CreatePost />}
|
|
72
|
+
/>
|
|
58
73
|
{/* <Route
|
|
59
74
|
path="settings"
|
|
60
75
|
element={<Settings />}
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import api from "./api";
|
|
2
|
-
|
|
3
|
-
export const register = async (formData) => {
|
|
4
|
-
const response = await api.post("/
|
|
5
|
-
return response.data;
|
|
6
|
-
};
|
|
7
|
-
|
|
8
|
-
export const login = async (data) => {
|
|
9
|
-
const response = await api.post(
|
|
10
|
-
"/
|
|
11
|
-
data
|
|
12
|
-
);
|
|
13
|
-
|
|
14
|
-
return response.data;
|
|
15
|
-
};
|
|
1
|
+
import api from "./api";
|
|
2
|
+
|
|
3
|
+
export const register = async (formData) => {
|
|
4
|
+
const response = await api.post("/auth/register", formData);
|
|
5
|
+
return response.data;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export const login = async (data) => {
|
|
9
|
+
const response = await api.post(
|
|
10
|
+
"/auth/login",
|
|
11
|
+
data
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
return response.data;
|
|
15
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export const checkPermission = (module, action) => {
|
|
2
|
+
const token = localStorage.getItem("token");
|
|
3
|
+
if (!token) return false;
|
|
4
|
+
|
|
5
|
+
try {
|
|
6
|
+
const payloadBase64 = token.split('.')[1];
|
|
7
|
+
// handle base64 strings not padded properly
|
|
8
|
+
const base64 = payloadBase64.replace(/-/g, '+').replace(/_/g, '/');
|
|
9
|
+
const jsonPayload = decodeURIComponent(atob(base64).split('').map(function (c) {
|
|
10
|
+
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
|
|
11
|
+
}).join(''));
|
|
12
|
+
|
|
13
|
+
const decodedPayload = JSON.parse(jsonPayload);
|
|
14
|
+
const permissions = decodedPayload.permissions || [];
|
|
15
|
+
|
|
16
|
+
// Example: "USERS:READ"
|
|
17
|
+
const requiredPermission = `${module}:${action}`;
|
|
18
|
+
|
|
19
|
+
return permissions.includes(requiredPermission);
|
|
20
|
+
} catch (e) {
|
|
21
|
+
console.error("Failed to parse token for permissions", e);
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const hasAnyPermission = (module, actions) => {
|
|
27
|
+
return actions.some(action => checkPermission(module, action));
|
|
28
|
+
};
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
useEffect,
|
|
3
|
-
useState
|
|
4
|
-
} from "react";
|
|
5
|
-
|
|
6
|
-
import {
|
|
7
|
-
useParams,
|
|
8
|
-
useNavigate
|
|
9
|
-
} from "react-router-dom";
|
|
10
|
-
|
|
11
|
-
import {
|
|
12
|
-
getUserById,
|
|
13
|
-
updateUser
|
|
14
|
-
} from "../services/userService";
|
|
15
|
-
|
|
16
|
-
const EditUser = () => {
|
|
17
|
-
|
|
18
|
-
const { id } = useParams();
|
|
19
|
-
|
|
20
|
-
const navigate = useNavigate();
|
|
21
|
-
|
|
22
|
-
const [formData, setFormData] =
|
|
23
|
-
useState({
|
|
24
|
-
name: "",
|
|
25
|
-
email: "",
|
|
26
|
-
role: ""
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
useEffect(() => {
|
|
30
|
-
|
|
31
|
-
fetchUser();
|
|
32
|
-
|
|
33
|
-
}, []);
|
|
34
|
-
|
|
35
|
-
const fetchUser = async () => {
|
|
36
|
-
|
|
37
|
-
const res =
|
|
38
|
-
await getUserById(id);
|
|
39
|
-
|
|
40
|
-
setFormData(res.data);
|
|
41
|
-
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
const handleChange = (e) => {
|
|
45
|
-
|
|
46
|
-
setFormData({
|
|
47
|
-
...formData,
|
|
48
|
-
[e.target.name]:
|
|
49
|
-
e.target.value
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
const handleSubmit = async (e) => {
|
|
55
|
-
|
|
56
|
-
e.preventDefault();
|
|
57
|
-
|
|
58
|
-
await updateUser(
|
|
59
|
-
id,
|
|
60
|
-
formData
|
|
61
|
-
);
|
|
62
|
-
|
|
63
|
-
navigate("/admin/users");
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
return (
|
|
67
|
-
<form
|
|
68
|
-
onSubmit={handleSubmit}
|
|
69
|
-
className="flex flex-col gap-4"
|
|
70
|
-
>
|
|
71
|
-
|
|
72
|
-
<input
|
|
73
|
-
name="name"
|
|
74
|
-
value={formData.name}
|
|
75
|
-
onChange={handleChange}
|
|
76
|
-
/>
|
|
77
|
-
|
|
78
|
-
<input
|
|
79
|
-
name="email"
|
|
80
|
-
value={formData.email}
|
|
81
|
-
onChange={handleChange}
|
|
82
|
-
/>
|
|
83
|
-
|
|
84
|
-
<select
|
|
85
|
-
name="role"
|
|
86
|
-
value={formData.role}
|
|
87
|
-
onChange={handleChange}
|
|
88
|
-
>
|
|
89
|
-
<option value="user">
|
|
90
|
-
User
|
|
91
|
-
</option>
|
|
92
|
-
|
|
93
|
-
<option value="admin">
|
|
94
|
-
Admin
|
|
95
|
-
</option>
|
|
96
|
-
</select>
|
|
97
|
-
|
|
98
|
-
<button type="submit">
|
|
99
|
-
Update User
|
|
100
|
-
</button>
|
|
101
|
-
|
|
102
|
-
</form>
|
|
103
|
-
);
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
export default EditUser;
|