209 lines
6.5 KiB
TypeScript
209 lines
6.5 KiB
TypeScript
import React, { useState } from 'react';
|
|
|
|
// Types for notification modal
|
|
export interface NotificationModalProps {
|
|
isOpen: boolean;
|
|
title?: string;
|
|
message: string;
|
|
onConfirm?: () => void;
|
|
onCancel?: () => void;
|
|
type?: 'info' | 'success' | 'warning' | 'error';
|
|
confirmButtonText?: string;
|
|
cancelButtonText?: string;
|
|
}
|
|
|
|
// Icon components for different types
|
|
const Icons = {
|
|
info: (props: React.SVGProps<SVGSVGElement>) => (
|
|
<svg {...props} viewBox="0 0 24 24" fill="none">
|
|
<circle cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="2" />
|
|
<path d="M12 16v-4" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
|
|
<path d="M12 8h.01" stroke="currentColor" strokeWidth="3" strokeLinecap="round" />
|
|
</svg>
|
|
),
|
|
success: (props: React.SVGProps<SVGSVGElement>) => (
|
|
<svg {...props} viewBox="0 0 24 24" fill="none">
|
|
<circle cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="2" />
|
|
<path d="M8 12l2.5 2.5L15.5 9" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
|
</svg>
|
|
),
|
|
warning: (props: React.SVGProps<SVGSVGElement>) => (
|
|
<svg {...props} viewBox="0 0 24 24" fill="none">
|
|
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" stroke="currentColor" strokeWidth="2" />
|
|
<line x1="12" y1="9" x2="12" y2="13" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
|
|
<circle cx="12" cy="17" r="1" fill="currentColor" />
|
|
</svg>
|
|
),
|
|
error: (props: React.SVGProps<SVGSVGElement>) => (
|
|
<svg {...props} viewBox="0 0 24 24" fill="none">
|
|
<circle cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="2" />
|
|
<line x1="15" y1="9" x2="9" y2="15" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
|
|
<line x1="9" y1="9" x2="15" y2="15" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
|
|
</svg>
|
|
),
|
|
};
|
|
|
|
// Default props
|
|
const defaultProps: Partial<NotificationModalProps> = {
|
|
title: 'Thông báo',
|
|
type: 'info',
|
|
confirmButtonText: 'OK',
|
|
cancelButtonText: 'Hủy',
|
|
};
|
|
|
|
export const NotificationModal: React.FC<NotificationModalProps> = ({
|
|
isOpen,
|
|
title = defaultProps.title,
|
|
message,
|
|
onConfirm,
|
|
onCancel,
|
|
type = defaultProps.type,
|
|
confirmButtonText = defaultProps.confirmButtonText,
|
|
cancelButtonText = defaultProps.cancelButtonText,
|
|
}) => {
|
|
const [isAnimating, setIsAnimating] = useState(false);
|
|
|
|
// Get colors based on type
|
|
const getTypeStyles = () => {
|
|
switch (type) {
|
|
case 'success':
|
|
return { bg: '#d4edda', border: '#c3e6cb', text: '#155724' };
|
|
case 'warning':
|
|
return { bg: '#fff3cd', border: '#ffeeba', text: '#856404' };
|
|
case 'error':
|
|
return { bg: '#f8d7da', border: '#f5c6cb', text: '#721c24' };
|
|
default:
|
|
return { bg: '#e2e3e5', border: '#d6d8db', text: '#383d41' };
|
|
}
|
|
};
|
|
|
|
const styles = getTypeStyles();
|
|
|
|
// Animation classes based on state
|
|
const getAnimationClass = () => {
|
|
if (!isOpen) return 'opacity-0 translate-y-4';
|
|
if (isAnimating && onCancel) return 'animate-fade-out';
|
|
return 'animate-fade-in';
|
|
};
|
|
|
|
// Handle confirm click
|
|
const handleConfirm = () => {
|
|
setIsAnimating(true);
|
|
onConfirm?.();
|
|
setTimeout(() => setIsAnimating(false), 300);
|
|
};
|
|
|
|
// Handle cancel click
|
|
const handleCancel = () => {
|
|
setIsAnimating(true);
|
|
onCancel?.();
|
|
setTimeout(() => setIsAnimating(false), 300);
|
|
};
|
|
|
|
if (!isOpen) return null;
|
|
|
|
return (
|
|
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-[100] p-4">
|
|
<div
|
|
className={`bg-white rounded-lg shadow-xl max-w-md w-full transform transition-all duration-300 ${getAnimationClass()}`}
|
|
role="alertdialog"
|
|
aria-modal="true"
|
|
aria-labelledby="modal-title"
|
|
aria-describedby="modal-message"
|
|
>
|
|
{/* Header */}
|
|
<div className={`p-6 border-b ${styles.border}`}>
|
|
<div className="flex items-center gap-3">
|
|
{type === 'success' && <Icons.success className="w-5 h-5 text-green-600" />}
|
|
{type === 'warning' && <Icons.warning className="w-5 h-5 text-yellow-600" />}
|
|
{type === 'error' && <Icons.error className="w-5 h-5 text-red-600" />}
|
|
{type === 'info' && <Icons.info className="w-5 h-5 text-blue-600" />}
|
|
<h2 id="modal-title" className={`text-xl font-semibold ${styles.text}`}>
|
|
{title}
|
|
</h2>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Body */}
|
|
<div className="p-6">
|
|
<p id="modal-message" className="text-gray-700 leading-relaxed">{message}</p>
|
|
</div>
|
|
|
|
{/* Footer */}
|
|
<div className={`px-6 py-4 flex justify-end gap-3 border-t ${styles.border}`}>
|
|
{onCancel && (
|
|
<button
|
|
onClick={handleCancel}
|
|
className="px-4 py-2 text-gray-700 hover:bg-gray-100 rounded-md transition-colors"
|
|
>
|
|
{cancelButtonText}
|
|
</button>
|
|
)}
|
|
{onConfirm && (
|
|
<button
|
|
onClick={handleConfirm}
|
|
className={`px-4 py-2 text-white rounded-md font-medium transition-colors ${
|
|
type === 'error'
|
|
? 'bg-red-600 hover:bg-red-700'
|
|
: 'bg-blue-600 hover:bg-blue-700'
|
|
}`}
|
|
>
|
|
{confirmButtonText}
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
// Hook for easy usage without props management
|
|
export const useNotificationModal = () => {
|
|
const [modalState, setModalState] = useState<{
|
|
isOpen: boolean;
|
|
title?: string;
|
|
message: string;
|
|
type?: 'info' | 'success' | 'warning' | 'error';
|
|
onConfirm?: () => void;
|
|
onCancel?: () => void;
|
|
} | null>(null);
|
|
|
|
const openModal = (
|
|
title: string,
|
|
message: string,
|
|
type: 'info' | 'success' | 'warning' | 'error' = 'info',
|
|
onConfirm?: () => void,
|
|
onCancel?: () => void,
|
|
) => {
|
|
setModalState({
|
|
isOpen: true,
|
|
title,
|
|
message,
|
|
type,
|
|
onConfirm,
|
|
onCancel,
|
|
});
|
|
|
|
// Auto-close after 5 seconds if no confirm action
|
|
const timer = setTimeout(() => {
|
|
if (onCancel) {
|
|
setModalState((prev) => ({ ...prev, isOpen: false }));
|
|
}
|
|
}, 5000);
|
|
|
|
return () => clearTimeout(timer);
|
|
};
|
|
|
|
const closeModal = () => {
|
|
setModalState((prev) => prev ? { ...prev, isOpen: false } : null);
|
|
};
|
|
|
|
return {
|
|
modalState,
|
|
openModal,
|
|
closeModal,
|
|
};
|
|
};
|
|
|
|
export default NotificationModal;
|