siftlode/frontend/src/components/ConfirmProvider.tsx
npeter83 48cb11434d feat(ui): reusable app-styled confirm dialog, replacing window.confirm
Add a promise-based ConfirmProvider + useConfirm() hook (built on the Modal shell,
liquid-glass styling, danger variant, Esc/backdrop = cancel, Enter = confirm) so
confirmations match the app instead of the native browser dialog. Wire it in at the
app root and replace both window.confirm call sites (playlist delete, channel
unsubscribe). Trilingual common.confirm/confirmTitle strings.
2026-06-15 15:06:47 +02:00

74 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>
);
}