@osimart/create-storefront 1.0.17 → 1.0.20
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/useCheckout.js +33 -4
- package/app/composables/useProductCustomization.js +94 -0
- package/app/composables/useProductDetail.js +18 -2
- 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 +3 -2
- package/app/pages/login.vue +0 -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 +11 -11
- 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 +5 -3
- package/package.json +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import checkoutServices from "~/services/CheckoutServices";
|
|
2
2
|
import utilsServices from "~/services/UtilsServices";
|
|
3
3
|
import paymentServices from "~/services/PaymentServices";
|
|
4
|
+
import UserServices from "~/services/UserServices";
|
|
4
5
|
import { authBus } from "~/services/AuthBus";
|
|
5
6
|
import { useToast } from "vue-toastification";
|
|
6
7
|
import { eventBus } from "~/utils/eventBus";
|
|
@@ -97,6 +98,11 @@ export function useCheckout() {
|
|
|
97
98
|
// --- user ---
|
|
98
99
|
const isLoggedIn = ref(false);
|
|
99
100
|
const needsUserInfo = ref(true);
|
|
101
|
+
// Phone saved on the customer's profile at load. Typing in the field must
|
|
102
|
+
// not flip this, so it's tracked apart from formData.mobile_number.
|
|
103
|
+
const profileHasPhone = ref(false);
|
|
104
|
+
// Guests, and logged-in customers with no phone on file, get PhoneNumInput.
|
|
105
|
+
const needsPhoneInput = computed(() => !isLoggedIn.value || !profileHasPhone.value);
|
|
100
106
|
const userAddresses = ref([]);
|
|
101
107
|
|
|
102
108
|
// --- reference data ---
|
|
@@ -243,7 +249,7 @@ export function useCheckout() {
|
|
|
243
249
|
email_required: "Email",
|
|
244
250
|
first_name: "First name",
|
|
245
251
|
last_name: "Last name",
|
|
246
|
-
...(
|
|
252
|
+
...(needsPhoneInput.value ? { mobile_number_required: "Phone number" } : {}),
|
|
247
253
|
};
|
|
248
254
|
return Object.keys(labels)
|
|
249
255
|
.filter((key) => formErrors.value[key])
|
|
@@ -269,11 +275,11 @@ export function useCheckout() {
|
|
|
269
275
|
() => !!(formErrors.value.email || formErrors.value.email_required),
|
|
270
276
|
);
|
|
271
277
|
const phoneErrorMessage = computed(() =>
|
|
272
|
-
|
|
278
|
+
needsPhoneInput.value ? formErrors.value.mobile_number || "" : "",
|
|
273
279
|
);
|
|
274
280
|
const phoneHasError = computed(
|
|
275
281
|
() =>
|
|
276
|
-
|
|
282
|
+
needsPhoneInput.value &&
|
|
277
283
|
!!(formErrors.value.mobile_number || formErrors.value.mobile_number_required),
|
|
278
284
|
);
|
|
279
285
|
|
|
@@ -347,6 +353,7 @@ export function useCheckout() {
|
|
|
347
353
|
formData.last_name = info.last_name || "";
|
|
348
354
|
formData.email = info.email || "";
|
|
349
355
|
formData.mobile_number = info.mobile_number || "";
|
|
356
|
+
profileHasPhone.value = !!info.mobile_number;
|
|
350
357
|
}
|
|
351
358
|
}
|
|
352
359
|
|
|
@@ -581,7 +588,7 @@ export function useCheckout() {
|
|
|
581
588
|
}
|
|
582
589
|
if (!formData.payment_method_id)
|
|
583
590
|
errors.payment_method_id = "Payment method is required.";
|
|
584
|
-
if (
|
|
591
|
+
if (needsPhoneInput.value) {
|
|
585
592
|
if (!formData.mobile_number) {
|
|
586
593
|
errors.mobile_number_required = true;
|
|
587
594
|
} else if (!phoneNumberResults.value?.isValid) {
|
|
@@ -704,6 +711,27 @@ export function useCheckout() {
|
|
|
704
711
|
|
|
705
712
|
try {
|
|
706
713
|
isCheckingOut.value = true;
|
|
714
|
+
// Logged-in customer with no phone on file: save it to the profile
|
|
715
|
+
// first, so the order (and future checkouts) have it.
|
|
716
|
+
if (isLoggedIn.value && !profileHasPhone.value) {
|
|
717
|
+
try {
|
|
718
|
+
await UserServices.updateUserDetail({
|
|
719
|
+
first_name: formData.first_name,
|
|
720
|
+
last_name: formData.last_name,
|
|
721
|
+
email: formData.email,
|
|
722
|
+
mobile_number: formData.mobile_number,
|
|
723
|
+
});
|
|
724
|
+
profileHasPhone.value = true;
|
|
725
|
+
authBus.checkAuth();
|
|
726
|
+
} catch (error) {
|
|
727
|
+
const data = error?.data;
|
|
728
|
+
const msg =
|
|
729
|
+
(data && typeof data === "object" && Object.values(data).flat()[0]) ||
|
|
730
|
+
"Couldn't save your phone number. Please try again.";
|
|
731
|
+
toast.error(msg);
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
707
735
|
const response = await checkoutServices.placeOrder(payload);
|
|
708
736
|
placedOrderId.value = response.order_id;
|
|
709
737
|
sessionStorage.setItem("orderId", response.order_id);
|
|
@@ -875,6 +903,7 @@ export function useCheckout() {
|
|
|
875
903
|
|
|
876
904
|
// user + form
|
|
877
905
|
isLoggedIn,
|
|
906
|
+
needsPhoneInput,
|
|
878
907
|
formData,
|
|
879
908
|
formErrors,
|
|
880
909
|
clearFieldError,
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { computed, reactive, watch } from "vue";
|
|
2
|
+
|
|
3
|
+
const EMPTY = (value) =>
|
|
4
|
+
value === null || value === undefined || value === false ||
|
|
5
|
+
(typeof value === "string" && value.trim() === "");
|
|
6
|
+
|
|
7
|
+
/** Product customization answers, scoped to the product and variant shown. */
|
|
8
|
+
export function useProductCustomization(product, selectedVariant) {
|
|
9
|
+
const values = reactive({});
|
|
10
|
+
const errors = reactive({});
|
|
11
|
+
|
|
12
|
+
const visibleSets = computed(() => {
|
|
13
|
+
const variantId = selectedVariant.value?.id;
|
|
14
|
+
return (product.value?.customization_sets || []).filter(
|
|
15
|
+
(set) => !variantId || !(set.excluded_variants || []).includes(variantId),
|
|
16
|
+
);
|
|
17
|
+
});
|
|
18
|
+
const visibleFields = computed(() =>
|
|
19
|
+
visibleSets.value.flatMap((set) => set.customization_fields || []),
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
watch(visibleFields, (fields) => {
|
|
23
|
+
const keys = new Set(fields.map((field) => field.key));
|
|
24
|
+
Object.keys(values).forEach((key) => { if (!keys.has(key)) delete values[key]; });
|
|
25
|
+
Object.keys(errors).forEach((key) => { if (!keys.has(key)) delete errors[key]; });
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
function optionFor(field) {
|
|
29
|
+
return field.options?.find((option) => option.id === values[field.key]) || null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function costOf(field) {
|
|
33
|
+
if (EMPTY(values[field.key])) return null;
|
|
34
|
+
if (field.type === "SELECT") {
|
|
35
|
+
const option = optionFor(field);
|
|
36
|
+
if (!option) return null;
|
|
37
|
+
return option.addon_variant
|
|
38
|
+
? { addon: option.addon_variant }
|
|
39
|
+
: { delta: option.price_delta ?? field.price_delta ?? 0 };
|
|
40
|
+
}
|
|
41
|
+
return field.addon_variant
|
|
42
|
+
? { addon: field.addon_variant }
|
|
43
|
+
: { delta: field.price_delta || 0 };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const deltaPerUnit = computed(() => visibleFields.value.reduce(
|
|
47
|
+
(sum, field) => sum + (Number(costOf(field)?.delta) || 0), 0,
|
|
48
|
+
));
|
|
49
|
+
const addons = computed(() => visibleFields.value.map(costOf).filter((cost) => cost?.addon).map((cost) => cost.addon));
|
|
50
|
+
|
|
51
|
+
function validate() {
|
|
52
|
+
Object.keys(errors).forEach((key) => delete errors[key]);
|
|
53
|
+
for (const field of visibleFields.value) {
|
|
54
|
+
const value = values[field.key];
|
|
55
|
+
if (EMPTY(value)) {
|
|
56
|
+
if (field.required) errors[field.key] = `${field.label} is required.`;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (field.type === "NUMBER" && Number.isNaN(Number(value))) {
|
|
60
|
+
errors[field.key] = `${field.label} must be a number.`;
|
|
61
|
+
} else if (["TEXT", "TEXTAREA"].includes(field.type) && field.max_length && String(value).trim().length > field.max_length) {
|
|
62
|
+
errors[field.key] = `${field.label} must be at most ${field.max_length} characters.`;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return Object.keys(errors).length === 0;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const payload = computed(() => {
|
|
69
|
+
const result = {};
|
|
70
|
+
for (const field of visibleFields.value) {
|
|
71
|
+
const value = values[field.key];
|
|
72
|
+
if (EMPTY(value)) continue;
|
|
73
|
+
if (field.type === "CHECKBOX") result[field.key] = true;
|
|
74
|
+
else if (field.type === "NUMBER") result[field.key] = Number(value);
|
|
75
|
+
else if (field.type === "SELECT") result[field.key] = value;
|
|
76
|
+
else result[field.key] = String(value).trim();
|
|
77
|
+
}
|
|
78
|
+
return result;
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
function applyServerErrors(error) {
|
|
82
|
+
const serverErrors = error?.data?.properties || error?.response?._data?.properties;
|
|
83
|
+
if (!serverErrors) return false;
|
|
84
|
+
Object.assign(errors, serverErrors);
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function reset() {
|
|
89
|
+
Object.keys(values).forEach((key) => delete values[key]);
|
|
90
|
+
Object.keys(errors).forEach((key) => delete errors[key]);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return { values, errors, visibleSets, visibleFields, deltaPerUnit, addons, costOf, validate, payload, applyServerErrors, reset };
|
|
94
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import ProductServices from "#osi/services/ProductServices";
|
|
2
2
|
import CartServices from "#osi/services/CartServices";
|
|
3
|
+
import { useProductCustomization } from "#osi/composables/useProductCustomization";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Product detail state — the whole of Terazar/Maqui's product page logic, lifted
|
|
@@ -334,6 +335,8 @@ export function createProductDetail({ product, settings = {}, loading = false }
|
|
|
334
335
|
return { gallery: [...unique].sort((a, b) => order(a) - order(b)) };
|
|
335
336
|
});
|
|
336
337
|
|
|
338
|
+
const customization = useProductCustomization(p, exactMatchVariant);
|
|
339
|
+
|
|
337
340
|
function selectVariant(value) {
|
|
338
341
|
if (value?.isAvailable === false) return;
|
|
339
342
|
variants.selectVariant(value);
|
|
@@ -745,19 +748,29 @@ export function createProductDetail({ product, settings = {}, loading = false }
|
|
|
745
748
|
return false;
|
|
746
749
|
}
|
|
747
750
|
|
|
751
|
+
if (!customization.validate()) {
|
|
752
|
+
nextTick(() => document.querySelector(".osi-pdp-customization .has-error")?.scrollIntoView({ behavior: "smooth", block: "center" }));
|
|
753
|
+
return false;
|
|
754
|
+
}
|
|
755
|
+
|
|
748
756
|
isAddingToCart.value = true;
|
|
749
757
|
try {
|
|
750
758
|
await CartServices.EditCart(exactMatchVariant.value.id, quantity.value, "add", {
|
|
751
759
|
product: p.value,
|
|
752
760
|
variant: exactMatchVariant.value,
|
|
753
|
-
});
|
|
761
|
+
}, customization.payload.value);
|
|
754
762
|
await fetchCart();
|
|
763
|
+
customization.reset();
|
|
755
764
|
|
|
756
765
|
if (buyNow) router.push("/checkout");
|
|
757
766
|
else openCartUi();
|
|
758
767
|
|
|
759
768
|
return true;
|
|
760
769
|
} catch (error) {
|
|
770
|
+
if (customization.applyServerErrors(error)) {
|
|
771
|
+
nextTick(() => document.querySelector(".osi-pdp-customization .has-error")?.scrollIntoView({ behavior: "smooth", block: "center" }));
|
|
772
|
+
return false;
|
|
773
|
+
}
|
|
761
774
|
showApiError(error, "Couldn't add that to your cart.");
|
|
762
775
|
return false;
|
|
763
776
|
} finally {
|
|
@@ -915,6 +928,9 @@ export function createProductDetail({ product, settings = {}, loading = false }
|
|
|
915
928
|
showError,
|
|
916
929
|
resetSelections,
|
|
917
930
|
|
|
931
|
+
// customization
|
|
932
|
+
customization,
|
|
933
|
+
|
|
918
934
|
// price
|
|
919
935
|
pricing,
|
|
920
936
|
priceRange,
|
|
@@ -978,4 +994,4 @@ export function provideProductDetail(context) {
|
|
|
978
994
|
*/
|
|
979
995
|
export function useProductDetail() {
|
|
980
996
|
return inject(PRODUCT_DETAIL_KEY, null);
|
|
981
|
-
}
|
|
997
|
+
}
|
|
@@ -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
|
@@ -253,7 +253,7 @@
|
|
|
253
253
|
</div>
|
|
254
254
|
|
|
255
255
|
<div
|
|
256
|
-
v-if="
|
|
256
|
+
v-if="needsPhoneInput"
|
|
257
257
|
data-field="mobile_number"
|
|
258
258
|
class="px-3"
|
|
259
259
|
:class="phoneHasError ? 'bg-red-50' : ''"
|
|
@@ -272,7 +272,7 @@
|
|
|
272
272
|
v-model="formData.mobile_number"
|
|
273
273
|
placeholder=" "
|
|
274
274
|
class="floating-input disabled:text-gray-500"
|
|
275
|
-
|
|
275
|
+
disabled
|
|
276
276
|
/>
|
|
277
277
|
<label for="mobile_number" class="floating-label">Phone</label>
|
|
278
278
|
</div>
|
|
@@ -985,6 +985,7 @@ const {
|
|
|
985
985
|
deleteItem,
|
|
986
986
|
// user + form
|
|
987
987
|
isLoggedIn,
|
|
988
|
+
needsPhoneInput,
|
|
988
989
|
formData,
|
|
989
990
|
formErrors,
|
|
990
991
|
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();
|
|
@@ -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,33 +1,33 @@
|
|
|
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="
|
|
30
|
+
<p class="osi-acc-muted font-bold">
|
|
31
31
|
{{ currency }}{{ order?.checkout_cart_price + order?.checkout_delivery_fees }}
|
|
32
32
|
</p>
|
|
33
33
|
</div>
|
|
@@ -41,10 +41,10 @@
|
|
|
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,11 +56,11 @@
|
|
|
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 }}
|
|
@@ -94,7 +94,7 @@
|
|
|
94
94
|
>
|
|
95
95
|
You have no orders at the moment.
|
|
96
96
|
</p>
|
|
97
|
-
<div class="text-center
|
|
97
|
+
<div class="text-center osi-acc-muted">
|
|
98
98
|
<p class="font-body text-sm">You don't have any orders yet.</p>
|
|
99
99
|
<p class="font-body text-sm">
|
|
100
100
|
You will find many interesting products on the "Shopping" page.
|
|
@@ -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;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div
|
|
3
|
-
class="osi-settings-card w-full xlg1:w-[70%] h-fit shadow-md
|
|
3
|
+
class="osi-settings-card w-full xlg1:w-[70%] osi-acc-card h-fit shadow-md flex justify-center items-center p-5 font-body"
|
|
4
4
|
>
|
|
5
5
|
<form
|
|
6
6
|
novalidate
|
|
@@ -180,7 +180,7 @@ export default {
|
|
|
180
180
|
|
|
181
181
|
<style scoped>
|
|
182
182
|
.osi-settings-card {
|
|
183
|
-
--osi-field-chip: #f9fafb;
|
|
183
|
+
--osi-field-chip: var(--acc-card, #f9fafb);
|
|
184
184
|
}
|
|
185
185
|
.osi-settings-submit {
|
|
186
186
|
align-self: center;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div>
|
|
2
|
+
<div class="osi-account">
|
|
3
3
|
<banner pageTitle="user details" />
|
|
4
4
|
<div class="w-full flex justify-center items-center">
|
|
5
5
|
<div
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
>
|
|
8
8
|
<div
|
|
9
9
|
v-if="isLoading"
|
|
10
|
-
class="w-full xlg1:w-[70%] shadow-md
|
|
10
|
+
class="osi-acc-card w-full xlg1:w-[70%] shadow-md flex flex-col gap-5"
|
|
11
11
|
>
|
|
12
12
|
<!-- Profile Card Skeleton -->
|
|
13
13
|
<div
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
<div v-if="isLoading" class="w-full xlg1:w-[70%] flex flex-col gap-5">
|
|
49
49
|
<!-- User Information Section Skeleton -->
|
|
50
50
|
<div
|
|
51
|
-
class="grid grid-cols-1 s:grid-cols-2 gap-x-5 gap-y-8 h-fit
|
|
51
|
+
class="osi-acc-card grid grid-cols-1 s:grid-cols-2 gap-x-5 gap-y-8 h-fit shadow-md p-5"
|
|
52
52
|
>
|
|
53
53
|
<div v-for="i in 4" :key="i" class="flex flex-col gap-2">
|
|
54
54
|
<Skeletor height="20px" width="120px" class="rounded" />
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
|
|
63
63
|
<!-- Addresses Section Skeleton -->
|
|
64
64
|
<div
|
|
65
|
-
class="w-full h-fit
|
|
65
|
+
class="osi-acc-card w-full h-fit shadow-md p-5"
|
|
66
66
|
>
|
|
67
67
|
<div class="w-full flex justify-between items-center gap-3">
|
|
68
68
|
<Skeletor height="24px" width="150px" />
|
|
@@ -72,10 +72,10 @@
|
|
|
72
72
|
<div
|
|
73
73
|
v-for="i in 2"
|
|
74
74
|
:key="i"
|
|
75
|
-
class="h-[200px]
|
|
75
|
+
class="osi-acc-tile h-[200px] shadow-sm flex flex-col justify-between p-3"
|
|
76
76
|
>
|
|
77
77
|
<div
|
|
78
|
-
class="w-full
|
|
78
|
+
class="w-full osi-acc-muted text-sm s:text-base grid gap-2"
|
|
79
79
|
>
|
|
80
80
|
<!-- Country -->
|
|
81
81
|
<div class="flex items-center gap-2">
|
|
@@ -249,4 +249,54 @@ export default {
|
|
|
249
249
|
};
|
|
250
250
|
</script>
|
|
251
251
|
|
|
252
|
-
<style
|
|
252
|
+
<style>
|
|
253
|
+
/*
|
|
254
|
+
* Account-page surfaces, derived from the active colour scheme
|
|
255
|
+
* (--osi-bg / --osi-text / --osi-border, published on .osi-root) so the page
|
|
256
|
+
* follows light AND dark themes instead of a hardcoded gray.
|
|
257
|
+
*
|
|
258
|
+
* --acc-card panels: a faint step away from the page background
|
|
259
|
+
* --acc-line dividers and panel borders
|
|
260
|
+
* --acc-muted secondary text
|
|
261
|
+
* tiles (.osi-acc-tile) sit directly on the page background colour, so they
|
|
262
|
+
* read as "inset" panels in both themes.
|
|
263
|
+
*/
|
|
264
|
+
.osi-account {
|
|
265
|
+
--acc-card: #f9fafb;
|
|
266
|
+
--acc-line: var(--osi-border, #e5e5e5);
|
|
267
|
+
--acc-muted: #6b7280;
|
|
268
|
+
}
|
|
269
|
+
@supports (color: color-mix(in srgb, red 50%, blue)) {
|
|
270
|
+
.osi-account {
|
|
271
|
+
--acc-card: color-mix(
|
|
272
|
+
in srgb,
|
|
273
|
+
var(--osi-text, #121212) 6%,
|
|
274
|
+
var(--osi-bg, #ffffff)
|
|
275
|
+
);
|
|
276
|
+
--acc-muted: color-mix(
|
|
277
|
+
in srgb,
|
|
278
|
+
var(--osi-text, #121212) 65%,
|
|
279
|
+
var(--osi-bg, #ffffff)
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
.osi-acc-card {
|
|
284
|
+
background: var(--acc-card, #f9fafb);
|
|
285
|
+
border: 1px solid var(--acc-line, #e5e5e5);
|
|
286
|
+
}
|
|
287
|
+
.osi-acc-tile {
|
|
288
|
+
background: var(--osi-bg, #ffffff);
|
|
289
|
+
}
|
|
290
|
+
.osi-acc-avatar {
|
|
291
|
+
background: var(--acc-card, #f9fafb);
|
|
292
|
+
border: 1px solid var(--acc-line, #e5e5e5);
|
|
293
|
+
}
|
|
294
|
+
/* :where() keeps specificity at 0 so hover / active utilities
|
|
295
|
+
(group-hover:text-primaryColor etc.) still win. */
|
|
296
|
+
:where(.osi-acc-muted) {
|
|
297
|
+
color: var(--acc-muted, #6b7280);
|
|
298
|
+
}
|
|
299
|
+
:where(.osi-acc-line) {
|
|
300
|
+
border-color: var(--acc-line, #e5e5e5);
|
|
301
|
+
}
|
|
302
|
+
</style>
|
|
@@ -27,14 +27,16 @@ const persistCartToken = (responseData) => {
|
|
|
27
27
|
* This is the single place add_to_cart / remove_from_cart are reported: every
|
|
28
28
|
* cart edit in the storefront goes through here, once.
|
|
29
29
|
*/
|
|
30
|
-
const EditCart = async (item_id, quantity = 1, action, context) => {
|
|
30
|
+
const EditCart = async (item_id, quantity = 1, action, context, properties = null) => {
|
|
31
31
|
try {
|
|
32
32
|
const baseUrl = getBaseUrl();
|
|
33
33
|
const before = analytics.peekCartLine(item_id);
|
|
34
|
+
const body = { quantity, item_id, action };
|
|
35
|
+
if (properties && Object.keys(properties).length) body.properties = properties;
|
|
34
36
|
const responseData = await apiService(`${baseUrl}store/apis/cart/update-item/`, {
|
|
35
37
|
method: "POST",
|
|
36
38
|
headers: cartHeaders(),
|
|
37
|
-
body: JSON.stringify(
|
|
39
|
+
body: JSON.stringify(body),
|
|
38
40
|
});
|
|
39
41
|
persistCartToken(responseData);
|
|
40
42
|
analytics.cartEdited({
|
|
@@ -88,4 +90,4 @@ export default {
|
|
|
88
90
|
EditCart,
|
|
89
91
|
fetchCart,
|
|
90
92
|
mergeCart,
|
|
91
|
-
};
|
|
93
|
+
};
|
package/package.json
CHANGED