@jhits/plugin-blog 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +216 -0
- package/package.json +57 -0
- package/src/api/README.md +224 -0
- package/src/api/categories.ts +43 -0
- package/src/api/check-title.ts +60 -0
- package/src/api/handler.ts +419 -0
- package/src/api/index.ts +33 -0
- package/src/api/route.ts +116 -0
- package/src/api/router.ts +114 -0
- package/src/api-server.ts +11 -0
- package/src/config.ts +161 -0
- package/src/hooks/README.md +91 -0
- package/src/hooks/index.ts +8 -0
- package/src/hooks/useBlog.ts +85 -0
- package/src/hooks/useBlogs.ts +123 -0
- package/src/index.server.ts +12 -0
- package/src/index.tsx +354 -0
- package/src/init.tsx +72 -0
- package/src/lib/blocks/BlockRenderer.tsx +141 -0
- package/src/lib/blocks/index.ts +6 -0
- package/src/lib/index.ts +9 -0
- package/src/lib/layouts/blocks/ColumnsBlock.tsx +134 -0
- package/src/lib/layouts/blocks/SectionBlock.tsx +104 -0
- package/src/lib/layouts/blocks/index.ts +8 -0
- package/src/lib/layouts/index.ts +52 -0
- package/src/lib/layouts/registerLayoutBlocks.ts +59 -0
- package/src/lib/mappers/apiMapper.ts +223 -0
- package/src/lib/migration/index.ts +6 -0
- package/src/lib/migration/mapper.ts +140 -0
- package/src/lib/rich-text/RichTextEditor.tsx +826 -0
- package/src/lib/rich-text/RichTextPreview.tsx +210 -0
- package/src/lib/rich-text/index.ts +10 -0
- package/src/lib/utils/blockHelpers.ts +72 -0
- package/src/lib/utils/configValidation.ts +137 -0
- package/src/lib/utils/index.ts +8 -0
- package/src/lib/utils/slugify.ts +79 -0
- package/src/registry/BlockRegistry.ts +142 -0
- package/src/registry/index.ts +11 -0
- package/src/state/EditorContext.tsx +277 -0
- package/src/state/index.ts +8 -0
- package/src/state/reducer.ts +694 -0
- package/src/state/types.ts +160 -0
- package/src/types/block.ts +269 -0
- package/src/types/index.ts +15 -0
- package/src/types/post.ts +165 -0
- package/src/utils/README.md +75 -0
- package/src/utils/client.ts +122 -0
- package/src/utils/index.ts +9 -0
- package/src/views/CanvasEditor/BlockWrapper.tsx +459 -0
- package/src/views/CanvasEditor/CanvasEditorView.tsx +917 -0
- package/src/views/CanvasEditor/EditorBody.tsx +475 -0
- package/src/views/CanvasEditor/EditorHeader.tsx +179 -0
- package/src/views/CanvasEditor/LayoutContainer.tsx +494 -0
- package/src/views/CanvasEditor/SaveConfirmationModal.tsx +233 -0
- package/src/views/CanvasEditor/components/CustomBlockItem.tsx +92 -0
- package/src/views/CanvasEditor/components/FeaturedMediaSection.tsx +130 -0
- package/src/views/CanvasEditor/components/LibraryItem.tsx +80 -0
- package/src/views/CanvasEditor/components/PrivacySettingsSection.tsx +212 -0
- package/src/views/CanvasEditor/components/index.ts +17 -0
- package/src/views/CanvasEditor/index.ts +16 -0
- package/src/views/PostManager/EmptyState.tsx +42 -0
- package/src/views/PostManager/PostActionsMenu.tsx +112 -0
- package/src/views/PostManager/PostCards.tsx +192 -0
- package/src/views/PostManager/PostFilters.tsx +80 -0
- package/src/views/PostManager/PostManagerView.tsx +280 -0
- package/src/views/PostManager/PostStats.tsx +81 -0
- package/src/views/PostManager/PostTable.tsx +225 -0
- package/src/views/PostManager/index.ts +15 -0
- package/src/views/Preview/PreviewBridgeView.tsx +64 -0
- package/src/views/Preview/index.ts +7 -0
- package/src/views/README.md +82 -0
- package/src/views/Settings/SettingsView.tsx +298 -0
- package/src/views/Settings/index.ts +7 -0
- package/src/views/SlugSEO/SlugSEOManagerView.tsx +94 -0
- package/src/views/SlugSEO/index.ts +7 -0
package/src/index.tsx
ADDED
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin Blog - Main Entry Point
|
|
3
|
+
* Block-Based Blog Management System
|
|
4
|
+
* Multi-Tenant Architecture: Accepts custom blocks from client applications
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use client';
|
|
8
|
+
|
|
9
|
+
import React, { useMemo, useEffect } from 'react';
|
|
10
|
+
import { EditorProvider } from './state/EditorContext';
|
|
11
|
+
import { ClientBlockDefinition } from './types/block';
|
|
12
|
+
import { PostManagerView } from './views/PostManager';
|
|
13
|
+
import { CanvasEditorView } from './views/CanvasEditor';
|
|
14
|
+
import { editorStateToAPI } from './lib/mappers/apiMapper';
|
|
15
|
+
import { SlugSEOManagerView } from './views/SlugSEO';
|
|
16
|
+
import { PreviewBridgeView } from './views/Preview';
|
|
17
|
+
import { SettingsView } from './views/Settings';
|
|
18
|
+
import { useHeroBlockValidation, findHeroBlock } from './lib/utils/configValidation';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Plugin Props Interface
|
|
22
|
+
* Matches the PluginProps from @jhits/jhits-dashboard
|
|
23
|
+
*/
|
|
24
|
+
export interface PluginProps {
|
|
25
|
+
subPath: string[];
|
|
26
|
+
siteId: string;
|
|
27
|
+
locale: string;
|
|
28
|
+
/** Custom blocks from client application (optional, can also come from window.__JHITS_PLUGIN_PROPS__) */
|
|
29
|
+
customBlocks?: ClientBlockDefinition[];
|
|
30
|
+
/** Enable dark mode for content area and wrappers (default: true) */
|
|
31
|
+
darkMode?: boolean;
|
|
32
|
+
/** Background colors for the editor */
|
|
33
|
+
backgroundColors?: {
|
|
34
|
+
/** Background color for light mode (REQUIRED) */
|
|
35
|
+
light: string;
|
|
36
|
+
/** Background color for dark mode (optional) */
|
|
37
|
+
dark?: string;
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Main Router Component
|
|
43
|
+
* Handles routing within the blog plugin
|
|
44
|
+
*
|
|
45
|
+
* Client Handshake:
|
|
46
|
+
* - Client apps can pass customBlocks via props
|
|
47
|
+
* - Or via window.__JHITS_PLUGIN_PROPS__['plugin-blog'].customBlocks
|
|
48
|
+
* - The EditorProvider will automatically register these blocks
|
|
49
|
+
*/
|
|
50
|
+
export default function BlogPlugin(props: PluginProps) {
|
|
51
|
+
const { subPath, siteId, locale, customBlocks: propsCustomBlocks, darkMode: propsDarkMode, backgroundColors: propsBackgroundColors } = props;
|
|
52
|
+
console.log('[BlogPlugin] Component rendering, propsDarkMode:', propsDarkMode);
|
|
53
|
+
|
|
54
|
+
// Get custom blocks from props or window global (client app injection point)
|
|
55
|
+
const customBlocks = useMemo(() => {
|
|
56
|
+
// First, try props
|
|
57
|
+
if (propsCustomBlocks && propsCustomBlocks.length > 0) {
|
|
58
|
+
return propsCustomBlocks;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Fallback to window global (for client app injection)
|
|
62
|
+
if (typeof window !== 'undefined' && (window as any).__JHITS_PLUGIN_PROPS__) {
|
|
63
|
+
const pluginProps = (window as any).__JHITS_PLUGIN_PROPS__['plugin-blog'];
|
|
64
|
+
if (pluginProps?.customBlocks) {
|
|
65
|
+
return pluginProps.customBlocks as ClientBlockDefinition[];
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return [];
|
|
70
|
+
}, [propsCustomBlocks]);
|
|
71
|
+
|
|
72
|
+
// Get dark mode setting from props, localStorage (dev settings), or window global
|
|
73
|
+
// Priority: localStorage (dev) > props > window global > default
|
|
74
|
+
const darkMode = useMemo(() => {
|
|
75
|
+
// First, check localStorage for dev settings (highest priority for dev)
|
|
76
|
+
if (typeof window !== 'undefined') {
|
|
77
|
+
try {
|
|
78
|
+
const saved = localStorage.getItem('__JHITS_PLUGIN_BLOG_CONFIG__');
|
|
79
|
+
if (saved) {
|
|
80
|
+
const config = JSON.parse(saved);
|
|
81
|
+
if (config.darkMode !== undefined) {
|
|
82
|
+
console.log('[BlogPlugin] Using darkMode from localStorage (dev settings):', config.darkMode);
|
|
83
|
+
return config.darkMode as boolean;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
} catch (e) {
|
|
87
|
+
// Ignore localStorage errors
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Then try props
|
|
92
|
+
if (propsDarkMode !== undefined) {
|
|
93
|
+
console.log('[BlogPlugin] Using darkMode from props:', propsDarkMode);
|
|
94
|
+
return propsDarkMode;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Fallback to window global if prop not provided
|
|
98
|
+
if (typeof window !== 'undefined' && (window as any).__JHITS_PLUGIN_PROPS__) {
|
|
99
|
+
const pluginProps = (window as any).__JHITS_PLUGIN_PROPS__['plugin-blog'];
|
|
100
|
+
if (pluginProps?.darkMode !== undefined) {
|
|
101
|
+
console.log('[BlogPlugin] Using darkMode from window global (fallback):', pluginProps.darkMode);
|
|
102
|
+
return pluginProps.darkMode as boolean;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
console.log('[BlogPlugin] darkMode not found, defaulting to true');
|
|
107
|
+
return true; // Default to dark mode enabled
|
|
108
|
+
}, [propsDarkMode]);
|
|
109
|
+
|
|
110
|
+
// Get background colors from props, localStorage (dev settings), or window global
|
|
111
|
+
// Priority: localStorage (dev) > props > window global
|
|
112
|
+
const backgroundColors = useMemo(() => {
|
|
113
|
+
// First, check localStorage for dev settings (highest priority for dev)
|
|
114
|
+
if (typeof window !== 'undefined') {
|
|
115
|
+
try {
|
|
116
|
+
const saved = localStorage.getItem('__JHITS_PLUGIN_BLOG_CONFIG__');
|
|
117
|
+
if (saved) {
|
|
118
|
+
const config = JSON.parse(saved);
|
|
119
|
+
if (config.backgroundColors) {
|
|
120
|
+
return config.backgroundColors;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
} catch (e) {
|
|
124
|
+
// Ignore localStorage errors
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Then try props
|
|
129
|
+
if (propsBackgroundColors) {
|
|
130
|
+
return propsBackgroundColors;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Fallback to window global
|
|
134
|
+
if (typeof window !== 'undefined' && (window as any).__JHITS_PLUGIN_PROPS__) {
|
|
135
|
+
const pluginProps = (window as any).__JHITS_PLUGIN_PROPS__['plugin-blog'];
|
|
136
|
+
if (pluginProps?.backgroundColors) {
|
|
137
|
+
return pluginProps.backgroundColors as { light: string; dark?: string };
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return undefined;
|
|
142
|
+
}, [propsBackgroundColors]);
|
|
143
|
+
|
|
144
|
+
const route = subPath[0] || 'posts';
|
|
145
|
+
|
|
146
|
+
// Validate hero block configuration (only checks when needed)
|
|
147
|
+
useHeroBlockValidation(route, customBlocks, propsCustomBlocks);
|
|
148
|
+
|
|
149
|
+
// Get hero block definition for logging/debugging (only for routes that need it)
|
|
150
|
+
const heroBlockDefinition = useMemo(() => {
|
|
151
|
+
const needsHeroBlock = route === 'editor' || route === 'new' || route === 'preview';
|
|
152
|
+
return needsHeroBlock ? findHeroBlock(customBlocks) : undefined;
|
|
153
|
+
}, [customBlocks, route]);
|
|
154
|
+
|
|
155
|
+
// Listen for config updates from settings screen
|
|
156
|
+
useEffect(() => {
|
|
157
|
+
if (typeof window === 'undefined') return;
|
|
158
|
+
|
|
159
|
+
const handleConfigUpdate = () => {
|
|
160
|
+
// Reload page to apply changes (simplest way to ensure all components pick up new values)
|
|
161
|
+
window.location.reload();
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
window.addEventListener('blog-plugin-config-updated', handleConfigUpdate as EventListener);
|
|
165
|
+
return () => {
|
|
166
|
+
window.removeEventListener('blog-plugin-config-updated', handleConfigUpdate as EventListener);
|
|
167
|
+
};
|
|
168
|
+
}, []);
|
|
169
|
+
|
|
170
|
+
console.log('[BlogPlugin] Final darkMode value:', darkMode);
|
|
171
|
+
console.log('[BlogPlugin] Background colors:', backgroundColors);
|
|
172
|
+
if (heroBlockDefinition !== undefined) {
|
|
173
|
+
console.log('[BlogPlugin] Hero block definition:', heroBlockDefinition ? 'found' : 'not found (REQUIRED)');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Route to appropriate view
|
|
177
|
+
switch (route) {
|
|
178
|
+
case 'posts':
|
|
179
|
+
return <PostManagerView siteId={siteId} locale={locale} />;
|
|
180
|
+
|
|
181
|
+
case 'editor':
|
|
182
|
+
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
|
+
return (
|
|
185
|
+
<EditorProvider
|
|
186
|
+
customBlocks={customBlocks}
|
|
187
|
+
darkMode={darkMode}
|
|
188
|
+
backgroundColors={backgroundColors}
|
|
189
|
+
onSave={async (state) => {
|
|
190
|
+
// Save to API - update existing post
|
|
191
|
+
// Use the route postId (original slug) to identify which blog to update
|
|
192
|
+
// If route postId is missing, use state.slug or state.postId as fallback
|
|
193
|
+
const originalSlug = postId || state.slug || state.postId;
|
|
194
|
+
if (!originalSlug) {
|
|
195
|
+
throw new Error('Cannot save: no post identifier available. Please reload the page.');
|
|
196
|
+
}
|
|
197
|
+
console.log('[BlogPlugin] Saving post with slug:', originalSlug);
|
|
198
|
+
console.log('[BlogPlugin] Post state:', {
|
|
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
|
+
});
|
|
214
|
+
const response = await fetch(`/api/plugin-blog/${originalSlug}`, {
|
|
215
|
+
method: 'PUT',
|
|
216
|
+
headers: { 'Content-Type': 'application/json' },
|
|
217
|
+
credentials: 'include', // Include cookies for authentication
|
|
218
|
+
body: JSON.stringify(apiData),
|
|
219
|
+
});
|
|
220
|
+
if (!response.ok) {
|
|
221
|
+
const error = await response.json();
|
|
222
|
+
console.error('[BlogPlugin] Save failed:', {
|
|
223
|
+
status: response.status,
|
|
224
|
+
statusText: response.statusText,
|
|
225
|
+
error,
|
|
226
|
+
missingFields: error.missingFields,
|
|
227
|
+
});
|
|
228
|
+
// Provide more detailed error message if available
|
|
229
|
+
const errorMessage = error.message || error.error || 'Failed to save post';
|
|
230
|
+
const missingFieldsMsg = error.missingFields && error.missingFields.length > 0
|
|
231
|
+
? ` Missing: ${error.missingFields.join(', ')}`
|
|
232
|
+
: '';
|
|
233
|
+
throw new Error(errorMessage + missingFieldsMsg);
|
|
234
|
+
}
|
|
235
|
+
const result = await response.json();
|
|
236
|
+
console.log('[BlogPlugin] Save successful:', result);
|
|
237
|
+
// If the slug changed, update the URL
|
|
238
|
+
if (result.slug && result.slug !== originalSlug) {
|
|
239
|
+
window.history.replaceState(null, '', `/dashboard/blog/editor/${result.slug}`);
|
|
240
|
+
}
|
|
241
|
+
return result;
|
|
242
|
+
}}
|
|
243
|
+
>
|
|
244
|
+
<CanvasEditorView postId={postId} siteId={siteId} locale={locale} darkMode={darkMode} backgroundColors={backgroundColors} />
|
|
245
|
+
</EditorProvider>
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
case 'new':
|
|
249
|
+
console.log('[BlogPlugin] Rendering new route with darkMode:', darkMode);
|
|
250
|
+
return (
|
|
251
|
+
<EditorProvider
|
|
252
|
+
customBlocks={customBlocks}
|
|
253
|
+
darkMode={darkMode}
|
|
254
|
+
backgroundColors={backgroundColors}
|
|
255
|
+
onSave={async (state) => {
|
|
256
|
+
// Save to API - create new post
|
|
257
|
+
const apiData = editorStateToAPI(state);
|
|
258
|
+
const response = await fetch('/api/plugin-blog/new', {
|
|
259
|
+
method: 'POST',
|
|
260
|
+
headers: { 'Content-Type': 'application/json' },
|
|
261
|
+
credentials: 'include', // Include cookies for authentication
|
|
262
|
+
body: JSON.stringify(apiData),
|
|
263
|
+
});
|
|
264
|
+
if (!response.ok) {
|
|
265
|
+
const error = await response.json();
|
|
266
|
+
throw new Error(error.message || 'Failed to create post');
|
|
267
|
+
}
|
|
268
|
+
const result = await response.json();
|
|
269
|
+
// Update the URL to the new post's slug
|
|
270
|
+
if (result.slug) {
|
|
271
|
+
window.history.replaceState(null, '', `/dashboard/blog/editor/${result.slug}`);
|
|
272
|
+
}
|
|
273
|
+
return result;
|
|
274
|
+
}}
|
|
275
|
+
>
|
|
276
|
+
<CanvasEditorView siteId={siteId} locale={locale} darkMode={darkMode} backgroundColors={backgroundColors} />
|
|
277
|
+
</EditorProvider>
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
case 'seo':
|
|
281
|
+
const seoPostId = subPath[1];
|
|
282
|
+
return <SlugSEOManagerView postId={seoPostId} siteId={siteId} locale={locale} />;
|
|
283
|
+
|
|
284
|
+
case 'preview':
|
|
285
|
+
const previewPostId = subPath[1];
|
|
286
|
+
return <PreviewBridgeView postId={previewPostId} siteId={siteId} locale={locale} />;
|
|
287
|
+
|
|
288
|
+
case 'settings':
|
|
289
|
+
case 'install':
|
|
290
|
+
return <SettingsView siteId={siteId} locale={locale} />;
|
|
291
|
+
|
|
292
|
+
default:
|
|
293
|
+
return <PostManagerView siteId={siteId} locale={locale} />;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
// Export for use as default
|
|
299
|
+
export { BlogPlugin as Index };
|
|
300
|
+
|
|
301
|
+
// Export types for client applications
|
|
302
|
+
export type {
|
|
303
|
+
Block,
|
|
304
|
+
BlockTypeDefinition,
|
|
305
|
+
ClientBlockDefinition,
|
|
306
|
+
RichTextFormattingConfig,
|
|
307
|
+
BlockEditProps,
|
|
308
|
+
BlockPreviewProps,
|
|
309
|
+
IBlockComponent,
|
|
310
|
+
} from './types';
|
|
311
|
+
|
|
312
|
+
// Export post types
|
|
313
|
+
export type {
|
|
314
|
+
SEOMetadata,
|
|
315
|
+
PublicationData,
|
|
316
|
+
PostStatus,
|
|
317
|
+
PostMetadata,
|
|
318
|
+
BlogPost,
|
|
319
|
+
PostListItem,
|
|
320
|
+
PostFilterOptions,
|
|
321
|
+
} from './types/post';
|
|
322
|
+
|
|
323
|
+
// Export initialization utility for easy setup
|
|
324
|
+
export { initBlogPlugin } from './init';
|
|
325
|
+
export type { BlogPluginConfig } from './init';
|
|
326
|
+
|
|
327
|
+
// Export rich text components for client applications
|
|
328
|
+
export { RichTextEditor, RichTextPreview } from './lib/rich-text';
|
|
329
|
+
export type { RichTextEditorProps, RichTextPreviewProps } from './lib/rich-text';
|
|
330
|
+
|
|
331
|
+
// Export hooks for client applications
|
|
332
|
+
export { useBlogs, useBlog } from './hooks';
|
|
333
|
+
export type { UseBlogsOptions, UseBlogsResult, UseBlogOptions, UseBlogResult } from './hooks';
|
|
334
|
+
|
|
335
|
+
// Export client utilities
|
|
336
|
+
export { fetchBlogs, fetchBlog } from './utils/client';
|
|
337
|
+
export type { FetchBlogsOptions, FetchBlogsResult, FetchBlogOptions } from './utils/client';
|
|
338
|
+
|
|
339
|
+
// Export block rendering components
|
|
340
|
+
export { BlockRenderer, BlocksRenderer } from './lib/blocks/BlockRenderer';
|
|
341
|
+
|
|
342
|
+
// Export block registry
|
|
343
|
+
export { blockRegistry } from './registry';
|
|
344
|
+
|
|
345
|
+
// Export layout block registration
|
|
346
|
+
export { registerLayoutBlocks } from './lib/layouts/registerLayoutBlocks';
|
|
347
|
+
|
|
348
|
+
// Export editor state management
|
|
349
|
+
export { EditorProvider, useEditor } from './state/EditorContext';
|
|
350
|
+
export type { EditorProviderProps, EditorState, EditorContextValue } from './state';
|
|
351
|
+
|
|
352
|
+
// Note: API handlers are server-only and exported from ./index.ts (server entry point)
|
|
353
|
+
// They are NOT exported here to prevent client/server context mixing
|
|
354
|
+
|
package/src/init.tsx
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Blog Plugin Initialization Utility
|
|
3
|
+
*
|
|
4
|
+
* Simple function to initialize the blog plugin with client configuration.
|
|
5
|
+
* Call this once in your app (e.g., in a script tag or root layout) to configure
|
|
6
|
+
* the blog plugin without needing a React component.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* import { initBlogPlugin } from '@jhits/plugin-blog/init';
|
|
11
|
+
* import { blogConfig } from '@/plugins/blog-config';
|
|
12
|
+
*
|
|
13
|
+
* // Call once when your app loads
|
|
14
|
+
* initBlogPlugin(blogConfig);
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
'use client';
|
|
19
|
+
|
|
20
|
+
import React from 'react';
|
|
21
|
+
import type { ClientBlockDefinition } from './types/block';
|
|
22
|
+
|
|
23
|
+
export interface BlogPluginConfig {
|
|
24
|
+
/** Custom blocks available in the editor */
|
|
25
|
+
customBlocks?: ClientBlockDefinition[];
|
|
26
|
+
/** Dark mode setting for the editor content area and wrappers (default: true) */
|
|
27
|
+
darkMode?: boolean;
|
|
28
|
+
/** Background colors for the editor */
|
|
29
|
+
backgroundColors?: {
|
|
30
|
+
/** Background color for light mode (REQUIRED) - CSS color value (hex, rgb, or named color) */
|
|
31
|
+
light: string;
|
|
32
|
+
/** Background color for dark mode (optional) - CSS color value (hex, rgb, or named color) */
|
|
33
|
+
dark?: string;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Initialize the blog plugin with client configuration
|
|
39
|
+
*
|
|
40
|
+
* This function sets up the window global that the plugin reads from automatically.
|
|
41
|
+
* Call this once when your app loads, before the plugin is rendered.
|
|
42
|
+
*
|
|
43
|
+
* @param config - Blog plugin configuration (customBlocks, darkMode, etc.)
|
|
44
|
+
*/
|
|
45
|
+
export function initBlogPlugin(config: BlogPluginConfig): void {
|
|
46
|
+
if (typeof window === 'undefined') {
|
|
47
|
+
// Server-side: no-op
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Initialize the global plugin props object if it doesn't exist
|
|
52
|
+
if (!(window as any).__JHITS_PLUGIN_PROPS__) {
|
|
53
|
+
(window as any).__JHITS_PLUGIN_PROPS__ = {};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Set blog plugin configuration
|
|
57
|
+
(window as any).__JHITS_PLUGIN_PROPS__['plugin-blog'] = {
|
|
58
|
+
customBlocks: config.customBlocks || [],
|
|
59
|
+
darkMode: config.darkMode !== undefined ? config.darkMode : true, // Default to true
|
|
60
|
+
backgroundColors: config.backgroundColors || undefined,
|
|
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
|
+
}
|
|
72
|
+
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Block Renderer
|
|
3
|
+
* Library component for rendering blocks (decoupled from editor)
|
|
4
|
+
* This is the "headless" rendering layer
|
|
5
|
+
*
|
|
6
|
+
* Multi-Tenant: Uses Preview components from client-provided blocks
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
'use client';
|
|
10
|
+
|
|
11
|
+
import React from 'react';
|
|
12
|
+
import { Block, BlockPreviewProps } from '../../types/block';
|
|
13
|
+
import { blockRegistry } from '../../registry/BlockRegistry';
|
|
14
|
+
import { getChildBlocks } from '../utils/blockHelpers';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Block Renderer Props
|
|
18
|
+
*/
|
|
19
|
+
export interface BlockRendererProps {
|
|
20
|
+
/** Block to render */
|
|
21
|
+
block: Block;
|
|
22
|
+
|
|
23
|
+
/** Custom renderers for specific block types (optional override) */
|
|
24
|
+
customRenderers?: Map<string, React.ComponentType<BlockPreviewProps>>;
|
|
25
|
+
|
|
26
|
+
/** Additional context for rendering */
|
|
27
|
+
context?: {
|
|
28
|
+
siteId?: string;
|
|
29
|
+
locale?: string;
|
|
30
|
+
[key: string]: unknown;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Block Renderer Component
|
|
36
|
+
* Renders a single block using its Preview component from the registry
|
|
37
|
+
*
|
|
38
|
+
* This is the headless rendering layer - it uses the Preview component
|
|
39
|
+
* provided by the client application, allowing each client to have
|
|
40
|
+
* their own design system in the frontend while the editor uses
|
|
41
|
+
* the dashboard's design system.
|
|
42
|
+
*/
|
|
43
|
+
export function BlockRenderer({
|
|
44
|
+
block,
|
|
45
|
+
customRenderers,
|
|
46
|
+
context = {}
|
|
47
|
+
}: BlockRendererProps) {
|
|
48
|
+
// Check for custom renderer override first
|
|
49
|
+
if (customRenderers?.has(block.type)) {
|
|
50
|
+
const CustomRenderer = customRenderers.get(block.type)!;
|
|
51
|
+
return <CustomRenderer block={block} context={context} />;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Get block definition from registry
|
|
55
|
+
const definition = blockRegistry.get(block.type);
|
|
56
|
+
if (!definition) {
|
|
57
|
+
console.warn(`Block type "${block.type}" not found in registry. Available types:`,
|
|
58
|
+
blockRegistry.getAll().map(b => b.type).join(', '));
|
|
59
|
+
return (
|
|
60
|
+
<div className="p-4 border border-red-300 bg-red-50 rounded">
|
|
61
|
+
<p className="text-red-600">Unknown block type: {block.type}</p>
|
|
62
|
+
<p className="text-xs text-red-500 mt-1">
|
|
63
|
+
Make sure this block type is registered via customBlocks prop
|
|
64
|
+
</p>
|
|
65
|
+
</div>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Use the Preview component from the block definition
|
|
70
|
+
// This is provided by the client application
|
|
71
|
+
const PreviewComponent = definition.components.Preview;
|
|
72
|
+
|
|
73
|
+
// Check if this is a container block with children
|
|
74
|
+
const isContainer = definition.isContainer === true;
|
|
75
|
+
const childBlocks = isContainer && block.children && Array.isArray(block.children) && block.children.length > 0
|
|
76
|
+
? (typeof block.children[0] === 'object'
|
|
77
|
+
? block.children as Block[]
|
|
78
|
+
: [])
|
|
79
|
+
: [];
|
|
80
|
+
|
|
81
|
+
// If container block, pass child blocks and render function
|
|
82
|
+
if (isContainer) {
|
|
83
|
+
return (
|
|
84
|
+
<PreviewComponent
|
|
85
|
+
block={block}
|
|
86
|
+
context={context}
|
|
87
|
+
childBlocks={childBlocks}
|
|
88
|
+
renderChild={(childBlock: Block) => (
|
|
89
|
+
<BlockRenderer
|
|
90
|
+
block={childBlock}
|
|
91
|
+
customRenderers={customRenderers}
|
|
92
|
+
context={context}
|
|
93
|
+
/>
|
|
94
|
+
)}
|
|
95
|
+
/>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return <PreviewComponent block={block} context={context} />;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Blocks Renderer
|
|
104
|
+
* Renders an array of blocks
|
|
105
|
+
*/
|
|
106
|
+
export interface BlocksRendererProps {
|
|
107
|
+
/** Array of blocks to render */
|
|
108
|
+
blocks: Block[];
|
|
109
|
+
|
|
110
|
+
/** Custom renderers for specific block types */
|
|
111
|
+
customRenderers?: Map<string, React.ComponentType<{ block: Block }>>;
|
|
112
|
+
|
|
113
|
+
/** Additional props to pass to renderers */
|
|
114
|
+
renderProps?: Record<string, unknown>;
|
|
115
|
+
|
|
116
|
+
/** Wrapper component for the blocks */
|
|
117
|
+
wrapper?: React.ComponentType<{ children: React.ReactNode }>;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function BlocksRenderer({
|
|
121
|
+
blocks,
|
|
122
|
+
customRenderers,
|
|
123
|
+
renderProps,
|
|
124
|
+
wrapper: Wrapper,
|
|
125
|
+
}: BlocksRendererProps) {
|
|
126
|
+
const content = blocks.map((block, index) => (
|
|
127
|
+
<BlockRenderer
|
|
128
|
+
key={block.id || index}
|
|
129
|
+
block={block}
|
|
130
|
+
customRenderers={customRenderers}
|
|
131
|
+
context={renderProps}
|
|
132
|
+
/>
|
|
133
|
+
));
|
|
134
|
+
|
|
135
|
+
if (Wrapper) {
|
|
136
|
+
return <Wrapper>{content}</Wrapper>;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return <>{content}</>;
|
|
140
|
+
}
|
|
141
|
+
|