@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,475 @@
1
+ /**
2
+ * Editor Body Component
3
+ * Primary root container for the editor canvas
4
+ * Acts as the main drop zone for blocks
5
+ */
6
+
7
+ 'use client';
8
+
9
+ import React, { useState, useRef, Fragment, useEffect } from 'react';
10
+ import { Plus } from 'lucide-react';
11
+ import { Block } from '../../types/block';
12
+ import { BlockWrapper } from './BlockWrapper';
13
+
14
+ export interface EditorBodyProps {
15
+ blocks: Block[];
16
+ onBlockAdd: (type: string, index: number, containerId?: string) => void;
17
+ onBlockUpdate: (id: string, data: Partial<Block['data']>) => void;
18
+ onBlockDelete: (id: string) => void;
19
+ onBlockMove: (id: string, newIndex: number, containerId?: string) => void;
20
+ /** Enable dark mode for content area and wrappers (default: true) */
21
+ darkMode?: boolean;
22
+ /** Background colors for the editor */
23
+ backgroundColors?: {
24
+ light: string;
25
+ dark?: string;
26
+ };
27
+ }
28
+
29
+ export function EditorBody({
30
+ blocks,
31
+ onBlockAdd,
32
+ onBlockUpdate,
33
+ onBlockDelete,
34
+ onBlockMove,
35
+ darkMode = true,
36
+ backgroundColors,
37
+ }: EditorBodyProps) {
38
+ // Debug: Log darkMode value
39
+ useEffect(() => {
40
+ console.log('[EditorBody] darkMode:', darkMode);
41
+ }, [darkMode]);
42
+
43
+ const [dragOverIndex, setDragOverIndex] = useState<number | null>(null);
44
+ const [isDragging, setIsDragging] = useState(false);
45
+ const [draggedBlockId, setDraggedBlockId] = useState<string | null>(null);
46
+ const [dropIndicatorPosition, setDropIndicatorPosition] = useState<{ top: number; left: number; width: number } | null>(null);
47
+ const containerRef = React.useRef<HTMLDivElement>(null);
48
+
49
+ // Listen for clear-drop-indicator events from nested containers
50
+ React.useEffect(() => {
51
+ const handleClearIndicator = () => {
52
+ setDropIndicatorPosition(null);
53
+ setDragOverIndex(null);
54
+ };
55
+
56
+ const container = containerRef.current;
57
+ if (container) {
58
+ container.addEventListener('clear-drop-indicator', handleClearIndicator);
59
+ return () => {
60
+ container.removeEventListener('clear-drop-indicator', handleClearIndicator);
61
+ };
62
+ }
63
+ }, []);
64
+
65
+ // Global drag end handler - clear all indicators when drag ends
66
+ React.useEffect(() => {
67
+ const handleGlobalDragEnd = () => {
68
+ setDropIndicatorPosition(null);
69
+ setDragOverIndex(null);
70
+ setIsDragging(false);
71
+ setDraggedBlockId(null);
72
+ };
73
+
74
+ document.addEventListener('dragend', handleGlobalDragEnd);
75
+ return () => {
76
+ document.removeEventListener('dragend', handleGlobalDragEnd);
77
+ };
78
+ }, []);
79
+ // Store refs for all blocks - use callback refs to avoid hook violations
80
+ const blockRefs = React.useRef<Map<string, HTMLDivElement>>(new Map());
81
+
82
+ const setBlockRef = (blockId: string) => (el: HTMLDivElement | null) => {
83
+ if (el) {
84
+ blockRefs.current.set(blockId, el);
85
+ } else {
86
+ blockRefs.current.delete(blockId);
87
+ }
88
+ };
89
+
90
+ const handleDragStart = (e: React.DragEvent) => {
91
+ // This is called when dragging starts from the container
92
+ // The actual block drag start is handled in BlockWrapper
93
+ setIsDragging(true);
94
+ };
95
+
96
+ const handleDragOver = (e: React.DragEvent, index: number, element?: HTMLElement) => {
97
+ e.preventDefault();
98
+ e.stopPropagation();
99
+
100
+ // Check if we're dragging over a nested container - if so, clear our indicator
101
+ const target = e.target as HTMLElement;
102
+ const nestedContainer = target.closest('[data-layout-container]');
103
+ if (nestedContainer && nestedContainer !== containerRef.current) {
104
+ // We're over a nested container, clear our indicator
105
+ setDropIndicatorPosition(null);
106
+ setDragOverIndex(null);
107
+ return;
108
+ }
109
+
110
+ setDragOverIndex(index);
111
+
112
+ // Calculate position for absolute drop indicator using mouse position
113
+ const container = containerRef.current;
114
+ if (container) {
115
+ requestAnimationFrame(() => {
116
+ // Double-check refs are still valid inside RAF callback
117
+ if (!containerRef.current) return;
118
+
119
+ const containerRect = containerRef.current.getBoundingClientRect();
120
+ // Use mouse Y position relative to container for accurate positioning
121
+ const mouseY = e.clientY;
122
+ const relativeTop = mouseY - containerRect.top;
123
+
124
+ // Determine if we should show above or below the element
125
+ let finalTop = relativeTop;
126
+ if (element) {
127
+ const elementRect = element.getBoundingClientRect();
128
+ const elementTop = elementRect.top - containerRect.top;
129
+ const elementCenter = elementTop + elementRect.height / 2;
130
+
131
+ // If mouse is in upper half of element, show above; otherwise show below
132
+ if (relativeTop < elementCenter) {
133
+ finalTop = elementTop;
134
+ } else {
135
+ finalTop = elementTop + elementRect.height;
136
+ }
137
+ }
138
+
139
+ // Calculate width - use container width minus padding (32px on each side)
140
+ const padding = 32;
141
+ const width = containerRect.width - (padding * 2);
142
+
143
+ setDropIndicatorPosition({
144
+ top: finalTop,
145
+ left: padding,
146
+ width: width,
147
+ });
148
+ });
149
+ }
150
+ };
151
+
152
+ const handleDragLeave = (e: React.DragEvent) => {
153
+ // Only clear if we're actually leaving the container
154
+ // Check if we're moving to a child element (nested container)
155
+ const relatedTarget = e.relatedTarget as Node;
156
+ if (!e.currentTarget.contains(relatedTarget)) {
157
+ // Check if relatedTarget is inside a nested LayoutContainer
158
+ const isMovingToNested = relatedTarget && (relatedTarget as HTMLElement).closest('[data-layout-container]');
159
+ if (!isMovingToNested) {
160
+ setDragOverIndex(null);
161
+ setDropIndicatorPosition(null);
162
+ }
163
+ }
164
+ };
165
+
166
+ const handleDrop = (e: React.DragEvent, index: number, containerId?: string) => {
167
+ e.preventDefault();
168
+ e.stopPropagation();
169
+
170
+ const dataTransferBlockId = e.dataTransfer.getData('block-id');
171
+ const globalBlockId = typeof window !== 'undefined' ? (window as any).__DRAGGED_BLOCK_ID__ : null;
172
+ const blockId = dataTransferBlockId || globalBlockId;
173
+ const blockType = e.dataTransfer.getData('block-type');
174
+
175
+ console.log('[EditorBody] Drop Event:', {
176
+ index,
177
+ containerId,
178
+ dataTransferBlockId,
179
+ globalBlockId,
180
+ resolvedBlockId: blockId,
181
+ blockType,
182
+ currentBlocks: blocks.map(b => ({ id: b.id, type: b.type })),
183
+ rootBlockIds: blocks.map(b => b.id),
184
+ });
185
+
186
+ // Clear the global dragged block ID
187
+ if (typeof window !== 'undefined') {
188
+ (window as any).__DRAGGED_BLOCK_ID__ = null;
189
+ }
190
+
191
+ if (blockId) {
192
+ // Moving existing block - check if it's in root or nested
193
+ const currentIndex = blocks.findIndex(b => b.id === blockId);
194
+ console.log('[EditorBody] Block location check:', {
195
+ blockId,
196
+ currentIndex,
197
+ isInRoot: currentIndex !== -1,
198
+ targetIndex: index,
199
+ containerId,
200
+ });
201
+
202
+ if (currentIndex !== -1) {
203
+ // Block is in root level - move to container or within root
204
+ if (containerId) {
205
+ // Moving from root to container
206
+ console.log('[EditorBody] Moving from root to container:', { blockId, index, containerId });
207
+ onBlockMove(blockId, index, containerId);
208
+ } else if (currentIndex !== index) {
209
+ // Moving within root level
210
+ const targetIndex = currentIndex < index ? index - 1 : index;
211
+ console.log('[EditorBody] Moving within root:', { blockId, currentIndex, targetIndex });
212
+ onBlockMove(blockId, targetIndex);
213
+ } else {
214
+ console.log('[EditorBody] Block already at target position, no move needed');
215
+ }
216
+ } else {
217
+ // Block is nested somewhere - need to move it to this location
218
+ console.log('[EditorBody] Moving nested block to root:', { blockId, index, containerId: containerId || 'root' });
219
+ // Pass undefined containerId to move to root
220
+ onBlockMove(blockId, index, undefined);
221
+ }
222
+ } else if (blockType) {
223
+ // Adding new block from library
224
+ console.log('[EditorBody] Adding new block from library:', { blockType, index, containerId });
225
+ onBlockAdd(blockType, index, containerId);
226
+ } else {
227
+ console.warn('[EditorBody] Drop event with no block ID or type!');
228
+ }
229
+
230
+ setDragOverIndex(null);
231
+ setIsDragging(false);
232
+ setDraggedBlockId(null);
233
+ setDropIndicatorPosition(null);
234
+ };
235
+
236
+ const handleBodyDragOver = (e: React.DragEvent) => {
237
+ e.preventDefault();
238
+ e.stopPropagation();
239
+
240
+ // Check if we're dragging over a nested container - if so, clear our indicator
241
+ const target = e.target as HTMLElement;
242
+ const nestedContainer = target.closest('[data-layout-container]');
243
+ if (nestedContainer && nestedContainer !== containerRef.current) {
244
+ // We're over a nested container, clear our indicator
245
+ setDropIndicatorPosition(null);
246
+ setDragOverIndex(null);
247
+ setIsDragging(false);
248
+ return;
249
+ }
250
+
251
+ setIsDragging(true);
252
+
253
+ // Calculate drop indicator position for body drop zone (at the end)
254
+ const container = containerRef.current;
255
+ if (container) {
256
+ requestAnimationFrame(() => {
257
+ if (!containerRef.current) return;
258
+
259
+ const containerRect = containerRef.current.getBoundingClientRect();
260
+ // Position at the bottom of the container
261
+ const relativeTop = containerRect.height;
262
+ const padding = 32;
263
+ const width = containerRect.width - (padding * 2);
264
+
265
+ setDropIndicatorPosition({
266
+ top: relativeTop,
267
+ left: padding,
268
+ width: width,
269
+ });
270
+ });
271
+ }
272
+ };
273
+
274
+ const handleBodyDrop = (e: React.DragEvent) => {
275
+ e.preventDefault();
276
+ e.stopPropagation();
277
+
278
+ const dataTransferBlockId = e.dataTransfer.getData('block-id');
279
+ const globalBlockId = typeof window !== 'undefined' ? (window as any).__DRAGGED_BLOCK_ID__ : null;
280
+ const blockId = dataTransferBlockId || globalBlockId;
281
+ const blockType = e.dataTransfer.getData('block-type');
282
+
283
+ console.log('[EditorBody] Body Drop Event:', {
284
+ dataTransferBlockId,
285
+ globalBlockId,
286
+ resolvedBlockId: blockId,
287
+ blockType,
288
+ currentBlocksCount: blocks.length,
289
+ rootBlockIds: blocks.map(b => b.id),
290
+ });
291
+
292
+ // Clear the global dragged block ID
293
+ if (typeof window !== 'undefined') {
294
+ (window as any).__DRAGGED_BLOCK_ID__ = null;
295
+ }
296
+
297
+ if (blockId) {
298
+ // Moving existing block to end of root level
299
+ const currentIndex = blocks.findIndex(b => b.id === blockId);
300
+ console.log('[EditorBody] Body drop - block location:', {
301
+ blockId,
302
+ currentIndex,
303
+ isInRoot: currentIndex !== -1,
304
+ targetIndex: blocks.length,
305
+ });
306
+
307
+ if (currentIndex !== -1) {
308
+ // Already in root, move to end
309
+ console.log('[EditorBody] Moving within root to end:', { blockId, targetIndex: blocks.length - 1 });
310
+ onBlockMove(blockId, blocks.length - 1);
311
+ } else {
312
+ // Block is nested - move it to root level at the end
313
+ console.log('[EditorBody] Moving nested block to root end:', { blockId, targetIndex: blocks.length });
314
+ onBlockMove(blockId, blocks.length, undefined);
315
+ }
316
+ } else if (blockType) {
317
+ // Adding new block to end
318
+ console.log('[EditorBody] Adding new block to end:', { blockType, index: blocks.length });
319
+ onBlockAdd(blockType, blocks.length);
320
+ } else {
321
+ console.warn('[EditorBody] Body drop with no block ID or type!');
322
+ }
323
+
324
+ setIsDragging(false);
325
+ setDragOverIndex(null);
326
+ setDraggedBlockId(null);
327
+ setDropIndicatorPosition(null);
328
+ };
329
+
330
+ return (
331
+ <div
332
+ ref={containerRef}
333
+ className={`relative w-full rounded-[2.5rem] border shadow-lg min-h-[400px] transition-all bg-transparent ${darkMode
334
+ ? 'border-neutral-200 dark:border-neutral-800 shadow-neutral-200/50 dark:shadow-neutral-900/50'
335
+ : 'border-neutral-200 shadow-neutral-200/50'
336
+ }`}
337
+ onDragOver={handleBodyDragOver}
338
+ onDrop={handleBodyDrop}
339
+ onDragLeave={handleDragLeave}
340
+ onDragStart={handleDragStart}
341
+ onDragEnd={() => {
342
+ setDropIndicatorPosition(null);
343
+ setDragOverIndex(null);
344
+ setIsDragging(false);
345
+ setDraggedBlockId(null);
346
+ }}
347
+ >
348
+ {/* Enhanced Drop Indicator - Visual drop zone */}
349
+ {dropIndicatorPosition && (
350
+ <div
351
+ className="absolute z-50 pointer-events-none"
352
+ style={{
353
+ top: `${dropIndicatorPosition.top - 12}px`,
354
+ left: `${dropIndicatorPosition.left}px`,
355
+ width: `${dropIndicatorPosition.width}px`,
356
+ height: '24px',
357
+ }}
358
+ >
359
+ {/* Drop zone background */}
360
+ <div className={`absolute inset-0 rounded-lg border border-dashed backdrop-blur-sm ${darkMode
361
+ ? 'bg-primary/10 dark:bg-primary/20 border-primary dark:border-primary/60'
362
+ : 'bg-primary/10 border-primary'
363
+ }`} />
364
+
365
+ {/* Center line indicator */}
366
+ <div className={`absolute top-1/2 left-0 right-0 h-0.5 rounded-full transform -translate-y-1/2 ${darkMode ? 'bg-primary dark:bg-primary/80' : 'bg-primary'
367
+ }`} />
368
+
369
+ {/* Drop icon indicator */}
370
+ <div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2">
371
+ <div className={`w-6 h-6 rounded-full flex items-center justify-center shadow-lg shadow-primary/30 ${darkMode ? 'bg-primary dark:bg-primary/90' : 'bg-primary'
372
+ }`}>
373
+ <div className={`w-2 h-2 rounded-full ${darkMode ? 'bg-white dark:bg-neutral-900' : 'bg-white'
374
+ }`} />
375
+ </div>
376
+ </div>
377
+ </div>
378
+ )}
379
+
380
+ <div className="flex flex-col p-8">
381
+ {blocks.length === 0 ? (
382
+ /* Empty State - Drop Zone */
383
+ <div
384
+ className={`flex flex-col items-center justify-center py-20 px-8 rounded-2xl border border-dashed transition-all ${darkMode
385
+ ? isDragging
386
+ ? 'border-primary/30 bg-primary/5 dark:bg-primary/10'
387
+ : 'border-gray-200/50 dark:border-neutral-700/50 bg-neutral-50/50 dark:bg-neutral-800/30'
388
+ : isDragging
389
+ ? 'border-primary/30 bg-primary/5'
390
+ : 'border-gray-200/50 bg-neutral-50/50'
391
+ }`}
392
+ >
393
+ <div
394
+ className={`p-4 rounded-full mb-4 transition-colors ${darkMode
395
+ ? isDragging
396
+ ? 'bg-primary/10 dark:bg-primary/20'
397
+ : 'bg-neutral-100 dark:bg-neutral-800'
398
+ : isDragging
399
+ ? 'bg-primary/10'
400
+ : 'bg-neutral-100'
401
+ }`}
402
+ >
403
+ <Plus
404
+ size={24}
405
+ className={`transition-colors ${darkMode
406
+ ? isDragging
407
+ ? 'text-primary'
408
+ : 'text-neutral-400 dark:text-neutral-500'
409
+ : isDragging
410
+ ? 'text-primary'
411
+ : 'text-neutral-400'
412
+ }`}
413
+ />
414
+ </div>
415
+ <p
416
+ className={`text-sm font-black uppercase tracking-wider transition-colors ${darkMode
417
+ ? isDragging
418
+ ? 'text-primary'
419
+ : 'text-neutral-500 dark:text-neutral-400'
420
+ : isDragging
421
+ ? 'text-primary'
422
+ : 'text-neutral-500'
423
+ }`}
424
+ >
425
+ {isDragging ? 'Drop Block Here' : 'Drop a component here'}
426
+ </p>
427
+ <p className={`text-xs mt-2 ${darkMode ? 'text-neutral-400 dark:text-neutral-500' : 'text-neutral-400'
428
+ }`}>
429
+ Drag blocks from the library to get started
430
+ </p>
431
+ </div>
432
+ ) : (
433
+ /* Blocks with visual drop zones between them */
434
+ blocks.map((block, index) => (
435
+ <Fragment key={block.id}>
436
+ {/* Visual Drop Zone - appears between blocks when dragging */}
437
+ {index > 0 && (
438
+ <div
439
+ className={`h-6 transition-all duration-200 ${isDragging && dragOverIndex === index
440
+ ? 'bg-primary/5 border-y border-dashed border-primary/20 rounded-lg'
441
+ : 'bg-transparent'
442
+ }`}
443
+ />
444
+ )}
445
+
446
+ {/* Block Container */}
447
+ <div
448
+ ref={setBlockRef(block.id)}
449
+ onDragOver={(e) => {
450
+ const blockEl = blockRefs.current.get(block.id);
451
+ if (blockEl) {
452
+ handleDragOver(e, index, blockEl);
453
+ }
454
+ }}
455
+ onDragLeave={handleDragLeave}
456
+ onDrop={(e) => handleDrop(e, index)}
457
+ className="relative mb-6 last:mb-0"
458
+ >
459
+ <BlockWrapper
460
+ block={block}
461
+ onUpdate={(data) => onBlockUpdate(block.id, data)}
462
+ onDelete={() => onBlockDelete(block.id)}
463
+ onMoveUp={index > 0 ? () => onBlockMove(block.id, index - 1) : undefined}
464
+ onMoveDown={index < blocks.length - 1 ? () => onBlockMove(block.id, index + 1) : undefined}
465
+ allBlocks={blocks}
466
+ />
467
+ </div>
468
+ </Fragment>
469
+ ))
470
+ )}
471
+ </div>
472
+ </div>
473
+ );
474
+ }
475
+
@@ -0,0 +1,179 @@
1
+ 'use client';
2
+
3
+ import React, { useState } from 'react';
4
+ import { ArrowLeft, Library, Settings2 } from 'lucide-react';
5
+ import { useEditor } from '../../state/EditorContext';
6
+ import { SaveConfirmationModal } from './SaveConfirmationModal';
7
+
8
+ export interface EditorHeaderProps {
9
+ isLibraryOpen: boolean;
10
+ onLibraryToggle: () => void;
11
+ isPreviewMode: boolean;
12
+ onPreviewToggle: () => void;
13
+ isSidebarOpen: boolean;
14
+ onSidebarToggle: () => void;
15
+ isSaving: boolean;
16
+ onSave: (publish?: boolean) => Promise<void>;
17
+ onSaveError: (error: string | null) => void;
18
+ }
19
+
20
+ export function EditorHeader({
21
+ isLibraryOpen,
22
+ onLibraryToggle,
23
+ isPreviewMode,
24
+ onPreviewToggle,
25
+ isSidebarOpen,
26
+ onSidebarToggle,
27
+ isSaving,
28
+ onSave,
29
+ onSaveError,
30
+ }: EditorHeaderProps) {
31
+ const { state, dispatch } = useEditor();
32
+ const [showConfirmModal, setShowConfirmModal] = useState(false);
33
+ const [saveAsDraft, setSaveAsDraft] = useState(false);
34
+ const [saveError, setSaveError] = useState<string | null>(null);
35
+
36
+ const handleSaveDraftClick = () => {
37
+ setSaveAsDraft(true);
38
+ setSaveError(null); // Clear any previous errors
39
+ setShowConfirmModal(true);
40
+ };
41
+
42
+ const handlePublishClick = () => {
43
+ setSaveAsDraft(false);
44
+ setSaveError(null); // Clear any previous errors
45
+ setShowConfirmModal(true);
46
+ };
47
+
48
+ const handleConfirmSave = async () => {
49
+ try {
50
+ const targetStatus = saveAsDraft ? 'draft' : 'published';
51
+ console.log('[EditorHeader] Starting save process...', { saveAsDraft, targetStatus, currentStatus: state.status });
52
+
53
+ // Set status before saving - ensure state is updated
54
+ if (saveAsDraft) {
55
+ dispatch({ type: 'SET_STATUS', payload: 'draft' });
56
+ } else {
57
+ dispatch({ type: 'SET_STATUS', payload: 'published' });
58
+ }
59
+
60
+ // Wait longer to ensure state update propagates through the reducer and context
61
+ // React state updates are asynchronous, so we need to wait for the state to actually update
62
+ await new Promise(resolve => setTimeout(resolve, 150));
63
+
64
+ // Verify status was updated
65
+ console.log('[EditorHeader] Status after update:', state.status, 'Expected:', targetStatus);
66
+
67
+ await onSave(!saveAsDraft);
68
+ console.log('[EditorHeader] Post saved successfully');
69
+ // Clear any previous errors
70
+ setSaveError(null);
71
+ // Modal will show success message and close automatically
72
+ } catch (error: any) {
73
+ console.error('[EditorHeader] Failed to save post:', error);
74
+ // Extract user-friendly error message
75
+ let errorMessage = error.message || 'Failed to save post';
76
+
77
+ // Make error messages more user-friendly
78
+ if (errorMessage.includes('Missing required fields')) {
79
+ // Keep the detailed message about missing fields
80
+ errorMessage = errorMessage.replace('Missing required fields for publishing:', 'To publish, please fill in:');
81
+ } else if (errorMessage.includes('All required fields')) {
82
+ errorMessage = 'To publish, please fill in all required fields: summary, featured image, category, and content.';
83
+ } else if (errorMessage.includes('Unauthorized')) {
84
+ errorMessage = 'You are not authorized to save this post. Please log in again.';
85
+ } else if (errorMessage.includes('Failed to save')) {
86
+ errorMessage = 'Unable to save the post. Please check your connection and try again.';
87
+ }
88
+
89
+ setSaveError(errorMessage);
90
+ onSaveError(errorMessage);
91
+ // Re-throw the error so the modal knows it failed and doesn't show success
92
+ throw error;
93
+ }
94
+ };
95
+
96
+ return (
97
+ <header className="flex items-center justify-between px-6 py-3 bg-dashboard-sidebar backdrop-blur-md z-10 border-b border-dashboard-border flex-none shrink-0">
98
+ <div className="flex items-center gap-6">
99
+ <button
100
+ onClick={() => window.location.href = '/dashboard/blog'}
101
+ className="text-neutral-500 dark:text-neutral-400 hover:text-neutral-950 dark:hover:text-white transition-colors"
102
+ >
103
+ <ArrowLeft size={20} strokeWidth={1.5} />
104
+ </button>
105
+ <div className="h-4 w-[1px] bg-neutral-300 dark:border-neutral-700" />
106
+ <button
107
+ onClick={onLibraryToggle}
108
+ className={`flex items-center gap-2 text-[10px] uppercase tracking-widest font-black transition-all ${isLibraryOpen ? 'text-dashboard-text' : 'text-neutral-500 dark:text-neutral-400'
109
+ }`}
110
+ >
111
+ <Library size={16} strokeWidth={1.5} />
112
+ Library
113
+ </button>
114
+ </div>
115
+
116
+ <div className="flex items-center gap-4">
117
+ <button
118
+ onClick={onPreviewToggle}
119
+ className={`text-[10px] uppercase tracking-widest font-bold transition-colors ${isPreviewMode
120
+ ? 'text-primary dark:text-primary'
121
+ : 'text-neutral-600 dark:text-neutral-400 hover:text-neutral-950 dark:hover:text-white'
122
+ }`}
123
+ >
124
+ {isPreviewMode ? 'Edit' : 'Preview'}
125
+ </button>
126
+ {/* Save Draft Button - Always visible for drafts and new posts */}
127
+ {(state.status === 'draft' || !state.postId) && (
128
+ <button
129
+ onClick={handleSaveDraftClick}
130
+ disabled={isSaving}
131
+ className={`px-4 py-2 border-2 border-dashboard-border text-dashboard-text rounded-full text-[10px] font-bold uppercase tracking-widest transition-all ${isSaving
132
+ ? 'opacity-50 cursor-not-allowed'
133
+ : 'hover:bg-dashboard-bg'
134
+ }`}
135
+ >
136
+ {isSaving ? 'Saving...' : 'Save Draft'}
137
+ </button>
138
+ )}
139
+ {/* Publish/Update Button */}
140
+ <button
141
+ onClick={handlePublishClick}
142
+ disabled={isSaving}
143
+ className={`px-6 py-2 bg-primary text-white rounded-full text-[10px] font-bold uppercase tracking-widest transition-all shadow-lg shadow-primary/20 ${isSaving
144
+ ? 'opacity-50 cursor-not-allowed'
145
+ : 'hover:bg-primary/90'
146
+ }`}
147
+ >
148
+ {isSaving ? 'Saving...' : state.status === 'published' ? 'Update Post' : 'Publish Post'}
149
+ </button>
150
+ <button
151
+ onClick={onSidebarToggle}
152
+ className={`p-2 rounded-full transition-colors ${isSidebarOpen
153
+ ? 'bg-dashboard-bg text-dashboard-text'
154
+ : 'text-neutral-500 dark:text-neutral-400 hover:bg-dashboard-bg'
155
+ }`}
156
+ >
157
+ <Settings2 size={18} />
158
+ </button>
159
+ </div>
160
+
161
+ {/* Save Confirmation Modal */}
162
+ <SaveConfirmationModal
163
+ isOpen={showConfirmModal}
164
+ onClose={() => {
165
+ setShowConfirmModal(false);
166
+ setSaveAsDraft(false);
167
+ setSaveError(null);
168
+ }}
169
+ onConfirm={handleConfirmSave}
170
+ isSaving={isSaving}
171
+ postTitle={state.title || undefined}
172
+ isPublished={state.status === 'published'}
173
+ saveAsDraft={saveAsDraft}
174
+ error={saveError}
175
+ />
176
+ </header>
177
+ );
178
+ }
179
+