Step 2.6: Implement AJAX property filtering with URL state management
This commit is contained in:
+1
-1
@@ -1 +1 @@
|
||||
(function(e){var a=e(".menu-toggle"),o=e(".mobile-navigation");a.length&&(a.on("click",function(){var n=e(this).attr("aria-expanded")==="true";e(this).attr("aria-expanded",!n),o.toggleClass("is-open"),n?e("body").removeClass("mobile-menu-open"):e("body").addClass("mobile-menu-open")}),e(document).on("keydown",function(n){n.key==="Escape"&&o.hasClass("is-open")&&(a.attr("aria-expanded","false"),o.removeClass("is-open"),e("body").removeClass("mobile-menu-open"))}),e(document).on("click",function(n){o.hasClass("is-open")&&!e(n.target).closest(".mobile-navigation").length&&!e(n.target).closest(".menu-toggle").length&&(a.attr("aria-expanded","false"),o.removeClass("is-open"),e("body").removeClass("mobile-menu-open"))}))})(jQuery);(function(e){e(function(){})})(jQuery);
|
||||
(function(r){var n=r(".menu-toggle"),t=r(".mobile-navigation");n.length&&(n.on("click",function(){var e=r(this).attr("aria-expanded")==="true";r(this).attr("aria-expanded",!e),t.toggleClass("is-open"),e?r("body").removeClass("mobile-menu-open"):r("body").addClass("mobile-menu-open")}),r(document).on("keydown",function(e){e.key==="Escape"&&t.hasClass("is-open")&&(n.attr("aria-expanded","false"),t.removeClass("is-open"),r("body").removeClass("mobile-menu-open"))}),r(document).on("click",function(e){t.hasClass("is-open")&&!r(e.target).closest(".mobile-navigation").length&&!r(e.target).closest(".menu-toggle").length&&(n.attr("aria-expanded","false"),t.removeClass("is-open"),r("body").removeClass("mobile-menu-open"))}))})(jQuery);(function(r){var n={$form:null,$results:null,$filters:null,isFirstLoad:!0,isLoading:!1,init:function(){this.$form=r(".filters-form"),this.$results=r("#property-results"),this.$filters=r("#property-filters"),!(!this.$form.length||!this.$results.length)&&(this.bindEvents(),this.initFromUrl())},bindEvents:function(){var t=this;this.$form.on("submit",function(e){e.preventDefault(),t.filterProperties(1)}),this.$form.find("select").on("change",function(){t.filterProperties(1)}),r(".filters-reset").on("click",function(e){e.preventDefault(),t.resetFilters()}),this.$results.on("click",".pagination a",function(e){e.preventDefault();var i=t.getPageFromUrl(r(this).attr("href"));t.filterProperties(i)}),r(window).on("popstate",function(e){e.originalEvent.state&&e.originalEvent.state.propertyFilters&&(t.setFormFromState(e.originalEvent.state.propertyFilters),t.filterProperties(e.originalEvent.state.page||1,!1))})},initFromUrl:function(){var t=new URLSearchParams(window.location.search),e=!1;if(this.$form.find("select").each(function(){var s=r(this).attr("name");t.has(s)&&(r(this).val(t.get(s)),e=!0)}),e){var i=this.getFormState();i.page=parseInt(t.get("paged"))||1,history.replaceState({propertyFilters:i,page:i.page},"",window.location.href)}},filterProperties:function(t,e){if(!this.isLoading){e=e!==!1,t=t||1;var i=this,s=this.getFormData();s.paged=t,this.isLoading=!0,this.$filters.addClass("is-loading"),this.isFirstLoad&&this.$results.html('<div class="property-results-loading"><div class="spinner"></div></div>'),r.ajax({url:homeprozAjax.ajaxUrl,type:"POST",data:{action:"homeproz_filter_properties",nonce:homeprozAjax.nonce,property_type:s.property_type,property_status:s.property_status,property_location:s.property_location,min_price:s.min_price,max_price:s.max_price,beds:s.beds,sort:s.sort,paged:t},success:function(o){o.success&&(i.$results.html(o.data.html),i.isFirstLoad=!1,e&&i.updateUrl(s,t),t>1&&r("html, body").animate({scrollTop:i.$filters.offset().top-100},300))},error:function(){i.$results.html('<div class="no-properties"><h3>Error</h3><p>Something went wrong. Please try again.</p></div>')},complete:function(){i.isLoading=!1,i.$filters.removeClass("is-loading")}})}},getFormData:function(){return{property_type:this.$form.find('[name="property_type"]').val()||"",property_status:this.$form.find('[name="property_status"]').val()||"",property_location:this.$form.find('[name="property_location"]').val()||"",min_price:this.$form.find('[name="min_price"]').val()||"",max_price:this.$form.find('[name="max_price"]').val()||"",beds:this.$form.find('[name="beds"]').val()||"",sort:this.$form.find('[name="sort"]').val()||"newest"}},getFormState:function(){return this.getFormData()},setFormFromState:function(t){for(var e in t)this.$form.find('[name="'+e+'"]').val(t[e])},updateUrl:function(t,e){var i=new URL(homeprozAjax.archiveUrl);for(var s in t)t[s]&&t[s]!=="newest"&&i.searchParams.set(s,t[s]);e>1&&i.searchParams.set("paged",e);var o={propertyFilters:t,page:e};history.pushState(o,"",i.toString())},getPageFromUrl:function(t){var e=t.match(/[?&]paged=(\d+)/);return e?parseInt(e[1]):1},resetFilters:function(){this.$form.find("select").val(""),this.$form.find('[name="sort"]').val("newest"),this.filterProperties(1)}};r(function(){n.init()})})(jQuery);(function(r){r(function(){})})(jQuery);
|
||||
|
||||
@@ -34,3 +34,6 @@ require_once HOMEPROZ_DIR . '/inc/custom-post-types.php';
|
||||
|
||||
// ACF field definitions
|
||||
require_once HOMEPROZ_DIR . '/inc/acf-fields.php';
|
||||
|
||||
// AJAX handlers
|
||||
require_once HOMEPROZ_DIR . '/inc/ajax-handlers.php';
|
||||
|
||||
@@ -0,0 +1,231 @@
|
||||
<?php
|
||||
/**
|
||||
* AJAX Handlers
|
||||
*
|
||||
* @package HomeProz
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle AJAX property filter requests
|
||||
*/
|
||||
function homeproz_ajax_filter_properties() {
|
||||
// Verify nonce
|
||||
if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'homeproz_filter_nonce')) {
|
||||
wp_send_json_error('Invalid nonce');
|
||||
}
|
||||
|
||||
// Get filter values
|
||||
$property_type = isset($_POST['property_type']) ? sanitize_text_field($_POST['property_type']) : '';
|
||||
$property_status = isset($_POST['property_status']) ? sanitize_text_field($_POST['property_status']) : '';
|
||||
$property_location = isset($_POST['property_location']) ? sanitize_text_field($_POST['property_location']) : '';
|
||||
$min_price = isset($_POST['min_price']) ? intval($_POST['min_price']) : '';
|
||||
$max_price = isset($_POST['max_price']) ? intval($_POST['max_price']) : '';
|
||||
$beds = isset($_POST['beds']) ? intval($_POST['beds']) : '';
|
||||
$sort = isset($_POST['sort']) ? sanitize_text_field($_POST['sort']) : 'newest';
|
||||
$paged = isset($_POST['paged']) ? intval($_POST['paged']) : 1;
|
||||
|
||||
// Build query args
|
||||
$args = array(
|
||||
'post_type' => 'property',
|
||||
'posts_per_page' => 12,
|
||||
'paged' => $paged,
|
||||
);
|
||||
|
||||
// Taxonomy filters
|
||||
$tax_query = array();
|
||||
|
||||
if ($property_type) {
|
||||
$tax_query[] = array(
|
||||
'taxonomy' => 'property_type',
|
||||
'field' => 'slug',
|
||||
'terms' => $property_type,
|
||||
);
|
||||
}
|
||||
|
||||
if ($property_status) {
|
||||
$tax_query[] = array(
|
||||
'taxonomy' => 'property_status',
|
||||
'field' => 'slug',
|
||||
'terms' => $property_status,
|
||||
);
|
||||
}
|
||||
|
||||
if ($property_location) {
|
||||
$tax_query[] = array(
|
||||
'taxonomy' => 'property_location',
|
||||
'field' => 'slug',
|
||||
'terms' => $property_location,
|
||||
);
|
||||
}
|
||||
|
||||
if (!empty($tax_query)) {
|
||||
$args['tax_query'] = $tax_query;
|
||||
if (count($tax_query) > 1) {
|
||||
$args['tax_query']['relation'] = 'AND';
|
||||
}
|
||||
}
|
||||
|
||||
// Meta query for price and bedrooms
|
||||
$meta_query = array();
|
||||
|
||||
if ($min_price) {
|
||||
$meta_query[] = array(
|
||||
'key' => 'property_price',
|
||||
'value' => $min_price,
|
||||
'type' => 'NUMERIC',
|
||||
'compare' => '>=',
|
||||
);
|
||||
}
|
||||
|
||||
if ($max_price) {
|
||||
$meta_query[] = array(
|
||||
'key' => 'property_price',
|
||||
'value' => $max_price,
|
||||
'type' => 'NUMERIC',
|
||||
'compare' => '<=',
|
||||
);
|
||||
}
|
||||
|
||||
if ($beds) {
|
||||
$meta_query[] = array(
|
||||
'key' => 'bedrooms',
|
||||
'value' => $beds,
|
||||
'type' => 'NUMERIC',
|
||||
'compare' => '>=',
|
||||
);
|
||||
}
|
||||
|
||||
if (!empty($meta_query)) {
|
||||
$args['meta_query'] = $meta_query;
|
||||
if (count($meta_query) > 1) {
|
||||
$args['meta_query']['relation'] = 'AND';
|
||||
}
|
||||
}
|
||||
|
||||
// Sorting
|
||||
switch ($sort) {
|
||||
case 'oldest':
|
||||
$args['orderby'] = 'date';
|
||||
$args['order'] = 'ASC';
|
||||
break;
|
||||
case 'price_low':
|
||||
$args['meta_key'] = 'property_price';
|
||||
$args['orderby'] = 'meta_value_num';
|
||||
$args['order'] = 'ASC';
|
||||
break;
|
||||
case 'price_high':
|
||||
$args['meta_key'] = 'property_price';
|
||||
$args['orderby'] = 'meta_value_num';
|
||||
$args['order'] = 'DESC';
|
||||
break;
|
||||
case 'newest':
|
||||
default:
|
||||
$args['orderby'] = 'date';
|
||||
$args['order'] = 'DESC';
|
||||
break;
|
||||
}
|
||||
|
||||
$properties = new WP_Query($args);
|
||||
|
||||
ob_start();
|
||||
|
||||
// Results Meta
|
||||
?>
|
||||
<div class="properties-meta">
|
||||
<p class="properties-count">
|
||||
<?php if ($properties->found_posts > 0) : ?>
|
||||
Showing <strong><?php echo esc_html($properties->found_posts); ?></strong>
|
||||
<?php echo $properties->found_posts === 1 ? 'property' : 'properties'; ?>
|
||||
<?php else : ?>
|
||||
No properties found
|
||||
<?php endif; ?>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<?php if ($properties->have_posts()) : ?>
|
||||
<div class="properties-grid">
|
||||
<?php
|
||||
while ($properties->have_posts()) :
|
||||
$properties->the_post();
|
||||
get_template_part('template-parts/property/property-card');
|
||||
endwhile;
|
||||
?>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
// Build pagination
|
||||
$big = 999999999;
|
||||
$base_url = get_post_type_archive_link('property');
|
||||
|
||||
// Build URL with current filters
|
||||
$filter_args = array();
|
||||
if ($property_type) $filter_args['property_type'] = $property_type;
|
||||
if ($property_status) $filter_args['property_status'] = $property_status;
|
||||
if ($property_location) $filter_args['property_location'] = $property_location;
|
||||
if ($min_price) $filter_args['min_price'] = $min_price;
|
||||
if ($max_price) $filter_args['max_price'] = $max_price;
|
||||
if ($beds) $filter_args['beds'] = $beds;
|
||||
if ($sort && $sort !== 'newest') $filter_args['sort'] = $sort;
|
||||
|
||||
$pagination = paginate_links(array(
|
||||
'base' => add_query_arg('paged', '%#%', $base_url),
|
||||
'format' => '',
|
||||
'current' => max(1, $paged),
|
||||
'total' => $properties->max_num_pages,
|
||||
'prev_text' => '← Previous',
|
||||
'next_text' => 'Next →',
|
||||
'type' => 'array',
|
||||
'add_args' => $filter_args,
|
||||
));
|
||||
|
||||
if ($pagination) :
|
||||
?>
|
||||
<nav class="pagination" aria-label="Property pagination">
|
||||
<div class="nav-links">
|
||||
<?php foreach ($pagination as $link) : ?>
|
||||
<?php echo $link; ?>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</nav>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php else : ?>
|
||||
<div class="no-properties">
|
||||
<h3>No Properties Found</h3>
|
||||
<p>We couldn't find any properties matching your criteria. Try adjusting your filters or browse all properties.</p>
|
||||
<a href="<?php echo esc_url(get_post_type_archive_link('property')); ?>" class="btn btn-primary">View All Properties</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php
|
||||
wp_reset_postdata();
|
||||
|
||||
$html = ob_get_clean();
|
||||
|
||||
wp_send_json_success(array(
|
||||
'html' => $html,
|
||||
'found_posts' => $properties->found_posts,
|
||||
'max_pages' => $properties->max_num_pages,
|
||||
));
|
||||
}
|
||||
add_action('wp_ajax_homeproz_filter_properties', 'homeproz_ajax_filter_properties');
|
||||
add_action('wp_ajax_nopriv_homeproz_filter_properties', 'homeproz_ajax_filter_properties');
|
||||
|
||||
/**
|
||||
* Localize script data for AJAX
|
||||
*/
|
||||
function homeproz_localize_ajax_data() {
|
||||
if (is_post_type_archive('property') || is_tax('property_type') || is_tax('property_status') || is_tax('property_location')) {
|
||||
wp_localize_script('homeproz-main', 'homeprozAjax', array(
|
||||
'ajaxUrl' => admin_url('admin-ajax.php'),
|
||||
'nonce' => wp_create_nonce('homeproz_filter_nonce'),
|
||||
'archiveUrl' => get_post_type_archive_link('property'),
|
||||
));
|
||||
}
|
||||
}
|
||||
add_action('wp_enqueue_scripts', 'homeproz_localize_ajax_data', 20);
|
||||
@@ -8,6 +8,7 @@ import './main.scss';
|
||||
|
||||
// Import component JS
|
||||
import '../template-parts/header/navigation.js';
|
||||
import '../template-parts/property/property-filters.js';
|
||||
|
||||
(function($) {
|
||||
'use strict';
|
||||
|
||||
@@ -0,0 +1,244 @@
|
||||
/**
|
||||
* Property Filters JavaScript
|
||||
*
|
||||
* AJAX filtering for property archive
|
||||
*
|
||||
* @package HomeProz
|
||||
*/
|
||||
|
||||
(function($) {
|
||||
'use strict';
|
||||
|
||||
var PropertyFilters = {
|
||||
// Elements
|
||||
$form: null,
|
||||
$results: null,
|
||||
$filters: null,
|
||||
|
||||
// State
|
||||
isFirstLoad: true,
|
||||
isLoading: false,
|
||||
|
||||
/**
|
||||
* Initialize
|
||||
*/
|
||||
init: function() {
|
||||
this.$form = $('.filters-form');
|
||||
this.$results = $('#property-results');
|
||||
this.$filters = $('#property-filters');
|
||||
|
||||
if (!this.$form.length || !this.$results.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.bindEvents();
|
||||
this.initFromUrl();
|
||||
},
|
||||
|
||||
/**
|
||||
* Bind events
|
||||
*/
|
||||
bindEvents: function() {
|
||||
var self = this;
|
||||
|
||||
// Form submission (Search button)
|
||||
this.$form.on('submit', function(e) {
|
||||
e.preventDefault();
|
||||
self.filterProperties(1);
|
||||
});
|
||||
|
||||
// Filter changes (auto-submit on select change)
|
||||
this.$form.find('select').on('change', function() {
|
||||
self.filterProperties(1);
|
||||
});
|
||||
|
||||
// Reset button
|
||||
$('.filters-reset').on('click', function(e) {
|
||||
e.preventDefault();
|
||||
self.resetFilters();
|
||||
});
|
||||
|
||||
// Pagination clicks (delegated)
|
||||
this.$results.on('click', '.pagination a', function(e) {
|
||||
e.preventDefault();
|
||||
var page = self.getPageFromUrl($(this).attr('href'));
|
||||
self.filterProperties(page);
|
||||
});
|
||||
|
||||
// Handle browser back/forward
|
||||
$(window).on('popstate', function(e) {
|
||||
if (e.originalEvent.state && e.originalEvent.state.propertyFilters) {
|
||||
self.setFormFromState(e.originalEvent.state.propertyFilters);
|
||||
self.filterProperties(e.originalEvent.state.page || 1, false);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Initialize filters from URL params
|
||||
*/
|
||||
initFromUrl: function() {
|
||||
var params = new URLSearchParams(window.location.search);
|
||||
var hasFilters = false;
|
||||
|
||||
// Set form values from URL
|
||||
this.$form.find('select').each(function() {
|
||||
var name = $(this).attr('name');
|
||||
if (params.has(name)) {
|
||||
$(this).val(params.get(name));
|
||||
hasFilters = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Save initial state
|
||||
if (hasFilters) {
|
||||
var state = this.getFormState();
|
||||
state.page = parseInt(params.get('paged')) || 1;
|
||||
history.replaceState({ propertyFilters: state, page: state.page }, '', window.location.href);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Filter properties via AJAX
|
||||
*/
|
||||
filterProperties: function(page, updateHistory) {
|
||||
if (this.isLoading) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateHistory = updateHistory !== false;
|
||||
page = page || 1;
|
||||
|
||||
var self = this;
|
||||
var formData = this.getFormData();
|
||||
formData.paged = page;
|
||||
|
||||
// Show loading state
|
||||
this.isLoading = true;
|
||||
this.$filters.addClass('is-loading');
|
||||
|
||||
// Show spinner only on first load
|
||||
if (this.isFirstLoad) {
|
||||
this.$results.html('<div class="property-results-loading"><div class="spinner"></div></div>');
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: homeprozAjax.ajaxUrl,
|
||||
type: 'POST',
|
||||
data: {
|
||||
action: 'homeproz_filter_properties',
|
||||
nonce: homeprozAjax.nonce,
|
||||
property_type: formData.property_type,
|
||||
property_status: formData.property_status,
|
||||
property_location: formData.property_location,
|
||||
min_price: formData.min_price,
|
||||
max_price: formData.max_price,
|
||||
beds: formData.beds,
|
||||
sort: formData.sort,
|
||||
paged: page
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
self.$results.html(response.data.html);
|
||||
self.isFirstLoad = false;
|
||||
|
||||
// Update URL
|
||||
if (updateHistory) {
|
||||
self.updateUrl(formData, page);
|
||||
}
|
||||
|
||||
// Scroll to top of results on page change
|
||||
if (page > 1) {
|
||||
$('html, body').animate({
|
||||
scrollTop: self.$filters.offset().top - 100
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
self.$results.html('<div class="no-properties"><h3>Error</h3><p>Something went wrong. Please try again.</p></div>');
|
||||
},
|
||||
complete: function() {
|
||||
self.isLoading = false;
|
||||
self.$filters.removeClass('is-loading');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Get form data as object
|
||||
*/
|
||||
getFormData: function() {
|
||||
return {
|
||||
property_type: this.$form.find('[name="property_type"]').val() || '',
|
||||
property_status: this.$form.find('[name="property_status"]').val() || '',
|
||||
property_location: this.$form.find('[name="property_location"]').val() || '',
|
||||
min_price: this.$form.find('[name="min_price"]').val() || '',
|
||||
max_price: this.$form.find('[name="max_price"]').val() || '',
|
||||
beds: this.$form.find('[name="beds"]').val() || '',
|
||||
sort: this.$form.find('[name="sort"]').val() || 'newest'
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Get form state for history
|
||||
*/
|
||||
getFormState: function() {
|
||||
return this.getFormData();
|
||||
},
|
||||
|
||||
/**
|
||||
* Set form from state
|
||||
*/
|
||||
setFormFromState: function(state) {
|
||||
for (var key in state) {
|
||||
this.$form.find('[name="' + key + '"]').val(state[key]);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Update browser URL
|
||||
*/
|
||||
updateUrl: function(formData, page) {
|
||||
var url = new URL(homeprozAjax.archiveUrl);
|
||||
|
||||
// Add non-empty filters to URL
|
||||
for (var key in formData) {
|
||||
if (formData[key] && formData[key] !== 'newest') {
|
||||
url.searchParams.set(key, formData[key]);
|
||||
}
|
||||
}
|
||||
|
||||
// Add page if > 1
|
||||
if (page > 1) {
|
||||
url.searchParams.set('paged', page);
|
||||
}
|
||||
|
||||
var state = { propertyFilters: formData, page: page };
|
||||
history.pushState(state, '', url.toString());
|
||||
},
|
||||
|
||||
/**
|
||||
* Get page number from pagination URL
|
||||
*/
|
||||
getPageFromUrl: function(url) {
|
||||
var match = url.match(/[?&]paged=(\d+)/);
|
||||
return match ? parseInt(match[1]) : 1;
|
||||
},
|
||||
|
||||
/**
|
||||
* Reset filters
|
||||
*/
|
||||
resetFilters: function() {
|
||||
this.$form.find('select').val('');
|
||||
this.$form.find('[name="sort"]').val('newest');
|
||||
this.filterProperties(1);
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize on document ready
|
||||
$(function() {
|
||||
PropertyFilters.init();
|
||||
});
|
||||
|
||||
})(jQuery);
|
||||
Reference in New Issue
Block a user