create-nextblock 0.2.33 → 0.2.35

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/package.json +1 -1
  2. package/templates/nextblock-template/app/[slug]/PageClientContent.tsx +67 -67
  3. package/templates/nextblock-template/app/[slug]/page.tsx +4 -4
  4. package/templates/nextblock-template/app/cms/blocks/actions.ts +5 -5
  5. package/templates/nextblock-template/app/cms/blocks/components/BackgroundSelector.tsx +348 -350
  6. package/templates/nextblock-template/app/cms/blocks/components/BlockEditorArea.tsx +13 -16
  7. package/templates/nextblock-template/app/cms/blocks/components/BlockEditorModal.tsx +1 -1
  8. package/templates/nextblock-template/app/cms/blocks/components/ColumnEditor.tsx +24 -42
  9. package/templates/nextblock-template/app/cms/blocks/components/EditableBlock.tsx +6 -6
  10. package/templates/nextblock-template/app/cms/blocks/editors/SectionBlockEditor.tsx +35 -56
  11. package/templates/nextblock-template/app/cms/blocks/editors/TextBlockEditor.tsx +81 -81
  12. package/templates/nextblock-template/app/cms/media/actions.ts +12 -12
  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 +120 -120
  16. package/templates/nextblock-template/app/cms/revisions/JsonDiffView.tsx +86 -87
  17. package/templates/nextblock-template/app/cms/revisions/RevisionHistoryButton.tsx +10 -16
  18. package/templates/nextblock-template/app/cms/revisions/service.ts +344 -344
  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 +0 -1
  21. package/templates/nextblock-template/app/providers.tsx +2 -2
  22. package/templates/nextblock-template/components/BlockRenderer.tsx +9 -9
  23. package/templates/nextblock-template/components/ResponsiveNav.tsx +22 -22
  24. package/templates/nextblock-template/components/blocks/PostsGridBlock.tsx +12 -12
  25. package/templates/nextblock-template/components/blocks/renderers/ClientTextBlockRenderer.tsx +26 -26
  26. package/templates/nextblock-template/components/blocks/renderers/HeroBlockRenderer.tsx +41 -41
  27. package/templates/nextblock-template/components/blocks/renderers/ImageBlockRenderer.tsx +7 -7
  28. package/templates/nextblock-template/components/theme-switcher.tsx +78 -78
  29. package/templates/nextblock-template/eslint.config.mjs +35 -37
  30. package/templates/nextblock-template/lib/blocks/blockRegistry.ts +10 -10
  31. package/templates/nextblock-template/next-env.d.ts +6 -6
  32. package/templates/nextblock-template/package.json +1 -1
@@ -1,120 +1,120 @@
1
- // app/cms/media/page.tsx
2
- import React from 'react';
3
- import { createClient } from "@nextblock-cms/db/server";
4
- // import Link from "next/link"; // Unused, MediaGridClient handles item links
5
- import type { Database } from "@nextblock-cms/db";
6
- // DropdownMenu related imports are now handled within MediaGridClient or its sub-components if needed individually.
7
-
8
- type Media = Database['public']['Tables']['media']['Row'];
9
- // If page.tsx itself doesn't directly use DropdownMenu, these can be removed from here.
10
- // For now, assuming MediaGridClient handles its own dropdowns.
11
- import MediaUploadForm from "./components/MediaUploadForm";
12
- // MediaImage and DeleteMediaButtonClient are used by MediaGridClient, not directly here anymore.
13
- import MediaGridClient from "./components/MediaGridClient"; // Import the new client component
14
- import FolderNavigator from "./components/FolderNavigator";
15
-
16
- async function getMediaItems(folder?: string, folderPrefix?: string, search?: string): Promise<Media[]> {
17
- const supabase = createClient();
18
- let query = supabase
19
- .from("media")
20
- .select("*")
21
- .order("created_at", { ascending: false });
22
-
23
- if (folder && folder.trim()) {
24
- query = query.eq('folder', folder);
25
- } else if (folderPrefix && folderPrefix.trim()) {
26
- query = query.ilike('folder', `${folderPrefix}%`);
27
- }
28
-
29
- if (search && search.trim()) {
30
- const term = search.trim();
31
- query = query.or(`file_name.ilike.%${term}%,description.ilike.%${term}%`);
32
- }
33
-
34
- const { data, error } = await query;
35
-
36
- if (error) {
37
- console.error("Error fetching media items:", error);
38
- return [];
39
- }
40
- return data || [];
41
- }
42
-
43
- async function getDistinctFolders(search?: string): Promise<string[]> {
44
- const supabase = createClient();
45
- const { data, error } = await supabase
46
- .from("media")
47
- .select("folder")
48
- .order("folder", { ascending: true });
49
- if (error) {
50
- console.error("Error fetching folders:", error);
51
- return [];
52
- }
53
- let folders = (data || [])
54
- .map((r) => r.folder)
55
- .filter((f): f is string => typeof f === 'string' && f.length > 0);
56
- if (search && search.trim()) {
57
- const t = search.trim().toLowerCase();
58
- folders = folders.filter((f: string) => f.toLowerCase().includes(t));
59
- }
60
- // Ensure trailing slash for consistency
61
- return Array.from(new Set(folders.map((f: string) => (f.endsWith('/') ? f : f + '/'))));
62
- }
63
-
64
- async function getFolderCounts(): Promise<Record<string, number>> {
65
- const supabase = createClient();
66
- const { data, error } = await supabase
67
- .from("media")
68
- .select("folder");
69
- if (error) {
70
- console.error("Error fetching folder counts:", error);
71
- return {};
72
- }
73
- const counts: Record<string, number> = {};
74
- (data || []).forEach((row) => {
75
- const f: string | null = row.folder;
76
- if (!f || typeof f !== 'string' || f.length === 0) return;
77
- const norm = f.endsWith('/') ? f : `${f}/`;
78
- // accumulate counts for each prefix in the path
79
- const parts = norm.replace(/^\/+/, '').split('/').filter(Boolean);
80
- let prefix = '';
81
- for (let i = 0; i < parts.length; i++) {
82
- prefix += (i === 0 ? '' : '/') + parts[i];
83
- const key = `${prefix}/`;
84
- counts[key] = (counts[key] || 0) + 1;
85
- }
86
- });
87
- return counts;
88
- }
89
-
90
- const R2_BASE_URL = process.env.NEXT_PUBLIC_R2_BASE_URL || "";
91
-
92
- export default async function CmsMediaLibraryPage(props: { searchParams?: Promise<{ folder?: string; folderPrefix?: string; q?: string }> }) {
93
- const searchParams = (await props.searchParams) || {};
94
- const selectedFolder = searchParams.folder;
95
- const selectedFolderPrefix = searchParams.folderPrefix;
96
- const searchQuery = searchParams.q;
97
- const [mediaItems, folders, folderCounts] = await Promise.all([
98
- getMediaItems(selectedFolder, selectedFolderPrefix, searchQuery),
99
- getDistinctFolders(searchQuery),
100
- getFolderCounts(),
101
- ]);
102
-
103
- return (
104
- <div className="w-full max-w-screen-2xl mx-auto px-4 overflow-x-hidden space-y-6">
105
- <div className="flex justify-between items-center">
106
- <h1 className="text-2xl font-semibold">Media Library</h1>
107
- </div>
108
-
109
- <MediaUploadForm />
110
-
111
- {/* Compact folder navigator with top tabs and subfolder pills */}
112
- <div className="mt-2">
113
- <FolderNavigator basePath="/cms/media" folders={folders} selectedFolder={selectedFolder || ''} selectedPrefix={selectedFolderPrefix || ''} counts={folderCounts} searchTerm={searchQuery || ''} />
114
- </div>
115
-
116
- {/* The media grid and empty state are now handled by MediaGridClient */}
117
- <MediaGridClient initialMediaItems={mediaItems} r2BaseUrl={R2_BASE_URL} />
118
- </div>
119
- );
120
- }
1
+ // app/cms/media/page.tsx
2
+ import React from 'react';
3
+ import { createClient } from "@nextblock-cms/db/server";
4
+ // import Link from "next/link"; // Unused, MediaGridClient handles item links
5
+ import type { Database } from "@nextblock-cms/db";
6
+ // DropdownMenu related imports are now handled within MediaGridClient or its sub-components if needed individually.
7
+
8
+ type Media = Database['public']['Tables']['media']['Row'];
9
+ // If page.tsx itself doesn't directly use DropdownMenu, these can be removed from here.
10
+ // For now, assuming MediaGridClient handles its own dropdowns.
11
+ import MediaUploadForm from "./components/MediaUploadForm";
12
+ // MediaImage and DeleteMediaButtonClient are used by MediaGridClient, not directly here anymore.
13
+ import MediaGridClient from "./components/MediaGridClient"; // Import the new client component
14
+ import FolderNavigator from "./components/FolderNavigator";
15
+
16
+ async function getMediaItems(folder?: string, folderPrefix?: string, search?: string): Promise<Media[]> {
17
+ const supabase = createClient();
18
+ let query = supabase
19
+ .from("media")
20
+ .select("*")
21
+ .order("created_at", { ascending: false });
22
+
23
+ if (folder && folder.trim()) {
24
+ query = query.eq('folder', folder);
25
+ } else if (folderPrefix && folderPrefix.trim()) {
26
+ query = query.ilike('folder', `${folderPrefix}%`);
27
+ }
28
+
29
+ if (search && search.trim()) {
30
+ const term = search.trim();
31
+ query = query.or(`file_name.ilike.%${term}%,description.ilike.%${term}%`);
32
+ }
33
+
34
+ const { data, error } = await query;
35
+
36
+ if (error) {
37
+ console.error("Error fetching media items:", error);
38
+ return [];
39
+ }
40
+ return data || [];
41
+ }
42
+
43
+ async function getDistinctFolders(search?: string): Promise<string[]> {
44
+ const supabase = createClient();
45
+ const { data, error } = await supabase
46
+ .from("media")
47
+ .select("folder")
48
+ .order("folder", { ascending: true });
49
+ if (error) {
50
+ console.error("Error fetching folders:", error);
51
+ return [];
52
+ }
53
+ let folders = (data || [])
54
+ .map((r: any) => r.folder)
55
+ .filter((f: any) => typeof f === 'string' && f.length > 0);
56
+ if (search && search.trim()) {
57
+ const t = search.trim().toLowerCase();
58
+ folders = folders.filter((f: string) => f.toLowerCase().includes(t));
59
+ }
60
+ // Ensure trailing slash for consistency
61
+ return Array.from(new Set(folders.map((f: string) => (f.endsWith('/') ? f : f + '/'))));
62
+ }
63
+
64
+ async function getFolderCounts(): Promise<Record<string, number>> {
65
+ const supabase = createClient();
66
+ const { data, error } = await supabase
67
+ .from("media")
68
+ .select("folder");
69
+ if (error) {
70
+ console.error("Error fetching folder counts:", error);
71
+ return {};
72
+ }
73
+ const counts: Record<string, number> = {};
74
+ (data || []).forEach((row: any) => {
75
+ const f: string | null = row.folder;
76
+ if (!f || typeof f !== 'string' || f.length === 0) return;
77
+ const norm = f.endsWith('/') ? f : `${f}/`;
78
+ // accumulate counts for each prefix in the path
79
+ const parts = norm.replace(/^\/+/, '').split('/').filter(Boolean);
80
+ let prefix = '';
81
+ for (let i = 0; i < parts.length; i++) {
82
+ prefix += (i === 0 ? '' : '/') + parts[i];
83
+ const key = `${prefix}/`;
84
+ counts[key] = (counts[key] || 0) + 1;
85
+ }
86
+ });
87
+ return counts;
88
+ }
89
+
90
+ const R2_BASE_URL = process.env.NEXT_PUBLIC_R2_BASE_URL || "";
91
+
92
+ export default async function CmsMediaLibraryPage(props: { searchParams?: Promise<{ folder?: string; folderPrefix?: string; q?: string }> }) {
93
+ const searchParams = (await props.searchParams) || {};
94
+ const selectedFolder = searchParams.folder;
95
+ const selectedFolderPrefix = searchParams.folderPrefix;
96
+ const searchQuery = searchParams.q;
97
+ const [mediaItems, folders, folderCounts] = await Promise.all([
98
+ getMediaItems(selectedFolder, selectedFolderPrefix, searchQuery),
99
+ getDistinctFolders(searchQuery),
100
+ getFolderCounts(),
101
+ ]);
102
+
103
+ return (
104
+ <div className="w-full max-w-screen-2xl mx-auto px-4 overflow-x-hidden space-y-6">
105
+ <div className="flex justify-between items-center">
106
+ <h1 className="text-2xl font-semibold">Media Library</h1>
107
+ </div>
108
+
109
+ <MediaUploadForm />
110
+
111
+ {/* Compact folder navigator with top tabs and subfolder pills */}
112
+ <div className="mt-2">
113
+ <FolderNavigator basePath="/cms/media" folders={folders} selectedFolder={selectedFolder || ''} selectedPrefix={selectedFolderPrefix || ''} counts={folderCounts} searchTerm={searchQuery || ''} />
114
+ </div>
115
+
116
+ {/* The media grid and empty state are now handled by MediaGridClient */}
117
+ <MediaGridClient initialMediaItems={mediaItems} r2BaseUrl={R2_BASE_URL} />
118
+ </div>
119
+ );
120
+ }
@@ -1,87 +1,86 @@
1
- "use client";
2
-
3
- import React, { useMemo } from 'react';
4
- import { compare, type Operation } from 'fast-json-patch';
5
-
6
- interface JsonDiffViewProps {
7
- oldValue: string;
8
- newValue: string;
9
- leftTitle?: string;
10
- rightTitle?: string;
11
- }
12
-
13
- function getByPointer(obj: unknown, pointer: string): unknown {
14
- if (!pointer || pointer === '/') return obj;
15
- const parts = pointer.split('/').slice(1).map(p => p.replace(/~1/g, '/').replace(/~0/g, '~'));
16
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
17
- let cur = obj as any;
18
- for (const part of parts) {
19
- if (cur == null) return undefined;
20
- cur = cur[part];
21
- }
22
- return cur;
23
- }
24
-
25
- function safeStringify(v: unknown): string {
26
- try {
27
- return JSON.stringify(v, null, 2);
28
- } catch {
29
- return String(v);
30
- }
31
- }
32
-
33
- export default function JsonDiffView({ oldValue, newValue, leftTitle = 'Current', rightTitle = 'Selected' }: JsonDiffViewProps) {
34
- const { ops, oldObj } = useMemo(() => {
35
- let a: unknown = null, b: unknown = null;
36
- try { a = JSON.parse(oldValue); } catch { a = oldValue; }
37
- try { b = JSON.parse(newValue); } catch { b = newValue; }
38
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
39
- const operations: Operation[] = compare(a as any, b as any);
40
- return { ops: operations, oldObj: a };
41
- }, [oldValue, newValue]);
42
-
43
- return (
44
- <div className="border rounded">
45
- <div className="flex items-center justify-between px-3 py-2 border-b text-sm text-muted-foreground">
46
- <div>{leftTitle}</div>
47
- <div>{rightTitle}</div>
48
- </div>
49
- <div className="p-3 text-sm space-y-2">
50
- <div className="flex items-center gap-3 text-xs text-muted-foreground">
51
- <span className="inline-flex items-center gap-1"><span className="inline-block w-3 h-3 rounded bg-green-200 border border-green-300" /> Current</span>
52
- <span className="inline-flex items-center gap-1"><span className="inline-block w-3 h-3 rounded bg-red-200 border border-red-300" /> Selected Version</span>
53
- </div>
54
- {ops.length === 0 && (
55
- <div className="text-muted-foreground">No differences.</div>
56
- )}
57
- {ops.map((op, idx) => {
58
- const oldAtPath = op.op !== 'add' ? getByPointer(oldObj, op.path) : undefined;
59
- const oldStr = op.op !== 'add' ? safeStringify(oldAtPath) : '';
60
- const newStr = op.op !== 'remove' && 'value' in op ? safeStringify((op as { value: unknown }).value) : '';
61
- return (
62
- <div key={idx} className="rounded border">
63
- <div className="px-2 py-1 border-b flex items-center gap-2 text-xs">
64
- <span className="uppercase tracking-wide font-semibold">{op.op}</span>
65
- <code className="text-muted-foreground break-all">{op.path || '/'}</code>
66
- </div>
67
- <div className="grid grid-cols-1 md:grid-cols-2">
68
- {op.op !== 'add' && (
69
- <div className="p-2 border-r md:border-r">
70
- <div className="text-xs text-muted-foreground mb-1">Current</div>
71
- <pre className="whitespace-pre-wrap break-words text-green-800 bg-green-50 rounded p-2 m-0">{oldStr}</pre>
72
- </div>
73
- )}
74
- {op.op !== 'remove' && (
75
- <div className="p-2">
76
- <div className="text-xs text-muted-foreground mb-1">Selected Version</div>
77
- <pre className="whitespace-pre-wrap break-words text-red-800 bg-red-50 rounded p-2 m-0">{newStr}</pre>
78
- </div>
79
- )}
80
- </div>
81
- </div>
82
- );
83
- })}
84
- </div>
85
- </div>
86
- );
87
- }
1
+ // apps/nextblock/app/cms/revisions/JsonDiffView.tsx
2
+ "use client";
3
+
4
+ import React, { useMemo } from 'react';
5
+ import { compare, type Operation } from 'fast-json-patch';
6
+
7
+ interface JsonDiffViewProps {
8
+ oldValue: string;
9
+ newValue: string;
10
+ leftTitle?: string;
11
+ rightTitle?: string;
12
+ }
13
+
14
+ function getByPointer(obj: any, pointer: string): any {
15
+ if (!pointer || pointer === '/') return obj;
16
+ const parts = pointer.split('/').slice(1).map(p => p.replace(/~1/g, '/').replace(/~0/g, '~'));
17
+ let cur = obj;
18
+ for (const part of parts) {
19
+ if (cur == null) return undefined;
20
+ cur = cur[part];
21
+ }
22
+ return cur;
23
+ }
24
+
25
+ function safeStringify(v: any): string {
26
+ try {
27
+ return JSON.stringify(v, null, 2);
28
+ } catch {
29
+ return String(v);
30
+ }
31
+ }
32
+
33
+ export default function JsonDiffView({ oldValue, newValue, leftTitle = 'Current', rightTitle = 'Selected' }: JsonDiffViewProps) {
34
+ const { ops, oldObj } = useMemo(() => {
35
+ let a: any = null, b: any = null;
36
+ try { a = JSON.parse(oldValue); } catch { a = oldValue; }
37
+ try { b = JSON.parse(newValue); } catch { b = newValue; }
38
+ const operations: Operation[] = compare(a, b);
39
+ return { ops: operations, oldObj: a };
40
+ }, [oldValue, newValue]);
41
+
42
+ return (
43
+ <div className="border rounded">
44
+ <div className="flex items-center justify-between px-3 py-2 border-b text-sm text-muted-foreground">
45
+ <div>{leftTitle}</div>
46
+ <div>{rightTitle}</div>
47
+ </div>
48
+ <div className="p-3 text-sm space-y-2">
49
+ <div className="flex items-center gap-3 text-xs text-muted-foreground">
50
+ <span className="inline-flex items-center gap-1"><span className="inline-block w-3 h-3 rounded bg-green-200 border border-green-300" /> Current</span>
51
+ <span className="inline-flex items-center gap-1"><span className="inline-block w-3 h-3 rounded bg-red-200 border border-red-300" /> Selected Version</span>
52
+ </div>
53
+ {ops.length === 0 && (
54
+ <div className="text-muted-foreground">No differences.</div>
55
+ )}
56
+ {ops.map((op, idx) => {
57
+ const oldAtPath = op.op !== 'add' ? getByPointer(oldObj, op.path) : undefined;
58
+ const oldStr = op.op !== 'add' ? safeStringify(oldAtPath) : '';
59
+ const newStr = op.op !== 'remove' ? safeStringify((op as any).value) : '';
60
+ return (
61
+ <div key={idx} className="rounded border">
62
+ <div className="px-2 py-1 border-b flex items-center gap-2 text-xs">
63
+ <span className="uppercase tracking-wide font-semibold">{op.op}</span>
64
+ <code className="text-muted-foreground break-all">{op.path || '/'}</code>
65
+ </div>
66
+ <div className="grid grid-cols-1 md:grid-cols-2">
67
+ {op.op !== 'add' && (
68
+ <div className="p-2 border-r md:border-r">
69
+ <div className="text-xs text-muted-foreground mb-1">Current</div>
70
+ <pre className="whitespace-pre-wrap break-words text-green-800 bg-green-50 rounded p-2 m-0">{oldStr}</pre>
71
+ </div>
72
+ )}
73
+ {op.op !== 'remove' && (
74
+ <div className="p-2">
75
+ <div className="text-xs text-muted-foreground mb-1">Selected Version</div>
76
+ <pre className="whitespace-pre-wrap break-words text-red-800 bg-red-50 rounded p-2 m-0">{newStr}</pre>
77
+ </div>
78
+ )}
79
+ </div>
80
+ </div>
81
+ );
82
+ })}
83
+ </div>
84
+ </div>
85
+ );
86
+ }
@@ -54,24 +54,18 @@ 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.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 as any).currentVersion ?? null);
64
+ }
65
65
  } else {
66
66
  const res = await listPostRevisions(parentId);
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
- }
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); }
75
69
  }
76
70
  } catch (e: unknown) {
77
71
  setError(e instanceof Error ? e.message : 'Failed to load revisions');