astro-tractstack 2.3.3 → 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 +5 -2
- package/dist/index.js +18 -0
- package/package.json +1 -1
- package/templates/custom/shopify/Cart.tsx +196 -104
- package/templates/custom/shopify/CartIcon.tsx +8 -8
- package/templates/custom/shopify/CheckoutModal.tsx +143 -66
- package/templates/custom/shopify/ShopifyCartManager.tsx +64 -19
- 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/src/components/Header.astro +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/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 +161 -66
- 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/templates/src/utils/customHelpers.ts +285 -2
- package/utils/inject-files.ts +18 -0
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
isTopLevelBlockNode,
|
|
9
9
|
parseCodeHook,
|
|
10
10
|
} from '@/utils/compositor/nodesHelper';
|
|
11
|
+
import { isStoryFragmentNode } from '@/utils/compositor/typeGuards';
|
|
11
12
|
import { PaneAddMode } from '@/types/compositorTypes';
|
|
12
13
|
import { NodeOverlay } from './tools/NodeOverlay';
|
|
13
14
|
import PanelVisibilityWrapper from '@/components/compositor/PanelVisibilityWrapper';
|
|
@@ -108,14 +109,6 @@ export const Node = memo((props: NodeProps) => {
|
|
|
108
109
|
case 'Pane':
|
|
109
110
|
{
|
|
110
111
|
const paneNode = node as PaneNode;
|
|
111
|
-
const storyfragmentNodeId = ctx.getClosestNodeTypeFromId(
|
|
112
|
-
node.id,
|
|
113
|
-
'StoryFragment'
|
|
114
|
-
);
|
|
115
|
-
const storyfragmentNode = ctx.allNodes
|
|
116
|
-
.get()
|
|
117
|
-
.get(storyfragmentNodeId) as StoryFragmentNode;
|
|
118
|
-
const first = paneNode.id === storyfragmentNode.paneIds?.[0];
|
|
119
112
|
const isHtmlAstPane = !!paneNode.htmlAst;
|
|
120
113
|
const paneNodes = ctx.getChildNodeIDs(node.id);
|
|
121
114
|
|
|
@@ -140,7 +133,7 @@ export const Node = memo((props: NodeProps) => {
|
|
|
140
133
|
);
|
|
141
134
|
}
|
|
142
135
|
|
|
143
|
-
if (!isPreview && !paneNodes.length) {
|
|
136
|
+
if (!isPreview && !paneNodes.length && !isHtmlAstPane) {
|
|
144
137
|
return (
|
|
145
138
|
<>
|
|
146
139
|
<ContextPanePanel nodeId={node.id} />
|
|
@@ -160,8 +153,45 @@ export const Node = memo((props: NodeProps) => {
|
|
|
160
153
|
</>
|
|
161
154
|
);
|
|
162
155
|
}
|
|
156
|
+
|
|
157
|
+
const contextContent = isHtmlAstPane ? (
|
|
158
|
+
<CreativePane nodeId={props.nodeId} htmlAst={paneNode.htmlAst!} />
|
|
159
|
+
) : (
|
|
160
|
+
<Pane {...props} />
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
element = (
|
|
164
|
+
<>
|
|
165
|
+
<div className="py-0.5">
|
|
166
|
+
<ConfigPanePanel
|
|
167
|
+
nodeId={props.nodeId}
|
|
168
|
+
isHtmlAstPane={isHtmlAstPane}
|
|
169
|
+
isSandboxMode={props.isSandboxMode || false}
|
|
170
|
+
/>
|
|
171
|
+
<PanelVisibilityWrapper
|
|
172
|
+
nodeId={props.nodeId}
|
|
173
|
+
panelType="settings"
|
|
174
|
+
ctx={ctx}
|
|
175
|
+
>
|
|
176
|
+
{contextContent}
|
|
177
|
+
</PanelVisibilityWrapper>
|
|
178
|
+
</div>
|
|
179
|
+
</>
|
|
180
|
+
);
|
|
181
|
+
break;
|
|
163
182
|
}
|
|
164
183
|
|
|
184
|
+
const storyfragmentNodeId = ctx.getClosestNodeTypeFromId(
|
|
185
|
+
node.id,
|
|
186
|
+
'StoryFragment'
|
|
187
|
+
);
|
|
188
|
+
const storyfragmentNode = storyfragmentNodeId
|
|
189
|
+
? (ctx.allNodes.get().get(storyfragmentNodeId) ?? null)
|
|
190
|
+
: null;
|
|
191
|
+
const first =
|
|
192
|
+
isStoryFragmentNode(storyfragmentNode) &&
|
|
193
|
+
paneNode.id === storyfragmentNode.paneIds?.[0];
|
|
194
|
+
|
|
165
195
|
const content = isHtmlAstPane ? (
|
|
166
196
|
<CreativePane nodeId={props.nodeId} htmlAst={paneNode.htmlAst!} />
|
|
167
197
|
) : (
|
|
@@ -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)}
|