PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.5
Yatra – Travel Booking & Tour Operator Software v3.0.5
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.5, at assets/js/trip.js

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