create-nextblock 0.2.57 → 0.2.59
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/bin/create-nextblock.js +197 -88
- package/libs/sdk/tsconfig.lib.json +3 -0
- package/package.json +1 -1
- package/templates/nextblock-template/app/(auth-pages)/sign-in/page.tsx +1 -1
- package/templates/nextblock-template/app/actions.ts +6 -2
- package/templates/nextblock-template/app/cms/blocks/components/BlockEditorModal.tsx +118 -67
- package/templates/nextblock-template/app/layout.tsx +23 -3
- package/templates/nextblock-template/next-env.d.ts +1 -1
- package/templates/nextblock-template/package.json +2 -2
- package/templates/nextblock-template/proxy.ts +25 -2
- package/templates/nextblock-template/public/images/metadata_image.webp +0 -0
- package/templates/nextblock-template/tsconfig.json +0 -1
package/bin/create-nextblock.js
CHANGED
|
@@ -55,13 +55,18 @@ const PACKAGE_VERSION_SOURCES = {
|
|
|
55
55
|
program
|
|
56
56
|
.name('create-nextblock')
|
|
57
57
|
.description('Bootstrap a NextBlock CMS project')
|
|
58
|
-
.argument(
|
|
58
|
+
.argument(
|
|
59
|
+
'[project-directory]',
|
|
60
|
+
'The name of the project directory to create',
|
|
61
|
+
)
|
|
59
62
|
.option('--skip-install', 'Skip installing dependencies')
|
|
60
63
|
.option('-y, --yes', 'Skip all interactive prompts and use defaults')
|
|
61
64
|
.action(handleCommand);
|
|
62
65
|
|
|
63
66
|
await program.parseAsync(process.argv).catch((error) => {
|
|
64
|
-
console.error(
|
|
67
|
+
console.error(
|
|
68
|
+
chalk.red(error instanceof Error ? error.message : String(error)),
|
|
69
|
+
);
|
|
65
70
|
process.exit(1);
|
|
66
71
|
});
|
|
67
72
|
|
|
@@ -74,12 +79,17 @@ async function handleCommand(projectDirectory, options) {
|
|
|
74
79
|
if (!projectName) {
|
|
75
80
|
if (yes) {
|
|
76
81
|
projectName = DEFAULT_PROJECT_NAME;
|
|
77
|
-
console.log(
|
|
82
|
+
console.log(
|
|
83
|
+
chalk.blue(
|
|
84
|
+
`Using default project name because --yes was provided: ${projectName}`,
|
|
85
|
+
),
|
|
86
|
+
);
|
|
78
87
|
} else {
|
|
79
88
|
const projectPrompt = await clack.text({
|
|
80
89
|
message: 'What is your project named?',
|
|
81
90
|
initialValue: DEFAULT_PROJECT_NAME,
|
|
82
|
-
validate: (value) =>
|
|
91
|
+
validate: (value) =>
|
|
92
|
+
!value ? 'Project name is required' : undefined,
|
|
83
93
|
});
|
|
84
94
|
if (clack.isCancel(projectPrompt)) {
|
|
85
95
|
handleWizardCancel('Setup cancelled.');
|
|
@@ -156,17 +166,27 @@ async function handleCommand(projectDirectory, options) {
|
|
|
156
166
|
if (!yes) {
|
|
157
167
|
await runSetupWizard(projectDir, projectName);
|
|
158
168
|
} else {
|
|
159
|
-
console.log(
|
|
169
|
+
console.log(
|
|
170
|
+
chalk.yellow(
|
|
171
|
+
'Skipping interactive setup wizard because --yes was provided.',
|
|
172
|
+
),
|
|
173
|
+
);
|
|
160
174
|
}
|
|
161
175
|
|
|
162
176
|
await initializeGit(projectDir);
|
|
163
177
|
|
|
164
|
-
console.log(
|
|
178
|
+
console.log(
|
|
179
|
+
chalk.green(
|
|
180
|
+
`\nSuccess! Your NextBlock CMS project "${projectName}" is ready.\n`,
|
|
181
|
+
),
|
|
182
|
+
);
|
|
165
183
|
console.log(chalk.cyan('Next step:'));
|
|
166
184
|
console.log(chalk.cyan(` cd ${projectName} && npm run dev`));
|
|
167
185
|
} catch (error) {
|
|
168
186
|
console.error(
|
|
169
|
-
chalk.red(
|
|
187
|
+
chalk.red(
|
|
188
|
+
error instanceof Error ? error.message : 'An unexpected error occurred',
|
|
189
|
+
),
|
|
170
190
|
);
|
|
171
191
|
process.exit(1);
|
|
172
192
|
}
|
|
@@ -183,10 +203,10 @@ async function runSetupWizard(projectDir, projectName) {
|
|
|
183
203
|
await resetSupabaseProjectRef(projectPath);
|
|
184
204
|
|
|
185
205
|
clack.note(
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
206
|
+
'Before proceeding, ensure you have a Supabase project ready.\n\n' +
|
|
207
|
+
'1. Supabase Cloud: Create a project at https://supabase.com/dashboard\n' +
|
|
208
|
+
'2. Vercel Storage: If created via Vercel > Storage, check your .env.local snippet on Vercel for keys.',
|
|
209
|
+
'Supabase Prerequisites',
|
|
190
210
|
);
|
|
191
211
|
|
|
192
212
|
clack.note('Connecting to Supabase...');
|
|
@@ -231,7 +251,10 @@ async function runSetupWizard(projectDir, projectName) {
|
|
|
231
251
|
}
|
|
232
252
|
const siteUrl = siteUrlPrompt.trim();
|
|
233
253
|
|
|
234
|
-
clack.note(
|
|
254
|
+
clack.note(
|
|
255
|
+
'Please go to your Supabase project dashboard to get the following secrets.\n' +
|
|
256
|
+
'Note: For "Access Token", go to Account > Access Tokens > Generate New Token.',
|
|
257
|
+
);
|
|
235
258
|
const supabaseKeys = await clack.group(
|
|
236
259
|
{
|
|
237
260
|
postgresUrl: () =>
|
|
@@ -239,7 +262,8 @@ async function runSetupWizard(projectDir, projectName) {
|
|
|
239
262
|
message:
|
|
240
263
|
'What is your Connection String? (Supabase: Project Dashboard > Connect (Top Left) > Connection String > URI | Vercel: POSTGRES_URL)',
|
|
241
264
|
placeholder: 'postgresql://...',
|
|
242
|
-
validate: (val) =>
|
|
265
|
+
validate: (val) =>
|
|
266
|
+
!val ? 'Connection string is required' : undefined,
|
|
243
267
|
}),
|
|
244
268
|
anonKey: () =>
|
|
245
269
|
clack.password({
|
|
@@ -251,7 +275,15 @@ async function runSetupWizard(projectDir, projectName) {
|
|
|
251
275
|
clack.password({
|
|
252
276
|
message:
|
|
253
277
|
'What is your Service Role Key (service_role key)? (Supabase: Project Settings > API Keys > Project Legacy API Keys | Vercel: SUPABASE_SERVICE_ROLE_KEY)',
|
|
254
|
-
validate: (val) =>
|
|
278
|
+
validate: (val) =>
|
|
279
|
+
!val ? 'Service Role Key is required' : undefined,
|
|
280
|
+
}),
|
|
281
|
+
accessToken: () =>
|
|
282
|
+
clack.password({
|
|
283
|
+
message:
|
|
284
|
+
'What is your Personal Access Token? (Supabase: Account > Access Tokens). Required for deployment.',
|
|
285
|
+
validate: (val) =>
|
|
286
|
+
!val ? 'Access Token is required for deployment' : undefined,
|
|
255
287
|
}),
|
|
256
288
|
},
|
|
257
289
|
{ onCancel: () => handleWizardCancel('Setup cancelled.') },
|
|
@@ -272,7 +304,8 @@ async function runSetupWizard(projectDir, projectName) {
|
|
|
272
304
|
|
|
273
305
|
if (!dbPassword) {
|
|
274
306
|
const passwordPrompt = await clack.password({
|
|
275
|
-
message:
|
|
307
|
+
message:
|
|
308
|
+
'Could not extract password from URL. What is your Database Password?',
|
|
276
309
|
validate: (val) => (!val ? 'Password is required' : undefined),
|
|
277
310
|
});
|
|
278
311
|
if (clack.isCancel(passwordPrompt)) {
|
|
@@ -284,11 +317,15 @@ async function runSetupWizard(projectDir, projectName) {
|
|
|
284
317
|
const envPath = resolve(projectPath, '.env');
|
|
285
318
|
const appendEnvBlock = async (label, lines) => {
|
|
286
319
|
const normalized = lines.join('\n');
|
|
287
|
-
const blockContent = normalized.endsWith('\n')
|
|
320
|
+
const blockContent = normalized.endsWith('\n')
|
|
321
|
+
? normalized
|
|
322
|
+
: `${normalized}\n`;
|
|
288
323
|
if (canWriteEnv) {
|
|
289
324
|
await fs.appendFile(envPath, blockContent);
|
|
290
325
|
} else {
|
|
291
|
-
clack.note(
|
|
326
|
+
clack.note(
|
|
327
|
+
`Add the following ${label} values to your existing .env:\n${blockContent}`,
|
|
328
|
+
);
|
|
292
329
|
}
|
|
293
330
|
};
|
|
294
331
|
const envLines = [
|
|
@@ -298,6 +335,7 @@ async function runSetupWizard(projectDir, projectName) {
|
|
|
298
335
|
`NEXT_PUBLIC_SUPABASE_URL=${supabaseUrl}`,
|
|
299
336
|
`NEXT_PUBLIC_SUPABASE_ANON_KEY=${supabaseKeys.anonKey}`,
|
|
300
337
|
`SUPABASE_SERVICE_ROLE_KEY=${supabaseKeys.serviceKey}`,
|
|
338
|
+
`SUPABASE_ACCESS_TOKEN=${supabaseKeys.accessToken}`,
|
|
301
339
|
`POSTGRES_URL=${postgresUrl}`,
|
|
302
340
|
'',
|
|
303
341
|
'# Revalidation',
|
|
@@ -316,7 +354,9 @@ async function runSetupWizard(projectDir, projectName) {
|
|
|
316
354
|
}
|
|
317
355
|
if (!overwrite) {
|
|
318
356
|
canWriteEnv = false;
|
|
319
|
-
clack.note(
|
|
357
|
+
clack.note(
|
|
358
|
+
'Keeping existing .env. Add/merge the generated values manually.',
|
|
359
|
+
);
|
|
320
360
|
}
|
|
321
361
|
}
|
|
322
362
|
|
|
@@ -353,7 +393,7 @@ async function runSetupWizard(projectDir, projectName) {
|
|
|
353
393
|
const linkArgs = supabaseBin === 'npx' ? ['supabase', 'link'] : ['link'];
|
|
354
394
|
linkArgs.push('--project-ref', projectId);
|
|
355
395
|
linkArgs.push('--password', dbPassword);
|
|
356
|
-
|
|
396
|
+
|
|
357
397
|
await execa(command, linkArgs, {
|
|
358
398
|
stdio: 'inherit',
|
|
359
399
|
cwd: projectPath,
|
|
@@ -361,7 +401,8 @@ async function runSetupWizard(projectDir, projectName) {
|
|
|
361
401
|
|
|
362
402
|
// 2. Push the schema using the linked state
|
|
363
403
|
dbPushSpinner.message('Pushing database schema...');
|
|
364
|
-
const pushArgs =
|
|
404
|
+
const pushArgs =
|
|
405
|
+
supabaseBin === 'npx' ? ['supabase', 'db', 'push'] : ['db', 'push'];
|
|
365
406
|
pushArgs.push('--include-all');
|
|
366
407
|
|
|
367
408
|
await execa(command, pushArgs, {
|
|
@@ -373,10 +414,31 @@ async function runSetupWizard(projectDir, projectName) {
|
|
|
373
414
|
SUPABASE_DB_PASSWORD: dbPassword,
|
|
374
415
|
},
|
|
375
416
|
});
|
|
376
|
-
|
|
417
|
+
|
|
418
|
+
// 3. Push the config (for Auth settings like site_url)
|
|
419
|
+
dbPushSpinner.message('Pushing Supabase config (auth settings)...');
|
|
420
|
+
const configPushArgs =
|
|
421
|
+
supabaseBin === 'npx'
|
|
422
|
+
? ['supabase', 'config', 'push']
|
|
423
|
+
: ['config', 'push'];
|
|
424
|
+
|
|
425
|
+
await execa(command, configPushArgs, {
|
|
426
|
+
stdio: ['pipe', 'inherit', 'inherit'],
|
|
427
|
+
cwd: projectPath,
|
|
428
|
+
env: {
|
|
429
|
+
...process.env,
|
|
430
|
+
SUPABASE_DB_PASSWORD: dbPassword,
|
|
431
|
+
// Ensure NEXT_PUBLIC_URL is available for env() substitution in config.toml
|
|
432
|
+
NEXT_PUBLIC_URL: siteUrl,
|
|
433
|
+
},
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
dbPushSpinner.stop('Database schema and config pushed successfully!');
|
|
377
437
|
}
|
|
378
438
|
} catch (error) {
|
|
379
|
-
dbPushSpinner.stop(
|
|
439
|
+
dbPushSpinner.stop(
|
|
440
|
+
'Database push failed. Please run `npx supabase db push` manually.',
|
|
441
|
+
);
|
|
380
442
|
if (error instanceof Error) {
|
|
381
443
|
clack.note(error.message);
|
|
382
444
|
}
|
|
@@ -386,7 +448,8 @@ async function runSetupWizard(projectDir, projectName) {
|
|
|
386
448
|
'Optional Cloudflare R2 Setup:\nHave your Account ID, API token (Access + Secret), bucket name, and public bucket URL handy if you want media storage ready now.',
|
|
387
449
|
);
|
|
388
450
|
const setupR2 = await clack.confirm({
|
|
389
|
-
message:
|
|
451
|
+
message:
|
|
452
|
+
'Do you want to set up Cloudflare R2 for media storage now? (Optional > populate .env keys)',
|
|
390
453
|
});
|
|
391
454
|
if (clack.isCancel(setupR2)) {
|
|
392
455
|
handleWizardCancel('Setup cancelled.');
|
|
@@ -401,14 +464,17 @@ async function runSetupWizard(projectDir, projectName) {
|
|
|
401
464
|
};
|
|
402
465
|
|
|
403
466
|
if (setupR2) {
|
|
404
|
-
clack.note(
|
|
467
|
+
clack.note(
|
|
468
|
+
'I will open your browser to the R2 dashboard.\nYou need to create a bucket and an R2 API Token.',
|
|
469
|
+
);
|
|
405
470
|
await open('https://dash.cloudflare.com/?to=/:account/r2', { wait: false });
|
|
406
471
|
|
|
407
472
|
const r2Keys = await clack.group(
|
|
408
473
|
{
|
|
409
474
|
accountId: () =>
|
|
410
475
|
clack.text({
|
|
411
|
-
message:
|
|
476
|
+
message:
|
|
477
|
+
'R2: Paste your Cloudflare Account ID (Overview > Account Details - Bottom right):',
|
|
412
478
|
validate: (val) => (!val ? 'Account ID is required' : undefined),
|
|
413
479
|
}),
|
|
414
480
|
bucketName: () =>
|
|
@@ -424,13 +490,15 @@ async function runSetupWizard(projectDir, projectName) {
|
|
|
424
490
|
secretKey: () =>
|
|
425
491
|
clack.password({
|
|
426
492
|
message: 'R2: Paste your Secret Access Key:',
|
|
427
|
-
validate: (val) =>
|
|
493
|
+
validate: (val) =>
|
|
494
|
+
!val ? 'Secret Access Key is required' : undefined,
|
|
428
495
|
}),
|
|
429
496
|
publicBaseUrl: () =>
|
|
430
497
|
clack.text({
|
|
431
498
|
message:
|
|
432
499
|
'R2: Public Base URL (Bucket > Settings > Public Development URL-Enable: e.g., https://pub-xxx.r2.dev)',
|
|
433
|
-
validate: (val) =>
|
|
500
|
+
validate: (val) =>
|
|
501
|
+
!val ? 'Public base URL is required' : undefined,
|
|
434
502
|
}),
|
|
435
503
|
},
|
|
436
504
|
{ onCancel: () => handleWizardCancel('Setup cancelled.') },
|
|
@@ -458,7 +526,9 @@ async function runSetupWizard(projectDir, projectName) {
|
|
|
458
526
|
if (setupR2) {
|
|
459
527
|
clack.note('Cloudflare R2 configuration saved!');
|
|
460
528
|
} else if (canWriteEnv) {
|
|
461
|
-
clack.note(
|
|
529
|
+
clack.note(
|
|
530
|
+
'Cloudflare R2 placeholders added to .env. Configure them later when ready.',
|
|
531
|
+
);
|
|
462
532
|
}
|
|
463
533
|
|
|
464
534
|
clack.note(
|
|
@@ -541,10 +611,14 @@ async function runSetupWizard(projectDir, projectName) {
|
|
|
541
611
|
if (setupSMTP) {
|
|
542
612
|
clack.note('SMTP configuration saved!');
|
|
543
613
|
} else if (canWriteEnv) {
|
|
544
|
-
clack.note(
|
|
614
|
+
clack.note(
|
|
615
|
+
'SMTP placeholders added to .env. Configure them later when ready.',
|
|
616
|
+
);
|
|
545
617
|
}
|
|
546
618
|
|
|
547
|
-
clack.outro(
|
|
619
|
+
clack.outro(
|
|
620
|
+
`🎉 Your NextBlock project ${projectName ? `"${projectName}" ` : ''}is ready!`,
|
|
621
|
+
);
|
|
548
622
|
}
|
|
549
623
|
|
|
550
624
|
function handleWizardCancel(message) {
|
|
@@ -560,7 +634,9 @@ async function ensureEmptyDirectory(projectDir) {
|
|
|
560
634
|
|
|
561
635
|
const contents = await fs.readdir(projectDir);
|
|
562
636
|
if (contents.length > 0) {
|
|
563
|
-
throw new Error(
|
|
637
|
+
throw new Error(
|
|
638
|
+
`Directory "${projectDir}" already exists and is not empty.`,
|
|
639
|
+
);
|
|
564
640
|
}
|
|
565
641
|
}
|
|
566
642
|
|
|
@@ -776,11 +852,14 @@ async function ensureSupabaseAssets(projectDir, options = {}) {
|
|
|
776
852
|
const destSupabaseDir = resolve(projectDir, 'supabase');
|
|
777
853
|
await fs.ensureDir(destSupabaseDir);
|
|
778
854
|
|
|
779
|
-
const { dir: packageSupabaseDir, triedPaths } =
|
|
855
|
+
const { dir: packageSupabaseDir, triedPaths } =
|
|
856
|
+
await resolvePackageSupabaseDir(projectDir);
|
|
780
857
|
if (!packageSupabaseDir) {
|
|
781
858
|
const message =
|
|
782
859
|
'Unable to locate supabase assets in @nextblock-cms/db. Please ensure dependencies are installed.' +
|
|
783
|
-
(triedPaths.length > 0
|
|
860
|
+
(triedPaths.length > 0
|
|
861
|
+
? `\nChecked:\n - ${triedPaths.join('\n - ')}`
|
|
862
|
+
: '');
|
|
784
863
|
if (required) {
|
|
785
864
|
throw new Error(message);
|
|
786
865
|
} else {
|
|
@@ -795,14 +874,20 @@ async function ensureSupabaseAssets(projectDir, options = {}) {
|
|
|
795
874
|
const sourceConfigPath = resolve(packageSupabaseDir, 'config.toml');
|
|
796
875
|
const destinationConfigPath = resolve(destSupabaseDir, 'config.toml');
|
|
797
876
|
if (await fs.pathExists(sourceConfigPath)) {
|
|
798
|
-
await fs.copy(sourceConfigPath, destinationConfigPath, {
|
|
877
|
+
await fs.copy(sourceConfigPath, destinationConfigPath, {
|
|
878
|
+
overwrite: true,
|
|
879
|
+
errorOnExist: false,
|
|
880
|
+
});
|
|
799
881
|
configCopied = true;
|
|
800
882
|
}
|
|
801
883
|
|
|
802
884
|
const sourceMigrations = resolve(packageSupabaseDir, 'migrations');
|
|
803
885
|
const destMigrations = resolve(destSupabaseDir, 'migrations');
|
|
804
886
|
if (await fs.pathExists(sourceMigrations)) {
|
|
805
|
-
await fs.copy(sourceMigrations, destMigrations, {
|
|
887
|
+
await fs.copy(sourceMigrations, destMigrations, {
|
|
888
|
+
overwrite: true,
|
|
889
|
+
errorOnExist: false,
|
|
890
|
+
});
|
|
806
891
|
migrationsCopied = true;
|
|
807
892
|
}
|
|
808
893
|
|
|
@@ -826,7 +911,12 @@ async function resolvePackageSupabaseDir(projectDir) {
|
|
|
826
911
|
const triedPaths = [];
|
|
827
912
|
const candidateBases = new Set();
|
|
828
913
|
|
|
829
|
-
const installDir = resolve(
|
|
914
|
+
const installDir = resolve(
|
|
915
|
+
projectDir,
|
|
916
|
+
'node_modules',
|
|
917
|
+
'@nextblock-cms',
|
|
918
|
+
'db',
|
|
919
|
+
);
|
|
830
920
|
candidateBases.add(installDir);
|
|
831
921
|
|
|
832
922
|
const tryResolveFrom = (fromPath) => {
|
|
@@ -880,7 +970,12 @@ async function resolvePackageSupabaseDir(projectDir) {
|
|
|
880
970
|
}
|
|
881
971
|
|
|
882
972
|
async function readSupabaseProjectRef(projectDir) {
|
|
883
|
-
const projectRefPath = resolve(
|
|
973
|
+
const projectRefPath = resolve(
|
|
974
|
+
projectDir,
|
|
975
|
+
'supabase',
|
|
976
|
+
'.temp',
|
|
977
|
+
'project-ref',
|
|
978
|
+
);
|
|
884
979
|
if (await fs.pathExists(projectRefPath)) {
|
|
885
980
|
const value = (await fs.readFile(projectRefPath, 'utf8')).trim();
|
|
886
981
|
if (/^[a-z0-9]{20,}$/i.test(value)) {
|
|
@@ -937,31 +1032,32 @@ async function ensureClientProviders(projectDir) {
|
|
|
937
1032
|
}
|
|
938
1033
|
|
|
939
1034
|
let content = await fs.readFile(providersPath, 'utf8');
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
1035
|
+
const wrapperImportStatement =
|
|
1036
|
+
"import { TranslationsProvider } from '@nextblock-cms/utils';";
|
|
1037
|
+
const existingImportRegex =
|
|
1038
|
+
/import\s+\{\s*TranslationsProvider\s*\}\s*from\s*['"]@nextblock-cms\/utils['"];?/;
|
|
1039
|
+
const legacyImportRegex =
|
|
1040
|
+
/import\s+\{\s*TranslationsProvider\s*\}\s*from\s*['"]@\/lib\/client-translations['"];?/;
|
|
1041
|
+
|
|
1042
|
+
if (existingImportRegex.test(content) || legacyImportRegex.test(content)) {
|
|
1043
|
+
content = content
|
|
1044
|
+
.replace(existingImportRegex, wrapperImportStatement)
|
|
1045
|
+
.replace(legacyImportRegex, wrapperImportStatement);
|
|
1046
|
+
} else if (!content.includes(wrapperImportStatement)) {
|
|
1047
|
+
const lines = content.split(/\r?\n/);
|
|
1048
|
+
const firstImport = lines.findIndex((line) => line.startsWith('import'));
|
|
1049
|
+
const insertIndex = firstImport === -1 ? 0 : firstImport + 1;
|
|
1050
|
+
lines.splice(insertIndex, 0, wrapperImportStatement);
|
|
1051
|
+
content = lines.join('\n');
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
await fs.writeFile(providersPath, content);
|
|
1055
|
+
|
|
1056
|
+
const wrapperPath = resolve(projectDir, 'lib/client-translations.tsx');
|
|
1057
|
+
if (await fs.pathExists(wrapperPath)) {
|
|
1058
|
+
await fs.remove(wrapperPath);
|
|
964
1059
|
}
|
|
1060
|
+
}
|
|
965
1061
|
|
|
966
1062
|
async function ensureEditorUtils(projectDir) {
|
|
967
1063
|
const exists = await fs.pathExists(EDITOR_UTILS_SOURCE_DIR);
|
|
@@ -970,7 +1066,9 @@ async function ensureEditorUtils(projectDir) {
|
|
|
970
1066
|
}
|
|
971
1067
|
|
|
972
1068
|
const entries = await fs.readdir(EDITOR_UTILS_SOURCE_DIR);
|
|
973
|
-
const utilNames = entries
|
|
1069
|
+
const utilNames = entries
|
|
1070
|
+
.filter((name) => name.endsWith('.ts'))
|
|
1071
|
+
.map((name) => name.replace(/\.ts$/, ''));
|
|
974
1072
|
|
|
975
1073
|
if (utilNames.length === 0) {
|
|
976
1074
|
return [];
|
|
@@ -989,7 +1087,10 @@ async function ensureEditorUtils(projectDir) {
|
|
|
989
1087
|
}
|
|
990
1088
|
|
|
991
1089
|
async function sanitizeBlockEditorImports(projectDir) {
|
|
992
|
-
const blockEditorPath = resolve(
|
|
1090
|
+
const blockEditorPath = resolve(
|
|
1091
|
+
projectDir,
|
|
1092
|
+
'app/cms/blocks/components/BlockEditorArea.tsx',
|
|
1093
|
+
);
|
|
993
1094
|
if (!(await fs.pathExists(blockEditorPath))) {
|
|
994
1095
|
return;
|
|
995
1096
|
}
|
|
@@ -1001,7 +1102,8 @@ async function sanitizeBlockEditorImports(projectDir) {
|
|
|
1001
1102
|
];
|
|
1002
1103
|
|
|
1003
1104
|
const updated = replacements.reduce(
|
|
1004
|
-
(current, { pattern, replacement }) =>
|
|
1105
|
+
(current, { pattern, replacement }) =>
|
|
1106
|
+
current.replace(pattern, replacement),
|
|
1005
1107
|
content,
|
|
1006
1108
|
);
|
|
1007
1109
|
|
|
@@ -1024,7 +1126,10 @@ async function sanitizeUiImports(projectDir) {
|
|
|
1024
1126
|
|
|
1025
1127
|
for (const filePath of files) {
|
|
1026
1128
|
const original = await fs.readFile(filePath, 'utf8');
|
|
1027
|
-
const updated = original.replace(
|
|
1129
|
+
const updated = original.replace(
|
|
1130
|
+
/@nextblock-cms\/ui\/(?!styles\/)[A-Za-z0-9/_-]+/g,
|
|
1131
|
+
'@nextblock-cms/ui',
|
|
1132
|
+
);
|
|
1028
1133
|
if (updated !== original) {
|
|
1029
1134
|
await fs.writeFile(filePath, updated);
|
|
1030
1135
|
}
|
|
@@ -1078,16 +1183,12 @@ async function sanitizeLayout(projectDir) {
|
|
|
1078
1183
|
];
|
|
1079
1184
|
|
|
1080
1185
|
const content = await fs.readFile(layoutPath, 'utf8');
|
|
1081
|
-
let updated = content.replace(
|
|
1082
|
-
|
|
1083
|
-
'',
|
|
1084
|
-
);
|
|
1085
|
-
updated = updated.replace(
|
|
1086
|
-
/import\s+['"]\.\/editor\.css['"];?\s*/g,
|
|
1087
|
-
'',
|
|
1088
|
-
);
|
|
1186
|
+
let updated = content.replace(/import\s+['"]\.\/globals\.css['"];?\s*/g, '');
|
|
1187
|
+
updated = updated.replace(/import\s+['"]\.\/editor\.css['"];?\s*/g, '');
|
|
1089
1188
|
|
|
1090
|
-
const missingImports = requiredImports.filter(
|
|
1189
|
+
const missingImports = requiredImports.filter(
|
|
1190
|
+
(statement) => !updated.includes(statement),
|
|
1191
|
+
);
|
|
1091
1192
|
if (missingImports.length > 0) {
|
|
1092
1193
|
updated = `${missingImports.join('\n')}\n${updated}`;
|
|
1093
1194
|
}
|
|
@@ -1125,7 +1226,7 @@ async function ensureEditorStyles(projectDir) {
|
|
|
1125
1226
|
if (
|
|
1126
1227
|
content === '' ||
|
|
1127
1228
|
content.startsWith('/* Editor styles placeholder') ||
|
|
1128
|
-
content.includes(
|
|
1229
|
+
content.includes('@nextblock-cms/editor/styles')
|
|
1129
1230
|
) {
|
|
1130
1231
|
await fs.remove(filePath);
|
|
1131
1232
|
}
|
|
@@ -1319,7 +1420,9 @@ async function transformPackageJson(projectDir) {
|
|
|
1319
1420
|
|
|
1320
1421
|
packageJson.dependencies = packageJson.dependencies ?? {};
|
|
1321
1422
|
|
|
1322
|
-
for (const [pkgName, manifestPath] of Object.entries(
|
|
1423
|
+
for (const [pkgName, manifestPath] of Object.entries(
|
|
1424
|
+
PACKAGE_VERSION_SOURCES,
|
|
1425
|
+
)) {
|
|
1323
1426
|
if (pkgName in packageJson.dependencies) {
|
|
1324
1427
|
const current = packageJson.dependencies[pkgName];
|
|
1325
1428
|
if (typeof current === 'string' && current.startsWith('workspace:')) {
|
|
@@ -1410,7 +1513,9 @@ async function runSupabaseCli(args, options = {}) {
|
|
|
1410
1513
|
if (code === 0) {
|
|
1411
1514
|
resolve();
|
|
1412
1515
|
} else {
|
|
1413
|
-
reject(
|
|
1516
|
+
reject(
|
|
1517
|
+
new Error(`supabase ${args.join(' ')} exited with code ${code}`),
|
|
1518
|
+
);
|
|
1414
1519
|
}
|
|
1415
1520
|
});
|
|
1416
1521
|
});
|
|
@@ -1431,7 +1536,11 @@ function buildNextConfigContent(editorUtilNames) {
|
|
|
1431
1536
|
|
|
1432
1537
|
for (const moduleName of UI_PROXY_MODULES) {
|
|
1433
1538
|
aliasLines.push(
|
|
1434
|
-
" '@nextblock-cms/ui/" +
|
|
1539
|
+
" '@nextblock-cms/ui/" +
|
|
1540
|
+
moduleName +
|
|
1541
|
+
"': path.join(process.cwd(), 'lib/ui/" +
|
|
1542
|
+
moduleName +
|
|
1543
|
+
"'),",
|
|
1435
1544
|
);
|
|
1436
1545
|
}
|
|
1437
1546
|
|
|
@@ -1455,17 +1564,17 @@ function buildNextConfigContent(editorUtilNames) {
|
|
|
1455
1564
|
" * @type {import('next').NextConfig}",
|
|
1456
1565
|
' **/',
|
|
1457
1566
|
'const nextConfig = {',
|
|
1458
|
-
|
|
1567
|
+
' outputFileTracingRoot: path.join(__dirname),',
|
|
1459
1568
|
' env: {',
|
|
1460
|
-
|
|
1461
|
-
|
|
1569
|
+
' NEXT_PUBLIC_SUPABASE_URL: process.env.NEXT_PUBLIC_SUPABASE_URL,',
|
|
1570
|
+
' NEXT_PUBLIC_SUPABASE_ANON_KEY: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,',
|
|
1462
1571
|
' },',
|
|
1463
1572
|
' images: {',
|
|
1464
1573
|
" formats: ['image/avif', 'image/webp'],",
|
|
1465
1574
|
' imageSizes: [16, 32, 48, 64, 96, 128, 256, 384, 512],',
|
|
1466
1575
|
' deviceSizes: [320, 480, 640, 750, 828, 1080, 1200, 1440, 1920, 2048, 2560],',
|
|
1467
1576
|
' minimumCacheTTL: 31536000,',
|
|
1468
|
-
|
|
1577
|
+
' dangerouslyAllowSVG: false,',
|
|
1469
1578
|
" contentSecurityPolicy: \"default-src 'self'; script-src 'none'; sandbox;\",",
|
|
1470
1579
|
' remotePatterns: [',
|
|
1471
1580
|
" { protocol: 'https', hostname: 'pub-a31e3f1a87d144898aeb489a8221f92e.r2.dev' },",
|
|
@@ -1474,14 +1583,14 @@ function buildNextConfigContent(editorUtilNames) {
|
|
|
1474
1583
|
' ? [',
|
|
1475
1584
|
' {',
|
|
1476
1585
|
" protocol: /** @type {'http' | 'https'} */ (new URL(process.env.NEXT_PUBLIC_URL).protocol.slice(0, -1)),",
|
|
1477
|
-
|
|
1586
|
+
' hostname: new URL(process.env.NEXT_PUBLIC_URL).hostname,',
|
|
1478
1587
|
' },',
|
|
1479
1588
|
' ]',
|
|
1480
1589
|
' : []),',
|
|
1481
1590
|
' ],',
|
|
1482
1591
|
' },',
|
|
1483
1592
|
' experimental: {',
|
|
1484
|
-
|
|
1593
|
+
' optimizeCss: true,',
|
|
1485
1594
|
" cssChunking: 'strict',",
|
|
1486
1595
|
' },',
|
|
1487
1596
|
" transpilePackages: ['@nextblock-cms/utils', '@nextblock-cms/ui', '@nextblock-cms/editor'],",
|
|
@@ -1519,8 +1628,8 @@ function buildNextConfigContent(editorUtilNames) {
|
|
|
1519
1628
|
' config.module = config.module || {};',
|
|
1520
1629
|
' config.module.rules = config.module.rules || [];',
|
|
1521
1630
|
' config.module.rules.push({',
|
|
1522
|
-
|
|
1523
|
-
|
|
1631
|
+
' test: /\\.svg$/i,',
|
|
1632
|
+
' issuer: /\\.[jt]sx?$/,',
|
|
1524
1633
|
" use: ['@svgr/webpack'],",
|
|
1525
1634
|
' });',
|
|
1526
1635
|
'',
|
|
@@ -1531,14 +1640,14 @@ function buildNextConfigContent(editorUtilNames) {
|
|
|
1531
1640
|
' cacheGroups: {',
|
|
1532
1641
|
' ...(((config.optimization ?? {}).splitChunks ?? {}).cacheGroups ?? {}),',
|
|
1533
1642
|
' tiptap: {',
|
|
1534
|
-
|
|
1643
|
+
' test: /[\\\\/]node_modules[\\\\/](@tiptap|prosemirror)[\\\\/]/,',
|
|
1535
1644
|
" name: 'tiptap',",
|
|
1536
1645
|
" chunks: 'async',",
|
|
1537
1646
|
' priority: 30,',
|
|
1538
1647
|
' reuseExistingChunk: true,',
|
|
1539
1648
|
' },',
|
|
1540
1649
|
' tiptapExtensions: {',
|
|
1541
|
-
|
|
1650
|
+
' test: /[\\\\/](tiptap-extensions|RichTextEditor|MenuBar|MediaLibraryModal)[\\\\/]/,',
|
|
1542
1651
|
" name: 'tiptap-extensions',",
|
|
1543
1652
|
" chunks: 'async',",
|
|
1544
1653
|
' priority: 25,',
|
package/package.json
CHANGED
|
@@ -31,7 +31,7 @@ export default function Login() {
|
|
|
31
31
|
const formMessage = getMessage(searchParams);
|
|
32
32
|
|
|
33
33
|
return (
|
|
34
|
-
<form className="flex-1 flex flex-col min-w-64">
|
|
34
|
+
<form className="flex-1 flex flex-col min-w-64 mx-auto">
|
|
35
35
|
<h1 className="text-2xl font-medium">{t('sign_in')}</h1>
|
|
36
36
|
<p className="text-sm text-foreground">
|
|
37
37
|
{t('dont_have_account')}{" "}
|
|
@@ -10,6 +10,8 @@ export const signUpAction = async (formData: FormData) => {
|
|
|
10
10
|
const password = formData.get("password")?.toString();
|
|
11
11
|
const supabase = await createClient();
|
|
12
12
|
const origin = (await headers()).get("origin");
|
|
13
|
+
const nextPublicUrl = process.env.NEXT_PUBLIC_URL;
|
|
14
|
+
const redirectBase = nextPublicUrl ? `https://${nextPublicUrl}` : origin;
|
|
13
15
|
|
|
14
16
|
if (!email || !password) {
|
|
15
17
|
return encodedRedirect(
|
|
@@ -23,7 +25,7 @@ export const signUpAction = async (formData: FormData) => {
|
|
|
23
25
|
email,
|
|
24
26
|
password,
|
|
25
27
|
options: {
|
|
26
|
-
emailRedirectTo: `${
|
|
28
|
+
emailRedirectTo: `${redirectBase}/auth/callback`,
|
|
27
29
|
},
|
|
28
30
|
});
|
|
29
31
|
|
|
@@ -72,6 +74,8 @@ export const forgotPasswordAction = async (formData: FormData) => {
|
|
|
72
74
|
const email = formData.get("email")?.toString();
|
|
73
75
|
const supabase = await createClient();
|
|
74
76
|
const origin = (await headers()).get("origin");
|
|
77
|
+
const nextPublicUrl = process.env.NEXT_PUBLIC_URL;
|
|
78
|
+
const redirectBase = nextPublicUrl ? `https://${nextPublicUrl}` : origin;
|
|
75
79
|
const callbackUrl = formData.get("callbackUrl")?.toString();
|
|
76
80
|
|
|
77
81
|
if (!email) {
|
|
@@ -79,7 +83,7 @@ export const forgotPasswordAction = async (formData: FormData) => {
|
|
|
79
83
|
}
|
|
80
84
|
|
|
81
85
|
const { error } = await supabase.auth.resetPasswordForEmail(email, {
|
|
82
|
-
redirectTo: `${
|
|
86
|
+
redirectTo: `${redirectBase}/auth/callback?redirect_to=/reset-password`,
|
|
83
87
|
});
|
|
84
88
|
|
|
85
89
|
if (error) {
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import { useState, useEffect, type ComponentType, Suspense, LazyExoticComponent } from "react";
|
|
1
|
+
import { useState, useEffect, type ComponentType, Suspense, LazyExoticComponent, useCallback } from "react";
|
|
4
2
|
import { cn } from "@nextblock-cms/utils";
|
|
5
3
|
import {
|
|
6
4
|
Dialog,
|
|
7
5
|
DialogContent,
|
|
8
6
|
DialogTitle,
|
|
9
|
-
|
|
7
|
+
DialogHeader,
|
|
8
|
+
DialogFooter,
|
|
9
|
+
DialogDescription,
|
|
10
10
|
} from "@nextblock-cms/ui";
|
|
11
11
|
import { Button } from "@nextblock-cms/ui";
|
|
12
12
|
import { blockRegistry, type BlockType } from "@/lib/blocks/blockRegistry";
|
|
@@ -46,12 +46,14 @@ export function BlockEditorModal({
|
|
|
46
46
|
sectionBackground,
|
|
47
47
|
}: BlockEditorModalProps) {
|
|
48
48
|
const [tempContent, setTempContent] = useState(block.content);
|
|
49
|
+
const [showConfirmClose, setShowConfirmClose] = useState(false);
|
|
49
50
|
const isValid = true; // Placeholder for future validation logic
|
|
50
51
|
|
|
51
52
|
useEffect(() => {
|
|
52
53
|
// When the modal is opened with a new block, reset the temp content
|
|
53
54
|
if (isOpen) {
|
|
54
55
|
setTempContent(block.content);
|
|
56
|
+
setShowConfirmClose(false);
|
|
55
57
|
}
|
|
56
58
|
}, [isOpen, block.content]);
|
|
57
59
|
|
|
@@ -59,6 +61,18 @@ export function BlockEditorModal({
|
|
|
59
61
|
onSave(tempContent);
|
|
60
62
|
};
|
|
61
63
|
|
|
64
|
+
const hasUnsavedChanges = useCallback(() => {
|
|
65
|
+
return JSON.stringify(block.content) !== JSON.stringify(tempContent);
|
|
66
|
+
}, [block.content, tempContent]);
|
|
67
|
+
|
|
68
|
+
const handleCloseAttempt = useCallback(() => {
|
|
69
|
+
if (hasUnsavedChanges()) {
|
|
70
|
+
setShowConfirmClose(true);
|
|
71
|
+
} else {
|
|
72
|
+
onClose();
|
|
73
|
+
}
|
|
74
|
+
}, [hasUnsavedChanges, onClose]);
|
|
75
|
+
|
|
62
76
|
const handleContentChange = (newContent: unknown) => {
|
|
63
77
|
setTempContent(newContent);
|
|
64
78
|
// Potentially add validation here and set isValid
|
|
@@ -68,70 +82,107 @@ export function BlockEditorModal({
|
|
|
68
82
|
const displayText = blockInfo?.label || "Block";
|
|
69
83
|
|
|
70
84
|
return (
|
|
71
|
-
|
|
72
|
-
<
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
85
|
+
<>
|
|
86
|
+
<Dialog open={isOpen} onOpenChange={(open) => {
|
|
87
|
+
if (!open) {
|
|
88
|
+
handleCloseAttempt();
|
|
89
|
+
}
|
|
90
|
+
}}>
|
|
91
|
+
<DialogContent
|
|
92
|
+
className="max-w-6xl h-[90vh] flex flex-col p-0 gap-0 overflow-hidden"
|
|
93
|
+
onInteractOutside={(e) => {
|
|
94
|
+
e.preventDefault();
|
|
95
|
+
handleCloseAttempt();
|
|
96
|
+
}}
|
|
97
|
+
onEscapeKeyDown={(e) => {
|
|
98
|
+
e.preventDefault();
|
|
99
|
+
handleCloseAttempt();
|
|
100
|
+
}}
|
|
101
|
+
>
|
|
102
|
+
{/* Header */}
|
|
103
|
+
<div className="flex items-center justify-between p-4 border-b bg-background/95 backdrop-blur z-10">
|
|
104
|
+
<div className="flex items-center gap-2">
|
|
105
|
+
<DialogTitle className="text-lg font-semibold">Edit {displayText}</DialogTitle>
|
|
106
|
+
</div>
|
|
107
|
+
<div className="flex items-center gap-2">
|
|
108
|
+
<Button variant="ghost" size="sm" onClick={handleCloseAttempt}>Cancel</Button>
|
|
109
|
+
<Button onClick={handleSave} disabled={!isValid} size="sm">
|
|
110
|
+
Save (CMD+S)
|
|
111
|
+
</Button>
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
|
|
115
|
+
{/* Editor Area with Contextual Background */}
|
|
116
|
+
<div
|
|
117
|
+
className={cn(
|
|
118
|
+
"flex-1 overflow-y-auto p-6",
|
|
119
|
+
// Conditional Background Logic:
|
|
120
|
+
// Only apply specific section background to 'text' and 'heading' blocks to allow "Live Preview" of copy.
|
|
121
|
+
// For complex blocks like Forms, Buttons, etc., keep a neutral background to ensure input field contrast.
|
|
122
|
+
(block.type === 'text' || block.type === 'heading') ? (
|
|
123
|
+
// If no specific background, use white/dark default
|
|
124
|
+
(!sectionBackground || sectionBackground.type === 'none') && "bg-muted/10"
|
|
125
|
+
) : "bg-muted/10", // Default for non-text blocks
|
|
92
126
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
127
|
+
// Apply theme classes if present (ONLY for text/heading)
|
|
128
|
+
(block.type === 'text' || block.type === 'heading') && sectionBackground?.type === 'theme' && sectionBackground.theme === 'primary' && 'bg-primary text-primary-foreground',
|
|
129
|
+
(block.type === 'text' || block.type === 'heading') && sectionBackground?.type === 'theme' && sectionBackground.theme === 'secondary' && 'bg-secondary text-secondary-foreground',
|
|
130
|
+
(block.type === 'text' || block.type === 'heading') && sectionBackground?.type === 'theme' && sectionBackground.theme === 'muted' && 'bg-muted text-muted-foreground',
|
|
131
|
+
|
|
132
|
+
// Dark mode prose invert if dark background (approximate check for solid color)
|
|
133
|
+
(block.type === 'text' || block.type === 'heading') && (sectionBackground?.type === 'solid' && sectionBackground.solid_color && ['#000', '#111', '#0f172a', 'black'].some(c => sectionBackground.solid_color?.includes(c))) && "[&_.prose]:prose-invert"
|
|
134
|
+
)}
|
|
135
|
+
style={{
|
|
136
|
+
// Only apply custom color/gradient styles for text/heading
|
|
137
|
+
backgroundColor: (block.type === 'text' || block.type === 'heading') && sectionBackground?.type === 'solid' ? sectionBackground.solid_color : undefined,
|
|
138
|
+
backgroundImage: (block.type === 'text' || block.type === 'heading') && sectionBackground?.type === 'gradient' && sectionBackground.gradient ?
|
|
139
|
+
`${sectionBackground.gradient.type}-gradient(${sectionBackground.gradient.direction}, ${sectionBackground.gradient.stops.map(s => `${s.color} ${s.position}%`).join(', ')})`
|
|
140
|
+
: undefined
|
|
141
|
+
}}
|
|
142
|
+
>
|
|
143
|
+
<div className="max-w-6xl mx-auto">
|
|
144
|
+
<Suspense fallback={<div className="flex justify-center items-center h-32">Loading editor...</div>}>
|
|
145
|
+
<EditorComponent
|
|
146
|
+
block={block}
|
|
147
|
+
content={tempContent}
|
|
148
|
+
onChange={handleContentChange}
|
|
149
|
+
className="bg-transparent border-none shadow-none focus-within:ring-0 min-h-[60vh]" // Make editor transparent
|
|
150
|
+
sectionBackground={sectionBackground} // Pass down if editor supports it
|
|
151
|
+
/>
|
|
152
|
+
</Suspense>
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
|
|
156
|
+
</DialogContent>
|
|
157
|
+
</Dialog>
|
|
104
158
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
</DialogContent>
|
|
135
|
-
</Dialog>
|
|
159
|
+
<Dialog open={showConfirmClose} onOpenChange={setShowConfirmClose}>
|
|
160
|
+
<DialogContent className="sm:max-w-[425px]">
|
|
161
|
+
<DialogHeader>
|
|
162
|
+
<DialogTitle>Save Changes?</DialogTitle>
|
|
163
|
+
<DialogDescription>
|
|
164
|
+
You have unsaved changes. Do you want to save them before closing?
|
|
165
|
+
</DialogDescription>
|
|
166
|
+
</DialogHeader>
|
|
167
|
+
<DialogFooter className="gap-2 sm:gap-0">
|
|
168
|
+
<Button variant="ghost" onClick={() => setShowConfirmClose(false)}>
|
|
169
|
+
Cancel
|
|
170
|
+
</Button>
|
|
171
|
+
<Button variant="destructive" onClick={() => {
|
|
172
|
+
setShowConfirmClose(false);
|
|
173
|
+
onClose();
|
|
174
|
+
}}>
|
|
175
|
+
Discard
|
|
176
|
+
</Button>
|
|
177
|
+
<Button onClick={() => {
|
|
178
|
+
setShowConfirmClose(false);
|
|
179
|
+
handleSave();
|
|
180
|
+
}}>
|
|
181
|
+
Save
|
|
182
|
+
</Button>
|
|
183
|
+
</DialogFooter>
|
|
184
|
+
</DialogContent>
|
|
185
|
+
</Dialog>
|
|
186
|
+
</>
|
|
136
187
|
);
|
|
137
188
|
}
|
|
@@ -17,9 +17,7 @@ import { getTranslations } from '@/app/cms/settings/extra-translations/actions';
|
|
|
17
17
|
import type { Database } from '@nextblock-cms/db';
|
|
18
18
|
import { headers, cookies } from 'next/headers';
|
|
19
19
|
|
|
20
|
-
const defaultUrl = process.env.NEXT_PUBLIC_URL
|
|
21
|
-
? `https://${process.env.NEXT_PUBLIC_URL}`
|
|
22
|
-
: "http://localhost:3000";
|
|
20
|
+
const defaultUrl = process.env.NEXT_PUBLIC_URL || "http://localhost:3000";
|
|
23
21
|
|
|
24
22
|
const DEFAULT_LOCALE_FOR_LAYOUT = 'en';
|
|
25
23
|
|
|
@@ -106,6 +104,28 @@ export const metadata: Metadata = {
|
|
|
106
104
|
metadataBase: new URL(defaultUrl),
|
|
107
105
|
title: 'Nextblock CMS',
|
|
108
106
|
description: 'Nextblock CMS pairs a visual block editor with a blazing-fast Next.js + Supabase architecture.',
|
|
107
|
+
openGraph: {
|
|
108
|
+
title: 'Nextblock CMS',
|
|
109
|
+
description: 'Nextblock CMS pairs a visual block editor with a blazing-fast Next.js + Supabase architecture.',
|
|
110
|
+
url: defaultUrl,
|
|
111
|
+
siteName: 'Nextblock CMS',
|
|
112
|
+
images: [
|
|
113
|
+
{
|
|
114
|
+
url: '/images/metadata_image.webp',
|
|
115
|
+
width: 1200,
|
|
116
|
+
height: 630,
|
|
117
|
+
alt: 'Nextblock CMS',
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
locale: 'en_US',
|
|
121
|
+
type: 'website',
|
|
122
|
+
},
|
|
123
|
+
twitter: {
|
|
124
|
+
card: 'summary_large_image',
|
|
125
|
+
title: 'Nextblock CMS',
|
|
126
|
+
description: 'Nextblock CMS pairs a visual block editor with a blazing-fast Next.js + Supabase architecture.',
|
|
127
|
+
images: ['/images/metadata_image.webp'],
|
|
128
|
+
},
|
|
109
129
|
icons: {
|
|
110
130
|
icon: [
|
|
111
131
|
{ url: '/favicon/favicon.ico' },
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/// <reference types="next" />
|
|
2
2
|
/// <reference types="next/image-types/global" />
|
|
3
|
-
import "./.next/types/routes.d.ts";
|
|
3
|
+
import "./.next/dev/types/routes.d.ts";
|
|
4
4
|
|
|
5
5
|
// NOTE: This file should not be edited
|
|
6
6
|
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nextblock-cms/template",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.37",
|
|
4
4
|
"private": true,
|
|
5
5
|
"scripts": {
|
|
6
6
|
"dev": "next dev",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"js-cookie": "^3.0.5",
|
|
25
25
|
"lodash.debounce": "^4.0.8",
|
|
26
26
|
"lucide-react": "^0.534.0",
|
|
27
|
-
"next": "
|
|
27
|
+
"next": "15.5.9",
|
|
28
28
|
"nodemailer": "^7.0.4",
|
|
29
29
|
"plaiceholder": "^3.0.0",
|
|
30
30
|
"react": "19.0.0",
|
|
@@ -189,14 +189,37 @@ export default async function proxy(request: NextRequest) {
|
|
|
189
189
|
if (process.env.NODE_ENV === 'production') {
|
|
190
190
|
const nonceValue = requestHeaders.get('x-nonce');
|
|
191
191
|
if (nonceValue) {
|
|
192
|
+
const supabaseHostname = new URL(supabaseUrl).hostname;
|
|
193
|
+
|
|
194
|
+
const r2BaseUrl = process.env.NEXT_PUBLIC_R2_BASE_URL;
|
|
195
|
+
const r2PublicUrl = process.env.NEXT_PUBLIC_R2_PUBLIC_URL;
|
|
196
|
+
const r2BucketName = process.env.R2_BUCKET_NAME;
|
|
197
|
+
|
|
198
|
+
let r2Hostnames = '';
|
|
199
|
+
if (r2BaseUrl) {
|
|
200
|
+
try {
|
|
201
|
+
r2Hostnames += ` https://${new URL(r2BaseUrl).hostname}`;
|
|
202
|
+
} catch (e) {
|
|
203
|
+
console.error('Invalid NEXT_PUBLIC_R2_BASE_URL', e);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (r2PublicUrl && r2BucketName) {
|
|
207
|
+
try {
|
|
208
|
+
const publicHostname = new URL(r2PublicUrl).hostname;
|
|
209
|
+
r2Hostnames += ` https://${r2BucketName}.${publicHostname}`;
|
|
210
|
+
} catch (e) {
|
|
211
|
+
console.error('Invalid NEXT_PUBLIC_R2_PUBLIC_URL', e);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
192
215
|
const csp = [
|
|
193
216
|
"default-src 'self'",
|
|
194
217
|
`script-src 'self' blob: data: 'nonce-${nonceValue}'`,
|
|
195
218
|
"style-src 'self' 'unsafe-inline'",
|
|
196
|
-
|
|
219
|
+
`img-src 'self' data: blob:${r2Hostnames}`,
|
|
197
220
|
"font-src 'self'",
|
|
198
221
|
"object-src 'none'",
|
|
199
|
-
|
|
222
|
+
`connect-src 'self' https://${supabaseHostname} wss://${supabaseHostname}${r2Hostnames}`,
|
|
200
223
|
"frame-src 'self' blob: data: https://www.youtube.com",
|
|
201
224
|
"form-action 'self'",
|
|
202
225
|
"base-uri 'self'",
|
|
Binary file
|