1window.addEventListener("scroll", function(e) { 2 var t = this.scrollY; 3 var scrollToTop = document.getElementById("scrollToTop"); 4 if (scrollToTop) { 5 scrollToTop.style.display = t >= 100 ? "block" : "none"; 6 } 7}), 8 9window.onscroll = function() { 10 let nav = document.getElementById('navbar'); 11 // Pages without the main navbar (e.g. the voucher form) must not 12 // throw on every scroll event. 13 if (!nav) { 14 return; 15 } 16 let navLogo = document.getElementById('navbarLogo') 17 let sub = document.getElementById('subNavbar') 18 let options = document.getElementById('destination-options') 19 let logo = document.getElementById('navbar-image'); 20 let mobileSearch = document.getElementById('mobile-search'); 21 // let navItem = document.getElementsByClassName('dropdown'); 22 let dropDownItem = document.getElementById('dropdown'); 23 let tourDropDown = document.getElementById('tourdropdown') 24 let navBar = document.getElementById('navbarCollapse') 25 26 if ( window.scrollY > 100 ) { 27 nav.classList.add("fixed"); 28 navLogo.classList.remove("d-none") 29 // mobileSearch.classList.remove('d-none'); 30 if (logo) { 31 logo.src = "/images/logo_menu.png"; 32 } 33 if (sub) { 34 sub.classList.add("fixed-sub") 35 dropDownItem.addEventListener('mouseenter', function() { 36 sub.classList.remove("fixed-sub") 37 }); 38 sub.classList.add("fixed-sub") 39 tourDropDown.addEventListener('mouseenter', function() { 40 sub.classList.remove("fixed-sub") 41 }); 42 } 43 } else { 44 nav.classList.remove("fixed"); 45 navLogo.classList.add("d-none") 46 // mobileSearch.classList.add('d-none'); 47 if (logo) { 48 logo.src = "/images/logo.png"; 49 } 50 if(sub ) { 51 sub.classList.remove("fixed-sub") 52 } 53 } 54 } 55 56 57 let myCarousel = document.querySelectorAll('#featureContainer .carousel .carousel-item'); 58 59const updateCarousel = () => { 60 let minPerSlide; 61 62 // Check screen width and set minPerSlide accordingly 63 if (window.matchMedia("(max-width: 767.98px)").matches) { 64 minPerSlide = 1; // 1 item per slide for screens less than 768px 65 } else if (window.matchMedia("(min-width: 768px) and (max-width: 1023.98px)").matches) { 66 minPerSlide = 2; // 2 items per slide for screens between 768px and 1023px 67 } else { 68 minPerSlide = 3; // 3 items per slide for screens 1024px and larger 69 } 70 71 // When there is only one item, show it on its own (centered) instead of cloning it 72 if (myCarousel.length <= 1) { 73 minPerSlide = 1; 74 } 75 76 myCarousel.forEach((el) => { 77 let next = el.nextElementSibling; 78 79 // Clear any previously cloned items 80 while (el.children.length > 1) { 81 el.removeChild(el.lastChild); 82 } 83 84 // Clone and append items based on minPerSlide 85 for (let i = 1; i < minPerSlide; i++) { 86 if (!next) { 87 next = myCarousel[0]; // Loop back to the first item if no next sibling 88 } 89 let cloneChild = next.cloneNode(true); 90 el.appendChild(cloneChild.children[0]); 91 next = next.nextElementSibling; 92 } 93 }); 94 95 // Center the item whenever only one is shown per slide (single item or mobile) 96 var featureInner = myCarousel.length ? myCarousel[0].parentElement : null; 97 if (featureInner) { 98 featureInner.classList.toggle('is-single', minPerSlide === 1); 99 } 100}; 101 102// Initial call to set up the carousel 103updateCarousel(); 104 105//tourtype carousel 106 107let tourTypeCarousel = document.querySelectorAll('#tourTypeContainer .carousel .carousel-item'); 108 109const updateTourtypeCarousel = () => { 110 let minPerSlide; 111 112 // Check screen width and set minPerSlide accordingly 113 if (window.matchMedia("(max-width: 767.98px)").matches) { 114 minPerSlide = 1; // 1 item per slide for screens less than 768px 115 } else if (window.matchMedia("(min-width: 768px) and (max-width: 1023.98px)").matches) { 116 minPerSlide = 2; // 2 items per slide for screens between 768px and 1023px 117 } else { 118 minPerSlide = 3; // 3 items per slide for screens 1024px and larger 119 } 120 121 // When there is only one item, show it on its own (centered) instead of cloning it 122 if (tourTypeCarousel.length <= 1) { 123 minPerSlide = 1; 124 } 125 126 tourTypeCarousel.forEach((el) => { 127 let next = el.nextElementSibling; 128 129 // Clear any previously cloned items 130 while (el.children.length > 1) { 131 el.removeChild(el.lastChild); 132 } 133 134 // Clone and append items based on minPerSlide 135 for (let i = 1; i < minPerSlide; i++) { 136 if (!next) { 137 next = tourTypeCarousel[0]; // Loop back to the first item if no next sibling 138 } 139 let cloneChild = next.cloneNode(true); 140 el.appendChild(cloneChild.children[0]); 141 next = next.nextElementSibling; 142 } 143 }); 144 145 // Center the item whenever only one is shown per slide (single item or mobile) 146 var tourTypeInner = tourTypeCarousel.length ? tourTypeCarousel[0].parentElement : null; 147 if (tourTypeInner) { 148 tourTypeInner.classList.toggle('is-single', minPerSlide === 1); 149 } 150}; 151 152// Initial call to set up the carousel
153updateTourtypeCarousel(); 154 155// Update the carousels on window resize 156window.addEventListener('resize', updateCarousel); 157window.addEventListener('resize', updateTourtypeCarousel); 158 159 // blog carousel 160 let blogCarousel = document.querySelectorAll('#blogContainer .carousel .carousel-item'); 161 blogCarousel.forEach((el) => { 162 const minPerSlide = 2 163 let next = el.nextElementSibling 164 for (var i=1; i<minPerSlide; i++) { 165 if (!next) { 166 next = blogCarousel[0] 167 } 168 let cloneChild = next.cloneNode(true) 169 el.appendChild(cloneChild.children[0]) 170 next = next.nextElementSibling 171 } 172 }) 173 174 let tourViews = document.querySelectorAll('#tourContainer .carousel .carousel-item'); 175 tourViews.forEach((el) => { 176 const minPerSlide = 3 177 let next = el.nextElementSibling 178 for (var i=1; i<minPerSlide; i++) { 179 if (!next) { 180 next = tourViews[0] 181 } 182 let cloneChild = next.cloneNode(true) 183 el.appendChild(cloneChild.children[0]) 184 next = next.nextElementSibling 185 } 186 }) 187 188 function loginIcon(anchor) { 189 var icon = anchor.querySelector("i"); 190 let plusIcon = document.getElementsByClassName('fa-plus'); 191 } 192 193 function changeIcon(anchor, showLogin = false) { 194 var icon = document.getElementById('faPlus'); 195 let login = document.getElementById('show-login'); 196 197 if (showLogin) { 198 icon.classList.remove('fa-plus'); 199 icon.classList.add('fa-minus'); 200 login.classList.remove('visually-hidden'); 201 } else { 202 icon.classList.toggle('fa-plus'); 203 icon.classList.toggle('fa-minus'); 204 205 login.classList.toggle('visually-hidden', icon.classList.contains('fa-plus')); 206 } 207} 208 209function checkAll(e) { 210 var checkboxes = document.querySelectorAll('.destinationCheck'); 211 if (e.checked) { 212 for (var i = 0; i < checkboxes.length; i++) { 213 if (checkboxes[i].type == 'checkbox') { 214 checkboxes[i].checked = true; 215 } 216 } 217 } else { 218 for (var i = 0; i < checkboxes.length; i++) { 219 if (checkboxes[i].type == 'checkbox') { 220 checkboxes[i].checked = false; 221 } 222 } 223 } 224} 225 226const currentUrl = window.location.href; 227const newUrl = new URL(currentUrl); 228const pathname = newUrl.pathname; 229const segments = pathname.split('/'); 230const segment = segments[1]; 231 232// Debounce helper to avoid firing a search request on every keystroke 233function debounce(fn, delayMs) { 234 let timeoutId; 235 236 function debounced(...args) { 237 const ctx = this; 238 clearTimeout(timeoutId); 239 timeoutId = setTimeout(() => fn.apply(ctx, args), delayMs); 240 } 241 242 debounced.cancel = () => { 243 clearTimeout(timeoutId); 244 timeoutId = undefined; 245 }; 246 247 return debounced; 248} 249 250// Shared config (avoids global const/let redeclare issues if scripts are included multiple times) 251window.__UTW_SEARCH_CONFIG = window.__UTW_SEARCH_CONFIG || { debounceMs: 2000, minChars: 2 }; 252 253$(function() { 254 const { debounceMs, minChars } = window.__UTW_SEARCH_CONFIG; 255 const $searchInput = $('#search-input') 256 const $navSearch = $('.nav-search') 257 const $hitsContainer = $('#hits-container') 258 const $searchResults = $('.search-results') 259 let debouncedPerformSearch = null 260 let activeSearchController = null 261 262 $searchInput.on('focus', () => { 263 if ($searchInput.val() !== '') { 264 $navSearch.addClass('active') 265 } 266 }) 267 268 $searchInput.on('keyup', () => { 269 $hitsContainer.scrollTop(0) 270 }) 271 272 $('.close-search').on('click', evt => { 273 evt.preventDefault() 274 $searchInput.val('') 275 $navSearch.removeClass('active') 276 $searchResults.hide() // Hide search results when input is cleared 277 debouncedPerformSearch?.cancel?.() 278 activeSearchController?.abort?.() 279 }) 280 281 $searchInput.on('blur', evt => { 282 if (!evt.isDefaultPrevented) { 283 $navSearch.removeClass('active') 284 } 285 }) 286 287 const performSearch = async (query) => { 288 const trimmedQuery = String(query || '').trim() 289 if (trimmedQuery.length < minChars) { 290 $searchResults.hide() 291 return 292 } 293 294 try { 295 const controller = new AbortController() 296 activeSearchController?.abort?.() 297 activeSearchController = controller 298 299 const response = await fetch(`/api/blog-search/${encodeURIComponent(trimmedQuery)}`, { 300 signal: controller.signal 301 }) 302 const results = await response.json() 303 304 if (results.length > 0) { 305 renderHits(results) 306 $searchResults.show() 307 } else { 308 renderNoResultsMessage(trimmedQuery) 309 $searchResults.show() 310 } 311 } catch (error) { 312 if (error?.name === 'AbortError') return 313 console.error('Error fetching search results:', error) 314 $searchResults.hide() 315 } finally { 316 activeSearchController = null 317 } 318 } 319 320 debouncedPerformSearch = debounce(performSearch, debounceMs) 321
322 const renderHits = (hits) => { 323 const newBlogPosts = `<div class="p-0 m-0 search-results-heading"><i class="text-primary-100 p-1 fa-regular fa-newspaper" size="xl"></i>Blog Posts</div>` 324 const hitsHtml = hits.map(blog => ` 325 <a href="${microsite === 'us' ? `/blog/${blog.slug}` : `/${microsite}/blog/${blog.slug}`}"> 326 <div class="hit"> 327 <div class="hit-content d-flex flex-column"> 328 <small class="fw-semibold">${blog.name.toUpperCase()}</small> 329 <small> 330 Author: ${blog.author} 331 </small> 332 </div> 333 </div> 334 </a> 335 `).join('') 336 337 $hitsContainer.html(newBlogPosts + hitsHtml) 338 } 339 340 const renderNoResultsMessage = (query) => { 341 $hitsContainer.html(` 342 <div id="no-results-message"> 343 <a href="/searchalltours">We didn't find any results for the search <em>"${query}"</em>.</a> 344 </div> 345 `) 346 } 347 348 $searchInput.on('input', () => { 349 const query = String($searchInput.val() || '').trim() 350 if (query !== '') { 351 $navSearch.addClass('active') 352 } else { 353 $navSearch.removeClass('active') 354 $searchResults.hide() 355 debouncedPerformSearch?.cancel?.() 356 activeSearchController?.abort?.() 357 return 358 } 359 debouncedPerformSearch(query) 360 }) 361}) 362 363const toTitleCase = str => str.replace(/(?:^|\s)\w/g, match => match.toUpperCase()) 364 365$(function() { 366 const { debounceMs, minChars } = window.__UTW_SEARCH_CONFIG; 367 const $searchInput = $('#search-input1') 368 const $navSearch = $('.nav-search') 369 const $hitsContainer = $('#hits-container1') 370 const $searchResults = $('.search-results') 371 let debouncedPerformSearch = null 372 let activeSearchController = null 373 374 $searchInput.on('focus', () => { 375 if ($searchInput.val() !== '') { 376 $navSearch.addClass('active') 377 } 378 }) 379 380 $searchInput.on('keyup', () => { 381 $hitsContainer.scrollTop(0) 382 }) 383 384 $('.close-search').on('click', evt => { 385 evt.preventDefault() 386 $searchInput.val('') 387 $navSearch.removeClass('active') 388 $searchResults.hide() // Hide search results when input is cleared 389 debouncedPerformSearch?.cancel?.() 390 activeSearchController?.abort?.() 391 }) 392 393 $searchInput.on('blur', evt => { 394 if (!evt.isDefaultPrevented) { 395 $navSearch.removeClass('active') 396 } 397 }) 398 399 const performSearch = async (query) => { 400 const trimmedQuery = String(query || '').trim() 401 if (trimmedQuery.length < minChars) { 402 $searchResults.hide() 403 return 404 } 405 406 try { 407 const controller = new AbortController() 408 activeSearchController?.abort?.() 409 activeSearchController = controller 410 411 const response = await fetch(`/api/blog-search/${encodeURIComponent(trimmedQuery)}`, { 412 signal: controller.signal 413 }) 414 const results = await response.json() 415 416 if (results.length > 0) { 417 renderHits(results) 418 $searchResults.show() 419 } else { 420 renderNoResultsMessage(trimmedQuery) 421 $searchResults.show() 422 } 423 } catch (error) { 424 if (error?.name === 'AbortError') return 425 console.error('Error fetching search results:', error) 426 $searchResults.hide() 427 } finally { 428 activeSearchController = null 429 } 430 } 431 432 debouncedPerformSearch = debounce(performSearch, debounceMs) 433 434 const renderHits = (hits) => { 435 const newBlogPosts = `<div class="p-0 m-0 search-results-heading"><i class="text-primary-100 p-1 fa-regular fa-newspaper" size="xl"></i>Blog Posts</div>` 436 const hitsHtml = hits.map(blog => ` 437 <a href="${microsite === 'us' ? `/blog/${blog.slug}` : `/${microsite}/blog/${blog.slug}`}"> 438 <div class="hit"> 439 <div class="hit-content d-flex flex-column"> 440 <small class="fw-semibold">${blog.name.toUpperCase()}</small> 441 <small> 442 Author: ${blog.author} 443 </small> 444 </div> 445 </div> 446 </a> 447 `).join('') 448 449 $hitsContainer.html(newBlogPosts + hitsHtml) 450 } 451 452 const renderNoResultsMessage = (query) => { 453 $hitsContainer.html(` 454 <div id="no-results-message"> 455 <a href="${microsite === 'us' ? `/blog` : `/${microsite}/blog`}">We didn't find any results for the search <em>"${query}"</em>.</a> 456 </div> 457 `) 458 } 459 460 $searchInput.on('input', () => { 461 const query = String($searchInput.val() || '').trim() 462 if (query !== '') { 463 $navSearch.addClass('active') 464 } else { 465 $navSearch.removeClass('active') 466 $searchResults.hide() 467 debouncedPerformSearch?.cancel?.() 468 activeSearchController?.abort?.() 469 return 470 } 471 debouncedPerformSearch(query) 472 }) 473}) 474 475const toTitleCase1 = str => str.replace(/(?:^|\s)\w/g, match => match.toUpperCase()) 476 477$(function() { 478 const { debounceMs, minChars } = window.__UTW_SEARCH_CONFIG; 479 const $searchInput = $('#search-input2') 480 const $navSearch = $('.nav-search') 481 const $hitsContainer = $('#hits-container2') 482 const $searchResults = $('.search-results') 483 let debouncedPerformSearch = null 484 let activeSearchController = null 485 486 $searchInput.on('focus', () => { 487 if ($searchInput.val() !== '') { 488 $navSearch.addClass('active') 489 } 490 }) 491 492 $searchInput.on('keyup', () => { 493 $hitsContainer.scrollTop(0) 494 }) 495 496 $('.close-search').on('click', evt => { 497 evt.preventDefault() 498 $searchInput.val('') 499 $navSearch.removeClass('active') 500 $searchResults.hide() // Hide search results when input is cleared 501 debouncedPerformSearch?.cancel?.() 502 activeSearchController?.abort?.() 503 }) 504 505 $searchInput.on('blur', evt => { 506 if (!evt.isDefaultPrevented) { 507 $navSearch.removeClass('active') 508 } 509 }) 510 511 const performSearch = async (query) => { 512 const trimmedQuery = String(query || '').trim() 513 if (trimmedQuery.length < minChars) { 514 $searchResults.hide() 515 return 516 } 517 518 try { 519 const controller = new AbortController() 520 activeSearchController?.abort?.() 521 activeSearchController = controller 522 523 const response = await fetch(`/api/blog-search/${encodeURIComponent(trimmedQuery)}`, { 524 signal: controller.signal 525 }) 526 const results = await response.json() 527 528 if (results.length > 0) { 529 renderHits(results) 530 $searchResults.show() 531 } else { 532 renderNoResultsMessage(trimmedQuery) 533 $searchResults.show() 534 } 535 } catch (error) { 536 if (error?.name === 'AbortError') return 537 console.error('Error fetching search results:', error) 538 $searchResults.hide() 539 } finally { 540 activeSearchController = null 541 } 542 } 543 544 debouncedPerformSearch = debounce(performSearch, debounceMs) 545
546 const renderHits = (hits) => { 547 const newBlogPosts = `<div class="p-0 m-0 search-results-heading"><i class="text-primary-100 p-1 fa-regular fa-newspaper" size="xl"></i>Blog Posts</div>` 548 const hitsHtml = hits.map(blog => ` 549 <a href="${microsite === 'us' ? `/blog/${blog.slug}` : `/${microsite}/blog/${blog.slug}`}"> 550 <div class="hit"> 551 <div class="hit-content d-flex flex-column"> 552 <small class="fw-semibold">${blog.name.toUpperCase()}</small> 553 <small> 554 Author: ${blog.author} 555 </small> 556 </div> 557 </div> 558 </a> 559 `).join('') 560 561 $hitsContainer.html(newBlogPosts + hitsHtml) 562 } 563 564 const renderNoResultsMessage = (query) => { 565 $hitsContainer.html(` 566 <div id="no-results-message"> 567 <a href="/searchalltours">We didn't find any results for the search <em>"${query}"</em>.</a> 568 </div> 569 `) 570 } 571 572 $searchInput.on('input', () => { 573 const query = String($searchInput.val() || '').trim() 574 if (query !== '') { 575 $navSearch.addClass('active') 576 } else { 577 $navSearch.removeClass('active') 578 $searchResults.hide() 579 debouncedPerformSearch?.cancel?.() 580 activeSearchController?.abort?.() 581 return 582 } 583 debouncedPerformSearch(query) 584 }) 585}) 586 587const toTitleCase2 = str => str.replace(/(?:^|\s)\w/g, match => match.toUpperCase()) 588 589 590// navbar global search 591$(function() { 592 const { debounceMs, minChars } = window.__UTW_SEARCH_CONFIG; 593 const $globalSearchInput = $('#navbar-search-input') 594 const $globalNavSearch = $('.navbar-search') 595 const $globalContainer = $('#navbar-container') 596 const $globalSearch = $('.navbar-search-results') 597 let debouncedPerformSearch = null 598 let activeSearchController = null 599 600 // If this input was already initialized elsewhere (e.g. inline navbar scripts), skip to avoid duplicate requests 601 if ($globalSearchInput.data('searchInit')) return 602 $globalSearchInput.data('searchInit', true) 603 604 $globalSearchInput.on('focus', () => { 605 if ($globalSearchInput.val() !== '') { 606 $globalNavSearch.addClass('active') 607 } 608 }) 609 610 $globalSearchInput.on('keyup', () => { 611 $globalContainer.scrollTop(0) 612 }) 613 614 $('.close-search').on('click', evt => { 615 evt.preventDefault() 616 $globalSearchInput.val('') 617 $globalNavSearch.removeClass('active') 618 $globalSearch.hide() // Hide search results when input is cleared 619 debouncedPerformSearch?.cancel?.() 620 activeSearchController?.abort?.() 621 }) 622 623 $globalSearchInput.on('blur', evt => { 624 if (!evt.isDefaultPrevented) { 625 $globalNavSearch.removeClass('active') 626 } 627 }) 628 629 const performSearch = async (query) => { 630 const trimmedQuery = String(query || '').trim() 631 if (trimmedQuery.length < minChars) { 632 $globalSearch.hide() 633 return 634 } 635 636 try { 637 const controller = new AbortController() 638 activeSearchController?.abort?.() 639 activeSearchController = controller 640 641 const validFallbackMicrosites = ['eu', 'us', 'za', 'ca', 'au', 'nz', 'in', 'uk']; 642 const fallbackMicrosite = validFallbackMicrosites.includes(microsite) ? microsite : 'us'; 643 const response = await fetch(window.UTWGlobalSearch ? window.UTWGlobalSearch.searchUrl(trimmedQuery) : `/api/${fallbackMicrosite}/global-filter/${encodeURIComponent(trimmedQuery)}`, { 644 signal: controller.signal 645 }) 646 const results = await response.json() 647 648 if (results.tours.length > 0 || results.blogPosts.length > 0 || results.destinations.length > 0) { 649 renderHits(results) 650 $globalSearch.show() 651 } else { 652 renderNoResultsMessage(trimmedQuery) 653 $globalSearch.show() 654 } 655 } catch (error) { 656 if (error?.name === 'AbortError') return 657 console.error('Error fetching search results:', error) 658 $globalSearch.hide() 659 } finally { 660 activeSearchController = null 661 } 662 } 663 664 debouncedPerformSearch = debounce(performSearch, debounceMs) 665 666 const renderHits = (hits) => { 667 // Convert ALL-CAPS API names to Title Case to match trust bar heading style 668 const toTitleCase = (str) => { 669 if (!str) return ''; 670 return str.toLowerCase().replace(/(?:^|\s|–|-)\S/g, ch => ch.toUpperCase()); 671 }; 672 673 const availableToursHeader = `<div class="search-results-heading"><i class="fa-regular fa-route"></i>Available Tours</div>` 674 const countriesHeader = `<div class="search-results-heading"><i class="fa-regular fa-globe"></i>Countries</div>` 675 const blogPostsHeader = `<div class="search-results-heading"><i class="fa-regular fa-newspaper"></i>Blog Posts</div>` 676 677 const toursHtml = hits.tours.map(tour => { 678 const href = window.UTWGlobalSearch 679 ? window.UTWGlobalSearch.itemUrl('tour', tour.slug) 680 : (microsite === 'us' ? `/tour/${tour.slug}` : `/${microsite}/tour/${tour.slug}`); 681 return ` 682 <a href="${href}"> 683 <div class="hit"> 684 <div class="hit-icon-wrap"><i class="fa-regular fa-map"></i></div> 685 <div class="hit-content">
686 <span class="hit-title">${toTitleCase(tour.name)}</span> 687 <span class="hit-meta">${tour.days} Days • From ${tour.price || 'On Request'}</span> 688 </div> 689 </div> 690 </a> 691 `; 692 }).join('') 693 694 const destinationsHtml = hits.destinations.map(destination => { 695 const slug = destination.slug || destination.name.toLowerCase().replace(/\s+/g, '-'); 696 const href = window.UTWGlobalSearch 697 ? window.UTWGlobalSearch.itemUrl('destination', slug) 698 : (microsite === 'us' ? `/destination/${slug}` : `/${microsite}/destination/${slug}`); 699 return ` 700 <a href="${href}"> 701 <div class="hit"> 702 <div class="hit-icon-wrap"><i class="fa-regular fa-map-pin"></i></div> 703 <div class="hit-content"> 704 <span class="hit-title">${toTitleCase(destination.name)}</span> 705 <span class="hit-meta">${destination.tours_count || 0} Tours</span> 706 </div> 707 </div> 708 </a> 709 `; 710 }).join('') 711 712 const blogPostsHtml = hits.blogPosts.map(blogPost => { 713 const href = window.UTWGlobalSearch ? window.UTWGlobalSearch.itemUrl('blog', blogPost.slug) : (microsite === 'us' ? `/blog/${blogPost.slug}` : `/${microsite}/blog/${blogPost.slug}`); 714 return ` 715 <a href="${href}"> 716 <div class="hit"> 717 <div class="hit-icon-wrap"><i class="fa-regular fa-file-lines"></i></div> 718 <div class="hit-content"> 719 <span class="hit-title">${toTitleCase(blogPost.name)}</span> 720 <span class="hit-meta">Author: ${blogPost.author || 'Staff'}</span> 721 </div> 722 </div> 723 </a> 724 `; 725 }).join('') 726 727 let html = ''; 728 if (hits.destinations.length > 0) { 729 html += countriesHeader + destinationsHtml; 730 } 731 if (hits.tours.length > 0) { 732 html += availableToursHeader + toursHtml; 733 } 734 if (hits.blogPosts.length > 0) { 735 html += blogPostsHeader + blogPostsHtml; 736 } 737 738 $globalContainer.html(html); 739 } 740 741 742 const renderNoResultsMessage = (query) => { 743 $globalContainer.html(` 744 <div id="no-results-message"> 745 <p>We didn't find any results for the search <em>"${query}"</em>.</p> 746 </div> 747 `) 748 } 749 750 $globalSearchInput.on('input', () => { 751 const query = String($globalSearchInput.val() || '').trim() 752 if (query !== '') { 753 $globalNavSearch.addClass('active') 754 } else { 755 $globalNavSearch.removeClass('active') 756 $globalSearch.hide() 757 debouncedPerformSearch?.cancel?.() 758 activeSearchController?.abort?.() 759 return 760 } 761 debouncedPerformSearch(query) 762 }) 763}) 764 765const navbarGlobalInput = str => str.replace(/(?:^|\s)\w/g, match => match.toUpperCase()) 766 767 768JsLoadingOverlay.setOptions({ 769 overlayBackgroundColor: "#FFFFFF", 770 overlayOpacity: 0.6, 771 spinnerIcon: "ball-spin-clockwise-fade", 772 spinnerColor: "#EE3D23", 773 spinnerSize: "2x", 774 overlayIDName: "overlay", 775 spinnerIDName: "spinner", 776 offsetX: 0, 777 offsetY: 0, 778 containerID: null, 779 lockScroll: true, 780 overlayZIndex: 9998, 781 spinnerZIndex: 9999, 782}); 783 784var tourCards; 785var lastPage; 786function scrollToID() { 787 var scrollToHere = document.getElementById("emptycontainer"); 788 var scrollToY = 789 scrollToHere.getBoundingClientRect().top + window.pageYOffset; 790 window.scrollTo({ 791 top: scrollToY - 150, 792 behavior: "smooth", 793 }); 794} 795 796var originalTourImages = []; 797let currentMicrosite = microsite === "us" ? "" : microsite; 798function toggle(index, newImageUrl) { 799 // Select all span elements 800 var spans = document.getElementsByClassName("tourElement"); 801 var images = document.querySelectorAll("#tourImage img"); 802 803 if (index >= 0 && index < images.length) { 804 // Toggle between original and new source 805 if (!originalTourImages[index]) { 806 originalTourImages[index] = images[index].src; 807 images[index].src = newImageUrl; 808 } else { 809 // Revert to original source 810 images[index].src = originalTourImages[index]; 811 originalTourImages[index] = null; // Reset original source for future toggles 812 } 813 } 814 815 // Check if index is valid 816 if (index >= 0 && index < spans.length) { 817 // Store original text if not already stored 818 if (!spans[index].originalText) { 819 spans[index].originalText = spans[index].innerHTML; 820 } 821 822 // Toggle between original and new text 823 if (spans[index].innerHTML === spans[index].originalText) { 824 spans[index].innerHTML = "Images"; 825 } else { 826 spans[index].innerHTML = spans[index].originalText; 827 } 828 } 829} 830 831var newOriginalImageResources = []; 832 833function toggleTextAtIndex(index, newImageUrl) { 834 // Select all span elements 835 var spans = document.getElementsByClassName("myElement"); 836 var images = document.querySelectorAll("#imageContainer img"); 837 838 if (index >= 0 && index < images.length) { 839 // Toggle between original and new source 840 if (!newOriginalImageResources[index]) { 841 newOriginalImageResources[index] = images[index].src; 842 images[index].src = newImageUrl; 843 } else { 844 // Revert to original source 845 images[index].src = newOriginalImageResources[index]; 846 newOriginalImageResources[index] = null; // Reset original source for future toggles 847 } 848 } 849 850 // Check if index is valid 851 if (index >= 0 && index < spans.length) { 852 // Store original text if not already stored 853 if (!spans[index].originalText) { 854 spans[index].originalText = spans[index].innerHTML; 855 } 856 857 // Toggle between original and new text 858 if (spans[index].innerHTML === spans[index].originalText) { 859 spans[index].innerHTML = "Map"; 860 } else { 861 spans[index].innerHTML = spans[index].originalText; 862 } 863 } 864} 865 866let sliderData = { 867 priceRange: { from: 0, to: 100 }, 868 durationMonth: { from: 1, to: 30 }, 869 monthYearRange: { from: null, to: null } 870} 871 872const getTours = ( 873 microsite, 874 destination, 875 tourTypes, 876 on_sale = Boolean, 877 sort, 878 min_duration, 879 max_duration, 880 start_date, 881 end_date, 882 min_price, 883 max_price, 884 per_page, 885 page 886) => { 887 JsLoadingOverlay.show(); 888 const hidePriceCheckbox = document.querySelector('input.hidepoa:checked'); 889 axios 890 .get( 891 `/api/${microsite && microsite !== 'us' ? microsite + '/' : ''}tour-filter`, { 892 params: { 893 destination: destination, 894 tourTypes: tourTypes, 895 per_page: per_page, 896 sort: sort, 897 min_duration: min_duration, 898 max_duration: max_duration, 899 start_date: start_date, 900 end_date: end_date, 901 on_sale: on_sale, 902 min_price: min_price, 903 max_price: max_price, 904 page: page, 905 hide_price_on_request: hidePriceCheckbox ? 'true' : 'false', 906 } 907 } 908 ) 909 .then((response) => { 910 document.getElementById("tours-cards").innerHTML = ""; 911 this.tourCards = response.data.data; 912 913 document.getElementById("tour-number").innerHTML = 914 response.data.total; 915 916 document.getElementById("mobile-tour-number").innerHTML = 917 response.data.total; 918 document.getElementById("mobile-tour-number1").innerHTML = 919 response.data.total; 920 document.getElementById("mobile-tour-number2").innerHTML = 921 response.data.total; 922 this.lastPage = response.data.last_page; 923 924 925 function stripHtml(html) { 926 const temp = document.createElement('div'); 927 temp.innerHTML = html; 928 return temp.textContent || temp.innerText || ''; 929 } 930 931 function escapeAttribute(value) { 932 return String(value ?? '') 933 .replace(/&/g, '&') 934 .replace(/"/g, '"') 935 .replace(/</g, '<') 936 .replace(/>/g, '>'); 937 } 938 939 const currencySymbol = response.data.microsite.currency_symbol 940 const listType = window.utwAnalytics?.currentListType?.() || 'tour_listing'; 941 942 window.utwAnalytics?.trackTourListResponse?.(response.data, { 943 destination, 944 tourTypes, 945 on_sale, 946 sort, 947 min_duration, 948 max_duration, 949 start_date, 950 end_date, 951 min_price, 952 max_price, 953 page, 954 hide_price_on_request: hidePriceCheckbox ? 'true' : 'false', 955 }); 956 957 958 function createPagination(totalPages, currentPage) { 959 const container = document.getElementById( 960 "pagination-container" 961 ); 962 container.innerHTML = ""; 963 964 if (totalPages <= 1) { 965 return; 966 } 967 968 const maxVisiblePages = response.data.per_page; 969 const halfMaxVisiblePages = Math.floor(maxVisiblePages / 2); 970 let startPage = Math.max(1, currentPage - halfMaxVisiblePages); 971 const endPage = Math.min( 972 totalPages, 973 startPage + maxVisiblePages - 1 974 ); 975 976 // Adjust startPage to keep currentPage in the center if possible 977 if (endPage - startPage + 1 < maxVisiblePages) { 978 startPage = Math.max(1, endPage - maxVisiblePages + 1); 979 } 980 981 const paginationList = document.createElement("ul"); 982 paginationList.classList.add("pagination"); 983 984 // Add Previous Button 985 const previousButton = createPaginationItem( 986 currentPage - 1, 987 "<", 988 currentPage === 1 989 ); 990 paginationList.appendChild(previousButton); 991 992 // Add First Page 993 if (startPage > 1) { 994 const firstPage = createPaginationItem(1, "1", false); 995 paginationList.appendChild(firstPage); 996 if (startPage > 2) { 997 paginationList.appendChild(createTripDot()); 998 } 999 } 1000 1001 // Add Pages 1002 for (let i = startPage; i <= endPage; i++) { 1003 const pageItem = createPaginationItem( 1004 i, 1005 i.toString(), 1006 i === currentPage 1007 ); 1008 paginationList.appendChild(pageItem); 1009 } 1010 1011 // Add Last Page 1012 if (endPage < totalPages) { 1013 if (endPage < totalPages - 1) { 1014 paginationList.appendChild(createTripDot()); 1015 } 1016 const lastPage = createPaginationItem( 1017 totalPages, 1018 totalPages.toString(), 1019 false 1020 ); 1021 paginationList.appendChild(lastPage); 1022 } 1023 1024 // Add Next Button 1025 const nextButton = createPaginationItem( 1026 currentPage + 1, 1027 ">", 1028 currentPage === totalPages 1029 ); 1030 paginationList.appendChild(nextButton); 1031 1032 container.appendChild(paginationList); 1033 } 1034 1035 function createPaginationItem(pageNumber, text, disabled) { 1036 const listItem = document.createElement("li"); 1037 listItem.classList.add("page-item"); 1038 if (disabled) { 1039 listItem.classList.add("disabled"); 1040 } 1041 1042 const link = document.createElement("a"); 1043 link.classList.add("page-link"); 1044 link.href = "#"; 1045 link.textContent = text; 1046 1047 if (!disabled && pageNumber !== 0) { 1048 link.addEventListener("click", (event) => { 1049 event.preventDefault(); 1050 getTours( 1051 microsite, 1052 destination, 1053 tourTypes, 1054 on_sale = on_sale, 1055 sort = "popularity_asc", 1056 sliderData.durationMonth.from, 1057 sliderData.durationMonth.to, 1058 sliderData.monthYearRange.from, 1059 sliderData.monthYearRange.to, 1060 sliderData.priceRange.from, 1061 sliderData.priceRange.to, 1062 per_page, 1063 pageNumber 1064 ); 1065 }); 1066 } 1067 1068 listItem.appendChild(link); 1069 return listItem; 1070 } 1071 1072 function createTripDot() { 1073 const listItem = document.createElement("li"); 1074 listItem.classList.add("page-item"); 1075 listItem.classList.add("disabled"); 1076 listItem.innerHTML = '<span class="page-link">…</span>'; 1077 return listItem; 1078 } 1079 1080 1081 1082 createPagination( 1083 response.data.last_page, 1084 response.data.current_page 1085 ); 1086 1087 document.getElementById("tour-min-price").innerHTML = 1088 response.data.lowest_price ? `${response.data.microsite.currency_symbol} ${response.data.lowest_price}` : 'On Request'; 1089 1090 document.getElementById("mobile-min-price").innerHTML = 1091 response.data.lowest_price ? `${response.data.microsite.currency_symbol} ${response.data.lowest_price}` : 'On Request'; 1092 1093 document.getElementById("mobile-min-price1").innerHTML = 1094 response.data.lowest_price ? `${response.data.microsite.currency_symbol} ${response.data.lowest_price}` : 'On Request'; 1095 1096 document.getElementById("mobile-min-price2").innerHTML = 1097 response.data.lowest_price ? `${response.data.microsite.currency_symbol} ${response.data.lowest_price}` : 'On Request'; 1098 1099 1100 1101 const defaultMin = 470; 1102 const defaultMax = 11175; 1103 1104 // Get the price range values from response or use defaults 1105 const maxPrice = response.data.highest_price || defaultMax; 1106 const minPrice = response.data.lowest_price || defaultMin; 1107 1108 // Update both desktop and mobile price range elements 1109 const updatePriceRange = (elementId) => { 1110 const element = document.getElementById(elementId); 1111 if (element) { 1112 element.setAttribute('data-max', maxPrice); 1113 element.setAttribute('data-min', minPrice); 1114 } 1115 }; 1116 1117 // Update both elements 1118 updatePriceRange("price_range"); 1119 updatePriceRange("mobile_price_range"); 1120 1121 function getUniqueCountriesFromObject(inputObject) { 1122 // Extract values from the object, then split, trim, and convert them to lowercase
1123 const countries = Object.values(inputObject) 1124 .flat() // In case the values are arrays of countries, flatten them 1125 .map(country => country.trim().toLowerCase()); 1126 1127 // Use a Set to remove duplicates and then convert it back to an array 1128 const uniqueCountries = [...new Set(countries)]; 1129 1130 // Return the country names with the first letter capitalized as an array of HTML <a> tags 1131 return uniqueCountries.map(country => { 1132 const countrySlug = country.replace(/\s+/g, '-'); // Replace spaces with dashes 1133 const capitalizedCountry = country.charAt(0).toUpperCase() + country.slice(1); // Capitalize first letter 1134 return `<a class="link-orange" href="${microsite === 'us' ? `/destination/${countrySlug}` : `/${microsite}/destination/${countrySlug}`}">${capitalizedCountry}</a>`; 1135 }).join(', '); // Join them with commas to appear as a list 1136 } 1137 1138 1139 1140 function getFirstNumber(inputString) { 1141 var regex = /\d+/; 1142 var match = inputString.match(regex); 1143 return match ? parseInt(match[0], 10) : ""; 1144 } 1145 1146 if (response.data.data.length === 0) { 1147 console.log("No tours found"); 1148 let tourCard = ` 1149 <div class="d-flex justify-content-center mt-5"> 1150 <p class="text-center lead-big noresults"> 1151 <strong>Sorry, we couldn't find any tours matching your requirements. <br> Please <span>reset</span> your search results and <span class="text-primary-100">try again</span>.</strong> 1152 </p> 1153 </div> 1154 `; 1155 document.getElementById("tours-cards").innerHTML = tourCard; 1156 JsLoadingOverlay.hide(); 1157 } else { 1158 tourCards.forEach((tour, index) => { 1159 const tourId = tour.tour_id || tour.id || tour.slug; 1160 const position = ((Number(response.data.current_page || 1) - 1) * Number(response.data.per_page || tourCards.length || 1)) + index + 1; 1161 const discountPercent = Number(tour.price_details?.discount_percent ?? tour.price_details?.saving ?? 0); 1162 let tourCard = ` 1163 <div class="col-xl-4 col-lg-6 col-md-6 col-xs-6 col-12 bg-white tour-search-info mx-auto p-md-1 pb-2 mb-3"> 1164 <div class="m-0 p-0 tour-box box-shadow-dark d-flex flex-column justify-content-between" style="min-height: 100%;"> 1165 <div> 1166 <div class="tour-box-title p-2 text-uppercase"><a class="link-orange" 1167 data-track="select_item" 1168 data-tour-name="${escapeAttribute(tour.name)}" 1169 data-tour-id="${escapeAttribute(tourId)}" 1170 data-position="${position}" 1171 data-list-type="${escapeAttribute(listType)}" 1172 href="${microsite === 'us' ? `/tour/${tour.slug}` : `/${microsite}/tour/${tour.slug}`}">${ 1173 tour.name 1174 }</a></div> 1175 <div class="tour-search-thumb-container" id="imageContainer"> 1176 ${ discountPercent > 0 ? `<div class="corner" id="corner"><span>- ${discountPercent}%</span></div>` : ""} 1177 <a class="link-orange togglemapimage px-2 myElement" style="cursor: pointer;" onclick="toggleTextAtIndex(${index}, '${ 1178 tour.coupon_image 1179 }')">Image</a> 1180 <a href="${microsite === 'us' ? `/tour/${tour.slug}` : `/${microsite}/tour/${tour.slug}`}"> 1181 <img src="${ 1182 tour.map_image 1183 }" width="96%" height="220px" 1184 alt="Map" class="tour-search-thumb show myImage" 1185 data-track="gallery_view" 1186 data-image-index="1" 1187 data-tour-name="${escapeAttribute(tour.name)}
1187" 1188 data-tour-id="${escapeAttribute(tourId)}"> 1189 </a> 1190 </div> 1191 <div class="tour-box-destination px-1 m-1"> 1192 </div> 1193 <div class="col-md-12 p-2 toursummary"> 1194 <div class="row"> 1195 <p class="leadsmallheading col-4 col-lg-5 p-0 m-0"> Duration: </p> 1196 <p class="leadsmall col-8 col-lg-7 p-0 m-0">${ 1197 tour.days 1198 } Days</p> 1199 <p class="leadsmallheading col-4 col-lg-5 p-0 m-0"> Country(ies): </p> 1200 <p class="leadsmall col-8 col-lg-7 p-0 m-0"> 1201 <a class="link-orange" href="${microsite === 'us' ? `/destination/${tour.slug}` : `/${microsite}/destination/${tour.slug}`}"> 1202 ${getUniqueCountriesFromObject(tour.countries)} 1203 </a> 1204 </p> 1205 <p class="leadsmallheading col-4 col-lg-5 p-0 m-0"> Tour Type: </p> 1206 <p class="leadsmall col-8 col-lg-7 p-0 m-0"><a class="link-orange" 1207 href="${microsite === 'us' ? `/tourtype/${tour.tour_type.toLowerCase().replace(/\s+/g, '-')}` : `/${microsite}/tourtype/${tour.tour_type.toLowerCase().replace(/\s+/g, '-')}`}">${ 1208 tour.tour_type 1209 }</a></p> 1210 <p class="leadsmallheading col-4 col-lg-5 p-0 m-0"> Group size:</p> 1211 <p class="leadsmall col-8 col-lg-7 p-0 m-0">Min: ${ 1212 tour.group_min_size 1213 } | Max: ${tour.group_max_size}</p> 1214 <p class="leadsmallheading col-4 col-lg-5 p-0 m-0"> Meals:</p> 1215 <p class="leadsmall col-8 col-lg-7 p-0 m-0"> 1216 B - ${tour.meals.breakfast || 0}, 1217 L - ${tour.meals.lunch || 0}, 1218 D - ${tour.meals.dinner || 0} 1219 </p> 1220 <p class="leadsmallheading col-4 col-lg-5 p-0 m-0"> Highlights: </p> 1221 <p class="leadsmall col-8 col-lg-7 p-0 m-0">${ 1222 tour.highlights 1223 }</p> 1224 </div> 1225 </div> 1226 </div> 1227 <div class="row p-2" style="background:#d7d7d7;"> 1228 <div class="col-1 p-0 m-0 mt-2 d-flex jusitfy-content-center align-items-center"> 1229 <p class="vertical-text px-1 pb-2 m-0"> 1230 ${discountPercent > 0 ? "Deal" : "From"} 1231 </p> 1232 </div> 1233 <div class="col-5 p-0 m-0 mb-2" style="height:50px;"> 1234 <p class="${tour.price_details.offer_price !== null ? 'text-left m-0 p-0 mt-2 tour-box-bold' : 'mt-2'}"> 1235 ${tour.price_details.offer_price !== null 1236 ? `<span class="text-black">${response.data.microsite.currency_symbol}${tour.price_details.offer_price}</span>
1237 <span class="tour-box-bold-f">(p/p)</span>` 1238 : '<span class="text-left m-0 p-0 tour-box-request text-black">On Request</span>'} 1239 </p> 1240 ${discountPercent > 0 ? `<del class="text-danger"><small>${response.data.microsite.currency_symbol} ${tour.price_details.price}</small></del>` : ""} 1241 </div> 1242 <div class="col-6 align-self-center text-right d-flex justify-content-end"> 1243 <a href="${microsite === 'us' ? `/tour/${tour.slug}` : `/${microsite}/tour/${tour.slug}`}" class="btn btn-orange booknow-btn" 1244 data-track="select_item" 1245 data-tour-name="${escapeAttribute(tour.name)}" 1246 data-tour-id="${escapeAttribute(tourId)}" 1247 data-position="${position}" 1248 data-list-type="${escapeAttribute(listType)}" 1249 title="Book Now">
1249View Tour</a> 1250 </div> 1251 </div> 1252 </div> 1253 </div> 1254 `; 1255 document.getElementById("tours-cards").innerHTML += 1256 tourCard; 1257 JsLoadingOverlay.hide(); 1258 scrollToID(); 1259 }); 1260 1261 var selectedValueDisplay = document.getElementById( 1262 "selectedValueDisplay" 1263 ); 1264 1265 var priceRange = $("#price_range") 1266 .ionRangeSlider({ 1267 grid: true, 1268 min: 0, 1269 max: 100, 1270 from: 50, 1271 step: 5, 1272 prefix: response.data.microsite.currency_symbol, 1273 onFinish: function (data) { 1274 sliderData.priceRange = { from: data.from, to: data.to }; 1275 1276 const durationData = sliderData.durationMonth; 1277 1278 getTours( 1279 microsite, 1280 [...new Set(newCheckedValues)].join(','), 1281 [...new Set(newSelectedValues)].join(','), 1282 onSale.checked ? true : false, 1283 regionSort.value + sortSelecet.value, 1284 durationData.from, 1285 durationData.to, 1286 start_date, 1287 end_date, 1288 data.from, 1289 data.to, 1290 selectElement.value, 1291 page 1292 ); 1293 }, 1294 }) 1295 .data("priceRange"); 1296 1297 var priceRange = $("#mobile_price_range") 1298 .ionRangeSlider({ 1299 grid: true, 1300 min: 0, 1301 max: 100, 1302 from: 50, 1303 step: 5, 1304 prefix: response.data.microsite.currency_symbol, 1305 onFinish: function (data) { 1306 data.from, data.to; 1307 getTours( 1308 microsite, 1309 [...new Set(newCheckedValues)].join(','), 1310 [...new Set(newSelectedValues)].join(','), 1311 onSale.checked ? true : false, 1312 regionSort.value + sortSelecet.value, 1313 min_duration, 1314 max_duration, 1315 start_date, 1316 end_date, 1317 data.from, 1318 data.to, 1319 selectElement.value, 1320 page 1321 ); 1322 }, 1323 }) 1324 .data("mobilePriceRange"); 1325 1326 var durationMonth = $("#duration") 1327 .ionRangeSlider({ 1328 type: "double", 1329 grid: true, 1330 min: 1, 1331 max: 30, 1332 from: 1, 1333 step: 1, 1334 postfix: " days", 1335 onFinish: function (data) { 1336 sliderData.durationMonth = { from: data.from, to: data.to }; 1337 1338 const priceData = sliderData.priceRange; 1339 getTours( 1340 microsite, 1341 [...new Set(newCheckedValues)].join(','), 1342 [...new Set(newSelectedValues)].join(','), 1343 onSale.checked ? true : false, 1344 regionSort.value + sortSelecet.value, 1345 data.from, 1346 data.to, 1347 start_date, 1348 end_date, 1349 priceData.from, 1350 priceData.to, 1351 selectElement.value, 1352 page 1353 ); 1354 }, 1355 }) 1356 .data("durationMonth"); 1357 1358 var durationMonth = $("#mobile_duration") 1359 .ionRangeSlider({ 1360 type: "double", 1361 grid: true, 1362 min: 1, 1363 max: 30, 1364 from: 1, 1365 step: 1, 1366 postfix: " days", 1367 onFinish: function (data) { 1368 // getTours(microsite, destinations) 1369 getTours( 1370 microsite, 1371 [...new Set(newCheckedValues)].join(','), 1372 [...new Set(newSelectedValues)].join(','), 1373 onSale.checked ? true : false, 1374 regionSort.value + sortSelecet.value, 1375 data.from, 1376 data.to, 1377 start_date, 1378 end_date, 1379 min_price, 1380 max_price, 1381 selectElement.value, 1382 page 1383 ); 1384 }, 1385 }) 1386 .data("mobileDurationMonth"); 1387 1388 $(document).ready(function () { 1389 var today = new Date(); 1390 var minYear = today.getFullYear(); 1391 var minMonth = today.getMonth(); 1392 var maxYear = minYear + 2; 1393 var maxMonth = minMonth; 1394 1395 var minDate = new Date(minYear, minMonth, 1).getTime(); 1396 var maxDate = new Date(maxYear, maxMonth, 1).getTime(); 1397 1398 $("#month-year-range-slider").ionRangeSlider({ 1399 type: "double", 1400 min: minDate, 1401 max: maxDate, 1402 from: minDate, 1403 to: maxDate, 1404 prettify: function (num) { 1405 var date = new Date(num);
1406 var month = date.toLocaleString("default", { month: "short" }); 1407 var year = date.getFullYear(); 1408 return month + " " + year; 1409 }, 1410 onFinish: function (data) { 1411 var fromDate = new Date(data.from); 1412 var toDate = new Date(data.to); 1413 1414 var fromMonth = ("0" + (fromDate.getMonth() + 1)).slice(-2); 1415 var fromYear = fromDate.getFullYear(); 1416 var toMonth = ("0" + (toDate.getMonth() + 1)).slice(-2); 1417 var toYear = toDate.getFullYear(); 1418 1419 sliderData.monthYearRange = { 1420 from: `${fromYear}-${fromMonth}`, 1421 to: `${toYear}-${toMonth}` 1422 }; 1423 1424 const priceData = sliderData.priceRange; 1425 const durationData = sliderData.durationMonth; 1426 1427 getTours( 1428 microsite, 1429 [...new Set(newCheckedValues)].join(','), 1430 [...new Set(newSelectedValues)].join(','), 1431 onSale.checked ? true : false, 1432 regionSort.value + sortSelecet.value, 1433 durationData.from, 1434 durationData.to, 1435 sliderData.monthYearRange.from, 1436 sliderData.monthYearRange.to, 1437 priceData.from, 1438 priceData.to, 1439 selectElement.value, 1440 page 1441 ); 1442 }, 1443 }); 1444 }); 1445 1446 1447 function hasNumber(inputString) { 1448 var regex = /\d/; 1449 return regex.test(inputString); 1450 } 1451 } 1452 1453 1454 $(document).ready(function () { 1455 var today = new Date(); 1456 var minYear = today.getFullYear(); 1457 var minMonth = today.getMonth(); 1458 var maxYear = minYear + 2; 1459 var maxMonth = minMonth; 1460 1461 var minDate = new Date(minYear, minMonth, 1).getTime(); 1462 var maxDate = new Date(maxYear, maxMonth, 1).getTime(); 1463 1464 $("#mobile-month-year-range-slider").ionRangeSlider({ 1465 type: "double", 1466 min: minDate, 1467 max: maxDate, 1468 from: minDate, 1469 to: maxDate, 1470 prettify: function (num) { 1471 var date = new Date(num); 1472 var month = date.toLocaleString("default", { month: "short" }); 1473 var year = date.getFullYear(); 1474 return month + " " + year; 1475 }, 1476 onFinish: function (data) { 1477 var fromDate = new Date(data.from); 1478 var toDate = new Date(data.to); 1479 1480 var fromMonth = ("0" + (fromDate.getMonth() + 1)).slice( 1481 -2 1482 ); 1483 var fromYear = fromDate.getFullYear(); 1484 var toMonth = ("0" + (toDate.getMonth() + 1)).slice(-2); 1485 var toYear = toDate.getFullYear(); 1486 1487 const priceData = sliderData.priceRange; 1488 const durationData = sliderData.durationMonth; 1489 1490 getTours( 1491 microsite, 1492 [...new Set(newCheckedValues)].join(','), 1493 [...new Set(newSelectedValues)].join(','), 1494 onSale.checked ? true : false, 1495 regionSort.value + sortSelecet.value, 1496 min_duration, 1497 max_duration, 1498 fromYear + "-" + fromMonth, 1499 toYear + "-" + toMonth, 1500 min_price, 1501 max_price, 1502 selectElement.value, 1503 page 1504 ); 1505 }, 1506 }); 1507 }); 1508 }) 1509 .catch((error) => { 1510 console.log(error); 1511 }); 1512}; 1513 1514document.addEventListener("DOMContentLoaded", function() { 1515 const datePriceContainers = document.querySelectorAll('.list-group-flush.bg-white'); 1516 const showMoreBtn = document.getElementById('showMoreDates'); 1517 const showAllBtn = document.getElementById('showAllDates'); 1518 1519 if (!datePriceContainers.length || !showMoreBtn || !showAllBtn) { 1520 return; 1521 } 1522 1523 let visibleItems = 5; 1524 1525 // Initialize display 1526 function updateDisplay() {
1527 datePriceContainers.forEach((container, index) => { 1528 container.style.display = index < visibleItems ? 'block' : 'none'; 1529 }); 1530 1531 // Show/hide buttons based on remaining items 1532 if (datePriceContainers.length <= visibleItems) { 1533 showMoreBtn.style.display = 'none'; 1534 showAllBtn.style.display = 'none'; 1535 } else { 1536 showMoreBtn.style.display = 'inline-block'; 1537 showAllBtn.style.display = 'inline-block'; 1538 } 1539 1540 // Disable "Show More" if we're showing all or nearly all 1541 if (visibleItems >= datePriceContainers.length) { 1542 showMoreBtn.disabled = true; 1543 showAllBtn.disabled = true; 1544 } 1545 } 1546 1547 // Event listeners 1548 showMoreBtn.addEventListener('click', function() { 1549 visibleItems = Math.min(visibleItems + 3, datePriceContainers.length); 1550 updateDisplay(); 1551 }); 1552 1553 showAllBtn.addEventListener('click', function() { 1554 visibleItems = datePriceContainers.length; 1555 updateDisplay(); 1556 }); 1557 1558 // Initial display 1559 updateDisplay(); 1560}); 1561 1562 // show all the tour notes 1563 document.addEventListener("DOMContentLoaded", function () { 1564 const showMoreBtnDesktop = document.getElementById("showDateAndPrices"); 1565 const showAllBtnDesktop = document.getElementById("showAllDateAndPrices"); 1566 const showMoreBtnMobile = document.getElementById("showMoreDates"); 1567 const showAllBtnMobile = document.getElementById("showAllDates"); 1568 1569 const allBtn = document.querySelector('[data-filter="all"]'); 1570 const buttons = document.querySelectorAll("[data-filter]"); 1571 1572 let numItemsToShow = 5; 1573 let currentFilter = "all"; 1574 1575 function getVisibleItems() { 1576 if (window.innerWidth >= 576) { 1577 return document.querySelectorAll(".desktop-item"); 1578 } else { 1579 return document.querySelectorAll(".mobile-item"); 1580 } 1581 } 1582 1583 function updateDisplay() { 1584 const items = getVisibleItems(); 1585 let visibleCount = 0; 1586 const isAll = currentFilter === "all"; 1587 1588 items.forEach(item => { 1589 const rawCategory = item.getAttribute("data-category") || "all"; 1590 const category = rawCategory.trim().toLowerCase(); 1591 const match = isAll || category === currentFilter.toLowerCase(); 1592 1593 if (match) { 1594 if (isAll && visibleCount >= numItemsToShow) { 1595 item.style.display = "none"; 1596 } else { 1597 item.style.display = "block"; 1598 visibleCount++; 1599 } 1600 } else { 1601 item.style.display = "none"; 1602 } 1603 }); 1604 1605 const activeShowMoreBtn = window.innerWidth >= 576 ? showMoreBtnDesktop : showMoreBtnMobile; 1606 const activeShowAllBtn = window.innerWidth >= 576 ? showAllBtnDesktop : showAllBtnMobile; 1607 1608 // Hide all first 1609 if (showMoreBtnDesktop) showMoreBtnDesktop.style.display = "none"; 1610 if (showAllBtnDesktop) showAllBtnDesktop.style.display = "none"; 1611 if (showMoreBtnMobile) showMoreBtnMobile.style.display = "none"; 1612 if (showAllBtnMobile) showAllBtnMobile.style.display = "none"; 1613 1614 if (isAll) { 1615 if (items.length <= numItemsToShow) { 1616 // Keep them hidden 1617 } else { 1618 if (activeShowMoreBtn) activeShowMoreBtn.style.display = "inline-block"; 1619 if (activeShowAllBtn) activeShowAllBtn.style.display = "inline-block"; 1620 } 1621 } 1622 } 1623 1624 function setActiveButton(btn) { 1625 buttons.forEach(b => { 1626 b.classList.remove("btn-orange", "active"); 1627 b.classList.add("bg-gray-100", "text-white"); 1628 }); 1629 btn.classList.remove("bg-gray-100", "text-white"); 1630 btn.classList.add("btn-orange", "active"); 1631 } 1632 1633 if (allBtn) { 1634 setActiveButton(allBtn); 1635 } 1636 updateDisplay(); 1637 1638 buttons.forEach(btn => { 1639 btn.addEventListener("click", function () { 1640 currentFilter = this.getAttribute("data-filter"); 1641 1642 if (currentFilter === "all") { 1643 numItemsToShow = 5; 1644 } else { 1645 numItemsToShow = getVisibleItems().length; 1646 } 1647 1648 setActiveButton(this); 1649 updateDisplay(); 1650 }); 1651 }); 1652 1653 const onShowMoreClick = function () { 1654 numItemsToShow += 5; 1655 updateDisplay(); 1656 }; 1657 1658 const onShowAllClick = function () { 1659 numItemsToShow = getVisibleItems().length; 1660 updateDisplay(); 1661 }; 1662 1663 showMoreBtnDesktop?.addEventListener("click", onShowMoreClick); 1664 showMoreBtnMobile?.addEventListener("click", onShowMoreClick); 1665 1666 showAllBtnDesktop?.addEventListener("click", onShowAllClick); 1667 showAllBtnMobile?.addEventListener("click", onShowAllClick); 1668 1669 window.addEventListener("resize", function () { 1670 updateDisplay(); 1671 }); 1672}); 1673 1674document.addEventListener("DOMContentLoaded", function () { 1675 const accordionButtons = document.querySelectorAll('.accordion-button-dates'); 1676 1677 function syncDateAccordionChrome(button, bookNowButton, icon, expanded) { 1678 if (expanded) { 1679 if (button.contains(bookNowButton)) { 1680 button.removeChild(bookNowButton); 1681 } 1682 if (!button.contains(icon)) { 1683 button.appendChild(icon); 1684 } 1685 } else { 1686 if (!button.contains(bookNowButton)) { 1687 button.appendChild(bookNowButton); 1688 } 1689 if (button.contains(icon)) { 1690 button.removeChild(icon); 1691 } 1692 } 1693 } 1694 1695 accordionButtons.forEach(button => { 1696 const targetSel = button.getAttribute('data-bs-target'); 1697 if (!targetSel) { 1698 return; 1699 }
1700 const collapseEl = document.querySelector(targetSel); 1701 if (!collapseEl) { 1702 return; 1703 } 1704 1705 const bookNowButton = document.createElement('span'); 1706 bookNowButton.classList.add('btn-orange', 'btn', 'text-white', 'text-uppercase', 'p-2', 'rounded', 'badge-date', 'cursor-click', 'border-0', 'booknow-btn', 'fs-7'); 1707 bookNowButton.textContent = 'book now'; 1708 1709 const icon = document.createElement('i'); 1710 icon.classList.add('fa-solid', 'fa-square-minus', 'cursor-click', 'fs-5', 'text-black'); 1711 1712 button.appendChild(bookNowButton); 1713 1714 syncDateAccordionChrome(button, bookNowButton, icon, collapseEl.classList.contains('show')); 1715 1716 collapseEl.addEventListener('shown.bs.collapse', function () { 1717 syncDateAccordionChrome(button, bookNowButton, icon, true); 1718 }); 1719 collapseEl.addEventListener('hidden.bs.collapse', function () { 1720 syncDateAccordionChrome(button, bookNowButton, icon, false); 1721 }); 1722 }); 1723}); 1724 1725 1726var perPage = document.getElementById('perpage') 1727var sortSelect = document.getElementById('sortorder') 1728var regionSelect = document.getElementById('regionsort') 1729var onSale = document.getElementById('on_sale') 1730var hidePrice = document.getElementById('hide_price') 1731// const onSale = document.getElementById('on_sale'); 1732 1733var selectElement = document.getElementById('perpage') 1734var sortSelecet = document.getElementById('sortorder') 1735var regionSort = document.getElementById('regionsort') 1736 1737var mobileSelectElement = document.getElementById('mobileperpage') 1738var mobileSortSelecet = document.getElementById('mobilesortorder') 1739var mobileRegionSort = document.getElementById('mobileregion') 1740var mobileonSale = document.getElementById('mobile_on_sale') 1741 1742var newCheckedDestination = [] 1743var newCheckedTourTypes = [] 1744let checkedDestinations = "" 1745let checkedTourTypes = "" 1746var newSelectedValues = [] 1747var newCheckedValues = [] 1748var toggleSelectAllData = [] 1749 1750let sort = "popularity_asc" 1751let min_duration = 1 1752let max_duration = 30 1753let start_date = "" 1754let end_date = "" 1755let min_price; 1756let max_price; 1757let per_page = 9 1758let page = 1 1759 1760// Define this globally or persist it somewhere between calls 1761let selectedCountries = new Set(); 1762 1763function toggleSelectAll(inputName, value = null) { 1764 const checkboxes = document.querySelectorAll('input[name="' + inputName + '"]'); 1765 let areAllChecked = true; 1766 1767 checkboxes.forEach(function (checkbox) { 1768 if (!checkbox.checked) { 1769 areAllChecked = false; 1770 } 1771 }); 1772 1773 checkboxes.forEach(function (checkbox) { 1774 checkbox.checked = !areAllChecked; 1775 }); 1776 1777 getCheckedValues(inputName, value); 1778} 1779 1780 1781 1782var url = window.location.pathname; 1783var parts = url.split('/').filter(part => part.trim() !== ''); 1784var lastParam = parts[parts.length - 1]; 1785var newSelectedValues = false; 1786 1787const regionCheck = { 1788 'egypt-middle-east': 'egypt-middle-east', 1789 'africa': 'regionAfrica', 1790 'indian-subcontinent': 'indian-subcontinent', 1791 'asia': 'regionAsia', 1792 'southeast-asia': 'southeast-asia', 1793 'south-america': 'south-america', 1794 'europe': 'regionEurope', 1795 'oceania': 'regionOceania' 1796}; 1797 1798if (lastParam && regionCheck.hasOwnProperty(lastParam)) { 1799 const checkboxId = regionCheck[lastParam]; 1800 document.getElementById(checkboxId).checked = true; 1801} 1802 1803const regionToCheckboxMap = { 1804 'egypt-middle-east': 'egyptCheck', 1805 'africa': 'africaCheck', 1806 'indian-subcontinent': 'subcontinentCheck', 1807 'asia': 'asiaCheck', 1808 'southeast-asia': 'southeastAsia', 1809 'south-america': 'southAmerica', 1810 'europe': 'europeCheck', 1811 'oceania': 'oceaniaCheck' 1812}; 1813 1814const tourTypeMap = { 1815 'small-group': 'Small Group', 1816 'shoestring': 'Shoestring', 1817 'city-break': 'City Break', 1818 'private': 'Private', 1819 'family': 'Family', 1820 'overland': 'Overland' 1821}; 1822 1823var newCheckedValues = []; 1824var newSelectedValues = []; 1825 1826function getCheckedValues(inputName, value = null) { 1827 // Define all region checkbox names used for destinations 1828 const allRegionNames = [ 1829 'egyptCheck', 'africaCheck', 'subcontinentCheck', 'asiaCheck', 1830 'southeastAsia', 'southAmerica', 'europeCheck', 'oceaniaCheck' 1831 ]; 1832 1833 // We always scan all regions and tour type checkboxes to keep global filter state fully synchronized 1834 const selectedCheckboxes = document.querySelectorAll('input[name="tourChecked"]'); 1835 1836 function normalizeValue(val) { 1837 return val ? val.replace(/\s+/g, '-').toLowerCase() : ''; 1838 } 1839 1840 // Handle onload URL param activation (value === 2 or 3) 1841 if (value === 2 || value === 3) { 1842 const normalizedParam = normalizeValue(lastParam); 1843 if (value === 2) { 1844 let selectedCheck = false;
1845 selectedCheckboxes.forEach(function(checkbox) { 1846 if (normalizeValue(checkbox.value) === lastParam.toLowerCase()) { 1847 if (!selectedCheck) { 1848 checkbox.checked = true; 1849 selectedCheck = true; 1850 } 1851 } 1852 }); 1853 } else if (value === 3) { 1854 let selectedChecked = false; 1855 allRegionNames.forEach(function(name) { 1856 var checkboxes = document.querySelectorAll('input[name="' + name + '"]'); 1857 checkboxes.forEach(function(checkbox) { 1858 if (normalizeValue(checkbox.value) === lastParam.toLowerCase()) { 1859 if (!selectedChecked) { 1860 checkbox.checked = true; 1861 selectedChecked = true; 1862 } 1863 } 1864 }); 1865 }); 1866 } 1867 } 1868 1869 // Gather checked tour types from DOM 1870 var selectedValues = []; 1871 selectedCheckboxes.forEach(function(checkbox) { 1872 if (checkbox.checked) { 1873 selectedValues.push(normalizeValue(checkbox.value)); 1874 } 1875 }); 1876 1877 // Gather checked destinations across ALL regions from DOM 1878 var checkedValues = []; 1879 allRegionNames.forEach(function(name) { 1880 var checkboxes = document.querySelectorAll('input[name="' + name + '"]'); 1881 checkboxes.forEach(function(checkbox) { 1882 if (checkbox.checked) { 1883 checkedValues.push(normalizeValue(checkbox.value)); 1884 } 1885 }); 1886 }); 1887 1888 // Update global state variables 1889 newCheckedValues = [...new Set(checkedValues)]; 1890 newSelectedValues = [...new Set(selectedValues)]; 1891 1892 let destinations = newCheckedValues.join(","); 1893 let tourTypes = newSelectedValues.join(","); 1894 1895 checkedDestinations = destinations; 1896 checkedTourTypes = tourTypes; 1897 newCheckedDestination = destinations; 1898 newCheckedTourTypes = tourTypes; 1899 1900 // Run the getTours API fetch with the synchronized states 1901 getTours( 1902 microsite, 1903 destinations, 1904 tourTypes, 1905 onSale.checked ? true : false, 1906 regionSort.value + sortSelecet.value, 1907 sliderData.durationMonth.from, 1908 sliderData.durationMonth.to, 1909 sliderData.monthYearRange.from, 1910 sliderData.monthYearRange.to, 1911 sliderData.priceRange.from, 1912 sliderData.priceRange.to, 1913 per_page, 1914 page 1915 ); 1916} 1917 1918function getCheckedTourTypes(inputName, value = null) { 1919 var checkboxes = document.querySelectorAll('input[name="' + inputName + '"]'); 1920 var selectedCheckboxes = document.querySelectorAll('input[name="tourChecked"]'); 1921 1922 // Helper function to normalize values 1923 function normalizeValue(value) { 1924 return value.replace(/\s+/g, '-').toLowerCase(); 1925 } 1926 1927 // Process tourChecked checkboxes 1928 var selectedValues = [] 1929 var checkedValues = [] 1930 selectedCheckboxes.forEach(function (checkbox) { 1931 if (checkbox.checked) { 1932 selectedValues.push(normalizeValue(checkbox.value)); 1933 } 1934 return checkedValues 1935 }); 1936 1937 // Process inputName checkboxes 1938 1939 checkboxes.forEach(function (checkbox) { 1940 if (checkbox.checked) { 1941 checkedValues.push(normalizeValue(checkbox.value)); 1942 } 1943 return selectedValues 1944 }); 1945 1946 // If there is a special case for specific values 1947 var selectedCheck = false; 1948 var selectedChecked = false; 1949 var normalizedValue = normalizeValue(lastParam); 1950 1951 // Process the checkboxes based on the value 1952 if (value === 2 || value === 3) { 1953 var targetCheckboxes = (value === 2) ? selectedCheckboxes : checkboxes; 1954 var targetValues = (value === 2) ? selectedValues : checkedValues; 1955 var isSelectedCheck = (value === 2) ? selectedCheck : selectedChecked; 1956 1957 targetCheckboxes.forEach(function (checkbox) { 1958 if (checkbox.value.replace(/\s+/g, '-').toLowerCase() === lastParam.toLowerCase()) { 1959 // If the checkbox is not already checked, update the checkbox and add value 1960 if (!isSelectedCheck) { 1961 checkbox.checked = true; 1962 isSelectedCheck = true; // Ensure the condition is met only once 1963 targetValues.push(normalizedValue); // Add the normalized value to the array 1964 } 1965 } 1966 }); 1967 } 1968 1969 1970 // Update the tour options 1971 let destinations = [...new Set(checkedValues)].join(","); 1972 let tourTypes = [...new Set(selectedValues)].join(","); 1973 1974 checkedDestinations = destinations; 1975 checkedTourTypes = tourTypes; 1976 1977 newCheckedDestination = destinations; 1978 newCheckedTourTypes = tourTypes; 1979 1980 newCheckedValues = checkedValues 1981 newSelectedValues = selectedValues 1982 1983 1984 getTours(microsite, [...new Set(newCheckedValues)].join(','), [...new Set(newSelectedValues)].join(','), onSale.checked ? true : false, regionSort.value + sortSelecet.value, sliderData.durationMonth.from, 1985 sliderData.durationMonth.to, sliderData.monthYearRange.from, sliderData.monthYearRange.to, 1986 sliderData.priceRange.from, sliderData.priceRange.to, per_page, page) 1987 1988 if (checkedTourTypes.length > 0) { 1989 getTours(microsite, [...new Set(newCheckedValues)].join(','), [...new Set(newSelectedValues)].join(','), onSale.checked ? true : false, regionSort.value + sortSelecet.value, sliderData.durationMonth.from, 1990 sliderData.durationMonth.to, sliderData.monthYearRange.from, sliderData.monthYearRange.to, 1991 sliderData.priceRange.from, sliderData.priceRange.to, per_page, page) 1992 } 1993} 1994 1995function normalizeForComparison(str) { 1996 if (!str) return ''; 1997
1998 return String(str).toLowerCase() 1999 .replace(/&/g, 'and') 2000 .replace(/[^a-z0-9]+/g, '-') 2001 .replace(/^-+|-+$/g, ''); 2002} 2003 2004function matchesDestination(destinationSlugs, param) { 2005 const normalizedParam = normalizeForComparison(param); 2006 return destinationSlugs.some(slug => normalizeForComparison(slug) === normalizedParam); 2007} 2008 2009function checkRegionCheckboxes(regionParam) { 2010 const normalizedParam = normalizeForComparison(regionParam); 2011 2012 for (const [region, checkboxName] of Object.entries(regionToCheckboxMap)) { 2013 if (normalizeForComparison(region) === normalizedParam) { 2014 const checkboxes = document.querySelectorAll(`input[name="${checkboxName}"]`); 2015 checkboxes.forEach(checkbox => { 2016 checkbox.checked = true; 2017 }); 2018 getCheckedValues(checkboxName, 3); 2019 return true; 2020 } 2021 } 2022 return false; 2023} 2024 2025function checkTourTypeCheckboxes(tourTypeParam) { 2026 const normalizedParam = normalizeForComparison(tourTypeParam); 2027 2028 for (const [typeSlug, typeValue] of Object.entries(tourTypeMap)) { 2029 if (normalizeForComparison(typeSlug) === normalizedParam) { 2030 const checkboxes = document.querySelectorAll('input[name="tourChecked"]'); 2031 checkboxes.forEach(checkbox => { 2032 if (normalizeForComparison(checkbox.value) === normalizeForComparison(typeValue)) { 2033 checkbox.checked = true; 2034 } 2035 }); 2036 getCheckedValues('', 2); 2037 return true; 2038 } 2039 } 2040 return false; 2041} 2042 2043if (!checkRegionCheckboxes(lastParam) || !checkTourTypeCheckboxes(lastParam)) { 2044 const destinations = window.destinations || {}; 2045 2046 if (destinations.egyptMiddleEast?.length && matchesDestination(destinations.egyptMiddleEast, lastParam)) { 2047 getCheckedValues('egyptCheck', 3); 2048 } 2049 else if (destinations.africa?.length && matchesDestination(destinations.africa, lastParam)) { 2050 getCheckedValues('africaCheck', 3); 2051 } 2052 else if (destinations.subcontinent?.length && matchesDestination(destinations.subcontinent, lastParam)) { 2053 getCheckedValues('subcontinentCheck', 3); 2054 } 2055 else if (destinations.asia?.length && matchesDestination(destinations.asia, lastParam)) { 2056 getCheckedValues('asiaCheck', 3); 2057 } 2058 else if (destinations.southeastAsia?.length && matchesDestination(destinations.southeastAsia, lastParam)) { 2059 getCheckedValues('southeastAsia', 3); 2060 } 2061 else if (destinations.southAmerica?.length && matchesDestination(destinations.southAmerica, lastParam)) { 2062 getCheckedValues('southAmerica', 3); 2063 } 2064 else if (destinations.europe?.length && matchesDestination(destinations.europe, lastParam)) { 2065 getCheckedValues('europeCheck', 3); 2066 } 2067 else if (destinations.oceania?.length && matchesDestination(destinations.oceania, lastParam)) { 2068 getCheckedValues('oceaniaCheck', 3); 2069 } 2070 else if (['antarctic', 'arctic'].includes(lastParam)) { 2071 getCheckedValues('polarRegions', 3); 2072 } 2073} 2074 2075if (['small-group', 'shoestring', 'city-break', 'private', 'family', 'overland'].includes(lastParam)) { 2076 getCheckedValues('', 2) 2077} 2078 2079 2080 2081document.addEventListener('DOMContentLoaded', (event) => { 2082 let destinationsArray = checkedDestinations.split(','); 2083 let uniqueDestinations = [...new Set(destinationsArray)]; 2084 const onSaleCheckbox = document.getElementById('on_sale'); 2085 if (window.location.pathname === `/${microsite}/travel-deals` || window.location.pathname === 2086 `/travel-deals`) { 2087 getTours(microsite, checkedDestinations, checkedTourTypes, onSale.checked = true, regionSort.value + sortSelecet.value, 2088 sliderData.durationMonth.from, 2089 sliderData.durationMonth.to, sliderData.monthYearRange.from, sliderData.monthYearRange.to, 2090 sliderData.priceRange.from, sliderData.priceRange.to, selectElement.value, page) 2091 2092 } 2093}); 2094 2095document.addEventListener('DOMContentLoaded', (event) => { 2096 if (window.location.pathname === `/${microsite}/safer-encounters` || window.location.pathname === 2097 `/safer-encounters`) { 2098 // checkedDestinations = checkedDestinations ? `${checkedDestinations},laos` : 'laos'; 2099 getTours(microsite, checkedDestinations, checkedTourTypes, onSale.checked, regionSort.value + sortSelecet.value, 2100 sliderData.durationMonth.from, 2101 sliderData.durationMonth.to, sliderData.monthYearRange.from, sliderData.monthYearRange.to, 2102 sliderData.priceRange.from, sliderData.priceRange.to, selectElement.value, page) 2103 } 2104}); 2105 2106document.addEventListener('DOMContentLoaded', (event) => { 2107 let destinationsArray = checkedDestinations.split(','); 2108 let uniqueDestinations = [...new Set(destinationsArray)]; 2109 const onSaleCheckbox = document.getElementById('on_sale'); 2110 if (window.location.pathname === `/${microsite}/destinations` || window.location.pathname === 2111 `/destinations`) { 2112 getTours(microsite, checkedDestinations, checkedTourTypes, onSale.checked ? true : false, regionSort.value + sortSelecet.value, 2113 sliderData.durationMonth.from, 2114 sliderData.durationMonth.to, sliderData.monthYearRange.from, sliderData.monthYearRange.to, 2115 sliderData.priceRange.from, sliderData.priceRange.to, selectElement.value, page) 2116 2117 } 2118}); 2119 2120 2121function getSale() { 2122 // remove duplicate 2123 let destinationsArray = checkedDestinations.split(','); 2124 let uniqueDestinations = [...new Set(destinationsArray)]; 2125 2126 2127 getTours(microsite, [...new Set(newCheckedValues)].join(','), [...new Set(newSelectedValues)].join(','), onSale.checked ? true : false, regionSort.value + sortSelecet.value, 2128 sliderData.durationMonth.from, 2129 sliderData.durationMonth.to, sliderData.monthYearRange.from, sliderData.monthYearRange.to, 2130 sliderData.priceRange.from, sliderData.priceRange.to, selectElement.value, page) 2131} 2132 2133function mobileGetSale() { 2134 // remove duplicate 2135 let destinationsArray = checkedDestinations.split(','); 2136 let uniqueDestinations = [...new Set(destinationsArray)]; 2137 2138 2139 getTours(microsite, [...new Set(newCheckedValues)].join(','), [...new Set(newSelectedValues)].join(','), mobileonSale.checked ? true : false, regionSort.value + sortSelecet.value, 2140 min_duration, max_duration, start_date, end_date, min_price, max_price, selectElement.value, page) 2141} 2142 2143function getHidePrices() { 2144 getTours( 2145 microsite, 2146 [...new Set(newCheckedValues)].join(','), 2147 [...new Set(newSelectedValues)].join(','), 2148 onSale.checked ? true : false, 2149 regionSort.value + sortSelecet.value, 2150 sliderData.durationMonth.from, 2151 sliderData.durationMonth.to, 2152 sliderData.monthYearRange.from, 2153 sliderData.monthYearRange.to, 2154 sliderData.priceRange.from, 2155 sliderData.priceRange.to, 2156 selectElement.value, 2157 page 2158 ); 2159} 2160 2161function getAllTours() { 2162 // remove duplicate 2163 let destinationsArray = checkedDestinations.split(','); 2164 let uniqueDestinations = [...new Set(destinationsArray)]; 2165 getTours(microsite, [...new Set(newCheckedValues)].join(','), [...new Set(newSelectedValues)].join(','), onSale.checked ? true : false, regionSort.value + sortSelecet.value, 2166 sliderData.durationMonth.from, 2167 sliderData.durationMonth.to, sliderData.monthYearRange.from, sliderData.monthYearRange.to, 2168 sliderData.priceRange.from, sliderData.priceRange.to, selectElement.value, page) 2169 } 2170 2171 function mobileAllTours() { 2172 // remove duplicate 2173 let destinationsArray = checkedDestinations.split(','); 2174 let uniqueDestinations = [...new Set(destinationsArray)]; 2175 getTours(microsite, [...new Set(newCheckedValues)].join(','), [...new Set(newSelectedValues)].join(','), mobileonSale.checked ? true : false, regionSort.value + sortSelecet.value, 2176 min_duration, max_duration, start_date, end_date, min_price, max_price, selectElement.value, page) 2177 } 2178 2179document.addEventListener('DOMContentLoaded', (event) => { 2180 let destinationsArray = checkedDestinations.split(','); 2181 let uniqueDestinations = [...new Set(destinationsArray)]; 2182 2183 if (window.location.pathname === `/${microsite}/searchalltours` || window.location.pathname === `/searchalltours`) { 2184 // Get URL parameters
2185 const urlParams = new URLSearchParams(window.location.search); 2186 const countryParam = urlParams.get('countries'); 2187 2188 // Initialize checkedDestinations as a string 2189 checkedDestinations = ''; 2190 2191 // Define Egypt & Middle East countries with proper capitalization 2192 const egyptMiddleEastCountries = { 2193 'egypt': 'Egypt', 2194 'jordan': 'Jordan', 2195 'israel': 'Israel', 2196 'lebanon': 'Lebanon', 2197 'sudan': 'Sudan' 2198 }; 2199 2200 // If countries parameter exists 2201 if (countryParam) { 2202 checkedDestinations = countryParam; 2203 const countryLower = countryParam.toLowerCase(); 2204 2205 // Check if it's an Egypt & Middle East country 2206 if (Object.keys(egyptMiddleEastCountries).includes(countryLower)) { 2207 // Check the Egypt & Middle East main checkbox 2208 const egyptCheckbox = document.querySelector('#egyptCheck'); 2209 if (egyptCheckbox) { 2210 egyptCheckbox.checked = true; 2211 } 2212 2213 // Find and check the specific country checkbox using proper capitalization 2214 const properCountryName = egyptMiddleEastCountries[countryLower]; 2215 const countryCheckbox = document.querySelector(`input[value="${properCountryName}"][name="egyptCheck"]`); 2216 if (countryCheckbox) { 2217 countryCheckbox.checked = true; 2218 } 2219 2220 // Call getCheckedValues after checking boxes 2221 getCheckedValues('egyptCheck', 3); 2222 } 2223 } 2224 2225 // Continue with existing getTours call 2226 getTours( 2227 microsite, 2228 checkedDestinations, 2229 checkedTourTypes, 2230 onSale.checked ? true : false, 2231 regionSort.value + sortSelecet.value, 2232 min_duration, 2233 max_duration, 2234 start_date, 2235 end_date, 2236 min_price, 2237 max_price, 2238 selectElement.value, 2239 page 2240 ); 2241 } 2242 2243 if (window.location.pathname === `/${microsite}/tourtypes` || window.location.pathname === `/tourtypes`) { 2244 getTours( 2245 microsite, 2246 checkedDestinations, 2247 checkedTourTypes, 2248 onSale.checked ? true : false, 2249 regionSort.value + sortSelecet.value, 2250 min_duration, 2251 max_duration, 2252 start_date, 2253 end_date, 2254 min_price, 2255 max_price, 2256 selectElement.value, 2257 page 2258 ); 2259 } 2260}); 2261 2262function getSelectedOption() { 2263 let destinations = ''; 2264 let tourTypes = ''; 2265 // let onSale = true 2266 let min_duration = 1 2267 let max_duration = 30 2268 let start_date = "" 2269 let end_date = "" 2270 let min_price; 2271 let max_price; 2272 let page = 1 2273 // remove duplicate 2274 let destinationsArray = checkedDestinations.split(','); 2275 let uniqueDestinations = [...new Set(destinationsArray)]; 2276 const checkedValues = newCheckedValues.length > 0 2277 ? [...new Set(newCheckedValues)].join(',') 2278 : '' 2279 2280 const selectedValues = newSelectedValues.length > 0 2281 ? [...new Set(newSelectedValues)].join(',') 2282 : '' 2283 getTours(microsite, checkedValues, selectedValues, onSale.checked ? true : false, regionSort.value + sortSelecet.value, 2284 sliderData.durationMonth.from, 2285 sliderData.durationMonth.to, sliderData.monthYearRange.from, sliderData.monthYearRange.to, 2286 sliderData.priceRange.from, sliderData.priceRange.to, selectElement.value, page) 2287 2288} 2289function mobileSelecectedOption() { 2290let destinations = ''; 2291let tourTypes = ''; 2292let min_duration = 1 2293let max_duration = 30 2294let start_date = "" 2295let end_date = "" 2296let min_price; 2297let max_price; 2298let page = 1 2299// remove duplicate 2300let destinationsArray = checkedDestinations.split(','); 2301let uniqueDestinations = [...new Set(destinationsArray)]; 2302const checkedValues = newCheckedValues.length > 0 2303 ? [...new Set(newCheckedValues)].join(',') 2304 : '' 2305 2306 const selectedValues = newSelectedValues.length > 0 2307 ? [...new Set(newSelectedValues)].join(',') 2308 : '' 2309getTours(microsite, checkedValues, selectedValues, onSale.checked ? true : false, mobileRegionSort.value + mobileSortSelecet.value, min_duration, 2310 max_duration, start_date, end_date, 2311 min_price, max_price, mobileSelectElement.value, page) 2312 2313} 2314 2315function getSortSelected() { 2316 let destinations = ''; 2317 let tourTypes = ''; 2318 // let onSale = true 2319 let min_duration = 1 2320 let max_duration = 30 2321 let start_date = "" 2322 let end_date = "" 2323 let min_price; 2324 let max_price; 2325 let per_page = 9 2326 let page = 1 2327 // remove duplicate 2328 let destinationsArray = checkedDestinations.split(','); 2329 let uniqueDestinations = [...new Set(destinationsArray)]; 2330 const checkedValues = newCheckedValues.length > 0 2331 ? [...new Set(newCheckedValues)].join(',') 2332 : '' 2333 2334 const selectedValues = newSelectedValues.length > 0 2335 ? [...new Set(newSelectedValues)].join(',') 2336 : '' 2337 getTours(microsite, checkedValues, selectedValues, onSale.checked ? true : false, regionSort.value + sortSelecet.value, 2338 sliderData.durationMonth.from, 2339 sliderData.durationMonth.to, sliderData.monthYearRange.from, sliderData.monthYearRange.to, 2340 sliderData.priceRange.from, sliderData.priceRange.to, selectElement.value, page) 2341 2342} 2343 2344function mobileSortSelected() { 2345let destinations = ''; 2346let tourTypes = ''; 2347// let onSale = true 2348let min_duration = 1 2349let max_duration = 30 2350let start_date = "" 2351let end_date = "" 2352let min_price; 2353let max_price; 2354let per_page = 9 2355let page = 1 2356// remove duplicate 2357let destinationsArray = checkedDestinations.split(','); 2358let uniqueDestinations = [...new Set(destinationsArray)]; 2359const checkedValues = newCheckedValues.length > 0 2360 ? [...new Set(newCheckedValues)].join(',') 2361 : '' 2362 2363 const selectedValues = newSelectedValues.length > 0 2364 ? [...new Set(newSelectedValues)].join(',') 2365 : '' 2366getTours(microsite, checkedValues, selectedValues, onSale.checked ? true : false, mobileRegionSort.value + mobileSortSelecet.value, min_duration, 2367 max_duration, start_date, end_date, 2368 min_price, max_price, mobileSelectElement.value, page) 2369 2370} 2371 2372 2373function getSortedRegion() { 2374 let destinations = ''; 2375 let tourTypes = ''; 2376 // let onSale = true 2377 let min_duration = 1 2378 let max_duration = 30 2379 let start_date = "" 2380 let end_date = "" 2381 let min_price; 2382 let max_price; 2383 let per_page = 9 2384 let page = 1 2385 // remove duplicate 2386 let destinationsArray = checkedDestinations.split(','); 2387 let uniqueDestinations = [...new Set(destinationsArray)]; 2388 const checkedValues = newCheckedValues.length > 0 2389 ? [...new Set(newCheckedValues)].join(',') 2390 : '' 2391 2392 const selectedValues = newSelectedValues.length > 0 2393 ? [...new Set(newSelectedValues)].join(',') 2394 : '' 2395 getTours(microsite, checkedValues, selectedValues, onSale.checked ? true : false, regionSort.value + sortSelecet.value, 2396 sliderData.durationMonth.from, 2397 sliderData.durationMonth.to, sliderData.monthYearRange.from, sliderData.monthYearRange.to, 2398 sliderData.priceRange.from, sliderData.priceRange.to, selectElement.value, page) 2399 2400} 2401 2402function mobileGetSorted() { 2403let destinations = ''; 2404let tourTypes = ''; 2405// let onSale = true 2406let min_duration = 1 2407let max_duration = 30 2408let start_date = "" 2409let end_date = "" 2410let min_price; 2411let max_price; 2412let per_page = 9 2413let page = 1 2414// remove duplicate 2415let destinationsArray = checkedDestinations.split(','); 2416let uniqueDestinations = [...new Set(destinationsArray)]; 2417const checkedValues = newCheckedValues.length > 0 2418 ? [...new Set(newCheckedValues)].join(',') 2419 : '' 2420 2421const selectedValues = newSelectedValues.length > 0 2422 ? [...new Set(newSelectedValues)].join(',') 2423 : '' 2424getTours(microsite, checkedValues, selectedValues, onSale.checked ? true : false, mobileRegionSort.value + mobileSortSelecet.value, min_duration, 2425 max_duration, start_date, end_date, 2426 min_price, max_price, mobileSelectElement.value, page) 2427 2428} 2429 2430document.addEventListener('DOMContentLoaded', function () { 2431 const currentUrl = new URL(window.location.href);
2432 const pathParts = currentUrl.pathname.split('/').filter(Boolean); 2433 const indexOfP = pathParts.indexOf('p'); 2434 if (indexOfP !== -1 && pathParts[indexOfP + 1] === 'safer-encounters') { 2435 const prefixParts = pathParts.slice(0, indexOfP); 2436 const newPath = '/' + [...prefixParts, 'safer-encounters'].join('/'); 2437 window.history.replaceState(null, '', newPath); 2438 } 2439}); 2440 2441document.addEventListener('DOMContentLoaded', function() { 2442 // Initialize all accordion icons 2443 updateAllAccordionIcons('.accordion-button-itinerary'); 2444 updateAllAccordionIcons('.accordion-button-notes'); 2445 2446 // Set up event listeners for itinerary accordions 2447 document.querySelectorAll('.accordion-button-itinerary').forEach(button => { 2448 button.addEventListener('click', function() { 2449 setTimeout(() => { 2450 updateAllAccordionIcons('.accordion-button-itinerary'); 2451 }, 100); 2452 }); 2453 }); 2454 2455 // Set up event listeners for notes accordions 2456 document.querySelectorAll('.accordion-button-notes').forEach(button => { 2457 button.addEventListener('click', function() { 2458 setTimeout(() => { 2459 updateAllAccordionIcons('.accordion-button-notes'); 2460 }, 100); 2461 }); 2462 }); 2463}); 2464 2465// Generic function to update icons for a specific accordion type 2466function updateAllAccordionIcons(accordionClass) { 2467 document.querySelectorAll(accordionClass).forEach(button => { 2468 const icon = button.querySelector('.accordion-icon'); 2469 const isExpanded = button.getAttribute('aria-expanded') === 'true'; 2470 2471 if (isExpanded) { 2472 icon.classList.replace('fa-plus-circle', 'fa-minus-circle'); 2473 } else { 2474 icon.classList.replace('fa-minus-circle', 'fa-plus-circle'); 2475 } 2476 }); 2477} 2478 2479// Original function for notes accordion (unchanged) 2480function toggleItineraryAccordion(action) { 2481 const accordions = document.querySelectorAll('.accordion-button-notes'); 2482 2483 accordions.forEach((accordion, index) => { 2484 if (index === 0) { 2485 const isExpanded = accordion.getAttribute('aria-expanded') === 'true'; 2486 if ((action === 'expand' && !isExpanded) || (action === 'collapse' && isExpanded)) { 2487 accordion.click(); 2488 } 2489 } else { 2490 if (action === 'expand' && accordion.classList.contains('collapsed')) { 2491 accordion.click(); 2492 } else if (action === 'collapse' && !accordion.classList.contains('collapsed')) { 2493 accordion.click(); 2494 } 2495 } 2496 }); 2497 2498 // Update icons after toggling 2499 setTimeout(() => { 2500 updateAllAccordionIcons('.accordion-button-notes'); 2501 }, 150); 2502} 2503 2504document.addEventListener("DOMContentLoaded", () => { 2505 const divMap = { 2506 toDiv1: "div1", 2507 toDiv2: "div2", 2508 toDiv3: "div3", 2509 toDiv4: "div4", 2510 toAfrica: "africa", 2511 toIndianSubcontinent: "indianSubcontinent", 2512 toAsia: "asia", 2513 toSoutheastAsia: "southeastAsia", 2514 toSouthAmerica: "southAmerica", 2515 toEurope: "europe", 2516 toOceania: "oceania", 2517 goback1: "div1" 2518 }; 2519 2520 2521 const showDiv = (showId) => { 2522 document.querySelectorAll(".modal-content").forEach(div => { 2523 div.classList.add("d-none"); 2524 }); 2525 document.getElementById(showId)?.classList.remove("d-none"); 2526 }; 2527 2528 Object.entries(divMap).forEach(([buttonId, showId]) => { 2529 const button = document.getElementById(buttonId); 2530 if (button) { 2531 button.addEventListener("click", () => showDiv(showId)); 2532 } 2533 }); 2534 2535 document.querySelectorAll(".backDestination").forEach(button => { 2536 button.addEventListener("click", () => showDiv("div3")); 2537 }); 2538}); 2539 2540(function () { 2541 const STORAGE_KEY = 'utw_wishlist'; 2542 2543 function getWishlist() { 2544 try { 2545 const raw = localStorage.getItem(STORAGE_KEY); 2546 const parsed = raw ? JSON.parse(raw) : []; 2547 return Array.isArray(parsed) ? parsed : []; 2548 } catch { 2549 return []; 2550 } 2551 } 2552 2553 function saveWishlist(items) { 2554 localStorage.setItem(STORAGE_KEY, JSON.stringify(items)); 2555 document.dispatchEvent(new CustomEvent('wishlist:updated', { detail: { items } })); 2556 } 2557 2558 function buildTourUrl(slug) { 2559 const ms = typeof microsite !== 'undefined' ? microsite : 'us'; 2560 return ms === 'us' ? `/tour/${slug}` : `/${ms}/tour/${slug}`; 2561 } 2562 2563 function itemFromElement(el) { 2564 const id = el.dataset.wishlistId; 2565 if (!id) return null; 2566 2567 return { 2568 id, 2569 slug: id, 2570 title: el.dataset.wishlistTitle || '', 2571 subtitle: el.dataset.wishlistSubtitle || '', 2572 category: el.dataset.wishlistCategory || '', 2573 image: el.dataset.wishlistImage || '', 2574 url: el.dataset.wishlistUrl || buildTourUrl(id), 2575 duration: el.dataset.wishlistDuration || '', 2576 price: el.dataset.wishlistPrice || '', 2577 }; 2578 } 2579 2580 function isInWishlist(id) { 2581 return getWishlist().some((item) => item.id === id); 2582 } 2583 2584 function addItem(item) { 2585 const items = getWishlist(); 2586 if (items.some((i) => i.id === item.id)) return items; 2587 items.push(item); 2588 saveWishlist(items); 2589 return items; 2590 } 2591 2592 function removeItem(id) { 2593 const items = getWishlist().filter((item) => item.id !== id); 2594 saveWishlist(items); 2595 return items; 2596 } 2597 2598 function toggleItem(item) { 2599 if (isInWishlist(item.id)) { 2600 return removeItem(item.id); 2601 } 2602 return addItem(item); 2603 } 2604 2605 function escapeHtml(str) { 2606 const div = document.createElement('div'); 2607 div.textContent = str; 2608 return div.innerHTML; 2609 } 2610 2611 function updateToggleButton(btn) { 2612 const id = btn.dataset.wishlistId; 2613 if (!id) return; 2614 2615 const added = isInWishlist(id); 2616 const icon = btn.querySelector('i'); 2617 const label = btn.querySelector('.wishlist-btn-label'); 2618 2619 btn.classList.toggle('is-added', added); 2620 btn.setAttribute('aria-pressed', added ? 'true' : 'false'); 2621 2622 if (icon) { 2623 icon.classList.remove('far', 'fas', 'fa-regular', 'fa-solid'); 2624 icon.classList.add(added ? 'fa-solid' : 'fa-regular'); 2625 icon.setAttribute('aria-hidden', 'true'); 2626 } 2627 2628 if (label) { 2629 label.textContent = added ? 'ADDED TO WISHLIST' : 'ADD TO WISHLIST'; 2630 } else if (btn.classList.contains('btn-wishlist')) { 2631 const text = added ? 'ADDED TO WISHLIST' : 'ADD TO WISHLIST'; 2632 btn.innerHTML = `<i class="${added ? 'fa-solid' : 'fa-regular'} fa-heart" aria-hidden="true"></i> ${text}`; 2633 } 2634 2635 if (btn.classList.contains('wishlist-card-heart')) { 2636 btn.classList.toggle('is-active', added); 2637 } 2638 } 2639 2640 function updateAllToggleButtons() { 2641 document.querySelectorAll('.js-wishlist-toggle').forEach(updateToggleButton); 2642 } 2643 2644 function updateCount() { 2645 const count = getWishlist().length; 2646 document.querySelectorAll('[data-wishlist-count]').forEach((el) => { 2647 el.textContent = String(count); 2648 }); 2649 } 2650 2651 function renderPanel() { 2652 const listEl = document.getElementById('wishlistPanelList'); 2653 const emptyEl = document.getElementById('wishlistPanelEmpty'); 2654 if (!listEl) return; 2655 2656 const items = getWishlist(); 2657 listEl.innerHTML = ''; 2658 2659 if (items.length === 0) { 2660 if (emptyEl) emptyEl.classList.remove('d-none'); 2661 return; 2662 } 2663 2664 if (emptyEl) emptyEl.classList.add('d-none'); 2665
2666 items.forEach((item) => { 2667 const row = document.createElement('div'); 2668 row.className = 'wishlist-item'; 2669 row.innerHTML = ` 2670 <a href="${escapeHtml(item.url)}" class="wishlist-item-link"> 2671 <img src="${escapeHtml(item.image)}" alt="" class="wishlist-item-img" onerror="this.style.display='none'"> 2672 <div class="wishlist-item-text"> 2673 <div class="wishlist-item-title">${escapeHtml(item.title)} Tour</div> 2674 <div class="wishlist-item-category">${escapeHtml(item.category)}</div> 2675 </div> 2676 </a> 2677 <button type="button" class="wishlist-item-remove" data-wishlist-remove="${escapeHtml(item.id)}" aria-label="Remove from wishlist">×</button> 2678 `; 2679 listEl.appendChild(row); 2680 }); 2681 } 2682 2683 function initPanelPortal() { 2684 const panel = document.getElementById('wishlistPanel'); 2685 if (panel && panel.parentElement !== document.body) { 2686 document.body.appendChild(panel); 2687 } 2688 } 2689 2690 function positionPanel() { 2691 const trigger = document.getElementById('wishlistTrigger'); 2692 const panel = document.getElementById('wishlistPanel'); 2693 if (!trigger || !panel || !panel.classList.contains('is-open')) return; 2694 2695 const rect = trigger.getBoundingClientRect(); 2696 const gap = 12; 2697 const panelWidth = panel.offsetWidth || 380; 2698 const panelHeight = panel.offsetHeight || 320; 2699 const viewportPadding = 16; 2700 2701 let top = rect.bottom + gap; 2702 if (top + panelHeight > window.innerHeight - viewportPadding) { 2703 top = Math.max(viewportPadding, rect.top - panelHeight - gap); 2704 } 2705 2706 let left = rect.right - panelWidth; 2707 left = Math.max(viewportPadding, Math.min(left, window.innerWidth - panelWidth - viewportPadding)); 2708 2709 panel.style.top = `${top}px`; 2710 panel.style.left = `${left}px`; 2711 panel.style.right = 'auto'; 2712 } 2713 2714 function openPanel() { 2715 const panel = document.getElementById('wishlistPanel'); 2716 if (!panel) return; 2717 2718 panel.classList.add('is-open'); 2719 panel.setAttribute('aria-hidden', 'false'); 2720 requestAnimationFrame(() => { 2721 positionPanel(); 2722 requestAnimationFrame(positionPanel); 2723 }); 2724 } 2725 2726 function closePanel() { 2727 const panel = document.getElementById('wishlistPanel'); 2728 if (panel) { 2729 panel.classList.remove('is-open'); 2730 panel.setAttribute('aria-hidden', 'true'); 2731 } 2732 } 2733 2734 function refreshUI() { 2735 updateCount(); 2736 updateAllToggleButtons(); 2737 renderPanel(); 2738 } 2739 2740 function bindEvents() { 2741 document.addEventListener('click', (e) => { 2742 const toggle = e.target.closest('.js-wishlist-toggle'); 2743 if (toggle) { 2744 e.preventDefault(); 2745 e.stopPropagation(); 2746 const item = itemFromElement(toggle); 2747 if (item) toggleItem(item); 2748 return; 2749 } 2750 2751 const removeBtn = e.target.closest('[data-wishlist-remove]'); 2752 if (removeBtn) { 2753 e.preventDefault(); 2754 removeItem(removeBtn.dataset.wishlistRemove); 2755 return; 2756 } 2757 2758 const trigger = e.target.closest('#wishlistTrigger'); 2759 if (trigger) { 2760 e.preventDefault(); 2761 const panel = document.getElementById('wishlistPanel'); 2762 if (panel?.classList.contains('is-open')) { 2763 closePanel(); 2764 } else { 2765 openPanel(); 2766 } 2767 return; 2768 } 2769 2770 const closeBtn = e.target.closest('#wishlistPanelClose'); 2771 if (closeBtn) { 2772 e.preventDefault(); 2773 closePanel(); 2774 return; 2775 } 2776 2777 // Click outside logic 2778 const panel = document.getElementById('wishlistPanel'); 2779 if (!panel?.classList.contains('is-open')) return; 2780 if (document.getElementById('wishlistTrigger')?.contains(e.target) || panel.contains(e.target)) return; 2781 closePanel(); 2782 }); 2783 2784 document.addEventListener('wishlist:updated', refreshUI); 2785 2786 document.addEventListener('keydown', (e) => { 2787 if (e.key === 'Escape') closePanel(); 2788 }); 2789 2790 window.addEventListener('resize', positionPanel); 2791 window.addEventListener('scroll', (e) => { 2792 if (e.target === document || e.target === window) { 2793 const nav = document.getElementById('navbar'); 2794 if (nav && nav.classList.contains('fixed')) { 2795 closePanel(); 2796 } else { 2797 positionPanel(); 2798 } 2799 } else { 2800 positionPanel(); 2801 } 2802 }, true); 2803 } 2804 2805 window.UTWWishlist = { 2806 getWishlist, 2807 addItem, 2808 removeItem, 2809 toggleItem, 2810 isInWishlist, 2811 refreshUI, 2812 }; 2813 2814 document.addEventListener('DOMContentLoaded', () => { 2815 initPanelPortal(); 2816 bindEvents(); 2817 refreshUI(); 2818 }); 2819})(); 2820 2821/* ============================================================ 2822 Home Hero Carousel
2823 ============================================================ */ 2824(function () { 2825 const hero = document.getElementById('home-hero'); 2826 if (!hero) return; 2827 2828 const bg = document.getElementById('home-hero-bg'); 2829 const slides = bg ? Array.from(bg.querySelectorAll('.home-hero-slide')) : []; 2830 const thumbs = Array.from(document.querySelectorAll('.home-hero-thumb')); 2831 const prevBtn = document.getElementById('homeHeroPrev'); 2832 const nextBtn = document.getElementById('homeHeroNext'); 2833 const titleEl = document.getElementById('home-hero-title'); 2834 const ctaEl = document.getElementById('home-hero-cta'); 2835 const ctaTextEl = document.getElementById('home-hero-cta-text'); 2836 2837 if (!slides.length) return; 2838 2839 let current = 0; 2840 let autoTimer = null; 2841 const INTERVAL = 5000; 2842 2843 // Collect slide data from the blade-rendered elements 2844 const slideData = slides.map((slide, i) => ({ 2845 desktopBg: slide.dataset.desktopBg || '', 2846 mobileBg: slide.dataset.mobileBg || slide.dataset.desktopBg || '', 2847 heading: slide.dataset.heading || '', 2848 ctaText: slide.dataset.ctaText || '', 2849 ctaUrl: slide.dataset.ctaUrl || '', 2850 })); 2851 2852 function isMobile() { 2853 return window.innerWidth < 768; 2854 } 2855 2856 function applyBg(slide, index) { 2857 const src = isMobile() ? slideData[index].mobileBg : slideData[index].desktopBg; 2858 if (src) slide.style.backgroundImage = `url('${src}')`; 2859 } 2860 2861 // Apply correct bg on resize 2862 window.addEventListener('resize', () => { 2863 slides.forEach((slide, i) => applyBg(slide, i)); 2864 }); 2865 slides.forEach((slide, i) => applyBg(slide, i)); 2866 2867 function goTo(index) { 2868 slides[current].classList.remove('active'); 2869 if (thumbs[current]) thumbs[current].classList.remove('active'); 2870 2871 current = (index + slides.length) % slides.length; 2872 2873 slides[current].classList.add('active'); 2874 if (thumbs[current]) thumbs[current].classList.add('active'); 2875 2876 if (titleEl) titleEl.textContent = slideData[current].heading; 2877 if (ctaTextEl) ctaTextEl.textContent = slideData[current].ctaText; 2878 if (ctaEl && slideData[current].ctaUrl) ctaEl.setAttribute('href', slideData[current].ctaUrl); 2879 } 2880 2881 function startAuto() { 2882 clearInterval(autoTimer); 2883 autoTimer = setInterval(() => goTo(current + 1), INTERVAL); 2884 } 2885 2886 function resetAuto() { 2887 startAuto(); 2888 } 2889 2890 // Thumbnail clicks 2891 thumbs.forEach((thumb, i) => { 2892 thumb.addEventListener('click', () => { 2893 goTo(i); 2894 resetAuto(); 2895 }); 2896 }); 2897 2898 // Arrow buttons 2899 if (prevBtn) { 2900 prevBtn.addEventListener('click', () => { 2901 goTo(current - 1); 2902 resetAuto(); 2903 }); 2904 } 2905 if (nextBtn) { 2906 nextBtn.addEventListener('click', () => { 2907 goTo(current + 1); 2908 resetAuto(); 2909 }); 2910 } 2911 2912 // Only auto-rotate when there is more than one slide 2913 if (slides.length > 1) { 2914 startAuto(); 2915 } 2916})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.