astro-tractstack 2.3.1 → 2.3.3
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 +3 -3
- package/dist/index.js +69 -11
- package/package.json +1 -1
- package/templates/custom/shopify/Cart.tsx +99 -19
- package/templates/custom/shopify/CheckoutModal.tsx +196 -10
- package/templates/custom/shopify/ShopifyCartManager.tsx +79 -76
- package/templates/custom/shopify/ShopifyCheckout.tsx +4 -33
- package/templates/custom/shopify/ShopifyProductGrid.tsx +42 -14
- package/templates/custom/shopify/ShopifyServiceList.tsx +94 -50
- package/templates/custom/shopify/cart.astro +7 -1
- package/templates/src/components/Footer.astro +2 -2
- package/templates/src/components/Header.astro +17 -9
- package/templates/src/components/Menu.tsx +157 -135
- package/templates/src/components/codehooks/BunnyVideoSetup.tsx +2 -2
- package/templates/src/components/codehooks/EpinetDurationSelector.tsx +27 -6
- package/templates/src/components/codehooks/EpinetTableView.tsx +153 -112
- package/templates/src/components/codehooks/EpinetWrapper.tsx +4 -1
- package/templates/src/components/codehooks/FeaturedArticleSetup.tsx +8 -1
- package/templates/src/components/codehooks/ProductCardSetup.tsx +9 -1
- package/templates/src/components/codehooks/ProductGridSetup.tsx +9 -1
- package/templates/src/components/compositor/nodes/BgPaneWrapper.tsx +2 -1
- package/templates/src/components/compositor/nodes/GhostInsertBlock.tsx +1 -1
- package/templates/src/components/edit/ToolBar.tsx +2 -1
- package/templates/src/components/edit/context/ContextPaneConfig_slug.tsx +2 -2
- package/templates/src/components/edit/pane/AddPanePanel_codehook.tsx +13 -0
- package/templates/src/components/edit/pane/AddPanePanel_newCustomCopy.tsx +2 -2
- package/templates/src/components/edit/pane/ConfigPanePanel.tsx +1 -1
- package/templates/src/components/edit/state/SaveModal.tsx +1 -1
- package/templates/src/components/edit/widgets/InteractiveDisclosureWidget.tsx +8 -3
- package/templates/src/components/form/DateTimeInput.tsx +10 -3
- package/templates/src/components/form/FileUpload.tsx +11 -5
- package/templates/src/components/form/NumberInput.tsx +2 -2
- package/templates/src/components/form/advanced/APIConfigSection.tsx +221 -39
- package/templates/src/components/form/brand/SiteConfigSection.tsx +10 -0
- package/templates/src/components/form/shopify/SchedulingSection.tsx +44 -0
- package/templates/src/components/storykeep/Dashboard_Advanced.tsx +10 -1
- package/templates/src/components/storykeep/Dashboard_Shopify.tsx +16 -8
- package/templates/src/components/storykeep/controls/content/BeliefForm.tsx +2 -2
- package/templates/src/components/storykeep/controls/content/ManageContent.tsx +1 -0
- package/templates/src/components/storykeep/controls/content/ProductTable.tsx +2 -2
- package/templates/src/components/storykeep/controls/content/ResourceBulkIngest.tsx +79 -51
- package/templates/src/components/storykeep/controls/content/ResourceForm.tsx +80 -0
- package/templates/src/components/storykeep/controls/content/ResourceTable.tsx +1 -0
- package/templates/src/components/storykeep/email-builder/Blocks.tsx +169 -0
- package/templates/src/components/storykeep/email-builder/EmailBuilder.tsx +223 -0
- package/templates/src/components/storykeep/email-builder/PreviewModal.tsx +136 -0
- package/templates/src/components/storykeep/email-builder/PropertyPanel.tsx +154 -0
- package/templates/src/components/storykeep/shopify/ShopifyDashboard.tsx +1 -8
- package/templates/src/components/storykeep/shopify/ShopifyDashboard_Bookings.tsx +118 -14
- package/templates/src/components/storykeep/shopify/ShopifyDashboard_Emails.tsx +105 -0
- package/templates/src/constants.ts +2 -0
- package/templates/src/layouts/Layout.astro +8 -5
- package/templates/src/pages/api/google/oauth/callback.ts +50 -0
- package/templates/src/pages/api/google/oauth/disconnect.ts +32 -0
- package/templates/src/pages/api/google/oauth/start.ts +32 -0
- package/templates/src/pages/api/google/oauth/status.ts +32 -0
- package/templates/src/pages/privacy.astro +84 -0
- package/templates/src/pages/terms.astro +47 -0
- package/templates/src/stores/shopify.ts +21 -0
- package/templates/src/types/formTypes.ts +4 -2
- package/templates/src/types/tractstack.ts +35 -2
- package/templates/src/utils/api/advancedHelpers.ts +16 -0
- package/templates/src/utils/api/bookingHelpers.ts +3 -1
- package/templates/src/utils/api/brandConfig.ts +2 -0
- package/templates/src/utils/api/brandHelpers.ts +24 -1
- package/templates/src/utils/api/emailHelpers.ts +105 -0
- package/templates/src/utils/booking/appointmentMode.ts +135 -0
- package/templates/src/utils/customHelpers.ts +2 -0
- package/templates/src/utils/tenantResolver.ts +1 -1
- package/utils/inject-files.ts +63 -5
- package/templates/src/components/codehooks/SandboxAuthWrapper.tsx +0 -101
- package/templates/src/utils/actions/actionButton.ts +0 -103
- package/templates/src/utils/actions/preParse_Clicked.ts +0 -87
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import type { ResourceNode } from '@/types/compositorTypes';
|
|
2
|
+
import type { CartItemState } from '@/stores/shopify';
|
|
3
|
+
|
|
4
|
+
export type AppointmentMode = 'IN_PERSON' | 'REMOTE';
|
|
5
|
+
|
|
6
|
+
export type AppointmentSchedulingInput = {
|
|
7
|
+
allowRemote?: boolean;
|
|
8
|
+
remoteOnly?: boolean;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type AppointmentModeConstraints = {
|
|
12
|
+
serviceResources: ResourceNode[];
|
|
13
|
+
anyServiceRemoteOnly: boolean;
|
|
14
|
+
someServiceForcesInPerson: boolean;
|
|
15
|
+
allServicesAllowRemote: boolean;
|
|
16
|
+
effectiveRemoteOnly: boolean;
|
|
17
|
+
effectiveAllowRemote: boolean;
|
|
18
|
+
remoteAvailable: boolean;
|
|
19
|
+
inPersonAvailable: boolean;
|
|
20
|
+
hasImpossibleRemoteMix: boolean;
|
|
21
|
+
/** Same as `remoteAvailable`; kept for cart naming parity */
|
|
22
|
+
canRemote: boolean;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Collects service resources involved in booking (same rules as Cart / CheckoutModal).
|
|
27
|
+
*/
|
|
28
|
+
export function collectBookingServiceResources(
|
|
29
|
+
cart: Record<string, CartItemState>,
|
|
30
|
+
resources: ResourceNode[]
|
|
31
|
+
): ResourceNode[] {
|
|
32
|
+
const dedupe = new Map<string, ResourceNode>();
|
|
33
|
+
for (const item of Object.values(cart)) {
|
|
34
|
+
const resource = resources.find((r) => r.id === item.resourceId);
|
|
35
|
+
if (
|
|
36
|
+
resource &&
|
|
37
|
+
(resource.categorySlug === 'service' ||
|
|
38
|
+
resource.optionsPayload?.bookingLengthMinutes)
|
|
39
|
+
) {
|
|
40
|
+
dedupe.set(resource.id, resource);
|
|
41
|
+
}
|
|
42
|
+
if (item.boundResourceId) {
|
|
43
|
+
const bound = resources.find((r) => r.id === item.boundResourceId);
|
|
44
|
+
if (bound) {
|
|
45
|
+
dedupe.set(bound.id, bound);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return Array.from(dedupe.values());
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Returns true if the cart would mix remote-only services with in-person-only services.
|
|
54
|
+
*/
|
|
55
|
+
export function wouldCartHaveImpossibleRemoteMix(
|
|
56
|
+
nextCart: Record<string, CartItemState>,
|
|
57
|
+
resources: ResourceNode[]
|
|
58
|
+
): boolean {
|
|
59
|
+
const svc = collectBookingServiceResources(nextCart, resources);
|
|
60
|
+
const anyRemoteOnly = svc.some((r) => Boolean(r.optionsPayload?.remoteOnly));
|
|
61
|
+
const someInPersonOnly = svc.some(
|
|
62
|
+
(r) =>
|
|
63
|
+
!Boolean(r.optionsPayload?.remoteOnly) &&
|
|
64
|
+
!Boolean(r.optionsPayload?.allowRemote)
|
|
65
|
+
);
|
|
66
|
+
return anyRemoteOnly && someInPersonOnly;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Single source of truth for tenant + per-service remote eligibility.
|
|
71
|
+
*/
|
|
72
|
+
export function deriveAppointmentConstraints(
|
|
73
|
+
cart: Record<string, CartItemState>,
|
|
74
|
+
resources: ResourceNode[],
|
|
75
|
+
tenantScheduling: AppointmentSchedulingInput
|
|
76
|
+
): AppointmentModeConstraints {
|
|
77
|
+
const serviceResources = collectBookingServiceResources(cart, resources);
|
|
78
|
+
const allowRemote = Boolean(tenantScheduling.allowRemote);
|
|
79
|
+
const tenantRemoteOnly = Boolean(tenantScheduling.remoteOnly);
|
|
80
|
+
|
|
81
|
+
const anyServiceRemoteOnly = serviceResources.some((r) =>
|
|
82
|
+
Boolean(r.optionsPayload?.remoteOnly)
|
|
83
|
+
);
|
|
84
|
+
const someServiceForcesInPerson = serviceResources.some(
|
|
85
|
+
(r) =>
|
|
86
|
+
!Boolean(r.optionsPayload?.remoteOnly) &&
|
|
87
|
+
!Boolean(r.optionsPayload?.allowRemote)
|
|
88
|
+
);
|
|
89
|
+
const allServicesAllowRemote =
|
|
90
|
+
serviceResources.length === 0 ||
|
|
91
|
+
serviceResources.every(
|
|
92
|
+
(r) =>
|
|
93
|
+
Boolean(r.optionsPayload?.remoteOnly) ||
|
|
94
|
+
Boolean(r.optionsPayload?.allowRemote)
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const effectiveRemoteOnly = tenantRemoteOnly || anyServiceRemoteOnly;
|
|
98
|
+
const effectiveAllowRemote = allowRemote || tenantRemoteOnly;
|
|
99
|
+
|
|
100
|
+
const remoteAvailable =
|
|
101
|
+
effectiveRemoteOnly ||
|
|
102
|
+
(effectiveAllowRemote &&
|
|
103
|
+
serviceResources.length > 0 &&
|
|
104
|
+
allServicesAllowRemote);
|
|
105
|
+
|
|
106
|
+
const inPersonAvailable = !effectiveRemoteOnly;
|
|
107
|
+
const hasImpossibleRemoteMix =
|
|
108
|
+
anyServiceRemoteOnly && someServiceForcesInPerson;
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
serviceResources,
|
|
112
|
+
anyServiceRemoteOnly,
|
|
113
|
+
someServiceForcesInPerson,
|
|
114
|
+
allServicesAllowRemote,
|
|
115
|
+
effectiveRemoteOnly,
|
|
116
|
+
effectiveAllowRemote,
|
|
117
|
+
remoteAvailable,
|
|
118
|
+
inPersonAvailable,
|
|
119
|
+
hasImpossibleRemoteMix,
|
|
120
|
+
canRemote: remoteAvailable,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function pickInitialAppointmentMode(
|
|
125
|
+
c: AppointmentModeConstraints,
|
|
126
|
+
currentPreferred: AppointmentMode
|
|
127
|
+
): AppointmentMode {
|
|
128
|
+
if (c.effectiveRemoteOnly) {
|
|
129
|
+
return 'REMOTE';
|
|
130
|
+
}
|
|
131
|
+
if (currentPreferred === 'REMOTE' && c.remoteAvailable) {
|
|
132
|
+
return 'REMOTE';
|
|
133
|
+
}
|
|
134
|
+
return 'IN_PERSON';
|
|
135
|
+
}
|
|
@@ -53,6 +53,8 @@ export const RESTRICTION_MESSAGES = {
|
|
|
53
53
|
TERMS: 'Please review the terms for this item before adding it to your cart.',
|
|
54
54
|
MAX_DURATION: (max: number) =>
|
|
55
55
|
`You cannot book more than ${max} minutes of services in one session.`,
|
|
56
|
+
INCOMPATIBLE_REMOTE:
|
|
57
|
+
'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
58
|
DEFAULT_ADD: (title: string) => `${title} has been added to your cart.`,
|
|
57
59
|
};
|
|
58
60
|
|
package/utils/inject-files.ts
CHANGED
|
@@ -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',
|
|
@@ -900,6 +896,14 @@ export async function injectTemplateFiles(
|
|
|
900
896
|
src: resolve('../templates/custom/shopify/cart.astro'),
|
|
901
897
|
dest: 'src/pages/cart.astro',
|
|
902
898
|
},
|
|
899
|
+
{
|
|
900
|
+
src: resolve('../templates/src/pages/privacy.astro'),
|
|
901
|
+
dest: 'src/pages/privacy.astro',
|
|
902
|
+
},
|
|
903
|
+
{
|
|
904
|
+
src: resolve('../templates/src/pages/terms.astro'),
|
|
905
|
+
dest: 'src/pages/terms.astro',
|
|
906
|
+
},
|
|
903
907
|
{
|
|
904
908
|
src: resolve('../templates/src/pages/404.astro'),
|
|
905
909
|
dest: 'src/pages/404.astro',
|
|
@@ -1000,6 +1004,22 @@ export async function injectTemplateFiles(
|
|
|
1000
1004
|
src: resolve('../templates/src/pages/api/auth/logout.ts'),
|
|
1001
1005
|
dest: 'src/pages/api/auth/logout.ts',
|
|
1002
1006
|
},
|
|
1007
|
+
{
|
|
1008
|
+
src: resolve('../templates/src/pages/api/google/oauth/start.ts'),
|
|
1009
|
+
dest: 'src/pages/api/google/oauth/start.ts',
|
|
1010
|
+
},
|
|
1011
|
+
{
|
|
1012
|
+
src: resolve('../templates/src/pages/api/google/oauth/status.ts'),
|
|
1013
|
+
dest: 'src/pages/api/google/oauth/status.ts',
|
|
1014
|
+
},
|
|
1015
|
+
{
|
|
1016
|
+
src: resolve('../templates/src/pages/api/google/oauth/disconnect.ts'),
|
|
1017
|
+
dest: 'src/pages/api/google/oauth/disconnect.ts',
|
|
1018
|
+
},
|
|
1019
|
+
{
|
|
1020
|
+
src: resolve('../templates/src/pages/api/google/oauth/callback.ts'),
|
|
1021
|
+
dest: 'src/pages/api/google/oauth/callback.ts',
|
|
1022
|
+
},
|
|
1003
1023
|
{
|
|
1004
1024
|
src: resolve('../templates/src/pages/api/orphan-analysis.ts'),
|
|
1005
1025
|
dest: 'src/pages/api/orphan-analysis.ts',
|
|
@@ -1258,6 +1278,36 @@ export async function injectTemplateFiles(
|
|
|
1258
1278
|
),
|
|
1259
1279
|
dest: 'src/components/storykeep/shopify/ShopifyDashboard_Services.tsx',
|
|
1260
1280
|
},
|
|
1281
|
+
{
|
|
1282
|
+
src: resolve(
|
|
1283
|
+
'../templates/src/components/storykeep/shopify/ShopifyDashboard_Emails.tsx'
|
|
1284
|
+
),
|
|
1285
|
+
dest: 'src/components/storykeep/shopify/ShopifyDashboard_Emails.tsx',
|
|
1286
|
+
},
|
|
1287
|
+
{
|
|
1288
|
+
src: resolve(
|
|
1289
|
+
'../templates/src/components/storykeep/email-builder/EmailBuilder.tsx'
|
|
1290
|
+
),
|
|
1291
|
+
dest: 'src/components/storykeep/email-builder/EmailBuilder.tsx',
|
|
1292
|
+
},
|
|
1293
|
+
{
|
|
1294
|
+
src: resolve(
|
|
1295
|
+
'../templates/src/components/storykeep/email-builder/Blocks.tsx'
|
|
1296
|
+
),
|
|
1297
|
+
dest: 'src/components/storykeep/email-builder/Blocks.tsx',
|
|
1298
|
+
},
|
|
1299
|
+
{
|
|
1300
|
+
src: resolve(
|
|
1301
|
+
'../templates/src/components/storykeep/email-builder/PropertyPanel.tsx'
|
|
1302
|
+
),
|
|
1303
|
+
dest: 'src/components/storykeep/email-builder/PropertyPanel.tsx',
|
|
1304
|
+
},
|
|
1305
|
+
{
|
|
1306
|
+
src: resolve(
|
|
1307
|
+
'../templates/src/components/storykeep/email-builder/PreviewModal.tsx'
|
|
1308
|
+
),
|
|
1309
|
+
dest: 'src/components/storykeep/email-builder/PreviewModal.tsx',
|
|
1310
|
+
},
|
|
1261
1311
|
{
|
|
1262
1312
|
src: resolve(
|
|
1263
1313
|
'../templates/src/components/storykeep/shopify/ShopifyDashboard_Search.tsx'
|
|
@@ -2267,6 +2317,10 @@ export async function injectTemplateFiles(
|
|
|
2267
2317
|
src: resolve('../templates/src/utils/api/setupHelpers.ts'),
|
|
2268
2318
|
dest: 'src/utils/api/setupHelpers.ts',
|
|
2269
2319
|
},
|
|
2320
|
+
{
|
|
2321
|
+
src: resolve('../templates/src/utils/api/emailHelpers.ts'),
|
|
2322
|
+
dest: 'src/utils/api/emailHelpers.ts',
|
|
2323
|
+
},
|
|
2270
2324
|
{
|
|
2271
2325
|
src: resolve(
|
|
2272
2326
|
'../templates/src/components/storykeep/widgets/HydrateWizard.tsx'
|
|
@@ -2317,6 +2371,11 @@ export async function injectTemplateFiles(
|
|
|
2317
2371
|
dest: 'src/utils/customHelpers.ts',
|
|
2318
2372
|
protected: true,
|
|
2319
2373
|
},
|
|
2374
|
+
{
|
|
2375
|
+
src: resolve('../templates/src/utils/booking/appointmentMode.ts'),
|
|
2376
|
+
dest: 'src/utils/booking/appointmentMode.ts',
|
|
2377
|
+
protected: true,
|
|
2378
|
+
},
|
|
2320
2379
|
{
|
|
2321
2380
|
src: resolve('../templates/custom/shopify/ShopifyProductGrid.tsx'),
|
|
2322
2381
|
dest: 'src/custom/shopify/ShopifyProductGrid.tsx',
|
|
@@ -2465,7 +2524,6 @@ function createPlaceholder(filePath: string): string {
|
|
|
2465
2524
|
|
|
2466
2525
|
if (filePath.endsWith('.tsx')) {
|
|
2467
2526
|
return `// TractStack placeholder component
|
|
2468
|
-
import React from 'react';
|
|
2469
2527
|
export default function Placeholder() {
|
|
2470
2528
|
return <div>TractStack placeholder: ${filePath}</div>;
|
|
2471
2529
|
}`;
|
|
@@ -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
|
-
};
|