@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.
- package/dist/api/router.d.ts.map +1 -1
- package/dist/api/router.js +16 -1
- package/dist/api/users.d.ts +12 -0
- package/dist/api/users.d.ts.map +1 -1
- package/dist/api/users.js +94 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/views/UserManagement.d.ts.map +1 -1
- package/dist/views/UserManagement.js +94 -22
- package/package.json +9 -9
- package/src/api/router.ts +19 -1
- package/src/api/users.ts +106 -0
- package/src/index.tsx +1 -7
- package/src/views/UserManagement.tsx +404 -211
|
@@ -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 [
|
|
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
|
|
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({ ...
|
|
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
|
|
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
|
-
|
|
113
|
+
setIsProcessing(true);
|
|
63
114
|
try {
|
|
64
|
-
const
|
|
65
|
-
|
|
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(
|
|
127
|
+
body: JSON.stringify(payload)
|
|
68
128
|
});
|
|
69
129
|
|
|
70
130
|
if (res.ok) {
|
|
71
|
-
const
|
|
72
|
-
|
|
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 ||
|
|
140
|
+
alert(err.error || `Failed to ${modalMode} user`);
|
|
78
141
|
}
|
|
79
142
|
} catch (err) {
|
|
80
|
-
alert("Network error
|
|
143
|
+
alert("Network error occurred");
|
|
81
144
|
} finally {
|
|
82
|
-
|
|
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 (
|
|
93
|
-
if (
|
|
94
|
-
if (!confirm(
|
|
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/${
|
|
98
|
-
if (res.ok) setUsers(users.filter(u => u._id !==
|
|
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
|
|
109
|
-
{/*
|
|
110
|
-
<div className="
|
|
111
|
-
<div
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
{/*
|
|
130
|
-
<div className="
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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-
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
-
{/*
|
|
195
|
-
<
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
<div
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
>
|
|
212
|
-
<
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
</
|
|
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
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
<div className="
|
|
241
|
-
<div
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
className="
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
|
|
261
|
-
|
|
262
|
-
<
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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
|
-
<
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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
|
-
|
|
328
|
-
|
|
521
|
+
)}
|
|
522
|
+
</AnimatePresence>
|
|
329
523
|
</div>
|
|
330
524
|
);
|
|
331
525
|
}
|
|
332
|
-
|