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.
- package/bin/create-tractstack.js +7 -4
- package/dist/index.js +51 -8
- package/package.json +1 -1
- package/templates/custom/shopify/Cart.tsx +279 -118
- package/templates/custom/shopify/CartIcon.tsx +8 -8
- package/templates/custom/shopify/CheckoutModal.tsx +328 -65
- package/templates/custom/shopify/ShopifyCartManager.tsx +117 -60
- 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/custom/shopify/cart.astro +7 -1
- package/templates/src/components/Header.astro +4 -2
- 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 +249 -4
- package/templates/src/components/form/brand/SiteConfigSection.tsx +9 -0
- package/templates/src/components/form/shopify/SchedulingSection.tsx +44 -0
- package/templates/src/components/storykeep/Dashboard_Advanced.tsx +66 -21
- package/templates/src/components/storykeep/Dashboard_Shopify.tsx +266 -18
- package/templates/src/components/storykeep/controls/content/ManageContent.tsx +1 -0
- package/templates/src/components/storykeep/controls/content/ProductTable.tsx +38 -24
- package/templates/src/components/storykeep/controls/content/ResourceForm.tsx +240 -65
- package/templates/src/components/storykeep/shopify/ShopifyDashboard.tsx +175 -48
- package/templates/src/components/storykeep/shopify/ShopifyDashboard_Bookings.tsx +91 -10
- 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/constants.ts +2 -0
- package/templates/src/layouts/Layout.astro +26 -0
- package/templates/src/pages/api/auth/logout.ts +35 -2
- package/templates/src/pages/api/google/oauth/callback.ts +50 -0
- package/templates/src/pages/api/google/oauth/disconnect.ts +32 -0
- package/templates/src/pages/api/google/oauth/start.ts +32 -0
- package/templates/src/pages/api/google/oauth/status.ts +32 -0
- 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/privacy.astro +84 -0
- package/templates/src/pages/storykeep/advanced.astro +4 -1
- package/templates/src/pages/terms.astro +47 -0
- package/templates/src/stores/nodes.ts +8 -0
- package/templates/src/stores/shopify.ts +5 -0
- package/templates/src/types/tractstack.ts +87 -0
- package/templates/src/utils/api/advancedConfig.ts +2 -1
- package/templates/src/utils/api/advancedHelpers.ts +20 -0
- package/templates/src/utils/api/bookingHelpers.ts +3 -1
- package/templates/src/utils/api/brandConfig.ts +2 -0
- package/templates/src/utils/api/brandHelpers.ts +14 -1
- package/templates/src/utils/api/salesHelpers.ts +21 -0
- package/templates/src/utils/booking/appointmentMode.ts +135 -0
- package/templates/src/utils/customHelpers.ts +287 -2
- package/utils/inject-files.ts +47 -4
- package/templates/src/components/codehooks/SandboxAuthWrapper.tsx +0 -101
- package/templates/src/utils/actions/actionButton.ts +0 -103
- 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
|
-
|
|
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 =
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
<
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
className="
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
{
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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=
|
|
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
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
-
|
|
296
|
-
<
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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={() =>
|
|
324
|
-
|
|
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
|
-
|
|
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
|
-
|
|
332
|
-
)
|
|
333
|
-
|
|
334
|
-
|
|
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
|
-
|
|
6
|
+
interface CartIconProps {
|
|
7
|
+
resources?: ResourceNode[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export default function CartIcon({ resources = [] }: CartIconProps) {
|
|
5
11
|
const cart = useStore(cartStore);
|
|
6
|
-
const
|
|
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';
|