Files
TourWebsite/js/main.js
2026-09-09 11:18:34 +03:30

420 lines
14 KiB
JavaScript

(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 += '<span class="pager__gap" aria-hidden="true">&hellip;</span>';
return;
}
html += '<button class="pager__num' + (n === page ? ' is-current' : '') +
'" type="button" data-page="' + n + '"' +
(n === page ? ' aria-current="page"' : '') +
' aria-label="Page ' + n + '">' + n + '</button>';
});
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 <b>' + total + '</b> ' + noun;
return 'Showing <b>' + (start + 1) + '&ndash;' + (start + shownCount) +
'</b> of <b>' + total + '</b> ' + 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();
}
})();