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
|
@@ -13,13 +13,28 @@ 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_SCOPES = [
|
|
23
|
+
'https://www.googleapis.com/auth/calendar.events',
|
|
24
|
+
'https://www.googleapis.com/auth/calendar.readonly',
|
|
25
|
+
] as const;
|
|
26
|
+
const GOOGLE_VERIFICATION_VIDEO_URL =
|
|
27
|
+
'https://www.youtube.com/watch?v=kJI4XdqiiAI';
|
|
28
|
+
|
|
16
29
|
export default function APIConfigSection({
|
|
17
30
|
formState,
|
|
18
31
|
status,
|
|
19
32
|
}: APIConfigSectionProps) {
|
|
20
|
-
const { state, updateField, errors } = formState;
|
|
33
|
+
const { state, updateField, errors, isDirty, saveState } = formState;
|
|
21
34
|
|
|
22
35
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
36
|
+
const [isGoogleConnecting, setIsGoogleConnecting] = useState(false);
|
|
37
|
+
const [isGoogleDisconnecting, setIsGoogleDisconnecting] = useState(false);
|
|
23
38
|
const goBackend =
|
|
24
39
|
import.meta.env.PUBLIC_GO_BACKEND || 'http://localhost:8080';
|
|
25
40
|
|
|
@@ -31,7 +46,20 @@ export default function APIConfigSection({
|
|
|
31
46
|
const shopifyVersionConfigured = Boolean(status?.shopifyApiVersion);
|
|
32
47
|
const shopifyAdminSlugConfigured = status?.shopifyAdminSlugSet;
|
|
33
48
|
const shopifyWebhooksConfigured = status?.userSetupWebhooks;
|
|
34
|
-
const resendConfigured = status?.
|
|
49
|
+
const resendConfigured = status?.hasResend;
|
|
50
|
+
const resendKeyConfigured = status?.resendApiKeySet;
|
|
51
|
+
const googleHasSync = status?.hasGoogleSync;
|
|
52
|
+
const googleClientIDConfigured = status?.googleOauthClientIdSet;
|
|
53
|
+
const googleClientSecretConfigured = status?.googleOauthClientSecretSet;
|
|
54
|
+
const googleCalendarConfigured = status?.googleCalendarIdSet;
|
|
55
|
+
const googleCredentialsSaved = Boolean(
|
|
56
|
+
googleClientIDConfigured &&
|
|
57
|
+
googleClientSecretConfigured &&
|
|
58
|
+
googleCalendarConfigured
|
|
59
|
+
);
|
|
60
|
+
const googleCredentialsPendingSave = isDirty || saveState === 'saving';
|
|
61
|
+
const canStartGoogleConnect =
|
|
62
|
+
googleCredentialsSaved && !googleCredentialsPendingSave && !googleHasSync;
|
|
35
63
|
|
|
36
64
|
const renderStatusBadge = (isConfigured: boolean | undefined) => {
|
|
37
65
|
if (status === null) {
|
|
@@ -52,6 +80,44 @@ export default function APIConfigSection({
|
|
|
52
80
|
);
|
|
53
81
|
};
|
|
54
82
|
|
|
83
|
+
const handleGoogleConnect = async () => {
|
|
84
|
+
setIsGoogleConnecting(true);
|
|
85
|
+
try {
|
|
86
|
+
const response = await fetch('/api/google/oauth/start');
|
|
87
|
+
const payload = await response.json();
|
|
88
|
+
if (!response.ok || !payload.authorization) {
|
|
89
|
+
throw new Error(payload?.error || 'Failed to start Google OAuth');
|
|
90
|
+
}
|
|
91
|
+
window.location.href = payload.authorization;
|
|
92
|
+
} catch (error) {
|
|
93
|
+
console.error('Failed to start Google OAuth:', error);
|
|
94
|
+
alert(error instanceof Error ? error.message : 'Google OAuth failed');
|
|
95
|
+
} finally {
|
|
96
|
+
setIsGoogleConnecting(false);
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const handleGoogleDisconnect = async () => {
|
|
101
|
+
setIsGoogleDisconnecting(true);
|
|
102
|
+
try {
|
|
103
|
+
const response = await fetch('/api/google/oauth/disconnect', {
|
|
104
|
+
method: 'POST',
|
|
105
|
+
});
|
|
106
|
+
const payload = await response.json();
|
|
107
|
+
if (!response.ok) {
|
|
108
|
+
throw new Error(payload?.error || 'Failed to disconnect Google OAuth');
|
|
109
|
+
}
|
|
110
|
+
window.location.reload();
|
|
111
|
+
} catch (error) {
|
|
112
|
+
console.error('Failed to disconnect Google OAuth:', error);
|
|
113
|
+
alert(
|
|
114
|
+
error instanceof Error ? error.message : 'Failed to disconnect Google'
|
|
115
|
+
);
|
|
116
|
+
} finally {
|
|
117
|
+
setIsGoogleDisconnecting(false);
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
55
121
|
return (
|
|
56
122
|
<div className="bg-white shadow md:rounded-lg">
|
|
57
123
|
<div className="px-4 py-5 md:p-6">
|
|
@@ -236,13 +302,192 @@ export default function APIConfigSection({
|
|
|
236
302
|
value={state.resendApiKey}
|
|
237
303
|
onChange={(value) => updateField('resendApiKey', value)}
|
|
238
304
|
type="password"
|
|
239
|
-
placeholder={
|
|
305
|
+
placeholder={resendKeyConfigured ? '••••••••••••••••' : 're_...'}
|
|
240
306
|
error={errors.resendApiKey}
|
|
241
307
|
/>
|
|
242
308
|
<p className="mt-2 text-xs text-gray-500">
|
|
243
309
|
Required for sending system emails.
|
|
244
|
-
{
|
|
310
|
+
{resendKeyConfigured && ' Leave blank to keep existing key.'}
|
|
245
311
|
</p>
|
|
312
|
+
<StringInput
|
|
313
|
+
label="From Email"
|
|
314
|
+
value={state.adminEmail}
|
|
315
|
+
onChange={(value) => updateField('adminEmail', value)}
|
|
316
|
+
type="email"
|
|
317
|
+
placeholder="admin@example.com"
|
|
318
|
+
required={true}
|
|
319
|
+
error={errors.adminEmail}
|
|
320
|
+
/>
|
|
321
|
+
<StringInput
|
|
322
|
+
label="From Name"
|
|
323
|
+
value={state.adminEmailName}
|
|
324
|
+
onChange={(value) => updateField('adminEmailName', value)}
|
|
325
|
+
placeholder="Your Site Name"
|
|
326
|
+
required={true}
|
|
327
|
+
error={errors.adminEmailName}
|
|
328
|
+
/>
|
|
329
|
+
</div>
|
|
330
|
+
|
|
331
|
+
{/* Google Calendar / Meet Section */}
|
|
332
|
+
<div className="border-t border-gray-100 pt-6">
|
|
333
|
+
<div className="mb-4 flex items-center justify-between">
|
|
334
|
+
<h4 className="text-sm font-bold text-gray-900">
|
|
335
|
+
Google Calendar + Meet (Remote Sync)
|
|
336
|
+
</h4>
|
|
337
|
+
{renderStatusBadge(googleHasSync)}
|
|
338
|
+
</div>
|
|
339
|
+
<div className="space-y-4">
|
|
340
|
+
<StringInput
|
|
341
|
+
label="Google OAuth Client ID"
|
|
342
|
+
value={state.googleOauthClientId}
|
|
343
|
+
onChange={(value) => updateField('googleOauthClientId', value)}
|
|
344
|
+
placeholder={
|
|
345
|
+
googleClientIDConfigured
|
|
346
|
+
? '••••••••••••••••'
|
|
347
|
+
: 'Enter Google OAuth client ID'
|
|
348
|
+
}
|
|
349
|
+
/>
|
|
350
|
+
<StringInput
|
|
351
|
+
label="Google OAuth Client Secret"
|
|
352
|
+
value={state.googleOauthClientSecret}
|
|
353
|
+
onChange={(value) =>
|
|
354
|
+
updateField('googleOauthClientSecret', value)
|
|
355
|
+
}
|
|
356
|
+
type="password"
|
|
357
|
+
placeholder={
|
|
358
|
+
googleClientSecretConfigured
|
|
359
|
+
? '••••••••••••••••'
|
|
360
|
+
: 'Enter Google OAuth client secret'
|
|
361
|
+
}
|
|
362
|
+
/>
|
|
363
|
+
<StringInput
|
|
364
|
+
label="Google Calendar ID"
|
|
365
|
+
value={state.googleCalendarId}
|
|
366
|
+
onChange={(value) => updateField('googleCalendarId', value)}
|
|
367
|
+
placeholder={
|
|
368
|
+
googleCalendarConfigured
|
|
369
|
+
? '••••••••••••••••'
|
|
370
|
+
: 'example@gmail.com or calendar ID'
|
|
371
|
+
}
|
|
372
|
+
/>
|
|
373
|
+
<div className="rounded-md border border-gray-200 bg-gray-50 p-4 text-xs text-gray-700">
|
|
374
|
+
<p className="font-bold text-gray-900">OAuth setup notes</p>
|
|
375
|
+
<p className="mt-2">
|
|
376
|
+
Ensure Google Calendar API is enabled in APIs & Services
|
|
377
|
+
for this same project before running OAuth.
|
|
378
|
+
</p>
|
|
379
|
+
<ol className="mt-2 list-decimal space-y-1 pl-4">
|
|
380
|
+
<li>
|
|
381
|
+
Open Google Auth Platform <strong>Clients</strong>, create a
|
|
382
|
+
Web application client, and set Authorized redirect URI to:
|
|
383
|
+
<code className="ml-1 rounded bg-gray-100 px-1 py-0.5">
|
|
384
|
+
{GOOGLE_REDIRECT_URI}
|
|
385
|
+
</code>
|
|
386
|
+
</li>
|
|
387
|
+
<li>
|
|
388
|
+
Open <strong>Audience</strong> and set:
|
|
389
|
+
<ul className="mt-1 list-disc space-y-1 pl-4">
|
|
390
|
+
<li>App type: External</li>
|
|
391
|
+
<li>Publishing status: In production</li>
|
|
392
|
+
</ul>
|
|
393
|
+
</li>
|
|
394
|
+
<li>
|
|
395
|
+
Open <strong>Data Access</strong> and add scopes:
|
|
396
|
+
<ul className="mt-1 list-disc space-y-1 pl-4">
|
|
397
|
+
{GOOGLE_SCOPES.map((scope) => (
|
|
398
|
+
<li key={scope}>
|
|
399
|
+
<code className="rounded bg-gray-100 px-1 py-0.5">
|
|
400
|
+
{scope}
|
|
401
|
+
</code>
|
|
402
|
+
</li>
|
|
403
|
+
))}
|
|
404
|
+
</ul>
|
|
405
|
+
</li>
|
|
406
|
+
<li>
|
|
407
|
+
Open <strong>Branding</strong> and set:
|
|
408
|
+
<ul className="mt-1 list-disc space-y-1 pl-4">
|
|
409
|
+
<li>
|
|
410
|
+
Application home page:
|
|
411
|
+
<code className="ml-1 rounded bg-gray-100 px-1 py-0.5">
|
|
412
|
+
{GOOGLE_APP_HOME_URL}
|
|
413
|
+
</code>
|
|
414
|
+
</li>
|
|
415
|
+
<li>
|
|
416
|
+
Application privacy policy link:
|
|
417
|
+
<code className="ml-1 rounded bg-gray-100 px-1 py-0.5">
|
|
418
|
+
{GOOGLE_PRIVACY_URL}
|
|
419
|
+
</code>
|
|
420
|
+
</li>
|
|
421
|
+
<li>
|
|
422
|
+
Application terms of service link:
|
|
423
|
+
<code className="ml-1 rounded bg-gray-100 px-1 py-0.5">
|
|
424
|
+
{GOOGLE_TERMS_URL}
|
|
425
|
+
</code>
|
|
426
|
+
</li>
|
|
427
|
+
<li>
|
|
428
|
+
Authorized domain:
|
|
429
|
+
<code className="ml-1 rounded bg-gray-100 px-1 py-0.5">
|
|
430
|
+
{GOOGLE_AUTHORIZED_DOMAIN}
|
|
431
|
+
</code>
|
|
432
|
+
</li>
|
|
433
|
+
</ul>
|
|
434
|
+
</li>
|
|
435
|
+
<li>
|
|
436
|
+
Open <strong>Verification Center</strong>, start
|
|
437
|
+
verification, and use this default demo video URL:
|
|
438
|
+
<code className="ml-1 rounded bg-gray-100 px-1 py-0.5">
|
|
439
|
+
{GOOGLE_VERIFICATION_VIDEO_URL}
|
|
440
|
+
</code>
|
|
441
|
+
</li>
|
|
442
|
+
<li>
|
|
443
|
+
Create client credentials and copy:
|
|
444
|
+
<ul className="mt-1 list-disc space-y-1 pl-4">
|
|
445
|
+
<li>
|
|
446
|
+
web.client_id into Google OAuth Client ID in StoryKeep.
|
|
447
|
+
</li>
|
|
448
|
+
<li>
|
|
449
|
+
web.client_secret into Google OAuth Client Secret in
|
|
450
|
+
StoryKeep.
|
|
451
|
+
</li>
|
|
452
|
+
</ul>
|
|
453
|
+
</li>
|
|
454
|
+
<li>
|
|
455
|
+
Open Google Calendar, select the target calendar, then copy
|
|
456
|
+
Calendar ID from Integrate calendar settings.
|
|
457
|
+
</li>
|
|
458
|
+
<li>
|
|
459
|
+
Save credentials in StoryKeep, then click Connect Google to
|
|
460
|
+
complete OAuth and store refresh token linkage.
|
|
461
|
+
</li>
|
|
462
|
+
</ol>
|
|
463
|
+
</div>
|
|
464
|
+
<div className="flex gap-3">
|
|
465
|
+
{canStartGoogleConnect ? (
|
|
466
|
+
<button
|
|
467
|
+
type="button"
|
|
468
|
+
onClick={handleGoogleConnect}
|
|
469
|
+
disabled={isGoogleConnecting}
|
|
470
|
+
className="rounded-md bg-black px-4 py-2 text-xs font-bold text-white hover:bg-gray-800 disabled:opacity-50"
|
|
471
|
+
>
|
|
472
|
+
{isGoogleConnecting ? 'Connecting...' : 'Connect Google'}
|
|
473
|
+
</button>
|
|
474
|
+
) : (
|
|
475
|
+
<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">
|
|
476
|
+
{googleHasSync
|
|
477
|
+
? 'Google is already connected'
|
|
478
|
+
: 'Save Google credentials before connecting'}
|
|
479
|
+
</div>
|
|
480
|
+
)}
|
|
481
|
+
<button
|
|
482
|
+
type="button"
|
|
483
|
+
onClick={handleGoogleDisconnect}
|
|
484
|
+
disabled={isGoogleDisconnecting || !googleHasSync}
|
|
485
|
+
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"
|
|
486
|
+
>
|
|
487
|
+
{isGoogleDisconnecting ? 'Disconnecting...' : 'Disconnect'}
|
|
488
|
+
</button>
|
|
489
|
+
</div>
|
|
490
|
+
</div>
|
|
246
491
|
</div>
|
|
247
492
|
</div>
|
|
248
493
|
</div>
|
|
@@ -58,6 +58,15 @@ export default function SiteConfigSection({
|
|
|
58
58
|
error={errors.adminEmail}
|
|
59
59
|
/>
|
|
60
60
|
|
|
61
|
+
<StringInput
|
|
62
|
+
value={state.adminEmailName}
|
|
63
|
+
onChange={(value) => updateField('adminEmailName', value)}
|
|
64
|
+
label="Admin Email Name"
|
|
65
|
+
placeholder="Your Site Name"
|
|
66
|
+
required={true}
|
|
67
|
+
error={errors.adminEmailName}
|
|
68
|
+
/>
|
|
69
|
+
|
|
61
70
|
<StringInput
|
|
62
71
|
value={state.gtag}
|
|
63
72
|
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,42 +1,71 @@
|
|
|
1
|
-
import { useState, useEffect } from 'react';
|
|
1
|
+
import { useState, useEffect, useRef, useCallback } from 'react';
|
|
2
2
|
import { useFormState } from '@/hooks/useFormState';
|
|
3
3
|
import {
|
|
4
|
-
convertToLocalState,
|
|
4
|
+
convertToLocalState as convertAdvancedToLocalState,
|
|
5
5
|
convertToBackendFormat,
|
|
6
6
|
validateAdvancedConfig,
|
|
7
7
|
advancedStateIntercept,
|
|
8
8
|
} from '@/utils/api/advancedHelpers';
|
|
9
|
+
import {
|
|
10
|
+
convertToLocalState as convertBrandToLocalState,
|
|
11
|
+
convertToBackendFormat as convertBrandToBackendFormat,
|
|
12
|
+
validateBrandConfig,
|
|
13
|
+
} from '@/utils/api/brandHelpers';
|
|
9
14
|
import {
|
|
10
15
|
getAdvancedConfigStatus,
|
|
11
16
|
saveAdvancedConfig,
|
|
12
17
|
} from '@/utils/api/advancedConfig';
|
|
18
|
+
import { getBrandConfig, saveBrandConfig } from '@/utils/api/brandConfig';
|
|
13
19
|
import UnsavedChangesBar from '@/components/form/UnsavedChangesBar';
|
|
14
20
|
import AuthConfigSection from '@/components/form/advanced/AuthConfigSection';
|
|
15
21
|
import APIConfigSection from '@/components/form/advanced/APIConfigSection';
|
|
16
22
|
import type {
|
|
17
23
|
AdvancedConfigState,
|
|
18
24
|
AdvancedConfigStatus,
|
|
25
|
+
BrandConfig,
|
|
19
26
|
} from '@/types/tractstack';
|
|
20
27
|
|
|
21
28
|
interface StoryKeepDashboardAdvancedProps {
|
|
29
|
+
brandConfig: BrandConfig;
|
|
22
30
|
initialize?: boolean;
|
|
23
31
|
}
|
|
24
32
|
|
|
25
33
|
export default function StoryKeepDashboard_Advanced({
|
|
34
|
+
brandConfig,
|
|
26
35
|
initialize = false,
|
|
27
36
|
}: StoryKeepDashboardAdvancedProps) {
|
|
28
37
|
const [status, setStatus] = useState<AdvancedConfigStatus | null>(null);
|
|
29
38
|
const [isLoading, setIsLoading] = useState(true);
|
|
30
39
|
const [error, setError] = useState<string>('');
|
|
40
|
+
const hasHydratedInitialFormState = useRef(false);
|
|
41
|
+
|
|
42
|
+
const tenantId = window.TRACTSTACK_CONFIG?.tenantId || 'default';
|
|
43
|
+
|
|
44
|
+
const validateAdvancedForm = useCallback(
|
|
45
|
+
(state: AdvancedConfigState) => {
|
|
46
|
+
const advancedErrors = validateAdvancedConfig(state);
|
|
47
|
+
const brandLocal = {
|
|
48
|
+
...convertBrandToLocalState(brandConfig),
|
|
49
|
+
adminEmail: state.adminEmail,
|
|
50
|
+
adminEmailName: state.adminEmailName,
|
|
51
|
+
};
|
|
52
|
+
const brandErrors = validateBrandConfig(brandLocal);
|
|
53
|
+
return {
|
|
54
|
+
...advancedErrors,
|
|
55
|
+
...(brandErrors.adminEmail && { adminEmail: brandErrors.adminEmail }),
|
|
56
|
+
...(brandErrors.adminEmailName && {
|
|
57
|
+
adminEmailName: brandErrors.adminEmailName,
|
|
58
|
+
}),
|
|
59
|
+
};
|
|
60
|
+
},
|
|
61
|
+
[brandConfig]
|
|
62
|
+
);
|
|
31
63
|
|
|
32
|
-
// Load status on mount
|
|
33
64
|
useEffect(() => {
|
|
34
65
|
async function loadStatus() {
|
|
35
66
|
try {
|
|
36
67
|
setIsLoading(true);
|
|
37
|
-
const statusData = await getAdvancedConfigStatus(
|
|
38
|
-
window.TRACTSTACK_CONFIG?.tenantId || 'default'
|
|
39
|
-
);
|
|
68
|
+
const statusData = await getAdvancedConfigStatus(tenantId);
|
|
40
69
|
setStatus(statusData);
|
|
41
70
|
} catch (err) {
|
|
42
71
|
setError(
|
|
@@ -49,27 +78,31 @@ export default function StoryKeepDashboard_Advanced({
|
|
|
49
78
|
}
|
|
50
79
|
}
|
|
51
80
|
loadStatus();
|
|
52
|
-
}, []);
|
|
81
|
+
}, [tenantId]);
|
|
53
82
|
|
|
54
83
|
const formState = useFormState<AdvancedConfigState>({
|
|
55
|
-
initialData:
|
|
56
|
-
validator:
|
|
84
|
+
initialData: convertAdvancedToLocalState(status),
|
|
85
|
+
validator: validateAdvancedForm,
|
|
57
86
|
interceptor: advancedStateIntercept,
|
|
58
87
|
onSave: async (state: AdvancedConfigState) => {
|
|
59
88
|
const backendPayload = convertToBackendFormat(state);
|
|
60
|
-
await saveAdvancedConfig(
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
);
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
);
|
|
89
|
+
await saveAdvancedConfig(tenantId, backendPayload);
|
|
90
|
+
|
|
91
|
+
const brandConfigFresh = await getBrandConfig(tenantId);
|
|
92
|
+
const brandLocal = convertBrandToLocalState(brandConfigFresh);
|
|
93
|
+
brandLocal.adminEmail = state.adminEmail.trim();
|
|
94
|
+
brandLocal.adminEmailName = state.adminEmailName.trim();
|
|
95
|
+
await saveBrandConfig(tenantId, convertBrandToBackendFormat(brandLocal));
|
|
96
|
+
|
|
97
|
+
const newStatus = await getAdvancedConfigStatus(tenantId);
|
|
69
98
|
setStatus(newStatus);
|
|
70
99
|
|
|
71
|
-
|
|
72
|
-
const newState =
|
|
100
|
+
const brandLocalHydrate = convertBrandToLocalState(brandConfigFresh);
|
|
101
|
+
const newState = {
|
|
102
|
+
...convertAdvancedToLocalState(newStatus),
|
|
103
|
+
adminEmail: brandLocalHydrate.adminEmail,
|
|
104
|
+
adminEmailName: brandLocalHydrate.adminEmailName,
|
|
105
|
+
};
|
|
73
106
|
formState.resetToState(newState);
|
|
74
107
|
|
|
75
108
|
window.location.reload();
|
|
@@ -77,6 +110,19 @@ export default function StoryKeepDashboard_Advanced({
|
|
|
77
110
|
},
|
|
78
111
|
});
|
|
79
112
|
|
|
113
|
+
useEffect(() => {
|
|
114
|
+
if (!status || hasHydratedInitialFormState.current) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const brandLocal = convertBrandToLocalState(brandConfig);
|
|
118
|
+
formState.resetToState({
|
|
119
|
+
...convertAdvancedToLocalState(status),
|
|
120
|
+
adminEmail: brandLocal.adminEmail,
|
|
121
|
+
adminEmailName: brandLocal.adminEmailName,
|
|
122
|
+
});
|
|
123
|
+
hasHydratedInitialFormState.current = true;
|
|
124
|
+
}, [status, formState, brandConfig]);
|
|
125
|
+
|
|
80
126
|
if (isLoading) {
|
|
81
127
|
return (
|
|
82
128
|
<div className="flex justify-center py-12">
|
|
@@ -93,7 +139,6 @@ export default function StoryKeepDashboard_Advanced({
|
|
|
93
139
|
);
|
|
94
140
|
}
|
|
95
141
|
|
|
96
|
-
// Database status component
|
|
97
142
|
const DatabaseStatusSection = () => {
|
|
98
143
|
const databaseType = status?.tursoEnabled
|
|
99
144
|
? 'Turso Cloud Database'
|