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

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