@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
|
@@ -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
|
+
|