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

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