@osimart/create-storefront 1.0.16 → 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.
@@ -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
- ...(isLoggedIn.value ? {} : { mobile_number_required: "Phone number" }),
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
- isLoggedIn.value ? "" : formErrors.value.mobile_number || "",
278
+ needsPhoneInput.value ? formErrors.value.mobile_number || "" : "",
273
279
  );
274
280
  const phoneHasError = computed(
275
281
  () =>
276
- !isLoggedIn.value &&
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 (!isLoggedIn.value) {
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",
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Guards customer-only pages (user-profile, etc.).
3
+ * Reads the same "token" cookie TokenService writes; useCookie works on
4
+ * both SSR and client, so the redirect happens before the page renders.
5
+ */
6
+ export default defineNuxtRouteMiddleware((to) => {
7
+ const token = useCookie("token");
8
+ if (!token.value) {
9
+ return navigateTo({ path: "/login", query: { redirect: to.fullPath } });
10
+ }
11
+ });
@@ -253,7 +253,7 @@
253
253
  </div>
254
254
 
255
255
  <div
256
- v-if="!isLoggedIn"
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
- :disabled="isLoggedIn && formData.mobile_number !== ''"
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,
@@ -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 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,33 +1,33 @@
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">
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 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,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="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 }}
@@ -94,7 +94,7 @@
94
94
  >
95
95
  You have no orders at the moment.
96
96
  </p>
97
- <div class="text-center text-gray-500">
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 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;
@@ -1,6 +1,6 @@
1
1
  <template>
2
2
  <div
3
- class="osi-settings-card w-full xlg1:w-[70%] h-fit shadow-md bg-gray-50 flex justify-center items-center p-5 font-body"
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 border border-gray-100 bg-gray-50 flex flex-col gap-5"
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 border border-gray-100 shadow-md p-5 bg-gray-50"
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 border border-gray-100 shadow-md p-5 bg-gray-50"
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] bg-gray-100 shadow-sm flex flex-col justify-between p-3"
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 text-gray-500 text-sm s:text-base grid gap-2"
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></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({ quantity, item_id, action }),
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@osimart/create-storefront",
3
- "version": "1.0.16",
3
+ "version": "1.0.20",
4
4
  "description": "Osimart storefront platform — logic, platform pages and CLI; extended by every theme repo",
5
5
  "type": "module",
6
6
  "main": "./nuxt.config.ts",