import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useState } from "react"; import { useMe } from "../api.ts"; import { authClient } from "../auth-client.ts"; import { Btn, Chip, Input, Tile } from "../design/ui.tsx"; type Role = "owner" | "admin" | "member"; const ROLES: Role[] = ["owner", "admin", "member"]; type Member = { id: string; role: string; createdAt: Date | string; userId: string; user: { id: string; email: string; name?: string | null; image?: string | null }; }; type Invitation = { id: string; email: string; role: string | null; status: string; expiresAt: Date | string; }; function memberQueryKey(orgId: string) { return ["org-members", orgId] as const; } function inviteQueryKey(orgId: string) { return ["org-invitations", orgId] as const; } export function OrgMembersCard() { const me = useMe(); const orgId = me.data?.org?.id; const myUserId = me.data?.user.id; const membersQ = useQuery({ queryKey: orgId ? memberQueryKey(orgId) : ["org-members", "none"], enabled: !!orgId, queryFn: async () => { const res = await authClient.organization.listMembers({ query: { organizationId: orgId } }); if (res.error) throw new Error(res.error.message ?? "Failed to load members"); return (res.data?.members ?? []) as Member[]; }, }); const invitesQ = useQuery({ queryKey: orgId ? inviteQueryKey(orgId) : ["org-invitations", "none"], enabled: !!orgId, queryFn: async () => { const res = await authClient.organization.listInvitations({ query: { organizationId: orgId }, }); if (res.error) throw new Error(res.error.message ?? "Failed to load invitations"); return (res.data ?? []) as Invitation[]; }, }); if (!orgId) return null; const myRole = membersQ.data?.find((m) => m.userId === myUserId)?.role ?? "member"; const canManage = myRole === "owner" || myRole === "admin"; return (
Members
{membersQ.isLoading ? (
Loading…
) : membersQ.error ? (
{String(membersQ.error)}
) : (
    {(membersQ.data ?? []).map((m) => ( ))}
)}
{(invitesQ.data?.filter((i) => i.status === "pending").length ?? 0) > 0 && (
Pending invitations
    {(invitesQ.data ?? []) .filter((i) => i.status === "pending") .map((inv) => ( ))}
)}
); } function InviteForm({ orgId, canManage }: { orgId: string; canManage: boolean }) { const [email, setEmail] = useState(""); const [role, setRole] = useState("member"); const [error, setError] = useState(null); const [success, setSuccess] = useState(null); const qc = useQueryClient(); const invite = useMutation({ mutationFn: async (input: { email: string; role: Role }) => { const res = await authClient.organization.inviteMember({ email: input.email, role: input.role, organizationId: orgId, }); if (res.error) throw new Error(res.error.message ?? "Failed to invite"); return res.data; }, onSuccess: (_data, vars) => { setEmail(""); setSuccess(`Invitation sent to ${vars.email}`); setTimeout(() => setSuccess(null), 3000); void qc.invalidateQueries({ queryKey: inviteQueryKey(orgId) }); }, }); if (!canManage) { return (

Only owners and admins can invite new members. Ask one of them for access.

); } const submit = (e: React.FormEvent) => { e.preventDefault(); setError(null); setSuccess(null); const trimmed = email.trim(); if (!trimmed) return; invite.mutate( { email: trimmed, role }, { onError: (e) => setError(e instanceof Error ? e.message : String(e)) }, ); }; return (
setEmail(e.target.value)} placeholder="teammate@example.com" required />
setRole(v)} />
Send invite
{error &&

{error}

} {success &&

{success}

}
); } function MemberRow({ member, orgId, isSelf, canManage, }: { member: Member; orgId: string; isSelf: boolean; canManage: boolean; }) { const qc = useQueryClient(); const [error, setError] = useState(null); const updateRole = useMutation({ mutationFn: async (newRole: Role) => { const res = await authClient.organization.updateMemberRole({ memberId: member.id, role: newRole, organizationId: orgId, }); if (res.error) throw new Error(res.error.message ?? "Failed to update role"); return res.data; }, onSuccess: () => qc.invalidateQueries({ queryKey: memberQueryKey(orgId) }), onError: (e) => setError(e instanceof Error ? e.message : String(e)), }); const remove = useMutation({ mutationFn: async () => { const res = await authClient.organization.removeMember({ memberIdOrEmail: member.id, organizationId: orgId, }); if (res.error) throw new Error(res.error.message ?? "Failed to remove member"); return res.data; }, onSuccess: () => qc.invalidateQueries({ queryKey: memberQueryKey(orgId) }), onError: (e) => setError(e instanceof Error ? e.message : String(e)), }); const role = member.role as Role; const canEditRole = canManage && !isSelf; return (
  • {member.user.name?.trim() || member.user.email} {isSelf && you}
    {member.user.name?.trim() && (
    {member.user.email}
    )} {error &&
    {error}
    }
    {canEditRole ? ( { if (v === role) return; setError(null); updateRole.mutate(v); }} /> ) : ( {role} )} {canManage && !isSelf && ( { if (!window.confirm(`Remove ${member.user.email} from this org?`)) return; setError(null); remove.mutate(); }} > Remove )}
  • ); } function InvitationRow({ invitation, orgId, canManage, }: { invitation: Invitation; orgId: string; canManage: boolean; }) { const qc = useQueryClient(); const [cancelError, setCancelError] = useState(null); const cancel = useMutation({ mutationFn: async () => { const res = await authClient.organization.cancelInvitation({ invitationId: invitation.id, }); if (res.error) throw new Error(res.error.message ?? "Failed to cancel"); return res.data; }, onSuccess: () => qc.invalidateQueries({ queryKey: inviteQueryKey(orgId) }), onError: (e) => setCancelError(e instanceof Error ? e.message : String(e)), }); return (
  • {invitation.email}
    Invited as {invitation.role ?? "member"} · expires{" "} {new Date(invitation.expiresAt).toLocaleDateString()}
    {cancelError &&
    {cancelError}
    }
    {canManage && ( { setCancelError(null); cancel.mutate(); }} > Cancel )}
  • ); } function RoleSelect({ value, onChange, disabled, }: { value: Role; onChange: (next: Role) => void; disabled?: boolean; }) { return (
    ); }