car_mms/app/components/ui/Modal.tsx
2025-09-11 14:22:27 +03:00

220 lines
6.5 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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