Enhance market module with new chat and events features

- Introduce chat module with components, services, and composables for real-time messaging.
- Implement events module with API service, components, and ticket purchasing functionality.
- Update app configuration to include new modules and their respective settings.
- Refactor existing components to integrate with the new chat and events features.
- Enhance market store and services to support new functionalities and improve order management.
- Update routing to accommodate new views for chat and events, ensuring seamless navigation.
This commit is contained in:
padreug 2025-09-05 00:01:40 +02:00
parent 519a9003d4
commit e40ac91417
46 changed files with 6305 additions and 3264 deletions

View file

@ -1,250 +0,0 @@
<template>
<div class="p-3 bg-muted rounded-lg">
<!-- Desktop Layout (horizontal) -->
<div class="hidden md:flex items-center space-x-4">
<!-- Product Image -->
<div class="flex-shrink-0">
<img
:src="item.product.images?.[0] || '/placeholder-product.png'"
:alt="item.product.name"
class="w-16 h-16 object-cover rounded-md"
@error="handleImageError"
/>
</div>
<!-- Product Details -->
<div class="flex-1 min-w-0">
<div class="flex items-start justify-between">
<div class="flex-1">
<h4 class="text-sm font-medium text-foreground truncate">
{{ item.product.name }}
</h4>
<p class="text-sm text-muted-foreground">
{{ item.product.stallName }}
</p>
<div class="flex items-center space-x-2 mt-1">
<Badge
v-for="category in item.product.categories?.slice(0, 2)"
:key="category"
variant="secondary"
class="text-xs"
>
{{ category }}
</Badge>
</div>
</div>
<!-- Price -->
<div class="text-right ml-4">
<p class="text-sm font-medium text-foreground">
{{ formatPrice(item.product.price, item.product.currency) }}
</p>
<p class="text-xs text-muted-foreground">
each
</p>
</div>
</div>
</div>
<!-- Quantity Controls -->
<div class="flex items-center space-x-2">
<Button
@click="decreaseQuantity"
variant="outline"
size="sm"
:disabled="item.quantity <= 1"
class="w-8 h-8 p-0"
>
<Minus class="w-3 h-3" />
</Button>
<div class="w-12 text-center">
<span class="text-sm font-medium text-foreground">
{{ item.quantity }}
</span>
</div>
<Button
@click="increaseQuantity"
variant="outline"
size="sm"
:disabled="item.quantity >= item.product.quantity"
class="w-8 h-8 p-0"
>
<Plus class="w-3 h-3" />
</Button>
</div>
<!-- Total Price -->
<div class="text-right min-w-[80px]">
<p class="text-sm font-semibold text-green-600">
{{ formatPrice(item.product.price * item.quantity, item.product.currency) }}
</p>
<p class="text-xs text-muted-foreground">
total
</p>
</div>
<!-- Remove Button -->
<Button
@click="removeItem"
variant="ghost"
size="sm"
class="text-red-500 hover:text-red-700 hover:bg-red-500/10"
>
<Trash2 class="w-4 h-4" />
</Button>
</div>
<!-- Mobile Layout (stacked) -->
<div class="md:hidden space-y-3">
<!-- Product Image and Details Row -->
<div class="flex items-start space-x-3">
<!-- Product Image -->
<div class="flex-shrink-0">
<img
:src="item.product.images?.[0] || '/placeholder-product.png'"
:alt="item.product.name"
class="w-16 h-16 object-cover rounded-md"
@error="handleImageError"
/>
</div>
<!-- Product Details -->
<div class="flex-1 min-w-0">
<h4 class="text-sm font-medium text-foreground">
{{ item.product.name }}
</h4>
<p class="text-sm text-muted-foreground">
{{ item.product.stallName }}
</p>
<div class="flex items-center space-x-2 mt-1">
<Badge
v-for="category in item.product.categories?.slice(0, 2)"
:key="category"
variant="secondary"
class="text-xs"
>
{{ category }}
</Badge>
</div>
</div>
<!-- Remove Button -->
<Button
@click="removeItem"
variant="ghost"
size="sm"
class="text-red-500 hover:text-red-700 hover:bg-red-500/10 flex-shrink-0"
>
<Trash2 class="w-4 h-4" />
</Button>
</div>
<!-- Price and Quantity Row -->
<div class="flex items-center justify-between">
<!-- Price per item -->
<div class="text-left">
<p class="text-sm font-medium text-foreground">
{{ formatPrice(item.product.price, item.product.currency) }}
</p>
<p class="text-xs text-muted-foreground">
each
</p>
</div>
<!-- Quantity Controls -->
<div class="flex items-center space-x-2">
<Button
@click="decreaseQuantity"
variant="outline"
size="sm"
:disabled="item.quantity <= 1"
class="w-8 h-8 p-0"
>
<Minus class="w-3 h-3" />
</Button>
<div class="w-12 text-center">
<span class="text-sm font-medium text-foreground">
{{ item.quantity }}
</span>
</div>
<Button
@click="increaseQuantity"
variant="outline"
size="sm"
:disabled="item.quantity >= item.product.quantity"
class="w-8 h-8 p-0"
>
<Plus class="w-3 h-3" />
</Button>
</div>
<!-- Total Price -->
<div class="text-right">
<p class="text-sm font-semibold text-green-600">
{{ formatPrice(item.product.price * item.quantity, item.product.currency) }}
</p>
<p class="text-xs text-muted-foreground">
total
</p>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { Minus, Plus, Trash2 } from 'lucide-vue-next'
import type { CartItem as CartItemType } from '@/stores/market'
interface Props {
item: CartItemType
stallId: string
}
const props = defineProps<Props>()
const emit = defineEmits<{
'update-quantity': [stallId: string, productId: string, quantity: number]
'remove-item': [stallId: string, productId: string]
}>()
// Methods
const increaseQuantity = () => {
const newQuantity = props.item.quantity + 1
if (newQuantity <= props.item.product.quantity) {
emit('update-quantity', props.stallId, props.item.product.id, newQuantity)
}
}
const decreaseQuantity = () => {
const newQuantity = props.item.quantity - 1
if (newQuantity > 0) {
emit('update-quantity', props.stallId, props.item.product.id, newQuantity)
}
}
const removeItem = () => {
emit('remove-item', props.stallId, props.item.product.id)
}
const handleImageError = (event: Event) => {
const target = event.target as HTMLImageElement
target.src = '/placeholder-product.png'
}
const formatPrice = (price: number, currency: string) => {
if (currency === 'sats' || currency === 'sat') {
return `${price.toLocaleString()} sats`
}
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: currency.toUpperCase()
}).format(price)
}
</script>

View file

@ -1,231 +1,4 @@
<template>
<div class="bg-card border rounded-lg p-6 shadow-sm">
<!-- Cart Summary Header -->
<div class="border-b border-border pb-4 mb-4">
<h3 class="text-lg font-semibold text-foreground">Order Summary</h3>
<p class="text-sm text-muted-foreground">
{{ itemCount }} item{{ itemCount !== 1 ? 's' : '' }} in cart
</p>
</div>
<!-- Cart Items Summary -->
<div class="space-y-3 mb-4">
<div
v-for="item in cartItems"
:key="item.product.id"
class="flex items-center justify-between text-sm"
>
<div class="flex items-center space-x-3">
<img
:src="item.product.images?.[0] || '/placeholder-product.png'"
:alt="item.product.name"
class="w-8 h-8 object-cover rounded"
/>
<div>
<p class="font-medium text-foreground">{{ item.product.name }}</p>
<p class="text-muted-foreground">Qty: {{ item.quantity }}</p>
</div>
</div>
<p class="font-medium text-foreground">
{{ formatPrice(item.product.price * item.quantity, item.product.currency) }}
</p>
</div>
</div>
<!-- Shipping Zone Selection -->
<div class="border-t border-border pt-4 mb-4">
<div class="flex items-center justify-between mb-2">
<label class="text-sm font-medium text-foreground">Shipping Zone</label>
<Button
v-if="availableShippingZones.length > 1"
@click="showShippingSelector = !showShippingSelector"
variant="ghost"
size="sm"
>
{{ selectedShippingZone ? 'Change' : 'Select' }}
</Button>
</div>
<div v-if="selectedShippingZone" class="flex items-center justify-between p-3 bg-muted rounded">
<div>
<p class="font-medium text-foreground">{{ selectedShippingZone.name }}</p>
<p v-if="selectedShippingZone.description" class="text-sm text-muted-foreground">
{{ selectedShippingZone.description }}
</p>
<p v-if="selectedShippingZone.estimatedDays" class="text-xs text-muted-foreground">
Estimated: {{ selectedShippingZone.estimatedDays }}
</p>
</div>
<p class="font-semibold text-foreground">
{{ formatPrice(selectedShippingZone.cost, selectedShippingZone.currency) }}
</p>
</div>
<!-- Shipping Zone Selector -->
<div v-if="showShippingSelector && availableShippingZones.length > 1" class="mt-2">
<div class="space-y-2">
<div
v-for="zone in availableShippingZones"
:key="zone.id"
@click="selectShippingZone(zone)"
class="flex items-center justify-between p-3 border rounded cursor-pointer hover:bg-muted/50"
:class="{ 'border-primary bg-primary/10': selectedShippingZone?.id === zone.id }"
>
<div>
<p class="font-medium text-foreground">{{ zone.name }}</p>
<p v-if="zone.description" class="text-sm text-muted-foreground">
{{ zone.description }}
</p>
<p v-if="zone.estimatedDays" class="text-xs text-muted-foreground">
Estimated: {{ zone.estimatedDays }}
</p>
</div>
<p class="font-semibold text-foreground">
{{ formatPrice(zone.cost, zone.currency) }}
</p>
</div>
</div>
</div>
<p v-if="!selectedShippingZone" class="text-sm text-red-600">
Please select a shipping zone
</p>
</div>
<!-- Price Breakdown -->
<div class="border-t border-border pt-4 mb-6">
<div class="space-y-2">
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">Subtotal</span>
<span class="text-foreground">{{ formatPrice(subtotal, currency) }}</span>
</div>
<div v-if="selectedShippingZone" class="flex justify-between text-sm">
<span class="text-muted-foreground">Shipping</span>
<span class="text-foreground">
{{ formatPrice(selectedShippingZone.cost, selectedShippingZone.currency) }}
</span>
</div>
<div class="border-t border-border pt-2 flex justify-between font-semibold text-lg">
<span class="text-foreground">Total</span>
<span class="text-green-600">{{ formatPrice(total, currency) }}</span>
</div>
</div>
</div>
<!-- Checkout Actions -->
<div class="space-y-3">
<Button
@click="continueShopping"
variant="outline"
class="w-full"
>
Back to Cart
</Button>
</div>
<!-- Security Note -->
<div class="mt-4 p-3 bg-muted/50 rounded-lg">
<div class="flex items-start space-x-2">
<Shield class="w-4 h-4 text-muted-foreground mt-0.5 flex-shrink-0" />
<div class="text-sm text-muted-foreground">
<p class="font-medium text-foreground">Secure Checkout</p>
<p>Your order will be encrypted and sent securely to the merchant using Nostr.</p>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useRouter } from 'vue-router'
import { Button } from '@/components/ui/button'
import { Shield } from 'lucide-vue-next'
import type { ShippingZone } from '@/stores/market'
interface Props {
stallId: string
cartItems: readonly {
readonly product: {
readonly id: string
readonly stall_id: string
readonly stallName: string
readonly name: string
readonly description?: string
readonly price: number
readonly currency: string
readonly quantity: number
readonly images?: readonly string[]
readonly categories?: readonly string[]
readonly createdAt: number
readonly updatedAt: number
}
readonly quantity: number
readonly stallId: string
}[]
subtotal: number
currency: string
availableShippingZones: readonly {
readonly id: string
readonly name: string
readonly cost: number
readonly currency: string
readonly description?: string
readonly estimatedDays?: string
readonly requiresPhysicalShipping?: boolean
}[]
selectedShippingZone?: {
readonly id: string
readonly name: string
readonly cost: number
readonly currency: string
readonly description?: string
readonly estimatedDays?: string
readonly requiresPhysicalShipping?: boolean
}
}
const props = defineProps<Props>()
const emit = defineEmits<{
'shipping-zone-selected': [shippingZone: ShippingZone]
}>()
const router = useRouter()
// const marketStore = useMarketStore()
// Local state
const showShippingSelector = ref(false)
// Computed properties
const itemCount = computed(() =>
props.cartItems.reduce((total, item) => total + item.quantity, 0)
)
const total = computed(() => {
const shippingCost = props.selectedShippingZone?.cost || 0
return props.subtotal + shippingCost
})
// Methods
const selectShippingZone = (shippingZone: ShippingZone) => {
emit('shipping-zone-selected', shippingZone)
showShippingSelector.value = false
}
const continueShopping = () => {
router.push('/cart')
}
const formatPrice = (price: number, currency: string) => {
if (currency === 'sats' || currency === 'sat') {
return `${price.toLocaleString()} sats`
}
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: currency.toUpperCase()
}).format(price)
}
</script>
<script lang="ts">
import CartSummaryComponent from '@/modules/market/components/CartSummary.vue'
export default CartSummaryComponent
</script>

View file

@ -1,311 +0,0 @@
<template>
<div class="space-y-6">
<!-- Stats Cards -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<!-- Total Orders -->
<div class="bg-card p-6 rounded-lg border shadow-sm">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium text-muted-foreground">Total Orders</p>
<p class="text-2xl font-bold text-foreground">{{ orderStats.total }}</p>
</div>
<div class="w-12 h-12 bg-primary/10 rounded-lg flex items-center justify-center">
<Package class="w-6 h-6 text-primary" />
</div>
</div>
<div class="mt-4">
<div class="flex items-center text-sm text-muted-foreground">
<span>{{ orderStats.pending }} pending</span>
<span class="mx-2"></span>
<span>{{ orderStats.paid }} paid</span>
</div>
</div>
</div>
<!-- Pending Payments -->
<div class="bg-card p-6 rounded-lg border shadow-sm">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium text-muted-foreground">Pending Payments</p>
<p class="text-2xl font-bold text-foreground">{{ orderStats.pendingPayments }}</p>
</div>
<div class="w-12 h-12 bg-yellow-500/10 rounded-lg flex items-center justify-center">
<DollarSign class="w-6 h-6 text-yellow-500" />
</div>
</div>
<div class="mt-4">
<div class="flex items-center text-sm text-muted-foreground">
<span>Total: {{ formatPrice(orderStats.pendingAmount, 'sat') }}</span>
</div>
</div>
</div>
<!-- Recent Sales (Merchant) -->
<div class="bg-card p-6 rounded-lg border shadow-sm">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium text-muted-foreground">Recent Sales</p>
<p class="text-2xl font-bold text-foreground">{{ orderStats.recentSales }}</p>
</div>
<div class="w-12 h-12 bg-green-500/10 rounded-lg flex items-center justify-center">
<Store class="w-6 h-6 text-green-500" />
</div>
</div>
<div class="mt-4">
<div class="flex items-center text-sm text-muted-foreground">
<span>Last 7 days</span>
</div>
</div>
</div>
<!-- Market Activity -->
<div class="bg-card p-6 rounded-lg border shadow-sm">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium text-muted-foreground">Market Activity</p>
<p class="text-2xl font-bold text-foreground">{{ orderStats.active }}</p>
</div>
<div class="w-12 h-12 bg-purple-500/10 rounded-lg flex items-center justify-center">
<BarChart3 class="w-6 h-6 text-purple-500" />
</div>
</div>
<div class="mt-4">
<div class="flex items-center text-sm text-muted-foreground">
<span>{{ orderStats.connected ? 'Connected' : 'Disconnected' }}</span>
</div>
</div>
</div>
</div>
<!-- Quick Actions -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- Customer Actions -->
<div class="bg-card p-6 rounded-lg border shadow-sm">
<h3 class="text-lg font-semibold text-foreground mb-4 flex items-center gap-2">
<ShoppingCart class="w-5 h-5 text-primary" />
Customer Actions
</h3>
<div class="space-y-3">
<Button
@click="navigateToMarket"
variant="default"
class="w-full justify-start"
>
<Store class="w-4 h-4 mr-2" />
Browse Market
</Button>
<Button
@click="navigateToOrders"
variant="outline"
class="w-full justify-start"
>
<Package class="w-4 h-4 mr-2" />
View All Orders
</Button>
<Button
@click="navigateToCart"
variant="outline"
class="w-full justify-start"
>
<ShoppingCart class="w-4 h-4 mr-2" />
Shopping Cart
</Button>
</div>
</div>
<!-- Merchant Actions -->
<div class="bg-card p-6 rounded-lg border shadow-sm">
<h3 class="text-lg font-semibold text-foreground mb-4 flex items-center gap-2">
<Store class="w-5 h-5 text-green-500" />
Merchant Actions
</h3>
<div class="space-y-3">
<Button
@click="navigateToStore"
variant="default"
class="w-full justify-start"
>
<Store class="w-4 h-4 mr-2" />
Manage Store
</Button>
<Button
@click="navigateToProducts"
variant="outline"
class="w-full justify-start"
>
<Package class="w-4 h-4 mr-2" />
Manage Products
</Button>
<Button
@click="navigateToOrders"
variant="outline"
class="w-full justify-start"
>
<Package class="w-4 h-4 mr-2" />
View Orders
</Button>
</div>
</div>
</div>
<!-- Recent Activity -->
<div class="bg-card p-6 rounded-lg border shadow-sm">
<h3 class="text-lg font-semibold text-foreground mb-4">Recent Activity</h3>
<div v-if="recentActivity.length > 0" class="space-y-3">
<div
v-for="activity in recentActivity"
:key="activity.id"
class="flex items-center justify-between p-3 bg-muted rounded-lg"
>
<div class="flex items-center gap-3">
<div class="w-8 h-8 bg-primary/10 rounded-full flex items-center justify-center">
<component :is="getActivityIcon(activity.type)" class="w-4 h-4 text-primary" />
</div>
<div>
<p class="text-sm font-medium text-foreground">{{ activity.title }}</p>
<p class="text-xs text-muted-foreground">{{ formatDate(activity.timestamp) }}</p>
</div>
</div>
<Badge :variant="getActivityVariant(activity.type)">
{{ activity.status }}
</Badge>
</div>
</div>
<div v-else class="text-center py-8 text-muted-foreground">
<Package class="w-12 h-12 mx-auto mb-3 text-muted-foreground/50" />
<p>No recent activity</p>
<p class="text-sm">Start shopping or selling to see activity here</p>
</div>
</div>
<!-- Market Status -->
<div class="bg-card p-6 rounded-lg border shadow-sm">
<h3 class="text-lg font-semibold text-foreground mb-4">Market Status</h3>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="flex items-center gap-3">
<div
class="w-3 h-3 rounded-full"
:class="orderEvents.isSubscribed ? 'bg-green-500' : 'bg-yellow-500'"
></div>
<span class="text-sm text-muted-foreground">
Order Events: {{ orderEvents.isSubscribed ? 'Connected' : 'Connecting...' }}
</span>
</div>
<div class="flex items-center gap-3">
<div
class="w-3 h-3 rounded-full"
:class="isConnected ? 'bg-green-500' : 'bg-red-500'"
></div>
<span class="text-sm text-muted-foreground">
Market: {{ isConnected ? 'Connected' : 'Disconnected' }}
</span>
</div>
<div class="flex items-center gap-3">
<div
class="w-3 h-3 rounded-full"
:class="auth.isAuthenticated ? 'bg-green-500' : 'bg-red-500'"
></div>
<span class="text-sm text-muted-foreground">
Auth: {{ auth.isAuthenticated ? 'Authenticated' : 'Not Authenticated' }}
</span>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useRouter } from 'vue-router'
import { useMarketStore } from '@/stores/market'
import { useAuth } from '@/composables/useAuth'
import { useMarket } from '@/composables/useMarket'
import { useOrderEvents } from '@/composables/useOrderEvents'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import {
Package,
Store,
ShoppingCart,
DollarSign,
BarChart3,
Clock
} from 'lucide-vue-next'
const router = useRouter()
const marketStore = useMarketStore()
const auth = useAuth()
const { isConnected } = useMarket()
const orderEvents = useOrderEvents()
// Computed properties
const orderStats = computed(() => {
const orders = Object.values(marketStore.orders)
const now = Date.now() / 1000
const sevenDaysAgo = now - (7 * 24 * 60 * 60)
return {
total: orders.length,
pending: orders.filter(o => o.status === 'pending').length,
paid: orders.filter(o => o.status === 'paid').length,
pendingPayments: orders.filter(o => o.paymentStatus === 'pending').length,
pendingAmount: orders
.filter(o => o.paymentStatus === 'pending')
.reduce((sum, o) => sum + o.total, 0),
recentSales: orders.filter(o =>
o.status === 'paid' && o.createdAt > sevenDaysAgo
).length,
active: orders.filter(o =>
['pending', 'paid', 'processing'].includes(o.status)
).length,
connected: false
}
})
const recentActivity = computed(() => {
const orders = Object.values(marketStore.orders)
const now = Date.now() / 1000
const recentOrders = orders
.filter(o => o.updatedAt > now - (24 * 60 * 60)) // Last 24 hours
.sort((a, b) => b.updatedAt - a.updatedAt)
.slice(0, 5)
return recentOrders.map(order => ({
id: order.id,
type: 'order',
title: `Order ${order.id.slice(-8)} - ${order.status}`,
status: order.status,
timestamp: order.updatedAt
}))
})
// Methods
const formatPrice = (price: number, currency: string) => {
return `${price} ${currency}`
}
const formatDate = (timestamp: number) => {
return new Date(timestamp * 1000).toLocaleString()
}
const getActivityIcon = (type: string) => {
switch (type) {
case 'order': return Package
default: return Clock
}
}
const getActivityVariant = (type: string) => {
switch (type) {
case 'order': return 'secondary'
default: return 'outline'
}
}
const navigateToMarket = () => router.push('/market')
const navigateToOrders = () => router.push('/market-dashboard?tab=orders')
const navigateToCart = () => router.push('/cart')
const navigateToStore = () => router.push('/market-dashboard?tab=store')
const navigateToProducts = () => router.push('/market-dashboard?tab=store')
</script>

View file

@ -1,331 +0,0 @@
<template>
<div class="space-y-6">
<!-- Header -->
<div>
<h2 class="text-2xl font-bold text-foreground">Market Settings</h2>
<p class="text-muted-foreground mt-1">Configure your store and market preferences</p>
</div>
<!-- Settings Tabs -->
<div class="border-b border-border">
<nav class="flex space-x-8">
<button
v-for="tab in settingsTabs"
:key="tab.id"
@click="activeSettingsTab = tab.id"
:class="[
'py-2 px-1 border-b-2 font-medium text-sm transition-colors',
activeSettingsTab === tab.id
? 'border-primary text-primary'
: 'border-transparent text-muted-foreground hover:text-foreground hover:border-muted-foreground'
]"
>
{{ tab.name }}
</button>
</nav>
</div>
<!-- Settings Content -->
<div class="min-h-[500px]">
<!-- Store Settings Tab -->
<div v-if="activeSettingsTab === 'store'" class="space-y-6">
<div class="bg-card p-6 rounded-lg border shadow-sm">
<h3 class="text-lg font-semibold text-foreground mb-4">Store Information</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label class="block text-sm font-medium text-foreground mb-2">Store Name</label>
<Input v-model="storeSettings.name" placeholder="Enter store name" />
</div>
<div>
<label class="block text-sm font-medium text-foreground mb-2">Store Description</label>
<Input v-model="storeSettings.description" placeholder="Enter store description" />
</div>
<div>
<label class="block text-sm font-medium text-foreground mb-2">Contact Email</label>
<Input v-model="storeSettings.contactEmail" type="email" placeholder="Enter contact email" />
</div>
<div>
<label class="block text-sm font-medium text-foreground mb-2">Store Category</label>
<select v-model="storeSettings.category" class="w-full px-3 py-2 border border-input rounded-md focus:outline-none focus:ring-2 focus:ring-ring focus:border-ring bg-background text-foreground">
<option value="">Select category</option>
<option value="electronics">Electronics</option>
<option value="clothing">Clothing</option>
<option value="books">Books</option>
<option value="food">Food & Beverages</option>
<option value="services">Services</option>
<option value="other">Other</option>
</select>
</div>
</div>
<div class="mt-6">
<Button @click="saveStoreSettings" variant="default">
Save Store Settings
</Button>
</div>
</div>
</div>
<!-- Payment Settings Tab -->
<div v-else-if="activeSettingsTab === 'payment'" class="space-y-6">
<div class="bg-card p-6 rounded-lg border shadow-sm">
<h3 class="text-lg font-semibold text-foreground mb-4">Payment Configuration</h3>
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-foreground mb-2">Default Currency</label>
<select v-model="paymentSettings.defaultCurrency" class="w-full px-3 py-2 border border-input rounded-md focus:outline-none focus:ring-2 focus:ring-ring focus:border-ring bg-background text-foreground">
<option value="sat">Satoshi (sats)</option>
<option value="btc">Bitcoin (BTC)</option>
<option value="usd">US Dollar (USD)</option>
<option value="eur">Euro (EUR)</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-foreground mb-2">Invoice Expiry (minutes)</label>
<Input v-model="paymentSettings.invoiceExpiry" type="number" min="5" max="1440" placeholder="60" />
</div>
<div>
<label class="block text-sm font-medium text-foreground mb-2">Auto-generate Invoices</label>
<div class="flex items-center">
<input
v-model="paymentSettings.autoGenerateInvoices"
type="checkbox"
class="h-4 w-4 text-primary focus:ring-primary border-input rounded"
/>
<label class="ml-2 text-sm text-foreground">
Automatically generate Lightning invoices for new orders
</label>
</div>
</div>
</div>
<div class="mt-6">
<Button @click="savePaymentSettings" variant="default">
Save Payment Settings
</Button>
</div>
</div>
</div>
<!-- Nostr Settings Tab -->
<div v-else-if="activeSettingsTab === 'nostr'" class="space-y-6">
<div class="bg-card p-6 rounded-lg border shadow-sm">
<h3 class="text-lg font-semibold text-foreground mb-4">Nostr Network Configuration</h3>
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-foreground mb-2">Relay Connections</label>
<div class="space-y-2">
<div v-for="relay in nostrSettings.relays" :key="relay" class="flex items-center gap-2">
<Input :value="relay" readonly class="flex-1" />
<Button @click="removeRelay(relay)" variant="outline" size="sm">
<X class="w-4 h-4" />
</Button>
</div>
<div class="flex gap-2">
<Input v-model="newRelay" placeholder="wss://relay.example.com" class="flex-1" />
<Button @click="addRelay" variant="outline">
Add Relay
</Button>
</div>
</div>
</div>
<div>
<label class="block text-sm font-medium text-foreground mb-2">Nostr Public Key</label>
<Input :value="nostrSettings.pubkey" readonly class="font-mono text-sm" />
<p class="text-xs text-muted-foreground mt-1">Your Nostr public key for receiving orders</p>
</div>
<div>
<label class="block text-sm font-medium text-foreground mb-2">Connection Status</label>
<div class="flex items-center gap-2">
<div
class="w-3 h-3 rounded-full"
:class="orderEvents.isSubscribed ? 'bg-green-500' : 'bg-yellow-500'"
></div>
<span class="text-sm text-muted-foreground">
{{ orderEvents.isSubscribed ? 'Connected to Nostr network' : 'Connecting to Nostr network...' }}
</span>
</div>
</div>
</div>
<div class="mt-6">
<Button @click="saveNostrSettings" variant="default">
Save Nostr Settings
</Button>
</div>
</div>
</div>
<!-- Shipping Settings Tab -->
<div v-else-if="activeSettingsTab === 'shipping'" class="space-y-6">
<div class="bg-card p-6 rounded-lg border shadow-sm">
<h3 class="text-lg font-semibold text-foreground mb-4">Shipping Zones</h3>
<div class="space-y-4">
<div v-for="zone in shippingSettings.zones" :key="zone.id" class="border border-border rounded-lg p-4">
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label class="block text-sm font-medium text-foreground mb-1">Zone Name</label>
<Input v-model="zone.name" placeholder="Zone name" />
</div>
<div>
<label class="block text-sm font-medium text-foreground mb-1">Cost</label>
<Input v-model="zone.cost" type="number" min="0" step="0.01" placeholder="0.00" />
</div>
<div>
<label class="block text-sm font-medium text-foreground mb-1">Currency</label>
<select v-model="zone.currency" class="w-full px-3 py-2 border border-input rounded-md focus:outline-none focus:ring-2 focus:ring-ring focus:border-ring bg-background text-foreground">
<option value="sat">Satoshi (sats)</option>
<option value="btc">Bitcoin (BTC)</option>
<option value="usd">US Dollar (USD)</option>
</select>
</div>
</div>
<div class="mt-3 flex justify-end">
<Button @click="removeShippingZone(zone.id)" variant="outline" size="sm">
Remove Zone
</Button>
</div>
</div>
<Button @click="addShippingZone" variant="outline">
<Plus class="w-4 h-4 mr-2" />
Add Shipping Zone
</Button>
</div>
<div class="mt-6">
<Button @click="saveShippingSettings" variant="default">
Save Shipping Settings
</Button>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useOrderEvents } from '@/composables/useOrderEvents'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Plus, X } from 'lucide-vue-next'
// const marketStore = useMarketStore()
const orderEvents = useOrderEvents()
// Local state
const activeSettingsTab = ref('store')
const newRelay = ref('')
// Settings data
const storeSettings = ref({
name: 'My Store',
description: 'A great place to shop',
contactEmail: 'store@example.com',
category: 'other'
})
const paymentSettings = ref({
defaultCurrency: 'sat',
invoiceExpiry: 60,
autoGenerateInvoices: true
})
const nostrSettings = ref({
relays: [
'wss://relay.damus.io',
'wss://relay.snort.social',
'wss://nostr-pub.wellorder.net'
],
pubkey: 'npub1...' // TODO: Get from auth
})
const shippingSettings = ref({
zones: [
{
id: '1',
name: 'Local',
cost: 0,
currency: 'sat',
estimatedDays: '1-2 days'
},
{
id: '2',
name: 'Domestic',
cost: 1000,
currency: 'sat',
estimatedDays: '3-5 days'
},
{
id: '3',
name: 'International',
cost: 5000,
currency: 'sat',
estimatedDays: '7-14 days'
}
]
})
// Settings tabs
const settingsTabs = [
{ id: 'store', name: 'Store Settings' },
{ id: 'payment', name: 'Payment Settings' },
{ id: 'nostr', name: 'Nostr Network' },
{ id: 'shipping', name: 'Shipping Zones' }
]
// Methods
const saveStoreSettings = () => {
// TODO: Save store settings
console.log('Saving store settings:', storeSettings.value)
}
const savePaymentSettings = () => {
// TODO: Save payment settings
console.log('Saving payment settings:', paymentSettings.value)
}
const saveNostrSettings = () => {
// TODO: Save Nostr settings
console.log('Saving Nostr settings:', nostrSettings.value)
}
const saveShippingSettings = () => {
// TODO: Save shipping settings
console.log('Saving shipping settings:', shippingSettings.value)
}
const addRelay = () => {
if (newRelay.value && !nostrSettings.value.relays.includes(newRelay.value)) {
nostrSettings.value.relays.push(newRelay.value)
newRelay.value = ''
}
}
const removeRelay = (relay: string) => {
const index = nostrSettings.value.relays.indexOf(relay)
if (index > -1) {
nostrSettings.value.relays.splice(index, 1)
}
}
const addShippingZone = () => {
const newZone = {
id: Date.now().toString(),
name: 'New Zone',
cost: 0,
currency: 'sat',
estimatedDays: '3-5 days'
}
shippingSettings.value.zones.push(newZone)
}
const removeShippingZone = (zoneId: string) => {
const index = shippingSettings.value.zones.findIndex(z => z.id === zoneId)
if (index > -1) {
shippingSettings.value.zones.splice(index, 1)
}
}
// Lifecycle
onMounted(() => {
console.log('Market Settings component loaded')
})
</script>

View file

@ -1,550 +0,0 @@
<template>
<div class="space-y-6">
<!-- Header -->
<div class="flex items-center justify-between">
<div>
<h2 class="text-2xl font-bold text-foreground">My Store</h2>
<p class="text-muted-foreground mt-1">Manage incoming orders and your products</p>
</div>
<div class="flex items-center gap-3">
<Button @click="navigateToMarket" variant="outline">
<Store class="w-4 h-4 mr-2" />
Browse Market
</Button>
<Button @click="addProduct" variant="default">
<Plus class="w-4 h-4 mr-2" />
Add Product
</Button>
</div>
</div>
<!-- Store Stats -->
<div class="grid grid-cols-1 md:grid-cols-4 gap-6">
<!-- Incoming Orders -->
<div class="bg-card p-6 rounded-lg border shadow-sm">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium text-muted-foreground">Incoming Orders</p>
<p class="text-2xl font-bold text-foreground">{{ storeStats.incomingOrders }}</p>
</div>
<div class="w-12 h-12 bg-primary/10 rounded-lg flex items-center justify-center">
<Package class="w-6 h-6 text-primary" />
</div>
</div>
<div class="mt-4">
<div class="flex items-center text-sm text-muted-foreground">
<span>{{ storeStats.pendingOrders }} pending</span>
<span class="mx-2"></span>
<span>{{ storeStats.paidOrders }} paid</span>
</div>
</div>
</div>
<!-- Total Sales -->
<div class="bg-card p-6 rounded-lg border shadow-sm">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium text-muted-foreground">Total Sales</p>
<p class="text-2xl font-bold text-foreground">{{ formatPrice(storeStats.totalSales, 'sat') }}</p>
</div>
<div class="w-12 h-12 bg-green-500/10 rounded-lg flex items-center justify-center">
<DollarSign class="w-6 h-6 text-green-500" />
</div>
</div>
<div class="mt-4">
<div class="flex items-center text-sm text-muted-foreground">
<span>Last 30 days</span>
</div>
</div>
</div>
<!-- Products -->
<div class="bg-card p-6 rounded-lg border shadow-sm">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium text-muted-foreground">Products</p>
<p class="text-2xl font-bold text-foreground">{{ storeStats.totalProducts }}</p>
</div>
<div class="w-12 h-12 bg-purple-500/10 rounded-lg flex items-center justify-center">
<Store class="w-6 h-6 text-purple-500" />
</div>
</div>
<div class="mt-4">
<div class="flex items-center text-sm text-muted-foreground">
<span>{{ storeStats.activeProducts }} active</span>
</div>
</div>
</div>
<!-- Customer Satisfaction -->
<div class="bg-card p-6 rounded-lg border shadow-sm">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium text-muted-foreground">Satisfaction</p>
<p class="text-2xl font-bold text-foreground">{{ storeStats.satisfaction }}%</p>
</div>
<div class="w-12 h-12 bg-yellow-500/10 rounded-lg flex items-center justify-center">
<Star class="w-6 h-6 text-yellow-500" />
</div>
</div>
<div class="mt-4">
<div class="flex items-center text-sm text-muted-foreground">
<span>{{ storeStats.totalReviews }} reviews</span>
</div>
</div>
</div>
</div>
<!-- Incoming Orders Section -->
<div class="bg-card rounded-lg border shadow-sm">
<div class="p-6 border-b border-border">
<h3 class="text-lg font-semibold text-foreground">Incoming Orders</h3>
<p class="text-sm text-muted-foreground mt-1">Orders waiting for your attention</p>
</div>
<div v-if="incomingOrders.length > 0" class="divide-y divide-border">
<div
v-for="order in incomingOrders"
:key="order.id"
class="p-6 hover:bg-muted/50 transition-colors"
>
<!-- Order Header -->
<div class="flex items-center justify-between mb-4">
<div class="flex items-center gap-3">
<div class="w-10 h-10 bg-primary/10 rounded-full flex items-center justify-center">
<Package class="w-5 h-5 text-primary" />
</div>
<div>
<h4 class="font-semibold text-foreground">Order #{{ order.id.slice(-8) }}</h4>
<p class="text-sm text-muted-foreground">
{{ formatDate(order.createdAt) }} {{ formatPrice(order.total, order.currency) }}
</p>
<div class="flex items-center gap-2 mt-1">
<Badge :variant="getStatusVariant(order.status)">
{{ formatStatus(order.status) }}
</Badge>
<Badge v-if="order.paymentStatus === 'pending'" variant="secondary">
Payment Pending
</Badge>
</div>
</div>
</div>
<div class="flex items-center gap-2">
<!-- Wallet Indicator -->
<div v-if="order.status === 'pending' && !order.lightningInvoice" class="text-xs text-muted-foreground mr-2">
<span>Wallet: {{ getFirstWalletName() }}</span>
</div>
<Button
v-if="order.status === 'pending' && !order.lightningInvoice"
@click="generateInvoice(order.id)"
:disabled="isGeneratingInvoice === order.id"
size="sm"
variant="default"
>
<div v-if="isGeneratingInvoice === order.id" class="flex items-center space-x-2">
<div class="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
<span>Generating...</span>
</div>
<div v-else class="flex items-center space-x-2">
<Zap class="w-4 h-4" />
<span>Generate Invoice</span>
</div>
</Button>
<Button
v-if="order.lightningInvoice"
@click="viewOrderDetails(order.id)"
size="sm"
variant="outline"
>
<Eye class="w-4 h-4 mr-2" />
View Details
</Button>
<Button
@click="processOrder(order.id)"
size="sm"
variant="outline"
>
<Check class="w-4 h-4 mr-2" />
Process
</Button>
</div>
</div>
<!-- Order Items -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<div>
<h5 class="font-medium text-foreground mb-2">Items</h5>
<div class="space-y-1">
<div
v-for="item in order.items"
:key="item.productId"
class="text-sm text-muted-foreground"
>
{{ item.productName }} × {{ item.quantity }}
</div>
</div>
</div>
<div>
<h5 class="font-medium text-foreground mb-2">Customer Info</h5>
<div class="space-y-1 text-sm text-muted-foreground">
<p v-if="order.contactInfo.email">
<span class="font-medium">Email:</span> {{ order.contactInfo.email }}
</p>
<p v-if="order.contactInfo.message">
<span class="font-medium">Message:</span> {{ order.contactInfo.message }}
</p>
<p v-if="order.contactInfo.address">
<span class="font-medium">Address:</span> {{ order.contactInfo.address }}
</p>
</div>
</div>
</div>
<!-- Payment Status -->
<div v-if="order.lightningInvoice" class="p-4 bg-green-500/10 border border-green-200 rounded-lg">
<div class="flex items-center justify-between">
<div class="flex items-center gap-2">
<CheckCircle class="w-5 h-5 text-green-600" />
<span class="text-sm font-medium text-green-900">Lightning Invoice Generated</span>
</div>
<div class="text-sm text-green-700">
Amount: {{ formatPrice(order.total, order.currency) }}
</div>
</div>
</div>
<div v-else class="p-4 bg-yellow-500/10 border border-yellow-200 rounded-lg">
<div class="flex items-center gap-2">
<AlertCircle class="w-5 h-5 text-yellow-600" />
<span class="text-sm font-medium text-yellow-900">Invoice Required</span>
</div>
</div>
</div>
</div>
<div v-else class="p-6 text-center text-muted-foreground">
<Package class="w-12 h-12 mx-auto mb-3 text-muted-foreground/50" />
<p>No incoming orders</p>
<p class="text-sm">Orders from customers will appear here</p>
</div>
</div>
<!-- Quick Actions -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- Order Management -->
<div class="bg-card p-6 rounded-lg border shadow-sm">
<h3 class="text-lg font-semibold text-foreground mb-4">Order Management</h3>
<div class="space-y-3">
<Button
@click="viewAllOrders"
variant="default"
class="w-full justify-start"
>
<Package class="w-4 h-4 mr-2" />
View All Orders
</Button>
<Button
@click="generateBulkInvoices"
variant="outline"
class="w-full justify-start"
>
<Zap class="w-4 h-4 mr-2" />
Generate Bulk Invoices
</Button>
<Button
@click="exportOrders"
variant="outline"
class="w-full justify-start"
>
<Download class="w-4 h-4 mr-2" />
Export Orders
</Button>
</div>
</div>
<!-- Store Management -->
<div class="bg-card p-6 rounded-lg border shadow-sm">
<h3 class="text-lg font-semibold text-foreground mb-4">Store Management</h3>
<div class="space-y-3">
<Button
@click="manageProducts"
variant="default"
class="w-full justify-start"
>
<Store class="w-4 h-4 mr-2" />
Manage Products
</Button>
<Button
@click="storeSettings"
variant="outline"
class="w-full justify-start"
>
<Settings class="w-4 h-4 mr-2" />
Store Settings
</Button>
<Button
@click="analytics"
variant="outline"
class="w-full justify-start"
>
<BarChart3 class="w-4 h-4 mr-2" />
View Analytics
</Button>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useMarketStore } from '@/stores/market'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import {
Package,
Store,
DollarSign,
Star,
Plus,
Zap,
Eye,
Check,
AlertCircle,
CheckCircle,
Download,
Settings,
BarChart3
} from 'lucide-vue-next'
import type { OrderStatus } from '@/stores/market'
import { nostrOrders } from '@/composables/useNostrOrders'
import { auth } from '@/composables/useAuth'
const router = useRouter()
const marketStore = useMarketStore()
const nostrOrdersComposable = nostrOrders
// Local state
const isGeneratingInvoice = ref<string | null>(null)
// Computed properties
const incomingOrders = computed(() => {
// For now, show all orders as "incoming" since we don't have merchant filtering yet
// In a real implementation, this would filter orders where the current user is the seller
return Object.values(marketStore.orders)
.filter(order => order.status === 'pending')
.sort((a, b) => b.createdAt - a.createdAt)
})
const storeStats = computed(() => {
const orders = Object.values(marketStore.orders)
const now = Date.now() / 1000
const thirtyDaysAgo = now - (30 * 24 * 60 * 60)
return {
incomingOrders: orders.filter(o => o.status === 'pending').length,
pendingOrders: orders.filter(o => o.status === 'pending').length,
paidOrders: orders.filter(o => o.status === 'paid').length,
totalSales: orders
.filter(o => o.status === 'paid' && o.createdAt > thirtyDaysAgo)
.reduce((sum, o) => sum + o.total, 0),
totalProducts: 0, // TODO: Implement product management
activeProducts: 0, // TODO: Implement product management
satisfaction: 95, // TODO: Implement review system
totalReviews: 0 // TODO: Implement review system
}
})
// Methods
const formatDate = (timestamp: number) => {
return new Date(timestamp * 1000).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
})
}
const formatStatus = (status: OrderStatus) => {
const statusMap: Record<OrderStatus, string> = {
pending: 'Pending',
paid: 'Paid',
processing: 'Processing',
shipped: 'Shipped',
delivered: 'Delivered',
cancelled: 'Cancelled'
}
return statusMap[status] || status
}
const getStatusVariant = (status: OrderStatus) => {
const variantMap: Record<OrderStatus, 'default' | 'secondary' | 'outline' | 'destructive'> = {
pending: 'outline',
paid: 'secondary',
processing: 'secondary',
shipped: 'default',
delivered: 'default',
cancelled: 'destructive'
}
return variantMap[status] || 'outline'
}
const formatPrice = (price: number, currency: string) => {
return marketStore.formatPrice(price, currency)
}
const generateInvoice = async (orderId: string) => {
console.log('Generating invoice for order:', orderId)
isGeneratingInvoice.value = orderId
try {
// Get the order from the store
const order = marketStore.orders[orderId]
if (!order) {
throw new Error('Order not found')
}
// Temporary fix: If buyerPubkey is missing, try to get it from auth
if (!order.buyerPubkey && auth.currentUser?.value?.pubkey) {
console.log('Fixing missing buyerPubkey for existing order')
marketStore.updateOrder(order.id, { buyerPubkey: auth.currentUser.value.pubkey })
}
// Temporary fix: If sellerPubkey is missing, use current user's pubkey
if (!order.sellerPubkey && auth.currentUser?.value?.pubkey) {
console.log('Fixing missing sellerPubkey for existing order')
marketStore.updateOrder(order.id, { sellerPubkey: auth.currentUser.value.pubkey })
}
// Get the updated order
const updatedOrder = marketStore.orders[orderId]
console.log('Order details for invoice generation:', {
orderId: updatedOrder.id,
orderFields: Object.keys(updatedOrder),
buyerPubkey: updatedOrder.buyerPubkey,
sellerPubkey: updatedOrder.sellerPubkey,
status: updatedOrder.status,
total: updatedOrder.total
})
// Get the user's wallet list
const userWallets = auth.currentUser?.value?.wallets || []
console.log('Available wallets:', userWallets)
if (userWallets.length === 0) {
throw new Error('No wallet available to generate invoice. Please ensure you have at least one wallet configured.')
}
// Use the first available wallet for invoice generation
const walletId = userWallets[0].id
const walletName = userWallets[0].name
const adminKey = userWallets[0].adminkey
console.log('Using wallet for invoice generation:', { walletId, walletName, balance: userWallets[0].balance_msat })
const invoice = await marketStore.createLightningInvoice(orderId, adminKey)
if (invoice) {
console.log('Lightning invoice created:', invoice)
// Send the invoice to the customer via Nostr
await sendInvoiceToCustomer(updatedOrder, invoice)
console.log('Invoice sent to customer successfully')
// Show success message (you could add a toast notification here)
alert(`Invoice generated successfully using wallet: ${walletName}`)
} else {
throw new Error('Failed to create Lightning invoice')
}
} catch (error) {
console.error('Failed to generate invoice:', error)
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'
// Show error message to user
alert(`Failed to generate invoice: ${errorMessage}`)
} finally {
isGeneratingInvoice.value = null
}
}
const sendInvoiceToCustomer = async (order: any, invoice: any) => {
try {
console.log('Sending invoice to customer for order:', {
orderId: order.id,
buyerPubkey: order.buyerPubkey,
sellerPubkey: order.sellerPubkey,
invoiceFields: Object.keys(invoice)
})
// Check if we have the buyer's public key
if (!order.buyerPubkey) {
console.error('Missing buyerPubkey in order:', order)
throw new Error('Cannot send invoice: buyer public key not found')
}
// Update the order with the invoice details
const updatedOrder = {
...order,
lightningInvoice: invoice,
paymentHash: invoice.payment_hash,
paymentStatus: 'pending',
paymentRequest: invoice.bolt11, // Use bolt11 field from LNBits response
updatedAt: Math.floor(Date.now() / 1000)
}
// Update the order in the store
marketStore.updateOrder(order.id, updatedOrder)
// Send the updated order to the customer via Nostr
// This will include the invoice information
await nostrOrdersComposable.publishOrderEvent(updatedOrder, order.buyerPubkey)
console.log('Updated order with invoice sent via Nostr to customer:', order.buyerPubkey)
} catch (error) {
console.error('Failed to send invoice to customer:', error)
throw error
}
}
const viewOrderDetails = (orderId: string) => {
// TODO: Navigate to detailed order view
console.log('Viewing order details:', orderId)
}
const processOrder = (orderId: string) => {
// TODO: Implement order processing
console.log('Processing order:', orderId)
}
const addProduct = () => {
// TODO: Navigate to add product form
console.log('Adding new product')
}
const navigateToMarket = () => router.push('/market')
const viewAllOrders = () => router.push('/market-dashboard?tab=orders')
const generateBulkInvoices = () => console.log('Generate bulk invoices')
const exportOrders = () => console.log('Export orders')
const manageProducts = () => console.log('Manage products')
const storeSettings = () => router.push('/market-dashboard?tab=settings')
const analytics = () => console.log('View analytics')
const getFirstWalletName = () => {
const userWallets = auth.currentUser?.value?.wallets || []
if (userWallets.length > 0) {
return userWallets[0].name
}
return 'N/A'
}
// Lifecycle
onMounted(() => {
console.log('Merchant Store component loaded')
})
</script>

View file

@ -1,500 +0,0 @@
<template>
<div class="space-y-6">
<!-- Header -->
<div class="flex items-center justify-between">
<div>
<h2 class="text-2xl font-bold text-foreground">My Orders</h2>
<p class="text-muted-foreground mt-1">Track all your market orders and payments</p>
</div>
<div class="flex items-center gap-3">
<!-- Order Events Status -->
<div class="flex items-center gap-2 text-sm text-muted-foreground">
<div class="w-2 h-2 rounded-full" :class="orderEvents.isSubscribed ? 'bg-green-500' : 'bg-yellow-500'"></div>
<span>{{ orderEvents.isSubscribed ? 'Live updates' : 'Connecting...' }}</span>
</div>
<Button @click="navigateToMarket" variant="outline">
<Store class="w-4 h-4 mr-2" />
Browse Market
</Button>
</div>
</div>
<!-- Filters and Stats -->
<div class="flex flex-col sm:flex-row gap-4 items-start sm:items-center justify-between">
<!-- Order Stats -->
<div class="flex gap-4 text-sm">
<div class="flex items-center gap-2">
<span class="text-muted-foreground">Total:</span>
<Badge variant="secondary">{{ totalOrders }}</Badge>
</div>
<div class="flex items-center gap-2">
<span class="text-muted-foreground">Pending:</span>
<Badge variant="outline" class="text-amber-600">{{ pendingOrders }}</Badge>
</div>
<div class="flex items-center gap-2">
<span class="text-muted-foreground">Paid:</span>
<Badge variant="outline" class="text-green-600">{{ paidOrders }}</Badge>
</div>
<div class="flex items-center gap-2">
<span class="text-muted-foreground">Payment Due:</span>
<Badge variant="outline" class="text-red-600">{{ pendingPayments }}</Badge>
</div>
</div>
<!-- Filter Controls -->
<div class="flex gap-2">
<select v-model="statusFilter"
class="px-3 py-2 border border-input rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-ring focus:border-ring bg-background text-foreground">
<option value="">All Statuses</option>
<option value="pending">Pending</option>
<option value="paid">Paid</option>
<option value="processing">Processing</option>
<option value="shipped">Shipped</option>
<option value="delivered">Delivered</option>
<option value="cancelled">Cancelled</option>
</select>
<select v-model="sortBy"
class="px-3 py-2 border border-input rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-ring focus:border-ring bg-background text-foreground">
<option value="createdAt">Date Created</option>
<option value="total">Order Total</option>
<option value="status">Status</option>
</select>
</div>
</div>
<!-- Orders List -->
<div v-if="filteredOrders.length > 0" class="space-y-4">
<div v-for="order in sortedOrders" :key="order.id"
class="bg-card border rounded-lg p-6 hover:shadow-md transition-shadow">
<!-- Order Header -->
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-4">
<div class="flex items-center gap-3">
<div class="w-10 h-10 bg-primary/10 rounded-full flex items-center justify-center">
<Package class="w-5 h-5 text-primary" />
</div>
<div>
<h3 class="font-semibold text-foreground">Order #{{ order.id.slice(-8) }}</h3>
<p class="text-sm text-muted-foreground">
{{ formatDate(order.createdAt) }}
</p>
</div>
</div>
<div class="flex items-center gap-3">
<Badge :variant="getStatusVariant(order.status)" :class="getStatusColor(order.status)">
{{ formatStatus(order.status) }}
</Badge>
<div class="text-right">
<p class="text-lg font-semibold text-foreground">
{{ formatPrice(order.total, order.currency) }}
</p>
</div>
</div>
</div>
<!-- Order Items -->
<div class="mb-4">
<h4 class="font-medium text-foreground mb-2">Items</h4>
<div class="space-y-1">
<div v-for="item in order.items.slice(0, 3)" :key="item.productId" class="text-sm text-muted-foreground">
{{ item.productName }} × {{ item.quantity }}
</div>
<div v-if="order.items.length > 3" class="text-sm text-muted-foreground">
+{{ order.items.length - 3 }} more items
</div>
</div>
</div>
<!-- Payment Section -->
<div v-if="order.lightningInvoice" class="mb-4 p-4 bg-muted/50 border border-border rounded-lg">
<div class="flex items-center justify-between mb-3">
<div class="flex items-center gap-2">
<Zap class="w-4 h-4 text-yellow-500" />
<span class="font-medium text-foreground">Payment Required</span>
</div>
<Badge :variant="order.paymentStatus === 'paid' ? 'default' : 'secondary'" class="text-xs"
:class="order.paymentStatus === 'paid' ? 'text-green-600' : 'text-amber-600'">
{{ order.paymentStatus === 'paid' ? 'Paid' : 'Pending' }}
</Badge>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<!-- Payment Details -->
<div class="space-y-3">
<!-- Payment Request -->
<div>
<label class="block text-xs font-medium text-muted-foreground mb-1">
Payment Request
</label>
<div class="flex items-center gap-2">
<input :value="order.lightningInvoice?.bolt11 || ''" readonly disabled
class="flex-1 font-mono text-xs bg-muted border border-input rounded-md px-3 py-1 text-foreground" />
<Button @click="copyPaymentRequest(order.lightningInvoice?.bolt11 || '')" variant="outline" size="sm">
<Copy class="w-3 h-3" />
</Button>
</div>
</div>
<!-- Payment Actions -->
<div class="flex gap-2">
<Button @click="openLightningWallet(order.lightningInvoice?.bolt11 || '')" variant="default" size="sm"
class="flex-1" :disabled="!order.lightningInvoice?.bolt11">
<Zap class="w-3 h-3 mr-1" />
Pay with Lightning
</Button>
<Button @click="toggleQRCode(order.id)" variant="outline" size="sm">
<QrCode class="w-3 h-3" />
{{ order.showQRCode ? 'Hide QR' : 'Show QR' }}
</Button>
</div>
</div>
<!-- QR Code -->
<div v-if="order.showQRCode" class="flex justify-center">
<div class="w-48 h-48">
<div v-if="order.qrCodeDataUrl && !order.qrCodeError" class="w-full h-full">
<img :src="order.qrCodeDataUrl"
:alt="`QR Code for ${formatPrice(order.total, order.currency)} payment`"
class="w-full h-full border border-border rounded-lg" />
</div>
<div v-else-if="order.qrCodeLoading"
class="w-full h-full bg-muted rounded-lg flex items-center justify-center">
<div class="text-center text-muted-foreground">
<div class="text-4xl mb-2 animate-pulse"></div>
<div class="text-sm">Generating QR...</div>
</div>
</div>
<div v-else class="w-full h-full bg-muted rounded-lg flex items-center justify-center">
<div class="text-center text-muted-foreground">
<div class="text-4xl mb-2"></div>
<div class="text-sm">No QR</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Waiting for Invoice -->
<div v-else-if="order.status === 'pending'" class="mb-4 p-4 bg-muted/50 border border-border rounded-lg">
<div class="flex items-center gap-2 mb-2">
<div class="w-2 h-2 bg-amber-500 rounded-full animate-pulse"></div>
<span class="font-medium text-foreground">Waiting for Payment Invoice</span>
</div>
<p class="text-sm text-muted-foreground">
The merchant will send you a Lightning invoice via Nostr once they process your order
</p>
</div>
<!-- Actions -->
<div class="flex justify-end gap-2 pt-4 border-t border-border">
<Button v-if="order.status === 'pending'" variant="outline" size="sm" @click="cancelOrder(order.id)">
Cancel Order
</Button>
<Button variant="outline" size="sm" @click="copyOrderId(order.id)">
Copy Order ID
</Button>
</div>
</div>
</div>
<!-- Debug Information (Development Only) -->
<div v-if="isDevelopment" class="mt-8 p-4 bg-gray-100 rounded-lg">
<h4 class="font-medium mb-2">Debug Information</h4>
<div class="text-sm space-y-1">
<div>Total Orders in Store: {{ Object.keys(marketStore.orders).length }}</div>
<div>Filtered Orders: {{ filteredOrders.length }}</div>
<div>Order Events Subscribed: {{ orderEvents.isSubscribed ? 'Yes' : 'No' }}</div>
<div>Relay Hub Connected: {{ relayHub.isConnected ? 'Yes' : 'No' }}</div>
<div>Auth Status: {{ auth.isAuthenticated ? 'Authenticated' : 'Not Authenticated' }}</div>
<div>Current User: {{ auth.currentUser?.value?.pubkey ? 'Yes' : 'No' }}</div>
</div>
</div>
<!-- Empty State -->
<div v-else class="text-center py-12">
<div class="w-16 h-16 bg-muted rounded-full flex items-center justify-center mx-auto mb-4">
<Package class="w-8 h-8 text-muted-foreground" />
</div>
<h3 class="text-lg font-medium text-foreground mb-2">No orders yet</h3>
<p class="text-muted-foreground mb-6">
Start shopping in the market to see your order history here
</p>
<Button @click="navigateToMarket" variant="default">
Browse Market
</Button>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, watch } from 'vue'
import { useRouter } from 'vue-router'
import { useMarketStore } from '@/stores/market'
import { useOrderEvents } from '@/composables/useOrderEvents'
import { relayHubComposable } from '@/composables/useRelayHub'
import { auth } from '@/composables/useAuth'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { Package, Store, Zap, Copy, QrCode } from 'lucide-vue-next'
import { toast } from 'vue-sonner'
import type { OrderStatus } from '@/stores/market'
const router = useRouter()
const marketStore = useMarketStore()
const relayHub = relayHubComposable
const orderEvents = useOrderEvents()
// Local state
const statusFilter = ref('')
const sortBy = ref('createdAt')
// Computed properties
const allOrders = computed(() => Object.values(marketStore.orders))
const filteredOrders = computed(() => {
if (!statusFilter.value) return allOrders.value
return allOrders.value.filter(order => order.status === statusFilter.value)
})
const sortedOrders = computed(() => {
const orders = [...filteredOrders.value]
switch (sortBy.value) {
case 'total':
return orders.sort((a, b) => b.total - a.total)
case 'status':
return orders.sort((a, b) => a.status.localeCompare(b.status))
case 'createdAt':
default:
return orders.sort((a, b) => b.createdAt - a.createdAt)
}
})
const totalOrders = computed(() => allOrders.value.length)
const pendingOrders = computed(() => allOrders.value.filter(o => o.status === 'pending').length)
const paidOrders = computed(() => allOrders.value.filter(o => o.status === 'paid').length)
const pendingPayments = computed(() => allOrders.value.filter(o => o.paymentStatus === 'pending').length)
const isDevelopment = computed(() => import.meta.env.DEV)
// Methods
const formatDate = (timestamp: number) => {
return new Date(timestamp * 1000).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
})
}
const formatStatus = (status: OrderStatus) => {
const statusMap: Record<OrderStatus, string> = {
pending: 'Pending',
paid: 'Paid',
processing: 'Processing',
shipped: 'Shipped',
delivered: 'Delivered',
cancelled: 'Cancelled'
}
return statusMap[status] || status
}
const getStatusVariant = (status: OrderStatus) => {
const variantMap: Record<OrderStatus, 'default' | 'secondary' | 'outline' | 'destructive'> = {
pending: 'outline',
paid: 'secondary',
processing: 'secondary',
shipped: 'default',
delivered: 'default',
cancelled: 'destructive'
}
return variantMap[status] || 'outline'
}
const getStatusColor = (status: OrderStatus) => {
const colorMap: Record<OrderStatus, string> = {
pending: 'text-amber-600',
paid: 'text-green-600',
processing: 'text-blue-600',
shipped: 'text-blue-600',
delivered: 'text-green-600',
cancelled: 'text-red-600'
}
return colorMap[status] || 'text-muted-foreground'
}
const formatPrice = (price: number, currency: string) => {
return marketStore.formatPrice(price, currency)
}
const cancelOrder = (orderId: string) => {
// TODO: Implement order cancellation
console.log('Cancelling order:', orderId)
}
const copyOrderId = async (orderId: string) => {
try {
await navigator.clipboard.writeText(orderId)
toast.success('Order ID copied to clipboard')
console.log('Order ID copied to clipboard')
} catch (err) {
console.error('Failed to copy order ID:', err)
toast.error('Failed to copy order ID')
}
}
const copyPaymentRequest = async (paymentRequest: string) => {
console.log('Copying payment request:', {
paymentRequest: paymentRequest?.substring(0, 50) + '...',
hasValue: !!paymentRequest,
length: paymentRequest?.length
})
if (!paymentRequest) {
toast.error('No payment request available', {
description: 'Please wait for the merchant to send the payment request'
})
return
}
try {
await navigator.clipboard.writeText(paymentRequest)
toast.success('Payment request copied to clipboard', {
description: 'You can now paste it into your Lightning wallet'
})
console.log('Payment request copied to clipboard')
} catch (err) {
console.error('Failed to copy payment request:', err)
toast.error('Failed to copy payment request', {
description: 'Please try again or copy manually'
})
}
}
const openLightningWallet = (paymentRequest: string) => {
// Try to open with common Lightning wallet protocols
const protocols = [
`lightning:${paymentRequest}`,
`bitcoin:${paymentRequest}`,
paymentRequest
]
// Try each protocol
for (const protocol of protocols) {
try {
window.open(protocol, '_blank')
toast.success('Opening Lightning wallet', {
description: 'If your wallet doesn\'t open, copy the payment request manually'
})
return
} catch (err) {
console.warn('Failed to open protocol:', protocol, err)
}
}
// Fallback: copy to clipboard
copyPaymentRequest(paymentRequest)
}
const toggleQRCode = async (orderId: string) => {
// Toggle QR code visibility for the order
const order = marketStore.orders[orderId]
if (order) {
// If showing QR code and it doesn't exist yet, generate it
if (!order.showQRCode && order.lightningInvoice?.bolt11 && !order.qrCodeDataUrl) {
await generateQRCode(orderId, order.lightningInvoice.bolt11)
}
marketStore.updateOrder(orderId, {
showQRCode: !order.showQRCode
})
}
}
const generateQRCode = async (orderId: string, bolt11: string) => {
try {
// Set loading state
marketStore.updateOrder(orderId, {
qrCodeLoading: true,
qrCodeError: null
})
// Import QRCode library dynamically
const QRCode = await import('qrcode')
// Generate QR code
const qrCodeDataUrl = await QRCode.toDataURL(bolt11, {
width: 192,
margin: 2,
color: {
dark: '#000000',
light: '#FFFFFF'
}
})
// Update order with QR code
marketStore.updateOrder(orderId, {
qrCodeDataUrl,
qrCodeLoading: false,
qrCodeError: null
})
console.log('QR code generated for order:', orderId)
} catch (error) {
console.error('Failed to generate QR code:', error)
marketStore.updateOrder(orderId, {
qrCodeLoading: false,
qrCodeError: error instanceof Error ? error.message : 'Failed to generate QR code'
})
}
}
const navigateToMarket = () => router.push('/market')
// Load orders on mount
onMounted(() => {
// Orders are already loaded in the market store
console.log('Order History component loaded with', allOrders.value.length, 'orders')
console.log('Market store orders:', marketStore.orders)
// Debug: Log order details for orders with payment requests
allOrders.value.forEach(order => {
if (order.paymentRequest) {
console.log('Order with payment request:', {
id: order.id,
paymentRequest: order.paymentRequest.substring(0, 50) + '...',
hasPaymentRequest: !!order.paymentRequest,
status: order.status,
paymentStatus: order.paymentStatus
})
}
})
console.log('Order events status:', orderEvents.isSubscribed.value)
console.log('Relay hub connected:', relayHub.isConnected.value)
console.log('Auth status:', auth.isAuthenticated)
console.log('Current user:', auth.currentUser?.value?.pubkey)
// Start listening for order events if not already listening
if (!orderEvents.isSubscribed.value) {
console.log('Starting order events listener...')
orderEvents.initialize()
} else {
console.log('Order events already listening')
}
})
// Watch for authentication and relay hub readiness
watch(
[() => auth.isAuthenticated, () => relayHub.isConnected.value],
([isAuth, isConnected]) => {
if (isAuth && isConnected && !orderEvents.isSubscribed.value) {
console.log('Auth and relay hub ready, starting order events listener...')
orderEvents.initialize()
}
},
{ immediate: true }
)
</script>

View file

@ -1,334 +1,4 @@
<template>
<div class="bg-background border rounded-lg p-6">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-foreground">Payment</h3>
<Badge :variant="getPaymentStatusVariant(paymentStatus)">
{{ formatPaymentStatus(paymentStatus) }}
</Badge>
</div>
<!-- Invoice Information -->
<div v-if="invoice" class="space-y-4">
<!-- Amount and Status -->
<div class="flex items-center justify-between p-4 bg-muted/50 rounded-lg">
<div>
<p class="text-sm text-muted-foreground">Amount</p>
<p class="text-2xl font-bold text-foreground">
{{ invoice.amount }} {{ currency }}
</p>
</div>
<div class="text-right">
<p class="text-sm text-muted-foreground">Status</p>
<p class="text-lg font-semibold" :class="getStatusColor(paymentStatus)">
{{ formatPaymentStatus(paymentStatus) }}
</p>
</div>
</div>
<!-- Lightning Invoice QR Code -->
<div class="text-center">
<div class="mb-4">
<h4 class="font-medium text-foreground mb-2">Lightning Invoice</h4>
<p class="text-sm text-muted-foreground">Scan with your Lightning wallet to pay</p>
</div>
<!-- QR Code -->
<div class="w-48 h-48 mx-auto mb-4">
<div v-if="qrCodeDataUrl && !qrCodeError" class="w-full h-full">
<img
:src="qrCodeDataUrl"
:alt="`QR Code for ${invoice.amount} ${currency} payment`"
class="w-full h-full border border-border rounded-lg"
/>
</div>
<div v-else-if="qrCodeLoading" class="w-full h-full bg-muted rounded-lg flex items-center justify-center">
<div class="text-center text-muted-foreground">
<div class="text-4xl mb-2 animate-pulse"></div>
<div class="text-sm">Generating QR...</div>
</div>
</div>
<div v-else-if="qrCodeError" class="w-full h-full bg-destructive/10 border border-destructive/20 rounded-lg flex items-center justify-center">
<div class="text-center text-destructive">
<div class="text-4xl mb-2"></div>
<div class="text-sm">{{ qrCodeError }}</div>
<Button
@click="retryQRCode"
variant="outline"
size="sm"
class="mt-2"
>
Retry
</Button>
</div>
</div>
<div v-else class="w-full h-full bg-muted rounded-lg flex items-center justify-center">
<div class="text-center text-muted-foreground">
<div class="text-4xl mb-2"></div>
<div class="text-sm">No invoice</div>
</div>
</div>
</div>
<!-- QR Code Actions -->
<div v-if="qrCodeDataUrl && !qrCodeError" class="mb-4">
<!-- Download button removed -->
</div>
<!-- Payment Request -->
<div class="mb-4">
<label class="block text-sm font-medium text-foreground mb-2">
Payment Request
</label>
<div class="flex items-center gap-2">
<Input
:value="invoice.bolt11"
readonly
class="flex-1 font-mono text-sm"
/>
<Button
@click="copyPaymentRequest"
variant="outline"
size="sm"
>
<Copy class="w-4 h-4" />
</Button>
</div>
</div>
<!-- Copy to Wallet Button -->
<Button
@click="openInWallet"
variant="default"
class="w-full"
>
<Wallet class="w-4 h-4 mr-2" />
Open in Lightning Wallet
</Button>
</div>
<!-- Payment Details -->
<div class="border-t pt-4">
<h4 class="font-medium text-foreground mb-3">Payment Details</h4>
<div class="space-y-2 text-sm">
<div class="flex justify-between">
<span class="text-muted-foreground">Payment Hash:</span>
<span class="font-mono text-foreground">{{ formatHash(invoice.payment_hash) }}</span>
</div>
<div class="flex justify-between">
<span class="text-muted-foreground">Created:</span>
<span class="text-foreground">{{ formatDate(invoice.created_at) }}</span>
</div>
<div class="flex justify-between">
<span class="text-muted-foreground">Expires:</span>
<span class="text-foreground">{{ formatDate(invoice.expiry) }}</span>
</div>
<div v-if="paidAt" class="flex justify-between">
<span class="text-muted-foreground">Paid At:</span>
<span class="text-foreground">{{ formatDate(paidAt) }}</span>
</div>
</div>
</div>
</div>
<!-- No Invoice State -->
<div v-else class="text-center py-8">
<div class="w-16 h-16 bg-muted rounded-full flex items-center justify-center mx-auto mb-4">
<Wallet class="w-8 h-8 text-muted-foreground" />
</div>
<h4 class="text-lg font-medium text-foreground mb-2">No Payment Invoice</h4>
<p class="text-muted-foreground mb-4">
A Lightning invoice will be sent by the merchant once they process your order.
</p>
<p class="text-sm text-muted-foreground">
You'll receive the invoice via Nostr when it's ready.
</p>
</div>
<!-- Payment Instructions -->
<div v-if="paymentStatus === 'pending'" class="mt-6 p-4 bg-muted/50 border border-border rounded-lg">
<div class="flex items-start space-x-3">
<div class="w-5 h-5 bg-muted rounded-full flex items-center justify-center mt-0.5">
<Info class="w-3 h-3 text-muted-foreground" />
</div>
<div class="text-sm text-muted-foreground">
<h5 class="font-medium mb-1 text-foreground">Payment Instructions</h5>
<ul class="space-y-1">
<li> Use a Lightning-compatible wallet (e.g., Phoenix, Breez, Alby)</li>
<li> Scan the QR code or copy the payment request</li>
<li> Confirm the payment amount and send</li>
<li> Your order will be processed once payment is confirmed</li>
</ul>
</div>
</div>
</div>
<!-- Payment Success -->
<div v-if="paymentStatus === 'paid'" class="mt-6 p-4 bg-green-50 border border-green-200 rounded-lg">
<div class="flex items-center space-x-3">
<div class="w-5 h-5 bg-green-100 rounded-full flex items-center justify-center">
<CheckCircle class="w-3 h-3 text-green-600" />
</div>
<div class="text-sm text-green-800">
<h5 class="font-medium">Payment Confirmed!</h5>
<p>Your order is being processed. You'll receive updates via Nostr.</p>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, onMounted, ref, watch } from 'vue'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Badge } from '@/components/ui/badge'
import {
Copy,
Wallet,
Info,
CheckCircle
} from 'lucide-vue-next'
import { useMarketStore } from '@/stores/market'
import QRCode from 'qrcode'
interface Props {
orderId: string
}
const props = defineProps<Props>()
const marketStore = useMarketStore()
// Computed properties
const order = computed(() => marketStore.orders[props.orderId])
const invoice = computed(() => order.value?.lightningInvoice)
const paymentStatus = computed(() => order.value?.paymentStatus || 'pending')
const currency = computed(() => order.value?.currency || 'sat')
const paidAt = computed(() => order.value?.paidAt)
// QR Code generation
const qrCodeDataUrl = ref<string | null>(null)
const qrCodeLoading = ref(false)
const qrCodeError = ref<string | null>(null)
const generateQRCode = async (paymentRequest: string) => {
try {
qrCodeLoading.value = true
qrCodeError.value = null
const dataUrl = await QRCode.toDataURL(paymentRequest, {
width: 192, // 48 * 4 for high DPI displays
margin: 2,
color: {
dark: '#000000',
light: '#FFFFFF'
}
})
qrCodeDataUrl.value = dataUrl
} catch (error) {
console.error('Failed to generate QR code:', error)
qrCodeError.value = 'Failed to generate QR code'
qrCodeDataUrl.value = null
} finally {
qrCodeLoading.value = false
}
}
// Methods
const getPaymentStatusVariant = (status: string) => {
switch (status) {
case 'paid': return 'default'
case 'pending': return 'secondary'
case 'expired': return 'destructive'
default: return 'outline'
}
}
const formatPaymentStatus = (status: string) => {
switch (status) {
case 'paid': return 'Paid'
case 'pending': return 'Pending'
case 'expired': return 'Expired'
default: return 'Unknown'
}
}
const getStatusColor = (status: string) => {
switch (status) {
case 'paid': return 'text-green-600'
case 'pending': return 'text-amber-600'
case 'expired': return 'text-destructive'
default: return 'text-muted-foreground'
}
}
const formatHash = (hash: string) => {
if (!hash) return 'N/A'
return `${hash.substring(0, 8)}...${hash.substring(hash.length - 8)}`
}
const formatDate = (dateValue: string | number | undefined) => {
if (!dateValue) return 'N/A'
let timestamp: number
if (typeof dateValue === 'string') {
// Handle ISO date strings from LNBits API
timestamp = new Date(dateValue).getTime()
} else {
// Handle Unix timestamps (seconds) from our store
timestamp = dateValue * 1000
}
return new Date(timestamp).toLocaleString()
}
const copyPaymentRequest = async () => {
if (!invoice.value?.bolt11) return
try {
await navigator.clipboard.writeText(invoice.value.bolt11)
// TODO: Show toast notification
console.log('Payment request copied to clipboard')
} catch (error) {
console.error('Failed to copy payment request:', error)
}
}
const openInWallet = () => {
if (!invoice.value?.bolt11) return
// Open in Lightning wallet
const walletUrl = `lightning:${invoice.value.bolt11}`
window.open(walletUrl, '_blank')
}
const retryQRCode = () => {
if (invoice.value?.bolt11) {
generateQRCode(invoice.value.bolt11)
}
}
// Lifecycle
onMounted(() => {
// Set up payment monitoring if invoice exists
if (invoice.value && props.orderId) {
// Payment monitoring is handled by the market store
console.log('Payment display mounted for order:', props.orderId)
// Generate QR code for the invoice
if (invoice.value.bolt11) {
generateQRCode(invoice.value.bolt11)
}
}
})
// Watch for invoice changes to regenerate QR code
watch(() => invoice.value?.bolt11, (newPaymentRequest) => {
if (newPaymentRequest) {
generateQRCode(newPaymentRequest)
} else {
qrCodeDataUrl.value = null
}
}, { immediate: true })
</script>
<script lang="ts">
import PaymentDisplayComponent from '@/modules/market/components/PaymentDisplay.vue'
export default PaymentDisplayComponent
</script>

View file

@ -1,287 +0,0 @@
<template>
<Dialog :open="modelValue" @update:open="updateOpen">
<DialogContent class="sm:max-w-md">
<DialogHeader>
<DialogTitle>Payment Request</DialogTitle>
<DialogDescription>
Complete your payment to finalize your order
</DialogDescription>
</DialogHeader>
<div v-if="paymentRequest" class="space-y-4">
<!-- Order Details -->
<div class="space-y-2">
<h4 class="text-sm font-medium">Order Details</h4>
<div class="text-xs text-muted-foreground">
Order ID: {{ paymentRequest.id }}
</div>
<div v-if="paymentRequest.message" class="text-sm">
{{ paymentRequest.message }}
</div>
</div>
<!-- Payment Options -->
<div class="space-y-2">
<h4 class="text-sm font-medium">Payment Options</h4>
<div v-for="option in paymentRequest.payment_options" :key="option.type" class="space-y-2">
<Card>
<CardContent class="p-4">
<div class="flex items-center justify-between">
<div class="flex-1">
<div class="font-medium">{{ getPaymentTypeLabel(option.type) }}</div>
<div class="text-xs text-muted-foreground truncate">{{ option.link }}</div>
</div>
<div class="ml-4">
<Button
:variant="option.type === 'ln' ? 'default' : 'outline'"
size="sm"
@click="handlePayment(option)"
>
<component :is="getPaymentTypeIcon(option.type)" class="h-4 w-4" />
</Button>
</div>
</div>
</CardContent>
</Card>
</div>
</div>
<!-- Lightning Invoice QR Code -->
<div v-if="lightningInvoice" class="space-y-4">
<h4 class="text-sm font-medium">Lightning Invoice</h4>
<div class="flex justify-center">
<div class="bg-white p-4 rounded-lg border">
<div v-if="qrCodeLoading" class="w-48 h-48 flex items-center justify-center">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900"></div>
</div>
<img
v-else-if="qrCodeDataUrl"
:src="qrCodeDataUrl"
alt="Lightning payment QR code"
class="w-48 h-48"
/>
<div v-else-if="qrCodeError" class="w-48 h-48 flex items-center justify-center text-red-500 text-sm">
{{ qrCodeError }}
</div>
</div>
</div>
<div class="flex justify-center space-x-2">
<Button
variant="outline"
@click="copyInvoice"
>
Copy Invoice
</Button>
<Button
@click="payWithWallet"
>
Pay with Wallet
</Button>
</div>
</div>
</div>
<div v-else class="text-center text-muted-foreground py-8">
No payment request available
</div>
<DialogFooter>
<Button variant="outline" @click="updateOpen(false)">
Close
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</template>
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import { toast } from 'vue-sonner'
import QRCode from 'qrcode'
import type { NostrmarketPaymentRequest } from '@/lib/services/nostrmarketService'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
import { Card, CardContent } from '@/components/ui/card'
import { Zap, Bitcoin, Link, QrCode, CreditCard } from 'lucide-vue-next'
// Props
interface Props {
modelValue: boolean
paymentRequest?: NostrmarketPaymentRequest
}
const props = defineProps<Props>()
// Emits
const emit = defineEmits<{
'update:modelValue': [value: boolean]
'payment-completed': [orderId: string]
}>()
// Computed
const lightningInvoice = computed(() => {
if (!props.paymentRequest) return null
const lightningOption = props.paymentRequest.payment_options.find(opt => opt.type === 'ln')
return lightningOption?.link || null
})
// QR Code generation
const qrCodeDataUrl = ref<string | null>(null)
const qrCodeLoading = ref(false)
const qrCodeError = ref<string | null>(null)
const generateQRCode = async (paymentRequest: string) => {
try {
qrCodeLoading.value = true
qrCodeError.value = null
const dataUrl = await QRCode.toDataURL(paymentRequest, {
width: 200,
margin: 2,
color: {
dark: '#000000',
light: '#FFFFFF'
}
})
qrCodeDataUrl.value = dataUrl
} catch (error) {
console.error('Failed to generate QR code:', error)
qrCodeError.value = 'Failed to generate QR code'
qrCodeDataUrl.value = null
} finally {
qrCodeLoading.value = false
}
}
// Watch for lightning invoice changes and generate QR code
watch(lightningInvoice, (newInvoice) => {
if (newInvoice) {
generateQRCode(newInvoice)
} else {
qrCodeDataUrl.value = null
}
}, { immediate: true })
// Methods
const updateOpen = (open: boolean) => {
emit('update:modelValue', open)
}
const getPaymentTypeLabel = (type: string): string => {
switch (type) {
case 'ln':
return 'Lightning Network'
case 'btc':
return 'Bitcoin On-Chain'
case 'url':
return 'Payment URL'
case 'lnurl':
return 'LNURL-Pay'
default:
return type.toUpperCase()
}
}
const getPaymentTypeIcon = (type: string) => {
switch (type) {
case 'ln':
return Zap
case 'btc':
return Bitcoin
case 'url':
return Link
case 'lnurl':
return QrCode
default:
return CreditCard
}
}
const handlePayment = async (option: { type: string; link: string }) => {
try {
switch (option.type) {
case 'ln':
// For Lightning invoices, we can either show QR code or pay directly
if (lightningInvoice.value) {
await payWithWallet()
}
break
case 'url':
// Open payment URL in new tab
window.open(option.link, '_blank')
break
case 'btc':
// Copy Bitcoin address
await navigator.clipboard.writeText(option.link)
toast.success('Bitcoin address copied to clipboard')
break
default:
console.warn('Unknown payment type:', option.type)
}
} catch (error) {
console.error('Payment handling error:', error)
toast.error('Failed to process payment')
}
}
const copyInvoice = async () => {
if (!lightningInvoice.value) return
try {
await navigator.clipboard.writeText(lightningInvoice.value)
toast.success('Lightning invoice copied to clipboard')
} catch (error) {
console.error('Failed to copy invoice:', error)
toast.error('Failed to copy invoice')
}
}
const payWithWallet = async () => {
if (!lightningInvoice.value) return
try {
// Import the payment API
const { payInvoiceWithWallet } = await import('@/lib/api/events')
// Get the current user's wallet info
const { useAuth } = await import('@/composables/useAuth')
const auth = useAuth()
if (!auth.currentUser.value?.wallets?.[0]?.id || !auth.currentUser.value?.wallets?.[0]?.adminkey) {
toast.error('Please connect your wallet to pay')
return
}
// Pay the invoice
const result = await payInvoiceWithWallet(
lightningInvoice.value,
auth.currentUser.value.wallets[0].id,
auth.currentUser.value.wallets[0].adminkey
)
console.log('Payment result:', result)
toast.success('Payment successful!')
// Emit payment completed event
if (props.paymentRequest) {
emit('payment-completed', props.paymentRequest.id)
}
// Close the dialog
updateOpen(false)
} catch (error) {
console.error('Payment failed:', error)
toast.error('Payment failed: ' + (error instanceof Error ? error.message : 'Unknown error'))
}
}
</script>

View file

@ -1,151 +0,0 @@
<template>
<Card class="overflow-hidden hover:shadow-lg transition-shadow duration-200">
<!-- Product Image -->
<div class="relative">
<img
:src="product.images?.[0] || '/placeholder-product.png'"
:alt="product.name"
class="w-full h-48 object-cover"
@error="handleImageError"
/>
<!-- Add to Cart Button -->
<Button
@click="addToCart"
:disabled="product.quantity < 1"
size="sm"
class="absolute top-2 right-2 bg-blue-600 hover:bg-blue-700 text-white"
>
<ShoppingCart class="w-4 h-4" />
</Button>
<!-- Out of Stock Badge -->
<Badge
v-if="product.quantity < 1"
variant="destructive"
class="absolute top-2 left-2"
>
Out of Stock
</Badge>
</div>
<CardContent class="p-4">
<!-- Product Name -->
<CardTitle class="text-lg font-semibold mb-2 line-clamp-2">
{{ product.name }}
</CardTitle>
<!-- Product Description -->
<p v-if="product.description" class="text-gray-600 text-sm mb-3 line-clamp-2">
{{ product.description }}
</p>
<!-- Price and Quantity -->
<div class="flex items-center justify-between mb-3">
<span class="text-xl font-bold text-green-600">
{{ formatPrice(product.price, product.currency) }}
</span>
<span class="text-sm text-gray-500">
{{ product.quantity }} left
</span>
</div>
<!-- Categories -->
<div v-if="product.categories && product.categories.length > 0" class="mb-3">
<div class="flex flex-wrap gap-1">
<Badge
v-for="category in product.categories.slice(0, 3)"
:key="category"
variant="secondary"
class="text-xs"
>
{{ category }}
</Badge>
<Badge
v-if="product.categories.length > 3"
variant="outline"
class="text-xs"
>
+{{ product.categories.length - 3 }} more
</Badge>
</div>
</div>
<!-- Stall Name -->
<div class="text-sm text-gray-500 mb-3">
{{ product.stallName }}
</div>
</CardContent>
<CardFooter class="p-4 pt-0">
<div class="flex w-full space-x-2">
<Button
variant="outline"
size="sm"
class="flex-1"
@click="$emit('view-stall', product.stall_id)"
>
Visit Stall
</Button>
<Button
size="sm"
class="flex-1"
@click="$emit('view-details', product)"
>
View Details
</Button>
</div>
</CardFooter>
</Card>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useMarketStore } from '@/stores/market'
import { Card, CardContent, CardFooter, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { ShoppingCart } from 'lucide-vue-next'
import type { Product } from '@/stores/market'
interface Props {
product: Product
}
const props = defineProps<Props>()
// const emit = defineEmits<{
// 'view-details': [product: Product]
// 'view-stall': [stallId: string]
// }>()
const marketStore = useMarketStore()
const imageError = ref(false)
const addToCart = () => {
marketStore.addToStallCart(props.product, 1)
}
const handleImageError = () => {
imageError.value = true
}
const formatPrice = (price: number, currency: string) => {
if (currency === 'sat' || currency === 'sats') {
return `${price.toLocaleString('en-US')} sats`
}
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: currency.toUpperCase()
}).format(price)
}
</script>
<style scoped>
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
</style>

View file

@ -1,250 +1,4 @@
<template>
<div class="space-y-6">
<!-- Cart Header -->
<div class="flex items-center justify-between">
<div>
<h2 class="text-2xl font-bold text-foreground">Shopping Cart</h2>
<p class="text-muted-foreground">
{{ totalCartItems }} items across {{ allStallCarts.length }} stalls
</p>
</div>
<div class="flex items-center space-x-4">
<div class="text-right">
<p class="text-sm text-muted-foreground">Total Value</p>
<p class="text-xl font-bold text-green-600">
{{ formatPrice(totalCartValue, 'sats') }}
</p>
</div>
<Button
v-if="allStallCarts.length > 0"
@click="clearAllCarts"
variant="outline"
size="sm"
>
Clear All
</Button>
</div>
</div>
<!-- Empty Cart State -->
<div v-if="allStallCarts.length === 0" class="text-center py-12">
<ShoppingCart class="mx-auto h-12 w-12 text-muted-foreground/50 mb-4" />
<h3 class="text-lg font-medium text-foreground mb-2">Your cart is empty</h3>
<p class="text-muted-foreground mb-6">Start shopping to add items to your cart</p>
<Button @click="$router.push('/market')" variant="default">
Continue Shopping
</Button>
</div>
<!-- Stall Carts -->
<div v-else class="space-y-6">
<div
v-for="cart in allStallCarts"
:key="cart.id"
class="border border-border rounded-lg p-6 bg-card shadow-sm"
>
<!-- Stall Header -->
<div class="flex items-center justify-between mb-4">
<div class="flex items-center space-x-3">
<div class="w-10 h-10 bg-primary/10 rounded-full flex items-center justify-center">
<Store class="w-5 h-5 text-primary" />
</div>
<div>
<h3 class="font-semibold text-foreground">
{{ getStallName(cart.id) }}
</h3>
<p class="text-sm text-muted-foreground">
{{ cart.products.length }} item{{ cart.products.length !== 1 ? 's' : '' }}
</p>
</div>
</div>
<div class="text-right">
<p class="text-sm text-muted-foreground">Stall Total</p>
<p class="text-lg font-semibold text-green-600">
{{ formatPrice(cart.subtotal, cart.currency) }}
</p>
</div>
</div>
<!-- Cart Items -->
<div class="space-y-3 mb-4">
<CartItem
v-for="item in cart.products"
:key="item.product.id"
:item="item"
:stall-id="cart.id"
@update-quantity="updateQuantity"
@remove-item="removeItem"
/>
</div>
<!-- Stall Cart Actions -->
<div class="pt-4 border-t border-border">
<!-- Desktop Layout -->
<div class="hidden md:flex items-center justify-between">
<div class="flex items-center space-x-4">
<Button
@click="clearStallCart(cart.id)"
variant="outline"
size="sm"
>
Clear Stall
</Button>
<Button
@click="viewStall(cart.id)"
variant="ghost"
size="sm"
>
View Stall
</Button>
</div>
<div class="flex items-center space-x-3">
<!-- Cart Summary for this stall -->
<div class="text-right mr-4">
<p class="text-sm text-muted-foreground">Total</p>
<p class="text-lg font-semibold text-green-600">
{{ formatPrice(cart.subtotal, cart.currency) }}
</p>
</div>
<Button
@click="proceedToCheckout(cart.id)"
:disabled="!canProceedToCheckout(cart.id)"
variant="default"
>
Checkout
<ArrowRight class="w-4 h-4 ml-2" />
</Button>
</div>
</div>
<!-- Mobile Layout -->
<div class="md:hidden space-y-4">
<!-- Action Buttons Row -->
<div class="flex items-center justify-between">
<div class="flex items-center space-x-2">
<Button
@click="clearStallCart(cart.id)"
variant="outline"
size="sm"
>
Clear Stall
</Button>
<Button
@click="viewStall(cart.id)"
variant="ghost"
size="sm"
>
View Stall
</Button>
</div>
</div>
<!-- Total and Checkout Row -->
<div class="flex items-center justify-between">
<!-- Cart Summary for this stall -->
<div class="text-left">
<p class="text-sm text-muted-foreground">Total</p>
<p class="text-lg font-semibold text-green-600">
{{ formatPrice(cart.subtotal, cart.currency) }}
</p>
</div>
<Button
@click="proceedToCheckout(cart.id)"
:disabled="!canProceedToCheckout(cart.id)"
variant="default"
class="flex items-center"
>
Checkout
<ArrowRight class="w-4 h-4 ml-2" />
</Button>
</div>
</div>
</div>
</div>
</div>
<!-- Continue Shopping Button -->
<div v-if="allStallCarts.length > 0" class="text-center mt-8">
<Button @click="$router.push('/market')" variant="outline" size="lg">
Continue Shopping
</Button>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useRouter } from 'vue-router'
import { useMarketStore } from '@/stores/market'
import { Button } from '@/components/ui/button'
import {
ShoppingCart,
Store,
ArrowRight
} from 'lucide-vue-next'
import CartItem from './CartItem.vue'
const router = useRouter()
const marketStore = useMarketStore()
// Computed properties
const allStallCarts = computed(() => marketStore.allStallCarts)
const totalCartItems = computed(() => marketStore.totalCartItems)
const totalCartValue = computed(() => marketStore.totalCartValue)
// Methods
const getStallName = (stallId: string) => {
const stall = marketStore.stalls.find(s => s.id === stallId)
return stall?.name || 'Unknown Stall'
}
const updateQuantity = (stallId: string, productId: string, quantity: number) => {
marketStore.updateStallCartQuantity(stallId, productId, quantity)
}
const removeItem = (stallId: string, productId: string) => {
marketStore.removeFromStallCart(stallId, productId)
}
const clearStallCart = (stallId: string) => {
marketStore.clearStallCart(stallId)
}
const clearAllCarts = () => {
marketStore.clearAllStallCarts()
}
const viewStall = (stallId: string) => {
// TODO: Navigate to stall page
console.log('View stall:', stallId)
}
const proceedToCheckout = (stallId: string) => {
marketStore.setCheckoutCart(stallId)
router.push(`/checkout/${stallId}`)
}
const canProceedToCheckout = (stallId: string) => {
const cart = marketStore.stallCarts[stallId]
return cart && cart.products.length > 0
}
const formatPrice = (price: number, currency: string) => {
if (currency === 'sats' || currency === 'sat') {
return `${price.toLocaleString()} sats`
}
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: currency.toUpperCase()
}).format(price)
}
</script>
<script lang="ts">
import ShoppingCartComponent from '@/modules/market/components/ShoppingCart.vue'
export default ShoppingCartComponent
</script>