File size: 3,398 Bytes
88da18c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
import React from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { CheckCircle, AlertCircle, Info, X } from 'lucide-react';
import { cn } from '@/utils/helpers';

export type ToastType = 'success' | 'error' | 'warning' | 'info';

interface ToastProps {
  type: ToastType;
  message: string;
  title?: string;
  onClose?: () => void;
  autoClose?: boolean;
  autoCloseDuration?: number;
  action?: {
    label: string;
    onClick: () => void;
  };
}

const Toast = React.forwardRef<HTMLDivElement, ToastProps>(
  (
    {
      type,
      message,
      title,
      onClose,
      autoClose = true,
      autoCloseDuration = 5000,
      action,
    },
    ref
  ) => {
    const [isVisible, setIsVisible] = React.useState(true);

    React.useEffect(() => {
      if (!autoClose) return;
      const timer = setTimeout(() => {
        setIsVisible(false);
        onClose?.();
      }, autoCloseDuration);
      return () => clearTimeout(timer);
    }, [autoClose, autoCloseDuration, onClose]);

    const icons = {
      success: <CheckCircle className="w-5 h-5" />,
      error: <AlertCircle className="w-5 h-5" />,
      warning: <AlertCircle className="w-5 h-5" />,
      info: <Info className="w-5 h-5" />,
    };

    const colors = {
      success: 'bg-emerald-500/20 border-emerald-500/30 text-emerald-200',
      error: 'bg-red-500/20 border-red-500/30 text-red-200',
      warning: 'bg-yellow-500/20 border-yellow-500/30 text-yellow-200',
      info: 'bg-blue-500/20 border-blue-500/30 text-blue-200',
    };

    const iconColors = {
      success: 'text-emerald-400',
      error: 'text-red-400',
      warning: 'text-yellow-400',
      info: 'text-blue-400',
    };

    return (
      <AnimatePresence>
        {isVisible && (
          <motion.div
            ref={ref}
            initial={{ opacity: 0, y: 20 }}
            animate={{ opacity: 1, y: 0 }}
            exit={{ opacity: 0, y: 20 }}
            className={cn(
              'flex items-start gap-3 w-full max-w-md px-4 py-3 rounded-lg border',
              colors[type]
            )}
          >
            <div className={cn('mt-0.5 flex-shrink-0', iconColors[type])}>
              {icons[type]}
            </div>
            <div className="flex-1 min-w-0">
              {title && (
                <p className="font-semibold text-sm mb-0.5">{title}</p>
              )}
              <p className="text-sm opacity-90">{message}</p>
              {action && (
                <motion.button
                  whileHover={{ x: 2 }}
                  onClick={action.onClick}
                  className="text-xs font-medium mt-2 opacity-80 hover:opacity-100 transition-opacity"
                >
                  {action.label}
                </motion.button>
              )}
            </div>
            {onClose && (
              <motion.button
                whileHover={{ scale: 1.1 }}
                whileTap={{ scale: 0.95 }}
                onClick={() => {
                  setIsVisible(false);
                  onClose?.();
                }}
                className="flex-shrink-0 opacity-50 hover:opacity-100 transition-opacity"
              >
                <X className="w-4 h-4" />
              </motion.button>
            )}
          </motion.div>
        )}
      </AnimatePresence>
    );
  }
);

Toast.displayName = 'Toast';

export default Toast;