@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,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>
@@ -17,37 +17,112 @@ const persistCartToken = (responseData) => {
17
17
  };
18
18
 
19
19
  /**
20
- * Add to / remove from a cart line (a line is a variant id).
20
+ * A cart response's lines as a list. Lines are keyed by their line id
21
+ * (order_item_id); one variant can sit on several lines (different
22
+ * customizations, or as an add-on of another product), so nothing may look a
23
+ * line up by variant id.
24
+ */
25
+ export function cartLines(response) {
26
+ return Object.entries(response?.cart || {}).map(([key, line]) => ({
27
+ ...line,
28
+ order_item_id: line?.order_item_id ?? key,
29
+ }));
30
+ }
31
+
32
+ /** A variant's own top-level lines summed: what "this product in the cart" means now. */
33
+ function variantSnapshot(lines, variantId) {
34
+ const own = lines.filter((l) => String(l.id) === String(variantId) && !l.parent_item);
35
+ if (!own.length) return null;
36
+ return { ...own[0], quantity: own.reduce((n, l) => n + (Number(l.quantity) || 0), 0) };
37
+ }
38
+
39
+ async function post(body) {
40
+ const baseUrl = getBaseUrl();
41
+ const responseData = await apiService(`${baseUrl}store/apis/cart/update-item/`, {
42
+ method: "POST",
43
+ headers: cartHeaders(),
44
+ body: JSON.stringify(body),
45
+ });
46
+ return persistCartToken(responseData);
47
+ }
48
+
49
+ /**
50
+ * Add a variant to the cart (the only action that takes a variant id).
21
51
  *
22
52
  * `context` is optional: callers that hold the product (product page, card,
23
53
  * quick buy) pass `{ product, variant }` so the analytics event carries its
24
- * brand and category, which the cart payload doesn't repeat. Cart-side callers
25
- * (drawer, checkout) omit it and get it from what was remembered at add time.
54
+ * brand and category, which the cart payload doesn't repeat.
55
+ *
56
+ * `properties` are the customization answers, keyed by field key
57
+ * ({ cover_name: "Lara", cover_color: "<option id>", gift_wrap: true }). The
58
+ * backend validates and prices them; a bad answer comes back as
59
+ * 400 { properties: { key: message } } and nothing is added. Same variant +
60
+ * same answers merges into the existing line, different answers make a new one.
26
61
  *
27
- * This is the single place add_to_cart / remove_from_cart are reported: every
28
- * cart edit in the storefront goes through here, once.
62
+ * Signature kept from before (item_id, quantity, action, context, properties)
63
+ * so existing callers don't change. Any action other than "add" is forwarded
64
+ * to editLine with `item_id` treated as the line id.
29
65
  */
30
- const EditCart = async (item_id, quantity = 1, action, context) => {
66
+ const EditCart = async (item_id, quantity = 1, action = "add", context, properties = null) => {
67
+ if (action !== "add") return editLine({ order_item_id: item_id }, action, quantity);
31
68
  try {
32
- const baseUrl = getBaseUrl();
33
- const before = analytics.peekCartLine(item_id);
34
- const responseData = await apiService(`${baseUrl}store/apis/cart/update-item/`, {
35
- method: "POST",
36
- headers: cartHeaders(),
37
- body: JSON.stringify({ quantity, item_id, action }),
38
- });
39
- persistCartToken(responseData);
69
+ const before = variantSnapshot(analytics.peekCartLines(), item_id);
70
+ const body = { item_id, quantity, action: "add" };
71
+ if (properties && Object.keys(properties).length) body.properties = properties;
72
+ const responseData = await post(body);
40
73
  analytics.cartEdited({
41
74
  itemId: item_id,
75
+ variantId: item_id,
42
76
  quantity,
43
- action,
77
+ action: "add",
44
78
  before,
45
- after: responseData?.cart?.[item_id] ?? null,
79
+ after: variantSnapshot(cartLines(responseData), item_id),
46
80
  context,
47
81
  });
48
82
  return responseData;
49
83
  } catch (error) {
50
- console.error('Cart fetch error:', error);
84
+ console.error('Cart update error:', error);
85
+ throw error;
86
+ }
87
+ };
88
+
89
+ /**
90
+ * Change one exact cart line.
91
+ *
92
+ * edit set the line's quantity to `quantity`
93
+ * remove take `quantity` off the line (0 deletes it)
94
+ * remove_all delete the line
95
+ *
96
+ * Add-on lines (parent_item set) follow their parent and can't be edited on
97
+ * their own — the backend rejects it, so callers never offer it.
98
+ */
99
+ const editLine = async (line, action, quantity) => {
100
+ const orderItemId = line?.order_item_id;
101
+ if (!orderItemId) throw new Error("Missing cart line id.");
102
+ try {
103
+ const before = analytics.peekCartLine(orderItemId);
104
+ const body = { order_item_id: orderItemId, action };
105
+ if (action !== "remove_all") body.quantity = quantity;
106
+ const responseData = await post(body);
107
+ const after = responseData?.cart?.[orderItemId]
108
+ ?? cartLines(responseData).find((l) => String(l.order_item_id) === String(orderItemId))
109
+ ?? null;
110
+ const prev = Number(before?.quantity ?? line?.quantity) || 0;
111
+ const next = Number(after?.quantity) || 0;
112
+ // Analytics speaks add/remove; "edit" is whichever way the quantity moved.
113
+ if (next !== prev) {
114
+ analytics.cartEdited({
115
+ itemId: orderItemId,
116
+ variantId: before?.id ?? line?.id,
117
+ quantity: Math.abs(next - prev),
118
+ action: next > prev ? "add" : "remove",
119
+ before: before ?? line,
120
+ after,
121
+ });
122
+ }
123
+ return responseData;
124
+ } catch (error) {
125
+ console.error('Cart update error:', error);
51
126
  throw error;
52
127
  }
53
128
  };
@@ -86,6 +161,7 @@ const mergeCart = async () => {
86
161
 
87
162
  export default {
88
163
  EditCart,
164
+ editLine,
89
165
  fetchCart,
90
166
  mergeCart,
91
- };
167
+ };
@@ -72,9 +72,9 @@ const state = {
72
72
  pixelIds: [],
73
73
  currency: "",
74
74
  consent: true,
75
- /** Last cart the server sent, keyed by line (variant) id. */
75
+ /** Last cart the server sent, keyed by line id (order_item_id). */
76
76
  cart: {},
77
- /** What we know about a line beyond the cart payload: brand, category… */
77
+ /** What we know about a variant beyond the cart payload: brand, category… (keyed by variant id) */
78
78
  lineContext: new Map(),
79
79
  /** Once-per-page guards (view_item, view_item_list). Cleared on navigation. */
80
80
  seen: new Set(),
@@ -240,12 +240,14 @@ export function productToItem(product, opts = {}) {
240
240
  }
241
241
 
242
242
  /**
243
- * A cart line (the server's `cart[id]` entry, keyed by variant id) -> item.
244
- * The payload doesn't repeat brand / category, so what the add-to-cart call
245
- * site knew is remembered per line and laid back over it here.
243
+ * A cart line -> item. `line.id` is the variant id (one variant can sit on
244
+ * several lines now, so lines are keyed by order_item_id); `id` is only the
245
+ * fallback. The payload doesn't repeat brand / category, so what the
246
+ * add-to-cart call site knew is remembered per variant and laid back over it.
246
247
  */
247
248
  function cartLineToItem(id, line, quantity) {
248
- const known = state.lineContext.get(String(id)) ?? {};
249
+ const variantId = line?.id ?? id;
250
+ const known = state.lineContext.get(String(variantId)) ?? {};
249
251
  const productId =
250
252
  known.item_id ?? line?.product_id ?? line?.product?.id ?? line?.product?.product?.id ?? id;
251
253
  return clean({
@@ -255,7 +257,7 @@ function cartLineToItem(id, line, quantity) {
255
257
  item_category: known.item_category,
256
258
  item_category2: known.item_category2,
257
259
  item_variant: variantLabel(line) || known.item_variant,
258
- variant_id: String(id),
260
+ variant_id: String(variantId),
259
261
  sku: line?.sku ?? known.sku,
260
262
  price: money(line?.price ?? known.price),
261
263
  quantity: quantity ?? Math.max(1, num(line?.quantity)),
@@ -499,11 +501,16 @@ function selectItem(product, override = {}) {
499
501
  log("select_item", ga);
500
502
  }
501
503
 
502
- /** What we last heard the cart contained for this line, before an edit. */
504
+ /** What we last heard the cart contained for this line (order_item_id), before an edit. */
503
505
  function peekCartLine(id) {
504
506
  return state.cart[String(id)] ?? null;
505
507
  }
506
508
 
509
+ /** Every line we last heard about, as a list (to sum a variant across lines). */
510
+ function peekCartLines() {
511
+ return Object.entries(state.cart).map(([key, line]) => ({ ...line, order_item_id: line?.order_item_id ?? key }));
512
+ }
513
+
507
514
  /** Remembers the server's cart. Called with every cart response. */
508
515
  function rememberCart(response) {
509
516
  if (!isClient() || !response || typeof response !== "object") return;
@@ -524,13 +531,16 @@ function resetCart() {
524
531
  * through, from product pages, cards, quick buy, the drawer and checkout — so
525
532
  * nothing is missed and nothing is counted twice.
526
533
  *
527
- * @param {{ itemId: string|number, quantity: number, action: "add"|"remove",
534
+ * `variantId` keys what the call site knew (brand, category); `itemId` is the
535
+ * line or variant the edit targeted.
536
+ *
537
+ * @param {{ itemId: string|number, variantId?: string|number, quantity: number, action: "add"|"remove",
528
538
  * before: object|null, after: object|null,
529
539
  * context?: { product?: object, variant?: object } }} edit
530
540
  */
531
- function cartEdited({ itemId, quantity, action, before, after, context }) {
541
+ function cartEdited({ itemId, variantId, quantity, action, before, after, context }) {
532
542
  if (!isClient()) return;
533
- const id = String(itemId);
543
+ const id = String(variantId ?? before?.id ?? after?.id ?? itemId);
534
544
 
535
545
  // Remember what the call site knew, for later edits made from the drawer.
536
546
  if (context?.product) {
@@ -832,6 +842,7 @@ const api = {
832
842
  viewItemList,
833
843
  selectItem,
834
844
  peekCartLine,
845
+ peekCartLines,
835
846
  rememberCart,
836
847
  resetCart,
837
848
  cartEdited,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@osimart/create-storefront",
3
- "version": "1.0.17",
3
+ "version": "1.0.21",
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",