112 lines
4.3 KiB
TypeScript
112 lines
4.3 KiB
TypeScript
'use client';
|
|
|
|
import { useCallback, useEffect, useState } from 'react';
|
|
|
|
type Member = { id: string; email: string; name: string | null; role: string };
|
|
|
|
export function MembersManager({ orgId, actorRole }: { orgId: string; actorRole: string }) {
|
|
const [members, setMembers] = useState<Member[]>([]);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const canManage = actorRole === 'owner' || actorRole === 'admin';
|
|
|
|
const load = useCallback(async () => {
|
|
const res = await fetch(`/api/orgs/${orgId}/members`);
|
|
const data = (await res.json().catch(() => ({}))) as { members?: Member[] };
|
|
setMembers(data.members ?? []);
|
|
}, [orgId]);
|
|
useEffect(() => {
|
|
void load();
|
|
}, [load]);
|
|
|
|
async function add(e: React.FormEvent<HTMLFormElement>) {
|
|
e.preventDefault();
|
|
setError(null);
|
|
const form = new FormData(e.currentTarget);
|
|
const res = await fetch(`/api/orgs/${orgId}/members`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ email: form.get('email'), role: form.get('role') }),
|
|
});
|
|
if (!res.ok) setError(((await res.json().catch(() => ({}))) as { error?: string }).error || 'Could not add the member.');
|
|
(e.target as HTMLFormElement).reset();
|
|
await load();
|
|
}
|
|
|
|
async function setRole(id: string, role: string) {
|
|
await fetch(`/api/orgs/${orgId}/members/${id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ role }) });
|
|
await load();
|
|
}
|
|
|
|
async function remove(id: string) {
|
|
if (!window.confirm('Remove this member?')) return;
|
|
await fetch(`/api/orgs/${orgId}/members/${id}`, { method: 'DELETE' });
|
|
await load();
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<h1 className="text-2xl font-bold">Members</h1>
|
|
|
|
{canManage ? (
|
|
<form className="card flex flex-wrap items-end gap-3" onSubmit={add}>
|
|
<label className="text-sm">
|
|
Email
|
|
<input name="email" type="email" required className="input mt-1" placeholder="teammate@example.com" />
|
|
</label>
|
|
<label className="text-sm">
|
|
Role
|
|
<select name="role" className="input mt-1" defaultValue="member">
|
|
<option value="member">member</option>
|
|
<option value="admin">admin</option>
|
|
</select>
|
|
</label>
|
|
<button type="submit" className="btn-primary">
|
|
Add member
|
|
</button>
|
|
{error && <p className="w-full text-sm text-red-600">{error}</p>}
|
|
</form>
|
|
) : (
|
|
<p className="muted text-sm">Only owners and admins can manage members.</p>
|
|
)}
|
|
|
|
<div className="overflow-x-auto rounded-xl border" style={{ borderColor: 'var(--border)' }}>
|
|
<table className="w-full text-sm">
|
|
<thead className="muted text-left text-xs uppercase">
|
|
<tr>
|
|
<th className="px-4 py-3">Email</th>
|
|
<th className="px-4 py-3">Name</th>
|
|
<th className="px-4 py-3">Role</th>
|
|
{canManage && <th className="px-4 py-3 text-right">Actions</th>}
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{members.map((m) => (
|
|
<tr key={m.id} className="border-t" style={{ borderColor: 'var(--border)' }}>
|
|
<td className="px-4 py-3 font-medium">{m.email}</td>
|
|
<td className="px-4 py-3">{m.name ?? '—'}</td>
|
|
<td className="px-4 py-3">{m.role}</td>
|
|
{canManage && (
|
|
<td className="px-4 py-3">
|
|
<div className="flex justify-end gap-2">
|
|
{m.role !== 'owner' && (
|
|
<>
|
|
<button type="button" className="btn-secondary" onClick={() => setRole(m.id, m.role === 'admin' ? 'member' : 'admin')}>
|
|
{m.role === 'admin' ? 'Make member' : 'Make admin'}
|
|
</button>
|
|
<button type="button" className="btn-danger" onClick={() => remove(m.id)}>
|
|
Remove
|
|
</button>
|
|
</>
|
|
)}
|
|
</div>
|
|
</td>
|
|
)}
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|