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

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

5,138 lines 177.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Yatra Single Trip Page JavaScript
3 * Class-based implementation for trip page features
4 */
5 /* jshint esversion: 6 */
6
7 (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.heroBookNowBtn = null;
1273 this.dateInput = null;
1274 this.participantsSelect = null;
1275 this.datepickerInstance = null;
1276 this.init();
1277 }
1278
1279 init() {
1280 if (document.readyState === 'loading') {
1281 document.addEventListener('DOMContentLoaded', () => this.setup());
1282 } else {
1283 this.setup();
1284 }
1285 }
1286
1287 setup() {
1288 this.checkAvailabilityBtn = document.getElementById('check-availability-btn');
1289 this.makeEnquiryBtn = document.getElementById('open-enquiry-modal');
1290 this.heroBookNowBtn = document.getElementById('yatra-hero-book-now-btn');
1291 this.dateInput = document.getElementById('travel_date');
1292 this.participantsSelect = document.querySelector('.yatra-participants-select');
1293
1294 if (!this.checkAvailabilityBtn) {
1295 console.warn('Check Availability button not found');
1296 }
1297
1298 this.attachEventListeners();
1299 this.initDateField();
1300 this.initTravelersField();
1301 }
1302
1303 attachEventListeners() {
1304 // Check Availability button
1305 if (this.checkAvailabilityBtn) {
1306 this.checkAvailabilityBtn.addEventListener('click', (e) => {
1307 e.preventDefault();
1308 this.handleCheckAvailability();
1309 });
1310 }
1311
1312 // Hero "Book Now" (should behave like Check Availability)
1313 if (this.heroBookNowBtn) {
1314 this.heroBookNowBtn.addEventListener('click', (e) => {
1315 e.preventDefault();
1316 const disabled =
1317 this.heroBookNowBtn.classList.contains('is-disabled') ||
1318 this.heroBookNowBtn.getAttribute('aria-disabled') === 'true';
1319 if (disabled) return;
1320
1321 const bookingSidebar = document.getElementById('booking') || document.querySelector('.yatra-trip-sidebar');
1322 if (bookingSidebar && bookingSidebar.scrollIntoView) {
1323 bookingSidebar.scrollIntoView({ behavior: 'smooth', block: 'start' });
1324 }
1325
1326 // Trigger the same availability flow
1327 if (this.checkAvailabilityBtn) {
1328 this.handleCheckAvailability();
1329 }
1330 });
1331 }
1332
1333 // Make Enquiry button
1334 if (this.makeEnquiryBtn) {
1335 this.makeEnquiryBtn.addEventListener('click', (e) => {
1336 e.preventDefault();
1337 this.handleMakeEnquiry();
1338 });
1339 }
1340 }
1341
1342 initDateField() {
1343 if (!this.dateInput || typeof flatpickr === 'undefined') return;
1344
1345 const today = new Date();
1346 today.setHours(0, 0, 0, 0);
1347
1348 // Format today's date as Y-m-d
1349 const todayStr = today.getFullYear() + '-' +
1350 String(today.getMonth() + 1).padStart(2, '0') + '-' +
1351 String(today.getDate()).padStart(2, '0');
1352
1353 // Set today's date as default value
1354 this.dateInput.value = todayStr;
1355
1356 // Enable button since we have a default date
1357 if (this.checkAvailabilityBtn) {
1358 this.checkAvailabilityBtn.disabled = false;
1359 this.checkAvailabilityBtn.classList.remove('disabled');
1360 }
1361
1362 // Destroy existing instance if any
1363 if (this.dateInput._flatpickr) {
1364 this.dateInput._flatpickr.destroy();
1365 }
1366
1367 const altFormat = getTripAltDateFormat();
1368
1369 this.datepickerInstance = flatpickr(this.dateInput, {
1370 // Keep backend value stable (Y-m-d), only change display
1371 dateFormat: 'Y-m-d',
1372 altInput: true,
1373 altFormat,
1374 minDate: today,
1375 defaultDate: today,
1376 enableTime: false,
1377 clickOpens: true,
1378 allowInput: false,
1379 static: false,
1380 monthSelectorType: 'static',
1381 animate: true,
1382 locale: {
1383 firstDayOfWeek: 1
1384 },
1385 onChange: (selectedDates, dateStr) => {
1386 this.dateInput.value = dateStr;
1387
1388 // Enable Check Availability button when date is selected
1389 if (this.checkAvailabilityBtn && dateStr) {
1390 this.checkAvailabilityBtn.disabled = false;
1391 this.checkAvailabilityBtn.classList.remove('disabled');
1392 }
1393 }
1394 });
1395
1396 // Make container clickable
1397 const container = this.dateInput.closest('.yatra-booking-field-select');
1398 if (container) {
1399 container.style.cursor = 'pointer';
1400 const openPicker = (e) => {
1401 // Do not interfere with other actionable controls in the same row
1402 if (e && e.target && e.target.closest && e.target.closest('button, a, select, textarea')) {
1403 return;
1404 }
1405 // Prevent container clicks from triggering other toggles
1406 if (e) {
1407 e.preventDefault();
1408 e.stopPropagation();
1409 }
1410 if (this.datepickerInstance && !this.datepickerInstance.isOpen) {
1411 this.datepickerInstance.open();
1412 }
1413 };
1414
1415 // Container click (icon/arrow/background)
1416 container.addEventListener('click', openPicker);
1417
1418 // Flatpickr altInput is the visible field when altInput:true
1419 if (this.dateInput && this.dateInput._flatpickr && this.dateInput._flatpickr.altInput) {
1420 this.dateInput._flatpickr.altInput.addEventListener('click', openPicker);
1421 }
1422 }
1423 }
1424
1425 initInlineQuantityControls() {
1426 // Handle inline quantity controls for simple booking (num_travelers)
1427 const inlineControls = document.querySelectorAll('.yatra-quantity-controls-inline');
1428
1429 inlineControls.forEach(controlsContainer => {
1430 const input = controlsContainer.querySelector('.yatra-quantity-input-simple');
1431 if (!input) return;
1432
1433 const minusBtn = controlsContainer.querySelector('.yatra-quantity-minus');
1434 const plusBtn = controlsContainer.querySelector('.yatra-quantity-plus');
1435
1436 if (!minusBtn || !plusBtn) return;
1437
1438 const updateButtonStates = () => {
1439 const current = parseInt(input.value) || 1;
1440 const min = parseInt(input.getAttribute('min')) || 1;
1441 const max = parseInt(input.getAttribute('max')) || 20;
1442
1443 minusBtn.disabled = current <= min;
1444 plusBtn.disabled = current >= max;
1445 };
1446
1447 // Minus button click
1448 minusBtn.addEventListener('click', (e) => {
1449 e.preventDefault();
1450 e.stopPropagation();
1451
1452 const current = parseInt(input.value) || 1;
1453 const min = parseInt(input.getAttribute('min')) || 1;
1454
1455 if (current > min) {
1456 input.value = current - 1;
1457 updateButtonStates();
1458 this.updateTotalDisplay();
1459 }
1460 });
1461
1462 // Plus button click
1463 plusBtn.addEventListener('click', (e) => {
1464 e.preventDefault();
1465 e.stopPropagation();
1466
1467 const current = parseInt(input.value) || 1;
1468 const max = parseInt(input.getAttribute('max')) || 20;
1469
1470 if (current < max) {
1471 input.value = current + 1;
1472 updateButtonStates();
1473 this.updateTotalDisplay();
1474 }
1475 });
1476
1477 // Initialize button states
1478 updateButtonStates();
1479 });
1480 }
1481
1482 updateTotalDisplay() {
1483 // Update total price display based on current travelers count
1484 const numTravelersInput = document.getElementById('num_travelers');
1485 const totalAmountEl = document.getElementById('total-amount');
1486 const displayPriceEl = document.getElementById('display-price');
1487
1488 if (!numTravelersInput) return;
1489
1490 const travelers = parseInt(numTravelersInput.value) || 1;
1491 const pricePerPerson = parseFloat(numTravelersInput.getAttribute('data-price')) || 0;
1492 const totalPrice = travelers * pricePerPerson;
1493
1494 // Update total display if exists
1495 if (totalAmountEl && typeof yatra_format_price_js === 'function') {
1496 totalAmountEl.textContent = yatra_format_price_js(totalPrice);
1497 }
1498 }
1499
1500 initTravelersField() {
1501 // First, initialize inline quantity controls for num_travelers (simple booking)
1502 this.initInlineQuantityControls();
1503
1504 // Initialize all dropdown travelers fields (there may be multiple instances)
1505 const participantsSelects = document.querySelectorAll('.yatra-participants-select');
1506
1507 participantsSelects.forEach((participantsSelect) => {
1508 this.initSingleTravelersField(participantsSelect);
1509 });
1510 }
1511
1512 initSingleTravelersField(participantsSelect) {
1513 if (!participantsSelect) return;
1514
1515 // Find display and selector within this specific participants select container
1516 const display = participantsSelect.querySelector('.yatra-participants-display');
1517 const selector = participantsSelect.querySelector('.yatra-booking-quantity-selector');
1518
1519 if (!display || !selector) return;
1520
1521 // Get all traveler category inputs dynamically
1522 const travelerInputs = selector.querySelectorAll('input[id^="traveler_"], input[id^="mobile_traveler_"]');
1523
1524 if (travelerInputs.length === 0) return;
1525
1526 // Make display clickable
1527 display.style.cursor = 'pointer';
1528
1529 // Toggle dropdown
1530 participantsSelect.onclick = (e) => {
1531 if (e.target.closest('.yatra-quantity-btn')) return;
1532 if (selector.contains(e.target)) return;
1533 e.preventDefault();
1534 e.stopPropagation();
1535 participantsSelect.classList.toggle('active');
1536 };
1537
1538 // Update display function - dynamically build text from all traveler inputs
1539 const updateDisplay = () => {
1540 const parts = [];
1541 travelerInputs.forEach((input) => {
1542 const value = parseInt(input.value || 0);
1543 if (value > 0) {
1544 const categoryLabel = input.getAttribute('data-category-label') || 'Traveler';
1545 parts.push(`${categoryLabel} x ${value}`);
1546 }
1547 });
1548
1549 if (parts.length === 0) {
1550 // Default to first category with value 1
1551 const firstInput = travelerInputs[0];
1552 if (firstInput) {
1553 const categoryLabel = firstInput.getAttribute('data-category-label') || 'Traveler';
1554 display.textContent = `${categoryLabel} x 1`;
1555 } else {
1556 display.textContent = '1 Traveler';
1557 }
1558 } else {
1559 display.textContent = parts.join(', ');
1560 }
1561 };
1562
1563 // Quantity button handlers
1564 selector.onclick = (e) => {
1565 const btn = e.target.closest('.yatra-quantity-btn');
1566 if (!btn) return;
1567
1568 e.preventDefault();
1569 e.stopPropagation();
1570
1571 const targetId = btn.getAttribute('data-target');
1572 let input = null;
1573 if (targetId) {
1574 try {
1575 input = selector.querySelector('#' + CSS.escape(targetId));
1576 } catch (_) {
1577 // ignore
1578 }
1579 if (!input) {
1580 input = document.getElementById(targetId);
1581 }
1582 }
1583 if (!input) return;
1584
1585 const current = parseInt(input.value || 0);
1586 const min = parseInt(input.getAttribute('min') || 0);
1587 const max = parseInt(input.getAttribute('max') || 999);
1588 const isPlus = btn.classList.contains('yatra-quantity-plus');
1589 const isMinus = btn.classList.contains('yatra-quantity-minus');
1590
1591 let newValue = current;
1592 if (isPlus && current < max) {
1593 newValue = current + 1;
1594 } else if (isMinus && current > min) {
1595 newValue = current - 1;
1596 }
1597
1598 if (newValue !== current) {
1599 input.value = newValue;
1600 updateDisplay();
1601
1602 // Update button states
1603 const row = input.closest('.yatra-quantity-row');
1604 if (row) {
1605 const minusBtn = row.querySelector('.yatra-quantity-minus');
1606 const plusBtn = row.querySelector('.yatra-quantity-plus');
1607 if (minusBtn) minusBtn.disabled = newValue <= min;
1608 if (plusBtn) plusBtn.disabled = newValue >= max;
1609 }
1610
1611 // Sync to availability cards
1612 syncToAvailabilityCards(travelerInputs);
1613 }
1614 };
1615
1616 // Function to sync sidebar travelers to availability cards
1617 const syncToAvailabilityCards = (inputs) => {
1618 const availabilitySection = document.getElementById('availability');
1619 if (!availabilitySection) return;
1620
1621 inputs.forEach((input) => {
1622 const categoryId = input.id.replace(/^mobile_/, '').replace('traveler_', '');
1623 const value = parseInt(input.value) || 0;
1624
1625 // Find all matching inputs in availability cards
1626 const availInputs = availabilitySection.querySelectorAll(`.yatra-availability-category[data-category="${categoryId}"]`);
1627 availInputs.forEach((availInput) => {
1628 availInput.value = value;
1629
1630 // Update button states
1631 const row = availInput.closest('.yatra-quantity-row');
1632 if (row) {
1633 const min = parseInt(availInput.getAttribute('min') || 0);
1634 const max = parseInt(availInput.getAttribute('max') || 999);
1635 const minusBtn = row.querySelector('.yatra-quantity-minus');
1636 const plusBtn = row.querySelector('.yatra-quantity-plus');
1637 if (minusBtn) minusBtn.disabled = value <= min;
1638 if (plusBtn) plusBtn.disabled = value >= max;
1639 }
1640
1641 // Update display for this card
1642 const participantsSelect = availInput.closest('.yatra-availability-participants');
1643 if (participantsSelect) {
1644 const display = participantsSelect.querySelector('.yatra-availability-participants-display');
1645 const categoryInputs = participantsSelect.querySelectorAll('.yatra-availability-category');
1646 if (display && categoryInputs.length > 0) {
1647 const parts = [];
1648 categoryInputs.forEach((catInput) => {
1649 const catValue = parseInt(catInput.value) || 0;
1650 if (catValue > 0) {
1651 const catRow = catInput.closest('.yatra-quantity-row');
1652 const categoryLabel = catRow ? catRow.querySelector('.yatra-quantity-title')?.textContent : 'Traveler';
1653 parts.push(`${categoryLabel} x ${catValue}`);
1654 }
1655 });
1656 if (parts.length > 0) {
1657 display.textContent = parts.join(', ');
1658 }
1659 }
1660 }
1661 });
1662 });
1663 };
1664
1665 // Close dropdown when clicking outside
1666 const closeHandler = (e) => {
1667 if (!participantsSelect.contains(e.target)) {
1668 participantsSelect.classList.remove('active');
1669 }
1670 };
1671 document.addEventListener('click', closeHandler);
1672
1673 // Initialize display
1674 updateDisplay();
1675
1676 // Initialize button states for all traveler inputs
1677 travelerInputs.forEach((input) => {
1678 const value = parseInt(input.value || 0);
1679 const min = parseInt(input.getAttribute('min') || 0);
1680 const max = parseInt(input.getAttribute('max') || 999);
1681 const row = input.closest('.yatra-quantity-row');
1682 if (row) {
1683 const minusBtn = row.querySelector('.yatra-quantity-minus');
1684 const plusBtn = row.querySelector('.yatra-quantity-plus');
1685 if (minusBtn) minusBtn.disabled = value <= min;
1686 if (plusBtn) plusBtn.disabled = value >= max;
1687 }
1688 });
1689 }
1690
1691 handleCheckAvailability() {
1692
1693
1694 // Get form values - support regular pricing (num_travelers), old structure (adults/children), and traveler-based pricing
1695 const dateInput = document.getElementById('travel_date');
1696 const numTravelersInput = document.getElementById('num_travelers');
1697 const adultsInput = document.getElementById('adults');
1698 const childrenInput = document.getElementById('children');
1699
1700 // Check for traveler-based pricing inputs (dynamic category inputs)
1701 const travelerInputs = document.querySelectorAll('input[id^="traveler_"]');
1702
1703 const date = dateInput ? dateInput.value : '';
1704
1705 // Check for travelers - prioritize traveler-based pricing, then num_travelers, then adults/children
1706 let totalTravelers = 0;
1707 if (travelerInputs.length > 0) {
1708 // Traveler-based pricing: sum all category inputs
1709 travelerInputs.forEach((input) => {
1710 totalTravelers += parseInt(input.value) || 0;
1711 });
1712 } else if (numTravelersInput) {
1713 // Regular pricing: single number input
1714 totalTravelers = parseInt(numTravelersInput.value) || 0;
1715 } else {
1716 // Old structure: adults + children
1717 const adults = adultsInput ? parseInt(adultsInput.value) || 0 : 0;
1718 const children = childrenInput ? parseInt(childrenInput.value) || 0 : 0;
1719 totalTravelers = adults + children;
1720 }
1721
1722
1723
1724 // Basic validation
1725 if (!date) {
1726 alert('Please select a travel date');
1727 return;
1728 }
1729
1730 if (totalTravelers === 0) {
1731 alert('Please select number of travelers');
1732 return;
1733 }
1734
1735 // Show loading state
1736 if (this.checkAvailabilityBtn && !this.checkAvailabilityBtn.dataset.originalHtml) {
1737 this.checkAvailabilityBtn.dataset.originalHtml = this.checkAvailabilityBtn.innerHTML;
1738 }
1739 if (this.checkAvailabilityBtn) {
1740 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...';
1741 this.checkAvailabilityBtn.disabled = true;
1742 }
1743
1744 // Get trip ID from data attribute or global variable
1745 const tripId = this.checkAvailabilityBtn?.dataset?.tripId ||
1746 document.querySelector('[data-trip-id]')?.dataset?.tripId ||
1747 (window.yatraTripData && window.yatraTripData.tripId) ||
1748 1; // Fallback to 1 for dummy data
1749
1750 // Check if availability section already exists
1751 let availabilitySection = document.getElementById('availability');
1752
1753 if (availabilitySection) {
1754 // Section already loaded, reload it with new date filter
1755 this.reloadAvailabilityWithDate(date, tripId);
1756 return;
1757 }
1758
1759 const monthKey = monthFilterKeyFromIsoDate(date);
1760 const restUrl = buildAvailabilityTemplateUrl(tripId, {
1761 date,
1762 month_filter: monthKey,
1763 sort: 'date-asc',
1764 page: 1,
1765 per_page: 10,
1766 });
1767
1768 fetch(restUrl, {
1769 method: 'GET',
1770 headers: {
1771 'Content-Type': 'application/json',
1772 },
1773 // Public endpoint: do not send cookies/nonces
1774 credentials: 'omit',
1775 })
1776 .then(response => {
1777
1778 if (!response.ok) {
1779 // Try to get error message from response
1780 return response.json().then(err => {
1781 console.error('API Error:', err);
1782 throw new Error(err.message || 'Failed to fetch availability');
1783 }).catch(() => {
1784 throw new Error('Failed to fetch availability (Status: ' + response.status + ')');
1785 });
1786 }
1787 return response.json();
1788 })
1789 .then(data => {
1790
1791 if (!data || !data.html) {
1792 throw new Error('No HTML returned from server');
1793 }
1794
1795 // Find the currently active section (first visible section or overview)
1796 const activeSection = this.findActiveSection();
1797
1798 // Create a temporary container to parse HTML
1799 const tempDiv = document.createElement('div');
1800 tempDiv.innerHTML = data.html;
1801 const newSection = tempDiv.firstElementChild;
1802
1803 if (!newSection) {
1804 throw new Error('Invalid HTML structure');
1805 }
1806
1807 // Insert before the active section
1808 if (activeSection && activeSection.parentNode) {
1809 activeSection.parentNode.insertBefore(newSection, activeSection);
1810 } else {
1811 // Fallback: insert at the end of main content
1812 const mainContent = document.querySelector('.yatra-trip-main');
1813 if (mainContent) {
1814 mainContent.appendChild(newSection);
1815 }
1816 }
1817
1818 // Add menu item to sticky nav at the same position
1819 // Pass the activeSection so we can find the corresponding menu item
1820 this.addAvailabilityMenuItem(activeSection);
1821
1822 // Initialize availability section handlers
1823 if (window.availabilitySection) {
1824 window.availabilitySection.setup();
1825 } else {
1826 // Initialize if not already done
1827 window.availabilitySection = new AvailabilitySection();
1828 window.availabilitySection.setup();
1829 }
1830
1831 // Re-setup sticky nav to include the new menu item
1832 if (window.stickyNav) {
1833 window.stickyNav.setup();
1834 }
1835
1836 // Scroll to the new section
1837 this.scrollToAvailability(newSection);
1838
1839 // Auto-select the card matching the selected date
1840 this.autoSelectDateCard(date);
1841
1842 // Sync sidebar travelers to cards
1843 this.syncSidebarTravelersToCards();
1844
1845 // Reset button state
1846 this.resetButton();
1847 })
1848 .catch(error => {
1849 console.error('Error loading availability:', error);
1850 alert('Failed to load availability. Please try again.');
1851 this.resetButton();
1852 });
1853 }
1854
1855 findActiveSection() {
1856 // Find the first visible section or default to overview
1857 const sections = document.querySelectorAll('.yatra-trip-section');
1858 for (let section of sections) {
1859 const rect = section.getBoundingClientRect();
1860 if (rect.top >= 0 && rect.top < window.innerHeight) {
1861 return section;
1862 }
1863 }
1864 // Default to overview section
1865 return document.getElementById('overview') || sections[0];
1866 }
1867
1868 addAvailabilityMenuItem(activeSection) {
1869 const stickyNav = document.querySelector('.yatra-sticky-nav-container');
1870 if (!stickyNav) return;
1871
1872 // Check if menu item already exists
1873 if (document.querySelector('.yatra-sticky-nav-item[href="#availability"]')) {
1874 return;
1875 }
1876
1877 // Find the corresponding menu item for the active section
1878 let insertBeforeItem = null;
1879 let activeSectionId = null;
1880
1881 if (activeSection) {
1882 activeSectionId = activeSection.id;
1883 if (activeSectionId) {
1884 // Find the menu item that links to this section
1885 insertBeforeItem = document.querySelector('.yatra-sticky-nav-item[href="#' + activeSectionId + '"]');
1886 }
1887 }
1888
1889 // If no active section or menu item found, default to inserting after "What's Included"
1890 if (!insertBeforeItem) {
1891 insertBeforeItem = document.querySelector('.yatra-sticky-nav-item[href="#included"]');
1892 if (insertBeforeItem) {
1893 insertBeforeItem = insertBeforeItem.nextSibling; // Insert after "What's Included"
1894 }
1895 }
1896
1897 // Create new menu item
1898 const newItem = document.createElement('a');
1899 newItem.href = '#availability';
1900 newItem.className = 'yatra-sticky-nav-item';
1901 newItem.innerHTML = `
1902 <svg class="yatra-icon-sm" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1903 <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"/>
1904 </svg>
1905 <span>Availability</span>
1906 `;
1907
1908 // Insert before the target item (or after "What's Included" if no target)
1909 if (insertBeforeItem && insertBeforeItem.parentNode) {
1910 insertBeforeItem.parentNode.insertBefore(newItem, insertBeforeItem);
1911 } else {
1912 // Fallback: append to the end
1913 stickyNav.appendChild(newItem);
1914 }
1915
1916 // Update sticky nav to handle new item
1917 if (window.stickyNav) {
1918 // Add availability to sections array if not already there
1919 // Insert it in the correct position based on where the section was inserted
1920 if (!window.stickyNav.sections.includes('availability')) {
1921 if (activeSectionId) {
1922 const activeIndex = window.stickyNav.sections.indexOf(activeSectionId);
1923 if (activeIndex >= 0) {
1924 // Insert availability before the active section
1925 window.stickyNav.sections.splice(activeIndex, 0, 'availability');
1926 } else {
1927 window.stickyNav.sections.push('availability');
1928 }
1929 } else {
1930 window.stickyNav.sections.push('availability');
1931 }
1932 }
1933 // Re-setup to attach event listeners to new menu item
1934 window.stickyNav.setup();
1935 }
1936 }
1937
1938 scrollToAvailability(section) {
1939 setTimeout(() => {
1940 const offset = 150;
1941 const elementPosition = section.getBoundingClientRect().top;
1942 const offsetPosition = elementPosition + window.pageYOffset - offset;
1943
1944 window.scrollTo({
1945 top: offsetPosition,
1946 behavior: 'smooth'
1947 });
1948
1949 // Update sticky nav active state after scroll completes
1950 setTimeout(() => {
1951 if (window.stickyNav) {
1952 window.stickyNav.updateActiveNav();
1953 }
1954 }, 300); // Wait for smooth scroll to complete
1955 }, 100);
1956 }
1957
1958 resetButton() {
1959 if (this.checkAvailabilityBtn) {
1960 const originalHtml = this.checkAvailabilityBtn.dataset.originalHtml;
1961 if (originalHtml) {
1962 this.checkAvailabilityBtn.innerHTML = originalHtml;
1963 }
1964 this.checkAvailabilityBtn.disabled = false;
1965 }
1966 }
1967
1968 reloadAvailabilityWithDate(date, tripId) {
1969 // Show loading state
1970 if (this.checkAvailabilityBtn && !this.checkAvailabilityBtn.dataset.originalHtml) {
1971 this.checkAvailabilityBtn.dataset.originalHtml = this.checkAvailabilityBtn.innerHTML;
1972 }
1973 if (this.checkAvailabilityBtn) {
1974 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...';
1975 this.checkAvailabilityBtn.disabled = true;
1976 }
1977
1978 const availSection = document.getElementById('availability');
1979 const listEl = availSection ? availSection.querySelector('.yatra-availability-list') : null;
1980 const sortKey = listEl?.dataset?.sort || 'date-asc';
1981 const monthKey = monthFilterKeyFromIsoDate(date);
1982
1983 const restUrl = buildAvailabilityTemplateUrl(tripId, {
1984 date,
1985 month_filter: monthKey,
1986 sort: sortKey,
1987 page: 1,
1988 per_page: 10,
1989 });
1990
1991 fetch(restUrl, {
1992 method: 'GET',
1993 headers: {
1994 'Content-Type': 'application/json',
1995 },
1996 credentials: 'omit',
1997 })
1998 .then(response => {
1999 if (!response.ok) {
2000 throw new Error('Failed to fetch availability');
2001 }
2002 return response.json();
2003 })
2004 .then(data => {
2005 if (!data || !data.html) {
2006 throw new Error('No HTML returned from server');
2007 }
2008
2009 // Replace existing availability section
2010 const existingSection = document.getElementById('availability');
2011 if (existingSection) {
2012 const tempDiv = document.createElement('div');
2013 tempDiv.innerHTML = data.html;
2014 const newSection = tempDiv.firstElementChild;
2015
2016 if (newSection) {
2017 existingSection.parentNode.replaceChild(newSection, existingSection);
2018
2019 // Re-initialize availability section handlers
2020 if (window.availabilitySection) {
2021 window.availabilitySection.setup();
2022 }
2023
2024 // Scroll to section
2025 this.scrollToAvailability(newSection);
2026
2027 // Sync sidebar travelers to cards
2028 this.syncSidebarTravelersToCards();
2029 }
2030 }
2031
2032 // Reset button state
2033 this.resetButton();
2034 })
2035 .catch(error => {
2036 console.error('Error reloading availability:', error);
2037 alert('Failed to reload availability. Please try again.');
2038 this.resetButton();
2039 });
2040 }
2041
2042 autoSelectDateCard(selectedDate) {
2043 if (!selectedDate) return;
2044
2045 // Wait a bit for the availability section to render
2046 setTimeout(() => {
2047 const availabilityCards = document.querySelectorAll('.yatra-availability-card');
2048 if (!availabilityCards.length) return;
2049
2050 let matchingCard = null;
2051 let nearestCard = null;
2052 let minDiff = Infinity;
2053
2054 const selectedTime = new Date(selectedDate).getTime();
2055
2056 // Find exact match or nearest date
2057 availabilityCards.forEach(card => {
2058 const cardDate = card.dataset.date;
2059 if (!cardDate) return;
2060
2061 if (cardDate === selectedDate) {
2062 matchingCard = card;
2063 } else {
2064 const cardTime = new Date(cardDate).getTime();
2065 const diff = Math.abs(cardTime - selectedTime);
2066 if (diff < minDiff) {
2067 minDiff = diff;
2068 nearestCard = card;
2069 }
2070 }
2071 });
2072
2073 // Use exact match if found, otherwise use nearest
2074 const cardToOpen = matchingCard || nearestCard;
2075
2076 if (cardToOpen) {
2077 // Open the card
2078 const toggle = cardToOpen.querySelector('.yatra-availability-toggle');
2079 if (toggle && !cardToOpen.classList.contains('open')) {
2080 toggle.click();
2081 }
2082
2083 // Scroll to the card
2084 setTimeout(() => {
2085 const offset = 150;
2086 const elementPosition = cardToOpen.getBoundingClientRect().top;
2087 const offsetPosition = elementPosition + window.pageYOffset - offset;
2088
2089 window.scrollTo({
2090 top: offsetPosition,
2091 behavior: 'smooth'
2092 });
2093 }, 300);
2094 }
2095 }, 500);
2096 }
2097
2098 updateSidebarForDate(dateStr) {
2099 if (!dateStr) return;
2100
2101 // Get trip ID
2102 const tripId = this.checkAvailabilityBtn?.dataset?.tripId ||
2103 document.querySelector('[data-trip-id]')?.dataset?.tripId ||
2104 (window.yatraTripData && window.yatraTripData.tripId);
2105
2106 if (!tripId) return;
2107
2108 // Build REST URL
2109 const { base: baseUrl, isPlain } = getRestBase();
2110
2111 let restUrl;
2112 if (isPlain && !baseUrl.includes('/yatra/v1')) {
2113 restUrl = `${baseUrl}/?rest_route=/yatra/v1/trips/${tripId}/date-pricing&date=${dateStr}`;
2114 } else if (baseUrl.includes('/yatra/v1')) {
2115 restUrl = baseUrl + '/trips/' + tripId + '/date-pricing?date=' + dateStr;
2116 } else {
2117 restUrl = baseUrl + '/yatra/v1/trips/' + tripId + '/date-pricing?date=' + dateStr;
2118 }
2119
2120 const nonce = (window.yatraTripData && window.yatraTripData.nonce) || '';
2121
2122 // Fetch date-specific data
2123 fetch(restUrl, {
2124 method: 'GET',
2125 headers: {
2126 'Content-Type': 'application/json',
2127 'X-WP-Nonce': nonce
2128 },
2129 credentials: 'same-origin'
2130 })
2131 .then(response => response.json())
2132 .then(data => {
2133 if (data && data.success) {
2134 // Update departures available text
2135 const departuresText = document.querySelector('.yatra-departures-available-text');
2136 if (departuresText && data.departures_count !== undefined) {
2137 departuresText.textContent = data.departures_count + ' departure' + (data.departures_count !== 1 ? 's' : '') + ' available';
2138 }
2139
2140 // Update traveler dropdown HTML
2141 const quantitySelector = document.getElementById('quantity-selector');
2142 if (quantitySelector && data.travelers_html) {
2143 quantitySelector.innerHTML = data.travelers_html;
2144 // Re-initialize quantity controls
2145 this.initQuantityControls();
2146 }
2147
2148 // Update total price
2149 this.updateTotal();
2150 }
2151 })
2152 .catch(error => {
2153 console.error('Error fetching date pricing:', error);
2154 });
2155 }
2156
2157 /**
2158 * Sync sidebar travelers to all availability cards
2159 */
2160 syncSidebarTravelersToCards() {
2161 // Get sidebar traveler values
2162 const travelerInputs = document.querySelectorAll('#quantity-selector input[id^="traveler_"]');
2163 const numTravelersInput = document.getElementById('num_travelers');
2164
2165 if (travelerInputs.length === 0 && !numTravelersInput) {
2166 return; // No traveler inputs found
2167 }
2168
2169 // Get all availability cards
2170 const availabilityCards = document.querySelectorAll('.yatra-availability-card');
2171 if (availabilityCards.length === 0) {
2172 return; // No availability cards found
2173 }
2174
2175 availabilityCards.forEach((card) => {
2176 const itemId = card.getAttribute('data-availability-id') || card.getAttribute('data-item');
2177 if (!itemId) return;
2178
2179 // Check if this card uses traveler-based pricing or simple pricing
2180 const travelerSelector = card.querySelector('.yatra-availability-quantity-selector[data-item="' + itemId + '"]');
2181 const simpleTravelers = card.querySelector('.yatra-availability-num-travelers[data-item="' + itemId + '"]');
2182
2183 if (travelerSelector && travelerInputs.length > 0) {
2184 // Traveler-based pricing - sync by category
2185 travelerInputs.forEach((input) => {
2186 const categoryId = input.id.replace('traveler_', '');
2187 const value = parseInt(input.value) || 0;
2188
2189 const row = travelerSelector.querySelector('.yatra-quantity-row[data-category-id="' + categoryId + '"]');
2190 if (row) {
2191 const cardInput = row.querySelector('input[type="number"]');
2192 if (cardInput) {
2193 const max = parseInt(cardInput.getAttribute('max')) || 99;
2194 cardInput.value = Math.min(value, max);
2195
2196 // Update button states
2197 const minusBtn = row.querySelector('.yatra-quantity-minus');
2198 const plusBtn = row.querySelector('.yatra-quantity-plus');
2199 const min = parseInt(cardInput.getAttribute('min')) || 0;
2200 if (minusBtn) minusBtn.disabled = cardInput.value <= min;
2201 if (plusBtn) plusBtn.disabled = cardInput.value >= max;
2202 }
2203 }
2204 });
2205
2206 // Update the display text for this card
2207 this.updateAvailabilityCardDisplay(itemId);
2208
2209 } else if (simpleTravelers && numTravelersInput) {
2210 // Simple pricing - sync total count
2211 const totalCount = parseInt(numTravelersInput.value) || 1;
2212 const max = parseInt(simpleTravelers.getAttribute('max')) || 99;
2213 simpleTravelers.value = Math.min(totalCount, max);
2214
2215 // Update button states
2216 const controls = simpleTravelers.closest('.yatra-quantity-controls-inline');
2217 if (controls) {
2218 const minusBtn = controls.querySelector('.yatra-quantity-minus');
2219 const plusBtn = controls.querySelector('.yatra-quantity-plus');
2220 const min = parseInt(simpleTravelers.getAttribute('min')) || 1;
2221 if (minusBtn) minusBtn.disabled = simpleTravelers.value <= min;
2222 if (plusBtn) plusBtn.disabled = simpleTravelers.value >= max;
2223 }
2224 }
2225
2226 // Update card totals
2227 this.updateAvailabilityCardTotals(itemId);
2228 });
2229 }
2230
2231 /**
2232 * Update availability card participants display
2233 */
2234 updateAvailabilityCardDisplay(itemId) {
2235 const display = document.querySelector('.yatra-availability-participants-display[data-item="' + itemId + '"]');
2236 if (!display) return;
2237
2238 const selector = document.querySelector('.yatra-availability-quantity-selector[data-item="' + itemId + '"]');
2239 if (!selector) return;
2240
2241 const rows = selector.querySelectorAll('.yatra-quantity-row');
2242 const parts = [];
2243
2244 rows.forEach((row) => {
2245 const input = row.querySelector('input[type="number"]');
2246 const labelEl = row.querySelector('.yatra-quantity-title');
2247 if (input && labelEl) {
2248 const count = parseInt(input.value) || 0;
2249 if (count > 0) {
2250 parts.push(labelEl.textContent.trim() + ' x ' + count);
2251 }
2252 }
2253 });
2254
2255 display.textContent = parts.length > 0 ? parts.join(', ') : '0 Travelers';
2256 }
2257
2258 /**
2259 * Update availability card total price
2260 */
2261 updateAvailabilityCardTotals(itemId) {
2262 const totalAmountEl = document.querySelector('.yatra-card-total-amount[data-item="' + itemId + '"]');
2263 const totalNoteEl = document.querySelector('.yatra-card-total-note[data-item="' + itemId + '"]');
2264 if (!totalAmountEl) return;
2265
2266 let total = 0;
2267 let totalTravelers = 0;
2268
2269 // Check for traveler-based pricing first
2270 const selector = document.querySelector('.yatra-availability-quantity-selector[data-item="' + itemId + '"]');
2271
2272 if (selector) {
2273 // Traveler-based pricing
2274 const rows = selector.querySelectorAll('.yatra-quantity-row');
2275 rows.forEach((row) => {
2276 const input = row.querySelector('input[type="number"]');
2277 const price = parseFloat(row.getAttribute('data-price')) || 0;
2278 const count = parseInt(input?.value) || 0;
2279 const pricingMode = row.getAttribute('data-pricing-mode') || (input ? input.getAttribute('data-pricing-mode') : '') || 'per_person';
2280
2281 if (pricingMode === 'per_group') {
2282 // Per group: charge flat price once if any travelers
2283 if (count > 0) {
2284 total += price;
2285 }
2286 } else {
2287 // Per person: charge per traveler
2288 total += price * count;
2289 }
2290 totalTravelers += count;
2291 });
2292 } else {
2293 // Simple pricing
2294 const numTravelersInput = document.querySelector('.yatra-availability-num-travelers[data-item="' + itemId + '"]');
2295 if (numTravelersInput) {
2296 const price = parseFloat(numTravelersInput.getAttribute('data-price')) || parseFloat(totalAmountEl.getAttribute('data-base-price')) || 0;
2297 const count = parseInt(numTravelersInput.value) || 1;
2298 total = price * count;
2299 totalTravelers = count;
2300 } else {
2301 total = parseFloat(totalAmountEl.getAttribute('data-base-price')) || 0;
2302 totalTravelers = 1;
2303 }
2304 }
2305
2306 // Format price (use existing format function if available)
2307 if (typeof yatra_format_price_js === 'function') {
2308 totalAmountEl.textContent = yatra_format_price_js(total);
2309 } else {
2310 // Fallback formatting
2311 totalAmountEl.textContent = String(total.toFixed(2));
2312 }
2313
2314 if (totalNoteEl) {
2315 totalNoteEl.textContent = 'for ' + totalTravelers + ' traveler' + (totalTravelers !== 1 ? 's' : '');
2316 }
2317
2318 // Update the card participants display
2319 this.updateAvailabilityCardDisplay(itemId);
2320 }
2321
2322
2323 handleMakeEnquiry() {
2324 // This will be handled by EnquiryModal class
2325 // Just trigger the modal open
2326 if (window.enquiryModal) {
2327 window.enquiryModal.open();
2328 }
2329 }
2330
2331 setTravelers(adults, children) {
2332 const adultsInput = document.getElementById('adults');
2333 const childrenInput = document.getElementById('children');
2334
2335 if (adultsInput) {
2336 adultsInput.value = adults;
2337 // Trigger change event to update display
2338 const event = new Event('change', { bubbles: true });
2339 adultsInput.dispatchEvent(event);
2340 }
2341
2342 if (childrenInput) {
2343 childrenInput.value = children;
2344 // Trigger change event to update display
2345 const event = new Event('change', { bubbles: true });
2346 childrenInput.dispatchEvent(event);
2347 }
2348 }
2349 }
2350
2351 /**
2352 * Enquiry Modal Class
2353 * Handles enquiry form modal functionality
2354 */
2355 class EnquiryModal {
2356 constructor() {
2357 this.modal = null;
2358 this.form = null;
2359 this.dateInput = null;
2360 this.participantsSelect = null;
2361 this.datepickerInstance = null;
2362 this.init();
2363 }
2364
2365 init() {
2366 if (document.readyState === 'loading') {
2367 document.addEventListener('DOMContentLoaded', () => this.setup());
2368 } else {
2369 this.setup();
2370 }
2371 }
2372
2373 setup() {
2374 this.modal = document.getElementById('enquiry-modal');
2375 if (!this.modal) return;
2376
2377 this.form = document.getElementById('enquiry-form');
2378 this.dateInput = document.getElementById('enquiry-travel-date');
2379 this.participantsSelect = document.querySelector('.yatra-enquiry-participants');
2380
2381 this.attachEventListeners();
2382 }
2383
2384 attachEventListeners() {
2385 // Close button
2386 const closeBtn = document.getElementById('close-enquiry-modal');
2387 const modalClose = document.querySelector('.yatra-enquiry-modal-close');
2388 const overlay = document.querySelector('.yatra-enquiry-modal-overlay');
2389
2390 if (closeBtn) {
2391 closeBtn.addEventListener('click', () => this.close());
2392 }
2393 if (modalClose) {
2394 modalClose.addEventListener('click', () => this.close());
2395 }
2396 if (overlay) {
2397 overlay.addEventListener('click', () => this.close());
2398 }
2399
2400 // ESC key
2401 document.addEventListener('keydown', (e) => {
2402 if (e.key === 'Escape' && this.modal.classList.contains('active')) {
2403 this.close();
2404 }
2405 });
2406
2407 // Form submission
2408 if (this.form) {
2409 this.form.addEventListener('submit', (e) => {
2410 e.preventDefault();
2411 this.handleSubmit();
2412 });
2413 }
2414 }
2415
2416 open() {
2417 if (!this.modal) return;
2418 this.modal.classList.add('active');
2419 document.body.style.overflow = 'hidden';
2420
2421 // Initialize form fields after modal is visible
2422 setTimeout(() => {
2423 this.initDateField();
2424 this.initTravelersField();
2425 }, 300);
2426 }
2427
2428 close() {
2429 if (!this.modal) return;
2430 this.modal.classList.remove('active');
2431 document.body.style.overflow = '';
2432 }
2433
2434 initDateField() {
2435 if (!this.dateInput || typeof flatpickr === 'undefined') return;
2436
2437 // Sync date from main form
2438 const mainDateInput = document.getElementById('travel_date');
2439 if (mainDateInput && mainDateInput.value) {
2440 this.dateInput.value = mainDateInput.value;
2441 }
2442
2443 // Destroy existing instance
2444 if (this.dateInput._flatpickr) {
2445 this.dateInput._flatpickr.destroy();
2446 }
2447
2448 const today = new Date();
2449 today.setHours(0, 0, 0, 0);
2450
2451 this.datepickerInstance = flatpickr(this.dateInput, {
2452 dateFormat: 'Y-m-d',
2453 altInput: true,
2454 altFormat: getTripAltDateFormat(),
2455 minDate: today,
2456 enableTime: false,
2457 clickOpens: true,
2458 allowInput: false,
2459 static: false,
2460 monthSelectorType: 'static',
2461 animate: true,
2462 locale: {
2463 firstDayOfWeek: 1
2464 },
2465 onChange: (selectedDates, dateStr) => {
2466 this.dateInput.value = dateStr;
2467 }
2468 });
2469
2470 // Make container clickable
2471 const container = this.dateInput.closest('.yatra-booking-field-select');
2472 if (container) {
2473 container.style.cursor = 'pointer';
2474 const openPicker = (e) => {
2475 if (e && e.target && e.target.closest && e.target.closest('button, a, select, textarea')) {
2476 return;
2477 }
2478 if (e) {
2479 e.preventDefault();
2480 e.stopPropagation();
2481 }
2482 if (this.datepickerInstance && !this.datepickerInstance.isOpen) {
2483 this.datepickerInstance.open();
2484 }
2485 };
2486
2487 container.addEventListener('click', openPicker);
2488 if (this.dateInput && this.dateInput._flatpickr && this.dateInput._flatpickr.altInput) {
2489 this.dateInput._flatpickr.altInput.addEventListener('click', openPicker);
2490 }
2491 }
2492 }
2493
2494 initTravelersField() {
2495 if (!this.participantsSelect) return;
2496
2497 const display = document.getElementById('enquiry-participants-display');
2498 const selector = document.getElementById('enquiry-quantity-selector');
2499 const adultsInput = document.getElementById('enquiry-adults');
2500 const childrenInput = document.getElementById('enquiry-children');
2501
2502 if (!display || !selector || !adultsInput || !childrenInput) return;
2503
2504 // Make display clickable
2505 display.style.cursor = 'pointer';
2506
2507 // Toggle dropdown
2508 this.participantsSelect.onclick = (e) => {
2509 if (e.target.closest('.yatra-quantity-btn')) return;
2510 if (selector.contains(e.target)) return;
2511 e.preventDefault();
2512 e.stopPropagation();
2513 this.participantsSelect.classList.toggle('active');
2514 };
2515
2516 // Update display function
2517 const updateDisplay = () => {
2518 const adults = parseInt(adultsInput.value || 1);
2519 const children = parseInt(childrenInput.value || 0);
2520 let text = '';
2521 if (adults > 0 && children > 0) {
2522 text = `Adult x ${adults}, Child x ${children}`;
2523 } else if (adults > 0) {
2524 text = `Adult x ${adults}`;
2525 } else if (children > 0) {
2526 text = `Child x ${children}`;
2527 } else {
2528 text = 'Adult x 1';
2529 }
2530 display.textContent = text;
2531 };
2532
2533 // Quantity button handlers
2534 selector.onclick = (e) => {
2535 const btn = e.target.closest('.yatra-quantity-btn');
2536 if (!btn) return;
2537
2538 e.preventDefault();
2539 e.stopPropagation();
2540
2541 const targetId = btn.getAttribute('data-target');
2542 const input = document.getElementById(targetId);
2543 if (!input) return;
2544
2545 const current = parseInt(input.value || 0);
2546 const min = parseInt(input.getAttribute('min') || 0);
2547 const max = parseInt(input.getAttribute('max') || 999);
2548 const isPlus = btn.classList.contains('yatra-quantity-plus');
2549 const isMinus = btn.classList.contains('yatra-quantity-minus');
2550
2551 let newValue = current;
2552 if (isPlus && current < max) {
2553 newValue = current + 1;
2554 } else if (isMinus && current > min) {
2555 newValue = current - 1;
2556 }
2557
2558 if (newValue !== current) {
2559 input.value = newValue;
2560 updateDisplay();
2561
2562 // Update button states
2563 const row = input.closest('.yatra-quantity-row');
2564 if (row) {
2565 const minusBtn = row.querySelector('.yatra-quantity-minus');
2566 const plusBtn = row.querySelector('.yatra-quantity-plus');
2567 if (minusBtn) minusBtn.disabled = newValue <= min;
2568 if (plusBtn) plusBtn.disabled = newValue >= max;
2569 }
2570 }
2571 };
2572
2573 // Close dropdown when clicking outside
2574 document.addEventListener('click', (e) => {
2575 if (!this.participantsSelect.contains(e.target)) {
2576 this.participantsSelect.classList.remove('active');
2577 }
2578 });
2579
2580 // Initialize
2581 updateDisplay();
2582 [adultsInput, childrenInput].forEach((input) => {
2583 const value = parseInt(input.value || 0);
2584 const min = parseInt(input.getAttribute('min') || 0);
2585 const max = parseInt(input.getAttribute('max') || 999);
2586 const row = input.closest('.yatra-quantity-row');
2587 if (row) {
2588 const minusBtn = row.querySelector('.yatra-quantity-minus');
2589 const plusBtn = row.querySelector('.yatra-quantity-plus');
2590 if (minusBtn) minusBtn.disabled = value <= min;
2591 if (plusBtn) plusBtn.disabled = value >= max;
2592 }
2593 });
2594 }
2595
2596 handleSubmit() {
2597 if (!this.form) return;
2598
2599 const formData = new FormData(this.form);
2600 const messageBox = document.getElementById('enquiry-message-box');
2601
2602 // Get values from hidden inputs for adults/children
2603 const adults = document.getElementById('enquiry-adults')?.value || 1;
2604 const children = document.getElementById('enquiry-children')?.value || 0;
2605
2606 // Add adults and children to formData
2607 formData.set('adults', adults);
2608 formData.set('children', children);
2609
2610 // Validation
2611 const name = formData.get('name');
2612 const email = formData.get('email');
2613 const message = formData.get('message');
2614
2615 if (!name || !email || !message) {
2616 this.showMessage('error', 'Please fill in all required fields.');
2617 return;
2618 }
2619
2620 // Basic email validation
2621 const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
2622 if (!emailRegex.test(email)) {
2623 this.showMessage('error', 'Please enter a valid email address.');
2624 return;
2625 }
2626
2627 if (message.length < 10) {
2628 this.showMessage('error', 'Please enter a message (at least 10 characters).');
2629 return;
2630 }
2631
2632 // Show loading
2633 const submitBtn = this.form.querySelector('.yatra-enquiry-submit');
2634 const originalText = submitBtn ? submitBtn.textContent : 'Send Enquiry';
2635 if (submitBtn) {
2636 submitBtn.textContent = 'Sending...';
2637 submitBtn.disabled = true;
2638 }
2639
2640 // Hide any existing messages
2641 if (messageBox) {
2642 messageBox.style.display = 'none';
2643 }
2644
2645 // Submit via REST API (public endpoint)
2646 const payload = Object.fromEntries(formData.entries());
2647
2648 // Build REST URL
2649 const baseUrlRaw = (window.yatraTripData?.apiUrl || window.yatraTripData?.restUrl || window.wpApiSettings?.root || '/wp-json').replace(/\/$/, '');
2650 const enquiriesUrl = baseUrlRaw.includes('/yatra/v1')
2651 ? `${baseUrlRaw}/enquiries`
2652 : `${baseUrlRaw}/yatra/v1/enquiries`;
2653
2654 fetch(enquiriesUrl, {
2655 method: 'POST',
2656 headers: {
2657 'Content-Type': 'application/json',
2658 },
2659 credentials: 'omit', // public endpoint
2660 body: JSON.stringify(payload),
2661 })
2662 .then(response => response.json())
2663 .then(result => {
2664 if (result.success) {
2665 this.showMessage('success', result.message || result.data?.message || 'Thank you for your enquiry! We will get back to you soon.');
2666 this.form.reset();
2667
2668 // Reset the travelers display
2669 const display = document.getElementById('enquiry-participants-display');
2670 if (display) {
2671 display.textContent = 'Adult x 1';
2672 }
2673 const adultsInput = document.getElementById('enquiry-adults');
2674 const childrenInput = document.getElementById('enquiry-children');
2675 if (adultsInput) adultsInput.value = 1;
2676 if (childrenInput) childrenInput.value = 0;
2677
2678 // Close modal after delay
2679 setTimeout(() => {
2680 this.close();
2681 if (messageBox) {
2682 messageBox.style.display = 'none';
2683 }
2684 }, 3000);
2685 } else {
2686 this.showMessage('error', result.message || result.data?.message || 'Failed to submit enquiry. Please try again.');
2687 }
2688 })
2689 .catch(error => {
2690 console.error('Enquiry submission error:', error);
2691 this.showMessage('error', 'An error occurred. Please try again later.');
2692 })
2693 .finally(() => {
2694 if (submitBtn) {
2695 submitBtn.textContent = originalText;
2696 submitBtn.disabled = false;
2697 }
2698 });
2699 }
2700
2701 showMessage(type, text) {
2702 const messageBox = document.getElementById('enquiry-message-box');
2703 if (!messageBox) return;
2704
2705 messageBox.style.display = 'block';
2706 messageBox.className = `yatra-enquiry-message yatra-enquiry-message-${type}`;
2707 messageBox.innerHTML = `<p>${text}</p>`;
2708
2709 // Scroll message into view
2710 messageBox.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
2711 }
2712 }
2713
2714 /**
2715 * Sticky Navigation Class
2716 * Handles sticky navigation bar functionality
2717 */
2718 class StickyNav {
2719 constructor() {
2720 this.nav = null;
2721 this.sidebar = null;
2722 this.navItems = [];
2723 this.sections = [];
2724 this.scrollThreshold = 200;
2725 this.lastScrollTop = 0;
2726 this.navHeight = 0;
2727 this.init();
2728 }
2729
2730 init() {
2731 if (document.readyState === 'loading') {
2732 document.addEventListener('DOMContentLoaded', () => this.setup());
2733 } else {
2734 this.setup();
2735 }
2736 }
2737
2738 setup() {
2739 this.nav = document.querySelector('.yatra-sticky-nav');
2740 if (!this.nav) return;
2741
2742 this.sidebar = document.querySelector('.yatra-trip-sidebar');
2743 this.navItems = this.nav.querySelectorAll('.yatra-sticky-nav-item');
2744
2745 // Update sections to match actual page hierarchy
2746 this.sections = this.getActualSections();
2747
2748 // Calculate nav height
2749 this.navHeight = this.nav.offsetHeight || 60;
2750
2751 this.attachEventListeners();
2752 this.handleScroll(); // Initial check
2753 this.updateActiveNav(); // Initial check
2754 this.updateSidebarPosition(); // Initial check
2755 }
2756
2757 getActualSections() {
2758 // Base sections that are always present in order
2759 const baseSections = ['overview', 'itinerary', 'included', 'location', 'important-info'];
2760
2761 // Conditional sections that may or may not be present
2762 const conditionalSections = ['downloads', 'faq', 'trip-story', 'what-makes-special', 'testimonials'];
2763
2764 // Combine all sections and filter to only include those that actually exist on the page
2765 const allSections = [...baseSections, ...conditionalSections];
2766
2767 return allSections.filter(sectionId => {
2768 const element = document.getElementById(sectionId);
2769 return element && element.offsetParent !== null; // Check if element exists and is visible
2770 });
2771 }
2772
2773 attachEventListeners() {
2774 // Handle clicks on nav items
2775 this.navItems.forEach((item) => {
2776 item.addEventListener('click', (e) => {
2777 e.preventDefault();
2778 const href = item.getAttribute('href');
2779 if (href && href.startsWith('#')) {
2780 const sectionId = href.substring(1);
2781 this.scrollToSection(sectionId);
2782
2783 // Update active state
2784 this.navItems.forEach((nav) => nav.classList.remove('active'));
2785 item.classList.add('active');
2786 }
2787 });
2788 });
2789
2790 // Handle scroll events
2791 window.addEventListener('scroll', () => this.onScroll(), { passive: true });
2792
2793 // Update sidebar position when nav visibility changes
2794 const observer = new MutationObserver(() => {
2795 this.updateSidebarPosition();
2796 });
2797 observer.observe(this.nav, { attributes: true, attributeFilter: ['class'] });
2798 }
2799
2800 updateSidebarPosition() {
2801 if (!this.sidebar) return;
2802
2803 const isNavVisible = this.nav.classList.contains('visible');
2804 const isAdminBar = document.body.classList.contains('admin-bar');
2805
2806 // Calculate base top position
2807 let topPosition = 0;
2808
2809 if (isAdminBar) {
2810 // Check if mobile admin bar (782px breakpoint)
2811 if (window.innerWidth <= 782) {
2812 topPosition = 46; // Mobile admin bar
2813 } else {
2814 topPosition = 32; // Desktop admin bar
2815 }
2816 }
2817
2818 // Add sticky nav height if visible
2819 if (isNavVisible) {
2820 topPosition += this.navHeight;
2821 }
2822
2823 // Apply to sidebar
2824 this.sidebar.style.top = topPosition + 'px';
2825 }
2826
2827 scrollToAvailability(section) {
2828 if (!section) return;
2829
2830 const offset = 100; // Account for sticky header
2831 const elementPosition = section.getBoundingClientRect().top;
2832 const offsetPosition = elementPosition + window.pageYOffset - offset;
2833
2834 window.scrollTo({
2835 top: offsetPosition,
2836 behavior: 'smooth'
2837 });
2838 }
2839
2840 scrollToSection(sectionId) {
2841 const section = document.getElementById(sectionId);
2842 if (!section) {
2843 console.warn(`Section with ID "${sectionId}" not found`);
2844 return;
2845 }
2846
2847 // Calculate offset - account for sticky nav and admin bar
2848 let offset = 100; // Base offset
2849 const isNavVisible = this.nav.classList.contains('visible');
2850 if (isNavVisible) {
2851 offset += this.navHeight;
2852 }
2853
2854 // Check for WordPress admin bar
2855 const adminBar = document.getElementById('wpadminbar');
2856 if (adminBar) {
2857 // Check if mobile admin bar (782px breakpoint)
2858 if (window.innerWidth <= 782) {
2859 offset += 46; // Mobile admin bar
2860 } else {
2861 offset += 32; // Desktop admin bar
2862 }
2863 }
2864
2865 const elementPosition = section.getBoundingClientRect().top;
2866 const offsetPosition = elementPosition + window.pageYOffset - offset;
2867
2868 window.scrollTo({
2869 top: offsetPosition,
2870 behavior: 'smooth'
2871 });
2872 }
2873
2874 autoSelectDateCard(selectedDate) {
2875 if (!selectedDate) return;
2876
2877 // Wait a bit for the availability section to render
2878 setTimeout(() => {
2879 const availabilityCards = document.querySelectorAll('.yatra-availability-card');
2880 if (!availabilityCards.length) return;
2881
2882 let matchingCard = null;
2883 let nearestCard = null;
2884 let minDiff = Infinity;
2885
2886 const selectedTime = new Date(selectedDate).getTime();
2887
2888 // Find exact match or nearest date
2889 availabilityCards.forEach(card => {
2890 const cardDate = card.dataset.date;
2891 if (!cardDate) return;
2892
2893 if (cardDate === selectedDate) {
2894 matchingCard = card;
2895 } else {
2896 const cardTime = new Date(cardDate).getTime();
2897 const diff = Math.abs(cardTime - selectedTime);
2898 if (diff < minDiff) {
2899 minDiff = diff;
2900 nearestCard = card;
2901 }
2902 }
2903 });
2904
2905 // Use exact match if found, otherwise use nearest
2906 const cardToOpen = matchingCard || nearestCard;
2907
2908 if (cardToOpen) {
2909 // Open the card
2910 const toggle = cardToOpen.querySelector('.yatra-availability-toggle');
2911 if (toggle && !cardToOpen.classList.contains('open')) {
2912 toggle.click();
2913 }
2914
2915 // Scroll to the card
2916 setTimeout(() => {
2917 const offset = 150;
2918 const elementPosition = cardToOpen.getBoundingClientRect().top;
2919 const offsetPosition = elementPosition + window.pageYOffset - offset;
2920
2921 window.scrollTo({
2922 top: offsetPosition,
2923 behavior: 'smooth'
2924 });
2925 }, 300);
2926 }
2927 }, 500);
2928 }
2929
2930 handleScroll() {
2931 const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
2932
2933 // Show nav when scrolling down past threshold
2934 if (scrollTop > this.scrollThreshold) {
2935 this.nav.classList.add('visible');
2936 } else {
2937 this.nav.classList.remove('visible');
2938 }
2939
2940 this.lastScrollTop = scrollTop;
2941 }
2942
2943 updateActiveNav() {
2944 const scrollPos = window.scrollY + 150;
2945 let activeSectionId = null;
2946
2947 // Find which section is currently in view
2948 this.sections.forEach((sectionId) => {
2949 const section = document.getElementById(sectionId);
2950 if (section) {
2951 const sectionTop = section.offsetTop;
2952 const sectionBottom = sectionTop + section.offsetHeight;
2953
2954 if (scrollPos >= sectionTop && scrollPos < sectionBottom) {
2955 activeSectionId = sectionId;
2956 }
2957 }
2958 });
2959
2960 // Update active state based on section ID (not index)
2961 if (activeSectionId) {
2962 this.navItems.forEach((nav) => {
2963 nav.classList.remove('active');
2964 const href = nav.getAttribute('href');
2965 if (href === '#' + activeSectionId) {
2966 nav.classList.add('active');
2967 }
2968 });
2969 }
2970 }
2971
2972 onScroll() {
2973 this.handleScroll();
2974 this.updateActiveNav();
2975 this.updateSidebarPosition();
2976 }
2977 }
2978
2979 /**
2980 * Availability Section Class
2981 * Handles availability section interactions (filters, load more)
2982 */
2983 class AvailabilitySection {
2984 constructor() {
2985 this.section = null;
2986 this.filterButtons = [];
2987 this.availabilityItems = [];
2988 this.currentMonthFilter = 'all';
2989 this.currentSortKey = 'date-asc';
2990 this.init();
2991 }
2992
2993 init() {
2994 if (document.readyState === 'loading') {
2995 document.addEventListener('DOMContentLoaded', () => this.setup());
2996 } else {
2997 this.setup();
2998 }
2999 }
3000
3001 setup() {
3002 this.section = document.getElementById('availability');
3003 if (!this.section) return;
3004
3005 const filterBar = this.section.querySelector('.yatra-availability-filters');
3006 this.filterButtons = filterBar
3007 ? filterBar.querySelectorAll('.yatra-availability-filter-btn')
3008 : this.section.querySelectorAll('.yatra-availability-filter-btn');
3009 this.availabilityItems = this.section.querySelectorAll('.yatra-availability-card');
3010
3011 const sortSelect = this.section.querySelector('#availability-sort');
3012 if (sortSelect && sortSelect.value) {
3013 this.currentSortKey = sortSelect.value;
3014 }
3015
3016 this.attachEventListeners();
3017
3018 const listEl = this.section.querySelector('.yatra-availability-list');
3019 if (listEl && listEl.dataset.monthFilter) {
3020 this.currentMonthFilter = listEl.dataset.monthFilter;
3021 } else {
3022 const activeFilterBtn = filterBar
3023 ? filterBar.querySelector('.yatra-availability-filter-btn.active')
3024 : this.section.querySelector('.yatra-availability-filter-btn.active');
3025 if (activeFilterBtn) {
3026 this.currentMonthFilter = activeFilterBtn.getAttribute('data-filter') || 'all';
3027 }
3028 }
3029 if (listEl && listEl.dataset.sort) {
3030 this.currentSortKey = listEl.dataset.sort;
3031 }
3032
3033 // Scroll to selected card if exists
3034 this.scrollToSelectedCard();
3035 }
3036
3037 scrollToSelectedCard() {
3038 // Find the selected card
3039 const selectedCard = this.section.querySelector('.yatra-availability-card.selected');
3040 if (selectedCard) {
3041 // Small delay to ensure rendering is complete
3042 setTimeout(() => {
3043 selectedCard.scrollIntoView({
3044 behavior: 'smooth',
3045 block: 'center'
3046 });
3047 }, 300);
3048 }
3049 }
3050
3051 /**
3052 * Single source of truth for availability "Book Now" (Pro: yatraBeforeBooking / additional services popup).
3053 * Previously attachBookNowHandlers() used a legacy session POST and skipped the hook — inconsistent by date after AJAX refresh.
3054 */
3055 handleAvailabilityBookNowClick(btn) {
3056 const self = this;
3057
3058 const tripId = btn.getAttribute('data-trip-id') || window.yatraTripData?.tripId;
3059 const date = btn.getAttribute('data-date');
3060 const itemIndex = btn.getAttribute('data-item');
3061 const availabilityId = btn.getAttribute('data-availability-id') || itemIndex;
3062
3063 const card = btn.closest('.yatra-availability-card');
3064
3065 let departureTime = '';
3066 if (card) {
3067 const timeElement = card.querySelector('.yatra-card-header-date');
3068 if (timeElement) {
3069 const timeText = timeElement.textContent?.trim();
3070 if (timeText && /^\d{1,2}:\d{2}\s*(AM|PM)?$/i.test(timeText)) {
3071 departureTime = timeText;
3072 }
3073 }
3074 }
3075
3076 let travelers = 0;
3077 const travelerCounts = {};
3078
3079 const categoryInputs = card?.querySelectorAll('.yatra-availability-category[data-item="' + itemIndex + '"]');
3080 if (categoryInputs && categoryInputs.length > 0) {
3081 categoryInputs.forEach((input) => {
3082 const categoryId = input.getAttribute('data-category');
3083 const count = parseInt(input.value) || 0;
3084 if (categoryId && count > 0) {
3085 travelerCounts[categoryId] = count;
3086 travelers += count;
3087 }
3088 });
3089 } else {
3090 const simpleInput = card?.querySelector('.yatra-availability-num-travelers[data-item="' + itemIndex + '"]');
3091 if (simpleInput) {
3092 travelers = parseInt(simpleInput.value) || 1;
3093 } else {
3094 const adults = this.getTravelerCount(itemIndex, 'adults');
3095 const children = this.getTravelerCount(itemIndex, 'children');
3096 travelers = parseInt(adults) + parseInt(children);
3097 }
3098 }
3099
3100 if (travelers < 1) {
3101 this.showBookingError(btn, 'Please select at least 1 traveler to continue.');
3102 return;
3103 }
3104
3105 const originalText = btn.innerHTML;
3106 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...';
3107 btn.disabled = true;
3108
3109 if (!tripId || isNaN(parseInt(tripId)) || parseInt(tripId) <= 0) {
3110 console.error('Invalid trip ID:', tripId);
3111 btn.innerHTML = originalText;
3112 btn.disabled = false;
3113 this.showBookingError(btn, 'Unable to process booking. Invalid trip data.');
3114 return;
3115 }
3116
3117 const pricingType = btn.getAttribute('data-pricing-type') || 'regular';
3118 const isDayTrip = btn.getAttribute('data-is-day-trip') === '1';
3119 let priceTypesJson = btn.getAttribute('data-price-types') || '';
3120 let priceTypes = [];
3121 try {
3122 if (priceTypesJson) {
3123 priceTypes = JSON.parse(priceTypesJson);
3124 }
3125 } catch (e) {
3126 console.error('Error parsing price types:', e);
3127 }
3128
3129 const btnDepartureTime = btn.getAttribute('data-departure-time') || '';
3130 if (btnDepartureTime) {
3131 departureTime = btnDepartureTime;
3132 }
3133
3134 const sessionPayload = {
3135 trip_id: parseInt(tripId, 10),
3136 travelers: travelers,
3137 travel_date: date || '',
3138 departure_time: departureTime,
3139 availability_id: availabilityId,
3140 pricing_type: pricingType,
3141 price_types: priceTypes,
3142 is_day_trip: isDayTrip,
3143 };
3144
3145 if (Object.keys(travelerCounts).length > 0) {
3146 sessionPayload.traveler_counts = travelerCounts;
3147 }
3148
3149 if (typeof window.yatraBeforeBooking === 'function') {
3150 Promise.resolve(window.yatraBeforeBooking({
3151 btn: btn,
3152 originalText: originalText,
3153 sessionPayload: sessionPayload,
3154 proceedToBooking: () => self.proceedToBooking(btn, originalText, sessionPayload)
3155 })).then(handled => {
3156 if (!handled) {
3157 self.proceedToBooking(btn, originalText, sessionPayload);
3158 }
3159 }).catch(error => {
3160 console.error('Error in yatraBeforeBooking hook:', error);
3161 self.proceedToBooking(btn, originalText, sessionPayload);
3162 });
3163 return;
3164 }
3165
3166 this.proceedToBooking(btn, originalText, sessionPayload);
3167 }
3168
3169 attachEventListeners() {
3170 const filterBar = this.section.querySelector('.yatra-availability-filters');
3171
3172 // Month filter buttons (server-side filter; buttons live only in .yatra-availability-filters)
3173 this.filterButtons.forEach((btn) => {
3174 btn.addEventListener('click', () => {
3175 const filter = btn.getAttribute('data-filter') || 'all';
3176 this.currentMonthFilter = filter;
3177 this.filterButtons.forEach((b) => b.classList.remove('active'));
3178 btn.classList.add('active');
3179 this.reloadAvailabilityFromServer({
3180 month_filter: filter,
3181 page: 1,
3182 sort: this.currentSortKey,
3183 });
3184 });
3185 });
3186
3187 const showAllCta = this.section.querySelector('.yatra-availability-show-all-dates');
3188 if (showAllCta && filterBar) {
3189 showAllCta.addEventListener('click', (e) => {
3190 e.preventDefault();
3191 const allPill = filterBar.querySelector('.yatra-availability-filter-btn[data-filter="all"]');
3192 if (allPill) {
3193 allPill.click();
3194 }
3195 });
3196 }
3197
3198 // Sort dropdown (server-side sort)
3199 const sortSelect = document.getElementById('availability-sort');
3200 if (sortSelect) {
3201 sortSelect.addEventListener('change', (e) => {
3202 const value = e && e.target ? e.target.value : '';
3203 this.currentSortKey = value || 'date-asc';
3204 this.reloadAvailabilityFromServer({
3205 month_filter: this.currentMonthFilter,
3206 page: 1,
3207 sort: this.currentSortKey,
3208 selectEl: sortSelect,
3209 });
3210 });
3211 }
3212
3213 this.setupToggleHandlers();
3214
3215 // Traveler selector buttons
3216 this.initTravelerSelectors();
3217
3218 // Book Now buttons - set session and redirect to booking page
3219 const bookButtons = this.section.querySelectorAll('.yatra-card-book-btn');
3220 bookButtons.forEach((btn) => {
3221 btn.addEventListener('click', (e) => {
3222 e.preventDefault();
3223 this.handleAvailabilityBookNowClick(btn);
3224 });
3225 });
3226
3227 // Load more button
3228 const loadMoreBtn = this.section.querySelector('.yatra-availability-load-more-btn');
3229 if (loadMoreBtn) {
3230 loadMoreBtn.addEventListener('click', () => {
3231 this.handleLoadMore();
3232 });
3233 }
3234 }
3235
3236 /**
3237 * Replace availability section from API (sort / month filter / reset page).
3238 * @param {{ month_filter?: string, page?: number, sort?: string, per_page?: number, selectEl?: HTMLSelectElement }} opts
3239 */
3240 reloadAvailabilityFromServer(opts = {}) {
3241 const tripId = window.yatraTripData?.tripId || this.section?.getAttribute('data-trip-id') || 0;
3242 if (!tripId) {
3243 if (opts.sort) {
3244 this.sortItems(opts.sort);
3245 }
3246 return Promise.resolve();
3247 }
3248
3249 const listEl = this.section?.querySelector('.yatra-availability-list');
3250 const monthFilter = opts.month_filter != null
3251 ? opts.month_filter
3252 : (listEl?.dataset?.monthFilter || this.currentMonthFilter || 'all');
3253 const sortKey = opts.sort || listEl?.dataset?.sort || this.currentSortKey || 'date-asc';
3254 const page = opts.page != null ? opts.page : 1;
3255 const perPage = opts.per_page != null
3256 ? opts.per_page
3257 : (parseInt(listEl?.dataset?.perPage, 10) || 10);
3258 const selectEl = opts.selectEl;
3259 const travelDateInput = document.getElementById('travel_date');
3260 const travelDate = travelDateInput && travelDateInput.value ? travelDateInput.value : undefined;
3261
3262 const prevDisabled = !!(selectEl && selectEl.disabled);
3263 if (selectEl) {
3264 selectEl.disabled = true;
3265 }
3266
3267 const url = buildAvailabilityTemplateUrl(tripId, {
3268 date: travelDate,
3269 month_filter: monthFilter,
3270 sort: sortKey,
3271 page,
3272 per_page: perPage,
3273 });
3274
3275 return fetch(url, {
3276 method: 'GET',
3277 headers: {
3278 'Content-Type': 'application/json',
3279 },
3280 credentials: 'omit',
3281 })
3282 .then((resp) => resp.json())
3283 .then((data) => {
3284 const html = data?.html || data?.data?.html;
3285 if (!html) {
3286 throw new Error('No HTML returned');
3287 }
3288
3289 const tempDiv = document.createElement('div');
3290 tempDiv.innerHTML = html;
3291 const newSection = tempDiv.querySelector('#availability') || tempDiv.firstElementChild;
3292 if (!newSection) {
3293 throw new Error('Invalid HTML');
3294 }
3295
3296 const oldSection = document.getElementById('availability');
3297 if (oldSection) {
3298 oldSection.replaceWith(newSection);
3299 }
3300
3301 this.currentMonthFilter = monthFilter;
3302 this.currentSortKey = sortKey;
3303 this.setup();
3304 })
3305 .catch((err) => {
3306 console.error('[Yatra] Availability reload failed:', err);
3307 if (opts.sort) {
3308 this.sortItems(opts.sort);
3309 }
3310 })
3311 .finally(() => {
3312 if (selectEl) {
3313 selectEl.disabled = prevDisabled;
3314 }
3315 });
3316 }
3317
3318 /**
3319 * Proceed to booking - creates session and redirects
3320 */
3321 proceedToBooking(btn, originalText, sessionPayload) {
3322 // Track Book Now click as InitiateCheckout (Facebook Pixel)
3323 if (typeof fbq !== 'undefined' && window.yatraAdmin?.facebookPixel?.connectionStatus?.pixelConnected) {
3324 const tripData = this.getTripData();
3325 const totalPrice = this.calculateTotalPrice();
3326
3327 fbq('track', 'InitiateCheckout', {
3328 content_name: tripData.title || 'Trip',
3329 content_ids: [sessionPayload.trip_id],
3330 content_type: 'product',
3331 value: totalPrice,
3332 currency: window.yatraBookingData?.currency || 'USD',
3333 num_items: sessionPayload.travelers || 1
3334 });
3335
3336 console.log('[Facebook Pixel] InitiateCheckout tracked for trip:', sessionPayload.trip_id);
3337 }
3338
3339 // Track Book Now click as begin_checkout (Google Analytics 4)
3340 if (typeof gtag !== 'undefined' && window.yatraAdmin?.googleAnalytics?.measurementId) {
3341 const tripData = this.getTripData();
3342 const totalPrice = this.calculateTotalPrice();
3343
3344 gtag('event', 'begin_checkout', {
3345 currency: window.yatraBookingData?.currency || 'USD',
3346 value: totalPrice,
3347 items: [{
3348 item_id: sessionPayload.trip_id,
3349 item_name: tripData.title || 'Trip',
3350 category: 'Travel',
3351 price: totalPrice / (sessionPayload.travelers || 1),
3352 quantity: sessionPayload.travelers || 1
3353 }]
3354 });
3355
3356 console.log('[Google Analytics] begin_checkout tracked for trip:', sessionPayload.trip_id);
3357 }
3358
3359 // Set booking session via REST API
3360 // credentials: 'same-origin' is required to send cookies for session
3361 const { base: baseUrl, isPlain } = getRestBase();
3362 let sessionUrl;
3363 if (isPlain && !baseUrl.includes('/yatra/v1')) {
3364 sessionUrl = `${baseUrl}/?rest_route=/yatra/v1/booking/session`;
3365 } else if (baseUrl.includes('/yatra/v1')) {
3366 sessionUrl = `${baseUrl}/booking/session`;
3367 } else {
3368 sessionUrl = `${baseUrl}/yatra/v1/booking/session`;
3369 }
3370
3371 fetch(sessionUrl, {
3372 method: 'POST',
3373 credentials: 'same-origin', // needed for PHP session cookie
3374 headers: {
3375 'Content-Type': 'application/json',
3376 },
3377 body: JSON.stringify(sessionPayload)
3378 })
3379 .then(response => response.json())
3380 .then(data => {
3381 if (data.success && data.redirect_url) {
3382 // Success - redirect to booking page
3383 window.location.href = data.redirect_url;
3384 } else {
3385 // Error - show message and reset button
3386 console.error('Booking session error:', data);
3387 btn.innerHTML = originalText;
3388 btn.disabled = false;
3389 this.showBookingError(btn, data.message || 'Unable to process booking. Please try again.');
3390 }
3391 })
3392 .catch(error => {
3393 console.error('Error setting booking session:', error);
3394 btn.innerHTML = originalText;
3395 btn.disabled = false;
3396 this.showBookingError(btn, 'Unable to process booking. Please try again.');
3397 });
3398 }
3399
3400 initTravelerSelectors() {
3401 // Initialize traveler-based pricing selectors (dropdowns with categories)
3402 const participantsSelects = this.section.querySelectorAll('.yatra-availability-participants');
3403
3404 participantsSelects.forEach((select) => {
3405 const itemIndex = select.getAttribute('data-item');
3406 const display = select.querySelector('.yatra-availability-participants-display');
3407 const quantitySelector = select.querySelector('.yatra-availability-quantity-selector');
3408 const categoryInputs = select.querySelectorAll('.yatra-availability-category');
3409
3410 if (!display || !quantitySelector || categoryInputs.length === 0) return;
3411
3412 // Make the whole select area clickable
3413 select.style.cursor = 'pointer';
3414
3415 // Toggle dropdown on select click
3416 select.addEventListener('click', (e) => {
3417 // Don't toggle if clicking on buttons or inputs
3418 if (e.target.closest('.yatra-quantity-btn') || e.target.closest('.yatra-quantity-input')) return;
3419 e.stopPropagation();
3420 select.classList.toggle('active');
3421 });
3422
3423 // Update display text based on category inputs
3424 const updateDisplay = () => {
3425 const parts = [];
3426 categoryInputs.forEach((input) => {
3427 const value = parseInt(input.value) || 0;
3428 if (value > 0) {
3429 const row = input.closest('.yatra-quantity-row');
3430 const categoryLabel = row ? row.querySelector('.yatra-quantity-title')?.textContent : 'Traveler';
3431 parts.push(`${categoryLabel} x ${value}`);
3432 }
3433 });
3434
3435 if (parts.length === 0) {
3436 // Default to first category
3437 const firstRow = categoryInputs[0]?.closest('.yatra-quantity-row');
3438 const firstLabel = firstRow ? firstRow.querySelector('.yatra-quantity-title')?.textContent : 'Traveler';
3439 display.textContent = `${firstLabel} x 1`;
3440 } else {
3441 display.textContent = parts.join(', ');
3442 }
3443 };
3444
3445 // Quantity button handlers
3446 const quantityButtons = select.querySelectorAll('.yatra-quantity-btn');
3447 quantityButtons.forEach((btn) => {
3448 btn.addEventListener('click', (e) => {
3449 e.preventDefault();
3450 e.stopPropagation();
3451
3452 const targetId = btn.getAttribute('data-target');
3453 // Find the input in the same row
3454 const row = btn.closest('.yatra-quantity-row');
3455 const input = row ? row.querySelector('.yatra-quantity-input') : null;
3456
3457 if (!input) return;
3458
3459 const isMinus = btn.classList.contains('yatra-quantity-minus');
3460 const isPlus = btn.classList.contains('yatra-quantity-plus');
3461
3462 let current = parseInt(input.value) || 0;
3463 const min = parseInt(input.getAttribute('min')) || 0;
3464 const max = parseInt(input.getAttribute('max')) || 999;
3465 let newValue = current;
3466
3467 if (isPlus && current < max) {
3468 newValue = current + 1;
3469 } else if (isMinus && current > min) {
3470 newValue = current - 1;
3471 }
3472
3473 if (newValue !== current) {
3474 input.value = newValue;
3475 updateDisplay();
3476
3477 // Update button states
3478 const minusBtn = row.querySelector('.yatra-quantity-minus');
3479 const plusBtn = row.querySelector('.yatra-quantity-plus');
3480 if (minusBtn) minusBtn.disabled = newValue <= min;
3481 if (plusBtn) plusBtn.disabled = newValue >= max;
3482
3483 // Update total price
3484 this.updateTotalPrice(itemIndex);
3485
3486 // Sync with sidebar if exists
3487 this.syncSidebarTravelers(categoryInputs);
3488 }
3489 });
3490 });
3491
3492 // Close dropdown when clicking outside
3493 document.addEventListener('click', (e) => {
3494 if (!select.contains(e.target)) {
3495 select.classList.remove('active');
3496 }
3497 });
3498
3499 // Initialize display
3500 updateDisplay();
3501
3502 // Initialize button states
3503 categoryInputs.forEach((input) => {
3504 const value = parseInt(input.value || 0);
3505 const min = parseInt(input.getAttribute('min') || 0);
3506 const max = parseInt(input.getAttribute('max') || 999);
3507 const row = input.closest('.yatra-quantity-row');
3508 if (row) {
3509 const minusBtn = row.querySelector('.yatra-quantity-minus');
3510 const plusBtn = row.querySelector('.yatra-quantity-plus');
3511 if (minusBtn) minusBtn.disabled = value <= min;
3512 if (plusBtn) plusBtn.disabled = value >= max;
3513 }
3514 });
3515
3516 // Initialize total price for this card
3517 this.updateTotalPrice(itemIndex);
3518 });
3519
3520 // Initialize regular pricing selectors (simple number inputs)
3521 const simpleTravelerInputs = this.section.querySelectorAll('.yatra-availability-travelers-simple');
3522 simpleTravelerInputs.forEach((container) => {
3523 const itemIndex = container.getAttribute('data-item');
3524 const input = container.querySelector('.yatra-availability-num-travelers');
3525 const quantityButtons = container.querySelectorAll('.yatra-quantity-btn');
3526
3527 if (!input) return;
3528
3529 quantityButtons.forEach((btn) => {
3530 btn.addEventListener('click', (e) => {
3531 e.preventDefault();
3532 e.stopPropagation();
3533
3534 const isMinus = btn.classList.contains('yatra-quantity-minus');
3535 const isPlus = btn.classList.contains('yatra-quantity-plus');
3536
3537 let current = parseInt(input.value) || 1;
3538 const min = parseInt(input.getAttribute('min')) || 1;
3539 const max = parseInt(input.getAttribute('max')) || 999;
3540 let newValue = current;
3541
3542 if (isPlus && current < max) {
3543 newValue = current + 1;
3544 } else if (isMinus && current > min) {
3545 newValue = current - 1;
3546 }
3547
3548 if (newValue !== current) {
3549 input.value = newValue;
3550
3551 // Update button states
3552 const minusBtn = container.querySelector('.yatra-quantity-minus');
3553 const plusBtn = container.querySelector('.yatra-quantity-plus');
3554 if (minusBtn) minusBtn.disabled = newValue <= min;
3555 if (plusBtn) plusBtn.disabled = newValue >= max;
3556
3557 // Update total price
3558 this.updateTotalPrice(itemIndex);
3559 }
3560 });
3561 });
3562
3563 // Initialize total price for this card
3564 this.updateTotalPrice(itemIndex);
3565 });
3566 }
3567
3568 setupToggleHandlers() {
3569 if (!this.availabilityItems || !this.availabilityItems.length) return;
3570 this.availabilityItems.forEach((item) => {
3571 const toggle = item.querySelector('.yatra-availability-toggle');
3572 if (!toggle) return;
3573
3574 toggle.addEventListener('click', (e) => {
3575 e.preventDefault();
3576 const isOpen = item.classList.contains('open');
3577 this.availabilityItems.forEach((other) => other.classList.remove('open'));
3578 if (!isOpen) {
3579 item.classList.add('open');
3580 }
3581 });
3582 });
3583 }
3584
3585 getTravelerCount(itemIndex, type) {
3586 const select = this.section.querySelector(
3587 `.yatra-availability-participants[data-item="${itemIndex}"]`
3588 );
3589 if (!select) return type === 'adults' ? 1 : 0;
3590
3591 const input = select.querySelector(
3592 type === 'adults' ? '.yatra-availability-adults' : '.yatra-availability-children'
3593 );
3594 if (!input) return type === 'adults' ? 1 : 0;
3595 const value = parseInt(input.value) || 0;
3596 return type === 'adults' && value === 0 ? 1 : value;
3597 }
3598
3599 syncSidebarTravelers(categoryInputs) {
3600 // Sync availability card travelers to sidebar
3601 const sidebarDisplay = document.querySelector('.yatra-booking-card .yatra-participants-display');
3602 if (!sidebarDisplay) return;
3603
3604 const sidebarSelector = document.querySelector('.yatra-booking-card .yatra-booking-quantity-selector');
3605 if (!sidebarSelector) return;
3606
3607 // Build display text from category inputs
3608 const parts = [];
3609 categoryInputs.forEach((input) => {
3610 const value = parseInt(input.value) || 0;
3611 if (value > 0) {
3612 const row = input.closest('.yatra-quantity-row');
3613 const categoryLabel = row ? row.querySelector('.yatra-quantity-title')?.textContent : 'Traveler';
3614 parts.push(`${categoryLabel} x ${value}`);
3615
3616 // Also update sidebar inputs if they exist
3617 const categoryId = input.getAttribute('data-category');
3618 const sidebarInput = sidebarSelector.querySelector(`input[id="traveler_${categoryId}"]`);
3619 if (sidebarInput) {
3620 sidebarInput.value = value;
3621 // Update button states
3622 const sidebarRow = sidebarInput.closest('.yatra-quantity-row');
3623 if (sidebarRow) {
3624 const min = parseInt(sidebarInput.getAttribute('min') || 0);
3625 const max = parseInt(sidebarInput.getAttribute('max') || 999);
3626 const minusBtn = sidebarRow.querySelector('.yatra-quantity-minus');
3627 const plusBtn = sidebarRow.querySelector('.yatra-quantity-plus');
3628 if (minusBtn) minusBtn.disabled = value <= min;
3629 if (plusBtn) plusBtn.disabled = value >= max;
3630 }
3631 }
3632 }
3633 });
3634
3635 // Update sidebar display text
3636 if (parts.length > 0) {
3637 sidebarDisplay.textContent = parts.join(', ');
3638 }
3639 }
3640
3641 updateTotalPrice(itemIndex) {
3642 const totalAmountElement = this.section.querySelector(
3643 `.yatra-card-total-amount[data-item="${itemIndex}"]`
3644 );
3645 if (!totalAmountElement) return;
3646
3647 // Check for traveler-based pricing first
3648 const categoryInputs = this.section.querySelectorAll(`.yatra-availability-category[data-item="${itemIndex}"]`);
3649 let totalPrice = 0;
3650 let totalTravelers = 0;
3651
3652 if (categoryInputs.length > 0) {
3653 // Traveler-based pricing: sum up each category's price * quantity (respecting pricing mode)
3654 categoryInputs.forEach((input) => {
3655 const quantity = parseInt(input.value) || 0;
3656 const row = input.closest('.yatra-quantity-row');
3657 const price = row ? parseFloat(row.getAttribute('data-price')) || 0 : 0;
3658 const pricingMode = (row ? row.getAttribute('data-pricing-mode') : null) || input.getAttribute('data-pricing-mode') || 'per_person';
3659
3660 if (pricingMode === 'per_group') {
3661 if (quantity > 0) {
3662 totalPrice += price;
3663 }
3664 } else {
3665 totalPrice += quantity * price;
3666 }
3667 totalTravelers += quantity;
3668 });
3669 } else {
3670 // Regular pricing: simple number of travelers
3671 const numTravelersInput = this.section.querySelector(`.yatra-availability-num-travelers[data-item="${itemIndex}"]`);
3672 if (numTravelersInput) {
3673 totalTravelers = parseInt(numTravelersInput.value) || 1;
3674 const basePrice = parseFloat(numTravelersInput.getAttribute('data-price')) || parseFloat(totalAmountElement.getAttribute('data-base-price')) || 0;
3675 totalPrice = totalTravelers * basePrice;
3676 }
3677 }
3678
3679 // Apply group discount if applicable
3680 const groupDiscountResult = this.calculateGroupDiscount(totalPrice, totalTravelers);
3681 const finalPrice = groupDiscountResult.finalPrice;
3682 const groupDiscountApplied = groupDiscountResult.discountApplied;
3683 const groupDiscountAmount = groupDiscountResult.discountAmount;
3684
3685 // Format price with shared formatter (uses yatraTripData settings)
3686 if (typeof yatra_format_price_js === 'function') {
3687 totalAmountElement.textContent = yatra_format_price_js(finalPrice);
3688 } else {
3689 totalAmountElement.textContent = String(finalPrice.toFixed(2));
3690 }
3691
3692 // Update the note text
3693 const noteElement = this.section.querySelector(
3694 `.yatra-card-total-note[data-item="${itemIndex}"]`
3695 );
3696 if (noteElement) {
3697 let travelerText = totalTravelers === 1
3698 ? 'for 1 traveler'
3699 : `for ${totalTravelers} travelers`;
3700
3701 // Add group discount info if applied
3702 if (groupDiscountApplied && groupDiscountAmount > 0) {
3703 const discountText = (typeof yatra_format_price_js === 'function')
3704 ? yatra_format_price_js(groupDiscountAmount)
3705 : String(groupDiscountAmount.toFixed(2));
3706 travelerText += ` (Group discount: -${discountText})`;
3707 }
3708 noteElement.textContent = travelerText;
3709 }
3710
3711 // Show/hide group discount applied badge
3712 this.updateGroupDiscountBadge(itemIndex, groupDiscountApplied, groupDiscountResult);
3713 }
3714
3715 /**
3716 * Calculate group discount based on total travelers
3717 */
3718 calculateGroupDiscount(totalPrice, totalTravelers) {
3719 const result = {
3720 finalPrice: totalPrice,
3721 discountApplied: false,
3722 discountAmount: 0,
3723 discountPercentage: 0,
3724 discountType: null
3725 };
3726
3727 // Get group discounts from the booking card data attribute
3728 const bookingCard = document.querySelector('.yatra-booking-card');
3729 if (!bookingCard) return result;
3730
3731 const groupDiscountsData = bookingCard.getAttribute('data-group-discounts');
3732 if (!groupDiscountsData) return result;
3733
3734 let groupDiscounts;
3735 try {
3736 groupDiscounts = JSON.parse(groupDiscountsData);
3737 } catch (e) {
3738 console.error('Failed to parse group discounts:', e);
3739 return result;
3740 }
3741
3742 if (!Array.isArray(groupDiscounts) || groupDiscounts.length === 0) return result;
3743
3744 // Find the applicable discount based on total travelers
3745 let applicableDiscount = null;
3746
3747 for (const discount of groupDiscounts) {
3748 // Check if discount has ranges (new format)
3749 if (discount.group_discount_ranges && discount.group_discount_ranges.length > 0) {
3750 for (const range of discount.group_discount_ranges) {
3751 const minSize = parseInt(range.min_group_size) || 0;
3752 const maxSize = range.max_group_size ? parseInt(range.max_group_size) : Infinity;
3753
3754 if (totalTravelers >= minSize && totalTravelers <= maxSize) {
3755 applicableDiscount = {
3756 type: range.discount_type || 'percentage',
3757 amount: parseFloat(range.discount_amount) || 0
3758 };
3759 break;
3760 }
3761 }
3762 } else {
3763 // Legacy format: single min_group_size
3764 const minSize = parseInt(discount.min_group_size) || 0;
3765 const maxSize = discount.max_group_size ? parseInt(discount.max_group_size) : Infinity;
3766
3767 if (totalTravelers >= minSize && totalTravelers <= maxSize) {
3768 applicableDiscount = {
3769 type: discount.discount_type || 'percentage',
3770 amount: parseFloat(discount.discount_amount) || 0
3771 };
3772 }
3773 }
3774
3775 if (applicableDiscount) break;
3776 }
3777
3778 // Apply the discount if found
3779 if (applicableDiscount && applicableDiscount.amount > 0) {
3780 result.discountApplied = true;
3781 result.discountType = applicableDiscount.type;
3782
3783 if (applicableDiscount.type === 'percentage') {
3784 result.discountPercentage = applicableDiscount.amount;
3785 result.discountAmount = totalPrice * (applicableDiscount.amount / 100);
3786 } else {
3787 // Fixed amount discount
3788 result.discountAmount = applicableDiscount.amount;
3789 }
3790
3791 result.finalPrice = Math.max(0, totalPrice - result.discountAmount);
3792 }
3793
3794 return result;
3795 }
3796
3797 /**
3798 * Update group discount badge visibility
3799 */
3800 updateGroupDiscountBadge(itemIndex, discountApplied, discountResult) {
3801 // Find or create the group discount applied badge for this item
3802 const availabilityItem = this.section.querySelector(`.yatra-availability-item[data-item="${itemIndex}"]`);
3803 if (!availabilityItem) return;
3804
3805 let badge = availabilityItem.querySelector('.yatra-group-discount-applied');
3806
3807 if (discountApplied && discountResult.discountAmount > 0) {
3808 if (!badge) {
3809 badge = document.createElement('div');
3810 badge.className = 'yatra-group-discount-applied';
3811 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;';
3812
3813 // Insert after the total amount
3814 const totalSection = availabilityItem.querySelector('.yatra-card-total');
3815 if (totalSection) {
3816 totalSection.appendChild(badge);
3817 }
3818 }
3819
3820 // Update badge content
3821 const discountText = discountResult.discountType === 'percentage'
3822 ? `${discountResult.discountPercentage}% group discount applied!`
3823 : `Group discount applied!`;
3824 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}`;
3825 } else if (badge) {
3826 badge.remove();
3827 }
3828 }
3829
3830 filterByMonth(filter) {
3831 this.availabilityItems.forEach((item) => {
3832 if (filter === 'all') {
3833 item.style.display = 'block';
3834 } else {
3835 const itemMonth = item.getAttribute('data-month');
3836 if (itemMonth === filter) {
3837 item.style.display = 'block';
3838 } else {
3839 item.style.display = 'none';
3840 }
3841 }
3842 });
3843 }
3844
3845 sortItems(sortBy) {
3846 const items = Array.from(this.availabilityItems);
3847 const list = this.section.querySelector('.yatra-availability-list');
3848 if (!list) return;
3849
3850 items.sort((a, b) => {
3851 switch (sortBy) {
3852 case 'date-asc':
3853 return new Date(a.getAttribute('data-date')) - new Date(b.getAttribute('data-date'));
3854 case 'date-desc':
3855 return new Date(b.getAttribute('data-date')) - new Date(a.getAttribute('data-date'));
3856 case 'price-asc':
3857 return parseFloat(a.getAttribute('data-price')) - parseFloat(b.getAttribute('data-price'));
3858 case 'price-desc':
3859 return parseFloat(b.getAttribute('data-price')) - parseFloat(a.getAttribute('data-price'));
3860 case 'seats-desc':
3861 return parseInt(b.getAttribute('data-seats')) - parseInt(a.getAttribute('data-seats'));
3862 default:
3863 return 0;
3864 }
3865 });
3866
3867 // Re-append sorted items
3868 items.forEach((item) => {
3869 list.appendChild(item);
3870 });
3871 }
3872
3873 handleBookNow(date, price, button, adults = 1, children = 0) {
3874 // Get travelers from the availability card if not provided
3875 const itemIndex = button.getAttribute('data-item');
3876 if (itemIndex) {
3877 adults = this.getTravelerCount(itemIndex, 'adults');
3878 children = this.getTravelerCount(itemIndex, 'children');
3879 }
3880
3881 // Update the main booking form with selected date
3882 const dateInput = document.getElementById('travel_date');
3883 if (dateInput) {
3884 // Format date for display (YYYY-MM-DD)
3885 dateInput.value = date;
3886 // Trigger change event if using Flatpickr
3887 if (dateInput._flatpickr) {
3888 dateInput._flatpickr.setDate(date, true);
3889 }
3890 }
3891
3892 // Update travelers in booking sidebar
3893 if (window.bookingSidebar) {
3894 window.bookingSidebar.setTravelers(adults, children);
3895 }
3896
3897 // Scroll to booking sidebar
3898 const bookingSidebar = document.getElementById('booking');
3899 if (bookingSidebar) {
3900 bookingSidebar.scrollIntoView({ behavior: 'smooth', block: 'start' });
3901 }
3902
3903 // Show success feedback
3904 const originalText = button.textContent;
3905 button.textContent = 'Selected!';
3906 button.style.background = '#10b981';
3907 setTimeout(() => {
3908 button.textContent = originalText;
3909 button.style.background = '';
3910 }, 2000);
3911 }
3912
3913 handleLoadMore() {
3914 const tripId = window.yatraTripData?.tripId || this.section?.getAttribute('data-trip-id') || 0;
3915 const loadMoreContainer = this.section.querySelector('.yatra-availability-load-more');
3916 const btn = this.section.querySelector('.yatra-availability-load-more-btn');
3917 const countInfo = this.section.querySelector('.yatra-availability-count-info');
3918 const list = this.section.querySelector('.yatra-availability-list');
3919
3920 if (!tripId || !btn || !list || !loadMoreContainer) return;
3921
3922 const perPage = parseInt(list.dataset.perPage, 10) || parseInt(loadMoreContainer.dataset.perPage, 10) || 10;
3923 const currentPage = parseInt(list.dataset.page, 10) || 1;
3924 const nextPage = currentPage + 1;
3925 const monthFilter = list.dataset.monthFilter || loadMoreContainer.dataset.monthFilter || this.currentMonthFilter || 'all';
3926 const sortKey = list.dataset.sort || loadMoreContainer.dataset.sort || this.currentSortKey || 'date-asc';
3927 const travelDateInput = document.getElementById('travel_date');
3928 const travelDate = travelDateInput && travelDateInput.value ? travelDateInput.value : undefined;
3929
3930 const originalHTML = btn.innerHTML;
3931 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> ';
3932 btn.innerHTML = loadingSvg + 'Loading...';
3933 btn.disabled = true;
3934
3935 const url = buildAvailabilityTemplateUrl(tripId, {
3936 date: travelDate,
3937 month_filter: monthFilter,
3938 sort: sortKey,
3939 page: nextPage,
3940 per_page: perPage,
3941 partial: true,
3942 });
3943
3944 fetch(url, {
3945 method: 'GET',
3946 headers: {
3947 'Content-Type': 'application/json',
3948 },
3949 credentials: 'omit',
3950 })
3951 .then((resp) => {
3952 if (!resp.ok) {
3953 throw new Error('Load more failed');
3954 }
3955 return resp.json();
3956 })
3957 .then((data) => {
3958 const html = data?.html || data?.data?.html;
3959 if (!html || typeof html !== 'string') {
3960 throw new Error('No HTML returned');
3961 }
3962
3963 const tempDiv = document.createElement('div');
3964 tempDiv.innerHTML = html.trim();
3965
3966 while (tempDiv.firstChild) {
3967 list.appendChild(tempDiv.firstChild);
3968 }
3969
3970 const total = Number(data.total) || parseInt(list.dataset.total, 10) || 0;
3971 const loadedCount = Number(data.loaded_count);
3972 const hasMore = !!data.has_more;
3973 const displayed = Number.isFinite(loadedCount)
3974 ? loadedCount
3975 : (parseInt(list.dataset.displayed, 10) || 0) + perPage;
3976
3977 list.dataset.total = String(total);
3978 list.dataset.displayed = String(displayed);
3979 list.dataset.page = String(nextPage);
3980
3981 this.availabilityItems = this.section.querySelectorAll('.yatra-availability-card');
3982 this.setupToggleHandlers();
3983 this.initTravelerSelectors();
3984 this.attachBookNowHandlers();
3985
3986 if (!hasMore) {
3987 loadMoreContainer.style.display = 'none';
3988 return;
3989 }
3990
3991 const remaining = Math.max(0, total - displayed);
3992 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)`;
3993 btn.disabled = false;
3994
3995 loadMoreContainer.dataset.currentPage = String(nextPage);
3996 loadMoreContainer.dataset.loaded = String(displayed);
3997 loadMoreContainer.dataset.total = String(total);
3998
3999 if (countInfo) {
4000 countInfo.textContent = `Showing ${displayed} of ${total} departures`;
4001 }
4002 })
4003 .catch((err) => {
4004 console.error('[Yatra] Load more failed:', err);
4005 btn.innerHTML = originalHTML;
4006 btn.disabled = false;
4007 });
4008 }
4009
4010 attachBookNowHandlers() {
4011 const bookButtons = this.section.querySelectorAll('.yatra-card-book-btn');
4012 bookButtons.forEach((btn) => {
4013 const newBtn = btn.cloneNode(true);
4014 btn.parentNode.replaceChild(newBtn, btn);
4015 newBtn.addEventListener('click', (e) => {
4016 e.preventDefault();
4017 this.handleAvailabilityBookNowClick(newBtn);
4018 });
4019 });
4020 }
4021
4022 /**
4023 * Show booking error message to user just before the Book Now button
4024 * @param {HTMLElement} button - The Book Now button that was clicked
4025 * @param {string} message - Error message to display
4026 */
4027 showBookingError(button, message) {
4028 // Find the card/row containing this button
4029 const card = button.closest('.yatra-availability-card') || button.closest('.yatra-availability-item');
4030 if (!card) return;
4031
4032 // Remove any existing error message in this card
4033 const existingError = card.querySelector('.yatra-booking-error-toast');
4034 if (existingError) {
4035 existingError.remove();
4036 }
4037
4038 // Create error toast
4039 const toast = document.createElement('div');
4040 toast.className = 'yatra-booking-error-toast';
4041 toast.innerHTML = `
4042 <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;">
4043 <svg width="20" height="20" fill="none" stroke="#dc2626" viewBox="0 0 24 24" style="flex-shrink: 0;">
4044 <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"/>
4045 </svg>
4046 <span style="flex: 1; color: #991b1b; font-size: 13px; font-weight: 500;">${message}</span>
4047 <button type="button" style="background: none; border: none; cursor: pointer; padding: 2px; color: #991b1b; line-height: 1;" onclick="this.parentElement.parentElement.remove()">
4048 <svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
4049 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
4050 </svg>
4051 </button>
4052 </div>
4053 `;
4054
4055 // Insert just before the button (or button's parent row)
4056 const buttonRow = button.closest('.yatra-card-actions') || button.parentElement;
4057 if (buttonRow) {
4058 buttonRow.insertAdjacentElement('beforebegin', toast);
4059 } else {
4060 button.insertAdjacentElement('beforebegin', toast);
4061 }
4062
4063 // Auto-remove after 8 seconds
4064 setTimeout(() => {
4065 if (toast.parentElement) {
4066 toast.remove();
4067 }
4068 }, 8000);
4069
4070 // Scroll to the error message
4071 toast.scrollIntoView({ behavior: 'smooth', block: 'center' });
4072 }
4073 }
4074
4075 /**
4076 * Itinerary Section Class
4077 * Handles day toggle functionality for itinerary timeline
4078 */
4079 class ItinerarySection {
4080 constructor() {
4081 this.init();
4082 }
4083
4084 init() {
4085 if (document.readyState === 'loading') {
4086 document.addEventListener('DOMContentLoaded', () => this.setup());
4087 } else {
4088 this.setup();
4089 }
4090 }
4091
4092 setup() {
4093 this.container = document.querySelector('.yatra-itinerary-timeline');
4094 if (!this.container) return;
4095
4096 this.days = this.container.querySelectorAll('.yatra-itinerary-day');
4097 this.toggleAllBtn = document.getElementById('yatra-toggle-all');
4098 this.isAllExpanded = false;
4099 this.attachEventListeners();
4100 this.initializeDefaultState();
4101 }
4102
4103 /** First day is expanded via PHP (`.is-day-expanded`); no inline styles. */
4104 initializeDefaultState() {}
4105
4106 attachEventListeners() {
4107 // Toggle All button
4108 if (this.toggleAllBtn) {
4109 this.toggleAllBtn.addEventListener('click', () => this.toggleAll());
4110 }
4111
4112 this.days.forEach(day => {
4113 const header = day.querySelector('.yatra-itinerary-day-header');
4114 const toggle = day.querySelector('.yatra-day-toggle');
4115 const content = day.querySelector('.yatra-itinerary-day-content');
4116
4117 if (header && content) {
4118 header.addEventListener('click', (e) => {
4119 // Don't toggle if clicking on a link inside header
4120 if (e.target.tagName === 'A') return;
4121 this.toggleDay(day, toggle);
4122 });
4123 }
4124 });
4125 }
4126
4127 toggleDay(day, toggle) {
4128 const isExpanded = day.classList.contains('is-day-expanded');
4129 if (isExpanded) {
4130 day.classList.remove('is-day-expanded');
4131 day.setAttribute('data-expanded', 'false');
4132 if (toggle) toggle.setAttribute('aria-expanded', 'false');
4133 } else {
4134 day.classList.add('is-day-expanded');
4135 day.setAttribute('data-expanded', 'true');
4136 if (toggle) toggle.setAttribute('aria-expanded', 'true');
4137 }
4138 }
4139
4140 toggleAll() {
4141 if (this.isAllExpanded) {
4142 this.days.forEach(day => {
4143 const toggle = day.querySelector('.yatra-day-toggle');
4144 day.classList.remove('is-day-expanded');
4145 day.setAttribute('data-expanded', 'false');
4146 if (toggle) toggle.setAttribute('aria-expanded', 'false');
4147 });
4148 this.updateToggleButton(false);
4149 } else {
4150 this.days.forEach(day => {
4151 const toggle = day.querySelector('.yatra-day-toggle');
4152 day.classList.add('is-day-expanded');
4153 day.setAttribute('data-expanded', 'true');
4154 if (toggle) toggle.setAttribute('aria-expanded', 'true');
4155 });
4156 this.updateToggleButton(true);
4157 }
4158 this.isAllExpanded = !this.isAllExpanded;
4159 }
4160
4161 updateToggleButton(isExpanded) {
4162 if (!this.toggleAllBtn) return;
4163 this.toggleAllBtn.classList.toggle('is-all-expanded', isExpanded);
4164 const toggleText = this.toggleAllBtn.querySelector('.toggle-text');
4165 if (toggleText) {
4166 toggleText.textContent = isExpanded ? 'Collapse All' : 'Expand All';
4167 }
4168 }
4169 }
4170
4171 // ============================================
4172 // REVIEW FORM
4173 // ============================================
4174 class ReviewForm {
4175 constructor() {
4176 this.form = document.getElementById('yatra-review-form');
4177 if (this.form) {
4178 this.init();
4179 }
4180 }
4181
4182 init() {
4183 this.setupStarRating();
4184 this.setupFormSubmission();
4185 }
4186
4187 setupStarRating() {
4188 const ratingContainer = this.form.querySelector('.yatra-star-rating-input');
4189 if (!ratingContainer) return;
4190
4191 const ratingInputs = Array.from(ratingContainer.querySelectorAll('input[type="radio"]'));
4192 const ratingLabels = Array.from(ratingContainer.querySelectorAll('.yatra-star-label'));
4193
4194 // Get the rating value for a label by its associated input
4195 const getLabelRating = (label) => {
4196 const forAttr = label.getAttribute('for');
4197 if (forAttr) {
4198 const input = document.getElementById(forAttr);
4199 return input ? parseInt(input.value) : 0;
4200 }
4201 return 0;
4202 };
4203
4204 // Function to update visual state
4205 const updateStars = (rating) => {
4206 ratingLabels.forEach((label) => {
4207 const labelRating = getLabelRating(label);
4208 if (labelRating <= rating) {
4209 label.classList.add('selected');
4210 } else {
4211 label.classList.remove('selected');
4212 }
4213 });
4214 };
4215
4216 // Initialize visual state based on currently checked rating
4217 const checkedInput = ratingContainer.querySelector('input:checked');
4218 if (checkedInput) {
4219 const rating = parseInt(checkedInput.value);
4220 // Use setTimeout to ensure DOM is fully ready
4221 setTimeout(() => {
4222 updateStars(rating);
4223 }, 0);
4224 }
4225
4226 // Handle rating changes via input change event
4227 ratingInputs.forEach(input => {
4228 input.addEventListener('change', () => {
4229 updateStars(parseInt(input.value));
4230 });
4231 });
4232
4233 // Handle direct clicks on labels
4234 ratingLabels.forEach((label) => {
4235 label.addEventListener('click', (e) => {
4236 e.preventDefault();
4237 e.stopPropagation();
4238
4239 const forAttr = label.getAttribute('for');
4240 if (!forAttr) return;
4241
4242 const targetInput = document.getElementById(forAttr);
4243 if (targetInput && !targetInput.disabled) {
4244 targetInput.checked = true;
4245 const ratingValue = parseInt(targetInput.value);
4246 updateStars(ratingValue);
4247 // Trigger change event for any other listeners
4248 targetInput.dispatchEvent(new Event('change', { bubbles: true }));
4249 }
4250 });
4251
4252 // Hover effect
4253 label.addEventListener('mouseenter', () => {
4254 const hoverRating = getLabelRating(label);
4255 ratingLabels.forEach((l) => {
4256 const lRating = getLabelRating(l);
4257 if (lRating <= hoverRating) {
4258 l.classList.add('hover');
4259 } else {
4260 l.classList.remove('hover');
4261 }
4262 });
4263 });
4264 });
4265
4266 // Remove hover effect when leaving the container
4267 ratingContainer.addEventListener('mouseleave', () => {
4268 ratingLabels.forEach(l => l.classList.remove('hover'));
4269 });
4270 }
4271
4272 setupFormSubmission() {
4273 this.form.addEventListener('submit', async (e) => {
4274 e.preventDefault();
4275
4276 const submitBtn = this.form.querySelector('.yatra-review-submit-btn');
4277 const originalText = submitBtn.innerHTML;
4278
4279 // Disable button and show loading
4280 submitBtn.disabled = true;
4281 submitBtn.innerHTML = '<span class="yatra-loading-spinner"></span> Submitting...';
4282
4283 try {
4284 const formData = new FormData(this.form);
4285 formData.append('action', 'yatra_submit_review');
4286
4287 const response = await fetch(yatraTripData?.ajaxUrl || '/wp-admin/admin-ajax.php', {
4288 method: 'POST',
4289 body: formData,
4290 credentials: 'same-origin'
4291 });
4292
4293 const result = await response.json();
4294
4295 if (result.success) {
4296 // Show success message
4297 this.showMessage('success', result.data?.message || 'Thank you for your review!');
4298
4299 // Check if this was an edit or new submission
4300 const isEdit = this.form.querySelector('input[name="action_type"]')?.value === 'edit';
4301
4302 if (!isEdit) {
4303 // Only reset form for new submissions, not edits
4304 this.form.reset();
4305
4306 // Reset star rating to 5 stars
4307 const fiveStarInput = this.form.querySelector('input[name="rating"][value="5"]');
4308 if (fiveStarInput) {
4309 fiveStarInput.checked = true;
4310 // Also update the visual display
4311 const ratingLabels = this.form.querySelectorAll('.yatra-star-label');
4312 ratingLabels.forEach(label => label.classList.add('selected'));
4313 }
4314 }
4315 } else {
4316 this.showMessage('error', result.data?.message || 'Failed to submit review. Please try again.');
4317 }
4318 } catch (error) {
4319 console.error('Review submission error:', error);
4320 this.showMessage('error', 'An error occurred. Please try again.');
4321 } finally {
4322 submitBtn.disabled = false;
4323 submitBtn.innerHTML = originalText;
4324 }
4325 });
4326 }
4327
4328 showMessage(type, message) {
4329 // Remove any existing messages
4330 const existingMsg = this.form.querySelector('.yatra-review-message');
4331 if (existingMsg) existingMsg.remove();
4332
4333 const msgEl = document.createElement('div');
4334 msgEl.className = `yatra-review-message yatra-review-message-${type}`;
4335 msgEl.innerHTML = `<p>${message}</p>`;
4336
4337 this.form.insertBefore(msgEl, this.form.firstChild);
4338
4339 // Auto-remove after 5 seconds
4340 setTimeout(() => {
4341 msgEl.remove();
4342 }, 5000);
4343 }
4344 }
4345
4346 /**
4347 * Similar Adventures Carousel
4348 * Handles carousel navigation for similar trips section
4349 */
4350 class SimilarTripsCarousel {
4351 constructor() {
4352 this.track = null;
4353 this.prevBtn = null;
4354 this.nextBtn = null;
4355 this.items = [];
4356 this.currentPosition = 0;
4357 this.itemWidth = 0;
4358 this.visibleItems = 4;
4359 this.gap = 24;
4360 this.init();
4361 }
4362
4363 init() {
4364 if (document.readyState === 'loading') {
4365 document.addEventListener('DOMContentLoaded', () => this.setup());
4366 } else {
4367 this.setup();
4368 }
4369 }
4370
4371 setup() {
4372 this.track = document.getElementById('similar-carousel');
4373 this.prevBtn = document.getElementById('similar-prev');
4374 this.nextBtn = document.getElementById('similar-next');
4375
4376 if (!this.track || !this.prevBtn || !this.nextBtn) return;
4377
4378 this.items = Array.from(this.track.querySelectorAll('.yatra-carousel-item'));
4379 if (this.items.length === 0) return;
4380
4381 this.calculateDimensions();
4382 this.attachEventListeners();
4383 this.updateButtonStates();
4384
4385 // Recalculate on resize
4386 window.addEventListener('resize', () => {
4387 this.calculateDimensions();
4388 this.updateButtonStates();
4389 });
4390 }
4391
4392 calculateDimensions() {
4393 const wrapper = this.track.parentElement;
4394 const wrapperWidth = wrapper.offsetWidth;
4395
4396 // Determine visible items based on screen width
4397 if (window.innerWidth <= 600) {
4398 this.visibleItems = 1;
4399 } else if (window.innerWidth <= 900) {
4400 this.visibleItems = 2;
4401 } else if (window.innerWidth <= 1200) {
4402 this.visibleItems = 3;
4403 } else {
4404 this.visibleItems = 4;
4405 }
4406
4407 // Calculate item width
4408 this.itemWidth = (wrapperWidth - (this.gap * (this.visibleItems - 1))) / this.visibleItems;
4409
4410 // Apply width to items
4411 this.items.forEach(item => {
4412 item.style.flex = `0 0 ${this.itemWidth}px`;
4413 item.style.minWidth = `${this.itemWidth}px`;
4414 });
4415
4416 // Reset position if out of bounds
4417 const maxPosition = Math.max(0, this.items.length - this.visibleItems);
4418 if (this.currentPosition > maxPosition) {
4419 this.currentPosition = maxPosition;
4420 this.updateTrackPosition();
4421 }
4422 }
4423
4424 attachEventListeners() {
4425 this.prevBtn.addEventListener('click', () => this.slide('prev'));
4426 this.nextBtn.addEventListener('click', () => this.slide('next'));
4427
4428 // Touch/swipe support for mobile
4429 let startX = 0;
4430 let currentX = 0;
4431 let isDragging = false;
4432
4433 this.track.addEventListener('touchstart', (e) => {
4434 startX = e.touches[0].clientX;
4435 isDragging = true;
4436 }, { passive: true });
4437
4438 this.track.addEventListener('touchmove', (e) => {
4439 if (!isDragging) return;
4440 currentX = e.touches[0].clientX;
4441 }, { passive: true });
4442
4443 this.track.addEventListener('touchend', () => {
4444 if (!isDragging) return;
4445 isDragging = false;
4446
4447 const diff = startX - currentX;
4448 const threshold = 50;
4449
4450 if (diff > threshold) {
4451 this.slide('next');
4452 } else if (diff < -threshold) {
4453 this.slide('prev');
4454 }
4455 });
4456 }
4457
4458 slide(direction) {
4459 const maxPosition = Math.max(0, this.items.length - this.visibleItems);
4460
4461 if (direction === 'next' && this.currentPosition < maxPosition) {
4462 this.currentPosition++;
4463 } else if (direction === 'prev' && this.currentPosition > 0) {
4464 this.currentPosition--;
4465 }
4466
4467 this.updateTrackPosition();
4468 this.updateButtonStates();
4469 }
4470
4471 updateTrackPosition() {
4472 const offset = this.currentPosition * (this.itemWidth + this.gap);
4473 this.track.style.transform = `translateX(-${offset}px)`;
4474 }
4475
4476 updateButtonStates() {
4477 const maxPosition = Math.max(0, this.items.length - this.visibleItems);
4478
4479 this.prevBtn.disabled = this.currentPosition <= 0;
4480 this.nextBtn.disabled = this.currentPosition >= maxPosition;
4481
4482 // Hide navigation if all items are visible
4483 const shouldHideNav = this.items.length <= this.visibleItems;
4484 this.prevBtn.style.display = shouldHideNav ? 'none' : 'flex';
4485 this.nextBtn.style.display = shouldHideNav ? 'none' : 'flex';
4486 }
4487 }
4488
4489
4490 /**
4491 * Wishlist Handler
4492 * Handles save/remove trip from wishlist functionality
4493 */
4494 class WishlistHandler {
4495 constructor() {
4496 if (!window.yatraTripData?.wishlistEnabled) {
4497 return;
4498 }
4499 this.apiUrl = window.yatraTripData?.apiUrl || '/wp-json/yatra/v1';
4500 this.nonce = window.yatraTripData?.nonce || '';
4501 this.tripId = window.yatraTripData?.tripId || 0;
4502 this.isLoggedIn = window.yatraTripData?.isLoggedIn || false;
4503 this.init();
4504 }
4505
4506 init() {
4507 // Check if user is logged in and update button states
4508 if (this.isLoggedIn && this.tripId) {
4509 this.checkSavedStatus();
4510 }
4511
4512 // Handle wishlist button clicks
4513 document.addEventListener('click', (e) => {
4514 const btn = e.target.closest('.yatra-favorite-btn');
4515 if (btn) {
4516 e.preventDefault();
4517 e.stopPropagation();
4518 this.handleWishlistClick(btn);
4519 }
4520 });
4521 }
4522
4523 async checkSavedStatus() {
4524 if (!this.tripId) return;
4525
4526 try {
4527 const response = await fetch(
4528 `${this.apiUrl}/saved-trips/check/${this.tripId}`,
4529 {
4530 method: 'GET',
4531 headers: {
4532 'X-WP-Nonce': this.nonce
4533 },
4534 credentials: 'same-origin'
4535 }
4536 );
4537
4538 const data = await response.json();
4539 if (data.success && data.data.is_saved) {
4540 this.updateButtonState(true);
4541 }
4542 } catch (error) {
4543 console.error('Error checking wishlist status:', error);
4544 }
4545 }
4546
4547 handleWishlistClick(btn) {
4548 if (!this.isLoggedIn) {
4549 this.showLoginPopup();
4550 return;
4551 }
4552
4553 const btnTripId = parseInt(btn.getAttribute('data-trip-id'), 10) || this.tripId;
4554 if (!btnTripId) {
4555 return;
4556 }
4557
4558 const isSaved = btn.classList.contains('saved');
4559 if (isSaved) {
4560 this.removeFromWishlist(btn, btnTripId);
4561 } else {
4562 this.addToWishlist(btn, btnTripId);
4563 }
4564 }
4565
4566 async addToWishlist(btn, tripId) {
4567 const tid = tripId || this.tripId;
4568 btn.disabled = true;
4569 btn.classList.add('loading');
4570
4571 try {
4572 // application/x-www-form-urlencoded is parsed reliably across hosts; JSON body can be dropped on some stacks.
4573 const params = new URLSearchParams();
4574 params.set('trip_id', String(tid));
4575 const response = await fetch(`${this.apiUrl}/saved-trips`, {
4576 method: 'POST',
4577 headers: {
4578 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
4579 'X-WP-Nonce': this.nonce
4580 },
4581 credentials: 'same-origin',
4582 body: params.toString()
4583 });
4584
4585 const data = await response.json();
4586 if (data.success) {
4587 this.updateButtonState(true, tid);
4588 this.showMessage(data.message || 'Trip saved to wishlist', 'success', btn);
4589 } else {
4590 this.showMessage(data.message || 'Failed to save trip', 'error', btn);
4591 }
4592 } catch (error) {
4593 console.error('Error saving trip:', error);
4594 this.showMessage('An error occurred. Please try again.', 'error', btn);
4595 } finally {
4596 btn.disabled = false;
4597 btn.classList.remove('loading');
4598 }
4599 }
4600
4601 async removeFromWishlist(btn, tripId) {
4602 const tid = tripId || this.tripId;
4603 btn.disabled = true;
4604 btn.classList.add('loading');
4605
4606 try {
4607 const response = await fetch(`${this.apiUrl}/saved-trips/${tid}`, {
4608 method: 'DELETE',
4609 headers: {
4610 'X-WP-Nonce': this.nonce
4611 },
4612 credentials: 'same-origin'
4613 });
4614
4615 const data = await response.json();
4616 if (data.success) {
4617 this.updateButtonState(false, tid);
4618 this.showMessage(data.message || 'Trip removed from wishlist', 'success', btn);
4619 } else {
4620 this.showMessage(data.message || 'Failed to remove trip', 'error', btn);
4621 }
4622 } catch (error) {
4623 console.error('Error removing trip:', error);
4624 this.showMessage('An error occurred. Please try again.', 'error', btn);
4625 } finally {
4626 btn.disabled = false;
4627 btn.classList.remove('loading');
4628 }
4629 }
4630
4631 updateButtonState(isSaved, tripId) {
4632 const tid = tripId || this.tripId;
4633 const selector = tid
4634 ? `.yatra-favorite-btn[data-trip-id="${tid}"]`
4635 : '.yatra-favorite-btn';
4636 const buttons = document.querySelectorAll(selector);
4637 buttons.forEach(btn => {
4638 if (isSaved) {
4639 btn.classList.add('saved', 'is-saved');
4640 btn.setAttribute('aria-label', 'Remove from favorites');
4641 // Fill the heart icon and change color
4642 const svg = btn.querySelector('svg');
4643 if (svg) {
4644 svg.style.fill = 'currentColor';
4645 svg.style.stroke = 'currentColor';
4646 btn.style.color = '#ef4444';
4647 }
4648 } else {
4649 btn.classList.remove('saved', 'is-saved');
4650 btn.setAttribute('aria-label', 'Add to favorites');
4651 // Unfill the heart icon
4652 const svg = btn.querySelector('svg');
4653 if (svg) {
4654 svg.style.fill = 'none';
4655 svg.style.stroke = 'currentColor';
4656 btn.style.color = '';
4657 }
4658 }
4659 });
4660 }
4661
4662 showLoginPopup() {
4663 // Check if enquiry modal exists and reuse its structure, or create a simple login prompt
4664 const enquiryModal = document.getElementById('enquiry-modal');
4665 if (enquiryModal) {
4666 // Show enquiry modal with login message
4667 enquiryModal.classList.add('active');
4668 document.body.style.overflow = 'hidden';
4669
4670 // Update modal content to show login message
4671 const modalContent = enquiryModal.querySelector('.yatra-enquiry-modal-content');
4672 if (modalContent) {
4673 const loginMessage = document.createElement('div');
4674 loginMessage.className = 'yatra-login-prompt';
4675 loginMessage.style.cssText = 'padding: 40px; text-align: center;';
4676 loginMessage.innerHTML = `
4677 <h3 style="margin-bottom: 20px;">Login Required</h3>
4678 <p style="margin-bottom: 30px;">Please login to save trips to your wishlist.</p>
4679 <a href="${window.yatraTripData?.loginUrl || '/wp-login.php'}" class="yatra-btn" style="display: inline-block; margin-right: 10px;">Login</a>
4680 <button type="button" class="yatra-btn-secondary" onclick="this.closest('.yatra-enquiry-modal').classList.remove('active'); document.body.style.overflow = '';">Cancel</button>
4681 `;
4682 modalContent.innerHTML = '';
4683 modalContent.appendChild(loginMessage);
4684 }
4685 } else {
4686 // Fallback: show alert and redirect to login
4687 if (confirm('Please login to save trips to your wishlist. Would you like to go to the login page?')) {
4688 window.location.href = window.yatraTripData?.loginUrl || '/wp-login.php';
4689 }
4690 }
4691 }
4692
4693 showMessage(message, type, button) {
4694 // Remove existing toast if any
4695 const existingToast = document.getElementById('yatra-wishlist-toast');
4696 if (existingToast) {
4697 existingToast.remove();
4698 }
4699
4700 // Create toast near the button
4701 const toast = document.createElement('div');
4702 toast.id = 'yatra-wishlist-toast';
4703 toast.textContent = message;
4704
4705 // Position relative to button
4706 const rect = button.getBoundingClientRect();
4707 const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
4708 const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft;
4709
4710 toast.style.cssText = `
4711 position: absolute;
4712 top: ${rect.bottom + scrollTop + 10}px;
4713 left: ${rect.left + scrollLeft}px;
4714 padding: 10px 16px;
4715 border-radius: 8px;
4716 z-index: 10001;
4717 box-shadow: 0 4px 12px rgba(0,0,0,0.2);
4718 transition: all 0.3s ease;
4719 white-space: nowrap;
4720 font-size: 14px;
4721 pointer-events: none;
4722 background-color: ${type === 'success' ? '#10b981' : '#ef4444'};
4723 color: #ffffff;
4724 opacity: 0;
4725 transform: translateY(-10px);
4726 `;
4727
4728 document.body.appendChild(toast);
4729
4730 // Animate in
4731 requestAnimationFrame(() => {
4732 toast.style.opacity = '1';
4733 toast.style.transform = 'translateY(0)';
4734 });
4735
4736 // Remove after delay
4737 setTimeout(() => {
4738 toast.style.opacity = '0';
4739 toast.style.transform = 'translateY(-10px)';
4740 setTimeout(() => {
4741 if (toast.parentNode) {
4742 toast.remove();
4743 }
4744 }, 300);
4745 }, 3000);
4746 }
4747 }
4748
4749
4750 /**
4751 * Trip Page Orchestrator
4752 * Centralized bootstrapper to keep initialization class-based and organized
4753 */
4754 class TripPage {
4755 constructor() {
4756 this.instances = {};
4757 }
4758
4759 init() {
4760 // Create all feature instances
4761 this.instances.galleryModal = new GalleryModal();
4762 this.instances.itineraryGalleryModal = new ItineraryGalleryModal();
4763 this.instances.heroMediaSwitcher = new HeroMediaSwitcher();
4764 this.instances.bookingSidebar = new BookingSidebar();
4765 this.instances.enquiryModal = new EnquiryModal();
4766 this.instances.stickyNav = new StickyNav();
4767 this.instances.wishlistHandler = new WishlistHandler();
4768 this.instances.availabilitySection = new AvailabilitySection();
4769 this.instances.itinerarySection = new ItinerarySection();
4770 this.instances.reviewForm = new ReviewForm();
4771 this.instances.similarTripsCarousel = new SimilarTripsCarousel();
4772
4773 // Expose for legacy access (existing templates rely on window.*)
4774 this.exposeGlobals();
4775
4776 // Extra page-level behaviors
4777 this.initHeroSmoothScroll();
4778
4779 return this.instances;
4780 }
4781
4782 exposeGlobals() {
4783 Object.entries(this.instances).forEach(([key, instance]) => {
4784 window[key] = instance;
4785 });
4786 // Templates use window.YatraEnquiry.open() — alias to EnquiryModal instance
4787 if (this.instances.enquiryModal) {
4788 window.YatraEnquiry = this.instances.enquiryModal;
4789 }
4790 // Also expose orchestrator for potential future hooks
4791 window.tripPage = this;
4792 }
4793
4794 initHeroSmoothScroll() {
4795 const attachScroll = () => {
4796 document.querySelectorAll('a[href="#yatra-booking-widget"]').forEach((link) => {
4797 link.addEventListener('click', (e) => {
4798 e.preventDefault();
4799 const target = document.getElementById('yatra-booking-widget');
4800 if (target) {
4801 target.scrollIntoView({ behavior: 'smooth', block: 'start' });
4802 }
4803 });
4804 });
4805 };
4806
4807 if (document.readyState === 'loading') {
4808 document.addEventListener('DOMContentLoaded', attachScroll);
4809 } else {
4810 attachScroll();
4811 }
4812 }
4813
4814 static bootstrap() {
4815 const start = () => {
4816 new TripPage().init();
4817 new ItineraryVideoHandler(); // Initialize itinerary video handler
4818 };
4819 if (document.readyState === 'loading') {
4820 document.addEventListener('DOMContentLoaded', start);
4821 } else {
4822 start();
4823 }
4824 }
4825 }
4826
4827 /**
4828 * Itinerary Video Handler Class
4829 * Handles video player functionality for itinerary section
4830 */
4831 class ItineraryVideoHandler {
4832 constructor() {
4833 this.init();
4834 }
4835
4836 init() {
4837 if (document.readyState === 'loading') {
4838 document.addEventListener('DOMContentLoaded', () => this.setup());
4839 } else {
4840 this.setup();
4841 }
4842 }
4843
4844 setup() {
4845 // Handle itinerary video clicks on overlay (both in gallery and separate video section)
4846 const videoOverlays = document.querySelectorAll('.yatra-entry-gallery .yatra-media-overlay, .yatra-entry-video .yatra-media-overlay');
4847 videoOverlays.forEach(overlay => {
4848 overlay.addEventListener('click', (e) => {
4849 e.preventDefault();
4850 e.stopPropagation();
4851 e.stopImmediatePropagation();
4852
4853 const link = overlay.closest('a');
4854 if (!link) return;
4855
4856 const videoUrl = link.getAttribute('href');
4857 if (!videoUrl || videoUrl === '#') return;
4858
4859 const img = overlay.closest('.yatra-media-card').querySelector('img');
4860 const videoTitle = img ? img.alt : 'Itinerary Video';
4861
4862 // Extract video ID from URL
4863 let videoId = '';
4864 if (videoUrl.includes('youtube.com') || videoUrl.includes('youtu.be')) {
4865 // YouTube video ID extraction
4866 const matches = videoUrl.match(/(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/)([^&\n?#]+)/);
4867 videoId = matches ? matches[1] : '';
4868 } else if (videoUrl.includes('vimeo.com')) {
4869 // Vimeo video ID extraction
4870 const matches = videoUrl.match(/vimeo\.com\/(?:.*#|.\/videos\/)?([0-9]+)/);
4871 videoId = matches ? matches[1] : '';
4872 }
4873
4874 // Play video using YatraVideoPlayer
4875 if (window.YatraVideoPlayer) {
4876 const videoData = {
4877 url: videoUrl,
4878 title: videoTitle
4879 };
4880
4881 // Only add video_id for YouTube videos, let player detect URL type for others
4882 if (videoUrl.includes('youtube.com') || videoUrl.includes('youtu.be')) {
4883 videoData.video_id = videoId;
4884 }
4885
4886 window.YatraVideoPlayer.play(videoData);
4887 }
4888 });
4889 });
4890
4891 // Prevent video links from opening in new tab (both in gallery and separate video section)
4892 const videoLinks = document.querySelectorAll('.yatra-entry-gallery .yatra-itinerary-video-link, .yatra-entry-video .yatra-itinerary-video-link');
4893 videoLinks.forEach(link => {
4894 link.addEventListener('click', (e) => {
4895 e.preventDefault();
4896 return false;
4897 });
4898 });
4899 }
4900 }
4901
4902 /**
4903 * Downloads Handler Class
4904 * Handles download section interactions including preview modal
4905 */
4906 class DownloadsHandler {
4907 constructor() {
4908 this.previewModal = null;
4909 this.currentDownloadId = null;
4910 this.init();
4911 }
4912
4913 init() {
4914 if (document.readyState === 'loading') {
4915 document.addEventListener('DOMContentLoaded', () => this.setup());
4916 } else {
4917 this.setup();
4918 }
4919 }
4920
4921 setup() {
4922 this.previewModal = document.getElementById('download-preview-modal');
4923 if (!this.previewModal) return;
4924
4925 this.attachEventListeners();
4926 }
4927
4928 attachEventListeners() {
4929 // Handle download button clicks
4930 document.addEventListener('click', (e) => {
4931 if (e.target.closest('.yatra-download-btn')) {
4932 e.preventDefault();
4933 const btn = e.target.closest('.yatra-download-btn');
4934 const downloadId = btn.dataset.downloadId;
4935 if (downloadId) {
4936 this.downloadFile(downloadId);
4937 }
4938 }
4939
4940 // Handle view/preview link clicks
4941 if (e.target.closest('.yatra-download-view-link, .yatra-download-preview-link')) {
4942 e.preventDefault();
4943 const link = e.target.closest('.yatra-download-view-link, .yatra-download-preview-link');
4944 const downloadId = link.dataset.downloadId;
4945 if (downloadId) {
4946 this.previewFile(downloadId);
4947 }
4948 }
4949
4950 // Handle modal close
4951 if (e.target.closest('.yatra-preview-modal-close, .yatra-preview-close-btn')) {
4952 this.closePreviewModal();
4953 }
4954
4955 // Handle modal overlay click
4956 if (e.target.classList.contains('yatra-preview-modal-overlay')) {
4957 this.closePreviewModal();
4958 }
4959
4960 // Handle preview modal download button
4961 if (e.target.closest('#preview-download-btn')) {
4962 if (this.currentDownloadId) {
4963 this.downloadFile(this.currentDownloadId);
4964 }
4965 }
4966 });
4967
4968 // Handle ESC key to close modal
4969 document.addEventListener('keydown', (e) => {
4970 if (e.key === 'Escape' && this.previewModal.style.display === 'block') {
4971 this.closePreviewModal();
4972 }
4973 });
4974 }
4975
4976 async downloadFile(downloadId) {
4977 try {
4978 // Get download URL with signature using centralized helper
4979 const response = await window.YatraApiHelper.getDownloadInfo(downloadId);
4980 if (!response.ok) {
4981 throw new Error('Download failed');
4982 }
4983
4984 const data = await response.json();
4985
4986 if (data.download_url) {
4987 // Create temporary link and trigger download
4988 const link = document.createElement('a');
4989 link.href = data.download_url;
4990 link.download = ''; // Let server determine filename
4991 document.body.appendChild(link);
4992 link.click();
4993 document.body.removeChild(link);
4994 } else {
4995 throw new Error('Download URL not found');
4996 }
4997 } catch (error) {
4998 console.error('Download error:', error);
4999 // Show error message to user
5000 this.showNotification('Download failed. Please try again.', 'error');
5001 }
5002 }
5003
5004 async previewFile(downloadId) {
5005 try {
5006 this.currentDownloadId = downloadId;
5007 this.showPreviewModal();
5008
5009 // Get download info using centralized helper
5010 const response = await window.YatraApiHelper.getDownloadInfo(downloadId);
5011 if (!response.ok) {
5012 throw new Error('Failed to get file info');
5013 }
5014
5015 const data = await response.json();
5016
5017 if (data.download_url) {
5018 this.loadPreview(data.download_url, data.title || 'Preview');
5019 } else {
5020 throw new Error('File URL not found');
5021 }
5022 } catch (error) {
5023 console.error('Preview error:', error);
5024 this.closePreviewModal();
5025 this.showNotification('Failed to load preview. Please try again.', 'error');
5026 }
5027 }
5028
5029 loadPreview(fileUrl, title) {
5030 const previewContent = document.getElementById('preview-content');
5031 const modalTitle = document.querySelector('.yatra-preview-modal-title');
5032
5033 if (modalTitle) {
5034 modalTitle.textContent = title;
5035 }
5036
5037 if (!previewContent) return;
5038
5039 // Hide loading and show preview
5040 const loading = previewContent.querySelector('.yatra-preview-loading');
5041 if (loading) {
5042 loading.style.display = 'none';
5043 }
5044
5045 // Check file type and load appropriate preview
5046 const fileType = this.getFileType(fileUrl);
5047
5048 if (fileType === 'image') {
5049 previewContent.innerHTML = `<img src="${fileUrl}" alt="${title}" style="max-width: 100%; height: auto;">`;
5050 } else if (fileType === 'pdf') {
5051 previewContent.innerHTML = `
5052 <iframe src="${fileUrl}" style="width: 100%; height: 500px; border: none; border-radius: 8px;"></iframe>
5053 `;
5054 } else {
5055 previewContent.innerHTML = `
5056 <div style="text-align: center; padding: 40px;">
5057 <div style="font-size: 48px; margin-bottom: 16px; opacity: 0.5;">📄</div>
5058 <p style="color: #6b7280; margin-bottom: 16px;">Preview not available for this file type</p>
5059 <p style="color: #9ca3af; font-size: 14px;">Please download the file to view its contents</p>
5060 </div>
5061 `;
5062 }
5063 }
5064
5065 getFileType(url) {
5066 const extension = url.split('.').pop().toLowerCase();
5067 const imageTypes = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'];
5068 const pdfTypes = ['pdf'];
5069
5070 if (imageTypes.includes(extension)) return 'image';
5071 if (pdfTypes.includes(extension)) return 'pdf';
5072 return 'other';
5073 }
5074
5075 showPreviewModal() {
5076 if (this.previewModal) {
5077 this.previewModal.style.display = 'block';
5078 document.body.style.overflow = 'hidden';
5079
5080 // Reset loading state
5081 const previewContent = document.getElementById('preview-content');
5082 if (previewContent) {
5083 previewContent.innerHTML = `
5084 <div class="yatra-preview-loading">
5085 <div class="yatra-preview-spinner"></div>
5086 <p>Loading preview...</p>
5087 </div>
5088 `;
5089 }
5090 }
5091 }
5092
5093 closePreviewModal() {
5094 if (this.previewModal) {
5095 this.previewModal.style.display = 'none';
5096 document.body.style.overflow = '';
5097 this.currentDownloadId = null;
5098 }
5099 }
5100
5101 showNotification(message, type = 'info') {
5102 // Simple notification - you can enhance this with a proper notification system
5103 const notification = document.createElement('div');
5104 notification.style.cssText = `
5105 position: fixed;
5106 top: 20px;
5107 right: 20px;
5108 background: ${type === 'error' ? '#ef4444' : '#3b82f6'};
5109 color: white;
5110 padding: 12px 20px;
5111 border-radius: 8px;
5112 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
5113 z-index: 10000;
5114 font-size: 14px;
5115 font-weight: 500;
5116 `;
5117 notification.textContent = message;
5118
5119 document.body.appendChild(notification);
5120
5121 setTimeout(() => {
5122 notification.style.opacity = '0';
5123 notification.style.transition = 'opacity 0.3s ease';
5124 setTimeout(() => {
5125 document.body.removeChild(notification);
5126 }, 300);
5127 }, 3000);
5128 }
5129 }
5130
5131 // Initialize downloads handler
5132 new DownloadsHandler();
5133
5134 // Kick things off
5135 TripPage.bootstrap();
5136
5137 })();
5138