zazzy
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-dialog

Run 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";

On this page