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">
|
<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>
|
<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; ?>>
|
<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) : ?>
|
<?php if ($has_image) : ?>
|
||||||
|
<div class="property-card-spinner">
|
||||||
|
<div class="spinner"></div>
|
||||||
|
</div>
|
||||||
|
<?php else : ?>
|
||||||
<div class="property-card-placeholder">
|
<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">
|
<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"/>
|
<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;
|
background-repeat: no-repeat;
|
||||||
|
|
||||||
&.has-image {
|
&.has-image {
|
||||||
// Loading state - shows subtle animation while image loads
|
|
||||||
background-color: var(--color-bg-card);
|
background-color: var(--color-bg-card);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hide spinner when image is loaded
|
||||||
|
&.is-loaded .property-card-spinner {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 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 {
|
.property-card-placeholder {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 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
|
// Initialize on document ready
|
||||||
$(function() {
|
$(function() {
|
||||||
PropertyFilters.init();
|
PropertyFilters.init();
|
||||||
ResponsiveView.init();
|
ResponsiveView.init();
|
||||||
LayoutCalculator.init();
|
LayoutCalculator.init();
|
||||||
|
CardImageLoader.init();
|
||||||
});
|
});
|
||||||
|
|
||||||
})(jQuery);
|
})(jQuery);
|
||||||
|
|||||||
Reference in New Issue
Block a user