Vectrum
Sign In
65 lines · 2.4 KB · TypeScript
"use client";

import { useActionState, useRef, useState, type ReactNode } from "react";
import { AlertDialog, Spinner, useToast } from "@unlocalhosted/metalui";
import type { FormState } from "@/app/actions/types";
import { Button, type ButtonVariant } from "./ui";

/**
 * One button that runs a server action, with its outcome on the toast deck: for actions that fail
 * for reasons worth reading (a conflict, a branch rule). With `confirm`, it asks first, like
 * ConfirmButton.
 */
export function ActionButton({
  action,
  children,
  icon,
  variant = "default",
  className = "",
  confirm,
}: {
  action: (prev: FormState, formData: FormData) => Promise<FormState>;
  children: ReactNode;
  icon?: ReactNode;
  variant?: ButtonVariant;
  className?: string;
  confirm?: { title: string; description: ReactNode; label: ReactNode; destructive?: boolean };
}) {
  const toast = useToast();
  const form = useRef<HTMLFormElement>(null);
  const [open, setOpen] = useState(false);
  const [, formAction, pending] = useActionState(async (prev: FormState, formData: FormData) => {
    const result = await action(prev, formData);
    if (result?.error) toast.show({ title: result.error, tone: "error" });
    else if (result?.success) toast.show({ title: result.success, tone: "success" });
    return result;
  }, undefined);
  return (
    <form ref={form} action={formAction} className={className}>
      <Button
        type={confirm ? "button" : "submit"}
        onClick={confirm ? () => setOpen(true) : undefined}
        variant={variant}
        disabled={pending}
        aria-busy={pending || undefined}
        icon={pending ? <Spinner size="small" label="Working" /> : icon}
      >
        {children}
      </Button>
      {confirm && (
        <AlertDialog open={open} onOpenChange={setOpen}>
          <AlertDialog.Popup>
            <AlertDialog.Title>{confirm.title}</AlertDialog.Title>
            <AlertDialog.Description>{confirm.description}</AlertDialog.Description>
            <AlertDialog.Actions>
              <AlertDialog.Cancel />
              <AlertDialog.Confirm tone={confirm.destructive || variant === "danger" ? "destructive" : "primary"} onClick={() => form.current?.requestSubmit()}>
                {confirm.label}
              </AlertDialog.Confirm>
            </AlertDialog.Actions>
          </AlertDialog.Popup>
        </AlertDialog>
      )}
    </form>
  );
}