PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.9
Yatra – Travel Booking & Tour Operator Software v3.0.9
3.0.16 3.0.15 3.0.14 3.0.14.1 3.0.14.2 3.0.12 3.0.13 3.0.11 3.0.10 3.0.9 3.0.8 3.0.7 3.0.6 3.0.5 3.0.5.1 3.0.4 3.0.3 3.0.2.9 3.0.2.7 3.0.2.8 3.0.2.6 trunk 1.0.0 2.0.0 2.0.1 All 84 releases
yatra / assets / js / trip.js

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

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