'use strict'; (() => { const cards = [...document.querySelectorAll('[data-hotel]')]; const search = document.querySelector('#hotel-search'); const area = document.querySelector('#area-filter'); const sort = document.querySelector('#sort-hotels'); const chips = [...document.querySelectorAll('[data-category]')]; const grid = document.querySelector('.hotel-grid'); const status = document.querySelector('#result-count'); const empty = document.querySelector('#empty-state'); let category = 'all'; let saved = new Set(); try { const value=JSON.parse(localStorage.getItem('sinatra-saved') || '[]'); if(Array.isArray(value)) saved=new Set(value.filter(x=>typeof x==='string')); } catch {} let lastSort=null; function update() { const query = search.value.trim().toLowerCase(); const val=(el,k,d)=>{const v=parseFloat(el.dataset[k]);return isNaN(v)?d:v;}; // Reorder the DOM only when the sort actually changed — never on filter/save, so toggling // a card's shortlist heart mid-page doesn't re-append every card and jump the scroll. if(sort.value!==lastSort){ lastSort=sort.value; [...cards].sort((a,b)=>{ switch(sort.value){ case 'name': return a.dataset.name.localeCompare(b.dataset.name); case 'price-asc': return val(a,'price',1e9)-val(b,'price',1e9); case 'rating-desc': return val(b,'rating',-1)-val(a,'rating',-1); case 'reviews-desc': return val(b,'reviews',-1)-val(a,'reviews',-1); case 'distance-asc': return val(a,'distance',1e9)-val(b,'distance',1e9); default: return Number(a.dataset.order)-Number(b.dataset.order); } }).forEach(card=>grid.append(card)); } let count=0; cards.forEach(card=>{ card.hidden = !(card.dataset.name.toLowerCase().includes(query) && (!area||area.value==='all'||card.dataset.area===area.value) && (category==='all'||(category==='saved'?saved.has(card.dataset.id):card.dataset.categories.split(' ').includes(category)))); if(!card.hidden) count++; }); chips.forEach(button=>button.setAttribute('aria-pressed',String(button.dataset.category===category))); const nS=status.dataset.noun||'stay', nP=status.dataset.nounPlural||'stays'; status.textContent=`${count} ${count===1?nS:nP}`; empty.hidden=count!==0; } function choose(value){category=value;update();} chips.forEach(button=>button.addEventListener('click',()=>{choose(button.dataset.category); if(button.classList.contains('collection')) document.querySelector('#hotels').scrollIntoView({behavior:'smooth'});})); search.addEventListener('input',update); if(area)area.addEventListener('change',update); sort.addEventListener('change',update); document.querySelectorAll('[data-reset]').forEach(button=>button.addEventListener('click',()=>{search.value='';if(area)area.value='all';sort.value='featured';choose('all');})); document.querySelectorAll('[data-area-link]').forEach(button=>button.addEventListener('click',()=>{area.value=button.dataset.areaLink;search.value='';choose('all');document.querySelector('#hotels').scrollIntoView({behavior:'smooth'});})); let toastTimer; document.querySelectorAll('.save').forEach(button=>{ const id=button.closest('[data-hotel]').dataset.id; function paint(){const active=saved.has(id);button.setAttribute('aria-pressed',String(active));button.textContent=active?'♥':'♡';button.setAttribute('aria-label',`${active?'Unsave':'Save'} ${button.closest('[data-hotel]').dataset.name}`);} paint(); button.addEventListener('click',()=>{saved.has(id)?saved.delete(id):saved.add(id);let persistent=true;try{localStorage.setItem('sinatra-saved',JSON.stringify([...saved]));}catch{persistent=false;}paint();update();const toast=document.querySelector('#toast');toast.textContent=(saved.has(id)?'Added to your shortlist':'Removed from your shortlist')+(persistent?'':' · this session only');toast.hidden=false;clearTimeout(toastTimer);toastTimer=setTimeout(()=>toast.hidden=true,2600);}); }); update(); // --- "Check Availability" → Booking (affiliate) --------------------------------------- // The hotel NAME stays a plain internal (→ our property page); only the CTA button // goes to Booking. Cloaked: the Booking base URL is base64 in window.__CLU and the full URL is // built at click time (never in the crawlable HTML). Crawlers are skipped, so they just follow // the button's internal href fallback. Label carries this hotel's page URL, our structure. (() => { const enc = window.__CLU; // base64 of the Booking base URL, or falsy if (!enc) return; const ua = navigator.userAgent || ''; if (/bot|crawl|spider|slurp|bing|google|yandex|baidu|duckduck|facebookexternalhit|lighthouse|pagespeed|headless|preview/i.test(ua) || navigator.webdriver) return; const bookingURL = (propUrl) => { const base = atob(enc); // https://www.booking.com/searchresults.html?dest_type=city&dest_id=…&aid=… const day = 864e5, iso = t => new Date(t).toISOString().slice(0, 10), now = Date.now(); const label = location.hostname + '-hotel-place-n-' + propUrl; // our label structure, per hotel return base + '&checkin=' + iso(now + day) + '&checkout=' + iso(now + 2 * day) + '&group_adults=2&group_children=0&selected_currency=&label=' + label; }; document.querySelectorAll('.check-btn').forEach(b => { b.addEventListener('click', (e) => { e.preventDefault(); const prop = b.dataset.u || location.href; // this hotel's page URL (label tail) window.open(bookingURL(prop), '_blank'); // Booking opens; our page stays }); }); })(); })();