farheen_blog_app 2.0.0 → 2.0.1
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
|
@@ -12,10 +12,10 @@ const UserTable = ({ users }) => {
|
|
|
12
12
|
|
|
13
13
|
<tbody className="divide-y divide-gray-100">
|
|
14
14
|
{users?.map((user, index) => (
|
|
15
|
-
<tr
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
<tr
|
|
16
|
+
key={user._id}
|
|
17
|
+
className="hover:bg-blue-50/50 transition-colors duration-200 animate-fade-in-up"
|
|
18
|
+
style={{ animationDelay: `${index * 0.08}s` }}
|
|
19
19
|
>
|
|
20
20
|
<td className="p-4">
|
|
21
21
|
<div className="flex items-center gap-4">
|
|
@@ -29,8 +29,8 @@ const UserTable = ({ users }) => {
|
|
|
29
29
|
</div>
|
|
30
30
|
</td>
|
|
31
31
|
<td className="p-4">
|
|
32
|
-
<span className={`px-3 py-1.5 text-xs font-semibold rounded-full border ${user.role === 'admin' ? 'bg-indigo-50 text-indigo-700 border-indigo-200' : 'bg-gray-50 text-gray-700 border-gray-200'}`}>
|
|
33
|
-
{(user.role || 'user').toUpperCase()}
|
|
32
|
+
<span className={`px-3 py-1.5 text-xs font-semibold rounded-full border ${user.role?.name === 'admin' ? 'bg-indigo-50 text-indigo-700 border-indigo-200' : 'bg-gray-50 text-gray-700 border-gray-200'}`}>
|
|
33
|
+
{(user.role?.name || 'user').toUpperCase()}
|
|
34
34
|
</span>
|
|
35
35
|
</td>
|
|
36
36
|
<td className="p-4 text-right">
|
|
@@ -2,7 +2,7 @@ import { useEffect, useState } from "react";
|
|
|
2
2
|
import { Link } from "react-router-dom";
|
|
3
3
|
import api from "../../services/api";
|
|
4
4
|
import useDebounce from "../../hooks/useDebounce";
|
|
5
|
-
import { getPosts } from "../../services/postService";
|
|
5
|
+
import { deletePost, getPosts } from "../../services/postService";
|
|
6
6
|
import { Search, FileX } from "lucide-react";
|
|
7
7
|
|
|
8
8
|
const ManageBlogs = () => {
|
|
@@ -20,12 +20,13 @@ const ManageBlogs = () => {
|
|
|
20
20
|
const loadBlogs = async () => {
|
|
21
21
|
try {
|
|
22
22
|
setLoading(true);
|
|
23
|
-
const res = await api.get(`/
|
|
23
|
+
const res = await api.get(`/posts?page=${page}&search=${debouncedSearch}`).catch(async () => {
|
|
24
24
|
// Fallback if admin route doesn't exist
|
|
25
25
|
return { data: { posts: await getPosts(), totalPages: 1 } };
|
|
26
26
|
});
|
|
27
|
-
|
|
28
|
-
const fetchedBlogs = res.data
|
|
27
|
+
|
|
28
|
+
const fetchedBlogs = res.data || [];
|
|
29
|
+
console.log(res);
|
|
29
30
|
setBlogs(Array.isArray(fetchedBlogs) ? fetchedBlogs : []);
|
|
30
31
|
setTotalPages(res.data?.totalPages || 1);
|
|
31
32
|
} catch (error) {
|
|
@@ -38,7 +39,7 @@ const ManageBlogs = () => {
|
|
|
38
39
|
const handleDelete = async (id) => {
|
|
39
40
|
if (window.confirm("Are you sure you want to delete this blog?")) {
|
|
40
41
|
try {
|
|
41
|
-
await
|
|
42
|
+
await deletePost(id);
|
|
42
43
|
loadBlogs();
|
|
43
44
|
} catch (error) {
|
|
44
45
|
console.log(error);
|
|
@@ -52,95 +53,95 @@ const ManageBlogs = () => {
|
|
|
52
53
|
}, [page, debouncedSearch]);
|
|
53
54
|
|
|
54
55
|
return (
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
56
|
+
<div className="p-8 max-w-[1400px] mx-auto animate-fade-in-up">
|
|
57
|
+
<div className="flex flex-col md:flex-row justify-between items-start md:items-center mb-8 bg-white p-6 rounded-2xl shadow-sm border border-slate-200">
|
|
58
|
+
<div className="mb-4 md:mb-0">
|
|
59
|
+
<h1 className="text-3xl font-bold text-slate-800 tracking-tight">Manage Blogs</h1>
|
|
60
|
+
<p className="text-slate-500 mt-1 text-sm">View, edit, and moderate all blog posts</p>
|
|
61
|
+
</div>
|
|
61
62
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
</div>
|
|
63
|
+
<div className="relative w-full md:w-auto">
|
|
64
|
+
<input
|
|
65
|
+
type="text"
|
|
66
|
+
value={search}
|
|
67
|
+
placeholder="Search blogs..."
|
|
68
|
+
onChange={(e) => setSearch(e.target.value)}
|
|
69
|
+
className="border border-slate-200 bg-slate-50 rounded-lg pl-11 pr-4 py-2.5 w-full md:w-80 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 focus:bg-white shadow-sm transition-all"
|
|
70
|
+
/>
|
|
71
|
+
<Search className="absolute left-4 top-3 text-slate-400" size={18} />
|
|
72
72
|
</div>
|
|
73
|
+
</div>
|
|
73
74
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
75
|
+
<div className="bg-white rounded-2xl shadow-sm border border-slate-200 overflow-hidden">
|
|
76
|
+
{loading ? (
|
|
77
|
+
<div className="flex justify-center py-32">
|
|
78
|
+
<div className="w-10 h-10 border-4 border-indigo-200 border-t-indigo-600 rounded-full animate-spin"></div>
|
|
79
|
+
</div>
|
|
80
|
+
) : (
|
|
81
|
+
<div className="overflow-x-auto">
|
|
82
|
+
<table className="w-full text-left border-collapse min-w-[800px]">
|
|
83
|
+
<thead className="bg-slate-50/80 border-b border-slate-200">
|
|
84
|
+
<tr>
|
|
85
|
+
<th className="px-6 py-4 font-semibold text-slate-600 text-xs tracking-wider uppercase">Image</th>
|
|
86
|
+
<th className="px-6 py-4 font-semibold text-slate-600 text-xs tracking-wider uppercase">Content</th>
|
|
87
|
+
<th className="px-6 py-4 font-semibold text-slate-600 text-xs tracking-wider uppercase">Author</th>
|
|
88
|
+
<th className="px-6 py-4 font-semibold text-slate-600 text-xs tracking-wider uppercase">Date</th>
|
|
89
|
+
<th className="px-6 py-4 font-semibold text-slate-600 text-xs tracking-wider uppercase text-center">Status</th>
|
|
90
|
+
<th className="px-6 py-4 font-semibold text-slate-600 text-xs tracking-wider uppercase text-right">Actions</th>
|
|
91
|
+
</tr>
|
|
92
|
+
</thead>
|
|
93
|
+
<tbody className="divide-y divide-slate-100">
|
|
94
|
+
{blogs.length === 0 ? (
|
|
83
95
|
<tr>
|
|
84
|
-
<
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
96
|
+
<td colSpan="6" className="p-16 text-center text-slate-400 bg-slate-50/50">
|
|
97
|
+
<div className="flex flex-col items-center justify-center">
|
|
98
|
+
<FileX size={48} strokeWidth={1} className="text-slate-300 mb-4" />
|
|
99
|
+
<p className="font-medium text-lg text-slate-500">No blogs found.</p>
|
|
100
|
+
<p className="text-sm mt-1">Try adjusting your search criteria</p>
|
|
101
|
+
</div>
|
|
102
|
+
</td>
|
|
90
103
|
</tr>
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
104
|
+
) : (
|
|
105
|
+
blogs.map((blog, index) => (
|
|
106
|
+
<tr key={blog.id} className="hover:bg-slate-50/80 transition-colors duration-200 animate-fade-in-up group" style={{ animationDelay: `${index * 0.05}s` }}>
|
|
107
|
+
<td className="px-6 py-4">
|
|
108
|
+
{blog.image_url ? (
|
|
109
|
+
<img src={`http://localhost:3000${blog.image_url}`} alt="Blog post" className="h-14 w-14 object-cover rounded-xl shadow-sm border border-slate-200" />
|
|
110
|
+
) : (
|
|
111
|
+
<div className="h-14 w-14 bg-gradient-to-br from-slate-100 to-slate-200 rounded-xl flex items-center justify-center text-xs text-slate-400 border border-slate-200 shadow-sm">N/A</div>
|
|
112
|
+
)}
|
|
113
|
+
</td>
|
|
114
|
+
<td className="px-6 py-4 w-2/5">
|
|
115
|
+
<p className="font-medium text-slate-800 line-clamp-2 leading-relaxed">{blog.content || 'No content'}</p>
|
|
116
|
+
</td>
|
|
117
|
+
<td className="px-6 py-4">
|
|
118
|
+
<div className="flex items-center gap-3">
|
|
119
|
+
<div className="w-8 h-8 rounded-full bg-indigo-100 text-indigo-700 flex items-center justify-center font-bold text-xs">
|
|
120
|
+
{(blog.User?.name || 'U').charAt(0).toUpperCase()}
|
|
121
|
+
</div>
|
|
122
|
+
<span className="font-medium text-slate-700">{blog.User?.name || 'Unknown'}</span>
|
|
100
123
|
</div>
|
|
101
124
|
</td>
|
|
125
|
+
<td className="px-6 py-4 text-slate-500 text-sm font-medium">
|
|
126
|
+
{blog.created_at ? new Date(blog.created_at).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) : 'N/A'}
|
|
127
|
+
</td>
|
|
128
|
+
<td className="px-6 py-4 text-center">
|
|
129
|
+
<span className="inline-flex px-3 py-1 rounded-full text-xs font-semibold bg-emerald-50 text-emerald-700 border border-emerald-200">
|
|
130
|
+
PUBLISHED
|
|
131
|
+
</span>
|
|
132
|
+
</td>
|
|
133
|
+
<td className="px-6 py-4 text-right whitespace-nowrap">
|
|
134
|
+
<button onClick={() => handleDelete(blog.id)} className="bg-white hover:bg-red-50 hover:text-red-600 text-slate-600 px-4 py-2 rounded-lg font-medium transition-all duration-200 shadow-sm border border-slate-200 hover:border-red-200 cursor-pointer text-sm">
|
|
135
|
+
Delete
|
|
136
|
+
</button>
|
|
137
|
+
</td>
|
|
102
138
|
</tr>
|
|
103
|
-
)
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
) : (
|
|
110
|
-
<div className="h-14 w-14 bg-gradient-to-br from-slate-100 to-slate-200 rounded-xl flex items-center justify-center text-xs text-slate-400 border border-slate-200 shadow-sm">N/A</div>
|
|
111
|
-
)}
|
|
112
|
-
</td>
|
|
113
|
-
<td className="px-6 py-4 w-2/5">
|
|
114
|
-
<p className="font-medium text-slate-800 line-clamp-2 leading-relaxed">{blog.content || 'No content'}</p>
|
|
115
|
-
</td>
|
|
116
|
-
<td className="px-6 py-4">
|
|
117
|
-
<div className="flex items-center gap-3">
|
|
118
|
-
<div className="w-8 h-8 rounded-full bg-indigo-100 text-indigo-700 flex items-center justify-center font-bold text-xs">
|
|
119
|
-
{(blog.User?.name || 'U').charAt(0).toUpperCase()}
|
|
120
|
-
</div>
|
|
121
|
-
<span className="font-medium text-slate-700">{blog.User?.name || 'Unknown'}</span>
|
|
122
|
-
</div>
|
|
123
|
-
</td>
|
|
124
|
-
<td className="px-6 py-4 text-slate-500 text-sm font-medium">
|
|
125
|
-
{blog.created_at ? new Date(blog.created_at).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) : 'N/A'}
|
|
126
|
-
</td>
|
|
127
|
-
<td className="px-6 py-4 text-center">
|
|
128
|
-
<span className="inline-flex px-3 py-1 rounded-full text-xs font-semibold bg-emerald-50 text-emerald-700 border border-emerald-200">
|
|
129
|
-
PUBLISHED
|
|
130
|
-
</span>
|
|
131
|
-
</td>
|
|
132
|
-
<td className="px-6 py-4 text-right whitespace-nowrap">
|
|
133
|
-
<button onClick={() => handleDelete(blog.id)} className="bg-white hover:bg-red-50 hover:text-red-600 text-slate-600 px-4 py-2 rounded-lg font-medium transition-all duration-200 shadow-sm border border-slate-200 hover:border-red-200 cursor-pointer text-sm">
|
|
134
|
-
Delete
|
|
135
|
-
</button>
|
|
136
|
-
</td>
|
|
137
|
-
</tr>
|
|
138
|
-
)))}
|
|
139
|
-
</tbody>
|
|
140
|
-
</table>
|
|
141
|
-
</div>
|
|
142
|
-
)}
|
|
143
|
-
</div>
|
|
139
|
+
)))}
|
|
140
|
+
</tbody>
|
|
141
|
+
</table>
|
|
142
|
+
</div>
|
|
143
|
+
)}
|
|
144
|
+
</div>
|
|
144
145
|
|
|
145
146
|
{/* Pagination */}
|
|
146
147
|
<div className="flex items-center justify-between mt-8">
|
|
@@ -12,4 +12,16 @@ export const createPost = async (formData) => {
|
|
|
12
12
|
);
|
|
13
13
|
|
|
14
14
|
return response.data;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const updatePost = async (id, formData) => {
|
|
18
|
+
const response = await api.put(
|
|
19
|
+
`/posts/${id}`,
|
|
20
|
+
formData
|
|
21
|
+
);
|
|
22
|
+
return response.data;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const deletePost = async (id) => {
|
|
26
|
+
return await api.delete(`/posts/${id}`);
|
|
15
27
|
};
|