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.
- package/package.json +1 -1
- package/scripts/sync-template.js +70 -52
- package/templates/nextblock-template/app/[slug]/page.tsx +55 -55
- package/templates/nextblock-template/app/cms/blocks/actions.ts +15 -15
- package/templates/nextblock-template/app/cms/blocks/components/BackgroundSelector.tsx +14 -12
- package/templates/nextblock-template/app/cms/blocks/components/BlockEditorArea.tsx +24 -21
- package/templates/nextblock-template/app/cms/blocks/components/BlockEditorModal.tsx +1 -1
- package/templates/nextblock-template/app/cms/blocks/components/ColumnEditor.tsx +42 -24
- package/templates/nextblock-template/app/cms/blocks/components/EditableBlock.tsx +16 -16
- package/templates/nextblock-template/app/cms/blocks/editors/SectionBlockEditor.tsx +56 -35
- package/templates/nextblock-template/app/cms/blocks/editors/TextBlockEditor.tsx +1 -1
- package/templates/nextblock-template/app/cms/media/actions.ts +47 -47
- package/templates/nextblock-template/app/cms/media/components/MediaGridClient.tsx +3 -3
- package/templates/nextblock-template/app/cms/media/components/MediaUploadForm.tsx +1 -1
- package/templates/nextblock-template/app/cms/media/page.tsx +3 -3
- package/templates/nextblock-template/app/cms/revisions/JsonDiffView.tsx +8 -7
- package/templates/nextblock-template/app/cms/revisions/RevisionHistoryButton.tsx +16 -10
- package/templates/nextblock-template/app/cms/revisions/service.ts +9 -9
- package/templates/nextblock-template/app/cms/settings/extra-translations/actions.ts +1 -1
- package/templates/nextblock-template/app/cms/settings/logos/[id]/edit/page.tsx +1 -0
- package/templates/nextblock-template/eslint.config.mjs +14 -10
- package/templates/nextblock-template/lib/blocks/blockRegistry.ts +29 -29
- package/templates/nextblock-template/package.json +5 -3
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,18 +1,18 @@
|
|
|
1
1
|
// app/[slug]/page.tsx
|
|
2
2
|
import React from 'react';
|
|
3
3
|
import { getSsgSupabaseClient } from "@nextblock-cms/db/server";
|
|
4
|
-
import { notFound } from "next/navigation";
|
|
5
|
-
import type { Metadata } from 'next';
|
|
4
|
+
import { notFound } from "next/navigation";
|
|
5
|
+
import type { Metadata } from 'next';
|
|
6
6
|
import PageClientContent from "./PageClientContent";
|
|
7
|
-
import { getPageDataBySlug } from "./page.utils";
|
|
8
|
-
import BlockRenderer from "../../components/BlockRenderer";
|
|
9
|
-
import type { HeroBlockContent } from '../../lib/blocks/blockRegistry';
|
|
10
|
-
import { cookies, headers } from "next/headers";
|
|
7
|
+
import { getPageDataBySlug } from "./page.utils";
|
|
8
|
+
import BlockRenderer from "../../components/BlockRenderer";
|
|
9
|
+
import type { HeroBlockContent } from '../../lib/blocks/blockRegistry';
|
|
10
|
+
import { cookies, headers } from "next/headers";
|
|
11
11
|
|
|
12
|
-
export const dynamicParams = true;
|
|
13
|
-
export const revalidate = 360;
|
|
14
|
-
export const dynamic = 'force-dynamic'; // keeps per-request locale; paired with short revalidate
|
|
15
|
-
export const fetchCache = 'force-no-store';
|
|
12
|
+
export const dynamicParams = true;
|
|
13
|
+
export const revalidate = 360;
|
|
14
|
+
export const dynamic = 'force-dynamic'; // keeps per-request locale; paired with short revalidate
|
|
15
|
+
export const fetchCache = 'force-no-store';
|
|
16
16
|
|
|
17
17
|
interface ResolvedPageParams {
|
|
18
18
|
slug: string;
|
|
@@ -40,30 +40,30 @@ 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: page.slug }));
|
|
43
|
+
return pages.map((page: { slug: string }) => ({ slug: page.slug }));
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
export async function generateMetadata(
|
|
47
|
-
{ params: paramsPromise }: PageProps,
|
|
48
|
-
): Promise<Metadata> {
|
|
49
|
-
const params = await paramsPromise;
|
|
50
|
-
let preferredLocale: string | undefined;
|
|
51
|
-
try {
|
|
52
|
-
const store = await cookies();
|
|
53
|
-
preferredLocale = store.get("NEXT_USER_LOCALE")?.value || store.get("NEXT_LOCALE")?.value;
|
|
54
|
-
} catch {
|
|
55
|
-
preferredLocale = undefined;
|
|
56
|
-
}
|
|
57
|
-
if (!preferredLocale) {
|
|
58
|
-
try {
|
|
59
|
-
const hdrs = await headers();
|
|
60
|
-
const al = hdrs.get("accept-language");
|
|
61
|
-
if (al) preferredLocale = al.split(",")[0]?.split("-")[0];
|
|
62
|
-
} catch {
|
|
63
|
-
// ignore header lookup errors
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
const pageData = await getPageDataBySlug(params.slug, preferredLocale);
|
|
46
|
+
export async function generateMetadata(
|
|
47
|
+
{ params: paramsPromise }: PageProps,
|
|
48
|
+
): Promise<Metadata> {
|
|
49
|
+
const params = await paramsPromise;
|
|
50
|
+
let preferredLocale: string | undefined;
|
|
51
|
+
try {
|
|
52
|
+
const store = await cookies();
|
|
53
|
+
preferredLocale = store.get("NEXT_USER_LOCALE")?.value || store.get("NEXT_LOCALE")?.value;
|
|
54
|
+
} catch {
|
|
55
|
+
preferredLocale = undefined;
|
|
56
|
+
}
|
|
57
|
+
if (!preferredLocale) {
|
|
58
|
+
try {
|
|
59
|
+
const hdrs = await headers();
|
|
60
|
+
const al = hdrs.get("accept-language");
|
|
61
|
+
if (al) preferredLocale = al.split(",")[0]?.split("-")[0];
|
|
62
|
+
} catch {
|
|
63
|
+
// ignore header lookup errors
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const pageData = await getPageDataBySlug(params.slug, preferredLocale);
|
|
67
67
|
|
|
68
68
|
if (!pageData) {
|
|
69
69
|
return { title: "Page Not Found" };
|
|
@@ -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 => {
|
|
91
|
-
const langInfo = languages.find(l => l.id === pt.language_id);
|
|
90
|
+
pageTranslations.forEach((pt: { language_id: string; slug: string }) => {
|
|
91
|
+
const langInfo = languages.find((l: { id: string; code: string }) => l.id === pt.language_id);
|
|
92
92
|
if (langInfo) {
|
|
93
93
|
alternates[langInfo.code] = `${siteUrl}/${pt.slug}`;
|
|
94
94
|
}
|
|
@@ -105,25 +105,25 @@ export async function generateMetadata(
|
|
|
105
105
|
};
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
export default async function DynamicPage({ params: paramsPromise }: PageProps) {
|
|
109
|
-
const params = await paramsPromise;
|
|
110
|
-
let preferredLocale: string | undefined;
|
|
111
|
-
try {
|
|
112
|
-
const store = await cookies();
|
|
113
|
-
preferredLocale = store.get("NEXT_USER_LOCALE")?.value || store.get("NEXT_LOCALE")?.value;
|
|
114
|
-
} catch {
|
|
115
|
-
preferredLocale = undefined;
|
|
116
|
-
}
|
|
117
|
-
if (!preferredLocale) {
|
|
118
|
-
try {
|
|
119
|
-
const hdrs = await headers();
|
|
120
|
-
const al = hdrs.get("accept-language");
|
|
121
|
-
if (al) preferredLocale = al.split(",")[0]?.split("-")[0];
|
|
122
|
-
} catch {
|
|
123
|
-
// ignore header lookup errors
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
const pageData = await getPageDataBySlug(params.slug, preferredLocale);
|
|
108
|
+
export default async function DynamicPage({ params: paramsPromise }: PageProps) {
|
|
109
|
+
const params = await paramsPromise;
|
|
110
|
+
let preferredLocale: string | undefined;
|
|
111
|
+
try {
|
|
112
|
+
const store = await cookies();
|
|
113
|
+
preferredLocale = store.get("NEXT_USER_LOCALE")?.value || store.get("NEXT_LOCALE")?.value;
|
|
114
|
+
} catch {
|
|
115
|
+
preferredLocale = undefined;
|
|
116
|
+
}
|
|
117
|
+
if (!preferredLocale) {
|
|
118
|
+
try {
|
|
119
|
+
const hdrs = await headers();
|
|
120
|
+
const al = hdrs.get("accept-language");
|
|
121
|
+
if (al) preferredLocale = al.split(",")[0]?.split("-")[0];
|
|
122
|
+
} catch {
|
|
123
|
+
// ignore header lookup errors
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const pageData = await getPageDataBySlug(params.slug, preferredLocale);
|
|
127
127
|
|
|
128
128
|
if (!pageData) {
|
|
129
129
|
notFound();
|
|
@@ -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.block_type === 'hero');
|
|
154
|
+
const heroBlock = pageData.blocks.find((block: { block_type: string; content: unknown }) => block.block_type === 'hero');
|
|
155
155
|
if (heroBlock) {
|
|
156
156
|
const heroContent = heroBlock.content as unknown as HeroBlockContent;
|
|
157
157
|
if (
|
|
@@ -177,4 +177,4 @@ export default async function DynamicPage({ params: paramsPromise }: PageProps)
|
|
|
177
177
|
</PageClientContent>
|
|
178
178
|
</>
|
|
179
179
|
);
|
|
180
|
-
}
|
|
180
|
+
}
|
|
@@ -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:
|
|
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
|
|
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:
|
|
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
|
|
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:
|
|
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:
|
|
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
|
|
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 } }
|
|
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 } }
|
|
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 } }
|
|
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 } }
|
|
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 } }
|
|
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 } }
|
|
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 } }
|
|
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:
|
|
332
|
-
const { name, value } = e.target
|
|
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<
|
|
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<
|
|
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:
|
|
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={() => {}}
|
|
476
|
-
onContentChange={() => {}}
|
|
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<
|
|
36
|
+
EditorComponent: LazyExoticComponent<ComponentType<any>>;
|
|
37
37
|
};
|
|
38
38
|
|
|
39
39
|
export function BlockEditorModal({
|