83 lines
2.1 KiB
TypeScript
83 lines
2.1 KiB
TypeScript
|
|
/**
|
||
|
|
* Modal Component
|
||
|
|
* Reusable modal overlay with animations
|
||
|
|
*/
|
||
|
|
|
||
|
|
'use client';
|
||
|
|
|
||
|
|
import { useEffect, useCallback } from 'react';
|
||
|
|
import { createPortal } from 'react-dom';
|
||
|
|
import styles from './Modal.module.css';
|
||
|
|
|
||
|
|
interface ModalProps {
|
||
|
|
isOpen: boolean;
|
||
|
|
onClose: () => void;
|
||
|
|
children: React.ReactNode;
|
||
|
|
title?: string;
|
||
|
|
size?: 'small' | 'medium' | 'large';
|
||
|
|
}
|
||
|
|
|
||
|
|
export function Modal({ isOpen, onClose, children, title, size = 'medium' }: ModalProps) {
|
||
|
|
const handleEscape = useCallback((e: KeyboardEvent) => {
|
||
|
|
if (e.key === 'Escape') {
|
||
|
|
onClose();
|
||
|
|
}
|
||
|
|
}, [onClose]);
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
if (!isOpen) return;
|
||
|
|
|
||
|
|
// Add event listener
|
||
|
|
document.addEventListener('keydown', handleEscape);
|
||
|
|
|
||
|
|
// Prevent body scroll
|
||
|
|
document.body.style.overflow = 'hidden';
|
||
|
|
|
||
|
|
return () => {
|
||
|
|
document.removeEventListener('keydown', handleEscape);
|
||
|
|
document.body.style.overflow = 'unset';
|
||
|
|
};
|
||
|
|
}, [isOpen, handleEscape]);
|
||
|
|
|
||
|
|
if (!isOpen || typeof window === 'undefined') return null;
|
||
|
|
|
||
|
|
const handleOverlayClick = (e: React.MouseEvent) => {
|
||
|
|
if (e.target === e.currentTarget) {
|
||
|
|
onClose();
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
return createPortal(
|
||
|
|
<div className={styles.overlay} onClick={handleOverlayClick}>
|
||
|
|
<div className={`${styles.modal} ${styles[size]}`}>
|
||
|
|
<div className={styles.header}>
|
||
|
|
{title && <h2 className={styles.title}>{title}</h2>}
|
||
|
|
<button
|
||
|
|
className={styles.closeButton}
|
||
|
|
onClick={onClose}
|
||
|
|
aria-label="Close modal"
|
||
|
|
>
|
||
|
|
<svg
|
||
|
|
width="24"
|
||
|
|
height="24"
|
||
|
|
viewBox="0 0 24 24"
|
||
|
|
fill="none"
|
||
|
|
stroke="currentColor"
|
||
|
|
strokeWidth="2"
|
||
|
|
strokeLinecap="round"
|
||
|
|
strokeLinejoin="round"
|
||
|
|
>
|
||
|
|
<line x1="18" y1="6" x2="6" y2="18" />
|
||
|
|
<line x1="6" y1="6" x2="18" y2="18" />
|
||
|
|
</svg>
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
<div className={styles.content}>
|
||
|
|
{children}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>,
|
||
|
|
document.body
|
||
|
|
);
|
||
|
|
}
|