SPB Git forge

spb/fetcha

Public
11commits 1branches 0releases
1.5 MBsize
maindefault branch
16 days agolast push
TypeScript 97.5% SQL 1.4% Python 0.8%
3.3 KB · 101 lines tsx
Raw Blame History
1"use client";2import * as React from "react";3import { useRouter } from "next/navigation";4import { Button, type ButtonProps } from "@/components/ui/button";5import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";6import type { AdminActionResult } from "@/actions/admin";78/**9 * Runs a server action inside a transition and refreshes the router. When `confirm` is given, a10 * dialog gates the (usually destructive) action.11 */12export function ActionButton({13  action,14  children,15  confirm,16  onDone,17  ...btn18}: {19  action: () => Promise<AdminActionResult<unknown>>;20  children: React.ReactNode;21  confirm?: { title: string; description?: React.ReactNode; confirmLabel?: string; variant?: ButtonProps["variant"] };22  onDone?: (r: AdminActionResult<unknown>) => void;23} & Omit<ButtonProps, "onClick" | "children">) {24  const router = useRouter();25  const [pending, start] = React.useTransition();26  const [open, setOpen] = React.useState(false);27  const [msg, setMsg] = React.useState<{ kind: "error" | "warning"; text: string } | null>(null);2829  const run = () =>30    start(async () => {31      setMsg(null);32      const r = await action();33      if (!r.ok) setMsg({ kind: "error", text: r.error });34      else if (r.warning) setMsg({ kind: "warning", text: r.warning });35      onDone?.(r);36      if (r.ok) {37        setOpen(false);38        router.refresh();39      }40    });4142  return (43    <>44      <span className="inline-flex flex-col items-start gap-1">45        <Button type="button" {...btn} loading={pending} onClick={() => (confirm ? setOpen(true) : run())}>46          {children}47        </Button>48        {msg && !confirm ? (49          <span role="alert" className={msg.kind === "error" ? "text-[12px] text-danger" : "text-[12px] text-warning"}>50            {msg.text}51          </span>52        ) : null}53      </span>54      {confirm ? (55        <Dialog open={open} onOpenChange={setOpen}>56          <DialogContent size="sm">57            <DialogHeader>58              <DialogTitle>{confirm.title}</DialogTitle>59              {confirm.description ? <DialogDescription>{confirm.description}</DialogDescription> : null}60            </DialogHeader>61            {msg ? (62              <p role="alert" className={msg.kind === "error" ? "text-[13px] text-danger" : "text-[13px] text-warning"}>63                {msg.text}64              </p>65            ) : null}66            <DialogFooter>67              <Button type="button" variant="outline" onClick={() => setOpen(false)}>68                Cancel69              </Button>70              <Button type="button" variant={confirm.variant ?? "danger"} loading={pending} onClick={run}>71                {confirm.confirmLabel ?? "Confirm"}72              </Button>73            </DialogFooter>74          </DialogContent>75        </Dialog>76      ) : null}77    </>78  );79}8081export function ResultMessage({ result }: { result: AdminActionResult<unknown> | null }) {82  if (!result) return null;83  if (!result.ok)84    return (85      <p role="alert" className="text-[12.5px] text-danger">86        {result.error}87      </p>88    );89  if (result.warning)90    return (91      <p role="status" className="text-[12.5px] text-warning">92        {result.warning}93      </p>94    );95  return (96    <p role="status" className="text-[12.5px] text-success">97      Saved.98    </p>99  );100}101