diff --git a/src/components/ui/Modal.tsx b/src/components/ui/Modal.tsx new file mode 100644 index 0000000..b389e6f --- /dev/null +++ b/src/components/ui/Modal.tsx @@ -0,0 +1,108 @@ +'use client' + +import { ReactNode, useEffect, useId, useRef, useState } from 'react' +import { createPortal } from 'react-dom' +import { X } from 'lucide-react' +import { cn } from '@/lib/utils' + +interface ModalProps { + open: boolean + onClose: () => void + title?: string + description?: string + children?: ReactNode + className?: string +} + +export default function Modal({ + open, + onClose, + title, + description, + children, + className +}: ModalProps) { + const [mounted, setMounted] = useState(false) + const closeButtonRef = useRef(null) + const titleId = useId() + const descriptionId = useId() + + useEffect(() => { + setMounted(true) + }, []) + + useEffect(() => { + if (!open) return + + const previousOverflow = document.body.style.overflow + document.body.style.overflow = 'hidden' + + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Escape') { + onClose() + } + } + + document.addEventListener('keydown', handleKeyDown) + return () => { + document.body.style.overflow = previousOverflow + document.removeEventListener('keydown', handleKeyDown) + } + }, [open, onClose]) + + useEffect(() => { + if (open) { + closeButtonRef.current?.focus() + } + }, [open]) + + if (!mounted || !open) { + return null + } + + return createPortal( +
{ + if (event.target === event.currentTarget) { + onClose() + } + }} + > +
+
+ {title ? ( +
+

+ {title} +

+ {description ? ( +

{description}

+ ) : null} +
+ ) : ( + + )} + +
+
{children}
+
+
, + document.body + ) +} \ No newline at end of file