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

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

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

677
src/Frontend/Search.php Normal file
View 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
View 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();
}
}

View 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' ); ?>">
&lsaquo;
</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' ); ?>">
&rsaquo;
</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;
}
}

View 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;
}
}

View 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;
}
}