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,434 @@
1
+ // app/cms/blocks/actions.ts
2
+ "use server";
3
+
4
+ import { createClient } from "@nextblock-cms/db/server";
5
+ import { revalidatePath } from "next/cache";
6
+ import type { Database, Json } from "@nextblock-cms/db";
7
+ import { getInitialContent, isValidBlockType } from "../../../lib/blocks/blockRegistry";
8
+ import { getFullPageContent, getFullPostContent } from "../revisions/utils";
9
+ import { createPageRevision, createPostRevision } from "../revisions/service";
10
+
11
+ type Block = Database['public']['Tables']['blocks']['Row'];
12
+ type BlockType = Database['public']['Tables']['blocks']['Row']['block_type'];
13
+
14
+ // Helper to verify user can edit the parent (page/post)
15
+ async function canEditParent(
16
+ supabase: ReturnType<typeof createClient>,
17
+ userId: string,
18
+ pageId?: number | null,
19
+ postId?: number | null
20
+ ): Promise<boolean> {
21
+ void pageId;
22
+ void postId;
23
+ const { data: profile } = await supabase
24
+ .from("profiles")
25
+ .select("role")
26
+ .eq("id", userId)
27
+ .single();
28
+
29
+ if (!profile || !["ADMIN", "WRITER"].includes(profile.role)) {
30
+ return false;
31
+ }
32
+ // Further checks could be added here to see if a WRITER owns the page/post
33
+ return true;
34
+ }
35
+
36
+ interface CreateBlockPayload {
37
+ page_id?: number | null;
38
+ post_id?: number | null;
39
+ language_id: number;
40
+ block_type: BlockType;
41
+ content: object; // Content structure defined by block registry
42
+ order: number;
43
+ }
44
+
45
+ export async function createBlockForPage(pageId: number, languageId: number, blockType: BlockType, order: number) {
46
+ const supabase = createClient();
47
+ const { data: { user } } = await supabase.auth.getUser();
48
+
49
+ if (!user) return { error: "User not authenticated." };
50
+ if (!(await canEditParent(supabase, user.id, pageId, null))) {
51
+ return { error: "Unauthorized to add blocks to this page." };
52
+ }
53
+
54
+ // Validate block type using registry
55
+ if (!isValidBlockType(blockType)) {
56
+ return { error: "Unknown block type." };
57
+ }
58
+
59
+ // Get initial content from registry
60
+ const initialContent = getInitialContent(blockType);
61
+ if (!initialContent) {
62
+ return { error: "Failed to get initial content for block type." };
63
+ }
64
+
65
+ const payload: CreateBlockPayload = {
66
+ page_id: pageId,
67
+ language_id: languageId,
68
+ block_type: blockType,
69
+ content: initialContent,
70
+ order: order,
71
+ };
72
+
73
+ // capture previous state for revision (before insert)
74
+ const previousContent = await getFullPageContent(pageId);
75
+
76
+ const { data, error } = await supabase.from("blocks").insert(payload).select().single();
77
+
78
+ if (error) {
79
+ console.error("Error creating block:", error);
80
+ return { error: `Failed to create block: ${error.message}` };
81
+ }
82
+
83
+ // create revision (after successful insert)
84
+ if (previousContent && user) {
85
+ const newContent = await getFullPageContent(pageId);
86
+ if (newContent) {
87
+ await createPageRevision(pageId, user.id, previousContent, newContent);
88
+ }
89
+ }
90
+
91
+ revalidatePath(`/cms/pages/${pageId}/edit`);
92
+ return { success: true, newBlock: data as Block };
93
+ }
94
+
95
+ export async function createBlockForPost(postId: number, languageId: number, blockType: BlockType, order: number) {
96
+ const supabase = createClient();
97
+ const { data: { user } } = await supabase.auth.getUser();
98
+
99
+ if (!user) return { error: "User not authenticated." };
100
+ if (!(await canEditParent(supabase, user.id, null, postId))) {
101
+ return { error: "Unauthorized to add blocks to this post." };
102
+ }
103
+
104
+ // Validate block type using registry
105
+ if (!isValidBlockType(blockType)) {
106
+ return { error: "Unknown block type." };
107
+ }
108
+
109
+ // Get initial content from registry
110
+ const initialContent = getInitialContent(blockType);
111
+ if (!initialContent) {
112
+ return { error: "Failed to get initial content for block type." };
113
+ }
114
+
115
+ const payload: CreateBlockPayload = {
116
+ post_id: postId,
117
+ language_id: languageId,
118
+ block_type: blockType,
119
+ content: initialContent,
120
+ order: order,
121
+ };
122
+
123
+ // capture previous content
124
+ const previousContent = await getFullPostContent(postId);
125
+
126
+ const { data, error } = await supabase.from("blocks").insert(payload).select().single();
127
+
128
+ if (error) {
129
+ console.error("Error creating block:", error);
130
+ return { error: `Failed to create block: ${error.message}` };
131
+ }
132
+
133
+ if (previousContent && user) {
134
+ const newContent = await getFullPostContent(postId);
135
+ if (newContent) {
136
+ await createPostRevision(postId, user.id, previousContent, newContent);
137
+ }
138
+ }
139
+
140
+ revalidatePath(`/cms/posts/${postId}/edit`);
141
+ return { success: true, newBlock: data as Block };
142
+ }
143
+
144
+ export async function updateBlock(blockId: number, newContent: unknown, pageId?: number | null, postId?: number | null) {
145
+ const supabase = createClient();
146
+ const { data: { user } } = await supabase.auth.getUser();
147
+
148
+ if (!user) return { error: "User not authenticated." };
149
+ if (!(await canEditParent(supabase, user.id, pageId, postId))) {
150
+ return { error: "Unauthorized to update this block." };
151
+ }
152
+
153
+ // fetch current block to identify parent and previous state
154
+ const { data: existingBlock, error: fetchError } = await supabase
155
+ .from('blocks')
156
+ .select('id, page_id, post_id, content')
157
+ .eq('id', blockId)
158
+ .single();
159
+ if (fetchError || !existingBlock) {
160
+ return { error: "Block not found." };
161
+ }
162
+
163
+ let prevContentAggregate: Awaited<ReturnType<typeof getFullPageContent>> | Awaited<ReturnType<typeof getFullPostContent>> | null = null;
164
+ if (existingBlock.page_id) {
165
+ prevContentAggregate = await getFullPageContent(existingBlock.page_id);
166
+ } else if (existingBlock.post_id) {
167
+ prevContentAggregate = await getFullPostContent(existingBlock.post_id);
168
+ }
169
+
170
+ const { data, error } = await supabase
171
+ .from("blocks")
172
+ .update({ content: newContent, updated_at: new Date().toISOString() })
173
+ .eq("id", blockId)
174
+ .select()
175
+ .single();
176
+
177
+ if (error) {
178
+ console.error("Error updating block:", error);
179
+ return { error: `Failed to update block: ${error.message}` };
180
+ }
181
+
182
+ // create revision after successful update
183
+ if (user && prevContentAggregate) {
184
+ if (existingBlock.page_id) {
185
+ const nextContentAggregate = await getFullPageContent(existingBlock.page_id, { overrideBlockId: blockId, overrideBlockContent: newContent });
186
+ if (nextContentAggregate) {
187
+ await createPageRevision(existingBlock.page_id, user.id, prevContentAggregate, nextContentAggregate);
188
+ }
189
+ } else if (existingBlock.post_id) {
190
+ const nextContentAggregate = await getFullPostContent(existingBlock.post_id, { overrideBlockId: blockId, overrideBlockContent: newContent });
191
+ if (nextContentAggregate) {
192
+ await createPostRevision(existingBlock.post_id, user.id, prevContentAggregate as any, nextContentAggregate as any);
193
+ }
194
+ }
195
+ }
196
+
197
+ return { success: true, updatedBlock: data as Block };
198
+ }
199
+
200
+ export async function updateMultipleBlockOrders(
201
+ updates: Array<{ id: number; order: number }>,
202
+ pageId?: number | null,
203
+ postId?: number | null
204
+ ) {
205
+ const supabase = createClient();
206
+ const { data: { user } } = await supabase.auth.getUser();
207
+
208
+ if (!user) return { error: "User not authenticated." };
209
+ if (!(await canEditParent(supabase, user.id, pageId, postId))) {
210
+ return { error: "Unauthorized to reorder blocks." };
211
+ }
212
+
213
+ // Supabase upsert can be used for batch updates if primary key `id` is included.
214
+ // Or loop through updates (less efficient for many updates but simpler to write without complex SQL).
215
+ const updatePromises = updates.map(update =>
216
+ supabase.from('blocks').update({ order: update.order, updated_at: new Date().toISOString() }).eq('id', update.id)
217
+ );
218
+
219
+ const results = await Promise.all(updatePromises);
220
+ const errors = results.filter(result => result.error);
221
+
222
+ if (errors.length > 0) {
223
+ console.error("Error updating block orders:", errors.map(e => e.error?.message).join(", "));
224
+ return { error: `Failed to update some block orders: ${errors.map(e => e.error?.message).join(", ")}` };
225
+ }
226
+
227
+ if (pageId) revalidatePath(`/cms/pages/${pageId}/edit`);
228
+ if (postId) revalidatePath(`/cms/posts/${postId}/edit`);
229
+
230
+ return { success: true };
231
+ }
232
+
233
+
234
+ export async function deleteBlock(blockId: number, pageId?: number | null, postId?: number | null) {
235
+ const supabase = createClient();
236
+ const { data: { user } } = await supabase.auth.getUser();
237
+
238
+ if (!user) return { error: "User not authenticated." };
239
+ if (!(await canEditParent(supabase, user.id, pageId, postId))) {
240
+ return { error: "Unauthorized to delete this block." };
241
+ }
242
+
243
+ // fetch parent and capture previous aggregate
244
+ const { data: existingBlock, error: fetchError } = await supabase
245
+ .from('blocks')
246
+ .select('id, page_id, post_id')
247
+ .eq('id', blockId)
248
+ .single();
249
+ if (fetchError || !existingBlock) {
250
+ return { error: "Block not found." };
251
+ }
252
+
253
+ let previousAggregate: Awaited<ReturnType<typeof getFullPageContent>> | Awaited<ReturnType<typeof getFullPostContent>> | null = null;
254
+ if (existingBlock.page_id) {
255
+ previousAggregate = await getFullPageContent(existingBlock.page_id);
256
+ } else if (existingBlock.post_id) {
257
+ previousAggregate = await getFullPostContent(existingBlock.post_id);
258
+ }
259
+
260
+ const { error } = await supabase.from("blocks").delete().eq("id", blockId);
261
+
262
+ if (error) {
263
+ console.error("Error deleting block:", error);
264
+ return { error: `Failed to delete block: ${error.message}` };
265
+ }
266
+
267
+ // create revision after delete
268
+ if (user && previousAggregate) {
269
+ if (existingBlock.page_id) {
270
+ const nextAggregate = await getFullPageContent(existingBlock.page_id, { excludeDeletedBlockId: blockId });
271
+ if (nextAggregate) {
272
+ await createPageRevision(existingBlock.page_id, user.id, previousAggregate, nextAggregate);
273
+ }
274
+ } else if (existingBlock.post_id) {
275
+ const nextAggregate = await getFullPostContent(existingBlock.post_id, { excludeDeletedBlockId: blockId });
276
+ if (nextAggregate) {
277
+ await createPostRevision(existingBlock.post_id, user.id, previousAggregate as any, nextAggregate as any);
278
+ }
279
+ }
280
+ }
281
+
282
+ if (pageId) revalidatePath(`/cms/pages/${pageId}/edit`);
283
+ if (postId) revalidatePath(`/cms/posts/${postId}/edit`);
284
+
285
+ return { success: true };
286
+ }
287
+
288
+ export async function copyBlocksFromLanguage(
289
+ parentId: number, // ID of the page or post being edited
290
+ parentType: "page" | "post",
291
+ sourceLanguageId: number,
292
+ targetLanguageId: number, // Language of the current page/post being edited
293
+ targetTranslationGroupId: string
294
+ ) {
295
+ "use server";
296
+ const supabase = createClient();
297
+ const { data: { user } } = await supabase.auth.getUser();
298
+
299
+ if (!user) {
300
+ return { error: "User not authenticated." };
301
+ }
302
+
303
+ if (!(await canEditParent(supabase, user.id, parentType === "page" ? parentId : null, parentType === "post" ? parentId : null))) {
304
+ return { error: "Unauthorized to modify blocks for this target." };
305
+ }
306
+
307
+ let sourceParentId: number | null = null;
308
+
309
+ // 1. Fetch Source Page/Post ID
310
+ try {
311
+ if (parentType === "page") {
312
+ const { data: sourcePage, error: sourcePageError } = await supabase
313
+ .from("pages")
314
+ .select("id")
315
+ .eq("translation_group_id", targetTranslationGroupId)
316
+ .eq("language_id", sourceLanguageId)
317
+ .single();
318
+
319
+ if (sourcePageError || !sourcePage) {
320
+ console.error("Error fetching source page:", sourcePageError);
321
+ return { error: "Source page not found or error fetching it." };
322
+ }
323
+ sourceParentId = sourcePage.id;
324
+ } else if (parentType === "post") {
325
+ const { data: sourcePost, error: sourcePostError } = await supabase
326
+ .from("posts")
327
+ .select("id")
328
+ .eq("translation_group_id", targetTranslationGroupId)
329
+ .eq("language_id", sourceLanguageId)
330
+ .single();
331
+
332
+ if (sourcePostError || !sourcePost) {
333
+ console.error("Error fetching source post:", sourcePostError);
334
+ return { error: "Source post not found or error fetching it." };
335
+ }
336
+ sourceParentId = sourcePost.id;
337
+ } else {
338
+ return { error: "Invalid parent type specified." };
339
+ }
340
+
341
+ if (!sourceParentId) {
342
+ return { error: "Could not determine source parent ID." };
343
+ }
344
+
345
+ // 2. Fetch Blocks from Source
346
+ const { data: sourceBlocks, error: sourceBlocksError } = await supabase
347
+ .from("blocks")
348
+ .select("page_id, post_id, language_id, block_type, content, order") // Select only existing columns
349
+ .eq(parentType === "page" ? "page_id" : "post_id", sourceParentId)
350
+ .order("order", { ascending: true });
351
+
352
+ if (sourceBlocksError) {
353
+ console.error("Error fetching source blocks:", sourceBlocksError);
354
+ return { error: `Failed to fetch blocks from source: ${sourceBlocksError.message}` };
355
+ }
356
+
357
+ // 3. Delete Existing Blocks from Target
358
+ const { error: deleteError } = await supabase
359
+ .from("blocks")
360
+ .delete()
361
+ .eq(parentType === "page" ? "page_id" : "post_id", parentId)
362
+ .eq("language_id", targetLanguageId); // Ensure we only delete for the target language
363
+
364
+ if (deleteError) {
365
+ console.error("Error deleting existing blocks:", deleteError);
366
+ return { error: `Failed to delete existing blocks: ${deleteError.message}` };
367
+ }
368
+
369
+ // 4. Re-create Blocks for Target
370
+ if (sourceBlocks && sourceBlocks.length > 0) {
371
+ const newBlocksToInsert = sourceBlocks.map((block: {
372
+ page_id: number | null;
373
+ post_id: number | null;
374
+ language_id: number;
375
+ block_type: BlockType;
376
+ content: Json;
377
+ order: number;
378
+ }) => ({
379
+ // id, created_at, updated_at will be set by DB
380
+ page_id: parentType === "page" ? parentId : null,
381
+ post_id: parentType === "post" ? parentId : null,
382
+ language_id: targetLanguageId,
383
+ block_type: block.block_type,
384
+ content: block.content, // Directly copy content, which includes any type-specific configs like cols_config
385
+ order: block.order,
386
+ }));
387
+
388
+ const { error: insertError } = await supabase.from("blocks").insert(newBlocksToInsert);
389
+
390
+ if (insertError) {
391
+ console.error("Error re-creating blocks:", insertError);
392
+ return { error: `Failed to re-create blocks: ${insertError.message}` };
393
+ }
394
+ }
395
+
396
+ // 5. Revalidation
397
+ let targetSlug: string | null = null;
398
+ if (parentType === "page") {
399
+ const { data: pageData, error: pageError } = await supabase
400
+ .from("pages")
401
+ .select("slug")
402
+ .eq("id", parentId)
403
+ .single();
404
+ if (pageError || !pageData) {
405
+ console.warn("Could not fetch target page slug for revalidation:", pageError);
406
+ } else {
407
+ targetSlug = pageData.slug;
408
+ if (targetSlug) revalidatePath(`/${targetSlug}`);
409
+ }
410
+ revalidatePath(`/cms/pages/${parentId}/edit`); // Revalidate edit page
411
+ } else if (parentType === "post") {
412
+ const { data: postData, error: postError } = await supabase
413
+ .from("posts")
414
+ .select("slug")
415
+ .eq("id", parentId)
416
+ .single();
417
+ if (postError || !postData) {
418
+ console.warn("Could not fetch target post slug for revalidation:", postError);
419
+ } else {
420
+ targetSlug = postData.slug;
421
+ if (targetSlug) revalidatePath(`/blog/${targetSlug}`);
422
+ }
423
+ revalidatePath(`/cms/posts/${parentId}/edit`); // Revalidate edit page
424
+ }
425
+
426
+
427
+ return { success: true, message: "Blocks copied successfully." };
428
+
429
+ } catch (e: unknown) {
430
+ console.error("Unexpected error in copyBlocksFromLanguage:", e);
431
+ const errorMessage = e instanceof Error ? e.message : "An unknown error occurred";
432
+ return { error: `An unexpected error occurred: ${errorMessage}` };
433
+ }
434
+ }