75 lines
2.3 KiB
TypeScript
75 lines
2.3 KiB
TypeScript
|
|
import { createContext, useCallback, useContext, useState, type ReactNode } from "react";
|
||
|
|
import { useTranslation } from "react-i18next";
|
||
|
|
import Modal from "./Modal";
|
||
|
|
|
||
|
|
// App-styled, promise-based replacement for window.confirm. Wrap the tree in
|
||
|
|
// <ConfirmProvider> once, then anywhere: const confirm = useConfirm();
|
||
|
|
// if (await confirm({ message: "…", danger: true })) { … }
|
||
|
|
export interface ConfirmOptions {
|
||
|
|
title?: string;
|
||
|
|
message: ReactNode;
|
||
|
|
confirmLabel?: string;
|
||
|
|
cancelLabel?: string;
|
||
|
|
danger?: boolean;
|
||
|
|
}
|
||
|
|
|
||
|
|
type ConfirmFn = (opts: ConfirmOptions) => Promise<boolean>;
|
||
|
|
|
||
|
|
const ConfirmContext = createContext<ConfirmFn>(() => Promise.resolve(false));
|
||
|
|
|
||
|
|
export function useConfirm(): ConfirmFn {
|
||
|
|
return useContext(ConfirmContext);
|
||
|
|
}
|
||
|
|
|
||
|
|
export function ConfirmProvider({ children }: { children: ReactNode }) {
|
||
|
|
const { t } = useTranslation();
|
||
|
|
const [state, setState] = useState<{
|
||
|
|
opts: ConfirmOptions;
|
||
|
|
resolve: (ok: boolean) => void;
|
||
|
|
} | null>(null);
|
||
|
|
|
||
|
|
const confirm = useCallback<ConfirmFn>(
|
||
|
|
(opts) => new Promise<boolean>((resolve) => setState({ opts, resolve })),
|
||
|
|
[]
|
||
|
|
);
|
||
|
|
|
||
|
|
function close(ok: boolean) {
|
||
|
|
state?.resolve(ok);
|
||
|
|
setState(null);
|
||
|
|
}
|
||
|
|
|
||
|
|
return (
|
||
|
|
<ConfirmContext.Provider value={confirm}>
|
||
|
|
{children}
|
||
|
|
{state && (
|
||
|
|
<Modal
|
||
|
|
title={state.opts.title ?? t("common.confirmTitle")}
|
||
|
|
onClose={() => close(false)}
|
||
|
|
maxWidth="max-w-sm"
|
||
|
|
>
|
||
|
|
<p className="text-sm text-muted leading-relaxed">{state.opts.message}</p>
|
||
|
|
<div className="flex justify-end gap-2 mt-5">
|
||
|
|
<button
|
||
|
|
onClick={() => close(false)}
|
||
|
|
className="px-4 py-2 rounded-lg text-sm border border-border text-muted hover:text-fg hover:bg-surface transition"
|
||
|
|
>
|
||
|
|
{state.opts.cancelLabel ?? t("common.cancel")}
|
||
|
|
</button>
|
||
|
|
<button
|
||
|
|
autoFocus
|
||
|
|
onClick={() => close(true)}
|
||
|
|
className={`px-4 py-2 rounded-lg text-sm font-semibold transition ${
|
||
|
|
state.opts.danger
|
||
|
|
? "bg-red-500 text-white hover:bg-red-600"
|
||
|
|
: "bg-accent text-accent-fg hover:opacity-90"
|
||
|
|
}`}
|
||
|
|
>
|
||
|
|
{state.opts.confirmLabel ?? t("common.confirm")}
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</Modal>
|
||
|
|
)}
|
||
|
|
</ConfirmContext.Provider>
|
||
|
|
);
|
||
|
|
}
|