1"use client";
2
3
14import { useState } from "react";
15import { Loader2, Mail, UserPlus } from "lucide-react";
16import { toast } from "@/stores/ui-store";
17import { useWorkspaceMembers } from "@/hooks/use-workspace";
18import { useInviteMember, ASSIGNABLE_ROLES, roleLabel } from "@/hooks/use-members";
19import { UserAvatar } from "@/components/database/cell-renderer";
20import { Dialog } from "@/components/dialogs/dialog";
21
22export interface InviteDialogProps {
23 open: boolean;
24 workspaceId: string;
25 workspaceName?: string;
26 onClose: () => void;
27}
28
29export function InviteDialog({ open, workspaceId, workspaceName, onClose }: InviteDialogProps) {
30 const members = useWorkspaceMembers(workspaceId);
31 const invite = useInviteMember({ workspaceId });
32
33 const [email, setEmail] = useState("");
34 const [role, setRole] = useState<string>("MEMBER");
35
36 const submit = () => {
37 if (!email.trim()) return;
38 invite.mutate(
39 { email: email.trim(), role: role as (typeof ASSIGNABLE_ROLES)[number] },
40 {
41 onSuccess: () => {
42 toast("success", "Invitation sent", email.trim());
43 setEmail("");
44 },
45 onError: (e) => toast("error", "Invite failed", (e as Error).message)
46 }
47 );
48 };
49
50 return (
51 <Dialog
52 open={open}
53 title="Members"
54 description={workspaceName ? `Manage access to ${workspaceName}` : "Manage workspace access"}
55 onClose={onClose}
56 width="max-w-md"
57 >
58 <div className="space-y-5">
59 {}
60 <div>
61 <label className="mb-1.5 flex items-center gap-1 text-xs font-medium text-muted-foreground">
62 <Mail className="size-3" aria-hidden /> Invite by email
63 </label>
64 <div className="flex items-center gap-1.5">
65 <input
66 type="email"
67 className="min-w-0 flex-1 rounded-md border border-input bg-background px-2.5 py-1.5 text-sm outline-none focus-visible:ring-1 focus-visible:ring-ring"
68 placeholder="name@example.com"
69 value={email}
70 onChange={(e) => setEmail(e.target.value)}
71 onKeyDown={(e) => e.key === "Enter" && submit()}
72 />
73 <select
74 className="rounded-md border border-input bg-background px-1.5 py-1.5 text-sm outline-none"
75 value={role}
76 onChange={(e) => setRole(e.target.value)}
77 aria-label="Role"
78 >
79 {ASSIGNABLE_ROLES.map((r) => (
80 <option key={r} value={r}>
81 {roleLabel(r)}
82 </option>
83 ))}
84 </select>
85 <button
86 type="button"
87 className="inline-flex items-center gap-1 rounded-md bg-primary px-2.5 py-1.5 text-sm font-medium text-primary-foreground hover:opacity-90 disabled:opacity-50"
88 disabled={!email.trim() || invite.isPending}
89 onClick={submit}
90 >
91 {invite.isPending ? <Loader2 className="size-3.5 animate-spin" /> : <UserPlus className="size-3.5" />}
92 Invite
93 </button>
94 </div>
95 </div>
96
97 {}
98 <div>
99 <p className="mb-1.5 text-xs font-medium text-muted-foreground">
100 Members ({members.data?.length ?? 0})
101 </p>
102 {members.isLoading && <p className="text-sm text-muted-foreground">Loading…</p>}
103 <ul className="space-y-1">
104 {(members.data ?? []).map((m) => (
105 <li key={m.userId} className="flex items-center gap-2.5 rounded-md px-2 py-1.5">
106 <UserAvatar user={{ id: m.user.id, name: m.user.name, avatarUrl: m.user.image ?? null }} size={26} />
107 <div className="min-w-0 flex-1">
108 <p className="truncate text-sm">{m.user.name}</p>
109 <p className="truncate text-xs text-muted-foreground">{m.user.email}</p>
110 </div>
111 <span className="rounded-full bg-muted px-2 py-0.5 text-[11px] font-medium text-muted-foreground">
112 {roleLabel(m.role)}
113 </span>
114 </li>
115 ))}
116 </ul>
117 </div>
118 </div>
119 </Dialog>
120 );
121}
122