create-nextblock 0.2.45 → 0.2.46

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-nextblock",
3
- "version": "0.2.45",
3
+ "version": "0.2.46",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -143,33 +143,51 @@ export default function BlockEditorArea({ parentId, parentType, initialBlocks, l
143
143
  let SelectedEditor: React.ComponentType<any> | null = null;
144
144
 
145
145
  try {
146
- switch (blockType) {
147
- case 'text':
148
- SelectedEditor = DynamicTextBlockEditor;
149
- break;
150
- case 'heading':
151
- SelectedEditor = DynamicHeadingBlockEditor;
152
- break;
153
- case 'image':
154
- SelectedEditor = DynamicImageBlockEditor;
155
- break;
156
- case 'button':
157
- SelectedEditor = DynamicButtonBlockEditor;
158
- break;
159
- case 'posts_grid':
160
- SelectedEditor = DynamicPostsGridBlockEditor;
161
- break;
162
- case 'video_embed':
163
- SelectedEditor = DynamicVideoEmbedBlockEditor;
164
- break;
165
- case 'section':
166
- SelectedEditor = DynamicSectionBlockEditor;
167
- break;
168
- default:
169
- console.warn(`No dynamic editor configured for nested block type: ${blockType}`);
170
- alert(`Error: Editor not configured for ${blockType}.`);
171
- setEditingNestedBlockInfo(null);
172
- return;
146
+ // Check block registry for editor component
147
+ const blockDef = getBlockDefinition(blockType);
148
+
149
+ if (blockDef?.EditorComponent) {
150
+ SelectedEditor = blockDef.EditorComponent;
151
+ } else if (blockDef?.editorComponentFilename) {
152
+ // We can't easily do dynamic imports with variable paths inside this useEffect
153
+ // without potentially breaking webpack analysis or needing a different strategy.
154
+ // However, for the core blocks, we have the pre-defined dynamic imports below.
155
+
156
+ switch (blockType) {
157
+ case 'text':
158
+ SelectedEditor = DynamicTextBlockEditor;
159
+ break;
160
+ case 'heading':
161
+ SelectedEditor = DynamicHeadingBlockEditor;
162
+ break;
163
+ case 'image':
164
+ SelectedEditor = DynamicImageBlockEditor;
165
+ break;
166
+ case 'button':
167
+ SelectedEditor = DynamicButtonBlockEditor;
168
+ break;
169
+ case 'posts_grid':
170
+ SelectedEditor = DynamicPostsGridBlockEditor;
171
+ break;
172
+ case 'video_embed':
173
+ SelectedEditor = DynamicVideoEmbedBlockEditor;
174
+ break;
175
+ case 'section':
176
+ SelectedEditor = DynamicSectionBlockEditor;
177
+ break;
178
+ default:
179
+ // Fallback for custom blocks that might use file-based routing but aren't in the switch
180
+ // This might still fail if webpack hasn't bundled them, but it's worth a try or we need to explicitly add them.
181
+ // For the PoC Testimonial block, it has EditorComponent so it hits the first if.
182
+ console.warn(`No dynamic editor configured for nested block type: ${blockType}`);
183
+ alert(`Error: Editor not configured for ${blockType}.`);
184
+ setEditingNestedBlockInfo(null);
185
+ return;
186
+ }
187
+ } else {
188
+ console.warn(`No definition found for nested block type: ${blockType}`);
189
+ setEditingNestedBlockInfo(null);
190
+ return;
173
191
  }
174
192
  setNestedBlockEditorComponent(() => SelectedEditor);
175
193
  setTempNestedBlockContent(JSON.parse(JSON.stringify(editingNestedBlockInfo.blockData.content)));
@@ -33,7 +33,7 @@ type BlockEditorModalProps = {
33
33
  isOpen: boolean;
34
34
  onClose: () => void;
35
35
  onSave: (updatedContent: unknown) => void;
36
- EditorComponent: LazyExoticComponent<ComponentType<BlockEditorProps<unknown>>>;
36
+ EditorComponent: LazyExoticComponent<ComponentType<BlockEditorProps<unknown>>> | ComponentType<BlockEditorProps<unknown>>;
37
37
  };
38
38
 
39
39
  export function BlockEditorModal({
@@ -121,7 +121,7 @@ type EditingBlock = ColumnBlock & { index: number };
121
121
  export default function ColumnEditor({ columnIndex, blocks, onBlocksChange, blockType }: ColumnEditorProps) {
122
122
  const [editingBlock, setEditingBlock] = useState<EditingBlock | null>(null);
123
123
  const [isBlockSelectorOpen, setIsBlockSelectorOpen] = useState(false);
124
- const [LazyEditor, setLazyEditor] = useState<React.LazyExoticComponent<React.ComponentType<any>> | null>(null);
124
+ const [LazyEditor, setLazyEditor] = useState<React.LazyExoticComponent<React.ComponentType<any>> | React.ComponentType<any> | null>(null);
125
125
  const [isConfirmOpen, setIsConfirmOpen] = useState(false);
126
126
  const [blockToDeleteIndex, setBlockToDeleteIndex] = useState<number | null>(null);
127
127
 
@@ -169,8 +169,18 @@ export default function ColumnEditor({ columnIndex, blocks, onBlocksChange, bloc
169
169
 
170
170
  const handleStartEdit = (block: ColumnBlock, index: number) => {
171
171
  const blockDef = getBlockDefinition(block.block_type);
172
- if (blockDef && blockDef.editorComponentFilename) {
173
- const Editor = lazy(() => import(`../editors/${blockDef.editorComponentFilename.replace(/\.tsx$/, '')}`));
172
+ if (!blockDef) {
173
+ console.error(`No definition found for block type: ${block.block_type}`);
174
+ return;
175
+ }
176
+
177
+ if (blockDef.EditorComponent) {
178
+ const Component = blockDef.EditorComponent;
179
+ setLazyEditor(() => Component);
180
+ setEditingBlock({ ...block, index });
181
+ } else if (blockDef.editorComponentFilename) {
182
+ const filename = blockDef.editorComponentFilename;
183
+ const Editor = lazy(() => import(`../editors/${filename.replace(/\.tsx$/, '')}`));
174
184
  setLazyEditor(Editor);
175
185
  setEditingBlock({ ...block, index });
176
186
  } else {
@@ -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<any>> | null>(null);
37
+ const [LazyEditor, setLazyEditor] = useState<LazyExoticComponent<ComponentType<any>> | ComponentType<any> | null>(null);
38
38
 
39
39
  const SectionEditor = useMemo(() => {
40
40
  if (block?.block_type === 'section' || block?.block_type === 'hero') {
@@ -58,14 +58,21 @@ export default function EditableBlock({
58
58
  if (block.block_type === 'section' || block.block_type === 'hero') {
59
59
  setIsConfigPanelOpen(prev => !prev);
60
60
  } else {
61
- const editorFilename = blockRegistry[block.block_type as BlockType]?.editorComponentFilename;
61
+ const blockDef = getBlockDefinition(block.block_type as BlockType);
62
+
62
63
  if (block.block_type === 'posts_grid') {
63
64
  const LazifiedPostsGridEditor = lazy(() => Promise.resolve({ default: PostsGridBlockEditor }));
64
65
  setLazyEditor(LazifiedPostsGridEditor);
65
66
  setEditingBlock(block);
66
67
  }
67
- else if (editorFilename) {
68
- const Editor = lazy(() => import(`../editors/${editorFilename}`));
68
+ else if (blockDef?.EditorComponent) {
69
+ const Component = blockDef.EditorComponent;
70
+ setLazyEditor(() => Component);
71
+ setEditingBlock(block);
72
+ }
73
+ else if (blockDef?.editorComponentFilename) {
74
+ const filename = blockDef.editorComponentFilename;
75
+ const Editor = lazy(() => import(`../editors/${filename.replace(/\.tsx$/, '')}`));
69
76
  setLazyEditor(Editor);
70
77
  setEditingBlock(block);
71
78
  }
@@ -1,4 +1,4 @@
1
- // components/BlockRenderer.tsx
1
+ // components/BlockRenderer.tsx
2
2
  import React from "react";
3
3
  import dynamic from "next/dynamic";
4
4
  import type { Database } from "@nextblock-cms/db";
@@ -46,6 +46,19 @@ const DynamicBlockRenderer: React.FC<DynamicBlockRendererProps> = ({
46
46
  return <ClientTextBlockRenderer content={block.content as any} languageId={languageId} />;
47
47
  }
48
48
 
49
+ // Check if the block definition provides a direct component (e.g., from SDK plugins)
50
+ if (blockDefinition.RendererComponent) {
51
+ const RendererComponent = blockDefinition.RendererComponent;
52
+ return (
53
+ <RendererComponent
54
+ content={block.content}
55
+ languageId={languageId}
56
+ isInEditor={false} // Assuming public view
57
+ className="my-4"
58
+ />
59
+ );
60
+ }
61
+
49
62
  // Create dynamic component with proper SSR handling for other blocks
50
63
  const RendererComponent = dynamic(
51
64
  () => import(`./blocks/renderers/${blockDefinition.rendererComponentFilename}`),
@@ -0,0 +1,126 @@
1
+ import React from 'react';
2
+ import { z } from 'zod';
3
+ import { BlockConfig, BlockProps, BlockEditorProps } from '@nextblock-cms/sdk';
4
+ import { Card, CardContent, Avatar, AvatarImage, AvatarFallback, Input, Label, Textarea } from '@nextblock-cms/ui';
5
+ import { MessageSquareQuote } from 'lucide-react';
6
+
7
+ // 1. Define the Schema
8
+ export const TestimonialSchema = z.object({
9
+ quote: z.string().min(1).describe('The testimonial text'),
10
+ author_name: z.string().min(1).describe('The person who gave the testimonial'),
11
+ author_title: z.string().optional().describe('Job title or company'),
12
+ image_url: z.string().url().optional().or(z.literal('')).describe('Author profile image URL'),
13
+ });
14
+
15
+ // 2. Derive the Content Type
16
+ export type TestimonialBlockContent = z.infer<typeof TestimonialSchema>;
17
+
18
+ // 3. Create the Renderer Component
19
+ const TestimonialBlockRenderer: React.FC<BlockProps<typeof TestimonialSchema>> = ({ content }) => {
20
+ return (
21
+ <div className="container m-8">
22
+ <Card className="h-full">
23
+ <CardContent className="pt-6 flex flex-col gap-4 h-full">
24
+ <MessageSquareQuote className="w-8 h-8 text-primary/40" />
25
+
26
+ <blockquote className="flex-grow text-lg italic text-muted-foreground">
27
+ "{content.quote}"
28
+ </blockquote>
29
+
30
+ <div className="flex items-center gap-3 mt-4">
31
+ <Avatar>
32
+ {content.image_url && <AvatarImage src={content.image_url} alt={content.author_name} />}
33
+ <AvatarFallback>{content.author_name.slice(0, 2).toUpperCase()}</AvatarFallback>
34
+ </Avatar>
35
+
36
+ <div>
37
+ <div className="font-semibold">{content.author_name}</div>
38
+ {content.author_title && (
39
+ <div className="text-sm text-muted-foreground">{content.author_title}</div>
40
+ )}
41
+ </div>
42
+ </div>
43
+ </CardContent>
44
+ </Card>
45
+ </div>
46
+ );
47
+ };
48
+
49
+ // 4. Create the Editor Component
50
+ const TestimonialBlockEditor: React.FC<BlockEditorProps<typeof TestimonialSchema>> = ({ content, onChange }) => {
51
+ const handleChange = (key: keyof TestimonialBlockContent, value: string) => {
52
+ onChange({
53
+ ...content,
54
+ [key]: value,
55
+ });
56
+ };
57
+
58
+ return (
59
+ <div className="space-y-4 p-1">
60
+ <div className="space-y-2">
61
+ <Label htmlFor="quote">Quote</Label>
62
+ <Textarea
63
+ id="quote"
64
+ value={content.quote}
65
+ onChange={(e) => handleChange('quote', e.target.value)}
66
+ placeholder="Enter the testimonial quote..."
67
+ rows={4}
68
+ />
69
+ </div>
70
+
71
+ <div className="space-y-2">
72
+ <Label htmlFor="author_name">Author Name</Label>
73
+ <Input
74
+ id="author_name"
75
+ value={content.author_name}
76
+ onChange={(e) => handleChange('author_name', e.target.value)}
77
+ placeholder="John Doe"
78
+ />
79
+ </div>
80
+
81
+ <div className="space-y-2">
82
+ <Label htmlFor="author_title">Author Title (Optional)</Label>
83
+ <Input
84
+ id="author_title"
85
+ value={content.author_title || ''}
86
+ onChange={(e) => handleChange('author_title', e.target.value)}
87
+ placeholder="CEO, Company Inc."
88
+ />
89
+ </div>
90
+
91
+ <div className="space-y-2">
92
+ <Label htmlFor="image_url">Author Image URL (Optional)</Label>
93
+ <div className="flex gap-2">
94
+ <Input
95
+ id="image_url"
96
+ value={content.image_url || ''}
97
+ onChange={(e) => handleChange('image_url', e.target.value)}
98
+ placeholder="https://example.com/image.jpg"
99
+ />
100
+ </div>
101
+ {content.image_url && (
102
+ <div className="mt-2 w-16 h-16 relative rounded-full overflow-hidden border">
103
+ {/* eslint-disable-next-line @next/next/no-img-element */}
104
+ <img src={content.image_url} alt="Preview" className="w-full h-full object-cover" />
105
+ </div>
106
+ )}
107
+ </div>
108
+ </div>
109
+ );
110
+ };
111
+
112
+ // 5. Export the Block Configuration
113
+ export const TestimonialBlockConfig: BlockConfig<typeof TestimonialSchema> = {
114
+ type: 'testimonial',
115
+ label: 'Testimonial',
116
+ icon: MessageSquareQuote,
117
+ schema: TestimonialSchema,
118
+ initialContent: {
119
+ quote: "This product changed my life! The workflow is so much smoother now.",
120
+ author_name: "Jane Doe",
121
+ author_title: "CEO, TechCorp",
122
+ image_url: "",
123
+ },
124
+ RendererComponent: TestimonialBlockRenderer,
125
+ EditorComponent: TestimonialBlockEditor,
126
+ };
@@ -0,0 +1,149 @@
1
+ # How to Create a Custom Block with the NextBlock SDK
2
+
3
+ Custom blocks allow you to extend the functionality of NextBlock CMS while ensuring type safety and seamless integration. The `@nextblock-cms/sdk` package provides the necessary tools and interfaces.
4
+
5
+ ## Step 1: Define the Schema (Zod)
6
+
7
+ The schema defines the structure of your block's content. It is used for validation and to generate TypeScript types.
8
+
9
+ ```typescript
10
+ import { z } from 'zod';
11
+
12
+ export const MyBlockSchema = z.object({
13
+ title: z.string().min(1).describe('The main title'),
14
+ description: z.string().optional().describe('Optional description'),
15
+ isActive: z.boolean().default(true),
16
+ });
17
+
18
+ // Derive the content type from the schema
19
+ export type MyBlockContent = z.infer<typeof MyBlockSchema>;
20
+ ```
21
+
22
+ ## Step 2: Define the Components (React/TS)
23
+
24
+ Create the components that will render your block. Use the `BlockProps` interface to ensure your component receives the correct data.
25
+
26
+ ```typescript
27
+ import React from 'react';
28
+ import { BlockProps } from '@nextblock-cms/sdk';
29
+
30
+ // The Renderer Component (Public View)
31
+ const MyBlockRenderer: React.FC<BlockProps<typeof MyBlockSchema>> = ({ content }) => {
32
+ return (
33
+ <div className="my-block">
34
+ <h2>{content.title}</h2>
35
+ {content.description && <p>{content.description}</p>}
36
+ </div>
37
+ );
38
+ };
39
+
40
+ // The Editor Component (CMS View)
41
+ // Currently, the CMS uses auto-generated forms based on the schema,
42
+ // but you can provide a custom preview or editor here.
43
+ const MyBlockEditor: React.FC<BlockProps<typeof MyBlockSchema>> = ({ content }) => {
44
+ return (
45
+ <div className="p-4 border">
46
+ Preview: {content.title}
47
+ </div>
48
+ );
49
+ };
50
+ ```
51
+
52
+ ## Step 3: Create the Configuration
53
+
54
+ Combine everything into a `BlockConfig` object. This object tells the CMS how to handle your block.
55
+
56
+ ```typescript
57
+ import { BlockConfig } from '@nextblock-cms/sdk';
58
+ import { Star } from 'lucide-react'; // Choose an icon
59
+
60
+ export const MyBlockConfig: BlockConfig<typeof MyBlockSchema> = {
61
+ type: 'my_block', // Unique identifier
62
+ label: 'My Custom Block',
63
+ icon: Star,
64
+ schema: MyBlockSchema,
65
+ initialContent: {
66
+ title: 'Default Title',
67
+ isActive: true,
68
+ },
69
+ RendererComponent: MyBlockRenderer,
70
+ EditorComponent: MyBlockEditor,
71
+ };
72
+ ```
73
+
74
+ ## Step 4: Building the Editor Component
75
+
76
+ The Editor Component is responsible for the editing experience within the CMS. It receives `BlockEditorProps`, which includes the current content and an `onChange` handler.
77
+
78
+ You should use the shared UI components from `@nextblock-cms/ui` (like `Input`, `Textarea`, `Button`, `Label`) to ensure a consistent look and feel with the rest of the Admin UI.
79
+
80
+ ```typescript
81
+ import React from 'react';
82
+ import { BlockEditorProps } from '@nextblock-cms/sdk';
83
+ import { Input, Label, Textarea } from '@nextblock-cms/ui';
84
+
85
+ const MyBlockEditor: React.FC<BlockEditorProps<typeof MyBlockSchema>> = ({ content, onChange }) => {
86
+
87
+ const handleChange = (key: keyof typeof content, value: any) => {
88
+ onChange({
89
+ ...content,
90
+ [key]: value,
91
+ });
92
+ };
93
+
94
+ return (
95
+ <div className="space-y-4">
96
+ <div className="space-y-2">
97
+ <Label htmlFor="title">Title</Label>
98
+ <Input
99
+ id="title"
100
+ value={content.title}
101
+ onChange={(e) => handleChange('title', e.target.value)}
102
+ />
103
+ </div>
104
+
105
+ <div className="space-y-2">
106
+ <Label htmlFor="description">Description</Label>
107
+ <Textarea
108
+ id="description"
109
+ value={content.description || ''}
110
+ onChange={(e) => handleChange('description', e.target.value)}
111
+ />
112
+ </div>
113
+ </div>
114
+ );
115
+ };
116
+ ```
117
+
118
+ ## Step 5: Register the Block
119
+
120
+ To make your block available in the CMS, import it into the main registry file: `apps/nextblock/lib/blocks/blockRegistry.ts`.
121
+
122
+ ```typescript
123
+ // apps/nextblock/lib/blocks/blockRegistry.ts
124
+ import { MyBlockConfig } from '../../components/blocks/MyBlock';
125
+
126
+ export const blockRegistry: Record<BlockType, BlockDefinition> = {
127
+ // ... existing blocks
128
+ [MyBlockConfig.type]: {
129
+ ...MyBlockConfig,
130
+ // ... any additional internal config if needed
131
+ } as BlockDefinition<MyBlockContent>,
132
+ };
133
+ ```
134
+
135
+ Once registered, your block will appear in the CMS block picker and render on the frontend.
136
+
137
+ ## Advanced Topic: Schema Evolution and Versioning
138
+
139
+ Modifying a Zod schema after a block is in production can be dangerous. If you change the shape of the data (e.g., rename a field, remove a required field), existing blocks in the database may fail to validate or render, causing runtime errors.
140
+
141
+ ### Recommended Strategy: Versioning
142
+
143
+ Instead of modifying the existing schema, create a new version of the block.
144
+
145
+ 1. **Duplicate the Block**: Create a new block type, e.g., `my_block_v2`, with the updated schema and renderer.
146
+ 2. **Register the New Block**: Add it to the registry. You can hide the old `my_block` from the picker if you want to prevent new usage, but keep the definition so existing blocks continue to work.
147
+ 3. **Migration (Optional)**: You can write a utility to transform `my_block` data to `my_block_v2` format if you want to migrate content programmatically.
148
+
149
+ While V1 of the SDK does not enforce a strict versioning system, treating your block schemas as immutable contracts is a best practice for stability.