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.
- package/bin/create-tractstack.js +7 -4
- package/dist/index.js +51 -8
- package/package.json +1 -1
- package/templates/custom/shopify/Cart.tsx +279 -118
- package/templates/custom/shopify/CartIcon.tsx +8 -8
- package/templates/custom/shopify/CheckoutModal.tsx +328 -65
- package/templates/custom/shopify/ShopifyCartManager.tsx +117 -60
- package/templates/custom/shopify/ShopifyCheckout.tsx +2 -26
- package/templates/custom/shopify/ShopifyProductGrid.tsx +8 -0
- package/templates/custom/shopify/ShopifyServiceList.tsx +25 -37
- package/templates/custom/shopify/cart.astro +7 -1
- package/templates/src/components/Header.astro +4 -2
- package/templates/src/components/compositor/Node.tsx +39 -9
- package/templates/src/components/compositor/nodes/CreativePane.tsx +175 -88
- package/templates/src/components/edit/pane/AddPanePanel.tsx +2 -2
- package/templates/src/components/edit/pane/AddPanePanel_new.tsx +6 -5
- package/templates/src/components/edit/pane/AddPanePanel_reuse.tsx +9 -15
- package/templates/src/components/edit/pane/ConfigPanePanel.tsx +1 -1
- package/templates/src/components/form/advanced/APIConfigSection.tsx +249 -4
- package/templates/src/components/form/brand/SiteConfigSection.tsx +9 -0
- package/templates/src/components/form/shopify/SchedulingSection.tsx +44 -0
- package/templates/src/components/storykeep/Dashboard_Advanced.tsx +66 -21
- package/templates/src/components/storykeep/Dashboard_Shopify.tsx +266 -18
- package/templates/src/components/storykeep/controls/content/ManageContent.tsx +1 -0
- package/templates/src/components/storykeep/controls/content/ProductTable.tsx +38 -24
- package/templates/src/components/storykeep/controls/content/ResourceForm.tsx +240 -65
- package/templates/src/components/storykeep/shopify/ShopifyDashboard.tsx +175 -48
- package/templates/src/components/storykeep/shopify/ShopifyDashboard_Bookings.tsx +91 -10
- package/templates/src/components/storykeep/shopify/ShopifyDashboard_Sales.tsx +479 -0
- package/templates/src/components/storykeep/shopify/ShopifyDashboard_Search.tsx +7 -3
- package/templates/src/constants.ts +2 -0
- package/templates/src/layouts/Layout.astro +26 -0
- package/templates/src/pages/api/auth/logout.ts +35 -2
- 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/api/sales/list.ts +66 -0
- package/templates/src/pages/api/sales/metrics.ts +60 -0
- package/templates/src/pages/context/[...contextSlug].astro +50 -31
- package/templates/src/pages/privacy.astro +84 -0
- package/templates/src/pages/storykeep/advanced.astro +4 -1
- package/templates/src/pages/terms.astro +47 -0
- package/templates/src/stores/nodes.ts +8 -0
- package/templates/src/stores/shopify.ts +5 -0
- package/templates/src/types/tractstack.ts +87 -0
- package/templates/src/utils/api/advancedConfig.ts +2 -1
- package/templates/src/utils/api/advancedHelpers.ts +20 -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 +14 -1
- package/templates/src/utils/api/salesHelpers.ts +21 -0
- package/templates/src/utils/booking/appointmentMode.ts +135 -0
- package/templates/src/utils/customHelpers.ts +287 -2
- package/utils/inject-files.ts +47 -4
- 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
|
@@ -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 = ['
|
|
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
|
+
}
|
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',
|
|
@@ -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
|
-
};
|