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:
padreug 2025-09-25 22:53:12 +02:00
parent f2080abce5
commit 86d3133978
4 changed files with 601 additions and 0 deletions

View 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>