From 77ed913e1b319422fa3be158a6f1b90b498fb457 Mon Sep 17 00:00:00 2001
From: padreug
Date: Tue, 16 Sep 2025 22:58:44 +0200
Subject: [PATCH] Add MarketProduct component and integrate into NostrFeed
- Introduced MarketProduct.vue to display market product details, including images, pricing, and availability status.
- Enhanced NostrFeed.vue to render MarketProduct components for market events, allowing users to view and share products.
- Implemented market data parsing in marketParser.ts to handle Nostr market events, ensuring structured data representation.
These changes improve the marketplace functionality within the feed, enhancing user engagement with market products.
---
.../nostr-feed/components/MarketProduct.vue | 171 ++++++++++++++++++
.../nostr-feed/components/NostrFeed.vue | 144 +++++++++++----
src/modules/nostr-feed/utils/marketParser.ts | 101 +++++++++++
3 files changed, 382 insertions(+), 34 deletions(-)
create mode 100644 src/modules/nostr-feed/components/MarketProduct.vue
create mode 100644 src/modules/nostr-feed/utils/marketParser.ts
diff --git a/src/modules/nostr-feed/components/MarketProduct.vue b/src/modules/nostr-feed/components/MarketProduct.vue
new file mode 100644
index 0000000..e80b842
--- /dev/null
+++ b/src/modules/nostr-feed/components/MarketProduct.vue
@@ -0,0 +1,171 @@
+
+
+
+
+
![]()
+
+
+ {{ formatPrice(product.price, product.currency) }}
+
+
+
+ Unavailable
+
+
+ Out of Stock
+
+
+ Limited Stock
+
+
+
+
+
+
+
+
{{ product.name }}
+
+
+ Market
+
+
+
+
+
+ {{ product.description }}
+
+
+
+
+
+
+
+
+ {{ formatPrice(product.price, product.currency) }}
+
+
+ {{ product.quantity > 0 ? `${product.quantity} available` : 'Out of stock' }}
+
+
+
+
+
+
+ Free shipping available
+ Shipping from {{ formatPrice(minShippingCost, product.currency) }}
+
+
+
+
+ Stall: {{ product.stall_id }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/modules/nostr-feed/components/NostrFeed.vue b/src/modules/nostr-feed/components/NostrFeed.vue
index a4a04f9..5472ffe 100644
--- a/src/modules/nostr-feed/components/NostrFeed.vue
+++ b/src/modules/nostr-feed/components/NostrFeed.vue
@@ -9,6 +9,8 @@ import { Megaphone, RefreshCw, AlertCircle } from 'lucide-vue-next'
import { useFeed } from '../composables/useFeed'
import appConfig from '@/app.config'
import type { ContentFilter } from '../services/FeedService'
+import MarketProduct from './MarketProduct.vue'
+import { parseMarketProduct, isMarketEvent, getMarketEventType } from '../utils/marketParser'
const props = defineProps<{
relays?: string[]
@@ -62,6 +64,34 @@ const feedDescription = computed(() => {
function isAdminPost(pubkey: string): boolean {
return adminPubkeys.includes(pubkey)
}
+
+// Get market product data for market events
+function getMarketProductData(note: any) {
+ if (note.kind === 30018) {
+ // Create a mock NostrEvent from our FeedPost
+ const mockEvent = {
+ id: note.id,
+ pubkey: note.pubkey,
+ content: note.content,
+ created_at: note.created_at,
+ kind: note.kind,
+ tags: note.tags
+ }
+ return parseMarketProduct(mockEvent)
+ }
+ return null
+}
+
+// Handle market product actions
+function onViewProduct(productId: string) {
+ console.log('View product:', productId)
+ // TODO: Navigate to product detail page or open modal
+}
+
+function onShareProduct(productId: string) {
+ console.log('Share product:', productId)
+ // TODO: Implement sharing functionality
+}
@@ -129,17 +159,13 @@ function isAdminPost(pubkey: string): boolean {
-
+
-
-
-
-
+
+
+
+
Admin
-
- Reply
-
-
- {{ formatDistanceToNow(note.created_at * 1000, { addSuffix: true }) }}
-
+ {{ formatDistanceToNow(note.created_at * 1000, { addSuffix: true }) }}
-
+
+
+
+
+
+ Invalid Market Product
+
+
+ {{ formatDistanceToNow(note.created_at * 1000, { addSuffix: true }) }}
+
+
+
+ Unable to parse market product data
+
+
+
-
-
- {{ note.content }}
-
+
+
+
+
+
+
+ Admin
+
+
+ Reply
+
+
+ {{ getMarketEventType({ kind: note.kind }) }}
+
+
+ {{ formatDistanceToNow(note.created_at * 1000, { addSuffix: true }) }}
+
+
+
-
-
-
-
Mentions:
-
- {{ mention.slice(0, 8) }}...
-
-
- +{{ note.mentions.length - 3 }} more
-
+
+
+ {{ note.content }}
+
+
+
+
+
+ Mentions:
+
+ {{ mention.slice(0, 8) }}...
+
+
+ +{{ note.mentions.length - 3 }} more
+
+
diff --git a/src/modules/nostr-feed/utils/marketParser.ts b/src/modules/nostr-feed/utils/marketParser.ts
new file mode 100644
index 0000000..1c2e807
--- /dev/null
+++ b/src/modules/nostr-feed/utils/marketParser.ts
@@ -0,0 +1,101 @@
+import type { Event as NostrEvent } from 'nostr-tools'
+import type { MarketProductData } from '../components/MarketProduct.vue'
+
+export interface MarketStallData {
+ id: string
+ name: string
+ description: string
+ currency: string
+ shipping: Array<{
+ id: string
+ cost: number
+ }>
+}
+
+/**
+ * Parse a Nostr market product event (kind 30018) into structured data
+ */
+export function parseMarketProduct(event: NostrEvent): MarketProductData | null {
+ try {
+ if (event.kind !== 30018) {
+ return null
+ }
+
+ // Parse the JSON content
+ const productData = JSON.parse(event.content)
+
+ // Validate required fields
+ if (!productData.id || !productData.name || typeof productData.price !== 'number') {
+ console.warn('Invalid market product data:', productData)
+ return null
+ }
+
+ return {
+ id: productData.id,
+ stall_id: productData.stall_id || '',
+ name: productData.name,
+ description: productData.description || '',
+ images: Array.isArray(productData.images) ? productData.images : [],
+ currency: productData.currency || 'sat',
+ price: productData.price,
+ quantity: productData.quantity || 0,
+ active: productData.active !== false, // Default to true if not specified
+ shipping: Array.isArray(productData.shipping) ? productData.shipping : []
+ }
+ } catch (error) {
+ console.error('Failed to parse market product event:', error, event)
+ return null
+ }
+}
+
+/**
+ * Parse a Nostr market stall event (kind 30017) into structured data
+ */
+export function parseMarketStall(event: NostrEvent): MarketStallData | null {
+ try {
+ if (event.kind !== 30017) {
+ return null
+ }
+
+ const stallData = JSON.parse(event.content)
+
+ if (!stallData.id || !stallData.name) {
+ console.warn('Invalid market stall data:', stallData)
+ return null
+ }
+
+ return {
+ id: stallData.id,
+ name: stallData.name,
+ description: stallData.description || '',
+ currency: stallData.currency || 'sat',
+ shipping: Array.isArray(stallData.shipping) ? stallData.shipping : []
+ }
+ } catch (error) {
+ console.error('Failed to parse market stall event:', error, event)
+ return null
+ }
+}
+
+/**
+ * Check if an event is a market-related event
+ */
+export function isMarketEvent(event: NostrEvent): boolean {
+ return event.kind === 30017 || event.kind === 30018 || event.kind === 30019
+}
+
+/**
+ * Get a human-readable type for a market event
+ */
+export function getMarketEventType(event: NostrEvent): string {
+ switch (event.kind) {
+ case 30017:
+ return 'Market Stall'
+ case 30018:
+ return 'Product'
+ case 30019:
+ return 'Market Activity'
+ default:
+ return 'Unknown Market Event'
+ }
+}
\ No newline at end of file