Files
wp-bnb/assets/js/frontend.js

826 lines
22 KiB
JavaScript
Raw Normal View History

/**
* 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 = `
<div class="wp-bnb-no-results">
<p>${WpBnb.config.i18n?.noResults || 'No rooms found matching your criteria.'}</p>
</div>
`;
return;
}
// Create results count.
if (replace) {
const countEl = document.createElement('div');
countEl.className = 'wp-bnb-results-count';
countEl.innerHTML = `<p>${WpBnb.config.i18n?.resultsFound?.replace('%d', total) || `${total} rooms found`}</p>`;
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 = `
<button type="button" class="wp-bnb-load-more wp-bnb-button">
${WpBnb.config.i18n?.loadMore || 'Load More'}
</button>
`;
this.resultsContainer.appendChild(loadMoreWrapper);
}
}
createRoomCard(room) {
const card = document.createElement('article');
card.className = 'wp-bnb-room-card';
let imageHtml = '';
if (room.thumbnail) {
imageHtml = `
<div class="wp-bnb-room-card-image">
<a href="${this.escapeHtml(room.permalink)}">
<img src="${this.escapeHtml(room.thumbnail)}" alt="${this.escapeHtml(room.title)}">
</a>
</div>
`;
}
let amenitiesHtml = '';
if (room.amenities && room.amenities.length > 0) {
const amenityItems = room.amenities.slice(0, 4).map(a =>
`<span class="wp-bnb-amenity-tag">${this.escapeHtml(a.name)}</span>`
).join('');
amenitiesHtml = `<div class="wp-bnb-room-card-amenities">${amenityItems}</div>`;
}
let priceHtml = '';
if (room.price_display) {
priceHtml = `
<div class="wp-bnb-room-card-price">
<span class="wp-bnb-price">${this.escapeHtml(room.price_display)}</span>
<span class="wp-bnb-price-unit">/ ${WpBnb.config.i18n?.perNight || 'night'}</span>
</div>
`;
}
card.innerHTML = `
${imageHtml}
<div class="wp-bnb-room-card-content">
<h3 class="wp-bnb-room-card-title">
<a href="${this.escapeHtml(room.permalink)}">${this.escapeHtml(room.title)}</a>
</h3>
${room.building_name ? `<p class="wp-bnb-room-card-building">${this.escapeHtml(room.building_name)}</p>` : ''}
<div class="wp-bnb-room-card-meta">
${room.capacity ? `<span class="wp-bnb-capacity">${room.capacity} ${WpBnb.config.i18n?.guests || 'guests'}</span>` : ''}
${room.room_type ? `<span class="wp-bnb-room-type">${this.escapeHtml(room.room_type)}</span>` : ''}
</div>
${amenitiesHtml}
${priceHtml}
<a href="${this.escapeHtml(room.permalink)}" class="wp-bnb-room-card-link wp-bnb-button wp-bnb-button-small">
${WpBnb.config.i18n?.viewDetails || 'View Details'}
</a>
</div>
`;
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 = `
<div class="wp-bnb-error">
<p>${this.escapeHtml(message)}</p>
</div>
`;
}
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 = `<div class="wp-bnb-availability-${type}">${this.escapeHtml(message)}</div>`;
if (type === 'success' && data && data.booking_url) {
html += `
<a href="${this.escapeHtml(data.booking_url)}" class="wp-bnb-button wp-bnb-book-now">
${WpBnb.config.i18n?.bookNow || 'Book Now'}
</a>
`;
}
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 = `
<span class="wp-bnb-price-label">${WpBnb.config.i18n?.total || 'Total'}:</span>
<span class="wp-bnb-price-amount">${this.escapeHtml(data.formatted_total)}</span>
`;
this.priceDisplay.style.display = 'block';
}
if (this.breakdownDisplay && data.breakdown) {
let breakdownHtml = '<ul class="wp-bnb-breakdown-list">';
if (data.breakdown.nights) {
breakdownHtml += `<li>${data.breakdown.nights} ${WpBnb.config.i18n?.nights || 'nights'}</li>`;
}
if (data.breakdown.tier) {
breakdownHtml += `<li>${this.escapeHtml(data.breakdown.tier)}</li>`;
}
if (data.breakdown.base_total) {
breakdownHtml += `<li>${WpBnb.config.i18n?.basePrice || 'Base'}: ${this.escapeHtml(data.breakdown.base_total)}</li>`;
}
if (data.breakdown.weekend_total && parseFloat(data.breakdown.weekend_total) > 0) {
breakdownHtml += `<li>${WpBnb.config.i18n?.weekendSurcharge || 'Weekend surcharge'}: ${this.escapeHtml(data.breakdown.weekend_total)}</li>`;
}
if (data.breakdown.season_name) {
breakdownHtml += `<li>${WpBnb.config.i18n?.season || 'Season'}: ${this.escapeHtml(data.breakdown.season_name)}</li>`;
}
breakdownHtml += '</ul>';
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;
})();