@jhits/plugin-blog 0.0.5 → 0.0.7
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/package.json +16 -16
- package/src/api/config-handler.ts +76 -0
- package/src/api/handler.ts +4 -4
- package/src/api/router.ts +17 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useCategories.ts +76 -0
- package/src/index.tsx +8 -27
- package/src/init.tsx +0 -9
- package/src/lib/config-storage.ts +65 -0
- package/src/lib/layouts/blocks/ColumnsBlock.tsx +177 -13
- package/src/lib/layouts/blocks/ColumnsBlock.tsx.tmp +81 -0
- package/src/lib/layouts/registerLayoutBlocks.ts +6 -1
- package/src/lib/mappers/apiMapper.ts +53 -22
- package/src/registry/BlockRegistry.ts +1 -4
- package/src/state/EditorContext.tsx +39 -33
- package/src/state/types.ts +1 -1
- package/src/types/index.ts +2 -0
- package/src/types/post.ts +4 -0
- package/src/views/CanvasEditor/BlockWrapper.tsx +87 -24
- package/src/views/CanvasEditor/CanvasEditorView.tsx +214 -794
- package/src/views/CanvasEditor/EditorBody.tsx +317 -127
- package/src/views/CanvasEditor/EditorHeader.tsx +106 -17
- package/src/views/CanvasEditor/LayoutContainer.tsx +208 -380
- package/src/views/CanvasEditor/components/EditorCanvas.tsx +160 -0
- package/src/views/CanvasEditor/components/EditorLibrary.tsx +122 -0
- package/src/views/CanvasEditor/components/EditorSidebar.tsx +181 -0
- package/src/views/CanvasEditor/components/ErrorBanner.tsx +31 -0
- package/src/views/CanvasEditor/components/FeaturedMediaSection.tsx +260 -49
- package/src/views/CanvasEditor/components/index.ts +11 -0
- package/src/views/CanvasEditor/hooks/index.ts +10 -0
- package/src/views/CanvasEditor/hooks/useHeroBlock.ts +103 -0
- package/src/views/CanvasEditor/hooks/useKeyboardShortcuts.ts +142 -0
- package/src/views/CanvasEditor/hooks/usePostLoader.ts +39 -0
- package/src/views/CanvasEditor/hooks/useRegisteredBlocks.ts +55 -0
- package/src/views/CanvasEditor/hooks/useUnsavedChanges.ts +339 -0
- package/src/views/PostManager/PostCards.tsx +18 -13
- package/src/views/PostManager/PostFilters.tsx +15 -0
- package/src/views/PostManager/PostManagerView.tsx +21 -15
- package/src/views/PostManager/PostTable.tsx +7 -4
|
@@ -1,49 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Layout Container Component
|
|
3
|
-
* Recursive drop zone for nested blocks
|
|
3
|
+
* Recursive drop zone for nested blocks
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
'use client';
|
|
7
7
|
|
|
8
|
-
import React, { useState,
|
|
8
|
+
import React, { useState, useEffect, useRef } from 'react';
|
|
9
9
|
import { Plus } from 'lucide-react';
|
|
10
10
|
import { Block } from '../../types/block';
|
|
11
11
|
import { BlockWrapper } from './BlockWrapper';
|
|
12
12
|
import { useEditor } from '../../state/EditorContext';
|
|
13
13
|
|
|
14
14
|
export interface LayoutContainerProps {
|
|
15
|
-
/** Child blocks to render */
|
|
16
15
|
blocks: Block[];
|
|
17
|
-
|
|
18
|
-
/** Container ID (for nested operations) */
|
|
19
16
|
containerId: string;
|
|
20
|
-
|
|
21
|
-
/** Callback when a block is added to this container */
|
|
22
17
|
onBlockAdd: (type: string, index: number, containerId: string) => void;
|
|
23
|
-
|
|
24
|
-
/** Callback when a block is updated */
|
|
25
18
|
onBlockUpdate: (id: string, data: Partial<Block['data']>, containerId: string) => void;
|
|
26
|
-
|
|
27
|
-
/** Callback when a block is deleted */
|
|
28
19
|
onBlockDelete: (id: string, containerId: string) => void;
|
|
29
|
-
|
|
30
|
-
/** Callback when a block is moved */
|
|
31
20
|
onBlockMove: (id: string, newIndex: number, containerId: string) => void;
|
|
32
|
-
|
|
33
|
-
/** Additional CSS classes for styling */
|
|
34
21
|
className?: string;
|
|
35
|
-
|
|
36
|
-
/** Whether this is an empty state (show drop zone) */
|
|
37
|
-
isEmpty?: boolean;
|
|
38
|
-
|
|
39
|
-
/** Label for empty state */
|
|
40
22
|
emptyLabel?: string;
|
|
41
23
|
}
|
|
42
24
|
|
|
43
|
-
/**
|
|
44
|
-
* Layout Container
|
|
45
|
-
* Recursive drop zone that can contain nested blocks
|
|
46
|
-
*/
|
|
47
25
|
export function LayoutContainer({
|
|
48
26
|
blocks,
|
|
49
27
|
containerId,
|
|
@@ -52,443 +30,293 @@ export function LayoutContainer({
|
|
|
52
30
|
onBlockDelete,
|
|
53
31
|
onBlockMove,
|
|
54
32
|
className = '',
|
|
55
|
-
isEmpty = false,
|
|
56
33
|
emptyLabel = 'Drop blocks here',
|
|
57
34
|
}: LayoutContainerProps) {
|
|
58
35
|
const { darkMode } = useEditor();
|
|
36
|
+
|
|
37
|
+
// --- State ---
|
|
59
38
|
const [dragOverIndex, setDragOverIndex] = useState<number | null>(null);
|
|
39
|
+
const [dropAtEnd, setDropAtEnd] = useState(false);
|
|
60
40
|
const [isDragging, setIsDragging] = useState(false);
|
|
61
|
-
const [draggedBlockId, setDraggedBlockId] = useState<string | null>(null);
|
|
62
41
|
const [dropIndicatorPosition, setDropIndicatorPosition] = useState<{ top: number; left: number; width: number } | null>(null);
|
|
63
|
-
const containerRef = React.useRef<HTMLDivElement>(null);
|
|
64
42
|
|
|
65
|
-
|
|
43
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
44
|
+
const blockRefs = useRef<Map<string, HTMLDivElement>>(new Map());
|
|
45
|
+
// Use ref to ensure we always have the latest dropAtEnd value (React state updates are async)
|
|
46
|
+
const dropAtEndRef = useRef(false);
|
|
47
|
+
|
|
48
|
+
// --- Cleanup & Event Listeners ---
|
|
66
49
|
useEffect(() => {
|
|
67
|
-
const
|
|
50
|
+
const resetState = () => {
|
|
68
51
|
setDropIndicatorPosition(null);
|
|
69
52
|
setDragOverIndex(null);
|
|
53
|
+
setDropAtEnd(false);
|
|
54
|
+
dropAtEndRef.current = false; // Reset ref too
|
|
55
|
+
setIsDragging(false);
|
|
56
|
+
// Clear global dragged block ID on dragend (in case drag was cancelled)
|
|
57
|
+
if (typeof window !== 'undefined') {
|
|
58
|
+
(window as any).__DRAGGED_BLOCK_ID__ = null;
|
|
59
|
+
}
|
|
70
60
|
};
|
|
71
61
|
|
|
72
62
|
const container = containerRef.current;
|
|
73
63
|
if (container) {
|
|
74
|
-
container.addEventListener('clear-drop-indicator',
|
|
64
|
+
container.addEventListener('clear-drop-indicator', resetState);
|
|
65
|
+
document.addEventListener('dragend', resetState);
|
|
75
66
|
return () => {
|
|
76
|
-
container.removeEventListener('clear-drop-indicator',
|
|
67
|
+
container.removeEventListener('clear-drop-indicator', resetState);
|
|
68
|
+
document.removeEventListener('dragend', resetState);
|
|
77
69
|
};
|
|
78
70
|
}
|
|
79
71
|
}, []);
|
|
80
72
|
|
|
81
|
-
//
|
|
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
|
-
};
|
|
73
|
+
// --- Drag & Drop Logic ---
|
|
106
74
|
|
|
107
|
-
const
|
|
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) => {
|
|
75
|
+
const handleDragOverBlock = (e: React.DragEvent, index: number, element: HTMLElement) => {
|
|
114
76
|
e.preventDefault();
|
|
115
|
-
e.stopPropagation();
|
|
77
|
+
e.stopPropagation();
|
|
116
78
|
|
|
117
|
-
// Check
|
|
79
|
+
// 1. Check for deeper nested containers
|
|
118
80
|
const target = e.target as HTMLElement;
|
|
119
81
|
const deeperContainer = target.closest('[data-layout-container]');
|
|
120
82
|
if (deeperContainer && deeperContainer !== containerRef.current) {
|
|
121
|
-
// We're over a deeper nested container, clear our indicator
|
|
122
83
|
setDropIndicatorPosition(null);
|
|
123
|
-
setDragOverIndex(null);
|
|
124
84
|
return;
|
|
125
85
|
}
|
|
126
86
|
|
|
127
|
-
//
|
|
128
|
-
|
|
129
|
-
const customEvent = new CustomEvent('clear-drop-indicator', { bubbles: true });
|
|
130
|
-
e.currentTarget.dispatchEvent(customEvent);
|
|
87
|
+
// 2. Notify parent containers to hide their indicators
|
|
88
|
+
e.currentTarget.dispatchEvent(new CustomEvent('clear-drop-indicator', { bubbles: true }));
|
|
131
89
|
|
|
132
|
-
|
|
90
|
+
// 3. Calculate "Above" vs "Below" and position indicator between blocks
|
|
91
|
+
const containerRect = containerRef.current!.getBoundingClientRect();
|
|
92
|
+
const elementRect = element.getBoundingClientRect();
|
|
93
|
+
const mouseRelativeToBlock = e.clientY - elementRect.top;
|
|
94
|
+
const isBottomHalf = mouseRelativeToBlock > (elementRect.height / 2);
|
|
133
95
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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;
|
|
96
|
+
setDragOverIndex(index);
|
|
97
|
+
setDropAtEnd(isBottomHalf);
|
|
98
|
+
dropAtEndRef.current = isBottomHalf; // Update ref immediately
|
|
99
|
+
setIsDragging(true);
|
|
145
100
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
const elementTop = elementRect.top - containerRect.top;
|
|
151
|
-
const elementCenter = elementTop + elementRect.height / 2;
|
|
101
|
+
// 4. Update Visual Indicator - always show between blocks
|
|
102
|
+
const elementTop = elementRect.top - containerRect.top;
|
|
103
|
+
const elementBottom = elementRect.bottom - containerRect.top;
|
|
104
|
+
let indicatorTop: number;
|
|
152
105
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
106
|
+
if (isBottomHalf) {
|
|
107
|
+
// Show below this block - position between current block and next block
|
|
108
|
+
if (index === blocks.length - 1) {
|
|
109
|
+
// Last block - show after it
|
|
110
|
+
indicatorTop = elementBottom;
|
|
111
|
+
} else {
|
|
112
|
+
// Get next block to find the gap
|
|
113
|
+
const nextBlock = blocks[index + 1];
|
|
114
|
+
const nextBlockEl = blockRefs.current.get(nextBlock.id);
|
|
115
|
+
if (nextBlockEl) {
|
|
116
|
+
const nextBlockRect = nextBlockEl.getBoundingClientRect();
|
|
117
|
+
const nextBlockTop = nextBlockRect.top - containerRect.top;
|
|
118
|
+
// Position in the middle of the gap (mb-4 = 16px margin)
|
|
119
|
+
indicatorTop = elementBottom + (nextBlockTop - elementBottom) / 2;
|
|
120
|
+
} else {
|
|
121
|
+
indicatorTop = elementBottom;
|
|
159
122
|
}
|
|
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
123
|
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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);
|
|
124
|
+
} else {
|
|
125
|
+
// Show above this block - position between previous block and current block
|
|
126
|
+
if (index === 0) {
|
|
127
|
+
// First block - show at top of container (before first block)
|
|
128
|
+
indicatorTop = 0;
|
|
129
|
+
} else {
|
|
130
|
+
// Get previous block to find the gap
|
|
131
|
+
const prevBlock = blocks[index - 1];
|
|
132
|
+
const prevBlockEl = blockRefs.current.get(prevBlock.id);
|
|
133
|
+
if (prevBlockEl) {
|
|
134
|
+
const prevBlockRect = prevBlockEl.getBoundingClientRect();
|
|
135
|
+
const prevBlockBottom = prevBlockRect.bottom - containerRect.top;
|
|
136
|
+
// Position in the middle of the gap (mb-4 = 16px margin)
|
|
137
|
+
indicatorTop = prevBlockBottom + (elementTop - prevBlockBottom) / 2;
|
|
226
138
|
} else {
|
|
227
|
-
|
|
139
|
+
indicatorTop = elementTop;
|
|
228
140
|
}
|
|
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
141
|
}
|
|
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
142
|
}
|
|
262
143
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
-
}
|
|
144
|
+
setDropIndicatorPosition({
|
|
145
|
+
top: indicatorTop,
|
|
146
|
+
left: 0,
|
|
147
|
+
width: containerRect.width,
|
|
148
|
+
});
|
|
286
149
|
};
|
|
287
150
|
|
|
288
|
-
const
|
|
151
|
+
const handleDrop = (e: React.DragEvent, index: number | null) => {
|
|
289
152
|
e.preventDefault();
|
|
290
153
|
e.stopPropagation();
|
|
291
154
|
|
|
292
|
-
const
|
|
293
|
-
const globalBlockId = typeof window !== 'undefined' ? (window as any).__DRAGGED_BLOCK_ID__ : null;
|
|
294
|
-
const blockId = dataTransferBlockId || globalBlockId;
|
|
155
|
+
const blockId = e.dataTransfer.getData('block-id') || (window as any).__DRAGGED_BLOCK_ID__;
|
|
295
156
|
const blockType = e.dataTransfer.getData('block-type');
|
|
296
157
|
|
|
297
|
-
|
|
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
|
|
158
|
+
// Clear the global dragged block ID immediately to prevent it from being used for new blocks
|
|
308
159
|
if (typeof window !== 'undefined') {
|
|
309
160
|
(window as any).__DRAGGED_BLOCK_ID__ = null;
|
|
310
161
|
}
|
|
311
162
|
|
|
163
|
+
// Logic: index is null when dropping on the container background (appends to end)
|
|
164
|
+
// When dropAtEnd is true, we want to place it AFTER the block at index, so targetIndex = index + 1
|
|
165
|
+
// When dropAtEnd is false, we want to place it BEFORE the block at index, so targetIndex = index
|
|
166
|
+
// Use ref to get the latest value (React state updates are async)
|
|
167
|
+
const isDropAtEnd = dropAtEndRef.current;
|
|
168
|
+
let targetIndex = index === null ? blocks.length : (isDropAtEnd ? index + 1 : index);
|
|
169
|
+
|
|
312
170
|
if (blockId) {
|
|
313
|
-
// Moving existing block - check if it's in this container or elsewhere
|
|
314
171
|
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
172
|
if (currentIndex !== -1) {
|
|
327
|
-
//
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
173
|
+
// Moving within the same array - need to adjust for removal
|
|
174
|
+
let finalMoveIndex = targetIndex;
|
|
175
|
+
|
|
176
|
+
if (currentIndex < targetIndex) {
|
|
177
|
+
// Moving forward: when we remove the item from currentIndex, everything after it shifts down by 1.
|
|
178
|
+
if (isDropAtEnd) {
|
|
179
|
+
// Dropping below: we want it at index + 1 in the original array
|
|
180
|
+
// If currentIndex <= index: after removal, block at index stays at index, so we want index + 1 = targetIndex
|
|
181
|
+
// If index < currentIndex < targetIndex: after removal, we still want index + 1, but since we removed
|
|
182
|
+
// an item before targetIndex, the position targetIndex in original = targetIndex - 1 in new array
|
|
183
|
+
if (index !== null && currentIndex <= index) {
|
|
184
|
+
// Item is at or before target block - no adjustment needed
|
|
185
|
+
finalMoveIndex = targetIndex;
|
|
186
|
+
} else {
|
|
187
|
+
// Item is after target block but before targetIndex - need to adjust
|
|
188
|
+
finalMoveIndex = targetIndex - 1;
|
|
189
|
+
}
|
|
190
|
+
} else {
|
|
191
|
+
// Dropping above: targetIndex = index means "before the block at index"
|
|
192
|
+
// After removal, if currentIndex < index, the block at index shifts to index - 1,
|
|
193
|
+
// so we want it at index - 1 in the new array.
|
|
194
|
+
finalMoveIndex = targetIndex - 1;
|
|
195
|
+
}
|
|
337
196
|
}
|
|
197
|
+
// If currentIndex >= targetIndex, no adjustment needed (moving backward or same position)
|
|
198
|
+
|
|
199
|
+
console.log('[LayoutContainer] Drop calculation:', {
|
|
200
|
+
blockId,
|
|
201
|
+
index,
|
|
202
|
+
dropAtEnd: isDropAtEnd,
|
|
203
|
+
currentIndex,
|
|
204
|
+
targetIndex,
|
|
205
|
+
finalMoveIndex,
|
|
206
|
+
blocksCount: blocks.length
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
onBlockMove(blockId, Math.max(0, finalMoveIndex), containerId);
|
|
338
210
|
} else {
|
|
339
|
-
//
|
|
340
|
-
console.log('[LayoutContainer] Moving block from elsewhere to container:', { blockId, targetIndex, containerId, isEmpty: blocks.length === 0 });
|
|
211
|
+
// Moving from another container - no adjustment needed
|
|
341
212
|
onBlockMove(blockId, targetIndex, containerId);
|
|
342
213
|
}
|
|
343
214
|
} 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 });
|
|
215
|
+
// Adding new block - use targetIndex as-is
|
|
347
216
|
onBlockAdd(blockType, targetIndex, containerId);
|
|
348
|
-
} else {
|
|
349
|
-
console.warn('[LayoutContainer] Body drop with no block ID or type!');
|
|
350
217
|
}
|
|
351
218
|
|
|
352
|
-
|
|
353
|
-
setDragOverIndex(null);
|
|
354
|
-
setDraggedBlockId(null);
|
|
219
|
+
// Clean up
|
|
355
220
|
setDropIndicatorPosition(null);
|
|
221
|
+
setDragOverIndex(null);
|
|
222
|
+
setDropAtEnd(false);
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
const setBlockRef = (id: string) => (el: HTMLDivElement | null) => {
|
|
226
|
+
if (el) blockRefs.current.set(id, el); else blockRefs.current.delete(id);
|
|
356
227
|
};
|
|
357
228
|
|
|
358
229
|
return (
|
|
359
230
|
<div
|
|
360
231
|
ref={containerRef}
|
|
361
232
|
data-layout-container={containerId}
|
|
362
|
-
className={`relative flex flex-col ${className}`}
|
|
363
|
-
onDragOver={
|
|
364
|
-
onDrop={
|
|
365
|
-
onDragLeave={
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
setDragOverIndex(null);
|
|
370
|
-
setIsDragging(false);
|
|
371
|
-
setDraggedBlockId(null);
|
|
233
|
+
className={`relative flex flex-col min-h-[40px] transition-colors ${className}`}
|
|
234
|
+
onDragOver={(e) => { e.preventDefault(); setIsDragging(true); }}
|
|
235
|
+
onDrop={(e) => handleDrop(e, null)}
|
|
236
|
+
onDragLeave={(e) => {
|
|
237
|
+
if (!e.currentTarget.contains(e.relatedTarget as Node)) {
|
|
238
|
+
setDropIndicatorPosition(null);
|
|
239
|
+
}
|
|
372
240
|
}}
|
|
373
241
|
>
|
|
374
|
-
{/*
|
|
375
|
-
{dropIndicatorPosition && (
|
|
376
|
-
<
|
|
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>
|
|
242
|
+
{/* 1. Visual Indicator Overlay */}
|
|
243
|
+
{dropIndicatorPosition && isDragging && (
|
|
244
|
+
<DropIndicator position={dropIndicatorPosition} darkMode={darkMode} />
|
|
404
245
|
)}
|
|
405
246
|
|
|
247
|
+
{/* 2. Content Area */}
|
|
406
248
|
{blocks.length === 0 ? (
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
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
|
-
>
|
|
249
|
+
<EmptyState isDragging={isDragging} darkMode={darkMode} label={emptyLabel} />
|
|
250
|
+
) : (
|
|
251
|
+
blocks.map((block, index) => (
|
|
418
252
|
<div
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
? 'bg-primary/10'
|
|
425
|
-
: 'bg-neutral-100'
|
|
426
|
-
}`}
|
|
253
|
+
key={block.id}
|
|
254
|
+
ref={setBlockRef(block.id)}
|
|
255
|
+
onDragOver={(e) => handleDragOverBlock(e, index, blockRefs.current.get(block.id)!)}
|
|
256
|
+
onDrop={(e) => handleDrop(e, index)}
|
|
257
|
+
className="relative mb-4 last:mb-0"
|
|
427
258
|
>
|
|
428
|
-
<
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
: isDragging
|
|
435
|
-
? 'text-primary'
|
|
436
|
-
: 'text-neutral-400'
|
|
437
|
-
}`}
|
|
259
|
+
<BlockWrapper
|
|
260
|
+
block={block}
|
|
261
|
+
onUpdate={(data) => onBlockUpdate(block.id, data, containerId)}
|
|
262
|
+
onDelete={() => onBlockDelete(block.id, containerId)}
|
|
263
|
+
onMoveUp={index > 0 ? () => onBlockMove(block.id, index - 1, containerId) : undefined}
|
|
264
|
+
onMoveDown={index < blocks.length - 1 ? () => onBlockMove(block.id, index + 1, containerId) : undefined}
|
|
438
265
|
/>
|
|
439
266
|
</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
267
|
))
|
|
490
268
|
)}
|
|
491
269
|
</div>
|
|
492
270
|
);
|
|
493
271
|
}
|
|
494
272
|
|
|
273
|
+
/**
|
|
274
|
+
* Visual Line that shows where the block will land
|
|
275
|
+
*/
|
|
276
|
+
function DropIndicator({ position, darkMode }: { position: any; darkMode: boolean }) {
|
|
277
|
+
return (
|
|
278
|
+
<div
|
|
279
|
+
className="absolute z-50 pointer-events-none"
|
|
280
|
+
style={{
|
|
281
|
+
top: `${position.top - 12}px`,
|
|
282
|
+
left: `${position.left}px`,
|
|
283
|
+
width: `${position.width}px`,
|
|
284
|
+
height: '24px',
|
|
285
|
+
}}
|
|
286
|
+
>
|
|
287
|
+
<div className={`absolute inset-0 rounded-lg border border-dashed backdrop-blur-sm
|
|
288
|
+
${darkMode ? 'bg-primary/20 border-primary/40' : 'bg-primary/10 border-primary/30'}`}
|
|
289
|
+
/>
|
|
290
|
+
<div className={`absolute top-1/2 left-0 right-0 h-0.5 transform -translate-y-1/2
|
|
291
|
+
${darkMode ? 'bg-primary' : 'bg-primary'}`}
|
|
292
|
+
/>
|
|
293
|
+
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2">
|
|
294
|
+
<div className="w-6 h-6 rounded-full flex items-center justify-center bg-primary shadow-lg">
|
|
295
|
+
<div className="w-2 h-2 rounded-full bg-white" />
|
|
296
|
+
</div>
|
|
297
|
+
</div>
|
|
298
|
+
</div>
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Placeholder when the container is empty
|
|
304
|
+
*/
|
|
305
|
+
function EmptyState({ isDragging, darkMode, label }: { isDragging: boolean; darkMode: boolean; label: string }) {
|
|
306
|
+
return (
|
|
307
|
+
<div className={`flex flex-col items-center justify-center py-12 px-6 rounded-2xl border border-dashed transition-all
|
|
308
|
+
${darkMode
|
|
309
|
+
? isDragging ? 'border-primary/50 bg-primary/10' : 'border-neutral-700 bg-neutral-800/20'
|
|
310
|
+
: isDragging ? 'border-primary/50 bg-primary/5' : 'border-neutral-200 bg-neutral-50/30'
|
|
311
|
+
}`}
|
|
312
|
+
>
|
|
313
|
+
<div className={`p-3 rounded-full mb-3 ${darkMode ? 'bg-neutral-800' : 'bg-neutral-100'}`}>
|
|
314
|
+
<Plus size={20} className={isDragging ? 'text-primary' : 'text-neutral-400'} />
|
|
315
|
+
</div>
|
|
316
|
+
<p className={`text-xs font-black uppercase tracking-wider
|
|
317
|
+
${isDragging ? 'text-primary' : 'text-neutral-500'}`}>
|
|
318
|
+
{isDragging ? 'Drop Block Here' : label}
|
|
319
|
+
</p>
|
|
320
|
+
</div>
|
|
321
|
+
);
|
|
322
|
+
}
|