(function () { 'use strict'; var header = document.getElementById('siteHeader'); var nav = document.getElementById('primaryNav'); var burger = document.getElementById('burger'); var navToggle = nav && nav.querySelector('.nav__toggle'); var mqDesktop = window.matchMedia('(min-width: 60.0625rem)'); /* ---------- mobile menu ---------- */ function closeMenu() { if (!nav || !burger) return; nav.classList.remove('is-open'); burger.setAttribute('aria-expanded', 'false'); burger.setAttribute('aria-label', 'Open menu'); } if (burger && nav) { burger.addEventListener('click', function () { var open = nav.classList.toggle('is-open'); burger.setAttribute('aria-expanded', String(open)); burger.setAttribute('aria-label', open ? 'Close menu' : 'Open menu'); }); document.addEventListener('click', function (e) { if (mqDesktop.matches) return; if (!nav.contains(e.target) && !burger.contains(e.target)) closeMenu(); }); nav.addEventListener('click', function (e) { if (!mqDesktop.matches && e.target.closest('a')) closeMenu(); }); } document.addEventListener('keydown', function (e) { if (e.key !== 'Escape') return; closeMenu(); if (navToggle) navToggle.setAttribute('aria-expanded', 'false'); }); mqDesktop.addEventListener('change', function (e) { if (e.matches) closeMenu(); }); /* ---------- Tours submenu ---------- */ if (navToggle) { navToggle.addEventListener('click', function () { var open = navToggle.getAttribute('aria-expanded') === 'true'; navToggle.setAttribute('aria-expanded', String(!open)); }); } /* ---------- mobile "Book now" inside the menu ---------- */ var desktopCta = document.querySelector('.header__cta'); var navList = nav && nav.querySelector('.nav__list'); if (desktopCta && navList) { var li = document.createElement('li'); li.className = 'nav__mobile-cta'; var clone = desktopCta.cloneNode(true); clone.classList.remove('header__cta'); clone.classList.add('nav__cta'); li.appendChild(clone); navList.appendChild(li); } /* ---------- sticky header past the hero ---------- */ var hero = document.querySelector('.hero, .hero-tour'); if (header && hero && 'IntersectionObserver' in window) { var sentinel = document.createElement('div'); sentinel.setAttribute('aria-hidden', 'true'); sentinel.style.cssText = 'position:absolute;top:0;left:0;width:1px;height:60vh;pointer-events:none'; hero.appendChild(sentinel); new IntersectionObserver(function (entries) { header.classList.toggle('is-stuck', !entries[0].isIntersecting); }, { threshold: 0 }).observe(sentinel); } /* ---------- card rails (prev / next) ---------- */ function railStep(rail) { var card = rail.firstElementChild; if (!card) return rail.clientWidth; var gap = parseFloat(getComputedStyle(rail).columnGap) || 0; return card.getBoundingClientRect().width + gap; } function syncRailButtons(rail, prev, next) { var max = rail.scrollWidth - rail.clientWidth - 1; if (prev) prev.disabled = rail.scrollLeft <= 0; if (next) next.disabled = rail.scrollLeft >= max; } Array.prototype.forEach.call(document.querySelectorAll('.rail'), function (rail) { var prev = document.querySelector('[data-rail-prev="' + rail.id + '"]'); var next = document.querySelector('[data-rail-next="' + rail.id + '"]'); if (prev) prev.addEventListener('click', function () { rail.scrollBy({ left: -railStep(rail), behavior: 'smooth' }); }); if (next) next.addEventListener('click', function () { rail.scrollBy({ left: railStep(rail), behavior: 'smooth' }); }); rail.addEventListener('scroll', function () { syncRailButtons(rail, prev, next); }, { passive: true }); window.addEventListener('resize', function () { syncRailButtons(rail, prev, next); }); syncRailButtons(rail, prev, next); }); /* ---------- testimonials marquee: duplicate for a seamless loop ---------- */ var marquee = document.querySelector('[data-marquee] .marquee__track'); if (marquee) { var originals = Array.prototype.slice.call(marquee.children); originals.forEach(function (node) { var copy = node.cloneNode(true); copy.setAttribute('aria-hidden', 'true'); marquee.appendChild(copy); }); } /* ---------- FAQ accordion ---------- */ var accordion = document.querySelector('[data-accordion]'); if (accordion) { accordion.addEventListener('click', function (e) { var btn = e.target.closest('.acc__q'); if (!btn) return; var open = btn.getAttribute('aria-expanded') === 'true'; Array.prototype.forEach.call(accordion.querySelectorAll('.acc__q'), function (other) { other.setAttribute('aria-expanded', 'false'); }); btn.setAttribute('aria-expanded', String(!open)); }); } /* ---------- date field: text placeholder, native picker on focus ---------- */ var dateField = document.querySelector('[data-datefield]'); if (dateField) { dateField.addEventListener('focus', function () { dateField.type = 'date'; if (typeof dateField.showPicker === 'function') { try { dateField.showPicker(); } catch (err) { /* not user-activated */ } } }); dateField.addEventListener('blur', function () { if (!dateField.value) dateField.type = 'text'; }); } /* ---------- booking form ---------- */ var form = document.getElementById('booking'); if (form) { form.addEventListener('submit', function (e) { e.preventDefault(); var data = Object.fromEntries(new FormData(form).entries()); console.log('Booking request', data); }); } /* ---------- tour details: check availability ---------- */ var availability = document.getElementById('availability'); if (availability) { availability.addEventListener('submit', function (e) { e.preventDefault(); console.log('Availability request'); }); } /* ---------- tour archive: filtering, sorting, pagination ---------- */ var filters = document.getElementById('filters'); var grid = document.getElementById('tourGrid'); if (filters && grid) { var PER_PAGE = 9; /* the card the design highlights — the middle one of a full row of three */ var HIGHLIGHT = 4; var items = Array.prototype.slice.call(grid.querySelectorAll('.titem')); var pager = document.getElementById('pager'); var pagerPages = document.getElementById('pagerPages'); var prevBtn = pager && pager.querySelector('[data-page="prev"]'); var nextBtn = pager && pager.querySelector('[data-page="next"]'); var countEl = document.getElementById('resultCount'); var emptyEl = document.getElementById('tourEmpty'); var resetBtns = Array.prototype.slice.call(document.querySelectorAll('[data-reset]')); var searchInput = filters.querySelector('#f-q'); var matches = items.slice(); var page = 1; var searchTimer = null; function value(name) { var field = filters.elements[name]; return field ? field.value : ''; } function checked(name) { var field = filters.elements[name]; return !!(field && field.checked); } function readFilters() { return { /* every word has to appear, so "night sydney" finds the same tour as "sydney night" */ words: value('q').trim().toLowerCase().split(/\s+/).filter(Boolean), destination: value('destination'), category: value('category'), price: value('price'), duration: value('duration'), sort: value('sort') || 'newest', freeCancellation: checked('free-cancellation'), bestDeal: checked('best-deal'), likelyToSellOut: checked('likely-to-sell-out') }; } function isFiltered(f) { return !!(f.words.length || f.destination || f.category || f.price || f.duration || f.freeCancellation || f.bestDeal || f.likelyToSellOut); } /* ranges are inclusive at the top and exclusive at the bottom, so "$50 - $100" and "Up to $50" tile without both claiming a $50 tour */ function inPriceRange(price, range) { if (!range) return true; if (range === '200-plus') return price > 200; var bounds = range.split('-'); var low = Number(bounds[0]); var high = Number(bounds[1]); return (low === 0 || price > low) && price <= high; } function keeps(el, f) { var data = el.dataset; if (f.destination && data.city !== f.destination) return false; if (f.category && data.category !== f.category) return false; if (f.duration && data.duration !== f.duration) return false; if (!inPriceRange(Number(data.price), f.price)) return false; if (f.freeCancellation && data.freeCancellation !== 'true') return false; if (f.bestDeal && data.bestDeal !== 'true') return false; if (f.likelyToSellOut && data.likelyToSellOut !== 'true') return false; return f.words.every(function (word) { return data.search.indexOf(word) > -1; }); } var SORTS = { newest: function (a, b) { return Number(b.dataset.added) - Number(a.dataset.added); }, 'price-asc': function (a, b) { return Number(a.dataset.price) - Number(b.dataset.price); }, 'price-desc': function (a, b) { return Number(b.dataset.price) - Number(a.dataset.price); }, rating: function (a, b) { return Number(b.dataset.rating) - Number(a.dataset.rating); }, duration: function (a, b) { return durationDays(a) - durationDays(b); } }; function durationDays(el) { return el.dataset.duration === 'half-day' ? 0.5 : Number(el.dataset.duration); } function pageNumbers(total, current) { var pages = []; var n; /* a short run fits without an ellipsis */ if (total <= 7) { for (n = 1; n <= total; n++) pages.push(n); return pages; } var push = function (n) { if (n >= 1 && n <= total && pages.indexOf(n) < 0) pages.push(n); }; push(1); push(current - 1); push(current); push(current + 1); push(total); pages.sort(function (a, b) { return a - b; }); var out = []; pages.forEach(function (n, i) { if (i > 0 && n - pages[i - 1] > 1) out.push(null); /* an ellipsis */ out.push(n); }); return out; } function renderPager(totalPages) { if (!pager) return; pager.hidden = totalPages < 2; if (pagerPages) { var html = ''; pageNumbers(totalPages, page).forEach(function (n) { if (n === null) { html += ''; return; } html += ''; }); pagerPages.innerHTML = html; } if (prevBtn) prevBtn.disabled = page === 1; if (nextBtn) nextBtn.disabled = page === totalPages; } /* built from numbers only, so the markup here is safe to assign as HTML */ function countText(start, shownCount) { var total = matches.length; if (!total) return 'No tours found'; var noun = total === 1 ? 'tour' : 'tours'; if (shownCount === total) return 'Showing ' + total + ' ' + noun; return 'Showing ' + (start + 1) + '–' + (start + shownCount) + ' of ' + total + ' ' + noun; } function render() { var totalPages = Math.max(1, Math.ceil(matches.length / PER_PAGE)); if (page > totalPages) page = totalPages; var start = (page - 1) * PER_PAGE; var shown = matches.slice(start, start + PER_PAGE); items.forEach(function (el) { el.hidden = true; setHighlight(el, false); }); /* re-order the DOM so the sort order is what a keyboard/screen reader sees */ matches.forEach(function (el) { grid.appendChild(el); }); shown.forEach(function (el, i) { el.hidden = false; setHighlight(el, shown.length > HIGHLIGHT && i === HIGHLIGHT); }); if (countEl) countEl.innerHTML = countText(start, shown.length); if (emptyEl) emptyEl.hidden = matches.length > 0; renderPager(totalPages); } function setHighlight(el, on) { el.classList.toggle('is-active', on); var card = el.querySelector('.dcard'); var badge = el.querySelector('.ring-badge--card'); if (card) card.classList.toggle('is-active', on); if (badge) badge.classList.toggle('ring-badge--accent', on); } function apply(resetPage) { var f = readFilters(); matches = items.filter(function (el) { return keeps(el, f); }); matches.sort(SORTS[f.sort] || SORTS.newest); if (resetPage !== false) page = 1; resetBtns.forEach(function (btn) { btn.hidden = !isFiltered(f); }); render(); } function goToPage(next, totalPages) { if (next < 1 || next > totalPages || next === page) return; page = next; render(); var list = document.getElementById('tour-list'); if (list) list.scrollIntoView({ block: 'start', behavior: 'smooth' }); } filters.addEventListener('submit', function (e) { e.preventDefault(); apply(); }); filters.addEventListener('change', function () { apply(); }); if (searchInput) { searchInput.addEventListener('input', function () { window.clearTimeout(searchTimer); searchTimer = window.setTimeout(apply, 200); }); } resetBtns.forEach(function (btn) { btn.addEventListener('click', function () { filters.reset(); apply(); filters.scrollIntoView({ block: 'center', behavior: 'smooth' }); }); }); if (pagerPages) { pagerPages.addEventListener('click', function (e) { var btn = e.target.closest('[data-page]'); if (!btn) return; goToPage(Number(btn.getAttribute('data-page')), Math.max(1, Math.ceil(matches.length / PER_PAGE))); }); } if (prevBtn) prevBtn.addEventListener('click', function () { goToPage(page - 1, Math.max(1, Math.ceil(matches.length / PER_PAGE))); }); if (nextBtn) nextBtn.addEventListener('click', function () { goToPage(page + 1, Math.max(1, Math.ceil(matches.length / PER_PAGE))); }); apply(); } })();