@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,694 @@
1
+ /**
2
+ * Editor Reducer
3
+ * Pure function that handles state transitions
4
+ */
5
+
6
+ import { EditorState, EditorAction, initialEditorState } from './types';
7
+ import { Block } from '../types/block';
8
+
9
+ /**
10
+ * Generate a unique block ID
11
+ */
12
+ function generateBlockId(): string {
13
+ // Use crypto.randomUUID if available, otherwise fallback to timestamp-based
14
+ if (typeof crypto !== 'undefined' && crypto.randomUUID) {
15
+ return crypto.randomUUID();
16
+ }
17
+ return `block-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
18
+ }
19
+
20
+ /**
21
+ * Clone a block with a new ID
22
+ */
23
+ function cloneBlock(block: Block): Block {
24
+ return {
25
+ ...block,
26
+ id: generateBlockId(),
27
+ data: { ...block.data },
28
+ meta: block.meta ? { ...block.meta } : undefined,
29
+ children: block.children ? (Array.isArray(block.children[0])
30
+ ? (block.children as Block[]).map(cloneBlock)
31
+ : [...(block.children as string[])]) : undefined,
32
+ };
33
+ }
34
+
35
+ /**
36
+ * Find a block by ID recursively (including nested blocks)
37
+ */
38
+ function findBlockById(blocks: Block[], id: string): Block | null {
39
+ for (const block of blocks) {
40
+ if (block.id === id) {
41
+ return block;
42
+ }
43
+ if (block.children && Array.isArray(block.children) && block.children.length > 0) {
44
+ // Check if children are Block objects or IDs
45
+ if (typeof block.children[0] === 'object') {
46
+ const found = findBlockById(block.children as Block[], id);
47
+ if (found) return found;
48
+ }
49
+ }
50
+ }
51
+ return null;
52
+ }
53
+
54
+ /**
55
+ * Update blocks recursively to add a block to a container
56
+ */
57
+ function addBlockToContainer(
58
+ blocks: Block[],
59
+ containerId: string,
60
+ newBlock: Block,
61
+ index?: number
62
+ ): Block[] {
63
+ return blocks.map(block => {
64
+ // Check if this is the container (exact match or column container like "block-123-col-0")
65
+ const isContainer = block.id === containerId;
66
+ const isColumnContainer = containerId.startsWith(`${block.id}-col-`);
67
+
68
+ if (isContainer) {
69
+ // Direct container match
70
+ const currentChildren = Array.isArray(block.children)
71
+ ? (typeof block.children[0] === 'object'
72
+ ? block.children as Block[]
73
+ : [])
74
+ : [];
75
+ const updatedChildren = [...currentChildren];
76
+ if (index !== undefined && index >= 0 && index <= updatedChildren.length) {
77
+ updatedChildren.splice(index, 0, newBlock);
78
+ } else {
79
+ updatedChildren.push(newBlock);
80
+ }
81
+ return {
82
+ ...block,
83
+ children: updatedChildren,
84
+ };
85
+ } else if (isColumnContainer) {
86
+ // Column container - extract column index and store in block meta
87
+ const columnIndex = parseInt(containerId.split('-col-')[1] || '0', 10);
88
+ newBlock.meta = {
89
+ ...newBlock.meta,
90
+ columnIndex,
91
+ };
92
+
93
+ const currentChildren = Array.isArray(block.children)
94
+ ? (typeof block.children[0] === 'object'
95
+ ? block.children as Block[]
96
+ : [])
97
+ : [];
98
+ const updatedChildren = [...currentChildren];
99
+ if (index !== undefined && index >= 0 && index <= updatedChildren.length) {
100
+ updatedChildren.splice(index, 0, newBlock);
101
+ } else {
102
+ updatedChildren.push(newBlock);
103
+ }
104
+ return {
105
+ ...block,
106
+ children: updatedChildren,
107
+ };
108
+ }
109
+
110
+ // Recursively search nested blocks
111
+ if (block.children && Array.isArray(block.children) && block.children.length > 0) {
112
+ if (typeof block.children[0] === 'object') {
113
+ return {
114
+ ...block,
115
+ children: addBlockToContainer(block.children as Block[], containerId, newBlock, index),
116
+ };
117
+ }
118
+ }
119
+ return block;
120
+ });
121
+ }
122
+
123
+ /**
124
+ * Update blocks recursively to update a nested block
125
+ */
126
+ function updateNestedBlock(
127
+ blocks: Block[],
128
+ id: string,
129
+ data: Partial<Block['data']>
130
+ ): Block[] {
131
+ return blocks.map(block => {
132
+ if (block.id === id) {
133
+ return {
134
+ ...block,
135
+ data: { ...block.data, ...data },
136
+ };
137
+ }
138
+ if (block.children && Array.isArray(block.children) && block.children.length > 0) {
139
+ if (typeof block.children[0] === 'object') {
140
+ return {
141
+ ...block,
142
+ children: updateNestedBlock(block.children as Block[], id, data),
143
+ };
144
+ }
145
+ }
146
+ return block;
147
+ });
148
+ }
149
+
150
+ /**
151
+ * Update blocks recursively to delete a nested block
152
+ */
153
+ function deleteNestedBlock(blocks: Block[], id: string): Block[] {
154
+ return blocks
155
+ .filter(block => block.id !== id)
156
+ .map(block => {
157
+ if (block.children && Array.isArray(block.children) && block.children.length > 0) {
158
+ if (typeof block.children[0] === 'object') {
159
+ return {
160
+ ...block,
161
+ children: deleteNestedBlock(block.children as Block[], id),
162
+ };
163
+ }
164
+ }
165
+ return block;
166
+ });
167
+ }
168
+
169
+ /**
170
+ * Find and remove a block from wherever it is (root or nested)
171
+ */
172
+ function removeBlockFromTree(blocks: Block[], blockId: string): { updatedBlocks: Block[]; removedBlock: Block | null } {
173
+ let removedBlock: Block | null = null;
174
+
175
+ console.log('[removeBlockFromTree] Searching for block:', {
176
+ blockId,
177
+ rootBlocks: blocks.map(b => ({ id: b.id, type: b.type })),
178
+ });
179
+
180
+ // First check root level
181
+ const rootIndex = blocks.findIndex(b => b.id === blockId);
182
+ if (rootIndex !== -1) {
183
+ removedBlock = blocks[rootIndex];
184
+ console.log('[removeBlockFromTree] Found at root level:', {
185
+ blockId,
186
+ index: rootIndex,
187
+ block: { id: removedBlock.id, type: removedBlock.type },
188
+ });
189
+ return {
190
+ updatedBlocks: blocks.filter((_, i) => i !== rootIndex),
191
+ removedBlock,
192
+ };
193
+ }
194
+
195
+ // Then check nested blocks
196
+ const updatedBlocks = blocks.map(block => {
197
+ if (block.children && Array.isArray(block.children) && block.children.length > 0) {
198
+ if (typeof block.children[0] === 'object') {
199
+ const children = block.children as Block[];
200
+ const childIndex = children.findIndex(b => b.id === blockId);
201
+ if (childIndex !== -1) {
202
+ removedBlock = children[childIndex];
203
+ console.log('[removeBlockFromTree] Found in nested container:', {
204
+ blockId,
205
+ containerId: block.id,
206
+ containerType: block.type,
207
+ childIndex,
208
+ block: { id: removedBlock.id, type: removedBlock.type },
209
+ });
210
+ return {
211
+ ...block,
212
+ children: children.filter((_, i) => i !== childIndex),
213
+ };
214
+ }
215
+ // Recursively search nested children
216
+ const { updatedBlocks: updatedChildren, removedBlock: foundBlock } = removeBlockFromTree(children, blockId);
217
+ if (foundBlock) {
218
+ removedBlock = foundBlock;
219
+ console.log('[removeBlockFromTree] Found in deeper nesting:', {
220
+ blockId,
221
+ containerId: block.id,
222
+ block: { id: removedBlock.id, type: removedBlock.type },
223
+ });
224
+ return {
225
+ ...block,
226
+ children: updatedChildren,
227
+ };
228
+ }
229
+ }
230
+ }
231
+ return block;
232
+ });
233
+
234
+ if (!removedBlock) {
235
+ console.warn('[removeBlockFromTree] Block not found in tree:', { blockId });
236
+ }
237
+
238
+ return { updatedBlocks, removedBlock };
239
+ }
240
+
241
+ /**
242
+ * Update blocks recursively to move a nested block within the same container
243
+ */
244
+ function moveNestedBlock(
245
+ blocks: Block[],
246
+ containerId: string,
247
+ blockId: string,
248
+ newIndex: number
249
+ ): Block[] {
250
+ return blocks.map(block => {
251
+ if (block.id === containerId && block.children && Array.isArray(block.children)) {
252
+ const children = typeof block.children[0] === 'object'
253
+ ? block.children as Block[]
254
+ : [];
255
+ const currentIndex = children.findIndex(b => b.id === blockId);
256
+
257
+ if (currentIndex !== -1 && newIndex >= 0 && newIndex < children.length) {
258
+ const updatedChildren = [...children];
259
+ const [movedBlock] = updatedChildren.splice(currentIndex, 1);
260
+ updatedChildren.splice(newIndex, 0, movedBlock);
261
+ return {
262
+ ...block,
263
+ children: updatedChildren,
264
+ };
265
+ }
266
+ }
267
+ if (block.children && Array.isArray(block.children) && block.children.length > 0) {
268
+ if (typeof block.children[0] === 'object') {
269
+ return {
270
+ ...block,
271
+ children: moveNestedBlock(block.children as Block[], containerId, blockId, newIndex),
272
+ };
273
+ }
274
+ }
275
+ return block;
276
+ });
277
+ }
278
+
279
+ /**
280
+ * Move a block to a container (handles cross-container moves)
281
+ */
282
+ function moveBlockToContainer(
283
+ blocks: Block[],
284
+ blockId: string,
285
+ containerId: string,
286
+ newIndex: number
287
+ ): Block[] {
288
+ console.log('[moveBlockToContainer] Starting move:', {
289
+ blockId,
290
+ containerId,
291
+ newIndex,
292
+ });
293
+
294
+ // First, find and remove the block from wherever it is
295
+ const { updatedBlocks, removedBlock } = removeBlockFromTree(blocks, blockId);
296
+
297
+ if (!removedBlock) {
298
+ // Block not found, return unchanged
299
+ console.warn('[moveBlockToContainer] Block not found, cannot move');
300
+ return blocks;
301
+ }
302
+
303
+ console.log('[moveBlockToContainer] Block removed, now adding to container:', {
304
+ removedBlock: { id: removedBlock.id, type: removedBlock.type },
305
+ containerId,
306
+ newIndex,
307
+ });
308
+
309
+ // Handle column containers
310
+ const isColumnContainer = containerId.includes('-col-');
311
+ if (isColumnContainer) {
312
+ const [parentId, columnPart] = containerId.split('-col-');
313
+ const columnIndex = parseInt(columnPart || '0', 10);
314
+ removedBlock.meta = {
315
+ ...removedBlock.meta,
316
+ columnIndex,
317
+ };
318
+ console.log('[moveBlockToContainer] Setting column index:', { columnIndex });
319
+ }
320
+
321
+ // Now add the block to the target container
322
+ const result = addBlockToContainer(updatedBlocks, containerId, removedBlock, newIndex);
323
+
324
+ console.log('[moveBlockToContainer] Move complete:', {
325
+ resultBlocks: result.map(b => ({ id: b.id, type: b.type })),
326
+ });
327
+
328
+ return result;
329
+ }
330
+
331
+ /**
332
+ * Editor Reducer
333
+ * Handles all state transitions for the editor
334
+ */
335
+ export function editorReducer(
336
+ state: EditorState,
337
+ action: EditorAction
338
+ ): EditorState {
339
+ switch (action.type) {
340
+ case 'SET_BLOCKS':
341
+ return {
342
+ ...state,
343
+ blocks: action.payload,
344
+ isDirty: true,
345
+ };
346
+
347
+ case 'ADD_BLOCK': {
348
+ const { block, index, containerId } = action.payload;
349
+ const newBlock: Block = {
350
+ ...block,
351
+ id: block.id || generateBlockId(),
352
+ };
353
+
354
+ // If containerId is provided, add to container's children
355
+ if (containerId) {
356
+ const updatedBlocks = addBlockToContainer(state.blocks, containerId, newBlock, index);
357
+ return {
358
+ ...state,
359
+ blocks: updatedBlocks,
360
+ selectedBlockId: newBlock.id,
361
+ isDirty: true,
362
+ };
363
+ }
364
+
365
+ // Otherwise, add to root level
366
+ const newBlocks = [...state.blocks];
367
+ if (index !== undefined && index >= 0 && index <= newBlocks.length) {
368
+ newBlocks.splice(index, 0, newBlock);
369
+ } else {
370
+ newBlocks.push(newBlock);
371
+ }
372
+
373
+ return {
374
+ ...state,
375
+ blocks: newBlocks,
376
+ selectedBlockId: newBlock.id,
377
+ isDirty: true,
378
+ };
379
+ }
380
+
381
+ case 'UPDATE_BLOCK': {
382
+ const { id, data } = action.payload;
383
+ // Check if block is at root level
384
+ const rootBlock = state.blocks.find(block => block.id === id);
385
+ if (rootBlock) {
386
+ const newBlocks = state.blocks.map(block =>
387
+ block.id === id
388
+ ? {
389
+ ...block,
390
+ data: { ...block.data, ...data },
391
+ }
392
+ : block
393
+ );
394
+ return {
395
+ ...state,
396
+ blocks: newBlocks,
397
+ isDirty: true,
398
+ };
399
+ }
400
+
401
+ // Otherwise, update nested block
402
+ const newBlocks = updateNestedBlock(state.blocks, id, data);
403
+ return {
404
+ ...state,
405
+ blocks: newBlocks,
406
+ isDirty: true,
407
+ };
408
+ }
409
+
410
+ case 'DELETE_BLOCK': {
411
+ const { id } = action.payload;
412
+ // Check if block is at root level
413
+ const rootBlock = state.blocks.find(block => block.id === id);
414
+ if (rootBlock) {
415
+ const newBlocks = state.blocks.filter(block => block.id !== id);
416
+ const wasSelected = state.selectedBlockId === id;
417
+ return {
418
+ ...state,
419
+ blocks: newBlocks,
420
+ selectedBlockId: wasSelected ? null : state.selectedBlockId,
421
+ isDirty: true,
422
+ };
423
+ }
424
+
425
+ // Otherwise, delete nested block
426
+ const newBlocks = deleteNestedBlock(state.blocks, id);
427
+ const wasSelected = state.selectedBlockId === id;
428
+ return {
429
+ ...state,
430
+ blocks: newBlocks,
431
+ selectedBlockId: wasSelected ? null : state.selectedBlockId,
432
+ isDirty: true,
433
+ };
434
+ }
435
+
436
+ case 'DUPLICATE_BLOCK': {
437
+ const { id } = action.payload;
438
+ const blockIndex = state.blocks.findIndex(block => block.id === id);
439
+
440
+ if (blockIndex === -1) {
441
+ return state;
442
+ }
443
+
444
+ const blockToDuplicate = state.blocks[blockIndex];
445
+ const duplicatedBlock = cloneBlock(blockToDuplicate);
446
+
447
+ const newBlocks = [...state.blocks];
448
+ newBlocks.splice(blockIndex + 1, 0, duplicatedBlock);
449
+
450
+ return {
451
+ ...state,
452
+ blocks: newBlocks,
453
+ selectedBlockId: duplicatedBlock.id,
454
+ isDirty: true,
455
+ };
456
+ }
457
+
458
+ case 'MOVE_BLOCK': {
459
+ const { id, newIndex, containerId: rawContainerId } = action.payload;
460
+
461
+ // Normalize 'root' string to undefined
462
+ const containerId = rawContainerId === 'root' || rawContainerId === undefined ? undefined : rawContainerId;
463
+
464
+ console.log('[Reducer] MOVE_BLOCK action:', {
465
+ blockId: id,
466
+ newIndex,
467
+ rawContainerId,
468
+ normalizedContainerId: containerId || 'root',
469
+ currentRootBlocks: state.blocks.map(b => ({ id: b.id, type: b.type, hasChildren: !!b.children })),
470
+ });
471
+
472
+ // If containerId is provided (and not 'root'), move to/within container
473
+ if (containerId) {
474
+ // First check if block is already in this container
475
+ const containerBlock = findBlockById(state.blocks, containerId);
476
+ console.log('[Reducer] Container lookup:', {
477
+ containerId,
478
+ found: !!containerBlock,
479
+ hasChildren: containerBlock?.children ? Array.isArray(containerBlock.children) : false,
480
+ });
481
+
482
+ if (containerBlock && containerBlock.children && Array.isArray(containerBlock.children)) {
483
+ const children = typeof containerBlock.children[0] === 'object'
484
+ ? containerBlock.children as Block[]
485
+ : [];
486
+ const currentIndex = children.findIndex(b => b.id === id);
487
+
488
+ console.log('[Reducer] Block in container check:', {
489
+ blockId: id,
490
+ currentIndex,
491
+ containerChildren: children.map(b => ({ id: b.id, type: b.type })),
492
+ });
493
+
494
+ if (currentIndex !== -1) {
495
+ // Block is already in this container - move within container
496
+ console.log('[Reducer] Moving within container');
497
+ const newBlocks = moveNestedBlock(state.blocks, containerId, id, newIndex);
498
+ console.log('[Reducer] After move within container:', {
499
+ newRootBlocks: newBlocks.map(b => ({ id: b.id, type: b.type })),
500
+ });
501
+ return {
502
+ ...state,
503
+ blocks: newBlocks,
504
+ isDirty: true,
505
+ };
506
+ }
507
+ }
508
+
509
+ // Block is not in this container - move it from wherever it is (root or nested)
510
+ console.log('[Reducer] Moving block to container from elsewhere');
511
+ const newBlocks = moveBlockToContainer(state.blocks, id, containerId, newIndex);
512
+ console.log('[Reducer] After move to container:', {
513
+ newRootBlocks: newBlocks.map(b => ({ id: b.id, type: b.type })),
514
+ });
515
+ return {
516
+ ...state,
517
+ blocks: newBlocks,
518
+ isDirty: true,
519
+ };
520
+ }
521
+
522
+ // Moving to root level (containerId is undefined)
523
+ const currentIndex = state.blocks.findIndex(block => block.id === id);
524
+
525
+ console.log('[Reducer] Moving to root level:', {
526
+ blockId: id,
527
+ currentIndex,
528
+ isInRoot: currentIndex !== -1,
529
+ newIndex,
530
+ rootBlocksCount: state.blocks.length,
531
+ });
532
+
533
+ if (currentIndex !== -1) {
534
+ // Block is already at root level - move within root
535
+ if (newIndex < 0 || newIndex >= state.blocks.length) {
536
+ console.warn('[Reducer] Invalid newIndex for root move:', { newIndex, blocksLength: state.blocks.length });
537
+ return state;
538
+ }
539
+
540
+ console.log('[Reducer] Moving within root level');
541
+ const newBlocks = [...state.blocks];
542
+ const [movedBlock] = newBlocks.splice(currentIndex, 1);
543
+ newBlocks.splice(newIndex, 0, movedBlock);
544
+
545
+ console.log('[Reducer] After move within root:', {
546
+ newRootBlocks: newBlocks.map(b => ({ id: b.id, type: b.type })),
547
+ });
548
+
549
+ return {
550
+ ...state,
551
+ blocks: newBlocks,
552
+ isDirty: true,
553
+ };
554
+ }
555
+
556
+ // Block is nested somewhere - move it to root level
557
+ console.log('[Reducer] Block is nested, removing from tree and adding to root');
558
+ const { updatedBlocks, removedBlock } = removeBlockFromTree(state.blocks, id);
559
+
560
+ console.log('[Reducer] Block removal result:', {
561
+ removedBlock: removedBlock ? { id: removedBlock.id, type: removedBlock.type } : null,
562
+ updatedBlocksCount: updatedBlocks.length,
563
+ });
564
+
565
+ if (removedBlock) {
566
+ // Clear any nested metadata (like columnIndex)
567
+ const cleanedBlock = {
568
+ ...removedBlock,
569
+ meta: removedBlock.meta ? {
570
+ ...removedBlock.meta,
571
+ columnIndex: undefined,
572
+ } : undefined,
573
+ };
574
+
575
+ const newBlocks = [...updatedBlocks];
576
+ if (newIndex >= 0 && newIndex <= newBlocks.length) {
577
+ newBlocks.splice(newIndex, 0, cleanedBlock);
578
+ } else {
579
+ newBlocks.push(cleanedBlock);
580
+ }
581
+
582
+ console.log('[Reducer] After move nested to root:', {
583
+ newRootBlocks: newBlocks.map(b => ({ id: b.id, type: b.type })),
584
+ insertedAt: newIndex >= 0 && newIndex <= updatedBlocks.length ? newIndex : newBlocks.length - 1,
585
+ });
586
+
587
+ return {
588
+ ...state,
589
+ blocks: newBlocks,
590
+ isDirty: true,
591
+ };
592
+ }
593
+
594
+ console.warn('[Reducer] Block not found in tree:', { blockId: id });
595
+ return state;
596
+ }
597
+
598
+ case 'SET_TITLE':
599
+ return {
600
+ ...state,
601
+ title: action.payload,
602
+ isDirty: true,
603
+ };
604
+
605
+ case 'SET_SLUG':
606
+ return {
607
+ ...state,
608
+ slug: action.payload,
609
+ isDirty: true,
610
+ };
611
+
612
+ case 'SET_SEO':
613
+ return {
614
+ ...state,
615
+ seo: { ...state.seo, ...action.payload },
616
+ isDirty: true,
617
+ };
618
+
619
+ case 'SET_METADATA':
620
+ return {
621
+ ...state,
622
+ metadata: { ...state.metadata, ...action.payload },
623
+ isDirty: true,
624
+ };
625
+
626
+ case 'SET_STATUS':
627
+ return {
628
+ ...state,
629
+ status: action.payload,
630
+ isDirty: true,
631
+ };
632
+
633
+ case 'SET_FOCUS_MODE':
634
+ return {
635
+ ...state,
636
+ focusMode: action.payload,
637
+ };
638
+
639
+ case 'SELECT_BLOCK':
640
+ return {
641
+ ...state,
642
+ selectedBlockId: action.payload,
643
+ };
644
+
645
+ case 'SET_DRAGGED_BLOCK':
646
+ return {
647
+ ...state,
648
+ draggedBlockId: action.payload,
649
+ };
650
+
651
+ case 'LOAD_POST': {
652
+ const post = action.payload;
653
+ return {
654
+ ...state,
655
+ blocks: post.blocks,
656
+ title: post.title,
657
+ slug: post.slug,
658
+ seo: post.seo,
659
+ metadata: post.metadata,
660
+ status: post.publication.status,
661
+ postId: post.id,
662
+ isDirty: false,
663
+ selectedBlockId: null,
664
+ };
665
+ }
666
+
667
+ case 'RESET_EDITOR':
668
+ return {
669
+ ...initialEditorState,
670
+ };
671
+
672
+ case 'MARK_CLEAN':
673
+ return {
674
+ ...state,
675
+ isDirty: false,
676
+ };
677
+
678
+ case 'MARK_DIRTY':
679
+ return {
680
+ ...state,
681
+ isDirty: true,
682
+ };
683
+
684
+ case 'UNDO':
685
+ case 'REDO':
686
+ case 'SAVE_HISTORY':
687
+ // These are handled by the context, not the reducer
688
+ return state;
689
+
690
+ default:
691
+ return state;
692
+ }
693
+ }
694
+