farheen_blog_app 1.0.6 → 2.0.0
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 +2 -1
- package/src/App.css +8 -296
- package/src/App.jsx +6 -5
- package/src/components/CommentTable.jsx +29 -19
- package/src/components/Navbar.jsx +23 -36
- package/src/components/Postcard.jsx +41 -15
- package/src/components/admin/AdminSidebar.jsx +57 -39
- package/src/components/admin/UserTable.jsx +52 -24
- package/src/layout/AdminLayout.jsx +3 -3
- package/src/middleware/ProtectedRoute.jsx +20 -13
- package/src/pages/CreatePost.jsx +104 -77
- package/src/pages/Home.jsx +57 -13
- package/src/pages/Login.jsx +54 -28
- package/src/pages/Register.jsx +91 -59
- package/src/pages/SinglePost.jsx +70 -8
- package/src/pages/admin/AdminDashboard.jsx +50 -28
- package/src/pages/admin/ManageBlogs.jsx +140 -63
- package/src/pages/admin/ManageUsers.jsx +89 -14
- package/src/routes/AppRoutes.jsx +0 -5
- package/src/services/authService.js +3 -3
- package/src/services/userService.js +28 -0
- package/src/utils/checkPermission.js +13 -1
- package/src/assets/hero.png +0 -0
- package/src/assets/react.svg +0 -1
- package/src/assets/vite.svg +0 -1
- package/src/components/Loader.jsx +0 -0
- package/src/pages/admin/CreateUser.jsx +0 -51
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "farheen_blog_app",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "2.0.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"dev": "vite",
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"dependencies": {
|
|
13
13
|
"@tanstack/react-query": "^5.100.14",
|
|
14
14
|
"axios": "^1.16.1",
|
|
15
|
+
"lucide-react": "^1.18.0",
|
|
15
16
|
"react": "^19.2.6",
|
|
16
17
|
"react-dom": "^19.2.6",
|
|
17
18
|
"react-hook-form": "^7.76.1",
|
package/src/App.css
CHANGED
|
@@ -1,300 +1,12 @@
|
|
|
1
|
-
/*
|
|
1
|
+
/* App.css - Cleaned up to use Tailwind CSS exclusively */
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
/* Animations for new components */
|
|
4
|
+
@keyframes fadeInUp {
|
|
5
|
+
from { opacity: 0; transform: translateY(15px); }
|
|
6
|
+
to { opacity: 1; transform: translateY(0); }
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/* Layout */
|
|
15
|
-
|
|
16
|
-
.layout {
|
|
17
|
-
display: flex;
|
|
18
|
-
min-height: 100vh;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/* Sidebar */
|
|
22
|
-
|
|
23
|
-
.sidebar {
|
|
24
|
-
width: 260px;
|
|
25
|
-
background: #161c2a;
|
|
26
|
-
color: white;
|
|
27
|
-
padding: 20px;
|
|
28
|
-
flex-shrink: 0;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
.logo {
|
|
32
|
-
margin-bottom: 40px;
|
|
33
|
-
font-size: 24px;
|
|
34
|
-
font-weight: 600;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
.sidebar nav {
|
|
38
|
-
display: flex;
|
|
39
|
-
flex-direction: column;
|
|
40
|
-
gap: 10px;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
.sidebar a {
|
|
44
|
-
text-decoration: none;
|
|
45
|
-
color: white;
|
|
46
|
-
padding: 12px 16px;
|
|
47
|
-
border-radius: 8px;
|
|
48
|
-
transition: 0.3s;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
.sidebar a:hover,
|
|
52
|
-
.sidebar a.active {
|
|
53
|
-
background: #1f2937;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/* Main Content */
|
|
57
|
-
|
|
58
|
-
.main-content {
|
|
59
|
-
flex: 1;
|
|
60
|
-
display: flex;
|
|
61
|
-
flex-direction: column;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/* Navbar */
|
|
65
|
-
|
|
66
|
-
.navbar {
|
|
67
|
-
height: 70px;
|
|
68
|
-
background: #161c2a;
|
|
69
|
-
display: flex;
|
|
70
|
-
justify-content: space-between;
|
|
71
|
-
align-items: center;
|
|
72
|
-
padding: 0 30px;
|
|
73
|
-
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
.navbar input {
|
|
77
|
-
width: 350px;
|
|
78
|
-
padding: 10px 15px;
|
|
79
|
-
border: 1px solid #d1d5db;
|
|
80
|
-
border-radius: 8px;
|
|
81
|
-
outline: none;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
.navbar-right {
|
|
85
|
-
display: flex;
|
|
86
|
-
align-items: center;
|
|
87
|
-
gap: 15px;
|
|
88
|
-
color: white;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
.navbar-right button {
|
|
92
|
-
border: none;
|
|
93
|
-
background: transparent;
|
|
94
|
-
cursor: pointer;
|
|
95
|
-
font-size: 18px;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
.navbar-right img {
|
|
99
|
-
width: 40px;
|
|
100
|
-
height: 40px;
|
|
101
|
-
border-radius: 50%;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/* Page Content */
|
|
105
|
-
|
|
106
|
-
.page-content {
|
|
107
|
-
flex: 1;
|
|
108
|
-
padding: 30px;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/* Dashboard */
|
|
112
|
-
|
|
113
|
-
.dashboard h1 {
|
|
114
|
-
margin-bottom: 25px;
|
|
115
|
-
color: #111827;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/* Cards */
|
|
119
|
-
|
|
120
|
-
.cards {
|
|
121
|
-
display: grid;
|
|
122
|
-
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
|
123
|
-
gap: 20px;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
.card {
|
|
127
|
-
background: white;
|
|
128
|
-
padding: 25px;
|
|
129
|
-
border-radius: 12px;
|
|
130
|
-
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.06);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
.card h4 {
|
|
134
|
-
color: #6b7280;
|
|
135
|
-
margin-bottom: 10px;
|
|
136
|
-
font-weight: 500;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
.card h2 {
|
|
140
|
-
color: #111827;
|
|
141
|
-
font-size: 32px;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/* Tables */
|
|
145
|
-
|
|
146
|
-
.table-container {
|
|
147
|
-
margin-top: 30px;
|
|
148
|
-
background: white;
|
|
149
|
-
border-radius: 12px;
|
|
150
|
-
overflow: hidden;
|
|
151
|
-
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.06);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
.user-table {
|
|
155
|
-
width: 100%;
|
|
156
|
-
border-collapse: collapse;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
.user-table th {
|
|
160
|
-
background: #111827;
|
|
161
|
-
color: white;
|
|
162
|
-
padding: 15px;
|
|
163
|
-
text-align: left;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
.user-table td {
|
|
167
|
-
padding: 15px;
|
|
168
|
-
border-bottom: 1px solid #e5e7eb;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
.user-table tr:hover {
|
|
172
|
-
background: #f9fafb;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/* Buttons */
|
|
176
|
-
|
|
177
|
-
.edit-btn {
|
|
178
|
-
background: #2563eb;
|
|
179
|
-
color: white;
|
|
180
|
-
border: none;
|
|
181
|
-
padding: 8px 14px;
|
|
182
|
-
border-radius: 6px;
|
|
183
|
-
cursor: pointer;
|
|
184
|
-
margin-right: 10px;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
.delete-btn {
|
|
188
|
-
background: #dc2626;
|
|
189
|
-
color: white;
|
|
190
|
-
border: none;
|
|
191
|
-
padding: 8px 14px;
|
|
192
|
-
border-radius: 6px;
|
|
193
|
-
cursor: pointer;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
/* Mobile */
|
|
197
|
-
|
|
198
|
-
@media (max-width: 768px) {
|
|
199
|
-
.layout {
|
|
200
|
-
flex-direction: column;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
.sidebar {
|
|
204
|
-
width: 100%;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
.navbar {
|
|
208
|
-
flex-direction: column;
|
|
209
|
-
gap: 10px;
|
|
210
|
-
height: auto;
|
|
211
|
-
padding: 15px;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
.navbar input {
|
|
215
|
-
width: 100%;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
.cards {
|
|
219
|
-
grid-template-columns: 1fr;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
.table-container {
|
|
223
|
-
overflow-x: auto;
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/* auth css */
|
|
228
|
-
.auth-container {
|
|
229
|
-
min-height: 100vh;
|
|
230
|
-
background: #f5f7fb;
|
|
231
|
-
display: flex;
|
|
232
|
-
justify-content: center;
|
|
233
|
-
align-items: center;
|
|
234
|
-
padding: 20px;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
.auth-card {
|
|
238
|
-
width: 100%;
|
|
239
|
-
max-width: 450px;
|
|
240
|
-
background: white;
|
|
241
|
-
padding: 40px;
|
|
242
|
-
border-radius: 16px;
|
|
243
|
-
box-shadow: 0 8px 30px rgba(0,0,0,0.08);
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
.auth-card h1 {
|
|
247
|
-
text-align: center;
|
|
248
|
-
margin-bottom: 10px;
|
|
249
|
-
color: #111827;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
.auth-card p {
|
|
253
|
-
text-align: center;
|
|
254
|
-
color: #6b7280;
|
|
255
|
-
margin-bottom: 30px;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
.auth-form {
|
|
259
|
-
display: flex;
|
|
260
|
-
flex-direction: column;
|
|
261
|
-
gap: 15px;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
.auth-form input {
|
|
265
|
-
padding: 14px;
|
|
266
|
-
border: 1px solid #d1d5db;
|
|
267
|
-
border-radius: 8px;
|
|
268
|
-
outline: none;
|
|
269
|
-
font-size: 15px;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
.auth-form input:focus {
|
|
273
|
-
border-color: #2563eb;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
.auth-form button {
|
|
277
|
-
background: #111827;
|
|
278
|
-
color: white;
|
|
279
|
-
border: none;
|
|
280
|
-
padding: 14px;
|
|
281
|
-
border-radius: 8px;
|
|
282
|
-
cursor: pointer;
|
|
283
|
-
font-size: 16px;
|
|
284
|
-
transition: .3s;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
.auth-form button:hover {
|
|
288
|
-
background: #1f2937;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
.auth-footer {
|
|
292
|
-
text-align: center;
|
|
293
|
-
margin-top: 20px;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
.auth-footer a {
|
|
297
|
-
color: #2563eb;
|
|
298
|
-
text-decoration: none;
|
|
299
|
-
font-weight: 600;
|
|
9
|
+
.animate-fade-in-up {
|
|
10
|
+
opacity: 0;
|
|
11
|
+
animation: fadeInUp 0.5s ease forwards;
|
|
300
12
|
}
|
package/src/App.jsx
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import { useState } from 'react'
|
|
2
|
-
import reactLogo from './assets/react.svg'
|
|
3
|
-
import viteLogo from './assets/vite.svg'
|
|
4
|
-
import heroImg from './assets/hero.png'
|
|
5
1
|
import './App.css'
|
|
6
2
|
import AppRoutes from './routes/AppRoutes'
|
|
3
|
+
import { UserProvider } from './context/UserContext'
|
|
7
4
|
|
|
8
5
|
function App() {
|
|
9
|
-
return
|
|
6
|
+
return (
|
|
7
|
+
<UserProvider>
|
|
8
|
+
<AppRoutes />
|
|
9
|
+
</UserProvider>
|
|
10
|
+
)
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
export default App
|
|
@@ -1,27 +1,37 @@
|
|
|
1
1
|
const CommentTable = ({ comments }) => {
|
|
2
2
|
if (!comments || comments.length === 0) {
|
|
3
|
-
return
|
|
3
|
+
return (
|
|
4
|
+
<div className="text-center py-12 px-4 rounded-2xl border border-dashed border-gray-300 bg-gray-50">
|
|
5
|
+
<div className="text-4xl mb-3 opacity-30">💬</div>
|
|
6
|
+
<h3 className="text-lg font-medium text-gray-900">No comments yet</h3>
|
|
7
|
+
<p className="text-gray-500 mt-1">Be the first to share your thoughts!</p>
|
|
8
|
+
</div>
|
|
9
|
+
);
|
|
4
10
|
}
|
|
5
11
|
|
|
6
12
|
return (
|
|
7
|
-
<
|
|
8
|
-
|
|
9
|
-
<
|
|
10
|
-
<
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
13
|
+
<div className="space-y-6">
|
|
14
|
+
{comments.map((comment, index) => (
|
|
15
|
+
<div key={comment._id || index} className="flex gap-4 animate-fade-in-up" style={{ animationDelay: `${index * 0.1}s` }}>
|
|
16
|
+
<div className="flex-shrink-0">
|
|
17
|
+
<div className="w-12 h-12 rounded-full bg-gradient-to-br from-blue-100 to-indigo-100 border border-indigo-200 flex items-center justify-center text-indigo-700 font-bold text-lg shadow-sm">
|
|
18
|
+
{(comment.user?.name || "A").charAt(0).toUpperCase()}
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
21
|
+
<div className="flex-1 bg-gray-50 rounded-2xl rounded-tl-none p-5 shadow-sm border border-gray-100">
|
|
22
|
+
<div className="flex items-center justify-between mb-2">
|
|
23
|
+
<h4 className="font-semibold text-gray-900">{comment.user?.name || "Anonymous"}</h4>
|
|
24
|
+
<span className="text-xs font-medium text-gray-500 bg-white px-2 py-1 rounded-full shadow-sm border border-gray-100">
|
|
25
|
+
{comment.createdAt ? new Date(comment.createdAt).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) : 'Just now'}
|
|
26
|
+
</span>
|
|
27
|
+
</div>
|
|
28
|
+
<p className="text-gray-700 leading-relaxed text-sm">
|
|
29
|
+
{comment.text}
|
|
30
|
+
</p>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
))}
|
|
34
|
+
</div>
|
|
25
35
|
);
|
|
26
36
|
};
|
|
27
37
|
|
|
@@ -1,48 +1,35 @@
|
|
|
1
1
|
import { Link } from "react-router-dom";
|
|
2
|
+
import { Search, Bell } from "lucide-react";
|
|
2
3
|
|
|
3
4
|
const Navbar = () => {
|
|
4
|
-
// return (
|
|
5
|
-
// <div className="bg-black text-white px-6 py-4 flex justify-between">
|
|
6
|
-
// <h1 className="text-2xl font-bold">
|
|
7
|
-
// Blog App
|
|
8
|
-
// </h1>
|
|
9
|
-
|
|
10
|
-
// <div className="flex gap-4">
|
|
11
|
-
// <Link to="/">Home</Link>
|
|
12
|
-
|
|
13
|
-
// <Link to="/create">
|
|
14
|
-
// Create Post
|
|
15
|
-
// </Link>
|
|
16
|
-
|
|
17
|
-
// <Link to="/login">
|
|
18
|
-
// Login
|
|
19
|
-
// </Link>
|
|
20
|
-
// </div>
|
|
21
|
-
// </div>
|
|
22
|
-
// );
|
|
23
|
-
|
|
24
5
|
return (
|
|
25
|
-
<header className="
|
|
26
|
-
<h1 className="text-
|
|
27
|
-
|
|
6
|
+
<header className="bg-white h-20 border-b border-slate-200 flex justify-between items-center px-8 shadow-[0_2px_10px_rgba(0,0,0,0.02)] z-10 sticky top-0 flex-shrink-0">
|
|
7
|
+
<h1 className="text-xl font-semibold text-slate-800">
|
|
8
|
+
{/* Optional page title could go here */}
|
|
28
9
|
</h1>
|
|
29
10
|
|
|
30
|
-
<div className="
|
|
31
|
-
<
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
11
|
+
<div className="flex items-center gap-6">
|
|
12
|
+
<div className="relative hidden md:block">
|
|
13
|
+
<input
|
|
14
|
+
type="text"
|
|
15
|
+
placeholder="Search anything..."
|
|
16
|
+
className="bg-slate-50 border border-slate-200 text-sm rounded-full pl-11 pr-4 py-2.5 w-64 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:bg-white transition-all"
|
|
17
|
+
/>
|
|
18
|
+
<Search className="absolute left-4 top-2.5 text-slate-400" size={18} />
|
|
19
|
+
</div>
|
|
20
|
+
<button className="relative text-slate-500 hover:text-indigo-600 transition-colors p-2 rounded-full hover:bg-indigo-50">
|
|
21
|
+
<Bell size={20} />
|
|
22
|
+
<span className="absolute top-1.5 right-1.5 w-2 h-2 bg-red-500 rounded-full border border-white"></span>
|
|
37
23
|
</button>
|
|
38
24
|
|
|
39
|
-
<
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
25
|
+
<div className="flex items-center gap-3 pl-4 border-l border-slate-200 cursor-pointer group">
|
|
26
|
+
<img
|
|
27
|
+
src="https://ui-avatars.com/api/?name=Admin+User&background=6366f1&color=fff"
|
|
28
|
+
alt="profile"
|
|
29
|
+
className="w-10 h-10 rounded-full border-2 border-indigo-100 group-hover:border-indigo-300 transition-all shadow-sm"
|
|
30
|
+
/>
|
|
31
|
+
</div>
|
|
44
32
|
</div>
|
|
45
|
-
|
|
46
33
|
</header>
|
|
47
34
|
);
|
|
48
35
|
};
|
|
@@ -1,22 +1,48 @@
|
|
|
1
|
+
import { Link } from "react-router-dom";
|
|
2
|
+
|
|
1
3
|
const PostCard = ({ post }) => {
|
|
2
4
|
return (
|
|
3
|
-
<
|
|
4
|
-
|
|
5
|
-
<
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
5
|
+
<Link to={`/post/${post._id}`} className="group block h-full">
|
|
6
|
+
<div className="flex flex-col h-full bg-white rounded-2xl overflow-hidden shadow-sm border border-gray-100 hover:shadow-xl hover:-translate-y-1 transition-all duration-300">
|
|
7
|
+
<div className="relative overflow-hidden h-56 bg-gray-100">
|
|
8
|
+
{post.image_url || post.image ? (
|
|
9
|
+
<img
|
|
10
|
+
src={`http://localhost:3000${post.image_url || post.image}`}
|
|
11
|
+
alt={post.title || "Blog post"}
|
|
12
|
+
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
|
|
13
|
+
onError={(e) => { e.target.onerror = null; e.target.src = "https://images.unsplash.com/photo-1499750310107-5fef28a66643?w=800&q=80"; }}
|
|
14
|
+
/>
|
|
15
|
+
) : (
|
|
16
|
+
<div className="w-full h-full flex items-center justify-center text-4xl opacity-20">
|
|
17
|
+
📝
|
|
18
|
+
</div>
|
|
19
|
+
)}
|
|
20
|
+
<div className="absolute top-4 left-4 bg-white/90 backdrop-blur-sm px-3 py-1 rounded-full text-xs font-semibold text-indigo-700 shadow-sm">
|
|
21
|
+
Blog
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
<div className="p-6 flex flex-col flex-grow">
|
|
26
|
+
<h2 className="text-xl font-bold text-gray-900 mb-3 group-hover:text-indigo-600 transition-colors line-clamp-2">
|
|
27
|
+
{post.title || "Untitled Post"}
|
|
28
|
+
</h2>
|
|
11
29
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
30
|
+
<p className="text-gray-600 mb-6 line-clamp-3 text-sm flex-grow">
|
|
31
|
+
{post.content ? post.content.substring(0, 120) + "..." : "No preview available for this post."}
|
|
32
|
+
</p>
|
|
15
33
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
34
|
+
<div className="flex items-center justify-between pt-4 border-t border-gray-100 mt-auto">
|
|
35
|
+
<div className="flex items-center gap-2">
|
|
36
|
+
<div className="w-8 h-8 rounded-full bg-gradient-to-br from-blue-500 to-indigo-600 flex items-center justify-center text-white text-xs font-bold">
|
|
37
|
+
{(post.User?.name || "A").charAt(0).toUpperCase()}
|
|
38
|
+
</div>
|
|
39
|
+
<span className="text-sm font-medium text-gray-700">{post.User?.name || "Author"}</span>
|
|
40
|
+
</div>
|
|
41
|
+
<span className="text-xs text-gray-500 font-medium">Read more →</span>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
</Link>
|
|
20
46
|
);
|
|
21
47
|
};
|
|
22
48
|
|
|
@@ -1,63 +1,81 @@
|
|
|
1
|
-
import { NavLink } from "react-router-dom";
|
|
1
|
+
import { NavLink, useNavigate } from "react-router-dom";
|
|
2
2
|
import { hasAnyPermission } from "../../utils/checkPermission";
|
|
3
|
+
import { LayoutDashboard, Users, FileText, PenTool, Settings, User, LogOut } from "lucide-react";
|
|
4
|
+
import { useUser } from "../../context/UserContext";
|
|
3
5
|
|
|
4
6
|
const AdminSidebar = () => {
|
|
7
|
+
const { logout, user } = useUser();
|
|
8
|
+
const navigate = useNavigate();
|
|
9
|
+
|
|
10
|
+
const handleLogout = (e) => {
|
|
11
|
+
e.preventDefault();
|
|
12
|
+
logout();
|
|
13
|
+
navigate("/");
|
|
14
|
+
};
|
|
5
15
|
|
|
6
16
|
const menuItems = [
|
|
7
|
-
{
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
}
|
|
17
|
+
{ path: "/admin", title: "Dashboard", icon: <LayoutDashboard size={20} /> },
|
|
18
|
+
{ path: "/admin/users", title: "Manage Users", module: "USERS", actions: ["READ", "UPDATE", "DELETE"], icon: <Users size={20} /> },
|
|
19
|
+
{ path: "/admin/blogs", title: "Manage Blogs", module: "POSTS", actions: ["READ", "UPDATE", "DELETE"], icon: <FileText size={20} /> },
|
|
20
|
+
{ path: "/admin/create", title: "Create Post", module: "POSTS", actions: ["CREATE"], icon: <PenTool size={20} /> },
|
|
21
|
+
{ path: "/admin/settings", title: "Settings", icon: <Settings size={20} /> }
|
|
39
22
|
];
|
|
40
23
|
|
|
41
24
|
return (
|
|
42
|
-
<aside className="
|
|
43
|
-
<
|
|
44
|
-
|
|
25
|
+
<aside className="w-64 bg-[#0f172a] text-slate-300 flex-shrink-0 flex flex-col min-h-screen border-r border-slate-800 z-20">
|
|
26
|
+
<div className="h-20 flex items-center gap-3 px-6 border-b border-slate-800/50">
|
|
27
|
+
<div className="w-8 h-8 bg-gradient-to-br from-indigo-500 to-purple-600 rounded-lg flex items-center justify-center font-bold text-white shadow-lg">B</div>
|
|
28
|
+
<h2 className="text-xl font-bold tracking-tight text-white">Blogify</h2>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<nav className="flex-1 py-6 px-4 flex flex-col gap-1.5 overflow-y-auto">
|
|
32
|
+
<div className="text-xs font-semibold text-slate-500 uppercase tracking-wider mb-2 px-3">Menu</div>
|
|
45
33
|
{menuItems.map((item, index) => {
|
|
46
34
|
let hasAccess = true;
|
|
47
|
-
|
|
48
35
|
if (item.module && item.actions) {
|
|
49
36
|
hasAccess = hasAnyPermission(item.module, item.actions);
|
|
50
37
|
}
|
|
51
|
-
|
|
52
38
|
if (!hasAccess) return null;
|
|
53
39
|
|
|
54
40
|
return (
|
|
55
|
-
<NavLink
|
|
41
|
+
<NavLink
|
|
42
|
+
key={index}
|
|
43
|
+
to={item.path}
|
|
44
|
+
end
|
|
45
|
+
className={({ isActive }) => `
|
|
46
|
+
flex items-center gap-3 px-3 py-2.5 rounded-lg transition-all duration-200 font-medium text-sm
|
|
47
|
+
${isActive
|
|
48
|
+
? 'bg-indigo-600/10 text-indigo-400'
|
|
49
|
+
: 'hover:bg-slate-800/50 hover:text-slate-100'
|
|
50
|
+
}
|
|
51
|
+
`}
|
|
52
|
+
>
|
|
53
|
+
{item.icon}
|
|
56
54
|
{item.title}
|
|
57
55
|
</NavLink>
|
|
58
56
|
);
|
|
59
57
|
})}
|
|
60
58
|
</nav>
|
|
59
|
+
|
|
60
|
+
<div className="p-4 border-t border-slate-800/50 flex flex-col gap-3">
|
|
61
|
+
<div className="bg-slate-800/50 p-3 rounded-xl flex items-center gap-3 border border-slate-700/50">
|
|
62
|
+
<div className="w-9 h-9 rounded-full bg-indigo-500/20 flex items-center justify-center text-indigo-400">
|
|
63
|
+
<User size={18} />
|
|
64
|
+
</div>
|
|
65
|
+
<div className="flex-1 overflow-hidden">
|
|
66
|
+
<p className="text-sm font-semibold text-white truncate">{user?.name || 'Admin User'}</p>
|
|
67
|
+
<p className="text-xs text-slate-400 truncate">{user?.role || 'Administrator'}</p>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<button
|
|
72
|
+
onClick={handleLogout}
|
|
73
|
+
className="flex items-center justify-center gap-2 w-full px-4 py-2.5 bg-red-500/10 hover:bg-red-500/20 text-red-400 hover:text-red-300 rounded-lg text-sm font-medium transition-colors border border-red-500/20"
|
|
74
|
+
>
|
|
75
|
+
<LogOut size={16} />
|
|
76
|
+
Logout
|
|
77
|
+
</button>
|
|
78
|
+
</div>
|
|
61
79
|
</aside>
|
|
62
80
|
);
|
|
63
81
|
};
|