72 lines
1.6 KiB
TypeScript
72 lines
1.6 KiB
TypeScript
import { useEffect } from 'react';
|
|
|
|
interface Props {
|
|
open: boolean;
|
|
title: string;
|
|
onClose: () => void;
|
|
children: React.ReactNode;
|
|
footer?: React.ReactNode;
|
|
}
|
|
|
|
export function Modal({ open, title, onClose, children, footer }: Props) {
|
|
useEffect(() => {
|
|
if (!open) return;
|
|
const onKey = (e: KeyboardEvent) => e.key === 'Escape' && onClose();
|
|
window.addEventListener('keydown', onKey);
|
|
return () => window.removeEventListener('keydown', onKey);
|
|
}, [open, onClose]);
|
|
|
|
if (!open) return null;
|
|
|
|
return (
|
|
<div className="modal-overlay" onClick={onClose}>
|
|
<div className="modal" onClick={(e) => e.stopPropagation()}>
|
|
<h3 className="modal-title">{title}</h3>
|
|
{children}
|
|
{footer && <div className="modal-footer">{footer}</div>}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/** Confirmation dialog with safe cancel default */
|
|
export function ConfirmDialog({
|
|
open,
|
|
title,
|
|
message,
|
|
confirmLabel = 'Confirm',
|
|
danger = false,
|
|
onConfirm,
|
|
onCancel,
|
|
}: {
|
|
open: boolean;
|
|
title: string;
|
|
message: string;
|
|
confirmLabel?: string;
|
|
danger?: boolean;
|
|
onConfirm: () => void;
|
|
onCancel: () => void;
|
|
}) {
|
|
return (
|
|
<Modal
|
|
open={open}
|
|
title={title}
|
|
onClose={onCancel}
|
|
footer={
|
|
<>
|
|
<button className="btn" onClick={onCancel}>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
className={danger ? 'btn btn-danger' : 'btn btn-primary'}
|
|
onClick={onConfirm}
|
|
>
|
|
{confirmLabel}
|
|
</button>
|
|
</>
|
|
}
|
|
>
|
|
<p>{message}</p>
|
|
</Modal>
|
|
);
|
|
}
|