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:
parent
519a9003d4
commit
e40ac91417
46 changed files with 6305 additions and 3264 deletions
500
src/modules/market/components/OrderHistory.vue
Normal file
500
src/modules/market/components/OrderHistory.vue
Normal file
|
|
@ -0,0 +1,500 @@
|
|||
<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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue