create-nextblock 0.2.77 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (413) hide show
  1. package/bin/create-nextblock.js +740 -459
  2. package/package.json +1 -2
  3. package/scripts/sync-template.js +18 -1
  4. package/templates/nextblock-template/.browserslistrc +11 -0
  5. package/templates/nextblock-template/.swcrc +30 -30
  6. package/templates/nextblock-template/README.md +23 -114
  7. package/templates/nextblock-template/app/(auth-pages)/post-sign-in/page.tsx +27 -28
  8. package/templates/nextblock-template/app/(auth-pages)/sign-in/page.tsx +50 -25
  9. package/templates/nextblock-template/app/(auth-pages)/sign-up/page.tsx +111 -56
  10. package/templates/nextblock-template/app/(auth-pages)/two-factor/actions.ts +91 -0
  11. package/templates/nextblock-template/app/(auth-pages)/two-factor/components/TwoFactorForm.tsx +118 -0
  12. package/templates/nextblock-template/app/(auth-pages)/two-factor/page.tsx +51 -0
  13. package/templates/nextblock-template/app/.well-known/ucp/route.ts +16 -0
  14. package/templates/nextblock-template/app/[slug]/PageClientContent.tsx +48 -28
  15. package/templates/nextblock-template/app/[slug]/page.tsx +63 -6
  16. package/templates/nextblock-template/app/[slug]/page.utils.ts +374 -157
  17. package/templates/nextblock-template/app/[slug]/pageClientActions.ts +7 -0
  18. package/templates/nextblock-template/app/actions/consent.ts +57 -0
  19. package/templates/nextblock-template/app/actions/formActions.ts +130 -11
  20. package/templates/nextblock-template/app/actions/languageActions.ts +31 -30
  21. package/templates/nextblock-template/app/actions/package-actions.ts +183 -0
  22. package/templates/nextblock-template/app/actions/postActions.ts +146 -48
  23. package/templates/nextblock-template/app/actions/twoFactorEmail.ts +21 -0
  24. package/templates/nextblock-template/app/actions/visualEditingActions.test.ts +179 -0
  25. package/templates/nextblock-template/app/actions/visualEditingActions.ts +345 -0
  26. package/templates/nextblock-template/app/actions.ts +67 -12
  27. package/templates/nextblock-template/app/api/ai/cortex/build-widget/route.ts +153 -0
  28. package/templates/nextblock-template/app/api/ai/generate-blocks/route.ts +96 -0
  29. package/templates/nextblock-template/app/api/ai/global-agent/route.ts +965 -0
  30. package/templates/nextblock-template/app/api/checkout/freemius/sync/route.ts +29 -0
  31. package/templates/nextblock-template/app/api/checkout/route.ts +146 -0
  32. package/templates/nextblock-template/app/api/cms/full-backup/export/route.ts +33 -0
  33. package/templates/nextblock-template/app/api/cms/full-backup/restore/route.ts +63 -0
  34. package/templates/nextblock-template/app/api/cron/reset-sandbox/route.ts +3413 -17
  35. package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +7830 -0
  36. package/templates/nextblock-template/app/api/cron/sync-currencies/route.ts +35 -0
  37. package/templates/nextblock-template/app/api/custom-blocks/db-relations/route.ts +92 -0
  38. package/templates/nextblock-template/app/api/custom-blocks/editor-definitions/route.ts +43 -0
  39. package/templates/nextblock-template/app/api/draft/disable/route.ts +25 -0
  40. package/templates/nextblock-template/app/api/draft/route.ts +93 -0
  41. package/templates/nextblock-template/app/api/draft/start/route.ts +77 -0
  42. package/templates/nextblock-template/app/api/media/library/route.ts +65 -0
  43. package/templates/nextblock-template/app/api/media/r2-presigned/route.ts +53 -0
  44. package/templates/nextblock-template/app/api/media/record/route.ts +160 -0
  45. package/templates/nextblock-template/app/api/search/route.ts +43 -0
  46. package/templates/nextblock-template/app/api/visual-editing/block-draft/route.ts +47 -0
  47. package/templates/nextblock-template/app/api/visual-editing/product-draft/route.ts +47 -0
  48. package/templates/nextblock-template/app/api/webhooks/freemius/route.ts +34 -0
  49. package/templates/nextblock-template/app/api/webhooks/stripe/route.ts +27 -0
  50. package/templates/nextblock-template/app/article/[slug]/PostClientContent.tsx +392 -128
  51. package/templates/nextblock-template/app/article/[slug]/page.tsx +179 -127
  52. package/templates/nextblock-template/app/article/[slug]/page.utils.ts +262 -77
  53. package/templates/nextblock-template/app/auth/callback/route.ts +31 -58
  54. package/templates/nextblock-template/app/cart/page.tsx +7 -0
  55. package/templates/nextblock-template/app/checkout/UcpCartHydrator.tsx +20 -0
  56. package/templates/nextblock-template/app/checkout/page.tsx +52 -0
  57. package/templates/nextblock-template/app/checkout/success/actions.ts +136 -0
  58. package/templates/nextblock-template/app/checkout/success/page.tsx +186 -0
  59. package/templates/nextblock-template/app/cms/CmsClientLayout.tsx +163 -33
  60. package/templates/nextblock-template/app/cms/blocks/actions.ts +424 -235
  61. package/templates/nextblock-template/app/cms/blocks/components/BackgroundSelector.tsx +212 -151
  62. package/templates/nextblock-template/app/cms/blocks/components/BlockEditorArea.tsx +41 -20
  63. package/templates/nextblock-template/app/cms/blocks/components/BlockEditorModal.tsx +152 -19
  64. package/templates/nextblock-template/app/cms/blocks/components/BlockTypeCard.tsx +25 -17
  65. package/templates/nextblock-template/app/cms/blocks/components/BlockTypeSelector.tsx +200 -18
  66. package/templates/nextblock-template/app/cms/blocks/components/ColumnEditor.tsx +33 -16
  67. package/templates/nextblock-template/app/cms/blocks/components/CustomBlockEditorPreview.tsx +160 -0
  68. package/templates/nextblock-template/app/cms/blocks/components/EditableBlock.tsx +37 -18
  69. package/templates/nextblock-template/app/cms/blocks/components/MediaLibraryModal.tsx +149 -67
  70. package/templates/nextblock-template/app/cms/blocks/components/SectionConfigPanel.tsx +108 -31
  71. package/templates/nextblock-template/app/cms/blocks/editors/DynamicCustomBlockEditor.tsx +167 -0
  72. package/templates/nextblock-template/app/cms/blocks/editors/FeaturedProductBlockEditor.tsx +31 -0
  73. package/templates/nextblock-template/app/cms/blocks/editors/FormBlockEditor.tsx +2 -2
  74. package/templates/nextblock-template/app/cms/blocks/editors/HeadingBlockEditor.tsx +1 -1
  75. package/templates/nextblock-template/app/cms/blocks/editors/ImageBlockEditor.tsx +29 -29
  76. package/templates/nextblock-template/app/cms/blocks/editors/PostsGridBlockEditor.tsx +14 -18
  77. package/templates/nextblock-template/app/cms/blocks/editors/ProductGridBlockEditor.tsx +41 -0
  78. package/templates/nextblock-template/app/cms/blocks/editors/SectionBlockEditor.tsx +318 -118
  79. package/templates/nextblock-template/app/cms/blocks/editors/TextBlockEditor.tsx +98 -21
  80. package/templates/nextblock-template/app/cms/blocks/editors/VideoEmbedBlockEditor.tsx +1 -1
  81. package/templates/nextblock-template/app/cms/components/ContentLanguageSwitcher.tsx +27 -9
  82. package/templates/nextblock-template/app/cms/components/CopyContentFromLanguage.tsx +1 -1
  83. package/templates/nextblock-template/app/cms/components/CortexAiActiveContext.tsx +23 -0
  84. package/templates/nextblock-template/app/cms/components/CortexAiPageContext.tsx +58 -0
  85. package/templates/nextblock-template/app/cms/components/CortexGlobalAgentChat.tsx +1507 -0
  86. package/templates/nextblock-template/app/cms/components/DraftStatusActions.tsx +145 -0
  87. package/templates/nextblock-template/app/cms/components/FeatureImageField.tsx +244 -0
  88. package/templates/nextblock-template/app/cms/components/FeedbackModal.tsx +38 -24
  89. package/templates/nextblock-template/app/cms/coupons/[id]/edit/page.tsx +16 -0
  90. package/templates/nextblock-template/app/cms/coupons/page.tsx +16 -0
  91. package/templates/nextblock-template/app/cms/custom-blocks/[id]/edit/page.tsx +66 -0
  92. package/templates/nextblock-template/app/cms/custom-blocks/actions.ts +519 -0
  93. package/templates/nextblock-template/app/cms/custom-blocks/components/BlockComposer.tsx +1522 -0
  94. package/templates/nextblock-template/app/cms/custom-blocks/components/BlocksLibraryTransferControls.tsx +256 -0
  95. package/templates/nextblock-template/app/cms/custom-blocks/components/DBRelationSelect.tsx +384 -0
  96. package/templates/nextblock-template/app/cms/custom-blocks/components/ImageR2Picker.tsx +221 -0
  97. package/templates/nextblock-template/app/cms/custom-blocks/new/page.tsx +12 -0
  98. package/templates/nextblock-template/app/cms/custom-blocks/page.tsx +438 -0
  99. package/templates/nextblock-template/app/cms/dashboard/actions.ts +228 -98
  100. package/templates/nextblock-template/app/cms/dashboard/components/DashboardComponents.tsx +200 -0
  101. package/templates/nextblock-template/app/cms/dashboard/page.tsx +191 -151
  102. package/templates/nextblock-template/app/cms/import-export/ContentTransferControls.tsx +391 -0
  103. package/templates/nextblock-template/app/cms/import-export/actions.ts +226 -0
  104. package/templates/nextblock-template/app/cms/layout.tsx +29 -10
  105. package/templates/nextblock-template/app/cms/media/UploadFolderContext.tsx +22 -22
  106. package/templates/nextblock-template/app/cms/media/actions.ts +45 -124
  107. package/templates/nextblock-template/app/cms/media/components/DeleteMediaButtonClient.tsx +1 -1
  108. package/templates/nextblock-template/app/cms/media/components/MediaEditForm.tsx +26 -26
  109. package/templates/nextblock-template/app/cms/media/components/MediaGridClient.tsx +69 -64
  110. package/templates/nextblock-template/app/cms/media/components/MediaPickerDialog.tsx +227 -158
  111. package/templates/nextblock-template/app/cms/media/components/MediaUploadForm.tsx +101 -89
  112. package/templates/nextblock-template/app/cms/media/page.tsx +1 -1
  113. package/templates/nextblock-template/app/cms/navigation/components/NavigationItemForm.tsx +2 -2
  114. package/templates/nextblock-template/app/cms/orders/[id]/MarkPaidButton.tsx +44 -0
  115. package/templates/nextblock-template/app/cms/orders/[id]/page.tsx +16 -0
  116. package/templates/nextblock-template/app/cms/orders/actions.ts +201 -0
  117. package/templates/nextblock-template/app/cms/orders/page.tsx +20 -0
  118. package/templates/nextblock-template/app/cms/orders/types.ts +20 -0
  119. package/templates/nextblock-template/app/cms/pages/[id]/edit/EditPageClient.tsx +156 -121
  120. package/templates/nextblock-template/app/cms/pages/[id]/edit/page.tsx +79 -26
  121. package/templates/nextblock-template/app/cms/pages/actions.ts +54 -38
  122. package/templates/nextblock-template/app/cms/pages/components/DeletePageButtonClient.tsx +1 -1
  123. package/templates/nextblock-template/app/cms/pages/components/PageForm.tsx +267 -116
  124. package/templates/nextblock-template/app/cms/pages/page.tsx +25 -18
  125. package/templates/nextblock-template/app/cms/payments/page.tsx +16 -0
  126. package/templates/nextblock-template/app/cms/posts/[id]/edit/page.tsx +132 -90
  127. package/templates/nextblock-template/app/cms/posts/actions.ts +71 -72
  128. package/templates/nextblock-template/app/cms/posts/components/DeletePostButtonClient.tsx +1 -1
  129. package/templates/nextblock-template/app/cms/posts/components/PostForm.tsx +256 -245
  130. package/templates/nextblock-template/app/cms/posts/new/page.tsx +1 -1
  131. package/templates/nextblock-template/app/cms/posts/page.tsx +20 -13
  132. package/templates/nextblock-template/app/cms/products/ClientNotionEditor.tsx +16 -0
  133. package/templates/nextblock-template/app/cms/products/ProductFormClientShell.tsx +56 -0
  134. package/templates/nextblock-template/app/cms/products/[id]/edit/page.tsx +292 -0
  135. package/templates/nextblock-template/app/cms/products/attributes/page.tsx +12 -0
  136. package/templates/nextblock-template/app/cms/products/categories/page.tsx +12 -0
  137. package/templates/nextblock-template/app/cms/products/inventory/page.tsx +13 -0
  138. package/templates/nextblock-template/app/cms/products/new/page.tsx +143 -0
  139. package/templates/nextblock-template/app/cms/products/page.tsx +42 -0
  140. package/templates/nextblock-template/app/cms/products/productFormData.ts +133 -0
  141. package/templates/nextblock-template/app/cms/products/settings/page.tsx +5 -0
  142. package/templates/nextblock-template/app/cms/promotions/PromotionsWorkspace.tsx +456 -0
  143. package/templates/nextblock-template/app/cms/promotions/actions.ts +115 -0
  144. package/templates/nextblock-template/app/cms/promotions/page.tsx +31 -0
  145. package/templates/nextblock-template/app/cms/revisions/RevisionHistoryButton.tsx +2 -2
  146. package/templates/nextblock-template/app/cms/revisions/actions.ts +285 -285
  147. package/templates/nextblock-template/app/cms/revisions/service.ts +19 -16
  148. package/templates/nextblock-template/app/cms/revisions/utils.ts +8 -3
  149. package/templates/nextblock-template/app/cms/settings/backup-restore/BackupRestoreWorkspace.tsx +1004 -0
  150. package/templates/nextblock-template/app/cms/settings/backup-restore/page.tsx +29 -0
  151. package/templates/nextblock-template/app/cms/settings/bot-protection/actions.ts +93 -0
  152. package/templates/nextblock-template/app/cms/settings/bot-protection/components/BotProtectionForm.tsx +129 -0
  153. package/templates/nextblock-template/app/cms/settings/bot-protection/page.tsx +24 -0
  154. package/templates/nextblock-template/app/cms/settings/copyright/actions.ts +1 -1
  155. package/templates/nextblock-template/app/cms/settings/copyright/components/CopyrightForm.tsx +2 -2
  156. package/templates/nextblock-template/app/cms/settings/copyright/page.tsx +1 -1
  157. package/templates/nextblock-template/app/cms/settings/cortex-ai/SandboxCortexAiSettingsClient.tsx +496 -0
  158. package/templates/nextblock-template/app/cms/settings/cortex-ai/StoredCortexAiSettingsClient.tsx +410 -0
  159. package/templates/nextblock-template/app/cms/settings/cortex-ai/actions.ts +248 -0
  160. package/templates/nextblock-template/app/cms/settings/cortex-ai/page.tsx +80 -0
  161. package/templates/nextblock-template/app/cms/settings/currencies/actions.ts +331 -0
  162. package/templates/nextblock-template/app/cms/settings/currencies/page.tsx +494 -0
  163. package/templates/nextblock-template/app/cms/settings/extra-translations/ExtraTranslationsWorkspace.tsx +767 -0
  164. package/templates/nextblock-template/app/cms/settings/extra-translations/actions.ts +203 -44
  165. package/templates/nextblock-template/app/cms/settings/extra-translations/page.tsx +93 -242
  166. package/templates/nextblock-template/app/cms/settings/global-css/actions.ts +65 -0
  167. package/templates/nextblock-template/app/cms/settings/global-css/components/GlobalCssForm.tsx +46 -0
  168. package/templates/nextblock-template/app/cms/settings/global-css/page.tsx +24 -0
  169. package/templates/nextblock-template/app/cms/settings/languages/components/DeleteLanguageButton.tsx +1 -1
  170. package/templates/nextblock-template/app/cms/settings/languages/components/LanguageForm.tsx +2 -2
  171. package/templates/nextblock-template/app/cms/settings/languages/page.tsx +1 -1
  172. package/templates/nextblock-template/app/cms/settings/logos/[id]/edit/page.tsx +7 -7
  173. package/templates/nextblock-template/app/cms/settings/logos/actions.ts +82 -6
  174. package/templates/nextblock-template/app/cms/settings/logos/components/BrandingSettingsForm.tsx +339 -0
  175. package/templates/nextblock-template/app/cms/settings/logos/components/DeleteLogoButton.tsx +21 -18
  176. package/templates/nextblock-template/app/cms/settings/logos/components/LogoForm.tsx +20 -16
  177. package/templates/nextblock-template/app/cms/settings/logos/components/SiteSeoSettingsForm.tsx +133 -0
  178. package/templates/nextblock-template/app/cms/settings/logos/new/page.tsx +8 -8
  179. package/templates/nextblock-template/app/cms/settings/logos/page.tsx +120 -82
  180. package/templates/nextblock-template/app/cms/settings/logos/types.ts +8 -8
  181. package/templates/nextblock-template/app/cms/settings/packages/activation-form.tsx +84 -0
  182. package/templates/nextblock-template/app/cms/settings/packages/package-card.tsx +122 -0
  183. package/templates/nextblock-template/app/cms/settings/packages/page.tsx +49 -0
  184. package/templates/nextblock-template/app/cms/settings/privacy/actions.ts +53 -0
  185. package/templates/nextblock-template/app/cms/settings/privacy/components/PrivacyForm.tsx +196 -0
  186. package/templates/nextblock-template/app/cms/settings/privacy/page.tsx +26 -0
  187. package/templates/nextblock-template/app/cms/settings/security/actions.ts +251 -0
  188. package/templates/nextblock-template/app/cms/settings/security/components/SecurityPanel.tsx +453 -0
  189. package/templates/nextblock-template/app/cms/settings/security/page.tsx +13 -0
  190. package/templates/nextblock-template/app/cms/settings/taxes/page.tsx +21 -0
  191. package/templates/nextblock-template/app/cms/shipping/page.tsx +20 -0
  192. package/templates/nextblock-template/app/cms/users/[id]/edit/page.tsx +28 -23
  193. package/templates/nextblock-template/app/cms/users/actions.ts +105 -40
  194. package/templates/nextblock-template/app/cms/users/components/DeleteUserButton.tsx +1 -1
  195. package/templates/nextblock-template/app/cms/users/components/UserForm.tsx +65 -152
  196. package/templates/nextblock-template/app/cms/users/page.tsx +15 -10
  197. package/templates/nextblock-template/app/globals.css +9 -0
  198. package/templates/nextblock-template/app/layout.tsx +372 -116
  199. package/templates/nextblock-template/app/lib/seo.test.ts +52 -0
  200. package/templates/nextblock-template/app/lib/seo.ts +279 -0
  201. package/templates/nextblock-template/app/lib/site-settings.ts +87 -0
  202. package/templates/nextblock-template/app/lib/sitemap-utils.ts +224 -39
  203. package/templates/nextblock-template/app/lib/ucp/protocol.ts +190 -0
  204. package/templates/nextblock-template/app/lib/ucp/server.test.ts +56 -0
  205. package/templates/nextblock-template/app/lib/ucp/server.ts +1914 -0
  206. package/templates/nextblock-template/app/page.tsx +165 -73
  207. package/templates/nextblock-template/app/product/[slug]/page.tsx +433 -0
  208. package/templates/nextblock-template/app/profile/ProfileAccountSidebar.tsx +73 -0
  209. package/templates/nextblock-template/app/profile/ProfilePageHeader.tsx +16 -0
  210. package/templates/nextblock-template/app/profile/ProfilePageMissingState.tsx +9 -0
  211. package/templates/nextblock-template/app/profile/account-data.ts +37 -0
  212. package/templates/nextblock-template/app/profile/account-links.ts +22 -0
  213. package/templates/nextblock-template/app/profile/account-types.ts +11 -0
  214. package/templates/nextblock-template/app/profile/orders/CustomerOrdersPageClient.tsx +124 -0
  215. package/templates/nextblock-template/app/profile/orders/[id]/CustomerOrderDetailPageClient.tsx +79 -0
  216. package/templates/nextblock-template/app/profile/orders/[id]/page.tsx +32 -0
  217. package/templates/nextblock-template/app/profile/orders/page.tsx +19 -0
  218. package/templates/nextblock-template/app/profile/page.tsx +51 -0
  219. package/templates/nextblock-template/app/profile/password/PasswordSettingsPageClient.tsx +128 -0
  220. package/templates/nextblock-template/app/profile/password/actions.ts +59 -0
  221. package/templates/nextblock-template/app/profile/password/page.tsx +27 -0
  222. package/templates/nextblock-template/app/providers.tsx +55 -17
  223. package/templates/nextblock-template/app/robots.txt/route.ts +11 -1
  224. package/templates/nextblock-template/app/sitemap.ts +128 -0
  225. package/templates/nextblock-template/app/ucp/v1/carts/[id]/cancel/route.ts +38 -0
  226. package/templates/nextblock-template/app/ucp/v1/carts/[id]/route.ts +68 -0
  227. package/templates/nextblock-template/app/ucp/v1/carts/route.ts +35 -0
  228. package/templates/nextblock-template/app/ucp/v1/catalog/lookup/route.ts +35 -0
  229. package/templates/nextblock-template/app/ucp/v1/catalog/product/route.ts +35 -0
  230. package/templates/nextblock-template/app/ucp/v1/catalog/search/route.ts +34 -0
  231. package/templates/nextblock-template/components/AppShell.tsx +154 -0
  232. package/templates/nextblock-template/components/BlockRenderer.tsx +210 -64
  233. package/templates/nextblock-template/components/CartDrawerLoader.tsx +7 -0
  234. package/templates/nextblock-template/components/CartTranslator.tsx +210 -0
  235. package/templates/nextblock-template/components/CurrentContentSetter.tsx +25 -0
  236. package/templates/nextblock-template/components/DeferredCartDrawer.tsx +23 -0
  237. package/templates/nextblock-template/components/DeferredCartTranslator.tsx +51 -0
  238. package/templates/nextblock-template/components/DeferredGlobalSearch.tsx +68 -0
  239. package/templates/nextblock-template/components/DeferredGoogleTagManager.tsx +70 -0
  240. package/templates/nextblock-template/components/DeferredSpeedInsights.tsx +69 -0
  241. package/templates/nextblock-template/components/FeatureImageHero.tsx +47 -0
  242. package/templates/nextblock-template/components/GitHubLoginButton.tsx +36 -0
  243. package/templates/nextblock-template/components/GlobalSearch.tsx +557 -0
  244. package/templates/nextblock-template/components/Header.tsx +49 -41
  245. package/templates/nextblock-template/components/LanguageSwitcher.tsx +55 -32
  246. package/templates/nextblock-template/components/ResponsiveNav.tsx +138 -43
  247. package/templates/nextblock-template/components/blocks/PostCardSkeleton.tsx +12 -8
  248. package/templates/nextblock-template/components/blocks/PostsGridBlock.tsx +12 -55
  249. package/templates/nextblock-template/components/blocks/PostsGridClient.tsx +42 -37
  250. package/templates/nextblock-template/components/blocks/TestimonialBlock.tsx +6 -2
  251. package/templates/nextblock-template/components/blocks/ecommerceRendererLoaders.ts +23 -0
  252. package/templates/nextblock-template/components/blocks/publicRendererLoaders.ts +25 -0
  253. package/templates/nextblock-template/components/blocks/renderers/ButtonBlockRenderer.tsx +92 -84
  254. package/templates/nextblock-template/components/blocks/renderers/CartBlockRenderer.tsx +17 -0
  255. package/templates/nextblock-template/components/blocks/renderers/CheckoutBlockRenderer.tsx +19 -0
  256. package/templates/nextblock-template/components/blocks/renderers/ClientTextBlockRenderer.tsx +262 -8
  257. package/templates/nextblock-template/components/blocks/renderers/FeaturedProductBlockRenderer.tsx +22 -0
  258. package/templates/nextblock-template/components/blocks/renderers/FormBlockRenderer.tsx +320 -37
  259. package/templates/nextblock-template/components/blocks/renderers/HeadingBlockRenderer.tsx +11 -8
  260. package/templates/nextblock-template/components/blocks/renderers/ImageBlockRenderer.tsx +12 -3
  261. package/templates/nextblock-template/components/blocks/renderers/PostsGridBlockRenderer.tsx +18 -13
  262. package/templates/nextblock-template/components/blocks/renderers/ProductDetailsBlockRenderer.tsx +90 -0
  263. package/templates/nextblock-template/components/blocks/renderers/ProductGridBlockRenderer.tsx +31 -0
  264. package/templates/nextblock-template/components/blocks/renderers/SectionBlockRenderer.tsx +424 -55
  265. package/templates/nextblock-template/components/blocks/renderers/SectionSlider.tsx +137 -0
  266. package/templates/nextblock-template/components/blocks/renderers/TestimonialBlockRenderer.tsx +57 -0
  267. package/templates/nextblock-template/components/blocks/renderers/TextBlockRenderer.tsx +37 -22
  268. package/templates/nextblock-template/components/blocks/renderers/VideoEmbedBlockRenderer.tsx +23 -15
  269. package/templates/nextblock-template/components/blocks/renderers/inline/AlertWidgetRenderer.tsx +1 -3
  270. package/templates/nextblock-template/components/blocks/renderers/inline/CtaWidgetRenderer.tsx +1 -3
  271. package/templates/nextblock-template/components/blocks/types.ts +7 -6
  272. package/templates/nextblock-template/components/env-var-warning.tsx +3 -3
  273. package/templates/nextblock-template/components/form-message.tsx +32 -26
  274. package/templates/nextblock-template/components/header-auth.tsx +69 -17
  275. package/templates/nextblock-template/components/privacy/ConsentBanner.tsx +127 -0
  276. package/templates/nextblock-template/components/privacy/ConsentGatedAnalytics.tsx +59 -0
  277. package/templates/nextblock-template/components/renderers/CachedDynamicLayoutEngine.tsx +28 -0
  278. package/templates/nextblock-template/components/renderers/DynamicLayoutEngine.test.tsx +166 -0
  279. package/templates/nextblock-template/components/renderers/DynamicLayoutEngine.tsx +464 -0
  280. package/templates/nextblock-template/components/theme-switcher.tsx +8 -8
  281. package/templates/nextblock-template/components/visual-editing/DeferredVisualEditing.tsx +21 -0
  282. package/templates/nextblock-template/components/visual-editing/NextblockVisualEditing.tsx +1172 -0
  283. package/templates/nextblock-template/context/AuthContext.tsx +23 -90
  284. package/templates/nextblock-template/context/CurrentContentContext.tsx +10 -4
  285. package/templates/nextblock-template/context/LanguageContext.tsx +16 -16
  286. package/templates/nextblock-template/context/language-rest-client.ts +31 -0
  287. package/templates/nextblock-template/docs/01-PROJECT-OVERVIEW.md +94 -0
  288. package/templates/nextblock-template/docs/02-ECOMMERCE-CAPABILITIES.md +364 -0
  289. package/templates/nextblock-template/docs/03-CMS-AND-EDITOR.md +202 -0
  290. package/templates/nextblock-template/docs/04-DATABASE-AND-AUTH.md +252 -0
  291. package/templates/nextblock-template/docs/05-DEVELOPER-GUIDE.md +238 -0
  292. package/templates/nextblock-template/docs/06-CLI-AND-SCAFFOLDING.md +125 -0
  293. package/templates/nextblock-template/docs/07-BLOCK-SDK-AND-EXTENSIBILITY.md +146 -0
  294. package/templates/nextblock-template/docs/08-NEXTBLOCK-CORTEX-AI-ARCHITECTURE.md +1319 -0
  295. package/templates/nextblock-template/docs/09-LIVE-DRAFT-MODE.md +104 -0
  296. package/templates/nextblock-template/docs/10-CUSTOM-BLOCKS.md +222 -0
  297. package/templates/nextblock-template/docs/README.md +34 -0
  298. package/templates/nextblock-template/docs/TECHNICAL_SPECIFICATION.md +12507 -0
  299. package/templates/nextblock-template/hooks/use-hotkeys.ts +21 -14
  300. package/templates/nextblock-template/hooks/useGlobalSearch.ts +101 -0
  301. package/templates/nextblock-template/index.d.ts +2 -0
  302. package/templates/nextblock-template/lib/ai-block-generation.ts +339 -0
  303. package/templates/nextblock-template/lib/ai-client.ts +247 -0
  304. package/templates/nextblock-template/lib/ai-config.ts +81 -0
  305. package/templates/nextblock-template/lib/ai-cortex-widget-builder.ts +125 -0
  306. package/templates/nextblock-template/lib/ai-global-agent-custom-block-tools.ts +363 -0
  307. package/templates/nextblock-template/lib/ai-global-agent-db-tools.test.ts +405 -0
  308. package/templates/nextblock-template/lib/ai-global-agent-db-tools.ts +1228 -0
  309. package/templates/nextblock-template/lib/ai-global-agent-ecommerce.ts +5 -0
  310. package/templates/nextblock-template/lib/ai-global-agent-tools-stats.test.ts +223 -0
  311. package/templates/nextblock-template/lib/ai-global-agent-tools.test.ts +2183 -0
  312. package/templates/nextblock-template/lib/ai-global-agent-tools.ts +4807 -0
  313. package/templates/nextblock-template/lib/ai-key-crypto.test.ts +70 -0
  314. package/templates/nextblock-template/lib/ai-key-crypto.ts +132 -0
  315. package/templates/nextblock-template/lib/ai-model-catalog.test.ts +49 -0
  316. package/templates/nextblock-template/lib/ai-model-catalog.ts +41 -0
  317. package/templates/nextblock-template/lib/ai-model-registry.test.ts +231 -0
  318. package/templates/nextblock-template/lib/ai-model-registry.ts +522 -0
  319. package/templates/nextblock-template/lib/auth/cookies.ts +47 -0
  320. package/templates/nextblock-template/lib/auth/crypto.ts +42 -0
  321. package/templates/nextblock-template/lib/auth/trustedDevices.ts +92 -0
  322. package/templates/nextblock-template/lib/auth/twoFactor.ts +167 -0
  323. package/templates/nextblock-template/lib/auth-redirects.ts +46 -0
  324. package/templates/nextblock-template/lib/blocks/FeaturedProductBlock.tsx +94 -0
  325. package/templates/nextblock-template/lib/blocks/ProductGridBlock.tsx +137 -0
  326. package/templates/nextblock-template/lib/blocks/README.md +13 -670
  327. package/templates/nextblock-template/lib/blocks/blockRegistry.ts +138 -56
  328. package/templates/nextblock-template/lib/blocks/blockTypes.ts +18 -0
  329. package/templates/nextblock-template/lib/blocks/ecommerce-block-schemas.ts +31 -0
  330. package/templates/nextblock-template/lib/cms-transfer/csv.test.ts +77 -0
  331. package/templates/nextblock-template/lib/cms-transfer/csv.ts +399 -0
  332. package/templates/nextblock-template/lib/cms-transfer/server.ts +2243 -0
  333. package/templates/nextblock-template/lib/cms-transfer/types.ts +145 -0
  334. package/templates/nextblock-template/lib/cortex-widget-registry.test.ts +199 -0
  335. package/templates/nextblock-template/lib/cortex-widget-registry.ts +88 -0
  336. package/templates/nextblock-template/lib/cortex-widget-schema.test.tsx +237 -0
  337. package/templates/nextblock-template/lib/cortex-widget-schema.ts +393 -0
  338. package/templates/nextblock-template/lib/custom-block-definitions.ts +87 -0
  339. package/templates/nextblock-template/lib/custom-block-r2-upload-shared.ts +178 -0
  340. package/templates/nextblock-template/lib/custom-block-r2-upload.test.ts +140 -0
  341. package/templates/nextblock-template/lib/custom-block-r2-upload.ts +68 -0
  342. package/templates/nextblock-template/lib/custom-block-relation-registry.ts +256 -0
  343. package/templates/nextblock-template/lib/custom-block-relations.test.ts +227 -0
  344. package/templates/nextblock-template/lib/custom-block-relations.ts +279 -0
  345. package/templates/nextblock-template/lib/custom-block-safelist.ts +14 -0
  346. package/templates/nextblock-template/lib/editor/dynamic-extension-core.test.ts +172 -0
  347. package/templates/nextblock-template/lib/editor/dynamic-extension-core.ts +213 -0
  348. package/templates/nextblock-template/lib/editor/dynamic-extension-loader.ts +22 -0
  349. package/templates/nextblock-template/lib/editor/dynamic-extensions.tsx +193 -0
  350. package/templates/nextblock-template/lib/full-backup/manifest.test.ts +121 -0
  351. package/templates/nextblock-template/lib/full-backup/manifest.ts +206 -0
  352. package/templates/nextblock-template/lib/full-backup/server.ts +743 -0
  353. package/templates/nextblock-template/lib/media/resolveMediaUrl.ts +45 -0
  354. package/templates/nextblock-template/lib/posts/readTime.ts +60 -0
  355. package/templates/nextblock-template/lib/privacy/consent-client.ts +57 -0
  356. package/templates/nextblock-template/lib/privacy/settings.ts +103 -0
  357. package/templates/nextblock-template/lib/privacy/types.ts +67 -0
  358. package/templates/nextblock-template/lib/promotions/server.test.ts +74 -0
  359. package/templates/nextblock-template/lib/promotions/server.ts +741 -0
  360. package/templates/nextblock-template/lib/resolve-block-relations.test.ts +142 -0
  361. package/templates/nextblock-template/lib/resolve-block-relations.ts +255 -0
  362. package/templates/nextblock-template/lib/search/server.ts +585 -0
  363. package/templates/nextblock-template/lib/search/types.ts +27 -0
  364. package/templates/nextblock-template/lib/visual-editing/draft-content.test.ts +105 -0
  365. package/templates/nextblock-template/lib/visual-editing/draft-content.ts +380 -0
  366. package/templates/nextblock-template/lib/visual-editing/draft-route.test.ts +42 -0
  367. package/templates/nextblock-template/lib/visual-editing/draft-route.ts +82 -0
  368. package/templates/nextblock-template/lib/visual-editing/edit-info.test.ts +143 -0
  369. package/templates/nextblock-template/lib/visual-editing/edit-info.ts +94 -0
  370. package/templates/nextblock-template/lib/visual-editing/mutations.ts +190 -0
  371. package/templates/nextblock-template/lib/visual-editing/product-drafts.test.ts +81 -0
  372. package/templates/nextblock-template/lib/visual-editing/product-drafts.ts +511 -0
  373. package/templates/nextblock-template/lib/visual-editing/types.ts +122 -0
  374. package/templates/nextblock-template/lib/zod-config.ts +5 -0
  375. package/templates/nextblock-template/next.config.js +190 -66
  376. package/templates/nextblock-template/package.json +34 -30
  377. package/templates/nextblock-template/proxy.ts +435 -253
  378. package/templates/nextblock-template/public/images/NBcover.webp +0 -0
  379. package/templates/nextblock-template/public/images/cap.webp +0 -0
  380. package/templates/nextblock-template/public/images/commerce-plan.webp +0 -0
  381. package/templates/nextblock-template/public/images/commerce-square.webp +0 -0
  382. package/templates/nextblock-template/public/images/commerce-wide.webp +0 -0
  383. package/templates/nextblock-template/public/images/cortex-ai-square.webp +0 -0
  384. package/templates/nextblock-template/public/images/cortex-ai.webp +0 -0
  385. package/templates/nextblock-template/public/images/extensibility.webp +0 -0
  386. package/templates/nextblock-template/public/images/goals.webp +0 -0
  387. package/templates/nextblock-template/public/images/included.webp +0 -0
  388. package/templates/nextblock-template/public/images/nx-graph.webp +0 -0
  389. package/templates/nextblock-template/public/images/pants.webp +0 -0
  390. package/templates/nextblock-template/public/images/t-shirt.webp +0 -0
  391. package/templates/nextblock-template/scripts/validate-editor-block-schema.ts +112 -0
  392. package/templates/nextblock-template/scripts/verify-cortex-ai-build-widget.tsx +100 -0
  393. package/templates/nextblock-template/scripts/verify-cortex-ai-generate-blocks.ts +62 -0
  394. package/templates/nextblock-template/scripts/verify-cortex-ai-global-tools.ts +537 -0
  395. package/templates/nextblock-template/scripts/verify-cortex-ai-routing.ts +58 -0
  396. package/templates/nextblock-template/scripts/verify-custom-block-definitions.ts +188 -0
  397. package/templates/nextblock-template/scripts/verify-dynamic-custom-block-extensions.ts +123 -0
  398. package/templates/nextblock-template/scripts/verify-dynamic-layout-engine.tsx +133 -0
  399. package/templates/nextblock-template/scripts/verify-milestone-2-custom-blocks.ts +65 -0
  400. package/templates/nextblock-template/tailwind.config.js +1 -0
  401. package/templates/nextblock-template/tools/configure-supabase-auth.js +282 -0
  402. package/templates/nextblock-template/tools/deploy-supabase.js +69 -71
  403. package/templates/nextblock-template/tsconfig.json +52 -66
  404. package/templates/nextblock-template/tsconfig.tsbuildinfo +1 -1
  405. package/templates/nextblock-template/types/jsdom.d.ts +6 -0
  406. package/templates/nextblock-template/app/force-styles.tsx +0 -31
  407. package/templates/nextblock-template/app/sitemap.xml/route.ts +0 -63
  408. package/templates/nextblock-template/components/blocks/renderers/HeroBlockRenderer.tsx +0 -273
  409. package/templates/nextblock-template/docs/How to Create a Custom Block.md +0 -149
  410. package/templates/nextblock-template/docs/cms-application-overview.md +0 -56
  411. package/templates/nextblock-template/docs/cms-architecture-overview.md +0 -73
  412. package/templates/nextblock-template/docs/files-structure.md +0 -426
  413. package/templates/nextblock-template/docs/tiptap-bundle-optimization-summary.md +0 -174
@@ -0,0 +1,585 @@
1
+ import 'server-only';
2
+
3
+ import { getSsgSupabaseClient, verifyPackageOnline } from '@nextblock-cms/db/server';
4
+ import { resolveMediaUrl } from '../media/resolveMediaUrl';
5
+ import type {
6
+ GlobalSearchFilter,
7
+ GlobalSearchResponse,
8
+ GlobalSearchResult,
9
+ GlobalSearchResultType,
10
+ } from './types';
11
+
12
+ const CANDIDATE_LIMIT = 180;
13
+ const DEFAULT_RESULT_LIMIT = 12;
14
+ const MAX_RESULT_LIMIT = 50;
15
+ const MAX_QUERY_LENGTH = 96;
16
+
17
+ const HUMAN_TEXT_KEYS = new Set([
18
+ 'alt_text',
19
+ 'author_name',
20
+ 'author_title',
21
+ 'caption',
22
+ 'html_content',
23
+ 'label',
24
+ 'placeholder',
25
+ 'quote',
26
+ 'submit_button_text',
27
+ 'success_message',
28
+ 'text',
29
+ 'text_content',
30
+ 'title',
31
+ ]);
32
+
33
+ const HUMAN_CONTAINER_KEYS = new Set([
34
+ 'blocks',
35
+ 'children',
36
+ 'column_blocks',
37
+ 'content',
38
+ 'fields',
39
+ 'items',
40
+ 'options',
41
+ ]);
42
+
43
+ const PRESENTATION_KEYS = new Set([
44
+ 'attrs',
45
+ 'background',
46
+ 'block_type',
47
+ 'blur_data_url',
48
+ 'categoryId',
49
+ 'class',
50
+ 'className',
51
+ 'color',
52
+ 'column_gap',
53
+ 'container_type',
54
+ 'desktop',
55
+ 'direction',
56
+ 'file_path',
57
+ 'gradient',
58
+ 'height',
59
+ 'href',
60
+ 'icon',
61
+ 'id',
62
+ 'image',
63
+ 'imagePosition',
64
+ 'image_url',
65
+ 'limit',
66
+ 'media',
67
+ 'media_id',
68
+ 'metadata',
69
+ 'mobile',
70
+ 'object_key',
71
+ 'overlay',
72
+ 'padding',
73
+ 'position',
74
+ 'productId',
75
+ 'quality',
76
+ 'responsive_columns',
77
+ 'showBackground',
78
+ 'showPagination',
79
+ 'size',
80
+ 'solid_color',
81
+ 'src',
82
+ 'style',
83
+ 'tablet',
84
+ 'theme',
85
+ 'type',
86
+ 'url',
87
+ 'value',
88
+ 'variant',
89
+ 'vertical_alignment',
90
+ 'width',
91
+ ]);
92
+
93
+ type SearchCandidate = Omit<GlobalSearchResult, 'score' | 'snippet'> & {
94
+ bodyText: string;
95
+ searchableText: string;
96
+ sortDate: string | null;
97
+ };
98
+
99
+ type PreparedQuery = {
100
+ display: string;
101
+ normalized: string;
102
+ tokens: string[];
103
+ };
104
+
105
+ function sanitizeSearchQuery(value: string | null | undefined) {
106
+ return (value || '').replace(/\s+/g, ' ').trim().slice(0, MAX_QUERY_LENGTH);
107
+ }
108
+
109
+ function normalizeForSearch(value: string | null | undefined) {
110
+ return (value || '')
111
+ .normalize('NFD')
112
+ .replace(/[\u0300-\u036f]/g, '')
113
+ .toLowerCase();
114
+ }
115
+
116
+ function prepareQuery(rawQuery: string): PreparedQuery | null {
117
+ const display = sanitizeSearchQuery(rawQuery);
118
+
119
+ if (display.length < 2) {
120
+ return null;
121
+ }
122
+
123
+ const normalized = normalizeForSearch(display);
124
+ const tokens = Array.from(
125
+ new Set(normalized.split(/[^a-z0-9]+/).filter((token) => token.length >= 2))
126
+ ).slice(0, 8);
127
+
128
+ if (tokens.length === 0) {
129
+ return null;
130
+ }
131
+
132
+ return { display, normalized, tokens };
133
+ }
134
+
135
+ function stripMarkup(value: string) {
136
+ return value
137
+ .replace(/<script[\s\S]*?<\/script>/gi, ' ')
138
+ .replace(/<style[\s\S]*?<\/style>/gi, ' ')
139
+ .replace(/<[^>]+>/g, ' ')
140
+ .replace(/&nbsp;/g, ' ')
141
+ .replace(/&amp;/g, '&')
142
+ .replace(/&lt;/g, '<')
143
+ .replace(/&gt;/g, '>')
144
+ .replace(/&quot;/g, '"')
145
+ .replace(/&#39;/g, "'")
146
+ .replace(/\s+/g, ' ')
147
+ .trim();
148
+ }
149
+
150
+ function compactText(parts: Array<string | null | undefined>) {
151
+ return parts
152
+ .map((part) => stripMarkup(part || ''))
153
+ .filter(Boolean)
154
+ .join(' ')
155
+ .replace(/\s+/g, ' ')
156
+ .trim();
157
+ }
158
+
159
+ function collectHumanText(value: unknown, depth = 0, parentKey?: string): string[] {
160
+ if (depth > 6 || value == null) {
161
+ return [];
162
+ }
163
+
164
+ if (typeof value === 'string') {
165
+ return parentKey && HUMAN_TEXT_KEYS.has(parentKey) ? [stripMarkup(value)] : [];
166
+ }
167
+
168
+ if (typeof value === 'number' || typeof value === 'boolean') {
169
+ return [];
170
+ }
171
+
172
+ if (Array.isArray(value)) {
173
+ return value.flatMap((item) => collectHumanText(item, depth + 1, parentKey));
174
+ }
175
+
176
+ if (typeof value === 'object') {
177
+ return Object.entries(value as Record<string, unknown>).flatMap(([key, nestedValue]) => {
178
+ if (PRESENTATION_KEYS.has(key)) {
179
+ return [];
180
+ }
181
+
182
+ if (HUMAN_TEXT_KEYS.has(key) || HUMAN_CONTAINER_KEYS.has(key)) {
183
+ return collectHumanText(nestedValue, depth + 1, key);
184
+ }
185
+
186
+ return [];
187
+ });
188
+ }
189
+
190
+ return [];
191
+ }
192
+
193
+ function getFirstRelation<T>(value: T | T[] | null | undefined): T | null {
194
+ if (Array.isArray(value)) {
195
+ return value[0] ?? null;
196
+ }
197
+
198
+ return value ?? null;
199
+ }
200
+
201
+ function getLanguageCode(row: { languages?: unknown }) {
202
+ const language = getFirstRelation(row.languages as { code?: string } | { code?: string }[] | null);
203
+ return language?.code ?? null;
204
+ }
205
+
206
+ function buildBodyFromBlocks(blocks: unknown) {
207
+ if (!Array.isArray(blocks)) {
208
+ return '';
209
+ }
210
+
211
+ return compactText(
212
+ blocks.flatMap((block) => {
213
+ const content = (block as { content?: unknown }).content;
214
+ return collectHumanText(content);
215
+ })
216
+ ).slice(0, 5000);
217
+ }
218
+
219
+ function getProductImageUrl(productMedia: unknown) {
220
+ if (!Array.isArray(productMedia)) {
221
+ return null;
222
+ }
223
+
224
+ const sorted = [...productMedia].sort((a, b) => {
225
+ const left = (a as { sort_order?: number | null }).sort_order ?? 0;
226
+ const right = (b as { sort_order?: number | null }).sort_order ?? 0;
227
+ return left - right;
228
+ });
229
+ const first = sorted[0] as { media?: { object_key?: string | null; file_path?: string | null } | null } | undefined;
230
+ const media = getFirstRelation(first?.media ?? null);
231
+
232
+ return resolveMediaUrl(media?.object_key || media?.file_path || null);
233
+ }
234
+
235
+ function candidateBaseScore(candidate: SearchCandidate, prepared: PreparedQuery) {
236
+ const title = normalizeForSearch(candidate.title);
237
+ const description = normalizeForSearch(candidate.description);
238
+ const body = normalizeForSearch(candidate.bodyText);
239
+ const href = normalizeForSearch(candidate.href);
240
+ const label = normalizeForSearch(candidate.meta.label);
241
+ const sku = normalizeForSearch(candidate.meta.sku);
242
+ const all = normalizeForSearch(candidate.searchableText);
243
+
244
+ if (!prepared.tokens.some((token) => all.includes(token))) {
245
+ return 0;
246
+ }
247
+
248
+ let score = 0;
249
+
250
+ if (title === prepared.normalized) score += 120;
251
+ if (title.startsWith(prepared.normalized)) score += 70;
252
+ if (title.includes(prepared.normalized)) score += 55;
253
+ if (href.includes(prepared.normalized)) score += 30;
254
+ if (description.includes(prepared.normalized)) score += 24;
255
+ if (body.includes(prepared.normalized)) score += 14;
256
+ if (sku && sku.includes(prepared.normalized)) score += 80;
257
+
258
+ for (const token of prepared.tokens) {
259
+ if (title.includes(token)) score += 28;
260
+ if (href.includes(token)) score += 18;
261
+ if (description.includes(token)) score += 14;
262
+ if (label.includes(token)) score += 12;
263
+ if (sku.includes(token)) score += 36;
264
+ if (body.includes(token)) score += 6;
265
+ }
266
+
267
+ if (candidate.type === 'page') score += 4;
268
+ if (candidate.type === 'product' && candidate.meta.sku) score += 4;
269
+
270
+ return score;
271
+ }
272
+
273
+ function buildSnippet(candidate: SearchCandidate, prepared: PreparedQuery) {
274
+ const source = compactText([candidate.description, candidate.bodyText]);
275
+
276
+ if (!source) {
277
+ return candidate.description || null;
278
+ }
279
+
280
+ const normalizedSource = normalizeForSearch(source);
281
+ let index = normalizedSource.indexOf(prepared.normalized);
282
+
283
+ if (index < 0) {
284
+ const matchedToken = prepared.tokens.find((token) => normalizedSource.includes(token));
285
+ index = matchedToken ? normalizedSource.indexOf(matchedToken) : 0;
286
+ }
287
+
288
+ const radius = 92;
289
+ const start = Math.max(0, index - radius);
290
+ const end = Math.min(source.length, index + radius);
291
+ const snippet = source.slice(start, end).replace(/\s+/g, ' ').trim();
292
+
293
+ return `${start > 0 ? '... ' : ''}${snippet}${end < source.length ? ' ...' : ''}`;
294
+ }
295
+
296
+ function scoreCandidates(candidates: SearchCandidate[], prepared: PreparedQuery) {
297
+ return candidates
298
+ .map((candidate) => ({
299
+ ...candidate,
300
+ score: candidateBaseScore(candidate, prepared),
301
+ snippet: buildSnippet(candidate, prepared),
302
+ }))
303
+ .filter((candidate) => candidate.score > 0)
304
+ .sort((left, right) => {
305
+ if (right.score !== left.score) {
306
+ return right.score - left.score;
307
+ }
308
+
309
+ return new Date(right.sortDate || 0).getTime() - new Date(left.sortDate || 0).getTime();
310
+ });
311
+ }
312
+
313
+ async function getLanguageId(locale: string | null) {
314
+ const normalizedLocale = sanitizeSearchQuery(locale).toLowerCase();
315
+
316
+ if (!normalizedLocale) {
317
+ return null;
318
+ }
319
+
320
+ const supabase = getSsgSupabaseClient();
321
+ const { data } = await supabase
322
+ .from('languages')
323
+ .select('id')
324
+ .eq('code', normalizedLocale)
325
+ .maybeSingle();
326
+
327
+ return typeof data?.id === 'number' ? data.id : null;
328
+ }
329
+
330
+ async function fetchPages(languageId: number | null): Promise<SearchCandidate[]> {
331
+ const supabase = getSsgSupabaseClient();
332
+ let query = supabase
333
+ .from('pages')
334
+ .select(
335
+ `
336
+ id,
337
+ title,
338
+ slug,
339
+ meta_title,
340
+ meta_description,
341
+ updated_at,
342
+ language_id,
343
+ languages!inner(code),
344
+ media:feature_image_id(object_key, blur_data_url, width, height),
345
+ blocks(content, block_type, order)
346
+ `
347
+ )
348
+ .eq('status', 'published')
349
+ .order('updated_at', { ascending: false })
350
+ .limit(CANDIDATE_LIMIT);
351
+
352
+ if (languageId) {
353
+ query = query.eq('language_id', languageId);
354
+ }
355
+
356
+ const { data, error } = await query;
357
+
358
+ if (error || !data) {
359
+ if (error) console.error('Global search page query failed:', error.message);
360
+ return [];
361
+ }
362
+
363
+ return data.map((page: any) => {
364
+ const bodyText = buildBodyFromBlocks(page.blocks);
365
+ const description = page.meta_description || null;
366
+ const href = page.slug === 'home' || page.slug === 'accueil' ? '/' : `/${page.slug}`;
367
+ const media = getFirstRelation(page.media as { object_key?: string | null } | { object_key?: string | null }[] | null);
368
+
369
+ return {
370
+ id: String(page.id),
371
+ type: 'page' as const,
372
+ title: page.meta_title || page.title,
373
+ description,
374
+ href,
375
+ locale: getLanguageCode(page),
376
+ imageUrl: resolveMediaUrl(media?.object_key || null),
377
+ bodyText,
378
+ searchableText: compactText([page.title, page.meta_title, description, page.slug, bodyText]),
379
+ sortDate: page.updated_at ?? null,
380
+ meta: {
381
+ updatedAt: page.updated_at ?? null,
382
+ },
383
+ };
384
+ });
385
+ }
386
+
387
+ async function fetchPosts(languageId: number | null): Promise<SearchCandidate[]> {
388
+ const supabase = getSsgSupabaseClient();
389
+ const now = new Date().toISOString();
390
+ let query = supabase
391
+ .from('posts')
392
+ .select(
393
+ `
394
+ id,
395
+ title,
396
+ slug,
397
+ label,
398
+ excerpt,
399
+ subtitle,
400
+ meta_title,
401
+ meta_description,
402
+ published_at,
403
+ updated_at,
404
+ language_id,
405
+ languages!inner(code),
406
+ media:feature_image_id(object_key, blur_data_url, width, height),
407
+ blocks(content, block_type, order)
408
+ `
409
+ )
410
+ .eq('status', 'published')
411
+ .or(`published_at.is.null,published_at.lte.${now}`)
412
+ .order('published_at', { ascending: false, nullsFirst: false })
413
+ .limit(CANDIDATE_LIMIT);
414
+
415
+ if (languageId) {
416
+ query = query.eq('language_id', languageId);
417
+ }
418
+
419
+ const { data, error } = await query;
420
+
421
+ if (error || !data) {
422
+ if (error) console.error('Global search post query failed:', error.message);
423
+ return [];
424
+ }
425
+
426
+ return data.map((post: any) => {
427
+ const bodyText = buildBodyFromBlocks(post.blocks);
428
+ const description = post.meta_description || post.excerpt || post.subtitle || null;
429
+ const media = getFirstRelation(post.media as { object_key?: string | null } | { object_key?: string | null }[] | null);
430
+
431
+ return {
432
+ id: String(post.id),
433
+ type: 'post' as const,
434
+ title: post.meta_title || post.title,
435
+ description,
436
+ href: `/article/${post.slug}`,
437
+ locale: getLanguageCode(post),
438
+ imageUrl: resolveMediaUrl(media?.object_key || null),
439
+ bodyText,
440
+ searchableText: compactText([
441
+ post.title,
442
+ post.meta_title,
443
+ description,
444
+ post.label,
445
+ post.slug,
446
+ bodyText,
447
+ ]),
448
+ sortDate: post.published_at || post.updated_at || null,
449
+ meta: {
450
+ label: post.label ?? null,
451
+ publishedAt: post.published_at ?? null,
452
+ updatedAt: post.updated_at ?? null,
453
+ },
454
+ };
455
+ });
456
+ }
457
+
458
+ async function fetchProducts(languageId: number | null): Promise<SearchCandidate[]> {
459
+ const supabase = getSsgSupabaseClient();
460
+ let query = supabase
461
+ .from('products')
462
+ .select(
463
+ `
464
+ id,
465
+ title,
466
+ slug,
467
+ sku,
468
+ short_description,
469
+ meta_title,
470
+ meta_description,
471
+ updated_at,
472
+ created_at,
473
+ language_id,
474
+ languages!inner(code),
475
+ product_media(sort_order, media(object_key, file_path, blur_data_url, width, height)),
476
+ blocks(content, block_type, order)
477
+ `
478
+ )
479
+ .eq('status', 'active')
480
+ .order('updated_at', { ascending: false })
481
+ .limit(CANDIDATE_LIMIT);
482
+
483
+ if (languageId) {
484
+ query = query.eq('language_id', languageId);
485
+ }
486
+
487
+ const { data, error } = await query;
488
+
489
+ if (error || !data) {
490
+ if (error) console.error('Global search product query failed:', error.message);
491
+ return [];
492
+ }
493
+
494
+ return data.map((product: any) => {
495
+ const bodyText = buildBodyFromBlocks(product.blocks);
496
+ const description = product.meta_description || product.short_description || null;
497
+
498
+ return {
499
+ id: String(product.id),
500
+ type: 'product' as const,
501
+ title: product.meta_title || product.title,
502
+ description,
503
+ href: `/product/${product.slug}`,
504
+ locale: getLanguageCode(product),
505
+ imageUrl: getProductImageUrl(product.product_media),
506
+ bodyText,
507
+ searchableText: compactText([
508
+ product.title,
509
+ product.meta_title,
510
+ description,
511
+ product.sku,
512
+ product.slug,
513
+ bodyText,
514
+ ]),
515
+ sortDate: product.updated_at || product.created_at || null,
516
+ meta: {
517
+ sku: product.sku ?? null,
518
+ updatedAt: product.updated_at ?? null,
519
+ },
520
+ };
521
+ });
522
+ }
523
+
524
+ function shouldSearchType(filter: GlobalSearchFilter, type: GlobalSearchResultType) {
525
+ return filter === 'all' || filter === type;
526
+ }
527
+
528
+ export async function searchPublicContent({
529
+ query,
530
+ locale,
531
+ filter = 'all',
532
+ limit = DEFAULT_RESULT_LIMIT,
533
+ }: {
534
+ query: string;
535
+ locale?: string | null;
536
+ filter?: GlobalSearchFilter;
537
+ limit?: number;
538
+ }): Promise<GlobalSearchResponse> {
539
+ const prepared = prepareQuery(query);
540
+
541
+ if (!prepared) {
542
+ return {
543
+ query: sanitizeSearchQuery(query),
544
+ results: [],
545
+ counts: { all: 0, page: 0, post: 0, product: 0 },
546
+ };
547
+ }
548
+
549
+ const resultLimit = Math.min(Math.max(limit || DEFAULT_RESULT_LIMIT, 1), MAX_RESULT_LIMIT);
550
+ const languageId = await getLanguageId(locale ?? null);
551
+ const ecommerceActive = shouldSearchType(filter, 'product')
552
+ ? await verifyPackageOnline('ecommerce').catch(() => false)
553
+ : false;
554
+
555
+ const [pages, posts, products] = await Promise.all([
556
+ shouldSearchType(filter, 'page') ? fetchPages(languageId) : Promise.resolve([]),
557
+ shouldSearchType(filter, 'post') ? fetchPosts(languageId) : Promise.resolve([]),
558
+ shouldSearchType(filter, 'product') && ecommerceActive
559
+ ? fetchProducts(languageId)
560
+ : Promise.resolve([]),
561
+ ]);
562
+
563
+ const scoredResults = scoreCandidates([...pages, ...posts, ...products], prepared);
564
+ const counts = scoredResults.reduce<Record<GlobalSearchResultType | 'all', number>>(
565
+ (accumulator, result) => {
566
+ accumulator[result.type] += 1;
567
+ accumulator.all += 1;
568
+ return accumulator;
569
+ },
570
+ { all: 0, page: 0, post: 0, product: 0 }
571
+ );
572
+
573
+ return {
574
+ query: prepared.display,
575
+ results: scoredResults
576
+ .slice(0, resultLimit)
577
+ .map(({ bodyText, searchableText, sortDate, ...result }) => {
578
+ void bodyText;
579
+ void searchableText;
580
+ void sortDate;
581
+ return result;
582
+ }),
583
+ counts,
584
+ };
585
+ }
@@ -0,0 +1,27 @@
1
+ export type GlobalSearchResultType = 'page' | 'post' | 'product';
2
+
3
+ export type GlobalSearchFilter = GlobalSearchResultType | 'all';
4
+
5
+ export interface GlobalSearchResult {
6
+ id: string;
7
+ type: GlobalSearchResultType;
8
+ title: string;
9
+ description: string | null;
10
+ snippet: string | null;
11
+ href: string;
12
+ locale: string | null;
13
+ imageUrl: string | null;
14
+ score: number;
15
+ meta: {
16
+ label?: string | null;
17
+ sku?: string | null;
18
+ publishedAt?: string | null;
19
+ updatedAt?: string | null;
20
+ };
21
+ }
22
+
23
+ export interface GlobalSearchResponse {
24
+ query: string;
25
+ results: GlobalSearchResult[];
26
+ counts: Record<GlobalSearchResultType | 'all', number>;
27
+ }
@@ -0,0 +1,105 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+
3
+ vi.mock("server-only", () => ({}));
4
+ vi.mock("@nextblock-cms/db/server", () => ({
5
+ createClient: vi.fn(),
6
+ getServiceRoleSupabaseClient: vi.fn(),
7
+ getSsgSupabaseClient: vi.fn(),
8
+ }));
9
+
10
+ import {
11
+ findDraftBlock,
12
+ updateDraftBlockContent,
13
+ } from "./draft-content";
14
+ import type { ContentDraftSnapshot } from "./types";
15
+
16
+ describe("draft content merge helpers", () => {
17
+ it("updates top-level draft block content without mutating the original snapshot", () => {
18
+ const snapshot: ContentDraftSnapshot = {
19
+ meta: {},
20
+ blocks: [
21
+ {
22
+ id: 10,
23
+ page_id: 1,
24
+ post_id: null,
25
+ language_id: 1,
26
+ block_type: "text",
27
+ content: { html_content: "<p>Old</p>" },
28
+ order: 0,
29
+ },
30
+ ],
31
+ };
32
+
33
+ const next = updateDraftBlockContent(
34
+ snapshot,
35
+ {
36
+ parentType: "page",
37
+ parentId: 1,
38
+ target: {
39
+ kind: "top-level",
40
+ blockId: 10,
41
+ blockIndex: 0,
42
+ blockType: "text",
43
+ },
44
+ },
45
+ { html_content: "<p>New</p>" }
46
+ );
47
+
48
+ expect(snapshot.blocks[0].content).toEqual({ html_content: "<p>Old</p>" });
49
+ expect(next.blocks[0].content).toEqual({ html_content: "<p>New</p>" });
50
+ });
51
+
52
+ it("finds and updates nested draft block content", () => {
53
+ const snapshot: ContentDraftSnapshot = {
54
+ meta: {},
55
+ blocks: [
56
+ {
57
+ id: 20,
58
+ page_id: 1,
59
+ post_id: null,
60
+ language_id: 1,
61
+ block_type: "section",
62
+ content: {
63
+ column_blocks: [
64
+ [
65
+ {
66
+ block_type: "heading",
67
+ content: { text_content: "Old heading", level: 2 },
68
+ },
69
+ ],
70
+ ],
71
+ },
72
+ order: 0,
73
+ },
74
+ ],
75
+ };
76
+ const request = {
77
+ parentType: "page" as const,
78
+ parentId: 1,
79
+ target: {
80
+ kind: "nested" as const,
81
+ parentBlockId: 20,
82
+ parentBlockIndex: 0,
83
+ parentBlockType: "section" as const,
84
+ columnIndex: 0,
85
+ blockIndex: 0,
86
+ blockType: "heading",
87
+ },
88
+ };
89
+
90
+ expect(findDraftBlock(snapshot, request)).toMatchObject({
91
+ block_type: "heading",
92
+ content: { text_content: "Old heading", level: 2 },
93
+ });
94
+
95
+ const next = updateDraftBlockContent(snapshot, request, {
96
+ text_content: "New heading",
97
+ level: 2,
98
+ });
99
+
100
+ expect((next.blocks[0].content as any).column_blocks[0][0].content).toEqual({
101
+ text_content: "New heading",
102
+ level: 2,
103
+ });
104
+ });
105
+ });