frontend_nyla/src/hooks/useConfirm.tsx
2026-04-21 23:49:50 +02:00

142 lines
4.5 KiB
TypeScript

/**
* useConfirm — application-level confirm dialog replacing native browser confirm().
*
* Usage:
* const { confirm, ConfirmDialog } = useConfirm();
* const ok = await confirm('Wirklich löschen?', { confirmLabel: 'Löschen', variant: 'danger' });
* // Render <ConfirmDialog /> once in the component tree.
*/
import React, { useState, useCallback, useRef, useMemo } from 'react';
import { useLanguage } from '../providers/language/LanguageContext';
export interface ConfirmOptions {
title?: string;
confirmLabel?: string;
cancelLabel?: string;
variant?: 'primary' | 'danger';
}
interface ConfirmState {
message: string;
options: Required<ConfirmOptions>;
resolve: (value: boolean) => void;
}
export function useConfirm() {
const { t } = useLanguage();
const [state, setState] = useState<ConfirmState | null>(null);
const resolveRef = useRef<((v: boolean) => void) | null>(null);
const _defaults: Required<ConfirmOptions> = useMemo(() => ({
title: t('Bestätigung'),
confirmLabel: t('Bestätigen'),
cancelLabel: t('Abbrechen'),
variant: 'primary' as const,
}), [t]);
const confirm = useCallback((message: string, options?: ConfirmOptions): Promise<boolean> => {
return new Promise<boolean>((resolve) => {
resolveRef.current = resolve;
setState({
message,
options: { ..._defaults, ...options },
resolve,
});
});
}, [_defaults]);
const _handleConfirm = useCallback(() => {
resolveRef.current?.(true);
resolveRef.current = null;
setState(null);
}, []);
const _handleCancel = useCallback(() => {
resolveRef.current?.(false);
resolveRef.current = null;
setState(null);
}, []);
const ConfirmDialog: React.FC = useCallback(() => {
if (!state) return null;
const { message, options } = state;
const isDanger = options.variant === 'danger';
return (
<div
// Backdrop intentionally has NO onClick handler: this confirm dialog
// must only close via the explicit Cancel/Confirm buttons or Escape.
// Accidental outside-clicks should NOT dismiss a decision the user
// hasn't made yet. (UX policy for all modal dialogs in PORTA.)
style={{
position: 'fixed', inset: 0, zIndex: 10000,
background: 'rgba(0,0,0,0.55)', backdropFilter: 'blur(2px)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
}}
onKeyDown={(e) => {
if (e.key === 'Escape') _handleCancel();
}}
tabIndex={-1}
>
<div
onClick={(e) => e.stopPropagation()}
style={{
background: 'var(--surface-color, #1a1a2e)',
border: '1px solid var(--color-border, #333)',
borderRadius: '12px',
padding: '1.5rem',
minWidth: 340, maxWidth: 480,
boxShadow: '0 8px 32px rgba(0,0,0,0.4)',
display: 'flex', flexDirection: 'column', gap: '1.25rem',
}}
>
<h3 style={{
margin: 0, fontSize: '1.05rem', fontWeight: 600,
color: 'var(--text-primary, #e0e0e0)',
}}>
{options.title}
</h3>
<p style={{
margin: 0, fontSize: '0.9rem', lineHeight: 1.5,
color: 'var(--text-secondary, #999)',
}}>
{message}
</p>
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '0.75rem' }}>
<button
onClick={_handleCancel}
style={{
padding: '8px 18px', borderRadius: '6px', fontSize: '0.875rem', fontWeight: 500,
border: '1px solid var(--color-border, #444)',
background: 'transparent',
color: 'var(--text-primary, #e8e8e8)',
cursor: 'pointer',
}}
>
{options.cancelLabel}
</button>
<button
onClick={_handleConfirm}
autoFocus
style={{
padding: '8px 18px', borderRadius: '6px', fontSize: '0.875rem', fontWeight: 600,
border: 'none',
background: isDanger ? '#ef4444' : 'var(--primary-color, #F25843)',
color: '#fff',
cursor: 'pointer',
}}
>
{options.confirmLabel}
</button>
</div>
</div>
</div>
);
}, [state, _handleConfirm, _handleCancel]);
return { confirm, ConfirmDialog };
}