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.
Files changed (58) hide show
  1. package/bin/create-tractstack.js +7 -4
  2. package/dist/index.js +51 -8
  3. package/package.json +1 -1
  4. package/templates/custom/shopify/Cart.tsx +279 -118
  5. package/templates/custom/shopify/CartIcon.tsx +8 -8
  6. package/templates/custom/shopify/CheckoutModal.tsx +328 -65
  7. package/templates/custom/shopify/ShopifyCartManager.tsx +117 -60
  8. package/templates/custom/shopify/ShopifyCheckout.tsx +2 -26
  9. package/templates/custom/shopify/ShopifyProductGrid.tsx +8 -0
  10. package/templates/custom/shopify/ShopifyServiceList.tsx +25 -37
  11. package/templates/custom/shopify/cart.astro +7 -1
  12. package/templates/src/components/Header.astro +4 -2
  13. package/templates/src/components/compositor/Node.tsx +39 -9
  14. package/templates/src/components/compositor/nodes/CreativePane.tsx +175 -88
  15. package/templates/src/components/edit/pane/AddPanePanel.tsx +2 -2
  16. package/templates/src/components/edit/pane/AddPanePanel_new.tsx +6 -5
  17. package/templates/src/components/edit/pane/AddPanePanel_reuse.tsx +9 -15
  18. package/templates/src/components/edit/pane/ConfigPanePanel.tsx +1 -1
  19. package/templates/src/components/form/advanced/APIConfigSection.tsx +249 -4
  20. package/templates/src/components/form/brand/SiteConfigSection.tsx +9 -0
  21. package/templates/src/components/form/shopify/SchedulingSection.tsx +44 -0
  22. package/templates/src/components/storykeep/Dashboard_Advanced.tsx +66 -21
  23. package/templates/src/components/storykeep/Dashboard_Shopify.tsx +266 -18
  24. package/templates/src/components/storykeep/controls/content/ManageContent.tsx +1 -0
  25. package/templates/src/components/storykeep/controls/content/ProductTable.tsx +38 -24
  26. package/templates/src/components/storykeep/controls/content/ResourceForm.tsx +240 -65
  27. package/templates/src/components/storykeep/shopify/ShopifyDashboard.tsx +175 -48
  28. package/templates/src/components/storykeep/shopify/ShopifyDashboard_Bookings.tsx +91 -10
  29. package/templates/src/components/storykeep/shopify/ShopifyDashboard_Sales.tsx +479 -0
  30. package/templates/src/components/storykeep/shopify/ShopifyDashboard_Search.tsx +7 -3
  31. package/templates/src/constants.ts +2 -0
  32. package/templates/src/layouts/Layout.astro +26 -0
  33. package/templates/src/pages/api/auth/logout.ts +35 -2
  34. package/templates/src/pages/api/google/oauth/callback.ts +50 -0
  35. package/templates/src/pages/api/google/oauth/disconnect.ts +32 -0
  36. package/templates/src/pages/api/google/oauth/start.ts +32 -0
  37. package/templates/src/pages/api/google/oauth/status.ts +32 -0
  38. package/templates/src/pages/api/sales/list.ts +66 -0
  39. package/templates/src/pages/api/sales/metrics.ts +60 -0
  40. package/templates/src/pages/context/[...contextSlug].astro +50 -31
  41. package/templates/src/pages/privacy.astro +84 -0
  42. package/templates/src/pages/storykeep/advanced.astro +4 -1
  43. package/templates/src/pages/terms.astro +47 -0
  44. package/templates/src/stores/nodes.ts +8 -0
  45. package/templates/src/stores/shopify.ts +5 -0
  46. package/templates/src/types/tractstack.ts +87 -0
  47. package/templates/src/utils/api/advancedConfig.ts +2 -1
  48. package/templates/src/utils/api/advancedHelpers.ts +20 -0
  49. package/templates/src/utils/api/bookingHelpers.ts +3 -1
  50. package/templates/src/utils/api/brandConfig.ts +2 -0
  51. package/templates/src/utils/api/brandHelpers.ts +14 -1
  52. package/templates/src/utils/api/salesHelpers.ts +21 -0
  53. package/templates/src/utils/booking/appointmentMode.ts +135 -0
  54. package/templates/src/utils/customHelpers.ts +287 -2
  55. package/utils/inject-files.ts +47 -4
  56. package/templates/src/components/codehooks/SandboxAuthWrapper.tsx +0 -101
  57. package/templates/src/utils/actions/actionButton.ts +0 -103
  58. package/templates/src/utils/actions/preParse_Clicked.ts +0 -87
@@ -1,5 +1,6 @@
1
+ import { getCartItemKey as baseGetCartItemKey } from '@/stores/shopify';
2
+ import type { CartItemState, CartKeyParams } from '@/stores/shopify';
1
3
  import type { ResourceNode } from '@/types/compositorTypes';
2
- import type { CartItemState } from '@/stores/shopify';
3
4
 
4
5
  // URL Helper: Strip category prefix from slug
5
6
  // e.g., "people-bleako" -> "bleako"
@@ -41,18 +42,42 @@ export function initSearch(): void {
41
42
  }
42
43
 
43
44
  // Field Visibility Controls for ResourceForm
44
- export const resourceFormHideFields = ['gid', 'shopifyImage'];
45
+ export const resourceFormHideFields = ['shopifyImage'];
45
46
 
46
47
  // Field Formatting Controls for ResourceForm
47
48
  // Fields listed here will be treated as JSON objects but rendered as stringified text areas
48
49
  export const resourceJsonifyFields = ['shopifyData', 'shopifyImage'];
49
50
 
51
+ const SERVICES_ATTR_LIMIT = 255;
52
+
53
+ type CheckoutLineAttribute = { key: string; value: string };
54
+
55
+ export type ShopifyCheckoutLine = {
56
+ merchandiseId: string;
57
+ quantity: number;
58
+ attributes?: CheckoutLineAttribute[];
59
+ };
60
+
61
+ export type DepositSummary = {
62
+ title: string;
63
+ amount: string;
64
+ currencyCode: string;
65
+ variantId: string;
66
+ };
67
+
68
+ export type SharedFeeChargeLineSummary = DepositSummary & {
69
+ servicesCount: number;
70
+ description?: string;
71
+ };
72
+
50
73
  export const RESTRICTION_MESSAGES = {
51
74
  BOOKING: (duration: number) =>
52
75
  `This is a ${duration} minute service. On checkout we'll help you book at your convenience.`,
53
76
  TERMS: 'Please review the terms for this item before adding it to your cart.',
54
77
  MAX_DURATION: (max: number) =>
55
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.',
56
81
  DEFAULT_ADD: (title: string) => `${title} has been added to your cart.`,
57
82
  };
58
83
 
@@ -85,3 +110,263 @@ export function calculateCartDuration(
85
110
  return total + (isNaN(duration) ? 0 : duration * item.quantity);
86
111
  }, 0);
87
112
  }
113
+
114
+ export function getProductByGid(
115
+ resources: ResourceNode[],
116
+ gid?: string
117
+ ): ResourceNode | undefined {
118
+ if (!gid) return undefined;
119
+ return resources.find(
120
+ (r) => r.categorySlug === 'product' && r.optionsPayload?.gid === gid
121
+ );
122
+ }
123
+
124
+ export function getServiceLinkedProduct(
125
+ service: ResourceNode,
126
+ resources: ResourceNode[]
127
+ ): ResourceNode | undefined {
128
+ const gid =
129
+ typeof service.optionsPayload?.gid === 'string'
130
+ ? service.optionsPayload.gid
131
+ : undefined;
132
+ return getProductByGid(resources, gid);
133
+ }
134
+
135
+ export function parsePrimaryShopifyProductData(
136
+ resource?: ResourceNode
137
+ ): any | null {
138
+ if (!resource?.optionsPayload?.shopifyData) {
139
+ return null;
140
+ }
141
+ try {
142
+ const parsed = JSON.parse(resource.optionsPayload.shopifyData);
143
+ return parsed.products?.[0] || parsed;
144
+ } catch {
145
+ return null;
146
+ }
147
+ }
148
+
149
+ function extractVariantForResource(resource?: ResourceNode): any | null {
150
+ const parsed = parsePrimaryShopifyProductData(resource);
151
+ if (!parsed) return null;
152
+ const variants = parsed.variants || [];
153
+ return variants[0] || null;
154
+ }
155
+
156
+ export function parseDepositFromProductResource(
157
+ product?: ResourceNode
158
+ ): DepositSummary | null {
159
+ if (!product) return null;
160
+ const parsed = parsePrimaryShopifyProductData(product);
161
+ const variant = extractVariantForResource(product);
162
+ const variantId = variant?.id;
163
+ if (!variantId) return null;
164
+ return {
165
+ title: parsed?.title || product.title,
166
+ amount: variant?.price?.amount || '0.00',
167
+ currencyCode: variant?.price?.currencyCode || 'USD',
168
+ variantId,
169
+ };
170
+ }
171
+
172
+ export function isSharedFeeService(
173
+ service: ResourceNode | undefined,
174
+ resources: ResourceNode[]
175
+ ): boolean {
176
+ if (!service || service.categorySlug !== 'service') return false;
177
+ const product = getServiceLinkedProduct(service, resources);
178
+ return product?.optionsPayload?.sharedServiceFee === true;
179
+ }
180
+
181
+ export function getServiceDisplayTitle(
182
+ service: ResourceNode | undefined,
183
+ resources: ResourceNode[]
184
+ ): string {
185
+ if (!service) return 'Service';
186
+ const product = getServiceLinkedProduct(service, resources);
187
+ const parsed = parsePrimaryShopifyProductData(product);
188
+ return parsed?.title || service.title;
189
+ }
190
+
191
+ export function getServiceVariantIdFromCanonicalProduct(
192
+ service: ResourceNode | undefined,
193
+ resources: ResourceNode[]
194
+ ): string | undefined {
195
+ if (!service || service.categorySlug !== 'service') {
196
+ return undefined;
197
+ }
198
+ const product = getServiceLinkedProduct(service, resources);
199
+ const variant = extractVariantForResource(product);
200
+ return typeof variant?.id === 'string' ? variant.id : undefined;
201
+ }
202
+
203
+ export function getCartItemKey(
204
+ params: CartKeyParams,
205
+ resource?: ResourceNode,
206
+ resources: ResourceNode[] = []
207
+ ): string {
208
+ if (resource && isSharedFeeService(resource, resources)) {
209
+ return params.resourceId;
210
+ }
211
+ return baseGetCartItemKey(params);
212
+ }
213
+
214
+ export function collectServiceGids(services: ResourceNode[]): Set<string> {
215
+ const gids = new Set<string>();
216
+ services.forEach((service) => {
217
+ if (typeof service.optionsPayload?.gid === 'string') {
218
+ gids.add(service.optionsPayload.gid);
219
+ }
220
+ });
221
+ return gids;
222
+ }
223
+
224
+ function formatServicesAttribute(
225
+ services: ResourceNode[]
226
+ ): CheckoutLineAttribute {
227
+ const titles = services.map((s) => s.title);
228
+ const joined = titles.join(', ');
229
+ if (joined.length <= SERVICES_ATTR_LIMIT) {
230
+ return { key: 'Services', value: joined };
231
+ }
232
+ return { key: 'Services', value: `${services.length} services` };
233
+ }
234
+
235
+ export function getDepositLineSummary(
236
+ cart: Record<string, CartItemState>,
237
+ resources: ResourceNode[]
238
+ ): DepositSummary | null {
239
+ const chargeLine = getSharedFeeChargeLineSummary(cart, resources);
240
+ if (!chargeLine) {
241
+ return null;
242
+ }
243
+ return {
244
+ title: chargeLine.title,
245
+ amount: chargeLine.amount,
246
+ currencyCode: chargeLine.currencyCode,
247
+ variantId: chargeLine.variantId,
248
+ };
249
+ }
250
+
251
+ export function getSharedFeeChargeLineSummary(
252
+ cart: Record<string, CartItemState>,
253
+ resources: ResourceNode[]
254
+ ): SharedFeeChargeLineSummary | null {
255
+ const serviceIds = new Set(
256
+ Object.values(cart).map((item) => item.resourceId)
257
+ );
258
+ const sharedServices = resources.filter(
259
+ (r) => serviceIds.has(r.id) && isSharedFeeService(r, resources)
260
+ );
261
+ if (sharedServices.length === 0) {
262
+ return null;
263
+ }
264
+ const canonicalProduct = getServiceLinkedProduct(
265
+ sharedServices[0],
266
+ resources
267
+ );
268
+ const deposit = parseDepositFromProductResource(canonicalProduct);
269
+ const canonicalProductData = parsePrimaryShopifyProductData(canonicalProduct);
270
+ const description =
271
+ typeof canonicalProductData?.description === 'string' &&
272
+ canonicalProductData.description.trim().length > 0
273
+ ? canonicalProductData.description
274
+ : undefined;
275
+ if (!deposit) {
276
+ return null;
277
+ }
278
+ return {
279
+ ...deposit,
280
+ servicesCount: sharedServices.length,
281
+ description,
282
+ };
283
+ }
284
+
285
+ export function buildShopifyCheckoutLines(
286
+ cart: Record<string, CartItemState>,
287
+ resources: ResourceNode[]
288
+ ): ShopifyCheckoutLine[] {
289
+ const lines: ShopifyCheckoutLine[] = [];
290
+ const cartItems = Object.values(cart);
291
+ const sharedFeeServices: ResourceNode[] = [];
292
+ const sharedFeeServiceIds = new Set<string>();
293
+
294
+ cartItems.forEach((item) => {
295
+ const resource = resources.find((r) => r.id === item.resourceId);
296
+ if (isSharedFeeService(resource, resources) && resource) {
297
+ sharedFeeServices.push(resource);
298
+ sharedFeeServiceIds.add(resource.id);
299
+ }
300
+ });
301
+
302
+ if (sharedFeeServices.length > 0) {
303
+ const canonicalProduct = getServiceLinkedProduct(
304
+ sharedFeeServices[0],
305
+ resources
306
+ );
307
+ const deposit = parseDepositFromProductResource(canonicalProduct);
308
+ if (deposit?.variantId) {
309
+ lines.push({
310
+ merchandiseId: deposit.variantId,
311
+ quantity: 1,
312
+ attributes: [formatServicesAttribute(sharedFeeServices)],
313
+ });
314
+ }
315
+ }
316
+
317
+ cartItems.forEach((item) => {
318
+ const resource = resources.find((r) => r.id === item.resourceId);
319
+ if (!resource) return;
320
+ if (sharedFeeServiceIds.has(resource.id)) return;
321
+ const nonSharedServiceVariant =
322
+ resource.categorySlug === 'service'
323
+ ? getServiceVariantIdFromCanonicalProduct(resource, resources)
324
+ : undefined;
325
+ const merchandiseId = item.variantId || nonSharedServiceVariant;
326
+ if (!merchandiseId) return;
327
+ lines.push({
328
+ merchandiseId,
329
+ quantity: item.quantity || 1,
330
+ });
331
+ });
332
+
333
+ return lines;
334
+ }
335
+
336
+ export function hasGidBackedCheckout(
337
+ cart: Record<string, CartItemState>,
338
+ resources: ResourceNode[]
339
+ ): boolean {
340
+ const cartItems = Object.values(cart);
341
+ return cartItems.some((item) => {
342
+ const resource = resources.find((r) => r.id === item.resourceId);
343
+ if (!resource) return false;
344
+ return (
345
+ typeof resource.optionsPayload?.gid === 'string' &&
346
+ !!resource.optionsPayload.gid
347
+ );
348
+ });
349
+ }
350
+
351
+ export function getCartIconCount(
352
+ cart: Record<string, CartItemState>,
353
+ resources: ResourceNode[]
354
+ ): number {
355
+ const cartValues = Object.values(cart);
356
+ const boundServiceIds = new Set(
357
+ cartValues.map((item) => item.boundResourceId).filter(Boolean)
358
+ );
359
+ let sharedFeeAdded = false;
360
+
361
+ return cartValues
362
+ .filter((item) => !boundServiceIds.has(item.resourceId))
363
+ .reduce((total, item) => {
364
+ const resource = resources.find((r) => r.id === item.resourceId);
365
+ if (isSharedFeeService(resource, resources)) {
366
+ if (sharedFeeAdded) return total;
367
+ sharedFeeAdded = true;
368
+ return total + 1;
369
+ }
370
+ return total + item.quantity;
371
+ }, 0);
372
+ }
@@ -753,10 +753,6 @@ export async function injectTemplateFiles(
753
753
  src: resolve('../templates/src/utils/actions/preParse_Action.ts'),
754
754
  dest: 'src/utils/actions/preParse_Action.ts',
755
755
  },
756
- {
757
- src: resolve('../templates/src/utils/actions/preParse_Clicked.ts'),
758
- dest: 'src/utils/actions/preParse_Clicked.ts',
759
- },
760
756
  {
761
757
  src: resolve('../templates/src/utils/actions/preParse_Impression.ts'),
762
758
  dest: 'src/utils/actions/preParse_Impression.ts',
@@ -826,6 +822,10 @@ export async function injectTemplateFiles(
826
822
  src: resolve('../templates/src/utils/api/bookingHelpers.ts'),
827
823
  dest: 'src/utils/api/bookingHelpers.ts',
828
824
  },
825
+ {
826
+ src: resolve('../templates/src/utils/api/salesHelpers.ts'),
827
+ dest: 'src/utils/api/salesHelpers.ts',
828
+ },
829
829
  {
830
830
  src: resolve('../templates/src/utils/api/menuHelpers.ts'),
831
831
  dest: 'src/utils/api/menuHelpers.ts',
@@ -900,6 +900,14 @@ export async function injectTemplateFiles(
900
900
  src: resolve('../templates/custom/shopify/cart.astro'),
901
901
  dest: 'src/pages/cart.astro',
902
902
  },
903
+ {
904
+ src: resolve('../templates/src/pages/privacy.astro'),
905
+ dest: 'src/pages/privacy.astro',
906
+ },
907
+ {
908
+ src: resolve('../templates/src/pages/terms.astro'),
909
+ dest: 'src/pages/terms.astro',
910
+ },
903
911
  {
904
912
  src: resolve('../templates/src/pages/404.astro'),
905
913
  dest: 'src/pages/404.astro',
@@ -956,6 +964,14 @@ export async function injectTemplateFiles(
956
964
  src: resolve('../templates/src/pages/api/booking/list.ts'),
957
965
  dest: 'src/pages/api/booking/list.ts',
958
966
  },
967
+ {
968
+ src: resolve('../templates/src/pages/api/sales/list.ts'),
969
+ dest: 'src/pages/api/sales/list.ts',
970
+ },
971
+ {
972
+ src: resolve('../templates/src/pages/api/sales/metrics.ts'),
973
+ dest: 'src/pages/api/sales/metrics.ts',
974
+ },
959
975
  {
960
976
  src: resolve('../templates/src/pages/api/booking/metrics.ts'),
961
977
  dest: 'src/pages/api/booking/metrics.ts',
@@ -1000,6 +1016,22 @@ export async function injectTemplateFiles(
1000
1016
  src: resolve('../templates/src/pages/api/auth/logout.ts'),
1001
1017
  dest: 'src/pages/api/auth/logout.ts',
1002
1018
  },
1019
+ {
1020
+ src: resolve('../templates/src/pages/api/google/oauth/start.ts'),
1021
+ dest: 'src/pages/api/google/oauth/start.ts',
1022
+ },
1023
+ {
1024
+ src: resolve('../templates/src/pages/api/google/oauth/status.ts'),
1025
+ dest: 'src/pages/api/google/oauth/status.ts',
1026
+ },
1027
+ {
1028
+ src: resolve('../templates/src/pages/api/google/oauth/disconnect.ts'),
1029
+ dest: 'src/pages/api/google/oauth/disconnect.ts',
1030
+ },
1031
+ {
1032
+ src: resolve('../templates/src/pages/api/google/oauth/callback.ts'),
1033
+ dest: 'src/pages/api/google/oauth/callback.ts',
1034
+ },
1003
1035
  {
1004
1036
  src: resolve('../templates/src/pages/api/orphan-analysis.ts'),
1005
1037
  dest: 'src/pages/api/orphan-analysis.ts',
@@ -1264,6 +1296,12 @@ export async function injectTemplateFiles(
1264
1296
  ),
1265
1297
  dest: 'src/components/storykeep/shopify/ShopifyDashboard_Emails.tsx',
1266
1298
  },
1299
+ {
1300
+ src: resolve(
1301
+ '../templates/src/components/storykeep/shopify/ShopifyDashboard_Sales.tsx'
1302
+ ),
1303
+ dest: 'src/components/storykeep/shopify/ShopifyDashboard_Sales.tsx',
1304
+ },
1267
1305
  {
1268
1306
  src: resolve(
1269
1307
  '../templates/src/components/storykeep/email-builder/EmailBuilder.tsx'
@@ -2351,6 +2389,11 @@ export async function injectTemplateFiles(
2351
2389
  dest: 'src/utils/customHelpers.ts',
2352
2390
  protected: true,
2353
2391
  },
2392
+ {
2393
+ src: resolve('../templates/src/utils/booking/appointmentMode.ts'),
2394
+ dest: 'src/utils/booking/appointmentMode.ts',
2395
+ protected: true,
2396
+ },
2354
2397
  {
2355
2398
  src: resolve('../templates/custom/shopify/ShopifyProductGrid.tsx'),
2356
2399
  dest: 'src/custom/shopify/ShopifyProductGrid.tsx',
@@ -1,101 +0,0 @@
1
- import { useState, useEffect } from 'react';
2
- import { Dialog } from '@ark-ui/react/dialog';
3
- import { Portal } from '@ark-ui/react/portal';
4
- import XMarkIcon from '@heroicons/react/24/solid/XMarkIcon';
5
- import { ProfileStorage } from '@/utils/profileStorage';
6
- import SandboxRegisterForm from '@/components/codehooks/SandboxRegisterForm';
7
-
8
- interface SandboxAuthWrapperProps {
9
- isServerSideAuthenticated: boolean;
10
- }
11
-
12
- export default function SandboxAuthWrapper({
13
- isServerSideAuthenticated,
14
- }: SandboxAuthWrapperProps) {
15
- const [profileExists, setProfileExists] = useState<boolean | null>(null);
16
-
17
- useEffect(() => {
18
- const hasLocalProfile = ProfileStorage.hasProfile();
19
-
20
- if (hasLocalProfile && !isServerSideAuthenticated) {
21
- const token = localStorage.getItem('tractstack_profile_token');
22
-
23
- if (token) {
24
- ProfileStorage.storeProfileToken(token);
25
- window.location.reload();
26
- return;
27
- } else {
28
- ProfileStorage.clearProfile();
29
- setProfileExists(false);
30
- }
31
- } else {
32
- setProfileExists(hasLocalProfile);
33
- }
34
- }, [isServerSideAuthenticated]);
35
-
36
- const handleRegistrationSuccess = () => {
37
- setProfileExists(true);
38
- window.location.reload();
39
- };
40
-
41
- const handleClose = () => {
42
- window.location.href = '/';
43
- };
44
-
45
- if (profileExists === true && isServerSideAuthenticated) {
46
- return null;
47
- }
48
-
49
- if (profileExists === null) {
50
- return null;
51
- }
52
-
53
- return (
54
- <Dialog.Root open={true} modal={true} trapFocus={false}>
55
- <Portal>
56
- <Dialog.Backdrop
57
- className="fixed inset-0 bg-black bg-opacity-75"
58
- style={{ zIndex: 9005 }}
59
- />
60
- <Dialog.Positioner
61
- className="fixed inset-0 flex items-center justify-center p-4"
62
- style={{ zIndex: 9005 }}
63
- >
64
- <Dialog.Content className="relative grid w-full max-w-6xl grid-cols-1 overflow-hidden rounded-lg bg-white shadow-2xl md:grid-cols-2">
65
- <button
66
- onClick={handleClose}
67
- className="absolute right-4 top-4 z-10 rounded-full bg-gray-100 p-2 text-gray-600 shadow-sm transition-colors hover:bg-gray-200"
68
- title="Close and exit Sandbox"
69
- >
70
- <XMarkIcon className="h-5 w-5" />
71
- </button>
72
-
73
- <div className="flex flex-col justify-center bg-gray-50 p-8 text-right">
74
- <h2 className="text-4xl font-bold text-gray-900 md:text-5xl">
75
- Press <span className="italic text-blue-600">your own</span>{' '}
76
- Tract Stack
77
- </h2>
78
- <p className="mt-4 text-lg text-gray-600">
79
- Create an interactive webpage in a sandbox! No credit card
80
- required.
81
- </p>
82
- <p className="mt-8 text-sm text-gray-500">
83
- Already connected?{' '}
84
- <a
85
- href="/storykeep/profile"
86
- className="font-bold text-blue-600 underline hover:text-blue-500"
87
- >
88
- Unlock your profile
89
- </a>
90
- </p>
91
- </div>
92
-
93
- <div className="flex flex-col justify-center p-8">
94
- <SandboxRegisterForm onSuccess={handleRegistrationSuccess} />
95
- </div>
96
- </Dialog.Content>
97
- </Dialog.Positioner>
98
- </Portal>
99
- </Dialog.Root>
100
- );
101
- }
@@ -1,103 +0,0 @@
1
- import { preParseClicked } from './preParse_Clicked';
2
- import type { BrandConfig } from '@/types/tractstack';
3
-
4
- interface ActionButtonParams {
5
- callbackPayload: any;
6
- targetUrl: string;
7
- paneId: string;
8
- config: BrandConfig;
9
- }
10
-
11
- // Import the sendAnalyticsEvent function to send events to backend
12
- async function sendAnalyticsEvent(event: {
13
- contentId: string;
14
- contentType: 'Pane' | 'StoryFragment';
15
- eventVerb: string;
16
- duration?: number;
17
- }): Promise<void> {
18
- try {
19
- const config = window.TRACTSTACK_CONFIG;
20
- if (!config || !config.sessionId) return;
21
- const backendUrl =
22
- import.meta.env.PUBLIC_GO_BACKEND || 'http://localhost:8080';
23
-
24
- const sessionId = config.sessionId;
25
- const formData: { [key: string]: string } = {
26
- beliefId: event.contentId,
27
- beliefType: event.contentType,
28
- beliefValue: event.eventVerb,
29
- paneId: '',
30
- };
31
-
32
- if (event.duration !== undefined) {
33
- formData.duration = event.duration.toString();
34
- }
35
-
36
- await fetch(`${backendUrl}/api/v1/state`, {
37
- method: 'POST',
38
- headers: {
39
- 'Content-Type': 'application/x-www-form-urlencoded',
40
- 'X-Tenant-ID': config.tenantId,
41
- 'X-TractStack-Session-ID': sessionId,
42
- 'X-StoryFragment-ID': config.storyfragmentId,
43
- },
44
- body: new URLSearchParams(formData),
45
- });
46
- } catch (error) {
47
- console.error('⛔ API ERROR: Analytics event failed', error, event);
48
- }
49
- }
50
-
51
- export function handleActionButtonClick({
52
- callbackPayload,
53
- targetUrl,
54
- paneId,
55
- config,
56
- }: ActionButtonParams): void {
57
- const event = preParseClicked(paneId, callbackPayload, config);
58
-
59
- if (event) {
60
- console.log(event);
61
- sendAnalyticsEvent({
62
- contentId: event.targetId || event.targetSlug || event.id,
63
- contentType: 'Pane',
64
- eventVerb: event.verb,
65
- });
66
- }
67
-
68
- // Handle URL navigation and scroll
69
- if (targetUrl.startsWith('#') || targetUrl.includes('#')) {
70
- const id = targetUrl.split('#')[1];
71
- const element = document.getElementById(id);
72
-
73
- if (element) {
74
- // Calculate the target position
75
- const elementRect = element.getBoundingClientRect();
76
- const targetPosition = elementRect.top + window.scrollY;
77
-
78
- // Perform smooth scroll
79
- window.scrollTo({
80
- top: targetPosition,
81
- behavior: 'smooth',
82
- });
83
-
84
- // After scrolling, ensure the page layout is preserved
85
- const checkScrollEnd = setInterval(() => {
86
- if (
87
- window.scrollY === targetPosition ||
88
- Math.abs(window.scrollY - targetPosition) < 2
89
- ) {
90
- clearInterval(checkScrollEnd);
91
- document.body.style.minHeight = `${Math.max(
92
- document.body.scrollHeight,
93
- document.documentElement.scrollHeight
94
- )}px`;
95
- }
96
- }, 100);
97
- } else {
98
- window.location.href = targetUrl;
99
- }
100
- } else {
101
- window.location.href = targetUrl;
102
- }
103
- }
@@ -1,87 +0,0 @@
1
- /* eslint-disable @typescript-eslint/no-explicit-any */
2
- import type { BrandConfig } from '@/types/tractstack';
3
-
4
- export const preParseClicked = (
5
- id: string,
6
- payload: any,
7
- config: BrandConfig
8
- ) => {
9
- const thisPayload = (payload && payload[0]) || false;
10
-
11
- if (!thisPayload || !config?.HOME_SLUG) {
12
- return null;
13
- }
14
-
15
- const command = (thisPayload && thisPayload[0] && thisPayload[0][0]) || null;
16
- const parameters =
17
- (thisPayload && thisPayload[0] && thisPayload[0][1]) || null;
18
-
19
- if (command === 'bunnyMoment' && parameters) {
20
- const videoId = parameters[0];
21
- return {
22
- id: id,
23
- type: `StartVideoMoment`,
24
- verb: `WATCHED`,
25
- targetId: videoId || null,
26
- };
27
- }
28
-
29
- if (command === `goto` && parameters) {
30
- const parameterOne = parameters[0] || null;
31
- const parameterTwo = parameters[1] || null;
32
- //const parameterThree = parameters[2] || null;
33
-
34
- switch (parameterOne) {
35
- case `home`:
36
- return {
37
- id: id,
38
- type: `PaneClicked`,
39
- verb: `CLICKED`,
40
- targetSlug: config?.HOME_SLUG,
41
- };
42
-
43
- case `storyFragment`:
44
- case `storyFragmentPane`:
45
- return {
46
- id: id,
47
- type: `PaneClicked`,
48
- verb: `CLICKED`,
49
- targetSlug: parameterTwo,
50
- };
51
-
52
- case `bunny`:
53
- return {
54
- id: id,
55
- type: `StartVideo`,
56
- verb: `WATCHED`,
57
- targetSlug: parameterTwo,
58
- };
59
-
60
- case `sandbox`:
61
- return {
62
- id: id,
63
- type: `SandboxAction`,
64
- verb: `CLICKED`,
65
- targetSlug: parameterTwo || 'main',
66
- };
67
-
68
- case `storykeep`:
69
- case `context`:
70
- case `concierge`:
71
- case `product`:
72
- case `url`:
73
- // ignore these
74
- break;
75
-
76
- default:
77
- console.log(
78
- `LispActionPayload preParseEvent misfire`,
79
- command,
80
- parameters
81
- );
82
- break;
83
- }
84
- }
85
-
86
- return null;
87
- };