Add shared ConfirmDialog; upgrade wordfilter with client actions and toasts.
Local Build and Deploy / deploy (push) Successful in 1m6s
Local Build and Deploy / deploy (push) Successful in 1m6s
Co-authored-by: Cursor <[email protected]>
This commit is contained in:
1 parent
1039044e48
commit
a2ca9b3c23
9 files changed
+366
-149
No files matched your search
@@ -0,0 +1,131 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
|
||||
export type ConfirmDialogProps = {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
title: string;
|
||||
description: string;
|
||||
confirmLabel?: string;
|
||||
cancelLabel?: string;
|
||||
variant?: "danger" | "default";
|
||||
loading?: boolean;
|
||||
onConfirm: () => void | Promise<void>;
|
||||
};
|
||||
|
||||
export function ConfirmDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
title,
|
||||
description,
|
||||
confirmLabel = "Confirm",
|
||||
cancelLabel = "Cancel",
|
||||
variant = "danger",
|
||||
loading,
|
||||
onConfirm,
|
||||
}: ConfirmDialogProps) {
|
||||
const [busy, setBusy] = useState(false);
|
||||
const isLoading = loading ?? busy;
|
||||
|
||||
async function handleConfirm() {
|
||||
setBusy(true);
|
||||
try {
|
||||
await onConfirm();
|
||||
onOpenChange(false);
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onOpenChange={(next) => {
|
||||
if (isLoading) return;
|
||||
onOpenChange(next);
|
||||
}}
|
||||
>
|
||||
<DialogContent showCloseButton={!isLoading}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
<DialogDescription>{description}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
disabled={isLoading}
|
||||
onClick={() => onOpenChange(false)}
|
||||
>
|
||||
{cancelLabel}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant={variant === "danger" ? "destructive" : "default"}
|
||||
disabled={isLoading}
|
||||
onClick={handleConfirm}
|
||||
>
|
||||
{confirmLabel}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export type ConfirmOptions = {
|
||||
title: string;
|
||||
description: string;
|
||||
confirmLabel?: string;
|
||||
cancelLabel?: string;
|
||||
variant?: "danger" | "default";
|
||||
};
|
||||
|
||||
/** Imperative confirm — returns a Promise. Render `{dialog}` in the tree. */
|
||||
export function useConfirmDialog() {
|
||||
const [opts, setOpts] = useState<(ConfirmOptions & { open: boolean }) | null>(
|
||||
null,
|
||||
);
|
||||
const resolver = useRef<((value: boolean) => void) | null>(null);
|
||||
|
||||
const confirm = useCallback((options: ConfirmOptions) => {
|
||||
return new Promise<boolean>((resolve) => {
|
||||
resolver.current = resolve;
|
||||
setOpts({ ...options, open: true });
|
||||
});
|
||||
}, []);
|
||||
|
||||
const settle = useCallback((value: boolean) => {
|
||||
const resolve = resolver.current;
|
||||
resolver.current = null;
|
||||
setOpts(null);
|
||||
resolve?.(value);
|
||||
}, []);
|
||||
|
||||
const dialog = opts ? (
|
||||
<ConfirmDialog
|
||||
open={opts.open}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) settle(false);
|
||||
}}
|
||||
title={opts.title}
|
||||
description={opts.description}
|
||||
confirmLabel={opts.confirmLabel}
|
||||
cancelLabel={opts.cancelLabel}
|
||||
variant={opts.variant}
|
||||
onConfirm={() => settle(true)}
|
||||
/>
|
||||
) : null;
|
||||
|
||||
return { confirm, dialog };
|
||||
}
|
||||
Reference in new issue
Block a user