Bundles the prior autonomous-session output that was sitting unstaged: - Em-dash sweep across src/ + tests/ (en-dash/em-dash to hyphen, ~2280 instances) - country-flag-icons rollout (CountryFlag component, replaces emoji glyphs that never rendered on Windows; lazy-loads the 3x2 SVG index as a single chunk after the per-subpath dynamic-import approach silently failed in webpack) - Admin IA Phase 1+2: 7-domain regroup, 41 to 38 pages, /admin/berths index, redirects (ocr to ai, reports to dashboard, invitations to users), docs/admin-ia-proposal.md - Per-template email tester (registry + endpoint + UI on Email admin page) - Cancel-document mode picker (delete-from-Documenso vs keep-for-audit) - Dashboard PDF report: 25 widgets, SVG charts, date-range picker, 11 resolvers - Customize-widgets per-region sortables at xl+ (charts/rails/feed); single flat sortable below xl when the layout stacks; per-viewport saved orders - Audit doc updates capturing each shipped item - Lint fixes: react-compiler immutability in DonutChart (reduce instead of let-reassign), set-state-in-effect disables in CountryFlag and UploadForSigning preview-bytes effect, unused 'confirm' destructures in interest contract + reservation tabs, unescaped apostrophe in test-template card copy
118 lines
3.6 KiB
TypeScript
118 lines
3.6 KiB
TypeScript
'use client';
|
|
|
|
import { useCallback, useRef, useState } from 'react';
|
|
|
|
import {
|
|
AlertDialog,
|
|
AlertDialogAction,
|
|
AlertDialogCancel,
|
|
AlertDialogContent,
|
|
AlertDialogDescription,
|
|
AlertDialogFooter,
|
|
AlertDialogHeader,
|
|
AlertDialogTitle,
|
|
} from '@/components/ui/alert-dialog';
|
|
import { cn } from '@/lib/utils';
|
|
|
|
interface ConfirmOptions {
|
|
title: string;
|
|
description: string;
|
|
/** Confirm button label. Default: "Delete". */
|
|
confirmLabel?: string;
|
|
/** Cancel button label. Default: "Cancel". */
|
|
cancelLabel?: string;
|
|
/** When true, confirm button is rendered in destructive red. Default: true. */
|
|
destructive?: boolean;
|
|
}
|
|
|
|
/**
|
|
* Programmatic, awaitable confirmation dialog - the imperative counterpart
|
|
* to `<ConfirmationDialog>` (which needs a trigger element).
|
|
*
|
|
* Replaces the native `window.confirm()` pattern in 17 destructive flows
|
|
* the audit's UI-UX pass flagged. `confirm(...)` is synchronous and
|
|
* browser-styled (escape-hatch UX); this hook is async, accessible
|
|
* (alert-dialog semantics + focus trap), keyboard-navigable, and matches
|
|
* the rest of the app's visual language.
|
|
*
|
|
* Usage:
|
|
* const { confirm, dialog } = useConfirmation();
|
|
*
|
|
* async function handleDelete(file) {
|
|
* const ok = await confirm({
|
|
* title: 'Delete file',
|
|
* description: `Delete "${file.filename}"? This cannot be undone.`,
|
|
* confirmLabel: 'Delete',
|
|
* });
|
|
* if (!ok) return;
|
|
* // ... actually delete
|
|
* }
|
|
*
|
|
* return <>{children}{dialog}</>;
|
|
*
|
|
* The dialog renders into the component's tree once, and `confirm()`
|
|
* resolves with the user's choice. Multiple sequential `confirm()`
|
|
* calls are safe - each gets its own promise.
|
|
*/
|
|
export function useConfirmation() {
|
|
const [state, setState] = useState<(ConfirmOptions & { open: boolean }) | null>(null);
|
|
const resolverRef = useRef<((ok: boolean) => void) | null>(null);
|
|
|
|
const confirm = useCallback((opts: ConfirmOptions): Promise<boolean> => {
|
|
// If a previous confirm is somehow still open, resolve it as a cancel
|
|
// before starting the next. Defensive against rapid double-fires.
|
|
if (resolverRef.current) {
|
|
resolverRef.current(false);
|
|
resolverRef.current = null;
|
|
}
|
|
setState({ ...opts, open: true });
|
|
return new Promise<boolean>((resolve) => {
|
|
resolverRef.current = resolve;
|
|
});
|
|
}, []);
|
|
|
|
const handleConfirm = useCallback(() => {
|
|
resolverRef.current?.(true);
|
|
resolverRef.current = null;
|
|
setState((prev) => (prev ? { ...prev, open: false } : null));
|
|
}, []);
|
|
|
|
const handleCancel = useCallback(() => {
|
|
resolverRef.current?.(false);
|
|
resolverRef.current = null;
|
|
setState((prev) => (prev ? { ...prev, open: false } : null));
|
|
}, []);
|
|
|
|
const dialog = state ? (
|
|
<AlertDialog
|
|
open={state.open}
|
|
onOpenChange={(open) => {
|
|
if (!open) handleCancel();
|
|
}}
|
|
>
|
|
<AlertDialogContent>
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle>{state.title}</AlertDialogTitle>
|
|
<AlertDialogDescription>{state.description}</AlertDialogDescription>
|
|
</AlertDialogHeader>
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel onClick={handleCancel}>
|
|
{state.cancelLabel ?? 'Cancel'}
|
|
</AlertDialogCancel>
|
|
<AlertDialogAction
|
|
onClick={handleConfirm}
|
|
className={cn(
|
|
state.destructive !== false &&
|
|
'bg-destructive text-destructive-foreground hover:bg-destructive/90',
|
|
)}
|
|
>
|
|
{state.confirmLabel ?? 'Delete'}
|
|
</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
) : null;
|
|
|
|
return { confirm, dialog };
|
|
}
|