create-nextblock 0.0.1

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 (206) hide show
  1. package/bin/create-nextblock.js +997 -0
  2. package/package.json +25 -0
  3. package/scripts/sync-template.js +284 -0
  4. package/templates/nextblock-template/.env.example +37 -0
  5. package/templates/nextblock-template/.swcrc +30 -0
  6. package/templates/nextblock-template/README.md +194 -0
  7. package/templates/nextblock-template/app/(auth-pages)/forgot-password/page.tsx +57 -0
  8. package/templates/nextblock-template/app/(auth-pages)/layout.tsx +9 -0
  9. package/templates/nextblock-template/app/(auth-pages)/post-sign-in/page.tsx +28 -0
  10. package/templates/nextblock-template/app/(auth-pages)/sign-in/page.tsx +67 -0
  11. package/templates/nextblock-template/app/(auth-pages)/sign-up/page.tsx +70 -0
  12. package/templates/nextblock-template/app/ToasterProvider.tsx +17 -0
  13. package/templates/nextblock-template/app/[slug]/PageClientContent.tsx +147 -0
  14. package/templates/nextblock-template/app/[slug]/page.tsx +145 -0
  15. package/templates/nextblock-template/app/[slug]/page.utils.ts +183 -0
  16. package/templates/nextblock-template/app/actions/email.ts +31 -0
  17. package/templates/nextblock-template/app/actions/formActions.ts +65 -0
  18. package/templates/nextblock-template/app/actions/languageActions.ts +130 -0
  19. package/templates/nextblock-template/app/actions/postActions.ts +80 -0
  20. package/templates/nextblock-template/app/actions.ts +146 -0
  21. package/templates/nextblock-template/app/api/process-image/route.ts +210 -0
  22. package/templates/nextblock-template/app/api/revalidate/route.ts +86 -0
  23. package/templates/nextblock-template/app/api/revalidate-log/route.ts +23 -0
  24. package/templates/nextblock-template/app/api/upload/presigned-url/route.ts +106 -0
  25. package/templates/nextblock-template/app/api/upload/proxy/route.ts +84 -0
  26. package/templates/nextblock-template/app/auth/callback/route.ts +58 -0
  27. package/templates/nextblock-template/app/blog/[slug]/PostClientContent.tsx +169 -0
  28. package/templates/nextblock-template/app/blog/[slug]/page.tsx +177 -0
  29. package/templates/nextblock-template/app/blog/[slug]/page.utils.ts +136 -0
  30. package/templates/nextblock-template/app/blog/page.tsx +77 -0
  31. package/templates/nextblock-template/app/cms/CmsClientLayout.tsx +321 -0
  32. package/templates/nextblock-template/app/cms/blocks/actions.ts +434 -0
  33. package/templates/nextblock-template/app/cms/blocks/components/BackgroundSelector.tsx +348 -0
  34. package/templates/nextblock-template/app/cms/blocks/components/BlockEditorArea.tsx +567 -0
  35. package/templates/nextblock-template/app/cms/blocks/components/BlockEditorModal.tsx +98 -0
  36. package/templates/nextblock-template/app/cms/blocks/components/BlockTypeCard.tsx +58 -0
  37. package/templates/nextblock-template/app/cms/blocks/components/BlockTypeSelector.tsx +62 -0
  38. package/templates/nextblock-template/app/cms/blocks/components/ColumnEditor.tsx +276 -0
  39. package/templates/nextblock-template/app/cms/blocks/components/DeleteBlockButtonClient.tsx +47 -0
  40. package/templates/nextblock-template/app/cms/blocks/components/EditableBlock.tsx +182 -0
  41. package/templates/nextblock-template/app/cms/blocks/components/MediaLibraryModal.tsx +120 -0
  42. package/templates/nextblock-template/app/cms/blocks/components/SectionConfigPanel.tsx +133 -0
  43. package/templates/nextblock-template/app/cms/blocks/components/SortableBlockItem.tsx +46 -0
  44. package/templates/nextblock-template/app/cms/blocks/editors/ButtonBlockEditor.tsx +85 -0
  45. package/templates/nextblock-template/app/cms/blocks/editors/FormBlockEditor.tsx +182 -0
  46. package/templates/nextblock-template/app/cms/blocks/editors/HeadingBlockEditor.tsx +111 -0
  47. package/templates/nextblock-template/app/cms/blocks/editors/ImageBlockEditor.tsx +150 -0
  48. package/templates/nextblock-template/app/cms/blocks/editors/PostsGridBlockEditor.tsx +79 -0
  49. package/templates/nextblock-template/app/cms/blocks/editors/SectionBlockEditor.tsx +337 -0
  50. package/templates/nextblock-template/app/cms/blocks/editors/TextBlockEditor.tsx +81 -0
  51. package/templates/nextblock-template/app/cms/blocks/editors/VideoEmbedBlockEditor.tsx +64 -0
  52. package/templates/nextblock-template/app/cms/components/ConfirmationModal.tsx +51 -0
  53. package/templates/nextblock-template/app/cms/components/ContentLanguageSwitcher.tsx +145 -0
  54. package/templates/nextblock-template/app/cms/components/CopyContentFromLanguage.tsx +203 -0
  55. package/templates/nextblock-template/app/cms/components/LanguageFilterSelect.tsx +69 -0
  56. package/templates/nextblock-template/app/cms/dashboard/page.tsx +247 -0
  57. package/templates/nextblock-template/app/cms/layout.tsx +10 -0
  58. package/templates/nextblock-template/app/cms/media/UploadFolderContext.tsx +22 -0
  59. package/templates/nextblock-template/app/cms/media/[id]/edit/page.tsx +80 -0
  60. package/templates/nextblock-template/app/cms/media/actions.ts +577 -0
  61. package/templates/nextblock-template/app/cms/media/components/DeleteMediaButtonClient.tsx +53 -0
  62. package/templates/nextblock-template/app/cms/media/components/FolderNavigator.tsx +273 -0
  63. package/templates/nextblock-template/app/cms/media/components/FolderTree.tsx +122 -0
  64. package/templates/nextblock-template/app/cms/media/components/MediaEditForm.tsx +157 -0
  65. package/templates/nextblock-template/app/cms/media/components/MediaGridClient.tsx +275 -0
  66. package/templates/nextblock-template/app/cms/media/components/MediaImage.tsx +70 -0
  67. package/templates/nextblock-template/app/cms/media/components/MediaPickerDialog.tsx +195 -0
  68. package/templates/nextblock-template/app/cms/media/components/MediaUploadForm.tsx +362 -0
  69. package/templates/nextblock-template/app/cms/media/page.tsx +120 -0
  70. package/templates/nextblock-template/app/cms/navigation/[id]/edit/page.tsx +101 -0
  71. package/templates/nextblock-template/app/cms/navigation/actions.ts +358 -0
  72. package/templates/nextblock-template/app/cms/navigation/components/DeleteNavItemButton.tsx +52 -0
  73. package/templates/nextblock-template/app/cms/navigation/components/NavigationItemForm.tsx +248 -0
  74. package/templates/nextblock-template/app/cms/navigation/components/NavigationLanguageSwitcher.tsx +132 -0
  75. package/templates/nextblock-template/app/cms/navigation/components/NavigationMenuDnd.tsx +701 -0
  76. package/templates/nextblock-template/app/cms/navigation/components/SortableNavItem.tsx +98 -0
  77. package/templates/nextblock-template/app/cms/navigation/new/page.tsx +26 -0
  78. package/templates/nextblock-template/app/cms/navigation/page.tsx +102 -0
  79. package/templates/nextblock-template/app/cms/navigation/utils.ts +51 -0
  80. package/templates/nextblock-template/app/cms/pages/[id]/edit/EditPageClient.tsx +121 -0
  81. package/templates/nextblock-template/app/cms/pages/[id]/edit/page.tsx +79 -0
  82. package/templates/nextblock-template/app/cms/pages/actions.ts +241 -0
  83. package/templates/nextblock-template/app/cms/pages/components/DeletePageButtonClient.tsx +47 -0
  84. package/templates/nextblock-template/app/cms/pages/components/PageForm.tsx +253 -0
  85. package/templates/nextblock-template/app/cms/pages/new/page.tsx +52 -0
  86. package/templates/nextblock-template/app/cms/pages/page.tsx +232 -0
  87. package/templates/nextblock-template/app/cms/posts/[id]/edit/page.tsx +183 -0
  88. package/templates/nextblock-template/app/cms/posts/actions.ts +309 -0
  89. package/templates/nextblock-template/app/cms/posts/components/DeletePostButtonClient.tsx +55 -0
  90. package/templates/nextblock-template/app/cms/posts/components/PostForm.tsx +419 -0
  91. package/templates/nextblock-template/app/cms/posts/new/page.tsx +21 -0
  92. package/templates/nextblock-template/app/cms/posts/page.tsx +192 -0
  93. package/templates/nextblock-template/app/cms/revisions/JsonDiffView.tsx +86 -0
  94. package/templates/nextblock-template/app/cms/revisions/RevisionHistoryButton.tsx +201 -0
  95. package/templates/nextblock-template/app/cms/revisions/actions.ts +84 -0
  96. package/templates/nextblock-template/app/cms/revisions/service.ts +344 -0
  97. package/templates/nextblock-template/app/cms/revisions/utils.ts +127 -0
  98. package/templates/nextblock-template/app/cms/settings/copyright/actions.ts +68 -0
  99. package/templates/nextblock-template/app/cms/settings/copyright/components/CopyrightForm.tsx +78 -0
  100. package/templates/nextblock-template/app/cms/settings/copyright/page.tsx +32 -0
  101. package/templates/nextblock-template/app/cms/settings/extra-translations/actions.ts +117 -0
  102. package/templates/nextblock-template/app/cms/settings/extra-translations/page.tsx +216 -0
  103. package/templates/nextblock-template/app/cms/settings/languages/[id]/edit/page.tsx +77 -0
  104. package/templates/nextblock-template/app/cms/settings/languages/actions.ts +261 -0
  105. package/templates/nextblock-template/app/cms/settings/languages/components/DeleteLanguageButton.tsx +76 -0
  106. package/templates/nextblock-template/app/cms/settings/languages/components/LanguageForm.tsx +167 -0
  107. package/templates/nextblock-template/app/cms/settings/languages/new/page.tsx +34 -0
  108. package/templates/nextblock-template/app/cms/settings/languages/page.tsx +156 -0
  109. package/templates/nextblock-template/app/cms/settings/logos/[id]/edit/page.tsx +19 -0
  110. package/templates/nextblock-template/app/cms/settings/logos/actions.ts +114 -0
  111. package/templates/nextblock-template/app/cms/settings/logos/components/LogoForm.tsx +177 -0
  112. package/templates/nextblock-template/app/cms/settings/logos/new/page.tsx +11 -0
  113. package/templates/nextblock-template/app/cms/settings/logos/page.tsx +118 -0
  114. package/templates/nextblock-template/app/cms/settings/logos/types.ts +8 -0
  115. package/templates/nextblock-template/app/cms/users/[id]/edit/page.tsx +91 -0
  116. package/templates/nextblock-template/app/cms/users/actions.ts +156 -0
  117. package/templates/nextblock-template/app/cms/users/components/DeleteUserButton.tsx +71 -0
  118. package/templates/nextblock-template/app/cms/users/components/UserForm.tsx +138 -0
  119. package/templates/nextblock-template/app/cms/users/page.tsx +183 -0
  120. package/templates/nextblock-template/app/favicon.ico +0 -0
  121. package/templates/nextblock-template/app/globals.css +401 -0
  122. package/templates/nextblock-template/app/layout.tsx +191 -0
  123. package/templates/nextblock-template/app/lib/sitemap-utils.ts +68 -0
  124. package/templates/nextblock-template/app/page.tsx +109 -0
  125. package/templates/nextblock-template/app/providers.tsx +43 -0
  126. package/templates/nextblock-template/app/robots.txt/route.ts +19 -0
  127. package/templates/nextblock-template/app/sitemap.xml/route.ts +63 -0
  128. package/templates/nextblock-template/app/unauthorized/page.tsx +27 -0
  129. package/templates/nextblock-template/backup/backup_2025-06-19.sql +8057 -0
  130. package/templates/nextblock-template/backup/backup_2025-06-20.sql +8159 -0
  131. package/templates/nextblock-template/backup/backup_2025-07-08.sql +8411 -0
  132. package/templates/nextblock-template/backup/backup_2025-07-09.sql +8442 -0
  133. package/templates/nextblock-template/backup/backup_2025-07-10.sql +8442 -0
  134. package/templates/nextblock-template/backup/backup_2025-10-01.sql +8803 -0
  135. package/templates/nextblock-template/backup/backup_2025-10-02.sql +9749 -0
  136. package/templates/nextblock-template/components/BlockRenderer.tsx +119 -0
  137. package/templates/nextblock-template/components/FooterNavigation.tsx +33 -0
  138. package/templates/nextblock-template/components/Header.tsx +42 -0
  139. package/templates/nextblock-template/components/HtmlScriptExecutor.tsx +47 -0
  140. package/templates/nextblock-template/components/LanguageSwitcher.tsx +103 -0
  141. package/templates/nextblock-template/components/ResponsiveNav.tsx +372 -0
  142. package/templates/nextblock-template/components/blocks/PostCardSkeleton.tsx +17 -0
  143. package/templates/nextblock-template/components/blocks/PostsGridBlock.tsx +93 -0
  144. package/templates/nextblock-template/components/blocks/PostsGridClient.tsx +180 -0
  145. package/templates/nextblock-template/components/blocks/renderers/ButtonBlockRenderer.tsx +92 -0
  146. package/templates/nextblock-template/components/blocks/renderers/ClientTextBlockRenderer.tsx +69 -0
  147. package/templates/nextblock-template/components/blocks/renderers/FormBlockRenderer.tsx +98 -0
  148. package/templates/nextblock-template/components/blocks/renderers/HeadingBlockRenderer.tsx +41 -0
  149. package/templates/nextblock-template/components/blocks/renderers/HeroBlockRenderer.tsx +240 -0
  150. package/templates/nextblock-template/components/blocks/renderers/ImageBlockRenderer.tsx +79 -0
  151. package/templates/nextblock-template/components/blocks/renderers/PostsGridBlockRenderer.tsx +33 -0
  152. package/templates/nextblock-template/components/blocks/renderers/SectionBlockRenderer.tsx +189 -0
  153. package/templates/nextblock-template/components/blocks/renderers/TextBlockRenderer.tsx +31 -0
  154. package/templates/nextblock-template/components/blocks/renderers/VideoEmbedBlockRenderer.tsx +59 -0
  155. package/templates/nextblock-template/components/blocks/renderers/inline/AlertWidgetRenderer.tsx +51 -0
  156. package/templates/nextblock-template/components/blocks/renderers/inline/CtaWidgetRenderer.tsx +40 -0
  157. package/templates/nextblock-template/components/blocks/types.ts +8 -0
  158. package/templates/nextblock-template/components/env-var-warning.tsx +33 -0
  159. package/templates/nextblock-template/components/form-message.tsx +26 -0
  160. package/templates/nextblock-template/components/header-auth.tsx +71 -0
  161. package/templates/nextblock-template/components/submit-button.tsx +23 -0
  162. package/templates/nextblock-template/components/theme-switcher.tsx +78 -0
  163. package/templates/nextblock-template/context/AuthContext.tsx +138 -0
  164. package/templates/nextblock-template/context/CurrentContentContext.tsx +42 -0
  165. package/templates/nextblock-template/context/LanguageContext.tsx +206 -0
  166. package/templates/nextblock-template/docs/cms-application-overview.md +56 -0
  167. package/templates/nextblock-template/docs/cms-architecture-overview.md +73 -0
  168. package/templates/nextblock-template/docs/files-structure.md +426 -0
  169. package/templates/nextblock-template/docs/tiptap-bundle-optimization-summary.md +174 -0
  170. package/templates/nextblock-template/eslint.config.mjs +28 -0
  171. package/templates/nextblock-template/index.d.ts +5 -0
  172. package/templates/nextblock-template/lib/blocks/README.md +670 -0
  173. package/templates/nextblock-template/lib/blocks/blockRegistry.ts +1001 -0
  174. package/templates/nextblock-template/lib/ui/ColorPicker.ts +1 -0
  175. package/templates/nextblock-template/lib/ui/ConfirmationDialog.ts +1 -0
  176. package/templates/nextblock-template/lib/ui/CustomSelectWithInput.ts +1 -0
  177. package/templates/nextblock-template/lib/ui/Skeleton.ts +1 -0
  178. package/templates/nextblock-template/lib/ui/avatar.ts +1 -0
  179. package/templates/nextblock-template/lib/ui/badge.ts +1 -0
  180. package/templates/nextblock-template/lib/ui/button.ts +1 -0
  181. package/templates/nextblock-template/lib/ui/card.ts +1 -0
  182. package/templates/nextblock-template/lib/ui/checkbox.ts +1 -0
  183. package/templates/nextblock-template/lib/ui/dialog.ts +1 -0
  184. package/templates/nextblock-template/lib/ui/dropdown-menu.ts +1 -0
  185. package/templates/nextblock-template/lib/ui/input.ts +1 -0
  186. package/templates/nextblock-template/lib/ui/label.ts +1 -0
  187. package/templates/nextblock-template/lib/ui/popover.ts +1 -0
  188. package/templates/nextblock-template/lib/ui/progress.ts +1 -0
  189. package/templates/nextblock-template/lib/ui/select.ts +1 -0
  190. package/templates/nextblock-template/lib/ui/separator.ts +1 -0
  191. package/templates/nextblock-template/lib/ui/table.ts +1 -0
  192. package/templates/nextblock-template/lib/ui/textarea.ts +1 -0
  193. package/templates/nextblock-template/lib/ui/tooltip.ts +1 -0
  194. package/templates/nextblock-template/lib/ui/ui.ts +1 -0
  195. package/templates/nextblock-template/middleware.ts +206 -0
  196. package/templates/nextblock-template/next-env.d.ts +6 -0
  197. package/templates/nextblock-template/next.config.js +99 -0
  198. package/templates/nextblock-template/package.json +52 -0
  199. package/templates/nextblock-template/postcss.config.js +6 -0
  200. package/templates/nextblock-template/project.json +7 -0
  201. package/templates/nextblock-template/public/.gitkeep +0 -0
  202. package/templates/nextblock-template/scripts/backfill-image-meta.ts +149 -0
  203. package/templates/nextblock-template/scripts/backup.js +53 -0
  204. package/templates/nextblock-template/scripts/test-bundle-optimization.js +114 -0
  205. package/templates/nextblock-template/tailwind.config.ts +19 -0
  206. package/templates/nextblock-template/tsconfig.json +62 -0
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "create-nextblock",
3
+ "version": "0.0.1",
4
+ "description": "",
5
+ "main": "index.js",
6
+ "bin": {
7
+ "create-nextblock": "./bin/create-nextblock.js"
8
+ },
9
+ "scripts": {
10
+ "test": "echo \"Error: no test specified\" && exit 1",
11
+ "sync-template": "node ./scripts/sync-template.js",
12
+ "prepack": "npm run sync-template"
13
+ },
14
+ "keywords": [],
15
+ "author": "",
16
+ "license": "ISC",
17
+ "type": "module",
18
+ "dependencies": {
19
+ "chalk": "^5.6.2",
20
+ "commander": "^14.0.1",
21
+ "fs-extra": "^11.3.2",
22
+ "inquirer": "^12.10.0",
23
+ "ora": "^8.0.1"
24
+ }
25
+ }
@@ -0,0 +1,284 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { resolve, relative, sep, dirname } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import fs from 'fs-extra';
6
+ import chalk from 'chalk';
7
+
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = dirname(__filename);
10
+ const PROJECT_ROOT = resolve(__dirname, '..');
11
+ const SOURCE_DIR = resolve(PROJECT_ROOT, '../nextblock');
12
+ const TARGET_DIR = resolve(PROJECT_ROOT, 'templates/nextblock-template');
13
+ const REPO_ROOT = resolve(PROJECT_ROOT, '..', '..');
14
+ const UI_GLOBALS_SOURCE = resolve(PROJECT_ROOT, '../../libs/ui/src/styles/globals.css');
15
+ const BACKUP_SOURCE_DIR = resolve(SOURCE_DIR, 'backup');
16
+ const BACKUP_TARGET_DIR = resolve(TARGET_DIR, 'backup');
17
+ const UI_PROXY_MODULES = [
18
+ 'avatar',
19
+ 'badge',
20
+ 'button',
21
+ 'card',
22
+ 'checkbox',
23
+ 'ColorPicker',
24
+ 'ConfirmationDialog',
25
+ 'CustomSelectWithInput',
26
+ 'dialog',
27
+ 'dropdown-menu',
28
+ 'input',
29
+ 'label',
30
+ 'popover',
31
+ 'progress',
32
+ 'select',
33
+ 'separator',
34
+ 'Skeleton',
35
+ 'table',
36
+ 'textarea',
37
+ 'tooltip',
38
+ 'ui',
39
+ ];
40
+
41
+ const IGNORED_SEGMENTS = new Set([
42
+ 'node_modules',
43
+ '.git',
44
+ '.next',
45
+ 'dist',
46
+ 'tmp',
47
+ 'coverage',
48
+ ]);
49
+
50
+ async function ensureTemplateSync() {
51
+ const sourceExists = await fs.pathExists(SOURCE_DIR);
52
+ if (!sourceExists) {
53
+ throw new Error(
54
+ `Source project not found at ${SOURCE_DIR}. Please ensure apps/nextblock exists before syncing.`,
55
+ );
56
+ }
57
+
58
+ console.log(
59
+ chalk.blue(
60
+ `Syncing template from ${chalk.bold(relative(PROJECT_ROOT, SOURCE_DIR))} to ${chalk.bold(
61
+ relative(PROJECT_ROOT, TARGET_DIR),
62
+ )}`,
63
+ ),
64
+ );
65
+
66
+ await fs.ensureDir(TARGET_DIR);
67
+ await fs.emptyDir(TARGET_DIR);
68
+
69
+ await fs.copy(SOURCE_DIR, TARGET_DIR, {
70
+ dereference: true,
71
+ filter: (src) => {
72
+ const rel = relative(SOURCE_DIR, src);
73
+ if (!rel) {
74
+ return true;
75
+ }
76
+
77
+ const segments = rel.split(sep);
78
+ return segments.every((segment) => !IGNORED_SEGMENTS.has(segment));
79
+ },
80
+ });
81
+
82
+ await ensureEnvExample();
83
+ await ensureGlobalStyles();
84
+ await ensureClientTranslations();
85
+ await sanitizeBlockEditorImports();
86
+ await sanitizeUiImports();
87
+ await ensureUiProxies();
88
+ await ensureBackups();
89
+ await ensureTemplateProjectJson();
90
+
91
+ console.log(chalk.green('Template sync complete.'));
92
+ }
93
+
94
+ async function ensureEnvExample() {
95
+ const envTargets = [
96
+ resolve(REPO_ROOT, '.env.example'),
97
+ resolve(REPO_ROOT, '.env.exemple'),
98
+ resolve(SOURCE_DIR, '.env.example'),
99
+ resolve(SOURCE_DIR, '.env.exemple'),
100
+ ];
101
+
102
+ const destination = resolve(TARGET_DIR, '.env.example');
103
+
104
+ for (const envPath of envTargets) {
105
+ if (await fs.pathExists(envPath)) {
106
+ await fs.copy(envPath, destination);
107
+ return;
108
+ }
109
+ }
110
+
111
+ const placeholder = `# Environment variables for NextBlock CMS
112
+ NEXT_PUBLIC_SUPABASE_URL=
113
+ NEXT_PUBLIC_SUPABASE_ANON_KEY=
114
+ SUPABASE_SERVICE_ROLE_KEY=
115
+ SUPABASE_JWT_SECRET=
116
+ NEXT_PUBLIC_URL=http://localhost:3000
117
+ `;
118
+
119
+ await fs.writeFile(destination, placeholder);
120
+ }
121
+
122
+ async function ensureGlobalStyles() {
123
+ const destination = resolve(TARGET_DIR, 'app/globals.css');
124
+
125
+ if (await fs.pathExists(destination)) {
126
+ await fs.remove(destination);
127
+ }
128
+
129
+ if (await fs.pathExists(UI_GLOBALS_SOURCE)) {
130
+ await fs.copy(UI_GLOBALS_SOURCE, destination);
131
+ return;
132
+ }
133
+
134
+ const fallback = `@tailwind base;
135
+ @tailwind components;
136
+ @tailwind utilities;
137
+ `;
138
+
139
+ await fs.outputFile(destination, fallback);
140
+ }
141
+
142
+ async function ensureClientTranslations() {
143
+ const providersPath = resolve(TARGET_DIR, 'app/providers.tsx');
144
+ if (!(await fs.pathExists(providersPath))) {
145
+ return;
146
+ }
147
+
148
+ let content = await fs.readFile(providersPath, 'utf8');
149
+ const wrapperImportPath = '@nextblock-cms/utils';
150
+ const wrapperImportStatement = `import { TranslationsProvider } from '${wrapperImportPath}';`;
151
+ const existingImportRegex =
152
+ /import\s+\{\s*TranslationsProvider\s*\}\s*from\s*['"]@nextblock-cms\/utils['"];?/;
153
+ const legacyImportRegex =
154
+ /import\s+\{\s*TranslationsProvider\s*\}\s*from\s*['"]@\/lib\/client-translations['"];?/;
155
+
156
+ if (existingImportRegex.test(content) || legacyImportRegex.test(content)) {
157
+ content = content
158
+ .replace(existingImportRegex, wrapperImportStatement)
159
+ .replace(legacyImportRegex, wrapperImportStatement);
160
+ } else if (!content.includes(wrapperImportStatement)) {
161
+ const lines = content.split(/\r?\n/);
162
+ const insertIndex = lines.findIndex((line) => line.startsWith('import')) + 1;
163
+ if (insertIndex > 0) {
164
+ lines.splice(insertIndex, 0, wrapperImportStatement);
165
+ content = lines.join('\n');
166
+ } else {
167
+ content = `${wrapperImportStatement}\n${content}`;
168
+ }
169
+ }
170
+
171
+ await fs.writeFile(providersPath, content);
172
+
173
+ const wrapperPath = resolve(TARGET_DIR, 'lib/client-translations.tsx');
174
+ if (await fs.pathExists(wrapperPath)) {
175
+ await fs.remove(wrapperPath);
176
+ }
177
+ }
178
+
179
+ async function sanitizeBlockEditorImports() {
180
+ const blockEditorPath = resolve(TARGET_DIR, 'app/cms/blocks/components/BlockEditorArea.tsx');
181
+ if (!(await fs.pathExists(blockEditorPath))) {
182
+ return;
183
+ }
184
+
185
+ const replacements = [
186
+ { pattern: /(\.\.\/editors\/[A-Za-z0-9_-]+)\.js/g, replacement: '$1.tsx' },
187
+ { pattern: /(\.\.\/actions)\.js/g, replacement: '$1.ts' },
188
+ ];
189
+
190
+ const content = await fs.readFile(blockEditorPath, 'utf8');
191
+ let updated = content;
192
+
193
+ for (const { pattern, replacement } of replacements) {
194
+ updated = updated.replace(pattern, replacement);
195
+ }
196
+
197
+ if (updated !== content) {
198
+ await fs.writeFile(blockEditorPath, updated);
199
+ }
200
+ }
201
+
202
+ async function sanitizeUiImports() {
203
+ const searchDirs = ['app', 'components', 'context', 'lib'];
204
+ const validExtensions = new Set(['.ts', '.tsx', '.js', '.jsx']);
205
+ const filesToProcess = [];
206
+
207
+ for (const relativeDir of searchDirs) {
208
+ const absoluteDir = resolve(TARGET_DIR, relativeDir);
209
+ if (await fs.pathExists(absoluteDir)) {
210
+ await collectFiles(absoluteDir, filesToProcess, validExtensions);
211
+ }
212
+ }
213
+
214
+ for (const filePath of filesToProcess) {
215
+ const original = await fs.readFile(filePath, 'utf8');
216
+ const replaced = original.replace(/@nextblock-cms\/ui\/(?!styles\/)[A-Za-z0-9/_-]+/g, '@nextblock-cms/ui');
217
+
218
+ if (replaced !== original) {
219
+ await fs.writeFile(filePath, replaced);
220
+ }
221
+ }
222
+ }
223
+
224
+ async function collectFiles(directory, accumulator, extensions) {
225
+ const entries = await fs.readdir(directory, { withFileTypes: true });
226
+ for (const entry of entries) {
227
+ const fullPath = resolve(directory, entry.name);
228
+ if (entry.isDirectory()) {
229
+ await collectFiles(fullPath, accumulator, extensions);
230
+ } else {
231
+ const dotIndex = entry.name.lastIndexOf('.');
232
+ if (dotIndex !== -1) {
233
+ const ext = entry.name.slice(dotIndex);
234
+ if (extensions.has(ext)) {
235
+ accumulator.push(fullPath);
236
+ }
237
+ }
238
+ }
239
+ }
240
+ }
241
+
242
+ async function ensureUiProxies() {
243
+ const proxiesDir = resolve(TARGET_DIR, 'lib/ui');
244
+ await fs.ensureDir(proxiesDir);
245
+
246
+ const proxyContent = "export * from '@nextblock-cms/ui';\n";
247
+
248
+ for (const moduleName of UI_PROXY_MODULES) {
249
+ const proxyPath = resolve(proxiesDir, `${moduleName}.ts`);
250
+ if (!(await fs.pathExists(proxyPath))) {
251
+ await fs.outputFile(proxyPath, proxyContent);
252
+ }
253
+ }
254
+ }
255
+
256
+ async function ensureBackups() {
257
+ if (!(await fs.pathExists(BACKUP_SOURCE_DIR))) {
258
+ await fs.remove(BACKUP_TARGET_DIR).catch(() => undefined);
259
+ return;
260
+ }
261
+
262
+ await fs.ensureDir(BACKUP_TARGET_DIR);
263
+ await fs.copy(BACKUP_SOURCE_DIR, BACKUP_TARGET_DIR, {
264
+ dereference: true,
265
+ });
266
+ }
267
+
268
+ async function ensureTemplateProjectJson() {
269
+ const projectJsonPath = resolve(TARGET_DIR, 'project.json');
270
+ const minimalConfig = {
271
+ name: 'nextblock-template',
272
+ projectType: 'application',
273
+ root: 'apps/create-nextblock/templates/nextblock-template',
274
+ sourceRoot: 'apps/create-nextblock/templates/nextblock-template',
275
+ targets: {},
276
+ };
277
+
278
+ await fs.writeJSON(projectJsonPath, minimalConfig, { spaces: 2 });
279
+ }
280
+
281
+ ensureTemplateSync().catch((error) => {
282
+ console.error(chalk.red(error instanceof Error ? error.message : String(error)));
283
+ process.exit(1);
284
+ });
@@ -0,0 +1,37 @@
1
+ NEXT_PUBLIC_URL=
2
+ # Vercel / Supabase
3
+ SUPABASE_PROJECT_ID=
4
+ POSTGRES_URL=
5
+ POSTGRES_PRISMA_URL=
6
+ SUPABASE_URL=
7
+ NEXT_PUBLIC_SUPABASE_URL=
8
+ POSTGRES_URL_NON_POOLING=
9
+ SUPABASE_JWT_SECRET=
10
+ POSTGRES_USER=
11
+ NEXT_PUBLIC_SUPABASE_ANON_KEY=
12
+ POSTGRES_PASSWORD=
13
+ POSTGRES_DATABASE=
14
+ SUPABASE_SERVICE_ROLE_KEY=
15
+ POSTGRES_HOST=
16
+ SUPABASE_ANON_KEY=
17
+
18
+ # Cloudflare
19
+ NEXT_PUBLIC_R2_BASE_URL=
20
+ R2_ACCESS_KEY_ID=
21
+ R2_SECRET_ACCESS_KEY=
22
+ R2_BUCKET_NAME=
23
+ R2_ACCOUNT_ID=
24
+ NEXT_PUBLIC_R2_PUBLIC_URL=
25
+ R2_TOKEN_VALUE=
26
+
27
+ NEXTJS_REVALIDATE_URL=
28
+ REVALIDATE_SECRET_TOKEN=
29
+ LHCI_GITHUB_APP_TOKEN=
30
+
31
+ # Email SMTP Configuration
32
+ SMTP_HOST=
33
+ SMTP_PORT=
34
+ SMTP_USER=
35
+ SMTP_PASS=
36
+ SMTP_FROM_EMAIL=
37
+ SMTP_FROM_NAME=
@@ -0,0 +1,30 @@
1
+ {
2
+ "jsc": {
3
+ "target": "es2017",
4
+ "parser": {
5
+ "syntax": "typescript",
6
+ "decorators": true,
7
+ "dynamicImport": true
8
+ },
9
+ "transform": {
10
+ "decoratorMetadata": true,
11
+ "legacyDecorator": true
12
+ },
13
+ "keepClassNames": true,
14
+ "externalHelpers": true,
15
+ "loose": true
16
+ },
17
+ "module": {
18
+ "type": "commonjs"
19
+ },
20
+ "sourceMaps": true,
21
+ "exclude": [
22
+ "jest.config.ts",
23
+ ".*\\.spec.tsx?$",
24
+ ".*\\.test.tsx?$",
25
+ "./src/jest-setup.ts$",
26
+ "./**/jest-setup.ts$",
27
+ ".*.js$",
28
+ ".*.d.ts$"
29
+ ]
30
+ }
@@ -0,0 +1,194 @@
1
+ # Next.js 15 & Supabase - Ultra-Fast CMS Template
2
+
3
+ This project is a starter template for building an ultra-fast, localized, block-based Content Management System (CMS) using Next.js 15 (App Router), Supabase for the backend (PostgreSQL, Auth, Storage via R2), Tailwind CSS for styling, and shadcn/ui for components.
4
+
5
+ It features:
6
+ - Role-Based Access Control (Admin, Writer, User)
7
+ - Internationalization (i18n) with client-side language switching on single URLs
8
+ - Block-based content editor for Pages and Posts
9
+ - Media uploads to Cloudflare R2 with a Media Library
10
+ - Static Site Generation (SSG) with Incremental Static Regeneration (ISR) for public-facing content
11
+ - On-demand revalidation via Supabase Database Webhooks
12
+
13
+ ## Features Implemented (Phases 1-6)
14
+
15
+ * **Authentication & Authorization (Phase 1):** User roles (ADMIN, WRITER, USER), profiles table linked to `auth.users`, Row Level Security (RLS) on tables, Next.js middleware for route protection, and client-side auth context.
16
+ * **Internationalization (Phase 2):** `languages` table in Supabase, client-side language switching using `LanguageContext` without URL path changes (e.g., `/about-us` serves content based on selected language), and auto-creation of localized placeholder content.
17
+ * **CMS Schema & Core CRUD (Phase 3):** Database tables for `pages`, `posts`, `media`, `blocks`, `navigation_items`. CRUD UIs and server actions for managing Pages, Posts, Navigation Items, Users (role changes), and Languages.
18
+ * **Block-Based Content Builder (Phase 4):** Dynamic block system for Pages (and Posts), UI for adding, editing (basic forms), deleting, and drag-and-drop reordering of content blocks.
19
+ * **Rich Text & Media (Phase 5):** Tiptap rich text editor integrated into "Text" blocks, image insertion from Media Library into Tiptap, and media uploads to Cloudflare R2 with a Media Library UI (upload, view, delete, edit metadata).
20
+ * **SSG & Revalidation (Phase 6):** Static generation of public pages/posts (default language), client-side content fetching for language changes, `generateStaticParams`, `generateMetadata`, and on-demand revalidation via Supabase Database Webhooks calling a Next.js API route.
21
+
22
+ ## Clone and Run Locally
23
+
24
+ 1. **Create a Supabase Project:**
25
+ * Go to the [Supabase dashboard](https://database.new) and create a new project.
26
+
27
+ 2. **Clone This Repository:**
28
+ ```bash
29
+ git clone <your-repository-url> your-cms-app
30
+ cd your-cms-app
31
+ ```
32
+
33
+ 3. **Install Dependencies:**
34
+ ```bash
35
+ npm install
36
+ # or
37
+ yarn install
38
+ # or
39
+ pnpm install
40
+ ```
41
+
42
+ 4. **Set Up Environment Variables:**
43
+ * Rename `.env.example` to `.env.local`.
44
+ * Update the following variables in `.env.local` with your Supabase project details and other configurations:
45
+
46
+ ```env
47
+ # Supabase Project Connection (from your Supabase project's API settings)
48
+ NEXT_PUBLIC_SUPABASE_URL=[https://your-project-ref.supabase.co](https://your-project-ref.supabase.co)
49
+ NEXT_PUBLIC_SUPABASE_ANON_KEY=your-public-anon-key
50
+ SUPABASE_SERVICE_ROLE_KEY=your-service-role-key # Found in API settings, needed for admin actions like deleting users
51
+ SUPABASE_PROJECT_ID=your-supabase-project-id # Used by the Supabase CLI (e.g., in supabase/config.toml)
52
+
53
+ # Cloudflare R2 Storage (from your Cloudflare R2 bucket settings & API token)
54
+ NEXT_PUBLIC_R2_BASE_URL=[https://your-r2-public-url.r2.dev/your-bucket-name](https://your-r2-public-url.r2.dev/your-bucket-name) # Or your custom domain for R2
55
+ R2_ACCOUNT_ID=your_cloudflare_account_id
56
+ R2_ACCESS_KEY_ID=your_r2_access_key_id
57
+ R2_SECRET_ACCESS_KEY=your_r2_secret_access_key
58
+ R2_BUCKET_NAME=your_r2_bucket_name
59
+ R2_S3_ENDPOINT=https://<R2_ACCOUNT_ID>.r2.cloudflarestorage.com # e.g., [https://abcdef12345.r2.cloudflarestorage.com](https://abcdef12345.r2.cloudflarestorage.com)
60
+ R2_REGION=auto # Typically 'auto' for R2
61
+
62
+ # Next.js Site Configuration
63
+ NEXT_PUBLIC_SITE_URL=http://localhost:3000 # For local dev; update for production (e.g., [https://www.yourdomain.com](https://www.yourdomain.com))
64
+ REVALIDATE_SECRET_TOKEN=generate_a_strong_random_string_here # Used to secure the on-demand revalidation API endpoint
65
+ ```
66
+ * `NEXT_PUBLIC_SUPABASE_URL` and `NEXT_PUBLIC_SUPABASE_ANON_KEY` can be found in your Supabase project's API settings.
67
+ * `SUPABASE_SERVICE_ROLE_KEY` is also in API settings (typically hidden by default, click "Reveal").
68
+ * Generate a strong, unique string for `REVALIDATE_SECRET_TOKEN`.
69
+
70
+ 5. **Apply Supabase Migrations:**
71
+ * Ensure you have the Supabase CLI installed and are logged in (`supabase login`).
72
+ * Link your local project to your Supabase project:
73
+ ```bash
74
+ supabase link --project-ref your-project-ref
75
+ ```
76
+ * Apply all database migrations:
77
+ ```bash
78
+ supabase db push
79
+ ```
80
+ Alternatively, if you prefer to run migrations individually (e.g., for a fresh setup or to ensure order):
81
+ ```bash
82
+ supabase migration up
83
+ ```
84
+ * This will create all necessary tables (`profiles`, `languages`, `pages`, `posts`, `media`, `blocks`, `navigation_items`), roles, RLS policies, and helper functions.
85
+
86
+ 6. **Configure Supabase Database Webhooks for On-Demand Revalidation:**
87
+ Since this project avoids using Supabase Edge Functions (and their Docker dependency) for revalidation, you need to manually set up Database Webhooks to call your Next.js API endpoint directly.
88
+
89
+ * Go to your Supabase Project Dashboard -> Database -> Webhooks.
90
+ * Click "Create a new webhook".
91
+
92
+ * **For the `pages` Table:**
93
+ * **Name:** `Next.js Revalidate Pages` (or similar)
94
+ * **Table:** Select `pages` (from the `public` schema).
95
+ * **Events:** Check `INSERT`, `UPDATE`, `DELETE`.
96
+ * **Webhook Type:** `HTTP Request`
97
+ * **HTTP URL:** Your Next.js application's revalidation API endpoint.
98
+ * For local development (if using a tunneling service like ngrok to expose localhost): `http://your-ngrok-url.ngrok.io/api/revalidate`
99
+ * For production (e.g., Vercel): `https://your-app-name.vercel.app/api/revalidate`
100
+ * **HTTP Method:** `POST`
101
+ * **HTTP Headers:**
102
+ * Click "Add header".
103
+ * Header name: `x-revalidate-secret`
104
+ * Header value: The same `REVALIDATE_SECRET_TOKEN` you set in your `.env.local`.
105
+ * Click "Add header" again.
106
+ * Header name: `Content-Type`
107
+ * Header value: `application/json`
108
+ * Click "Create webhook".
109
+
110
+ * **For the `posts` Table:**
111
+ * Create another webhook with similar settings:
112
+ * **Name:** `Next.js Revalidate Posts`
113
+ * **Table:** `posts`
114
+ * **Events:** `INSERT`, `UPDATE`, `DELETE`.
115
+ * **HTTP URL:** Same as above.
116
+ * **HTTP Method:** `POST`
117
+ * **HTTP Headers:**
118
+ * `x-revalidate-secret`: Your `REVALIDATE_SECRET_TOKEN`
119
+ * `Content-Type`: `application/json`
120
+ * Click "Create webhook".
121
+
122
+ 7. **Run the Next.js Development Server:**
123
+ ```bash
124
+ npm run dev
125
+ # or
126
+ yarn dev
127
+ # or
128
+ pnpm dev
129
+ ```
130
+ The application should now be running on [http://localhost:3000](http://localhost:3000/).
131
+
132
+ 8. **Initial Admin User Setup:**
133
+ * Sign up for a new user account through the application's sign-up page.
134
+ * After signing up and verifying the email, you'll need to manually update this user's role to `ADMIN` in the Supabase `profiles` table. You can do this via the Supabase Studio (Table Editor -> `profiles` table).
135
+ * Find your user's row (by their ID, which matches `auth.users.id`).
136
+ * Change the `role` column value from `USER` to `ADMIN`.
137
+
138
+ 9. **Shadcn/UI Styling (Optional):**
139
+ * This template comes with the default shadcn/ui style initialized. If you want to customize the theme or use a different base color, you can delete `components.json` and re-initialize shadcn/ui following their [official documentation](https://ui.shadcn.com/docs/installation/next).
140
+
141
+ ## Project Structure Highlights
142
+
143
+ * `app/`: Next.js App Router.
144
+ * `app/(auth-pages)/`: Routes for sign-in, sign-up, etc.
145
+ * `app/cms/`: CMS admin panel routes and layouts.
146
+ * `app/cms/[entity]/`: CRUD pages for different content types (pages, posts, media, users, navigation, languages).
147
+ * `app/cms/blocks/`: Components and actions related to the block editor.
148
+ * `app/[slug]/`: Dynamic route for public "Pages".
149
+ * `app/blog/[slug]/`: Dynamic route for public "Posts".
150
+ * `app/api/`: API routes (e.g., for revalidation, R2 pre-signed URLs).
151
+ * `components/`: Shared UI components (shadcn/ui based).
152
+ * `components/ui/`: shadcn/ui components.
153
+ * `context/`: React Context providers (e.g., `AuthContext`, `LanguageContext`).
154
+ * `lib/`: Utility functions and configurations.
155
+ * `lib/cloudflare/`: Client for Cloudflare R2.
156
+ * `utils/supabase/`: Supabase client setup, types, and middleware helpers.
157
+ * `supabase/migrations/`: SQL database migrations.
158
+
159
+ ## Documentation
160
+
161
+ For a deeper understanding of the CMS's internal workings, please refer to the detailed documentation:
162
+
163
+ * **[CMS Application Overview](./docs/cms-application-overview.md):** A high-level guide to the Next.js application structure, core modules, and key functionalities.
164
+ * **[Block Editor Architecture](./docs/cms-architecture-overview.md):** A technical deep-dive into the architecture of the block-based content editor.
165
+
166
+ ## Deployment
167
+
168
+ This project is optimized for deployment on [Vercel](https://vercel.com/).
169
+
170
+ 1. Push your code to a GitHub/GitLab/Bitbucket repository.
171
+ 2. Import the project into Vercel.
172
+ 3. **Configure Environment Variables in Vercel:**
173
+ * Add all the environment variables from your `.env.local` file to your Vercel project settings (Project Settings -> Environment Variables). This includes Supabase keys, R2 keys, `NEXT_PUBLIC_SITE_URL` (set to your production domain), and `REVALIDATE_SECRET_TOKEN`.
174
+ 4. Vercel will automatically build and deploy your Next.js application.
175
+ 5. Ensure your Supabase Database Webhooks are pointing to your production Next.js API endpoint for revalidation.
176
+
177
+ ## Database Backup
178
+
179
+ This project includes a simple script to backup your Supabase PostgreSQL database.
180
+
181
+ **Requirements:**
182
+ * You must have the PostgreSQL command-line tools (`pg_dump`) installed and available in your system's PATH.
183
+
184
+ **Usage:**
185
+ To create a backup, run the following command from your project root:
186
+
187
+ ```bash
188
+ npm run db:backup
189
+ ```
190
+
191
+ This command will generate a timestamped SQL dump file and save it to the `backup/` directory.
192
+ ## Feedback and Issues
193
+
194
+ Please file feedback and issues on the GitHub repository for this project.
@@ -0,0 +1,57 @@
1
+ 'use client';
2
+
3
+ import React from "react";
4
+ import { forgotPasswordAction } from "../../actions";
5
+ import { FormMessage, Message } from "../../../components/form-message";
6
+ import { SubmitButton } from "../../../components/submit-button";
7
+ import { Input } from "@nextblock-cms/ui";
8
+ import { Label } from "@nextblock-cms/ui";
9
+ import Link from "next/link";
10
+ import { useTranslations } from "@nextblock-cms/utils";
11
+ import { useSearchParams } from "next/navigation";
12
+
13
+ function getMessage(searchParams: URLSearchParams): Message | undefined {
14
+ if (searchParams.has('error')) {
15
+ const error = searchParams.get('error');
16
+ if (error) return { error };
17
+ }
18
+ if (searchParams.has('success')) {
19
+ const success = searchParams.get('success');
20
+ if (success) return { success };
21
+ }
22
+ if (searchParams.has('message')) {
23
+ const message = searchParams.get('message');
24
+ if (message) return { message };
25
+ }
26
+ return undefined;
27
+ }
28
+
29
+ export default function ForgotPassword() {
30
+ const { t } = useTranslations();
31
+ const searchParams = useSearchParams();
32
+ const formMessage = getMessage(searchParams);
33
+
34
+ return (
35
+ <>
36
+ <form className="flex-1 flex flex-col w-full gap-2 text-foreground [&>input]:mb-6 min-w-64 max-w-64 mx-auto">
37
+ <div>
38
+ <h1 className="text-2xl font-medium">{t('reset_password')}</h1>
39
+ <p className="text-sm text-secondary-foreground">
40
+ {t('already_have_account')}{" "}
41
+ <Link className="text-primary underline" href="/sign-in">
42
+ {t('sign_in')}
43
+ </Link>
44
+ </p>
45
+ </div>
46
+ <div className="flex flex-col gap-2 [&>input]:mb-3 mt-8">
47
+ <Label htmlFor="email">{t('email')}</Label>
48
+ <Input name="email" placeholder={t('you_at_example_com')} required />
49
+ <SubmitButton formAction={forgotPasswordAction}>
50
+ {t('reset_password')}
51
+ </SubmitButton>
52
+ <FormMessage message={formMessage} />
53
+ </div>
54
+ </form>
55
+ </>
56
+ );
57
+ }
@@ -0,0 +1,9 @@
1
+ export default async function Layout({
2
+ children,
3
+ }: {
4
+ children: React.ReactNode;
5
+ }) {
6
+ return (
7
+ <div className="max-w-7xl mx-auto flex flex-col gap-12 items-start mb-4">{children}</div>
8
+ );
9
+ }
@@ -0,0 +1,28 @@
1
+ 'use client';
2
+
3
+ import { useEffect } from 'react';
4
+ import { useSearchParams } from 'next/navigation';
5
+
6
+ export default function PostSignIn() {
7
+ const searchParams = useSearchParams();
8
+ const redirectTo = searchParams.get('redirect_to');
9
+
10
+ useEffect(() => {
11
+ if (redirectTo) {
12
+ window.location.href = redirectTo;
13
+ } else {
14
+ window.location.href = '/';
15
+ }
16
+ }, [redirectTo]);
17
+
18
+ return (
19
+ <div className="flex justify-center items-center h-full w-full py-20">
20
+ <div className="relative">
21
+ <div className="h-16 w-16 rounded-full border-t-4 border-b-4 border-primary animate-spin"></div>
22
+ <div className="absolute inset-0 flex items-center justify-center">
23
+ <div className="h-10 w-10 rounded-full bg-background"></div>
24
+ </div>
25
+ </div>
26
+ </div>
27
+ );
28
+ }