ConfirmModal.tsx 3.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899
  1. import { useEffect } from 'react';
  2. import { useTranslation } from 'react-i18next';
  3. import { AlertTriangle, Loader2 } from 'lucide-react';
  4. import { Card, CardContent } from './Card';
  5. import { Button } from './Button';
  6. interface ConfirmModalProps {
  7. title: string;
  8. message: string;
  9. confirmText?: string;
  10. cancelText?: string;
  11. variant?: 'danger' | 'warning' | 'default';
  12. isLoading?: boolean;
  13. loadingText?: string;
  14. onConfirm: () => void;
  15. onCancel: () => void;
  16. }
  17. export function ConfirmModal({
  18. title,
  19. message,
  20. confirmText,
  21. cancelText,
  22. variant = 'default',
  23. isLoading = false,
  24. loadingText,
  25. onConfirm,
  26. onCancel,
  27. }: ConfirmModalProps) {
  28. const { t } = useTranslation();
  29. const resolvedConfirmText = confirmText ?? t('common.confirm');
  30. const resolvedCancelText = cancelText ?? t('common.cancel');
  31. const resolvedLoadingText = loadingText ?? t('common.loading');
  32. // Close on Escape key (but not while loading)
  33. useEffect(() => {
  34. const handleKeyDown = (e: KeyboardEvent) => {
  35. if (e.key === 'Escape' && !isLoading) onCancel();
  36. };
  37. window.addEventListener('keydown', handleKeyDown);
  38. return () => window.removeEventListener('keydown', handleKeyDown);
  39. }, [onCancel, isLoading]);
  40. const variantStyles = {
  41. danger: {
  42. icon: 'text-red-400',
  43. button: 'bg-red-500 hover:bg-red-600',
  44. },
  45. warning: {
  46. icon: 'text-yellow-400',
  47. button: 'bg-yellow-500 hover:bg-yellow-600',
  48. },
  49. default: {
  50. icon: 'text-bambu-green',
  51. button: 'bg-bambu-green hover:bg-bambu-green-dark',
  52. },
  53. };
  54. const styles = variantStyles[variant];
  55. return (
  56. <div
  57. className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4"
  58. onClick={isLoading ? undefined : onCancel}
  59. >
  60. <Card className="w-full max-w-md" onClick={(e: React.MouseEvent) => e.stopPropagation()}>
  61. <CardContent className="p-6">
  62. <div className="flex items-start gap-4">
  63. <div className={`p-2 rounded-full bg-bambu-dark ${styles.icon}`}>
  64. <AlertTriangle className="w-6 h-6" />
  65. </div>
  66. <div className="flex-1">
  67. <h3 className="text-lg font-semibold text-white mb-2">{title}</h3>
  68. <p className="text-bambu-gray text-sm">{message}</p>
  69. </div>
  70. </div>
  71. <div className="flex gap-3 mt-6">
  72. <Button variant="secondary" onClick={onCancel} className="flex-1" disabled={isLoading}>
  73. {resolvedCancelText}
  74. </Button>
  75. <Button
  76. onClick={onConfirm}
  77. className={`flex-1 ${styles.button}`}
  78. disabled={isLoading}
  79. >
  80. {isLoading ? (
  81. <>
  82. <Loader2 className="w-4 h-4 mr-2 animate-spin" />
  83. {resolvedLoadingText}
  84. </>
  85. ) : (
  86. resolvedConfirmText
  87. )}
  88. </Button>
  89. </div>
  90. </CardContent>
  91. </Card>
  92. </div>
  93. );
  94. }