"use client"; import { useCallback, useEffect, useState } from "react"; import { toast } from "sonner"; import { api, staff, AdminError, type AdminRole, type StaffMember, } from "@/lib/api"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Avatar } from "@/components/ui/avatar"; import { Table, THead, TH, TR, TD } from "@/components/ui/table"; import { useConfirm } from "@/components/confirm"; import { Clamp } from "@/components/ui/clamp"; const errMsg = (err: unknown, fallback: string) => err instanceof AdminError ? err.message : fallback; export function StaffTab() { const confirm = useConfirm(); const [members, setMembers] = useState(null); const [roles, setRoles] = useState([]); const [selfId, setSelfId] = useState(null); const [handle, setHandle] = useState(""); const [grantRole, setGrantRole] = useState(""); const [busy, setBusy] = useState(null); const load = useCallback(async () => { try { const [m, r, me] = await Promise.all([ staff.list(), staff.roles(), api.me(), ]); setMembers(m); setRoles(r); setSelfId(me?.id ?? null); setGrantRole( (prev) => prev || r.find((x) => x.key === "moderator")?.id || r[0]?.id || "", ); } catch (err) { toast.error(errMsg(err, "Could not load staff")); setMembers([]); } }, []); useEffect(() => { void load(); }, [load]); const run = async (key: string, fn: () => Promise, ok: string) => { setBusy(key); try { await fn(); toast.success(ok); await load(); } catch (err) { toast.error(errMsg(err, "That didn't work")); } finally { setBusy(null); } }; const grant = () => { const h = handle.trim().replace(/^@/, ""); if (!h || !grantRole) return; // Only someone new to the panel gets the welcome email; granting to an // existing staff member just changes their role. const existing = members?.some((m) => m.handle === h); void run( "grant", () => staff.grant(h, grantRole), existing ? `@${h}'s role was changed` : `@${h} is now staff — we've emailed them how to log in`, ).then(() => setHandle("")); }; const block = async (m: StaffMember) => { const blocking = !m.blocked_at; if ( blocking && !(await confirm({ title: `Block ${m.name}?`, message: "They keep their role but can't use the admin panel until you unblock them.", confirmLabel: "Block", destructive: true, })) ) { return; } void run( m.user_id, () => staff.setBlocked(m.user_id, blocking), blocking ? "Blocked" : "Unblocked", ); }; const revoke = async (m: StaffMember) => { if ( !(await confirm({ title: `Remove ${m.name} from staff?`, message: "They lose admin access entirely. Their user account is unaffected.", confirmLabel: "Remove", destructive: true, })) ) { return; } void run(m.user_id, () => staff.revoke(m.user_id), "Removed from staff"); }; return (

Add a staff member

setHandle(e.target.value)} onKeyDown={(e) => e.key === "Enter" && grant()} placeholder="@handle" className="max-w-xs" /> sizes itself to its longest option; a long custom // role name must not widen the form past the screen. className="h-10 min-w-0 max-w-full flex-1 truncate rounded-lg border border-input bg-card px-3 text-sm sm:max-w-xs sm:flex-none" > {roles.map((r) => ( ))}
{members === null ? (

Loading…

) : ( {members.map((m) => { const self = m.user_id === selfId; return ( ); })}
Person Role Status Actions
{m.name}{" "} {self && ( (you) )} {`@${m.handle}`}
{/* Your own row is read-only: nobody changes their own access, so a superadmin can't demote themselves into a locked-out panel by accident. */} {self ? ( {m.role_name} ) : ( )} {m.blocked_at ? ( Blocked ) : ( Active )} {!self && (
)}
)}
); }