web-app/src/modules/market/components/ProductCard.vue
padreug fe9fbe201b 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.
2025-09-27 21:20:36 +02:00

166 lines
No EOL
4.5 KiB
Vue

<template>
<Card class="overflow-hidden hover:shadow-lg transition-shadow duration-200">
<!-- Product Image -->
<div class="relative">
<!-- Show actual image if available -->
<ProgressiveImage
v-if="product.images?.[0]"
:src="product.images[0]"
:alt="product.name"
container-class="w-full h-48 bg-muted/50"
image-class="w-full h-48 object-cover"
:blur-radius="8"
:transition-duration="400"
loading="lazy"
:show-loading-indicator="false"
@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 -->
<Button
@click="addToCart"
:disabled="product.quantity < 1"
size="sm"
class="absolute top-2 right-2 bg-blue-600 hover:bg-blue-700 text-white"
>
<ShoppingCart class="w-4 h-4" />
</Button>
<!-- Out of Stock Badge -->
<Badge
v-if="product.quantity < 1"
variant="destructive"
class="absolute top-2 left-2"
>
Out of Stock
</Badge>
</div>
<CardContent class="p-4">
<!-- Product Name -->
<CardTitle class="text-lg font-semibold mb-2 line-clamp-2">
{{ product.name }}
</CardTitle>
<!-- Product Description -->
<p v-if="product.description" class="text-gray-600 text-sm mb-3 line-clamp-2">
{{ product.description }}
</p>
<!-- Price and Quantity -->
<div class="flex items-center justify-between mb-3">
<span class="text-xl font-bold text-green-600">
{{ formatPrice(product.price, product.currency) }}
</span>
<span class="text-sm text-gray-500">
{{ product.quantity }} left
</span>
</div>
<!-- Categories -->
<div v-if="product.categories && product.categories.length > 0" class="mb-3">
<div class="flex flex-wrap gap-1">
<Badge
v-for="category in product.categories.slice(0, 3)"
:key="category"
variant="secondary"
class="text-xs"
>
{{ category }}
</Badge>
<Badge
v-if="product.categories.length > 3"
variant="outline"
class="text-xs"
>
+{{ product.categories.length - 3 }} more
</Badge>
</div>
</div>
<!-- Stall Name -->
<div class="text-sm text-gray-500 mb-3">
{{ product.stallName }}
</div>
</CardContent>
<CardFooter class="p-4 pt-0">
<div class="flex w-full space-x-2">
<Button
variant="outline"
size="sm"
class="flex-1"
@click="$emit('view-stall', product.stall_id)"
>
Visit Stall
</Button>
<Button
size="sm"
class="flex-1"
@click="$emit('view-details', product)"
>
View Details
</Button>
</div>
</CardFooter>
</Card>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { Card, CardContent, CardFooter, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import ProgressiveImage from '@/components/ui/ProgressiveImage.vue'
import { ShoppingCart, Package } from 'lucide-vue-next'
import type { Product } from '@/modules/market/stores/market'
interface Props {
product: Product
}
const props = defineProps<Props>()
const emit = defineEmits<{
'add-to-cart': [product: Product]
'view-details': [product: Product]
'view-stall': [stallId: string]
}>()
const imageError = ref(false)
const addToCart = () => {
emit('add-to-cart', props.product)
}
const handleImageError = () => {
imageError.value = true
}
const formatPrice = (price: number, currency: string) => {
if (currency === 'sat' || currency === 'sats') {
return `${price.toLocaleString('en-US')} sats`
}
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: currency.toUpperCase()
}).format(price)
}
</script>
<style scoped>
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
</style>