@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,494 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layout Container Component
|
|
3
|
+
* Recursive drop zone for nested blocks (used in Section, Columns, etc.)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use client';
|
|
7
|
+
|
|
8
|
+
import React, { useState, Fragment, useEffect } from 'react';
|
|
9
|
+
import { Plus } from 'lucide-react';
|
|
10
|
+
import { Block } from '../../types/block';
|
|
11
|
+
import { BlockWrapper } from './BlockWrapper';
|
|
12
|
+
import { useEditor } from '../../state/EditorContext';
|
|
13
|
+
|
|
14
|
+
export interface LayoutContainerProps {
|
|
15
|
+
/** Child blocks to render */
|
|
16
|
+
blocks: Block[];
|
|
17
|
+
|
|
18
|
+
/** Container ID (for nested operations) */
|
|
19
|
+
containerId: string;
|
|
20
|
+
|
|
21
|
+
/** Callback when a block is added to this container */
|
|
22
|
+
onBlockAdd: (type: string, index: number, containerId: string) => void;
|
|
23
|
+
|
|
24
|
+
/** Callback when a block is updated */
|
|
25
|
+
onBlockUpdate: (id: string, data: Partial<Block['data']>, containerId: string) => void;
|
|
26
|
+
|
|
27
|
+
/** Callback when a block is deleted */
|
|
28
|
+
onBlockDelete: (id: string, containerId: string) => void;
|
|
29
|
+
|
|
30
|
+
/** Callback when a block is moved */
|
|
31
|
+
onBlockMove: (id: string, newIndex: number, containerId: string) => void;
|
|
32
|
+
|
|
33
|
+
/** Additional CSS classes for styling */
|
|
34
|
+
className?: string;
|
|
35
|
+
|
|
36
|
+
/** Whether this is an empty state (show drop zone) */
|
|
37
|
+
isEmpty?: boolean;
|
|
38
|
+
|
|
39
|
+
/** Label for empty state */
|
|
40
|
+
emptyLabel?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Layout Container
|
|
45
|
+
* Recursive drop zone that can contain nested blocks
|
|
46
|
+
*/
|
|
47
|
+
export function LayoutContainer({
|
|
48
|
+
blocks,
|
|
49
|
+
containerId,
|
|
50
|
+
onBlockAdd,
|
|
51
|
+
onBlockUpdate,
|
|
52
|
+
onBlockDelete,
|
|
53
|
+
onBlockMove,
|
|
54
|
+
className = '',
|
|
55
|
+
isEmpty = false,
|
|
56
|
+
emptyLabel = 'Drop blocks here',
|
|
57
|
+
}: LayoutContainerProps) {
|
|
58
|
+
const { darkMode } = useEditor();
|
|
59
|
+
const [dragOverIndex, setDragOverIndex] = useState<number | null>(null);
|
|
60
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
61
|
+
const [draggedBlockId, setDraggedBlockId] = useState<string | null>(null);
|
|
62
|
+
const [dropIndicatorPosition, setDropIndicatorPosition] = useState<{ top: number; left: number; width: number } | null>(null);
|
|
63
|
+
const containerRef = React.useRef<HTMLDivElement>(null);
|
|
64
|
+
|
|
65
|
+
// Listen for clear-drop-indicator events from nested containers
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
const handleClearIndicator = () => {
|
|
68
|
+
setDropIndicatorPosition(null);
|
|
69
|
+
setDragOverIndex(null);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const container = containerRef.current;
|
|
73
|
+
if (container) {
|
|
74
|
+
container.addEventListener('clear-drop-indicator', handleClearIndicator);
|
|
75
|
+
return () => {
|
|
76
|
+
container.removeEventListener('clear-drop-indicator', handleClearIndicator);
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
}, []);
|
|
80
|
+
|
|
81
|
+
// Global drag end handler - clear all indicators when drag ends
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
const handleGlobalDragEnd = () => {
|
|
84
|
+
setDropIndicatorPosition(null);
|
|
85
|
+
setDragOverIndex(null);
|
|
86
|
+
setIsDragging(false);
|
|
87
|
+
setDraggedBlockId(null);
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
document.addEventListener('dragend', handleGlobalDragEnd);
|
|
91
|
+
return () => {
|
|
92
|
+
document.removeEventListener('dragend', handleGlobalDragEnd);
|
|
93
|
+
};
|
|
94
|
+
}, []);
|
|
95
|
+
|
|
96
|
+
// Store refs for all blocks - use callback refs to avoid hook violations
|
|
97
|
+
const blockRefs = React.useRef<Map<string, HTMLDivElement>>(new Map());
|
|
98
|
+
|
|
99
|
+
const setBlockRef = (blockId: string) => (el: HTMLDivElement | null) => {
|
|
100
|
+
if (el) {
|
|
101
|
+
blockRefs.current.set(blockId, el);
|
|
102
|
+
} else {
|
|
103
|
+
blockRefs.current.delete(blockId);
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const handleDragStart = (e: React.DragEvent) => {
|
|
108
|
+
// This is called when dragging starts from the container
|
|
109
|
+
// The actual block drag start is handled in BlockWrapper
|
|
110
|
+
setIsDragging(true);
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const handleDragOver = (e: React.DragEvent, index: number, element?: HTMLElement) => {
|
|
114
|
+
e.preventDefault();
|
|
115
|
+
e.stopPropagation(); // Prevent parent containers from also handling this
|
|
116
|
+
|
|
117
|
+
// Check if we're dragging over a deeper nested container - if so, clear our indicator
|
|
118
|
+
const target = e.target as HTMLElement;
|
|
119
|
+
const deeperContainer = target.closest('[data-layout-container]');
|
|
120
|
+
if (deeperContainer && deeperContainer !== containerRef.current) {
|
|
121
|
+
// We're over a deeper nested container, clear our indicator
|
|
122
|
+
setDropIndicatorPosition(null);
|
|
123
|
+
setDragOverIndex(null);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Clear any parent container indicators by dispatching a custom event
|
|
128
|
+
// This ensures only the active (deepest) container shows an indicator
|
|
129
|
+
const customEvent = new CustomEvent('clear-drop-indicator', { bubbles: true });
|
|
130
|
+
e.currentTarget.dispatchEvent(customEvent);
|
|
131
|
+
|
|
132
|
+
setDragOverIndex(index);
|
|
133
|
+
|
|
134
|
+
// Calculate position for absolute drop indicator using mouse position
|
|
135
|
+
const container = containerRef.current;
|
|
136
|
+
if (container) {
|
|
137
|
+
requestAnimationFrame(() => {
|
|
138
|
+
// Double-check refs are still valid inside RAF callback
|
|
139
|
+
if (!containerRef.current) return;
|
|
140
|
+
|
|
141
|
+
const containerRect = containerRef.current.getBoundingClientRect();
|
|
142
|
+
// Use mouse Y position relative to container for accurate positioning
|
|
143
|
+
const mouseY = e.clientY;
|
|
144
|
+
const relativeTop = mouseY - containerRect.top;
|
|
145
|
+
|
|
146
|
+
// Determine if we should show above or below the element
|
|
147
|
+
let finalTop = relativeTop;
|
|
148
|
+
if (element) {
|
|
149
|
+
const elementRect = element.getBoundingClientRect();
|
|
150
|
+
const elementTop = elementRect.top - containerRect.top;
|
|
151
|
+
const elementCenter = elementTop + elementRect.height / 2;
|
|
152
|
+
|
|
153
|
+
// If mouse is in upper half of element, show above; otherwise show below
|
|
154
|
+
if (relativeTop < elementCenter) {
|
|
155
|
+
finalTop = elementTop;
|
|
156
|
+
} else {
|
|
157
|
+
finalTop = elementTop + elementRect.height;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
setDropIndicatorPosition({
|
|
162
|
+
top: finalTop,
|
|
163
|
+
left: 0,
|
|
164
|
+
width: containerRect.width,
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const handleDragLeave = (e: React.DragEvent) => {
|
|
171
|
+
// Only clear if we're actually leaving the container
|
|
172
|
+
// Check if we're moving to a child element (nested container)
|
|
173
|
+
const relatedTarget = e.relatedTarget as Node;
|
|
174
|
+
if (!e.currentTarget.contains(relatedTarget)) {
|
|
175
|
+
// Check if relatedTarget is inside a nested LayoutContainer
|
|
176
|
+
const isMovingToNested = relatedTarget && (relatedTarget as HTMLElement).closest('[data-layout-container]');
|
|
177
|
+
if (!isMovingToNested) {
|
|
178
|
+
setDragOverIndex(null);
|
|
179
|
+
setDropIndicatorPosition(null);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const handleDrop = (e: React.DragEvent, index: number) => {
|
|
185
|
+
e.preventDefault();
|
|
186
|
+
e.stopPropagation();
|
|
187
|
+
|
|
188
|
+
const dataTransferBlockId = e.dataTransfer.getData('block-id');
|
|
189
|
+
const globalBlockId = typeof window !== 'undefined' ? (window as any).__DRAGGED_BLOCK_ID__ : null;
|
|
190
|
+
const blockId = dataTransferBlockId || globalBlockId;
|
|
191
|
+
const blockType = e.dataTransfer.getData('block-type');
|
|
192
|
+
|
|
193
|
+
console.log('[LayoutContainer] Drop Event:', {
|
|
194
|
+
containerId,
|
|
195
|
+
index,
|
|
196
|
+
dataTransferBlockId,
|
|
197
|
+
globalBlockId,
|
|
198
|
+
resolvedBlockId: blockId,
|
|
199
|
+
blockType,
|
|
200
|
+
currentContainerBlocks: blocks.map(b => ({ id: b.id, type: b.type })),
|
|
201
|
+
containerBlockIds: blocks.map(b => b.id),
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// Clear the global dragged block ID
|
|
205
|
+
if (typeof window !== 'undefined') {
|
|
206
|
+
(window as any).__DRAGGED_BLOCK_ID__ = null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (blockId) {
|
|
210
|
+
// Moving existing block - check if it's in this container or elsewhere
|
|
211
|
+
const currentIndex = blocks.findIndex(b => b.id === blockId);
|
|
212
|
+
console.log('[LayoutContainer] Block location check:', {
|
|
213
|
+
blockId,
|
|
214
|
+
currentIndex,
|
|
215
|
+
isInThisContainer: currentIndex !== -1,
|
|
216
|
+
targetIndex: index,
|
|
217
|
+
containerId,
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
if (currentIndex !== -1) {
|
|
221
|
+
// Block is in this container - move within container
|
|
222
|
+
if (currentIndex !== index) {
|
|
223
|
+
const targetIndex = currentIndex < index ? index - 1 : index;
|
|
224
|
+
console.log('[LayoutContainer] Moving within container:', { blockId, currentIndex, targetIndex, containerId });
|
|
225
|
+
onBlockMove(blockId, targetIndex, containerId);
|
|
226
|
+
} else {
|
|
227
|
+
console.log('[LayoutContainer] Block already at target position in container');
|
|
228
|
+
}
|
|
229
|
+
} else {
|
|
230
|
+
// Block is in root or another container - move it here
|
|
231
|
+
console.log('[LayoutContainer] Moving block from elsewhere to container:', { blockId, index, containerId });
|
|
232
|
+
onBlockMove(blockId, index, containerId);
|
|
233
|
+
}
|
|
234
|
+
} else if (blockType) {
|
|
235
|
+
// Adding new block from library
|
|
236
|
+
console.log('[LayoutContainer] Adding new block to container:', { blockType, index, containerId });
|
|
237
|
+
onBlockAdd(blockType, index, containerId);
|
|
238
|
+
} else {
|
|
239
|
+
console.warn('[LayoutContainer] Drop event with no block ID or type!');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
setDragOverIndex(null);
|
|
243
|
+
setIsDragging(false);
|
|
244
|
+
setDraggedBlockId(null);
|
|
245
|
+
setDropIndicatorPosition(null);
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
const handleBodyDragOver = (e: React.DragEvent) => {
|
|
249
|
+
e.preventDefault();
|
|
250
|
+
e.stopPropagation(); // Prevent parent containers from also handling this
|
|
251
|
+
|
|
252
|
+
// Check if we're dragging over a deeper nested container - if so, clear our indicator
|
|
253
|
+
const target = e.target as HTMLElement;
|
|
254
|
+
const deeperContainer = target.closest('[data-layout-container]');
|
|
255
|
+
if (deeperContainer && deeperContainer !== containerRef.current) {
|
|
256
|
+
// We're over a deeper nested container, clear our indicator
|
|
257
|
+
setDropIndicatorPosition(null);
|
|
258
|
+
setDragOverIndex(null);
|
|
259
|
+
setIsDragging(false);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Clear any parent container indicators by dispatching a custom event
|
|
264
|
+
const customEvent = new CustomEvent('clear-drop-indicator', { bubbles: true });
|
|
265
|
+
e.currentTarget.dispatchEvent(customEvent);
|
|
266
|
+
|
|
267
|
+
setIsDragging(true);
|
|
268
|
+
|
|
269
|
+
// Calculate drop indicator position for body drop zone (at the end)
|
|
270
|
+
const container = containerRef.current;
|
|
271
|
+
if (container) {
|
|
272
|
+
requestAnimationFrame(() => {
|
|
273
|
+
if (!containerRef.current) return;
|
|
274
|
+
|
|
275
|
+
const containerRect = containerRef.current.getBoundingClientRect();
|
|
276
|
+
// Position at the bottom of the container
|
|
277
|
+
const relativeTop = containerRect.height;
|
|
278
|
+
|
|
279
|
+
setDropIndicatorPosition({
|
|
280
|
+
top: relativeTop,
|
|
281
|
+
left: 0,
|
|
282
|
+
width: containerRect.width,
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
const handleBodyDrop = (e: React.DragEvent) => {
|
|
289
|
+
e.preventDefault();
|
|
290
|
+
e.stopPropagation();
|
|
291
|
+
|
|
292
|
+
const dataTransferBlockId = e.dataTransfer.getData('block-id');
|
|
293
|
+
const globalBlockId = typeof window !== 'undefined' ? (window as any).__DRAGGED_BLOCK_ID__ : null;
|
|
294
|
+
const blockId = dataTransferBlockId || globalBlockId;
|
|
295
|
+
const blockType = e.dataTransfer.getData('block-type');
|
|
296
|
+
|
|
297
|
+
console.log('[LayoutContainer] Body Drop Event (empty state):', {
|
|
298
|
+
containerId,
|
|
299
|
+
dataTransferBlockId,
|
|
300
|
+
globalBlockId,
|
|
301
|
+
resolvedBlockId: blockId,
|
|
302
|
+
blockType,
|
|
303
|
+
currentContainerBlocks: blocks.map(b => ({ id: b.id, type: b.type })),
|
|
304
|
+
containerBlockIds: blocks.map(b => b.id),
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
// Clear the global dragged block ID
|
|
308
|
+
if (typeof window !== 'undefined') {
|
|
309
|
+
(window as any).__DRAGGED_BLOCK_ID__ = null;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (blockId) {
|
|
313
|
+
// Moving existing block - check if it's in this container or elsewhere
|
|
314
|
+
const currentIndex = blocks.findIndex(b => b.id === blockId);
|
|
315
|
+
const targetIndex = blocks.length; // When empty, this is 0, otherwise it's the end
|
|
316
|
+
|
|
317
|
+
console.log('[LayoutContainer] Body drop - block location:', {
|
|
318
|
+
blockId,
|
|
319
|
+
currentIndex,
|
|
320
|
+
isInThisContainer: currentIndex !== -1,
|
|
321
|
+
targetIndex,
|
|
322
|
+
containerId,
|
|
323
|
+
isEmpty: blocks.length === 0,
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
if (currentIndex !== -1) {
|
|
327
|
+
// Block is already in this container
|
|
328
|
+
if (blocks.length === 0) {
|
|
329
|
+
// Empty container - shouldn't happen, but handle it
|
|
330
|
+
console.log('[LayoutContainer] Block already in empty container (should not happen)');
|
|
331
|
+
} else if (currentIndex !== blocks.length - 1) {
|
|
332
|
+
// Move to end
|
|
333
|
+
console.log('[LayoutContainer] Moving within container to end:', { blockId, currentIndex, targetIndex: blocks.length - 1, containerId });
|
|
334
|
+
onBlockMove(blockId, blocks.length - 1, containerId);
|
|
335
|
+
} else {
|
|
336
|
+
console.log('[LayoutContainer] Block already at end of container');
|
|
337
|
+
}
|
|
338
|
+
} else {
|
|
339
|
+
// Block is in root or another container - move it here
|
|
340
|
+
console.log('[LayoutContainer] Moving block from elsewhere to container:', { blockId, targetIndex, containerId, isEmpty: blocks.length === 0 });
|
|
341
|
+
onBlockMove(blockId, targetIndex, containerId);
|
|
342
|
+
}
|
|
343
|
+
} else if (blockType) {
|
|
344
|
+
// Adding new block
|
|
345
|
+
const targetIndex = blocks.length; // When empty, this is 0
|
|
346
|
+
console.log('[LayoutContainer] Adding new block to container:', { blockType, index: targetIndex, containerId, isEmpty: blocks.length === 0 });
|
|
347
|
+
onBlockAdd(blockType, targetIndex, containerId);
|
|
348
|
+
} else {
|
|
349
|
+
console.warn('[LayoutContainer] Body drop with no block ID or type!');
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
setIsDragging(false);
|
|
353
|
+
setDragOverIndex(null);
|
|
354
|
+
setDraggedBlockId(null);
|
|
355
|
+
setDropIndicatorPosition(null);
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
return (
|
|
359
|
+
<div
|
|
360
|
+
ref={containerRef}
|
|
361
|
+
data-layout-container={containerId}
|
|
362
|
+
className={`relative flex flex-col ${className}`}
|
|
363
|
+
onDragOver={handleBodyDragOver}
|
|
364
|
+
onDrop={handleBodyDrop}
|
|
365
|
+
onDragLeave={handleDragLeave}
|
|
366
|
+
onDragStart={handleDragStart}
|
|
367
|
+
onDragEnd={() => {
|
|
368
|
+
setDropIndicatorPosition(null);
|
|
369
|
+
setDragOverIndex(null);
|
|
370
|
+
setIsDragging(false);
|
|
371
|
+
setDraggedBlockId(null);
|
|
372
|
+
}}
|
|
373
|
+
>
|
|
374
|
+
{/* Enhanced Drop Indicator - Visual drop zone */}
|
|
375
|
+
{dropIndicatorPosition && (
|
|
376
|
+
<div
|
|
377
|
+
className="absolute z-50 pointer-events-none"
|
|
378
|
+
style={{
|
|
379
|
+
top: `${dropIndicatorPosition.top - 12}px`,
|
|
380
|
+
left: `${dropIndicatorPosition.left}px`,
|
|
381
|
+
width: `${dropIndicatorPosition.width}px`,
|
|
382
|
+
height: '24px',
|
|
383
|
+
}}
|
|
384
|
+
>
|
|
385
|
+
{/* Drop zone background */}
|
|
386
|
+
<div className={`absolute inset-0 rounded-lg border border-dashed backdrop-blur-sm ${darkMode
|
|
387
|
+
? 'bg-primary/10 dark:bg-primary/20 border-primary/30 dark:border-primary/40'
|
|
388
|
+
: 'bg-primary/10 border-primary/30'
|
|
389
|
+
}`} />
|
|
390
|
+
|
|
391
|
+
{/* Center line indicator */}
|
|
392
|
+
<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'
|
|
393
|
+
}`} />
|
|
394
|
+
|
|
395
|
+
{/* Drop icon indicator */}
|
|
396
|
+
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2">
|
|
397
|
+
<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'
|
|
398
|
+
}`}>
|
|
399
|
+
<div className={`w-2 h-2 rounded-full ${darkMode ? 'bg-white dark:bg-neutral-900' : 'bg-white'
|
|
400
|
+
}`} />
|
|
401
|
+
</div>
|
|
402
|
+
</div>
|
|
403
|
+
</div>
|
|
404
|
+
)}
|
|
405
|
+
|
|
406
|
+
{blocks.length === 0 ? (
|
|
407
|
+
/* Empty State - Drop Zone */
|
|
408
|
+
<div
|
|
409
|
+
className={`flex flex-col items-center justify-center py-12 px-6 rounded-2xl border border-dashed transition-all ${darkMode
|
|
410
|
+
? isDragging
|
|
411
|
+
? 'border-primary/30 bg-primary/5 dark:bg-primary/10'
|
|
412
|
+
: 'border-gray-200/50 dark:border-neutral-700/50 bg-neutral-50/30 dark:bg-neutral-800/20'
|
|
413
|
+
: isDragging
|
|
414
|
+
? 'border-primary/30 bg-primary/5'
|
|
415
|
+
: 'border-gray-200/50 bg-neutral-50/30'
|
|
416
|
+
}`}
|
|
417
|
+
>
|
|
418
|
+
<div
|
|
419
|
+
className={`p-3 rounded-full mb-3 transition-colors ${darkMode
|
|
420
|
+
? isDragging
|
|
421
|
+
? 'bg-primary/10 dark:bg-primary/20'
|
|
422
|
+
: 'bg-neutral-100 dark:bg-neutral-800'
|
|
423
|
+
: isDragging
|
|
424
|
+
? 'bg-primary/10'
|
|
425
|
+
: 'bg-neutral-100'
|
|
426
|
+
}`}
|
|
427
|
+
>
|
|
428
|
+
<Plus
|
|
429
|
+
size={20}
|
|
430
|
+
className={`transition-colors ${darkMode
|
|
431
|
+
? isDragging
|
|
432
|
+
? 'text-primary'
|
|
433
|
+
: 'text-neutral-400 dark:text-neutral-500'
|
|
434
|
+
: isDragging
|
|
435
|
+
? 'text-primary'
|
|
436
|
+
: 'text-neutral-400'
|
|
437
|
+
}`}
|
|
438
|
+
/>
|
|
439
|
+
</div>
|
|
440
|
+
<p
|
|
441
|
+
className={`text-xs font-black uppercase tracking-wider transition-colors ${darkMode
|
|
442
|
+
? isDragging
|
|
443
|
+
? 'text-primary'
|
|
444
|
+
: 'text-neutral-500 dark:text-neutral-400'
|
|
445
|
+
: isDragging
|
|
446
|
+
? 'text-primary'
|
|
447
|
+
: 'text-neutral-500'
|
|
448
|
+
}`}
|
|
449
|
+
>
|
|
450
|
+
{isDragging ? 'Drop Block Here' : emptyLabel}
|
|
451
|
+
</p>
|
|
452
|
+
</div>
|
|
453
|
+
) : (
|
|
454
|
+
/* Blocks with visual drop zones between them */
|
|
455
|
+
blocks.map((block, index) => (
|
|
456
|
+
<Fragment key={block.id}>
|
|
457
|
+
{/* Visual Drop Zone - appears between blocks when dragging */}
|
|
458
|
+
{index > 0 && (
|
|
459
|
+
<div
|
|
460
|
+
className={`h-6 transition-all duration-200 ${isDragging && dragOverIndex === index
|
|
461
|
+
? 'bg-primary/5 border-y border-dashed border-primary/20 rounded-lg'
|
|
462
|
+
: 'bg-transparent'
|
|
463
|
+
}`}
|
|
464
|
+
/>
|
|
465
|
+
)}
|
|
466
|
+
|
|
467
|
+
{/* Block Container */}
|
|
468
|
+
<div
|
|
469
|
+
ref={setBlockRef(block.id)}
|
|
470
|
+
onDragOver={(e) => {
|
|
471
|
+
const blockEl = blockRefs.current.get(block.id);
|
|
472
|
+
if (blockEl) {
|
|
473
|
+
handleDragOver(e, index, blockEl);
|
|
474
|
+
}
|
|
475
|
+
}}
|
|
476
|
+
onDragLeave={handleDragLeave}
|
|
477
|
+
onDrop={(e) => handleDrop(e, index)}
|
|
478
|
+
className="relative mb-4 last:mb-0"
|
|
479
|
+
>
|
|
480
|
+
<BlockWrapper
|
|
481
|
+
block={block}
|
|
482
|
+
onUpdate={(data) => onBlockUpdate(block.id, data, containerId)}
|
|
483
|
+
onDelete={() => onBlockDelete(block.id, containerId)}
|
|
484
|
+
onMoveUp={index > 0 ? () => onBlockMove(block.id, index - 1, containerId) : undefined}
|
|
485
|
+
onMoveDown={index < blocks.length - 1 ? () => onBlockMove(block.id, index + 1, containerId) : undefined}
|
|
486
|
+
/>
|
|
487
|
+
</div>
|
|
488
|
+
</Fragment>
|
|
489
|
+
))
|
|
490
|
+
)}
|
|
491
|
+
</div>
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
|