astro-tractstack 2.2.10 → 2.3.0
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 +2 -2
- package/dist/index.js +89 -8
- package/package.json +3 -1
- package/templates/custom/minimal/CodeHook.astro +14 -5
- package/templates/custom/shopify/CalDotComBooking.tsx +44 -0
- package/templates/custom/shopify/Cart.tsx +345 -0
- package/templates/custom/shopify/CartIcon.tsx +47 -0
- package/templates/custom/shopify/CartModal.tsx +63 -0
- package/templates/custom/shopify/CheckoutModal.tsx +187 -0
- package/templates/custom/shopify/ShopifyCartManager.tsx +145 -0
- package/templates/custom/shopify/ShopifyCheckout.tsx +167 -0
- package/templates/custom/shopify/ShopifyProductGrid.tsx +281 -0
- package/templates/custom/shopify/ShopifyServiceList.tsx +118 -0
- package/templates/custom/shopify/cart.astro +23 -0
- package/templates/custom/with-examples/CodeHook.astro +9 -1
- package/templates/custom/with-examples/ProductGrid.astro +1 -1
- package/templates/src/client/app.js +4 -2
- package/templates/src/components/Header.astro +37 -11
- package/templates/src/components/form/advanced/APIConfigSection.tsx +165 -38
- package/templates/src/components/storykeep/Dashboard.tsx +17 -3
- 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 +525 -0
- package/templates/src/components/storykeep/StoryKeepBackdrop.astro +43 -23
- 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/ManageContent.tsx +4 -11
- package/templates/src/components/storykeep/controls/content/ProductTable.tsx +254 -0
- package/templates/src/components/storykeep/controls/content/ResourceForm.tsx +108 -8
- package/templates/src/lib/resources.ts +11 -21
- package/templates/src/pages/api/shopify/createCart.ts +73 -0
- package/templates/src/pages/api/shopify/getProducts.ts +64 -0
- package/templates/src/pages/storykeep/login.astro +5 -10
- 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 +210 -0
- package/templates/src/types/tractstack.ts +21 -0
- package/templates/src/utils/api/advancedConfig.ts +5 -1
- package/templates/src/utils/api/advancedHelpers.ts +48 -5
- package/templates/src/utils/api/brandHelpers.ts +4 -0
- package/templates/src/utils/api/resourceConfig.ts +13 -5
- package/templates/src/utils/customHelpers.ts +70 -0
- package/templates/src/utils/helpers.ts +59 -0
- package/utils/inject-files.ts +83 -2
|
@@ -16,53 +16,180 @@ export default function APIConfigSection({
|
|
|
16
16
|
}: APIConfigSectionProps) {
|
|
17
17
|
const { state, updateField, errors } = formState;
|
|
18
18
|
|
|
19
|
+
// Status flags
|
|
19
20
|
const aaiConfigured = status?.aaiAPIKeySet;
|
|
21
|
+
const shopifyStorefrontConfigured = status?.shopifyStorefrontTokenSet;
|
|
22
|
+
const shopifySecretConfigured = status?.shopifyApiSecretSet;
|
|
23
|
+
const shopifyDomainConfigured = status?.shopifyStoreDomainSet;
|
|
24
|
+
const shopifyVersionConfigured = Boolean(status?.shopifyApiVersion);
|
|
25
|
+
|
|
26
|
+
const resendConfigured = status?.resendApiKeySet;
|
|
27
|
+
|
|
28
|
+
const renderStatusBadge = (isConfigured: boolean | undefined) => {
|
|
29
|
+
if (status === null) {
|
|
30
|
+
return (
|
|
31
|
+
<span className="ml-2 inline-flex items-center rounded-full bg-gray-100 px-2 py-0.5 text-xs font-bold text-gray-800">
|
|
32
|
+
Loading...
|
|
33
|
+
</span>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
return isConfigured ? (
|
|
37
|
+
<span className="ml-2 inline-flex items-center rounded-full bg-green-100 px-2 py-0.5 text-xs font-bold text-green-800">
|
|
38
|
+
✓ Set
|
|
39
|
+
</span>
|
|
40
|
+
) : (
|
|
41
|
+
<span className="ml-2 inline-flex items-center rounded-full bg-yellow-100 px-2 py-0.5 text-xs font-bold text-yellow-800">
|
|
42
|
+
Not Set
|
|
43
|
+
</span>
|
|
44
|
+
);
|
|
45
|
+
};
|
|
20
46
|
|
|
21
47
|
return (
|
|
22
48
|
<div className="bg-white shadow md:rounded-lg">
|
|
23
49
|
<div className="px-4 py-5 md:p-6">
|
|
24
|
-
<
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
</h3>
|
|
28
|
-
<div className="flex items-center">
|
|
29
|
-
{aaiConfigured ? (
|
|
30
|
-
<span className="inline-flex items-center rounded-full bg-green-100 px-2.5 py-0.5 text-xs font-bold text-green-800">
|
|
31
|
-
✓ Configured
|
|
32
|
-
</span>
|
|
33
|
-
) : status !== null ? (
|
|
34
|
-
<span className="inline-flex items-center rounded-full bg-yellow-100 px-2.5 py-0.5 text-xs font-bold text-yellow-800">
|
|
35
|
-
Optional
|
|
36
|
-
</span>
|
|
37
|
-
) : (
|
|
38
|
-
<span className="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-bold text-gray-800">
|
|
39
|
-
Loading...
|
|
40
|
-
</span>
|
|
41
|
-
)}
|
|
42
|
-
</div>
|
|
43
|
-
</div>
|
|
50
|
+
<h3 className="text-base font-bold leading-6 text-gray-900">
|
|
51
|
+
API Integrations
|
|
52
|
+
</h3>
|
|
44
53
|
<div className="mt-2 max-w-xl text-sm text-gray-500">
|
|
45
|
-
<p>Configure external API keys for enhanced functionality.</p>
|
|
46
|
-
</div>
|
|
47
|
-
<div className="mt-5">
|
|
48
|
-
<StringInput
|
|
49
|
-
label="AssemblyAI API Key"
|
|
50
|
-
value={state.aaiApiKey}
|
|
51
|
-
onChange={(value) => updateField('aaiApiKey', value)}
|
|
52
|
-
type="password"
|
|
53
|
-
placeholder={
|
|
54
|
-
aaiConfigured ? '••••••••••••••••' : 'Enter AssemblyAI API key'
|
|
55
|
-
}
|
|
56
|
-
error={errors.aaiApiKey}
|
|
57
|
-
/>
|
|
58
|
-
</div>
|
|
59
|
-
<div className="mt-3 text-xs text-gray-500">
|
|
60
54
|
<p>
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
{aaiConfigured && ' Leave blank to keep existing key.'}
|
|
55
|
+
Configure external services to enable advanced features like AI,
|
|
56
|
+
Commerce, and Email.
|
|
64
57
|
</p>
|
|
65
58
|
</div>
|
|
59
|
+
|
|
60
|
+
<div className="mt-6 space-y-8">
|
|
61
|
+
{/* AssemblyAI Section */}
|
|
62
|
+
<div className="border-t border-gray-100 pt-6">
|
|
63
|
+
<div className="mb-4 flex items-center justify-between">
|
|
64
|
+
<h4 className="text-sm font-bold text-gray-900">
|
|
65
|
+
AssemblyAI (Audio Intelligence)
|
|
66
|
+
</h4>
|
|
67
|
+
{renderStatusBadge(aaiConfigured)}
|
|
68
|
+
</div>
|
|
69
|
+
<StringInput
|
|
70
|
+
label="API Key"
|
|
71
|
+
value={state.aaiApiKey}
|
|
72
|
+
onChange={(value) => updateField('aaiApiKey', value)}
|
|
73
|
+
type="password"
|
|
74
|
+
placeholder={
|
|
75
|
+
aaiConfigured ? '••••••••••••••••' : 'Enter AssemblyAI API key'
|
|
76
|
+
}
|
|
77
|
+
error={errors.aaiApiKey}
|
|
78
|
+
/>
|
|
79
|
+
<p className="mt-2 text-xs text-gray-500">
|
|
80
|
+
Required for audio transcription and analysis.
|
|
81
|
+
{aaiConfigured && ' Leave blank to keep existing key.'}
|
|
82
|
+
</p>
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
{/* Shopify Section */}
|
|
86
|
+
<div className="border-t border-gray-100 pt-6">
|
|
87
|
+
<div className="mb-4 flex items-center justify-between">
|
|
88
|
+
<h4 className="text-sm font-bold text-gray-900">
|
|
89
|
+
Shopify (Commerce)
|
|
90
|
+
</h4>
|
|
91
|
+
{renderStatusBadge(
|
|
92
|
+
shopifyStorefrontConfigured &&
|
|
93
|
+
shopifySecretConfigured &&
|
|
94
|
+
shopifyDomainConfigured &&
|
|
95
|
+
shopifyVersionConfigured
|
|
96
|
+
)}
|
|
97
|
+
</div>
|
|
98
|
+
<div className="space-y-4">
|
|
99
|
+
<div>
|
|
100
|
+
<StringInput
|
|
101
|
+
label="Shopify Store Domain"
|
|
102
|
+
value={state.shopifyStoreDomain}
|
|
103
|
+
onChange={(value) => updateField('shopifyStoreDomain', value)}
|
|
104
|
+
placeholder={
|
|
105
|
+
shopifyDomainConfigured
|
|
106
|
+
? 'your-shop.myshopify.com'
|
|
107
|
+
: 'your-shop.myshopify.com'
|
|
108
|
+
}
|
|
109
|
+
error={errors.shopifyStoreDomain}
|
|
110
|
+
/>
|
|
111
|
+
<p className="mt-1 text-xs text-gray-500">
|
|
112
|
+
The primary .myshopify.com domain for your store.
|
|
113
|
+
</p>
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
<div>
|
|
117
|
+
<StringInput
|
|
118
|
+
label="API Version"
|
|
119
|
+
value={state.shopifyApiVersion}
|
|
120
|
+
onChange={(value) => updateField('shopifyApiVersion', value)}
|
|
121
|
+
type="text"
|
|
122
|
+
placeholder="2026-01"
|
|
123
|
+
error={errors.shopifyApiVersion}
|
|
124
|
+
/>
|
|
125
|
+
<p className="mt-1 text-xs text-gray-500">
|
|
126
|
+
Target specific Shopify API version (YYYY-MM).
|
|
127
|
+
</p>
|
|
128
|
+
</div>
|
|
129
|
+
|
|
130
|
+
<div>
|
|
131
|
+
<StringInput
|
|
132
|
+
label="Headless channel, Private Access Token"
|
|
133
|
+
value={state.shopifyStorefrontToken}
|
|
134
|
+
onChange={(value) =>
|
|
135
|
+
updateField('shopifyStorefrontToken', value)
|
|
136
|
+
}
|
|
137
|
+
type="password"
|
|
138
|
+
placeholder={
|
|
139
|
+
shopifyStorefrontConfigured
|
|
140
|
+
? '••••••••••••••••'
|
|
141
|
+
: 'shpat_...'
|
|
142
|
+
}
|
|
143
|
+
error={errors.shopifyStorefrontToken}
|
|
144
|
+
/>
|
|
145
|
+
<p className="mt-1 text-xs text-gray-500">
|
|
146
|
+
Private access token for fetching products.
|
|
147
|
+
{shopifyStorefrontConfigured &&
|
|
148
|
+
' Leave blank to keep existing.'}
|
|
149
|
+
</p>
|
|
150
|
+
</div>
|
|
151
|
+
|
|
152
|
+
<div>
|
|
153
|
+
<StringInput
|
|
154
|
+
label="API Secret Key"
|
|
155
|
+
value={state.shopifyApiSecret}
|
|
156
|
+
onChange={(value) => updateField('shopifyApiSecret', value)}
|
|
157
|
+
type="password"
|
|
158
|
+
placeholder={
|
|
159
|
+
shopifySecretConfigured ? '••••••••••••••••' : 'shpss_...'
|
|
160
|
+
}
|
|
161
|
+
error={errors.shopifyApiSecret}
|
|
162
|
+
/>
|
|
163
|
+
<p className="mt-1 text-xs text-gray-500">
|
|
164
|
+
Required for Webhook signature verification.
|
|
165
|
+
{shopifySecretConfigured && ' Leave blank to keep existing.'}
|
|
166
|
+
</p>
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
|
+
|
|
171
|
+
{/* Resend Section */}
|
|
172
|
+
<div className="border-t border-gray-100 pt-6">
|
|
173
|
+
<div className="mb-4 flex items-center justify-between">
|
|
174
|
+
<h4 className="text-sm font-bold text-gray-900">
|
|
175
|
+
Resend (Transactional Email)
|
|
176
|
+
</h4>
|
|
177
|
+
{renderStatusBadge(resendConfigured)}
|
|
178
|
+
</div>
|
|
179
|
+
<StringInput
|
|
180
|
+
label="API Key"
|
|
181
|
+
value={state.resendApiKey}
|
|
182
|
+
onChange={(value) => updateField('resendApiKey', value)}
|
|
183
|
+
type="password"
|
|
184
|
+
placeholder={resendConfigured ? '••••••••••••••••' : 're_...'}
|
|
185
|
+
error={errors.resendApiKey}
|
|
186
|
+
/>
|
|
187
|
+
<p className="mt-2 text-xs text-gray-500">
|
|
188
|
+
Required for sending system emails.
|
|
189
|
+
{resendConfigured && ' Leave blank to keep existing key.'}
|
|
190
|
+
</p>
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
66
193
|
</div>
|
|
67
194
|
</div>
|
|
68
195
|
);
|
|
@@ -52,9 +52,14 @@ export default function StoryKeepDashboard({
|
|
|
52
52
|
current: shouldShowInactiveTabs ? false : activeTab === 'analytics',
|
|
53
53
|
},
|
|
54
54
|
{
|
|
55
|
-
id: '
|
|
56
|
-
name: '
|
|
57
|
-
current: activeTab === '
|
|
55
|
+
id: 'pages',
|
|
56
|
+
name: 'Web Pages',
|
|
57
|
+
current: activeTab === 'pages',
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
id: 'manage',
|
|
61
|
+
name: 'Manage Content',
|
|
62
|
+
current: activeTab === 'manage',
|
|
58
63
|
},
|
|
59
64
|
{
|
|
60
65
|
id: 'branding',
|
|
@@ -70,6 +75,15 @@ export default function StoryKeepDashboard({
|
|
|
70
75
|
},
|
|
71
76
|
]
|
|
72
77
|
: []),
|
|
78
|
+
...(currentBrandConfig?.HAS_SHOPIFY
|
|
79
|
+
? [
|
|
80
|
+
{
|
|
81
|
+
id: 'shopify',
|
|
82
|
+
name: 'Shopify',
|
|
83
|
+
current: activeTab === 'shopify',
|
|
84
|
+
},
|
|
85
|
+
]
|
|
86
|
+
: []),
|
|
73
87
|
];
|
|
74
88
|
|
|
75
89
|
useEffect(() => {
|
|
@@ -1,39 +1,17 @@
|
|
|
1
1
|
import { useState, useEffect } from 'react';
|
|
2
|
-
import { classNames } from '@/utils/helpers';
|
|
3
2
|
import { TractStackAPI } from '@/utils/api';
|
|
4
|
-
import {
|
|
5
|
-
handleContentSubtabChange,
|
|
6
|
-
restoreTabNavigation,
|
|
7
|
-
} from '@/stores/navigation';
|
|
8
3
|
import ContentBrowser from './controls/content/ContentBrowser';
|
|
9
|
-
import ManageContent from './controls/content/ManageContent';
|
|
10
4
|
import type { FullContentMapItem } from '@/types/tractstack';
|
|
11
5
|
|
|
12
6
|
interface StoryKeepDashboardContentProps {
|
|
13
7
|
fullContentMap: FullContentMapItem[];
|
|
14
8
|
homeSlug: string;
|
|
15
|
-
createMenu: boolean;
|
|
16
9
|
}
|
|
17
10
|
|
|
18
|
-
interface ContentTab {
|
|
19
|
-
id: string;
|
|
20
|
-
name: string;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const contentTabs: ContentTab[] = [
|
|
24
|
-
{ id: 'webpages', name: 'Web Pages' },
|
|
25
|
-
{ id: 'manage', name: 'Manage Content' },
|
|
26
|
-
];
|
|
27
|
-
|
|
28
11
|
const StoryKeepDashboard_Content = ({
|
|
29
12
|
fullContentMap,
|
|
30
13
|
homeSlug,
|
|
31
|
-
createMenu,
|
|
32
14
|
}: StoryKeepDashboardContentProps) => {
|
|
33
|
-
const [activeContentTab, setActiveContentTab] = useState('webpages');
|
|
34
|
-
const [navigationRestored, setNavigationRestored] = useState(false);
|
|
35
|
-
|
|
36
|
-
// Lightweight analytics data state - only for hotContent
|
|
37
15
|
const [analytics, setAnalytics] = useState<{
|
|
38
16
|
dashboard: {
|
|
39
17
|
hotContent?: Array<{ id: string; totalEvents: number }>;
|
|
@@ -48,29 +26,6 @@ const StoryKeepDashboard_Content = ({
|
|
|
48
26
|
error: null,
|
|
49
27
|
});
|
|
50
28
|
|
|
51
|
-
// Restore navigation state when component mounts or when returning to Content tab
|
|
52
|
-
useEffect(() => {
|
|
53
|
-
if (!navigationRestored) {
|
|
54
|
-
const contentNavigation = restoreTabNavigation();
|
|
55
|
-
if (contentNavigation) {
|
|
56
|
-
setActiveContentTab(contentNavigation.subtab);
|
|
57
|
-
}
|
|
58
|
-
setNavigationRestored(true);
|
|
59
|
-
}
|
|
60
|
-
}, [navigationRestored]);
|
|
61
|
-
|
|
62
|
-
// Enhanced content tab change with navigation tracking
|
|
63
|
-
const handleContentTabChange = (tabId: string) => {
|
|
64
|
-
handleContentSubtabChange(tabId as any, setActiveContentTab);
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
useEffect(() => {
|
|
68
|
-
if (createMenu) {
|
|
69
|
-
setActiveContentTab('manage');
|
|
70
|
-
}
|
|
71
|
-
}, [createMenu]);
|
|
72
|
-
|
|
73
|
-
// Lightweight content summary fetch with retry logic
|
|
74
29
|
useEffect(() => {
|
|
75
30
|
let retryCount = 0;
|
|
76
31
|
const maxRetries = 2;
|
|
@@ -79,7 +34,6 @@ const StoryKeepDashboard_Content = ({
|
|
|
79
34
|
try {
|
|
80
35
|
setAnalytics((prev) => ({ ...prev, isLoading: true, error: null }));
|
|
81
36
|
|
|
82
|
-
// Use TractStackAPI like FetchAnalytics does
|
|
83
37
|
const api = new TractStackAPI(
|
|
84
38
|
window.TRACTSTACK_CONFIG?.tenantId || 'default'
|
|
85
39
|
);
|
|
@@ -91,8 +45,6 @@ const StoryKeepDashboard_Content = ({
|
|
|
91
45
|
}
|
|
92
46
|
|
|
93
47
|
const data = response.data;
|
|
94
|
-
|
|
95
|
-
// Check if we got actual data
|
|
96
48
|
const hasData = data.hotContent && data.hotContent.length > 0;
|
|
97
49
|
|
|
98
50
|
setAnalytics({
|
|
@@ -140,56 +92,13 @@ const StoryKeepDashboard_Content = ({
|
|
|
140
92
|
fetchContentSummary();
|
|
141
93
|
}, []);
|
|
142
94
|
|
|
143
|
-
const renderContentTabContent = () => {
|
|
144
|
-
switch (activeContentTab) {
|
|
145
|
-
case 'webpages':
|
|
146
|
-
return (
|
|
147
|
-
<ContentBrowser
|
|
148
|
-
analytics={analytics}
|
|
149
|
-
fullContentMap={fullContentMap}
|
|
150
|
-
homeSlug={homeSlug}
|
|
151
|
-
/>
|
|
152
|
-
);
|
|
153
|
-
case 'manage':
|
|
154
|
-
return (
|
|
155
|
-
<ManageContent
|
|
156
|
-
fullContentMap={fullContentMap}
|
|
157
|
-
homeSlug={homeSlug}
|
|
158
|
-
createMenu={createMenu}
|
|
159
|
-
/>
|
|
160
|
-
);
|
|
161
|
-
default:
|
|
162
|
-
return null;
|
|
163
|
-
}
|
|
164
|
-
};
|
|
165
|
-
|
|
166
95
|
return (
|
|
167
96
|
<div className="w-full">
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
<button
|
|
174
|
-
key={tab.id}
|
|
175
|
-
onClick={() => handleContentTabChange(tab.id)}
|
|
176
|
-
className={classNames(
|
|
177
|
-
activeContentTab === tab.id
|
|
178
|
-
? 'border-cyan-500 text-cyan-600'
|
|
179
|
-
: 'border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700',
|
|
180
|
-
'whitespace-nowrap border-b-2 px-1 py-3 text-sm font-bold'
|
|
181
|
-
)}
|
|
182
|
-
aria-current={activeContentTab === tab.id ? 'page' : undefined}
|
|
183
|
-
>
|
|
184
|
-
{tab.name}
|
|
185
|
-
</button>
|
|
186
|
-
))}
|
|
187
|
-
</nav>
|
|
188
|
-
</div>
|
|
189
|
-
</div>
|
|
190
|
-
|
|
191
|
-
{/* Content Tab Content */}
|
|
192
|
-
{renderContentTabContent()}
|
|
97
|
+
<ContentBrowser
|
|
98
|
+
analytics={analytics}
|
|
99
|
+
fullContentMap={fullContentMap}
|
|
100
|
+
homeSlug={homeSlug}
|
|
101
|
+
/>
|
|
193
102
|
</div>
|
|
194
103
|
);
|
|
195
104
|
};
|