create-nextblock 0.2.78 → 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 +182 -154
  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 -120
  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
@@ -2,23 +2,142 @@
2
2
  "use server";
3
3
 
4
4
  import { sendEmail } from './email';
5
+ import { getServiceRoleSupabaseClient } from '@nextblock-cms/db/server';
5
6
 
6
- interface FormSubmissionResult {
7
- success: boolean;
8
- message: string;
9
- }
7
+ interface FormSubmissionResult {
8
+ success: boolean;
9
+ message: string;
10
+ }
11
+
12
+ type BotProtectionProvider = 'none' | 'turnstile' | 'recaptcha';
13
+
14
+ type FormSubmissionConfig = {
15
+ recipient: string;
16
+ botProtectionProvider?: BotProtectionProvider;
17
+ };
18
+
19
+ function normalizeSubmissionConfig(config: string | FormSubmissionConfig) {
20
+ if (typeof config === 'string') {
21
+ return {
22
+ recipient: config,
23
+ botProtectionProvider: undefined,
24
+ };
25
+ }
26
+
27
+ return config;
28
+ }
29
+
30
+ export async function handleFormSubmission(
31
+ config: string | FormSubmissionConfig,
32
+ prevState: unknown,
33
+ formData: FormData
34
+ ): Promise<FormSubmissionResult> {
35
+ const { recipient, botProtectionProvider } = normalizeSubmissionConfig(config);
36
+
37
+ // Phase 1: Honeypot Validation
38
+ const honeypot = formData.get('verification_secondary_email');
39
+ if (honeypot && typeof honeypot === 'string' && honeypot.length > 0) {
40
+ console.warn("[Bot Protection] Honeypot triggered. Discarding submission from bot.");
41
+ // Fool the bot by returning a fake success response immediately
42
+ return { success: true, message: "Submission successful!" };
43
+ }
44
+
45
+ // Phase 2: Advanced Captcha Verification
46
+ try {
47
+ const supabase = getServiceRoleSupabaseClient();
48
+
49
+ // Fetch global bot protection settings
50
+ const { data: publicSetting } = await supabase
51
+ .from('site_settings')
52
+ .select('value')
53
+ .eq('key', 'bot_protection_public')
54
+ .maybeSingle();
55
+
56
+ const { data: secretSetting } = await supabase
57
+ .from('site_settings')
58
+ .select('value')
59
+ .eq('key', 'bot_protection_secret')
60
+ .maybeSingle();
61
+
62
+ const publicVal = (publicSetting?.value || {}) as Record<string, any>;
63
+ const secretVal = (secretSetting?.value || {}) as Record<string, any>;
64
+
65
+ const blockProvider =
66
+ botProtectionProvider === 'turnstile' || botProtectionProvider === 'recaptcha'
67
+ ? botProtectionProvider
68
+ : undefined;
69
+ const provider = blockProvider || publicVal.provider || 'none';
70
+ const secretKey = secretVal.secretKey ||
71
+ (provider === 'turnstile' ? process.env.TURNSTILE_SECRET_KEY : process.env.RECAPTCHA_SECRET_KEY) ||
72
+ '';
73
+
74
+ if (provider === 'turnstile') {
75
+ const token = formData.get('cf-turnstile-response') as string;
76
+ if (!token) {
77
+ return { success: false, message: "Security verification token is missing. Please try again." };
78
+ }
79
+ if (!secretKey) {
80
+ console.error("[Bot Protection] Turnstile secret key is not configured.");
81
+ return { success: false, message: "Bot protection is misconfigured. Please contact support." };
82
+ }
83
+
84
+ const res = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', {
85
+ method: 'POST',
86
+ headers: {
87
+ 'Content-Type': 'application/x-www-form-urlencoded',
88
+ },
89
+ body: `secret=${encodeURIComponent(secretKey)}&response=${encodeURIComponent(token)}`,
90
+ });
10
91
 
11
- export async function handleFormSubmission(
12
- recipient: string,
13
- prevState: unknown,
14
- formData: FormData
15
- ): Promise<FormSubmissionResult> {
92
+ const outcome = await res.json();
93
+ if (!outcome.success) {
94
+ console.warn("[Bot Protection] Turnstile verification failed:", outcome);
95
+ return { success: false, message: "Security verification failed. Please try again." };
96
+ }
97
+ } else if (provider === 'recaptcha') {
98
+ const token = formData.get('g-recaptcha-response') as string;
99
+ if (!token) {
100
+ return { success: false, message: "Security verification token is missing. Please try again." };
101
+ }
102
+ if (!secretKey) {
103
+ console.error("[Bot Protection] reCAPTCHA secret key is not configured.");
104
+ return { success: false, message: "Bot protection is misconfigured. Please contact support." };
105
+ }
106
+
107
+ const res = await fetch('https://www.google.com/recaptcha/api/siteverify', {
108
+ method: 'POST',
109
+ headers: {
110
+ 'Content-Type': 'application/x-www-form-urlencoded',
111
+ },
112
+ body: `secret=${encodeURIComponent(secretKey)}&response=${encodeURIComponent(token)}`,
113
+ });
114
+
115
+ const outcome = await res.json();
116
+ if (!outcome.success || outcome.score < 0.5) {
117
+ console.warn("[Bot Protection] reCAPTCHA verification failed:", outcome);
118
+ return { success: false, message: "Security verification failed. Please try again." };
119
+ }
120
+ }
121
+ } catch (error) {
122
+ console.error("[Bot Protection] Error during validation:", error);
123
+ // If database or fetch error occurs, we gracefully degrade or warn, but let's be secure and fail open/closed depending on preference.
124
+ // The requirement says: "If the API indicates a verification failure or falls below a threshold... reject the operation securely."
125
+ // Let's return error message.
126
+ return { success: false, message: "Sorry, security verification could not be completed at this time." };
127
+ }
16
128
 
17
129
  const data: Record<string, string | File> = {};
18
130
  let submitterEmail = 'a user'; // Default value
19
131
 
20
132
  formData.forEach((value, key) => {
21
- if (typeof value === 'string' && !key.startsWith('$')) {
133
+ // Avoid sending internal bot protection tokens and honeypots in the notification email
134
+ if (
135
+ typeof value === 'string' &&
136
+ !key.startsWith('$') &&
137
+ key !== 'verification_secondary_email' &&
138
+ key !== 'g-recaptcha-response' &&
139
+ key !== 'cf-turnstile-response'
140
+ ) {
22
141
  data[key] = value;
23
142
  // Attempt to find a field that looks like an email address to use in the subject
24
143
  if (key.toLowerCase().includes('email')) {
@@ -62,4 +181,4 @@ export async function handleFormSubmission(
62
181
  console.error("Email sending failed:", error);
63
182
  return { success: false, message: "Sorry, there was an error sending your message. Please try again later." };
64
183
  }
65
- }
184
+ }
@@ -3,7 +3,7 @@
3
3
  import { createClient } from '@nextblock-cms/db';
4
4
  import { cookies } from 'next/headers';
5
5
  import { redirect } from 'next/navigation';
6
- import { getLanguageByCode } from '@/app/cms/settings/languages/actions';
6
+ import { getLanguageByCode } from '../cms/settings/languages/actions';
7
7
 
8
8
  export interface Language {
9
9
  id: number;
@@ -33,43 +33,37 @@ export async function setCurrentLocaleCookie(locale: string) {
33
33
  cookieStore.set('NEXT_LOCALE', locale, { path: '/' });
34
34
  }
35
35
 
36
- export async function getPageTranslations(translationGroupId: string): Promise<{ slug: string, language_code: string }[]> {
36
+ export async function getContentTranslations(translationGroupId: string, type: 'pages' | 'posts' | 'products' = 'pages'): Promise<{ slug: string, language_code: string }[]> {
37
37
  if (!translationGroupId) {
38
- console.warn('getPageTranslations called without translationGroupId');
38
+ console.warn('getContentTranslations called without translationGroupId');
39
39
  return [];
40
40
  }
41
41
  const supabase = createClient();
42
42
 
43
43
  const { data, error } = await supabase
44
- .from('pages')
45
- .select('slug, status, languages(code)') // Use actual table name for join
44
+ .from(type)
45
+ .select('slug, languages(code)')
46
46
  .eq('translation_group_id', translationGroupId)
47
- .eq('status', 'published');
47
+ .eq('status', type === 'products' ? 'active' : 'published'); // Products use 'active' or 'draft' status usually, but let's check
48
48
 
49
49
  if (error) {
50
- console.error('Error fetching page translations:', error);
50
+ console.error(`Error fetching translations for ${type}:`, error);
51
51
  return [];
52
52
  }
53
53
 
54
- interface PageWithLanguage {
55
- slug: string;
56
- status: string; // Or your actual status type
57
- languages: { code: string } | { code: string }[] | null; // Can be object, array of objects, or null
58
- }
59
-
60
54
  // Map the data to the expected format { slug: string, language_code: string }
61
55
  const formattedTranslations = data
62
- ? (data as PageWithLanguage[]).map(page => {
56
+ ? (data as any[]).map(item => {
63
57
  let langCode = '';
64
- if (page.languages) {
65
- if (Array.isArray(page.languages)) {
66
- langCode = page.languages[0]?.code || '';
67
- } else { // It's an object
68
- langCode = page.languages.code || '';
58
+ if (item.languages) {
59
+ if (Array.isArray(item.languages)) {
60
+ langCode = item.languages[0]?.code || '';
61
+ } else {
62
+ langCode = item.languages.code || '';
69
63
  }
70
64
  }
71
65
  return {
72
- slug: page.slug,
66
+ slug: item.slug,
73
67
  language_code: langCode,
74
68
  };
75
69
  }).filter(t => t.language_code)
@@ -78,6 +72,10 @@ export async function getPageTranslations(translationGroupId: string): Promise<{
78
72
  return formattedTranslations;
79
73
  }
80
74
 
75
+ export async function getPageTranslations(translationGroupId: string): Promise<{ slug: string, language_code: string }[]> {
76
+ return getContentTranslations(translationGroupId, 'pages');
77
+ }
78
+
81
79
  // Helper to get language details by code, potentially used by LanguageSwitcher or other components
82
80
  export async function getLanguageDetails(localeCode: string): Promise<Language | null> {
83
81
  const { data, error } = await getLanguageByCode(localeCode);
@@ -89,9 +87,9 @@ export async function getLanguageDetails(localeCode: string): Promise<Language |
89
87
  return data;
90
88
  }
91
89
 
92
- export async function getPageMetadataBySlugAndLocale(slug: string, localeCode: string): Promise<{ slug: string; translation_group_id: string | null } | null> {
90
+ export async function getContentMetadataBySlugAndLocale(slug: string, localeCode: string, type: 'pages' | 'posts' | 'products' = 'pages'): Promise<{ slug: string; translation_group_id: string | null; type: string } | null> {
93
91
  if (!slug || !localeCode) {
94
- console.warn('getPageMetadataBySlugAndLocale called without slug or localeCode');
92
+ console.warn('getContentMetadataBySlugAndLocale called without slug or localeCode');
95
93
  return null;
96
94
  }
97
95
  const supabase = createClient();
@@ -102,24 +100,27 @@ export async function getPageMetadataBySlugAndLocale(slug: string, localeCode: s
102
100
  return null;
103
101
  }
104
102
 
105
- const { data: page, error } = await supabase
106
- .from('pages')
103
+ const { data: item, error } = await supabase
104
+ .from(type)
107
105
  .select('slug, translation_group_id')
108
106
  .eq('slug', slug)
109
107
  .eq('language_id', languageData.id)
110
108
  .maybeSingle();
111
109
 
112
110
  if (error) {
113
- console.error(`Error fetching page metadata for slug ${slug} and locale ${localeCode}:`, error);
111
+ console.error(`Error fetching metadata for ${type} slug ${slug} and locale ${localeCode}:`, error);
114
112
  return null;
115
113
  }
116
- if (!page) {
117
- // It's possible the slug is for a different content type (e.g. blog post) or doesn't exist.
118
- // For now, we only search 'pages'. This might need to be expanded or handled gracefully.
119
- console.warn(`No page found for slug ${slug} and locale ${localeCode}`);
114
+ if (!item) {
120
115
  return null;
121
116
  }
122
- return page;
117
+ return { ...item, type };
118
+ }
119
+
120
+ export async function getPageMetadataBySlugAndLocale(slug: string, localeCode: string): Promise<{ slug: string; translation_group_id: string | null } | null> {
121
+ const metadata = await getContentMetadataBySlugAndLocale(slug, localeCode, 'pages');
122
+ if (!metadata) return null;
123
+ return { slug: metadata.slug, translation_group_id: metadata.translation_group_id };
123
124
  }
124
125
 
125
126
  export async function changeLanguage(newLocale: string, currentPath: string) {
@@ -0,0 +1,183 @@
1
+ 'use server';
2
+
3
+ import { createClient } from '@supabase/supabase-js';
4
+ import { NEXTBLOCK_PACKAGES } from '@nextblock-cms/utils';
5
+ import { headers } from 'next/headers';
6
+ import { revalidatePath } from 'next/cache';
7
+
8
+ // Freemius handles both Sandbox and Production keys on the same API domain.
9
+ // The key itself determines the environment.
10
+ const FM_API_URL = 'https://api.freemius.com/v1';
11
+
12
+ // Helper to get service role client
13
+ const getServiceRoleClient = () => {
14
+ const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
15
+ const supabaseServiceKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
16
+
17
+ if (!supabaseUrl || !supabaseServiceKey) {
18
+ console.error('Missing Supabase credentials');
19
+ throw new Error('Missing Supabase credentials (Service Key required for activation).');
20
+ }
21
+
22
+ if (supabaseServiceKey === process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY) {
23
+ console.warn('CRITICAL WARNING: SUPABASE_SERVICE_ROLE_KEY matches NEXT_PUBLIC_SUPABASE_ANON_KEY. This will likely cause Permission Denied errors as RLS cannot be bypassed.');
24
+ }
25
+
26
+ return createClient(supabaseUrl, supabaseServiceKey, {
27
+ auth: {
28
+ persistSession: false,
29
+ autoRefreshToken: false,
30
+ detectSessionInUrl: false,
31
+ },
32
+ });
33
+ };
34
+
35
+ export async function activatePackage(key: string) {
36
+ if (process.env.NEXT_PUBLIC_IS_SANDBOX === 'true') {
37
+ return { error: 'License activation is disabled in Sandbox mode. To purchase a real license, visit nextblock.ca' };
38
+ }
39
+
40
+ if (!key) {
41
+ return { error: 'License key is required.' };
42
+ }
43
+
44
+ const headerList = await headers();
45
+ // instance_name is usually the domain, for local dev use 'localhost' or actual host
46
+ const instanceName = headerList.get('host') || 'nextblock-instance';
47
+
48
+ // Freemius requires a 32-char unique identifier for the install.
49
+ // We hash the instance (domain) to ensure reactivations on the same domain use the same UID.
50
+ const crypto = require('crypto');
51
+ const uid = crypto.createHash('md5').update(instanceName).digest('hex');
52
+
53
+ try {
54
+ let data = null;
55
+ let pkg = null;
56
+ let fmProductId = null;
57
+ let hasLicenseError = false;
58
+ let specificErrorMsg: string | null = null;
59
+
60
+ // We don't know the exact package just from the license key, so we try activating
61
+ // against our known Freemius Product IDs from the NEXTBLOCK_PACKAGES registry.
62
+ const packages = Object.values(NEXTBLOCK_PACKAGES);
63
+
64
+ for (const p of packages) {
65
+ if (!p.fm_product_id) continue;
66
+
67
+ const siteUrl = encodeURIComponent(`http://${instanceName}`);
68
+ const response = await fetch(`${FM_API_URL}/products/${p.fm_product_id}/licenses/activate.json?uid=${uid}&license_key=${encodeURIComponent(key)}&url=${siteUrl}`, {
69
+ method: 'POST',
70
+ headers: {
71
+ 'Accept': 'application/json',
72
+ 'Content-Type': 'application/json',
73
+ }
74
+ });
75
+
76
+ const responseData = await response.json();
77
+
78
+ // Freemius returns the license object directly if successful, or an error/api_response
79
+ if (response.ok && responseData.install_id) {
80
+ data = responseData;
81
+ pkg = p;
82
+ fmProductId = p.fm_product_id;
83
+ break;
84
+ }
85
+
86
+ const errorCode = responseData?.error?.code;
87
+ if (errorCode === 'not_found' || errorCode === 'invalid_license_key') {
88
+ hasLicenseError = true;
89
+ } else if (responseData?.error?.message) {
90
+ specificErrorMsg = responseData.error.message;
91
+ }
92
+ }
93
+
94
+ if (!data || !pkg) {
95
+ if (hasLicenseError && process.env.NEXT_PUBLIC_IS_SANDBOX !== 'true' && !specificErrorMsg) {
96
+ return { error: 'Sorry, this is a sandbox key. Please purchase the real key at nextblock.ca' };
97
+ }
98
+ return { error: specificErrorMsg || 'Activation failed. Invalid key, wrong product, or limit reached.' };
99
+ }
100
+
101
+ // 3. Store in DB - USE SERVICE ROLE
102
+ const supabase = getServiceRoleClient();
103
+
104
+ const { error: dbError } = await supabase
105
+ .from('package_activations')
106
+ .upsert({
107
+ license_key: key,
108
+ instance_name: instanceName,
109
+ package_id: pkg.id,
110
+ status: 'active',
111
+ meta: {
112
+ ...data,
113
+ fm_product_id: fmProductId,
114
+ fm_install_id: data.install_id,
115
+ fm_uid: uid
116
+ },
117
+ last_validated_at: new Date().toISOString(),
118
+ }, { onConflict: 'license_key, package_id' });
119
+
120
+ if (dbError) {
121
+ console.error('DB Error activating package:', dbError);
122
+ return { error: 'Activation successful, but local saving failed: ' + dbError.message };
123
+ }
124
+
125
+ revalidatePath('/cms/settings/packages');
126
+ return { success: true, package: pkg.name };
127
+
128
+ } catch (err: any) {
129
+ console.error('Activation Action Error:', err);
130
+ return { error: err.message || 'An unexpected error occurred.' };
131
+ }
132
+ }
133
+
134
+ export async function deactivatePackage(packageId: string) {
135
+ if (process.env.NEXT_PUBLIC_IS_SANDBOX === 'true') {
136
+ return { error: 'License deactivation is disabled in Sandbox mode.' };
137
+ }
138
+
139
+ const supabase = getServiceRoleClient();
140
+
141
+ // 1. Get current activation
142
+ const { data: activation, error: fetchError } = await supabase
143
+ .from('package_activations')
144
+ .select('id, license_key, instance_name, meta')
145
+ .eq('package_id', packageId)
146
+ .eq('status', 'active')
147
+ .single();
148
+
149
+ if (fetchError || !activation) {
150
+ return { error: 'No active license found for this package.' };
151
+ }
152
+
153
+ // 2. Deactivate at Freemius
154
+ try {
155
+ const fmProductId = activation.meta?.fm_product_id;
156
+ const uid = activation.meta?.fm_uid;
157
+ const installId = activation.meta?.fm_install_id;
158
+
159
+ if (fmProductId && uid && installId) {
160
+ await fetch(`${FM_API_URL}/products/${fmProductId}/licenses/deactivate.json?uid=${uid}&install_id=${installId}&license_key=${encodeURIComponent(activation.license_key)}`, {
161
+ method: 'POST',
162
+ headers: {
163
+ 'Accept': 'application/json',
164
+ }
165
+ });
166
+ }
167
+ } catch (err) {
168
+ console.warn('Freemius Deactivation failed (network?), removing locally anyway.', err);
169
+ }
170
+
171
+ // 3. Remove/Update local DB
172
+ const { error: deleteError } = await supabase
173
+ .from('package_activations')
174
+ .delete()
175
+ .eq('id', activation.id);
176
+
177
+ if (deleteError) {
178
+ return { error: 'Failed to remove local activation record.' };
179
+ }
180
+
181
+ revalidatePath('/cms/settings/packages');
182
+ return { success: true };
183
+ }
@@ -1,48 +1,147 @@
1
- 'use server';
2
-
3
- import { cache } from 'react';
4
- import { createClient } from '@nextblock-cms/db/server';
5
- import { revalidatePath } from 'next/cache';
6
- import type { PostWithMediaDimensions } from '../../components/blocks/types';
7
-
8
- export async function fetchPaginatedPublishedPosts(languageId: number, page: number, limit: number): Promise<{ posts: PostWithMediaDimensions[], totalCount: number, error?: string }> {
9
- const supabase = createClient();
10
- const offset = (page - 1) * limit;
11
-
12
- const { data: posts, error, count } = await supabase
13
- .from('posts')
14
- .select('*, media:media!feature_image_id(*)', { count: 'exact' })
15
- .eq('status', 'published')
16
- .eq('language_id', languageId)
17
- .order('published_at', { ascending: false })
18
- .range(offset, offset + limit - 1);
19
-
20
- if (error) {
21
- console.error("Error fetching paginated posts:", error);
22
- return { posts: [], totalCount: 0, error: error.message };
23
- }
24
-
25
- return { posts: posts as PostWithMediaDimensions[], totalCount: count || 0, error: undefined }; // Return error: undefined on success
26
- }
27
-
28
- // You could also move fetchInitialPublishedPosts here if it makes sense for organization
29
- export const fetchInitialPublishedPosts = cache(async (languageId: number, limit: number): Promise<{ posts: PostWithMediaDimensions[], totalCount: number, error?: string | null }> => {
30
- const supabase = createClient(); // This createClient is from utils/supabase/server
31
- const { data: posts, error, count } = await supabase
32
- .from('posts')
33
- .select('*, media:media!feature_image_id(*)', { count: 'exact' })
34
- .eq('status', 'published')
35
- .eq('language_id', languageId)
36
- .order('published_at', { ascending: false })
37
- .limit(limit);
38
-
39
- if (error) {
40
- console.error("Error fetching initial posts:", error);
41
- return { posts: [], totalCount: 0, error: error.message };
42
- }
43
-
44
- return { posts: posts as PostWithMediaDimensions[], totalCount: count || 0, error: null };
45
- });
1
+ 'use server';
2
+
3
+ import { cache } from 'react';
4
+ import { createClient } from '@nextblock-cms/db/server';
5
+ import { revalidatePath } from 'next/cache';
6
+ import type { Database } from '@nextblock-cms/db';
7
+ import type { PostWithMediaDimensions } from '../../components/blocks/types';
8
+ import { resolveMediaUrl } from '../../lib/media/resolveMediaUrl';
9
+ import { estimateReadTimeMinutesFromBlocks } from '../../lib/posts/readTime';
10
+
11
+ type PostRow = Database['public']['Tables']['posts']['Row'];
12
+ type BlockRow = Database['public']['Tables']['blocks']['Row'];
13
+ type FeatureMediaSelection = {
14
+ object_key: string;
15
+ width?: number | null;
16
+ height?: number | null;
17
+ blur_data_url?: string | null;
18
+ };
19
+
20
+ type PostQueryRow = PostRow & {
21
+ feature_media_object?: FeatureMediaSelection | FeatureMediaSelection[] | null;
22
+ };
23
+
24
+ type PostWithFeatureMedia = PostRow & {
25
+ feature_media_object?: FeatureMediaSelection | null;
26
+ };
27
+
28
+ function normalizeFeatureMediaObject(
29
+ mediaObject: PostQueryRow['feature_media_object']
30
+ ): FeatureMediaSelection | null {
31
+ if (Array.isArray(mediaObject)) {
32
+ return mediaObject[0] ?? null;
33
+ }
34
+
35
+ return mediaObject ?? null;
36
+ }
37
+
38
+ async function getEstimatedReadTimeMap(
39
+ supabase: ReturnType<typeof createClient>,
40
+ postIds: number[]
41
+ ) {
42
+ if (postIds.length === 0) {
43
+ return new Map<number, number>();
44
+ }
45
+
46
+ const { data: textBlocks, error } = await supabase
47
+ .from('blocks')
48
+ .select('post_id, block_type, content')
49
+ .in('post_id', postIds)
50
+ .eq('block_type', 'text')
51
+ .order('order', { ascending: true });
52
+
53
+ if (error) {
54
+ console.error('Error fetching post text blocks for read-time estimation:', error);
55
+ return new Map(postIds.map((postId) => [postId, 1]));
56
+ }
57
+
58
+ const blocksByPostId = new Map<number, Array<Pick<BlockRow, 'block_type' | 'content'>>>();
59
+ postIds.forEach((postId) => blocksByPostId.set(postId, []));
60
+
61
+ (textBlocks || []).forEach((block) => {
62
+ if (typeof block.post_id !== 'number') {
63
+ return;
64
+ }
65
+
66
+ const blocks = blocksByPostId.get(block.post_id) || [];
67
+ blocks.push(block as Pick<BlockRow, 'block_type' | 'content'>);
68
+ blocksByPostId.set(block.post_id, blocks);
69
+ });
70
+
71
+ return new Map(
72
+ postIds.map((postId) => [
73
+ postId,
74
+ estimateReadTimeMinutesFromBlocks(blocksByPostId.get(postId)),
75
+ ])
76
+ );
77
+ }
78
+
79
+ function normalizePostsForCards(
80
+ posts: PostWithFeatureMedia[],
81
+ estimatedReadTimeMap: Map<number, number>
82
+ ) {
83
+ return posts.map((post) => {
84
+ const mediaObject = post.feature_media_object || null;
85
+
86
+ return {
87
+ ...post,
88
+ feature_image_url: resolveMediaUrl(mediaObject?.object_key),
89
+ feature_image_width: mediaObject?.width || null,
90
+ feature_image_height: mediaObject?.height || null,
91
+ blur_data_url: mediaObject?.blur_data_url || null,
92
+ estimated_read_time_minutes: estimatedReadTimeMap.get(post.id) || 1,
93
+ } satisfies PostWithMediaDimensions;
94
+ });
95
+ }
96
+
97
+ async function fetchPublishedPostsPage(languageId: number, page: number, limit: number) {
98
+ const supabase = createClient();
99
+ const offset = (page - 1) * limit;
100
+
101
+ const { data: posts, error, count } = await supabase
102
+ .from('posts')
103
+ .select(
104
+ 'id, title, slug, label, excerpt, subtitle, published_at, language_id, status, created_at, updated_at, translation_group_id, feature_image_id, version, author_id, meta_title, meta_description, feature_media_object:media!feature_image_id(object_key, width, height, blur_data_url)',
105
+ { count: 'exact' }
106
+ )
107
+ .eq('status', 'published')
108
+ .eq('language_id', languageId)
109
+ .order('published_at', { ascending: false })
110
+ .range(offset, offset + limit - 1);
111
+
112
+ if (error) {
113
+ console.error('Error fetching published posts:', error);
114
+ return { posts: [], totalCount: 0, error: error.message };
115
+ }
116
+
117
+ const normalizedPosts: PostWithFeatureMedia[] = ((posts as PostQueryRow[] | null) ?? []).map(
118
+ (post) => ({
119
+ ...post,
120
+ feature_media_object: normalizeFeatureMediaObject(post.feature_media_object),
121
+ })
122
+ );
123
+ const postIds = normalizedPosts.map((post) => post.id);
124
+ const estimatedReadTimeMap = await getEstimatedReadTimeMap(supabase, postIds);
125
+
126
+ return {
127
+ posts: normalizePostsForCards(normalizedPosts, estimatedReadTimeMap),
128
+ totalCount: count || 0,
129
+ error: undefined,
130
+ };
131
+ }
132
+
133
+ export async function fetchPaginatedPublishedPosts(languageId: number, page: number, limit: number): Promise<{ posts: PostWithMediaDimensions[], totalCount: number, error?: string }> {
134
+ return fetchPublishedPostsPage(languageId, page, limit);
135
+ }
136
+
137
+ export const fetchInitialPublishedPosts = cache(async (languageId: number, limit: number): Promise<{ posts: PostWithMediaDimensions[], totalCount: number, error?: string | null }> => {
138
+ const result = await fetchPublishedPostsPage(languageId, 1, limit);
139
+ return {
140
+ posts: result.posts,
141
+ totalCount: result.totalCount,
142
+ error: result.error ?? null,
143
+ };
144
+ });
46
145
  export async function revalidateAndLog(path: string): Promise<{ success: boolean; error?: string }> {
47
146
  try {
48
147
  // Step 1: Revalidate the path
@@ -69,12 +168,11 @@ export async function revalidateAndLog(path: string): Promise<{ success: boolean
69
168
  throw new Error(`Failed to log revalidation: ${response.status} ${response.statusText} - ${errorBody.error}`);
70
169
  }
71
170
 
72
- console.log(`Successfully revalidated and logged path: ${path}`);
73
- return { success: true };
171
+ return { success: true };
74
172
 
75
173
  } catch (error) {
76
174
  const errorMessage = error instanceof Error ? error.message : 'An unknown error occurred';
77
175
  console.error(`Error in revalidateAndLog for path "${path}":`, errorMessage);
78
176
  return { success: false, error: errorMessage };
79
177
  }
80
- }
178
+ }