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
369
src/stores/market.ts
Normal file
369
src/stores/market.ts
Normal file
|
|
@ -0,0 +1,369 @@
|
|||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import { config } from '@/lib/config'
|
||||
|
||||
// Types
|
||||
export interface Market {
|
||||
d: string
|
||||
pubkey: string
|
||||
relays: string[]
|
||||
selected: boolean
|
||||
opts: {
|
||||
name: string
|
||||
description?: string
|
||||
logo?: string
|
||||
banner?: string
|
||||
merchants: string[]
|
||||
ui: Record<string, any>
|
||||
}
|
||||
}
|
||||
|
||||
export interface Stall {
|
||||
id: string
|
||||
pubkey: string
|
||||
name: string
|
||||
description?: string
|
||||
logo?: string
|
||||
categories?: string[]
|
||||
shipping?: Record<string, any>
|
||||
}
|
||||
|
||||
export interface Product {
|
||||
id: string
|
||||
stall_id: string
|
||||
stallName: string
|
||||
name: string
|
||||
description?: string
|
||||
price: number
|
||||
currency: string
|
||||
quantity: number
|
||||
images?: string[]
|
||||
categories?: string[]
|
||||
createdAt: number
|
||||
updatedAt: number
|
||||
}
|
||||
|
||||
export interface Order {
|
||||
id: string
|
||||
stall_id: string
|
||||
product_id: string
|
||||
buyer_pubkey: string
|
||||
seller_pubkey: string
|
||||
quantity: number
|
||||
total_price: number
|
||||
currency: string
|
||||
status: 'pending' | 'paid' | 'shipped' | 'delivered' | 'cancelled'
|
||||
payment_request?: string
|
||||
created_at: number
|
||||
updated_at: number
|
||||
}
|
||||
|
||||
export interface FilterData {
|
||||
categories: string[]
|
||||
merchants: string[]
|
||||
stalls: string[]
|
||||
currency: string | null
|
||||
priceFrom: number | null
|
||||
priceTo: number | null
|
||||
}
|
||||
|
||||
export interface SortOptions {
|
||||
field: string
|
||||
order: 'asc' | 'desc'
|
||||
}
|
||||
|
||||
export const useMarketStore = defineStore('market', () => {
|
||||
// Core market state
|
||||
const markets = ref<Market[]>([])
|
||||
const stalls = ref<Stall[]>([])
|
||||
const products = ref<Product[]>([])
|
||||
const orders = ref<Record<string, Order>>({})
|
||||
const profiles = ref<Record<string, any>>({})
|
||||
|
||||
// Active selections
|
||||
const activeMarket = ref<Market | null>(null)
|
||||
const activeStall = ref<Stall | null>(null)
|
||||
const activeProduct = ref<Product | null>(null)
|
||||
|
||||
// UI state
|
||||
const isLoading = ref(false)
|
||||
const searchText = ref('')
|
||||
const showFilterDetails = ref(false)
|
||||
|
||||
// Filtering and sorting
|
||||
const filterData = ref<FilterData>({
|
||||
categories: [],
|
||||
merchants: [],
|
||||
stalls: [],
|
||||
currency: null,
|
||||
priceFrom: null,
|
||||
priceTo: null
|
||||
})
|
||||
|
||||
const sortOptions = ref<SortOptions>({
|
||||
field: 'name',
|
||||
order: 'asc'
|
||||
})
|
||||
|
||||
// Shopping cart
|
||||
const shoppingCart = ref<Record<string, { product: Product; quantity: number }>>({})
|
||||
|
||||
// Computed properties
|
||||
const filteredProducts = computed(() => {
|
||||
let filtered = products.value
|
||||
|
||||
// Search filter
|
||||
if (searchText.value) {
|
||||
const searchLower = searchText.value.toLowerCase()
|
||||
filtered = filtered.filter(product =>
|
||||
product.name.toLowerCase().includes(searchLower) ||
|
||||
product.description?.toLowerCase().includes(searchLower) ||
|
||||
product.stallName.toLowerCase().includes(searchLower)
|
||||
)
|
||||
}
|
||||
|
||||
// Category filter
|
||||
if (filterData.value.categories.length > 0) {
|
||||
filtered = filtered.filter(product =>
|
||||
product.categories?.some(cat => filterData.value.categories.includes(cat))
|
||||
)
|
||||
}
|
||||
|
||||
// Merchant filter
|
||||
if (filterData.value.merchants.length > 0) {
|
||||
filtered = filtered.filter(product =>
|
||||
filterData.value.merchants.includes(product.stall_id)
|
||||
)
|
||||
}
|
||||
|
||||
// Stall filter
|
||||
if (filterData.value.stalls.length > 0) {
|
||||
filtered = filtered.filter(product =>
|
||||
filterData.value.stalls.includes(product.stall_id)
|
||||
)
|
||||
}
|
||||
|
||||
// Currency filter
|
||||
if (filterData.value.currency) {
|
||||
filtered = filtered.filter(product =>
|
||||
product.currency === filterData.value.currency
|
||||
)
|
||||
}
|
||||
|
||||
// Price range filter
|
||||
if (filterData.value.priceFrom !== null) {
|
||||
filtered = filtered.filter(product =>
|
||||
product.price >= filterData.value.priceFrom!
|
||||
)
|
||||
}
|
||||
|
||||
if (filterData.value.priceTo !== null) {
|
||||
filtered = filtered.filter(product =>
|
||||
product.price <= filterData.value.priceTo!
|
||||
)
|
||||
}
|
||||
|
||||
// Sort
|
||||
filtered.sort((a, b) => {
|
||||
const aVal = a[sortOptions.value.field as keyof Product]
|
||||
const bVal = b[sortOptions.value.field as keyof Product]
|
||||
|
||||
if (typeof aVal === 'string' && typeof bVal === 'string') {
|
||||
return sortOptions.value.order === 'asc'
|
||||
? aVal.localeCompare(bVal)
|
||||
: bVal.localeCompare(aVal)
|
||||
}
|
||||
|
||||
if (typeof aVal === 'number' && typeof bVal === 'number') {
|
||||
return sortOptions.value.order === 'asc'
|
||||
? aVal - bVal
|
||||
: bVal - aVal
|
||||
}
|
||||
|
||||
return 0
|
||||
})
|
||||
|
||||
return filtered
|
||||
})
|
||||
|
||||
const allCategories = computed(() => {
|
||||
const categories = new Set<string>()
|
||||
products.value.forEach(product => {
|
||||
product.categories?.forEach(cat => categories.add(cat))
|
||||
})
|
||||
return Array.from(categories).map(category => ({
|
||||
category,
|
||||
count: products.value.filter(p => p.categories?.includes(category)).length,
|
||||
selected: filterData.value.categories.includes(category)
|
||||
}))
|
||||
})
|
||||
|
||||
const cartTotal = computed(() => {
|
||||
return Object.values(shoppingCart.value).reduce((total, item) => {
|
||||
return total + (item.product.price * item.quantity)
|
||||
}, 0)
|
||||
})
|
||||
|
||||
const cartItemCount = computed(() => {
|
||||
return Object.values(shoppingCart.value).reduce((count, item) => {
|
||||
return count + item.quantity
|
||||
}, 0)
|
||||
})
|
||||
|
||||
// Actions
|
||||
const setLoading = (loading: boolean) => {
|
||||
isLoading.value = loading
|
||||
}
|
||||
|
||||
const setSearchText = (text: string) => {
|
||||
searchText.value = text
|
||||
}
|
||||
|
||||
const setActiveMarket = (market: Market | null) => {
|
||||
activeMarket.value = market
|
||||
}
|
||||
|
||||
const setActiveStall = (stall: Stall | null) => {
|
||||
activeStall.value = stall
|
||||
}
|
||||
|
||||
const setActiveProduct = (product: Product | null) => {
|
||||
activeProduct.value = product
|
||||
}
|
||||
|
||||
const addProduct = (product: Product) => {
|
||||
const existingIndex = products.value.findIndex(p => p.id === product.id)
|
||||
if (existingIndex >= 0) {
|
||||
products.value[existingIndex] = product
|
||||
} else {
|
||||
products.value.push(product)
|
||||
}
|
||||
}
|
||||
|
||||
const addStall = (stall: Stall) => {
|
||||
const existingIndex = stalls.value.findIndex(s => s.id === stall.id)
|
||||
if (existingIndex >= 0) {
|
||||
stalls.value[existingIndex] = stall
|
||||
} else {
|
||||
stalls.value.push(stall)
|
||||
}
|
||||
}
|
||||
|
||||
const addMarket = (market: Market) => {
|
||||
const existingIndex = markets.value.findIndex(m => m.d === market.d)
|
||||
if (existingIndex >= 0) {
|
||||
markets.value[existingIndex] = market
|
||||
} else {
|
||||
markets.value.push(market)
|
||||
}
|
||||
}
|
||||
|
||||
const addToCart = (product: Product, quantity: number = 1) => {
|
||||
const existing = shoppingCart.value[product.id]
|
||||
if (existing) {
|
||||
existing.quantity += quantity
|
||||
} else {
|
||||
shoppingCart.value[product.id] = { product, quantity }
|
||||
}
|
||||
}
|
||||
|
||||
const removeFromCart = (productId: string) => {
|
||||
delete shoppingCart.value[productId]
|
||||
}
|
||||
|
||||
const updateCartQuantity = (productId: string, quantity: number) => {
|
||||
if (quantity <= 0) {
|
||||
removeFromCart(productId)
|
||||
} else {
|
||||
const item = shoppingCart.value[productId]
|
||||
if (item) {
|
||||
item.quantity = quantity
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const clearCart = () => {
|
||||
shoppingCart.value = {}
|
||||
}
|
||||
|
||||
const updateFilterData = (newFilterData: Partial<FilterData>) => {
|
||||
filterData.value = { ...filterData.value, ...newFilterData }
|
||||
}
|
||||
|
||||
const clearFilters = () => {
|
||||
filterData.value = {
|
||||
categories: [],
|
||||
merchants: [],
|
||||
stalls: [],
|
||||
currency: null,
|
||||
priceFrom: null,
|
||||
priceTo: null
|
||||
}
|
||||
}
|
||||
|
||||
const toggleCategoryFilter = (category: string) => {
|
||||
const index = filterData.value.categories.indexOf(category)
|
||||
if (index >= 0) {
|
||||
filterData.value.categories.splice(index, 1)
|
||||
} else {
|
||||
filterData.value.categories.push(category)
|
||||
}
|
||||
}
|
||||
|
||||
const updateSortOptions = (field: string, order: 'asc' | 'desc' = 'asc') => {
|
||||
sortOptions.value = { field, order }
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
return {
|
||||
// State
|
||||
markets: readonly(markets),
|
||||
stalls: readonly(stalls),
|
||||
products: readonly(products),
|
||||
orders: readonly(orders),
|
||||
profiles: readonly(profiles),
|
||||
activeMarket: readonly(activeMarket),
|
||||
activeStall: readonly(activeStall),
|
||||
activeProduct: readonly(activeProduct),
|
||||
isLoading: readonly(isLoading),
|
||||
searchText: readonly(searchText),
|
||||
showFilterDetails: readonly(showFilterDetails),
|
||||
filterData: readonly(filterData),
|
||||
sortOptions: readonly(sortOptions),
|
||||
shoppingCart: readonly(shoppingCart),
|
||||
|
||||
// Computed
|
||||
filteredProducts,
|
||||
allCategories,
|
||||
cartTotal,
|
||||
cartItemCount,
|
||||
|
||||
// Actions
|
||||
setLoading,
|
||||
setSearchText,
|
||||
setActiveMarket,
|
||||
setActiveStall,
|
||||
setActiveProduct,
|
||||
addProduct,
|
||||
addStall,
|
||||
addMarket,
|
||||
addToCart,
|
||||
removeFromCart,
|
||||
updateCartQuantity,
|
||||
clearCart,
|
||||
updateFilterData,
|
||||
clearFilters,
|
||||
toggleCategoryFilter,
|
||||
updateSortOptions,
|
||||
formatPrice
|
||||
}
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue