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.
- package/package.json +1 -1
- package/scripts/sync-template.js +70 -52
- package/templates/nextblock-template/app/[slug]/PageClientContent.tsx +67 -67
- package/templates/nextblock-template/app/[slug]/page.tsx +4 -4
- package/templates/nextblock-template/app/cms/blocks/actions.ts +10 -10
- package/templates/nextblock-template/app/cms/blocks/components/BackgroundSelector.tsx +348 -348
- package/templates/nextblock-template/app/cms/blocks/components/BlockEditorArea.tsx +8 -8
- package/templates/nextblock-template/app/cms/blocks/components/EditableBlock.tsx +10 -10
- package/templates/nextblock-template/app/cms/blocks/editors/TextBlockEditor.tsx +81 -81
- package/templates/nextblock-template/app/cms/media/actions.ts +35 -35
- package/templates/nextblock-template/app/cms/media/page.tsx +120 -120
- package/templates/nextblock-template/app/cms/revisions/JsonDiffView.tsx +86 -86
- package/templates/nextblock-template/app/cms/revisions/service.ts +344 -344
- package/templates/nextblock-template/app/providers.tsx +2 -2
- package/templates/nextblock-template/components/BlockRenderer.tsx +9 -9
- package/templates/nextblock-template/components/ResponsiveNav.tsx +22 -22
- package/templates/nextblock-template/components/blocks/PostsGridBlock.tsx +12 -12
- package/templates/nextblock-template/components/blocks/renderers/ClientTextBlockRenderer.tsx +26 -26
- package/templates/nextblock-template/components/blocks/renderers/HeroBlockRenderer.tsx +41 -41
- package/templates/nextblock-template/components/blocks/renderers/ImageBlockRenderer.tsx +7 -7
- package/templates/nextblock-template/components/theme-switcher.tsx +78 -78
- package/templates/nextblock-template/eslint.config.mjs +35 -37
- package/templates/nextblock-template/lib/blocks/blockRegistry.ts +19 -19
- package/templates/nextblock-template/next-env.d.ts +6 -6
- package/templates/nextblock-template/package.json +1 -1
package/package.json
CHANGED
package/scripts/sync-template.js
CHANGED
|
@@ -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
|
|
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
|
|
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(
|
|
91
|
-
const langInfo = languages.find(
|
|
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(
|
|
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
|
}
|