@actuate-media/cms-admin 0.15.0 → 0.16.1

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 (308) hide show
  1. package/dist/AdminRoot.d.ts.map +1 -1
  2. package/dist/AdminRoot.js +148 -2
  3. package/dist/AdminRoot.js.map +1 -1
  4. package/dist/__tests__/components/posts-badges.test.d.ts +2 -0
  5. package/dist/__tests__/components/posts-badges.test.d.ts.map +1 -0
  6. package/dist/__tests__/components/posts-badges.test.js +68 -0
  7. package/dist/__tests__/components/posts-badges.test.js.map +1 -0
  8. package/dist/__tests__/layout/sidebar-posts.test.d.ts +2 -0
  9. package/dist/__tests__/layout/sidebar-posts.test.d.ts.map +1 -0
  10. package/dist/__tests__/layout/sidebar-posts.test.js +162 -0
  11. package/dist/__tests__/layout/sidebar-posts.test.js.map +1 -0
  12. package/dist/__tests__/lib/page-editor-service.test.d.ts +2 -0
  13. package/dist/__tests__/lib/page-editor-service.test.d.ts.map +1 -0
  14. package/dist/__tests__/lib/page-editor-service.test.js +137 -0
  15. package/dist/__tests__/lib/page-editor-service.test.js.map +1 -0
  16. package/dist/__tests__/lib/pages-service.test.d.ts +2 -0
  17. package/dist/__tests__/lib/pages-service.test.d.ts.map +1 -0
  18. package/dist/__tests__/lib/pages-service.test.js +140 -0
  19. package/dist/__tests__/lib/pages-service.test.js.map +1 -0
  20. package/dist/__tests__/lib/post-editor-service.test.d.ts +2 -0
  21. package/dist/__tests__/lib/post-editor-service.test.d.ts.map +1 -0
  22. package/dist/__tests__/lib/post-editor-service.test.js +185 -0
  23. package/dist/__tests__/lib/post-editor-service.test.js.map +1 -0
  24. package/dist/__tests__/lib/posts-service.test.d.ts +2 -0
  25. package/dist/__tests__/lib/posts-service.test.d.ts.map +1 -0
  26. package/dist/__tests__/lib/posts-service.test.js +244 -0
  27. package/dist/__tests__/lib/posts-service.test.js.map +1 -0
  28. package/dist/__tests__/lib/useApiData.test.d.ts +2 -0
  29. package/dist/__tests__/lib/useApiData.test.d.ts.map +1 -0
  30. package/dist/__tests__/lib/useApiData.test.js +84 -0
  31. package/dist/__tests__/lib/useApiData.test.js.map +1 -0
  32. package/dist/__tests__/views/dashboard.test.d.ts +2 -0
  33. package/dist/__tests__/views/dashboard.test.d.ts.map +1 -0
  34. package/dist/__tests__/views/dashboard.test.js +138 -0
  35. package/dist/__tests__/views/dashboard.test.js.map +1 -0
  36. package/dist/__tests__/views/posts-list-view.test.d.ts +2 -0
  37. package/dist/__tests__/views/posts-list-view.test.d.ts.map +1 -0
  38. package/dist/__tests__/views/posts-list-view.test.js +98 -0
  39. package/dist/__tests__/views/posts-list-view.test.js.map +1 -0
  40. package/dist/actuate-admin.css +1 -1
  41. package/dist/components/pages/tag-badges.d.ts +21 -0
  42. package/dist/components/pages/tag-badges.d.ts.map +1 -0
  43. package/dist/components/pages/tag-badges.js +56 -0
  44. package/dist/components/pages/tag-badges.js.map +1 -0
  45. package/dist/components/posts/badges.d.ts +25 -0
  46. package/dist/components/posts/badges.d.ts.map +1 -0
  47. package/dist/components/posts/badges.js +123 -0
  48. package/dist/components/posts/badges.js.map +1 -0
  49. package/dist/components/posts/icons.d.ts +12 -0
  50. package/dist/components/posts/icons.d.ts.map +1 -0
  51. package/dist/components/posts/icons.js +26 -0
  52. package/dist/components/posts/icons.js.map +1 -0
  53. package/dist/layout/Layout.js +36 -8
  54. package/dist/layout/Layout.js.map +1 -1
  55. package/dist/layout/Sidebar.d.ts +9 -0
  56. package/dist/layout/Sidebar.d.ts.map +1 -1
  57. package/dist/layout/Sidebar.js +100 -48
  58. package/dist/layout/Sidebar.js.map +1 -1
  59. package/dist/lib/page-editor-service.d.ts +45 -0
  60. package/dist/lib/page-editor-service.d.ts.map +1 -0
  61. package/dist/lib/page-editor-service.js +153 -0
  62. package/dist/lib/page-editor-service.js.map +1 -0
  63. package/dist/lib/pages-service.d.ts +143 -0
  64. package/dist/lib/pages-service.d.ts.map +1 -0
  65. package/dist/lib/pages-service.js +444 -0
  66. package/dist/lib/pages-service.js.map +1 -0
  67. package/dist/lib/post-editor-service.d.ts +57 -0
  68. package/dist/lib/post-editor-service.d.ts.map +1 -0
  69. package/dist/lib/post-editor-service.js +234 -0
  70. package/dist/lib/post-editor-service.js.map +1 -0
  71. package/dist/lib/posts-service.d.ts +73 -0
  72. package/dist/lib/posts-service.d.ts.map +1 -0
  73. package/dist/lib/posts-service.js +245 -0
  74. package/dist/lib/posts-service.js.map +1 -0
  75. package/dist/lib/preview-link.d.ts +32 -0
  76. package/dist/lib/preview-link.d.ts.map +1 -0
  77. package/dist/lib/preview-link.js +40 -0
  78. package/dist/lib/preview-link.js.map +1 -0
  79. package/dist/router/index.d.ts +1 -0
  80. package/dist/router/index.d.ts.map +1 -1
  81. package/dist/router/index.js +9 -1
  82. package/dist/router/index.js.map +1 -1
  83. package/dist/views/Dashboard.d.ts.map +1 -1
  84. package/dist/views/Dashboard.js +64 -20
  85. package/dist/views/Dashboard.js.map +1 -1
  86. package/dist/views/Pages/PageTagManager.d.ts +14 -0
  87. package/dist/views/Pages/PageTagManager.d.ts.map +1 -0
  88. package/dist/views/Pages/PageTagManager.js +145 -0
  89. package/dist/views/Pages/PageTagManager.js.map +1 -0
  90. package/dist/views/Pages/PagesListView.d.ts +18 -0
  91. package/dist/views/Pages/PagesListView.d.ts.map +1 -0
  92. package/dist/views/Pages/PagesListView.js +287 -0
  93. package/dist/views/Pages/PagesListView.js.map +1 -0
  94. package/dist/views/Posts/NewPostPicker.d.ts +16 -0
  95. package/dist/views/Posts/NewPostPicker.d.ts.map +1 -0
  96. package/dist/views/Posts/NewPostPicker.js +59 -0
  97. package/dist/views/Posts/NewPostPicker.js.map +1 -0
  98. package/dist/views/Posts/NewPostTypeInstructions.d.ts +18 -0
  99. package/dist/views/Posts/NewPostTypeInstructions.d.ts.map +1 -0
  100. package/dist/views/Posts/NewPostTypeInstructions.js +147 -0
  101. package/dist/views/Posts/NewPostTypeInstructions.js.map +1 -0
  102. package/dist/views/Posts/PostTypesView.d.ts +26 -0
  103. package/dist/views/Posts/PostTypesView.d.ts.map +1 -0
  104. package/dist/views/Posts/PostTypesView.js +52 -0
  105. package/dist/views/Posts/PostTypesView.js.map +1 -0
  106. package/dist/views/Posts/PostsListView.d.ts +25 -0
  107. package/dist/views/Posts/PostsListView.d.ts.map +1 -0
  108. package/dist/views/Posts/PostsListView.js +239 -0
  109. package/dist/views/Posts/PostsListView.js.map +1 -0
  110. package/dist/views/Posts/post-type-presets.d.ts +32 -0
  111. package/dist/views/Posts/post-type-presets.d.ts.map +1 -0
  112. package/dist/views/Posts/post-type-presets.js +72 -0
  113. package/dist/views/Posts/post-type-presets.js.map +1 -0
  114. package/dist/views/page-builder/Widgets.d.ts +13 -0
  115. package/dist/views/page-builder/Widgets.d.ts.map +1 -0
  116. package/dist/views/page-builder/Widgets.js +15 -0
  117. package/dist/views/page-builder/Widgets.js.map +1 -0
  118. package/dist/views/page-editor/AddSectionModal.d.ts +20 -0
  119. package/dist/views/page-editor/AddSectionModal.d.ts.map +1 -0
  120. package/dist/views/page-editor/AddSectionModal.js +22 -0
  121. package/dist/views/page-editor/AddSectionModal.js.map +1 -0
  122. package/dist/views/page-editor/EditorCanvas.d.ts +18 -0
  123. package/dist/views/page-editor/EditorCanvas.d.ts.map +1 -0
  124. package/dist/views/page-editor/EditorCanvas.js +64 -0
  125. package/dist/views/page-editor/EditorCanvas.js.map +1 -0
  126. package/dist/views/page-editor/EditorTopBar.d.ts +30 -0
  127. package/dist/views/page-editor/EditorTopBar.d.ts.map +1 -0
  128. package/dist/views/page-editor/EditorTopBar.js +42 -0
  129. package/dist/views/page-editor/EditorTopBar.js.map +1 -0
  130. package/dist/views/page-editor/PagePreview.d.ts +13 -0
  131. package/dist/views/page-editor/PagePreview.d.ts.map +1 -0
  132. package/dist/views/page-editor/PagePreview.js +37 -0
  133. package/dist/views/page-editor/PagePreview.js.map +1 -0
  134. package/dist/views/page-editor/PageSectionEditor.d.ts +16 -0
  135. package/dist/views/page-editor/PageSectionEditor.d.ts.map +1 -0
  136. package/dist/views/page-editor/PageSectionEditor.js +272 -0
  137. package/dist/views/page-editor/PageSectionEditor.js.map +1 -0
  138. package/dist/views/page-editor/PageSettingsModal.d.ts +12 -0
  139. package/dist/views/page-editor/PageSettingsModal.d.ts.map +1 -0
  140. package/dist/views/page-editor/PageSettingsModal.js +52 -0
  141. package/dist/views/page-editor/PageSettingsModal.js.map +1 -0
  142. package/dist/views/page-editor/SectionInspector.d.ts +17 -0
  143. package/dist/views/page-editor/SectionInspector.d.ts.map +1 -0
  144. package/dist/views/page-editor/SectionInspector.js +98 -0
  145. package/dist/views/page-editor/SectionInspector.js.map +1 -0
  146. package/dist/views/page-editor/SectionsPanel.d.ts +15 -0
  147. package/dist/views/page-editor/SectionsPanel.d.ts.map +1 -0
  148. package/dist/views/page-editor/SectionsPanel.js +43 -0
  149. package/dist/views/page-editor/SectionsPanel.js.map +1 -0
  150. package/dist/views/page-editor/ValidationSummary.d.ts +9 -0
  151. package/dist/views/page-editor/ValidationSummary.d.ts.map +1 -0
  152. package/dist/views/page-editor/ValidationSummary.js +10 -0
  153. package/dist/views/page-editor/ValidationSummary.js.map +1 -0
  154. package/dist/views/page-editor/section-icons.d.ts +4 -0
  155. package/dist/views/page-editor/section-icons.d.ts.map +1 -0
  156. package/dist/views/page-editor/section-icons.js +20 -0
  157. package/dist/views/page-editor/section-icons.js.map +1 -0
  158. package/dist/views/page-editor/section-types.d.ts +11 -0
  159. package/dist/views/page-editor/section-types.d.ts.map +1 -0
  160. package/dist/views/page-editor/section-types.js +10 -0
  161. package/dist/views/page-editor/section-types.js.map +1 -0
  162. package/dist/views/page-editor/sections/ArticleBodySection.d.ts +13 -0
  163. package/dist/views/page-editor/sections/ArticleBodySection.d.ts.map +1 -0
  164. package/dist/views/page-editor/sections/ArticleBodySection.js +23 -0
  165. package/dist/views/page-editor/sections/ArticleBodySection.js.map +1 -0
  166. package/dist/views/page-editor/sections/AuthorBioSection.d.ts +8 -0
  167. package/dist/views/page-editor/sections/AuthorBioSection.d.ts.map +1 -0
  168. package/dist/views/page-editor/sections/AuthorBioSection.js +21 -0
  169. package/dist/views/page-editor/sections/AuthorBioSection.js.map +1 -0
  170. package/dist/views/page-editor/sections/ByTheNumbersSection.d.ts +5 -0
  171. package/dist/views/page-editor/sections/ByTheNumbersSection.d.ts.map +1 -0
  172. package/dist/views/page-editor/sections/ByTheNumbersSection.js +10 -0
  173. package/dist/views/page-editor/sections/ByTheNumbersSection.js.map +1 -0
  174. package/dist/views/page-editor/sections/CtaBannerSection.d.ts +5 -0
  175. package/dist/views/page-editor/sections/CtaBannerSection.d.ts.map +1 -0
  176. package/dist/views/page-editor/sections/CtaBannerSection.js +10 -0
  177. package/dist/views/page-editor/sections/CtaBannerSection.js.map +1 -0
  178. package/dist/views/page-editor/sections/FeatureHighlightSection.d.ts +5 -0
  179. package/dist/views/page-editor/sections/FeatureHighlightSection.d.ts.map +1 -0
  180. package/dist/views/page-editor/sections/FeatureHighlightSection.js +18 -0
  181. package/dist/views/page-editor/sections/FeatureHighlightSection.js.map +1 -0
  182. package/dist/views/page-editor/sections/GallerySection.d.ts +6 -0
  183. package/dist/views/page-editor/sections/GallerySection.d.ts.map +1 -0
  184. package/dist/views/page-editor/sections/GallerySection.js +17 -0
  185. package/dist/views/page-editor/sections/GallerySection.js.map +1 -0
  186. package/dist/views/page-editor/sections/HeroBannerSection.d.ts +5 -0
  187. package/dist/views/page-editor/sections/HeroBannerSection.d.ts.map +1 -0
  188. package/dist/views/page-editor/sections/HeroBannerSection.js +11 -0
  189. package/dist/views/page-editor/sections/HeroBannerSection.js.map +1 -0
  190. package/dist/views/page-editor/sections/MissionSection.d.ts +6 -0
  191. package/dist/views/page-editor/sections/MissionSection.d.ts.map +1 -0
  192. package/dist/views/page-editor/sections/MissionSection.js +12 -0
  193. package/dist/views/page-editor/sections/MissionSection.js.map +1 -0
  194. package/dist/views/page-editor/sections/PullQuoteSection.d.ts +6 -0
  195. package/dist/views/page-editor/sections/PullQuoteSection.d.ts.map +1 -0
  196. package/dist/views/page-editor/sections/PullQuoteSection.js +13 -0
  197. package/dist/views/page-editor/sections/PullQuoteSection.js.map +1 -0
  198. package/dist/views/page-editor/sections/RelatedPostsSection.d.ts +12 -0
  199. package/dist/views/page-editor/sections/RelatedPostsSection.d.ts.map +1 -0
  200. package/dist/views/page-editor/sections/RelatedPostsSection.js +23 -0
  201. package/dist/views/page-editor/sections/RelatedPostsSection.js.map +1 -0
  202. package/dist/views/page-editor/sections/SectionRenderer.d.ts +22 -0
  203. package/dist/views/page-editor/sections/SectionRenderer.d.ts.map +1 -0
  204. package/dist/views/page-editor/sections/SectionRenderer.js +52 -0
  205. package/dist/views/page-editor/sections/SectionRenderer.js.map +1 -0
  206. package/dist/views/page-editor/sections/index.d.ts +14 -0
  207. package/dist/views/page-editor/sections/index.d.ts.map +1 -0
  208. package/dist/views/page-editor/sections/index.js +12 -0
  209. package/dist/views/page-editor/sections/index.js.map +1 -0
  210. package/dist/views/page-editor/sections/parts.d.ts +55 -0
  211. package/dist/views/page-editor/sections/parts.d.ts.map +1 -0
  212. package/dist/views/page-editor/sections/parts.js +60 -0
  213. package/dist/views/page-editor/sections/parts.js.map +1 -0
  214. package/dist/views/page-editor/viewports.d.ts +14 -0
  215. package/dist/views/page-editor/viewports.d.ts.map +1 -0
  216. package/dist/views/page-editor/viewports.js +12 -0
  217. package/dist/views/page-editor/viewports.js.map +1 -0
  218. package/dist/views/post-editor/PostEditorCanvas.d.ts +21 -0
  219. package/dist/views/post-editor/PostEditorCanvas.d.ts.map +1 -0
  220. package/dist/views/post-editor/PostEditorCanvas.js +58 -0
  221. package/dist/views/post-editor/PostEditorCanvas.js.map +1 -0
  222. package/dist/views/post-editor/PostFieldsModal.d.ts +12 -0
  223. package/dist/views/post-editor/PostFieldsModal.d.ts.map +1 -0
  224. package/dist/views/post-editor/PostFieldsModal.js +53 -0
  225. package/dist/views/post-editor/PostFieldsModal.js.map +1 -0
  226. package/dist/views/post-editor/PostHeader.d.ts +15 -0
  227. package/dist/views/post-editor/PostHeader.d.ts.map +1 -0
  228. package/dist/views/post-editor/PostHeader.js +27 -0
  229. package/dist/views/post-editor/PostHeader.js.map +1 -0
  230. package/dist/views/post-editor/PostHeaderPanel.d.ts +11 -0
  231. package/dist/views/post-editor/PostHeaderPanel.d.ts.map +1 -0
  232. package/dist/views/post-editor/PostHeaderPanel.js +21 -0
  233. package/dist/views/post-editor/PostHeaderPanel.js.map +1 -0
  234. package/dist/views/post-editor/PostPreview.d.ts +14 -0
  235. package/dist/views/post-editor/PostPreview.d.ts.map +1 -0
  236. package/dist/views/post-editor/PostPreview.js +53 -0
  237. package/dist/views/post-editor/PostPreview.js.map +1 -0
  238. package/dist/views/post-editor/PostSectionEditor.d.ts +17 -0
  239. package/dist/views/post-editor/PostSectionEditor.d.ts.map +1 -0
  240. package/dist/views/post-editor/PostSectionEditor.js +290 -0
  241. package/dist/views/post-editor/PostSectionEditor.js.map +1 -0
  242. package/dist/views/post-editor/PostTemplateEditor.d.ts +9 -0
  243. package/dist/views/post-editor/PostTemplateEditor.d.ts.map +1 -0
  244. package/dist/views/post-editor/PostTemplateEditor.js +144 -0
  245. package/dist/views/post-editor/PostTemplateEditor.js.map +1 -0
  246. package/package.json +2 -2
  247. package/src/AdminRoot.tsx +214 -2
  248. package/src/__tests__/components/posts-badges.test.tsx +77 -0
  249. package/src/__tests__/layout/sidebar-posts.test.ts +179 -0
  250. package/src/__tests__/lib/page-editor-service.test.ts +173 -0
  251. package/src/__tests__/lib/pages-service.test.ts +157 -0
  252. package/src/__tests__/lib/post-editor-service.test.ts +215 -0
  253. package/src/__tests__/lib/posts-service.test.ts +279 -0
  254. package/src/__tests__/lib/useApiData.test.ts +105 -0
  255. package/src/__tests__/views/dashboard.test.tsx +180 -0
  256. package/src/__tests__/views/posts-list-view.test.ts +110 -0
  257. package/src/components/pages/tag-badges.tsx +104 -0
  258. package/src/components/posts/badges.tsx +170 -0
  259. package/src/components/posts/icons.ts +40 -0
  260. package/src/layout/Layout.tsx +66 -10
  261. package/src/layout/Sidebar.tsx +116 -50
  262. package/src/lib/page-editor-service.ts +233 -0
  263. package/src/lib/pages-service.ts +599 -0
  264. package/src/lib/post-editor-service.ts +342 -0
  265. package/src/lib/posts-service.ts +366 -0
  266. package/src/lib/preview-link.ts +63 -0
  267. package/src/router/index.ts +10 -1
  268. package/src/views/Dashboard.tsx +142 -89
  269. package/src/views/Pages/PageTagManager.tsx +436 -0
  270. package/src/views/Pages/PagesListView.tsx +944 -0
  271. package/src/views/Posts/NewPostPicker.tsx +147 -0
  272. package/src/views/Posts/NewPostTypeInstructions.tsx +344 -0
  273. package/src/views/Posts/PostTypesView.tsx +194 -0
  274. package/src/views/Posts/PostsListView.tsx +869 -0
  275. package/src/views/Posts/post-type-presets.ts +105 -0
  276. package/src/views/page-builder/Widgets.tsx +44 -0
  277. package/src/views/page-editor/AddSectionModal.tsx +56 -0
  278. package/src/views/page-editor/EditorCanvas.tsx +127 -0
  279. package/src/views/page-editor/EditorTopBar.tsx +233 -0
  280. package/src/views/page-editor/PagePreview.tsx +96 -0
  281. package/src/views/page-editor/PageSectionEditor.tsx +476 -0
  282. package/src/views/page-editor/PageSettingsModal.tsx +154 -0
  283. package/src/views/page-editor/SectionInspector.tsx +599 -0
  284. package/src/views/page-editor/SectionsPanel.tsx +270 -0
  285. package/src/views/page-editor/ValidationSummary.tsx +53 -0
  286. package/src/views/page-editor/section-icons.ts +33 -0
  287. package/src/views/page-editor/section-types.ts +27 -0
  288. package/src/views/page-editor/sections/ArticleBodySection.tsx +57 -0
  289. package/src/views/page-editor/sections/AuthorBioSection.tsx +61 -0
  290. package/src/views/page-editor/sections/ByTheNumbersSection.tsx +41 -0
  291. package/src/views/page-editor/sections/CtaBannerSection.tsx +29 -0
  292. package/src/views/page-editor/sections/FeatureHighlightSection.tsx +63 -0
  293. package/src/views/page-editor/sections/GallerySection.tsx +54 -0
  294. package/src/views/page-editor/sections/HeroBannerSection.tsx +44 -0
  295. package/src/views/page-editor/sections/MissionSection.tsx +22 -0
  296. package/src/views/page-editor/sections/PullQuoteSection.tsx +27 -0
  297. package/src/views/page-editor/sections/RelatedPostsSection.tsx +55 -0
  298. package/src/views/page-editor/sections/SectionRenderer.tsx +102 -0
  299. package/src/views/page-editor/sections/index.ts +13 -0
  300. package/src/views/page-editor/sections/parts.tsx +113 -0
  301. package/src/views/page-editor/viewports.ts +25 -0
  302. package/src/views/post-editor/PostEditorCanvas.tsx +123 -0
  303. package/src/views/post-editor/PostFieldsModal.tsx +196 -0
  304. package/src/views/post-editor/PostHeader.tsx +87 -0
  305. package/src/views/post-editor/PostHeaderPanel.tsx +107 -0
  306. package/src/views/post-editor/PostPreview.tsx +114 -0
  307. package/src/views/post-editor/PostSectionEditor.tsx +506 -0
  308. package/src/views/post-editor/PostTemplateEditor.tsx +347 -0
@@ -0,0 +1,476 @@
1
+ 'use client'
2
+
3
+ import { useCallback, useEffect, useMemo, useState } from 'react'
4
+ import { AlertTriangle, Loader2 } from 'lucide-react'
5
+ import { toast } from 'sonner'
6
+ import { ConfirmDialog } from '../../components/ui/ConfirmDialog.js'
7
+ import { ErrorBoundary } from '../../components/ErrorBoundary.js'
8
+ import {
9
+ addSection,
10
+ createSection,
11
+ duplicateSection,
12
+ emptyPage,
13
+ fetchPageForEditor,
14
+ moveSection,
15
+ publishPage,
16
+ removeSection,
17
+ reorderSections,
18
+ savePageDraft,
19
+ toggleSectionVisibility,
20
+ updateSectionContent,
21
+ updateSectionMeta,
22
+ updateSectionSettings,
23
+ validatePage,
24
+ type EditorPage,
25
+ type ValidationResult,
26
+ } from '../../lib/page-editor-service.js'
27
+ import {
28
+ buildPublicPreviewUrl,
29
+ createPreviewToken,
30
+ resolveSiteUrl,
31
+ } from '../../lib/preview-link.js'
32
+ import type { SectionSettings, SectionTypeId } from './section-types.js'
33
+ import { DEFAULT_VIEWPORT, type ViewportId } from './viewports.js'
34
+ import { EditorTopBar, type SaveState } from './EditorTopBar.js'
35
+ import { SectionsPanel } from './SectionsPanel.js'
36
+ import { EditorCanvas } from './EditorCanvas.js'
37
+ import { SectionInspector } from './SectionInspector.js'
38
+ import { AddSectionModal } from './AddSectionModal.js'
39
+ import { PageSettingsModal } from './PageSettingsModal.js'
40
+ import { ValidationSummary } from './ValidationSummary.js'
41
+
42
+ interface PageSectionEditorProps {
43
+ /** undefined when creating a new page (`/pages/new`) */
44
+ documentId?: string
45
+ config?: any
46
+ session?: any
47
+ onNavigate: (path: string) => void
48
+ /**
49
+ * Open a preview route in a new browser tab. `prepare` runs before the tab
50
+ * navigates (used to save an unsaved draft first); returning `false` cancels
51
+ * the open. Falls back to in-tab navigation when not provided.
52
+ */
53
+ onOpenPreview?: (path: string, prepare?: () => Promise<boolean | void> | boolean | void) => void
54
+ }
55
+
56
+ function deriveCan(session: any): { canEdit: boolean; canPublish: boolean } {
57
+ const role: string | undefined = session?.user?.role
58
+ // No session info (e.g. local dev) → allow; otherwise gate by role.
59
+ if (!role) return { canEdit: true, canPublish: true }
60
+ const editors = ['ADMIN', 'EDITOR', 'AUTHOR']
61
+ const publishers = ['ADMIN', 'EDITOR']
62
+ return { canEdit: editors.includes(role), canPublish: publishers.includes(role) }
63
+ }
64
+
65
+ export function PageSectionEditor({
66
+ documentId,
67
+ config,
68
+ session,
69
+ onNavigate,
70
+ onOpenPreview,
71
+ }: PageSectionEditorProps) {
72
+ const [page, setPage] = useState<EditorPage | null>(null)
73
+ const [loading, setLoading] = useState(true)
74
+ const [loadError, setLoadError] = useState<string | null>(null)
75
+
76
+ const [selectedId, setSelectedId] = useState<string | null>(null)
77
+ const [viewport, setViewport] = useState<ViewportId>(DEFAULT_VIEWPORT)
78
+ const [dirty, setDirty] = useState(false)
79
+ const [saveState, setSaveState] = useState<SaveState>('idle')
80
+ const [publishing, setPublishing] = useState(false)
81
+
82
+ const [addOpen, setAddOpen] = useState(false)
83
+ const [settingsOpen, setSettingsOpen] = useState(false)
84
+ const [deleteTargetId, setDeleteTargetId] = useState<string | null>(null)
85
+ const [confirmPublish, setConfirmPublish] = useState(false)
86
+ const [leaveTarget, setLeaveTarget] = useState<string | null>(null)
87
+ const [validation, setValidation] = useState<ValidationResult | null>(null)
88
+
89
+ const { canEdit, canPublish } = useMemo(() => deriveCan(session), [session])
90
+
91
+ // ─── Load ──────────────────────────────────────────────────────────
92
+ useEffect(() => {
93
+ let cancelled = false
94
+ setLoading(true)
95
+ setLoadError(null)
96
+
97
+ if (!documentId) {
98
+ setPage(emptyPage())
99
+ setLoading(false)
100
+ return
101
+ }
102
+
103
+ fetchPageForEditor(documentId)
104
+ .then((p) => {
105
+ if (cancelled) return
106
+ setPage(p)
107
+ // Default to a clean canvas (no selection / no inspector) to match
108
+ // the editor's default composition; the inspector opens only once
109
+ // the user picks a section to edit.
110
+ setSelectedId(null)
111
+ })
112
+ .catch((err) => {
113
+ if (!cancelled) setLoadError(err instanceof Error ? err.message : 'Failed to load page')
114
+ })
115
+ .finally(() => {
116
+ if (!cancelled) setLoading(false)
117
+ })
118
+
119
+ return () => {
120
+ cancelled = true
121
+ }
122
+ }, [documentId])
123
+
124
+ // ─── Unsaved-changes guard (browser unload) ──────────────────────────
125
+ useEffect(() => {
126
+ if (!dirty) return
127
+ function onBeforeUnload(e: BeforeUnloadEvent) {
128
+ e.preventDefault()
129
+ e.returnValue = ''
130
+ }
131
+ window.addEventListener('beforeunload', onBeforeUnload)
132
+ return () => window.removeEventListener('beforeunload', onBeforeUnload)
133
+ }, [dirty])
134
+
135
+ // ─── Section mutations ───────────────────────────────────────────────
136
+ const mutateSections = useCallback(
137
+ (fn: (sections: EditorPage['sections']) => EditorPage['sections']) => {
138
+ setPage((prev) => (prev ? { ...prev, sections: fn(prev.sections) } : prev))
139
+ setDirty(true)
140
+ setSaveState('idle')
141
+ },
142
+ [],
143
+ )
144
+
145
+ const handleAdd = useCallback(
146
+ (type: SectionTypeId) => {
147
+ const section = createSection(type)
148
+ mutateSections((s) => addSection(s, section))
149
+ setSelectedId(section.id)
150
+ setAddOpen(false)
151
+ },
152
+ [mutateSections],
153
+ )
154
+
155
+ const handleReorder = useCallback(
156
+ (orderedIds: string[]) => mutateSections((s) => reorderSections(s, orderedIds)),
157
+ [mutateSections],
158
+ )
159
+
160
+ const handleToggleVisibility = useCallback(
161
+ (id: string) => mutateSections((s) => toggleSectionVisibility(s, id)),
162
+ [mutateSections],
163
+ )
164
+
165
+ const handleDuplicate = useCallback(
166
+ (id: string) => mutateSections((s) => duplicateSection(s, id)),
167
+ [mutateSections],
168
+ )
169
+
170
+ const confirmDelete = useCallback(() => {
171
+ if (!deleteTargetId) return
172
+ mutateSections((s) => removeSection(s, deleteTargetId))
173
+ setSelectedId((cur) => (cur === deleteTargetId ? null : cur))
174
+ setDeleteTargetId(null)
175
+ }, [deleteTargetId, mutateSections])
176
+
177
+ const handleContentChange = useCallback(
178
+ (patch: Record<string, unknown>) => {
179
+ if (!selectedId) return
180
+ mutateSections((s) => updateSectionContent(s, selectedId, patch))
181
+ },
182
+ [selectedId, mutateSections],
183
+ )
184
+
185
+ const handleSettingsChange = useCallback(
186
+ (patch: Partial<SectionSettings>) => {
187
+ if (!selectedId) return
188
+ mutateSections((s) => updateSectionSettings(s, selectedId, patch))
189
+ },
190
+ [selectedId, mutateSections],
191
+ )
192
+
193
+ const handleMetaChange = useCallback(
194
+ (patch: { name?: string; internalLabel?: string }) => {
195
+ if (!selectedId) return
196
+ mutateSections((s) => updateSectionMeta(s, selectedId, patch))
197
+ },
198
+ [selectedId, mutateSections],
199
+ )
200
+
201
+ // ─── Page-level settings ─────────────────────────────────────────────
202
+ const handlePageSettings = useCallback((patch: Partial<EditorPage>) => {
203
+ setPage((prev) => (prev ? { ...prev, ...patch } : prev))
204
+ setDirty(true)
205
+ setSaveState('idle')
206
+ }, [])
207
+
208
+ // ─── Save / publish ──────────────────────────────────────────────────
209
+ const handleSaveDraft = useCallback(async (): Promise<boolean> => {
210
+ if (!page) return false
211
+ const result = validatePage(page, false)
212
+ if (result.errors.length > 0) {
213
+ setValidation(result)
214
+ toast.error(result.errors[0]!)
215
+ setSettingsOpen(true)
216
+ return false
217
+ }
218
+ setSaveState('saving')
219
+ try {
220
+ const saved = await savePageDraft(page)
221
+ setPage(saved)
222
+ setDirty(false)
223
+ setSaveState('saved')
224
+ setValidation(null)
225
+ toast.success('Draft saved')
226
+ if (!documentId && saved.id) onNavigate(`/pages/${saved.id}/edit`)
227
+ return true
228
+ } catch (err) {
229
+ setSaveState('error')
230
+ toast.error(err instanceof Error ? err.message : 'Failed to save draft')
231
+ return false
232
+ }
233
+ }, [page, documentId, onNavigate])
234
+
235
+ const requestPublish = useCallback(() => {
236
+ if (!page) return
237
+ const result = validatePage(page, true)
238
+ setValidation(result.errors.length || result.warnings.length ? result : null)
239
+ if (result.errors.length > 0) {
240
+ toast.error('Resolve the listed issues before publishing')
241
+ return
242
+ }
243
+ setConfirmPublish(true)
244
+ }, [page])
245
+
246
+ const doPublish = useCallback(async () => {
247
+ if (!page) return
248
+ setPublishing(true)
249
+ try {
250
+ const published = await publishPage(page)
251
+ setPage(published)
252
+ setDirty(false)
253
+ setSaveState('saved')
254
+ setValidation(null)
255
+ toast.success('Page published')
256
+ if (!documentId && published.id) onNavigate(`/pages/${published.id}/edit`)
257
+ } catch (err) {
258
+ toast.error(err instanceof Error ? err.message : 'Failed to publish page')
259
+ } finally {
260
+ setPublishing(false)
261
+ }
262
+ }, [page, documentId, onNavigate])
263
+
264
+ // ─── Navigation with unsaved guard ───────────────────────────────────
265
+ const guardedNavigate = useCallback(
266
+ (path: string) => {
267
+ if (dirty) setLeaveTarget(path)
268
+ else onNavigate(path)
269
+ },
270
+ [dirty, onNavigate],
271
+ )
272
+
273
+ const handlePreview = useCallback(() => {
274
+ if (!page?.id) {
275
+ toast.error('Save the page before previewing')
276
+ return
277
+ }
278
+ const path = `/pages/${page.id}/preview`
279
+ // New-tab preview: save first when there are unsaved edits so the preview
280
+ // (which reads the persisted document) reflects what's on screen.
281
+ if (onOpenPreview) {
282
+ onOpenPreview(path, dirty ? handleSaveDraft : undefined)
283
+ } else {
284
+ guardedNavigate(path)
285
+ }
286
+ }, [page, dirty, onOpenPreview, handleSaveDraft, guardedNavigate])
287
+
288
+ // "View on site": render the draft through the consumer's real public route
289
+ // via a signed preview token. The blank tab is opened synchronously inside
290
+ // the click (popup-safe), then pointed at the public URL once any unsaved
291
+ // draft is persisted and the token is minted.
292
+ const handleViewOnSite = useCallback(() => {
293
+ if (!page?.id || !page.slug) {
294
+ toast.error('Save the page before viewing it on the site')
295
+ return
296
+ }
297
+ const tab = window.open('about:blank', '_blank')
298
+ if (!tab) {
299
+ toast.error('Enable pop-ups for this site to open the preview in a new tab.')
300
+ return
301
+ }
302
+ void (async () => {
303
+ try {
304
+ if (dirty) {
305
+ const ok = await handleSaveDraft()
306
+ if (!ok) {
307
+ tab.close()
308
+ return
309
+ }
310
+ }
311
+ const { token } = await createPreviewToken('pages', page.id!)
312
+ const target = buildPublicPreviewUrl({
313
+ siteUrl: resolveSiteUrl(config?.seo?.siteUrl),
314
+ urlPrefix: '',
315
+ slug: page.slug,
316
+ token,
317
+ })
318
+ try {
319
+ tab.opener = null
320
+ } catch {
321
+ /* same-origin guard */
322
+ }
323
+ tab.location.replace(target)
324
+ } catch (err) {
325
+ tab.close()
326
+ toast.error(err instanceof Error ? err.message : 'Failed to open site preview')
327
+ }
328
+ })()
329
+ }, [page, dirty, config, handleSaveDraft])
330
+
331
+ // ─── Render states ───────────────────────────────────────────────────
332
+ if (loading) {
333
+ return (
334
+ <div className="bg-background flex h-full items-center justify-center">
335
+ <Loader2 className="text-muted-foreground h-6 w-6 animate-spin" aria-hidden />
336
+ <span className="sr-only">Loading page editor…</span>
337
+ </div>
338
+ )
339
+ }
340
+
341
+ if (loadError || !page) {
342
+ return (
343
+ <div className="bg-background flex h-full items-center justify-center p-6">
344
+ <div className="border-destructive/30 bg-destructive/5 flex max-w-md items-center gap-3 rounded-lg border p-4">
345
+ <AlertTriangle className="text-destructive shrink-0" size={20} aria-hidden />
346
+ <div className="min-w-0">
347
+ <p className="text-foreground text-sm font-medium">Failed to load page</p>
348
+ <p className="text-muted-foreground mt-1 text-sm">{loadError ?? 'Page not found.'}</p>
349
+ <button
350
+ type="button"
351
+ onClick={() => onNavigate('/pages')}
352
+ className="text-primary mt-2 text-sm font-medium hover:underline"
353
+ >
354
+ Back to Pages
355
+ </button>
356
+ </div>
357
+ </div>
358
+ </div>
359
+ )
360
+ }
361
+
362
+ const selected = page.sections.find((s) => s.id === selectedId) ?? null
363
+
364
+ return (
365
+ <ErrorBoundary>
366
+ <div className="bg-background flex h-full flex-col overflow-hidden">
367
+ <EditorTopBar
368
+ title={page.title}
369
+ status={page.status}
370
+ dirty={dirty}
371
+ saveState={saveState}
372
+ publishing={publishing}
373
+ canEdit={canEdit}
374
+ canPublish={canPublish && canEdit}
375
+ viewport={viewport}
376
+ onViewport={setViewport}
377
+ onBack={() => guardedNavigate('/pages')}
378
+ onPreview={handlePreview}
379
+ onViewOnSite={page.id ? handleViewOnSite : undefined}
380
+ onOpenSettings={() => setSettingsOpen(true)}
381
+ onSaveDraft={handleSaveDraft}
382
+ onPublish={requestPublish}
383
+ />
384
+
385
+ {validation && (
386
+ <ValidationSummary
387
+ errors={validation.errors}
388
+ warnings={validation.warnings}
389
+ onDismiss={() => setValidation(null)}
390
+ />
391
+ )}
392
+
393
+ <div className="flex flex-1 overflow-hidden">
394
+ <SectionsPanel
395
+ sections={page.sections}
396
+ selectedId={selectedId}
397
+ canEdit={canEdit}
398
+ onSelect={setSelectedId}
399
+ onReorder={handleReorder}
400
+ onToggleVisibility={handleToggleVisibility}
401
+ onDuplicate={handleDuplicate}
402
+ onRequestDelete={(id) => setDeleteTargetId(id)}
403
+ onAdd={() => setAddOpen(true)}
404
+ />
405
+
406
+ <div className="flex-1 overflow-hidden">
407
+ <EditorCanvas
408
+ sections={page.sections}
409
+ selectedId={selectedId}
410
+ viewport={viewport}
411
+ onSelect={setSelectedId}
412
+ onAddSection={() => setAddOpen(true)}
413
+ />
414
+ </div>
415
+
416
+ {selected && (
417
+ <SectionInspector
418
+ section={selected}
419
+ canEdit={canEdit}
420
+ onClose={() => setSelectedId(null)}
421
+ onContentChange={handleContentChange}
422
+ onSettingsChange={handleSettingsChange}
423
+ onMetaChange={handleMetaChange}
424
+ onToggleVisibility={() => handleToggleVisibility(selected.id)}
425
+ />
426
+ )}
427
+ </div>
428
+ </div>
429
+
430
+ <AddSectionModal open={addOpen} onClose={() => setAddOpen(false)} onAdd={handleAdd} />
431
+
432
+ <PageSettingsModal
433
+ open={settingsOpen}
434
+ page={page}
435
+ canEdit={canEdit}
436
+ onClose={() => setSettingsOpen(false)}
437
+ onSave={handlePageSettings}
438
+ />
439
+
440
+ <ConfirmDialog
441
+ open={deleteTargetId !== null}
442
+ onClose={() => setDeleteTargetId(null)}
443
+ onConfirm={confirmDelete}
444
+ title="Delete section?"
445
+ description="This removes the section from the page. You can undo by leaving without saving."
446
+ confirmLabel="Delete"
447
+ destructive
448
+ />
449
+
450
+ <ConfirmDialog
451
+ open={confirmPublish}
452
+ onClose={() => setConfirmPublish(false)}
453
+ onConfirm={doPublish}
454
+ title="Publish this page?"
455
+ description="The current content will go live and replace the published version."
456
+ confirmLabel="Publish"
457
+ />
458
+
459
+ <ConfirmDialog
460
+ open={leaveTarget !== null}
461
+ onClose={() => setLeaveTarget(null)}
462
+ onConfirm={() => {
463
+ const target = leaveTarget
464
+ setLeaveTarget(null)
465
+ setDirty(false)
466
+ if (target) onNavigate(target)
467
+ }}
468
+ title="Discard unsaved changes?"
469
+ description="You have unsaved changes that will be lost if you leave now."
470
+ confirmLabel="Leave"
471
+ cancelLabel="Stay"
472
+ destructive
473
+ />
474
+ </ErrorBoundary>
475
+ )
476
+ }
@@ -0,0 +1,154 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useState } from 'react'
4
+ import { Modal } from '../../components/ui/Modal.js'
5
+ import { Button } from '../../components/ui/Button.js'
6
+ import type { EditorPage } from '../../lib/page-editor-service.js'
7
+
8
+ interface PageSettingsModalProps {
9
+ open: boolean
10
+ page: EditorPage
11
+ canEdit: boolean
12
+ onClose: () => void
13
+ onSave: (patch: Partial<EditorPage>) => void
14
+ }
15
+
16
+ const LABEL = 'text-foreground mb-1 block text-xs font-medium'
17
+ const INPUT =
18
+ 'border-input bg-input-background text-foreground focus-visible:ring-ring w-full rounded-md border px-2.5 py-1.5 text-sm focus-visible:ring-2 focus-visible:outline-none'
19
+
20
+ function slugify(value: string): string {
21
+ return value
22
+ .toLowerCase()
23
+ .trim()
24
+ .replace(/[^a-z0-9]+/g, '-')
25
+ .replace(/^-+|-+$/g, '')
26
+ }
27
+
28
+ /** Edits page-level fields: title, slug, path, and SEO. */
29
+ export function PageSettingsModal({
30
+ open,
31
+ page,
32
+ canEdit,
33
+ onClose,
34
+ onSave,
35
+ }: PageSettingsModalProps) {
36
+ const [title, setTitle] = useState(page.title)
37
+ const [slug, setSlug] = useState(page.slug)
38
+ const [path, setPath] = useState(page.path)
39
+ const [seoTitle, setSeoTitle] = useState(page.seoTitle)
40
+ const [seoDescription, setSeoDescription] = useState(page.seoDescription)
41
+ // Auto-derive slug/path from title until the user edits slug manually.
42
+ const [slugTouched, setSlugTouched] = useState(Boolean(page.slug))
43
+
44
+ useEffect(() => {
45
+ if (!open) return
46
+ setTitle(page.title)
47
+ setSlug(page.slug)
48
+ setPath(page.path)
49
+ setSeoTitle(page.seoTitle)
50
+ setSeoDescription(page.seoDescription)
51
+ setSlugTouched(Boolean(page.slug))
52
+ }, [open, page])
53
+
54
+ function handleTitle(next: string) {
55
+ setTitle(next)
56
+ if (!slugTouched) {
57
+ const s = slugify(next)
58
+ setSlug(s)
59
+ if (!page.path || page.path === `/${slug}`) setPath(s ? `/${s}` : '')
60
+ }
61
+ }
62
+
63
+ function handleSave() {
64
+ onSave({ title, slug, path: path || (slug ? `/${slug}` : '/'), seoTitle, seoDescription })
65
+ onClose()
66
+ }
67
+
68
+ return (
69
+ <Modal
70
+ open={open}
71
+ onClose={onClose}
72
+ title="Page settings"
73
+ actions={
74
+ <>
75
+ <Button variant="ghost" onClick={onClose}>
76
+ Cancel
77
+ </Button>
78
+ <Button variant="primary" onClick={handleSave} disabled={!canEdit}>
79
+ Done
80
+ </Button>
81
+ </>
82
+ }
83
+ >
84
+ <div className="space-y-3">
85
+ <div>
86
+ <label className={LABEL} htmlFor="ps-title">
87
+ Title <span className="text-destructive">*</span>
88
+ </label>
89
+ <input
90
+ id="ps-title"
91
+ className={INPUT}
92
+ value={title}
93
+ disabled={!canEdit}
94
+ onChange={(e) => handleTitle(e.target.value)}
95
+ />
96
+ </div>
97
+ <div className="grid grid-cols-2 gap-3">
98
+ <div>
99
+ <label className={LABEL} htmlFor="ps-slug">
100
+ Slug
101
+ </label>
102
+ <input
103
+ id="ps-slug"
104
+ className={INPUT}
105
+ value={slug}
106
+ disabled={!canEdit}
107
+ onChange={(e) => {
108
+ setSlugTouched(true)
109
+ setSlug(slugify(e.target.value))
110
+ }}
111
+ />
112
+ </div>
113
+ <div>
114
+ <label className={LABEL} htmlFor="ps-path">
115
+ Path <span className="text-destructive">*</span>
116
+ </label>
117
+ <input
118
+ id="ps-path"
119
+ className={INPUT}
120
+ value={path}
121
+ disabled={!canEdit}
122
+ placeholder="/about/story"
123
+ onChange={(e) => setPath(e.target.value)}
124
+ />
125
+ </div>
126
+ </div>
127
+ <div>
128
+ <label className={LABEL} htmlFor="ps-seo-title">
129
+ SEO title
130
+ </label>
131
+ <input
132
+ id="ps-seo-title"
133
+ className={INPUT}
134
+ value={seoTitle}
135
+ disabled={!canEdit}
136
+ onChange={(e) => setSeoTitle(e.target.value)}
137
+ />
138
+ </div>
139
+ <div>
140
+ <label className={LABEL} htmlFor="ps-seo-desc">
141
+ SEO description
142
+ </label>
143
+ <textarea
144
+ id="ps-seo-desc"
145
+ className={`${INPUT} min-h-[64px] resize-y`}
146
+ value={seoDescription}
147
+ disabled={!canEdit}
148
+ onChange={(e) => setSeoDescription(e.target.value)}
149
+ />
150
+ </div>
151
+ </div>
152
+ </Modal>
153
+ )
154
+ }