@osimart/create-storefront 1.0.17 → 1.0.21

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.
@@ -1,4 +1,5 @@
1
1
  import CartServices from "#osi/services/CartServices";
2
+ import { isCustomizationError, needsPersonalization } from "./useProductCustomization";
2
3
 
3
4
  /**
4
5
  * Quick-shop: pick a variant and add to cart without leaving the grid.
@@ -26,6 +27,14 @@ export function useQuickShop({ product, variants, onAdded, closeOnAdd = true } =
26
27
 
27
28
  const { fetchCart } = useCart();
28
29
  const { showApiError } = useApiErrorToast();
30
+ const { productUrl } = useProductUrl();
31
+ const router = useRouter();
32
+
33
+ /** Required personalization: answered on the product page, not here. */
34
+ function goPersonalize() {
35
+ close();
36
+ router.push(productUrl(p.value));
37
+ }
29
38
 
30
39
  // Mobile only: the sheet covers the page, the desktop overlay doesn't.
31
40
  useBodyScrollLock(() => mode.value === "MOBILE");
@@ -61,6 +70,11 @@ export function useQuickShop({ product, variants, onAdded, closeOnAdd = true } =
61
70
  return false;
62
71
  }
63
72
 
73
+ if (needsPersonalization(p.value)) {
74
+ goPersonalize();
75
+ return false;
76
+ }
77
+
64
78
  adding.value = true;
65
79
  try {
66
80
  await CartServices.EditCart(
@@ -74,6 +88,11 @@ export function useQuickShop({ product, variants, onAdded, closeOnAdd = true } =
74
88
  if (closeOnAdd) close();
75
89
  return true;
76
90
  } catch (error) {
91
+ // 400 { properties }: the product needs personalizing first.
92
+ if (isCustomizationError(error)) {
93
+ goPersonalize();
94
+ return false;
95
+ }
77
96
  // Stock and cart-limit errors come back as field errors, so they need the
78
97
  // flattening — a bare error.message here would print "[object Object]".
79
98
  flashError(showApiError(error, "Couldn't add that to your cart."));
@@ -111,5 +130,6 @@ export function useQuickShop({ product, variants, onAdded, closeOnAdd = true } =
111
130
  addVariant,
112
131
  addSelected,
113
132
  flashError,
133
+ goPersonalize,
114
134
  };
115
135
  }
@@ -1,4 +1,3 @@
1
- import CartServices from "#osi/services/CartServices";
2
1
  import ProductServices from "#osi/services/ProductServices";
3
2
 
4
3
  export default function useLogic() {
@@ -33,7 +32,7 @@ export default function useLogic() {
33
32
  const txt = (key) => String(t(settings.value[key]) ?? "").trim() || TEXT_DEFAULTS[key] || "";
34
33
  const scheme = useColorScheme(computed(() => settings.value.CART_DRAWER_COLOR_SCHEME || null));
35
34
  const { isCartSidebarVisible, toggleCartSidebar } = useSidebar();
36
- const { cartItems, cartTotal, count, fetchCart } = useCart();
35
+ const { cartItems, topLevelItems, childrenOf, cartTotal, count, fetchCart, isUpdating, increaseItem, decreaseItem, removeItem } = useCart();
37
36
  const { showApiError } = useApiErrorToast();
38
37
  const { mediaUrl } = useMediaUrl();
39
38
  const { productUrl } = useProductUrl();
@@ -124,27 +123,20 @@ export default function useLogic() {
124
123
 
125
124
  /* ---------------------------------------------------------------- lines */
126
125
 
127
- // Per-line, not a single global flag: one slow line shouldn't disable the
128
- // controls on every other line in the drawer.
129
- const updatingIds = ref(new Set());
130
- const isUpdating = (item) => updatingIds.value.has(item.id);
131
-
132
- async function edit(item, action) {
133
- if (isUpdating(item)) return;
134
- updatingIds.value = new Set(updatingIds.value).add(item.id);
126
+ // Edits target one line by its order_item_id (one variant can have several
127
+ // lines with different customizations). Busy state is per line, in useCart,
128
+ // so the checkout page and header badge move with the drawer. Each edit
129
+ // returns the whole cart, so there's no follow-up fetch.
130
+ async function edit(fn, item) {
135
131
  try {
136
- await CartServices.EditCart(item.id, item.quantity_step || 1, action);
137
- await fetchCart();
132
+ await fn(item);
138
133
  } catch (error) {
139
134
  showApiError(error, "Couldn't update the cart.");
140
- } finally {
141
- const next = new Set(updatingIds.value);
142
- next.delete(item.id);
143
- updatingIds.value = next;
144
135
  }
145
136
  }
146
- const increase = (item) => edit(item, "add");
147
- const decrease = (item) => edit(item, "remove");
137
+ const increase = (item) => edit(increaseItem, item);
138
+ const decrease = (item) => edit(decreaseItem, item);
139
+ const remove = (item) => edit(removeItem, item);
148
140
 
149
141
  function close() {
150
142
  toggleCartSidebar(false);
@@ -269,5 +261,5 @@ export default function useLogic() {
269
261
  // rely on a page's onMounted to load the cart.
270
262
  onMounted(fetchCart);
271
263
 
272
- return { settings, t, TEXT_DEFAULTS, txt, scheme, isCartSidebarVisible, toggleCartSidebar, cartItems, cartTotal, count, fetchCart, showApiError, mediaUrl, productUrl, handleAddToCart, store, currency, money, px, has, VARIANTS, buttonVars, button, checkoutBtn, emptyBtn, addBtn, cardStyle, RATIOS, rootStyle, updatingIds, isUpdating, edit, increase, decrease, close, go, threshold, shipping, recs, recsLoading, recsKey, pickedIds, limit, loadRecs, inCart, shownRecs, recsLayout, pricePos, adding, addRec, priceOf, analytics };
264
+ return { settings, t, TEXT_DEFAULTS, txt, scheme, isCartSidebarVisible, toggleCartSidebar, cartItems, topLevelItems, childrenOf, cartTotal, count, fetchCart, showApiError, mediaUrl, productUrl, handleAddToCart, store, currency, money, px, has, VARIANTS, buttonVars, button, checkoutBtn, emptyBtn, addBtn, cardStyle, RATIOS, rootStyle, isUpdating, edit, increase, decrease, remove, close, go, threshold, shipping, recs, recsLoading, recsKey, pickedIds, limit, loadRecs, inCart, shownRecs, recsLayout, pricePos, adding, addRec, priceOf, analytics };
273
265
  }
@@ -27,5 +27,22 @@ export default function useLogic({ props, emit }) {
27
27
  emit("add-item", props.item);
28
28
  }
29
29
 
30
- return { mediaUrl, store, currency, imageSrc, variantLabel, handleRemove, handleAdd };
30
+ function handleDelete() {
31
+ if (props.loading) return;
32
+ emit("delete-item", props.item);
33
+ }
34
+
35
+ /** Customization answers, as "Label: value" (a ticked checkbox reads "Yes"). */
36
+ const properties = computed(() =>
37
+ (Array.isArray(props.item?.properties) ? props.item.properties : []).map((p) => ({
38
+ key: p.key,
39
+ label: p.label,
40
+ value: p.value === true ? "Yes" : p.value,
41
+ }))
42
+ );
43
+
44
+ // Add-on lines follow their product's quantity and go with it.
45
+ const isAddon = computed(() => !!props.item?.parent_item);
46
+
47
+ return { mediaUrl, store, currency, imageSrc, variantLabel, properties, isAddon, handleRemove, handleAdd, handleDelete };
31
48
  }
@@ -28,6 +28,7 @@ export default function useLogic({ props }) {
28
28
  TITLE: 6,
29
29
  PRICE: 20,
30
30
  VARIANT_PICKER: 24,
31
+ CUSTOMIZATION: 16,
31
32
  QUANTITY: 16,
32
33
  BUY_BUTTONS: 24,
33
34
  DESCRIPTION: 24,
@@ -274,6 +275,8 @@ export default function useLogic({ props }) {
274
275
  return !!(text(t(s.value.ICON_TEXT_LABEL)) || icon.value);
275
276
  case "VARIANT_PICKER":
276
277
  return !!detail?.hasOptions.value;
278
+ case "CUSTOMIZATION":
279
+ return !!detail?.customization.visibleSets.value.length;
277
280
  case "PRODUCT_META":
278
281
  return !!(
279
282
  (s.value.META_SHOW_SKU !== false && detail?.sku.value) ||
@@ -4,6 +4,7 @@ export default function useLogic({ props }) {
4
4
  { id: "fp-title", type: "TITLE", settings: {} },
5
5
  { id: "fp-price", type: "PRICE", settings: {} },
6
6
  { id: "fp-picker", type: "VARIANT_PICKER", settings: {} },
7
+ { id: "fp-customization", type: "CUSTOMIZATION", settings: {} },
7
8
  { id: "fp-buy", type: "BUY_BUTTONS", settings: { BUTTON_HEIGHT: 44 } },
8
9
  { id: "fp-description", type: "DESCRIPTION", settings: { DESCRIPTION_COLLAPSE: true, DESCRIPTION_CLAMP: 3 } },
9
10
  {
@@ -32,6 +32,7 @@ export default function useLogic({ props }) {
32
32
  { id: "default-title", type: "TITLE", settings: {} },
33
33
  { id: "default-price", type: "PRICE", settings: {} },
34
34
  { id: "default-picker", type: "VARIANT_PICKER", settings: { PICKER_STYLE: "SWATCHES" } },
35
+ { id: "default-customization", type: "CUSTOMIZATION", settings: {} },
35
36
  {
36
37
  id: "default-buy",
37
38
  type: "BUY_BUTTONS",
@@ -1,6 +1,14 @@
1
1
  <template>
2
2
  <div class="bg-gray-50 font-body text-primaryColor">
3
3
 
4
+ <!-- Customization changed since the item was added: placeOrder said so. -->
5
+ <div v-if="cartError" class="mx-auto max-w-6xl px-4 pt-4 lg:px-9" role="alert">
6
+ <div class="flex items-center justify-between gap-4 rounded-xl border border-red-200 bg-red-50 p-3 text-sm text-red-700">
7
+ <span>{{ cartError }}</span>
8
+ <button type="button" class="shrink-0 underline" @click="reviewCart">Review cart</button>
9
+ </div>
10
+ </div>
11
+
4
12
  <!-- ============ MOBILE ORDER STRIP ============ -->
5
13
  <div class="border-b border-gray-200 bg-white lg:hidden">
6
14
  <button
@@ -14,17 +22,17 @@
14
22
  <template v-else>
15
23
  <div class="flex flex-shrink-0 gap-1.5">
16
24
  <div
17
- v-for="item in CartItems.slice(0, 2)"
18
- :key="`strip-${item.id}`"
25
+ v-for="item in TopLevelItems.slice(0, 2)"
26
+ :key="`strip-${item.order_item_id}`"
19
27
  class="h-11 w-9 overflow-hidden rounded-md border border-gray-200 bg-white"
20
28
  >
21
29
  <img :src="imageUrl(item.image)" :alt="item.name" class="h-full w-full object-cover" />
22
30
  </div>
23
31
  <div
24
- v-if="CartItems.length > 2"
32
+ v-if="TopLevelItems.length > 2"
25
33
  class="flex h-11 w-9 items-center justify-center rounded-md border border-dashed border-gray-300 text-xs text-gray-500"
26
34
  >
27
- +{{ CartItems.length - 2 }}
35
+ +{{ TopLevelItems.length - 2 }}
28
36
  </div>
29
37
  </div>
30
38
  <div class="min-w-0 flex-1">
@@ -53,7 +61,9 @@
53
61
  <!-- line items with steppers -->
54
62
  <div
55
63
  v-for="item in CartItems"
56
- :key="`strip-item-${item.id}`"
64
+ :key="`strip-item-${item.order_item_id}`"
65
+ :class="item.parent_item ? 'ms-5 opacity-80' : ''"
66
+ :data-checkout-lines="item === CartItems[0] ? 'mobile' : undefined"
57
67
  class="group flex items-center gap-3 py-2"
58
68
  >
59
69
  <div class="h-14 w-12 flex-shrink-0 overflow-hidden rounded-md border border-gray-200 bg-white">
@@ -62,7 +72,8 @@
62
72
  <div class="min-w-0 flex-1">
63
73
  <p class="truncate text-[13px]">{{ item.name }}</p>
64
74
  <p v-if="Array.isArray(item.values) ? item.values.length : item.values" class="truncate text-xs text-gray-400">{{ Array.isArray(item.values) && item.values.length ? item.values.join(' | ') : item.values || '' }}</p>
65
- <div class="mt-1.5 flex items-center gap-3">
75
+ <p v-for="prop in item.properties || []" :key="prop.key" class="text-xs text-gray-500">{{ prop.label }}: {{ prop.value === true ? 'Yes' : prop.value }}</p>
76
+ <div v-if="!item.parent_item" class="mt-1.5 flex items-center gap-3">
66
77
  <div class="flex h-7 w-fit items-center rounded-full border border-gray-300">
67
78
  <button
68
79
  type="button"
@@ -253,7 +264,7 @@
253
264
  </div>
254
265
 
255
266
  <div
256
- v-if="!isLoggedIn"
267
+ v-if="needsPhoneInput"
257
268
  data-field="mobile_number"
258
269
  class="px-3"
259
270
  :class="phoneHasError ? 'bg-red-50' : ''"
@@ -272,7 +283,7 @@
272
283
  v-model="formData.mobile_number"
273
284
  placeholder=" "
274
285
  class="floating-input disabled:text-gray-500"
275
- :disabled="isLoggedIn && formData.mobile_number !== ''"
286
+ disabled
276
287
  />
277
288
  <label for="mobile_number" class="floating-label">Phone</label>
278
289
  </div>
@@ -572,7 +583,9 @@
572
583
 
573
584
  <div
574
585
  v-for="item in CartItems"
575
- :key="`rail-${item.id}`"
586
+ :key="`rail-${item.order_item_id}`"
587
+ :class="item.parent_item ? 'ms-5 opacity-80' : ''"
588
+ :data-checkout-lines="item === CartItems[0] ? 'rail' : undefined"
576
589
  class="group flex items-center gap-3 py-2"
577
590
  >
578
591
  <div class="h-14 w-12 flex-shrink-0 overflow-hidden rounded-md border border-gray-200 bg-white">
@@ -581,7 +594,8 @@
581
594
  <div class="min-w-0 flex-1">
582
595
  <p class="truncate text-[13px]">{{ item.name }}</p>
583
596
  <p v-if="Array.isArray(item.values) ? item.values.length : item.values" class="truncate text-xs text-gray-400">{{ Array.isArray(item.values) && item.values.length ? item.values.join(' | ') : item.values || '' }}</p>
584
- <div class="mt-1.5 flex items-center gap-3">
597
+ <p v-for="prop in item.properties || []" :key="prop.key" class="text-xs text-gray-500">{{ prop.label }}: {{ prop.value === true ? 'Yes' : prop.value }}</p>
598
+ <div v-if="!item.parent_item" class="mt-1.5 flex items-center gap-3">
585
599
  <div class="flex h-7 w-fit items-center rounded-full border border-gray-300">
586
600
  <button
587
601
  type="button"
@@ -774,7 +788,8 @@
774
788
  <!-- line items -->
775
789
  <div
776
790
  v-for="item in CartItems"
777
- :key="`bottom-${item.id}`"
791
+ :key="`bottom-${item.order_item_id}`"
792
+ :class="item.parent_item ? 'ms-5 opacity-80' : ''"
778
793
  class="group flex items-center gap-3 py-2"
779
794
  >
780
795
  <div class="h-14 w-12 flex-shrink-0 overflow-hidden rounded-md border border-gray-200 bg-white">
@@ -783,7 +798,8 @@
783
798
  <div class="min-w-0 flex-1">
784
799
  <p class="truncate text-[13px]">{{ item.name }}</p>
785
800
  <p v-if="Array.isArray(item.values) ? item.values.length : item.values" class="truncate text-xs text-gray-400">{{ Array.isArray(item.values) && item.values.length ? item.values.join(' | ') : item.values || '' }}</p>
786
- <div class="mt-1.5 flex items-center gap-3">
801
+ <p v-for="prop in item.properties || []" :key="prop.key" class="text-xs text-gray-500">{{ prop.label }}: {{ prop.value === true ? 'Yes' : prop.value }}</p>
802
+ <div v-if="!item.parent_item" class="mt-1.5 flex items-center gap-3">
787
803
  <div class="flex h-7 w-fit items-center rounded-full border border-gray-300">
788
804
  <button
789
805
  type="button"
@@ -977,14 +993,18 @@ const {
977
993
  accordionAfterEnter,
978
994
  // cart
979
995
  CartItems,
996
+ TopLevelItems,
980
997
  total_price,
981
998
  totalItemsCount,
982
999
  isItemUpdating,
983
1000
  increaseQuantity,
984
1001
  decreaseQuantity,
985
1002
  deleteItem,
1003
+ cartError,
1004
+ reviewCart,
986
1005
  // user + form
987
1006
  isLoggedIn,
1007
+ needsPhoneInput,
988
1008
  formData,
989
1009
  formErrors,
990
1010
  clearFieldError,
@@ -368,10 +368,8 @@ export default {
368
368
 
369
369
  try {
370
370
  this.isLoginLoading = true;
371
- const deviceInfo = getDeviceInfo();
372
371
  const loginResponse = await this.login({
373
372
  ...this.formData,
374
- ...deviceInfo,
375
373
  });
376
374
  toast.success("You have successfully signed into your account!");
377
375
  authBus.checkAuth();
@@ -209,8 +209,9 @@
209
209
  <div class="flex flex-col gap-4 mb-6">
210
210
  <div
211
211
  v-for="(item, index) in cartItemsWithLink"
212
- :key="index"
212
+ :key="item.id ?? index"
213
213
  class="flex gap-4 items-start"
214
+ :class="item.parent_item ? 'ms-6 opacity-80' : ''"
214
215
  >
215
216
  <component
216
217
  :is="item.productLink ? 'RouterLink' : 'div'"
@@ -242,6 +243,14 @@
242
243
  <p v-if="item.variant_values" class="text-gray-500 text-xs mt-1">
243
244
  {{ item.variant_values }}
244
245
  </p>
246
+ <!-- Customization snapshot taken at checkout. -->
247
+ <p
248
+ v-for="prop in item.properties || []"
249
+ :key="prop.key"
250
+ class="text-gray-500 text-xs mt-1"
251
+ >
252
+ {{ prop.label }}: {{ prop.value === true ? "Yes" : prop.value }}
253
+ </p>
245
254
  </div>
246
255
  <p class="text-gray-800 font-medium text-sm whitespace-nowrap">
247
256
  {{ currency }}{{ item.checkout_price }}
@@ -387,8 +396,22 @@ const sortedStatuses = computed(() =>
387
396
 
388
397
  const currentStatus = computed(() => sortedStatuses.value[0]?.status_name || "");
389
398
 
399
+ // Each product followed by its add-on lines (parent_item = the parent's id).
400
+ const orderedItems = computed(() => {
401
+ const items = cartItems.value;
402
+ const parents = items.filter((item) => !item.parent_item);
403
+ const ids = new Set(parents.map((item) => String(item.id)));
404
+ return [
405
+ ...parents.flatMap((parent) => [
406
+ parent,
407
+ ...items.filter((child) => child.parent_item && String(child.parent_item) === String(parent.id)),
408
+ ]),
409
+ ...items.filter((item) => item.parent_item && !ids.has(String(item.parent_item))),
410
+ ];
411
+ });
412
+
390
413
  const cartItemsWithLink = computed(() =>
391
- cartItems.value.map((item) => {
414
+ orderedItems.value.map((item) => {
392
415
  const product = item?.product?.product;
393
416
  const categorySlug = product?.categories?.[0]?.category?.slugified_name;
394
417
  const productSlug = product?.slugified_name;
@@ -1,10 +1,10 @@
1
1
  <template>
2
2
  <div
3
- class="w-full xlg1:w-[70%] h-fit border border-gray-100 shadow-md p-5 bg-gray-50 font-body"
3
+ class="w-full xlg1:w-[70%] osi-acc-card h-fit shadow-md p-5 font-body"
4
4
  >
5
5
  <div v-if="wishlistItems && wishlistItems.length > 0">
6
6
  <div
7
- class="bg-gray-50 grid grid-cols-1 s:grid-cols-2 l:grid-cols-3 gap-3"
7
+ class="grid grid-cols-1 s:grid-cols-2 l:grid-cols-3 gap-3"
8
8
  >
9
9
  <div
10
10
  v-for="item in wishlistItems"
@@ -75,7 +75,7 @@
75
75
  <p class="text-primaryColor font-body text-xl s:text-2xl">
76
76
  Wishlist is empty
77
77
  </p>
78
- <div class="text-center text-gray-500">
78
+ <div class="text-center osi-acc-muted">
79
79
  <p class="font-body text-sm">
80
80
  You don't have any products in your wishlist yet.
81
81
  </p>
@@ -3,11 +3,11 @@
3
3
  class="w-full flex flex-col justify-center items-center gap-5 font-body"
4
4
  >
5
5
  <div
6
- class="flex flex-col w-full xlg1:w-[70%] shadow-md border border-gray-100 bg-gray-50"
6
+ class="flex flex-col w-full xlg1:w-[70%] osi-acc-card shadow-md"
7
7
  >
8
8
  <div class="w-full flex flex-col justify-center items-center pb-10 gap-5">
9
9
  <div
10
- class="w-[80px] s:w-[100px] h-[80px] s:h-[100px] rounded-full border border-gray-50 shadow-md flex justify-center items-center p-1 bg-white absolute"
10
+ class="w-[80px] s:w-[100px] h-[80px] s:h-[100px] osi-acc-avatar rounded-full shadow-md flex justify-center items-center p-1 absolute"
11
11
  >
12
12
  <img
13
13
  v-if="logoSrc"
@@ -25,10 +25,10 @@
25
25
  class="cursor-pointer w-full md:w-auto"
26
26
  >
27
27
  <div
28
- class="w-full py-3 border-b border-gray-100 md:border-none flex justify-start items-center gap-2"
28
+ class="w-full py-3 border-b osi-acc-line md:border-none flex justify-start items-center gap-2"
29
29
  :class="{
30
30
  'text-primaryColor': activeStep === 'profileDetails',
31
- 'text-gray-500': activeStep !== 'profileDetails',
31
+ 'osi-acc-muted': activeStep !== 'profileDetails',
32
32
  }"
33
33
  >
34
34
  <svg
@@ -58,10 +58,10 @@
58
58
  @click="selectStep('Whishlist')"
59
59
  >
60
60
  <div
61
- class="w-full py-3 border-b border-gray-100 md:border-none flex justify-start items-center gap-2 group-hover:text-primaryColor"
61
+ class="w-full py-3 border-b osi-acc-line md:border-none flex justify-start items-center gap-2 group-hover:text-primaryColor"
62
62
  :class="{
63
63
  'text-primaryColor': activeStep === 'Whishlist',
64
- 'text-gray-500': activeStep !== 'Whishlist',
64
+ 'osi-acc-muted': activeStep !== 'Whishlist',
65
65
  }"
66
66
  >
67
67
  <svg
@@ -89,10 +89,10 @@
89
89
  @click="selectStep('Orders')"
90
90
  >
91
91
  <div
92
- class="w-full py-3 border-b border-gray-100 md:border-none group-hover:text-primaryColor flex justify-start items-center gap-2"
92
+ class="w-full py-3 border-b osi-acc-line md:border-none group-hover:text-primaryColor flex justify-start items-center gap-2"
93
93
  :class="{
94
94
  'text-primaryColor': activeStep === 'Orders',
95
- 'text-gray-500': activeStep !== 'Orders',
95
+ 'osi-acc-muted': activeStep !== 'Orders',
96
96
  }"
97
97
  >
98
98
  <svg
@@ -124,10 +124,10 @@
124
124
  @click="selectStep('settings')"
125
125
  >
126
126
  <div
127
- class="w-full border-b border-gray-100 md:border-none py-3 flex justify-start items-center gap-2"
127
+ class="w-full border-b osi-acc-line md:border-none py-3 flex justify-start items-center gap-2"
128
128
  :class="{
129
129
  'text-primaryColor': activeStep === 'settings',
130
- 'text-gray-500': activeStep !== 'settings',
130
+ 'osi-acc-muted': activeStep !== 'settings',
131
131
  }"
132
132
  >
133
133
  <svg
@@ -157,12 +157,12 @@
157
157
 
158
158
  <router-link @click="logout" class="group" to="/">
159
159
  <div
160
- class="w-full py-3 flex justify-start items-center gap-2 text-gray-500"
160
+ class="w-full py-3 flex justify-start items-center gap-2 osi-acc-muted"
161
161
  >
162
162
  <svg
163
163
  xmlns="http://www.w3.org/2000/svg"
164
164
  viewBox="0 0 24 24"
165
- class="text-gray-500 group-hover:text-primaryColor duration-300"
165
+ class="osi-acc-muted group-hover:text-primaryColor duration-300"
166
166
  width="24"
167
167
  height="24"
168
168
  color="#000000"
@@ -1,50 +1,50 @@
1
1
  <template>
2
2
  <div
3
- class="w-full xlg1:w-[70%] h-fit shadow-md bg-gray-50 px-5 pb-5 font-body"
3
+ class="w-full xlg1:w-[70%] osi-acc-card h-fit shadow-md px-5 pb-5 font-body"
4
4
  >
5
5
  <div v-if="allOrders.length">
6
6
  <div class="w-full flex flex-col mt-5 gap-5">
7
7
  <div
8
8
  v-for="order in sortedOrders"
9
9
  :key="order.id"
10
- class="w-full flex bg-white flex-col py-5 px-5"
10
+ class="osi-acc-tile w-full flex flex-col py-5 px-5"
11
11
  >
12
12
  <div class="flex flex-col gap-5">
13
13
  <div class="flex justify-between">
14
14
  <div>
15
- <p class="text-gray-500 uppercase text-[17px]">
15
+ <p class="osi-acc-muted uppercase text-[17px]">
16
16
  {{ getLatestStatus(order) || "NA" }}
17
17
  </p>
18
- <p class="text-gray-500 font-heading uppercase text-[17px]">
18
+ <p class="osi-acc-muted font-heading uppercase text-[17px]">
19
19
  {{ new Date(order?.purchased_at).toLocaleString() }}
20
20
  </p>
21
21
  </div>
22
22
  <div
23
- class="text-gray-500 cursor-pointer"
23
+ class="osi-acc-muted cursor-pointer"
24
24
  @click="goToOrderHistory(order?.id)"
25
25
  >
26
26
  Go to order
27
27
  </div>
28
28
  </div>
29
29
  <div class="pb-5">
30
- <p class="text-gray-500 font-bold">
31
- {{ currency }}{{ order?.checkout_cart_price + order?.checkout_delivery_fees }}
30
+ <p class="osi-acc-muted font-bold">
31
+ {{ currency }}{{ orderTotal(order) }}
32
32
  </p>
33
33
  </div>
34
34
  </div>
35
35
  <div class="w-full max-w-[1800px] grid grid-cols-2 sm:grid-cols-4">
36
36
  <div
37
37
  v-for="(item, index) in getCartItemsWithLink(order?.items || [])"
38
- :key="index"
38
+ :key="item.id ?? index"
39
39
  class="transition-all w-full sm:w-[200px] duration-300"
40
40
  >
41
41
  <component
42
42
  :is="item.productLink ? 'RouterLink' : 'div'"
43
43
  :to="item.productLink"
44
- class="border w-full sm:w-[200px] flex border-gray-100 relative"
44
+ class="border w-full sm:w-[200px] flex osi-acc-line relative"
45
45
  >
46
46
  <div
47
- class="absolute top-3 right-3 bg-white h-[30px] w-[30px] p-1 border border-primaryColor flex justify-center items-center text-primaryColor"
47
+ class="osi-acc-tile absolute top-3 right-3 h-[30px] w-[30px] p-1 border border-primaryColor flex justify-center items-center text-primaryColor"
48
48
  >
49
49
  {{ item.quantity }}
50
50
  </div>
@@ -56,15 +56,26 @@
56
56
  </component>
57
57
  <div class="w-full flex gap-2 p-2 justify-between items-start">
58
58
  <div class="flex flex-col gap-2">
59
- <p class="text-gray-500 text-sm line-clamp-1">
59
+ <p class="osi-acc-muted text-sm line-clamp-1">
60
60
  {{ item.name }}
61
61
  </p>
62
62
  <p
63
- class="text-gray-500 text-xs mt-1"
63
+ class="osi-acc-muted text-xs mt-1"
64
64
  v-if="item.variant_values"
65
65
  >
66
66
  {{ item.variant_values }}
67
67
  </p>
68
+ <!-- Add-on line (gift wrap…) bought with another product. -->
69
+ <p v-if="item.parent_item" class="osi-acc-muted text-xs">
70
+ Add-on{{ parentName(order, item) ? ` · ${parentName(order, item)}` : "" }}
71
+ </p>
72
+ <p
73
+ v-for="prop in item.properties || []"
74
+ :key="prop.key"
75
+ class="osi-acc-muted text-xs"
76
+ >
77
+ {{ prop.label }}: {{ prop.value === true ? "Yes" : prop.value }}
78
+ </p>
68
79
  </div>
69
80
  </div>
70
81
  </div>
@@ -94,7 +105,7 @@
94
105
  >
95
106
  You have no orders at the moment.
96
107
  </p>
97
- <div class="text-center text-gray-500">
108
+ <div class="text-center osi-acc-muted">
98
109
  <p class="font-body text-sm">You don't have any orders yet.</p>
99
110
  <p class="font-body text-sm">
100
111
  You will find many interesting products on the "Shopping" page.
@@ -146,6 +157,16 @@ const getLatestStatus = (order) => {
146
157
  return latest.status_name;
147
158
  };
148
159
 
160
+ // The server's own total (promo, fees and customization included). The old
161
+ // sum of two fields concatenated when the API returned them as strings.
162
+ const orderTotal = (order) =>
163
+ order?.checkout_order_total ??
164
+ (Number(order?.checkout_cart_price) || 0) + (Number(order?.checkout_delivery_fees) || 0);
165
+
166
+ /** The product an add-on line was bought with. */
167
+ const parentName = (order, item) =>
168
+ (order?.items || []).find((line) => String(line.id) === String(item.parent_item))?.name || "";
169
+
149
170
  const getCartItemsWithLink = (items = []) => {
150
171
  return items.map((item) => {
151
172
  const product = item?.product?.product;
@@ -10,7 +10,7 @@
10
10
  @cancel-delete="closeDeleteModal"
11
11
  />
12
12
  <!-- User Information Section -->
13
- <div class="osi-profile-card w-full h-fit border border-gray-100 shadow-md p-5 bg-gray-50">
13
+ <div class="osi-profile-card osi-acc-card w-full h-fit shadow-md p-5">
14
14
  <div class="w-full flex justify-between items-center gap-3">
15
15
  <h1 class="text-primaryColor font-body text-xl">Profile</h1>
16
16
 
@@ -127,7 +127,7 @@
127
127
  </div>
128
128
  </div>
129
129
 
130
- <div class="osi-profile-card w-full h-fit border border-gray-100 shadow-md p-5 bg-gray-50">
130
+ <div class="osi-profile-card osi-acc-card w-full h-fit shadow-md p-5">
131
131
  <div class="w-full flex justify-between items-center gap-3 pr-2">
132
132
  <h1 class="text-primaryColor font-body text-xl">Addresses</h1>
133
133
 
@@ -186,7 +186,7 @@
186
186
  >
187
187
  You have not added any addresses yet
188
188
  </p>
189
- <div class="text-center text-gray-500">
189
+ <div class="text-center osi-acc-muted">
190
190
  <p class="font-body text-sm">
191
191
  Add your addresses for a quicker and more efficient checkout.
192
192
  </p>
@@ -201,7 +201,7 @@
201
201
  >
202
202
  <div class="h-[200px]">
203
203
  <div
204
- class="bg-white shadow-sm h-[200px] flex flex-col gap-3 p-3"
204
+ class="osi-acc-tile shadow-sm h-[200px] flex flex-col gap-3 p-3"
205
205
  >
206
206
 
207
207
  <div class="flex w-full justify-end gap-1">
@@ -226,7 +226,7 @@
226
226
  </div>
227
227
 
228
228
  <div
229
- class="w-full bg-white text-gray-500 text-sm s:text-base grid gap-2"
229
+ class="w-full osi-acc-muted text-sm s:text-base grid gap-2"
230
230
  >
231
231
  <p class="font-body">
232
232
  <span class="font-semibold">Country:</span>
@@ -552,7 +552,7 @@ export default {
552
552
  /* Fields and buttons come from the theme (OsiField / .osi-btn). */
553
553
  .osi-profile-card {
554
554
  /* Floating labels cut the field border with a chip in this colour. */
555
- --osi-field-chip: #f9fafb;
555
+ --osi-field-chip: var(--acc-card, #f9fafb);
556
556
  }
557
557
  .osi-profile-icon-btn {
558
558
  --btn-pad-y: 5px;