@jhits/plugin-blog 0.0.18 → 0.0.20

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 (291) hide show
  1. package/dist/api/categories.d.ts.map +1 -1
  2. package/dist/api/categories.js +42 -38
  3. package/dist/api/handler.d.ts +1 -26
  4. package/dist/api/handler.d.ts.map +1 -1
  5. package/dist/api/handler.js +81 -500
  6. package/dist/api/router.d.ts +0 -5
  7. package/dist/api/router.d.ts.map +1 -1
  8. package/dist/api/router.js +8 -35
  9. package/dist/api/service.d.ts +80 -0
  10. package/dist/api/service.d.ts.map +1 -0
  11. package/dist/api/service.js +219 -0
  12. package/dist/hooks/useAutoSave.d.ts +10 -0
  13. package/dist/hooks/useAutoSave.d.ts.map +1 -0
  14. package/dist/hooks/useAutoSave.js +57 -0
  15. package/dist/hooks/useCategories.d.ts +1 -1
  16. package/dist/hooks/useCategories.d.ts.map +1 -1
  17. package/dist/hooks/useCategories.js +15 -46
  18. package/dist/index.d.ts +24 -31
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +44 -201
  21. package/dist/init.d.ts +20 -7
  22. package/dist/init.d.ts.map +1 -1
  23. package/dist/init.js +8 -7
  24. package/dist/lib/blocks/BlockRenderer.d.ts.map +1 -1
  25. package/dist/lib/layouts/blocks/ColumnsBlock.d.ts.map +1 -1
  26. package/dist/lib/layouts/blocks/ColumnsBlock.js +30 -113
  27. package/dist/lib/layouts/blocks/SectionBlock.d.ts.map +1 -1
  28. package/dist/lib/layouts/blocks/SectionBlock.js +9 -21
  29. package/dist/lib/layouts/index.d.ts +3 -3
  30. package/dist/lib/layouts/index.js +4 -4
  31. package/dist/lib/mappers/apiMapper.d.ts +10 -0
  32. package/dist/lib/mappers/apiMapper.d.ts.map +1 -1
  33. package/dist/lib/mappers/apiMapper.js +47 -32
  34. package/dist/lib/rich-text/RichTextEditor.d.ts +4 -2
  35. package/dist/lib/rich-text/RichTextEditor.d.ts.map +1 -1
  36. package/dist/lib/rich-text/RichTextEditor.js +12 -9
  37. package/dist/lib/utils/config-resolver.d.ts +28 -0
  38. package/dist/lib/utils/config-resolver.d.ts.map +1 -0
  39. package/dist/lib/utils/config-resolver.js +46 -0
  40. package/dist/lib/utils/tree.d.ts +29 -0
  41. package/dist/lib/utils/tree.d.ts.map +1 -0
  42. package/dist/lib/utils/tree.js +129 -0
  43. package/dist/state/EditorContext.d.ts +3 -25
  44. package/dist/state/EditorContext.d.ts.map +1 -1
  45. package/dist/state/EditorContext.js +124 -174
  46. package/dist/state/reducer.d.ts +1 -5
  47. package/dist/state/reducer.d.ts.map +1 -1
  48. package/dist/state/reducer.js +128 -521
  49. package/dist/state/types.d.ts +12 -1
  50. package/dist/state/types.d.ts.map +1 -1
  51. package/dist/types/block.d.ts +9 -0
  52. package/dist/types/block.d.ts.map +1 -1
  53. package/dist/types/post.d.ts +17 -1
  54. package/dist/types/post.d.ts.map +1 -1
  55. package/dist/views/CanvasEditor/BlockWrapper.d.ts +5 -6
  56. package/dist/views/CanvasEditor/BlockWrapper.d.ts.map +1 -1
  57. package/dist/views/CanvasEditor/BlockWrapper.js +56 -264
  58. package/dist/views/CanvasEditor/CanvasEditorView.d.ts +5 -3
  59. package/dist/views/CanvasEditor/CanvasEditorView.d.ts.map +1 -1
  60. package/dist/views/CanvasEditor/CanvasEditorView.js +55 -315
  61. package/dist/views/CanvasEditor/EditorBody.d.ts +6 -8
  62. package/dist/views/CanvasEditor/EditorBody.d.ts.map +1 -1
  63. package/dist/views/CanvasEditor/EditorBody.js +34 -482
  64. package/dist/views/CanvasEditor/EditorHeader.d.ts.map +1 -1
  65. package/dist/views/CanvasEditor/EditorHeader.js +27 -63
  66. package/dist/views/CanvasEditor/LayoutContainer.d.ts.map +1 -1
  67. package/dist/views/CanvasEditor/LayoutContainer.js +49 -70
  68. package/dist/views/CanvasEditor/components/CustomBlockItem.js +1 -1
  69. package/dist/views/CanvasEditor/components/EditorCanvas.d.ts +15 -3
  70. package/dist/views/CanvasEditor/components/EditorCanvas.d.ts.map +1 -1
  71. package/dist/views/CanvasEditor/components/EditorCanvas.js +40 -18
  72. package/dist/views/CanvasEditor/components/EditorLibrary.d.ts +5 -1
  73. package/dist/views/CanvasEditor/components/EditorLibrary.d.ts.map +1 -1
  74. package/dist/views/CanvasEditor/components/EditorLibrary.js +11 -7
  75. package/dist/views/CanvasEditor/components/EditorSidebar.d.ts.map +1 -1
  76. package/dist/views/CanvasEditor/components/EditorSidebar.js +32 -14
  77. package/dist/views/CanvasEditor/components/FeaturedMediaSection.d.ts +0 -6
  78. package/dist/views/CanvasEditor/components/FeaturedMediaSection.d.ts.map +1 -1
  79. package/dist/views/CanvasEditor/components/FeaturedMediaSection.js +17 -128
  80. package/dist/views/CanvasEditor/components/JSONInspector.d.ts +9 -0
  81. package/dist/views/CanvasEditor/components/JSONInspector.d.ts.map +1 -0
  82. package/dist/views/CanvasEditor/components/JSONInspector.js +56 -0
  83. package/dist/views/CanvasEditor/components/LibraryItem.js +2 -2
  84. package/dist/views/CanvasEditor/components/PrivacySettingsSection.d.ts +0 -4
  85. package/dist/views/CanvasEditor/components/PrivacySettingsSection.d.ts.map +1 -1
  86. package/dist/views/CanvasEditor/components/PrivacySettingsSection.js +6 -28
  87. package/dist/views/CanvasEditor/components/index.d.ts +2 -0
  88. package/dist/views/CanvasEditor/components/index.d.ts.map +1 -1
  89. package/dist/views/CanvasEditor/components/index.js +1 -0
  90. package/dist/views/CanvasEditor/hooks/useHeroBlock.d.ts.map +1 -1
  91. package/dist/views/CanvasEditor/hooks/useHeroBlock.js +15 -18
  92. package/dist/views/CanvasEditor/hooks/usePostLoader.d.ts +3 -0
  93. package/dist/views/CanvasEditor/hooks/usePostLoader.d.ts.map +1 -1
  94. package/dist/views/CanvasEditor/hooks/usePostLoader.js +12 -13
  95. package/dist/views/CanvasEditor/hooks/useUnsavedChanges.js +0 -4
  96. package/dist/views/PostManager/EmptyState.d.ts +1 -1
  97. package/dist/views/PostManager/EmptyState.js +4 -4
  98. package/dist/views/PostManager/FilterDropdown.d.ts +21 -0
  99. package/dist/views/PostManager/FilterDropdown.d.ts.map +1 -0
  100. package/dist/views/PostManager/FilterDropdown.js +28 -0
  101. package/dist/views/PostManager/LanguageFlags.d.ts.map +1 -1
  102. package/dist/views/PostManager/LanguageFlags.js +4 -1
  103. package/dist/views/PostManager/PostCards.d.ts.map +1 -1
  104. package/dist/views/PostManager/PostCards.js +23 -40
  105. package/dist/views/PostManager/PostFilters.d.ts.map +1 -1
  106. package/dist/views/PostManager/PostFilters.js +34 -3
  107. package/dist/views/PostManager/PostManagerView.d.ts +1 -2
  108. package/dist/views/PostManager/PostManagerView.d.ts.map +1 -1
  109. package/dist/views/PostManager/PostManagerView.js +30 -96
  110. package/dist/views/PostManager/PostStats.d.ts.map +1 -1
  111. package/dist/views/PostManager/PostStats.js +10 -10
  112. package/dist/views/PostManager/PostTable.d.ts.map +1 -1
  113. package/dist/views/PostManager/PostTable.js +23 -40
  114. package/dist/views/Settings/SettingsView.d.ts +1 -1
  115. package/dist/views/Settings/SettingsView.d.ts.map +1 -1
  116. package/dist/views/Settings/SettingsView.js +12 -39
  117. package/dist/views/SlugSEO/SlugSEOManagerView.d.ts.map +1 -1
  118. package/dist/views/SlugSEO/SlugSEOManagerView.js +2 -2
  119. package/package.json +42 -6
  120. package/src/api/categories.ts +48 -52
  121. package/src/api/handler.ts +87 -604
  122. package/src/api/router.ts +15 -65
  123. package/src/api/service.ts +241 -0
  124. package/src/hooks/useAutoSave.ts +64 -0
  125. package/src/hooks/useCategories.ts +19 -47
  126. package/src/index.tsx +79 -293
  127. package/src/init.tsx +24 -11
  128. package/src/lib/blocks/BlockRenderer.tsx +1 -0
  129. package/src/lib/layouts/blocks/ColumnsBlock.tsx +60 -173
  130. package/src/lib/layouts/blocks/SectionBlock.tsx +22 -26
  131. package/src/lib/layouts/index.ts +4 -4
  132. package/src/lib/mappers/apiMapper.ts +63 -32
  133. package/src/lib/rich-text/RichTextEditor.tsx +16 -9
  134. package/src/lib/utils/config-resolver.ts +64 -0
  135. package/src/lib/utils/tree.ts +150 -0
  136. package/src/state/EditorContext.tsx +153 -232
  137. package/src/state/reducer.ts +141 -606
  138. package/src/state/types.ts +14 -1
  139. package/src/types/block.ts +10 -0
  140. package/src/types/post.ts +19 -1
  141. package/src/views/CanvasEditor/BlockWrapper.tsx +130 -460
  142. package/src/views/CanvasEditor/CanvasEditorView.tsx +145 -420
  143. package/src/views/CanvasEditor/EditorBody.tsx +98 -610
  144. package/src/views/CanvasEditor/EditorHeader.tsx +176 -196
  145. package/src/views/CanvasEditor/LayoutContainer.tsx +74 -89
  146. package/src/views/CanvasEditor/components/CustomBlockItem.tsx +7 -8
  147. package/src/views/CanvasEditor/components/EditorCanvas.tsx +139 -84
  148. package/src/views/CanvasEditor/components/EditorLibrary.tsx +25 -10
  149. package/src/views/CanvasEditor/components/EditorSidebar.tsx +196 -127
  150. package/src/views/CanvasEditor/components/FeaturedMediaSection.tsx +78 -210
  151. package/src/views/CanvasEditor/components/JSONInspector.tsx +125 -0
  152. package/src/views/CanvasEditor/components/LibraryItem.tsx +5 -6
  153. package/src/views/CanvasEditor/components/PrivacySettingsSection.tsx +73 -124
  154. package/src/views/CanvasEditor/components/index.ts +2 -1
  155. package/src/views/CanvasEditor/hooks/useHeroBlock.ts +15 -18
  156. package/src/views/CanvasEditor/hooks/usePostLoader.ts +21 -13
  157. package/src/views/CanvasEditor/hooks/useUnsavedChanges.ts +4 -4
  158. package/src/views/PostManager/EmptyState.tsx +9 -10
  159. package/src/views/PostManager/FilterDropdown.tsx +95 -0
  160. package/src/views/PostManager/LanguageFlags.tsx +6 -2
  161. package/src/views/PostManager/PostCards.tsx +127 -133
  162. package/src/views/PostManager/PostFilters.tsx +73 -68
  163. package/src/views/PostManager/PostManagerView.tsx +132 -179
  164. package/src/views/PostManager/PostStats.tsx +21 -20
  165. package/src/views/PostManager/PostTable.tsx +137 -165
  166. package/src/views/Settings/SettingsView.tsx +64 -180
  167. package/src/views/SlugSEO/SlugSEOManagerView.tsx +59 -44
  168. package/src/hooks/index.d.ts +0 -8
  169. package/src/hooks/index.d.ts.map +0 -1
  170. package/src/hooks/useBlog.d.ts +0 -31
  171. package/src/hooks/useBlog.d.ts.map +0 -1
  172. package/src/hooks/useBlogs.d.ts +0 -39
  173. package/src/hooks/useBlogs.d.ts.map +0 -1
  174. package/src/hooks/useCategories.d.ts +0 -9
  175. package/src/hooks/useCategories.d.ts.map +0 -1
  176. package/src/lib/blocks/BlockRenderer.d.ts +0 -54
  177. package/src/lib/blocks/BlockRenderer.d.ts.map +0 -1
  178. package/src/lib/config-storage.d.ts +0 -30
  179. package/src/lib/config-storage.d.ts.map +0 -1
  180. package/src/lib/layouts/blocks/ColumnsBlock.d.ts +0 -25
  181. package/src/lib/layouts/blocks/ColumnsBlock.d.ts.map +0 -1
  182. package/src/lib/layouts/blocks/SectionBlock.d.ts +0 -25
  183. package/src/lib/layouts/blocks/SectionBlock.d.ts.map +0 -1
  184. package/src/lib/layouts/index.d.ts +0 -23
  185. package/src/lib/layouts/index.d.ts.map +0 -1
  186. package/src/lib/layouts/registerLayoutBlocks.d.ts +0 -9
  187. package/src/lib/layouts/registerLayoutBlocks.d.ts.map +0 -1
  188. package/src/lib/mappers/apiMapper.d.ts +0 -66
  189. package/src/lib/mappers/apiMapper.d.ts.map +0 -1
  190. package/src/lib/rich-text/RichTextEditor.d.ts +0 -45
  191. package/src/lib/rich-text/RichTextEditor.d.ts.map +0 -1
  192. package/src/lib/rich-text/RichTextPreview.d.ts +0 -16
  193. package/src/lib/rich-text/RichTextPreview.d.ts.map +0 -1
  194. package/src/lib/rich-text/index.d.ts +0 -9
  195. package/src/lib/rich-text/index.d.ts.map +0 -1
  196. package/src/lib/utils/blockHelpers.d.ts +0 -23
  197. package/src/lib/utils/blockHelpers.d.ts.map +0 -1
  198. package/src/lib/utils/configValidation.d.ts +0 -23
  199. package/src/lib/utils/configValidation.d.ts.map +0 -1
  200. package/src/registry/BlockRegistry.d.ts +0 -62
  201. package/src/registry/BlockRegistry.d.ts.map +0 -1
  202. package/src/registry/index.d.ts +0 -6
  203. package/src/registry/index.d.ts.map +0 -1
  204. package/src/state/EditorContext.d.ts +0 -45
  205. package/src/state/EditorContext.d.ts.map +0 -1
  206. package/src/state/index.d.ts +0 -7
  207. package/src/state/index.d.ts.map +0 -1
  208. package/src/state/reducer.d.ts +0 -11
  209. package/src/state/reducer.d.ts.map +0 -1
  210. package/src/state/types.d.ts +0 -162
  211. package/src/state/types.d.ts.map +0 -1
  212. package/src/types/block.d.ts +0 -221
  213. package/src/types/block.d.ts.map +0 -1
  214. package/src/types/index.d.ts +0 -8
  215. package/src/types/index.d.ts.map +0 -1
  216. package/src/types/post.d.ts +0 -136
  217. package/src/types/post.d.ts.map +0 -1
  218. package/src/utils/client.d.ts +0 -48
  219. package/src/utils/client.d.ts.map +0 -1
  220. package/src/views/CanvasEditor/BlockWrapper.d.ts +0 -16
  221. package/src/views/CanvasEditor/BlockWrapper.d.ts.map +0 -1
  222. package/src/views/CanvasEditor/CanvasEditorView.d.ts +0 -14
  223. package/src/views/CanvasEditor/CanvasEditorView.d.ts.map +0 -1
  224. package/src/views/CanvasEditor/EditorBody.d.ts +0 -22
  225. package/src/views/CanvasEditor/EditorBody.d.ts.map +0 -1
  226. package/src/views/CanvasEditor/EditorHeader.d.ts +0 -18
  227. package/src/views/CanvasEditor/EditorHeader.d.ts.map +0 -1
  228. package/src/views/CanvasEditor/LayoutContainer.d.ts +0 -17
  229. package/src/views/CanvasEditor/LayoutContainer.d.ts.map +0 -1
  230. package/src/views/CanvasEditor/SaveConfirmationModal.d.ts +0 -13
  231. package/src/views/CanvasEditor/SaveConfirmationModal.d.ts.map +0 -1
  232. package/src/views/CanvasEditor/components/CustomBlockItem.d.ts +0 -14
  233. package/src/views/CanvasEditor/components/CustomBlockItem.d.ts.map +0 -1
  234. package/src/views/CanvasEditor/components/EditorCanvas.d.ts +0 -29
  235. package/src/views/CanvasEditor/components/EditorCanvas.d.ts.map +0 -1
  236. package/src/views/CanvasEditor/components/EditorLibrary.d.ts +0 -7
  237. package/src/views/CanvasEditor/components/EditorLibrary.d.ts.map +0 -1
  238. package/src/views/CanvasEditor/components/EditorSidebar.d.ts +0 -13
  239. package/src/views/CanvasEditor/components/EditorSidebar.d.ts.map +0 -1
  240. package/src/views/CanvasEditor/components/ErrorBanner.d.ts +0 -6
  241. package/src/views/CanvasEditor/components/ErrorBanner.d.ts.map +0 -1
  242. package/src/views/CanvasEditor/components/FeaturedMediaSection.d.ts +0 -25
  243. package/src/views/CanvasEditor/components/FeaturedMediaSection.d.ts.map +0 -1
  244. package/src/views/CanvasEditor/components/LibraryItem.d.ts +0 -14
  245. package/src/views/CanvasEditor/components/LibraryItem.d.ts.map +0 -1
  246. package/src/views/CanvasEditor/components/PrivacySettingsSection.d.ts +0 -15
  247. package/src/views/CanvasEditor/components/PrivacySettingsSection.d.ts.map +0 -1
  248. package/src/views/CanvasEditor/components/index.d.ts +0 -21
  249. package/src/views/CanvasEditor/components/index.d.ts.map +0 -1
  250. package/src/views/CanvasEditor/hooks/index.d.ts +0 -10
  251. package/src/views/CanvasEditor/hooks/index.d.ts.map +0 -1
  252. package/src/views/CanvasEditor/hooks/useHeroBlock.d.ts +0 -8
  253. package/src/views/CanvasEditor/hooks/useHeroBlock.d.ts.map +0 -1
  254. package/src/views/CanvasEditor/hooks/useKeyboardShortcuts.d.ts +0 -3
  255. package/src/views/CanvasEditor/hooks/useKeyboardShortcuts.d.ts.map +0 -1
  256. package/src/views/CanvasEditor/hooks/usePostLoader.d.ts +0 -5
  257. package/src/views/CanvasEditor/hooks/usePostLoader.d.ts.map +0 -1
  258. package/src/views/CanvasEditor/hooks/useRegisteredBlocks.d.ts +0 -2
  259. package/src/views/CanvasEditor/hooks/useRegisteredBlocks.d.ts.map +0 -1
  260. package/src/views/CanvasEditor/hooks/useUnsavedChanges.d.ts +0 -25
  261. package/src/views/CanvasEditor/hooks/useUnsavedChanges.d.ts.map +0 -1
  262. package/src/views/CanvasEditor/index.d.ts +0 -16
  263. package/src/views/CanvasEditor/index.d.ts.map +0 -1
  264. package/src/views/PostManager/EmptyState.d.ts +0 -10
  265. package/src/views/PostManager/EmptyState.d.ts.map +0 -1
  266. package/src/views/PostManager/PostActionsMenu.d.ts +0 -12
  267. package/src/views/PostManager/PostActionsMenu.d.ts.map +0 -1
  268. package/src/views/PostManager/PostCards.d.ts +0 -15
  269. package/src/views/PostManager/PostCards.d.ts.map +0 -1
  270. package/src/views/PostManager/PostFilters.d.ts +0 -16
  271. package/src/views/PostManager/PostFilters.d.ts.map +0 -1
  272. package/src/views/PostManager/PostManagerView.d.ts +0 -11
  273. package/src/views/PostManager/PostManagerView.d.ts.map +0 -1
  274. package/src/views/PostManager/PostStats.d.ts +0 -11
  275. package/src/views/PostManager/PostStats.d.ts.map +0 -1
  276. package/src/views/PostManager/PostTable.d.ts +0 -15
  277. package/src/views/PostManager/PostTable.d.ts.map +0 -1
  278. package/src/views/PostManager/index.d.ts +0 -12
  279. package/src/views/PostManager/index.d.ts.map +0 -1
  280. package/src/views/Preview/PreviewBridgeView.d.ts +0 -12
  281. package/src/views/Preview/PreviewBridgeView.d.ts.map +0 -1
  282. package/src/views/Preview/index.d.ts +0 -6
  283. package/src/views/Preview/index.d.ts.map +0 -1
  284. package/src/views/Settings/SettingsView.d.ts +0 -10
  285. package/src/views/Settings/SettingsView.d.ts.map +0 -1
  286. package/src/views/Settings/index.d.ts +0 -6
  287. package/src/views/Settings/index.d.ts.map +0 -1
  288. package/src/views/SlugSEO/SlugSEOManagerView.d.ts +0 -12
  289. package/src/views/SlugSEO/SlugSEOManagerView.d.ts.map +0 -1
  290. package/src/views/SlugSEO/index.d.ts +0 -6
  291. package/src/views/SlugSEO/index.d.ts.map +0 -1
package/src/api/router.ts CHANGED
@@ -3,9 +3,6 @@
3
3
  /**
4
4
  * Plugin Blog API Router
5
5
  * Centralized API handler for all blog plugin routes
6
- *
7
- * This router handles requests to /api/plugin-blog/*
8
- * and routes them to the appropriate handler
9
6
  */
10
7
 
11
8
  import { NextRequest, NextResponse } from 'next/server';
@@ -26,15 +23,12 @@ export interface BlogApiRouterConfig {
26
23
 
27
24
  /**
28
25
  * Handle blog API requests
29
- * Routes requests to appropriate handlers based on path
30
- * Similar to plugin-dep, accepts config directly instead of requiring initialization
31
26
  */
32
27
  export async function handleBlogApi(
33
28
  req: NextRequest,
34
29
  path: string[],
35
30
  config: BlogApiRouterConfig
36
31
  ): Promise<NextResponse> {
37
- // Create the blog API config from the router config
38
32
  const blogApiConfig = createBlogApiConfig({
39
33
  getDb: config.getDb,
40
34
  getUserId: config.getUserId,
@@ -42,87 +36,43 @@ export async function handleBlogApi(
42
36
  });
43
37
 
44
38
  const method = req.method;
45
- // Handle empty path array - means we're at /api/plugin-blog
46
- // Ensure path is always an array
47
39
  const safePath = Array.isArray(path) ? path : [];
48
40
  const route = safePath.length > 0 ? safePath[0] : '';
49
41
 
50
42
  try {
51
- // Route: /api/plugin-blog (list/create) - empty path or 'list'
52
- // This handles both /api/plugin-blog and /api/plugin-blog?limit=3
53
43
  if (!route || route === 'list') {
44
+ if (method === 'GET') return await BlogListHandler(req, blogApiConfig);
45
+ if (method === 'POST') return await BlogCreateHandler(req, blogApiConfig);
46
+ } else if (route === 'new') {
47
+ if (method === 'POST') return await BlogCreateHandler(req, blogApiConfig);
48
+ } else if (route === 'categories') {
54
49
  if (method === 'GET') {
55
- return await BlogListHandler(req, blogApiConfig);
56
- }
57
- if (method === 'POST') {
58
- return await BlogCreateHandler(req, blogApiConfig);
59
- }
60
- // Method not allowed for root route
61
- return NextResponse.json(
62
- { error: `Method ${method} not allowed for route: /` },
63
- { status: 405 }
64
- );
65
- }
66
- // Route: /api/plugin-blog/new (create new)
67
- else if (route === 'new') {
68
- if (method === 'POST') {
69
- return await BlogCreateHandler(req, blogApiConfig);
70
- }
71
- }
72
- // Route: /api/plugin-blog/categories (get categories)
73
- else if (route === 'categories') {
74
- if (method === 'GET') {
75
- // Import categories handler
76
50
  const categoriesModule = await import('./categories');
77
51
  return await categoriesModule.GET(req, blogApiConfig);
78
52
  }
79
- }
80
- // Route: /api/plugin-blog/check-title (check title duplicate)
81
- else if (route === 'check-title') {
53
+ } else if (route === 'check-title') {
82
54
  if (method === 'GET') {
83
55
  const checkTitleModule = await import('./check-title');
84
56
  return await checkTitleModule.GET(req, blogApiConfig);
85
57
  }
86
- }
87
- // Route: /api/plugin-blog/config (get/save plugin config)
88
- else if (route === 'config') {
58
+ } else if (route === 'config') {
89
59
  const configApiConfig = {
90
60
  getDb: config.getDb,
91
61
  getUserId: config.getUserId,
92
62
  siteId: config.siteId || 'default',
93
63
  };
94
- if (method === 'GET') {
95
- return await ConfigGetHandler(req, configApiConfig);
96
- }
97
- if (method === 'POST') {
98
- return await ConfigPostHandler(req, configApiConfig);
99
- }
100
- }
101
- // Route: /api/plugin-blog/[slug] (get/update/delete by slug)
102
- else {
64
+ if (method === 'GET') return await ConfigGetHandler(req, configApiConfig);
65
+ if (method === 'POST') return await ConfigPostHandler(req, configApiConfig);
66
+ } else {
103
67
  const slug = route;
104
- if (method === 'GET') {
105
- return await BlogGetHandler(req, { params: Promise.resolve({ slug }) }, blogApiConfig);
106
- }
107
- if (method === 'PUT') {
108
- return await BlogUpdateHandler(req, { params: Promise.resolve({ slug }) }, blogApiConfig);
109
- }
110
- if (method === 'DELETE') {
111
- return await BlogDeleteHandler(req, { params: Promise.resolve({ slug }) }, blogApiConfig);
112
- }
68
+ if (method === 'GET') return await BlogGetHandler(req, { params: Promise.resolve({ slug }) }, blogApiConfig);
69
+ if (method === 'PUT') return await BlogUpdateHandler(req, { params: Promise.resolve({ slug }) }, blogApiConfig);
70
+ if (method === 'DELETE') return await BlogDeleteHandler(req, { params: Promise.resolve({ slug }) }, blogApiConfig);
113
71
  }
114
72
 
115
- // Method not allowed
116
- return NextResponse.json(
117
- { error: `Method ${method} not allowed for route: ${route || '/'}` },
118
- { status: 405 }
119
- );
73
+ return NextResponse.json({ error: `Method ${method} not allowed for route: ${route || '/'}` }, { status: 405 });
120
74
  } catch (error: any) {
121
75
  console.error('[BlogApiRouter] Error:', error);
122
- return NextResponse.json(
123
- { error: error.message || 'Internal server error' },
124
- { status: 500 }
125
- );
76
+ return NextResponse.json({ error: error.message || 'Internal server error' }, { status: 500 });
126
77
  }
127
78
  }
128
-
@@ -0,0 +1,241 @@
1
+ /**
2
+ * Blog API Service
3
+ * Shared server-side logic for MongoDB operations and language fallbacks
4
+ */
5
+
6
+ import { BlogApiConfig } from './handler';
7
+ import { slugify } from '../lib/utils/slugify';
8
+
9
+ export class BlogService {
10
+ constructor(private config: BlogApiConfig) {}
11
+
12
+ /**
13
+ * Common aggregation pipeline for including author data
14
+ */
15
+ private getAuthorLookupPipeline() {
16
+ return [
17
+ {
18
+ $lookup: {
19
+ from: 'users',
20
+ let: { authorId: '$authorId' },
21
+ pipeline: [
22
+ { $match: { $expr: { $or: [{ $eq: ['$_id', '$$authorId'] }, { $eq: [{ $toString: '$_id' }, '$$authorId'] }] } } },
23
+ { $project: { password: 0 } }
24
+ ],
25
+ as: 'author'
26
+ }
27
+ },
28
+ { $addFields: { author: { $arrayElemAt: ['$author', 0] } } }
29
+ ];
30
+ }
31
+
32
+ /**
33
+ * List blogs with filters and language fallbacks
34
+ */
35
+ async listBlogs(options: {
36
+ limit: number;
37
+ skip: number;
38
+ status?: string;
39
+ isAdmin: boolean;
40
+ userId?: string | null;
41
+ requestedLanguage: string;
42
+ }) {
43
+ const dbConn = await this.config.getDb();
44
+ const db = dbConn.db();
45
+ const collection = db.collection(this.config.collectionName || 'blogs');
46
+
47
+ let query: any = {};
48
+ if (options.isAdmin && options.userId) {
49
+ if (options.status) query = { 'publicationData.status': options.status, authorId: options.userId };
50
+ else query = { authorId: options.userId };
51
+ } else {
52
+ query = {
53
+ [`languages.${options.requestedLanguage}.status`]: 'published',
54
+ 'publicationData.date': { $lte: new Date() },
55
+ };
56
+ }
57
+
58
+ const pipeline: any[] = [
59
+ { $match: query },
60
+ { $sort: { 'publicationData.date': -1 } },
61
+ { $skip: options.skip },
62
+ { $limit: options.isAdmin ? 1000 : options.limit },
63
+ ...this.getAuthorLookupPipeline()
64
+ ];
65
+
66
+ const [data, totalCount] = await Promise.all([
67
+ collection.aggregate(pipeline).toArray(),
68
+ collection.countDocuments(query),
69
+ ]);
70
+
71
+ const formatted = data.map((doc: any) => this.formatWithLanguage(doc, options.requestedLanguage, options.isAdmin)).filter(Boolean);
72
+ return { blogs: formatted, total: totalCount };
73
+ }
74
+
75
+ /**
76
+ * Get single blog by slug
77
+ */
78
+ async getBlogBySlug(slug: string, requestedLanguage: string, isAdmin: boolean = false) {
79
+ const dbConn = await this.config.getDb();
80
+ const db = dbConn.db();
81
+ const collection = db.collection(this.config.collectionName || 'blogs');
82
+
83
+ const pipeline = [{ $match: { slug } }, ...this.getAuthorLookupPipeline()];
84
+ const results = await collection.aggregate(pipeline).toArray();
85
+ const blog = results[0];
86
+
87
+ if (!blog) return null;
88
+ return this.formatWithLanguage(blog, requestedLanguage, isAdmin);
89
+ }
90
+
91
+ /**
92
+ * Create or update the language-specific document structure
93
+ */
94
+ prepareUpdateData(body: any, existingBlog: any | null, language: string) {
95
+ const { title, summary, contentBlocks, image, categoryTags, publicationData, seo } = body;
96
+ const now = new Date();
97
+ const finalStatus = publicationData?.status || 'concept';
98
+
99
+ // Generate a localized slug for this specific language edition
100
+ // If it's not published, we add a draft suffix
101
+ const isPublishing = finalStatus === 'published';
102
+ let localizedSlug = slugify(title);
103
+
104
+ // Only append draft suffix if NOT publishing
105
+ if (!isPublishing) {
106
+ localizedSlug = `${localizedSlug}-draft-${Date.now().toString().slice(-4)}`;
107
+ }
108
+
109
+
110
+
111
+ const langData = {
112
+ blocks: contentBlocks || [],
113
+ metadata: {
114
+ title: title.trim(),
115
+ slug: localizedSlug, // Store the slug inside the language object
116
+ excerpt: (summary || '').trim(),
117
+ featuredImage: image,
118
+ categories: categoryTags?.category ? [categoryTags.category] : [],
119
+ tags: categoryTags?.tags || [],
120
+ seo: seo || {},
121
+ },
122
+ updatedAt: now.toISOString(),
123
+ status: finalStatus,
124
+ };
125
+
126
+ const updatedLanguages = {
127
+ ...(existingBlog?.languages || {}),
128
+ [language]: langData,
129
+ };
130
+
131
+ // Determine global status based on ALL languages
132
+ // If at least ONE language is published, the global status is 'published'
133
+ // This ensures a new draft edition doesn't hide existing published editions
134
+ const anyPublished = Object.values(updatedLanguages).some((l: any) => l.status === 'published');
135
+ const globalStatus = anyPublished ? 'published' : 'draft';
136
+
137
+ // Update root slug if this is the primary language
138
+ const isPrimaryLanguage = !existingBlog || language === (existingBlog.metadata?.lang || 'nl');
139
+ const rootSlug = isPrimaryLanguage ? localizedSlug : (existingBlog?.slug || localizedSlug);
140
+
141
+ return {
142
+ // Keep the primary title/summary but allow them to be updated
143
+ title: title.trim(),
144
+ slug: rootSlug, // Include root slug in update
145
+ summary: (summary || '').trim(),
146
+ contentBlocks: contentBlocks || [],
147
+ image: image || {},
148
+ categoryTags: {
149
+ category: categoryTags?.category?.trim() || '',
150
+ tags: categoryTags?.tags || [],
151
+ },
152
+ publicationData: {
153
+ ...(existingBlog?.publicationData || {}),
154
+ ...publicationData,
155
+ status: globalStatus,
156
+ date: publicationData?.date ? new Date(publicationData.date) : (existingBlog?.publicationData?.date || now),
157
+ },
158
+ languages: updatedLanguages,
159
+ updatedAt: now,
160
+ };
161
+ }
162
+
163
+ /**
164
+ * Format a document for a specific language
165
+ */
166
+ private formatWithLanguage(doc: any, requestedLanguage: string, isAdmin: boolean) {
167
+ const languages = doc.languages || {};
168
+ let bestLanguage = requestedLanguage;
169
+ let isMissingTranslation = !languages[requestedLanguage];
170
+
171
+ if (isMissingTranslation && !isAdmin) {
172
+ const fallbacks = [doc.metadata?.lang || 'nl', 'nl', 'en'];
173
+ for (const lang of fallbacks) {
174
+ if (languages[lang]) { bestLanguage = lang; isMissingTranslation = false; break; }
175
+ }
176
+ if (isMissingTranslation) return null; // Public view requires translation or fallback
177
+ }
178
+
179
+ const content = languages[bestLanguage] || {};
180
+ const meta = content.metadata || {};
181
+
182
+ const status = isMissingTranslation ? 'draft' : (content.status || 'draft');
183
+
184
+ // --- ROBUST SLUG RESOLUTION ---
185
+ let resolvedSlug = doc.slug;
186
+ if (!isMissingTranslation) {
187
+ if (meta.slug) {
188
+ resolvedSlug = meta.slug;
189
+ } else if (meta.title) {
190
+ resolvedSlug = slugify(meta.title);
191
+ if (status !== 'published') resolvedSlug += `-draft-${Date.now().toString().slice(-4)}`;
192
+ }
193
+ }
194
+
195
+ return {
196
+ id: doc.id || doc._id?.toString(),
197
+ // Prefer localized slug if available, fallback to root slug or generated one
198
+ slug: resolvedSlug,
199
+ // If missing translation, provide root content as template but keep status 'not-translated'
200
+ title: isMissingTranslation ? (doc.title || '') : (meta.title || doc.title || ''),
201
+ summary: isMissingTranslation ? (doc.summary || '') : (meta.excerpt || doc.summary || ''),
202
+ contentBlocks: isMissingTranslation ? (doc.contentBlocks || []) : (content.blocks || doc.contentBlocks || []),
203
+ image: isMissingTranslation ? doc.image : (meta.featuredImage || doc.image),
204
+ categoryTags: isMissingTranslation ? { category: '', tags: [] } : {
205
+ category: meta.categories?.[0] || '',
206
+ tags: meta.tags || []
207
+ },
208
+ lang: bestLanguage,
209
+ isMissingTranslation,
210
+ requestedLanguage,
211
+ availableLanguages: Object.keys(languages),
212
+ updatedAt: doc.updatedAt || doc.publicationData?.date || new Date().toISOString(),
213
+ // Include a lightweight summary of all languages for admin view (tooltips)
214
+ languages: isAdmin ? (() => {
215
+ const summary: any = {};
216
+ // Use Object.keys(languages) to ensure we iterate over all existing translations
217
+ Object.keys(languages).forEach(lang => {
218
+ const lData = languages[lang] || {};
219
+ // Normalize status: handle 'concept' -> 'draft'
220
+ let langStatus = lData.status || 'draft';
221
+ if (langStatus === 'concept') langStatus = 'draft';
222
+
223
+ summary[lang] = {
224
+ status: langStatus,
225
+ metadata: {
226
+ title: lData.metadata?.title || doc.title || 'Untitled'
227
+ }
228
+ };
229
+ });
230
+ return summary;
231
+ })() : undefined,
232
+ status: status,
233
+ publication: {
234
+ status: status,
235
+ // FORCE undefined date for new editions so they don't look published or inherit old dates
236
+ date: isMissingTranslation ? undefined : (doc.publicationData?.date || doc.updatedAt)
237
+ },
238
+ author: doc.author ? { name: doc.author.name, image: doc.author.image, displayRole: doc.author.displayRole } : undefined
239
+ };
240
+ }
241
+ }
@@ -0,0 +1,64 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect, useRef } from 'react';
4
+
5
+ /**
6
+ * Hook for automatic saving of editor state
7
+ */
8
+ export function useAutoSave(
9
+ postId: string | undefined,
10
+ state: any,
11
+ onSave: (state: any) => Promise<void>,
12
+ delay = 10000
13
+ ) {
14
+ const [autoSaveEnabled, setAutoSaveEnabled] = useState(true);
15
+ const [saveStatus, setSaveStatus] = useState<'idle' | 'saving' | 'saved' | 'error'>('idle');
16
+ const [countdown, setCountdown] = useState<number | null>(null);
17
+ const timerRef = useRef<NodeJS.Timeout | null>(null);
18
+ const countdownIntervalRef = useRef<NodeJS.Timeout | null>(null);
19
+
20
+ useEffect(() => {
21
+ if (!autoSaveEnabled || !state.isDirty || !postId) {
22
+ setCountdown(null);
23
+ if (timerRef.current) clearTimeout(timerRef.current);
24
+ if (countdownIntervalRef.current) clearInterval(countdownIntervalRef.current);
25
+ return;
26
+ }
27
+
28
+ // Reset timer on state change
29
+ if (timerRef.current) clearTimeout(timerRef.current);
30
+ if (countdownIntervalRef.current) clearInterval(countdownIntervalRef.current);
31
+
32
+ const startTime = Date.now();
33
+ setCountdown(Math.ceil(delay / 1000));
34
+
35
+ countdownIntervalRef.current = setInterval(() => {
36
+ const remaining = Math.max(0, delay - (Date.now() - startTime));
37
+ setCountdown(Math.ceil(remaining / 1000));
38
+ }, 1000);
39
+
40
+ timerRef.current = setTimeout(async () => {
41
+ setSaveStatus('saving');
42
+ try {
43
+ await onSave(state);
44
+ setSaveStatus('saved');
45
+ setTimeout(() => setSaveStatus('idle'), 2000);
46
+ } catch (error) {
47
+ setSaveStatus('error');
48
+ setTimeout(() => setSaveStatus('idle'), 3000);
49
+ }
50
+ }, delay);
51
+
52
+ return () => {
53
+ if (timerRef.current) clearTimeout(timerRef.current);
54
+ if (countdownIntervalRef.current) clearInterval(countdownIntervalRef.current);
55
+ };
56
+ }, [state, autoSaveEnabled, postId, delay, onSave]);
57
+
58
+ return {
59
+ autoSaveEnabled,
60
+ setAutoSaveEnabled,
61
+ saveStatus,
62
+ countdown
63
+ };
64
+ }
@@ -7,62 +7,34 @@
7
7
 
8
8
  import { useState, useEffect } from 'react';
9
9
 
10
- export function useCategories() {
10
+ export function useCategories(language?: string) {
11
11
  const [categories, setCategories] = useState<string[]>([]);
12
12
  const [loading, setLoading] = useState(true);
13
13
 
14
14
  useEffect(() => {
15
15
  const fetchCategories = async () => {
16
+
16
17
  try {
17
- const categorySet = new Set<string>();
18
+ // We rely on the specialized categories endpoint which is strictly filtered by the Orchestrator
19
+ const url = language
20
+ ? `/api/plugin-blog/categories?language=${language}`
21
+ : '/api/plugin-blog/categories';
18
22
 
19
- // 1. Fetch from categories endpoint (legacy categoryTags.category)
20
- try {
21
- const categoriesResponse = await fetch('/api/plugin-blog/categories');
22
- if (categoriesResponse.ok) {
23
- const categoriesData = await categoriesResponse.json();
24
- if (Array.isArray(categoriesData.categories)) {
25
- categoriesData.categories.forEach((cat: string) => {
26
- if (cat && cat.trim()) {
27
- categorySet.add(cat.trim());
28
- }
29
- });
30
- }
31
- }
32
- } catch (e) {
33
- // Categories endpoint not available, continue
34
- }
23
+
24
+ const response = await fetch(url);
35
25
 
36
- // 2. Fetch all blog posts and extract categories from Hero blocks
37
- try {
38
- const response = await fetch('/api/plugin-blog?admin=true&limit=1000');
39
- if (response.ok) {
40
- const data = await response.json();
41
- const posts = Array.isArray(data.blogs) ? data.blogs : (Array.isArray(data) ? data : []);
42
-
43
- posts.forEach((post: any) => {
44
- if (post.blocks && Array.isArray(post.blocks)) {
45
- // Find Hero block
46
- const heroBlock = post.blocks.find((block: any) => block.type === 'hero');
47
- if (heroBlock && heroBlock.data && heroBlock.data.category) {
48
- const category = heroBlock.data.category.trim();
49
- if (category) {
50
- categorySet.add(category);
51
- }
52
- }
53
- }
54
- });
55
- }
56
- } catch (e) {
57
- console.error('Failed to fetch posts for categories:', e);
26
+ if (response.ok) {
27
+ const data = await response.json();
28
+ const list = Array.isArray(data.categories) ? data.categories : [];
29
+
30
+
31
+ setCategories(list);
32
+ } else {
33
+ console.error(`[useCategories] API_ERROR: ${response.status}`);
34
+ setCategories([]);
58
35
  }
59
-
60
- // Convert to sorted array
61
- const sortedCategories = Array.from(categorySet).sort();
62
- setCategories(sortedCategories);
63
36
  } catch (error) {
64
- console.error('Failed to fetch categories:', error);
65
- // Fallback to empty array
37
+ console.error('[useCategories] CRITICAL_SYNC_FAILURE:', error);
66
38
  setCategories([]);
67
39
  } finally {
68
40
  setLoading(false);
@@ -70,7 +42,7 @@ export function useCategories() {
70
42
  };
71
43
 
72
44
  fetchCategories();
73
- }, []);
45
+ }, [language]);
74
46
 
75
47
  return { categories, loading };
76
48
  }