astro-tractstack 2.3.3 → 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.
- package/bin/create-tractstack.js +5 -2
- package/dist/index.js +18 -0
- package/package.json +1 -1
- package/templates/custom/shopify/Cart.tsx +196 -104
- package/templates/custom/shopify/CartIcon.tsx +8 -8
- package/templates/custom/shopify/CheckoutModal.tsx +143 -66
- package/templates/custom/shopify/ShopifyCartManager.tsx +64 -19
- package/templates/custom/shopify/ShopifyCheckout.tsx +2 -26
- package/templates/custom/shopify/ShopifyProductGrid.tsx +8 -0
- package/templates/custom/shopify/ShopifyServiceList.tsx +25 -37
- package/templates/src/components/Header.astro +1 -1
- package/templates/src/components/compositor/Node.tsx +39 -9
- package/templates/src/components/compositor/nodes/CreativePane.tsx +175 -88
- package/templates/src/components/edit/pane/AddPanePanel.tsx +2 -2
- package/templates/src/components/edit/pane/AddPanePanel_new.tsx +6 -5
- package/templates/src/components/edit/pane/AddPanePanel_reuse.tsx +9 -15
- package/templates/src/components/edit/pane/ConfigPanePanel.tsx +1 -1
- package/templates/src/components/form/advanced/APIConfigSection.tsx +35 -8
- package/templates/src/components/form/brand/SiteConfigSection.tsx +9 -0
- package/templates/src/components/storykeep/Dashboard_Advanced.tsx +59 -23
- package/templates/src/components/storykeep/Dashboard_Shopify.tsx +257 -18
- package/templates/src/components/storykeep/controls/content/ProductTable.tsx +38 -24
- package/templates/src/components/storykeep/controls/content/ResourceForm.tsx +161 -66
- package/templates/src/components/storykeep/shopify/ShopifyDashboard.tsx +175 -48
- package/templates/src/components/storykeep/shopify/ShopifyDashboard_Bookings.tsx +5 -2
- package/templates/src/components/storykeep/shopify/ShopifyDashboard_Sales.tsx +479 -0
- package/templates/src/components/storykeep/shopify/ShopifyDashboard_Search.tsx +7 -3
- package/templates/src/layouts/Layout.astro +26 -0
- package/templates/src/pages/api/auth/logout.ts +35 -2
- package/templates/src/pages/api/sales/list.ts +66 -0
- package/templates/src/pages/api/sales/metrics.ts +60 -0
- package/templates/src/pages/context/[...contextSlug].astro +50 -31
- package/templates/src/pages/storykeep/advanced.astro +4 -1
- package/templates/src/stores/nodes.ts +8 -0
- package/templates/src/types/tractstack.ts +57 -0
- package/templates/src/utils/api/advancedConfig.ts +2 -1
- package/templates/src/utils/api/advancedHelpers.ts +4 -0
- package/templates/src/utils/api/brandConfig.ts +2 -0
- package/templates/src/utils/api/brandHelpers.ts +6 -0
- package/templates/src/utils/api/salesHelpers.ts +21 -0
- package/templates/src/utils/customHelpers.ts +285 -2
- package/utils/inject-files.ts +18 -0
|
@@ -27,6 +27,15 @@ import {
|
|
|
27
27
|
} from '@/utils/booking/appointmentMode';
|
|
28
28
|
import { NativeBookingCalendar } from './NativeBookingCalendar';
|
|
29
29
|
import { ProfileStorage } from '@/utils/profileStorage';
|
|
30
|
+
import {
|
|
31
|
+
buildShopifyCheckoutLines,
|
|
32
|
+
getServiceLinkedProduct,
|
|
33
|
+
getServiceVariantIdFromCanonicalProduct,
|
|
34
|
+
getSharedFeeChargeLineSummary,
|
|
35
|
+
hasGidBackedCheckout,
|
|
36
|
+
isSharedFeeService,
|
|
37
|
+
parsePrimaryShopifyProductData,
|
|
38
|
+
} from '@/utils/customHelpers';
|
|
30
39
|
import type { ResourceNode } from '@/types/compositorTypes';
|
|
31
40
|
|
|
32
41
|
type CheckoutState =
|
|
@@ -102,21 +111,35 @@ export default function CheckoutModal({
|
|
|
102
111
|
const enrichedCart = useMemo(() => {
|
|
103
112
|
return Object.values($cartItems).map((item: any) => {
|
|
104
113
|
const resource = resources.find((r) => r.id === item.resourceId);
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
}
|
|
114
|
+
const productResources = resources.filter(
|
|
115
|
+
(r) => r.categorySlug === 'product'
|
|
116
|
+
);
|
|
117
|
+
const priceSource =
|
|
118
|
+
resource?.categorySlug === 'service'
|
|
119
|
+
? getServiceLinkedProduct(resource, resources) || resource
|
|
120
|
+
: resource;
|
|
121
|
+
const productData = parsePrimaryShopifyProductData(priceSource) || {};
|
|
122
|
+
const fallbackVariantId =
|
|
123
|
+
resource?.categorySlug === 'service'
|
|
124
|
+
? getServiceVariantIdFromCanonicalProduct(resource, resources)
|
|
125
|
+
: undefined;
|
|
126
|
+
const resolvedVariantId = item.variantId || fallbackVariantId;
|
|
113
127
|
const variant = (productData?.variants || []).find(
|
|
114
|
-
(v: any) => v.id ===
|
|
128
|
+
(v: any) => v.id === resolvedVariantId
|
|
115
129
|
);
|
|
130
|
+
const sharedFeeService =
|
|
131
|
+
resource?.categorySlug === 'service' &&
|
|
132
|
+
isSharedFeeService(resource, productResources);
|
|
133
|
+
const resolvedTitle =
|
|
134
|
+
resource?.categorySlug === 'service'
|
|
135
|
+
? resource.title
|
|
136
|
+
: productData?.title || resource?.title || 'Loading...';
|
|
116
137
|
return {
|
|
117
138
|
...item,
|
|
118
|
-
title:
|
|
139
|
+
title: resolvedTitle,
|
|
119
140
|
price: variant?.price?.amount || '0.00',
|
|
141
|
+
currencyCode: variant?.price?.currencyCode || 'USD',
|
|
142
|
+
sharedFeeService,
|
|
120
143
|
resourceNode: resource,
|
|
121
144
|
resource: {
|
|
122
145
|
id: item.resourceId,
|
|
@@ -134,9 +157,18 @@ export default function CheckoutModal({
|
|
|
134
157
|
() => enrichedCart.some((item) => item.resource?.needsBooking),
|
|
135
158
|
[enrichedCart]
|
|
136
159
|
);
|
|
160
|
+
const checkoutLines = useMemo(
|
|
161
|
+
() => buildShopifyCheckoutLines($cartItems, resources),
|
|
162
|
+
[$cartItems, resources]
|
|
163
|
+
);
|
|
164
|
+
const sharedFeeChargeLine = useMemo(
|
|
165
|
+
() => getSharedFeeChargeLineSummary($cartItems, resources),
|
|
166
|
+
[$cartItems, resources]
|
|
167
|
+
);
|
|
137
168
|
const needsPayment = useMemo(
|
|
138
|
-
() =>
|
|
139
|
-
|
|
169
|
+
() =>
|
|
170
|
+
hasGidBackedCheckout($cartItems, resources) || checkoutLines.length > 0,
|
|
171
|
+
[$cartItems, resources, checkoutLines]
|
|
140
172
|
);
|
|
141
173
|
const totalDuration = useMemo(() => {
|
|
142
174
|
const rawMinutes = enrichedCart.reduce(
|
|
@@ -201,6 +233,21 @@ export default function CheckoutModal({
|
|
|
201
233
|
}
|
|
202
234
|
}, [ProfileStorage.isProfileUnlocked(), $customer.email]);
|
|
203
235
|
|
|
236
|
+
useEffect(() => {
|
|
237
|
+
if (needsBooking) {
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
if (selectedSlot) {
|
|
241
|
+
setSelectedSlot(null);
|
|
242
|
+
}
|
|
243
|
+
if (shopTimeZone) {
|
|
244
|
+
setShopTimeZone(undefined);
|
|
245
|
+
}
|
|
246
|
+
if (internalState === 'BOOKING') {
|
|
247
|
+
setInternalState('SUMMARY');
|
|
248
|
+
}
|
|
249
|
+
}, [needsBooking, selectedSlot, shopTimeZone, internalState]);
|
|
250
|
+
|
|
204
251
|
useEffect(() => {
|
|
205
252
|
if (
|
|
206
253
|
$globalCartState !== CART_STATES.CHECKOUT ||
|
|
@@ -274,6 +321,8 @@ export default function CheckoutModal({
|
|
|
274
321
|
transactionTraceId.set('');
|
|
275
322
|
cartState.set(CART_STATES.READY);
|
|
276
323
|
setInternalState('IDENTITY_EMAIL');
|
|
324
|
+
setSelectedSlot(null);
|
|
325
|
+
setShopTimeZone(undefined);
|
|
277
326
|
setError(null);
|
|
278
327
|
if (redirect) window.location.href = `/`;
|
|
279
328
|
};
|
|
@@ -405,6 +454,8 @@ export default function CheckoutModal({
|
|
|
405
454
|
);
|
|
406
455
|
if (response && response.success) {
|
|
407
456
|
cartStore.set({});
|
|
457
|
+
setSelectedSlot(null);
|
|
458
|
+
setShopTimeZone(undefined);
|
|
408
459
|
setInternalState('SUCCESS');
|
|
409
460
|
} else {
|
|
410
461
|
setError(response?.error || 'Failed to confirm booking.');
|
|
@@ -420,53 +471,43 @@ export default function CheckoutModal({
|
|
|
420
471
|
setError(null);
|
|
421
472
|
setInternalState('PROCESSING');
|
|
422
473
|
try {
|
|
474
|
+
const lines = buildShopifyCheckoutLines(cartStore.get(), resources);
|
|
475
|
+
if (lines.length === 0) {
|
|
476
|
+
throw new Error('No checkout lines available.');
|
|
477
|
+
}
|
|
423
478
|
const response = await fetch('/api/shopify/createCart', {
|
|
424
479
|
method: 'POST',
|
|
425
480
|
headers: { 'Content-Type': 'application/json' },
|
|
426
481
|
body: JSON.stringify({
|
|
427
|
-
lines
|
|
428
|
-
.filter((i) => i.variantId)
|
|
429
|
-
.map((i) => ({
|
|
430
|
-
merchandiseId: i.variantId,
|
|
431
|
-
quantity: i.quantity || 1,
|
|
432
|
-
})),
|
|
482
|
+
lines,
|
|
433
483
|
email: $customer.email,
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
appointmentMode === 'REMOTE'
|
|
462
|
-
? 'Remote'
|
|
463
|
-
: 'In Person',
|
|
464
|
-
},
|
|
465
|
-
]
|
|
466
|
-
: []),
|
|
467
|
-
],
|
|
468
|
-
}
|
|
469
|
-
: {}),
|
|
484
|
+
attributes: [
|
|
485
|
+
{ key: 'bookingId', value: transactionTraceId.get() },
|
|
486
|
+
{ key: 'leadId', value: $customer.leadId },
|
|
487
|
+
...(needsBooking && selectedSlot
|
|
488
|
+
? [
|
|
489
|
+
{
|
|
490
|
+
key: 'Appointment Date',
|
|
491
|
+
value: selectedSlot.start.toLocaleDateString('en-US', {
|
|
492
|
+
timeZone: shopTimeZone,
|
|
493
|
+
}),
|
|
494
|
+
},
|
|
495
|
+
{
|
|
496
|
+
key: 'Appointment Time',
|
|
497
|
+
value: selectedSlot.start.toLocaleTimeString('en-US', {
|
|
498
|
+
hour: '2-digit',
|
|
499
|
+
minute: '2-digit',
|
|
500
|
+
timeZone: shopTimeZone,
|
|
501
|
+
}),
|
|
502
|
+
},
|
|
503
|
+
{
|
|
504
|
+
key: 'Appointment Mode',
|
|
505
|
+
value:
|
|
506
|
+
appointmentMode === 'REMOTE' ? 'Remote' : 'In Person',
|
|
507
|
+
},
|
|
508
|
+
]
|
|
509
|
+
: []),
|
|
510
|
+
],
|
|
470
511
|
}),
|
|
471
512
|
});
|
|
472
513
|
const result = await response.json();
|
|
@@ -476,6 +517,8 @@ export default function CheckoutModal({
|
|
|
476
517
|
isShopifyHandoff.set(true);
|
|
477
518
|
cartStore.set({});
|
|
478
519
|
cartState.set(CART_STATES.READY);
|
|
520
|
+
setSelectedSlot(null);
|
|
521
|
+
setShopTimeZone(undefined);
|
|
479
522
|
window.location.href = result.checkoutUrl;
|
|
480
523
|
} else {
|
|
481
524
|
throw new Error('No checkout URL');
|
|
@@ -654,21 +697,55 @@ export default function CheckoutModal({
|
|
|
654
697
|
<h3 className="mb-4 font-bold text-gray-900">
|
|
655
698
|
Order Summary
|
|
656
699
|
</h3>
|
|
657
|
-
{enrichedCart.map((item, idx) =>
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
700
|
+
{enrichedCart.map((item, idx) => {
|
|
701
|
+
if (item.sharedFeeService) {
|
|
702
|
+
return (
|
|
703
|
+
<div
|
|
704
|
+
key={idx}
|
|
705
|
+
className="flex justify-between text-sm text-gray-700"
|
|
706
|
+
>
|
|
707
|
+
<span>
|
|
708
|
+
{item.resourceNode?.title || item.title}
|
|
709
|
+
</span>
|
|
710
|
+
</div>
|
|
711
|
+
);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
return (
|
|
715
|
+
<div
|
|
716
|
+
key={idx}
|
|
717
|
+
className="flex justify-between text-sm text-gray-700"
|
|
718
|
+
>
|
|
719
|
+
<span>{item.title}</span>
|
|
720
|
+
<span className="font-bold">
|
|
721
|
+
$
|
|
722
|
+
{(
|
|
723
|
+
parseFloat(item.price) * (item.quantity || 1)
|
|
724
|
+
).toFixed(2)}
|
|
725
|
+
{item.currencyCode ? ` ${item.currencyCode}` : ''}
|
|
726
|
+
</span>
|
|
727
|
+
</div>
|
|
728
|
+
);
|
|
729
|
+
})}
|
|
730
|
+
{sharedFeeChargeLine && (
|
|
731
|
+
<div className="mt-2 flex justify-between border-t border-gray-200 pt-2 text-sm text-gray-700">
|
|
732
|
+
<div>
|
|
733
|
+
<span className="font-bold">
|
|
734
|
+
{sharedFeeChargeLine.title}
|
|
735
|
+
</span>
|
|
736
|
+
{sharedFeeChargeLine.description && (
|
|
737
|
+
<p className="text-xs text-gray-500">
|
|
738
|
+
{sharedFeeChargeLine.description}
|
|
739
|
+
</p>
|
|
740
|
+
)}
|
|
741
|
+
</div>
|
|
663
742
|
<span className="font-bold">
|
|
664
|
-
$
|
|
665
|
-
{
|
|
666
|
-
parseFloat(item.price) * (item.quantity || 1)
|
|
667
|
-
).toFixed(2)}
|
|
743
|
+
${parseFloat(sharedFeeChargeLine.amount).toFixed(2)}{' '}
|
|
744
|
+
{sharedFeeChargeLine.currencyCode}
|
|
668
745
|
</span>
|
|
669
746
|
</div>
|
|
670
|
-
)
|
|
671
|
-
{selectedSlot && (
|
|
747
|
+
)}
|
|
748
|
+
{needsBooking && selectedSlot && (
|
|
672
749
|
<div className="mt-6 border-t border-gray-200 pt-6">
|
|
673
750
|
<p className="text-xs font-bold uppercase text-gray-500">
|
|
674
751
|
Appointment
|
|
@@ -5,12 +5,13 @@ import {
|
|
|
5
5
|
cartStore,
|
|
6
6
|
modalState,
|
|
7
7
|
transactionTraceId,
|
|
8
|
-
getCartItemKey,
|
|
9
8
|
} from '@/stores/shopify';
|
|
10
9
|
import { bookingHelpers } from '@/utils/api/bookingHelpers';
|
|
11
10
|
import {
|
|
12
11
|
RESTRICTION_MESSAGES,
|
|
13
12
|
calculateCartDuration,
|
|
13
|
+
getCartItemKey,
|
|
14
|
+
isSharedFeeService,
|
|
14
15
|
} from '@/utils/customHelpers';
|
|
15
16
|
import { wouldCartHaveImpossibleRemoteMix } from '@/utils/booking/appointmentMode';
|
|
16
17
|
import type { ResourceNode } from '@/types/compositorTypes';
|
|
@@ -27,6 +28,9 @@ export default function ShopifyCartManager({
|
|
|
27
28
|
brandConfig,
|
|
28
29
|
}: ShopifyCartManagerProps) {
|
|
29
30
|
const queue = useStore(addQueue);
|
|
31
|
+
const productResources = resources.filter(
|
|
32
|
+
(r) => r.categorySlug === 'product'
|
|
33
|
+
);
|
|
30
34
|
|
|
31
35
|
useEffect(() => {
|
|
32
36
|
if (queue.length > 0) {
|
|
@@ -39,19 +43,37 @@ export default function ShopifyCartManager({
|
|
|
39
43
|
return;
|
|
40
44
|
}
|
|
41
45
|
|
|
42
|
-
const key = getCartItemKey(actionItem);
|
|
46
|
+
const key = getCartItemKey(actionItem, resource, productResources);
|
|
43
47
|
const currentCart = cartStore.get();
|
|
44
48
|
const currentItem = currentCart[key];
|
|
45
|
-
const
|
|
49
|
+
const isSharedFeeResource = isSharedFeeService(
|
|
50
|
+
resource,
|
|
51
|
+
productResources
|
|
52
|
+
);
|
|
53
|
+
const legacySharedKeys = isSharedFeeResource
|
|
54
|
+
? Object.keys(currentCart).filter(
|
|
55
|
+
(cartKey) =>
|
|
56
|
+
cartKey !== key &&
|
|
57
|
+
currentCart[cartKey]?.resourceId === actionItem.resourceId
|
|
58
|
+
)
|
|
59
|
+
: [];
|
|
60
|
+
const legacySharedItems = legacySharedKeys
|
|
61
|
+
.map((cartKey) => currentCart[cartKey])
|
|
62
|
+
.filter((item): item is CartItemState => !!item);
|
|
63
|
+
const mergedCurrentItem = currentItem || legacySharedItems[0];
|
|
64
|
+
const currentQty = isSharedFeeResource
|
|
65
|
+
? currentItem?.quantity ||
|
|
66
|
+
legacySharedItems.reduce((total, item) => total + item.quantity, 0)
|
|
67
|
+
: currentItem?.quantity || 0;
|
|
46
68
|
const nextCart = { ...currentCart };
|
|
47
69
|
|
|
48
70
|
if (actionItem.action === 'remove') {
|
|
49
|
-
const newQty = Math.max(0, currentQty - 1);
|
|
71
|
+
const newQty = isSharedFeeResource ? 0 : Math.max(0, currentQty - 1);
|
|
50
72
|
|
|
51
73
|
if (newQty === 0) {
|
|
52
74
|
if (
|
|
53
75
|
resource?.optionsPayload?.needsBooking ||
|
|
54
|
-
|
|
76
|
+
mergedCurrentItem?.boundResourceId
|
|
55
77
|
) {
|
|
56
78
|
const traceId = transactionTraceId.get();
|
|
57
79
|
if (traceId) {
|
|
@@ -59,23 +81,34 @@ export default function ShopifyCartManager({
|
|
|
59
81
|
.releaseHold(traceId)
|
|
60
82
|
.catch((err) =>
|
|
61
83
|
console.error('Failed to release hold on cart removal:', err)
|
|
62
|
-
)
|
|
84
|
+
)
|
|
85
|
+
.finally(() => {
|
|
86
|
+
transactionTraceId.set('');
|
|
87
|
+
});
|
|
63
88
|
}
|
|
64
89
|
}
|
|
65
90
|
delete nextCart[key];
|
|
91
|
+
legacySharedKeys.forEach((legacyKey) => {
|
|
92
|
+
delete nextCart[legacyKey];
|
|
93
|
+
});
|
|
66
94
|
} else {
|
|
67
95
|
nextCart[key] = {
|
|
68
|
-
...
|
|
96
|
+
...mergedCurrentItem,
|
|
69
97
|
resourceId: actionItem.resourceId,
|
|
70
98
|
quantity: newQty,
|
|
71
99
|
};
|
|
72
100
|
}
|
|
73
101
|
|
|
74
|
-
if (
|
|
102
|
+
if (mergedCurrentItem?.boundResourceId || actionItem.boundResourceId) {
|
|
75
103
|
const boundId =
|
|
76
|
-
|
|
104
|
+
mergedCurrentItem?.boundResourceId || actionItem.boundResourceId;
|
|
77
105
|
if (boundId) {
|
|
78
|
-
const
|
|
106
|
+
const boundResource = resources.find((r) => r.id === boundId);
|
|
107
|
+
const serviceKey = getCartItemKey(
|
|
108
|
+
{ resourceId: boundId },
|
|
109
|
+
boundResource,
|
|
110
|
+
productResources
|
|
111
|
+
);
|
|
79
112
|
const serviceItem = nextCart[serviceKey];
|
|
80
113
|
if (serviceItem) {
|
|
81
114
|
const newServiceQty = Math.max(0, serviceItem.quantity - 1);
|
|
@@ -95,27 +128,39 @@ export default function ShopifyCartManager({
|
|
|
95
128
|
addQueue.set(remaining);
|
|
96
129
|
} else if (actionItem.action === 'add') {
|
|
97
130
|
transactionTraceId.set('');
|
|
98
|
-
const newQty = currentQty + 1;
|
|
131
|
+
const newQty = isSharedFeeResource ? 1 : currentQty + 1;
|
|
99
132
|
|
|
100
133
|
const newItem: CartItemState = {
|
|
101
134
|
resourceId: actionItem.resourceId,
|
|
102
135
|
quantity: newQty,
|
|
103
|
-
gid: actionItem.gid ||
|
|
104
|
-
variantId:
|
|
136
|
+
gid: actionItem.gid || mergedCurrentItem?.gid,
|
|
137
|
+
variantId: isSharedFeeResource
|
|
138
|
+
? undefined
|
|
139
|
+
: actionItem.variantId || mergedCurrentItem?.variantId,
|
|
105
140
|
variantIdShipped:
|
|
106
|
-
actionItem.variantIdShipped ||
|
|
141
|
+
actionItem.variantIdShipped || mergedCurrentItem?.variantIdShipped,
|
|
107
142
|
variantIdPickup:
|
|
108
|
-
actionItem.variantIdPickup ||
|
|
143
|
+
actionItem.variantIdPickup || mergedCurrentItem?.variantIdPickup,
|
|
109
144
|
boundResourceId:
|
|
110
|
-
actionItem.boundResourceId ||
|
|
145
|
+
actionItem.boundResourceId || mergedCurrentItem?.boundResourceId,
|
|
111
146
|
};
|
|
112
147
|
|
|
148
|
+
legacySharedKeys.forEach((legacyKey) => {
|
|
149
|
+
delete nextCart[legacyKey];
|
|
150
|
+
});
|
|
113
151
|
nextCart[key] = newItem;
|
|
114
152
|
|
|
115
153
|
if (newItem.boundResourceId) {
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
154
|
+
const boundResource = resources.find(
|
|
155
|
+
(r) => r.id === newItem.boundResourceId
|
|
156
|
+
);
|
|
157
|
+
const serviceKey = getCartItemKey(
|
|
158
|
+
{
|
|
159
|
+
resourceId: newItem.boundResourceId,
|
|
160
|
+
},
|
|
161
|
+
boundResource,
|
|
162
|
+
productResources
|
|
163
|
+
);
|
|
119
164
|
const serviceItem = nextCart[serviceKey];
|
|
120
165
|
|
|
121
166
|
if (serviceItem) {
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
CART_STATES,
|
|
7
7
|
isShopifyHandoff,
|
|
8
8
|
} from '@/stores/shopify';
|
|
9
|
+
import { buildShopifyCheckoutLines } from '@/utils/customHelpers';
|
|
9
10
|
import type { ResourceNode } from '@/types/compositorTypes';
|
|
10
11
|
|
|
11
12
|
interface ShopifyCheckoutProps {
|
|
@@ -33,32 +34,7 @@ export default function ShopifyCheckout({
|
|
|
33
34
|
setStatus('PROCESSING');
|
|
34
35
|
|
|
35
36
|
try {
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
const lines = cartItems
|
|
39
|
-
.map((item) => {
|
|
40
|
-
// Resolve the ResourceNode for this item
|
|
41
|
-
const resource = resources.find((r) => r.id === item.resourceId);
|
|
42
|
-
|
|
43
|
-
if (!resource) {
|
|
44
|
-
return null;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Use the explicitly sanitized variant ID from the cart state
|
|
48
|
-
const merchandiseId = item.variantId;
|
|
49
|
-
|
|
50
|
-
// If we have no ID, we cannot add this item to the Shopify cart.
|
|
51
|
-
if (!merchandiseId) return null;
|
|
52
|
-
|
|
53
|
-
return {
|
|
54
|
-
merchandiseId,
|
|
55
|
-
quantity: item.quantity,
|
|
56
|
-
};
|
|
57
|
-
})
|
|
58
|
-
.filter((line) => line !== null) as Array<{
|
|
59
|
-
merchandiseId: string;
|
|
60
|
-
quantity: number;
|
|
61
|
-
}>;
|
|
37
|
+
const lines = buildShopifyCheckoutLines(cart, resources);
|
|
62
38
|
|
|
63
39
|
if (lines.length === 0) {
|
|
64
40
|
throw new Error(
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useState } from 'react';
|
|
2
2
|
import { useStore } from '@nanostores/react';
|
|
3
3
|
import { cartStore, addQueue, type CartAction } from '@/stores/shopify';
|
|
4
|
+
import { collectServiceGids } from '@/utils/customHelpers';
|
|
4
5
|
import { getShopifyImage } from '@/utils/helpers';
|
|
5
6
|
import type { ResourceNode } from '@/types/compositorTypes';
|
|
6
7
|
|
|
@@ -240,6 +241,13 @@ export default function ShopifyProductGrid({ resources = {}, options }: Props) {
|
|
|
240
241
|
products = products.filter((p) => p.optionsPayload?.group === group);
|
|
241
242
|
}
|
|
242
243
|
|
|
244
|
+
const serviceGids = collectServiceGids(services);
|
|
245
|
+
products = products.filter((p) => {
|
|
246
|
+
const gid =
|
|
247
|
+
typeof p.optionsPayload?.gid === 'string' ? p.optionsPayload.gid : '';
|
|
248
|
+
return gid === '' || !serviceGids.has(gid);
|
|
249
|
+
});
|
|
250
|
+
|
|
243
251
|
if (products.length === 0) return null;
|
|
244
252
|
|
|
245
253
|
return (
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { useStore } from '@nanostores/react';
|
|
2
|
+
import { cartStore, addQueue, type CartAction } from '@/stores/shopify';
|
|
2
3
|
import {
|
|
3
|
-
cartStore,
|
|
4
|
-
addQueue,
|
|
5
4
|
getCartItemKey,
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
getServiceVariantIdFromCanonicalProduct,
|
|
6
|
+
isSharedFeeService,
|
|
7
|
+
} from '@/utils/customHelpers';
|
|
8
8
|
import type { ResourceNode } from '@/types/compositorTypes';
|
|
9
9
|
|
|
10
10
|
interface Props {
|
|
@@ -55,33 +55,17 @@ export default function ShopifyServiceList({ resources = {}, options }: Props) {
|
|
|
55
55
|
(s) => !boundServiceSlugs.has(s.slug)
|
|
56
56
|
);
|
|
57
57
|
|
|
58
|
-
const getServiceVariantId = (resource: ResourceNode): string | undefined => {
|
|
59
|
-
try {
|
|
60
|
-
if (resource.optionsPayload?.shopifyData) {
|
|
61
|
-
const data = JSON.parse(resource.optionsPayload.shopifyData);
|
|
62
|
-
// Handle both raw product data and simplified product objects
|
|
63
|
-
const product = data.products?.[0] || data;
|
|
64
|
-
return product?.variants?.[0]?.id;
|
|
65
|
-
}
|
|
66
|
-
} catch (e) {
|
|
67
|
-
return undefined;
|
|
68
|
-
}
|
|
69
|
-
return undefined;
|
|
70
|
-
};
|
|
71
|
-
|
|
72
58
|
const handleToggle = (resource: ResourceNode, currentQuantity: number) => {
|
|
73
59
|
const actionType = currentQuantity > 0 ? 'remove' : 'add';
|
|
60
|
+
const gid =
|
|
61
|
+
typeof resource.optionsPayload?.gid === 'string'
|
|
62
|
+
? resource.optionsPayload.gid
|
|
63
|
+
: undefined;
|
|
74
64
|
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
if (resource.optionsPayload?.shopifyData) {
|
|
80
|
-
const data = JSON.parse(resource.optionsPayload.shopifyData);
|
|
81
|
-
const product = data.products?.[0] || data;
|
|
82
|
-
gid = product?.id;
|
|
83
|
-
}
|
|
84
|
-
} catch (e) {}
|
|
65
|
+
const sharedFee = isSharedFeeService(resource, products);
|
|
66
|
+
const variantId = sharedFee
|
|
67
|
+
? undefined
|
|
68
|
+
: getServiceVariantIdFromCanonicalProduct(resource, products);
|
|
85
69
|
|
|
86
70
|
const newAction: CartAction = {
|
|
87
71
|
resourceId: resource.id,
|
|
@@ -115,14 +99,20 @@ export default function ShopifyServiceList({ resources = {}, options }: Props) {
|
|
|
115
99
|
<section className="w-full">
|
|
116
100
|
<div className="space-y-4">
|
|
117
101
|
{displayServices.map((resource) => {
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
102
|
+
const key = getCartItemKey(
|
|
103
|
+
{ resourceId: resource.id },
|
|
104
|
+
resource,
|
|
105
|
+
products
|
|
106
|
+
);
|
|
123
107
|
|
|
124
108
|
const cartItem = cart[key];
|
|
125
|
-
const
|
|
109
|
+
const legacyCartItem = Object.values(cart).find(
|
|
110
|
+
(item) =>
|
|
111
|
+
item.resourceId === resource.id && (item.quantity || 0) > 0
|
|
112
|
+
);
|
|
113
|
+
const selectedQuantity =
|
|
114
|
+
cartItem?.quantity || legacyCartItem?.quantity || 0;
|
|
115
|
+
const isSelected = selectedQuantity > 0;
|
|
126
116
|
const duration = resource.optionsPayload?.bookingLengthMinutes;
|
|
127
117
|
|
|
128
118
|
return (
|
|
@@ -152,9 +142,7 @@ export default function ShopifyServiceList({ resources = {}, options }: Props) {
|
|
|
152
142
|
|
|
153
143
|
<div className="ml-4 flex-shrink-0">
|
|
154
144
|
<button
|
|
155
|
-
onClick={() =>
|
|
156
|
-
handleToggle(resource, cartItem?.quantity || 0)
|
|
157
|
-
}
|
|
145
|
+
onClick={() => handleToggle(resource, selectedQuantity)}
|
|
158
146
|
className={`relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none ${
|
|
159
147
|
isSelected ? 'bg-black' : 'bg-gray-200'
|
|
160
148
|
}`}
|