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:
parent
fae19436b1
commit
fe9fbe201b
4 changed files with 72 additions and 39 deletions
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,18 +2,27 @@
|
||||||
<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"
|
||||||
:show-loading-indicator="false"
|
:show-loading-indicator="false"
|
||||||
@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
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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,9 +290,10 @@ 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 {
|
import ProgressiveImage from '@/components/ui/ProgressiveImage.vue'
|
||||||
Package,
|
import {
|
||||||
CheckCircle
|
Package,
|
||||||
|
CheckCircle
|
||||||
} from 'lucide-vue-next'
|
} from 'lucide-vue-next'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue