PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.6
Yatra – Travel Booking & Tour Operator Software v3.0.6
3.0.16 3.0.15 3.0.14 3.0.14.1 3.0.14.2 3.0.12 3.0.13 3.0.11 3.0.10 3.0.9 3.0.8 3.0.7 3.0.6 3.0.5 3.0.5.1 3.0.4 3.0.3 3.0.2.9 3.0.2.7 3.0.2.8 3.0.2.6 trunk 1.0.0 2.0.0 2.0.1 All 84 releases
yatra / resources / js / components / ui / toast.tsx

toast.tsx in Yatra – Travel Booking & Tour Operator Software 3.0.6, at resources/js/components/ui/toast.tsx

173 lines 4.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Toast Notification Component
3 * Displays temporary notifications at the top-right of the screen
4 */
5
6 import React, {
7 createContext,
8 useContext,
9 useState,
10 useCallback,
11 useEffect,
12 } from "react";
13 import {
14 X,
15 CheckCircle2,
16 AlertCircle,
17 Info,
18 AlertTriangle,
19 } from "lucide-react";
20 import { __ } from "../../lib/i18n";
21
22 export type ToastType = "success" | "error" | "warning" | "info";
23
24 export interface Toast {
25 id: string;
26 message: string;
27 type: ToastType;
28 duration?: number;
29 }
30
31 interface ToastContextType {
32 showToast: (message: string, type?: ToastType, duration?: number) => void;
33 removeToast: (id: string) => void;
34 }
35
36 const ToastContext = createContext<ToastContextType | undefined>(undefined);
37
38 export const useToast = () => {
39 const context = useContext(ToastContext);
40 if (!context) {
41 throw new Error("useToast must be used within ToastProvider");
42 }
43 return context;
44 };
45
46 export const ToastProvider: React.FC<{ children: React.ReactNode }> = ({
47 children,
48 }) => {
49 const [toasts, setToasts] = useState<Toast[]>([]);
50
51 const showToast = useCallback(
52 (message: string, type: ToastType = "success", duration: number = 4000) => {
53 const id = `toast-${Date.now()}-${Math.random()}`;
54 const newToast: Toast = { id, message, type, duration };
55
56 setToasts((prev) => [...prev, newToast]);
57
58 if (duration > 0) {
59 setTimeout(() => {
60 removeToast(id);
61 }, duration);
62 }
63 },
64 // eslint-disable-next-line react-hooks/exhaustive-deps
65 [],
66 );
67
68 const removeToast = useCallback((id: string) => {
69 setToasts((prev) => prev.filter((toast) => toast.id !== id));
70 }, []);
71
72 return (
73 <ToastContext.Provider value={{ showToast, removeToast }}>
74 {children}
75 <ToastContainer toasts={toasts} onRemove={removeToast} />
76 </ToastContext.Provider>
77 );
78 };
79
80 interface ToastContainerProps {
81 toasts: Toast[];
82 onRemove: (id: string) => void;
83 }
84
85 const ToastContainer: React.FC<ToastContainerProps> = ({
86 toasts,
87 onRemove,
88 }) => {
89 if (toasts.length === 0) return null;
90
91 return (
92 <div className="fixed top-4 right-4 z-[9999] flex flex-col gap-2 pointer-events-none">
93 {toasts.map((toast) => (
94 <ToastItem key={toast.id} toast={toast} onRemove={onRemove} />
95 ))}
96 </div>
97 );
98 };
99
100 interface ToastItemProps {
101 toast: Toast;
102 onRemove: (id: string) => void;
103 }
104
105 const ToastItem: React.FC<ToastItemProps> = ({ toast, onRemove }) => {
106 const [isVisible, setIsVisible] = useState(false);
107
108 useEffect(() => {
109 // Trigger animation
110 setTimeout(() => setIsVisible(true), 10);
111 }, []);
112
113 const handleRemove = () => {
114 setIsVisible(false);
115 setTimeout(() => onRemove(toast.id), 300);
116 };
117
118 const getIcon = () => {
119 switch (toast.type) {
120 case "success":
121 return <CheckCircle2 className="w-5 h-5 text-green-600" />;
122 case "error":
123 return <AlertCircle className="w-5 h-5 text-red-600" />;
124 case "warning":
125 return <AlertTriangle className="w-5 h-5 text-yellow-600" />;
126 case "info":
127 return <Info className="w-5 h-5 text-blue-600" />;
128 default:
129 return <Info className="w-5 h-5 text-gray-600" />;
130 }
131 };
132
133 const getStyles = () => {
134 switch (toast.type) {
135 case "success":
136 return "bg-green-50 border-green-200 text-green-800 dark:bg-green-900/20 dark:border-green-800 dark:text-green-400";
137 case "error":
138 return "bg-red-50 border-red-200 text-red-800 dark:bg-red-900/20 dark:border-red-800 dark:text-red-400";
139 case "warning":
140 return "bg-yellow-50 border-yellow-200 text-yellow-800 dark:bg-yellow-900/20 dark:border-yellow-800 dark:text-yellow-400";
141 case "info":
142 return "bg-blue-50 border-blue-200 text-blue-800 dark:bg-blue-900/20 dark:border-blue-800 dark:text-blue-400";
143 default:
144 return "bg-gray-50 border-gray-200 text-gray-800 dark:bg-gray-900/20 dark:border-gray-800 dark:text-gray-400";
145 }
146 };
147
148 return (
149 <div
150 className={`
151 pointer-events-auto
152 flex items-start gap-3
153 p-4 rounded-lg border
154 shadow-lg backdrop-blur-sm
155 min-w-[320px] max-w-[400px]
156 transition-all duration-300 ease-out
157 ${isVisible ? "translate-x-0 opacity-100" : "translate-x-full opacity-0"}
158 ${getStyles()}
159 `}
160 >
161 <div className="flex-shrink-0 mt-0.5">{getIcon()}</div>
162 <div className="flex-1 text-sm font-medium">{toast.message}</div>
163 <button
164 onClick={handleRemove}
165 className="flex-shrink-0 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors"
166 aria-label={__("Close", "yatra")}
167 >
168 <X className="w-4 h-4" />
169 </button>
170 </div>
171 );
172 };
173