create-nextblock 0.2.30 → 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 (23) hide show
  1. package/package.json +1 -1
  2. package/scripts/sync-template.js +70 -52
  3. package/templates/nextblock-template/app/[slug]/page.tsx +55 -55
  4. package/templates/nextblock-template/app/cms/blocks/actions.ts +15 -15
  5. package/templates/nextblock-template/app/cms/blocks/components/BackgroundSelector.tsx +14 -12
  6. package/templates/nextblock-template/app/cms/blocks/components/BlockEditorArea.tsx +24 -21
  7. package/templates/nextblock-template/app/cms/blocks/components/BlockEditorModal.tsx +1 -1
  8. package/templates/nextblock-template/app/cms/blocks/components/ColumnEditor.tsx +42 -24
  9. package/templates/nextblock-template/app/cms/blocks/components/EditableBlock.tsx +16 -16
  10. package/templates/nextblock-template/app/cms/blocks/editors/SectionBlockEditor.tsx +56 -35
  11. package/templates/nextblock-template/app/cms/blocks/editors/TextBlockEditor.tsx +1 -1
  12. package/templates/nextblock-template/app/cms/media/actions.ts +47 -47
  13. package/templates/nextblock-template/app/cms/media/components/MediaGridClient.tsx +3 -3
  14. package/templates/nextblock-template/app/cms/media/components/MediaUploadForm.tsx +1 -1
  15. package/templates/nextblock-template/app/cms/media/page.tsx +3 -3
  16. package/templates/nextblock-template/app/cms/revisions/JsonDiffView.tsx +8 -7
  17. package/templates/nextblock-template/app/cms/revisions/RevisionHistoryButton.tsx +16 -10
  18. package/templates/nextblock-template/app/cms/revisions/service.ts +9 -9
  19. package/templates/nextblock-template/app/cms/settings/extra-translations/actions.ts +1 -1
  20. package/templates/nextblock-template/app/cms/settings/logos/[id]/edit/page.tsx +1 -0
  21. package/templates/nextblock-template/eslint.config.mjs +14 -10
  22. package/templates/nextblock-template/lib/blocks/blockRegistry.ts +29 -29
  23. package/templates/nextblock-template/package.json +5 -3
@@ -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');