@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,506 @@
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
+ emptyPostFromTemplate,
13
+ fetchPostForEditor,
14
+ fetchTemplateForEditor,
15
+ moveSection,
16
+ publishPost,
17
+ removeSection,
18
+ reorderSections,
19
+ savePostDraft,
20
+ toggleSectionVisibility,
21
+ updateSectionContent,
22
+ updateSectionMeta,
23
+ updateSectionSettings,
24
+ validatePost,
25
+ type EditorPost,
26
+ type PostHeaderConfig,
27
+ type ValidationResult,
28
+ } from '../../lib/post-editor-service.js'
29
+ import { defaultPostHeader } from '@actuate-media/cms-core/page-sections'
30
+ import {
31
+ buildPublicPreviewUrl,
32
+ createPreviewToken,
33
+ resolveSiteUrl,
34
+ } from '../../lib/preview-link.js'
35
+ import type { SectionSettings, SectionTypeId } from '../page-editor/section-types.js'
36
+ import type { PostRenderContext } from '../page-editor/sections/index.js'
37
+ import { DEFAULT_VIEWPORT, type ViewportId } from '../page-editor/viewports.js'
38
+ import { EditorTopBar, type SaveState } from '../page-editor/EditorTopBar.js'
39
+ import { SectionsPanel } from '../page-editor/SectionsPanel.js'
40
+ import { SectionInspector } from '../page-editor/SectionInspector.js'
41
+ import { AddSectionModal } from '../page-editor/AddSectionModal.js'
42
+ import { ValidationSummary } from '../page-editor/ValidationSummary.js'
43
+ import { PostEditorCanvas } from './PostEditorCanvas.js'
44
+ import { PostFieldsModal } from './PostFieldsModal.js'
45
+
46
+ interface PostSectionEditorProps {
47
+ postType: string
48
+ /** undefined when creating a new post (`/posts/:type/new`) */
49
+ documentId?: string
50
+ config?: any
51
+ session?: any
52
+ onNavigate: (path: string) => void
53
+ /**
54
+ * Open a preview route in a new browser tab. `prepare` runs before the tab
55
+ * navigates (used to save an unsaved draft first); returning `false` cancels
56
+ * the open. Falls back to in-tab navigation when not provided.
57
+ */
58
+ onOpenPreview?: (path: string, prepare?: () => Promise<boolean | void> | boolean | void) => void
59
+ }
60
+
61
+ function deriveCan(session: any): { canEdit: boolean; canPublish: boolean } {
62
+ const role: string | undefined = session?.user?.role
63
+ if (!role) return { canEdit: true, canPublish: true }
64
+ const editors = ['ADMIN', 'EDITOR', 'AUTHOR']
65
+ const publishers = ['ADMIN', 'EDITOR']
66
+ return { canEdit: editors.includes(role), canPublish: publishers.includes(role) }
67
+ }
68
+
69
+ export function PostSectionEditor({
70
+ postType,
71
+ documentId,
72
+ config,
73
+ session,
74
+ onNavigate,
75
+ onOpenPreview,
76
+ }: PostSectionEditorProps) {
77
+ const [post, setPost] = useState<EditorPost | null>(null)
78
+ const [header, setHeader] = useState<PostHeaderConfig>(defaultPostHeader())
79
+ const [loading, setLoading] = useState(true)
80
+ const [loadError, setLoadError] = useState<string | null>(null)
81
+
82
+ const [selectedId, setSelectedId] = useState<string | null>(null)
83
+ const [viewport, setViewport] = useState<ViewportId>(DEFAULT_VIEWPORT)
84
+ const [dirty, setDirty] = useState(false)
85
+ const [saveState, setSaveState] = useState<SaveState>('idle')
86
+ const [publishing, setPublishing] = useState(false)
87
+
88
+ const [addOpen, setAddOpen] = useState(false)
89
+ const [fieldsOpen, setFieldsOpen] = useState(false)
90
+ const [deleteTargetId, setDeleteTargetId] = useState<string | null>(null)
91
+ const [confirmPublish, setConfirmPublish] = useState(false)
92
+ const [leaveTarget, setLeaveTarget] = useState<string | null>(null)
93
+ const [validation, setValidation] = useState<ValidationResult | null>(null)
94
+
95
+ const { canEdit, canPublish } = useMemo(() => deriveCan(session), [session])
96
+
97
+ const typeLabel = useMemo(() => {
98
+ const col = config?.collections?.[postType]
99
+ return col?.labels?.singular ?? postType
100
+ }, [config, postType])
101
+
102
+ // ─── Load ──────────────────────────────────────────────────────────
103
+ useEffect(() => {
104
+ let cancelled = false
105
+ setLoading(true)
106
+ setLoadError(null)
107
+
108
+ // Always load the type template header (the post editor renders it but
109
+ // never edits it — header design lives at the type level).
110
+ fetchTemplateForEditor(postType)
111
+ .then((t) => {
112
+ if (!cancelled) setHeader(t.header)
113
+ })
114
+ .catch(() => {
115
+ /* fall back to the default header */
116
+ })
117
+
118
+ const load = documentId
119
+ ? fetchPostForEditor(postType, documentId)
120
+ : emptyPostFromTemplate(postType)
121
+
122
+ load
123
+ .then((p) => {
124
+ if (cancelled) return
125
+ setPost(p)
126
+ setSelectedId(null)
127
+ if (!documentId) setDirty(true)
128
+ })
129
+ .catch((err) => {
130
+ if (!cancelled) setLoadError(err instanceof Error ? err.message : 'Failed to load post')
131
+ })
132
+ .finally(() => {
133
+ if (!cancelled) setLoading(false)
134
+ })
135
+
136
+ return () => {
137
+ cancelled = true
138
+ }
139
+ }, [postType, documentId])
140
+
141
+ // ─── Unsaved-changes guard ───────────────────────────────────────────
142
+ useEffect(() => {
143
+ if (!dirty) return
144
+ function onBeforeUnload(e: BeforeUnloadEvent) {
145
+ e.preventDefault()
146
+ e.returnValue = ''
147
+ }
148
+ window.addEventListener('beforeunload', onBeforeUnload)
149
+ return () => window.removeEventListener('beforeunload', onBeforeUnload)
150
+ }, [dirty])
151
+
152
+ const context: PostRenderContext = useMemo(
153
+ () => ({
154
+ title: post?.title,
155
+ excerpt: post?.excerpt,
156
+ body: post?.body,
157
+ featuredImageUrl: post?.featuredImage,
158
+ publishDate: post?.publishDate ?? post?.updatedAt ?? null,
159
+ category: post?.category,
160
+ author: { name: session?.user?.name || session?.user?.email },
161
+ }),
162
+ [post, session],
163
+ )
164
+
165
+ // ─── Section mutations ───────────────────────────────────────────────
166
+ const mutateSections = useCallback(
167
+ (fn: (sections: EditorPost['sections']) => EditorPost['sections']) => {
168
+ setPost((prev) => (prev ? { ...prev, sections: fn(prev.sections) } : prev))
169
+ setDirty(true)
170
+ setSaveState('idle')
171
+ },
172
+ [],
173
+ )
174
+
175
+ const handleAdd = useCallback(
176
+ (type: SectionTypeId) => {
177
+ const section = createSection(type)
178
+ mutateSections((s) => addSection(s, section))
179
+ setSelectedId(section.id)
180
+ setAddOpen(false)
181
+ },
182
+ [mutateSections],
183
+ )
184
+
185
+ const handleReorder = useCallback(
186
+ (orderedIds: string[]) => mutateSections((s) => reorderSections(s, orderedIds)),
187
+ [mutateSections],
188
+ )
189
+ const handleToggleVisibility = useCallback(
190
+ (id: string) => mutateSections((s) => toggleSectionVisibility(s, id)),
191
+ [mutateSections],
192
+ )
193
+ const handleDuplicate = useCallback(
194
+ (id: string) => mutateSections((s) => duplicateSection(s, id)),
195
+ [mutateSections],
196
+ )
197
+ const confirmDelete = useCallback(() => {
198
+ if (!deleteTargetId) return
199
+ mutateSections((s) => removeSection(s, deleteTargetId))
200
+ setSelectedId((cur) => (cur === deleteTargetId ? null : cur))
201
+ setDeleteTargetId(null)
202
+ }, [deleteTargetId, mutateSections])
203
+
204
+ const handleContentChange = useCallback(
205
+ (patch: Record<string, unknown>) => {
206
+ if (!selectedId) return
207
+ mutateSections((s) => updateSectionContent(s, selectedId, patch))
208
+ },
209
+ [selectedId, mutateSections],
210
+ )
211
+ const handleSettingsChange = useCallback(
212
+ (patch: Partial<SectionSettings>) => {
213
+ if (!selectedId) return
214
+ mutateSections((s) => updateSectionSettings(s, selectedId, patch))
215
+ },
216
+ [selectedId, mutateSections],
217
+ )
218
+ const handleMetaChange = useCallback(
219
+ (patch: { name?: string; internalLabel?: string }) => {
220
+ if (!selectedId) return
221
+ mutateSections((s) => updateSectionMeta(s, selectedId, patch))
222
+ },
223
+ [selectedId, mutateSections],
224
+ )
225
+
226
+ const handleFields = useCallback((patch: Partial<EditorPost>) => {
227
+ setPost((prev) => (prev ? { ...prev, ...patch } : prev))
228
+ setDirty(true)
229
+ setSaveState('idle')
230
+ }, [])
231
+
232
+ // ─── Save / publish ──────────────────────────────────────────────────
233
+ const handleSaveDraft = useCallback(async (): Promise<boolean> => {
234
+ if (!post) return false
235
+ const result = validatePost(post, false)
236
+ if (result.errors.length > 0) {
237
+ setValidation(result)
238
+ toast.error(result.errors[0]!)
239
+ setFieldsOpen(true)
240
+ return false
241
+ }
242
+ setSaveState('saving')
243
+ try {
244
+ const saved = await savePostDraft(post)
245
+ setPost(saved)
246
+ setDirty(false)
247
+ setSaveState('saved')
248
+ setValidation(null)
249
+ toast.success('Draft saved')
250
+ if (!documentId && saved.id) onNavigate(`/posts/${postType}/${saved.id}/edit`)
251
+ return true
252
+ } catch (err) {
253
+ setSaveState('error')
254
+ toast.error(err instanceof Error ? err.message : 'Failed to save draft')
255
+ return false
256
+ }
257
+ }, [post, documentId, postType, onNavigate])
258
+
259
+ const requestPublish = useCallback(() => {
260
+ if (!post) return
261
+ const result = validatePost(post, true)
262
+ setValidation(result.errors.length || result.warnings.length ? result : null)
263
+ if (result.errors.length > 0) {
264
+ toast.error('Resolve the listed issues before publishing')
265
+ return
266
+ }
267
+ setConfirmPublish(true)
268
+ }, [post])
269
+
270
+ const doPublish = useCallback(async () => {
271
+ if (!post) return
272
+ setPublishing(true)
273
+ try {
274
+ const published = await publishPost(post)
275
+ setPost(published)
276
+ setDirty(false)
277
+ setSaveState('saved')
278
+ setValidation(null)
279
+ toast.success('Post published')
280
+ if (!documentId && published.id) onNavigate(`/posts/${postType}/${published.id}/edit`)
281
+ } catch (err) {
282
+ toast.error(err instanceof Error ? err.message : 'Failed to publish post')
283
+ } finally {
284
+ setPublishing(false)
285
+ }
286
+ }, [post, documentId, postType, onNavigate])
287
+
288
+ const guardedNavigate = useCallback(
289
+ (path: string) => {
290
+ if (dirty) setLeaveTarget(path)
291
+ else onNavigate(path)
292
+ },
293
+ [dirty, onNavigate],
294
+ )
295
+
296
+ const handlePreview = useCallback(() => {
297
+ if (!post?.id) {
298
+ toast.error('Save the post before previewing')
299
+ return
300
+ }
301
+ const path = `/posts/${postType}/${post.id}/preview`
302
+ // New-tab preview: save first when there are unsaved edits so the preview
303
+ // (which reads the persisted document) reflects what's on screen.
304
+ if (onOpenPreview) {
305
+ onOpenPreview(path, dirty ? handleSaveDraft : undefined)
306
+ } else {
307
+ guardedNavigate(path)
308
+ }
309
+ }, [post, postType, dirty, onOpenPreview, handleSaveDraft, guardedNavigate])
310
+
311
+ // "View on site": render the draft through the consumer's real public route
312
+ // (`/<urlPrefix>/<slug>?preview=<token>`) via a signed preview token.
313
+ const handleViewOnSite = useCallback(() => {
314
+ if (!post?.id || !post.slug) {
315
+ toast.error('Save the post before viewing it on the site')
316
+ return
317
+ }
318
+ const tab = window.open('about:blank', '_blank')
319
+ if (!tab) {
320
+ toast.error('Enable pop-ups for this site to open the preview in a new tab.')
321
+ return
322
+ }
323
+ void (async () => {
324
+ try {
325
+ if (dirty) {
326
+ const ok = await handleSaveDraft()
327
+ if (!ok) {
328
+ tab.close()
329
+ return
330
+ }
331
+ }
332
+ const { token } = await createPreviewToken(postType, post.id!)
333
+ const urlPrefix = config?.collections?.[postType]?.urlPrefix ?? postType
334
+ const target = buildPublicPreviewUrl({
335
+ siteUrl: resolveSiteUrl(config?.seo?.siteUrl),
336
+ urlPrefix,
337
+ slug: post.slug,
338
+ token,
339
+ })
340
+ try {
341
+ tab.opener = null
342
+ } catch {
343
+ /* same-origin guard */
344
+ }
345
+ tab.location.replace(target)
346
+ } catch (err) {
347
+ tab.close()
348
+ toast.error(err instanceof Error ? err.message : 'Failed to open site preview')
349
+ }
350
+ })()
351
+ }, [post, postType, dirty, config, handleSaveDraft])
352
+
353
+ // ─── Render states ───────────────────────────────────────────────────
354
+ if (loading) {
355
+ return (
356
+ <div className="bg-background flex h-full items-center justify-center">
357
+ <Loader2 className="text-muted-foreground h-6 w-6 animate-spin" aria-hidden />
358
+ <span className="sr-only">Loading post editor…</span>
359
+ </div>
360
+ )
361
+ }
362
+
363
+ if (loadError || !post) {
364
+ return (
365
+ <div className="bg-background flex h-full items-center justify-center p-6">
366
+ <div className="border-destructive/30 bg-destructive/5 flex max-w-md items-center gap-3 rounded-lg border p-4">
367
+ <AlertTriangle className="text-destructive shrink-0" size={20} aria-hidden />
368
+ <div className="min-w-0">
369
+ <p className="text-foreground text-sm font-medium">Failed to load post</p>
370
+ <p className="text-muted-foreground mt-1 text-sm">{loadError ?? 'Post not found.'}</p>
371
+ <button
372
+ type="button"
373
+ onClick={() => onNavigate('/posts')}
374
+ className="text-primary mt-2 text-sm font-medium hover:underline"
375
+ >
376
+ Back to Posts
377
+ </button>
378
+ </div>
379
+ </div>
380
+ </div>
381
+ )
382
+ }
383
+
384
+ const selected = post.sections.find((s) => s.id === selectedId) ?? null
385
+
386
+ return (
387
+ <ErrorBoundary>
388
+ <div className="bg-background flex h-full flex-col overflow-hidden">
389
+ <EditorTopBar
390
+ title={post.title || `New ${typeLabel}`}
391
+ status={post.status}
392
+ dirty={dirty}
393
+ saveState={saveState}
394
+ publishing={publishing}
395
+ canEdit={canEdit}
396
+ canPublish={canPublish && canEdit}
397
+ viewport={viewport}
398
+ backLabel="Posts"
399
+ onViewport={setViewport}
400
+ onBack={() => guardedNavigate('/posts')}
401
+ onPreview={handlePreview}
402
+ onViewOnSite={post.id ? handleViewOnSite : undefined}
403
+ onOpenSettings={() => setFieldsOpen(true)}
404
+ onSaveDraft={handleSaveDraft}
405
+ onPublish={requestPublish}
406
+ />
407
+
408
+ {validation && (
409
+ <ValidationSummary
410
+ errors={validation.errors}
411
+ warnings={validation.warnings}
412
+ onDismiss={() => setValidation(null)}
413
+ />
414
+ )}
415
+
416
+ <div className="flex flex-1 overflow-hidden">
417
+ <SectionsPanel
418
+ sections={post.sections}
419
+ selectedId={selectedId}
420
+ canEdit={canEdit}
421
+ onSelect={setSelectedId}
422
+ onReorder={handleReorder}
423
+ onToggleVisibility={handleToggleVisibility}
424
+ onDuplicate={handleDuplicate}
425
+ onRequestDelete={(id) => setDeleteTargetId(id)}
426
+ onAdd={() => setAddOpen(true)}
427
+ />
428
+
429
+ <div className="flex-1 overflow-hidden">
430
+ <PostEditorCanvas
431
+ sections={post.sections}
432
+ header={header}
433
+ context={context}
434
+ selectedId={selectedId}
435
+ viewport={viewport}
436
+ onSelect={setSelectedId}
437
+ onAddSection={() => setAddOpen(true)}
438
+ />
439
+ </div>
440
+
441
+ {selected && (
442
+ <SectionInspector
443
+ section={selected}
444
+ canEdit={canEdit}
445
+ onClose={() => setSelectedId(null)}
446
+ onContentChange={handleContentChange}
447
+ onSettingsChange={handleSettingsChange}
448
+ onMetaChange={handleMetaChange}
449
+ onToggleVisibility={() => handleToggleVisibility(selected.id)}
450
+ />
451
+ )}
452
+ </div>
453
+ </div>
454
+
455
+ <AddSectionModal
456
+ open={addOpen}
457
+ scope="post"
458
+ onClose={() => setAddOpen(false)}
459
+ onAdd={handleAdd}
460
+ />
461
+
462
+ <PostFieldsModal
463
+ open={fieldsOpen}
464
+ post={post}
465
+ canEdit={canEdit}
466
+ onClose={() => setFieldsOpen(false)}
467
+ onSave={handleFields}
468
+ />
469
+
470
+ <ConfirmDialog
471
+ open={deleteTargetId !== null}
472
+ onClose={() => setDeleteTargetId(null)}
473
+ onConfirm={confirmDelete}
474
+ title="Delete section?"
475
+ description="This removes the section from the post. You can undo by leaving without saving."
476
+ confirmLabel="Delete"
477
+ destructive
478
+ />
479
+
480
+ <ConfirmDialog
481
+ open={confirmPublish}
482
+ onClose={() => setConfirmPublish(false)}
483
+ onConfirm={doPublish}
484
+ title="Publish this post?"
485
+ description="The current content will go live and replace the published version."
486
+ confirmLabel="Publish"
487
+ />
488
+
489
+ <ConfirmDialog
490
+ open={leaveTarget !== null}
491
+ onClose={() => setLeaveTarget(null)}
492
+ onConfirm={() => {
493
+ const target = leaveTarget
494
+ setLeaveTarget(null)
495
+ setDirty(false)
496
+ if (target) onNavigate(target)
497
+ }}
498
+ title="Discard unsaved changes?"
499
+ description="You have unsaved changes that will be lost if you leave now."
500
+ confirmLabel="Leave"
501
+ cancelLabel="Stay"
502
+ destructive
503
+ />
504
+ </ErrorBoundary>
505
+ )
506
+ }