pratikfrontend / components /ConfirmationDialog.tsx
Antaram's picture
Upload 61 files
24c454f verified
Raw
History Blame Contribute Delete
8 kB
// Confirmation Dialog Component
// Replaces browser confirm() with proper UI dialogs for destructive actions
import React from 'react';
import { AlertTriangle, X } from 'lucide-react';
// ============================================
// TYPES
// ============================================
export type ConfirmationVariant = 'danger' | 'warning' | 'info';
export interface ConfirmationDialogProps {
isOpen: boolean;
onClose: () => void;
onConfirm: () => void;
title: string;
message?: string;
confirmText?: string;
cancelText?: string;
variant?: ConfirmationVariant;
isLoading?: boolean;
}
// ============================================
// COMPONENT
// ============================================
export const ConfirmationDialog = ({
isOpen,
onClose,
onConfirm,
title,
message,
confirmText = 'Confirm',
cancelText = 'Cancel',
variant = 'danger',
isLoading = false
}: ConfirmationDialogProps) => {
if (!isOpen) return null;
const variantConfig = getVariantConfig(variant);
return (
<div className="fixed inset-0 z-[9998] flex items-center justify-center p-4">
{/* Backdrop */}
<div
className="absolute inset-0 bg-black/50 transition-opacity"
onClick={onClose}
/>
{/* Dialog */}
<div
className="relative bg-white rounded-xl shadow-2xl max-w-md w-full overflow-hidden animate-scale-in"
role="dialog"
aria-modal="true"
aria-labelledby="dialog-title"
>
{/* Close button */}
<button
onClick={onClose}
className="absolute top-3 right-3 p-1.5 rounded-lg hover:bg-gray-100 transition-colors text-gray-400 hover:text-gray-600"
disabled={isLoading}
>
<X size={18} />
</button>
{/* Content */}
<div className="p-6">
{/* Icon */}
<div className={`mx-auto w-12 h-12 rounded-full flex items-center justify-center mb-4 ${variantConfig.bgClass}`}>
<AlertTriangle className={variantConfig.iconClass} size={24} />
</div>
{/* Title */}
<h3
id="dialog-title"
className={`text-lg font-semibold text-center mb-2 ${variantConfig.titleClass}`}
>
{title}
</h3>
{/* Message */}
{message && (
<p className="text-sm text-gray-600 text-center mb-6">
{message}
</p>
)}
{/* Actions */}
<div className="flex gap-3">
<button
onClick={onClose}
disabled={isLoading}
className="flex-1 px-4 py-2.5 text-sm font-medium text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors disabled:opacity-50"
>
{cancelText}
</button>
<button
onClick={onConfirm}
disabled={isLoading}
className={`flex-1 px-4 py-2.5 text-sm font-medium text-white rounded-lg transition-colors disabled:opacity-50 flex items-center justify-center gap-2 ${variantConfig.buttonClass}`}
>
{isLoading && (
<svg className="animate-spin h-4 w-4" viewBox="0 0 24 24">
<circle
className="opacity-25"
cx="12" cy="12" r="10"
stroke="currentColor"
strokeWidth="4"
fill="none"
/>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
)}
{confirmText}
</button>
</div>
</div>
</div>
{/* Animation styles */}
<style>{`
@keyframes scaleIn {
from {
transform: scale(0.95);
opacity: 0;
}
to {
transform: scale(1);
opacity: 1;
}
}
.animate-scale-in {
animation: scaleIn 0.2s ease-out;
}
`}</style>
</div>
);
};
// ============================================
// VARIANT CONFIG
// ============================================
interface VariantConfig {
bgClass: string;
iconClass: string;
titleClass: string;
buttonClass: string;
}
const getVariantConfig = (variant: ConfirmationVariant): VariantConfig => {
const configs: Record<ConfirmationVariant, VariantConfig> = {
danger: {
bgClass: 'bg-red-100',
iconClass: 'text-red-500',
titleClass: 'text-red-600',
buttonClass: 'bg-red-500 hover:bg-red-600'
},
warning: {
bgClass: 'bg-amber-100',
iconClass: 'text-amber-500',
titleClass: 'text-amber-600',
buttonClass: 'bg-amber-500 hover:bg-amber-600'
},
info: {
bgClass: 'bg-blue-100',
iconClass: 'text-blue-500',
titleClass: 'text-blue-600',
buttonClass: 'bg-blue-500 hover:bg-blue-600'
}
};
return configs[variant];
};
// ============================================
// HOOK FOR EASIER USAGE
// ============================================
import { useState, useCallback } from 'react';
interface UseConfirmationOptions {
title: string;
message?: string;
confirmText?: string;
cancelText?: string;
variant?: ConfirmationVariant;
onConfirm: () => void | Promise<void>;
}
export const useConfirmation = () => {
const [isOpen, setIsOpen] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [options, setOptions] = useState<UseConfirmationOptions | null>(null);
const confirm = useCallback((opts: UseConfirmationOptions) => {
setOptions(opts);
setIsOpen(true);
}, []);
const handleClose = useCallback(() => {
setIsOpen(false);
setOptions(null);
}, []);
const handleConfirm = useCallback(async () => {
if (!options) return;
setIsLoading(true);
try {
await options.onConfirm();
handleClose();
} catch (error) {
console.error('Confirmation action failed:', error);
} finally {
setIsLoading(false);
}
}, [options, handleClose]);
const dialogProps = {
isOpen,
onClose: handleClose,
onConfirm: handleConfirm,
title: options?.title || '',
message: options?.message,
confirmText: options?.confirmText,
cancelText: options?.cancelText,
variant: options?.variant,
isLoading
};
return { confirm, dialogProps };
};
export default ConfirmationDialog;