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,309 @@
1
+ // app/cms/posts/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"; // Ensure this is correctly imported
12
+ import { getFullPostContent } from "../revisions/utils";
13
+ import { createPostRevision } from "../revisions/service";
14
+
15
+ // --- createPost and updatePost functions to be updated similarly for error returns ---
16
+
17
+ export async function createPost(formData: FormData) {
18
+ const supabase = createClient();
19
+ const { data: { user } } = await supabase.auth.getUser();
20
+ if (!user) {
21
+ return encodedRedirect("error", "/cms/posts/new", "User not authenticated.");
22
+ }
23
+
24
+ const featureImageIdStr_create = formData.get("feature_image_id") as string;
25
+ let featureImageId_create: string | null = null;
26
+ if (featureImageIdStr_create && featureImageIdStr_create.trim() !== "") {
27
+ featureImageId_create = featureImageIdStr_create;
28
+ }
29
+
30
+ const rawFormData = {
31
+ title: formData.get("title") as string,
32
+ slug: formData.get("slug") as string,
33
+ language_id: parseInt(formData.get("language_id") as string, 10),
34
+ status: formData.get("status") as PageStatus,
35
+ excerpt: formData.get("excerpt") as string || null,
36
+ published_at: formData.get("published_at") as string || null,
37
+ meta_title: formData.get("meta_title") as string || null,
38
+ meta_description: formData.get("meta_description") as string || null,
39
+ feature_image_id: featureImageId_create,
40
+ };
41
+
42
+ if (!rawFormData.title || !rawFormData.slug || isNaN(rawFormData.language_id) || !rawFormData.status) {
43
+ return encodedRedirect("error", "/cms/posts/new", "Missing required fields: title, slug, language, or status.");
44
+ }
45
+
46
+ let publishedAtISO: string | null = null;
47
+ if (rawFormData.published_at) {
48
+ const parsedDate = new Date(rawFormData.published_at);
49
+ if (!isNaN(parsedDate.getTime())) publishedAtISO = parsedDate.toISOString();
50
+ else publishedAtISO = rawFormData.published_at;
51
+ }
52
+
53
+ const newTranslationGroupId = uuidv4();
54
+
55
+ const postData: UpsertPostPayload = {
56
+ ...rawFormData,
57
+ published_at: publishedAtISO,
58
+ author_id: user.id,
59
+ translation_group_id: newTranslationGroupId,
60
+ feature_image_id: rawFormData.feature_image_id,
61
+ };
62
+
63
+ const { data: newPost, error: createError } = await supabase
64
+ .from("posts")
65
+ .insert(postData)
66
+ .select("id, title, slug, language_id, translation_group_id, excerpt, feature_image_id") // Added excerpt, feature_image_id
67
+ .single();
68
+
69
+ if (createError) {
70
+ console.error("Error creating post:", createError);
71
+ if (createError.code === '23505' && createError.message.includes('posts_language_id_slug_key')) {
72
+ return encodedRedirect("error", "/cms/posts/new", `The slug "${postData.slug}" already exists for the selected language. Please use a unique slug.`);
73
+ }
74
+ return encodedRedirect("error", "/cms/posts/new", `Failed to create post: ${createError.message}`);
75
+ }
76
+
77
+ let successMessage = "Post created successfully.";
78
+
79
+ if (newPost) {
80
+ const { data: languages, error: langError } = await supabase
81
+ .from("languages")
82
+ .select("id, code")
83
+ .neq("id", newPost.language_id);
84
+
85
+ if (langError) {
86
+ console.error("Error fetching other languages for post auto-creation:", langError);
87
+ } else if (languages && languages.length > 0) {
88
+ let placeholderCreations = 0;
89
+ for (const lang of languages) {
90
+ const placeholderSlug = generatePlaceholderSlug(newPost.title, lang.code);
91
+ const placeholderPostData: Omit<UpsertPostPayload, 'author_id'> & {author_id?: string | null} = {
92
+ language_id: lang.id,
93
+ title: `[${lang.code.toUpperCase()}] ${newPost.title}`,
94
+ slug: placeholderSlug,
95
+ status: 'draft',
96
+ published_at: null,
97
+ excerpt: `Placeholder for ${lang.code.toUpperCase()} translation. Original excerpt: ${newPost.excerpt || ''}`.substring(0, 250),
98
+ meta_title: null,
99
+ meta_description: null,
100
+ translation_group_id: newPost.translation_group_id,
101
+ author_id: user.id,
102
+ };
103
+ const { error: placeholderError } = await supabase.from("posts").insert(placeholderPostData);
104
+ if (placeholderError) {
105
+ console.error(`Error auto-creating post for language ${lang.code} (slug: ${placeholderSlug}):`, placeholderError);
106
+ } else {
107
+ placeholderCreations++;
108
+ }
109
+ }
110
+ if (placeholderCreations > 0) {
111
+ successMessage += ` ${placeholderCreations} placeholder version(s) also created (draft status, please edit their slugs and content).`;
112
+ }
113
+ }
114
+ }
115
+
116
+ revalidatePath("/cms/posts");
117
+ if (newPost?.slug) revalidatePath(`/blog/${newPost.slug}`);
118
+
119
+ if (newPost?.id) {
120
+ redirect(`/cms/posts/${newPost.id}/edit?success=${encodeURIComponent(successMessage)}`);
121
+ } else {
122
+ redirect(`/cms/posts?success=${encodeURIComponent(successMessage)}`);
123
+ }
124
+ }
125
+
126
+ export async function updatePost(postId: number, formData: FormData) {
127
+ const supabase = createClient();
128
+ const { data: { user } } = await supabase.auth.getUser();
129
+ const postEditPath = `/cms/posts/${postId}/edit`;
130
+
131
+ if (!user) return encodedRedirect("error", postEditPath, "User not authenticated.");
132
+
133
+ const { data: existingPost, error: fetchError } = await supabase
134
+ .from("posts")
135
+ .select("slug, translation_group_id, language_id")
136
+ .eq("id", postId)
137
+ .single();
138
+
139
+ if (fetchError || !existingPost) {
140
+ return encodedRedirect("error", "/cms/posts", "Original post not found or error fetching it.");
141
+ }
142
+
143
+ const featureImageIdStr_update = formData.get("feature_image_id") as string;
144
+ let featureImageId_update: string | null = null;
145
+ if (featureImageIdStr_update && featureImageIdStr_update.trim() !== "") {
146
+ featureImageId_update = featureImageIdStr_update;
147
+ }
148
+
149
+ const rawFormData = {
150
+ title: formData.get("title") as string,
151
+ slug: formData.get("slug") as string,
152
+ language_id: existingPost.language_id, // Use existing post's language_id
153
+ status: formData.get("status") as PageStatus,
154
+ excerpt: formData.get("excerpt") as string || null,
155
+ published_at: formData.get("published_at") as string || null,
156
+ meta_title: formData.get("meta_title") as string || null,
157
+ meta_description: formData.get("meta_description") as string || null,
158
+ feature_image_id: featureImageId_update,
159
+ };
160
+
161
+ if (!rawFormData.title || !rawFormData.slug || isNaN(rawFormData.language_id) || !rawFormData.status) {
162
+ return encodedRedirect("error", postEditPath, "Missing required fields: title, slug, language, or status.");
163
+ }
164
+ if (rawFormData.language_id !== existingPost.language_id) {
165
+ return encodedRedirect("error", postEditPath, "Changing the language of an existing post version is not allowed. Create a new translation instead.");
166
+ }
167
+
168
+ let publishedAtISO: string | null = null;
169
+ if (rawFormData.published_at) {
170
+ const parsedDate = new Date(rawFormData.published_at);
171
+ if (!isNaN(parsedDate.getTime())) publishedAtISO = parsedDate.toISOString();
172
+ else publishedAtISO = rawFormData.published_at;
173
+ }
174
+
175
+ const postUpdateData: Partial<Omit<UpsertPostPayload, 'translation_group_id' | 'author_id'>> = {
176
+ title: rawFormData.title,
177
+ slug: rawFormData.slug,
178
+ language_id: rawFormData.language_id,
179
+ excerpt: rawFormData.excerpt,
180
+ status: rawFormData.status,
181
+ published_at: publishedAtISO,
182
+ meta_title: rawFormData.meta_title,
183
+ meta_description: rawFormData.meta_description,
184
+ feature_image_id: rawFormData.feature_image_id,
185
+ };
186
+
187
+ // capture previous full content
188
+ const previousContent = await getFullPostContent(postId);
189
+
190
+ const { error: updateError } = await supabase
191
+ .from("posts")
192
+ .update(postUpdateData)
193
+ .eq("id", postId);
194
+
195
+ if (updateError) {
196
+ console.error("Error updating post:", updateError);
197
+ if (updateError.code === '23505' && updateError.message.includes('posts_language_id_slug_key')) {
198
+ return encodedRedirect("error", postEditPath, `The slug "${postUpdateData.slug}" already exists for the selected language. Please use a unique slug.`);
199
+ }
200
+ return encodedRedirect("error", postEditPath, `Failed to update post: ${updateError.message}`);
201
+ }
202
+
203
+ // create revision after update
204
+ if (previousContent && user) {
205
+ const newContent = await getFullPostContent(postId);
206
+ if (newContent) {
207
+ await createPostRevision(postId, user.id, previousContent, newContent);
208
+ }
209
+ }
210
+
211
+ revalidatePath("/cms/posts");
212
+ if (existingPost.slug) revalidatePath(`/blog/${existingPost.slug}`);
213
+ if (rawFormData.slug && rawFormData.slug !== existingPost.slug) {
214
+ revalidatePath(`/blog/${rawFormData.slug}`);
215
+ }
216
+ revalidatePath(postEditPath);
217
+ redirect(`${postEditPath}?success=Post updated successfully`);
218
+ }
219
+
220
+
221
+ export async function deletePost(postId: number) {
222
+ const supabase = createClient();
223
+
224
+ // 1. Fetch the Translation Group
225
+ const { data: post, error: fetchError } = await supabase
226
+ .from("posts")
227
+ .select("translation_group_id")
228
+ .eq("id", postId)
229
+ .single();
230
+
231
+ if (fetchError || !post) {
232
+ return encodedRedirect("error", "/cms/posts", "Post not found or error fetching details.");
233
+ }
234
+
235
+ const { translation_group_id } = post;
236
+
237
+ // 2. Find All Related Posts
238
+ const { data: relatedPosts, error: relatedPostsError } = await supabase
239
+ .from("posts")
240
+ .select("slug")
241
+ .eq("translation_group_id", translation_group_id);
242
+
243
+ if (relatedPostsError) {
244
+ return encodedRedirect("error", "/cms/posts", "Could not fetch related posts for deletion.");
245
+ }
246
+
247
+ // 3. Delete All Associated Navigation Links
248
+ if (relatedPosts && relatedPosts.length > 0) {
249
+ const urlsToDelete = relatedPosts.map(p => `/blog/${p.slug}`);
250
+ const { error: navError } = await supabase
251
+ .from("navigation_items")
252
+ .delete()
253
+ .in("url", urlsToDelete);
254
+
255
+ if (navError) {
256
+ console.error("Error deleting navigation links:", navError);
257
+ // Not returning an error to the user, but logging it.
258
+ }
259
+ }
260
+
261
+ // 4. Delete All Related Posts
262
+ const { error: deletePostsError } = await supabase
263
+ .from("posts")
264
+ .delete()
265
+ .eq("translation_group_id", translation_group_id);
266
+
267
+ if (deletePostsError) {
268
+ return encodedRedirect("error", "/cms/posts", `Failed to delete posts: ${deletePostsError.message}`);
269
+ }
270
+
271
+ // Revalidate paths
272
+ revalidatePath("/cms/posts");
273
+ revalidatePath("/cms/navigation");
274
+ if (relatedPosts) {
275
+ relatedPosts.forEach(p => {
276
+ if (p.slug) {
277
+ revalidatePath(`/blog/${p.slug}`);
278
+ }
279
+ });
280
+ }
281
+
282
+ // 5. Update Redirect Message
283
+ redirect(`/cms/posts?success=${encodeURIComponent("Post and all its translations were deleted successfully.")}`);
284
+ }
285
+
286
+ // Helper function (already defined in page actions, ensure consistent or shared)
287
+ function generatePlaceholderSlug(title: string, langCode: string): string {
288
+ const baseSlug = title.toLowerCase()
289
+ .replace(/\s+/g, '-')
290
+ .replace(/[^\w-]+/g, '')
291
+ .replace(/--+/g, '-')
292
+ .replace(/^-+|-+$/g, '')
293
+ .substring(0, 70);
294
+ return `${baseSlug}-${langCode}-${uuidv4().substring(0, 6)}`;
295
+ }
296
+
297
+ type UpsertPostPayload = {
298
+ language_id: number;
299
+ author_id: string | null;
300
+ title: string;
301
+ slug: string;
302
+ excerpt?: string | null;
303
+ status: PageStatus;
304
+ published_at?: string | null;
305
+ meta_title?: string | null;
306
+ meta_description?: string | null;
307
+ translation_group_id: string;
308
+ feature_image_id?: string | null;
309
+ };
@@ -0,0 +1,55 @@
1
+ // app/cms/posts/components/DeletePostButtonClient.tsx
2
+ "use client";
3
+
4
+ import React, { useState, useRef } from 'react';
5
+ import { DropdownMenuItem } from "@nextblock-cms/ui";
6
+ import { Trash2 } from "lucide-react";
7
+ import { deletePost } from "../actions";
8
+ import { ConfirmationModal } from '@/app/cms/components/ConfirmationModal';
9
+
10
+ interface DeletePostButtonClientProps {
11
+ postId: number;
12
+ }
13
+
14
+ export default function DeletePostButtonClient({ postId }: DeletePostButtonClientProps) {
15
+ const [isModalOpen, setIsModalOpen] = useState(false);
16
+ const formRef = useRef<HTMLFormElement>(null);
17
+
18
+ const deletePostActionWithId = deletePost.bind(null, postId);
19
+
20
+ const handleSelect = (event: Event) => {
21
+ event.preventDefault();
22
+ setIsModalOpen(true);
23
+ };
24
+
25
+ const handleConfirmDelete = () => {
26
+ if (formRef.current) {
27
+ formRef.current.requestSubmit();
28
+ }
29
+ setIsModalOpen(false);
30
+ };
31
+
32
+ return (
33
+ <>
34
+ <form action={deletePostActionWithId} ref={formRef} className="w-full">
35
+ {/* The button is now of type button to prevent form submission on click */}
36
+ <button type="button" className="w-full text-left" onClick={(e) => e.preventDefault()}>
37
+ <DropdownMenuItem
38
+ className="text-red-600 hover:!text-red-600 hover:!bg-red-50 dark:hover:!bg-red-700/20 cursor-pointer"
39
+ onSelect={handleSelect}
40
+ >
41
+ <Trash2 className="mr-2 h-4 w-4" />
42
+ Delete
43
+ </DropdownMenuItem>
44
+ </button>
45
+ </form>
46
+ <ConfirmationModal
47
+ isOpen={isModalOpen}
48
+ onClose={() => setIsModalOpen(false)}
49
+ onConfirm={handleConfirmDelete}
50
+ title="Are you sure?"
51
+ description="This will permanently delete the post. This action cannot be undone."
52
+ />
53
+ </>
54
+ );
55
+ }