Add frontend features with search, shortcodes, widgets, and blocks (v0.6.0)
All checks were successful
Create Release Package / build-release (push) Successful in 1m20s

- Room search with availability, capacity, room type, amenity, price range, and building filters
- AJAX-powered search with pagination and load more
- Shortcodes: [bnb_buildings], [bnb_rooms], [bnb_room_search], [bnb_building], [bnb_room]
- Widgets: Similar Rooms, Building Rooms, Availability Calendar
- Gutenberg blocks: Building, Room, Room Search, Buildings List, Rooms List
- Frontend CSS with responsive design and CSS custom properties
- Frontend JavaScript with SearchForm, CalendarWidget, AvailabilityForm, PriceCalculator

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-02 14:08:11 +01:00
parent 05f24fdec7
commit 864b8b2869
14 changed files with 5573 additions and 19 deletions

View File

@@ -0,0 +1,86 @@
/**
* WP BnB Block Editor Styles
*
* @package Magdev\WpBnb
*/
/* Block placeholder styling */
.wp-bnb-block-placeholder {
padding: 20px;
background: #f0f0f0;
border: 2px dashed #ccc;
text-align: center;
color: #666;
border-radius: 4px;
}
/* Server-side render container */
.wp-block-wp-bnb-building,
.wp-block-wp-bnb-room,
.wp-block-wp-bnb-room-search,
.wp-block-wp-bnb-buildings,
.wp-block-wp-bnb-rooms {
margin-bottom: 1em;
}
/* Placeholder in editor */
.wp-block-wp-bnb-building .components-placeholder,
.wp-block-wp-bnb-room .components-placeholder,
.wp-block-wp-bnb-room-search .components-placeholder,
.wp-block-wp-bnb-buildings .components-placeholder,
.wp-block-wp-bnb-rooms .components-placeholder {
min-height: 150px;
}
/* Loading spinner container */
.wp-block-wp-bnb-building .components-spinner,
.wp-block-wp-bnb-room .components-spinner,
.wp-block-wp-bnb-room-search .components-spinner,
.wp-block-wp-bnb-buildings .components-spinner,
.wp-block-wp-bnb-rooms .components-spinner {
margin: 0 auto;
}
/* Inspector control sections */
.wp-block-wp-bnb-building .components-panel__body,
.wp-block-wp-bnb-room .components-panel__body,
.wp-block-wp-bnb-room-search .components-panel__body,
.wp-block-wp-bnb-buildings .components-panel__body,
.wp-block-wp-bnb-rooms .components-panel__body {
padding-bottom: 16px;
}
/* Select control styling */
.wp-block-wp-bnb-building .components-select-control__input,
.wp-block-wp-bnb-room .components-select-control__input,
.wp-block-wp-bnb-room-search .components-select-control__input,
.wp-block-wp-bnb-buildings .components-select-control__input,
.wp-block-wp-bnb-rooms .components-select-control__input {
min-width: 200px;
}
/* Preview container in editor */
.wp-bnb-editor-preview {
pointer-events: none;
opacity: 0.9;
}
/* Disable interactive elements in preview */
.wp-bnb-editor-preview a,
.wp-bnb-editor-preview button,
.wp-bnb-editor-preview input,
.wp-bnb-editor-preview select {
pointer-events: none;
}
/* Add visual indicator that this is a preview */
.wp-bnb-editor-preview::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.1);
pointer-events: none;
}

File diff suppressed because it is too large Load Diff

489
assets/js/blocks-editor.js Normal file
View File

@@ -0,0 +1,489 @@
/**
* WP BnB Gutenberg Blocks
*
* @package Magdev\WpBnb
*/
(function(wp) {
'use strict';
const { registerBlockType } = wp.blocks;
const { createElement, Fragment } = wp.element;
const { InspectorControls, useBlockProps } = wp.blockEditor;
const { PanelBody, SelectControl, ToggleControl, RangeControl, Placeholder, Spinner } = wp.components;
const { ServerSideRender } = wp.editor || wp.serverSideRender;
const { __ } = wp.i18n;
const el = createElement;
// Get localized data
const { buildings, rooms, roomTypes, i18n } = wpBnbBlocks;
// Building options for select
const buildingOptions = [
{ value: 0, label: i18n.selectBuilding },
...buildings
];
// Room options for select
const roomOptions = [
{ value: 0, label: i18n.selectRoom },
...rooms.map(r => ({
value: r.value,
label: r.building ? `${r.label} (${r.building})` : r.label
}))
];
// Room type options
const roomTypeOptions = [
{ value: '', label: i18n.allTypes },
...roomTypes.map(t => ({
value: t.slug,
label: t.name
}))
];
// Building filter options for rooms block
const buildingFilterOptions = [
{ value: 0, label: i18n.allBuildings },
...buildings
];
/**
* Building Block
*/
registerBlockType('wp-bnb/building', {
title: i18n.buildingBlock,
icon: 'building',
category: 'widgets',
attributes: {
buildingId: { type: 'number', default: 0 },
showImage: { type: 'boolean', default: true },
showAddress: { type: 'boolean', default: true },
showRooms: { type: 'boolean', default: true },
showContact: { type: 'boolean', default: true }
},
edit: function(props) {
const { attributes, setAttributes } = props;
const blockProps = useBlockProps();
return el(Fragment, {},
el(InspectorControls, {},
el(PanelBody, { title: i18n.displaySettings },
el(SelectControl, {
label: i18n.buildingBlock,
value: attributes.buildingId,
options: buildingOptions,
onChange: (value) => setAttributes({ buildingId: parseInt(value, 10) })
}),
el(ToggleControl, {
label: i18n.showImage,
checked: attributes.showImage,
onChange: (value) => setAttributes({ showImage: value })
}),
el(ToggleControl, {
label: i18n.showAddress,
checked: attributes.showAddress,
onChange: (value) => setAttributes({ showAddress: value })
}),
el(ToggleControl, {
label: i18n.showRooms,
checked: attributes.showRooms,
onChange: (value) => setAttributes({ showRooms: value })
}),
el(ToggleControl, {
label: i18n.showContact,
checked: attributes.showContact,
onChange: (value) => setAttributes({ showContact: value })
})
)
),
el('div', blockProps,
attributes.buildingId ?
el(ServerSideRender, {
block: 'wp-bnb/building',
attributes: attributes,
LoadingResponsePlaceholder: () => el(Placeholder, { icon: 'building', label: i18n.buildingBlock }, el(Spinner))
}) :
el(Placeholder, { icon: 'building', label: i18n.buildingBlock },
buildings.length === 0 ?
el('p', {}, i18n.noBuildings) :
el(SelectControl, {
value: attributes.buildingId,
options: buildingOptions,
onChange: (value) => setAttributes({ buildingId: parseInt(value, 10) })
})
)
)
);
},
save: function() {
return null; // Server-side rendered
}
});
/**
* Room Block
*/
registerBlockType('wp-bnb/room', {
title: i18n.roomBlock,
icon: 'admin-home',
category: 'widgets',
attributes: {
roomId: { type: 'number', default: 0 },
showImage: { type: 'boolean', default: true },
showGallery: { type: 'boolean', default: true },
showPrice: { type: 'boolean', default: true },
showAmenities: { type: 'boolean', default: true },
showAvailability: { type: 'boolean', default: true }
},
edit: function(props) {
const { attributes, setAttributes } = props;
const blockProps = useBlockProps();
return el(Fragment, {},
el(InspectorControls, {},
el(PanelBody, { title: i18n.displaySettings },
el(SelectControl, {
label: i18n.roomBlock,
value: attributes.roomId,
options: roomOptions,
onChange: (value) => setAttributes({ roomId: parseInt(value, 10) })
}),
el(ToggleControl, {
label: i18n.showImage,
checked: attributes.showImage,
onChange: (value) => setAttributes({ showImage: value })
}),
el(ToggleControl, {
label: i18n.showGallery,
checked: attributes.showGallery,
onChange: (value) => setAttributes({ showGallery: value })
}),
el(ToggleControl, {
label: i18n.showPrice,
checked: attributes.showPrice,
onChange: (value) => setAttributes({ showPrice: value })
}),
el(ToggleControl, {
label: i18n.showAmenities,
checked: attributes.showAmenities,
onChange: (value) => setAttributes({ showAmenities: value })
}),
el(ToggleControl, {
label: i18n.showAvailability,
checked: attributes.showAvailability,
onChange: (value) => setAttributes({ showAvailability: value })
})
)
),
el('div', blockProps,
attributes.roomId ?
el(ServerSideRender, {
block: 'wp-bnb/room',
attributes: attributes,
LoadingResponsePlaceholder: () => el(Placeholder, { icon: 'admin-home', label: i18n.roomBlock }, el(Spinner))
}) :
el(Placeholder, { icon: 'admin-home', label: i18n.roomBlock },
rooms.length === 0 ?
el('p', {}, i18n.noRooms) :
el(SelectControl, {
value: attributes.roomId,
options: roomOptions,
onChange: (value) => setAttributes({ roomId: parseInt(value, 10) })
})
)
)
);
},
save: function() {
return null;
}
});
/**
* Room Search Block
*/
registerBlockType('wp-bnb/room-search', {
title: i18n.roomSearchBlock,
icon: 'search',
category: 'widgets',
attributes: {
layout: { type: 'string', default: 'grid' },
columns: { type: 'number', default: 3 },
showDates: { type: 'boolean', default: true },
showGuests: { type: 'boolean', default: true },
showRoomType: { type: 'boolean', default: true },
showAmenities: { type: 'boolean', default: true },
showPriceRange: { type: 'boolean', default: true },
showBuilding: { type: 'boolean', default: true },
resultsPerPage: { type: 'number', default: 12 }
},
edit: function(props) {
const { attributes, setAttributes } = props;
const blockProps = useBlockProps();
return el(Fragment, {},
el(InspectorControls, {},
el(PanelBody, { title: i18n.displaySettings },
el(SelectControl, {
label: i18n.layout,
value: attributes.layout,
options: [
{ value: 'grid', label: i18n.grid },
{ value: 'list', label: i18n.list }
],
onChange: (value) => setAttributes({ layout: value })
}),
el(RangeControl, {
label: i18n.columns,
value: attributes.columns,
onChange: (value) => setAttributes({ columns: value }),
min: 1,
max: 4
}),
el(RangeControl, {
label: i18n.resultsPerPage,
value: attributes.resultsPerPage,
onChange: (value) => setAttributes({ resultsPerPage: value }),
min: 4,
max: 48
})
),
el(PanelBody, { title: i18n.filterSettings, initialOpen: false },
el(ToggleControl, {
label: i18n.showDates,
checked: attributes.showDates,
onChange: (value) => setAttributes({ showDates: value })
}),
el(ToggleControl, {
label: i18n.showGuests,
checked: attributes.showGuests,
onChange: (value) => setAttributes({ showGuests: value })
}),
el(ToggleControl, {
label: i18n.showRoomType,
checked: attributes.showRoomType,
onChange: (value) => setAttributes({ showRoomType: value })
}),
el(ToggleControl, {
label: i18n.showAmenities,
checked: attributes.showAmenities,
onChange: (value) => setAttributes({ showAmenities: value })
}),
el(ToggleControl, {
label: i18n.showPriceRange,
checked: attributes.showPriceRange,
onChange: (value) => setAttributes({ showPriceRange: value })
}),
el(ToggleControl, {
label: i18n.showBuilding,
checked: attributes.showBuilding,
onChange: (value) => setAttributes({ showBuilding: value })
})
)
),
el('div', blockProps,
el(ServerSideRender, {
block: 'wp-bnb/room-search',
attributes: attributes,
LoadingResponsePlaceholder: () => el(Placeholder, { icon: 'search', label: i18n.roomSearchBlock }, el(Spinner))
})
)
);
},
save: function() {
return null;
}
});
/**
* Buildings List Block
*/
registerBlockType('wp-bnb/buildings', {
title: i18n.buildingsBlock,
icon: 'building',
category: 'widgets',
attributes: {
layout: { type: 'string', default: 'grid' },
columns: { type: 'number', default: 3 },
limit: { type: 'number', default: -1 },
showImage: { type: 'boolean', default: true },
showAddress: { type: 'boolean', default: true },
showRoomsCount: { type: 'boolean', default: true }
},
edit: function(props) {
const { attributes, setAttributes } = props;
const blockProps = useBlockProps();
return el(Fragment, {},
el(InspectorControls, {},
el(PanelBody, { title: i18n.displaySettings },
el(SelectControl, {
label: i18n.layout,
value: attributes.layout,
options: [
{ value: 'grid', label: i18n.grid },
{ value: 'list', label: i18n.list }
],
onChange: (value) => setAttributes({ layout: value })
}),
el(RangeControl, {
label: i18n.columns,
value: attributes.columns,
onChange: (value) => setAttributes({ columns: value }),
min: 1,
max: 4
}),
el(RangeControl, {
label: i18n.limit,
value: attributes.limit,
onChange: (value) => setAttributes({ limit: value }),
min: -1,
max: 20
}),
el(ToggleControl, {
label: i18n.showImage,
checked: attributes.showImage,
onChange: (value) => setAttributes({ showImage: value })
}),
el(ToggleControl, {
label: i18n.showAddress,
checked: attributes.showAddress,
onChange: (value) => setAttributes({ showAddress: value })
}),
el(ToggleControl, {
label: i18n.showRoomsCount,
checked: attributes.showRoomsCount,
onChange: (value) => setAttributes({ showRoomsCount: value })
})
)
),
el('div', blockProps,
el(ServerSideRender, {
block: 'wp-bnb/buildings',
attributes: attributes,
LoadingResponsePlaceholder: () => el(Placeholder, { icon: 'building', label: i18n.buildingsBlock }, el(Spinner))
})
)
);
},
save: function() {
return null;
}
});
/**
* Rooms List Block
*/
registerBlockType('wp-bnb/rooms', {
title: i18n.roomsBlock,
icon: 'admin-home',
category: 'widgets',
attributes: {
layout: { type: 'string', default: 'grid' },
columns: { type: 'number', default: 3 },
limit: { type: 'number', default: 12 },
buildingId: { type: 'number', default: 0 },
roomType: { type: 'string', default: '' },
showImage: { type: 'boolean', default: true },
showPrice: { type: 'boolean', default: true },
showCapacity: { type: 'boolean', default: true },
showAmenities: { type: 'boolean', default: true },
showBuilding: { type: 'boolean', default: true }
},
edit: function(props) {
const { attributes, setAttributes } = props;
const blockProps = useBlockProps();
return el(Fragment, {},
el(InspectorControls, {},
el(PanelBody, { title: i18n.displaySettings },
el(SelectControl, {
label: i18n.layout,
value: attributes.layout,
options: [
{ value: 'grid', label: i18n.grid },
{ value: 'list', label: i18n.list }
],
onChange: (value) => setAttributes({ layout: value })
}),
el(RangeControl, {
label: i18n.columns,
value: attributes.columns,
onChange: (value) => setAttributes({ columns: value }),
min: 1,
max: 4
}),
el(RangeControl, {
label: i18n.limit,
value: attributes.limit,
onChange: (value) => setAttributes({ limit: value }),
min: 1,
max: 48
}),
el(ToggleControl, {
label: i18n.showImage,
checked: attributes.showImage,
onChange: (value) => setAttributes({ showImage: value })
}),
el(ToggleControl, {
label: i18n.showPrice,
checked: attributes.showPrice,
onChange: (value) => setAttributes({ showPrice: value })
}),
el(ToggleControl, {
label: i18n.showCapacity,
checked: attributes.showCapacity,
onChange: (value) => setAttributes({ showCapacity: value })
}),
el(ToggleControl, {
label: i18n.showAmenities,
checked: attributes.showAmenities,
onChange: (value) => setAttributes({ showAmenities: value })
}),
el(ToggleControl, {
label: i18n.showBuilding,
checked: attributes.showBuilding,
onChange: (value) => setAttributes({ showBuilding: value })
})
),
el(PanelBody, { title: i18n.filterSettings, initialOpen: false },
el(SelectControl, {
label: i18n.buildingBlock,
value: attributes.buildingId,
options: buildingFilterOptions,
onChange: (value) => setAttributes({ buildingId: parseInt(value, 10) })
}),
el(SelectControl, {
label: i18n.roomType,
value: attributes.roomType,
options: roomTypeOptions,
onChange: (value) => setAttributes({ roomType: value })
})
)
),
el('div', blockProps,
el(ServerSideRender, {
block: 'wp-bnb/rooms',
attributes: attributes,
LoadingResponsePlaceholder: () => el(Placeholder, { icon: 'admin-home', label: i18n.roomsBlock }, el(Spinner))
})
)
);
},
save: function() {
return null;
}
});
})(window.wp);

View File

@@ -1,12 +1,825 @@
/**
* WP BnB Frontend JavaScript
*
* Handles search forms, calendar widgets, and interactive elements.
*
* @package Magdev\WpBnb
*/
(function() {
'use strict';
// Placeholder - Frontend scripts will be added as features are implemented
/**
* 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;
})();