farheen_blog_app 2.0.1 → 3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "farheen_blog_app",
3
3
  "private": false,
4
- "version": "2.0.1",
4
+ "version": "3.0.0",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "dev": "vite",
package/src/App.css CHANGED
@@ -9,4 +9,13 @@
9
9
  .animate-fade-in-up {
10
10
  opacity: 0;
11
11
  animation: fadeInUp 0.5s ease forwards;
12
+ }
13
+
14
+ @keyframes slideInRight {
15
+ from { opacity: 0; transform: translateX(100%); }
16
+ to { opacity: 1; transform: translateX(0); }
17
+ }
18
+
19
+ .animate-slide-in-right {
20
+ animation: slideInRight 0.3s cubic-bezier(0.16, 1, 0.3, 1) forwards;
12
21
  }
package/src/App.jsx CHANGED
@@ -1,11 +1,14 @@
1
1
  import './App.css'
2
2
  import AppRoutes from './routes/AppRoutes'
3
3
  import { UserProvider } from './context/UserContext'
4
+ import { ToastProvider } from './context/ToastContext'
4
5
 
5
6
  function App() {
6
7
  return (
7
8
  <UserProvider>
8
- <AppRoutes />
9
+ <ToastProvider>
10
+ <AppRoutes />
11
+ </ToastProvider>
9
12
  </UserProvider>
10
13
  )
11
14
  }
@@ -1,8 +1,147 @@
1
- const CommentTable = ({ comments }) => {
1
+ import { useState } from "react";
2
+ import { useUser } from "../context/UserContext";
3
+ import { Trash2, Reply, CornerDownRight, X, MessageSquare } from "lucide-react";
4
+
5
+ const CommentNode = ({ comment, onAddReply, onDeleteComment, depth = 0 }) => {
6
+ const { user } = useUser();
7
+ const [isReplying, setIsReplying] = useState(false);
8
+ const [replyText, setReplyText] = useState("");
9
+
10
+ const handleReplySubmit = (e) => {
11
+ e.preventDefault();
12
+ if (!replyText.trim()) return;
13
+ onAddReply(comment._id, replyText);
14
+ setReplyText("");
15
+ setIsReplying(false);
16
+ };
17
+
18
+ const authorName = comment.author?.name || "Anonymous";
19
+ const avatarLetter = authorName.charAt(0).toUpperCase();
20
+ const isAuthor = user && comment.author && (comment.author._id === user.id || comment.author === user.id);
21
+ const isAdmin = user && (user.isAdmin || user.role === "Admin" || user.role === "admin");
22
+ const canDelete = isAuthor || isAdmin;
23
+
24
+ const displayAvatar = comment.author?.profilePicture
25
+ ? `http://localhost:3000${comment.author.profilePicture}`
26
+ : null;
27
+
28
+ return (
29
+ <div className="flex flex-col">
30
+ {/* Comment Body */}
31
+ <div className="flex gap-4 group">
32
+ <div className="flex-shrink-0 relative">
33
+ {displayAvatar ? (
34
+ <img
35
+ src={displayAvatar}
36
+ alt={authorName}
37
+ className="w-10 h-10 rounded-full object-cover border border-indigo-100 shadow-sm"
38
+ onError={(e) => {
39
+ e.target.onerror = null;
40
+ e.target.style.display = "none";
41
+ }}
42
+ />
43
+ ) : (
44
+ <div className="w-10 h-10 rounded-full bg-gradient-to-br from-indigo-500 to-purple-600 border border-indigo-100 flex items-center justify-center text-white font-bold text-base shadow-sm">
45
+ {avatarLetter}
46
+ </div>
47
+ )}
48
+ </div>
49
+
50
+ <div className="flex-1 bg-gray-50 hover:bg-gray-100/70 rounded-2xl rounded-tl-none p-4 shadow-sm border border-gray-100 transition-all duration-200">
51
+ <div className="flex items-center justify-between mb-2">
52
+ <div className="flex items-center gap-2">
53
+ <h4 className="font-semibold text-gray-900 text-sm">{authorName}</h4>
54
+ {comment.author?.role === "Admin" && (
55
+ <span className="px-2 py-0.5 rounded text-[10px] font-bold bg-rose-50 text-rose-600 border border-rose-100 uppercase">Admin</span>
56
+ )}
57
+ </div>
58
+ <span className="text-xs text-gray-500 font-medium bg-white px-2 py-0.5 rounded-full border border-gray-100 shadow-[0_1px_2px_rgba(0,0,0,0.02)]">
59
+ {comment.createdAt ? new Date(comment.createdAt).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) : 'Just now'}
60
+ </span>
61
+ </div>
62
+
63
+ <p className="text-gray-700 leading-relaxed text-sm whitespace-pre-wrap">
64
+ {comment.content}
65
+ </p>
66
+
67
+ <div className="flex items-center gap-4 mt-3 pt-2 border-t border-gray-200/50">
68
+ {user && (
69
+ <button
70
+ onClick={() => setIsReplying(!isReplying)}
71
+ className="inline-flex items-center text-xs font-semibold text-indigo-600 hover:text-indigo-800 transition-colors gap-1 cursor-pointer"
72
+ >
73
+ <Reply className="w-3 h-3" />
74
+ Reply
75
+ </button>
76
+ )}
77
+ {canDelete && (
78
+ <button
79
+ onClick={() => onDeleteComment(comment._id)}
80
+ className="inline-flex items-center text-xs font-semibold text-rose-600 hover:text-rose-800 transition-colors gap-1 ml-auto md:opacity-0 group-hover:opacity-100 cursor-pointer"
81
+ >
82
+ <Trash2 className="w-3 h-3" />
83
+ Delete
84
+ </button>
85
+ )}
86
+ </div>
87
+ </div>
88
+ </div>
89
+
90
+ {/* Inline Reply Form */}
91
+ {isReplying && (
92
+ <div className="flex gap-4 mt-3 ml-6 pl-4 border-l-2 border-dashed border-indigo-200">
93
+ <div className="w-6 h-6 rounded-full bg-indigo-50 flex items-center justify-center text-indigo-500 text-xs font-bold border border-indigo-100">
94
+ <CornerDownRight className="w-3 h-3" />
95
+ </div>
96
+ <form onSubmit={handleReplySubmit} className="flex-1 flex gap-2">
97
+ <input
98
+ type="text"
99
+ value={replyText}
100
+ onChange={(e) => setReplyText(e.target.value)}
101
+ placeholder={`Reply to ${authorName}...`}
102
+ className="flex-1 bg-white border border-gray-300 rounded-xl px-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500/20 focus:border-indigo-500"
103
+ autoFocus
104
+ />
105
+ <button
106
+ type="submit"
107
+ className="bg-indigo-600 text-white rounded-xl px-4 py-2 text-xs font-semibold hover:bg-indigo-700 active:bg-indigo-800 transition-colors cursor-pointer"
108
+ >
109
+ Reply
110
+ </button>
111
+ <button
112
+ type="button"
113
+ onClick={() => setIsReplying(false)}
114
+ className="bg-gray-100 text-gray-500 rounded-xl px-3 py-2 text-xs font-semibold hover:bg-gray-200 transition-colors cursor-pointer"
115
+ >
116
+ <X className="w-3.5 h-3.5" />
117
+ </button>
118
+ </form>
119
+ </div>
120
+ )}
121
+
122
+ {/* Render Nested Replies */}
123
+ {comment.replies && comment.replies.length > 0 && (
124
+ <div className="ml-6 md:ml-10 border-l border-indigo-100 pl-4 md:pl-6 mt-3 space-y-4">
125
+ {comment.replies.map((reply) => (
126
+ <CommentNode
127
+ key={reply._id}
128
+ comment={reply}
129
+ onAddReply={onAddReply}
130
+ onDeleteComment={onDeleteComment}
131
+ depth={depth + 1}
132
+ />
133
+ ))}
134
+ </div>
135
+ )}
136
+ </div>
137
+ );
138
+ };
139
+
140
+ const CommentTable = ({ comments, onAddReply, onDeleteComment }) => {
2
141
  if (!comments || comments.length === 0) {
3
142
  return (
4
143
  <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>
144
+ <MessageSquare className="w-12 h-12 mx-auto text-gray-300 mb-3" />
6
145
  <h3 className="text-lg font-medium text-gray-900">No comments yet</h3>
7
146
  <p className="text-gray-500 mt-1">Be the first to share your thoughts!</p>
8
147
  </div>
@@ -11,25 +150,13 @@ const CommentTable = ({ comments }) => {
11
150
 
12
151
  return (
13
152
  <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>
153
+ {comments.map((comment) => (
154
+ <CommentNode
155
+ key={comment._id}
156
+ comment={comment}
157
+ onAddReply={onAddReply}
158
+ onDeleteComment={onDeleteComment}
159
+ />
33
160
  ))}
34
161
  </div>
35
162
  );
@@ -0,0 +1,246 @@
1
+ import { useState, useEffect } from "react";
2
+ import { useForm } from "react-hook-form";
3
+ import { updatePost } from "../services/postService";
4
+ import { useToast } from "../context/ToastContext";
5
+ import FormField from "./FormField";
6
+ import { X, Image as ImageIcon, Save, Loader2, AlertCircle } from "lucide-react";
7
+
8
+ const EditBlogModal = ({ blog, isOpen, onClose, onSuccess }) => {
9
+ const { showToast } = useToast();
10
+ const [loading, setLoading] = useState(false);
11
+ const [existingImages, setExistingImages] = useState([]);
12
+ const [newPreviewUrls, setNewPreviewUrls] = useState([]);
13
+
14
+ const {
15
+ register,
16
+ handleSubmit,
17
+ watch,
18
+ reset,
19
+ formState: { errors }
20
+ } = useForm();
21
+
22
+ const newImageFiles = watch("images");
23
+
24
+ // Populate form fields when blog changes
25
+ useEffect(() => {
26
+ if (blog && isOpen) {
27
+ reset({
28
+ title: blog.title || "",
29
+ description: blog.description || "",
30
+ content: blog.content || "",
31
+ images: null
32
+ });
33
+ setExistingImages(blog.images || []);
34
+ setNewPreviewUrls([]);
35
+ }
36
+ }, [blog, isOpen, reset]);
37
+
38
+ // Handle previewing newly selected files
39
+ useEffect(() => {
40
+ if (newImageFiles && newImageFiles.length > 0) {
41
+ const urls = Array.from(newImageFiles).map(file => URL.createObjectURL(file));
42
+ setNewPreviewUrls(urls);
43
+ return () => {
44
+ urls.forEach(url => URL.revokeObjectURL(url));
45
+ };
46
+ } else {
47
+ setNewPreviewUrls([]);
48
+ }
49
+ }, [newImageFiles]);
50
+
51
+ if (!isOpen || !blog) return null;
52
+
53
+ const onSubmit = async (data) => {
54
+ try {
55
+ setLoading(true);
56
+
57
+ const formData = new FormData();
58
+ formData.append("title", data.title);
59
+ formData.append("description", data.description);
60
+ formData.append("content", data.content);
61
+
62
+ if (data.images && data.images.length > 0) {
63
+ Array.from(data.images).forEach((file) => {
64
+ formData.append("images", file);
65
+ });
66
+ }
67
+
68
+ const id = blog._id || blog.id;
69
+ await updatePost(id, formData);
70
+ showToast("Blog post updated successfully!", "success");
71
+ onSuccess();
72
+ onClose();
73
+ } catch (error) {
74
+ console.error("Update blog error:", error);
75
+ showToast(
76
+ error.response?.data?.message ||
77
+ error.response?.message ||
78
+ "Failed to update post",
79
+ "error"
80
+ );
81
+ } finally {
82
+ setLoading(false);
83
+ }
84
+ };
85
+
86
+ return (
87
+ <div className="fixed inset-0 z-50 overflow-y-auto bg-slate-900/60 backdrop-blur-sm flex items-center justify-center p-4">
88
+ <div className="bg-white rounded-3xl shadow-2xl border border-slate-100 max-w-2xl w-full max-h-[90vh] overflow-hidden flex flex-col animate-fade-in-up">
89
+
90
+ {/* Header */}
91
+ <div className="px-6 py-5 border-b border-slate-100 flex items-center justify-between">
92
+ <div>
93
+ <h2 className="text-xl font-bold text-slate-800">Edit Blog Post</h2>
94
+ <p className="text-xs text-slate-500 mt-0.5">Modify the title, description, content, or cover images</p>
95
+ </div>
96
+ <button
97
+ onClick={onClose}
98
+ className="text-slate-400 hover:text-slate-600 hover:bg-slate-50 p-2 rounded-xl transition-all cursor-pointer"
99
+ >
100
+ <X className="w-5 h-5" />
101
+ </button>
102
+ </div>
103
+
104
+ {/* Form Body */}
105
+ <form onSubmit={handleSubmit(onSubmit)} className="flex-1 overflow-y-auto p-6 space-y-6">
106
+ <FormField
107
+ label="Post Title"
108
+ placeholder="Catchy blog title"
109
+ error={errors.title}
110
+ {...register("title", {
111
+ required: "Title is required",
112
+ minLength: {
113
+ value: 5,
114
+ message: "Title must be at least 5 characters"
115
+ }
116
+ })}
117
+ />
118
+
119
+ <FormField
120
+ label="Post Description"
121
+ placeholder="Brief summary"
122
+ error={errors.description}
123
+ {...register("description", {
124
+ maxLength: {
125
+ value: 200,
126
+ message: "Description cannot exceed 200 characters"
127
+ }
128
+ })}
129
+ />
130
+
131
+ <FormField
132
+ label="Post Content"
133
+ type="textarea"
134
+ placeholder="Post content..."
135
+ error={errors.content}
136
+ className="h-44"
137
+ {...register("content", {
138
+ required: "Post content is required",
139
+ minLength: {
140
+ value: 20,
141
+ message: "Post content must be at least 20 characters"
142
+ }
143
+ })}
144
+ />
145
+
146
+ {/* Existing Images */}
147
+ {existingImages.length > 0 && (
148
+ <div>
149
+ <label className="block text-sm font-semibold text-slate-700 mb-2">
150
+ Current Cover Images
151
+ </label>
152
+ <div className="grid grid-cols-3 sm:grid-cols-4 gap-3 bg-slate-50 p-3 rounded-2xl border border-slate-100">
153
+ {existingImages.map((img, idx) => (
154
+ <div key={idx} className="relative aspect-video rounded-xl overflow-hidden border border-slate-200 shadow-sm">
155
+ <img src={`http://localhost:3000${img}`} alt="Current" className="w-full h-full object-cover" />
156
+ </div>
157
+ ))}
158
+ </div>
159
+ </div>
160
+ )}
161
+
162
+ {/* New Upload Input */}
163
+ <div>
164
+ <label className="block text-sm font-semibold text-slate-700 mb-2">
165
+ Upload New Cover Images (Optional)
166
+ </label>
167
+
168
+ {newPreviewUrls.length > 0 && (
169
+ <div className="bg-amber-50 border border-amber-200/60 rounded-xl p-3.5 flex items-start gap-2.5 text-amber-800 text-xs font-medium mb-3">
170
+ <AlertCircle className="w-4 h-4 text-amber-600 flex-shrink-0 mt-0.5" />
171
+ <span>Note: Uploading new images will replace all current cover images for this blog post.</span>
172
+ </div>
173
+ )}
174
+
175
+ <div className="flex items-center justify-center w-full">
176
+ <label className="flex flex-col items-center justify-center w-full min-h-32 border-2 border-slate-200 border-dashed rounded-xl cursor-pointer bg-slate-50 hover:bg-slate-100 transition-colors group overflow-hidden relative">
177
+ {newPreviewUrls.length > 0 ? (
178
+ <div className="w-full p-4 flex flex-col gap-3">
179
+ <div className="grid grid-cols-3 sm:grid-cols-4 gap-3">
180
+ {newPreviewUrls.map((url, idx) => (
181
+ <div key={idx} className="relative aspect-video rounded-xl overflow-hidden shadow-sm border border-slate-200">
182
+ <img src={url} alt={`Preview ${idx + 1}`} className="w-full h-full object-cover" />
183
+ </div>
184
+ ))}
185
+ </div>
186
+ <div className="text-center">
187
+ <span className="text-[11px] font-semibold text-indigo-700 bg-indigo-50 border border-indigo-100 rounded-lg px-2.5 py-1 inline-block">
188
+ Change New Images ({newPreviewUrls.length} selected)
189
+ </span>
190
+ </div>
191
+ </div>
192
+ ) : (
193
+ <div className="flex flex-col items-center justify-center py-4">
194
+ <ImageIcon className="w-6 h-6 text-slate-400 mb-1 group-hover:scale-110 transition-transform" />
195
+ <p className="text-xs text-slate-500 font-medium">
196
+ Click to replace cover images
197
+ </p>
198
+ </div>
199
+ )}
200
+ <input
201
+ type="file"
202
+ className="hidden"
203
+ accept="image/*"
204
+ multiple
205
+ {...register("images")}
206
+ />
207
+ </label>
208
+ </div>
209
+ </div>
210
+ </form>
211
+
212
+ {/* Footer Actions */}
213
+ <div className="px-6 py-4 border-t border-slate-100 bg-slate-50/50 flex justify-end gap-3">
214
+ <button
215
+ type="button"
216
+ onClick={onClose}
217
+ className="px-4 py-2 border border-slate-200 text-slate-600 rounded-xl text-sm font-semibold hover:bg-slate-100 active:bg-slate-200 transition-colors cursor-pointer"
218
+ >
219
+ Cancel
220
+ </button>
221
+ <button
222
+ type="button"
223
+ onClick={handleSubmit(onSubmit)}
224
+ disabled={loading}
225
+ className="px-5 py-2 bg-indigo-600 hover:bg-indigo-700 text-white rounded-xl text-sm font-semibold flex items-center gap-1.5 shadow-md shadow-indigo-100 disabled:opacity-50 cursor-pointer"
226
+ >
227
+ {loading ? (
228
+ <>
229
+ <Loader2 className="w-4 h-4 animate-spin" />
230
+ Saving...
231
+ </>
232
+ ) : (
233
+ <>
234
+ <Save className="w-4 h-4" />
235
+ Save Changes
236
+ </>
237
+ )}
238
+ </button>
239
+ </div>
240
+
241
+ </div>
242
+ </div>
243
+ );
244
+ };
245
+
246
+ export default EditBlogModal;
@@ -0,0 +1,60 @@
1
+ import React from "react";
2
+
3
+ const FormField = React.forwardRef(({
4
+ label,
5
+ name,
6
+ type = "text",
7
+ placeholder,
8
+ error,
9
+ className = "",
10
+ ...props
11
+ }, ref) => {
12
+ return (
13
+ <div className={`flex flex-col gap-1.5 w-full ${className}`}>
14
+ {label && (
15
+ <label className="block text-sm font-semibold text-slate-700 ml-1">
16
+ {label}
17
+ </label>
18
+ )}
19
+
20
+ {type === "textarea" ? (
21
+ <textarea
22
+ id={name}
23
+ name={name}
24
+ placeholder={placeholder}
25
+ ref={ref}
26
+ className={`w-full px-4 py-3 bg-gray-50/50 border rounded-xl focus:outline-none focus:ring-2 focus:bg-white transition-all shadow-sm text-sm resize-y leading-relaxed ${
27
+ error
28
+ ? "border-rose-300 focus:ring-rose-500/20 focus:border-rose-500 bg-rose-50/30"
29
+ : "border-gray-200 focus:ring-indigo-500/50 focus:border-indigo-500"
30
+ }`}
31
+ {...props}
32
+ />
33
+ ) : (
34
+ <input
35
+ id={name}
36
+ name={name}
37
+ type={type}
38
+ placeholder={placeholder}
39
+ ref={ref}
40
+ className={`w-full px-4 py-3 bg-gray-50/50 border rounded-xl focus:outline-none focus:ring-2 focus:bg-white transition-all shadow-sm text-sm ${
41
+ error
42
+ ? "border-rose-300 focus:ring-rose-500/20 focus:border-rose-500 bg-rose-50/30"
43
+ : "border-gray-200 focus:ring-indigo-500/50 focus:border-indigo-500"
44
+ }`}
45
+ {...props}
46
+ />
47
+ )}
48
+
49
+ {error && (
50
+ <span className="text-xs font-medium text-rose-600 ml-1 mt-0.5 animate-fade-in-up">
51
+ {error.message}
52
+ </span>
53
+ )}
54
+ </div>
55
+ );
56
+ });
57
+
58
+ FormField.displayName = "FormField";
59
+
60
+ export default FormField;
@@ -1,37 +1,54 @@
1
- import { Link } from "react-router-dom";
2
- import { Search, Bell } from "lucide-react";
3
-
4
- const Navbar = () => {
5
- return (
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 */}
9
- </h1>
10
-
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>
23
- </button>
24
-
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>
32
- </div>
33
- </header>
34
- );
35
- };
36
-
1
+ import { useState } from "react";
2
+ import { Link } from "react-router-dom";
3
+ import { Search, Bell } from "lucide-react";
4
+ import { useUser } from "../context/UserContext";
5
+ import ProfileModal from "./ProfileModal";
6
+
7
+ const Navbar = () => {
8
+ const { user } = useUser();
9
+ const [isProfileOpen, setIsProfileOpen] = useState(false);
10
+
11
+ const displayAvatar = user?.profilePicture
12
+ ? `http://localhost:3000${user.profilePicture}`
13
+ : `https://ui-avatars.com/api/?name=${encodeURIComponent(user?.name || "User")}&background=6366f1&color=fff`;
14
+
15
+ return (
16
+ <>
17
+ <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">
18
+ <h1 className="text-xl font-semibold text-slate-800">
19
+ {/* Optional page title could go here */}
20
+ </h1>
21
+
22
+ <div className="flex items-center gap-6">
23
+ <div className="relative hidden md:block">
24
+ <input
25
+ type="text"
26
+ placeholder="Search anything..."
27
+ 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"
28
+ />
29
+ <Search className="absolute left-4 top-2.5 text-slate-400" size={18} />
30
+ </div>
31
+ <button className="relative text-slate-500 hover:text-indigo-600 transition-colors p-2 rounded-full hover:bg-indigo-50">
32
+ <Bell size={20} />
33
+ <span className="absolute top-1.5 right-1.5 w-2 h-2 bg-red-500 rounded-full border border-white"></span>
34
+ </button>
35
+
36
+ <div
37
+ onClick={() => setIsProfileOpen(true)}
38
+ className="flex items-center gap-3 pl-4 border-l border-slate-200 cursor-pointer group"
39
+ >
40
+ <img
41
+ src={displayAvatar}
42
+ alt="profile"
43
+ className="w-10 h-10 rounded-full border-2 border-indigo-100 group-hover:border-indigo-300 transition-all shadow-sm object-cover"
44
+ />
45
+ </div>
46
+ </div>
47
+ </header>
48
+
49
+ <ProfileModal isOpen={isProfileOpen} onClose={() => setIsProfileOpen(false)} />
50
+ </>
51
+ );
52
+ };
53
+
37
54
  export default Navbar;