feat: enhance ProgressiveImage component with dynamic error handling and improved no-image state

- Updated the ProgressiveImage component to display a dynamic error message based on the image availability, including a specific message for no-image scenarios.
- Modified ProductCard, ProductDetailDialog, and CheckoutPage components to utilize the enhanced ProgressiveImage, ensuring a consistent user experience when images are unavailable.
- Improved visual feedback by adding a placeholder for missing images across various components.

These changes enhance the user experience by providing clearer messaging and visual cues when images fail to load or are not available.

refactor: update background colors and gradients for theme-aware semantic styling consistency across components

- Changed background colors in ProgressiveImage, ProductCard, ProductDetailDialog, and CheckoutPage components to utilize theme-aware colors instead of fixed values.
- Enhanced gradient backgrounds to ensure better visual integration with the overall theme, improving the user interface and experience.

These updates promote a more cohesive design and improve maintainability by leveraging theme variables.
This commit is contained in:
padreug 2025-09-27 19:14:07 +02:00
parent fae19436b1
commit fe9fbe201b
4 changed files with 72 additions and 39 deletions

View file

@ -3,7 +3,10 @@
<!-- Blur placeholder background --> <!-- Blur placeholder background -->
<div v-if="!isLoaded" <div v-if="!isLoaded"
:class="['progressive-image-placeholder', { 'shimmer-active': !isLoaded && !hasError }]" :class="['progressive-image-placeholder', { 'shimmer-active': !isLoaded && !hasError }]"
:style="placeholderStyle" /> :style="placeholderStyle">
<!-- Branded shimmer effect using semantic colors -->
<div v-if="!isLoaded && !hasError" class="absolute inset-0 bg-gradient-to-r from-accent/40 via-primary/40 via-accent/70 to-accent/50 animate-pulse"></div>
</div>
<!-- Main image --> <!-- Main image -->
<img ref="imageRef" :src="src" :alt="alt" :class="[ <img ref="imageRef" :src="src" :alt="alt" :class="[
@ -14,19 +17,19 @@
'progressive-image-loaded': isLoaded, 'progressive-image-loaded': isLoaded,
'progressive-image-error': hasError 'progressive-image-error': hasError
} }
]" :loading="loading" @load="handleLoad" @error="handleError" /> ]" :loading="loading" @load="handleLoad" @error="handleError" /> -->
<!-- Loading indicator (optional) --> <!-- Loading indicator (optional) -->
<div v-if="showLoadingIndicator && !isLoaded && !hasError" class="progressive-image-loading-indicator"> <div v-if="showLoadingIndicator && !isLoaded && !hasError" class="progressive-image-loading-indicator">
<div class="progressive-image-spinner" /> <div class="progressive-image-spinner" />
</div> </div>
<!-- Error state (optional) --> <!-- Error or no image state (optional) -->
<div v-if="hasError && showErrorState" class="progressive-image-error-state"> <div v-if="hasError && showErrorState" class="progressive-image-error-state">
<slot name="error"> <slot name="error">
<div class="progressive-image-error-content"> <div class="progressive-image-error-content">
<Package class="w-6 h-6 text-muted-foreground" /> <Package class="w-6 h-6 text-muted-foreground" />
<span class="text-xs text-muted-foreground">Failed to load</span> <span class="text-xs text-muted-foreground">{{ errorMessage }}</span>
</div> </div>
</slot> </slot>
</div> </div>
@ -109,7 +112,7 @@ interface Emits {
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
blurRadius: 10, blurRadius: 10,
backgroundColor: '#f3f4f6', backgroundColor: 'hsl(var(--muted))',
transitionDuration: 300, transitionDuration: 300,
loading: 'lazy', loading: 'lazy',
showLoadingIndicator: false, showLoadingIndicator: false,
@ -124,14 +127,29 @@ const isLoaded = ref(false)
const hasError = ref(false) const hasError = ref(false)
const isLoading = 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 // Computed styles
const placeholderStyle = computed(() => { const placeholderStyle = computed(() => {
const hasThumb = props.thumbnail && props.thumbnail.trim() 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 const gradientBg = hasThumb
? 'none' ? '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 { return {
'--blur-radius': `${props.blurRadius}px`, '--blur-radius': `${props.blurRadius}px`,
@ -208,24 +226,21 @@ defineExpose({
transition: opacity var(--transition-duration, 300ms) ease-out; 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 { .progressive-image-placeholder.shimmer-active {
background-image: position: relative;
linear-gradient( background: linear-gradient(135deg, hsl(var(--muted)) 0%, hsl(var(--muted) / 0.5) 50%, hsl(var(--muted)) 100%) !important;
90deg, overflow: hidden;
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;
} }
/* Accessibility: Respect user's reduced motion preference */
@media (prefers-reduced-motion: reduce) {
.shimmer-overlay {
animation: none !important;
}
}
.progressive-image { .progressive-image {
width: 100%; width: 100%;
height: 100%; height: 100%;
@ -281,7 +296,7 @@ defineExpose({
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: 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 { .progressive-image-error-content {
@ -307,12 +322,13 @@ defineExpose({
} }
} }
/* DEBUG: Enhanced keyframes for testing */
@keyframes shimmer { @keyframes shimmer {
0% { 0% {
background-position: -200px 0, 0 0; background-position: -100% 0;
} }
100% { 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; animation: fadeInScale var(--transition-duration, 300ms) ease-out;
} }
</style> </style>

View file

@ -2,12 +2,13 @@
<Card class="overflow-hidden hover:shadow-lg transition-shadow duration-200"> <Card class="overflow-hidden hover:shadow-lg transition-shadow duration-200">
<!-- Product Image --> <!-- Product Image -->
<div class="relative"> <div class="relative">
<!-- Show actual image if available -->
<ProgressiveImage <ProgressiveImage
:src="product.images?.[0] || '/placeholder-product.png'" v-if="product.images?.[0]"
:src="product.images[0]"
:alt="product.name" :alt="product.name"
container-class="w-full h-48 bg-gray-100" container-class="w-full h-48 bg-muted/50"
image-class="w-full h-48 object-cover" image-class="w-full h-48 object-cover"
:background-color="'#f3f4f6'"
:blur-radius="8" :blur-radius="8"
:transition-duration="400" :transition-duration="400"
loading="lazy" loading="lazy"
@ -15,6 +16,14 @@
@error="handleImageError" @error="handleImageError"
/> />
<!-- Show placeholder when no image -->
<div v-else class="w-full h-48 bg-gradient-to-br from-muted/50 to-muted flex items-center justify-center">
<div class="text-center">
<Package class="w-12 h-12 mx-auto text-muted-foreground mb-2" />
<span class="text-xs text-muted-foreground">No image available</span>
</div>
</div>
<!-- Add to Cart Button --> <!-- Add to Cart Button -->
<Button <Button
@click="addToCart" @click="addToCart"
@ -111,7 +120,7 @@ import { Card, CardContent, CardFooter, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge' import { Badge } from '@/components/ui/badge'
import ProgressiveImage from '@/components/ui/ProgressiveImage.vue' import ProgressiveImage from '@/components/ui/ProgressiveImage.vue'
import { ShoppingCart } from 'lucide-vue-next' import { ShoppingCart, Package } from 'lucide-vue-next'
import type { Product } from '@/modules/market/stores/market' import type { Product } from '@/modules/market/stores/market'
interface Props { interface Props {

View file

@ -18,15 +18,17 @@
:alt="product.name" :alt="product.name"
container-class="w-full h-full" container-class="w-full h-full"
image-class="w-full h-full object-cover" image-class="w-full h-full object-cover"
:background-color="'#f3f4f6'"
:blur-radius="12" :blur-radius="12"
:transition-duration="500" :transition-duration="500"
loading="lazy" loading="lazy"
:show-loading-indicator="true" :show-loading-indicator="true"
@error="handleImageError" @error="handleImageError"
/> />
<div v-else class="w-full h-full flex items-center justify-center"> <div v-else class="w-full h-full bg-gradient-to-br from-muted/50 to-muted flex items-center justify-center">
<Package class="w-24 h-24 text-gray-300" /> <div class="text-center">
<Package class="w-24 h-24 mx-auto text-muted-foreground mb-4" />
<span class="text-sm text-muted-foreground">No image available</span>
</div>
</div> </div>
</div> </div>
@ -44,7 +46,6 @@
:alt="`${product.name} - Image ${index + 1}`" :alt="`${product.name} - Image ${index + 1}`"
container-class="w-full h-full" container-class="w-full h-full"
image-class="w-full h-full object-cover" image-class="w-full h-full object-cover"
:background-color="'#f3f4f6'"
:blur-radius="6" :blur-radius="6"
:transition-duration="300" :transition-duration="300"
loading="lazy" loading="lazy"

View file

@ -54,14 +54,19 @@
<div class="flex items-center space-x-4"> <div class="flex items-center space-x-4">
<!-- Product Image --> <!-- Product Image -->
<div class="w-16 h-16 bg-muted rounded-lg flex items-center justify-center"> <div class="w-16 h-16 bg-muted rounded-lg flex items-center justify-center">
<img <ProgressiveImage
v-if="item.product.images?.[0]" v-if="item.product.images?.[0]"
:src="item.product.images[0]" :src="item.product.images[0]"
:alt="item.product.name" :alt="item.product.name"
class="w-full h-full object-cover rounded-lg" container-class="w-full h-full"
image-class="w-full h-full object-cover rounded-lg"
:blur-radius="4"
:transition-duration="300"
loading="lazy" loading="lazy"
/> />
<Package v-else class="w-8 h-8 text-muted-foreground" /> <div v-else class="w-full h-full flex items-center justify-center">
<Package class="w-8 h-8 text-muted-foreground" />
</div>
</div> </div>
<!-- Product Details --> <!-- Product Details -->
@ -285,6 +290,7 @@ import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Textarea } from '@/components/ui/textarea' import { Textarea } from '@/components/ui/textarea'
import { Label } from '@/components/ui/label' import { Label } from '@/components/ui/label'
import ProgressiveImage from '@/components/ui/ProgressiveImage.vue'
import { import {
Package, Package,
CheckCircle CheckCircle