- 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.
166 lines
No EOL
4.5 KiB
Vue
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> |