create-nextblock 0.2.31 → 0.2.33

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 (21) hide show
  1. package/package.json +1 -1
  2. package/scripts/sync-template.js +70 -52
  3. package/templates/nextblock-template/app/cms/blocks/actions.ts +15 -15
  4. package/templates/nextblock-template/app/cms/blocks/components/BackgroundSelector.tsx +14 -12
  5. package/templates/nextblock-template/app/cms/blocks/components/BlockEditorArea.tsx +24 -21
  6. package/templates/nextblock-template/app/cms/blocks/components/BlockEditorModal.tsx +1 -1
  7. package/templates/nextblock-template/app/cms/blocks/components/ColumnEditor.tsx +42 -24
  8. package/templates/nextblock-template/app/cms/blocks/components/EditableBlock.tsx +16 -16
  9. package/templates/nextblock-template/app/cms/blocks/editors/SectionBlockEditor.tsx +56 -35
  10. package/templates/nextblock-template/app/cms/blocks/editors/TextBlockEditor.tsx +1 -1
  11. package/templates/nextblock-template/app/cms/media/actions.ts +47 -47
  12. package/templates/nextblock-template/app/cms/media/components/MediaGridClient.tsx +3 -3
  13. package/templates/nextblock-template/app/cms/media/components/MediaUploadForm.tsx +1 -1
  14. package/templates/nextblock-template/app/cms/media/page.tsx +3 -3
  15. package/templates/nextblock-template/app/cms/revisions/JsonDiffView.tsx +8 -7
  16. package/templates/nextblock-template/app/cms/revisions/RevisionHistoryButton.tsx +16 -10
  17. package/templates/nextblock-template/app/cms/revisions/service.ts +9 -9
  18. package/templates/nextblock-template/app/cms/settings/extra-translations/actions.ts +1 -1
  19. package/templates/nextblock-template/app/cms/settings/logos/[id]/edit/page.tsx +1 -0
  20. package/templates/nextblock-template/lib/blocks/blockRegistry.ts +29 -29
  21. package/templates/nextblock-template/package.json +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-nextblock",
3
- "version": "0.2.31",
3
+ "version": "0.2.33",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -36,16 +36,17 @@ const UI_PROXY_MODULES = [
36
36
  'ui',
37
37
  ];
38
38
 
39
- const IGNORED_SEGMENTS = new Set([
40
- 'node_modules',
41
- '.git',
42
- '.next',
43
- 'dist',
44
- 'tmp',
45
- 'coverage',
46
- 'backup',
47
- 'backups',
48
- ]);
39
+ const IGNORED_SEGMENTS = new Set([
40
+ 'node_modules',
41
+ '.git',
42
+ '.next',
43
+ 'dist',
44
+ 'tmp',
45
+ 'coverage',
46
+ 'backup',
47
+ 'backups',
48
+ ]);
49
+
49
50
 
50
51
  async function ensureTemplateSync() {
51
52
  const sourceExists = await fs.pathExists(SOURCE_DIR);
@@ -64,7 +65,7 @@ async function ensureTemplateSync() {
64
65
  );
65
66
 
66
67
  await fs.ensureDir(TARGET_DIR);
67
- await fs.emptyDir(TARGET_DIR);
68
+ await emptyDirWithRetry(TARGET_DIR);
68
69
 
69
70
  await fs.copy(SOURCE_DIR, TARGET_DIR, {
70
71
  dereference: true,
@@ -79,10 +80,10 @@ async function ensureTemplateSync() {
79
80
  },
80
81
  });
81
82
 
82
- await ensureEnvExample();
83
- await ensureTemplateGitignore();
84
- await ensureGlobalStyles();
85
- await ensureClientTranslations();
83
+ await ensureEnvExample();
84
+ await ensureTemplateGitignore();
85
+ await ensureGlobalStyles();
86
+ await ensureClientTranslations();
86
87
  await sanitizeBlockEditorImports();
87
88
  await sanitizeUiImports();
88
89
  await ensureUiProxies();
@@ -92,7 +93,7 @@ async function ensureTemplateSync() {
92
93
  console.log(chalk.green('Template sync complete.'));
93
94
  }
94
95
 
95
- async function ensureEnvExample() {
96
+ async function ensureEnvExample() {
96
97
  const envTargets = [
97
98
  resolve(REPO_ROOT, '.env.example'),
98
99
  resolve(REPO_ROOT, '.env.exemple'),
@@ -118,42 +119,42 @@ NEXT_PUBLIC_URL=http://localhost:3000
118
119
  `;
119
120
 
120
121
  await fs.writeFile(destination, placeholder);
121
- }
122
-
123
- async function ensureTemplateGitignore() {
124
- const destination = resolve(TARGET_DIR, '.gitignore');
125
- const content = `.DS_Store
126
- node_modules
127
- dist
128
- .next
129
- out
130
- build
131
- coverage
132
- *.log
133
- logs
134
- npm-debug.log*
135
- yarn-debug.log*
136
- yarn-error.log*
137
- pnpm-debug.log*
138
-
139
- .env
140
- .env.*
141
- .env.local
142
- .env.development.local
143
- .env.production.local
144
- .env.test.local
145
-
146
- .vscode
147
- .idea
148
- .swp
149
- *.sw?
150
-
151
- supabase/.temp
152
- supabase/.branches
153
- `;
154
- await fs.outputFile(destination, content);
155
- }
156
-
122
+ }
123
+
124
+ async function ensureTemplateGitignore() {
125
+ const destination = resolve(TARGET_DIR, '.gitignore');
126
+ const content = `.DS_Store
127
+ node_modules
128
+ dist
129
+ .next
130
+ out
131
+ build
132
+ coverage
133
+ *.log
134
+ logs
135
+ npm-debug.log*
136
+ yarn-debug.log*
137
+ yarn-error.log*
138
+ pnpm-debug.log*
139
+
140
+ .env
141
+ .env.*
142
+ .env.local
143
+ .env.development.local
144
+ .env.production.local
145
+ .env.test.local
146
+
147
+ .vscode
148
+ .idea
149
+ .swp
150
+ *.sw?
151
+
152
+ supabase/.temp
153
+ supabase/.branches
154
+ `;
155
+ await fs.outputFile(destination, content);
156
+ }
157
+
157
158
  async function ensureGlobalStyles() {
158
159
  const destination = resolve(TARGET_DIR, 'app/globals.css');
159
160
 
@@ -312,3 +313,20 @@ ensureTemplateSync().catch((error) => {
312
313
  console.error(chalk.red(error instanceof Error ? error.message : String(error)));
313
314
  process.exit(1);
314
315
  });
316
+
317
+ async function emptyDirWithRetry(dir, retries = 5, delay = 1000) {
318
+ for (let i = 0; i < retries; i++) {
319
+ try {
320
+ await fs.emptyDir(dir);
321
+ return;
322
+ } catch (err) {
323
+ if (i === retries - 1) throw err;
324
+ if (err.code === 'EBUSY' || err.code === 'EPERM') {
325
+ console.log(chalk.yellow(`Locked file encountered. Retrying in ${delay}ms... (${i + 1}/${retries})`));
326
+ await new Promise((resolve) => setTimeout(resolve, delay));
327
+ } else {
328
+ throw err;
329
+ }
330
+ }
331
+ }
332
+ }
@@ -5,22 +5,22 @@ import { createClient } from "@nextblock-cms/db/server";
5
5
  import { revalidatePath } from "next/cache";
6
6
  import type { Database, Json } from "@nextblock-cms/db";
7
7
  import { getInitialContent, isValidBlockType } from "../../../lib/blocks/blockRegistry";
8
- import { getFullPageContent, getFullPostContent } from "../revisions/utils";
8
+ import { getFullPageContent, getFullPostContent, type FullPageContent, type FullPostContent } from "../revisions/utils";
9
9
  import { createPageRevision, createPostRevision } from "../revisions/service";
10
10
 
11
11
  type Block = Database['public']['Tables']['blocks']['Row'];
12
12
  type BlockType = Database['public']['Tables']['blocks']['Row']['block_type'];
13
13
 
14
14
  // Helper to verify user can edit the parent (page/post)
15
- async function canEditParent(
16
- supabase: ReturnType<typeof createClient>,
17
- userId: string,
18
- pageId?: number | null,
19
- postId?: number | null
20
- ): Promise<boolean> {
21
- void pageId;
22
- void postId;
23
- const { data: profile } = await supabase
15
+ async function canEditParent(
16
+ supabase: ReturnType<typeof createClient>,
17
+ userId: string,
18
+ pageId?: number | null,
19
+ postId?: number | null
20
+ ): Promise<boolean> {
21
+ void pageId;
22
+ void postId;
23
+ const { data: profile } = await supabase
24
24
  .from("profiles")
25
25
  .select("role")
26
26
  .eq("id", userId)
@@ -160,7 +160,7 @@ export async function updateBlock(blockId: number, newContent: unknown, pageId?:
160
160
  return { error: "Block not found." };
161
161
  }
162
162
 
163
- let prevContentAggregate: Awaited<ReturnType<typeof getFullPageContent>> | Awaited<ReturnType<typeof getFullPostContent>> | null = null;
163
+ let prevContentAggregate: FullPageContent | FullPostContent | null = null;
164
164
  if (existingBlock.page_id) {
165
165
  prevContentAggregate = await getFullPageContent(existingBlock.page_id);
166
166
  } else if (existingBlock.post_id) {
@@ -189,7 +189,7 @@ export async function updateBlock(blockId: number, newContent: unknown, pageId?:
189
189
  } else if (existingBlock.post_id) {
190
190
  const nextContentAggregate = await getFullPostContent(existingBlock.post_id, { overrideBlockId: blockId, overrideBlockContent: newContent });
191
191
  if (nextContentAggregate) {
192
- await createPostRevision(existingBlock.post_id, user.id, prevContentAggregate as any, nextContentAggregate as any);
192
+ await createPostRevision(existingBlock.post_id, user.id, prevContentAggregate as FullPostContent, nextContentAggregate as FullPostContent);
193
193
  }
194
194
  }
195
195
  }
@@ -250,7 +250,7 @@ export async function deleteBlock(blockId: number, pageId?: number | null, postI
250
250
  return { error: "Block not found." };
251
251
  }
252
252
 
253
- let previousAggregate: Awaited<ReturnType<typeof getFullPageContent>> | Awaited<ReturnType<typeof getFullPostContent>> | null = null;
253
+ let previousAggregate: FullPageContent | FullPostContent | null = null;
254
254
  if (existingBlock.page_id) {
255
255
  previousAggregate = await getFullPageContent(existingBlock.page_id);
256
256
  } else if (existingBlock.post_id) {
@@ -274,7 +274,7 @@ export async function deleteBlock(blockId: number, pageId?: number | null, postI
274
274
  } else if (existingBlock.post_id) {
275
275
  const nextAggregate = await getFullPostContent(existingBlock.post_id, { excludeDeletedBlockId: blockId });
276
276
  if (nextAggregate) {
277
- await createPostRevision(existingBlock.post_id, user.id, previousAggregate as any, nextAggregate as any);
277
+ await createPostRevision(existingBlock.post_id, user.id, previousAggregate as FullPostContent, nextAggregate as FullPostContent);
278
278
  }
279
279
  }
280
280
  }
@@ -418,7 +418,7 @@ export async function copyBlocksFromLanguage(
418
418
  console.warn("Could not fetch target post slug for revalidation:", postError);
419
419
  } else {
420
420
  targetSlug = postData.slug;
421
- if (targetSlug) revalidatePath(`/article/${targetSlug}`);
421
+ if (targetSlug) revalidatePath(`/article/${targetSlug}`);
422
422
  }
423
423
  revalidatePath(`/cms/posts/${parentId}/edit`); // Revalidate edit page
424
424
  }
@@ -21,6 +21,8 @@ interface BackgroundSelectorProps {
21
21
  onChange: (newBackground: SectionBlockContent["background"]) => void;
22
22
  }
23
23
 
24
+ type ChangeEventLike = { target: { name: string; value: string } };
25
+
24
26
  export default function BackgroundSelector({ background, onChange }: BackgroundSelectorProps) {
25
27
 
26
28
  const backgroundType = background?.type || "none";
@@ -127,12 +129,12 @@ export default function BackgroundSelector({ background, onChange }: BackgroundS
127
129
  }
128
130
  };
129
131
 
130
- const handleBackgroundPropertyChange = (e: React.ChangeEvent<HTMLInputElement>) => {
132
+ const handleBackgroundPropertyChange = (e: ChangeEventLike) => {
131
133
  const { name, value } = e.target;
132
134
  onChange({ ...background, [name]: value });
133
135
  };
134
136
 
135
- const handleOverlayGradientChange = (e: React.ChangeEvent<HTMLInputElement>) => {
137
+ const handleOverlayGradientChange = (e: ChangeEventLike) => {
136
138
  const { name, value } = e.target;
137
139
  if (background?.type === "image" && background.image) {
138
140
  const { image } = background;
@@ -170,7 +172,7 @@ export default function BackgroundSelector({ background, onChange }: BackgroundS
170
172
  <div className="space-y-4">
171
173
  <div className="grid gap-2">
172
174
  <Label>Background Type</Label>
173
- <Select value={backgroundType} onValueChange={(v) => handleTypeChange(v as any)}>
175
+ <Select value={backgroundType} onValueChange={(v) => handleTypeChange(v as SectionBlockContent["background"]["type"])}>
174
176
  <SelectTrigger className="w-full max-w-[250px]"><SelectValue placeholder="Select type" /></SelectTrigger>
175
177
  <SelectContent>
176
178
  <SelectItem value="none">None</SelectItem>
@@ -184,7 +186,7 @@ export default function BackgroundSelector({ background, onChange }: BackgroundS
184
186
  <Label htmlFor="min_height">Minimum Height (e.g., 250px)</Label>
185
187
  <div className="flex items-center gap-2">
186
188
  <Input id="min_height" name="min_height" value={minHeight} onChange={(e) => setMinHeight(e.target.value)} placeholder="e.g., 250px" className="max-w-[200px]" />
187
- <Button type="button" variant="ghost" size="icon" onClick={() => handleBackgroundPropertyChange({ target: { name: "min_height", value: minHeight } } as any)} disabled={!hasMinHeightChanged} title="Save Minimum Height">
189
+ <Button type="button" variant="ghost" size="icon" onClick={() => handleBackgroundPropertyChange({ target: { name: "min_height", value: minHeight } })} disabled={!hasMinHeightChanged} title="Save Minimum Height">
188
190
  <Save className={cn("h-5 w-5", hasMinHeightChanged && "text-green-600")} />
189
191
  </Button>
190
192
  </div>
@@ -272,7 +274,7 @@ export default function BackgroundSelector({ background, onChange }: BackgroundS
272
274
  ]}
273
275
  />
274
276
  </div>
275
- <Button size="icon" variant="ghost" onClick={() => handleOverlayGradientChange({ target: { name: "direction", value: overlayDirection } } as any)} disabled={!hasOverlayDirectionChanged} title="Save Overlay Direction">
277
+ <Button size="icon" variant="ghost" onClick={() => handleOverlayGradientChange({ target: { name: "direction", value: overlayDirection } })} disabled={!hasOverlayDirectionChanged} title="Save Overlay Direction">
276
278
  <Save className={cn("h-5 w-5 mt-[1.3rem]", hasOverlayDirectionChanged && "text-green-600")} />
277
279
  </Button>
278
280
  </div>
@@ -280,12 +282,12 @@ export default function BackgroundSelector({ background, onChange }: BackgroundS
280
282
  <ColorPicker
281
283
  label="Start Color"
282
284
  color={selectedImage.overlay.gradient?.stops?.[0]?.color || "rgba(0,0,0,0.5)"}
283
- onChange={(color) => handleOverlayGradientChange({ target: { name: "startColor", value: color } } as any)}
285
+ onChange={(color) => handleOverlayGradientChange({ target: { name: "startColor", value: color } })}
284
286
  />
285
287
  <ColorPicker
286
288
  label="End Color"
287
289
  color={selectedImage.overlay.gradient?.stops?.[1]?.color || "rgba(0,0,0,0)"}
288
- onChange={(color) => handleOverlayGradientChange({ target: { name: "endColor", value: color } } as any)}
290
+ onChange={(color) => handleOverlayGradientChange({ target: { name: "endColor", value: color } })}
289
291
  />
290
292
  </div>
291
293
  </div>
@@ -300,7 +302,7 @@ export default function BackgroundSelector({ background, onChange }: BackgroundS
300
302
  label="Direction"
301
303
  tooltipContent="Select a preset or enter a custom angle like '45deg' or 'to top left'. See MDN's linear-gradient docs for more options."
302
304
  value={background.gradient?.direction || "to right"}
303
- onChange={(value: string) => handleBackgroundGradientChange({ target: { name: "direction", value } } as any)}
305
+ onChange={(value: string) => handleBackgroundGradientChange({ target: { name: "direction", value } })}
304
306
  options={[
305
307
  { value: "to right", label: "To Right" },
306
308
  { value: "to left", label: "To Left" },
@@ -315,12 +317,12 @@ export default function BackgroundSelector({ background, onChange }: BackgroundS
315
317
  <ColorPicker
316
318
  label="Start Color"
317
319
  color={background.gradient?.stops?.[0]?.color || "#3b82f6"}
318
- onChange={(color) => handleBackgroundGradientChange({ target: { name: "startColor", value: color } } as any)}
320
+ onChange={(color) => handleBackgroundGradientChange({ target: { name: "startColor", value: color } })}
319
321
  />
320
322
  <ColorPicker
321
323
  label="End Color"
322
324
  color={background.gradient?.stops?.[1]?.color || "#8b5cf6"}
323
- onChange={(color) => handleBackgroundGradientChange({ target: { name: "endColor", value: color } } as any)}
325
+ onChange={(color) => handleBackgroundGradientChange({ target: { name: "endColor", value: color } })}
324
326
  />
325
327
  </div>
326
328
  </div>
@@ -328,8 +330,8 @@ export default function BackgroundSelector({ background, onChange }: BackgroundS
328
330
  </div>
329
331
  </TooltipProvider>
330
332
  );
331
- const handleBackgroundGradientChange = (e: React.ChangeEvent<HTMLInputElement>) => {
332
- const { name, value } = e.target as any;
333
+ const handleBackgroundGradientChange = (e: ChangeEventLike) => {
334
+ const { name, value } = e.target;
333
335
  if (backgroundType !== 'gradient') return;
334
336
  const current = background.gradient || { type: 'linear' as const, direction: 'to right', stops: [ { color: '#3b82f6', position: 0 }, { color: '#8b5cf6', position: 100 } ] };
335
337
  if (name === 'direction') {
@@ -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<any> | null>(null);
85
+ const [NestedBlockEditorComponent, setNestedBlockEditorComponent] = useState<ComponentType<Record<string, unknown>> | null>(null);
86
86
  const [tempNestedBlockContent, setTempNestedBlockContent] = useState<Json | null>(null);
87
87
 
88
88
  useEffect(() => {
@@ -129,41 +129,41 @@ export default function BlockEditorArea({ parentId, parentType, initialBlocks, l
129
129
  debouncedSave(updatedBlock);
130
130
  };
131
131
 
132
- const DynamicTextBlockEditor = dynamic(() => import('../editors/TextBlockEditor').then(mod => mod.default), { loading: () => <p>Loading editor...</p> });
133
- const DynamicHeadingBlockEditor = dynamic(() => import('../editors/HeadingBlockEditor').then(mod => mod.default), { loading: () => <p>Loading editor...</p> });
134
- const DynamicImageBlockEditor = dynamic(() => import('../editors/ImageBlockEditor').then(mod => mod.default), { loading: () => <p>Loading editor...</p> });
135
- const DynamicButtonBlockEditor = dynamic(() => import('../editors/ButtonBlockEditor').then(mod => mod.default), { loading: () => <p>Loading editor...</p> });
136
- const DynamicPostsGridBlockEditor = dynamic(() => import('../editors/PostsGridBlockEditor').then(mod => mod.default), { loading: () => <p>Loading editor...</p> });
137
- const DynamicVideoEmbedBlockEditor = dynamic(() => import('../editors/VideoEmbedBlockEditor').then(mod => mod.default), { loading: () => <p>Loading editor...</p> });
138
- const DynamicSectionBlockEditor = dynamic(() => import('../editors/SectionBlockEditor').then(mod => mod.default), { loading: () => <p>Loading editor...</p> });
132
+ const DynamicTextBlockEditor = dynamic(() => import('../editors/TextBlockEditor').then(mod => mod.default), { loading: () => <p>Loading editor...</p> });
133
+ const DynamicHeadingBlockEditor = dynamic(() => import('../editors/HeadingBlockEditor').then(mod => mod.default), { loading: () => <p>Loading editor...</p> });
134
+ const DynamicImageBlockEditor = dynamic(() => import('../editors/ImageBlockEditor').then(mod => mod.default), { loading: () => <p>Loading editor...</p> });
135
+ const DynamicButtonBlockEditor = dynamic(() => import('../editors/ButtonBlockEditor').then(mod => mod.default), { loading: () => <p>Loading editor...</p> });
136
+ const DynamicPostsGridBlockEditor = dynamic(() => import('../editors/PostsGridBlockEditor').then(mod => mod.default), { loading: () => <p>Loading editor...</p> });
137
+ const DynamicVideoEmbedBlockEditor = dynamic(() => import('../editors/VideoEmbedBlockEditor').then(mod => mod.default), { loading: () => <p>Loading editor...</p> });
138
+ const DynamicSectionBlockEditor = dynamic(() => import('../editors/SectionBlockEditor').then(mod => mod.default), { loading: () => <p>Loading editor...</p> });
139
139
 
140
140
  useEffect(() => {
141
141
  if (editingNestedBlockInfo) {
142
142
  const blockType = editingNestedBlockInfo.blockData.block_type;
143
- let SelectedEditor: React.ComponentType<any> | null = null;
143
+ let SelectedEditor: React.ComponentType<Record<string, unknown>> | null = null;
144
144
 
145
145
  try {
146
146
  switch (blockType) {
147
147
  case 'text':
148
- SelectedEditor = DynamicTextBlockEditor;
148
+ SelectedEditor = DynamicTextBlockEditor as unknown as ComponentType<Record<string, unknown>>;
149
149
  break;
150
150
  case 'heading':
151
- SelectedEditor = DynamicHeadingBlockEditor;
151
+ SelectedEditor = DynamicHeadingBlockEditor as unknown as ComponentType<Record<string, unknown>>;
152
152
  break;
153
153
  case 'image':
154
- SelectedEditor = DynamicImageBlockEditor;
154
+ SelectedEditor = DynamicImageBlockEditor as unknown as ComponentType<Record<string, unknown>>;
155
155
  break;
156
156
  case 'button':
157
- SelectedEditor = DynamicButtonBlockEditor;
157
+ SelectedEditor = DynamicButtonBlockEditor as unknown as ComponentType<Record<string, unknown>>;
158
158
  break;
159
159
  case 'posts_grid':
160
- SelectedEditor = DynamicPostsGridBlockEditor;
160
+ SelectedEditor = DynamicPostsGridBlockEditor as unknown as ComponentType<Record<string, unknown>>;
161
161
  break;
162
162
  case 'video_embed':
163
- SelectedEditor = DynamicVideoEmbedBlockEditor;
163
+ SelectedEditor = DynamicVideoEmbedBlockEditor as unknown as ComponentType<Record<string, unknown>>;
164
164
  break;
165
165
  case 'section':
166
- SelectedEditor = DynamicSectionBlockEditor;
166
+ SelectedEditor = DynamicSectionBlockEditor as unknown as ComponentType<Record<string, unknown>>;
167
167
  break;
168
168
  default:
169
169
  console.warn(`No dynamic editor configured for nested block type: ${blockType}`);
@@ -389,7 +389,10 @@ export default function BlockEditorArea({ parentId, parentType, initialBlocks, l
389
389
  parentBlockId: parentBlockIdStr,
390
390
  columnIndex,
391
391
  blockIndexInColumn,
392
- blockData: nestedBlockData,
392
+ blockData: {
393
+ ...nestedBlockData,
394
+ content: nestedBlockData.content as unknown as Json
395
+ },
393
396
  });
394
397
  } else {
395
398
  console.error("Nested block not found at specified indices:", { parentBlockIdStr, columnIndex, blockIndexInColumn });
@@ -444,7 +447,7 @@ export default function BlockEditorArea({ parentId, parentType, initialBlocks, l
444
447
  onContentChange={handleContentChange}
445
448
  onDelete={async (blockIdToDelete) => {
446
449
  startTransition(async () => {
447
- const result = await import("../actions").then(({ deleteBlock }) =>
450
+ const result = await import("../actions").then(({ deleteBlock }) =>
448
451
  deleteBlock(
449
452
  blockIdToDelete,
450
453
  parentType === "page" ? parentId : null,
@@ -472,8 +475,8 @@ export default function BlockEditorArea({ parentId, parentType, initialBlocks, l
472
475
  <EditableBlock
473
476
  block={activeBlock}
474
477
  className="h-full"
475
- onDelete={() => {}} // eslint-disable-line @typescript-eslint/no-empty-function
476
- onContentChange={() => {}} // eslint-disable-line @typescript-eslint/no-empty-function
478
+ onDelete={() => {}}
479
+ onContentChange={() => {}}
477
480
  />
478
481
  </div>
479
482
  ) : null}
@@ -522,7 +525,7 @@ export default function BlockEditorArea({ parentId, parentType, initialBlocks, l
522
525
  if (blockType === "posts_grid") {
523
526
  const fullBlockForEditor: Block = {
524
527
  block_type: editingNestedBlockInfo.blockData.block_type,
525
- content: tempNestedBlockContent,
528
+ content: tempNestedBlockContent as Json,
526
529
  id: 0, // Temporary ID for nested blocks
527
530
  language_id: languageId,
528
531
  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<BlockEditorProps<unknown>>>;
36
+ EditorComponent: LazyExoticComponent<ComponentType<any>>;
37
37
  };
38
38
 
39
39
  export function BlockEditorModal({
@@ -28,6 +28,19 @@ 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
+
31
44
  function SortableColumnBlock({ block, index, columnIndex, onEdit, onDelete, blockType, onClick }: SortableColumnBlockProps) {
32
45
  const {
33
46
  attributes,
@@ -85,30 +98,35 @@ function SortableColumnBlock({ block, index, columnIndex, onEdit, onDelete, bloc
85
98
  </div>
86
99
  </div>
87
100
  <div className="text-xs text-gray-500 dark:text-gray-400">
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
- )}
101
+ {(() => {
102
+ const content = block.content as AnyBlockContent;
103
+ return (
104
+ <>
105
+ {block.block_type === 'text' && (
106
+ <div dangerouslySetInnerHTML={{ __html: (content.html_content || 'Empty text').substring(0, 50) + (content.html_content && content.html_content.length > 50 ? '...' : '') }} />
107
+ )}
108
+ {block.block_type === 'heading' && (
109
+ <div>H{content.level || 1}: {(content.text_content || 'Empty heading').substring(0, 30) + (content.text_content && content.text_content.length > 30 ? '...' : '')}</div>
110
+ )}
111
+ {block.block_type === 'image' && (
112
+ <div>Image: {content.alt_text || content.media_id ? 'Image selected' : 'No image selected'}</div>
113
+ )}
114
+ {block.block_type === 'button' && (
115
+ <div>Button: {content.text || 'No text'} → {content.url || '#'}</div>
116
+ )}
117
+ {block.block_type === 'video_embed' && (
118
+ <div>Video: {content.title || content.url || 'No URL set'}</div>
119
+ )}
120
+ {block.block_type === 'posts_grid' && (
121
+ <div>Posts Grid: {content.columns || 3} cols, {content.postsPerPage || 12} posts</div>
122
+ )}
123
+ </>
124
+ );
125
+ })()}
106
126
  </div>
107
127
  </div>
108
128
  );
109
129
  }
110
-
111
- // Column editor component
112
130
  export interface ColumnEditorProps {
113
131
  columnIndex: number;
114
132
  blocks: ColumnBlock[];
@@ -121,7 +139,7 @@ type EditingBlock = ColumnBlock & { index: number };
121
139
  export default function ColumnEditor({ columnIndex, blocks, onBlocksChange, blockType }: ColumnEditorProps) {
122
140
  const [editingBlock, setEditingBlock] = useState<EditingBlock | null>(null);
123
141
  const [isBlockSelectorOpen, setIsBlockSelectorOpen] = useState(false);
124
- const [LazyEditor, setLazyEditor] = useState<React.LazyExoticComponent<React.ComponentType<any>> | null>(null);
142
+ const [LazyEditor, setLazyEditor] = useState<React.LazyExoticComponent<React.ComponentType<Record<string, unknown>>> | null>(null);
125
143
  const [isConfirmOpen, setIsConfirmOpen] = useState(false);
126
144
  const [blockToDeleteIndex, setBlockToDeleteIndex] = useState<number | null>(null);
127
145
 
@@ -134,7 +152,7 @@ export default function ColumnEditor({ columnIndex, blocks, onBlocksChange, bloc
134
152
  const initialContent = getInitialContent(selectedBlockType);
135
153
  const newBlock: ColumnBlock = {
136
154
  block_type: selectedBlockType,
137
- content: initialContent || {},
155
+ content: (initialContent as Record<string, unknown>) || {},
138
156
  temp_id: `temp-${Date.now()}-${Math.random()}`
139
157
  };
140
158
  onBlocksChange([...blocks, newBlock]);
@@ -178,13 +196,13 @@ export default function ColumnEditor({ columnIndex, blocks, onBlocksChange, bloc
178
196
  }
179
197
  };
180
198
 
181
- const handleSave = (newContent: any) => {
199
+ const handleSave = (newContent: unknown) => {
182
200
  if (editingBlock === null) return;
183
201
 
184
202
  const updatedBlocks = [...blocks];
185
203
  updatedBlocks[editingBlock.index] = {
186
204
  ...updatedBlocks[editingBlock.index],
187
- content: newContent,
205
+ content: newContent as Record<string, unknown>,
188
206
  };
189
207
  onBlocksChange(updatedBlocks);
190
208
  setEditingBlock(null);
@@ -16,25 +16,25 @@ 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, any>) => void;
20
- dragHandleProps?: Record<string, any>;
19
+ onContentChange: (blockId: number, newContent: Record<string, unknown>) => void;
20
+ dragHandleProps?: Record<string, unknown>;
21
21
  onEditNestedBlock?: (parentBlockId: string, columnIndex: number, blockIndexInColumn: number) => void;
22
22
  className?: string;
23
23
  }
24
24
 
25
- export default function EditableBlock({
26
- block,
27
- onDelete,
28
- onContentChange,
29
- dragHandleProps,
30
- onEditNestedBlock,
31
- className,
32
- }: EditableBlockProps) {
33
- void onEditNestedBlock;
34
- // Move all hooks to the top before any conditional returns
25
+ export default function EditableBlock({
26
+ block,
27
+ onDelete,
28
+ onContentChange,
29
+ dragHandleProps,
30
+ onEditNestedBlock,
31
+ className,
32
+ }: EditableBlockProps) {
33
+ void onEditNestedBlock;
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<Record<string, unknown>>> | 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, any>) => onContentChange(block.id, newContent)} blockType={block.block_type as 'section' | 'hero'} isConfigPanelOpen={isConfigPanelOpen} />}
159
+ {SectionEditor && <SectionEditor block={block} content={block.content || {}} onChange={(newContent: Record<string, unknown>) => 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: any) => {
174
- onContentChange(block.id, newContent);
173
+ onSave={(newContent: unknown) => {
174
+ onContentChange(block.id, newContent as Record<string, unknown>);
175
175
  setEditingBlock(null);
176
176
  setLazyEditor(null);
177
177
  }}
@@ -38,6 +38,19 @@ 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
+
41
54
  export default function SectionBlockEditor({
42
55
  content,
43
56
  onChange,
@@ -67,8 +80,9 @@ export default function SectionBlockEditor({
67
80
  }, [content]);
68
81
 
69
82
 
83
+ type ColumnBlock = SectionBlockContent['column_blocks'][0][0];
70
84
  const [activeId, setActiveId] = useState<string | null>(null);
71
- const [draggedBlock, setDraggedBlock] = useState<any>(null);
85
+ const [draggedBlock, setDraggedBlock] = useState<ColumnBlock | null>(null);
72
86
 
73
87
  // DND sensors for cross-column dragging
74
88
  const sensors = useSensors(
@@ -292,40 +306,47 @@ return (
292
306
  </span>
293
307
  </div>
294
308
  <div className="text-xs text-blue-600 dark:text-blue-400 mt-1">
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
- )}
309
+ {(() => {
310
+ const content = draggedBlock.content as AnyBlockContent;
311
+ return (
312
+ <>
313
+ {draggedBlock.block_type === "text" && (
314
+ <div
315
+ dangerouslySetInnerHTML={{
316
+ __html:
317
+ (
318
+ content.html_content || "Empty text"
319
+ ).substring(0, 30) + "...",
320
+ }}
321
+ />
322
+ )}
323
+ {draggedBlock.block_type === "heading" && (
324
+ <div>
325
+ H{content.level || 1}:{" "}
326
+ {(
327
+ content.text_content || "Empty heading"
328
+ ).substring(0, 20) + "..."}
329
+ </div>
330
+ )}
331
+ {draggedBlock.block_type === "image" && (
332
+ <div>
333
+ Image: {content.alt_text || "No alt text"}
334
+ </div>
335
+ )}
336
+ {draggedBlock.block_type === "button" && (
337
+ <div>Button: {content.text || "No text"}</div>
338
+ )}
339
+ {draggedBlock.block_type === "video_embed" && (
340
+ <div>Video: {content.title || "No title"}</div>
341
+ )}
342
+ {draggedBlock.block_type === "posts_grid" && (
343
+ <div>
344
+ Posts Grid: {content.columns || 3} cols
345
+ </div>
346
+ )}
347
+ </>
348
+ );
349
+ })()}
329
350
  </div>
330
351
  </div>
331
352
  ) : null}
@@ -30,7 +30,7 @@ export default function TextBlockEditor({
30
30
  }: BlockEditorProps<Partial<TextBlockContent>>) {
31
31
  const labelId = useId();
32
32
  const [pickerOpen, setPickerOpen] = useState(false);
33
- const resolverRef = useRef<null | ((v: any) => void)>(null);
33
+ const resolverRef = useRef<null | ((v: { src: string; alt?: string; width?: number | null; height?: number | null; blurDataURL?: string | null }) => void)>(null);
34
34
  const R2_BASE_URL = process.env.NEXT_PUBLIC_R2_BASE_URL || '';
35
35
  const openImagePicker = useCallback(() => {
36
36
  setPickerOpen(true);
@@ -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 } from "@nextblock-cms/db";
6
+ import type { Database, Json } 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 any, // Store all variants including the original
114
+ variants: allVariantsToStore as unknown as Json, // 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
  };
@@ -201,24 +201,24 @@ export async function deleteMediaItem(mediaId: string, objectKey: string) {
201
201
  return encodedRedirect("error", "/cms/media", "Forbidden: Insufficient permissions.");
202
202
  }
203
203
 
204
- const { DeleteObjectCommand } = await import("@aws-sdk/client-s3");
205
- const { getS3Client } = await import("@nextblock-cms/utils/server");
206
- const s3Client = await getS3Client();
207
- const R2_BUCKET_NAME = process.env.R2_BUCKET_NAME;
208
-
209
- if (!R2_BUCKET_NAME) {
210
- return encodedRedirect("error", "/cms/media", "R2 Bucket not configured for deletion.");
211
- }
212
- if (!s3Client) {
213
- return encodedRedirect("error", "/cms/media", "R2 client is not configured for deletion.");
214
- }
204
+ const { DeleteObjectCommand } = await import("@aws-sdk/client-s3");
205
+ const { getS3Client } = await import("@nextblock-cms/utils/server");
206
+ const s3Client = await getS3Client();
207
+ const R2_BUCKET_NAME = process.env.R2_BUCKET_NAME;
208
+
209
+ if (!R2_BUCKET_NAME) {
210
+ return encodedRedirect("error", "/cms/media", "R2 Bucket not configured for deletion.");
211
+ }
212
+ if (!s3Client) {
213
+ return encodedRedirect("error", "/cms/media", "R2 client is not configured for deletion.");
214
+ }
215
215
 
216
216
  try {
217
217
  const deleteCommand = new DeleteObjectCommand({
218
218
  Bucket: R2_BUCKET_NAME,
219
219
  Key: objectKey,
220
220
  });
221
- await s3Client.send(deleteCommand);
221
+ await s3Client.send(deleteCommand);
222
222
  } catch (r2Error: unknown) {
223
223
  console.error("Error deleting from R2:", r2Error);
224
224
  // Decide if you want to proceed with DB deletion if R2 deletion fails
@@ -255,17 +255,17 @@ export async function deleteMultipleMediaItems(items: Array<{ id: string; object
255
255
  return { error: "No items selected for deletion." };
256
256
  }
257
257
 
258
- const { DeleteObjectsCommand } = await import("@aws-sdk/client-s3"); // Use DeleteObjects for batch
259
- const { getS3Client } = await import("@nextblock-cms/utils/server");
260
- const s3Client = await getS3Client();
261
- const R2_BUCKET_NAME = process.env.R2_BUCKET_NAME;
262
-
263
- if (!R2_BUCKET_NAME) {
264
- return { error: "R2 Bucket not configured for deletion." };
265
- }
266
- if (!s3Client) {
267
- return { error: "R2 client is not configured for deletion." };
268
- }
258
+ const { DeleteObjectsCommand } = await import("@aws-sdk/client-s3"); // Use DeleteObjects for batch
259
+ const { getS3Client } = await import("@nextblock-cms/utils/server");
260
+ const s3Client = await getS3Client();
261
+ const R2_BUCKET_NAME = process.env.R2_BUCKET_NAME;
262
+
263
+ if (!R2_BUCKET_NAME) {
264
+ return { error: "R2 Bucket not configured for deletion." };
265
+ }
266
+ if (!s3Client) {
267
+ return { error: "R2 client is not configured for deletion." };
268
+ }
269
269
 
270
270
  const r2ObjectsToDelete = items.map(item => ({ Key: item.objectKey }));
271
271
  const itemIdsToDelete = items.map(item => item.id);
@@ -345,18 +345,18 @@ export async function moveMultipleMediaItems(
345
345
  };
346
346
  const folder = sanitizeFolder(destinationFolder);
347
347
 
348
- const { CopyObjectCommand, DeleteObjectCommand, ListObjectsV2Command, HeadObjectCommand } = await import("@aws-sdk/client-s3");
349
- const { getS3Client } = await import("@nextblock-cms/utils/server");
350
- const s3Client = await getS3Client();
351
- const R2_BUCKET_NAME = process.env.R2_BUCKET_NAME;
352
- const R2_PUBLIC_URL_BASE = process.env.NEXT_PUBLIC_R2_BASE_URL || '';
353
-
354
- if (!R2_BUCKET_NAME) {
355
- return { error: "R2 Bucket not configured for move." };
356
- }
357
- if (!s3Client) {
358
- return { error: "R2 client is not configured for move." };
359
- }
348
+ const { CopyObjectCommand, DeleteObjectCommand, ListObjectsV2Command, HeadObjectCommand } = await import("@aws-sdk/client-s3");
349
+ const { getS3Client } = await import("@nextblock-cms/utils/server");
350
+ const s3Client = await getS3Client();
351
+ const R2_BUCKET_NAME = process.env.R2_BUCKET_NAME;
352
+ const R2_PUBLIC_URL_BASE = process.env.NEXT_PUBLIC_R2_BASE_URL || '';
353
+
354
+ if (!R2_BUCKET_NAME) {
355
+ return { error: "R2 Bucket not configured for move." };
356
+ }
357
+ if (!s3Client) {
358
+ return { error: "R2 client is not configured for move." };
359
+ }
360
360
 
361
361
  if (!items || items.length === 0) {
362
362
  return { error: "No items selected for move." };
@@ -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 = { 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) : []);
385
+ type Variant = ImageVariant & { [k: string]: unknown };
386
+ const oldVariants: Variant[] = Array.isArray(mediaRow.variants) ? (mediaRow.variants as unknown as Variant[]) :
387
+ (typeof mediaRow.variants === 'object' && mediaRow.variants !== null ? (mediaRow.variants as unknown as Variant[]) : []);
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: any) {
433
- const name = err?.name || '';
434
- const message = err?.message || String(err);
432
+ } catch (err: unknown) {
433
+ const name = (err as Error)?.name || '';
434
+ const message = (err as Error)?.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 } as any;
502
+ const updated = { ...v, objectKey: updatedKey };
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 any })
510
+ .update({ object_key: newMainKey, file_path: newMainKey, folder, variants: newVariants as unknown as Json })
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: any) {
519
- const msg = err?.name && err?.message ? `${err.name}: ${err.message}` : (err?.message || String(err));
518
+ } catch (err: unknown) {
519
+ const msg = (err as Error)?.name && (err as Error)?.message ? `${(err as Error).name}: ${(err as Error).message}` : ((err as Error)?.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 ((res as any)?.error) {
106
+ if ('error' in res && res.error) {
107
107
  // Accumulate errors but keep going
108
108
  hadError = true;
109
- setMoveError((prev) => (prev ? prev + " | " : "") + (res as any).error);
109
+ setMoveError((prev) => (prev ? prev + " | " : "") + res.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 } as any;
116
+ return { ...m, object_key: `${folder}${filename}`, folder };
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 any)?.digest === 'string' && (err as any).digest.startsWith('NEXT_REDIRECT'));
262
+ const isRedirect = (err instanceof Error && err.message === 'NEXT_REDIRECT') || (typeof (err as { digest?: unknown })?.digest === 'string' && ((err as { digest: string }).digest.startsWith('NEXT_REDIRECT')));
263
263
 
264
264
  if (isRedirect && !returnJustData) {
265
265
  setUploadStatus("success");
@@ -51,8 +51,8 @@ async function getDistinctFolders(search?: string): Promise<string[]> {
51
51
  return [];
52
52
  }
53
53
  let folders = (data || [])
54
- .map((r: any) => r.folder)
55
- .filter((f: any) => typeof f === 'string' && f.length > 0);
54
+ .map((r) => r.folder)
55
+ .filter((f): f is string => typeof f === 'string' && f.length > 0);
56
56
  if (search && search.trim()) {
57
57
  const t = search.trim().toLowerCase();
58
58
  folders = folders.filter((f: string) => f.toLowerCase().includes(t));
@@ -71,7 +71,7 @@ async function getFolderCounts(): Promise<Record<string, number>> {
71
71
  return {};
72
72
  }
73
73
  const counts: Record<string, number> = {};
74
- (data || []).forEach((row: any) => {
74
+ (data || []).forEach((row) => {
75
75
  const f: string | null = row.folder;
76
76
  if (!f || typeof f !== 'string' || f.length === 0) return;
77
77
  const norm = f.endsWith('/') ? f : `${f}/`;
@@ -1,4 +1,3 @@
1
- // apps/nextblock/app/cms/revisions/JsonDiffView.tsx
2
1
  "use client";
3
2
 
4
3
  import React, { useMemo } from 'react';
@@ -11,10 +10,11 @@ interface JsonDiffViewProps {
11
10
  rightTitle?: string;
12
11
  }
13
12
 
14
- function getByPointer(obj: any, pointer: string): any {
13
+ function getByPointer(obj: unknown, pointer: string): unknown {
15
14
  if (!pointer || pointer === '/') return obj;
16
15
  const parts = pointer.split('/').slice(1).map(p => p.replace(/~1/g, '/').replace(/~0/g, '~'));
17
- let cur = obj;
16
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
17
+ let cur = obj as any;
18
18
  for (const part of parts) {
19
19
  if (cur == null) return undefined;
20
20
  cur = cur[part];
@@ -22,7 +22,7 @@ function getByPointer(obj: any, pointer: string): any {
22
22
  return cur;
23
23
  }
24
24
 
25
- function safeStringify(v: any): string {
25
+ function safeStringify(v: unknown): string {
26
26
  try {
27
27
  return JSON.stringify(v, null, 2);
28
28
  } catch {
@@ -32,10 +32,11 @@ function safeStringify(v: any): string {
32
32
 
33
33
  export default function JsonDiffView({ oldValue, newValue, leftTitle = 'Current', rightTitle = 'Selected' }: JsonDiffViewProps) {
34
34
  const { ops, oldObj } = useMemo(() => {
35
- let a: any = null, b: any = null;
35
+ let a: unknown = null, b: unknown = null;
36
36
  try { a = JSON.parse(oldValue); } catch { a = oldValue; }
37
37
  try { b = JSON.parse(newValue); } catch { b = newValue; }
38
- const operations: Operation[] = compare(a, b);
38
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
39
+ const operations: Operation[] = compare(a as any, b as any);
39
40
  return { ops: operations, oldObj: a };
40
41
  }, [oldValue, newValue]);
41
42
 
@@ -56,7 +57,7 @@ export default function JsonDiffView({ oldValue, newValue, leftTitle = 'Current'
56
57
  {ops.map((op, idx) => {
57
58
  const oldAtPath = op.op !== 'add' ? getByPointer(oldObj, op.path) : undefined;
58
59
  const oldStr = op.op !== 'add' ? safeStringify(oldAtPath) : '';
59
- const newStr = op.op !== 'remove' ? safeStringify((op as any).value) : '';
60
+ const newStr = op.op !== 'remove' && 'value' in op ? safeStringify((op as { value: unknown }).value) : '';
60
61
  return (
61
62
  <div key={idx} className="rounded border">
62
63
  <div className="px-2 py-1 border-b flex items-center gap-2 text-xs">
@@ -54,18 +54,24 @@ export default function RevisionHistoryButton({ parentType, parentId }: Revision
54
54
  try {
55
55
  if (parentType === 'page') {
56
56
  const res = await listPageRevisions(parentId);
57
- if ('error' in res) {
58
- setError(res.error ?? 'Unknown error');
59
- setRevisions(null);
60
- setCurrentVersion(null);
61
- } else {
62
- setRevisions(res.revisions as unknown as RevisionItem[]);
63
- setCurrentVersion((res as any).currentVersion ?? null);
64
- }
57
+ if ('error' in res) {
58
+ setError(res.error ?? 'Unknown error');
59
+ setRevisions(null);
60
+ setCurrentVersion(null);
61
+ } else {
62
+ setRevisions(res.revisions as unknown as RevisionItem[]);
63
+ setCurrentVersion(res.currentVersion ?? null);
64
+ }
65
65
  } else {
66
66
  const res = await listPostRevisions(parentId);
67
- if ('error' in res) { setError(res.error ?? 'Unknown error'); setRevisions(null); setCurrentVersion(null); }
68
- else { setRevisions(res.revisions as unknown as RevisionItem[]); setCurrentVersion((res as any).currentVersion ?? null); }
67
+ if ('error' in res) {
68
+ setError(res.error ?? 'Unknown error');
69
+ setRevisions(null);
70
+ setCurrentVersion(null);
71
+ } else {
72
+ setRevisions(res.revisions as unknown as RevisionItem[]);
73
+ setCurrentVersion(res.currentVersion ?? null);
74
+ }
69
75
  }
70
76
  } catch (e: unknown) {
71
77
  setError(e instanceof Error ? e.message : 'Failed to load revisions');
@@ -3,7 +3,7 @@
3
3
 
4
4
  import { createClient } from "@nextblock-cms/db/server";
5
5
  import type { Json } from "@nextblock-cms/db";
6
- import { compare, applyPatch } from 'fast-json-patch';
6
+ import { compare, applyPatch, type Operation } from 'fast-json-patch';
7
7
  import type { FullPageContent, FullPostContent } from './utils';
8
8
 
9
9
 
@@ -120,8 +120,8 @@ export async function restorePageToVersion(pageId: number, targetVersion: number
120
120
 
121
121
  for (const r of diffs || []) {
122
122
  if (r.revision_type === 'diff') {
123
- const ops = r.content as any[];
124
- const result = applyPatch(content as any, ops, /*validate*/ false, /*mutateDocument*/ true);
123
+ const ops = r.content as unknown as Operation[];
124
+ const result = applyPatch(content as unknown as Record<string, unknown>, ops, /*validate*/ false, /*mutateDocument*/ true);
125
125
  content = result.newDocument as unknown as FullPageContent;
126
126
  } else {
127
127
  content = r.content as unknown as FullPageContent;
@@ -208,8 +208,8 @@ export async function restorePostToVersion(postId: number, targetVersion: number
208
208
 
209
209
  for (const r of diffs || []) {
210
210
  if (r.revision_type === 'diff') {
211
- const ops = r.content as any[];
212
- const result = applyPatch(content as any, ops, /*validate*/ false, /*mutateDocument*/ true);
211
+ const ops = r.content as unknown as Operation[];
212
+ const result = applyPatch(content as unknown as Record<string, unknown>, ops, /*validate*/ false, /*mutateDocument*/ true);
213
213
  content = result.newDocument as unknown as FullPostContent;
214
214
  } else {
215
215
  content = r.content as unknown as FullPostContent;
@@ -296,8 +296,8 @@ export async function reconstructPageVersionContent(pageId: number, targetVersio
296
296
 
297
297
  for (const r of diffs || []) {
298
298
  if (r.revision_type === 'diff') {
299
- const ops = r.content as any[];
300
- const result = applyPatch(content as any, ops, false, true);
299
+ const ops = r.content as unknown as Operation[];
300
+ const result = applyPatch(content as unknown as Record<string, unknown>, ops, false, true);
301
301
  content = result.newDocument as unknown as FullPageContent;
302
302
  } else {
303
303
  content = r.content as unknown as FullPageContent;
@@ -333,8 +333,8 @@ export async function reconstructPostVersionContent(postId: number, targetVersio
333
333
 
334
334
  for (const r of diffs || []) {
335
335
  if (r.revision_type === 'diff') {
336
- const ops = r.content as any[];
337
- const result = applyPatch(content as any, ops, false, true);
336
+ const ops = r.content as unknown as Operation[];
337
+ const result = applyPatch(content as unknown as Record<string, unknown>, ops, false, true);
338
338
  content = result.newDocument as unknown as FullPostContent;
339
339
  } else {
340
340
  content = r.content as unknown as FullPostContent;
@@ -59,7 +59,7 @@ export async function getTranslations() {
59
59
 
60
60
  export async function updateTranslation(prevState: unknown, formData: FormData) {
61
61
  const supabase = createClient();
62
- const data = Object.fromEntries(formData as any);
62
+ const data = Object.fromEntries(formData.entries());
63
63
  const key = data.key as string;
64
64
 
65
65
  if (!key) {
@@ -13,6 +13,7 @@ export default async function EditLogoPage(props: { params: Promise<{ id: string
13
13
  return (
14
14
  <div>
15
15
  <h1 className="text-2xl font-semibold mb-6">Edit Logo</h1>
16
+ {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
16
17
  <LogoForm logo={logo} action={updateLogo as any} />
17
18
  </div>
18
19
  )
@@ -117,21 +117,21 @@ export interface SectionBlockContent {
117
117
  solid_color?: string;
118
118
  min_height?: string;
119
119
  gradient?: Gradient;
120
- image?: {
121
- media_id: string;
122
- object_key: string;
123
- alt_text?: string;
124
- width?: number;
125
- height?: number;
126
- blur_data_url?: string;
127
- size: 'cover' | 'contain';
128
- position: 'center' | 'top' | 'bottom' | 'left' | 'right';
129
- quality?: number | null;
130
- overlay?: {
131
- type: 'gradient';
132
- gradient: Gradient;
133
- };
134
- };
120
+ image?: {
121
+ media_id: string;
122
+ object_key: string;
123
+ alt_text?: string;
124
+ width?: number;
125
+ height?: number;
126
+ blur_data_url?: string;
127
+ size: 'cover' | 'contain';
128
+ position: 'center' | 'top' | 'bottom' | 'left' | 'right';
129
+ quality?: number | null;
130
+ overlay?: {
131
+ type: 'gradient';
132
+ gradient: Gradient;
133
+ };
134
+ };
135
135
  };
136
136
  /** Responsive column configuration */
137
137
  responsive_columns: {
@@ -149,7 +149,7 @@ export interface SectionBlockContent {
149
149
  /** Array of blocks within columns - 2D array where each index represents a column */
150
150
  column_blocks: Array<Array<{
151
151
  block_type: BlockType;
152
- content: Record<string, any>;
152
+ content: Record<string, unknown>;
153
153
  temp_id?: string; // For client-side management before save
154
154
  }>>;
155
155
  }
@@ -211,7 +211,7 @@ export interface ContentPropertyDefinition {
211
211
  /** Human-readable description of the property */
212
212
  description?: string;
213
213
  /** Default value for the property */
214
- default?: any;
214
+ default?: unknown;
215
215
  /** For union types, the possible values */
216
216
  unionValues?: readonly string[];
217
217
  /** For array types, the type of array elements */
@@ -221,7 +221,7 @@ export interface ContentPropertyDefinition {
221
221
  min?: number;
222
222
  max?: number;
223
223
  pattern?: string;
224
- enum?: readonly any[];
224
+ enum?: readonly unknown[];
225
225
  };
226
226
  }
227
227
 
@@ -229,7 +229,7 @@ export interface ContentPropertyDefinition {
229
229
  * Enhanced block definition interface with generic type parameter
230
230
  * Links the TypeScript interface to the block definition for better type safety
231
231
  */
232
- export interface BlockDefinition<T = any> {
232
+ export interface BlockDefinition<T = unknown> {
233
233
  /** The unique identifier for the block type */
234
234
  type: BlockType;
235
235
  /** User-friendly display name for the block */
@@ -799,7 +799,7 @@ export function getBlockDefinition(blockType: BlockType): BlockDefinition | unde
799
799
  * @param blockType - The type of block to get initial content for
800
800
  * @returns The initial content object or undefined if block type not found
801
801
  */
802
- export function getInitialContent(blockType: BlockType): object | undefined {
802
+ export function getInitialContent(blockType: BlockType): unknown {
803
803
  return blockRegistry[blockType]?.initialContent;
804
804
  }
805
805
 
@@ -870,7 +870,7 @@ export type AllBlockContent =
870
870
  */
871
871
  export function validateBlockContent(
872
872
  blockType: BlockType,
873
- content: Record<string, any>
873
+ content: Record<string, unknown>
874
874
  ): {
875
875
  isValid: boolean;
876
876
  errors: string[];
@@ -972,9 +972,9 @@ export function getRequiredProperties(blockType: BlockType): string[] {
972
972
  const schema = getContentSchema(blockType);
973
973
  if (!schema) return [];
974
974
 
975
- return Object.entries(schema)
976
- .filter(([, def]) => def.required)
977
- .map(([name]) => name);
975
+ return Object.entries(schema)
976
+ .filter(([, def]) => def.required)
977
+ .map(([name]) => name);
978
978
  }
979
979
 
980
980
  /**
@@ -984,13 +984,13 @@ export function getRequiredProperties(blockType: BlockType): string[] {
984
984
  * @param blockType - The type of block
985
985
  * @returns Complete default content object
986
986
  */
987
- export function generateDefaultContent(blockType: BlockType): Record<string, any> {
987
+ export function generateDefaultContent(blockType: BlockType): Record<string, unknown> {
988
988
  const schema = getContentSchema(blockType);
989
- const initialContent = getInitialContent(blockType) || {};
989
+ const initialContent = getInitialContent(blockType) || ({} as Record<string, unknown>);
990
990
 
991
- if (!schema) return initialContent;
991
+ if (!schema) return initialContent as Record<string, unknown>;
992
992
 
993
- const defaultContent: Record<string, any> = { ...initialContent };
993
+ const defaultContent: Record<string, unknown> = { ...(initialContent as Record<string, unknown>) };
994
994
 
995
995
  for (const [propertyName, propertyDef] of Object.entries(schema)) {
996
996
  if (defaultContent[propertyName] === undefined && propertyDef.default !== undefined) {
@@ -999,4 +999,4 @@ export function generateDefaultContent(blockType: BlockType): Record<string, any
999
999
  }
1000
1000
 
1001
1001
  return defaultContent;
1002
- }
1002
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextblock-cms/template",
3
- "version": "0.2.9",
3
+ "version": "0.2.11",
4
4
  "private": true,
5
5
  "scripts": {
6
6
  "dev": "next dev",