PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.17
Yatra – Travel Booking & Tour Operator Software v3.0.17
3.0.17 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 All 85 releases
← All changes | assets/js/listing-wishlist.js +375 -150 3.0.3 → 3.0.17 View file →
@@ -1,21 +1,59 @@
1 -// Wishlist functionality for trip listing page (Yatra Pro + setting)
1 +// Wishlist functionality for trip listing / single / similar trips.
2 +
3 +// Translation helpers — idempotent shim matching the other frontend bundles.
4 +(function () {
5 + if (typeof window.__ === 'function') return;
6 + if (window.wp && window.wp.i18n && typeof window.wp.i18n.__ === 'function') {
7 + window.__ = function (text, domain) { return window.wp.i18n.__(text, domain || 'yatra'); };
8 + window._n = function (s, p, n, domain) { return window.wp.i18n._n(s, p, n, domain || 'yatra'); };
9 + window._x = function (text, ctx, domain) { return window.wp.i18n._x(text, ctx, domain || 'yatra'); };
10 + window.sprintf = window.sprintf || (window.wp.i18n.sprintf || function (fmt) { return fmt; });
11 + } else {
12 + window.__ = function (text) { return text; };
13 + window._n = function (s, p, n) { return n === 1 ? s : p; };
14 + window._x = function (text) { return text; };
15 + window.sprintf = window.sprintf || function (fmt) { return fmt; };
16 + }
17 +})();
18 +
2 19 class TripListingWishlist {
3 20 constructor() {
4 - var cfg = window.yatraWishlistConfig || {};
5 - var base = (cfg.restUrl || '').replace(/\/$/, '');
6 - this.apiUrl = base || (window.location && window.location.origin ? window.location.origin + '/wp-json/yatra/v1' : '/wp-json/yatra/v1');
21 + const cfg = window.yatraWishlistConfig || {};
22 + const base = (cfg.restUrl || '').replace(/\/$/, '');
23 + this.apiUrl = base
24 + || (window.location && window.location.origin
25 + ? window.location.origin + '/wp-json/yatra/v1'
26 + : '/wp-json/yatra/v1');
7 27 this.nonce = cfg.nonce || '';
8 28 this.isLoggedIn = !!cfg.isLoggedIn;
9 -
10 - // Debug logging
11 -
12 -
29 + this.loginUrl = cfg.loginUrl || '/wp-login.php';
30 +
31 + // Server-localized strings — fall back to JS-side __() so the bundle
32 + // still works if the PHP localizer didn't ship the i18n bag.
33 + const i = cfg.i18n || {};
34 + this.strings = {
35 + loginRequired: i.loginRequired || __('Login Required', 'yatra'),
36 + loginPrompt: i.loginPrompt || __('Please login to save trips to your wishlist.', 'yatra'),
37 + login: i.login || __('Login', 'yatra'),
38 + cancel: i.cancel || __('Cancel', 'yatra'),
39 + genericError: i.genericError || __('An error occurred. Please try again.', 'yatra'),
40 + saved: i.saved || __('Trip saved to wishlist', 'yatra'),
41 + removed: i.removed || __('Trip removed from wishlist', 'yatra'),
42 + saveFailed: i.saveFailed || __('Failed to save trip', 'yatra'),
43 + removeFailed: i.removeFailed || __('Failed to remove trip', 'yatra'),
44 + addAria: i.addAria || __('Add to favorites', 'yatra'),
45 + removeAria: i.removeAria || __('Remove from favorites', 'yatra'),
46 + };
47 +
48 + // Per-button in-flight guard so rapid double-clicks don't fire two
49 + // requests. Keyed by trip ID.
50 + this.inFlight = new Set();
51 +
13 52 this.init();
14 53 }
15 54
16 55 init() {
17 - // Handle wishlist button clicks
18 56 document.addEventListener('click', (e) => {
19 57 const btn = e.target.closest('.yatra-favorite-btn, .yatra-similar-favorite-btn');
20 58 if (btn) {
21 59 e.preventDefault();
@@ -23,40 +61,54 @@
23 61 this.handleWishlistClick(btn);
24 62 }
25 63 });
26 64
27 - // Check saved status for all trips if logged in
28 65 if (this.isLoggedIn) {
29 66 this.checkAllSavedStatus();
30 67 }
31 68 }
32 69
70 + // Hydrate the saved/unsaved state for every wishlist button on the page.
71 + // We Promise.all the lookups so errors aren't swallowed by `forEach(async)`
72 + // and the caller can await full settling if needed.
33 73 async checkAllSavedStatus() {
34 - const buttons = document.querySelectorAll('.yatra-favorite-btn[data-trip-id], .yatra-similar-favorite-btn[data-trip-id]');
35 - buttons.forEach(async (btn) => {
36 - const tripId = btn.getAttribute('data-trip-id');
37 - if (tripId) {
38 - try {
39 - const response = await fetch(
40 - `${this.apiUrl}/saved-trips/check/${tripId}`,
41 - {
42 - method: 'GET',
43 - headers: {
44 - 'X-WP-Nonce': this.nonce
45 - },
46 - credentials: 'same-origin'
47 - }
48 - );
74 + const buttons = Array.from(document.querySelectorAll(
75 + '.yatra-favorite-btn[data-trip-id], .yatra-similar-favorite-btn[data-trip-id]'
76 + ));
77 + if (buttons.length === 0) return;
49 78
50 - const data = await response.json();
51 - if (data.success && data.data.is_saved) {
52 - this.updateButtonState(btn, true);
79 + // Dedupe — multiple buttons can point at the same trip (hero +
80 + // similar-trips card). One lookup, many DOM updates.
81 + const byId = new Map();
82 + for (const btn of buttons) {
83 + const id = parseInt(btn.getAttribute('data-trip-id'), 10);
84 + if (!id || id <= 0) continue;
85 + if (!byId.has(id)) byId.set(id, []);
86 + byId.get(id).push(btn);
87 + }
88 +
89 + await Promise.all(Array.from(byId.entries()).map(async ([tripId, btns]) => {
90 + try {
91 + const response = await fetch(
92 + `${this.apiUrl}/saved-trips/check/${tripId}`,
93 + {
94 + method: 'GET',
95 + headers: { 'X-WP-Nonce': this.nonce },
96 + credentials: 'same-origin',
53 97 }
54 - } catch (error) {
55 - console.error('Error checking wishlist status:', error);
98 + );
99 + if (!response.ok) return;
100 + const data = await response.json();
101 + if (data && data.success && data.data && data.data.is_saved) {
102 + btns.forEach((b) => this.updateButtonState(b, true));
56 103 }
104 + } catch (error) {
105 + // Silent — page still functions, button just shows unsaved.
106 + if (window.console && console.warn) {
107 + console.warn('Yatra wishlist: status check failed for trip', tripId, error);
108 + }
57 109 }
58 - });
110 + }));
59 111 }
60 112
61 113 handleWishlistClick(btn) {
62 114 if (!this.isLoggedIn) {
@@ -63,14 +115,17 @@
63 115 this.showLoginPopup();
64 116 return;
65 117 }
66 118
67 - const tripId = btn.getAttribute('data-trip-id');
68 - if (!tripId) {
69 - console.error('Trip ID not found');
119 + const tripId = parseInt(btn.getAttribute('data-trip-id'), 10);
120 + if (!tripId || tripId <= 0) {
70 121 return;
71 122 }
72 123
124 + // Idempotent guard: ignore clicks while a request for this trip is in
125 + // flight. Prevents double-POSTs from rapid taps / accidental dbl-click.
126 + if (this.inFlight.has(tripId)) return;
127 +
73 128 const isSaved = btn.classList.contains('saved');
74 129 if (isSaved) {
75 130 this.removeFromWishlist(btn, tripId);
76 131 } else {
@@ -77,42 +132,55 @@
77 132 this.addToWishlist(btn, tripId);
78 133 }
79 134 }
80 135
136 + // Mirror the new state across every button on the page pointing at this
137 + // trip ID so the hero + card + similar-trips all stay in sync.
138 + syncAllButtonsForTrip(tripId, isSaved) {
139 + document.querySelectorAll(
140 + `.yatra-favorite-btn[data-trip-id="${tripId}"], .yatra-similar-favorite-btn[data-trip-id="${tripId}"]`
141 + ).forEach((b) => this.updateButtonState(b, isSaved));
142 + }
143 +
81 144 async addToWishlist(btn, tripId) {
145 + this.inFlight.add(tripId);
82 146 btn.disabled = true;
83 147 btn.classList.add('loading');
84 148
85 149 try {
86 150 const params = new URLSearchParams();
87 - params.set('trip_id', String(parseInt(tripId, 10)));
151 + params.set('trip_id', String(tripId));
88 152 const response = await fetch(`${this.apiUrl}/saved-trips`, {
89 153 method: 'POST',
90 154 headers: {
91 155 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
92 - 'X-WP-Nonce': this.nonce
156 + 'X-WP-Nonce': this.nonce,
93 157 },
94 158 credentials: 'same-origin',
95 - body: params.toString()
159 + body: params.toString(),
96 160 });
97 161
98 - const data = await response.json();
99 - if (data.success) {
100 - this.updateButtonState(btn, true);
101 - this.showMessage(data.message || 'Trip saved to wishlist', 'success', btn);
162 + const data = await response.json().catch(() => ({}));
163 + if (response.ok && data && data.success) {
164 + this.syncAllButtonsForTrip(tripId, true);
165 + this.showMessage(data.message || this.strings.saved, 'success', btn);
102 166 } else {
103 - this.showMessage(data.message || 'Failed to save trip', 'error', btn);
167 + this.showMessage((data && data.message) || this.strings.saveFailed, 'error', btn);
104 168 }
105 169 } catch (error) {
106 - console.error('Error saving trip:', error);
107 - this.showMessage('An error occurred. Please try again.', 'error', btn);
170 + if (window.console && console.error) {
171 + console.error('Yatra wishlist: save failed', error);
172 + }
173 + this.showMessage(this.strings.genericError, 'error', btn);
108 174 } finally {
109 175 btn.disabled = false;
110 176 btn.classList.remove('loading');
177 + this.inFlight.delete(tripId);
111 178 }
112 179 }
113 180
114 181 async removeFromWishlist(btn, tripId) {
182 + this.inFlight.add(tripId);
115 183 btn.disabled = true;
116 184 btn.classList.add('loading');
117 185
118 186 try {
@@ -117,139 +185,304 @@
117 185
118 186 try {
119 187 const response = await fetch(`${this.apiUrl}/saved-trips/${tripId}`, {
120 188 method: 'DELETE',
121 - headers: {
122 - 'X-WP-Nonce': this.nonce
123 - },
124 - credentials: 'same-origin'
189 + headers: { 'X-WP-Nonce': this.nonce },
190 + credentials: 'same-origin',
125 191 });
126 192
127 - const data = await response.json();
128 - if (data.success) {
129 - this.updateButtonState(btn, false);
130 - this.showMessage(data.message || 'Trip removed from wishlist', 'success', btn);
193 + const data = await response.json().catch(() => ({}));
194 + if (response.ok && data && data.success) {
195 + this.syncAllButtonsForTrip(tripId, false);
196 + this.showMessage(data.message || this.strings.removed, 'success', btn);
131 197 } else {
132 - this.showMessage(data.message || 'Failed to remove trip', 'error', btn);
198 + this.showMessage((data && data.message) || this.strings.removeFailed, 'error', btn);
133 199 }
134 200 } catch (error) {
135 - console.error('Error removing trip:', error);
136 - this.showMessage('An error occurred. Please try again.', 'error', btn);
201 + if (window.console && console.error) {
202 + console.error('Yatra wishlist: remove failed', error);
203 + }
204 + this.showMessage(this.strings.genericError, 'error', btn);
137 205 } finally {
138 206 btn.disabled = false;
139 207 btn.classList.remove('loading');
208 + this.inFlight.delete(tripId);
140 209 }
141 210 }
142 211
143 212 updateButtonState(btn, isSaved) {
213 + // Visual state is driven entirely by CSS — see
214 + // `.yatra-favorite-btn.saved` / `.yatra-favorite-btn.is-saved` rules.
215 + // We only toggle classes + a11y attributes here so the markup
216 + // stays clean and the CSS can be edited in one place.
144 217 if (isSaved) {
145 218 btn.classList.add('saved', 'is-saved');
146 - btn.setAttribute('aria-label', 'Remove from favorites');
147 - btn.setAttribute('title', 'Remove from favorites');
148 - // Fill the heart icon and change color
149 - const svg = btn.querySelector('svg');
150 - if (svg) {
151 - svg.style.fill = 'currentColor';
152 - svg.style.stroke = 'currentColor';
153 - btn.style.color = '#ef4444';
154 - }
219 + btn.setAttribute('aria-label', this.strings.removeAria);
220 + btn.setAttribute('title', this.strings.removeAria);
155 221 } else {
156 222 btn.classList.remove('saved', 'is-saved');
157 - btn.setAttribute('aria-label', 'Add to favorites');
158 - btn.setAttribute('title', 'Add to favorites');
159 - // Unfill the heart icon
160 - const svg = btn.querySelector('svg');
161 - if (svg) {
162 - svg.style.fill = 'none';
163 - svg.style.stroke = 'currentColor';
164 - btn.style.color = '';
223 + btn.setAttribute('aria-label', this.strings.addAria);
224 + btn.setAttribute('title', this.strings.addAria);
225 + }
226 +
227 + // Clear any inline styles a previous version of this script may have
228 + // left behind so the CSS rules can take effect cleanly.
229 + if (btn.style.color) btn.style.color = '';
230 + const svg = btn.querySelector('svg');
231 + if (svg) {
232 + if (svg.style.fill) svg.style.fill = '';
233 + if (svg.style.stroke) svg.style.stroke = '';
234 + }
235 + }
236 +
237 + // Build a login URL that returns the user back to the page they were on
238 + // after authenticating, rather than dropping them on the dashboard.
239 + buildLoginUrl() {
240 + const here = (window.location && window.location.href) || '/';
241 + try {
242 + const u = new URL(this.loginUrl, window.location.origin);
243 + u.searchParams.set('redirect_to', here);
244 + return u.toString();
245 + } catch (_e) {
246 + const sep = this.loginUrl.indexOf('?') >= 0 ? '&' : '?';
247 + return this.loginUrl + sep + 'redirect_to=' + encodeURIComponent(here);
248 + }
249 + }
250 +
251 + // Inject the modal stylesheet once per page. Uses Yatra design tokens
252 + // (--yatra-primary, --yatra-text-*, --yatra-radius-*, --yatra-shadow-*)
253 + // from common.css with safe fallbacks if the stylesheet hasn't loaded.
254 + ensureModalStyles() {
255 + if (document.getElementById('yatra-login-modal-styles')) return;
256 + const style = document.createElement('style');
257 + style.id = 'yatra-login-modal-styles';
258 + style.textContent = `
259 + .yatra-login-modal {
260 + position: fixed; inset: 0; z-index: 100000;
261 + display: flex; align-items: center; justify-content: center;
262 + font-family: inherit;
263 + animation: yatraLoginFade .18s ease-out;
165 264 }
166 - }
265 + .yatra-login-modal__overlay {
266 + position: absolute; inset: 0;
267 + background: rgba(17, 24, 39, 0.55);
268 + backdrop-filter: blur(3px);
269 + -webkit-backdrop-filter: blur(3px);
270 + }
271 + .yatra-login-modal__panel {
272 + position: relative;
273 + background: var(--yatra-bg-white, #ffffff);
274 + border-radius: var(--yatra-radius-lg, 16px);
275 + box-shadow: var(--yatra-shadow-lg, 0 10px 15px -3px rgba(0,0,0,.1), 0 4px 6px -4px rgba(0,0,0,.1));
276 + padding: var(--yatra-space-xl, 32px) var(--yatra-space-lg, 24px) var(--yatra-space-lg, 24px);
277 + max-width: 420px; width: calc(100% - 32px);
278 + text-align: center; box-sizing: border-box;
279 + animation: yatraLoginPop .22s cubic-bezier(.2,.9,.4,1.2);
280 + }
281 + .yatra-login-modal__close {
282 + position: absolute; top: var(--yatra-space-sm, 8px); right: var(--yatra-space-sm, 8px);
283 + width: 32px; height: 32px; border-radius: 50%; border: 0;
284 + background: transparent; cursor: pointer;
285 + color: var(--yatra-text-muted, #6b7280);
286 + display: inline-flex; align-items: center; justify-content: center;
287 + transition: background .15s ease, color .15s ease;
288 + padding: 0;
289 + }
290 + .yatra-login-modal__close:hover,
291 + .yatra-login-modal__close:focus-visible {
292 + background: var(--yatra-bg-gray-100, #f3f4f6);
293 + color: var(--yatra-text-primary, #1f2937);
294 + outline: none;
295 + }
296 + .yatra-login-modal__icon {
297 + width: 56px; height: 56px; border-radius: 50%;
298 + background: var(--yatra-primary-light-soft, #eff6ff);
299 + color: var(--yatra-primary, #3b82f6);
300 + display: inline-flex; align-items: center; justify-content: center;
301 + margin: var(--yatra-space-xs, 4px) auto var(--yatra-space-md, 16px);
302 + }
303 + .yatra-login-modal__icon svg { width: 28px; height: 28px; fill: currentColor; }
304 + .yatra-login-modal__title {
305 + margin: 0 0 var(--yatra-space-sm, 8px);
306 + font-size: 20px; font-weight: 700;
307 + color: var(--yatra-text-primary, #1f2937);
308 + line-height: 1.3; letter-spacing: -.01em;
309 + }
310 + .yatra-login-modal__body {
311 + margin: 0 0 var(--yatra-space-lg, 24px);
312 + font-size: 14.5px;
313 + color: var(--yatra-text-secondary, #4b5563);
314 + line-height: 1.55;
315 + }
316 + .yatra-login-modal__actions {
317 + display: flex; gap: var(--yatra-space-sm, 10px);
318 + justify-content: center; flex-wrap: wrap;
319 + }
320 + .yatra-login-modal__btn {
321 + display: inline-flex; align-items: center; justify-content: center;
322 + min-width: 120px;
323 + padding: 11px var(--yatra-space-lg, 22px);
324 + font-size: 14.5px; font-weight: 600;
325 + border-radius: var(--yatra-radius, 8px);
326 + cursor: pointer; border: 1px solid transparent;
327 + transition: transform .08s ease, box-shadow .15s ease, background .15s ease, color .15s ease, border-color .15s ease;
328 + text-decoration: none; line-height: 1;
329 + font-family: inherit; box-sizing: border-box;
330 + }
331 + .yatra-login-modal__btn:active { transform: translateY(1px); }
332 + .yatra-login-modal__btn--primary {
333 + background: var(--yatra-primary, #3b82f6);
334 + color: var(--yatra-bg-white, #ffffff);
335 + box-shadow: var(--yatra-shadow-md, 0 4px 6px -1px rgba(0,0,0,.1), 0 2px 4px -2px rgba(0,0,0,.1));
336 + }
337 + .yatra-login-modal__btn--primary:hover,
338 + .yatra-login-modal__btn--primary:focus-visible {
339 + background: var(--yatra-primary-dark, #2563eb);
340 + color: var(--yatra-bg-white, #ffffff);
341 + outline: none;
342 + }
343 + .yatra-login-modal__btn--ghost {
344 + background: var(--yatra-bg-white, #ffffff);
345 + color: var(--yatra-text-secondary, #4b5563);
346 + border-color: var(--yatra-border-color, #e5e7eb);
347 + }
348 + .yatra-login-modal__btn--ghost:hover,
349 + .yatra-login-modal__btn--ghost:focus-visible {
350 + background: var(--yatra-bg-gray-50, #f9fafb);
351 + color: var(--yatra-text-primary, #1f2937);
352 + border-color: var(--yatra-text-light, #9ca3af);
353 + outline: none;
354 + }
355 + @keyframes yatraLoginFade { from { opacity: 0 } to { opacity: 1 } }
356 + @keyframes yatraLoginPop {
357 + from { opacity: 0; transform: translateY(8px) scale(.96) }
358 + to { opacity: 1; transform: translateY(0) scale(1) }
359 + }
360 + @media (max-width: 480px) {
361 + .yatra-login-modal__panel {
362 + padding: var(--yatra-space-lg, 24px) var(--yatra-space-md, 18px) var(--yatra-space-md, 18px);
363 + border-radius: var(--yatra-radius-md, 12px);
364 + }
365 + .yatra-login-modal__btn { min-width: 0; flex: 1; }
366 + }
367 + `;
368 + document.head.appendChild(style);
167 369 }
168 370
169 371 showLoginPopup() {
170 - // Create a simple modal for login prompt
372 + this.ensureModalStyles();
373 +
374 + // Remove an existing instance first to avoid stacking modals.
375 + const existing = document.getElementById('yatra-login-modal');
376 + if (existing) existing.remove();
377 +
378 + const loginHref = this.buildLoginUrl();
379 +
171 380 const modal = document.createElement('div');
381 + modal.id = 'yatra-login-modal';
172 382 modal.className = 'yatra-login-modal';
173 - modal.innerHTML = `
174 - <div class="yatra-login-modal-overlay"></div>
175 - <div class="yatra-login-modal-content">
176 - <div class="yatra-login-prompt">
177 - <h3>Login Required</h3>
178 - <p>Please login to save trips to your wishlist.</p>
179 - <div class="yatra-login-buttons">
180 - <a href="${window.yatraAdmin?.login_url || '/wp-login.php'}" class="yatra-btn yatra-btn-primary">Login</a>
181 - <button type="button" class="yatra-btn yatra-btn-secondary yatra-close-modal">Cancel</button>
182 - </div>
183 - </div>
184 - </div>
185 - `;
186 -
187 - // Add styles
188 - modal.style.cssText = `
189 - position: fixed;
190 - top: 0;
191 - left: 0;
192 - width: 100%;
193 - height: 100%;
194 - z-index: 10000;
195 - display: flex;
196 - align-items: center;
197 - justify-content: center;
198 - `;
199 -
200 - const overlay = modal.querySelector('.yatra-login-modal-overlay');
201 - overlay.style.cssText = `
202 - position: absolute;
203 - top: 0;
204 - left: 0;
205 - width: 100%;
206 - height: 100%;
207 - background: rgba(0, 0, 0, 0.5);
208 - `;
209 -
210 - const content = modal.querySelector('.yatra-login-modal-content');
211 - content.style.cssText = `
212 - position: relative;
213 - background: white;
214 - border-radius: 8px;
215 - padding: 40px;
216 - max-width: 400px;
217 - width: 90%;
218 - text-align: center;
219 - box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
220 - `;
221 -
383 + modal.setAttribute('role', 'dialog');
384 + modal.setAttribute('aria-modal', 'true');
385 + modal.setAttribute('aria-labelledby', 'yatra-login-modal-title');
386 + modal.setAttribute('aria-describedby', 'yatra-login-modal-body');
387 +
388 + const overlay = document.createElement('div');
389 + overlay.className = 'yatra-login-modal__overlay';
390 +
391 + const panel = document.createElement('div');
392 + panel.className = 'yatra-login-modal__panel';
393 +
394 + const closeX = document.createElement('button');
395 + closeX.type = 'button';
396 + closeX.className = 'yatra-login-modal__close';
397 + closeX.setAttribute('aria-label', this.strings.cancel);
398 + closeX.innerHTML = '<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>';
399 +
400 + const iconWrap = document.createElement('div');
401 + iconWrap.className = 'yatra-login-modal__icon';
402 + iconWrap.setAttribute('aria-hidden', 'true');
403 + iconWrap.innerHTML = '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 21s-7.5-4.5-9.5-9.2C1.1 8.6 2.7 5 6.2 5c2 0 3.3 1 3.8 2 .5-1 1.8-2 3.8-2 3.5 0 5.1 3.6 3.7 6.8C19.5 16.5 12 21 12 21z"/></svg>';
404 +
405 + const title = document.createElement('h3');
406 + title.id = 'yatra-login-modal-title';
407 + title.className = 'yatra-login-modal__title';
408 + title.textContent = this.strings.loginRequired;
409 +
410 + const body = document.createElement('p');
411 + body.id = 'yatra-login-modal-body';
412 + body.className = 'yatra-login-modal__body';
413 + body.textContent = this.strings.loginPrompt;
414 +
415 + const actions = document.createElement('div');
416 + actions.className = 'yatra-login-modal__actions';
417 +
418 + const cancelBtn = document.createElement('button');
419 + cancelBtn.type = 'button';
420 + cancelBtn.className = 'yatra-login-modal__btn yatra-login-modal__btn--ghost';
421 + cancelBtn.textContent = this.strings.cancel;
422 +
423 + const loginLink = document.createElement('a');
424 + loginLink.href = loginHref;
425 + loginLink.className = 'yatra-login-modal__btn yatra-login-modal__btn--primary';
426 + loginLink.textContent = this.strings.login;
427 +
428 + // Cancel on the left, Login (primary action) on the right — matches
429 + // standard dialog conventions on web/macOS.
430 + actions.appendChild(cancelBtn);
431 + actions.appendChild(loginLink);
432 + panel.appendChild(closeX);
433 + panel.appendChild(iconWrap);
434 + panel.appendChild(title);
435 + panel.appendChild(body);
436 + panel.appendChild(actions);
437 + modal.appendChild(overlay);
438 + modal.appendChild(panel);
439 +
440 + const prevOverflow = document.body.style.overflow;
222 441 document.body.appendChild(modal);
223 442 document.body.style.overflow = 'hidden';
224 -
225 - // Handle close
443 +
226 444 const closeModal = () => {
227 445 modal.remove();
228 - document.body.style.overflow = '';
446 + document.body.style.overflow = prevOverflow;
447 + document.removeEventListener('keydown', onKey);
229 448 };
230 -
449 + const onKey = (e) => {
450 + if (e.key === 'Escape') closeModal();
451 + // Trap Tab focus within the dialog.
452 + if (e.key === 'Tab') {
453 + const focusables = [closeX, cancelBtn, loginLink];
454 + const idx = focusables.indexOf(document.activeElement);
455 + if (e.shiftKey) {
456 + if (idx <= 0) { e.preventDefault(); focusables[focusables.length - 1].focus(); }
457 + } else {
458 + if (idx === focusables.length - 1) { e.preventDefault(); focusables[0].focus(); }
459 + }
460 + }
461 + };
462 +
231 463 overlay.addEventListener('click', closeModal);
232 - modal.querySelector('.yatra-close-modal').addEventListener('click', closeModal);
464 + closeX.addEventListener('click', closeModal);
465 + cancelBtn.addEventListener('click', closeModal);
466 + document.addEventListener('keydown', onKey);
467 +
468 + // Focus the primary action so keyboard users can press Enter to log in.
469 + setTimeout(() => { try { loginLink.focus(); } catch (_e) {} }, 0);
233 470 }
234 471
235 472 showMessage(message, type, button) {
236 - // Remove existing toast if any
237 473 const existingToast = document.getElementById('yatra-wishlist-toast');
238 - if (existingToast) {
239 - existingToast.remove();
240 - }
474 + if (existingToast) existingToast.remove();
241 475
242 - // Create toast near the button
243 476 const toast = document.createElement('div');
244 477 toast.id = 'yatra-wishlist-toast';
478 + toast.setAttribute('role', type === 'error' ? 'alert' : 'status');
245 479 toast.textContent = message;
246 -
247 - // Position relative to button
480 +
248 481 const rect = button.getBoundingClientRect();
249 482 const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
250 483 const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft;
251 -
484 +
252 485 toast.style.cssText = `
253 486 position: absolute;
254 487 top: ${rect.bottom + scrollTop + 10}px;
255 488 left: ${rect.left + scrollLeft}px;
@@ -265,33 +498,25 @@
265 498 color: #ffffff;
266 499 opacity: 0;
267 500 transform: translateY(-10px);
268 501 `;
269 -
502 +
270 503 document.body.appendChild(toast);
271 -
272 - // Animate in
504 +
273 505 requestAnimationFrame(() => {
274 506 toast.style.opacity = '1';
275 507 toast.style.transform = 'translateY(0)';
276 508 });
277 509
278 - // Remove after delay
279 510 setTimeout(() => {
280 511 toast.style.opacity = '0';
281 512 toast.style.transform = 'translateY(-10px)';
282 - setTimeout(() => {
283 - if (toast.parentNode) {
284 - toast.remove();
285 - }
286 - }, 300);
513 + setTimeout(() => { if (toast.parentNode) toast.remove(); }, 300);
287 514 }, 3000);
288 515 }
289 516 }
290 517
291 -document.addEventListener('DOMContentLoaded', function() {
292 - var cfg = window.yatraWishlistConfig || {};
293 - if (!cfg.enabled) {
294 - return;
295 - }
518 +document.addEventListener('DOMContentLoaded', function () {
519 + const cfg = window.yatraWishlistConfig || {};
520 + if (!cfg.enabled) return;
296 521 window.tripListingWishlist = new TripListingWishlist();
297 522 });