@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.
Files changed (209) hide show
  1. package/app/components/catalog/product/ProductAccordion/ProductAccordion.vue +41 -0
  2. package/app/components/catalog/product/ProductAccordion/__tests__/ProductAccordion.spec.ts +15 -0
  3. package/app/components/catalog/product/ProductAccordion/types.ts +5 -0
  4. package/app/components/catalog/product/ProductProperties/ProductProperties.vue +52 -0
  5. package/app/components/catalog/product/ProductProperties/__tests__/ProductProperties.spec.ts +15 -0
  6. package/app/components/catalog/product/ProductProperties/types.ts +5 -0
  7. package/app/components/catalog/product/ProductSlider/ProductSlider.vue +28 -0
  8. package/app/components/catalog/product/ProductSlider/__tests__/ProductSlider.spec.ts +14 -0
  9. package/app/components/catalog/product/ProductSlider/types.ts +7 -0
  10. package/app/components/catalog/product/RecommendedProducts/RecommendedProducts.vue +12 -0
  11. package/app/components/catalog/product/RecommendedProducts/types.ts +5 -0
  12. package/app/components/catalog/product/RenderContentProductSlider/RenderContentProductSlider.vue +11 -0
  13. package/app/components/catalog/product/add-attribute.vue +54 -0
  14. package/app/components/catalog/product/add-product-type.vue +54 -0
  15. package/app/components/catalog/product/add-product.vue +53 -0
  16. package/app/components/catalog/product/add-showcase.vue +52 -0
  17. package/app/components/catalog/product/add-station.vue +54 -0
  18. package/app/components/catalog/product/bestsellers.vue +69 -0
  19. package/app/components/catalog/product/bidding.vue +93 -0
  20. package/app/components/catalog/product/colorOptions.vue +58 -0
  21. package/app/components/catalog/product/deals.vue +61 -0
  22. package/app/components/catalog/product/exclusives.vue +58 -0
  23. package/app/components/catalog/product/featuredproducts.vue +69 -0
  24. package/app/components/catalog/product/giftCard.vue +63 -0
  25. package/app/components/catalog/product/latestproducts.vue +58 -0
  26. package/app/components/catalog/product/productCard.vue +71 -0
  27. package/app/components/catalog/product/productCompare.vue +60 -0
  28. package/app/components/catalog/product/productCompareTable.vue +441 -0
  29. package/app/components/catalog/product/productDetails.vue +120 -0
  30. package/app/components/catalog/product/productFaqs.vue +17 -0
  31. package/app/components/catalog/product/productGallery.vue +16 -0
  32. package/app/components/catalog/product/productQty.vue +54 -0
  33. package/app/components/catalog/product/productReviews.vue +56 -0
  34. package/app/components/catalog/product/productSpecs.vue +116 -0
  35. package/app/components/catalog/product/radiostation.vue +36 -0
  36. package/app/components/catalog/product/recentlyviewed.vue +43 -0
  37. package/app/components/catalog/product/relatedbrands.vue +54 -0
  38. package/app/components/catalog/product/relatedproducts.vue +58 -0
  39. package/app/components/catalog/product/relatedstations.vue +40 -0
  40. package/app/components/catalog/product/shippingOptions.vue +41 -0
  41. package/app/components/catalog/product/sizeOptions.vue +47 -0
  42. package/app/components/catalog/product/update-attribute-set.vue +209 -0
  43. package/app/components/catalog/product/update-attribute.vue +118 -0
  44. package/app/components/catalog/product/update-product.vue +372 -0
  45. package/app/components/catalog/product/update-showcase.vue +153 -0
  46. package/app/components/catalog/shops/relatedstores.vue +52 -0
  47. package/app/components/catalog/shops/restaurant.vue +66 -0
  48. package/app/components/catalog/shops/stores.vue +44 -0
  49. package/app/components/catalog/vendor/README.md +3 -0
  50. package/app/components/catalog/vendor/blocks/biggestcustomers.vue +33 -0
  51. package/app/components/catalog/vendor/blocks/lowestselling.vue +33 -0
  52. package/app/components/catalog/vendor/blocks/topcategories.vue +33 -0
  53. package/app/components/catalog/vendor/blocks/topproducts.vue +27 -0
  54. package/app/components/catalog/vendor/pages/attributes.vue +43 -0
  55. package/app/components/catalog/vendor/pages/commissions.vue +43 -0
  56. package/app/components/catalog/vendor/pages/crm.vue +67 -0
  57. package/app/components/catalog/vendor/pages/dashboard.vue +46 -0
  58. package/app/components/catalog/vendor/pages/emails.vue +43 -0
  59. package/app/components/catalog/vendor/pages/enquiries.vue +43 -0
  60. package/app/components/catalog/vendor/pages/invoices.vue +43 -0
  61. package/app/components/catalog/vendor/pages/orders.vue +68 -0
  62. package/app/components/catalog/vendor/pages/products.vue +55 -0
  63. package/app/components/catalog/vendor/pages/reviews.vue +48 -0
  64. package/app/components/catalog/vendor/pages/shipments.vue +43 -0
  65. package/app/components/catalog/vendor/pages/stores.vue +43 -0
  66. package/app/components/content/blocks/breadcrumbs.vue +0 -0
  67. package/app/components/content/blocks/currencySwitcher.vue +0 -0
  68. package/app/components/content/blocks/languageSwitcher.vue +0 -0
  69. package/app/components/content/blocks/videoproduct.vue +9 -0
  70. package/app/components/content/pages/checkout.vue +118 -0
  71. package/app/components/content/pages/meeoviGlobal.vue +68 -0
  72. package/app/components/content/pages/pickup-locations.vue +238 -0
  73. package/app/components/content/pages/showcases.vue +90 -0
  74. package/app/components/content/pages/success.vue +60 -0
  75. package/app/components/marketing/add-brand.vue +54 -0
  76. package/app/components/marketing/add-incentive.vue +54 -0
  77. package/app/components/marketing/promotions/giftcards.vue +127 -0
  78. package/app/components/marketing/promotions/subscriptions.vue +134 -0
  79. package/app/components/marketing/update-incentive.vue +326 -0
  80. package/app/components/menus/lowernav.vue +78 -0
  81. package/app/components/partials/LocaleSelector.vue +24 -0
  82. package/app/components/partials/ShoppingCart.vue +128 -0
  83. package/app/components/partials/StripePayment.vue +149 -0
  84. package/app/components/partials/addToCartBtn.vue +40 -0
  85. package/app/components/partials/cartItem.vue +124 -0
  86. package/app/components/partials/checkoutButton.vue +44 -0
  87. package/app/components/partials/compareBtn.vue +68 -0
  88. package/app/components/partials/ratings.vue +13 -0
  89. package/app/components/partials/store/CurrencySelector.vue +133 -0
  90. package/app/components/partials/store/StoreSwitcher.vue +13 -0
  91. package/app/components/related/brandCard.vue +41 -0
  92. package/app/components/related/incentiveCard.vue +44 -0
  93. package/app/components/related/invoiceCard.vue +43 -0
  94. package/app/components/related/orderCard.vue +43 -0
  95. package/app/components/related/relatedproducts.vue +17 -0
  96. package/app/components/sales/CartPageContent/CartPageContent.vue +43 -0
  97. package/app/components/sales/CheckoutAddress/CheckoutAddress.vue +50 -0
  98. package/app/components/sales/CheckoutAddress/__tests__/CheckoutAddress.spec.ts +16 -0
  99. package/app/components/sales/CheckoutAddress/types.ts +16 -0
  100. package/app/components/sales/CheckoutPayment/CheckoutPayment.vue +65 -0
  101. package/app/components/sales/CheckoutPayment/__tests__/CheckoutPayment.spec.ts +14 -0
  102. package/app/components/sales/CheckoutPayment/types.ts +12 -0
  103. package/app/components/sales/OrderSummary/OrderSummary.vue +57 -0
  104. package/app/components/sales/OrderSummary/__tests__/ContactInformation.spec.ts +52 -0
  105. package/app/components/sales/OrderSummary/types.ts +5 -0
  106. package/app/components/sales/incentives.vue +247 -0
  107. package/app/components/sales/invoices.vue +107 -0
  108. package/app/components/sales/orders.vue +378 -0
  109. package/app/components/sales/shipments.vue +65 -0
  110. package/app/components/sales/transactions.vue +109 -0
  111. package/app/components/shop/add-shop.vue +54 -0
  112. package/app/components/shop/cart/cartItem.vue +182 -0
  113. package/app/components/shop/cart/checkout.vue +415 -0
  114. package/app/components/shop/checkout/StripeCardElement.vue +206 -0
  115. package/app/components/shop/checkout/StripeCheckout.vue +49 -0
  116. package/app/components/shop/checkout/addressBilling.vue +263 -0
  117. package/app/components/shop/checkout/addressShipping.vue +175 -0
  118. package/app/components/shop/checkout/cart/ProductItem.vue +56 -0
  119. package/app/components/shop/checkout/cart/PromotionItem.vue +53 -0
  120. package/app/composables/useCustomer/__tests__/useCustomer.spec.ts +1 -1
  121. package/app/composables/useProductReviews/__tests__/useProductReviews.spec.ts +1 -1
  122. package/app/stores/cart.ts +1 -1
  123. package/app/types/Direction.type.ts +1 -1
  124. package/app/types/Global.type.ts +6 -6
  125. package/app/types/Layout.type.ts +1 -1
  126. package/app/types/index.ts +1 -1
  127. package/app/{normalizers → types/normalizers}/Cart.query.ts +1 -1
  128. package/app/{normalizers → types/normalizers}/Cart.type.ts +2 -2
  129. package/app/{normalizers → types/normalizers}/Checkout.query.ts +2 -2
  130. package/app/{normalizers → types/normalizers}/Config.query.ts +1 -1
  131. package/app/{normalizers → types/normalizers}/Config.type.ts +1 -1
  132. package/app/{normalizers → types/normalizers}/ContactForm.query.ts +2 -2
  133. package/app/{normalizers → types/normalizers}/CreditMemo.type.ts +1 -1
  134. package/app/{normalizers → types/normalizers}/GiftCard.type.ts +1 -1
  135. package/app/{normalizers → types/normalizers}/Invoice.type.ts +1 -1
  136. package/app/{normalizers → types/normalizers}/MyAccount.query.ts +1 -1
  137. package/app/{normalizers → types/normalizers}/MyAccount.type.ts +1 -1
  138. package/app/{normalizers → types/normalizers}/NewsletterSubscription.query.ts +1 -1
  139. package/app/{normalizers → types/normalizers}/Order.query.ts +1 -1
  140. package/app/{normalizers → types/normalizers}/Order.type.ts +2 -2
  141. package/app/{normalizers → types/normalizers}/Payment.type.ts +1 -1
  142. package/app/{normalizers → types/normalizers}/ProductCompare.query.ts +1 -1
  143. package/app/{normalizers → types/normalizers}/ProductCompare.type.ts +1 -1
  144. package/app/{normalizers → types/normalizers}/ProductList.query.ts +2 -2
  145. package/app/{normalizers → types/normalizers}/ProductList.type.ts +2 -2
  146. package/app/{normalizers → types/normalizers}/Return.type.ts +1 -1
  147. package/app/{normalizers → types/normalizers}/Review.query.ts +1 -1
  148. package/app/{normalizers → types/normalizers}/Review.type.ts +1 -1
  149. package/app/{normalizers → types/normalizers}/StoreInPickUp.query.ts +1 -1
  150. package/app/{normalizers → types/normalizers}/Subscription.type.ts +1 -1
  151. package/app/{normalizers → types/normalizers}/Transaction.type.ts +1 -1
  152. package/app/{normalizers → types/normalizers}/UrlRewrites.query.ts +1 -1
  153. package/app/{normalizers → types/normalizers}/UrlRewrites.type.ts +1 -1
  154. package/app/{normalizers → types/normalizers}/Wishlist.query.ts +4 -4
  155. package/app/{normalizers → types/normalizers}/Wishlist.type.ts +1 -1
  156. package/app/utils/Address/Address.type.ts +1 -1
  157. package/app/utils/Address/index.ts +5 -5
  158. package/app/utils/Cart/Cart.ts +1 -1
  159. package/app/utils/Currency/Currency.ts +1 -1
  160. package/app/utils/History/History.type.ts +1 -1
  161. package/app/utils/Menu/Menu.ts +1 -1
  162. package/app/utils/Menu/Menu.type.ts +2 -2
  163. package/app/utils/Orders/Orders.ts +1 -1
  164. package/app/utils/Preload/CategoryPreload.ts +2 -2
  165. package/app/utils/Preload/ProductPreload.ts +1 -1
  166. package/app/utils/Preload/index.ts +1 -1
  167. package/app/utils/Price/Price.ts +1 -1
  168. package/app/utils/Product/Extract.ts +1 -1
  169. package/app/utils/Product/Product.ts +1 -1
  170. package/app/utils/Product/Product.type.ts +1 -1
  171. package/app/utils/Product/Transform.ts +1 -1
  172. package/app/utils/Wishlist/Wishlist.ts +1 -1
  173. package/app/utils/client.ts +20 -20
  174. package/package.json +1 -3
  175. package/tsconfig.json +2 -2
  176. package/app/cart/useCart.ts +0 -1
  177. /package/app/{components → composables}/ChevronIcon/ChevronIcon.config.ts +0 -0
  178. /package/app/{components → composables}/DateSelect/DateSelect.config.ts +0 -0
  179. /package/app/{components → composables}/Field/Field.config.ts +0 -0
  180. /package/app/{components → composables}/FieldDate/FieldDate.config.ts +0 -0
  181. /package/app/{components → composables}/Form/Form.type.ts +0 -0
  182. /package/app/{components → composables}/Product/Product.config.ts +0 -0
  183. /package/app/{components → composables}/Product/Stock.config.ts +0 -0
  184. /package/app/{components → composables}/ProductCustomizableOption/ProductCustomizableOption.config.ts +0 -0
  185. /package/app/{components → composables}/ProductGallery/ProductGallery.config.ts +0 -0
  186. /package/app/{components → composables}/ProductReviews/ProductReviews.config.ts +0 -0
  187. /package/app/{normalizers → types/normalizers}/Category.query.ts +0 -0
  188. /package/app/{normalizers → types/normalizers}/Category.type.ts +0 -0
  189. /package/app/{normalizers → types/normalizers}/CheckEmail.query.ts +0 -0
  190. /package/app/{normalizers → types/normalizers}/Checkout.type.ts +0 -0
  191. /package/app/{normalizers → types/normalizers}/CmsBlock.query.ts +0 -0
  192. /package/app/{normalizers → types/normalizers}/CmsBlock.type.ts +0 -0
  193. /package/app/{normalizers → types/normalizers}/CmsPage.query.ts +0 -0
  194. /package/app/{normalizers → types/normalizers}/CmsPage.type.ts +0 -0
  195. /package/app/{normalizers → types/normalizers}/Menu.query.ts +0 -0
  196. /package/app/{normalizers → types/normalizers}/Menu.type.ts +0 -0
  197. /package/app/{normalizers → types/normalizers}/ProductAlerts.query.ts +0 -0
  198. /package/app/{normalizers → types/normalizers}/Region.query.ts +0 -0
  199. /package/app/{normalizers → types/normalizers}/Region.type.ts +0 -0
  200. /package/app/{normalizers → types/normalizers}/Slider.query.ts +0 -0
  201. /package/app/{normalizers → types/normalizers}/Slider.type.ts +0 -0
  202. /package/app/{normalizers → types/normalizers}/StoreInPickUp.type.ts +0 -0
  203. /package/app/{routes → types/routes}/CategoryPage/CategoryPage.config.ts +0 -0
  204. /package/app/{routes → types/routes}/CategoryPage/CategoryPage.type.ts +0 -0
  205. /package/app/{routes → types/routes}/Checkout/Checkout.config.ts +0 -0
  206. /package/app/{routes → types/routes}/Checkout/Checkout.type.ts +0 -0
  207. /package/app/{routes → types/routes}/MyAccount/MyAccount.config.ts +0 -0
  208. /package/app/{routes → types/routes}/SearchPage/SearchPage.config.ts +0 -0
  209. /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>