astro-tractstack 2.3.3 → 2.3.5
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 +32 -4
- package/package.json +1 -1
- package/templates/custom/customHelpers.ts +45 -0
- package/templates/custom/shopify/Cart.tsx +197 -105
- package/templates/custom/shopify/CartIcon.tsx +8 -8
- package/templates/custom/shopify/CheckoutModal.tsx +145 -68
- package/templates/custom/shopify/ShopifyCartManager.tsx +67 -22
- 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/shopifyCustomHelper.ts +10 -0
- package/templates/custom/shopify/shopifyHelpers.ts +298 -0
- package/templates/src/components/Header.astro +2 -2
- package/templates/src/components/codehooks/SearchWidget.tsx +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/search/SearchResults.tsx +1 -1
- package/templates/src/components/search/SearchWrapper.tsx +1 -1
- 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 +162 -67
- 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/utils/inject-files.ts +32 -4
- package/templates/src/utils/customHelpers.ts +0 -89
- /package/templates/{src/utils/booking → custom/shopify}/appointmentMode.ts +0 -0
package/bin/create-tractstack.js
CHANGED
|
@@ -13,6 +13,9 @@ import { execSync } from 'child_process';
|
|
|
13
13
|
import prompts from 'prompts';
|
|
14
14
|
import kleur from 'kleur';
|
|
15
15
|
|
|
16
|
+
// Keep in sync with package.json pnpm.overrides["@internationalized/date"]
|
|
17
|
+
const INTL_DATE_VERSION = '3.10.1';
|
|
18
|
+
|
|
16
19
|
// Detect package manager
|
|
17
20
|
function detectPackageManager() {
|
|
18
21
|
if (existsSync('pnpm-lock.yaml')) return 'pnpm';
|
|
@@ -348,7 +351,7 @@ PUBLIC_ENABLE_BUNNY="${finalResponses.enableBunny ? 'true' : 'false'}"
|
|
|
348
351
|
|
|
349
352
|
// Install UI components
|
|
350
353
|
execSync(
|
|
351
|
-
`${addCommand} @ark-ui/react@^5.30.0 @heroicons/react@^2.1.1 @internationalized/date
|
|
354
|
+
`${addCommand} @ark-ui/react@^5.30.0 @heroicons/react@^2.1.1 @internationalized/date@${INTL_DATE_VERSION}`,
|
|
352
355
|
{
|
|
353
356
|
stdio: 'inherit',
|
|
354
357
|
}
|
|
@@ -382,7 +385,7 @@ PUBLIC_ENABLE_BUNNY="${finalResponses.enableBunny ? 'true' : 'false'}"
|
|
|
382
385
|
console.log('Please run manually:');
|
|
383
386
|
console.log(
|
|
384
387
|
kleur.cyan(
|
|
385
|
-
`${addCommand} react@^19.0.0 react-dom@^19.0.0 astro@^5.16.6 @astrojs/react@^4.4.2 @astrojs/node@^9.4.3 @nanostores/react@^1.0.0 nanostores@^1.0.1 @nanostores/persistent ulid@^3.0.1 @ark-ui/react@^5.30.0 @heroicons/react@^2.1.1 @internationalized/date
|
|
388
|
+
`${addCommand} react@^19.0.0 react-dom@^19.0.0 astro@^5.16.6 @astrojs/react@^4.4.2 @astrojs/node@^9.4.3 @nanostores/react@^1.0.0 nanostores@^1.0.1 @nanostores/persistent ulid@^3.0.1 @ark-ui/react@^5.30.0 @heroicons/react@^2.1.1 @internationalized/date@${INTL_DATE_VERSION} d3@^7.9.0 d3-sankey@^0.12.3 recharts@^3.1.2 player.js@^0.1.0 tinycolor2@1.6.0 html-to-image@^1.11.13 path-to-regexp@^8.0.0 postcss postcss-selector-parser`
|
|
386
389
|
)
|
|
387
390
|
);
|
|
388
391
|
console.log(
|
package/dist/index.js
CHANGED
|
@@ -815,6 +815,10 @@ async function y(t, e, c) {
|
|
|
815
815
|
src: t("../templates/src/utils/api/bookingHelpers.ts"),
|
|
816
816
|
dest: "src/utils/api/bookingHelpers.ts"
|
|
817
817
|
},
|
|
818
|
+
{
|
|
819
|
+
src: t("../templates/src/utils/api/salesHelpers.ts"),
|
|
820
|
+
dest: "src/utils/api/salesHelpers.ts"
|
|
821
|
+
},
|
|
818
822
|
{
|
|
819
823
|
src: t("../templates/src/utils/api/menuHelpers.ts"),
|
|
820
824
|
dest: "src/utils/api/menuHelpers.ts"
|
|
@@ -949,6 +953,14 @@ async function y(t, e, c) {
|
|
|
949
953
|
src: t("../templates/src/pages/api/booking/list.ts"),
|
|
950
954
|
dest: "src/pages/api/booking/list.ts"
|
|
951
955
|
},
|
|
956
|
+
{
|
|
957
|
+
src: t("../templates/src/pages/api/sales/list.ts"),
|
|
958
|
+
dest: "src/pages/api/sales/list.ts"
|
|
959
|
+
},
|
|
960
|
+
{
|
|
961
|
+
src: t("../templates/src/pages/api/sales/metrics.ts"),
|
|
962
|
+
dest: "src/pages/api/sales/metrics.ts"
|
|
963
|
+
},
|
|
952
964
|
{
|
|
953
965
|
src: t("../templates/src/pages/api/booking/metrics.ts"),
|
|
954
966
|
dest: "src/pages/api/booking/metrics.ts"
|
|
@@ -1268,6 +1280,12 @@ async function y(t, e, c) {
|
|
|
1268
1280
|
),
|
|
1269
1281
|
dest: "src/components/storykeep/shopify/ShopifyDashboard_Emails.tsx"
|
|
1270
1282
|
},
|
|
1283
|
+
{
|
|
1284
|
+
src: t(
|
|
1285
|
+
"../templates/src/components/storykeep/shopify/ShopifyDashboard_Sales.tsx"
|
|
1286
|
+
),
|
|
1287
|
+
dest: "src/components/storykeep/shopify/ShopifyDashboard_Sales.tsx"
|
|
1288
|
+
},
|
|
1271
1289
|
{
|
|
1272
1290
|
src: t(
|
|
1273
1291
|
"../templates/src/components/storykeep/email-builder/EmailBuilder.tsx"
|
|
@@ -2323,13 +2341,23 @@ async function y(t, e, c) {
|
|
|
2323
2341
|
protected: !0
|
|
2324
2342
|
},
|
|
2325
2343
|
{
|
|
2326
|
-
src: t("../templates/
|
|
2327
|
-
dest: "src/
|
|
2344
|
+
src: t("../templates/custom/customHelpers.ts"),
|
|
2345
|
+
dest: "src/custom/customHelpers.ts",
|
|
2346
|
+
protected: !0
|
|
2347
|
+
},
|
|
2348
|
+
{
|
|
2349
|
+
src: t("../templates/custom/shopify/shopifyCustomHelper.ts"),
|
|
2350
|
+
dest: "src/custom/shopify/shopifyCustomHelper.ts",
|
|
2351
|
+
protected: !0
|
|
2352
|
+
},
|
|
2353
|
+
{
|
|
2354
|
+
src: t("../templates/custom/shopify/shopifyHelpers.ts"),
|
|
2355
|
+
dest: "src/custom/shopify/shopifyHelpers.ts",
|
|
2328
2356
|
protected: !0
|
|
2329
2357
|
},
|
|
2330
2358
|
{
|
|
2331
|
-
src: t("../templates/
|
|
2332
|
-
dest: "src/
|
|
2359
|
+
src: t("../templates/custom/shopify/appointmentMode.ts"),
|
|
2360
|
+
dest: "src/custom/shopify/appointmentMode.ts",
|
|
2333
2361
|
protected: !0
|
|
2334
2362
|
},
|
|
2335
2363
|
{
|
package/package.json
CHANGED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// URL Helper: Strip category prefix from slug
|
|
2
|
+
// e.g., "people-bleako" -> "bleako"
|
|
3
|
+
export function getCleanSlug(categorySlug: string, fullSlug: string): string {
|
|
4
|
+
const prefix = `${categorySlug}-`;
|
|
5
|
+
return fullSlug.startsWith(prefix) ? fullSlug.slice(prefix.length) : fullSlug;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// Build proper URL for resource
|
|
9
|
+
// e.g., category="people", slug="people-bleako" -> "/people/bleako"
|
|
10
|
+
export function getResourceUrl(categorySlug: string, fullSlug: string): string {
|
|
11
|
+
const cleanSlug = getCleanSlug(categorySlug, fullSlug);
|
|
12
|
+
return `/${categorySlug}/${cleanSlug}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Image Helper: Placeholder implementation
|
|
16
|
+
export function getResourceImage(
|
|
17
|
+
id: string,
|
|
18
|
+
slug: string,
|
|
19
|
+
category: string
|
|
20
|
+
): string {
|
|
21
|
+
console.log(`please define getResourceImage`, id, slug, category);
|
|
22
|
+
return '/static.jpg';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function getResourceDescription(
|
|
26
|
+
id: string,
|
|
27
|
+
slug: string,
|
|
28
|
+
category: string
|
|
29
|
+
): string | null {
|
|
30
|
+
console.log(`please define getResourceDescription`, id, slug, category);
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Initialize search data - override in custom implementation
|
|
35
|
+
export function initSearch(): void {
|
|
36
|
+
// Default implementation does nothing
|
|
37
|
+
// Override this function in your custom implementation to load search data
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Field Visibility Controls for ResourceForm
|
|
41
|
+
export const resourceFormHideFields = ['shopifyImage'];
|
|
42
|
+
|
|
43
|
+
// Field Formatting Controls for ResourceForm
|
|
44
|
+
// Fields listed here will be treated as JSON objects but rendered as stringified text areas
|
|
45
|
+
export const resourceJsonifyFields = ['shopifyData', 'shopifyImage'];
|
|
@@ -12,7 +12,14 @@ import {
|
|
|
12
12
|
type CartItemState,
|
|
13
13
|
} from '@/stores/shopify';
|
|
14
14
|
import { getShopifyImage } from '@/utils/helpers';
|
|
15
|
-
import { deriveAppointmentConstraints } from '@/
|
|
15
|
+
import { deriveAppointmentConstraints } from '@/custom/shopify/appointmentMode';
|
|
16
|
+
import {
|
|
17
|
+
getServiceLinkedProduct,
|
|
18
|
+
getServiceVariantIdFromCanonicalProduct,
|
|
19
|
+
getSharedFeeChargeLineSummary,
|
|
20
|
+
isSharedFeeService,
|
|
21
|
+
parsePrimaryShopifyProductData,
|
|
22
|
+
} from '@/custom/shopify/shopifyHelpers';
|
|
16
23
|
import type { ResourceNode } from '@/types/compositorTypes';
|
|
17
24
|
|
|
18
25
|
interface CartProps {
|
|
@@ -38,6 +45,12 @@ const getCleanVariantTitle = (variant: any) => {
|
|
|
38
45
|
return title === 'Default Title' ? '' : title;
|
|
39
46
|
};
|
|
40
47
|
|
|
48
|
+
const getCategoryPriority = (categorySlug?: string): number => {
|
|
49
|
+
if (categorySlug === 'product') return 0;
|
|
50
|
+
if (categorySlug === 'service') return 1;
|
|
51
|
+
return 2;
|
|
52
|
+
};
|
|
53
|
+
|
|
41
54
|
export default function Cart({
|
|
42
55
|
resources = [],
|
|
43
56
|
allowRemote = false,
|
|
@@ -70,6 +83,28 @@ export default function Cart({
|
|
|
70
83
|
},
|
|
71
84
|
{} as Record<string, CartItemState[]>
|
|
72
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]);
|
|
73
108
|
|
|
74
109
|
const hasService = cartValues.some((item) => {
|
|
75
110
|
const resource = resources.find((r) => r.id === item.resourceId);
|
|
@@ -114,6 +149,10 @@ export default function Cart({
|
|
|
114
149
|
}, [canPickup]);
|
|
115
150
|
|
|
116
151
|
const isPickupMode = canPickup && pickupEnabled;
|
|
152
|
+
const productResources = resources.filter(
|
|
153
|
+
(r) => r.categorySlug === 'product'
|
|
154
|
+
);
|
|
155
|
+
const sharedFeeChargeLine = getSharedFeeChargeLineSummary(cart, resources);
|
|
117
156
|
|
|
118
157
|
const dispatchAction = (item: CartItemState, action: 'add' | 'remove') => {
|
|
119
158
|
addQueue.set([
|
|
@@ -214,12 +253,16 @@ export default function Cart({
|
|
|
214
253
|
</div>
|
|
215
254
|
|
|
216
255
|
<ul className="divide-y divide-gray-200">
|
|
217
|
-
{
|
|
256
|
+
{orderedResourceIds.map((resourceId) => {
|
|
218
257
|
const items = groupedItems[resourceId];
|
|
219
258
|
const resource = resources.find((r) => r.id === resourceId);
|
|
220
259
|
if (!resource || items.length === 0) return null;
|
|
221
260
|
|
|
222
261
|
const isService = !!resource.optionsPayload?.bookingLengthMinutes;
|
|
262
|
+
const sharedFeeService = isSharedFeeService(
|
|
263
|
+
resource,
|
|
264
|
+
productResources
|
|
265
|
+
);
|
|
223
266
|
const serviceDuration = resource.optionsPayload?.bookingLengthMinutes;
|
|
224
267
|
|
|
225
268
|
const firstItem = items[0];
|
|
@@ -231,10 +274,14 @@ export default function Cart({
|
|
|
231
274
|
const activeVariantIdFirst = isPickupMode
|
|
232
275
|
? firstItem.variantIdPickup
|
|
233
276
|
: firstItem.variantIdShipped;
|
|
277
|
+
const fallbackServiceVariantId = isService
|
|
278
|
+
? getServiceVariantIdFromCanonicalProduct(resource, resources)
|
|
279
|
+
: undefined;
|
|
234
280
|
const displayIdFirst =
|
|
235
281
|
firstItem.variantId ||
|
|
236
282
|
activeVariantIdFirst ||
|
|
237
|
-
firstItem.variantIdPickup
|
|
283
|
+
firstItem.variantIdPickup ||
|
|
284
|
+
fallbackServiceVariantId;
|
|
238
285
|
|
|
239
286
|
const { src, srcSet } = getShopifyImage(
|
|
240
287
|
resource,
|
|
@@ -242,14 +289,12 @@ export default function Cart({
|
|
|
242
289
|
displayIdFirst
|
|
243
290
|
);
|
|
244
291
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
console.error('Failed to parse Shopify data', resource.id);
|
|
252
|
-
}
|
|
292
|
+
const priceResource = isService
|
|
293
|
+
? getServiceLinkedProduct(resource, resources) || resource
|
|
294
|
+
: resource;
|
|
295
|
+
|
|
296
|
+
const productData =
|
|
297
|
+
parsePrimaryShopifyProductData(priceResource) || {};
|
|
253
298
|
const variants = productData?.variants || [];
|
|
254
299
|
|
|
255
300
|
return (
|
|
@@ -266,7 +311,7 @@ export default function Cart({
|
|
|
266
311
|
/>
|
|
267
312
|
</div>
|
|
268
313
|
)}
|
|
269
|
-
<div className=
|
|
314
|
+
<div className={`${isService ? '' : 'ml-4'} flex-1`}>
|
|
270
315
|
<div className="flex justify-between">
|
|
271
316
|
<div>
|
|
272
317
|
<div className="flex items-center gap-2">
|
|
@@ -300,107 +345,132 @@ export default function Cart({
|
|
|
300
345
|
{resource.oneliner}
|
|
301
346
|
</p>
|
|
302
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
|
+
)}
|
|
303
365
|
</div>
|
|
304
366
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
<div className="mr-6 text-right">
|
|
357
|
-
<p className="text-sm font-bold text-gray-900">
|
|
358
|
-
{price && parseFloat(price) > 0
|
|
359
|
-
? `${(parseFloat(price) * item.quantity).toFixed(2)} ${currency}`
|
|
360
|
-
: 'No Charge'}
|
|
361
|
-
</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
|
+
))}
|
|
362
418
|
</div>
|
|
363
419
|
|
|
364
|
-
|
|
365
|
-
<
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
className="rounded-md border border-gray-300 px-3 py-1 text-sm font-bold text-gray-600 hover:bg-gray-100"
|
|
377
|
-
>
|
|
378
|
-
Remove
|
|
379
|
-
</button>
|
|
380
|
-
) : (
|
|
381
|
-
<div className="flex items-center rounded-md border border-gray-300">
|
|
382
|
-
<button
|
|
383
|
-
onClick={() => dispatchAction(item, 'remove')}
|
|
384
|
-
className="px-3 py-1 text-gray-600 hover:bg-gray-100"
|
|
385
|
-
>
|
|
386
|
-
-
|
|
387
|
-
</button>
|
|
388
|
-
<span className="border-l border-r border-gray-300 px-3 py-1 text-gray-900">
|
|
389
|
-
{item.quantity}
|
|
390
|
-
</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 ? (
|
|
391
432
|
<button
|
|
392
|
-
onClick={() =>
|
|
393
|
-
|
|
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"
|
|
394
444
|
>
|
|
395
|
-
|
|
445
|
+
Remove
|
|
396
446
|
</button>
|
|
397
|
-
|
|
398
|
-
|
|
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>
|
|
399
469
|
</div>
|
|
400
|
-
|
|
401
|
-
)
|
|
402
|
-
|
|
403
|
-
|
|
470
|
+
);
|
|
471
|
+
})}
|
|
472
|
+
</div>
|
|
473
|
+
)}
|
|
404
474
|
</div>
|
|
405
475
|
</div>
|
|
406
476
|
</li>
|
|
@@ -408,6 +478,28 @@ export default function Cart({
|
|
|
408
478
|
})}
|
|
409
479
|
</ul>
|
|
410
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
|
+
|
|
411
503
|
<div className="rounded-b-lg border-t border-gray-200 bg-gray-50 px-6 py-6">
|
|
412
504
|
<div className="flex justify-end">
|
|
413
505
|
<button
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import { useStore } from '@nanostores/react';
|
|
2
2
|
import { cartStore } from '@/stores/shopify';
|
|
3
|
+
import { getCartIconCount } from '@/custom/shopify/shopifyHelpers';
|
|
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';
|