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
|
@@ -40,6 +40,12 @@ const ampmOptions = [
|
|
|
40
40
|
{ value: 'PM', label: 'PM' },
|
|
41
41
|
];
|
|
42
42
|
|
|
43
|
+
const datetimeSelectItemHighlightStyles = `
|
|
44
|
+
.datetime-select-item[data-highlighted] {
|
|
45
|
+
background-color: #eff6ff;
|
|
46
|
+
}
|
|
47
|
+
`;
|
|
48
|
+
|
|
43
49
|
const DateTimeInput = ({
|
|
44
50
|
value,
|
|
45
51
|
onChange,
|
|
@@ -450,6 +456,7 @@ const DateTimeInput = ({
|
|
|
450
456
|
{/* Time Picker Section (only show if withTime is true and not date-only) */}
|
|
451
457
|
{withTime && displayFormat !== 'date' && (
|
|
452
458
|
<div className="space-y-2">
|
|
459
|
+
<style>{datetimeSelectItemHighlightStyles}</style>
|
|
453
460
|
<div className="text-xs font-bold text-gray-600">
|
|
454
461
|
Time (Local Timezone)
|
|
455
462
|
</div>
|
|
@@ -489,7 +496,7 @@ const DateTimeInput = ({
|
|
|
489
496
|
<Select.Item
|
|
490
497
|
key={option.value}
|
|
491
498
|
item={option}
|
|
492
|
-
className="flex cursor-pointer items-center justify-between px-3 py-2 text-sm hover:bg-blue-50
|
|
499
|
+
className="datetime-select-item flex cursor-pointer items-center justify-between px-3 py-2 text-sm hover:bg-blue-50"
|
|
493
500
|
>
|
|
494
501
|
<Select.ItemText>{option.label}</Select.ItemText>
|
|
495
502
|
<Select.ItemIndicator>
|
|
@@ -540,7 +547,7 @@ const DateTimeInput = ({
|
|
|
540
547
|
<Select.Item
|
|
541
548
|
key={option.value}
|
|
542
549
|
item={option}
|
|
543
|
-
className="flex cursor-pointer items-center justify-between px-3 py-2 text-sm hover:bg-blue-50
|
|
550
|
+
className="datetime-select-item flex cursor-pointer items-center justify-between px-3 py-2 text-sm hover:bg-blue-50"
|
|
544
551
|
>
|
|
545
552
|
<Select.ItemText>{option.label}</Select.ItemText>
|
|
546
553
|
<Select.ItemIndicator>
|
|
@@ -589,7 +596,7 @@ const DateTimeInput = ({
|
|
|
589
596
|
<Select.Item
|
|
590
597
|
key={option.value}
|
|
591
598
|
item={option}
|
|
592
|
-
className="flex cursor-pointer items-center justify-between px-3 py-2 text-sm hover:bg-blue-50
|
|
599
|
+
className="datetime-select-item flex cursor-pointer items-center justify-between px-3 py-2 text-sm hover:bg-blue-50"
|
|
593
600
|
>
|
|
594
601
|
<Select.ItemText>{option.label}</Select.ItemText>
|
|
595
602
|
<Select.ItemIndicator>
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
useRef,
|
|
3
|
+
useState,
|
|
4
|
+
useId,
|
|
5
|
+
type ChangeEvent,
|
|
6
|
+
type DragEvent,
|
|
7
|
+
} from 'react';
|
|
2
8
|
import { classNames } from '@/utils/helpers';
|
|
3
9
|
import {
|
|
4
10
|
validateFile,
|
|
@@ -151,14 +157,14 @@ const FileUpload = ({
|
|
|
151
157
|
}
|
|
152
158
|
};
|
|
153
159
|
|
|
154
|
-
const handleFileSelect = (e:
|
|
160
|
+
const handleFileSelect = (e: ChangeEvent<HTMLInputElement>) => {
|
|
155
161
|
const file = e.target.files?.[0];
|
|
156
162
|
if (file) {
|
|
157
163
|
processFile(file);
|
|
158
164
|
}
|
|
159
165
|
};
|
|
160
166
|
|
|
161
|
-
const handleDrop = (e:
|
|
167
|
+
const handleDrop = (e: DragEvent<HTMLDivElement>) => {
|
|
162
168
|
e.preventDefault();
|
|
163
169
|
setDragOver(false);
|
|
164
170
|
|
|
@@ -170,14 +176,14 @@ const FileUpload = ({
|
|
|
170
176
|
}
|
|
171
177
|
};
|
|
172
178
|
|
|
173
|
-
const handleDragOver = (e:
|
|
179
|
+
const handleDragOver = (e: DragEvent<HTMLDivElement>) => {
|
|
174
180
|
e.preventDefault();
|
|
175
181
|
if (!disabled) {
|
|
176
182
|
setDragOver(true);
|
|
177
183
|
}
|
|
178
184
|
};
|
|
179
185
|
|
|
180
|
-
const handleDragLeave = (e:
|
|
186
|
+
const handleDragLeave = (e: DragEvent<HTMLDivElement>) => {
|
|
181
187
|
e.preventDefault();
|
|
182
188
|
setDragOver(false);
|
|
183
189
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { forwardRef, useId } from 'react';
|
|
1
|
+
import { forwardRef, useId, type ChangeEvent } from 'react';
|
|
2
2
|
import { classNames } from '@/utils/helpers';
|
|
3
3
|
|
|
4
4
|
interface NumberInputProps {
|
|
@@ -42,7 +42,7 @@ const NumberInput = forwardRef<HTMLInputElement, NumberInputProps>(
|
|
|
42
42
|
const errorId = `${inputId}-error`;
|
|
43
43
|
const inputName = customName || inputId;
|
|
44
44
|
|
|
45
|
-
const handleChange = (e:
|
|
45
|
+
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
|
|
46
46
|
const newValue = parseFloat(e.target.value);
|
|
47
47
|
if (!isNaN(newValue)) {
|
|
48
48
|
onChange(newValue);
|
|
@@ -13,13 +13,25 @@ interface APIConfigSectionProps {
|
|
|
13
13
|
status: AdvancedConfigStatus | null;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
const GOOGLE_APP_HOME_URL = 'https://renees.freewebpress.com/';
|
|
17
|
+
const GOOGLE_PRIVACY_URL = 'https://renees.freewebpress.com/privacy';
|
|
18
|
+
const GOOGLE_TERMS_URL = 'https://renees.freewebpress.com/terms';
|
|
19
|
+
const GOOGLE_AUTHORIZED_DOMAIN = 'freewebpress.com';
|
|
20
|
+
const GOOGLE_REDIRECT_URI =
|
|
21
|
+
'https://renees.freewebpress.com/api/google/oauth/callback';
|
|
22
|
+
const GOOGLE_SCOPE = 'https://www.googleapis.com/auth/calendar.events';
|
|
23
|
+
const GOOGLE_VERIFICATION_VIDEO_URL =
|
|
24
|
+
'https://www.youtube.com/watch?v=kJI4XdqiiAI';
|
|
25
|
+
|
|
16
26
|
export default function APIConfigSection({
|
|
17
27
|
formState,
|
|
18
28
|
status,
|
|
19
29
|
}: APIConfigSectionProps) {
|
|
20
|
-
const { state, updateField, errors } = formState;
|
|
30
|
+
const { state, updateField, errors, isDirty, saveState } = formState;
|
|
21
31
|
|
|
22
32
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
33
|
+
const [isGoogleConnecting, setIsGoogleConnecting] = useState(false);
|
|
34
|
+
const [isGoogleDisconnecting, setIsGoogleDisconnecting] = useState(false);
|
|
23
35
|
const goBackend =
|
|
24
36
|
import.meta.env.PUBLIC_GO_BACKEND || 'http://localhost:8080';
|
|
25
37
|
|
|
@@ -32,6 +44,18 @@ export default function APIConfigSection({
|
|
|
32
44
|
const shopifyAdminSlugConfigured = status?.shopifyAdminSlugSet;
|
|
33
45
|
const shopifyWebhooksConfigured = status?.userSetupWebhooks;
|
|
34
46
|
const resendConfigured = status?.resendApiKeySet;
|
|
47
|
+
const googleHasSync = status?.hasGoogleSync;
|
|
48
|
+
const googleClientIDConfigured = status?.googleOauthClientIdSet;
|
|
49
|
+
const googleClientSecretConfigured = status?.googleOauthClientSecretSet;
|
|
50
|
+
const googleCalendarConfigured = status?.googleCalendarIdSet;
|
|
51
|
+
const googleCredentialsSaved = Boolean(
|
|
52
|
+
googleClientIDConfigured &&
|
|
53
|
+
googleClientSecretConfigured &&
|
|
54
|
+
googleCalendarConfigured
|
|
55
|
+
);
|
|
56
|
+
const googleCredentialsPendingSave = isDirty || saveState === 'saving';
|
|
57
|
+
const canStartGoogleConnect =
|
|
58
|
+
googleCredentialsSaved && !googleCredentialsPendingSave && !googleHasSync;
|
|
35
59
|
|
|
36
60
|
const renderStatusBadge = (isConfigured: boolean | undefined) => {
|
|
37
61
|
if (status === null) {
|
|
@@ -52,6 +76,44 @@ export default function APIConfigSection({
|
|
|
52
76
|
);
|
|
53
77
|
};
|
|
54
78
|
|
|
79
|
+
const handleGoogleConnect = async () => {
|
|
80
|
+
setIsGoogleConnecting(true);
|
|
81
|
+
try {
|
|
82
|
+
const response = await fetch('/api/google/oauth/start');
|
|
83
|
+
const payload = await response.json();
|
|
84
|
+
if (!response.ok || !payload.authorization) {
|
|
85
|
+
throw new Error(payload?.error || 'Failed to start Google OAuth');
|
|
86
|
+
}
|
|
87
|
+
window.location.href = payload.authorization;
|
|
88
|
+
} catch (error) {
|
|
89
|
+
console.error('Failed to start Google OAuth:', error);
|
|
90
|
+
alert(error instanceof Error ? error.message : 'Google OAuth failed');
|
|
91
|
+
} finally {
|
|
92
|
+
setIsGoogleConnecting(false);
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const handleGoogleDisconnect = async () => {
|
|
97
|
+
setIsGoogleDisconnecting(true);
|
|
98
|
+
try {
|
|
99
|
+
const response = await fetch('/api/google/oauth/disconnect', {
|
|
100
|
+
method: 'POST',
|
|
101
|
+
});
|
|
102
|
+
const payload = await response.json();
|
|
103
|
+
if (!response.ok) {
|
|
104
|
+
throw new Error(payload?.error || 'Failed to disconnect Google OAuth');
|
|
105
|
+
}
|
|
106
|
+
window.location.reload();
|
|
107
|
+
} catch (error) {
|
|
108
|
+
console.error('Failed to disconnect Google OAuth:', error);
|
|
109
|
+
alert(
|
|
110
|
+
error instanceof Error ? error.message : 'Failed to disconnect Google'
|
|
111
|
+
);
|
|
112
|
+
} finally {
|
|
113
|
+
setIsGoogleDisconnecting(false);
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
55
117
|
return (
|
|
56
118
|
<div className="bg-white shadow md:rounded-lg">
|
|
57
119
|
<div className="px-4 py-5 md:p-6">
|
|
@@ -212,7 +274,7 @@ export default function APIConfigSection({
|
|
|
212
274
|
|
|
213
275
|
<BooleanToggle
|
|
214
276
|
label="Webhooks Manually Configured"
|
|
215
|
-
description="I have manually created the required webhooks
|
|
277
|
+
description="I have manually created the required webhooks in my Shopify Admin."
|
|
216
278
|
value={state.userSetupWebhooks}
|
|
217
279
|
onChange={(value) =>
|
|
218
280
|
updateField('userSetupWebhooks', value)
|
|
@@ -244,6 +306,162 @@ export default function APIConfigSection({
|
|
|
244
306
|
{resendConfigured && ' Leave blank to keep existing key.'}
|
|
245
307
|
</p>
|
|
246
308
|
</div>
|
|
309
|
+
|
|
310
|
+
{/* Google Calendar / Meet Section */}
|
|
311
|
+
<div className="border-t border-gray-100 pt-6">
|
|
312
|
+
<div className="mb-4 flex items-center justify-between">
|
|
313
|
+
<h4 className="text-sm font-bold text-gray-900">
|
|
314
|
+
Google Calendar + Meet (Remote Sync)
|
|
315
|
+
</h4>
|
|
316
|
+
{renderStatusBadge(googleHasSync)}
|
|
317
|
+
</div>
|
|
318
|
+
<div className="space-y-4">
|
|
319
|
+
<StringInput
|
|
320
|
+
label="Google OAuth Client ID"
|
|
321
|
+
value={state.googleOauthClientId}
|
|
322
|
+
onChange={(value) => updateField('googleOauthClientId', value)}
|
|
323
|
+
placeholder={
|
|
324
|
+
googleClientIDConfigured
|
|
325
|
+
? '••••••••••••••••'
|
|
326
|
+
: 'Enter Google OAuth client ID'
|
|
327
|
+
}
|
|
328
|
+
/>
|
|
329
|
+
<StringInput
|
|
330
|
+
label="Google OAuth Client Secret"
|
|
331
|
+
value={state.googleOauthClientSecret}
|
|
332
|
+
onChange={(value) =>
|
|
333
|
+
updateField('googleOauthClientSecret', value)
|
|
334
|
+
}
|
|
335
|
+
type="password"
|
|
336
|
+
placeholder={
|
|
337
|
+
googleClientSecretConfigured
|
|
338
|
+
? '••••••••••••••••'
|
|
339
|
+
: 'Enter Google OAuth client secret'
|
|
340
|
+
}
|
|
341
|
+
/>
|
|
342
|
+
<StringInput
|
|
343
|
+
label="Google Calendar ID"
|
|
344
|
+
value={state.googleCalendarId}
|
|
345
|
+
onChange={(value) => updateField('googleCalendarId', value)}
|
|
346
|
+
placeholder={
|
|
347
|
+
googleCalendarConfigured
|
|
348
|
+
? '••••••••••••••••'
|
|
349
|
+
: 'example@gmail.com or calendar ID'
|
|
350
|
+
}
|
|
351
|
+
/>
|
|
352
|
+
<div className="rounded-md border border-gray-200 bg-gray-50 p-4 text-xs text-gray-700">
|
|
353
|
+
<p className="font-bold text-gray-900">OAuth setup notes</p>
|
|
354
|
+
<p className="mt-2">
|
|
355
|
+
Ensure Google Calendar API is enabled in APIs & Services
|
|
356
|
+
for this same project before running OAuth.
|
|
357
|
+
</p>
|
|
358
|
+
<ol className="mt-2 list-decimal space-y-1 pl-4">
|
|
359
|
+
<li>
|
|
360
|
+
Open Google Auth Platform <strong>Clients</strong>, create a
|
|
361
|
+
Web application client, and set Authorized redirect URI to:
|
|
362
|
+
<code className="ml-1 rounded bg-gray-100 px-1 py-0.5">
|
|
363
|
+
{GOOGLE_REDIRECT_URI}
|
|
364
|
+
</code>
|
|
365
|
+
</li>
|
|
366
|
+
<li>
|
|
367
|
+
Open <strong>Audience</strong> and set:
|
|
368
|
+
<ul className="mt-1 list-disc space-y-1 pl-4">
|
|
369
|
+
<li>App type: External</li>
|
|
370
|
+
<li>Publishing status: In production</li>
|
|
371
|
+
</ul>
|
|
372
|
+
</li>
|
|
373
|
+
<li>
|
|
374
|
+
Open <strong>Data Access</strong> and add scope:
|
|
375
|
+
<code className="ml-1 rounded bg-gray-100 px-1 py-0.5">
|
|
376
|
+
{GOOGLE_SCOPE}
|
|
377
|
+
</code>
|
|
378
|
+
</li>
|
|
379
|
+
<li>
|
|
380
|
+
Open <strong>Branding</strong> and set:
|
|
381
|
+
<ul className="mt-1 list-disc space-y-1 pl-4">
|
|
382
|
+
<li>
|
|
383
|
+
Application home page:
|
|
384
|
+
<code className="ml-1 rounded bg-gray-100 px-1 py-0.5">
|
|
385
|
+
{GOOGLE_APP_HOME_URL}
|
|
386
|
+
</code>
|
|
387
|
+
</li>
|
|
388
|
+
<li>
|
|
389
|
+
Application privacy policy link:
|
|
390
|
+
<code className="ml-1 rounded bg-gray-100 px-1 py-0.5">
|
|
391
|
+
{GOOGLE_PRIVACY_URL}
|
|
392
|
+
</code>
|
|
393
|
+
</li>
|
|
394
|
+
<li>
|
|
395
|
+
Application terms of service link:
|
|
396
|
+
<code className="ml-1 rounded bg-gray-100 px-1 py-0.5">
|
|
397
|
+
{GOOGLE_TERMS_URL}
|
|
398
|
+
</code>
|
|
399
|
+
</li>
|
|
400
|
+
<li>
|
|
401
|
+
Authorized domain:
|
|
402
|
+
<code className="ml-1 rounded bg-gray-100 px-1 py-0.5">
|
|
403
|
+
{GOOGLE_AUTHORIZED_DOMAIN}
|
|
404
|
+
</code>
|
|
405
|
+
</li>
|
|
406
|
+
</ul>
|
|
407
|
+
</li>
|
|
408
|
+
<li>
|
|
409
|
+
Open <strong>Verification Center</strong>, start
|
|
410
|
+
verification, and use this default demo video URL:
|
|
411
|
+
<code className="ml-1 rounded bg-gray-100 px-1 py-0.5">
|
|
412
|
+
{GOOGLE_VERIFICATION_VIDEO_URL}
|
|
413
|
+
</code>
|
|
414
|
+
</li>
|
|
415
|
+
<li>
|
|
416
|
+
Create client credentials and copy:
|
|
417
|
+
<ul className="mt-1 list-disc space-y-1 pl-4">
|
|
418
|
+
<li>
|
|
419
|
+
web.client_id into Google OAuth Client ID in StoryKeep.
|
|
420
|
+
</li>
|
|
421
|
+
<li>
|
|
422
|
+
web.client_secret into Google OAuth Client Secret in
|
|
423
|
+
StoryKeep.
|
|
424
|
+
</li>
|
|
425
|
+
</ul>
|
|
426
|
+
</li>
|
|
427
|
+
<li>
|
|
428
|
+
Open Google Calendar, select the target calendar, then copy
|
|
429
|
+
Calendar ID from Integrate calendar settings.
|
|
430
|
+
</li>
|
|
431
|
+
<li>
|
|
432
|
+
Save credentials in StoryKeep, then click Connect Google to
|
|
433
|
+
complete OAuth and store refresh token linkage.
|
|
434
|
+
</li>
|
|
435
|
+
</ol>
|
|
436
|
+
</div>
|
|
437
|
+
<div className="flex gap-3">
|
|
438
|
+
{canStartGoogleConnect ? (
|
|
439
|
+
<button
|
|
440
|
+
type="button"
|
|
441
|
+
onClick={handleGoogleConnect}
|
|
442
|
+
disabled={isGoogleConnecting}
|
|
443
|
+
className="rounded-md bg-black px-4 py-2 text-xs font-bold text-white hover:bg-gray-800 disabled:opacity-50"
|
|
444
|
+
>
|
|
445
|
+
{isGoogleConnecting ? 'Connecting...' : 'Connect Google'}
|
|
446
|
+
</button>
|
|
447
|
+
) : (
|
|
448
|
+
<div className="flex items-center rounded-md border border-amber-200 bg-amber-50 px-3 py-2 text-xs font-bold text-amber-800">
|
|
449
|
+
{googleHasSync
|
|
450
|
+
? 'Google is already connected'
|
|
451
|
+
: 'Save Google credentials before connecting'}
|
|
452
|
+
</div>
|
|
453
|
+
)}
|
|
454
|
+
<button
|
|
455
|
+
type="button"
|
|
456
|
+
onClick={handleGoogleDisconnect}
|
|
457
|
+
disabled={isGoogleDisconnecting || !googleHasSync}
|
|
458
|
+
className="rounded-md border border-gray-300 bg-white px-4 py-2 text-xs font-bold text-gray-700 hover:bg-gray-100 disabled:opacity-50"
|
|
459
|
+
>
|
|
460
|
+
{isGoogleDisconnecting ? 'Disconnecting...' : 'Disconnect'}
|
|
461
|
+
</button>
|
|
462
|
+
</div>
|
|
463
|
+
</div>
|
|
464
|
+
</div>
|
|
247
465
|
</div>
|
|
248
466
|
</div>
|
|
249
467
|
|
|
@@ -300,7 +518,7 @@ export default function APIConfigSection({
|
|
|
300
518
|
<ul className="mt-2 list-disc space-y-1 pl-5">
|
|
301
519
|
<li>
|
|
302
520
|
<strong>Event:</strong> Select the specific event (e.g.,
|
|
303
|
-
Order payment, Product
|
|
521
|
+
Order payment, Product update).
|
|
304
522
|
</li>
|
|
305
523
|
<li>
|
|
306
524
|
<strong>Format:</strong> Select JSON (TractStack relies
|
|
@@ -358,12 +576,6 @@ export default function APIConfigSection({
|
|
|
358
576
|
</span>{' '}
|
|
359
577
|
Order payment
|
|
360
578
|
</li>
|
|
361
|
-
<li>
|
|
362
|
-
<span className="font-bold text-gray-900">
|
|
363
|
-
Topic Header:
|
|
364
|
-
</span>{' '}
|
|
365
|
-
orders/paid
|
|
366
|
-
</li>
|
|
367
579
|
<li>
|
|
368
580
|
<span className="font-bold text-gray-900">
|
|
369
581
|
Purpose:
|
|
@@ -374,24 +586,6 @@ export default function APIConfigSection({
|
|
|
374
586
|
</ul>
|
|
375
587
|
</div>
|
|
376
588
|
|
|
377
|
-
<div className="rounded border border-gray-200 bg-gray-50 p-4">
|
|
378
|
-
<h5 className="font-bold">2. Product Creation</h5>
|
|
379
|
-
<ul className="mt-2 space-y-1 text-xs">
|
|
380
|
-
<li>
|
|
381
|
-
<span className="font-bold text-gray-900">
|
|
382
|
-
Shopify Event Name:
|
|
383
|
-
</span>{' '}
|
|
384
|
-
Product creation
|
|
385
|
-
</li>
|
|
386
|
-
<li>
|
|
387
|
-
<span className="font-bold text-gray-900">
|
|
388
|
-
Topic Header:
|
|
389
|
-
</span>{' '}
|
|
390
|
-
products/create
|
|
391
|
-
</li>
|
|
392
|
-
</ul>
|
|
393
|
-
</div>
|
|
394
|
-
|
|
395
589
|
<div className="rounded border border-gray-200 bg-gray-50 p-4">
|
|
396
590
|
<h5 className="font-bold">3. Product Update</h5>
|
|
397
591
|
<ul className="mt-2 space-y-1 text-xs">
|
|
@@ -401,12 +595,6 @@ export default function APIConfigSection({
|
|
|
401
595
|
</span>{' '}
|
|
402
596
|
Product update
|
|
403
597
|
</li>
|
|
404
|
-
<li>
|
|
405
|
-
<span className="font-bold text-gray-900">
|
|
406
|
-
Topic Header:
|
|
407
|
-
</span>{' '}
|
|
408
|
-
products/update
|
|
409
|
-
</li>
|
|
410
598
|
</ul>
|
|
411
599
|
</div>
|
|
412
600
|
|
|
@@ -419,12 +607,6 @@ export default function APIConfigSection({
|
|
|
419
607
|
</span>{' '}
|
|
420
608
|
Product deletion
|
|
421
609
|
</li>
|
|
422
|
-
<li>
|
|
423
|
-
<span className="font-bold text-gray-900">
|
|
424
|
-
Topic Header:
|
|
425
|
-
</span>{' '}
|
|
426
|
-
products/delete
|
|
427
|
-
</li>
|
|
428
610
|
</ul>
|
|
429
611
|
</div>
|
|
430
612
|
</div>
|
|
@@ -48,6 +48,16 @@ export default function SiteConfigSection({
|
|
|
48
48
|
error={errors.footer}
|
|
49
49
|
/>
|
|
50
50
|
|
|
51
|
+
<StringInput
|
|
52
|
+
value={state.adminEmail}
|
|
53
|
+
onChange={(value) => updateField('adminEmail', value)}
|
|
54
|
+
label="Admin Email"
|
|
55
|
+
type="email"
|
|
56
|
+
placeholder="admin@example.com"
|
|
57
|
+
required={true}
|
|
58
|
+
error={errors.adminEmail}
|
|
59
|
+
/>
|
|
60
|
+
|
|
51
61
|
<StringInput
|
|
52
62
|
value={state.gtag}
|
|
53
63
|
onChange={(value) => updateField('gtag', value)}
|
|
@@ -222,6 +222,50 @@ export default function SchedulingSection({
|
|
|
222
222
|
className="mt-2 block w-full rounded-md border-gray-300 px-2 py-3 focus:border-cyan-700 focus:ring-cyan-700 md:text-sm"
|
|
223
223
|
/>
|
|
224
224
|
</div>
|
|
225
|
+
<div className="md:col-span-3 xl:col-span-4">
|
|
226
|
+
<label className="block text-xs font-black uppercase tracking-widest text-gray-500">
|
|
227
|
+
Allow Remote Booking
|
|
228
|
+
</label>
|
|
229
|
+
<label className="mt-2 flex items-center gap-3">
|
|
230
|
+
<input
|
|
231
|
+
type="checkbox"
|
|
232
|
+
checked={config.remoteOnly ? true : config.allowRemote}
|
|
233
|
+
disabled={config.remoteOnly}
|
|
234
|
+
onChange={(e) =>
|
|
235
|
+
updateField('scheduling', {
|
|
236
|
+
...config,
|
|
237
|
+
allowRemote: e.target.checked,
|
|
238
|
+
})
|
|
239
|
+
}
|
|
240
|
+
className="h-4 w-4 rounded border-gray-300 text-cyan-700 focus:ring-cyan-700 disabled:opacity-50"
|
|
241
|
+
/>
|
|
242
|
+
<span className="text-sm font-bold text-gray-700">
|
|
243
|
+
Enable remote option in checkout
|
|
244
|
+
</span>
|
|
245
|
+
</label>
|
|
246
|
+
</div>
|
|
247
|
+
<div className="md:col-span-3 xl:col-span-4">
|
|
248
|
+
<label className="block text-xs font-black uppercase tracking-widest text-gray-500">
|
|
249
|
+
Remote Only
|
|
250
|
+
</label>
|
|
251
|
+
<label className="mt-2 flex items-center gap-3">
|
|
252
|
+
<input
|
|
253
|
+
type="checkbox"
|
|
254
|
+
checked={config.remoteOnly}
|
|
255
|
+
onChange={(e) =>
|
|
256
|
+
updateField('scheduling', {
|
|
257
|
+
...config,
|
|
258
|
+
remoteOnly: e.target.checked,
|
|
259
|
+
allowRemote: e.target.checked ? true : config.allowRemote,
|
|
260
|
+
})
|
|
261
|
+
}
|
|
262
|
+
className="h-4 w-4 rounded border-gray-300 text-cyan-700 focus:ring-cyan-700"
|
|
263
|
+
/>
|
|
264
|
+
<span className="text-sm font-bold text-gray-700">
|
|
265
|
+
Force all bookings to remote
|
|
266
|
+
</span>
|
|
267
|
+
</label>
|
|
268
|
+
</div>
|
|
225
269
|
</div>
|
|
226
270
|
</div>
|
|
227
271
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState, useEffect } from 'react';
|
|
1
|
+
import { useState, useEffect, useRef } from 'react';
|
|
2
2
|
import { useFormState } from '@/hooks/useFormState';
|
|
3
3
|
import {
|
|
4
4
|
convertToLocalState,
|
|
@@ -28,6 +28,7 @@ export default function StoryKeepDashboard_Advanced({
|
|
|
28
28
|
const [status, setStatus] = useState<AdvancedConfigStatus | null>(null);
|
|
29
29
|
const [isLoading, setIsLoading] = useState(true);
|
|
30
30
|
const [error, setError] = useState<string>('');
|
|
31
|
+
const hasHydratedInitialFormState = useRef(false);
|
|
31
32
|
|
|
32
33
|
// Load status on mount
|
|
33
34
|
useEffect(() => {
|
|
@@ -77,6 +78,14 @@ export default function StoryKeepDashboard_Advanced({
|
|
|
77
78
|
},
|
|
78
79
|
});
|
|
79
80
|
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
if (!status || hasHydratedInitialFormState.current) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
formState.resetToState(convertToLocalState(status));
|
|
86
|
+
hasHydratedInitialFormState.current = true;
|
|
87
|
+
}, [status, formState]);
|
|
88
|
+
|
|
80
89
|
if (isLoading) {
|
|
81
90
|
return (
|
|
82
91
|
<div className="flex justify-center py-12">
|
|
@@ -26,6 +26,7 @@ import ShopifyDashboard_Services from './shopify/ShopifyDashboard_Services';
|
|
|
26
26
|
import ShopifyDashboard_Schedule from './shopify/ShopifyDashboard_Schedule';
|
|
27
27
|
import ShopifyDashboard_Search from './shopify/ShopifyDashboard_Search';
|
|
28
28
|
import ShopifyDashboard_Bookings from './shopify/ShopifyDashboard_Bookings';
|
|
29
|
+
import ShopifyDashboard_Emails from './shopify/ShopifyDashboard_Emails';
|
|
29
30
|
|
|
30
31
|
interface DashboardShopifyProps {
|
|
31
32
|
brandConfig: BrandConfig;
|
|
@@ -78,7 +79,8 @@ export default function StoryKeepDashboard_Shopify({
|
|
|
78
79
|
{ id: 'products', name: 'Products' },
|
|
79
80
|
{ id: 'services', name: 'Services' },
|
|
80
81
|
{ id: 'schedule', name: 'Schedule' },
|
|
81
|
-
{ id: 'search', name: '
|
|
82
|
+
{ id: 'search', name: 'Import Products' },
|
|
83
|
+
{ id: 'emails', name: 'Emails' },
|
|
82
84
|
];
|
|
83
85
|
|
|
84
86
|
useEffect(() => {
|
|
@@ -171,7 +173,7 @@ export default function StoryKeepDashboard_Shopify({
|
|
|
171
173
|
|
|
172
174
|
const executePreFlightCheck = (category: string, product: ShopifyProduct) => {
|
|
173
175
|
const hasMode = product.options.some((opt) => opt.name === 'Mode');
|
|
174
|
-
if (hasMode) {
|
|
176
|
+
if (category === 'service' || hasMode) {
|
|
175
177
|
startCreateFlow(category, product);
|
|
176
178
|
} else {
|
|
177
179
|
setPendingImport({ category, product });
|
|
@@ -276,6 +278,12 @@ export default function StoryKeepDashboard_Shopify({
|
|
|
276
278
|
group: { type: 'string', optional: true },
|
|
277
279
|
shopifyData: { type: 'string', optional: true },
|
|
278
280
|
shopifyImage: { type: 'string', optional: true, defaultValue: '{}' },
|
|
281
|
+
allowRemote: {
|
|
282
|
+
type: 'boolean',
|
|
283
|
+
optional: false,
|
|
284
|
+
defaultValue: false,
|
|
285
|
+
},
|
|
286
|
+
remoteOnly: { type: 'boolean', optional: false, defaultValue: false },
|
|
279
287
|
bookingLengthMinutes: {
|
|
280
288
|
type: 'number',
|
|
281
289
|
optional: false,
|
|
@@ -465,15 +473,12 @@ export default function StoryKeepDashboard_Shopify({
|
|
|
465
473
|
</div>
|
|
466
474
|
)}
|
|
467
475
|
|
|
468
|
-
{activeTab === 'dashboards' &&
|
|
469
|
-
<ShopifyDashboard existingResources={resources} />
|
|
470
|
-
)}
|
|
476
|
+
{activeTab === 'dashboards' && <ShopifyDashboard />}
|
|
471
477
|
|
|
472
478
|
{activeTab === 'bookings' && (
|
|
473
479
|
<ShopifyDashboard_Bookings existingResources={resources} />
|
|
474
480
|
)}
|
|
475
481
|
|
|
476
|
-
{/* Local Management Tabs */}
|
|
477
482
|
{activeTab === 'products' && (
|
|
478
483
|
<ShopifyDashboard_Products
|
|
479
484
|
resources={resources}
|
|
@@ -492,12 +497,10 @@ export default function StoryKeepDashboard_Shopify({
|
|
|
492
497
|
/>
|
|
493
498
|
)}
|
|
494
499
|
|
|
495
|
-
{/* Schedule Tab */}
|
|
496
500
|
{activeTab === 'schedule' && (
|
|
497
501
|
<ShopifyDashboard_Schedule brandConfig={brandConfig} />
|
|
498
502
|
)}
|
|
499
503
|
|
|
500
|
-
{/* Catalog Discovery Tab */}
|
|
501
504
|
{activeTab === 'search' && (
|
|
502
505
|
<ShopifyDashboard_Search
|
|
503
506
|
linkedResourceMap={linkedResourceMap}
|
|
@@ -507,6 +510,8 @@ export default function StoryKeepDashboard_Shopify({
|
|
|
507
510
|
onEdit={handleEditFromCatalog}
|
|
508
511
|
/>
|
|
509
512
|
)}
|
|
513
|
+
|
|
514
|
+
{activeTab === 'emails' && <ShopifyDashboard_Emails />}
|
|
510
515
|
</div>
|
|
511
516
|
|
|
512
517
|
{/* Shared Modals */}
|
|
@@ -652,6 +657,9 @@ export default function StoryKeepDashboard_Shopify({
|
|
|
652
657
|
draftResource.categorySlug || ''
|
|
653
658
|
] || {}
|
|
654
659
|
}
|
|
660
|
+
tenantRemoteOnly={Boolean(
|
|
661
|
+
internalBrandConfig?.scheduling?.remoteOnly
|
|
662
|
+
)}
|
|
655
663
|
isCreate={isCreateMode}
|
|
656
664
|
onClose={(saved) => {
|
|
657
665
|
setShowResourceModal(false);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState, useEffect } from 'react';
|
|
1
|
+
import { useState, useEffect, type KeyboardEvent } from 'react';
|
|
2
2
|
import { useStore } from '@nanostores/react';
|
|
3
3
|
import PlusIcon from '@heroicons/react/24/outline/PlusIcon';
|
|
4
4
|
import XMarkIcon from '@heroicons/react/24/outline/XMarkIcon';
|
|
@@ -125,7 +125,7 @@ export default function BeliefForm({
|
|
|
125
125
|
}
|
|
126
126
|
};
|
|
127
127
|
|
|
128
|
-
const handleKeyDown = (e:
|
|
128
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
129
129
|
if (e.key === 'Enter') {
|
|
130
130
|
e.preventDefault();
|
|
131
131
|
handleAddCustomValue();
|
|
@@ -278,6 +278,7 @@ const ManageContent = ({
|
|
|
278
278
|
fullContentMap={currentContentMap}
|
|
279
279
|
categorySlug={activeResourceForm.category}
|
|
280
280
|
categorySchema={knownResources[activeResourceForm.category] || {}}
|
|
281
|
+
tenantRemoteOnly={Boolean(brandConfig?.SCHEDULING?.remoteOnly)}
|
|
281
282
|
onClose={async (saved: boolean) => {
|
|
282
283
|
setActiveResourceForm(null);
|
|
283
284
|
if (saved) await refreshData();
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState, useRef, useEffect } from 'react';
|
|
1
|
+
import { useState, useRef, useEffect, type ChangeEvent } from 'react';
|
|
2
2
|
import { useStore } from '@nanostores/react';
|
|
3
3
|
import ChevronRightIcon from '@heroicons/react/24/outline/ChevronRightIcon';
|
|
4
4
|
import ChevronLeftIcon from '@heroicons/react/24/outline/ChevronLeftIcon';
|
|
@@ -53,7 +53,7 @@ export default function ProductTable({
|
|
|
53
53
|
};
|
|
54
54
|
}, []);
|
|
55
55
|
|
|
56
|
-
const handleSearchChange = (e:
|
|
56
|
+
const handleSearchChange = (e: ChangeEvent<HTMLInputElement>) => {
|
|
57
57
|
const val = e.target.value;
|
|
58
58
|
setInputValue(val);
|
|
59
59
|
setIsDebouncing(true);
|