@jhits/plugin-users 0.0.14 → 0.0.15

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.
@@ -1,27 +1,41 @@
1
1
  'use client';
2
2
 
3
- import { useState, useEffect } from "react";
3
+ import { useState, useEffect, useMemo } from "react";
4
4
  import {
5
5
  Users, UserPlus, Shield,
6
- Trash2, Key, Search, Loader2, X, Eye, EyeOff, Copy, Check, Calendar
6
+ Trash2, Key, Search, Loader2, X, Eye, EyeOff, Copy, Check, Calendar,
7
+ UserCircle, Mail, MoreHorizontal, Edit2, ShieldCheck, Activity, ChevronRight, Globe2
7
8
  } from "lucide-react";
9
+ import { motion, AnimatePresence } from "framer-motion";
10
+ import { useSession } from "next-auth/react";
11
+ import Image from "next/image";
8
12
 
9
13
  interface User {
10
14
  _id: string;
11
15
  name: string;
12
16
  email: string;
17
+ image?: string;
13
18
  role: 'dev' | 'admin' | 'editor';
14
19
  createdAt: string;
15
20
  }
16
21
 
17
22
  export default function UserManagement({ locale = 'en' }: { locale?: string }) {
23
+ const { data: session } = useSession();
18
24
  const [users, setUsers] = useState<User[]>([]);
19
25
  const [loading, setLoading] = useState(true);
20
26
  const [searchTerm, setSearchTerm] = useState("");
21
27
 
28
+ // Current User Info
29
+ const currentUserEmail = session?.user?.email;
30
+ const currentUserRole = (session?.user as any)?.role || 'editor';
31
+
22
32
  // Modal State
23
33
  const [isModalOpen, setIsModalOpen] = useState(false);
24
- const [isCreating, setIsCreating] = useState(false);
34
+ const [modalMode, setModalMode] = useState<'create' | 'edit'>('create');
35
+ const [selectedUser, setSelectedUser] = useState<User | null>(null);
36
+ const [activeUserIdx, setActiveUserIdx] = useState(0);
37
+
38
+ const [isProcessing, setIsProcessing] = useState(false);
25
39
  const [showPassword, setShowPassword] = useState(false);
26
40
  const [copied, setCopied] = useState(false);
27
41
 
@@ -30,7 +44,7 @@ export default function UserManagement({ locale = 'en' }: { locale?: string }) {
30
44
  name: "",
31
45
  email: "",
32
46
  password: "",
33
- role: "editor" as const
47
+ role: "editor" as 'dev' | 'admin' | 'editor'
34
48
  });
35
49
 
36
50
  const fetchUsers = async () => {
@@ -48,38 +62,87 @@ export default function UserManagement({ locale = 'en' }: { locale?: string }) {
48
62
 
49
63
  useEffect(() => { fetchUsers(); }, []);
50
64
 
65
+ // Permission Logic
66
+ const canManageEcosystem = currentUserRole === 'dev' || currentUserRole === 'admin';
67
+
68
+ const canActionUser = (targetUser: User) => {
69
+ if (!canManageEcosystem) return false;
70
+ if (targetUser.email === currentUserEmail) return false; // Can't delete/edit role of self in this view
71
+
72
+ // Dev can manage everyone
73
+ if (currentUserRole === 'dev') return true;
74
+
75
+ // Admin can only manage editors (cannot touch devs or other admins)
76
+ if (currentUserRole === 'admin') {
77
+ return targetUser.role === 'editor';
78
+ }
79
+
80
+ return false;
81
+ };
82
+
51
83
  const generateTempPassword = () => {
52
84
  const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%";
53
85
  let retVal = "";
54
86
  for (let i = 0, n = charset.length; i < 10; ++i) {
55
87
  retVal += charset.charAt(Math.floor(Math.random() * n));
56
88
  }
57
- setFormData({ ...formData, password: retVal });
89
+ setFormData(prev => ({ ...prev, password: retVal }));
90
+ };
91
+
92
+ const handleOpenCreate = () => {
93
+ setModalMode('create');
94
+ setFormData({ name: "", email: "", password: "", role: "editor" });
95
+ generateTempPassword();
96
+ setIsModalOpen(true);
58
97
  };
59
98
 
60
- const handleCreateUser = async (e: React.FormEvent) => {
99
+ const handleOpenEdit = (user: User) => {
100
+ setModalMode('edit');
101
+ setSelectedUser(user);
102
+ setFormData({
103
+ name: user.name,
104
+ email: user.email,
105
+ password: "", // Don't show existing password
106
+ role: user.role
107
+ });
108
+ setIsModalOpen(true);
109
+ };
110
+
111
+ const handleSubmit = async (e: React.FormEvent) => {
61
112
  e.preventDefault();
62
- setIsCreating(true);
113
+ setIsProcessing(true);
63
114
  try {
64
- const res = await fetch('/api/users', {
65
- method: 'POST',
115
+ const url = modalMode === 'create' ? '/api/users' : `/api/users/${selectedUser?._id}`;
116
+ const method = modalMode === 'create' ? 'POST' : 'PATCH';
117
+
118
+ // For editing, only send password if provided
119
+ const payload = { ...formData };
120
+ if (modalMode === 'edit' && !payload.password) {
121
+ delete (payload as any).password;
122
+ }
123
+
124
+ const res = await fetch(url, {
125
+ method,
66
126
  headers: { 'Content-Type': 'application/json' },
67
- body: JSON.stringify(formData)
127
+ body: JSON.stringify(payload)
68
128
  });
69
129
 
70
130
  if (res.ok) {
71
- const newUser = await res.json();
72
- setUsers([...users, newUser]);
131
+ const updatedUser = await res.json();
132
+ if (modalMode === 'create') {
133
+ setUsers([...users, updatedUser]);
134
+ } else {
135
+ setUsers(users.map(u => u._id === updatedUser._id ? updatedUser : u));
136
+ }
73
137
  setIsModalOpen(false);
74
- setFormData({ name: "", email: "", password: "", role: "editor" });
75
138
  } else {
76
139
  const err = await res.json();
77
- alert(err.error || "Failed to create user");
140
+ alert(err.error || `Failed to ${modalMode} user`);
78
141
  }
79
142
  } catch (err) {
80
- alert("Network error while creating user");
143
+ alert("Network error occurred");
81
144
  } finally {
82
- setIsCreating(false);
145
+ setIsProcessing(false);
83
146
  }
84
147
  };
85
148
 
@@ -89,13 +152,13 @@ export default function UserManagement({ locale = 'en' }: { locale?: string }) {
89
152
  setTimeout(() => setCopied(false), 2000);
90
153
  };
91
154
 
92
- const handleDelete = async (userId: string, userRole: string) => {
93
- if (userRole === 'dev') return alert("Cannot delete developer account.");
94
- if (!confirm("Are you sure you want to delete this user?")) return;
155
+ const handleDelete = async (user: User) => {
156
+ if (!canActionUser(user)) return;
157
+ if (!confirm(`Are you certain you want to remove ${user.name}? This action is final.`)) return;
95
158
 
96
159
  try {
97
- const res = await fetch(`/api/users/${userId}`, { method: 'DELETE' });
98
- if (res.ok) setUsers(users.filter(u => u._id !== userId));
160
+ const res = await fetch(`/api/users/${user._id}`, { method: 'DELETE' });
161
+ if (res.ok) setUsers(users.filter(u => u._id !== user._id));
99
162
  } catch (err) { alert("Error deleting user."); }
100
163
  };
101
164
 
@@ -104,229 +167,359 @@ export default function UserManagement({ locale = 'en' }: { locale?: string }) {
104
167
  user.email?.toLowerCase().includes(searchTerm.toLowerCase())
105
168
  );
106
169
 
170
+ const getRoleStyles = (role: string) => {
171
+ switch (role) {
172
+ case 'dev': return 'bg-primary/20 text-primary border-primary/30 shadow-[0_0_20px_rgba(var(--color-primary),0.2)]';
173
+ case 'admin': return 'bg-emerald-500/20 text-emerald-500 border-emerald-500/30 shadow-[0_0_20px_rgba(16,185,129,0.1)]';
174
+ default: return 'bg-amber-500/20 text-amber-500 border-amber-500/30';
175
+ }
176
+ };
177
+
178
+ const permissions = {
179
+ dev: ['Full System Access', 'Kernel Management', 'Database Write', 'Ecosystem Logic', 'Security Protocol'],
180
+ admin: ['User Management', 'Content Orchestration', 'Settings Access', 'Analytical Review', 'Standard Security'],
181
+ editor: ['Content Creation', 'Media Management', 'Basic Settings', 'Public Preview']
182
+ };
183
+
184
+ const activeUser = filteredUsers[activeUserIdx] || filteredUsers[0];
185
+
107
186
  return (
108
- <div className="h-full w-full rounded-[2.5rem] bg-dashboard-card dark:bg-neutral-900 p-8 overflow-y-auto">
109
- {/* Header Section */}
110
- <div className="flex flex-col md:flex-row md:items-center justify-between gap-6 mb-8">
111
- <div>
112
- <h1 className="text-3xl font-black text-dashboard-text uppercase tracking-tighter mb-2">
113
- User Management
114
- </h1>
115
- <p className="text-sm text-dashboard-text-secondary">
116
- Manage who has access to the dashboard.
117
- </p>
118
- </div>
187
+ <div className="h-full flex flex-col overflow-hidden bg-transparent">
188
+ {/* VIBRANT HERO HEADER */}
189
+ <div className="shrink-0 p-8 lg:p-10 pb-6 border-b border-dashboard-border/30 bg-dashboard-card/20 backdrop-blur-md relative overflow-hidden">
190
+ <div className="absolute top-0 right-0 w-1/3 h-full bg-gradient-to-l from-primary/10 to-transparent pointer-events-none" />
191
+ <div className="flex flex-col lg:flex-row lg:items-center justify-between gap-8 relative z-10">
192
+ <div className="space-y-4">
193
+ <div className="inline-flex items-center gap-2 px-4 py-1.5 rounded-full bg-primary/15 border border-primary/30 text-primary text-[10px] font-bold uppercase tracking-[0.2em] shadow-lg shadow-primary/5">
194
+ <Activity size={14} className="animate-pulse" />
195
+ <span>System Access Control</span>
196
+ </div>
197
+ <h1 className="text-5xl font-bold text-dashboard-text tracking-tight leading-none">
198
+ User <span className="text-primary italic">Management</span>
199
+ </h1>
200
+ </div>
119
201
 
120
- <button
121
- onClick={() => { setIsModalOpen(true); generateTempPassword(); }}
122
- className="inline-flex items-center gap-2 px-6 py-3 bg-primary text-white rounded-full text-[10px] font-black uppercase tracking-widest hover:bg-primary/90 transition-all shadow-lg shadow-primary/20"
123
- >
124
- <UserPlus size={16} />
125
- New User
126
- </button>
202
+ <div className="flex items-center gap-6">
203
+ <div className="flex flex-col items-end px-6 border-r border-dashboard-border/40">
204
+ <span className="text-[10px] font-bold text-dashboard-text-secondary uppercase tracking-[0.3em] opacity-50 mb-1">Team Members</span>
205
+ <div className="flex items-center gap-3">
206
+ <div className="flex -space-x-3">
207
+ {users.slice(0, 3).map((u, i) => (
208
+ <div key={i} className="size-8 rounded-full border-2 border-dashboard-card bg-primary flex items-center justify-center text-[10px] font-bold text-white shadow-xl overflow-hidden">
209
+ {u.image ? <img src={u.image} className="size-full object-cover" /> : u.name[0]}
210
+ </div>
211
+ ))}
212
+ {users.length > 3 && (
213
+ <div className="size-8 rounded-full border-2 border-dashboard-card bg-dashboard-card flex items-center justify-center text-[10px] font-bold text-dashboard-text-secondary">
214
+ +{users.length - 3}
215
+ </div>
216
+ )}
217
+ </div>
218
+ <span className="text-2xl font-bold text-dashboard-text">{users.length}</span>
219
+ </div>
220
+ </div>
221
+ {canManageEcosystem && (
222
+ <button
223
+ onClick={handleOpenCreate}
224
+ className="group flex items-center gap-4 px-8 py-4 bg-primary text-white rounded-2xl text-xs font-bold uppercase tracking-widest shadow-2xl shadow-primary/30 hover:shadow-primary/50 hover:scale-105 active:scale-95 transition-all"
225
+ >
226
+ <UserPlus size={20} />
227
+ <span>Add New User</span>
228
+ </button>
229
+ )}
230
+ </div>
231
+ </div>
127
232
  </div>
128
233
 
129
- {/* Toolbar */}
130
- <div className="bg-dashboard-bg dark:bg-neutral-800/50 rounded-2xl sm:rounded-[2rem] border border-dashboard-border overflow-hidden">
131
- <div className="p-4 sm:p-6 border-b border-dashboard-border flex items-center justify-between">
132
- <div className="relative w-full sm:w-72">
133
- <Search className="absolute left-3 top-1/2 -translate-y-1/2 text-dashboard-text-secondary" size={16} />
234
+ {/* MAIN COMMAND CENTER SPLIT VIEW */}
235
+ <div className="flex-1 flex overflow-hidden p-6 lg:p-8 gap-8">
236
+ {/* LEFT: THE ROSTER */}
237
+ <aside className="w-96 flex flex-col gap-6 shrink-0 h-full">
238
+ <div className="relative group">
239
+ <Search className="absolute left-4 top-1/2 -translate-y-1/2 text-primary/40 group-focus-within:text-primary transition-colors" size={18} />
134
240
  <input
135
241
  type="text"
136
242
  placeholder="Search users..."
137
243
  value={searchTerm}
138
244
  onChange={(e) => setSearchTerm(e.target.value)}
139
- className="w-full pl-10 pr-4 py-2.5 bg-dashboard-card border border-dashboard-border rounded-lg text-sm outline-none focus:ring-2 focus:ring-primary/20 transition-all text-dashboard-text placeholder:text-dashboard-text-secondary"
245
+ className="w-full pl-12 pr-6 py-3.5 bg-dashboard-card/40 border border-dashboard-border/40 rounded-2xl text-sm font-semibold outline-none focus:ring-2 focus:ring-primary/20 transition-all text-dashboard-text placeholder:text-dashboard-text-secondary/30"
140
246
  />
141
247
  </div>
142
- {loading && <Loader2 className="animate-spin text-primary ml-4" size={20} />}
143
- </div>
144
248
 
145
- {/* Desktop Table */}
146
- <div className="hidden md:block overflow-x-auto">
147
- <table className="w-full text-left border-collapse">
148
- <thead>
149
- <tr className="text-[11px] font-bold text-dashboard-text-secondary uppercase tracking-widest border-b border-dashboard-border">
150
- <th className="px-8 py-4">User</th>
151
- <th className="px-8 py-4">Role</th>
152
- <th className="px-8 py-4">Since</th>
153
- <th className="px-8 py-4 text-right">Actions</th>
154
- </tr>
155
- </thead>
156
- <tbody className="divide-y divide-dashboard-border">
157
- {filteredUsers.map((user) => (
158
- <tr key={user._id} className="group hover:bg-dashboard-card transition-colors">
159
- <td className="px-8 py-5">
160
- <div className="flex items-center gap-3">
161
- <div className="w-10 h-10 rounded-full bg-primary/10 text-primary flex items-center justify-center font-bold border border-primary/20">
162
- {user.name?.[0] || 'U'}
163
- </div>
164
- <div>
165
- <p className="font-bold text-dashboard-text text-sm">{user.name}</p>
166
- <p className="text-xs text-dashboard-text-secondary">{user.email}</p>
249
+ <div className="flex-1 overflow-y-auto custom-scrollbar pr-2 space-y-3">
250
+ <AnimatePresence mode="popLayout">
251
+ {filteredUsers.map((user, index) => {
252
+ const isActive = activeUser?._id === user._id;
253
+ const isSelf = user.email === currentUserEmail;
254
+ return (
255
+ <motion.button
256
+ key={user._id}
257
+ onClick={() => setActiveUserIdx(index)}
258
+ initial={{ opacity: 0, x: -20 }}
259
+ animate={{ opacity: 1, x: 0 }}
260
+ className={`w-full text-left p-4 rounded-2xl border transition-all flex items-center gap-4 group ${
261
+ isActive
262
+ ? 'bg-primary text-white border-primary shadow-xl shadow-primary/20'
263
+ : 'bg-dashboard-card/30 border-dashboard-border/40 hover:border-primary/40 text-dashboard-text'
264
+ }`}
265
+ >
266
+ <div className={`size-12 rounded-xl flex items-center justify-center font-bold text-white shrink-0 shadow-lg ${isActive ? 'bg-white/20' : 'bg-primary'}`}>
267
+ {user.image ? <img src={user.image} className="size-full object-cover rounded-xl" /> : user.name[0].toUpperCase()}
268
+ </div>
269
+ <div className="flex-1 min-w-0">
270
+ <div className="flex items-center gap-2">
271
+ <span className={`block text-sm font-bold truncate ${isActive ? 'text-white' : 'text-dashboard-text'}`}>{user.name}</span>
272
+ {isSelf && <span className="size-1.5 rounded-full bg-emerald-400 animate-pulse" />}
167
273
  </div>
274
+ <span className={`block text-[10px] font-medium uppercase tracking-wider opacity-60 truncate ${isActive ? 'text-white' : 'text-dashboard-text-secondary'}`}>{user.role}</span>
168
275
  </div>
169
- </td>
170
- <td className="px-8 py-5">
171
- <span className={`inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-[10px] font-bold uppercase tracking-wider ${user.role === 'dev' ? 'bg-primary text-white' : 'bg-dashboard-bg text-dashboard-text'
172
- }`}>
173
- <Shield size={10} /> {user.role}
174
- </span>
175
- </td>
176
- <td className="px-8 py-5 text-sm text-dashboard-text-secondary">
177
- {user.createdAt ? new Date(user.createdAt).toLocaleDateString() : '-'}
178
- </td>
179
- <td className="px-8 py-5 text-right">
180
- <button
181
- onClick={() => handleDelete(user._id, user.role)}
182
- className="p-2 text-dashboard-text-secondary hover:text-red-500 transition-colors disabled:opacity-0"
183
- disabled={user.role === 'dev'}
184
- >
185
- <Trash2 size={16} />
186
- </button>
187
- </td>
188
- </tr>
189
- ))}
190
- </tbody>
191
- </table>
192
- </div>
276
+ <ChevronRight size={16} className={`transition-transform ${isActive ? 'translate-x-1 opacity-100' : 'opacity-0 group-hover:opacity-40'}`} />
277
+ </motion.button>
278
+ );
279
+ })}
280
+ </AnimatePresence>
281
+ </div>
282
+ </aside>
193
283
 
194
- {/* Mobile Card List */}
195
- <div className="md:hidden divide-y divide-dashboard-border">
196
- {filteredUsers.map((user) => (
197
- <div key={user._id} className="p-5 space-y-4">
198
- <div className="flex justify-between items-start">
199
- <div className="flex items-center gap-3">
200
- <div className="w-10 h-10 rounded-full bg-primary/10 text-primary flex items-center justify-center font-bold border border-primary/20">
201
- {user.name?.[0] || 'U'}
284
+ {/* RIGHT: THE CORE DETAIL VIEW */}
285
+ <main className="flex-1 min-w-0 h-full">
286
+ <AnimatePresence mode="wait">
287
+ {activeUser ? (
288
+ <motion.div
289
+ key={activeUser._id}
290
+ initial={{ opacity: 0, scale: 0.98 }}
291
+ animate={{ opacity: 1, scale: 1 }}
292
+ className="h-full bg-dashboard-card/40 backdrop-blur-2xl rounded-[2.5rem] border border-dashboard-border/40 overflow-hidden flex flex-col relative"
293
+ >
294
+ {/* PROFILE AMBIANCE */}
295
+ <div className="absolute top-0 right-0 w-full h-64 bg-gradient-to-b from-primary/10 to-transparent pointer-events-none" />
296
+
297
+ <div className="flex-1 overflow-y-auto custom-scrollbar p-10 space-y-12 relative z-10">
298
+ {/* CORE IDENTITY HEADER */}
299
+ <div className="flex items-center gap-10">
300
+ <div className="relative">
301
+ <div className="size-32 rounded-[2.5rem] bg-primary flex items-center justify-center text-4xl font-bold text-white shadow-2xl shadow-primary/30 overflow-hidden">
302
+ {activeUser.image ? <img src={activeUser.image} className="size-full object-cover" /> : activeUser.name[0].toUpperCase()}
303
+ </div>
304
+ <div className="absolute -bottom-2 -right-2 p-3 bg-dashboard-card border border-dashboard-border rounded-2xl shadow-xl">
305
+ {activeUser.role === 'dev' ? <ShieldCheck className="text-primary" size={24} /> : <Shield className="text-emerald-500" size={24} />}
306
+ </div>
307
+ </div>
308
+
309
+ <div className="space-y-3">
310
+ <div className="flex items-center gap-4">
311
+ <h2 className="text-4xl font-bold text-dashboard-text tracking-tight uppercase italic">
312
+ {activeUser.name}
313
+ </h2>
314
+ {activeUser.email === currentUserEmail && (
315
+ <span className="px-3 py-1 bg-emerald-500/10 text-emerald-500 text-[10px] font-bold uppercase tracking-widest rounded-lg border border-emerald-500/20">Active Session</span>
316
+ )}
317
+ </div>
318
+ <div className="flex items-center gap-6 text-sm text-dashboard-text-secondary/70">
319
+ <div className="flex items-center gap-2">
320
+ <Mail size={16} className="text-primary/60" />
321
+ {activeUser.email}
322
+ </div>
323
+ <div className="flex items-center gap-2">
324
+ <Calendar size={16} className="text-primary/60" />
325
+ Joined {new Date(activeUser.createdAt).toLocaleDateString()}
326
+ </div>
327
+ </div>
328
+ <div className={`inline-flex items-center px-4 py-1.5 rounded-full border text-[10px] font-bold uppercase tracking-[0.2em] ${getRoleStyles(activeUser.role)}`}>
329
+ {activeUser.role} Account
330
+ </div>
331
+ </div>
202
332
  </div>
203
- <div className="min-w-0">
204
- <p className="font-bold text-dashboard-text text-sm truncate">{user.name}</p>
205
- <p className="text-[11px] text-dashboard-text-secondary truncate">{user.email}</p>
333
+
334
+ {/* PERMISSION MATRIX VISUALIZATION */}
335
+ <div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
336
+ <div className="space-y-6">
337
+ <h4 className="text-[10px] font-bold text-primary uppercase tracking-[0.3em] ml-1">Account Permissions</h4>
338
+ <div className="space-y-3">
339
+ {permissions[activeUser.role as keyof typeof permissions].map((perm, i) => (
340
+ <div key={i} className="flex items-center gap-4 p-4 bg-dashboard-bg/40 border border-dashboard-border/30 rounded-2xl group hover:border-primary/40 transition-all">
341
+ <div className="size-2 rounded-full bg-primary/40 group-hover:bg-primary shadow-[0_0_8px_rgba(var(--color-primary),0.5)] transition-all" />
342
+ <span className="text-sm font-semibold text-dashboard-text/90">{perm}</span>
343
+ <Check size={14} className="ml-auto text-emerald-500" />
344
+ </div>
345
+ ))}
346
+ </div>
347
+ </div>
348
+
349
+ <div className="space-y-6">
350
+ <h4 className="text-[10px] font-bold text-primary uppercase tracking-[0.3em] ml-1">Account Actions</h4>
351
+ <div className="bg-dashboard-bg/40 border border-dashboard-border/30 rounded-3xl p-8 space-y-8 h-fit">
352
+ <p className="text-xs text-dashboard-text-secondary leading-relaxed font-medium">
353
+ Manage this user's details, password, and access level within your dashboard.
354
+ </p>
355
+
356
+ <div className="flex flex-col gap-3">
357
+ {canActionUser(activeUser) ? (
358
+ <>
359
+ <button
360
+ onClick={() => handleOpenEdit(activeUser)}
361
+ className="w-full flex items-center justify-center gap-3 py-4 bg-dashboard-card border border-dashboard-border/60 rounded-2xl text-xs font-bold uppercase tracking-widest text-dashboard-text hover:bg-primary hover:text-white hover:border-primary transition-all active:scale-[0.98]"
362
+ >
363
+ <Edit2 size={16} />
364
+ Edit User Details
365
+ </button>
366
+ <button
367
+ onClick={() => handleDelete(activeUser)}
368
+ className="w-full flex items-center justify-center gap-3 py-4 bg-red-500/10 border border-red-500/20 rounded-2xl text-xs font-bold uppercase tracking-widest text-red-500 hover:bg-red-500 hover:text-white transition-all active:scale-[0.98]"
369
+ >
370
+ <Trash2 size={16} />
371
+ Remove User
372
+ </button>
373
+ </>
374
+ ) : (
375
+ <div className="p-6 rounded-2xl bg-neutral-500/5 border border-dashed border-neutral-500/20 text-center">
376
+ <Shield size={24} className="mx-auto mb-3 text-neutral-500/40" />
377
+ <span className="text-[10px] font-bold text-neutral-500 uppercase tracking-widest">Protected Account</span>
378
+ </div>
379
+ )}
380
+ </div>
381
+ </div>
382
+ </div>
206
383
  </div>
207
384
  </div>
208
- <button
209
- onClick={() => handleDelete(user._id, user.role)}
210
- className={`p-2 rounded-lg bg-red-50 dark:bg-red-950/20 text-red-400 ${user.role === 'dev' ? 'hidden' : 'block'}`}
211
- >
212
- <Trash2 size={16} />
213
- </button>
214
- </div>
215
-
216
- <div className="flex items-center justify-between pt-2 border-t border-dashboard-border">
217
- <span className={`inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-[9px] font-black uppercase tracking-wider ${user.role === 'dev' ? 'bg-primary text-white' : 'bg-dashboard-bg text-dashboard-text'
218
- }`}>
219
- <Shield size={10} /> {user.role}
220
- </span>
221
- <div className="flex items-center gap-1.5 text-[10px] text-dashboard-text-secondary font-medium">
222
- <Calendar size={12} />
223
- {user.createdAt ? new Date(user.createdAt).toLocaleDateString() : '-'}
385
+ </motion.div>
386
+ ) : (
387
+ <div className="h-full flex items-center justify-center bg-dashboard-card/20 rounded-[2.5rem] border border-dashed border-dashboard-border/40">
388
+ <div className="text-center space-y-4">
389
+ <div className="size-20 bg-primary/10 rounded-3xl flex items-center justify-center mx-auto text-primary/40">
390
+ <Users size={40} />
391
+ </div>
392
+ <p className="text-sm font-bold text-dashboard-text-secondary uppercase tracking-widest">Select a node to inspect</p>
224
393
  </div>
225
394
  </div>
226
- </div>
227
- ))}
228
- </div>
229
-
230
- {filteredUsers.length === 0 && !loading && (
231
- <div className="p-12 text-center text-dashboard-text-secondary text-sm">
232
- No users found matching your search.
233
- </div>
234
- )}
395
+ )}
396
+ </AnimatePresence>
397
+ </main>
235
398
  </div>
236
399
 
237
- {/* Create User Modal */}
238
- {isModalOpen && (
239
- <div className="fixed inset-0 z-50 flex items-end sm:items-center justify-center p-0 sm:p-4 bg-black/40 backdrop-blur-sm animate-in fade-in duration-200">
240
- <div className="bg-dashboard-card w-full max-w-md rounded-t-[2.5rem] sm:rounded-[2.5rem] shadow-2xl overflow-hidden animate-in slide-in-from-bottom sm:zoom-in-95 duration-300 max-h-[90vh] overflow-y-auto border border-dashboard-border">
241
- <div className="p-6 sm:p-8 bg-primary text-white flex justify-between items-center sticky top-0 z-10">
242
- <h2 className="text-xl sm:text-2xl font-black uppercase tracking-tighter">New User</h2>
243
- <button onClick={() => setIsModalOpen(false)} className="hover:rotate-90 transition-transform p-2">
244
- <X size={24} />
245
- </button>
246
- </div>
247
-
248
- <form onSubmit={handleCreateUser} className="p-6 sm:p-8 space-y-5">
249
- <div className="space-y-2">
250
- <label className="text-xs font-bold text-dashboard-text-secondary uppercase tracking-widest ml-1">Name</label>
251
- <input
252
- required
253
- className="w-full px-5 py-3 bg-dashboard-bg border border-dashboard-border rounded-2xl focus:ring-2 focus:ring-primary outline-none text-sm transition-all text-dashboard-text"
254
- value={formData.name}
255
- onChange={e => setFormData({ ...formData, name: e.target.value })}
256
- placeholder="e.g. John Doe"
257
- />
400
+ {/* Combined Create/Edit Modal */}
401
+ <AnimatePresence>
402
+ {isModalOpen && (
403
+ <div className="fixed inset-0 z-50 flex items-center justify-center p-4 sm:p-6 bg-black/40 backdrop-blur-md">
404
+ <motion.div
405
+ initial={{ opacity: 0, scale: 0.95, y: 10 }}
406
+ animate={{ opacity: 1, scale: 1, y: 0 }}
407
+ exit={{ opacity: 0, scale: 0.95, y: 10 }}
408
+ className="bg-dashboard-card/90 backdrop-blur-2xl w-full max-w-md rounded-[2rem] shadow-2xl overflow-hidden flex flex-col max-h-[90vh] border border-dashboard-border/40"
409
+ >
410
+ {/* Modal Header */}
411
+ <div className="p-8 pb-4 flex justify-between items-center shrink-0">
412
+ <div>
413
+ <h2 className="text-2xl font-bold text-dashboard-text tracking-tight leading-none mb-2">
414
+ {modalMode === 'create' ? 'New Account' : 'Update Account'}
415
+ </h2>
416
+ <p className="text-[10px] font-bold text-primary uppercase tracking-widest opacity-80">
417
+ {modalMode === 'create' ? 'Define identity and permissions' : `Modifying ${selectedUser?.name}`}
418
+ </p>
419
+ </div>
420
+ <button onClick={() => setIsModalOpen(false)} className="hover:text-red-500 transition-all p-2 bg-dashboard-bg/50 rounded-xl border border-dashboard-border/40 active:scale-90">
421
+ <X size={20} />
422
+ </button>
258
423
  </div>
259
424
 
260
- <div className="space-y-2">
261
- <label className="text-xs font-bold text-dashboard-text-secondary uppercase tracking-widest ml-1">Email Address</label>
262
- <input
263
- required
264
- type="email"
265
- className="w-full px-5 py-3 bg-dashboard-bg border border-dashboard-border rounded-2xl focus:ring-2 focus:ring-primary outline-none text-sm transition-all text-dashboard-text"
266
- value={formData.email}
267
- onChange={e => setFormData({ ...formData, email: e.target.value })}
268
- placeholder="email@example.com"
269
- />
270
- </div>
425
+ {/* Modal Form */}
426
+ <form onSubmit={handleSubmit} className="flex-1 overflow-y-auto px-8 pb-8 space-y-6 custom-scrollbar">
427
+ <div className="space-y-2">
428
+ <label className="text-[10px] font-bold text-dashboard-text-secondary uppercase tracking-widest ml-1 opacity-60">Account Identity</label>
429
+ <div className="relative group">
430
+ <UserCircle className="absolute left-4 top-1/2 -translate-y-1/2 text-dashboard-text-secondary/40 group-focus-within:text-primary transition-colors" size={18} />
431
+ <input
432
+ required
433
+ className="w-full pl-12 pr-6 py-3 bg-dashboard-bg/50 border border-dashboard-border/40 rounded-xl focus:ring-2 focus:ring-primary/20 focus:border-primary/30 outline-none text-sm font-semibold transition-all text-dashboard-text"
434
+ value={formData.name}
435
+ onChange={e => setFormData({ ...formData, name: e.target.value })}
436
+ placeholder="e.g. Sarah Jenkins"
437
+ />
438
+ </div>
439
+ </div>
271
440
 
272
- <div className="space-y-2">
273
- <label className="text-xs font-bold text-dashboard-text-secondary uppercase tracking-widest ml-1">Temporary Password</label>
274
- <div className="relative">
275
- <input
276
- required
277
- type={showPassword ? "text" : "password"}
278
- className="w-full px-5 py-3 bg-dashboard-bg border border-dashboard-border rounded-2xl focus:ring-2 focus:ring-primary outline-none text-sm transition-all font-mono text-dashboard-text"
279
- value={formData.password}
280
- onChange={e => setFormData({ ...formData, password: e.target.value })}
281
- />
282
- <div className="absolute right-3 top-1/2 -translate-y-1/2 flex items-center gap-1">
283
- <button type="button" onClick={() => setShowPassword(!showPassword)} className="p-1.5 text-dashboard-text-secondary hover:text-primary">
284
- {showPassword ? <EyeOff size={16} /> : <Eye size={16} />}
285
- </button>
286
- <button type="button" onClick={copyToClipboard} className="p-1.5 text-dashboard-text-secondary hover:text-primary">
287
- {copied ? <Check size={16} className="text-emerald-500" /> : <Copy size={16} />}
288
- </button>
441
+ <div className="space-y-2">
442
+ <label className="text-[10px] font-bold text-dashboard-text-secondary uppercase tracking-widest ml-1 opacity-60">Email Endpoint</label>
443
+ <div className="relative group">
444
+ <Mail className="absolute left-4 top-1/2 -translate-y-1/2 text-dashboard-text-secondary/40 group-focus-within:text-primary transition-colors" size={18} />
445
+ <input
446
+ required
447
+ type="email"
448
+ className="w-full pl-12 pr-6 py-3 bg-dashboard-bg/50 border border-dashboard-border/40 rounded-xl focus:ring-2 focus:ring-primary/20 focus:border-primary/30 outline-none text-sm font-semibold transition-all text-dashboard-text"
449
+ value={formData.email}
450
+ onChange={e => setFormData({ ...formData, email: e.target.value })}
451
+ placeholder="sarah@yourteam.com"
452
+ />
289
453
  </div>
290
454
  </div>
291
- <button
292
- type="button"
293
- onClick={generateTempPassword}
294
- className="text-[10px] text-primary font-bold uppercase tracking-wider hover:underline ml-1"
295
- >
296
- Generate new password
297
- </button>
298
- </div>
299
455
 
300
- <div className="space-y-2">
301
- <label className="text-xs font-bold text-dashboard-text-secondary uppercase tracking-widest ml-1">Role</label>
302
- <div className="relative">
303
- <select
304
- className="w-full px-5 py-3 bg-dashboard-bg border border-dashboard-border rounded-2xl focus:ring-2 focus:ring-primary outline-none text-sm appearance-none cursor-pointer text-dashboard-text"
305
- value={formData.role}
306
- onChange={e => setFormData({ ...formData, role: e.target.value as any })}
456
+ <div className="space-y-2">
457
+ <label className="text-[10px] font-bold text-dashboard-text-secondary uppercase tracking-widest ml-1 opacity-60">
458
+ {modalMode === 'create' ? 'Security Credentials' : 'New Password (Optional)'}
459
+ </label>
460
+ <div className="relative group">
461
+ <Key className="absolute left-4 top-1/2 -translate-y-1/2 text-dashboard-text-secondary/40 group-focus-within:text-primary transition-colors" size={18} />
462
+ <input
463
+ required={modalMode === 'create'}
464
+ type={showPassword ? "text" : "password"}
465
+ className="w-full pl-12 pr-28 py-3 bg-dashboard-bg/50 border border-dashboard-border/40 rounded-xl focus:ring-2 focus:ring-primary/20 focus:border-primary/30 outline-none text-sm font-mono font-bold transition-all text-dashboard-text"
466
+ value={formData.password}
467
+ onChange={e => setFormData({ ...formData, password: e.target.value })}
468
+ placeholder={modalMode === 'edit' ? "Keep current" : ""}
469
+ />
470
+ <div className="absolute right-3 top-1/2 -translate-y-1/2 flex items-center gap-1.5">
471
+ <button type="button" onClick={() => setShowPassword(!showPassword)} className="p-1.5 text-dashboard-text-secondary/60 hover:text-primary transition-colors bg-dashboard-card/50 rounded-lg border border-dashboard-border/40">
472
+ {showPassword ? <EyeOff size={14} /> : <Eye size={14} />}
473
+ </button>
474
+ <button type="button" onClick={copyToClipboard} className="p-1.5 text-dashboard-text-secondary/60 hover:text-primary transition-colors bg-dashboard-card/50 rounded-lg border border-dashboard-border/40">
475
+ {copied ? <Check size={14} className="text-emerald-500" /> : <Copy size={14} />}
476
+ </button>
477
+ </div>
478
+ </div>
479
+ <button
480
+ type="button"
481
+ onClick={generateTempPassword}
482
+ className="text-[9px] text-primary/80 font-bold uppercase tracking-widest hover:text-primary transition-colors ml-1 flex items-center gap-2"
307
483
  >
308
- <option value="editor">Editor (Content only)</option>
309
- <option value="dev">Developer</option>
310
- <option value="admin">Admin (All settings)</option>
311
- </select>
312
- <div className="absolute right-4 top-1/2 -translate-y-1/2 pointer-events-none text-dashboard-text-secondary">
313
- <Shield size={16} />
484
+ <Key size={10} /> Auto-generate secure protocol
485
+ </button>
486
+ </div>
487
+
488
+ <div className="space-y-2">
489
+ <label className="text-[10px] font-bold text-dashboard-text-secondary uppercase tracking-widest ml-1 opacity-60">Access Configuration</label>
490
+ <div className="relative">
491
+ <select
492
+ className="w-full px-5 py-3 bg-dashboard-bg/50 border border-dashboard-border/40 rounded-xl focus:ring-2 focus:ring-primary/20 focus:border-primary/30 outline-none text-xs font-bold uppercase tracking-wider appearance-none cursor-pointer text-dashboard-text"
493
+ value={formData.role}
494
+ onChange={e => setFormData({ ...formData, role: e.target.value as any })}
495
+ >
496
+ <option value="editor">Content Editor</option>
497
+ <option value="admin">System Administrator</option>
498
+ {currentUserRole === 'dev' && <option value="dev">Technical Developer</option>}
499
+ </select>
500
+ <div className="absolute right-5 top-1/2 -translate-y-1/2 pointer-events-none text-primary/60">
501
+ <Shield size={16} />
502
+ </div>
314
503
  </div>
315
504
  </div>
316
- </div>
317
505
 
318
- <button
319
- type="submit"
320
- disabled={isCreating}
321
- className="w-full py-4 bg-primary text-white rounded-full text-[10px] font-black uppercase tracking-widest shadow-lg shadow-primary/20 hover:bg-primary/90 transition-all mt-4 flex items-center justify-center gap-2"
322
- >
323
- {isCreating ? <Loader2 className="animate-spin" size={20} /> : "Add User"}
324
- </button>
325
- </form>
506
+ <button
507
+ type="submit"
508
+ disabled={isProcessing}
509
+ className="group relative w-full py-4 bg-primary text-white rounded-xl text-xs font-bold uppercase tracking-widest overflow-hidden transition-all shadow-lg shadow-primary/20 hover:scale-[1.01] active:scale-95 flex items-center justify-center gap-3 mt-4"
510
+ >
511
+ {isProcessing ? <Loader2 className="animate-spin relative z-10" size={18} /> : (
512
+ <>
513
+ {modalMode === 'create' ? <UserPlus size={18} className="relative z-10" /> : <Edit2 size={18} className="relative z-10" />}
514
+ <span className="relative z-10">{modalMode === 'create' ? 'Initialize Account' : 'Commit Updates'}</span>
515
+ </>
516
+ )}
517
+ </button>
518
+ </form>
519
+ </motion.div>
326
520
  </div>
327
- </div>
328
- )}
521
+ )}
522
+ </AnimatePresence>
329
523
  </div>
330
524
  );
331
525
  }
332
-