astro-tractstack 2.0.0-rc.8 → 2.0.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/LICENSE +8 -97
- package/README.md +7 -5
- package/bin/create-tractstack.js +35 -11
- package/dist/index.js +106 -29
- package/package.json +10 -5
- package/templates/css/frontend.css +1 -1
- package/templates/custom/minimal/CodeHook.astro +13 -12
- package/templates/custom/minimal/CustomRoutes.astro +25 -31
- package/templates/custom/with-examples/CodeHook.astro +22 -11
- package/templates/custom/with-examples/CustomRoutes.astro +4 -8
- package/templates/custom/with-examples/ProductCard.astro +29 -0
- package/templates/custom/with-examples/ProductCardWrapper.astro +43 -0
- package/templates/custom/with-examples/ProductGrid.astro +64 -0
- package/templates/custom/with-examples/pages/Collections.astro +58 -98
- package/templates/gitignore +42 -0
- package/templates/prettierignore +5 -0
- package/templates/prettierrc +19 -0
- package/templates/src/client/app.js +127 -0
- package/templates/src/client/htmx.min.js +3519 -0
- package/templates/src/client/view.js +429 -0
- package/templates/src/components/Footer.astro +4 -9
- package/templates/src/components/Header.astro +67 -60
- package/templates/src/components/Menu.tsx +188 -52
- package/templates/src/components/codehooks/BunnyVideoSetup.tsx +2 -2
- package/templates/src/components/codehooks/EpinetDurationSelector.tsx +9 -13
- package/templates/src/components/codehooks/EpinetTableView.tsx +11 -7
- package/templates/src/components/codehooks/EpinetWrapper.tsx +1 -0
- package/templates/src/components/codehooks/FeaturedArticle.astro +105 -0
- package/templates/src/components/codehooks/FeaturedArticleSetup.tsx +318 -0
- package/templates/src/components/codehooks/ListContent.astro +32 -162
- package/templates/src/components/codehooks/ListContentSetup.tsx +43 -138
- package/templates/src/components/codehooks/ProductCardSetup.tsx +152 -0
- package/templates/src/components/codehooks/ProductGridSetup.tsx +274 -0
- package/templates/src/components/codehooks/SearchWidget.tsx +453 -0
- package/templates/src/components/compositor/Node.tsx +3 -6
- package/templates/src/components/compositor/PanelVisibilityWrapper.tsx +21 -11
- package/templates/src/components/compositor/elements/BunnyVideo.tsx +21 -20
- package/templates/src/components/compositor/nodes/Pane.tsx +51 -21
- package/templates/src/components/compositor/nodes/RenderChildren.tsx +6 -1
- package/templates/src/components/compositor/nodes/Widget.tsx +16 -2
- package/templates/src/components/compositor/preview/FeaturedArticlePreview.tsx +155 -0
- package/templates/src/components/compositor/preview/PaneSnapshotGenerator.tsx +20 -1
- package/templates/src/components/edit/Header.tsx +10 -4
- package/templates/src/components/edit/PanelSwitch.tsx +11 -7
- package/templates/src/components/edit/SettingsPanel.tsx +29 -18
- package/templates/src/components/edit/ToolBar.tsx +1 -28
- package/templates/src/components/edit/ToolMode.tsx +45 -32
- package/templates/src/components/edit/pane/AddPanePanel_break.tsx +12 -2
- package/templates/src/components/edit/pane/AddPanePanel_codehook.tsx +8 -2
- package/templates/src/components/edit/pane/AddPanePanel_newAICopy_modal.tsx +1 -1
- package/templates/src/components/edit/pane/ConfigPanePanel.tsx +17 -27
- package/templates/src/components/edit/pane/PageGenSelector.tsx +16 -16
- package/templates/src/components/edit/pane/PageGenSpecial.tsx +26 -49
- package/templates/src/components/edit/pane/PageGen_preview.tsx +17 -2
- package/templates/src/components/edit/pane/PanePanel_path.tsx +2 -4
- package/templates/src/components/edit/pane/PanePanel_title.tsx +243 -76
- package/templates/src/components/edit/panels/StyleBreakPanel.tsx +17 -19
- package/templates/src/components/edit/panels/StyleCodeHookPanel.tsx +48 -37
- package/templates/src/components/edit/panels/StyleElementPanel_add.tsx +60 -55
- package/templates/src/components/edit/panels/StyleImagePanel_add.tsx +56 -50
- package/templates/src/components/edit/panels/StyleLiElementPanel_add.tsx +54 -47
- package/templates/src/components/edit/panels/StyleLinkPanel_add.tsx +54 -44
- package/templates/src/components/edit/panels/StyleLinkPanel_config.tsx +113 -138
- package/templates/src/components/edit/panels/StyleParentPanel_add.tsx +54 -40
- package/templates/src/components/edit/panels/StyleWidgetPanel.tsx +3 -3
- package/templates/src/components/edit/panels/StyleWidgetPanel_add.tsx +56 -49
- package/templates/src/components/edit/panels/StyleWidgetPanel_config.tsx +14 -5
- package/templates/src/components/edit/state/SaveModal.tsx +316 -169
- package/templates/src/components/edit/storyfragment/StoryFragmentPanel_og.tsx +1 -1
- package/templates/src/components/edit/storyfragment/StoryFragmentPanel_slug.tsx +56 -55
- package/templates/src/components/edit/widgets/BunnyWidget.tsx +538 -59
- package/templates/src/components/edit/widgets/InteractiveDisclosureWidget.tsx +656 -0
- package/templates/src/components/edit/widgets/ToggleWidget.tsx +9 -16
- package/templates/src/components/fields/ArtpackImage.tsx +4 -1
- package/templates/src/components/fields/BackgroundImage.tsx +1 -1
- package/templates/src/components/fields/BackgroundImageWrapper.tsx +127 -35
- package/templates/src/components/fields/ColorPickerCombo.tsx +66 -62
- package/templates/src/components/fields/ImageUpload.tsx +1 -1
- package/templates/src/components/fields/ViewportComboBox.tsx +59 -42
- package/templates/src/components/form/ActionBuilderBeliefSelector.tsx +117 -0
- package/templates/src/components/form/ActionBuilderField.tsx +306 -87
- package/templates/src/components/search/SearchModal.tsx +420 -0
- package/templates/src/components/search/SearchResults.tsx +367 -0
- package/templates/src/components/search/SearchWrapper.tsx +46 -0
- package/templates/src/components/storykeep/Dashboard_Advanced.tsx +1 -1
- package/templates/src/components/storykeep/Dashboard_Analytics.tsx +34 -8
- package/templates/src/components/storykeep/Dashboard_Content.tsx +6 -0
- package/templates/src/components/storykeep/StoryKeepBackdrop.astro +87 -0
- package/templates/src/components/storykeep/controls/content/BeliefForm.tsx +37 -33
- package/templates/src/components/storykeep/controls/content/MenuForm.tsx +55 -7
- package/templates/src/components/storykeep/controls/content/ResourceForm.tsx +17 -2
- package/templates/src/components/storykeep/controls/content/StoryFragmentTable.tsx +5 -8
- package/templates/src/components/storykeep/state/FetchAnalytics.tsx +274 -228
- package/templates/src/components/storykeep/widgets/Wizard.tsx +14 -7
- package/templates/src/components/tenant/RegistrationForm.tsx +1 -1
- package/templates/src/components/widgets/ImpressionWrapper.tsx +0 -1
- package/templates/src/constants/shapes.ts +9 -0
- package/templates/src/constants.ts +2121 -16
- package/templates/src/hooks/useSearch.ts +228 -0
- package/templates/src/layouts/Layout.astro +213 -104
- package/templates/src/lib/storyData.ts +4 -1
- package/templates/src/pages/[...slug]/edit.astro +14 -14
- package/templates/src/pages/[...slug].astro +82 -21
- package/templates/src/pages/api/orphan-analysis.ts +0 -1
- package/templates/src/pages/api/tailwind.ts +23 -21
- package/templates/src/pages/context/[...contextSlug]/edit.astro +14 -14
- package/templates/src/pages/context/[...contextSlug].astro +7 -2
- package/templates/src/pages/storykeep/advanced.astro +5 -4
- package/templates/src/pages/storykeep/branding.astro +5 -4
- package/templates/src/pages/storykeep/content.astro +5 -4
- package/templates/src/pages/storykeep/init.astro +40 -1
- package/templates/src/pages/storykeep/login.astro +1 -1
- package/templates/src/pages/storykeep.astro +5 -4
- package/templates/src/stores/nodes.ts +59 -88
- package/templates/src/stores/orphanAnalysis.ts +19 -21
- package/templates/src/stores/storykeep.ts +7 -0
- package/templates/src/types/compositorTypes.ts +6 -0
- package/templates/src/types/tractstack.ts +17 -0
- package/templates/src/utils/actions/lispLexer.ts +2 -2
- package/templates/src/utils/actions/preParse_Action.ts +3 -0
- package/templates/src/utils/api/beliefHelpers.ts +12 -36
- package/templates/src/utils/api/menuHelpers.ts +2 -2
- package/templates/src/utils/api.ts +26 -0
- package/templates/src/utils/compositor/TemplateNodes.ts +7 -0
- package/templates/src/utils/compositor/allowInsert.ts +5 -3
- package/templates/src/utils/compositor/nodesHelper.ts +4 -0
- package/templates/src/utils/compositor/processMarkdown.ts +16 -2
- package/templates/src/utils/compositor/reduceNodesClassNames.ts +4 -0
- package/templates/src/utils/compositor/templateMarkdownStyles.ts +13 -13
- package/templates/src/utils/compositor/typeGuards.ts +1 -0
- package/templates/src/utils/customHelpers.ts +38 -0
- package/templates/src/utils/helpers.ts +2 -2
- package/templates/src/utils/layout.ts +65 -144
- package/utils/inject-files.ts +95 -18
- package/templates/src/client/analytics-events.js +0 -207
- package/templates/src/client/belief-events.js +0 -191
- package/templates/src/client/sse.js +0 -613
- package/templates/src/components/codehooks/FeaturedContent.astro +0 -273
- package/templates/src/components/codehooks/FeaturedContentSetup.tsx +0 -738
- package/templates/src/components/compositor/preview/FeaturedContentPreview.tsx +0 -128
- package/templates/src/components/edit/pane/PanePanel_slug.tsx +0 -219
|
@@ -11,12 +11,15 @@ import {
|
|
|
11
11
|
fullContentMapStore,
|
|
12
12
|
getPendingImageOperation,
|
|
13
13
|
clearPendingImageOperation,
|
|
14
|
+
pendingHomePageSlugStore,
|
|
14
15
|
} from '@/stores/storykeep';
|
|
15
16
|
import { startLoadingAnimation } from '@/utils/helpers';
|
|
16
17
|
import type {
|
|
18
|
+
FlatNode,
|
|
17
19
|
BaseNode,
|
|
18
20
|
PaneNode,
|
|
19
21
|
StoryFragmentNode,
|
|
22
|
+
MarkdownPaneFragmentNode,
|
|
20
23
|
} from '@/types/compositorTypes';
|
|
21
24
|
|
|
22
25
|
type SaveStage =
|
|
@@ -27,12 +30,15 @@ type SaveStage =
|
|
|
27
30
|
| 'SAVING_STORY_FRAGMENTS'
|
|
28
31
|
| 'LINKING_FILES'
|
|
29
32
|
| 'PROCESSING_STYLES'
|
|
33
|
+
| 'UPDATING_HOME_PAGE'
|
|
30
34
|
| 'COMPLETED'
|
|
31
35
|
| 'ERROR';
|
|
32
36
|
|
|
33
37
|
interface SaveStageProgress {
|
|
34
38
|
currentStep: number;
|
|
35
39
|
totalSteps: number;
|
|
40
|
+
currentFileName?: string;
|
|
41
|
+
isUploading?: boolean;
|
|
36
42
|
}
|
|
37
43
|
|
|
38
44
|
interface SaveModalProps {
|
|
@@ -42,6 +48,13 @@ interface SaveModalProps {
|
|
|
42
48
|
onClose: () => void;
|
|
43
49
|
}
|
|
44
50
|
|
|
51
|
+
const PROGRESS_PHASES = {
|
|
52
|
+
PREPARATION: 5,
|
|
53
|
+
UPLOADS: 60,
|
|
54
|
+
PROCESSING: 25,
|
|
55
|
+
FINALIZATION: 10,
|
|
56
|
+
};
|
|
57
|
+
|
|
45
58
|
export default function SaveModal({
|
|
46
59
|
show,
|
|
47
60
|
slug,
|
|
@@ -59,13 +72,8 @@ export default function SaveModal({
|
|
|
59
72
|
const [debugMessages, setDebugMessages] = useState<string[]>([]);
|
|
60
73
|
const isSaving = useRef(false);
|
|
61
74
|
const [isNavigating, setIsNavigating] = useState(false);
|
|
62
|
-
|
|
63
|
-
// Determine if we're in create mode
|
|
64
75
|
const isCreateMode = slug === 'create';
|
|
65
|
-
|
|
66
|
-
const contentMap = fullContentMapStore.get();
|
|
67
|
-
|
|
68
|
-
// Get backend URL
|
|
76
|
+
const pendingHomePageSlug = pendingHomePageSlugStore.get();
|
|
69
77
|
const goBackend =
|
|
70
78
|
import.meta.env.PUBLIC_GO_BACKEND || 'http://localhost:8080';
|
|
71
79
|
const tenantId = import.meta.env.PUBLIC_TENANTID || 'default';
|
|
@@ -75,9 +83,7 @@ export default function SaveModal({
|
|
|
75
83
|
setDebugMessages((prev) => [...prev, `${timestamp}: ${message}`]);
|
|
76
84
|
};
|
|
77
85
|
|
|
78
|
-
// Main save process
|
|
79
86
|
useEffect(() => {
|
|
80
|
-
// Reset state when modal is hidden or if save is already running
|
|
81
87
|
if (!show) {
|
|
82
88
|
setStage('PREPARING');
|
|
83
89
|
setProgress(0);
|
|
@@ -96,12 +102,13 @@ export default function SaveModal({
|
|
|
96
102
|
|
|
97
103
|
try {
|
|
98
104
|
setStage('PREPARING');
|
|
99
|
-
setProgress(
|
|
105
|
+
setProgress(PROGRESS_PHASES.PREPARATION);
|
|
100
106
|
addDebugMessage(
|
|
101
|
-
`Starting save process... (${
|
|
107
|
+
`Starting save process... (${
|
|
108
|
+
isContext ? 'Context' : 'StoryFragment'
|
|
109
|
+
} mode, ${isCreateMode ? 'CREATE' : 'UPDATE'})`
|
|
102
110
|
);
|
|
103
111
|
|
|
104
|
-
// Filter nodes based on context mode
|
|
105
112
|
let dirtyPanes = allDirtyNodes.filter(
|
|
106
113
|
(node) => node.nodeType === 'Pane'
|
|
107
114
|
);
|
|
@@ -109,7 +116,6 @@ export default function SaveModal({
|
|
|
109
116
|
(node) => node.nodeType === 'StoryFragment'
|
|
110
117
|
);
|
|
111
118
|
|
|
112
|
-
// In context mode, we only care about panes, not story fragments
|
|
113
119
|
if (isContext) {
|
|
114
120
|
dirtyStoryFragments = [];
|
|
115
121
|
addDebugMessage('Context mode: Ignoring StoryFragment nodes');
|
|
@@ -119,8 +125,6 @@ export default function SaveModal({
|
|
|
119
125
|
(node): node is BaseNode & { base64Data?: string } =>
|
|
120
126
|
'base64Data' in node && !!node.base64Data
|
|
121
127
|
);
|
|
122
|
-
|
|
123
|
-
// Check for story fragments with pending OG image operations
|
|
124
128
|
const storyFragmentsWithPendingImages = dirtyStoryFragments.filter(
|
|
125
129
|
(fragment) => {
|
|
126
130
|
const pendingOp = getPendingImageOperation(fragment.id);
|
|
@@ -128,6 +132,20 @@ export default function SaveModal({
|
|
|
128
132
|
}
|
|
129
133
|
);
|
|
130
134
|
|
|
135
|
+
const totalFileBytes = nodesWithPendingFiles.reduce(
|
|
136
|
+
(sum, node) => sum + (node.base64Data?.length || 0),
|
|
137
|
+
0
|
|
138
|
+
);
|
|
139
|
+
const totalOgBytes = storyFragmentsWithPendingImages.reduce(
|
|
140
|
+
(sum, fragment) => {
|
|
141
|
+
const pendingOp = getPendingImageOperation(fragment.id);
|
|
142
|
+
return sum + (pendingOp?.data?.length || 0);
|
|
143
|
+
},
|
|
144
|
+
0
|
|
145
|
+
);
|
|
146
|
+
const totalUploadBytes = totalFileBytes + totalOgBytes;
|
|
147
|
+
let completedUploadBytes = 0;
|
|
148
|
+
|
|
131
149
|
const relevantNodeCount =
|
|
132
150
|
dirtyPanes.length + dirtyStoryFragments.length;
|
|
133
151
|
addDebugMessage(
|
|
@@ -143,7 +161,8 @@ export default function SaveModal({
|
|
|
143
161
|
if (
|
|
144
162
|
relevantNodeCount === 0 &&
|
|
145
163
|
nodesWithPendingFiles.length === 0 &&
|
|
146
|
-
storyFragmentsWithPendingImages.length === 0
|
|
164
|
+
storyFragmentsWithPendingImages.length === 0 &&
|
|
165
|
+
!pendingHomePageSlug
|
|
147
166
|
) {
|
|
148
167
|
addDebugMessage('No changes to save');
|
|
149
168
|
setStage('COMPLETED');
|
|
@@ -151,35 +170,25 @@ export default function SaveModal({
|
|
|
151
170
|
return;
|
|
152
171
|
}
|
|
153
172
|
|
|
154
|
-
const totalSteps =
|
|
155
|
-
nodesWithPendingFiles.length +
|
|
156
|
-
storyFragmentsWithPendingImages.length +
|
|
157
|
-
dirtyPanes.length +
|
|
158
|
-
dirtyStoryFragments.length +
|
|
159
|
-
2; // +1 for file linking, +1 for styles
|
|
160
|
-
|
|
161
|
-
addDebugMessage(
|
|
162
|
-
`Save plan: ${nodesWithPendingFiles.length} files, ${storyFragmentsWithPendingImages.length} og images, ${dirtyPanes.length} panes, ${dirtyStoryFragments.length} story fragments, 1 file linking, 1 styles = ${totalSteps} total steps`
|
|
163
|
-
);
|
|
164
|
-
|
|
165
|
-
let completedSteps = 1;
|
|
166
|
-
|
|
167
|
-
// PHASE 1: Upload all pending files and OG images first
|
|
168
173
|
const uploadedOGPaths: Record<string, string> = {};
|
|
169
174
|
|
|
170
|
-
// Handle pending files
|
|
171
175
|
if (nodesWithPendingFiles.length > 0) {
|
|
172
176
|
setStage('SAVING_PENDING_FILES');
|
|
173
|
-
setStageProgress({
|
|
174
|
-
currentStep: 0,
|
|
175
|
-
totalSteps: nodesWithPendingFiles.length,
|
|
176
|
-
});
|
|
177
|
-
|
|
178
177
|
for (let i = 0; i < nodesWithPendingFiles.length; i++) {
|
|
179
178
|
const fileNode = nodesWithPendingFiles[i];
|
|
179
|
+
const fileBytes = fileNode.base64Data?.length || 0;
|
|
180
180
|
const endpoint = `${goBackend}/api/v1/nodes/files/create`;
|
|
181
|
+
|
|
182
|
+
setStageProgress({
|
|
183
|
+
currentStep: i + 1,
|
|
184
|
+
totalSteps: nodesWithPendingFiles.length,
|
|
185
|
+
currentFileName: `${fileNode.id}.jpg`,
|
|
186
|
+
isUploading: true,
|
|
187
|
+
});
|
|
181
188
|
addDebugMessage(
|
|
182
|
-
`Processing file ${i + 1}/${nodesWithPendingFiles.length}: ${
|
|
189
|
+
`Processing file ${i + 1}/${nodesWithPendingFiles.length}: ${
|
|
190
|
+
fileNode.id
|
|
191
|
+
} -> POST ${endpoint}`
|
|
183
192
|
);
|
|
184
193
|
|
|
185
194
|
try {
|
|
@@ -190,7 +199,7 @@ export default function SaveModal({
|
|
|
190
199
|
'X-Tenant-ID': tenantId,
|
|
191
200
|
},
|
|
192
201
|
credentials: 'include',
|
|
193
|
-
body: JSON.stringify({ base64Data: fileNode.base64Data }),
|
|
202
|
+
body: JSON.stringify({ base64Data: fileNode.base64Data }),
|
|
194
203
|
});
|
|
195
204
|
|
|
196
205
|
if (!response.ok) {
|
|
@@ -198,16 +207,12 @@ export default function SaveModal({
|
|
|
198
207
|
}
|
|
199
208
|
|
|
200
209
|
const result = await response.json();
|
|
201
|
-
|
|
202
|
-
// Update tree with response data - handle different node types properly
|
|
203
210
|
const updatedNode = { ...fileNode, isChanged: true };
|
|
204
211
|
|
|
205
|
-
// Remove base64Data and add file properties
|
|
206
212
|
if ('base64Data' in updatedNode) {
|
|
207
213
|
delete updatedNode.base64Data;
|
|
208
214
|
}
|
|
209
215
|
|
|
210
|
-
// Add file properties - these properties already exist in FlatNode and BgImageNode types
|
|
211
216
|
if ('fileId' in updatedNode) {
|
|
212
217
|
updatedNode.fileId = result.fileId;
|
|
213
218
|
}
|
|
@@ -232,29 +237,39 @@ export default function SaveModal({
|
|
|
232
237
|
);
|
|
233
238
|
}
|
|
234
239
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
240
|
+
completedUploadBytes += fileBytes;
|
|
241
|
+
const uploadProgress =
|
|
242
|
+
totalUploadBytes > 0
|
|
243
|
+
? (completedUploadBytes / totalUploadBytes) *
|
|
244
|
+
PROGRESS_PHASES.UPLOADS
|
|
245
|
+
: 0;
|
|
246
|
+
setProgress(PROGRESS_PHASES.PREPARATION + uploadProgress);
|
|
247
|
+
setStageProgress((prev) => ({ ...prev, isUploading: false }));
|
|
238
248
|
}
|
|
239
249
|
}
|
|
240
250
|
|
|
241
|
-
// Handle OG image uploads
|
|
242
251
|
if (storyFragmentsWithPendingImages.length > 0) {
|
|
243
252
|
setStage('PROCESSING_OG_IMAGES');
|
|
244
|
-
setStageProgress({
|
|
245
|
-
currentStep: 0,
|
|
246
|
-
totalSteps: storyFragmentsWithPendingImages.length,
|
|
247
|
-
});
|
|
248
253
|
for (let i = 0; i < storyFragmentsWithPendingImages.length; i++) {
|
|
249
254
|
const fragment = storyFragmentsWithPendingImages[i];
|
|
250
255
|
const pendingOp = getPendingImageOperation(fragment.id);
|
|
256
|
+
const imageBytes = pendingOp?.data?.length || 0;
|
|
251
257
|
|
|
252
258
|
if (pendingOp && pendingOp.type === 'upload' && pendingOp.data) {
|
|
253
259
|
const ogUploadEndpoint = `${goBackend}/api/v1/nodes/images/og`;
|
|
254
260
|
addDebugMessage(
|
|
255
|
-
`Processing OG image ${i + 1}/${
|
|
261
|
+
`Processing OG image ${i + 1}/${
|
|
262
|
+
storyFragmentsWithPendingImages.length
|
|
263
|
+
}: ${fragment.id} -> POST ${ogUploadEndpoint}`
|
|
256
264
|
);
|
|
257
265
|
|
|
266
|
+
setStageProgress({
|
|
267
|
+
currentStep: i + 1,
|
|
268
|
+
totalSteps: storyFragmentsWithPendingImages.length,
|
|
269
|
+
currentFileName: pendingOp?.filename || `${fragment.id}-og.png`,
|
|
270
|
+
isUploading: true,
|
|
271
|
+
});
|
|
272
|
+
|
|
258
273
|
const uploadPayload = {
|
|
259
274
|
data: pendingOp.data,
|
|
260
275
|
filename:
|
|
@@ -292,78 +307,136 @@ export default function SaveModal({
|
|
|
292
307
|
`Failed to upload OG image for ${fragment.id}: ${errorMsg}`
|
|
293
308
|
);
|
|
294
309
|
}
|
|
310
|
+
completedUploadBytes += imageBytes;
|
|
311
|
+
const uploadProgress =
|
|
312
|
+
totalUploadBytes > 0
|
|
313
|
+
? (completedUploadBytes / totalUploadBytes) *
|
|
314
|
+
PROGRESS_PHASES.UPLOADS
|
|
315
|
+
: 0;
|
|
316
|
+
setProgress(PROGRESS_PHASES.PREPARATION + uploadProgress);
|
|
317
|
+
setStageProgress((prev) => ({ ...prev, isUploading: false }));
|
|
295
318
|
}
|
|
296
|
-
|
|
297
|
-
setStageProgress((prev) => ({ ...prev, currentStep: i + 1 }));
|
|
298
|
-
completedSteps++;
|
|
299
|
-
setProgress((completedSteps / totalSteps) * 80);
|
|
300
319
|
}
|
|
301
320
|
}
|
|
302
321
|
|
|
303
|
-
|
|
322
|
+
if (totalUploadBytes > 0) {
|
|
323
|
+
setProgress(PROGRESS_PHASES.PREPARATION + PROGRESS_PHASES.UPLOADS);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const totalProcessingSteps =
|
|
327
|
+
dirtyPanes.length + dirtyStoryFragments.length;
|
|
328
|
+
let completedProcessingSteps = 0;
|
|
329
|
+
|
|
304
330
|
if (dirtyPanes.length > 0) {
|
|
305
331
|
setStage('SAVING_PANES');
|
|
306
332
|
setStageProgress({
|
|
307
333
|
currentStep: 0,
|
|
308
334
|
totalSteps: dirtyPanes.length,
|
|
309
335
|
});
|
|
310
|
-
for (let i = 0; i < dirtyPanes.length; i++) {
|
|
311
|
-
const paneNode = dirtyPanes[i];
|
|
312
|
-
|
|
313
|
-
try {
|
|
314
|
-
const payload = transformLivePaneForSave(
|
|
315
|
-
ctx,
|
|
316
|
-
paneNode.id,
|
|
317
|
-
isContext
|
|
318
|
-
);
|
|
319
336
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
);
|
|
324
|
-
const isCreatePaneMode = !paneExistsInBackend;
|
|
325
|
-
const endpoint = isCreatePaneMode
|
|
326
|
-
? `${goBackend}/api/v1/nodes/panes/create`
|
|
327
|
-
: `${goBackend}/api/v1/nodes/panes/${payload.id}`;
|
|
328
|
-
const method = isCreatePaneMode ? 'POST' : 'PUT';
|
|
337
|
+
const bulkPayload = dirtyPanes.map((paneNode) =>
|
|
338
|
+
transformLivePaneForSave(ctx, paneNode.id, isContext)
|
|
339
|
+
);
|
|
329
340
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
);
|
|
341
|
+
bulkPayload.forEach((payload) => {
|
|
342
|
+
payload.optionsPayload.nodes.forEach((transformedNode) => {
|
|
343
|
+
const liveNode = ctx.allNodes.get().get(transformedNode.id);
|
|
344
|
+
if (!liveNode) return;
|
|
345
|
+
|
|
346
|
+
let needsUpdate = false;
|
|
347
|
+
let updatedNode: BaseNode = { ...liveNode };
|
|
348
|
+
|
|
349
|
+
if (
|
|
350
|
+
transformedNode.nodeType === 'TagElement' &&
|
|
351
|
+
transformedNode.elementCss
|
|
352
|
+
) {
|
|
353
|
+
const flatNode = liveNode as FlatNode;
|
|
354
|
+
if (flatNode.elementCss !== transformedNode.elementCss) {
|
|
355
|
+
(updatedNode as FlatNode).elementCss =
|
|
356
|
+
transformedNode.elementCss;
|
|
357
|
+
needsUpdate = true;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
333
360
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
361
|
+
if (
|
|
362
|
+
transformedNode.nodeType === 'Markdown' &&
|
|
363
|
+
transformedNode.parentCss
|
|
364
|
+
) {
|
|
365
|
+
const markdownNode = liveNode as MarkdownPaneFragmentNode;
|
|
366
|
+
const currentParentCss = markdownNode.parentCss;
|
|
367
|
+
const newParentCss = transformedNode.parentCss as string[];
|
|
368
|
+
|
|
369
|
+
const isDifferent =
|
|
370
|
+
!currentParentCss ||
|
|
371
|
+
currentParentCss.length !== newParentCss.length ||
|
|
372
|
+
currentParentCss.some(
|
|
373
|
+
(css, index) => css !== newParentCss[index]
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
if (isDifferent) {
|
|
377
|
+
(updatedNode as MarkdownPaneFragmentNode).parentCss =
|
|
378
|
+
newParentCss;
|
|
379
|
+
needsUpdate = true;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
343
382
|
|
|
344
|
-
if (
|
|
345
|
-
|
|
383
|
+
if (needsUpdate) {
|
|
384
|
+
ctx.allNodes.get().set(transformedNode.id, updatedNode);
|
|
346
385
|
}
|
|
386
|
+
});
|
|
387
|
+
});
|
|
347
388
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
389
|
+
const endpoint = `${goBackend}/api/v1/nodes/panes/bulk`;
|
|
390
|
+
addDebugMessage(
|
|
391
|
+
`Processing ${dirtyPanes.length} panes via -> POST ${endpoint}`
|
|
392
|
+
);
|
|
393
|
+
|
|
394
|
+
try {
|
|
395
|
+
const response = await fetch(endpoint, {
|
|
396
|
+
method: 'POST',
|
|
397
|
+
headers: {
|
|
398
|
+
'Content-Type': 'application/json',
|
|
399
|
+
'X-Tenant-ID': tenantId,
|
|
400
|
+
},
|
|
401
|
+
credentials: 'include',
|
|
402
|
+
body: JSON.stringify({ panes: bulkPayload }),
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
if (!response.ok) {
|
|
406
|
+
const errorText = await response.text();
|
|
355
407
|
throw new Error(
|
|
356
|
-
`
|
|
408
|
+
`HTTP error! status: ${response.status} - ${errorText}`
|
|
357
409
|
);
|
|
358
410
|
}
|
|
359
411
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
412
|
+
await response.json();
|
|
413
|
+
addDebugMessage(
|
|
414
|
+
`${dirtyPanes.length} panes saved successfully via bulk endpoint.`
|
|
415
|
+
);
|
|
416
|
+
} catch (bulkError) {
|
|
417
|
+
const errorMsg =
|
|
418
|
+
bulkError instanceof Error
|
|
419
|
+
? bulkError.message
|
|
420
|
+
: 'Unknown bulk save error';
|
|
421
|
+
addDebugMessage(`Bulk pane save failed: ${errorMsg}`);
|
|
422
|
+
throw new Error(`Failed to save panes in bulk: ${errorMsg}`);
|
|
363
423
|
}
|
|
424
|
+
|
|
425
|
+
setStageProgress({
|
|
426
|
+
currentStep: dirtyPanes.length,
|
|
427
|
+
totalSteps: dirtyPanes.length,
|
|
428
|
+
});
|
|
429
|
+
completedProcessingSteps += dirtyPanes.length;
|
|
430
|
+
const processingProgress =
|
|
431
|
+
(completedProcessingSteps / totalProcessingSteps) *
|
|
432
|
+
PROGRESS_PHASES.PROCESSING;
|
|
433
|
+
setProgress(
|
|
434
|
+
PROGRESS_PHASES.PREPARATION +
|
|
435
|
+
PROGRESS_PHASES.UPLOADS +
|
|
436
|
+
processingProgress
|
|
437
|
+
);
|
|
364
438
|
}
|
|
365
439
|
|
|
366
|
-
// Handle story fragments
|
|
367
440
|
if (!isContext && dirtyStoryFragments.length > 0) {
|
|
368
441
|
setStage('SAVING_STORY_FRAGMENTS');
|
|
369
442
|
setStageProgress({
|
|
@@ -380,7 +453,6 @@ export default function SaveModal({
|
|
|
380
453
|
window.TRACTSTACK_CONFIG?.tenantId || 'default'
|
|
381
454
|
);
|
|
382
455
|
|
|
383
|
-
// If we uploaded an OG image for this fragment, use that path
|
|
384
456
|
if (uploadedOGPaths[fragment.id]) {
|
|
385
457
|
payload.socialImagePath = uploadedOGPaths[fragment.id];
|
|
386
458
|
}
|
|
@@ -391,7 +463,9 @@ export default function SaveModal({
|
|
|
391
463
|
const method = isCreateMode ? 'POST' : 'PUT';
|
|
392
464
|
|
|
393
465
|
addDebugMessage(
|
|
394
|
-
`Processing story fragment ${i + 1}/${
|
|
466
|
+
`Processing story fragment ${i + 1}/${
|
|
467
|
+
dirtyStoryFragments.length
|
|
468
|
+
}: ${fragment.id} -> ${method} ${endpoint}`
|
|
395
469
|
);
|
|
396
470
|
|
|
397
471
|
const response = await fetch(endpoint, {
|
|
@@ -408,13 +482,11 @@ export default function SaveModal({
|
|
|
408
482
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
409
483
|
}
|
|
410
484
|
|
|
411
|
-
//const result =
|
|
412
485
|
await response.json();
|
|
413
486
|
addDebugMessage(
|
|
414
487
|
`StoryFragment ${fragment.id} saved successfully`
|
|
415
488
|
);
|
|
416
489
|
|
|
417
|
-
// Clear pending image operation after successful save
|
|
418
490
|
if (uploadedOGPaths[fragment.id]) {
|
|
419
491
|
clearPendingImageOperation(fragment.id);
|
|
420
492
|
addDebugMessage(
|
|
@@ -433,17 +505,28 @@ export default function SaveModal({
|
|
|
433
505
|
}
|
|
434
506
|
|
|
435
507
|
setStageProgress((prev) => ({ ...prev, currentStep: i + 1 }));
|
|
436
|
-
|
|
437
|
-
|
|
508
|
+
completedProcessingSteps++;
|
|
509
|
+
const processingProgress =
|
|
510
|
+
(completedProcessingSteps / totalProcessingSteps) *
|
|
511
|
+
PROGRESS_PHASES.PROCESSING;
|
|
512
|
+
setProgress(
|
|
513
|
+
PROGRESS_PHASES.PREPARATION +
|
|
514
|
+
PROGRESS_PHASES.UPLOADS +
|
|
515
|
+
processingProgress
|
|
516
|
+
);
|
|
438
517
|
}
|
|
439
518
|
}
|
|
440
519
|
|
|
441
|
-
|
|
520
|
+
const baseFinalizationProgress =
|
|
521
|
+
PROGRESS_PHASES.PREPARATION +
|
|
522
|
+
PROGRESS_PHASES.UPLOADS +
|
|
523
|
+
PROGRESS_PHASES.PROCESSING;
|
|
524
|
+
|
|
442
525
|
if (dirtyPanes.length > 0) {
|
|
443
526
|
setStage('LINKING_FILES');
|
|
527
|
+
setProgress(baseFinalizationProgress);
|
|
444
528
|
addDebugMessage('Starting file-pane relationship linking...');
|
|
445
529
|
|
|
446
|
-
// Extract pane<>file relationships from saved panes
|
|
447
530
|
const relationships = [];
|
|
448
531
|
for (const paneNode of dirtyPanes) {
|
|
449
532
|
const fileIds = ctx.getPaneImageFileIds(paneNode.id);
|
|
@@ -491,84 +574,132 @@ export default function SaveModal({
|
|
|
491
574
|
} else {
|
|
492
575
|
addDebugMessage('No file relationships to link');
|
|
493
576
|
}
|
|
494
|
-
|
|
495
|
-
completedSteps++;
|
|
496
|
-
setProgress((completedSteps / totalSteps) * 90);
|
|
497
577
|
}
|
|
498
578
|
|
|
499
|
-
// PHASE 4: Styles processing (2-step process)
|
|
500
579
|
setStage('PROCESSING_STYLES');
|
|
501
|
-
setProgress(
|
|
580
|
+
setProgress(
|
|
581
|
+
baseFinalizationProgress + PROGRESS_PHASES.FINALIZATION / 2
|
|
582
|
+
);
|
|
502
583
|
addDebugMessage(`Processing styles...`);
|
|
503
584
|
|
|
504
585
|
try {
|
|
505
586
|
const { dirtyPaneIds, classes: dirtyClasses } =
|
|
506
587
|
ctx.getDirtyNodesClassData();
|
|
507
588
|
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
589
|
+
const astroEndpoint = `/api/tailwind`;
|
|
590
|
+
const astroPayload = { dirtyPaneIds, dirtyClasses };
|
|
591
|
+
const astroResponse = await fetch(astroEndpoint, {
|
|
592
|
+
method: 'POST',
|
|
593
|
+
headers: {
|
|
594
|
+
'Content-Type': 'application/json',
|
|
595
|
+
'X-Tenant-ID': tenantId,
|
|
596
|
+
},
|
|
597
|
+
credentials: 'include',
|
|
598
|
+
body: JSON.stringify(astroPayload),
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
if (!astroResponse.ok) {
|
|
602
|
+
throw new Error(
|
|
603
|
+
`CSS generation failed! status: ${astroResponse.status}`
|
|
511
604
|
);
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
const astroResult = await astroResponse.json();
|
|
608
|
+
|
|
609
|
+
if (!astroResult.success || !astroResult.generatedCss) {
|
|
610
|
+
throw new Error('CSS generation failed: no CSS returned');
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
addDebugMessage(
|
|
614
|
+
`CSS generated: ${astroResult.generatedCss.length} bytes for ${dirtyClasses.length} classes`
|
|
615
|
+
);
|
|
616
|
+
|
|
617
|
+
const goEndpoint = `${goBackend}/api/v1/tailwind/update`;
|
|
618
|
+
const goPayload = { frontendCss: astroResult.generatedCss };
|
|
619
|
+
const goResponse = await fetch(goEndpoint, {
|
|
620
|
+
method: 'POST',
|
|
621
|
+
headers: {
|
|
622
|
+
'Content-Type': 'application/json',
|
|
623
|
+
'X-Tenant-ID': tenantId,
|
|
624
|
+
},
|
|
625
|
+
credentials: 'include',
|
|
626
|
+
body: JSON.stringify(goPayload),
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
if (!goResponse.ok) {
|
|
630
|
+
throw new Error(`CSS save failed! status: ${goResponse.status}`);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
const goResult = await goResponse.json();
|
|
634
|
+
addDebugMessage(
|
|
635
|
+
`CSS saved successfully: stylesVer ${goResult.stylesVer}`
|
|
636
|
+
);
|
|
637
|
+
} catch (error) {
|
|
638
|
+
const errorMsg =
|
|
639
|
+
error instanceof Error ? error.message : 'Unknown error';
|
|
640
|
+
addDebugMessage(`Styles processing failed: ${errorMsg}`);
|
|
641
|
+
throw new Error(`Failed to process styles: ${errorMsg}`);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
if (pendingHomePageSlug) {
|
|
645
|
+
setStage('UPDATING_HOME_PAGE');
|
|
646
|
+
setProgress(
|
|
647
|
+
baseFinalizationProgress + (PROGRESS_PHASES.FINALIZATION - 2)
|
|
648
|
+
);
|
|
649
|
+
addDebugMessage(`Updating home page to: ${pendingHomePageSlug}`);
|
|
650
|
+
|
|
651
|
+
try {
|
|
652
|
+
const response = await fetch(`${goBackend}/api/v1/config/brand`, {
|
|
653
|
+
method: 'GET',
|
|
518
654
|
headers: {
|
|
519
655
|
'Content-Type': 'application/json',
|
|
520
656
|
'X-Tenant-ID': tenantId,
|
|
521
657
|
},
|
|
522
658
|
credentials: 'include',
|
|
523
|
-
body: JSON.stringify(astroPayload),
|
|
524
659
|
});
|
|
525
660
|
|
|
526
|
-
if (!
|
|
661
|
+
if (!response.ok) {
|
|
527
662
|
throw new Error(
|
|
528
|
-
`
|
|
663
|
+
`Failed to get current brand config: ${response.status}`
|
|
529
664
|
);
|
|
530
665
|
}
|
|
531
666
|
|
|
532
|
-
const
|
|
667
|
+
const currentBrandConfig = await response.json();
|
|
533
668
|
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
669
|
+
const updatedBrandConfig = {
|
|
670
|
+
...currentBrandConfig,
|
|
671
|
+
HOME_SLUG: pendingHomePageSlug,
|
|
672
|
+
};
|
|
537
673
|
|
|
538
|
-
|
|
539
|
-
|
|
674
|
+
const updateResponse = await fetch(
|
|
675
|
+
`${goBackend}/api/v1/config/brand`,
|
|
676
|
+
{
|
|
677
|
+
method: 'PUT',
|
|
678
|
+
headers: {
|
|
679
|
+
'Content-Type': 'application/json',
|
|
680
|
+
'X-Tenant-ID': tenantId,
|
|
681
|
+
},
|
|
682
|
+
credentials: 'include',
|
|
683
|
+
body: JSON.stringify(updatedBrandConfig),
|
|
684
|
+
}
|
|
540
685
|
);
|
|
541
686
|
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
method: 'POST',
|
|
547
|
-
headers: {
|
|
548
|
-
'Content-Type': 'application/json',
|
|
549
|
-
'X-Tenant-ID': tenantId,
|
|
550
|
-
},
|
|
551
|
-
credentials: 'include',
|
|
552
|
-
body: JSON.stringify(goPayload),
|
|
553
|
-
});
|
|
554
|
-
|
|
555
|
-
if (!goResponse.ok) {
|
|
556
|
-
throw new Error(`CSS save failed! status: ${goResponse.status}`);
|
|
687
|
+
if (!updateResponse.ok) {
|
|
688
|
+
throw new Error(
|
|
689
|
+
`Failed to update home page: ${updateResponse.status}`
|
|
690
|
+
);
|
|
557
691
|
}
|
|
558
692
|
|
|
559
|
-
|
|
560
|
-
addDebugMessage(
|
|
561
|
-
|
|
562
|
-
|
|
693
|
+
pendingHomePageSlugStore.set(null);
|
|
694
|
+
addDebugMessage('Home page updated successfully');
|
|
695
|
+
} catch (error) {
|
|
696
|
+
const errorMsg =
|
|
697
|
+
error instanceof Error ? error.message : 'Unknown error';
|
|
698
|
+
addDebugMessage(`Home page update failed: ${errorMsg}`);
|
|
699
|
+
throw new Error(`Failed to update home page: ${errorMsg}`);
|
|
563
700
|
}
|
|
564
|
-
} catch (error) {
|
|
565
|
-
const errorMsg =
|
|
566
|
-
error instanceof Error ? error.message : 'Unknown error';
|
|
567
|
-
addDebugMessage(`Styles processing failed: ${errorMsg}`);
|
|
568
|
-
throw new Error(`Failed to process styles: ${errorMsg}`);
|
|
569
701
|
}
|
|
570
702
|
|
|
571
|
-
// Success!
|
|
572
703
|
setStage('COMPLETED');
|
|
573
704
|
setProgress(100);
|
|
574
705
|
addDebugMessage('Save process completed successfully!');
|
|
@@ -589,10 +720,18 @@ export default function SaveModal({
|
|
|
589
720
|
}, [show, slug, isContext, isCreateMode, goBackend, tenantId]);
|
|
590
721
|
|
|
591
722
|
const getStageDescription = () => {
|
|
592
|
-
const
|
|
593
|
-
stageProgress
|
|
594
|
-
|
|
595
|
-
|
|
723
|
+
const { currentStep, totalSteps, currentFileName, isUploading } =
|
|
724
|
+
stageProgress;
|
|
725
|
+
|
|
726
|
+
const getProgressText = () => {
|
|
727
|
+
if (currentFileName && isUploading) {
|
|
728
|
+
return ` - Uploading ${currentFileName}...`;
|
|
729
|
+
}
|
|
730
|
+
if (currentFileName && !isUploading) {
|
|
731
|
+
return ` - Completed ${currentFileName}`;
|
|
732
|
+
}
|
|
733
|
+
return totalSteps > 0 ? ` (${currentStep}/${totalSteps})` : '';
|
|
734
|
+
};
|
|
596
735
|
|
|
597
736
|
const modeText = isContext ? 'Context Pane' : 'Story Fragment';
|
|
598
737
|
const actionText = isCreateMode ? 'Creating' : 'Updating';
|
|
@@ -601,9 +740,9 @@ export default function SaveModal({
|
|
|
601
740
|
case 'PREPARING':
|
|
602
741
|
return `Preparing ${actionText.toLowerCase()} ${modeText.toLowerCase()}...`;
|
|
603
742
|
case 'SAVING_PENDING_FILES':
|
|
604
|
-
return `Uploading files
|
|
743
|
+
return `Uploading files${getProgressText()}`;
|
|
605
744
|
case 'PROCESSING_OG_IMAGES':
|
|
606
|
-
return `Processing
|
|
745
|
+
return `Processing social images${getProgressText()}`;
|
|
607
746
|
case 'SAVING_PANES':
|
|
608
747
|
return `${actionText} pane content...${getProgressText()}`;
|
|
609
748
|
case 'SAVING_STORY_FRAGMENTS':
|
|
@@ -612,6 +751,8 @@ export default function SaveModal({
|
|
|
612
751
|
return 'Linking file relationships...';
|
|
613
752
|
case 'PROCESSING_STYLES':
|
|
614
753
|
return 'Processing styles...';
|
|
754
|
+
case 'UPDATING_HOME_PAGE':
|
|
755
|
+
return 'Updating home page...';
|
|
615
756
|
case 'COMPLETED':
|
|
616
757
|
return `${actionText} ${modeText.toLowerCase()} completed successfully!`;
|
|
617
758
|
case 'ERROR':
|
|
@@ -634,19 +775,15 @@ export default function SaveModal({
|
|
|
634
775
|
|
|
635
776
|
if (isCreateMode) {
|
|
636
777
|
let actualSlug: string;
|
|
778
|
+
const ctx = getCtx();
|
|
779
|
+
const allDirtyNodes = ctx.getDirtyNodes();
|
|
637
780
|
|
|
638
781
|
if (isContext) {
|
|
639
|
-
// For context mode, get slug from the saved pane
|
|
640
|
-
const ctx = getCtx();
|
|
641
|
-
const allDirtyNodes = ctx.getDirtyNodes();
|
|
642
782
|
const dirtyPanes = allDirtyNodes.filter(
|
|
643
783
|
(node): node is PaneNode => node.nodeType === 'Pane'
|
|
644
784
|
);
|
|
645
785
|
actualSlug = dirtyPanes[0].slug;
|
|
646
786
|
} else {
|
|
647
|
-
// For storyfragment mode, get slug from the saved storyfragment
|
|
648
|
-
const ctx = getCtx();
|
|
649
|
-
const allDirtyNodes = ctx.getDirtyNodes();
|
|
650
787
|
const dirtyStoryFragments = allDirtyNodes.filter(
|
|
651
788
|
(node): node is StoryFragmentNode =>
|
|
652
789
|
node.nodeType === 'StoryFragment'
|
|
@@ -725,7 +862,11 @@ export default function SaveModal({
|
|
|
725
862
|
<div className="p-6">
|
|
726
863
|
<div className="mb-4">
|
|
727
864
|
<div className="mb-2 flex items-center justify-between">
|
|
728
|
-
<span
|
|
865
|
+
<span
|
|
866
|
+
className={`text-sm text-gray-700 ${
|
|
867
|
+
stageProgress.isUploading ? 'animate-pulse' : ''
|
|
868
|
+
}`}
|
|
869
|
+
>
|
|
729
870
|
{getStageDescription()}
|
|
730
871
|
</span>
|
|
731
872
|
{stage !== 'ERROR' && (
|
|
@@ -764,7 +905,7 @@ export default function SaveModal({
|
|
|
764
905
|
|
|
765
906
|
{stage === 'ERROR' && (
|
|
766
907
|
<div className="mb-4 rounded bg-red-50 p-3 text-red-800">
|
|
767
|
-
<div className="font-
|
|
908
|
+
<div className="font-bold">Save failed</div>
|
|
768
909
|
<div className="mt-1 text-sm">{error}</div>
|
|
769
910
|
</div>
|
|
770
911
|
)}
|
|
@@ -790,6 +931,12 @@ export default function SaveModal({
|
|
|
790
931
|
>
|
|
791
932
|
Keep Editing
|
|
792
933
|
</button>
|
|
934
|
+
<a
|
|
935
|
+
href="/storykeep/content"
|
|
936
|
+
className={`rounded bg-black px-4 py-2 text-white transition-colors hover:bg-white hover:text-black`}
|
|
937
|
+
>
|
|
938
|
+
Dashboard
|
|
939
|
+
</a>
|
|
793
940
|
</>
|
|
794
941
|
)}
|
|
795
942
|
{stage === 'ERROR' && (
|