@jhits/plugin-blog 0.0.19 → 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.
- package/dist/api/categories.d.ts.map +1 -1
- package/dist/api/categories.js +42 -38
- package/dist/api/handler.d.ts +1 -26
- package/dist/api/handler.d.ts.map +1 -1
- package/dist/api/handler.js +81 -490
- package/dist/api/router.d.ts +0 -5
- package/dist/api/router.d.ts.map +1 -1
- package/dist/api/router.js +8 -35
- package/dist/api/service.d.ts +80 -0
- package/dist/api/service.d.ts.map +1 -0
- package/dist/api/service.js +219 -0
- package/dist/hooks/useAutoSave.d.ts +10 -0
- package/dist/hooks/useAutoSave.d.ts.map +1 -0
- package/dist/hooks/useAutoSave.js +57 -0
- package/dist/hooks/useCategories.d.ts +1 -1
- package/dist/hooks/useCategories.d.ts.map +1 -1
- package/dist/hooks/useCategories.js +15 -46
- package/dist/index.d.ts +24 -31
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +44 -201
- package/dist/init.d.ts +20 -7
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +8 -7
- package/dist/lib/blocks/BlockRenderer.d.ts.map +1 -1
- package/dist/lib/layouts/blocks/ColumnsBlock.d.ts.map +1 -1
- package/dist/lib/layouts/blocks/ColumnsBlock.js +30 -113
- package/dist/lib/layouts/blocks/SectionBlock.d.ts.map +1 -1
- package/dist/lib/layouts/blocks/SectionBlock.js +9 -21
- package/dist/lib/layouts/index.d.ts +3 -3
- package/dist/lib/layouts/index.js +4 -4
- package/dist/lib/mappers/apiMapper.d.ts +10 -0
- package/dist/lib/mappers/apiMapper.d.ts.map +1 -1
- package/dist/lib/mappers/apiMapper.js +47 -32
- package/dist/lib/rich-text/RichTextEditor.d.ts +4 -2
- package/dist/lib/rich-text/RichTextEditor.d.ts.map +1 -1
- package/dist/lib/rich-text/RichTextEditor.js +12 -9
- package/dist/lib/utils/config-resolver.d.ts +28 -0
- package/dist/lib/utils/config-resolver.d.ts.map +1 -0
- package/dist/lib/utils/config-resolver.js +46 -0
- package/dist/lib/utils/tree.d.ts +29 -0
- package/dist/lib/utils/tree.d.ts.map +1 -0
- package/dist/lib/utils/tree.js +129 -0
- package/dist/state/EditorContext.d.ts +3 -25
- package/dist/state/EditorContext.d.ts.map +1 -1
- package/dist/state/EditorContext.js +124 -174
- package/dist/state/reducer.d.ts +1 -5
- package/dist/state/reducer.d.ts.map +1 -1
- package/dist/state/reducer.js +128 -521
- package/dist/state/types.d.ts +12 -1
- package/dist/state/types.d.ts.map +1 -1
- package/dist/types/block.d.ts +9 -0
- package/dist/types/block.d.ts.map +1 -1
- package/dist/types/post.d.ts +17 -1
- package/dist/types/post.d.ts.map +1 -1
- package/dist/views/CanvasEditor/BlockWrapper.d.ts +5 -6
- package/dist/views/CanvasEditor/BlockWrapper.d.ts.map +1 -1
- package/dist/views/CanvasEditor/BlockWrapper.js +56 -264
- package/dist/views/CanvasEditor/CanvasEditorView.d.ts +5 -3
- package/dist/views/CanvasEditor/CanvasEditorView.d.ts.map +1 -1
- package/dist/views/CanvasEditor/CanvasEditorView.js +55 -315
- package/dist/views/CanvasEditor/EditorBody.d.ts +6 -8
- package/dist/views/CanvasEditor/EditorBody.d.ts.map +1 -1
- package/dist/views/CanvasEditor/EditorBody.js +34 -482
- package/dist/views/CanvasEditor/EditorHeader.d.ts.map +1 -1
- package/dist/views/CanvasEditor/EditorHeader.js +27 -63
- package/dist/views/CanvasEditor/LayoutContainer.d.ts.map +1 -1
- package/dist/views/CanvasEditor/LayoutContainer.js +49 -70
- package/dist/views/CanvasEditor/components/CustomBlockItem.js +1 -1
- package/dist/views/CanvasEditor/components/EditorCanvas.d.ts +15 -3
- package/dist/views/CanvasEditor/components/EditorCanvas.d.ts.map +1 -1
- package/dist/views/CanvasEditor/components/EditorCanvas.js +40 -18
- package/dist/views/CanvasEditor/components/EditorLibrary.d.ts +5 -1
- package/dist/views/CanvasEditor/components/EditorLibrary.d.ts.map +1 -1
- package/dist/views/CanvasEditor/components/EditorLibrary.js +11 -7
- package/dist/views/CanvasEditor/components/EditorSidebar.d.ts.map +1 -1
- package/dist/views/CanvasEditor/components/EditorSidebar.js +32 -14
- package/dist/views/CanvasEditor/components/FeaturedMediaSection.d.ts +0 -6
- package/dist/views/CanvasEditor/components/FeaturedMediaSection.d.ts.map +1 -1
- package/dist/views/CanvasEditor/components/FeaturedMediaSection.js +17 -128
- package/dist/views/CanvasEditor/components/JSONInspector.d.ts +9 -0
- package/dist/views/CanvasEditor/components/JSONInspector.d.ts.map +1 -0
- package/dist/views/CanvasEditor/components/JSONInspector.js +56 -0
- package/dist/views/CanvasEditor/components/LibraryItem.js +2 -2
- package/dist/views/CanvasEditor/components/PrivacySettingsSection.d.ts +0 -4
- package/dist/views/CanvasEditor/components/PrivacySettingsSection.d.ts.map +1 -1
- package/dist/views/CanvasEditor/components/PrivacySettingsSection.js +6 -28
- package/dist/views/CanvasEditor/components/index.d.ts +2 -0
- package/dist/views/CanvasEditor/components/index.d.ts.map +1 -1
- package/dist/views/CanvasEditor/components/index.js +1 -0
- package/dist/views/CanvasEditor/hooks/useHeroBlock.d.ts.map +1 -1
- package/dist/views/CanvasEditor/hooks/useHeroBlock.js +15 -18
- package/dist/views/CanvasEditor/hooks/usePostLoader.d.ts +3 -0
- package/dist/views/CanvasEditor/hooks/usePostLoader.d.ts.map +1 -1
- package/dist/views/CanvasEditor/hooks/usePostLoader.js +12 -13
- package/dist/views/CanvasEditor/hooks/useUnsavedChanges.js +0 -4
- package/dist/views/PostManager/EmptyState.d.ts +1 -1
- package/dist/views/PostManager/EmptyState.js +4 -4
- package/dist/views/PostManager/FilterDropdown.d.ts +21 -0
- package/dist/views/PostManager/FilterDropdown.d.ts.map +1 -0
- package/dist/views/PostManager/FilterDropdown.js +28 -0
- package/dist/views/PostManager/LanguageFlags.d.ts.map +1 -1
- package/dist/views/PostManager/LanguageFlags.js +4 -1
- package/dist/views/PostManager/PostCards.d.ts.map +1 -1
- package/dist/views/PostManager/PostCards.js +23 -40
- package/dist/views/PostManager/PostFilters.d.ts.map +1 -1
- package/dist/views/PostManager/PostFilters.js +34 -3
- package/dist/views/PostManager/PostManagerView.d.ts +1 -2
- package/dist/views/PostManager/PostManagerView.d.ts.map +1 -1
- package/dist/views/PostManager/PostManagerView.js +30 -96
- package/dist/views/PostManager/PostStats.d.ts.map +1 -1
- package/dist/views/PostManager/PostStats.js +10 -10
- package/dist/views/PostManager/PostTable.d.ts.map +1 -1
- package/dist/views/PostManager/PostTable.js +23 -40
- package/dist/views/Settings/SettingsView.d.ts +1 -1
- package/dist/views/Settings/SettingsView.d.ts.map +1 -1
- package/dist/views/Settings/SettingsView.js +12 -39
- package/dist/views/SlugSEO/SlugSEOManagerView.d.ts.map +1 -1
- package/dist/views/SlugSEO/SlugSEOManagerView.js +2 -2
- package/package.json +42 -6
- package/src/api/categories.ts +48 -52
- package/src/api/handler.ts +87 -594
- package/src/api/router.ts +15 -65
- package/src/api/service.ts +241 -0
- package/src/hooks/useAutoSave.ts +64 -0
- package/src/hooks/useCategories.ts +19 -47
- package/src/index.tsx +79 -293
- package/src/init.tsx +24 -11
- package/src/lib/blocks/BlockRenderer.tsx +1 -0
- package/src/lib/layouts/blocks/ColumnsBlock.tsx +60 -173
- package/src/lib/layouts/blocks/SectionBlock.tsx +22 -26
- package/src/lib/layouts/index.ts +4 -4
- package/src/lib/mappers/apiMapper.ts +63 -32
- package/src/lib/rich-text/RichTextEditor.tsx +16 -9
- package/src/lib/utils/config-resolver.ts +64 -0
- package/src/lib/utils/tree.ts +150 -0
- package/src/state/EditorContext.tsx +153 -232
- package/src/state/reducer.ts +141 -606
- package/src/state/types.ts +14 -1
- package/src/types/block.ts +10 -0
- package/src/types/post.ts +19 -1
- package/src/views/CanvasEditor/BlockWrapper.tsx +130 -460
- package/src/views/CanvasEditor/CanvasEditorView.tsx +145 -420
- package/src/views/CanvasEditor/EditorBody.tsx +98 -610
- package/src/views/CanvasEditor/EditorHeader.tsx +176 -196
- package/src/views/CanvasEditor/LayoutContainer.tsx +74 -89
- package/src/views/CanvasEditor/components/CustomBlockItem.tsx +7 -8
- package/src/views/CanvasEditor/components/EditorCanvas.tsx +139 -84
- package/src/views/CanvasEditor/components/EditorLibrary.tsx +25 -10
- package/src/views/CanvasEditor/components/EditorSidebar.tsx +196 -127
- package/src/views/CanvasEditor/components/FeaturedMediaSection.tsx +78 -210
- package/src/views/CanvasEditor/components/JSONInspector.tsx +125 -0
- package/src/views/CanvasEditor/components/LibraryItem.tsx +5 -6
- package/src/views/CanvasEditor/components/PrivacySettingsSection.tsx +73 -124
- package/src/views/CanvasEditor/components/index.ts +2 -1
- package/src/views/CanvasEditor/hooks/useHeroBlock.ts +15 -18
- package/src/views/CanvasEditor/hooks/usePostLoader.ts +21 -13
- package/src/views/CanvasEditor/hooks/useUnsavedChanges.ts +4 -4
- package/src/views/PostManager/EmptyState.tsx +9 -10
- package/src/views/PostManager/FilterDropdown.tsx +95 -0
- package/src/views/PostManager/LanguageFlags.tsx +6 -2
- package/src/views/PostManager/PostCards.tsx +127 -133
- package/src/views/PostManager/PostFilters.tsx +73 -68
- package/src/views/PostManager/PostManagerView.tsx +132 -179
- package/src/views/PostManager/PostStats.tsx +21 -20
- package/src/views/PostManager/PostTable.tsx +137 -165
- package/src/views/Settings/SettingsView.tsx +64 -180
- package/src/views/SlugSEO/SlugSEOManagerView.tsx +59 -44
- package/src/hooks/index.d.ts +0 -8
- package/src/hooks/index.d.ts.map +0 -1
- package/src/hooks/useBlog.d.ts +0 -31
- package/src/hooks/useBlog.d.ts.map +0 -1
- package/src/hooks/useBlogs.d.ts +0 -39
- package/src/hooks/useBlogs.d.ts.map +0 -1
- package/src/hooks/useCategories.d.ts +0 -9
- package/src/hooks/useCategories.d.ts.map +0 -1
- package/src/lib/blocks/BlockRenderer.d.ts +0 -54
- package/src/lib/blocks/BlockRenderer.d.ts.map +0 -1
- package/src/lib/config-storage.d.ts +0 -30
- package/src/lib/config-storage.d.ts.map +0 -1
- package/src/lib/layouts/blocks/ColumnsBlock.d.ts +0 -25
- package/src/lib/layouts/blocks/ColumnsBlock.d.ts.map +0 -1
- package/src/lib/layouts/blocks/SectionBlock.d.ts +0 -25
- package/src/lib/layouts/blocks/SectionBlock.d.ts.map +0 -1
- package/src/lib/layouts/index.d.ts +0 -23
- package/src/lib/layouts/index.d.ts.map +0 -1
- package/src/lib/layouts/registerLayoutBlocks.d.ts +0 -9
- package/src/lib/layouts/registerLayoutBlocks.d.ts.map +0 -1
- package/src/lib/mappers/apiMapper.d.ts +0 -66
- package/src/lib/mappers/apiMapper.d.ts.map +0 -1
- package/src/lib/rich-text/RichTextEditor.d.ts +0 -45
- package/src/lib/rich-text/RichTextEditor.d.ts.map +0 -1
- package/src/lib/rich-text/RichTextPreview.d.ts +0 -16
- package/src/lib/rich-text/RichTextPreview.d.ts.map +0 -1
- package/src/lib/rich-text/index.d.ts +0 -9
- package/src/lib/rich-text/index.d.ts.map +0 -1
- package/src/lib/utils/blockHelpers.d.ts +0 -23
- package/src/lib/utils/blockHelpers.d.ts.map +0 -1
- package/src/lib/utils/configValidation.d.ts +0 -23
- package/src/lib/utils/configValidation.d.ts.map +0 -1
- package/src/registry/BlockRegistry.d.ts +0 -62
- package/src/registry/BlockRegistry.d.ts.map +0 -1
- package/src/registry/index.d.ts +0 -6
- package/src/registry/index.d.ts.map +0 -1
- package/src/state/EditorContext.d.ts +0 -45
- package/src/state/EditorContext.d.ts.map +0 -1
- package/src/state/index.d.ts +0 -7
- package/src/state/index.d.ts.map +0 -1
- package/src/state/reducer.d.ts +0 -11
- package/src/state/reducer.d.ts.map +0 -1
- package/src/state/types.d.ts +0 -162
- package/src/state/types.d.ts.map +0 -1
- package/src/types/block.d.ts +0 -221
- package/src/types/block.d.ts.map +0 -1
- package/src/types/index.d.ts +0 -8
- package/src/types/index.d.ts.map +0 -1
- package/src/types/post.d.ts +0 -136
- package/src/types/post.d.ts.map +0 -1
- package/src/utils/client.d.ts +0 -48
- package/src/utils/client.d.ts.map +0 -1
- package/src/views/CanvasEditor/BlockWrapper.d.ts +0 -16
- package/src/views/CanvasEditor/BlockWrapper.d.ts.map +0 -1
- package/src/views/CanvasEditor/CanvasEditorView.d.ts +0 -14
- package/src/views/CanvasEditor/CanvasEditorView.d.ts.map +0 -1
- package/src/views/CanvasEditor/EditorBody.d.ts +0 -22
- package/src/views/CanvasEditor/EditorBody.d.ts.map +0 -1
- package/src/views/CanvasEditor/EditorHeader.d.ts +0 -18
- package/src/views/CanvasEditor/EditorHeader.d.ts.map +0 -1
- package/src/views/CanvasEditor/LayoutContainer.d.ts +0 -17
- package/src/views/CanvasEditor/LayoutContainer.d.ts.map +0 -1
- package/src/views/CanvasEditor/SaveConfirmationModal.d.ts +0 -13
- package/src/views/CanvasEditor/SaveConfirmationModal.d.ts.map +0 -1
- package/src/views/CanvasEditor/components/CustomBlockItem.d.ts +0 -14
- package/src/views/CanvasEditor/components/CustomBlockItem.d.ts.map +0 -1
- package/src/views/CanvasEditor/components/EditorCanvas.d.ts +0 -29
- package/src/views/CanvasEditor/components/EditorCanvas.d.ts.map +0 -1
- package/src/views/CanvasEditor/components/EditorLibrary.d.ts +0 -7
- package/src/views/CanvasEditor/components/EditorLibrary.d.ts.map +0 -1
- package/src/views/CanvasEditor/components/EditorSidebar.d.ts +0 -13
- package/src/views/CanvasEditor/components/EditorSidebar.d.ts.map +0 -1
- package/src/views/CanvasEditor/components/ErrorBanner.d.ts +0 -6
- package/src/views/CanvasEditor/components/ErrorBanner.d.ts.map +0 -1
- package/src/views/CanvasEditor/components/FeaturedMediaSection.d.ts +0 -25
- package/src/views/CanvasEditor/components/FeaturedMediaSection.d.ts.map +0 -1
- package/src/views/CanvasEditor/components/LibraryItem.d.ts +0 -14
- package/src/views/CanvasEditor/components/LibraryItem.d.ts.map +0 -1
- package/src/views/CanvasEditor/components/PrivacySettingsSection.d.ts +0 -15
- package/src/views/CanvasEditor/components/PrivacySettingsSection.d.ts.map +0 -1
- package/src/views/CanvasEditor/components/index.d.ts +0 -21
- package/src/views/CanvasEditor/components/index.d.ts.map +0 -1
- package/src/views/CanvasEditor/hooks/index.d.ts +0 -10
- package/src/views/CanvasEditor/hooks/index.d.ts.map +0 -1
- package/src/views/CanvasEditor/hooks/useHeroBlock.d.ts +0 -8
- package/src/views/CanvasEditor/hooks/useHeroBlock.d.ts.map +0 -1
- package/src/views/CanvasEditor/hooks/useKeyboardShortcuts.d.ts +0 -3
- package/src/views/CanvasEditor/hooks/useKeyboardShortcuts.d.ts.map +0 -1
- package/src/views/CanvasEditor/hooks/usePostLoader.d.ts +0 -5
- package/src/views/CanvasEditor/hooks/usePostLoader.d.ts.map +0 -1
- package/src/views/CanvasEditor/hooks/useRegisteredBlocks.d.ts +0 -2
- package/src/views/CanvasEditor/hooks/useRegisteredBlocks.d.ts.map +0 -1
- package/src/views/CanvasEditor/hooks/useUnsavedChanges.d.ts +0 -25
- package/src/views/CanvasEditor/hooks/useUnsavedChanges.d.ts.map +0 -1
- package/src/views/CanvasEditor/index.d.ts +0 -16
- package/src/views/CanvasEditor/index.d.ts.map +0 -1
- package/src/views/PostManager/EmptyState.d.ts +0 -10
- package/src/views/PostManager/EmptyState.d.ts.map +0 -1
- package/src/views/PostManager/PostActionsMenu.d.ts +0 -12
- package/src/views/PostManager/PostActionsMenu.d.ts.map +0 -1
- package/src/views/PostManager/PostCards.d.ts +0 -15
- package/src/views/PostManager/PostCards.d.ts.map +0 -1
- package/src/views/PostManager/PostFilters.d.ts +0 -16
- package/src/views/PostManager/PostFilters.d.ts.map +0 -1
- package/src/views/PostManager/PostManagerView.d.ts +0 -11
- package/src/views/PostManager/PostManagerView.d.ts.map +0 -1
- package/src/views/PostManager/PostStats.d.ts +0 -11
- package/src/views/PostManager/PostStats.d.ts.map +0 -1
- package/src/views/PostManager/PostTable.d.ts +0 -15
- package/src/views/PostManager/PostTable.d.ts.map +0 -1
- package/src/views/PostManager/index.d.ts +0 -12
- package/src/views/PostManager/index.d.ts.map +0 -1
- package/src/views/Preview/PreviewBridgeView.d.ts +0 -12
- package/src/views/Preview/PreviewBridgeView.d.ts.map +0 -1
- package/src/views/Preview/index.d.ts +0 -6
- package/src/views/Preview/index.d.ts.map +0 -1
- package/src/views/Settings/SettingsView.d.ts +0 -10
- package/src/views/Settings/SettingsView.d.ts.map +0 -1
- package/src/views/Settings/index.d.ts +0 -6
- package/src/views/Settings/index.d.ts.map +0 -1
- package/src/views/SlugSEO/SlugSEOManagerView.d.ts +0 -12
- package/src/views/SlugSEO/SlugSEOManagerView.d.ts.map +0 -1
- package/src/views/SlugSEO/index.d.ts +0 -6
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
20
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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('
|
|
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
|
}
|