create-nextblock 0.2.77 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (413) hide show
  1. package/bin/create-nextblock.js +740 -459
  2. package/package.json +1 -2
  3. package/scripts/sync-template.js +18 -1
  4. package/templates/nextblock-template/.browserslistrc +11 -0
  5. package/templates/nextblock-template/.swcrc +30 -30
  6. package/templates/nextblock-template/README.md +23 -114
  7. package/templates/nextblock-template/app/(auth-pages)/post-sign-in/page.tsx +27 -28
  8. package/templates/nextblock-template/app/(auth-pages)/sign-in/page.tsx +50 -25
  9. package/templates/nextblock-template/app/(auth-pages)/sign-up/page.tsx +111 -56
  10. package/templates/nextblock-template/app/(auth-pages)/two-factor/actions.ts +91 -0
  11. package/templates/nextblock-template/app/(auth-pages)/two-factor/components/TwoFactorForm.tsx +118 -0
  12. package/templates/nextblock-template/app/(auth-pages)/two-factor/page.tsx +51 -0
  13. package/templates/nextblock-template/app/.well-known/ucp/route.ts +16 -0
  14. package/templates/nextblock-template/app/[slug]/PageClientContent.tsx +48 -28
  15. package/templates/nextblock-template/app/[slug]/page.tsx +63 -6
  16. package/templates/nextblock-template/app/[slug]/page.utils.ts +374 -157
  17. package/templates/nextblock-template/app/[slug]/pageClientActions.ts +7 -0
  18. package/templates/nextblock-template/app/actions/consent.ts +57 -0
  19. package/templates/nextblock-template/app/actions/formActions.ts +130 -11
  20. package/templates/nextblock-template/app/actions/languageActions.ts +31 -30
  21. package/templates/nextblock-template/app/actions/package-actions.ts +183 -0
  22. package/templates/nextblock-template/app/actions/postActions.ts +146 -48
  23. package/templates/nextblock-template/app/actions/twoFactorEmail.ts +21 -0
  24. package/templates/nextblock-template/app/actions/visualEditingActions.test.ts +179 -0
  25. package/templates/nextblock-template/app/actions/visualEditingActions.ts +345 -0
  26. package/templates/nextblock-template/app/actions.ts +67 -12
  27. package/templates/nextblock-template/app/api/ai/cortex/build-widget/route.ts +153 -0
  28. package/templates/nextblock-template/app/api/ai/generate-blocks/route.ts +96 -0
  29. package/templates/nextblock-template/app/api/ai/global-agent/route.ts +965 -0
  30. package/templates/nextblock-template/app/api/checkout/freemius/sync/route.ts +29 -0
  31. package/templates/nextblock-template/app/api/checkout/route.ts +146 -0
  32. package/templates/nextblock-template/app/api/cms/full-backup/export/route.ts +33 -0
  33. package/templates/nextblock-template/app/api/cms/full-backup/restore/route.ts +63 -0
  34. package/templates/nextblock-template/app/api/cron/reset-sandbox/route.ts +3413 -17
  35. package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +7830 -0
  36. package/templates/nextblock-template/app/api/cron/sync-currencies/route.ts +35 -0
  37. package/templates/nextblock-template/app/api/custom-blocks/db-relations/route.ts +92 -0
  38. package/templates/nextblock-template/app/api/custom-blocks/editor-definitions/route.ts +43 -0
  39. package/templates/nextblock-template/app/api/draft/disable/route.ts +25 -0
  40. package/templates/nextblock-template/app/api/draft/route.ts +93 -0
  41. package/templates/nextblock-template/app/api/draft/start/route.ts +77 -0
  42. package/templates/nextblock-template/app/api/media/library/route.ts +65 -0
  43. package/templates/nextblock-template/app/api/media/r2-presigned/route.ts +53 -0
  44. package/templates/nextblock-template/app/api/media/record/route.ts +160 -0
  45. package/templates/nextblock-template/app/api/search/route.ts +43 -0
  46. package/templates/nextblock-template/app/api/visual-editing/block-draft/route.ts +47 -0
  47. package/templates/nextblock-template/app/api/visual-editing/product-draft/route.ts +47 -0
  48. package/templates/nextblock-template/app/api/webhooks/freemius/route.ts +34 -0
  49. package/templates/nextblock-template/app/api/webhooks/stripe/route.ts +27 -0
  50. package/templates/nextblock-template/app/article/[slug]/PostClientContent.tsx +392 -128
  51. package/templates/nextblock-template/app/article/[slug]/page.tsx +179 -127
  52. package/templates/nextblock-template/app/article/[slug]/page.utils.ts +262 -77
  53. package/templates/nextblock-template/app/auth/callback/route.ts +31 -58
  54. package/templates/nextblock-template/app/cart/page.tsx +7 -0
  55. package/templates/nextblock-template/app/checkout/UcpCartHydrator.tsx +20 -0
  56. package/templates/nextblock-template/app/checkout/page.tsx +52 -0
  57. package/templates/nextblock-template/app/checkout/success/actions.ts +136 -0
  58. package/templates/nextblock-template/app/checkout/success/page.tsx +186 -0
  59. package/templates/nextblock-template/app/cms/CmsClientLayout.tsx +163 -33
  60. package/templates/nextblock-template/app/cms/blocks/actions.ts +424 -235
  61. package/templates/nextblock-template/app/cms/blocks/components/BackgroundSelector.tsx +212 -151
  62. package/templates/nextblock-template/app/cms/blocks/components/BlockEditorArea.tsx +41 -20
  63. package/templates/nextblock-template/app/cms/blocks/components/BlockEditorModal.tsx +152 -19
  64. package/templates/nextblock-template/app/cms/blocks/components/BlockTypeCard.tsx +25 -17
  65. package/templates/nextblock-template/app/cms/blocks/components/BlockTypeSelector.tsx +200 -18
  66. package/templates/nextblock-template/app/cms/blocks/components/ColumnEditor.tsx +33 -16
  67. package/templates/nextblock-template/app/cms/blocks/components/CustomBlockEditorPreview.tsx +160 -0
  68. package/templates/nextblock-template/app/cms/blocks/components/EditableBlock.tsx +37 -18
  69. package/templates/nextblock-template/app/cms/blocks/components/MediaLibraryModal.tsx +149 -67
  70. package/templates/nextblock-template/app/cms/blocks/components/SectionConfigPanel.tsx +108 -31
  71. package/templates/nextblock-template/app/cms/blocks/editors/DynamicCustomBlockEditor.tsx +167 -0
  72. package/templates/nextblock-template/app/cms/blocks/editors/FeaturedProductBlockEditor.tsx +31 -0
  73. package/templates/nextblock-template/app/cms/blocks/editors/FormBlockEditor.tsx +2 -2
  74. package/templates/nextblock-template/app/cms/blocks/editors/HeadingBlockEditor.tsx +1 -1
  75. package/templates/nextblock-template/app/cms/blocks/editors/ImageBlockEditor.tsx +29 -29
  76. package/templates/nextblock-template/app/cms/blocks/editors/PostsGridBlockEditor.tsx +14 -18
  77. package/templates/nextblock-template/app/cms/blocks/editors/ProductGridBlockEditor.tsx +41 -0
  78. package/templates/nextblock-template/app/cms/blocks/editors/SectionBlockEditor.tsx +318 -118
  79. package/templates/nextblock-template/app/cms/blocks/editors/TextBlockEditor.tsx +98 -21
  80. package/templates/nextblock-template/app/cms/blocks/editors/VideoEmbedBlockEditor.tsx +1 -1
  81. package/templates/nextblock-template/app/cms/components/ContentLanguageSwitcher.tsx +27 -9
  82. package/templates/nextblock-template/app/cms/components/CopyContentFromLanguage.tsx +1 -1
  83. package/templates/nextblock-template/app/cms/components/CortexAiActiveContext.tsx +23 -0
  84. package/templates/nextblock-template/app/cms/components/CortexAiPageContext.tsx +58 -0
  85. package/templates/nextblock-template/app/cms/components/CortexGlobalAgentChat.tsx +1507 -0
  86. package/templates/nextblock-template/app/cms/components/DraftStatusActions.tsx +145 -0
  87. package/templates/nextblock-template/app/cms/components/FeatureImageField.tsx +244 -0
  88. package/templates/nextblock-template/app/cms/components/FeedbackModal.tsx +38 -24
  89. package/templates/nextblock-template/app/cms/coupons/[id]/edit/page.tsx +16 -0
  90. package/templates/nextblock-template/app/cms/coupons/page.tsx +16 -0
  91. package/templates/nextblock-template/app/cms/custom-blocks/[id]/edit/page.tsx +66 -0
  92. package/templates/nextblock-template/app/cms/custom-blocks/actions.ts +519 -0
  93. package/templates/nextblock-template/app/cms/custom-blocks/components/BlockComposer.tsx +1522 -0
  94. package/templates/nextblock-template/app/cms/custom-blocks/components/BlocksLibraryTransferControls.tsx +256 -0
  95. package/templates/nextblock-template/app/cms/custom-blocks/components/DBRelationSelect.tsx +384 -0
  96. package/templates/nextblock-template/app/cms/custom-blocks/components/ImageR2Picker.tsx +221 -0
  97. package/templates/nextblock-template/app/cms/custom-blocks/new/page.tsx +12 -0
  98. package/templates/nextblock-template/app/cms/custom-blocks/page.tsx +438 -0
  99. package/templates/nextblock-template/app/cms/dashboard/actions.ts +228 -98
  100. package/templates/nextblock-template/app/cms/dashboard/components/DashboardComponents.tsx +200 -0
  101. package/templates/nextblock-template/app/cms/dashboard/page.tsx +191 -151
  102. package/templates/nextblock-template/app/cms/import-export/ContentTransferControls.tsx +391 -0
  103. package/templates/nextblock-template/app/cms/import-export/actions.ts +226 -0
  104. package/templates/nextblock-template/app/cms/layout.tsx +29 -10
  105. package/templates/nextblock-template/app/cms/media/UploadFolderContext.tsx +22 -22
  106. package/templates/nextblock-template/app/cms/media/actions.ts +45 -124
  107. package/templates/nextblock-template/app/cms/media/components/DeleteMediaButtonClient.tsx +1 -1
  108. package/templates/nextblock-template/app/cms/media/components/MediaEditForm.tsx +26 -26
  109. package/templates/nextblock-template/app/cms/media/components/MediaGridClient.tsx +69 -64
  110. package/templates/nextblock-template/app/cms/media/components/MediaPickerDialog.tsx +227 -158
  111. package/templates/nextblock-template/app/cms/media/components/MediaUploadForm.tsx +101 -89
  112. package/templates/nextblock-template/app/cms/media/page.tsx +1 -1
  113. package/templates/nextblock-template/app/cms/navigation/components/NavigationItemForm.tsx +2 -2
  114. package/templates/nextblock-template/app/cms/orders/[id]/MarkPaidButton.tsx +44 -0
  115. package/templates/nextblock-template/app/cms/orders/[id]/page.tsx +16 -0
  116. package/templates/nextblock-template/app/cms/orders/actions.ts +201 -0
  117. package/templates/nextblock-template/app/cms/orders/page.tsx +20 -0
  118. package/templates/nextblock-template/app/cms/orders/types.ts +20 -0
  119. package/templates/nextblock-template/app/cms/pages/[id]/edit/EditPageClient.tsx +156 -121
  120. package/templates/nextblock-template/app/cms/pages/[id]/edit/page.tsx +79 -26
  121. package/templates/nextblock-template/app/cms/pages/actions.ts +54 -38
  122. package/templates/nextblock-template/app/cms/pages/components/DeletePageButtonClient.tsx +1 -1
  123. package/templates/nextblock-template/app/cms/pages/components/PageForm.tsx +267 -116
  124. package/templates/nextblock-template/app/cms/pages/page.tsx +25 -18
  125. package/templates/nextblock-template/app/cms/payments/page.tsx +16 -0
  126. package/templates/nextblock-template/app/cms/posts/[id]/edit/page.tsx +132 -90
  127. package/templates/nextblock-template/app/cms/posts/actions.ts +71 -72
  128. package/templates/nextblock-template/app/cms/posts/components/DeletePostButtonClient.tsx +1 -1
  129. package/templates/nextblock-template/app/cms/posts/components/PostForm.tsx +256 -245
  130. package/templates/nextblock-template/app/cms/posts/new/page.tsx +1 -1
  131. package/templates/nextblock-template/app/cms/posts/page.tsx +20 -13
  132. package/templates/nextblock-template/app/cms/products/ClientNotionEditor.tsx +16 -0
  133. package/templates/nextblock-template/app/cms/products/ProductFormClientShell.tsx +56 -0
  134. package/templates/nextblock-template/app/cms/products/[id]/edit/page.tsx +292 -0
  135. package/templates/nextblock-template/app/cms/products/attributes/page.tsx +12 -0
  136. package/templates/nextblock-template/app/cms/products/categories/page.tsx +12 -0
  137. package/templates/nextblock-template/app/cms/products/inventory/page.tsx +13 -0
  138. package/templates/nextblock-template/app/cms/products/new/page.tsx +143 -0
  139. package/templates/nextblock-template/app/cms/products/page.tsx +42 -0
  140. package/templates/nextblock-template/app/cms/products/productFormData.ts +133 -0
  141. package/templates/nextblock-template/app/cms/products/settings/page.tsx +5 -0
  142. package/templates/nextblock-template/app/cms/promotions/PromotionsWorkspace.tsx +456 -0
  143. package/templates/nextblock-template/app/cms/promotions/actions.ts +115 -0
  144. package/templates/nextblock-template/app/cms/promotions/page.tsx +31 -0
  145. package/templates/nextblock-template/app/cms/revisions/RevisionHistoryButton.tsx +2 -2
  146. package/templates/nextblock-template/app/cms/revisions/actions.ts +285 -285
  147. package/templates/nextblock-template/app/cms/revisions/service.ts +19 -16
  148. package/templates/nextblock-template/app/cms/revisions/utils.ts +8 -3
  149. package/templates/nextblock-template/app/cms/settings/backup-restore/BackupRestoreWorkspace.tsx +1004 -0
  150. package/templates/nextblock-template/app/cms/settings/backup-restore/page.tsx +29 -0
  151. package/templates/nextblock-template/app/cms/settings/bot-protection/actions.ts +93 -0
  152. package/templates/nextblock-template/app/cms/settings/bot-protection/components/BotProtectionForm.tsx +129 -0
  153. package/templates/nextblock-template/app/cms/settings/bot-protection/page.tsx +24 -0
  154. package/templates/nextblock-template/app/cms/settings/copyright/actions.ts +1 -1
  155. package/templates/nextblock-template/app/cms/settings/copyright/components/CopyrightForm.tsx +2 -2
  156. package/templates/nextblock-template/app/cms/settings/copyright/page.tsx +1 -1
  157. package/templates/nextblock-template/app/cms/settings/cortex-ai/SandboxCortexAiSettingsClient.tsx +496 -0
  158. package/templates/nextblock-template/app/cms/settings/cortex-ai/StoredCortexAiSettingsClient.tsx +410 -0
  159. package/templates/nextblock-template/app/cms/settings/cortex-ai/actions.ts +248 -0
  160. package/templates/nextblock-template/app/cms/settings/cortex-ai/page.tsx +80 -0
  161. package/templates/nextblock-template/app/cms/settings/currencies/actions.ts +331 -0
  162. package/templates/nextblock-template/app/cms/settings/currencies/page.tsx +494 -0
  163. package/templates/nextblock-template/app/cms/settings/extra-translations/ExtraTranslationsWorkspace.tsx +767 -0
  164. package/templates/nextblock-template/app/cms/settings/extra-translations/actions.ts +203 -44
  165. package/templates/nextblock-template/app/cms/settings/extra-translations/page.tsx +93 -242
  166. package/templates/nextblock-template/app/cms/settings/global-css/actions.ts +65 -0
  167. package/templates/nextblock-template/app/cms/settings/global-css/components/GlobalCssForm.tsx +46 -0
  168. package/templates/nextblock-template/app/cms/settings/global-css/page.tsx +24 -0
  169. package/templates/nextblock-template/app/cms/settings/languages/components/DeleteLanguageButton.tsx +1 -1
  170. package/templates/nextblock-template/app/cms/settings/languages/components/LanguageForm.tsx +2 -2
  171. package/templates/nextblock-template/app/cms/settings/languages/page.tsx +1 -1
  172. package/templates/nextblock-template/app/cms/settings/logos/[id]/edit/page.tsx +7 -7
  173. package/templates/nextblock-template/app/cms/settings/logos/actions.ts +82 -6
  174. package/templates/nextblock-template/app/cms/settings/logos/components/BrandingSettingsForm.tsx +339 -0
  175. package/templates/nextblock-template/app/cms/settings/logos/components/DeleteLogoButton.tsx +21 -18
  176. package/templates/nextblock-template/app/cms/settings/logos/components/LogoForm.tsx +20 -16
  177. package/templates/nextblock-template/app/cms/settings/logos/components/SiteSeoSettingsForm.tsx +133 -0
  178. package/templates/nextblock-template/app/cms/settings/logos/new/page.tsx +8 -8
  179. package/templates/nextblock-template/app/cms/settings/logos/page.tsx +120 -82
  180. package/templates/nextblock-template/app/cms/settings/logos/types.ts +8 -8
  181. package/templates/nextblock-template/app/cms/settings/packages/activation-form.tsx +84 -0
  182. package/templates/nextblock-template/app/cms/settings/packages/package-card.tsx +122 -0
  183. package/templates/nextblock-template/app/cms/settings/packages/page.tsx +49 -0
  184. package/templates/nextblock-template/app/cms/settings/privacy/actions.ts +53 -0
  185. package/templates/nextblock-template/app/cms/settings/privacy/components/PrivacyForm.tsx +196 -0
  186. package/templates/nextblock-template/app/cms/settings/privacy/page.tsx +26 -0
  187. package/templates/nextblock-template/app/cms/settings/security/actions.ts +251 -0
  188. package/templates/nextblock-template/app/cms/settings/security/components/SecurityPanel.tsx +453 -0
  189. package/templates/nextblock-template/app/cms/settings/security/page.tsx +13 -0
  190. package/templates/nextblock-template/app/cms/settings/taxes/page.tsx +21 -0
  191. package/templates/nextblock-template/app/cms/shipping/page.tsx +20 -0
  192. package/templates/nextblock-template/app/cms/users/[id]/edit/page.tsx +28 -23
  193. package/templates/nextblock-template/app/cms/users/actions.ts +105 -40
  194. package/templates/nextblock-template/app/cms/users/components/DeleteUserButton.tsx +1 -1
  195. package/templates/nextblock-template/app/cms/users/components/UserForm.tsx +65 -152
  196. package/templates/nextblock-template/app/cms/users/page.tsx +15 -10
  197. package/templates/nextblock-template/app/globals.css +9 -0
  198. package/templates/nextblock-template/app/layout.tsx +372 -116
  199. package/templates/nextblock-template/app/lib/seo.test.ts +52 -0
  200. package/templates/nextblock-template/app/lib/seo.ts +279 -0
  201. package/templates/nextblock-template/app/lib/site-settings.ts +87 -0
  202. package/templates/nextblock-template/app/lib/sitemap-utils.ts +224 -39
  203. package/templates/nextblock-template/app/lib/ucp/protocol.ts +190 -0
  204. package/templates/nextblock-template/app/lib/ucp/server.test.ts +56 -0
  205. package/templates/nextblock-template/app/lib/ucp/server.ts +1914 -0
  206. package/templates/nextblock-template/app/page.tsx +165 -73
  207. package/templates/nextblock-template/app/product/[slug]/page.tsx +433 -0
  208. package/templates/nextblock-template/app/profile/ProfileAccountSidebar.tsx +73 -0
  209. package/templates/nextblock-template/app/profile/ProfilePageHeader.tsx +16 -0
  210. package/templates/nextblock-template/app/profile/ProfilePageMissingState.tsx +9 -0
  211. package/templates/nextblock-template/app/profile/account-data.ts +37 -0
  212. package/templates/nextblock-template/app/profile/account-links.ts +22 -0
  213. package/templates/nextblock-template/app/profile/account-types.ts +11 -0
  214. package/templates/nextblock-template/app/profile/orders/CustomerOrdersPageClient.tsx +124 -0
  215. package/templates/nextblock-template/app/profile/orders/[id]/CustomerOrderDetailPageClient.tsx +79 -0
  216. package/templates/nextblock-template/app/profile/orders/[id]/page.tsx +32 -0
  217. package/templates/nextblock-template/app/profile/orders/page.tsx +19 -0
  218. package/templates/nextblock-template/app/profile/page.tsx +51 -0
  219. package/templates/nextblock-template/app/profile/password/PasswordSettingsPageClient.tsx +128 -0
  220. package/templates/nextblock-template/app/profile/password/actions.ts +59 -0
  221. package/templates/nextblock-template/app/profile/password/page.tsx +27 -0
  222. package/templates/nextblock-template/app/providers.tsx +55 -17
  223. package/templates/nextblock-template/app/robots.txt/route.ts +11 -1
  224. package/templates/nextblock-template/app/sitemap.ts +128 -0
  225. package/templates/nextblock-template/app/ucp/v1/carts/[id]/cancel/route.ts +38 -0
  226. package/templates/nextblock-template/app/ucp/v1/carts/[id]/route.ts +68 -0
  227. package/templates/nextblock-template/app/ucp/v1/carts/route.ts +35 -0
  228. package/templates/nextblock-template/app/ucp/v1/catalog/lookup/route.ts +35 -0
  229. package/templates/nextblock-template/app/ucp/v1/catalog/product/route.ts +35 -0
  230. package/templates/nextblock-template/app/ucp/v1/catalog/search/route.ts +34 -0
  231. package/templates/nextblock-template/components/AppShell.tsx +154 -0
  232. package/templates/nextblock-template/components/BlockRenderer.tsx +210 -64
  233. package/templates/nextblock-template/components/CartDrawerLoader.tsx +7 -0
  234. package/templates/nextblock-template/components/CartTranslator.tsx +210 -0
  235. package/templates/nextblock-template/components/CurrentContentSetter.tsx +25 -0
  236. package/templates/nextblock-template/components/DeferredCartDrawer.tsx +23 -0
  237. package/templates/nextblock-template/components/DeferredCartTranslator.tsx +51 -0
  238. package/templates/nextblock-template/components/DeferredGlobalSearch.tsx +68 -0
  239. package/templates/nextblock-template/components/DeferredGoogleTagManager.tsx +70 -0
  240. package/templates/nextblock-template/components/DeferredSpeedInsights.tsx +69 -0
  241. package/templates/nextblock-template/components/FeatureImageHero.tsx +47 -0
  242. package/templates/nextblock-template/components/GitHubLoginButton.tsx +36 -0
  243. package/templates/nextblock-template/components/GlobalSearch.tsx +557 -0
  244. package/templates/nextblock-template/components/Header.tsx +49 -41
  245. package/templates/nextblock-template/components/LanguageSwitcher.tsx +55 -32
  246. package/templates/nextblock-template/components/ResponsiveNav.tsx +138 -43
  247. package/templates/nextblock-template/components/blocks/PostCardSkeleton.tsx +12 -8
  248. package/templates/nextblock-template/components/blocks/PostsGridBlock.tsx +12 -55
  249. package/templates/nextblock-template/components/blocks/PostsGridClient.tsx +42 -37
  250. package/templates/nextblock-template/components/blocks/TestimonialBlock.tsx +6 -2
  251. package/templates/nextblock-template/components/blocks/ecommerceRendererLoaders.ts +23 -0
  252. package/templates/nextblock-template/components/blocks/publicRendererLoaders.ts +25 -0
  253. package/templates/nextblock-template/components/blocks/renderers/ButtonBlockRenderer.tsx +92 -84
  254. package/templates/nextblock-template/components/blocks/renderers/CartBlockRenderer.tsx +17 -0
  255. package/templates/nextblock-template/components/blocks/renderers/CheckoutBlockRenderer.tsx +19 -0
  256. package/templates/nextblock-template/components/blocks/renderers/ClientTextBlockRenderer.tsx +262 -8
  257. package/templates/nextblock-template/components/blocks/renderers/FeaturedProductBlockRenderer.tsx +22 -0
  258. package/templates/nextblock-template/components/blocks/renderers/FormBlockRenderer.tsx +320 -37
  259. package/templates/nextblock-template/components/blocks/renderers/HeadingBlockRenderer.tsx +11 -8
  260. package/templates/nextblock-template/components/blocks/renderers/ImageBlockRenderer.tsx +12 -3
  261. package/templates/nextblock-template/components/blocks/renderers/PostsGridBlockRenderer.tsx +18 -13
  262. package/templates/nextblock-template/components/blocks/renderers/ProductDetailsBlockRenderer.tsx +90 -0
  263. package/templates/nextblock-template/components/blocks/renderers/ProductGridBlockRenderer.tsx +31 -0
  264. package/templates/nextblock-template/components/blocks/renderers/SectionBlockRenderer.tsx +424 -55
  265. package/templates/nextblock-template/components/blocks/renderers/SectionSlider.tsx +137 -0
  266. package/templates/nextblock-template/components/blocks/renderers/TestimonialBlockRenderer.tsx +57 -0
  267. package/templates/nextblock-template/components/blocks/renderers/TextBlockRenderer.tsx +37 -22
  268. package/templates/nextblock-template/components/blocks/renderers/VideoEmbedBlockRenderer.tsx +23 -15
  269. package/templates/nextblock-template/components/blocks/renderers/inline/AlertWidgetRenderer.tsx +1 -3
  270. package/templates/nextblock-template/components/blocks/renderers/inline/CtaWidgetRenderer.tsx +1 -3
  271. package/templates/nextblock-template/components/blocks/types.ts +7 -6
  272. package/templates/nextblock-template/components/env-var-warning.tsx +3 -3
  273. package/templates/nextblock-template/components/form-message.tsx +32 -26
  274. package/templates/nextblock-template/components/header-auth.tsx +69 -17
  275. package/templates/nextblock-template/components/privacy/ConsentBanner.tsx +127 -0
  276. package/templates/nextblock-template/components/privacy/ConsentGatedAnalytics.tsx +59 -0
  277. package/templates/nextblock-template/components/renderers/CachedDynamicLayoutEngine.tsx +28 -0
  278. package/templates/nextblock-template/components/renderers/DynamicLayoutEngine.test.tsx +166 -0
  279. package/templates/nextblock-template/components/renderers/DynamicLayoutEngine.tsx +464 -0
  280. package/templates/nextblock-template/components/theme-switcher.tsx +8 -8
  281. package/templates/nextblock-template/components/visual-editing/DeferredVisualEditing.tsx +21 -0
  282. package/templates/nextblock-template/components/visual-editing/NextblockVisualEditing.tsx +1172 -0
  283. package/templates/nextblock-template/context/AuthContext.tsx +23 -90
  284. package/templates/nextblock-template/context/CurrentContentContext.tsx +10 -4
  285. package/templates/nextblock-template/context/LanguageContext.tsx +16 -16
  286. package/templates/nextblock-template/context/language-rest-client.ts +31 -0
  287. package/templates/nextblock-template/docs/01-PROJECT-OVERVIEW.md +94 -0
  288. package/templates/nextblock-template/docs/02-ECOMMERCE-CAPABILITIES.md +364 -0
  289. package/templates/nextblock-template/docs/03-CMS-AND-EDITOR.md +202 -0
  290. package/templates/nextblock-template/docs/04-DATABASE-AND-AUTH.md +252 -0
  291. package/templates/nextblock-template/docs/05-DEVELOPER-GUIDE.md +238 -0
  292. package/templates/nextblock-template/docs/06-CLI-AND-SCAFFOLDING.md +125 -0
  293. package/templates/nextblock-template/docs/07-BLOCK-SDK-AND-EXTENSIBILITY.md +146 -0
  294. package/templates/nextblock-template/docs/08-NEXTBLOCK-CORTEX-AI-ARCHITECTURE.md +1319 -0
  295. package/templates/nextblock-template/docs/09-LIVE-DRAFT-MODE.md +104 -0
  296. package/templates/nextblock-template/docs/10-CUSTOM-BLOCKS.md +222 -0
  297. package/templates/nextblock-template/docs/README.md +34 -0
  298. package/templates/nextblock-template/docs/TECHNICAL_SPECIFICATION.md +12507 -0
  299. package/templates/nextblock-template/hooks/use-hotkeys.ts +21 -14
  300. package/templates/nextblock-template/hooks/useGlobalSearch.ts +101 -0
  301. package/templates/nextblock-template/index.d.ts +2 -0
  302. package/templates/nextblock-template/lib/ai-block-generation.ts +339 -0
  303. package/templates/nextblock-template/lib/ai-client.ts +247 -0
  304. package/templates/nextblock-template/lib/ai-config.ts +81 -0
  305. package/templates/nextblock-template/lib/ai-cortex-widget-builder.ts +125 -0
  306. package/templates/nextblock-template/lib/ai-global-agent-custom-block-tools.ts +363 -0
  307. package/templates/nextblock-template/lib/ai-global-agent-db-tools.test.ts +405 -0
  308. package/templates/nextblock-template/lib/ai-global-agent-db-tools.ts +1228 -0
  309. package/templates/nextblock-template/lib/ai-global-agent-ecommerce.ts +5 -0
  310. package/templates/nextblock-template/lib/ai-global-agent-tools-stats.test.ts +223 -0
  311. package/templates/nextblock-template/lib/ai-global-agent-tools.test.ts +2183 -0
  312. package/templates/nextblock-template/lib/ai-global-agent-tools.ts +4807 -0
  313. package/templates/nextblock-template/lib/ai-key-crypto.test.ts +70 -0
  314. package/templates/nextblock-template/lib/ai-key-crypto.ts +132 -0
  315. package/templates/nextblock-template/lib/ai-model-catalog.test.ts +49 -0
  316. package/templates/nextblock-template/lib/ai-model-catalog.ts +41 -0
  317. package/templates/nextblock-template/lib/ai-model-registry.test.ts +231 -0
  318. package/templates/nextblock-template/lib/ai-model-registry.ts +522 -0
  319. package/templates/nextblock-template/lib/auth/cookies.ts +47 -0
  320. package/templates/nextblock-template/lib/auth/crypto.ts +42 -0
  321. package/templates/nextblock-template/lib/auth/trustedDevices.ts +92 -0
  322. package/templates/nextblock-template/lib/auth/twoFactor.ts +167 -0
  323. package/templates/nextblock-template/lib/auth-redirects.ts +46 -0
  324. package/templates/nextblock-template/lib/blocks/FeaturedProductBlock.tsx +94 -0
  325. package/templates/nextblock-template/lib/blocks/ProductGridBlock.tsx +137 -0
  326. package/templates/nextblock-template/lib/blocks/README.md +13 -670
  327. package/templates/nextblock-template/lib/blocks/blockRegistry.ts +138 -56
  328. package/templates/nextblock-template/lib/blocks/blockTypes.ts +18 -0
  329. package/templates/nextblock-template/lib/blocks/ecommerce-block-schemas.ts +31 -0
  330. package/templates/nextblock-template/lib/cms-transfer/csv.test.ts +77 -0
  331. package/templates/nextblock-template/lib/cms-transfer/csv.ts +399 -0
  332. package/templates/nextblock-template/lib/cms-transfer/server.ts +2243 -0
  333. package/templates/nextblock-template/lib/cms-transfer/types.ts +145 -0
  334. package/templates/nextblock-template/lib/cortex-widget-registry.test.ts +199 -0
  335. package/templates/nextblock-template/lib/cortex-widget-registry.ts +88 -0
  336. package/templates/nextblock-template/lib/cortex-widget-schema.test.tsx +237 -0
  337. package/templates/nextblock-template/lib/cortex-widget-schema.ts +393 -0
  338. package/templates/nextblock-template/lib/custom-block-definitions.ts +87 -0
  339. package/templates/nextblock-template/lib/custom-block-r2-upload-shared.ts +178 -0
  340. package/templates/nextblock-template/lib/custom-block-r2-upload.test.ts +140 -0
  341. package/templates/nextblock-template/lib/custom-block-r2-upload.ts +68 -0
  342. package/templates/nextblock-template/lib/custom-block-relation-registry.ts +256 -0
  343. package/templates/nextblock-template/lib/custom-block-relations.test.ts +227 -0
  344. package/templates/nextblock-template/lib/custom-block-relations.ts +279 -0
  345. package/templates/nextblock-template/lib/custom-block-safelist.ts +14 -0
  346. package/templates/nextblock-template/lib/editor/dynamic-extension-core.test.ts +172 -0
  347. package/templates/nextblock-template/lib/editor/dynamic-extension-core.ts +213 -0
  348. package/templates/nextblock-template/lib/editor/dynamic-extension-loader.ts +22 -0
  349. package/templates/nextblock-template/lib/editor/dynamic-extensions.tsx +193 -0
  350. package/templates/nextblock-template/lib/full-backup/manifest.test.ts +121 -0
  351. package/templates/nextblock-template/lib/full-backup/manifest.ts +206 -0
  352. package/templates/nextblock-template/lib/full-backup/server.ts +743 -0
  353. package/templates/nextblock-template/lib/media/resolveMediaUrl.ts +45 -0
  354. package/templates/nextblock-template/lib/posts/readTime.ts +60 -0
  355. package/templates/nextblock-template/lib/privacy/consent-client.ts +57 -0
  356. package/templates/nextblock-template/lib/privacy/settings.ts +103 -0
  357. package/templates/nextblock-template/lib/privacy/types.ts +67 -0
  358. package/templates/nextblock-template/lib/promotions/server.test.ts +74 -0
  359. package/templates/nextblock-template/lib/promotions/server.ts +741 -0
  360. package/templates/nextblock-template/lib/resolve-block-relations.test.ts +142 -0
  361. package/templates/nextblock-template/lib/resolve-block-relations.ts +255 -0
  362. package/templates/nextblock-template/lib/search/server.ts +585 -0
  363. package/templates/nextblock-template/lib/search/types.ts +27 -0
  364. package/templates/nextblock-template/lib/visual-editing/draft-content.test.ts +105 -0
  365. package/templates/nextblock-template/lib/visual-editing/draft-content.ts +380 -0
  366. package/templates/nextblock-template/lib/visual-editing/draft-route.test.ts +42 -0
  367. package/templates/nextblock-template/lib/visual-editing/draft-route.ts +82 -0
  368. package/templates/nextblock-template/lib/visual-editing/edit-info.test.ts +143 -0
  369. package/templates/nextblock-template/lib/visual-editing/edit-info.ts +94 -0
  370. package/templates/nextblock-template/lib/visual-editing/mutations.ts +190 -0
  371. package/templates/nextblock-template/lib/visual-editing/product-drafts.test.ts +81 -0
  372. package/templates/nextblock-template/lib/visual-editing/product-drafts.ts +511 -0
  373. package/templates/nextblock-template/lib/visual-editing/types.ts +122 -0
  374. package/templates/nextblock-template/lib/zod-config.ts +5 -0
  375. package/templates/nextblock-template/next.config.js +190 -66
  376. package/templates/nextblock-template/package.json +34 -30
  377. package/templates/nextblock-template/proxy.ts +435 -253
  378. package/templates/nextblock-template/public/images/NBcover.webp +0 -0
  379. package/templates/nextblock-template/public/images/cap.webp +0 -0
  380. package/templates/nextblock-template/public/images/commerce-plan.webp +0 -0
  381. package/templates/nextblock-template/public/images/commerce-square.webp +0 -0
  382. package/templates/nextblock-template/public/images/commerce-wide.webp +0 -0
  383. package/templates/nextblock-template/public/images/cortex-ai-square.webp +0 -0
  384. package/templates/nextblock-template/public/images/cortex-ai.webp +0 -0
  385. package/templates/nextblock-template/public/images/extensibility.webp +0 -0
  386. package/templates/nextblock-template/public/images/goals.webp +0 -0
  387. package/templates/nextblock-template/public/images/included.webp +0 -0
  388. package/templates/nextblock-template/public/images/nx-graph.webp +0 -0
  389. package/templates/nextblock-template/public/images/pants.webp +0 -0
  390. package/templates/nextblock-template/public/images/t-shirt.webp +0 -0
  391. package/templates/nextblock-template/scripts/validate-editor-block-schema.ts +112 -0
  392. package/templates/nextblock-template/scripts/verify-cortex-ai-build-widget.tsx +100 -0
  393. package/templates/nextblock-template/scripts/verify-cortex-ai-generate-blocks.ts +62 -0
  394. package/templates/nextblock-template/scripts/verify-cortex-ai-global-tools.ts +537 -0
  395. package/templates/nextblock-template/scripts/verify-cortex-ai-routing.ts +58 -0
  396. package/templates/nextblock-template/scripts/verify-custom-block-definitions.ts +188 -0
  397. package/templates/nextblock-template/scripts/verify-dynamic-custom-block-extensions.ts +123 -0
  398. package/templates/nextblock-template/scripts/verify-dynamic-layout-engine.tsx +133 -0
  399. package/templates/nextblock-template/scripts/verify-milestone-2-custom-blocks.ts +65 -0
  400. package/templates/nextblock-template/tailwind.config.js +1 -0
  401. package/templates/nextblock-template/tools/configure-supabase-auth.js +282 -0
  402. package/templates/nextblock-template/tools/deploy-supabase.js +69 -71
  403. package/templates/nextblock-template/tsconfig.json +52 -66
  404. package/templates/nextblock-template/tsconfig.tsbuildinfo +1 -1
  405. package/templates/nextblock-template/types/jsdom.d.ts +6 -0
  406. package/templates/nextblock-template/app/force-styles.tsx +0 -31
  407. package/templates/nextblock-template/app/sitemap.xml/route.ts +0 -63
  408. package/templates/nextblock-template/components/blocks/renderers/HeroBlockRenderer.tsx +0 -273
  409. package/templates/nextblock-template/docs/How to Create a Custom Block.md +0 -149
  410. package/templates/nextblock-template/docs/cms-application-overview.md +0 -56
  411. package/templates/nextblock-template/docs/cms-architecture-overview.md +0 -73
  412. package/templates/nextblock-template/docs/files-structure.md +0 -426
  413. package/templates/nextblock-template/docs/tiptap-bundle-optimization-summary.md +0 -174
@@ -3,12 +3,11 @@
3
3
 
4
4
  import { createClient } from "@nextblock-cms/db/server";
5
5
  import { revalidatePath } from "next/cache";
6
- import type { Database, Json } from "@nextblock-cms/db";
6
+ import type { Database } from "@nextblock-cms/db";
7
7
  import { getInitialContent, isValidBlockType } from "../../../lib/blocks/blockRegistry";
8
- import { getFullPageContent, getFullPostContent } from "../revisions/utils";
9
- import { createPageRevision, createPostRevision } from "../revisions/service";
8
+ import { getOrCreateContentDraft } from "../../../lib/visual-editing/draft-content";
9
+ import { getOrCreateProductDraft } from "../../../lib/visual-editing/product-drafts";
10
10
 
11
- type Block = Database['public']['Tables']['blocks']['Row'];
12
11
  type BlockType = Database['public']['Tables']['blocks']['Row']['block_type'];
13
12
 
14
13
  // Helper to verify user can edit the parent (page/post)
@@ -16,10 +15,12 @@ async function canEditParent(
16
15
  supabase: ReturnType<typeof createClient>,
17
16
  userId: string,
18
17
  pageId?: number | null,
19
- postId?: number | null
18
+ postId?: number | null,
19
+ productId?: string | null
20
20
  ): Promise<boolean> {
21
21
  void pageId;
22
22
  void postId;
23
+ void productId;
23
24
  const { data: profile } = await supabase
24
25
  .from("profiles")
25
26
  .select("role")
@@ -29,19 +30,9 @@ async function canEditParent(
29
30
  if (!profile || !["ADMIN", "WRITER"].includes(profile.role)) {
30
31
  return false;
31
32
  }
32
- // Further checks could be added here to see if a WRITER owns the page/post
33
33
  return true;
34
34
  }
35
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
36
  export async function createBlockForPage(pageId: number, languageId: number, blockType: BlockType, order: number) {
46
37
  const supabase = createClient();
47
38
  const { data: { user } } = await supabase.auth.getUser();
@@ -62,34 +53,44 @@ export async function createBlockForPage(pageId: number, languageId: number, blo
62
53
  return { error: "Failed to get initial content for block type." };
63
54
  }
64
55
 
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
- }
56
+ try {
57
+ const draft = await getOrCreateContentDraft(supabase, "page", pageId, user.id);
58
+ let newBlockId = -1 - Math.floor(Math.random() * 9999999);
59
+ while (draft.blocks.some(b => b.id === newBlockId)) {
60
+ newBlockId = -1 - Math.floor(Math.random() * 9999999);
61
+ }
82
62
 
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);
63
+ const newBlock = {
64
+ id: newBlockId,
65
+ page_id: pageId,
66
+ post_id: null,
67
+ language_id: languageId,
68
+ block_type: blockType,
69
+ content: initialContent,
70
+ order: order,
71
+ created_at: new Date().toISOString(),
72
+ updated_at: new Date().toISOString(),
73
+ };
74
+
75
+ const updatedBlocks = [...draft.blocks, newBlock];
76
+ updatedBlocks.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
77
+
78
+ const { error: updateError } = await supabase
79
+ .from("content_drafts")
80
+ .update({ blocks: updatedBlocks as any })
81
+ .eq("id", draft.id);
82
+
83
+ if (updateError) {
84
+ console.error("Error creating draft block for page:", updateError);
85
+ return { error: `Failed to save draft block: ${updateError.message}` };
88
86
  }
89
- }
90
87
 
91
- revalidatePath(`/cms/pages/${pageId}/edit`);
92
- return { success: true, newBlock: data as Block };
88
+ revalidatePath(`/cms/pages/${pageId}/edit`);
89
+ return { success: true, newBlock: newBlock as any };
90
+ } catch (err: any) {
91
+ console.error("Error getting draft for page blocks update:", err);
92
+ return { error: `Failed to load page draft: ${err.message || err}` };
93
+ }
93
94
  }
94
95
 
95
96
  export async function createBlockForPost(postId: number, languageId: number, blockType: BlockType, order: number) {
@@ -112,184 +113,338 @@ export async function createBlockForPost(postId: number, languageId: number, blo
112
113
  return { error: "Failed to get initial content for block type." };
113
114
  }
114
115
 
115
- const payload: CreateBlockPayload = {
116
- post_id: postId,
117
- language_id: languageId,
118
- block_type: blockType,
119
- content: initialContent,
120
- order: order,
121
- };
116
+ try {
117
+ const draft = await getOrCreateContentDraft(supabase, "post", postId, user.id);
118
+ let newBlockId = -1 - Math.floor(Math.random() * 9999999);
119
+ while (draft.blocks.some(b => b.id === newBlockId)) {
120
+ newBlockId = -1 - Math.floor(Math.random() * 9999999);
121
+ }
122
122
 
123
- // capture previous content
124
- const previousContent = await getFullPostContent(postId);
123
+ const newBlock = {
124
+ id: newBlockId,
125
+ page_id: null,
126
+ post_id: postId,
127
+ language_id: languageId,
128
+ block_type: blockType,
129
+ content: initialContent,
130
+ order: order,
131
+ created_at: new Date().toISOString(),
132
+ updated_at: new Date().toISOString(),
133
+ };
134
+
135
+ const updatedBlocks = [...draft.blocks, newBlock];
136
+ updatedBlocks.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
137
+
138
+ const { error: updateError } = await supabase
139
+ .from("content_drafts")
140
+ .update({ blocks: updatedBlocks as any })
141
+ .eq("id", draft.id);
142
+
143
+ if (updateError) {
144
+ console.error("Error creating draft block for post:", updateError);
145
+ return { error: `Failed to save draft block: ${updateError.message}` };
146
+ }
125
147
 
126
- const { data, error } = await supabase.from("blocks").insert(payload).select().single();
148
+ revalidatePath(`/cms/posts/${postId}/edit`);
149
+ return { success: true, newBlock: newBlock as any };
150
+ } catch (err: any) {
151
+ console.error("Error getting draft for post blocks update:", err);
152
+ return { error: `Failed to load post draft: ${err.message || err}` };
153
+ }
154
+ }
155
+
156
+ export async function createBlockForProduct(productId: string, languageId: number, blockType: BlockType, order: number) {
157
+ const supabase = createClient();
158
+ const { data: { user } } = await supabase.auth.getUser();
127
159
 
128
- if (error) {
129
- console.error("Error creating block:", error);
130
- return { error: `Failed to create block: ${error.message}` };
160
+ if (!user) return { error: "User not authenticated." };
161
+ if (!(await canEditParent(supabase, user.id, null, null, productId))) {
162
+ return { error: "Unauthorized to add blocks to this product." };
131
163
  }
132
164
 
133
- if (previousContent && user) {
134
- const newContent = await getFullPostContent(postId);
135
- if (newContent) {
136
- await createPostRevision(postId, user.id, previousContent, newContent);
137
- }
165
+ // Validate block type using registry
166
+ if (!isValidBlockType(blockType)) {
167
+ return { error: "Unknown block type." };
138
168
  }
139
169
 
140
- revalidatePath(`/cms/posts/${postId}/edit`);
141
- return { success: true, newBlock: data as Block };
170
+ // Get initial content from registry
171
+ const initialContent = getInitialContent(blockType);
172
+ if (!initialContent) {
173
+ return { error: "Failed to get initial content for block type." };
174
+ }
175
+
176
+ try {
177
+ const draft = await getOrCreateProductDraft(supabase, productId, user.id);
178
+ let newBlockId = -1 - Math.floor(Math.random() * 9999999);
179
+ while (draft.blocks && draft.blocks.some(b => b.id === newBlockId)) {
180
+ newBlockId = -1 - Math.floor(Math.random() * 9999999);
181
+ }
182
+
183
+ const newBlock = {
184
+ id: newBlockId,
185
+ page_id: null,
186
+ post_id: null,
187
+ product_id: productId,
188
+ language_id: languageId,
189
+ block_type: blockType,
190
+ content: initialContent,
191
+ order: order,
192
+ created_at: new Date().toISOString(),
193
+ updated_at: new Date().toISOString(),
194
+ };
195
+
196
+ const updatedBlocks = [...(draft.blocks || []), newBlock];
197
+ updatedBlocks.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
198
+
199
+ const { error: updateError } = await supabase
200
+ .from("product_drafts")
201
+ .update({ blocks: updatedBlocks as any })
202
+ .eq("id", draft.id);
203
+
204
+ if (updateError) {
205
+ console.error("Error creating draft block for product:", updateError);
206
+ return { error: `Failed to save draft block: ${updateError.message}` };
207
+ }
208
+
209
+ revalidatePath(`/cms/products/${productId}/edit`);
210
+ return { success: true, newBlock: newBlock as any };
211
+ } catch (err: any) {
212
+ console.error("Error getting draft for product blocks update:", err);
213
+ return { error: `Failed to load product draft: ${err.message || err}` };
214
+ }
142
215
  }
143
216
 
144
- export async function updateBlock(blockId: number, newContent: unknown, pageId?: number | null, postId?: number | null) {
217
+ export async function updateBlock(
218
+ blockId: number,
219
+ newContent: unknown,
220
+ pageId?: number | null,
221
+ postId?: number | null,
222
+ productId?: string | null
223
+ ) {
145
224
  const supabase = createClient();
146
225
  const { data: { user } } = await supabase.auth.getUser();
147
226
 
148
227
  if (!user) return { error: "User not authenticated." };
149
- if (!(await canEditParent(supabase, user.id, pageId, postId))) {
228
+ if (!(await canEditParent(supabase, user.id, pageId, postId, productId))) {
150
229
  return { error: "Unauthorized to update this block." };
151
230
  }
152
231
 
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
- }
232
+ if (productId) {
233
+ try {
234
+ const draft = await getOrCreateProductDraft(supabase, productId, user.id);
235
+ const existingBlock = (draft.blocks || []).find(b => b.id === blockId);
236
+ if (!existingBlock) {
237
+ return { error: "Block not found in draft." };
238
+ }
162
239
 
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
- }
240
+ const updatedBlocks = (draft.blocks || []).map(b =>
241
+ b.id === blockId
242
+ ? { ...b, content: newContent as any, updated_at: new Date().toISOString() }
243
+ : b
244
+ );
169
245
 
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();
246
+ const { error: updateError } = await supabase
247
+ .from("product_drafts")
248
+ .update({ blocks: updatedBlocks as any })
249
+ .eq("id", draft.id);
176
250
 
177
- if (error) {
178
- console.error("Error updating block:", error);
179
- return { error: `Failed to update block: ${error.message}` };
180
- }
251
+ if (updateError) {
252
+ console.error("Error updating draft block content for product:", updateError);
253
+ return { error: `Failed to update draft block: ${updateError.message}` };
254
+ }
181
255
 
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);
256
+ return { success: true, updatedBlock: { ...existingBlock, content: newContent } as any };
257
+ } catch (err: any) {
258
+ console.error("Error getting draft for product block content update:", err);
259
+ return { error: `Failed to load product draft: ${err.message || err}` };
260
+ }
261
+ } else {
262
+ const parentType = pageId ? "page" : "post";
263
+ const parentId = pageId || postId;
264
+ if (!parentId) return { error: "Missing pageId or postId." };
265
+
266
+ try {
267
+ const draft = await getOrCreateContentDraft(supabase, parentType, parentId, user.id);
268
+ const existingBlock = draft.blocks.find(b => b.id === blockId);
269
+ if (!existingBlock) {
270
+ return { error: "Block not found in draft." };
188
271
  }
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);
272
+
273
+ const updatedBlocks = draft.blocks.map(b =>
274
+ b.id === blockId
275
+ ? { ...b, content: newContent as any, updated_at: new Date().toISOString() }
276
+ : b
277
+ );
278
+
279
+ const { error: updateError } = await supabase
280
+ .from("content_drafts")
281
+ .update({ blocks: updatedBlocks as any })
282
+ .eq("id", draft.id);
283
+
284
+ if (updateError) {
285
+ console.error("Error updating draft block content:", updateError);
286
+ return { error: `Failed to update draft block: ${updateError.message}` };
193
287
  }
288
+
289
+ return { success: true, updatedBlock: { ...existingBlock, content: newContent } as any };
290
+ } catch (err: any) {
291
+ console.error("Error getting draft for block content update:", err);
292
+ return { error: `Failed to load draft: ${err.message || err}` };
194
293
  }
195
294
  }
196
-
197
- return { success: true, updatedBlock: data as Block };
198
295
  }
199
296
 
200
297
  export async function updateMultipleBlockOrders(
201
298
  updates: Array<{ id: number; order: number }>,
202
299
  pageId?: number | null,
203
- postId?: number | null
300
+ postId?: number | null,
301
+ productId?: string | null
204
302
  ) {
205
303
  const supabase = createClient();
206
304
  const { data: { user } } = await supabase.auth.getUser();
207
305
 
208
306
  if (!user) return { error: "User not authenticated." };
209
- if (!(await canEditParent(supabase, user.id, pageId, postId))) {
307
+ if (!(await canEditParent(supabase, user.id, pageId, postId, productId))) {
210
308
  return { error: "Unauthorized to reorder blocks." };
211
309
  }
212
310
 
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);
311
+ if (productId) {
312
+ try {
313
+ const draft = await getOrCreateProductDraft(supabase, productId, user.id);
314
+ const updatedBlocks = (draft.blocks || []).map(b => {
315
+ const update = updates.find(u => u.id === b.id);
316
+ if (update) {
317
+ return { ...b, order: update.order, updated_at: new Date().toISOString() };
318
+ }
319
+ return b;
320
+ });
321
+
322
+ updatedBlocks.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
323
+
324
+ const { error: updateError } = await supabase
325
+ .from("product_drafts")
326
+ .update({ blocks: updatedBlocks as any })
327
+ .eq("id", draft.id);
328
+
329
+ if (updateError) {
330
+ console.error("Error updating draft blocks order for product:", updateError);
331
+ return { error: `Failed to save blocks order draft: ${updateError.message}` };
332
+ }
221
333
 
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
- }
334
+ revalidatePath(`/cms/products/${productId}/edit`);
335
+ return { success: true };
336
+ } catch (err: any) {
337
+ console.error("Error getting draft for product blocks order update:", err);
338
+ return { error: `Failed to load product draft: ${err.message || err}` };
339
+ }
340
+ } else {
341
+ const parentType = pageId ? "page" : "post";
342
+ const parentId = pageId || postId;
343
+ if (!parentId) return { error: "Missing pageId or postId." };
344
+
345
+ try {
346
+ const draft = await getOrCreateContentDraft(supabase, parentType, parentId, user.id);
347
+ const updatedBlocks = draft.blocks.map(b => {
348
+ const update = updates.find(u => u.id === b.id);
349
+ if (update) {
350
+ return { ...b, order: update.order, updated_at: new Date().toISOString() };
351
+ }
352
+ return b;
353
+ });
354
+
355
+ updatedBlocks.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
356
+
357
+ const { error: updateError } = await supabase
358
+ .from("content_drafts")
359
+ .update({ blocks: updatedBlocks as any })
360
+ .eq("id", draft.id);
361
+
362
+ if (updateError) {
363
+ console.error("Error updating draft blocks order:", updateError);
364
+ return { error: `Failed to save blocks order draft: ${updateError.message}` };
365
+ }
226
366
 
227
- if (pageId) revalidatePath(`/cms/pages/${pageId}/edit`);
228
- if (postId) revalidatePath(`/cms/posts/${postId}/edit`);
367
+ if (pageId) revalidatePath(`/cms/pages/${pageId}/edit`);
368
+ if (postId) revalidatePath(`/cms/posts/${postId}/edit`);
229
369
 
230
- return { success: true };
370
+ return { success: true };
371
+ } catch (err: any) {
372
+ console.error("Error getting draft for blocks order update:", err);
373
+ return { error: `Failed to load draft: ${err.message || err}` };
374
+ }
375
+ }
231
376
  }
232
377
 
233
-
234
- export async function deleteBlock(blockId: number, pageId?: number | null, postId?: number | null) {
378
+ export async function deleteBlock(
379
+ blockId: number,
380
+ pageId?: number | null,
381
+ postId?: number | null,
382
+ productId?: string | null
383
+ ) {
235
384
  const supabase = createClient();
236
385
  const { data: { user } } = await supabase.auth.getUser();
237
386
 
238
387
  if (!user) return { error: "User not authenticated." };
239
- if (!(await canEditParent(supabase, user.id, pageId, postId))) {
388
+ if (!(await canEditParent(supabase, user.id, pageId, postId, productId))) {
240
389
  return { error: "Unauthorized to delete this block." };
241
390
  }
242
391
 
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
- }
392
+ if (productId) {
393
+ try {
394
+ const draft = await getOrCreateProductDraft(supabase, productId, user.id);
395
+ const updatedBlocks = (draft.blocks || []).filter(b => b.id !== blockId);
259
396
 
260
- const { error } = await supabase.from("blocks").delete().eq("id", blockId);
397
+ const { error: updateError } = await supabase
398
+ .from("product_drafts")
399
+ .update({ blocks: updatedBlocks as any })
400
+ .eq("id", draft.id);
261
401
 
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);
402
+ if (updateError) {
403
+ console.error("Error deleting draft block for product:", updateError);
404
+ return { error: `Failed to delete block from product draft: ${updateError.message}` };
278
405
  }
406
+
407
+ revalidatePath(`/cms/products/${productId}/edit`);
408
+ return { success: true };
409
+ } catch (err: any) {
410
+ console.error("Error getting draft for product block deletion:", err);
411
+ return { error: `Failed to load product draft: ${err.message || err}` };
279
412
  }
280
- }
413
+ } else {
414
+ const parentType = pageId ? "page" : "post";
415
+ const parentId = pageId || postId;
416
+ if (!parentId) return { error: "Missing pageId or postId." };
417
+
418
+ try {
419
+ const draft = await getOrCreateContentDraft(supabase, parentType, parentId, user.id);
420
+ const updatedBlocks = draft.blocks.filter(b => b.id !== blockId);
421
+
422
+ const { error: updateError } = await supabase
423
+ .from("content_drafts")
424
+ .update({ blocks: updatedBlocks as any })
425
+ .eq("id", draft.id);
426
+
427
+ if (updateError) {
428
+ console.error("Error deleting draft block:", updateError);
429
+ return { error: `Failed to delete block from draft: ${updateError.message}` };
430
+ }
281
431
 
282
- if (pageId) revalidatePath(`/cms/pages/${pageId}/edit`);
283
- if (postId) revalidatePath(`/cms/posts/${postId}/edit`);
432
+ if (pageId) revalidatePath(`/cms/pages/${pageId}/edit`);
433
+ if (postId) revalidatePath(`/cms/posts/${postId}/edit`);
284
434
 
285
- return { success: true };
435
+ return { success: true };
436
+ } catch (err: any) {
437
+ console.error("Error getting draft for block deletion:", err);
438
+ return { error: `Failed to load draft: ${err.message || err}` };
439
+ }
440
+ }
286
441
  }
287
442
 
288
443
  export async function copyBlocksFromLanguage(
289
- parentId: number, // ID of the page or post being edited
290
- parentType: "page" | "post",
444
+ parentId: number | string, // ID of the page, post, or product being edited
445
+ parentType: "page" | "post" | "product",
291
446
  sourceLanguageId: number,
292
- targetLanguageId: number, // Language of the current page/post being edited
447
+ targetLanguageId: number, // Language of the target being edited
293
448
  targetTranslationGroupId: string
294
449
  ) {
295
450
  "use server";
@@ -300,13 +455,12 @@ export async function copyBlocksFromLanguage(
300
455
  return { error: "User not authenticated." };
301
456
  }
302
457
 
303
- if (!(await canEditParent(supabase, user.id, parentType === "page" ? parentId : null, parentType === "post" ? parentId : null))) {
458
+ if (!(await canEditParent(supabase, user.id, parentType === "page" ? parentId as number : null, parentType === "post" ? parentId as number : null, parentType === "product" ? parentId as string : null))) {
304
459
  return { error: "Unauthorized to modify blocks for this target." };
305
460
  }
306
461
 
307
- let sourceParentId: number | null = null;
462
+ let sourceParentId: number | string | null = null;
308
463
 
309
- // 1. Fetch Source Page/Post ID
310
464
  try {
311
465
  if (parentType === "page") {
312
466
  const { data: sourcePage, error: sourcePageError } = await supabase
@@ -334,6 +488,19 @@ export async function copyBlocksFromLanguage(
334
488
  return { error: "Source post not found or error fetching it." };
335
489
  }
336
490
  sourceParentId = sourcePost.id;
491
+ } else if (parentType === "product") {
492
+ const { data: sourceProduct, error: sourceProductError } = await supabase
493
+ .from("products")
494
+ .select("id")
495
+ .eq("translation_group_id", targetTranslationGroupId)
496
+ .eq("language_id", sourceLanguageId)
497
+ .single();
498
+
499
+ if (sourceProductError || !sourceProduct) {
500
+ console.error("Error fetching source product:", sourceProductError);
501
+ return { error: "Source product not found or error fetching it." };
502
+ }
503
+ sourceParentId = sourceProduct.id;
337
504
  } else {
338
505
  return { error: "Invalid parent type specified." };
339
506
  }
@@ -342,88 +509,110 @@ export async function copyBlocksFromLanguage(
342
509
  return { error: "Could not determine source parent ID." };
343
510
  }
344
511
 
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}` };
512
+ let blocksToCopy: any[] = [];
513
+ if (parentType === "product") {
514
+ // 1. Check if source draft exists
515
+ const { data: sourceDraft } = await supabase
516
+ .from("product_drafts")
517
+ .select("blocks")
518
+ .eq("product_id", sourceParentId)
519
+ .maybeSingle();
520
+
521
+ if (sourceDraft && sourceDraft.blocks && Array.isArray(sourceDraft.blocks)) {
522
+ blocksToCopy = sourceDraft.blocks;
523
+ } else {
524
+ // 2. Fetch live blocks from source
525
+ const { data: liveBlocks, error: sourceBlocksError } = await supabase
526
+ .from("blocks")
527
+ .select("block_type, content, order")
528
+ .eq("product_id", sourceParentId)
529
+ .order("order", { ascending: true });
530
+
531
+ if (sourceBlocksError) {
532
+ console.error("Error fetching source blocks:", sourceBlocksError);
533
+ return { error: `Failed to fetch blocks from source: ${sourceBlocksError.message}` };
534
+ }
535
+ blocksToCopy = liveBlocks || [];
536
+ }
537
+ } else {
538
+ // 1. Check if source draft exists
539
+ const { data: sourceDraft } = await supabase
540
+ .from("content_drafts")
541
+ .select("blocks")
542
+ .eq("parent_type", parentType)
543
+ .eq("parent_id", sourceParentId)
544
+ .maybeSingle();
545
+
546
+ if (sourceDraft && sourceDraft.blocks && Array.isArray(sourceDraft.blocks)) {
547
+ blocksToCopy = sourceDraft.blocks;
548
+ } else {
549
+ // 2. Fetch live blocks from source
550
+ const { data: liveBlocks, error: sourceBlocksError } = await supabase
551
+ .from("blocks")
552
+ .select("block_type, content, order")
553
+ .eq(parentType === "page" ? "page_id" : "post_id", sourceParentId)
554
+ .order("order", { ascending: true });
555
+
556
+ if (sourceBlocksError) {
557
+ console.error("Error fetching source blocks:", sourceBlocksError);
558
+ return { error: `Failed to fetch blocks from source: ${sourceBlocksError.message}` };
559
+ }
560
+ blocksToCopy = liveBlocks || [];
561
+ }
367
562
  }
368
563
 
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,
564
+ // 3. Generate new blocks with negative IDs for draft array
565
+ const copiedBlocks = blocksToCopy.map((block: any, index: number) => {
566
+ const newId = -1 - Math.floor(Math.random() * 9999999);
567
+ return {
568
+ id: newId,
569
+ page_id: null,
570
+ post_id: null,
571
+ product_id: parentType === "product" ? (parentId as string) : null,
382
572
  language_id: targetLanguageId,
383
573
  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);
574
+ content: block.content,
575
+ order: block.order ?? index,
576
+ created_at: new Date().toISOString(),
577
+ updated_at: new Date().toISOString(),
578
+ };
579
+ });
580
+
581
+ if (parentType === "product") {
582
+ // 4. Load or create target draft and update blocks
583
+ const targetDraft = await getOrCreateProductDraft(supabase, parentId as string, user.id);
584
+ const { error: updateError } = await supabase
585
+ .from("product_drafts")
586
+ .update({ blocks: copiedBlocks as any })
587
+ .eq("id", targetDraft.id);
588
+
589
+ if (updateError) {
590
+ console.error("Error writing copied blocks to target product draft:", updateError);
591
+ return { error: `Failed to copy blocks to draft: ${updateError.message}` };
592
+ }
389
593
 
390
- if (insertError) {
391
- console.error("Error re-creating blocks:", insertError);
392
- return { error: `Failed to re-create blocks: ${insertError.message}` };
594
+ revalidatePath(`/cms/products/${parentId}/edit`);
595
+ } else {
596
+ // 4. Load or create target draft and update blocks
597
+ const targetDraft = await getOrCreateContentDraft(supabase, parentType, parentId as number, user.id);
598
+ const { error: updateError } = await supabase
599
+ .from("content_drafts")
600
+ .update({ blocks: copiedBlocks as any })
601
+ .eq("id", targetDraft.id);
602
+
603
+ if (updateError) {
604
+ console.error("Error writing copied blocks to target draft:", updateError);
605
+ return { error: `Failed to copy blocks to draft: ${updateError.message}` };
393
606
  }
394
- }
395
607
 
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(`/article/${targetSlug}`);
422
- }
423
- revalidatePath(`/cms/posts/${parentId}/edit`); // Revalidate edit page
608
+ // 5. Revalidation
609
+ if (parentType === "page") {
610
+ revalidatePath(`/cms/pages/${parentId}/edit`);
611
+ } else if (parentType === "post") {
612
+ revalidatePath(`/cms/posts/${parentId}/edit`);
613
+ }
424
614
  }
425
615
 
426
-
427
616
  return { success: true, message: "Blocks copied successfully." };
428
617
 
429
618
  } catch (e: unknown) {