Add loading spinner for property card images
- Show spinning loader while images load - Lazy load images as cards enter viewport (with 200px buffer) - Use data-bg attribute to defer background-image loading - MutationObserver detects AJAX-loaded content - Spinner hides when image loads or on error - Fallback to placeholder on load error 🤖 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
+1
-1
File diff suppressed because one or more lines are too long
@@ -67,8 +67,12 @@ $has_image = !empty($image_url);
|
||||
|
||||
<article id="property-<?php echo esc_attr($listing_key); ?>" data-property-id="<?php echo esc_attr($listing_key); ?>" class="property-card card mls-property">
|
||||
<a href="<?php echo esc_url($property_url); ?>" class="property-card-link-overlay" aria-hidden="true" tabindex="-1"></a>
|
||||
<div class="property-card-image<?php echo $has_image ? ' has-image' : ''; ?>"<?php if ($has_image) : ?> style="background-image: url('<?php echo esc_url($image_url); ?>');"<?php endif; ?>>
|
||||
<?php if (!$has_image) : ?>
|
||||
<div class="property-card-image<?php echo $has_image ? ' has-image is-loading' : ''; ?>"<?php if ($has_image) : ?> data-bg="<?php echo esc_url($image_url); ?>"<?php endif; ?>>
|
||||
<?php if ($has_image) : ?>
|
||||
<div class="property-card-spinner">
|
||||
<div class="spinner"></div>
|
||||
</div>
|
||||
<?php else : ?>
|
||||
<div class="property-card-placeholder">
|
||||
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true">
|
||||
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>
|
||||
|
||||
@@ -45,10 +45,14 @@
|
||||
background-repeat: no-repeat;
|
||||
|
||||
&.has-image {
|
||||
// Loading state - shows subtle animation while image loads
|
||||
background-color: var(--color-bg-card);
|
||||
}
|
||||
|
||||
// Hide spinner when image is loaded
|
||||
&.is-loaded .property-card-spinner {
|
||||
display: none;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@@ -56,6 +60,34 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Loading spinner for property card images
|
||||
.property-card-spinner {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: var(--color-bg-card);
|
||||
|
||||
.spinner {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: 3px solid var(--color-border);
|
||||
border-top-color: var(--color-accent);
|
||||
border-radius: 50%;
|
||||
animation: card-spin 0.8s linear infinite;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes card-spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.property-card-placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
@@ -721,11 +721,93 @@
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Property Card Image Loader
|
||||
* Lazy loads background images with loading spinner
|
||||
*/
|
||||
var CardImageLoader = {
|
||||
init: function() {
|
||||
this.loadVisibleImages();
|
||||
this.bindEvents();
|
||||
},
|
||||
|
||||
bindEvents: function() {
|
||||
var self = this;
|
||||
var scrollTimeout;
|
||||
|
||||
// Load images on scroll (throttled)
|
||||
$(window).on('scroll', function() {
|
||||
clearTimeout(scrollTimeout);
|
||||
scrollTimeout = setTimeout(function() {
|
||||
self.loadVisibleImages();
|
||||
}, 100);
|
||||
});
|
||||
|
||||
// Also observe DOM changes for AJAX-loaded content
|
||||
if (typeof MutationObserver !== 'undefined') {
|
||||
var observer = new MutationObserver(function(mutations) {
|
||||
self.loadVisibleImages();
|
||||
});
|
||||
var resultsContainer = document.getElementById('property-results');
|
||||
if (resultsContainer) {
|
||||
observer.observe(resultsContainer, { childList: true, subtree: true });
|
||||
}
|
||||
var gridContainer = document.getElementById('property-results-grid');
|
||||
if (gridContainer) {
|
||||
observer.observe(gridContainer, { childList: true, subtree: true });
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
loadVisibleImages: function() {
|
||||
var self = this;
|
||||
var viewportHeight = $(window).height();
|
||||
var scrollTop = $(window).scrollTop();
|
||||
var buffer = 200; // Load images 200px before they enter viewport
|
||||
|
||||
$('.property-card-image.is-loading[data-bg]').each(function() {
|
||||
var $el = $(this);
|
||||
var elTop = $el.offset().top;
|
||||
var elBottom = elTop + $el.outerHeight();
|
||||
|
||||
// Check if element is in or near viewport
|
||||
if (elBottom >= scrollTop - buffer && elTop <= scrollTop + viewportHeight + buffer) {
|
||||
self.loadImage($el);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
loadImage: function($el) {
|
||||
var bgUrl = $el.data('bg');
|
||||
if (!bgUrl) return;
|
||||
|
||||
// Remove from loading queue immediately to prevent double-load
|
||||
$el.removeClass('is-loading').removeAttr('data-bg');
|
||||
|
||||
// Create image to preload
|
||||
var img = new Image();
|
||||
|
||||
img.onload = function() {
|
||||
$el.css('background-image', 'url(' + bgUrl + ')');
|
||||
$el.addClass('is-loaded');
|
||||
};
|
||||
|
||||
img.onerror = function() {
|
||||
// On error, show placeholder
|
||||
$el.addClass('is-loaded');
|
||||
$el.removeClass('has-image');
|
||||
};
|
||||
|
||||
img.src = bgUrl;
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize on document ready
|
||||
$(function() {
|
||||
PropertyFilters.init();
|
||||
ResponsiveView.init();
|
||||
LayoutCalculator.init();
|
||||
CardImageLoader.init();
|
||||
});
|
||||
|
||||
})(jQuery);
|
||||
|
||||
Reference in New Issue
Block a user