@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.
- package/app/composables/useAddToCart.js +16 -6
- package/app/composables/useCart.js +66 -25
- package/app/composables/useCheckout.js +80 -10
- package/app/composables/useProductCustomization.js +186 -0
- package/app/composables/useProductDetail.js +39 -2
- package/app/composables/useQuickShop.js +20 -0
- package/app/logic/components/CartDrawer.js +11 -19
- package/app/logic/components/CartItem.js +18 -1
- package/app/logic/components/ProductInfoBlock.js +3 -0
- package/app/logic/sections/FeaturedProductSection.js +1 -0
- package/app/logic/sections/MainProductSection.js +1 -0
- package/app/pages/checkout.vue +32 -12
- package/app/pages/login.vue +0 -2
- package/app/pages/order-detail.vue +25 -2
- package/app/pages/user-profile/components/Whishlist.vue +3 -3
- package/app/pages/user-profile/components/left-bar.vue +12 -12
- package/app/pages/user-profile/components/orders.vue +34 -13
- package/app/pages/user-profile/components/profile-details.vue +6 -6
- package/app/pages/user-profile/components/settings.vue +2 -2
- package/app/pages/user-profile/index.vue +57 -7
- package/app/services/CartServices.js +94 -18
- package/app/utils/analytics.js +22 -11
- package/package.json +1 -1
|
@@ -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
|
-
//
|
|
128
|
-
//
|
|
129
|
-
|
|
130
|
-
|
|
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
|
|
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(
|
|
147
|
-
const decrease = (item) => edit(
|
|
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,
|
|
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
|
-
|
|
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",
|
package/app/pages/checkout.vue
CHANGED
|
@@ -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
|
|
18
|
-
:key="`strip-${item.
|
|
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="
|
|
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
|
-
+{{
|
|
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.
|
|
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
|
-
<
|
|
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="
|
|
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
|
-
|
|
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.
|
|
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
|
-
<
|
|
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.
|
|
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
|
-
|
|
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,
|
package/app/pages/login.vue
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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="
|
|
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
|
|
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%]
|
|
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
|
|
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
|
|
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
|
-
'
|
|
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
|
|
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
|
-
'
|
|
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
|
|
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
|
-
'
|
|
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
|
|
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
|
-
'
|
|
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
|
|
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="
|
|
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
|
|
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
|
|
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="
|
|
15
|
+
<p class="osi-acc-muted uppercase text-[17px]">
|
|
16
16
|
{{ getLatestStatus(order) || "NA" }}
|
|
17
17
|
</p>
|
|
18
|
-
<p class="
|
|
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="
|
|
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="
|
|
31
|
-
{{ currency }}{{ order
|
|
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
|
|
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
|
|
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="
|
|
59
|
+
<p class="osi-acc-muted text-sm line-clamp-1">
|
|
60
60
|
{{ item.name }}
|
|
61
61
|
</p>
|
|
62
62
|
<p
|
|
63
|
-
class="
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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="
|
|
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
|
|
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;
|