Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 864b8b2869 |
69
CHANGELOG.md
69
CHANGELOG.md
@@ -5,6 +5,74 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [0.6.0] - 2026-02-02
|
||||
|
||||
### Added
|
||||
|
||||
- Frontend Features System:
|
||||
- Room search with multiple filters (availability, capacity, room type, amenities, price range, building)
|
||||
- AJAX-powered search with pagination and "Load More" functionality
|
||||
- Date validation (check-out after check-in, minimum today)
|
||||
- Real-time availability checking on single room pages
|
||||
- Price calculator with breakdown display
|
||||
- Shortcodes:
|
||||
- `[bnb_buildings]` - Display buildings list/grid with filtering and sorting
|
||||
- `[bnb_rooms]` - Display rooms list/grid with multiple filter options
|
||||
- `[bnb_room_search]` - Interactive room search form with results
|
||||
- `[bnb_building id="X"]` - Display single building details
|
||||
- `[bnb_room id="X"]` - Display single room details with availability form
|
||||
- WordPress Widgets:
|
||||
- Similar Rooms widget (shows rooms from same building/type)
|
||||
- Building Rooms widget (lists all rooms in a building)
|
||||
- Availability Calendar widget (mini calendar with booking status)
|
||||
- Gutenberg Blocks:
|
||||
- Building block with ID selector
|
||||
- Room block with ID selector
|
||||
- Room Search block with filter presets
|
||||
- Buildings List block with layout options
|
||||
- Rooms List block with filter options
|
||||
- Server-side rendered blocks for consistent output
|
||||
- Frontend Search Class (`src/Frontend/Search.php`):
|
||||
- Core search functionality with availability filtering
|
||||
- Price range filtering with Calculator integration
|
||||
- Pagination support
|
||||
- AJAX endpoints: search_rooms, get_availability, get_calendar, calculate_price
|
||||
- Room data formatting for JSON responses
|
||||
- Frontend Shortcodes Class (`src/Frontend/Shortcodes.php`):
|
||||
- All shortcode registration and handlers
|
||||
- Grid/list layout support
|
||||
- Column configuration (1-4 columns)
|
||||
- Sorting options (title, date, price, capacity)
|
||||
- Limit and offset support
|
||||
- Block Registrar Class (`src/Blocks/BlockRegistrar.php`):
|
||||
- Gutenberg block registration
|
||||
- Block editor assets (CSS/JS)
|
||||
- Server-side render callbacks
|
||||
- Block data localization for editor
|
||||
- Frontend Assets:
|
||||
- Comprehensive CSS with CSS custom properties for theming
|
||||
- Building and room card styles
|
||||
- Search form and results styling
|
||||
- Calendar widget styling with availability states
|
||||
- Responsive design (breakpoints: 480px, 768px, 1024px)
|
||||
- JavaScript with SearchForm, CalendarWidget, AvailabilityForm, PriceCalculator classes
|
||||
- AJAX integration with proper error handling
|
||||
- XSS-safe DOM construction (no innerHTML with user data)
|
||||
|
||||
### Changed
|
||||
|
||||
- Plugin.php updated with frontend component initialization
|
||||
- Frontend assets now include localized script data with AJAX URL, nonce, and i18n strings
|
||||
- Widget registration added to init_frontend() method
|
||||
- Search, Shortcodes, and BlockRegistrar initialized when license is valid
|
||||
|
||||
### Security
|
||||
|
||||
- AJAX nonce verification on all frontend requests
|
||||
- Input sanitization on all search parameters
|
||||
- Output escaping in shortcode and widget templates
|
||||
- XSS prevention in JavaScript (textContent instead of innerHTML)
|
||||
|
||||
## [0.5.0] - 2026-01-31
|
||||
|
||||
### Added
|
||||
@@ -290,6 +358,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Input sanitization and output escaping
|
||||
- Server secret masking in license settings
|
||||
|
||||
[0.6.0]: https://src.bundespruefstelle.ch/magdev/wp-bnb/releases/tag/v0.6.0
|
||||
[0.5.0]: https://src.bundespruefstelle.ch/magdev/wp-bnb/releases/tag/v0.5.0
|
||||
[0.4.0]: https://src.bundespruefstelle.ch/magdev/wp-bnb/releases/tag/v0.4.0
|
||||
[0.3.0]: https://src.bundespruefstelle.ch/magdev/wp-bnb/releases/tag/v0.3.0
|
||||
|
||||
29
PLAN.md
29
PLAN.md
@@ -116,11 +116,11 @@ This document outlines the implementation plan for the WP BnB Management plugin.
|
||||
- [x] Automatic price calculation
|
||||
- [x] Service summary display
|
||||
|
||||
## Phase 6: Frontend Features (v0.6.0)
|
||||
## Phase 6: Frontend Features (v0.6.0) - Complete
|
||||
|
||||
### Search & Filtering
|
||||
|
||||
- [ ] Room search with filters
|
||||
- [x] Room search with filters
|
||||
- Date range (availability)
|
||||
- Capacity
|
||||
- Room type
|
||||
@@ -130,23 +130,24 @@ This document outlines the implementation plan for the WP BnB Management plugin.
|
||||
|
||||
### Display Components
|
||||
|
||||
- [ ] Building list/grid shortcode
|
||||
- [ ] Room list/grid shortcode
|
||||
- [ ] Room detail template
|
||||
- [ ] Availability widget
|
||||
- [x] Building list/grid shortcode
|
||||
- [x] Room list/grid shortcode
|
||||
- [x] Room detail template
|
||||
- [x] Availability widget
|
||||
|
||||
### Gutenberg Blocks
|
||||
|
||||
- [ ] Building block
|
||||
- [ ] Room block
|
||||
- [ ] Room search block
|
||||
- [ ] Booking form block
|
||||
- [x] Building block
|
||||
- [x] Room block
|
||||
- [x] Room search block
|
||||
- [x] Buildings list block
|
||||
- [x] Rooms list block
|
||||
|
||||
### Widgets
|
||||
|
||||
- [ ] Similar rooms widget
|
||||
- [ ] Building rooms widget
|
||||
- [ ] Availability calendar widget
|
||||
- [x] Similar rooms widget
|
||||
- [x] Building rooms widget
|
||||
- [x] Availability calendar widget
|
||||
|
||||
## Phase 7: Contact Form 7 Integration (v0.7.0)
|
||||
|
||||
@@ -293,7 +294,7 @@ The plugin will provide extensive hooks for customization:
|
||||
| 0.3.0 | Bookings | Complete |
|
||||
| 0.4.0 | Guests | Complete |
|
||||
| 0.5.0 | Services | Complete |
|
||||
| 0.6.0 | Frontend | TBD |
|
||||
| 0.6.0 | Frontend | Complete |
|
||||
| 0.7.0 | CF7 Integration | TBD |
|
||||
| 0.8.0 | Dashboard | TBD |
|
||||
| 1.0.0 | Stable Release | TBD |
|
||||
|
||||
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;
|
||||
|
||||
})();
|
||||
|
||||
465
src/Blocks/BlockRegistrar.php
Normal file
465
src/Blocks/BlockRegistrar.php
Normal file
@@ -0,0 +1,465 @@
|
||||
<?php
|
||||
/**
|
||||
* Block registrar.
|
||||
*
|
||||
* Handles registration of all Gutenberg blocks.
|
||||
*
|
||||
* @package Magdev\WpBnb\Blocks
|
||||
*/
|
||||
|
||||
declare( strict_types=1 );
|
||||
|
||||
namespace Magdev\WpBnb\Blocks;
|
||||
|
||||
use Magdev\WpBnb\Frontend\Search;
|
||||
use Magdev\WpBnb\Frontend\Shortcodes;
|
||||
use Magdev\WpBnb\PostTypes\Building;
|
||||
use Magdev\WpBnb\PostTypes\Room;
|
||||
|
||||
/**
|
||||
* Block registrar class.
|
||||
*/
|
||||
final class BlockRegistrar {
|
||||
|
||||
/**
|
||||
* Initialize block registration.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function init(): void {
|
||||
add_action( 'init', array( self::class, 'register_blocks' ) );
|
||||
add_action( 'enqueue_block_editor_assets', array( self::class, 'enqueue_editor_assets' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register all blocks.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function register_blocks(): void {
|
||||
// Building block.
|
||||
register_block_type(
|
||||
'wp-bnb/building',
|
||||
array(
|
||||
'attributes' => array(
|
||||
'buildingId' => array(
|
||||
'type' => 'number',
|
||||
'default' => 0,
|
||||
),
|
||||
'showImage' => array(
|
||||
'type' => 'boolean',
|
||||
'default' => true,
|
||||
),
|
||||
'showAddress' => array(
|
||||
'type' => 'boolean',
|
||||
'default' => true,
|
||||
),
|
||||
'showRooms' => array(
|
||||
'type' => 'boolean',
|
||||
'default' => true,
|
||||
),
|
||||
'showContact' => array(
|
||||
'type' => 'boolean',
|
||||
'default' => true,
|
||||
),
|
||||
),
|
||||
'render_callback' => array( self::class, 'render_building_block' ),
|
||||
'editor_script' => 'wp-bnb-blocks-editor',
|
||||
)
|
||||
);
|
||||
|
||||
// Room block.
|
||||
register_block_type(
|
||||
'wp-bnb/room',
|
||||
array(
|
||||
'attributes' => array(
|
||||
'roomId' => array(
|
||||
'type' => 'number',
|
||||
'default' => 0,
|
||||
),
|
||||
'showImage' => array(
|
||||
'type' => 'boolean',
|
||||
'default' => true,
|
||||
),
|
||||
'showGallery' => array(
|
||||
'type' => 'boolean',
|
||||
'default' => true,
|
||||
),
|
||||
'showPrice' => array(
|
||||
'type' => 'boolean',
|
||||
'default' => true,
|
||||
),
|
||||
'showAmenities' => array(
|
||||
'type' => 'boolean',
|
||||
'default' => true,
|
||||
),
|
||||
'showAvailability' => array(
|
||||
'type' => 'boolean',
|
||||
'default' => true,
|
||||
),
|
||||
),
|
||||
'render_callback' => array( self::class, 'render_room_block' ),
|
||||
'editor_script' => 'wp-bnb-blocks-editor',
|
||||
)
|
||||
);
|
||||
|
||||
// Room Search block.
|
||||
register_block_type(
|
||||
'wp-bnb/room-search',
|
||||
array(
|
||||
'attributes' => array(
|
||||
'layout' => array(
|
||||
'type' => 'string',
|
||||
'default' => 'grid',
|
||||
),
|
||||
'columns' => array(
|
||||
'type' => 'number',
|
||||
'default' => 3,
|
||||
),
|
||||
'showDates' => array(
|
||||
'type' => 'boolean',
|
||||
'default' => true,
|
||||
),
|
||||
'showGuests' => array(
|
||||
'type' => 'boolean',
|
||||
'default' => true,
|
||||
),
|
||||
'showRoomType' => array(
|
||||
'type' => 'boolean',
|
||||
'default' => true,
|
||||
),
|
||||
'showAmenities' => array(
|
||||
'type' => 'boolean',
|
||||
'default' => true,
|
||||
),
|
||||
'showPriceRange' => array(
|
||||
'type' => 'boolean',
|
||||
'default' => true,
|
||||
),
|
||||
'showBuilding' => array(
|
||||
'type' => 'boolean',
|
||||
'default' => true,
|
||||
),
|
||||
'resultsPerPage' => array(
|
||||
'type' => 'number',
|
||||
'default' => 12,
|
||||
),
|
||||
),
|
||||
'render_callback' => array( self::class, 'render_room_search_block' ),
|
||||
'editor_script' => 'wp-bnb-blocks-editor',
|
||||
)
|
||||
);
|
||||
|
||||
// Buildings List block.
|
||||
register_block_type(
|
||||
'wp-bnb/buildings',
|
||||
array(
|
||||
'attributes' => array(
|
||||
'layout' => array(
|
||||
'type' => 'string',
|
||||
'default' => 'grid',
|
||||
),
|
||||
'columns' => array(
|
||||
'type' => 'number',
|
||||
'default' => 3,
|
||||
),
|
||||
'limit' => array(
|
||||
'type' => 'number',
|
||||
'default' => -1,
|
||||
),
|
||||
'showImage' => array(
|
||||
'type' => 'boolean',
|
||||
'default' => true,
|
||||
),
|
||||
'showAddress' => array(
|
||||
'type' => 'boolean',
|
||||
'default' => true,
|
||||
),
|
||||
'showRoomsCount' => array(
|
||||
'type' => 'boolean',
|
||||
'default' => true,
|
||||
),
|
||||
),
|
||||
'render_callback' => array( self::class, 'render_buildings_block' ),
|
||||
'editor_script' => 'wp-bnb-blocks-editor',
|
||||
)
|
||||
);
|
||||
|
||||
// Rooms List block.
|
||||
register_block_type(
|
||||
'wp-bnb/rooms',
|
||||
array(
|
||||
'attributes' => array(
|
||||
'layout' => array(
|
||||
'type' => 'string',
|
||||
'default' => 'grid',
|
||||
),
|
||||
'columns' => array(
|
||||
'type' => 'number',
|
||||
'default' => 3,
|
||||
),
|
||||
'limit' => array(
|
||||
'type' => 'number',
|
||||
'default' => 12,
|
||||
),
|
||||
'buildingId' => array(
|
||||
'type' => 'number',
|
||||
'default' => 0,
|
||||
),
|
||||
'roomType' => array(
|
||||
'type' => 'string',
|
||||
'default' => '',
|
||||
),
|
||||
'showImage' => array(
|
||||
'type' => 'boolean',
|
||||
'default' => true,
|
||||
),
|
||||
'showPrice' => array(
|
||||
'type' => 'boolean',
|
||||
'default' => true,
|
||||
),
|
||||
'showCapacity' => array(
|
||||
'type' => 'boolean',
|
||||
'default' => true,
|
||||
),
|
||||
'showAmenities' => array(
|
||||
'type' => 'boolean',
|
||||
'default' => true,
|
||||
),
|
||||
'showBuilding' => array(
|
||||
'type' => 'boolean',
|
||||
'default' => true,
|
||||
),
|
||||
),
|
||||
'render_callback' => array( self::class, 'render_rooms_block' ),
|
||||
'editor_script' => 'wp-bnb-blocks-editor',
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue editor assets.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function enqueue_editor_assets(): void {
|
||||
// Register the editor script.
|
||||
wp_register_script(
|
||||
'wp-bnb-blocks-editor',
|
||||
WP_BNB_URL . 'assets/js/blocks-editor.js',
|
||||
array( 'wp-blocks', 'wp-element', 'wp-editor', 'wp-components', 'wp-i18n', 'wp-data' ),
|
||||
WP_BNB_VERSION,
|
||||
true
|
||||
);
|
||||
|
||||
// Get buildings and rooms for selectors.
|
||||
$buildings = get_posts(
|
||||
array(
|
||||
'post_type' => Building::POST_TYPE,
|
||||
'post_status' => 'publish',
|
||||
'posts_per_page' => -1,
|
||||
'orderby' => 'title',
|
||||
'order' => 'ASC',
|
||||
)
|
||||
);
|
||||
|
||||
$rooms = get_posts(
|
||||
array(
|
||||
'post_type' => Room::POST_TYPE,
|
||||
'post_status' => 'publish',
|
||||
'posts_per_page' => -1,
|
||||
'orderby' => 'title',
|
||||
'order' => 'ASC',
|
||||
)
|
||||
);
|
||||
|
||||
$form_data = Search::get_search_form_data();
|
||||
|
||||
wp_localize_script(
|
||||
'wp-bnb-blocks-editor',
|
||||
'wpBnbBlocks',
|
||||
array(
|
||||
'buildings' => array_map(
|
||||
function ( $building ) {
|
||||
return array(
|
||||
'value' => $building->ID,
|
||||
'label' => $building->post_title,
|
||||
);
|
||||
},
|
||||
$buildings
|
||||
),
|
||||
'rooms' => array_map(
|
||||
function ( $room ) {
|
||||
$building_id = get_post_meta( $room->ID, '_bnb_room_building_id', true );
|
||||
$building = $building_id ? get_post( $building_id ) : null;
|
||||
return array(
|
||||
'value' => $room->ID,
|
||||
'label' => $room->post_title,
|
||||
'building' => $building ? $building->post_title : '',
|
||||
);
|
||||
},
|
||||
$rooms
|
||||
),
|
||||
'roomTypes' => $form_data['room_types'],
|
||||
'amenities' => $form_data['amenities'],
|
||||
'i18n' => array(
|
||||
'selectBuilding' => __( 'Select a building', 'wp-bnb' ),
|
||||
'selectRoom' => __( 'Select a room', 'wp-bnb' ),
|
||||
'noBuildings' => __( 'No buildings found. Create a building first.', 'wp-bnb' ),
|
||||
'noRooms' => __( 'No rooms found. Create a room first.', 'wp-bnb' ),
|
||||
'buildingBlock' => __( 'Building', 'wp-bnb' ),
|
||||
'roomBlock' => __( 'Room', 'wp-bnb' ),
|
||||
'roomSearchBlock' => __( 'Room Search', 'wp-bnb' ),
|
||||
'buildingsBlock' => __( 'Buildings List', 'wp-bnb' ),
|
||||
'roomsBlock' => __( 'Rooms List', 'wp-bnb' ),
|
||||
'displaySettings' => __( 'Display Settings', 'wp-bnb' ),
|
||||
'filterSettings' => __( 'Filter Settings', 'wp-bnb' ),
|
||||
'layout' => __( 'Layout', 'wp-bnb' ),
|
||||
'grid' => __( 'Grid', 'wp-bnb' ),
|
||||
'list' => __( 'List', 'wp-bnb' ),
|
||||
'columns' => __( 'Columns', 'wp-bnb' ),
|
||||
'limit' => __( 'Limit', 'wp-bnb' ),
|
||||
'showImage' => __( 'Show image', 'wp-bnb' ),
|
||||
'showAddress' => __( 'Show address', 'wp-bnb' ),
|
||||
'showRooms' => __( 'Show rooms', 'wp-bnb' ),
|
||||
'showRoomsCount' => __( 'Show rooms count', 'wp-bnb' ),
|
||||
'showContact' => __( 'Show contact', 'wp-bnb' ),
|
||||
'showGallery' => __( 'Show gallery', 'wp-bnb' ),
|
||||
'showPrice' => __( 'Show price', 'wp-bnb' ),
|
||||
'showAmenities' => __( 'Show amenities', 'wp-bnb' ),
|
||||
'showAvailability' => __( 'Show availability', 'wp-bnb' ),
|
||||
'showCapacity' => __( 'Show capacity', 'wp-bnb' ),
|
||||
'showBuilding' => __( 'Show building', 'wp-bnb' ),
|
||||
'showDates' => __( 'Show date filter', 'wp-bnb' ),
|
||||
'showGuests' => __( 'Show guests filter', 'wp-bnb' ),
|
||||
'showRoomType' => __( 'Show room type filter', 'wp-bnb' ),
|
||||
'showPriceRange' => __( 'Show price range filter', 'wp-bnb' ),
|
||||
'resultsPerPage' => __( 'Results per page', 'wp-bnb' ),
|
||||
'roomType' => __( 'Room Type', 'wp-bnb' ),
|
||||
'allTypes' => __( 'All Types', 'wp-bnb' ),
|
||||
'allBuildings' => __( 'All Buildings', 'wp-bnb' ),
|
||||
'previewPlaceholder' => __( 'Preview will appear here', 'wp-bnb' ),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
// Editor styles.
|
||||
wp_enqueue_style(
|
||||
'wp-bnb-blocks-editor',
|
||||
WP_BNB_URL . 'assets/css/blocks-editor.css',
|
||||
array(),
|
||||
WP_BNB_VERSION
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render building block.
|
||||
*
|
||||
* @param array $attributes Block attributes.
|
||||
* @return string HTML output.
|
||||
*/
|
||||
public static function render_building_block( array $attributes ): string {
|
||||
$building_id = $attributes['buildingId'] ?? 0;
|
||||
|
||||
if ( ! $building_id ) {
|
||||
return '<p class="wp-bnb-block-placeholder">' . esc_html__( 'Please select a building.', 'wp-bnb' ) . '</p>';
|
||||
}
|
||||
|
||||
return Shortcodes::render_single_building(
|
||||
array(
|
||||
'id' => $building_id,
|
||||
'show_rooms' => ( $attributes['showRooms'] ?? true ) ? 'yes' : 'no',
|
||||
'show_address' => ( $attributes['showAddress'] ?? true ) ? 'yes' : 'no',
|
||||
'show_contact' => ( $attributes['showContact'] ?? true ) ? 'yes' : 'no',
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render room block.
|
||||
*
|
||||
* @param array $attributes Block attributes.
|
||||
* @return string HTML output.
|
||||
*/
|
||||
public static function render_room_block( array $attributes ): string {
|
||||
$room_id = $attributes['roomId'] ?? 0;
|
||||
|
||||
if ( ! $room_id ) {
|
||||
return '<p class="wp-bnb-block-placeholder">' . esc_html__( 'Please select a room.', 'wp-bnb' ) . '</p>';
|
||||
}
|
||||
|
||||
return Shortcodes::render_single_room(
|
||||
array(
|
||||
'id' => $room_id,
|
||||
'show_gallery' => ( $attributes['showGallery'] ?? true ) ? 'yes' : 'no',
|
||||
'show_pricing' => ( $attributes['showPrice'] ?? true ) ? 'yes' : 'no',
|
||||
'show_amenities' => ( $attributes['showAmenities'] ?? true ) ? 'yes' : 'no',
|
||||
'show_availability' => ( $attributes['showAvailability'] ?? true ) ? 'yes' : 'no',
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render room search block.
|
||||
*
|
||||
* @param array $attributes Block attributes.
|
||||
* @return string HTML output.
|
||||
*/
|
||||
public static function render_room_search_block( array $attributes ): string {
|
||||
return Shortcodes::render_room_search(
|
||||
array(
|
||||
'layout' => $attributes['layout'] ?? 'grid',
|
||||
'columns' => $attributes['columns'] ?? 3,
|
||||
'show_dates' => ( $attributes['showDates'] ?? true ) ? 'yes' : 'no',
|
||||
'show_guests' => ( $attributes['showGuests'] ?? true ) ? 'yes' : 'no',
|
||||
'show_room_type' => ( $attributes['showRoomType'] ?? true ) ? 'yes' : 'no',
|
||||
'show_amenities' => ( $attributes['showAmenities'] ?? true ) ? 'yes' : 'no',
|
||||
'show_price_range' => ( $attributes['showPriceRange'] ?? true ) ? 'yes' : 'no',
|
||||
'show_building' => ( $attributes['showBuilding'] ?? true ) ? 'yes' : 'no',
|
||||
'results_per_page' => $attributes['resultsPerPage'] ?? 12,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render buildings list block.
|
||||
*
|
||||
* @param array $attributes Block attributes.
|
||||
* @return string HTML output.
|
||||
*/
|
||||
public static function render_buildings_block( array $attributes ): string {
|
||||
return Shortcodes::render_buildings(
|
||||
array(
|
||||
'layout' => $attributes['layout'] ?? 'grid',
|
||||
'columns' => $attributes['columns'] ?? 3,
|
||||
'limit' => $attributes['limit'] ?? -1,
|
||||
'show_image' => ( $attributes['showImage'] ?? true ) ? 'yes' : 'no',
|
||||
'show_address' => ( $attributes['showAddress'] ?? true ) ? 'yes' : 'no',
|
||||
'show_rooms_count' => ( $attributes['showRoomsCount'] ?? true ) ? 'yes' : 'no',
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render rooms list block.
|
||||
*
|
||||
* @param array $attributes Block attributes.
|
||||
* @return string HTML output.
|
||||
*/
|
||||
public static function render_rooms_block( array $attributes ): string {
|
||||
return Shortcodes::render_rooms(
|
||||
array(
|
||||
'layout' => $attributes['layout'] ?? 'grid',
|
||||
'columns' => $attributes['columns'] ?? 3,
|
||||
'limit' => $attributes['limit'] ?? 12,
|
||||
'building_id' => $attributes['buildingId'] ?? 0,
|
||||
'room_type' => $attributes['roomType'] ?? '',
|
||||
'show_image' => ( $attributes['showImage'] ?? true ) ? 'yes' : 'no',
|
||||
'show_price' => ( $attributes['showPrice'] ?? true ) ? 'yes' : 'no',
|
||||
'show_capacity' => ( $attributes['showCapacity'] ?? true ) ? 'yes' : 'no',
|
||||
'show_amenities' => ( $attributes['showAmenities'] ?? true ) ? 'yes' : 'no',
|
||||
'show_building' => ( $attributes['showBuilding'] ?? true ) ? 'yes' : 'no',
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
677
src/Frontend/Search.php
Normal file
677
src/Frontend/Search.php
Normal file
@@ -0,0 +1,677 @@
|
||||
<?php
|
||||
/**
|
||||
* Frontend room search.
|
||||
*
|
||||
* Handles room search with availability checking and filtering.
|
||||
*
|
||||
* @package Magdev\WpBnb\Frontend
|
||||
*/
|
||||
|
||||
declare( strict_types=1 );
|
||||
|
||||
namespace Magdev\WpBnb\Frontend;
|
||||
|
||||
use Magdev\WpBnb\Booking\Availability;
|
||||
use Magdev\WpBnb\PostTypes\Booking;
|
||||
use Magdev\WpBnb\PostTypes\Building;
|
||||
use Magdev\WpBnb\PostTypes\Room;
|
||||
use Magdev\WpBnb\Pricing\Calculator;
|
||||
use Magdev\WpBnb\Pricing\PricingTier;
|
||||
use Magdev\WpBnb\Taxonomies\Amenity;
|
||||
use Magdev\WpBnb\Taxonomies\RoomType;
|
||||
|
||||
/**
|
||||
* Search class for frontend room searches.
|
||||
*/
|
||||
final class Search {
|
||||
|
||||
/**
|
||||
* Initialize the search system.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function init(): void {
|
||||
// Public AJAX handlers (no login required).
|
||||
add_action( 'wp_ajax_wp_bnb_search_rooms', array( self::class, 'ajax_search_rooms' ) );
|
||||
add_action( 'wp_ajax_nopriv_wp_bnb_search_rooms', array( self::class, 'ajax_search_rooms' ) );
|
||||
|
||||
add_action( 'wp_ajax_wp_bnb_get_availability', array( self::class, 'ajax_get_availability' ) );
|
||||
add_action( 'wp_ajax_nopriv_wp_bnb_get_availability', array( self::class, 'ajax_get_availability' ) );
|
||||
|
||||
add_action( 'wp_ajax_wp_bnb_get_calendar', array( self::class, 'ajax_get_calendar' ) );
|
||||
add_action( 'wp_ajax_nopriv_wp_bnb_get_calendar', array( self::class, 'ajax_get_calendar' ) );
|
||||
|
||||
add_action( 'wp_ajax_wp_bnb_calculate_price', array( self::class, 'ajax_calculate_price' ) );
|
||||
add_action( 'wp_ajax_nopriv_wp_bnb_calculate_price', array( self::class, 'ajax_calculate_price' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for rooms with filters.
|
||||
*
|
||||
* @param array $args Search arguments.
|
||||
* @return array Array of room data.
|
||||
*/
|
||||
public static function search( array $args = array() ): array {
|
||||
$defaults = array(
|
||||
'check_in' => '',
|
||||
'check_out' => '',
|
||||
'guests' => 0,
|
||||
'room_type' => '',
|
||||
'amenities' => array(),
|
||||
'price_min' => 0,
|
||||
'price_max' => 0,
|
||||
'building_id' => 0,
|
||||
'orderby' => 'title',
|
||||
'order' => 'ASC',
|
||||
'limit' => -1,
|
||||
'offset' => 0,
|
||||
);
|
||||
|
||||
$args = wp_parse_args( $args, $defaults );
|
||||
|
||||
// Build base query.
|
||||
$query_args = array(
|
||||
'post_type' => Room::POST_TYPE,
|
||||
'post_status' => 'publish',
|
||||
'posts_per_page' => (int) $args['limit'],
|
||||
'offset' => (int) $args['offset'],
|
||||
'meta_query' => array(
|
||||
'relation' => 'AND',
|
||||
),
|
||||
'tax_query' => array(
|
||||
'relation' => 'AND',
|
||||
),
|
||||
);
|
||||
|
||||
// Filter by building.
|
||||
if ( ! empty( $args['building_id'] ) ) {
|
||||
$query_args['meta_query'][] = array(
|
||||
'key' => '_bnb_room_building_id',
|
||||
'value' => (int) $args['building_id'],
|
||||
);
|
||||
}
|
||||
|
||||
// Filter by capacity.
|
||||
if ( ! empty( $args['guests'] ) && (int) $args['guests'] > 0 ) {
|
||||
$query_args['meta_query'][] = array(
|
||||
'key' => '_bnb_room_capacity',
|
||||
'value' => (int) $args['guests'],
|
||||
'compare' => '>=',
|
||||
'type' => 'NUMERIC',
|
||||
);
|
||||
}
|
||||
|
||||
// Filter by room status (only available rooms).
|
||||
$query_args['meta_query'][] = array(
|
||||
'relation' => 'OR',
|
||||
array(
|
||||
'key' => '_bnb_room_status',
|
||||
'value' => 'available',
|
||||
),
|
||||
array(
|
||||
'key' => '_bnb_room_status',
|
||||
'compare' => 'NOT EXISTS',
|
||||
),
|
||||
);
|
||||
|
||||
// Filter by room type.
|
||||
if ( ! empty( $args['room_type'] ) ) {
|
||||
$query_args['tax_query'][] = array(
|
||||
'taxonomy' => RoomType::TAXONOMY,
|
||||
'field' => is_numeric( $args['room_type'] ) ? 'term_id' : 'slug',
|
||||
'terms' => $args['room_type'],
|
||||
);
|
||||
}
|
||||
|
||||
// Filter by amenities (all must match).
|
||||
if ( ! empty( $args['amenities'] ) ) {
|
||||
$amenities = is_array( $args['amenities'] ) ? $args['amenities'] : explode( ',', $args['amenities'] );
|
||||
$amenities = array_map( 'trim', $amenities );
|
||||
$amenities = array_filter( $amenities );
|
||||
|
||||
if ( ! empty( $amenities ) ) {
|
||||
$query_args['tax_query'][] = array(
|
||||
'taxonomy' => Amenity::TAXONOMY,
|
||||
'field' => is_numeric( $amenities[0] ) ? 'term_id' : 'slug',
|
||||
'terms' => $amenities,
|
||||
'operator' => 'AND',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle ordering.
|
||||
switch ( $args['orderby'] ) {
|
||||
case 'price':
|
||||
$query_args['meta_key'] = '_bnb_room_price_' . PricingTier::SHORT_TERM->value;
|
||||
$query_args['orderby'] = 'meta_value_num';
|
||||
break;
|
||||
case 'capacity':
|
||||
$query_args['meta_key'] = '_bnb_room_capacity';
|
||||
$query_args['orderby'] = 'meta_value_num';
|
||||
break;
|
||||
case 'date':
|
||||
$query_args['orderby'] = 'date';
|
||||
break;
|
||||
case 'random':
|
||||
$query_args['orderby'] = 'rand';
|
||||
break;
|
||||
default:
|
||||
$query_args['orderby'] = 'title';
|
||||
break;
|
||||
}
|
||||
|
||||
$query_args['order'] = strtoupper( $args['order'] ) === 'DESC' ? 'DESC' : 'ASC';
|
||||
|
||||
// Execute query.
|
||||
$rooms = get_posts( $query_args );
|
||||
|
||||
// Filter by availability if dates provided.
|
||||
if ( ! empty( $args['check_in'] ) && ! empty( $args['check_out'] ) ) {
|
||||
$rooms = self::filter_by_availability( $rooms, $args['check_in'], $args['check_out'] );
|
||||
}
|
||||
|
||||
// Filter by price range.
|
||||
if ( ( ! empty( $args['price_min'] ) || ! empty( $args['price_max'] ) ) && ! empty( $args['check_in'] ) && ! empty( $args['check_out'] ) ) {
|
||||
$rooms = self::filter_by_price_range(
|
||||
$rooms,
|
||||
(float) $args['price_min'],
|
||||
(float) $args['price_max'],
|
||||
$args['check_in'],
|
||||
$args['check_out']
|
||||
);
|
||||
}
|
||||
|
||||
// Build result array with room data.
|
||||
$results = array();
|
||||
foreach ( $rooms as $room ) {
|
||||
$results[] = self::get_room_data( $room, $args['check_in'], $args['check_out'] );
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter rooms by availability.
|
||||
*
|
||||
* @param array $rooms Array of WP_Post objects.
|
||||
* @param string $check_in Check-in date (Y-m-d).
|
||||
* @param string $check_out Check-out date (Y-m-d).
|
||||
* @return array Filtered rooms.
|
||||
*/
|
||||
public static function filter_by_availability( array $rooms, string $check_in, string $check_out ): array {
|
||||
return array_filter(
|
||||
$rooms,
|
||||
function ( $room ) use ( $check_in, $check_out ) {
|
||||
return Availability::is_available( $room->ID, $check_in, $check_out );
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter rooms by price range.
|
||||
*
|
||||
* @param array $rooms Array of WP_Post objects.
|
||||
* @param float $min Minimum price.
|
||||
* @param float $max Maximum price.
|
||||
* @param string $check_in Check-in date.
|
||||
* @param string $check_out Check-out date.
|
||||
* @return array Filtered rooms.
|
||||
*/
|
||||
public static function filter_by_price_range( array $rooms, float $min, float $max, string $check_in, string $check_out ): array {
|
||||
return array_filter(
|
||||
$rooms,
|
||||
function ( $room ) use ( $min, $max, $check_in, $check_out ) {
|
||||
try {
|
||||
$calculator = new Calculator( $room->ID, $check_in, $check_out );
|
||||
$price = $calculator->calculate();
|
||||
|
||||
if ( $min > 0 && $price < $min ) {
|
||||
return false;
|
||||
}
|
||||
if ( $max > 0 && $price > $max ) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} catch ( \Exception $e ) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get complete room data for display.
|
||||
*
|
||||
* @param \WP_Post $room Room post object.
|
||||
* @param string $check_in Optional check-in date.
|
||||
* @param string $check_out Optional check-out date.
|
||||
* @return array Room data array.
|
||||
*/
|
||||
public static function get_room_data( \WP_Post $room, string $check_in = '', string $check_out = '' ): array {
|
||||
$building_id = get_post_meta( $room->ID, '_bnb_room_building_id', true );
|
||||
$building = $building_id ? get_post( $building_id ) : null;
|
||||
|
||||
// Get room types.
|
||||
$room_types = wp_get_post_terms( $room->ID, RoomType::TAXONOMY, array( 'fields' => 'names' ) );
|
||||
|
||||
// Get amenities with icons.
|
||||
$amenities = wp_get_post_terms( $room->ID, Amenity::TAXONOMY );
|
||||
$amenity_list = array();
|
||||
foreach ( $amenities as $amenity ) {
|
||||
$amenity_list[] = array(
|
||||
'id' => $amenity->term_id,
|
||||
'name' => $amenity->name,
|
||||
'slug' => $amenity->slug,
|
||||
'icon' => get_term_meta( $amenity->term_id, 'amenity_icon', true ),
|
||||
);
|
||||
}
|
||||
|
||||
// Get gallery images.
|
||||
$gallery_ids = get_post_meta( $room->ID, '_bnb_room_gallery', true );
|
||||
$gallery = array();
|
||||
if ( $gallery_ids ) {
|
||||
$ids = explode( ',', $gallery_ids );
|
||||
foreach ( $ids as $id ) {
|
||||
$image = wp_get_attachment_image_src( (int) $id, 'large' );
|
||||
if ( $image ) {
|
||||
$gallery[] = array(
|
||||
'id' => (int) $id,
|
||||
'url' => $image[0],
|
||||
'width' => $image[1],
|
||||
'height' => $image[2],
|
||||
'thumb' => wp_get_attachment_image_src( (int) $id, 'thumbnail' )[0] ?? $image[0],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get pricing.
|
||||
$pricing = Calculator::getRoomPricing( $room->ID );
|
||||
$nightly_price = $pricing[ PricingTier::SHORT_TERM->value ]['price'] ?? null;
|
||||
|
||||
// Calculate stay price if dates provided.
|
||||
$stay_price = null;
|
||||
$nights = 0;
|
||||
if ( ! empty( $check_in ) && ! empty( $check_out ) ) {
|
||||
try {
|
||||
$calculator = new Calculator( $room->ID, $check_in, $check_out );
|
||||
$stay_price = $calculator->calculate();
|
||||
$nights = $calculator->getNights();
|
||||
} catch ( \Exception $e ) {
|
||||
$stay_price = null;
|
||||
}
|
||||
}
|
||||
|
||||
return array(
|
||||
'id' => $room->ID,
|
||||
'title' => $room->post_title,
|
||||
'slug' => $room->post_name,
|
||||
'excerpt' => get_the_excerpt( $room ),
|
||||
'content' => apply_filters( 'the_content', $room->post_content ),
|
||||
'permalink' => get_permalink( $room->ID ),
|
||||
'featured_image' => get_the_post_thumbnail_url( $room->ID, 'large' ),
|
||||
'thumbnail' => get_the_post_thumbnail_url( $room->ID, 'medium' ),
|
||||
'gallery' => $gallery,
|
||||
'building' => $building ? array(
|
||||
'id' => $building->ID,
|
||||
'title' => $building->post_title,
|
||||
'permalink' => get_permalink( $building->ID ),
|
||||
'city' => get_post_meta( $building->ID, '_bnb_building_city', true ),
|
||||
) : null,
|
||||
'room_number' => get_post_meta( $room->ID, '_bnb_room_room_number', true ),
|
||||
'floor' => (int) get_post_meta( $room->ID, '_bnb_room_floor', true ),
|
||||
'capacity' => (int) get_post_meta( $room->ID, '_bnb_room_capacity', true ),
|
||||
'max_adults' => (int) get_post_meta( $room->ID, '_bnb_room_max_adults', true ),
|
||||
'max_children' => (int) get_post_meta( $room->ID, '_bnb_room_max_children', true ),
|
||||
'size' => (float) get_post_meta( $room->ID, '_bnb_room_size', true ),
|
||||
'beds' => get_post_meta( $room->ID, '_bnb_room_beds', true ),
|
||||
'bathrooms' => (float) get_post_meta( $room->ID, '_bnb_room_bathrooms', true ),
|
||||
'room_types' => $room_types,
|
||||
'amenities' => $amenity_list,
|
||||
'nightly_price' => $nightly_price,
|
||||
'price_formatted' => $nightly_price ? Calculator::formatPrice( $nightly_price ) : null,
|
||||
'stay_price' => $stay_price,
|
||||
'stay_price_formatted' => $stay_price ? Calculator::formatPrice( $stay_price ) : null,
|
||||
'nights' => $nights,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get data for search form (room types, amenities, buildings).
|
||||
*
|
||||
* @return array Form data.
|
||||
*/
|
||||
public static function get_search_form_data(): array {
|
||||
// Get all room types.
|
||||
$room_types = get_terms(
|
||||
array(
|
||||
'taxonomy' => RoomType::TAXONOMY,
|
||||
'hide_empty' => true,
|
||||
'orderby' => 'meta_value_num',
|
||||
'meta_key' => 'room_type_sort_order',
|
||||
'order' => 'ASC',
|
||||
)
|
||||
);
|
||||
|
||||
// Get all amenities.
|
||||
$amenities = get_terms(
|
||||
array(
|
||||
'taxonomy' => Amenity::TAXONOMY,
|
||||
'hide_empty' => true,
|
||||
'orderby' => 'name',
|
||||
'order' => 'ASC',
|
||||
)
|
||||
);
|
||||
|
||||
// Get all buildings with rooms.
|
||||
$buildings = get_posts(
|
||||
array(
|
||||
'post_type' => Building::POST_TYPE,
|
||||
'post_status' => 'publish',
|
||||
'posts_per_page' => -1,
|
||||
'orderby' => 'title',
|
||||
'order' => 'ASC',
|
||||
)
|
||||
);
|
||||
|
||||
// Filter buildings to only those with rooms.
|
||||
$buildings_with_rooms = array();
|
||||
foreach ( $buildings as $building ) {
|
||||
$rooms = Room::get_rooms_for_building( $building->ID );
|
||||
if ( ! empty( $rooms ) ) {
|
||||
$buildings_with_rooms[] = array(
|
||||
'id' => $building->ID,
|
||||
'title' => $building->post_title,
|
||||
'city' => get_post_meta( $building->ID, '_bnb_building_city', true ),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Get price range from all rooms.
|
||||
$price_range = self::get_price_range();
|
||||
|
||||
// Get capacity range.
|
||||
$capacity_range = self::get_capacity_range();
|
||||
|
||||
return array(
|
||||
'room_types' => array_map(
|
||||
function ( $term ) {
|
||||
return array(
|
||||
'id' => $term->term_id,
|
||||
'name' => $term->name,
|
||||
'slug' => $term->slug,
|
||||
'parent' => $term->parent,
|
||||
'count' => $term->count,
|
||||
'capacity' => (int) get_term_meta( $term->term_id, 'room_type_base_capacity', true ),
|
||||
);
|
||||
},
|
||||
$room_types
|
||||
),
|
||||
'amenities' => array_map(
|
||||
function ( $term ) {
|
||||
return array(
|
||||
'id' => $term->term_id,
|
||||
'name' => $term->name,
|
||||
'slug' => $term->slug,
|
||||
'icon' => get_term_meta( $term->term_id, 'amenity_icon', true ),
|
||||
'count' => $term->count,
|
||||
);
|
||||
},
|
||||
$amenities
|
||||
),
|
||||
'buildings' => $buildings_with_rooms,
|
||||
'price_range' => $price_range,
|
||||
'capacity_range' => $capacity_range,
|
||||
'currency' => get_option( 'wp_bnb_currency', 'CHF' ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get price range from all rooms.
|
||||
*
|
||||
* @return array Min and max prices.
|
||||
*/
|
||||
public static function get_price_range(): array {
|
||||
global $wpdb;
|
||||
|
||||
$meta_key = '_bnb_room_price_' . PricingTier::SHORT_TERM->value;
|
||||
|
||||
$result = $wpdb->get_row(
|
||||
$wpdb->prepare(
|
||||
"SELECT MIN(CAST(meta_value AS DECIMAL(10,2))) as min_price,
|
||||
MAX(CAST(meta_value AS DECIMAL(10,2))) as max_price
|
||||
FROM {$wpdb->postmeta} pm
|
||||
JOIN {$wpdb->posts} p ON pm.post_id = p.ID
|
||||
WHERE pm.meta_key = %s
|
||||
AND pm.meta_value != ''
|
||||
AND pm.meta_value > 0
|
||||
AND p.post_type = %s
|
||||
AND p.post_status = 'publish'",
|
||||
$meta_key,
|
||||
Room::POST_TYPE
|
||||
)
|
||||
);
|
||||
|
||||
return array(
|
||||
'min' => $result ? (float) $result->min_price : 0,
|
||||
'max' => $result ? (float) $result->max_price : 500,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get capacity range from all rooms.
|
||||
*
|
||||
* @return array Min and max capacity.
|
||||
*/
|
||||
public static function get_capacity_range(): array {
|
||||
global $wpdb;
|
||||
|
||||
$result = $wpdb->get_row(
|
||||
$wpdb->prepare(
|
||||
"SELECT MIN(CAST(meta_value AS UNSIGNED)) as min_capacity,
|
||||
MAX(CAST(meta_value AS UNSIGNED)) as max_capacity
|
||||
FROM {$wpdb->postmeta} pm
|
||||
JOIN {$wpdb->posts} p ON pm.post_id = p.ID
|
||||
WHERE pm.meta_key = '_bnb_room_capacity'
|
||||
AND pm.meta_value != ''
|
||||
AND p.post_type = %s
|
||||
AND p.post_status = 'publish'",
|
||||
Room::POST_TYPE
|
||||
)
|
||||
);
|
||||
|
||||
return array(
|
||||
'min' => $result && $result->min_capacity ? (int) $result->min_capacity : 1,
|
||||
'max' => $result && $result->max_capacity ? (int) $result->max_capacity : 10,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX handler for room search.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function ajax_search_rooms(): void {
|
||||
// phpcs:disable WordPress.Security.NonceVerification.Missing -- Public API.
|
||||
$args = array(
|
||||
'check_in' => isset( $_POST['check_in'] ) ? sanitize_text_field( wp_unslash( $_POST['check_in'] ) ) : '',
|
||||
'check_out' => isset( $_POST['check_out'] ) ? sanitize_text_field( wp_unslash( $_POST['check_out'] ) ) : '',
|
||||
'guests' => isset( $_POST['guests'] ) ? absint( $_POST['guests'] ) : 0,
|
||||
'room_type' => isset( $_POST['room_type'] ) ? sanitize_text_field( wp_unslash( $_POST['room_type'] ) ) : '',
|
||||
'amenities' => isset( $_POST['amenities'] ) ? array_map( 'sanitize_text_field', (array) $_POST['amenities'] ) : array(),
|
||||
'price_min' => isset( $_POST['price_min'] ) ? (float) $_POST['price_min'] : 0,
|
||||
'price_max' => isset( $_POST['price_max'] ) ? (float) $_POST['price_max'] : 0,
|
||||
'building_id' => isset( $_POST['building_id'] ) ? absint( $_POST['building_id'] ) : 0,
|
||||
'orderby' => isset( $_POST['orderby'] ) ? sanitize_text_field( wp_unslash( $_POST['orderby'] ) ) : 'title',
|
||||
'order' => isset( $_POST['order'] ) ? sanitize_text_field( wp_unslash( $_POST['order'] ) ) : 'ASC',
|
||||
'limit' => isset( $_POST['limit'] ) ? absint( $_POST['limit'] ) : 12,
|
||||
'offset' => isset( $_POST['offset'] ) ? absint( $_POST['offset'] ) : 0,
|
||||
);
|
||||
// phpcs:enable WordPress.Security.NonceVerification.Missing
|
||||
|
||||
// Validate dates if provided.
|
||||
if ( ! empty( $args['check_in'] ) && ! empty( $args['check_out'] ) ) {
|
||||
$check_in = strtotime( $args['check_in'] );
|
||||
$check_out = strtotime( $args['check_out'] );
|
||||
|
||||
if ( ! $check_in || ! $check_out || $check_out <= $check_in ) {
|
||||
wp_send_json_error(
|
||||
array( 'message' => __( 'Invalid date range.', 'wp-bnb' ) )
|
||||
);
|
||||
}
|
||||
|
||||
if ( $check_in < strtotime( 'today' ) ) {
|
||||
wp_send_json_error(
|
||||
array( 'message' => __( 'Check-in date cannot be in the past.', 'wp-bnb' ) )
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$results = self::search( $args );
|
||||
|
||||
wp_send_json_success(
|
||||
array(
|
||||
'rooms' => $results,
|
||||
'count' => count( $results ),
|
||||
'args' => $args,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX handler for availability check.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function ajax_get_availability(): void {
|
||||
// phpcs:disable WordPress.Security.NonceVerification.Missing -- Public API.
|
||||
$room_id = isset( $_POST['room_id'] ) ? absint( $_POST['room_id'] ) : 0;
|
||||
$check_in = isset( $_POST['check_in'] ) ? sanitize_text_field( wp_unslash( $_POST['check_in'] ) ) : '';
|
||||
$check_out = isset( $_POST['check_out'] ) ? sanitize_text_field( wp_unslash( $_POST['check_out'] ) ) : '';
|
||||
// phpcs:enable WordPress.Security.NonceVerification.Missing
|
||||
|
||||
if ( ! $room_id || ! $check_in || ! $check_out ) {
|
||||
wp_send_json_error(
|
||||
array( 'message' => __( 'Missing required parameters.', 'wp-bnb' ) )
|
||||
);
|
||||
}
|
||||
|
||||
$available = Availability::is_available( $room_id, $check_in, $check_out );
|
||||
|
||||
$result = array(
|
||||
'available' => $available,
|
||||
'room_id' => $room_id,
|
||||
'check_in' => $check_in,
|
||||
'check_out' => $check_out,
|
||||
);
|
||||
|
||||
if ( $available ) {
|
||||
try {
|
||||
$calculator = new Calculator( $room_id, $check_in, $check_out );
|
||||
$price = $calculator->calculate();
|
||||
|
||||
$result['price'] = $price;
|
||||
$result['price_formatted'] = Calculator::formatPrice( $price );
|
||||
$result['nights'] = $calculator->getNights();
|
||||
$result['breakdown'] = $calculator->getBreakdown();
|
||||
} catch ( \Exception $e ) {
|
||||
$result['price_error'] = $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
wp_send_json_success( $result );
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX handler for calendar data.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function ajax_get_calendar(): void {
|
||||
// phpcs:disable WordPress.Security.NonceVerification.Missing -- Public API.
|
||||
$room_id = isset( $_POST['room_id'] ) ? absint( $_POST['room_id'] ) : 0;
|
||||
$year = isset( $_POST['year'] ) ? absint( $_POST['year'] ) : (int) gmdate( 'Y' );
|
||||
$month = isset( $_POST['month'] ) ? absint( $_POST['month'] ) : (int) gmdate( 'n' );
|
||||
// phpcs:enable WordPress.Security.NonceVerification.Missing
|
||||
|
||||
if ( ! $room_id ) {
|
||||
wp_send_json_error(
|
||||
array( 'message' => __( 'Room ID is required.', 'wp-bnb' ) )
|
||||
);
|
||||
}
|
||||
|
||||
// Validate month.
|
||||
$month = max( 1, min( 12, $month ) );
|
||||
|
||||
// Get calendar data.
|
||||
$calendar = Availability::get_calendar_data( $room_id, $year, $month );
|
||||
|
||||
// Simplify for frontend (remove booking details, just show availability).
|
||||
$days = array();
|
||||
foreach ( $calendar['days'] as $day_num => $day_data ) {
|
||||
$days[ $day_num ] = array(
|
||||
'date' => $day_data['date'],
|
||||
'day' => $day_data['day'],
|
||||
'available' => ! $day_data['is_booked'],
|
||||
'is_past' => $day_data['is_past'],
|
||||
'is_today' => $day_data['is_today'],
|
||||
);
|
||||
}
|
||||
|
||||
wp_send_json_success(
|
||||
array(
|
||||
'room_id' => $room_id,
|
||||
'year' => $year,
|
||||
'month' => $month,
|
||||
'month_name' => $calendar['month_name'],
|
||||
'days_in_month' => $calendar['days_in_month'],
|
||||
'first_day_of_week' => $calendar['first_day_of_week'],
|
||||
'days' => $days,
|
||||
'prev_month' => $calendar['prev_month'],
|
||||
'next_month' => $calendar['next_month'],
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX handler for price calculation.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function ajax_calculate_price(): void {
|
||||
// phpcs:disable WordPress.Security.NonceVerification.Missing -- Public API.
|
||||
$room_id = isset( $_POST['room_id'] ) ? absint( $_POST['room_id'] ) : 0;
|
||||
$check_in = isset( $_POST['check_in'] ) ? sanitize_text_field( wp_unslash( $_POST['check_in'] ) ) : '';
|
||||
$check_out = isset( $_POST['check_out'] ) ? sanitize_text_field( wp_unslash( $_POST['check_out'] ) ) : '';
|
||||
// phpcs:enable WordPress.Security.NonceVerification.Missing
|
||||
|
||||
if ( ! $room_id || ! $check_in || ! $check_out ) {
|
||||
wp_send_json_error(
|
||||
array( 'message' => __( 'Missing required parameters.', 'wp-bnb' ) )
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
$calculator = new Calculator( $room_id, $check_in, $check_out );
|
||||
$price = $calculator->calculate();
|
||||
$breakdown = $calculator->getBreakdown();
|
||||
|
||||
wp_send_json_success(
|
||||
array(
|
||||
'room_id' => $room_id,
|
||||
'check_in' => $check_in,
|
||||
'check_out' => $check_out,
|
||||
'nights' => $calculator->getNights(),
|
||||
'price' => $price,
|
||||
'price_formatted' => Calculator::formatPrice( $price ),
|
||||
'tier' => $breakdown['tier'] ?? null,
|
||||
'breakdown' => $breakdown,
|
||||
)
|
||||
);
|
||||
} catch ( \Exception $e ) {
|
||||
wp_send_json_error(
|
||||
array( 'message' => $e->getMessage() )
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
867
src/Frontend/Shortcodes.php
Normal file
867
src/Frontend/Shortcodes.php
Normal file
@@ -0,0 +1,867 @@
|
||||
<?php
|
||||
/**
|
||||
* Frontend shortcodes.
|
||||
*
|
||||
* Handles all shortcode registration and rendering.
|
||||
*
|
||||
* @package Magdev\WpBnb\Frontend
|
||||
*/
|
||||
|
||||
declare( strict_types=1 );
|
||||
|
||||
namespace Magdev\WpBnb\Frontend;
|
||||
|
||||
use Magdev\WpBnb\Booking\Availability;
|
||||
use Magdev\WpBnb\PostTypes\Building;
|
||||
use Magdev\WpBnb\PostTypes\Room;
|
||||
use Magdev\WpBnb\Pricing\Calculator;
|
||||
use Magdev\WpBnb\Pricing\PricingTier;
|
||||
use Magdev\WpBnb\Taxonomies\Amenity;
|
||||
use Magdev\WpBnb\Taxonomies\RoomType;
|
||||
|
||||
/**
|
||||
* Shortcodes class.
|
||||
*/
|
||||
final class Shortcodes {
|
||||
|
||||
/**
|
||||
* Initialize shortcodes.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function init(): void {
|
||||
add_shortcode( 'bnb_buildings', array( self::class, 'render_buildings' ) );
|
||||
add_shortcode( 'bnb_rooms', array( self::class, 'render_rooms' ) );
|
||||
add_shortcode( 'bnb_room_search', array( self::class, 'render_room_search' ) );
|
||||
add_shortcode( 'bnb_building', array( self::class, 'render_single_building' ) );
|
||||
add_shortcode( 'bnb_room', array( self::class, 'render_single_room' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Render buildings list/grid shortcode.
|
||||
*
|
||||
* @param array $atts Shortcode attributes.
|
||||
* @return string HTML output.
|
||||
*/
|
||||
public static function render_buildings( $atts ): string {
|
||||
$atts = shortcode_atts(
|
||||
array(
|
||||
'layout' => 'grid',
|
||||
'columns' => 3,
|
||||
'limit' => -1,
|
||||
'orderby' => 'title',
|
||||
'order' => 'ASC',
|
||||
'show_image' => 'yes',
|
||||
'show_address' => 'yes',
|
||||
'show_rooms_count' => 'yes',
|
||||
),
|
||||
$atts,
|
||||
'bnb_buildings'
|
||||
);
|
||||
|
||||
// Query buildings.
|
||||
$query_args = array(
|
||||
'post_type' => Building::POST_TYPE,
|
||||
'post_status' => 'publish',
|
||||
'posts_per_page' => (int) $atts['limit'],
|
||||
'orderby' => sanitize_text_field( $atts['orderby'] ),
|
||||
'order' => strtoupper( $atts['order'] ) === 'DESC' ? 'DESC' : 'ASC',
|
||||
);
|
||||
|
||||
$buildings = get_posts( $query_args );
|
||||
|
||||
if ( empty( $buildings ) ) {
|
||||
return '<p class="wp-bnb-no-results">' . esc_html__( 'No buildings found.', 'wp-bnb' ) . '</p>';
|
||||
}
|
||||
|
||||
$layout = sanitize_text_field( $atts['layout'] );
|
||||
$columns = max( 1, min( 4, (int) $atts['columns'] ) );
|
||||
|
||||
$classes = array(
|
||||
'wp-bnb-buildings',
|
||||
'wp-bnb-buildings-' . $layout,
|
||||
);
|
||||
|
||||
if ( 'grid' === $layout ) {
|
||||
$classes[] = 'wp-bnb-columns-' . $columns;
|
||||
}
|
||||
|
||||
ob_start();
|
||||
?>
|
||||
<div class="<?php echo esc_attr( implode( ' ', $classes ) ); ?>">
|
||||
<?php foreach ( $buildings as $building ) : ?>
|
||||
<?php echo self::render_building_card( $building, $atts ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php
|
||||
return ob_get_clean();
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a single building card.
|
||||
*
|
||||
* @param \WP_Post $building Building post.
|
||||
* @param array $atts Display attributes.
|
||||
* @return string HTML output.
|
||||
*/
|
||||
private static function render_building_card( \WP_Post $building, array $atts ): string {
|
||||
$show_image = 'yes' === $atts['show_image'];
|
||||
$show_address = 'yes' === $atts['show_address'];
|
||||
$show_rooms_count = 'yes' === $atts['show_rooms_count'];
|
||||
|
||||
// Get room count.
|
||||
$rooms = Room::get_rooms_for_building( $building->ID );
|
||||
$room_count = count( $rooms );
|
||||
|
||||
ob_start();
|
||||
?>
|
||||
<div class="wp-bnb-building-card">
|
||||
<?php if ( $show_image && has_post_thumbnail( $building->ID ) ) : ?>
|
||||
<div class="wp-bnb-building-image">
|
||||
<a href="<?php echo esc_url( get_permalink( $building->ID ) ); ?>">
|
||||
<?php echo get_the_post_thumbnail( $building->ID, 'medium_large' ); ?>
|
||||
</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="wp-bnb-building-content">
|
||||
<h3 class="wp-bnb-building-title">
|
||||
<a href="<?php echo esc_url( get_permalink( $building->ID ) ); ?>">
|
||||
<?php echo esc_html( $building->post_title ); ?>
|
||||
</a>
|
||||
</h3>
|
||||
|
||||
<?php if ( $show_address ) : ?>
|
||||
<?php
|
||||
$city = get_post_meta( $building->ID, '_bnb_building_city', true );
|
||||
$country = get_post_meta( $building->ID, '_bnb_building_country', true );
|
||||
if ( $city || $country ) :
|
||||
$countries = Building::get_countries();
|
||||
$country_name = $countries[ $country ] ?? $country;
|
||||
?>
|
||||
<p class="wp-bnb-building-address">
|
||||
<span class="dashicons dashicons-location"></span>
|
||||
<?php echo esc_html( implode( ', ', array_filter( array( $city, $country_name ) ) ) ); ?>
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ( $show_rooms_count && $room_count > 0 ) : ?>
|
||||
<p class="wp-bnb-building-rooms">
|
||||
<span class="dashicons dashicons-admin-home"></span>
|
||||
<?php
|
||||
printf(
|
||||
/* translators: %d: Number of rooms */
|
||||
esc_html( _n( '%d room', '%d rooms', $room_count, 'wp-bnb' ) ),
|
||||
(int) $room_count
|
||||
);
|
||||
?>
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ( has_excerpt( $building->ID ) ) : ?>
|
||||
<div class="wp-bnb-building-excerpt">
|
||||
<?php echo wp_kses_post( get_the_excerpt( $building->ID ) ); ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<a href="<?php echo esc_url( get_permalink( $building->ID ) ); ?>" class="wp-bnb-button">
|
||||
<?php esc_html_e( 'View Details', 'wp-bnb' ); ?>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
return ob_get_clean();
|
||||
}
|
||||
|
||||
/**
|
||||
* Render rooms list/grid shortcode.
|
||||
*
|
||||
* @param array $atts Shortcode attributes.
|
||||
* @return string HTML output.
|
||||
*/
|
||||
public static function render_rooms( $atts ): string {
|
||||
$atts = shortcode_atts(
|
||||
array(
|
||||
'layout' => 'grid',
|
||||
'columns' => 3,
|
||||
'limit' => 12,
|
||||
'building_id' => 0,
|
||||
'room_type' => '',
|
||||
'amenities' => '',
|
||||
'orderby' => 'title',
|
||||
'order' => 'ASC',
|
||||
'show_image' => 'yes',
|
||||
'show_price' => 'yes',
|
||||
'show_capacity' => 'yes',
|
||||
'show_amenities' => 'yes',
|
||||
'show_building' => 'yes',
|
||||
),
|
||||
$atts,
|
||||
'bnb_rooms'
|
||||
);
|
||||
|
||||
// Use search function for filtering.
|
||||
$search_args = array(
|
||||
'building_id' => (int) $atts['building_id'],
|
||||
'room_type' => sanitize_text_field( $atts['room_type'] ),
|
||||
'amenities' => $atts['amenities'] ? explode( ',', $atts['amenities'] ) : array(),
|
||||
'orderby' => sanitize_text_field( $atts['orderby'] ),
|
||||
'order' => $atts['order'],
|
||||
'limit' => (int) $atts['limit'],
|
||||
);
|
||||
|
||||
$rooms = Search::search( $search_args );
|
||||
|
||||
if ( empty( $rooms ) ) {
|
||||
return '<p class="wp-bnb-no-results">' . esc_html__( 'No rooms found.', 'wp-bnb' ) . '</p>';
|
||||
}
|
||||
|
||||
$layout = sanitize_text_field( $atts['layout'] );
|
||||
$columns = max( 1, min( 4, (int) $atts['columns'] ) );
|
||||
|
||||
$classes = array(
|
||||
'wp-bnb-rooms',
|
||||
'wp-bnb-rooms-' . $layout,
|
||||
);
|
||||
|
||||
if ( 'grid' === $layout ) {
|
||||
$classes[] = 'wp-bnb-columns-' . $columns;
|
||||
}
|
||||
|
||||
ob_start();
|
||||
?>
|
||||
<div class="<?php echo esc_attr( implode( ' ', $classes ) ); ?>">
|
||||
<?php foreach ( $rooms as $room_data ) : ?>
|
||||
<?php echo self::render_room_card( $room_data, $atts ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php
|
||||
return ob_get_clean();
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a single room card.
|
||||
*
|
||||
* @param array $room Room data array.
|
||||
* @param array $atts Display attributes.
|
||||
* @return string HTML output.
|
||||
*/
|
||||
private static function render_room_card( array $room, array $atts ): string {
|
||||
$show_image = 'yes' === $atts['show_image'];
|
||||
$show_price = 'yes' === $atts['show_price'];
|
||||
$show_capacity = 'yes' === $atts['show_capacity'];
|
||||
$show_amenities = 'yes' === $atts['show_amenities'];
|
||||
$show_building = 'yes' === $atts['show_building'];
|
||||
|
||||
ob_start();
|
||||
?>
|
||||
<div class="wp-bnb-room-card" data-room-id="<?php echo esc_attr( $room['id'] ); ?>">
|
||||
<?php if ( $show_image && ! empty( $room['featured_image'] ) ) : ?>
|
||||
<div class="wp-bnb-room-image">
|
||||
<a href="<?php echo esc_url( $room['permalink'] ); ?>">
|
||||
<img src="<?php echo esc_url( $room['featured_image'] ); ?>" alt="<?php echo esc_attr( $room['title'] ); ?>">
|
||||
</a>
|
||||
<?php if ( ! empty( $room['room_types'] ) ) : ?>
|
||||
<span class="wp-bnb-room-type-badge"><?php echo esc_html( $room['room_types'][0] ); ?></span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="wp-bnb-room-content">
|
||||
<h3 class="wp-bnb-room-title">
|
||||
<a href="<?php echo esc_url( $room['permalink'] ); ?>">
|
||||
<?php echo esc_html( $room['title'] ); ?>
|
||||
</a>
|
||||
</h3>
|
||||
|
||||
<?php if ( $show_building && ! empty( $room['building'] ) ) : ?>
|
||||
<p class="wp-bnb-room-building">
|
||||
<span class="dashicons dashicons-building"></span>
|
||||
<a href="<?php echo esc_url( $room['building']['permalink'] ); ?>">
|
||||
<?php echo esc_html( $room['building']['title'] ); ?>
|
||||
</a>
|
||||
<?php if ( ! empty( $room['building']['city'] ) ) : ?>
|
||||
<span class="wp-bnb-room-city">, <?php echo esc_html( $room['building']['city'] ); ?></span>
|
||||
<?php endif; ?>
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="wp-bnb-room-meta">
|
||||
<?php if ( $show_capacity && ! empty( $room['capacity'] ) ) : ?>
|
||||
<span class="wp-bnb-room-capacity">
|
||||
<span class="dashicons dashicons-groups"></span>
|
||||
<?php
|
||||
printf(
|
||||
/* translators: %d: Number of guests */
|
||||
esc_html( _n( '%d guest', '%d guests', $room['capacity'], 'wp-bnb' ) ),
|
||||
(int) $room['capacity']
|
||||
);
|
||||
?>
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ( ! empty( $room['size'] ) ) : ?>
|
||||
<span class="wp-bnb-room-size">
|
||||
<span class="dashicons dashicons-editor-expand"></span>
|
||||
<?php echo esc_html( $room['size'] ); ?> m²
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ( ! empty( $room['beds'] ) ) : ?>
|
||||
<span class="wp-bnb-room-beds">
|
||||
<span class="dashicons dashicons-admin-home"></span>
|
||||
<?php echo esc_html( $room['beds'] ); ?>
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php if ( $show_amenities && ! empty( $room['amenities'] ) ) : ?>
|
||||
<div class="wp-bnb-room-amenities">
|
||||
<?php foreach ( array_slice( $room['amenities'], 0, 4 ) as $amenity ) : ?>
|
||||
<span class="wp-bnb-amenity" title="<?php echo esc_attr( $amenity['name'] ); ?>">
|
||||
<?php if ( ! empty( $amenity['icon'] ) ) : ?>
|
||||
<span class="dashicons dashicons-<?php echo esc_attr( $amenity['icon'] ); ?>"></span>
|
||||
<?php endif; ?>
|
||||
<span class="wp-bnb-amenity-name"><?php echo esc_html( $amenity['name'] ); ?></span>
|
||||
</span>
|
||||
<?php endforeach; ?>
|
||||
<?php if ( count( $room['amenities'] ) > 4 ) : ?>
|
||||
<span class="wp-bnb-amenity-more">
|
||||
+<?php echo (int) ( count( $room['amenities'] ) - 4 ); ?>
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="wp-bnb-room-footer">
|
||||
<?php if ( $show_price && ! empty( $room['price_formatted'] ) ) : ?>
|
||||
<span class="wp-bnb-room-price">
|
||||
<span class="wp-bnb-price-amount"><?php echo esc_html( $room['price_formatted'] ); ?></span>
|
||||
<span class="wp-bnb-price-unit"><?php esc_html_e( '/night', 'wp-bnb' ); ?></span>
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
|
||||
<a href="<?php echo esc_url( $room['permalink'] ); ?>" class="wp-bnb-button">
|
||||
<?php esc_html_e( 'View Details', 'wp-bnb' ); ?>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
return ob_get_clean();
|
||||
}
|
||||
|
||||
/**
|
||||
* Render room search form with results.
|
||||
*
|
||||
* @param array $atts Shortcode attributes.
|
||||
* @return string HTML output.
|
||||
*/
|
||||
public static function render_room_search( $atts ): string {
|
||||
$atts = shortcode_atts(
|
||||
array(
|
||||
'layout' => 'grid',
|
||||
'columns' => 3,
|
||||
'show_dates' => 'yes',
|
||||
'show_guests' => 'yes',
|
||||
'show_room_type' => 'yes',
|
||||
'show_amenities' => 'yes',
|
||||
'show_price_range' => 'yes',
|
||||
'show_building' => 'yes',
|
||||
'results_per_page' => 12,
|
||||
),
|
||||
$atts,
|
||||
'bnb_room_search'
|
||||
);
|
||||
|
||||
// Get search form data.
|
||||
$form_data = Search::get_search_form_data();
|
||||
|
||||
$layout = sanitize_text_field( $atts['layout'] );
|
||||
$columns = max( 1, min( 4, (int) $atts['columns'] ) );
|
||||
|
||||
ob_start();
|
||||
?>
|
||||
<div class="wp-bnb-room-search" data-layout="<?php echo esc_attr( $layout ); ?>" data-columns="<?php echo esc_attr( $columns ); ?>" data-per-page="<?php echo esc_attr( $atts['results_per_page'] ); ?>">
|
||||
<form class="wp-bnb-search-form" id="wp-bnb-search-form">
|
||||
<div class="wp-bnb-search-fields">
|
||||
<?php if ( 'yes' === $atts['show_dates'] ) : ?>
|
||||
<div class="wp-bnb-field wp-bnb-field-dates">
|
||||
<div class="wp-bnb-field-group">
|
||||
<label for="wp-bnb-check-in"><?php esc_html_e( 'Check-in', 'wp-bnb' ); ?></label>
|
||||
<input type="date" id="wp-bnb-check-in" name="check_in" min="<?php echo esc_attr( gmdate( 'Y-m-d' ) ); ?>">
|
||||
</div>
|
||||
<div class="wp-bnb-field-group">
|
||||
<label for="wp-bnb-check-out"><?php esc_html_e( 'Check-out', 'wp-bnb' ); ?></label>
|
||||
<input type="date" id="wp-bnb-check-out" name="check_out" min="<?php echo esc_attr( gmdate( 'Y-m-d', strtotime( '+1 day' ) ) ); ?>">
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ( 'yes' === $atts['show_guests'] ) : ?>
|
||||
<div class="wp-bnb-field wp-bnb-field-guests">
|
||||
<label for="wp-bnb-guests"><?php esc_html_e( 'Guests', 'wp-bnb' ); ?></label>
|
||||
<select id="wp-bnb-guests" name="guests">
|
||||
<option value=""><?php esc_html_e( 'Any', 'wp-bnb' ); ?></option>
|
||||
<?php for ( $i = 1; $i <= $form_data['capacity_range']['max']; $i++ ) : ?>
|
||||
<option value="<?php echo esc_attr( $i ); ?>">
|
||||
<?php echo esc_html( $i ); ?>
|
||||
</option>
|
||||
<?php endfor; ?>
|
||||
</select>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ( 'yes' === $atts['show_room_type'] && ! empty( $form_data['room_types'] ) ) : ?>
|
||||
<div class="wp-bnb-field wp-bnb-field-room-type">
|
||||
<label for="wp-bnb-room-type"><?php esc_html_e( 'Room Type', 'wp-bnb' ); ?></label>
|
||||
<select id="wp-bnb-room-type" name="room_type">
|
||||
<option value=""><?php esc_html_e( 'All Types', 'wp-bnb' ); ?></option>
|
||||
<?php foreach ( $form_data['room_types'] as $type ) : ?>
|
||||
<option value="<?php echo esc_attr( $type['slug'] ); ?>">
|
||||
<?php echo esc_html( $type['name'] ); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ( 'yes' === $atts['show_building'] && ! empty( $form_data['buildings'] ) ) : ?>
|
||||
<div class="wp-bnb-field wp-bnb-field-building">
|
||||
<label for="wp-bnb-building"><?php esc_html_e( 'Building', 'wp-bnb' ); ?></label>
|
||||
<select id="wp-bnb-building" name="building_id">
|
||||
<option value=""><?php esc_html_e( 'All Buildings', 'wp-bnb' ); ?></option>
|
||||
<?php foreach ( $form_data['buildings'] as $building ) : ?>
|
||||
<option value="<?php echo esc_attr( $building['id'] ); ?>">
|
||||
<?php echo esc_html( $building['title'] ); ?>
|
||||
<?php if ( ! empty( $building['city'] ) ) : ?>
|
||||
(<?php echo esc_html( $building['city'] ); ?>)
|
||||
<?php endif; ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ( 'yes' === $atts['show_price_range'] && $form_data['price_range']['max'] > 0 ) : ?>
|
||||
<div class="wp-bnb-field wp-bnb-field-price-range">
|
||||
<label><?php esc_html_e( 'Price Range', 'wp-bnb' ); ?></label>
|
||||
<div class="wp-bnb-price-range-inputs">
|
||||
<input type="number" id="wp-bnb-price-min" name="price_min" placeholder="<?php esc_attr_e( 'Min', 'wp-bnb' ); ?>" min="0" step="10">
|
||||
<span class="wp-bnb-price-separator">-</span>
|
||||
<input type="number" id="wp-bnb-price-max" name="price_max" placeholder="<?php esc_attr_e( 'Max', 'wp-bnb' ); ?>" min="0" step="10">
|
||||
<span class="wp-bnb-currency"><?php echo esc_html( $form_data['currency'] ); ?></span>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php if ( 'yes' === $atts['show_amenities'] && ! empty( $form_data['amenities'] ) ) : ?>
|
||||
<div class="wp-bnb-search-amenities">
|
||||
<label><?php esc_html_e( 'Amenities', 'wp-bnb' ); ?></label>
|
||||
<div class="wp-bnb-amenities-list">
|
||||
<?php foreach ( $form_data['amenities'] as $amenity ) : ?>
|
||||
<label class="wp-bnb-amenity-checkbox">
|
||||
<input type="checkbox" name="amenities[]" value="<?php echo esc_attr( $amenity['slug'] ); ?>">
|
||||
<?php if ( ! empty( $amenity['icon'] ) ) : ?>
|
||||
<span class="dashicons dashicons-<?php echo esc_attr( $amenity['icon'] ); ?>"></span>
|
||||
<?php endif; ?>
|
||||
<span><?php echo esc_html( $amenity['name'] ); ?></span>
|
||||
</label>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="wp-bnb-search-actions">
|
||||
<button type="submit" class="wp-bnb-button wp-bnb-button-primary">
|
||||
<span class="dashicons dashicons-search"></span>
|
||||
<?php esc_html_e( 'Search Rooms', 'wp-bnb' ); ?>
|
||||
</button>
|
||||
<button type="reset" class="wp-bnb-button wp-bnb-button-secondary">
|
||||
<?php esc_html_e( 'Clear', 'wp-bnb' ); ?>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="wp-bnb-search-results-container">
|
||||
<div class="wp-bnb-search-status">
|
||||
<span class="wp-bnb-results-count"></span>
|
||||
<div class="wp-bnb-sort-options">
|
||||
<label for="wp-bnb-sort"><?php esc_html_e( 'Sort by:', 'wp-bnb' ); ?></label>
|
||||
<select id="wp-bnb-sort" name="orderby">
|
||||
<option value="title"><?php esc_html_e( 'Name', 'wp-bnb' ); ?></option>
|
||||
<option value="price"><?php esc_html_e( 'Price', 'wp-bnb' ); ?></option>
|
||||
<option value="capacity"><?php esc_html_e( 'Capacity', 'wp-bnb' ); ?></option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wp-bnb-search-results wp-bnb-rooms wp-bnb-rooms-<?php echo esc_attr( $layout ); ?> wp-bnb-columns-<?php echo esc_attr( $columns ); ?>">
|
||||
<div class="wp-bnb-loading">
|
||||
<span class="wp-bnb-spinner"></span>
|
||||
<span><?php esc_html_e( 'Loading rooms...', 'wp-bnb' ); ?></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wp-bnb-search-pagination">
|
||||
<button type="button" class="wp-bnb-button wp-bnb-load-more" style="display:none;">
|
||||
<?php esc_html_e( 'Load More', 'wp-bnb' ); ?>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
return ob_get_clean();
|
||||
}
|
||||
|
||||
/**
|
||||
* Render single building shortcode.
|
||||
*
|
||||
* @param array $atts Shortcode attributes.
|
||||
* @return string HTML output.
|
||||
*/
|
||||
public static function render_single_building( $atts ): string {
|
||||
$atts = shortcode_atts(
|
||||
array(
|
||||
'id' => 0,
|
||||
'show_rooms' => 'yes',
|
||||
'show_address' => 'yes',
|
||||
'show_contact' => 'yes',
|
||||
),
|
||||
$atts,
|
||||
'bnb_building'
|
||||
);
|
||||
|
||||
$building_id = (int) $atts['id'];
|
||||
|
||||
if ( ! $building_id ) {
|
||||
return '<p class="wp-bnb-error">' . esc_html__( 'Building ID is required.', 'wp-bnb' ) . '</p>';
|
||||
}
|
||||
|
||||
$building = get_post( $building_id );
|
||||
|
||||
if ( ! $building || Building::POST_TYPE !== $building->post_type ) {
|
||||
return '<p class="wp-bnb-error">' . esc_html__( 'Building not found.', 'wp-bnb' ) . '</p>';
|
||||
}
|
||||
|
||||
$show_rooms = 'yes' === $atts['show_rooms'];
|
||||
$show_address = 'yes' === $atts['show_address'];
|
||||
$show_contact = 'yes' === $atts['show_contact'];
|
||||
|
||||
ob_start();
|
||||
?>
|
||||
<div class="wp-bnb-building-single">
|
||||
<?php if ( has_post_thumbnail( $building->ID ) ) : ?>
|
||||
<div class="wp-bnb-building-featured-image">
|
||||
<?php echo get_the_post_thumbnail( $building->ID, 'large' ); ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="wp-bnb-building-header">
|
||||
<h2 class="wp-bnb-building-title"><?php echo esc_html( $building->post_title ); ?></h2>
|
||||
</div>
|
||||
|
||||
<div class="wp-bnb-building-details">
|
||||
<?php if ( $show_address ) : ?>
|
||||
<?php $address = Building::get_formatted_address( $building->ID ); ?>
|
||||
<?php if ( ! empty( $address ) ) : ?>
|
||||
<div class="wp-bnb-building-address">
|
||||
<h4><?php esc_html_e( 'Address', 'wp-bnb' ); ?></h4>
|
||||
<address><?php echo nl2br( esc_html( $address ) ); ?></address>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ( $show_contact ) : ?>
|
||||
<?php
|
||||
$phone = get_post_meta( $building->ID, '_bnb_building_phone', true );
|
||||
$email = get_post_meta( $building->ID, '_bnb_building_email', true );
|
||||
$website = get_post_meta( $building->ID, '_bnb_building_website', true );
|
||||
?>
|
||||
<?php if ( $phone || $email || $website ) : ?>
|
||||
<div class="wp-bnb-building-contact">
|
||||
<h4><?php esc_html_e( 'Contact', 'wp-bnb' ); ?></h4>
|
||||
<?php if ( $phone ) : ?>
|
||||
<p class="wp-bnb-contact-phone">
|
||||
<span class="dashicons dashicons-phone"></span>
|
||||
<a href="tel:<?php echo esc_attr( $phone ); ?>"><?php echo esc_html( $phone ); ?></a>
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
<?php if ( $email ) : ?>
|
||||
<p class="wp-bnb-contact-email">
|
||||
<span class="dashicons dashicons-email"></span>
|
||||
<a href="mailto:<?php echo esc_attr( $email ); ?>"><?php echo esc_html( $email ); ?></a>
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
<?php if ( $website ) : ?>
|
||||
<p class="wp-bnb-contact-website">
|
||||
<span class="dashicons dashicons-admin-site"></span>
|
||||
<a href="<?php echo esc_url( $website ); ?>" target="_blank" rel="noopener"><?php echo esc_html( $website ); ?></a>
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php
|
||||
$check_in_time = get_post_meta( $building->ID, '_bnb_building_check_in_time', true );
|
||||
$check_out_time = get_post_meta( $building->ID, '_bnb_building_check_out_time', true );
|
||||
if ( $check_in_time || $check_out_time ) :
|
||||
?>
|
||||
<div class="wp-bnb-building-times">
|
||||
<h4><?php esc_html_e( 'Check-in / Check-out', 'wp-bnb' ); ?></h4>
|
||||
<?php if ( $check_in_time ) : ?>
|
||||
<p><strong><?php esc_html_e( 'Check-in:', 'wp-bnb' ); ?></strong> <?php echo esc_html( $check_in_time ); ?></p>
|
||||
<?php endif; ?>
|
||||
<?php if ( $check_out_time ) : ?>
|
||||
<p><strong><?php esc_html_e( 'Check-out:', 'wp-bnb' ); ?></strong> <?php echo esc_html( $check_out_time ); ?></p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php if ( ! empty( $building->post_content ) ) : ?>
|
||||
<div class="wp-bnb-building-description">
|
||||
<?php echo wp_kses_post( apply_filters( 'the_content', $building->post_content ) ); ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ( $show_rooms ) : ?>
|
||||
<?php $rooms = Room::get_rooms_for_building( $building->ID ); ?>
|
||||
<?php if ( ! empty( $rooms ) ) : ?>
|
||||
<div class="wp-bnb-building-rooms">
|
||||
<h3><?php esc_html_e( 'Available Rooms', 'wp-bnb' ); ?></h3>
|
||||
<?php
|
||||
echo self::render_rooms(
|
||||
array(
|
||||
'building_id' => $building->ID,
|
||||
'show_building' => 'no',
|
||||
'limit' => -1,
|
||||
)
|
||||
);
|
||||
?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php
|
||||
return ob_get_clean();
|
||||
}
|
||||
|
||||
/**
|
||||
* Render single room shortcode.
|
||||
*
|
||||
* @param array $atts Shortcode attributes.
|
||||
* @return string HTML output.
|
||||
*/
|
||||
public static function render_single_room( $atts ): string {
|
||||
$atts = shortcode_atts(
|
||||
array(
|
||||
'id' => 0,
|
||||
'show_gallery' => 'yes',
|
||||
'show_pricing' => 'yes',
|
||||
'show_amenities' => 'yes',
|
||||
'show_availability' => 'yes',
|
||||
),
|
||||
$atts,
|
||||
'bnb_room'
|
||||
);
|
||||
|
||||
$room_id = (int) $atts['id'];
|
||||
|
||||
if ( ! $room_id ) {
|
||||
return '<p class="wp-bnb-error">' . esc_html__( 'Room ID is required.', 'wp-bnb' ) . '</p>';
|
||||
}
|
||||
|
||||
$room = get_post( $room_id );
|
||||
|
||||
if ( ! $room || Room::POST_TYPE !== $room->post_type ) {
|
||||
return '<p class="wp-bnb-error">' . esc_html__( 'Room not found.', 'wp-bnb' ) . '</p>';
|
||||
}
|
||||
|
||||
$show_gallery = 'yes' === $atts['show_gallery'];
|
||||
$show_pricing = 'yes' === $atts['show_pricing'];
|
||||
$show_amenities = 'yes' === $atts['show_amenities'];
|
||||
$show_availability = 'yes' === $atts['show_availability'];
|
||||
|
||||
// Get room data.
|
||||
$room_data = Search::get_room_data( $room );
|
||||
|
||||
ob_start();
|
||||
?>
|
||||
<div class="wp-bnb-room-single" data-room-id="<?php echo esc_attr( $room->ID ); ?>">
|
||||
<?php if ( $show_gallery && ( has_post_thumbnail( $room->ID ) || ! empty( $room_data['gallery'] ) ) ) : ?>
|
||||
<div class="wp-bnb-room-gallery">
|
||||
<?php if ( has_post_thumbnail( $room->ID ) ) : ?>
|
||||
<div class="wp-bnb-room-featured-image">
|
||||
<?php echo get_the_post_thumbnail( $room->ID, 'large' ); ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ( ! empty( $room_data['gallery'] ) ) : ?>
|
||||
<div class="wp-bnb-room-gallery-thumbnails">
|
||||
<?php foreach ( $room_data['gallery'] as $image ) : ?>
|
||||
<a href="<?php echo esc_url( $image['url'] ); ?>" class="wp-bnb-gallery-thumb" data-gallery>
|
||||
<img src="<?php echo esc_url( $image['thumb'] ); ?>" alt="">
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="wp-bnb-room-header">
|
||||
<div class="wp-bnb-room-header-content">
|
||||
<h2 class="wp-bnb-room-title"><?php echo esc_html( $room->post_title ); ?></h2>
|
||||
|
||||
<?php if ( ! empty( $room_data['building'] ) ) : ?>
|
||||
<p class="wp-bnb-room-building">
|
||||
<span class="dashicons dashicons-building"></span>
|
||||
<a href="<?php echo esc_url( $room_data['building']['permalink'] ); ?>">
|
||||
<?php echo esc_html( $room_data['building']['title'] ); ?>
|
||||
</a>
|
||||
<?php if ( ! empty( $room_data['building']['city'] ) ) : ?>
|
||||
<span>, <?php echo esc_html( $room_data['building']['city'] ); ?></span>
|
||||
<?php endif; ?>
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ( ! empty( $room_data['room_types'] ) ) : ?>
|
||||
<span class="wp-bnb-room-type"><?php echo esc_html( implode( ', ', $room_data['room_types'] ) ); ?></span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php if ( $show_pricing && ! empty( $room_data['price_formatted'] ) ) : ?>
|
||||
<div class="wp-bnb-room-header-price">
|
||||
<span class="wp-bnb-price-label"><?php esc_html_e( 'From', 'wp-bnb' ); ?></span>
|
||||
<span class="wp-bnb-price-amount"><?php echo esc_html( $room_data['price_formatted'] ); ?></span>
|
||||
<span class="wp-bnb-price-unit"><?php esc_html_e( '/night', 'wp-bnb' ); ?></span>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="wp-bnb-room-info">
|
||||
<div class="wp-bnb-room-specs">
|
||||
<?php if ( ! empty( $room_data['capacity'] ) ) : ?>
|
||||
<div class="wp-bnb-spec">
|
||||
<span class="dashicons dashicons-groups"></span>
|
||||
<span class="wp-bnb-spec-label"><?php esc_html_e( 'Capacity', 'wp-bnb' ); ?></span>
|
||||
<span class="wp-bnb-spec-value">
|
||||
<?php
|
||||
printf(
|
||||
/* translators: %d: Number of guests */
|
||||
esc_html( _n( '%d guest', '%d guests', $room_data['capacity'], 'wp-bnb' ) ),
|
||||
(int) $room_data['capacity']
|
||||
);
|
||||
?>
|
||||
</span>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ( ! empty( $room_data['size'] ) ) : ?>
|
||||
<div class="wp-bnb-spec">
|
||||
<span class="dashicons dashicons-editor-expand"></span>
|
||||
<span class="wp-bnb-spec-label"><?php esc_html_e( 'Size', 'wp-bnb' ); ?></span>
|
||||
<span class="wp-bnb-spec-value"><?php echo esc_html( $room_data['size'] ); ?> m²</span>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ( ! empty( $room_data['beds'] ) ) : ?>
|
||||
<div class="wp-bnb-spec">
|
||||
<span class="dashicons dashicons-admin-home"></span>
|
||||
<span class="wp-bnb-spec-label"><?php esc_html_e( 'Beds', 'wp-bnb' ); ?></span>
|
||||
<span class="wp-bnb-spec-value"><?php echo esc_html( $room_data['beds'] ); ?></span>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ( ! empty( $room_data['bathrooms'] ) ) : ?>
|
||||
<div class="wp-bnb-spec">
|
||||
<span class="dashicons dashicons-admin-page"></span>
|
||||
<span class="wp-bnb-spec-label"><?php esc_html_e( 'Bathrooms', 'wp-bnb' ); ?></span>
|
||||
<span class="wp-bnb-spec-value"><?php echo esc_html( $room_data['bathrooms'] ); ?></span>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ( ! empty( $room_data['floor'] ) ) : ?>
|
||||
<div class="wp-bnb-spec">
|
||||
<span class="dashicons dashicons-building"></span>
|
||||
<span class="wp-bnb-spec-label"><?php esc_html_e( 'Floor', 'wp-bnb' ); ?></span>
|
||||
<span class="wp-bnb-spec-value"><?php echo esc_html( $room_data['floor'] ); ?></span>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php if ( $show_amenities && ! empty( $room_data['amenities'] ) ) : ?>
|
||||
<div class="wp-bnb-room-amenities-full">
|
||||
<h4><?php esc_html_e( 'Amenities', 'wp-bnb' ); ?></h4>
|
||||
<ul class="wp-bnb-amenities-list">
|
||||
<?php foreach ( $room_data['amenities'] as $amenity ) : ?>
|
||||
<li class="wp-bnb-amenity">
|
||||
<?php if ( ! empty( $amenity['icon'] ) ) : ?>
|
||||
<span class="dashicons dashicons-<?php echo esc_attr( $amenity['icon'] ); ?>"></span>
|
||||
<?php endif; ?>
|
||||
<span><?php echo esc_html( $amenity['name'] ); ?></span>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php if ( ! empty( $room->post_content ) ) : ?>
|
||||
<div class="wp-bnb-room-description">
|
||||
<?php echo wp_kses_post( apply_filters( 'the_content', $room->post_content ) ); ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ( $show_pricing ) : ?>
|
||||
<?php $pricing = Calculator::getRoomPricing( $room->ID ); ?>
|
||||
<div class="wp-bnb-room-pricing-details">
|
||||
<h4><?php esc_html_e( 'Pricing', 'wp-bnb' ); ?></h4>
|
||||
<table class="wp-bnb-pricing-table">
|
||||
<tbody>
|
||||
<?php foreach ( PricingTier::cases() as $tier ) : ?>
|
||||
<?php $price = $pricing[ $tier->value ]['price'] ?? null; ?>
|
||||
<?php if ( $price ) : ?>
|
||||
<tr>
|
||||
<td class="wp-bnb-tier-label"><?php echo esc_html( $tier->label() ); ?></td>
|
||||
<td class="wp-bnb-tier-price">
|
||||
<?php echo esc_html( Calculator::formatPrice( $price ) ); ?>
|
||||
<span class="wp-bnb-tier-unit"><?php echo esc_html( $tier->unit() ); ?></span>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ( $show_availability ) : ?>
|
||||
<div class="wp-bnb-room-availability">
|
||||
<h4><?php esc_html_e( 'Check Availability', 'wp-bnb' ); ?></h4>
|
||||
<form class="wp-bnb-availability-form" data-room-id="<?php echo esc_attr( $room->ID ); ?>">
|
||||
<div class="wp-bnb-availability-fields">
|
||||
<div class="wp-bnb-field-group">
|
||||
<label for="wp-bnb-avail-check-in"><?php esc_html_e( 'Check-in', 'wp-bnb' ); ?></label>
|
||||
<input type="date" id="wp-bnb-avail-check-in" name="check_in" min="<?php echo esc_attr( gmdate( 'Y-m-d' ) ); ?>" required>
|
||||
</div>
|
||||
<div class="wp-bnb-field-group">
|
||||
<label for="wp-bnb-avail-check-out"><?php esc_html_e( 'Check-out', 'wp-bnb' ); ?></label>
|
||||
<input type="date" id="wp-bnb-avail-check-out" name="check_out" min="<?php echo esc_attr( gmdate( 'Y-m-d', strtotime( '+1 day' ) ) ); ?>" required>
|
||||
</div>
|
||||
<button type="submit" class="wp-bnb-button wp-bnb-button-primary">
|
||||
<?php esc_html_e( 'Check', 'wp-bnb' ); ?>
|
||||
</button>
|
||||
</div>
|
||||
<div class="wp-bnb-availability-result" style="display:none;"></div>
|
||||
</form>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php
|
||||
return ob_get_clean();
|
||||
}
|
||||
}
|
||||
298
src/Frontend/Widgets/AvailabilityCalendar.php
Normal file
298
src/Frontend/Widgets/AvailabilityCalendar.php
Normal file
@@ -0,0 +1,298 @@
|
||||
<?php
|
||||
/**
|
||||
* Availability Calendar widget.
|
||||
*
|
||||
* Displays a mini calendar showing room availability.
|
||||
*
|
||||
* @package Magdev\WpBnb\Frontend\Widgets
|
||||
*/
|
||||
|
||||
declare( strict_types=1 );
|
||||
|
||||
namespace Magdev\WpBnb\Frontend\Widgets;
|
||||
|
||||
use Magdev\WpBnb\Booking\Availability;
|
||||
use Magdev\WpBnb\PostTypes\Room;
|
||||
|
||||
/**
|
||||
* Availability Calendar widget class.
|
||||
*/
|
||||
class AvailabilityCalendar extends \WP_Widget {
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
parent::__construct(
|
||||
'wp_bnb_availability_calendar',
|
||||
__( 'WP BnB: Availability Calendar', 'wp-bnb' ),
|
||||
array(
|
||||
'classname' => 'wp-bnb-widget-availability-calendar',
|
||||
'description' => __( 'Display a room availability calendar.', 'wp-bnb' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Output the widget content.
|
||||
*
|
||||
* @param array $args Widget arguments.
|
||||
* @param array $instance Widget instance settings.
|
||||
* @return void
|
||||
*/
|
||||
public function widget( $args, $instance ): void {
|
||||
$title = ! empty( $instance['title'] ) ? $instance['title'] : __( 'Availability', 'wp-bnb' );
|
||||
$room_id = ! empty( $instance['room_id'] ) ? (int) $instance['room_id'] : 0;
|
||||
$months_to_show = ! empty( $instance['months'] ) ? (int) $instance['months'] : 1;
|
||||
$show_legend = ! empty( $instance['show_legend'] );
|
||||
$show_navigation = ! empty( $instance['show_navigation'] );
|
||||
|
||||
// Auto-detect room from single room page.
|
||||
if ( ! $room_id && is_singular( Room::POST_TYPE ) ) {
|
||||
$room_id = get_the_ID();
|
||||
}
|
||||
|
||||
if ( ! $room_id ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$room = get_post( $room_id );
|
||||
if ( ! $room || Room::POST_TYPE !== $room->post_type ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Limit months to show.
|
||||
$months_to_show = max( 1, min( 3, $months_to_show ) );
|
||||
|
||||
// Get current month or from request.
|
||||
$year = (int) gmdate( 'Y' );
|
||||
$month = (int) gmdate( 'n' );
|
||||
|
||||
echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
|
||||
if ( $title ) {
|
||||
echo $args['before_title'] . esc_html( apply_filters( 'widget_title', $title ) ) . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
}
|
||||
?>
|
||||
|
||||
<div class="wp-bnb-availability-calendar-widget" data-room-id="<?php echo esc_attr( $room_id ); ?>">
|
||||
<?php for ( $i = 0; $i < $months_to_show; $i++ ) : ?>
|
||||
<?php
|
||||
$display_year = $year;
|
||||
$display_month = $month + $i;
|
||||
|
||||
if ( $display_month > 12 ) {
|
||||
$display_month -= 12;
|
||||
$display_year++;
|
||||
}
|
||||
|
||||
$calendar = Availability::get_calendar_data( $room_id, $display_year, $display_month );
|
||||
?>
|
||||
|
||||
<div class="wp-bnb-calendar-month" data-year="<?php echo esc_attr( $display_year ); ?>" data-month="<?php echo esc_attr( $display_month ); ?>">
|
||||
<div class="wp-bnb-calendar-header">
|
||||
<?php if ( $show_navigation && 0 === $i ) : ?>
|
||||
<button type="button" class="wp-bnb-calendar-nav wp-bnb-calendar-prev" data-direction="prev" aria-label="<?php esc_attr_e( 'Previous month', 'wp-bnb' ); ?>">
|
||||
‹
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
|
||||
<span class="wp-bnb-calendar-month-name">
|
||||
<?php echo esc_html( $calendar['month_name'] . ' ' . $display_year ); ?>
|
||||
</span>
|
||||
|
||||
<?php if ( $show_navigation && $i === $months_to_show - 1 ) : ?>
|
||||
<button type="button" class="wp-bnb-calendar-nav wp-bnb-calendar-next" data-direction="next" aria-label="<?php esc_attr_e( 'Next month', 'wp-bnb' ); ?>">
|
||||
›
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<table class="wp-bnb-calendar-grid">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?php esc_html_e( 'Su', 'wp-bnb' ); ?></th>
|
||||
<th><?php esc_html_e( 'Mo', 'wp-bnb' ); ?></th>
|
||||
<th><?php esc_html_e( 'Tu', 'wp-bnb' ); ?></th>
|
||||
<th><?php esc_html_e( 'We', 'wp-bnb' ); ?></th>
|
||||
<th><?php esc_html_e( 'Th', 'wp-bnb' ); ?></th>
|
||||
<th><?php esc_html_e( 'Fr', 'wp-bnb' ); ?></th>
|
||||
<th><?php esc_html_e( 'Sa', 'wp-bnb' ); ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php
|
||||
$day = 1;
|
||||
$total_days = $calendar['days_in_month'];
|
||||
$first_day = $calendar['first_day_of_week']; // 0 = Sunday.
|
||||
|
||||
// Calculate weeks.
|
||||
$weeks = ceil( ( $first_day + $total_days ) / 7 );
|
||||
|
||||
for ( $week = 0; $week < $weeks; $week++ ) :
|
||||
?>
|
||||
<tr>
|
||||
<?php for ( $dow = 0; $dow < 7; $dow++ ) : ?>
|
||||
<?php
|
||||
$cell_index = $week * 7 + $dow;
|
||||
|
||||
if ( $cell_index < $first_day || $day > $total_days ) {
|
||||
echo '<td class="wp-bnb-calendar-empty"></td>';
|
||||
} else {
|
||||
$day_data = $calendar['days'][ $day ] ?? null;
|
||||
$classes = array( 'wp-bnb-calendar-day' );
|
||||
|
||||
if ( $day_data ) {
|
||||
if ( $day_data['is_booked'] ) {
|
||||
$classes[] = 'wp-bnb-booked';
|
||||
} else {
|
||||
$classes[] = 'wp-bnb-available';
|
||||
}
|
||||
|
||||
if ( $day_data['is_past'] ) {
|
||||
$classes[] = 'wp-bnb-past';
|
||||
}
|
||||
|
||||
if ( $day_data['is_today'] ) {
|
||||
$classes[] = 'wp-bnb-today';
|
||||
}
|
||||
}
|
||||
?>
|
||||
<td class="<?php echo esc_attr( implode( ' ', $classes ) ); ?>" data-date="<?php echo esc_attr( $day_data['date'] ?? '' ); ?>">
|
||||
<?php echo esc_html( $day ); ?>
|
||||
</td>
|
||||
<?php
|
||||
$day++;
|
||||
}
|
||||
?>
|
||||
<?php endfor; ?>
|
||||
</tr>
|
||||
<?php endfor; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php endfor; ?>
|
||||
|
||||
<?php if ( $show_legend ) : ?>
|
||||
<div class="wp-bnb-calendar-legend">
|
||||
<span class="wp-bnb-legend-item wp-bnb-legend-available">
|
||||
<span class="wp-bnb-legend-color"></span>
|
||||
<?php esc_html_e( 'Available', 'wp-bnb' ); ?>
|
||||
</span>
|
||||
<span class="wp-bnb-legend-item wp-bnb-legend-booked">
|
||||
<span class="wp-bnb-legend-color"></span>
|
||||
<?php esc_html_e( 'Booked', 'wp-bnb' ); ?>
|
||||
</span>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
}
|
||||
|
||||
/**
|
||||
* Output the widget settings form.
|
||||
*
|
||||
* @param array $instance Current widget instance settings.
|
||||
* @return void
|
||||
*/
|
||||
public function form( $instance ): void {
|
||||
$title = ! empty( $instance['title'] ) ? $instance['title'] : __( 'Availability', 'wp-bnb' );
|
||||
$room_id = ! empty( $instance['room_id'] ) ? (int) $instance['room_id'] : 0;
|
||||
$months = ! empty( $instance['months'] ) ? (int) $instance['months'] : 1;
|
||||
$show_legend = ! empty( $instance['show_legend'] );
|
||||
$show_navigation = ! empty( $instance['show_navigation'] );
|
||||
|
||||
// Get all rooms.
|
||||
$rooms = get_posts(
|
||||
array(
|
||||
'post_type' => Room::POST_TYPE,
|
||||
'post_status' => 'publish',
|
||||
'posts_per_page' => -1,
|
||||
'orderby' => 'title',
|
||||
'order' => 'ASC',
|
||||
)
|
||||
);
|
||||
?>
|
||||
<p>
|
||||
<label for="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>">
|
||||
<?php esc_html_e( 'Title:', 'wp-bnb' ); ?>
|
||||
</label>
|
||||
<input class="widefat" id="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>"
|
||||
name="<?php echo esc_attr( $this->get_field_name( 'title' ) ); ?>"
|
||||
type="text" value="<?php echo esc_attr( $title ); ?>">
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label for="<?php echo esc_attr( $this->get_field_id( 'room_id' ) ); ?>">
|
||||
<?php esc_html_e( 'Room:', 'wp-bnb' ); ?>
|
||||
</label>
|
||||
<select class="widefat" id="<?php echo esc_attr( $this->get_field_id( 'room_id' ) ); ?>"
|
||||
name="<?php echo esc_attr( $this->get_field_name( 'room_id' ) ); ?>">
|
||||
<option value="0"><?php esc_html_e( '— Auto-detect from page —', 'wp-bnb' ); ?></option>
|
||||
<?php foreach ( $rooms as $room ) : ?>
|
||||
<?php
|
||||
$building_id = get_post_meta( $room->ID, '_bnb_room_building_id', true );
|
||||
$building = $building_id ? get_post( $building_id ) : null;
|
||||
?>
|
||||
<option value="<?php echo esc_attr( $room->ID ); ?>" <?php selected( $room_id, $room->ID ); ?>>
|
||||
<?php echo esc_html( $room->post_title ); ?>
|
||||
<?php if ( $building ) : ?>
|
||||
(<?php echo esc_html( $building->post_title ); ?>)
|
||||
<?php endif; ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<small><?php esc_html_e( 'Leave as auto-detect to show calendar of the current room page.', 'wp-bnb' ); ?></small>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label for="<?php echo esc_attr( $this->get_field_id( 'months' ) ); ?>">
|
||||
<?php esc_html_e( 'Months to show:', 'wp-bnb' ); ?>
|
||||
</label>
|
||||
<select class="widefat" id="<?php echo esc_attr( $this->get_field_id( 'months' ) ); ?>"
|
||||
name="<?php echo esc_attr( $this->get_field_name( 'months' ) ); ?>">
|
||||
<option value="1" <?php selected( $months, 1 ); ?>>1</option>
|
||||
<option value="2" <?php selected( $months, 2 ); ?>>2</option>
|
||||
<option value="3" <?php selected( $months, 3 ); ?>>3</option>
|
||||
</select>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<input class="checkbox" type="checkbox" id="<?php echo esc_attr( $this->get_field_id( 'show_legend' ) ); ?>"
|
||||
name="<?php echo esc_attr( $this->get_field_name( 'show_legend' ) ); ?>"
|
||||
<?php checked( $show_legend ); ?>>
|
||||
<label for="<?php echo esc_attr( $this->get_field_id( 'show_legend' ) ); ?>">
|
||||
<?php esc_html_e( 'Show legend', 'wp-bnb' ); ?>
|
||||
</label>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<input class="checkbox" type="checkbox" id="<?php echo esc_attr( $this->get_field_id( 'show_navigation' ) ); ?>"
|
||||
name="<?php echo esc_attr( $this->get_field_name( 'show_navigation' ) ); ?>"
|
||||
<?php checked( $show_navigation ); ?>>
|
||||
<label for="<?php echo esc_attr( $this->get_field_id( 'show_navigation' ) ); ?>">
|
||||
<?php esc_html_e( 'Allow navigation', 'wp-bnb' ); ?>
|
||||
</label>
|
||||
</p>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Update widget settings.
|
||||
*
|
||||
* @param array $new_instance New settings.
|
||||
* @param array $old_instance Old settings.
|
||||
* @return array Updated settings.
|
||||
*/
|
||||
public function update( $new_instance, $old_instance ): array {
|
||||
$instance = array();
|
||||
$instance['title'] = ! empty( $new_instance['title'] ) ? sanitize_text_field( $new_instance['title'] ) : '';
|
||||
$instance['room_id'] = ! empty( $new_instance['room_id'] ) ? absint( $new_instance['room_id'] ) : 0;
|
||||
$instance['months'] = ! empty( $new_instance['months'] ) ? absint( $new_instance['months'] ) : 1;
|
||||
$instance['show_legend'] = ! empty( $new_instance['show_legend'] );
|
||||
$instance['show_navigation'] = ! empty( $new_instance['show_navigation'] );
|
||||
return $instance;
|
||||
}
|
||||
}
|
||||
261
src/Frontend/Widgets/BuildingRooms.php
Normal file
261
src/Frontend/Widgets/BuildingRooms.php
Normal file
@@ -0,0 +1,261 @@
|
||||
<?php
|
||||
/**
|
||||
* Building Rooms widget.
|
||||
*
|
||||
* Displays all rooms in a specific building.
|
||||
*
|
||||
* @package Magdev\WpBnb\Frontend\Widgets
|
||||
*/
|
||||
|
||||
declare( strict_types=1 );
|
||||
|
||||
namespace Magdev\WpBnb\Frontend\Widgets;
|
||||
|
||||
use Magdev\WpBnb\Frontend\Search;
|
||||
use Magdev\WpBnb\PostTypes\Building;
|
||||
use Magdev\WpBnb\PostTypes\Room;
|
||||
|
||||
/**
|
||||
* Building Rooms widget class.
|
||||
*/
|
||||
class BuildingRooms extends \WP_Widget {
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
parent::__construct(
|
||||
'wp_bnb_building_rooms',
|
||||
__( 'WP BnB: Building Rooms', 'wp-bnb' ),
|
||||
array(
|
||||
'classname' => 'wp-bnb-widget-building-rooms',
|
||||
'description' => __( 'Display all rooms in a building.', 'wp-bnb' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Output the widget content.
|
||||
*
|
||||
* @param array $args Widget arguments.
|
||||
* @param array $instance Widget instance settings.
|
||||
* @return void
|
||||
*/
|
||||
public function widget( $args, $instance ): void {
|
||||
$title = ! empty( $instance['title'] ) ? $instance['title'] : __( 'Rooms', 'wp-bnb' );
|
||||
$building_id = ! empty( $instance['building_id'] ) ? (int) $instance['building_id'] : 0;
|
||||
$count = ! empty( $instance['count'] ) ? (int) $instance['count'] : -1;
|
||||
$show_availability = ! empty( $instance['show_availability'] );
|
||||
$show_price = ! empty( $instance['show_price'] );
|
||||
$layout = ! empty( $instance['layout'] ) ? $instance['layout'] : 'list';
|
||||
|
||||
// Auto-detect building from single building page.
|
||||
if ( ! $building_id && is_singular( Building::POST_TYPE ) ) {
|
||||
$building_id = get_the_ID();
|
||||
}
|
||||
|
||||
if ( ! $building_id ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get rooms for building.
|
||||
$search_args = array(
|
||||
'building_id' => $building_id,
|
||||
'limit' => $count,
|
||||
'orderby' => 'title',
|
||||
'order' => 'ASC',
|
||||
);
|
||||
|
||||
$rooms = Search::search( $search_args );
|
||||
|
||||
if ( empty( $rooms ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
|
||||
if ( $title ) {
|
||||
echo $args['before_title'] . esc_html( apply_filters( 'widget_title', $title ) ) . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
}
|
||||
|
||||
$list_class = 'compact' === $layout ? 'wp-bnb-building-rooms-compact' : 'wp-bnb-building-rooms-list';
|
||||
echo '<ul class="' . esc_attr( $list_class ) . '">';
|
||||
|
||||
foreach ( $rooms as $room ) {
|
||||
$status = get_post_meta( $room['id'], '_bnb_room_status', true ) ?: 'available';
|
||||
?>
|
||||
<li class="wp-bnb-building-room">
|
||||
<a href="<?php echo esc_url( $room['permalink'] ); ?>" class="wp-bnb-building-room-link">
|
||||
<span class="wp-bnb-building-room-title"><?php echo esc_html( $room['title'] ); ?></span>
|
||||
|
||||
<?php if ( ! empty( $room['room_number'] ) ) : ?>
|
||||
<span class="wp-bnb-building-room-number">#<?php echo esc_html( $room['room_number'] ); ?></span>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ( $show_availability ) : ?>
|
||||
<span class="wp-bnb-building-room-status wp-bnb-status-<?php echo esc_attr( $status ); ?>">
|
||||
<?php
|
||||
$statuses = Room::get_room_statuses();
|
||||
echo esc_html( $statuses[ $status ] ?? $status );
|
||||
?>
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ( $show_price && ! empty( $room['price_formatted'] ) ) : ?>
|
||||
<span class="wp-bnb-building-room-price">
|
||||
<?php echo esc_html( $room['price_formatted'] ); ?>
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
</a>
|
||||
|
||||
<?php if ( 'list' === $layout ) : ?>
|
||||
<div class="wp-bnb-building-room-meta">
|
||||
<?php if ( ! empty( $room['capacity'] ) ) : ?>
|
||||
<span class="wp-bnb-meta-item">
|
||||
<span class="dashicons dashicons-groups"></span>
|
||||
<?php echo esc_html( $room['capacity'] ); ?>
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ( ! empty( $room['room_types'] ) ) : ?>
|
||||
<span class="wp-bnb-meta-item">
|
||||
<?php echo esc_html( $room['room_types'][0] ); ?>
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</li>
|
||||
<?php
|
||||
}
|
||||
|
||||
echo '</ul>';
|
||||
|
||||
// Show view all link if there are more rooms.
|
||||
$building = get_post( $building_id );
|
||||
if ( $building && $count > 0 && count( $rooms ) >= $count ) {
|
||||
$all_rooms = Room::get_rooms_for_building( $building_id );
|
||||
if ( count( $all_rooms ) > $count ) {
|
||||
printf(
|
||||
'<a href="%s" class="wp-bnb-view-all-rooms">%s</a>',
|
||||
esc_url( get_permalink( $building_id ) ),
|
||||
esc_html__( 'View all rooms', 'wp-bnb' )
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
}
|
||||
|
||||
/**
|
||||
* Output the widget settings form.
|
||||
*
|
||||
* @param array $instance Current widget instance settings.
|
||||
* @return void
|
||||
*/
|
||||
public function form( $instance ): void {
|
||||
$title = ! empty( $instance['title'] ) ? $instance['title'] : __( 'Rooms', 'wp-bnb' );
|
||||
$building_id = ! empty( $instance['building_id'] ) ? (int) $instance['building_id'] : 0;
|
||||
$count = ! empty( $instance['count'] ) ? (int) $instance['count'] : -1;
|
||||
$show_availability = ! empty( $instance['show_availability'] );
|
||||
$show_price = ! empty( $instance['show_price'] );
|
||||
$layout = ! empty( $instance['layout'] ) ? $instance['layout'] : 'list';
|
||||
|
||||
// Get all buildings.
|
||||
$buildings = get_posts(
|
||||
array(
|
||||
'post_type' => Building::POST_TYPE,
|
||||
'post_status' => 'publish',
|
||||
'posts_per_page' => -1,
|
||||
'orderby' => 'title',
|
||||
'order' => 'ASC',
|
||||
)
|
||||
);
|
||||
?>
|
||||
<p>
|
||||
<label for="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>">
|
||||
<?php esc_html_e( 'Title:', 'wp-bnb' ); ?>
|
||||
</label>
|
||||
<input class="widefat" id="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>"
|
||||
name="<?php echo esc_attr( $this->get_field_name( 'title' ) ); ?>"
|
||||
type="text" value="<?php echo esc_attr( $title ); ?>">
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label for="<?php echo esc_attr( $this->get_field_id( 'building_id' ) ); ?>">
|
||||
<?php esc_html_e( 'Building:', 'wp-bnb' ); ?>
|
||||
</label>
|
||||
<select class="widefat" id="<?php echo esc_attr( $this->get_field_id( 'building_id' ) ); ?>"
|
||||
name="<?php echo esc_attr( $this->get_field_name( 'building_id' ) ); ?>">
|
||||
<option value="0"><?php esc_html_e( '— Auto-detect from page —', 'wp-bnb' ); ?></option>
|
||||
<?php foreach ( $buildings as $building ) : ?>
|
||||
<option value="<?php echo esc_attr( $building->ID ); ?>" <?php selected( $building_id, $building->ID ); ?>>
|
||||
<?php echo esc_html( $building->post_title ); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<small><?php esc_html_e( 'Leave as auto-detect to show rooms of the current building page.', 'wp-bnb' ); ?></small>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label for="<?php echo esc_attr( $this->get_field_id( 'count' ) ); ?>">
|
||||
<?php esc_html_e( 'Number of rooms:', 'wp-bnb' ); ?>
|
||||
</label>
|
||||
<input class="tiny-text" id="<?php echo esc_attr( $this->get_field_id( 'count' ) ); ?>"
|
||||
name="<?php echo esc_attr( $this->get_field_name( 'count' ) ); ?>"
|
||||
type="number" min="-1" max="50" value="<?php echo esc_attr( $count ); ?>">
|
||||
<small><?php esc_html_e( '-1 for all rooms', 'wp-bnb' ); ?></small>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label for="<?php echo esc_attr( $this->get_field_id( 'layout' ) ); ?>">
|
||||
<?php esc_html_e( 'Layout:', 'wp-bnb' ); ?>
|
||||
</label>
|
||||
<select class="widefat" id="<?php echo esc_attr( $this->get_field_id( 'layout' ) ); ?>"
|
||||
name="<?php echo esc_attr( $this->get_field_name( 'layout' ) ); ?>">
|
||||
<option value="list" <?php selected( $layout, 'list' ); ?>>
|
||||
<?php esc_html_e( 'List (with details)', 'wp-bnb' ); ?>
|
||||
</option>
|
||||
<option value="compact" <?php selected( $layout, 'compact' ); ?>>
|
||||
<?php esc_html_e( 'Compact', 'wp-bnb' ); ?>
|
||||
</option>
|
||||
</select>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<input class="checkbox" type="checkbox" id="<?php echo esc_attr( $this->get_field_id( 'show_availability' ) ); ?>"
|
||||
name="<?php echo esc_attr( $this->get_field_name( 'show_availability' ) ); ?>"
|
||||
<?php checked( $show_availability ); ?>>
|
||||
<label for="<?php echo esc_attr( $this->get_field_id( 'show_availability' ) ); ?>">
|
||||
<?php esc_html_e( 'Show availability status', 'wp-bnb' ); ?>
|
||||
</label>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<input class="checkbox" type="checkbox" id="<?php echo esc_attr( $this->get_field_id( 'show_price' ) ); ?>"
|
||||
name="<?php echo esc_attr( $this->get_field_name( 'show_price' ) ); ?>"
|
||||
<?php checked( $show_price ); ?>>
|
||||
<label for="<?php echo esc_attr( $this->get_field_id( 'show_price' ) ); ?>">
|
||||
<?php esc_html_e( 'Show price', 'wp-bnb' ); ?>
|
||||
</label>
|
||||
</p>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Update widget settings.
|
||||
*
|
||||
* @param array $new_instance New settings.
|
||||
* @param array $old_instance Old settings.
|
||||
* @return array Updated settings.
|
||||
*/
|
||||
public function update( $new_instance, $old_instance ): array {
|
||||
$instance = array();
|
||||
$instance['title'] = ! empty( $new_instance['title'] ) ? sanitize_text_field( $new_instance['title'] ) : '';
|
||||
$instance['building_id'] = ! empty( $new_instance['building_id'] ) ? absint( $new_instance['building_id'] ) : 0;
|
||||
$instance['count'] = isset( $new_instance['count'] ) ? (int) $new_instance['count'] : -1;
|
||||
$instance['show_availability'] = ! empty( $new_instance['show_availability'] );
|
||||
$instance['show_price'] = ! empty( $new_instance['show_price'] );
|
||||
$instance['layout'] = ! empty( $new_instance['layout'] ) ? sanitize_text_field( $new_instance['layout'] ) : 'list';
|
||||
return $instance;
|
||||
}
|
||||
}
|
||||
233
src/Frontend/Widgets/SimilarRooms.php
Normal file
233
src/Frontend/Widgets/SimilarRooms.php
Normal file
@@ -0,0 +1,233 @@
|
||||
<?php
|
||||
/**
|
||||
* Similar Rooms widget.
|
||||
*
|
||||
* Displays rooms similar to the current room based on building or room type.
|
||||
*
|
||||
* @package Magdev\WpBnb\Frontend\Widgets
|
||||
*/
|
||||
|
||||
declare( strict_types=1 );
|
||||
|
||||
namespace Magdev\WpBnb\Frontend\Widgets;
|
||||
|
||||
use Magdev\WpBnb\Frontend\Search;
|
||||
use Magdev\WpBnb\PostTypes\Room;
|
||||
use Magdev\WpBnb\Taxonomies\RoomType;
|
||||
|
||||
/**
|
||||
* Similar Rooms widget class.
|
||||
*/
|
||||
class SimilarRooms extends \WP_Widget {
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
parent::__construct(
|
||||
'wp_bnb_similar_rooms',
|
||||
__( 'WP BnB: Similar Rooms', 'wp-bnb' ),
|
||||
array(
|
||||
'classname' => 'wp-bnb-widget-similar-rooms',
|
||||
'description' => __( 'Display rooms similar to the current room.', 'wp-bnb' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Output the widget content.
|
||||
*
|
||||
* @param array $args Widget arguments.
|
||||
* @param array $instance Widget instance settings.
|
||||
* @return void
|
||||
*/
|
||||
public function widget( $args, $instance ): void {
|
||||
$title = ! empty( $instance['title'] ) ? $instance['title'] : __( 'Similar Rooms', 'wp-bnb' );
|
||||
$count = ! empty( $instance['count'] ) ? (int) $instance['count'] : 3;
|
||||
$match_by = ! empty( $instance['match_by'] ) ? $instance['match_by'] : 'building';
|
||||
$show_price = ! empty( $instance['show_price'] );
|
||||
$show_image = ! empty( $instance['show_image'] );
|
||||
|
||||
// Get current room.
|
||||
$current_room_id = 0;
|
||||
if ( is_singular( Room::POST_TYPE ) ) {
|
||||
$current_room_id = get_the_ID();
|
||||
}
|
||||
|
||||
if ( ! $current_room_id ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Build query based on match type.
|
||||
$search_args = array(
|
||||
'limit' => $count + 1, // Get extra in case current room is included.
|
||||
);
|
||||
|
||||
switch ( $match_by ) {
|
||||
case 'building':
|
||||
$building_id = get_post_meta( $current_room_id, '_bnb_room_building_id', true );
|
||||
if ( $building_id ) {
|
||||
$search_args['building_id'] = (int) $building_id;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'room_type':
|
||||
$terms = wp_get_post_terms( $current_room_id, RoomType::TAXONOMY, array( 'fields' => 'slugs' ) );
|
||||
if ( ! empty( $terms ) ) {
|
||||
$search_args['room_type'] = $terms[0];
|
||||
}
|
||||
break;
|
||||
|
||||
case 'amenities':
|
||||
$amenities = wp_get_post_terms( $current_room_id, 'bnb_amenity', array( 'fields' => 'slugs' ) );
|
||||
if ( ! empty( $amenities ) ) {
|
||||
$search_args['amenities'] = array_slice( $amenities, 0, 3 );
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
$rooms = Search::search( $search_args );
|
||||
|
||||
// Remove current room from results.
|
||||
$rooms = array_filter(
|
||||
$rooms,
|
||||
function ( $room ) use ( $current_room_id ) {
|
||||
return $room['id'] !== $current_room_id;
|
||||
}
|
||||
);
|
||||
|
||||
// Limit to requested count.
|
||||
$rooms = array_slice( $rooms, 0, $count );
|
||||
|
||||
if ( empty( $rooms ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
|
||||
if ( $title ) {
|
||||
echo $args['before_title'] . esc_html( apply_filters( 'widget_title', $title ) ) . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
}
|
||||
|
||||
echo '<ul class="wp-bnb-similar-rooms-list">';
|
||||
|
||||
foreach ( $rooms as $room ) {
|
||||
?>
|
||||
<li class="wp-bnb-similar-room">
|
||||
<?php if ( $show_image && ! empty( $room['thumbnail'] ) ) : ?>
|
||||
<div class="wp-bnb-similar-room-image">
|
||||
<a href="<?php echo esc_url( $room['permalink'] ); ?>">
|
||||
<img src="<?php echo esc_url( $room['thumbnail'] ); ?>" alt="<?php echo esc_attr( $room['title'] ); ?>">
|
||||
</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="wp-bnb-similar-room-content">
|
||||
<h4 class="wp-bnb-similar-room-title">
|
||||
<a href="<?php echo esc_url( $room['permalink'] ); ?>">
|
||||
<?php echo esc_html( $room['title'] ); ?>
|
||||
</a>
|
||||
</h4>
|
||||
|
||||
<?php if ( $show_price && ! empty( $room['price_formatted'] ) ) : ?>
|
||||
<span class="wp-bnb-similar-room-price">
|
||||
<?php echo esc_html( $room['price_formatted'] ); ?>
|
||||
<span class="wp-bnb-price-unit"><?php esc_html_e( '/night', 'wp-bnb' ); ?></span>
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</li>
|
||||
<?php
|
||||
}
|
||||
|
||||
echo '</ul>';
|
||||
|
||||
echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
}
|
||||
|
||||
/**
|
||||
* Output the widget settings form.
|
||||
*
|
||||
* @param array $instance Current widget instance settings.
|
||||
* @return void
|
||||
*/
|
||||
public function form( $instance ): void {
|
||||
$title = ! empty( $instance['title'] ) ? $instance['title'] : __( 'Similar Rooms', 'wp-bnb' );
|
||||
$count = ! empty( $instance['count'] ) ? (int) $instance['count'] : 3;
|
||||
$match_by = ! empty( $instance['match_by'] ) ? $instance['match_by'] : 'building';
|
||||
$show_price = ! empty( $instance['show_price'] );
|
||||
$show_image = ! empty( $instance['show_image'] );
|
||||
?>
|
||||
<p>
|
||||
<label for="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>">
|
||||
<?php esc_html_e( 'Title:', 'wp-bnb' ); ?>
|
||||
</label>
|
||||
<input class="widefat" id="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>"
|
||||
name="<?php echo esc_attr( $this->get_field_name( 'title' ) ); ?>"
|
||||
type="text" value="<?php echo esc_attr( $title ); ?>">
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label for="<?php echo esc_attr( $this->get_field_id( 'count' ) ); ?>">
|
||||
<?php esc_html_e( 'Number of rooms:', 'wp-bnb' ); ?>
|
||||
</label>
|
||||
<input class="tiny-text" id="<?php echo esc_attr( $this->get_field_id( 'count' ) ); ?>"
|
||||
name="<?php echo esc_attr( $this->get_field_name( 'count' ) ); ?>"
|
||||
type="number" min="1" max="10" value="<?php echo esc_attr( $count ); ?>">
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label for="<?php echo esc_attr( $this->get_field_id( 'match_by' ) ); ?>">
|
||||
<?php esc_html_e( 'Match by:', 'wp-bnb' ); ?>
|
||||
</label>
|
||||
<select class="widefat" id="<?php echo esc_attr( $this->get_field_id( 'match_by' ) ); ?>"
|
||||
name="<?php echo esc_attr( $this->get_field_name( 'match_by' ) ); ?>">
|
||||
<option value="building" <?php selected( $match_by, 'building' ); ?>>
|
||||
<?php esc_html_e( 'Same Building', 'wp-bnb' ); ?>
|
||||
</option>
|
||||
<option value="room_type" <?php selected( $match_by, 'room_type' ); ?>>
|
||||
<?php esc_html_e( 'Same Room Type', 'wp-bnb' ); ?>
|
||||
</option>
|
||||
<option value="amenities" <?php selected( $match_by, 'amenities' ); ?>>
|
||||
<?php esc_html_e( 'Similar Amenities', 'wp-bnb' ); ?>
|
||||
</option>
|
||||
</select>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<input class="checkbox" type="checkbox" id="<?php echo esc_attr( $this->get_field_id( 'show_image' ) ); ?>"
|
||||
name="<?php echo esc_attr( $this->get_field_name( 'show_image' ) ); ?>"
|
||||
<?php checked( $show_image ); ?>>
|
||||
<label for="<?php echo esc_attr( $this->get_field_id( 'show_image' ) ); ?>">
|
||||
<?php esc_html_e( 'Show image', 'wp-bnb' ); ?>
|
||||
</label>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<input class="checkbox" type="checkbox" id="<?php echo esc_attr( $this->get_field_id( 'show_price' ) ); ?>"
|
||||
name="<?php echo esc_attr( $this->get_field_name( 'show_price' ) ); ?>"
|
||||
<?php checked( $show_price ); ?>>
|
||||
<label for="<?php echo esc_attr( $this->get_field_id( 'show_price' ) ); ?>">
|
||||
<?php esc_html_e( 'Show price', 'wp-bnb' ); ?>
|
||||
</label>
|
||||
</p>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Update widget settings.
|
||||
*
|
||||
* @param array $new_instance New settings.
|
||||
* @param array $old_instance Old settings.
|
||||
* @return array Updated settings.
|
||||
*/
|
||||
public function update( $new_instance, $old_instance ): array {
|
||||
$instance = array();
|
||||
$instance['title'] = ! empty( $new_instance['title'] ) ? sanitize_text_field( $new_instance['title'] ) : '';
|
||||
$instance['count'] = ! empty( $new_instance['count'] ) ? absint( $new_instance['count'] ) : 3;
|
||||
$instance['match_by'] = ! empty( $new_instance['match_by'] ) ? sanitize_text_field( $new_instance['match_by'] ) : 'building';
|
||||
$instance['show_price'] = ! empty( $new_instance['show_price'] );
|
||||
$instance['show_image'] = ! empty( $new_instance['show_image'] );
|
||||
return $instance;
|
||||
}
|
||||
}
|
||||
@@ -11,14 +11,21 @@ namespace Magdev\WpBnb;
|
||||
|
||||
use Magdev\WpBnb\Admin\Calendar as CalendarAdmin;
|
||||
use Magdev\WpBnb\Admin\Seasons as SeasonsAdmin;
|
||||
use Magdev\WpBnb\Blocks\BlockRegistrar;
|
||||
use Magdev\WpBnb\Booking\Availability;
|
||||
use Magdev\WpBnb\Booking\EmailNotifier;
|
||||
use Magdev\WpBnb\Frontend\Search;
|
||||
use Magdev\WpBnb\Frontend\Shortcodes;
|
||||
use Magdev\WpBnb\Frontend\Widgets\AvailabilityCalendar;
|
||||
use Magdev\WpBnb\Frontend\Widgets\BuildingRooms;
|
||||
use Magdev\WpBnb\Frontend\Widgets\SimilarRooms;
|
||||
use Magdev\WpBnb\License\Manager as LicenseManager;
|
||||
use Magdev\WpBnb\PostTypes\Booking;
|
||||
use Magdev\WpBnb\PostTypes\Building;
|
||||
use Magdev\WpBnb\PostTypes\Guest;
|
||||
use Magdev\WpBnb\PostTypes\Room;
|
||||
use Magdev\WpBnb\PostTypes\Service;
|
||||
use Magdev\WpBnb\Pricing\Calculator;
|
||||
use Magdev\WpBnb\Privacy\Manager as PrivacyManager;
|
||||
use Magdev\WpBnb\Pricing\Season;
|
||||
use Magdev\WpBnb\Taxonomies\Amenity;
|
||||
@@ -165,7 +172,28 @@ final class Plugin {
|
||||
* @return void
|
||||
*/
|
||||
private function init_frontend(): void {
|
||||
// Frontend shortcodes, blocks, and widgets will be added here.
|
||||
// Initialize search (registers AJAX handlers).
|
||||
Search::init();
|
||||
|
||||
// Initialize shortcodes.
|
||||
Shortcodes::init();
|
||||
|
||||
// Initialize Gutenberg blocks.
|
||||
BlockRegistrar::init();
|
||||
|
||||
// Register widgets.
|
||||
add_action( 'widgets_init', array( $this, 'register_widgets' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register frontend widgets.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register_widgets(): void {
|
||||
register_widget( SimilarRooms::class );
|
||||
register_widget( BuildingRooms::class );
|
||||
register_widget( AvailabilityCalendar::class );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -282,6 +310,35 @@ final class Plugin {
|
||||
WP_BNB_VERSION,
|
||||
true
|
||||
);
|
||||
|
||||
wp_localize_script(
|
||||
'wp-bnb-frontend',
|
||||
'wpBnbFrontend',
|
||||
array(
|
||||
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
|
||||
'nonce' => wp_create_nonce( 'wp_bnb_frontend_nonce' ),
|
||||
'i18n' => array(
|
||||
'searching' => __( 'Searching...', 'wp-bnb' ),
|
||||
'noResults' => __( 'No rooms found matching your criteria.', 'wp-bnb' ),
|
||||
'resultsFound' => __( '%d rooms found', 'wp-bnb' ),
|
||||
'loadMore' => __( 'Load More', 'wp-bnb' ),
|
||||
'viewDetails' => __( 'View Details', 'wp-bnb' ),
|
||||
'perNight' => __( 'night', 'wp-bnb' ),
|
||||
'guests' => __( 'guests', 'wp-bnb' ),
|
||||
'invalidDateRange' => __( 'Check-out must be after check-in', 'wp-bnb' ),
|
||||
'selectDates' => __( 'Please select check-in and check-out dates.', 'wp-bnb' ),
|
||||
'available' => __( 'Room is available!', 'wp-bnb' ),
|
||||
'notAvailable' => __( 'Sorry, the room is not available for these dates.', 'wp-bnb' ),
|
||||
'totalPrice' => __( 'Total', 'wp-bnb' ),
|
||||
'bookNow' => __( 'Book Now', 'wp-bnb' ),
|
||||
'total' => __( 'Total', 'wp-bnb' ),
|
||||
'nights' => __( 'nights', 'wp-bnb' ),
|
||||
'basePrice' => __( 'Base', 'wp-bnb' ),
|
||||
'weekendSurcharge' => __( 'Weekend surcharge', 'wp-bnb' ),
|
||||
'season' => __( 'Season', 'wp-bnb' ),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Plugin Name: WP BnB Management
|
||||
* Plugin URI: https://src.bundespruefstelle.ch/magdev/wp-bnb
|
||||
* Description: A comprehensive Bed & Breakfast management system for WordPress. Manage buildings, rooms, bookings, and guests.
|
||||
* Version: 0.5.0
|
||||
* Version: 0.6.0
|
||||
* Requires at least: 6.0
|
||||
* Requires PHP: 8.3
|
||||
* Author: Marco Graetsch
|
||||
@@ -24,7 +24,7 @@ if ( ! defined( 'ABSPATH' ) ) {
|
||||
}
|
||||
|
||||
// Plugin version constant - MUST match Version in header above.
|
||||
define( 'WP_BNB_VERSION', '0.5.0' );
|
||||
define( 'WP_BNB_VERSION', '0.6.0' );
|
||||
|
||||
// Plugin path constants.
|
||||
define( 'WP_BNB_PATH', plugin_dir_path( __FILE__ ) );
|
||||
|
||||
Reference in New Issue
Block a user