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
@@ -0,0 +1,241 @@
1
+ // app/cms/pages/actions.ts
2
+ "use server";
3
+
4
+ import { createClient } from "@nextblock-cms/db/server";
5
+ import { revalidatePath } from "next/cache";
6
+ import { redirect } from "next/navigation";
7
+ import type { Database } from "@nextblock-cms/db";
8
+ import { v4 as uuidv4 } from 'uuid';
9
+
10
+ type PageStatus = Database['public']['Enums']['page_status'];
11
+ import { encodedRedirect } from "@nextblock-cms/utils/server";
12
+ import { getFullPageContent } from "../revisions/utils";
13
+ import { createPageRevision } from "../revisions/service";
14
+
15
+ // --- createPage and updatePage functions remain unchanged ---
16
+
17
+ export async function createPage(formData: FormData) {
18
+ const supabase = createClient();
19
+ const { data: { user } } = await supabase.auth.getUser();
20
+ if (!user) return encodedRedirect("error", "/cms/pages/new", "User not authenticated.");
21
+
22
+
23
+ const rawFormData = {
24
+ title: formData.get("title") as string,
25
+ slug: formData.get("slug") as string,
26
+ language_id: parseInt(formData.get("language_id") as string, 10),
27
+ status: formData.get("status") as PageStatus,
28
+ meta_title: formData.get("meta_title") as string || null,
29
+ meta_description: formData.get("meta_description") as string || null,
30
+ };
31
+
32
+ if (!rawFormData.title || !rawFormData.slug || isNaN(rawFormData.language_id) || !rawFormData.status) {
33
+ return encodedRedirect("error", "/cms/pages/new", "Missing required fields: title, slug, language, or status.");
34
+ }
35
+
36
+ const translation_group_id = formData.get("translation_group_id") as string || uuidv4();
37
+
38
+ // Check if a translation for this language already exists
39
+ if (formData.get("translation_group_id")) {
40
+ const { data: existingTranslation, error: checkError } = await supabase
41
+ .from("pages")
42
+ .select("id")
43
+ .eq("translation_group_id", formData.get("translation_group_id") as string)
44
+ .eq("language_id", rawFormData.language_id)
45
+ .maybeSingle();
46
+
47
+ if (checkError) {
48
+ console.error("Error checking for existing translation:", checkError);
49
+ // Decide if we should halt or just log. For now, we'll proceed.
50
+ }
51
+
52
+ if (existingTranslation) {
53
+ // A translation for this language already exists, redirect to its edit page.
54
+ redirect(`/cms/pages/${existingTranslation.id}/edit?warning=${encodeURIComponent("A page for this language already exists. You are now editing it.")}`);
55
+ }
56
+ }
57
+
58
+ const pageData: UpsertPagePayload = {
59
+ ...rawFormData,
60
+ author_id: user.id,
61
+ translation_group_id: translation_group_id,
62
+ };
63
+
64
+ const { data: newPage, error: createError } = await supabase
65
+ .from("pages")
66
+ .insert(pageData)
67
+ .select("id, title, slug, language_id, translation_group_id")
68
+ .single();
69
+
70
+ if (createError) {
71
+ console.error("Error creating page:", createError);
72
+ if (createError.code === '23505' && createError.message.includes('pages_language_id_slug_key')) {
73
+ return encodedRedirect("error", "/cms/pages/new", `The slug "${pageData.slug}" already exists for the selected language. Please use a unique slug.`);
74
+ }
75
+ return encodedRedirect("error", "/cms/pages/new", `Failed to create page: ${createError.message}`);
76
+ }
77
+
78
+ revalidatePath("/cms/pages");
79
+ if (newPage?.slug) revalidatePath(`/${newPage.slug}`);
80
+
81
+ if (newPage?.id) {
82
+ redirect(`/cms/pages/${newPage.id}/edit?success=${encodeURIComponent("Page created successfully.")}`);
83
+ } else {
84
+ redirect(`/cms/pages?success=${encodeURIComponent("Page created successfully.")}`);
85
+ }
86
+ }
87
+
88
+ export async function updatePage(pageId: number, formData: FormData) {
89
+ const supabase = createClient();
90
+ const { data: { user } } = await supabase.auth.getUser();
91
+ const pageEditPath = `/cms/pages/${pageId}/edit`;
92
+
93
+ if (!user) return encodedRedirect("error", pageEditPath, "User not authenticated.");
94
+
95
+ const { data: existingPage, error: fetchError } = await supabase
96
+ .from("pages")
97
+ .select("translation_group_id, slug")
98
+ .eq("id", pageId)
99
+ .single();
100
+
101
+ if (fetchError || !existingPage) {
102
+ return encodedRedirect("error", "/cms/pages", "Original page not found or error fetching it.");
103
+ }
104
+
105
+ const rawFormData = {
106
+ title: formData.get("title") as string,
107
+ slug: formData.get("slug") as string,
108
+ language_id: parseInt(formData.get("language_id") as string, 10),
109
+ status: formData.get("status") as PageStatus,
110
+ meta_title: formData.get("meta_title") as string || null,
111
+ meta_description: formData.get("meta_description") as string || null,
112
+ };
113
+
114
+ if (!rawFormData.title || !rawFormData.slug || isNaN(rawFormData.language_id) || !rawFormData.status) {
115
+ return encodedRedirect("error", pageEditPath, "Missing required fields: title, slug, language, or status.");
116
+ }
117
+
118
+ const pageUpdateData: Partial<Omit<UpsertPagePayload, 'translation_group_id' | 'author_id'>> = {
119
+ title: rawFormData.title,
120
+ slug: rawFormData.slug,
121
+ language_id: rawFormData.language_id,
122
+ status: rawFormData.status,
123
+ meta_title: rawFormData.meta_title,
124
+ meta_description: rawFormData.meta_description,
125
+ };
126
+
127
+ // capture previous full content before update
128
+ const previousContent = await getFullPageContent(pageId);
129
+
130
+ const { error: updateError } = await supabase
131
+ .from("pages")
132
+ .update(pageUpdateData)
133
+ .eq("id", pageId);
134
+
135
+ if (updateError) {
136
+ console.error("Error updating page:", updateError);
137
+ if (updateError.code === '23505' && updateError.message.includes('pages_language_id_slug_key')) {
138
+ return encodedRedirect("error", pageEditPath, `The slug "${pageUpdateData.slug}" already exists for the selected language. Please use a unique slug.`);
139
+ }
140
+ return encodedRedirect("error", pageEditPath, `Failed to update page: ${updateError.message}`);
141
+ }
142
+
143
+ // create revision after update
144
+ if (previousContent && user) {
145
+ const newContent = await getFullPageContent(pageId);
146
+ if (newContent) {
147
+ await createPageRevision(pageId, user.id, previousContent, newContent);
148
+ }
149
+ }
150
+
151
+ revalidatePath("/cms/pages");
152
+ if (existingPage.slug) revalidatePath(`/${existingPage.slug}`);
153
+ if (rawFormData.slug && rawFormData.slug !== existingPage.slug) {
154
+ revalidatePath(`/${rawFormData.slug}`);
155
+ }
156
+ revalidatePath(pageEditPath);
157
+ redirect(`${pageEditPath}?success=Page updated successfully`);
158
+ }
159
+
160
+
161
+ export async function deletePage(pageId: number) {
162
+ const supabase = createClient();
163
+
164
+ // 1. Fetch the Translation Group
165
+ const { data: page, error: fetchError } = await supabase
166
+ .from("pages")
167
+ .select("translation_group_id")
168
+ .eq("id", pageId)
169
+ .single();
170
+
171
+ if (fetchError || !page) {
172
+ console.error("Error fetching page for deletion:", fetchError);
173
+ return encodedRedirect("error", "/cms/pages", "Page not found.");
174
+ }
175
+
176
+ const { translation_group_id } = page;
177
+
178
+ // 2. Find All Related Pages
179
+ const { data: relatedPages, error: relatedPagesError } = await supabase
180
+ .from("pages")
181
+ .select("slug")
182
+ .eq("translation_group_id", translation_group_id);
183
+
184
+ if (relatedPagesError) {
185
+ console.error("Error fetching related pages:", relatedPagesError);
186
+ return encodedRedirect("error", "/cms/pages", "Could not fetch related pages for deletion.");
187
+ }
188
+
189
+ // 3. Delete All Associated Navigation Links
190
+ if (relatedPages && relatedPages.length > 0) {
191
+ const slugs = relatedPages.map(p => p.slug).filter((s): s is string => s !== null);
192
+ if (slugs.length > 0) {
193
+ const pathsToDelete = slugs.map(slug => `/${slug}`);
194
+ const { error: navError } = await supabase
195
+ .from("navigation_items")
196
+ .delete()
197
+ .in("url", pathsToDelete);
198
+
199
+ if (navError) {
200
+ console.error("Error deleting navigation links:", navError);
201
+ // Do not block deletion of pages if nav items fail to delete
202
+ }
203
+ }
204
+ }
205
+
206
+ // 4. Delete All Related Pages
207
+ const { error: deletePagesError } = await supabase
208
+ .from("pages")
209
+ .delete()
210
+ .eq("translation_group_id", translation_group_id);
211
+
212
+ if (deletePagesError) {
213
+ console.error("Error deleting pages:", deletePagesError);
214
+ return encodedRedirect("error", "/cms/pages", `Failed to delete pages: ${deletePagesError.message}`);
215
+ }
216
+
217
+ // Revalidate paths to reflect the deletion
218
+ revalidatePath("/cms/pages");
219
+ revalidatePath("/cms/navigation");
220
+ if (relatedPages) {
221
+ relatedPages.forEach(p => {
222
+ if (p.slug) {
223
+ revalidatePath(`/${p.slug}`);
224
+ }
225
+ });
226
+ }
227
+
228
+ // 5. Update Redirect Message
229
+ redirect(`/cms/pages?success=${encodeURIComponent("Page and all its translations were deleted successfully.")}`);
230
+ }
231
+
232
+ type UpsertPagePayload = {
233
+ language_id: number;
234
+ author_id: string | null;
235
+ title: string;
236
+ slug: string; // Now language-specific
237
+ status: PageStatus;
238
+ meta_title?: string | null;
239
+ meta_description?: string | null;
240
+ translation_group_id: string; // UUID
241
+ };
@@ -0,0 +1,47 @@
1
+ // app/cms/pages/components/DeletePageButtonClient.tsx
2
+ "use client";
3
+
4
+ import React, { useState, useTransition } from 'react';
5
+ import { DropdownMenuItem } from "@nextblock-cms/ui";
6
+ import { Trash2 } from "lucide-react";
7
+ import { deletePage } from "../actions";
8
+ import { ConfirmationModal } from '@/app/cms/components/ConfirmationModal';
9
+
10
+ interface DeletePageButtonClientProps {
11
+ pageId: number;
12
+ pageTitle: string;
13
+ }
14
+
15
+ export default function DeletePageButtonClient({ pageId, pageTitle }: DeletePageButtonClientProps) {
16
+ const [isModalOpen, setIsModalOpen] = useState(false);
17
+ const [isPending, startTransition] = useTransition();
18
+
19
+ const handleDelete = () => {
20
+ startTransition(() => {
21
+ deletePage(pageId);
22
+ });
23
+ };
24
+
25
+ return (
26
+ <>
27
+ <DropdownMenuItem
28
+ className="text-red-600 hover:!text-red-600 hover:!bg-red-50 dark:hover:!bg-red-700/20 cursor-pointer"
29
+ onSelect={(e) => {
30
+ e.preventDefault();
31
+ setIsModalOpen(true);
32
+ }}
33
+ >
34
+ <Trash2 className="mr-2 h-4 w-4" />
35
+ Delete
36
+ </DropdownMenuItem>
37
+ <ConfirmationModal
38
+ isOpen={isModalOpen}
39
+ onClose={() => setIsModalOpen(false)}
40
+ onConfirm={handleDelete}
41
+ title="Are you sure?"
42
+ description={`This will permanently delete the page '${pageTitle}'. This action cannot be undone.`}
43
+ confirmText={isPending ? "Deleting..." : "Delete"}
44
+ />
45
+ </>
46
+ );
47
+ }
@@ -0,0 +1,253 @@
1
+ // app/cms/pages/components/PageForm.tsx
2
+ "use client";
3
+
4
+ import { useEffect, useState, useTransition } from "react";
5
+ import { useRouter, useSearchParams } from "next/navigation";
6
+ import { Button } from "@nextblock-cms/ui";
7
+ import { Input } from "@nextblock-cms/ui";
8
+ import { Label } from "@nextblock-cms/ui";
9
+ import {
10
+ Select,
11
+ SelectContent,
12
+ SelectItem,
13
+ SelectTrigger,
14
+ SelectValue,
15
+ } from "@nextblock-cms/ui";
16
+ import { Textarea } from "@nextblock-cms/ui";
17
+ import type { Database } from "@nextblock-cms/db";
18
+ import { useAuth } from "@/context/AuthContext";
19
+
20
+ type Page = Database['public']['Tables']['pages']['Row'];
21
+ type PageStatus = Database['public']['Enums']['page_status'];
22
+ type Language = Database['public']['Tables']['languages']['Row'];
23
+ // Remove: import { getActiveLanguagesClientSide } from "@nextblock-cms/db";
24
+
25
+ interface PageFormProps {
26
+ page?: Page | null;
27
+ formAction: (formData: FormData) => Promise<{ error?: string } | void>;
28
+ actionButtonText?: string;
29
+ isEditing?: boolean;
30
+ availableLanguagesProp: Language[]; // New prop
31
+ translationGroupId?: string;
32
+ target_lang_id?: string;
33
+ }
34
+
35
+ export default function PageForm({
36
+ page,
37
+ formAction,
38
+ actionButtonText = "Save Page",
39
+ isEditing = false,
40
+ availableLanguagesProp, // Use the new prop
41
+ translationGroupId,
42
+ target_lang_id,
43
+ }: PageFormProps) {
44
+ const router = useRouter();
45
+ const searchParams = useSearchParams();
46
+ const [isPending, startTransition] = useTransition();
47
+ const { user, isLoading: authLoading } = useAuth();
48
+
49
+ const [title, setTitle] = useState(page?.title || "");
50
+ const [slug, setSlug] = useState(page?.slug || "");
51
+ const [languageId, setLanguageId] = useState<string>(() => {
52
+ // If editing, use the page's language
53
+ if (page?.language_id) {
54
+ return page.language_id.toString();
55
+ }
56
+ // If creating a translation, use the target language
57
+ if (target_lang_id) {
58
+ return target_lang_id;
59
+ }
60
+ // Otherwise, find the default language from the available languages
61
+ if (availableLanguagesProp && availableLanguagesProp.length > 0) {
62
+ const defaultLang = availableLanguagesProp.find((l) => l.is_default);
63
+ if (defaultLang) {
64
+ return defaultLang.id.toString();
65
+ }
66
+ // As a fallback, use the first available language
67
+ return availableLanguagesProp[0].id.toString();
68
+ }
69
+ // If no languages are available, default to an empty string
70
+ return "";
71
+ });
72
+ const [status, setStatus] = useState<PageStatus>(page?.status || "draft");
73
+ const [metaTitle, setMetaTitle] = useState(page?.meta_title || "");
74
+ const [metaDescription, setMetaDescription] = useState(
75
+ page?.meta_description || ""
76
+ );
77
+
78
+ // Use the passed-in languages
79
+ const [availableLanguages] = useState<Language[]>(availableLanguagesProp);
80
+ // languagesLoading is no longer needed if languages are passed as props
81
+ // const [languagesLoading, setLanguagesLoading] = useState(true); // Remove or set to false initially
82
+
83
+ const [formMessage, setFormMessage] = useState<{ type: 'success' | 'error', text: string } | null>(null);
84
+
85
+ useEffect(() => {
86
+ const successMessage = searchParams.get('success');
87
+ const errorMessage = searchParams.get('error');
88
+ if (successMessage) {
89
+ setFormMessage({ type: 'success', text: successMessage });
90
+ } else if (errorMessage) {
91
+ setFormMessage({ type: 'error', text: errorMessage });
92
+ }
93
+ }, [searchParams]);
94
+
95
+
96
+
97
+ const handleTitleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
98
+ const newTitle = e.target.value;
99
+ setTitle(newTitle);
100
+ if (!isEditing || !slug) {
101
+ setSlug(newTitle.toLowerCase().replace(/\s+/g, "-").replace(/[^\w-]+/g, ""));
102
+ }
103
+ };
104
+
105
+ const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
106
+ event.preventDefault();
107
+ setFormMessage(null);
108
+ const formData = new FormData(event.currentTarget);
109
+
110
+ startTransition(async () => {
111
+ const result = await formAction(formData);
112
+ if (result?.error) {
113
+ setFormMessage({ type: 'error', text: result.error });
114
+ }
115
+ });
116
+ };
117
+
118
+ // Removed languagesLoading from this condition
119
+ if (authLoading) {
120
+ return <div>Loading form...</div>;
121
+ }
122
+
123
+ if (!user) {
124
+ return <div>Please log in to manage pages.</div>;
125
+ }
126
+
127
+ return (
128
+ <form onSubmit={handleSubmit} className="space-y-6 w-full mx-auto px-6">
129
+ {/* ... (rest of the form remains the same, but `availableLanguages` is now populated by the prop) ... */}
130
+ {formMessage && (
131
+ <div
132
+ className={`p-3 rounded-md text-sm ${
133
+ formMessage.type === 'success'
134
+ ? 'bg-green-100 text-green-700 border border-green-200'
135
+ : 'bg-red-100 text-red-700 border border-red-200'
136
+ }`}
137
+ >
138
+ {formMessage.text}
139
+ </div>
140
+ )}
141
+ {translationGroupId && (
142
+ <input type="hidden" name="translation_group_id" value={translationGroupId} />
143
+ )}
144
+ <div>
145
+ <Label htmlFor="title">Title</Label>
146
+ <Input
147
+ id="title"
148
+ name="title"
149
+ value={title}
150
+ onChange={handleTitleChange}
151
+ required
152
+ className="mt-1"
153
+ />
154
+ </div>
155
+
156
+ <div>
157
+ <Label htmlFor="slug">Slug</Label>
158
+ <Input
159
+ id="slug"
160
+ name="slug"
161
+ value={slug}
162
+ onChange={(e) => setSlug(e.target.value)}
163
+ required
164
+ className="mt-1"
165
+ />
166
+ <p className="text-xs text-muted-foreground mt-1">URL-friendly identifier. Auto-generated from title if left empty on creation.</p>
167
+ </div>
168
+
169
+ <div>
170
+ <Label htmlFor="language_id">Language</Label>
171
+ {availableLanguages.length > 0 ? (
172
+ <Select
173
+ name="language_id"
174
+ defaultValue={target_lang_id}
175
+ value={languageId}
176
+ onValueChange={setLanguageId}
177
+ required
178
+ >
179
+ <SelectTrigger className="mt-1">
180
+ <SelectValue placeholder="Select language" />
181
+ </SelectTrigger>
182
+ <SelectContent>
183
+ {availableLanguages.map((lang) => (
184
+ <SelectItem key={lang.id} value={lang.id.toString()}>
185
+ {lang.name} ({lang.code})
186
+ </SelectItem>
187
+ ))}
188
+ </SelectContent>
189
+ </Select>
190
+ ) : (
191
+ <p className="text-sm text-muted-foreground mt-1">No languages available. Please add languages in CMS settings.</p>
192
+ )}
193
+ </div>
194
+
195
+ <div>
196
+ <Label htmlFor="status">Status</Label>
197
+ <Select
198
+ name="status"
199
+ value={status}
200
+ onValueChange={(value) => setStatus(value as PageStatus)}
201
+ required
202
+ >
203
+ <SelectTrigger className="mt-1">
204
+ <SelectValue placeholder="Select status" />
205
+ </SelectTrigger>
206
+ <SelectContent>
207
+ <SelectItem value="draft">Draft</SelectItem>
208
+ <SelectItem value="published">Published</SelectItem>
209
+ <SelectItem value="archived">Archived</SelectItem>
210
+ </SelectContent>
211
+ </Select>
212
+ </div>
213
+
214
+ <div>
215
+ <Label htmlFor="meta_title">Meta Title (SEO)</Label>
216
+ <Input
217
+ id="meta_title"
218
+ name="meta_title"
219
+ value={metaTitle}
220
+ onChange={(e) => setMetaTitle(e.target.value)}
221
+ className="mt-1"
222
+ />
223
+ </div>
224
+
225
+ <div>
226
+ <Label htmlFor="meta_description">Meta Description (SEO)</Label>
227
+ <Textarea
228
+ id="meta_description"
229
+ name="meta_description"
230
+ value={metaDescription}
231
+ onChange={(e) => setMetaDescription(e.target.value)}
232
+ className="mt-1"
233
+ rows={3}
234
+ />
235
+ </div>
236
+
237
+ <div className="flex justify-end space-x-3">
238
+ <Button
239
+ type="button"
240
+ variant="outline"
241
+ onClick={() => router.push("/cms/pages")}
242
+ disabled={isPending}
243
+ >
244
+ Cancel
245
+ </Button>
246
+ {/* Ensure button is not disabled due to removed languagesLoading */}
247
+ <Button type="submit" disabled={isPending || authLoading || availableLanguages.length === 0}>
248
+ {isPending ? "Saving..." : actionButtonText}
249
+ </Button>
250
+ </div>
251
+ </form>
252
+ );
253
+ }
@@ -0,0 +1,52 @@
1
+ // app/cms/pages/new/page.tsx
2
+ import PageForm from "../components/PageForm";
3
+ import { createPage } from "../actions"; // Server action for creating a page
4
+ import { createClient } from "@nextblock-cms/db/server";
5
+ import type { Database } from "@nextblock-cms/db";
6
+
7
+ type Language = Database['public']['Tables']['languages']['Row'];
8
+
9
+ interface NewPageProps {
10
+ searchParams?: Promise<{
11
+ from_group?: string | string[];
12
+ target_lang_id?: string | string[];
13
+ }>;
14
+ }
15
+
16
+ export default async function NewPage(props: NewPageProps) {
17
+ const searchParams = (await props.searchParams) || {};
18
+ const supabase = createClient();
19
+ const { data: fetchedLanguages, error: languagesError } = await supabase
20
+ .from("languages")
21
+ .select("*")
22
+ .order("name");
23
+
24
+ if (languagesError) {
25
+ console.error("Error fetching languages for NewPage:", languagesError.message);
26
+ // Optionally, you could redirect or show a more user-friendly error
27
+ }
28
+
29
+ const availableLanguages: Language[] = fetchedLanguages || [];
30
+
31
+ return (
32
+ <div className="max-w-2xl mx-auto">
33
+ <h1 className="text-2xl font-bold mb-6">Create New Page</h1>
34
+ <PageForm
35
+ formAction={createPage}
36
+ actionButtonText="Create Page"
37
+ isEditing={false}
38
+ availableLanguagesProp={availableLanguages}
39
+ translationGroupId={
40
+ Array.isArray(searchParams.from_group)
41
+ ? searchParams.from_group[0]
42
+ : searchParams.from_group
43
+ }
44
+ target_lang_id={
45
+ Array.isArray(searchParams.target_lang_id)
46
+ ? searchParams.target_lang_id[0]
47
+ : searchParams.target_lang_id
48
+ }
49
+ />
50
+ </div>
51
+ );
52
+ }