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

5,514 lines 195.5 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
3901 if (categoryInputs.length > 0) {
3902 // Traveler-based pricing: sum up each category's price * quantity (respecting pricing mode)
3903 categoryInputs.forEach((input) => {
3904 const quantity = parseInt(input.value) || 0;
3905 const row = input.closest('.yatra-quantity-row');
3906 const price = row ? parseFloat(row.getAttribute('data-price')) || 0 : 0;
3907 const pricingMode = (row ? row.getAttribute('data-pricing-mode') : null) || input.getAttribute('data-pricing-mode') || 'per_person';
3908 const groupOverflow = (row ? row.getAttribute('data-group-overflow') : null) || input.getAttribute('data-group-overflow') || 'block';
3909 const maxPax = parseInt((row ? row.getAttribute('data-max-pax') : null) || input.getAttribute('data-max-pax')) || 0;
3910
3911 if (pricingMode === 'per_group') {
3912 if (quantity > 0) {
3913 totalPrice += (groupOverflow === 'per_block' && maxPax > 0)
3914 ? price * Math.ceil(quantity / maxPax)
3915 : price;
3916 }
3917 } else {
3918 totalPrice += quantity * price;
3919 }
3920 totalTravelers += quantity;
3921 });
3922 } else {
3923 // Regular pricing: simple number of travelers
3924 const numTravelersInput = this.section.querySelector(`.yatra-availability-num-travelers[data-item="${itemIndex}"]`);
3925 if (numTravelersInput) {
3926 totalTravelers = parseInt(numTravelersInput.value) || 1;
3927 const basePrice = parseFloat(numTravelersInput.getAttribute('data-price')) || parseFloat(totalAmountElement.getAttribute('data-base-price')) || 0;
3928 totalPrice = totalTravelers * basePrice;
3929 }
3930 }
3931
3932 // Apply group discount if applicable
3933 const groupDiscountResult = this.calculateGroupDiscount(totalPrice, totalTravelers);
3934 const finalPrice = groupDiscountResult.finalPrice;
3935 const groupDiscountApplied = groupDiscountResult.discountApplied;
3936 const groupDiscountAmount = groupDiscountResult.discountAmount;
3937
3938 // Format price with shared formatter (uses yatraTripData settings)
3939 if (typeof yatra_format_price_js === 'function') {
3940 totalAmountElement.textContent = yatra_format_price_js(finalPrice);
3941 } else {
3942 totalAmountElement.textContent = String(finalPrice.toFixed(2));
3943 }
3944
3945 // Header "per person" must match chargable average (row totals + group discount), not static PHP.
3946 const cardRoot = this.section.querySelector('.yatra-availability-card[data-item="' + itemIndex + '"]');
3947 const headerSaleEl = cardRoot ? cardRoot.querySelector('.yatra-card-header-price .yatra-sale-price') : null;
3948 if (headerSaleEl) {
3949 let perPerson = 0;
3950 if (totalTravelers > 0) {
3951 perPerson = finalPrice / totalTravelers;
3952 } else if (totalPrice > 0) {
3953 perPerson = totalPrice;
3954 }
3955 if (perPerson > 0) {
3956 headerSaleEl.textContent =
3957 typeof yatra_format_price_js === 'function' ? yatra_format_price_js(perPerson) : String(perPerson.toFixed(2));
3958 }
3959 }
3960
3961 // Update the note text
3962 const noteElement = this.section.querySelector(
3963 `.yatra-card-total-note[data-item="${itemIndex}"]`
3964 );
3965 if (noteElement) {
3966 let travelerText = sprintf(
3967 /* translators: %d is the number of travelers. Singular form is "for 1 traveler", plural "for N travelers". */
3968 _n('for %d traveler', 'for %d travelers', totalTravelers, 'yatra'),
3969 totalTravelers
3970 );
3971
3972 // Add group discount info if applied
3973 if (groupDiscountApplied && groupDiscountAmount > 0) {
3974 const discountText = (typeof yatra_format_price_js === 'function')
3975 ? yatra_format_price_js(groupDiscountAmount)
3976 : String(groupDiscountAmount.toFixed(2));
3977 travelerText += sprintf(
3978 /* translators: %s: formatted discount amount. */
3979 __(' (Group discount: -%s)', 'yatra'),
3980 discountText
3981 );
3982 }
3983 noteElement.textContent = travelerText;
3984 }
3985
3986 // Show/hide group discount applied badge
3987 this.updateGroupDiscountBadge(itemIndex, groupDiscountApplied, groupDiscountResult);
3988 }
3989
3990 /**
3991 * Calculate group discount based on total travelers
3992 */
3993 calculateGroupDiscount(totalPrice, totalTravelers) {
3994 const result = {
3995 finalPrice: totalPrice,
3996 discountApplied: false,
3997 discountAmount: 0,
3998 discountPercentage: 0,
3999 discountType: null
4000 };
4001
4002 const groupDiscounts = window.yatraTripData && window.yatraTripData.sidebarGroupDiscounts;
4003 if (!Array.isArray(groupDiscounts) || groupDiscounts.length === 0) return result;
4004
4005 // Find the applicable discount based on total travelers
4006 let applicableDiscount = null;
4007
4008 for (const discount of groupDiscounts) {
4009 // Check if discount has ranges (new format)
4010 if (discount.group_discount_ranges && discount.group_discount_ranges.length > 0) {
4011 for (const range of discount.group_discount_ranges) {
4012 const minSize = parseInt(range.min_group_size) || 0;
4013 const maxSize = range.max_group_size ? parseInt(range.max_group_size) : Infinity;
4014
4015 if (totalTravelers >= minSize && totalTravelers <= maxSize) {
4016 applicableDiscount = {
4017 type: range.discount_type || 'percentage',
4018 amount: parseFloat(range.discount_amount) || 0
4019 };
4020 break;
4021 }
4022 }
4023 } else {
4024 // Legacy format: single min_group_size
4025 const minSize = parseInt(discount.min_group_size) || 0;
4026 const maxSize = discount.max_group_size ? parseInt(discount.max_group_size) : Infinity;
4027
4028 if (totalTravelers >= minSize && totalTravelers <= maxSize) {
4029 applicableDiscount = {
4030 type: discount.discount_type || 'percentage',
4031 amount: parseFloat(discount.discount_amount) || 0
4032 };
4033 }
4034 }
4035
4036 if (applicableDiscount) break;
4037 }
4038
4039 // Apply the discount if found
4040 if (applicableDiscount && applicableDiscount.amount > 0) {
4041 result.discountApplied = true;
4042 result.discountType = applicableDiscount.type;
4043
4044 if (applicableDiscount.type === 'percentage') {
4045 result.discountPercentage = applicableDiscount.amount;
4046 result.discountAmount = totalPrice * (applicableDiscount.amount / 100);
4047 } else {
4048 // Fixed amount discount
4049 result.discountAmount = applicableDiscount.amount;
4050 }
4051
4052 result.finalPrice = Math.max(0, totalPrice - result.discountAmount);
4053 }
4054
4055 return result;
4056 }
4057
4058 /**
4059 * Recompute footer + header pricing for every availability card (e.g. after sidebar traveler sync).
4060 */
4061 updateAllCardTotals() {
4062 if (!this.section) return;
4063 const seen = new Set();
4064 this.section.querySelectorAll('.yatra-availability-card[data-item]').forEach((card) => {
4065 const id = card.getAttribute('data-item');
4066 if (id && !seen.has(id)) {
4067 seen.add(id);
4068 this.updateTotalPrice(id);
4069 }
4070 });
4071 }
4072
4073 /**
4074 * Update group discount badge visibility
4075 */
4076 updateGroupDiscountBadge(itemIndex, discountApplied, discountResult) {
4077 // Find or create the group discount applied badge for this item
4078 const availabilityItem = this.section.querySelector(`.yatra-availability-item[data-item="${itemIndex}"]`);
4079 if (!availabilityItem) return;
4080
4081 let badge = availabilityItem.querySelector('.yatra-group-discount-applied');
4082
4083 if (discountApplied && discountResult.discountAmount > 0) {
4084 if (!badge) {
4085 badge = document.createElement('div');
4086 badge.className = 'yatra-group-discount-applied';
4087 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;';
4088
4089 // Insert after the total amount
4090 const totalSection = availabilityItem.querySelector('.yatra-card-total');
4091 if (totalSection) {
4092 totalSection.appendChild(badge);
4093 }
4094 }
4095
4096 // Update badge content
4097 const discountText = discountResult.discountType === 'percentage'
4098 ? sprintf(
4099 /* translators: %s: discount percentage. */
4100 __('%s%% group discount applied!', 'yatra'),
4101 discountResult.discountPercentage
4102 )
4103 : __('Group discount applied!', 'yatra');
4104 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}`;
4105 } else if (badge) {
4106 badge.remove();
4107 }
4108 }
4109
4110 filterByMonth(filter) {
4111 this.availabilityItems.forEach((item) => {
4112 if (filter === 'all') {
4113 item.style.display = 'block';
4114 } else {
4115 const itemMonth = item.getAttribute('data-month');
4116 if (itemMonth === filter) {
4117 item.style.display = 'block';
4118 } else {
4119 item.style.display = 'none';
4120 }
4121 }
4122 });
4123 }
4124
4125 sortItems(sortBy) {
4126 const items = Array.from(this.availabilityItems);
4127 const list = this.section.querySelector('.yatra-availability-list');
4128 if (!list) return;
4129
4130 items.sort((a, b) => {
4131 switch (sortBy) {
4132 case 'date-asc':
4133 return new Date(a.getAttribute('data-date')) - new Date(b.getAttribute('data-date'));
4134 case 'date-desc':
4135 return new Date(b.getAttribute('data-date')) - new Date(a.getAttribute('data-date'));
4136 case 'price-asc':
4137 return parseFloat(a.getAttribute('data-price')) - parseFloat(b.getAttribute('data-price'));
4138 case 'price-desc':
4139 return parseFloat(b.getAttribute('data-price')) - parseFloat(a.getAttribute('data-price'));
4140 case 'seats-desc':
4141 return parseInt(b.getAttribute('data-seats')) - parseInt(a.getAttribute('data-seats'));
4142 default:
4143 return 0;
4144 }
4145 });
4146
4147 // Re-append sorted items
4148 items.forEach((item) => {
4149 list.appendChild(item);
4150 });
4151 }
4152
4153 handleBookNow(date, price, button, adults = 1, children = 0) {
4154 // Get travelers from the availability card if not provided
4155 const itemIndex = button.getAttribute('data-item');
4156 if (itemIndex) {
4157 adults = this.getTravelerCount(itemIndex, 'adults');
4158 children = this.getTravelerCount(itemIndex, 'children');
4159 }
4160
4161 // Update the main booking form with selected date
4162 const dateInput = document.getElementById('travel_date');
4163 if (dateInput) {
4164 // Format date for display (YYYY-MM-DD)
4165 dateInput.value = date;
4166 // Trigger change event if using Flatpickr
4167 if (dateInput._flatpickr) {
4168 dateInput._flatpickr.setDate(date, true);
4169 }
4170 }
4171
4172 // Update travelers in booking sidebar
4173 if (window.bookingSidebar) {
4174 window.bookingSidebar.setTravelers(adults, children);
4175 }
4176
4177 // Scroll to booking sidebar
4178 const bookingSidebar = document.getElementById('booking');
4179 if (bookingSidebar) {
4180 bookingSidebar.scrollIntoView({ behavior: 'smooth', block: 'start' });
4181 }
4182
4183 // Show success feedback
4184 const originalText = button.textContent;
4185 button.textContent = __('Selected!', 'yatra');
4186 button.style.background = '#10b981';
4187 setTimeout(() => {
4188 button.textContent = originalText;
4189 button.style.background = '';
4190 }, 2000);
4191 }
4192
4193 handleLoadMore() {
4194 const tripId = window.yatraTripData?.tripId || this.section?.getAttribute('data-trip-id') || 0;
4195 const loadMoreContainer = this.section.querySelector('.yatra-availability-load-more');
4196 const btn = this.section.querySelector('.yatra-availability-load-more-btn');
4197 const countInfo = this.section.querySelector('.yatra-availability-count-info');
4198 const list = this.section.querySelector('.yatra-availability-list');
4199
4200 if (!tripId || !btn || !list || !loadMoreContainer) return;
4201
4202 const perPage = parseInt(list.dataset.perPage, 10) || parseInt(loadMoreContainer.dataset.perPage, 10) || 10;
4203 const currentPage = parseInt(list.dataset.page, 10) || 1;
4204 const nextPage = currentPage + 1;
4205 const monthFilter = list.dataset.monthFilter || loadMoreContainer.dataset.monthFilter || this.currentMonthFilter || 'all';
4206 const sortKey = list.dataset.sort || loadMoreContainer.dataset.sort || this.currentSortKey || 'date-asc';
4207 const travelDateInput = document.getElementById('travel_date');
4208 const travelDate = travelDateInput && travelDateInput.value ? travelDateInput.value : undefined;
4209
4210 const originalHTML = btn.innerHTML;
4211 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> ';
4212 // Spinner SVG is fixed markup; only the visible label is wrapped
4213 // so it ends up in the .pot and translates with the rest of the UI.
4214 btn.innerHTML = loadingSvg + __('Loading...', 'yatra');
4215 btn.disabled = true;
4216
4217 const url = buildAvailabilityTemplateUrl(tripId, {
4218 date: travelDate,
4219 month_filter: monthFilter,
4220 sort: sortKey,
4221 page: nextPage,
4222 per_page: perPage,
4223 partial: true,
4224 });
4225
4226 fetch(url, {
4227 method: 'GET',
4228 headers: {
4229 'Content-Type': 'application/json',
4230 },
4231 credentials: 'omit',
4232 })
4233 .then((resp) => {
4234 if (!resp.ok) {
4235 throw new Error(__('Load more failed', 'yatra'));
4236 }
4237 return resp.json();
4238 })
4239 .then((data) => {
4240 const html = data?.html || data?.data?.html;
4241 if (!html || typeof html !== 'string') {
4242 throw new Error('No HTML returned');
4243 }
4244
4245 const tempDiv = document.createElement('div');
4246 tempDiv.innerHTML = html.trim();
4247
4248 while (tempDiv.firstChild) {
4249 list.appendChild(tempDiv.firstChild);
4250 }
4251
4252 const total = Number(data.total) || parseInt(list.dataset.total, 10) || 0;
4253 const loadedCount = Number(data.loaded_count);
4254 const hasMore = !!data.has_more;
4255 const displayed = Number.isFinite(loadedCount)
4256 ? loadedCount
4257 : (parseInt(list.dataset.displayed, 10) || 0) + perPage;
4258
4259 list.dataset.total = String(total);
4260 list.dataset.displayed = String(displayed);
4261 list.dataset.page = String(nextPage);
4262
4263 this.availabilityItems = this.section.querySelectorAll('.yatra-availability-card');
4264 this.setupToggleHandlers();
4265 this.initTravelerSelectors();
4266 this.attachBookNowHandlers();
4267
4268 if (!hasMore) {
4269 loadMoreContainer.style.display = 'none';
4270 return;
4271 }
4272
4273 const remaining = Math.max(0, total - displayed);
4274 /* translators: %d: number of additional departures still available to load. */
4275 const loadMoreLabel = sprintf(__('Load more departures (%d remaining)', 'yatra'), remaining);
4276 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}`;
4277 btn.disabled = false;
4278
4279 loadMoreContainer.dataset.currentPage = String(nextPage);
4280 loadMoreContainer.dataset.loaded = String(displayed);
4281 loadMoreContainer.dataset.total = String(total);
4282
4283 if (countInfo) {
4284 // Pagination summary. Two placeholders preserved as
4285 // %1$d / %2$d so translators can reorder them (some
4286 // locales prefer "Of N, showing M" word order).
4287 /* translators: %1$d is the number of departures shown so far; %2$d is the total available. */
4288 countInfo.textContent = sprintf(
4289 __('Showing %1$d of %2$d departures', 'yatra'),
4290 displayed,
4291 total
4292 );
4293 }
4294 })
4295 .catch((err) => {
4296 console.error('[Yatra] Load more failed:', err);
4297 btn.innerHTML = originalHTML;
4298 btn.disabled = false;
4299 });
4300 }
4301
4302 attachBookNowHandlers() {
4303 const bookButtons = this.section.querySelectorAll('.yatra-card-book-btn');
4304 bookButtons.forEach((btn) => {
4305 const newBtn = btn.cloneNode(true);
4306 btn.parentNode.replaceChild(newBtn, btn);
4307 newBtn.addEventListener('click', (e) => {
4308 e.preventDefault();
4309 this.handleAvailabilityBookNowClick(newBtn);
4310 });
4311 });
4312 }
4313
4314 /**
4315 * Show booking error message to user just before the Book Now button
4316 * @param {HTMLElement} button - The Book Now button that was clicked
4317 * @param {string} message - Error message to display
4318 */
4319 showBookingError(button, message) {
4320 // Find the card/row containing this button
4321 const card = button.closest('.yatra-availability-card') || button.closest('.yatra-availability-item');
4322 if (!card) return;
4323
4324 // Remove any existing error message in this card
4325 const existingError = card.querySelector('.yatra-booking-error-toast');
4326 if (existingError) {
4327 existingError.remove();
4328 }
4329
4330 // Create error toast
4331 const toast = document.createElement('div');
4332 toast.className = 'yatra-booking-error-toast';
4333 toast.innerHTML = `
4334 <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;">
4335 <svg width="20" height="20" fill="none" stroke="#dc2626" viewBox="0 0 24 24" style="flex-shrink: 0;">
4336 <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"/>
4337 </svg>
4338 <span style="flex: 1; color: #991b1b; font-size: 13px; font-weight: 500;">${message}</span>
4339 <button type="button" style="background: none; border: none; cursor: pointer; padding: 2px; color: #991b1b; line-height: 1;" onclick="this.parentElement.parentElement.remove()">
4340 <svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
4341 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
4342 </svg>
4343 </button>
4344 </div>
4345 `;
4346
4347 // Insert just before the button (or button's parent row)
4348 const buttonRow = button.closest('.yatra-card-actions') || button.parentElement;
4349 if (buttonRow) {
4350 buttonRow.insertAdjacentElement('beforebegin', toast);
4351 } else {
4352 button.insertAdjacentElement('beforebegin', toast);
4353 }
4354
4355 // Auto-remove after 8 seconds
4356 setTimeout(() => {
4357 if (toast.parentElement) {
4358 toast.remove();
4359 }
4360 }, 8000);
4361
4362 // Scroll to the error message
4363 toast.scrollIntoView({ behavior: 'smooth', block: 'center' });
4364 }
4365 }
4366
4367 /**
4368 * Itinerary Section Class
4369 * Handles day toggle functionality for itinerary timeline
4370 */
4371 class ItinerarySection {
4372 constructor() {
4373 this.init();
4374 }
4375
4376 init() {
4377 if (document.readyState === 'loading') {
4378 document.addEventListener('DOMContentLoaded', () => this.setup());
4379 } else {
4380 this.setup();
4381 }
4382 }
4383
4384 setup() {
4385 this.container = document.querySelector('.yatra-itinerary-timeline');
4386 if (!this.container) return;
4387
4388 this.days = this.container.querySelectorAll('.yatra-itinerary-day');
4389 this.toggleAllBtn = document.getElementById('yatra-toggle-all');
4390 this.isAllExpanded = false;
4391 this.attachEventListeners();
4392 this.initializeDefaultState();
4393 }
4394
4395 /** First day is expanded via PHP (`.is-day-expanded`); no inline styles. */
4396 initializeDefaultState() {}
4397
4398 attachEventListeners() {
4399 // Toggle All button
4400 if (this.toggleAllBtn) {
4401 this.toggleAllBtn.addEventListener('click', () => this.toggleAll());
4402 }
4403
4404 this.days.forEach(day => {
4405 const header = day.querySelector('.yatra-itinerary-day-header');
4406 const toggle = day.querySelector('.yatra-day-toggle');
4407 const content = day.querySelector('.yatra-itinerary-day-content');
4408
4409 if (header && content) {
4410 header.addEventListener('click', (e) => {
4411 // Don't toggle if clicking on a link inside header
4412 if (e.target.tagName === 'A') return;
4413 this.toggleDay(day, toggle);
4414 });
4415 }
4416 });
4417 }
4418
4419 toggleDay(day, toggle) {
4420 const isExpanded = day.classList.contains('is-day-expanded');
4421 if (isExpanded) {
4422 day.classList.remove('is-day-expanded');
4423 day.setAttribute('data-expanded', 'false');
4424 if (toggle) toggle.setAttribute('aria-expanded', 'false');
4425 } else {
4426 day.classList.add('is-day-expanded');
4427 day.setAttribute('data-expanded', 'true');
4428 if (toggle) toggle.setAttribute('aria-expanded', 'true');
4429 }
4430 }
4431
4432 toggleAll() {
4433 if (this.isAllExpanded) {
4434 this.days.forEach(day => {
4435 const toggle = day.querySelector('.yatra-day-toggle');
4436 day.classList.remove('is-day-expanded');
4437 day.setAttribute('data-expanded', 'false');
4438 if (toggle) toggle.setAttribute('aria-expanded', 'false');
4439 });
4440 this.updateToggleButton(false);
4441 } else {
4442 this.days.forEach(day => {
4443 const toggle = day.querySelector('.yatra-day-toggle');
4444 day.classList.add('is-day-expanded');
4445 day.setAttribute('data-expanded', 'true');
4446 if (toggle) toggle.setAttribute('aria-expanded', 'true');
4447 });
4448 this.updateToggleButton(true);
4449 }
4450 this.isAllExpanded = !this.isAllExpanded;
4451 }
4452
4453 updateToggleButton(isExpanded) {
4454 if (!this.toggleAllBtn) return;
4455 this.toggleAllBtn.classList.toggle('is-all-expanded', isExpanded);
4456 const toggleText = this.toggleAllBtn.querySelector('.toggle-text');
4457 if (toggleText) {
4458 toggleText.textContent = isExpanded ? __('Collapse All', 'yatra') : __('Expand All', 'yatra');
4459 }
4460 }
4461 }
4462
4463 // ============================================
4464 // REVIEW FORM
4465 // ============================================
4466 class ReviewForm {
4467 constructor() {
4468 this.form = document.getElementById('yatra-review-form');
4469 if (this.form) {
4470 this.init();
4471 }
4472 }
4473
4474 init() {
4475 this.setupStarRating();
4476 this.setupFormSubmission();
4477 }
4478
4479 setupStarRating() {
4480 const ratingContainer = this.form.querySelector('.yatra-star-rating-input');
4481 if (!ratingContainer) return;
4482
4483 const ratingInputs = Array.from(ratingContainer.querySelectorAll('input[type="radio"]'));
4484 const ratingLabels = Array.from(ratingContainer.querySelectorAll('.yatra-star-label'));
4485
4486 // Get the rating value for a label by its associated input
4487 const getLabelRating = (label) => {
4488 const forAttr = label.getAttribute('for');
4489 if (forAttr) {
4490 const input = document.getElementById(forAttr);
4491 return input ? parseInt(input.value) : 0;
4492 }
4493 return 0;
4494 };
4495
4496 // Function to update visual state
4497 const updateStars = (rating) => {
4498 ratingLabels.forEach((label) => {
4499 const labelRating = getLabelRating(label);
4500 if (labelRating <= rating) {
4501 label.classList.add('selected');
4502 } else {
4503 label.classList.remove('selected');
4504 }
4505 });
4506 };
4507
4508 // Initialize visual state based on currently checked rating
4509 const checkedInput = ratingContainer.querySelector('input:checked');
4510 if (checkedInput) {
4511 const rating = parseInt(checkedInput.value);
4512 // Use setTimeout to ensure DOM is fully ready
4513 setTimeout(() => {
4514 updateStars(rating);
4515 }, 0);
4516 }
4517
4518 // Handle rating changes via input change event
4519 ratingInputs.forEach(input => {
4520 input.addEventListener('change', () => {
4521 updateStars(parseInt(input.value));
4522 });
4523 });
4524
4525 // Handle direct clicks on labels
4526 ratingLabels.forEach((label) => {
4527 label.addEventListener('click', (e) => {
4528 e.preventDefault();
4529 e.stopPropagation();
4530
4531 const forAttr = label.getAttribute('for');
4532 if (!forAttr) return;
4533
4534 const targetInput = document.getElementById(forAttr);
4535 if (targetInput && !targetInput.disabled) {
4536 targetInput.checked = true;
4537 const ratingValue = parseInt(targetInput.value);
4538 updateStars(ratingValue);
4539 // Trigger change event for any other listeners
4540 targetInput.dispatchEvent(new Event('change', { bubbles: true }));
4541 }
4542 });
4543
4544 // Hover effect
4545 label.addEventListener('mouseenter', () => {
4546 const hoverRating = getLabelRating(label);
4547 ratingLabels.forEach((l) => {
4548 const lRating = getLabelRating(l);
4549 if (lRating <= hoverRating) {
4550 l.classList.add('hover');
4551 } else {
4552 l.classList.remove('hover');
4553 }
4554 });
4555 });
4556 });
4557
4558 // Remove hover effect when leaving the container
4559 ratingContainer.addEventListener('mouseleave', () => {
4560 ratingLabels.forEach(l => l.classList.remove('hover'));
4561 });
4562 }
4563
4564 setupFormSubmission() {
4565 this.form.addEventListener('submit', async (e) => {
4566 e.preventDefault();
4567
4568 const submitBtn = this.form.querySelector('.yatra-review-submit-btn');
4569 const originalText = submitBtn.innerHTML;
4570
4571 // Disable button and show loading
4572 submitBtn.disabled = true;
4573 submitBtn.innerHTML =
4574 '<span class="yatra-loading-spinner"></span> ' + __('Submitting...', 'yatra');
4575
4576 try {
4577 const formData = new FormData(this.form);
4578 formData.append('action', 'yatra_submit_review');
4579
4580 const response = await fetch(yatraTripData?.ajaxUrl || '/wp-admin/admin-ajax.php', {
4581 method: 'POST',
4582 body: formData,
4583 credentials: 'same-origin'
4584 });
4585
4586 const result = await response.json();
4587
4588 if (result.success) {
4589 // Show success message
4590 this.showMessage('success', result.data?.message || __('Thank you for your review!', 'yatra'));
4591
4592 // Check if this was an edit or new submission
4593 const isEdit = this.form.querySelector('input[name="action_type"]')?.value === 'edit';
4594
4595 if (!isEdit) {
4596 // Only reset form for new submissions, not edits
4597 this.form.reset();
4598
4599 // Reset star rating to 5 stars
4600 const fiveStarInput = this.form.querySelector('input[name="rating"][value="5"]');
4601 if (fiveStarInput) {
4602 fiveStarInput.checked = true;
4603 // Also update the visual display
4604 const ratingLabels = this.form.querySelectorAll('.yatra-star-label');
4605 ratingLabels.forEach(label => label.classList.add('selected'));
4606 }
4607 }
4608 } else {
4609 this.showMessage('error', result.data?.message || __('Failed to submit review. Please try again.', 'yatra'));
4610 }
4611 } catch (error) {
4612 console.error('Review submission error:', error);
4613 this.showMessage('error', __('An error occurred. Please try again.', 'yatra'));
4614 } finally {
4615 submitBtn.disabled = false;
4616 submitBtn.innerHTML = originalText;
4617 }
4618 });
4619 }
4620
4621 showMessage(type, message) {
4622 // Remove any existing messages
4623 const existingMsg = this.form.querySelector('.yatra-review-message');
4624 if (existingMsg) existingMsg.remove();
4625
4626 const msgEl = document.createElement('div');
4627 msgEl.className = `yatra-review-message yatra-review-message-${type}`;
4628 msgEl.innerHTML = `<p>${message}</p>`;
4629
4630 this.form.insertBefore(msgEl, this.form.firstChild);
4631
4632 // Auto-remove after 5 seconds
4633 setTimeout(() => {
4634 msgEl.remove();
4635 }, 5000);
4636 }
4637 }
4638
4639 /**
4640 * Similar Adventures Carousel
4641 * Handles carousel navigation for similar trips section
4642 */
4643 class SimilarTripsCarousel {
4644 constructor() {
4645 this.track = null;
4646 this.prevBtn = null;
4647 this.nextBtn = null;
4648 this.items = [];
4649 this.currentPosition = 0;
4650 this.itemWidth = 0;
4651 this.visibleItems = 4;
4652 this.gap = 24;
4653 this.init();
4654 }
4655
4656 init() {
4657 if (document.readyState === 'loading') {
4658 document.addEventListener('DOMContentLoaded', () => this.setup());
4659 } else {
4660 this.setup();
4661 }
4662 }
4663
4664 setup() {
4665 this.track = document.getElementById('similar-carousel');
4666 this.prevBtn = document.getElementById('similar-prev');
4667 this.nextBtn = document.getElementById('similar-next');
4668
4669 if (!this.track || !this.prevBtn || !this.nextBtn) return;
4670
4671 this.items = Array.from(this.track.querySelectorAll('.yatra-carousel-item'));
4672 if (this.items.length === 0) return;
4673
4674 this.calculateDimensions();
4675 this.attachEventListeners();
4676 this.updateButtonStates();
4677
4678 // Recalculate on resize
4679 window.addEventListener('resize', () => {
4680 this.calculateDimensions();
4681 this.updateButtonStates();
4682 });
4683 }
4684
4685 calculateDimensions() {
4686 const wrapper = this.track.parentElement;
4687 const wrapperWidth = wrapper.offsetWidth;
4688
4689 // Determine visible items based on screen width
4690 if (window.innerWidth <= 600) {
4691 this.visibleItems = 1;
4692 } else if (window.innerWidth <= 900) {
4693 this.visibleItems = 2;
4694 } else if (window.innerWidth <= 1200) {
4695 this.visibleItems = 3;
4696 } else {
4697 this.visibleItems = 4;
4698 }
4699
4700 // Calculate item width
4701 this.itemWidth = (wrapperWidth - (this.gap * (this.visibleItems - 1))) / this.visibleItems;
4702
4703 // Apply width to items
4704 this.items.forEach(item => {
4705 item.style.flex = `0 0 ${this.itemWidth}px`;
4706 item.style.minWidth = `${this.itemWidth}px`;
4707 });
4708
4709 // Reset position if out of bounds
4710 const maxPosition = Math.max(0, this.items.length - this.visibleItems);
4711 if (this.currentPosition > maxPosition) {
4712 this.currentPosition = maxPosition;
4713 this.updateTrackPosition();
4714 }
4715 }
4716
4717 attachEventListeners() {
4718 this.prevBtn.addEventListener('click', () => this.slide('prev'));
4719 this.nextBtn.addEventListener('click', () => this.slide('next'));
4720
4721 // Touch/swipe support for mobile
4722 let startX = 0;
4723 let currentX = 0;
4724 let isDragging = false;
4725
4726 this.track.addEventListener('touchstart', (e) => {
4727 startX = e.touches[0].clientX;
4728 isDragging = true;
4729 }, { passive: true });
4730
4731 this.track.addEventListener('touchmove', (e) => {
4732 if (!isDragging) return;
4733 currentX = e.touches[0].clientX;
4734 }, { passive: true });
4735
4736 this.track.addEventListener('touchend', () => {
4737 if (!isDragging) return;
4738 isDragging = false;
4739
4740 const diff = startX - currentX;
4741 const threshold = 50;
4742
4743 if (diff > threshold) {
4744 this.slide('next');
4745 } else if (diff < -threshold) {
4746 this.slide('prev');
4747 }
4748 });
4749 }
4750
4751 slide(direction) {
4752 const maxPosition = Math.max(0, this.items.length - this.visibleItems);
4753
4754 if (direction === 'next' && this.currentPosition < maxPosition) {
4755 this.currentPosition++;
4756 } else if (direction === 'prev' && this.currentPosition > 0) {
4757 this.currentPosition--;
4758 }
4759
4760 this.updateTrackPosition();
4761 this.updateButtonStates();
4762 }
4763
4764 updateTrackPosition() {
4765 const offset = this.currentPosition * (this.itemWidth + this.gap);
4766 this.track.style.transform = `translateX(-${offset}px)`;
4767 }
4768
4769 updateButtonStates() {
4770 const maxPosition = Math.max(0, this.items.length - this.visibleItems);
4771
4772 this.prevBtn.disabled = this.currentPosition <= 0;
4773 this.nextBtn.disabled = this.currentPosition >= maxPosition;
4774
4775 // Hide navigation if all items are visible
4776 const shouldHideNav = this.items.length <= this.visibleItems;
4777 this.prevBtn.style.display = shouldHideNav ? 'none' : 'flex';
4778 this.nextBtn.style.display = shouldHideNav ? 'none' : 'flex';
4779 }
4780 }
4781
4782
4783 /**
4784 * Wishlist Handler
4785 * Handles save/remove trip from wishlist functionality
4786 */
4787 class WishlistHandler {
4788 constructor() {
4789 // Wishlist clicks are now handled exclusively by listing-wishlist.js
4790 // (which covers every .yatra-favorite-btn on listing, single-trip hero,
4791 // and similar-trips). Registering this second handler caused every click
4792 // to fire two REST requests racing against each other.
4793 return;
4794 }
4795
4796 init() {
4797 // Check if user is logged in and update button states
4798 if (this.isLoggedIn && this.tripId) {
4799 this.checkSavedStatus();
4800 }
4801
4802 // Handle wishlist button clicks
4803 document.addEventListener('click', (e) => {
4804 const btn = e.target.closest('.yatra-favorite-btn');
4805 if (btn) {
4806 e.preventDefault();
4807 e.stopPropagation();
4808 this.handleWishlistClick(btn);
4809 }
4810 });
4811 }
4812
4813 async checkSavedStatus() {
4814 if (!this.tripId) return;
4815
4816 try {
4817 const response = await fetch(
4818 `${this.apiUrl}/saved-trips/check/${this.tripId}`,
4819 {
4820 method: 'GET',
4821 headers: {
4822 'X-WP-Nonce': this.nonce
4823 },
4824 credentials: 'same-origin'
4825 }
4826 );
4827
4828 const data = await response.json();
4829 if (data.success && data.data.is_saved) {
4830 this.updateButtonState(true);
4831 }
4832 } catch (error) {
4833 console.error('Error checking wishlist status:', error);
4834 }
4835 }
4836
4837 handleWishlistClick(btn) {
4838 if (!this.isLoggedIn) {
4839 this.showLoginPopup();
4840 return;
4841 }
4842
4843 const btnTripId = parseInt(btn.getAttribute('data-trip-id'), 10) || this.tripId;
4844 if (!btnTripId) {
4845 return;
4846 }
4847
4848 const isSaved = btn.classList.contains('saved');
4849 if (isSaved) {
4850 this.removeFromWishlist(btn, btnTripId);
4851 } else {
4852 this.addToWishlist(btn, btnTripId);
4853 }
4854 }
4855
4856 async addToWishlist(btn, tripId) {
4857 const tid = tripId || this.tripId;
4858 btn.disabled = true;
4859 btn.classList.add('loading');
4860
4861 try {
4862 // application/x-www-form-urlencoded is parsed reliably across hosts; JSON body can be dropped on some stacks.
4863 const params = new URLSearchParams();
4864 params.set('trip_id', String(tid));
4865 const response = await fetch(`${this.apiUrl}/saved-trips`, {
4866 method: 'POST',
4867 headers: {
4868 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
4869 'X-WP-Nonce': this.nonce
4870 },
4871 credentials: 'same-origin',
4872 body: params.toString()
4873 });
4874
4875 const data = await response.json();
4876 if (data.success) {
4877 this.updateButtonState(true, tid);
4878 this.showMessage(data.message || __('Trip saved to wishlist', 'yatra'), 'success', btn);
4879 } else {
4880 this.showMessage(data.message || __('Failed to save trip', 'yatra'), 'error', btn);
4881 }
4882 } catch (error) {
4883 console.error('Error saving trip:', error);
4884 this.showMessage(__('An error occurred. Please try again.', 'yatra'), 'error', btn);
4885 } finally {
4886 btn.disabled = false;
4887 btn.classList.remove('loading');
4888 }
4889 }
4890
4891 async removeFromWishlist(btn, tripId) {
4892 const tid = tripId || this.tripId;
4893 btn.disabled = true;
4894 btn.classList.add('loading');
4895
4896 try {
4897 const response = await fetch(`${this.apiUrl}/saved-trips/${tid}`, {
4898 method: 'DELETE',
4899 headers: {
4900 'X-WP-Nonce': this.nonce
4901 },
4902 credentials: 'same-origin'
4903 });
4904
4905 const data = await response.json();
4906 if (data.success) {
4907 this.updateButtonState(false, tid);
4908 this.showMessage(data.message || __('Trip removed from wishlist', 'yatra'), 'success', btn);
4909 } else {
4910 this.showMessage(data.message || __('Failed to remove trip', 'yatra'), 'error', btn);
4911 }
4912 } catch (error) {
4913 console.error('Error removing trip:', error);
4914 this.showMessage(__('An error occurred. Please try again.', 'yatra'), 'error', btn);
4915 } finally {
4916 btn.disabled = false;
4917 btn.classList.remove('loading');
4918 }
4919 }
4920
4921 updateButtonState(isSaved, tripId) {
4922 const tid = tripId || this.tripId;
4923 const selector = tid
4924 ? `.yatra-favorite-btn[data-trip-id="${tid}"]`
4925 : '.yatra-favorite-btn';
4926 const buttons = document.querySelectorAll(selector);
4927 buttons.forEach(btn => {
4928 if (isSaved) {
4929 btn.classList.add('saved', 'is-saved');
4930 btn.setAttribute('aria-label', __('Remove from favorites', 'yatra'));
4931 // Fill the heart icon and change color
4932 const svg = btn.querySelector('svg');
4933 if (svg) {
4934 svg.style.fill = 'currentColor';
4935 svg.style.stroke = 'currentColor';
4936 btn.style.color = '#ef4444';
4937 }
4938 } else {
4939 btn.classList.remove('saved', 'is-saved');
4940 btn.setAttribute('aria-label', __('Add to favorites', 'yatra'));
4941 // Unfill the heart icon
4942 const svg = btn.querySelector('svg');
4943 if (svg) {
4944 svg.style.fill = 'none';
4945 svg.style.stroke = 'currentColor';
4946 btn.style.color = '';
4947 }
4948 }
4949 });
4950 }
4951
4952 showLoginPopup() {
4953 // Check if enquiry modal exists and reuse its structure, or create a simple login prompt
4954 const enquiryModal = document.getElementById('enquiry-modal');
4955 if (enquiryModal) {
4956 // Show enquiry modal with login message
4957 enquiryModal.classList.add('active');
4958 document.body.style.overflow = 'hidden';
4959
4960 // Update modal content to show login message
4961 const modalContent = enquiryModal.querySelector('.yatra-enquiry-modal-content');
4962 if (modalContent) {
4963 const loginMessage = document.createElement('div');
4964 loginMessage.className = 'yatra-login-prompt';
4965 loginMessage.style.cssText = 'padding: 40px; text-align: center;';
4966 const loginRequiredHeading = __('Login Required', 'yatra');
4967 const loginRequiredBody = __('Please login to save trips to your wishlist.', 'yatra');
4968 const loginButtonLabel = __('Login', 'yatra');
4969 const cancelButtonLabel = __('Cancel', 'yatra');
4970 loginMessage.innerHTML = `
4971 <h3 style="margin-bottom: 20px;">${loginRequiredHeading}</h3>
4972 <p style="margin-bottom: 30px;">${loginRequiredBody}</p>
4973 <a href="${window.yatraTripData?.loginUrl || '/wp-login.php'}" class="yatra-btn" style="display: inline-block; margin-right: 10px;">${loginButtonLabel}</a>
4974 <button type="button" class="yatra-btn-secondary" onclick="this.closest('.yatra-enquiry-modal').classList.remove('active'); document.body.style.overflow = '';">${cancelButtonLabel}</button>
4975 `;
4976 modalContent.innerHTML = '';
4977 modalContent.appendChild(loginMessage);
4978 }
4979 } else {
4980 // Fallback: show alert and redirect to login
4981 if (confirm(__('Please login to save trips to your wishlist. Would you like to go to the login page?', 'yatra'))) {
4982 window.location.href = window.yatraTripData?.loginUrl || '/wp-login.php';
4983 }
4984 }
4985 }
4986
4987 showMessage(message, type, button) {
4988 // Remove existing toast if any
4989 const existingToast = document.getElementById('yatra-wishlist-toast');
4990 if (existingToast) {
4991 existingToast.remove();
4992 }
4993
4994 // Create toast near the button
4995 const toast = document.createElement('div');
4996 toast.id = 'yatra-wishlist-toast';
4997 toast.textContent = message;
4998
4999 // Position relative to button
5000 const rect = button.getBoundingClientRect();
5001 const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
5002 const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft;
5003
5004 toast.style.cssText = `
5005 position: absolute;
5006 top: ${rect.bottom + scrollTop + 10}px;
5007 left: ${rect.left + scrollLeft}px;
5008 padding: 10px 16px;
5009 border-radius: 8px;
5010 z-index: 10001;
5011 box-shadow: 0 4px 12px rgba(0,0,0,0.2);
5012 transition: all 0.3s ease;
5013 white-space: nowrap;
5014 font-size: 14px;
5015 pointer-events: none;
5016 background-color: ${type === 'success' ? '#10b981' : '#ef4444'};
5017 color: #ffffff;
5018 opacity: 0;
5019 transform: translateY(-10px);
5020 `;
5021
5022 document.body.appendChild(toast);
5023
5024 // Animate in
5025 requestAnimationFrame(() => {
5026 toast.style.opacity = '1';
5027 toast.style.transform = 'translateY(0)';
5028 });
5029
5030 // Remove after delay
5031 setTimeout(() => {
5032 toast.style.opacity = '0';
5033 toast.style.transform = 'translateY(-10px)';
5034 setTimeout(() => {
5035 if (toast.parentNode) {
5036 toast.remove();
5037 }
5038 }, 300);
5039 }, 3000);
5040 }
5041 }
5042
5043
5044 /**
5045 * Trip Page Orchestrator
5046 * Centralized bootstrapper to keep initialization class-based and organized
5047 */
5048 class TripPage {
5049 constructor() {
5050 this.instances = {};
5051 }
5052
5053 init() {
5054 // Create all feature instances
5055 this.instances.galleryModal = new GalleryModal();
5056 this.instances.itineraryGalleryModal = new ItineraryGalleryModal();
5057 this.instances.heroMediaSwitcher = new HeroMediaSwitcher();
5058 this.instances.bookingSidebar = new BookingSidebar();
5059 this.instances.enquiryModal = new EnquiryModal();
5060 this.instances.stickyNav = new StickyNav();
5061 this.instances.wishlistHandler = new WishlistHandler();
5062 this.instances.availabilitySection = new AvailabilitySection();
5063 this.instances.itinerarySection = new ItinerarySection();
5064 this.instances.reviewForm = new ReviewForm();
5065 this.instances.similarTripsCarousel = new SimilarTripsCarousel();
5066
5067 // Expose for legacy access (existing templates rely on window.*)
5068 this.exposeGlobals();
5069
5070 // Extra page-level behaviors
5071 this.initHeroSmoothScroll();
5072 this.initGroupDiscountPopovers();
5073
5074 return this.instances;
5075 }
5076
5077 /**
5078 * Group discount popover in booking sidebar / mobile sticky (tap outside to close, Esc to close).
5079 */
5080 initGroupDiscountPopovers() {
5081 const syncOpenState = (wrap, open) => {
5082 const btn = wrap.querySelector('.yatra-group-discount-popover__trigger');
5083 const panel = wrap.querySelector('.yatra-group-discount-popover__panel');
5084 wrap.classList.toggle('is-open', open);
5085 if (btn) {
5086 btn.setAttribute('aria-expanded', open ? 'true' : 'false');
5087 }
5088 if (panel) {
5089 panel.setAttribute('aria-hidden', open ? 'false' : 'true');
5090 }
5091 };
5092
5093 const closeAll = (exceptWrap = null) => {
5094 document.querySelectorAll('[data-yatra-group-discount-popover]').forEach((w) => {
5095 if (exceptWrap && w === exceptWrap) {
5096 return;
5097 }
5098 syncOpenState(w, false);
5099 });
5100 };
5101
5102 const attach = () => {
5103 document.querySelectorAll('[data-yatra-group-discount-popover]').forEach((wrap) => {
5104 const btn = wrap.querySelector('.yatra-group-discount-popover__trigger');
5105 if (!btn || btn.dataset.yatraGdBound === '1') {
5106 return;
5107 }
5108 btn.dataset.yatraGdBound = '1';
5109 btn.addEventListener('click', (e) => {
5110 e.preventDefault();
5111 e.stopPropagation();
5112 const open = !wrap.classList.contains('is-open');
5113 if (open) {
5114 closeAll(wrap);
5115 syncOpenState(wrap, true);
5116 } else {
5117 syncOpenState(wrap, false);
5118 }
5119 });
5120 });
5121
5122 if (document.documentElement.dataset.yatraGroupDiscountPopoverDocBound === '1') {
5123 return;
5124 }
5125 document.documentElement.dataset.yatraGroupDiscountPopoverDocBound = '1';
5126
5127 document.addEventListener('click', (e) => {
5128 if (!e.target.closest('[data-yatra-group-discount-popover]')) {
5129 closeAll();
5130 }
5131 });
5132
5133 document.addEventListener('keydown', (e) => {
5134 if (e.key !== 'Escape') {
5135 return;
5136 }
5137 const openWrap = document.querySelector('[data-yatra-group-discount-popover].is-open');
5138 if (openWrap) {
5139 syncOpenState(openWrap, false);
5140 const btn = openWrap.querySelector('.yatra-group-discount-popover__trigger');
5141 if (btn) {
5142 btn.focus();
5143 }
5144 }
5145 });
5146 };
5147
5148 if (document.readyState === 'loading') {
5149 document.addEventListener('DOMContentLoaded', attach);
5150 } else {
5151 attach();
5152 }
5153 }
5154
5155 exposeGlobals() {
5156 Object.entries(this.instances).forEach(([key, instance]) => {
5157 window[key] = instance;
5158 });
5159 // Templates use window.YatraEnquiry.open() — alias to EnquiryModal instance
5160 if (this.instances.enquiryModal) {
5161 window.YatraEnquiry = this.instances.enquiryModal;
5162 }
5163 // Also expose orchestrator for potential future hooks
5164 window.tripPage = this;
5165 }
5166
5167 initHeroSmoothScroll() {
5168 const attachScroll = () => {
5169 document.querySelectorAll('a[href="#yatra-booking-widget"]').forEach((link) => {
5170 link.addEventListener('click', (e) => {
5171 e.preventDefault();
5172 const target = document.getElementById('yatra-booking-widget');
5173 if (target) {
5174 target.scrollIntoView({ behavior: 'smooth', block: 'start' });
5175 }
5176 });
5177 });
5178 };
5179
5180 if (document.readyState === 'loading') {
5181 document.addEventListener('DOMContentLoaded', attachScroll);
5182 } else {
5183 attachScroll();
5184 }
5185 }
5186
5187 static bootstrap() {
5188 const start = () => {
5189 new TripPage().init();
5190 new ItineraryVideoHandler(); // Initialize itinerary video handler
5191 };
5192 if (document.readyState === 'loading') {
5193 document.addEventListener('DOMContentLoaded', start);
5194 } else {
5195 start();
5196 }
5197 }
5198 }
5199
5200 /**
5201 * Itinerary Video Handler Class
5202 * Handles video player functionality for itinerary section
5203 */
5204 class ItineraryVideoHandler {
5205 constructor() {
5206 this.init();
5207 }
5208
5209 init() {
5210 if (document.readyState === 'loading') {
5211 document.addEventListener('DOMContentLoaded', () => this.setup());
5212 } else {
5213 this.setup();
5214 }
5215 }
5216
5217 setup() {
5218 // Handle itinerary video clicks on overlay (both in gallery and separate video section)
5219 const videoOverlays = document.querySelectorAll('.yatra-entry-gallery .yatra-media-overlay, .yatra-entry-video .yatra-media-overlay');
5220 videoOverlays.forEach(overlay => {
5221 overlay.addEventListener('click', (e) => {
5222 e.preventDefault();
5223 e.stopPropagation();
5224 e.stopImmediatePropagation();
5225
5226 const link = overlay.closest('a');
5227 if (!link) return;
5228
5229 const videoUrl = link.getAttribute('href');
5230 if (!videoUrl || videoUrl === '#') return;
5231
5232 const img = overlay.closest('.yatra-media-card').querySelector('img');
5233 const videoTitle = img ? img.alt : __('Itinerary Video', 'yatra');
5234
5235 // Extract video ID from URL
5236 let videoId = '';
5237 if (videoUrl.includes('youtube.com') || videoUrl.includes('youtu.be')) {
5238 // YouTube video ID extraction
5239 const matches = videoUrl.match(/(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/|youtube\.com\/shorts\/|youtube\.com\/live\/)([^&\n?#\/]+)/);
5240 videoId = matches ? matches[1] : '';
5241 } else if (videoUrl.includes('vimeo.com')) {
5242 // Vimeo video ID extraction
5243 const matches = videoUrl.match(/vimeo\.com\/(?:.*#|.\/videos\/)?([0-9]+)/);
5244 videoId = matches ? matches[1] : '';
5245 }
5246
5247 // Play video using YatraVideoPlayer
5248 if (window.YatraVideoPlayer) {
5249 const videoData = {
5250 url: videoUrl,
5251 title: videoTitle
5252 };
5253
5254 // Only add video_id for YouTube videos, let player detect URL type for others
5255 if (videoUrl.includes('youtube.com') || videoUrl.includes('youtu.be')) {
5256 videoData.video_id = videoId;
5257 }
5258
5259 window.YatraVideoPlayer.play(videoData);
5260 }
5261 });
5262 });
5263
5264 // Prevent video links from opening in new tab (both in gallery and separate video section)
5265 const videoLinks = document.querySelectorAll('.yatra-entry-gallery .yatra-itinerary-video-link, .yatra-entry-video .yatra-itinerary-video-link');
5266 videoLinks.forEach(link => {
5267 link.addEventListener('click', (e) => {
5268 e.preventDefault();
5269 return false;
5270 });
5271 });
5272 }
5273 }
5274
5275 /**
5276 * Downloads Handler Class
5277 * Handles download section interactions including preview modal
5278 */
5279 class DownloadsHandler {
5280 constructor() {
5281 this.previewModal = null;
5282 this.currentDownloadId = null;
5283 this.init();
5284 }
5285
5286 init() {
5287 if (document.readyState === 'loading') {
5288 document.addEventListener('DOMContentLoaded', () => this.setup());
5289 } else {
5290 this.setup();
5291 }
5292 }
5293
5294 setup() {
5295 this.previewModal = document.getElementById('download-preview-modal');
5296 if (!this.previewModal) return;
5297
5298 this.attachEventListeners();
5299 }
5300
5301 attachEventListeners() {
5302 // Handle download button clicks
5303 document.addEventListener('click', (e) => {
5304 if (e.target.closest('.yatra-download-btn')) {
5305 e.preventDefault();
5306 const btn = e.target.closest('.yatra-download-btn');
5307 const downloadId = btn.dataset.downloadId;
5308 if (downloadId) {
5309 this.downloadFile(downloadId);
5310 }
5311 }
5312
5313 // Handle view/preview link clicks
5314 if (e.target.closest('.yatra-download-view-link, .yatra-download-preview-link')) {
5315 e.preventDefault();
5316 const link = e.target.closest('.yatra-download-view-link, .yatra-download-preview-link');
5317 const downloadId = link.dataset.downloadId;
5318 if (downloadId) {
5319 this.previewFile(downloadId);
5320 }
5321 }
5322
5323 // Handle modal close
5324 if (e.target.closest('.yatra-preview-modal-close, .yatra-preview-close-btn')) {
5325 this.closePreviewModal();
5326 }
5327
5328 // Handle modal overlay click
5329 if (e.target.classList.contains('yatra-preview-modal-overlay')) {
5330 this.closePreviewModal();
5331 }
5332
5333 // Handle preview modal download button
5334 if (e.target.closest('#preview-download-btn')) {
5335 if (this.currentDownloadId) {
5336 this.downloadFile(this.currentDownloadId);
5337 }
5338 }
5339 });
5340
5341 // Handle ESC key to close modal
5342 document.addEventListener('keydown', (e) => {
5343 if (e.key === 'Escape' && this.previewModal.style.display === 'block') {
5344 this.closePreviewModal();
5345 }
5346 });
5347 }
5348
5349 async downloadFile(downloadId) {
5350 try {
5351 // Get download URL with signature using centralized helper
5352 const response = await window.YatraApiHelper.getDownloadInfo(downloadId);
5353 if (!response.ok) {
5354 throw new Error(__('Download failed', 'yatra'));
5355 }
5356
5357 const data = await response.json();
5358
5359 if (data.download_url) {
5360 // Create temporary link and trigger download
5361 const link = document.createElement('a');
5362 link.href = data.download_url;
5363 link.download = ''; // Let server determine filename
5364 document.body.appendChild(link);
5365 link.click();
5366 document.body.removeChild(link);
5367 } else {
5368 throw new Error('Download URL not found');
5369 }
5370 } catch (error) {
5371 console.error('Download error:', error);
5372 // Show error message to user
5373 this.showNotification(__('Download failed. Please try again.', 'yatra'), 'error');
5374 }
5375 }
5376
5377 async previewFile(downloadId) {
5378 try {
5379 this.currentDownloadId = downloadId;
5380 this.showPreviewModal();
5381
5382 // Get download info using centralized helper
5383 const response = await window.YatraApiHelper.getDownloadInfo(downloadId);
5384 if (!response.ok) {
5385 throw new Error(__('Failed to get file info', 'yatra'));
5386 }
5387
5388 const data = await response.json();
5389
5390 if (data.download_url) {
5391 this.loadPreview(data.download_url, data.title || __('Preview', 'yatra'));
5392 } else {
5393 throw new Error('File URL not found');
5394 }
5395 } catch (error) {
5396 console.error('Preview error:', error);
5397 this.closePreviewModal();
5398 this.showNotification(__('Failed to load preview. Please try again.', 'yatra'), 'error');
5399 }
5400 }
5401
5402 loadPreview(fileUrl, title) {
5403 const previewContent = document.getElementById('preview-content');
5404 const modalTitle = document.querySelector('.yatra-preview-modal-title');
5405
5406 if (modalTitle) {
5407 modalTitle.textContent = title;
5408 }
5409
5410 if (!previewContent) return;
5411
5412 // Hide loading and show preview
5413 const loading = previewContent.querySelector('.yatra-preview-loading');
5414 if (loading) {
5415 loading.style.display = 'none';
5416 }
5417
5418 // Check file type and load appropriate preview
5419 const fileType = this.getFileType(fileUrl);
5420
5421 if (fileType === 'image') {
5422 previewContent.innerHTML = `<img src="${fileUrl}" alt="${title}" style="max-width: 100%; height: auto;">`;
5423 } else if (fileType === 'pdf') {
5424 previewContent.innerHTML = `
5425 <iframe src="${fileUrl}" style="width: 100%; height: 500px; border: none; border-radius: 8px;"></iframe>
5426 `;
5427 } else {
5428 const previewUnavailableTitle = __('Preview not available for this file type', 'yatra');
5429 const previewUnavailableHint = __('Please download the file to view its contents', 'yatra');
5430 previewContent.innerHTML = `
5431 <div style="text-align: center; padding: 40px;">
5432 <div style="font-size: 48px; margin-bottom: 16px; opacity: 0.5;">📄</div>
5433 <p style="color: #6b7280; margin-bottom: 16px;">${previewUnavailableTitle}</p>
5434 <p style="color: #9ca3af; font-size: 14px;">${previewUnavailableHint}</p>
5435 </div>
5436 `;
5437 }
5438 }
5439
5440 getFileType(url) {
5441 const extension = url.split('.').pop().toLowerCase();
5442 const imageTypes = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'];
5443 const pdfTypes = ['pdf'];
5444
5445 if (imageTypes.includes(extension)) return 'image';
5446 if (pdfTypes.includes(extension)) return 'pdf';
5447 return 'other';
5448 }
5449
5450 showPreviewModal() {
5451 if (this.previewModal) {
5452 this.previewModal.style.display = 'block';
5453 document.body.style.overflow = 'hidden';
5454
5455 // Reset loading state
5456 const previewContent = document.getElementById('preview-content');
5457 if (previewContent) {
5458 const loadingPreviewLabel = __('Loading preview...', 'yatra');
5459 previewContent.innerHTML = `
5460 <div class="yatra-preview-loading">
5461 <div class="yatra-preview-spinner"></div>
5462 <p>${loadingPreviewLabel}</p>
5463 </div>
5464 `;
5465 }
5466 }
5467 }
5468
5469 closePreviewModal() {
5470 if (this.previewModal) {
5471 this.previewModal.style.display = 'none';
5472 document.body.style.overflow = '';
5473 this.currentDownloadId = null;
5474 }
5475 }
5476
5477 showNotification(message, type = 'info') {
5478 // Simple notification - you can enhance this with a proper notification system
5479 const notification = document.createElement('div');
5480 notification.style.cssText = `
5481 position: fixed;
5482 top: 20px;
5483 right: 20px;
5484 background: ${type === 'error' ? '#ef4444' : '#3b82f6'};
5485 color: white;
5486 padding: 12px 20px;
5487 border-radius: 8px;
5488 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
5489 z-index: 10000;
5490 font-size: 14px;
5491 font-weight: 500;
5492 `;
5493 notification.textContent = message;
5494
5495 document.body.appendChild(notification);
5496
5497 setTimeout(() => {
5498 notification.style.opacity = '0';
5499 notification.style.transition = 'opacity 0.3s ease';
5500 setTimeout(() => {
5501 document.body.removeChild(notification);
5502 }, 300);
5503 }, 3000);
5504 }
5505 }
5506
5507 // Initialize downloads handler
5508 new DownloadsHandler();
5509
5510 // Kick things off
5511 TripPage.bootstrap();
5512
5513 })();
5514