- Removed direct store dependency from ProductCard.vue, replacing it with event emission for adding products to the cart. - Enhanced ProductGrid.vue to manage product detail dialog internally, improving user interaction and state handling. - Streamlined MarketPage.vue by removing redundant product detail dialog logic, focusing on cleaner component structure. - Updated event handling for adding products to the cart with quantity support, enhancing flexibility in product management. These changes improve the modularity and maintainability of the market components, providing a better user experience when interacting with products.
310 lines
No EOL
10 KiB
Vue
310 lines
No EOL
10 KiB
Vue
<template>
|
|
<div class="container mx-auto px-4 py-3 sm:py-6">
|
|
<!-- Stall Header -->
|
|
<div class="mb-4 sm:mb-8">
|
|
<!-- Back to Market Button -->
|
|
<Button
|
|
@click="goBackToMarket"
|
|
variant="ghost"
|
|
size="sm"
|
|
class="mb-2 sm:mb-4"
|
|
>
|
|
<ArrowLeft class="w-4 h-4 mr-2" />
|
|
Back to Market
|
|
</Button>
|
|
|
|
<!-- Compact Stall Info Card -->
|
|
<Card class="relative overflow-hidden border-l-4 border-l-primary/60 bg-gradient-to-r from-primary/5 via-background to-accent/5 shadow-md">
|
|
<div class="relative p-3 sm:p-4">
|
|
<div class="flex items-start gap-3">
|
|
<!-- Stall Logo (Enhanced) -->
|
|
<div class="flex-shrink-0">
|
|
<div v-if="stall?.logo" class="w-12 h-12 sm:w-14 sm:h-14 rounded-xl bg-gradient-to-br from-primary/20 to-accent/20 border-2 border-primary/20 shadow-lg overflow-hidden ring-2 ring-primary/10">
|
|
<img
|
|
:src="stall.logo"
|
|
:alt="stall.name"
|
|
class="w-full h-full object-cover"
|
|
@error="handleLogoError"
|
|
/>
|
|
</div>
|
|
<div v-else class="w-12 h-12 sm:w-14 sm:h-14 rounded-xl bg-gradient-to-br from-primary/20 to-accent/20 flex items-center justify-center shadow-lg ring-2 ring-primary/10 border-2 border-primary/20">
|
|
<Store class="w-6 h-6 sm:w-7 sm:h-7 text-primary" />
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Stall Info -->
|
|
<div class="flex-1 min-w-0">
|
|
<!-- Title and Description -->
|
|
<div class="mb-2">
|
|
<h1 class="text-lg sm:text-xl font-bold bg-gradient-to-r from-foreground to-foreground/80 bg-clip-text text-transparent truncate">{{ stall?.name || 'Unknown Stall' }}</h1>
|
|
<p v-if="stall?.description" class="text-sm text-muted-foreground/90 line-clamp-2 mt-1">
|
|
{{ stall.description }}
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Enhanced Stats Row -->
|
|
<div class="flex flex-wrap gap-2 mb-3">
|
|
<Badge class="text-xs font-medium bg-gradient-to-r from-primary to-primary/80 text-primary-foreground shadow-sm hover:shadow-md transition-shadow">
|
|
<span class="font-bold">{{ productCount }}</span>
|
|
<span class="ml-1 opacity-90">Products</span>
|
|
</Badge>
|
|
<Badge class="text-xs font-medium bg-gradient-to-r from-accent to-accent/80 text-accent-foreground shadow-sm">
|
|
<span class="font-bold">{{ stall?.currency || 'sats' }}</span>
|
|
<span class="ml-1 opacity-90">Currency</span>
|
|
</Badge>
|
|
<Badge v-if="stall?.shipping?.length" class="text-xs font-medium bg-gradient-to-r from-green-500 to-green-600 text-white shadow-sm">
|
|
<span class="font-bold">{{ stall.shipping.length }}</span>
|
|
<span class="ml-1 opacity-90">Shipping</span>
|
|
</Badge>
|
|
</div>
|
|
|
|
<!-- Categories (Enhanced) -->
|
|
<div v-if="stallCategories.length > 0" class="flex flex-wrap gap-1">
|
|
<Badge
|
|
v-for="category in stallCategories"
|
|
:key="category"
|
|
:variant="selectedCategories.includes(category) ? 'default' : 'secondary'"
|
|
class="text-xs px-2 py-0.5 cursor-pointer transition-all duration-200 hover:scale-105 hover:shadow-sm"
|
|
:class="{
|
|
'bg-gradient-to-r from-primary to-primary/90 text-primary-foreground shadow-md': selectedCategories.includes(category),
|
|
'hover:bg-primary/10 hover:border-primary/50': !selectedCategories.includes(category)
|
|
}"
|
|
@click="toggleCategoryFilter(category)"
|
|
>
|
|
{{ category }}
|
|
</Badge>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
|
|
<!-- Search and Filter Bar -->
|
|
<div class="mb-4 sm:mb-6 flex flex-col sm:flex-row gap-2 sm:gap-4">
|
|
<div class="flex-1">
|
|
<FuzzySearch
|
|
:data="stallProducts"
|
|
:options="searchOptions"
|
|
placeholder="Search products in this stall..."
|
|
@results="handleSearchResults"
|
|
class="w-full"
|
|
/>
|
|
</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>
|
|
|
|
<!-- Products Grid with Loading and Empty States -->
|
|
<ProductGrid
|
|
:products="filteredProducts as Product[]"
|
|
:is-loading="isLoading"
|
|
loading-message="Loading stall products..."
|
|
empty-title="No Products Found"
|
|
:empty-message="searchQuery || selectedCategories.length > 0
|
|
? 'Try adjusting your filters or search terms'
|
|
: 'This stall doesn\'t have any products yet'"
|
|
@view-stall="viewStall"
|
|
@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 } from '@/components/ui/card'
|
|
import { Button } from '@/components/ui/button'
|
|
import { Badge } from '@/components/ui/badge'
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from '@/components/ui/select'
|
|
import { ArrowLeft, Store, X } from 'lucide-vue-next'
|
|
import FuzzySearch from '@/components/ui/fuzzy-search/FuzzySearch.vue'
|
|
import ProductGrid from '../components/ProductGrid.vue'
|
|
import type { Product, Stall } from '../types/market'
|
|
import type { FuzzySearchOptions } from '@/composables/useFuzzySearch'
|
|
|
|
const route = useRoute()
|
|
const router = useRouter()
|
|
const marketStore = useMarketStore()
|
|
|
|
// State
|
|
const isLoading = ref(false)
|
|
const searchResults = ref<Product[]>([])
|
|
const searchQuery = ref('')
|
|
const sortBy = ref('name')
|
|
const selectedCategories = ref<string[]>([])
|
|
const logoError = ref(false)
|
|
|
|
// Fuzzy search configuration for stall products
|
|
const searchOptions: FuzzySearchOptions<Product> = {
|
|
fuseOptions: {
|
|
keys: [
|
|
{ name: 'name', weight: 0.8 }, // Product name has highest weight in stall view
|
|
{ name: 'description', weight: 0.4 }, // Description is important for specific product search
|
|
{ name: 'categories', weight: 0.3 } // Categories for filtering within stall
|
|
],
|
|
threshold: 0.2, // More strict matching since we're within a single stall
|
|
ignoreLocation: true,
|
|
findAllMatches: true,
|
|
minMatchCharLength: 2,
|
|
shouldSort: true
|
|
},
|
|
resultLimit: 100, // Less restrictive limit for stall view
|
|
minSearchLength: 2,
|
|
matchAllWhenSearchEmpty: true
|
|
}
|
|
|
|
// Get stall ID from route params
|
|
const stallId = computed(() => route.params.stallId as string)
|
|
|
|
// Get stall data
|
|
const stall = computed(() => {
|
|
return marketStore.stalls.find(s => s.id === stallId.value) as Stall | undefined
|
|
})
|
|
|
|
// 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 (using fuzzy search results when available)
|
|
const filteredProducts = computed(() => {
|
|
// Use search results if available, otherwise use all stall products
|
|
let products = searchResults.value.length > 0 || searchResults.value.length === 0
|
|
? [...searchResults.value]
|
|
: [...stallProducts.value]
|
|
|
|
// 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 = []
|
|
searchResults.value = []
|
|
searchQuery.value = ''
|
|
}
|
|
|
|
// Handle fuzzy search results
|
|
const handleSearchResults = (results: Product[]) => {
|
|
searchResults.value = results
|
|
// Extract search query from fuzzy search component if needed
|
|
// For now, we'll track it separately
|
|
}
|
|
|
|
const handleAddToCart = (product: Product, quantity?: number) => {
|
|
marketStore.addToStallCart(product, quantity || 1)
|
|
}
|
|
|
|
|
|
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()
|
|
}
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
/* Add any custom styles here if needed */
|
|
</style> |