- Implemented lazy loading for product images in CartItem, CartSummary, ProductCard, ProductDetailDialog, CheckoutPage, and StallView components. - This enhancement improves performance by deferring the loading of off-screen images, resulting in faster initial page load times and a smoother user experience. These changes optimize image handling across the market module, contributing to better resource management and user interaction.
232 lines
7.5 KiB
Vue
232 lines
7.5 KiB
Vue
<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"
|
|
loading="lazy"
|
|
/>
|
|
<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 '@/modules/market/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>
|