Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 10s
Build & Deploy / 🧪 QA (push) Failing after 1m26s
Build & Deploy / 🏗️ Build (push) Failing after 3m19s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🩺 Health Check (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
83 lines
2.4 KiB
TypeScript
83 lines
2.4 KiB
TypeScript
"use client";
|
|
|
|
import * as React from "react";
|
|
import { createPortal } from "react-dom";
|
|
import { motion, AnimatePresence } from "framer-motion";
|
|
import { X } from "lucide-react";
|
|
import { cn } from "../utils/cn";
|
|
|
|
interface ModalProps {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
title: string;
|
|
children: React.ReactNode;
|
|
maxWidth?: string;
|
|
}
|
|
|
|
export function Modal({
|
|
isOpen,
|
|
onClose,
|
|
title,
|
|
children,
|
|
maxWidth = "max-w-lg",
|
|
}: ModalProps) {
|
|
const [mounted, setMounted] = React.useState(false);
|
|
|
|
React.useEffect(() => {
|
|
setMounted(true);
|
|
return () => setMounted(false);
|
|
}, []);
|
|
|
|
// Close on escape key
|
|
React.useEffect(() => {
|
|
const handleEsc = (e: KeyboardEvent) => {
|
|
if (e.key === "Escape") onClose();
|
|
};
|
|
window.addEventListener("keydown", handleEsc);
|
|
return () => window.removeEventListener("keydown", handleEsc);
|
|
}, [onClose]);
|
|
|
|
if (!mounted) return null;
|
|
|
|
return createPortal(
|
|
<AnimatePresence>
|
|
{isOpen && (
|
|
<>
|
|
<motion.div
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
exit={{ opacity: 0 }}
|
|
onClick={onClose}
|
|
className="fixed inset-0 bg-slate-900/60 backdrop-blur-md z-[9999]"
|
|
/>
|
|
<div className="fixed inset-0 flex items-center justify-center p-2 z-[10000] pointer-events-none">
|
|
<motion.div
|
|
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
|
animate={{ opacity: 1, scale: 1, y: 0 }}
|
|
exit={{ opacity: 0, scale: 0.95, y: 20 }}
|
|
className={cn(
|
|
"bg-white w-full rounded-[2rem] shadow-2xl pointer-events-auto overflow-hidden border border-slate-100",
|
|
maxWidth,
|
|
)}
|
|
>
|
|
<div className="px-6 md:px-8 border-b border-slate-50 flex items-center justify-between bg-slate-50/50">
|
|
<h3 className="text-lg md:text-xl font-bold text-slate-900 tracking-tight">
|
|
{title}
|
|
</h3>
|
|
<button
|
|
onClick={onClose}
|
|
className="p-1.5 hover:bg-slate-100 rounded-full transition-colors text-slate-400 hover:text-slate-900"
|
|
>
|
|
<X size={18} />
|
|
</button>
|
|
</div>
|
|
<div className="p-6 md:p-8">{children}</div>
|
|
</motion.div>
|
|
</div>
|
|
</>
|
|
)}
|
|
</AnimatePresence>,
|
|
document.body,
|
|
);
|
|
}
|