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.
Files changed (49) hide show
  1. package/bin/create-tractstack.js +5 -2
  2. package/dist/index.js +32 -4
  3. package/package.json +1 -1
  4. package/templates/custom/customHelpers.ts +45 -0
  5. package/templates/custom/shopify/Cart.tsx +197 -105
  6. package/templates/custom/shopify/CartIcon.tsx +8 -8
  7. package/templates/custom/shopify/CheckoutModal.tsx +145 -68
  8. package/templates/custom/shopify/ShopifyCartManager.tsx +67 -22
  9. package/templates/custom/shopify/ShopifyCheckout.tsx +2 -26
  10. package/templates/custom/shopify/ShopifyProductGrid.tsx +8 -0
  11. package/templates/custom/shopify/ShopifyServiceList.tsx +25 -37
  12. package/templates/custom/shopify/shopifyCustomHelper.ts +10 -0
  13. package/templates/custom/shopify/shopifyHelpers.ts +298 -0
  14. package/templates/src/components/Header.astro +2 -2
  15. package/templates/src/components/codehooks/SearchWidget.tsx +1 -1
  16. package/templates/src/components/compositor/Node.tsx +39 -9
  17. package/templates/src/components/compositor/nodes/CreativePane.tsx +175 -88
  18. package/templates/src/components/edit/pane/AddPanePanel.tsx +2 -2
  19. package/templates/src/components/edit/pane/AddPanePanel_new.tsx +6 -5
  20. package/templates/src/components/edit/pane/AddPanePanel_reuse.tsx +9 -15
  21. package/templates/src/components/edit/pane/ConfigPanePanel.tsx +1 -1
  22. package/templates/src/components/form/advanced/APIConfigSection.tsx +35 -8
  23. package/templates/src/components/form/brand/SiteConfigSection.tsx +9 -0
  24. package/templates/src/components/search/SearchResults.tsx +1 -1
  25. package/templates/src/components/search/SearchWrapper.tsx +1 -1
  26. package/templates/src/components/storykeep/Dashboard_Advanced.tsx +59 -23
  27. package/templates/src/components/storykeep/Dashboard_Shopify.tsx +257 -18
  28. package/templates/src/components/storykeep/controls/content/ProductTable.tsx +38 -24
  29. package/templates/src/components/storykeep/controls/content/ResourceForm.tsx +162 -67
  30. package/templates/src/components/storykeep/shopify/ShopifyDashboard.tsx +175 -48
  31. package/templates/src/components/storykeep/shopify/ShopifyDashboard_Bookings.tsx +5 -2
  32. package/templates/src/components/storykeep/shopify/ShopifyDashboard_Sales.tsx +479 -0
  33. package/templates/src/components/storykeep/shopify/ShopifyDashboard_Search.tsx +7 -3
  34. package/templates/src/layouts/Layout.astro +26 -0
  35. package/templates/src/pages/api/auth/logout.ts +35 -2
  36. package/templates/src/pages/api/sales/list.ts +66 -0
  37. package/templates/src/pages/api/sales/metrics.ts +60 -0
  38. package/templates/src/pages/context/[...contextSlug].astro +50 -31
  39. package/templates/src/pages/storykeep/advanced.astro +4 -1
  40. package/templates/src/stores/nodes.ts +8 -0
  41. package/templates/src/types/tractstack.ts +57 -0
  42. package/templates/src/utils/api/advancedConfig.ts +2 -1
  43. package/templates/src/utils/api/advancedHelpers.ts +4 -0
  44. package/templates/src/utils/api/brandConfig.ts +2 -0
  45. package/templates/src/utils/api/brandHelpers.ts +6 -0
  46. package/templates/src/utils/api/salesHelpers.ts +21 -0
  47. package/utils/inject-files.ts +32 -4
  48. package/templates/src/utils/customHelpers.ts +0 -89
  49. /package/templates/{src/utils/booking → custom/shopify}/appointmentMode.ts +0 -0
@@ -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@3.10.0`,
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@3.10.0 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`
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/src/utils/customHelpers.ts"),
2327
- dest: "src/utils/customHelpers.ts",
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/src/utils/booking/appointmentMode.ts"),
2332
- dest: "src/utils/booking/appointmentMode.ts",
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "astro-tractstack",
3
- "version": "2.3.3",
3
+ "version": "2.3.5",
4
4
  "description": "Astro integration for TractStack - the free web press by At Risk Media",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -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 '@/utils/booking/appointmentMode';
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
- {Object.keys(groupedItems).map((resourceId) => {
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
- let productData: any = {};
246
- try {
247
- if (resource.optionsPayload?.shopifyData) {
248
- productData = JSON.parse(resource.optionsPayload.shopifyData);
249
- }
250
- } catch (e) {
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="ml-4 flex-1">
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
- <div className="mt-4 space-y-4 border-t border-gray-100 pt-4">
306
- {items.map((item, idx) => {
307
- const activeVariantId = isPickupMode
308
- ? item.variantIdPickup
309
- : item.variantIdShipped;
310
-
311
- const displayId =
312
- item.variantId ||
313
- activeVariantId ||
314
- item.variantIdPickup;
315
-
316
- let price = '0.00';
317
- let currency = 'USD';
318
- let variantTitle = '';
319
-
320
- const variant = variants.find(
321
- (v: any) => v.id === displayId
322
- );
323
-
324
- if (variant) {
325
- price = variant.price?.amount || '0.00';
326
- currency = variant.price?.currencyCode || 'USD';
327
- variantTitle = getCleanVariantTitle(variant);
328
- }
329
-
330
- return (
331
- <div
332
- key={`${item.resourceId}_${displayId}_${idx}`}
333
- className="flex items-center justify-between"
334
- >
335
- <div className="flex items-center gap-2">
336
- {variantTitle && (
337
- <div className="text-sm font-bold text-gray-700">
338
- <span>{variantTitle}</span>
339
- </div>
340
- )}
341
- {isPickupMode &&
342
- !isService &&
343
- (item.variantIdPickup &&
344
- item.variantIdPickup !== item.variantIdShipped ? (
345
- <span className="inline-flex items-center rounded bg-gray-100 px-2 py-0.5 text-xs font-bold text-gray-800">
346
- Store Pickup
347
- </span>
348
- ) : (
349
- <span className="inline-flex items-center rounded bg-red-50 px-2 py-0.5 text-xs font-bold text-red-700">
350
- Not available for pickup
351
- </span>
352
- ))}
353
- </div>
354
-
355
- <div className="flex items-center">
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
- {isService ? (
365
- <button
366
- onClick={() =>
367
- addQueue.set([
368
- ...addQueue.get(),
369
- {
370
- resourceId: item.resourceId,
371
- action: 'remove',
372
- variantId: item.variantId,
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={() => dispatchAction(item, 'add')}
393
- 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"
394
444
  >
395
- +
445
+ Remove
396
446
  </button>
397
- </div>
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
- </div>
401
- );
402
- })}
403
- </div>
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
- 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';