Files
school_compare/nextjs-app/components/Modal.tsx

83 lines
2.1 KiB
TypeScript
Raw Normal View History

/**
* 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
);
}