Source

components/ui/Modal.jsx

import React, { useEffect } from 'react';

/**
 * A reusable Modal component.
 *
 * @param {object} props - The component props.
 * @param {boolean} props.isOpen - Determines if the modal is currently open.
 * @param {function} props.onClose - Function to call when the modal should be closed.
 * @param {string} props.title - The title to display at the top of the modal.
 * @param {React.ReactNode} props.children - The content to be displayed within the modal body.
 * @param {'sm' | 'md' | 'lg' | 'xl'} [props.size='md'] - Controls the maximum width of the modal.
 * @returns {JSX.Element | null} The Modal component or null if not open.
 */
const Modal = ({ isOpen, onClose, title, children, size = 'md' }) => {
  /**
 * Handles side effects when the modal's open state changes.
 * - Adds/removes event listener for ESC key to close the modal.
 * - Prevents/restores body scrolling when the modal is open/closed.
 */
  useEffect(() => {
    const handleEsc = (event) => {
      if (event.keyCode === 27) onClose();
    };
    
    if (isOpen) {
      document.addEventListener('keydown', handleEsc);
      // Prevent body scroll
      document.body.style.overflow = 'hidden';
    }
    
    return () => {
      document.removeEventListener('keydown', handleEsc);
      document.body.style.overflow = 'unset';
    };
  }, [isOpen, onClose]);

  if (!isOpen) return null;
  /** Defines Tailwind CSS classes for different modal sizes. */

  const sizeClasses = {
    sm: 'max-w-sm',
    md: 'max-w-md',
    lg: 'max-w-2xl',
    xl: 'max-w-4xl',
  };

  return (
    <div className="fixed inset-0 z-50 flex items-center justify-center p-4">
      {/* Background overlay with blur/dim effect */}
      <div
        className="fixed inset-0 bg-black/50 backdrop-blur-sm"
        onClick={onClose}
      ></div>

      {/* Modal box */}
      <div
        className={`z-50 bg-white rounded-xl shadow-2xl p-6 w-full ${sizeClasses[size]} relative max-h-[90vh] overflow-y-auto`}
        onClick={(e) => e.stopPropagation()} // Prevent closing when clicking inside modal
      >
        <div className="flex items-center justify-between mb-4">
          <h2 className="text-xl font-bold text-gray-800">{title}</h2>
          <button
            onClick={onClose}
            className="text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-full w-8 h-8 flex items-center justify-center text-2xl font-bold transition-colors"
            aria-label="Close modal"
          >
            &times;
          </button>
        </div>
        <div>{children}</div>
      </div>
    </div>
  );
};

export default Modal;