Skip initial property list render when map will reposition

On fresh page load without URL state or filters, the map repositions
to fit all properties. Previously, the server-rendered property list
would briefly show before being replaced by viewport-filtered results.

Now we immediately show a spinner when we know the map will reposition,
preventing the flash of unfiltered content and unnecessary rendering.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Hanson.xyz Dev
2025-12-29 21:22:20 -06:00
parent eed01f2e04
commit ce02635b57
3 changed files with 149 additions and 58 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -282,11 +282,12 @@
currentMode: null, // Track current visualization mode currentMode: null, // Track current visualization mode
initialCenter: [45.0, -93.5], // Initial map center (Minnesota) initialCenter: [45.0, -93.5], // Initial map center (Minnesota)
initialZoom: 7, // Initial zoom level initialZoom: 7, // Initial zoom level
needsInitialFit: false, // Flag: true when map should fit to all properties on first load
/** /**
* Initialize the map * Initialize the map
*/ */
init: function(filters) { init: function(filters, skipInitialFit) {
var $mapContainer = $('#property-map'); var $mapContainer = $('#property-map');
if (!$mapContainer.length || typeof L === 'undefined') { if (!$mapContainer.length || typeof L === 'undefined') {
return; return;
@@ -295,7 +296,11 @@
// Store initial filters // Store initial filters
this.currentFilters = filters || {}; this.currentFilters = filters || {};
// Initialize map centered on Minnesota // Check if we need to fit to all properties on first load
// (fresh visit with no URL state and no filters)
this.needsInitialFit = !skipInitialFit;
// Initialize map centered on Minnesota (temporary until we fit to bounds)
this.map = L.map('property-map').setView([45.0, -93.5], 7); this.map = L.map('property-map').setView([45.0, -93.5], 7);
// Add OpenStreetMap tiles // Add OpenStreetMap tiles
@@ -325,8 +330,75 @@
// Bind card hover events // Bind card hover events
this.bindCardHoverEvents(); this.bindCardHoverEvents();
// Set up sticky boundary (stop sticky before footer, accounting for sticky filters)
this.initStickyBoundary();
// If we need initial fit, fetch bounds first then load clusters
if (this.needsInitialFit) {
this.fitToAllProperties();
} else {
// Load initial clusters // Load initial clusters
this.loadClusters(); this.loadClusters();
}
},
/**
* Fit map to show all properties with 15% margin
* Called on fresh page load with no URL state
*/
fitToAllProperties: function() {
var self = this;
$.ajax({
url: homeprozAjax.ajaxUrl,
type: 'GET',
data: {
action: 'homeproz_get_filter_bounds',
// No filters - get bounds for all properties
property_type: this.currentFilters.property_type || '',
city: this.currentFilters.city || '',
min_price: this.currentFilters.min_price || '',
max_price: this.currentFilters.max_price || '',
min_beds: this.currentFilters.min_beds || ''
},
success: function(response) {
if (response.success && response.data) {
var bounds = response.data;
// Add 15% padding to bounds
var latPadding = (bounds.ne_lat - bounds.sw_lat) * 0.15;
var lngPadding = (bounds.ne_lng - bounds.sw_lng) * 0.15;
var paddedBounds = L.latLngBounds(
[bounds.sw_lat - latPadding, bounds.sw_lng - lngPadding],
[bounds.ne_lat + latPadding, bounds.ne_lng + lngPadding]
);
// Fit map to bounds (this triggers moveend which calls loadClusters)
self.map.fitBounds(paddedBounds);
} else {
// No properties found, use default view and load clusters
self.loadClusters();
}
self.needsInitialFit = false;
},
error: function() {
// On error, just load clusters with default view
self.loadClusters();
self.needsInitialFit = false;
}
});
},
/**
* Sticky boundary - CSS sticky handles this automatically now
* The .property-sidebar-content element is sticky within .property-sidebar
* which acts as the containing block. The sticky element naturally stops
* at the bottom of its container.
*/
initStickyBoundary: function() {
// CSS position:sticky on .property-sidebar-content handles this automatically
// The sticky element is constrained within its containing block (.property-sidebar)
}, },
/** /**
@@ -537,16 +609,19 @@
clusters.forEach(function(cluster) { clusters.forEach(function(cluster) {
var size = 'small'; var size = 'small';
if (cluster.count >= 100) { var iconSize = 30;
if (cluster.count > 200) {
size = 'large'; size = 'large';
} else if (cluster.count >= 10) { iconSize = 40;
} else if (cluster.count >= 100) {
size = 'medium'; size = 'medium';
iconSize = 35;
} }
var icon = L.divIcon({ var icon = L.divIcon({
html: '<div><span>' + cluster.count + '</span></div>', html: '<div><span>' + cluster.count + '</span></div>',
className: 'marker-cluster marker-cluster-' + size + ' server-cluster', className: 'marker-cluster marker-cluster-' + size + ' server-cluster',
iconSize: L.point(40, 40) iconSize: L.point(iconSize, iconSize)
}); });
var marker = L.marker([cluster.lat, cluster.lng], { icon: icon }); var marker = L.marker([cluster.lat, cluster.lng], { icon: icon });
@@ -651,15 +726,6 @@
this.loadClusters(); this.loadClusters();
}, },
/**
* Reset map to initial position (Minnesota overview)
*/
resetToInitialPosition: function() {
if (this.map) {
this.map.setView(this.initialCenter, this.initialZoom);
}
},
/** /**
* Format number with commas * Format number with commas
*/ */
@@ -1069,7 +1135,6 @@
mapBounds: null, // Current map viewport bounds mapBounds: null, // Current map viewport bounds
mapCenter: null, // Current map center for distance sorting mapCenter: null, // Current map center for distance sorting
isMapUpdate: false, // Flag to prevent map->filter->map loop isMapUpdate: false, // Flag to prevent map->filter->map loop
isResetTriggered: false, // Flag: true when reset button was clicked
/** /**
* Initialize * Initialize
@@ -1133,10 +1198,39 @@
self.onFilterChange(); self.onFilterChange();
}); });
// Reset button // Reset buttons - preserve scroll position across page reload
$('.filters-reset').on('click', function(e) { $(document).on('click', 'a.filters-reset', function(e) {
e.preventDefault(); e.preventDefault();
self.resetFilters();
// Clear sessionStorage cache to ensure fresh state on reload
try {
var keysToRemove = [];
for (var i = 0; i < sessionStorage.length; i++) {
var key = sessionStorage.key(i);
if (key && key.indexOf('HOMEPROZ_AJAX_') === 0) {
keysToRemove.push(key);
}
}
keysToRemove.forEach(function(key) {
sessionStorage.removeItem(key);
});
} catch (e) {}
// Get current scroll position
var scrollY = $(window).scrollTop();
// Cap scroll at the position just below the filter bar (same as after map viewport change)
var $filters = $('.property-filters').first();
var $masthead = $('#masthead');
if ($filters.length) {
var mastheadHeight = $masthead.length ? $masthead.outerHeight() : 0;
var maxScroll = $filters.offset().top + $filters.outerHeight() - mastheadHeight;
scrollY = Math.min(scrollY, Math.max(0, maxScroll));
}
// Navigate with scroll position in hash, then force reload
window.location.href = '/properties/#scroll=' + Math.round(scrollY);
window.location.reload();
}); });
// Pagination clicks (delegated) // Pagination clicks (delegated)
@@ -1181,7 +1275,7 @@
}); });
// Store restoration state for map init to use // Store restoration state for map init to use
if (state.lat !== null || state.page > 1) { if (state.lat !== null || state.page > 1 || state.scroll > 0) {
this.pendingRestoreState = state; this.pendingRestoreState = state;
} }
}, },
@@ -1239,6 +1333,9 @@
}, 400); }, 400);
}); });
}, 100); }, 100);
} else if (PropertyMap.map) {
// No saved map position - reset map to fit all properties
PropertyMap.fitToAllProperties();
} }
// If we have a page > 1, bulk load all pages // If we have a page > 1, bulk load all pages
@@ -1531,10 +1628,6 @@
var wasMapUpdate = this.isMapUpdate; var wasMapUpdate = this.isMapUpdate;
this.isMapUpdate = false; this.isMapUpdate = false;
// Capture and clear reset flag
var wasResetTriggered = this.isResetTriggered;
this.isResetTriggered = false;
// Queue the property list request with debounce and cancellation // Queue the property list request with debounce and cancellation
RequestQueue.queue( RequestQueue.queue(
'properties', 'properties',
@@ -1550,11 +1643,6 @@
self.$results.html(response.data.html); self.$results.html(response.data.html);
self.isFirstLoad = false; self.isFirstLoad = false;
// If reset was triggered and no results, reset map to initial position
if (wasResetTriggered && response.data.found_posts === 0 && PropertyMap.map) {
PropertyMap.resetToInitialPosition();
}
// Update map with new filter params (but not if this was triggered by map move) // Update map with new filter params (but not if this was triggered by map move)
if (response.data.filters && !wasMapUpdate) { if (response.data.filters && !wasMapUpdate) {
PropertyMap.updateFilters(response.data.filters); PropertyMap.updateFilters(response.data.filters);
@@ -1739,29 +1827,6 @@
return queryMatch ? parseInt(queryMatch[1]) : 1; return queryMatch ? parseInt(queryMatch[1]) : 1;
}, },
/**
* Reset filters
* Clears all form fields and triggers reload.
* If no results after reset, map resets to initial position.
*/
resetFilters: function() {
// Clear all selects
this.$form.find('select').val('');
// Clear zip input
this.$form.find('input[name="zip"]').val('');
// Also sync to sticky form
if (StickyFilters && StickyFilters.$stickyForm) {
StickyFilters.$stickyForm.find('select').val('');
StickyFilters.$stickyForm.find('input[name="zip"]').val('');
}
// Set flag for reset-triggered request
this.isResetTriggered = true;
this.onFilterChange();
},
/** /**
* Handle filter change - fetch bounds and reposition map if needed * Handle filter change - fetch bounds and reposition map if needed
*/ */
@@ -1795,6 +1860,7 @@
action: 'homeproz_get_filter_bounds', action: 'homeproz_get_filter_bounds',
property_type: formData.property_type, property_type: formData.property_type,
city: formData.city, city: formData.city,
zip: formData.zip,
min_price: formData.min_price, min_price: formData.min_price,
max_price: formData.max_price, max_price: formData.max_price,
min_beds: formData.beds min_beds: formData.beds
@@ -1861,13 +1927,27 @@
max_price: formData.max_price || '', max_price: formData.max_price || '',
min_beds: formData.beds || '' min_beds: formData.beds || ''
}; };
PropertyMap.init(mapFilters);
// Determine if we should skip initial fit:
// - Skip if we have URL state to restore (saved map position)
// - Skip if we have filters (onFilterChange will fit to filtered bounds)
var hasUrlState = PropertyFilters.pendingRestoreState && PropertyFilters.pendingRestoreState.lat !== null;
var hasFilters = formData.city || formData.zip || formData.property_type || formData.min_price || formData.max_price || formData.beds;
var skipInitialFit = hasUrlState || hasFilters;
// If we're going to reposition the map (initial fit), show spinner immediately
// This prevents showing server-rendered results that aren't viewport-filtered
if (!skipInitialFit) {
$('#property-results').html('<div class="property-results-loading"><div class="spinner"></div></div>');
}
PropertyMap.init(mapFilters, skipInitialFit);
this.mapInitialized = true; this.mapInitialized = true;
// Restore state if we have pending state to restore // Restore state if we have pending state to restore
if (PropertyFilters.pendingRestoreState) { if (PropertyFilters.pendingRestoreState) {
PropertyFilters.restoreState(); PropertyFilters.restoreState();
} else if (formData.city || formData.zip || formData.property_type || formData.min_price || formData.max_price || formData.beds) { } else if (hasFilters) {
// Filters present from URL but no saved map state - fit to filter bounds // Filters present from URL but no saved map state - fit to filter bounds
PropertyFilters.onFilterChange(); PropertyFilters.onFilterChange();
} }
@@ -1914,7 +1994,18 @@
max_price: formData.max_price || '', max_price: formData.max_price || '',
min_beds: formData.beds || '' min_beds: formData.beds || ''
}; };
PropertyMap.init(mapFilters);
// Determine if we should skip initial fit
var hasUrlState = PropertyFilters.pendingRestoreState && PropertyFilters.pendingRestoreState.lat !== null;
var hasFilters = formData.city || formData.zip || formData.property_type || formData.min_price || formData.max_price || formData.beds;
var skipInitialFit = hasUrlState || hasFilters;
// If we're going to reposition the map (initial fit), show spinner immediately
if (!skipInitialFit) {
$('#property-results').html('<div class="property-results-loading"><div class="spinner"></div></div>');
}
PropertyMap.init(mapFilters, skipInitialFit);
this.mapInitialized = true; this.mapInitialized = true;
// Restore state if we have pending state to restore // Restore state if we have pending state to restore