/** * Main app bootstrap — fetches property data, mounts components. */ // ── Toast utility (global) ────────────────────────── window.showToast = function(message, type = 'info', duration = 5000) { let container = document.getElementById('toast-container'); if (!container) { container = document.createElement('div'); container.id = 'toast-container'; container.className = 'toast-container'; document.body.appendChild(container); } const toast = document.createElement('div'); toast.className = `toast toast--${type}`; toast.textContent = message; container.appendChild(toast); setTimeout(() => { toast.style.opacity = '0'; toast.style.transition = 'opacity 0.3s'; setTimeout(() => toast.remove(), 300); }, duration); }; // ── Photo gallery ──────────────────────────────────── function initGallery(photos) { const hero = document.querySelector('.hero__gallery'); const dotsContainer = document.querySelector('.hero__dots'); if (!hero || !photos?.length) return; photos.forEach((photo, i) => { const slide = document.createElement('div'); slide.className = `hero__slide${i === 0 ? ' active' : ''}`; slide.style.backgroundImage = `url('${photo.url}')`; hero.appendChild(slide); const dot = document.createElement('button'); dot.className = `hero__dot${i === 0 ? ' active' : ''}`; dot.setAttribute('aria-label', photo.caption || `Photo ${i+1}`); dot.addEventListener('click', () => goToSlide(i)); dotsContainer?.appendChild(dot); }); let current = 0; function goToSlide(idx) { hero.querySelectorAll('.hero__slide')[current]?.classList.remove('active'); dotsContainer?.querySelectorAll('.hero__dot')[current]?.classList.remove('active'); current = idx; hero.querySelectorAll('.hero__slide')[current]?.classList.add('active'); dotsContainer?.querySelectorAll('.hero__dot')[current]?.classList.add('active'); } // Auto-advance every 5s setInterval(() => goToSlide((current + 1) % photos.length), 5000); } // ── Photo grid ─────────────────────────────────────── function initPhotoGrid(photos) { const grid = document.querySelector('.photo-grid'); if (!grid || !photos?.length) return; grid.innerHTML = ''; const visible = photos.slice(0, 5); visible.forEach((photo, i) => { const item = document.createElement('div'); item.className = 'photo-grid__item'; const img = document.createElement('img'); img.src = photo.url; img.alt = photo.caption || ''; img.loading = 'lazy'; img.decoding = 'async'; item.appendChild(img); if (i === 4 && photos.length > 5) { const more = document.createElement('div'); more.className = 'photo-grid__more'; more.textContent = `+${photos.length - 5} more`; item.appendChild(more); } grid.appendChild(item); }); } // ── Amenities ───────────────────────────────────────── const AMENITY_ICONS = { 'WiFi': '📶', 'Full kitchen': '🍳', 'Free parking': '🅿️', 'Hot tub': '♨️', 'Fire pit': '🔥', 'Washer/dryer': '👕', 'Air conditioning': '❄️', 'Heating': '🌡️', 'Smart TV': '📺', 'Outdoor dining area': '🌿', 'BBQ grill': '🍖', 'Game room': '🎮', 'Fireplace': '🪵', 'Pool': '🏊', }; function initAmenities(amenities) { const grid = document.querySelector('.amenities-grid'); if (!grid || !amenities?.length) return; grid.innerHTML = ''; amenities.forEach(name => { const el = document.createElement('div'); el.className = 'amenity'; el.innerHTML = `${AMENITY_ICONS[name] || '✓'}${name}`; grid.appendChild(el); }); } // ── Sticky nav ──────────────────────────────────────── function initNav() { const nav = document.querySelector('.nav'); if (!nav) return; window.addEventListener('scroll', () => { nav.classList.toggle('scrolled', window.scrollY > 80); }, { passive: true }); } // ── Smooth scroll CTA ───────────────────────────────── function initCTAs() { document.querySelectorAll('[data-scroll]').forEach(el => { el.addEventListener('click', () => { const target = document.querySelector(el.dataset.scroll); target?.scrollIntoView({ behavior: 'smooth' }); }); }); // Booking bar quick-search const checkBtn = document.getElementById('check-availability-btn'); if (checkBtn) { checkBtn.addEventListener('click', () => { const checkIn = document.getElementById('bar-checkin')?.value; const checkOut = document.getElementById('bar-checkout')?.value; if (checkIn && checkOut) { booking.onDatesSelected({ checkIn, checkOut }); } document.querySelector('#section-dates')?.scrollIntoView({ behavior: 'smooth' }); }); } // Step navigation document.getElementById('btn-to-details')?.addEventListener('click', () => booking.goToDetails()); document.getElementById('btn-to-payment')?.addEventListener('click', () => booking.goToPayment()); document.getElementById('btn-back-to-dates')?.addEventListener('click', () => { document.getElementById('section-dates').hidden = false; document.getElementById('section-details').hidden = true; document.getElementById('section-dates').scrollIntoView({ behavior: 'smooth' }); }); document.getElementById('btn-back-to-details')?.addEventListener('click', () => { document.getElementById('section-payment').hidden = true; document.getElementById('section-details').hidden = false; document.getElementById('section-details').scrollIntoView({ behavior: 'smooth' }); }); document.getElementById('pay-btn')?.addEventListener('click', () => booking.submitPayment()); } // ── Property data ───────────────────────────────────── async function loadProperty() { try { const data = await api.getProperty(); const h = data.hospitable; const p = data.property; // Apply branding if (p.branding) { const r = document.documentElement.style; if (p.branding.primaryColor) r.setProperty('--color-primary', p.branding.primaryColor); if (p.branding.accentColor) r.setProperty('--color-accent', p.branding.accentColor); } // Populate content document.querySelectorAll('.property-name').forEach(el => el.textContent = h.name || p.name); document.title = `${h.name || p.name} — Direct Booking`; const descEl = document.querySelector('.property-description'); if (descEl && h.description) descEl.textContent = h.description; // Stats const setMeta = (sel, val) => { const el = document.querySelector(sel); if(el) el.textContent = val; }; setMeta('.stat-bedrooms', h.bedrooms || '—'); setMeta('.stat-bathrooms', h.bathrooms || '—'); setMeta('.stat-guests', h.maxGuests || '—'); setMeta('.stat-type', h.propertyType || 'Entire home'); setMeta('#hero-checkin', h.checkInTime ? `Check-in: ${h.checkInTime}` : ''); setMeta('#hero-checkout', h.checkOutTime ? `Check-out: ${h.checkOutTime}` : ''); initGallery(h.photos || []); initPhotoGrid(h.photos || []); initAmenities(h.amenities || []); // Mount calendar const calEl = document.getElementById('availability-calendar'); if (calEl) { new AvailabilityCalendar(calEl, { minStay: h.minimumStay || 2, onSelect: ({ checkIn, checkOut }) => { booking.onDatesSelected({ checkIn, checkOut }); // Sync booking bar inputs const ci = document.getElementById('bar-checkin'); const co = document.getElementById('bar-checkout'); if (ci) ci.value = checkIn; if (co) co.value = checkOut; }, }); } } catch (err) { console.error('Failed to load property:', err); showToast('Could not load property information. Please refresh.', 'error'); } } // ── Boot ────────────────────────────────────────────── document.addEventListener('DOMContentLoaded', () => { initNav(); initCTAs(); loadProperty(); });