feat: Add market integration roadmap to NOSTR architecture documentation
- Introduce a comprehensive roadmap for integrating nostr-market-app purchasing functionality into the web-app. - Outline key components of the shopping cart system, checkout process, and order management. - Detail phased implementation strategy, including enhanced user experience and advanced features. - Include security, performance, and testing considerations to ensure robust integration. feat: Enhance market store with new order and cart management features - Introduce new interfaces for Order, OrderItem, ContactInfo, and ShippingZone to support enhanced order management. - Update Stall and Product interfaces to include currency and shipping details. - Implement a comprehensive shopping cart system with stall-specific carts, including methods for adding, removing, and updating items. - Add payment-related interfaces and methods for managing payment requests and statuses. - Enhance filter options to include in-stock status and payment methods, improving product filtering capabilities. - Refactor computed properties and methods for better cart management and checkout processes. feat: Implement shopping cart functionality with new components and routing - Add ShoppingCart, CartItem, and CartSummary components to manage cart items and display summaries. - Introduce Cart.vue page to serve as the main shopping cart interface, integrating cart and summary components. - Update Navbar.vue to include a cart icon with item count, enhancing user navigation. - Implement cart management features in the market store, including item addition, quantity updates, and removal. - Establish routing for the cart page, ensuring seamless navigation for users. - Enhance ProductCard.vue to support adding items to the cart directly from the product listing. feat: Update cart and checkout functionality with improved navigation and button labels - Change "Proceed to Checkout" button text to dynamic "Place Order" based on context in CartSummary.vue. - Update "Continue Shopping" button to "Back to Cart" in CartSummary.vue for clearer navigation. - Modify routing for checkout to include stall ID in ShoppingCart.vue, enhancing checkout process. - Simplify Cart.vue by removing CartSummary component and focusing on ShoppingCart display. - Add new route for checkout with stall ID in router configuration for better handling of checkout flows. feat: Enhance cart and checkout components with improved shipping address handling - Update CartSummary.vue to use readonly types for cart items and shipping zones, ensuring immutability. - Modify Checkout.vue to conditionally display the shipping address field based on the selected shipping zone's requirements for physical shipping. - Add a digital delivery note for products that do not require a shipping address. - Introduce a computed property to determine if a shipping address is required, improving validation logic during checkout. - Update market store to include a new property for shipping zones indicating if physical shipping is required. feat: Implement order placement functionality in checkout process - Add a "Place Order" button in Checkout.vue that triggers the order placement process. - Introduce loading state during order placement to enhance user experience. - Implement createAndPlaceOrder method in market store to handle order creation and status updates. - Include error handling for order placement failures, providing user feedback on errors. - Update checkout logic to validate shipping zone and contact information before proceeding. feat: Add Order History page and update Navbar for order tracking - Introduce a new OrderHistory.vue page to display users' past orders with filtering and sorting options. - Update Navbar.vue to include an "Order History" option with a badge showing the count of orders. - Implement computed properties for order count and enhance user navigation experience. feat: Integrate Nostr functionality for order management and user notifications - Add NostrExtensionGuide component to inform users about the required Nostr extension for order transmission. - Implement useNostrOrders composable to manage Nostr connection, event creation, and order sending. - Update Checkout.vue to display Nostr connection status and provide feedback on order transmission. - Enhance OrderHistory.vue to show Nostr transmission status and details for each order. - Modify market store to handle Nostr event details and errors during order placement, ensuring local fallback. - Introduce types for Nostr events to improve type safety and integration with the existing order management system. refactor: Update Nostr relay configuration to use environment variable - Change DEFAULT_RELAYS to dynamically retrieve relay URLs from the VITE_MARKET_RELAYS environment variable. - Add error handling to ensure relays are configured before establishing a connection. - Modify createBlankEvent function to return a more precise type. - Update event signing process to ensure the event ID is generated correctly before signing. refactor: useAuth switch Enhance Nostr order management with authentication checks - Integrate user authentication checks to ensure Nostr features are only accessible to authenticated users. - Replace direct window.nostr calls with auth store methods for retrieving public and private keys. - Implement a helper function for signing events and mock encryption for order content. - Remove obsolete Nostr type definitions to streamline the codebase. feat: Enhance Checkout.vue with Nostr processing feedback and cleanup - Update the checkout button to disable based on order placement state. - Simplify order placement feedback by removing unnecessary Nostr processing checks. - Introduce a new visual indicator for Nostr order processing status. - Refactor computed properties for better clarity and efficiency in shipping zone handling. refactor: Streamline Nostr order handling and integrate buyer public key retrieval - Remove redundant Nostr relay tag from order event creation in useNostrOrders. - Update Checkout.vue to retrieve the buyer's public key from the auth store, enhancing order placement logic. - Modify createAndPlaceOrder method in market store to accept an optional Nostr orders instance for improved flexibility in order processing. refactor: Remove Nostr-related components and streamline order processing - Delete NostrExtensionGuide.vue and associated type definitions to simplify the codebase. - Remove unused useNostr.ts file and related logic from useNostrOrders.ts. - Update order handling in market store to directly integrate Nostr publishing without relying on external components. - Enhance Checkout.vue and Cart.vue to reflect changes in Nostr integration and provide clearer order status feedback. feat: Enhance Nostr chat functionality with malformed message handling - Introduce tracking for malformed message IDs to prevent repeated processing attempts. - Implement functions to mark messages as malformed, clean up old entries, and retrieve statistics on malformed messages. - Add periodic cleanup of malformed messages to manage memory usage. - Enhance message processing logic to skip previously identified malformed messages and provide detailed error handling for decryption failures. - Update the return object to include new functions for managing malformed messages. ZZ feat: Implement Lightning invoice management in market store - Add functionality to create and manage Lightning invoices for orders. - Introduce payment monitoring and status updates for invoices. - Implement payment confirmation messaging via Nostr upon successful payment. - Enhance order interface to include new fields for Lightning invoice details and payment status. ZZ feat: Enhance OrderHistory.vue with payment status indicators and invoice management - Add visual indicators for payment status, including 'Paid' and 'Payment Pending' badges. - Implement expandable payment display for orders with Lightning invoices. - Introduce functionality to toggle payment display and generate Lightning invoices. - Update order status messaging to reflect payment requirements and invoice generation status. ZZ feat: Enhance OrderHistory.vue with payment status indicators and invoice management - Add visual indicators for payment status, including 'Paid' and 'Payment Pending' badges. - Implement expandable payment display for orders with Lightning invoices. - Introduce functionality to toggle payment display and generate Lightning invoices. - Update order status messaging to reflect payment requirements and invoice generation status. feat: Implement order event handling in useOrderEvents composable - Introduce useOrderEvents composable to manage subscription and processing of order-related events. - Define order event types and interfaces for better type safety and clarity. - Implement methods to handle payment requests, order status updates, and invoice generation. - Enhance OrderHistory.vue to display order event subscription status and last update timestamp. - Update market store to include order update functionality for better integration with order events. FIX: Build errors refactor: Update component styles and improve UI consistency across market pages - Replace various color classes with updated design tokens for better consistency. - Change background colors of components to align with the new design system. - Update text colors to enhance readability and maintain a cohesive look. - Refactor class names in CartItem.vue, CartSummary.vue, DashboardOverview.vue, and other components to use the new color scheme. - Ensure all components reflect the updated design guidelines for a unified user experience. refactor: Remove Order History references from Navbar component - Eliminate order count computation and related UI elements from the Navbar. - Streamline the Navbar by removing the Order History button and badge. - Maintain existing functionality for other menu items, ensuring a cleaner user interface. feat: Implement QR code generation and download functionality in PaymentDisplay component - Add QR code generation for payment requests using the qrcode library. - Enhance UI to display loading states and error messages during QR code generation. - Introduce a download button for users to save the generated QR code. - Implement logic to regenerate QR code when the invoice changes. refactor: Replace useRelayHub with relayHubComposable across components - Update imports in multiple components and composables to use the new relayHubComposable for better consistency and maintainability. - Enhance OrderHistory.vue with debug information for development, displaying key states related to orders, authentication, and relay hub connectivity. - Remove unnecessary reconnect button from RelayHubStatus.vue to streamline user interactions. - Improve logging in useOrderEvents for better debugging and monitoring of order event subscriptions. refactor: Update OrderHistory.vue styles for improved UI consistency - Replace color classes with updated design tokens for better alignment with the new design system. - Enhance readability by adjusting text colors and background styles for payment status indicators. - Ensure a cohesive look across the component by standardizing class names and styles. refactor: Update component styles for improved UI consistency across checkout pages - Replace color classes with updated design tokens for better alignment with the new design system. - Enhance readability by adjusting text colors and background styles in CartSummary.vue, PaymentDisplay.vue, Checkout.vue, and OrderHistory.vue. - Standardize class names and styles to ensure a cohesive look across all components. feat: Implement invoice generation and Nostr integration in MerchantStore component - Add functionality to generate Lightning invoices for orders and send them to customers via Nostr. - Introduce a new sendInvoiceToCustomer method to update order details and publish invoice information. - Enhance order event handling in useOrderEvents to update existing orders with new invoice data. - Improve error handling and logging for invoice generation and sending processes. feat: Enhance MerchantStore and PaymentDisplay components for improved invoice handling - Add wallet indicator in MerchantStore to display the selected wallet name during pending orders. - Implement temporary fixes for missing buyer and seller public keys when generating invoices. - Update invoice generation logic to utilize the first available wallet and improve error handling. - Modify PaymentDisplay to use the new bolt11 field for payment requests and enhance date formatting. - Refactor order event handling to ensure accurate updates and invoice management across components. feat: Enhance order event processing in useOrderEvents composable - Refactor processOrderEvent to handle incoming Nostr market order events with improved validation and logging. - Implement logic to update existing orders or create new ones based on event data, ensuring accurate order management. - Add detailed console logging for better debugging and tracking of order events and their statuses. - Ensure compatibility with market order structure and invoice details for seamless integration with payment processing. feat: Enhance order management with localStorage persistence - Update createOrder method to optionally accept an order ID from events, improving order tracking. - Convert items from readonly to mutable for better manipulation. - Implement localStorage persistence for orders, ensuring data is saved and loaded across sessions. - Add methods to save and load orders from localStorage, enhancing user experience and data reliability. feat: Update invoice creation to support additional metadata and nostrmarket compatibility - Modify createInvoice method to accept an optional extra parameter for additional metadata. - Change invoice tag to 'nostrmarket' for improved compatibility with Nostr market. - Include merchant and buyer public keys in the invoice data for better integration. - Update invoice creation in market store to utilize new parameters for enhanced functionality. feat: Enhance order and invoice handling for Nostr market compatibility - Add originalOrderId to order events for tracking Nostr order IDs. - Update invoice creation to utilize original Nostr order ID when generating invoices. - Improve logging for invoice requests to LNBits, providing better visibility into the data being sent. - Ensure compatibility with nostrmarket by adjusting order ID handling in the market store. fix: Refine invoice creation logic for Nostr market compatibility - Adjust order ID handling in invoice creation to prioritize originalOrderId for better compatibility with nostrmarket. - Enhance logging to provide clearer insights into the order ID being used during invoice generation. feat: Integrate nostrmarket service for order publishing and merchant catalog management - Implement functionality to publish orders via the nostrmarket protocol, replacing the previous Nostr integration. - Add methods to publish merchant catalogs, including stalls and products, to nostrmarket with event ID tracking. - Enhance order interface to include nostrEventId for better integration with nostrmarket. - Improve error handling and logging for nostrmarket publishing processes. refactor: Simplify order creation logic in useOrderEvents and update contact structure in nostrmarketService - Streamline order creation by using event.id and defaulting to 'unknown' for stallId. - Update contact structure to include address and message, removing optional email and phone fields for clarity. - Ensure compatibility with new order data structure for improved integration with nostrmarket. feat: Add bech32 to hex conversion utility and integrate into nostrmarketService - Implement a new utility function to convert bech32 keys to hex format, enhancing key handling. - Update nostrmarketService to utilize the new conversion function for user public and private keys. - Modify contact structure to include additional fields for improved order information management. feat: Add nostrclient configuration to AppConfig for enhanced Nostr integration - Introduce a new nostrclient property in AppConfig to manage Nostr client settings. - Include url and enabled fields to configure the Nostr client connection dynamically. - Ensure compatibility with environment variables for flexible deployment configurations. feat: Introduce comprehensive order management and fulfillment documentation - Add ORDER_MANAGEMENT_FULFILLMENT.md to detail the complete order lifecycle, including order states, data models, and merchant/customer interfaces. - Implement test scripts for verifying order and payment request formats in test-nostrmarket-format.js. - Create PaymentRequestDialog.vue for handling payment requests with dynamic options and QR code generation. - Enhance useOrderEvents.ts to process nostrmarket protocol messages for order management. - Update nostrmarketService.ts to handle payment requests and order status updates, ensuring seamless integration with the marketplace. - Integrate payment request dialog in Market.vue and manage its state in the market store. refactor: Remove obsolete test script for nostrmarket order format - Delete test-nostrmarket-format.js as it is no longer needed for verifying order and payment request formats. - Update PaymentRequestDialog.vue to enhance UI components and integrate QR code generation for payment requests. - Refactor payment handling and notification logic to utilize toast notifications instead of Quasar's notify system. feat: Enhance OrderHistory component with payment request handling and QR code generation - Add UI elements to display payment request status and options in OrderHistory.vue. - Implement functions to copy payment requests, open Lightning wallets, and download QR codes. - Update nostrmarketService to generate QR codes for payment requests and manage order statuses effectively. - Remove obsolete PaymentRequestDialog integration from Market.vue for a cleaner UI. feat: Add debug information and toast notifications in OrderHistory component - Introduce debug info display for payment requests and hashes in OrderHistory.vue. - Implement toast notifications for actions like copying payment requests, opening wallets, and downloading QR codes. - Enhance error handling with user feedback for various order-related actions. - Remove obsolete payment request dialog methods from market store for cleaner code. feat: Revamp CartItem and ShoppingCart components for improved layout and functionality - Enhance CartItem.vue with responsive design for desktop and mobile views, including better organization of product details, price, quantity controls, and remove button. - Update ShoppingCart.vue to separate desktop and mobile layouts, improving the user experience with clearer action buttons and cart summary display. - Implement consistent styling and layout adjustments for better visual coherence across different screen sizes.
This commit is contained in:
parent
93ffb8bf32
commit
ea5a2380f1
43 changed files with 8983 additions and 146 deletions
68
src/pages/Cart.vue
Normal file
68
src/pages/Cart.vue
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
<template>
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<!-- Success Message -->
|
||||
<div v-if="orderSuccess" class="mb-8">
|
||||
<div class="bg-green-500/10 border border-green-200 rounded-lg p-6">
|
||||
<div class="flex items-center space-x-3">
|
||||
<div class="w-8 h-8 bg-green-500/20 rounded-full flex items-center justify-center">
|
||||
<CheckCircle class="w-5 h-5 text-green-600" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-green-900">Order Placed Successfully!</h3>
|
||||
<p class="text-green-700">
|
||||
Your order has been placed and sent to the merchant.
|
||||
<span v-if="orderId" class="font-medium">Order ID: {{ orderId }}</span>
|
||||
</p>
|
||||
<!-- Nostr Status -->
|
||||
<div v-if="orderId && marketStore.orders[orderId]" class="mt-2">
|
||||
<div v-if="marketStore.orders[orderId].sentViaNostr" class="flex items-center gap-2 text-sm text-green-600">
|
||||
<div class="w-2 h-2 bg-green-500 rounded-full"></div>
|
||||
<span>✓ Sent via Nostr network</span>
|
||||
</div>
|
||||
<div v-else class="flex items-center gap-2 text-sm text-yellow-600">
|
||||
<div class="w-2 h-2 bg-yellow-500 rounded-full"></div>
|
||||
<span>⚠ Stored locally only</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Page Header -->
|
||||
<div class="mb-8">
|
||||
<h1 class="text-3xl font-bold text-foreground">Shopping Cart</h1>
|
||||
<p class="text-muted-foreground mt-2">
|
||||
Review your items and proceed to checkout for each stall
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Cart Content -->
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<ShoppingCart />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useMarketStore } from '@/stores/market'
|
||||
import { CheckCircle } from 'lucide-vue-next'
|
||||
import ShoppingCart from '@/components/market/ShoppingCart.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const marketStore = useMarketStore()
|
||||
|
||||
// Check for order success from query params
|
||||
const orderSuccess = computed(() => route.query.orderSuccess === 'true')
|
||||
const orderId = computed(() => route.query.orderId as string)
|
||||
|
||||
// Set the first cart as active if none is selected (for navigation purposes)
|
||||
onMounted(() => {
|
||||
if (marketStore.allStallCarts.length > 0 && !marketStore.activeStallCart) {
|
||||
const firstCart = marketStore.allStallCarts[0]
|
||||
marketStore.setCheckoutCart(firstCart.id)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
438
src/pages/Checkout.vue
Normal file
438
src/pages/Checkout.vue
Normal file
|
|
@ -0,0 +1,438 @@
|
|||
<template>
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<!-- Loading State -->
|
||||
<div v-if="!isReady" class="flex justify-center items-center min-h-64">
|
||||
<div class="flex flex-col items-center space-y-4">
|
||||
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-primary"></div>
|
||||
<p class="text-muted-foreground">Loading checkout...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Error State -->
|
||||
<div v-else-if="error" class="text-center py-12">
|
||||
<h2 class="text-2xl font-bold text-red-600 mb-4">Checkout Error</h2>
|
||||
<p class="text-muted-foreground mb-4">{{ error }}</p>
|
||||
<Button @click="$router.push('/cart')" variant="outline">
|
||||
Back to Cart
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- Checkout Content -->
|
||||
<div v-else-if="checkoutCart && checkoutStall" class="max-w-4xl mx-auto">
|
||||
<!-- Page Header -->
|
||||
<div class="mb-8">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-foreground">Checkout</h1>
|
||||
<p class="text-muted-foreground mt-2">
|
||||
Complete your purchase from {{ checkoutStall.name }}
|
||||
</p>
|
||||
</div>
|
||||
<Button @click="$router.push('/cart')" variant="outline">
|
||||
Back to Cart
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Checkout Grid -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
<!-- Main Checkout Form -->
|
||||
<div class="lg:col-span-2 space-y-6">
|
||||
<!-- Stall Information -->
|
||||
<div class="bg-card border rounded-lg p-6">
|
||||
<div class="flex items-center space-x-4 mb-4">
|
||||
<div class="w-12 h-12 bg-primary/10 rounded-full flex items-center justify-center">
|
||||
<Store class="w-6 h-6 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-foreground">{{ checkoutStall.name }}</h3>
|
||||
<p v-if="checkoutStall.description" class="text-muted-foreground">
|
||||
{{ checkoutStall.description }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Contact Information -->
|
||||
<div class="bg-card border rounded-lg p-6">
|
||||
<h3 class="text-lg font-semibold text-foreground mb-4">Contact Information</h3>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-foreground mb-2">
|
||||
Email (optional)
|
||||
</label>
|
||||
<Input
|
||||
v-model="contactInfo.email"
|
||||
type="email"
|
||||
placeholder="your@email.com"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-foreground mb-2">
|
||||
Message to Merchant (optional)
|
||||
</label>
|
||||
<textarea
|
||||
v-model="contactInfo.message"
|
||||
rows="3"
|
||||
placeholder="Any special requests or notes for the merchant..."
|
||||
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"
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Shipping Information -->
|
||||
<div class="bg-card border rounded-lg p-6">
|
||||
<h3 class="text-lg font-semibold text-foreground mb-4">Shipping Information</h3>
|
||||
|
||||
<!-- Shipping Zone Selection -->
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-foreground mb-2">
|
||||
Shipping Zone
|
||||
</label>
|
||||
<div v-if="availableShippingZones.length > 0" 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>
|
||||
<p v-else class="text-sm text-muted-foreground">
|
||||
No shipping zones available for this stall.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Shipping Address (only show for physical shipping) -->
|
||||
<div v-if="selectedShippingZone && requiresPhysicalShipping" class="mt-4">
|
||||
<label class="block text-sm font-medium text-foreground mb-2">
|
||||
Shipping Address <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<textarea
|
||||
v-model="contactInfo.address"
|
||||
rows="3"
|
||||
placeholder="Enter your shipping address..."
|
||||
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"
|
||||
required
|
||||
></textarea>
|
||||
<p class="text-sm text-muted-foreground mt-1">
|
||||
Required for physical product delivery
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Digital Delivery Note -->
|
||||
<div v-if="selectedShippingZone && !requiresPhysicalShipping" class="mt-4 p-3 bg-muted/50 border border-border rounded-md">
|
||||
<div class="flex items-center space-x-2">
|
||||
<div class="w-5 h-5 text-muted-foreground">📧</div>
|
||||
<div class="text-sm text-muted-foreground">
|
||||
<p class="font-medium text-foreground">Digital Delivery</p>
|
||||
<p>This product will be delivered digitally. No shipping address required.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Payment Method -->
|
||||
<div class="bg-card border rounded-lg p-6">
|
||||
<h3 class="text-lg font-semibold text-foreground mb-4">Payment Method</h3>
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-center space-x-3">
|
||||
<input
|
||||
type="radio"
|
||||
id="lightning"
|
||||
v-model="paymentMethod"
|
||||
value="lightning"
|
||||
class="text-primary focus:ring-primary"
|
||||
/>
|
||||
<label for="lightning" class="text-sm font-medium text-foreground">
|
||||
⚡ Lightning Network (Recommended)
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex items-center space-x-3">
|
||||
<input
|
||||
type="radio"
|
||||
id="btc_onchain"
|
||||
v-model="paymentMethod"
|
||||
value="btc_onchain"
|
||||
class="text-primary focus:ring-primary"
|
||||
/>
|
||||
<label for="btc_onchain" class="text-sm font-medium text-foreground">
|
||||
₿ Bitcoin Onchain
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Place Order Button -->
|
||||
<div class="bg-card border rounded-lg p-6">
|
||||
<!-- Nostr Status Indicator -->
|
||||
<div class="mb-4 p-3 rounded-lg border" :class="{
|
||||
'bg-green-500/10 border-green-200': nostrOrders.isReady.value,
|
||||
'bg-yellow-500/10 border-yellow-200': !nostrOrders.isReady.value
|
||||
}">
|
||||
<div class="flex items-center gap-2 text-sm">
|
||||
<div v-if="nostrOrders.isReady.value" class="flex items-center gap-2 text-green-700">
|
||||
<Wifi class="w-4 h-4" />
|
||||
<span>Connected to Nostr network</span>
|
||||
</div>
|
||||
<div v-else class="flex items-center gap-2 text-yellow-700">
|
||||
<WifiOff class="w-4 h-4" />
|
||||
<span>Nostr network unavailable</span>
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="!nostrOrders.isReady.value" class="text-xs text-yellow-600 mt-1">
|
||||
Orders will be stored locally only. Please log in to send orders to merchants.
|
||||
</p>
|
||||
|
||||
<!-- Test Encryption Button -->
|
||||
<div v-if="nostrOrders.isReady.value" class="mt-3">
|
||||
<Button
|
||||
@click="testEncryption"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
:disabled="isTestingEncryption"
|
||||
class="text-xs"
|
||||
>
|
||||
<div v-if="isTestingEncryption" class="flex items-center gap-2">
|
||||
<div class="animate-spin rounded-full h-3 w-3 border-b-2 border-primary"></div>
|
||||
<span>Testing...</span>
|
||||
</div>
|
||||
<div v-else class="flex items-center gap-2">
|
||||
<span>Test NIP-04 Encryption</span>
|
||||
</div>
|
||||
</Button>
|
||||
<p v-if="encryptionTestResult" class="text-xs mt-1" :class="{
|
||||
'text-green-600': encryptionTestResult === 'success',
|
||||
'text-red-600': encryptionTestResult === 'error'
|
||||
}">
|
||||
{{ encryptionTestResult === 'success' ? '✓ Encryption test passed' : '✗ Encryption test failed' }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
@click="handleCheckout"
|
||||
:disabled="!canProceedToCheckout || isPlacingOrder"
|
||||
variant="default"
|
||||
class="w-full"
|
||||
size="lg"
|
||||
>
|
||||
<div v-if="isPlacingOrder" class="flex items-center space-x-2">
|
||||
<div class="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
|
||||
<span>Placing Order...</span>
|
||||
</div>
|
||||
<div v-else class="flex items-center space-x-2">
|
||||
<Lock class="w-4 h-4" />
|
||||
<span>Place Order</span>
|
||||
</div>
|
||||
</Button>
|
||||
|
||||
<p v-if="error" class="text-sm text-red-600 mt-2 text-center">
|
||||
{{ error }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Order Summary Sidebar -->
|
||||
<div class="lg:col-span-1">
|
||||
<div class="sticky top-8">
|
||||
<CartSummary
|
||||
:stall-id="checkoutCart.id"
|
||||
:cart-items="checkoutCart.products"
|
||||
:subtotal="checkoutCart.subtotal"
|
||||
:currency="checkoutCart.currency"
|
||||
:available-shipping-zones="availableShippingZones"
|
||||
:selected-shipping-zone="selectedShippingZone || undefined"
|
||||
@shipping-zone-selected="selectShippingZone"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useMarketStore } from '@/stores/market'
|
||||
import { nostrOrders } from '@/composables/useNostrOrders'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Store, Lock, Wifi, WifiOff } from 'lucide-vue-next'
|
||||
import CartSummary from '@/components/market/CartSummary.vue'
|
||||
import type { ShippingZone, ContactInfo } from '@/stores/market'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const marketStore = useMarketStore()
|
||||
const auth = useAuth()
|
||||
|
||||
// Route parameters
|
||||
const stallId = route.params.stallId as string
|
||||
|
||||
// Local state
|
||||
const contactInfo = ref<ContactInfo>({
|
||||
email: '',
|
||||
message: '',
|
||||
address: ''
|
||||
})
|
||||
|
||||
const paymentMethod = ref<'lightning' | 'btc_onchain'>('lightning')
|
||||
const selectedShippingZone = ref<ShippingZone | null>(null)
|
||||
const error = ref<string | null>(null)
|
||||
const isPlacingOrder = ref(false)
|
||||
const isTestingEncryption = ref(false)
|
||||
const encryptionTestResult = ref<'success' | 'error' | null>(null)
|
||||
|
||||
// Computed properties
|
||||
const checkoutCart = computed(() => marketStore.checkoutCart)
|
||||
const checkoutStall = computed(() => marketStore.checkoutStall)
|
||||
|
||||
const availableShippingZones = computed(() => {
|
||||
if (!checkoutStall.value) return []
|
||||
return checkoutStall.value.shipping || []
|
||||
})
|
||||
|
||||
const isReady = computed(() => {
|
||||
return checkoutCart.value && checkoutStall.value
|
||||
})
|
||||
|
||||
const requiresPhysicalShipping = computed(() => {
|
||||
return selectedShippingZone.value?.requiresPhysicalShipping || false
|
||||
})
|
||||
|
||||
const canProceedToCheckout = computed(() => {
|
||||
return selectedShippingZone.value && (requiresPhysicalShipping.value ? contactInfo.value.address : true)
|
||||
})
|
||||
|
||||
// Methods
|
||||
const selectShippingZone = (shippingZone: ShippingZone) => {
|
||||
selectedShippingZone.value = shippingZone
|
||||
if (checkoutCart.value) {
|
||||
marketStore.setShippingZone(checkoutCart.value.id, shippingZone)
|
||||
}
|
||||
}
|
||||
|
||||
const handleCheckout = async () => {
|
||||
// Validate required fields
|
||||
if (!selectedShippingZone.value) {
|
||||
error.value = 'Please select a shipping zone'
|
||||
return
|
||||
}
|
||||
|
||||
if (requiresPhysicalShipping.value && !contactInfo.value.address) {
|
||||
error.value = 'Please provide a shipping address'
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// Clear any previous errors
|
||||
error.value = null
|
||||
isPlacingOrder.value = true
|
||||
|
||||
// Create the order
|
||||
const order = await marketStore.createAndPlaceOrder({
|
||||
cartId: checkoutCart.value!.id,
|
||||
stallId: checkoutCart.value!.id,
|
||||
buyerPubkey: auth.currentUser?.value?.pubkey || '', // Get from authenticated user
|
||||
sellerPubkey: checkoutStall.value!.pubkey,
|
||||
status: 'pending',
|
||||
items: checkoutCart.value!.products.map(item => ({
|
||||
productId: item.product.id,
|
||||
productName: item.product.name,
|
||||
quantity: item.quantity,
|
||||
price: item.product.price,
|
||||
currency: item.product.currency
|
||||
})),
|
||||
contactInfo: contactInfo.value,
|
||||
shippingZone: selectedShippingZone.value,
|
||||
paymentMethod: paymentMethod.value,
|
||||
subtotal: checkoutCart.value!.subtotal,
|
||||
shippingCost: selectedShippingZone.value.cost,
|
||||
total: checkoutCart.value!.subtotal + selectedShippingZone.value.cost,
|
||||
currency: checkoutCart.value!.currency
|
||||
})
|
||||
|
||||
// Show success message
|
||||
console.log('Order placed successfully:', order)
|
||||
|
||||
// TODO: Navigate to payment page or show payment modal
|
||||
// For now, redirect to cart with success message
|
||||
router.push({
|
||||
path: '/cart',
|
||||
query: { orderSuccess: 'true', orderId: order.id }
|
||||
})
|
||||
|
||||
} catch (err) {
|
||||
error.value = err instanceof Error ? err.message : 'Failed to place order'
|
||||
console.error('Order placement failed:', err)
|
||||
} finally {
|
||||
isPlacingOrder.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const testEncryption = async () => {
|
||||
try {
|
||||
isTestingEncryption.value = true
|
||||
encryptionTestResult.value = null
|
||||
|
||||
const success = await nostrOrders.testEncryption()
|
||||
encryptionTestResult.value = success ? 'success' : 'error'
|
||||
|
||||
if (success) {
|
||||
console.log('NIP-04 encryption test passed!')
|
||||
} else {
|
||||
console.error('NIP-04 encryption test failed!')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Encryption test error:', error)
|
||||
encryptionTestResult.value = 'error'
|
||||
} finally {
|
||||
isTestingEncryption.value = false
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// Initialize checkout
|
||||
onMounted(() => {
|
||||
if (!stallId) {
|
||||
error.value = 'No stall ID provided'
|
||||
return
|
||||
}
|
||||
|
||||
// Set the checkout cart for this stall
|
||||
marketStore.setCheckoutCart(stallId)
|
||||
|
||||
// Auto-select shipping zone if only one available
|
||||
if (availableShippingZones.value.length === 1) {
|
||||
selectShippingZone(availableShippingZones.value[0])
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
|
@ -85,10 +85,10 @@
|
|||
</div>
|
||||
|
||||
<!-- Cart Summary -->
|
||||
<div v-if="marketStore.cartItemCount > 0" class="fixed bottom-4 right-4">
|
||||
<div v-if="marketStore.totalCartItems > 0" class="fixed bottom-4 right-4">
|
||||
<Button @click="viewCart" class="shadow-lg">
|
||||
<ShoppingCart class="w-5 h-5 mr-2" />
|
||||
Cart ({{ marketStore.cartItemCount }})
|
||||
Cart ({{ marketStore.totalCartItems }})
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -97,6 +97,7 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useMarketStore } from '@/stores/market'
|
||||
import { useMarket } from '@/composables/useMarket'
|
||||
import { useMarketPreloader } from '@/composables/useMarketPreloader'
|
||||
|
|
@ -108,6 +109,7 @@ import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar'
|
|||
import { ShoppingCart } from 'lucide-vue-next'
|
||||
import ProductCard from '@/components/market/ProductCard.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const marketStore = useMarketStore()
|
||||
const market = useMarket()
|
||||
const marketPreloader = useMarketPreloader()
|
||||
|
|
@ -121,7 +123,7 @@ const needsToLoadMarket = computed(() => {
|
|||
marketStore.products.length === 0
|
||||
})
|
||||
|
||||
// Check if market data is ready (either preloaded or loaded)
|
||||
// Check if market data is ready (either preloaded or loaded)
|
||||
const isMarketReady = computed(() => {
|
||||
const isLoading = marketStore.isLoading ?? false
|
||||
const ready = marketPreloader.isPreloaded.value ||
|
||||
|
|
@ -158,12 +160,12 @@ const addToCart = (product: any) => {
|
|||
marketStore.addToCart(product)
|
||||
}
|
||||
|
||||
const viewProduct = (product: any) => {
|
||||
const viewProduct = (_product: any) => {
|
||||
// TODO: Navigate to product detail page
|
||||
}
|
||||
|
||||
const viewCart = () => {
|
||||
// TODO: Navigate to cart page
|
||||
router.push('/cart')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
|
|
|
|||
126
src/pages/MarketDashboard.vue
Normal file
126
src/pages/MarketDashboard.vue
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
<template>
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<!-- Page Header -->
|
||||
<div class="mb-8">
|
||||
<h1 class="text-3xl font-bold text-foreground">Market Dashboard</h1>
|
||||
<p class="text-muted-foreground mt-2">
|
||||
Manage your market activities as both a customer and merchant
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Dashboard Tabs -->
|
||||
<div class="mb-6">
|
||||
<nav class="flex space-x-8 border-b border-border">
|
||||
<button
|
||||
v-for="tab in tabs"
|
||||
:key="tab.id"
|
||||
@click="activeTab = tab.id"
|
||||
:class="[
|
||||
'py-2 px-1 border-b-2 font-medium text-sm transition-colors',
|
||||
activeTab === tab.id
|
||||
? 'border-primary text-primary'
|
||||
: 'border-transparent text-muted-foreground hover:text-foreground hover:border-muted-foreground'
|
||||
]"
|
||||
>
|
||||
<div class="flex items-center space-x-2">
|
||||
<component :is="tab.icon" class="w-4 h-4" />
|
||||
<span>{{ tab.name }}</span>
|
||||
<Badge v-if="tab.badge" variant="secondary" class="text-xs">
|
||||
{{ tab.badge }}
|
||||
</Badge>
|
||||
</div>
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<!-- Tab Content -->
|
||||
<div class="min-h-[600px]">
|
||||
<!-- Overview Tab -->
|
||||
<div v-if="activeTab === 'overview'" class="space-y-6">
|
||||
<DashboardOverview />
|
||||
</div>
|
||||
|
||||
<!-- My Orders Tab (Customer) -->
|
||||
<div v-else-if="activeTab === 'orders'" class="space-y-6">
|
||||
<OrderHistory />
|
||||
</div>
|
||||
|
||||
<!-- My Store Tab (Merchant) -->
|
||||
<div v-else-if="activeTab === 'store'" class="space-y-6">
|
||||
<MerchantStore />
|
||||
</div>
|
||||
|
||||
<!-- Settings Tab -->
|
||||
<div v-else-if="activeTab === 'settings'" class="space-y-6">
|
||||
<MarketSettings />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
// import { useAuth } from '@/composables/useAuth'
|
||||
import { useMarketStore } from '@/stores/market'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import {
|
||||
BarChart3,
|
||||
Package,
|
||||
Store,
|
||||
Settings,
|
||||
|
||||
} from 'lucide-vue-next'
|
||||
import DashboardOverview from '@/components/market/DashboardOverview.vue'
|
||||
import OrderHistory from '@/components/market/OrderHistory.vue'
|
||||
import MerchantStore from '@/components/market/MerchantStore.vue'
|
||||
import MarketSettings from '@/components/market/MarketSettings.vue'
|
||||
|
||||
// const auth = useAuth()
|
||||
const marketStore = useMarketStore()
|
||||
|
||||
// Local state
|
||||
const activeTab = ref('overview')
|
||||
|
||||
// Computed properties for tab badges
|
||||
const orderCount = computed(() => Object.keys(marketStore.orders).length)
|
||||
const pendingOrders = computed(() =>
|
||||
Object.values(marketStore.orders).filter(o => o.status === 'pending').length
|
||||
)
|
||||
// const pendingPayments = computed(() =>
|
||||
// Object.values(marketStore.orders).filter(o => o.paymentStatus === 'pending').length
|
||||
// )
|
||||
|
||||
// Dashboard tabs
|
||||
const tabs = computed(() => [
|
||||
{
|
||||
id: 'overview',
|
||||
name: 'Overview',
|
||||
icon: BarChart3,
|
||||
badge: null
|
||||
},
|
||||
{
|
||||
id: 'orders',
|
||||
name: 'My Orders',
|
||||
icon: Package,
|
||||
badge: orderCount.value > 0 ? orderCount.value : null
|
||||
},
|
||||
{
|
||||
id: 'store',
|
||||
name: 'My Store',
|
||||
icon: Store,
|
||||
badge: pendingOrders.value > 0 ? pendingOrders.value : null
|
||||
},
|
||||
{
|
||||
id: 'settings',
|
||||
name: 'Settings',
|
||||
icon: Settings,
|
||||
badge: null
|
||||
}
|
||||
])
|
||||
|
||||
// Lifecycle
|
||||
onMounted(() => {
|
||||
console.log('Market Dashboard mounted')
|
||||
})
|
||||
</script>
|
||||
|
||||
425
src/pages/OrderHistory.vue
Normal file
425
src/pages/OrderHistory.vue
Normal file
|
|
@ -0,0 +1,425 @@
|
|||
<template>
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<!-- Page Header -->
|
||||
<div class="mb-8">
|
||||
<h1 class="text-3xl font-bold text-foreground">Order History</h1>
|
||||
<p class="text-muted-foreground mt-2">
|
||||
View and track all your market orders
|
||||
</p>
|
||||
<!-- Order Events Status -->
|
||||
<div class="mt-3 flex items-center gap-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<div
|
||||
class="w-2 h-2 rounded-full"
|
||||
:class="orderEvents.isSubscribed ? 'bg-green-500' : 'bg-yellow-500'"
|
||||
></div>
|
||||
<span class="text-sm text-muted-foreground">
|
||||
{{ orderEvents.isSubscribed ? 'Listening for order updates' : 'Connecting to order events...' }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="orderEvents.lastEventTimestamp.value > 0" class="text-xs text-muted-foreground">
|
||||
Last update: {{ formatDate(orderEvents.lastEventTimestamp.value) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filters and Stats -->
|
||||
<div class="mb-6 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 Orders:</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-yellow-600">{{ pendingOrders }}</Badge>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-muted-foreground">Completed:</span>
|
||||
<Badge variant="outline" class="text-green-600">{{ completedOrders }}</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filter Controls -->
|
||||
<div class="flex gap-2">
|
||||
<select v-model="statusFilter" class="w-40 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="w-40 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>
|
||||
<!-- Nostr Status -->
|
||||
<div v-if="order.sentViaNostr !== undefined" class="flex items-center gap-2 mt-1">
|
||||
<div v-if="order.sentViaNostr" class="flex items-center gap-1 text-xs text-green-600">
|
||||
<div class="w-2 h-2 bg-green-500 rounded-full"></div>
|
||||
Sent via Nostr
|
||||
</div>
|
||||
<div v-else class="flex items-center gap-1 text-xs text-red-600">
|
||||
<div class="w-2 h-2 bg-red-500 rounded-full"></div>
|
||||
Nostr failed
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3">
|
||||
<Badge :variant="getStatusVariant(order.status)">
|
||||
{{ formatStatus(order.status) }}
|
||||
</Badge>
|
||||
<!-- Payment Status Indicator -->
|
||||
<div v-if="order.lightningInvoice" class="flex items-center gap-2">
|
||||
<Badge
|
||||
:variant="order.paymentStatus === 'paid' ? 'default' : 'secondary'"
|
||||
class="text-xs"
|
||||
>
|
||||
<div class="flex items-center gap-1">
|
||||
<div v-if="order.paymentStatus === 'paid'" class="w-2 h-2 bg-green-500 rounded-full"></div>
|
||||
<div v-else class="w-2 h-2 bg-yellow-500 rounded-full animate-pulse"></div>
|
||||
{{ order.paymentStatus === 'paid' ? 'Paid' : 'Payment Pending' }}
|
||||
</div>
|
||||
</Badge>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<p class="text-lg font-semibold text-foreground">
|
||||
{{ formatPrice(order.total, order.currency) }}
|
||||
</p>
|
||||
<p class="text-sm text-muted-foreground">{{ order.currency.toUpperCase() }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Order Details -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-4">
|
||||
<!-- Items -->
|
||||
<div>
|
||||
<h4 class="font-medium text-foreground mb-2">Items</h4>
|
||||
<div class="space-y-2">
|
||||
<div
|
||||
v-for="item in order.items"
|
||||
:key="item.productId"
|
||||
class="flex justify-between text-sm"
|
||||
>
|
||||
<span class="text-muted-foreground">
|
||||
{{ item.productName }} × {{ item.quantity }}
|
||||
</span>
|
||||
<span class="font-medium">
|
||||
{{ formatPrice(item.price * item.quantity, item.currency) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Order Info -->
|
||||
<div>
|
||||
<h4 class="font-medium text-foreground mb-2">Order Details</h4>
|
||||
<div class="space-y-2 text-sm">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-muted-foreground">Subtotal:</span>
|
||||
<span>{{ formatPrice(order.subtotal, order.currency) }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-muted-foreground">Shipping:</span>
|
||||
<span>{{ formatPrice(order.shippingCost, order.currency) }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-muted-foreground">Payment Method:</span>
|
||||
<span class="capitalize">{{ order.paymentMethod.replace('_', ' ') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Contact & Shipping -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-4">
|
||||
<!-- Contact Info -->
|
||||
<div v-if="order.contactInfo.email || order.contactInfo.message">
|
||||
<h4 class="font-medium text-foreground mb-2">Contact Information</h4>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Shipping Info -->
|
||||
<div v-if="order.shippingZone">
|
||||
<h4 class="font-medium text-foreground mb-2">Shipping</h4>
|
||||
<div class="space-y-1 text-sm text-muted-foreground">
|
||||
<p>
|
||||
<span class="font-medium">Zone:</span> {{ order.shippingZone.name }}
|
||||
</p>
|
||||
<p v-if="order.shippingZone.estimatedDays">
|
||||
<span class="font-medium">Est. Delivery:</span> {{ order.shippingZone.estimatedDays }}
|
||||
</p>
|
||||
<p v-if="order.contactInfo.address">
|
||||
<span class="font-medium">Address:</span> {{ order.contactInfo.address }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Nostr Event Details -->
|
||||
<div v-if="order.sentViaNostr !== undefined" class="mb-4 p-4 bg-muted rounded-lg">
|
||||
<h4 class="font-medium text-foreground mb-2">Nostr Network Status</h4>
|
||||
<div class="space-y-2 text-sm">
|
||||
<div v-if="order.sentViaNostr" class="space-y-1">
|
||||
<p v-if="order.nostrEventId" class="text-muted-foreground">
|
||||
<span class="font-medium">Event ID:</span>
|
||||
<code class="bg-background px-2 py-1 rounded text-xs">{{ order.nostrEventId.slice(0, 16) }}...</code>
|
||||
</p>
|
||||
<p v-if="order.nostrEventSig" class="text-muted-foreground">
|
||||
<span class="font-medium">Signature:</span>
|
||||
<code class="bg-background px-2 py-1 rounded text-xs">{{ order.nostrEventSig.slice(0, 16) }}...</code>
|
||||
</p>
|
||||
<p class="text-green-600">
|
||||
<span class="font-medium">✓</span> Order successfully transmitted to merchant via Nostr network
|
||||
</p>
|
||||
</div>
|
||||
<div v-else class="space-y-1">
|
||||
<p v-if="order.nostrError" class="text-red-600">
|
||||
<span class="font-medium">✗</span> Failed to send via Nostr: {{ order.nostrError }}
|
||||
</p>
|
||||
<p class="text-yellow-600">
|
||||
<span class="font-medium">⚠</span> Order stored locally only - merchant may not receive it
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Payment Status & Actions -->
|
||||
<div v-if="order.status === 'pending'" class="mb-4 p-4 bg-muted/50 border border-border rounded-lg">
|
||||
<div class="flex-1">
|
||||
<h4 class="font-medium text-foreground mb-2">Payment Required</h4>
|
||||
<div v-if="order.lightningInvoice" class="space-y-2">
|
||||
<p class="text-sm text-muted-foreground">
|
||||
<span class="font-medium text-green-600">✓</span> Lightning invoice received from merchant
|
||||
</p>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
Amount: <span class="font-medium text-foreground">{{ formatPrice(order.total, order.currency) }}</span>
|
||||
</p>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
Status: <span class="font-medium text-foreground">{{ order.paymentStatus === 'paid' ? 'Paid' : 'Pending Payment' }}</span>
|
||||
</p>
|
||||
</div>
|
||||
<div v-else class="space-y-2">
|
||||
<p class="text-sm text-muted-foreground">
|
||||
<span class="font-medium text-amber-600">⏳</span> Waiting for merchant to generate payment invoice
|
||||
</p>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
The merchant will send you a Lightning invoice via Nostr once they process your order
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</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
|
||||
v-if="order.lightningInvoice"
|
||||
variant="default"
|
||||
size="sm"
|
||||
@click="togglePaymentDisplay(order.id)"
|
||||
>
|
||||
<Wallet class="w-4 h-4 mr-2" />
|
||||
{{ expandedPayments.has(order.id) ? 'Hide' : 'Show' }} Payment
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@click="copyOrderId(order.id)"
|
||||
>
|
||||
Copy Order ID
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- Payment Display (Expandable) -->
|
||||
<div v-if="expandedPayments.has(order.id) && order.lightningInvoice" class="mt-4 pt-4 border-t border-border">
|
||||
<PaymentDisplay
|
||||
:order-id="order.id"
|
||||
/>
|
||||
</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/50" />
|
||||
</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="router.push('/market')" variant="default">
|
||||
Browse Market
|
||||
</Button>
|
||||
</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, Wallet } from 'lucide-vue-next'
|
||||
import type { OrderStatus } from '@/stores/market'
|
||||
import PaymentDisplay from '@/components/market/PaymentDisplay.vue'
|
||||
import { orderEvents } from '@/composables/useOrderEvents'
|
||||
|
||||
const router = useRouter()
|
||||
const marketStore = useMarketStore()
|
||||
|
||||
// Local state
|
||||
const statusFilter = ref('')
|
||||
const sortBy = ref('createdAt')
|
||||
const expandedPayments = ref(new Set<string>())
|
||||
|
||||
// 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 completedOrders = computed(() =>
|
||||
allOrders.value.filter(o => ['delivered', 'shipped'].includes(o.status)).length
|
||||
)
|
||||
|
||||
// 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 cancelOrder = (orderId: string) => {
|
||||
// TODO: Implement order cancellation
|
||||
console.log('Cancelling order:', orderId)
|
||||
}
|
||||
|
||||
// const viewPayment = (_order: any) => {
|
||||
// // TODO: Implement payment viewing
|
||||
// console.log('Viewing payment for order:', _order.id)
|
||||
// }
|
||||
|
||||
const copyOrderId = async (orderId: string) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(orderId)
|
||||
// TODO: Show toast notification
|
||||
console.log('Order ID copied to clipboard')
|
||||
} catch (err) {
|
||||
console.error('Failed to copy order ID:', err)
|
||||
}
|
||||
}
|
||||
|
||||
const togglePaymentDisplay = (orderId: string) => {
|
||||
if (expandedPayments.value.has(orderId)) {
|
||||
expandedPayments.value.delete(orderId)
|
||||
} else {
|
||||
expandedPayments.value.add(orderId)
|
||||
}
|
||||
}
|
||||
|
||||
// Load orders on mount
|
||||
onMounted(() => {
|
||||
// Orders are already loaded in the market store
|
||||
console.log('Order History page loaded with', allOrders.value.length, 'orders')
|
||||
|
||||
// Start listening for order events if not already listening
|
||||
if (!orderEvents.isSubscribed.value) {
|
||||
orderEvents.startListening()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
|
@ -184,8 +184,8 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import RelayHubStatus from '@/components/RelayHubStatus.vue'
|
||||
import { useRelayHub } from '@/composables/useRelayHub'
|
||||
import { config } from '@/lib/config'
|
||||
import { relayHubComposable } from '@/composables/useRelayHub'
|
||||
|
||||
const {
|
||||
isConnected,
|
||||
|
|
@ -196,7 +196,7 @@ const {
|
|||
initialize,
|
||||
connect,
|
||||
subscribe
|
||||
} = useRelayHub()
|
||||
} = relayHubComposable
|
||||
|
||||
// Test state
|
||||
const isTesting = ref(false)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue