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
367
src/composables/useMarket.ts
Normal file
367
src/composables/useMarket.ts
Normal 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
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue