create-nextblock 0.2.31 → 0.2.34

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 (25) hide show
  1. package/package.json +1 -1
  2. package/scripts/sync-template.js +70 -52
  3. package/templates/nextblock-template/app/[slug]/PageClientContent.tsx +67 -67
  4. package/templates/nextblock-template/app/[slug]/page.tsx +4 -4
  5. package/templates/nextblock-template/app/cms/blocks/actions.ts +10 -10
  6. package/templates/nextblock-template/app/cms/blocks/components/BackgroundSelector.tsx +348 -348
  7. package/templates/nextblock-template/app/cms/blocks/components/BlockEditorArea.tsx +8 -8
  8. package/templates/nextblock-template/app/cms/blocks/components/EditableBlock.tsx +10 -10
  9. package/templates/nextblock-template/app/cms/blocks/editors/TextBlockEditor.tsx +81 -81
  10. package/templates/nextblock-template/app/cms/media/actions.ts +35 -35
  11. package/templates/nextblock-template/app/cms/media/page.tsx +120 -120
  12. package/templates/nextblock-template/app/cms/revisions/JsonDiffView.tsx +86 -86
  13. package/templates/nextblock-template/app/cms/revisions/service.ts +344 -344
  14. package/templates/nextblock-template/app/providers.tsx +2 -2
  15. package/templates/nextblock-template/components/BlockRenderer.tsx +9 -9
  16. package/templates/nextblock-template/components/ResponsiveNav.tsx +22 -22
  17. package/templates/nextblock-template/components/blocks/PostsGridBlock.tsx +12 -12
  18. package/templates/nextblock-template/components/blocks/renderers/ClientTextBlockRenderer.tsx +26 -26
  19. package/templates/nextblock-template/components/blocks/renderers/HeroBlockRenderer.tsx +41 -41
  20. package/templates/nextblock-template/components/blocks/renderers/ImageBlockRenderer.tsx +7 -7
  21. package/templates/nextblock-template/components/theme-switcher.tsx +78 -78
  22. package/templates/nextblock-template/eslint.config.mjs +35 -37
  23. package/templates/nextblock-template/lib/blocks/blockRegistry.ts +19 -19
  24. package/templates/nextblock-template/next-env.d.ts +6 -6
  25. 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.34",
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
+ }
@@ -1,19 +1,19 @@
1
1
  // app/[slug]/PageClientContent.tsx
2
2
  "use client";
3
3
 
4
- import React, { useState, useEffect, useMemo } from 'react';
5
- import { useRouter } from 'next/navigation'; // For navigation on lang switch
6
- import type { Database } from "@nextblock-cms/db";
7
- import { useLanguage } from '@/context/LanguageContext';
8
- import { useCurrentContent } from '@/context/CurrentContentContext';
9
- import Link from 'next/link';
10
- import { createClient } from '@nextblock-cms/db';
11
-
12
- type PageType = Database['public']['Tables']['pages']['Row'];
13
- type BlockType = Database['public']['Tables']['blocks']['Row'];
14
-
15
- interface PageClientContentProps {
16
- initialPageData: (PageType & { blocks: BlockType[]; language_code: string; language_id: number; translation_group_id: string; }) | null;
4
+ import React, { useState, useEffect, useMemo } from 'react';
5
+ import { useRouter } from 'next/navigation'; // For navigation on lang switch
6
+ import type { Database } from "@nextblock-cms/db";
7
+ import { useLanguage } from '@/context/LanguageContext';
8
+ import { useCurrentContent } from '@/context/CurrentContentContext';
9
+ import Link from 'next/link';
10
+ import { createClient } from '@nextblock-cms/db';
11
+
12
+ type PageType = Database['public']['Tables']['pages']['Row'];
13
+ type BlockType = Database['public']['Tables']['blocks']['Row'];
14
+
15
+ interface PageClientContentProps {
16
+ initialPageData: (PageType & { blocks: BlockType[]; language_code: string; language_id: number; translation_group_id: string; }) | null;
17
17
  currentSlug: string; // The slug of the currently viewed page
18
18
  children: React.ReactNode;
19
19
  translatedSlugs?: { [key: string]: string };
@@ -43,62 +43,62 @@ interface PageClientContentProps {
43
43
  // }
44
44
 
45
45
 
46
- export default function PageClientContent({ initialPageData, currentSlug, children, translatedSlugs }: PageClientContentProps) {
47
- const { currentLocale, isLoadingLanguages } = useLanguage();
48
- const { currentContent, setCurrentContent } = useCurrentContent();
49
- const router = useRouter();
50
- // currentPageData is the data for the slug currently in the URL.
51
- // It's initially set by the server for the slug it resolved.
52
- const [currentPageData, setCurrentPageData] = useState(initialPageData);
53
- const [isLoadingTargetLang, setIsLoadingTargetLang] = useState(false);
54
- const supabase = useMemo(() => createClient(), []);
46
+ export default function PageClientContent({ initialPageData, currentSlug, children, translatedSlugs }: PageClientContentProps) {
47
+ const { currentLocale, isLoadingLanguages } = useLanguage();
48
+ const { currentContent, setCurrentContent } = useCurrentContent();
49
+ const router = useRouter();
50
+ // currentPageData is the data for the slug currently in the URL.
51
+ // It's initially set by the server for the slug it resolved.
52
+ const [currentPageData, setCurrentPageData] = useState(initialPageData);
53
+ const [isLoadingTargetLang, setIsLoadingTargetLang] = useState(false);
54
+ const supabase = useMemo(() => createClient(), []);
55
55
 
56
56
  // Memoize pageId and pageSlug
57
- const pageId = useMemo(() => currentPageData?.id, [currentPageData?.id]);
58
- const pageSlug = useMemo(() => currentPageData?.slug, [currentPageData?.id, currentLocale]); // include locale so updates propagate
59
-
60
- useEffect(() => {
61
- if (currentLocale && currentPageData && currentPageData.language_code !== currentLocale && translatedSlugs) {
62
- // Current page's language doesn't match context, try to navigate to translated version
63
- setIsLoadingTargetLang(true);
64
- const targetSlug = translatedSlugs[currentLocale];
65
-
66
- if (targetSlug && targetSlug !== currentSlug) {
67
- router.push(`/${targetSlug}`); // Navigate to the translated slug's URL
68
- } else if (targetSlug && targetSlug === currentSlug) {
69
- // Same slug across languages - refetch the page in the target language and update content
70
- (async () => {
71
- const { data, error } = await supabase
72
- .from("pages")
73
- .select("*, languages!inner(code), blocks(*)")
74
- .eq("slug", targetSlug)
75
- .eq("languages.code", currentLocale)
76
- .eq("status", "published")
77
- .order('order', { foreignTable: 'blocks', ascending: true })
78
- .maybeSingle();
79
-
80
- if (!error && data) {
81
- const langInfo = Array.isArray(data.languages) ? data.languages[0] : (data.languages as unknown as { code?: string });
82
- setCurrentPageData({
83
- ...(data as PageType),
84
- blocks: (data as any).blocks || [],
85
- language_code: langInfo?.code || currentLocale,
86
- language_id: data.language_id,
87
- translation_group_id: data.translation_group_id || currentPageData.translation_group_id,
88
- } as typeof currentPageData);
89
- } else {
90
- // fallback to refresh if fetch fails
91
- router.refresh();
92
- }
93
- setIsLoadingTargetLang(false);
94
- })();
95
- } else {
96
- console.warn(`No published translation found for group ${currentPageData.translation_group_id} in language ${currentLocale} using pre-fetched slugs.`);
97
- // Optionally, provide feedback to the user that translation is not available
98
- setIsLoadingTargetLang(false);
99
- }
100
- }
101
- }, [currentLocale, currentPageData, currentSlug, router, initialPageData, translatedSlugs]); // Rerun if initialPageData changes (e.g. after revalidation)
57
+ const pageId = useMemo(() => currentPageData?.id, [currentPageData?.id]);
58
+ const pageSlug = useMemo(() => currentPageData?.slug, [currentPageData?.id, currentLocale]); // include locale so updates propagate
59
+
60
+ useEffect(() => {
61
+ if (currentLocale && currentPageData && currentPageData.language_code !== currentLocale && translatedSlugs) {
62
+ // Current page's language doesn't match context, try to navigate to translated version
63
+ setIsLoadingTargetLang(true);
64
+ const targetSlug = translatedSlugs[currentLocale];
65
+
66
+ if (targetSlug && targetSlug !== currentSlug) {
67
+ router.push(`/${targetSlug}`); // Navigate to the translated slug's URL
68
+ } else if (targetSlug && targetSlug === currentSlug) {
69
+ // Same slug across languages - refetch the page in the target language and update content
70
+ (async () => {
71
+ const { data, error } = await supabase
72
+ .from("pages")
73
+ .select("*, languages!inner(code), blocks(*)")
74
+ .eq("slug", targetSlug)
75
+ .eq("languages.code", currentLocale)
76
+ .eq("status", "published")
77
+ .order('order', { foreignTable: 'blocks', ascending: true })
78
+ .maybeSingle();
79
+
80
+ if (!error && data) {
81
+ const langInfo = Array.isArray(data.languages) ? data.languages[0] : (data.languages as unknown as { code?: string });
82
+ setCurrentPageData({
83
+ ...(data as PageType),
84
+ blocks: (data as any).blocks || [],
85
+ language_code: langInfo?.code || currentLocale,
86
+ language_id: data.language_id,
87
+ translation_group_id: data.translation_group_id || currentPageData.translation_group_id,
88
+ } as typeof currentPageData);
89
+ } else {
90
+ // fallback to refresh if fetch fails
91
+ router.refresh();
92
+ }
93
+ setIsLoadingTargetLang(false);
94
+ })();
95
+ } else {
96
+ console.warn(`No published translation found for group ${currentPageData.translation_group_id} in language ${currentLocale} using pre-fetched slugs.`);
97
+ // Optionally, provide feedback to the user that translation is not available
98
+ setIsLoadingTargetLang(false);
99
+ }
100
+ }
101
+ }, [currentLocale, currentPageData, currentSlug, router, initialPageData, translatedSlugs]); // Rerun if initialPageData changes (e.g. after revalidation)
102
102
 
103
103
  // Update HTML lang attribute based on the *actually displayed* content's language
104
104
  useEffect(() => {
@@ -40,7 +40,7 @@ export async function generateStaticParams(): Promise<ResolvedPageParams[]> {
40
40
  console.error("SSG: Error fetching page slugs for static params:", error);
41
41
  return [];
42
42
  }
43
- return pages.map((page: { slug: string }) => ({ slug: page.slug }));
43
+ return pages.map((page) => ({ slug: page.slug }));
44
44
  }
45
45
 
46
46
  export async function generateMetadata(
@@ -87,8 +87,8 @@ export async function generateMetadata(
87
87
 
88
88
  const alternates: { [key: string]: string } = {};
89
89
  if (languages && pageTranslations) {
90
- pageTranslations.forEach((pt: { language_id: string; slug: string }) => {
91
- const langInfo = languages.find((l: { id: string; code: string }) => l.id === pt.language_id);
90
+ pageTranslations.forEach(pt => {
91
+ const langInfo = languages.find(l => l.id === pt.language_id);
92
92
  if (langInfo) {
93
93
  alternates[langInfo.code] = `${siteUrl}/${pt.slug}`;
94
94
  }
@@ -151,7 +151,7 @@ export default async function DynamicPage({ params: paramsPromise }: PageProps)
151
151
  const r2BaseUrl = process.env.NEXT_PUBLIC_R2_BASE_URL || "";
152
152
 
153
153
  if (pageData && pageData.blocks && r2BaseUrl) {
154
- const heroBlock = pageData.blocks.find((block: { block_type: string; content: unknown }) => block.block_type === 'hero');
154
+ const heroBlock = pageData.blocks.find(block => block.block_type === 'hero');
155
155
  if (heroBlock) {
156
156
  const heroContent = heroBlock.content as unknown as HeroBlockContent;
157
157
  if (
@@ -12,15 +12,15 @@ 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)
@@ -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
  }