PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.2.8
Yatra – Travel Booking & Tour Operator Software v3.0.2.8
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 / notices / InlineNotices.tsx

InlineNotices.tsx in Yatra – Travel Booking & Tour Operator Software 3.0.2.8, at resources/js/components/notices/InlineNotices.tsx

209 lines 7.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import React, { useCallback, useEffect, useState } from "react";
2 import { apiService } from "../../lib/api-client";
3 import { __ } from "../../lib/i18n";
4 import { Crown, Star, Zap } from "lucide-react";
5
6 type NoticeAction = {
7 label: string;
8 url: string;
9 target?: string;
10 };
11
12 type NoticeItem = {
13 id: string;
14 type?: "info" | "success" | "warning" | "error";
15 title?: string;
16 message: string;
17 actions?: NoticeAction[];
18 };
19
20 function typeToClasses(type: NoticeItem["type"]) {
21 switch (type) {
22 case "success":
23 return "bg-green-50 dark:bg-green-950/30 border-green-200 dark:border-green-900/40 text-green-900 dark:text-green-100";
24 case "warning":
25 return "bg-amber-50 dark:bg-amber-950/25 border-amber-200 dark:border-amber-900/40 text-amber-900 dark:text-amber-100";
26 case "error":
27 return "bg-red-50 dark:bg-red-950/30 border-red-200 dark:border-red-900/40 text-red-900 dark:text-red-100";
28 default:
29 return "bg-blue-50 dark:bg-blue-950/25 border-blue-200 dark:border-blue-900/40 text-blue-900 dark:text-blue-100";
30 }
31 }
32
33 export const InlineNotices: React.FC = () => {
34 const [notices, setNotices] = useState<NoticeItem[]>([]);
35
36 const load = useCallback(async () => {
37 try {
38 const res = await apiService.getNotices();
39 const data = (res as any)?.data;
40 setNotices(Array.isArray(data) ? data : []);
41 } catch {
42 // Fail silent: notices should never block the UI.
43 setNotices([]);
44 }
45 }, []);
46
47 useEffect(() => {
48 load();
49 }, [load]);
50
51 const dismiss = useCallback(
52 async (id: string) => {
53 // Optimistic UI.
54 setNotices((prev) => prev.filter((n) => n.id !== id));
55 try {
56 await apiService.dismissNotice(id);
57 } catch {
58 // Reload if dismiss failed (permissions / nonce / etc.)
59 load();
60 }
61 },
62 [load],
63 );
64
65 if (notices.length === 0) {
66 return null;
67 }
68
69 const renderUpgradeNotice = (n: NoticeItem) => {
70 const primary = Array.isArray(n.actions) ? n.actions[0] : undefined;
71 return (
72 <div
73 key={n.id}
74 className="mx-6 mt-4 border border-amber-200/60 dark:border-amber-900/40 rounded-xl overflow-hidden bg-amber-50/60 dark:bg-amber-950/20 shadow-sm"
75 >
76 <div className="border-l-4 border-amber-400">
77 <div className="p-4">
78 <div className="flex items-start justify-between gap-4">
79 <div className="flex items-start gap-3 min-w-0">
80 <div className="w-9 h-9 rounded-lg bg-amber-100/70 dark:bg-amber-900/20 border border-amber-200/60 dark:border-amber-900/40 flex items-center justify-center shrink-0">
81 <Crown className="w-5 h-5 text-amber-700 dark:text-amber-300" />
82 </div>
83 <div className="min-w-0">
84 <div className="font-semibold text-gray-900 dark:text-gray-100">
85 {n.title || __("Upgrade to Pro", "yatra")}
86 </div>
87 <div className="text-sm text-gray-600 dark:text-gray-300 mt-1">
88 {n.message}
89 </div>
90 </div>
91 </div>
92
93 <div className="flex items-center gap-3 shrink-0">
94 <span className="text-[11px] font-bold px-2 py-0.5 rounded-full bg-amber-500 text-white">
95 {__("LIMITED TIME", "yatra")}
96 </span>
97 <button
98 type="button"
99 onClick={() => dismiss(n.id)}
100 className="text-gray-400 hover:text-gray-700 dark:hover:text-gray-200"
101 aria-label={__("Dismiss notice", "yatra")}
102 >
103 ×
104 </button>
105 </div>
106 </div>
107
108 <div className="mt-3 rounded-lg bg-amber-100/50 dark:bg-amber-900/15 border-l-4 border-amber-400 px-3 py-2 text-amber-800 dark:text-amber-200 text-sm font-medium flex items-center gap-2">
109 <Zap className="w-4 h-4" />
110 {__(
111 "Special Offer: Upgrade today and save 30%+ with premium features and priority support.",
112 "yatra",
113 )}
114 </div>
115
116 <div className="mt-4 flex items-center gap-3">
117 {primary?.url && primary?.label && (
118 <a
119 href={primary.url}
120 target={primary.target}
121 rel={
122 primary.target === "_blank"
123 ? "noopener noreferrer"
124 : undefined
125 }
126 className="inline-flex items-center gap-2 px-3 py-1.5 rounded-md bg-amber-500 hover:bg-amber-600 text-white text-xs font-semibold transition-colors"
127 >
128 <Zap className="w-4 h-4" />
129 {primary.label}
130 </a>
131 )}
132 <button
133 type="button"
134 onClick={() => dismiss(n.id)}
135 className="text-sm text-gray-500 hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-200"
136 >
137 {__("Maybe later", "yatra")}
138 </button>
139 </div>
140 </div>
141 </div>
142 </div>
143 );
144 };
145
146 return (
147 <div className="space-y-3">
148 {notices.map((n) => {
149 if (n.id === "buy_pro") {
150 return renderUpgradeNotice(n);
151 }
152
153 const primary = Array.isArray(n.actions) ? n.actions[0] : undefined;
154 return (
155 <div
156 key={n.id}
157 className="mx-6 mt-4 border border-gray-200 dark:border-gray-800 rounded-xl bg-white dark:bg-gray-900 shadow-sm"
158 role="status"
159 aria-live="polite"
160 >
161 <div className="p-4 flex items-start justify-between gap-4">
162 <div className="flex items-start gap-3 min-w-0">
163 <div className="w-10 h-10 rounded-lg bg-blue-50 dark:bg-blue-950/30 border border-blue-200/60 dark:border-blue-900/40 flex items-center justify-center shrink-0">
164 <Star className="w-5 h-5 text-blue-700 dark:text-blue-300" />
165 </div>
166 <div className="min-w-0">
167 {n.title && (
168 <div className="font-semibold text-gray-900 dark:text-gray-100">
169 {n.title}
170 </div>
171 )}
172 <div className="text-sm text-gray-700 dark:text-gray-300 mt-1">
173 {n.message}
174 </div>
175 </div>
176 </div>
177
178 <div className="flex items-center gap-3 shrink-0">
179 {primary?.url && primary?.label && (
180 <a
181 href={primary.url}
182 target={primary.target}
183 rel={
184 primary.target === "_blank"
185 ? "noopener noreferrer"
186 : undefined
187 }
188 className="inline-flex items-center px-3 py-1.5 rounded-md bg-blue-600 hover:bg-blue-700 text-white text-xs font-semibold transition-colors"
189 >
190 {primary.label}
191 </a>
192 )}
193 <button
194 type="button"
195 onClick={() => dismiss(n.id)}
196 className="text-gray-400 hover:text-gray-700 dark:hover:text-gray-200"
197 aria-label={__("Dismiss notice", "yatra")}
198 >
199 ×
200 </button>
201 </div>
202 </div>
203 </div>
204 );
205 })}
206 </div>
207 );
208 };
209