| @@ -3,11 +3,126 @@ | ||
| 3 | 3 | * Class-based implementation for trip page features |
| 4 | 4 | */ |
| 5 | 5 | /* jshint esversion: 6 */ |
| 6 | 6 | |
| 7 | +// Translation helpers (booking.js installs the same shims if it runs first; | |
| 8 | +// idempotent setup so either load order works). | |
| 9 | +// Parse a numeric min/max attribute while preserving a legitimate 0. | |
| 10 | +// `parseInt(el.getAttribute('max')) || 99` treated max="0" — what a sold-out | |
| 11 | +// departure renders — as "unset" and fell back to the placeholder ceiling, so | |
| 12 | +// the guest selector stayed usable and ignored the departure's real capacity. | |
| 13 | +// Only an absent or non-numeric value may fall back. Installed once and shared | |
| 14 | +// by every frontend script, so load order does not matter. | |
| 15 | +if (typeof window.yatraNumOr !== 'function') { | |
| 16 | + window.yatraNumOr = function (raw, fallback) { | |
| 17 | + var parsed = parseInt(raw, 10); | |
| 18 | + return isNaN(parsed) ? fallback : parsed; | |
| 19 | + }; | |
| 20 | +} | |
| 21 | + | |
| 22 | +(function () { | |
| 23 | + var i18n = (window.wp && window.wp.i18n) ? window.wp.i18n : null; | |
| 24 | + if (i18n && typeof i18n.__ === 'function') { | |
| 25 | + window.__ = function (text, domain) { return i18n.__(text, domain || 'yatra'); }; | |
| 26 | + window._n = function (s, p, n, domain) { return i18n._n(s, p, n, domain || 'yatra'); }; | |
| 27 | + window._x = function (text, ctx, domain) { return i18n._x(text, ctx, domain || 'yatra'); }; | |
| 28 | + // Always bind WordPress's own sprintf (it substitutes %d/%s/%1$s). | |
| 29 | + // Do NOT defer to a pre-existing window.sprintf: a theme or other | |
| 30 | + // plugin can define an incompatible global that leaves placeholders | |
| 31 | + // unsubstituted (the cause of literal "%d"/"%s" in the UI). | |
| 32 | + if (typeof i18n.sprintf === 'function') { | |
| 33 | + window.sprintf = function () { return i18n.sprintf.apply(i18n, arguments); }; | |
| 34 | + } else if (typeof window.sprintf !== 'function') { | |
| 35 | + window.sprintf = function (fmt) { return fmt; }; | |
| 36 | + } | |
| 37 | + } else { | |
| 38 | + // wp.i18n not present — install passthroughs only if nothing usable | |
| 39 | + // exists yet (don't clobber a real binding installed earlier). | |
| 40 | + if (typeof window.__ !== 'function') window.__ = function (text) { return text; }; | |
| 41 | + if (typeof window._n !== 'function') window._n = function (s, p, n) { return n === 1 ? s : p; }; | |
| 42 | + if (typeof window._x !== 'function') window._x = function (text) { return text; }; | |
| 43 | + if (typeof window.sprintf !== 'function') window.sprintf = function (fmt) { return fmt; }; | |
| 44 | + } | |
| 45 | +})(); | |
| 46 | + | |
| 7 | 47 | (function() { |
| 8 | 48 | 'use strict'; |
| 9 | 49 | |
| 50 | + /** | |
| 51 | + * Convert PHP date format (WordPress/Yatra) to Flatpickr format. | |
| 52 | + * Covers the common tokens exposed in Yatra Settings UI. | |
| 53 | + * | |
| 54 | + * @param {string} php | |
| 55 | + * @returns {string} | |
| 56 | + */ | |
| 57 | + const phpDateFormatToFlatpickr = (php) => { | |
| 58 | + if (!php || typeof php !== 'string') return 'F j, Y'; | |
| 59 | + const map = { | |
| 60 | + // Day | |
| 61 | + d: 'd', | |
| 62 | + j: 'j', | |
| 63 | + D: 'D', | |
| 64 | + l: 'l', | |
| 65 | + // Month | |
| 66 | + m: 'm', | |
| 67 | + n: 'n', | |
| 68 | + M: 'M', | |
| 69 | + F: 'F', | |
| 70 | + // Year | |
| 71 | + Y: 'Y', | |
| 72 | + y: 'y', | |
| 73 | + // Time (for any datetime pickers) | |
| 74 | + H: 'H', | |
| 75 | + G: 'H', | |
| 76 | + h: 'h', | |
| 77 | + g: 'h', | |
| 78 | + i: 'i', | |
| 79 | + s: 'S', | |
| 80 | + a: 'K', | |
| 81 | + A: 'K' | |
| 82 | + }; | |
| 83 | + | |
| 84 | + let out = ''; | |
| 85 | + let escaping = false; | |
| 86 | + for (let i = 0; i < php.length; i++) { | |
| 87 | + const ch = php[i]; | |
| 88 | + if (escaping) { | |
| 89 | + out += ch; | |
| 90 | + escaping = false; | |
| 91 | + continue; | |
| 92 | + } | |
| 93 | + if (ch === '\\') { | |
| 94 | + escaping = true; | |
| 95 | + continue; | |
| 96 | + } | |
| 97 | + out += map[ch] || ch; | |
| 98 | + } | |
| 99 | + return out || 'F j, Y'; | |
| 100 | + }; | |
| 101 | + | |
| 102 | + const getTripAltDateFormat = () => { | |
| 103 | + const cfg = window.yatraTripData || {}; | |
| 104 | + const php = cfg.dateFormat || cfg.date_format || 'Y-m-d'; | |
| 105 | + return phpDateFormatToFlatpickr(php); | |
| 106 | + }; | |
| 107 | + | |
| 108 | + /** | |
| 109 | + * Flatpickr locale from PHP ({@see FrontendAssetsProvider::buildFlatpickrLocalePayload}). | |
| 110 | + * @returns {Record<string, unknown>} | |
| 111 | + */ | |
| 112 | + const getFlatpickrLocale = () => { | |
| 113 | + const cfg = window.yatraTripData || {}; | |
| 114 | + const l = cfg.flatpickrLocale; | |
| 115 | + if (!l || typeof l !== 'object') { | |
| 116 | + return { firstDayOfWeek: 1 }; | |
| 117 | + } | |
| 118 | + const out = Object.assign({}, l); | |
| 119 | + if (typeof out.firstDayOfWeek !== 'number' || Number.isNaN(out.firstDayOfWeek)) { | |
| 120 | + out.firstDayOfWeek = 1; | |
| 121 | + } | |
| 122 | + return out; | |
| 123 | + }; | |
| 124 | + | |
| 10 | 125 | const getRestBase = () => { |
| 11 | 126 | const siteUrl = |
| 12 | 127 | (window.yatraTripData && window.yatraTripData.siteUrl) || |
| 13 | 128 | (window.yatraAdmin && window.yatraAdmin.siteUrl) || |
| @@ -79,9 +194,13 @@ | ||
| 79 | 194 | const symbol = (typeof cfg.currencySymbol === 'string' && cfg.currencySymbol.length) |
| 80 | 195 | ? cfg.currencySymbol |
| 81 | 196 | : (typeof cfg.currency === 'string' ? cfg.currency : '$'); |
| 82 | 197 | |
| 83 | - const position = (cfg.currencyPosition || 'before').toString(); | |
| 198 | + const position = ( | |
| 199 | + cfg.currencyPosition || | |
| 200 | + cfg.currency_position || | |
| 201 | + 'before' | |
| 202 | + ).toString().toLowerCase().trim(); | |
| 84 | 203 | const decimals = Number.isFinite(Number(cfg.decimalPlaces)) ? Number(cfg.decimalPlaces) : 2; |
| 85 | 204 | const thousandSep = (typeof cfg.thousandSeparator === 'string') ? cfg.thousandSeparator : ','; |
| 86 | 205 | const decimalSep = (typeof cfg.decimalSeparator === 'string') ? cfg.decimalSeparator : '.'; |
| 87 | 206 | |
| @@ -86,9 +205,14 @@ | ||
| 86 | 205 | const decimalSep = (typeof cfg.decimalSeparator === 'string') ? cfg.decimalSeparator : '.'; |
| 87 | 206 | |
| 88 | 207 | const num = Number(amount) || 0; |
| 89 | 208 | |
| 90 | - const formatted = new Intl.NumberFormat(undefined, { | |
| 209 | + // Format with a FIXED 'en-US' base (grouping ',', decimal '.') so the | |
| 210 | + // replace-with-configured-separators below is deterministic. With the | |
| 211 | + // viewer's locale (undefined), a European browser already yields | |
| 212 | + // '1.234,56' and the naive comma/dot replacement SWAPS the separators — | |
| 213 | + // making the configured decimal separator appear ignored. | |
| 214 | + const formatted = new Intl.NumberFormat('en-US', { | |
| 91 | 215 | minimumFractionDigits: decimals, |
| 92 | 216 | maximumFractionDigits: decimals, |
| 93 | 217 | }).format(num) |
| 94 | 218 | .replace(/,/g, 'TEMP_THOUSAND') |
| @@ -95,17 +219,66 @@ | ||
| 95 | 219 | .replace(/\./g, 'TEMP_DECIMAL') |
| 96 | 220 | .replace(/TEMP_THOUSAND/g, thousandSep) |
| 97 | 221 | .replace(/TEMP_DECIMAL/g, decimalSep); |
| 98 | 222 | |
| 99 | - // Accept common variants: before/left vs after/right | |
| 100 | - if (position === 'after' || position === 'right') { | |
| 101 | - return `${formatted} ${symbol}`; | |
| 223 | + var pos = position; | |
| 224 | + if (pos === 'before') { | |
| 225 | + pos = 'left_space'; | |
| 226 | + } else if (pos === 'after') { | |
| 227 | + pos = 'right_space'; | |
| 102 | 228 | } |
| 103 | - return `${symbol}${formatted}`; | |
| 229 | + if (pos === 'right') { | |
| 230 | + return formatted + symbol; | |
| 231 | + } | |
| 232 | + if (pos === 'right_space') { | |
| 233 | + return formatted + ' ' + symbol; | |
| 234 | + } | |
| 235 | + if (pos === 'left') { | |
| 236 | + return symbol + formatted; | |
| 237 | + } | |
| 238 | + return symbol + ' ' + formatted; | |
| 104 | 239 | }; |
| 105 | 240 | } |
| 106 | 241 | |
| 107 | 242 | /** |
| 243 | + * Resolve the real image URL from an <img> element. | |
| 244 | + * | |
| 245 | + * Caching / performance plugins (LiteSpeed Cache, WP Rocket, W3 Total Cache, | |
| 246 | + * Autoptimize, etc.) replace the `src` attribute with a tiny base64 | |
| 247 | + * placeholder and store the real URL in a data-* attribute. We check our | |
| 248 | + * own `data-yatra-src` first (added in PHP templates and never touched by | |
| 249 | + * any third-party plugin), then fall back to the most common lazy-load | |
| 250 | + * attribute names, and finally fall back to `img.src` while filtering out | |
| 251 | + * obvious placeholders so we never pass a base64 GIF to the modal. | |
| 252 | + */ | |
| 253 | + function getImageSrc(img) { | |
| 254 | + // 1. Our own reliable attribute – always present on Yatra gallery images. | |
| 255 | + const yatra = img.getAttribute('data-yatra-src'); | |
| 256 | + if (yatra) return yatra; | |
| 257 | + | |
| 258 | + // 2. Common lazy-load attributes used by popular caching plugins. | |
| 259 | + const lazyAttrs = [ | |
| 260 | + 'data-src', // LiteSpeed Cache, WP Rocket, Smush, BJ Lazy Load | |
| 261 | + 'data-lazyload', // LiteSpeed Cache (older attribute) | |
| 262 | + 'data-lazy-src', // WordPress core lazy-load | |
| 263 | + 'data-original', // jQuery.lazyLoad plugin | |
| 264 | + 'data-pagespeed-lazy-src', // Google PageSpeed / mod_pagespeed | |
| 265 | + 'data-lazy', // Jetpack | |
| 266 | + 'data-full-size', // Yatra itinerary images | |
| 267 | + ]; | |
| 268 | + for (const attr of lazyAttrs) { | |
| 269 | + const val = img.getAttribute(attr); | |
| 270 | + if (val && !val.startsWith('data:')) return val; | |
| 271 | + } | |
| 272 | + | |
| 273 | + // 3. Native src – only use if it is not a placeholder. | |
| 274 | + const src = img.src || ''; | |
| 275 | + if (src && !src.startsWith('data:')) return src; | |
| 276 | + | |
| 277 | + return ''; | |
| 278 | + } | |
| 279 | + | |
| 280 | + /** | |
| 108 | 281 | * Gallery Modal Class |
| 109 | 282 | * Handles image gallery modal functionality |
| 110 | 283 | */ |
| 111 | 284 | class GalleryModal { |
| @@ -148,55 +321,60 @@ | ||
| 148 | 321 | |
| 149 | 322 | collectImages() { |
| 150 | 323 | this.images = []; |
| 151 | 324 | |
| 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 | - }); | |
| 325 | + // Helper: deduplicate by resolved URL. | |
| 326 | + const add = (img, fallbackAlt) => { | |
| 327 | + const src = getImageSrc(img); | |
| 328 | + if (!src) return; | |
| 329 | + if (!this.images.some(i => i.src === src)) { | |
| 330 | + this.images.push({ src, alt: img.alt || fallbackAlt }); | |
| 331 | + } | |
| 332 | + }; | |
| 161 | 333 | |
| 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' | |
| 334 | + // 1. Prefer the JSON media-data blob (never touched by caching plugins). | |
| 335 | + const mediaScript = document.getElementById('yatra-hero-media-data'); | |
| 336 | + if (mediaScript) { | |
| 337 | + try { | |
| 338 | + const mediaData = JSON.parse(mediaScript.textContent || '{}'); | |
| 339 | + const imageUrls = Array.isArray(mediaData.images) ? mediaData.images : []; | |
| 340 | + imageUrls.forEach((url, index) => { | |
| 341 | + if (url && typeof url === 'string' && !this.images.some(i => i.src === url)) { | |
| 342 | + this.images.push({ | |
| 343 | + src: url, | |
| 344 | + // Indexed alt text — sprintf isn't guaranteed | |
| 345 | + // available on every page, so we replace() the | |
| 346 | + // %d placeholder ourselves. Keeps the msgid a | |
| 347 | + // literal so makepot extracts cleanly. | |
| 348 | + /* translators: %d is the 1-based image index in the gallery. */ | |
| 349 | + alt: __('Gallery Image %d', 'yatra').replace('%d', String(index + 1)), | |
| 350 | + }); | |
| 351 | + } | |
| 171 | 352 | }); |
| 353 | + } catch (e) { | |
| 354 | + // JSON parse error – fall through to DOM-based collection. | |
| 172 | 355 | } |
| 356 | + } | |
| 357 | + | |
| 358 | + // 2. Collect from hero slides (covers any images not in the JSON blob). | |
| 359 | + document.querySelectorAll('.yatra-trip-hero-slide img').forEach((img, index) => { | |
| 360 | + /* translators: %d is the 1-based image index in the gallery. */ | |
| 361 | + add(img, __('Gallery Image %d', 'yatra').replace('%d', String(index + 1))); | |
| 173 | 362 | }); |
| 174 | 363 | |
| 175 | - // Collect from hero main image | |
| 364 | + // 3. Collect from gallery section (hero gallery only, NOT itinerary). | |
| 365 | + document.querySelectorAll('.yatra-trip-gallery .yatra-gallery-item img').forEach((img) => { | |
| 366 | + add(img, __('Gallery Image', 'yatra')); | |
| 367 | + }); | |
| 368 | + | |
| 369 | + // 4. Collect from hero main image. | |
| 176 | 370 | 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 | - } | |
| 371 | + if (heroMainImg) add(heroMainImg, __('Main Image', 'yatra')); | |
| 184 | 372 | |
| 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 | - } | |
| 373 | + // 5. Collect from side images. | |
| 374 | + document.querySelectorAll('.yatra-side-image-item img').forEach((img) => { | |
| 375 | + add(img, __('Side Image', 'yatra')); | |
| 196 | 376 | }); |
| 197 | - | |
| 198 | - console.log('Total gallery images collected:', this.images.length); | |
| 199 | 377 | } |
| 200 | 378 | |
| 201 | 379 | createThumbnails() { |
| 202 | 380 | const container = this.modal.querySelector('.yatra-gallery-thumbnails-container'); |
| @@ -348,9 +526,10 @@ | ||
| 348 | 526 | galleryItems.forEach((item, index) => { |
| 349 | 527 | item.addEventListener('click', () => { |
| 350 | 528 | const img = item.querySelector('img'); |
| 351 | 529 | if (img) { |
| 352 | - const imageIndex = this.images.findIndex(i => i.src === img.src); | |
| 530 | + const src = getImageSrc(img); | |
| 531 | + const imageIndex = this.images.findIndex(i => i.src === src); | |
| 353 | 532 | this.open(imageIndex >= 0 ? imageIndex : index); |
| 354 | 533 | } |
| 355 | 534 | }); |
| 356 | 535 | }); |
| @@ -363,9 +542,10 @@ | ||
| 363 | 542 | return; |
| 364 | 543 | } |
| 365 | 544 | const img = item.querySelector('img'); |
| 366 | 545 | if (img) { |
| 367 | - const imageIndex = this.images.findIndex(i => i.src === img.src); | |
| 546 | + const src = getImageSrc(img); | |
| 547 | + const imageIndex = this.images.findIndex(i => i.src === src); | |
| 368 | 548 | this.open(imageIndex >= 0 ? imageIndex : 0); |
| 369 | 549 | } |
| 370 | 550 | }); |
| 371 | 551 | }); |
| @@ -387,14 +567,21 @@ | ||
| 387 | 567 | } |
| 388 | 568 | |
| 389 | 569 | /** |
| 390 | 570 | * Itinerary Gallery Modal Class |
| 391 | - * Separate gallery modal for itinerary section - independent from hero gallery | |
| 571 | + * Separate gallery modal for itinerary section - independent from hero gallery. | |
| 572 | + * | |
| 573 | + * Scoping: a single modal element is reused, but the image set is RECOMPUTED on | |
| 574 | + * every click from the *closest* `.yatra-entry-gallery` ancestor. Clicking a | |
| 575 | + * thumbnail in Day 2 must open only Day 2's images (starting at the clicked one) — | |
| 576 | + * never the global pool of every day's images. The hidden | |
| 577 | + * `.yatra-itinerary-trip-gallery` div on the page is for SEO / structured data | |
| 578 | + * only; it intentionally no longer drives the modal. | |
| 392 | 579 | */ |
| 393 | 580 | class ItineraryGalleryModal { |
| 394 | 581 | constructor() { |
| 395 | 582 | this.modal = null; |
| 396 | - this.images = []; | |
| 583 | + this.images = []; // currently-visible image set (the active scope) | |
| 397 | 584 | this.currentIndex = 0; |
| 398 | 585 | this.init(); |
| 399 | 586 | } |
| 400 | 587 | |
| @@ -411,56 +598,69 @@ | ||
| 411 | 598 | if (!this.modal) { |
| 412 | 599 | console.warn('Itinerary gallery modal not found'); |
| 413 | 600 | return; |
| 414 | 601 | } |
| 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 | 602 | this.attachEventListeners(); |
| 421 | 603 | } |
| 422 | 604 | |
| 423 | - collectImages() { | |
| 424 | - this.images = []; | |
| 605 | + /** | |
| 606 | + * Build the image list from a scope element (an `.yatra-entry-gallery` block). | |
| 607 | + * Falls back to the document if no scope is given (preserves the legacy | |
| 608 | + * "global pool" behaviour for the SEO-only thumbnails clicker). | |
| 609 | + */ | |
| 610 | + collectImagesFrom(scope) { | |
| 611 | + const root = scope || document; | |
| 612 | + const images = []; | |
| 425 | 613 | |
| 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 | - }); | |
| 614 | + const add = (img, fallbackAlt) => { | |
| 615 | + const src = img.getAttribute('data-full-size') || getImageSrc(img); | |
| 616 | + if (!src) return; | |
| 617 | + if (!images.some(i => i.src === src)) { | |
| 618 | + images.push({ src, alt: img.alt || fallbackAlt }); | |
| 619 | + } | |
| 620 | + }; | |
| 621 | + | |
| 622 | + // Visible gallery thumbnails inside the scope | |
| 623 | + root.querySelectorAll('.yatra-media-card img').forEach((img) => { | |
| 624 | + add(img, sprintf( | |
| 625 | + /* translators: %d: gallery image sequence number. */ | |
| 626 | + __('Itinerary Gallery Image %d', 'yatra'), | |
| 627 | + images.length + 1 | |
| 628 | + )); | |
| 434 | 629 | }); |
| 435 | 630 | |
| 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 | - } | |
| 631 | + // Video links inside the scope (fallback selector) | |
| 632 | + root.querySelectorAll('.yatra-itinerary-video-link img').forEach((img) => { | |
| 633 | + add(img, sprintf( | |
| 634 | + /* translators: %d: itinerary video sequence number. */ | |
| 635 | + __('Itinerary Video %d', 'yatra'), | |
| 636 | + images.length + 1 | |
| 637 | + )); | |
| 447 | 638 | }); |
| 448 | 639 | |
| 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 | - }); | |
| 640 | + // If still empty AND we were given no scope, fall back to the hidden global pool | |
| 641 | + // so legacy click handlers on `.yatra-itinerary-trip-gallery .yatra-gallery-item` | |
| 642 | + // continue to function. | |
| 643 | + if (images.length === 0 && !scope) { | |
| 644 | + document.querySelectorAll('.yatra-itinerary-trip-gallery .yatra-gallery-item img').forEach((img) => { | |
| 645 | + add(img, sprintf( | |
| 646 | + /* translators: %d: itinerary image sequence number. */ | |
| 647 | + __('Itinerary Image %d', 'yatra'), | |
| 648 | + images.length + 1 | |
| 649 | + )); | |
| 650 | + }); | |
| 651 | + } | |
| 652 | + | |
| 653 | + return images; | |
| 461 | 654 | } |
| 462 | 655 | |
| 656 | + /** Replace the active image set and rebuild thumbnails. */ | |
| 657 | + setActiveImages(images) { | |
| 658 | + this.images = images; | |
| 659 | + this.currentIndex = 0; | |
| 660 | + this.createThumbnails(); | |
| 661 | + } | |
| 662 | + | |
| 463 | 663 | createThumbnails() { |
| 464 | 664 | const container = this.modal.querySelector('.yatra-gallery-thumbnails-container'); |
| 465 | 665 | if (!container) return; |
| 466 | 666 | |
| @@ -581,29 +781,57 @@ | ||
| 581 | 781 | if (e.key === 'ArrowLeft') this.prev(); |
| 582 | 782 | if (e.key === 'ArrowRight') this.next(); |
| 583 | 783 | }); |
| 584 | 784 | |
| 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 | - }); | |
| 785 | + // Itinerary day gallery thumbnails — SCOPED to the clicked day. | |
| 786 | + // Every click recomputes the image set from the closest .yatra-entry-gallery so | |
| 787 | + // Day 2's modal only ever shows Day 2's photos, starting at the clicked one. | |
| 788 | + // Video links are intentionally NOT captured here — they have their own handler | |
| 789 | + // that opens YatraVideoPlayer (see further down in this file). | |
| 790 | + const onItineraryClick = (e) => { | |
| 791 | + const link = e.target.closest('.yatra-itinerary-gallery-link'); | |
| 792 | + if (!link) return; | |
| 793 | + // Don't hijack video link clicks even if they happen to match | |
| 794 | + if (link.classList.contains('yatra-itinerary-video-link')) return; | |
| 795 | + const scope = link.closest('.yatra-entry-gallery'); | |
| 796 | + if (!scope) return; | |
| 596 | 797 | |
| 597 | - // Attach click handlers to itinerary gallery items in hidden section | |
| 798 | + e.preventDefault(); | |
| 799 | + | |
| 800 | + const scopedImages = this.collectImagesFrom(scope); | |
| 801 | + if (scopedImages.length === 0) return; | |
| 802 | + | |
| 803 | + // Find the clicked image's index within the scoped set | |
| 804 | + const clickedImg = link.querySelector('img'); | |
| 805 | + const clickedSrc = clickedImg | |
| 806 | + ? (clickedImg.getAttribute('data-full-size') || getImageSrc(clickedImg)) | |
| 807 | + : null; | |
| 808 | + const startIndex = clickedSrc | |
| 809 | + ? Math.max(0, scopedImages.findIndex((i) => i.src === clickedSrc)) | |
| 810 | + : 0; | |
| 811 | + | |
| 812 | + this.setActiveImages(scopedImages); | |
| 813 | + this.open(startIndex); | |
| 814 | + }; | |
| 815 | + // Delegated listener — works even if entries are added/removed dynamically | |
| 816 | + document.addEventListener('click', onItineraryClick); | |
| 817 | + | |
| 818 | + // Legacy: clicks on the hidden SEO pool (kept for back-compat). These open | |
| 819 | + // the full pool because there's no per-day scope on the SEO container. | |
| 598 | 820 | const galleryItems = document.querySelectorAll('.yatra-itinerary-trip-gallery .yatra-gallery-item'); |
| 599 | 821 | galleryItems.forEach((item, index) => { |
| 600 | 822 | item.addEventListener('click', () => { |
| 823 | + const allImages = this.collectImagesFrom(null); // fall back to global | |
| 824 | + if (allImages.length === 0) return; | |
| 601 | 825 | 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 | - } | |
| 826 | + const src = img | |
| 827 | + ? (img.getAttribute('data-full-size') || getImageSrc(img)) | |
| 828 | + : null; | |
| 829 | + const startIndex = src | |
| 830 | + ? Math.max(0, allImages.findIndex((i) => i.src === src)) | |
| 831 | + : index; | |
| 832 | + this.setActiveImages(allImages); | |
| 833 | + this.open(startIndex); | |
| 606 | 834 | }); |
| 607 | 835 | }); |
| 608 | 836 | } |
| 609 | 837 | } |
| @@ -758,9 +986,9 @@ | ||
| 758 | 986 | } |
| 759 | 987 | |
| 760 | 988 | if (images.length > 0) { |
| 761 | 989 | img.src = images[0]; |
| 762 | - img.alt = 'Trip Image 1'; | |
| 990 | + img.alt = __('Trip Image 1', 'yatra'); | |
| 763 | 991 | if (link) { |
| 764 | 992 | link.href = '#'; |
| 765 | 993 | link.dataset.imageIndex = '0'; |
| 766 | 994 | } |
| @@ -978,8 +1206,12 @@ | ||
| 978 | 1206 | if (existingOverlay) existingOverlay.remove(); |
| 979 | 1207 | |
| 980 | 1208 | const overlay = document.createElement('div'); |
| 981 | 1209 | overlay.className = 'yatra-media-overlay yatra-media-tour-overlay'; |
| 1210 | + const tourFallbackTitle = __('360° Virtual Tour', 'yatra'); | |
| 1211 | + /* translators: %s: tour type (e.g. "360°"). */ | |
| 1212 | + const tourTypeLabel = sprintf(__('%s Experience', 'yatra'), tour.tour_type || '360°'); | |
| 1213 | + const tourActionLabel = __('Click to explore', 'yatra'); | |
| 982 | 1214 | overlay.innerHTML = ` |
| 983 | 1215 | <div class="yatra-tour-info"> |
| 984 | 1216 | <div class="yatra-tour-icon"> |
| 985 | 1217 | <svg width="80" height="80" viewBox="0 0 100 100" fill="none"> |
| @@ -992,11 +1224,11 @@ | ||
| 992 | 1224 | <text x="50" y="58" text-anchor="middle" fill="white" font-size="24" font-weight="bold">360°</text> |
| 993 | 1225 | </svg> |
| 994 | 1226 | </div> |
| 995 | 1227 | <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> | |
| 1228 | + <div class="yatra-tour-title">${tour.title || tourFallbackTitle}</div> | |
| 1229 | + <div class="yatra-tour-type">${tourTypeLabel}</div> | |
| 1230 | + <div class="yatra-tour-action">${tourActionLabel}</div> | |
| 999 | 1231 | </div> |
| 1000 | 1232 | </div> |
| 1001 | 1233 | `; |
| 1002 | 1234 | |
| @@ -1034,8 +1266,9 @@ | ||
| 1034 | 1266 | if (existingOverlay) existingOverlay.remove(); |
| 1035 | 1267 | |
| 1036 | 1268 | const overlay = document.createElement('div'); |
| 1037 | 1269 | overlay.className = 'yatra-media-overlay yatra-media-document-overlay'; |
| 1270 | + const documentFallbackTitle = __('Document', 'yatra'); | |
| 1038 | 1271 | overlay.innerHTML = ` |
| 1039 | 1272 | <div class="yatra-document-info"> |
| 1040 | 1273 | <div class="yatra-document-icon"> |
| 1041 | 1274 | <svg width="48" height="48" viewBox="0 0 24 24" fill="none"> |
| @@ -1045,9 +1278,9 @@ | ||
| 1045 | 1278 | <path d="M16 17H8" stroke="#374151" stroke-width="2"/> |
| 1046 | 1279 | </svg> |
| 1047 | 1280 | </div> |
| 1048 | 1281 | <div class="yatra-document-details"> |
| 1049 | - <div class="yatra-document-title">${doc.title || 'Document'}</div> | |
| 1282 | + <div class="yatra-document-title">${doc.title || documentFallbackTitle}</div> | |
| 1050 | 1283 | <div class="yatra-document-size">${this.formatFileSize(doc.file_size || 0)}</div> |
| 1051 | 1284 | </div> |
| 1052 | 1285 | </div> |
| 1053 | 1286 | `; |
| @@ -1054,11 +1287,11 @@ | ||
| 1054 | 1287 | container.appendChild(overlay); |
| 1055 | 1288 | } |
| 1056 | 1289 | |
| 1057 | 1290 | formatFileSize(bytes) { |
| 1058 | - if (bytes === 0) return '0 Bytes'; | |
| 1291 | + if (bytes === 0) return '0 ' + __('Bytes', 'yatra'); | |
| 1059 | 1292 | const k = 1024; |
| 1060 | - const sizes = ['Bytes', 'KB', 'MB', 'GB']; | |
| 1293 | + const sizes = [__('Bytes', 'yatra'), 'KB', 'MB', 'GB']; | |
| 1061 | 1294 | const i = Math.floor(Math.log(bytes) / Math.log(k)); |
| 1062 | 1295 | return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; |
| 1063 | 1296 | } |
| 1064 | 1297 | |
| @@ -1088,9 +1321,10 @@ | ||
| 1088 | 1321 | sideImages.forEach((item, index) => { |
| 1089 | 1322 | const img = item.querySelector('img'); |
| 1090 | 1323 | if (img && images[index + 1]) { |
| 1091 | 1324 | img.src = images[index + 1]; |
| 1092 | - img.alt = `Trip Image ${index + 2}`; | |
| 1325 | + /* translators: %d: image sequence number in the trip gallery. */ | |
| 1326 | + img.alt = sprintf(__('Trip Image %d', 'yatra'), index + 2); | |
| 1093 | 1327 | } |
| 1094 | 1328 | }); |
| 1095 | 1329 | } |
| 1096 | 1330 | |
| @@ -1095,8 +1329,9 @@ | ||
| 1095 | 1329 | } |
| 1096 | 1330 | |
| 1097 | 1331 | updateSideVideos(sideImages) { |
| 1098 | 1332 | const videos = this.mediaData.videos || []; |
| 1333 | + const fallbackVideoTitle = __('Trip Video', 'yatra'); | |
| 1099 | 1334 | sideImages.forEach((item, index) => { |
| 1100 | 1335 | const img = item.querySelector('img'); |
| 1101 | 1336 | if (img && videos[index + 1]) { |
| 1102 | 1337 | const video = videos[index + 1]; |
| @@ -1102,9 +1337,9 @@ | ||
| 1102 | 1337 | const video = videos[index + 1]; |
| 1103 | 1338 | if (video.thumbnail) { |
| 1104 | 1339 | img.src = video.thumbnail; |
| 1105 | 1340 | } |
| 1106 | - img.alt = video.title || 'Trip Video'; | |
| 1341 | + img.alt = video.title || fallbackVideoTitle; | |
| 1107 | 1342 | } |
| 1108 | 1343 | }); |
| 1109 | 1344 | } |
| 1110 | 1345 | |
| @@ -1109,8 +1344,9 @@ | ||
| 1109 | 1344 | } |
| 1110 | 1345 | |
| 1111 | 1346 | updateSideYoutubeVideos(sideImages) { |
| 1112 | 1347 | const youtubeVideos = this.mediaData.youtube_videos || []; |
| 1348 | + const fallbackYoutubeTitle = __('YouTube Video', 'yatra'); | |
| 1113 | 1349 | sideImages.forEach((item, index) => { |
| 1114 | 1350 | const img = item.querySelector('img'); |
| 1115 | 1351 | if (img && youtubeVideos[index + 1]) { |
| 1116 | 1352 | const video = youtubeVideos[index + 1]; |
| @@ -1116,9 +1352,9 @@ | ||
| 1116 | 1352 | const video = youtubeVideos[index + 1]; |
| 1117 | 1353 | if (video.thumbnail) { |
| 1118 | 1354 | img.src = video.thumbnail; |
| 1119 | 1355 | } |
| 1120 | - img.alt = video.title || 'YouTube Video'; | |
| 1356 | + img.alt = video.title || fallbackYoutubeTitle; | |
| 1121 | 1357 | } |
| 1122 | 1358 | }); |
| 1123 | 1359 | } |
| 1124 | 1360 | |
| @@ -1123,8 +1359,9 @@ | ||
| 1123 | 1359 | } |
| 1124 | 1360 | |
| 1125 | 1361 | updateSideTours(sideImages) { |
| 1126 | 1362 | const virtualTours = this.mediaData.virtual_tours || []; |
| 1363 | + const fallbackTourTitle = __('360° Virtual Tour', 'yatra'); | |
| 1127 | 1364 | sideImages.forEach((item, index) => { |
| 1128 | 1365 | const img = item.querySelector('img'); |
| 1129 | 1366 | if (img && virtualTours[index + 1]) { |
| 1130 | 1367 | const tour = virtualTours[index + 1]; |
| @@ -1130,9 +1367,9 @@ | ||
| 1130 | 1367 | const tour = virtualTours[index + 1]; |
| 1131 | 1368 | if (tour.thumbnail) { |
| 1132 | 1369 | img.src = tour.thumbnail; |
| 1133 | 1370 | } |
| 1134 | - img.alt = tour.title || '360° Virtual Tour'; | |
| 1371 | + img.alt = tour.title || fallbackTourTitle; | |
| 1135 | 1372 | } |
| 1136 | 1373 | }); |
| 1137 | 1374 | } |
| 1138 | 1375 | |
| @@ -1137,14 +1374,15 @@ | ||
| 1137 | 1374 | } |
| 1138 | 1375 | |
| 1139 | 1376 | updateSideDocuments(sideImages) { |
| 1140 | 1377 | const documents = this.mediaData.documents || []; |
| 1378 | + const fallbackDocumentTitle = __('Trip Document', 'yatra'); | |
| 1141 | 1379 | sideImages.forEach((item, index) => { |
| 1142 | 1380 | const img = item.querySelector('img'); |
| 1143 | 1381 | if (img && documents[index + 1]) { |
| 1144 | 1382 | const doc = documents[index + 1]; |
| 1145 | 1383 | // Keep existing image for documents |
| 1146 | - img.alt = doc.title || 'Trip Document'; | |
| 1384 | + img.alt = doc.title || fallbackDocumentTitle; | |
| 1147 | 1385 | } |
| 1148 | 1386 | }); |
| 1149 | 1387 | } |
| 1150 | 1388 | |
| @@ -1186,8 +1424,9 @@ | ||
| 1186 | 1424 | class BookingSidebar { |
| 1187 | 1425 | constructor() { |
| 1188 | 1426 | this.checkAvailabilityBtn = null; |
| 1189 | 1427 | this.makeEnquiryBtn = null; |
| 1428 | + this.heroBookNowBtn = null; | |
| 1190 | 1429 | this.dateInput = null; |
| 1191 | 1430 | this.participantsSelect = null; |
| 1192 | 1431 | this.datepickerInstance = null; |
| 1193 | 1432 | this.init(); |
| @@ -1203,8 +1442,9 @@ | ||
| 1203 | 1442 | |
| 1204 | 1443 | setup() { |
| 1205 | 1444 | this.checkAvailabilityBtn = document.getElementById('check-availability-btn'); |
| 1206 | 1445 | this.makeEnquiryBtn = document.getElementById('open-enquiry-modal'); |
| 1446 | + this.heroBookNowBtn = document.getElementById('yatra-hero-book-now-btn'); | |
| 1207 | 1447 | this.dateInput = document.getElementById('travel_date'); |
| 1208 | 1448 | this.participantsSelect = document.querySelector('.yatra-participants-select'); |
| 1209 | 1449 | |
| 1210 | 1450 | if (!this.checkAvailabilityBtn) { |
| @@ -1224,8 +1464,29 @@ | ||
| 1224 | 1464 | this.handleCheckAvailability(); |
| 1225 | 1465 | }); |
| 1226 | 1466 | } |
| 1227 | 1467 | |
| 1468 | + // Hero "Book Now" (should behave like Check Availability) | |
| 1469 | + if (this.heroBookNowBtn) { | |
| 1470 | + this.heroBookNowBtn.addEventListener('click', (e) => { | |
| 1471 | + e.preventDefault(); | |
| 1472 | + const disabled = | |
| 1473 | + this.heroBookNowBtn.classList.contains('is-disabled') || | |
| 1474 | + this.heroBookNowBtn.getAttribute('aria-disabled') === 'true'; | |
| 1475 | + if (disabled) return; | |
| 1476 | + | |
| 1477 | + const bookingSidebar = document.getElementById('booking') || document.querySelector('.yatra-trip-sidebar'); | |
| 1478 | + if (bookingSidebar && bookingSidebar.scrollIntoView) { | |
| 1479 | + bookingSidebar.scrollIntoView({ behavior: 'smooth', block: 'start' }); | |
| 1480 | + } | |
| 1481 | + | |
| 1482 | + // Trigger the same availability flow | |
| 1483 | + if (this.checkAvailabilityBtn) { | |
| 1484 | + this.handleCheckAvailability(); | |
| 1485 | + } | |
| 1486 | + }); | |
| 1487 | + } | |
| 1488 | + | |
| 1228 | 1489 | // Make Enquiry button |
| 1229 | 1490 | if (this.makeEnquiryBtn) { |
| 1230 | 1491 | this.makeEnquiryBtn.addEventListener('click', (e) => { |
| 1231 | 1492 | e.preventDefault(); |
| @@ -1234,10 +1495,29 @@ | ||
| 1234 | 1495 | } |
| 1235 | 1496 | } |
| 1236 | 1497 | |
| 1237 | 1498 | initDateField() { |
| 1238 | - if (!this.dateInput || typeof flatpickr === 'undefined') return; | |
| 1499 | + if (!this.dateInput) return; | |
| 1239 | 1500 | |
| 1501 | + // Pro "date-as-dropdown" mode renders a <select> instead of a flatpickr | |
| 1502 | + // input. Wire it up as a native dropdown — emit `change` on selection so | |
| 1503 | + // the existing Check Availability listener picks the date up — and skip | |
| 1504 | + // the rest of the flatpickr setup. | |
| 1505 | + if (this.dateInput.tagName === 'SELECT') { | |
| 1506 | + const setEnabled = () => { | |
| 1507 | + if (this.checkAvailabilityBtn) { | |
| 1508 | + const hasValue = !!this.dateInput.value; | |
| 1509 | + this.checkAvailabilityBtn.disabled = !hasValue; | |
| 1510 | + this.checkAvailabilityBtn.classList.toggle('disabled', !hasValue); | |
| 1511 | + } | |
| 1512 | + }; | |
| 1513 | + setEnabled(); | |
| 1514 | + this.dateInput.addEventListener('change', setEnabled); | |
| 1515 | + return; | |
| 1516 | + } | |
| 1517 | + | |
| 1518 | + if (typeof flatpickr === 'undefined') return; | |
| 1519 | + | |
| 1240 | 1520 | const today = new Date(); |
| 1241 | 1521 | today.setHours(0, 0, 0, 0); |
| 1242 | 1522 | |
| 1243 | 1523 | // Format today's date as Y-m-d |
| @@ -1246,9 +1526,9 @@ | ||
| 1246 | 1526 | String(today.getDate()).padStart(2, '0'); |
| 1247 | 1527 | |
| 1248 | 1528 | // Set today's date as default value |
| 1249 | 1529 | this.dateInput.value = todayStr; |
| 1250 | - | |
| 1530 | + | |
| 1251 | 1531 | // Enable button since we have a default date |
| 1252 | 1532 | if (this.checkAvailabilityBtn) { |
| 1253 | 1533 | this.checkAvailabilityBtn.disabled = false; |
| 1254 | 1534 | this.checkAvailabilityBtn.classList.remove('disabled'); |
| @@ -1258,10 +1538,15 @@ | ||
| 1258 | 1538 | if (this.dateInput._flatpickr) { |
| 1259 | 1539 | this.dateInput._flatpickr.destroy(); |
| 1260 | 1540 | } |
| 1261 | 1541 | |
| 1542 | + const altFormat = getTripAltDateFormat(); | |
| 1543 | + | |
| 1262 | 1544 | this.datepickerInstance = flatpickr(this.dateInput, { |
| 1545 | + // Keep backend value stable (Y-m-d), only change display | |
| 1263 | 1546 | dateFormat: 'Y-m-d', |
| 1547 | + altInput: true, | |
| 1548 | + altFormat, | |
| 1264 | 1549 | minDate: today, |
| 1265 | 1550 | defaultDate: today, |
| 1266 | 1551 | enableTime: false, |
| 1267 | 1552 | clickOpens: true, |
| @@ -1268,11 +1553,9 @@ | ||
| 1268 | 1553 | allowInput: false, |
| 1269 | 1554 | static: false, |
| 1270 | 1555 | monthSelectorType: 'static', |
| 1271 | 1556 | animate: true, |
| 1272 | - locale: { | |
| 1273 | - firstDayOfWeek: 1 | |
| 1274 | - }, | |
| 1557 | + locale: getFlatpickrLocale(), | |
| 1275 | 1558 | onChange: (selectedDates, dateStr) => { |
| 1276 | 1559 | this.dateInput.value = dateStr; |
| 1277 | 1560 | |
| 1278 | 1561 | // Enable Check Availability button when date is selected |
| @@ -1286,18 +1569,30 @@ | ||
| 1286 | 1569 | // Make container clickable |
| 1287 | 1570 | const container = this.dateInput.closest('.yatra-booking-field-select'); |
| 1288 | 1571 | if (container) { |
| 1289 | 1572 | 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 { | |
| 1573 | + const openPicker = (e) => { | |
| 1574 | + // Do not interfere with other actionable controls in the same row | |
| 1575 | + if (e && e.target && e.target.closest && e.target.closest('button, a, select, textarea')) { | |
| 1576 | + return; | |
| 1577 | + } | |
| 1578 | + // Prevent container clicks from triggering other toggles | |
| 1579 | + if (e) { | |
| 1580 | + e.preventDefault(); | |
| 1581 | + e.stopPropagation(); | |
| 1582 | + } | |
| 1583 | + if (this.datepickerInstance && !this.datepickerInstance.isOpen) { | |
| 1297 | 1584 | this.datepickerInstance.open(); |
| 1298 | 1585 | } |
| 1299 | 1586 | }; |
| 1587 | + | |
| 1588 | + // Container click (icon/arrow/background) | |
| 1589 | + container.addEventListener('click', openPicker); | |
| 1590 | + | |
| 1591 | + // Flatpickr altInput is the visible field when altInput:true | |
| 1592 | + if (this.dateInput && this.dateInput._flatpickr && this.dateInput._flatpickr.altInput) { | |
| 1593 | + this.dateInput._flatpickr.altInput.addEventListener('click', openPicker); | |
| 1594 | + } | |
| 1300 | 1595 | } |
| 1301 | 1596 | } |
| 1302 | 1597 | |
| 1303 | 1598 | initInlineQuantityControls() { |
| @@ -1314,10 +1609,10 @@ | ||
| 1314 | 1609 | if (!minusBtn || !plusBtn) return; |
| 1315 | 1610 | |
| 1316 | 1611 | const updateButtonStates = () => { |
| 1317 | 1612 | const current = parseInt(input.value) || 1; |
| 1318 | - const min = parseInt(input.getAttribute('min')) || 1; | |
| 1319 | - const max = parseInt(input.getAttribute('max')) || 20; | |
| 1613 | + const min = window.yatraNumOr(input.getAttribute('min'), 1); | |
| 1614 | + const max = window.yatraNumOr(input.getAttribute('max'), 20); | |
| 1320 | 1615 | |
| 1321 | 1616 | minusBtn.disabled = current <= min; |
| 1322 | 1617 | plusBtn.disabled = current >= max; |
| 1323 | 1618 | }; |
| @@ -1327,9 +1622,9 @@ | ||
| 1327 | 1622 | e.preventDefault(); |
| 1328 | 1623 | e.stopPropagation(); |
| 1329 | 1624 | |
| 1330 | 1625 | const current = parseInt(input.value) || 1; |
| 1331 | - const min = parseInt(input.getAttribute('min')) || 1; | |
| 1626 | + const min = window.yatraNumOr(input.getAttribute('min'), 1); | |
| 1332 | 1627 | |
| 1333 | 1628 | if (current > min) { |
| 1334 | 1629 | input.value = current - 1; |
| 1335 | 1630 | updateButtonStates(); |
| @@ -1342,9 +1637,9 @@ | ||
| 1342 | 1637 | e.preventDefault(); |
| 1343 | 1638 | e.stopPropagation(); |
| 1344 | 1639 | |
| 1345 | 1640 | const current = parseInt(input.value) || 1; |
| 1346 | - const max = parseInt(input.getAttribute('max')) || 20; | |
| 1641 | + const max = window.yatraNumOr(input.getAttribute('max'), 20); | |
| 1347 | 1642 | |
| 1348 | 1643 | if (current < max) { |
| 1349 | 1644 | input.value = current + 1; |
| 1350 | 1645 | updateButtonStates(); |
| @@ -1396,9 +1691,9 @@ | ||
| 1396 | 1691 | |
| 1397 | 1692 | if (!display || !selector) return; |
| 1398 | 1693 | |
| 1399 | 1694 | // Get all traveler category inputs dynamically |
| 1400 | - const travelerInputs = selector.querySelectorAll('input[id^="traveler_"]'); | |
| 1695 | + const travelerInputs = selector.querySelectorAll('input[id^="traveler_"], input[id^="mobile_traveler_"]'); | |
| 1401 | 1696 | |
| 1402 | 1697 | if (travelerInputs.length === 0) return; |
| 1403 | 1698 | |
| 1404 | 1699 | // Make display clickable |
| @@ -1418,21 +1713,25 @@ | ||
| 1418 | 1713 | const parts = []; |
| 1419 | 1714 | travelerInputs.forEach((input) => { |
| 1420 | 1715 | const value = parseInt(input.value || 0); |
| 1421 | 1716 | if (value > 0) { |
| 1422 | - const categoryLabel = input.getAttribute('data-category-label') || 'Traveler'; | |
| 1717 | + const categoryLabel = input.getAttribute('data-category-label') || __('Traveler', 'yatra'); | |
| 1423 | 1718 | parts.push(`${categoryLabel} x ${value}`); |
| 1424 | 1719 | } |
| 1425 | 1720 | }); |
| 1426 | 1721 | |
| 1427 | 1722 | if (parts.length === 0) { |
| 1428 | - // Default to first category with value 1 | |
| 1723 | + // Default to first category with value 1. `data-category-label` | |
| 1724 | + // is rendered server-side via the PHP textdomain, so it's | |
| 1725 | + // already translated. The "x 1" suffix is the only piece | |
| 1726 | + // we need to keep translatable here. | |
| 1429 | 1727 | const firstInput = travelerInputs[0]; |
| 1430 | 1728 | if (firstInput) { |
| 1431 | - const categoryLabel = firstInput.getAttribute('data-category-label') || 'Traveler'; | |
| 1432 | - display.textContent = `${categoryLabel} x 1`; | |
| 1729 | + const categoryLabel = firstInput.getAttribute('data-category-label') || __('Traveler', 'yatra'); | |
| 1730 | + /* translators: %s is the traveler-category label (e.g. "Adult", "Child"). Renders as "Adult x 1". */ | |
| 1731 | + display.textContent = __('%s x 1', 'yatra').replace('%s', categoryLabel); | |
| 1433 | 1732 | } else { |
| 1434 | - display.textContent = '1 Traveler'; | |
| 1733 | + display.textContent = __('1 Traveler', 'yatra'); | |
| 1435 | 1734 | } |
| 1436 | 1735 | } else { |
| 1437 | 1736 | display.textContent = parts.join(', '); |
| 1438 | 1737 | } |
| @@ -1446,9 +1745,19 @@ | ||
| 1446 | 1745 | e.preventDefault(); |
| 1447 | 1746 | e.stopPropagation(); |
| 1448 | 1747 | |
| 1449 | 1748 | const targetId = btn.getAttribute('data-target'); |
| 1450 | - const input = document.getElementById(targetId); | |
| 1749 | + let input = null; | |
| 1750 | + if (targetId) { | |
| 1751 | + try { | |
| 1752 | + input = selector.querySelector('#' + CSS.escape(targetId)); | |
| 1753 | + } catch (_) { | |
| 1754 | + // ignore | |
| 1755 | + } | |
| 1756 | + if (!input) { | |
| 1757 | + input = document.getElementById(targetId); | |
| 1758 | + } | |
| 1759 | + } | |
| 1451 | 1760 | if (!input) return; |
| 1452 | 1761 | |
| 1453 | 1762 | const current = parseInt(input.value || 0); |
| 1454 | 1763 | const min = parseInt(input.getAttribute('min') || 0); |
| @@ -1486,9 +1795,9 @@ | ||
| 1486 | 1795 | const availabilitySection = document.getElementById('availability'); |
| 1487 | 1796 | if (!availabilitySection) return; |
| 1488 | 1797 | |
| 1489 | 1798 | inputs.forEach((input) => { |
| 1490 | - const categoryId = input.id.replace('traveler_', ''); | |
| 1799 | + const categoryId = input.id.replace(/^mobile_/, '').replace('traveler_', ''); | |
| 1491 | 1800 | const value = parseInt(input.value) || 0; |
| 1492 | 1801 | |
| 1493 | 1802 | // Find all matching inputs in availability cards |
| 1494 | 1803 | const availInputs = availabilitySection.querySelectorAll(`.yatra-availability-category[data-category="${categoryId}"]`); |
| @@ -1516,9 +1825,9 @@ | ||
| 1516 | 1825 | categoryInputs.forEach((catInput) => { |
| 1517 | 1826 | const catValue = parseInt(catInput.value) || 0; |
| 1518 | 1827 | if (catValue > 0) { |
| 1519 | 1828 | const catRow = catInput.closest('.yatra-quantity-row'); |
| 1520 | - const categoryLabel = catRow ? catRow.querySelector('.yatra-quantity-title')?.textContent : 'Traveler'; | |
| 1829 | + const categoryLabel = catRow ? catRow.querySelector('.yatra-quantity-title')?.textContent : __('Traveler', 'yatra'); | |
| 1521 | 1830 | parts.push(`${categoryLabel} x ${catValue}`); |
| 1522 | 1831 | } |
| 1523 | 1832 | }); |
| 1524 | 1833 | if (parts.length > 0) { |
| @@ -1527,8 +1836,11 @@ | ||
| 1527 | 1836 | } |
| 1528 | 1837 | } |
| 1529 | 1838 | }); |
| 1530 | 1839 | }); |
| 1840 | + if (window.availabilitySection && typeof window.availabilitySection.updateAllCardTotals === 'function') { | |
| 1841 | + window.availabilitySection.updateAllCardTotals(); | |
| 1842 | + } | |
| 1531 | 1843 | }; |
| 1532 | 1844 | |
| 1533 | 1845 | // Close dropdown when clicking outside |
| 1534 | 1846 | const closeHandler = (e) => { |
| @@ -1590,14 +1902,14 @@ | ||
| 1590 | 1902 | |
| 1591 | 1903 | |
| 1592 | 1904 | // Basic validation |
| 1593 | 1905 | if (!date) { |
| 1594 | - alert('Please select a travel date'); | |
| 1906 | + alert(__('Please select a travel date', 'yatra')); | |
| 1595 | 1907 | return; |
| 1596 | 1908 | } |
| 1597 | 1909 | |
| 1598 | 1910 | if (totalTravelers === 0) { |
| 1599 | - alert('Please select number of travelers'); | |
| 1911 | + alert(__('Please select number of travelers', 'yatra')); | |
| 1600 | 1912 | return; |
| 1601 | 1913 | } |
| 1602 | 1914 | |
| 1603 | 1915 | // Show loading state |
| @@ -1604,9 +1916,17 @@ | ||
| 1604 | 1916 | if (this.checkAvailabilityBtn && !this.checkAvailabilityBtn.dataset.originalHtml) { |
| 1605 | 1917 | this.checkAvailabilityBtn.dataset.originalHtml = this.checkAvailabilityBtn.innerHTML; |
| 1606 | 1918 | } |
| 1607 | 1919 | 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...'; | |
| 1920 | + // Spinner SVG is fixed markup (not translatable). Only the | |
| 1921 | + // user-visible "Checking..." label runs through __() so it | |
| 1922 | + // ends up in the .pot with a clean msgid and translates with | |
| 1923 | + // the rest of the UI. | |
| 1924 | + this.checkAvailabilityBtn.innerHTML = | |
| 1925 | + '<svg class="animate-spin" width="18" height="18" fill="none" viewBox="0 0 24 24">' + | |
| 1926 | + '<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" opacity="0.25"></circle>' + | |
| 1927 | + '<path fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>' + | |
| 1928 | + '</svg> ' + __('Checking...', 'yatra'); | |
| 1609 | 1929 | this.checkAvailabilityBtn.disabled = true; |
| 1610 | 1930 | } |
| 1611 | 1931 | |
| 1612 | 1932 | // Get trip ID from data attribute or global variable |
| @@ -1646,11 +1966,12 @@ | ||
| 1646 | 1966 | if (!response.ok) { |
| 1647 | 1967 | // Try to get error message from response |
| 1648 | 1968 | return response.json().then(err => { |
| 1649 | 1969 | console.error('API Error:', err); |
| 1650 | - throw new Error(err.message || 'Failed to fetch availability'); | |
| 1970 | + throw new Error(err.message || __('Failed to fetch availability', 'yatra')); | |
| 1651 | 1971 | }).catch(() => { |
| 1652 | - throw new Error('Failed to fetch availability (Status: ' + response.status + ')'); | |
| 1972 | + // translators: %d: HTTP status code | |
| 1973 | + throw new Error(sprintf(__('Failed to fetch availability (Status: %d)', 'yatra'), response.status)); | |
| 1653 | 1974 | }); |
| 1654 | 1975 | } |
| 1655 | 1976 | return response.json(); |
| 1656 | 1977 | }) |
| @@ -1714,9 +2035,9 @@ | ||
| 1714 | 2035 | this.resetButton(); |
| 1715 | 2036 | }) |
| 1716 | 2037 | .catch(error => { |
| 1717 | 2038 | console.error('Error loading availability:', error); |
| 1718 | - alert('Failed to load availability. Please try again.'); | |
| 2039 | + alert(__('Failed to load availability. Please try again.', 'yatra')); | |
| 1719 | 2040 | this.resetButton(); |
| 1720 | 2041 | }); |
| 1721 | 2042 | } |
| 1722 | 2043 | |
| @@ -1765,13 +2086,14 @@ | ||
| 1765 | 2086 | // Create new menu item |
| 1766 | 2087 | const newItem = document.createElement('a'); |
| 1767 | 2088 | newItem.href = '#availability'; |
| 1768 | 2089 | newItem.className = 'yatra-sticky-nav-item'; |
| 2090 | + const availabilityLabel = __('Availability', 'yatra'); | |
| 1769 | 2091 | newItem.innerHTML = ` |
| 1770 | 2092 | <svg class="yatra-icon-sm" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
| 1771 | 2093 | <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/> |
| 1772 | 2094 | </svg> |
| 1773 | - <span>Availability</span> | |
| 2095 | + <span>${availabilityLabel}</span> | |
| 1774 | 2096 | `; |
| 1775 | 2097 | |
| 1776 | 2098 | // Insert before the target item (or after "What's Included" if no target) |
| 1777 | 2099 | if (insertBeforeItem && insertBeforeItem.parentNode) { |
| @@ -1838,9 +2160,9 @@ | ||
| 1838 | 2160 | if (this.checkAvailabilityBtn && !this.checkAvailabilityBtn.dataset.originalHtml) { |
| 1839 | 2161 | this.checkAvailabilityBtn.dataset.originalHtml = this.checkAvailabilityBtn.innerHTML; |
| 1840 | 2162 | } |
| 1841 | 2163 | 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...'; | |
| 2164 | + this.checkAvailabilityBtn.innerHTML = '<svg class="animate-spin" width="18" height="18" fill="none" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" opacity="0.25"></circle><path fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path></svg> ' + __('Loading...', 'yatra'); | |
| 1843 | 2165 | this.checkAvailabilityBtn.disabled = true; |
| 1844 | 2166 | } |
| 1845 | 2167 | |
| 1846 | 2168 | const availSection = document.getElementById('availability'); |
| @@ -1864,9 +2186,9 @@ | ||
| 1864 | 2186 | credentials: 'omit', |
| 1865 | 2187 | }) |
| 1866 | 2188 | .then(response => { |
| 1867 | 2189 | if (!response.ok) { |
| 1868 | - throw new Error('Failed to fetch availability'); | |
| 2190 | + throw new Error(__('Failed to fetch availability', 'yatra')); | |
| 1869 | 2191 | } |
| 1870 | 2192 | return response.json(); |
| 1871 | 2193 | }) |
| 1872 | 2194 | .then(data => { |
| @@ -1901,9 +2223,9 @@ | ||
| 1901 | 2223 | this.resetButton(); |
| 1902 | 2224 | }) |
| 1903 | 2225 | .catch(error => { |
| 1904 | 2226 | console.error('Error reloading availability:', error); |
| 1905 | - alert('Failed to reload availability. Please try again.'); | |
| 2227 | + alert(__('Failed to reload availability. Please try again.', 'yatra')); | |
| 1906 | 2228 | this.resetButton(); |
| 1907 | 2229 | }); |
| 1908 | 2230 | } |
| 1909 | 2231 | |
| @@ -2001,9 +2323,13 @@ | ||
| 2001 | 2323 | if (data && data.success) { |
| 2002 | 2324 | // Update departures available text |
| 2003 | 2325 | const departuresText = document.querySelector('.yatra-departures-available-text'); |
| 2004 | 2326 | if (departuresText && data.departures_count !== undefined) { |
| 2005 | - departuresText.textContent = data.departures_count + ' departure' + (data.departures_count !== 1 ? 's' : '') + ' available'; | |
| 2327 | + departuresText.textContent = sprintf( | |
| 2328 | + /* translators: %d: number of departures available. */ | |
| 2329 | + _n('%d departure available', '%d departures available', data.departures_count, 'yatra'), | |
| 2330 | + data.departures_count | |
| 2331 | + ); | |
| 2006 | 2332 | } |
| 2007 | 2333 | |
| 2008 | 2334 | // Update traveler dropdown HTML |
| 2009 | 2335 | const quantitySelector = document.getElementById('quantity-selector'); |
| @@ -2057,15 +2383,15 @@ | ||
| 2057 | 2383 | const row = travelerSelector.querySelector('.yatra-quantity-row[data-category-id="' + categoryId + '"]'); |
| 2058 | 2384 | if (row) { |
| 2059 | 2385 | const cardInput = row.querySelector('input[type="number"]'); |
| 2060 | 2386 | if (cardInput) { |
| 2061 | - const max = parseInt(cardInput.getAttribute('max')) || 99; | |
| 2387 | + const max = window.yatraNumOr(cardInput.getAttribute('max'), 99); | |
| 2062 | 2388 | cardInput.value = Math.min(value, max); |
| 2063 | 2389 | |
| 2064 | 2390 | // Update button states |
| 2065 | 2391 | const minusBtn = row.querySelector('.yatra-quantity-minus'); |
| 2066 | 2392 | const plusBtn = row.querySelector('.yatra-quantity-plus'); |
| 2067 | - const min = parseInt(cardInput.getAttribute('min')) || 0; | |
| 2393 | + const min = window.yatraNumOr(cardInput.getAttribute('min'), 0); | |
| 2068 | 2394 | if (minusBtn) minusBtn.disabled = cardInput.value <= min; |
| 2069 | 2395 | if (plusBtn) plusBtn.disabled = cardInput.value >= max; |
| 2070 | 2396 | } |
| 2071 | 2397 | } |
| @@ -2076,9 +2402,9 @@ | ||
| 2076 | 2402 | |
| 2077 | 2403 | } else if (simpleTravelers && numTravelersInput) { |
| 2078 | 2404 | // Simple pricing - sync total count |
| 2079 | 2405 | const totalCount = parseInt(numTravelersInput.value) || 1; |
| 2080 | - const max = parseInt(simpleTravelers.getAttribute('max')) || 99; | |
| 2406 | + const max = window.yatraNumOr(simpleTravelers.getAttribute('max'), 99); | |
| 2081 | 2407 | simpleTravelers.value = Math.min(totalCount, max); |
| 2082 | 2408 | |
| 2083 | 2409 | // Update button states |
| 2084 | 2410 | const controls = simpleTravelers.closest('.yatra-quantity-controls-inline'); |
| @@ -2084,9 +2410,9 @@ | ||
| 2084 | 2410 | const controls = simpleTravelers.closest('.yatra-quantity-controls-inline'); |
| 2085 | 2411 | if (controls) { |
| 2086 | 2412 | const minusBtn = controls.querySelector('.yatra-quantity-minus'); |
| 2087 | 2413 | const plusBtn = controls.querySelector('.yatra-quantity-plus'); |
| 2088 | - const min = parseInt(simpleTravelers.getAttribute('min')) || 1; | |
| 2414 | + const min = window.yatraNumOr(simpleTravelers.getAttribute('min'), 1); | |
| 2089 | 2415 | if (minusBtn) minusBtn.disabled = simpleTravelers.value <= min; |
| 2090 | 2416 | if (plusBtn) plusBtn.disabled = simpleTravelers.value >= max; |
| 2091 | 2417 | } |
| 2092 | 2418 | } |
| @@ -2119,9 +2445,9 @@ | ||
| 2119 | 2445 | } |
| 2120 | 2446 | } |
| 2121 | 2447 | }); |
| 2122 | 2448 | |
| 2123 | - display.textContent = parts.length > 0 ? parts.join(', ') : '0 Travelers'; | |
| 2449 | + display.textContent = parts.length > 0 ? parts.join(', ') : __('0 Travelers', 'yatra'); | |
| 2124 | 2450 | } |
| 2125 | 2451 | |
| 2126 | 2452 | /** |
| 2127 | 2453 | * Update availability card total price |
| @@ -2126,8 +2452,13 @@ | ||
| 2126 | 2452 | /** |
| 2127 | 2453 | * Update availability card total price |
| 2128 | 2454 | */ |
| 2129 | 2455 | updateAvailabilityCardTotals(itemId) { |
| 2456 | + if (window.availabilitySection && typeof window.availabilitySection.updateTotalPrice === 'function') { | |
| 2457 | + window.availabilitySection.updateTotalPrice(itemId); | |
| 2458 | + return; | |
| 2459 | + } | |
| 2460 | + | |
| 2130 | 2461 | const totalAmountEl = document.querySelector('.yatra-card-total-amount[data-item="' + itemId + '"]'); |
| 2131 | 2462 | const totalNoteEl = document.querySelector('.yatra-card-total-note[data-item="' + itemId + '"]'); |
| 2132 | 2463 | if (!totalAmountEl) return; |
| 2133 | 2464 | |
| @@ -2144,13 +2475,17 @@ | ||
| 2144 | 2475 | const input = row.querySelector('input[type="number"]'); |
| 2145 | 2476 | const price = parseFloat(row.getAttribute('data-price')) || 0; |
| 2146 | 2477 | const count = parseInt(input?.value) || 0; |
| 2147 | 2478 | const pricingMode = row.getAttribute('data-pricing-mode') || (input ? input.getAttribute('data-pricing-mode') : '') || 'per_person'; |
| 2148 | - | |
| 2479 | + const groupOverflow = row.getAttribute('data-group-overflow') || (input ? input.getAttribute('data-group-overflow') : '') || 'block'; | |
| 2480 | + const maxPax = parseInt(row.getAttribute('data-max-pax') || (input ? input.getAttribute('data-max-pax') : '')) || 0; | |
| 2481 | + | |
| 2149 | 2482 | if (pricingMode === 'per_group') { |
| 2150 | - // Per group: charge flat price once if any travelers | |
| 2483 | + // Per group: one flat price, or per block of maxPax when "per_block". | |
| 2151 | 2484 | if (count > 0) { |
| 2152 | - total += price; | |
| 2485 | + total += (groupOverflow === 'per_block' && maxPax > 0) | |
| 2486 | + ? price * Math.ceil(count / maxPax) | |
| 2487 | + : price; | |
| 2153 | 2488 | } |
| 2154 | 2489 | } else { |
| 2155 | 2490 | // Per person: charge per traveler |
| 2156 | 2491 | total += price * count; |
| @@ -2179,9 +2514,10 @@ | ||
| 2179 | 2514 | totalAmountEl.textContent = String(total.toFixed(2)); |
| 2180 | 2515 | } |
| 2181 | 2516 | |
| 2182 | 2517 | if (totalNoteEl) { |
| 2183 | - totalNoteEl.textContent = 'for ' + totalTravelers + ' traveler' + (totalTravelers !== 1 ? 's' : ''); | |
| 2518 | + /* translators: %d is the number of travelers. Singular form is "for 1 traveler", plural "for N travelers". */ | |
| 2519 | + totalNoteEl.textContent = sprintf(_n('for %d traveler', 'for %d travelers', totalTravelers, 'yatra'), totalTravelers); | |
| 2184 | 2520 | } |
| 2185 | 2521 | |
| 2186 | 2522 | // Update the card participants display |
| 2187 | 2523 | this.updateAvailabilityCardDisplay(itemId); |
| @@ -2317,8 +2653,10 @@ | ||
| 2317 | 2653 | today.setHours(0, 0, 0, 0); |
| 2318 | 2654 | |
| 2319 | 2655 | this.datepickerInstance = flatpickr(this.dateInput, { |
| 2320 | 2656 | dateFormat: 'Y-m-d', |
| 2657 | + altInput: true, | |
| 2658 | + altFormat: getTripAltDateFormat(), | |
| 2321 | 2659 | minDate: today, |
| 2322 | 2660 | enableTime: false, |
| 2323 | 2661 | clickOpens: true, |
| 2324 | 2662 | allowInput: false, |
| @@ -2324,11 +2662,9 @@ | ||
| 2324 | 2662 | allowInput: false, |
| 2325 | 2663 | static: false, |
| 2326 | 2664 | monthSelectorType: 'static', |
| 2327 | 2665 | animate: true, |
| 2328 | - locale: { | |
| 2329 | - firstDayOfWeek: 1 | |
| 2330 | - }, | |
| 2666 | + locale: getFlatpickrLocale(), | |
| 2331 | 2667 | onChange: (selectedDates, dateStr) => { |
| 2332 | 2668 | this.dateInput.value = dateStr; |
| 2333 | 2669 | } |
| 2334 | 2670 | }); |
| @@ -2336,18 +2672,25 @@ | ||
| 2336 | 2672 | // Make container clickable |
| 2337 | 2673 | const container = this.dateInput.closest('.yatra-booking-field-select'); |
| 2338 | 2674 | if (container) { |
| 2339 | 2675 | 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 { | |
| 2676 | + const openPicker = (e) => { | |
| 2677 | + if (e && e.target && e.target.closest && e.target.closest('button, a, select, textarea')) { | |
| 2678 | + return; | |
| 2679 | + } | |
| 2680 | + if (e) { | |
| 2681 | + e.preventDefault(); | |
| 2682 | + e.stopPropagation(); | |
| 2683 | + } | |
| 2684 | + if (this.datepickerInstance && !this.datepickerInstance.isOpen) { | |
| 2347 | 2685 | this.datepickerInstance.open(); |
| 2348 | 2686 | } |
| 2349 | 2687 | }; |
| 2688 | + | |
| 2689 | + container.addEventListener('click', openPicker); | |
| 2690 | + if (this.dateInput && this.dateInput._flatpickr && this.dateInput._flatpickr.altInput) { | |
| 2691 | + this.dateInput._flatpickr.altInput.addEventListener('click', openPicker); | |
| 2692 | + } | |
| 2350 | 2693 | } |
| 2351 | 2694 | } |
| 2352 | 2695 | |
| 2353 | 2696 | initTravelersField() { |
| @@ -2375,17 +2718,22 @@ | ||
| 2375 | 2718 | // Update display function |
| 2376 | 2719 | const updateDisplay = () => { |
| 2377 | 2720 | const adults = parseInt(adultsInput.value || 1); |
| 2378 | 2721 | const children = parseInt(childrenInput.value || 0); |
| 2722 | + // translators: %d: number of adults | |
| 2723 | + const adultPart = sprintf(_n('Adult x %d', 'Adult x %d', adults, 'yatra'), adults); | |
| 2724 | + // translators: %d: number of children | |
| 2725 | + const childPart = sprintf(_n('Child x %d', 'Child x %d', children, 'yatra'), children); | |
| 2379 | 2726 | let text = ''; |
| 2380 | 2727 | if (adults > 0 && children > 0) { |
| 2381 | - text = `Adult x ${adults}, Child x ${children}`; | |
| 2728 | + // translators: %1$s: adult count phrase, %2$s: child count phrase | |
| 2729 | + text = sprintf(__('%1$s, %2$s', 'yatra'), adultPart, childPart); | |
| 2382 | 2730 | } else if (adults > 0) { |
| 2383 | - text = `Adult x ${adults}`; | |
| 2731 | + text = adultPart; | |
| 2384 | 2732 | } else if (children > 0) { |
| 2385 | - text = `Child x ${children}`; | |
| 2733 | + text = childPart; | |
| 2386 | 2734 | } else { |
| 2387 | - text = 'Adult x 1'; | |
| 2735 | + text = sprintf(_n('Adult x %d', 'Adult x %d', 1, 'yatra'), 1); | |
| 2388 | 2736 | } |
| 2389 | 2737 | display.textContent = text; |
| 2390 | 2738 | }; |
| 2391 | 2739 | |
| @@ -2471,9 +2819,9 @@ | ||
| 2471 | 2819 | const email = formData.get('email'); |
| 2472 | 2820 | const message = formData.get('message'); |
| 2473 | 2821 | |
| 2474 | 2822 | if (!name || !email || !message) { |
| 2475 | - this.showMessage('error', 'Please fill in all required fields.'); | |
| 2823 | + this.showMessage('error', __('Please fill in all required fields.', 'yatra')); | |
| 2476 | 2824 | return; |
| 2477 | 2825 | } |
| 2478 | 2826 | |
| 2479 | 2827 | // Basic email validation |
| @@ -2478,22 +2826,22 @@ | ||
| 2478 | 2826 | |
| 2479 | 2827 | // Basic email validation |
| 2480 | 2828 | const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; |
| 2481 | 2829 | if (!emailRegex.test(email)) { |
| 2482 | - this.showMessage('error', 'Please enter a valid email address.'); | |
| 2830 | + this.showMessage('error', __('Please enter a valid email address.', 'yatra')); | |
| 2483 | 2831 | return; |
| 2484 | 2832 | } |
| 2485 | 2833 | |
| 2486 | 2834 | if (message.length < 10) { |
| 2487 | - this.showMessage('error', 'Please enter a message (at least 10 characters).'); | |
| 2835 | + this.showMessage('error', __('Please enter a message (at least 10 characters).', 'yatra')); | |
| 2488 | 2836 | return; |
| 2489 | 2837 | } |
| 2490 | 2838 | |
| 2491 | 2839 | // Show loading |
| 2492 | 2840 | const submitBtn = this.form.querySelector('.yatra-enquiry-submit'); |
| 2493 | - const originalText = submitBtn ? submitBtn.textContent : 'Send Enquiry'; | |
| 2841 | + const originalText = submitBtn ? submitBtn.textContent : __('Send Enquiry', 'yatra'); | |
| 2494 | 2842 | if (submitBtn) { |
| 2495 | - submitBtn.textContent = 'Sending...'; | |
| 2843 | + submitBtn.textContent = __('Sending...', 'yatra'); | |
| 2496 | 2844 | submitBtn.disabled = true; |
| 2497 | 2845 | } |
| 2498 | 2846 | |
| 2499 | 2847 | // Hide any existing messages |
| @@ -2502,8 +2850,15 @@ | ||
| 2502 | 2850 | } |
| 2503 | 2851 | |
| 2504 | 2852 | // Submit via REST API (public endpoint) |
| 2505 | 2853 | const payload = Object.fromEntries(formData.entries()); |
| 2854 | + // Ensure trip context is sent even if the hidden input is missing/empty. | |
| 2855 | + if ((!payload.trip_id || String(payload.trip_id) === '0') && window.yatraTripData?.tripId) { | |
| 2856 | + payload.trip_id = String(window.yatraTripData.tripId); | |
| 2857 | + } | |
| 2858 | + if (!payload.trip_slug && window.yatraTripData?.tripSlug) { | |
| 2859 | + payload.trip_slug = String(window.yatraTripData.tripSlug); | |
| 2860 | + } | |
| 2506 | 2861 | |
| 2507 | 2862 | // Build REST URL |
| 2508 | 2863 | const baseUrlRaw = (window.yatraTripData?.apiUrl || window.yatraTripData?.restUrl || window.wpApiSettings?.root || '/wp-json').replace(/\/$/, ''); |
| 2509 | 2864 | const enquiriesUrl = baseUrlRaw.includes('/yatra/v1') |
| @@ -2509,26 +2864,38 @@ | ||
| 2509 | 2864 | const enquiriesUrl = baseUrlRaw.includes('/yatra/v1') |
| 2510 | 2865 | ? `${baseUrlRaw}/enquiries` |
| 2511 | 2866 | : `${baseUrlRaw}/yatra/v1/enquiries`; |
| 2512 | 2867 | |
| 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 | - }) | |
| 2868 | + // reCAPTCHA v3: attach a fresh token when the enquiry form is protected. | |
| 2869 | + const yatraRc = window.yatraRecaptcha; | |
| 2870 | + const enquiryTokenPromise = (yatraRc && yatraRc.protects && yatraRc.protects('enquiry')) | |
| 2871 | + ? yatraRc.execute('enquiry') | |
| 2872 | + : Promise.resolve(''); | |
| 2873 | + | |
| 2874 | + enquiryTokenPromise | |
| 2875 | + .then(recaptchaToken => { | |
| 2876 | + if (recaptchaToken) { | |
| 2877 | + payload.recaptcha_token = recaptchaToken; | |
| 2878 | + } | |
| 2879 | + return fetch(enquiriesUrl, { | |
| 2880 | + method: 'POST', | |
| 2881 | + headers: { | |
| 2882 | + 'Content-Type': 'application/json', | |
| 2883 | + }, | |
| 2884 | + credentials: 'omit', // public endpoint | |
| 2885 | + body: JSON.stringify(payload), | |
| 2886 | + }); | |
| 2887 | + }) | |
| 2521 | 2888 | .then(response => response.json()) |
| 2522 | 2889 | .then(result => { |
| 2523 | 2890 | if (result.success) { |
| 2524 | - this.showMessage('success', result.message || result.data?.message || 'Thank you for your enquiry! We will get back to you soon.'); | |
| 2891 | + this.showMessage('success', result.message || result.data?.message || __('Thank you for your enquiry! We will get back to you soon.', 'yatra')); | |
| 2525 | 2892 | this.form.reset(); |
| 2526 | 2893 | |
| 2527 | 2894 | // Reset the travelers display |
| 2528 | 2895 | const display = document.getElementById('enquiry-participants-display'); |
| 2529 | 2896 | if (display) { |
| 2530 | - display.textContent = 'Adult x 1'; | |
| 2897 | + display.textContent = __('Adult x 1', 'yatra'); | |
| 2531 | 2898 | } |
| 2532 | 2899 | const adultsInput = document.getElementById('enquiry-adults'); |
| 2533 | 2900 | const childrenInput = document.getElementById('enquiry-children'); |
| 2534 | 2901 | if (adultsInput) adultsInput.value = 1; |
| @@ -2541,14 +2908,14 @@ | ||
| 2541 | 2908 | messageBox.style.display = 'none'; |
| 2542 | 2909 | } |
| 2543 | 2910 | }, 3000); |
| 2544 | 2911 | } else { |
| 2545 | - this.showMessage('error', result.message || result.data?.message || 'Failed to submit enquiry. Please try again.'); | |
| 2912 | + this.showMessage('error', result.message || result.data?.message || __('Failed to submit enquiry. Please try again.', 'yatra')); | |
| 2546 | 2913 | } |
| 2547 | 2914 | }) |
| 2548 | 2915 | .catch(error => { |
| 2549 | 2916 | console.error('Enquiry submission error:', error); |
| 2550 | - this.showMessage('error', 'An error occurred. Please try again later.'); | |
| 2917 | + this.showMessage('error', __('An error occurred. Please try again later.', 'yatra')); | |
| 2551 | 2918 | }) |
| 2552 | 2919 | .finally(() => { |
| 2553 | 2920 | if (submitBtn) { |
| 2554 | 2921 | submitBtn.textContent = originalText; |
| @@ -2613,21 +2980,33 @@ | ||
| 2613 | 2980 | this.updateSidebarPosition(); // Initial check |
| 2614 | 2981 | } |
| 2615 | 2982 | |
| 2616 | 2983 | 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 | |
| 2984 | + // Derive section IDs from the sticky nav items themselves rather | |
| 2985 | + // than hardcoding the list. The previous hardcoded version only knew | |
| 2986 | + // about the built-in sections (overview, itinerary, included, …) so | |
| 2987 | + // admin-defined CUSTOM tabs — whose ID is `custom_<timestamp>` — were | |
| 2988 | + // silently dropped: their nav-link still scrolled, but updateActiveNav | |
| 2989 | + // never highlighted them as you scrolled past, and the "is this | |
| 2990 | + // section on-screen" check below was always false for them. | |
| 2991 | + // | |
| 2992 | + // Reading from this.navItems means every enabled tab (built-in OR | |
| 2993 | + // custom) participates in active-state tracking without needing this | |
| 2994 | + // list updated for each new tab type. | |
| 2995 | + const seen = new Set(); | |
| 2996 | + const ids = []; | |
| 2997 | + this.navItems.forEach((item) => { | |
| 2998 | + const href = item.getAttribute('href'); | |
| 2999 | + if (!href || !href.startsWith('#')) return; | |
| 3000 | + const id = href.substring(1); | |
| 3001 | + if (!id || seen.has(id)) return; | |
| 3002 | + const element = document.getElementById(id); | |
| 3003 | + if (element && element.offsetParent !== null) { | |
| 3004 | + seen.add(id); | |
| 3005 | + ids.push(id); | |
| 3006 | + } | |
| 2629 | 3007 | }); |
| 3008 | + return ids; | |
| 2630 | 3009 | } |
| 2631 | 3010 | |
| 2632 | 3011 | attachEventListeners() { |
| 2633 | 3012 | // Handle clicks on nav items |
| @@ -2799,8 +3178,15 @@ | ||
| 2799 | 3178 | this.lastScrollTop = scrollTop; |
| 2800 | 3179 | } |
| 2801 | 3180 | |
| 2802 | 3181 | updateActiveNav() { |
| 3182 | + // `offsetTop` is relative to the closest positioned ancestor, not the | |
| 3183 | + // document. When a section lives inside a `position: relative` wrapper | |
| 3184 | + // (any of the layout containers on this page), offsetTop is wrong by | |
| 3185 | + // the wrapper's offset, so the "active" pill never lit up correctly | |
| 3186 | + // for some tabs — most visibly the newly-added custom tabs that sit | |
| 3187 | + // further down the DOM. `getBoundingClientRect().top + pageYOffset` | |
| 3188 | + // always gives the document-relative top regardless of ancestors. | |
| 2803 | 3189 | const scrollPos = window.scrollY + 150; |
| 2804 | 3190 | let activeSectionId = null; |
| 2805 | 3191 | |
| 2806 | 3192 | // Find which section is currently in view |
| @@ -2806,10 +3192,11 @@ | ||
| 2806 | 3192 | // Find which section is currently in view |
| 2807 | 3193 | this.sections.forEach((sectionId) => { |
| 2808 | 3194 | const section = document.getElementById(sectionId); |
| 2809 | 3195 | if (section) { |
| 2810 | - const sectionTop = section.offsetTop; | |
| 2811 | - const sectionBottom = sectionTop + section.offsetHeight; | |
| 3196 | + const rect = section.getBoundingClientRect(); | |
| 3197 | + const sectionTop = rect.top + window.pageYOffset; | |
| 3198 | + const sectionBottom = sectionTop + rect.height; | |
| 2812 | 3199 | |
| 2813 | 3200 | if (scrollPos >= sectionTop && scrollPos < sectionBottom) { |
| 2814 | 3201 | activeSectionId = sectionId; |
| 2815 | 3202 | } |
| @@ -2956,14 +3343,14 @@ | ||
| 2956 | 3343 | } |
| 2957 | 3344 | } |
| 2958 | 3345 | |
| 2959 | 3346 | if (travelers < 1) { |
| 2960 | - this.showBookingError(btn, 'Please select at least 1 traveler to continue.'); | |
| 3347 | + this.showBookingError(btn, __('Please select at least 1 traveler to continue.', 'yatra')); | |
| 2961 | 3348 | return; |
| 2962 | 3349 | } |
| 2963 | 3350 | |
| 2964 | 3351 | 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...'; | |
| 3352 | + btn.innerHTML = '<svg class="animate-spin" width="18" height="18" fill="none" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" opacity="0.25"></circle><path fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path></svg> ' + __('Processing...', 'yatra'); | |
| 2966 | 3353 | btn.disabled = true; |
| 2967 | 3354 | |
| 2968 | 3355 | if (!tripId || isNaN(parseInt(tripId)) || parseInt(tripId) <= 0) { |
| 2969 | 3356 | console.error('Invalid trip ID:', tripId); |
| @@ -2968,9 +3355,9 @@ | ||
| 2968 | 3355 | if (!tripId || isNaN(parseInt(tripId)) || parseInt(tripId) <= 0) { |
| 2969 | 3356 | console.error('Invalid trip ID:', tripId); |
| 2970 | 3357 | btn.innerHTML = originalText; |
| 2971 | 3358 | btn.disabled = false; |
| 2972 | - this.showBookingError(btn, 'Unable to process booking. Invalid trip data.'); | |
| 3359 | + this.showBookingError(btn, __('Unable to process booking. Invalid trip data.', 'yatra')); | |
| 2973 | 3360 | return; |
| 2974 | 3361 | } |
| 2975 | 3362 | |
| 2976 | 3363 | const pricingType = btn.getAttribute('data-pricing-type') || 'regular'; |
| @@ -3191,9 +3578,9 @@ | ||
| 3191 | 3578 | currency: window.yatraBookingData?.currency || 'USD', |
| 3192 | 3579 | num_items: sessionPayload.travelers || 1 |
| 3193 | 3580 | }); |
| 3194 | 3581 | |
| 3195 | - console.log('[Facebook Pixel] InitiateCheckout tracked for trip:', sessionPayload.trip_id); | |
| 3582 | + window.YATRA_DEBUG && console.log('[Facebook Pixel] InitiateCheckout tracked for trip:', sessionPayload.trip_id); | |
| 3196 | 3583 | } |
| 3197 | 3584 | |
| 3198 | 3585 | // Track Book Now click as begin_checkout (Google Analytics 4) |
| 3199 | 3586 | if (typeof gtag !== 'undefined' && window.yatraAdmin?.googleAnalytics?.measurementId) { |
| @@ -3211,9 +3598,9 @@ | ||
| 3211 | 3598 | quantity: sessionPayload.travelers || 1 |
| 3212 | 3599 | }] |
| 3213 | 3600 | }); |
| 3214 | 3601 | |
| 3215 | - console.log('[Google Analytics] begin_checkout tracked for trip:', sessionPayload.trip_id); | |
| 3602 | + window.YATRA_DEBUG && console.log('[Google Analytics] begin_checkout tracked for trip:', sessionPayload.trip_id); | |
| 3216 | 3603 | } |
| 3217 | 3604 | |
| 3218 | 3605 | // Set booking session via REST API |
| 3219 | 3606 | // credentials: 'same-origin' is required to send cookies for session |
| @@ -3244,9 +3631,9 @@ | ||
| 3244 | 3631 | // Error - show message and reset button |
| 3245 | 3632 | console.error('Booking session error:', data); |
| 3246 | 3633 | btn.innerHTML = originalText; |
| 3247 | 3634 | btn.disabled = false; |
| 3248 | - this.showBookingError(btn, data.message || 'Unable to process booking. Please try again.'); | |
| 3635 | + this.showBookingError(btn, data.message || __('Unable to process booking. Please try again.', 'yatra')); | |
| 3249 | 3636 | } |
| 3250 | 3637 | }) |
| 3251 | 3638 | .catch(error => { |
| 3252 | 3639 | console.error('Error setting booking session:', error); |
| @@ -3251,9 +3638,9 @@ | ||
| 3251 | 3638 | .catch(error => { |
| 3252 | 3639 | console.error('Error setting booking session:', error); |
| 3253 | 3640 | btn.innerHTML = originalText; |
| 3254 | 3641 | btn.disabled = false; |
| 3255 | - this.showBookingError(btn, 'Unable to process booking. Please try again.'); | |
| 3642 | + this.showBookingError(btn, __('Unable to process booking. Please try again.', 'yatra')); | |
| 3256 | 3643 | }); |
| 3257 | 3644 | } |
| 3258 | 3645 | |
| 3259 | 3646 | initTravelerSelectors() { |
| @@ -3285,18 +3672,21 @@ | ||
| 3285 | 3672 | categoryInputs.forEach((input) => { |
| 3286 | 3673 | const value = parseInt(input.value) || 0; |
| 3287 | 3674 | if (value > 0) { |
| 3288 | 3675 | const row = input.closest('.yatra-quantity-row'); |
| 3289 | - const categoryLabel = row ? row.querySelector('.yatra-quantity-title')?.textContent : 'Traveler'; | |
| 3676 | + const categoryLabel = row ? row.querySelector('.yatra-quantity-title')?.textContent : __('Traveler', 'yatra'); | |
| 3290 | 3677 | parts.push(`${categoryLabel} x ${value}`); |
| 3291 | 3678 | } |
| 3292 | 3679 | }); |
| 3293 | 3680 | |
| 3294 | 3681 | if (parts.length === 0) { |
| 3295 | - // Default to first category | |
| 3682 | + // Default to first category. The label is read from the | |
| 3683 | + // DOM (server-rendered → already translated). Only the | |
| 3684 | + // " x 1" suffix is the literal we have to localise here. | |
| 3296 | 3685 | 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`; | |
| 3686 | + const firstLabel = firstRow ? firstRow.querySelector('.yatra-quantity-title')?.textContent : __('Traveler', 'yatra'); | |
| 3687 | + /* translators: %s is the traveler-category label (e.g. "Adult", "Child"). Renders as "Adult x 1". */ | |
| 3688 | + display.textContent = __('%s x 1', 'yatra').replace('%s', firstLabel); | |
| 3299 | 3689 | } else { |
| 3300 | 3690 | display.textContent = parts.join(', '); |
| 3301 | 3691 | } |
| 3302 | 3692 | }; |
| @@ -3318,10 +3708,10 @@ | ||
| 3318 | 3708 | const isMinus = btn.classList.contains('yatra-quantity-minus'); |
| 3319 | 3709 | const isPlus = btn.classList.contains('yatra-quantity-plus'); |
| 3320 | 3710 | |
| 3321 | 3711 | let current = parseInt(input.value) || 0; |
| 3322 | - const min = parseInt(input.getAttribute('min')) || 0; | |
| 3323 | - const max = parseInt(input.getAttribute('max')) || 999; | |
| 3712 | + const min = window.yatraNumOr(input.getAttribute('min'), 0); | |
| 3713 | + const max = window.yatraNumOr(input.getAttribute('max'), 999); | |
| 3324 | 3714 | let newValue = current; |
| 3325 | 3715 | |
| 3326 | 3716 | if (isPlus && current < max) { |
| 3327 | 3717 | newValue = current + 1; |
| @@ -3393,10 +3783,10 @@ | ||
| 3393 | 3783 | const isMinus = btn.classList.contains('yatra-quantity-minus'); |
| 3394 | 3784 | const isPlus = btn.classList.contains('yatra-quantity-plus'); |
| 3395 | 3785 | |
| 3396 | 3786 | let current = parseInt(input.value) || 1; |
| 3397 | - const min = parseInt(input.getAttribute('min')) || 1; | |
| 3398 | - const max = parseInt(input.getAttribute('max')) || 999; | |
| 3787 | + const min = window.yatraNumOr(input.getAttribute('min'), 1); | |
| 3788 | + const max = window.yatraNumOr(input.getAttribute('max'), 999); | |
| 3399 | 3789 | let newValue = current; |
| 3400 | 3790 | |
| 3401 | 3791 | if (isPlus && current < max) { |
| 3402 | 3792 | newValue = current + 1; |
| @@ -3468,9 +3858,9 @@ | ||
| 3468 | 3858 | categoryInputs.forEach((input) => { |
| 3469 | 3859 | const value = parseInt(input.value) || 0; |
| 3470 | 3860 | if (value > 0) { |
| 3471 | 3861 | const row = input.closest('.yatra-quantity-row'); |
| 3472 | - const categoryLabel = row ? row.querySelector('.yatra-quantity-title')?.textContent : 'Traveler'; | |
| 3862 | + const categoryLabel = row ? row.querySelector('.yatra-quantity-title')?.textContent : __('Traveler', 'yatra'); | |
| 3473 | 3863 | parts.push(`${categoryLabel} x ${value}`); |
| 3474 | 3864 | |
| 3475 | 3865 | // Also update sidebar inputs if they exist |
| 3476 | 3866 | const categoryId = input.getAttribute('data-category'); |
| @@ -3506,8 +3896,9 @@ | ||
| 3506 | 3896 | // Check for traveler-based pricing first |
| 3507 | 3897 | const categoryInputs = this.section.querySelectorAll(`.yatra-availability-category[data-item="${itemIndex}"]`); |
| 3508 | 3898 | let totalPrice = 0; |
| 3509 | 3899 | let totalTravelers = 0; |
| 3900 | + let totalGroups = 0; // per-group categories: number of group prices charged | |
| 3510 | 3901 | |
| 3511 | 3902 | if (categoryInputs.length > 0) { |
| 3512 | 3903 | // Traveler-based pricing: sum up each category's price * quantity (respecting pricing mode) |
| 3513 | 3904 | categoryInputs.forEach((input) => { |
| @@ -3514,12 +3905,16 @@ | ||
| 3514 | 3905 | const quantity = parseInt(input.value) || 0; |
| 3515 | 3906 | const row = input.closest('.yatra-quantity-row'); |
| 3516 | 3907 | const price = row ? parseFloat(row.getAttribute('data-price')) || 0 : 0; |
| 3517 | 3908 | const pricingMode = (row ? row.getAttribute('data-pricing-mode') : null) || input.getAttribute('data-pricing-mode') || 'per_person'; |
| 3518 | - | |
| 3909 | + const groupOverflow = (row ? row.getAttribute('data-group-overflow') : null) || input.getAttribute('data-group-overflow') || 'block'; | |
| 3910 | + const maxPax = parseInt((row ? row.getAttribute('data-max-pax') : null) || input.getAttribute('data-max-pax')) || 0; | |
| 3911 | + | |
| 3519 | 3912 | if (pricingMode === 'per_group') { |
| 3520 | 3913 | if (quantity > 0) { |
| 3521 | - totalPrice += price; | |
| 3914 | + const blocks = (groupOverflow === 'per_block' && maxPax > 0) ? Math.ceil(quantity / maxPax) : 1; | |
| 3915 | + totalPrice += price * blocks; | |
| 3916 | + totalGroups += blocks; | |
| 3522 | 3917 | } |
| 3523 | 3918 | } else { |
| 3524 | 3919 | totalPrice += quantity * price; |
| 3525 | 3920 | } |
| @@ -3547,16 +3942,41 @@ | ||
| 3547 | 3942 | } else { |
| 3548 | 3943 | totalAmountElement.textContent = String(finalPrice.toFixed(2)); |
| 3549 | 3944 | } |
| 3550 | 3945 | |
| 3946 | + // Header price must match the chargeable amount (row totals + group discount), not static PHP. | |
| 3947 | + // Its unit follows the card's price label: "per person" → average per traveller; | |
| 3948 | + // "per group" → the price per group actually charged (never divided by headcount, | |
| 3949 | + // which would turn a flat $1,100 group price into "$550 per group" for two travellers). | |
| 3950 | + const cardRoot = this.section.querySelector('.yatra-availability-card[data-item="' + itemIndex + '"]'); | |
| 3951 | + const headerPriceWrap = cardRoot ? cardRoot.querySelector('.yatra-card-header-price') : null; | |
| 3952 | + const headerSaleEl = headerPriceWrap ? headerPriceWrap.querySelector('.yatra-sale-price') : null; | |
| 3953 | + if (headerSaleEl) { | |
| 3954 | + const headerUnit = headerPriceWrap.getAttribute('data-price-unit') || 'per_person'; | |
| 3955 | + let perPerson = 0; | |
| 3956 | + if (headerUnit === 'per_group') { | |
| 3957 | + perPerson = totalGroups > 0 ? finalPrice / totalGroups : (totalPrice > 0 ? totalPrice : 0); | |
| 3958 | + } else if (totalTravelers > 0) { | |
| 3959 | + perPerson = finalPrice / totalTravelers; | |
| 3960 | + } else if (totalPrice > 0) { | |
| 3961 | + perPerson = totalPrice; | |
| 3962 | + } | |
| 3963 | + if (perPerson > 0) { | |
| 3964 | + headerSaleEl.textContent = | |
| 3965 | + typeof yatra_format_price_js === 'function' ? yatra_format_price_js(perPerson) : String(perPerson.toFixed(2)); | |
| 3966 | + } | |
| 3967 | + } | |
| 3968 | + | |
| 3551 | 3969 | // Update the note text |
| 3552 | 3970 | const noteElement = this.section.querySelector( |
| 3553 | 3971 | `.yatra-card-total-note[data-item="${itemIndex}"]` |
| 3554 | 3972 | ); |
| 3555 | 3973 | if (noteElement) { |
| 3556 | - let travelerText = totalTravelers === 1 | |
| 3557 | - ? 'for 1 traveler' | |
| 3558 | - : `for ${totalTravelers} travelers`; | |
| 3974 | + let travelerText = sprintf( | |
| 3975 | + /* translators: %d is the number of travelers. Singular form is "for 1 traveler", plural "for N travelers". */ | |
| 3976 | + _n('for %d traveler', 'for %d travelers', totalTravelers, 'yatra'), | |
| 3977 | + totalTravelers | |
| 3978 | + ); | |
| 3559 | 3979 | |
| 3560 | 3980 | // Add group discount info if applied |
| 3561 | 3981 | if (groupDiscountApplied && groupDiscountAmount > 0) { |
| 3562 | 3982 | const discountText = (typeof yatra_format_price_js === 'function') |
| @@ -3561,9 +3981,13 @@ | ||
| 3561 | 3981 | if (groupDiscountApplied && groupDiscountAmount > 0) { |
| 3562 | 3982 | const discountText = (typeof yatra_format_price_js === 'function') |
| 3563 | 3983 | ? yatra_format_price_js(groupDiscountAmount) |
| 3564 | 3984 | : String(groupDiscountAmount.toFixed(2)); |
| 3565 | - travelerText += ` (Group discount: -${discountText})`; | |
| 3985 | + travelerText += sprintf( | |
| 3986 | + /* translators: %s: formatted discount amount. */ | |
| 3987 | + __(' (Group discount: -%s)', 'yatra'), | |
| 3988 | + discountText | |
| 3989 | + ); | |
| 3566 | 3990 | } |
| 3567 | 3991 | noteElement.textContent = travelerText; |
| 3568 | 3992 | } |
| 3569 | 3993 | |
| @@ -3582,23 +4006,9 @@ | ||
| 3582 | 4006 | discountPercentage: 0, |
| 3583 | 4007 | discountType: null |
| 3584 | 4008 | }; |
| 3585 | 4009 | |
| 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 | - | |
| 4010 | + const groupDiscounts = window.yatraTripData && window.yatraTripData.sidebarGroupDiscounts; | |
| 3601 | 4011 | if (!Array.isArray(groupDiscounts) || groupDiscounts.length === 0) return result; |
| 3602 | 4012 | |
| 3603 | 4013 | // Find the applicable discount based on total travelers |
| 3604 | 4014 | let applicableDiscount = null; |
| @@ -3653,8 +4063,23 @@ | ||
| 3653 | 4063 | return result; |
| 3654 | 4064 | } |
| 3655 | 4065 | |
| 3656 | 4066 | /** |
| 4067 | + * Recompute footer + header pricing for every availability card (e.g. after sidebar traveler sync). | |
| 4068 | + */ | |
| 4069 | + updateAllCardTotals() { | |
| 4070 | + if (!this.section) return; | |
| 4071 | + const seen = new Set(); | |
| 4072 | + this.section.querySelectorAll('.yatra-availability-card[data-item]').forEach((card) => { | |
| 4073 | + const id = card.getAttribute('data-item'); | |
| 4074 | + if (id && !seen.has(id)) { | |
| 4075 | + seen.add(id); | |
| 4076 | + this.updateTotalPrice(id); | |
| 4077 | + } | |
| 4078 | + }); | |
| 4079 | + } | |
| 4080 | + | |
| 4081 | + /** | |
| 3657 | 4082 | * Update group discount badge visibility |
| 3658 | 4083 | */ |
| 3659 | 4084 | updateGroupDiscountBadge(itemIndex, discountApplied, discountResult) { |
| 3660 | 4085 | // Find or create the group discount applied badge for this item |
| @@ -3677,10 +4102,14 @@ | ||
| 3677 | 4102 | } |
| 3678 | 4103 | |
| 3679 | 4104 | // Update badge content |
| 3680 | 4105 | const discountText = discountResult.discountType === 'percentage' |
| 3681 | - ? `${discountResult.discountPercentage}% group discount applied!` | |
| 3682 | - : `Group discount applied!`; | |
| 4106 | + ? sprintf( | |
| 4107 | + /* translators: %s: discount percentage. */ | |
| 4108 | + __('%s%% group discount applied!', 'yatra'), | |
| 4109 | + discountResult.discountPercentage | |
| 4110 | + ) | |
| 4111 | + : __('Group discount applied!', 'yatra'); | |
| 3683 | 4112 | 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 | 4113 | } else if (badge) { |
| 3685 | 4114 | badge.remove(); |
| 3686 | 4115 | } |
| @@ -3760,9 +4189,9 @@ | ||
| 3760 | 4189 | } |
| 3761 | 4190 | |
| 3762 | 4191 | // Show success feedback |
| 3763 | 4192 | const originalText = button.textContent; |
| 3764 | - button.textContent = 'Selected!'; | |
| 4193 | + button.textContent = __('Selected!', 'yatra'); | |
| 3765 | 4194 | button.style.background = '#10b981'; |
| 3766 | 4195 | setTimeout(() => { |
| 3767 | 4196 | button.textContent = originalText; |
| 3768 | 4197 | button.style.background = ''; |
| @@ -3787,9 +4216,11 @@ | ||
| 3787 | 4216 | const travelDate = travelDateInput && travelDateInput.value ? travelDateInput.value : undefined; |
| 3788 | 4217 | |
| 3789 | 4218 | const originalHTML = btn.innerHTML; |
| 3790 | 4219 | 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...'; | |
| 4220 | + // Spinner SVG is fixed markup; only the visible label is wrapped | |
| 4221 | + // so it ends up in the .pot and translates with the rest of the UI. | |
| 4222 | + btn.innerHTML = loadingSvg + __('Loading...', 'yatra'); | |
| 3792 | 4223 | btn.disabled = true; |
| 3793 | 4224 | |
| 3794 | 4225 | const url = buildAvailabilityTemplateUrl(tripId, { |
| 3795 | 4226 | date: travelDate, |
| @@ -3808,9 +4239,9 @@ | ||
| 3808 | 4239 | credentials: 'omit', |
| 3809 | 4240 | }) |
| 3810 | 4241 | .then((resp) => { |
| 3811 | 4242 | if (!resp.ok) { |
| 3812 | - throw new Error('Load more failed'); | |
| 4243 | + throw new Error(__('Load more failed', 'yatra')); | |
| 3813 | 4244 | } |
| 3814 | 4245 | return resp.json(); |
| 3815 | 4246 | }) |
| 3816 | 4247 | .then((data) => { |
| @@ -3847,9 +4278,11 @@ | ||
| 3847 | 4278 | return; |
| 3848 | 4279 | } |
| 3849 | 4280 | |
| 3850 | 4281 | 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)`; | |
| 4282 | + /* translators: %d: number of additional departures still available to load. */ | |
| 4283 | + const loadMoreLabel = sprintf(__('Load more departures (%d remaining)', 'yatra'), remaining); | |
| 4284 | + btn.innerHTML = `<svg width="18" height="18" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/></svg> ${loadMoreLabel}`; | |
| 3852 | 4285 | btn.disabled = false; |
| 3853 | 4286 | |
| 3854 | 4287 | loadMoreContainer.dataset.currentPage = String(nextPage); |
| 3855 | 4288 | loadMoreContainer.dataset.loaded = String(displayed); |
| @@ -3855,9 +4288,17 @@ | ||
| 3855 | 4288 | loadMoreContainer.dataset.loaded = String(displayed); |
| 3856 | 4289 | loadMoreContainer.dataset.total = String(total); |
| 3857 | 4290 | |
| 3858 | 4291 | if (countInfo) { |
| 3859 | - countInfo.textContent = `Showing ${displayed} of ${total} departures`; | |
| 4292 | + // Pagination summary. Two placeholders preserved as | |
| 4293 | + // %1$d / %2$d so translators can reorder them (some | |
| 4294 | + // locales prefer "Of N, showing M" word order). | |
| 4295 | + /* translators: %1$d is the number of departures shown so far; %2$d is the total available. */ | |
| 4296 | + countInfo.textContent = sprintf( | |
| 4297 | + __('Showing %1$d of %2$d departures', 'yatra'), | |
| 4298 | + displayed, | |
| 4299 | + total | |
| 4300 | + ); | |
| 3860 | 4301 | } |
| 3861 | 4302 | }) |
| 3862 | 4303 | .catch((err) => { |
| 3863 | 4304 | console.error('[Yatra] Load more failed:', err); |
| @@ -4021,9 +4462,9 @@ | ||
| 4021 | 4462 | if (!this.toggleAllBtn) return; |
| 4022 | 4463 | this.toggleAllBtn.classList.toggle('is-all-expanded', isExpanded); |
| 4023 | 4464 | const toggleText = this.toggleAllBtn.querySelector('.toggle-text'); |
| 4024 | 4465 | if (toggleText) { |
| 4025 | - toggleText.textContent = isExpanded ? 'Collapse All' : 'Expand All'; | |
| 4466 | + toggleText.textContent = isExpanded ? __('Collapse All', 'yatra') : __('Expand All', 'yatra'); | |
| 4026 | 4467 | } |
| 4027 | 4468 | } |
| 4028 | 4469 | } |
| 4029 | 4470 | |
| @@ -4136,9 +4577,10 @@ | ||
| 4136 | 4577 | const originalText = submitBtn.innerHTML; |
| 4137 | 4578 | |
| 4138 | 4579 | // Disable button and show loading |
| 4139 | 4580 | submitBtn.disabled = true; |
| 4140 | - submitBtn.innerHTML = '<span class="yatra-loading-spinner"></span> Submitting...'; | |
| 4581 | + submitBtn.innerHTML = | |
| 4582 | + '<span class="yatra-loading-spinner"></span> ' + __('Submitting...', 'yatra'); | |
| 4141 | 4583 | |
| 4142 | 4584 | try { |
| 4143 | 4585 | const formData = new FormData(this.form); |
| 4144 | 4586 | formData.append('action', 'yatra_submit_review'); |
| @@ -4152,9 +4594,9 @@ | ||
| 4152 | 4594 | const result = await response.json(); |
| 4153 | 4595 | |
| 4154 | 4596 | if (result.success) { |
| 4155 | 4597 | // Show success message |
| 4156 | - this.showMessage('success', result.data?.message || 'Thank you for your review!'); | |
| 4598 | + this.showMessage('success', result.data?.message || __('Thank you for your review!', 'yatra')); | |
| 4157 | 4599 | |
| 4158 | 4600 | // Check if this was an edit or new submission |
| 4159 | 4601 | const isEdit = this.form.querySelector('input[name="action_type"]')?.value === 'edit'; |
| 4160 | 4602 | |
| @@ -4171,13 +4613,13 @@ | ||
| 4171 | 4613 | ratingLabels.forEach(label => label.classList.add('selected')); |
| 4172 | 4614 | } |
| 4173 | 4615 | } |
| 4174 | 4616 | } else { |
| 4175 | - this.showMessage('error', result.data?.message || 'Failed to submit review. Please try again.'); | |
| 4617 | + this.showMessage('error', result.data?.message || __('Failed to submit review. Please try again.', 'yatra')); | |
| 4176 | 4618 | } |
| 4177 | 4619 | } catch (error) { |
| 4178 | 4620 | console.error('Review submission error:', error); |
| 4179 | - this.showMessage('error', 'An error occurred. Please try again.'); | |
| 4621 | + this.showMessage('error', __('An error occurred. Please try again.', 'yatra')); | |
| 4180 | 4622 | } finally { |
| 4181 | 4623 | submitBtn.disabled = false; |
| 4182 | 4624 | submitBtn.innerHTML = originalText; |
| 4183 | 4625 | } |
| @@ -4351,16 +4793,13 @@ | ||
| 4351 | 4793 | * Handles save/remove trip from wishlist functionality |
| 4352 | 4794 | */ |
| 4353 | 4795 | class WishlistHandler { |
| 4354 | 4796 | 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(); | |
| 4797 | + // Wishlist clicks are now handled exclusively by listing-wishlist.js | |
| 4798 | + // (which covers every .yatra-favorite-btn on listing, single-trip hero, | |
| 4799 | + // and similar-trips). Registering this second handler caused every click | |
| 4800 | + // to fire two REST requests racing against each other. | |
| 4801 | + return; | |
| 4363 | 4802 | } |
| 4364 | 4803 | |
| 4365 | 4804 | init() { |
| 4366 | 4805 | // Check if user is logged in and update button states |
| @@ -4443,15 +4882,15 @@ | ||
| 4443 | 4882 | |
| 4444 | 4883 | const data = await response.json(); |
| 4445 | 4884 | if (data.success) { |
| 4446 | 4885 | this.updateButtonState(true, tid); |
| 4447 | - this.showMessage(data.message || 'Trip saved to wishlist', 'success', btn); | |
| 4886 | + this.showMessage(data.message || __('Trip saved to wishlist', 'yatra'), 'success', btn); | |
| 4448 | 4887 | } else { |
| 4449 | - this.showMessage(data.message || 'Failed to save trip', 'error', btn); | |
| 4888 | + this.showMessage(data.message || __('Failed to save trip', 'yatra'), 'error', btn); | |
| 4450 | 4889 | } |
| 4451 | 4890 | } catch (error) { |
| 4452 | 4891 | console.error('Error saving trip:', error); |
| 4453 | - this.showMessage('An error occurred. Please try again.', 'error', btn); | |
| 4892 | + this.showMessage(__('An error occurred. Please try again.', 'yatra'), 'error', btn); | |
| 4454 | 4893 | } finally { |
| 4455 | 4894 | btn.disabled = false; |
| 4456 | 4895 | btn.classList.remove('loading'); |
| 4457 | 4896 | } |
| @@ -4473,15 +4912,15 @@ | ||
| 4473 | 4912 | |
| 4474 | 4913 | const data = await response.json(); |
| 4475 | 4914 | if (data.success) { |
| 4476 | 4915 | this.updateButtonState(false, tid); |
| 4477 | - this.showMessage(data.message || 'Trip removed from wishlist', 'success', btn); | |
| 4916 | + this.showMessage(data.message || __('Trip removed from wishlist', 'yatra'), 'success', btn); | |
| 4478 | 4917 | } else { |
| 4479 | - this.showMessage(data.message || 'Failed to remove trip', 'error', btn); | |
| 4918 | + this.showMessage(data.message || __('Failed to remove trip', 'yatra'), 'error', btn); | |
| 4480 | 4919 | } |
| 4481 | 4920 | } catch (error) { |
| 4482 | 4921 | console.error('Error removing trip:', error); |
| 4483 | - this.showMessage('An error occurred. Please try again.', 'error', btn); | |
| 4922 | + this.showMessage(__('An error occurred. Please try again.', 'yatra'), 'error', btn); | |
| 4484 | 4923 | } finally { |
| 4485 | 4924 | btn.disabled = false; |
| 4486 | 4925 | btn.classList.remove('loading'); |
| 4487 | 4926 | } |
| @@ -4495,9 +4934,9 @@ | ||
| 4495 | 4934 | const buttons = document.querySelectorAll(selector); |
| 4496 | 4935 | buttons.forEach(btn => { |
| 4497 | 4936 | if (isSaved) { |
| 4498 | 4937 | btn.classList.add('saved', 'is-saved'); |
| 4499 | - btn.setAttribute('aria-label', 'Remove from favorites'); | |
| 4938 | + btn.setAttribute('aria-label', __('Remove from favorites', 'yatra')); | |
| 4500 | 4939 | // Fill the heart icon and change color |
| 4501 | 4940 | const svg = btn.querySelector('svg'); |
| 4502 | 4941 | if (svg) { |
| 4503 | 4942 | svg.style.fill = 'currentColor'; |
| @@ -4505,9 +4944,9 @@ | ||
| 4505 | 4944 | btn.style.color = '#ef4444'; |
| 4506 | 4945 | } |
| 4507 | 4946 | } else { |
| 4508 | 4947 | btn.classList.remove('saved', 'is-saved'); |
| 4509 | - btn.setAttribute('aria-label', 'Add to favorites'); | |
| 4948 | + btn.setAttribute('aria-label', __('Add to favorites', 'yatra')); | |
| 4510 | 4949 | // Unfill the heart icon |
| 4511 | 4950 | const svg = btn.querySelector('svg'); |
| 4512 | 4951 | if (svg) { |
| 4513 | 4952 | svg.style.fill = 'none'; |
| @@ -4531,13 +4970,17 @@ | ||
| 4531 | 4970 | if (modalContent) { |
| 4532 | 4971 | const loginMessage = document.createElement('div'); |
| 4533 | 4972 | loginMessage.className = 'yatra-login-prompt'; |
| 4534 | 4973 | loginMessage.style.cssText = 'padding: 40px; text-align: center;'; |
| 4974 | + const loginRequiredHeading = __('Login Required', 'yatra'); | |
| 4975 | + const loginRequiredBody = __('Please login to save trips to your wishlist.', 'yatra'); | |
| 4976 | + const loginButtonLabel = __('Login', 'yatra'); | |
| 4977 | + const cancelButtonLabel = __('Cancel', 'yatra'); | |
| 4535 | 4978 | 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> | |
| 4979 | + <h3 style="margin-bottom: 20px;">${loginRequiredHeading}</h3> | |
| 4980 | + <p style="margin-bottom: 30px;">${loginRequiredBody}</p> | |
| 4981 | + <a href="${window.yatraTripData?.loginUrl || '/wp-login.php'}" class="yatra-btn" style="display: inline-block; margin-right: 10px;">${loginButtonLabel}</a> | |
| 4982 | + <button type="button" class="yatra-btn-secondary" onclick="this.closest('.yatra-enquiry-modal').classList.remove('active'); document.body.style.overflow = '';">${cancelButtonLabel}</button> | |
| 4540 | 4983 | `; |
| 4541 | 4984 | modalContent.innerHTML = ''; |
| 4542 | 4985 | modalContent.appendChild(loginMessage); |
| 4543 | 4986 | } |
| @@ -4542,9 +4985,9 @@ | ||
| 4542 | 4985 | modalContent.appendChild(loginMessage); |
| 4543 | 4986 | } |
| 4544 | 4987 | } else { |
| 4545 | 4988 | // 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?')) { | |
| 4989 | + if (confirm(__('Please login to save trips to your wishlist. Would you like to go to the login page?', 'yatra'))) { | |
| 4547 | 4990 | window.location.href = window.yatraTripData?.loginUrl || '/wp-login.php'; |
| 4548 | 4991 | } |
| 4549 | 4992 | } |
| 4550 | 4993 | } |
| @@ -4633,12 +5076,91 @@ | ||
| 4633 | 5076 | this.exposeGlobals(); |
| 4634 | 5077 | |
| 4635 | 5078 | // Extra page-level behaviors |
| 4636 | 5079 | this.initHeroSmoothScroll(); |
| 5080 | + this.initGroupDiscountPopovers(); | |
| 4637 | 5081 | |
| 4638 | 5082 | return this.instances; |
| 4639 | 5083 | } |
| 4640 | 5084 | |
| 5085 | + /** | |
| 5086 | + * Group discount popover in booking sidebar / mobile sticky (tap outside to close, Esc to close). | |
| 5087 | + */ | |
| 5088 | + initGroupDiscountPopovers() { | |
| 5089 | + const syncOpenState = (wrap, open) => { | |
| 5090 | + const btn = wrap.querySelector('.yatra-group-discount-popover__trigger'); | |
| 5091 | + const panel = wrap.querySelector('.yatra-group-discount-popover__panel'); | |
| 5092 | + wrap.classList.toggle('is-open', open); | |
| 5093 | + if (btn) { | |
| 5094 | + btn.setAttribute('aria-expanded', open ? 'true' : 'false'); | |
| 5095 | + } | |
| 5096 | + if (panel) { | |
| 5097 | + panel.setAttribute('aria-hidden', open ? 'false' : 'true'); | |
| 5098 | + } | |
| 5099 | + }; | |
| 5100 | + | |
| 5101 | + const closeAll = (exceptWrap = null) => { | |
| 5102 | + document.querySelectorAll('[data-yatra-group-discount-popover]').forEach((w) => { | |
| 5103 | + if (exceptWrap && w === exceptWrap) { | |
| 5104 | + return; | |
| 5105 | + } | |
| 5106 | + syncOpenState(w, false); | |
| 5107 | + }); | |
| 5108 | + }; | |
| 5109 | + | |
| 5110 | + const attach = () => { | |
| 5111 | + document.querySelectorAll('[data-yatra-group-discount-popover]').forEach((wrap) => { | |
| 5112 | + const btn = wrap.querySelector('.yatra-group-discount-popover__trigger'); | |
| 5113 | + if (!btn || btn.dataset.yatraGdBound === '1') { | |
| 5114 | + return; | |
| 5115 | + } | |
| 5116 | + btn.dataset.yatraGdBound = '1'; | |
| 5117 | + btn.addEventListener('click', (e) => { | |
| 5118 | + e.preventDefault(); | |
| 5119 | + e.stopPropagation(); | |
| 5120 | + const open = !wrap.classList.contains('is-open'); | |
| 5121 | + if (open) { | |
| 5122 | + closeAll(wrap); | |
| 5123 | + syncOpenState(wrap, true); | |
| 5124 | + } else { | |
| 5125 | + syncOpenState(wrap, false); | |
| 5126 | + } | |
| 5127 | + }); | |
| 5128 | + }); | |
| 5129 | + | |
| 5130 | + if (document.documentElement.dataset.yatraGroupDiscountPopoverDocBound === '1') { | |
| 5131 | + return; | |
| 5132 | + } | |
| 5133 | + document.documentElement.dataset.yatraGroupDiscountPopoverDocBound = '1'; | |
| 5134 | + | |
| 5135 | + document.addEventListener('click', (e) => { | |
| 5136 | + if (!e.target.closest('[data-yatra-group-discount-popover]')) { | |
| 5137 | + closeAll(); | |
| 5138 | + } | |
| 5139 | + }); | |
| 5140 | + | |
| 5141 | + document.addEventListener('keydown', (e) => { | |
| 5142 | + if (e.key !== 'Escape') { | |
| 5143 | + return; | |
| 5144 | + } | |
| 5145 | + const openWrap = document.querySelector('[data-yatra-group-discount-popover].is-open'); | |
| 5146 | + if (openWrap) { | |
| 5147 | + syncOpenState(openWrap, false); | |
| 5148 | + const btn = openWrap.querySelector('.yatra-group-discount-popover__trigger'); | |
| 5149 | + if (btn) { | |
| 5150 | + btn.focus(); | |
| 5151 | + } | |
| 5152 | + } | |
| 5153 | + }); | |
| 5154 | + }; | |
| 5155 | + | |
| 5156 | + if (document.readyState === 'loading') { | |
| 5157 | + document.addEventListener('DOMContentLoaded', attach); | |
| 5158 | + } else { | |
| 5159 | + attach(); | |
| 5160 | + } | |
| 5161 | + } | |
| 5162 | + | |
| 4641 | 5163 | exposeGlobals() { |
| 4642 | 5164 | Object.entries(this.instances).forEach(([key, instance]) => { |
| 4643 | 5165 | window[key] = instance; |
| 4644 | 5166 | }); |
| @@ -4715,15 +5237,15 @@ | ||
| 4715 | 5237 | const videoUrl = link.getAttribute('href'); |
| 4716 | 5238 | if (!videoUrl || videoUrl === '#') return; |
| 4717 | 5239 | |
| 4718 | 5240 | const img = overlay.closest('.yatra-media-card').querySelector('img'); |
| 4719 | - const videoTitle = img ? img.alt : 'Itinerary Video'; | |
| 5241 | + const videoTitle = img ? img.alt : __('Itinerary Video', 'yatra'); | |
| 4720 | 5242 | |
| 4721 | 5243 | // Extract video ID from URL |
| 4722 | 5244 | let videoId = ''; |
| 4723 | 5245 | if (videoUrl.includes('youtube.com') || videoUrl.includes('youtu.be')) { |
| 4724 | 5246 | // YouTube video ID extraction |
| 4725 | - const matches = videoUrl.match(/(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/)([^&\n?#]+)/); | |
| 5247 | + const matches = videoUrl.match(/(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/|youtube\.com\/shorts\/|youtube\.com\/live\/)([^&\n?#\/]+)/); | |
| 4726 | 5248 | videoId = matches ? matches[1] : ''; |
| 4727 | 5249 | } else if (videoUrl.includes('vimeo.com')) { |
| 4728 | 5250 | // Vimeo video ID extraction |
| 4729 | 5251 | const matches = videoUrl.match(/vimeo\.com\/(?:.*#|.\/videos\/)?([0-9]+)/); |
| @@ -4836,9 +5358,9 @@ | ||
| 4836 | 5358 | try { |
| 4837 | 5359 | // Get download URL with signature using centralized helper |
| 4838 | 5360 | const response = await window.YatraApiHelper.getDownloadInfo(downloadId); |
| 4839 | 5361 | if (!response.ok) { |
| 4840 | - throw new Error('Download failed'); | |
| 5362 | + throw new Error(__('Download failed', 'yatra')); | |
| 4841 | 5363 | } |
| 4842 | 5364 | |
| 4843 | 5365 | const data = await response.json(); |
| 4844 | 5366 | |
| @@ -4855,9 +5377,9 @@ | ||
| 4855 | 5377 | } |
| 4856 | 5378 | } catch (error) { |
| 4857 | 5379 | console.error('Download error:', error); |
| 4858 | 5380 | // Show error message to user |
| 4859 | - this.showNotification('Download failed. Please try again.', 'error'); | |
| 5381 | + this.showNotification(__('Download failed. Please try again.', 'yatra'), 'error'); | |
| 4860 | 5382 | } |
| 4861 | 5383 | } |
| 4862 | 5384 | |
| 4863 | 5385 | async previewFile(downloadId) { |
| @@ -4867,15 +5389,15 @@ | ||
| 4867 | 5389 | |
| 4868 | 5390 | // Get download info using centralized helper |
| 4869 | 5391 | const response = await window.YatraApiHelper.getDownloadInfo(downloadId); |
| 4870 | 5392 | if (!response.ok) { |
| 4871 | - throw new Error('Failed to get file info'); | |
| 5393 | + throw new Error(__('Failed to get file info', 'yatra')); | |
| 4872 | 5394 | } |
| 4873 | 5395 | |
| 4874 | 5396 | const data = await response.json(); |
| 4875 | 5397 | |
| 4876 | 5398 | if (data.download_url) { |
| 4877 | - this.loadPreview(data.download_url, data.title || 'Preview'); | |
| 5399 | + this.loadPreview(data.download_url, data.title || __('Preview', 'yatra')); | |
| 4878 | 5400 | } else { |
| 4879 | 5401 | throw new Error('File URL not found'); |
| 4880 | 5402 | } |
| 4881 | 5403 | } catch (error) { |
| @@ -4880,9 +5402,9 @@ | ||
| 4880 | 5402 | } |
| 4881 | 5403 | } catch (error) { |
| 4882 | 5404 | console.error('Preview error:', error); |
| 4883 | 5405 | this.closePreviewModal(); |
| 4884 | - this.showNotification('Failed to load preview. Please try again.', 'error'); | |
| 5406 | + this.showNotification(__('Failed to load preview. Please try again.', 'yatra'), 'error'); | |
| 4885 | 5407 | } |
| 4886 | 5408 | } |
| 4887 | 5409 | |
| 4888 | 5410 | loadPreview(fileUrl, title) { |
| @@ -4910,13 +5432,15 @@ | ||
| 4910 | 5432 | previewContent.innerHTML = ` |
| 4911 | 5433 | <iframe src="${fileUrl}" style="width: 100%; height: 500px; border: none; border-radius: 8px;"></iframe> |
| 4912 | 5434 | `; |
| 4913 | 5435 | } else { |
| 5436 | + const previewUnavailableTitle = __('Preview not available for this file type', 'yatra'); | |
| 5437 | + const previewUnavailableHint = __('Please download the file to view its contents', 'yatra'); | |
| 4914 | 5438 | previewContent.innerHTML = ` |
| 4915 | 5439 | <div style="text-align: center; padding: 40px;"> |
| 4916 | 5440 | <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> | |
| 5441 | + <p style="color: #6b7280; margin-bottom: 16px;">${previewUnavailableTitle}</p> | |
| 5442 | + <p style="color: #9ca3af; font-size: 14px;">${previewUnavailableHint}</p> | |
| 4919 | 5443 | </div> |
| 4920 | 5444 | `; |
| 4921 | 5445 | } |
| 4922 | 5446 | } |
| @@ -4938,12 +5462,13 @@ | ||
| 4938 | 5462 | |
| 4939 | 5463 | // Reset loading state |
| 4940 | 5464 | const previewContent = document.getElementById('preview-content'); |
| 4941 | 5465 | if (previewContent) { |
| 5466 | + const loadingPreviewLabel = __('Loading preview...', 'yatra'); | |
| 4942 | 5467 | previewContent.innerHTML = ` |
| 4943 | 5468 | <div class="yatra-preview-loading"> |
| 4944 | 5469 | <div class="yatra-preview-spinner"></div> |
| 4945 | - <p>Loading preview...</p> | |
| 5470 | + <p>${loadingPreviewLabel}</p> | |
| 4946 | 5471 | </div> |
| 4947 | 5472 | `; |
| 4948 | 5473 | } |
| 4949 | 5474 | } |