astro-tractstack 2.2.10 → 2.3.1
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/README.md +1 -1
- package/bin/create-tractstack.js +2 -2
- package/dist/index.js +177 -18
- package/package.json +4 -2
- package/templates/custom/minimal/CodeHook.astro +22 -5
- package/templates/custom/shopify/Cart.tsx +372 -0
- package/templates/custom/shopify/CartIcon.tsx +47 -0
- package/templates/custom/shopify/CartModal.tsx +63 -0
- package/templates/custom/shopify/CheckoutModal.tsx +576 -0
- package/templates/custom/shopify/NativeBookingCalendar.tsx +375 -0
- package/templates/custom/shopify/ShopifyCartManager.tsx +200 -0
- package/templates/custom/shopify/ShopifyCheckout.tsx +167 -0
- package/templates/custom/shopify/ShopifyProductGrid.tsx +247 -0
- package/templates/custom/shopify/ShopifyServiceList.tsx +135 -0
- package/templates/custom/shopify/cart.astro +23 -0
- package/templates/custom/with-examples/CodeHook.astro +17 -1
- package/templates/custom/with-examples/ProductGrid.astro +1 -1
- package/templates/src/client/app.js +4 -2
- package/templates/src/components/Footer.astro +4 -4
- package/templates/src/components/Header.astro +44 -12
- package/templates/src/components/edit/pane/AddPanePanel_new.tsx +3 -3
- package/templates/src/components/edit/pane/AiRestylePaneModal.tsx +2 -2
- package/templates/src/components/edit/pane/steps/AiCreativeDesignStep.tsx +2 -2
- package/templates/src/components/edit/pane/steps/AiLibraryCopyStep.tsx +3 -3
- package/templates/src/components/edit/pane/steps/AiRefineDesignStep.tsx +2 -2
- package/templates/src/components/edit/pane/steps/AiStandardDesignStep.tsx +7 -7
- package/templates/src/components/form/advanced/APIConfigSection.tsx +407 -38
- package/templates/src/components/form/shopify/SchedulingSection.tsx +354 -0
- package/templates/src/components/storykeep/Dashboard.tsx +18 -4
- package/templates/src/components/storykeep/Dashboard_Advanced.tsx +1 -0
- package/templates/src/components/storykeep/Dashboard_Content.tsx +5 -96
- package/templates/src/components/storykeep/Dashboard_Shopify.tsx +668 -0
- package/templates/src/components/storykeep/StoryKeepBackdrop.astro +43 -23
- package/templates/src/components/storykeep/controls/content/BeliefTable.tsx +14 -5
- package/templates/src/components/storykeep/controls/content/ContentBrowser.tsx +0 -14
- package/templates/src/components/storykeep/controls/content/KnownResourceForm.tsx +36 -13
- package/templates/src/components/storykeep/controls/content/KnownResourceTable.tsx +5 -2
- package/templates/src/components/storykeep/controls/content/ManageContent.tsx +4 -11
- package/templates/src/components/storykeep/controls/content/MenuTable.tsx +14 -5
- package/templates/src/components/storykeep/controls/content/ProductTable.tsx +333 -0
- package/templates/src/components/storykeep/controls/content/ResourceBulkIngest.tsx +9 -5
- package/templates/src/components/storykeep/controls/content/ResourceForm.tsx +108 -8
- package/templates/src/components/storykeep/controls/content/ResourceTable.tsx +13 -4
- package/templates/src/components/storykeep/controls/content/StoryFragmentTable.tsx +14 -5
- package/templates/src/components/storykeep/shopify/ShopifyDashboard.tsx +111 -0
- package/templates/src/components/storykeep/shopify/ShopifyDashboard_Bookings.tsx +393 -0
- package/templates/src/components/storykeep/shopify/ShopifyDashboard_Products.tsx +46 -0
- package/templates/src/components/storykeep/shopify/ShopifyDashboard_Schedule.tsx +78 -0
- package/templates/src/components/storykeep/shopify/ShopifyDashboard_Search.tsx +55 -0
- package/templates/src/components/storykeep/shopify/ShopifyDashboard_Services.tsx +47 -0
- package/templates/src/lib/resources.ts +11 -21
- package/templates/src/pages/api/auth/lookup-lead.ts +72 -0
- package/templates/src/pages/api/booking/availability.ts +72 -0
- package/templates/src/pages/api/booking/cancel.ts +73 -0
- package/templates/src/pages/api/booking/confirm.ts +82 -0
- package/templates/src/pages/api/booking/hold.ts +75 -0
- package/templates/src/pages/api/booking/list.ts +66 -0
- package/templates/src/pages/api/booking/metrics.ts +60 -0
- package/templates/src/pages/api/booking/release.ts +76 -0
- package/templates/src/pages/api/sandbox.ts +2 -2
- package/templates/src/pages/api/shopify/createCart.ts +69 -0
- package/templates/src/pages/api/shopify/getProducts.ts +64 -0
- package/templates/src/pages/storykeep/login.astro +26 -24
- package/templates/src/pages/storykeep/logout.astro +1 -10
- package/templates/src/pages/storykeep/manage.astro +69 -0
- package/templates/src/pages/storykeep/{content.astro → pages.astro} +4 -8
- package/templates/src/pages/storykeep/shopify.astro +101 -0
- package/templates/src/stores/navigation.ts +3 -42
- package/templates/src/stores/nodes.ts +3 -1
- package/templates/src/stores/resources.ts +7 -10
- package/templates/src/stores/shopify.ts +266 -0
- package/templates/src/types/tractstack.ts +75 -0
- package/templates/src/utils/api/advancedConfig.ts +7 -1
- package/templates/src/utils/api/advancedHelpers.ts +87 -7
- package/templates/src/utils/api/bookingHelpers.ts +125 -0
- package/templates/src/utils/api/brandHelpers.ts +14 -0
- package/templates/src/utils/api/resourceConfig.ts +13 -5
- package/templates/src/utils/auth.ts +29 -9
- package/templates/src/utils/compositor/aiGeneration.ts +3 -3
- package/templates/src/utils/compositor/aiPaneParser.ts +2 -2
- package/templates/src/utils/customHelpers.ts +49 -0
- package/templates/src/utils/helpers.ts +59 -0
- package/templates/src/utils/profileStorage.ts +5 -0
- package/templates/src/utils/tenantResolver.ts +2 -1
- package/utils/inject-files.ts +161 -2
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { atom, map, onMount } from 'nanostores';
|
|
2
|
+
import { persistentAtom } from '@nanostores/persistent';
|
|
3
|
+
|
|
4
|
+
export interface ShopifyVariant {
|
|
5
|
+
id: string;
|
|
6
|
+
title: string;
|
|
7
|
+
price: {
|
|
8
|
+
amount: string;
|
|
9
|
+
currencyCode: string;
|
|
10
|
+
};
|
|
11
|
+
compareAtPrice?: {
|
|
12
|
+
amount: string;
|
|
13
|
+
currencyCode: string;
|
|
14
|
+
};
|
|
15
|
+
sku?: string;
|
|
16
|
+
availableForSale: boolean;
|
|
17
|
+
requiresShipping: boolean;
|
|
18
|
+
selectedOptions: {
|
|
19
|
+
name: string;
|
|
20
|
+
value: string;
|
|
21
|
+
}[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const modalState = atom<{
|
|
25
|
+
isOpen: boolean;
|
|
26
|
+
type: 'success' | 'restriction';
|
|
27
|
+
title: string;
|
|
28
|
+
message: string;
|
|
29
|
+
}>({
|
|
30
|
+
isOpen: false,
|
|
31
|
+
type: 'success',
|
|
32
|
+
title: '',
|
|
33
|
+
message: '',
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
export interface ShopifyOption {
|
|
37
|
+
name: string;
|
|
38
|
+
values: string[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface ShopifyImage {
|
|
42
|
+
url: string;
|
|
43
|
+
altText?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface ShopifyProduct {
|
|
47
|
+
id: string;
|
|
48
|
+
title: string;
|
|
49
|
+
handle: string;
|
|
50
|
+
description: string;
|
|
51
|
+
options: ShopifyOption[];
|
|
52
|
+
images: ShopifyImage[];
|
|
53
|
+
variants: ShopifyVariant[];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface ShopifyPageInfo {
|
|
57
|
+
hasNextPage: boolean;
|
|
58
|
+
endCursor: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export type CartActionType = 'add' | 'remove';
|
|
62
|
+
|
|
63
|
+
export interface CartAction {
|
|
64
|
+
resourceId: string;
|
|
65
|
+
gid?: string;
|
|
66
|
+
variantId?: string;
|
|
67
|
+
variantIdShipped?: string;
|
|
68
|
+
variantIdPickup?: string;
|
|
69
|
+
boundResourceId?: string;
|
|
70
|
+
action: CartActionType;
|
|
71
|
+
suppressModal?: boolean;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface CartItemState {
|
|
75
|
+
resourceId: string;
|
|
76
|
+
quantity: number;
|
|
77
|
+
variantId?: string;
|
|
78
|
+
gid?: string;
|
|
79
|
+
variantIdShipped?: string;
|
|
80
|
+
variantIdPickup?: string;
|
|
81
|
+
boundResourceId?: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export const QUEUE_STATES = {
|
|
85
|
+
READY: 'READY',
|
|
86
|
+
ADDING: 'ADDING',
|
|
87
|
+
HAS_REQUIREMENTS: 'HAS_REQUIREMENTS',
|
|
88
|
+
} as const;
|
|
89
|
+
|
|
90
|
+
export type QueueState = (typeof QUEUE_STATES)[keyof typeof QUEUE_STATES];
|
|
91
|
+
|
|
92
|
+
export const CART_STATES = {
|
|
93
|
+
INIT: 'INIT',
|
|
94
|
+
READY: 'READY',
|
|
95
|
+
LOADED: 'LOADED',
|
|
96
|
+
CHECKOUT: 'CHECKOUT',
|
|
97
|
+
BOOKING: 'BOOKING',
|
|
98
|
+
SHOPIFY_HANDOFF: 'SHOPIFY_HANDOFF',
|
|
99
|
+
} as const;
|
|
100
|
+
|
|
101
|
+
export type CartState = (typeof CART_STATES)[keyof typeof CART_STATES];
|
|
102
|
+
|
|
103
|
+
export const isShopifyHandoff = atom<boolean>(false);
|
|
104
|
+
export const shopifyActiveTabStore = atom<string>('dashboards');
|
|
105
|
+
|
|
106
|
+
export const shopifyData = atom<{
|
|
107
|
+
products: ShopifyProduct[];
|
|
108
|
+
pageInfo?: ShopifyPageInfo;
|
|
109
|
+
lastFetched: number;
|
|
110
|
+
}>({
|
|
111
|
+
products: [],
|
|
112
|
+
lastFetched: 0,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
export const shopifyStatus = map<{
|
|
116
|
+
isLoading: boolean;
|
|
117
|
+
error: string | null;
|
|
118
|
+
}>({
|
|
119
|
+
isLoading: false,
|
|
120
|
+
error: null,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
export const addQueue = persistentAtom<CartAction[]>(
|
|
124
|
+
'tractstack_shopify_queue',
|
|
125
|
+
[],
|
|
126
|
+
{
|
|
127
|
+
encode: JSON.stringify,
|
|
128
|
+
decode: JSON.parse,
|
|
129
|
+
}
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
export const cartPersistence = persistentAtom<Record<string, CartItemState>>(
|
|
133
|
+
'tractstack_shopify_cart',
|
|
134
|
+
{},
|
|
135
|
+
{
|
|
136
|
+
encode: JSON.stringify,
|
|
137
|
+
decode: JSON.parse,
|
|
138
|
+
}
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
export const cartStore = map<Record<string, CartItemState>>(
|
|
142
|
+
cartPersistence.get()
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
onMount(cartStore, () => {
|
|
146
|
+
cartStore.set(cartPersistence.get());
|
|
147
|
+
|
|
148
|
+
const unbind = cartStore.listen((value) => {
|
|
149
|
+
cartPersistence.set(value);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
return () => unbind();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
export const queueState = persistentAtom<QueueState>(
|
|
156
|
+
'tractstack_shopify_queue_state',
|
|
157
|
+
QUEUE_STATES.READY
|
|
158
|
+
);
|
|
159
|
+
export const cartState = persistentAtom<CartState>(
|
|
160
|
+
'tractstack_shopify_cart_state',
|
|
161
|
+
CART_STATES.INIT
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
let currentAbortController: AbortController | null = null;
|
|
165
|
+
|
|
166
|
+
export async function fetchShopifyProducts(
|
|
167
|
+
q: string = '',
|
|
168
|
+
cursor: string | null = null
|
|
169
|
+
) {
|
|
170
|
+
if (currentAbortController) {
|
|
171
|
+
currentAbortController.abort();
|
|
172
|
+
}
|
|
173
|
+
currentAbortController = new AbortController();
|
|
174
|
+
const signal = currentAbortController.signal;
|
|
175
|
+
|
|
176
|
+
shopifyStatus.set({ isLoading: true, error: null });
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
const params = new URLSearchParams();
|
|
180
|
+
if (q) params.set('q', q);
|
|
181
|
+
if (cursor) params.set('cursor', cursor);
|
|
182
|
+
|
|
183
|
+
const queryString = params.toString();
|
|
184
|
+
const url = `/api/shopify/getProducts${queryString ? `?${queryString}` : ''}`;
|
|
185
|
+
|
|
186
|
+
const response = await fetch(url, { signal });
|
|
187
|
+
const result = await response.json();
|
|
188
|
+
|
|
189
|
+
if (!response.ok) {
|
|
190
|
+
throw new Error(result.error || 'Failed to fetch products');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
shopifyData.set({
|
|
194
|
+
products: result.products || [],
|
|
195
|
+
pageInfo: result.pageInfo,
|
|
196
|
+
lastFetched: Date.now(),
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
shopifyStatus.set({ isLoading: false, error: null });
|
|
200
|
+
} catch (error: unknown) {
|
|
201
|
+
if (error instanceof DOMException && error.name === 'AbortError') {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
console.error('Shopify fetch failed:', error);
|
|
206
|
+
shopifyStatus.set({
|
|
207
|
+
isLoading: false,
|
|
208
|
+
error:
|
|
209
|
+
error instanceof Error ? error.message : 'Failed to fetch products',
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function clearShopifySearch() {
|
|
215
|
+
if (currentAbortController) {
|
|
216
|
+
currentAbortController.abort();
|
|
217
|
+
}
|
|
218
|
+
shopifyData.set({
|
|
219
|
+
products: [],
|
|
220
|
+
pageInfo: undefined,
|
|
221
|
+
lastFetched: 0,
|
|
222
|
+
});
|
|
223
|
+
shopifyStatus.set({ isLoading: false, error: null });
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export const transactionTraceId = persistentAtom<string>(
|
|
227
|
+
'tractstack_shopify_trace_id',
|
|
228
|
+
''
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
export interface CustomerDetails {
|
|
232
|
+
name: string;
|
|
233
|
+
email: string;
|
|
234
|
+
leadId: string;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export const customerDetails = persistentAtom<CustomerDetails>(
|
|
238
|
+
'tractstack_shopify_customer',
|
|
239
|
+
{
|
|
240
|
+
name: '',
|
|
241
|
+
email: '',
|
|
242
|
+
leadId: '',
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
encode: JSON.stringify,
|
|
246
|
+
decode: JSON.parse,
|
|
247
|
+
}
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
export function setCustomerDetails(details: Partial<CustomerDetails>) {
|
|
251
|
+
customerDetails.set({
|
|
252
|
+
...customerDetails.get(),
|
|
253
|
+
...details,
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export function clearCommerceState() {
|
|
258
|
+
cartStore.set({});
|
|
259
|
+
customerDetails.set({
|
|
260
|
+
name: '',
|
|
261
|
+
email: '',
|
|
262
|
+
leadId: '',
|
|
263
|
+
});
|
|
264
|
+
transactionTraceId.set('');
|
|
265
|
+
cartState.set(CART_STATES.READY);
|
|
266
|
+
}
|
|
@@ -152,6 +152,19 @@ export interface FullContentMapItem {
|
|
|
152
152
|
scale?: string;
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
+
export interface TimeBlock {
|
|
156
|
+
start: string; // "09:00" for business hours or ISO-8601 for unavailable blocks
|
|
157
|
+
end: string;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export interface SchedulingConfig {
|
|
161
|
+
timezone: string;
|
|
162
|
+
bufferGapsMinutes: number;
|
|
163
|
+
maxLengthMinutes: number;
|
|
164
|
+
businessHours: Record<string, TimeBlock>;
|
|
165
|
+
unavailableHours: TimeBlock[];
|
|
166
|
+
}
|
|
167
|
+
|
|
155
168
|
export interface BrandConfig {
|
|
156
169
|
TENANT_ID: string;
|
|
157
170
|
SITE_INIT?: boolean;
|
|
@@ -183,7 +196,13 @@ export interface BrandConfig {
|
|
|
183
196
|
KNOWN_RESOURCES?: KnownResourcesConfig;
|
|
184
197
|
DESIGN_LIBRARY?: DesignLibraryConfig;
|
|
185
198
|
HAS_AAI?: boolean;
|
|
199
|
+
HAS_SHOPIFY?: boolean;
|
|
200
|
+
SHOW_SHOPIFY_HELPER?: boolean;
|
|
201
|
+
SHOPIFY_ADMIN_SLUG?: string;
|
|
202
|
+
USER_SETUP_WEBHOOKS?: boolean;
|
|
203
|
+
HAS_RESEND?: boolean;
|
|
186
204
|
HAS_HYDRATION_TOKEN?: boolean;
|
|
205
|
+
SCHEDULING?: SchedulingConfig;
|
|
187
206
|
}
|
|
188
207
|
|
|
189
208
|
export interface BrandConfigState {
|
|
@@ -217,7 +236,11 @@ export interface BrandConfigState {
|
|
|
217
236
|
knownResources: KnownResourcesConfig;
|
|
218
237
|
designLibrary?: DesignLibraryConfig;
|
|
219
238
|
hasAAI: boolean;
|
|
239
|
+
hasShopify: boolean;
|
|
240
|
+
showShopifyHelper: boolean;
|
|
241
|
+
hasResend: boolean;
|
|
220
242
|
hasHydrationToken: boolean;
|
|
243
|
+
scheduling: SchedulingConfig;
|
|
221
244
|
}
|
|
222
245
|
|
|
223
246
|
// Form validation types
|
|
@@ -251,6 +274,14 @@ export interface AdvancedConfigStatus {
|
|
|
251
274
|
editorPasswordSet: boolean;
|
|
252
275
|
aaiAPIKeySet: boolean;
|
|
253
276
|
tursoEnabled: boolean;
|
|
277
|
+
shopifyStorefrontTokenSet: boolean;
|
|
278
|
+
shopifyAdminApiKeySet: boolean;
|
|
279
|
+
shopifyApiSecretSet: boolean;
|
|
280
|
+
shopifyApiVersion: string;
|
|
281
|
+
shopifyStoreDomainSet: boolean;
|
|
282
|
+
resendApiKeySet: boolean;
|
|
283
|
+
shopifyAdminSlugSet: boolean;
|
|
284
|
+
userSetupWebhooks: boolean;
|
|
254
285
|
}
|
|
255
286
|
|
|
256
287
|
export interface AdvancedConfigState {
|
|
@@ -259,6 +290,14 @@ export interface AdvancedConfigState {
|
|
|
259
290
|
adminPassword: string;
|
|
260
291
|
editorPassword: string;
|
|
261
292
|
aaiApiKey: string;
|
|
293
|
+
shopifyStorefrontToken: string;
|
|
294
|
+
shopifyAdminApiKey: string;
|
|
295
|
+
shopifyApiSecret: string;
|
|
296
|
+
shopifyApiVersion: string;
|
|
297
|
+
shopifyStoreDomain: string;
|
|
298
|
+
shopifyAdminSlug: string;
|
|
299
|
+
userSetupWebhooks: boolean;
|
|
300
|
+
resendApiKey: string;
|
|
262
301
|
}
|
|
263
302
|
|
|
264
303
|
export interface AdvancedConfigUpdateRequest {
|
|
@@ -269,6 +308,13 @@ export interface AdvancedConfigUpdateRequest {
|
|
|
269
308
|
AAI_API_KEY?: string;
|
|
270
309
|
HOME_SLUG?: string;
|
|
271
310
|
TRACTSTACK_HOME_SLUG?: string;
|
|
311
|
+
SHOPIFY_STOREFRONT_TOKEN?: string;
|
|
312
|
+
SHOPIFY_API_SECRET?: string;
|
|
313
|
+
SHOPIFY_API_VERSION?: string;
|
|
314
|
+
SHOPIFY_STORE_DOMAIN?: string;
|
|
315
|
+
SHOPIFY_ADMIN_SLUG?: string;
|
|
316
|
+
USER_SETUP_WEBHOOKS?: boolean;
|
|
317
|
+
RESEND_API_KEY?: string;
|
|
272
318
|
}
|
|
273
319
|
|
|
274
320
|
export interface MenuNodeState {
|
|
@@ -469,3 +515,32 @@ export interface CategorizedResults {
|
|
|
469
515
|
contextPaneResults: FTSResult[];
|
|
470
516
|
resourceResults: FTSResult[];
|
|
471
517
|
}
|
|
518
|
+
|
|
519
|
+
export type BookingStatus = 'PENDING' | 'CONFIRMED' | 'CANCELLED';
|
|
520
|
+
|
|
521
|
+
export interface BookingEntity {
|
|
522
|
+
id: string; // traceId
|
|
523
|
+
resourceIds: string[];
|
|
524
|
+
leadId: string;
|
|
525
|
+
startTime: string; // ISO-8601 UTC string
|
|
526
|
+
endTime: string; // ISO-8601 UTC string
|
|
527
|
+
status: BookingStatus;
|
|
528
|
+
shopifyOrderId?: string;
|
|
529
|
+
createdAt: string; // ISO-8601 UTC string
|
|
530
|
+
leadEmail?: string;
|
|
531
|
+
leadName?: string;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
export interface BookingListResponse {
|
|
535
|
+
data: BookingEntity[];
|
|
536
|
+
totalCount: number;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
export interface BookingMetricsResponse {
|
|
540
|
+
totalMonthlyConfirmed: number;
|
|
541
|
+
totalAnnualConfirmed: number;
|
|
542
|
+
totalWeeklyConfirmed: number;
|
|
543
|
+
leadConversionAnchor: number;
|
|
544
|
+
pendingLast24h: number;
|
|
545
|
+
confirmedLast24h: number;
|
|
546
|
+
}
|
|
@@ -28,7 +28,13 @@ export async function getAdvancedConfigStatus(
|
|
|
28
28
|
typeof data.tursoTokenSet !== 'boolean' ||
|
|
29
29
|
typeof data.adminPasswordSet !== 'boolean' ||
|
|
30
30
|
typeof data.editorPasswordSet !== 'boolean' ||
|
|
31
|
-
typeof data.aaiAPIKeySet !== 'boolean'
|
|
31
|
+
typeof data.aaiAPIKeySet !== 'boolean' ||
|
|
32
|
+
typeof data.shopifyStorefrontTokenSet !== 'boolean' ||
|
|
33
|
+
typeof data.shopifyApiSecretSet !== 'boolean' ||
|
|
34
|
+
typeof data.shopifyStoreDomainSet !== 'boolean' ||
|
|
35
|
+
typeof data.shopifyAdminSlugSet !== 'boolean' ||
|
|
36
|
+
typeof data.userSetupWebhooks !== 'boolean' ||
|
|
37
|
+
typeof data.resendApiKeySet !== 'boolean'
|
|
32
38
|
) {
|
|
33
39
|
throw new Error('Invalid response format from server');
|
|
34
40
|
}
|
|
@@ -19,6 +19,14 @@ export function convertToLocalState(
|
|
|
19
19
|
adminPassword: '',
|
|
20
20
|
editorPassword: '',
|
|
21
21
|
aaiApiKey: '',
|
|
22
|
+
shopifyStorefrontToken: '',
|
|
23
|
+
shopifyAdminApiKey: '',
|
|
24
|
+
shopifyApiSecret: '',
|
|
25
|
+
shopifyApiVersion: '',
|
|
26
|
+
shopifyStoreDomain: '',
|
|
27
|
+
resendApiKey: '',
|
|
28
|
+
shopifyAdminSlug: '',
|
|
29
|
+
userSetupWebhooks: false,
|
|
22
30
|
};
|
|
23
31
|
}
|
|
24
32
|
|
|
@@ -31,6 +39,14 @@ export function convertToLocalState(
|
|
|
31
39
|
adminPassword: '',
|
|
32
40
|
editorPassword: '',
|
|
33
41
|
aaiApiKey: '',
|
|
42
|
+
shopifyStorefrontToken: '',
|
|
43
|
+
shopifyAdminApiKey: '',
|
|
44
|
+
shopifyApiSecret: '',
|
|
45
|
+
shopifyApiVersion: status.shopifyApiVersion || '',
|
|
46
|
+
shopifyStoreDomain: '',
|
|
47
|
+
resendApiKey: '',
|
|
48
|
+
shopifyAdminSlug: '',
|
|
49
|
+
userSetupWebhooks: status.userSetupWebhooks,
|
|
34
50
|
};
|
|
35
51
|
}
|
|
36
52
|
|
|
@@ -64,6 +80,34 @@ export function convertToBackendFormat(
|
|
|
64
80
|
request.AAI_API_KEY = state.aaiApiKey.trim();
|
|
65
81
|
}
|
|
66
82
|
|
|
83
|
+
if (state.shopifyStorefrontToken?.trim()) {
|
|
84
|
+
request.SHOPIFY_STOREFRONT_TOKEN = state.shopifyStorefrontToken.trim();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (state.shopifyApiSecret?.trim()) {
|
|
88
|
+
request.SHOPIFY_API_SECRET = state.shopifyApiSecret.trim();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (state.shopifyApiVersion?.trim()) {
|
|
92
|
+
request.SHOPIFY_API_VERSION = state.shopifyApiVersion.trim();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (state.shopifyStoreDomain?.trim()) {
|
|
96
|
+
request.SHOPIFY_STORE_DOMAIN = state.shopifyStoreDomain.trim();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (state.shopifyAdminSlug?.trim()) {
|
|
100
|
+
request.SHOPIFY_ADMIN_SLUG = state.shopifyAdminSlug.trim();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (state.userSetupWebhooks !== undefined) {
|
|
104
|
+
request.USER_SETUP_WEBHOOKS = state.userSetupWebhooks;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (state.resendApiKey?.trim()) {
|
|
108
|
+
request.RESEND_API_KEY = state.resendApiKey.trim();
|
|
109
|
+
}
|
|
110
|
+
|
|
67
111
|
return request;
|
|
68
112
|
}
|
|
69
113
|
|
|
@@ -75,21 +119,16 @@ export function validateAdvancedConfig(
|
|
|
75
119
|
): FieldErrors {
|
|
76
120
|
const errors: FieldErrors = {};
|
|
77
121
|
|
|
78
|
-
// Turso validation: both URL and token must be provided together
|
|
79
122
|
const hasTursoUrl = Boolean(state.tursoUrl?.trim());
|
|
80
123
|
const hasTursoToken = Boolean(state.tursoToken?.trim());
|
|
81
|
-
|
|
82
124
|
if (hasTursoUrl && !hasTursoToken) {
|
|
83
125
|
errors.tursoToken =
|
|
84
126
|
'Turso Auth Token is required when Turso URL is provided';
|
|
85
127
|
}
|
|
86
|
-
|
|
87
128
|
if (hasTursoToken && !hasTursoUrl) {
|
|
88
129
|
errors.tursoUrl =
|
|
89
130
|
'Turso Database URL is required when Turso Token is provided';
|
|
90
131
|
}
|
|
91
|
-
|
|
92
|
-
// Basic URL validation for Turso
|
|
93
132
|
if (hasTursoUrl && state.tursoUrl) {
|
|
94
133
|
const urlPattern = /^libsql:\/\/.+/;
|
|
95
134
|
if (!urlPattern.test(state.tursoUrl.trim())) {
|
|
@@ -97,12 +136,53 @@ export function validateAdvancedConfig(
|
|
|
97
136
|
}
|
|
98
137
|
}
|
|
99
138
|
|
|
100
|
-
|
|
139
|
+
if (state.shopifyStoreDomain?.trim()) {
|
|
140
|
+
const domainPattern = /^[a-zA-Z0-9-]+\.myshopify\.com$/;
|
|
141
|
+
if (!domainPattern.test(state.shopifyStoreDomain.trim())) {
|
|
142
|
+
errors.shopifyStoreDomain =
|
|
143
|
+
'Store domain must be in the format "your-shop.myshopify.com"';
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (state.shopifyApiVersion?.trim()) {
|
|
148
|
+
if (!/^\d{4}-\d{2}$/.test(state.shopifyApiVersion.trim())) {
|
|
149
|
+
errors.shopifyApiVersion =
|
|
150
|
+
'Version must match YYYY-MM format (e.g. 2026-01)';
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (state.shopifyAdminSlug?.trim()) {
|
|
155
|
+
if (
|
|
156
|
+
state.shopifyAdminSlug.includes('/') ||
|
|
157
|
+
state.shopifyAdminSlug.includes('.')
|
|
158
|
+
) {
|
|
159
|
+
errors.shopifyAdminSlug =
|
|
160
|
+
'Please enter only the store slug, not the full URL';
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const isConfiguringShopify = Boolean(
|
|
165
|
+
state.shopifyStoreDomain?.trim() ||
|
|
166
|
+
state.shopifyStorefrontToken?.trim() ||
|
|
167
|
+
state.shopifyApiSecret?.trim() ||
|
|
168
|
+
state.shopifyAdminSlug?.trim()
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
if (isConfiguringShopify) {
|
|
172
|
+
if (!state.shopifyAdminSlug?.trim()) {
|
|
173
|
+
errors.shopifyAdminSlug =
|
|
174
|
+
'Admin slug is required to generate webhook links.';
|
|
175
|
+
}
|
|
176
|
+
if (!state.userSetupWebhooks) {
|
|
177
|
+
errors.userSetupWebhooks =
|
|
178
|
+
'You must confirm webhooks are configured to ensure synchronization.';
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
101
182
|
if (state.adminPassword && state.adminPassword.length < 8) {
|
|
102
183
|
errors.adminPassword =
|
|
103
184
|
'Admin password should be at least 8 characters long';
|
|
104
185
|
}
|
|
105
|
-
|
|
106
186
|
if (state.editorPassword && state.editorPassword.length < 8) {
|
|
107
187
|
errors.editorPassword =
|
|
108
188
|
'Editor password should be at least 8 characters long';
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { customerDetails } from '@/stores/shopify';
|
|
2
|
+
|
|
3
|
+
export const bookingHelpers = {
|
|
4
|
+
/**
|
|
5
|
+
* Checks if a user profile already exists for a given email via the BFF.
|
|
6
|
+
*/
|
|
7
|
+
lookupLead: async (email: string) => {
|
|
8
|
+
const response = await fetch('/api/auth/lookup-lead', {
|
|
9
|
+
method: 'POST',
|
|
10
|
+
headers: {
|
|
11
|
+
'Content-Type': 'application/json',
|
|
12
|
+
},
|
|
13
|
+
body: JSON.stringify({ email }),
|
|
14
|
+
});
|
|
15
|
+
return await response.json();
|
|
16
|
+
},
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Asks backend for availability slots
|
|
20
|
+
*/
|
|
21
|
+
getAvailability: async (start: string, end: string) => {
|
|
22
|
+
const query = new URLSearchParams({
|
|
23
|
+
start,
|
|
24
|
+
end,
|
|
25
|
+
});
|
|
26
|
+
const response = await fetch(
|
|
27
|
+
`/api/booking/availability?${query.toString()}`
|
|
28
|
+
);
|
|
29
|
+
return await response.json();
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Places a temporary hold on a specific slot for the manifest of services and pickup products.
|
|
34
|
+
*/
|
|
35
|
+
holdSlot: async (
|
|
36
|
+
traceId: string,
|
|
37
|
+
startTime: string,
|
|
38
|
+
endTime: string,
|
|
39
|
+
resourceIds: string[]
|
|
40
|
+
) => {
|
|
41
|
+
const details = customerDetails.get();
|
|
42
|
+
|
|
43
|
+
const response = await fetch('/api/booking/hold', {
|
|
44
|
+
method: 'POST',
|
|
45
|
+
headers: {
|
|
46
|
+
'Content-Type': 'application/json',
|
|
47
|
+
},
|
|
48
|
+
body: JSON.stringify({
|
|
49
|
+
traceId,
|
|
50
|
+
leadId: details.leadId,
|
|
51
|
+
resourceIds,
|
|
52
|
+
startTime,
|
|
53
|
+
endTime,
|
|
54
|
+
}),
|
|
55
|
+
});
|
|
56
|
+
return await response.json();
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Releases a temporary hold, typically used when a user abandons checkout.
|
|
61
|
+
*/
|
|
62
|
+
releaseHold: async (traceId: string) => {
|
|
63
|
+
const response = await fetch('/api/booking/release', {
|
|
64
|
+
method: 'POST',
|
|
65
|
+
headers: {
|
|
66
|
+
'Content-Type': 'application/json',
|
|
67
|
+
},
|
|
68
|
+
body: JSON.stringify({ traceId }),
|
|
69
|
+
});
|
|
70
|
+
return await response.json();
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Confirms a pending hold, finalizing the booking for free services.
|
|
75
|
+
*/
|
|
76
|
+
confirmBooking: async (traceId: string) => {
|
|
77
|
+
const response = await fetch('/api/booking/confirm', {
|
|
78
|
+
method: 'POST',
|
|
79
|
+
headers: {
|
|
80
|
+
'Content-Type': 'application/json',
|
|
81
|
+
},
|
|
82
|
+
body: JSON.stringify({ traceId }),
|
|
83
|
+
});
|
|
84
|
+
return await response.json();
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Retrieves a paginated list of all bookings for the administrative dashboard.
|
|
89
|
+
*/
|
|
90
|
+
listBookings: async (
|
|
91
|
+
limit: number = 50,
|
|
92
|
+
offset: number = 0,
|
|
93
|
+
status: string = 'ALL'
|
|
94
|
+
) => {
|
|
95
|
+
const query = new URLSearchParams({
|
|
96
|
+
limit: limit.toString(),
|
|
97
|
+
offset: offset.toString(),
|
|
98
|
+
status,
|
|
99
|
+
});
|
|
100
|
+
const response = await fetch(`/api/booking/list?${query.toString()}`);
|
|
101
|
+
return await response.json();
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Retrieves aggregated booking volume and conversion statistics.
|
|
106
|
+
*/
|
|
107
|
+
getMetrics: async () => {
|
|
108
|
+
const response = await fetch('/api/booking/metrics');
|
|
109
|
+
return await response.json();
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Manually cancels an existing booking from the administrative dashboard.
|
|
114
|
+
*/
|
|
115
|
+
cancelBooking: async (traceId: string) => {
|
|
116
|
+
const response = await fetch('/api/booking/cancel', {
|
|
117
|
+
method: 'POST',
|
|
118
|
+
headers: {
|
|
119
|
+
'Content-Type': 'application/json',
|
|
120
|
+
},
|
|
121
|
+
body: JSON.stringify({ traceId }),
|
|
122
|
+
});
|
|
123
|
+
return await response.json();
|
|
124
|
+
},
|
|
125
|
+
};
|
|
@@ -40,7 +40,17 @@ export function convertToLocalState(
|
|
|
40
40
|
knownResources: brandConfig.KNOWN_RESOURCES ?? {},
|
|
41
41
|
designLibrary: brandConfig.DESIGN_LIBRARY ?? undefined,
|
|
42
42
|
hasAAI: brandConfig.HAS_AAI ?? false,
|
|
43
|
+
hasShopify: brandConfig.HAS_SHOPIFY ?? false,
|
|
44
|
+
showShopifyHelper: brandConfig.SHOW_SHOPIFY_HELPER ?? false,
|
|
45
|
+
hasResend: brandConfig.HAS_RESEND ?? false,
|
|
43
46
|
hasHydrationToken: brandConfig.HAS_HYDRATION_TOKEN ?? false,
|
|
47
|
+
scheduling: brandConfig.SCHEDULING ?? {
|
|
48
|
+
timezone: 'UTC',
|
|
49
|
+
bufferGapsMinutes: 15,
|
|
50
|
+
maxLengthMinutes: 0,
|
|
51
|
+
businessHours: {},
|
|
52
|
+
unavailableHours: [],
|
|
53
|
+
},
|
|
44
54
|
};
|
|
45
55
|
}
|
|
46
56
|
|
|
@@ -72,6 +82,10 @@ export function convertToBackendFormat(
|
|
|
72
82
|
KNOWN_RESOURCES: localState.knownResources,
|
|
73
83
|
DESIGN_LIBRARY: localState.designLibrary,
|
|
74
84
|
HAS_AAI: localState.hasAAI,
|
|
85
|
+
HAS_SHOPIFY: localState.hasShopify,
|
|
86
|
+
SHOW_SHOPIFY_HELPER: localState.showShopifyHelper,
|
|
87
|
+
HAS_RESEND: localState.hasResend,
|
|
88
|
+
SCHEDULING: localState.scheduling,
|
|
75
89
|
|
|
76
90
|
// ALWAYS send asset paths (current state)
|
|
77
91
|
LOGO: localState.logo,
|