astro-tractstack 2.3.2 → 2.3.4

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.
Files changed (58) hide show
  1. package/bin/create-tractstack.js +7 -4
  2. package/dist/index.js +51 -8
  3. package/package.json +1 -1
  4. package/templates/custom/shopify/Cart.tsx +279 -118
  5. package/templates/custom/shopify/CartIcon.tsx +8 -8
  6. package/templates/custom/shopify/CheckoutModal.tsx +328 -65
  7. package/templates/custom/shopify/ShopifyCartManager.tsx +117 -60
  8. package/templates/custom/shopify/ShopifyCheckout.tsx +2 -26
  9. package/templates/custom/shopify/ShopifyProductGrid.tsx +8 -0
  10. package/templates/custom/shopify/ShopifyServiceList.tsx +25 -37
  11. package/templates/custom/shopify/cart.astro +7 -1
  12. package/templates/src/components/Header.astro +4 -2
  13. package/templates/src/components/compositor/Node.tsx +39 -9
  14. package/templates/src/components/compositor/nodes/CreativePane.tsx +175 -88
  15. package/templates/src/components/edit/pane/AddPanePanel.tsx +2 -2
  16. package/templates/src/components/edit/pane/AddPanePanel_new.tsx +6 -5
  17. package/templates/src/components/edit/pane/AddPanePanel_reuse.tsx +9 -15
  18. package/templates/src/components/edit/pane/ConfigPanePanel.tsx +1 -1
  19. package/templates/src/components/form/advanced/APIConfigSection.tsx +249 -4
  20. package/templates/src/components/form/brand/SiteConfigSection.tsx +9 -0
  21. package/templates/src/components/form/shopify/SchedulingSection.tsx +44 -0
  22. package/templates/src/components/storykeep/Dashboard_Advanced.tsx +66 -21
  23. package/templates/src/components/storykeep/Dashboard_Shopify.tsx +266 -18
  24. package/templates/src/components/storykeep/controls/content/ManageContent.tsx +1 -0
  25. package/templates/src/components/storykeep/controls/content/ProductTable.tsx +38 -24
  26. package/templates/src/components/storykeep/controls/content/ResourceForm.tsx +240 -65
  27. package/templates/src/components/storykeep/shopify/ShopifyDashboard.tsx +175 -48
  28. package/templates/src/components/storykeep/shopify/ShopifyDashboard_Bookings.tsx +91 -10
  29. package/templates/src/components/storykeep/shopify/ShopifyDashboard_Sales.tsx +479 -0
  30. package/templates/src/components/storykeep/shopify/ShopifyDashboard_Search.tsx +7 -3
  31. package/templates/src/constants.ts +2 -0
  32. package/templates/src/layouts/Layout.astro +26 -0
  33. package/templates/src/pages/api/auth/logout.ts +35 -2
  34. package/templates/src/pages/api/google/oauth/callback.ts +50 -0
  35. package/templates/src/pages/api/google/oauth/disconnect.ts +32 -0
  36. package/templates/src/pages/api/google/oauth/start.ts +32 -0
  37. package/templates/src/pages/api/google/oauth/status.ts +32 -0
  38. package/templates/src/pages/api/sales/list.ts +66 -0
  39. package/templates/src/pages/api/sales/metrics.ts +60 -0
  40. package/templates/src/pages/context/[...contextSlug].astro +50 -31
  41. package/templates/src/pages/privacy.astro +84 -0
  42. package/templates/src/pages/storykeep/advanced.astro +4 -1
  43. package/templates/src/pages/terms.astro +47 -0
  44. package/templates/src/stores/nodes.ts +8 -0
  45. package/templates/src/stores/shopify.ts +5 -0
  46. package/templates/src/types/tractstack.ts +87 -0
  47. package/templates/src/utils/api/advancedConfig.ts +2 -1
  48. package/templates/src/utils/api/advancedHelpers.ts +20 -0
  49. package/templates/src/utils/api/bookingHelpers.ts +3 -1
  50. package/templates/src/utils/api/brandConfig.ts +2 -0
  51. package/templates/src/utils/api/brandHelpers.ts +14 -1
  52. package/templates/src/utils/api/salesHelpers.ts +21 -0
  53. package/templates/src/utils/booking/appointmentMode.ts +135 -0
  54. package/templates/src/utils/customHelpers.ts +287 -2
  55. package/utils/inject-files.ts +47 -4
  56. package/templates/src/components/codehooks/SandboxAuthWrapper.tsx +0 -101
  57. package/templates/src/utils/actions/actionButton.ts +0 -103
  58. package/templates/src/utils/actions/preParse_Clicked.ts +0 -87
@@ -1,4 +1,4 @@
1
- import { useState, useEffect } from 'react';
1
+ import { useState, useEffect, useMemo } from 'react';
2
2
  import { ulid } from 'ulid';
3
3
  import { useStore } from '@nanostores/react';
4
4
  import {
@@ -7,14 +7,25 @@ import {
7
7
  cartState,
8
8
  CART_STATES,
9
9
  isShopifyHandoff,
10
+ preferredAppointmentMode,
10
11
  transactionTraceId,
11
12
  type CartItemState,
12
13
  } from '@/stores/shopify';
13
14
  import { getShopifyImage } from '@/utils/helpers';
15
+ import { deriveAppointmentConstraints } from '@/utils/booking/appointmentMode';
16
+ import {
17
+ getServiceLinkedProduct,
18
+ getServiceVariantIdFromCanonicalProduct,
19
+ getSharedFeeChargeLineSummary,
20
+ isSharedFeeService,
21
+ parsePrimaryShopifyProductData,
22
+ } from '@/utils/customHelpers';
14
23
  import type { ResourceNode } from '@/types/compositorTypes';
15
24
 
16
25
  interface CartProps {
17
26
  resources: ResourceNode[];
27
+ allowRemote?: boolean;
28
+ remoteOnly?: boolean;
18
29
  }
19
30
 
20
31
  const getCleanVariantTitle = (variant: any) => {
@@ -34,9 +45,20 @@ const getCleanVariantTitle = (variant: any) => {
34
45
  return title === 'Default Title' ? '' : title;
35
46
  };
36
47
 
37
- export default function Cart({ resources = [] }: CartProps) {
48
+ const getCategoryPriority = (categorySlug?: string): number => {
49
+ if (categorySlug === 'product') return 0;
50
+ if (categorySlug === 'service') return 1;
51
+ return 2;
52
+ };
53
+
54
+ export default function Cart({
55
+ resources = [],
56
+ allowRemote = false,
57
+ remoteOnly = false,
58
+ }: CartProps) {
38
59
  const cart = useStore(cartStore);
39
60
  const isHandoff = useStore(isShopifyHandoff);
61
+ const appointmentMode = useStore(preferredAppointmentMode);
40
62
  const [pickupEnabled, setPickupEnabled] = useState(false);
41
63
 
42
64
  const cartValues = Object.values(cart);
@@ -61,18 +83,62 @@ export default function Cart({ resources = [] }: CartProps) {
61
83
  },
62
84
  {} as Record<string, CartItemState[]>
63
85
  );
86
+ const orderedResourceIds = useMemo(() => {
87
+ const firstSeenIndex = new Map<string, number>();
88
+ displayableItems.forEach((item, index) => {
89
+ if (!firstSeenIndex.has(item.resourceId)) {
90
+ firstSeenIndex.set(item.resourceId, index);
91
+ }
92
+ });
93
+
94
+ return Object.keys(groupedItems).sort((a, b) => {
95
+ const categoryA = resources.find((r) => r.id === a)?.categorySlug;
96
+ const categoryB = resources.find((r) => r.id === b)?.categorySlug;
97
+ const priorityA = getCategoryPriority(categoryA);
98
+ const priorityB = getCategoryPriority(categoryB);
99
+ if (priorityA !== priorityB) {
100
+ return priorityA - priorityB;
101
+ }
102
+ return (
103
+ (firstSeenIndex.get(a) ?? Number.MAX_SAFE_INTEGER) -
104
+ (firstSeenIndex.get(b) ?? Number.MAX_SAFE_INTEGER)
105
+ );
106
+ });
107
+ }, [displayableItems, groupedItems, resources]);
64
108
 
65
109
  const hasService = cartValues.some((item) => {
66
110
  const resource = resources.find((r) => r.id === item.resourceId);
67
111
  return !!resource?.optionsPayload?.bookingLengthMinutes;
68
112
  });
69
113
 
114
+ const appointmentConstraints = useMemo(
115
+ () =>
116
+ deriveAppointmentConstraints(cart, resources, {
117
+ allowRemote,
118
+ remoteOnly,
119
+ }),
120
+ [cart, resources, allowRemote, remoteOnly]
121
+ );
122
+ const { effectiveRemoteOnly, remoteAvailable, inPersonAvailable, canRemote } =
123
+ appointmentConstraints;
124
+
70
125
  const hasPhysicalProductWithPickup = cartValues.some(
71
126
  (item) =>
72
127
  item.variantIdPickup && item.variantIdPickup !== item.variantIdShipped
73
128
  );
74
129
 
75
- const canPickup = hasService && hasPhysicalProductWithPickup;
130
+ const canPickup =
131
+ hasService && hasPhysicalProductWithPickup && appointmentMode !== 'REMOTE';
132
+
133
+ useEffect(() => {
134
+ if (effectiveRemoteOnly) {
135
+ preferredAppointmentMode.set('REMOTE');
136
+ return;
137
+ }
138
+ if (!canRemote && preferredAppointmentMode.get() === 'REMOTE') {
139
+ preferredAppointmentMode.set('IN_PERSON');
140
+ }
141
+ }, [effectiveRemoteOnly, canRemote]);
76
142
 
77
143
  useEffect(() => {
78
144
  if (canPickup) {
@@ -83,6 +149,10 @@ export default function Cart({ resources = [] }: CartProps) {
83
149
  }, [canPickup]);
84
150
 
85
151
  const isPickupMode = canPickup && pickupEnabled;
152
+ const productResources = resources.filter(
153
+ (r) => r.categorySlug === 'product'
154
+ );
155
+ const sharedFeeChargeLine = getSharedFeeChargeLineSummary(cart, resources);
86
156
 
87
157
  const dispatchAction = (item: CartItemState, action: 'add' | 'remove') => {
88
158
  addQueue.set([
@@ -131,26 +201,68 @@ export default function Cart({ resources = [] }: CartProps) {
131
201
  <div className="rounded-lg bg-white shadow">
132
202
  <div className="flex items-center justify-between border-b border-gray-200 px-6 py-4">
133
203
  <h2 className="text-xl font-bold text-gray-800">Shopping Cart</h2>
134
- {canPickup && (
135
- <label className="flex items-center space-x-2 text-sm font-bold text-gray-900">
136
- <input
137
- type="checkbox"
138
- checked={pickupEnabled}
139
- onChange={(e) => setPickupEnabled(e.target.checked)}
140
- className="h-4 w-4 rounded border-gray-300 text-black focus:ring-black"
141
- />
142
- <span>Pick up at Store</span>
143
- </label>
144
- )}
204
+ <div className="flex items-center gap-4">
205
+ {hasService && remoteAvailable && inPersonAvailable && (
206
+ <div className="flex flex-col items-end gap-1">
207
+ <p className="text-xs font-bold uppercase tracking-wide text-gray-500">
208
+ Appointment Mode
209
+ </p>
210
+ <div className="flex gap-2">
211
+ <button
212
+ type="button"
213
+ onClick={() => preferredAppointmentMode.set('IN_PERSON')}
214
+ className={`rounded-md px-3 py-2 text-sm font-bold ${
215
+ appointmentMode === 'IN_PERSON'
216
+ ? 'bg-black text-white'
217
+ : 'border border-gray-300 bg-white text-gray-700'
218
+ }`}
219
+ >
220
+ In Person
221
+ </button>
222
+ <button
223
+ type="button"
224
+ onClick={() => preferredAppointmentMode.set('REMOTE')}
225
+ className={`rounded-md px-3 py-2 text-sm font-bold ${
226
+ appointmentMode === 'REMOTE'
227
+ ? 'bg-black text-white'
228
+ : 'border border-gray-300 bg-white text-gray-700'
229
+ }`}
230
+ >
231
+ Remote
232
+ </button>
233
+ </div>
234
+ </div>
235
+ )}
236
+ {hasService && effectiveRemoteOnly && (
237
+ <p className="text-sm font-bold text-gray-900">
238
+ Appointment: Remote
239
+ </p>
240
+ )}
241
+ {canPickup && (
242
+ <label className="flex items-center space-x-2 text-sm font-bold text-gray-900">
243
+ <input
244
+ type="checkbox"
245
+ checked={pickupEnabled}
246
+ onChange={(e) => setPickupEnabled(e.target.checked)}
247
+ className="h-4 w-4 rounded border-gray-300 text-black focus:ring-black"
248
+ />
249
+ <span>Pick up at Store</span>
250
+ </label>
251
+ )}
252
+ </div>
145
253
  </div>
146
254
 
147
255
  <ul className="divide-y divide-gray-200">
148
- {Object.keys(groupedItems).map((resourceId) => {
256
+ {orderedResourceIds.map((resourceId) => {
149
257
  const items = groupedItems[resourceId];
150
258
  const resource = resources.find((r) => r.id === resourceId);
151
259
  if (!resource || items.length === 0) return null;
152
260
 
153
261
  const isService = !!resource.optionsPayload?.bookingLengthMinutes;
262
+ const sharedFeeService = isSharedFeeService(
263
+ resource,
264
+ productResources
265
+ );
154
266
  const serviceDuration = resource.optionsPayload?.bookingLengthMinutes;
155
267
 
156
268
  const firstItem = items[0];
@@ -162,10 +274,14 @@ export default function Cart({ resources = [] }: CartProps) {
162
274
  const activeVariantIdFirst = isPickupMode
163
275
  ? firstItem.variantIdPickup
164
276
  : firstItem.variantIdShipped;
277
+ const fallbackServiceVariantId = isService
278
+ ? getServiceVariantIdFromCanonicalProduct(resource, resources)
279
+ : undefined;
165
280
  const displayIdFirst =
166
281
  firstItem.variantId ||
167
282
  activeVariantIdFirst ||
168
- firstItem.variantIdPickup;
283
+ firstItem.variantIdPickup ||
284
+ fallbackServiceVariantId;
169
285
 
170
286
  const { src, srcSet } = getShopifyImage(
171
287
  resource,
@@ -173,14 +289,12 @@ export default function Cart({ resources = [] }: CartProps) {
173
289
  displayIdFirst
174
290
  );
175
291
 
176
- let productData: any = {};
177
- try {
178
- if (resource.optionsPayload?.shopifyData) {
179
- productData = JSON.parse(resource.optionsPayload.shopifyData);
180
- }
181
- } catch (e) {
182
- console.error('Failed to parse Shopify data', resource.id);
183
- }
292
+ const priceResource = isService
293
+ ? getServiceLinkedProduct(resource, resources) || resource
294
+ : resource;
295
+
296
+ const productData =
297
+ parsePrimaryShopifyProductData(priceResource) || {};
184
298
  const variants = productData?.variants || [];
185
299
 
186
300
  return (
@@ -197,7 +311,7 @@ export default function Cart({ resources = [] }: CartProps) {
197
311
  />
198
312
  </div>
199
313
  )}
200
- <div className="ml-4 flex-1">
314
+ <div className={`${isService ? '' : 'ml-4'} flex-1`}>
201
315
  <div className="flex justify-between">
202
316
  <div>
203
317
  <div className="flex items-center gap-2">
@@ -231,107 +345,132 @@ export default function Cart({ resources = [] }: CartProps) {
231
345
  {resource.oneliner}
232
346
  </p>
233
347
  </div>
348
+ {isService && sharedFeeService && (
349
+ <button
350
+ onClick={() =>
351
+ addQueue.set([
352
+ ...addQueue.get(),
353
+ {
354
+ resourceId: firstItem.resourceId,
355
+ action: 'remove',
356
+ variantId: firstItem.variantId,
357
+ },
358
+ ])
359
+ }
360
+ className="ml-4 rounded-md border border-gray-300 px-3 py-1 text-sm font-bold text-gray-600 hover:bg-gray-100"
361
+ >
362
+ Remove
363
+ </button>
364
+ )}
234
365
  </div>
235
366
 
236
- <div className="mt-4 space-y-4 border-t border-gray-100 pt-4">
237
- {items.map((item, idx) => {
238
- const activeVariantId = isPickupMode
239
- ? item.variantIdPickup
240
- : item.variantIdShipped;
241
-
242
- const displayId =
243
- item.variantId ||
244
- activeVariantId ||
245
- item.variantIdPickup;
246
-
247
- let price = '0.00';
248
- let currency = 'USD';
249
- let variantTitle = '';
250
-
251
- const variant = variants.find(
252
- (v: any) => v.id === displayId
253
- );
254
-
255
- if (variant) {
256
- price = variant.price?.amount || '0.00';
257
- currency = variant.price?.currencyCode || 'USD';
258
- variantTitle = getCleanVariantTitle(variant);
259
- }
260
-
261
- return (
262
- <div
263
- key={`${item.resourceId}_${displayId}_${idx}`}
264
- className="flex items-center justify-between"
265
- >
266
- <div className="flex items-center gap-2">
267
- {variantTitle && (
268
- <div className="text-sm font-bold text-gray-700">
269
- <span>{variantTitle}</span>
270
- </div>
271
- )}
272
- {isPickupMode &&
273
- !isService &&
274
- (item.variantIdPickup &&
275
- item.variantIdPickup !== item.variantIdShipped ? (
276
- <span className="inline-flex items-center rounded bg-gray-100 px-2 py-0.5 text-xs font-bold text-gray-800">
277
- Store Pickup
278
- </span>
279
- ) : (
280
- <span className="inline-flex items-center rounded bg-red-50 px-2 py-0.5 text-xs font-bold text-red-700">
281
- Not available for pickup
282
- </span>
283
- ))}
284
- </div>
285
-
286
- <div className="flex items-center">
287
- <div className="mr-6 text-right">
288
- <p className="text-sm font-bold text-gray-900">
289
- {price && parseFloat(price) > 0
290
- ? `${(parseFloat(price) * item.quantity).toFixed(2)} ${currency}`
291
- : 'No Charge'}
292
- </p>
367
+ {!(isService && sharedFeeService) && (
368
+ <div className="mt-4 space-y-4 border-t border-gray-100 pt-4">
369
+ {items.map((item, idx) => {
370
+ const activeVariantId = isPickupMode
371
+ ? item.variantIdPickup
372
+ : item.variantIdShipped;
373
+
374
+ const displayId =
375
+ item.variantId ||
376
+ activeVariantId ||
377
+ item.variantIdPickup ||
378
+ fallbackServiceVariantId;
379
+
380
+ let price = '0.00';
381
+ let currency = 'USD';
382
+ let variantTitle = '';
383
+
384
+ const variant = variants.find(
385
+ (v: any) => v.id === displayId
386
+ );
387
+
388
+ if (variant) {
389
+ price = variant.price?.amount || '0.00';
390
+ currency = variant.price?.currencyCode || 'USD';
391
+ variantTitle = getCleanVariantTitle(variant);
392
+ }
393
+
394
+ return (
395
+ <div
396
+ key={`${item.resourceId}_${displayId}_${idx}`}
397
+ className="flex items-center justify-between"
398
+ >
399
+ <div className="flex items-center gap-2">
400
+ {variantTitle && (
401
+ <div className="text-sm font-bold text-gray-700">
402
+ <span>{variantTitle}</span>
403
+ </div>
404
+ )}
405
+ {isPickupMode &&
406
+ !isService &&
407
+ (item.variantIdPickup &&
408
+ item.variantIdPickup !==
409
+ item.variantIdShipped ? (
410
+ <span className="inline-flex items-center rounded bg-gray-100 px-2 py-0.5 text-xs font-bold text-gray-800">
411
+ Store Pickup
412
+ </span>
413
+ ) : (
414
+ <span className="inline-flex items-center rounded bg-red-50 px-2 py-0.5 text-xs font-bold text-red-700">
415
+ Not available for pickup
416
+ </span>
417
+ ))}
293
418
  </div>
294
419
 
295
- {isService ? (
296
- <button
297
- onClick={() =>
298
- addQueue.set([
299
- ...addQueue.get(),
300
- {
301
- resourceId: item.resourceId,
302
- action: 'remove',
303
- variantId: item.variantId,
304
- },
305
- ])
306
- }
307
- className="rounded-md border border-gray-300 px-3 py-1 text-sm font-bold text-gray-600 hover:bg-gray-100"
308
- >
309
- Remove
310
- </button>
311
- ) : (
312
- <div className="flex items-center rounded-md border border-gray-300">
313
- <button
314
- onClick={() => dispatchAction(item, 'remove')}
315
- className="px-3 py-1 text-gray-600 hover:bg-gray-100"
316
- >
317
- -
318
- </button>
319
- <span className="border-l border-r border-gray-300 px-3 py-1 text-gray-900">
320
- {item.quantity}
321
- </span>
420
+ <div className="flex items-center">
421
+ <div className="mr-6 text-right">
422
+ <p className="text-sm font-bold text-gray-900">
423
+ {isService && sharedFeeService
424
+ ? ''
425
+ : price && parseFloat(price) > 0
426
+ ? `${(parseFloat(price) * item.quantity).toFixed(2)} ${currency}`
427
+ : 'No Charge'}
428
+ </p>
429
+ </div>
430
+
431
+ {isService ? (
322
432
  <button
323
- onClick={() => dispatchAction(item, 'add')}
324
- className="px-3 py-1 text-gray-600 hover:bg-gray-100"
433
+ onClick={() =>
434
+ addQueue.set([
435
+ ...addQueue.get(),
436
+ {
437
+ resourceId: item.resourceId,
438
+ action: 'remove',
439
+ variantId: item.variantId,
440
+ },
441
+ ])
442
+ }
443
+ className="rounded-md border border-gray-300 px-3 py-1 text-sm font-bold text-gray-600 hover:bg-gray-100"
325
444
  >
326
- +
445
+ Remove
327
446
  </button>
328
- </div>
329
- )}
447
+ ) : (
448
+ <div className="flex items-center rounded-md border border-gray-300">
449
+ <button
450
+ onClick={() =>
451
+ dispatchAction(item, 'remove')
452
+ }
453
+ className="px-3 py-1 text-gray-600 hover:bg-gray-100"
454
+ >
455
+ -
456
+ </button>
457
+ <span className="border-l border-r border-gray-300 px-3 py-1 text-gray-900">
458
+ {item.quantity}
459
+ </span>
460
+ <button
461
+ onClick={() => dispatchAction(item, 'add')}
462
+ className="px-3 py-1 text-gray-600 hover:bg-gray-100"
463
+ >
464
+ +
465
+ </button>
466
+ </div>
467
+ )}
468
+ </div>
330
469
  </div>
331
- </div>
332
- );
333
- })}
334
- </div>
470
+ );
471
+ })}
472
+ </div>
473
+ )}
335
474
  </div>
336
475
  </div>
337
476
  </li>
@@ -339,6 +478,28 @@ export default function Cart({ resources = [] }: CartProps) {
339
478
  })}
340
479
  </ul>
341
480
 
481
+ {sharedFeeChargeLine && (
482
+ <div className="border-t border-gray-200 bg-white px-6 py-4">
483
+ <div className="flex items-center justify-between">
484
+ <div>
485
+ <p className="text-sm font-bold text-gray-900">
486
+ {sharedFeeChargeLine.title}
487
+ </p>
488
+ {sharedFeeChargeLine.description && (
489
+ <p className="text-xs text-gray-500">
490
+ {sharedFeeChargeLine.description}
491
+ </p>
492
+ )}
493
+ </div>
494
+ <p className="text-sm font-bold text-gray-900">
495
+ {parseFloat(sharedFeeChargeLine.amount) > 0
496
+ ? `${parseFloat(sharedFeeChargeLine.amount).toFixed(2)} ${sharedFeeChargeLine.currencyCode}`
497
+ : 'No Charge'}
498
+ </p>
499
+ </div>
500
+ </div>
501
+ )}
502
+
342
503
  <div className="rounded-b-lg border-t border-gray-200 bg-gray-50 px-6 py-6">
343
504
  <div className="flex justify-end">
344
505
  <button
@@ -1,15 +1,15 @@
1
1
  import { useStore } from '@nanostores/react';
2
2
  import { cartStore } from '@/stores/shopify';
3
+ import { getCartIconCount } from '@/utils/customHelpers';
4
+ import type { ResourceNode } from '@/types/compositorTypes';
3
5
 
4
- export default function CartIcon() {
6
+ interface CartIconProps {
7
+ resources?: ResourceNode[];
8
+ }
9
+
10
+ export default function CartIcon({ resources = [] }: CartIconProps) {
5
11
  const cart = useStore(cartStore);
6
- const cartValues = Object.values(cart);
7
- const boundServiceIds = new Set(
8
- cartValues.map((item) => item.boundResourceId).filter(Boolean)
9
- );
10
- const totalQuantity = cartValues
11
- .filter((item) => !boundServiceIds.has(item.resourceId))
12
- .reduce((total, item) => total + item.quantity, 0);
12
+ const totalQuantity = getCartIconCount(cart, resources);
13
13
 
14
14
  const handleOpenCart = () => {
15
15
  window.location.href = '/cart';