65 lines
1.8 KiB
TypeScript
65 lines
1.8 KiB
TypeScript
"use client";
|
||
|
||
import { type ReactNode, useEffect } from "react";
|
||
|
||
interface ModalProps {
|
||
open: boolean;
|
||
onClose: () => void;
|
||
title: string;
|
||
children: ReactNode;
|
||
wide?: boolean;
|
||
/**
|
||
* When true, X + Esc prompt for confirmation before closing.
|
||
* Backdrop clicks never close — user must use X.
|
||
*/
|
||
confirmClose?: boolean;
|
||
confirmMessage?: string;
|
||
}
|
||
|
||
export default function Modal({
|
||
open,
|
||
onClose,
|
||
title,
|
||
children,
|
||
wide,
|
||
confirmClose,
|
||
confirmMessage = "Discard changes? Any unsaved data will be lost.",
|
||
}: ModalProps) {
|
||
const requestClose = () => {
|
||
if (confirmClose && !window.confirm(confirmMessage)) return;
|
||
onClose();
|
||
};
|
||
|
||
useEffect(() => {
|
||
if (!open) return;
|
||
const onKey = (e: KeyboardEvent) => {
|
||
if (e.key === "Escape") requestClose();
|
||
};
|
||
window.addEventListener("keydown", onKey);
|
||
return () => window.removeEventListener("keydown", onKey);
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
}, [open, confirmClose]);
|
||
|
||
if (!open) return null;
|
||
return (
|
||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 p-4">
|
||
<div
|
||
onClick={(e) => e.stopPropagation()}
|
||
className={`bg-neutral-900 border border-neutral-700 rounded-lg shadow-2xl w-full ${wide ? "max-w-4xl" : "max-w-lg"} max-h-[90vh] overflow-y-auto`}
|
||
>
|
||
<div className="flex items-center justify-between px-6 py-4 border-b border-neutral-700">
|
||
<h2 className="text-lg font-semibold text-neutral-100">{title}</h2>
|
||
<button
|
||
onClick={requestClose}
|
||
className="text-neutral-400 hover:text-neutral-100 text-2xl leading-none"
|
||
aria-label="Close"
|
||
>
|
||
×
|
||
</button>
|
||
</div>
|
||
<div className="p-6">{children}</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|