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
@@ -1,285 +1,285 @@
1
- // apps/nextblock/app/cms/revisions/actions.ts
2
- "use server";
3
-
4
- import { createClient } from "@nextblock-cms/db/server";
5
- import { restorePageToVersion, restorePostToVersion, reconstructPageVersionContent, reconstructPostVersionContent } from './service';
6
- import { getFullPageContent, getFullPostContent } from './utils';
7
- import { compare } from 'fast-json-patch';
8
-
9
- type RevisionListItem = {
10
- id: number;
11
- version: number;
12
- revision_type: 'snapshot' | 'diff';
13
- created_at: string;
14
- author_id: string | null;
15
- author?: { full_name?: string | null; username?: string | null } | null;
16
- };
17
-
18
- const REVISIONS_PER_PAGE = 10; // Reduced to 10 as requested
19
-
20
- export async function listPageRevisions(pageId: number, page = 1, startDate?: string, endDate?: string) {
21
- const supabase = createClient();
22
-
23
- let query = supabase
24
- .from('page_revisions')
25
- .select('id, page_id, author_id, version, revision_type, created_at, content, author:profiles(full_name, username)', { count: 'exact' })
26
- .eq('page_id', pageId)
27
- .order('version', { ascending: false })
28
- .limit(1000); // Fetch up to 1000 recent revisions to allow dense pagination after filtering
29
-
30
- if (startDate) {
31
- query = query.gte('created_at', `${startDate}T00:00:00.000Z`);
32
- }
33
- if (endDate) {
34
- query = query.lte('created_at', `${endDate}T23:59:59.999Z`);
35
- }
36
-
37
- const { data, error } = await query; // count is less useful now as we filter
38
-
39
- if (error) return { error: error.message } as const;
40
-
41
- const { data: pageRow } = await supabase.from('pages').select('version, created_at').eq('id', pageId).single();
42
- const currentVersion = pageRow?.version ?? null;
43
- const currentContent = await getFullPageContent(pageId);
44
-
45
- // Map data to include useful flags instead of filtering
46
- const allRevisions = (data || []).map((r: any) => {
47
- let has_changes = true;
48
-
49
- // 1. Check Diffs
50
- if (r.revision_type === 'diff') {
51
- let ops = r.content;
52
- if (typeof ops === 'string') {
53
- try { ops = JSON.parse(ops); } catch { ops = []; }
54
- }
55
-
56
- // Strict check: Diff must be a non-empty array of operations
57
- if (!Array.isArray(ops) || ops.length === 0) {
58
- has_changes = false;
59
- } else {
60
- // Check metadata/variant
61
- const ignoredSuffixes = ['/updated_at', '/created_at', '/last_modified', '/modified_at', '/version', '/id', '/published_at', '/author_id', '/variant'];
62
- const usefulOps = ops.filter((op: any) => {
63
- if (!op.path) return true;
64
- return !ignoredSuffixes.some(suffix => op.path.endsWith(suffix));
65
- });
66
- if (usefulOps.length === 0) has_changes = false;
67
- }
68
- }
69
-
70
- // 2. Check Snapshots
71
- if (r.revision_type === 'snapshot' && currentContent && r.version !== currentVersion) {
72
- let snapshotContent = r.content;
73
- if (typeof snapshotContent === 'string') {
74
- try { snapshotContent = JSON.parse(snapshotContent); } catch { /* default true */ }
75
- }
76
-
77
- const ops = compare(currentContent, snapshotContent);
78
-
79
- const ignoredSuffixes = ['/updated_at', '/created_at', '/last_modified', '/modified_at', '/version', '/id', '/published_at', '/author_id', '/variant'];
80
- const usefulOps = ops.filter((op: any) => {
81
- if (!op.path) return true;
82
- return !ignoredSuffixes.some(suffix => op.path.endsWith(suffix));
83
- });
84
-
85
- if (usefulOps.length === 0) has_changes = false;
86
- }
87
-
88
- return { ...r, has_changes };
89
- }) as (RevisionListItem & { has_changes: boolean; content: any })[];
90
-
91
- // Append Synthetic V1
92
- const hasVersion1 = allRevisions.some(r => r.version === 1);
93
- const pageCreatedAt = pageRow?.created_at;
94
-
95
- let matchesDate = true;
96
- if (pageCreatedAt) {
97
- if (startDate && pageCreatedAt < `${startDate}T00:00:00.000Z`) matchesDate = false;
98
- if (endDate && pageCreatedAt > `${endDate}T23:59:59.999Z`) matchesDate = false;
99
- }
100
-
101
- if (!hasVersion1 && (currentVersion ?? 0) > 1 && matchesDate) {
102
- allRevisions.push({
103
- id: -1,
104
- version: 1,
105
- revision_type: 'snapshot',
106
- created_at: pageCreatedAt ?? new Date().toISOString(),
107
- author_id: null,
108
- author: { full_name: 'System (Initial)', username: 'system' },
109
- has_changes: true, // V1 always counts
110
- content: null
111
- });
112
- }
113
-
114
- allRevisions.sort((a, b) => b.version - a.version);
115
-
116
- const totalFiltered = allRevisions.length;
117
- const totalPages = Math.ceil(totalFiltered / REVISIONS_PER_PAGE);
118
-
119
- const fromIndex = (page - 1) * REVISIONS_PER_PAGE;
120
- // Use map to strip heavy content if not needed, but keep flags
121
- const slicedRevisions = allRevisions
122
- .slice(fromIndex, fromIndex + REVISIONS_PER_PAGE)
123
- .map(({ content, ...rest }) => rest);
124
-
125
- return {
126
- success: true as const,
127
- revisions: slicedRevisions,
128
- currentVersion,
129
- count: totalFiltered,
130
- totalPages,
131
- hasMore: (page * REVISIONS_PER_PAGE) < totalFiltered
132
- };
133
- }
134
-
135
- export async function listPostRevisions(postId: number, page = 1, startDate?: string, endDate?: string) {
136
- const supabase = createClient();
137
-
138
-
139
-
140
- let query = supabase
141
- .from('post_revisions')
142
- .select('id, post_id, author_id, version, revision_type, created_at, content, author:profiles(full_name, username)', { count: 'exact' })
143
- .eq('post_id', postId)
144
- .order('version', { ascending: false })
145
- .limit(1000);
146
-
147
- if (startDate) {
148
- query = query.gte('created_at', `${startDate}T00:00:00.000Z`);
149
- }
150
- if (endDate) {
151
- query = query.lte('created_at', `${endDate}T23:59:59.999Z`);
152
- }
153
-
154
- const { data, error } = await query;
155
-
156
- if (error) return { error: error.message } as const;
157
-
158
- const { data: postRow } = await supabase.from('posts').select('version, created_at').eq('id', postId).single();
159
- const currentVersion = postRow?.version ?? null;
160
- const currentContent = await getFullPostContent(postId);
161
-
162
- const allRevisions = (data || []).map((r: any) => {
163
- let has_changes = true;
164
-
165
- // 1. Check Diffs
166
- if (r.revision_type === 'diff') {
167
- let ops = r.content;
168
- if (typeof ops === 'string') {
169
- try { ops = JSON.parse(ops); } catch { ops = []; }
170
- }
171
-
172
- // Strict check: Diff must be a non-empty array of operations
173
- if (!Array.isArray(ops) || ops.length === 0) {
174
- has_changes = false;
175
- } else {
176
- // Check metadata/variant
177
- const ignoredSuffixes = ['/updated_at', '/created_at', '/last_modified', '/modified_at', '/version', '/id', '/published_at', '/author_id', '/variant'];
178
- const usefulOps = ops.filter((op: any) => {
179
- if (!op.path) return true;
180
- return !ignoredSuffixes.some(suffix => op.path.endsWith(suffix));
181
- });
182
- if (usefulOps.length === 0) has_changes = false;
183
- }
184
- }
185
-
186
- // 2. Check Snapshots
187
- if (r.revision_type === 'snapshot' && currentContent && r.version !== currentVersion) {
188
- let snapshotContent = r.content;
189
- if (typeof snapshotContent === 'string') {
190
- try { snapshotContent = JSON.parse(snapshotContent); } catch { /* default true */ }
191
- }
192
-
193
- const ops = compare(currentContent, snapshotContent);
194
-
195
- const ignoredSuffixes = ['/updated_at', '/created_at', '/last_modified', '/modified_at', '/version', '/id', '/published_at', '/author_id', '/variant'];
196
- const usefulOps = ops.filter((op: any) => {
197
- if (!op.path) return true;
198
- return !ignoredSuffixes.some(suffix => op.path.endsWith(suffix));
199
- });
200
-
201
- if (usefulOps.length === 0) has_changes = false;
202
- }
203
-
204
- return { ...r, has_changes };
205
- }) as (RevisionListItem & { has_changes: boolean; content: any })[];
206
-
207
- // Synthetic V1
208
- const hasVersion1 = allRevisions.some(r => r.version === 1);
209
- const postCreatedAt = postRow?.created_at;
210
-
211
- let matchesDate = true;
212
- if (postCreatedAt) {
213
- if (startDate && postCreatedAt < `${startDate}T00:00:00.000Z`) matchesDate = false;
214
- if (endDate && postCreatedAt > `${endDate}T23:59:59.999Z`) matchesDate = false;
215
- }
216
-
217
- if (!hasVersion1 && (currentVersion ?? 0) > 1 && matchesDate) {
218
- allRevisions.push({
219
- id: -1,
220
- version: 1,
221
- revision_type: 'snapshot',
222
- created_at: postCreatedAt ?? new Date().toISOString(),
223
- author_id: null,
224
- author: { full_name: 'System (Initial)', username: 'system' },
225
- has_changes: true,
226
- content: null
227
- });
228
- }
229
-
230
- allRevisions.sort((a, b) => b.version - a.version);
231
-
232
- const totalFiltered = allRevisions.length;
233
- const totalPages = Math.ceil(totalFiltered / REVISIONS_PER_PAGE);
234
-
235
- const fromIndex = (page - 1) * REVISIONS_PER_PAGE;
236
- // Use map to strip heavy content if not needed, but keep flags
237
- const slicedRevisions = allRevisions
238
- .slice(fromIndex, fromIndex + REVISIONS_PER_PAGE)
239
- .map(({ content, ...rest }) => rest);
240
-
241
- return { success: true as const, revisions: slicedRevisions, currentVersion, count: totalFiltered, totalPages, hasMore: (page * REVISIONS_PER_PAGE) < totalFiltered };
242
- }
243
-
244
- export async function restorePageVersion(pageId: number, targetVersion: number) {
245
- const supabase = createClient();
246
- const { data: { user } } = await supabase.auth.getUser();
247
- if (!user) return { error: 'User not authenticated.' } as const;
248
- // Role checks are enforced by RLS; we can still short-circuit if needed
249
- return await restorePageToVersion(pageId, targetVersion, user.id);
250
- }
251
-
252
- export async function restorePostVersion(postId: number, targetVersion: number) {
253
- const supabase = createClient();
254
- const { data: { user } } = await supabase.auth.getUser();
255
- if (!user) return { error: 'User not authenticated.' } as const;
256
- return await restorePostToVersion(postId, targetVersion, user.id);
257
- }
258
-
259
- import type { FullPageContent, FullPostContent } from './utils';
260
-
261
- type CompareResponse<T> = { success: true; current: T; target: T } | { error: string };
262
-
263
- export async function comparePageVersion(pageId: number, targetVersion: number): Promise<CompareResponse<FullPageContent>> {
264
- const supabase = createClient();
265
- const { data: { user } } = await supabase.auth.getUser();
266
- if (!user) return { error: 'User not authenticated.' } as const;
267
-
268
- const current = await getFullPageContent(pageId);
269
- if (!current) return { error: 'Failed to fetch current content.' } as const;
270
- const reconstructed = await reconstructPageVersionContent(pageId, targetVersion);
271
- if ('error' in reconstructed) return { error: reconstructed.error ?? 'Unknown error occurred while reconstructing page version' } as const;
272
- return { success: true as const, current, target: reconstructed.content };
273
- }
274
-
275
- export async function comparePostVersion(postId: number, targetVersion: number): Promise<CompareResponse<FullPostContent>> {
276
- const supabase = createClient();
277
- const { data: { user } } = await supabase.auth.getUser();
278
- if (!user) return { error: 'User not authenticated.' } as const;
279
-
280
- const current = await getFullPostContent(postId);
281
- if (!current) return { error: 'Failed to fetch current content.' } as const;
282
- const reconstructed = await reconstructPostVersionContent(postId, targetVersion);
283
- if ('error' in reconstructed) return { error: reconstructed.error ?? 'Unknown error occurred while reconstructing post version' } as const;
284
- return { success: true as const, current, target: reconstructed.content };
285
- }
1
+ // apps/nextblock/app/cms/revisions/actions.ts
2
+ "use server";
3
+
4
+ import { createClient } from "@nextblock-cms/db/server";
5
+ import { restorePageToVersion, restorePostToVersion, reconstructPageVersionContent, reconstructPostVersionContent } from './service';
6
+ import { getFullPageContent, getFullPostContent } from './utils';
7
+ import { compare } from 'fast-json-patch';
8
+
9
+ type RevisionListItem = {
10
+ id: number;
11
+ version: number;
12
+ revision_type: 'snapshot' | 'diff';
13
+ created_at: string;
14
+ author_id: string | null;
15
+ author?: { full_name?: string | null; github_username?: string | null } | null;
16
+ };
17
+
18
+ const REVISIONS_PER_PAGE = 10; // Reduced to 10 as requested
19
+
20
+ export async function listPageRevisions(pageId: number, page = 1, startDate?: string, endDate?: string) {
21
+ const supabase = createClient();
22
+
23
+ let query = supabase
24
+ .from('page_revisions')
25
+ .select('id, page_id, author_id, version, revision_type, created_at, content, author:profiles(full_name, github_username)', { count: 'exact' })
26
+ .eq('page_id', pageId)
27
+ .order('version', { ascending: false })
28
+ .limit(1000); // Fetch up to 1000 recent revisions to allow dense pagination after filtering
29
+
30
+ if (startDate) {
31
+ query = query.gte('created_at', `${startDate}T00:00:00.000Z`);
32
+ }
33
+ if (endDate) {
34
+ query = query.lte('created_at', `${endDate}T23:59:59.999Z`);
35
+ }
36
+
37
+ const { data, error } = await query; // count is less useful now as we filter
38
+
39
+ if (error) return { error: error.message } as const;
40
+
41
+ const { data: pageRow } = await supabase.from('pages').select('version, created_at').eq('id', pageId).single();
42
+ const currentVersion = pageRow?.version ?? null;
43
+ const currentContent = await getFullPageContent(pageId);
44
+
45
+ // Map data to include useful flags instead of filtering
46
+ const allRevisions = (data || []).map((r: any) => {
47
+ let has_changes = true;
48
+
49
+ // 1. Check Diffs
50
+ if (r.revision_type === 'diff') {
51
+ let ops = r.content;
52
+ if (typeof ops === 'string') {
53
+ try { ops = JSON.parse(ops); } catch { ops = []; }
54
+ }
55
+
56
+ // Strict check: Diff must be a non-empty array of operations
57
+ if (!Array.isArray(ops) || ops.length === 0) {
58
+ has_changes = false;
59
+ } else {
60
+ // Check metadata/variant
61
+ const ignoredSuffixes = ['/updated_at', '/created_at', '/last_modified', '/modified_at', '/version', '/id', '/published_at', '/author_id', '/variant'];
62
+ const usefulOps = ops.filter((op: any) => {
63
+ if (!op.path) return true;
64
+ return !ignoredSuffixes.some(suffix => op.path.endsWith(suffix));
65
+ });
66
+ if (usefulOps.length === 0) has_changes = false;
67
+ }
68
+ }
69
+
70
+ // 2. Check Snapshots
71
+ if (r.revision_type === 'snapshot' && currentContent && r.version !== currentVersion) {
72
+ let snapshotContent = r.content;
73
+ if (typeof snapshotContent === 'string') {
74
+ try { snapshotContent = JSON.parse(snapshotContent); } catch { /* default true */ }
75
+ }
76
+
77
+ const ops = compare(currentContent, snapshotContent);
78
+
79
+ const ignoredSuffixes = ['/updated_at', '/created_at', '/last_modified', '/modified_at', '/version', '/id', '/published_at', '/author_id', '/variant'];
80
+ const usefulOps = ops.filter((op: any) => {
81
+ if (!op.path) return true;
82
+ return !ignoredSuffixes.some(suffix => op.path.endsWith(suffix));
83
+ });
84
+
85
+ if (usefulOps.length === 0) has_changes = false;
86
+ }
87
+
88
+ return { ...r, has_changes };
89
+ }) as (RevisionListItem & { has_changes: boolean; content: any })[];
90
+
91
+ // Append Synthetic V1
92
+ const hasVersion1 = allRevisions.some(r => r.version === 1);
93
+ const pageCreatedAt = pageRow?.created_at;
94
+
95
+ let matchesDate = true;
96
+ if (pageCreatedAt) {
97
+ if (startDate && pageCreatedAt < `${startDate}T00:00:00.000Z`) matchesDate = false;
98
+ if (endDate && pageCreatedAt > `${endDate}T23:59:59.999Z`) matchesDate = false;
99
+ }
100
+
101
+ if (!hasVersion1 && (currentVersion ?? 0) > 1 && matchesDate) {
102
+ allRevisions.push({
103
+ id: -1,
104
+ version: 1,
105
+ revision_type: 'snapshot',
106
+ created_at: pageCreatedAt ?? new Date().toISOString(),
107
+ author_id: null,
108
+ author: { full_name: 'System (Initial)', github_username: 'system' },
109
+ has_changes: true, // V1 always counts
110
+ content: null
111
+ });
112
+ }
113
+
114
+ allRevisions.sort((a, b) => b.version - a.version);
115
+
116
+ const totalFiltered = allRevisions.length;
117
+ const totalPages = Math.ceil(totalFiltered / REVISIONS_PER_PAGE);
118
+
119
+ const fromIndex = (page - 1) * REVISIONS_PER_PAGE;
120
+ // Use map to strip heavy content if not needed, but keep flags
121
+ const slicedRevisions = allRevisions
122
+ .slice(fromIndex, fromIndex + REVISIONS_PER_PAGE)
123
+ .map(({ content, ...rest }) => rest);
124
+
125
+ return {
126
+ success: true as const,
127
+ revisions: slicedRevisions,
128
+ currentVersion,
129
+ count: totalFiltered,
130
+ totalPages,
131
+ hasMore: (page * REVISIONS_PER_PAGE) < totalFiltered
132
+ };
133
+ }
134
+
135
+ export async function listPostRevisions(postId: number, page = 1, startDate?: string, endDate?: string) {
136
+ const supabase = createClient();
137
+
138
+
139
+
140
+ let query = supabase
141
+ .from('post_revisions')
142
+ .select('id, post_id, author_id, version, revision_type, created_at, content, author:profiles(full_name, github_username)', { count: 'exact' })
143
+ .eq('post_id', postId)
144
+ .order('version', { ascending: false })
145
+ .limit(1000);
146
+
147
+ if (startDate) {
148
+ query = query.gte('created_at', `${startDate}T00:00:00.000Z`);
149
+ }
150
+ if (endDate) {
151
+ query = query.lte('created_at', `${endDate}T23:59:59.999Z`);
152
+ }
153
+
154
+ const { data, error } = await query;
155
+
156
+ if (error) return { error: error.message } as const;
157
+
158
+ const { data: postRow } = await supabase.from('posts').select('version, created_at').eq('id', postId).single();
159
+ const currentVersion = postRow?.version ?? null;
160
+ const currentContent = await getFullPostContent(postId);
161
+
162
+ const allRevisions = (data || []).map((r: any) => {
163
+ let has_changes = true;
164
+
165
+ // 1. Check Diffs
166
+ if (r.revision_type === 'diff') {
167
+ let ops = r.content;
168
+ if (typeof ops === 'string') {
169
+ try { ops = JSON.parse(ops); } catch { ops = []; }
170
+ }
171
+
172
+ // Strict check: Diff must be a non-empty array of operations
173
+ if (!Array.isArray(ops) || ops.length === 0) {
174
+ has_changes = false;
175
+ } else {
176
+ // Check metadata/variant
177
+ const ignoredSuffixes = ['/updated_at', '/created_at', '/last_modified', '/modified_at', '/version', '/id', '/published_at', '/author_id', '/variant'];
178
+ const usefulOps = ops.filter((op: any) => {
179
+ if (!op.path) return true;
180
+ return !ignoredSuffixes.some(suffix => op.path.endsWith(suffix));
181
+ });
182
+ if (usefulOps.length === 0) has_changes = false;
183
+ }
184
+ }
185
+
186
+ // 2. Check Snapshots
187
+ if (r.revision_type === 'snapshot' && currentContent && r.version !== currentVersion) {
188
+ let snapshotContent = r.content;
189
+ if (typeof snapshotContent === 'string') {
190
+ try { snapshotContent = JSON.parse(snapshotContent); } catch { /* default true */ }
191
+ }
192
+
193
+ const ops = compare(currentContent, snapshotContent);
194
+
195
+ const ignoredSuffixes = ['/updated_at', '/created_at', '/last_modified', '/modified_at', '/version', '/id', '/published_at', '/author_id', '/variant'];
196
+ const usefulOps = ops.filter((op: any) => {
197
+ if (!op.path) return true;
198
+ return !ignoredSuffixes.some(suffix => op.path.endsWith(suffix));
199
+ });
200
+
201
+ if (usefulOps.length === 0) has_changes = false;
202
+ }
203
+
204
+ return { ...r, has_changes };
205
+ }) as (RevisionListItem & { has_changes: boolean; content: any })[];
206
+
207
+ // Synthetic V1
208
+ const hasVersion1 = allRevisions.some(r => r.version === 1);
209
+ const postCreatedAt = postRow?.created_at;
210
+
211
+ let matchesDate = true;
212
+ if (postCreatedAt) {
213
+ if (startDate && postCreatedAt < `${startDate}T00:00:00.000Z`) matchesDate = false;
214
+ if (endDate && postCreatedAt > `${endDate}T23:59:59.999Z`) matchesDate = false;
215
+ }
216
+
217
+ if (!hasVersion1 && (currentVersion ?? 0) > 1 && matchesDate) {
218
+ allRevisions.push({
219
+ id: -1,
220
+ version: 1,
221
+ revision_type: 'snapshot',
222
+ created_at: postCreatedAt ?? new Date().toISOString(),
223
+ author_id: null,
224
+ author: { full_name: 'System (Initial)', github_username: 'system' },
225
+ has_changes: true,
226
+ content: null
227
+ });
228
+ }
229
+
230
+ allRevisions.sort((a, b) => b.version - a.version);
231
+
232
+ const totalFiltered = allRevisions.length;
233
+ const totalPages = Math.ceil(totalFiltered / REVISIONS_PER_PAGE);
234
+
235
+ const fromIndex = (page - 1) * REVISIONS_PER_PAGE;
236
+ // Use map to strip heavy content if not needed, but keep flags
237
+ const slicedRevisions = allRevisions
238
+ .slice(fromIndex, fromIndex + REVISIONS_PER_PAGE)
239
+ .map(({ content, ...rest }) => rest);
240
+
241
+ return { success: true as const, revisions: slicedRevisions, currentVersion, count: totalFiltered, totalPages, hasMore: (page * REVISIONS_PER_PAGE) < totalFiltered };
242
+ }
243
+
244
+ export async function restorePageVersion(pageId: number, targetVersion: number) {
245
+ const supabase = createClient();
246
+ const { data: { user } } = await supabase.auth.getUser();
247
+ if (!user) return { error: 'User not authenticated.' } as const;
248
+ // Role checks are enforced by RLS; we can still short-circuit if needed
249
+ return await restorePageToVersion(pageId, targetVersion, user.id);
250
+ }
251
+
252
+ export async function restorePostVersion(postId: number, targetVersion: number) {
253
+ const supabase = createClient();
254
+ const { data: { user } } = await supabase.auth.getUser();
255
+ if (!user) return { error: 'User not authenticated.' } as const;
256
+ return await restorePostToVersion(postId, targetVersion, user.id);
257
+ }
258
+
259
+ import type { FullPageContent, FullPostContent } from './utils';
260
+
261
+ type CompareResponse<T> = { success: true; current: T; target: T } | { error: string };
262
+
263
+ export async function comparePageVersion(pageId: number, targetVersion: number): Promise<CompareResponse<FullPageContent>> {
264
+ const supabase = createClient();
265
+ const { data: { user } } = await supabase.auth.getUser();
266
+ if (!user) return { error: 'User not authenticated.' } as const;
267
+
268
+ const current = await getFullPageContent(pageId);
269
+ if (!current) return { error: 'Failed to fetch current content.' } as const;
270
+ const reconstructed = await reconstructPageVersionContent(pageId, targetVersion);
271
+ if ('error' in reconstructed) return { error: reconstructed.error ?? 'Unknown error occurred while reconstructing page version' } as const;
272
+ return { success: true as const, current, target: reconstructed.content };
273
+ }
274
+
275
+ export async function comparePostVersion(postId: number, targetVersion: number): Promise<CompareResponse<FullPostContent>> {
276
+ const supabase = createClient();
277
+ const { data: { user } } = await supabase.auth.getUser();
278
+ if (!user) return { error: 'User not authenticated.' } as const;
279
+
280
+ const current = await getFullPostContent(postId);
281
+ if (!current) return { error: 'Failed to fetch current content.' } as const;
282
+ const reconstructed = await reconstructPostVersionContent(postId, targetVersion);
283
+ if ('error' in reconstructed) return { error: reconstructed.error ?? 'Unknown error occurred while reconstructing post version' } as const;
284
+ return { success: true as const, current, target: reconstructed.content };
285
+ }
@@ -150,7 +150,7 @@ export async function restorePageToVersion(pageId: number, targetVersion: number
150
150
  // Fallback for missing Version 1: use empty content with current meta
151
151
  const { data: pageMeta } = await supabase
152
152
  .from('pages')
153
- .select('title, slug, language_id, status, meta_title, meta_description')
153
+ .select('title, slug, language_id, status, meta_title, meta_description, feature_image_id')
154
154
  .eq('id', pageId)
155
155
  .single();
156
156
  if (!pageMeta) return { error: 'Page not found.' } as const;
@@ -201,10 +201,11 @@ export async function restorePageToVersion(pageId: number, targetVersion: number
201
201
  title: content.meta.title,
202
202
  slug: content.meta.slug,
203
203
  language_id: content.meta.language_id,
204
- status: content.meta.status,
205
- meta_title: content.meta.meta_title,
206
- meta_description: content.meta.meta_description,
207
- version: newVersion,
204
+ status: content.meta.status,
205
+ meta_title: content.meta.meta_title,
206
+ meta_description: content.meta.meta_description,
207
+ feature_image_id: content.meta.feature_image_id,
208
+ version: newVersion,
208
209
  })
209
210
  .eq('id', pageId);
210
211
  if (updatePageError) return { error: `Failed to update page: ${updatePageError.message}` } as const;
@@ -261,7 +262,7 @@ export async function restorePostToVersion(postId: number, targetVersion: number
261
262
  } else if (targetVersion === 1) {
262
263
  const { data: postMeta } = await supabase
263
264
  .from('posts')
264
- .select('title, slug, language_id, status, meta_title, meta_description, excerpt, published_at, feature_image_id')
265
+ .select('title, slug, language_id, status, meta_title, meta_description, label, excerpt, subtitle, published_at, feature_image_id')
265
266
  .eq('id', postId)
266
267
  .single();
267
268
  if (!postMeta) return { error: 'Post not found.' } as const;
@@ -309,14 +310,16 @@ export async function restorePostToVersion(postId: number, targetVersion: number
309
310
  .update({
310
311
  title: content.meta.title,
311
312
  slug: content.meta.slug,
312
- language_id: content.meta.language_id,
313
- status: content.meta.status,
314
- meta_title: content.meta.meta_title,
315
- meta_description: content.meta.meta_description,
316
- excerpt: content.meta.excerpt,
317
- published_at: content.meta.published_at,
318
- feature_image_id: content.meta.feature_image_id,
319
- version: newVersion,
313
+ language_id: content.meta.language_id,
314
+ status: content.meta.status,
315
+ meta_title: content.meta.meta_title,
316
+ meta_description: content.meta.meta_description,
317
+ label: content.meta.label,
318
+ excerpt: content.meta.excerpt,
319
+ subtitle: content.meta.subtitle,
320
+ published_at: content.meta.published_at,
321
+ feature_image_id: content.meta.feature_image_id,
322
+ version: newVersion,
320
323
  })
321
324
  .eq('id', postId);
322
325
  if (updatePostError) return { error: `Failed to update post: ${updatePostError.message}` } as const;
@@ -371,7 +374,7 @@ export async function reconstructPageVersionContent(pageId: number, targetVersio
371
374
  } else if (targetVersion === 1) {
372
375
  const { data: pageMeta } = await supabase
373
376
  .from('pages')
374
- .select('title, slug, language_id, status, meta_title, meta_description')
377
+ .select('title, slug, language_id, status, meta_title, meta_description, feature_image_id')
375
378
  .eq('id', pageId)
376
379
  .single();
377
380
  if (!pageMeta) return { error: 'Page not found.' } as const;
@@ -430,7 +433,7 @@ export async function reconstructPostVersionContent(postId: number, targetVersio
430
433
  } else if (targetVersion === 1) {
431
434
  const { data: postMeta } = await supabase
432
435
  .from('posts')
433
- .select('title, slug, language_id, status, meta_title, meta_description, excerpt, published_at, feature_image_id')
436
+ .select('title, slug, language_id, status, meta_title, meta_description, label, excerpt, subtitle, published_at, feature_image_id')
434
437
  .eq('id', postId)
435
438
  .single();
436
439
  if (!postMeta) return { error: 'Post not found.' } as const;