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:
padreug 2025-08-02 16:50:25 +02:00
parent 2fc87fa032
commit 4d3d69f527
6 changed files with 1079 additions and 1 deletions

View file

@ -0,0 +1,367 @@
import { ref, computed, readonly } from 'vue'
import { useNostrStore } from '@/stores/nostr'
import { useMarketStore, type Market, type Stall, type Product } from '@/stores/market'
import { config } from '@/lib/config'
// Nostr event kinds for market functionality
const MARKET_EVENT_KINDS = {
MARKET: 30019,
STALL: 30017,
PRODUCT: 30018,
ORDER: 30020
} as const
export function useMarket() {
const nostrStore = useNostrStore()
const marketStore = useMarketStore()
const isLoading = ref(false)
const error = ref<string | null>(null)
const isConnected = ref(false)
// Market loading state
const loadMarket = async (naddr: string) => {
try {
isLoading.value = true
error.value = null
// Decode naddr
const { type, data } = window.NostrTools.nip19.decode(naddr)
if (type !== 'naddr' || data.kind !== MARKET_EVENT_KINDS.MARKET) {
throw new Error('Invalid market naddr')
}
// Load market data from Nostr
await loadMarketData(data)
} catch (err) {
error.value = err instanceof Error ? err.message : 'Failed to load market'
throw err
} finally {
isLoading.value = false
}
}
const loadMarketData = async (marketData: any) => {
try {
// Get Nostr client
const client = nostrStore.getClient()
// Load market configuration
await loadMarketConfig(marketData)
// Load stalls for this market
await loadStalls(marketData.pubkey)
// Load products for all stalls
await loadProducts()
// Subscribe to real-time updates
subscribeToMarketUpdates()
} catch (err) {
console.error('Error loading market data:', err)
throw err
}
}
const loadMarketConfig = async (marketData: any) => {
try {
const client = nostrStore.getClient()
// Fetch market configuration event
const filters = [{
kinds: [MARKET_EVENT_KINDS.MARKET],
authors: [marketData.pubkey],
'#d': [marketData.d]
}]
const events = await client.fetchNotes({ filters })
if (events.length > 0) {
const marketEvent = events[0]
const market: Market = {
d: marketData.d,
pubkey: marketData.pubkey,
relays: config.market.supportedRelays,
selected: true,
opts: JSON.parse(marketEvent.content)
}
marketStore.addMarket(market)
marketStore.setActiveMarket(market)
}
} catch (err) {
console.error('Error loading market config:', err)
throw err
}
}
const loadStalls = async (marketPubkey: string) => {
try {
const client = nostrStore.getClient()
// Fetch stall events for this market
const filters = [{
kinds: [MARKET_EVENT_KINDS.STALL],
authors: [marketPubkey]
}]
const events = await client.fetchNotes({ filters })
events.forEach(event => {
try {
const stallData = JSON.parse(event.content)
const stall: Stall = {
id: event.id,
pubkey: event.pubkey,
name: stallData.name,
description: stallData.description,
logo: stallData.logo,
categories: stallData.categories,
shipping: stallData.shipping
}
marketStore.addStall(stall)
} catch (err) {
console.warn('Failed to parse stall event:', err)
}
})
} catch (err) {
console.error('Error loading stalls:', err)
throw err
}
}
const loadProducts = async () => {
try {
const client = nostrStore.getClient()
// Get all stall pubkeys
const stallPubkeys = marketStore.stalls.map(stall => stall.pubkey)
if (stallPubkeys.length === 0) return
// Fetch product events from all stalls
const filters = [{
kinds: [MARKET_EVENT_KINDS.PRODUCT],
authors: stallPubkeys
}]
const events = await client.fetchNotes({ filters })
events.forEach(event => {
try {
const productData = JSON.parse(event.content)
const stall = marketStore.stalls.find(s => s.pubkey === event.pubkey)
if (stall) {
const product: Product = {
id: event.id,
stall_id: stall.id,
stallName: stall.name,
name: productData.name,
description: productData.description,
price: productData.price,
currency: productData.currency,
quantity: productData.quantity,
images: productData.images,
categories: productData.categories,
createdAt: event.created_at,
updatedAt: event.created_at
}
marketStore.addProduct(product)
}
} catch (err) {
console.warn('Failed to parse product event:', err)
}
})
} catch (err) {
console.error('Error loading products:', err)
throw err
}
}
const subscribeToMarketUpdates = () => {
try {
const client = nostrStore.getClient()
// Subscribe to real-time market updates
const filters = [
{
kinds: [MARKET_EVENT_KINDS.STALL, MARKET_EVENT_KINDS.PRODUCT],
since: Math.floor(Date.now() / 1000)
}
]
const unsubscribe = client.subscribeToNotes((event) => {
handleMarketEvent(event)
}, filters)
// Store unsubscribe function for cleanup
return unsubscribe
} catch (err) {
console.error('Error subscribing to market updates:', err)
}
}
const handleMarketEvent = (event: any) => {
try {
switch (event.kind) {
case MARKET_EVENT_KINDS.STALL:
handleStallEvent(event)
break
case MARKET_EVENT_KINDS.PRODUCT:
handleProductEvent(event)
break
case MARKET_EVENT_KINDS.ORDER:
handleOrderEvent(event)
break
}
} catch (err) {
console.error('Error handling market event:', err)
}
}
const handleStallEvent = (event: any) => {
try {
const stallData = JSON.parse(event.content)
const stall: Stall = {
id: event.id,
pubkey: event.pubkey,
name: stallData.name,
description: stallData.description,
logo: stallData.logo,
categories: stallData.categories,
shipping: stallData.shipping
}
marketStore.addStall(stall)
} catch (err) {
console.warn('Failed to parse stall event:', err)
}
}
const handleProductEvent = (event: any) => {
try {
const productData = JSON.parse(event.content)
const stall = marketStore.stalls.find(s => s.pubkey === event.pubkey)
if (stall) {
const product: Product = {
id: event.id,
stall_id: stall.id,
stallName: stall.name,
name: productData.name,
description: productData.description,
price: productData.price,
currency: productData.currency,
quantity: productData.quantity,
images: productData.images,
categories: productData.categories,
createdAt: event.created_at,
updatedAt: event.created_at
}
marketStore.addProduct(product)
}
} catch (err) {
console.warn('Failed to parse product event:', err)
}
}
const handleOrderEvent = (event: any) => {
try {
const orderData = JSON.parse(event.content)
// Handle order events (for future order management)
console.log('Order event received:', orderData)
} catch (err) {
console.warn('Failed to parse order event:', err)
}
}
const publishProduct = async (productData: any) => {
try {
const client = nostrStore.getClient()
const event = {
kind: MARKET_EVENT_KINDS.PRODUCT,
content: JSON.stringify(productData),
tags: [
['d', productData.id],
['t', 'product']
]
}
await client.publishEvent(event)
} catch (err) {
console.error('Error publishing product:', err)
throw err
}
}
const publishStall = async (stallData: any) => {
try {
const client = nostrStore.getClient()
const event = {
kind: MARKET_EVENT_KINDS.STALL,
content: JSON.stringify(stallData),
tags: [
['d', stallData.id],
['t', 'stall']
]
}
await client.publishEvent(event)
} catch (err) {
console.error('Error publishing stall:', err)
throw err
}
}
const connectToMarket = async () => {
try {
if (!nostrStore.isConnected) {
await nostrStore.connect()
}
isConnected.value = nostrStore.isConnected
if (!isConnected.value) {
throw new Error('Failed to connect to Nostr relays')
}
} catch (err) {
console.error('Error connecting to market:', err)
throw err
}
}
const disconnectFromMarket = () => {
// Cleanup subscriptions and connections
isConnected.value = false
}
return {
// State
isLoading: readonly(isLoading),
error: readonly(error),
isConnected: readonly(isConnected),
// Actions
loadMarket,
connectToMarket,
disconnectFromMarket,
publishProduct,
publishStall,
subscribeToMarketUpdates
}
}