diff --git a/src/components/ui/ProgressiveImage.vue b/src/components/ui/ProgressiveImage.vue index 6e4dbb0..042546c 100644 --- a/src/components/ui/ProgressiveImage.vue +++ b/src/components/ui/ProgressiveImage.vue @@ -3,7 +3,10 @@
+ :style="placeholderStyle"> + +
+
+ ]" :loading="loading" @load="handleLoad" @error="handleError" /> -->
- +
- Failed to load + {{ errorMessage }}
@@ -109,7 +112,7 @@ interface Emits { const props = withDefaults(defineProps(), { blurRadius: 10, - backgroundColor: '#f3f4f6', + backgroundColor: 'hsl(var(--muted))', transitionDuration: 300, loading: 'lazy', showLoadingIndicator: false, @@ -124,14 +127,29 @@ const isLoaded = ref(false) const hasError = ref(false) const isLoading = ref(false) +// Check if this is a placeholder/no-image URL +const isNoImage = computed(() => { + return props.src.includes('/placeholder-product.png') || + props.src === '' || + !props.src +}) + +// Dynamic error message based on whether image exists +const errorMessage = computed(() => { + if (isNoImage.value) { + return 'No image available' + } + return 'Failed to load' +}) + // Computed styles const placeholderStyle = computed(() => { const hasThumb = props.thumbnail && props.thumbnail.trim() - // Create a subtle gradient pattern when no thumbnail + // Create a subtle gradient pattern when no thumbnail (theme-aware) const gradientBg = hasThumb ? 'none' - : `linear-gradient(135deg, #e5e7eb 0%, #f3f4f6 50%, #e5e7eb 100%)` + : `linear-gradient(135deg, hsl(var(--muted)) 0%, hsl(var(--muted) / 0.5) 50%, hsl(var(--muted)) 100%)` return { '--blur-radius': `${props.blurRadius}px`, @@ -208,24 +226,21 @@ defineExpose({ transition: opacity var(--transition-duration, 300ms) ease-out; } -/* Add shimmer effect when image is loading */ +/* Add shimmer effect when image is loading - follows Tailwind animation patterns */ .progressive-image-placeholder.shimmer-active { - background-image: - linear-gradient( - 90deg, - transparent 0%, - rgba(255, 255, 255, 0.3) 25%, - rgba(255, 255, 255, 0.7) 50%, - rgba(255, 255, 255, 0.3) 75%, - transparent 100% - ), - linear-gradient(135deg, #e5e7eb 0%, #f3f4f6 50%, #e5e7eb 100%); - background-size: 200px 100%, 100% 100%; - background-position: -200px 0, 0 0; - background-repeat: no-repeat, no-repeat; - animation: shimmer 1.2s infinite linear; + position: relative; + background: linear-gradient(135deg, hsl(var(--muted)) 0%, hsl(var(--muted) / 0.5) 50%, hsl(var(--muted)) 100%) !important; + overflow: hidden; } +/* Accessibility: Respect user's reduced motion preference */ +@media (prefers-reduced-motion: reduce) { + .shimmer-overlay { + animation: none !important; + } +} + + .progressive-image { width: 100%; height: 100%; @@ -281,7 +296,7 @@ defineExpose({ display: flex; align-items: center; justify-content: center; - background-color: hsl(var(--muted)); + background: linear-gradient(135deg, hsl(var(--muted) / 0.5), hsl(var(--muted))); } .progressive-image-error-content { @@ -307,12 +322,13 @@ defineExpose({ } } +/* DEBUG: Enhanced keyframes for testing */ @keyframes shimmer { 0% { - background-position: -200px 0, 0 0; + background-position: -100% 0; } 100% { - background-position: calc(200px + 100%) 0, 0 0; + background-position: 100% 0; } } @@ -332,3 +348,4 @@ defineExpose({ animation: fadeInScale var(--transition-duration, 300ms) ease-out; } + diff --git a/src/modules/market/components/ProductCard.vue b/src/modules/market/components/ProductCard.vue index 3d2ceb8..c570930 100644 --- a/src/modules/market/components/ProductCard.vue +++ b/src/modules/market/components/ProductCard.vue @@ -2,18 +2,27 @@
+ + + +
+
+ + No image available +
+