Spaces:
Sleeping
Sleeping
| // 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; | |