Blocks
Org invite dialog
A compact dialog for inviting a member to the active org, wired to the orgs invite mutation.
A compact "Invite member" dialog for the active org — drop it in a dashboard
header or members toolbar. It wraps the orgs invite.create mutation; on success
it toasts, closes, and refreshes the pending-invite list. Managing invites is an
admin+ action server-side, so a member who submits sees the mutation rejected.
Requires: orgs
Preview
"use client";import { useState } from "react";import { zodResolver } from "@hookform/resolvers/zod";import { Controller, useForm } from "react-hook-form";import { toast } from "sonner";import { Button } from "@/components/ui/button";import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger,} from "@/components/ui/dialog";import { Input } from "@/components/ui/input";import { Label } from "@/components/ui/label";import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue,} from "@/components/ui/select";import { inviteSchema, type InviteValues } from "@/features/orgs/lib/schemas";import { trpc } from "@/lib/trpc";/** * A compact "Invite member" dialog for the active org — drop it in a dashboard * header or members toolbar. Wraps the orgs `invite.create` mutation; on success * it toasts, closes, and invalidates the pending-invite list so any list on the * page refreshes. Managing invites is an admin+ action server-side, so a member * who opens it sees the mutation rejected with that error. */export function InviteMemberDialog() { const [open, setOpen] = useState(false); const utils = trpc.useUtils(); const create = trpc.invite.create.useMutation({ onSuccess: () => { toast.success("Invitation sent."); setOpen(false); return utils.invite.invalidate(); }, }); const { register, control, handleSubmit, reset, formState: { errors, isSubmitting }, } = useForm<InviteValues>({ resolver: zodResolver(inviteSchema), defaultValues: { email: "", role: "member" }, }); const onSubmit = handleSubmit(async (values) => { create.reset(); await create.mutateAsync(values); reset({ email: "", role: "member" }); }); return ( <Dialog open={open} onOpenChange={setOpen}> <DialogTrigger asChild> <Button>Invite member</Button> </DialogTrigger> <DialogContent> <DialogHeader> <DialogTitle>Invite a member</DialogTitle> <DialogDescription> They join after signing in and accepting the emailed invitation. </DialogDescription> </DialogHeader> <form onSubmit={onSubmit} className="flex flex-col gap-4" noValidate> <div className="flex flex-col gap-1.5"> <Label htmlFor="invite-email">Email address</Label> <Input id="invite-email" type="email" placeholder="person@example.com" autoComplete="off" {...register("email")} /> {errors.email && ( <p className="text-sm text-destructive">{errors.email.message}</p> )} </div> <div className="flex flex-col gap-1.5"> <Label htmlFor="invite-role">Role</Label> <Controller control={control} name="role" render={({ field }) => ( <Select value={field.value} onValueChange={field.onChange}> <SelectTrigger id="invite-role" aria-label="Role"> <SelectValue /> </SelectTrigger> <SelectContent> <SelectItem value="member">Member</SelectItem> <SelectItem value="admin">Admin</SelectItem> </SelectContent> </Select> )} /> </div> {create.isError && ( <p role="alert" className="text-sm text-destructive"> {create.error.message || "Could not send that invitation."} </p> )} <DialogFooter> <Button type="submit" disabled={isSubmitting}> {isSubmitting ? "Inviting…" : "Send invite"} </Button> </DialogFooter> </form> </DialogContent> </Dialog> );}Offline preview
The docs site has no backend, so the block runs against a stand-in tRPC client: the trigger and form are fully interactive, but submitting does not call a real server.
Install
npx create-saas-starter add org-invite-dialogRun it from your app's root. add only writes new files — wiring is up to you:
import { InviteMemberDialog } from "@/components/blocks/org-invite-dialog/invite-member-dialog";