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