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

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