PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.3
Yatra – Travel Booking & Tour Operator Software v3.0.3
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 2.0.10 All 83 releases
yatra / resources / js / components / ui / toast.tsx

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

172 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 [],
65 );
66
67 const removeToast = useCallback((id: string) => {
68 setToasts((prev) => prev.filter((toast) => toast.id !== id));
69 }, []);
70
71 return (
72 <ToastContext.Provider value={{ showToast, removeToast }}>
73 {children}
74 <ToastContainer toasts={toasts} onRemove={removeToast} />
75 </ToastContext.Provider>
76 );
77 };
78
79 interface ToastContainerProps {
80 toasts: Toast[];
81 onRemove: (id: string) => void;
82 }
83
84 const ToastContainer: React.FC<ToastContainerProps> = ({
85 toasts,
86 onRemove,
87 }) => {
88 if (toasts.length === 0) return null;
89
90 return (
91 <div className="fixed top-4 right-4 z-[9999] flex flex-col gap-2 pointer-events-none">
92 {toasts.map((toast) => (
93 <ToastItem key={toast.id} toast={toast} onRemove={onRemove} />
94 ))}
95 </div>
96 );
97 };
98
99 interface ToastItemProps {
100 toast: Toast;
101 onRemove: (id: string) => void;
102 }
103
104 const ToastItem: React.FC<ToastItemProps> = ({ toast, onRemove }) => {
105 const [isVisible, setIsVisible] = useState(false);
106
107 useEffect(() => {
108 // Trigger animation
109 setTimeout(() => setIsVisible(true), 10);
110 }, []);
111
112 const handleRemove = () => {
113 setIsVisible(false);
114 setTimeout(() => onRemove(toast.id), 300);
115 };
116
117 const getIcon = () => {
118 switch (toast.type) {
119 case "success":
120 return <CheckCircle2 className="w-5 h-5 text-green-600" />;
121 case "error":
122 return <AlertCircle className="w-5 h-5 text-red-600" />;
123 case "warning":
124 return <AlertTriangle className="w-5 h-5 text-yellow-600" />;
125 case "info":
126 return <Info className="w-5 h-5 text-blue-600" />;
127 default:
128 return <Info className="w-5 h-5 text-gray-600" />;
129 }
130 };
131
132 const getStyles = () => {
133 switch (toast.type) {
134 case "success":
135 return "bg-green-50 border-green-200 text-green-800 dark:bg-green-900/20 dark:border-green-800 dark:text-green-400";
136 case "error":
137 return "bg-red-50 border-red-200 text-red-800 dark:bg-red-900/20 dark:border-red-800 dark:text-red-400";
138 case "warning":
139 return "bg-yellow-50 border-yellow-200 text-yellow-800 dark:bg-yellow-900/20 dark:border-yellow-800 dark:text-yellow-400";
140 case "info":
141 return "bg-blue-50 border-blue-200 text-blue-800 dark:bg-blue-900/20 dark:border-blue-800 dark:text-blue-400";
142 default:
143 return "bg-gray-50 border-gray-200 text-gray-800 dark:bg-gray-900/20 dark:border-gray-800 dark:text-gray-400";
144 }
145 };
146
147 return (
148 <div
149 className={`
150 pointer-events-auto
151 flex items-start gap-3
152 p-4 rounded-lg border
153 shadow-lg backdrop-blur-sm
154 min-w-[320px] max-w-[400px]
155 transition-all duration-300 ease-out
156 ${isVisible ? "translate-x-0 opacity-100" : "translate-x-full opacity-0"}
157 ${getStyles()}
158 `}
159 >
160 <div className="flex-shrink-0 mt-0.5">{getIcon()}</div>
161 <div className="flex-1 text-sm font-medium">{toast.message}</div>
162 <button
163 onClick={handleRemove}
164 className="flex-shrink-0 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors"
165 aria-label={__("Close", "yatra")}
166 >
167 <X className="w-4 h-4" />
168 </button>
169 </div>
170 );
171 };
172