astro-tractstack 2.3.3 → 2.3.5
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 +5 -2
- package/dist/index.js +32 -4
- package/package.json +1 -1
- package/templates/custom/customHelpers.ts +45 -0
- package/templates/custom/shopify/Cart.tsx +197 -105
- package/templates/custom/shopify/CartIcon.tsx +8 -8
- package/templates/custom/shopify/CheckoutModal.tsx +145 -68
- package/templates/custom/shopify/ShopifyCartManager.tsx +67 -22
- 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/shopifyCustomHelper.ts +10 -0
- package/templates/custom/shopify/shopifyHelpers.ts +298 -0
- package/templates/src/components/Header.astro +2 -2
- package/templates/src/components/codehooks/SearchWidget.tsx +1 -1
- 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 +35 -8
- package/templates/src/components/form/brand/SiteConfigSection.tsx +9 -0
- package/templates/src/components/search/SearchResults.tsx +1 -1
- package/templates/src/components/search/SearchWrapper.tsx +1 -1
- package/templates/src/components/storykeep/Dashboard_Advanced.tsx +59 -23
- package/templates/src/components/storykeep/Dashboard_Shopify.tsx +257 -18
- package/templates/src/components/storykeep/controls/content/ProductTable.tsx +38 -24
- package/templates/src/components/storykeep/controls/content/ResourceForm.tsx +162 -67
- package/templates/src/components/storykeep/shopify/ShopifyDashboard.tsx +175 -48
- package/templates/src/components/storykeep/shopify/ShopifyDashboard_Bookings.tsx +5 -2
- 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/layouts/Layout.astro +26 -0
- package/templates/src/pages/api/auth/logout.ts +35 -2
- 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/storykeep/advanced.astro +4 -1
- package/templates/src/stores/nodes.ts +8 -0
- package/templates/src/types/tractstack.ts +57 -0
- package/templates/src/utils/api/advancedConfig.ts +2 -1
- package/templates/src/utils/api/advancedHelpers.ts +4 -0
- package/templates/src/utils/api/brandConfig.ts +2 -0
- package/templates/src/utils/api/brandHelpers.ts +6 -0
- package/templates/src/utils/api/salesHelpers.ts +21 -0
- package/utils/inject-files.ts +32 -4
- package/templates/src/utils/customHelpers.ts +0 -89
- /package/templates/{src/utils/booking → custom/shopify}/appointmentMode.ts +0 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
useEffect,
|
|
3
|
+
useLayoutEffect,
|
|
3
4
|
useState,
|
|
4
5
|
useRef,
|
|
5
6
|
type FocusEvent,
|
|
@@ -23,16 +24,147 @@ const viewportMap = {
|
|
|
23
24
|
desktop: 'xl',
|
|
24
25
|
} as const;
|
|
25
26
|
|
|
27
|
+
function removeEditProxies(container: HTMLElement) {
|
|
28
|
+
container.querySelectorAll('[data-proxy-positioned]').forEach((el) => {
|
|
29
|
+
const htmlEl = el as HTMLElement;
|
|
30
|
+
const prev = htmlEl.getAttribute('data-proxy-prev-position');
|
|
31
|
+
htmlEl.style.position = prev ?? '';
|
|
32
|
+
htmlEl.removeAttribute('data-proxy-positioned');
|
|
33
|
+
htmlEl.removeAttribute('data-proxy-prev-position');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
container
|
|
37
|
+
.querySelectorAll('[data-proxy-for]')
|
|
38
|
+
.forEach((icon) => icon.remove());
|
|
39
|
+
|
|
40
|
+
container.querySelectorAll('[data-ast-id]').forEach((el) => {
|
|
41
|
+
const htmlEl = el as HTMLElement;
|
|
42
|
+
htmlEl.style.outline = '';
|
|
43
|
+
htmlEl.style.outlineOffset = '';
|
|
44
|
+
htmlEl.style.cursor = '';
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function ensurePositionedAnchor(anchorEl: HTMLElement) {
|
|
49
|
+
if (getComputedStyle(anchorEl).position !== 'static') return;
|
|
50
|
+
|
|
51
|
+
anchorEl.setAttribute('data-proxy-positioned', 'true');
|
|
52
|
+
anchorEl.setAttribute('data-proxy-prev-position', anchorEl.style.position);
|
|
53
|
+
anchorEl.style.position = 'relative';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function createProxyIcon(
|
|
57
|
+
htmlEl: HTMLElement,
|
|
58
|
+
astId: string,
|
|
59
|
+
htmlAst: CreativePanePayload,
|
|
60
|
+
paneNodeId: string
|
|
61
|
+
) {
|
|
62
|
+
const icon = document.createElement('div');
|
|
63
|
+
icon.setAttribute('data-proxy-for', astId);
|
|
64
|
+
icon.className = 'compositor-chrome';
|
|
65
|
+
icon.style.position = 'absolute';
|
|
66
|
+
icon.style.zIndex = '1003';
|
|
67
|
+
icon.style.width = '24px';
|
|
68
|
+
icon.style.height = '24px';
|
|
69
|
+
icon.style.backgroundColor = '#06b6d4';
|
|
70
|
+
icon.style.borderRadius = '9999px';
|
|
71
|
+
icon.style.display = 'flex';
|
|
72
|
+
icon.style.alignItems = 'center';
|
|
73
|
+
icon.style.justifyContent = 'center';
|
|
74
|
+
icon.style.color = 'white';
|
|
75
|
+
icon.style.fontSize = '12px';
|
|
76
|
+
icon.style.boxShadow = '0 10px 15px -3px rgb(0 0 0 / 0.1)';
|
|
77
|
+
icon.style.cursor = 'pointer';
|
|
78
|
+
icon.style.pointerEvents = 'auto';
|
|
79
|
+
icon.innerHTML = '✎';
|
|
80
|
+
|
|
81
|
+
icon.onmouseenter = () => {
|
|
82
|
+
htmlEl.style.outline = '3px solid #06b6d4';
|
|
83
|
+
};
|
|
84
|
+
icon.onmouseleave = () => {
|
|
85
|
+
htmlEl.style.outline = '2px dotted #06b6d4';
|
|
86
|
+
};
|
|
87
|
+
icon.onclick = (e) => {
|
|
88
|
+
e.stopPropagation();
|
|
89
|
+
const meta = htmlAst.editableElements?.[astId];
|
|
90
|
+
if (!meta) return;
|
|
91
|
+
|
|
92
|
+
let action = '';
|
|
93
|
+
if (meta.isCssBackground) {
|
|
94
|
+
action = 'style-creative-bg';
|
|
95
|
+
} else if (meta.tagName === 'img') {
|
|
96
|
+
action = 'style-creative-img';
|
|
97
|
+
} else if (meta.tagName === 'a') {
|
|
98
|
+
action = 'style-creative-link';
|
|
99
|
+
} else if (meta.tagName === 'button') {
|
|
100
|
+
action = 'style-creative-btn';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (action) {
|
|
104
|
+
settingsPanelStore.set({
|
|
105
|
+
action,
|
|
106
|
+
nodeId: paneNodeId,
|
|
107
|
+
childId: astId,
|
|
108
|
+
expanded: true,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
return icon;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function syncEditProxies(
|
|
117
|
+
container: HTMLElement,
|
|
118
|
+
htmlAst: CreativePanePayload,
|
|
119
|
+
paneNodeId: string
|
|
120
|
+
) {
|
|
121
|
+
removeEditProxies(container);
|
|
122
|
+
|
|
123
|
+
container.querySelectorAll('[data-ast-id]').forEach((el) => {
|
|
124
|
+
const htmlEl = el as HTMLElement;
|
|
125
|
+
if (htmlEl.isContentEditable) return;
|
|
126
|
+
|
|
127
|
+
const astId = htmlEl.getAttribute('data-ast-id');
|
|
128
|
+
if (!astId) return;
|
|
129
|
+
|
|
130
|
+
htmlEl.style.outline = '2px dotted #06b6d4';
|
|
131
|
+
htmlEl.style.outlineOffset = '2px';
|
|
132
|
+
|
|
133
|
+
const icon = createProxyIcon(htmlEl, astId, htmlAst, paneNodeId);
|
|
134
|
+
|
|
135
|
+
if (htmlEl.tagName === 'IMG') {
|
|
136
|
+
const parent = htmlEl.parentElement;
|
|
137
|
+
if (parent instanceof HTMLElement) {
|
|
138
|
+
ensurePositionedAnchor(parent);
|
|
139
|
+
icon.style.top = `${htmlEl.offsetTop - 12}px`;
|
|
140
|
+
icon.style.left = `${htmlEl.offsetLeft - 12}px`;
|
|
141
|
+
parent.appendChild(icon);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
ensurePositionedAnchor(htmlEl);
|
|
147
|
+
icon.style.top = '-12px';
|
|
148
|
+
icon.style.left = '-12px';
|
|
149
|
+
htmlEl.appendChild(icon);
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
26
153
|
export const CreativePane = ({
|
|
27
154
|
nodeId,
|
|
28
155
|
htmlAst,
|
|
29
156
|
isProtected = false,
|
|
30
157
|
}: CreativePaneProps) => {
|
|
158
|
+
const ctx = getCtx();
|
|
31
159
|
const previews = useStore(renderedPreviews);
|
|
32
160
|
const { value: viewportKey } = useStore(viewportKeyStore);
|
|
161
|
+
const { value: toolModeVal } = useStore(ctx.toolModeValStore);
|
|
33
162
|
const [loading, setLoading] = useState(false);
|
|
34
163
|
const [error, setError] = useState<string | null>(null);
|
|
35
164
|
const contentRef = useRef<HTMLDivElement>(null);
|
|
165
|
+
const previewRef = useRef<HTMLDivElement>(null);
|
|
166
|
+
const htmlAstRef = useRef(htmlAst);
|
|
167
|
+
htmlAstRef.current = htmlAst;
|
|
36
168
|
|
|
37
169
|
const activeViewport = viewportMap[viewportKey];
|
|
38
170
|
const htmlContent = previews[nodeId];
|
|
@@ -101,100 +233,56 @@ export const CreativePane = ({
|
|
|
101
233
|
};
|
|
102
234
|
}, [htmlAst?.css, htmlAst?.tree, nodeId]);
|
|
103
235
|
|
|
104
|
-
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
236
|
+
useLayoutEffect(() => {
|
|
237
|
+
const el = previewRef.current;
|
|
238
|
+
if (!el || htmlContent == null) return;
|
|
239
|
+
if (el.innerHTML !== htmlContent) {
|
|
240
|
+
el.innerHTML = htmlContent;
|
|
241
|
+
}
|
|
242
|
+
}, [htmlContent]);
|
|
109
243
|
|
|
110
|
-
|
|
244
|
+
useLayoutEffect(() => {
|
|
245
|
+
const container = contentRef.current;
|
|
246
|
+
if (!container || !htmlContent) return;
|
|
111
247
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
248
|
+
if (toolModeVal === 'text') {
|
|
249
|
+
syncEditProxies(container, htmlAstRef.current, nodeId);
|
|
250
|
+
} else {
|
|
251
|
+
removeEditProxies(container);
|
|
252
|
+
}
|
|
253
|
+
});
|
|
118
254
|
|
|
119
|
-
|
|
120
|
-
|
|
255
|
+
useEffect(() => {
|
|
256
|
+
const container = contentRef.current;
|
|
257
|
+
if (!container || !htmlContent) return;
|
|
121
258
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
icon.style.height = '24px';
|
|
133
|
-
icon.style.backgroundColor = '#06b6d4';
|
|
134
|
-
icon.style.borderRadius = '9999px';
|
|
135
|
-
icon.style.display = 'flex';
|
|
136
|
-
icon.style.alignItems = 'center';
|
|
137
|
-
icon.style.justifyContent = 'center';
|
|
138
|
-
icon.style.color = 'white';
|
|
139
|
-
icon.style.fontSize = '12px';
|
|
140
|
-
icon.style.boxShadow = '0 10px 15px -3px rgb(0 0 0 / 0.1)';
|
|
141
|
-
icon.style.cursor = 'pointer';
|
|
142
|
-
icon.innerHTML = '✎';
|
|
143
|
-
|
|
144
|
-
const rect = htmlEl.getBoundingClientRect();
|
|
145
|
-
const containerRect = container.getBoundingClientRect();
|
|
146
|
-
icon.style.top = `${rect.top - containerRect.top - 12}px`;
|
|
147
|
-
icon.style.left = `${rect.left - containerRect.left - 12}px`;
|
|
148
|
-
|
|
149
|
-
icon.onmouseenter = () => {
|
|
150
|
-
htmlEl.style.outline = '3px solid #06b6d4';
|
|
151
|
-
};
|
|
152
|
-
icon.onmouseleave = () => {
|
|
153
|
-
htmlEl.style.outline = '2px dotted #06b6d4';
|
|
154
|
-
};
|
|
155
|
-
icon.onclick = () => {
|
|
156
|
-
const meta = htmlAst.editableElements?.[astId];
|
|
157
|
-
if (meta) {
|
|
158
|
-
let action = '';
|
|
159
|
-
if (meta.isCssBackground) {
|
|
160
|
-
action = 'style-creative-bg';
|
|
161
|
-
} else if (meta.tagName === 'img') {
|
|
162
|
-
action = 'style-creative-img';
|
|
163
|
-
} else if (meta.tagName === 'a') {
|
|
164
|
-
action = 'style-creative-link';
|
|
165
|
-
} else if (meta.tagName === 'button') {
|
|
166
|
-
action = 'style-creative-btn';
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
if (action) {
|
|
170
|
-
settingsPanelStore.set({
|
|
171
|
-
action,
|
|
172
|
-
nodeId,
|
|
173
|
-
childId: astId,
|
|
174
|
-
expanded: true,
|
|
175
|
-
});
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
};
|
|
179
|
-
|
|
180
|
-
container.appendChild(icon);
|
|
181
|
-
});
|
|
182
|
-
} else {
|
|
183
|
-
editables.forEach((el) => {
|
|
184
|
-
(el as HTMLElement).style.outline = '';
|
|
185
|
-
(el as HTMLElement).style.outlineOffset = '';
|
|
186
|
-
(el as HTMLElement).style.cursor = '';
|
|
187
|
-
});
|
|
188
|
-
const icons = container.querySelectorAll('[data-proxy-for]');
|
|
189
|
-
icons.forEach((icon) => icon.remove());
|
|
190
|
-
}
|
|
259
|
+
let debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
260
|
+
|
|
261
|
+
const resizeObserver = new ResizeObserver(() => {
|
|
262
|
+
if (ctx.toolModeValStore.get().value !== 'text') return;
|
|
263
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
264
|
+
debounceTimer = setTimeout(() => {
|
|
265
|
+
if (ctx.toolModeValStore.get().value === 'text') {
|
|
266
|
+
syncEditProxies(container, htmlAstRef.current, nodeId);
|
|
267
|
+
}
|
|
268
|
+
}, 100);
|
|
191
269
|
});
|
|
270
|
+
resizeObserver.observe(container);
|
|
192
271
|
|
|
193
|
-
return () =>
|
|
194
|
-
|
|
272
|
+
return () => {
|
|
273
|
+
resizeObserver.disconnect();
|
|
274
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
275
|
+
};
|
|
276
|
+
}, [htmlContent, nodeId, toolModeVal, ctx]);
|
|
277
|
+
|
|
278
|
+
useEffect(() => {
|
|
279
|
+
return () => {
|
|
280
|
+
const container = contentRef.current;
|
|
281
|
+
if (container) removeEditProxies(container);
|
|
282
|
+
};
|
|
283
|
+
}, []);
|
|
195
284
|
|
|
196
285
|
const handleKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {
|
|
197
|
-
const ctx = getCtx();
|
|
198
286
|
const mode = ctx.toolModeValStore.get().value;
|
|
199
287
|
if (mode !== 'text' || isProtected) return;
|
|
200
288
|
|
|
@@ -208,7 +296,6 @@ export const CreativePane = ({
|
|
|
208
296
|
};
|
|
209
297
|
|
|
210
298
|
const handleBlur = (e: FocusEvent<HTMLDivElement>) => {
|
|
211
|
-
const ctx = getCtx();
|
|
212
299
|
const mode = ctx.toolModeValStore.get().value;
|
|
213
300
|
if (mode !== 'text' || isProtected) return;
|
|
214
301
|
|
|
@@ -255,7 +342,7 @@ export const CreativePane = ({
|
|
|
255
342
|
{isProtected && (
|
|
256
343
|
<div className="absolute inset-0 z-50 cursor-crosshair bg-transparent" />
|
|
257
344
|
)}
|
|
258
|
-
<div
|
|
345
|
+
<div ref={previewRef} />
|
|
259
346
|
</div>
|
|
260
347
|
</>
|
|
261
348
|
);
|
|
@@ -94,7 +94,7 @@ const AddPanePanel = ({
|
|
|
94
94
|
/>
|
|
95
95
|
) : mode === PaneAddMode.REUSE && !isContextPane ? (
|
|
96
96
|
<AddPaneReUsePanel nodeId={nodeId} first={first} setMode={setMode} />
|
|
97
|
-
) : mode === PaneAddMode.CODEHOOK ? (
|
|
97
|
+
) : mode === PaneAddMode.CODEHOOK && !isContextPane ? (
|
|
98
98
|
<AddPaneCodeHookPanel
|
|
99
99
|
nodeId={nodeId}
|
|
100
100
|
first={first}
|
|
@@ -139,7 +139,7 @@ const AddPanePanel = ({
|
|
|
139
139
|
)}
|
|
140
140
|
</>
|
|
141
141
|
)}
|
|
142
|
-
{!isTemplate && (
|
|
142
|
+
{!isTemplate && !isContextPane && (
|
|
143
143
|
<button
|
|
144
144
|
onClick={() => setMode(PaneAddMode.CODEHOOK)}
|
|
145
145
|
className="rounded bg-white px-2 py-1 text-sm text-cyan-700 shadow-sm transition-colors hover:bg-cyan-700 hover:text-white"
|
|
@@ -294,13 +294,14 @@ const AddPaneNewPanel = ({
|
|
|
294
294
|
|
|
295
295
|
<div className="mx-auto mb-6 max-w-xl text-center">
|
|
296
296
|
<h3 className="text-lg font-bold text-gray-800">
|
|
297
|
-
|
|
297
|
+
{isContextPane
|
|
298
|
+
? 'One focused page for in-context links.'
|
|
299
|
+
: "Don't publish a page. Stack your story."}
|
|
298
300
|
</h3>
|
|
299
301
|
<p className="text-sm text-gray-500">
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
leans in, and when they drift away.
|
|
302
|
+
{isContextPane
|
|
303
|
+
? 'Readers open this URL from links on your Smart Tract—definitions, offers, FAQs, or anything that needs its own address without breaking the flow. Design this single pane for that job; they close the page and return to where they came from.'
|
|
304
|
+
: 'A static page is a wall of text. A Smart Tract is a series of beats. Build one Pane at a time and give each one a single job. Every Pane acts as a listening device—telling you exactly when your audience leans in, and when they drift away.'}
|
|
304
305
|
</p>
|
|
305
306
|
</div>
|
|
306
307
|
|
|
@@ -174,24 +174,13 @@ const AddPaneReUsePanel = ({
|
|
|
174
174
|
|
|
175
175
|
templateData.paneNode.parentId = storyfragmentId;
|
|
176
176
|
|
|
177
|
-
let specificIdx = -1;
|
|
178
|
-
let elIdx = -1;
|
|
179
177
|
const location = first ? 'before' : 'after';
|
|
180
|
-
|
|
181
|
-
specificIdx =
|
|
182
|
-
elIdx
|
|
183
|
-
if (elIdx === -1) {
|
|
184
|
-
storyFragmentNode.paneIds.push(templateData.paneNode.id);
|
|
185
|
-
} else {
|
|
178
|
+
const elIdx = storyFragmentNode.paneIds.indexOf(nodeId);
|
|
179
|
+
let specificIdx = elIdx;
|
|
180
|
+
if (elIdx !== -1) {
|
|
186
181
|
if (location === 'before') {
|
|
187
|
-
storyFragmentNode.paneIds.splice(elIdx, 0, templateData.paneNode.id);
|
|
188
182
|
specificIdx = Math.max(0, specificIdx - 1);
|
|
189
183
|
} else {
|
|
190
|
-
storyFragmentNode.paneIds.splice(
|
|
191
|
-
elIdx + 1,
|
|
192
|
-
0,
|
|
193
|
-
templateData.paneNode.id
|
|
194
|
-
);
|
|
195
184
|
specificIdx = Math.min(
|
|
196
185
|
specificIdx + 1,
|
|
197
186
|
storyFragmentNode.paneIds.length
|
|
@@ -206,7 +195,12 @@ const AddPaneReUsePanel = ({
|
|
|
206
195
|
specificIdx
|
|
207
196
|
);
|
|
208
197
|
ctx.addNodes(templateData.childNodes);
|
|
209
|
-
ctx.
|
|
198
|
+
ctx.insertPaneId(
|
|
199
|
+
storyfragmentId,
|
|
200
|
+
templateData.paneNode.id,
|
|
201
|
+
nodeId,
|
|
202
|
+
location
|
|
203
|
+
);
|
|
210
204
|
|
|
211
205
|
setMode(PaneAddMode.DEFAULT);
|
|
212
206
|
} catch (error) {
|
|
@@ -19,7 +19,10 @@ const GOOGLE_TERMS_URL = 'https://renees.freewebpress.com/terms';
|
|
|
19
19
|
const GOOGLE_AUTHORIZED_DOMAIN = 'freewebpress.com';
|
|
20
20
|
const GOOGLE_REDIRECT_URI =
|
|
21
21
|
'https://renees.freewebpress.com/api/google/oauth/callback';
|
|
22
|
-
const
|
|
22
|
+
const GOOGLE_SCOPES = [
|
|
23
|
+
'https://www.googleapis.com/auth/calendar.events',
|
|
24
|
+
'https://www.googleapis.com/auth/calendar.readonly',
|
|
25
|
+
] as const;
|
|
23
26
|
const GOOGLE_VERIFICATION_VIDEO_URL =
|
|
24
27
|
'https://www.youtube.com/watch?v=kJI4XdqiiAI';
|
|
25
28
|
|
|
@@ -43,7 +46,8 @@ export default function APIConfigSection({
|
|
|
43
46
|
const shopifyVersionConfigured = Boolean(status?.shopifyApiVersion);
|
|
44
47
|
const shopifyAdminSlugConfigured = status?.shopifyAdminSlugSet;
|
|
45
48
|
const shopifyWebhooksConfigured = status?.userSetupWebhooks;
|
|
46
|
-
const resendConfigured = status?.
|
|
49
|
+
const resendConfigured = status?.hasResend;
|
|
50
|
+
const resendKeyConfigured = status?.resendApiKeySet;
|
|
47
51
|
const googleHasSync = status?.hasGoogleSync;
|
|
48
52
|
const googleClientIDConfigured = status?.googleOauthClientIdSet;
|
|
49
53
|
const googleClientSecretConfigured = status?.googleOauthClientSecretSet;
|
|
@@ -298,13 +302,30 @@ export default function APIConfigSection({
|
|
|
298
302
|
value={state.resendApiKey}
|
|
299
303
|
onChange={(value) => updateField('resendApiKey', value)}
|
|
300
304
|
type="password"
|
|
301
|
-
placeholder={
|
|
305
|
+
placeholder={resendKeyConfigured ? '••••••••••••••••' : 're_...'}
|
|
302
306
|
error={errors.resendApiKey}
|
|
303
307
|
/>
|
|
304
308
|
<p className="mt-2 text-xs text-gray-500">
|
|
305
309
|
Required for sending system emails.
|
|
306
|
-
{
|
|
310
|
+
{resendKeyConfigured && ' Leave blank to keep existing key.'}
|
|
307
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
|
+
/>
|
|
308
329
|
</div>
|
|
309
330
|
|
|
310
331
|
{/* Google Calendar / Meet Section */}
|
|
@@ -371,10 +392,16 @@ export default function APIConfigSection({
|
|
|
371
392
|
</ul>
|
|
372
393
|
</li>
|
|
373
394
|
<li>
|
|
374
|
-
Open <strong>Data Access</strong> and add
|
|
375
|
-
<
|
|
376
|
-
{
|
|
377
|
-
|
|
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>
|
|
378
405
|
</li>
|
|
379
406
|
<li>
|
|
380
407
|
Open <strong>Branding</strong> and set:
|
|
@@ -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)}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useState, useEffect } from 'react';
|
|
2
2
|
import MagnifyingGlassIcon from '@heroicons/react/24/outline/MagnifyingGlassIcon';
|
|
3
|
-
import { initSearch } from '@/
|
|
3
|
+
import { initSearch } from '@/custom/customHelpers';
|
|
4
4
|
import SearchModal from './SearchModal';
|
|
5
5
|
import type { FullContentMapItem } from '@/types/tractstack';
|
|
6
6
|
|
|
@@ -1,28 +1,37 @@
|
|
|
1
|
-
import { useState, useEffect, useRef } 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);
|
|
@@ -30,14 +39,33 @@ export default function StoryKeepDashboard_Advanced({
|
|
|
30
39
|
const [error, setError] = useState<string>('');
|
|
31
40
|
const hasHydratedInitialFormState = useRef(false);
|
|
32
41
|
|
|
33
|
-
|
|
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
|
+
);
|
|
63
|
+
|
|
34
64
|
useEffect(() => {
|
|
35
65
|
async function loadStatus() {
|
|
36
66
|
try {
|
|
37
67
|
setIsLoading(true);
|
|
38
|
-
const statusData = await getAdvancedConfigStatus(
|
|
39
|
-
window.TRACTSTACK_CONFIG?.tenantId || 'default'
|
|
40
|
-
);
|
|
68
|
+
const statusData = await getAdvancedConfigStatus(tenantId);
|
|
41
69
|
setStatus(statusData);
|
|
42
70
|
} catch (err) {
|
|
43
71
|
setError(
|
|
@@ -50,27 +78,31 @@ export default function StoryKeepDashboard_Advanced({
|
|
|
50
78
|
}
|
|
51
79
|
}
|
|
52
80
|
loadStatus();
|
|
53
|
-
}, []);
|
|
81
|
+
}, [tenantId]);
|
|
54
82
|
|
|
55
83
|
const formState = useFormState<AdvancedConfigState>({
|
|
56
|
-
initialData:
|
|
57
|
-
validator:
|
|
84
|
+
initialData: convertAdvancedToLocalState(status),
|
|
85
|
+
validator: validateAdvancedForm,
|
|
58
86
|
interceptor: advancedStateIntercept,
|
|
59
87
|
onSave: async (state: AdvancedConfigState) => {
|
|
60
88
|
const backendPayload = convertToBackendFormat(state);
|
|
61
|
-
await saveAdvancedConfig(
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
);
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
);
|
|
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);
|
|
70
98
|
setStatus(newStatus);
|
|
71
99
|
|
|
72
|
-
|
|
73
|
-
const newState =
|
|
100
|
+
const brandLocalHydrate = convertBrandToLocalState(brandConfigFresh);
|
|
101
|
+
const newState = {
|
|
102
|
+
...convertAdvancedToLocalState(newStatus),
|
|
103
|
+
adminEmail: brandLocalHydrate.adminEmail,
|
|
104
|
+
adminEmailName: brandLocalHydrate.adminEmailName,
|
|
105
|
+
};
|
|
74
106
|
formState.resetToState(newState);
|
|
75
107
|
|
|
76
108
|
window.location.reload();
|
|
@@ -82,9 +114,14 @@ export default function StoryKeepDashboard_Advanced({
|
|
|
82
114
|
if (!status || hasHydratedInitialFormState.current) {
|
|
83
115
|
return;
|
|
84
116
|
}
|
|
85
|
-
|
|
117
|
+
const brandLocal = convertBrandToLocalState(brandConfig);
|
|
118
|
+
formState.resetToState({
|
|
119
|
+
...convertAdvancedToLocalState(status),
|
|
120
|
+
adminEmail: brandLocal.adminEmail,
|
|
121
|
+
adminEmailName: brandLocal.adminEmailName,
|
|
122
|
+
});
|
|
86
123
|
hasHydratedInitialFormState.current = true;
|
|
87
|
-
}, [status, formState]);
|
|
124
|
+
}, [status, formState, brandConfig]);
|
|
88
125
|
|
|
89
126
|
if (isLoading) {
|
|
90
127
|
return (
|
|
@@ -102,7 +139,6 @@ export default function StoryKeepDashboard_Advanced({
|
|
|
102
139
|
);
|
|
103
140
|
}
|
|
104
141
|
|
|
105
|
-
// Database status component
|
|
106
142
|
const DatabaseStatusSection = () => {
|
|
107
143
|
const databaseType = status?.tursoEnabled
|
|
108
144
|
? 'Turso Cloud Database'
|