tours and tour pages

This commit is contained in:
vargha1
2026-09-09 11:18:34 +03:30
parent 85dfe6b433
commit 99e5413fee
11 changed files with 6130 additions and 13 deletions

View File

@@ -64,7 +64,7 @@
}
/* ---------- sticky header past the hero ---------- */
var hero = document.querySelector('.hero');
var hero = document.querySelector('.hero, .hero-tour');
if (header && hero && 'IntersectionObserver' in window) {
var sentinel = document.createElement('div');
sentinel.setAttribute('aria-hidden', 'true');
@@ -158,4 +158,262 @@
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();
}
})();