import { Icon } from "./components/Icon"; // In-app dialogs and toasts instead of the browser's alert/confirm/prompt. import { useEffect, useRef, useState, useSyncExternalStore } from "react"; import { t } from "./i18n"; interface ConfirmOpts { title?: string; message: string; confirmLabel?: string; /** Destructive action: red button. */ danger?: boolean; } interface PromptOpts { title?: string; message: string; defaultValue?: string; placeholder?: string; password?: boolean; confirmLabel?: string; } type Dialog = | ({ kind: "confirm"; id: number; resolve: (v: boolean) => void } & ConfirmOpts) | ({ kind: "prompt"; id: number; resolve: (v: string | null) => void } & PromptOpts); interface Toast { id: number; message: string; kind: "error" | "info" | "success"; } let dialogs: Dialog[] = []; let toasts: Toast[] = []; let seq = 0; const listeners = new Set<() => void>(); const emit = () => listeners.forEach((l) => l()); const subscribe = (l: () => void) => (listeners.add(l), () => listeners.delete(l)); export function confirmDialog(opts: ConfirmOpts | string): Promise { const o = typeof opts === "string" ? { message: opts } : opts; return new Promise((resolve) => { dialogs = [...dialogs, { kind: "confirm", id: ++seq, resolve, ...o }]; emit(); }); } export function promptDialog(opts: PromptOpts | string): Promise { const o = typeof opts === "string" ? { message: opts } : opts; return new Promise((resolve) => { dialogs = [...dialogs, { kind: "prompt", id: ++seq, resolve, ...o }]; emit(); }); } export function toast(message: unknown, kind: Toast["kind"] = "error") { const text = message instanceof Error ? message.message : typeof message === "string" ? message : String((message as any)?.message ?? message); const id = ++seq; toasts = [...toasts, { id, message: text, kind }]; emit(); setTimeout(() => { toasts = toasts.filter((x) => x.id !== id); emit(); }, kind === "error" ? 7000 : 3500); } function close(d: Dialog, value: any) { dialogs = dialogs.filter((x) => x.id !== d.id); emit(); (d.resolve as (v: any) => void)(value); } function DialogView({ d }: { d: Dialog }) { const [value, setValue] = useState(d.kind === "prompt" ? d.defaultValue ?? "" : ""); const input = useRef(null); const ok = useRef(null); const cancelValue = d.kind === "confirm" ? false : null; useEffect(() => { if (d.kind === "prompt") { input.current?.focus(); input.current?.select(); } else ok.current?.focus(); const h = (e: KeyboardEvent) => e.key === "Escape" && close(d, cancelValue); window.addEventListener("keydown", h); return () => window.removeEventListener("keydown", h); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const submit = () => close(d, d.kind === "confirm" ? true : value); return (
close(d, cancelValue)}>
e.stopPropagation()} onSubmit={(e) => (e.preventDefault(), submit())} > {d.title &&

{d.title}

}

{d.message}

{d.kind === "prompt" && ( setValue(e.target.value)} autoComplete={d.password ? "new-password" : "off"} style={{ width: "100%" }} /> )}
); } /** Renders open dialogs and toasts; mounted once in App. */ export function DialogHost() { const state = useSyncExternalStore(subscribe, () => dialogs); const ts = useSyncExternalStore(subscribe, () => toasts); return ( <> {state.map((d) => ( ))}
{ts.map((x) => (
((toasts = toasts.filter((y) => y.id !== x.id)), emit())}> {x.kind === "error" ? : x.kind === "success" ? : null} {x.message}
))}
); }