Enhance shipping zone functionality in CheckoutPage and market types
- Add 'shipping_zones' property to the Stall interface to support LNbits format. - Update ShippingZone interface to include optional 'countries' property for better zone coverage representation. - Modify CheckoutPage.vue to display shipping zone details, including a message for zones that do not require physical shipping. - Improve user feedback by updating messages related to shipping address requirements based on selected shipping zone. - Refactor logic to auto-select shipping zones and handle cases where no shipping zones are available, enhancing user experience during checkout.
This commit is contained in:
parent
dc6a9ed283
commit
143c8afcc3
2 changed files with 77 additions and 18 deletions
|
|
@ -21,6 +21,7 @@ export interface Stall {
|
||||||
logo?: string
|
logo?: string
|
||||||
categories?: string[]
|
categories?: string[]
|
||||||
shipping?: ShippingZone[]
|
shipping?: ShippingZone[]
|
||||||
|
shipping_zones?: ShippingZone[] // LNbits format
|
||||||
currency: string
|
currency: string
|
||||||
nostrEventId?: string
|
nostrEventId?: string
|
||||||
}
|
}
|
||||||
|
|
@ -94,6 +95,7 @@ export interface ShippingZone {
|
||||||
name: string
|
name: string
|
||||||
cost: number
|
cost: number
|
||||||
currency: string
|
currency: string
|
||||||
|
countries?: string[] // Countries/regions this zone covers
|
||||||
description?: string
|
description?: string
|
||||||
estimatedDays?: string
|
estimatedDays?: string
|
||||||
requiresPhysicalShipping?: boolean
|
requiresPhysicalShipping?: boolean
|
||||||
|
|
|
||||||
|
|
@ -110,7 +110,15 @@
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center">
|
||||||
<div>
|
<div>
|
||||||
<h3 class="font-medium text-foreground">{{ zone.name }}</h3>
|
<h3 class="font-medium text-foreground">{{ zone.name }}</h3>
|
||||||
<p class="text-sm text-muted-foreground">{{ zone.countries?.join(', ') || 'Available' }}</p>
|
<p class="text-sm text-muted-foreground">
|
||||||
|
{{ zone.countries?.join(', ') || 'Available' }}
|
||||||
|
<span v-if="!zone.requiresPhysicalShipping" class="ml-2 text-blue-600">
|
||||||
|
• No shipping required
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<p v-if="zone.description" class="text-xs text-muted-foreground mt-1">
|
||||||
|
{{ zone.description }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-right">
|
<div class="text-right">
|
||||||
<p class="font-semibold text-foreground">
|
<p class="font-semibold text-foreground">
|
||||||
|
|
@ -123,14 +131,15 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="text-center py-6">
|
<div v-else class="text-center py-6">
|
||||||
<p class="text-muted-foreground">No shipping zones available</p>
|
<p class="text-muted-foreground">This merchant hasn't configured shipping zones yet.</p>
|
||||||
|
<p class="text-sm text-muted-foreground">Please contact the merchant for shipping information.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Confirm Order Button -->
|
<!-- Confirm Order Button -->
|
||||||
<div class="mt-6">
|
<div class="mt-6">
|
||||||
<Button
|
<Button
|
||||||
@click="confirmOrder"
|
@click="confirmOrder"
|
||||||
:disabled="!selectedShippingZone"
|
:disabled="availableShippingZones.length > 0 && !selectedShippingZone"
|
||||||
class="w-full"
|
class="w-full"
|
||||||
size="lg"
|
size="lg"
|
||||||
>
|
>
|
||||||
|
|
@ -172,14 +181,22 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Label for="address">Shipping Address</Label>
|
<Label for="address">
|
||||||
|
{{ selectedShippingZone?.requiresPhysicalShipping !== false ? 'Shipping Address' : 'Contact Address (optional)' }}
|
||||||
|
</Label>
|
||||||
<Textarea
|
<Textarea
|
||||||
id="address"
|
id="address"
|
||||||
v-model="contactData.address"
|
v-model="contactData.address"
|
||||||
placeholder="Full shipping address..."
|
:placeholder="selectedShippingZone?.requiresPhysicalShipping !== false
|
||||||
|
? 'Full shipping address...'
|
||||||
|
: 'Contact address (optional)...'"
|
||||||
rows="3"
|
rows="3"
|
||||||
/>
|
/>
|
||||||
<p class="text-xs text-muted-foreground mt-1">Required for physical shipping</p>
|
<p class="text-xs text-muted-foreground mt-1">
|
||||||
|
{{ selectedShippingZone?.requiresPhysicalShipping !== false
|
||||||
|
? 'Required for physical delivery'
|
||||||
|
: 'Optional for digital items or pickup' }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -216,15 +233,16 @@
|
||||||
<div class="pt-4 border-t border-border">
|
<div class="pt-4 border-t border-border">
|
||||||
<Button
|
<Button
|
||||||
@click="placeOrder"
|
@click="placeOrder"
|
||||||
:disabled="isPlacingOrder || !contactData.address"
|
:disabled="isPlacingOrder || (selectedShippingZone?.requiresPhysicalShipping !== false && !contactData.address)"
|
||||||
class="w-full"
|
class="w-full"
|
||||||
size="lg"
|
size="lg"
|
||||||
>
|
>
|
||||||
<span v-if="isPlacingOrder" class="animate-spin mr-2">⚡</span>
|
<span v-if="isPlacingOrder" class="animate-spin mr-2">⚡</span>
|
||||||
Place Order - {{ formatPrice(orderTotal, checkoutCart.currency) }}
|
Place Order - {{ formatPrice(orderTotal, checkoutCart.currency) }}
|
||||||
</Button>
|
</Button>
|
||||||
<p v-if="!contactData.address" class="text-xs text-destructive mt-2 text-center">
|
<p v-if="selectedShippingZone?.requiresPhysicalShipping !== false && !contactData.address"
|
||||||
Shipping address is required
|
class="text-xs text-destructive mt-2 text-center">
|
||||||
|
Shipping address is required for physical delivery
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|
@ -308,6 +326,11 @@ const stallName = computed(() => {
|
||||||
return stall?.name || 'Unknown Stall'
|
return stall?.name || 'Unknown Stall'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const currentStall = computed(() => {
|
||||||
|
if (!checkoutCart.value) return null
|
||||||
|
return marketStore.stalls.find(s => s.id === checkoutCart.value?.id)
|
||||||
|
})
|
||||||
|
|
||||||
const orderSubtotal = computed(() => {
|
const orderSubtotal = computed(() => {
|
||||||
if (!checkoutCart.value?.products) return 0
|
if (!checkoutCart.value?.products) return 0
|
||||||
return checkoutCart.value.products.reduce((total, item) =>
|
return checkoutCart.value.products.reduce((total, item) =>
|
||||||
|
|
@ -321,12 +344,36 @@ const orderTotal = computed(() => {
|
||||||
return subtotal + shipping
|
return subtotal + shipping
|
||||||
})
|
})
|
||||||
|
|
||||||
// Mock shipping zones (in real app, would come from stall data)
|
// Get shipping zones from the current stall
|
||||||
const availableShippingZones = computed(() => [
|
const availableShippingZones = computed(() => {
|
||||||
{ id: 'local', name: 'Local Delivery', cost: 500, countries: ['Local Area'] },
|
if (!currentStall.value) return []
|
||||||
{ id: 'national', name: 'National Shipping', cost: 2000, countries: ['Domestic'] },
|
|
||||||
{ id: 'international', name: 'International', cost: 5000, countries: ['Worldwide'] }
|
// Check if stall has shipping_zones (LNbits format) or shipping (nostr-market-app format)
|
||||||
])
|
const zones = currentStall.value.shipping_zones || currentStall.value.shipping || []
|
||||||
|
|
||||||
|
// Ensure zones have required properties and determine shipping requirements
|
||||||
|
return zones.map(zone => {
|
||||||
|
const zoneName = zone.name || 'Shipping Zone'
|
||||||
|
const lowerName = zoneName.toLowerCase()
|
||||||
|
|
||||||
|
// Determine if this zone requires physical shipping
|
||||||
|
const requiresPhysicalShipping = zone.requiresPhysicalShipping !== false &&
|
||||||
|
!lowerName.includes('digital') &&
|
||||||
|
!lowerName.includes('pickup') &&
|
||||||
|
!lowerName.includes('download') &&
|
||||||
|
zone.cost > 0 // Free usually means digital or pickup
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: zone.id || zoneName.toLowerCase().replace(/\s+/g, '-'),
|
||||||
|
name: zoneName,
|
||||||
|
cost: Number(zone.cost) || 0,
|
||||||
|
countries: zone.countries || [],
|
||||||
|
currency: zone.currency || currentStall.value?.currency || 'sats',
|
||||||
|
requiresPhysicalShipping,
|
||||||
|
description: zone.description
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
// Methods
|
// Methods
|
||||||
const selectShippingZone = (zone: any) => {
|
const selectShippingZone = (zone: any) => {
|
||||||
|
|
@ -334,7 +381,8 @@ const selectShippingZone = (zone: any) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const confirmOrder = () => {
|
const confirmOrder = () => {
|
||||||
if (!selectedShippingZone.value) {
|
// Allow proceeding if no shipping zones are available (e.g., for digital goods)
|
||||||
|
if (availableShippingZones.value.length > 0 && !selectedShippingZone.value) {
|
||||||
error.value = 'Please select a shipping zone'
|
error.value = 'Please select a shipping zone'
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -342,8 +390,11 @@ const confirmOrder = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const placeOrder = async () => {
|
const placeOrder = async () => {
|
||||||
if (!contactData.value.address) {
|
// Only require shipping address if selected zone requires physical shipping
|
||||||
error.value = 'Shipping address is required'
|
const requiresShippingAddress = selectedShippingZone.value?.requiresPhysicalShipping !== false
|
||||||
|
|
||||||
|
if (requiresShippingAddress && !contactData.value.address) {
|
||||||
|
error.value = 'Shipping address is required for this delivery method'
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -392,6 +443,12 @@ onMounted(() => {
|
||||||
if (!cart || cart.id !== stallId.value) {
|
if (!cart || cart.id !== stallId.value) {
|
||||||
error.value = 'No checkout data found for this stall'
|
error.value = 'No checkout data found for this stall'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Auto-select shipping zone if there's only one
|
||||||
|
if (availableShippingZones.value.length === 1) {
|
||||||
|
selectedShippingZone.value = availableShippingZones.value[0]
|
||||||
|
}
|
||||||
|
|
||||||
isLoading.value = false
|
isLoading.value = false
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
Loading…
Add table
Add a link
Reference in a new issue