"use client" import { useState } from "react" import { useRouter } from "next/navigation" import { toast } from "sonner" import { Loader2, Trash2, UserPlus } from "lucide-react" type MemberRole = "member" | "viewer" type MemberStatus = "pending" | "active" | "revoked" export type TeamMember = { id: string email: string role: MemberRole status: MemberStatus } const inputClass = "w-full rounded-lg border border-white/10 bg-white/5 px-4 py-2.5 text-sm text-white placeholder-white/30 outline-none ring-indigo-500 transition focus:border-indigo-500/50 focus:ring-1" export function TeamManager({ initialMembers }: { initialMembers: TeamMember[] }) { const router = useRouter() const [members, setMembers] = useState(initialMembers) const [email, setEmail] = useState("") const [role, setRole] = useState("member") const [inviting, setInviting] = useState(false) const [busyId, setBusyId] = useState(null) async function handleInvite(e: React.FormEvent) { e.preventDefault() if (!email.trim()) return setInviting(true) const res = await fetch("/api/team", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email: email.trim(), role }), }) setInviting(false) if (!res.ok) { const data = await res.json().catch(() => ({})) toast.error(typeof data.error === "string" ? data.error : "Failed to send invite") return } toast.success(`Invite sent to ${email.trim()}`) setEmail("") setRole("member") router.refresh() } async function handleRoleToggle(member: TeamMember) { const nextRole: MemberRole = member.role === "member" ? "viewer" : "member" setBusyId(member.id) const res = await fetch(`/api/team/${member.id}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ role: nextRole }), }) setBusyId(null) if (!res.ok) { const data = await res.json().catch(() => ({})) toast.error(typeof data.error === "string" ? data.error : "Failed to update role") return } setMembers((prev) => prev.map((m) => (m.id === member.id ? { ...m, role: nextRole } : m)) ) toast.success("Role updated") router.refresh() } async function handleRevoke(member: TeamMember) { setBusyId(member.id) const res = await fetch(`/api/team/${member.id}`, { method: "DELETE" }) setBusyId(null) if (!res.ok) { const data = await res.json().catch(() => ({})) toast.error(typeof data.error === "string" ? data.error : "Failed to revoke access") return } setMembers((prev) => prev.filter((m) => m.id !== member.id)) toast.success(`Removed ${member.email}`) router.refresh() } return (
{/* Invite form */}

Invite a team member

They'll get an email with a link to join your account.

setEmail(e.target.value)} placeholder="teammate@example.com" className={inputClass + " sm:flex-1"} />

Members can view and edit your portfolio. Viewers have read-only access.

{/* Members list */}

Team members{" "} ({members.length})

{members.length === 0 ? (
No team members yet. Invite someone above to get started.
) : (
    {members.map((member) => { const busy = busyId === member.id return (
  • {member.email}

    {member.role}
  • ) })}
)}
) } function StatusBadge({ status }: { status: MemberStatus }) { const styles: Record = { active: "border-emerald-500/20 bg-emerald-500/10 text-emerald-400", pending: "border-amber-500/20 bg-amber-500/10 text-amber-400", revoked: "border-white/10 bg-white/5 text-white/40", } return ( {status} ) }