create-nextblock 0.0.4 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/bin/create-nextblock.js +1193 -920
  2. package/package.json +6 -2
  3. package/scripts/sync-template.js +279 -276
  4. package/templates/nextblock-template/.env.example +1 -14
  5. package/templates/nextblock-template/README.md +1 -1
  6. package/templates/nextblock-template/app/[slug]/PageClientContent.tsx +67 -40
  7. package/templates/nextblock-template/app/[slug]/page.tsx +45 -10
  8. package/templates/nextblock-template/app/[slug]/page.utils.ts +92 -45
  9. package/templates/nextblock-template/app/api/revalidate/route.ts +15 -15
  10. package/templates/nextblock-template/app/{blog → article}/[slug]/PostClientContent.tsx +45 -43
  11. package/templates/nextblock-template/app/{blog → article}/[slug]/page.tsx +108 -98
  12. package/templates/nextblock-template/app/{blog → article}/[slug]/page.utils.ts +10 -3
  13. package/templates/nextblock-template/app/cms/CmsClientLayout.tsx +25 -19
  14. package/templates/nextblock-template/app/cms/blocks/actions.ts +1 -1
  15. package/templates/nextblock-template/app/cms/posts/[id]/edit/page.tsx +1 -1
  16. package/templates/nextblock-template/app/cms/posts/actions.ts +47 -44
  17. package/templates/nextblock-template/app/cms/posts/page.tsx +2 -2
  18. package/templates/nextblock-template/app/cms/settings/languages/actions.ts +16 -15
  19. package/templates/nextblock-template/app/layout.tsx +9 -9
  20. package/templates/nextblock-template/app/lib/sitemap-utils.ts +52 -52
  21. package/templates/nextblock-template/app/sitemap.xml/route.ts +2 -2
  22. package/templates/nextblock-template/components/ResponsiveNav.tsx +22 -16
  23. package/templates/nextblock-template/components/blocks/PostsGridBlock.tsx +12 -7
  24. package/templates/nextblock-template/components/blocks/PostsGridClient.tsx +25 -26
  25. package/templates/nextblock-template/package.json +1 -1
  26. package/templates/nextblock-template/proxy.ts +4 -4
  27. package/templates/nextblock-template/public/images/NBcover.webp +0 -0
  28. package/templates/nextblock-template/public/images/developer.webp +0 -0
  29. package/templates/nextblock-template/public/images/nextblock-logo-small.webp +0 -0
  30. package/templates/nextblock-template/public/images/nx-graph.webp +0 -0
  31. package/templates/nextblock-template/public/images/programmer-upscaled.webp +0 -0
  32. package/templates/nextblock-template/scripts/backup.js +142 -47
  33. package/templates/nextblock-template/scripts/restore-working.js +102 -0
  34. package/templates/nextblock-template/scripts/restore.js +434 -0
  35. package/templates/nextblock-template/app/blog/page.tsx +0 -77
  36. package/templates/nextblock-template/backup/backup_2025-06-19.sql +0 -8057
  37. package/templates/nextblock-template/backup/backup_2025-06-20.sql +0 -8159
  38. package/templates/nextblock-template/backup/backup_2025-07-08.sql +0 -8411
  39. package/templates/nextblock-template/backup/backup_2025-07-09.sql +0 -8442
  40. package/templates/nextblock-template/backup/backup_2025-07-10.sql +0 -8442
  41. package/templates/nextblock-template/backup/backup_2025-10-01.sql +0 -8803
  42. package/templates/nextblock-template/backup/backup_2025-10-02.sql +0 -9749
@@ -0,0 +1,434 @@
1
+ // apps/nextblock/scripts/restore.js
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const { spawn } = require('child_process');
5
+ const readline = require('readline');
6
+
7
+ function createPrefixSuppressor(regexes, label) {
8
+ return (line) => {
9
+ for (const regex of regexes) {
10
+ if (regex.test(line)) {
11
+ return label;
12
+ }
13
+ }
14
+ return null;
15
+ };
16
+ }
17
+
18
+ const stdoutSuppressors = [
19
+ createPrefixSuppressor([/^SET\b/i, /^RESET\b/i], 'SET/RESET statements'),
20
+ createPrefixSuppressor([/^CREATE\b/i], 'CREATE statements'),
21
+ createPrefixSuppressor([/^ALTER\b/i], 'ALTER statements'),
22
+ createPrefixSuppressor([/^DROP\b/i], 'DROP statements'),
23
+ createPrefixSuppressor([/^COMMENT\b/i], 'COMMENT statements'),
24
+ createPrefixSuppressor([/^GRANT\b/i], 'GRANT statements'),
25
+ createPrefixSuppressor([/^REVOKE\b/i], 'REVOKE statements'),
26
+ createPrefixSuppressor([/^COPY\b/i], 'COPY statements'),
27
+ createPrefixSuppressor([/^SELECT\s+pg_catalog\.setval/i, /^\s*setval\b/i], 'setval outputs'),
28
+ createPrefixSuppressor([/^\(\d+\s+rows?\)$/i, /^\(1 row\)$/i], 'row count outputs'),
29
+ createPrefixSuppressor([/^\s*set_config\b/i], 'set_config outputs'),
30
+ createPrefixSuppressor([/^\s*-{3,}\s*$/], 'table separators'),
31
+ createPrefixSuppressor([/^\s*\d+\s*$/], 'numeric result lines'),
32
+ ];
33
+
34
+ function extractMultiplier(line) {
35
+ const match = line.match(/\sx(\d+)$/i);
36
+ return match ? parseInt(match[1], 10) : 1;
37
+ }
38
+
39
+ function createDuplicateCollapser(outputFn, options = {}) {
40
+ const suppressors = options.suppressors || [];
41
+ const suppressedCounts = new Map();
42
+ let buffer = '';
43
+ let lastLine = null;
44
+ let repeatCount = 0;
45
+ let finished = false;
46
+
47
+ const flush = () => {
48
+ if (lastLine === null) {
49
+ return;
50
+ }
51
+ const baseLine = lastLine;
52
+ const multiplier = extractMultiplier(baseLine);
53
+ const occurrences = repeatCount * multiplier;
54
+
55
+ for (const suppressor of suppressors) {
56
+ const label = suppressor(baseLine);
57
+ if (label) {
58
+ suppressedCounts.set(label, (suppressedCounts.get(label) || 0) + occurrences);
59
+ lastLine = null;
60
+ repeatCount = 0;
61
+ return;
62
+ }
63
+ }
64
+
65
+ const line =
66
+ repeatCount > 1 ? `${baseLine} x${repeatCount}` : baseLine;
67
+ outputFn(line);
68
+ lastLine = null;
69
+ repeatCount = 0;
70
+ };
71
+
72
+ const handleLine = (line) => {
73
+ const normalized = line.replace(/\r$/, '');
74
+ if (!normalized.trim()) {
75
+ return;
76
+ }
77
+ if (lastLine === null) {
78
+ lastLine = normalized;
79
+ repeatCount = 1;
80
+ return;
81
+ }
82
+ if (normalized === lastLine) {
83
+ repeatCount += 1;
84
+ return;
85
+ }
86
+ flush();
87
+ lastLine = normalized;
88
+ repeatCount = 1;
89
+ };
90
+
91
+ return {
92
+ write(chunk) {
93
+ if (finished) {
94
+ return;
95
+ }
96
+ buffer += chunk.toString();
97
+ let newlineIndex;
98
+ while ((newlineIndex = buffer.indexOf('\n')) !== -1) {
99
+ const line = buffer.slice(0, newlineIndex);
100
+ handleLine(line);
101
+ buffer = buffer.slice(newlineIndex + 1);
102
+ }
103
+ },
104
+ finish() {
105
+ if (finished) {
106
+ return suppressedCounts;
107
+ }
108
+ if (buffer.length > 0) {
109
+ handleLine(buffer);
110
+ buffer = '';
111
+ }
112
+ flush();
113
+ finished = true;
114
+ return suppressedCounts;
115
+ },
116
+ };
117
+ }
118
+
119
+ function createErrorFilter(outputFn) {
120
+ let buffer = '';
121
+ const suppressedCounts = new Map();
122
+ let finished = false;
123
+
124
+ const ignorePatterns = [
125
+ {
126
+ regex: /ERROR:\s+permission denied to change default privileges/i,
127
+ label: 'permission denied to change default privileges',
128
+ },
129
+ {
130
+ regex: /ERROR:\s+Non-superuser owned event trigger must execute a non-superuser owned function/i,
131
+ label: 'event trigger requires non-superuser function',
132
+ },
133
+ {
134
+ regex: /DETAIL:\s+The current user "postgres" is not a superuser and the function "extensions\.[^"]+" is owned by a superuser/i,
135
+ },
136
+ {
137
+ regex: /ERROR:\s+cannot drop function/i,
138
+ label: 'function drop dependency issues',
139
+ },
140
+ {
141
+ regex: /ERROR:\s+cannot drop schema/i,
142
+ label: 'schema drop dependency issues',
143
+ },
144
+ {
145
+ regex: /ERROR:\s+schema "[^"]+" already exists/i,
146
+ label: 'schema exists warnings',
147
+ },
148
+ {
149
+ regex: /ERROR:\s+type "[^"]+" already exists/i,
150
+ label: 'type exists warnings',
151
+ },
152
+ {
153
+ regex: /ERROR:\s+function "[^"]+" already exists/i,
154
+ label: 'function exists warnings',
155
+ },
156
+ {
157
+ regex: /ERROR:\s+relation "[^"]+" already exists/i,
158
+ label: 'relation exists warnings',
159
+ },
160
+ {
161
+ regex: /ERROR:\s+permission denied for schema/i,
162
+ label: 'schema permission warnings',
163
+ },
164
+ {
165
+ regex: /^(event trigger\s+|function\s+extensions\.)/i,
166
+ label: 'dependency detail lines',
167
+ },
168
+ {
169
+ regex: /ERROR:\s+must be able to SET ROLE/i,
170
+ label: 'SET ROLE permission issues',
171
+ },
172
+ {
173
+ regex: /ERROR:\s+must be owner of/i,
174
+ label: 'ownership warnings',
175
+ },
176
+ {
177
+ regex: /ERROR:\s+permission denied for table/i,
178
+ label: 'table permission warnings',
179
+ },
180
+ {
181
+ regex: /ERROR:\s+duplicate key value violates unique constraint/i,
182
+ label: 'duplicate key conflicts',
183
+ },
184
+ {
185
+ regex: /ERROR:\s+insert or update on table .* violates foreign key constraint/i,
186
+ label: 'foreign key conflicts',
187
+ },
188
+ {
189
+ regex: /ERROR:\s+grant options cannot be granted back to your own grantor/i,
190
+ label: 'grant option warnings',
191
+ },
192
+ {
193
+ regex: /ERROR:\s+function extensions\.pg_stat_statements_reset\(oid, oid, bigint\) does not exist/i,
194
+ label: 'pg_stat_statements_reset missing',
195
+ },
196
+ {
197
+ regex: /ERROR:\s+trailing junk after numeric literal/i,
198
+ label: 'copy trailing junk errors',
199
+ },
200
+ {
201
+ regex: /ERROR:\s+invalid command \\\S+/i,
202
+ label: 'copy invalid command errors',
203
+ },
204
+ {
205
+ regex: /WARNING:\s+no privileges were granted/i,
206
+ label: 'privilege grant warnings',
207
+ },
208
+ {
209
+ regex: /WARNING:\s+no privileges could be revoked/i,
210
+ label: 'privilege revoke warnings',
211
+ },
212
+ {
213
+ regex: /WARNING:\s+not all privileges were granted/i,
214
+ label: 'partial privilege warnings',
215
+ },
216
+ {
217
+ regex: /ERROR:\s+permission denied to set parameter/i,
218
+ label: 'configuration permission warnings',
219
+ },
220
+ {
221
+ regex: /^DETAIL:/i,
222
+ label: 'suppressed detail lines',
223
+ },
224
+ {
225
+ regex: /^HINT:/i,
226
+ label: 'suppressed hint lines',
227
+ },
228
+ {
229
+ regex: /^CONTEXT:\s+/i,
230
+ label: 'suppressed context lines',
231
+ },
232
+ {
233
+ regex: /^LINE \d+:/i,
234
+ label: 'suppressed context lines',
235
+ },
236
+ {
237
+ regex: /^\s*\^$/i,
238
+ label: 'suppressed context lines',
239
+ },
240
+ ];
241
+
242
+ const recordSuppressed = (label) => {
243
+ if (!label) {
244
+ return;
245
+ }
246
+ suppressedCounts.set(label, (suppressedCounts.get(label) || 0) + 1);
247
+ };
248
+
249
+ const handleLine = (line) => {
250
+ const normalized = line.replace(/\r$/, '');
251
+ if (!normalized.trim()) {
252
+ return;
253
+ }
254
+ for (const { regex, label } of ignorePatterns) {
255
+ if (regex.test(normalized)) {
256
+ recordSuppressed(label);
257
+ return;
258
+ }
259
+ }
260
+ outputFn(normalized);
261
+ };
262
+
263
+ const finish = () => {
264
+ if (finished) {
265
+ return suppressedCounts;
266
+ }
267
+ if (buffer.length > 0) {
268
+ handleLine(buffer);
269
+ buffer = '';
270
+ }
271
+ finished = true;
272
+ return suppressedCounts;
273
+ };
274
+
275
+ return {
276
+ write(chunk) {
277
+ if (finished) {
278
+ return;
279
+ }
280
+ buffer += chunk.toString();
281
+ let newlineIndex;
282
+ while ((newlineIndex = buffer.indexOf('\n')) !== -1) {
283
+ const line = buffer.slice(0, newlineIndex);
284
+ handleLine(line);
285
+ buffer = buffer.slice(newlineIndex + 1);
286
+ }
287
+ },
288
+ finish,
289
+ };
290
+ }
291
+
292
+ // Load target database connection from env
293
+ require('dotenv').config({ path: '.env.local' }); // ensure this has the new DB credentials
294
+
295
+ const dbUrl = process.env.POSTGRES_URL || process.env.DATABASE_URL;
296
+ if (!dbUrl) {
297
+ console.error("❌ No database connection URL found for restore.");
298
+ process.exit(1);
299
+ }
300
+
301
+ // Parse connection URL for target DB (similar to backup.js)
302
+ let connectionUrl;
303
+ try {
304
+ connectionUrl = new URL(dbUrl);
305
+ } catch (err) {
306
+ if (!dbUrl.startsWith('postgresql://') && dbUrl.startsWith('postgres://')) {
307
+ connectionUrl = new URL(dbUrl.replace(/^postgres:\/\//, 'postgresql://'));
308
+ } else {
309
+ console.error("❌ Invalid database URL format:", err.message);
310
+ process.exit(1);
311
+ }
312
+ }
313
+ const host = connectionUrl.hostname;
314
+ const port = connectionUrl.port || '5432';
315
+ const dbName = connectionUrl.pathname.replace(/^\//, '');
316
+ const user = connectionUrl.username;
317
+ const password = connectionUrl.password;
318
+ const sslMode = connectionUrl.searchParams.get('sslmode') || 'require';
319
+
320
+ function formatBackupLabel(dirName) {
321
+ const delimiter = '__';
322
+ const idx = dirName.indexOf(delimiter);
323
+ if (idx === -1) {
324
+ return dirName;
325
+ }
326
+ const label = dirName.slice(idx + delimiter.length);
327
+ return label || dirName;
328
+ }
329
+
330
+ // List available backup folders
331
+ const backupsPath = path.join(__dirname, '../backups');
332
+ if (!fs.existsSync(backupsPath)) {
333
+ console.error("❌ Backups directory not found.");
334
+ process.exit(1);
335
+ }
336
+ const backupDirs = fs.readdirSync(backupsPath, { withFileTypes: true })
337
+ .filter(dirent => dirent.isDirectory())
338
+ .map(dirent => dirent.name);
339
+ // Sort by name (timestamp in name means lexicographical sort is chronological)
340
+ backupDirs.sort().reverse(); // latest first
341
+
342
+ if (backupDirs.length === 0) {
343
+ console.error("❌ No backups found in the backups directory.");
344
+ process.exit(1);
345
+ }
346
+
347
+ // Show list of backups
348
+ console.log("Available backups:");
349
+ backupDirs.forEach((dir, index) => {
350
+ const label = formatBackupLabel(dir);
351
+ const prefix = index === 0 ? '(latest) ' : '';
352
+ console.log(`${index + 1}. ${prefix}${label}`);
353
+ });
354
+
355
+ // Prompt user to choose a backup
356
+ const rl = readline.createInterface({
357
+ input: process.stdin,
358
+ output: process.stdout
359
+ });
360
+ rl.question("Enter the number of the backup to restore (press Enter for latest): ", (answer) => {
361
+ rl.close();
362
+ const trimmed = answer.trim();
363
+ const choice = trimmed === '' ? 1 : parseInt(trimmed, 10);
364
+ if (isNaN(choice) || choice < 1 || choice > backupDirs.length) {
365
+ console.error("❌ Invalid selection. Exiting.");
366
+ process.exit(1);
367
+ }
368
+
369
+ const selectedDir = backupDirs[choice - 1];
370
+ const dumpFile = path.join(backupsPath, selectedDir, 'dump.sql');
371
+ if (!fs.existsSync(dumpFile)) {
372
+ console.error(`❌ dump.sql not found in backup folder: ${selectedDir}`);
373
+ process.exit(1);
374
+ }
375
+
376
+ console.log(`🔄 Restoring database from backup "${selectedDir}"...`);
377
+
378
+ // Spawn psql to execute the dump file
379
+ const restoreArgs = [
380
+ '-h', host,
381
+ '-U', user,
382
+ '-p', port,
383
+ '-d', dbName,
384
+ '-f', dumpFile
385
+ ];
386
+ const envVars = { ...process.env, PGPASSWORD: password, PGSSLMODE: sslMode };
387
+ const psql = spawn('psql', restoreArgs, { env: envVars });
388
+ const stdoutCollapser = createDuplicateCollapser(line => {
389
+ process.stdout.write(`${line}\n`);
390
+ }, { suppressors: stdoutSuppressors });
391
+ const stderrFilter = createErrorFilter(line => {
392
+ process.stderr.write(`${line}\n`);
393
+ });
394
+
395
+ psql.stdout.on('data', chunk => stdoutCollapser.write(chunk));
396
+ psql.stderr.on('data', chunk => stderrFilter.write(chunk));
397
+
398
+ psql.on('close', code => {
399
+ const stdoutSuppressed = stdoutCollapser.finish();
400
+ const stderrSuppressed = stderrFilter.finish();
401
+
402
+ const formatSummary = (label, map) => {
403
+ if (!map || map.size === 0) {
404
+ return null;
405
+ }
406
+ const parts = [];
407
+ let subtotal = 0;
408
+ for (const [entryLabel, count] of map.entries()) {
409
+ subtotal += count;
410
+ parts.push(`${entryLabel} x${count}`);
411
+ }
412
+ return { label, subtotal, parts };
413
+ };
414
+
415
+ const summaries = [
416
+ formatSummary('stdout', stdoutSuppressed),
417
+ formatSummary('stderr', stderrSuppressed),
418
+ ].filter(Boolean);
419
+
420
+ if (summaries.length > 0) {
421
+ const totalSuppressed = summaries.reduce((acc, { subtotal }) => acc + subtotal, 0);
422
+ const detail = summaries
423
+ .map(({ label, parts }) => `${label}: ${parts.join(', ')}`)
424
+ .join(' | ');
425
+ console.log(`ℹ️ Suppressed ${totalSuppressed} known restore messages (${detail}).`);
426
+ }
427
+
428
+ if (code === 0) {
429
+ console.log("✅ Restore completed successfully.");
430
+ } else {
431
+ console.error(`❌ psql exited with code ${code}. The restore may have errors.`);
432
+ }
433
+ });
434
+ });
@@ -1,77 +0,0 @@
1
- // app/blog/page.tsx
2
- import React from 'react';
3
- import { getSsgSupabaseClient } from "@nextblock-cms/db";
4
- import { notFound } from "next/navigation";
5
- import type { Metadata } from 'next';
6
- import PageClientContent from "../[slug]/PageClientContent";
7
- import { getPageDataBySlug } from "../[slug]/page.utils";
8
- import BlockRenderer from "../../components/BlockRenderer";
9
- import { getPageTranslations } from '@/app/actions/languageActions'; // Added import
10
-
11
- export const dynamicParams = true;
12
- export const revalidate = 3600;
13
-
14
- export async function generateMetadata(): Promise<Metadata> {
15
- const slug = "blog"; // Hardcoded slug
16
- const pageData = await getPageDataBySlug(slug);
17
-
18
- if (!pageData) {
19
- return { title: "Blog Page Not Found" };
20
- }
21
-
22
- const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || "";
23
- const supabase = getSsgSupabaseClient();
24
- const { data: languages } = await supabase.from('languages').select('id, code');
25
- const { data: pageTranslations } = await supabase
26
- .from('pages')
27
- .select('language_id, slug')
28
- .eq('translation_group_id', pageData.translation_group_id)
29
- .eq('status', 'published');
30
-
31
- const alternates: { [key: string]: string } = {};
32
- if (languages && pageTranslations) {
33
- pageTranslations.forEach(pt => {
34
- const langInfo = languages.find(l => l.id === pt.language_id);
35
- if (langInfo) {
36
- alternates[langInfo.code] = `${siteUrl}/${pt.slug}`;
37
- }
38
- });
39
- }
40
-
41
- return {
42
- title: pageData.meta_title || pageData.title,
43
- description: pageData.meta_description || "",
44
- alternates: {
45
- canonical: `${siteUrl}/${slug}`,
46
- languages: Object.keys(alternates).length > 0 ? alternates : undefined,
47
- },
48
- };
49
- }
50
-
51
- export default async function BlogPage() {
52
- const slug = "blog"; // Hardcoded slug
53
- const pageData = await getPageDataBySlug(slug);
54
-
55
- if (!pageData) {
56
- notFound();
57
- }
58
-
59
- const translatedSlugs: { [key: string]: string } = {};
60
- // Ensure pageData and translation_group_id are available before fetching translations
61
- if (pageData && pageData.translation_group_id) {
62
- const translations = await getPageTranslations(pageData.translation_group_id);
63
- translations.forEach(t => {
64
- if (t.language_code && t.slug) { // Ensure both properties exist
65
- translatedSlugs[t.language_code] = t.slug;
66
- }
67
- });
68
- }
69
-
70
- const pageBlocks = pageData ? <BlockRenderer blocks={pageData.blocks} languageId={pageData.language_id} /> : null;
71
-
72
- return (
73
- <PageClientContent initialPageData={pageData} currentSlug={slug} translatedSlugs={translatedSlugs}>
74
- {pageBlocks}
75
- </PageClientContent>
76
- );
77
- }