Add stall view and product detail dialog in market module
- Introduced a new route for viewing individual stalls, allowing users to navigate to a specific stall's page. - Created a ProductDetailDialog component to display detailed information about products, including images, descriptions, and stock status. - Updated MarketPage to handle stall navigation and integrate the new dialog for product details. These enhancements improve the user experience by providing more detailed product information and easier navigation within the market module.
This commit is contained in:
parent
f2080abce5
commit
86d3133978
4 changed files with 601 additions and 0 deletions
319
src/modules/market/views/StallView.vue
Normal file
319
src/modules/market/views/StallView.vue
Normal file
|
|
@ -0,0 +1,319 @@
|
|||
<template>
|
||||
<div class="container mx-auto px-4 py-6">
|
||||
<!-- Stall Header -->
|
||||
<div class="mb-8">
|
||||
<!-- Back to Market Button -->
|
||||
<Button
|
||||
@click="goBackToMarket"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="mb-4"
|
||||
>
|
||||
<ArrowLeft class="w-4 h-4 mr-2" />
|
||||
Back to Market
|
||||
</Button>
|
||||
|
||||
<!-- Stall Info Card -->
|
||||
<Card class="overflow-hidden">
|
||||
<div class="relative h-48 bg-gradient-to-r from-blue-500 to-purple-600">
|
||||
<!-- Stall Banner/Logo -->
|
||||
<div class="absolute inset-0 flex items-center justify-center">
|
||||
<div v-if="stall?.logo" class="w-32 h-32 rounded-full bg-white p-2 shadow-lg">
|
||||
<img
|
||||
:src="stall.logo"
|
||||
:alt="stall.name"
|
||||
class="w-full h-full object-contain rounded-full"
|
||||
@error="handleLogoError"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="w-32 h-32 rounded-full bg-white/90 flex items-center justify-center shadow-lg">
|
||||
<Store class="w-16 h-16 text-gray-600" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CardContent class="pt-20 pb-6">
|
||||
<div class="text-center">
|
||||
<h1 class="text-3xl font-bold mb-2">{{ stall?.name || 'Unknown Stall' }}</h1>
|
||||
<p v-if="stall?.description" class="text-gray-600 mb-4 max-w-2xl mx-auto">
|
||||
{{ stall.description }}
|
||||
</p>
|
||||
|
||||
<!-- Stall Stats -->
|
||||
<div class="flex justify-center gap-8 mt-6">
|
||||
<div class="text-center">
|
||||
<div class="text-2xl font-bold text-blue-600">{{ productCount }}</div>
|
||||
<div class="text-sm text-gray-600">Products</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="text-2xl font-bold text-green-600">{{ stall?.currency || 'sats' }}</div>
|
||||
<div class="text-sm text-gray-600">Currency</div>
|
||||
</div>
|
||||
<div v-if="stall?.shipping?.length" class="text-center">
|
||||
<div class="text-2xl font-bold text-purple-600">{{ stall.shipping.length }}</div>
|
||||
<div class="text-sm text-gray-600">Shipping Zones</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Categories -->
|
||||
<div v-if="stallCategories.length > 0" class="mt-6">
|
||||
<div class="flex flex-wrap justify-center gap-2">
|
||||
<Badge
|
||||
v-for="category in stallCategories"
|
||||
:key="category"
|
||||
variant="secondary"
|
||||
class="cursor-pointer hover:bg-primary hover:text-primary-foreground transition-colors"
|
||||
@click="toggleCategoryFilter(category)"
|
||||
:class="{ 'bg-primary text-primary-foreground': selectedCategories.includes(category) }"
|
||||
>
|
||||
{{ category }}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<!-- Search and Filter Bar -->
|
||||
<div class="mb-6 flex flex-col sm:flex-row gap-4">
|
||||
<div class="flex-1">
|
||||
<div class="relative">
|
||||
<Search class="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
|
||||
<Input
|
||||
v-model="searchQuery"
|
||||
placeholder="Search products in this stall..."
|
||||
class="pl-10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<Select v-model="sortBy">
|
||||
<SelectTrigger class="w-[180px]">
|
||||
<SelectValue placeholder="Sort by..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="name">Name</SelectItem>
|
||||
<SelectItem value="price-asc">Price: Low to High</SelectItem>
|
||||
<SelectItem value="price-desc">Price: High to Low</SelectItem>
|
||||
<SelectItem value="newest">Newest First</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Button
|
||||
v-if="selectedCategories.length > 0"
|
||||
@click="clearFilters"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
>
|
||||
Clear Filters
|
||||
<X class="w-4 h-4 ml-1" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading State -->
|
||||
<div v-if="isLoading" class="flex justify-center items-center h-64">
|
||||
<Loader2 class="w-12 h-12 animate-spin text-primary" />
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div v-else-if="!filteredProducts.length" class="text-center py-12">
|
||||
<Package class="w-24 h-24 text-gray-300 mx-auto mb-4" />
|
||||
<h2 class="text-xl font-semibold mb-2">No Products Found</h2>
|
||||
<p class="text-gray-600">
|
||||
{{ searchQuery || selectedCategories.length > 0
|
||||
? 'Try adjusting your filters or search terms'
|
||||
: 'This stall doesn\'t have any products yet' }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Products Grid -->
|
||||
<div v-else class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
||||
<ProductCard
|
||||
v-for="product in filteredProducts"
|
||||
:key="product.id"
|
||||
:product="product"
|
||||
@view-details="viewProductDetails"
|
||||
@view-stall="viewStall"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Product Detail Dialog -->
|
||||
<ProductDetailDialog
|
||||
v-if="selectedProduct"
|
||||
:product="selectedProduct"
|
||||
:isOpen="showProductDetail"
|
||||
@close="closeProductDetail"
|
||||
@add-to-cart="handleAddToCart"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useMarketStore } from '@/modules/market/stores/market'
|
||||
import { Card, CardContent } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import { ArrowLeft, Store, Search, Package, Loader2, X } from 'lucide-vue-next'
|
||||
import ProductCard from '../components/ProductCard.vue'
|
||||
import ProductDetailDialog from '../components/ProductDetailDialog.vue'
|
||||
import type { Product, Stall } from '../types/market'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const marketStore = useMarketStore()
|
||||
|
||||
// State
|
||||
const isLoading = ref(false)
|
||||
const searchQuery = ref('')
|
||||
const sortBy = ref('name')
|
||||
const selectedCategories = ref<string[]>([])
|
||||
const showProductDetail = ref(false)
|
||||
const selectedProduct = ref<Product | null>(null)
|
||||
const logoError = ref(false)
|
||||
|
||||
// Get stall ID from route params
|
||||
const stallId = computed(() => route.params.stallId as string)
|
||||
|
||||
// Get stall data
|
||||
const stall = computed<Stall | undefined>(() => {
|
||||
return marketStore.stalls.find(s => s.id === stallId.value)
|
||||
})
|
||||
|
||||
// Get products for this stall
|
||||
const stallProducts = computed(() => {
|
||||
return marketStore.products.filter(p => p.stall_id === stallId.value)
|
||||
})
|
||||
|
||||
// Get unique categories for this stall
|
||||
const stallCategories = computed(() => {
|
||||
const categories = new Set<string>()
|
||||
stallProducts.value.forEach(product => {
|
||||
product.categories?.forEach(cat => categories.add(cat))
|
||||
})
|
||||
return Array.from(categories).sort()
|
||||
})
|
||||
|
||||
// Product count
|
||||
const productCount = computed(() => stallProducts.value.length)
|
||||
|
||||
// Filtered and sorted products
|
||||
const filteredProducts = computed(() => {
|
||||
let products = [...stallProducts.value]
|
||||
|
||||
// Filter by search query
|
||||
if (searchQuery.value) {
|
||||
const query = searchQuery.value.toLowerCase()
|
||||
products = products.filter(p =>
|
||||
p.name.toLowerCase().includes(query) ||
|
||||
p.description?.toLowerCase().includes(query)
|
||||
)
|
||||
}
|
||||
|
||||
// Filter by selected categories
|
||||
if (selectedCategories.value.length > 0) {
|
||||
products = products.filter(p =>
|
||||
p.categories?.some(cat => selectedCategories.value.includes(cat))
|
||||
)
|
||||
}
|
||||
|
||||
// Sort products
|
||||
switch (sortBy.value) {
|
||||
case 'price-asc':
|
||||
products.sort((a, b) => a.price - b.price)
|
||||
break
|
||||
case 'price-desc':
|
||||
products.sort((a, b) => b.price - a.price)
|
||||
break
|
||||
case 'newest':
|
||||
products.sort((a, b) => b.createdAt - a.createdAt)
|
||||
break
|
||||
case 'name':
|
||||
default:
|
||||
products.sort((a, b) => a.name.localeCompare(b.name))
|
||||
}
|
||||
|
||||
return products
|
||||
})
|
||||
|
||||
// Methods
|
||||
const goBackToMarket = () => {
|
||||
router.push('/market')
|
||||
}
|
||||
|
||||
const toggleCategoryFilter = (category: string) => {
|
||||
const index = selectedCategories.value.indexOf(category)
|
||||
if (index >= 0) {
|
||||
selectedCategories.value.splice(index, 1)
|
||||
} else {
|
||||
selectedCategories.value.push(category)
|
||||
}
|
||||
}
|
||||
|
||||
const clearFilters = () => {
|
||||
selectedCategories.value = []
|
||||
searchQuery.value = ''
|
||||
}
|
||||
|
||||
const viewProductDetails = (product: Product) => {
|
||||
selectedProduct.value = product
|
||||
showProductDetail.value = true
|
||||
}
|
||||
|
||||
const closeProductDetail = () => {
|
||||
showProductDetail.value = false
|
||||
selectedProduct.value = null
|
||||
}
|
||||
|
||||
const handleAddToCart = (product: Product, quantity: number) => {
|
||||
marketStore.addToStallCart(product, quantity)
|
||||
closeProductDetail()
|
||||
}
|
||||
|
||||
const viewStall = (otherStallId: string) => {
|
||||
if (otherStallId !== stallId.value) {
|
||||
router.push(`/market/stall/${otherStallId}`)
|
||||
}
|
||||
}
|
||||
|
||||
const handleLogoError = () => {
|
||||
logoError.value = true
|
||||
}
|
||||
|
||||
// Load stall data if needed
|
||||
onMounted(async () => {
|
||||
if (!stall.value) {
|
||||
isLoading.value = true
|
||||
// You might want to load the stall data here if it's not already loaded
|
||||
// For now, we'll assume it's already in the store
|
||||
setTimeout(() => {
|
||||
isLoading.value = false
|
||||
}, 1000)
|
||||
}
|
||||
})
|
||||
|
||||
// Watch for route changes to update the view
|
||||
watch(() => route.params.stallId, (newStallId) => {
|
||||
if (newStallId && newStallId !== stallId.value) {
|
||||
// Reset filters when navigating to a different stall
|
||||
clearFilters()
|
||||
closeProductDetail()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Add any custom styles here if needed */
|
||||
</style>
|
||||
Loading…
Add table
Add a link
Reference in a new issue