create-nextblock 0.2.33 → 0.2.35
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 +1 -1
- package/templates/nextblock-template/app/[slug]/PageClientContent.tsx +67 -67
- package/templates/nextblock-template/app/[slug]/page.tsx +4 -4
- package/templates/nextblock-template/app/cms/blocks/actions.ts +5 -5
- package/templates/nextblock-template/app/cms/blocks/components/BackgroundSelector.tsx +348 -350
- package/templates/nextblock-template/app/cms/blocks/components/BlockEditorArea.tsx +13 -16
- package/templates/nextblock-template/app/cms/blocks/components/BlockEditorModal.tsx +1 -1
- package/templates/nextblock-template/app/cms/blocks/components/ColumnEditor.tsx +24 -42
- package/templates/nextblock-template/app/cms/blocks/components/EditableBlock.tsx +6 -6
- package/templates/nextblock-template/app/cms/blocks/editors/SectionBlockEditor.tsx +35 -56
- package/templates/nextblock-template/app/cms/blocks/editors/TextBlockEditor.tsx +81 -81
- package/templates/nextblock-template/app/cms/media/actions.ts +12 -12
- package/templates/nextblock-template/app/cms/media/components/MediaGridClient.tsx +3 -3
- package/templates/nextblock-template/app/cms/media/components/MediaUploadForm.tsx +1 -1
- package/templates/nextblock-template/app/cms/media/page.tsx +120 -120
- package/templates/nextblock-template/app/cms/revisions/JsonDiffView.tsx +86 -87
- package/templates/nextblock-template/app/cms/revisions/RevisionHistoryButton.tsx +10 -16
- package/templates/nextblock-template/app/cms/revisions/service.ts +344 -344
- package/templates/nextblock-template/app/cms/settings/extra-translations/actions.ts +1 -1
- package/templates/nextblock-template/app/cms/settings/logos/[id]/edit/page.tsx +0 -1
- package/templates/nextblock-template/app/providers.tsx +2 -2
- package/templates/nextblock-template/components/BlockRenderer.tsx +9 -9
- package/templates/nextblock-template/components/ResponsiveNav.tsx +22 -22
- package/templates/nextblock-template/components/blocks/PostsGridBlock.tsx +12 -12
- package/templates/nextblock-template/components/blocks/renderers/ClientTextBlockRenderer.tsx +26 -26
- package/templates/nextblock-template/components/blocks/renderers/HeroBlockRenderer.tsx +41 -41
- package/templates/nextblock-template/components/blocks/renderers/ImageBlockRenderer.tsx +7 -7
- package/templates/nextblock-template/components/theme-switcher.tsx +78 -78
- package/templates/nextblock-template/eslint.config.mjs +35 -37
- package/templates/nextblock-template/lib/blocks/blockRegistry.ts +10 -10
- package/templates/nextblock-template/next-env.d.ts +6 -6
- package/templates/nextblock-template/package.json +1 -1
|
@@ -82,7 +82,7 @@ export default function BlockEditorArea({ parentId, parentType, initialBlocks, l
|
|
|
82
82
|
const [activeBlock, setActiveBlock] = useState<Block | null>(null);
|
|
83
83
|
const [insertionIndex, setInsertionIndex] = useState<number | null>(null);
|
|
84
84
|
const [editingNestedBlockInfo, setEditingNestedBlockInfo] = useState<EditingNestedBlockInfo | null>(null);
|
|
85
|
-
const [NestedBlockEditorComponent, setNestedBlockEditorComponent] = useState<ComponentType<
|
|
85
|
+
const [NestedBlockEditorComponent, setNestedBlockEditorComponent] = useState<ComponentType<any> | null>(null);
|
|
86
86
|
const [tempNestedBlockContent, setTempNestedBlockContent] = useState<Json | null>(null);
|
|
87
87
|
|
|
88
88
|
useEffect(() => {
|
|
@@ -140,30 +140,30 @@ export default function BlockEditorArea({ parentId, parentType, initialBlocks, l
|
|
|
140
140
|
useEffect(() => {
|
|
141
141
|
if (editingNestedBlockInfo) {
|
|
142
142
|
const blockType = editingNestedBlockInfo.blockData.block_type;
|
|
143
|
-
let SelectedEditor: React.ComponentType<
|
|
143
|
+
let SelectedEditor: React.ComponentType<any> | null = null;
|
|
144
144
|
|
|
145
145
|
try {
|
|
146
146
|
switch (blockType) {
|
|
147
147
|
case 'text':
|
|
148
|
-
SelectedEditor = DynamicTextBlockEditor
|
|
148
|
+
SelectedEditor = DynamicTextBlockEditor;
|
|
149
149
|
break;
|
|
150
150
|
case 'heading':
|
|
151
|
-
SelectedEditor = DynamicHeadingBlockEditor
|
|
151
|
+
SelectedEditor = DynamicHeadingBlockEditor;
|
|
152
152
|
break;
|
|
153
153
|
case 'image':
|
|
154
|
-
SelectedEditor = DynamicImageBlockEditor
|
|
154
|
+
SelectedEditor = DynamicImageBlockEditor;
|
|
155
155
|
break;
|
|
156
156
|
case 'button':
|
|
157
|
-
SelectedEditor = DynamicButtonBlockEditor
|
|
157
|
+
SelectedEditor = DynamicButtonBlockEditor;
|
|
158
158
|
break;
|
|
159
159
|
case 'posts_grid':
|
|
160
|
-
SelectedEditor = DynamicPostsGridBlockEditor
|
|
160
|
+
SelectedEditor = DynamicPostsGridBlockEditor;
|
|
161
161
|
break;
|
|
162
162
|
case 'video_embed':
|
|
163
|
-
SelectedEditor = DynamicVideoEmbedBlockEditor
|
|
163
|
+
SelectedEditor = DynamicVideoEmbedBlockEditor;
|
|
164
164
|
break;
|
|
165
165
|
case 'section':
|
|
166
|
-
SelectedEditor = DynamicSectionBlockEditor
|
|
166
|
+
SelectedEditor = DynamicSectionBlockEditor;
|
|
167
167
|
break;
|
|
168
168
|
default:
|
|
169
169
|
console.warn(`No dynamic editor configured for nested block type: ${blockType}`);
|
|
@@ -389,10 +389,7 @@ export default function BlockEditorArea({ parentId, parentType, initialBlocks, l
|
|
|
389
389
|
parentBlockId: parentBlockIdStr,
|
|
390
390
|
columnIndex,
|
|
391
391
|
blockIndexInColumn,
|
|
392
|
-
blockData:
|
|
393
|
-
...nestedBlockData,
|
|
394
|
-
content: nestedBlockData.content as unknown as Json
|
|
395
|
-
},
|
|
392
|
+
blockData: nestedBlockData,
|
|
396
393
|
});
|
|
397
394
|
} else {
|
|
398
395
|
console.error("Nested block not found at specified indices:", { parentBlockIdStr, columnIndex, blockIndexInColumn });
|
|
@@ -475,8 +472,8 @@ export default function BlockEditorArea({ parentId, parentType, initialBlocks, l
|
|
|
475
472
|
<EditableBlock
|
|
476
473
|
block={activeBlock}
|
|
477
474
|
className="h-full"
|
|
478
|
-
onDelete={() => {}}
|
|
479
|
-
onContentChange={() => {}}
|
|
475
|
+
onDelete={() => {}} // eslint-disable-line @typescript-eslint/no-empty-function
|
|
476
|
+
onContentChange={() => {}} // eslint-disable-line @typescript-eslint/no-empty-function
|
|
480
477
|
/>
|
|
481
478
|
</div>
|
|
482
479
|
) : null}
|
|
@@ -525,7 +522,7 @@ export default function BlockEditorArea({ parentId, parentType, initialBlocks, l
|
|
|
525
522
|
if (blockType === "posts_grid") {
|
|
526
523
|
const fullBlockForEditor: Block = {
|
|
527
524
|
block_type: editingNestedBlockInfo.blockData.block_type,
|
|
528
|
-
content: tempNestedBlockContent
|
|
525
|
+
content: tempNestedBlockContent,
|
|
529
526
|
id: 0, // Temporary ID for nested blocks
|
|
530
527
|
language_id: languageId,
|
|
531
528
|
order: 0, // Temporary order for nested blocks
|
|
@@ -33,7 +33,7 @@ type BlockEditorModalProps = {
|
|
|
33
33
|
isOpen: boolean;
|
|
34
34
|
onClose: () => void;
|
|
35
35
|
onSave: (updatedContent: unknown) => void;
|
|
36
|
-
EditorComponent: LazyExoticComponent<ComponentType<
|
|
36
|
+
EditorComponent: LazyExoticComponent<ComponentType<BlockEditorProps<unknown>>>;
|
|
37
37
|
};
|
|
38
38
|
|
|
39
39
|
export function BlockEditorModal({
|
|
@@ -28,19 +28,6 @@ interface SortableColumnBlockProps {
|
|
|
28
28
|
onClick: (e: React.MouseEvent<HTMLDivElement>) => void;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
type AnyBlockContent = {
|
|
32
|
-
html_content?: string;
|
|
33
|
-
text_content?: string;
|
|
34
|
-
level?: number;
|
|
35
|
-
alt_text?: string;
|
|
36
|
-
media_id?: string;
|
|
37
|
-
text?: string;
|
|
38
|
-
url?: string;
|
|
39
|
-
title?: string;
|
|
40
|
-
columns?: number;
|
|
41
|
-
postsPerPage?: number;
|
|
42
|
-
};
|
|
43
|
-
|
|
44
31
|
function SortableColumnBlock({ block, index, columnIndex, onEdit, onDelete, blockType, onClick }: SortableColumnBlockProps) {
|
|
45
32
|
const {
|
|
46
33
|
attributes,
|
|
@@ -98,35 +85,30 @@ function SortableColumnBlock({ block, index, columnIndex, onEdit, onDelete, bloc
|
|
|
98
85
|
</div>
|
|
99
86
|
</div>
|
|
100
87
|
<div className="text-xs text-gray-500 dark:text-gray-400">
|
|
101
|
-
{
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
)}
|
|
120
|
-
{block.block_type === 'posts_grid' && (
|
|
121
|
-
<div>Posts Grid: {content.columns || 3} cols, {content.postsPerPage || 12} posts</div>
|
|
122
|
-
)}
|
|
123
|
-
</>
|
|
124
|
-
);
|
|
125
|
-
})()}
|
|
88
|
+
{block.block_type === 'text' && (
|
|
89
|
+
<div dangerouslySetInnerHTML={{ __html: (block.content.html_content || 'Empty text').substring(0, 50) + (block.content.html_content && block.content.html_content.length > 50 ? '...' : '') }} />
|
|
90
|
+
)}
|
|
91
|
+
{block.block_type === 'heading' && (
|
|
92
|
+
<div>H{block.content.level || 1}: {(block.content.text_content || 'Empty heading').substring(0, 30) + (block.content.text_content && block.content.text_content.length > 30 ? '...' : '')}</div>
|
|
93
|
+
)}
|
|
94
|
+
{block.block_type === 'image' && (
|
|
95
|
+
<div>Image: {block.content.alt_text || block.content.media_id ? 'Image selected' : 'No image selected'}</div>
|
|
96
|
+
)}
|
|
97
|
+
{block.block_type === 'button' && (
|
|
98
|
+
<div>Button: {block.content.text || 'No text'} → {block.content.url || '#'}</div>
|
|
99
|
+
)}
|
|
100
|
+
{block.block_type === 'video_embed' && (
|
|
101
|
+
<div>Video: {block.content.title || block.content.url || 'No URL set'}</div>
|
|
102
|
+
)}
|
|
103
|
+
{block.block_type === 'posts_grid' && (
|
|
104
|
+
<div>Posts Grid: {block.content.columns || 3} cols, {block.content.postsPerPage || 12} posts</div>
|
|
105
|
+
)}
|
|
126
106
|
</div>
|
|
127
107
|
</div>
|
|
128
108
|
);
|
|
129
109
|
}
|
|
110
|
+
|
|
111
|
+
// Column editor component
|
|
130
112
|
export interface ColumnEditorProps {
|
|
131
113
|
columnIndex: number;
|
|
132
114
|
blocks: ColumnBlock[];
|
|
@@ -139,7 +121,7 @@ type EditingBlock = ColumnBlock & { index: number };
|
|
|
139
121
|
export default function ColumnEditor({ columnIndex, blocks, onBlocksChange, blockType }: ColumnEditorProps) {
|
|
140
122
|
const [editingBlock, setEditingBlock] = useState<EditingBlock | null>(null);
|
|
141
123
|
const [isBlockSelectorOpen, setIsBlockSelectorOpen] = useState(false);
|
|
142
|
-
const [LazyEditor, setLazyEditor] = useState<React.LazyExoticComponent<React.ComponentType<
|
|
124
|
+
const [LazyEditor, setLazyEditor] = useState<React.LazyExoticComponent<React.ComponentType<any>> | null>(null);
|
|
143
125
|
const [isConfirmOpen, setIsConfirmOpen] = useState(false);
|
|
144
126
|
const [blockToDeleteIndex, setBlockToDeleteIndex] = useState<number | null>(null);
|
|
145
127
|
|
|
@@ -152,7 +134,7 @@ export default function ColumnEditor({ columnIndex, blocks, onBlocksChange, bloc
|
|
|
152
134
|
const initialContent = getInitialContent(selectedBlockType);
|
|
153
135
|
const newBlock: ColumnBlock = {
|
|
154
136
|
block_type: selectedBlockType,
|
|
155
|
-
content:
|
|
137
|
+
content: initialContent || {},
|
|
156
138
|
temp_id: `temp-${Date.now()}-${Math.random()}`
|
|
157
139
|
};
|
|
158
140
|
onBlocksChange([...blocks, newBlock]);
|
|
@@ -196,13 +178,13 @@ export default function ColumnEditor({ columnIndex, blocks, onBlocksChange, bloc
|
|
|
196
178
|
}
|
|
197
179
|
};
|
|
198
180
|
|
|
199
|
-
const handleSave = (newContent:
|
|
181
|
+
const handleSave = (newContent: any) => {
|
|
200
182
|
if (editingBlock === null) return;
|
|
201
183
|
|
|
202
184
|
const updatedBlocks = [...blocks];
|
|
203
185
|
updatedBlocks[editingBlock.index] = {
|
|
204
186
|
...updatedBlocks[editingBlock.index],
|
|
205
|
-
content: newContent
|
|
187
|
+
content: newContent,
|
|
206
188
|
};
|
|
207
189
|
onBlocksChange(updatedBlocks);
|
|
208
190
|
setEditingBlock(null);
|
|
@@ -16,8 +16,8 @@ import { cn } from '@nextblock-cms/utils';
|
|
|
16
16
|
export interface EditableBlockProps {
|
|
17
17
|
block: Block;
|
|
18
18
|
onDelete: (blockId: number) => void;
|
|
19
|
-
onContentChange: (blockId: number, newContent: Record<string,
|
|
20
|
-
dragHandleProps?: Record<string,
|
|
19
|
+
onContentChange: (blockId: number, newContent: Record<string, any>) => void;
|
|
20
|
+
dragHandleProps?: Record<string, any>;
|
|
21
21
|
onEditNestedBlock?: (parentBlockId: string, columnIndex: number, blockIndexInColumn: number) => void;
|
|
22
22
|
className?: string;
|
|
23
23
|
}
|
|
@@ -34,7 +34,7 @@ export default function EditableBlock({
|
|
|
34
34
|
// Move all hooks to the top before any conditional returns
|
|
35
35
|
const [isConfigPanelOpen, setIsConfigPanelOpen] = useState(false);
|
|
36
36
|
const [editingBlock, setEditingBlock] = useState<Block | null>(null);
|
|
37
|
-
const [LazyEditor, setLazyEditor] = useState<LazyExoticComponent<ComponentType<
|
|
37
|
+
const [LazyEditor, setLazyEditor] = useState<LazyExoticComponent<ComponentType<any>> | null>(null);
|
|
38
38
|
|
|
39
39
|
const SectionEditor = useMemo(() => {
|
|
40
40
|
if (block?.block_type === 'section' || block?.block_type === 'hero') {
|
|
@@ -156,7 +156,7 @@ export default function EditableBlock({
|
|
|
156
156
|
{isSection ? (
|
|
157
157
|
<div className="mt-2 min-h-[200px]">
|
|
158
158
|
<Suspense fallback={<div className="flex justify-center items-center h-full"><p>Loading Editor...</p></div>}>
|
|
159
|
-
{SectionEditor && <SectionEditor block={block} content={block.content || {}} onChange={(newContent: Record<string,
|
|
159
|
+
{SectionEditor && <SectionEditor block={block} content={block.content || {}} onChange={(newContent: Record<string, any>) => onContentChange(block.id, newContent)} blockType={block.block_type as 'section' | 'hero'} isConfigPanelOpen={isConfigPanelOpen} />}
|
|
160
160
|
</Suspense>
|
|
161
161
|
</div>
|
|
162
162
|
) : renderPreview()}
|
|
@@ -170,8 +170,8 @@ export default function EditableBlock({
|
|
|
170
170
|
setEditingBlock(null);
|
|
171
171
|
setLazyEditor(null);
|
|
172
172
|
}}
|
|
173
|
-
onSave={(newContent:
|
|
174
|
-
onContentChange(block.id, newContent
|
|
173
|
+
onSave={(newContent: any) => {
|
|
174
|
+
onContentChange(block.id, newContent);
|
|
175
175
|
setEditingBlock(null);
|
|
176
176
|
setLazyEditor(null);
|
|
177
177
|
}}
|
|
@@ -38,19 +38,6 @@ interface SectionBlockEditorProps {
|
|
|
38
38
|
blockType: 'section' | 'hero';
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
type AnyBlockContent = {
|
|
42
|
-
html_content?: string;
|
|
43
|
-
text_content?: string;
|
|
44
|
-
level?: number;
|
|
45
|
-
alt_text?: string;
|
|
46
|
-
media_id?: string;
|
|
47
|
-
text?: string;
|
|
48
|
-
url?: string;
|
|
49
|
-
title?: string;
|
|
50
|
-
columns?: number;
|
|
51
|
-
postsPerPage?: number;
|
|
52
|
-
};
|
|
53
|
-
|
|
54
41
|
export default function SectionBlockEditor({
|
|
55
42
|
content,
|
|
56
43
|
onChange,
|
|
@@ -80,9 +67,8 @@ export default function SectionBlockEditor({
|
|
|
80
67
|
}, [content]);
|
|
81
68
|
|
|
82
69
|
|
|
83
|
-
type ColumnBlock = SectionBlockContent['column_blocks'][0][0];
|
|
84
70
|
const [activeId, setActiveId] = useState<string | null>(null);
|
|
85
|
-
const [draggedBlock, setDraggedBlock] = useState<
|
|
71
|
+
const [draggedBlock, setDraggedBlock] = useState<any>(null);
|
|
86
72
|
|
|
87
73
|
// DND sensors for cross-column dragging
|
|
88
74
|
const sensors = useSensors(
|
|
@@ -306,47 +292,40 @@ return (
|
|
|
306
292
|
</span>
|
|
307
293
|
</div>
|
|
308
294
|
<div className="text-xs text-blue-600 dark:text-blue-400 mt-1">
|
|
309
|
-
{
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
<div>
|
|
344
|
-
Posts Grid: {content.columns || 3} cols
|
|
345
|
-
</div>
|
|
346
|
-
)}
|
|
347
|
-
</>
|
|
348
|
-
);
|
|
349
|
-
})()}
|
|
295
|
+
{draggedBlock.block_type === "text" && (
|
|
296
|
+
<div
|
|
297
|
+
dangerouslySetInnerHTML={{
|
|
298
|
+
__html:
|
|
299
|
+
(
|
|
300
|
+
draggedBlock.content.html_content || "Empty text"
|
|
301
|
+
).substring(0, 30) + "...",
|
|
302
|
+
}}
|
|
303
|
+
/>
|
|
304
|
+
)}
|
|
305
|
+
{draggedBlock.block_type === "heading" && (
|
|
306
|
+
<div>
|
|
307
|
+
H{draggedBlock.content.level || 1}:{" "}
|
|
308
|
+
{(
|
|
309
|
+
draggedBlock.content.text_content || "Empty heading"
|
|
310
|
+
).substring(0, 20) + "..."}
|
|
311
|
+
</div>
|
|
312
|
+
)}
|
|
313
|
+
{draggedBlock.block_type === "image" && (
|
|
314
|
+
<div>
|
|
315
|
+
Image: {draggedBlock.content.alt_text || "No alt text"}
|
|
316
|
+
</div>
|
|
317
|
+
)}
|
|
318
|
+
{draggedBlock.block_type === "button" && (
|
|
319
|
+
<div>Button: {draggedBlock.content.text || "No text"}</div>
|
|
320
|
+
)}
|
|
321
|
+
{draggedBlock.block_type === "video_embed" && (
|
|
322
|
+
<div>Video: {draggedBlock.content.title || "No title"}</div>
|
|
323
|
+
)}
|
|
324
|
+
{draggedBlock.block_type === "posts_grid" && (
|
|
325
|
+
<div>
|
|
326
|
+
Posts Grid: {draggedBlock.content.columns || 3} cols
|
|
327
|
+
</div>
|
|
328
|
+
)}
|
|
350
329
|
</div>
|
|
351
330
|
</div>
|
|
352
331
|
) : null}
|
|
@@ -1,81 +1,81 @@
|
|
|
1
|
-
// app/cms/blocks/editors/TextBlockEditor.tsx
|
|
2
|
-
'use client';
|
|
3
|
-
|
|
4
|
-
import React, { useId, useState, useRef, useCallback } from 'react';
|
|
5
|
-
import dynamic from 'next/dynamic';
|
|
6
|
-
import MediaPickerDialog from '@/app/cms/media/components/MediaPickerDialog';
|
|
7
|
-
import { Label } from '@nextblock-cms/ui';
|
|
8
|
-
import { BlockEditorProps } from '../components/BlockEditorModal';
|
|
9
|
-
|
|
10
|
-
// Props expected by NotionEditor
|
|
11
|
-
type NotionEditorProps = {
|
|
12
|
-
content: string;
|
|
13
|
-
onChange: (html: string) => void;
|
|
14
|
-
openImagePicker?: () => Promise<{ src: string; alt?: string; width?: number | null; height?: number | null; blurDataURL?: string | null } | null>;
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
// Use the alias that resolves in your repo; if you mapped @nextblock-cms/editor, swap it here.
|
|
18
|
-
const NotionEditor = dynamic<NotionEditorProps>(
|
|
19
|
-
() => import('@nextblock-cms/editor').then((m) => m.NotionEditor),
|
|
20
|
-
{ ssr: false }
|
|
21
|
-
);
|
|
22
|
-
|
|
23
|
-
export type TextBlockContent = {
|
|
24
|
-
html_content?: string;
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
export default function TextBlockEditor({
|
|
28
|
-
content,
|
|
29
|
-
onChange,
|
|
30
|
-
}: BlockEditorProps<Partial<TextBlockContent>>) {
|
|
31
|
-
const labelId = useId();
|
|
32
|
-
const [pickerOpen, setPickerOpen] = useState(false);
|
|
33
|
-
const resolverRef = useRef<null | ((v:
|
|
34
|
-
const R2_BASE_URL = process.env.NEXT_PUBLIC_R2_BASE_URL || '';
|
|
35
|
-
const openImagePicker = useCallback(() => {
|
|
36
|
-
setPickerOpen(true);
|
|
37
|
-
return new Promise<{ src: string; alt?: string; width?: number | null; height?: number | null; blurDataURL?: string | null } | null>((resolve) => {
|
|
38
|
-
resolverRef.current = resolve;
|
|
39
|
-
});
|
|
40
|
-
}, []);
|
|
41
|
-
|
|
42
|
-
return (
|
|
43
|
-
<div className="h-full flex flex-col">
|
|
44
|
-
<Label htmlFor={labelId} className="sr-only">
|
|
45
|
-
Text Content
|
|
46
|
-
</Label>
|
|
47
|
-
|
|
48
|
-
<div id={labelId} role="group" aria-labelledby={labelId} className="flex-1 min-h-0 flex flex-col">
|
|
49
|
-
<NotionEditor
|
|
50
|
-
content={content?.html_content ?? ''}
|
|
51
|
-
onChange={(html) => onChange({ html_content: html })}
|
|
52
|
-
openImagePicker={openImagePicker}
|
|
53
|
-
/>
|
|
54
|
-
|
|
55
|
-
{/* Hidden controlled MediaPickerDialog for image selection */}
|
|
56
|
-
<div className="sr-only" aria-hidden>
|
|
57
|
-
<MediaPickerDialog
|
|
58
|
-
hideTrigger
|
|
59
|
-
open={pickerOpen}
|
|
60
|
-
onOpenChange={setPickerOpen}
|
|
61
|
-
title="Select or Upload Image"
|
|
62
|
-
accept={(m) => !!m.file_type?.startsWith('image/')}
|
|
63
|
-
onSelect={(media) => {
|
|
64
|
-
const src = `${R2_BASE_URL}/${media.object_key}`;
|
|
65
|
-
resolverRef.current?.({
|
|
66
|
-
src,
|
|
67
|
-
alt: media.description || media.file_name || undefined,
|
|
68
|
-
width: media.width ?? null,
|
|
69
|
-
height: media.height ?? null,
|
|
70
|
-
blurDataURL: media.blur_data_url ?? null,
|
|
71
|
-
});
|
|
72
|
-
resolverRef.current = null;
|
|
73
|
-
setPickerOpen(false);
|
|
74
|
-
}}
|
|
75
|
-
/>
|
|
76
|
-
</div>
|
|
77
|
-
</div>
|
|
78
|
-
</div>
|
|
79
|
-
);
|
|
80
|
-
}
|
|
81
|
-
|
|
1
|
+
// app/cms/blocks/editors/TextBlockEditor.tsx
|
|
2
|
+
'use client';
|
|
3
|
+
|
|
4
|
+
import React, { useId, useState, useRef, useCallback } from 'react';
|
|
5
|
+
import dynamic from 'next/dynamic';
|
|
6
|
+
import MediaPickerDialog from '@/app/cms/media/components/MediaPickerDialog';
|
|
7
|
+
import { Label } from '@nextblock-cms/ui';
|
|
8
|
+
import { BlockEditorProps } from '../components/BlockEditorModal';
|
|
9
|
+
|
|
10
|
+
// Props expected by NotionEditor
|
|
11
|
+
type NotionEditorProps = {
|
|
12
|
+
content: string;
|
|
13
|
+
onChange: (html: string) => void;
|
|
14
|
+
openImagePicker?: () => Promise<{ src: string; alt?: string; width?: number | null; height?: number | null; blurDataURL?: string | null } | null>;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// Use the alias that resolves in your repo; if you mapped @nextblock-cms/editor, swap it here.
|
|
18
|
+
const NotionEditor = dynamic<NotionEditorProps>(
|
|
19
|
+
() => import('@nextblock-cms/editor').then((m) => m.NotionEditor),
|
|
20
|
+
{ ssr: false }
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
export type TextBlockContent = {
|
|
24
|
+
html_content?: string;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export default function TextBlockEditor({
|
|
28
|
+
content,
|
|
29
|
+
onChange,
|
|
30
|
+
}: BlockEditorProps<Partial<TextBlockContent>>) {
|
|
31
|
+
const labelId = useId();
|
|
32
|
+
const [pickerOpen, setPickerOpen] = useState(false);
|
|
33
|
+
const resolverRef = useRef<null | ((v: any) => void)>(null);
|
|
34
|
+
const R2_BASE_URL = process.env.NEXT_PUBLIC_R2_BASE_URL || '';
|
|
35
|
+
const openImagePicker = useCallback(() => {
|
|
36
|
+
setPickerOpen(true);
|
|
37
|
+
return new Promise<{ src: string; alt?: string; width?: number | null; height?: number | null; blurDataURL?: string | null } | null>((resolve) => {
|
|
38
|
+
resolverRef.current = resolve;
|
|
39
|
+
});
|
|
40
|
+
}, []);
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<div className="h-full flex flex-col">
|
|
44
|
+
<Label htmlFor={labelId} className="sr-only">
|
|
45
|
+
Text Content
|
|
46
|
+
</Label>
|
|
47
|
+
|
|
48
|
+
<div id={labelId} role="group" aria-labelledby={labelId} className="flex-1 min-h-0 flex flex-col">
|
|
49
|
+
<NotionEditor
|
|
50
|
+
content={content?.html_content ?? ''}
|
|
51
|
+
onChange={(html) => onChange({ html_content: html })}
|
|
52
|
+
openImagePicker={openImagePicker}
|
|
53
|
+
/>
|
|
54
|
+
|
|
55
|
+
{/* Hidden controlled MediaPickerDialog for image selection */}
|
|
56
|
+
<div className="sr-only" aria-hidden>
|
|
57
|
+
<MediaPickerDialog
|
|
58
|
+
hideTrigger
|
|
59
|
+
open={pickerOpen}
|
|
60
|
+
onOpenChange={setPickerOpen}
|
|
61
|
+
title="Select or Upload Image"
|
|
62
|
+
accept={(m) => !!m.file_type?.startsWith('image/')}
|
|
63
|
+
onSelect={(media) => {
|
|
64
|
+
const src = `${R2_BASE_URL}/${media.object_key}`;
|
|
65
|
+
resolverRef.current?.({
|
|
66
|
+
src,
|
|
67
|
+
alt: media.description || media.file_name || undefined,
|
|
68
|
+
width: media.width ?? null,
|
|
69
|
+
height: media.height ?? null,
|
|
70
|
+
blurDataURL: media.blur_data_url ?? null,
|
|
71
|
+
});
|
|
72
|
+
resolverRef.current = null;
|
|
73
|
+
setPickerOpen(false);
|
|
74
|
+
}}
|
|
75
|
+
/>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
import { createClient } from "@nextblock-cms/db/server";
|
|
5
5
|
import { revalidatePath } from "next/cache";
|
|
6
|
-
import type { Database
|
|
6
|
+
import type { Database } from "@nextblock-cms/db";
|
|
7
7
|
import { encodedRedirect } from "@nextblock-cms/utils/server";
|
|
8
8
|
|
|
9
9
|
type Media = Database['public']['Tables']['media']['Row'];
|
|
@@ -111,7 +111,7 @@ export async function recordMediaUpload(payload: {
|
|
|
111
111
|
description: computedDescription,
|
|
112
112
|
width: primaryVariant.width,
|
|
113
113
|
height: primaryVariant.height,
|
|
114
|
-
variants: allVariantsToStore as
|
|
114
|
+
variants: allVariantsToStore as any, // Store all variants including the original
|
|
115
115
|
blur_data_url: payload.blurDataUrl || null, // Store if provided
|
|
116
116
|
// Ensure all other required fields for 'Media' type are present or nullable
|
|
117
117
|
};
|
|
@@ -382,9 +382,9 @@ export async function moveMultipleMediaItems(
|
|
|
382
382
|
let newMainKey = `${folder}${getFilename(mediaRow.object_key)}`;
|
|
383
383
|
|
|
384
384
|
// Build list of keys to move: primary + variant keys (if any)
|
|
385
|
-
type Variant =
|
|
386
|
-
const oldVariants: Variant[] = Array.isArray(mediaRow.variants) ? (mediaRow.variants as
|
|
387
|
-
(typeof mediaRow.variants === 'object' && mediaRow.variants !== null ? (mediaRow.variants as
|
|
385
|
+
type Variant = { objectKey: string; url?: string; [k: string]: any };
|
|
386
|
+
const oldVariants: Variant[] = Array.isArray(mediaRow.variants) ? (mediaRow.variants as Variant[]) :
|
|
387
|
+
(typeof mediaRow.variants === 'object' && mediaRow.variants !== null ? (mediaRow.variants as any) : []);
|
|
388
388
|
|
|
389
389
|
const variantMoves = (oldVariants || []).map((v) => ({
|
|
390
390
|
oldKey: v.objectKey,
|
|
@@ -429,9 +429,9 @@ export async function moveMultipleMediaItems(
|
|
|
429
429
|
await s3Client.send(new DeleteObjectCommand({ Bucket: R2_BUCKET_NAME, Key: oldKey }));
|
|
430
430
|
movedKeys.add(oldKey);
|
|
431
431
|
if (isMain) mainMoved = true;
|
|
432
|
-
} catch (err:
|
|
433
|
-
const name =
|
|
434
|
-
const message =
|
|
432
|
+
} catch (err: any) {
|
|
433
|
+
const name = err?.name || '';
|
|
434
|
+
const message = err?.message || String(err);
|
|
435
435
|
if (isMain) {
|
|
436
436
|
// Main object missing: attempt fallback to any existing variant
|
|
437
437
|
let promoted = false;
|
|
@@ -499,7 +499,7 @@ export async function moveMultipleMediaItems(
|
|
|
499
499
|
.map((v) => {
|
|
500
500
|
const filename = getFilename(v.objectKey);
|
|
501
501
|
const updatedKey = `${folder}${filename}`;
|
|
502
|
-
const updated = { ...v, objectKey: updatedKey };
|
|
502
|
+
const updated = { ...v, objectKey: updatedKey } as any;
|
|
503
503
|
if (R2_PUBLIC_URL_BASE) updated.url = `${R2_PUBLIC_URL_BASE}/${updatedKey}`;
|
|
504
504
|
return updated;
|
|
505
505
|
});
|
|
@@ -507,7 +507,7 @@ export async function moveMultipleMediaItems(
|
|
|
507
507
|
// Update DB
|
|
508
508
|
const { error: updateError } = await supabase
|
|
509
509
|
.from('media')
|
|
510
|
-
.update({ object_key: newMainKey, file_path: newMainKey, folder, variants: newVariants as
|
|
510
|
+
.update({ object_key: newMainKey, file_path: newMainKey, folder, variants: newVariants as any })
|
|
511
511
|
.eq('id', item.id);
|
|
512
512
|
if (updateError) {
|
|
513
513
|
results.push({ id: item.id, ok: false, error: updateError.message });
|
|
@@ -515,8 +515,8 @@ export async function moveMultipleMediaItems(
|
|
|
515
515
|
}
|
|
516
516
|
|
|
517
517
|
results.push({ id: item.id, ok: true });
|
|
518
|
-
} catch (err:
|
|
519
|
-
const msg =
|
|
518
|
+
} catch (err: any) {
|
|
519
|
+
const msg = err?.name && err?.message ? `${err.name}: ${err.message}` : (err?.message || String(err));
|
|
520
520
|
results.push({ id: item.id, ok: false, error: msg });
|
|
521
521
|
}
|
|
522
522
|
}
|
|
@@ -103,17 +103,17 @@ export default function MediaGridClient({ initialMediaItems, r2BaseUrl }: MediaG
|
|
|
103
103
|
let hadError = false;
|
|
104
104
|
for (const item of selectedItems) {
|
|
105
105
|
const res = await moveSingleMediaItem(item, dest);
|
|
106
|
-
if (
|
|
106
|
+
if ((res as any)?.error) {
|
|
107
107
|
// Accumulate errors but keep going
|
|
108
108
|
hadError = true;
|
|
109
|
-
setMoveError((prev) => (prev ? prev + " | " : "") + res.error);
|
|
109
|
+
setMoveError((prev) => (prev ? prev + " | " : "") + (res as any).error);
|
|
110
110
|
} else {
|
|
111
111
|
// Update local list
|
|
112
112
|
setMediaItems((prev) => prev.map((m) => {
|
|
113
113
|
if (m.id !== item.id) return m;
|
|
114
114
|
const filename = m.object_key.substring(m.object_key.lastIndexOf('/') + 1);
|
|
115
115
|
const folder = ensureFolderSlash(dest);
|
|
116
|
-
return { ...m, object_key: `${folder}${filename}`, folder };
|
|
116
|
+
return { ...m, object_key: `${folder}${filename}`, folder } as any;
|
|
117
117
|
}));
|
|
118
118
|
}
|
|
119
119
|
moved += 1;
|
|
@@ -259,7 +259,7 @@ export default function MediaUploadForm({ onUploadSuccess, returnJustData, defau
|
|
|
259
259
|
}
|
|
260
260
|
|
|
261
261
|
} catch (err: unknown) {
|
|
262
|
-
const isRedirect = (err instanceof Error && err.message === 'NEXT_REDIRECT') || (typeof (err as
|
|
262
|
+
const isRedirect = (err instanceof Error && err.message === 'NEXT_REDIRECT') || (typeof (err as any)?.digest === 'string' && (err as any).digest.startsWith('NEXT_REDIRECT'));
|
|
263
263
|
|
|
264
264
|
if (isRedirect && !returnJustData) {
|
|
265
265
|
setUploadStatus("success");
|