/**
* Yatra Single Trip Page JavaScript
* Class-based implementation for trip page features
*/
/* jshint esversion: 6 */
(function() {
'use strict';
/**
* Convert PHP date format (WordPress/Yatra) to Flatpickr format.
* Covers the common tokens exposed in Yatra Settings UI.
*
* @param {string} php
* @returns {string}
*/
const phpDateFormatToFlatpickr = (php) => {
if (!php || typeof php !== 'string') return 'F j, Y';
const map = {
// Day
d: 'd',
j: 'j',
D: 'D',
l: 'l',
// Month
m: 'm',
n: 'n',
M: 'M',
F: 'F',
// Year
Y: 'Y',
y: 'y',
// Time (for any datetime pickers)
H: 'H',
G: 'H',
h: 'h',
g: 'h',
i: 'i',
s: 'S',
a: 'K',
A: 'K'
};
let out = '';
let escaping = false;
for (let i = 0; i < php.length; i++) {
const ch = php[i];
if (escaping) {
out += ch;
escaping = false;
continue;
}
if (ch === '\\') {
escaping = true;
continue;
}
out += map[ch] || ch;
}
return out || 'F j, Y';
};
const getTripAltDateFormat = () => {
const cfg = window.yatraTripData || {};
const php = cfg.dateFormat || cfg.date_format || 'Y-m-d';
return phpDateFormatToFlatpickr(php);
};
const getRestBase = () => {
const siteUrl =
(window.yatraTripData && window.yatraTripData.siteUrl) ||
(window.yatraAdmin && window.yatraAdmin.siteUrl) ||
window.location.origin ||
'';
let base =
(window.yatraTripData && (window.yatraTripData.apiUrl || window.yatraTripData.restUrl)) ||
(window.wpApiSettings && window.wpApiSettings.root) ||
`${siteUrl.replace(/\/$/, '')}/wp-json`;
base = base.replace(/\/$/, '');
// Support plain permalinks via rest_route when pretty permalinks are off
const permalinkStructure =
(window.yatraTripData && window.yatraTripData.permalinkStructure) ||
(window.yatraAdmin && window.yatraAdmin.permalinkStructure) ||
'';
const isPlain = permalinkStructure === 'plain' || !permalinkStructure;
return { base, isPlain };
};
/**
* YYYY-MM from a calendar day; server maps to its locale M-Y to match card data-month.
* @param {string} dateStr YYYY-MM-DD
*/
const monthFilterKeyFromIsoDate = (dateStr) => {
if (!dateStr || typeof dateStr !== 'string') {
return 'all';
}
const m = dateStr.match(/^(\d{4})-(\d{2})-\d{2}/);
if (!m) {
return 'all';
}
return `${m[1]}-${m[2]}`;
};
/**
* Build public REST URL for trip availability HTML (full section or card fragment).
* @param {string|number} tripId
* @param {{date?: string, month_filter?: string, sort?: string, page?: number, per_page?: number, partial?: boolean}} params
*/
const buildAvailabilityTemplateUrl = (tripId, params = {}) => {
const { base: baseUrl, isPlain } = getRestBase();
const q = new URLSearchParams();
if (params.date) {
q.set('date', params.date);
}
const mf = params.month_filter != null && params.month_filter !== '' ? params.month_filter : 'all';
q.set('month_filter', mf);
q.set('sort', params.sort || 'date-asc');
q.set('page', String(Math.max(1, parseInt(params.page, 10) || 1)));
q.set('per_page', String(Math.min(50, Math.max(1, parseInt(params.per_page, 10) || 10))));
if (params.partial) {
q.set('partial', '1');
}
const qs = q.toString();
if (isPlain && baseUrl.includes('rest_route=')) {
return `${baseUrl}/trips/${tripId}/availability-template&${qs}`;
}
if (baseUrl.includes('/yatra/v1')) {
return `${baseUrl}/trips/${tripId}/availability-template?${qs}`;
}
return `${baseUrl}/yatra/v1/trips/${tripId}/availability-template?${qs}`;
};
// Currency formatting helper for trip page (uses localized yatraTripData)
if (typeof window.yatra_format_price_js !== 'function') {
window.yatra_format_price_js = function(amount) {
const cfg = window.yatraTripData || {};
const symbol = (typeof cfg.currencySymbol === 'string' && cfg.currencySymbol.length)
? cfg.currencySymbol
: (typeof cfg.currency === 'string' ? cfg.currency : '$');
const position = (cfg.currencyPosition || 'before').toString();
const decimals = Number.isFinite(Number(cfg.decimalPlaces)) ? Number(cfg.decimalPlaces) : 2;
const thousandSep = (typeof cfg.thousandSeparator === 'string') ? cfg.thousandSeparator : ',';
const decimalSep = (typeof cfg.decimalSeparator === 'string') ? cfg.decimalSeparator : '.';
const num = Number(amount) || 0;
const formatted = new Intl.NumberFormat(undefined, {
minimumFractionDigits: decimals,
maximumFractionDigits: decimals,
}).format(num)
.replace(/,/g, 'TEMP_THOUSAND')
.replace(/\./g, 'TEMP_DECIMAL')
.replace(/TEMP_THOUSAND/g, thousandSep)
.replace(/TEMP_DECIMAL/g, decimalSep);
// Accept common variants: before/left vs after/right
if (position === 'after' || position === 'right') {
return `${formatted} ${symbol}`;
}
return `${symbol}${formatted}`;
};
}
/**
* Resolve the real image URL from an element.
*
* Caching / performance plugins (LiteSpeed Cache, WP Rocket, W3 Total Cache,
* Autoptimize, etc.) replace the `src` attribute with a tiny base64
* placeholder and store the real URL in a data-* attribute. We check our
* own `data-yatra-src` first (added in PHP templates and never touched by
* any third-party plugin), then fall back to the most common lazy-load
* attribute names, and finally fall back to `img.src` while filtering out
* obvious placeholders so we never pass a base64 GIF to the modal.
*/
function getImageSrc(img) {
// 1. Our own reliable attribute – always present on Yatra gallery images.
const yatra = img.getAttribute('data-yatra-src');
if (yatra) return yatra;
// 2. Common lazy-load attributes used by popular caching plugins.
const lazyAttrs = [
'data-src', // LiteSpeed Cache, WP Rocket, Smush, BJ Lazy Load
'data-lazyload', // LiteSpeed Cache (older attribute)
'data-lazy-src', // WordPress core lazy-load
'data-original', // jQuery.lazyLoad plugin
'data-pagespeed-lazy-src', // Google PageSpeed / mod_pagespeed
'data-lazy', // Jetpack
'data-full-size', // Yatra itinerary images
];
for (const attr of lazyAttrs) {
const val = img.getAttribute(attr);
if (val && !val.startsWith('data:')) return val;
}
// 3. Native src – only use if it is not a placeholder.
const src = img.src || '';
if (src && !src.startsWith('data:')) return src;
return '';
}
/**
* Gallery Modal Class
* Handles image gallery modal functionality
*/
class GalleryModal {
constructor() {
this.modal = null;
this.images = [];
this.currentIndex = 0;
this.init();
}
init() {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => this.setup());
} else {
this.setup();
}
}
setup() {
// Wait for DOM to be ready if needed
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => this.initializeGallery());
} else {
this.initializeGallery();
}
}
initializeGallery() {
this.modal = document.getElementById('hero-gallery');
if (!this.modal) {
console.warn('Hero gallery modal not found');
return;
}
this.collectImages();
this.createThumbnails();
this.attachEventListeners();
this.attachItineraryVideoListeners();
}
collectImages() {
this.images = [];
// Helper: deduplicate by resolved URL.
const add = (img, fallbackAlt) => {
const src = getImageSrc(img);
if (!src) return;
if (!this.images.some(i => i.src === src)) {
this.images.push({ src, alt: img.alt || fallbackAlt });
}
};
// 1. Prefer the JSON media-data blob (never touched by caching plugins).
const mediaScript = document.getElementById('yatra-hero-media-data');
if (mediaScript) {
try {
const mediaData = JSON.parse(mediaScript.textContent || '{}');
const imageUrls = Array.isArray(mediaData.images) ? mediaData.images : [];
imageUrls.forEach((url, index) => {
if (url && typeof url === 'string' && !this.images.some(i => i.src === url)) {
this.images.push({ src: url, alt: `Gallery Image ${index + 1}` });
}
});
} catch (e) {
// JSON parse error – fall through to DOM-based collection.
}
}
// 2. Collect from hero slides (covers any images not in the JSON blob).
document.querySelectorAll('.yatra-trip-hero-slide img').forEach((img, index) => {
add(img, `Gallery Image ${index + 1}`);
});
// 3. Collect from gallery section (hero gallery only, NOT itinerary).
document.querySelectorAll('.yatra-trip-gallery .yatra-gallery-item img').forEach((img) => {
add(img, 'Gallery Image');
});
// 4. Collect from hero main image.
const heroMainImg = document.querySelector('.yatra-hero-main-img');
if (heroMainImg) add(heroMainImg, 'Main Image');
// 5. Collect from side images.
document.querySelectorAll('.yatra-side-image-item img').forEach((img) => {
add(img, 'Side Image');
});
}
createThumbnails() {
const container = this.modal.querySelector('.yatra-gallery-thumbnails-container');
if (!container) return;
container.innerHTML = '';
const totalCount = this.modal.querySelector('.yatra-gallery-total-count');
if (totalCount) {
totalCount.textContent = this.images.length;
}
this.images.forEach((img, index) => {
const thumbnail = document.createElement('div');
thumbnail.className = 'yatra-gallery-thumbnail';
thumbnail.setAttribute('data-index', index);
if (index === 0) thumbnail.classList.add('active');
const thumbnailImg = document.createElement('img');
thumbnailImg.src = img.src;
thumbnailImg.alt = img.alt;
thumbnail.appendChild(thumbnailImg);
thumbnail.addEventListener('click', () => this.showImage(index));
container.appendChild(thumbnail);
});
}
showImage(index) {
if (index < 0 || index >= this.images.length) return;
this.currentIndex = index;
const image = this.images[index];
const modalImage = this.modal.querySelector('.yatra-gallery-modal-image');
const modalLoader = this.modal.querySelector('.yatra-gallery-modal-loader');
const currentIndexSpan = this.modal.querySelector('.yatra-gallery-current-index');
const prevBtn = this.modal.querySelector('.yatra-gallery-modal-prev');
const nextBtn = this.modal.querySelector('.yatra-gallery-modal-next');
if (currentIndexSpan) {
currentIndexSpan.textContent = index + 1;
}
// Update thumbnails
const thumbnails = this.modal.querySelectorAll('.yatra-gallery-thumbnail');
thumbnails.forEach((thumb, i) => {
thumb.classList.toggle('active', i === index);
});
// Show loader
if (modalLoader) modalLoader.classList.add('active');
if (modalImage) modalImage.classList.remove('loaded');
// Load image
const img = new Image();
img.onload = () => {
if (modalImage) {
modalImage.src = image.src;
modalImage.alt = image.alt;
modalImage.classList.add('loaded');
}
if (modalLoader) modalLoader.classList.remove('active');
};
img.onerror = () => {
if (modalLoader) modalLoader.classList.remove('active');
if (modalImage) modalImage.src = image.src;
};
img.src = image.src;
// Update button states
if (prevBtn) {
prevBtn.style.opacity = index === 0 ? '0.5' : '1';
prevBtn.style.pointerEvents = index === 0 ? 'none' : 'auto';
}
if (nextBtn) {
nextBtn.style.opacity = index === this.images.length - 1 ? '0.5' : '1';
nextBtn.style.pointerEvents = index === this.images.length - 1 ? 'none' : 'auto';
}
}
open(startIndex = 0) {
if (this.images.length === 0) return;
this.modal.classList.add('active');
document.body.style.overflow = 'hidden';
this.showImage(startIndex);
}
close() {
this.modal.classList.remove('active');
document.body.style.overflow = '';
}
next() {
if (this.currentIndex < this.images.length - 1) {
this.showImage(this.currentIndex + 1);
}
}
prev() {
if (this.currentIndex > 0) {
this.showImage(this.currentIndex - 1);
}
}
attachItineraryVideoListeners() {
// This method is called from setup() but the actual event listeners
// are handled in the inline script in content-itinerary.php
// This is just a placeholder to prevent errors
}
attachEventListeners() {
// Close button
const closeBtn = this.modal.querySelector('.yatra-gallery-modal-close');
if (closeBtn) {
closeBtn.addEventListener('click', () => this.close());
}
// Overlay
const overlay = this.modal.querySelector('.yatra-gallery-modal-overlay');
if (overlay) {
overlay.addEventListener('click', () => this.close());
}
// Navigation buttons
const prevBtn = this.modal.querySelector('.yatra-gallery-modal-prev');
const nextBtn = this.modal.querySelector('.yatra-gallery-modal-next');
if (prevBtn) prevBtn.addEventListener('click', () => this.prev());
if (nextBtn) nextBtn.addEventListener('click', () => this.next());
// Keyboard navigation
document.addEventListener('keydown', (e) => {
if (!this.modal.classList.contains('active')) return;
if (e.key === 'Escape') this.close();
if (e.key === 'ArrowLeft') this.prev();
if (e.key === 'ArrowRight') this.next();
});
// Open from play button
const playButton = document.querySelector('.yatra-hero-gallery-play');
const galleryPlayBtn = document.querySelector('.yatra-gallery-play-btn');
if (playButton) {
playButton.addEventListener('click', () => this.open(0));
}
if (galleryPlayBtn) {
galleryPlayBtn.addEventListener('click', () => this.open(0));
}
// Open from gallery items
const galleryItems = document.querySelectorAll('.yatra-trip-gallery .yatra-gallery-item');
galleryItems.forEach((item, index) => {
item.addEventListener('click', () => {
const img = item.querySelector('img');
if (img) {
const src = getImageSrc(img);
const imageIndex = this.images.findIndex(i => i.src === src);
this.open(imageIndex >= 0 ? imageIndex : index);
}
});
});
// Open from side images
const sideImages = document.querySelectorAll('.yatra-side-image-item');
sideImages.forEach((item) => {
item.addEventListener('click', (e) => {
if (e.target.closest('.yatra-favorite-btn') || e.target.closest('.yatra-view-all-photos-btn')) {
return;
}
const img = item.querySelector('img');
if (img) {
const src = getImageSrc(img);
const imageIndex = this.images.findIndex(i => i.src === src);
this.open(imageIndex >= 0 ? imageIndex : 0);
}
});
});
// Open from main image link
const mainImageLink = document.querySelector('.yatra-hero-main-img-link');
if (mainImageLink) {
mainImageLink.addEventListener('click', (e) => {
// Check if gallery should be prevented (for non-image media types)
if (mainImageLink.getAttribute('data-prevent-gallery') === 'true') {
// Don't prevent default - let the overlay handle the click
return;
}
e.preventDefault();
this.open(0); // Open with featured image as first image
});
}
}
}
/**
* Itinerary Gallery Modal Class
* Separate gallery modal for itinerary section - independent from hero gallery
*/
class ItineraryGalleryModal {
constructor() {
this.modal = null;
this.images = [];
this.currentIndex = 0;
this.init();
}
init() {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => this.setup());
} else {
this.setup();
}
}
setup() {
this.modal = document.getElementById('itinerary-gallery');
if (!this.modal) {
console.warn('Itinerary gallery modal not found');
return;
}
console.log('Itinerary gallery modal found, collecting images...');
this.collectImages();
console.log('Itinerary gallery images collected:', this.images.length);
this.createThumbnails();
this.attachEventListeners();
}
collectImages() {
this.images = [];
const add = (img, fallbackAlt) => {
const src = img.getAttribute('data-full-size') || getImageSrc(img);
if (!src) return;
if (!this.images.some(i => i.src === src)) {
this.images.push({ src, alt: img.alt || fallbackAlt });
}
};
// Collect from itinerary gallery section (hidden gallery)
document.querySelectorAll('.yatra-itinerary-trip-gallery .yatra-gallery-item img').forEach((img) => {
add(img, `Itinerary Image ${this.images.length + 1}`);
});
// Collect from visible itinerary gallery items
document.querySelectorAll('.yatra-entry-gallery .yatra-media-card img').forEach((img) => {
add(img, `Itinerary Gallery Image ${this.images.length + 1}`);
});
// Collect from itinerary video items
document.querySelectorAll('.yatra-entry-video .yatra-media-card img, .yatra-entry-gallery .yatra-itinerary-video-link img').forEach((img) => {
add(img, `Itinerary Video Image ${this.images.length + 1}`);
});
}
createThumbnails() {
const container = this.modal.querySelector('.yatra-gallery-thumbnails-container');
if (!container) return;
container.innerHTML = '';
const totalCount = this.modal.querySelector('.yatra-gallery-total-count');
if (totalCount) {
totalCount.textContent = this.images.length;
}
this.images.forEach((image, index) => {
const thumb = document.createElement('div');
thumb.className = 'yatra-gallery-thumbnail';
thumb.innerHTML = `
`;
thumb.addEventListener('click', () => this.showImage(index));
container.appendChild(thumb);
});
}
showImage(index) {
if (index < 0 || index >= this.images.length) return;
this.currentIndex = index;
const image = this.images[index];
const modalImage = this.modal.querySelector('.yatra-gallery-modal-image');
const modalLoader = this.modal.querySelector('.yatra-gallery-modal-loader');
const currentIndexSpan = this.modal.querySelector('.yatra-gallery-current-index');
const prevBtn = this.modal.querySelector('.yatra-gallery-modal-prev');
const nextBtn = this.modal.querySelector('.yatra-gallery-modal-next');
if (currentIndexSpan) {
currentIndexSpan.textContent = index + 1;
}
// Show loader
if (modalLoader) modalLoader.classList.add('active');
if (modalImage) modalImage.classList.remove('loaded');
// Load image
const img = new Image();
img.onload = () => {
if (modalImage) {
modalImage.src = image.src;
modalImage.alt = image.alt;
modalImage.classList.add('loaded');
}
if (modalLoader) modalLoader.classList.remove('active');
};
img.onerror = () => {
if (modalLoader) modalLoader.classList.remove('active');
if (modalImage) modalImage.src = image.src;
};
img.src = image.src;
// Update button states
if (prevBtn) {
prevBtn.style.opacity = index === 0 ? '0.5' : '1';
prevBtn.style.pointerEvents = index === 0 ? 'none' : 'auto';
}
if (nextBtn) {
nextBtn.style.opacity = index === this.images.length - 1 ? '0.5' : '1';
nextBtn.style.pointerEvents = index === this.images.length - 1 ? 'none' : 'auto';
}
// Update thumbnails
const thumbnails = this.modal.querySelectorAll('.yatra-gallery-thumbnail');
thumbnails.forEach((thumb, i) => {
thumb.classList.toggle('active', i === index);
});
}
open(startIndex = 0) {
if (this.images.length === 0) return;
this.modal.classList.add('active');
document.body.style.overflow = 'hidden';
this.showImage(startIndex);
}
close() {
this.modal.classList.remove('active');
document.body.style.overflow = '';
}
next() {
if (this.currentIndex < this.images.length - 1) {
this.showImage(this.currentIndex + 1);
}
}
prev() {
if (this.currentIndex > 0) {
this.showImage(this.currentIndex - 1);
}
}
attachEventListeners() {
// Close button
const closeBtn = this.modal.querySelector('.yatra-gallery-modal-close');
if (closeBtn) {
closeBtn.addEventListener('click', () => this.close());
}
// Overlay
const overlay = this.modal.querySelector('.yatra-gallery-modal-overlay');
if (overlay) {
overlay.addEventListener('click', () => this.close());
}
// Navigation buttons
const prevBtn = this.modal.querySelector('.yatra-gallery-modal-prev');
const nextBtn = this.modal.querySelector('.yatra-gallery-modal-next');
if (prevBtn) prevBtn.addEventListener('click', () => this.prev());
if (nextBtn) nextBtn.addEventListener('click', () => this.next());
// Keyboard navigation
document.addEventListener('keydown', (e) => {
if (!this.modal.classList.contains('active')) return;
if (e.key === 'Escape') this.close();
if (e.key === 'ArrowLeft') this.prev();
if (e.key === 'ArrowRight') this.next();
});
// Attach click handlers to itinerary gallery links
const galleryLinks = document.querySelectorAll('.yatra-itinerary-gallery-link');
console.log('Attaching listeners to itinerary gallery links:', galleryLinks.length);
galleryLinks.forEach((link) => {
link.addEventListener('click', (e) => {
e.preventDefault();
const imageIndex = parseInt(link.getAttribute('data-image-index') || '0');
console.log('Itinerary gallery link clicked, opening at index:', imageIndex);
this.open(imageIndex);
});
});
// Attach click handlers to itinerary gallery items in hidden section
const galleryItems = document.querySelectorAll('.yatra-itinerary-trip-gallery .yatra-gallery-item');
galleryItems.forEach((item, index) => {
item.addEventListener('click', () => {
const img = item.querySelector('img');
if (img) {
const src = img.getAttribute('data-full-size') || getImageSrc(img);
const imageIndex = this.images.findIndex(i => i.src === src);
this.open(imageIndex >= 0 ? imageIndex : index);
}
});
});
}
}
/**
* Hero Media Switcher Class
* Handles media type switching in the hero section
*/
class HeroMediaSwitcher {
constructor() {
this.currentMediaType = 'images';
this.mediaData = null;
this.init();
}
init() {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => this.setup());
} else {
this.setup();
}
}
setup() {
// Get media data from JSON script
const mediaDataScript = document.getElementById('yatra-hero-media-data');
if (!mediaDataScript) return;
try {
this.mediaData = JSON.parse(mediaDataScript.textContent);
} catch (e) {
console.error('Error parsing media data:', e);
return;
}
this.attachEventListeners();
}
attachEventListeners() {
// Media switcher buttons
const switcherBtns = document.querySelectorAll('.yatra-media-switcher-btn');
switcherBtns.forEach(btn => {
btn.addEventListener('click', (e) => {
const mediaType = e.currentTarget.dataset.media;
this.switchMediaType(mediaType);
});
});
// Media badges click
const mediaBadges = document.querySelectorAll('.yatra-media-badge');
mediaBadges.forEach(badge => {
badge.addEventListener('click', (e) => {
const badgeType = e.currentTarget.classList.contains('yatra-media-badge-image') ? 'images' :
e.currentTarget.classList.contains('yatra-media-badge-video') ? 'videos' :
e.currentTarget.classList.contains('yatra-media-badge-youtube') ? 'youtube' :
e.currentTarget.classList.contains('yatra-media-badge-tour') ? 'tours' : 'documents';
this.switchMediaType(badgeType);
});
});
}
switchMediaType(mediaType) {
if (this.currentMediaType === mediaType) return;
this.currentMediaType = mediaType;
this.updateSwitcherButtons();
this.updateMediaBadgesVisibility();
this.updateMainContent();
this.updateSideContent();
this.updateSideImagesVisibility();
}
updateSwitcherButtons() {
const switcherBtns = document.querySelectorAll('.yatra-media-switcher-btn');
switcherBtns.forEach(btn => {
if (btn.dataset.media === this.currentMediaType) {
btn.classList.add('yatra-media-switcher-active');
} else {
btn.classList.remove('yatra-media-switcher-active');
}
});
}
updateMediaBadgesVisibility() {
const badges = {
image: document.querySelector('.yatra-media-badge-image'),
video: document.querySelector('.yatra-media-badge-video'),
youtube: document.querySelector('.yatra-media-badge-youtube'),
tour: document.querySelector('.yatra-media-badge-tour'),
document: document.querySelector('.yatra-media-badge-document')
};
// Hide all badges first
Object.values(badges).forEach(badge => {
if (badge) badge.style.display = 'none';
});
// Show only the active media type badge
switch (this.currentMediaType) {
case 'images':
if (badges.image) badges.image.style.display = 'inline-flex';
break;
case 'videos':
if (badges.video) badges.video.style.display = 'inline-flex';
break;
case 'youtube':
if (badges.youtube) badges.youtube.style.display = 'inline-flex';
break;
case 'tours':
if (badges.tour) badges.tour.style.display = 'inline-flex';
break;
case 'documents':
if (badges.document) badges.document.style.display = 'inline-flex';
break;
}
}
updateMainContent() {
const mainImageLink = document.querySelector('.yatra-hero-main-img-link');
const mainImage = document.querySelector('.yatra-hero-main-img');
if (!mainImage) return;
switch (this.currentMediaType) {
case 'videos':
this.showVideoContent(mainImageLink, mainImage);
break;
case 'youtube':
this.showYoutubeContent(mainImageLink, mainImage);
break;
case 'tours':
this.showTourContent(mainImageLink, mainImage);
break;
case 'documents':
this.showDocumentContent(mainImageLink, mainImage);
break;
default: // images
this.showImageContent(mainImageLink, mainImage);
}
}
showImageContent(link, img) {
const images = this.mediaData.images || [];
// Remove any existing overlays (YouTube, tour, document, etc.)
const container = img.parentElement;
if (container) {
const existingOverlay = container.querySelector('.yatra-media-overlay');
if (existingOverlay) {
existingOverlay.remove();
}
}
if (images.length > 0) {
img.src = images[0];
img.alt = 'Trip Image 1';
if (link) {
link.href = '#';
link.dataset.imageIndex = '0';
}
}
}
showVideoContent(link, img) {
const videos = this.mediaData.videos || [];
// Remove any existing overlays from other media types
const container = img.parentElement;
if (container) {
const existingOverlay = container.querySelector('.yatra-media-overlay');
if (existingOverlay) {
existingOverlay.remove();
}
}
if (videos.length > 0) {
const video = videos[0];
// Show video thumbnail
img.src = video.thumbnail || img.src; // Keep current image if no thumbnail
img.alt = video.title || 'Trip Video';
// Add play button overlay
this.addPlayButtonOverlay(img.parentElement);
if (link) {
link.href = video.url || '#';
link.dataset.videoUrl = video.url;
link.dataset.videoTitle = video.title || '';
// Prevent link from opening in new tab
link.addEventListener('click', (e) => {
e.preventDefault();
return false;
});
}
}
}
showYoutubeContent(link, img) {
const youtubeVideos = this.mediaData.youtube_videos || [];
if (youtubeVideos.length > 0) {
const video = youtubeVideos[0];
// Prevent link from opening in new tab
if (link) {
link.addEventListener('click', (e) => {
e.preventDefault();
return false;
});
}
// Show YouTube thumbnail
img.src = video.thumbnail || img.src; // Keep current image if no thumbnail
img.alt = video.title || 'YouTube Video';
// Add YouTube play button overlay
this.addYoutubePlayButtonOverlay(img.parentElement, video);
if (link) {
link.href = video.url || '#';
link.dataset.youtubeUrl = video.url;
link.dataset.youtubeVideoId = video.video_id || '';
link.dataset.youtubeTitle = video.title || '';
link.dataset.embedUrl = video.embed_url || '';
}
} else {
console.warn('No YouTube videos available');
}
}
showTourContent(link, img) {
const tours = this.mediaData.virtual_tours || [];
if (tours.length > 0) {
const tour = tours[0];
// Prevent link from opening in new tab
if (link) {
link.addEventListener('click', (e) => {
e.preventDefault();
return false;
});
}
// Show tour thumbnail or keep current image
if (tour.thumbnail) {
img.src = tour.thumbnail;
}
img.alt = tour.title || '360° Virtual Tour';
// Add 360° tour overlay
this.addTourOverlay(img.parentElement, tour);
if (link) {
link.href = tour.url || '#';
link.dataset.tourUrl = tour.url;
link.dataset.tourTitle = tour.title || '';
link.dataset.tourType = tour.tour_type || '360';
link.dataset.isEmbeddable = tour.is_embeddable || 'false';
}
}
}
showDocumentContent(link, img) {
const documents = this.mediaData.documents || [];
if (documents.length > 0) {
const doc = documents[0];
// Keep current image for document display
img.alt = doc.title || 'Trip Document';
// Add document icon overlay
this.addDocumentIconOverlay(img.parentElement, doc);
if (link) {
link.href = doc.url || '#';
link.dataset.documentUrl = doc.url;
link.dataset.documentTitle = doc.title || '';
link.dataset.downloadable = doc.is_downloadable || 'false';
}
}
}
addPlayButtonOverlay(container) {
// Remove existing overlays
const existingOverlay = container.querySelector('.yatra-media-overlay');
if (existingOverlay) existingOverlay.remove();
const overlay = document.createElement('div');
overlay.className = 'yatra-media-overlay yatra-media-play-overlay';
overlay.innerHTML = `
${text}
`; // Scroll message into view messageBox.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); } } /** * Sticky Navigation Class * Handles sticky navigation bar functionality */ class StickyNav { constructor() { this.nav = null; this.sidebar = null; this.navItems = []; this.sections = []; this.scrollThreshold = 200; this.lastScrollTop = 0; this.navHeight = 0; this.init(); } init() { if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => this.setup()); } else { this.setup(); } } setup() { this.nav = document.querySelector('.yatra-sticky-nav'); if (!this.nav) return; this.sidebar = document.querySelector('.yatra-trip-sidebar'); this.navItems = this.nav.querySelectorAll('.yatra-sticky-nav-item'); // Update sections to match actual page hierarchy this.sections = this.getActualSections(); // Calculate nav height this.navHeight = this.nav.offsetHeight || 60; this.attachEventListeners(); this.handleScroll(); // Initial check this.updateActiveNav(); // Initial check this.updateSidebarPosition(); // Initial check } getActualSections() { // Base sections that are always present in order const baseSections = ['overview', 'itinerary', 'included', 'location', 'important-info']; // Conditional sections that may or may not be present const conditionalSections = ['downloads', 'faq', 'trip-story', 'what-makes-special', 'testimonials']; // Combine all sections and filter to only include those that actually exist on the page const allSections = [...baseSections, ...conditionalSections]; return allSections.filter(sectionId => { const element = document.getElementById(sectionId); return element && element.offsetParent !== null; // Check if element exists and is visible }); } attachEventListeners() { // Handle clicks on nav items this.navItems.forEach((item) => { item.addEventListener('click', (e) => { e.preventDefault(); const href = item.getAttribute('href'); if (href && href.startsWith('#')) { const sectionId = href.substring(1); this.scrollToSection(sectionId); // Update active state this.navItems.forEach((nav) => nav.classList.remove('active')); item.classList.add('active'); } }); }); // Handle scroll events window.addEventListener('scroll', () => this.onScroll(), { passive: true }); // Update sidebar position when nav visibility changes const observer = new MutationObserver(() => { this.updateSidebarPosition(); }); observer.observe(this.nav, { attributes: true, attributeFilter: ['class'] }); } updateSidebarPosition() { if (!this.sidebar) return; const isNavVisible = this.nav.classList.contains('visible'); const isAdminBar = document.body.classList.contains('admin-bar'); // Calculate base top position let topPosition = 0; if (isAdminBar) { // Check if mobile admin bar (782px breakpoint) if (window.innerWidth <= 782) { topPosition = 46; // Mobile admin bar } else { topPosition = 32; // Desktop admin bar } } // Add sticky nav height if visible if (isNavVisible) { topPosition += this.navHeight; } // Apply to sidebar this.sidebar.style.top = topPosition + 'px'; } scrollToAvailability(section) { if (!section) return; const offset = 100; // Account for sticky header const elementPosition = section.getBoundingClientRect().top; const offsetPosition = elementPosition + window.pageYOffset - offset; window.scrollTo({ top: offsetPosition, behavior: 'smooth' }); } scrollToSection(sectionId) { const section = document.getElementById(sectionId); if (!section) { console.warn(`Section with ID "${sectionId}" not found`); return; } // Calculate offset - account for sticky nav and admin bar let offset = 100; // Base offset const isNavVisible = this.nav.classList.contains('visible'); if (isNavVisible) { offset += this.navHeight; } // Check for WordPress admin bar const adminBar = document.getElementById('wpadminbar'); if (adminBar) { // Check if mobile admin bar (782px breakpoint) if (window.innerWidth <= 782) { offset += 46; // Mobile admin bar } else { offset += 32; // Desktop admin bar } } const elementPosition = section.getBoundingClientRect().top; const offsetPosition = elementPosition + window.pageYOffset - offset; window.scrollTo({ top: offsetPosition, behavior: 'smooth' }); } autoSelectDateCard(selectedDate) { if (!selectedDate) return; // Wait a bit for the availability section to render setTimeout(() => { const availabilityCards = document.querySelectorAll('.yatra-availability-card'); if (!availabilityCards.length) return; let matchingCard = null; let nearestCard = null; let minDiff = Infinity; const selectedTime = new Date(selectedDate).getTime(); // Find exact match or nearest date availabilityCards.forEach(card => { const cardDate = card.dataset.date; if (!cardDate) return; if (cardDate === selectedDate) { matchingCard = card; } else { const cardTime = new Date(cardDate).getTime(); const diff = Math.abs(cardTime - selectedTime); if (diff < minDiff) { minDiff = diff; nearestCard = card; } } }); // Use exact match if found, otherwise use nearest const cardToOpen = matchingCard || nearestCard; if (cardToOpen) { // Open the card const toggle = cardToOpen.querySelector('.yatra-availability-toggle'); if (toggle && !cardToOpen.classList.contains('open')) { toggle.click(); } // Scroll to the card setTimeout(() => { const offset = 150; const elementPosition = cardToOpen.getBoundingClientRect().top; const offsetPosition = elementPosition + window.pageYOffset - offset; window.scrollTo({ top: offsetPosition, behavior: 'smooth' }); }, 300); } }, 500); } handleScroll() { const scrollTop = window.pageYOffset || document.documentElement.scrollTop; // Show nav when scrolling down past threshold if (scrollTop > this.scrollThreshold) { this.nav.classList.add('visible'); } else { this.nav.classList.remove('visible'); } this.lastScrollTop = scrollTop; } updateActiveNav() { const scrollPos = window.scrollY + 150; let activeSectionId = null; // Find which section is currently in view this.sections.forEach((sectionId) => { const section = document.getElementById(sectionId); if (section) { const sectionTop = section.offsetTop; const sectionBottom = sectionTop + section.offsetHeight; if (scrollPos >= sectionTop && scrollPos < sectionBottom) { activeSectionId = sectionId; } } }); // Update active state based on section ID (not index) if (activeSectionId) { this.navItems.forEach((nav) => { nav.classList.remove('active'); const href = nav.getAttribute('href'); if (href === '#' + activeSectionId) { nav.classList.add('active'); } }); } } onScroll() { this.handleScroll(); this.updateActiveNav(); this.updateSidebarPosition(); } } /** * Availability Section Class * Handles availability section interactions (filters, load more) */ class AvailabilitySection { constructor() { this.section = null; this.filterButtons = []; this.availabilityItems = []; this.currentMonthFilter = 'all'; this.currentSortKey = 'date-asc'; this.init(); } init() { if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => this.setup()); } else { this.setup(); } } setup() { this.section = document.getElementById('availability'); if (!this.section) return; const filterBar = this.section.querySelector('.yatra-availability-filters'); this.filterButtons = filterBar ? filterBar.querySelectorAll('.yatra-availability-filter-btn') : this.section.querySelectorAll('.yatra-availability-filter-btn'); this.availabilityItems = this.section.querySelectorAll('.yatra-availability-card'); const sortSelect = this.section.querySelector('#availability-sort'); if (sortSelect && sortSelect.value) { this.currentSortKey = sortSelect.value; } this.attachEventListeners(); const listEl = this.section.querySelector('.yatra-availability-list'); if (listEl && listEl.dataset.monthFilter) { this.currentMonthFilter = listEl.dataset.monthFilter; } else { const activeFilterBtn = filterBar ? filterBar.querySelector('.yatra-availability-filter-btn.active') : this.section.querySelector('.yatra-availability-filter-btn.active'); if (activeFilterBtn) { this.currentMonthFilter = activeFilterBtn.getAttribute('data-filter') || 'all'; } } if (listEl && listEl.dataset.sort) { this.currentSortKey = listEl.dataset.sort; } // Scroll to selected card if exists this.scrollToSelectedCard(); } scrollToSelectedCard() { // Find the selected card const selectedCard = this.section.querySelector('.yatra-availability-card.selected'); if (selectedCard) { // Small delay to ensure rendering is complete setTimeout(() => { selectedCard.scrollIntoView({ behavior: 'smooth', block: 'center' }); }, 300); } } /** * Single source of truth for availability "Book Now" (Pro: yatraBeforeBooking / additional services popup). * Previously attachBookNowHandlers() used a legacy session POST and skipped the hook — inconsistent by date after AJAX refresh. */ handleAvailabilityBookNowClick(btn) { const self = this; const tripId = btn.getAttribute('data-trip-id') || window.yatraTripData?.tripId; const date = btn.getAttribute('data-date'); const itemIndex = btn.getAttribute('data-item'); const availabilityId = btn.getAttribute('data-availability-id') || itemIndex; const card = btn.closest('.yatra-availability-card'); let departureTime = ''; if (card) { const timeElement = card.querySelector('.yatra-card-header-date'); if (timeElement) { const timeText = timeElement.textContent?.trim(); if (timeText && /^\d{1,2}:\d{2}\s*(AM|PM)?$/i.test(timeText)) { departureTime = timeText; } } } let travelers = 0; const travelerCounts = {}; const categoryInputs = card?.querySelectorAll('.yatra-availability-category[data-item="' + itemIndex + '"]'); if (categoryInputs && categoryInputs.length > 0) { categoryInputs.forEach((input) => { const categoryId = input.getAttribute('data-category'); const count = parseInt(input.value) || 0; if (categoryId && count > 0) { travelerCounts[categoryId] = count; travelers += count; } }); } else { const simpleInput = card?.querySelector('.yatra-availability-num-travelers[data-item="' + itemIndex + '"]'); if (simpleInput) { travelers = parseInt(simpleInput.value) || 1; } else { const adults = this.getTravelerCount(itemIndex, 'adults'); const children = this.getTravelerCount(itemIndex, 'children'); travelers = parseInt(adults) + parseInt(children); } } if (travelers < 1) { this.showBookingError(btn, 'Please select at least 1 traveler to continue.'); return; } const originalText = btn.innerHTML; btn.innerHTML = ' Processing...'; btn.disabled = true; if (!tripId || isNaN(parseInt(tripId)) || parseInt(tripId) <= 0) { console.error('Invalid trip ID:', tripId); btn.innerHTML = originalText; btn.disabled = false; this.showBookingError(btn, 'Unable to process booking. Invalid trip data.'); return; } const pricingType = btn.getAttribute('data-pricing-type') || 'regular'; const isDayTrip = btn.getAttribute('data-is-day-trip') === '1'; let priceTypesJson = btn.getAttribute('data-price-types') || ''; let priceTypes = []; try { if (priceTypesJson) { priceTypes = JSON.parse(priceTypesJson); } } catch (e) { console.error('Error parsing price types:', e); } const btnDepartureTime = btn.getAttribute('data-departure-time') || ''; if (btnDepartureTime) { departureTime = btnDepartureTime; } const sessionPayload = { trip_id: parseInt(tripId, 10), travelers: travelers, travel_date: date || '', departure_time: departureTime, availability_id: availabilityId, pricing_type: pricingType, price_types: priceTypes, is_day_trip: isDayTrip, }; if (Object.keys(travelerCounts).length > 0) { sessionPayload.traveler_counts = travelerCounts; } if (typeof window.yatraBeforeBooking === 'function') { Promise.resolve(window.yatraBeforeBooking({ btn: btn, originalText: originalText, sessionPayload: sessionPayload, proceedToBooking: () => self.proceedToBooking(btn, originalText, sessionPayload) })).then(handled => { if (!handled) { self.proceedToBooking(btn, originalText, sessionPayload); } }).catch(error => { console.error('Error in yatraBeforeBooking hook:', error); self.proceedToBooking(btn, originalText, sessionPayload); }); return; } this.proceedToBooking(btn, originalText, sessionPayload); } attachEventListeners() { const filterBar = this.section.querySelector('.yatra-availability-filters'); // Month filter buttons (server-side filter; buttons live only in .yatra-availability-filters) this.filterButtons.forEach((btn) => { btn.addEventListener('click', () => { const filter = btn.getAttribute('data-filter') || 'all'; this.currentMonthFilter = filter; this.filterButtons.forEach((b) => b.classList.remove('active')); btn.classList.add('active'); this.reloadAvailabilityFromServer({ month_filter: filter, page: 1, sort: this.currentSortKey, }); }); }); const showAllCta = this.section.querySelector('.yatra-availability-show-all-dates'); if (showAllCta && filterBar) { showAllCta.addEventListener('click', (e) => { e.preventDefault(); const allPill = filterBar.querySelector('.yatra-availability-filter-btn[data-filter="all"]'); if (allPill) { allPill.click(); } }); } // Sort dropdown (server-side sort) const sortSelect = document.getElementById('availability-sort'); if (sortSelect) { sortSelect.addEventListener('change', (e) => { const value = e && e.target ? e.target.value : ''; this.currentSortKey = value || 'date-asc'; this.reloadAvailabilityFromServer({ month_filter: this.currentMonthFilter, page: 1, sort: this.currentSortKey, selectEl: sortSelect, }); }); } this.setupToggleHandlers(); // Traveler selector buttons this.initTravelerSelectors(); // Book Now buttons - set session and redirect to booking page const bookButtons = this.section.querySelectorAll('.yatra-card-book-btn'); bookButtons.forEach((btn) => { btn.addEventListener('click', (e) => { e.preventDefault(); this.handleAvailabilityBookNowClick(btn); }); }); // Load more button const loadMoreBtn = this.section.querySelector('.yatra-availability-load-more-btn'); if (loadMoreBtn) { loadMoreBtn.addEventListener('click', () => { this.handleLoadMore(); }); } } /** * Replace availability section from API (sort / month filter / reset page). * @param {{ month_filter?: string, page?: number, sort?: string, per_page?: number, selectEl?: HTMLSelectElement }} opts */ reloadAvailabilityFromServer(opts = {}) { const tripId = window.yatraTripData?.tripId || this.section?.getAttribute('data-trip-id') || 0; if (!tripId) { if (opts.sort) { this.sortItems(opts.sort); } return Promise.resolve(); } const listEl = this.section?.querySelector('.yatra-availability-list'); const monthFilter = opts.month_filter != null ? opts.month_filter : (listEl?.dataset?.monthFilter || this.currentMonthFilter || 'all'); const sortKey = opts.sort || listEl?.dataset?.sort || this.currentSortKey || 'date-asc'; const page = opts.page != null ? opts.page : 1; const perPage = opts.per_page != null ? opts.per_page : (parseInt(listEl?.dataset?.perPage, 10) || 10); const selectEl = opts.selectEl; const travelDateInput = document.getElementById('travel_date'); const travelDate = travelDateInput && travelDateInput.value ? travelDateInput.value : undefined; const prevDisabled = !!(selectEl && selectEl.disabled); if (selectEl) { selectEl.disabled = true; } const url = buildAvailabilityTemplateUrl(tripId, { date: travelDate, month_filter: monthFilter, sort: sortKey, page, per_page: perPage, }); return fetch(url, { method: 'GET', headers: { 'Content-Type': 'application/json', }, credentials: 'omit', }) .then((resp) => resp.json()) .then((data) => { const html = data?.html || data?.data?.html; if (!html) { throw new Error('No HTML returned'); } const tempDiv = document.createElement('div'); tempDiv.innerHTML = html; const newSection = tempDiv.querySelector('#availability') || tempDiv.firstElementChild; if (!newSection) { throw new Error('Invalid HTML'); } const oldSection = document.getElementById('availability'); if (oldSection) { oldSection.replaceWith(newSection); } this.currentMonthFilter = monthFilter; this.currentSortKey = sortKey; this.setup(); }) .catch((err) => { console.error('[Yatra] Availability reload failed:', err); if (opts.sort) { this.sortItems(opts.sort); } }) .finally(() => { if (selectEl) { selectEl.disabled = prevDisabled; } }); } /** * Proceed to booking - creates session and redirects */ proceedToBooking(btn, originalText, sessionPayload) { // Track Book Now click as InitiateCheckout (Facebook Pixel) if (typeof fbq !== 'undefined' && window.yatraAdmin?.facebookPixel?.connectionStatus?.pixelConnected) { const tripData = this.getTripData(); const totalPrice = this.calculateTotalPrice(); fbq('track', 'InitiateCheckout', { content_name: tripData.title || 'Trip', content_ids: [sessionPayload.trip_id], content_type: 'product', value: totalPrice, currency: window.yatraBookingData?.currency || 'USD', num_items: sessionPayload.travelers || 1 }); console.log('[Facebook Pixel] InitiateCheckout tracked for trip:', sessionPayload.trip_id); } // Track Book Now click as begin_checkout (Google Analytics 4) if (typeof gtag !== 'undefined' && window.yatraAdmin?.googleAnalytics?.measurementId) { const tripData = this.getTripData(); const totalPrice = this.calculateTotalPrice(); gtag('event', 'begin_checkout', { currency: window.yatraBookingData?.currency || 'USD', value: totalPrice, items: [{ item_id: sessionPayload.trip_id, item_name: tripData.title || 'Trip', category: 'Travel', price: totalPrice / (sessionPayload.travelers || 1), quantity: sessionPayload.travelers || 1 }] }); console.log('[Google Analytics] begin_checkout tracked for trip:', sessionPayload.trip_id); } // Set booking session via REST API // credentials: 'same-origin' is required to send cookies for session const { base: baseUrl, isPlain } = getRestBase(); let sessionUrl; if (isPlain && !baseUrl.includes('/yatra/v1')) { sessionUrl = `${baseUrl}/?rest_route=/yatra/v1/booking/session`; } else if (baseUrl.includes('/yatra/v1')) { sessionUrl = `${baseUrl}/booking/session`; } else { sessionUrl = `${baseUrl}/yatra/v1/booking/session`; } fetch(sessionUrl, { method: 'POST', credentials: 'same-origin', // needed for PHP session cookie headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(sessionPayload) }) .then(response => response.json()) .then(data => { if (data.success && data.redirect_url) { // Success - redirect to booking page window.location.href = data.redirect_url; } else { // Error - show message and reset button console.error('Booking session error:', data); btn.innerHTML = originalText; btn.disabled = false; this.showBookingError(btn, data.message || 'Unable to process booking. Please try again.'); } }) .catch(error => { console.error('Error setting booking session:', error); btn.innerHTML = originalText; btn.disabled = false; this.showBookingError(btn, 'Unable to process booking. Please try again.'); }); } initTravelerSelectors() { // Initialize traveler-based pricing selectors (dropdowns with categories) const participantsSelects = this.section.querySelectorAll('.yatra-availability-participants'); participantsSelects.forEach((select) => { const itemIndex = select.getAttribute('data-item'); const display = select.querySelector('.yatra-availability-participants-display'); const quantitySelector = select.querySelector('.yatra-availability-quantity-selector'); const categoryInputs = select.querySelectorAll('.yatra-availability-category'); if (!display || !quantitySelector || categoryInputs.length === 0) return; // Make the whole select area clickable select.style.cursor = 'pointer'; // Toggle dropdown on select click select.addEventListener('click', (e) => { // Don't toggle if clicking on buttons or inputs if (e.target.closest('.yatra-quantity-btn') || e.target.closest('.yatra-quantity-input')) return; e.stopPropagation(); select.classList.toggle('active'); }); // Update display text based on category inputs const updateDisplay = () => { const parts = []; categoryInputs.forEach((input) => { const value = parseInt(input.value) || 0; if (value > 0) { const row = input.closest('.yatra-quantity-row'); const categoryLabel = row ? row.querySelector('.yatra-quantity-title')?.textContent : 'Traveler'; parts.push(`${categoryLabel} x ${value}`); } }); if (parts.length === 0) { // Default to first category const firstRow = categoryInputs[0]?.closest('.yatra-quantity-row'); const firstLabel = firstRow ? firstRow.querySelector('.yatra-quantity-title')?.textContent : 'Traveler'; display.textContent = `${firstLabel} x 1`; } else { display.textContent = parts.join(', '); } }; // Quantity button handlers const quantityButtons = select.querySelectorAll('.yatra-quantity-btn'); quantityButtons.forEach((btn) => { btn.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); const targetId = btn.getAttribute('data-target'); // Find the input in the same row const row = btn.closest('.yatra-quantity-row'); const input = row ? row.querySelector('.yatra-quantity-input') : null; if (!input) return; const isMinus = btn.classList.contains('yatra-quantity-minus'); const isPlus = btn.classList.contains('yatra-quantity-plus'); let current = parseInt(input.value) || 0; const min = parseInt(input.getAttribute('min')) || 0; const max = parseInt(input.getAttribute('max')) || 999; let newValue = current; if (isPlus && current < max) { newValue = current + 1; } else if (isMinus && current > min) { newValue = current - 1; } if (newValue !== current) { input.value = newValue; updateDisplay(); // Update button states const minusBtn = row.querySelector('.yatra-quantity-minus'); const plusBtn = row.querySelector('.yatra-quantity-plus'); if (minusBtn) minusBtn.disabled = newValue <= min; if (plusBtn) plusBtn.disabled = newValue >= max; // Update total price this.updateTotalPrice(itemIndex); // Sync with sidebar if exists this.syncSidebarTravelers(categoryInputs); } }); }); // Close dropdown when clicking outside document.addEventListener('click', (e) => { if (!select.contains(e.target)) { select.classList.remove('active'); } }); // Initialize display updateDisplay(); // Initialize button states categoryInputs.forEach((input) => { const value = parseInt(input.value || 0); const min = parseInt(input.getAttribute('min') || 0); const max = parseInt(input.getAttribute('max') || 999); const row = input.closest('.yatra-quantity-row'); if (row) { const minusBtn = row.querySelector('.yatra-quantity-minus'); const plusBtn = row.querySelector('.yatra-quantity-plus'); if (minusBtn) minusBtn.disabled = value <= min; if (plusBtn) plusBtn.disabled = value >= max; } }); // Initialize total price for this card this.updateTotalPrice(itemIndex); }); // Initialize regular pricing selectors (simple number inputs) const simpleTravelerInputs = this.section.querySelectorAll('.yatra-availability-travelers-simple'); simpleTravelerInputs.forEach((container) => { const itemIndex = container.getAttribute('data-item'); const input = container.querySelector('.yatra-availability-num-travelers'); const quantityButtons = container.querySelectorAll('.yatra-quantity-btn'); if (!input) return; quantityButtons.forEach((btn) => { btn.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); const isMinus = btn.classList.contains('yatra-quantity-minus'); const isPlus = btn.classList.contains('yatra-quantity-plus'); let current = parseInt(input.value) || 1; const min = parseInt(input.getAttribute('min')) || 1; const max = parseInt(input.getAttribute('max')) || 999; let newValue = current; if (isPlus && current < max) { newValue = current + 1; } else if (isMinus && current > min) { newValue = current - 1; } if (newValue !== current) { input.value = newValue; // Update button states const minusBtn = container.querySelector('.yatra-quantity-minus'); const plusBtn = container.querySelector('.yatra-quantity-plus'); if (minusBtn) minusBtn.disabled = newValue <= min; if (plusBtn) plusBtn.disabled = newValue >= max; // Update total price this.updateTotalPrice(itemIndex); } }); }); // Initialize total price for this card this.updateTotalPrice(itemIndex); }); } setupToggleHandlers() { if (!this.availabilityItems || !this.availabilityItems.length) return; this.availabilityItems.forEach((item) => { const toggle = item.querySelector('.yatra-availability-toggle'); if (!toggle) return; toggle.addEventListener('click', (e) => { e.preventDefault(); const isOpen = item.classList.contains('open'); this.availabilityItems.forEach((other) => other.classList.remove('open')); if (!isOpen) { item.classList.add('open'); } }); }); } getTravelerCount(itemIndex, type) { const select = this.section.querySelector( `.yatra-availability-participants[data-item="${itemIndex}"]` ); if (!select) return type === 'adults' ? 1 : 0; const input = select.querySelector( type === 'adults' ? '.yatra-availability-adults' : '.yatra-availability-children' ); if (!input) return type === 'adults' ? 1 : 0; const value = parseInt(input.value) || 0; return type === 'adults' && value === 0 ? 1 : value; } syncSidebarTravelers(categoryInputs) { // Sync availability card travelers to sidebar const sidebarDisplay = document.querySelector('.yatra-booking-card .yatra-participants-display'); if (!sidebarDisplay) return; const sidebarSelector = document.querySelector('.yatra-booking-card .yatra-booking-quantity-selector'); if (!sidebarSelector) return; // Build display text from category inputs const parts = []; categoryInputs.forEach((input) => { const value = parseInt(input.value) || 0; if (value > 0) { const row = input.closest('.yatra-quantity-row'); const categoryLabel = row ? row.querySelector('.yatra-quantity-title')?.textContent : 'Traveler'; parts.push(`${categoryLabel} x ${value}`); // Also update sidebar inputs if they exist const categoryId = input.getAttribute('data-category'); const sidebarInput = sidebarSelector.querySelector(`input[id="traveler_${categoryId}"]`); if (sidebarInput) { sidebarInput.value = value; // Update button states const sidebarRow = sidebarInput.closest('.yatra-quantity-row'); if (sidebarRow) { const min = parseInt(sidebarInput.getAttribute('min') || 0); const max = parseInt(sidebarInput.getAttribute('max') || 999); const minusBtn = sidebarRow.querySelector('.yatra-quantity-minus'); const plusBtn = sidebarRow.querySelector('.yatra-quantity-plus'); if (minusBtn) minusBtn.disabled = value <= min; if (plusBtn) plusBtn.disabled = value >= max; } } } }); // Update sidebar display text if (parts.length > 0) { sidebarDisplay.textContent = parts.join(', '); } } updateTotalPrice(itemIndex) { const totalAmountElement = this.section.querySelector( `.yatra-card-total-amount[data-item="${itemIndex}"]` ); if (!totalAmountElement) return; // Check for traveler-based pricing first const categoryInputs = this.section.querySelectorAll(`.yatra-availability-category[data-item="${itemIndex}"]`); let totalPrice = 0; let totalTravelers = 0; if (categoryInputs.length > 0) { // Traveler-based pricing: sum up each category's price * quantity (respecting pricing mode) categoryInputs.forEach((input) => { const quantity = parseInt(input.value) || 0; const row = input.closest('.yatra-quantity-row'); const price = row ? parseFloat(row.getAttribute('data-price')) || 0 : 0; const pricingMode = (row ? row.getAttribute('data-pricing-mode') : null) || input.getAttribute('data-pricing-mode') || 'per_person'; if (pricingMode === 'per_group') { if (quantity > 0) { totalPrice += price; } } else { totalPrice += quantity * price; } totalTravelers += quantity; }); } else { // Regular pricing: simple number of travelers const numTravelersInput = this.section.querySelector(`.yatra-availability-num-travelers[data-item="${itemIndex}"]`); if (numTravelersInput) { totalTravelers = parseInt(numTravelersInput.value) || 1; const basePrice = parseFloat(numTravelersInput.getAttribute('data-price')) || parseFloat(totalAmountElement.getAttribute('data-base-price')) || 0; totalPrice = totalTravelers * basePrice; } } // Apply group discount if applicable const groupDiscountResult = this.calculateGroupDiscount(totalPrice, totalTravelers); const finalPrice = groupDiscountResult.finalPrice; const groupDiscountApplied = groupDiscountResult.discountApplied; const groupDiscountAmount = groupDiscountResult.discountAmount; // Format price with shared formatter (uses yatraTripData settings) if (typeof yatra_format_price_js === 'function') { totalAmountElement.textContent = yatra_format_price_js(finalPrice); } else { totalAmountElement.textContent = String(finalPrice.toFixed(2)); } // Update the note text const noteElement = this.section.querySelector( `.yatra-card-total-note[data-item="${itemIndex}"]` ); if (noteElement) { let travelerText = totalTravelers === 1 ? 'for 1 traveler' : `for ${totalTravelers} travelers`; // Add group discount info if applied if (groupDiscountApplied && groupDiscountAmount > 0) { const discountText = (typeof yatra_format_price_js === 'function') ? yatra_format_price_js(groupDiscountAmount) : String(groupDiscountAmount.toFixed(2)); travelerText += ` (Group discount: -${discountText})`; } noteElement.textContent = travelerText; } // Show/hide group discount applied badge this.updateGroupDiscountBadge(itemIndex, groupDiscountApplied, groupDiscountResult); } /** * Calculate group discount based on total travelers */ calculateGroupDiscount(totalPrice, totalTravelers) { const result = { finalPrice: totalPrice, discountApplied: false, discountAmount: 0, discountPercentage: 0, discountType: null }; // Get group discounts from the booking card data attribute const bookingCard = document.querySelector('.yatra-booking-card'); if (!bookingCard) return result; const groupDiscountsData = bookingCard.getAttribute('data-group-discounts'); if (!groupDiscountsData) return result; let groupDiscounts; try { groupDiscounts = JSON.parse(groupDiscountsData); } catch (e) { console.error('Failed to parse group discounts:', e); return result; } if (!Array.isArray(groupDiscounts) || groupDiscounts.length === 0) return result; // Find the applicable discount based on total travelers let applicableDiscount = null; for (const discount of groupDiscounts) { // Check if discount has ranges (new format) if (discount.group_discount_ranges && discount.group_discount_ranges.length > 0) { for (const range of discount.group_discount_ranges) { const minSize = parseInt(range.min_group_size) || 0; const maxSize = range.max_group_size ? parseInt(range.max_group_size) : Infinity; if (totalTravelers >= minSize && totalTravelers <= maxSize) { applicableDiscount = { type: range.discount_type || 'percentage', amount: parseFloat(range.discount_amount) || 0 }; break; } } } else { // Legacy format: single min_group_size const minSize = parseInt(discount.min_group_size) || 0; const maxSize = discount.max_group_size ? parseInt(discount.max_group_size) : Infinity; if (totalTravelers >= minSize && totalTravelers <= maxSize) { applicableDiscount = { type: discount.discount_type || 'percentage', amount: parseFloat(discount.discount_amount) || 0 }; } } if (applicableDiscount) break; } // Apply the discount if found if (applicableDiscount && applicableDiscount.amount > 0) { result.discountApplied = true; result.discountType = applicableDiscount.type; if (applicableDiscount.type === 'percentage') { result.discountPercentage = applicableDiscount.amount; result.discountAmount = totalPrice * (applicableDiscount.amount / 100); } else { // Fixed amount discount result.discountAmount = applicableDiscount.amount; } result.finalPrice = Math.max(0, totalPrice - result.discountAmount); } return result; } /** * Update group discount badge visibility */ updateGroupDiscountBadge(itemIndex, discountApplied, discountResult) { // Find or create the group discount applied badge for this item const availabilityItem = this.section.querySelector(`.yatra-availability-item[data-item="${itemIndex}"]`); if (!availabilityItem) return; let badge = availabilityItem.querySelector('.yatra-group-discount-applied'); if (discountApplied && discountResult.discountAmount > 0) { if (!badge) { badge = document.createElement('div'); badge.className = 'yatra-group-discount-applied'; 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;'; // Insert after the total amount const totalSection = availabilityItem.querySelector('.yatra-card-total'); if (totalSection) { totalSection.appendChild(badge); } } // Update badge content const discountText = discountResult.discountType === 'percentage' ? `${discountResult.discountPercentage}% group discount applied!` : `Group discount applied!`; badge.innerHTML = ` ${discountText}`; } else if (badge) { badge.remove(); } } filterByMonth(filter) { this.availabilityItems.forEach((item) => { if (filter === 'all') { item.style.display = 'block'; } else { const itemMonth = item.getAttribute('data-month'); if (itemMonth === filter) { item.style.display = 'block'; } else { item.style.display = 'none'; } } }); } sortItems(sortBy) { const items = Array.from(this.availabilityItems); const list = this.section.querySelector('.yatra-availability-list'); if (!list) return; items.sort((a, b) => { switch (sortBy) { case 'date-asc': return new Date(a.getAttribute('data-date')) - new Date(b.getAttribute('data-date')); case 'date-desc': return new Date(b.getAttribute('data-date')) - new Date(a.getAttribute('data-date')); case 'price-asc': return parseFloat(a.getAttribute('data-price')) - parseFloat(b.getAttribute('data-price')); case 'price-desc': return parseFloat(b.getAttribute('data-price')) - parseFloat(a.getAttribute('data-price')); case 'seats-desc': return parseInt(b.getAttribute('data-seats')) - parseInt(a.getAttribute('data-seats')); default: return 0; } }); // Re-append sorted items items.forEach((item) => { list.appendChild(item); }); } handleBookNow(date, price, button, adults = 1, children = 0) { // Get travelers from the availability card if not provided const itemIndex = button.getAttribute('data-item'); if (itemIndex) { adults = this.getTravelerCount(itemIndex, 'adults'); children = this.getTravelerCount(itemIndex, 'children'); } // Update the main booking form with selected date const dateInput = document.getElementById('travel_date'); if (dateInput) { // Format date for display (YYYY-MM-DD) dateInput.value = date; // Trigger change event if using Flatpickr if (dateInput._flatpickr) { dateInput._flatpickr.setDate(date, true); } } // Update travelers in booking sidebar if (window.bookingSidebar) { window.bookingSidebar.setTravelers(adults, children); } // Scroll to booking sidebar const bookingSidebar = document.getElementById('booking'); if (bookingSidebar) { bookingSidebar.scrollIntoView({ behavior: 'smooth', block: 'start' }); } // Show success feedback const originalText = button.textContent; button.textContent = 'Selected!'; button.style.background = '#10b981'; setTimeout(() => { button.textContent = originalText; button.style.background = ''; }, 2000); } handleLoadMore() { const tripId = window.yatraTripData?.tripId || this.section?.getAttribute('data-trip-id') || 0; const loadMoreContainer = this.section.querySelector('.yatra-availability-load-more'); const btn = this.section.querySelector('.yatra-availability-load-more-btn'); const countInfo = this.section.querySelector('.yatra-availability-count-info'); const list = this.section.querySelector('.yatra-availability-list'); if (!tripId || !btn || !list || !loadMoreContainer) return; const perPage = parseInt(list.dataset.perPage, 10) || parseInt(loadMoreContainer.dataset.perPage, 10) || 10; const currentPage = parseInt(list.dataset.page, 10) || 1; const nextPage = currentPage + 1; const monthFilter = list.dataset.monthFilter || loadMoreContainer.dataset.monthFilter || this.currentMonthFilter || 'all'; const sortKey = list.dataset.sort || loadMoreContainer.dataset.sort || this.currentSortKey || 'date-asc'; const travelDateInput = document.getElementById('travel_date'); const travelDate = travelDateInput && travelDateInput.value ? travelDateInput.value : undefined; const originalHTML = btn.innerHTML; const loadingSvg = ' '; btn.innerHTML = loadingSvg + 'Loading...'; btn.disabled = true; const url = buildAvailabilityTemplateUrl(tripId, { date: travelDate, month_filter: monthFilter, sort: sortKey, page: nextPage, per_page: perPage, partial: true, }); fetch(url, { method: 'GET', headers: { 'Content-Type': 'application/json', }, credentials: 'omit', }) .then((resp) => { if (!resp.ok) { throw new Error('Load more failed'); } return resp.json(); }) .then((data) => { const html = data?.html || data?.data?.html; if (!html || typeof html !== 'string') { throw new Error('No HTML returned'); } const tempDiv = document.createElement('div'); tempDiv.innerHTML = html.trim(); while (tempDiv.firstChild) { list.appendChild(tempDiv.firstChild); } const total = Number(data.total) || parseInt(list.dataset.total, 10) || 0; const loadedCount = Number(data.loaded_count); const hasMore = !!data.has_more; const displayed = Number.isFinite(loadedCount) ? loadedCount : (parseInt(list.dataset.displayed, 10) || 0) + perPage; list.dataset.total = String(total); list.dataset.displayed = String(displayed); list.dataset.page = String(nextPage); this.availabilityItems = this.section.querySelectorAll('.yatra-availability-card'); this.setupToggleHandlers(); this.initTravelerSelectors(); this.attachBookNowHandlers(); if (!hasMore) { loadMoreContainer.style.display = 'none'; return; } const remaining = Math.max(0, total - displayed); btn.innerHTML = ` Load more departures (${remaining} remaining)`; btn.disabled = false; loadMoreContainer.dataset.currentPage = String(nextPage); loadMoreContainer.dataset.loaded = String(displayed); loadMoreContainer.dataset.total = String(total); if (countInfo) { countInfo.textContent = `Showing ${displayed} of ${total} departures`; } }) .catch((err) => { console.error('[Yatra] Load more failed:', err); btn.innerHTML = originalHTML; btn.disabled = false; }); } attachBookNowHandlers() { const bookButtons = this.section.querySelectorAll('.yatra-card-book-btn'); bookButtons.forEach((btn) => { const newBtn = btn.cloneNode(true); btn.parentNode.replaceChild(newBtn, btn); newBtn.addEventListener('click', (e) => { e.preventDefault(); this.handleAvailabilityBookNowClick(newBtn); }); }); } /** * Show booking error message to user just before the Book Now button * @param {HTMLElement} button - The Book Now button that was clicked * @param {string} message - Error message to display */ showBookingError(button, message) { // Find the card/row containing this button const card = button.closest('.yatra-availability-card') || button.closest('.yatra-availability-item'); if (!card) return; // Remove any existing error message in this card const existingError = card.querySelector('.yatra-booking-error-toast'); if (existingError) { existingError.remove(); } // Create error toast const toast = document.createElement('div'); toast.className = 'yatra-booking-error-toast'; toast.innerHTML = `${message}
`; this.form.insertBefore(msgEl, this.form.firstChild); // Auto-remove after 5 seconds setTimeout(() => { msgEl.remove(); }, 5000); } } /** * Similar Adventures Carousel * Handles carousel navigation for similar trips section */ class SimilarTripsCarousel { constructor() { this.track = null; this.prevBtn = null; this.nextBtn = null; this.items = []; this.currentPosition = 0; this.itemWidth = 0; this.visibleItems = 4; this.gap = 24; this.init(); } init() { if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => this.setup()); } else { this.setup(); } } setup() { this.track = document.getElementById('similar-carousel'); this.prevBtn = document.getElementById('similar-prev'); this.nextBtn = document.getElementById('similar-next'); if (!this.track || !this.prevBtn || !this.nextBtn) return; this.items = Array.from(this.track.querySelectorAll('.yatra-carousel-item')); if (this.items.length === 0) return; this.calculateDimensions(); this.attachEventListeners(); this.updateButtonStates(); // Recalculate on resize window.addEventListener('resize', () => { this.calculateDimensions(); this.updateButtonStates(); }); } calculateDimensions() { const wrapper = this.track.parentElement; const wrapperWidth = wrapper.offsetWidth; // Determine visible items based on screen width if (window.innerWidth <= 600) { this.visibleItems = 1; } else if (window.innerWidth <= 900) { this.visibleItems = 2; } else if (window.innerWidth <= 1200) { this.visibleItems = 3; } else { this.visibleItems = 4; } // Calculate item width this.itemWidth = (wrapperWidth - (this.gap * (this.visibleItems - 1))) / this.visibleItems; // Apply width to items this.items.forEach(item => { item.style.flex = `0 0 ${this.itemWidth}px`; item.style.minWidth = `${this.itemWidth}px`; }); // Reset position if out of bounds const maxPosition = Math.max(0, this.items.length - this.visibleItems); if (this.currentPosition > maxPosition) { this.currentPosition = maxPosition; this.updateTrackPosition(); } } attachEventListeners() { this.prevBtn.addEventListener('click', () => this.slide('prev')); this.nextBtn.addEventListener('click', () => this.slide('next')); // Touch/swipe support for mobile let startX = 0; let currentX = 0; let isDragging = false; this.track.addEventListener('touchstart', (e) => { startX = e.touches[0].clientX; isDragging = true; }, { passive: true }); this.track.addEventListener('touchmove', (e) => { if (!isDragging) return; currentX = e.touches[0].clientX; }, { passive: true }); this.track.addEventListener('touchend', () => { if (!isDragging) return; isDragging = false; const diff = startX - currentX; const threshold = 50; if (diff > threshold) { this.slide('next'); } else if (diff < -threshold) { this.slide('prev'); } }); } slide(direction) { const maxPosition = Math.max(0, this.items.length - this.visibleItems); if (direction === 'next' && this.currentPosition < maxPosition) { this.currentPosition++; } else if (direction === 'prev' && this.currentPosition > 0) { this.currentPosition--; } this.updateTrackPosition(); this.updateButtonStates(); } updateTrackPosition() { const offset = this.currentPosition * (this.itemWidth + this.gap); this.track.style.transform = `translateX(-${offset}px)`; } updateButtonStates() { const maxPosition = Math.max(0, this.items.length - this.visibleItems); this.prevBtn.disabled = this.currentPosition <= 0; this.nextBtn.disabled = this.currentPosition >= maxPosition; // Hide navigation if all items are visible const shouldHideNav = this.items.length <= this.visibleItems; this.prevBtn.style.display = shouldHideNav ? 'none' : 'flex'; this.nextBtn.style.display = shouldHideNav ? 'none' : 'flex'; } } /** * Wishlist Handler * Handles save/remove trip from wishlist functionality */ class WishlistHandler { constructor() { if (!window.yatraTripData?.wishlistEnabled) { return; } this.apiUrl = window.yatraTripData?.apiUrl || '/wp-json/yatra/v1'; this.nonce = window.yatraTripData?.nonce || ''; this.tripId = window.yatraTripData?.tripId || 0; this.isLoggedIn = window.yatraTripData?.isLoggedIn || false; this.init(); } init() { // Check if user is logged in and update button states if (this.isLoggedIn && this.tripId) { this.checkSavedStatus(); } // Handle wishlist button clicks document.addEventListener('click', (e) => { const btn = e.target.closest('.yatra-favorite-btn'); if (btn) { e.preventDefault(); e.stopPropagation(); this.handleWishlistClick(btn); } }); } async checkSavedStatus() { if (!this.tripId) return; try { const response = await fetch( `${this.apiUrl}/saved-trips/check/${this.tripId}`, { method: 'GET', headers: { 'X-WP-Nonce': this.nonce }, credentials: 'same-origin' } ); const data = await response.json(); if (data.success && data.data.is_saved) { this.updateButtonState(true); } } catch (error) { console.error('Error checking wishlist status:', error); } } handleWishlistClick(btn) { if (!this.isLoggedIn) { this.showLoginPopup(); return; } const btnTripId = parseInt(btn.getAttribute('data-trip-id'), 10) || this.tripId; if (!btnTripId) { return; } const isSaved = btn.classList.contains('saved'); if (isSaved) { this.removeFromWishlist(btn, btnTripId); } else { this.addToWishlist(btn, btnTripId); } } async addToWishlist(btn, tripId) { const tid = tripId || this.tripId; btn.disabled = true; btn.classList.add('loading'); try { // application/x-www-form-urlencoded is parsed reliably across hosts; JSON body can be dropped on some stacks. const params = new URLSearchParams(); params.set('trip_id', String(tid)); const response = await fetch(`${this.apiUrl}/saved-trips`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', 'X-WP-Nonce': this.nonce }, credentials: 'same-origin', body: params.toString() }); const data = await response.json(); if (data.success) { this.updateButtonState(true, tid); this.showMessage(data.message || 'Trip saved to wishlist', 'success', btn); } else { this.showMessage(data.message || 'Failed to save trip', 'error', btn); } } catch (error) { console.error('Error saving trip:', error); this.showMessage('An error occurred. Please try again.', 'error', btn); } finally { btn.disabled = false; btn.classList.remove('loading'); } } async removeFromWishlist(btn, tripId) { const tid = tripId || this.tripId; btn.disabled = true; btn.classList.add('loading'); try { const response = await fetch(`${this.apiUrl}/saved-trips/${tid}`, { method: 'DELETE', headers: { 'X-WP-Nonce': this.nonce }, credentials: 'same-origin' }); const data = await response.json(); if (data.success) { this.updateButtonState(false, tid); this.showMessage(data.message || 'Trip removed from wishlist', 'success', btn); } else { this.showMessage(data.message || 'Failed to remove trip', 'error', btn); } } catch (error) { console.error('Error removing trip:', error); this.showMessage('An error occurred. Please try again.', 'error', btn); } finally { btn.disabled = false; btn.classList.remove('loading'); } } updateButtonState(isSaved, tripId) { const tid = tripId || this.tripId; const selector = tid ? `.yatra-favorite-btn[data-trip-id="${tid}"]` : '.yatra-favorite-btn'; const buttons = document.querySelectorAll(selector); buttons.forEach(btn => { if (isSaved) { btn.classList.add('saved', 'is-saved'); btn.setAttribute('aria-label', 'Remove from favorites'); // Fill the heart icon and change color const svg = btn.querySelector('svg'); if (svg) { svg.style.fill = 'currentColor'; svg.style.stroke = 'currentColor'; btn.style.color = '#ef4444'; } } else { btn.classList.remove('saved', 'is-saved'); btn.setAttribute('aria-label', 'Add to favorites'); // Unfill the heart icon const svg = btn.querySelector('svg'); if (svg) { svg.style.fill = 'none'; svg.style.stroke = 'currentColor'; btn.style.color = ''; } } }); } showLoginPopup() { // Check if enquiry modal exists and reuse its structure, or create a simple login prompt const enquiryModal = document.getElementById('enquiry-modal'); if (enquiryModal) { // Show enquiry modal with login message enquiryModal.classList.add('active'); document.body.style.overflow = 'hidden'; // Update modal content to show login message const modalContent = enquiryModal.querySelector('.yatra-enquiry-modal-content'); if (modalContent) { const loginMessage = document.createElement('div'); loginMessage.className = 'yatra-login-prompt'; loginMessage.style.cssText = 'padding: 40px; text-align: center;'; loginMessage.innerHTML = `Please login to save trips to your wishlist.
Login `; modalContent.innerHTML = ''; modalContent.appendChild(loginMessage); } } else { // Fallback: show alert and redirect to login if (confirm('Please login to save trips to your wishlist. Would you like to go to the login page?')) { window.location.href = window.yatraTripData?.loginUrl || '/wp-login.php'; } } } showMessage(message, type, button) { // Remove existing toast if any const existingToast = document.getElementById('yatra-wishlist-toast'); if (existingToast) { existingToast.remove(); } // Create toast near the button const toast = document.createElement('div'); toast.id = 'yatra-wishlist-toast'; toast.textContent = message; // Position relative to button const rect = button.getBoundingClientRect(); const scrollTop = window.pageYOffset || document.documentElement.scrollTop; const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft; toast.style.cssText = ` position: absolute; top: ${rect.bottom + scrollTop + 10}px; left: ${rect.left + scrollLeft}px; padding: 10px 16px; border-radius: 8px; z-index: 10001; box-shadow: 0 4px 12px rgba(0,0,0,0.2); transition: all 0.3s ease; white-space: nowrap; font-size: 14px; pointer-events: none; background-color: ${type === 'success' ? '#10b981' : '#ef4444'}; color: #ffffff; opacity: 0; transform: translateY(-10px); `; document.body.appendChild(toast); // Animate in requestAnimationFrame(() => { toast.style.opacity = '1'; toast.style.transform = 'translateY(0)'; }); // Remove after delay setTimeout(() => { toast.style.opacity = '0'; toast.style.transform = 'translateY(-10px)'; setTimeout(() => { if (toast.parentNode) { toast.remove(); } }, 300); }, 3000); } } /** * Trip Page Orchestrator * Centralized bootstrapper to keep initialization class-based and organized */ class TripPage { constructor() { this.instances = {}; } init() { // Create all feature instances this.instances.galleryModal = new GalleryModal(); this.instances.itineraryGalleryModal = new ItineraryGalleryModal(); this.instances.heroMediaSwitcher = new HeroMediaSwitcher(); this.instances.bookingSidebar = new BookingSidebar(); this.instances.enquiryModal = new EnquiryModal(); this.instances.stickyNav = new StickyNav(); this.instances.wishlistHandler = new WishlistHandler(); this.instances.availabilitySection = new AvailabilitySection(); this.instances.itinerarySection = new ItinerarySection(); this.instances.reviewForm = new ReviewForm(); this.instances.similarTripsCarousel = new SimilarTripsCarousel(); // Expose for legacy access (existing templates rely on window.*) this.exposeGlobals(); // Extra page-level behaviors this.initHeroSmoothScroll(); return this.instances; } exposeGlobals() { Object.entries(this.instances).forEach(([key, instance]) => { window[key] = instance; }); // Templates use window.YatraEnquiry.open() — alias to EnquiryModal instance if (this.instances.enquiryModal) { window.YatraEnquiry = this.instances.enquiryModal; } // Also expose orchestrator for potential future hooks window.tripPage = this; } initHeroSmoothScroll() { const attachScroll = () => { document.querySelectorAll('a[href="#yatra-booking-widget"]').forEach((link) => { link.addEventListener('click', (e) => { e.preventDefault(); const target = document.getElementById('yatra-booking-widget'); if (target) { target.scrollIntoView({ behavior: 'smooth', block: 'start' }); } }); }); }; if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', attachScroll); } else { attachScroll(); } } static bootstrap() { const start = () => { new TripPage().init(); new ItineraryVideoHandler(); // Initialize itinerary video handler }; if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', start); } else { start(); } } } /** * Itinerary Video Handler Class * Handles video player functionality for itinerary section */ class ItineraryVideoHandler { constructor() { this.init(); } init() { if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => this.setup()); } else { this.setup(); } } setup() { // Handle itinerary video clicks on overlay (both in gallery and separate video section) const videoOverlays = document.querySelectorAll('.yatra-entry-gallery .yatra-media-overlay, .yatra-entry-video .yatra-media-overlay'); videoOverlays.forEach(overlay => { overlay.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); const link = overlay.closest('a'); if (!link) return; const videoUrl = link.getAttribute('href'); if (!videoUrl || videoUrl === '#') return; const img = overlay.closest('.yatra-media-card').querySelector('img'); const videoTitle = img ? img.alt : 'Itinerary Video'; // Extract video ID from URL let videoId = ''; if (videoUrl.includes('youtube.com') || videoUrl.includes('youtu.be')) { // YouTube video ID extraction const matches = videoUrl.match(/(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/)([^&\n?#]+)/); videoId = matches ? matches[1] : ''; } else if (videoUrl.includes('vimeo.com')) { // Vimeo video ID extraction const matches = videoUrl.match(/vimeo\.com\/(?:.*#|.\/videos\/)?([0-9]+)/); videoId = matches ? matches[1] : ''; } // Play video using YatraVideoPlayer if (window.YatraVideoPlayer) { const videoData = { url: videoUrl, title: videoTitle }; // Only add video_id for YouTube videos, let player detect URL type for others if (videoUrl.includes('youtube.com') || videoUrl.includes('youtu.be')) { videoData.video_id = videoId; } window.YatraVideoPlayer.play(videoData); } }); }); // Prevent video links from opening in new tab (both in gallery and separate video section) const videoLinks = document.querySelectorAll('.yatra-entry-gallery .yatra-itinerary-video-link, .yatra-entry-video .yatra-itinerary-video-link'); videoLinks.forEach(link => { link.addEventListener('click', (e) => { e.preventDefault(); return false; }); }); } } /** * Downloads Handler Class * Handles download section interactions including preview modal */ class DownloadsHandler { constructor() { this.previewModal = null; this.currentDownloadId = null; this.init(); } init() { if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => this.setup()); } else { this.setup(); } } setup() { this.previewModal = document.getElementById('download-preview-modal'); if (!this.previewModal) return; this.attachEventListeners(); } attachEventListeners() { // Handle download button clicks document.addEventListener('click', (e) => { if (e.target.closest('.yatra-download-btn')) { e.preventDefault(); const btn = e.target.closest('.yatra-download-btn'); const downloadId = btn.dataset.downloadId; if (downloadId) { this.downloadFile(downloadId); } } // Handle view/preview link clicks if (e.target.closest('.yatra-download-view-link, .yatra-download-preview-link')) { e.preventDefault(); const link = e.target.closest('.yatra-download-view-link, .yatra-download-preview-link'); const downloadId = link.dataset.downloadId; if (downloadId) { this.previewFile(downloadId); } } // Handle modal close if (e.target.closest('.yatra-preview-modal-close, .yatra-preview-close-btn')) { this.closePreviewModal(); } // Handle modal overlay click if (e.target.classList.contains('yatra-preview-modal-overlay')) { this.closePreviewModal(); } // Handle preview modal download button if (e.target.closest('#preview-download-btn')) { if (this.currentDownloadId) { this.downloadFile(this.currentDownloadId); } } }); // Handle ESC key to close modal document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && this.previewModal.style.display === 'block') { this.closePreviewModal(); } }); } async downloadFile(downloadId) { try { // Get download URL with signature using centralized helper const response = await window.YatraApiHelper.getDownloadInfo(downloadId); if (!response.ok) { throw new Error('Download failed'); } const data = await response.json(); if (data.download_url) { // Create temporary link and trigger download const link = document.createElement('a'); link.href = data.download_url; link.download = ''; // Let server determine filename document.body.appendChild(link); link.click(); document.body.removeChild(link); } else { throw new Error('Download URL not found'); } } catch (error) { console.error('Download error:', error); // Show error message to user this.showNotification('Download failed. Please try again.', 'error'); } } async previewFile(downloadId) { try { this.currentDownloadId = downloadId; this.showPreviewModal(); // Get download info using centralized helper const response = await window.YatraApiHelper.getDownloadInfo(downloadId); if (!response.ok) { throw new Error('Failed to get file info'); } const data = await response.json(); if (data.download_url) { this.loadPreview(data.download_url, data.title || 'Preview'); } else { throw new Error('File URL not found'); } } catch (error) { console.error('Preview error:', error); this.closePreviewModal(); this.showNotification('Failed to load preview. Please try again.', 'error'); } } loadPreview(fileUrl, title) { const previewContent = document.getElementById('preview-content'); const modalTitle = document.querySelector('.yatra-preview-modal-title'); if (modalTitle) { modalTitle.textContent = title; } if (!previewContent) return; // Hide loading and show preview const loading = previewContent.querySelector('.yatra-preview-loading'); if (loading) { loading.style.display = 'none'; } // Check file type and load appropriate preview const fileType = this.getFileType(fileUrl); if (fileType === 'image') { previewContent.innerHTML = `Preview not available for this file type
Please download the file to view its contents
Loading preview...