feat: Implement market functionality with ProductCard, useMarket composable, and market store
- Add ProductCard.vue component for displaying product details, including image, name, description, price, and stock status. - Create useMarket.ts composable to manage market loading, data fetching, and real-time updates from Nostr. - Introduce market.ts store to handle market, stall, product, and order states, along with filtering and sorting capabilities. - Develop Market.vue page to present market content, including loading states, error handling, and product grid. - Update router to include a new market route for user navigation.
This commit is contained in:
parent
2fc87fa032
commit
4d3d69f527
6 changed files with 1079 additions and 1 deletions
146
src/components/market/ProductCard.vue
Normal file
146
src/components/market/ProductCard.vue
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
<template>
|
||||
<Card class="overflow-hidden hover:shadow-lg transition-shadow duration-200">
|
||||
<!-- Product Image -->
|
||||
<div class="relative">
|
||||
<img
|
||||
:src="product.images?.[0] || '/placeholder-product.png'"
|
||||
:alt="product.name"
|
||||
class="w-full h-48 object-cover"
|
||||
@error="handleImageError"
|
||||
/>
|
||||
|
||||
<!-- Add to Cart Button -->
|
||||
<Button
|
||||
@click="$emit('add-to-cart', product)"
|
||||
:disabled="product.quantity < 1"
|
||||
size="sm"
|
||||
class="absolute top-2 right-2 bg-blue-600 hover:bg-blue-700 text-white"
|
||||
>
|
||||
<ShoppingCartIcon 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 { ShoppingCartIcon } from '@heroicons/vue/24/outline'
|
||||
import type { Product } from '@/stores/market'
|
||||
|
||||
interface Props {
|
||||
product: Product
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
|
||||
defineEmits<{
|
||||
'add-to-cart': [product: Product]
|
||||
'view-details': [product: Product]
|
||||
'view-stall': [stallId: string]
|
||||
}>()
|
||||
|
||||
const imageError = ref(false)
|
||||
|
||||
const handleImageError = () => {
|
||||
imageError.value = true
|
||||
}
|
||||
|
||||
const formatPrice = (price: number, currency: string) => {
|
||||
if (currency === 'sat') {
|
||||
return `${price} 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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue