Files
travelplanning/components/NotificationModal.tsx
T

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;