tours and tour pages
This commit is contained in:
260
js/main.js
260
js/main.js
@@ -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">…</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) + '–' + (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();
|
||||
}
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user