astro-tractstack 2.3.4 → 2.4.0

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 (64) hide show
  1. package/bin/create-tractstack.js +38 -59
  2. package/dist/index.js +74 -40
  3. package/package.json +46 -9
  4. package/templates/custom/customHelpers.ts +45 -0
  5. package/templates/custom/minimal/codehooks.ts +13 -0
  6. package/templates/custom/shopify/Cart.tsx +2 -2
  7. package/templates/custom/shopify/CartIcon.tsx +1 -1
  8. package/templates/custom/shopify/CheckoutModal.tsx +3 -3
  9. package/templates/custom/shopify/ShopifyCartManager.tsx +3 -3
  10. package/templates/custom/shopify/ShopifyCheckout.tsx +1 -1
  11. package/templates/custom/shopify/ShopifyProductGrid.tsx +5 -5
  12. package/templates/custom/shopify/ShopifyServiceList.tsx +5 -5
  13. package/templates/custom/shopify/shopifyCustomHelper.ts +10 -0
  14. package/templates/{src/utils/customHelpers.ts → custom/shopify/shopifyHelpers.ts} +0 -74
  15. package/templates/custom/with-examples/codehooks.ts +15 -0
  16. package/templates/src/components/Header.astro +1 -1
  17. package/templates/src/components/codehooks/EpinetDurationSelector.tsx +38 -23
  18. package/templates/src/components/codehooks/EpinetTableView.tsx +5 -2
  19. package/templates/src/components/codehooks/EpinetWrapper.tsx +10 -5
  20. package/templates/src/components/codehooks/FeaturedArticle.astro +3 -3
  21. package/templates/src/components/codehooks/ListContent.astro +3 -3
  22. package/templates/src/components/codehooks/SearchWidget.tsx +1 -1
  23. package/templates/src/components/compositor/Node.tsx +13 -2
  24. package/templates/src/components/compositor/nodes/Pane.tsx +2 -14
  25. package/templates/src/components/edit/pane/AddPanePanel.tsx +3 -2
  26. package/templates/src/components/edit/pane/AddPanePanel_codehook.tsx +35 -14
  27. package/templates/src/components/edit/pane/ConfigPanePanel.tsx +1 -1
  28. package/templates/src/components/edit/storyfragment/StoryFragmentPanel_menu.tsx +2 -2
  29. package/templates/src/components/search/SearchResults.tsx +1 -1
  30. package/templates/src/components/search/SearchWrapper.tsx +1 -1
  31. package/templates/src/components/storykeep/Dashboard_Analytics.tsx +8 -4
  32. package/templates/src/components/storykeep/controls/content/ContentBrowser.tsx +5 -2
  33. package/templates/src/components/storykeep/controls/content/ResourceForm.tsx +1 -1
  34. package/templates/src/components/storykeep/shopify/ShopifyDashboard_Sales.tsx +1 -1
  35. package/templates/src/components/storykeep/state/FetchAnalytics.tsx +8 -4
  36. package/templates/src/components/storykeep/widgets/Wizard.tsx +4 -2
  37. package/templates/src/lib/codeHookHelper.ts +156 -0
  38. package/templates/src/lib/resources.ts +41 -0
  39. package/templates/src/lib/storyData.ts +1 -2
  40. package/templates/src/pages/[...slug]/edit.astro +3 -3
  41. package/templates/src/pages/[...slug].astro +76 -70
  42. package/templates/src/pages/codehooks/[...hookId].astro +18 -0
  43. package/templates/src/pages/codehooks/bunny-video.astro +9 -0
  44. package/templates/src/pages/codehooks/custom-hero.astro +6 -0
  45. package/templates/src/pages/codehooks/epinet.astro +15 -0
  46. package/templates/src/pages/codehooks/featured-article.astro +13 -0
  47. package/templates/src/pages/codehooks/get-crafting.astro +8 -0
  48. package/templates/src/pages/codehooks/list-content.astro +13 -0
  49. package/templates/src/pages/codehooks/search-widget.astro +13 -0
  50. package/templates/src/pages/codehooks/shopify-product-grid.astro +25 -0
  51. package/templates/src/pages/codehooks/shopify-service-list.astro +25 -0
  52. package/templates/src/pages/context/[...contextSlug]/edit.astro +3 -3
  53. package/templates/src/pages/context/[...contextSlug].astro +47 -10
  54. package/templates/src/pages/sandbox.astro +3 -14
  55. package/templates/src/stores/analytics.ts +77 -107
  56. package/utils/inject-files.ts +76 -41
  57. package/templates/custom/minimal/CodeHook.astro +0 -72
  58. package/templates/custom/with-examples/CodeHook.astro +0 -81
  59. package/templates/custom/with-examples/ProductCard.astro +0 -29
  60. package/templates/custom/with-examples/ProductCardWrapper.astro +0 -43
  61. package/templates/custom/with-examples/ProductGrid.astro +0 -64
  62. package/templates/src/components/codehooks/ProductCardSetup.tsx +0 -157
  63. package/templates/src/components/codehooks/ProductGridSetup.tsx +0 -279
  64. /package/templates/{src/utils/booking → custom/shopify}/appointmentMode.ts +0 -0
@@ -2,52 +2,6 @@ import { getCartItemKey as baseGetCartItemKey } from '@/stores/shopify';
2
2
  import type { CartItemState, CartKeyParams } from '@/stores/shopify';
3
3
  import type { ResourceNode } from '@/types/compositorTypes';
4
4
 
5
- // URL Helper: Strip category prefix from slug
6
- // e.g., "people-bleako" -> "bleako"
7
- export function getCleanSlug(categorySlug: string, fullSlug: string): string {
8
- const prefix = `${categorySlug}-`;
9
- return fullSlug.startsWith(prefix) ? fullSlug.slice(prefix.length) : fullSlug;
10
- }
11
-
12
- // Build proper URL for resource
13
- // e.g., category="people", slug="people-bleako" -> "/people/bleako"
14
- export function getResourceUrl(categorySlug: string, fullSlug: string): string {
15
- const cleanSlug = getCleanSlug(categorySlug, fullSlug);
16
- return `/${categorySlug}/${cleanSlug}`;
17
- }
18
-
19
- // Image Helper: Placeholder implementation
20
- export function getResourceImage(
21
- id: string,
22
- slug: string,
23
- category: string
24
- ): string {
25
- console.log(`please define getResourceImage`, id, slug, category);
26
- return '/static.jpg';
27
- }
28
-
29
- export function getResourceDescription(
30
- id: string,
31
- slug: string,
32
- category: string
33
- ): string | null {
34
- console.log(`please define getResourceDescription`, id, slug, category);
35
- return null;
36
- }
37
-
38
- // Initialize search data - override in custom implementation
39
- export function initSearch(): void {
40
- // Default implementation does nothing
41
- // Override this function in your custom implementation to load search data
42
- }
43
-
44
- // Field Visibility Controls for ResourceForm
45
- export const resourceFormHideFields = ['shopifyImage'];
46
-
47
- // Field Formatting Controls for ResourceForm
48
- // Fields listed here will be treated as JSON objects but rendered as stringified text areas
49
- export const resourceJsonifyFields = ['shopifyData', 'shopifyImage'];
50
-
51
5
  const SERVICES_ATTR_LIMIT = 255;
52
6
 
53
7
  type CheckoutLineAttribute = { key: string; value: string };
@@ -70,34 +24,6 @@ export type SharedFeeChargeLineSummary = DepositSummary & {
70
24
  description?: string;
71
25
  };
72
26
 
73
- export const RESTRICTION_MESSAGES = {
74
- BOOKING: (duration: number) =>
75
- `This is a ${duration} minute service. On checkout we'll help you book at your convenience.`,
76
- TERMS: 'Please review the terms for this item before adding it to your cart.',
77
- MAX_DURATION: (max: number) =>
78
- `You cannot book more than ${max} minutes of services in one session.`,
79
- INCOMPATIBLE_REMOTE:
80
- 'This service cannot be combined with the services already in your cart. Some require remote-only delivery while others can only be delivered in person.',
81
- DEFAULT_ADD: (title: string) => `${title} has been added to your cart.`,
82
- };
83
-
84
- // For CartModal.tsx
85
- export function checkRestrictions(resource: ResourceNode): boolean {
86
- // 1. Service / Booking Requirement
87
- // We check for the explicit option payload value used by services
88
- if (resource.optionsPayload?.bookingLengthMinutes) {
89
- return true;
90
- }
91
-
92
- // 2. Final Sale / Terms Check
93
- // Placeholder: In the future, check for flags like resource.optionsPayload?.finalSale
94
- // if (resource.optionsPayload?.finalSale) {
95
- // return true;
96
- // }
97
-
98
- return false;
99
- }
100
-
101
27
  export function calculateCartDuration(
102
28
  cart: Record<string, CartItemState>,
103
29
  resources: ResourceNode[]
@@ -0,0 +1,15 @@
1
+ // Static, hand-maintained per-install manifest of available codehook ids.
2
+ // The backend never sees this; it is a frontend build capability list only.
3
+ // To add/remove a hook, edit this array AND add/remove the matching blade at
4
+ // src/pages/codehooks/{id}.astro. Not regenerated automatically.
5
+ export const availableCodeHookIds: string[] = [
6
+ 'featured-article',
7
+ 'list-content',
8
+ 'search-widget',
9
+ 'bunny-video',
10
+ 'epinet',
11
+ 'shopify-product-grid',
12
+ 'shopify-service-list',
13
+ 'custom-hero',
14
+ 'get-crafting',
15
+ ];
@@ -141,7 +141,7 @@ if (hasShopify) {
141
141
  <div
142
142
  class="flex flex-row flex-nowrap justify-between bg-mywhite px-4 pb-3 pt-4 shadow-inner md:px-8"
143
143
  >
144
- <h1 class="truncate text-xl text-mydarkgrey">{title}</h1>
144
+ <h1 class="truncate text-2xl text-mydarkgrey">{title}</h1>
145
145
  <div class="flex flex-row flex-nowrap items-center gap-x-2">
146
146
  {
147
147
  !isHome ? (
@@ -11,7 +11,10 @@ import XMarkIcon from '@heroicons/react/24/outline/XMarkIcon';
11
11
  import CheckCircleIcon from '@heroicons/react/24/outline/CheckCircleIcon';
12
12
  import ChevronLeftIcon from '@heroicons/react/24/outline/ChevronLeftIcon';
13
13
  import ChevronRightIcon from '@heroicons/react/24/outline/ChevronRightIcon';
14
- import { epinetCustomFilters } from '@/stores/analytics';
14
+ import {
15
+ epinetCustomFilters,
16
+ setEpinetCustomFilters,
17
+ } from '@/stores/analytics';
15
18
  import EpinetTableView from './EpinetTableView';
16
19
  import { MAX_ANALYTICS_HOURS } from '@/constants';
17
20
  import type { AppliedFilter } from '@/stores/analytics';
@@ -220,7 +223,7 @@ const EpinetDurationSelector = ({
220
223
  }
221
224
  setIsApplying(true);
222
225
  try {
223
- epinetCustomFilters.set(window.TRACTSTACK_CONFIG?.tenantId || 'default', {
226
+ setEpinetCustomFilters(window.TRACTSTACK_CONFIG?.tenantId || 'default', {
224
227
  ...$epinetCustomFilters,
225
228
  visitorType: localFilters.visitorType,
226
229
  selectedUserId: localFilters.selectedUserId,
@@ -794,10 +797,16 @@ const EpinetDurationSelector = ({
794
797
  collection={createListCollection({
795
798
  items: [
796
799
  { value: '', label: 'Select user' },
797
- ...paginatedUserCounts.map((user) => ({
798
- value: user.id,
799
- label: `${user.id} (${user.count} events)`,
800
- })),
800
+ ...paginatedUserCounts.map(
801
+ (user: {
802
+ id: string;
803
+ count: number;
804
+ isKnown: boolean;
805
+ }) => ({
806
+ value: user.id,
807
+ label: `${user.id} (${user.count} events)`,
808
+ })
809
+ ),
801
810
  ],
802
811
  })}
803
812
  value={
@@ -844,23 +853,29 @@ const EpinetDurationSelector = ({
844
853
  Select user
845
854
  </Select.ItemText>
846
855
  </Select.Item>,
847
- ...paginatedUserCounts.map((user) => (
848
- <Select.Item
849
- key={user.id}
850
- item={{
851
- value: user.id,
852
- label: `${user.id} (${user.count} events)`,
853
- }}
854
- className="epinet-user-select-item cursor-pointer select-none p-2 text-sm text-gray-700 hover:bg-slate-100"
855
- >
856
- <Select.ItemText>
857
- {user.id}{' '}
858
- <span className="text-xs text-gray-500">
859
- ({user.count} events)
860
- </span>
861
- </Select.ItemText>
862
- </Select.Item>
863
- )),
856
+ ...paginatedUserCounts.map(
857
+ (user: {
858
+ id: string;
859
+ count: number;
860
+ isKnown: boolean;
861
+ }) => (
862
+ <Select.Item
863
+ key={user.id}
864
+ item={{
865
+ value: user.id,
866
+ label: `${user.id} (${user.count} events)`,
867
+ }}
868
+ className="epinet-user-select-item cursor-pointer select-none p-2 text-sm text-gray-700 hover:bg-slate-100"
869
+ >
870
+ <Select.ItemText>
871
+ {user.id}{' '}
872
+ <span className="text-xs text-gray-500">
873
+ ({user.count} events)
874
+ </span>
875
+ </Select.ItemText>
876
+ </Select.Item>
877
+ )
878
+ ),
864
879
  ]
865
880
  ) : (
866
881
  <div className="p-2 text-sm text-gray-500">
@@ -1,7 +1,10 @@
1
1
  import { useState, useEffect } from 'react';
2
2
  import { useStore } from '@nanostores/react';
3
3
  import { classNames } from '@/utils/helpers';
4
- import { epinetCustomFilters } from '@/stores/analytics';
4
+ import {
5
+ epinetCustomFilters,
6
+ setEpinetCustomFilters,
7
+ } from '@/stores/analytics';
5
8
  import { Accordion } from '@ark-ui/react';
6
9
  import ChevronLeftIcon from '@heroicons/react/24/outline/ChevronLeftIcon';
7
10
  import ChevronRightIcon from '@heroicons/react/24/outline/ChevronRightIcon';
@@ -176,7 +179,7 @@ const EpinetTableView = ({
176
179
  const endTimeUTC = new Date(
177
180
  Date.UTC(year, month - 1, day, hour, 59, 59, 999)
178
181
  );
179
- epinetCustomFilters.set(window.TRACTSTACK_CONFIG?.tenantId || 'default', {
182
+ setEpinetCustomFilters(window.TRACTSTACK_CONFIG?.tenantId || 'default', {
180
183
  ...$epinetCustomFilters,
181
184
  startTimeUTC: startTimeUTC.toISOString(),
182
185
  endTimeUTC: endTimeUTC.toISOString(),
@@ -6,7 +6,12 @@ import {
6
6
  type ReactNode,
7
7
  } from 'react';
8
8
  import { useStore } from '@nanostores/react';
9
- import { epinetCustomFilters, type AppliedFilter } from '@/stores/analytics';
9
+ import {
10
+ epinetCustomFilters,
11
+ getEpinetCustomFilters,
12
+ setEpinetCustomFilters,
13
+ type AppliedFilter,
14
+ } from '@/stores/analytics';
10
15
  import { TractStackAPI } from '@/utils/api';
11
16
  import SankeyDiagram from './SankeyDiagram';
12
17
  import EpinetDurationSelector from './EpinetDurationSelector';
@@ -97,7 +102,7 @@ const EpinetWrapper = ({
97
102
  useEffect(() => {
98
103
  const nowUTC = new Date();
99
104
  const oneWeekAgoUTC = new Date(nowUTC.getTime() - 7 * 24 * 60 * 60 * 1000);
100
- epinetCustomFilters.set(window.TRACTSTACK_CONFIG?.tenantId || 'default', {
105
+ setEpinetCustomFilters(window.TRACTSTACK_CONFIG?.tenantId || 'default', {
101
106
  enabled: true,
102
107
  visitorType: 'all',
103
108
  selectedUserId: null,
@@ -133,7 +138,7 @@ const EpinetWrapper = ({
133
138
 
134
139
  const handleBeliefFilterChange = (beliefSlug: string, value: string) => {
135
140
  const tenantId = window.TRACTSTACK_CONFIG?.tenantId || 'default';
136
- const currentFilters = epinetCustomFilters.get();
141
+ const currentFilters = getEpinetCustomFilters();
137
142
  let newFilters: AppliedFilter[] = [
138
143
  ...(currentFilters.appliedFilters || []),
139
144
  ];
@@ -151,7 +156,7 @@ const EpinetWrapper = ({
151
156
  }
152
157
  }
153
158
 
154
- epinetCustomFilters.set(tenantId, {
159
+ setEpinetCustomFilters(tenantId, {
155
160
  ...currentFilters,
156
161
  appliedFilters: newFilters,
157
162
  });
@@ -229,7 +234,7 @@ const EpinetWrapper = ({
229
234
  error: null,
230
235
  isLoading: false,
231
236
  });
232
- epinetCustomFilters.set(
237
+ setEpinetCustomFilters(
233
238
  window.TRACTSTACK_CONFIG?.tenantId || 'default',
234
239
  {
235
240
  ...$epinetCustomFilters,
@@ -7,10 +7,10 @@ export interface Props {
7
7
  options?: string;
8
8
  };
9
9
  };
10
- contentMap: FullContentMapItem[];
10
+ fullContentMap: FullContentMapItem[];
11
11
  }
12
12
 
13
- const { options, contentMap } = Astro.props;
13
+ const { options, fullContentMap } = Astro.props;
14
14
 
15
15
  let parsedOptions;
16
16
  try {
@@ -22,7 +22,7 @@ try {
22
22
 
23
23
  const slug = parsedOptions.slug || '';
24
24
 
25
- const featuredStory = contentMap.find(
25
+ const featuredStory = fullContentMap.find(
26
26
  (item: FullContentMapItem) =>
27
27
  item.slug === slug &&
28
28
  item.type === 'StoryFragment' &&
@@ -7,10 +7,10 @@ export interface Props {
7
7
  options?: string;
8
8
  };
9
9
  };
10
- contentMap: FullContentMapItem[];
10
+ fullContentMap: FullContentMapItem[];
11
11
  }
12
12
 
13
- const { options, contentMap } = Astro.props;
13
+ const { options, fullContentMap } = Astro.props;
14
14
 
15
15
  // Parse component options
16
16
  let parsedOptions;
@@ -36,7 +36,7 @@ const title = parsedOptions.title;
36
36
  const bgColor = parsedOptions.bgColor || '';
37
37
 
38
38
  // Filter for valid stories to display
39
- const validPages = contentMap.filter(
39
+ const validPages = fullContentMap.filter(
40
40
  (item: FullContentMapItem): boolean =>
41
41
  item.type === 'StoryFragment' &&
42
42
  typeof item.description === 'string' &&
@@ -16,7 +16,7 @@ import {
16
16
  getResourceUrl,
17
17
  getResourceImage,
18
18
  getResourceDescription,
19
- } from '@/utils/customHelpers';
19
+ } from '@/custom/customHelpers';
20
20
 
21
21
  // --- TYPES ---
22
22
  interface SearchWidgetProps {
@@ -149,6 +149,7 @@ export const Node = memo((props: NodeProps) => {
149
149
  first={true}
150
150
  ctx={ctx}
151
151
  isContextPane={true}
152
+ isSandboxMode={props.isSandboxMode || false}
152
153
  />
153
154
  </>
154
155
  );
@@ -201,7 +202,12 @@ export const Node = memo((props: NodeProps) => {
201
202
  element = (
202
203
  <>
203
204
  {first && (
204
- <AddPanePanel nodeId={props.nodeId} first={true} ctx={ctx} />
205
+ <AddPanePanel
206
+ nodeId={props.nodeId}
207
+ first={true}
208
+ ctx={ctx}
209
+ isSandboxMode={props.isSandboxMode || false}
210
+ />
205
211
  )}
206
212
  <div className="py-0.5">
207
213
  <ConfigPanePanel
@@ -217,7 +223,12 @@ export const Node = memo((props: NodeProps) => {
217
223
  {content}
218
224
  </PanelVisibilityWrapper>
219
225
  </div>
220
- <AddPanePanel nodeId={props.nodeId} first={false} ctx={ctx} />
226
+ <AddPanePanel
227
+ nodeId={props.nodeId}
228
+ first={false}
229
+ ctx={ctx}
230
+ isSandboxMode={props.isSandboxMode || false}
231
+ />
221
232
  </>
222
233
  );
223
234
  }
@@ -5,8 +5,6 @@ import { RenderChildren } from './RenderChildren';
5
5
  import FeaturedArticleSetup from '@/components/codehooks/FeaturedArticleSetup';
6
6
  import ListContentSetup from '@/components/codehooks/ListContentSetup';
7
7
  import BunnyVideoSetup from '@/components/codehooks/BunnyVideoSetup';
8
- import { ProductCardSetup } from '@/components/codehooks/ProductCardSetup';
9
- import { ProductGridSetup } from '@/components/codehooks/ProductGridSetup';
10
8
  import { PaneOverlay } from '@/components/compositor/tools/PaneOverlay';
11
9
  import type {
12
10
  PaneNode,
@@ -15,13 +13,7 @@ import type {
15
13
  } from '@/types/compositorTypes';
16
14
  import type { NodeProps } from '@/types/nodeProps';
17
15
 
18
- const TARGETS = [
19
- 'list-content',
20
- 'featured-article',
21
- 'bunny-video',
22
- 'product-card',
23
- 'product-grid',
24
- ];
16
+ const TARGETS = ['list-content', 'featured-article', 'bunny-video'];
25
17
 
26
18
  const CodeHookContainer = ({
27
19
  payload,
@@ -171,11 +163,7 @@ const Pane = memo(
171
163
  id={getCtx(props).getNodeSlug(props.nodeId)}
172
164
  className={useFlexLayout ? '' : wrapperClasses}
173
165
  >
174
- {codeHookPayload && codeHookTarget === 'product-card' ? (
175
- <ProductCardSetup nodeId={props.nodeId} params={codeHookParams} />
176
- ) : codeHookPayload && codeHookTarget === 'product-grid' ? (
177
- <ProductGridSetup nodeId={props.nodeId} params={codeHookParams} />
178
- ) : codeHookPayload && codeHookTarget === 'featured-article' ? (
166
+ {codeHookPayload && codeHookTarget === 'featured-article' ? (
179
167
  <FeaturedArticleSetup
180
168
  nodeId={props.nodeId}
181
169
  params={codeHookParams}
@@ -94,13 +94,14 @@ const AddPanePanel = ({
94
94
  />
95
95
  ) : mode === PaneAddMode.REUSE && !isContextPane ? (
96
96
  <AddPaneReUsePanel nodeId={nodeId} first={first} setMode={setMode} />
97
- ) : mode === PaneAddMode.CODEHOOK && !isContextPane ? (
97
+ ) : mode === PaneAddMode.CODEHOOK ? (
98
98
  <AddPaneCodeHookPanel
99
99
  nodeId={nodeId}
100
100
  first={first}
101
101
  setMode={setMode}
102
102
  isStoryFragment={isStoryFragment}
103
103
  isContextPane={isContextPane}
104
+ isSandboxMode={isSandboxMode}
104
105
  />
105
106
  ) : mode === PaneAddMode.PASTE ? (
106
107
  <AddPanePanel_paste
@@ -139,7 +140,7 @@ const AddPanePanel = ({
139
140
  )}
140
141
  </>
141
142
  )}
142
- {!isTemplate && !isContextPane && (
143
+ {!isTemplate && (
143
144
  <button
144
145
  onClick={() => setMode(PaneAddMode.CODEHOOK)}
145
146
  className="rounded bg-white px-2 py-1 text-sm text-cyan-700 shadow-sm transition-colors hover:bg-cyan-700 hover:text-white"
@@ -5,7 +5,11 @@ import { Combobox } from '@ark-ui/react';
5
5
  import { createListCollection } from '@ark-ui/react/collection';
6
6
  import ChevronUpDownIcon from '@heroicons/react/20/solid/ChevronUpDownIcon';
7
7
  import CheckIcon from '@heroicons/react/20/solid/CheckIcon';
8
- import { codehookMapStore, fullContentMapStore } from '@/stores/storykeep';
8
+ import {
9
+ brandConfigStore,
10
+ codehookMapStore,
11
+ fullContentMapStore,
12
+ } from '@/stores/storykeep';
9
13
  import { getCtx } from '@/stores/nodes';
10
14
  import { findUniqueSlug } from '@/utils/helpers';
11
15
  import { PaneAddMode, type TemplatePane } from '@/types/compositorTypes';
@@ -16,6 +20,7 @@ interface AddPaneCodeHookPanelProps {
16
20
  setMode: (mode: PaneAddMode) => void;
17
21
  isStoryFragment?: boolean;
18
22
  isContextPane?: boolean;
23
+ isSandboxMode?: boolean;
19
24
  }
20
25
 
21
26
  const AddPaneCodeHookPanel = ({
@@ -24,10 +29,13 @@ const AddPaneCodeHookPanel = ({
24
29
  setMode,
25
30
  isStoryFragment = false,
26
31
  isContextPane = false,
32
+ isSandboxMode = false,
27
33
  }: AddPaneCodeHookPanelProps) => {
28
34
  const [selected, setSelected] = useState<string | null>(null);
29
35
  const [query, setQuery] = useState('');
30
36
  const $contentMap = useStore(fullContentMapStore);
37
+ const brandConfig = useStore(brandConfigStore);
38
+ const hasShopify = brandConfig?.HAS_SHOPIFY === true;
31
39
 
32
40
  const existingSlugs = $contentMap
33
41
  .filter((item) => ['Pane', 'StoryFragment'].includes(item.type))
@@ -39,9 +47,22 @@ const AddPaneCodeHookPanel = ({
39
47
 
40
48
  const availableCodeHooks = codehookMapStore.get();
41
49
 
42
- // Filter hooks based on search query
50
+ const isHookVisibleInPicker = (hookName: string) => {
51
+ if (
52
+ (hookName === 'shopify-product-grid' ||
53
+ hookName === 'shopify-service-list') &&
54
+ !hasShopify
55
+ ) {
56
+ return false;
57
+ }
58
+ if (hookName === 'get-crafting' && !isSandboxMode) {
59
+ return false;
60
+ }
61
+ return true;
62
+ };
63
+
64
+ // Filter hooks based on search query and tenant/sandbox gates
43
65
  const filteredHooks = useMemo(() => {
44
- // Start with available hooks
45
66
  const hooks =
46
67
  query === ''
47
68
  ? [...availableCodeHooks]
@@ -49,9 +70,8 @@ const AddPaneCodeHookPanel = ({
49
70
  hook.toLowerCase().includes(query.toLowerCase())
50
71
  );
51
72
 
52
- // Create a new array with unavailable hooks removed (don't just filter - we want to show them as disabled)
53
- return hooks;
54
- }, [availableCodeHooks, query]);
73
+ return hooks.filter(isHookVisibleInPicker);
74
+ }, [availableCodeHooks, query, hasShopify, isSandboxMode]);
55
75
 
56
76
  // Create collection for Ark UI Combobox
57
77
  const collection = useMemo(() => {
@@ -64,21 +84,22 @@ const AddPaneCodeHookPanel = ({
64
84
 
65
85
  const isHookAvailable = (hookName: string) => {
66
86
  if (
67
- (hookName === 'featured-content' ||
68
- hookName === 'list-content' ||
69
- hookName === 'featured-article') &&
87
+ (hookName === 'list-content' || hookName === 'featured-article') &&
70
88
  !hasStoryFragments
71
89
  ) {
72
- return hasStoryFragments;
90
+ return false;
91
+ }
92
+ if (
93
+ hookName === 'bunny-video' &&
94
+ import.meta.env.PUBLIC_ENABLE_BUNNY !== 'true'
95
+ ) {
96
+ return false;
73
97
  }
74
98
  return true;
75
99
  };
76
100
 
77
101
  const getDisplayName = (hookName: string) => {
78
- if (
79
- (hookName === 'featured-content' || hookName === 'list-content') &&
80
- !hasStoryFragments
81
- ) {
102
+ if (hookName === 'list-content' && !hasStoryFragments) {
82
103
  return `${hookName} (not yet available; no pages found)`;
83
104
  }
84
105
  return hookName;
@@ -260,7 +260,7 @@ const ConfigPanePanel = ({
260
260
  )}
261
261
  </>
262
262
  )}
263
- {isCodeHook && !isContextPane && (
263
+ {isCodeHook && (
264
264
  <button
265
265
  onClick={handleCodeHookConfig}
266
266
  className={buttonClass}
@@ -13,7 +13,7 @@ import {
13
13
  type MenuNode,
14
14
  } from '@/types/compositorTypes';
15
15
  import MenuForm from '@/components/storykeep/controls/content/MenuForm';
16
- import { fullContentMapStore, getFullContentMap } from '@/stores/analytics';
16
+ import { getFullContentMap, setTenantFullContentMap } from '@/stores/analytics';
17
17
  import type { FullContentMapItem } from '@/types/tractstack';
18
18
 
19
19
  interface StoryFragmentMenuPanelProps {
@@ -53,7 +53,7 @@ const StoryFragmentMenuPanel = ({
53
53
  if (!contentMap) {
54
54
  const currentContentMap = await api.getContentMapWithTimestamp();
55
55
  if (currentContentMap.success && currentContentMap.data) {
56
- fullContentMapStore.set(tenantId, currentContentMap.data);
56
+ setTenantFullContentMap(tenantId, currentContentMap.data);
57
57
  setContentMap(currentContentMap.data.data);
58
58
  }
59
59
  }
@@ -8,7 +8,7 @@ import {
8
8
  getResourceUrl,
9
9
  getResourceImage,
10
10
  getResourceDescription,
11
- } from '@/utils/customHelpers';
11
+ } from '@/custom/customHelpers';
12
12
 
13
13
  const VERBOSE = false;
14
14
 
@@ -1,6 +1,6 @@
1
1
  import { useState, useEffect } from 'react';
2
2
  import MagnifyingGlassIcon from '@heroicons/react/24/outline/MagnifyingGlassIcon';
3
- import { initSearch } from '@/utils/customHelpers';
3
+ import { initSearch } from '@/custom/customHelpers';
4
4
  import SearchModal from './SearchModal';
5
5
  import type { FullContentMapItem } from '@/types/tractstack';
6
6
 
@@ -1,7 +1,11 @@
1
1
  import { useState, useCallback, useMemo, Component } from 'react';
2
2
  import type { ReactNode } from 'react';
3
3
  import { useStore } from '@nanostores/react';
4
- import { epinetCustomFilters } from '@/stores/analytics';
4
+ import {
5
+ epinetCustomFilters,
6
+ getEpinetCustomFilters,
7
+ setEpinetCustomFilters,
8
+ } from '@/stores/analytics';
5
9
  import { classNames } from '@/utils/helpers';
6
10
  import ArrowDownTrayIcon from '@heroicons/react/24/outline/ArrowDownTrayIcon';
7
11
  import DashboardActivity from './Dashboard_Activity';
@@ -167,7 +171,7 @@ export default function StoryKeepDashboard_Analytics({
167
171
 
168
172
  const handleBeliefFilterChange = (beliefSlug: string, value: string) => {
169
173
  const tenantId = window.TRACTSTACK_CONFIG?.tenantId || 'default';
170
- const currentFilters = epinetCustomFilters.get();
174
+ const currentFilters = getEpinetCustomFilters();
171
175
  let newFilters = [...(currentFilters.appliedFilters || [])];
172
176
 
173
177
  if (value === 'All') {
@@ -183,7 +187,7 @@ export default function StoryKeepDashboard_Analytics({
183
187
  }
184
188
  }
185
189
 
186
- epinetCustomFilters.set(tenantId, {
190
+ setEpinetCustomFilters(tenantId, {
187
191
  ...currentFilters,
188
192
  appliedFilters: newFilters,
189
193
  });
@@ -220,7 +224,7 @@ export default function StoryKeepDashboard_Analytics({
220
224
  nowUTC.getTime() - hoursBack * 60 * 60 * 1000
221
225
  );
222
226
 
223
- epinetCustomFilters.set(window.TRACTSTACK_CONFIG?.tenantId || 'default', {
227
+ setEpinetCustomFilters(window.TRACTSTACK_CONFIG?.tenantId || 'default', {
224
228
  ...$epinetCustomFilters,
225
229
  enabled: true,
226
230
  startTimeUTC: startTimeUTC.toISOString(),
@@ -1,7 +1,10 @@
1
1
  import { useState, useEffect, useRef } from 'react';
2
2
  import { Switch } from '@ark-ui/react';
3
3
  import { useStore } from '@nanostores/react';
4
- import { epinetCustomFilters } from '@/stores/analytics';
4
+ import {
5
+ epinetCustomFilters,
6
+ setEpinetCustomFilters,
7
+ } from '@/stores/analytics';
5
8
  import { classNames } from '@/utils/helpers';
6
9
  import type { FullContentMapItem } from '@/types/tractstack';
7
10
 
@@ -104,7 +107,7 @@ const ContentBrowser = ({
104
107
  const setStandardDuration = (hours: number) => {
105
108
  const nowUTC = new Date();
106
109
  const startTimeUTC = new Date(nowUTC.getTime() - hours * 60 * 60 * 1000);
107
- epinetCustomFilters.set(window.TRACTSTACK_CONFIG?.tenantId || 'default', {
110
+ setEpinetCustomFilters(window.TRACTSTACK_CONFIG?.tenantId || 'default', {
108
111
  ...$epinetCustomFilters,
109
112
  enabled: true,
110
113
  startTimeUTC: startTimeUTC.toISOString(),
@@ -13,7 +13,7 @@ import EnumSelect from '@/components/form/EnumSelect';
13
13
  import {
14
14
  resourceFormHideFields,
15
15
  resourceJsonifyFields,
16
- } from '@/utils/customHelpers';
16
+ } from '@/custom/customHelpers';
17
17
  import type {
18
18
  ResourceConfig,
19
19
  ResourceState,
@@ -8,7 +8,7 @@ import {
8
8
  getServiceLinkedProduct,
9
9
  isSharedFeeService,
10
10
  parsePrimaryShopifyProductData,
11
- } from '@/utils/customHelpers';
11
+ } from '@/custom/shopify/shopifyHelpers';
12
12
 
13
13
  interface ShopifyDashboardSalesProps {
14
14
  existingResources: ResourceNode[];