@actuate-media/cms-admin 0.4.0 → 0.7.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.
Files changed (212) hide show
  1. package/dist/AdminRoot.d.ts.map +1 -1
  2. package/dist/AdminRoot.js +35 -0
  3. package/dist/AdminRoot.js.map +1 -1
  4. package/dist/actuate-admin.css +1 -1
  5. package/dist/components/Breadcrumbs.d.ts.map +1 -1
  6. package/dist/components/Breadcrumbs.js +1 -0
  7. package/dist/components/Breadcrumbs.js.map +1 -1
  8. package/dist/components/ErrorBoundary.js +1 -1
  9. package/dist/components/ErrorBoundary.js.map +1 -1
  10. package/dist/hooks/useBuilderState.d.ts +49 -0
  11. package/dist/hooks/useBuilderState.d.ts.map +1 -0
  12. package/dist/hooks/useBuilderState.js +238 -0
  13. package/dist/hooks/useBuilderState.js.map +1 -0
  14. package/dist/index.d.ts +7 -0
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +4 -0
  17. package/dist/index.js.map +1 -1
  18. package/dist/layout/Sidebar.d.ts.map +1 -1
  19. package/dist/layout/Sidebar.js +2 -2
  20. package/dist/layout/Sidebar.js.map +1 -1
  21. package/dist/views/ForgotPassword.d.ts +5 -0
  22. package/dist/views/ForgotPassword.d.ts.map +1 -0
  23. package/dist/views/ForgotPassword.js +41 -0
  24. package/dist/views/ForgotPassword.js.map +1 -0
  25. package/dist/views/ResetPassword.d.ts +6 -0
  26. package/dist/views/ResetPassword.d.ts.map +1 -0
  27. package/dist/views/ResetPassword.js +46 -0
  28. package/dist/views/ResetPassword.js.map +1 -0
  29. package/dist/views/ScriptTagEditor.d.ts +6 -0
  30. package/dist/views/ScriptTagEditor.d.ts.map +1 -0
  31. package/dist/views/ScriptTagEditor.js +109 -0
  32. package/dist/views/ScriptTagEditor.js.map +1 -0
  33. package/dist/views/ScriptTags.d.ts +5 -0
  34. package/dist/views/ScriptTags.d.ts.map +1 -0
  35. package/dist/views/ScriptTags.js +54 -0
  36. package/dist/views/ScriptTags.js.map +1 -0
  37. package/dist/views/page-builder/AIBlockAssist.d.ts +9 -0
  38. package/dist/views/page-builder/AIBlockAssist.d.ts.map +1 -0
  39. package/dist/views/page-builder/AIBlockAssist.js +40 -0
  40. package/dist/views/page-builder/AIBlockAssist.js.map +1 -0
  41. package/dist/views/page-builder/AIGenerateDialog.d.ts +8 -0
  42. package/dist/views/page-builder/AIGenerateDialog.d.ts.map +1 -0
  43. package/dist/views/page-builder/AIGenerateDialog.js +170 -0
  44. package/dist/views/page-builder/AIGenerateDialog.js.map +1 -0
  45. package/dist/views/page-builder/BlockEditor.d.ts +11 -0
  46. package/dist/views/page-builder/BlockEditor.d.ts.map +1 -0
  47. package/dist/views/page-builder/BlockEditor.js +67 -0
  48. package/dist/views/page-builder/BlockEditor.js.map +1 -0
  49. package/dist/views/page-builder/BlockPicker.d.ts +7 -0
  50. package/dist/views/page-builder/BlockPicker.d.ts.map +1 -0
  51. package/dist/views/page-builder/BlockPicker.js +102 -0
  52. package/dist/views/page-builder/BlockPicker.js.map +1 -0
  53. package/dist/views/page-builder/BottomBar.d.ts +9 -0
  54. package/dist/views/page-builder/BottomBar.d.ts.map +1 -0
  55. package/dist/views/page-builder/BottomBar.js +13 -0
  56. package/dist/views/page-builder/BottomBar.js.map +1 -0
  57. package/dist/views/page-builder/BuilderToolbar.d.ts +21 -0
  58. package/dist/views/page-builder/BuilderToolbar.d.ts.map +1 -0
  59. package/dist/views/page-builder/BuilderToolbar.js +18 -0
  60. package/dist/views/page-builder/BuilderToolbar.js.map +1 -0
  61. package/dist/views/page-builder/ContextPanel.d.ts +20 -0
  62. package/dist/views/page-builder/ContextPanel.d.ts.map +1 -0
  63. package/dist/views/page-builder/ContextPanel.js +40 -0
  64. package/dist/views/page-builder/ContextPanel.js.map +1 -0
  65. package/dist/views/page-builder/DesignScore.d.ts +6 -0
  66. package/dist/views/page-builder/DesignScore.d.ts.map +1 -0
  67. package/dist/views/page-builder/DesignScore.js +93 -0
  68. package/dist/views/page-builder/DesignScore.js.map +1 -0
  69. package/dist/views/page-builder/NodeSettings.d.ts +12 -0
  70. package/dist/views/page-builder/NodeSettings.d.ts.map +1 -0
  71. package/dist/views/page-builder/NodeSettings.js +80 -0
  72. package/dist/views/page-builder/NodeSettings.js.map +1 -0
  73. package/dist/views/page-builder/PageBuilder.d.ts +8 -0
  74. package/dist/views/page-builder/PageBuilder.d.ts.map +1 -0
  75. package/dist/views/page-builder/PageBuilder.js +126 -0
  76. package/dist/views/page-builder/PageBuilder.js.map +1 -0
  77. package/dist/views/page-builder/PageSettings.d.ts +7 -0
  78. package/dist/views/page-builder/PageSettings.d.ts.map +1 -0
  79. package/dist/views/page-builder/PageSettings.js +27 -0
  80. package/dist/views/page-builder/PageSettings.js.map +1 -0
  81. package/dist/views/page-builder/SEOPanel.d.ts +10 -0
  82. package/dist/views/page-builder/SEOPanel.d.ts.map +1 -0
  83. package/dist/views/page-builder/SEOPanel.js +105 -0
  84. package/dist/views/page-builder/SEOPanel.js.map +1 -0
  85. package/dist/views/page-builder/SavedSections.d.ts +6 -0
  86. package/dist/views/page-builder/SavedSections.d.ts.map +1 -0
  87. package/dist/views/page-builder/SavedSections.js +145 -0
  88. package/dist/views/page-builder/SavedSections.js.map +1 -0
  89. package/dist/views/page-builder/TemplatePicker.d.ts +7 -0
  90. package/dist/views/page-builder/TemplatePicker.d.ts.map +1 -0
  91. package/dist/views/page-builder/TemplatePicker.js +68 -0
  92. package/dist/views/page-builder/TemplatePicker.js.map +1 -0
  93. package/dist/views/page-builder/block-renderers/CTAPreview.d.ts +3 -0
  94. package/dist/views/page-builder/block-renderers/CTAPreview.d.ts.map +1 -0
  95. package/dist/views/page-builder/block-renderers/CTAPreview.js +19 -0
  96. package/dist/views/page-builder/block-renderers/CTAPreview.js.map +1 -0
  97. package/dist/views/page-builder/block-renderers/CardsPreview.d.ts +3 -0
  98. package/dist/views/page-builder/block-renderers/CardsPreview.d.ts.map +1 -0
  99. package/dist/views/page-builder/block-renderers/CardsPreview.js +22 -0
  100. package/dist/views/page-builder/block-renderers/CardsPreview.js.map +1 -0
  101. package/dist/views/page-builder/block-renderers/CodePreview.d.ts +3 -0
  102. package/dist/views/page-builder/block-renderers/CodePreview.d.ts.map +1 -0
  103. package/dist/views/page-builder/block-renderers/CodePreview.js +16 -0
  104. package/dist/views/page-builder/block-renderers/CodePreview.js.map +1 -0
  105. package/dist/views/page-builder/block-renderers/FAQPreview.d.ts +3 -0
  106. package/dist/views/page-builder/block-renderers/FAQPreview.d.ts.map +1 -0
  107. package/dist/views/page-builder/block-renderers/FAQPreview.js +24 -0
  108. package/dist/views/page-builder/block-renderers/FAQPreview.js.map +1 -0
  109. package/dist/views/page-builder/block-renderers/FallbackPreview.d.ts +6 -0
  110. package/dist/views/page-builder/block-renderers/FallbackPreview.d.ts.map +1 -0
  111. package/dist/views/page-builder/block-renderers/FallbackPreview.js +7 -0
  112. package/dist/views/page-builder/block-renderers/FallbackPreview.js.map +1 -0
  113. package/dist/views/page-builder/block-renderers/FormPreview.d.ts +3 -0
  114. package/dist/views/page-builder/block-renderers/FormPreview.d.ts.map +1 -0
  115. package/dist/views/page-builder/block-renderers/FormPreview.js +14 -0
  116. package/dist/views/page-builder/block-renderers/FormPreview.js.map +1 -0
  117. package/dist/views/page-builder/block-renderers/GalleryPreview.d.ts +3 -0
  118. package/dist/views/page-builder/block-renderers/GalleryPreview.d.ts.map +1 -0
  119. package/dist/views/page-builder/block-renderers/GalleryPreview.js +21 -0
  120. package/dist/views/page-builder/block-renderers/GalleryPreview.js.map +1 -0
  121. package/dist/views/page-builder/block-renderers/HeroPreview.d.ts +3 -0
  122. package/dist/views/page-builder/block-renderers/HeroPreview.d.ts.map +1 -0
  123. package/dist/views/page-builder/block-renderers/HeroPreview.js +19 -0
  124. package/dist/views/page-builder/block-renderers/HeroPreview.js.map +1 -0
  125. package/dist/views/page-builder/block-renderers/ImagePreview.d.ts +3 -0
  126. package/dist/views/page-builder/block-renderers/ImagePreview.d.ts.map +1 -0
  127. package/dist/views/page-builder/block-renderers/ImagePreview.js +17 -0
  128. package/dist/views/page-builder/block-renderers/ImagePreview.js.map +1 -0
  129. package/dist/views/page-builder/block-renderers/TextPreview.d.ts +3 -0
  130. package/dist/views/page-builder/block-renderers/TextPreview.d.ts.map +1 -0
  131. package/dist/views/page-builder/block-renderers/TextPreview.js +26 -0
  132. package/dist/views/page-builder/block-renderers/TextPreview.js.map +1 -0
  133. package/dist/views/page-builder/block-renderers/VideoPreview.d.ts +3 -0
  134. package/dist/views/page-builder/block-renderers/VideoPreview.d.ts.map +1 -0
  135. package/dist/views/page-builder/block-renderers/VideoPreview.js +21 -0
  136. package/dist/views/page-builder/block-renderers/VideoPreview.js.map +1 -0
  137. package/dist/views/page-builder/block-renderers/index.d.ts +9 -0
  138. package/dist/views/page-builder/block-renderers/index.d.ts.map +1 -0
  139. package/dist/views/page-builder/block-renderers/index.js +25 -0
  140. package/dist/views/page-builder/block-renderers/index.js.map +1 -0
  141. package/dist/views/page-builder/canvas/BlockRenderer.d.ts +8 -0
  142. package/dist/views/page-builder/canvas/BlockRenderer.d.ts.map +1 -0
  143. package/dist/views/page-builder/canvas/BlockRenderer.js +30 -0
  144. package/dist/views/page-builder/canvas/BlockRenderer.js.map +1 -0
  145. package/dist/views/page-builder/canvas/BuilderCanvas.d.ts +10 -0
  146. package/dist/views/page-builder/canvas/BuilderCanvas.d.ts.map +1 -0
  147. package/dist/views/page-builder/canvas/BuilderCanvas.js +26 -0
  148. package/dist/views/page-builder/canvas/BuilderCanvas.js.map +1 -0
  149. package/dist/views/page-builder/canvas/ColumnRenderer.d.ts +8 -0
  150. package/dist/views/page-builder/canvas/ColumnRenderer.d.ts.map +1 -0
  151. package/dist/views/page-builder/canvas/ColumnRenderer.js +36 -0
  152. package/dist/views/page-builder/canvas/ColumnRenderer.js.map +1 -0
  153. package/dist/views/page-builder/canvas/ContainerRenderer.d.ts +8 -0
  154. package/dist/views/page-builder/canvas/ContainerRenderer.d.ts.map +1 -0
  155. package/dist/views/page-builder/canvas/ContainerRenderer.js +33 -0
  156. package/dist/views/page-builder/canvas/ContainerRenderer.js.map +1 -0
  157. package/dist/views/page-builder/canvas/RowRenderer.d.ts +8 -0
  158. package/dist/views/page-builder/canvas/RowRenderer.d.ts.map +1 -0
  159. package/dist/views/page-builder/canvas/RowRenderer.js +32 -0
  160. package/dist/views/page-builder/canvas/RowRenderer.js.map +1 -0
  161. package/dist/views/page-builder/canvas/SectionRenderer.d.ts +8 -0
  162. package/dist/views/page-builder/canvas/SectionRenderer.d.ts.map +1 -0
  163. package/dist/views/page-builder/canvas/SectionRenderer.js +54 -0
  164. package/dist/views/page-builder/canvas/SectionRenderer.js.map +1 -0
  165. package/dist/views/page-builder/canvas/index.d.ts +3 -0
  166. package/dist/views/page-builder/canvas/index.d.ts.map +1 -0
  167. package/dist/views/page-builder/canvas/index.js +2 -0
  168. package/dist/views/page-builder/canvas/index.js.map +1 -0
  169. package/package.json +7 -4
  170. package/src/AdminRoot.tsx +41 -0
  171. package/src/components/Breadcrumbs.tsx +1 -0
  172. package/src/components/ErrorBoundary.tsx +3 -3
  173. package/src/hooks/useBuilderState.ts +328 -0
  174. package/src/index.ts +8 -0
  175. package/src/layout/Sidebar.tsx +7 -0
  176. package/src/views/ForgotPassword.tsx +136 -0
  177. package/src/views/ResetPassword.tsx +192 -0
  178. package/src/views/ScriptTagEditor.tsx +361 -0
  179. package/src/views/ScriptTags.tsx +174 -0
  180. package/src/views/page-builder/AIBlockAssist.tsx +68 -0
  181. package/src/views/page-builder/AIGenerateDialog.tsx +574 -0
  182. package/src/views/page-builder/BlockEditor.tsx +352 -0
  183. package/src/views/page-builder/BlockPicker.tsx +338 -0
  184. package/src/views/page-builder/BottomBar.tsx +64 -0
  185. package/src/views/page-builder/BuilderToolbar.tsx +218 -0
  186. package/src/views/page-builder/ContextPanel.tsx +145 -0
  187. package/src/views/page-builder/DesignScore.tsx +258 -0
  188. package/src/views/page-builder/NodeSettings.tsx +515 -0
  189. package/src/views/page-builder/PageBuilder.tsx +288 -0
  190. package/src/views/page-builder/PageSettings.tsx +161 -0
  191. package/src/views/page-builder/SEOPanel.tsx +485 -0
  192. package/src/views/page-builder/SavedSections.tsx +486 -0
  193. package/src/views/page-builder/TemplatePicker.tsx +201 -0
  194. package/src/views/page-builder/block-renderers/CTAPreview.tsx +81 -0
  195. package/src/views/page-builder/block-renderers/CardsPreview.tsx +71 -0
  196. package/src/views/page-builder/block-renderers/CodePreview.tsx +46 -0
  197. package/src/views/page-builder/block-renderers/FAQPreview.tsx +90 -0
  198. package/src/views/page-builder/block-renderers/FallbackPreview.tsx +18 -0
  199. package/src/views/page-builder/block-renderers/FormPreview.tsx +69 -0
  200. package/src/views/page-builder/block-renderers/GalleryPreview.tsx +93 -0
  201. package/src/views/page-builder/block-renderers/HeroPreview.tsx +103 -0
  202. package/src/views/page-builder/block-renderers/ImagePreview.tsx +54 -0
  203. package/src/views/page-builder/block-renderers/TextPreview.tsx +81 -0
  204. package/src/views/page-builder/block-renderers/VideoPreview.tsx +78 -0
  205. package/src/views/page-builder/block-renderers/index.ts +34 -0
  206. package/src/views/page-builder/canvas/BlockRenderer.tsx +62 -0
  207. package/src/views/page-builder/canvas/BuilderCanvas.tsx +90 -0
  208. package/src/views/page-builder/canvas/ColumnRenderer.tsx +86 -0
  209. package/src/views/page-builder/canvas/ContainerRenderer.tsx +71 -0
  210. package/src/views/page-builder/canvas/RowRenderer.tsx +72 -0
  211. package/src/views/page-builder/canvas/SectionRenderer.tsx +97 -0
  212. package/src/views/page-builder/canvas/index.ts +2 -0
package/src/AdminRoot.tsx CHANGED
@@ -13,12 +13,18 @@ import { FormEditor } from './views/FormEditor.js';
13
13
  import { FormSubmissions } from './views/FormSubmissions.js';
14
14
  import { Users } from './views/Users.js';
15
15
  import { SEO } from './views/SEO.js';
16
+ import { ScriptTags } from './views/ScriptTags.js';
17
+ import { ScriptTagEditor } from './views/ScriptTagEditor.js';
16
18
  import { SetupWizard } from './views/SetupWizard.js';
17
19
  import { Login } from './views/Login.js';
20
+ import { ForgotPassword } from './views/ForgotPassword.js';
21
+ import { ResetPassword } from './views/ResetPassword.js';
18
22
  import { ErrorBoundary } from './components/ErrorBoundary.js';
19
23
  import { ThemeProvider } from './components/ThemeProvider.js';
20
24
  import { LocaleProvider } from './components/LocaleProvider.js';
21
25
  import { useKeyboardShortcuts } from './hooks/useKeyboardShortcuts.js';
26
+ import { PageBuilder } from './views/page-builder/PageBuilder.js';
27
+ import { SavedSections } from './views/page-builder/SavedSections.js';
22
28
 
23
29
  export interface AdminRootProps {
24
30
  config: any;
@@ -63,6 +69,16 @@ function AdminShell({ config, session, basePath = '/admin', initialPath = '/', s
63
69
  }
64
70
 
65
71
  if (!session && !setupRequired) {
72
+ if (matchRoute('/forgot-password')) {
73
+ return <ForgotPassword onNavigate={navigate} />;
74
+ }
75
+
76
+ const resetMatch = matchRoute('/reset-password');
77
+ if (resetMatch) {
78
+ const params = new URLSearchParams(typeof window !== 'undefined' ? window.location.search : '');
79
+ return <ResetPassword onNavigate={navigate} token={params.get('token')} />;
80
+ }
81
+
66
82
  if (onLogin) {
67
83
  return <Login onLogin={onLogin} onNavigate={navigate} captchaConfig={captchaConfig} />;
68
84
  }
@@ -85,6 +101,16 @@ function AdminShell({ config, session, basePath = '/admin', initialPath = '/', s
85
101
  return <Dashboard config={config} session={session} onNavigate={navigate} />;
86
102
  }
87
103
 
104
+ const pageBuilderEdit = matchRoute('/page-builder/:id');
105
+ if (pageBuilderEdit?.id) {
106
+ return <PageBuilder documentId={pageBuilderEdit.id} collectionSlug="pages" config={config} onNavigate={navigate} />;
107
+ }
108
+
109
+ const pageBuilderNew = matchRoute('/page-builder/new');
110
+ if (pageBuilderNew) {
111
+ return <PageBuilder collectionSlug="pages" config={config} onNavigate={navigate} />;
112
+ }
113
+
88
114
  for (const slug of collectionSlugs) {
89
115
  const newMatch = matchRoute(`/${slug}/new`);
90
116
  if (newMatch) {
@@ -146,6 +172,21 @@ function AdminShell({ config, session, basePath = '/admin', initialPath = '/', s
146
172
  return <SEO onNavigate={navigate} initialTab="pages" />;
147
173
  }
148
174
 
175
+ if (matchRoute('/script-tags/new')) {
176
+ return <ScriptTagEditor onNavigate={navigate} />;
177
+ }
178
+ const scriptTagEdit = matchRoute('/script-tags/:id');
179
+ if (scriptTagEdit?.id) {
180
+ return <ScriptTagEditor tagId={scriptTagEdit.id} onNavigate={navigate} />;
181
+ }
182
+ if (matchRoute('/script-tags')) {
183
+ return <ScriptTags onNavigate={navigate} />;
184
+ }
185
+
186
+ if (matchRoute('/saved-sections')) {
187
+ return <SavedSections onNavigate={navigate} config={config} />;
188
+ }
189
+
149
190
  if (matchRoute('/users')) {
150
191
  return <Users onNavigate={navigate} />;
151
192
  }
@@ -12,6 +12,7 @@ const LABEL_MAP: Record<string, string> = {
12
12
  canonicals: 'Canonicalization',
13
13
  links: 'Link Health',
14
14
  users: 'Users',
15
+ 'script-tags': 'Script Tags',
15
16
  settings: 'Settings',
16
17
  collections: 'Collections',
17
18
  submissions: 'Submissions',
@@ -32,13 +32,13 @@ export class ErrorBoundary extends Component<Props, State> {
32
32
  return (
33
33
  <div className="flex min-h-[200px] items-center justify-center p-6">
34
34
  <div className="text-center">
35
- <h2 className="text-lg font-semibold text-gray-900 mb-2">Something went wrong</h2>
36
- <p className="text-sm text-gray-600 mb-4">
35
+ <h2 className="text-lg font-medium text-foreground mb-2">Something went wrong</h2>
36
+ <p className="text-sm text-muted-foreground mb-4">
37
37
  {this.state.error?.message ?? 'An unexpected error occurred'}
38
38
  </p>
39
39
  <button
40
40
  onClick={() => this.setState({ hasError: false, error: null })}
41
- className="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-md hover:bg-blue-700"
41
+ className="px-4 py-2 text-sm font-medium text-primary-foreground bg-primary rounded-md hover:bg-primary/90 transition-colors"
42
42
  >
43
43
  Try Again
44
44
  </button>
@@ -0,0 +1,328 @@
1
+ 'use client';
2
+
3
+ import { useState, useCallback, useRef, useMemo } from 'react';
4
+ import type {
5
+ PageNode,
6
+ BuilderNode,
7
+ BlockNode,
8
+ SectionNode,
9
+ ColumnNode,
10
+ } from '@actuate-media/cms-core';
11
+ import {
12
+ addNode,
13
+ removeNode,
14
+ moveNode,
15
+ updateNodeSettings,
16
+ updateBlockData,
17
+ findNode,
18
+ findParent,
19
+ createEmptyPage,
20
+ createSection,
21
+ createContainer,
22
+ createRow,
23
+ createColumn,
24
+ createBlock,
25
+ hasChildren,
26
+ } from '@actuate-media/cms-core';
27
+
28
+ export type DeviceMode = 'desktop' | 'tablet' | 'mobile';
29
+ export type PanelTab = 'block' | 'node' | 'page' | 'seo' | 'design';
30
+
31
+ export interface PageSettings {
32
+ title: string;
33
+ slug: string;
34
+ template?: string;
35
+ metaTitle?: string;
36
+ metaDescription?: string;
37
+ ogImage?: string;
38
+ focusKeyphrase?: string;
39
+ schemaType?: string;
40
+ }
41
+
42
+ export interface BuilderState {
43
+ tree: PageNode;
44
+ selectedNodeId: string | null;
45
+ selectedNode: BuilderNode | null;
46
+ deviceMode: DeviceMode;
47
+ activeTab: PanelTab;
48
+ pageSettings: PageSettings;
49
+ dirty: boolean;
50
+ canUndo: boolean;
51
+ canRedo: boolean;
52
+ showGridOverlay: boolean;
53
+ }
54
+
55
+ export interface BuilderActions {
56
+ selectNode: (id: string | null) => void;
57
+ setDeviceMode: (mode: DeviceMode) => void;
58
+ setActiveTab: (tab: PanelTab) => void;
59
+ setPageSettings: (settings: Partial<PageSettings>) => void;
60
+ setShowGridOverlay: (show: boolean) => void;
61
+
62
+ addSection: () => void;
63
+ addRowToSection: (sectionId: string) => void;
64
+ addBlockToColumn: (columnId: string, blockType: string, variant?: string) => void;
65
+ addNodeAtId: (parentId: string, node: BuilderNode, index?: number) => void;
66
+ removeNodeById: (id: string) => void;
67
+ moveNodeById: (id: string, newParentId: string, index?: number) => void;
68
+ updateSettings: (id: string, settings: Record<string, unknown>) => void;
69
+ updateBlock: (id: string, data: Record<string, unknown>) => void;
70
+ duplicateNode: (id: string) => void;
71
+ moveNodeUp: (id: string) => void;
72
+ moveNodeDown: (id: string) => void;
73
+
74
+ undo: () => void;
75
+ redo: () => void;
76
+ markClean: () => void;
77
+ replaceTree: (tree: PageNode) => void;
78
+ }
79
+
80
+ const MAX_HISTORY = 50;
81
+
82
+ function deepClone<T>(obj: T): T {
83
+ return JSON.parse(JSON.stringify(obj));
84
+ }
85
+
86
+ function regenerateIds(node: BuilderNode): BuilderNode {
87
+ const cloned = deepClone(node);
88
+ let counter = 0;
89
+ function walk(n: any) {
90
+ n.id = `node_${Date.now().toString(36)}_${(counter++).toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
91
+ if (Array.isArray(n.children)) {
92
+ for (const child of n.children) walk(child);
93
+ }
94
+ }
95
+ walk(cloned);
96
+ return cloned;
97
+ }
98
+
99
+ export function useBuilderState(
100
+ initialTree?: PageNode,
101
+ initialPageSettings?: Partial<PageSettings>,
102
+ ): BuilderState & BuilderActions {
103
+ const [tree, setTree] = useState<PageNode>(() => initialTree ?? createEmptyPage());
104
+ const [selectedNodeId, setSelectedNodeId] = useState<string | null>(null);
105
+ const [deviceMode, setDeviceMode] = useState<DeviceMode>('desktop');
106
+ const [activeTab, setActiveTab] = useState<PanelTab>('page');
107
+ const [showGridOverlay, setShowGridOverlay] = useState(false);
108
+ const [pageSettings, setPageSettingsState] = useState<PageSettings>({
109
+ title: '',
110
+ slug: '',
111
+ ...initialPageSettings,
112
+ });
113
+ const [dirty, setDirty] = useState(false);
114
+
115
+ const undoStack = useRef<PageNode[]>([]);
116
+ const redoStack = useRef<PageNode[]>([]);
117
+
118
+ const pushHistory = useCallback((currentTree: PageNode) => {
119
+ undoStack.current = [...undoStack.current.slice(-MAX_HISTORY + 1), currentTree];
120
+ redoStack.current = [];
121
+ }, []);
122
+
123
+ const [canUndo, setCanUndo] = useState(false);
124
+ const [canRedo, setCanRedo] = useState(false);
125
+
126
+ const applyTreeChange = useCallback((fn: (prev: PageNode) => PageNode) => {
127
+ setTree((prev) => {
128
+ pushHistory(prev);
129
+ const next = fn(prev);
130
+ setDirty(true);
131
+ setCanUndo(true);
132
+ setCanRedo(false);
133
+ return next;
134
+ });
135
+ }, [pushHistory]);
136
+
137
+ const selectedNode = useMemo(() => {
138
+ if (!selectedNodeId) return null;
139
+ return findNode(tree, selectedNodeId) ?? null;
140
+ }, [tree, selectedNodeId]);
141
+
142
+ const selectNode = useCallback((id: string | null) => {
143
+ setSelectedNodeId(id);
144
+ if (id) {
145
+ const node = findNode(tree, id);
146
+ if (node) {
147
+ if (node.type === 'block') setActiveTab('block');
148
+ else if (node.type === 'page') setActiveTab('page');
149
+ else setActiveTab('node');
150
+ }
151
+ }
152
+ }, [tree]);
153
+
154
+ const setPageSettings = useCallback((settings: Partial<PageSettings>) => {
155
+ setPageSettingsState((prev) => ({ ...prev, ...settings }));
156
+ setDirty(true);
157
+ }, []);
158
+
159
+ const addSection = useCallback(() => {
160
+ applyTreeChange((prev) => {
161
+ const section = createSection({ paddingTop: '64px', paddingBottom: '64px' });
162
+ const container = createContainer();
163
+ const col = createColumn(12);
164
+ const row = createRow([col]);
165
+ (container as any).children = [row];
166
+ (section as any).children = [container];
167
+ return addNode(prev, prev.id, section as BuilderNode);
168
+ });
169
+ }, [applyTreeChange]);
170
+
171
+ const addRowToSection = useCallback((sectionId: string) => {
172
+ applyTreeChange((prev) => {
173
+ const section = findNode(prev, sectionId);
174
+ if (!section || !hasChildren(section)) return prev;
175
+ const containers = (section as SectionNode).children.filter((c) => c.type === 'container');
176
+ const targetId = containers.length > 0 ? containers[containers.length - 1]!.id : sectionId;
177
+ const col = createColumn(12);
178
+ const row = createRow([col]);
179
+ return addNode(prev, targetId, row as BuilderNode);
180
+ });
181
+ }, [applyTreeChange]);
182
+
183
+ const addBlockToColumn = useCallback((columnId: string, blockType: string, variant?: string) => {
184
+ applyTreeChange((prev) => {
185
+ const block = createBlock(blockType, variant);
186
+ return addNode(prev, columnId, block as BuilderNode);
187
+ });
188
+ }, [applyTreeChange]);
189
+
190
+ const addNodeAtId = useCallback((parentId: string, node: BuilderNode, index?: number) => {
191
+ applyTreeChange((prev) => addNode(prev, parentId, node, index));
192
+ }, [applyTreeChange]);
193
+
194
+ const removeNodeById = useCallback((id: string) => {
195
+ applyTreeChange((prev) => {
196
+ const result = removeNode(prev, id);
197
+ if (selectedNodeId === id) setSelectedNodeId(null);
198
+ return result;
199
+ });
200
+ }, [applyTreeChange, selectedNodeId]);
201
+
202
+ const moveNodeById = useCallback((id: string, newParentId: string, index?: number) => {
203
+ applyTreeChange((prev) => moveNode(prev, id, newParentId, index));
204
+ }, [applyTreeChange]);
205
+
206
+ const updateSettings = useCallback((id: string, settings: Record<string, unknown>) => {
207
+ applyTreeChange((prev) => updateNodeSettings(prev, id, settings));
208
+ }, [applyTreeChange]);
209
+
210
+ const updateBlock = useCallback((id: string, data: Record<string, unknown>) => {
211
+ applyTreeChange((prev) => updateBlockData(prev, id, data));
212
+ }, [applyTreeChange]);
213
+
214
+ const duplicateNode = useCallback((id: string) => {
215
+ applyTreeChange((prev) => {
216
+ const node = findNode(prev, id);
217
+ if (!node) return prev;
218
+ const parent = findParent(prev, id);
219
+ if (!parent) return prev;
220
+ const siblings = (parent as any).children as BuilderNode[];
221
+ const idx = siblings.findIndex((c) => c.id === id);
222
+ const cloned = regenerateIds(node);
223
+ return addNode(prev, parent.id, cloned, idx + 1);
224
+ });
225
+ }, [applyTreeChange]);
226
+
227
+ const moveNodeUp = useCallback((id: string) => {
228
+ applyTreeChange((prev) => {
229
+ const parent = findParent(prev, id);
230
+ if (!parent) return prev;
231
+ const siblings = (parent as any).children as BuilderNode[];
232
+ const idx = siblings.findIndex((c) => c.id === id);
233
+ if (idx <= 0) return prev;
234
+ const withoutNode = removeNode(prev, id);
235
+ const node = findNode(prev, id);
236
+ if (!node) return prev;
237
+ return addNode(withoutNode, parent.id, deepClone(node), idx - 1);
238
+ });
239
+ }, [applyTreeChange]);
240
+
241
+ const moveNodeDown = useCallback((id: string) => {
242
+ applyTreeChange((prev) => {
243
+ const parent = findParent(prev, id);
244
+ if (!parent) return prev;
245
+ const siblings = (parent as any).children as BuilderNode[];
246
+ const idx = siblings.findIndex((c) => c.id === id);
247
+ if (idx >= siblings.length - 1) return prev;
248
+ const withoutNode = removeNode(prev, id);
249
+ const node = findNode(prev, id);
250
+ if (!node) return prev;
251
+ return addNode(withoutNode, parent.id, deepClone(node), idx + 1);
252
+ });
253
+ }, [applyTreeChange]);
254
+
255
+ const undo = useCallback(() => {
256
+ if (undoStack.current.length === 0) return;
257
+ const prev = undoStack.current[undoStack.current.length - 1]!;
258
+ undoStack.current = undoStack.current.slice(0, -1);
259
+ setTree((current) => {
260
+ redoStack.current = [...redoStack.current, current];
261
+ setCanUndo(undoStack.current.length > 0);
262
+ setCanRedo(true);
263
+ return prev;
264
+ });
265
+ }, []);
266
+
267
+ const redo = useCallback(() => {
268
+ if (redoStack.current.length === 0) return;
269
+ const next = redoStack.current[redoStack.current.length - 1]!;
270
+ redoStack.current = redoStack.current.slice(0, -1);
271
+ setTree((current) => {
272
+ undoStack.current = [...undoStack.current, current];
273
+ setCanRedo(redoStack.current.length > 0);
274
+ setCanUndo(true);
275
+ return next;
276
+ });
277
+ }, []);
278
+
279
+ const markClean = useCallback(() => {
280
+ setDirty(false);
281
+ }, []);
282
+
283
+ const replaceTree = useCallback((newTree: PageNode) => {
284
+ setTree((prev) => {
285
+ pushHistory(prev);
286
+ setDirty(true);
287
+ setCanUndo(true);
288
+ setCanRedo(false);
289
+ return newTree;
290
+ });
291
+ }, [pushHistory]);
292
+
293
+ return {
294
+ tree,
295
+ selectedNodeId,
296
+ selectedNode,
297
+ deviceMode,
298
+ activeTab,
299
+ pageSettings,
300
+ dirty,
301
+ canUndo,
302
+ canRedo,
303
+ showGridOverlay,
304
+
305
+ selectNode,
306
+ setDeviceMode,
307
+ setActiveTab,
308
+ setPageSettings,
309
+ setShowGridOverlay,
310
+
311
+ addSection,
312
+ addRowToSection,
313
+ addBlockToColumn,
314
+ addNodeAtId,
315
+ removeNodeById,
316
+ moveNodeById,
317
+ updateSettings,
318
+ updateBlock,
319
+ duplicateNode,
320
+ moveNodeUp,
321
+ moveNodeDown,
322
+
323
+ undo,
324
+ redo,
325
+ markClean,
326
+ replaceTree,
327
+ };
328
+ }
package/src/index.ts CHANGED
@@ -26,9 +26,17 @@ export { SetupWizard } from './views/SetupWizard.js';
26
26
  export type { SetupWizardProps } from './views/SetupWizard.js';
27
27
  export { Login } from './views/Login.js';
28
28
  export type { LoginProps, CaptchaConfig } from './views/Login.js';
29
+ export { ForgotPassword } from './views/ForgotPassword.js';
30
+ export type { ForgotPasswordProps } from './views/ForgotPassword.js';
31
+ export { ResetPassword } from './views/ResetPassword.js';
32
+ export type { ResetPasswordProps } from './views/ResetPassword.js';
29
33
  export { CollectionList } from './views/CollectionList.js';
30
34
  export { DocumentEdit } from './views/DocumentEdit.js';
31
35
 
36
+ export { PageBuilder } from './views/page-builder/PageBuilder.js';
37
+ export { useBuilderState } from './hooks/useBuilderState.js';
38
+ export type { BuilderState, BuilderActions, DeviceMode, PanelTab, PageSettings as BuilderPageSettings } from './hooks/useBuilderState.js';
39
+
32
40
  export { Breadcrumbs } from './components/Breadcrumbs.js';
33
41
  export { CommandPalette } from './components/CommandPalette.js';
34
42
  export { ErrorBoundary } from './components/ErrorBoundary.js';
@@ -19,6 +19,9 @@ import {
19
19
  PanelTop,
20
20
  PanelBottom,
21
21
  Layers,
22
+ Code2,
23
+ LayoutTemplate,
24
+ Library,
22
25
  } from 'lucide-react';
23
26
  import type { LucideIcon } from 'lucide-react';
24
27
 
@@ -253,9 +256,13 @@ function buildNavItems(config: any): NavItem[] {
253
256
  }
254
257
 
255
258
  items.push(
259
+ { path: '/page-builder/new', label: 'Page Builder', icon: LayoutTemplate, group: 'Pages' },
260
+ { path: '/saved-sections', label: 'Saved Sections', icon: Library, group: 'Pages' },
261
+ { path: '/page-templates', label: 'Templates', icon: Layers, group: 'Pages' },
256
262
  { path: '/media', label: 'Media', icon: Image },
257
263
  { path: '/forms', label: 'Forms', icon: ClipboardList },
258
264
  { path: '/seo', label: 'SEO', icon: SearchIcon },
265
+ { path: '/script-tags', label: 'Script Tags', icon: Code2 },
259
266
  { path: '/users', label: 'Users', icon: Users },
260
267
  { path: '/settings', label: 'Settings', icon: Settings },
261
268
  );
@@ -0,0 +1,136 @@
1
+ 'use client';
2
+
3
+ import { useState, type FormEvent } from 'react';
4
+ import { Shield, ArrowLeft, Loader2, CheckCircle2, AlertTriangle } from 'lucide-react';
5
+ import { cmsApi } from '../lib/api.js';
6
+
7
+ export interface ForgotPasswordProps {
8
+ onNavigate: (path: string) => void;
9
+ }
10
+
11
+ export function ForgotPassword({ onNavigate }: ForgotPasswordProps) {
12
+ const [email, setEmail] = useState('');
13
+ const [submitting, setSubmitting] = useState(false);
14
+ const [sent, setSent] = useState(false);
15
+ const [error, setError] = useState('');
16
+
17
+ const canSubmit = email.trim() && !submitting;
18
+
19
+ const handleSubmit = async (e: FormEvent) => {
20
+ e.preventDefault();
21
+ if (!canSubmit) return;
22
+
23
+ setError('');
24
+ setSubmitting(true);
25
+
26
+ try {
27
+ const result = await cmsApi('/auth/forgot-password', {
28
+ method: 'POST',
29
+ body: JSON.stringify({ email: email.trim() }),
30
+ });
31
+
32
+ if (result.error && result.status === 429) {
33
+ setError('Too many requests. Please try again later.');
34
+ } else {
35
+ setSent(true);
36
+ }
37
+ } catch {
38
+ setError('An unexpected error occurred. Please try again.');
39
+ } finally {
40
+ setSubmitting(false);
41
+ }
42
+ };
43
+
44
+ return (
45
+ <div className="min-h-screen flex items-center justify-center bg-gray-50 px-4">
46
+ <div className="w-full max-w-md">
47
+ <div className="text-center mb-8">
48
+ <div className="mx-auto mb-4 w-14 h-14 bg-blue-600 rounded-xl flex items-center justify-center">
49
+ <Shield className="w-7 h-7 text-white" />
50
+ </div>
51
+ <h1 className="text-2xl font-bold text-gray-900">Reset Password</h1>
52
+ <p className="text-gray-600 mt-2">
53
+ {sent
54
+ ? 'Check your inbox for a reset link'
55
+ : "Enter your email and we'll send you a reset link"}
56
+ </p>
57
+ </div>
58
+
59
+ <div className="bg-white rounded-xl border border-gray-200 p-6 shadow-sm space-y-5">
60
+ {sent ? (
61
+ <div className="text-center space-y-4">
62
+ <div className="mx-auto w-12 h-12 bg-green-100 rounded-full flex items-center justify-center">
63
+ <CheckCircle2 className="w-6 h-6 text-green-600" />
64
+ </div>
65
+ <p className="text-sm text-gray-600">
66
+ If an account exists for <strong>{email}</strong>, you will receive a password reset email shortly.
67
+ </p>
68
+ <p className="text-xs text-gray-500">
69
+ The link expires in 1 hour. Check your spam folder if you don't see it.
70
+ </p>
71
+ <button
72
+ type="button"
73
+ onClick={() => onNavigate('/login')}
74
+ className="w-full py-2.5 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 transition-colors"
75
+ >
76
+ Back to Sign In
77
+ </button>
78
+ </div>
79
+ ) : (
80
+ <form onSubmit={handleSubmit} className="space-y-5">
81
+ {error && (
82
+ <div className="flex items-start gap-3 p-3 bg-red-50 border border-red-200 rounded-lg">
83
+ <AlertTriangle className="w-5 h-5 text-red-600 mt-0.5 shrink-0" />
84
+ <p className="text-sm text-red-800">{error}</p>
85
+ </div>
86
+ )}
87
+
88
+ <div>
89
+ <label htmlFor="forgot-email" className="block text-sm font-medium text-gray-700 mb-1.5">
90
+ Email Address
91
+ </label>
92
+ <input
93
+ id="forgot-email"
94
+ type="email"
95
+ value={email}
96
+ onChange={(e) => setEmail(e.target.value)}
97
+ placeholder="admin@example.com"
98
+ className="w-full px-3 py-2.5 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
99
+ required
100
+ autoFocus
101
+ autoComplete="email"
102
+ />
103
+ </div>
104
+
105
+ <button
106
+ type="submit"
107
+ disabled={!canSubmit}
108
+ className="w-full py-2.5 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
109
+ >
110
+ {submitting ? (
111
+ <>
112
+ <Loader2 className="w-4 h-4 animate-spin" />
113
+ Sending...
114
+ </>
115
+ ) : (
116
+ 'Send Reset Link'
117
+ )}
118
+ </button>
119
+ </form>
120
+ )}
121
+
122
+ {!sent && (
123
+ <button
124
+ type="button"
125
+ onClick={() => onNavigate('/login')}
126
+ className="w-full flex items-center justify-center gap-2 text-sm text-gray-600 hover:text-gray-800 transition-colors"
127
+ >
128
+ <ArrowLeft className="w-4 h-4" />
129
+ Back to Sign In
130
+ </button>
131
+ )}
132
+ </div>
133
+ </div>
134
+ </div>
135
+ );
136
+ }