Files
2026-06-16 14:50:09 +05:30

80 lines
2.1 KiB
TypeScript

"use client";
import {
createContext,
useCallback,
useContext,
useState,
type ReactNode,
} from "react";
import { Modal } from "@/components/ui/modal";
import { Button } from "@/components/ui/button";
type ConfirmOpts = {
title: string;
message?: string;
confirmLabel?: string;
destructive?: boolean;
};
type ConfirmFn = (opts: ConfirmOpts) => Promise<boolean>;
const ConfirmContext = createContext<ConfirmFn | null>(null);
/** Imperative confirm — `if (await confirm({...})) { … }`. Renders a styled
* modal instead of the browser's window.confirm. Provided by ConfirmProvider. */
export function useConfirm(): ConfirmFn {
const fn = useContext(ConfirmContext);
if (!fn) throw new Error("useConfirm must be used within a ConfirmProvider");
return fn;
}
export function ConfirmProvider({ children }: { children: ReactNode }) {
const [pending, setPending] = useState<{
opts: ConfirmOpts;
resolve: (v: boolean) => void;
} | null>(null);
const confirm = useCallback<ConfirmFn>(
(opts) => new Promise<boolean>((resolve) => setPending({ opts, resolve })),
[],
);
function settle(value: boolean) {
pending?.resolve(value);
setPending(null);
}
return (
<ConfirmContext.Provider value={confirm}>
{children}
<Modal
open={!!pending}
onClose={() => settle(false)}
title={pending?.opts.title ?? ""}
>
{pending ? (
<div className="space-y-5">
{pending.opts.message ? (
<p className="text-sm text-muted-foreground">
{pending.opts.message}
</p>
) : null}
<div className="flex justify-end gap-2">
<Button variant="ghost" onClick={() => settle(false)}>
Cancel
</Button>
<Button
variant={pending.opts.destructive ? "destructive" : "primary"}
onClick={() => settle(true)}
>
{pending.opts.confirmLabel ?? "Confirm"}
</Button>
</div>
</div>
) : null}
</Modal>
</ConfirmContext.Provider>
);
}