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
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:
86
assets/css/blocks-editor.css
Normal file
86
assets/css/blocks-editor.css
Normal 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
489
assets/js/blocks-editor.js
Normal 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);
|
||||
@@ -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;
|
||||
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user