PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.9
Yatra – Travel Booking & Tour Operator Software v3.0.9
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.9, at resources/js/components/ui/toast.tsx

187 lines 5.3 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 // Sit below the WordPress admin bar (and above it in stacking order) so
90 // toasts aren't hidden behind it on the front-end account page. The admin
91 // bar is 46px on small screens (<=782px) and 32px otherwise; +16px gap.
92 const topOffset = React.useMemo(() => {
93 if (typeof document === "undefined") return 16;
94 if (!document.body.classList.contains("admin-bar")) return 16;
95 const small = typeof window !== "undefined" && window.innerWidth <= 782;
96 return (small ? 46 : 32) + 16;
97 }, []);
98
99 if (toasts.length === 0) return null;
100
101 return (
102 <div
103 style={{ top: topOffset }}
104 // z-index above the WP admin bar (99999) so it never hides the toast.
105 className="fixed right-4 z-[100000] flex flex-col gap-2 pointer-events-none"
106 >
107 {toasts.map((toast) => (
108 <ToastItem key={toast.id} toast={toast} onRemove={onRemove} />
109 ))}
110 </div>
111 );
112 };
113
114 interface ToastItemProps {
115 toast: Toast;
116 onRemove: (id: string) => void;
117 }
118
119 const ToastItem: React.FC<ToastItemProps> = ({ toast, onRemove }) => {
120 const [isVisible, setIsVisible] = useState(false);
121
122 useEffect(() => {
123 // Trigger animation
124 setTimeout(() => setIsVisible(true), 10);
125 }, []);
126
127 const handleRemove = () => {
128 setIsVisible(false);
129 setTimeout(() => onRemove(toast.id), 300);
130 };
131
132 const getIcon = () => {
133 switch (toast.type) {
134 case "success":
135 return <CheckCircle2 className="w-5 h-5 text-green-600" />;
136 case "error":
137 return <AlertCircle className="w-5 h-5 text-red-600" />;
138 case "warning":
139 return <AlertTriangle className="w-5 h-5 text-yellow-600" />;
140 case "info":
141 return <Info className="w-5 h-5 text-blue-600" />;
142 default:
143 return <Info className="w-5 h-5 text-gray-600" />;
144 }
145 };
146
147 const getStyles = () => {
148 switch (toast.type) {
149 case "success":
150 return "bg-green-50 border-green-200 text-green-800 dark:bg-green-900/20 dark:border-green-800 dark:text-green-400";
151 case "error":
152 return "bg-red-50 border-red-200 text-red-800 dark:bg-red-900/20 dark:border-red-800 dark:text-red-400";
153 case "warning":
154 return "bg-yellow-50 border-yellow-200 text-yellow-800 dark:bg-yellow-900/20 dark:border-yellow-800 dark:text-yellow-400";
155 case "info":
156 return "bg-blue-50 border-blue-200 text-blue-800 dark:bg-blue-900/20 dark:border-blue-800 dark:text-blue-400";
157 default:
158 return "bg-gray-50 border-gray-200 text-gray-800 dark:bg-gray-900/20 dark:border-gray-800 dark:text-gray-400";
159 }
160 };
161
162 return (
163 <div
164 className={`
165 pointer-events-auto
166 flex items-start gap-3
167 p-4 rounded-lg border
168 shadow-lg backdrop-blur-sm
169 min-w-[320px] max-w-[400px]
170 transition-all duration-300 ease-out
171 ${isVisible ? "translate-x-0 opacity-100" : "translate-x-full opacity-0"}
172 ${getStyles()}
173 `}
174 >
175 <div className="flex-shrink-0 mt-0.5">{getIcon()}</div>
176 <div className="flex-1 text-sm font-medium">{toast.message}</div>
177 <button
178 onClick={handleRemove}
179 className="flex-shrink-0 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors"
180 aria-label={__("Close", "yatra")}
181 >
182 <X className="w-4 h-4" />
183 </button>
184 </div>
185 );
186 };
187