templates2.Saas.Next/components/org/MembersManager.tsx
w4c-templates 6538da5797
Some checks are pending
CI / quality (push) Waiting to run
chore(template): publish Saas.Next
2026-10-11 23:24:56 +03:00

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>
);
}