@mframework/layer-commerce 0.0.5 → 0.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/components/catalog/product/ProductAccordion/ProductAccordion.vue +41 -0
- package/app/components/catalog/product/ProductAccordion/__tests__/ProductAccordion.spec.ts +15 -0
- package/app/components/catalog/product/ProductAccordion/types.ts +5 -0
- package/app/components/catalog/product/ProductProperties/ProductProperties.vue +52 -0
- package/app/components/catalog/product/ProductProperties/__tests__/ProductProperties.spec.ts +15 -0
- package/app/components/catalog/product/ProductProperties/types.ts +5 -0
- package/app/components/catalog/product/ProductSlider/ProductSlider.vue +28 -0
- package/app/components/catalog/product/ProductSlider/__tests__/ProductSlider.spec.ts +14 -0
- package/app/components/catalog/product/ProductSlider/types.ts +7 -0
- package/app/components/catalog/product/RecommendedProducts/RecommendedProducts.vue +12 -0
- package/app/components/catalog/product/RecommendedProducts/types.ts +5 -0
- package/app/components/catalog/product/RenderContentProductSlider/RenderContentProductSlider.vue +11 -0
- package/app/components/catalog/product/add-attribute.vue +54 -0
- package/app/components/catalog/product/add-product-type.vue +54 -0
- package/app/components/catalog/product/add-product.vue +53 -0
- package/app/components/catalog/product/add-showcase.vue +52 -0
- package/app/components/catalog/product/add-station.vue +54 -0
- package/app/components/catalog/product/bestsellers.vue +69 -0
- package/app/components/catalog/product/bidding.vue +93 -0
- package/app/components/catalog/product/colorOptions.vue +58 -0
- package/app/components/catalog/product/deals.vue +61 -0
- package/app/components/catalog/product/exclusives.vue +58 -0
- package/app/components/catalog/product/featuredproducts.vue +69 -0
- package/app/components/catalog/product/giftCard.vue +63 -0
- package/app/components/catalog/product/latestproducts.vue +58 -0
- package/app/components/catalog/product/productCard.vue +71 -0
- package/app/components/catalog/product/productCompare.vue +60 -0
- package/app/components/catalog/product/productCompareTable.vue +441 -0
- package/app/components/catalog/product/productDetails.vue +120 -0
- package/app/components/catalog/product/productFaqs.vue +17 -0
- package/app/components/catalog/product/productGallery.vue +16 -0
- package/app/components/catalog/product/productQty.vue +54 -0
- package/app/components/catalog/product/productReviews.vue +56 -0
- package/app/components/catalog/product/productSpecs.vue +116 -0
- package/app/components/catalog/product/radiostation.vue +36 -0
- package/app/components/catalog/product/recentlyviewed.vue +43 -0
- package/app/components/catalog/product/relatedbrands.vue +54 -0
- package/app/components/catalog/product/relatedproducts.vue +58 -0
- package/app/components/catalog/product/relatedstations.vue +40 -0
- package/app/components/catalog/product/shippingOptions.vue +41 -0
- package/app/components/catalog/product/sizeOptions.vue +47 -0
- package/app/components/catalog/product/update-attribute-set.vue +209 -0
- package/app/components/catalog/product/update-attribute.vue +118 -0
- package/app/components/catalog/product/update-product.vue +372 -0
- package/app/components/catalog/product/update-showcase.vue +153 -0
- package/app/components/catalog/shops/relatedstores.vue +52 -0
- package/app/components/catalog/shops/restaurant.vue +66 -0
- package/app/components/catalog/shops/stores.vue +44 -0
- package/app/components/catalog/vendor/README.md +3 -0
- package/app/components/catalog/vendor/blocks/biggestcustomers.vue +33 -0
- package/app/components/catalog/vendor/blocks/lowestselling.vue +33 -0
- package/app/components/catalog/vendor/blocks/topcategories.vue +33 -0
- package/app/components/catalog/vendor/blocks/topproducts.vue +27 -0
- package/app/components/catalog/vendor/pages/attributes.vue +43 -0
- package/app/components/catalog/vendor/pages/commissions.vue +43 -0
- package/app/components/catalog/vendor/pages/crm.vue +67 -0
- package/app/components/catalog/vendor/pages/dashboard.vue +46 -0
- package/app/components/catalog/vendor/pages/emails.vue +43 -0
- package/app/components/catalog/vendor/pages/enquiries.vue +43 -0
- package/app/components/catalog/vendor/pages/invoices.vue +43 -0
- package/app/components/catalog/vendor/pages/orders.vue +68 -0
- package/app/components/catalog/vendor/pages/products.vue +55 -0
- package/app/components/catalog/vendor/pages/reviews.vue +48 -0
- package/app/components/catalog/vendor/pages/shipments.vue +43 -0
- package/app/components/catalog/vendor/pages/stores.vue +43 -0
- package/app/components/content/blocks/breadcrumbs.vue +0 -0
- package/app/components/content/blocks/currencySwitcher.vue +0 -0
- package/app/components/content/blocks/languageSwitcher.vue +0 -0
- package/app/components/content/blocks/videoproduct.vue +9 -0
- package/app/components/content/pages/checkout.vue +118 -0
- package/app/components/content/pages/meeoviGlobal.vue +68 -0
- package/app/components/content/pages/pickup-locations.vue +238 -0
- package/app/components/content/pages/showcases.vue +90 -0
- package/app/components/content/pages/success.vue +60 -0
- package/app/components/marketing/add-brand.vue +54 -0
- package/app/components/marketing/add-incentive.vue +54 -0
- package/app/components/marketing/promotions/giftcards.vue +127 -0
- package/app/components/marketing/promotions/subscriptions.vue +134 -0
- package/app/components/marketing/update-incentive.vue +326 -0
- package/app/components/menus/lowernav.vue +78 -0
- package/app/components/partials/LocaleSelector.vue +24 -0
- package/app/components/partials/ShoppingCart.vue +128 -0
- package/app/components/partials/StripePayment.vue +149 -0
- package/app/components/partials/addToCartBtn.vue +40 -0
- package/app/components/partials/cartItem.vue +124 -0
- package/app/components/partials/checkoutButton.vue +44 -0
- package/app/components/partials/compareBtn.vue +68 -0
- package/app/components/partials/ratings.vue +13 -0
- package/app/components/partials/store/CurrencySelector.vue +133 -0
- package/app/components/partials/store/StoreSwitcher.vue +13 -0
- package/app/components/related/brandCard.vue +41 -0
- package/app/components/related/incentiveCard.vue +44 -0
- package/app/components/related/invoiceCard.vue +43 -0
- package/app/components/related/orderCard.vue +43 -0
- package/app/components/related/relatedproducts.vue +17 -0
- package/app/components/sales/CartPageContent/CartPageContent.vue +43 -0
- package/app/components/sales/CheckoutAddress/CheckoutAddress.vue +50 -0
- package/app/components/sales/CheckoutAddress/__tests__/CheckoutAddress.spec.ts +16 -0
- package/app/components/sales/CheckoutAddress/types.ts +16 -0
- package/app/components/sales/CheckoutPayment/CheckoutPayment.vue +65 -0
- package/app/components/sales/CheckoutPayment/__tests__/CheckoutPayment.spec.ts +14 -0
- package/app/components/sales/CheckoutPayment/types.ts +12 -0
- package/app/components/sales/OrderSummary/OrderSummary.vue +57 -0
- package/app/components/sales/OrderSummary/__tests__/ContactInformation.spec.ts +52 -0
- package/app/components/sales/OrderSummary/types.ts +5 -0
- package/app/components/sales/incentives.vue +247 -0
- package/app/components/sales/invoices.vue +107 -0
- package/app/components/sales/orders.vue +378 -0
- package/app/components/sales/shipments.vue +65 -0
- package/app/components/sales/transactions.vue +109 -0
- package/app/components/shop/add-shop.vue +54 -0
- package/app/components/shop/cart/cartItem.vue +182 -0
- package/app/components/shop/cart/checkout.vue +415 -0
- package/app/components/shop/checkout/StripeCardElement.vue +206 -0
- package/app/components/shop/checkout/StripeCheckout.vue +49 -0
- package/app/components/shop/checkout/addressBilling.vue +263 -0
- package/app/components/shop/checkout/addressShipping.vue +175 -0
- package/app/components/shop/checkout/cart/ProductItem.vue +56 -0
- package/app/components/shop/checkout/cart/PromotionItem.vue +53 -0
- package/app/composables/useCustomer/__tests__/useCustomer.spec.ts +1 -1
- package/app/composables/useProductReviews/__tests__/useProductReviews.spec.ts +1 -1
- package/app/stores/cart.ts +1 -1
- package/app/types/Direction.type.ts +1 -1
- package/app/types/Global.type.ts +6 -6
- package/app/types/Layout.type.ts +1 -1
- package/app/types/index.ts +1 -1
- package/app/{normalizers → types/normalizers}/Cart.query.ts +1 -1
- package/app/{normalizers → types/normalizers}/Cart.type.ts +2 -2
- package/app/{normalizers → types/normalizers}/Checkout.query.ts +2 -2
- package/app/{normalizers → types/normalizers}/Config.query.ts +1 -1
- package/app/{normalizers → types/normalizers}/Config.type.ts +1 -1
- package/app/{normalizers → types/normalizers}/ContactForm.query.ts +2 -2
- package/app/{normalizers → types/normalizers}/CreditMemo.type.ts +1 -1
- package/app/{normalizers → types/normalizers}/GiftCard.type.ts +1 -1
- package/app/{normalizers → types/normalizers}/Invoice.type.ts +1 -1
- package/app/{normalizers → types/normalizers}/MyAccount.query.ts +1 -1
- package/app/{normalizers → types/normalizers}/MyAccount.type.ts +1 -1
- package/app/{normalizers → types/normalizers}/NewsletterSubscription.query.ts +1 -1
- package/app/{normalizers → types/normalizers}/Order.query.ts +1 -1
- package/app/{normalizers → types/normalizers}/Order.type.ts +2 -2
- package/app/{normalizers → types/normalizers}/Payment.type.ts +1 -1
- package/app/{normalizers → types/normalizers}/ProductCompare.query.ts +1 -1
- package/app/{normalizers → types/normalizers}/ProductCompare.type.ts +1 -1
- package/app/{normalizers → types/normalizers}/ProductList.query.ts +2 -2
- package/app/{normalizers → types/normalizers}/ProductList.type.ts +2 -2
- package/app/{normalizers → types/normalizers}/Return.type.ts +1 -1
- package/app/{normalizers → types/normalizers}/Review.query.ts +1 -1
- package/app/{normalizers → types/normalizers}/Review.type.ts +1 -1
- package/app/{normalizers → types/normalizers}/StoreInPickUp.query.ts +1 -1
- package/app/{normalizers → types/normalizers}/Subscription.type.ts +1 -1
- package/app/{normalizers → types/normalizers}/Transaction.type.ts +1 -1
- package/app/{normalizers → types/normalizers}/UrlRewrites.query.ts +1 -1
- package/app/{normalizers → types/normalizers}/UrlRewrites.type.ts +1 -1
- package/app/{normalizers → types/normalizers}/Wishlist.query.ts +4 -4
- package/app/{normalizers → types/normalizers}/Wishlist.type.ts +1 -1
- package/app/utils/Address/Address.type.ts +1 -1
- package/app/utils/Address/index.ts +5 -5
- package/app/utils/Cart/Cart.ts +1 -1
- package/app/utils/Currency/Currency.ts +1 -1
- package/app/utils/History/History.type.ts +1 -1
- package/app/utils/Menu/Menu.ts +1 -1
- package/app/utils/Menu/Menu.type.ts +2 -2
- package/app/utils/Orders/Orders.ts +1 -1
- package/app/utils/Preload/CategoryPreload.ts +2 -2
- package/app/utils/Preload/ProductPreload.ts +1 -1
- package/app/utils/Preload/index.ts +1 -1
- package/app/utils/Price/Price.ts +1 -1
- package/app/utils/Product/Extract.ts +1 -1
- package/app/utils/Product/Product.ts +1 -1
- package/app/utils/Product/Product.type.ts +1 -1
- package/app/utils/Product/Transform.ts +1 -1
- package/app/utils/Wishlist/Wishlist.ts +1 -1
- package/app/utils/client.ts +20 -20
- package/package.json +1 -3
- package/tsconfig.json +2 -2
- package/app/cart/useCart.ts +0 -1
- /package/app/{components → composables}/ChevronIcon/ChevronIcon.config.ts +0 -0
- /package/app/{components → composables}/DateSelect/DateSelect.config.ts +0 -0
- /package/app/{components → composables}/Field/Field.config.ts +0 -0
- /package/app/{components → composables}/FieldDate/FieldDate.config.ts +0 -0
- /package/app/{components → composables}/Form/Form.type.ts +0 -0
- /package/app/{components → composables}/Product/Product.config.ts +0 -0
- /package/app/{components → composables}/Product/Stock.config.ts +0 -0
- /package/app/{components → composables}/ProductCustomizableOption/ProductCustomizableOption.config.ts +0 -0
- /package/app/{components → composables}/ProductGallery/ProductGallery.config.ts +0 -0
- /package/app/{components → composables}/ProductReviews/ProductReviews.config.ts +0 -0
- /package/app/{normalizers → types/normalizers}/Category.query.ts +0 -0
- /package/app/{normalizers → types/normalizers}/Category.type.ts +0 -0
- /package/app/{normalizers → types/normalizers}/CheckEmail.query.ts +0 -0
- /package/app/{normalizers → types/normalizers}/Checkout.type.ts +0 -0
- /package/app/{normalizers → types/normalizers}/CmsBlock.query.ts +0 -0
- /package/app/{normalizers → types/normalizers}/CmsBlock.type.ts +0 -0
- /package/app/{normalizers → types/normalizers}/CmsPage.query.ts +0 -0
- /package/app/{normalizers → types/normalizers}/CmsPage.type.ts +0 -0
- /package/app/{normalizers → types/normalizers}/Menu.query.ts +0 -0
- /package/app/{normalizers → types/normalizers}/Menu.type.ts +0 -0
- /package/app/{normalizers → types/normalizers}/ProductAlerts.query.ts +0 -0
- /package/app/{normalizers → types/normalizers}/Region.query.ts +0 -0
- /package/app/{normalizers → types/normalizers}/Region.type.ts +0 -0
- /package/app/{normalizers → types/normalizers}/Slider.query.ts +0 -0
- /package/app/{normalizers → types/normalizers}/Slider.type.ts +0 -0
- /package/app/{normalizers → types/normalizers}/StoreInPickUp.type.ts +0 -0
- /package/app/{routes → types/routes}/CategoryPage/CategoryPage.config.ts +0 -0
- /package/app/{routes → types/routes}/CategoryPage/CategoryPage.type.ts +0 -0
- /package/app/{routes → types/routes}/Checkout/Checkout.config.ts +0 -0
- /package/app/{routes → types/routes}/Checkout/Checkout.type.ts +0 -0
- /package/app/{routes → types/routes}/MyAccount/MyAccount.config.ts +0 -0
- /package/app/{routes → types/routes}/SearchPage/SearchPage.config.ts +0 -0
- /package/app/{routes → types/routes}/UrlRewrites/UrlRewrites.config.ts +0 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<section class="shop2 marketm4_shop2 cid-uHg6RhKlpj" id="shop2-av">
|
|
3
|
+
<div class="container">
|
|
4
|
+
<div class="row justify-content-between align-items-center">
|
|
5
|
+
<div class="col-xl-6 col-lg-7 col-md-12 content__block">
|
|
6
|
+
<h2 class="mbr-section-title mbr-bold mbr-fonts-style display-5">
|
|
7
|
+
Shopping Cart ({{ itemCount }} items)
|
|
8
|
+
</h2>
|
|
9
|
+
<div class="block__products">
|
|
10
|
+
<div v-if="product" class="d-sm-flex align-items-center product__item">
|
|
11
|
+
<!-- Product Image -->
|
|
12
|
+
<div class="image__item shrink-0">
|
|
13
|
+
<NuxtImg :src="product?.featuredAsset?.preview" :alt="product?.name" loading="lazy" />
|
|
14
|
+
</div>
|
|
15
|
+
<!-- Product Details -->
|
|
16
|
+
<div class="item__text">
|
|
17
|
+
<div class="d-flex justify-content-between align-items-baseline item__title">
|
|
18
|
+
<p class="name__item mbr-medium mbr-fonts-style display-4">
|
|
19
|
+
{{ product?.name }}
|
|
20
|
+
</p>
|
|
21
|
+
<p class="item__price mbr-fonts-style display-4">
|
|
22
|
+
{{ formatPrice(product?.unitPriceWithTax) }}
|
|
23
|
+
</p>
|
|
24
|
+
</div>
|
|
25
|
+
<!-- Quantity Controls -->
|
|
26
|
+
<div class="item__buttons">
|
|
27
|
+
<div class="mbr-section-btn">
|
|
28
|
+
<div class="flex border border-neutral-300 rounded-md">
|
|
29
|
+
<!-- Decrease Quantity -->
|
|
30
|
+
<v-btn variant="tertiary" :disabled="count <= min" square
|
|
31
|
+
class="rounded-r-none p-3" :aria-controls="inputId"
|
|
32
|
+
aria-label="Decrease quantity" @click="handleDecrease">
|
|
33
|
+
<SfIconRemove />
|
|
34
|
+
</v-btn>
|
|
35
|
+
|
|
36
|
+
<!-- Quantity Input -->
|
|
37
|
+
<input :id="inputId" v-model="count" type="number"
|
|
38
|
+
class="grow appearance-none mx-2 w-8 text-center bg-transparent font-medium"
|
|
39
|
+
:min="min" :max="max" @change="handleQuantityChange"
|
|
40
|
+
aria-label="Product quantity" />
|
|
41
|
+
|
|
42
|
+
<!-- Increase Quantity -->
|
|
43
|
+
<v-btn variant="tertiary" :disabled="count >= max" square
|
|
44
|
+
class="rounded-l-none p-3" :aria-controls="inputId"
|
|
45
|
+
aria-label="Increase quantity" @click="handleIncrease">
|
|
46
|
+
<SfIconAdd />
|
|
47
|
+
</v-btn>
|
|
48
|
+
</div>
|
|
49
|
+
<!-- Stock Information -->
|
|
50
|
+
<p class="self-center mt-1 mb-4 text-xs text-neutral-500 xs:mb-0">
|
|
51
|
+
<strong class="text-neutral-900">{{ max }}</strong> in stock
|
|
52
|
+
</p>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
<!-- Empty State -->
|
|
58
|
+
<div v-else class="empty-product">
|
|
59
|
+
<p>Product not available</p>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
</section>
|
|
66
|
+
</template>
|
|
67
|
+
|
|
68
|
+
<script setup lang="ts">
|
|
69
|
+
import { ref, computed, watch } from 'vue';
|
|
70
|
+
import { SfIconAdd, SfIconRemove, useId } from '@storefront-ui/vue';
|
|
71
|
+
import { useVendureMutation } from '../../composables/useVendureMutation';
|
|
72
|
+
import adjustOrderLineMutation from '#graphql/app/commerce/mutations/adjustOrderLine.gql';
|
|
73
|
+
import removeOrderLineMutation from '#graphql/app/commerce/mutations/removeOrderLine.gql';
|
|
74
|
+
|
|
75
|
+
const props = defineProps({
|
|
76
|
+
product: { type: Object, required: true },
|
|
77
|
+
min: { type: Number, default: 1 },
|
|
78
|
+
max: { type: Number, default: 99 },
|
|
79
|
+
cart: { type: Object, required: false }, // for itemCount
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const count = ref(props.product.quantity);
|
|
83
|
+
const inputId = useId();
|
|
84
|
+
|
|
85
|
+
const { mutate: adjustOrderLine } = useVendureMutation(adjustOrderLineMutation);
|
|
86
|
+
const { mutate: removeOrderLine } = useVendureMutation(removeOrderLineMutation);
|
|
87
|
+
|
|
88
|
+
const itemCount = computed(() => {
|
|
89
|
+
return props.cart?.lines?.reduce((total: number, line: any) => total + (line.quantity || 1), 0) || 0;
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const formatPrice = (amount: number) => {
|
|
93
|
+
if (!amount) return '$0.00';
|
|
94
|
+
return new Intl.NumberFormat('en-US', {
|
|
95
|
+
style: 'currency',
|
|
96
|
+
currency: 'USD',
|
|
97
|
+
}).format(amount / 100);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const handleQuantityChange = async () => {
|
|
101
|
+
if (count.value >= props.min && count.value <= props.max) {
|
|
102
|
+
await adjustOrderLine({ orderLineId: props.product.id, quantity: count.value });
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const handleIncrease = async () => {
|
|
107
|
+
if (count.value < props.max) {
|
|
108
|
+
count.value++;
|
|
109
|
+
await adjustOrderLine({ orderLineId: props.product.id, quantity: count.value });
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const handleDecrease = async () => {
|
|
114
|
+
if (count.value > props.min) {
|
|
115
|
+
count.value--;
|
|
116
|
+
await adjustOrderLine({ orderLineId: props.product.id, quantity: count.value });
|
|
117
|
+
} else {
|
|
118
|
+
await removeOrderLine({ orderLineId: props.product.id });
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
watch(() => props.product.quantity, (newQuantity) => {
|
|
123
|
+
if (newQuantity !== count.value) {
|
|
124
|
+
count.value = newQuantity;
|
|
125
|
+
}
|
|
126
|
+
}, { immediate: true });
|
|
127
|
+
</script>
|
|
128
|
+
|
|
129
|
+
<style scoped>
|
|
130
|
+
.product__item {
|
|
131
|
+
padding: 1rem;
|
|
132
|
+
margin-bottom: 1rem;
|
|
133
|
+
border-bottom: 1px solid #eee;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.image__item {
|
|
137
|
+
width: 100px;
|
|
138
|
+
height: 100px;
|
|
139
|
+
margin-right: 1rem;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.image__item img {
|
|
143
|
+
width: 100%;
|
|
144
|
+
height: 100%;
|
|
145
|
+
object-fit: cover;
|
|
146
|
+
border-radius: 4px;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.item__text {
|
|
150
|
+
flex: 1;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.name__item {
|
|
154
|
+
margin-bottom: 0.5rem;
|
|
155
|
+
font-weight: 500;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.item__price {
|
|
159
|
+
font-weight: bold;
|
|
160
|
+
color: var(--primary-color);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.item__buttons {
|
|
164
|
+
margin-top: 1rem;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
input[type="number"] {
|
|
168
|
+
-moz-appearance: textfield;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
input[type="number"]::-webkit-outer-spin-button,
|
|
172
|
+
input[type="number"]::-webkit-inner-spin-button {
|
|
173
|
+
-webkit-appearance: none;
|
|
174
|
+
margin: 0;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.empty-product {
|
|
178
|
+
padding: 1rem;
|
|
179
|
+
text-align: center;
|
|
180
|
+
color: #666;
|
|
181
|
+
}
|
|
182
|
+
</style>
|
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="checkout-form">
|
|
3
|
+
<form @submit.prevent="handleSubmit">
|
|
4
|
+
<div v-if="error" class="error-message">
|
|
5
|
+
{{ error }}
|
|
6
|
+
</div>
|
|
7
|
+
|
|
8
|
+
<!-- Shipping Address -->
|
|
9
|
+
<div class="form-section">
|
|
10
|
+
<h3>Shipping Address</h3>
|
|
11
|
+
<div class="form-row">
|
|
12
|
+
<div class="form-group">
|
|
13
|
+
<label for="shipping-firstname">First Name*</label>
|
|
14
|
+
<input
|
|
15
|
+
id="shipping-firstname"
|
|
16
|
+
v-model="shippingAddress.firstname"
|
|
17
|
+
type="text"
|
|
18
|
+
required
|
|
19
|
+
/>
|
|
20
|
+
</div>
|
|
21
|
+
<div class="form-group">
|
|
22
|
+
<label for="shipping-lastname">Last Name*</label>
|
|
23
|
+
<input
|
|
24
|
+
id="shipping-lastname"
|
|
25
|
+
v-model="shippingAddress.lastname"
|
|
26
|
+
type="text"
|
|
27
|
+
required
|
|
28
|
+
/>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
<div class="form-group">
|
|
32
|
+
<label for="shipping-street">Street Address*</label>
|
|
33
|
+
<input
|
|
34
|
+
id="shipping-street"
|
|
35
|
+
v-model="shippingAddress.street[0]"
|
|
36
|
+
type="text"
|
|
37
|
+
required
|
|
38
|
+
/>
|
|
39
|
+
</div>
|
|
40
|
+
<div class="form-row">
|
|
41
|
+
<div class="form-group">
|
|
42
|
+
<label for="shipping-city">City*</label>
|
|
43
|
+
<input
|
|
44
|
+
id="shipping-city"
|
|
45
|
+
v-model="shippingAddress.city"
|
|
46
|
+
type="text"
|
|
47
|
+
required
|
|
48
|
+
/>
|
|
49
|
+
</div>
|
|
50
|
+
<div class="form-group">
|
|
51
|
+
<label for="shipping-postcode">Postcode*</label>
|
|
52
|
+
<input
|
|
53
|
+
id="shipping-postcode"
|
|
54
|
+
v-model="shippingAddress.postcode"
|
|
55
|
+
type="text"
|
|
56
|
+
required
|
|
57
|
+
/>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
<div class="form-row">
|
|
61
|
+
<div class="form-group">
|
|
62
|
+
<label for="shipping-country">Country*</label>
|
|
63
|
+
<select
|
|
64
|
+
id="shipping-country"
|
|
65
|
+
v-model="shippingAddress.country_code"
|
|
66
|
+
required
|
|
67
|
+
>
|
|
68
|
+
<option value="">Select Country</option>
|
|
69
|
+
<option v-for="country in countries" :key="country.id" :value="country.id">
|
|
70
|
+
{{ country.full_name_locale }}
|
|
71
|
+
</option>
|
|
72
|
+
</select>
|
|
73
|
+
</div>
|
|
74
|
+
<div class="form-group">
|
|
75
|
+
<label for="shipping-telephone">Phone Number*</label>
|
|
76
|
+
<input
|
|
77
|
+
id="shipping-telephone"
|
|
78
|
+
v-model="shippingAddress.telephone"
|
|
79
|
+
type="tel"
|
|
80
|
+
required
|
|
81
|
+
/>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
<!-- Shipping Methods -->
|
|
87
|
+
<div class="form-section">
|
|
88
|
+
<h3>Shipping Method</h3>
|
|
89
|
+
<ShippingOptions v-model="selectedShipping" />
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<!-- Billing Address -->
|
|
93
|
+
<div class="form-section">
|
|
94
|
+
<h3>Billing Address</h3>
|
|
95
|
+
<div class="form-check">
|
|
96
|
+
<input
|
|
97
|
+
type="checkbox"
|
|
98
|
+
id="same-as-shipping"
|
|
99
|
+
v-model="sameAsShipping"
|
|
100
|
+
/>
|
|
101
|
+
<label for="same-as-shipping">Same as shipping address</label>
|
|
102
|
+
</div>
|
|
103
|
+
<div v-if="!sameAsShipping">
|
|
104
|
+
<!-- Billing address fields (same structure as shipping) -->
|
|
105
|
+
<div class="form-row">
|
|
106
|
+
<div class="form-group">
|
|
107
|
+
<label for="billing-firstname">First Name*</label>
|
|
108
|
+
<input
|
|
109
|
+
id="billing-firstname"
|
|
110
|
+
v-model="billingAddress.firstname"
|
|
111
|
+
type="text"
|
|
112
|
+
required
|
|
113
|
+
/>
|
|
114
|
+
</div>
|
|
115
|
+
<div class="form-group">
|
|
116
|
+
<label for="billing-lastname">Last Name*</label>
|
|
117
|
+
<input
|
|
118
|
+
id="billing-lastname"
|
|
119
|
+
v-model="billingAddress.lastname"
|
|
120
|
+
type="text"
|
|
121
|
+
required
|
|
122
|
+
/>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
<!-- Add other billing address fields similar to shipping -->
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
|
|
129
|
+
<!-- Payment Section -->
|
|
130
|
+
<div class="form-section">
|
|
131
|
+
<h3>Payment</h3>
|
|
132
|
+
<p>You will be redirected to a secure Stripe Checkout page to complete payment.</p>
|
|
133
|
+
</div>
|
|
134
|
+
|
|
135
|
+
<!-- Order Summary -->
|
|
136
|
+
<div class="form-section">
|
|
137
|
+
<h3>Order Summary</h3>
|
|
138
|
+
<div class="order-summary">
|
|
139
|
+
<div class="summary-row">
|
|
140
|
+
<span>Subtotal:</span>
|
|
141
|
+
<span>{{ formatPrice(cart.subtotal) }}</span>
|
|
142
|
+
</div>
|
|
143
|
+
<div class="summary-row">
|
|
144
|
+
<span>Shipping:</span>
|
|
145
|
+
<span>{{ formatPrice(cart.shipping) }}</span>
|
|
146
|
+
</div>
|
|
147
|
+
<div class="summary-row">
|
|
148
|
+
<span>Tax:</span>
|
|
149
|
+
<span>{{ formatPrice(cart.tax) }}</span>
|
|
150
|
+
</div>
|
|
151
|
+
<div class="summary-row total">
|
|
152
|
+
<span>Total:</span>
|
|
153
|
+
<span>{{ formatPrice(cart.total) }}</span>
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
|
|
158
|
+
<button class="submit-button" type="submit" :disabled="loading">
|
|
159
|
+
<span v-if="loading">Processing...</span>
|
|
160
|
+
<span v-else>Pay {{ formatPrice(cart.total) }}</span>
|
|
161
|
+
</button>
|
|
162
|
+
</form>
|
|
163
|
+
</div>
|
|
164
|
+
</template>
|
|
165
|
+
|
|
166
|
+
<script setup>
|
|
167
|
+
import { ref, onMounted, computed, watch } from 'vue'
|
|
168
|
+
import { loadStripe } from '@stripe/stripe-js'
|
|
169
|
+
import { useCartStore } from '~/stores/cart'
|
|
170
|
+
import ShippingOptions from '../../catalog/product/shippingOptions.vue'
|
|
171
|
+
|
|
172
|
+
// Component state
|
|
173
|
+
const loading = ref(false)
|
|
174
|
+
const error = ref(null)
|
|
175
|
+
const sameAsShipping = ref(true)
|
|
176
|
+
|
|
177
|
+
const shippingAddress = ref({
|
|
178
|
+
firstname: '',
|
|
179
|
+
lastname: '',
|
|
180
|
+
street: [''],
|
|
181
|
+
city: '',
|
|
182
|
+
region: { code: '', label: '' },
|
|
183
|
+
postcode: '',
|
|
184
|
+
country_code: '',
|
|
185
|
+
telephone: ''
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
const billingAddress = ref({
|
|
189
|
+
firstname: '',
|
|
190
|
+
lastname: '',
|
|
191
|
+
street: [''],
|
|
192
|
+
city: '',
|
|
193
|
+
region: { code: '', label: '' },
|
|
194
|
+
postcode: '',
|
|
195
|
+
country_code: '',
|
|
196
|
+
telephone: ''
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
// Cart store
|
|
200
|
+
const cartStore = useCartStore()
|
|
201
|
+
const cart = computed(() => cartStore.cart ?? { subtotal: 0, tax_amount: 0, shipping_amount: 0, total: 0, currency: 'USD' })
|
|
202
|
+
|
|
203
|
+
// Selected shipping (v-model) bound to ShippingOptions
|
|
204
|
+
const selectedShipping = computed({
|
|
205
|
+
get: () => cartStore.cart?.shipping_method_id ?? cartStore.cart?.shipping_method ?? null,
|
|
206
|
+
set: async (val) => {
|
|
207
|
+
try {
|
|
208
|
+
if (cartStore && cartStore.setShippingOption) {
|
|
209
|
+
await cartStore.setShippingOption({ id: val })
|
|
210
|
+
}
|
|
211
|
+
} catch (e) {
|
|
212
|
+
// eslint-disable-next-line no-console
|
|
213
|
+
console.warn('Failed to set shipping from checkout', e)
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
const nuxtApp = useNuxtApp()
|
|
219
|
+
|
|
220
|
+
// Watch for same as shipping changes
|
|
221
|
+
watch(sameAsShipping, (newValue) => {
|
|
222
|
+
if (newValue) {
|
|
223
|
+
billingAddress.value = { ...shippingAddress.value }
|
|
224
|
+
}
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
// We use Stripe Checkout (server-created session) instead of client Payment Element
|
|
228
|
+
|
|
229
|
+
// Handle form submission: persist addresses to Directus cart record and proceed to Stripe
|
|
230
|
+
const handleSubmit = async () => {
|
|
231
|
+
try {
|
|
232
|
+
loading.value = true
|
|
233
|
+
error.value = null
|
|
234
|
+
|
|
235
|
+
const cartId = cartStore.cart?.id
|
|
236
|
+
if (!cartId) throw new Error('Cart not found')
|
|
237
|
+
|
|
238
|
+
// Persist addresses to cart
|
|
239
|
+
try {
|
|
240
|
+
const payload = {
|
|
241
|
+
shipping_address: shippingAddress.value,
|
|
242
|
+
billing_address: sameAsShipping.value ? shippingAddress.value : billingAddress.value,
|
|
243
|
+
updated_at: new Date().toISOString()
|
|
244
|
+
}
|
|
245
|
+
await nuxtApp.$directus.request(nuxtApp.$updateItem('cart', cartId, payload))
|
|
246
|
+
await cartStore.fetchCart()
|
|
247
|
+
} catch (e) {
|
|
248
|
+
console.warn('Failed to persist addresses to Directus cart', e)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Create Stripe Checkout session via server API
|
|
252
|
+
const data = await cartStore.createCheckoutSession(cartId)
|
|
253
|
+
if (data?.url) {
|
|
254
|
+
window.location.href = data.url
|
|
255
|
+
return
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (data?.id) {
|
|
259
|
+
// fallback: try client redirect using session id
|
|
260
|
+
const stripeKey = import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY
|
|
261
|
+
if (!stripeKey) throw new Error('Stripe publishable key missing')
|
|
262
|
+
const stripe = await loadStripe(stripeKey)
|
|
263
|
+
await stripe.redirectToCheckout({ sessionId: data.id })
|
|
264
|
+
return
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
throw new Error('Failed to create checkout session')
|
|
268
|
+
} catch (err) {
|
|
269
|
+
error.value = err?.message || 'Payment failed. Please try again.'
|
|
270
|
+
console.error('Checkout error:', err)
|
|
271
|
+
} finally {
|
|
272
|
+
loading.value = false
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Directory (countries)
|
|
277
|
+
import { useDirectory } from '../../composables/sales/useDirectory'
|
|
278
|
+
const { getCountries } = useDirectory()
|
|
279
|
+
const countries = ref([])
|
|
280
|
+
onMounted(async () => {
|
|
281
|
+
try {
|
|
282
|
+
countries.value = await getCountries()
|
|
283
|
+
} catch (e) {
|
|
284
|
+
// ignore
|
|
285
|
+
}
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
// Format price helper - accepts numbers or objects { value, currency }
|
|
289
|
+
const formatPrice = (price) => {
|
|
290
|
+
let amount = 0
|
|
291
|
+
let currency = 'USD'
|
|
292
|
+
if (!price) return '$0.00'
|
|
293
|
+
if (typeof price === 'number') {
|
|
294
|
+
amount = price
|
|
295
|
+
} else if (price && typeof price === 'object') {
|
|
296
|
+
amount = Number(price.value ?? price.amount ?? 0)
|
|
297
|
+
currency = price.currency ?? price.currency_code ?? 'USD'
|
|
298
|
+
}
|
|
299
|
+
return new Intl.NumberFormat('en-US', { style: 'currency', currency: (currency || 'USD').toUpperCase() }).format(amount)
|
|
300
|
+
}
|
|
301
|
+
// initializeStripe is invoked onMounted above
|
|
302
|
+
</script>
|
|
303
|
+
|
|
304
|
+
<style scoped>
|
|
305
|
+
.checkout-form {
|
|
306
|
+
max-width: 800px;
|
|
307
|
+
margin: 0 auto;
|
|
308
|
+
padding: 20px;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
.form-section {
|
|
312
|
+
margin-bottom: 2rem;
|
|
313
|
+
padding: 1.5rem;
|
|
314
|
+
border: 1px solid #e6e6e6;
|
|
315
|
+
border-radius: 8px;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
.form-section h3 {
|
|
319
|
+
margin-bottom: 1rem;
|
|
320
|
+
font-size: 1.25rem;
|
|
321
|
+
font-weight: 600;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
.form-row {
|
|
325
|
+
display: grid;
|
|
326
|
+
grid-template-columns: 1fr 1fr;
|
|
327
|
+
gap: 1rem;
|
|
328
|
+
margin-bottom: 1rem;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
.form-group {
|
|
332
|
+
margin-bottom: 1rem;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
.form-group label {
|
|
336
|
+
display: block;
|
|
337
|
+
margin-bottom: 0.5rem;
|
|
338
|
+
font-weight: 500;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
.form-group input,
|
|
342
|
+
.form-group select {
|
|
343
|
+
width: 100%;
|
|
344
|
+
padding: 0.75rem;
|
|
345
|
+
border: 1px solid #e6e6e6;
|
|
346
|
+
border-radius: 4px;
|
|
347
|
+
font-size: 1rem;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
.form-check {
|
|
351
|
+
margin-bottom: 1rem;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
.error-message {
|
|
355
|
+
color: #df1b41;
|
|
356
|
+
margin-bottom: 16px;
|
|
357
|
+
padding: 12px;
|
|
358
|
+
border-radius: 4px;
|
|
359
|
+
background-color: #fff0f0;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
.submit-button {
|
|
363
|
+
background: #5469d4;
|
|
364
|
+
color: #ffffff;
|
|
365
|
+
border-radius: 4px;
|
|
366
|
+
border: 0;
|
|
367
|
+
padding: 12px 16px;
|
|
368
|
+
font-size: 16px;
|
|
369
|
+
font-weight: 600;
|
|
370
|
+
cursor: pointer;
|
|
371
|
+
display: block;
|
|
372
|
+
width: 100%;
|
|
373
|
+
transition: all 0.2s ease;
|
|
374
|
+
box-shadow: 0px 4px 5.5px 0px rgba(0, 0, 0, 0.07);
|
|
375
|
+
margin-top: 24px;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
.submit-button:hover {
|
|
379
|
+
filter: brightness(1.1);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
.submit-button:disabled {
|
|
383
|
+
opacity: 0.5;
|
|
384
|
+
cursor: default;
|
|
385
|
+
background-color: #7795f8;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
.order-summary {
|
|
389
|
+
margin-top: 1rem;
|
|
390
|
+
padding: 1rem;
|
|
391
|
+
background-color: #f8f9fa;
|
|
392
|
+
border-radius: 4px;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
.summary-row {
|
|
396
|
+
display: flex;
|
|
397
|
+
justify-content: space-between;
|
|
398
|
+
margin-bottom: 0.5rem;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
.summary-row.total {
|
|
402
|
+
margin-top: 1rem;
|
|
403
|
+
padding-top: 1rem;
|
|
404
|
+
border-top: 1px solid #e6e6e6;
|
|
405
|
+
font-weight: 600;
|
|
406
|
+
font-size: 1.1rem;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
:deep(.stripe-element) {
|
|
410
|
+
padding: 12px;
|
|
411
|
+
border: 1px solid #e6e6e6;
|
|
412
|
+
border-radius: 4px;
|
|
413
|
+
background: white;
|
|
414
|
+
}
|
|
415
|
+
</style>
|