@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.
Files changed (75) hide show
  1. package/README.md +216 -0
  2. package/package.json +57 -0
  3. package/src/api/README.md +224 -0
  4. package/src/api/categories.ts +43 -0
  5. package/src/api/check-title.ts +60 -0
  6. package/src/api/handler.ts +419 -0
  7. package/src/api/index.ts +33 -0
  8. package/src/api/route.ts +116 -0
  9. package/src/api/router.ts +114 -0
  10. package/src/api-server.ts +11 -0
  11. package/src/config.ts +161 -0
  12. package/src/hooks/README.md +91 -0
  13. package/src/hooks/index.ts +8 -0
  14. package/src/hooks/useBlog.ts +85 -0
  15. package/src/hooks/useBlogs.ts +123 -0
  16. package/src/index.server.ts +12 -0
  17. package/src/index.tsx +354 -0
  18. package/src/init.tsx +72 -0
  19. package/src/lib/blocks/BlockRenderer.tsx +141 -0
  20. package/src/lib/blocks/index.ts +6 -0
  21. package/src/lib/index.ts +9 -0
  22. package/src/lib/layouts/blocks/ColumnsBlock.tsx +134 -0
  23. package/src/lib/layouts/blocks/SectionBlock.tsx +104 -0
  24. package/src/lib/layouts/blocks/index.ts +8 -0
  25. package/src/lib/layouts/index.ts +52 -0
  26. package/src/lib/layouts/registerLayoutBlocks.ts +59 -0
  27. package/src/lib/mappers/apiMapper.ts +223 -0
  28. package/src/lib/migration/index.ts +6 -0
  29. package/src/lib/migration/mapper.ts +140 -0
  30. package/src/lib/rich-text/RichTextEditor.tsx +826 -0
  31. package/src/lib/rich-text/RichTextPreview.tsx +210 -0
  32. package/src/lib/rich-text/index.ts +10 -0
  33. package/src/lib/utils/blockHelpers.ts +72 -0
  34. package/src/lib/utils/configValidation.ts +137 -0
  35. package/src/lib/utils/index.ts +8 -0
  36. package/src/lib/utils/slugify.ts +79 -0
  37. package/src/registry/BlockRegistry.ts +142 -0
  38. package/src/registry/index.ts +11 -0
  39. package/src/state/EditorContext.tsx +277 -0
  40. package/src/state/index.ts +8 -0
  41. package/src/state/reducer.ts +694 -0
  42. package/src/state/types.ts +160 -0
  43. package/src/types/block.ts +269 -0
  44. package/src/types/index.ts +15 -0
  45. package/src/types/post.ts +165 -0
  46. package/src/utils/README.md +75 -0
  47. package/src/utils/client.ts +122 -0
  48. package/src/utils/index.ts +9 -0
  49. package/src/views/CanvasEditor/BlockWrapper.tsx +459 -0
  50. package/src/views/CanvasEditor/CanvasEditorView.tsx +917 -0
  51. package/src/views/CanvasEditor/EditorBody.tsx +475 -0
  52. package/src/views/CanvasEditor/EditorHeader.tsx +179 -0
  53. package/src/views/CanvasEditor/LayoutContainer.tsx +494 -0
  54. package/src/views/CanvasEditor/SaveConfirmationModal.tsx +233 -0
  55. package/src/views/CanvasEditor/components/CustomBlockItem.tsx +92 -0
  56. package/src/views/CanvasEditor/components/FeaturedMediaSection.tsx +130 -0
  57. package/src/views/CanvasEditor/components/LibraryItem.tsx +80 -0
  58. package/src/views/CanvasEditor/components/PrivacySettingsSection.tsx +212 -0
  59. package/src/views/CanvasEditor/components/index.ts +17 -0
  60. package/src/views/CanvasEditor/index.ts +16 -0
  61. package/src/views/PostManager/EmptyState.tsx +42 -0
  62. package/src/views/PostManager/PostActionsMenu.tsx +112 -0
  63. package/src/views/PostManager/PostCards.tsx +192 -0
  64. package/src/views/PostManager/PostFilters.tsx +80 -0
  65. package/src/views/PostManager/PostManagerView.tsx +280 -0
  66. package/src/views/PostManager/PostStats.tsx +81 -0
  67. package/src/views/PostManager/PostTable.tsx +225 -0
  68. package/src/views/PostManager/index.ts +15 -0
  69. package/src/views/Preview/PreviewBridgeView.tsx +64 -0
  70. package/src/views/Preview/index.ts +7 -0
  71. package/src/views/README.md +82 -0
  72. package/src/views/Settings/SettingsView.tsx +298 -0
  73. package/src/views/Settings/index.ts +7 -0
  74. package/src/views/SlugSEO/SlugSEOManagerView.tsx +94 -0
  75. package/src/views/SlugSEO/index.ts +7 -0
@@ -0,0 +1,277 @@
1
+ /**
2
+ * Editor Context
3
+ * React Context for managing editor state
4
+ * Multi-Tenant: Accepts custom blocks from client applications
5
+ */
6
+
7
+ 'use client';
8
+
9
+ import React, { createContext, useContext, useReducer, useCallback, useMemo, useEffect, useRef, useState } from 'react';
10
+ import { editorReducer } from './reducer';
11
+ import { EditorContextValue, EditorState, EditorAction, initialEditorState } from './types';
12
+ import { Block } from '../types/block';
13
+ import { BlogPost } from '../types/post';
14
+ import { blockRegistry } from '../registry/BlockRegistry';
15
+ import { ClientBlockDefinition } from '../types/block';
16
+ import { registerLayoutBlocks } from '../lib/layouts/registerLayoutBlocks';
17
+
18
+ // Create the context
19
+ const EditorContext = createContext<EditorContextValue | null>(null);
20
+
21
+ /**
22
+ * Editor Provider Props
23
+ */
24
+ export interface EditorProviderProps {
25
+ children: React.ReactNode;
26
+ /** Initial state (optional) */
27
+ initialState?: Partial<EditorState>;
28
+ /** Callback when save is triggered */
29
+ onSave?: (state: EditorState) => Promise<void>;
30
+ /**
31
+ * Custom blocks from client application
32
+ * These blocks will be registered in the BlockRegistry on mount
33
+ */
34
+ customBlocks?: ClientBlockDefinition[];
35
+ /** Enable dark mode for content area and wrappers (default: true) */
36
+ darkMode?: boolean;
37
+ /** Background colors for the editor */
38
+ backgroundColors?: {
39
+ /** Background color for light mode (REQUIRED) */
40
+ light: string;
41
+ /** Background color for dark mode (optional) */
42
+ dark?: string;
43
+ };
44
+ }
45
+
46
+ /**
47
+ * Editor Provider
48
+ * Provides editor state and actions to child components
49
+ * Automatically registers client-provided blocks on mount
50
+ */
51
+ export function EditorProvider({
52
+ children,
53
+ initialState,
54
+ onSave,
55
+ customBlocks = [],
56
+ darkMode = true,
57
+ backgroundColors
58
+ }: EditorProviderProps) {
59
+ console.log('[EditorProvider] Rendering with darkMode:', darkMode);
60
+
61
+ // Register core layout blocks on mount
62
+ useEffect(() => {
63
+ console.log('[EditorContext] Registering layout blocks...');
64
+ registerLayoutBlocks();
65
+ const layoutBlocks = blockRegistry.getByCategory('layout');
66
+ console.log('[EditorContext] Layout blocks registered:', layoutBlocks.length, layoutBlocks.map(b => b.type));
67
+ }, []);
68
+
69
+ // Register client blocks on mount
70
+ useEffect(() => {
71
+ if (customBlocks && customBlocks.length > 0) {
72
+ try {
73
+ console.log('[EditorContext] Registering custom blocks:', customBlocks.length, customBlocks.map(b => b.type));
74
+ blockRegistry.registerClientBlocks(customBlocks);
75
+ const allBlocks = blockRegistry.getAll();
76
+ console.log('[EditorContext] Total blocks after registration:', allBlocks.length, allBlocks.map(b => b.type));
77
+ } catch (error) {
78
+ console.error('[EditorContext] Failed to register custom blocks:', error);
79
+ }
80
+ } else {
81
+ console.log('[EditorContext] No custom blocks provided');
82
+ }
83
+ }, [customBlocks]);
84
+
85
+ const [state, dispatch] = useReducer(
86
+ editorReducer,
87
+ { ...initialEditorState, ...initialState }
88
+ );
89
+
90
+ // Use a ref to always have access to the latest state in callbacks
91
+ const stateRef = useRef(state);
92
+ stateRef.current = state;
93
+
94
+ // History state for undo/redo
95
+ const [history, setHistory] = useState<EditorState[]>([]);
96
+ const [historyIndex, setHistoryIndex] = useState(-1);
97
+ const isRestoringRef = useRef(false);
98
+ const MAX_HISTORY = 50; // Limit history to prevent memory issues
99
+
100
+ // Save current state to history after state changes (but not during undo/redo)
101
+ useEffect(() => {
102
+ if (isRestoringRef.current) {
103
+ isRestoringRef.current = false;
104
+ return;
105
+ }
106
+
107
+ // Save current state to history
108
+ setHistory(prev => {
109
+ const newHistory = [...prev];
110
+ // Remove any future history if we're not at the end
111
+ if (historyIndex < newHistory.length - 1) {
112
+ newHistory.splice(historyIndex + 1);
113
+ }
114
+ // Add current state
115
+ newHistory.push({ ...state });
116
+ // Limit history size
117
+ if (newHistory.length > MAX_HISTORY) {
118
+ newHistory.shift();
119
+ return newHistory;
120
+ }
121
+ return newHistory;
122
+ });
123
+ setHistoryIndex(prev => {
124
+ const newIndex = prev + 1;
125
+ return newIndex >= MAX_HISTORY ? MAX_HISTORY - 1 : newIndex;
126
+ });
127
+ }, [state.blocks, state.title, state.slug, state.seo, state.metadata, state.status]);
128
+
129
+ // Helper: Add a new block (supports nested containers)
130
+ const addBlock = useCallback((type: string, index?: number, containerId?: string) => {
131
+ const blockDefinition = blockRegistry.get(type);
132
+ if (!blockDefinition) {
133
+ console.warn(`Block type "${type}" not found in registry. Available types:`,
134
+ blockRegistry.getAll().map(b => b.type).join(', '));
135
+ return;
136
+ }
137
+
138
+ const newBlock: Block = {
139
+ id: `block-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
140
+ type,
141
+ data: { ...blockDefinition.defaultData },
142
+ };
143
+
144
+ dispatch({ type: 'ADD_BLOCK', payload: { block: newBlock, index, containerId } });
145
+ }, []);
146
+
147
+ // Helper: Update a block
148
+ const updateBlock = useCallback((id: string, data: Partial<Block['data']>) => {
149
+ dispatch({ type: 'UPDATE_BLOCK', payload: { id, data } });
150
+ }, []);
151
+
152
+ // Helper: Delete a block
153
+ const deleteBlock = useCallback((id: string) => {
154
+ dispatch({ type: 'DELETE_BLOCK', payload: { id } });
155
+ }, []);
156
+
157
+ // Helper: Duplicate a block
158
+ const duplicateBlock = useCallback((id: string) => {
159
+ dispatch({ type: 'DUPLICATE_BLOCK', payload: { id } });
160
+ }, []);
161
+
162
+ // Helper: Move a block (supports nested containers)
163
+ const moveBlock = useCallback((id: string, newIndex: number, containerId?: string) => {
164
+ dispatch({ type: 'MOVE_BLOCK', payload: { id, newIndex, containerId } });
165
+ }, []);
166
+
167
+ // Helper: Load a post
168
+ const loadPost = useCallback((post: BlogPost) => {
169
+ dispatch({ type: 'LOAD_POST', payload: post });
170
+ }, []);
171
+
172
+ // Helper: Reset editor
173
+ const resetEditor = useCallback(() => {
174
+ dispatch({ type: 'RESET_EDITOR' });
175
+ }, []);
176
+
177
+ // Helper: Save
178
+ // Uses stateRef to always get the latest state, avoiding stale closure issues
179
+ const save = useCallback(async () => {
180
+ if (onSave) {
181
+ // Use stateRef.current to get the absolute latest state
182
+ // This ensures we don't have stale closure issues with React state updates
183
+ console.log('[EditorContext] save() called with status:', stateRef.current.status);
184
+ await onSave(stateRef.current);
185
+ dispatch({ type: 'MARK_CLEAN' });
186
+ }
187
+ }, [onSave]);
188
+
189
+ // Helper: Undo
190
+ const undo = useCallback(() => {
191
+ if (historyIndex > 0 && history.length > 0) {
192
+ const previousState = history[historyIndex - 1];
193
+ if (previousState) {
194
+ isRestoringRef.current = true;
195
+ setHistoryIndex(prev => prev - 1);
196
+ dispatch({ type: 'LOAD_POST', payload: {
197
+ id: previousState.postId || '',
198
+ title: previousState.title,
199
+ slug: previousState.slug,
200
+ blocks: previousState.blocks,
201
+ seo: previousState.seo,
202
+ publication: {
203
+ status: previousState.status,
204
+ authorId: undefined,
205
+ },
206
+ metadata: previousState.metadata,
207
+ createdAt: new Date().toISOString(),
208
+ updatedAt: new Date().toISOString(),
209
+ } });
210
+ }
211
+ }
212
+ }, [history, historyIndex, dispatch]);
213
+
214
+ // Helper: Redo
215
+ const redo = useCallback(() => {
216
+ if (historyIndex < history.length - 1) {
217
+ const nextState = history[historyIndex + 1];
218
+ if (nextState) {
219
+ isRestoringRef.current = true;
220
+ setHistoryIndex(prev => prev + 1);
221
+ dispatch({ type: 'LOAD_POST', payload: {
222
+ id: nextState.postId || '',
223
+ title: nextState.title,
224
+ slug: nextState.slug,
225
+ blocks: nextState.blocks,
226
+ seo: nextState.seo,
227
+ publication: {
228
+ status: nextState.status,
229
+ authorId: undefined,
230
+ },
231
+ metadata: nextState.metadata,
232
+ createdAt: new Date().toISOString(),
233
+ updatedAt: new Date().toISOString(),
234
+ } });
235
+ }
236
+ }
237
+ }, [history, historyIndex, dispatch]);
238
+
239
+ // Memoize the context value
240
+ const value = useMemo<EditorContextValue>(
241
+ () => ({
242
+ state,
243
+ dispatch,
244
+ darkMode,
245
+ backgroundColors,
246
+ helpers: {
247
+ addBlock,
248
+ updateBlock,
249
+ deleteBlock,
250
+ duplicateBlock,
251
+ moveBlock,
252
+ loadPost,
253
+ resetEditor,
254
+ save,
255
+ undo,
256
+ redo,
257
+ },
258
+ canUndo: historyIndex > 0 && history.length > 0,
259
+ canRedo: historyIndex < history.length - 1,
260
+ }),
261
+ [state, dispatch, darkMode, backgroundColors, addBlock, updateBlock, deleteBlock, duplicateBlock, moveBlock, loadPost, resetEditor, save, undo, redo, historyIndex, history.length]
262
+ );
263
+
264
+ return <EditorContext.Provider value={value}>{children}</EditorContext.Provider>;
265
+ }
266
+
267
+ /**
268
+ * Hook to access editor context
269
+ * @throws Error if used outside EditorProvider
270
+ */
271
+ export function useEditor(): EditorContextValue {
272
+ const context = useContext(EditorContext);
273
+ if (!context) {
274
+ throw new Error('useEditor must be used within an EditorProvider');
275
+ }
276
+ return context;
277
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * State management exports
3
+ */
4
+
5
+ export * from './types';
6
+ export * from './reducer';
7
+ export * from './EditorContext';
8
+