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:
Hanson.xyz Dev
2025-12-15 23:48:59 -06:00
parent 72b932b25e
commit 30eb593020
5 changed files with 123 additions and 5 deletions
File diff suppressed because one or more lines are too long
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);