PageSourceSearch

https://happytravels.com.au/wp-content/themes/bonvoyage-child/js/algolia-scripts.js?ver=1791300062

js happytravels.com.au collected 2026-10-06 19:59:22 UTC 45,200 bytes, 947 lines download raw bytes

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.