1(function() { 2 let INDEX_NAME = algolia_ajax_object.algolia_index_name; 3 let searchBox = document.querySelector('#searchbox'); 4 if(!searchBox){ 5 return false; 6 } 7 // Wait for InstantSearch to be loaded 8 function initializeSearch() { 9 // console.log('initialized'); 10 if (typeof instantsearch === 'undefined' || typeof algoliasearch === 'undefined') { 11 setTimeout(initializeSearch, 100); 12 return; 13 } 14 15 let urlParams = new URLSearchParams(window.location.search); 16 let defaultQuery = urlParams.get('keyword') || ''; // fallback to empty string 17 let defaultIndexName = INDEX_NAME + '_popular'; 18 const search = instantsearch({ 19 indexName: defaultIndexName, 20 searchClient: algoliasearch(algolia_ajax_object.algolia_api_id, algolia_ajax_object.algolia_search_key), 21 routing: false, 22 insights: true, 23 initialUiState: { 24 [defaultIndexName]: { 25 query: defaultQuery 26 }, 27 }, 28 searchParameters: { 29 hitsPerPage: 9, // set default here, 30 } 31 }); 32 let containerDestination = document.querySelector('.ht-tax-destination'); 33 let destination = containerDestination ? containerDestination.dataset.id : null; 34 let containerActivities = document.querySelector('.ht-tax-activities'); 35 let activities = containerActivities ? containerActivities.dataset.id : null; 36 let pageTemplateContainer = document.querySelector('.ht-page-template'); 37 let pageTemplateId = pageTemplateContainer ? pageTemplateContainer.dataset.id : null; 38 // SOLUTION: Create a custom widget for dynamic filtering for adding filters 39 const dynamicFilterWidget = { 40 $$type: 'custom.dynamicFilter', 41 42 init({ helper }) { 43 // Clear price refinements on init 44 helper.clearRefinements('price.current'); 45 }, 46 47 render({ helper }) { 48 const currentIndex = helper.state.index; 49 const filters = [filterStatus]; 50 51 // Add on_sale filter if using on_sale_first index 52 if (currentIndex && currentIndex.includes('on_sale_first')) { 53 filters.push('price.on_sale:true'); 54 } 55 // Add destination/activities filters 56 if (destination) filters.push(`activity_destination.name:"${destination}"`); 57 if (activities) filters.push(`categories.name:"${activities}"`); 58 59 let excluded = algolia_ajax_object.algolia_ajax_excluded || []; 60 if (excluded.length) { 61 let excluded_filters = excluded 62 .map(id => `NOT objectID:${id}`) 63 .join(' AND '); 64 filters.push(excluded_filters); 65 } 66 67 const filterValue = filters.join(' AND '); 68 69 // Update filters if they've changed 70 if (helper.state.filters !== filterValue) { 71 helper.setQueryParameter('filters', filterValue).search(); 72 } 73 } 74 }; 75 let filterStatus = 'status:publish'; 76 let filterProducts = []; 77 // STRICT ID filter (OR condition) for showing aloglia search filter in page template ( east coast, discount pages, etc ) 78 filterProducts = algolia_ajax_object.algolia_page_products.map(item => `objectID:${item.product}`) .join(" OR "); 79 if(filterProducts.length > 0 ){ 80 filterStatus += ` AND (${filterProducts})`; 81 } 82 let excluded = algolia_ajax_object.algolia_ajax_excluded || []; 83 if (excluded.length) { 84 filterStatus += ` AND ` + excluded 85 .map(id => `NOT objectID:${id}`) 86 .join(' AND '); 87 } 88 // Configure search parameters 89 let configureParams = { 90 attributesToSnippet: ['short_description:50'], 91 snippetEllipsisText: '...', 92 removeWordsIfNoResults: 'lastWords', 93 hitsPerPage: 9, 94 filters: filterStatus, 95 }; 96 97 let optionalFilters = []; 98 99 if ( 100 algolia_ajax_object.taxonomy_product_orders && 101 algolia_ajax_object.taxonomy_product_orders.length > 0 102 ) { 103 optionalFilters = algolia_ajax_object.taxonomy_product_orders.map((id, index) => { 104 const priorityScore = 1000 - index; 105 return `objectID:${id}<score=${priorityScore}>`; 106 }); 107 } 108 else if( 109 algolia_ajax_object.algolia_page_products && 110 algolia_ajax_object.algolia_page_products.length > 0 111 ) { 112 optionalFilters = algolia_ajax_object.algolia_page_products.map((item, index) => { 113 const priorityScore = 1000 - index; 114 return `objectID:${item.product}<score=${priorityScore}>`; 115 116 }); 117 } 118 119 if (optionalFilters.length > 0) {
120 configureParams.optionalFilters = optionalFilters; 121 configureParams.sumOrFiltersScores = true; 122 } 123 // Function to check if container exists 124 function containerExists(selector) { 125 return document.querySelector(selector) !== null; 126 } 127 128 // Create widgets array 129 const widgets = []; 130 131 // â Add the dynamic filter widget FIRST 132 widgets.push(dynamicFilterWidget); 133 134 // Add widgets only if containers exist 135 if (containerExists('#searchbox')) { 136 widgets.push(instantsearch.widgets.searchBox({ 137 container: '#searchbox', 138 placeholder: 'Search for tours and destinations...', 139 showReset: true, 140 showSubmit: true, 141 showLoadingIndicator: true, 142 queryHook: function(query, search) { 143 // Clear previous timeout 144 if (this.timeoutId) clearTimeout(this.timeoutId); 145 146 // Delay the search by 500ms 147 this.timeoutId = setTimeout(() => { 148 search(query); 149 }, 500); 150 } 151 })); 152 } 153 154 if (containerExists('#stats')) { 155 widgets.push(instantsearch.widgets.stats({ 156 container: '#stats', 157 templates: { 158 text: ({ nbHits, processingTimeMS }) => { 159 const hitsText = nbHits === 1 ? 'product' : 'products'; 160 return `<strong>${nbHits.toLocaleString()}</strong> ${hitsText} found</strong>`; 161 } 162 } 163 })); 164 } 165 166 if (containerExists('#current-refinements')) { 167 widgets.push(instantsearch.widgets.currentRefinements({ 168 container: '#current-refinements', 169 excludedAttributes: ['query','price.current'], // hide price from pills 170 transformItems(items) { 171 // Check if there are any refinements 172 if (items.length === 0) { 173 document.body.classList.add('no-refinements'); 174 } else { 175 document.body.classList.remove('no-refinements'); 176 } 177 return items.map(item => { 178 if (item.attribute == 'price.current') { 179 return { ...item, label: 'Price' }; 180 } 181 else if (item.attribute == 'categories.name') { 182 return { ...item, label: 'Destination' }; 183 } 184 return item; 185 }); 186 }, 187 templates: { 188 item(item, { html }) { 189 return html` 190 ${item.refinements 191 .map( 192 ref => html` 193 <span class="ais-CurrentRefinements-value"> 194 <button class="ais-CurrentRefinements-delete" data-value="${ref.value}"> 195 Ã 196 </button> 197 </span> 198 ` 199 ) 200 .join('')} 201 `; 202 }, 203 }, 204 })); 205 } 206 if (containerExists('#current-refinements-mobile')) { 207 widgets.push(instantsearch.widgets.currentRefinements({ 208 container: '#current-refinements-mobile', 209 transformItems(items) { 210 return items.map(item => { 211 if (item.attribute == 'price.current') { 212 return { ...item, label: 'Price' }; 213 } 214 else if (item.attribute == 'categories.name') { 215 return { ...item, label: 'Destination' }; 216 } 217 return item; 218 }); 219 }, 220 templates: { 221 item(item, { html }) { 222 return html` 223 ${item.refinements 224 .map( 225 ref => html`
226 <span class="ais-CurrentRefinements-value"> 227 <button class="ais-CurrentRefinements-delete" data-value="${ref.value}"> 228 Ã 229 </button> 230 </span> 231 ` 232 ) 233 .join('')} 234 `; 235 }, 236 }, 237 })); 238 } 239 240 // if (containerExists('#categories-refinement-list')) { 241 // widgets.push(instantsearch.widgets.refinementList({ 242 // container: '#categories-refinement-list', 243 // attribute: 'categories.name', 244 // limit: 5, 245 // showMore: true, 246 // showMoreLimit: 20, 247 // searchable: false, 248 // searchablePlaceholder: 'Search activities...' 249 // })); 250 // } 251 252 if (containerExists('#categories-refinement-list')) { 253 widgets.push(instantsearch.widgets.refinementList({ 254 container: '#categories-refinement-list', 255 attribute: 'custom_taxonomy.name', 256 limit: 5, 257 showMore: true, 258 showMoreLimit: 15, 259 searchable: false, 260 searchablePlaceholder: 'Search activities...' 261 })); 262 } 263 264 if (containerExists('#price-range')) { 265 widgets.push(instantsearch.widgets.rangeSlider({ 266 container: '#price-range', 267 attribute: 'price.current', 268 min: 0, 269 max: 2000, 270 step: 1, 271 tooltips: { 272 format: (value) => `$${Math.round(value).toLocaleString()}` 273 } 274 })); 275 } 276 277 if (containerExists('#accomodation')) { 278 widgets.push(instantsearch.widgets.refinementList({ 279 container: '#accomodation', 280 attribute: 'accomodation', 281 limit: 5, 282 showMore: true, 283 showMoreLimit: 15, 284 searchable: false, 285 searchablePlaceholder: 'Search accomodation...' 286 })); 287 } 288 if (containerExists('#departs')) { 289 widgets.push(instantsearch.widgets.refinementList({ 290 container: '#departs', 291 attribute: 'departs', 292 limit: 5, 293 showMore: true, 294 showMoreLimit: 15, 295 searchable: false, 296 searchablePlaceholder: 'Search departs...' 297 })); 298 } 299 if (containerExists('#finishes')) { 300 widgets.push(instantsearch.widgets.refinementList({ 301 container: '#finishes', 302 attribute: 'finishes', 303 limit: 5, 304 showMore: true, 305 showMoreLimit: 15, 306 searchable: false, 307 searchablePlaceholder: 'Search finishes...' 308 })); 309 } 310 if (containerExists('#travel_type')) { // Travel style(group,solo,etc) is used so travel_type is used for Travel style filter 311 widgets.push(instantsearch.widgets.refinementList({ 312 container: '#travel_type', 313 attribute: 'travel_type', 314 limit: 5, 315 showMore: true, 316 showMoreLimit: 15, 317 searchable: false, 318 searchablePlaceholder: 'Search travel style...' 319 })); 320 } 321 322 if (containerExists('#destination-refinement-list')) { 323 widgets.push(instantsearch.widgets.refinementList({ 324 container: '#destination-refinement-list', 325 attribute: 'activity_destination.name', 326 limit: 5, 327 showMore: true, 328 showMoreLimit: 15, 329 searchable: false, 330 searchablePlaceholder: 'Search destination...' 331 })); 332 } 333 334 if (containerExists('#destination-heir-list')) { 335 // widgets.push( 336 // instantsearch.widgets.hierarchicalMenu({ 337 // container: '[data-widget="destination"]', 338 // attributes: [ 339 // 'destination_hierarchy.lvl0', 340 // 'destination_hierarchy.lvl1', 341 // // 'destination_hierarchy.lvl2', // include if you index it 342 // ], 343 // limit: 5, 344 // showMore: true, 345 // showMoreLimit: 20, 346 // // sortBy: ['name:asc'], 347 // translations: { 348 // showMoreButtonText({ isShowingMore }) { 349 // return isShowingMore ? 'Show less' : 'Show more'; 350 // } 351 // } 352 // }) 353 // ); 354 355 widgets.push(instantsearch.widgets.refinementList({ 356 container: '[data-widget="destination"]', 357 attribute: 'activity_destination.name', 358 limit: 5, 359 showMore: true, 360 showMoreLimit: 15, 361 searchable: false, 362 searchablePlaceholder: 'Search destination...' 363 })); 364 } 365 366 367 if (containerExists('#on-sale-toggle')) { 368 widgets.push(instantsearch.widgets.toggleRefinement({ 369 container: '#on-sale-toggle', 370 attribute: 'price.on_sale', 371 label: 'On Sale', 372 })); 373 } 374 375 // if (containerExists('#availability-filter')) { 376 // widgets.push(instantsearch.widgets.refinementList({ 377 // container: '#availability-filter', 378 // attribute: 'availability_by_month', 379 // searchable: false, 380 // })); 381 // } 382 383 // if (containerExists('#travel-style-filter')) { 384 // widgets.push(instantsearch.widgets.refinementList({ 385 // container: '#travel-style-filter', 386 // attribute: 'travel_style', 387 // searchable: false, 388 // })); 389 // } 390 391 if (containerExists('#travel-duration-filter')) { 392 widgets.push(instantsearch.widgets.refinementList({ 393 container: '#travel-duration-filter', 394 attribute: 'travel_duration', 395 searchable: false, 396 limit: 100, 397 sortBy: ['custom'], // use 'custom' sorting 398 transformItems(items) { 399 const pinnedLabel = '1/2 Day'; 400 const pinnedItem = items.find(item => item.label === pinnedLabel); 401 402 const otherItems = items 403 .filter(item => item.label !== pinnedLabel) 404 // Natural sort: compare numbers in strings 405 .sort((a, b) => { 406 const numA = parseFloat(a.label); 407 const numB = parseFloat(b.label); 408 if (!isNaN(numA) && !isNaN(numB)) { 409 return numA - numB; // numeric ascending 410 } 411 return a.label.localeCompare(b.label); // fallback to string compare 412 }); 413 414 return pinnedItem ? [pinnedItem, ...otherItems] : otherItems; 415 }, 416 })); 417 } 418 419 if (containerExists('#sort-by')) { 420 widgets.push(instantsearch.widgets.sortBy({ 421 container: '#sort-by', 422 items: [ 423 { label: 'Most Popular', value: INDEX_NAME + '_popular' }, 424 { label: 'Price: Low to High', value: INDEX_NAME + '_price_asc' }, 425 { label: 'Price: High to Low', value: INDEX_NAME + '_price_desc' }, 426 { label: 'Newest First', value: INDEX_NAME + '_date_desc' }, 427 { label: 'On Sale', value: INDEX_NAME + '_on_sale_first' } 428 ], 429 defaultOption: INDEX_NAME + '_popular', 430 })); 431 } 432 433 if (containerExists('#hits-per-page')) { 434 widgets.push(instantsearch.widgets.hitsPerPage({ 435 container: '#hits-per-page', 436 items: [ 437 { label: '9 per page', value: 9, default: true }, 438 { label: '12 per page', value: 12 }, 439 { label: '20 per page', value: 20 } 440 ] 441 })); 442 } 443 444 if (containerExists('#hits')) { 445 widgets.push(instantsearch.widgets.hits({ 446 container: '#hits', 447 templates: { 448 item: (hit, { html }) => { 449 // Decode HTML entities 450 let decodeHTML = (str) => { 451 let txt = document.createElement("textarea"); 452 txt.innerHTML = str; 453 return txt.value; 454 }; 455 // console.log("Hits", hit); 456 // Helper: Render stars 457 let renderStars = (rating) => { 458 if (!rating || rating === 0) return ''; 459 let fullStars = Math.floor(rating); 460 let stars = ''; 461 for (let i = 0; i < fullStars; i++) { 462 stars += '<span class="star">â </span>'; 463 } 464 let emptyStars = 5 - fullStars; 465 for (let i = 0; i < emptyStars; i++) { 466 stars += '<span class="star" style="color: #e5e7eb;">â </span>'; 467 } 468 return stars; 469 }; 470 471 let productTitle = html`<h4 class="talkk_ht-latest-products__title">${html([decodeHTML(hit.title)])}</h4>`; 472 let featuredImage = html`<img src="${hit.featured_image}" alt="${hit.title}" />`; 473 if( algolia_ajax_object.algolia_page_products.length > 0 ) { 474 let matchedProduct = algolia_ajax_object.algolia_page_products.find( 475 item => item.product == hit.objectID 476 ); 477 featuredImage = html`<img src="${ 478 matchedProduct?.product_image_override_url || hit.featured_image 479 }" alt="${hit.title}" />`; 480 let hitTitle = matchedProduct?.product_title_override || hit.title; 481 productTitle = html`<h4 class="talkk_ht-latest-products__title">${html([decodeHTML(hitTitle)])}</h4>`; 482 } 483 // Helper: Format price and remove .00 if not needed 484 let formatPrice = (price, currency = 'AUD') => { 485 if (price === null || price === undefined) return ''; 486 let formatted = parseFloat(price).toLocaleStr
486ing('en-AU', { 487 style: 'currency', 488 currency: currency, 489 minimumFractionDigits: 0, 490 maximumFractionDigits: 2 491 }); 492 // remove trailing .00 (e.g. $199.00 â $199) 493 return formatted.replace(/\.00$/, ''); 494 }; 495 496 // Tags as tags 497 let categories = []; 498 if (hit.activity_packages && Array.isArray(hit.activity_packages)) { 499 hit.activity_packages.forEach(category => { 500 if( category.name != "Uncategorized" ){ 501 categories.push(html`<span class="talkk_ht-latest-products__category product-tag-${category.slug}">${decodeHtml(category.name)}</span>`); 502 } 503 }); 504 } 505 506 let metaItems = []; 507 if( destination ){ 508 metaItems.push(html` 509 <div class="talkk_ht-latest-products__meta-item"> 510 <svg width="9" height="10" viewBox="0 0 9 10" fill="none" xmlns="http://www.w3.org/2000/svg"> 511 <path d="M4.18709 9.99947L3.89664 9.75051C3.49623 9.41524 0 6.39576 0 4.18709C0 1.87463 1.87463 0 4.18709 0C6.49955 0 8.37418 1.87463 8.37418 4.18709C8.37418 6.39578 4.87795 9.41525 4.4792 9.75217L4.18709 9.99947ZM4.18709 0.905371C2.37551 0.907422 0.907441 2.37549 0.905391 4.18707C0.905391 5.57461 3.05641 7.79117 4.18709 8.80569C5.31779 7.79076 7.46879 5.57293 7.46879 4.18707C7.46674 2.37549 5.99869 0.907441 4.18709 0.905371Z" fill="#5C5C5C" /> 512 <path d="M4.18707 5.8468C3.27043 5.8468 2.52734 5.10371 2.52734 4.18707C2.52734 3.27043 3.27043 2.52734 4.18707 2.52734C5.10371 2.52734 5.8468 3.27043 5.8468 4.18707C5.8468 5.10371 5.10373 5.8468 4.18707 5.8468ZM4.18707 3.35719C3.72875 3.35719 3.35721 3.72873 3.35721 4.18705C3.35721 4.64537 3.72875 5.01691 4.18707 5.01691C4.64539 5.01691 5.01693 4.64537 5.01693 4.18705C5.01693 3.72873 4.64541 3.35719 4.18707 3.35719Z" fill="#5C5C5C" /> 513 </svg> 514 <span>${destination}</span> 515 </div> 516 `); 517 } 518 else if (hit.destination_child && hit.destination_child.length > 0) { 519 metaItems.push(html` 520 <div class="talkk_ht-latest-products__meta-item"> 521 <svg width="9" height="10" viewBox="0 0 9 10" fill="none" xmlns="http://www.w3.org/2000/svg"> 522 <path d="M4.18709 9.99947L3.89664 9.75051C3.49623 9.41524 0 6.39576 0 4.18709C0 1.87463 1.87463 0 4.18709 0C6.49955 0 8.37418 1.87463 8.37418 4.18709C8.37418 6.39578 4.87795 9.41525 4.4792 9.75217L4.18709 9.99947ZM4.18709 0.905371C2.37551 0.907422 0.907441 2.37549 0.905391 4.18707C0.905391 5.57461 3.05641 7.79117 4.18709 8.80569C5.31779 7.79076 7.46879 5.57293 7.46879 4.18707C7.46674 2.37549 5.99869 0.907441 4.18709 0.905371Z" fill="#5C5C5C" /> 523 <path d="M4.18707 5.8468C3.27043 5.8468 2.52734 5.10371 2.52734 4.18707C2.52734 3.27043 3.27043 2.52734 4.18707 2.52734C5.10371 2.52734 5.8468 3.27043 5.8468 4.18707C5.8468 5.10371 5.10373 5.8468 4.18707 5.8468ZM4.18707 3.35719C3.72875 3.35719 3.35721 3.72873 3.35721 4.18705C3.35721 4.64537 3.72875 5.01691 4.18707 5.01691C4.64539 5.01691 5.01693 4.64537 5.01693 4.18705C5.01693 3.72873 4.64541 3.35719 4.18707 3.35719Z" fill="#5C5C5C" /> 524 </svg> 525 <span>${decodeHTML(hit.destination_child[0])}</span> 526 </div> 527 `); 528 } 529 else{ 530 metaItems.push(html` 531 <div class="talkk_ht-latest-products__meta-item"> 532 <svg width="9" height="10" viewB
532ox="0 0 9 10" fill="none" xmlns="http://www.w3.org/2000/svg"> 533 <path d="M4.18709 9.99947L3.89664 9.75051C3.49623 9.41524 0 6.39576 0 4.18709C0 1.87463 1.87463 0 4.18709 0C6.49955 0 8.37418 1.87463 8.37418 4.18709C8.37418 6.39578 4.87795 9.41525 4.4792 9.75217L4.18709 9.99947ZM4.18709 0.905371C2.37551 0.907422 0.907441 2.37549 0.905391 4.18707C0.905391 5.57461 3.05641 7.79117 4.18709 8.80569C5.31779 7.79076 7.46879 5.57293 7.46879 4.18707C7.46674 2.37549 5.99869 0.907441 4.18709 0.905371Z" fill="#5C5C5C" /> 534 <path d="M4.18707 5.8468C3.27043 5.8468 2.52734 5.10371 2.52734 4.18707C2.52734 3.27043 3.27043 2.52734 4.18707 2.52734C5.10371 2.52734 5.8468 3.27043 5.8468 4.18707C5.8468 5.10371 5.10373 5.8468 4.18707 5.8468ZM4.18707 3.35719C3.72875 3.35719 3.35721 3.72873 3.35721 4.18705C3.35721 4.64537 3.72875 5.01691 4.18707 5.01691C4.64539 5.01691 5.01693 4.64537 5.01693 4.18705C5.01693 3.72873 4.64541 3.35719 4.18707 3.35719Z" fill="#5C5C5C" /> 535 </svg> 536 <span>Australia</span> 537 </div> 538 `); 539 } 540 if (hit.travel_duration && hit.travel_duration != '' ) { 541 metaItems.push(html` 542 <div class="talkk_ht-latest-products__meta-item"> 543 <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> 544 <path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10 10-4.5 10-10S17.5 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm.5-13H11v6l5.2 3.2.8-1.3-4.5-2.7V7z" fill="currentColor" /> 545 </svg> 546 <span>${hit.travel_duration}</span> 547 </div> 548 `); 549 } 550 if (hit.travel_style && hit.travel_style != '') { 551 metaItems.push(html` 552 <div class="talkk_ht-latest-products__meta-item"> 553 <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> 554 <path d="M16 11c1.66 0 2.99-1.34 2.99-3S17.66 5 16 5c-1.66 0-3 1.34-3 3s1.34 3 3 3zm-8 0c1.66 0 2.99-1.34 2.99-3S9.66 5 8 5C6.34 5 5 6.34 5 8s1.34 3 3 3zm0 2c-2.33 0-7 1.17-7 3.5V19h14v-2.5c0-2.33-4.67-3.5-7-3.5zm8 0c-.29 0-.62.02-.97.05 1.16.84 1.97 1.97 1.97 3.45V19h6v-2.5c0-2.33-4.67-3.5-7-3.5z" fill="currentColor" /> 555 </svg> 556 <span>${hit.travel_style}</span> 557 </div> 558 `); 559 } 560 561 // let departureItems = []; 562 // if (hit.departs && hit.departs.length) { 563 // departureItems.push(html` 564 // <div class="talkk_ht-latest-products__meta-item"> 565 // Departs: 566 // <span>${hit.departs.join(' / ')}</span> 567 // </div> 568 // `); 569 // } 570 // if (hit.finishes && hit.finishes.length) { 571 // departureItems.push(html` 572 // <div class="talkk_ht-latest-products__meta-item" 573 // Finishes: 574 // <span>${hit.finishes.join(' / ')}</span> 575 // </div> 576 // `); 577 // } 578 // let travelTypeItems = []; 579 // if (hit.travel_type && hit.travel_type.length) { 580 // travelTypeItems.push(html` 581 // <span class="talkk_ht-latest-products__category">${hit.travel_type.join(', ')}</span> 582 // `); 583 // } 584 // if (hit.accomodation && hit.accomodation.length) { 585 // travelTypeItems.push(html`
586 // <span class="talkk_ht-latest-products__category">${hit.accomodation.join(', ')}</span> 587 // `); 588 // } 589 function decodeHtml(html) { 590 const txt = document.createElement("textarea"); 591 txt.innerHTML = html; 592 return txt.value; 593 } 594 595 // Stock class 596 let stockClass = hit.stock?.in_stock ? 'in-stock' : 'out-of-stock'; 597 stockClass += hit.special_product ? ' is_custom_package' : ''; 598 599 // Sale logic 600 let now = new Date(); 601 let saleFrom = hit.sale_from ? new Date(hit.sale_from) : null; 602 let saleTo = hit.sale_to ? new Date(hit.sale_to) : null; 603 let isSaleActive = 604 hit.price?.on_sale && 605 hit.price.sale !== null && 606 (!saleFrom || now >= saleFrom) && 607 (!saleTo || now <= saleTo); 608 609 // Tags as tags 610 let badge = ''; 611 if (hit.tags && Array.isArray(hit.tags) && hit.tags.length > 0) { 612 badge = html`<span class="talkk_ht-latest-products__label">${hit.tags[0].name || hit.tags[0]}</span>`; 613 } 614 if(isSaleActive && hit.price.regular){ 615 badge = html`<span class="talkk_ht-latest-products__label green">Save $${(hit.price.regular - hit.price.sale).toFixed(2).replace(/\.00$/, '') }</span>`; 616 } 617 // Rating badge 618 let ratingBadge = ''; 619 if (hit.rating && hit.rating.count > 0) { 620 ratingBadge = html` 621 <div class="talkk_ht-latest-products__rating-badge"> 622 <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> 623 <path d="M12 2L15.09 8.26L22 9.27L17 14.14L18.18 21.02L12 17.77L5.82 21.02L7 14.14L2 9.27L8.91 8.26L12 2Z" fill="#FF8A00"/> 624 </svg> 625 <span class="talkk_ht-latest-products__rating-value">${hit.rating.average.toFixed(1)}</span> 626 <span class="talkk_ht-latest-products__rating-count">(${hit.rating.count})</span> 627 </div> 628 `; 629 } 630 631 return html` 632 <a class="talkk_ht-latest-products__single ${stockClass}" href="${hit.link}"> 633 <div class="talkk_ht-latest-products__img"> 634 ${badge} 635 ${ratingBadge} 636 ${featuredImage} 637 </div> 638 <div class="talkk_ht-latest-products__content"> 639 <div class="talkk_ht-latest-products__body"> 640 ${productTitle} 641 642 <div class="talkk_ht-latest-products__body--desc"> 643 ${hit.description 644 ? html`<p class="product-description">${html([decodeHTML(hit.description)])}</p>` 645 : ''} 646 </div> 647 <div class="talkk_ht-latest-products__meta">${metaItems}</div> 648 <div class="talkk_ht-latest-products__meta talkk_ht-latest-products__categories">${categories}</div> 649 </div> 650 <div class="talkk_ht-latest-products__footer"> 651 <div class="talkk_ht-latest-products__footer--left"> 652 <div class="talkk_ht-latest-products__price--fromAUD"> 653 From (AUD) 654 </div> 655 656 ${isSaleActive 657 ? html` 658 <!-- SALE PRICE --> 659 <div class="talkk_ht-latest-products__price"> 660 <bdi>${formatPrice(hit.price.sale, hit.price.currency)}</bdi> 661 <div class="talkk_ht-latest-products__price--regular-crossed"> 662 <bdi>${formatPrice(hit.price.regular, hit.price.currency)}</bdi> 663 </div> 664 </div> 665 ` 666 : html` 667 <!-- REGULAR PRICE --> 668 <div class="talkk_ht-latest-products__price"> 669 <bdi>${formatPrice(Math.floor(hit.price.regular), hit.price.currency)}</bdi> 670 </div> 671 ` 672 } 673 </div> 674 <div class="talkk_ht-latest-products__footer--right"> 675 <div class="talkk_ht-latest-products__btn">Book Now</div> 676 </div> 677 </div> 678 </div> 679 </a> 680 `; 681 }, 682 683 empty: ({ query }, { html }) => html` 684 <div class="no-results"> 685 <div class="no-results-icon">ð</div> 686 <h3>No products found</h3> 687 <p> 688 ${query 689 ? html`No results for <strong>"${query}"</strong>. Try different keywords.` 690 : 'Try searching for products or adju
690sting your filters.' 691 } 692 </p> 693 </div> 694 ` 695 } 696 })); 697 } 698 699 if (containerExists('#pagination')) { 700 widgets.push(instantsearch.widgets.pagination({ 701 container: '#pagination', 702 padding: 2, 703 showFirst: true, 704 showLast: true, 705 scrollTo: '.algolia-instantsearch-container', 706 // scrollTo: false, // â disable default scroll behavior 707 })); 708 // // ð Add a render listener to handle custom scroll logic 709 // search.on('render', () => { 710 // const target = document.querySelector('.algolia-instantsearch-container'); 711 // const offset = 200; // adjust this value as needed (in px) 712 // if (target) { 713 // const y = target.getBoundingClientRect().top + window.scrollY - offset; 714 // window.scrollTo({ 715 // top: y, 716 // behavior: 'smooth', 717 // }); 718 // } 719 // }); 720 721 // // Force price slider to start with no filter 722 // search.on('render', () => { 723 // if (!search._priceResetDone) { 724 // search.helper.clearRefinements('price.current').search(); 725 // search._priceResetDone = true; 726 // } 727 // }); 728 } 729 730 if (containerExists('#clear-refinements')) { 731 widgets.push(instantsearch.widgets.clearRefinements({ 732 container: '#clear-refinements', 733 excludedAttributes: ['query'], 734 templates: { 735 resetLabel: 'Clear All' 736 }, 737 })); 738 739 } 740 if (containerExists('#clear-refinements-mobile')) { 741 widgets.push(instantsearch.widgets.clearRefinements({ 742 container: '#clear-refinements-mobile', 743 excludedAttributes: ['query'], 744 templates: { 745 resetLabel: 'Clear All' 746 }, 747 })); 748 } 749 console.log(configureParams); 750 // Always add configure widget 751 widgets.push(instantsearch.widgets.configure(configureParams)); 752 753 // Add all widgets to search 754 search.addWidgets(widgets); 755 console.log('search:', search); 756 // Error handling 757 search.on('error', ({ error }) => { 758 console.error('InstantSearch error:', error); 759 }); 760 761 // Start search 762 search.start(); 763 } 764 765 document.addEventListener('DOMContentLoaded', initializeSearch); 766})(); 767(function () { 768 const OPEN_CLASS = 'open'; 769 const ANIMATING_CLASS = 'animating'; 770 771 // Toggle open/close on header click 772 document.addEventListener('click', (e) => { 773 const toggle = e.target.closest('.filter-toggle'); 774 if (!toggle) return; 775 776 const widget = toggle.closest('.filter-widget'); 777 if (!widget) return; 778 779 // Close other open widgets first 780 document.querySelectorAll('.filter-widget.' + OPEN_CLASS).forEach(w => { 781 if (w !== widget) closeWidget(w); 782 }); 783 784 // Toggle this one 785 if (widget.classList.contains(OPEN_CLASS)) { 786 closeWidget(widget); 787 widget.classList.remove(OPEN_CLASS); 788 widget.classList.remove(ANIMATING_CLASS); 789 } else { 790 openWidget(widget); 791 } 792 }); 793 794 // Outside click 795 document.addEventListener('click', outsideClick); 796 797 // ESC to close all 798 document.addEventListener('keydown', (e) => { 799 if (e.key === 'Escape') { 800 document.querySelectorAll('.filter-widget.' + OPEN_CLASS).forEach(closeWidget); 801 } 802 }); 803 804 function outsideClick(e) { 805 // Ignore clicks inside toggle or content 806 if (closestInPath(e, '.filter-toggle') || closestInPath(e, '.filter-content')) return; 807 document.querySelectorAll('.filter-widget.' + OPEN_CLASS).forEach(closeWidget); 808 } 809 810 // --------------------- 811 // Helpers 812 // --------------------- 813 814 function openWidget(widget) { 815 // if (widget.classList.contains(ANIMATING_CLASS)) return; 816 widget.classList.add(ANIMATING_CLASS); 817 818 const panel = widget.querySelector('.filter-content'); 819 // if (!panel) return; 820 821 widget.classList.add(OPEN_CLASS); 822 // panel.hidden = false;
823 824 // ð¥ Force layout before measuring height 825 panel.style.maxHeight = '0px'; 826 panel.offsetHeight; // Force reflow 827 828 const target = panel.scrollHeight; 829 panel.style.maxHeight = target + 'px'; 830 831 const onEnd = (ev) => { 832 if (ev.propertyName !== 'max-height') return; 833 panel.style.maxHeight = 'none'; // allow dynamic height after open 834 widget.classList.remove(ANIMATING_CLASS); 835 panel.removeEventListener('transitionend', onEnd); 836 }; 837 panel.addEventListener('transitionend', onEnd); 838 widget.classList.remove(ANIMATING_CLASS); 839 } 840 841 function closeWidget(widget) { 842 if (widget.classList.contains(ANIMATING_CLASS)) return; 843 widget.classList.add(ANIMATING_CLASS); 844 845 const panel = widget.querySelector('.filter-content'); 846 // if (!panel) return; 847 848 // If currently no maxHeight, set it first to enable transition 849 if (getComputedStyle(panel).maxHeight === 'none') { 850 panel.style.maxHeight = panel.scrollHeight + 'px'; 851 panel.offsetHeight; // reflow 852 } 853 854 panel.style.maxHeight = '0px'; 855 widget.classList.remove(OPEN_CLASS); 856 857 const onEnd = (ev) => { 858 // if (ev.propertyName !== 'max-height') return; 859 // panel.hidden = true; 860 widget.classList.remove(ANIMATING_CLASS); 861 panel.removeEventListener('transitionend', onEnd); 862 }; 863 panel.addEventListener('transitionend', onEnd); 864 } 865 866 // Reliable inside detection 867 function closestInPath(event, selector) { 868 if (event.composedPath) { 869 for (const node of event.composedPath()) { 870 if (node instanceof Element && node.matches(selector)) return node; 871 } 872 } 873 return event.target.closest(selector); 874 } 875 })(); 876 877 // ------------------------ 878 // Mobile Filters (jQuery) 879 // ------------------------ 880 jQuery(function ($) { 881 const $filters = $('.filters-section'); 882 const $modal = $('#mobileFilters'); 883 const $modalBody = $('#mobileFiltersBody'); 884 const BP = 768; 885 let active = false; 886 887 function isMobile() { 888 return window.matchMedia('(max-width:' + BP + 'px)').matches; 889 } 890 891 function openFilters() { 892 $('body').addClass('filters-open'); 893 $modal.attr('aria-hidden', 'false'); 894 $('.filter-mobile').attr('aria-expanded', 'true'); 895 } 896 897 function closeFilters() { 898 $modal.attr('aria-hidden', 'true'); 899 $('body').removeClass('filters-open'); 900 $('.filter-mobile').attr('aria-expanded', 'false'); 901 } 902 903 function bindMobile() { 904 if (active) return; 905 active = true; 906 907 // Move filters into modal 908 if ($filters.length && $filters.parent()[0] !== $modalBody[0]) { 909 $filters.detach().appendTo($modalBody).show(); 910 } 911 912 // Mobile events 913 $(document) 914 .on('click.filtersMobile', '.filter-mobile', function (e) { e.preventDefault(); openFilters(); }) 915 .on('click.filtersMobile', '.js-close-filters', function (e) { e.preventDefault(); closeFilters(); }) 916 .on('keydown.filtersMobile', function (e) { 917 if (e.key === 'Escape' && $modal.attr('aria-hidden') === 'false') closeFilters(); 918 }) 919 .on('click.filtersMobile', '.js-apply-filters', function () { closeFilters(); }) 920 .on('click.filtersMobile', '.js-clear-filters', function () { 921 const $btn = $filters.find('#clear-refinements .ais-ClearRefinements-button'); 922 if ($btn.length) { 923 $btn.trigger('click'); 924 } else { 925 $filters.find('input[type="checkbox"], input[type="radio"]').prop('checked', false).trigger('change'); 926 $filters.find('input[type="text"], input[type="search"], input[type="number"]').val('').trigger('input change'); 927 $filters.find('select').prop('selectedIndex', 0).trigger('change'); 928 } 929 }); 930 } 931 932 function unbindMobile() { 933 if (!active) return; 934 active = false; 935 closeFilters(); 936 $(document).off('.filtersMobile'); 937 // Optional: move filters back to desktop 938 // $filters.detach().insertAfter('#someDesktopAnchor'); 939 } 940 941 function toggleByViewport() { 942 if (isMobile()) bindMobile(); else unbindMobile(); 943 } 944 945 toggleByViewport(); 946 $(window).on('resize orientationchange', toggleByViewport); 947 });
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.