create-nextblock 0.2.78 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (413) hide show
  1. package/bin/create-nextblock.js +793 -472
  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
@@ -4,14 +4,16 @@ import React, { useState, lazy } from 'react';
4
4
  import { cn } from '@nextblock-cms/utils';
5
5
  import { Button } from '@nextblock-cms/ui';
6
6
  import { PlusCircle, Trash2, Edit2, GripVertical, Image as ImageIcon } from "lucide-react";
7
- import type { SectionBlockContent } from '@/lib/blocks/blockRegistry';
8
- import { availableBlockTypes, getBlockDefinition, getInitialContent, BlockType } from '@/lib/blocks/blockRegistry';
7
+ import { SectionBlockContent } from '../../../../lib/blocks/blockRegistry';
8
+ import { availableBlockTypes, getBlockDefinition, getInitialContent, BlockType } from '../../../../lib/blocks/blockRegistry';
9
9
  import { useDroppable } from "@dnd-kit/core";
10
10
  import { useSortable } from "@dnd-kit/sortable";
11
11
  import { CSS } from "@dnd-kit/utilities";
12
12
  import { BlockEditorModal } from './BlockEditorModal';
13
13
  import { ConfirmationDialog } from '@nextblock-cms/ui';
14
14
  import BlockTypeSelector from './BlockTypeSelector';
15
+ import { resolveMediaUrl } from '../../../../lib/media/resolveMediaUrl';
16
+ import { CustomBlockEditorPreview } from './CustomBlockEditorPreview';
15
17
 
16
18
  type ColumnBlock = SectionBlockContent['column_blocks'][0][0];
17
19
 
@@ -22,7 +24,7 @@ interface SortableColumnBlockProps {
22
24
  columnIndex: number;
23
25
  onEdit: () => void;
24
26
  onDelete: () => void;
25
- blockType: 'section' | 'hero';
27
+ blockType: 'section';
26
28
  onClick: (e: React.MouseEvent<HTMLDivElement>) => void;
27
29
  sectionBackground?: SectionBlockContent['background'];
28
30
  }
@@ -51,9 +53,6 @@ function SortableColumnBlock({ block, index, columnIndex, onEdit, onDelete, bloc
51
53
  transition,
52
54
  opacity: isDragging ? 0.5 : 1,
53
55
  };
54
-
55
- const R2_BASE_URL = process.env.NEXT_PUBLIC_R2_BASE_URL || '';
56
-
57
56
  // Helper to check for dark background
58
57
  const isDarkBackground = React.useMemo(() => {
59
58
  if (!sectionBackground) return false;
@@ -181,7 +180,7 @@ function SortableColumnBlock({ block, index, columnIndex, onEdit, onDelete, bloc
181
180
  );
182
181
  }
183
182
  case 'image': {
184
- const imageUrl = block.content.object_key ? `${R2_BASE_URL}/${block.content.object_key}` : block.content.src;
183
+ const imageUrl = resolveMediaUrl(block.content.object_key) || block.content.src;
185
184
  return (
186
185
  <div className="flex gap-3">
187
186
  <div className="flex-shrink-0 h-10 w-10 bg-muted/20 rounded overflow-hidden flex items-center justify-center border border-white/10">
@@ -255,10 +254,26 @@ function SortableColumnBlock({ block, index, columnIndex, onEdit, onDelete, bloc
255
254
  </div>
256
255
  </div>
257
256
  );
258
- default:
259
- // For fallback blocks, we might still want the label if we can't render a preview
260
- // But user asked to remove it. Let's show a generic "configured" message or similar.
261
- return <div className={cn("text-xs", isDarkBackground ? "text-white/50" : "text-muted-foreground")}>Unknown Block Type</div>;
257
+ default: {
258
+ const formattedLabel = block.block_type
259
+ .split(/[-_]/)
260
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
261
+ .join(" ");
262
+ const placeholder = (
263
+ <div className={cn("text-xs flex flex-col gap-0.5", isDarkBackground ? "text-white/70" : "text-muted-foreground")}>
264
+ <span className="font-medium">{formattedLabel}</span>
265
+ <span className="text-[10px] opacity-70">Click edit to configure</span>
266
+ </div>
267
+ );
268
+ // Custom blocks (block_type === definition slug) render their real layout.
269
+ return (
270
+ <CustomBlockEditorPreview
271
+ blockType={block.block_type}
272
+ content={(block.content || {}) as Record<string, any>}
273
+ fallback={placeholder}
274
+ />
275
+ );
276
+ }
262
277
  }
263
278
  };
264
279
 
@@ -301,7 +316,7 @@ export interface ColumnEditorProps {
301
316
  columnIndex: number;
302
317
  blocks: ColumnBlock[];
303
318
  onBlocksChange: (newBlocks: ColumnBlock[]) => void;
304
- blockType: 'section' | 'hero';
319
+ blockType: 'section';
305
320
  sectionBackground?: SectionBlockContent['background'];
306
321
  }
307
322
 
@@ -374,7 +389,7 @@ export default function ColumnEditor({ columnIndex, blocks, onBlocksChange, bloc
374
389
  const initialContent = getInitialContent(selectedBlockType);
375
390
  const newBlock: ColumnBlock = {
376
391
  block_type: selectedBlockType,
377
- content: initialContent || {},
392
+ content: (initialContent || {}) as Record<string, any>,
378
393
  temp_id: `temp-${Date.now()}-${Math.random()}`
379
394
  };
380
395
  onBlocksChange([...blocks, newBlock]);
@@ -406,7 +421,9 @@ export default function ColumnEditor({ columnIndex, blocks, onBlocksChange, bloc
406
421
  const handleStartEdit = (block: ColumnBlock, index: number) => {
407
422
  const blockDef = getBlockDefinition(block.block_type);
408
423
  if (!blockDef) {
409
- console.error(`No definition found for block type: ${block.block_type}`);
424
+ const Editor = lazy(() => import(`../editors/DynamicCustomBlockEditor`));
425
+ setLazyEditor(Editor);
426
+ setEditingBlock({ ...block, index });
410
427
  return;
411
428
  }
412
429
 
@@ -534,9 +551,9 @@ export default function ColumnEditor({ columnIndex, blocks, onBlocksChange, bloc
534
551
  onOpenChange={setIsBlockSelectorOpen}
535
552
  onSelectBlockType={handleSelectBlockType}
536
553
  allowedBlockTypes={availableBlockTypes.filter(
537
- (type) => type !== 'section' && type !== 'hero'
554
+ (type) => type !== 'section'
538
555
  )}
539
556
  />
540
557
  </div>
541
558
  );
542
- }
559
+ }
@@ -0,0 +1,160 @@
1
+ "use client";
2
+
3
+ import React, { useEffect, useMemo, useState } from "react";
4
+ import { Loader2 } from "lucide-react";
5
+ import { DynamicLayoutEngine } from "../../../../components/renderers/DynamicLayoutEngine";
6
+
7
+ // Module-level cache so every block preview shares a single network request for
8
+ // the custom block definitions instead of re-fetching per card.
9
+ type EditorDefinition = {
10
+ slug: string;
11
+ name: string;
12
+ description?: string | null;
13
+ fields: any[];
14
+ layout_schema: any;
15
+ };
16
+
17
+ const definitionCache: { definitions: EditorDefinition[]; promise: Promise<EditorDefinition[]> | null } = {
18
+ definitions: [],
19
+ promise: null,
20
+ };
21
+
22
+ function loadCustomBlockDefinitions(): Promise<EditorDefinition[]> {
23
+ if (definitionCache.definitions.length > 0) {
24
+ return Promise.resolve(definitionCache.definitions);
25
+ }
26
+ if (!definitionCache.promise) {
27
+ definitionCache.promise = fetch("/api/custom-blocks/editor-definitions")
28
+ .then((res) => (res.ok ? res.json() : { definitions: [] }))
29
+ .then((data) => {
30
+ definitionCache.definitions = (data?.definitions ?? []) as EditorDefinition[];
31
+ return definitionCache.definitions;
32
+ })
33
+ .catch(() => []);
34
+ }
35
+ return definitionCache.promise;
36
+ }
37
+
38
+ function useCustomBlockDefinition(slug: string) {
39
+ const [definitions, setDefinitions] = useState<EditorDefinition[]>(definitionCache.definitions);
40
+ const [loading, setLoading] = useState(definitionCache.definitions.length === 0);
41
+
42
+ useEffect(() => {
43
+ let cancelled = false;
44
+ loadCustomBlockDefinitions().then((defs) => {
45
+ if (!cancelled) {
46
+ setDefinitions(defs);
47
+ setLoading(false);
48
+ }
49
+ });
50
+ return () => {
51
+ cancelled = true;
52
+ };
53
+ }, []);
54
+
55
+ const definition = useMemo(
56
+ () => definitions.find((def) => def.slug === slug) ?? null,
57
+ [definitions, slug]
58
+ );
59
+
60
+ return { definition, loading };
61
+ }
62
+
63
+ // Resolve db_relation field values into records so the preview matches the front
64
+ // end (including relation images), mirroring the server-side hydration.
65
+ function useResolvedRelations(definition: EditorDefinition | null, content: Record<string, any>) {
66
+ const [resolvedRelations, setResolvedRelations] = useState<Record<string, any>>({});
67
+
68
+ const relationSignature = useMemo(() => {
69
+ if (!definition) return "";
70
+ return definition.fields
71
+ .filter((field) => field.type === "db_relation")
72
+ .map((field) => `${field.key}:${JSON.stringify(content?.[field.key] ?? null)}`)
73
+ .join("|");
74
+ }, [definition, content]);
75
+
76
+ useEffect(() => {
77
+ if (!definition) return;
78
+ const relationFields = definition.fields.filter((field) => field.type === "db_relation");
79
+ if (relationFields.length === 0) {
80
+ setResolvedRelations({});
81
+ return;
82
+ }
83
+
84
+ let cancelled = false;
85
+ (async () => {
86
+ const next: Record<string, any> = {};
87
+ await Promise.all(
88
+ relationFields.map(async (field) => {
89
+ const raw = content?.[field.key];
90
+ const values = Array.isArray(raw) ? raw : raw ? [raw] : [];
91
+ if (values.length === 0) return;
92
+
93
+ const params = new URLSearchParams({
94
+ table: field.table,
95
+ valueColumn: field.value_column || "id",
96
+ displayColumn: field.display_column || "title",
97
+ values: values.map(String).join(","),
98
+ limit: String(values.length),
99
+ });
100
+
101
+ try {
102
+ const res = await fetch(`/api/custom-blocks/db-relations?${params.toString()}`, {
103
+ cache: "no-store",
104
+ });
105
+ if (!res.ok) return;
106
+ const data = await res.json();
107
+ const items = data?.items ?? [];
108
+ next[field.key] = field.multiple ? items : items[0] ?? null;
109
+ } catch {
110
+ // Leave the relation unresolved; the engine falls back gracefully.
111
+ }
112
+ })
113
+ );
114
+
115
+ if (!cancelled) {
116
+ setResolvedRelations(next);
117
+ }
118
+ })();
119
+
120
+ return () => {
121
+ cancelled = true;
122
+ };
123
+ }, [definition, relationSignature]);
124
+
125
+ return resolvedRelations;
126
+ }
127
+
128
+ export interface CustomBlockEditorPreviewProps {
129
+ blockType: string;
130
+ content: Record<string, any>;
131
+ fallback: React.ReactNode;
132
+ }
133
+
134
+ export function CustomBlockEditorPreview({ blockType, content, fallback }: CustomBlockEditorPreviewProps) {
135
+ const { definition, loading } = useCustomBlockDefinition(blockType);
136
+ const resolvedRelations = useResolvedRelations(definition, content);
137
+
138
+ if (!definition) {
139
+ if (loading) {
140
+ return (
141
+ <div className="flex items-center gap-2 py-3 text-xs text-muted-foreground">
142
+ <Loader2 className="h-3.5 w-3.5 animate-spin" />
143
+ Loading block preview…
144
+ </div>
145
+ );
146
+ }
147
+ return <>{fallback}</>;
148
+ }
149
+
150
+ return (
151
+ // pointer-events-none lets clicks fall through to the card so editing still opens.
152
+ <div className="pointer-events-none w-full overflow-hidden">
153
+ <DynamicLayoutEngine
154
+ fields={definition.fields}
155
+ layoutSchema={definition.layout_schema}
156
+ data={{ ...(content || {}), resolved_relations: resolvedRelations }}
157
+ />
158
+ </div>
159
+ );
160
+ }
@@ -8,12 +8,13 @@ import PostsGridBlockEditor from '../editors/PostsGridBlockEditor';
8
8
  type Block = Database['public']['Tables']['blocks']['Row'];
9
9
  import { Button, Card, CardContent, Avatar, AvatarImage, AvatarFallback } from "@nextblock-cms/ui";
10
10
  import { GripVertical, Edit2, Image as ImageIcon, MessageSquareQuote } from "lucide-react";
11
- import { getBlockDefinition, blockRegistry, BlockType } from "@/lib/blocks/blockRegistry";
11
+ import { getBlockDefinition, blockRegistry, BlockType } from '../../../../lib/blocks/blockRegistry';
12
12
  import { BlockEditorModal } from './BlockEditorModal';
13
13
  import { DeleteBlockButtonClient } from './DeleteBlockButtonClient';
14
14
  import { cn } from '@nextblock-cms/utils';
15
-
16
- const R2_BASE_URL = process.env.NEXT_PUBLIC_R2_BASE_URL || '';
15
+ import { resolveMediaUrl } from '../../../../lib/media/resolveMediaUrl';
16
+ import { SimpleTiptapRenderer } from '@nextblock-cms/ecommerce';
17
+ import { CustomBlockEditorPreview } from './CustomBlockEditorPreview';
17
18
 
18
19
  export interface EditableBlockProps {
19
20
  block: Block;
@@ -39,7 +40,7 @@ export default function EditableBlock({
39
40
  const [LazyEditor, setLazyEditor] = useState<LazyExoticComponent<ComponentType<any>> | ComponentType<any> | null>(null);
40
41
 
41
42
  const SectionEditor = useMemo(() => {
42
- if (block?.block_type === 'section' || block?.block_type === 'hero') {
43
+ if (block?.block_type === 'section') {
43
44
  const editorFilename = blockRegistry[block.block_type as BlockType]?.editorComponentFilename;
44
45
  if (editorFilename) {
45
46
  return lazy(() => import(`../editors/${editorFilename}`));
@@ -57,7 +58,7 @@ export default function EditableBlock({
57
58
 
58
59
 
59
60
  const handleEditClick = () => {
60
- if (block.block_type === 'section' || block.block_type === 'hero') {
61
+ if (block.block_type === 'section') {
61
62
  setIsConfigPanelOpen(prev => !prev);
62
63
  } else {
63
64
  const blockDef = getBlockDefinition(block.block_type as BlockType);
@@ -78,6 +79,11 @@ export default function EditableBlock({
78
79
  setLazyEditor(Editor);
79
80
  setEditingBlock(block);
80
81
  }
82
+ else {
83
+ const Editor = lazy(() => import(`../editors/DynamicCustomBlockEditor`));
84
+ setLazyEditor(Editor);
85
+ setEditingBlock(block);
86
+ }
81
87
  }
82
88
  };
83
89
 
@@ -90,7 +96,7 @@ export default function EditableBlock({
90
96
 
91
97
  // If the click was on the card's background (not a button), and it's an editable block type,
92
98
  // then trigger the edit handler.
93
- if (block.block_type !== 'section' && block.block_type !== 'hero') {
99
+ if (block.block_type !== 'section') {
94
100
  handleEditClick();
95
101
  }
96
102
  };
@@ -109,19 +115,24 @@ export default function EditableBlock({
109
115
  const isCentered = htmlContent.includes('text-align: center') || htmlContent.includes('class="text-center"');
110
116
  const isRight = htmlContent.includes('text-align: right') || htmlContent.includes('class="text-right"');
111
117
  const alignmentClass = isCentered ? 'text-center' : isRight ? 'text-right' : 'text-left';
118
+ const isJson = htmlContent.trim().startsWith('{') || htmlContent.trim().startsWith('[');
112
119
 
113
120
  return (
114
121
  <div className="py-2">
115
122
  <div className={cn("text-xs w-full", alignmentClass)}>
116
123
  {htmlContent ? (
117
- <div
118
- dangerouslySetInnerHTML={{ __html: htmlContent }}
119
- className={cn(
120
- "prose prose-sm max-w-none [&>p]:my-0 [&>h1]:my-0 [&>h2]:my-0 [&>h3]:my-0 dark:prose-invert",
121
- isCentered && "[&_*]:text-center",
122
- isRight && "[&_*]:text-right"
123
- )}
124
- />
124
+ isJson ? (
125
+ <SimpleTiptapRenderer content={htmlContent} className="prose prose-sm max-w-none [&>p]:my-0 [&>h1]:my-0 [&>h2]:my-0 [&>h3]:my-0 dark:prose-invert" />
126
+ ) : (
127
+ <div
128
+ dangerouslySetInnerHTML={{ __html: htmlContent }}
129
+ className={cn(
130
+ "prose prose-sm max-w-none [&>p]:my-0 [&>h1]:my-0 [&>h2]:my-0 [&>h3]:my-0 dark:prose-invert",
131
+ isCentered && "[&_*]:text-center",
132
+ isRight && "[&_*]:text-right"
133
+ )}
134
+ />
135
+ )
125
136
  ) : <span className="text-muted-foreground italic">Empty text block</span>}
126
137
  </div>
127
138
  </div>
@@ -168,7 +179,7 @@ export default function EditableBlock({
168
179
  }
169
180
  case 'image': {
170
181
  const content = (block.content || {}) as any;
171
- const imageUrl = content.object_key ? `${R2_BASE_URL}/${content.object_key}` : content.src;
182
+ const imageUrl = resolveMediaUrl(content.object_key) || content.src;
172
183
  return (
173
184
  <div className="flex gap-4 py-2">
174
185
  <div className="flex-shrink-0 h-16 w-16 bg-muted rounded overflow-hidden flex items-center justify-center border">
@@ -257,7 +268,7 @@ export default function EditableBlock({
257
268
  default: {
258
269
  const blockDefinition = getBlockDefinition(currentBlockType as BlockType);
259
270
  const blockLabel = blockDefinition?.label || currentBlockType;
260
- return (
271
+ const placeholder = (
261
272
  <div
262
273
  className="py-4 flex flex-col items-center justify-center space-y-2 min-h-[80px] border border-dashed rounded-md bg-muted/20 cursor-pointer hover:border-primary"
263
274
  onClick={handleCardClick}
@@ -268,11 +279,19 @@ export default function EditableBlock({
268
279
  </div>
269
280
  </div>
270
281
  );
282
+ // Custom blocks (block_type === definition slug) render their real layout.
283
+ return (
284
+ <CustomBlockEditorPreview
285
+ blockType={currentBlockType}
286
+ content={(block.content || {}) as Record<string, any>}
287
+ fallback={placeholder}
288
+ />
289
+ );
271
290
  }
272
291
  }
273
292
  };
274
293
 
275
- const isSection = block?.block_type === 'section' || block?.block_type === 'hero';
294
+ const isSection = block?.block_type === 'section';
276
295
  const blockDefinition = getBlockDefinition(block.block_type as BlockType);
277
296
 
278
297
  return (
@@ -313,7 +332,7 @@ export default function EditableBlock({
313
332
  {isSection ? (
314
333
  <div className="mt-2 min-h-[200px]">
315
334
  <Suspense fallback={<div className="flex justify-center items-center h-full"><p>Loading Editor...</p></div>}>
316
- {SectionEditor && <SectionEditor block={block} content={block.content || {}} onChange={(newContent: Record<string, any>) => onContentChange(block.id, newContent)} blockType={block.block_type as 'section' | 'hero'} isConfigPanelOpen={isConfigPanelOpen} />}
335
+ {SectionEditor && <SectionEditor block={block} content={block.content || {}} onChange={(newContent: Record<string, any>) => onContentChange(block.id, newContent)} blockType={block.block_type as 'section'} isConfigPanelOpen={isConfigPanelOpen} />}
317
336
  </Suspense>
318
337
  </div>
319
338
  ) : renderPreview()}
@@ -1,6 +1,6 @@
1
- "use client";
2
-
3
- import React, { useState, useCallback, useEffect } from 'react';
1
+ "use client";
2
+
3
+ import React, { useState, useCallback, useEffect, useRef } from 'react';
4
4
  import { Editor } from '@tiptap/react';
5
5
  import Image from 'next/image';
6
6
  import { Image as ImageIconLucide, Search, CheckCircle } from 'lucide-react';
@@ -14,49 +14,113 @@ import {
14
14
  DialogFooter,
15
15
  DialogClose,
16
16
  } from '@nextblock-cms/ui';
17
- import { Input } from '@nextblock-cms/ui';
18
- import type { Database } from '@nextblock-cms/db';
19
- import { createClient as createBrowserClient } from '@nextblock-cms/db';
20
-
21
- type Media = Database['public']['Tables']['media']['Row'];
22
-
23
- const R2_BASE_URL = process.env.NEXT_PUBLIC_R2_BASE_URL || "";
17
+ import { Input } from '@nextblock-cms/ui';
18
+ import type { Database } from '@nextblock-cms/db';
19
+ import { resolveMediaUrl } from '../../../../lib/media/resolveMediaUrl';
20
+
21
+ type Media = Database['public']['Tables']['media']['Row'];
22
+
23
+ const MEDIA_REQUEST_TIMEOUT_MS = 8000;
24
+ const MEDIA_LIBRARY_LIMIT = 20;
25
+
26
+ function resolveMediaPreviewPath(media: Media) {
27
+ return media.file_path || media.object_key || null;
28
+ }
29
+
30
+ function resolveMediaPreviewSrc(path: string) {
31
+ return resolveMediaUrl(path);
32
+ }
24
33
 
25
34
  interface MediaLibraryModalProps {
26
35
  editor: Editor | null;
27
36
  }
28
37
 
29
- export const MediaLibraryModal = ({ editor }: MediaLibraryModalProps) => {
30
- const [isModalOpen, setIsModalOpen] = useState(false);
31
- const [mediaLibrary, setMediaLibrary] = useState<Media[]>([]);
32
- const [isLoadingMedia, setIsLoadingMedia] = useState(false);
33
- const [searchTerm, setSearchTerm] = useState("");
34
- const supabase = createBrowserClient();
35
-
36
- const fetchLibrary = useCallback(async () => {
37
- if (!isModalOpen) return;
38
- setIsLoadingMedia(true);
39
- let query = supabase.from('media').select('*').order('created_at', { ascending: false }).limit(20);
40
- if (searchTerm) {
41
- query = query.ilike('file_name', `%${searchTerm}%`);
42
- }
43
- const { data, error } = await query;
44
- if (data) setMediaLibrary(data);
45
- else console.error("Error fetching media library:", error);
46
- setIsLoadingMedia(false);
47
- }, [isModalOpen, searchTerm, supabase]);
38
+ export const MediaLibraryModal = ({ editor }: MediaLibraryModalProps) => {
39
+ const [isModalOpen, setIsModalOpen] = useState(false);
40
+ const [mediaLibrary, setMediaLibrary] = useState<Media[]>([]);
41
+ const [isLoadingMedia, setIsLoadingMedia] = useState(false);
42
+ const [loadError, setLoadError] = useState<string | null>(null);
43
+ const [searchTerm, setSearchTerm] = useState("");
44
+ const requestIdRef = useRef(0);
45
+
46
+ const fetchLibrary = useCallback(async () => {
47
+ if (!isModalOpen) return;
48
+ const requestId = ++requestIdRef.current;
49
+ setIsLoadingMedia(true);
50
+ setLoadError(null);
51
+ let timeoutId: ReturnType<typeof setTimeout> | null = null;
52
+ try {
53
+ const controller = new AbortController();
54
+ timeoutId = setTimeout(
55
+ () => controller.abort(new Error('Media library request timed out.')),
56
+ MEDIA_REQUEST_TIMEOUT_MS
57
+ );
58
+ const params = new URLSearchParams({
59
+ limit: MEDIA_LIBRARY_LIMIT.toString(),
60
+ });
61
+
62
+ if (searchTerm.trim()) {
63
+ params.set('q', searchTerm.trim());
64
+ }
65
+
66
+ const response = await fetch(`/api/media/library?${params.toString()}`, {
67
+ method: 'GET',
68
+ cache: 'no-store',
69
+ signal: controller.signal,
70
+ });
71
+ clearTimeout(timeoutId);
72
+
73
+ const payload = (await response.json()) as {
74
+ items?: Media[];
75
+ error?: string;
76
+ };
77
+
78
+ if (requestId !== requestIdRef.current) {
79
+ return;
80
+ }
81
+
82
+ if (!response.ok) {
83
+ console.error('Error fetching media library:', payload.error);
84
+ setMediaLibrary([]);
85
+ setLoadError(payload.error || 'We could not load the media library. Please retry.');
86
+ return;
87
+ }
88
+
89
+ setMediaLibrary(payload.items || []);
90
+ } catch (error: any) {
91
+ if (requestId !== requestIdRef.current) {
92
+ return;
93
+ }
94
+
95
+ console.error('Error fetching media library:', error);
96
+ setMediaLibrary([]);
97
+ if (error?.name === 'AbortError' || error?.message === 'Media library request timed out.') {
98
+ setLoadError('The media library took too long to respond. Please retry.');
99
+ } else {
100
+ setLoadError('We could not load the media library. Please retry.');
101
+ }
102
+ } finally {
103
+ if (timeoutId) {
104
+ clearTimeout(timeoutId);
105
+ }
106
+ if (requestId === requestIdRef.current) {
107
+ setIsLoadingMedia(false);
108
+ }
109
+ }
110
+ }, [isModalOpen, searchTerm]);
48
111
 
49
112
  useEffect(() => {
50
113
  fetchLibrary();
51
114
  }, [fetchLibrary]);
52
115
 
53
- const handleSelectMedia = (mediaItem: Media) => {
54
- if (editor && mediaItem.file_type?.startsWith("image/")) {
55
- const imageUrl = `${R2_BASE_URL}/${mediaItem.object_key}`;
56
- editor.chain().focus().insertContent(`<img src="${imageUrl}" alt="${mediaItem.description || mediaItem.file_name}" />`).run();
57
- }
58
- setIsModalOpen(false);
59
- };
116
+ const handleSelectMedia = (mediaItem: Media) => {
117
+ const previewPath = resolveMediaPreviewPath(mediaItem);
118
+ const imageUrl = previewPath ? resolveMediaPreviewSrc(previewPath) : null;
119
+ if (editor && mediaItem.file_type?.startsWith("image/") && imageUrl) {
120
+ editor.chain().focus().insertContent(`<img src="${imageUrl}" alt="${mediaItem.description || mediaItem.file_name}" />`).run();
121
+ }
122
+ setIsModalOpen(false);
123
+ };
60
124
 
61
125
  return (
62
126
  <Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
@@ -79,36 +143,54 @@ export const MediaLibraryModal = ({ editor }: MediaLibraryModalProps) => {
79
143
  />
80
144
  <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
81
145
  </div>
82
- {isLoadingMedia ? (
83
- <div className="flex-grow flex items-center justify-center"><p>Loading media...</p></div>
84
- ) : mediaLibrary.length === 0 ? (
85
- <div className="flex-grow flex items-center justify-center"><p>No media found.</p></div>
86
- ) : (
87
- <div className="grid grid-cols-3 sm:grid-cols-4 md:grid-cols-5 lg:grid-cols-6 gap-3 overflow-y-auto flex-grow pr-2">
88
- {mediaLibrary.filter(m => m.file_type?.startsWith("image/")).map((media) => (
89
- <button
90
- key={media.id}
91
- type="button"
92
- className="relative aspect-square border rounded-md overflow-hidden group focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2"
93
- onClick={() => handleSelectMedia(media)}
94
- >
95
- <Image
96
- src={`${R2_BASE_URL}/${media.object_key}`}
97
- alt={media.description || media.file_name}
98
- width={200}
99
- height={200}
100
- className="h-full w-full object-cover"
101
- />
102
- <div className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 group-focus:opacity-100 transition-opacity flex items-center justify-center">
103
- <CheckCircle className="h-8 w-8 text-white" />
104
- </div>
105
- <p className="absolute bottom-0 left-0 right-0 bg-black/50 text-white text-xs p-1 truncate text-center">
106
- {media.file_name}
107
- </p>
108
- </button>
109
- ))}
110
- </div>
111
- )}
146
+ {isLoadingMedia ? (
147
+ <div className="flex-grow flex items-center justify-center"><p>Loading media...</p></div>
148
+ ) : loadError ? (
149
+ <div className="flex-grow flex flex-col items-center justify-center gap-3 text-center">
150
+ <p className="max-w-sm text-sm text-muted-foreground">{loadError}</p>
151
+ <Button type="button" variant="outline" size="sm" onClick={() => void fetchLibrary()}>
152
+ Retry
153
+ </Button>
154
+ </div>
155
+ ) : mediaLibrary.length === 0 ? (
156
+ <div className="flex-grow flex items-center justify-center"><p>No media found.</p></div>
157
+ ) : (
158
+ <div className="grid grid-cols-3 sm:grid-cols-4 md:grid-cols-5 lg:grid-cols-6 gap-3 overflow-y-auto flex-grow pr-2">
159
+ {mediaLibrary.filter(m => m.file_type?.startsWith("image/")).map((media) => {
160
+ const previewPath = resolveMediaPreviewPath(media);
161
+ const previewSrc = previewPath ? resolveMediaPreviewSrc(previewPath) : null;
162
+
163
+ return (
164
+ <button
165
+ key={media.id}
166
+ type="button"
167
+ className="relative aspect-square border rounded-md overflow-hidden group focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2"
168
+ onClick={() => handleSelectMedia(media)}
169
+ >
170
+ {previewSrc ? (
171
+ <Image
172
+ src={previewSrc}
173
+ alt={media.description || media.file_name}
174
+ width={200}
175
+ height={200}
176
+ className="h-full w-full object-cover"
177
+ />
178
+ ) : (
179
+ <div className="flex h-full w-full items-center justify-center bg-muted text-xs text-muted-foreground">
180
+ Preview unavailable
181
+ </div>
182
+ )}
183
+ <div className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 group-focus:opacity-100 transition-opacity flex items-center justify-center">
184
+ <CheckCircle className="h-8 w-8 text-white" />
185
+ </div>
186
+ <p className="absolute bottom-0 left-0 right-0 bg-black/50 text-white text-xs p-1 truncate text-center">
187
+ {media.file_name}
188
+ </p>
189
+ </button>
190
+ );
191
+ })}
192
+ </div>
193
+ )}
112
194
  <DialogFooter className="mt-4">
113
195
  <DialogClose asChild>
114
196
  <Button type="button" variant="outline">Cancel</Button>
@@ -117,4 +199,4 @@ export const MediaLibraryModal = ({ editor }: MediaLibraryModalProps) => {
117
199
  </DialogContent>
118
200
  </Dialog>
119
201
  );
120
- };
202
+ };