Refactor NostrFeed module and remove marketplace components
- Deleted the MarketProduct component and associated market parsing logic, streamlining the NostrFeed module. - Updated FeedService to exclude marketplace events from the main feed, ensuring clearer event management. - Adjusted content filters to remove marketplace-related entries, enhancing the organization of content filtering. These changes improve the clarity and efficiency of the NostrFeed module by separating marketplace functionality.
This commit is contained in:
parent
2a9915a727
commit
3c20d1c584
6 changed files with 3 additions and 389 deletions
|
|
@ -154,10 +154,10 @@ const presets = computed(() => [
|
||||||
{ id: 'all', label: 'All Content' },
|
{ id: 'all', label: 'All Content' },
|
||||||
{ id: 'announcements', label: 'Announcements' },
|
{ id: 'announcements', label: 'Announcements' },
|
||||||
{ id: 'community', label: 'Community' },
|
{ id: 'community', label: 'Community' },
|
||||||
{ id: 'marketplace', label: 'Marketplace' },
|
|
||||||
{ id: 'social', label: 'Social' },
|
{ id: 'social', label: 'Social' },
|
||||||
{ id: 'events', label: 'Events' },
|
{ id: 'events', label: 'Events' },
|
||||||
{ id: 'content', label: 'Articles' }
|
{ id: 'content', label: 'Articles' },
|
||||||
|
{ id: 'rideshare', label: 'Rideshare' }
|
||||||
])
|
])
|
||||||
|
|
||||||
// Current active filters
|
// Current active filters
|
||||||
|
|
|
||||||
|
|
@ -1,171 +0,0 @@
|
||||||
<template>
|
|
||||||
<Card class="overflow-hidden">
|
|
||||||
<!-- Product Image -->
|
|
||||||
<div v-if="product.images && product.images.length > 0" class="relative">
|
|
||||||
<img
|
|
||||||
:src="product.images[0]"
|
|
||||||
:alt="product.name"
|
|
||||||
class="w-full h-48 object-cover"
|
|
||||||
@error="onImageError"
|
|
||||||
/>
|
|
||||||
<!-- Price Badge -->
|
|
||||||
<div class="absolute top-2 right-2 bg-black/80 text-white px-3 py-1 rounded-full text-sm font-semibold">
|
|
||||||
{{ formatPrice(product.price, product.currency) }}
|
|
||||||
</div>
|
|
||||||
<!-- Availability Badge -->
|
|
||||||
<div v-if="!product.active" class="absolute top-2 left-2 bg-red-500 text-white px-2 py-1 rounded text-xs font-medium">
|
|
||||||
Unavailable
|
|
||||||
</div>
|
|
||||||
<div v-else-if="product.quantity <= 0" class="absolute top-2 left-2 bg-orange-500 text-white px-2 py-1 rounded text-xs font-medium">
|
|
||||||
Out of Stock
|
|
||||||
</div>
|
|
||||||
<div v-else-if="product.quantity <= 5" class="absolute top-2 left-2 bg-yellow-500 text-white px-2 py-1 rounded text-xs font-medium">
|
|
||||||
Limited Stock
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<CardContent class="p-4">
|
|
||||||
<!-- Product Header -->
|
|
||||||
<div class="space-y-2">
|
|
||||||
<div class="flex items-start justify-between gap-2">
|
|
||||||
<h3 class="font-semibold text-lg line-clamp-1">{{ product.name }}</h3>
|
|
||||||
<Badge variant="secondary" class="text-xs">
|
|
||||||
<ShoppingBag class="w-3 h-3 mr-1" />
|
|
||||||
Market
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Description -->
|
|
||||||
<p class="text-muted-foreground text-sm line-clamp-2">
|
|
||||||
{{ product.description }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Product Details -->
|
|
||||||
<div class="mt-4 space-y-3">
|
|
||||||
<!-- Price and Quantity -->
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<div class="text-2xl font-bold">
|
|
||||||
{{ formatPrice(product.price, product.currency) }}
|
|
||||||
</div>
|
|
||||||
<div class="text-sm text-muted-foreground">
|
|
||||||
{{ product.quantity > 0 ? `${product.quantity} available` : 'Out of stock' }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Shipping Info -->
|
|
||||||
<div v-if="product.shipping && product.shipping.length > 0" class="flex items-center gap-2 text-sm text-muted-foreground">
|
|
||||||
<Truck class="w-4 h-4" />
|
|
||||||
<span v-if="hasFreelShipping">Free shipping available</span>
|
|
||||||
<span v-else>Shipping from {{ formatPrice(minShippingCost, product.currency) }}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Stall ID (for debugging/reference) -->
|
|
||||||
<div class="text-xs text-muted-foreground">
|
|
||||||
Stall: {{ product.stall_id }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Action Buttons -->
|
|
||||||
<div class="mt-4 flex gap-2">
|
|
||||||
<Button
|
|
||||||
class="flex-1"
|
|
||||||
:disabled="!product.active || product.quantity <= 0"
|
|
||||||
@click="onViewProduct"
|
|
||||||
>
|
|
||||||
<ShoppingCart class="w-4 h-4 mr-2" />
|
|
||||||
{{ product.active && product.quantity > 0 ? 'View Product' : 'Unavailable' }}
|
|
||||||
</Button>
|
|
||||||
<Button variant="outline" size="icon" @click="onShareProduct">
|
|
||||||
<Share2 class="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { computed } from 'vue'
|
|
||||||
import { Card, CardContent } from '@/components/ui/card'
|
|
||||||
import { Button } from '@/components/ui/button'
|
|
||||||
import { Badge } from '@/components/ui/badge'
|
|
||||||
import { ShoppingBag, ShoppingCart, Truck, Share2 } from 'lucide-vue-next'
|
|
||||||
|
|
||||||
export interface MarketProductData {
|
|
||||||
id: string
|
|
||||||
stall_id: string
|
|
||||||
name: string
|
|
||||||
description: string
|
|
||||||
images?: string[]
|
|
||||||
currency: string
|
|
||||||
price: number
|
|
||||||
quantity: number
|
|
||||||
active: boolean
|
|
||||||
shipping?: Array<{
|
|
||||||
id: string
|
|
||||||
cost: number
|
|
||||||
}>
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
product: MarketProductData
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Emits {
|
|
||||||
(e: 'view-product', productId: string): void
|
|
||||||
(e: 'share-product', productId: string): void
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = defineProps<Props>()
|
|
||||||
const emit = defineEmits<Emits>()
|
|
||||||
|
|
||||||
// Computed properties
|
|
||||||
const hasFreelShipping = computed(() => {
|
|
||||||
return props.product.shipping?.some(shipping => shipping.cost === 0) || false
|
|
||||||
})
|
|
||||||
|
|
||||||
const minShippingCost = computed(() => {
|
|
||||||
if (!props.product.shipping?.length) return 0
|
|
||||||
return Math.min(...props.product.shipping.map(s => s.cost))
|
|
||||||
})
|
|
||||||
|
|
||||||
// Methods
|
|
||||||
const formatPrice = (price: number, currency: string) => {
|
|
||||||
if (currency.toLowerCase() === 'sat' || currency.toLowerCase() === 'sats') {
|
|
||||||
return `${price.toLocaleString()} sats`
|
|
||||||
}
|
|
||||||
if (currency.toLowerCase() === 'btc') {
|
|
||||||
return `₿${(price / 100000000).toFixed(8)}`
|
|
||||||
}
|
|
||||||
return `${price} ${currency.toUpperCase()}`
|
|
||||||
}
|
|
||||||
|
|
||||||
const onImageError = (event: Event) => {
|
|
||||||
const img = event.target as HTMLImageElement
|
|
||||||
img.style.display = 'none'
|
|
||||||
}
|
|
||||||
|
|
||||||
const onViewProduct = () => {
|
|
||||||
emit('view-product', props.product.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
const onShareProduct = () => {
|
|
||||||
emit('share-product', props.product.id)
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.line-clamp-1 {
|
|
||||||
overflow: hidden;
|
|
||||||
display: -webkit-box;
|
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
-webkit-line-clamp: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.line-clamp-2 {
|
|
||||||
overflow: hidden;
|
|
||||||
display: -webkit-box;
|
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
-webkit-line-clamp: 2;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -9,8 +9,6 @@ import { useProfiles } from '../composables/useProfiles'
|
||||||
import { useReactions } from '../composables/useReactions'
|
import { useReactions } from '../composables/useReactions'
|
||||||
import appConfig from '@/app.config'
|
import appConfig from '@/app.config'
|
||||||
import type { ContentFilter } from '../services/FeedService'
|
import type { ContentFilter } from '../services/FeedService'
|
||||||
import MarketProduct from './MarketProduct.vue'
|
|
||||||
import { parseMarketProduct } from '../utils/marketParser'
|
|
||||||
|
|
||||||
interface Emits {
|
interface Emits {
|
||||||
(e: 'reply-to-note', note: { id: string; content: string; pubkey: string }): void
|
(e: 'reply-to-note', note: { id: string; content: string; pubkey: string }): void
|
||||||
|
|
@ -130,34 +128,6 @@ function getRideshareType(note: any): string | null {
|
||||||
return 'Rideshare'
|
return 'Rideshare'
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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,
|
|
||||||
sig: '' // Required by Event interface
|
|
||||||
}
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle reply to note
|
// Handle reply to note
|
||||||
function onReplyToNote(note: any) {
|
function onReplyToNote(note: any) {
|
||||||
|
|
@ -247,53 +217,8 @@ async function onToggleLike(note: any) {
|
||||||
<div v-else class="h-full overflow-y-auto scrollbar-thin scrollbar-thumb-border scrollbar-track-transparent">
|
<div v-else class="h-full overflow-y-auto scrollbar-thin scrollbar-thumb-border scrollbar-track-transparent">
|
||||||
<div>
|
<div>
|
||||||
<div v-for="(note, index) in notes" :key="note.id" :class="{ 'bg-muted/20': index % 2 === 1 }">
|
<div v-for="(note, index) in notes" :key="note.id" :class="{ 'bg-muted/20': index % 2 === 1 }">
|
||||||
<!-- Market Product Component (kind 30018) -->
|
<!-- Text Posts and Other Event Types -->
|
||||||
<template v-if="note.kind === 30018">
|
|
||||||
<div class="p-3 border-b border-border/40">
|
|
||||||
<div class="mb-2 flex items-center justify-between">
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<Badge
|
|
||||||
v-if="isAdminPost(note.pubkey)"
|
|
||||||
variant="default"
|
|
||||||
class="text-xs"
|
|
||||||
>
|
|
||||||
Admin
|
|
||||||
</Badge>
|
|
||||||
<span class="text-sm font-medium">{{ getDisplayName(note.pubkey) }}</span>
|
|
||||||
</div>
|
|
||||||
<span class="text-xs text-muted-foreground">
|
|
||||||
{{ formatDistanceToNow(note.created_at * 1000, { addSuffix: true }) }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<MarketProduct
|
|
||||||
v-if="getMarketProductData(note)"
|
|
||||||
:product="getMarketProductData(note)!"
|
|
||||||
@view-product="onViewProduct"
|
|
||||||
@share-product="onShareProduct"
|
|
||||||
/>
|
|
||||||
<!-- Fallback for invalid market data -->
|
|
||||||
<div
|
|
||||||
v-else
|
|
||||||
class="p-3 border rounded-lg border-destructive/20"
|
|
||||||
>
|
|
||||||
<div class="flex items-center gap-2 mb-2">
|
|
||||||
<Badge variant="destructive" class="text-xs">
|
|
||||||
Invalid Market Product
|
|
||||||
</Badge>
|
|
||||||
<span class="text-xs text-muted-foreground">
|
|
||||||
{{ formatDistanceToNow(note.created_at * 1000, { addSuffix: true }) }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="text-sm text-muted-foreground">
|
|
||||||
Unable to parse market product data
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- Regular Text Posts and Other Event Types -->
|
|
||||||
<div
|
<div
|
||||||
v-else
|
|
||||||
class="p-3 hover:bg-accent/50 transition-colors border-b border-border/40"
|
class="p-3 hover:bg-accent/50 transition-colors border-b border-border/40"
|
||||||
>
|
>
|
||||||
<!-- Note Header -->
|
<!-- Note Header -->
|
||||||
|
|
@ -313,13 +238,6 @@ async function onToggleLike(note: any) {
|
||||||
>
|
>
|
||||||
Reply
|
Reply
|
||||||
</Badge>
|
</Badge>
|
||||||
<Badge
|
|
||||||
v-if="note.kind === 30018"
|
|
||||||
variant="outline"
|
|
||||||
class="text-xs px-1.5 py-0.5"
|
|
||||||
>
|
|
||||||
Market Product
|
|
||||||
</Badge>
|
|
||||||
<Badge
|
<Badge
|
||||||
v-if="isRidesharePost(note)"
|
v-if="isRidesharePost(note)"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
|
|
|
||||||
|
|
@ -30,27 +30,6 @@ export const CONTENT_FILTERS: Record<string, ContentFilter> = {
|
||||||
filterByAuthor: 'exclude-admin'
|
filterByAuthor: 'exclude-admin'
|
||||||
},
|
},
|
||||||
|
|
||||||
// Market content
|
|
||||||
marketStalls: {
|
|
||||||
id: 'market-stalls',
|
|
||||||
label: 'Market Stalls',
|
|
||||||
kinds: [30017], // NIP-15: Nostr Marketplace
|
|
||||||
description: 'Marketplace stall listings'
|
|
||||||
},
|
|
||||||
|
|
||||||
marketProducts: {
|
|
||||||
id: 'market-products',
|
|
||||||
label: 'Market Products',
|
|
||||||
kinds: [30018], // NIP-15: Nostr Marketplace
|
|
||||||
description: 'Product listings and updates'
|
|
||||||
},
|
|
||||||
|
|
||||||
marketGeneral: {
|
|
||||||
id: 'market-general',
|
|
||||||
label: 'Market Activity',
|
|
||||||
kinds: [30019], // NIP-15: Nostr Marketplace
|
|
||||||
description: 'General marketplace activity'
|
|
||||||
},
|
|
||||||
|
|
||||||
// Chat messages (if user wants to see them in feed)
|
// Chat messages (if user wants to see them in feed)
|
||||||
chatMessages: {
|
chatMessages: {
|
||||||
|
|
@ -122,7 +101,6 @@ export const FILTER_PRESETS: Record<string, ContentFilter[]> = {
|
||||||
CONTENT_FILTERS.calendarEvents,
|
CONTENT_FILTERS.calendarEvents,
|
||||||
CONTENT_FILTERS.longFormContent
|
CONTENT_FILTERS.longFormContent
|
||||||
// Note: reactions (kind 7) are handled separately by ReactionService
|
// Note: reactions (kind 7) are handled separately by ReactionService
|
||||||
// Note: market items removed - they have their own dedicated section
|
|
||||||
],
|
],
|
||||||
|
|
||||||
announcements: [
|
announcements: [
|
||||||
|
|
@ -136,12 +114,6 @@ export const FILTER_PRESETS: Record<string, ContentFilter[]> = {
|
||||||
// Note: reactions are handled separately for counts
|
// Note: reactions are handled separately for counts
|
||||||
],
|
],
|
||||||
|
|
||||||
marketplace: [
|
|
||||||
CONTENT_FILTERS.marketStalls,
|
|
||||||
CONTENT_FILTERS.marketProducts,
|
|
||||||
CONTENT_FILTERS.marketGeneral
|
|
||||||
// Marketplace is a separate section - not mixed with regular feed
|
|
||||||
],
|
|
||||||
|
|
||||||
social: [
|
social: [
|
||||||
CONTENT_FILTERS.textNotes,
|
CONTENT_FILTERS.textNotes,
|
||||||
|
|
|
||||||
|
|
@ -279,10 +279,6 @@ export class FeedService extends BaseService {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Exclude marketplace events (they have their own dedicated section)
|
|
||||||
if (event.kind === 30017 || event.kind === 30018 || event.kind === 30019) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
const isAdminPost = config.adminPubkeys?.includes(event.pubkey) || false
|
const isAdminPost = config.adminPubkeys?.includes(event.pubkey) || false
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,101 +0,0 @@
|
||||||
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'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue