@jhits/plugin-blog 0.0.4 → 0.0.6
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/package.json +55 -58
- package/src/api/handler.ts +10 -11
- package/src/api/router.ts +1 -4
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useCategories.ts +76 -0
- package/src/index.tsx +5 -27
- package/src/init.tsx +0 -9
- package/src/lib/mappers/apiMapper.ts +53 -22
- package/src/registry/BlockRegistry.ts +1 -4
- package/src/state/EditorContext.tsx +39 -33
- package/src/state/types.ts +1 -1
- package/src/types/post.ts +4 -0
- package/src/utils/index.ts +2 -1
- package/src/views/CanvasEditor/BlockWrapper.tsx +7 -8
- package/src/views/CanvasEditor/CanvasEditorView.tsx +208 -794
- package/src/views/CanvasEditor/EditorBody.tsx +317 -127
- package/src/views/CanvasEditor/EditorHeader.tsx +106 -17
- package/src/views/CanvasEditor/LayoutContainer.tsx +208 -380
- package/src/views/CanvasEditor/components/EditorCanvas.tsx +160 -0
- package/src/views/CanvasEditor/components/EditorLibrary.tsx +122 -0
- package/src/views/CanvasEditor/components/EditorSidebar.tsx +181 -0
- package/src/views/CanvasEditor/components/ErrorBanner.tsx +31 -0
- package/src/views/CanvasEditor/components/FeaturedMediaSection.tsx +229 -46
- package/src/views/CanvasEditor/components/index.ts +11 -0
- package/src/views/CanvasEditor/hooks/index.ts +10 -0
- package/src/views/CanvasEditor/hooks/useHeroBlock.ts +103 -0
- package/src/views/CanvasEditor/hooks/useKeyboardShortcuts.ts +142 -0
- package/src/views/CanvasEditor/hooks/usePostLoader.ts +39 -0
- package/src/views/CanvasEditor/hooks/useRegisteredBlocks.ts +55 -0
- package/src/views/CanvasEditor/hooks/useUnsavedChanges.ts +339 -0
- package/src/views/PostManager/PostActionsMenu.tsx +1 -1
- package/src/views/PostManager/PostCards.tsx +18 -13
- package/src/views/PostManager/PostFilters.tsx +15 -0
- package/src/views/PostManager/PostManagerView.tsx +29 -20
- package/src/views/PostManager/PostStats.tsx +5 -5
- package/src/views/PostManager/PostTable.tsx +10 -5
package/package.json
CHANGED
|
@@ -1,60 +1,57 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
"name": "@jhits/plugin-blog",
|
|
3
|
+
"version": "0.0.6",
|
|
4
|
+
"description": "Professional blog management system for the JHITS ecosystem",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public"
|
|
7
|
+
},
|
|
8
|
+
"main": "./src/index.tsx",
|
|
9
|
+
"types": "./src/index.tsx",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./src/index.tsx",
|
|
13
|
+
"default": "./src/index.tsx"
|
|
7
14
|
},
|
|
8
|
-
"
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
"
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
"
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
"typescript": "^5"
|
|
52
|
-
},
|
|
53
|
-
"files": [
|
|
54
|
-
"src/**/*.{ts,tsx,json}",
|
|
55
|
-
"!src/**/*.md",
|
|
56
|
-
"!src/**/README.md",
|
|
57
|
-
"!README.md",
|
|
58
|
-
"package.json"
|
|
59
|
-
]
|
|
60
|
-
}
|
|
15
|
+
"./server": {
|
|
16
|
+
"types": "./src/index.server.ts",
|
|
17
|
+
"default": "./src/index.server.ts"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"bcrypt": "^6.0.0",
|
|
22
|
+
"framer-motion": "^12.23.26",
|
|
23
|
+
"lucide-react": "^0.562.0",
|
|
24
|
+
"mongodb": "^7.0.0",
|
|
25
|
+
"next-auth": "^4.24.13",
|
|
26
|
+
"@jhits/plugin-core": "0.0.1",
|
|
27
|
+
"@jhits/plugin-content": "0.0.3",
|
|
28
|
+
"@jhits/plugin-images": "0.0.5"
|
|
29
|
+
},
|
|
30
|
+
"peerDependencies": {
|
|
31
|
+
"next": ">=15.0.0",
|
|
32
|
+
"next-intl": ">=4.0.0",
|
|
33
|
+
"next-themes": ">=0.4.0",
|
|
34
|
+
"react": ">=18.0.0",
|
|
35
|
+
"react-dom": ">=18.0.0"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@tailwindcss/postcss": "^4",
|
|
39
|
+
"@types/bcrypt": "^6.0.0",
|
|
40
|
+
"@types/node": "^20.19.27",
|
|
41
|
+
"@types/react": "^19",
|
|
42
|
+
"@types/react-dom": "^19",
|
|
43
|
+
"eslint": "^9",
|
|
44
|
+
"eslint-config-next": "16.1.1",
|
|
45
|
+
"next": "16.1.1",
|
|
46
|
+
"next-intl": "4.6.1",
|
|
47
|
+
"next-themes": "0.4.6",
|
|
48
|
+
"react": "19.2.3",
|
|
49
|
+
"react-dom": "19.2.3",
|
|
50
|
+
"tailwindcss": "^4",
|
|
51
|
+
"typescript": "^5"
|
|
52
|
+
},
|
|
53
|
+
"files": [
|
|
54
|
+
"src",
|
|
55
|
+
"package.json"
|
|
56
|
+
]
|
|
57
|
+
}
|
package/src/api/handler.ts
CHANGED
|
@@ -8,7 +8,6 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { NextRequest, NextResponse } from 'next/server';
|
|
11
|
-
import { APIBlogDocument, apiToBlogPost, editorStateToAPI } from '../lib/mappers/apiMapper';
|
|
12
11
|
import { slugify } from '../lib/utils/slugify';
|
|
13
12
|
|
|
14
13
|
export interface BlogApiConfig {
|
|
@@ -68,7 +67,7 @@ export async function GET(req: NextRequest, config: BlogApiConfig): Promise<Next
|
|
|
68
67
|
.find(query)
|
|
69
68
|
.sort({ 'publicationData.date': -1 })
|
|
70
69
|
.skip(skip)
|
|
71
|
-
.limit(limit)
|
|
70
|
+
.limit(isAdminView ? 0 : limit)
|
|
72
71
|
.toArray(),
|
|
73
72
|
blogs.countDocuments(query),
|
|
74
73
|
]);
|
|
@@ -125,7 +124,7 @@ export async function POST(req: NextRequest, config: BlogApiConfig): Promise<Nex
|
|
|
125
124
|
if (isPublishing) {
|
|
126
125
|
// Publishing requires all fields
|
|
127
126
|
if (!summary?.trim()) errors.push('Summary is required for publishing');
|
|
128
|
-
if (!image?.
|
|
127
|
+
if (!image?.id?.trim()) errors.push('Featured image is required for publishing');
|
|
129
128
|
// Only require category if it's explicitly provided and empty
|
|
130
129
|
// If categoryTags is undefined or category is undefined, that's also missing
|
|
131
130
|
if (!categoryTags || !categoryTags.category || !categoryTags.category.trim()) {
|
|
@@ -169,7 +168,7 @@ export async function POST(req: NextRequest, config: BlogApiConfig): Promise<Nex
|
|
|
169
168
|
summary: (summary || '').trim(),
|
|
170
169
|
contentBlocks: contentBlocks || [],
|
|
171
170
|
content: content || [],
|
|
172
|
-
image: image || {
|
|
171
|
+
image: image || { id: '', alt: '' }, // Only store id and alt - plugin-images handles transforms
|
|
173
172
|
categoryTags: {
|
|
174
173
|
category: categoryTags?.category?.trim() || '',
|
|
175
174
|
tags: categoryTags?.tags || [],
|
|
@@ -292,33 +291,33 @@ export async function PUT_BY_SLUG(
|
|
|
292
291
|
const hasContent =
|
|
293
292
|
(contentBlocks && Array.isArray(contentBlocks) && contentBlocks.length > 0) ||
|
|
294
293
|
(content && Array.isArray(content) && content.length > 0);
|
|
295
|
-
|
|
294
|
+
|
|
296
295
|
// Collect all missing fields for better error messages
|
|
297
296
|
const missingFields: string[] = [];
|
|
298
297
|
if (!summary?.trim()) missingFields.push('summary');
|
|
299
|
-
if (!image?.
|
|
298
|
+
if (!image?.id?.trim()) missingFields.push('featured image');
|
|
300
299
|
// Only require category if it's explicitly provided and empty
|
|
301
300
|
// If categoryTags is undefined or category is undefined, that's also missing
|
|
302
301
|
if (!categoryTags || !categoryTags.category || !categoryTags.category.trim()) {
|
|
303
302
|
missingFields.push('category');
|
|
304
303
|
}
|
|
305
304
|
if (!hasContent) missingFields.push('content');
|
|
306
|
-
|
|
305
|
+
|
|
307
306
|
if (missingFields.length > 0) {
|
|
308
307
|
console.log('[BlogAPI] PUT_BY_SLUG validation failed:', {
|
|
309
308
|
isPublishing,
|
|
310
309
|
missingFields,
|
|
311
310
|
summary: summary?.trim() || 'missing',
|
|
312
|
-
|
|
311
|
+
imageId: image?.id?.trim() || 'missing',
|
|
313
312
|
category: categoryTags?.category?.trim() || 'missing',
|
|
314
313
|
hasContent,
|
|
315
314
|
contentBlocksLength: contentBlocks?.length || 0,
|
|
316
315
|
contentLength: content?.length || 0,
|
|
317
316
|
});
|
|
318
317
|
return NextResponse.json(
|
|
319
|
-
{
|
|
318
|
+
{
|
|
320
319
|
message: `Missing required fields for publishing: ${missingFields.join(', ')}`,
|
|
321
|
-
missingFields
|
|
320
|
+
missingFields
|
|
322
321
|
},
|
|
323
322
|
{ status: 400 }
|
|
324
323
|
);
|
|
@@ -341,7 +340,7 @@ export async function PUT_BY_SLUG(
|
|
|
341
340
|
} else if (publicationData?.status === 'draft') {
|
|
342
341
|
finalStatus = 'concept';
|
|
343
342
|
}
|
|
344
|
-
|
|
343
|
+
|
|
345
344
|
const updateData = {
|
|
346
345
|
title: title.trim(),
|
|
347
346
|
summary: (summary || '').trim(),
|
package/src/api/router.ts
CHANGED
|
@@ -37,21 +37,18 @@ export async function handleBlogApi(
|
|
|
37
37
|
getUserId: config.getUserId,
|
|
38
38
|
collectionName: config.collectionName || 'blogs',
|
|
39
39
|
});
|
|
40
|
-
|
|
40
|
+
|
|
41
41
|
const method = req.method;
|
|
42
42
|
// Handle empty path array - means we're at /api/plugin-blog
|
|
43
43
|
// Ensure path is always an array
|
|
44
44
|
const safePath = Array.isArray(path) ? path : [];
|
|
45
45
|
const route = safePath.length > 0 ? safePath[0] : '';
|
|
46
46
|
|
|
47
|
-
console.log(`[BlogApiRouter] method=${method}, path=${JSON.stringify(safePath)}, route=${route}, url=${req.url}`);
|
|
48
|
-
|
|
49
47
|
try {
|
|
50
48
|
// Route: /api/plugin-blog (list/create) - empty path or 'list'
|
|
51
49
|
// This handles both /api/plugin-blog and /api/plugin-blog?limit=3
|
|
52
50
|
if (!route || route === 'list') {
|
|
53
51
|
if (method === 'GET') {
|
|
54
|
-
console.log('[BlogApiRouter] Routing to BlogListHandler');
|
|
55
52
|
return await BlogListHandler(req, blogApiConfig);
|
|
56
53
|
}
|
|
57
54
|
if (method === 'POST') {
|
package/src/hooks/index.ts
CHANGED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook to fetch categories from existing blog posts
|
|
3
|
+
* Extracts categories from Hero blocks in all posts
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use client';
|
|
7
|
+
|
|
8
|
+
import { useState, useEffect } from 'react';
|
|
9
|
+
|
|
10
|
+
export function useCategories() {
|
|
11
|
+
const [categories, setCategories] = useState<string[]>([]);
|
|
12
|
+
const [loading, setLoading] = useState(true);
|
|
13
|
+
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
const fetchCategories = async () => {
|
|
16
|
+
try {
|
|
17
|
+
const categorySet = new Set<string>();
|
|
18
|
+
|
|
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
|
+
}
|
|
35
|
+
|
|
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);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Convert to sorted array
|
|
61
|
+
const sortedCategories = Array.from(categorySet).sort();
|
|
62
|
+
setCategories(sortedCategories);
|
|
63
|
+
} catch (error) {
|
|
64
|
+
console.error('Failed to fetch categories:', error);
|
|
65
|
+
// Fallback to empty array
|
|
66
|
+
setCategories([]);
|
|
67
|
+
} finally {
|
|
68
|
+
setLoading(false);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
fetchCategories();
|
|
73
|
+
}, []);
|
|
74
|
+
|
|
75
|
+
return { categories, loading };
|
|
76
|
+
}
|
package/src/index.tsx
CHANGED
|
@@ -49,7 +49,6 @@ export interface PluginProps {
|
|
|
49
49
|
*/
|
|
50
50
|
export default function BlogPlugin(props: PluginProps) {
|
|
51
51
|
const { subPath, siteId, locale, customBlocks: propsCustomBlocks, darkMode: propsDarkMode, backgroundColors: propsBackgroundColors } = props;
|
|
52
|
-
console.log('[BlogPlugin] Component rendering, propsDarkMode:', propsDarkMode);
|
|
53
52
|
|
|
54
53
|
// Get custom blocks from props or window global (client app injection point)
|
|
55
54
|
const customBlocks = useMemo(() => {
|
|
@@ -79,7 +78,6 @@ export default function BlogPlugin(props: PluginProps) {
|
|
|
79
78
|
if (saved) {
|
|
80
79
|
const config = JSON.parse(saved);
|
|
81
80
|
if (config.darkMode !== undefined) {
|
|
82
|
-
console.log('[BlogPlugin] Using darkMode from localStorage (dev settings):', config.darkMode);
|
|
83
81
|
return config.darkMode as boolean;
|
|
84
82
|
}
|
|
85
83
|
}
|
|
@@ -90,7 +88,6 @@ export default function BlogPlugin(props: PluginProps) {
|
|
|
90
88
|
|
|
91
89
|
// Then try props
|
|
92
90
|
if (propsDarkMode !== undefined) {
|
|
93
|
-
console.log('[BlogPlugin] Using darkMode from props:', propsDarkMode);
|
|
94
91
|
return propsDarkMode;
|
|
95
92
|
}
|
|
96
93
|
|
|
@@ -98,12 +95,10 @@ export default function BlogPlugin(props: PluginProps) {
|
|
|
98
95
|
if (typeof window !== 'undefined' && (window as any).__JHITS_PLUGIN_PROPS__) {
|
|
99
96
|
const pluginProps = (window as any).__JHITS_PLUGIN_PROPS__['plugin-blog'];
|
|
100
97
|
if (pluginProps?.darkMode !== undefined) {
|
|
101
|
-
console.log('[BlogPlugin] Using darkMode from window global (fallback):', pluginProps.darkMode);
|
|
102
98
|
return pluginProps.darkMode as boolean;
|
|
103
99
|
}
|
|
104
100
|
}
|
|
105
101
|
|
|
106
|
-
console.log('[BlogPlugin] darkMode not found, defaulting to true');
|
|
107
102
|
return true; // Default to dark mode enabled
|
|
108
103
|
}, [propsDarkMode]);
|
|
109
104
|
|
|
@@ -167,8 +162,6 @@ export default function BlogPlugin(props: PluginProps) {
|
|
|
167
162
|
};
|
|
168
163
|
}, []);
|
|
169
164
|
|
|
170
|
-
console.log('[BlogPlugin] Final darkMode value:', darkMode);
|
|
171
|
-
console.log('[BlogPlugin] Background colors:', backgroundColors);
|
|
172
165
|
if (heroBlockDefinition !== undefined) {
|
|
173
166
|
console.log('[BlogPlugin] Hero block definition:', heroBlockDefinition ? 'found' : 'not found (REQUIRED)');
|
|
174
167
|
}
|
|
@@ -180,13 +173,12 @@ export default function BlogPlugin(props: PluginProps) {
|
|
|
180
173
|
|
|
181
174
|
case 'editor':
|
|
182
175
|
const postId = subPath[1]; // This is actually the slug, not the ID
|
|
183
|
-
console.log('[BlogPlugin] Rendering editor route with postId (slug):', postId, 'darkMode:', darkMode);
|
|
184
176
|
return (
|
|
185
177
|
<EditorProvider
|
|
186
178
|
customBlocks={customBlocks}
|
|
187
179
|
darkMode={darkMode}
|
|
188
180
|
backgroundColors={backgroundColors}
|
|
189
|
-
onSave={async (state) => {
|
|
181
|
+
onSave={async (state, heroBlock) => {
|
|
190
182
|
// Save to API - update existing post
|
|
191
183
|
// Use the route postId (original slug) to identify which blog to update
|
|
192
184
|
// If route postId is missing, use state.slug or state.postId as fallback
|
|
@@ -195,22 +187,7 @@ export default function BlogPlugin(props: PluginProps) {
|
|
|
195
187
|
throw new Error('Cannot save: no post identifier available. Please reload the page.');
|
|
196
188
|
}
|
|
197
189
|
console.log('[BlogPlugin] Saving post with slug:', originalSlug);
|
|
198
|
-
|
|
199
|
-
title: state.title,
|
|
200
|
-
status: state.status,
|
|
201
|
-
blocksCount: state.blocks.length,
|
|
202
|
-
blocks: state.blocks.map(b => ({ id: b.id, type: b.type, hasData: !!b.data })),
|
|
203
|
-
hasFeaturedImage: !!state.metadata.featuredImage,
|
|
204
|
-
});
|
|
205
|
-
const apiData = editorStateToAPI(state);
|
|
206
|
-
console.log('[BlogPlugin] API data being sent:', {
|
|
207
|
-
title: apiData.title,
|
|
208
|
-
status: apiData.publicationData?.status,
|
|
209
|
-
contentBlocksCount: apiData.contentBlocks?.length || 0,
|
|
210
|
-
contentBlocks: apiData.contentBlocks?.map((b: any) => ({ id: b.id, type: b.type, hasData: !!b.data })) || [],
|
|
211
|
-
hasImage: !!apiData.image,
|
|
212
|
-
fullApiData: JSON.stringify(apiData, null, 2),
|
|
213
|
-
});
|
|
190
|
+
const apiData = editorStateToAPI(state, undefined, heroBlock);
|
|
214
191
|
const response = await fetch(`/api/plugin-blog/${originalSlug}`, {
|
|
215
192
|
method: 'PUT',
|
|
216
193
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -233,7 +210,6 @@ export default function BlogPlugin(props: PluginProps) {
|
|
|
233
210
|
throw new Error(errorMessage + missingFieldsMsg);
|
|
234
211
|
}
|
|
235
212
|
const result = await response.json();
|
|
236
|
-
console.log('[BlogPlugin] Save successful:', result);
|
|
237
213
|
// If the slug changed, update the URL
|
|
238
214
|
if (result.slug && result.slug !== originalSlug) {
|
|
239
215
|
window.history.replaceState(null, '', `/dashboard/blog/editor/${result.slug}`);
|
|
@@ -246,7 +222,6 @@ export default function BlogPlugin(props: PluginProps) {
|
|
|
246
222
|
);
|
|
247
223
|
|
|
248
224
|
case 'new':
|
|
249
|
-
console.log('[BlogPlugin] Rendering new route with darkMode:', darkMode);
|
|
250
225
|
return (
|
|
251
226
|
<EditorProvider
|
|
252
227
|
customBlocks={customBlocks}
|
|
@@ -349,6 +324,9 @@ export { registerLayoutBlocks } from './lib/layouts/registerLayoutBlocks';
|
|
|
349
324
|
export { EditorProvider, useEditor } from './state/EditorContext';
|
|
350
325
|
export type { EditorProviderProps, EditorState, EditorContextValue } from './state';
|
|
351
326
|
|
|
327
|
+
// Export hooks
|
|
328
|
+
export { useCategories } from './hooks/useCategories';
|
|
329
|
+
|
|
352
330
|
// Note: API handlers are server-only and exported from ./index.ts (server entry point)
|
|
353
331
|
// They are NOT exported here to prevent client/server context mixing
|
|
354
332
|
|
package/src/init.tsx
CHANGED
|
@@ -59,14 +59,5 @@ export function initBlogPlugin(config: BlogPluginConfig): void {
|
|
|
59
59
|
darkMode: config.darkMode !== undefined ? config.darkMode : true, // Default to true
|
|
60
60
|
backgroundColors: config.backgroundColors || undefined,
|
|
61
61
|
};
|
|
62
|
-
|
|
63
|
-
console.log('[BlogPlugin] Initialized with config:', {
|
|
64
|
-
customBlocks: config.customBlocks?.length || 0,
|
|
65
|
-
darkMode: config.darkMode !== undefined ? config.darkMode : true,
|
|
66
|
-
backgroundColors: config.backgroundColors ? {
|
|
67
|
-
light: config.backgroundColors.light,
|
|
68
|
-
dark: config.backgroundColors.dark || 'not set',
|
|
69
|
-
} : 'not set',
|
|
70
|
-
});
|
|
71
62
|
}
|
|
72
63
|
|
|
@@ -18,10 +18,12 @@ export interface APIBlogDocument {
|
|
|
18
18
|
content?: any[]; // Legacy format
|
|
19
19
|
summary?: string;
|
|
20
20
|
image?: {
|
|
21
|
-
|
|
21
|
+
id?: string; // Semantic ID (preferred) - plugin-images handles everything else
|
|
22
|
+
src?: string; // Legacy support - will be converted to id when loading
|
|
22
23
|
alt?: string;
|
|
23
|
-
|
|
24
|
-
blur
|
|
24
|
+
isCustom?: boolean;
|
|
25
|
+
// Transform fields (brightness, blur, scale, positionX, positionY) are NOT stored here
|
|
26
|
+
// They are handled by plugin-images API only
|
|
25
27
|
};
|
|
26
28
|
categoryTags?: {
|
|
27
29
|
category?: string;
|
|
@@ -50,6 +52,7 @@ export function apiToBlogPost(doc: APIBlogDocument): BlogPost {
|
|
|
50
52
|
const id = doc._id?.toString() || doc.id || '';
|
|
51
53
|
|
|
52
54
|
// Use contentBlocks if available, otherwise fallback to content (legacy)
|
|
55
|
+
// Hero block is included in contentBlocks
|
|
53
56
|
const blocks = doc.contentBlocks || [];
|
|
54
57
|
|
|
55
58
|
// Convert publication data
|
|
@@ -69,12 +72,14 @@ export function apiToBlogPost(doc: APIBlogDocument): BlogPost {
|
|
|
69
72
|
};
|
|
70
73
|
|
|
71
74
|
// Convert metadata
|
|
75
|
+
// Only store semantic ID (id) and alt - plugin-images handles everything else
|
|
72
76
|
const metadata: PostMetadata = {
|
|
73
77
|
featuredImage: doc.image ? {
|
|
74
|
-
src
|
|
78
|
+
// Prefer id (semantic ID) over src (legacy)
|
|
79
|
+
id: doc.image.id || doc.image.src,
|
|
75
80
|
alt: doc.image.alt,
|
|
76
|
-
|
|
77
|
-
|
|
81
|
+
isCustom: doc.image.isCustom,
|
|
82
|
+
// Don't load transform fields - plugin-images handles those
|
|
78
83
|
} : undefined,
|
|
79
84
|
categories: doc.categoryTags?.category ? [doc.categoryTags.category] : [],
|
|
80
85
|
tags: doc.categoryTags?.tags || [],
|
|
@@ -121,11 +126,12 @@ export function blogPostToAPI(post: BlogPost, authorId?: string): Partial<APIBlo
|
|
|
121
126
|
slug: post.slug,
|
|
122
127
|
contentBlocks: post.blocks, // Use new block format
|
|
123
128
|
summary: post.metadata.excerpt,
|
|
129
|
+
// Only save semantic ID (id) and alt - plugin-images handles transform data
|
|
124
130
|
image: post.metadata.featuredImage ? {
|
|
125
|
-
|
|
131
|
+
id: post.metadata.featuredImage.id,
|
|
126
132
|
alt: post.metadata.featuredImage.alt,
|
|
127
|
-
|
|
128
|
-
|
|
133
|
+
isCustom: post.metadata.featuredImage.isCustom,
|
|
134
|
+
// Don't save transform fields - plugin-images API handles those
|
|
129
135
|
} : undefined,
|
|
130
136
|
categoryTags: {
|
|
131
137
|
category: post.metadata.categories?.[0] || '',
|
|
@@ -150,6 +156,9 @@ export function blogPostToAPI(post: BlogPost, authorId?: string): Partial<APIBlo
|
|
|
150
156
|
|
|
151
157
|
/**
|
|
152
158
|
* Convert EditorState to API format for saving
|
|
159
|
+
* @param state - Editor state
|
|
160
|
+
* @param authorId - Optional author ID
|
|
161
|
+
* @param heroBlock - Optional hero block (stored separately from content blocks)
|
|
153
162
|
*/
|
|
154
163
|
export function editorStateToAPI(state: {
|
|
155
164
|
title: string;
|
|
@@ -159,7 +168,7 @@ export function editorStateToAPI(state: {
|
|
|
159
168
|
metadata: PostMetadata;
|
|
160
169
|
status: PostStatus;
|
|
161
170
|
postId?: string | null;
|
|
162
|
-
}, authorId?: string): Partial<APIBlogDocument> {
|
|
171
|
+
}, authorId?: string, heroBlock?: Block | null): Partial<APIBlogDocument> {
|
|
163
172
|
// Map status: draft -> concept, published -> published, everything else stays as-is
|
|
164
173
|
const apiStatus = state.status === 'draft' ? 'concept' : state.status;
|
|
165
174
|
|
|
@@ -174,10 +183,10 @@ export function editorStateToAPI(state: {
|
|
|
174
183
|
if (state.metadata.categories && state.metadata.categories.length > 0 && state.metadata.categories[0]?.trim()) {
|
|
175
184
|
category = state.metadata.categories[0].trim();
|
|
176
185
|
} else {
|
|
177
|
-
// Check hero block for category
|
|
178
|
-
const
|
|
179
|
-
if (
|
|
180
|
-
const heroCategory = (
|
|
186
|
+
// Check hero block for category - use the passed heroBlock parameter first, then check state.blocks
|
|
187
|
+
const heroBlockToCheck = heroBlock || state.blocks.find(block => block.type === 'hero');
|
|
188
|
+
if (heroBlockToCheck && heroBlockToCheck.data && typeof heroBlockToCheck.data === 'object') {
|
|
189
|
+
const heroCategory = (heroBlockToCheck.data as any).category;
|
|
181
190
|
if (heroCategory && typeof heroCategory === 'string' && heroCategory.trim()) {
|
|
182
191
|
category = heroCategory.trim();
|
|
183
192
|
}
|
|
@@ -186,20 +195,42 @@ export function editorStateToAPI(state: {
|
|
|
186
195
|
|
|
187
196
|
console.log('[editorStateToAPI] Category resolution:', {
|
|
188
197
|
fromMetadata: state.metadata.categories?.[0],
|
|
189
|
-
fromHeroBlock: state.blocks.find(b => b.type === 'hero')?.data ? (state.blocks.find(b => b.type === 'hero')!.data as any).category : undefined,
|
|
190
|
-
finalCategory: category
|
|
198
|
+
fromHeroBlock: (heroBlock || state.blocks.find(b => b.type === 'hero'))?.data ? ((heroBlock || state.blocks.find(b => b.type === 'hero'))!.data as any).category : undefined,
|
|
199
|
+
finalCategory: category,
|
|
200
|
+
hasHeroBlock: !!heroBlock,
|
|
201
|
+
heroBlockImage: heroBlock?.data ? (heroBlock.data as any)?.image : undefined,
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// Include hero block in contentBlocks if it exists
|
|
205
|
+
// Filter out any existing hero blocks from state.blocks first, then add the current hero block
|
|
206
|
+
const contentBlocksWithoutHero = state.blocks.filter(block => block.type !== 'hero');
|
|
207
|
+
const allBlocks = heroBlock
|
|
208
|
+
? [heroBlock, ...contentBlocksWithoutHero]
|
|
209
|
+
: contentBlocksWithoutHero;
|
|
210
|
+
|
|
211
|
+
console.log('[editorStateToAPI] Hero block details:', {
|
|
212
|
+
hasHeroBlock: !!heroBlock,
|
|
213
|
+
heroBlockType: heroBlock?.type,
|
|
214
|
+
heroBlockId: heroBlock?.id,
|
|
215
|
+
heroBlockImage: heroBlock?.data ? (heroBlock.data as any)?.image : undefined,
|
|
216
|
+
heroBlockImageSrc: heroBlock?.data ? (heroBlock.data as any)?.image?.src : undefined,
|
|
217
|
+
contentBlocksCount: allBlocks.length,
|
|
218
|
+
contentBlocksTypes: allBlocks.map(b => b.type),
|
|
219
|
+
heroBlockInContentBlocks: allBlocks.find(b => b.type === 'hero')?.data ? (allBlocks.find(b => b.type === 'hero')!.data as any)?.image : undefined,
|
|
191
220
|
});
|
|
192
221
|
|
|
193
222
|
return {
|
|
194
223
|
title: state.title,
|
|
195
224
|
slug: state.slug,
|
|
196
|
-
contentBlocks:
|
|
225
|
+
contentBlocks: allBlocks,
|
|
197
226
|
summary: state.metadata.excerpt,
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
227
|
+
// Only save semantic ID (id) and alt - plugin-images handles transform data
|
|
228
|
+
// Only create image object if id exists and is not empty
|
|
229
|
+
image: state.metadata.featuredImage?.id?.trim() ? {
|
|
230
|
+
id: state.metadata.featuredImage.id.trim(),
|
|
231
|
+
alt: state.metadata.featuredImage.alt || '',
|
|
232
|
+
isCustom: state.metadata.featuredImage.isCustom,
|
|
233
|
+
// Don't save transform fields - plugin-images API handles those
|
|
203
234
|
} : undefined,
|
|
204
235
|
categoryTags: {
|
|
205
236
|
category: category,
|
|
@@ -35,10 +35,7 @@ class BlockRegistryImpl implements IBlockRegistry {
|
|
|
35
35
|
);
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
if (
|
|
39
|
-
console.warn(`Block type "${definition.type}" is already registered. Overwriting...`);
|
|
40
|
-
}
|
|
41
|
-
|
|
38
|
+
// Silently overwrite if already registered (expected in React 18 Strict Mode)
|
|
42
39
|
this._types.set(definition.type, definition);
|
|
43
40
|
}
|
|
44
41
|
|