Add US geo data tables, filter bounds API, and URL hash state management
- Add mls_geo_cities and mls_geo_zipcodes tables with 29,880 cities and 33,144 zip codes - Add get_filter_bounds() method to reposition map when filters don't intersect current view - Move all URL state (filters, page, scroll, map position) to hash to avoid WordPress 404s - Add filter bounds AJAX endpoint for map repositioning on filter change Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
+1
-1
File diff suppressed because one or more lines are too long
@@ -125,7 +125,7 @@ function homeproz_ajax_filter_properties() {
|
||||
<div class="properties-meta">
|
||||
<p class="properties-count">
|
||||
<?php if ($total > 0) : ?>
|
||||
Showing <strong><?php echo esc_html($total); ?></strong>
|
||||
Showing <strong><?php echo esc_html(number_format($total)); ?></strong>
|
||||
<?php echo $total === 1 ? 'property' : 'properties'; ?>
|
||||
<?php if ($has_map_bounds) : ?>
|
||||
in view
|
||||
@@ -207,6 +207,55 @@ function homeproz_ajax_filter_properties() {
|
||||
add_action('wp_ajax_homeproz_filter_properties', 'homeproz_ajax_filter_properties');
|
||||
add_action('wp_ajax_nopriv_homeproz_filter_properties', 'homeproz_ajax_filter_properties');
|
||||
|
||||
/**
|
||||
* Get geographic bounds for filtered properties
|
||||
* Used to reposition map when filters change
|
||||
*/
|
||||
function homeproz_ajax_get_filter_bounds() {
|
||||
// Check if MLS plugin is available
|
||||
if (!function_exists('mls_get_filter_bounds')) {
|
||||
wp_send_json_error('MLS plugin not available');
|
||||
}
|
||||
|
||||
// Get filter values
|
||||
$property_type = isset($_GET['property_type']) ? sanitize_text_field($_GET['property_type']) : '';
|
||||
$property_location = isset($_GET['city']) ? sanitize_text_field($_GET['city']) : '';
|
||||
$min_price = isset($_GET['min_price']) ? intval($_GET['min_price']) : '';
|
||||
$max_price = isset($_GET['max_price']) ? intval($_GET['max_price']) : '';
|
||||
$min_beds = isset($_GET['min_beds']) ? intval($_GET['min_beds']) : '';
|
||||
|
||||
// Build filter args
|
||||
$filter_args = array(
|
||||
'status' => 'Active',
|
||||
);
|
||||
|
||||
if ($property_type) {
|
||||
$filter_args['property_type'] = $property_type;
|
||||
}
|
||||
if ($property_location) {
|
||||
$filter_args['city'] = $property_location;
|
||||
}
|
||||
if ($min_price) {
|
||||
$filter_args['min_price'] = $min_price;
|
||||
}
|
||||
if ($max_price) {
|
||||
$filter_args['max_price'] = $max_price;
|
||||
}
|
||||
if ($min_beds) {
|
||||
$filter_args['min_beds'] = $min_beds;
|
||||
}
|
||||
|
||||
$bounds = mls_get_filter_bounds($filter_args);
|
||||
|
||||
if (!$bounds) {
|
||||
wp_send_json_error('No properties found');
|
||||
}
|
||||
|
||||
wp_send_json_success($bounds);
|
||||
}
|
||||
add_action('wp_ajax_homeproz_get_filter_bounds', 'homeproz_ajax_get_filter_bounds');
|
||||
add_action('wp_ajax_nopriv_homeproz_get_filter_bounds', 'homeproz_ajax_get_filter_bounds');
|
||||
|
||||
/**
|
||||
* Localize script data for AJAX
|
||||
*/
|
||||
|
||||
@@ -1079,7 +1079,7 @@
|
||||
// Filter changes (auto-submit on select change)
|
||||
this.$form.find('select').on('change', function() {
|
||||
self.clearPinSelection();
|
||||
self.filterProperties(1);
|
||||
self.onFilterChange();
|
||||
});
|
||||
|
||||
// Reset button
|
||||
@@ -1103,33 +1103,34 @@
|
||||
},
|
||||
|
||||
/**
|
||||
* Initialize filters from URL (hash for page, scroll, map state)
|
||||
* Initialize filters from URL (all state from hash)
|
||||
*/
|
||||
initFromUrl: function() {
|
||||
var self = this;
|
||||
var params = new URLSearchParams(window.location.search);
|
||||
|
||||
// Set form values from URL query params (filters only, not page)
|
||||
// Get full state from hash (includes filters now)
|
||||
var state = this.getStateFromHash();
|
||||
|
||||
if (!state) return;
|
||||
|
||||
// Set form values from hash state
|
||||
this.$form.find('select').each(function() {
|
||||
var name = $(this).attr('name');
|
||||
if (params.has(name)) {
|
||||
$(this).val(params.get(name));
|
||||
if (state[name]) {
|
||||
$(this).val(state[name]);
|
||||
}
|
||||
});
|
||||
|
||||
// Also handle text inputs (like zip)
|
||||
this.$form.find('input[type="text"]').each(function() {
|
||||
var name = $(this).attr('name');
|
||||
if (params.has(name)) {
|
||||
$(this).val(params.get(name));
|
||||
if (state[name]) {
|
||||
$(this).val(state[name]);
|
||||
}
|
||||
});
|
||||
|
||||
// Get full state from hash
|
||||
var state = this.getStateFromHash();
|
||||
|
||||
// Store restoration state for map init to use
|
||||
if (state && (state.lat !== null || state.page > 1)) {
|
||||
if (state.lat !== null || state.page > 1) {
|
||||
this.pendingRestoreState = state;
|
||||
}
|
||||
},
|
||||
@@ -1145,6 +1146,17 @@
|
||||
if (!state) return;
|
||||
this.pendingRestoreState = null;
|
||||
|
||||
// Update map filters from form (which was set from hash in initFromUrl)
|
||||
var formData = this.getFormData();
|
||||
PropertyMap.currentFilters = {
|
||||
status: 'Active',
|
||||
property_type: formData.property_type || '',
|
||||
city: formData.property_location || '',
|
||||
min_price: formData.min_price || '',
|
||||
max_price: formData.max_price || '',
|
||||
min_beds: formData.beds || ''
|
||||
};
|
||||
|
||||
// If we have map coordinates, set map position first (without triggering load)
|
||||
if (state.lat !== null && state.lng !== null && state.zoom !== null && PropertyMap.map) {
|
||||
// Temporarily disable map events
|
||||
@@ -1563,22 +1575,19 @@
|
||||
},
|
||||
|
||||
/**
|
||||
* Update browser URL (filters in query, state in hash)
|
||||
* Stores: page, scroll position, map position/zoom
|
||||
* Update browser URL (all state in hash to avoid WordPress query var conflicts)
|
||||
* Stores: filters, page, scroll position, map position/zoom
|
||||
*/
|
||||
updateUrl: function(formData, page) {
|
||||
var url = new URL(homeprozAjax.archiveUrl);
|
||||
var hashParts = [];
|
||||
|
||||
// Add non-empty filters to URL query params
|
||||
// Add non-empty filters to hash
|
||||
for (var key in formData) {
|
||||
if (formData[key]) {
|
||||
url.searchParams.set(key, formData[key]);
|
||||
hashParts.push(key + '=' + encodeURIComponent(formData[key]));
|
||||
}
|
||||
}
|
||||
|
||||
// Build hash with all state info
|
||||
var hashParts = [];
|
||||
|
||||
if (page > 1) {
|
||||
hashParts.push('page=' + page);
|
||||
}
|
||||
@@ -1600,10 +1609,10 @@
|
||||
hashParts.push('zoom=' + zoom);
|
||||
}
|
||||
|
||||
url.hash = hashParts.length ? hashParts.join('&') : '';
|
||||
var newUrl = homeprozAjax.archiveUrl + (hashParts.length ? '#' + hashParts.join('&') : '');
|
||||
|
||||
// Use replaceState to avoid adding history entries for every page
|
||||
history.replaceState(null, '', url.toString());
|
||||
history.replaceState(null, '', newUrl);
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -1624,26 +1633,35 @@
|
||||
},
|
||||
|
||||
/**
|
||||
* Parse state from URL hash
|
||||
* Parse state from URL hash (includes filters, page, scroll, map state)
|
||||
*/
|
||||
getStateFromHash: function() {
|
||||
var hash = window.location.hash.replace('#', '');
|
||||
if (!hash) return null;
|
||||
|
||||
var state = {};
|
||||
var raw = {};
|
||||
hash.split('&').forEach(function(part) {
|
||||
var kv = part.split('=');
|
||||
if (kv.length === 2) {
|
||||
state[kv[0]] = kv[1];
|
||||
raw[kv[0]] = decodeURIComponent(kv[1]);
|
||||
}
|
||||
});
|
||||
|
||||
// Return object with all values - filters will be accessed by name
|
||||
return {
|
||||
page: state.page ? parseInt(state.page) : 1,
|
||||
scroll: state.scroll ? parseInt(state.scroll) : 0,
|
||||
lat: state.lat ? parseFloat(state.lat) : null,
|
||||
lng: state.lng ? parseFloat(state.lng) : null,
|
||||
zoom: state.zoom ? parseInt(state.zoom) : null
|
||||
// Filters
|
||||
property_type: raw.property_type || '',
|
||||
property_location: raw.property_location || '',
|
||||
zip: raw.zip || '',
|
||||
min_price: raw.min_price || '',
|
||||
max_price: raw.max_price || '',
|
||||
beds: raw.beds || '',
|
||||
// State
|
||||
page: raw.page ? parseInt(raw.page) : 1,
|
||||
scroll: raw.scroll ? parseInt(raw.scroll) : 0,
|
||||
lat: raw.lat ? parseFloat(raw.lat) : null,
|
||||
lng: raw.lng ? parseFloat(raw.lng) : null,
|
||||
zoom: raw.zoom ? parseInt(raw.zoom) : null
|
||||
};
|
||||
},
|
||||
|
||||
@@ -1666,7 +1684,88 @@
|
||||
*/
|
||||
resetFilters: function() {
|
||||
this.$form.find('select').val('');
|
||||
this.filterProperties(1);
|
||||
this.onFilterChange();
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle filter change - fetch bounds and reposition map if needed
|
||||
*/
|
||||
onFilterChange: function() {
|
||||
var self = this;
|
||||
var formData = this.getFormData();
|
||||
|
||||
// Update map filters
|
||||
if (PropertyMap.map) {
|
||||
PropertyMap.currentFilters = {
|
||||
status: 'Active',
|
||||
property_type: formData.property_type || '',
|
||||
city: formData.property_location || '',
|
||||
min_price: formData.min_price || '',
|
||||
max_price: formData.max_price || '',
|
||||
min_beds: formData.beds || ''
|
||||
};
|
||||
}
|
||||
|
||||
// If map is not visible, just filter properties
|
||||
if (!PropertyMap.map) {
|
||||
this.filterProperties(1);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch bounds for the new filter set
|
||||
$.ajax({
|
||||
url: homeprozAjax.ajaxUrl,
|
||||
type: 'GET',
|
||||
data: {
|
||||
action: 'homeproz_get_filter_bounds',
|
||||
property_type: formData.property_type,
|
||||
city: formData.property_location,
|
||||
min_price: formData.min_price,
|
||||
max_price: formData.max_price,
|
||||
min_beds: formData.beds
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.success && response.data) {
|
||||
var filterBounds = response.data;
|
||||
var mapBounds = PropertyMap.map.getBounds();
|
||||
|
||||
// Check if map view intersects with filter bounds
|
||||
var filterLatLngBounds = L.latLngBounds(
|
||||
[filterBounds.sw_lat, filterBounds.sw_lng],
|
||||
[filterBounds.ne_lat, filterBounds.ne_lng]
|
||||
);
|
||||
|
||||
// Check if ANY of the filter bounds corners are visible in the map
|
||||
// OR if the map view is fully contained within the filter bounds
|
||||
var mapIntersects = mapBounds.intersects(filterLatLngBounds);
|
||||
|
||||
if (!mapIntersects) {
|
||||
// Add 10% padding to bounds
|
||||
var latPadding = (filterBounds.ne_lat - filterBounds.sw_lat) * 0.1;
|
||||
var lngPadding = (filterBounds.ne_lng - filterBounds.sw_lng) * 0.1;
|
||||
|
||||
var paddedBounds = L.latLngBounds(
|
||||
[filterBounds.sw_lat - latPadding, filterBounds.sw_lng - lngPadding],
|
||||
[filterBounds.ne_lat + latPadding, filterBounds.ne_lng + lngPadding]
|
||||
);
|
||||
|
||||
// Reposition map to show filtered properties
|
||||
PropertyMap.map.fitBounds(paddedBounds);
|
||||
// The moveend event will trigger loadClusters and updateFromMap
|
||||
} else {
|
||||
// Map already shows relevant area, just reload clusters and properties
|
||||
PropertyMap.loadClusters();
|
||||
}
|
||||
} else {
|
||||
// No properties found with these filters, just reload
|
||||
PropertyMap.loadClusters();
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
// On error, just proceed with normal filter
|
||||
PropertyMap.loadClusters();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1693,7 +1792,17 @@
|
||||
|
||||
// Initialize map if above breakpoint, map view selected, and we have data
|
||||
if (this.isAboveBreakpoint && this.isMapView && typeof homeprozMapData !== 'undefined') {
|
||||
PropertyMap.init(homeprozMapData.initialFilters || {});
|
||||
// Get filters from form (which was set from hash in initFromUrl)
|
||||
var formData = PropertyFilters.getFormData();
|
||||
var mapFilters = {
|
||||
status: 'Active',
|
||||
property_type: formData.property_type || '',
|
||||
city: formData.property_location || '',
|
||||
min_price: formData.min_price || '',
|
||||
max_price: formData.max_price || '',
|
||||
min_beds: formData.beds || ''
|
||||
};
|
||||
PropertyMap.init(mapFilters);
|
||||
this.mapInitialized = true;
|
||||
|
||||
// Restore state if we have pending state to restore
|
||||
@@ -1733,7 +1842,17 @@
|
||||
|
||||
// Initialize map if not already done
|
||||
if (!this.mapInitialized && typeof homeprozMapData !== 'undefined') {
|
||||
PropertyMap.init(homeprozMapData.initialFilters || {});
|
||||
// Get filters from form
|
||||
var formData = PropertyFilters.getFormData();
|
||||
var mapFilters = {
|
||||
status: 'Active',
|
||||
property_type: formData.property_type || '',
|
||||
city: formData.property_location || '',
|
||||
min_price: formData.min_price || '',
|
||||
max_price: formData.max_price || '',
|
||||
min_beds: formData.beds || ''
|
||||
};
|
||||
PropertyMap.init(mapFilters);
|
||||
this.mapInitialized = true;
|
||||
|
||||
// Restore state if we have pending state to restore
|
||||
|
||||
Reference in New Issue
Block a user