220 lines
6.5 KiB
TypeScript
220 lines
6.5 KiB
TypeScript
import { ReactNode, useEffect, useState } from 'react';
|
||
import { createPortal } from 'react-dom';
|
||
import { Button } from './Button';
|
||
import { Text } from './Text';
|
||
import { defaultLayoutConfig, type LayoutConfig } from '~/lib/layout-utils';
|
||
|
||
interface ModalProps {
|
||
isOpen: boolean;
|
||
onClose: () => void;
|
||
title: string;
|
||
children: ReactNode;
|
||
size?: 'sm' | 'md' | 'lg' | 'xl';
|
||
className?: string;
|
||
config?: Partial<LayoutConfig>;
|
||
showCloseButton?: boolean;
|
||
}
|
||
|
||
export function Modal({
|
||
isOpen,
|
||
onClose,
|
||
title,
|
||
children,
|
||
size = 'md',
|
||
className = '',
|
||
config = {},
|
||
showCloseButton = true,
|
||
}: ModalProps) {
|
||
const layoutConfig = { ...defaultLayoutConfig, ...config };
|
||
const [isMounted, setIsMounted] = useState(false);
|
||
|
||
useEffect(() => {
|
||
setIsMounted(true);
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
if (isOpen) {
|
||
document.body.style.overflow = 'hidden';
|
||
} else {
|
||
document.body.style.overflow = 'unset';
|
||
}
|
||
|
||
return () => {
|
||
document.body.style.overflow = 'unset';
|
||
};
|
||
}, [isOpen]);
|
||
|
||
useEffect(() => {
|
||
const handleEscape = (e: KeyboardEvent) => {
|
||
if (e.key === 'Escape') {
|
||
onClose();
|
||
}
|
||
};
|
||
|
||
if (isOpen) {
|
||
document.addEventListener('keydown', handleEscape);
|
||
}
|
||
|
||
return () => {
|
||
document.removeEventListener('keydown', handleEscape);
|
||
};
|
||
}, [isOpen, onClose]);
|
||
|
||
if (!isOpen || !isMounted) return null;
|
||
|
||
const sizeClasses = {
|
||
sm: 'max-w-md',
|
||
md: 'max-w-lg',
|
||
lg: 'max-w-2xl',
|
||
xl: 'max-w-4xl',
|
||
};
|
||
|
||
const modalContent = (
|
||
<div className="fixed inset-0 z-50 overflow-y-auto" dir={layoutConfig.direction}>
|
||
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
|
||
{/* Backdrop */}
|
||
<div
|
||
className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"
|
||
onClick={onClose}
|
||
/>
|
||
|
||
{/* Modal panel */}
|
||
<div
|
||
className={`relative transform overflow-hidden rounded-lg bg-white text-right shadow-xl transition-all sm:my-8 sm:w-full ${sizeClasses[size]} ${className}`}
|
||
>
|
||
{/* Header */}
|
||
<div className="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
||
<div className="flex items-center justify-between mb-4">
|
||
<Text as="h3" size="lg" weight="medium">
|
||
{title}
|
||
</Text>
|
||
{showCloseButton && (
|
||
<button
|
||
onClick={onClose}
|
||
className="rounded-md bg-white text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
|
||
>
|
||
<span className="sr-only">إغلاق</span>
|
||
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||
</svg>
|
||
</button>
|
||
)}
|
||
</div>
|
||
|
||
{/* Content */}
|
||
<div>
|
||
{children}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
|
||
return createPortal(modalContent, document.body);
|
||
}
|
||
|
||
interface ConfirmModalProps {
|
||
isOpen: boolean;
|
||
onClose: () => void;
|
||
onConfirm: () => void;
|
||
title: string;
|
||
message: string;
|
||
confirmText?: string;
|
||
cancelText?: string;
|
||
variant?: 'danger' | 'warning' | 'info';
|
||
config?: Partial<LayoutConfig>;
|
||
}
|
||
|
||
export function ConfirmModal({
|
||
isOpen,
|
||
onClose,
|
||
onConfirm,
|
||
title,
|
||
message,
|
||
confirmText = "تأكيد",
|
||
cancelText = "إلغاء",
|
||
variant = 'info',
|
||
config = {},
|
||
}: ConfirmModalProps) {
|
||
const layoutConfig = { ...defaultLayoutConfig, ...config };
|
||
const [isMounted, setIsMounted] = useState(false);
|
||
|
||
useEffect(() => {
|
||
setIsMounted(true);
|
||
}, []);
|
||
|
||
const handleConfirm = () => {
|
||
onConfirm();
|
||
onClose();
|
||
};
|
||
|
||
const getIcon = () => {
|
||
switch (variant) {
|
||
case 'danger':
|
||
return (
|
||
<div className="mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10">
|
||
<svg className="h-6 w-6 text-red-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z" />
|
||
</svg>
|
||
</div>
|
||
);
|
||
case 'warning':
|
||
return (
|
||
<div className="mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-yellow-100 sm:mx-0 sm:h-10 sm:w-10">
|
||
<svg className="h-6 w-6 text-yellow-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z" />
|
||
</svg>
|
||
</div>
|
||
);
|
||
default:
|
||
return (
|
||
<div className="mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-blue-100 sm:mx-0 sm:h-10 sm:w-10">
|
||
<svg className="h-6 w-6 text-blue-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||
</svg>
|
||
</div>
|
||
);
|
||
}
|
||
};
|
||
|
||
return (
|
||
<Modal
|
||
isOpen={isOpen}
|
||
onClose={onClose}
|
||
title=""
|
||
size="sm"
|
||
showCloseButton={false}
|
||
config={config}
|
||
>
|
||
<div className="sm:flex sm:items-start" dir={layoutConfig.direction}>
|
||
{getIcon()}
|
||
<div className="mt-3 text-center sm:mt-0 sm:mr-4 sm:text-right">
|
||
<Text as="h3" size="lg" weight="medium" className="mb-2">
|
||
{title}
|
||
</Text>
|
||
<Text color="secondary" className="mb-4">
|
||
{message}
|
||
</Text>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="mt-5 sm:mt-4 sm:flex w-full sm:flex-row space-x-2">
|
||
<Button
|
||
onClick={handleConfirm}
|
||
variant={variant === 'danger' ? 'danger' : 'primary'}
|
||
className="w-full sm:w-auto sm:ml-3"
|
||
>
|
||
{confirmText}
|
||
</Button>
|
||
<Button
|
||
onClick={onClose}
|
||
variant="outline"
|
||
className="mt-3 w-full sm:mt-0 sm:w-auto"
|
||
>
|
||
{cancelText}
|
||
</Button>
|
||
</div>
|
||
</Modal>
|
||
);
|
||
} |