This commit is contained in:
2026-01-23 20:35:40 +03:00
parent cf3b0e48ec
commit 66c151653e
137 changed files with 41495 additions and 0 deletions

220
app/components/ui/Modal.tsx Normal file
View File

@@ -0,0 +1,220 @@
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>
);
}