astro-tractstack 2.0.0-rc.25 → 2.0.0-rc.27
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/package.json +1 -1
- package/templates/src/components/compositor/Node.tsx +1 -1
- package/templates/src/components/compositor/nodes/Pane.tsx +3 -3
- package/templates/src/components/edit/Header.tsx +9 -3
- package/templates/src/components/edit/SettingsPanel.tsx +21 -17
- package/templates/src/components/edit/ToolMode.tsx +93 -33
- package/templates/src/components/edit/state/SaveModal.tsx +68 -6
- package/templates/src/components/edit/storyfragment/StoryFragmentPanel_slug.tsx +56 -55
- package/templates/src/components/storykeep/state/FetchAnalytics.tsx +273 -225
- package/templates/src/pages/[...slug]/edit.astro +10 -3
- package/templates/src/pages/context/[...contextSlug]/edit.astro +10 -3
- package/templates/src/pages/storykeep/advanced.astro +1 -2
- package/templates/src/pages/storykeep/branding.astro +1 -2
- package/templates/src/pages/storykeep/content.astro +1 -2
- package/templates/src/pages/storykeep.astro +1 -2
- package/templates/src/stores/storykeep.ts +2 -0
- package/templates/src/utils/layout.ts +66 -121
package/package.json
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useEffect, useState, memo, type CSSProperties } from 'react';
|
|
1
|
+
import { useEffect, useState, memo, Fragment, type CSSProperties } from 'react';
|
|
2
2
|
import { getCtx } from '@/stores/nodes';
|
|
3
3
|
import { viewportKeyStore } from '@/stores/storykeep';
|
|
4
4
|
import { RenderChildren } from './RenderChildren';
|
|
@@ -27,7 +27,7 @@ const CodeHookContainer = ({
|
|
|
27
27
|
{Object.entries(payload.params).map(
|
|
28
28
|
([key, value]) =>
|
|
29
29
|
value && (
|
|
30
|
-
|
|
30
|
+
<Fragment key={key}>
|
|
31
31
|
<span className="min-w-24 font-bold text-gray-600">{key}:</span>
|
|
32
32
|
<div className="ml-2 flex flex-wrap gap-1">
|
|
33
33
|
{value.split('|').map((item, index) => (
|
|
@@ -39,7 +39,7 @@ const CodeHookContainer = ({
|
|
|
39
39
|
</span>
|
|
40
40
|
))}
|
|
41
41
|
</div>
|
|
42
|
-
|
|
42
|
+
</Fragment>
|
|
43
43
|
)
|
|
44
44
|
)}
|
|
45
45
|
</div>
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
viewportModeStore,
|
|
14
14
|
setViewportMode,
|
|
15
15
|
settingsPanelStore,
|
|
16
|
+
pendingHomePageSlugStore,
|
|
16
17
|
} from '@/stores/storykeep';
|
|
17
18
|
import { getCtx, ROOT_NODE_NAME } from '@/stores/nodes';
|
|
18
19
|
import SaveModal from '@/components/edit/state/SaveModal';
|
|
@@ -24,6 +25,7 @@ interface StoryKeepHeaderProps {
|
|
|
24
25
|
|
|
25
26
|
const StoryKeepHeader = ({ slug, isContext = false }: StoryKeepHeaderProps) => {
|
|
26
27
|
const viewport = useStore(viewportModeStore);
|
|
28
|
+
const pendingHomePageSlug = useStore(pendingHomePageSlugStore);
|
|
27
29
|
const ctx = getCtx();
|
|
28
30
|
const hasTitle = useStore(ctx.hasTitle);
|
|
29
31
|
const hasPanes = useStore(ctx.hasPanes);
|
|
@@ -61,7 +63,8 @@ const StoryKeepHeader = ({ slug, isContext = false }: StoryKeepHeaderProps) => {
|
|
|
61
63
|
};
|
|
62
64
|
|
|
63
65
|
const handleVisitPage = () => {
|
|
64
|
-
|
|
66
|
+
const hasChanges = canUndo || pendingHomePageSlug;
|
|
67
|
+
if (hasChanges) {
|
|
65
68
|
if (
|
|
66
69
|
confirm(
|
|
67
70
|
'You have unsaved changes. Do you want to visit the page anyway?'
|
|
@@ -89,6 +92,9 @@ const StoryKeepHeader = ({ slug, isContext = false }: StoryKeepHeaderProps) => {
|
|
|
89
92
|
{ value: 'desktop', Icon: ComputerDesktopIcon, title: 'Desktop Viewport' },
|
|
90
93
|
];
|
|
91
94
|
|
|
95
|
+
// Show save button if there are undo changes OR pending home page change
|
|
96
|
+
const shouldShowSave = canUndo || pendingHomePageSlug;
|
|
97
|
+
|
|
92
98
|
if (!hasTitle && !hasPanes) return null;
|
|
93
99
|
|
|
94
100
|
return (
|
|
@@ -127,7 +133,7 @@ const StoryKeepHeader = ({ slug, isContext = false }: StoryKeepHeaderProps) => {
|
|
|
127
133
|
className={`${iconClassName} relative`}
|
|
128
134
|
>
|
|
129
135
|
<GlobeAltIcon />
|
|
130
|
-
{
|
|
136
|
+
{shouldShowSave && (
|
|
131
137
|
<ExclamationTriangleIcon className="absolute -right-1 -top-1 h-3 w-3 rounded-full bg-white text-amber-500" />
|
|
132
138
|
)}
|
|
133
139
|
</button>
|
|
@@ -156,7 +162,7 @@ const StoryKeepHeader = ({ slug, isContext = false }: StoryKeepHeaderProps) => {
|
|
|
156
162
|
</div>
|
|
157
163
|
)}
|
|
158
164
|
|
|
159
|
-
{
|
|
165
|
+
{shouldShowSave && (
|
|
160
166
|
<div className="flex flex-wrap items-center justify-center gap-2 text-sm">
|
|
161
167
|
<button
|
|
162
168
|
onClick={handleSave}
|
|
@@ -17,13 +17,13 @@ const SettingsPanel = ({ config, availableCodeHooks }: SettingsPanelProps) => {
|
|
|
17
17
|
const ctx = getCtx();
|
|
18
18
|
const { value: toolModeVal } = useStore(ctx.toolModeValStore);
|
|
19
19
|
|
|
20
|
-
if (toolModeVal !==
|
|
20
|
+
if (toolModeVal !== 'styles' || !signal) {
|
|
21
21
|
return null;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
return (
|
|
25
25
|
<div
|
|
26
|
-
className="bg-mydarkgrey max-w-
|
|
26
|
+
className="bg-mydarkgrey flex h-full max-w-sm flex-col rounded-xl bg-opacity-20 p-0.5 backdrop-blur-sm"
|
|
27
27
|
style={
|
|
28
28
|
{
|
|
29
29
|
animation: window.matchMedia(
|
|
@@ -37,26 +37,30 @@ const SettingsPanel = ({ config, availableCodeHooks }: SettingsPanelProps) => {
|
|
|
37
37
|
}
|
|
38
38
|
>
|
|
39
39
|
<style>{`
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
40
|
+
@keyframes fadeInFromHalf {
|
|
41
|
+
0% { opacity: var(--fade-start, 0.5); }
|
|
42
|
+
100% { opacity: var(--fade-end, 1); }
|
|
43
|
+
}
|
|
44
|
+
`}</style>
|
|
45
45
|
<div
|
|
46
|
-
className="w-full rounded-lg border border-gray-200 bg-white
|
|
46
|
+
className="flex h-full min-h-0 w-full flex-col rounded-lg border border-gray-200 bg-white bg-opacity-85 shadow-xl"
|
|
47
47
|
style={{ maxWidth: '90vw' }}
|
|
48
48
|
>
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
<
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
49
|
+
{/* Header Section (fixed height) */}
|
|
50
|
+
<div className="flex-shrink-0 p-1.5 md:p-2.5">
|
|
51
|
+
<div className="mb-4 flex items-center justify-between">
|
|
52
|
+
<h3 className="text-myblue text-lg font-bold">{panelTitle}</h3>
|
|
53
|
+
<button
|
|
54
|
+
onClick={() => settingsPanelStore.set(null)}
|
|
55
|
+
className="hover:text-myblue text-gray-500"
|
|
56
|
+
>
|
|
57
|
+
<XMarkIcon className="h-5 w-5" />
|
|
58
|
+
</button>
|
|
59
|
+
</div>
|
|
57
60
|
</div>
|
|
58
61
|
|
|
59
|
-
|
|
62
|
+
{/* Scrollable Content Section */}
|
|
63
|
+
<div className="min-h-0 flex-1 space-y-4 overflow-y-auto p-1.5 pt-0 md:p-2.5 md:pt-0">
|
|
60
64
|
<div className="rounded bg-gray-50 p-1.5 md:p-2.5">
|
|
61
65
|
<PanelSwitch
|
|
62
66
|
config={config}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useEffect } from 'react';
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
2
|
import { useStore } from '@nanostores/react';
|
|
3
3
|
import PencilSquareIcon from '@heroicons/react/24/outline/PencilSquareIcon';
|
|
4
4
|
import PaintBrushIcon from '@heroicons/react/24/outline/PaintBrushIcon';
|
|
@@ -8,8 +8,11 @@ import PlusIcon from '@heroicons/react/24/outline/PlusIcon';
|
|
|
8
8
|
import BugAntIcon from '@heroicons/react/24/outline/BugAntIcon';
|
|
9
9
|
import { settingsPanelStore } from '@/stores/storykeep';
|
|
10
10
|
import { getCtx } from '@/stores/nodes';
|
|
11
|
+
import { debounce } from '@/utils/helpers';
|
|
11
12
|
import type { ToolModeVal } from '@/types/compositorTypes';
|
|
12
13
|
|
|
14
|
+
const SHORT_THRESHOLD = 650;
|
|
15
|
+
|
|
13
16
|
const storykeepToolModes = [
|
|
14
17
|
{
|
|
15
18
|
key: 'styles' as const,
|
|
@@ -41,12 +44,6 @@ const storykeepToolModes = [
|
|
|
41
44
|
title: 'Move',
|
|
42
45
|
description: 'Keyboard accessible re-order',
|
|
43
46
|
},
|
|
44
|
-
{
|
|
45
|
-
key: 'debug' as const,
|
|
46
|
-
Icon: BugAntIcon,
|
|
47
|
-
title: 'Debug',
|
|
48
|
-
description: 'Debug node ids',
|
|
49
|
-
},
|
|
50
47
|
] as const;
|
|
51
48
|
|
|
52
49
|
interface StoryKeepToolModeProps {
|
|
@@ -54,9 +51,10 @@ interface StoryKeepToolModeProps {
|
|
|
54
51
|
}
|
|
55
52
|
|
|
56
53
|
const StoryKeepToolMode = ({ isContext }: StoryKeepToolModeProps) => {
|
|
57
|
-
//const signal = useStore(settingsPanelStore);
|
|
58
54
|
const ctx = getCtx();
|
|
59
55
|
const { value: toolModeVal } = useStore(ctx.toolModeValStore);
|
|
56
|
+
const showGuids = useStore(ctx.showGuids);
|
|
57
|
+
const navRef = useRef<HTMLElement>(null);
|
|
60
58
|
|
|
61
59
|
const hasTitle = useStore(ctx.hasTitle);
|
|
62
60
|
const hasPanes = useStore(ctx.hasPanes);
|
|
@@ -64,6 +62,8 @@ const StoryKeepToolMode = ({ isContext }: StoryKeepToolModeProps) => {
|
|
|
64
62
|
const className =
|
|
65
63
|
'w-8 h-8 py-1 rounded-xl bg-white text-myblue hover:bg-mygreen/20 hover:text-black hover:rotate-3 cursor-pointer transition-all';
|
|
66
64
|
const classNameActive = 'w-8 h-8 py-1.5 rounded-md bg-myblue text-white';
|
|
65
|
+
const classNameDebugActive =
|
|
66
|
+
'w-8 h-8 py-1.5 rounded-md bg-orange-500 text-white';
|
|
67
67
|
|
|
68
68
|
const currentToolMode =
|
|
69
69
|
storykeepToolModes.find((mode) => mode.key === toolModeVal) ??
|
|
@@ -72,49 +72,109 @@ const StoryKeepToolMode = ({ isContext }: StoryKeepToolModeProps) => {
|
|
|
72
72
|
const handleClick = (mode: ToolModeVal) => {
|
|
73
73
|
settingsPanelStore.set(null);
|
|
74
74
|
ctx.toolModeValStore.set({ value: mode });
|
|
75
|
-
ctx.showGuids.set(mode === `debug`);
|
|
76
75
|
ctx.notifyNode('root');
|
|
77
76
|
};
|
|
78
77
|
|
|
79
|
-
|
|
78
|
+
const handleDebugToggle = () => {
|
|
79
|
+
ctx.showGuids.set(!showGuids);
|
|
80
|
+
ctx.notifyNode('root');
|
|
81
|
+
};
|
|
82
|
+
|
|
80
83
|
useEffect(() => {
|
|
81
84
|
const handleEscapeKey = (event: KeyboardEvent) => {
|
|
82
85
|
if (event.key === 'Escape') {
|
|
83
86
|
ctx.toolModeValStore.set({ value: 'text' });
|
|
84
|
-
console.log('Tool mode reset to text via Escape');
|
|
85
87
|
}
|
|
86
88
|
};
|
|
89
|
+
|
|
90
|
+
const toolModeNav = navRef.current;
|
|
91
|
+
|
|
92
|
+
// If the <nav> element hasn't been rendered yet, do nothing.
|
|
93
|
+
// The hook will re-run when hasTitle/hasPanes changes and it does render.
|
|
94
|
+
if (!toolModeNav) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const updateToolbarLayout = debounce(() => {
|
|
99
|
+
const isWideAndShort =
|
|
100
|
+
window.innerWidth >= 801 && window.innerHeight <= SHORT_THRESHOLD;
|
|
101
|
+
toolModeNav.classList.toggle('is-compact-widget', isWideAndShort);
|
|
102
|
+
}, 50);
|
|
103
|
+
|
|
87
104
|
document.addEventListener('keydown', handleEscapeKey);
|
|
105
|
+
window.addEventListener('resize', updateToolbarLayout);
|
|
106
|
+
|
|
107
|
+
updateToolbarLayout(); // Initial check
|
|
108
|
+
|
|
88
109
|
return () => {
|
|
89
110
|
document.removeEventListener('keydown', handleEscapeKey);
|
|
111
|
+
window.removeEventListener('resize', updateToolbarLayout);
|
|
90
112
|
};
|
|
91
|
-
|
|
113
|
+
// This dependency array is the key. The effect will re-run when the render conditions change.
|
|
114
|
+
}, [ctx, hasTitle, hasPanes, isContext]);
|
|
92
115
|
|
|
93
|
-
if (!hasTitle || (!hasPanes && !isContext))
|
|
116
|
+
if (!hasTitle || (!hasPanes && !isContext)) {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
94
119
|
|
|
95
120
|
return (
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
121
|
+
<>
|
|
122
|
+
<style>{`
|
|
123
|
+
#mainNav.is-compact-widget {
|
|
124
|
+
position: fixed;
|
|
125
|
+
bottom: 0.25rem;
|
|
126
|
+
left: 0rem;
|
|
127
|
+
top: auto;
|
|
128
|
+
right: auto;
|
|
129
|
+
height: auto;
|
|
130
|
+
width: auto;
|
|
131
|
+
padding: 0.5rem;
|
|
132
|
+
border-radius: 0 0.75rem 0.75rem 0;
|
|
133
|
+
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
|
|
134
|
+
background-color: rgba(252, 252, 252, 0.7);
|
|
135
|
+
backdrop-filter: blur(4px);
|
|
136
|
+
border: 1px solid rgba(0, 0, 0, 0.05);
|
|
137
|
+
}
|
|
138
|
+
#mainNav.is-compact-widget > div {
|
|
139
|
+
flex-direction: row;
|
|
140
|
+
flex-wrap: nowrap;
|
|
141
|
+
align-items: center;
|
|
142
|
+
gap: 1rem;
|
|
143
|
+
margin: 0;
|
|
144
|
+
padding: 0;
|
|
145
|
+
height: auto;
|
|
146
|
+
}
|
|
147
|
+
`}</style>
|
|
148
|
+
<nav
|
|
149
|
+
id="mainNav"
|
|
150
|
+
ref={navRef}
|
|
151
|
+
className="z-102 bg-mywhite fixed bottom-0 left-0 right-0 pt-1.5 md:sticky md:bottom-auto md:left-0 md:top-24 md:h-screen md:w-16 md:pt-0"
|
|
152
|
+
>
|
|
153
|
+
<div className="flex flex-wrap justify-around gap-4 py-0.5 md:mt-0 md:flex-col md:items-center md:gap-8 md:space-x-0 md:space-y-2 md:py-2">
|
|
154
|
+
<div className="text-mydarkgrey text-center text-sm font-bold">
|
|
155
|
+
mode:
|
|
156
|
+
<div className="font-action text-myblue pt-1.5 text-center text-xs">
|
|
157
|
+
{currentToolMode.title}
|
|
158
|
+
</div>
|
|
105
159
|
</div>
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
160
|
+
{storykeepToolModes.map(({ key, Icon, description }) => (
|
|
161
|
+
<div title={description} key={key}>
|
|
162
|
+
{key === toolModeVal ? (
|
|
163
|
+
<Icon className={classNameActive} />
|
|
164
|
+
) : (
|
|
165
|
+
<Icon className={className} onClick={() => handleClick(key)} />
|
|
166
|
+
)}
|
|
167
|
+
</div>
|
|
168
|
+
))}
|
|
169
|
+
<div title="Toggle debug node ids" key="debug">
|
|
170
|
+
<BugAntIcon
|
|
171
|
+
className={showGuids ? classNameDebugActive : className}
|
|
172
|
+
onClick={handleDebugToggle}
|
|
173
|
+
/>
|
|
114
174
|
</div>
|
|
115
|
-
|
|
116
|
-
</
|
|
117
|
-
|
|
175
|
+
</div>
|
|
176
|
+
</nav>
|
|
177
|
+
</>
|
|
118
178
|
);
|
|
119
179
|
};
|
|
120
180
|
|
|
@@ -11,6 +11,7 @@ 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 {
|
|
@@ -29,6 +30,7 @@ type SaveStage =
|
|
|
29
30
|
| 'SAVING_STORY_FRAGMENTS'
|
|
30
31
|
| 'LINKING_FILES'
|
|
31
32
|
| 'PROCESSING_STYLES'
|
|
33
|
+
| 'UPDATING_HOME_PAGE'
|
|
32
34
|
| 'COMPLETED'
|
|
33
35
|
| 'ERROR';
|
|
34
36
|
|
|
@@ -61,13 +63,9 @@ export default function SaveModal({
|
|
|
61
63
|
const [debugMessages, setDebugMessages] = useState<string[]>([]);
|
|
62
64
|
const isSaving = useRef(false);
|
|
63
65
|
const [isNavigating, setIsNavigating] = useState(false);
|
|
64
|
-
|
|
65
|
-
// Determine if we're in create mode
|
|
66
66
|
const isCreateMode = slug === 'create';
|
|
67
|
-
|
|
68
67
|
const contentMap = fullContentMapStore.get();
|
|
69
|
-
|
|
70
|
-
// Get backend URL
|
|
68
|
+
const pendingHomePageSlug = pendingHomePageSlugStore.get();
|
|
71
69
|
const goBackend =
|
|
72
70
|
import.meta.env.PUBLIC_GO_BACKEND || 'http://localhost:8080';
|
|
73
71
|
const tenantId = import.meta.env.PUBLIC_TENANTID || 'default';
|
|
@@ -145,7 +143,8 @@ export default function SaveModal({
|
|
|
145
143
|
if (
|
|
146
144
|
relevantNodeCount === 0 &&
|
|
147
145
|
nodesWithPendingFiles.length === 0 &&
|
|
148
|
-
storyFragmentsWithPendingImages.length === 0
|
|
146
|
+
storyFragmentsWithPendingImages.length === 0 &&
|
|
147
|
+
!pendingHomePageSlug
|
|
149
148
|
) {
|
|
150
149
|
addDebugMessage('No changes to save');
|
|
151
150
|
setStage('COMPLETED');
|
|
@@ -614,6 +613,67 @@ export default function SaveModal({
|
|
|
614
613
|
throw new Error(`Failed to process styles: ${errorMsg}`);
|
|
615
614
|
}
|
|
616
615
|
|
|
616
|
+
// Check if we need to update home page
|
|
617
|
+
if (pendingHomePageSlug) {
|
|
618
|
+
setStage('UPDATING_HOME_PAGE');
|
|
619
|
+
setProgress(98);
|
|
620
|
+
addDebugMessage(`Updating home page to: ${pendingHomePageSlug}`);
|
|
621
|
+
|
|
622
|
+
try {
|
|
623
|
+
// First get current brand config
|
|
624
|
+
const response = await fetch(`${goBackend}/api/v1/config/brand`, {
|
|
625
|
+
method: 'GET',
|
|
626
|
+
headers: {
|
|
627
|
+
'Content-Type': 'application/json',
|
|
628
|
+
'X-Tenant-ID': tenantId,
|
|
629
|
+
},
|
|
630
|
+
credentials: 'include',
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
if (!response.ok) {
|
|
634
|
+
throw new Error(
|
|
635
|
+
`Failed to get current brand config: ${response.status}`
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
const currentBrandConfig = await response.json();
|
|
640
|
+
|
|
641
|
+
// Update HOME_SLUG
|
|
642
|
+
const updatedBrandConfig = {
|
|
643
|
+
...currentBrandConfig,
|
|
644
|
+
HOME_SLUG: pendingHomePageSlug,
|
|
645
|
+
};
|
|
646
|
+
|
|
647
|
+
const updateResponse = await fetch(
|
|
648
|
+
`${goBackend}/api/v1/config/brand`,
|
|
649
|
+
{
|
|
650
|
+
method: 'PUT',
|
|
651
|
+
headers: {
|
|
652
|
+
'Content-Type': 'application/json',
|
|
653
|
+
'X-Tenant-ID': tenantId,
|
|
654
|
+
},
|
|
655
|
+
credentials: 'include',
|
|
656
|
+
body: JSON.stringify(updatedBrandConfig),
|
|
657
|
+
}
|
|
658
|
+
);
|
|
659
|
+
|
|
660
|
+
if (!updateResponse.ok) {
|
|
661
|
+
throw new Error(
|
|
662
|
+
`Failed to update home page: ${updateResponse.status}`
|
|
663
|
+
);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// Clear the pending operation
|
|
667
|
+
pendingHomePageSlugStore.set(null);
|
|
668
|
+
addDebugMessage('Home page updated successfully');
|
|
669
|
+
} catch (error) {
|
|
670
|
+
const errorMsg =
|
|
671
|
+
error instanceof Error ? error.message : 'Unknown error';
|
|
672
|
+
addDebugMessage(`Home page update failed: ${errorMsg}`);
|
|
673
|
+
throw new Error(`Failed to update home page: ${errorMsg}`);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
617
677
|
// Success!
|
|
618
678
|
setStage('COMPLETED');
|
|
619
679
|
setProgress(100);
|
|
@@ -658,6 +718,8 @@ export default function SaveModal({
|
|
|
658
718
|
return 'Linking file relationships...';
|
|
659
719
|
case 'PROCESSING_STYLES':
|
|
660
720
|
return 'Processing styles...';
|
|
721
|
+
case 'UPDATING_HOME_PAGE':
|
|
722
|
+
return 'Updating home page...';
|
|
661
723
|
case 'COMPLETED':
|
|
662
724
|
return `${actionText} ${modeText.toLowerCase()} completed successfully!`;
|
|
663
725
|
case 'ERROR':
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { useState, useEffect, type ChangeEvent } from 'react';
|
|
2
|
+
import { useStore } from '@nanostores/react';
|
|
2
3
|
import ExclamationTriangleIcon from '@heroicons/react/24/outline/ExclamationTriangleIcon';
|
|
3
4
|
import CheckIcon from '@heroicons/react/24/outline/CheckIcon';
|
|
4
5
|
import LockClosedIcon from '@heroicons/react/24/outline/LockClosedIcon';
|
|
6
|
+
import { Switch } from '@ark-ui/react/switch';
|
|
5
7
|
import { getCtx } from '@/stores/nodes';
|
|
8
|
+
import { pendingHomePageSlugStore } from '@/stores/storykeep';
|
|
6
9
|
import { cloneDeep } from '@/utils/helpers';
|
|
7
10
|
import type { BrandConfig } from '@/types/tractstack';
|
|
8
11
|
import {
|
|
@@ -28,6 +31,8 @@ const StoryFragmentSlugPanel = ({
|
|
|
28
31
|
const [validationError, setValidationError] = useState<string | null>(null);
|
|
29
32
|
const [canSave, setCanSave] = useState(false);
|
|
30
33
|
const isHomeSlug = slug === config.HOME_SLUG;
|
|
34
|
+
const pendingHomePageSlug = useStore(pendingHomePageSlugStore);
|
|
35
|
+
const isSetAsHomePage = pendingHomePageSlug === slug;
|
|
31
36
|
|
|
32
37
|
const ctx = getCtx();
|
|
33
38
|
const allNodes = ctx.allNodes.get();
|
|
@@ -129,6 +134,14 @@ const StoryFragmentSlugPanel = ({
|
|
|
129
134
|
}
|
|
130
135
|
};
|
|
131
136
|
|
|
137
|
+
const handleSetAsHomePageChange = (details: { checked: boolean }) => {
|
|
138
|
+
if (details.checked) {
|
|
139
|
+
pendingHomePageSlugStore.set(slug);
|
|
140
|
+
} else {
|
|
141
|
+
pendingHomePageSlugStore.set(null);
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
|
|
132
145
|
return (
|
|
133
146
|
<div className="group mb-4 w-full rounded-b-md bg-white px-1.5 py-6">
|
|
134
147
|
<div className="px-3.5">
|
|
@@ -194,72 +207,60 @@ const StoryFragmentSlugPanel = ({
|
|
|
194
207
|
</span>
|
|
195
208
|
</div>
|
|
196
209
|
</div>
|
|
210
|
+
|
|
197
211
|
{validationError && (
|
|
198
212
|
<div className="mt-2 text-sm text-red-600">
|
|
199
213
|
<ExclamationTriangleIcon className="mr-1 inline h-4 w-4" />
|
|
200
214
|
{validationError}
|
|
201
215
|
</div>
|
|
202
216
|
)}
|
|
217
|
+
|
|
203
218
|
{isHomeSlug && (
|
|
204
|
-
<div className="mt-
|
|
205
|
-
<
|
|
206
|
-
|
|
219
|
+
<div className="mt-4">
|
|
220
|
+
<div className="inline-flex items-center rounded-full bg-blue-100 px-3 py-1.5 text-sm font-medium text-blue-800">
|
|
221
|
+
<LockClosedIcon className="mr-1.5 h-4 w-4" />
|
|
222
|
+
Home Page
|
|
223
|
+
</div>
|
|
224
|
+
<div className="mt-2 text-sm text-gray-600">
|
|
225
|
+
This is your current home page
|
|
226
|
+
</div>
|
|
207
227
|
</div>
|
|
208
228
|
)}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
{
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
Slug must be at least 3 characters
|
|
238
|
-
</span>
|
|
239
|
-
)}
|
|
240
|
-
{charCount >= 3 && charCount < 5 && !validationError && (
|
|
241
|
-
<span className="text-gray-500">
|
|
242
|
-
Consider adding more characters for better description
|
|
243
|
-
</span>
|
|
244
|
-
)}
|
|
245
|
-
{warning && !validationError && (
|
|
246
|
-
<span className="text-yellow-500">
|
|
247
|
-
Slug is getting long - consider shortening it
|
|
248
|
-
</span>
|
|
249
|
-
)}
|
|
250
|
-
{isValid && canSave && charCount >= 5 && !validationError && (
|
|
251
|
-
<span className="text-green-500">
|
|
252
|
-
Good URL length and format!
|
|
253
|
-
</span>
|
|
254
|
-
)}
|
|
255
|
-
{isValid && !canSave && !validationError && (
|
|
256
|
-
<span className="text-gray-500">
|
|
257
|
-
Valid characters but needs proper formatting to save
|
|
258
|
-
</span>
|
|
259
|
-
)}
|
|
260
|
-
</>
|
|
229
|
+
|
|
230
|
+
{!isHomeSlug && isValid && canSave && (
|
|
231
|
+
<div className="mt-4">
|
|
232
|
+
<div className="flex items-center space-x-3">
|
|
233
|
+
<Switch.Root
|
|
234
|
+
checked={isSetAsHomePage}
|
|
235
|
+
onCheckedChange={handleSetAsHomePageChange}
|
|
236
|
+
className="flex items-center"
|
|
237
|
+
>
|
|
238
|
+
<Switch.Control
|
|
239
|
+
className={`relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none ${
|
|
240
|
+
isSetAsHomePage ? 'bg-cyan-600' : 'bg-gray-200'
|
|
241
|
+
}`}
|
|
242
|
+
>
|
|
243
|
+
<Switch.Thumb
|
|
244
|
+
className={`pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow-lg ring-0 transition duration-200 ease-in-out ${
|
|
245
|
+
isSetAsHomePage ? 'translate-x-5' : 'translate-x-0'
|
|
246
|
+
}`}
|
|
247
|
+
/>
|
|
248
|
+
</Switch.Control>
|
|
249
|
+
<Switch.HiddenInput />
|
|
250
|
+
</Switch.Root>
|
|
251
|
+
<span className="text-sm text-gray-700">Set as Home Page</span>
|
|
252
|
+
</div>
|
|
253
|
+
{isSetAsHomePage && (
|
|
254
|
+
<div className="mt-2 text-sm text-cyan-600">
|
|
255
|
+
✓ Will be set as home page when saved
|
|
256
|
+
</div>
|
|
261
257
|
)}
|
|
262
258
|
</div>
|
|
259
|
+
)}
|
|
260
|
+
|
|
261
|
+
<div className="mt-4 text-sm text-gray-600">
|
|
262
|
+
Create a clean, descriptive URL slug that helps users and search
|
|
263
|
+
engines understand the page content.
|
|
263
264
|
</div>
|
|
264
265
|
</div>
|
|
265
266
|
</div>
|