/** * WP BnB Frontend JavaScript * * Handles search forms, calendar widgets, and interactive elements. * * @package Magdev\WpBnb */ (function() { 'use strict'; /** * WP BnB Frontend namespace. */ const WpBnb = { /** * Configuration from localized script. */ config: window.wpBnbFrontend || {}, /** * Initialize all frontend components. */ init: function() { this.initSearchForms(); this.initCalendarWidgets(); this.initAvailabilityForms(); this.initPriceCalculators(); }, /** * Initialize room search forms. */ initSearchForms: function() { const forms = document.querySelectorAll('.wp-bnb-search-form'); forms.forEach(form => { new SearchForm(form); }); }, /** * Initialize calendar widgets. */ initCalendarWidgets: function() { const calendars = document.querySelectorAll('.wp-bnb-availability-calendar-widget'); calendars.forEach(calendar => { new CalendarWidget(calendar); }); }, /** * Initialize availability check forms on single room pages. */ initAvailabilityForms: function() { const forms = document.querySelectorAll('.wp-bnb-availability-check'); forms.forEach(form => { new AvailabilityForm(form); }); }, /** * Initialize price calculator forms. */ initPriceCalculators: function() { const calculators = document.querySelectorAll('.wp-bnb-price-calculator'); calculators.forEach(calculator => { new PriceCalculator(calculator); }); }, /** * Make an AJAX request. * * @param {string} action The AJAX action. * @param {Object} data The request data. * @return {Promise} Promise resolving to response data. */ ajax: function(action, data = {}) { const formData = new FormData(); formData.append('action', action); formData.append('nonce', this.config.nonce || ''); Object.keys(data).forEach(key => { if (data[key] !== null && data[key] !== undefined) { formData.append(key, data[key]); } }); return fetch(this.config.ajaxUrl || '/wp-admin/admin-ajax.php', { method: 'POST', body: formData, credentials: 'same-origin' }) .then(response => { if (!response.ok) { throw new Error('Network response was not ok'); } return response.json(); }) .then(data => { if (!data.success) { throw new Error(data.data?.message || 'Request failed'); } return data.data; }); }, /** * Format a date as YYYY-MM-DD. * * @param {Date} date The date object. * @return {string} Formatted date string. */ formatDate: function(date) { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); return `${year}-${month}-${day}`; }, /** * Parse a date string. * * @param {string} dateStr Date string in YYYY-MM-DD format. * @return {Date|null} Date object or null if invalid. */ parseDate: function(dateStr) { if (!dateStr) return null; const parts = dateStr.split('-'); if (parts.length !== 3) return null; return new Date(parseInt(parts[0]), parseInt(parts[1]) - 1, parseInt(parts[2])); }, /** * Calculate nights between two dates. * * @param {Date} checkIn Check-in date. * @param {Date} checkOut Check-out date. * @return {number} Number of nights. */ calculateNights: function(checkIn, checkOut) { if (!checkIn || !checkOut) return 0; const diffTime = checkOut.getTime() - checkIn.getTime(); return Math.ceil(diffTime / (1000 * 60 * 60 * 24)); }, /** * Debounce a function. * * @param {Function} func The function to debounce. * @param {number} wait Wait time in milliseconds. * @return {Function} Debounced function. */ debounce: function(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; } }; /** * Search Form handler class. */ class SearchForm { constructor(element) { this.form = element; this.resultsContainer = document.querySelector( this.form.dataset.results || '.wp-bnb-search-results' ); this.currentPage = 1; this.isLoading = false; this.bindEvents(); } bindEvents() { // Form submission. this.form.addEventListener('submit', (e) => { e.preventDefault(); this.currentPage = 1; this.search(); }); // Date validation. const checkIn = this.form.querySelector('[name="check_in"]'); const checkOut = this.form.querySelector('[name="check_out"]'); if (checkIn && checkOut) { // Set min date to today. const today = WpBnb.formatDate(new Date()); checkIn.setAttribute('min', today); checkIn.addEventListener('change', () => { if (checkIn.value) { // Set check-out min to day after check-in. const minCheckOut = WpBnb.parseDate(checkIn.value); if (minCheckOut) { minCheckOut.setDate(minCheckOut.getDate() + 1); checkOut.setAttribute('min', WpBnb.formatDate(minCheckOut)); // Clear check-out if it's before new minimum. if (checkOut.value && checkOut.value <= checkIn.value) { checkOut.value = ''; } } } }); checkOut.addEventListener('change', () => { if (checkOut.value && checkIn.value && checkOut.value <= checkIn.value) { alert(WpBnb.config.i18n?.invalidDateRange || 'Check-out must be after check-in'); checkOut.value = ''; } }); } // Reset button. const resetBtn = this.form.querySelector('[type="reset"]'); if (resetBtn) { resetBtn.addEventListener('click', () => { setTimeout(() => { this.clearResults(); }, 0); }); } // Load more button. if (this.resultsContainer) { this.resultsContainer.addEventListener('click', (e) => { if (e.target.classList.contains('wp-bnb-load-more')) { e.preventDefault(); this.loadMore(); } }); } } getFormData() { const formData = new FormData(this.form); const data = {}; formData.forEach((value, key) => { if (value) { // Handle array fields (amenities[]). if (key.endsWith('[]')) { const cleanKey = key.slice(0, -2); if (!data[cleanKey]) { data[cleanKey] = []; } data[cleanKey].push(value); } else { data[key] = value; } } }); // Convert arrays to comma-separated strings for AJAX. Object.keys(data).forEach(key => { if (Array.isArray(data[key])) { data[key] = data[key].join(','); } }); return data; } search() { if (this.isLoading) return; this.isLoading = true; this.showLoading(); const data = this.getFormData(); data.page = this.currentPage; data.per_page = this.form.dataset.perPage || 12; WpBnb.ajax('wp_bnb_search_rooms', data) .then(response => { this.renderResults(response, this.currentPage === 1); }) .catch(error => { this.showError(error.message); }) .finally(() => { this.isLoading = false; this.hideLoading(); }); } loadMore() { this.currentPage++; this.search(); } renderResults(response, replace = true) { if (!this.resultsContainer) return; const { rooms, total, page, total_pages } = response; if (replace) { this.resultsContainer.innerHTML = ''; } else { // Remove existing load more button. const existingLoadMore = this.resultsContainer.querySelector('.wp-bnb-load-more-wrapper'); if (existingLoadMore) { existingLoadMore.remove(); } } if (rooms.length === 0 && replace) { this.resultsContainer.innerHTML = `

${WpBnb.config.i18n?.noResults || 'No rooms found matching your criteria.'}

`; return; } // Create results count. if (replace) { const countEl = document.createElement('div'); countEl.className = 'wp-bnb-results-count'; countEl.innerHTML = `

${WpBnb.config.i18n?.resultsFound?.replace('%d', total) || `${total} rooms found`}

`; this.resultsContainer.appendChild(countEl); } // Create grid container. let grid = this.resultsContainer.querySelector('.wp-bnb-rooms-grid'); if (!grid) { grid = document.createElement('div'); grid.className = 'wp-bnb-rooms-grid wp-bnb-grid wp-bnb-grid-3'; this.resultsContainer.appendChild(grid); } // Render room cards. rooms.forEach(room => { const card = this.createRoomCard(room); grid.appendChild(card); }); // Add load more button if there are more pages. if (page < total_pages) { const loadMoreWrapper = document.createElement('div'); loadMoreWrapper.className = 'wp-bnb-load-more-wrapper'; loadMoreWrapper.innerHTML = ` `; this.resultsContainer.appendChild(loadMoreWrapper); } } createRoomCard(room) { const card = document.createElement('article'); card.className = 'wp-bnb-room-card'; let imageHtml = ''; if (room.thumbnail) { imageHtml = `
${this.escapeHtml(room.title)}
`; } let amenitiesHtml = ''; if (room.amenities && room.amenities.length > 0) { const amenityItems = room.amenities.slice(0, 4).map(a => `${this.escapeHtml(a.name)}` ).join(''); amenitiesHtml = `
${amenityItems}
`; } let priceHtml = ''; if (room.price_display) { priceHtml = `
${this.escapeHtml(room.price_display)} / ${WpBnb.config.i18n?.perNight || 'night'}
`; } card.innerHTML = ` ${imageHtml}

${this.escapeHtml(room.title)}

${room.building_name ? `

${this.escapeHtml(room.building_name)}

` : ''}
${room.capacity ? `${room.capacity} ${WpBnb.config.i18n?.guests || 'guests'}` : ''} ${room.room_type ? `${this.escapeHtml(room.room_type)}` : ''}
${amenitiesHtml} ${priceHtml} ${WpBnb.config.i18n?.viewDetails || 'View Details'}
`; return card; } escapeHtml(str) { if (!str) return ''; const div = document.createElement('div'); div.textContent = str; return div.innerHTML; } showLoading() { this.form.classList.add('wp-bnb-loading'); const submitBtn = this.form.querySelector('[type="submit"]'); if (submitBtn) { submitBtn.disabled = true; submitBtn.dataset.originalText = submitBtn.textContent; submitBtn.textContent = WpBnb.config.i18n?.searching || 'Searching...'; } } hideLoading() { this.form.classList.remove('wp-bnb-loading'); const submitBtn = this.form.querySelector('[type="submit"]'); if (submitBtn) { submitBtn.disabled = false; if (submitBtn.dataset.originalText) { submitBtn.textContent = submitBtn.dataset.originalText; } } } showError(message) { if (!this.resultsContainer) return; this.resultsContainer.innerHTML = `

${this.escapeHtml(message)}

`; } clearResults() { if (this.resultsContainer) { this.resultsContainer.innerHTML = ''; } } } /** * Calendar Widget handler class. */ class CalendarWidget { constructor(element) { this.container = element; this.roomId = element.dataset.roomId; this.currentYear = parseInt(element.querySelector('[data-year]')?.dataset.year) || new Date().getFullYear(); this.currentMonth = parseInt(element.querySelector('[data-month]')?.dataset.month) || (new Date().getMonth() + 1); this.bindEvents(); } bindEvents() { // Navigation buttons. this.container.addEventListener('click', (e) => { const navBtn = e.target.closest('.wp-bnb-calendar-nav'); if (navBtn) { e.preventDefault(); const direction = navBtn.dataset.direction; if (direction === 'prev') { this.navigatePrev(); } else if (direction === 'next') { this.navigateNext(); } } }); } navigatePrev() { this.currentMonth--; if (this.currentMonth < 1) { this.currentMonth = 12; this.currentYear--; } this.loadCalendar(); } navigateNext() { this.currentMonth++; if (this.currentMonth > 12) { this.currentMonth = 1; this.currentYear++; } this.loadCalendar(); } loadCalendar() { this.container.classList.add('wp-bnb-loading'); WpBnb.ajax('wp_bnb_get_calendar', { room_id: this.roomId, year: this.currentYear, month: this.currentMonth }) .then(response => { this.renderCalendar(response); }) .catch(error => { console.error('Calendar load error:', error); }) .finally(() => { this.container.classList.remove('wp-bnb-loading'); }); } renderCalendar(data) { const monthContainer = this.container.querySelector('.wp-bnb-calendar-month'); if (!monthContainer) return; // Update month/year attributes. monthContainer.dataset.year = this.currentYear; monthContainer.dataset.month = this.currentMonth; // Update month name. const monthNameEl = monthContainer.querySelector('.wp-bnb-calendar-month-name'); if (monthNameEl) { monthNameEl.textContent = `${data.month_name} ${this.currentYear}`; } // Rebuild calendar grid. const tbody = monthContainer.querySelector('.wp-bnb-calendar-grid tbody'); if (!tbody) return; tbody.innerHTML = ''; let day = 1; const totalDays = data.days_in_month; const firstDay = data.first_day_of_week; const weeks = Math.ceil((firstDay + totalDays) / 7); for (let week = 0; week < weeks; week++) { const tr = document.createElement('tr'); for (let dow = 0; dow < 7; dow++) { const td = document.createElement('td'); const cellIndex = week * 7 + dow; if (cellIndex < firstDay || day > totalDays) { td.className = 'wp-bnb-calendar-empty'; } else { const dayData = data.days[day]; const classes = ['wp-bnb-calendar-day']; if (dayData) { if (dayData.is_booked) { classes.push('wp-bnb-booked'); } else { classes.push('wp-bnb-available'); } if (dayData.is_past) { classes.push('wp-bnb-past'); } if (dayData.is_today) { classes.push('wp-bnb-today'); } td.dataset.date = dayData.date || ''; } td.className = classes.join(' '); td.textContent = day; day++; } tr.appendChild(td); } tbody.appendChild(tr); } } } /** * Availability Form handler class. * For checking availability on single room pages. */ class AvailabilityForm { constructor(element) { this.form = element; this.roomId = element.dataset.roomId; this.resultContainer = element.querySelector('.wp-bnb-availability-result'); this.bindEvents(); } bindEvents() { this.form.addEventListener('submit', (e) => { e.preventDefault(); this.checkAvailability(); }); // Date validation. const checkIn = this.form.querySelector('[name="check_in"]'); const checkOut = this.form.querySelector('[name="check_out"]'); if (checkIn && checkOut) { const today = WpBnb.formatDate(new Date()); checkIn.setAttribute('min', today); checkIn.addEventListener('change', () => { if (checkIn.value) { const minCheckOut = WpBnb.parseDate(checkIn.value); if (minCheckOut) { minCheckOut.setDate(minCheckOut.getDate() + 1); checkOut.setAttribute('min', WpBnb.formatDate(minCheckOut)); } } this.clearResult(); }); checkOut.addEventListener('change', () => { this.clearResult(); }); } } checkAvailability() { const checkIn = this.form.querySelector('[name="check_in"]')?.value; const checkOut = this.form.querySelector('[name="check_out"]')?.value; if (!checkIn || !checkOut) { this.showResult('error', WpBnb.config.i18n?.selectDates || 'Please select check-in and check-out dates.'); return; } if (checkOut <= checkIn) { this.showResult('error', WpBnb.config.i18n?.invalidDateRange || 'Check-out must be after check-in.'); return; } this.form.classList.add('wp-bnb-loading'); WpBnb.ajax('wp_bnb_get_availability', { room_id: this.roomId, check_in: checkIn, check_out: checkOut }) .then(response => { if (response.available) { let message = WpBnb.config.i18n?.available || 'Room is available!'; if (response.price_display) { message += ` ${WpBnb.config.i18n?.totalPrice || 'Total'}: ${response.price_display}`; } this.showResult('success', message, response); } else { this.showResult('error', WpBnb.config.i18n?.notAvailable || 'Sorry, the room is not available for these dates.'); } }) .catch(error => { this.showResult('error', error.message); }) .finally(() => { this.form.classList.remove('wp-bnb-loading'); }); } showResult(type, message, data = null) { if (!this.resultContainer) return; let html = `
${this.escapeHtml(message)}
`; if (type === 'success' && data && data.booking_url) { html += ` ${WpBnb.config.i18n?.bookNow || 'Book Now'} `; } this.resultContainer.innerHTML = html; this.resultContainer.style.display = 'block'; } clearResult() { if (this.resultContainer) { this.resultContainer.innerHTML = ''; this.resultContainer.style.display = 'none'; } } escapeHtml(str) { if (!str) return ''; const div = document.createElement('div'); div.textContent = str; return div.innerHTML; } } /** * Price Calculator handler class. */ class PriceCalculator { constructor(element) { this.container = element; this.roomId = element.dataset.roomId; this.priceDisplay = element.querySelector('.wp-bnb-calculated-price'); this.breakdownDisplay = element.querySelector('.wp-bnb-price-breakdown'); this.bindEvents(); } bindEvents() { const checkIn = this.container.querySelector('[name="check_in"]'); const checkOut = this.container.querySelector('[name="check_out"]'); if (checkIn && checkOut) { const debouncedCalculate = WpBnb.debounce(() => this.calculate(), 300); checkIn.addEventListener('change', debouncedCalculate); checkOut.addEventListener('change', debouncedCalculate); } } calculate() { const checkIn = this.container.querySelector('[name="check_in"]')?.value; const checkOut = this.container.querySelector('[name="check_out"]')?.value; if (!checkIn || !checkOut || checkOut <= checkIn) { this.clearDisplay(); return; } this.container.classList.add('wp-bnb-loading'); WpBnb.ajax('wp_bnb_calculate_price', { room_id: this.roomId, check_in: checkIn, check_out: checkOut }) .then(response => { this.displayPrice(response); }) .catch(error => { console.error('Price calculation error:', error); this.clearDisplay(); }) .finally(() => { this.container.classList.remove('wp-bnb-loading'); }); } displayPrice(data) { if (this.priceDisplay) { this.priceDisplay.innerHTML = ` ${WpBnb.config.i18n?.total || 'Total'}: ${this.escapeHtml(data.formatted_total)} `; this.priceDisplay.style.display = 'block'; } if (this.breakdownDisplay && data.breakdown) { let breakdownHtml = ''; this.breakdownDisplay.innerHTML = breakdownHtml; this.breakdownDisplay.style.display = 'block'; } } clearDisplay() { if (this.priceDisplay) { this.priceDisplay.innerHTML = ''; this.priceDisplay.style.display = 'none'; } if (this.breakdownDisplay) { this.breakdownDisplay.innerHTML = ''; this.breakdownDisplay.style.display = 'none'; } } escapeHtml(str) { if (!str) return ''; const div = document.createElement('div'); div.textContent = str; return div.innerHTML; } } // Initialize on DOM ready. if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => WpBnb.init()); } else { WpBnb.init(); } // Expose to global scope for potential external use. window.WpBnb = WpBnb; })();