PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.13
Yatra – Travel Booking & Tour Operator Software v3.0.13
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 / assets / js / trip.js

trip.js in Yatra – Travel Booking & Tour Operator Software 3.0.13, at assets/js/trip.js

5,509 lines 195.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Yatra Single Trip Page JavaScript
3 * Class-based implementation for trip page features
4 */
5 /* jshint esversion: 6 */
6
7 // Translation helpers (booking.js installs the same shims if it runs first;
8 // idempotent setup so either load order works).
9 // Parse a numeric min/max attribute while preserving a legitimate 0.
10 // `parseInt(el.getAttribute('max')) || 99` treated max="0" — what a sold-out
11 // departure renders — as "unset" and fell back to the placeholder ceiling, so
12 // the guest selector stayed usable and ignored the departure's real capacity.
13 // Only an absent or non-numeric value may fall back. Installed once and shared
14 // by every frontend script, so load order does not matter.
15 if (typeof window.yatraNumOr !== 'function') {
16 window.yatraNumOr = function (raw, fallback) {
17 var parsed = parseInt(raw, 10);
18 return isNaN(parsed) ? fallback : parsed;
19 };
20 }
21
22 (function () {
23 var i18n = (window.wp && window.wp.i18n) ? window.wp.i18n : null;
24 if (i18n && typeof i18n.__ === 'function') {
25 window.__ = function (text, domain) { return i18n.__(text, domain || 'yatra'); };
26 window._n = function (s, p, n, domain) { return i18n._n(s, p, n, domain || 'yatra'); };
27 window._x = function (text, ctx, domain) { return i18n._x(text, ctx, domain || 'yatra'); };
28 // Always bind WordPress's own sprintf (it substitutes %d/%s/%1$s).
29 // Do NOT defer to a pre-existing window.sprintf: a theme or other
30 // plugin can define an incompatible global that leaves placeholders
31 // unsubstituted (the cause of literal "%d"/"%s" in the UI).
32 if (typeof i18n.sprintf === 'function') {
33 window.sprintf = function () { return i18n.sprintf.apply(i18n, arguments); };
34 } else if (typeof window.sprintf !== 'function') {
35 window.sprintf = function (fmt) { return fmt; };
36 }
37 } else {
38 // wp.i18n not present — install passthroughs only if nothing usable
39 // exists yet (don't clobber a real binding installed earlier).
40 if (typeof window.__ !== 'function') window.__ = function (text) { return text; };
41 if (typeof window._n !== 'function') window._n = function (s, p, n) { return n === 1 ? s : p; };
42 if (typeof window._x !== 'function') window._x = function (text) { return text; };
43 if (typeof window.sprintf !== 'function') window.sprintf = function (fmt) { return fmt; };
44 }
45 })();
46
47 (function() {
48 'use strict';
49
50 /**
51 * Convert PHP date format (WordPress/Yatra) to Flatpickr format.
52 * Covers the common tokens exposed in Yatra Settings UI.
53 *
54 * @param {string} php
55 * @returns {string}
56 */
57 const phpDateFormatToFlatpickr = (php) => {
58 if (!php || typeof php !== 'string') return 'F j, Y';
59 const map = {
60 // Day
61 d: 'd',
62 j: 'j',
63 D: 'D',
64 l: 'l',
65 // Month
66 m: 'm',
67 n: 'n',
68 M: 'M',
69 F: 'F',
70 // Year
71 Y: 'Y',
72 y: 'y',
73 // Time (for any datetime pickers)
74 H: 'H',
75 G: 'H',
76 h: 'h',
77 g: 'h',
78 i: 'i',
79 s: 'S',
80 a: 'K',
81 A: 'K'
82 };
83
84 let out = '';
85 let escaping = false;
86 for (let i = 0; i < php.length; i++) {
87 const ch = php[i];
88 if (escaping) {
89 out += ch;
90 escaping = false;
91 continue;
92 }
93 if (ch === '\\') {
94 escaping = true;
95 continue;
96 }
97 out += map[ch] || ch;
98 }
99 return out || 'F j, Y';
100 };
101
102 const getTripAltDateFormat = () => {
103 const cfg = window.yatraTripData || {};
104 const php = cfg.dateFormat || cfg.date_format || 'Y-m-d';
105 return phpDateFormatToFlatpickr(php);
106 };
107
108 /**
109 * Flatpickr locale from PHP ({@see FrontendAssetsProvider::buildFlatpickrLocalePayload}).
110 * @returns {Record<string, unknown>}
111 */
112 const getFlatpickrLocale = () => {
113 const cfg = window.yatraTripData || {};
114 const l = cfg.flatpickrLocale;
115 if (!l || typeof l !== 'object') {
116 return { firstDayOfWeek: 1 };
117 }
118 const out = Object.assign({}, l);
119 if (typeof out.firstDayOfWeek !== 'number' || Number.isNaN(out.firstDayOfWeek)) {
120 out.firstDayOfWeek = 1;
121 }
122 return out;
123 };
124
125 const getRestBase = () => {
126 const siteUrl =
127 (window.yatraTripData && window.yatraTripData.siteUrl) ||
128 (window.yatraAdmin && window.yatraAdmin.siteUrl) ||
129 window.location.origin ||
130 '';
131 let base =
132 (window.yatraTripData && (window.yatraTripData.apiUrl || window.yatraTripData.restUrl)) ||
133 (window.wpApiSettings && window.wpApiSettings.root) ||
134 `${siteUrl.replace(/\/$/, '')}/wp-json`;
135 base = base.replace(/\/$/, '');
136 // Support plain permalinks via rest_route when pretty permalinks are off
137 const permalinkStructure =
138 (window.yatraTripData && window.yatraTripData.permalinkStructure) ||
139 (window.yatraAdmin && window.yatraAdmin.permalinkStructure) ||
140 '';
141 const isPlain = permalinkStructure === 'plain' || !permalinkStructure;
142 return { base, isPlain };
143 };
144
145 /**
146 * YYYY-MM from a calendar day; server maps to its locale M-Y to match card data-month.
147 * @param {string} dateStr YYYY-MM-DD
148 */
149 const monthFilterKeyFromIsoDate = (dateStr) => {
150 if (!dateStr || typeof dateStr !== 'string') {
151 return 'all';
152 }
153 const m = dateStr.match(/^(\d{4})-(\d{2})-\d{2}/);
154 if (!m) {
155 return 'all';
156 }
157 return `${m[1]}-${m[2]}`;
158 };
159
160 /**
161 * Build public REST URL for trip availability HTML (full section or card fragment).
162 * @param {string|number} tripId
163 * @param {{date?: string, month_filter?: string, sort?: string, page?: number, per_page?: number, partial?: boolean}} params
164 */
165
166 const buildAvailabilityTemplateUrl = (tripId, params = {}) => {
167 const { base: baseUrl, isPlain } = getRestBase();
168 const q = new URLSearchParams();
169 if (params.date) {
170 q.set('date', params.date);
171 }
172 const mf = params.month_filter != null && params.month_filter !== '' ? params.month_filter : 'all';
173 q.set('month_filter', mf);
174 q.set('sort', params.sort || 'date-asc');
175 q.set('page', String(Math.max(1, parseInt(params.page, 10) || 1)));
176 q.set('per_page', String(Math.min(50, Math.max(1, parseInt(params.per_page, 10) || 10))));
177 if (params.partial) {
178 q.set('partial', '1');
179 }
180 const qs = q.toString();
181 if (isPlain && baseUrl.includes('rest_route=')) {
182 return `${baseUrl}/trips/${tripId}/availability-template&${qs}`;
183 }
184 if (baseUrl.includes('/yatra/v1')) {
185 return `${baseUrl}/trips/${tripId}/availability-template?${qs}`;
186 }
187 return `${baseUrl}/yatra/v1/trips/${tripId}/availability-template?${qs}`;
188 };
189
190 // Currency formatting helper for trip page (uses localized yatraTripData)
191 if (typeof window.yatra_format_price_js !== 'function') {
192 window.yatra_format_price_js = function(amount) {
193 const cfg = window.yatraTripData || {};
194 const symbol = (typeof cfg.currencySymbol === 'string' && cfg.currencySymbol.length)
195 ? cfg.currencySymbol
196 : (typeof cfg.currency === 'string' ? cfg.currency : '$');
197
198 const position = (
199 cfg.currencyPosition ||
200 cfg.currency_position ||
201 'before'
202 ).toString().toLowerCase().trim();
203 const decimals = Number.isFinite(Number(cfg.decimalPlaces)) ? Number(cfg.decimalPlaces) : 2;
204 const thousandSep = (typeof cfg.thousandSeparator === 'string') ? cfg.thousandSeparator : ',';
205 const decimalSep = (typeof cfg.decimalSeparator === 'string') ? cfg.decimalSeparator : '.';
206
207 const num = Number(amount) || 0;
208
209 const formatted = new Intl.NumberFormat(undefined, {
210 minimumFractionDigits: decimals,
211 maximumFractionDigits: decimals,
212 }).format(num)
213 .replace(/,/g, 'TEMP_THOUSAND')
214 .replace(/\./g, 'TEMP_DECIMAL')
215 .replace(/TEMP_THOUSAND/g, thousandSep)
216 .replace(/TEMP_DECIMAL/g, decimalSep);
217
218 var pos = position;
219 if (pos === 'before') {
220 pos = 'left_space';
221 } else if (pos === 'after') {
222 pos = 'right_space';
223 }
224 if (pos === 'right') {
225 return formatted + symbol;
226 }
227 if (pos === 'right_space') {
228 return formatted + ' ' + symbol;
229 }
230 if (pos === 'left') {
231 return symbol + formatted;
232 }
233 return symbol + ' ' + formatted;
234 };
235 }
236
237 /**
238 * Resolve the real image URL from an <img> element.
239 *
240 * Caching / performance plugins (LiteSpeed Cache, WP Rocket, W3 Total Cache,
241 * Autoptimize, etc.) replace the `src` attribute with a tiny base64
242 * placeholder and store the real URL in a data-* attribute. We check our
243 * own `data-yatra-src` first (added in PHP templates and never touched by
244 * any third-party plugin), then fall back to the most common lazy-load
245 * attribute names, and finally fall back to `img.src` while filtering out
246 * obvious placeholders so we never pass a base64 GIF to the modal.
247 */
248 function getImageSrc(img) {
249 // 1. Our own reliable attribute – always present on Yatra gallery images.
250 const yatra = img.getAttribute('data-yatra-src');
251 if (yatra) return yatra;
252
253 // 2. Common lazy-load attributes used by popular caching plugins.
254 const lazyAttrs = [
255 'data-src', // LiteSpeed Cache, WP Rocket, Smush, BJ Lazy Load
256 'data-lazyload', // LiteSpeed Cache (older attribute)
257 'data-lazy-src', // WordPress core lazy-load
258 'data-original', // jQuery.lazyLoad plugin
259 'data-pagespeed-lazy-src', // Google PageSpeed / mod_pagespeed
260 'data-lazy', // Jetpack
261 'data-full-size', // Yatra itinerary images
262 ];
263 for (const attr of lazyAttrs) {
264 const val = img.getAttribute(attr);
265 if (val && !val.startsWith('data:')) return val;
266 }
267
268 // 3. Native src – only use if it is not a placeholder.
269 const src = img.src || '';
270 if (src && !src.startsWith('data:')) return src;
271
272 return '';
273 }
274
275 /**
276 * Gallery Modal Class
277 * Handles image gallery modal functionality
278 */
279 class GalleryModal {
280 constructor() {
281 this.modal = null;
282 this.images = [];
283 this.currentIndex = 0;
284 this.init();
285 }
286
287 init() {
288 if (document.readyState === 'loading') {
289 document.addEventListener('DOMContentLoaded', () => this.setup());
290 } else {
291 this.setup();
292 }
293 }
294
295 setup() {
296 // Wait for DOM to be ready if needed
297 if (document.readyState === 'loading') {
298 document.addEventListener('DOMContentLoaded', () => this.initializeGallery());
299 } else {
300 this.initializeGallery();
301 }
302 }
303
304 initializeGallery() {
305 this.modal = document.getElementById('hero-gallery');
306 if (!this.modal) {
307 console.warn('Hero gallery modal not found');
308 return;
309 }
310
311 this.collectImages();
312 this.createThumbnails();
313 this.attachEventListeners();
314 this.attachItineraryVideoListeners();
315 }
316
317 collectImages() {
318 this.images = [];
319
320 // Helper: deduplicate by resolved URL.
321 const add = (img, fallbackAlt) => {
322 const src = getImageSrc(img);
323 if (!src) return;
324 if (!this.images.some(i => i.src === src)) {
325 this.images.push({ src, alt: img.alt || fallbackAlt });
326 }
327 };
328
329 // 1. Prefer the JSON media-data blob (never touched by caching plugins).
330 const mediaScript = document.getElementById('yatra-hero-media-data');
331 if (mediaScript) {
332 try {
333 const mediaData = JSON.parse(mediaScript.textContent || '{}');
334 const imageUrls = Array.isArray(mediaData.images) ? mediaData.images : [];
335 imageUrls.forEach((url, index) => {
336 if (url && typeof url === 'string' && !this.images.some(i => i.src === url)) {
337 this.images.push({
338 src: url,
339 // Indexed alt text — sprintf isn't guaranteed
340 // available on every page, so we replace() the
341 // %d placeholder ourselves. Keeps the msgid a
342 // literal so makepot extracts cleanly.
343 /* translators: %d is the 1-based image index in the gallery. */
344 alt: __('Gallery Image %d', 'yatra').replace('%d', String(index + 1)),
345 });
346 }
347 });
348 } catch (e) {
349 // JSON parse error – fall through to DOM-based collection.
350 }
351 }
352
353 // 2. Collect from hero slides (covers any images not in the JSON blob).
354 document.querySelectorAll('.yatra-trip-hero-slide img').forEach((img, index) => {
355 /* translators: %d is the 1-based image index in the gallery. */
356 add(img, __('Gallery Image %d', 'yatra').replace('%d', String(index + 1)));
357 });
358
359 // 3. Collect from gallery section (hero gallery only, NOT itinerary).
360 document.querySelectorAll('.yatra-trip-gallery .yatra-gallery-item img').forEach((img) => {
361 add(img, __('Gallery Image', 'yatra'));
362 });
363
364 // 4. Collect from hero main image.
365 const heroMainImg = document.querySelector('.yatra-hero-main-img');
366 if (heroMainImg) add(heroMainImg, __('Main Image', 'yatra'));
367
368 // 5. Collect from side images.
369 document.querySelectorAll('.yatra-side-image-item img').forEach((img) => {
370 add(img, __('Side Image', 'yatra'));
371 });
372 }
373
374 createThumbnails() {
375 const container = this.modal.querySelector('.yatra-gallery-thumbnails-container');
376 if (!container) return;
377
378 container.innerHTML = '';
379 const totalCount = this.modal.querySelector('.yatra-gallery-total-count');
380 if (totalCount) {
381 totalCount.textContent = this.images.length;
382 }
383
384 this.images.forEach((img, index) => {
385 const thumbnail = document.createElement('div');
386 thumbnail.className = 'yatra-gallery-thumbnail';
387 thumbnail.setAttribute('data-index', index);
388 if (index === 0) thumbnail.classList.add('active');
389
390 const thumbnailImg = document.createElement('img');
391 thumbnailImg.src = img.src;
392 thumbnailImg.alt = img.alt;
393 thumbnail.appendChild(thumbnailImg);
394
395 thumbnail.addEventListener('click', () => this.showImage(index));
396 container.appendChild(thumbnail);
397 });
398 }
399
400 showImage(index) {
401 if (index < 0 || index >= this.images.length) return;
402
403 this.currentIndex = index;
404 const image = this.images[index];
405 const modalImage = this.modal.querySelector('.yatra-gallery-modal-image');
406 const modalLoader = this.modal.querySelector('.yatra-gallery-modal-loader');
407 const currentIndexSpan = this.modal.querySelector('.yatra-gallery-current-index');
408 const prevBtn = this.modal.querySelector('.yatra-gallery-modal-prev');
409 const nextBtn = this.modal.querySelector('.yatra-gallery-modal-next');
410
411 if (currentIndexSpan) {
412 currentIndexSpan.textContent = index + 1;
413 }
414
415 // Update thumbnails
416 const thumbnails = this.modal.querySelectorAll('.yatra-gallery-thumbnail');
417 thumbnails.forEach((thumb, i) => {
418 thumb.classList.toggle('active', i === index);
419 });
420
421 // Show loader
422 if (modalLoader) modalLoader.classList.add('active');
423 if (modalImage) modalImage.classList.remove('loaded');
424
425 // Load image
426 const img = new Image();
427 img.onload = () => {
428 if (modalImage) {
429 modalImage.src = image.src;
430 modalImage.alt = image.alt;
431 modalImage.classList.add('loaded');
432 }
433 if (modalLoader) modalLoader.classList.remove('active');
434 };
435 img.onerror = () => {
436 if (modalLoader) modalLoader.classList.remove('active');
437 if (modalImage) modalImage.src = image.src;
438 };
439 img.src = image.src;
440
441 // Update button states
442 if (prevBtn) {
443 prevBtn.style.opacity = index === 0 ? '0.5' : '1';
444 prevBtn.style.pointerEvents = index === 0 ? 'none' : 'auto';
445 }
446 if (nextBtn) {
447 nextBtn.style.opacity = index === this.images.length - 1 ? '0.5' : '1';
448 nextBtn.style.pointerEvents = index === this.images.length - 1 ? 'none' : 'auto';
449 }
450 }
451
452 open(startIndex = 0) {
453 if (this.images.length === 0) return;
454 this.modal.classList.add('active');
455 document.body.style.overflow = 'hidden';
456 this.showImage(startIndex);
457 }
458
459 close() {
460 this.modal.classList.remove('active');
461 document.body.style.overflow = '';
462 }
463
464 next() {
465 if (this.currentIndex < this.images.length - 1) {
466 this.showImage(this.currentIndex + 1);
467 }
468 }
469
470 prev() {
471 if (this.currentIndex > 0) {
472 this.showImage(this.currentIndex - 1);
473 }
474 }
475
476 attachItineraryVideoListeners() {
477 // This method is called from setup() but the actual event listeners
478 // are handled in the inline script in content-itinerary.php
479 // This is just a placeholder to prevent errors
480 }
481
482 attachEventListeners() {
483 // Close button
484 const closeBtn = this.modal.querySelector('.yatra-gallery-modal-close');
485 if (closeBtn) {
486 closeBtn.addEventListener('click', () => this.close());
487 }
488
489 // Overlay
490 const overlay = this.modal.querySelector('.yatra-gallery-modal-overlay');
491 if (overlay) {
492 overlay.addEventListener('click', () => this.close());
493 }
494
495 // Navigation buttons
496 const prevBtn = this.modal.querySelector('.yatra-gallery-modal-prev');
497 const nextBtn = this.modal.querySelector('.yatra-gallery-modal-next');
498 if (prevBtn) prevBtn.addEventListener('click', () => this.prev());
499 if (nextBtn) nextBtn.addEventListener('click', () => this.next());
500
501 // Keyboard navigation
502 document.addEventListener('keydown', (e) => {
503 if (!this.modal.classList.contains('active')) return;
504 if (e.key === 'Escape') this.close();
505 if (e.key === 'ArrowLeft') this.prev();
506 if (e.key === 'ArrowRight') this.next();
507 });
508
509 // Open from play button
510 const playButton = document.querySelector('.yatra-hero-gallery-play');
511 const galleryPlayBtn = document.querySelector('.yatra-gallery-play-btn');
512 if (playButton) {
513 playButton.addEventListener('click', () => this.open(0));
514 }
515 if (galleryPlayBtn) {
516 galleryPlayBtn.addEventListener('click', () => this.open(0));
517 }
518
519 // Open from gallery items
520 const galleryItems = document.querySelectorAll('.yatra-trip-gallery .yatra-gallery-item');
521 galleryItems.forEach((item, index) => {
522 item.addEventListener('click', () => {
523 const img = item.querySelector('img');
524 if (img) {
525 const src = getImageSrc(img);
526 const imageIndex = this.images.findIndex(i => i.src === src);
527 this.open(imageIndex >= 0 ? imageIndex : index);
528 }
529 });
530 });
531
532 // Open from side images
533 const sideImages = document.querySelectorAll('.yatra-side-image-item');
534 sideImages.forEach((item) => {
535 item.addEventListener('click', (e) => {
536 if (e.target.closest('.yatra-favorite-btn') || e.target.closest('.yatra-view-all-photos-btn')) {
537 return;
538 }
539 const img = item.querySelector('img');
540 if (img) {
541 const src = getImageSrc(img);
542 const imageIndex = this.images.findIndex(i => i.src === src);
543 this.open(imageIndex >= 0 ? imageIndex : 0);
544 }
545 });
546 });
547
548 // Open from main image link
549 const mainImageLink = document.querySelector('.yatra-hero-main-img-link');
550 if (mainImageLink) {
551 mainImageLink.addEventListener('click', (e) => {
552 // Check if gallery should be prevented (for non-image media types)
553 if (mainImageLink.getAttribute('data-prevent-gallery') === 'true') {
554 // Don't prevent default - let the overlay handle the click
555 return;
556 }
557 e.preventDefault();
558 this.open(0); // Open with featured image as first image
559 });
560 }
561 }
562 }
563
564 /**
565 * Itinerary Gallery Modal Class
566 * Separate gallery modal for itinerary section - independent from hero gallery.
567 *
568 * Scoping: a single modal element is reused, but the image set is RECOMPUTED on
569 * every click from the *closest* `.yatra-entry-gallery` ancestor. Clicking a
570 * thumbnail in Day 2 must open only Day 2's images (starting at the clicked one) —
571 * never the global pool of every day's images. The hidden
572 * `.yatra-itinerary-trip-gallery` div on the page is for SEO / structured data
573 * only; it intentionally no longer drives the modal.
574 */
575 class ItineraryGalleryModal {
576 constructor() {
577 this.modal = null;
578 this.images = []; // currently-visible image set (the active scope)
579 this.currentIndex = 0;
580 this.init();
581 }
582
583 init() {
584 if (document.readyState === 'loading') {
585 document.addEventListener('DOMContentLoaded', () => this.setup());
586 } else {
587 this.setup();
588 }
589 }
590
591 setup() {
592 this.modal = document.getElementById('itinerary-gallery');
593 if (!this.modal) {
594 console.warn('Itinerary gallery modal not found');
595 return;
596 }
597 this.attachEventListeners();
598 }
599
600 /**
601 * Build the image list from a scope element (an `.yatra-entry-gallery` block).
602 * Falls back to the document if no scope is given (preserves the legacy
603 * "global pool" behaviour for the SEO-only thumbnails clicker).
604 */
605 collectImagesFrom(scope) {
606 const root = scope || document;
607 const images = [];
608
609 const add = (img, fallbackAlt) => {
610 const src = img.getAttribute('data-full-size') || getImageSrc(img);
611 if (!src) return;
612 if (!images.some(i => i.src === src)) {
613 images.push({ src, alt: img.alt || fallbackAlt });
614 }
615 };
616
617 // Visible gallery thumbnails inside the scope
618 root.querySelectorAll('.yatra-media-card img').forEach((img) => {
619 add(img, sprintf(
620 /* translators: %d: gallery image sequence number. */
621 __('Itinerary Gallery Image %d', 'yatra'),
622 images.length + 1
623 ));
624 });
625
626 // Video links inside the scope (fallback selector)
627 root.querySelectorAll('.yatra-itinerary-video-link img').forEach((img) => {
628 add(img, sprintf(
629 /* translators: %d: itinerary video sequence number. */
630 __('Itinerary Video %d', 'yatra'),
631 images.length + 1
632 ));
633 });
634
635 // If still empty AND we were given no scope, fall back to the hidden global pool
636 // so legacy click handlers on `.yatra-itinerary-trip-gallery .yatra-gallery-item`
637 // continue to function.
638 if (images.length === 0 && !scope) {
639 document.querySelectorAll('.yatra-itinerary-trip-gallery .yatra-gallery-item img').forEach((img) => {
640 add(img, sprintf(
641 /* translators: %d: itinerary image sequence number. */
642 __('Itinerary Image %d', 'yatra'),
643 images.length + 1
644 ));
645 });
646 }
647
648 return images;
649 }
650
651 /** Replace the active image set and rebuild thumbnails. */
652 setActiveImages(images) {
653 this.images = images;
654 this.currentIndex = 0;
655 this.createThumbnails();
656 }
657
658 createThumbnails() {
659 const container = this.modal.querySelector('.yatra-gallery-thumbnails-container');
660 if (!container) return;
661
662 container.innerHTML = '';
663 const totalCount = this.modal.querySelector('.yatra-gallery-total-count');
664 if (totalCount) {
665 totalCount.textContent = this.images.length;
666 }
667
668 this.images.forEach((image, index) => {
669 const thumb = document.createElement('div');
670 thumb.className = 'yatra-gallery-thumbnail';
671 thumb.innerHTML = `<img src="${image.src}" alt="${image.alt}">`;
672 thumb.addEventListener('click', () => this.showImage(index));
673 container.appendChild(thumb);
674 });
675 }
676
677 showImage(index) {
678 if (index < 0 || index >= this.images.length) return;
679
680 this.currentIndex = index;
681 const image = this.images[index];
682 const modalImage = this.modal.querySelector('.yatra-gallery-modal-image');
683 const modalLoader = this.modal.querySelector('.yatra-gallery-modal-loader');
684 const currentIndexSpan = this.modal.querySelector('.yatra-gallery-current-index');
685 const prevBtn = this.modal.querySelector('.yatra-gallery-modal-prev');
686 const nextBtn = this.modal.querySelector('.yatra-gallery-modal-next');
687
688 if (currentIndexSpan) {
689 currentIndexSpan.textContent = index + 1;
690 }
691
692 // Show loader
693 if (modalLoader) modalLoader.classList.add('active');
694 if (modalImage) modalImage.classList.remove('loaded');
695
696 // Load image
697 const img = new Image();
698 img.onload = () => {
699 if (modalImage) {
700 modalImage.src = image.src;
701 modalImage.alt = image.alt;
702 modalImage.classList.add('loaded');
703 }
704 if (modalLoader) modalLoader.classList.remove('active');
705 };
706 img.onerror = () => {
707 if (modalLoader) modalLoader.classList.remove('active');
708 if (modalImage) modalImage.src = image.src;
709 };
710 img.src = image.src;
711
712 // Update button states
713 if (prevBtn) {
714 prevBtn.style.opacity = index === 0 ? '0.5' : '1';
715 prevBtn.style.pointerEvents = index === 0 ? 'none' : 'auto';
716 }
717 if (nextBtn) {
718 nextBtn.style.opacity = index === this.images.length - 1 ? '0.5' : '1';
719 nextBtn.style.pointerEvents = index === this.images.length - 1 ? 'none' : 'auto';
720 }
721
722 // Update thumbnails
723 const thumbnails = this.modal.querySelectorAll('.yatra-gallery-thumbnail');
724 thumbnails.forEach((thumb, i) => {
725 thumb.classList.toggle('active', i === index);
726 });
727 }
728
729 open(startIndex = 0) {
730 if (this.images.length === 0) return;
731 this.modal.classList.add('active');
732 document.body.style.overflow = 'hidden';
733 this.showImage(startIndex);
734 }
735
736 close() {
737 this.modal.classList.remove('active');
738 document.body.style.overflow = '';
739 }
740
741 next() {
742 if (this.currentIndex < this.images.length - 1) {
743 this.showImage(this.currentIndex + 1);
744 }
745 }
746
747 prev() {
748 if (this.currentIndex > 0) {
749 this.showImage(this.currentIndex - 1);
750 }
751 }
752
753 attachEventListeners() {
754 // Close button
755 const closeBtn = this.modal.querySelector('.yatra-gallery-modal-close');
756 if (closeBtn) {
757 closeBtn.addEventListener('click', () => this.close());
758 }
759
760 // Overlay
761 const overlay = this.modal.querySelector('.yatra-gallery-modal-overlay');
762 if (overlay) {
763 overlay.addEventListener('click', () => this.close());
764 }
765
766 // Navigation buttons
767 const prevBtn = this.modal.querySelector('.yatra-gallery-modal-prev');
768 const nextBtn = this.modal.querySelector('.yatra-gallery-modal-next');
769 if (prevBtn) prevBtn.addEventListener('click', () => this.prev());
770 if (nextBtn) nextBtn.addEventListener('click', () => this.next());
771
772 // Keyboard navigation
773 document.addEventListener('keydown', (e) => {
774 if (!this.modal.classList.contains('active')) return;
775 if (e.key === 'Escape') this.close();
776 if (e.key === 'ArrowLeft') this.prev();
777 if (e.key === 'ArrowRight') this.next();
778 });
779
780 // Itinerary day gallery thumbnails — SCOPED to the clicked day.
781 // Every click recomputes the image set from the closest .yatra-entry-gallery so
782 // Day 2's modal only ever shows Day 2's photos, starting at the clicked one.
783 // Video links are intentionally NOT captured here — they have their own handler
784 // that opens YatraVideoPlayer (see further down in this file).
785 const onItineraryClick = (e) => {
786 const link = e.target.closest('.yatra-itinerary-gallery-link');
787 if (!link) return;
788 // Don't hijack video link clicks even if they happen to match
789 if (link.classList.contains('yatra-itinerary-video-link')) return;
790 const scope = link.closest('.yatra-entry-gallery');
791 if (!scope) return;
792
793 e.preventDefault();
794
795 const scopedImages = this.collectImagesFrom(scope);
796 if (scopedImages.length === 0) return;
797
798 // Find the clicked image's index within the scoped set
799 const clickedImg = link.querySelector('img');
800 const clickedSrc = clickedImg
801 ? (clickedImg.getAttribute('data-full-size') || getImageSrc(clickedImg))
802 : null;
803 const startIndex = clickedSrc
804 ? Math.max(0, scopedImages.findIndex((i) => i.src === clickedSrc))
805 : 0;
806
807 this.setActiveImages(scopedImages);
808 this.open(startIndex);
809 };
810 // Delegated listener — works even if entries are added/removed dynamically
811 document.addEventListener('click', onItineraryClick);
812
813 // Legacy: clicks on the hidden SEO pool (kept for back-compat). These open
814 // the full pool because there's no per-day scope on the SEO container.
815 const galleryItems = document.querySelectorAll('.yatra-itinerary-trip-gallery .yatra-gallery-item');
816 galleryItems.forEach((item, index) => {
817 item.addEventListener('click', () => {
818 const allImages = this.collectImagesFrom(null); // fall back to global
819 if (allImages.length === 0) return;
820 const img = item.querySelector('img');
821 const src = img
822 ? (img.getAttribute('data-full-size') || getImageSrc(img))
823 : null;
824 const startIndex = src
825 ? Math.max(0, allImages.findIndex((i) => i.src === src))
826 : index;
827 this.setActiveImages(allImages);
828 this.open(startIndex);
829 });
830 });
831 }
832 }
833
834 /**
835 * Hero Media Switcher Class
836 * Handles media type switching in the hero section
837 */
838 class HeroMediaSwitcher {
839 constructor() {
840 this.currentMediaType = 'images';
841 this.mediaData = null;
842 this.init();
843 }
844
845 init() {
846 if (document.readyState === 'loading') {
847 document.addEventListener('DOMContentLoaded', () => this.setup());
848 } else {
849 this.setup();
850 }
851 }
852
853 setup() {
854 // Get media data from JSON script
855 const mediaDataScript = document.getElementById('yatra-hero-media-data');
856 if (!mediaDataScript) return;
857
858 try {
859 this.mediaData = JSON.parse(mediaDataScript.textContent);
860 } catch (e) {
861 console.error('Error parsing media data:', e);
862 return;
863 }
864
865 this.attachEventListeners();
866 }
867
868 attachEventListeners() {
869 // Media switcher buttons
870 const switcherBtns = document.querySelectorAll('.yatra-media-switcher-btn');
871 switcherBtns.forEach(btn => {
872 btn.addEventListener('click', (e) => {
873 const mediaType = e.currentTarget.dataset.media;
874 this.switchMediaType(mediaType);
875 });
876 });
877
878 // Media badges click
879 const mediaBadges = document.querySelectorAll('.yatra-media-badge');
880 mediaBadges.forEach(badge => {
881 badge.addEventListener('click', (e) => {
882 const badgeType = e.currentTarget.classList.contains('yatra-media-badge-image') ? 'images' :
883 e.currentTarget.classList.contains('yatra-media-badge-video') ? 'videos' :
884 e.currentTarget.classList.contains('yatra-media-badge-youtube') ? 'youtube' :
885 e.currentTarget.classList.contains('yatra-media-badge-tour') ? 'tours' : 'documents';
886 this.switchMediaType(badgeType);
887 });
888 });
889 }
890
891 switchMediaType(mediaType) {
892 if (this.currentMediaType === mediaType) return;
893
894 this.currentMediaType = mediaType;
895 this.updateSwitcherButtons();
896 this.updateMediaBadgesVisibility();
897 this.updateMainContent();
898 this.updateSideContent();
899 this.updateSideImagesVisibility();
900 }
901
902 updateSwitcherButtons() {
903 const switcherBtns = document.querySelectorAll('.yatra-media-switcher-btn');
904 switcherBtns.forEach(btn => {
905 if (btn.dataset.media === this.currentMediaType) {
906 btn.classList.add('yatra-media-switcher-active');
907 } else {
908 btn.classList.remove('yatra-media-switcher-active');
909 }
910 });
911 }
912
913 updateMediaBadgesVisibility() {
914 const badges = {
915 image: document.querySelector('.yatra-media-badge-image'),
916 video: document.querySelector('.yatra-media-badge-video'),
917 youtube: document.querySelector('.yatra-media-badge-youtube'),
918 tour: document.querySelector('.yatra-media-badge-tour'),
919 document: document.querySelector('.yatra-media-badge-document')
920 };
921
922 // Hide all badges first
923 Object.values(badges).forEach(badge => {
924 if (badge) badge.style.display = 'none';
925 });
926
927 // Show only the active media type badge
928 switch (this.currentMediaType) {
929 case 'images':
930 if (badges.image) badges.image.style.display = 'inline-flex';
931 break;
932 case 'videos':
933 if (badges.video) badges.video.style.display = 'inline-flex';
934 break;
935 case 'youtube':
936 if (badges.youtube) badges.youtube.style.display = 'inline-flex';
937 break;
938 case 'tours':
939 if (badges.tour) badges.tour.style.display = 'inline-flex';
940 break;
941 case 'documents':
942 if (badges.document) badges.document.style.display = 'inline-flex';
943 break;
944 }
945 }
946
947 updateMainContent() {
948 const mainImageLink = document.querySelector('.yatra-hero-main-img-link');
949 const mainImage = document.querySelector('.yatra-hero-main-img');
950
951 if (!mainImage) return;
952
953 switch (this.currentMediaType) {
954 case 'videos':
955 this.showVideoContent(mainImageLink, mainImage);
956 break;
957 case 'youtube':
958 this.showYoutubeContent(mainImageLink, mainImage);
959 break;
960 case 'tours':
961 this.showTourContent(mainImageLink, mainImage);
962 break;
963 case 'documents':
964 this.showDocumentContent(mainImageLink, mainImage);
965 break;
966 default: // images
967 this.showImageContent(mainImageLink, mainImage);
968 }
969 }
970
971 showImageContent(link, img) {
972 const images = this.mediaData.images || [];
973
974 // Remove any existing overlays (YouTube, tour, document, etc.)
975 const container = img.parentElement;
976 if (container) {
977 const existingOverlay = container.querySelector('.yatra-media-overlay');
978 if (existingOverlay) {
979 existingOverlay.remove();
980 }
981 }
982
983 if (images.length > 0) {
984 img.src = images[0];
985 img.alt = __('Trip Image 1', 'yatra');
986 if (link) {
987 link.href = '#';
988 link.dataset.imageIndex = '0';
989 }
990 }
991 }
992
993 showVideoContent(link, img) {
994 const videos = this.mediaData.videos || [];
995
996 // Remove any existing overlays from other media types
997 const container = img.parentElement;
998 if (container) {
999 const existingOverlay = container.querySelector('.yatra-media-overlay');
1000 if (existingOverlay) {
1001 existingOverlay.remove();
1002 }
1003 }
1004
1005 if (videos.length > 0) {
1006 const video = videos[0];
1007 // Show video thumbnail
1008 img.src = video.thumbnail || img.src; // Keep current image if no thumbnail
1009 img.alt = video.title || 'Trip Video';
1010
1011 // Add play button overlay
1012 this.addPlayButtonOverlay(img.parentElement);
1013
1014 if (link) {
1015 link.href = video.url || '#';
1016 link.dataset.videoUrl = video.url;
1017 link.dataset.videoTitle = video.title || '';
1018
1019 // Prevent link from opening in new tab
1020 link.addEventListener('click', (e) => {
1021 e.preventDefault();
1022 return false;
1023 });
1024 }
1025 }
1026 }
1027
1028 showYoutubeContent(link, img) {
1029 const youtubeVideos = this.mediaData.youtube_videos || [];
1030
1031 if (youtubeVideos.length > 0) {
1032 const video = youtubeVideos[0];
1033
1034 // Prevent link from opening in new tab
1035 if (link) {
1036 link.addEventListener('click', (e) => {
1037 e.preventDefault();
1038 return false;
1039 });
1040 }
1041
1042 // Show YouTube thumbnail
1043 img.src = video.thumbnail || img.src; // Keep current image if no thumbnail
1044 img.alt = video.title || 'YouTube Video';
1045
1046 // Add YouTube play button overlay
1047 this.addYoutubePlayButtonOverlay(img.parentElement, video);
1048
1049 if (link) {
1050 link.href = video.url || '#';
1051 link.dataset.youtubeUrl = video.url;
1052 link.dataset.youtubeVideoId = video.video_id || '';
1053 link.dataset.youtubeTitle = video.title || '';
1054 link.dataset.embedUrl = video.embed_url || '';
1055 }
1056 } else {
1057 console.warn('No YouTube videos available');
1058 }
1059 }
1060
1061 showTourContent(link, img) {
1062 const tours = this.mediaData.virtual_tours || [];
1063
1064 if (tours.length > 0) {
1065 const tour = tours[0];
1066
1067 // Prevent link from opening in new tab
1068 if (link) {
1069 link.addEventListener('click', (e) => {
1070 e.preventDefault();
1071 return false;
1072 });
1073 }
1074 // Show tour thumbnail or keep current image
1075 if (tour.thumbnail) {
1076 img.src = tour.thumbnail;
1077 }
1078 img.alt = tour.title || '360° Virtual Tour';
1079
1080 // Add 360° tour overlay
1081 this.addTourOverlay(img.parentElement, tour);
1082
1083 if (link) {
1084 link.href = tour.url || '#';
1085 link.dataset.tourUrl = tour.url;
1086 link.dataset.tourTitle = tour.title || '';
1087 link.dataset.tourType = tour.tour_type || '360';
1088 link.dataset.isEmbeddable = tour.is_embeddable || 'false';
1089 }
1090 }
1091 }
1092
1093 showDocumentContent(link, img) {
1094 const documents = this.mediaData.documents || [];
1095 if (documents.length > 0) {
1096 const doc = documents[0];
1097 // Keep current image for document display
1098 img.alt = doc.title || 'Trip Document';
1099
1100 // Add document icon overlay
1101 this.addDocumentIconOverlay(img.parentElement, doc);
1102
1103 if (link) {
1104 link.href = doc.url || '#';
1105 link.dataset.documentUrl = doc.url;
1106 link.dataset.documentTitle = doc.title || '';
1107 link.dataset.downloadable = doc.is_downloadable || 'false';
1108 }
1109 }
1110 }
1111
1112 addPlayButtonOverlay(container) {
1113 // Remove existing overlays
1114 const existingOverlay = container.querySelector('.yatra-media-overlay');
1115 if (existingOverlay) existingOverlay.remove();
1116
1117 const overlay = document.createElement('div');
1118 overlay.className = 'yatra-media-overlay yatra-media-play-overlay';
1119 overlay.innerHTML = `
1120 <div class="yatra-play-button">
1121 <svg width="60" height="60" viewBox="0 0 24 24" fill="none">
1122 <circle cx="12" cy="12" r="10" fill="rgba(0,0,0,0.6)"/>
1123 <path d="M10 8l6 4-6 4V8z" fill="white"/>
1124 </svg>
1125 </div>
1126 `;
1127
1128 // Add click handler to play video using YatraVideoPlayer module
1129 overlay.addEventListener('click', (e) => {
1130 // Only handle real user clicks, not programmatic ones
1131 if (!e.isTrusted) {
1132 return;
1133 }
1134
1135 e.preventDefault();
1136 e.stopPropagation();
1137 e.stopImmediatePropagation();
1138
1139 // Get the video data from the current media type
1140 const videos = this.mediaData.videos || [];
1141 if (videos.length > 0 && window.YatraVideoPlayer) {
1142 window.YatraVideoPlayer.play(videos[0]);
1143 } else {
1144 console.error('Cannot play video - missing video data or YatraVideoPlayer', {
1145 hasVideo: videos.length > 0,
1146 hasPlayer: !!window.YatraVideoPlayer
1147 });
1148 }
1149 }, { capture: false });
1150
1151 container.appendChild(overlay);
1152 }
1153
1154 addYoutubePlayButtonOverlay(container, video) {
1155 // Remove existing overlays
1156 const existingOverlay = container.querySelector('.yatra-media-overlay');
1157 if (existingOverlay) existingOverlay.remove();
1158
1159 const overlay = document.createElement('div');
1160 overlay.className = 'yatra-media-overlay yatra-media-youtube-overlay';
1161 overlay.innerHTML = `
1162 <div class="yatra-youtube-play-button">
1163 <svg width="100" height="100" viewBox="0 0 100 100" fill="none">
1164 <circle cx="50" cy="50" r="45" fill="rgba(255, 0, 0, 0.9)" stroke="white" stroke-width="3"/>
1165 <path d="M40 30 L70 50 L40 70 Z" fill="white"/>
1166 </svg>
1167 </div>
1168 `;
1169
1170 // Add click handler to open YouTube video using YatraVideoPlayer module
1171 overlay.addEventListener('click', (e) => {
1172 // Only handle real user clicks, not programmatic ones
1173 if (!e.isTrusted) {
1174
1175 return;
1176 }
1177
1178 e.preventDefault();
1179 e.stopPropagation();
1180 e.stopImmediatePropagation();
1181
1182
1183
1184 if (video.url && window.YatraVideoPlayer) {
1185
1186 window.YatraVideoPlayer.play(video);
1187 } else {
1188 console.error('Cannot play video - missing url or YatraVideoPlayer', {
1189 hasUrl: !!video.url,
1190 hasPlayer: !!window.YatraVideoPlayer
1191 });
1192 }
1193 }, { capture: false });
1194
1195 container.appendChild(overlay);
1196 }
1197
1198 addTourOverlay(container, tour) {
1199 // Remove existing overlays
1200 const existingOverlay = container.querySelector('.yatra-media-overlay');
1201 if (existingOverlay) existingOverlay.remove();
1202
1203 const overlay = document.createElement('div');
1204 overlay.className = 'yatra-media-overlay yatra-media-tour-overlay';
1205 const tourFallbackTitle = __('360° Virtual Tour', 'yatra');
1206 /* translators: %s: tour type (e.g. "360°"). */
1207 const tourTypeLabel = sprintf(__('%s Experience', 'yatra'), tour.tour_type || '360°');
1208 const tourActionLabel = __('Click to explore', 'yatra');
1209 overlay.innerHTML = `
1210 <div class="yatra-tour-info">
1211 <div class="yatra-tour-icon">
1212 <svg width="80" height="80" viewBox="0 0 100 100" fill="none">
1213 <circle cx="50" cy="50" r="45" fill="rgba(16, 185, 129, 0.9)" stroke="white" stroke-width="3"/>
1214 <circle cx="50" cy="50" r="35" fill="none" stroke="white" stroke-width="2" opacity="0.4"/>
1215 <circle cx="50" cy="50" r="25" fill="none" stroke="white" stroke-width="2" opacity="0.4"/>
1216 <path d="M 50 5 Q 75 50 50 95" stroke="white" stroke-width="2" fill="none" opacity="0.4"/>
1217 <path d="M 50 5 Q 25 50 50 95" stroke="white" stroke-width="2" fill="none" opacity="0.4"/>
1218 <line x1="5" y1="50" x2="95" y2="50" stroke="white" stroke-width="2" opacity="0.4"/>
1219 <text x="50" y="58" text-anchor="middle" fill="white" font-size="24" font-weight="bold">360°</text>
1220 </svg>
1221 </div>
1222 <div class="yatra-tour-details">
1223 <div class="yatra-tour-title">${tour.title || tourFallbackTitle}</div>
1224 <div class="yatra-tour-type">${tourTypeLabel}</div>
1225 <div class="yatra-tour-action">${tourActionLabel}</div>
1226 </div>
1227 </div>
1228 `;
1229
1230 // Add click handler to open virtual tour using YatraTourViewer module
1231 overlay.addEventListener('click', (e) => {
1232 // Only handle real user clicks, not programmatic ones
1233 if (!e.isTrusted) {
1234
1235 return;
1236 }
1237
1238 e.preventDefault();
1239 e.stopPropagation();
1240 e.stopImmediatePropagation();
1241
1242
1243
1244 if (tour.url && window.YatraTourViewer) {
1245
1246 window.YatraTourViewer.view(tour);
1247 } else {
1248 console.error('Cannot view tour - missing url or YatraTourViewer', {
1249 hasUrl: !!tour.url,
1250 hasViewer: !!window.YatraTourViewer
1251 });
1252 }
1253 }, { capture: false });
1254
1255 container.appendChild(overlay);
1256 }
1257
1258 addDocumentIconOverlay(container, doc) {
1259 // Remove existing overlays
1260 const existingOverlay = container.querySelector('.yatra-media-overlay');
1261 if (existingOverlay) existingOverlay.remove();
1262
1263 const overlay = document.createElement('div');
1264 overlay.className = 'yatra-media-overlay yatra-media-document-overlay';
1265 const documentFallbackTitle = __('Document', 'yatra');
1266 overlay.innerHTML = `
1267 <div class="yatra-document-info">
1268 <div class="yatra-document-icon">
1269 <svg width="48" height="48" viewBox="0 0 24 24" fill="none">
1270 <path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8l-6-6z" fill="rgba(255,255,255,0.9)"/>
1271 <path d="M14 2v6h6" fill="rgba(255,255,255,0.7)"/>
1272 <path d="M16 13H8" stroke="#374151" stroke-width="2"/>
1273 <path d="M16 17H8" stroke="#374151" stroke-width="2"/>
1274 </svg>
1275 </div>
1276 <div class="yatra-document-details">
1277 <div class="yatra-document-title">${doc.title || documentFallbackTitle}</div>
1278 <div class="yatra-document-size">${this.formatFileSize(doc.file_size || 0)}</div>
1279 </div>
1280 </div>
1281 `;
1282 container.appendChild(overlay);
1283 }
1284
1285 formatFileSize(bytes) {
1286 if (bytes === 0) return '0 ' + __('Bytes', 'yatra');
1287 const k = 1024;
1288 const sizes = [__('Bytes', 'yatra'), 'KB', 'MB', 'GB'];
1289 const i = Math.floor(Math.log(bytes) / Math.log(k));
1290 return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
1291 }
1292
1293 updateSideContent() {
1294 const sideImages = document.querySelectorAll('.yatra-side-image-item');
1295
1296 switch (this.currentMediaType) {
1297 case 'videos':
1298 this.updateSideVideos(sideImages);
1299 break;
1300 case 'youtube':
1301 this.updateSideYoutubeVideos(sideImages);
1302 break;
1303 case 'tours':
1304 this.updateSideTours(sideImages);
1305 break;
1306 case 'documents':
1307 this.updateSideDocuments(sideImages);
1308 break;
1309 default: // images
1310 this.updateSideImages(sideImages);
1311 }
1312 }
1313
1314 updateSideImages(sideImages) {
1315 const images = this.mediaData.images || [];
1316 sideImages.forEach((item, index) => {
1317 const img = item.querySelector('img');
1318 if (img && images[index + 1]) {
1319 img.src = images[index + 1];
1320 /* translators: %d: image sequence number in the trip gallery. */
1321 img.alt = sprintf(__('Trip Image %d', 'yatra'), index + 2);
1322 }
1323 });
1324 }
1325
1326 updateSideVideos(sideImages) {
1327 const videos = this.mediaData.videos || [];
1328 const fallbackVideoTitle = __('Trip Video', 'yatra');
1329 sideImages.forEach((item, index) => {
1330 const img = item.querySelector('img');
1331 if (img && videos[index + 1]) {
1332 const video = videos[index + 1];
1333 if (video.thumbnail) {
1334 img.src = video.thumbnail;
1335 }
1336 img.alt = video.title || fallbackVideoTitle;
1337 }
1338 });
1339 }
1340
1341 updateSideYoutubeVideos(sideImages) {
1342 const youtubeVideos = this.mediaData.youtube_videos || [];
1343 const fallbackYoutubeTitle = __('YouTube Video', 'yatra');
1344 sideImages.forEach((item, index) => {
1345 const img = item.querySelector('img');
1346 if (img && youtubeVideos[index + 1]) {
1347 const video = youtubeVideos[index + 1];
1348 if (video.thumbnail) {
1349 img.src = video.thumbnail;
1350 }
1351 img.alt = video.title || fallbackYoutubeTitle;
1352 }
1353 });
1354 }
1355
1356 updateSideTours(sideImages) {
1357 const virtualTours = this.mediaData.virtual_tours || [];
1358 const fallbackTourTitle = __('360° Virtual Tour', 'yatra');
1359 sideImages.forEach((item, index) => {
1360 const img = item.querySelector('img');
1361 if (img && virtualTours[index + 1]) {
1362 const tour = virtualTours[index + 1];
1363 if (tour.thumbnail) {
1364 img.src = tour.thumbnail;
1365 }
1366 img.alt = tour.title || fallbackTourTitle;
1367 }
1368 });
1369 }
1370
1371 updateSideDocuments(sideImages) {
1372 const documents = this.mediaData.documents || [];
1373 const fallbackDocumentTitle = __('Trip Document', 'yatra');
1374 sideImages.forEach((item, index) => {
1375 const img = item.querySelector('img');
1376 if (img && documents[index + 1]) {
1377 const doc = documents[index + 1];
1378 // Keep existing image for documents
1379 img.alt = doc.title || fallbackDocumentTitle;
1380 }
1381 });
1382 }
1383
1384 updateSideImagesVisibility() {
1385 const sideImagesContainer = document.querySelector('.yatra-hero-side-images');
1386 const heroImagesContainer = document.querySelector('.yatra-hero-images');
1387 const mainImageLink = document.querySelector('.yatra-hero-main-img-link');
1388
1389 if (!sideImagesContainer) return;
1390
1391 // Hide side images for non-image media types and make main image full width
1392 if (this.currentMediaType === 'images') {
1393 sideImagesContainer.classList.remove('yatra-media-non-image');
1394 if (heroImagesContainer) {
1395 heroImagesContainer.classList.remove('yatra-media-full-width');
1396 }
1397 // Re-enable gallery click for images
1398 if (mainImageLink) {
1399 mainImageLink.removeAttribute('data-prevent-gallery');
1400 }
1401 } else {
1402 sideImagesContainer.classList.add('yatra-media-non-image');
1403 if (heroImagesContainer) {
1404 heroImagesContainer.classList.add('yatra-media-full-width');
1405 }
1406 // Mark to prevent gallery opening for non-image media
1407 if (mainImageLink) {
1408 mainImageLink.setAttribute('data-prevent-gallery', 'true');
1409 }
1410 }
1411 }
1412 }
1413
1414 /**
1415 * Booking Sidebar Class
1416 * Handles booking form interactions (Check Availability & Make Enquiry buttons)
1417 * Also handles date picker and travelers selector for main booking form
1418 */
1419 class BookingSidebar {
1420 constructor() {
1421 this.checkAvailabilityBtn = null;
1422 this.makeEnquiryBtn = null;
1423 this.heroBookNowBtn = null;
1424 this.dateInput = null;
1425 this.participantsSelect = null;
1426 this.datepickerInstance = null;
1427 this.init();
1428 }
1429
1430 init() {
1431 if (document.readyState === 'loading') {
1432 document.addEventListener('DOMContentLoaded', () => this.setup());
1433 } else {
1434 this.setup();
1435 }
1436 }
1437
1438 setup() {
1439 this.checkAvailabilityBtn = document.getElementById('check-availability-btn');
1440 this.makeEnquiryBtn = document.getElementById('open-enquiry-modal');
1441 this.heroBookNowBtn = document.getElementById('yatra-hero-book-now-btn');
1442 this.dateInput = document.getElementById('travel_date');
1443 this.participantsSelect = document.querySelector('.yatra-participants-select');
1444
1445 if (!this.checkAvailabilityBtn) {
1446 console.warn('Check Availability button not found');
1447 }
1448
1449 this.attachEventListeners();
1450 this.initDateField();
1451 this.initTravelersField();
1452 }
1453
1454 attachEventListeners() {
1455 // Check Availability button
1456 if (this.checkAvailabilityBtn) {
1457 this.checkAvailabilityBtn.addEventListener('click', (e) => {
1458 e.preventDefault();
1459 this.handleCheckAvailability();
1460 });
1461 }
1462
1463 // Hero "Book Now" (should behave like Check Availability)
1464 if (this.heroBookNowBtn) {
1465 this.heroBookNowBtn.addEventListener('click', (e) => {
1466 e.preventDefault();
1467 const disabled =
1468 this.heroBookNowBtn.classList.contains('is-disabled') ||
1469 this.heroBookNowBtn.getAttribute('aria-disabled') === 'true';
1470 if (disabled) return;
1471
1472 const bookingSidebar = document.getElementById('booking') || document.querySelector('.yatra-trip-sidebar');
1473 if (bookingSidebar && bookingSidebar.scrollIntoView) {
1474 bookingSidebar.scrollIntoView({ behavior: 'smooth', block: 'start' });
1475 }
1476
1477 // Trigger the same availability flow
1478 if (this.checkAvailabilityBtn) {
1479 this.handleCheckAvailability();
1480 }
1481 });
1482 }
1483
1484 // Make Enquiry button
1485 if (this.makeEnquiryBtn) {
1486 this.makeEnquiryBtn.addEventListener('click', (e) => {
1487 e.preventDefault();
1488 this.handleMakeEnquiry();
1489 });
1490 }
1491 }
1492
1493 initDateField() {
1494 if (!this.dateInput) return;
1495
1496 // Pro "date-as-dropdown" mode renders a <select> instead of a flatpickr
1497 // input. Wire it up as a native dropdown — emit `change` on selection so
1498 // the existing Check Availability listener picks the date up — and skip
1499 // the rest of the flatpickr setup.
1500 if (this.dateInput.tagName === 'SELECT') {
1501 const setEnabled = () => {
1502 if (this.checkAvailabilityBtn) {
1503 const hasValue = !!this.dateInput.value;
1504 this.checkAvailabilityBtn.disabled = !hasValue;
1505 this.checkAvailabilityBtn.classList.toggle('disabled', !hasValue);
1506 }
1507 };
1508 setEnabled();
1509 this.dateInput.addEventListener('change', setEnabled);
1510 return;
1511 }
1512
1513 if (typeof flatpickr === 'undefined') return;
1514
1515 const today = new Date();
1516 today.setHours(0, 0, 0, 0);
1517
1518 // Format today's date as Y-m-d
1519 const todayStr = today.getFullYear() + '-' +
1520 String(today.getMonth() + 1).padStart(2, '0') + '-' +
1521 String(today.getDate()).padStart(2, '0');
1522
1523 // Set today's date as default value
1524 this.dateInput.value = todayStr;
1525
1526 // Enable button since we have a default date
1527 if (this.checkAvailabilityBtn) {
1528 this.checkAvailabilityBtn.disabled = false;
1529 this.checkAvailabilityBtn.classList.remove('disabled');
1530 }
1531
1532 // Destroy existing instance if any
1533 if (this.dateInput._flatpickr) {
1534 this.dateInput._flatpickr.destroy();
1535 }
1536
1537 const altFormat = getTripAltDateFormat();
1538
1539 this.datepickerInstance = flatpickr(this.dateInput, {
1540 // Keep backend value stable (Y-m-d), only change display
1541 dateFormat: 'Y-m-d',
1542 altInput: true,
1543 altFormat,
1544 minDate: today,
1545 defaultDate: today,
1546 enableTime: false,
1547 clickOpens: true,
1548 allowInput: false,
1549 static: false,
1550 monthSelectorType: 'static',
1551 animate: true,
1552 locale: getFlatpickrLocale(),
1553 onChange: (selectedDates, dateStr) => {
1554 this.dateInput.value = dateStr;
1555
1556 // Enable Check Availability button when date is selected
1557 if (this.checkAvailabilityBtn && dateStr) {
1558 this.checkAvailabilityBtn.disabled = false;
1559 this.checkAvailabilityBtn.classList.remove('disabled');
1560 }
1561 }
1562 });
1563
1564 // Make container clickable
1565 const container = this.dateInput.closest('.yatra-booking-field-select');
1566 if (container) {
1567 container.style.cursor = 'pointer';
1568 const openPicker = (e) => {
1569 // Do not interfere with other actionable controls in the same row
1570 if (e && e.target && e.target.closest && e.target.closest('button, a, select, textarea')) {
1571 return;
1572 }
1573 // Prevent container clicks from triggering other toggles
1574 if (e) {
1575 e.preventDefault();
1576 e.stopPropagation();
1577 }
1578 if (this.datepickerInstance && !this.datepickerInstance.isOpen) {
1579 this.datepickerInstance.open();
1580 }
1581 };
1582
1583 // Container click (icon/arrow/background)
1584 container.addEventListener('click', openPicker);
1585
1586 // Flatpickr altInput is the visible field when altInput:true
1587 if (this.dateInput && this.dateInput._flatpickr && this.dateInput._flatpickr.altInput) {
1588 this.dateInput._flatpickr.altInput.addEventListener('click', openPicker);
1589 }
1590 }
1591 }
1592
1593 initInlineQuantityControls() {
1594 // Handle inline quantity controls for simple booking (num_travelers)
1595 const inlineControls = document.querySelectorAll('.yatra-quantity-controls-inline');
1596
1597 inlineControls.forEach(controlsContainer => {
1598 const input = controlsContainer.querySelector('.yatra-quantity-input-simple');
1599 if (!input) return;
1600
1601 const minusBtn = controlsContainer.querySelector('.yatra-quantity-minus');
1602 const plusBtn = controlsContainer.querySelector('.yatra-quantity-plus');
1603
1604 if (!minusBtn || !plusBtn) return;
1605
1606 const updateButtonStates = () => {
1607 const current = parseInt(input.value) || 1;
1608 const min = window.yatraNumOr(input.getAttribute('min'), 1);
1609 const max = window.yatraNumOr(input.getAttribute('max'), 20);
1610
1611 minusBtn.disabled = current <= min;
1612 plusBtn.disabled = current >= max;
1613 };
1614
1615 // Minus button click
1616 minusBtn.addEventListener('click', (e) => {
1617 e.preventDefault();
1618 e.stopPropagation();
1619
1620 const current = parseInt(input.value) || 1;
1621 const min = window.yatraNumOr(input.getAttribute('min'), 1);
1622
1623 if (current > min) {
1624 input.value = current - 1;
1625 updateButtonStates();
1626 this.updateTotalDisplay();
1627 }
1628 });
1629
1630 // Plus button click
1631 plusBtn.addEventListener('click', (e) => {
1632 e.preventDefault();
1633 e.stopPropagation();
1634
1635 const current = parseInt(input.value) || 1;
1636 const max = window.yatraNumOr(input.getAttribute('max'), 20);
1637
1638 if (current < max) {
1639 input.value = current + 1;
1640 updateButtonStates();
1641 this.updateTotalDisplay();
1642 }
1643 });
1644
1645 // Initialize button states
1646 updateButtonStates();
1647 });
1648 }
1649
1650 updateTotalDisplay() {
1651 // Update total price display based on current travelers count
1652 const numTravelersInput = document.getElementById('num_travelers');
1653 const totalAmountEl = document.getElementById('total-amount');
1654 const displayPriceEl = document.getElementById('display-price');
1655
1656 if (!numTravelersInput) return;
1657
1658 const travelers = parseInt(numTravelersInput.value) || 1;
1659 const pricePerPerson = parseFloat(numTravelersInput.getAttribute('data-price')) || 0;
1660 const totalPrice = travelers * pricePerPerson;
1661
1662 // Update total display if exists
1663 if (totalAmountEl && typeof yatra_format_price_js === 'function') {
1664 totalAmountEl.textContent = yatra_format_price_js(totalPrice);
1665 }
1666 }
1667
1668 initTravelersField() {
1669 // First, initialize inline quantity controls for num_travelers (simple booking)
1670 this.initInlineQuantityControls();
1671
1672 // Initialize all dropdown travelers fields (there may be multiple instances)
1673 const participantsSelects = document.querySelectorAll('.yatra-participants-select');
1674
1675 participantsSelects.forEach((participantsSelect) => {
1676 this.initSingleTravelersField(participantsSelect);
1677 });
1678 }
1679
1680 initSingleTravelersField(participantsSelect) {
1681 if (!participantsSelect) return;
1682
1683 // Find display and selector within this specific participants select container
1684 const display = participantsSelect.querySelector('.yatra-participants-display');
1685 const selector = participantsSelect.querySelector('.yatra-booking-quantity-selector');
1686
1687 if (!display || !selector) return;
1688
1689 // Get all traveler category inputs dynamically
1690 const travelerInputs = selector.querySelectorAll('input[id^="traveler_"], input[id^="mobile_traveler_"]');
1691
1692 if (travelerInputs.length === 0) return;
1693
1694 // Make display clickable
1695 display.style.cursor = 'pointer';
1696
1697 // Toggle dropdown
1698 participantsSelect.onclick = (e) => {
1699 if (e.target.closest('.yatra-quantity-btn')) return;
1700 if (selector.contains(e.target)) return;
1701 e.preventDefault();
1702 e.stopPropagation();
1703 participantsSelect.classList.toggle('active');
1704 };
1705
1706 // Update display function - dynamically build text from all traveler inputs
1707 const updateDisplay = () => {
1708 const parts = [];
1709 travelerInputs.forEach((input) => {
1710 const value = parseInt(input.value || 0);
1711 if (value > 0) {
1712 const categoryLabel = input.getAttribute('data-category-label') || __('Traveler', 'yatra');
1713 parts.push(`${categoryLabel} x ${value}`);
1714 }
1715 });
1716
1717 if (parts.length === 0) {
1718 // Default to first category with value 1. `data-category-label`
1719 // is rendered server-side via the PHP textdomain, so it's
1720 // already translated. The "x 1" suffix is the only piece
1721 // we need to keep translatable here.
1722 const firstInput = travelerInputs[0];
1723 if (firstInput) {
1724 const categoryLabel = firstInput.getAttribute('data-category-label') || __('Traveler', 'yatra');
1725 /* translators: %s is the traveler-category label (e.g. "Adult", "Child"). Renders as "Adult x 1". */
1726 display.textContent = __('%s x 1', 'yatra').replace('%s', categoryLabel);
1727 } else {
1728 display.textContent = __('1 Traveler', 'yatra');
1729 }
1730 } else {
1731 display.textContent = parts.join(', ');
1732 }
1733 };
1734
1735 // Quantity button handlers
1736 selector.onclick = (e) => {
1737 const btn = e.target.closest('.yatra-quantity-btn');
1738 if (!btn) return;
1739
1740 e.preventDefault();
1741 e.stopPropagation();
1742
1743 const targetId = btn.getAttribute('data-target');
1744 let input = null;
1745 if (targetId) {
1746 try {
1747 input = selector.querySelector('#' + CSS.escape(targetId));
1748 } catch (_) {
1749 // ignore
1750 }
1751 if (!input) {
1752 input = document.getElementById(targetId);
1753 }
1754 }
1755 if (!input) return;
1756
1757 const current = parseInt(input.value || 0);
1758 const min = parseInt(input.getAttribute('min') || 0);
1759 const max = parseInt(input.getAttribute('max') || 999);
1760 const isPlus = btn.classList.contains('yatra-quantity-plus');
1761 const isMinus = btn.classList.contains('yatra-quantity-minus');
1762
1763 let newValue = current;
1764 if (isPlus && current < max) {
1765 newValue = current + 1;
1766 } else if (isMinus && current > min) {
1767 newValue = current - 1;
1768 }
1769
1770 if (newValue !== current) {
1771 input.value = newValue;
1772 updateDisplay();
1773
1774 // Update button states
1775 const row = input.closest('.yatra-quantity-row');
1776 if (row) {
1777 const minusBtn = row.querySelector('.yatra-quantity-minus');
1778 const plusBtn = row.querySelector('.yatra-quantity-plus');
1779 if (minusBtn) minusBtn.disabled = newValue <= min;
1780 if (plusBtn) plusBtn.disabled = newValue >= max;
1781 }
1782
1783 // Sync to availability cards
1784 syncToAvailabilityCards(travelerInputs);
1785 }
1786 };
1787
1788 // Function to sync sidebar travelers to availability cards
1789 const syncToAvailabilityCards = (inputs) => {
1790 const availabilitySection = document.getElementById('availability');
1791 if (!availabilitySection) return;
1792
1793 inputs.forEach((input) => {
1794 const categoryId = input.id.replace(/^mobile_/, '').replace('traveler_', '');
1795 const value = parseInt(input.value) || 0;
1796
1797 // Find all matching inputs in availability cards
1798 const availInputs = availabilitySection.querySelectorAll(`.yatra-availability-category[data-category="${categoryId}"]`);
1799 availInputs.forEach((availInput) => {
1800 availInput.value = value;
1801
1802 // Update button states
1803 const row = availInput.closest('.yatra-quantity-row');
1804 if (row) {
1805 const min = parseInt(availInput.getAttribute('min') || 0);
1806 const max = parseInt(availInput.getAttribute('max') || 999);
1807 const minusBtn = row.querySelector('.yatra-quantity-minus');
1808 const plusBtn = row.querySelector('.yatra-quantity-plus');
1809 if (minusBtn) minusBtn.disabled = value <= min;
1810 if (plusBtn) plusBtn.disabled = value >= max;
1811 }
1812
1813 // Update display for this card
1814 const participantsSelect = availInput.closest('.yatra-availability-participants');
1815 if (participantsSelect) {
1816 const display = participantsSelect.querySelector('.yatra-availability-participants-display');
1817 const categoryInputs = participantsSelect.querySelectorAll('.yatra-availability-category');
1818 if (display && categoryInputs.length > 0) {
1819 const parts = [];
1820 categoryInputs.forEach((catInput) => {
1821 const catValue = parseInt(catInput.value) || 0;
1822 if (catValue > 0) {
1823 const catRow = catInput.closest('.yatra-quantity-row');
1824 const categoryLabel = catRow ? catRow.querySelector('.yatra-quantity-title')?.textContent : __('Traveler', 'yatra');
1825 parts.push(`${categoryLabel} x ${catValue}`);
1826 }
1827 });
1828 if (parts.length > 0) {
1829 display.textContent = parts.join(', ');
1830 }
1831 }
1832 }
1833 });
1834 });
1835 if (window.availabilitySection && typeof window.availabilitySection.updateAllCardTotals === 'function') {
1836 window.availabilitySection.updateAllCardTotals();
1837 }
1838 };
1839
1840 // Close dropdown when clicking outside
1841 const closeHandler = (e) => {
1842 if (!participantsSelect.contains(e.target)) {
1843 participantsSelect.classList.remove('active');
1844 }
1845 };
1846 document.addEventListener('click', closeHandler);
1847
1848 // Initialize display
1849 updateDisplay();
1850
1851 // Initialize button states for all traveler inputs
1852 travelerInputs.forEach((input) => {
1853 const value = parseInt(input.value || 0);
1854 const min = parseInt(input.getAttribute('min') || 0);
1855 const max = parseInt(input.getAttribute('max') || 999);
1856 const row = input.closest('.yatra-quantity-row');
1857 if (row) {
1858 const minusBtn = row.querySelector('.yatra-quantity-minus');
1859 const plusBtn = row.querySelector('.yatra-quantity-plus');
1860 if (minusBtn) minusBtn.disabled = value <= min;
1861 if (plusBtn) plusBtn.disabled = value >= max;
1862 }
1863 });
1864 }
1865
1866 handleCheckAvailability() {
1867
1868
1869 // Get form values - support regular pricing (num_travelers), old structure (adults/children), and traveler-based pricing
1870 const dateInput = document.getElementById('travel_date');
1871 const numTravelersInput = document.getElementById('num_travelers');
1872 const adultsInput = document.getElementById('adults');
1873 const childrenInput = document.getElementById('children');
1874
1875 // Check for traveler-based pricing inputs (dynamic category inputs)
1876 const travelerInputs = document.querySelectorAll('input[id^="traveler_"]');
1877
1878 const date = dateInput ? dateInput.value : '';
1879
1880 // Check for travelers - prioritize traveler-based pricing, then num_travelers, then adults/children
1881 let totalTravelers = 0;
1882 if (travelerInputs.length > 0) {
1883 // Traveler-based pricing: sum all category inputs
1884 travelerInputs.forEach((input) => {
1885 totalTravelers += parseInt(input.value) || 0;
1886 });
1887 } else if (numTravelersInput) {
1888 // Regular pricing: single number input
1889 totalTravelers = parseInt(numTravelersInput.value) || 0;
1890 } else {
1891 // Old structure: adults + children
1892 const adults = adultsInput ? parseInt(adultsInput.value) || 0 : 0;
1893 const children = childrenInput ? parseInt(childrenInput.value) || 0 : 0;
1894 totalTravelers = adults + children;
1895 }
1896
1897
1898
1899 // Basic validation
1900 if (!date) {
1901 alert(__('Please select a travel date', 'yatra'));
1902 return;
1903 }
1904
1905 if (totalTravelers === 0) {
1906 alert(__('Please select number of travelers', 'yatra'));
1907 return;
1908 }
1909
1910 // Show loading state
1911 if (this.checkAvailabilityBtn && !this.checkAvailabilityBtn.dataset.originalHtml) {
1912 this.checkAvailabilityBtn.dataset.originalHtml = this.checkAvailabilityBtn.innerHTML;
1913 }
1914 if (this.checkAvailabilityBtn) {
1915 // Spinner SVG is fixed markup (not translatable). Only the
1916 // user-visible "Checking..." label runs through __() so it
1917 // ends up in the .pot with a clean msgid and translates with
1918 // the rest of the UI.
1919 this.checkAvailabilityBtn.innerHTML =
1920 '<svg class="animate-spin" width="18" height="18" fill="none" viewBox="0 0 24 24">' +
1921 '<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" opacity="0.25"></circle>' +
1922 '<path fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>' +
1923 '</svg> ' + __('Checking...', 'yatra');
1924 this.checkAvailabilityBtn.disabled = true;
1925 }
1926
1927 // Get trip ID from data attribute or global variable
1928 const tripId = this.checkAvailabilityBtn?.dataset?.tripId ||
1929 document.querySelector('[data-trip-id]')?.dataset?.tripId ||
1930 (window.yatraTripData && window.yatraTripData.tripId) ||
1931 1; // Fallback to 1 for dummy data
1932
1933 // Check if availability section already exists
1934 let availabilitySection = document.getElementById('availability');
1935
1936 if (availabilitySection) {
1937 // Section already loaded, reload it with new date filter
1938 this.reloadAvailabilityWithDate(date, tripId);
1939 return;
1940 }
1941
1942 const monthKey = monthFilterKeyFromIsoDate(date);
1943 const restUrl = buildAvailabilityTemplateUrl(tripId, {
1944 date,
1945 month_filter: monthKey,
1946 sort: 'date-asc',
1947 page: 1,
1948 per_page: 10,
1949 });
1950
1951 fetch(restUrl, {
1952 method: 'GET',
1953 headers: {
1954 'Content-Type': 'application/json',
1955 },
1956 // Public endpoint: do not send cookies/nonces
1957 credentials: 'omit',
1958 })
1959 .then(response => {
1960
1961 if (!response.ok) {
1962 // Try to get error message from response
1963 return response.json().then(err => {
1964 console.error('API Error:', err);
1965 throw new Error(err.message || __('Failed to fetch availability', 'yatra'));
1966 }).catch(() => {
1967 // translators: %d: HTTP status code
1968 throw new Error(sprintf(__('Failed to fetch availability (Status: %d)', 'yatra'), response.status));
1969 });
1970 }
1971 return response.json();
1972 })
1973 .then(data => {
1974
1975 if (!data || !data.html) {
1976 throw new Error('No HTML returned from server');
1977 }
1978
1979 // Find the currently active section (first visible section or overview)
1980 const activeSection = this.findActiveSection();
1981
1982 // Create a temporary container to parse HTML
1983 const tempDiv = document.createElement('div');
1984 tempDiv.innerHTML = data.html;
1985 const newSection = tempDiv.firstElementChild;
1986
1987 if (!newSection) {
1988 throw new Error('Invalid HTML structure');
1989 }
1990
1991 // Insert before the active section
1992 if (activeSection && activeSection.parentNode) {
1993 activeSection.parentNode.insertBefore(newSection, activeSection);
1994 } else {
1995 // Fallback: insert at the end of main content
1996 const mainContent = document.querySelector('.yatra-trip-main');
1997 if (mainContent) {
1998 mainContent.appendChild(newSection);
1999 }
2000 }
2001
2002 // Add menu item to sticky nav at the same position
2003 // Pass the activeSection so we can find the corresponding menu item
2004 this.addAvailabilityMenuItem(activeSection);
2005
2006 // Initialize availability section handlers
2007 if (window.availabilitySection) {
2008 window.availabilitySection.setup();
2009 } else {
2010 // Initialize if not already done
2011 window.availabilitySection = new AvailabilitySection();
2012 window.availabilitySection.setup();
2013 }
2014
2015 // Re-setup sticky nav to include the new menu item
2016 if (window.stickyNav) {
2017 window.stickyNav.setup();
2018 }
2019
2020 // Scroll to the new section
2021 this.scrollToAvailability(newSection);
2022
2023 // Auto-select the card matching the selected date
2024 this.autoSelectDateCard(date);
2025
2026 // Sync sidebar travelers to cards
2027 this.syncSidebarTravelersToCards();
2028
2029 // Reset button state
2030 this.resetButton();
2031 })
2032 .catch(error => {
2033 console.error('Error loading availability:', error);
2034 alert(__('Failed to load availability. Please try again.', 'yatra'));
2035 this.resetButton();
2036 });
2037 }
2038
2039 findActiveSection() {
2040 // Find the first visible section or default to overview
2041 const sections = document.querySelectorAll('.yatra-trip-section');
2042 for (let section of sections) {
2043 const rect = section.getBoundingClientRect();
2044 if (rect.top >= 0 && rect.top < window.innerHeight) {
2045 return section;
2046 }
2047 }
2048 // Default to overview section
2049 return document.getElementById('overview') || sections[0];
2050 }
2051
2052 addAvailabilityMenuItem(activeSection) {
2053 const stickyNav = document.querySelector('.yatra-sticky-nav-container');
2054 if (!stickyNav) return;
2055
2056 // Check if menu item already exists
2057 if (document.querySelector('.yatra-sticky-nav-item[href="#availability"]')) {
2058 return;
2059 }
2060
2061 // Find the corresponding menu item for the active section
2062 let insertBeforeItem = null;
2063 let activeSectionId = null;
2064
2065 if (activeSection) {
2066 activeSectionId = activeSection.id;
2067 if (activeSectionId) {
2068 // Find the menu item that links to this section
2069 insertBeforeItem = document.querySelector('.yatra-sticky-nav-item[href="#' + activeSectionId + '"]');
2070 }
2071 }
2072
2073 // If no active section or menu item found, default to inserting after "What's Included"
2074 if (!insertBeforeItem) {
2075 insertBeforeItem = document.querySelector('.yatra-sticky-nav-item[href="#included"]');
2076 if (insertBeforeItem) {
2077 insertBeforeItem = insertBeforeItem.nextSibling; // Insert after "What's Included"
2078 }
2079 }
2080
2081 // Create new menu item
2082 const newItem = document.createElement('a');
2083 newItem.href = '#availability';
2084 newItem.className = 'yatra-sticky-nav-item';
2085 const availabilityLabel = __('Availability', 'yatra');
2086 newItem.innerHTML = `
2087 <svg class="yatra-icon-sm" fill="none" stroke="currentColor" viewBox="0 0 24 24">
2088 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>
2089 </svg>
2090 <span>${availabilityLabel}</span>
2091 `;
2092
2093 // Insert before the target item (or after "What's Included" if no target)
2094 if (insertBeforeItem && insertBeforeItem.parentNode) {
2095 insertBeforeItem.parentNode.insertBefore(newItem, insertBeforeItem);
2096 } else {
2097 // Fallback: append to the end
2098 stickyNav.appendChild(newItem);
2099 }
2100
2101 // Update sticky nav to handle new item
2102 if (window.stickyNav) {
2103 // Add availability to sections array if not already there
2104 // Insert it in the correct position based on where the section was inserted
2105 if (!window.stickyNav.sections.includes('availability')) {
2106 if (activeSectionId) {
2107 const activeIndex = window.stickyNav.sections.indexOf(activeSectionId);
2108 if (activeIndex >= 0) {
2109 // Insert availability before the active section
2110 window.stickyNav.sections.splice(activeIndex, 0, 'availability');
2111 } else {
2112 window.stickyNav.sections.push('availability');
2113 }
2114 } else {
2115 window.stickyNav.sections.push('availability');
2116 }
2117 }
2118 // Re-setup to attach event listeners to new menu item
2119 window.stickyNav.setup();
2120 }
2121 }
2122
2123 scrollToAvailability(section) {
2124 setTimeout(() => {
2125 const offset = 150;
2126 const elementPosition = section.getBoundingClientRect().top;
2127 const offsetPosition = elementPosition + window.pageYOffset - offset;
2128
2129 window.scrollTo({
2130 top: offsetPosition,
2131 behavior: 'smooth'
2132 });
2133
2134 // Update sticky nav active state after scroll completes
2135 setTimeout(() => {
2136 if (window.stickyNav) {
2137 window.stickyNav.updateActiveNav();
2138 }
2139 }, 300); // Wait for smooth scroll to complete
2140 }, 100);
2141 }
2142
2143 resetButton() {
2144 if (this.checkAvailabilityBtn) {
2145 const originalHtml = this.checkAvailabilityBtn.dataset.originalHtml;
2146 if (originalHtml) {
2147 this.checkAvailabilityBtn.innerHTML = originalHtml;
2148 }
2149 this.checkAvailabilityBtn.disabled = false;
2150 }
2151 }
2152
2153 reloadAvailabilityWithDate(date, tripId) {
2154 // Show loading state
2155 if (this.checkAvailabilityBtn && !this.checkAvailabilityBtn.dataset.originalHtml) {
2156 this.checkAvailabilityBtn.dataset.originalHtml = this.checkAvailabilityBtn.innerHTML;
2157 }
2158 if (this.checkAvailabilityBtn) {
2159 this.checkAvailabilityBtn.innerHTML = '<svg class="animate-spin" width="18" height="18" fill="none" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" opacity="0.25"></circle><path fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path></svg> ' + __('Loading...', 'yatra');
2160 this.checkAvailabilityBtn.disabled = true;
2161 }
2162
2163 const availSection = document.getElementById('availability');
2164 const listEl = availSection ? availSection.querySelector('.yatra-availability-list') : null;
2165 const sortKey = listEl?.dataset?.sort || 'date-asc';
2166 const monthKey = monthFilterKeyFromIsoDate(date);
2167
2168 const restUrl = buildAvailabilityTemplateUrl(tripId, {
2169 date,
2170 month_filter: monthKey,
2171 sort: sortKey,
2172 page: 1,
2173 per_page: 10,
2174 });
2175
2176 fetch(restUrl, {
2177 method: 'GET',
2178 headers: {
2179 'Content-Type': 'application/json',
2180 },
2181 credentials: 'omit',
2182 })
2183 .then(response => {
2184 if (!response.ok) {
2185 throw new Error(__('Failed to fetch availability', 'yatra'));
2186 }
2187 return response.json();
2188 })
2189 .then(data => {
2190 if (!data || !data.html) {
2191 throw new Error('No HTML returned from server');
2192 }
2193
2194 // Replace existing availability section
2195 const existingSection = document.getElementById('availability');
2196 if (existingSection) {
2197 const tempDiv = document.createElement('div');
2198 tempDiv.innerHTML = data.html;
2199 const newSection = tempDiv.firstElementChild;
2200
2201 if (newSection) {
2202 existingSection.parentNode.replaceChild(newSection, existingSection);
2203
2204 // Re-initialize availability section handlers
2205 if (window.availabilitySection) {
2206 window.availabilitySection.setup();
2207 }
2208
2209 // Scroll to section
2210 this.scrollToAvailability(newSection);
2211
2212 // Sync sidebar travelers to cards
2213 this.syncSidebarTravelersToCards();
2214 }
2215 }
2216
2217 // Reset button state
2218 this.resetButton();
2219 })
2220 .catch(error => {
2221 console.error('Error reloading availability:', error);
2222 alert(__('Failed to reload availability. Please try again.', 'yatra'));
2223 this.resetButton();
2224 });
2225 }
2226
2227 autoSelectDateCard(selectedDate) {
2228 if (!selectedDate) return;
2229
2230 // Wait a bit for the availability section to render
2231 setTimeout(() => {
2232 const availabilityCards = document.querySelectorAll('.yatra-availability-card');
2233 if (!availabilityCards.length) return;
2234
2235 let matchingCard = null;
2236 let nearestCard = null;
2237 let minDiff = Infinity;
2238
2239 const selectedTime = new Date(selectedDate).getTime();
2240
2241 // Find exact match or nearest date
2242 availabilityCards.forEach(card => {
2243 const cardDate = card.dataset.date;
2244 if (!cardDate) return;
2245
2246 if (cardDate === selectedDate) {
2247 matchingCard = card;
2248 } else {
2249 const cardTime = new Date(cardDate).getTime();
2250 const diff = Math.abs(cardTime - selectedTime);
2251 if (diff < minDiff) {
2252 minDiff = diff;
2253 nearestCard = card;
2254 }
2255 }
2256 });
2257
2258 // Use exact match if found, otherwise use nearest
2259 const cardToOpen = matchingCard || nearestCard;
2260
2261 if (cardToOpen) {
2262 // Open the card
2263 const toggle = cardToOpen.querySelector('.yatra-availability-toggle');
2264 if (toggle && !cardToOpen.classList.contains('open')) {
2265 toggle.click();
2266 }
2267
2268 // Scroll to the card
2269 setTimeout(() => {
2270 const offset = 150;
2271 const elementPosition = cardToOpen.getBoundingClientRect().top;
2272 const offsetPosition = elementPosition + window.pageYOffset - offset;
2273
2274 window.scrollTo({
2275 top: offsetPosition,
2276 behavior: 'smooth'
2277 });
2278 }, 300);
2279 }
2280 }, 500);
2281 }
2282
2283 updateSidebarForDate(dateStr) {
2284 if (!dateStr) return;
2285
2286 // Get trip ID
2287 const tripId = this.checkAvailabilityBtn?.dataset?.tripId ||
2288 document.querySelector('[data-trip-id]')?.dataset?.tripId ||
2289 (window.yatraTripData && window.yatraTripData.tripId);
2290
2291 if (!tripId) return;
2292
2293 // Build REST URL
2294 const { base: baseUrl, isPlain } = getRestBase();
2295
2296 let restUrl;
2297 if (isPlain && !baseUrl.includes('/yatra/v1')) {
2298 restUrl = `${baseUrl}/?rest_route=/yatra/v1/trips/${tripId}/date-pricing&date=${dateStr}`;
2299 } else if (baseUrl.includes('/yatra/v1')) {
2300 restUrl = baseUrl + '/trips/' + tripId + '/date-pricing?date=' + dateStr;
2301 } else {
2302 restUrl = baseUrl + '/yatra/v1/trips/' + tripId + '/date-pricing?date=' + dateStr;
2303 }
2304
2305 const nonce = (window.yatraTripData && window.yatraTripData.nonce) || '';
2306
2307 // Fetch date-specific data
2308 fetch(restUrl, {
2309 method: 'GET',
2310 headers: {
2311 'Content-Type': 'application/json',
2312 'X-WP-Nonce': nonce
2313 },
2314 credentials: 'same-origin'
2315 })
2316 .then(response => response.json())
2317 .then(data => {
2318 if (data && data.success) {
2319 // Update departures available text
2320 const departuresText = document.querySelector('.yatra-departures-available-text');
2321 if (departuresText && data.departures_count !== undefined) {
2322 departuresText.textContent = sprintf(
2323 /* translators: %d: number of departures available. */
2324 _n('%d departure available', '%d departures available', data.departures_count, 'yatra'),
2325 data.departures_count
2326 );
2327 }
2328
2329 // Update traveler dropdown HTML
2330 const quantitySelector = document.getElementById('quantity-selector');
2331 if (quantitySelector && data.travelers_html) {
2332 quantitySelector.innerHTML = data.travelers_html;
2333 // Re-initialize quantity controls
2334 this.initQuantityControls();
2335 }
2336
2337 // Update total price
2338 this.updateTotal();
2339 }
2340 })
2341 .catch(error => {
2342 console.error('Error fetching date pricing:', error);
2343 });
2344 }
2345
2346 /**
2347 * Sync sidebar travelers to all availability cards
2348 */
2349 syncSidebarTravelersToCards() {
2350 // Get sidebar traveler values
2351 const travelerInputs = document.querySelectorAll('#quantity-selector input[id^="traveler_"]');
2352 const numTravelersInput = document.getElementById('num_travelers');
2353
2354 if (travelerInputs.length === 0 && !numTravelersInput) {
2355 return; // No traveler inputs found
2356 }
2357
2358 // Get all availability cards
2359 const availabilityCards = document.querySelectorAll('.yatra-availability-card');
2360 if (availabilityCards.length === 0) {
2361 return; // No availability cards found
2362 }
2363
2364 availabilityCards.forEach((card) => {
2365 const itemId = card.getAttribute('data-availability-id') || card.getAttribute('data-item');
2366 if (!itemId) return;
2367
2368 // Check if this card uses traveler-based pricing or simple pricing
2369 const travelerSelector = card.querySelector('.yatra-availability-quantity-selector[data-item="' + itemId + '"]');
2370 const simpleTravelers = card.querySelector('.yatra-availability-num-travelers[data-item="' + itemId + '"]');
2371
2372 if (travelerSelector && travelerInputs.length > 0) {
2373 // Traveler-based pricing - sync by category
2374 travelerInputs.forEach((input) => {
2375 const categoryId = input.id.replace('traveler_', '');
2376 const value = parseInt(input.value) || 0;
2377
2378 const row = travelerSelector.querySelector('.yatra-quantity-row[data-category-id="' + categoryId + '"]');
2379 if (row) {
2380 const cardInput = row.querySelector('input[type="number"]');
2381 if (cardInput) {
2382 const max = window.yatraNumOr(cardInput.getAttribute('max'), 99);
2383 cardInput.value = Math.min(value, max);
2384
2385 // Update button states
2386 const minusBtn = row.querySelector('.yatra-quantity-minus');
2387 const plusBtn = row.querySelector('.yatra-quantity-plus');
2388 const min = window.yatraNumOr(cardInput.getAttribute('min'), 0);
2389 if (minusBtn) minusBtn.disabled = cardInput.value <= min;
2390 if (plusBtn) plusBtn.disabled = cardInput.value >= max;
2391 }
2392 }
2393 });
2394
2395 // Update the display text for this card
2396 this.updateAvailabilityCardDisplay(itemId);
2397
2398 } else if (simpleTravelers && numTravelersInput) {
2399 // Simple pricing - sync total count
2400 const totalCount = parseInt(numTravelersInput.value) || 1;
2401 const max = window.yatraNumOr(simpleTravelers.getAttribute('max'), 99);
2402 simpleTravelers.value = Math.min(totalCount, max);
2403
2404 // Update button states
2405 const controls = simpleTravelers.closest('.yatra-quantity-controls-inline');
2406 if (controls) {
2407 const minusBtn = controls.querySelector('.yatra-quantity-minus');
2408 const plusBtn = controls.querySelector('.yatra-quantity-plus');
2409 const min = window.yatraNumOr(simpleTravelers.getAttribute('min'), 1);
2410 if (minusBtn) minusBtn.disabled = simpleTravelers.value <= min;
2411 if (plusBtn) plusBtn.disabled = simpleTravelers.value >= max;
2412 }
2413 }
2414
2415 // Update card totals
2416 this.updateAvailabilityCardTotals(itemId);
2417 });
2418 }
2419
2420 /**
2421 * Update availability card participants display
2422 */
2423 updateAvailabilityCardDisplay(itemId) {
2424 const display = document.querySelector('.yatra-availability-participants-display[data-item="' + itemId + '"]');
2425 if (!display) return;
2426
2427 const selector = document.querySelector('.yatra-availability-quantity-selector[data-item="' + itemId + '"]');
2428 if (!selector) return;
2429
2430 const rows = selector.querySelectorAll('.yatra-quantity-row');
2431 const parts = [];
2432
2433 rows.forEach((row) => {
2434 const input = row.querySelector('input[type="number"]');
2435 const labelEl = row.querySelector('.yatra-quantity-title');
2436 if (input && labelEl) {
2437 const count = parseInt(input.value) || 0;
2438 if (count > 0) {
2439 parts.push(labelEl.textContent.trim() + ' x ' + count);
2440 }
2441 }
2442 });
2443
2444 display.textContent = parts.length > 0 ? parts.join(', ') : __('0 Travelers', 'yatra');
2445 }
2446
2447 /**
2448 * Update availability card total price
2449 */
2450 updateAvailabilityCardTotals(itemId) {
2451 if (window.availabilitySection && typeof window.availabilitySection.updateTotalPrice === 'function') {
2452 window.availabilitySection.updateTotalPrice(itemId);
2453 return;
2454 }
2455
2456 const totalAmountEl = document.querySelector('.yatra-card-total-amount[data-item="' + itemId + '"]');
2457 const totalNoteEl = document.querySelector('.yatra-card-total-note[data-item="' + itemId + '"]');
2458 if (!totalAmountEl) return;
2459
2460 let total = 0;
2461 let totalTravelers = 0;
2462
2463 // Check for traveler-based pricing first
2464 const selector = document.querySelector('.yatra-availability-quantity-selector[data-item="' + itemId + '"]');
2465
2466 if (selector) {
2467 // Traveler-based pricing
2468 const rows = selector.querySelectorAll('.yatra-quantity-row');
2469 rows.forEach((row) => {
2470 const input = row.querySelector('input[type="number"]');
2471 const price = parseFloat(row.getAttribute('data-price')) || 0;
2472 const count = parseInt(input?.value) || 0;
2473 const pricingMode = row.getAttribute('data-pricing-mode') || (input ? input.getAttribute('data-pricing-mode') : '') || 'per_person';
2474 const groupOverflow = row.getAttribute('data-group-overflow') || (input ? input.getAttribute('data-group-overflow') : '') || 'block';
2475 const maxPax = parseInt(row.getAttribute('data-max-pax') || (input ? input.getAttribute('data-max-pax') : '')) || 0;
2476
2477 if (pricingMode === 'per_group') {
2478 // Per group: one flat price, or per block of maxPax when "per_block".
2479 if (count > 0) {
2480 total += (groupOverflow === 'per_block' && maxPax > 0)
2481 ? price * Math.ceil(count / maxPax)
2482 : price;
2483 }
2484 } else {
2485 // Per person: charge per traveler
2486 total += price * count;
2487 }
2488 totalTravelers += count;
2489 });
2490 } else {
2491 // Simple pricing
2492 const numTravelersInput = document.querySelector('.yatra-availability-num-travelers[data-item="' + itemId + '"]');
2493 if (numTravelersInput) {
2494 const price = parseFloat(numTravelersInput.getAttribute('data-price')) || parseFloat(totalAmountEl.getAttribute('data-base-price')) || 0;
2495 const count = parseInt(numTravelersInput.value) || 1;
2496 total = price * count;
2497 totalTravelers = count;
2498 } else {
2499 total = parseFloat(totalAmountEl.getAttribute('data-base-price')) || 0;
2500 totalTravelers = 1;
2501 }
2502 }
2503
2504 // Format price (use existing format function if available)
2505 if (typeof yatra_format_price_js === 'function') {
2506 totalAmountEl.textContent = yatra_format_price_js(total);
2507 } else {
2508 // Fallback formatting
2509 totalAmountEl.textContent = String(total.toFixed(2));
2510 }
2511
2512 if (totalNoteEl) {
2513 /* translators: %d is the number of travelers. Singular form is "for 1 traveler", plural "for N travelers". */
2514 totalNoteEl.textContent = sprintf(_n('for %d traveler', 'for %d travelers', totalTravelers, 'yatra'), totalTravelers);
2515 }
2516
2517 // Update the card participants display
2518 this.updateAvailabilityCardDisplay(itemId);
2519 }
2520
2521
2522 handleMakeEnquiry() {
2523 // This will be handled by EnquiryModal class
2524 // Just trigger the modal open
2525 if (window.enquiryModal) {
2526 window.enquiryModal.open();
2527 }
2528 }
2529
2530 setTravelers(adults, children) {
2531 const adultsInput = document.getElementById('adults');
2532 const childrenInput = document.getElementById('children');
2533
2534 if (adultsInput) {
2535 adultsInput.value = adults;
2536 // Trigger change event to update display
2537 const event = new Event('change', { bubbles: true });
2538 adultsInput.dispatchEvent(event);
2539 }
2540
2541 if (childrenInput) {
2542 childrenInput.value = children;
2543 // Trigger change event to update display
2544 const event = new Event('change', { bubbles: true });
2545 childrenInput.dispatchEvent(event);
2546 }
2547 }
2548 }
2549
2550 /**
2551 * Enquiry Modal Class
2552 * Handles enquiry form modal functionality
2553 */
2554 class EnquiryModal {
2555 constructor() {
2556 this.modal = null;
2557 this.form = null;
2558 this.dateInput = null;
2559 this.participantsSelect = null;
2560 this.datepickerInstance = null;
2561 this.init();
2562 }
2563
2564 init() {
2565 if (document.readyState === 'loading') {
2566 document.addEventListener('DOMContentLoaded', () => this.setup());
2567 } else {
2568 this.setup();
2569 }
2570 }
2571
2572 setup() {
2573 this.modal = document.getElementById('enquiry-modal');
2574 if (!this.modal) return;
2575
2576 this.form = document.getElementById('enquiry-form');
2577 this.dateInput = document.getElementById('enquiry-travel-date');
2578 this.participantsSelect = document.querySelector('.yatra-enquiry-participants');
2579
2580 this.attachEventListeners();
2581 }
2582
2583 attachEventListeners() {
2584 // Close button
2585 const closeBtn = document.getElementById('close-enquiry-modal');
2586 const modalClose = document.querySelector('.yatra-enquiry-modal-close');
2587 const overlay = document.querySelector('.yatra-enquiry-modal-overlay');
2588
2589 if (closeBtn) {
2590 closeBtn.addEventListener('click', () => this.close());
2591 }
2592 if (modalClose) {
2593 modalClose.addEventListener('click', () => this.close());
2594 }
2595 if (overlay) {
2596 overlay.addEventListener('click', () => this.close());
2597 }
2598
2599 // ESC key
2600 document.addEventListener('keydown', (e) => {
2601 if (e.key === 'Escape' && this.modal.classList.contains('active')) {
2602 this.close();
2603 }
2604 });
2605
2606 // Form submission
2607 if (this.form) {
2608 this.form.addEventListener('submit', (e) => {
2609 e.preventDefault();
2610 this.handleSubmit();
2611 });
2612 }
2613 }
2614
2615 open() {
2616 if (!this.modal) return;
2617 this.modal.classList.add('active');
2618 document.body.style.overflow = 'hidden';
2619
2620 // Initialize form fields after modal is visible
2621 setTimeout(() => {
2622 this.initDateField();
2623 this.initTravelersField();
2624 }, 300);
2625 }
2626
2627 close() {
2628 if (!this.modal) return;
2629 this.modal.classList.remove('active');
2630 document.body.style.overflow = '';
2631 }
2632
2633 initDateField() {
2634 if (!this.dateInput || typeof flatpickr === 'undefined') return;
2635
2636 // Sync date from main form
2637 const mainDateInput = document.getElementById('travel_date');
2638 if (mainDateInput && mainDateInput.value) {
2639 this.dateInput.value = mainDateInput.value;
2640 }
2641
2642 // Destroy existing instance
2643 if (this.dateInput._flatpickr) {
2644 this.dateInput._flatpickr.destroy();
2645 }
2646
2647 const today = new Date();
2648 today.setHours(0, 0, 0, 0);
2649
2650 this.datepickerInstance = flatpickr(this.dateInput, {
2651 dateFormat: 'Y-m-d',
2652 altInput: true,
2653 altFormat: getTripAltDateFormat(),
2654 minDate: today,
2655 enableTime: false,
2656 clickOpens: true,
2657 allowInput: false,
2658 static: false,
2659 monthSelectorType: 'static',
2660 animate: true,
2661 locale: getFlatpickrLocale(),
2662 onChange: (selectedDates, dateStr) => {
2663 this.dateInput.value = dateStr;
2664 }
2665 });
2666
2667 // Make container clickable
2668 const container = this.dateInput.closest('.yatra-booking-field-select');
2669 if (container) {
2670 container.style.cursor = 'pointer';
2671 const openPicker = (e) => {
2672 if (e && e.target && e.target.closest && e.target.closest('button, a, select, textarea')) {
2673 return;
2674 }
2675 if (e) {
2676 e.preventDefault();
2677 e.stopPropagation();
2678 }
2679 if (this.datepickerInstance && !this.datepickerInstance.isOpen) {
2680 this.datepickerInstance.open();
2681 }
2682 };
2683
2684 container.addEventListener('click', openPicker);
2685 if (this.dateInput && this.dateInput._flatpickr && this.dateInput._flatpickr.altInput) {
2686 this.dateInput._flatpickr.altInput.addEventListener('click', openPicker);
2687 }
2688 }
2689 }
2690
2691 initTravelersField() {
2692 if (!this.participantsSelect) return;
2693
2694 const display = document.getElementById('enquiry-participants-display');
2695 const selector = document.getElementById('enquiry-quantity-selector');
2696 const adultsInput = document.getElementById('enquiry-adults');
2697 const childrenInput = document.getElementById('enquiry-children');
2698
2699 if (!display || !selector || !adultsInput || !childrenInput) return;
2700
2701 // Make display clickable
2702 display.style.cursor = 'pointer';
2703
2704 // Toggle dropdown
2705 this.participantsSelect.onclick = (e) => {
2706 if (e.target.closest('.yatra-quantity-btn')) return;
2707 if (selector.contains(e.target)) return;
2708 e.preventDefault();
2709 e.stopPropagation();
2710 this.participantsSelect.classList.toggle('active');
2711 };
2712
2713 // Update display function
2714 const updateDisplay = () => {
2715 const adults = parseInt(adultsInput.value || 1);
2716 const children = parseInt(childrenInput.value || 0);
2717 // translators: %d: number of adults
2718 const adultPart = sprintf(_n('Adult x %d', 'Adult x %d', adults, 'yatra'), adults);
2719 // translators: %d: number of children
2720 const childPart = sprintf(_n('Child x %d', 'Child x %d', children, 'yatra'), children);
2721 let text = '';
2722 if (adults > 0 && children > 0) {
2723 // translators: %1$s: adult count phrase, %2$s: child count phrase
2724 text = sprintf(__('%1$s, %2$s', 'yatra'), adultPart, childPart);
2725 } else if (adults > 0) {
2726 text = adultPart;
2727 } else if (children > 0) {
2728 text = childPart;
2729 } else {
2730 text = sprintf(_n('Adult x %d', 'Adult x %d', 1, 'yatra'), 1);
2731 }
2732 display.textContent = text;
2733 };
2734
2735 // Quantity button handlers
2736 selector.onclick = (e) => {
2737 const btn = e.target.closest('.yatra-quantity-btn');
2738 if (!btn) return;
2739
2740 e.preventDefault();
2741 e.stopPropagation();
2742
2743 const targetId = btn.getAttribute('data-target');
2744 const input = document.getElementById(targetId);
2745 if (!input) return;
2746
2747 const current = parseInt(input.value || 0);
2748 const min = parseInt(input.getAttribute('min') || 0);
2749 const max = parseInt(input.getAttribute('max') || 999);
2750 const isPlus = btn.classList.contains('yatra-quantity-plus');
2751 const isMinus = btn.classList.contains('yatra-quantity-minus');
2752
2753 let newValue = current;
2754 if (isPlus && current < max) {
2755 newValue = current + 1;
2756 } else if (isMinus && current > min) {
2757 newValue = current - 1;
2758 }
2759
2760 if (newValue !== current) {
2761 input.value = newValue;
2762 updateDisplay();
2763
2764 // Update button states
2765 const row = input.closest('.yatra-quantity-row');
2766 if (row) {
2767 const minusBtn = row.querySelector('.yatra-quantity-minus');
2768 const plusBtn = row.querySelector('.yatra-quantity-plus');
2769 if (minusBtn) minusBtn.disabled = newValue <= min;
2770 if (plusBtn) plusBtn.disabled = newValue >= max;
2771 }
2772 }
2773 };
2774
2775 // Close dropdown when clicking outside
2776 document.addEventListener('click', (e) => {
2777 if (!this.participantsSelect.contains(e.target)) {
2778 this.participantsSelect.classList.remove('active');
2779 }
2780 });
2781
2782 // Initialize
2783 updateDisplay();
2784 [adultsInput, childrenInput].forEach((input) => {
2785 const value = parseInt(input.value || 0);
2786 const min = parseInt(input.getAttribute('min') || 0);
2787 const max = parseInt(input.getAttribute('max') || 999);
2788 const row = input.closest('.yatra-quantity-row');
2789 if (row) {
2790 const minusBtn = row.querySelector('.yatra-quantity-minus');
2791 const plusBtn = row.querySelector('.yatra-quantity-plus');
2792 if (minusBtn) minusBtn.disabled = value <= min;
2793 if (plusBtn) plusBtn.disabled = value >= max;
2794 }
2795 });
2796 }
2797
2798 handleSubmit() {
2799 if (!this.form) return;
2800
2801 const formData = new FormData(this.form);
2802 const messageBox = document.getElementById('enquiry-message-box');
2803
2804 // Get values from hidden inputs for adults/children
2805 const adults = document.getElementById('enquiry-adults')?.value || 1;
2806 const children = document.getElementById('enquiry-children')?.value || 0;
2807
2808 // Add adults and children to formData
2809 formData.set('adults', adults);
2810 formData.set('children', children);
2811
2812 // Validation
2813 const name = formData.get('name');
2814 const email = formData.get('email');
2815 const message = formData.get('message');
2816
2817 if (!name || !email || !message) {
2818 this.showMessage('error', __('Please fill in all required fields.', 'yatra'));
2819 return;
2820 }
2821
2822 // Basic email validation
2823 const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
2824 if (!emailRegex.test(email)) {
2825 this.showMessage('error', __('Please enter a valid email address.', 'yatra'));
2826 return;
2827 }
2828
2829 if (message.length < 10) {
2830 this.showMessage('error', __('Please enter a message (at least 10 characters).', 'yatra'));
2831 return;
2832 }
2833
2834 // Show loading
2835 const submitBtn = this.form.querySelector('.yatra-enquiry-submit');
2836 const originalText = submitBtn ? submitBtn.textContent : __('Send Enquiry', 'yatra');
2837 if (submitBtn) {
2838 submitBtn.textContent = __('Sending...', 'yatra');
2839 submitBtn.disabled = true;
2840 }
2841
2842 // Hide any existing messages
2843 if (messageBox) {
2844 messageBox.style.display = 'none';
2845 }
2846
2847 // Submit via REST API (public endpoint)
2848 const payload = Object.fromEntries(formData.entries());
2849 // Ensure trip context is sent even if the hidden input is missing/empty.
2850 if ((!payload.trip_id || String(payload.trip_id) === '0') && window.yatraTripData?.tripId) {
2851 payload.trip_id = String(window.yatraTripData.tripId);
2852 }
2853 if (!payload.trip_slug && window.yatraTripData?.tripSlug) {
2854 payload.trip_slug = String(window.yatraTripData.tripSlug);
2855 }
2856
2857 // Build REST URL
2858 const baseUrlRaw = (window.yatraTripData?.apiUrl || window.yatraTripData?.restUrl || window.wpApiSettings?.root || '/wp-json').replace(/\/$/, '');
2859 const enquiriesUrl = baseUrlRaw.includes('/yatra/v1')
2860 ? `${baseUrlRaw}/enquiries`
2861 : `${baseUrlRaw}/yatra/v1/enquiries`;
2862
2863 // reCAPTCHA v3: attach a fresh token when the enquiry form is protected.
2864 const yatraRc = window.yatraRecaptcha;
2865 const enquiryTokenPromise = (yatraRc && yatraRc.protects && yatraRc.protects('enquiry'))
2866 ? yatraRc.execute('enquiry')
2867 : Promise.resolve('');
2868
2869 enquiryTokenPromise
2870 .then(recaptchaToken => {
2871 if (recaptchaToken) {
2872 payload.recaptcha_token = recaptchaToken;
2873 }
2874 return fetch(enquiriesUrl, {
2875 method: 'POST',
2876 headers: {
2877 'Content-Type': 'application/json',
2878 },
2879 credentials: 'omit', // public endpoint
2880 body: JSON.stringify(payload),
2881 });
2882 })
2883 .then(response => response.json())
2884 .then(result => {
2885 if (result.success) {
2886 this.showMessage('success', result.message || result.data?.message || __('Thank you for your enquiry! We will get back to you soon.', 'yatra'));
2887 this.form.reset();
2888
2889 // Reset the travelers display
2890 const display = document.getElementById('enquiry-participants-display');
2891 if (display) {
2892 display.textContent = __('Adult x 1', 'yatra');
2893 }
2894 const adultsInput = document.getElementById('enquiry-adults');
2895 const childrenInput = document.getElementById('enquiry-children');
2896 if (adultsInput) adultsInput.value = 1;
2897 if (childrenInput) childrenInput.value = 0;
2898
2899 // Close modal after delay
2900 setTimeout(() => {
2901 this.close();
2902 if (messageBox) {
2903 messageBox.style.display = 'none';
2904 }
2905 }, 3000);
2906 } else {
2907 this.showMessage('error', result.message || result.data?.message || __('Failed to submit enquiry. Please try again.', 'yatra'));
2908 }
2909 })
2910 .catch(error => {
2911 console.error('Enquiry submission error:', error);
2912 this.showMessage('error', __('An error occurred. Please try again later.', 'yatra'));
2913 })
2914 .finally(() => {
2915 if (submitBtn) {
2916 submitBtn.textContent = originalText;
2917 submitBtn.disabled = false;
2918 }
2919 });
2920 }
2921
2922 showMessage(type, text) {
2923 const messageBox = document.getElementById('enquiry-message-box');
2924 if (!messageBox) return;
2925
2926 messageBox.style.display = 'block';
2927 messageBox.className = `yatra-enquiry-message yatra-enquiry-message-${type}`;
2928 messageBox.innerHTML = `<p>${text}</p>`;
2929
2930 // Scroll message into view
2931 messageBox.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
2932 }
2933 }
2934
2935 /**
2936 * Sticky Navigation Class
2937 * Handles sticky navigation bar functionality
2938 */
2939 class StickyNav {
2940 constructor() {
2941 this.nav = null;
2942 this.sidebar = null;
2943 this.navItems = [];
2944 this.sections = [];
2945 this.scrollThreshold = 200;
2946 this.lastScrollTop = 0;
2947 this.navHeight = 0;
2948 this.init();
2949 }
2950
2951 init() {
2952 if (document.readyState === 'loading') {
2953 document.addEventListener('DOMContentLoaded', () => this.setup());
2954 } else {
2955 this.setup();
2956 }
2957 }
2958
2959 setup() {
2960 this.nav = document.querySelector('.yatra-sticky-nav');
2961 if (!this.nav) return;
2962
2963 this.sidebar = document.querySelector('.yatra-trip-sidebar');
2964 this.navItems = this.nav.querySelectorAll('.yatra-sticky-nav-item');
2965
2966 // Update sections to match actual page hierarchy
2967 this.sections = this.getActualSections();
2968
2969 // Calculate nav height
2970 this.navHeight = this.nav.offsetHeight || 60;
2971
2972 this.attachEventListeners();
2973 this.handleScroll(); // Initial check
2974 this.updateActiveNav(); // Initial check
2975 this.updateSidebarPosition(); // Initial check
2976 }
2977
2978 getActualSections() {
2979 // Derive section IDs from the sticky nav items themselves rather
2980 // than hardcoding the list. The previous hardcoded version only knew
2981 // about the built-in sections (overview, itinerary, included, …) so
2982 // admin-defined CUSTOM tabs — whose ID is `custom_<timestamp>` — were
2983 // silently dropped: their nav-link still scrolled, but updateActiveNav
2984 // never highlighted them as you scrolled past, and the "is this
2985 // section on-screen" check below was always false for them.
2986 //
2987 // Reading from this.navItems means every enabled tab (built-in OR
2988 // custom) participates in active-state tracking without needing this
2989 // list updated for each new tab type.
2990 const seen = new Set();
2991 const ids = [];
2992 this.navItems.forEach((item) => {
2993 const href = item.getAttribute('href');
2994 if (!href || !href.startsWith('#')) return;
2995 const id = href.substring(1);
2996 if (!id || seen.has(id)) return;
2997 const element = document.getElementById(id);
2998 if (element && element.offsetParent !== null) {
2999 seen.add(id);
3000 ids.push(id);
3001 }
3002 });
3003 return ids;
3004 }
3005
3006 attachEventListeners() {
3007 // Handle clicks on nav items
3008 this.navItems.forEach((item) => {
3009 item.addEventListener('click', (e) => {
3010 e.preventDefault();
3011 const href = item.getAttribute('href');
3012 if (href && href.startsWith('#')) {
3013 const sectionId = href.substring(1);
3014 this.scrollToSection(sectionId);
3015
3016 // Update active state
3017 this.navItems.forEach((nav) => nav.classList.remove('active'));
3018 item.classList.add('active');
3019 }
3020 });
3021 });
3022
3023 // Handle scroll events
3024 window.addEventListener('scroll', () => this.onScroll(), { passive: true });
3025
3026 // Update sidebar position when nav visibility changes
3027 const observer = new MutationObserver(() => {
3028 this.updateSidebarPosition();
3029 });
3030 observer.observe(this.nav, { attributes: true, attributeFilter: ['class'] });
3031 }
3032
3033 updateSidebarPosition() {
3034 if (!this.sidebar) return;
3035
3036 const isNavVisible = this.nav.classList.contains('visible');
3037 const isAdminBar = document.body.classList.contains('admin-bar');
3038
3039 // Calculate base top position
3040 let topPosition = 0;
3041
3042 if (isAdminBar) {
3043 // Check if mobile admin bar (782px breakpoint)
3044 if (window.innerWidth <= 782) {
3045 topPosition = 46; // Mobile admin bar
3046 } else {
3047 topPosition = 32; // Desktop admin bar
3048 }
3049 }
3050
3051 // Add sticky nav height if visible
3052 if (isNavVisible) {
3053 topPosition += this.navHeight;
3054 }
3055
3056 // Apply to sidebar
3057 this.sidebar.style.top = topPosition + 'px';
3058 }
3059
3060 scrollToAvailability(section) {
3061 if (!section) return;
3062
3063 const offset = 100; // Account for sticky header
3064 const elementPosition = section.getBoundingClientRect().top;
3065 const offsetPosition = elementPosition + window.pageYOffset - offset;
3066
3067 window.scrollTo({
3068 top: offsetPosition,
3069 behavior: 'smooth'
3070 });
3071 }
3072
3073 scrollToSection(sectionId) {
3074 const section = document.getElementById(sectionId);
3075 if (!section) {
3076 console.warn(`Section with ID "${sectionId}" not found`);
3077 return;
3078 }
3079
3080 // Calculate offset - account for sticky nav and admin bar
3081 let offset = 100; // Base offset
3082 const isNavVisible = this.nav.classList.contains('visible');
3083 if (isNavVisible) {
3084 offset += this.navHeight;
3085 }
3086
3087 // Check for WordPress admin bar
3088 const adminBar = document.getElementById('wpadminbar');
3089 if (adminBar) {
3090 // Check if mobile admin bar (782px breakpoint)
3091 if (window.innerWidth <= 782) {
3092 offset += 46; // Mobile admin bar
3093 } else {
3094 offset += 32; // Desktop admin bar
3095 }
3096 }
3097
3098 const elementPosition = section.getBoundingClientRect().top;
3099 const offsetPosition = elementPosition + window.pageYOffset - offset;
3100
3101 window.scrollTo({
3102 top: offsetPosition,
3103 behavior: 'smooth'
3104 });
3105 }
3106
3107 autoSelectDateCard(selectedDate) {
3108 if (!selectedDate) return;
3109
3110 // Wait a bit for the availability section to render
3111 setTimeout(() => {
3112 const availabilityCards = document.querySelectorAll('.yatra-availability-card');
3113 if (!availabilityCards.length) return;
3114
3115 let matchingCard = null;
3116 let nearestCard = null;
3117 let minDiff = Infinity;
3118
3119 const selectedTime = new Date(selectedDate).getTime();
3120
3121 // Find exact match or nearest date
3122 availabilityCards.forEach(card => {
3123 const cardDate = card.dataset.date;
3124 if (!cardDate) return;
3125
3126 if (cardDate === selectedDate) {
3127 matchingCard = card;
3128 } else {
3129 const cardTime = new Date(cardDate).getTime();
3130 const diff = Math.abs(cardTime - selectedTime);
3131 if (diff < minDiff) {
3132 minDiff = diff;
3133 nearestCard = card;
3134 }
3135 }
3136 });
3137
3138 // Use exact match if found, otherwise use nearest
3139 const cardToOpen = matchingCard || nearestCard;
3140
3141 if (cardToOpen) {
3142 // Open the card
3143 const toggle = cardToOpen.querySelector('.yatra-availability-toggle');
3144 if (toggle && !cardToOpen.classList.contains('open')) {
3145 toggle.click();
3146 }
3147
3148 // Scroll to the card
3149 setTimeout(() => {
3150 const offset = 150;
3151 const elementPosition = cardToOpen.getBoundingClientRect().top;
3152 const offsetPosition = elementPosition + window.pageYOffset - offset;
3153
3154 window.scrollTo({
3155 top: offsetPosition,
3156 behavior: 'smooth'
3157 });
3158 }, 300);
3159 }
3160 }, 500);
3161 }
3162
3163 handleScroll() {
3164 const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
3165
3166 // Show nav when scrolling down past threshold
3167 if (scrollTop > this.scrollThreshold) {
3168 this.nav.classList.add('visible');
3169 } else {
3170 this.nav.classList.remove('visible');
3171 }
3172
3173 this.lastScrollTop = scrollTop;
3174 }
3175
3176 updateActiveNav() {
3177 // `offsetTop` is relative to the closest positioned ancestor, not the
3178 // document. When a section lives inside a `position: relative` wrapper
3179 // (any of the layout containers on this page), offsetTop is wrong by
3180 // the wrapper's offset, so the "active" pill never lit up correctly
3181 // for some tabs — most visibly the newly-added custom tabs that sit
3182 // further down the DOM. `getBoundingClientRect().top + pageYOffset`
3183 // always gives the document-relative top regardless of ancestors.
3184 const scrollPos = window.scrollY + 150;
3185 let activeSectionId = null;
3186
3187 // Find which section is currently in view
3188 this.sections.forEach((sectionId) => {
3189 const section = document.getElementById(sectionId);
3190 if (section) {
3191 const rect = section.getBoundingClientRect();
3192 const sectionTop = rect.top + window.pageYOffset;
3193 const sectionBottom = sectionTop + rect.height;
3194
3195 if (scrollPos >= sectionTop && scrollPos < sectionBottom) {
3196 activeSectionId = sectionId;
3197 }
3198 }
3199 });
3200
3201 // Update active state based on section ID (not index)
3202 if (activeSectionId) {
3203 this.navItems.forEach((nav) => {
3204 nav.classList.remove('active');
3205 const href = nav.getAttribute('href');
3206 if (href === '#' + activeSectionId) {
3207 nav.classList.add('active');
3208 }
3209 });
3210 }
3211 }
3212
3213 onScroll() {
3214 this.handleScroll();
3215 this.updateActiveNav();
3216 this.updateSidebarPosition();
3217 }
3218 }
3219
3220 /**
3221 * Availability Section Class
3222 * Handles availability section interactions (filters, load more)
3223 */
3224 class AvailabilitySection {
3225 constructor() {
3226 this.section = null;
3227 this.filterButtons = [];
3228 this.availabilityItems = [];
3229 this.currentMonthFilter = 'all';
3230 this.currentSortKey = 'date-asc';
3231 this.init();
3232 }
3233
3234 init() {
3235 if (document.readyState === 'loading') {
3236 document.addEventListener('DOMContentLoaded', () => this.setup());
3237 } else {
3238 this.setup();
3239 }
3240 }
3241
3242 setup() {
3243 this.section = document.getElementById('availability');
3244 if (!this.section) return;
3245
3246 const filterBar = this.section.querySelector('.yatra-availability-filters');
3247 this.filterButtons = filterBar
3248 ? filterBar.querySelectorAll('.yatra-availability-filter-btn')
3249 : this.section.querySelectorAll('.yatra-availability-filter-btn');
3250 this.availabilityItems = this.section.querySelectorAll('.yatra-availability-card');
3251
3252 const sortSelect = this.section.querySelector('#availability-sort');
3253 if (sortSelect && sortSelect.value) {
3254 this.currentSortKey = sortSelect.value;
3255 }
3256
3257 this.attachEventListeners();
3258
3259 const listEl = this.section.querySelector('.yatra-availability-list');
3260 if (listEl && listEl.dataset.monthFilter) {
3261 this.currentMonthFilter = listEl.dataset.monthFilter;
3262 } else {
3263 const activeFilterBtn = filterBar
3264 ? filterBar.querySelector('.yatra-availability-filter-btn.active')
3265 : this.section.querySelector('.yatra-availability-filter-btn.active');
3266 if (activeFilterBtn) {
3267 this.currentMonthFilter = activeFilterBtn.getAttribute('data-filter') || 'all';
3268 }
3269 }
3270 if (listEl && listEl.dataset.sort) {
3271 this.currentSortKey = listEl.dataset.sort;
3272 }
3273
3274 // Scroll to selected card if exists
3275 this.scrollToSelectedCard();
3276 }
3277
3278 scrollToSelectedCard() {
3279 // Find the selected card
3280 const selectedCard = this.section.querySelector('.yatra-availability-card.selected');
3281 if (selectedCard) {
3282 // Small delay to ensure rendering is complete
3283 setTimeout(() => {
3284 selectedCard.scrollIntoView({
3285 behavior: 'smooth',
3286 block: 'center'
3287 });
3288 }, 300);
3289 }
3290 }
3291
3292 /**
3293 * Single source of truth for availability "Book Now" (Pro: yatraBeforeBooking / additional services popup).
3294 * Previously attachBookNowHandlers() used a legacy session POST and skipped the hook — inconsistent by date after AJAX refresh.
3295 */
3296 handleAvailabilityBookNowClick(btn) {
3297 const self = this;
3298
3299 const tripId = btn.getAttribute('data-trip-id') || window.yatraTripData?.tripId;
3300 const date = btn.getAttribute('data-date');
3301 const itemIndex = btn.getAttribute('data-item');
3302 const availabilityId = btn.getAttribute('data-availability-id') || itemIndex;
3303
3304 const card = btn.closest('.yatra-availability-card');
3305
3306 let departureTime = '';
3307 if (card) {
3308 const timeElement = card.querySelector('.yatra-card-header-date');
3309 if (timeElement) {
3310 const timeText = timeElement.textContent?.trim();
3311 if (timeText && /^\d{1,2}:\d{2}\s*(AM|PM)?$/i.test(timeText)) {
3312 departureTime = timeText;
3313 }
3314 }
3315 }
3316
3317 let travelers = 0;
3318 const travelerCounts = {};
3319
3320 const categoryInputs = card?.querySelectorAll('.yatra-availability-category[data-item="' + itemIndex + '"]');
3321 if (categoryInputs && categoryInputs.length > 0) {
3322 categoryInputs.forEach((input) => {
3323 const categoryId = input.getAttribute('data-category');
3324 const count = parseInt(input.value) || 0;
3325 if (categoryId && count > 0) {
3326 travelerCounts[categoryId] = count;
3327 travelers += count;
3328 }
3329 });
3330 } else {
3331 const simpleInput = card?.querySelector('.yatra-availability-num-travelers[data-item="' + itemIndex + '"]');
3332 if (simpleInput) {
3333 travelers = parseInt(simpleInput.value) || 1;
3334 } else {
3335 const adults = this.getTravelerCount(itemIndex, 'adults');
3336 const children = this.getTravelerCount(itemIndex, 'children');
3337 travelers = parseInt(adults) + parseInt(children);
3338 }
3339 }
3340
3341 if (travelers < 1) {
3342 this.showBookingError(btn, __('Please select at least 1 traveler to continue.', 'yatra'));
3343 return;
3344 }
3345
3346 const originalText = btn.innerHTML;
3347 btn.innerHTML = '<svg class="animate-spin" width="18" height="18" fill="none" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" opacity="0.25"></circle><path fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path></svg> ' + __('Processing...', 'yatra');
3348 btn.disabled = true;
3349
3350 if (!tripId || isNaN(parseInt(tripId)) || parseInt(tripId) <= 0) {
3351 console.error('Invalid trip ID:', tripId);
3352 btn.innerHTML = originalText;
3353 btn.disabled = false;
3354 this.showBookingError(btn, __('Unable to process booking. Invalid trip data.', 'yatra'));
3355 return;
3356 }
3357
3358 const pricingType = btn.getAttribute('data-pricing-type') || 'regular';
3359 const isDayTrip = btn.getAttribute('data-is-day-trip') === '1';
3360 let priceTypesJson = btn.getAttribute('data-price-types') || '';
3361 let priceTypes = [];
3362 try {
3363 if (priceTypesJson) {
3364 priceTypes = JSON.parse(priceTypesJson);
3365 }
3366 } catch (e) {
3367 console.error('Error parsing price types:', e);
3368 }
3369
3370 const btnDepartureTime = btn.getAttribute('data-departure-time') || '';
3371 if (btnDepartureTime) {
3372 departureTime = btnDepartureTime;
3373 }
3374
3375 const sessionPayload = {
3376 trip_id: parseInt(tripId, 10),
3377 travelers: travelers,
3378 travel_date: date || '',
3379 departure_time: departureTime,
3380 availability_id: availabilityId,
3381 pricing_type: pricingType,
3382 price_types: priceTypes,
3383 is_day_trip: isDayTrip,
3384 };
3385
3386 if (Object.keys(travelerCounts).length > 0) {
3387 sessionPayload.traveler_counts = travelerCounts;
3388 }
3389
3390 if (typeof window.yatraBeforeBooking === 'function') {
3391 Promise.resolve(window.yatraBeforeBooking({
3392 btn: btn,
3393 originalText: originalText,
3394 sessionPayload: sessionPayload,
3395 proceedToBooking: () => self.proceedToBooking(btn, originalText, sessionPayload)
3396 })).then(handled => {
3397 if (!handled) {
3398 self.proceedToBooking(btn, originalText, sessionPayload);
3399 }
3400 }).catch(error => {
3401 console.error('Error in yatraBeforeBooking hook:', error);
3402 self.proceedToBooking(btn, originalText, sessionPayload);
3403 });
3404 return;
3405 }
3406
3407 this.proceedToBooking(btn, originalText, sessionPayload);
3408 }
3409
3410 attachEventListeners() {
3411 const filterBar = this.section.querySelector('.yatra-availability-filters');
3412
3413 // Month filter buttons (server-side filter; buttons live only in .yatra-availability-filters)
3414 this.filterButtons.forEach((btn) => {
3415 btn.addEventListener('click', () => {
3416 const filter = btn.getAttribute('data-filter') || 'all';
3417 this.currentMonthFilter = filter;
3418 this.filterButtons.forEach((b) => b.classList.remove('active'));
3419 btn.classList.add('active');
3420 this.reloadAvailabilityFromServer({
3421 month_filter: filter,
3422 page: 1,
3423 sort: this.currentSortKey,
3424 });
3425 });
3426 });
3427
3428 const showAllCta = this.section.querySelector('.yatra-availability-show-all-dates');
3429 if (showAllCta && filterBar) {
3430 showAllCta.addEventListener('click', (e) => {
3431 e.preventDefault();
3432 const allPill = filterBar.querySelector('.yatra-availability-filter-btn[data-filter="all"]');
3433 if (allPill) {
3434 allPill.click();
3435 }
3436 });
3437 }
3438
3439 // Sort dropdown (server-side sort)
3440 const sortSelect = document.getElementById('availability-sort');
3441 if (sortSelect) {
3442 sortSelect.addEventListener('change', (e) => {
3443 const value = e && e.target ? e.target.value : '';
3444 this.currentSortKey = value || 'date-asc';
3445 this.reloadAvailabilityFromServer({
3446 month_filter: this.currentMonthFilter,
3447 page: 1,
3448 sort: this.currentSortKey,
3449 selectEl: sortSelect,
3450 });
3451 });
3452 }
3453
3454 this.setupToggleHandlers();
3455
3456 // Traveler selector buttons
3457 this.initTravelerSelectors();
3458
3459 // Book Now buttons - set session and redirect to booking page
3460 const bookButtons = this.section.querySelectorAll('.yatra-card-book-btn');
3461 bookButtons.forEach((btn) => {
3462 btn.addEventListener('click', (e) => {
3463 e.preventDefault();
3464 this.handleAvailabilityBookNowClick(btn);
3465 });
3466 });
3467
3468 // Load more button
3469 const loadMoreBtn = this.section.querySelector('.yatra-availability-load-more-btn');
3470 if (loadMoreBtn) {
3471 loadMoreBtn.addEventListener('click', () => {
3472 this.handleLoadMore();
3473 });
3474 }
3475 }
3476
3477 /**
3478 * Replace availability section from API (sort / month filter / reset page).
3479 * @param {{ month_filter?: string, page?: number, sort?: string, per_page?: number, selectEl?: HTMLSelectElement }} opts
3480 */
3481 reloadAvailabilityFromServer(opts = {}) {
3482 const tripId = window.yatraTripData?.tripId || this.section?.getAttribute('data-trip-id') || 0;
3483 if (!tripId) {
3484 if (opts.sort) {
3485 this.sortItems(opts.sort);
3486 }
3487 return Promise.resolve();
3488 }
3489
3490 const listEl = this.section?.querySelector('.yatra-availability-list');
3491 const monthFilter = opts.month_filter != null
3492 ? opts.month_filter
3493 : (listEl?.dataset?.monthFilter || this.currentMonthFilter || 'all');
3494 const sortKey = opts.sort || listEl?.dataset?.sort || this.currentSortKey || 'date-asc';
3495 const page = opts.page != null ? opts.page : 1;
3496 const perPage = opts.per_page != null
3497 ? opts.per_page
3498 : (parseInt(listEl?.dataset?.perPage, 10) || 10);
3499 const selectEl = opts.selectEl;
3500 const travelDateInput = document.getElementById('travel_date');
3501 const travelDate = travelDateInput && travelDateInput.value ? travelDateInput.value : undefined;
3502
3503 const prevDisabled = !!(selectEl && selectEl.disabled);
3504 if (selectEl) {
3505 selectEl.disabled = true;
3506 }
3507
3508 const url = buildAvailabilityTemplateUrl(tripId, {
3509 date: travelDate,
3510 month_filter: monthFilter,
3511 sort: sortKey,
3512 page,
3513 per_page: perPage,
3514 });
3515
3516 return fetch(url, {
3517 method: 'GET',
3518 headers: {
3519 'Content-Type': 'application/json',
3520 },
3521 credentials: 'omit',
3522 })
3523 .then((resp) => resp.json())
3524 .then((data) => {
3525 const html = data?.html || data?.data?.html;
3526 if (!html) {
3527 throw new Error('No HTML returned');
3528 }
3529
3530 const tempDiv = document.createElement('div');
3531 tempDiv.innerHTML = html;
3532 const newSection = tempDiv.querySelector('#availability') || tempDiv.firstElementChild;
3533 if (!newSection) {
3534 throw new Error('Invalid HTML');
3535 }
3536
3537 const oldSection = document.getElementById('availability');
3538 if (oldSection) {
3539 oldSection.replaceWith(newSection);
3540 }
3541
3542 this.currentMonthFilter = monthFilter;
3543 this.currentSortKey = sortKey;
3544 this.setup();
3545 })
3546 .catch((err) => {
3547 console.error('[Yatra] Availability reload failed:', err);
3548 if (opts.sort) {
3549 this.sortItems(opts.sort);
3550 }
3551 })
3552 .finally(() => {
3553 if (selectEl) {
3554 selectEl.disabled = prevDisabled;
3555 }
3556 });
3557 }
3558
3559 /**
3560 * Proceed to booking - creates session and redirects
3561 */
3562 proceedToBooking(btn, originalText, sessionPayload) {
3563 // Track Book Now click as InitiateCheckout (Facebook Pixel)
3564 if (typeof fbq !== 'undefined' && window.yatraAdmin?.facebookPixel?.connectionStatus?.pixelConnected) {
3565 const tripData = this.getTripData();
3566 const totalPrice = this.calculateTotalPrice();
3567
3568 fbq('track', 'InitiateCheckout', {
3569 content_name: tripData.title || 'Trip',
3570 content_ids: [sessionPayload.trip_id],
3571 content_type: 'product',
3572 value: totalPrice,
3573 currency: window.yatraBookingData?.currency || 'USD',
3574 num_items: sessionPayload.travelers || 1
3575 });
3576
3577 window.YATRA_DEBUG && console.log('[Facebook Pixel] InitiateCheckout tracked for trip:', sessionPayload.trip_id);
3578 }
3579
3580 // Track Book Now click as begin_checkout (Google Analytics 4)
3581 if (typeof gtag !== 'undefined' && window.yatraAdmin?.googleAnalytics?.measurementId) {
3582 const tripData = this.getTripData();
3583 const totalPrice = this.calculateTotalPrice();
3584
3585 gtag('event', 'begin_checkout', {
3586 currency: window.yatraBookingData?.currency || 'USD',
3587 value: totalPrice,
3588 items: [{
3589 item_id: sessionPayload.trip_id,
3590 item_name: tripData.title || 'Trip',
3591 category: 'Travel',
3592 price: totalPrice / (sessionPayload.travelers || 1),
3593 quantity: sessionPayload.travelers || 1
3594 }]
3595 });
3596
3597 window.YATRA_DEBUG && console.log('[Google Analytics] begin_checkout tracked for trip:', sessionPayload.trip_id);
3598 }
3599
3600 // Set booking session via REST API
3601 // credentials: 'same-origin' is required to send cookies for session
3602 const { base: baseUrl, isPlain } = getRestBase();
3603 let sessionUrl;
3604 if (isPlain && !baseUrl.includes('/yatra/v1')) {
3605 sessionUrl = `${baseUrl}/?rest_route=/yatra/v1/booking/session`;
3606 } else if (baseUrl.includes('/yatra/v1')) {
3607 sessionUrl = `${baseUrl}/booking/session`;
3608 } else {
3609 sessionUrl = `${baseUrl}/yatra/v1/booking/session`;
3610 }
3611
3612 fetch(sessionUrl, {
3613 method: 'POST',
3614 credentials: 'same-origin', // needed for PHP session cookie
3615 headers: {
3616 'Content-Type': 'application/json',
3617 },
3618 body: JSON.stringify(sessionPayload)
3619 })
3620 .then(response => response.json())
3621 .then(data => {
3622 if (data.success && data.redirect_url) {
3623 // Success - redirect to booking page
3624 window.location.href = data.redirect_url;
3625 } else {
3626 // Error - show message and reset button
3627 console.error('Booking session error:', data);
3628 btn.innerHTML = originalText;
3629 btn.disabled = false;
3630 this.showBookingError(btn, data.message || __('Unable to process booking. Please try again.', 'yatra'));
3631 }
3632 })
3633 .catch(error => {
3634 console.error('Error setting booking session:', error);
3635 btn.innerHTML = originalText;
3636 btn.disabled = false;
3637 this.showBookingError(btn, __('Unable to process booking. Please try again.', 'yatra'));
3638 });
3639 }
3640
3641 initTravelerSelectors() {
3642 // Initialize traveler-based pricing selectors (dropdowns with categories)
3643 const participantsSelects = this.section.querySelectorAll('.yatra-availability-participants');
3644
3645 participantsSelects.forEach((select) => {
3646 const itemIndex = select.getAttribute('data-item');
3647 const display = select.querySelector('.yatra-availability-participants-display');
3648 const quantitySelector = select.querySelector('.yatra-availability-quantity-selector');
3649 const categoryInputs = select.querySelectorAll('.yatra-availability-category');
3650
3651 if (!display || !quantitySelector || categoryInputs.length === 0) return;
3652
3653 // Make the whole select area clickable
3654 select.style.cursor = 'pointer';
3655
3656 // Toggle dropdown on select click
3657 select.addEventListener('click', (e) => {
3658 // Don't toggle if clicking on buttons or inputs
3659 if (e.target.closest('.yatra-quantity-btn') || e.target.closest('.yatra-quantity-input')) return;
3660 e.stopPropagation();
3661 select.classList.toggle('active');
3662 });
3663
3664 // Update display text based on category inputs
3665 const updateDisplay = () => {
3666 const parts = [];
3667 categoryInputs.forEach((input) => {
3668 const value = parseInt(input.value) || 0;
3669 if (value > 0) {
3670 const row = input.closest('.yatra-quantity-row');
3671 const categoryLabel = row ? row.querySelector('.yatra-quantity-title')?.textContent : __('Traveler', 'yatra');
3672 parts.push(`${categoryLabel} x ${value}`);
3673 }
3674 });
3675
3676 if (parts.length === 0) {
3677 // Default to first category. The label is read from the
3678 // DOM (server-rendered → already translated). Only the
3679 // " x 1" suffix is the literal we have to localise here.
3680 const firstRow = categoryInputs[0]?.closest('.yatra-quantity-row');
3681 const firstLabel = firstRow ? firstRow.querySelector('.yatra-quantity-title')?.textContent : __('Traveler', 'yatra');
3682 /* translators: %s is the traveler-category label (e.g. "Adult", "Child"). Renders as "Adult x 1". */
3683 display.textContent = __('%s x 1', 'yatra').replace('%s', firstLabel);
3684 } else {
3685 display.textContent = parts.join(', ');
3686 }
3687 };
3688
3689 // Quantity button handlers
3690 const quantityButtons = select.querySelectorAll('.yatra-quantity-btn');
3691 quantityButtons.forEach((btn) => {
3692 btn.addEventListener('click', (e) => {
3693 e.preventDefault();
3694 e.stopPropagation();
3695
3696 const targetId = btn.getAttribute('data-target');
3697 // Find the input in the same row
3698 const row = btn.closest('.yatra-quantity-row');
3699 const input = row ? row.querySelector('.yatra-quantity-input') : null;
3700
3701 if (!input) return;
3702
3703 const isMinus = btn.classList.contains('yatra-quantity-minus');
3704 const isPlus = btn.classList.contains('yatra-quantity-plus');
3705
3706 let current = parseInt(input.value) || 0;
3707 const min = window.yatraNumOr(input.getAttribute('min'), 0);
3708 const max = window.yatraNumOr(input.getAttribute('max'), 999);
3709 let newValue = current;
3710
3711 if (isPlus && current < max) {
3712 newValue = current + 1;
3713 } else if (isMinus && current > min) {
3714 newValue = current - 1;
3715 }
3716
3717 if (newValue !== current) {
3718 input.value = newValue;
3719 updateDisplay();
3720
3721 // Update button states
3722 const minusBtn = row.querySelector('.yatra-quantity-minus');
3723 const plusBtn = row.querySelector('.yatra-quantity-plus');
3724 if (minusBtn) minusBtn.disabled = newValue <= min;
3725 if (plusBtn) plusBtn.disabled = newValue >= max;
3726
3727 // Update total price
3728 this.updateTotalPrice(itemIndex);
3729
3730 // Sync with sidebar if exists
3731 this.syncSidebarTravelers(categoryInputs);
3732 }
3733 });
3734 });
3735
3736 // Close dropdown when clicking outside
3737 document.addEventListener('click', (e) => {
3738 if (!select.contains(e.target)) {
3739 select.classList.remove('active');
3740 }
3741 });
3742
3743 // Initialize display
3744 updateDisplay();
3745
3746 // Initialize button states
3747 categoryInputs.forEach((input) => {
3748 const value = parseInt(input.value || 0);
3749 const min = parseInt(input.getAttribute('min') || 0);
3750 const max = parseInt(input.getAttribute('max') || 999);
3751 const row = input.closest('.yatra-quantity-row');
3752 if (row) {
3753 const minusBtn = row.querySelector('.yatra-quantity-minus');
3754 const plusBtn = row.querySelector('.yatra-quantity-plus');
3755 if (minusBtn) minusBtn.disabled = value <= min;
3756 if (plusBtn) plusBtn.disabled = value >= max;
3757 }
3758 });
3759
3760 // Initialize total price for this card
3761 this.updateTotalPrice(itemIndex);
3762 });
3763
3764 // Initialize regular pricing selectors (simple number inputs)
3765 const simpleTravelerInputs = this.section.querySelectorAll('.yatra-availability-travelers-simple');
3766 simpleTravelerInputs.forEach((container) => {
3767 const itemIndex = container.getAttribute('data-item');
3768 const input = container.querySelector('.yatra-availability-num-travelers');
3769 const quantityButtons = container.querySelectorAll('.yatra-quantity-btn');
3770
3771 if (!input) return;
3772
3773 quantityButtons.forEach((btn) => {
3774 btn.addEventListener('click', (e) => {
3775 e.preventDefault();
3776 e.stopPropagation();
3777
3778 const isMinus = btn.classList.contains('yatra-quantity-minus');
3779 const isPlus = btn.classList.contains('yatra-quantity-plus');
3780
3781 let current = parseInt(input.value) || 1;
3782 const min = window.yatraNumOr(input.getAttribute('min'), 1);
3783 const max = window.yatraNumOr(input.getAttribute('max'), 999);
3784 let newValue = current;
3785
3786 if (isPlus && current < max) {
3787 newValue = current + 1;
3788 } else if (isMinus && current > min) {
3789 newValue = current - 1;
3790 }
3791
3792 if (newValue !== current) {
3793 input.value = newValue;
3794
3795 // Update button states
3796 const minusBtn = container.querySelector('.yatra-quantity-minus');
3797 const plusBtn = container.querySelector('.yatra-quantity-plus');
3798 if (minusBtn) minusBtn.disabled = newValue <= min;
3799 if (plusBtn) plusBtn.disabled = newValue >= max;
3800
3801 // Update total price
3802 this.updateTotalPrice(itemIndex);
3803 }
3804 });
3805 });
3806
3807 // Initialize total price for this card
3808 this.updateTotalPrice(itemIndex);
3809 });
3810 }
3811
3812 setupToggleHandlers() {
3813 if (!this.availabilityItems || !this.availabilityItems.length) return;
3814 this.availabilityItems.forEach((item) => {
3815 const toggle = item.querySelector('.yatra-availability-toggle');
3816 if (!toggle) return;
3817
3818 toggle.addEventListener('click', (e) => {
3819 e.preventDefault();
3820 const isOpen = item.classList.contains('open');
3821 this.availabilityItems.forEach((other) => other.classList.remove('open'));
3822 if (!isOpen) {
3823 item.classList.add('open');
3824 }
3825 });
3826 });
3827 }
3828
3829 getTravelerCount(itemIndex, type) {
3830 const select = this.section.querySelector(
3831 `.yatra-availability-participants[data-item="${itemIndex}"]`
3832 );
3833 if (!select) return type === 'adults' ? 1 : 0;
3834
3835 const input = select.querySelector(
3836 type === 'adults' ? '.yatra-availability-adults' : '.yatra-availability-children'
3837 );
3838 if (!input) return type === 'adults' ? 1 : 0;
3839 const value = parseInt(input.value) || 0;
3840 return type === 'adults' && value === 0 ? 1 : value;
3841 }
3842
3843 syncSidebarTravelers(categoryInputs) {
3844 // Sync availability card travelers to sidebar
3845 const sidebarDisplay = document.querySelector('.yatra-booking-card .yatra-participants-display');
3846 if (!sidebarDisplay) return;
3847
3848 const sidebarSelector = document.querySelector('.yatra-booking-card .yatra-booking-quantity-selector');
3849 if (!sidebarSelector) return;
3850
3851 // Build display text from category inputs
3852 const parts = [];
3853 categoryInputs.forEach((input) => {
3854 const value = parseInt(input.value) || 0;
3855 if (value > 0) {
3856 const row = input.closest('.yatra-quantity-row');
3857 const categoryLabel = row ? row.querySelector('.yatra-quantity-title')?.textContent : __('Traveler', 'yatra');
3858 parts.push(`${categoryLabel} x ${value}`);
3859
3860 // Also update sidebar inputs if they exist
3861 const categoryId = input.getAttribute('data-category');
3862 const sidebarInput = sidebarSelector.querySelector(`input[id="traveler_${categoryId}"]`);
3863 if (sidebarInput) {
3864 sidebarInput.value = value;
3865 // Update button states
3866 const sidebarRow = sidebarInput.closest('.yatra-quantity-row');
3867 if (sidebarRow) {
3868 const min = parseInt(sidebarInput.getAttribute('min') || 0);
3869 const max = parseInt(sidebarInput.getAttribute('max') || 999);
3870 const minusBtn = sidebarRow.querySelector('.yatra-quantity-minus');
3871 const plusBtn = sidebarRow.querySelector('.yatra-quantity-plus');
3872 if (minusBtn) minusBtn.disabled = value <= min;
3873 if (plusBtn) plusBtn.disabled = value >= max;
3874 }
3875 }
3876 }
3877 });
3878
3879 // Update sidebar display text
3880 if (parts.length > 0) {
3881 sidebarDisplay.textContent = parts.join(', ');
3882 }
3883 }
3884
3885 updateTotalPrice(itemIndex) {
3886 const totalAmountElement = this.section.querySelector(
3887 `.yatra-card-total-amount[data-item="${itemIndex}"]`
3888 );
3889 if (!totalAmountElement) return;
3890
3891 // Check for traveler-based pricing first
3892 const categoryInputs = this.section.querySelectorAll(`.yatra-availability-category[data-item="${itemIndex}"]`);
3893 let totalPrice = 0;
3894 let totalTravelers = 0;
3895
3896 if (categoryInputs.length > 0) {
3897 // Traveler-based pricing: sum up each category's price * quantity (respecting pricing mode)
3898 categoryInputs.forEach((input) => {
3899 const quantity = parseInt(input.value) || 0;
3900 const row = input.closest('.yatra-quantity-row');
3901 const price = row ? parseFloat(row.getAttribute('data-price')) || 0 : 0;
3902 const pricingMode = (row ? row.getAttribute('data-pricing-mode') : null) || input.getAttribute('data-pricing-mode') || 'per_person';
3903 const groupOverflow = (row ? row.getAttribute('data-group-overflow') : null) || input.getAttribute('data-group-overflow') || 'block';
3904 const maxPax = parseInt((row ? row.getAttribute('data-max-pax') : null) || input.getAttribute('data-max-pax')) || 0;
3905
3906 if (pricingMode === 'per_group') {
3907 if (quantity > 0) {
3908 totalPrice += (groupOverflow === 'per_block' && maxPax > 0)
3909 ? price * Math.ceil(quantity / maxPax)
3910 : price;
3911 }
3912 } else {
3913 totalPrice += quantity * price;
3914 }
3915 totalTravelers += quantity;
3916 });
3917 } else {
3918 // Regular pricing: simple number of travelers
3919 const numTravelersInput = this.section.querySelector(`.yatra-availability-num-travelers[data-item="${itemIndex}"]`);
3920 if (numTravelersInput) {
3921 totalTravelers = parseInt(numTravelersInput.value) || 1;
3922 const basePrice = parseFloat(numTravelersInput.getAttribute('data-price')) || parseFloat(totalAmountElement.getAttribute('data-base-price')) || 0;
3923 totalPrice = totalTravelers * basePrice;
3924 }
3925 }
3926
3927 // Apply group discount if applicable
3928 const groupDiscountResult = this.calculateGroupDiscount(totalPrice, totalTravelers);
3929 const finalPrice = groupDiscountResult.finalPrice;
3930 const groupDiscountApplied = groupDiscountResult.discountApplied;
3931 const groupDiscountAmount = groupDiscountResult.discountAmount;
3932
3933 // Format price with shared formatter (uses yatraTripData settings)
3934 if (typeof yatra_format_price_js === 'function') {
3935 totalAmountElement.textContent = yatra_format_price_js(finalPrice);
3936 } else {
3937 totalAmountElement.textContent = String(finalPrice.toFixed(2));
3938 }
3939
3940 // Header "per person" must match chargable average (row totals + group discount), not static PHP.
3941 const cardRoot = this.section.querySelector('.yatra-availability-card[data-item="' + itemIndex + '"]');
3942 const headerSaleEl = cardRoot ? cardRoot.querySelector('.yatra-card-header-price .yatra-sale-price') : null;
3943 if (headerSaleEl) {
3944 let perPerson = 0;
3945 if (totalTravelers > 0) {
3946 perPerson = finalPrice / totalTravelers;
3947 } else if (totalPrice > 0) {
3948 perPerson = totalPrice;
3949 }
3950 if (perPerson > 0) {
3951 headerSaleEl.textContent =
3952 typeof yatra_format_price_js === 'function' ? yatra_format_price_js(perPerson) : String(perPerson.toFixed(2));
3953 }
3954 }
3955
3956 // Update the note text
3957 const noteElement = this.section.querySelector(
3958 `.yatra-card-total-note[data-item="${itemIndex}"]`
3959 );
3960 if (noteElement) {
3961 let travelerText = sprintf(
3962 /* translators: %d is the number of travelers. Singular form is "for 1 traveler", plural "for N travelers". */
3963 _n('for %d traveler', 'for %d travelers', totalTravelers, 'yatra'),
3964 totalTravelers
3965 );
3966
3967 // Add group discount info if applied
3968 if (groupDiscountApplied && groupDiscountAmount > 0) {
3969 const discountText = (typeof yatra_format_price_js === 'function')
3970 ? yatra_format_price_js(groupDiscountAmount)
3971 : String(groupDiscountAmount.toFixed(2));
3972 travelerText += sprintf(
3973 /* translators: %s: formatted discount amount. */
3974 __(' (Group discount: -%s)', 'yatra'),
3975 discountText
3976 );
3977 }
3978 noteElement.textContent = travelerText;
3979 }
3980
3981 // Show/hide group discount applied badge
3982 this.updateGroupDiscountBadge(itemIndex, groupDiscountApplied, groupDiscountResult);
3983 }
3984
3985 /**
3986 * Calculate group discount based on total travelers
3987 */
3988 calculateGroupDiscount(totalPrice, totalTravelers) {
3989 const result = {
3990 finalPrice: totalPrice,
3991 discountApplied: false,
3992 discountAmount: 0,
3993 discountPercentage: 0,
3994 discountType: null
3995 };
3996
3997 const groupDiscounts = window.yatraTripData && window.yatraTripData.sidebarGroupDiscounts;
3998 if (!Array.isArray(groupDiscounts) || groupDiscounts.length === 0) return result;
3999
4000 // Find the applicable discount based on total travelers
4001 let applicableDiscount = null;
4002
4003 for (const discount of groupDiscounts) {
4004 // Check if discount has ranges (new format)
4005 if (discount.group_discount_ranges && discount.group_discount_ranges.length > 0) {
4006 for (const range of discount.group_discount_ranges) {
4007 const minSize = parseInt(range.min_group_size) || 0;
4008 const maxSize = range.max_group_size ? parseInt(range.max_group_size) : Infinity;
4009
4010 if (totalTravelers >= minSize && totalTravelers <= maxSize) {
4011 applicableDiscount = {
4012 type: range.discount_type || 'percentage',
4013 amount: parseFloat(range.discount_amount) || 0
4014 };
4015 break;
4016 }
4017 }
4018 } else {
4019 // Legacy format: single min_group_size
4020 const minSize = parseInt(discount.min_group_size) || 0;
4021 const maxSize = discount.max_group_size ? parseInt(discount.max_group_size) : Infinity;
4022
4023 if (totalTravelers >= minSize && totalTravelers <= maxSize) {
4024 applicableDiscount = {
4025 type: discount.discount_type || 'percentage',
4026 amount: parseFloat(discount.discount_amount) || 0
4027 };
4028 }
4029 }
4030
4031 if (applicableDiscount) break;
4032 }
4033
4034 // Apply the discount if found
4035 if (applicableDiscount && applicableDiscount.amount > 0) {
4036 result.discountApplied = true;
4037 result.discountType = applicableDiscount.type;
4038
4039 if (applicableDiscount.type === 'percentage') {
4040 result.discountPercentage = applicableDiscount.amount;
4041 result.discountAmount = totalPrice * (applicableDiscount.amount / 100);
4042 } else {
4043 // Fixed amount discount
4044 result.discountAmount = applicableDiscount.amount;
4045 }
4046
4047 result.finalPrice = Math.max(0, totalPrice - result.discountAmount);
4048 }
4049
4050 return result;
4051 }
4052
4053 /**
4054 * Recompute footer + header pricing for every availability card (e.g. after sidebar traveler sync).
4055 */
4056 updateAllCardTotals() {
4057 if (!this.section) return;
4058 const seen = new Set();
4059 this.section.querySelectorAll('.yatra-availability-card[data-item]').forEach((card) => {
4060 const id = card.getAttribute('data-item');
4061 if (id && !seen.has(id)) {
4062 seen.add(id);
4063 this.updateTotalPrice(id);
4064 }
4065 });
4066 }
4067
4068 /**
4069 * Update group discount badge visibility
4070 */
4071 updateGroupDiscountBadge(itemIndex, discountApplied, discountResult) {
4072 // Find or create the group discount applied badge for this item
4073 const availabilityItem = this.section.querySelector(`.yatra-availability-item[data-item="${itemIndex}"]`);
4074 if (!availabilityItem) return;
4075
4076 let badge = availabilityItem.querySelector('.yatra-group-discount-applied');
4077
4078 if (discountApplied && discountResult.discountAmount > 0) {
4079 if (!badge) {
4080 badge = document.createElement('div');
4081 badge.className = 'yatra-group-discount-applied';
4082 badge.style.cssText = 'background: linear-gradient(135deg, #10b981, #059669); color: white; padding: 4px 8px; border-radius: 4px; font-size: 12px; font-weight: 500; margin-top: 8px; display: inline-flex; align-items: center; gap: 4px;';
4083
4084 // Insert after the total amount
4085 const totalSection = availabilityItem.querySelector('.yatra-card-total');
4086 if (totalSection) {
4087 totalSection.appendChild(badge);
4088 }
4089 }
4090
4091 // Update badge content
4092 const discountText = discountResult.discountType === 'percentage'
4093 ? sprintf(
4094 /* translators: %s: discount percentage. */
4095 __('%s%% group discount applied!', 'yatra'),
4096 discountResult.discountPercentage
4097 )
4098 : __('Group discount applied!', 'yatra');
4099 badge.innerHTML = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></svg> ${discountText}`;
4100 } else if (badge) {
4101 badge.remove();
4102 }
4103 }
4104
4105 filterByMonth(filter) {
4106 this.availabilityItems.forEach((item) => {
4107 if (filter === 'all') {
4108 item.style.display = 'block';
4109 } else {
4110 const itemMonth = item.getAttribute('data-month');
4111 if (itemMonth === filter) {
4112 item.style.display = 'block';
4113 } else {
4114 item.style.display = 'none';
4115 }
4116 }
4117 });
4118 }
4119
4120 sortItems(sortBy) {
4121 const items = Array.from(this.availabilityItems);
4122 const list = this.section.querySelector('.yatra-availability-list');
4123 if (!list) return;
4124
4125 items.sort((a, b) => {
4126 switch (sortBy) {
4127 case 'date-asc':
4128 return new Date(a.getAttribute('data-date')) - new Date(b.getAttribute('data-date'));
4129 case 'date-desc':
4130 return new Date(b.getAttribute('data-date')) - new Date(a.getAttribute('data-date'));
4131 case 'price-asc':
4132 return parseFloat(a.getAttribute('data-price')) - parseFloat(b.getAttribute('data-price'));
4133 case 'price-desc':
4134 return parseFloat(b.getAttribute('data-price')) - parseFloat(a.getAttribute('data-price'));
4135 case 'seats-desc':
4136 return parseInt(b.getAttribute('data-seats')) - parseInt(a.getAttribute('data-seats'));
4137 default:
4138 return 0;
4139 }
4140 });
4141
4142 // Re-append sorted items
4143 items.forEach((item) => {
4144 list.appendChild(item);
4145 });
4146 }
4147
4148 handleBookNow(date, price, button, adults = 1, children = 0) {
4149 // Get travelers from the availability card if not provided
4150 const itemIndex = button.getAttribute('data-item');
4151 if (itemIndex) {
4152 adults = this.getTravelerCount(itemIndex, 'adults');
4153 children = this.getTravelerCount(itemIndex, 'children');
4154 }
4155
4156 // Update the main booking form with selected date
4157 const dateInput = document.getElementById('travel_date');
4158 if (dateInput) {
4159 // Format date for display (YYYY-MM-DD)
4160 dateInput.value = date;
4161 // Trigger change event if using Flatpickr
4162 if (dateInput._flatpickr) {
4163 dateInput._flatpickr.setDate(date, true);
4164 }
4165 }
4166
4167 // Update travelers in booking sidebar
4168 if (window.bookingSidebar) {
4169 window.bookingSidebar.setTravelers(adults, children);
4170 }
4171
4172 // Scroll to booking sidebar
4173 const bookingSidebar = document.getElementById('booking');
4174 if (bookingSidebar) {
4175 bookingSidebar.scrollIntoView({ behavior: 'smooth', block: 'start' });
4176 }
4177
4178 // Show success feedback
4179 const originalText = button.textContent;
4180 button.textContent = __('Selected!', 'yatra');
4181 button.style.background = '#10b981';
4182 setTimeout(() => {
4183 button.textContent = originalText;
4184 button.style.background = '';
4185 }, 2000);
4186 }
4187
4188 handleLoadMore() {
4189 const tripId = window.yatraTripData?.tripId || this.section?.getAttribute('data-trip-id') || 0;
4190 const loadMoreContainer = this.section.querySelector('.yatra-availability-load-more');
4191 const btn = this.section.querySelector('.yatra-availability-load-more-btn');
4192 const countInfo = this.section.querySelector('.yatra-availability-count-info');
4193 const list = this.section.querySelector('.yatra-availability-list');
4194
4195 if (!tripId || !btn || !list || !loadMoreContainer) return;
4196
4197 const perPage = parseInt(list.dataset.perPage, 10) || parseInt(loadMoreContainer.dataset.perPage, 10) || 10;
4198 const currentPage = parseInt(list.dataset.page, 10) || 1;
4199 const nextPage = currentPage + 1;
4200 const monthFilter = list.dataset.monthFilter || loadMoreContainer.dataset.monthFilter || this.currentMonthFilter || 'all';
4201 const sortKey = list.dataset.sort || loadMoreContainer.dataset.sort || this.currentSortKey || 'date-asc';
4202 const travelDateInput = document.getElementById('travel_date');
4203 const travelDate = travelDateInput && travelDateInput.value ? travelDateInput.value : undefined;
4204
4205 const originalHTML = btn.innerHTML;
4206 const loadingSvg = '<svg class="yatra-spinner" width="18" height="18" viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="3" opacity="0.25"></circle><path fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path></svg> ';
4207 // Spinner SVG is fixed markup; only the visible label is wrapped
4208 // so it ends up in the .pot and translates with the rest of the UI.
4209 btn.innerHTML = loadingSvg + __('Loading...', 'yatra');
4210 btn.disabled = true;
4211
4212 const url = buildAvailabilityTemplateUrl(tripId, {
4213 date: travelDate,
4214 month_filter: monthFilter,
4215 sort: sortKey,
4216 page: nextPage,
4217 per_page: perPage,
4218 partial: true,
4219 });
4220
4221 fetch(url, {
4222 method: 'GET',
4223 headers: {
4224 'Content-Type': 'application/json',
4225 },
4226 credentials: 'omit',
4227 })
4228 .then((resp) => {
4229 if (!resp.ok) {
4230 throw new Error(__('Load more failed', 'yatra'));
4231 }
4232 return resp.json();
4233 })
4234 .then((data) => {
4235 const html = data?.html || data?.data?.html;
4236 if (!html || typeof html !== 'string') {
4237 throw new Error('No HTML returned');
4238 }
4239
4240 const tempDiv = document.createElement('div');
4241 tempDiv.innerHTML = html.trim();
4242
4243 while (tempDiv.firstChild) {
4244 list.appendChild(tempDiv.firstChild);
4245 }
4246
4247 const total = Number(data.total) || parseInt(list.dataset.total, 10) || 0;
4248 const loadedCount = Number(data.loaded_count);
4249 const hasMore = !!data.has_more;
4250 const displayed = Number.isFinite(loadedCount)
4251 ? loadedCount
4252 : (parseInt(list.dataset.displayed, 10) || 0) + perPage;
4253
4254 list.dataset.total = String(total);
4255 list.dataset.displayed = String(displayed);
4256 list.dataset.page = String(nextPage);
4257
4258 this.availabilityItems = this.section.querySelectorAll('.yatra-availability-card');
4259 this.setupToggleHandlers();
4260 this.initTravelerSelectors();
4261 this.attachBookNowHandlers();
4262
4263 if (!hasMore) {
4264 loadMoreContainer.style.display = 'none';
4265 return;
4266 }
4267
4268 const remaining = Math.max(0, total - displayed);
4269 /* translators: %d: number of additional departures still available to load. */
4270 const loadMoreLabel = sprintf(__('Load more departures (%d remaining)', 'yatra'), remaining);
4271 btn.innerHTML = `<svg width="18" height="18" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/></svg> ${loadMoreLabel}`;
4272 btn.disabled = false;
4273
4274 loadMoreContainer.dataset.currentPage = String(nextPage);
4275 loadMoreContainer.dataset.loaded = String(displayed);
4276 loadMoreContainer.dataset.total = String(total);
4277
4278 if (countInfo) {
4279 // Pagination summary. Two placeholders preserved as
4280 // %1$d / %2$d so translators can reorder them (some
4281 // locales prefer "Of N, showing M" word order).
4282 /* translators: %1$d is the number of departures shown so far; %2$d is the total available. */
4283 countInfo.textContent = sprintf(
4284 __('Showing %1$d of %2$d departures', 'yatra'),
4285 displayed,
4286 total
4287 );
4288 }
4289 })
4290 .catch((err) => {
4291 console.error('[Yatra] Load more failed:', err);
4292 btn.innerHTML = originalHTML;
4293 btn.disabled = false;
4294 });
4295 }
4296
4297 attachBookNowHandlers() {
4298 const bookButtons = this.section.querySelectorAll('.yatra-card-book-btn');
4299 bookButtons.forEach((btn) => {
4300 const newBtn = btn.cloneNode(true);
4301 btn.parentNode.replaceChild(newBtn, btn);
4302 newBtn.addEventListener('click', (e) => {
4303 e.preventDefault();
4304 this.handleAvailabilityBookNowClick(newBtn);
4305 });
4306 });
4307 }
4308
4309 /**
4310 * Show booking error message to user just before the Book Now button
4311 * @param {HTMLElement} button - The Book Now button that was clicked
4312 * @param {string} message - Error message to display
4313 */
4314 showBookingError(button, message) {
4315 // Find the card/row containing this button
4316 const card = button.closest('.yatra-availability-card') || button.closest('.yatra-availability-item');
4317 if (!card) return;
4318
4319 // Remove any existing error message in this card
4320 const existingError = card.querySelector('.yatra-booking-error-toast');
4321 if (existingError) {
4322 existingError.remove();
4323 }
4324
4325 // Create error toast
4326 const toast = document.createElement('div');
4327 toast.className = 'yatra-booking-error-toast';
4328 toast.innerHTML = `
4329 <div style="display: flex; align-items: center; gap: 10px; padding: 12px 16px; background: #fef2f2; border: 1px solid #fecaca; border-radius: 8px; margin-top: 12px; margin-bottom: 12px;">
4330 <svg width="20" height="20" fill="none" stroke="#dc2626" viewBox="0 0 24 24" style="flex-shrink: 0;">
4331 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
4332 </svg>
4333 <span style="flex: 1; color: #991b1b; font-size: 13px; font-weight: 500;">${message}</span>
4334 <button type="button" style="background: none; border: none; cursor: pointer; padding: 2px; color: #991b1b; line-height: 1;" onclick="this.parentElement.parentElement.remove()">
4335 <svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
4336 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
4337 </svg>
4338 </button>
4339 </div>
4340 `;
4341
4342 // Insert just before the button (or button's parent row)
4343 const buttonRow = button.closest('.yatra-card-actions') || button.parentElement;
4344 if (buttonRow) {
4345 buttonRow.insertAdjacentElement('beforebegin', toast);
4346 } else {
4347 button.insertAdjacentElement('beforebegin', toast);
4348 }
4349
4350 // Auto-remove after 8 seconds
4351 setTimeout(() => {
4352 if (toast.parentElement) {
4353 toast.remove();
4354 }
4355 }, 8000);
4356
4357 // Scroll to the error message
4358 toast.scrollIntoView({ behavior: 'smooth', block: 'center' });
4359 }
4360 }
4361
4362 /**
4363 * Itinerary Section Class
4364 * Handles day toggle functionality for itinerary timeline
4365 */
4366 class ItinerarySection {
4367 constructor() {
4368 this.init();
4369 }
4370
4371 init() {
4372 if (document.readyState === 'loading') {
4373 document.addEventListener('DOMContentLoaded', () => this.setup());
4374 } else {
4375 this.setup();
4376 }
4377 }
4378
4379 setup() {
4380 this.container = document.querySelector('.yatra-itinerary-timeline');
4381 if (!this.container) return;
4382
4383 this.days = this.container.querySelectorAll('.yatra-itinerary-day');
4384 this.toggleAllBtn = document.getElementById('yatra-toggle-all');
4385 this.isAllExpanded = false;
4386 this.attachEventListeners();
4387 this.initializeDefaultState();
4388 }
4389
4390 /** First day is expanded via PHP (`.is-day-expanded`); no inline styles. */
4391 initializeDefaultState() {}
4392
4393 attachEventListeners() {
4394 // Toggle All button
4395 if (this.toggleAllBtn) {
4396 this.toggleAllBtn.addEventListener('click', () => this.toggleAll());
4397 }
4398
4399 this.days.forEach(day => {
4400 const header = day.querySelector('.yatra-itinerary-day-header');
4401 const toggle = day.querySelector('.yatra-day-toggle');
4402 const content = day.querySelector('.yatra-itinerary-day-content');
4403
4404 if (header && content) {
4405 header.addEventListener('click', (e) => {
4406 // Don't toggle if clicking on a link inside header
4407 if (e.target.tagName === 'A') return;
4408 this.toggleDay(day, toggle);
4409 });
4410 }
4411 });
4412 }
4413
4414 toggleDay(day, toggle) {
4415 const isExpanded = day.classList.contains('is-day-expanded');
4416 if (isExpanded) {
4417 day.classList.remove('is-day-expanded');
4418 day.setAttribute('data-expanded', 'false');
4419 if (toggle) toggle.setAttribute('aria-expanded', 'false');
4420 } else {
4421 day.classList.add('is-day-expanded');
4422 day.setAttribute('data-expanded', 'true');
4423 if (toggle) toggle.setAttribute('aria-expanded', 'true');
4424 }
4425 }
4426
4427 toggleAll() {
4428 if (this.isAllExpanded) {
4429 this.days.forEach(day => {
4430 const toggle = day.querySelector('.yatra-day-toggle');
4431 day.classList.remove('is-day-expanded');
4432 day.setAttribute('data-expanded', 'false');
4433 if (toggle) toggle.setAttribute('aria-expanded', 'false');
4434 });
4435 this.updateToggleButton(false);
4436 } else {
4437 this.days.forEach(day => {
4438 const toggle = day.querySelector('.yatra-day-toggle');
4439 day.classList.add('is-day-expanded');
4440 day.setAttribute('data-expanded', 'true');
4441 if (toggle) toggle.setAttribute('aria-expanded', 'true');
4442 });
4443 this.updateToggleButton(true);
4444 }
4445 this.isAllExpanded = !this.isAllExpanded;
4446 }
4447
4448 updateToggleButton(isExpanded) {
4449 if (!this.toggleAllBtn) return;
4450 this.toggleAllBtn.classList.toggle('is-all-expanded', isExpanded);
4451 const toggleText = this.toggleAllBtn.querySelector('.toggle-text');
4452 if (toggleText) {
4453 toggleText.textContent = isExpanded ? __('Collapse All', 'yatra') : __('Expand All', 'yatra');
4454 }
4455 }
4456 }
4457
4458 // ============================================
4459 // REVIEW FORM
4460 // ============================================
4461 class ReviewForm {
4462 constructor() {
4463 this.form = document.getElementById('yatra-review-form');
4464 if (this.form) {
4465 this.init();
4466 }
4467 }
4468
4469 init() {
4470 this.setupStarRating();
4471 this.setupFormSubmission();
4472 }
4473
4474 setupStarRating() {
4475 const ratingContainer = this.form.querySelector('.yatra-star-rating-input');
4476 if (!ratingContainer) return;
4477
4478 const ratingInputs = Array.from(ratingContainer.querySelectorAll('input[type="radio"]'));
4479 const ratingLabels = Array.from(ratingContainer.querySelectorAll('.yatra-star-label'));
4480
4481 // Get the rating value for a label by its associated input
4482 const getLabelRating = (label) => {
4483 const forAttr = label.getAttribute('for');
4484 if (forAttr) {
4485 const input = document.getElementById(forAttr);
4486 return input ? parseInt(input.value) : 0;
4487 }
4488 return 0;
4489 };
4490
4491 // Function to update visual state
4492 const updateStars = (rating) => {
4493 ratingLabels.forEach((label) => {
4494 const labelRating = getLabelRating(label);
4495 if (labelRating <= rating) {
4496 label.classList.add('selected');
4497 } else {
4498 label.classList.remove('selected');
4499 }
4500 });
4501 };
4502
4503 // Initialize visual state based on currently checked rating
4504 const checkedInput = ratingContainer.querySelector('input:checked');
4505 if (checkedInput) {
4506 const rating = parseInt(checkedInput.value);
4507 // Use setTimeout to ensure DOM is fully ready
4508 setTimeout(() => {
4509 updateStars(rating);
4510 }, 0);
4511 }
4512
4513 // Handle rating changes via input change event
4514 ratingInputs.forEach(input => {
4515 input.addEventListener('change', () => {
4516 updateStars(parseInt(input.value));
4517 });
4518 });
4519
4520 // Handle direct clicks on labels
4521 ratingLabels.forEach((label) => {
4522 label.addEventListener('click', (e) => {
4523 e.preventDefault();
4524 e.stopPropagation();
4525
4526 const forAttr = label.getAttribute('for');
4527 if (!forAttr) return;
4528
4529 const targetInput = document.getElementById(forAttr);
4530 if (targetInput && !targetInput.disabled) {
4531 targetInput.checked = true;
4532 const ratingValue = parseInt(targetInput.value);
4533 updateStars(ratingValue);
4534 // Trigger change event for any other listeners
4535 targetInput.dispatchEvent(new Event('change', { bubbles: true }));
4536 }
4537 });
4538
4539 // Hover effect
4540 label.addEventListener('mouseenter', () => {
4541 const hoverRating = getLabelRating(label);
4542 ratingLabels.forEach((l) => {
4543 const lRating = getLabelRating(l);
4544 if (lRating <= hoverRating) {
4545 l.classList.add('hover');
4546 } else {
4547 l.classList.remove('hover');
4548 }
4549 });
4550 });
4551 });
4552
4553 // Remove hover effect when leaving the container
4554 ratingContainer.addEventListener('mouseleave', () => {
4555 ratingLabels.forEach(l => l.classList.remove('hover'));
4556 });
4557 }
4558
4559 setupFormSubmission() {
4560 this.form.addEventListener('submit', async (e) => {
4561 e.preventDefault();
4562
4563 const submitBtn = this.form.querySelector('.yatra-review-submit-btn');
4564 const originalText = submitBtn.innerHTML;
4565
4566 // Disable button and show loading
4567 submitBtn.disabled = true;
4568 submitBtn.innerHTML =
4569 '<span class="yatra-loading-spinner"></span> ' + __('Submitting...', 'yatra');
4570
4571 try {
4572 const formData = new FormData(this.form);
4573 formData.append('action', 'yatra_submit_review');
4574
4575 const response = await fetch(yatraTripData?.ajaxUrl || '/wp-admin/admin-ajax.php', {
4576 method: 'POST',
4577 body: formData,
4578 credentials: 'same-origin'
4579 });
4580
4581 const result = await response.json();
4582
4583 if (result.success) {
4584 // Show success message
4585 this.showMessage('success', result.data?.message || __('Thank you for your review!', 'yatra'));
4586
4587 // Check if this was an edit or new submission
4588 const isEdit = this.form.querySelector('input[name="action_type"]')?.value === 'edit';
4589
4590 if (!isEdit) {
4591 // Only reset form for new submissions, not edits
4592 this.form.reset();
4593
4594 // Reset star rating to 5 stars
4595 const fiveStarInput = this.form.querySelector('input[name="rating"][value="5"]');
4596 if (fiveStarInput) {
4597 fiveStarInput.checked = true;
4598 // Also update the visual display
4599 const ratingLabels = this.form.querySelectorAll('.yatra-star-label');
4600 ratingLabels.forEach(label => label.classList.add('selected'));
4601 }
4602 }
4603 } else {
4604 this.showMessage('error', result.data?.message || __('Failed to submit review. Please try again.', 'yatra'));
4605 }
4606 } catch (error) {
4607 console.error('Review submission error:', error);
4608 this.showMessage('error', __('An error occurred. Please try again.', 'yatra'));
4609 } finally {
4610 submitBtn.disabled = false;
4611 submitBtn.innerHTML = originalText;
4612 }
4613 });
4614 }
4615
4616 showMessage(type, message) {
4617 // Remove any existing messages
4618 const existingMsg = this.form.querySelector('.yatra-review-message');
4619 if (existingMsg) existingMsg.remove();
4620
4621 const msgEl = document.createElement('div');
4622 msgEl.className = `yatra-review-message yatra-review-message-${type}`;
4623 msgEl.innerHTML = `<p>${message}</p>`;
4624
4625 this.form.insertBefore(msgEl, this.form.firstChild);
4626
4627 // Auto-remove after 5 seconds
4628 setTimeout(() => {
4629 msgEl.remove();
4630 }, 5000);
4631 }
4632 }
4633
4634 /**
4635 * Similar Adventures Carousel
4636 * Handles carousel navigation for similar trips section
4637 */
4638 class SimilarTripsCarousel {
4639 constructor() {
4640 this.track = null;
4641 this.prevBtn = null;
4642 this.nextBtn = null;
4643 this.items = [];
4644 this.currentPosition = 0;
4645 this.itemWidth = 0;
4646 this.visibleItems = 4;
4647 this.gap = 24;
4648 this.init();
4649 }
4650
4651 init() {
4652 if (document.readyState === 'loading') {
4653 document.addEventListener('DOMContentLoaded', () => this.setup());
4654 } else {
4655 this.setup();
4656 }
4657 }
4658
4659 setup() {
4660 this.track = document.getElementById('similar-carousel');
4661 this.prevBtn = document.getElementById('similar-prev');
4662 this.nextBtn = document.getElementById('similar-next');
4663
4664 if (!this.track || !this.prevBtn || !this.nextBtn) return;
4665
4666 this.items = Array.from(this.track.querySelectorAll('.yatra-carousel-item'));
4667 if (this.items.length === 0) return;
4668
4669 this.calculateDimensions();
4670 this.attachEventListeners();
4671 this.updateButtonStates();
4672
4673 // Recalculate on resize
4674 window.addEventListener('resize', () => {
4675 this.calculateDimensions();
4676 this.updateButtonStates();
4677 });
4678 }
4679
4680 calculateDimensions() {
4681 const wrapper = this.track.parentElement;
4682 const wrapperWidth = wrapper.offsetWidth;
4683
4684 // Determine visible items based on screen width
4685 if (window.innerWidth <= 600) {
4686 this.visibleItems = 1;
4687 } else if (window.innerWidth <= 900) {
4688 this.visibleItems = 2;
4689 } else if (window.innerWidth <= 1200) {
4690 this.visibleItems = 3;
4691 } else {
4692 this.visibleItems = 4;
4693 }
4694
4695 // Calculate item width
4696 this.itemWidth = (wrapperWidth - (this.gap * (this.visibleItems - 1))) / this.visibleItems;
4697
4698 // Apply width to items
4699 this.items.forEach(item => {
4700 item.style.flex = `0 0 ${this.itemWidth}px`;
4701 item.style.minWidth = `${this.itemWidth}px`;
4702 });
4703
4704 // Reset position if out of bounds
4705 const maxPosition = Math.max(0, this.items.length - this.visibleItems);
4706 if (this.currentPosition > maxPosition) {
4707 this.currentPosition = maxPosition;
4708 this.updateTrackPosition();
4709 }
4710 }
4711
4712 attachEventListeners() {
4713 this.prevBtn.addEventListener('click', () => this.slide('prev'));
4714 this.nextBtn.addEventListener('click', () => this.slide('next'));
4715
4716 // Touch/swipe support for mobile
4717 let startX = 0;
4718 let currentX = 0;
4719 let isDragging = false;
4720
4721 this.track.addEventListener('touchstart', (e) => {
4722 startX = e.touches[0].clientX;
4723 isDragging = true;
4724 }, { passive: true });
4725
4726 this.track.addEventListener('touchmove', (e) => {
4727 if (!isDragging) return;
4728 currentX = e.touches[0].clientX;
4729 }, { passive: true });
4730
4731 this.track.addEventListener('touchend', () => {
4732 if (!isDragging) return;
4733 isDragging = false;
4734
4735 const diff = startX - currentX;
4736 const threshold = 50;
4737
4738 if (diff > threshold) {
4739 this.slide('next');
4740 } else if (diff < -threshold) {
4741 this.slide('prev');
4742 }
4743 });
4744 }
4745
4746 slide(direction) {
4747 const maxPosition = Math.max(0, this.items.length - this.visibleItems);
4748
4749 if (direction === 'next' && this.currentPosition < maxPosition) {
4750 this.currentPosition++;
4751 } else if (direction === 'prev' && this.currentPosition > 0) {
4752 this.currentPosition--;
4753 }
4754
4755 this.updateTrackPosition();
4756 this.updateButtonStates();
4757 }
4758
4759 updateTrackPosition() {
4760 const offset = this.currentPosition * (this.itemWidth + this.gap);
4761 this.track.style.transform = `translateX(-${offset}px)`;
4762 }
4763
4764 updateButtonStates() {
4765 const maxPosition = Math.max(0, this.items.length - this.visibleItems);
4766
4767 this.prevBtn.disabled = this.currentPosition <= 0;
4768 this.nextBtn.disabled = this.currentPosition >= maxPosition;
4769
4770 // Hide navigation if all items are visible
4771 const shouldHideNav = this.items.length <= this.visibleItems;
4772 this.prevBtn.style.display = shouldHideNav ? 'none' : 'flex';
4773 this.nextBtn.style.display = shouldHideNav ? 'none' : 'flex';
4774 }
4775 }
4776
4777
4778 /**
4779 * Wishlist Handler
4780 * Handles save/remove trip from wishlist functionality
4781 */
4782 class WishlistHandler {
4783 constructor() {
4784 // Wishlist clicks are now handled exclusively by listing-wishlist.js
4785 // (which covers every .yatra-favorite-btn on listing, single-trip hero,
4786 // and similar-trips). Registering this second handler caused every click
4787 // to fire two REST requests racing against each other.
4788 return;
4789 }
4790
4791 init() {
4792 // Check if user is logged in and update button states
4793 if (this.isLoggedIn && this.tripId) {
4794 this.checkSavedStatus();
4795 }
4796
4797 // Handle wishlist button clicks
4798 document.addEventListener('click', (e) => {
4799 const btn = e.target.closest('.yatra-favorite-btn');
4800 if (btn) {
4801 e.preventDefault();
4802 e.stopPropagation();
4803 this.handleWishlistClick(btn);
4804 }
4805 });
4806 }
4807
4808 async checkSavedStatus() {
4809 if (!this.tripId) return;
4810
4811 try {
4812 const response = await fetch(
4813 `${this.apiUrl}/saved-trips/check/${this.tripId}`,
4814 {
4815 method: 'GET',
4816 headers: {
4817 'X-WP-Nonce': this.nonce
4818 },
4819 credentials: 'same-origin'
4820 }
4821 );
4822
4823 const data = await response.json();
4824 if (data.success && data.data.is_saved) {
4825 this.updateButtonState(true);
4826 }
4827 } catch (error) {
4828 console.error('Error checking wishlist status:', error);
4829 }
4830 }
4831
4832 handleWishlistClick(btn) {
4833 if (!this.isLoggedIn) {
4834 this.showLoginPopup();
4835 return;
4836 }
4837
4838 const btnTripId = parseInt(btn.getAttribute('data-trip-id'), 10) || this.tripId;
4839 if (!btnTripId) {
4840 return;
4841 }
4842
4843 const isSaved = btn.classList.contains('saved');
4844 if (isSaved) {
4845 this.removeFromWishlist(btn, btnTripId);
4846 } else {
4847 this.addToWishlist(btn, btnTripId);
4848 }
4849 }
4850
4851 async addToWishlist(btn, tripId) {
4852 const tid = tripId || this.tripId;
4853 btn.disabled = true;
4854 btn.classList.add('loading');
4855
4856 try {
4857 // application/x-www-form-urlencoded is parsed reliably across hosts; JSON body can be dropped on some stacks.
4858 const params = new URLSearchParams();
4859 params.set('trip_id', String(tid));
4860 const response = await fetch(`${this.apiUrl}/saved-trips`, {
4861 method: 'POST',
4862 headers: {
4863 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
4864 'X-WP-Nonce': this.nonce
4865 },
4866 credentials: 'same-origin',
4867 body: params.toString()
4868 });
4869
4870 const data = await response.json();
4871 if (data.success) {
4872 this.updateButtonState(true, tid);
4873 this.showMessage(data.message || __('Trip saved to wishlist', 'yatra'), 'success', btn);
4874 } else {
4875 this.showMessage(data.message || __('Failed to save trip', 'yatra'), 'error', btn);
4876 }
4877 } catch (error) {
4878 console.error('Error saving trip:', error);
4879 this.showMessage(__('An error occurred. Please try again.', 'yatra'), 'error', btn);
4880 } finally {
4881 btn.disabled = false;
4882 btn.classList.remove('loading');
4883 }
4884 }
4885
4886 async removeFromWishlist(btn, tripId) {
4887 const tid = tripId || this.tripId;
4888 btn.disabled = true;
4889 btn.classList.add('loading');
4890
4891 try {
4892 const response = await fetch(`${this.apiUrl}/saved-trips/${tid}`, {
4893 method: 'DELETE',
4894 headers: {
4895 'X-WP-Nonce': this.nonce
4896 },
4897 credentials: 'same-origin'
4898 });
4899
4900 const data = await response.json();
4901 if (data.success) {
4902 this.updateButtonState(false, tid);
4903 this.showMessage(data.message || __('Trip removed from wishlist', 'yatra'), 'success', btn);
4904 } else {
4905 this.showMessage(data.message || __('Failed to remove trip', 'yatra'), 'error', btn);
4906 }
4907 } catch (error) {
4908 console.error('Error removing trip:', error);
4909 this.showMessage(__('An error occurred. Please try again.', 'yatra'), 'error', btn);
4910 } finally {
4911 btn.disabled = false;
4912 btn.classList.remove('loading');
4913 }
4914 }
4915
4916 updateButtonState(isSaved, tripId) {
4917 const tid = tripId || this.tripId;
4918 const selector = tid
4919 ? `.yatra-favorite-btn[data-trip-id="${tid}"]`
4920 : '.yatra-favorite-btn';
4921 const buttons = document.querySelectorAll(selector);
4922 buttons.forEach(btn => {
4923 if (isSaved) {
4924 btn.classList.add('saved', 'is-saved');
4925 btn.setAttribute('aria-label', __('Remove from favorites', 'yatra'));
4926 // Fill the heart icon and change color
4927 const svg = btn.querySelector('svg');
4928 if (svg) {
4929 svg.style.fill = 'currentColor';
4930 svg.style.stroke = 'currentColor';
4931 btn.style.color = '#ef4444';
4932 }
4933 } else {
4934 btn.classList.remove('saved', 'is-saved');
4935 btn.setAttribute('aria-label', __('Add to favorites', 'yatra'));
4936 // Unfill the heart icon
4937 const svg = btn.querySelector('svg');
4938 if (svg) {
4939 svg.style.fill = 'none';
4940 svg.style.stroke = 'currentColor';
4941 btn.style.color = '';
4942 }
4943 }
4944 });
4945 }
4946
4947 showLoginPopup() {
4948 // Check if enquiry modal exists and reuse its structure, or create a simple login prompt
4949 const enquiryModal = document.getElementById('enquiry-modal');
4950 if (enquiryModal) {
4951 // Show enquiry modal with login message
4952 enquiryModal.classList.add('active');
4953 document.body.style.overflow = 'hidden';
4954
4955 // Update modal content to show login message
4956 const modalContent = enquiryModal.querySelector('.yatra-enquiry-modal-content');
4957 if (modalContent) {
4958 const loginMessage = document.createElement('div');
4959 loginMessage.className = 'yatra-login-prompt';
4960 loginMessage.style.cssText = 'padding: 40px; text-align: center;';
4961 const loginRequiredHeading = __('Login Required', 'yatra');
4962 const loginRequiredBody = __('Please login to save trips to your wishlist.', 'yatra');
4963 const loginButtonLabel = __('Login', 'yatra');
4964 const cancelButtonLabel = __('Cancel', 'yatra');
4965 loginMessage.innerHTML = `
4966 <h3 style="margin-bottom: 20px;">${loginRequiredHeading}</h3>
4967 <p style="margin-bottom: 30px;">${loginRequiredBody}</p>
4968 <a href="${window.yatraTripData?.loginUrl || '/wp-login.php'}" class="yatra-btn" style="display: inline-block; margin-right: 10px;">${loginButtonLabel}</a>
4969 <button type="button" class="yatra-btn-secondary" onclick="this.closest('.yatra-enquiry-modal').classList.remove('active'); document.body.style.overflow = '';">${cancelButtonLabel}</button>
4970 `;
4971 modalContent.innerHTML = '';
4972 modalContent.appendChild(loginMessage);
4973 }
4974 } else {
4975 // Fallback: show alert and redirect to login
4976 if (confirm(__('Please login to save trips to your wishlist. Would you like to go to the login page?', 'yatra'))) {
4977 window.location.href = window.yatraTripData?.loginUrl || '/wp-login.php';
4978 }
4979 }
4980 }
4981
4982 showMessage(message, type, button) {
4983 // Remove existing toast if any
4984 const existingToast = document.getElementById('yatra-wishlist-toast');
4985 if (existingToast) {
4986 existingToast.remove();
4987 }
4988
4989 // Create toast near the button
4990 const toast = document.createElement('div');
4991 toast.id = 'yatra-wishlist-toast';
4992 toast.textContent = message;
4993
4994 // Position relative to button
4995 const rect = button.getBoundingClientRect();
4996 const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
4997 const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft;
4998
4999 toast.style.cssText = `
5000 position: absolute;
5001 top: ${rect.bottom + scrollTop + 10}px;
5002 left: ${rect.left + scrollLeft}px;
5003 padding: 10px 16px;
5004 border-radius: 8px;
5005 z-index: 10001;
5006 box-shadow: 0 4px 12px rgba(0,0,0,0.2);
5007 transition: all 0.3s ease;
5008 white-space: nowrap;
5009 font-size: 14px;
5010 pointer-events: none;
5011 background-color: ${type === 'success' ? '#10b981' : '#ef4444'};
5012 color: #ffffff;
5013 opacity: 0;
5014 transform: translateY(-10px);
5015 `;
5016
5017 document.body.appendChild(toast);
5018
5019 // Animate in
5020 requestAnimationFrame(() => {
5021 toast.style.opacity = '1';
5022 toast.style.transform = 'translateY(0)';
5023 });
5024
5025 // Remove after delay
5026 setTimeout(() => {
5027 toast.style.opacity = '0';
5028 toast.style.transform = 'translateY(-10px)';
5029 setTimeout(() => {
5030 if (toast.parentNode) {
5031 toast.remove();
5032 }
5033 }, 300);
5034 }, 3000);
5035 }
5036 }
5037
5038
5039 /**
5040 * Trip Page Orchestrator
5041 * Centralized bootstrapper to keep initialization class-based and organized
5042 */
5043 class TripPage {
5044 constructor() {
5045 this.instances = {};
5046 }
5047
5048 init() {
5049 // Create all feature instances
5050 this.instances.galleryModal = new GalleryModal();
5051 this.instances.itineraryGalleryModal = new ItineraryGalleryModal();
5052 this.instances.heroMediaSwitcher = new HeroMediaSwitcher();
5053 this.instances.bookingSidebar = new BookingSidebar();
5054 this.instances.enquiryModal = new EnquiryModal();
5055 this.instances.stickyNav = new StickyNav();
5056 this.instances.wishlistHandler = new WishlistHandler();
5057 this.instances.availabilitySection = new AvailabilitySection();
5058 this.instances.itinerarySection = new ItinerarySection();
5059 this.instances.reviewForm = new ReviewForm();
5060 this.instances.similarTripsCarousel = new SimilarTripsCarousel();
5061
5062 // Expose for legacy access (existing templates rely on window.*)
5063 this.exposeGlobals();
5064
5065 // Extra page-level behaviors
5066 this.initHeroSmoothScroll();
5067 this.initGroupDiscountPopovers();
5068
5069 return this.instances;
5070 }
5071
5072 /**
5073 * Group discount popover in booking sidebar / mobile sticky (tap outside to close, Esc to close).
5074 */
5075 initGroupDiscountPopovers() {
5076 const syncOpenState = (wrap, open) => {
5077 const btn = wrap.querySelector('.yatra-group-discount-popover__trigger');
5078 const panel = wrap.querySelector('.yatra-group-discount-popover__panel');
5079 wrap.classList.toggle('is-open', open);
5080 if (btn) {
5081 btn.setAttribute('aria-expanded', open ? 'true' : 'false');
5082 }
5083 if (panel) {
5084 panel.setAttribute('aria-hidden', open ? 'false' : 'true');
5085 }
5086 };
5087
5088 const closeAll = (exceptWrap = null) => {
5089 document.querySelectorAll('[data-yatra-group-discount-popover]').forEach((w) => {
5090 if (exceptWrap && w === exceptWrap) {
5091 return;
5092 }
5093 syncOpenState(w, false);
5094 });
5095 };
5096
5097 const attach = () => {
5098 document.querySelectorAll('[data-yatra-group-discount-popover]').forEach((wrap) => {
5099 const btn = wrap.querySelector('.yatra-group-discount-popover__trigger');
5100 if (!btn || btn.dataset.yatraGdBound === '1') {
5101 return;
5102 }
5103 btn.dataset.yatraGdBound = '1';
5104 btn.addEventListener('click', (e) => {
5105 e.preventDefault();
5106 e.stopPropagation();
5107 const open = !wrap.classList.contains('is-open');
5108 if (open) {
5109 closeAll(wrap);
5110 syncOpenState(wrap, true);
5111 } else {
5112 syncOpenState(wrap, false);
5113 }
5114 });
5115 });
5116
5117 if (document.documentElement.dataset.yatraGroupDiscountPopoverDocBound === '1') {
5118 return;
5119 }
5120 document.documentElement.dataset.yatraGroupDiscountPopoverDocBound = '1';
5121
5122 document.addEventListener('click', (e) => {
5123 if (!e.target.closest('[data-yatra-group-discount-popover]')) {
5124 closeAll();
5125 }
5126 });
5127
5128 document.addEventListener('keydown', (e) => {
5129 if (e.key !== 'Escape') {
5130 return;
5131 }
5132 const openWrap = document.querySelector('[data-yatra-group-discount-popover].is-open');
5133 if (openWrap) {
5134 syncOpenState(openWrap, false);
5135 const btn = openWrap.querySelector('.yatra-group-discount-popover__trigger');
5136 if (btn) {
5137 btn.focus();
5138 }
5139 }
5140 });
5141 };
5142
5143 if (document.readyState === 'loading') {
5144 document.addEventListener('DOMContentLoaded', attach);
5145 } else {
5146 attach();
5147 }
5148 }
5149
5150 exposeGlobals() {
5151 Object.entries(this.instances).forEach(([key, instance]) => {
5152 window[key] = instance;
5153 });
5154 // Templates use window.YatraEnquiry.open() — alias to EnquiryModal instance
5155 if (this.instances.enquiryModal) {
5156 window.YatraEnquiry = this.instances.enquiryModal;
5157 }
5158 // Also expose orchestrator for potential future hooks
5159 window.tripPage = this;
5160 }
5161
5162 initHeroSmoothScroll() {
5163 const attachScroll = () => {
5164 document.querySelectorAll('a[href="#yatra-booking-widget"]').forEach((link) => {
5165 link.addEventListener('click', (e) => {
5166 e.preventDefault();
5167 const target = document.getElementById('yatra-booking-widget');
5168 if (target) {
5169 target.scrollIntoView({ behavior: 'smooth', block: 'start' });
5170 }
5171 });
5172 });
5173 };
5174
5175 if (document.readyState === 'loading') {
5176 document.addEventListener('DOMContentLoaded', attachScroll);
5177 } else {
5178 attachScroll();
5179 }
5180 }
5181
5182 static bootstrap() {
5183 const start = () => {
5184 new TripPage().init();
5185 new ItineraryVideoHandler(); // Initialize itinerary video handler
5186 };
5187 if (document.readyState === 'loading') {
5188 document.addEventListener('DOMContentLoaded', start);
5189 } else {
5190 start();
5191 }
5192 }
5193 }
5194
5195 /**
5196 * Itinerary Video Handler Class
5197 * Handles video player functionality for itinerary section
5198 */
5199 class ItineraryVideoHandler {
5200 constructor() {
5201 this.init();
5202 }
5203
5204 init() {
5205 if (document.readyState === 'loading') {
5206 document.addEventListener('DOMContentLoaded', () => this.setup());
5207 } else {
5208 this.setup();
5209 }
5210 }
5211
5212 setup() {
5213 // Handle itinerary video clicks on overlay (both in gallery and separate video section)
5214 const videoOverlays = document.querySelectorAll('.yatra-entry-gallery .yatra-media-overlay, .yatra-entry-video .yatra-media-overlay');
5215 videoOverlays.forEach(overlay => {
5216 overlay.addEventListener('click', (e) => {
5217 e.preventDefault();
5218 e.stopPropagation();
5219 e.stopImmediatePropagation();
5220
5221 const link = overlay.closest('a');
5222 if (!link) return;
5223
5224 const videoUrl = link.getAttribute('href');
5225 if (!videoUrl || videoUrl === '#') return;
5226
5227 const img = overlay.closest('.yatra-media-card').querySelector('img');
5228 const videoTitle = img ? img.alt : __('Itinerary Video', 'yatra');
5229
5230 // Extract video ID from URL
5231 let videoId = '';
5232 if (videoUrl.includes('youtube.com') || videoUrl.includes('youtu.be')) {
5233 // YouTube video ID extraction
5234 const matches = videoUrl.match(/(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/|youtube\.com\/shorts\/|youtube\.com\/live\/)([^&\n?#\/]+)/);
5235 videoId = matches ? matches[1] : '';
5236 } else if (videoUrl.includes('vimeo.com')) {
5237 // Vimeo video ID extraction
5238 const matches = videoUrl.match(/vimeo\.com\/(?:.*#|.\/videos\/)?([0-9]+)/);
5239 videoId = matches ? matches[1] : '';
5240 }
5241
5242 // Play video using YatraVideoPlayer
5243 if (window.YatraVideoPlayer) {
5244 const videoData = {
5245 url: videoUrl,
5246 title: videoTitle
5247 };
5248
5249 // Only add video_id for YouTube videos, let player detect URL type for others
5250 if (videoUrl.includes('youtube.com') || videoUrl.includes('youtu.be')) {
5251 videoData.video_id = videoId;
5252 }
5253
5254 window.YatraVideoPlayer.play(videoData);
5255 }
5256 });
5257 });
5258
5259 // Prevent video links from opening in new tab (both in gallery and separate video section)
5260 const videoLinks = document.querySelectorAll('.yatra-entry-gallery .yatra-itinerary-video-link, .yatra-entry-video .yatra-itinerary-video-link');
5261 videoLinks.forEach(link => {
5262 link.addEventListener('click', (e) => {
5263 e.preventDefault();
5264 return false;
5265 });
5266 });
5267 }
5268 }
5269
5270 /**
5271 * Downloads Handler Class
5272 * Handles download section interactions including preview modal
5273 */
5274 class DownloadsHandler {
5275 constructor() {
5276 this.previewModal = null;
5277 this.currentDownloadId = null;
5278 this.init();
5279 }
5280
5281 init() {
5282 if (document.readyState === 'loading') {
5283 document.addEventListener('DOMContentLoaded', () => this.setup());
5284 } else {
5285 this.setup();
5286 }
5287 }
5288
5289 setup() {
5290 this.previewModal = document.getElementById('download-preview-modal');
5291 if (!this.previewModal) return;
5292
5293 this.attachEventListeners();
5294 }
5295
5296 attachEventListeners() {
5297 // Handle download button clicks
5298 document.addEventListener('click', (e) => {
5299 if (e.target.closest('.yatra-download-btn')) {
5300 e.preventDefault();
5301 const btn = e.target.closest('.yatra-download-btn');
5302 const downloadId = btn.dataset.downloadId;
5303 if (downloadId) {
5304 this.downloadFile(downloadId);
5305 }
5306 }
5307
5308 // Handle view/preview link clicks
5309 if (e.target.closest('.yatra-download-view-link, .yatra-download-preview-link')) {
5310 e.preventDefault();
5311 const link = e.target.closest('.yatra-download-view-link, .yatra-download-preview-link');
5312 const downloadId = link.dataset.downloadId;
5313 if (downloadId) {
5314 this.previewFile(downloadId);
5315 }
5316 }
5317
5318 // Handle modal close
5319 if (e.target.closest('.yatra-preview-modal-close, .yatra-preview-close-btn')) {
5320 this.closePreviewModal();
5321 }
5322
5323 // Handle modal overlay click
5324 if (e.target.classList.contains('yatra-preview-modal-overlay')) {
5325 this.closePreviewModal();
5326 }
5327
5328 // Handle preview modal download button
5329 if (e.target.closest('#preview-download-btn')) {
5330 if (this.currentDownloadId) {
5331 this.downloadFile(this.currentDownloadId);
5332 }
5333 }
5334 });
5335
5336 // Handle ESC key to close modal
5337 document.addEventListener('keydown', (e) => {
5338 if (e.key === 'Escape' && this.previewModal.style.display === 'block') {
5339 this.closePreviewModal();
5340 }
5341 });
5342 }
5343
5344 async downloadFile(downloadId) {
5345 try {
5346 // Get download URL with signature using centralized helper
5347 const response = await window.YatraApiHelper.getDownloadInfo(downloadId);
5348 if (!response.ok) {
5349 throw new Error(__('Download failed', 'yatra'));
5350 }
5351
5352 const data = await response.json();
5353
5354 if (data.download_url) {
5355 // Create temporary link and trigger download
5356 const link = document.createElement('a');
5357 link.href = data.download_url;
5358 link.download = ''; // Let server determine filename
5359 document.body.appendChild(link);
5360 link.click();
5361 document.body.removeChild(link);
5362 } else {
5363 throw new Error('Download URL not found');
5364 }
5365 } catch (error) {
5366 console.error('Download error:', error);
5367 // Show error message to user
5368 this.showNotification(__('Download failed. Please try again.', 'yatra'), 'error');
5369 }
5370 }
5371
5372 async previewFile(downloadId) {
5373 try {
5374 this.currentDownloadId = downloadId;
5375 this.showPreviewModal();
5376
5377 // Get download info using centralized helper
5378 const response = await window.YatraApiHelper.getDownloadInfo(downloadId);
5379 if (!response.ok) {
5380 throw new Error(__('Failed to get file info', 'yatra'));
5381 }
5382
5383 const data = await response.json();
5384
5385 if (data.download_url) {
5386 this.loadPreview(data.download_url, data.title || __('Preview', 'yatra'));
5387 } else {
5388 throw new Error('File URL not found');
5389 }
5390 } catch (error) {
5391 console.error('Preview error:', error);
5392 this.closePreviewModal();
5393 this.showNotification(__('Failed to load preview. Please try again.', 'yatra'), 'error');
5394 }
5395 }
5396
5397 loadPreview(fileUrl, title) {
5398 const previewContent = document.getElementById('preview-content');
5399 const modalTitle = document.querySelector('.yatra-preview-modal-title');
5400
5401 if (modalTitle) {
5402 modalTitle.textContent = title;
5403 }
5404
5405 if (!previewContent) return;
5406
5407 // Hide loading and show preview
5408 const loading = previewContent.querySelector('.yatra-preview-loading');
5409 if (loading) {
5410 loading.style.display = 'none';
5411 }
5412
5413 // Check file type and load appropriate preview
5414 const fileType = this.getFileType(fileUrl);
5415
5416 if (fileType === 'image') {
5417 previewContent.innerHTML = `<img src="${fileUrl}" alt="${title}" style="max-width: 100%; height: auto;">`;
5418 } else if (fileType === 'pdf') {
5419 previewContent.innerHTML = `
5420 <iframe src="${fileUrl}" style="width: 100%; height: 500px; border: none; border-radius: 8px;"></iframe>
5421 `;
5422 } else {
5423 const previewUnavailableTitle = __('Preview not available for this file type', 'yatra');
5424 const previewUnavailableHint = __('Please download the file to view its contents', 'yatra');
5425 previewContent.innerHTML = `
5426 <div style="text-align: center; padding: 40px;">
5427 <div style="font-size: 48px; margin-bottom: 16px; opacity: 0.5;">📄</div>
5428 <p style="color: #6b7280; margin-bottom: 16px;">${previewUnavailableTitle}</p>
5429 <p style="color: #9ca3af; font-size: 14px;">${previewUnavailableHint}</p>
5430 </div>
5431 `;
5432 }
5433 }
5434
5435 getFileType(url) {
5436 const extension = url.split('.').pop().toLowerCase();
5437 const imageTypes = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'];
5438 const pdfTypes = ['pdf'];
5439
5440 if (imageTypes.includes(extension)) return 'image';
5441 if (pdfTypes.includes(extension)) return 'pdf';
5442 return 'other';
5443 }
5444
5445 showPreviewModal() {
5446 if (this.previewModal) {
5447 this.previewModal.style.display = 'block';
5448 document.body.style.overflow = 'hidden';
5449
5450 // Reset loading state
5451 const previewContent = document.getElementById('preview-content');
5452 if (previewContent) {
5453 const loadingPreviewLabel = __('Loading preview...', 'yatra');
5454 previewContent.innerHTML = `
5455 <div class="yatra-preview-loading">
5456 <div class="yatra-preview-spinner"></div>
5457 <p>${loadingPreviewLabel}</p>
5458 </div>
5459 `;
5460 }
5461 }
5462 }
5463
5464 closePreviewModal() {
5465 if (this.previewModal) {
5466 this.previewModal.style.display = 'none';
5467 document.body.style.overflow = '';
5468 this.currentDownloadId = null;
5469 }
5470 }
5471
5472 showNotification(message, type = 'info') {
5473 // Simple notification - you can enhance this with a proper notification system
5474 const notification = document.createElement('div');
5475 notification.style.cssText = `
5476 position: fixed;
5477 top: 20px;
5478 right: 20px;
5479 background: ${type === 'error' ? '#ef4444' : '#3b82f6'};
5480 color: white;
5481 padding: 12px 20px;
5482 border-radius: 8px;
5483 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
5484 z-index: 10000;
5485 font-size: 14px;
5486 font-weight: 500;
5487 `;
5488 notification.textContent = message;
5489
5490 document.body.appendChild(notification);
5491
5492 setTimeout(() => {
5493 notification.style.opacity = '0';
5494 notification.style.transition = 'opacity 0.3s ease';
5495 setTimeout(() => {
5496 document.body.removeChild(notification);
5497 }, 300);
5498 }, 3000);
5499 }
5500 }
5501
5502 // Initialize downloads handler
5503 new DownloadsHandler();
5504
5505 // Kick things off
5506 TripPage.bootstrap();
5507
5508 })();
5509