Add role selector to invite form and user management

This commit is contained in:
2026-01-28 18:46:02 +00:00
parent 4b753cb57a
commit 872a06d713
3 changed files with 50 additions and 13 deletions

4
dist/index.html vendored
View File

@@ -6,8 +6,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="Todo App - Task management made simple" /> <meta name="description" content="Todo App - Task management made simple" />
<title>Todo App</title> <title>Todo App</title>
<script type="module" crossorigin src="/assets/index-Ce_4Zv7a.js"></script> <script type="module" crossorigin src="/assets/index-CyVj0RaD.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-Cuyvk5mt.css"> <link rel="stylesheet" crossorigin href="/assets/index-D509-xKq.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

View File

@@ -250,7 +250,7 @@ class ApiClient {
await this.fetch(`/admin/users/${id}`, { method: 'DELETE' }); await this.fetch(`/admin/users/${id}`, { method: 'DELETE' });
} }
async createInvite(data: { email: string; name: string }): Promise<Invite & { setupUrl: string }> { async createInvite(data: { email: string; name: string; role?: 'admin' | 'user' }): Promise<Invite & { setupUrl: string }> {
return this.fetch('/admin/invites', { return this.fetch('/admin/invites', {
method: 'POST', method: 'POST',
body: JSON.stringify(data), body: JSON.stringify(data),

View File

@@ -16,6 +16,7 @@ export function AdminPage() {
const [showInviteForm, setShowInviteForm] = useState(false); const [showInviteForm, setShowInviteForm] = useState(false);
const [inviteEmail, setInviteEmail] = useState(''); const [inviteEmail, setInviteEmail] = useState('');
const [inviteName, setInviteName] = useState(''); const [inviteName, setInviteName] = useState('');
const [inviteRole, setInviteRole] = useState<'admin' | 'user'>('user');
const [inviteError, setInviteError] = useState(''); const [inviteError, setInviteError] = useState('');
const [inviteUrl, setInviteUrl] = useState(''); const [inviteUrl, setInviteUrl] = useState('');
const [copied, setCopied] = useState(false); const [copied, setCopied] = useState(false);
@@ -45,7 +46,7 @@ export function AdminPage() {
setInviteError(''); setInviteError('');
try { try {
const result = await api.createInvite({ email: inviteEmail, name: inviteName }); const result = await api.createInvite({ email: inviteEmail, name: inviteName, role: inviteRole });
setInviteUrl(result.setupUrl); setInviteUrl(result.setupUrl);
setInvites([result, ...invites]); setInvites([result, ...invites]);
// Keep form open to show the URL // Keep form open to show the URL
@@ -60,6 +61,15 @@ export function AdminPage() {
setTimeout(() => setCopied(false), 2000); setTimeout(() => setCopied(false), 2000);
}; };
const handleChangeRole = async (userId: string, role: 'admin' | 'user' | 'service') => {
try {
await api.updateUserRole(userId, role);
setUsers(users.map(u => u.id === userId ? { ...u, role } : u));
} catch (error) {
console.error('Failed to update role:', error);
}
};
const handleDeleteUser = async (userId: string) => { const handleDeleteUser = async (userId: string) => {
if (!confirm('Are you sure you want to delete this user?')) return; if (!confirm('Are you sure you want to delete this user?')) return;
@@ -84,6 +94,7 @@ export function AdminPage() {
setShowInviteForm(false); setShowInviteForm(false);
setInviteEmail(''); setInviteEmail('');
setInviteName(''); setInviteName('');
setInviteRole('user');
setInviteError(''); setInviteError('');
setInviteUrl(''); setInviteUrl('');
}; };
@@ -149,14 +160,29 @@ export function AdminPage() {
<td className="px-4 py-3 text-sm font-medium text-gray-900">{user.name}</td> <td className="px-4 py-3 text-sm font-medium text-gray-900">{user.name}</td>
<td className="px-4 py-3 text-sm text-gray-600">{user.email}</td> <td className="px-4 py-3 text-sm text-gray-600">{user.email}</td>
<td className="px-4 py-3"> <td className="px-4 py-3">
<span className={cn( {user.id === currentUser?.id || user.role === 'service' ? (
'px-2 py-1 text-xs rounded-full', <span className={cn(
user.role === 'admin' ? 'bg-purple-100 text-purple-700' : 'px-2 py-1 text-xs rounded-full',
user.role === 'service' ? 'bg-blue-100 text-blue-700' : user.role === 'admin' ? 'bg-purple-100 text-purple-700' :
'bg-gray-100 text-gray-700' user.role === 'service' ? 'bg-blue-100 text-blue-700' :
)}> 'bg-gray-100 text-gray-700'
{user.role} )}>
</span> {user.role}
</span>
) : (
<select
value={user.role}
onChange={(e) => handleChangeRole(user.id, e.target.value as 'admin' | 'user')}
className={cn(
'px-2 py-1 text-xs rounded-full border-none cursor-pointer appearance-none',
user.role === 'admin' ? 'bg-purple-100 text-purple-700' :
'bg-gray-100 text-gray-700'
)}
>
<option value="user">user</option>
<option value="admin">admin</option>
</select>
)}
</td> </td>
<td className="px-4 py-3 text-sm text-gray-500"> <td className="px-4 py-3 text-sm text-gray-500">
{new Date(user.createdAt).toLocaleDateString()} {new Date(user.createdAt).toLocaleDateString()}
@@ -221,7 +247,7 @@ export function AdminPage() {
{inviteError && ( {inviteError && (
<p className="text-sm text-red-500">{inviteError}</p> <p className="text-sm text-red-500">{inviteError}</p>
)} )}
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-3 gap-4">
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1">Name</label> <label className="block text-sm font-medium text-gray-700 mb-1">Name</label>
<input <input
@@ -244,6 +270,17 @@ export function AdminPage() {
placeholder="john@example.com" placeholder="john@example.com"
/> />
</div> </div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Role</label>
<select
value={inviteRole}
onChange={(e) => setInviteRole(e.target.value as 'admin' | 'user')}
className="input"
>
<option value="user">User</option>
<option value="admin">Admin</option>
</select>
</div>
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<button type="submit" className="btn btn-primary"> <button type="submit" className="btn btn-primary">